目录:
集合
用来存储一组同一类型的数据。
与数组相比的优势:①集合的大小可以随着存储数据的多少动态改变。②集合的实现类中实现了增删查改等方法,方便操作数据。
集合分为单列集合和双列集合。单列集合指存储的数据元素是单个数据;双列集合指存储的数据元素是一对数据,即包含一个Key值和一个Value值的一个键值对数据。
Collection<E>(单列集合)
Collection接口是单列集合的总接口,下面有两个主要的子接口:
- List:主要的实现类添加元素时会记录元素的添加顺序和元素本身的信息,并且允许元素可以重复
- Set:主要的实现类(除LinkedHashSet外)添加元素时不会记录元素的添加顺序,都会记录元素本身的信息,并且不允许元素重复。
Collection接口中规定了各种单列集合都应当具有的共有的一些方法,例如:
- boolean add(E e);//向集合中添加某个元素,返回添加是否成功
- boolean remove(Object obj);//删除集合中的某个元素,返回删除是否成功
一些理解:没有查找和更改方法的原因:Collection接口的某些实现类不能实现有意义的查找方法和更改方法。
元素存储在单列集合中时具有两个信息,一个是元素本身的信息,一个是元素在集合中的位置信息。查找时,一方面会根据位置信息查找元素信息(例如:List接口中的E get(int index)方法),另一方面会根据元素信息查找位置信息(例如:List接口中的int indexOf(Object o)方法)。更改时,一般期望在不改变位置信息的前提下,只更改元素信息,那么会先根据位置信息或元素信息先找到元素,再改变元素信息(例如:List接口中的E set(int index,E e))。
而在Collection接口的主要实现类中,Set接口中的HashSet实现类中元素的位置信息是无意义的(ArrayList、LinkedList、LinkedHashSet中元素的位置信息包含了元素存储时的顺序,TreeSet中元素的位置信息包含了根据比较器排列后的排列顺序),所以无法实现有意义的查找方法和更改方法,因此不能在Collection接口中规定所有实现类都要具有查找方法和更改方法
- boolean contains(Object obj);//判断集合是否包含某个元素
- boolean isEmpty();//判断集合是否为空
- int size();//获取集合中元素的个数
- void clear();//清空集合
- Object[] toArray();//将集合转换为对应类型的数组
- Iterator< E > iterator();//获取迭代器,用来遍历集合
Iterator是一个接口,其中规定了几个用于遍历的方法
- hasNext();//是否有下一个元素
- next();//获取下一个元素
- remove();//删除上一个获取的元素
用迭代器遍历集合时出现ConcurrentModificationException异常的原因:Collection接口的主要实现类中都定义了一个modCount
变量用于记录集合的操作次数,对集合进行新增、删除等操作会改变modCount
的值。在创建迭代器对象时,迭代器的构造方法中会将创建时的modCount
记录下来(expectedModCount = modCount;
),每次调用next()方法时都会调用checkForComodification()
方法检查当前集合的操作次数是否改变,如果改变就会抛出ConcurrentModificationException异常。
使用迭代器的remove()方法删除元素不会抛出异常是因为,在remove方法中调用当前集合的remove方法后,会重新给迭代器对象中的expectedModCount
变量赋值。
List<E>
List接口是各种列表的总接口,主要有两个实现类:
- ArrayList:底层通过数组存储元素。根据索引值查找元素很快,在指定位置增删元素较慢
- LinkedList:底层通过链表存储元素,根据索引值查找元素较慢,在指定位置增删元素较快
List接口中规定了列表集合应该具有的一些方法:
- void add(int index,E e);//在指定位置添加元素
- E remove(int index);//删除指定位置的元素
- E get(int index);//获取指定位置的元素
- E set(int index,E e);//更改指定位置的元素
- List< E > subList(int beginIndex,int endIndex);//截取指定索引值范围内的元素,构成一个新的集合
- int indexOf(Object o);//获取指定元素第一次出现的位置
- int lastIndexOf(Object o);//获取指定元素最后一次出现的位置
ArrayList<E>
ArrayList集合是以数组存储元素的,相比于数组,它实现了定义后容量的动态增长。
扩容:调用ArrayList的无参构造创建一个ArrayList对象时,它的容量为0,第一次调用add方法时,数组容量会扩增到10,以后每次扩容后新容量为旧容量的1.5倍(int newCapacity = oldCapacity + (oldCapacity >> 1);
)。此外,如果在创建对象时指定容量,会直接创建一个指定容量的数组(this.elementData = new Object[initialCapacity];
)。
ArrayList集合根据索引值查找元素很快的原因:可以根据数组的地址、索引值以及元素所占的内存大小快速计算出要查找元素的地址,进而直接访问对应元素。
在指定位置增删元素慢的原因:为了保持数组所有元素的地址是连续的,增删元素后,在确保容量足够的情况下,需要将指定位置后的所有元素后移/前移一个位置。
LinkedList<E>
LinkedList 集合是以双向链表来存储元素的:双向链表由一个个的节点连接而成,每个节点由element、prev、next三部分组成,element是元素本身,next指向下一个节点,prev指向上一个节点。
相比于ArrayList集合LinkedList实现了真正意义上的动态容量,新增元素时,集合会新建一个节点,链接到链表最后面。
LinkedList集合根据索引值查找元素较慢的原因:对于双向链表无法根据索引值快速得知对应元素的地址,只能从头结点/尾结点不断获取下一个/上一个节点的地址,直到对应元素所处的节点。从头结点还是尾结点开始取决于索引值与元素个数的相对大小(index < (size >> 1)
)
LinkedList集合在指定位置增删元素较快的原因:在查找到指定位置的元素节点后,只需要改变当前节点与前一个节点的next、prev的值就可以完成增删。
由于LinkedList中存储了头结点(first)和尾结点(last)的信息,所以有一些独有的与首尾操作有关的方法:
- void addFirst(E e);//在第一个元素前插入一个元素
- void addLast(E e);//在链表最后增加一个元素
- String removeFirst();//删除第一个元素
- String removeLast();//删除最后一个元素
- String getFirst();//获取第一个元素
- String getLast();//获取最后一个元素
Set<E>
Set是集的总接口,它的三个主要的实现类(HashSet、LinkedHashSet、TreeSet)都是基于Map的主要实现类实现的。不同的是在Map中每个元素结点中存储了key和value一对键值对数据,而Set中value的值为PRESENT
,PRESENT是一个静态常量,并没有实际意义,用来填充value只是为了保证元素结点的结构与Map相同,进而可以通过Map来完成数据的存储等功能。此外,Set集合可以通过迭代器来遍历集合,而Map没有实现迭代器接口(因为Map的核心是映射,更多是为了查找迅速;而单列集合则更多是收集数据,处理数据)。
HashSet<E>
底层是HashMap。
迭代器的迭代顺序:先从数组索引值为0找起,如果有元素,则按照对应位置下链表与红黑树中元素结点的next依次遍历,遍历完成后,继续在数组上寻找,直到遍历完数组。
LinkedHashSet<E>
底层是LinkedHashMap
迭代器的迭代顺序:从双向链表的头结点开始,根据每个节点的after依次遍历整个链表
TreeSet<E>
底层是TreeMap
迭代器的迭代顺序:按照以比较器或者存储元素的compareTo方法比较后的排列顺序迭代
Map<K ,V>(双列集合)
Map是双列集合的总接口,主要的实现类通过哈希表+单向链表/红黑树或者红黑树构建了从key到key-value键值对的一一映射:
- HashMap:以哈希表+单向链表/红黑树来存储元素,元素不会重复,不会记录元素的存储顺序
- LinkedHashMap:基于HashMap的存储原理来存储元素,元素不会重复,不过每个元素结点中加入了
after
和before
两个引用,用于记录元素的存储顺序 - TreeMap:以红黑树来存储元素,元素不会重复,不会记录元素的存储顺序,但在存储每个元素时,会根据比较器或key的compareTo方法将元素插入到对应的位置
Map中规定了双列集合应该具有的一些方法:
- V put(K k,V v);//如果k存在,则新的v替换旧的v,返回被替换的v;如果k不存在,则返回null
- V remove(Object key);//根据key删除整个元素节点,返回被删除元素的v;如果Key不存在,则返回null
- V get(Object key);//根据key获取v,如果key不存在则返回null
- boolean containsKey(Object key);//集合是否包含key
- boolean containsValue(Object value);//集合是否包含value
- Set<K>keySet();//获取存储所有key的Set类型的集合
- Collection<V> values();//获取存储所有value的Collection类型的集合
- Set<Entry<K,V>> entrySet();//获取存储所有键值对的Set类型的集合
- boolean isEmpty();//判断集合是否为空
- void clear();//清空集合
- int size();//获取集合中元素的个数
HashMap<K ,V>
通过哈希表+单向链表/红黑树存储元素,每个元素结点由hash(key的哈希值)、key、value、next /(parent、left、right、prev、next)几部分组成。
1、HashMap新增元素过程:
- 根据key计算hash值,再通过hash%数组长度(
(tab.length - 1) & hash
)找到元素在数组中的存储位置 - 判断该位置是否有元素
- 如果没有元素,则根据元素信息创建新结点,使数组对应位置指向该节点;
- 如果有元素,则判断头结点是否是树结点
- 是树结点,则按照红黑树的规则找到添加元素,key重复时,会更改原来的value值
- 不是树结点,则判断key是否重复,重复会更改原来的value值;没有重复时,则添加到链表最后,并判断当前链表的个数是否大于8个,大于8个时会试图将链表转换为红黑树
- 当整个数组的长度大于等于64时,会转换为红黑树
- 数组长度小于64时,会扩容数组
- 新增元素后会判断集合中元素个数是否大于数组长度的75%,如果大于,则扩容数组,以保证数组每个索引值下的元素不会太多,减少查找时间
2、判断元素重复的规则:哈希值相同&&((地址值相同)||(equals方法返回值为true))(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))
)
3、HashMap扩容的规则:如果集合是刚创建的,则将数组长度设置为16;如果已经创建,则扩容一倍(newCap = oldCap << 1
),并将链表或红黑树拆分为两部分,分别挂在原来索引值位置下和间隔旧数组长度的索引值位置下。
这样扩容可以保证数组长度始终是2的n次幂,一方面可以将元素哈希值%数组长度的运算转换为位运算(只有在tab.length等于2的幂时(tab.length-1)&hash==tab.length%hash),节省时间;另一方面在拆分链表或红黑树时也可以使用位运算,加快速度。
LinkedHashMap<K ,V>
LinkedHashMap是HashMap的子类,它的元素结点比HashMap多了两个引用:after
指向下一个加入集合的结点、before
指向上一个加入集合的结点
对于每个元素结点来说,从hash(key的哈希值)、key、value、next的角度看,它属于哈希表;从before、after的角度看,它属于双向链表
LinkedHashMap按照HashMap的规则添加元素,在添加完成后,会更改新增结点的after和before值,将它挂在双向链表最后。此外,根据key更改value后,也会调整更改结点的位置到链表最后。
TreeMap<K ,V>
TreeMap通过红黑树来存储元素。存储元素时会对元素进行排序。
在创建TreeMap时,需要传入比较器或者要保证Key类型实现了Comparable接口,否则会抛出ClassCastException异常。此外,如果Key类型实现了Comparable接口,也传入了比较器,那么会根据比较器的规则进行排序。