集合相关知识

摘要:本文主要介绍了几种集合类型以及有关的一些知识点。

集合类图

类图

类图说明

所有集合类都位于java.util包下。Java的集合类主要由两个接口派生而出:Collection和Map,Collection和Map是Java集合框架的根接口,这两个接口又包含了一些子接口或实现类。

接口用短虚线表示,表示不同集合类型,是集合框架的基础。例如Collection,Map,List,Set,Iterator等。

抽象类用长虚线表示,对接口的部分实现。例如AbstractMap,AbstractCollection,AbstractList,AbstractSet等。

实现类用实线表示,对接口的具体实现。例如ArrayList,LinkedList,HashSet,HashMap等。

集合概述

Collection接口是一组允许重复的对象,属于单列集合。

List接口继承自Collection接口,允许插入重复的数据,是有序的集合,可以通过索引访问元素。

Set接口继承自Collection接口,不允许插入重复的数据,是无序的集合。

Map接口是一组保存了key-value键值对的对象,属于双列集合,只能根据每个键值对的key访问value。

Collection接口

Collection是一个接口,是高度抽象出来的集合,它包含了集合的基本操作和属性。Collection包含了List和Set两大分支。

常用方法

添加单个元素:boolean add(Object object);

添加一个集合里的所有元素:boolean addAll(Collection<? extends E> collection);

删除单个元素:boolean remove(Object object);

删除指定集合里有的元素:boolean removeAll(Collection collection);

删除两个集合都有的元素:boolean retainAll(Collection collection);

判断是否包含某个元素:boolean contains(Object object);

判断是否包含指定集合的所有元素:boolean containsAll(Collection<?> collection);

判断集合是否为空:boolean isEmpty();

清除集合里的元素:void clear();

获取集合元素个数:int size();

将集合转换为数组:Object[] toArray();

将集合转换为指定类型的数组:<T> T[] toArray(T[] array);

获取集合迭代器:Iterator iterator();

集合同数组的比较

数组长度一旦固定,不能再改变,集合的长度是可以改变的。

数组只能保存相同类型的数据,集合可以保存指定类型或其子类型的数据。

数组在使用的时候相对比较麻烦,集合可以利用多种方法,还有工具类。

List接口

List接口继承自Collection接口,允许定义一个重复的有序集合,集合中的每个元素都有对应的一个索引,可以通过索引访问List中的元素。

实现List接口的实现类主要有:ArrayList、LinkedList、Vector、Stack。

特点

允许重复。

有序,取出的顺序和插入的顺序一致。

为每一个元素提供一个索引值,默认从0开始。

常用方法

在指定索引位置添加单个元素:void add(int index, Object object);

在指定索引位置添加一个集合:boolean addAll(int index, Collection<? extends E> collection);

删除指定位置的单个元素:Object remove(int index);

获取指定位置的单个元素:Object get(int index);

替换指定位置的单个元素:Object set(int index, Object object);

获取指定元素的出现的第一个索引:int indexOf(Object object);

获取指定元素的出现的最后一个索引:int lastIndexOf(Object object);

获取指定位置的集合,包含起始位置,不包含结束位置:List<E> subList(int fromIndex, int toIndex);

获取集合迭代器:ListIterator<E> listIterator();

ArrayList类

特点

ArrayList是动态数组结构,也是我们最常用的集合,允许任何符合规则的元素插入,包括null。

ArrayList提供了索引机制,可以通过索引迅速查找元素,查找效率高。但是每次增加或删除元素时,身后的元素都要移动,所以增删效率低。

ArrayList的操作是非同步的,是线程不安全的。

扩容机制

数组结构都会有容量的概念,ArrayList的初始容量为10,加载因子是1,当快插入元素后长度超出原有长度时会进行扩增,扩容增量是0.5,扩增后容量为1.5倍,可使用方法手动扩容和缩减。

如果一开始就明确所插入元素的多少,最好指定一个初始容量值,避免过多的进行扩容操作而浪费时间和效率。

Vector类

特点

与ArrayList相似,它的操作与ArrayList几乎一样。

Vector是同步的,是线程安全的动态数组,但是效率低。

扩容机制

初识容量为10,加载因子为1,扩容增量是1,扩增后容量为原来长度的2倍,适用于数据量大的环境。

LinkedList类

特点

LinkedList是双向链表结构,额外提供了操作列表首尾元素的方法,因为不是数组结构,所以不存在扩容机制。

