集合
在jdk的java.util包下,集合主要派生自Collection接口和Map接口,Collection接口属于对象的集合,Map接口属于键值对的集合。
实现自Collection接口的又有List接口(有序可重复)和Set接口(不可重复)。
List接口的实现类有LinkedList(基于链表,增删快,查询慢,线程不安全),ArrayList(基于数组,查询快,增删慢,线程不安全),Vector(基于数组,线程安全,读写都加锁,每个方法都加了synchronized),CopyOnWriteArrayList(线程安全,写加锁,读不加锁,写时拷贝一个新的数组,写结束后数组指针指向新数组),Stack(基于数组,继承自Vector,线程安全的栈,先进后出)。
Set接口的实现类有HashSet(基于HashMap实现),TreeSet(底层红黑树,基于TreeMap实现),LinkedHashSet(继承自HashSet,保留元素插入顺序)。
实现自Map接口的HashMap(基于哈希映射,线程不安全,key唯一,key和value都可以为null),Hashtable(key和value都不能为null,大部分方法加锁,线程安全,效率低),ConcurrentHashMap(key或value不能为空,根据key的hash值找到桶的位置,锁住头节点,写操作后释放锁;针对读操作采用volatile修饰Node<K, V>[]类型的集合元素),TreeMap(底层红黑树,元素可排序),LinkedHashMap(基于哈希表和双向链表,保留元素插入顺序)。
Collection<E>是Java集合框架中的基本接口;
Collections是Java集合框架提供的一个工具类,其中包含了用于操作或返回集合的静态方法。
比如Collections.sort(list):对指定的list按升序进行排序,list中的元素要实现Comparable接口。
Collections.reverse(list):对指定的list进行反转。
Collections.copy(list,li):将li元素拷贝到list中。
Collections.synchronizedMap(Map):创建线程安全的Map集合。
1.1 HashMap
-
jdk1.7中HashMap默认采用数组+单链表方式存储元素,当元素出现哈希冲突时,会存储到该位置的单链表中。1.8中,除了数组和单链表外,当单链表中元素个数超过8个时(且元素数量大于64(小于64时会扩容而不是转红黑树)),会进而转化为红黑树存储,巧妙地将遍历元素的时间复杂度从O(n)降低到了O(logn))。1.7扩容时需要重新计算哈希值和索引位置,1.8并不重新计算哈希值,巧妙地采用和扩容后容量进行**&**操作来计算新的索引位置【(e.hash & oldCap) == 0/1,扩容后元素在新数组中的位置不变或+oldCap这两种】
-
执行构造函数时,存储元素的数组并不会进行初始化,而是在第一次放入元素的时候,才会进行初始化操作。创建HashMap对象时,仅仅计算初始容量tableSizeFor()和新增阈值。
-
HashMap在jdk1.7中采用头插入法,在扩容时会改变链表中元素原本的顺序,以至于在并发场景下导致链表成环的问题。而在jdk1.8中采用尾插入法,在扩容时会保持链表元素原本的顺序,就不会出现链表成环的问题了。但并发下put方法还是会出现数据覆盖的问题。
1.1.1 HashMap的底层数据结构?
JDK1.7:数组+单链表。JDK1.8:数组+单链表(单链表中元素个数超过8个且数组节点数超过64时转为红黑树存储,否则扩容)
链表查询O(N),插入Olog1;
红黑树查询插入都是OlogN
int hash = hash(key);//根据key计算哈希值
int i = indexFor(hash, table.length);//根据哈希值和数组长度计算在数组中的索引位置
//(n - 1) & hash数组长度和hash值相与计算index
1.1.2 HashMap的存取原理?
通过获取key对象的hashcode计算出该对象的哈希值,通过该哈希值与数组长度减去1进行位与运算(n-1 & hash),得到数组的位置,没有冲突就新建节点,当发生hash冲突时,如果key值一样,则会替换旧的key的value,key不一样则新建next链表结点,当链表的长度超过8且数组容量大于64(树形化最小容量),则转换为红黑树存储。
1.1.3 Java7和Java8的区别?
JDK1.7:数组+单链表。JDK1.8:数组+单链表(单链表中元素个数超过8个时转为红黑树存储)。
JDK1.7扩容时转移原来的数据到newtable中采用头插法,1.8改为尾插法。
JDK1.7创建hashmap对象会创建一个长度为16的Entry[] table用来存储键值对数据。jdk1.8之后不是在构造方法创建了,而是在第一次调用put方法时才进行创建Node[] table。
1.1.4 为啥会线程不安全?
jdk1.7中,在多线程环境下,(头插法)扩容时会造成环形链或数据丢失。
jdk1.8中,在多线程环境下,PUT方法会发生数据覆盖的情况。
1.1.1 有什么线程安全的类代替么?
currentHashMap 以及 hashTable
1.1.6 默认初始化大小是多少?为啥是这么多?为啥大小都是2的幂?
设置太大,就会浪费内存空间;设置太小,放几个元素就会扩容。
与index计算公式有关【index = (n - 1) & hash】,16-1=11的所有二进制位全为1,这种情况下,index的结果等同于hash后几位的值,只要计算出的hash本身分布均匀,Hash算法的结果就是均匀的。
数组下标 = (数组长度 - 1)& hash。当数组长度即n是2的幂时,n-1的二进制则为0000 1111…,保证了后几位全都是1,此时再和hash值相与,就保证了结果一定是hash值的后几位。
①为什么要下标结果和hash值后几位相同?—这样可以保证只要hash值分布足够均匀,数组下标就足够分散。
②这里的后几位是多少位?(为什么用数组长度-1?) —n-1,当n是8时,8-1的二进制为0000 1111。此时与hash值相与,后四位得到的结果一定小于等于1111,也就保证了结果一定小于8且大于等于0。同时也就保证了数组下标大于等于0,小于数组长度n。不会出现数组下标越界的情况。
方便位运算。
1.1.7 HashMap的扩容方式?负载因子是多少?为什么是这么多?
当前插入元素大于Capacity*(HashMap当前长度)* x LoadFactor*(负载因子,默认值0.71f)*时进行扩容。
①:创建一个原数组两倍长度的空数组【确保2的幂】;
②:jdk1.7:遍历原数组,把所有元素重新计算hash和index到新数组。
jdk1.8:根据(e.hash & oldCap=0/1)决定是原下标位置,还是原下标+oldCap下标位置。1
0.71----当负载因子过大的时候,意味着会出现大量的Hash的冲突,底层的红黑树变得异常复杂。对于查询效率极其不利(时间换空间)。当负载因子过小,数组所能存储的元素就会变少(空间换时间)。负载因子是0.71的时候,存储元素比较多,避免了过多的Hash冲突,使得底层的链表或者是红黑树的高度比较低,提升了空间效率。threshold = loadFactor * capacity。【0.71正好是3/4,而capacity又是2的幂。所以,两个数的乘积都是整数】
1.1.8 HashMap的主要参数都有哪些?
/**
* 默认初始化容量,必须是2的次方
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
/**
* 最大容量。即HashMap的数组容量必须小于等于 1 << 30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;
/**
* 默认的负载因子
*/
static final float DEFAULT_LOAD_FACTOR = 0.71f;
/**
* 树形化阈值;即当链表的长度大于8的时候,会将链表转为红黑树,优化查询效率。链表查询的时间复杂度为
* o(n) , 红黑树查询的时间复杂度为 o(log n)
*/
static final int TREEIFY_THRESHOLD = 8;
/**
* 解树形化阈值;其实就是当红黑树的节点的个数小于等于6时,会将红黑树结构转为链表结构。
*/
static final int UNTREEIFY_THRESHOLD = 6;
/**
* 树形化的最小容量;前面我们看到有一个树形化阈值,就是当链表的长度大于8的时候,会从链表转为红黑 * 树。其实不一定是这样的。转为红黑树有两个条件:
* ① 链表的长度大于8
* ② HashMap数组的容量大于等于64
* 需要当上述两个条件都成立的情况下,链表结构才会转为红黑树。
*/
static final int MIN_TREEIFY_CAPACITY = 64;
1.1.9 HashMap是怎么处理hash碰撞的?
put流程:
如果是第一次put调用 resize初始化数组;
首先根据key的hashcode内存地址与自己右移16位进行异或得到hash值;
hash值与(数组容量-1)相与,求出数组下标,下标位置为空直接新建节点;
下标位置有值且key值相同则直接替换;
key值不同说明出现hash冲突,是树节点就用树节点添加方式加入;
不是树节点就添加到链表上,添加之后校验链表节点是否超过8,并且数组容量是否超过64,是则转为红黑树,否则将数组扩容;
put后如果节点数量大于阈值则扩容。
1.1.10 hash的计算规则?
将对象的key.hashcode()方法返回的hash值,进行无符号的右移16位,并与原来的hash值进行按位异或操作,目的是将hash的低16bit和高16bit做了一个异或,使返回的值足够散列
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
因为当前hashcode方法计算的散列值仍会出现较多冲突,由于数组的边界影响,hashcode值的高位几乎不会用到。所以利用右移16位后的hashcode与自身进行异或,提升hash值的散列性,减少系统的损失。
1.1.11 如何解决初始化,输入的值不是2的n次幂
tableSizeFor()方法,将值的二进制数,从左边第一个出现的1开始,右边的所有值都通过位或运算变成1,使得可以找出比当前值大一点点的2的幂的数
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
1.1.12 Set集合是如何保证元素不重复的?
因为HashSet的底层实现都是创建了一个HashMap,而HashMap的add方法,是将元素作为map的key进行存储的,map的key是不会重复的,所以HashSet中的元素也不会重复。
1.1.13 重写equals方法,还必须要重写hashcode方法
使用hashcode方法将对象定义到一个地址上,hashcode值不同则对象一定不同;hashcode相同之后再用equals比较,减少了equals的使用。
如果重写了equals方法比较值是否相等,而没重写hashcode方法,会出现值相等的对象equals相同,但前一步的hashcode不同,结果导致equals不相等。
1.1.14 看hashmap源码学到了什么?
- 计算机位运算更快
- hashmap平时用的最多,看源码能理解原理,出现问题知道原因在哪。
- 也学到了编码方式。
1.1.15 线程安全的map
-
jdk1.7中,在多线程环境下,(头插法)扩容时会造成环形链或数据丢失。
-
jdk1.8中,在多线程环境下,PUT方法会发生数据覆盖的情况。
实现线程安全的map:
-
Collections.synchronizedMap(new HashMap<>());
将map对象付给mutex对象,之后每次操作map都会在代码块加上synchronized(mutex)。
-
Hashtable
对大部分方法加上synchronized,缺点就是效率低,有时候不需要加锁,却仍要线程等待。
-
ConcurrentHashMap
在jdk1.7中ConcurrentHashMap的底层数据结构是数组(Segment)加链表(HashEntry)
对于ConcurrentHashMap的添加,删除操作,在操作开始前,线程都会获取Segment的互斥锁;操作完毕之后,才会释放。而对于读取操作,它是通过volatile去实现的,HashEntry数组是volatile类型的
jdk1.8使用红黑树O(logn)来优化链表O(n);并取消了segment数组,直接用Node[]保存数据,降低锁的粒度,减少并发冲突的概率。通过cas的方式添加元素。
List
-
ArrayList和LinkedList区别
Arraylist是基于数组实现的,随机访问较快,插入慢,因为要移动插入位置后面的所有元素。
LinkedList是基于链表实现的,查询慢,但是插入删除快,因为不需要移动元素,也不用扩容。
查询多用Arraylist,删除插入多用Linkedlist。
-
Arraylist
jdk1.8中,初始化时容量为空,第一次添加元素才扩大为10;也可以初始化时自定义容量。
扩容:
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 oldCapacity + (oldCapacity >> 1),也就是旧容量的 1.5 倍。10->15->22>。。。
扩容操作需要调用 Arrays.copyOf() 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
ArrayList动态扩容的全过程。如果通过无参构造的话,初始数组容量为0,当真正对数组进行添加时,才真正分配容量。每次按照1.5倍(位运算)的比率通过copeOf的方式扩容。 在JKD1.6中实现是,如果通过无参构造的话,初始数组容量为10.每次通过copeOf的方式扩容后容量为原来的1.5倍加1
-
不允许插入空值的集合
会自然排序的都不能插入空值,比如concurrenthashmap、treeset、treemap基于红黑树不能插空值,hashtable。
-
Treemap注意事项
不能插入null值,因为底层数据结构是红黑树;
作为key的对象必须实现comparable接口,实现排序;
因为要比较和排序,所以key必须是同一种类型。
-
线程安全的list
-
Collections.synchronizedList(new ArrayList())
代码块加synchronized(mutex),类似于Collections.synchronizedMap。
-
vector
每个方法都用synchronized修饰,太慢了。
-
CopyOnWriteArrayList
- 在修改时,复制出一个新数组,修改的操作在新数组中完成,最后将指针指向新数组。
- 写时方法内部自动加锁,读不加锁,因为读的是旧数组
缺点:
- 占用太多内存,每次修改都要复制出一个新的数组
- 只能保证数据最终一致性,不能保证数据实时一致性
-