集合体系
- Collection(单列)
- List:ArrrayList, LinkedList, Vector
- Set:HashSet, LinkedHashSet, TreeSet
- Map(双列)
- HashMap, Hashtable, LinkedHashMap, TreeMap, Properties
总结:
- ArrayList的实现底层是一个数组;当有需要扩容的时候,调用Array.copyOf()方法来创建更长的数组和复制数组中的内容
- LinkedList实现的基础是内部类Node,Node的成员属性定义了item指向List中的元素,而成员属性next和prev则指向了链表的下一个和上一个元素;LinkedList中还有成员属性first和last指向链表中的第一个和最后一个元素
- HashSet实现的底层是HashMap;HashMap的原理是数组和(单向)链表,HashMap内部定义了一个Node数组来放载元素,其实现的基础内部类Node继承了Map.Entry
,除了成员属性key和value,Node还定义了hash来存储元素的hash值,和Node next用于指向链表的下一个元素 - LinkedHashSet实现的底层是LinkedHashMap;LinkedHashMap继承了HashMap,实现的原理跟HashMap大同小异,不过与HashMap的区别在于,数组内使用的是LinkedHashMap.Entry来记录数据;LinkedHashMap.Entry继承了HashMap.Node,不过除了HashMap.Node原有的成员属性,LinkedHashMap.Entry多增了before和after来指向双向链表的上一个和下一个;同时LinkedHashMap中还重新了newNode方法,每次向Map中新增元素的时候都会通过newNode来更新链表的last,以及末尾元素的prev和next
- TreeSet实现的底层是TreeMap,TreeMap中使用TreeMap.Entry来放载数据,除了key和value,Entry还定义了left和right指向了元素左边和右边的元素;添加元素的时候,会使用comparator的compare()方法或者元素本身的compareTo()来决定元素的顺序;除此之外,TreeMap中还定义了root成员变量来指向所有元素的根,root会随着新元素的加入而改变
实战中集合选择原则
- 根据存储的数据类型:单列 / 双列
- 单列:存储的数据是否允许重复?平时的操作是增删多一点还是改查多一点?对存储数据的排序是否有要求?如果有,是要求元素按一定的规则排序?还是要求插入和取出的顺序一致?
- 双列:对键的排序是否有要求?如果有,是要求键按一定的规则排序?还是要求插入和取出的顺序一致?如果是用于读取文件中的键值对 -> Properties 常用方法
Collections工具类中的一些常用方法(均为静态方法)
- 虽然工具类的名字叫做Collections,不过还是有些工具方法是针对Map类对象定义的
- reverse( List ) - 反转List中元素的顺序
- shuffle( List ) - 对List集合中的元素进行随机排序
- sort( List ) - 根据元素的自然顺序对指定List集合中的元素进行排序
- sort( List, Comparator ) - 根据指定的Comparator对List集合中的元素进行排序
- 其底层还是如果有Comparator的话就会根据重写的compare()来进行比较,不然的话就是元素本身的compareTo()来比较
- swap( List, int, int ) - 将List集合中的指定位置的两个元素进行交换
- copy( List dest, List src ) - 将src集合中的元素拷贝至dest集合中;不过在拷贝之前会先对两者的size进行比较,如果dest的size小于src的size,那么就会抛出异常IndexOutOfBoundsException
- 注意是比较size,而不是比较elementData的capacity
Note
- ArrayList每次扩容的倍数是1.5,HashMap是2倍,而TreeMap则是扩容到2倍加1
Collection接口和常用方法:
- add() 添加指定元素;remove() 删除指定元素;contains() 查找元素是否存在;size() 获取元素个数;isEmpty() 判断是否为空;clear() 清空;addAll() 添加多个元素;containsAll() 查找多个元素是否都存在;removeAll() 删除多个元素
- iterator()
- next();hasNext();remove()
- 当iterator迭代器指向最后一个元素时,再次调用next()将会抛出NoSuchElement的异常;可以通过重新赋值iterator的方法来重置指针
- 增强for的底层也还是迭代器
List接口和常用方法
- List接口
- List集合类中的元素有序,添加和取出的顺序一致,而且可以重复
- List集合中的每个元素都有其对应的顺序索引
- List接口的常用方法
- add(int index, Object ele) 在index位置插入元素ele;addAll(int index, Collection eles) 在index位置插入集合eles;get() 获取指定位置的元素;indexOf() 返回某个元素在集合中第一次出现的位置;lastIndexOf() 返回某个元素在集合中最后一次出现的位置;remove() 移除指定index位置的元素,并返回该元素;set(int index, Object ele) 设置指定index位置的元素为ele,相当于是替换;subList(int fromIndex, int toIndex) 返回从fromIndex到toIndex位置的子集合
- 因为List是有序的,所以add()&addAll()方法可以指定插入的位置,而不像Collection中的那样只能在末位 插入;而且可以按照参数index来查找或者移除特定位置上的元素
ArrayList底层结构和源码分析
- ArrayList底层实现其实是一个Object类型的数组elementData;当创建ArrayList对象时,如果调用的是无参构造器,那么默认的数组长度为0,之后第一次添加元素会扩容到10,之后的再次扩容则为之前容量的1.5倍;也可以调用有指定初始数组大小的构造器,不过同样之后的扩容为1.5倍
ArrayList中add() 方法调用的过程
- add() ->
- ensureCapacityInternal( size + 1 ) ->
- 确定 minCapacity -> 根据elementData是否 == DEFAULTTCAPACITY_EMPTY_ELEMENTDATA
- (如果ArrayList一开始创建时调用的是无参构造,那么就会将DEFAULTTCAPACITY_EMPTY_ELEMENTDATA赋值给elementData)
- ensureExplicitCapacity( minCapacity )
- modCount ++ (记录ArrayList被修改次数?)
- 判断 minCapacity - elementData.length > 0
- 如果当前elementData容量不够 grow( minCapacity ),如果容量够用就不会调用 grow
- 先根据当前elementData长度计算正常扩容后的容量大小(1.5倍)
- 判断新的容量大小是否能满足 minCapacity(minCapacity = size + 1 或者 10, DEFAULT_CAPACITY)
- 所以新的容量要么为原来容量的1.5倍要么为10
- 根据新容量调用 elementData = Arrays.copyOf( elementData, newCapacity )
- 确保了elementData有足够空间之后(要么原有空间不够,则会调用grow( minCapacity )进行扩容;要么原来空间足够,则会不进行任何操作,不过还是会modCount),就将要添加的元素加到elementData中:elementData[ size++ ] = e;
Vector的底层结构和源码分析
- Vector的基本介绍
- 其底层也是一个对象数组;Vector是线程安全的,其操作方法一般都带有synchronized关键字,因此也导致了其运行效率不如ArrayList
- 调用无参构造也会默认创建一个长度为10的elementData;在数组容量满了之后每次扩容的倍数为2
LinkedList
- LinkedList底层的实现原理是一个双向链表;其中维护了两个属性:first & last,分别指向了首节点和尾节点
- 节点(Node对象),其中维护了prev,next,item三个属性;prev指向前一个,next指向下一个;node class中只包含了一个构造方法:Node( Node
prev, E element, Node next) - LinkedList元素的增删不是通过数组完成的,所以相对而言效率比较高
- LinkedList的无参构造其中没有包含任何操作
LinkedList执行add()方法添加元素
- add( element ) -> linklast( element )
- 首先,LinkedList添加元素的思路就是往链表的最后一位添加元素,所以是将LinkedList对象中的last和原最后一位的next进行修改
- final Node
l 指向当前的last - final Node
newNode = new Node<>(l, e, null),创建新的last节点,因为是加在链表的最后一位,所以last为null - last = newNode,给last赋予新值
- 判断 l 变量(即原last变量)是否为null
- 如果为null,即之前的last为null,说明之前的链表为空,那么同时将first也赋值为newNode
- 否则只将 l 变量的next赋值为newNode就足够了
- size++ & modCount ++
LinkedList中的remove方法
- remove() -> 无参数,int参数,Object参数,如果无参数,那么默认移除链表中的第一个元素
- removeFirst / removeLast
- removeFirstOccurrence / removeLastOccurrence
Set接口
- 无序 -> 所以放入和取出的顺序是不一致的,但是每次取出的顺序是固定的;不允许重复元素 -> 所以也导致了只能包含一个null;但是不能使用索引的方式来遍历
Set接口的实现类:HashSet
- 其底层本质是一个HashMap,HashMap的底层是数组+单向链表+红黑树
- HashSet扩容机制
- 当HashSet添加一个元素时,会先得到该元素的hash值,并使用hash值作为存储时的key值(HashSet的本质是一个HashMap)
- 根据hash值(key),找到Map中的对应位置是否有元素
- 如果没有则直接放入表中;如果已经有元素则依次调用equals方法比较两者是否相同,如果相同则放弃添加,如果不相同则将新元素加到已有末位元素next的位置上
- 在java8中,如果某条链表的元素个数达到TREEIFY_THRESHOLD(8个),而且整个table的大小 >= MIN_TREEIFY_CAPACITY(64个),那么就会进行树化(红黑树)
创建HashSet对象并且添加元素的过程
- Set
testSet = new HashSet<>(); -> 底层是:map = new HashMap<>(); - HashSet.add( E element ); -> map.put( element, PRESENT );
- PRESENT 是HashSet类中的一个静态属性,仅仅是一个Object对象,作用只是在put方法中占位
- element 和 PRESENT 传递到 map.put() 之后,被接收为 key & value
- putVal( hash( key ), key, value, false, true );
- hash( key ) 会计算key的hash值并做一定的处理,减低不同对象hash值碰撞的可能性
- putVal()
- 初始化变量 Node
[] tab & Node p & int n & int i - 然后将tab指向属性table
- 如果是添加第一个元素的话,那么table为null -> n = ( tab = resize() ).length;
- resize() 会判断并创建一个新的长度为16的Node数组,并对table赋值;现在的tab为一个长度16的空数组(同样也是table),n也被赋值为该数组的长度,即16
- 然后根据 ((p = tab[i = (n - 1) & hash]) == null),算出来该key应该放到数组中的哪个索引位置,赋值给变量i,并且将该位置上的元素赋值给p;判断p是否为null
- 为null则以hash和key创建Node对象,将该Node对象放到数组的i位置上
- 如果不是第一次添加元素
- 同样还是会根据 (p = tab[i = (n - 1) & hash]) 找出当前元素应该放到数组中的位置;如果该位置上还是null的,就直接放置该元素;如果不是null,则要进行比较两者是否相同
- 判断1:根据hash值和equals判断当前元素和该位置链表的第一个元素是否相同
- 判断2:当前节点是否为一个红黑树节点
- 判断3:遍历该位置链表上其他的元素,如果不相同则轮到下一个,直到找到相同的元素则break退出或者找到null值,那么就是遍历到链表末位,因为已经遍历到链表末位都没有找到相同的元素,那么就将当前元素添加到该链表的末位
- note:如果添加成功,则会返回一个null对象,那么整个add方法的返回值为true;不然为false
- note:每次创建Node对象收录的时调用的构造方法都是:newNode(hash, key, value, null);,意思是之前传递的PRESENT Object() 也会一并收录到table中
- note:class Node为HashMap中的一个内部类,有hash, key, value, next 4个成员属性
- 初始化变量 Node
LinkedHashSet
- LinkedHashSet底层是一个LinkedHashMap,LinkedHashMap是HashMap的子类,其维护了一个数组+双向链表;根据元素的hashCode值来决定元素的存储位置,同时使用链表来维护元素的次序,是的元素看起来是以插入顺序保存的
- LinkedHashSet的创建
- 调用无参构造new LinkedHashSet<>() -> super( initialCapacity, loadFactor, dummy ) -> new LinkedHashMap<>(initialCapacity, loadFactor);HashSet(LinkedHashSet的父类中专门为LinkedHashSet定义了一个构造器,会生成一个LinkedHashMap),然后LinkedHashMap是继承自HashMap的,调用LinkedHashMap的构造器的时候,会调用其父类的构造器生成一个HashMap
- LinkedHashMap中:Entry是LinkedHashMap中用于存放元素的单位,定义为LinkedHashMap中的一个内部类,继承了HashMap中的Node类,除了hash, key, value, next这些Node类中的成员属性之外,还定义了 Entry
before, after用于指向双向链表中的上一个和下一个;Entry head, tail 用于定位双向链表的第一个和最后一个 - LinkedHashMap中重写了 newNode() 方法,在创建了新的Entry之后,会LinkNodeLast()方法,将新的Entry挂载到链表的末尾,同时根据情况更新LinkedHashMap中head & tail指向的对象(根据putVal()方法中的定义,只有当参数跟已有元素不重复的情况下才会调用newNode,确保了每次调用newNode就是有新元素要放入到table中)
Map接口:
- 用于保存具有映射关系的数据:Key - Value(双列元素);当具有相同key值的元素被放到Map中时,会替换掉原先的元素;同样地,可以以null值为key,不过null key只可以有一个
- 除了一个个的Node,在HashMap中还有 entrySet,values,keySet 成员属性方便程序员遍历Map中的元素(其中values和keySet不是直接在HashMap定义的成员变量);不过要注意 keySet 和 entrySet 取出来是 Set 的形式;而 values 取出来是 Collection 的形式
- HashMap放入元素的过程(概述)
- 调用 hash( key ) 根据键值求出来当前元素应该放置的位置;然后再依次跟已在该位置上的元素进行比较,比较的依据是两者的hash已经equals()方法;一旦找到相同的key,那么就会退出循环,将旧的value替换为新的value,并返回被替换掉的旧value;如果一直到循环到null都没有找到相同的元素,那么就会依据传入的参数创建新的Node,并放置到null的位置上,然后返回null
- 最后,put()方法根据putVal()的返回值是否为null,输出最后的布尔值
Map接口的常用方法
- remove() 根据键删除映射关系;get() 根据键获取值;size() 获取元素个数;isEmpty() 判断个数是否为0;clea() 清除键值对;containsKey() 查找键是否存在;keySet() 获取所有的键;entrySet() 获取所有的键值对;values() 获取所有的值
HashTable 简介
- HashTable的键和值都不能为null,否则会抛出NullPointerException
- 如果尝试着向HashTable中put key 或者 value为null的键值对的话,put()方法中会首先判断是否为null,如果是null的话就会直接抛出NullPointerException;如果key是null,在随后的代码块中调用 key.hashCode()的时候就会抛出异常
- HashTable的使用方法基本上都有加synchronized关键字,所以这些方法都是线程安全,不过这也导致了HashTable的效率对比HashMap会较低一点
Properties
- 继承了HashTable,集合中所有的键值对都是String
- 通常用于从 xxx.properties 文件中加载数据到 Properties 类对象中
TreeSet & TreeMap
调用 put() 向TreeMap中添加元素的过程
- TreeMap 中维护了一个成员属性root,类型为Entry
,其中有 key, value, left, right, parent 等成员属性;仅有一个接收 key, value, parent 三个构造参数的构造方法 - TreeMap中还有一个成员属性comparator,用于指向对TreeMap中元素进行排序的比较器,一般可以在创建TreeMap的时候将比较器作为参数传递进来
- 添加过程
- 定义赋值变量 t,指向成员变量 root,root的数据类型就是Entry
- 检查 t 是否为null(说明之前没有向该TreeMap中添加过元素),如果是那么就直接使用传递过来的 key value 值创建Entry实例,并赋值给 root,并返回null
- 如果 t 不为null
- 定义辅助变量 cmp & parent,并创建变量cpr指向成员变量comparator,然后检查cpr(也就是comparator)是否为null,如果不为null那么就调用comparator的compare方法对key进行比较,如果comparator为null,那么就调用key本身的compareTo来比较key的大小
- note:如果TreeMap中没有定义comparator,那么在每次调用put()朝里面添加元素的时候都会将添加的元素转型为一个Comparable的对象,如果该元素没有实现Comparable接口的话就会报错 -> 这是为了保证所有的元素都能按照某个规则来排序
- 比较的原则有三条:
- 原则一:如果比较方法(指comparator的compare() 或者是 比较对象本身的 compareTo() )的返回值为0,那么就说明当前传递元素的key值与已有元素的key值重复,那么就调用重复元素Entry的setValue方法,替换新老value值,并将原有oldValue返回
- 原则二:如果比较方法返回值为负,则将添加的元素放到当前元素的左边;如果返回值为正,则放到右边;note:比较过后唯一可以确定的就只有是左边还是右边,确切的位置要等到一直比较出现null为止才可以确定
- 原则三:root会根据新元素的加入而改变,估计root的确定跟红黑树有关?
调用add()方法向TreeSet中添加元素
- TreeSet的底层实现是一个TreeMap,每次调用TreeSet的add()方法,其底层都是调用了TreeMap的put()方法
- 要向TreeSet中添加的元素作为key传递,而value则是TreeSet中一个static final的成员变量PRESENT