LinkedList使用了链表结构,通过修改前后两个元素的链接指向实现增加和删除操作,增删效率高,但是查找操作必须从开头或者结尾便利整个列表,所以查找效率低。

LinkedList的操作是非同步的,是线程不安全的。

特殊方法

在开头位置插入元素:void addFirst(Object object);

在结尾位置插入元素:void addLast(Object object);

删除开头位置的元素并返回:Object removeFirst();

删除结尾位置的元素并返回:Object removeLast();

获取开头位置的元素:Object getFirst();

获取结尾位置的元素:Object getLast();

Set接口

Set接口继承自Collection接口,允许定义一个不重复的无序集合,集合中只允许存在一个null值。

实现Set接口的实现类主要有:HashSet、LinkedHashSet、TreeSet。

特点

不可以重复,只能插入一个空值。

无序,不能保证插入的顺序和输出的顺序一致。

没有索引。

HashSet类

特点

HashSet的底层是HashMap。

HashSet使用了一个散列集存储数据,通过元素的Hash值进行排序,不能保证插入和输出的顺序一致。

HashSet不能插入重复的元素,只能存在一个null值。

HashSet内部通过哈希表进行排序,具有很好的查找和存取功能。

HashSet是非同步的,线程不安全。

扩容机制

和HashMap相同。

LinkedHashSet类

特点

LinkedHashSet继承自HashSet,其底层是基于LinkedHashMap来实现的。

LinkedHashSet使用链表维护元素的次序,同时根据元素的hash值来决定元素的存储位置,遍历集合时候,会以元素的添加顺序访问集合的元素。

LinkedHashSet不能插入重复的元素,只能存在一个null值。

LinkedHashSet插入性能略低于HashSet,但在迭代访问Set里的全部元素时有很好的性能。

LinkedHashSet是非同步的,线程不安全。

扩容机制

和HashMap相同。

TreeSet类

特点

TreeSet的底层是TreeMap。

TreeSet基于二叉树结构,可以实现自然排序。

TreeSet通过比较方法的返回值来判断元素是否相等,因此不能添加null的数据,不能添加重复元素,只能插入同一类型的数据。

TreeSet支持两种排序方式,自然排序和定制排序。

自动排序:添加自定义对象的时候,必须要实现Comparable接口,并要覆盖compareTo方法来自定义比较规则。

定制排序:创建TreeSet对象时,传入Comparator接口的实现类。要求Comparator接口的compare方法的返回值和两个元素的equals方法具有一致的返回值。

Map接口

Map与List、Set接口不同,它是由一系列键值对组成的集合,提供了key到value的映射.

同时它也没有继承Collection。

特点

一个key对应一个value,不能存在相同的key值,value值可以相同。

key和value之间存在单向一对一关系,即通过指定的key总能找到唯一确定的value。

实现Map接口的实现类主要有:HashMap、LinkedHashMap、TreeMap。

常用方法

插入一个键值对并返回value的值:V put(K key, V value);

插入一个键值对集合:void putAll(Map<? extends K,? extends V> m);

根据key值移除对应的元素:V remove(Object key);

根据key值获取对应的元素:V get(Object key);

获取Entry的Set集合:Set<Map.Entry<K,V>> entrySet();

获取key的Set集合:Set<K> keySet();

获取value的Collection集合:Collection<V> values();

遍历Map

通过Key遍历

1 Set set = map.keySet();
2 for(Object obj : set) {
3     System.out.println(obj + " -> " + map.get(obj));
4 }

通过Entry遍历

1 Set set = map.entrySet();
2 for(Object obj : set) {
3     Map.Entry entry = (Map.Entry)obj;
4     System.out.println(entry);
5     System.out.println(entry.getKey() + " -> " + entry.getValue());
6 }

HashMap类

特点

HashMap底层使用数组结构和链表结构。

HashMap根据键的Hash值存储数据,根据键可以直接获取它的值,具有很快的访问速度,遍历时,取得数据的顺序是完全随机的。

因为键对象不可以重复,所以HashMap最多只允许一条记录的键为null,允许多条记录的值为null。

HashMap是非同步的,线程不安全。

扩容机制

JDK1.7及以前,默认初始容量16,加载因子是0.75,扩容增量是1。

当同时满足两个条件时才会扩容:当前数据存储的数量大小必须大于等于阈值。当前加入的数据发生了hash冲突。

