概述
集合类存放于java.util包中。集合类存放的都是对象的引用,而非对象本身。常见的集合主要有三种——Set(集)、List(列表)和Map(映射)。List和Set 都实现了 Collection 接口,并且List和Set也是接口,Map 为独立接口。
常见的实现类如下:
List 的实现类有:ArrayList、Vector、LinkedList;
Set 的实现类有:HashSet、LinkedHashSet、TreeSet;
Map 的实现类有:Hashtable、HashMap、ArrayMap、LinkedHashMap、TreeMap。
List 有序,可重复
-
ArrayList
优点: 底层数据结构是数组Array,查询快。增删慢。
缺点: 线程不安全,效率高 -
Vector
优点: 底层数据结构是数组Array,查询快。增删慢。
缺点: 线程安全,效率低
Vector是实现了 synchronized 的,这也是Vector和ArrayList的唯一的区别。 -
LinkedList
优点: 底层数据结构是链表LinkedList,增删快。查询慢。
缺点: 线程不安全,效率高。
链表的每一个节点(Node)都包含两方面的内容:1.节点本身的数据(data);2.下一个节点的信息(nextNode)。所以当对LinkedList做添加,删除动作的时候就不用像基于Array的List一样,必须进行大量的数据移动。只要更改nextNode的相关信息就可以实现了。这就是LinkedList的优势。
Set 无序 不可重复
-
HashSet
底层数据结构是哈希表,无序。依赖两个方法来保证元素唯一性:hashCode()和equals()。
调用add()方法向Set中添加对象,使用对象的值来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性。 -
LinkedHashSet
底层数据结构是双向链表和哈希表,有序。
由链表保证元素有序,由哈希表保证元素唯一。 -
TreeSet
底层数据结构是红黑树,内部实现排序,也可以自定义排序规则。
自然排序、比较器排序保证元素排序。根据比较的返回值是否是0来保证元素唯一性。 -
ArraySet
底层数据结构是双数组,有序。
Android.util包下的类,实时扩容,节约内存。推荐代替使用HashSet。
Map
K-V结构、键唯一、值不唯一。Map 集合中存储的是键值对,键不能重复,值可以重复。根据键得到值,对 map 集合遍历时先得到键的 set 集合,对 set 集合进行遍历,得到相应的值。
- HashMap :底层是哈希表,无序,允许null 键和null值,线程不安全的,效率高(通过 hashCode()和equals()保证元素唯一);
工作原理:
通过put()方法传递键(key)和值(value)时,我们先对键调用hashCode()方法,计算并返回的hashCode是用于找到Map数组的位置来储存Entry对象。hashCode()方法中是根据key的hash值来求得对应数组中的位置。因为HashMap的数据结构是数组和链表的结合,如果元素位置尽量的分布均匀些,使得每个位置上的元素数量只有一个,当我们用hash算法求得这个位置的时候,马上就可以知道对应位置的元素就是我们要的,而不用再去遍历链表。所以,我们首先想到的就是把hashcode对数组长度取模运算。
1、取模法
假如数组的长度等于5,这时有一个数据6,我们如何把这个6储存到长度只有5的数组中呢?取余法计算6%5等于1,即把6这个数据放到数组下标为1的位置。如果再有一个数据需要存储,取余法计算后也等于1,就调用equals()判断值是否相同,相同的话就不存储。但是,问题来了,值不相同就会造成Hash碰撞冲突了。
2、Hash碰撞冲突
HashCode()的作用就是保证对象返回唯一hash值对应Map数组中的bucket存储位置,但当两个数据计算后的值一样时,这就发生了碰撞冲突。例如,前面有6%5=1 得到数组下标为1的位置。此时,有个数据是11,那么11%5=1,但是这个为1的位置已经有了6这个数据了,这就叫Hash冲突。
3、解决Hash冲突
(1)开放定址法
开放地址法有个非常关键的特征,就是所有输入的元素全部存放在哈希表里。它的实现是,发生哈希冲突,就以当前地址为基准,某种探查技术在散列表中形成一个探查(测)序列,去寻找下一个地址,若发生冲突再去寻找,直至找到一个为空的地址为止。假如关键字key的哈希地址(hashCode)的值 p = H(key),此时出现冲突,就以p为基础,产生另一个哈希地址p1,如果p1仍然冲突,再以p为基础,产生另一个哈希地址p2,…,直到找出一个不冲突的哈希地址pi ,将相应元素存入其中。所以这种方法又称为再散列法。
开放定址法分为:线性探查法、二次探查法、伪随机探测法
线性探查:顺序查看表中下一单元,直到找出一个空单元或查遍全表;
二次探查:在表的左右进行跳跃式探测,比较灵活;
伪随机探测:建立一个伪随机数序列,如i=(i+p) % m,并给定一个随机数做起点,每次去加上这个伪随机数就可以了。
(2)拉链法
HashMap、HashSet其实都是采用的拉链法来解决哈希冲突的。主要是采用链表的形式去处理发生哈希冲突的关键字key。(jdk1.8之后采用链表+红黑树)
实现思想:将所有哈希地址为 i 的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。
拉链法与开放定址法相比
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
拉链法需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
-
HashTable :底层是哈希表,无序,不允许null 键和null值;(因为equlas()方法需要对象,线程安全的,效率低,内部的方法基本都经synchronized修饰;父类是Dictionary。
-
LinkedHashMap:底层是哈希表和链表;
-
ConcurrentHashMap:底层采用分段的数组+链表实现,线程安全,通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容。
-
TreeMap —— 有序
能够把它保存的键值对根据key排序,基于红黑树,从而保证TreeMap中所有键值对处于有序状态。
List、Set、Map值是否可以为null
List 允许为null
- 可以看到ArrayList可以存储多个null,ArrayList底层是数组,添加null并未对他的数据结构造成影响。
- LinkedList底层为双向链表,node.value = null 也没有问题。
- Vector 底层是数组,所以不会管你元素的内容是什么,可以存储多个null。
public void testArrayList(){
ArrayList<String> list = new ArrayList<>();
list.add(null);
list.add(null);
Assert.assertEquals(2,list.size()); // success
}
public void testLinkedList(){
LinkedList<String> list = new LinkedList<>();
list.add(null);
list.add(null);
Assert.assertEquals(2,list.size()); // success
}
public void VectorTest(){
Vector box = new Vector();
box.add(null);
box.add(null);
Assert.assertEquals(2,box.size()); //success
}
//Vector的add函数源码
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
Set
- HashSet底层是HashMap,可以有1个为null的元素。
- LinkHashSet底层也是hashmap,允许存在一个为null的元素
- TreeSet不能有key为null的元素,会报NullPointerException
public void testHashSet(){
HashSet<String> set = new HashSet<>();
set.add(null);
Assert.assertEquals(1,set.size()); //OK size = 1
set.add(null);
Assert.assertEquals(2,set.size()); //Error size = 1
}
public void testTreeSet(){
TreeSet<String> set = new TreeSet<>();
set.add(null); //Error NullPointException
}
Map
- HashMap中只能有一个key为null的节点。因为Map的key相同时,后面的节点会替换之前相同key的节点。
public void testHashMap(){
HashMap<String,String> map = new HashMap<>();
map.put(null,null);
Assert.assertEquals(1,map.size()); //OK size = 1
map.put(null,null);
Assert.assertEquals(2,map.size()); //Error size = 1
}
- TreeMap的put方法会调用compareTo方法,对象为null时,会报空指针错。
public void testTreeMap(){
TreeMap<String,String> map = new TreeMap<>();
map.put(null,null);
Assert.assertEquals(1,map.size()); //Error NullPointException
}
- HashTable底层为散列表,无论是key为null,还是value为null,都会报错
public void HashTableTest(){
Hashtable table = new Hashtable();
table.put(new Object(),null); //Exception
table.put(null,new Object()); //Exception
table.put(null,null); //Exception
Assert.assertEquals(1,table.size());
}
//hashTable put函数源码
public synchronized V put(K key, V value) {
if (value == null) { //value 需要判空,所以value不可为null
throw new NullPointerException();
}
Entry<?,?> tab[] = table;
int hash = key.hashCode(); //key需要拥有实例去调用hashCode方法,所以也不能为空
int index = (hash & 0x7FFFFFFF) % tab.length;
@SuppressWarnings("unchecked")
Entry<K,V> entry = (Entry<K,V>)tab[index];
for(; entry != null ; entry = entry.next) {
if ((entry.hash == hash) && entry.key.equals(key)) {
V old = entry.value;
entry.value = value;
return old;
}
}
addEntry(hash, key, value, index);
return null;
}
参考链接
Java集合中List,Set以及Map等集合体系详解(史上最全)
Java中 List、Set、Map 之间的区别
关于List,Set,Map能否存储null