Java集合

106 阅读6分钟

1. 集合概述

集合: 用于存储数量不确定以及具有映射关系数据的容器。

集合和数组区别:

  • 数组的长度是固定的,集合的长度是可变的
  • 数组可以存储基本数据类型和引用类型,集合只能存储引用类型
  • 数组存储的元素必须是同一个数据类型,集合存储的对象可以是不同数据类型

2. 集合框架体系

QQ截图20220630095048.png QQ截图20220630095104.png

Map接口和Collection接口是所有集合框架的父接口,其中Collection的子接口包含List接口和Set接口。List接口的实现类有ArrayList、LinkedList、Vector等,Set接口的实现类有HashSet、TreeSet、LinkedHashSet等,Map接口的实现类有HashMap、TreeMap、HashTable、CounrrentHashMap以及Properties等。

  • List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有ArrayList、LinkedList和Vector。
  • Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素, 只允许存入一个 null元素,必须保证元素唯一性。Set 接口常用实现类有HashSet、 LinkedHashSet以及TreeSet等。
  • Map:一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。 Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。常用实现类有HashMap、TreeMap、HashTable、LinkedHashMap、CurrentHashMap等。

3. 常用集合类

3.1 ArrayLsit和Vector区别

  • ArrayList是List的主要实现类,底层使用Object数组存储,实现了RandomAcces接口,支持随机访问,适用于频繁的查找工作。由于线程不安全,在性能方面比Vector高。 QQ截图20220630102615.png
  • Vector是List的古老实现类,底层同样使用Object数组存储,由于Vector类的操作方法使用了synchronized实现线程同步,因此线程是安全的。

3.2 ArrayList和LinkedList区别

  • 是否保证线程安全:两者都不保证线程安全。
  • 底层数据结构:ArrayList底层使用Object数组,LinkedList底层使用双向链表(jdk1.6之前为循环双向链表,jdk1.7取消了循环)。
  • 插入和删除效率:在非首尾位置进行插入和删除操作,LinkedLsit由于链表结构要比ArrayList效率高,ArrayList插入和删除操作需要影响数组内其他数据的下标。
  • 内存空间占用:由于LInkedList需要维护前驱和后继引用比ArrayList更占内存。
  • 随机访问效率:LinkedList不支持随机访问,ArrayLsit支持随机访问,可以根据元素的序号快速获取元素对象。

3.3 ArrayList和Vector扩容机制

(1)当创建ArrayList对象时,如果使用的是无参构造器,则初始数组容量为0,第一次添加,则扩容数组容量为10,如需再次扩容,则扩容为数组容量的1.5倍;如果使用的是有参构造器(指定大小),则数组初始容量为指定大小,如需扩容,则直接扩容为数组容量的1.5倍。 (2)当创建Vector对象时,如果使用的是无参构造器,默认初始数组容量为10,当数组容量满后,则扩容为数组容量的2倍;如果使用的是有参构造器(指定大小),每次直接扩容数组容量2倍。

3.4 HashSet

HashSet是基于HashMap实现的,HashSet的值存放在HashMap的key上,HashMap的value统一为常量PRESENT。HashSet的实现很简单,相关操作基本都是直接调用底层HashMap完成的,线程不安全。 QQ截图20220630102615.png

3.5 HashSet、LinkedHashSet和TreeSet三者区别

  • HashSet底层HashMap,线程不安全。
  • LinkedHashSet是HashSet子类,底层是一个LinkedHashMap,能够按照添加的顺序遍历。
  • TreeSet底层使用红黑树,能够按照添加的顺序进行遍历,排序有自然排序和定制排序,相关操作调用TreeMap完成。

3.6 HashMap的底层实现

JDK7JDK8
存储结构数组+链表数组+链表+红黑树
hash计算方式扰动处理=9次扰动=4次位运算+5次异或运算扰动处理=2次扰动=1次位运算+1次异或运算
存放数据规则无hash冲突时,存放数组。hash冲突时,存放链表无hahs冲突时,存放数组。hash冲突且链表长度<=8,存放单链表。hash冲突且链表长度>8但数组长度<64,则数组扩容,若数组长度大于64,转化为红黑树。

3.7 HashMap中String、Integer为什么适合做key

String、Integer等包装类特性能够保证Hash值得不可更改性和计算准确性,能够有效减少hash冲突几率,原因如下。

  • 都是final类,保证key的不可更改性,不会存在获取hash值不同的情况;
  • 内部重写了equals、hashcode方法,遵守HashMap内部规范,不容易出现hash值计算错误的情况。

3.8 HashMap为什么不直接使用hashcode方法处理后的哈希值直接作为数组table的下标

hashcode方法返回的是int整数类型,其范围为-(2^31)~(2^31-1),约有40亿个映射空间,然而HashMap容量范围是16~(2^30),HashMap通常情况下取不到最大值,从而导致通过hashcode方法计算出的hash值可能不在数组大小范围内,进而无法匹配存储位置。通过如下方法解决:

  • hashmap自己实现了自己的hash方法,通过两次扰动使得自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率使得数据分布均匀;
  • 在保证数组长度为2的幂次方时,使用hash&(length-1)来获取数组下标的方式进行存储,一是该操作比取余操作更加有效率(位运算比取余运算效率高),二是因为只有数组长度为2的幂次方时,hash&(length-1)等价于hash%length,三是解决了hash值与数组大小范围不匹配的问题。

两次扰动可以达到让高位低位同时参与运算,加大hash低位值得随机性,分布更均匀,减少hash冲突的概率。

3.9 HashMap扩容机制

HashMap底层维护了Node类型的数组table,默认为null,当创建对象时,将加载因子(loadfactor)初始化为0.75,当添加key、value时,通过key的哈希值得到table的索引,然后判断该索引处是否有元素,如果没有直接添加,如果有,继续判断该索引处key的值和准备加入的key是否相等,如果相等,则直接替换value,如果不相等需要判断是树结构还是链表结构,如果添加发现容量不够,则需要扩容。第一次添加,扩容table容量为16,临界值为12(16*0.75),以后再扩容,则需要扩容table容量为原来2倍(32),临界值为24,以此类推,在JDK8中,如果一条链表元素个数超过8且table数组长度>=64,会进行转化为红黑树,否则table数组扩容。

3.10 ConcurrentHashMap和Hashtable区别

两者均可保证线程安全,只是实现线程安全的方式不同,JDK7中ConcurrentHashMap底层数据结构采用分段的数组+链表实现,JDK8中底层数据结构使用数组+链表/红黑树实现,Hashtable底层数据结构采用数组+链表实现。JDK7中ConcurrentHashMap采用分段锁对整个桶数组进行分割分段(segment),每一把锁只锁容器中的一部分数据,多线程访问容器中的不同数据段的数据,不会存在锁竞争,提高并发访问效率(默认分配16个segment),在JDK8中摒弃了segment的概念,采用Node数组+链表+红黑树的数据结构直接实现,并发控制使用synchronized和CAS操作。然而HashTable使用synchronized实现独占锁保证线程安全,效率非常低下,当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态。三者区别如下图: QQ截图20220630161652.png QQ截图20220630161703.png QQ截图20220630161640.png