这就有可能导致存储超多值得时候仍没有扩容:一开始存储的11个值全部hash碰撞,导致存入了同一个链表,后面存入的15个值全部没有hash碰撞,这时存入的个数为26个,但是并不会扩容。

JDK1.8,默认初始容量16,加载因子是0.75,扩容增量是1。

当存入个数超过8时,会将链表转换为红黑树。

当存入个数超过容量的0.75倍时,就会进行扩容,并且扩容增量也变成了一倍。

底层实现原理

HashMap的底层是Entry类型的,名字叫table的数组。

Entry数组中的一个元素保存了一组Key-Value映射。

存储结构中使用了数组结构和链表结构。

添加元素时,根据key所在类的hashCode()得到key的hashCode值,并通过hash算法得到在底层数组中的存放位置,如果hashCode相同,那么位置也是相同的。

如果此位置上没有其他元素,则添加成功。如果此位置上已经有元素存在,则调用key所在类的equals()方法比较key是否相同。

如果返回true,使用添加的entry的value替换原有位置entry的value,返回原值。

如果返回false,表示发生了hash冲突,新的entry仍能添加成功,与旧的entry之间使用链表存储,新添加的在首部。

Hashtable类

特点

Hashtable是Map的古老实现类,使用哈希表算法。

不能存储null的键和值。

线程安全,但是效率低。

扩容机制

默认初始容量为11。

LinkedHashMap

特点

LinkedHashMap继承自HashMap,使用哈希表和链表实现。

LinkedHashMap使用链表维护元素的次序,保留了元素的插入顺序,可以按照顺序遍历。

LinkedHashMap允许使用null值和null键。

LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能,但在迭代访问Map里的全部元素时将有很好的性能,因为它以链表来维护内部顺序。

LinkedHashMap是非同步的,线程不安全。

扩容机制

和HashMap相同。

TreeMap

特点

TreeMap是基于红黑树实现的,TreeMap存储时会进行排序,按照添加进Map中的元素的Key的指定属性进行排序。

TreeMap的排序方式有两种,自然排序和定制排序。

自然排序:TreeMap中所有的key必须实现Comparable接口,并且所有的key都应该是同一个类的对象,否则会报ClassCastException异常。

定制排序:定义TreeMap时,创建一个comparator对象,该对象对所有的treeMap中所有的key值进行排序,采用定制排序的时候不需要TreeMap中所有的key必须实现Comparable接口。

TreeMap判断两个Key相等的标准是通过compareTo()方法或者compare()方法。

TreeMap是非同步的,线程不安全。

HashMap的扩容机制以及默认大小为何是2次幂

hash方法

在Object类中有一个hashCode()方法,用来获取对象的hashCode值,它被native修饰,意味着这个方法和平台有关,对于有些JVM,hashCode()返回的就是对象的地址,但大多数情况下是根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,这个数值称作为散列值。

对于包含容器类型的程序设计语言来说,基本上都会涉及到hashCode。在Java中也一样,hashCode的主要作用是为了配合基于散列的集合一起正常运行,这样的散列集合包括HashSet、HashMap以及HashTable。

当向集合中插入对象时,如果调用equals()逐个进行比较,虽然可行但是这样做的效率很低。因此,先调用hashCode()进行判断,如果相同再调用equals()判断,会快很多。

因为在计算元素在HashMap中的下标时,是通过 hash & (length-1) 计算得到的,通过和长度减1进行与运算只会用到低位,所以在使用hashCode之前需要再次进行处理生成新的hash,保证对象的hashCode的32位值只要有一位发生改变,整个hash就会改变,高位的变化也会影响低位,这时再使用低位计算下标就能使元素的分布更加合理。

◆ 在JDK1.7及以前的hash()是进行一系列的移位和按位或运算

1 final int hash(Object k) {
2     int h = hashSeed;
3     if (0 != h && k instanceof String) {
4         return sun.misc.Hashing.stringHash32((String) k);
5     }
6     h ^= k.hashCode();
7     h ^= (h >>> 20) ^ (h >>> 12);
8     return h ^ (h >>> 7) ^ (h >>> 4);
9 }

◆ 在JDK1.8以后的hash()只需要进行一次异或运算

1 static final int hash(Object key) {
2     int h;
3     return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
4 }

为什么默认长度为2的次幂

长度为2的次幂,是因为要通过hash计算一个合适的下标,使用的方法是 hash & (length-1) 将hash和长度减1进行按位与运算,如果长度为2的次幂,那么长度减1得到的二进制表示的每个位上的数字都是1,任何数字同1进行与运算得到的结果都是它本身。

如果长度改为其他不为2的次幂的数字,长度减1得到的二进制表示的某个位上会是0,0同任何数字相与都是0。这个位上为1的位置永远都不会被放入元素,而且hash在这个位置上不管为1还是0,得到的位置都是一样的,造成了额外的碰撞。

比如,长度为15时,15减1得到的二进制表示为1110,那么1001,0101,0011这种末位为1的位置上将永远都不会有元素,造成位置浪费,而且hash为1101和hash为1100得到的位置都是1100,产生了碰撞,还需要进一步判断。

只有当所有位置都是1,也就是长度为2的次幂时,才会让所有位置都有可能被用到,并且每个二进制末4位不同的数字都能有唯一的位置,减少了碰撞的产生。

如果一开始设置的长度不是2的次幂

如果手动设置了长度,那么HashMap会对传入的长度进行处理,通过调用tableSizeFor方法,将长度转为二进制位都为1的并且大于传入长度的一个数字,然后加1返回就得到了一个2次幂的数字。

何时会进行扩容

◆ 在JDK1.7及以前,判断是否要扩容的条件是: (size >= threshold) && (null != table[bucketIndex]) 。

第一个条件是长度的阈值,阈值用threshold表示,一般是长度和加载因子的乘积,加载因子默认是0.75。

第二个条件是当前位置上不能为空,也就是说发生了hash碰撞。

只有同时满足了这两个条件,才会进行扩容,这就有可能导致在当前长度超出阈值的情况下仍不会进行扩容操作。

◆ 在JDK1.8以后,判断条件是: ++size > threshold 。

只要在插入之后的长度超过了当前的阈值,就会进行扩容操作。

扩容机制resize方法

在JDK1.7及以前,HashMap使用的是数组加链表的方式存储的。在进行扩容后,原来的元素都要重新计算hash,通过重新计算索引位置后,如果在新表的索引位置相同,则链表元素会倒置 。

 1 while(null != e) {
 2     Entry<K,V> next = e.next;
 3     if (rehash) {
 4         e.hash = null == e.key ? 0 : hash(e.key);
 5     }
 6     int i = indexFor(e.hash, newCapacity);
 7     e.next = newTable[i];
 8     newTable[i] = e;
 9     e = next;
10 }

在JDK1.8以后,使用的是数组加红黑树的存储方式。在进行扩容后,不再进行重新计算hash,而是通过将原hash同原长度进行按位与运算,判断hash的高位是否为0,如果为0则放回原位置,如果不为0则放在高位。

 1 Node<K,V> e;
 2 if ((e = oldTab[j]) != null) {
 3     oldTab[j] = null;
 4     if (e.next == null)
 5         newTab[e.hash & (newCap - 1)] = e;
 6     else if (e instanceof TreeNode)
 7         ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
 8     else { // preserve order
 9         Node<K,V> loHead = null, loTail = null;
10         Node<K,V> hiHead = null, hiTail = null;
11         Node<K,V> next;
12         do {
13             next = e.next;
14             if ((e.hash & oldCap) == 0) {
15                 if (loTail == null)
16                     loHead = e;
17                 else
18                     loTail.next = e;
19                 loTail = e;
20             }
21             else {
22                 if (hiTail == null)
23                     hiHead = e;
24                 else
25                     hiTail.next = e;
26                 hiTail = e;
27             }
28         } while ((e = next) != null);
29         if (loTail != null) {
30             loTail.next = null;
31             newTab[j] = loHead;
32         }
33         if (hiTail != null) {
34             hiTail.next = null;
35             newTab[j + oldCap] = hiHead;
36         }
37     }
38 }

重写equals方法和hashCode方法

在发生hash碰撞的时候,一个桶里的两个元素key值不相等,但是他们的hashCode是相等的,如果两个key值也相等,则说明两个key相等。也就是说:

◆ 如果两个对象equals相等,那么这两个对象的HashCode一定也相同。

◆ 如果两个对象的HashCode相同,不代表两个对象就相同,只能说明这两个对象在散列存储结构中,存放于同一个位置。

一般在重写equals方法的时候,也会尽量重写hashCode方法,就是为了在equals方法判断相等的时候也保证让hashCode方法也判断相等。

猜你喜欢

转载自www.cnblogs.com/shamao/p/10299106.html
今日推荐