Java集合容器知识点整理
集合
1、什么是集合?
集合是一个用于存储数据的容器,任何集合框架都包含:对外的接口、接口的实现、集合运算的算法。
接口:表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。
实现:集合接口的具体实现,是重用性很高的数据结构。
算法:在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。
2、集合的特点
- 对象的封装,对象多了也是需要存储的,而集合用于存储对象;
- 对象的个数确定时可以使用数组,但是对象的个数不确定时可以采用集合。因为集合的长度是可变的。
3、集合和数组的区别以及相互转换
区别:
- 集合的长度是可变的,数组的长度是固定的;
- 数组既可以存储基本数据类型,也可以存储引用数据类型,而集合只可以存储引用数据类型;
- 数组存储的元素必须是同一数据类型的,而集合存储的对象可以是不同数据类型的。
转换:
- 集合转数组:使用List自带的方法toArray();
- 数组转集合:使用Arrays.asList(array)进行转换;
4、使用集合框架的好处有哪些?
- 容量自增长,集合有扩容机制,当达到一定数量时就会进行扩容;
- 提供了高性能的数据结构和算法,使得编码更加轻松,提高了程序速度和质量;
- 可以方便的扩展或改写集合,提高代码复用性和可操作性;
- 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。
5、集合的总体框架介绍


Java容器分为Collection和Map两大接口,Collection接口的子接口有Set、List、Queue三种。
我们比较常用的是Set、List、Map接口,其中Map接口不是Collection的子接口。
5.1 各类接口的简介
(1)Collection集合主要有List和Set两大接口,其次还有Queue接口。
List:它是一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,并且可以插入多个null元素,元素都有索引。常用的实现类有:ArrayList、LinkedList和Vector。
Set:它是一个无序(存入和去除顺序有可能不一致)容器,不可能存储重复元素,只允许存入一个null元素,但是必须保持元素唯一性。Set接口常用的实现类有HashSet、LinkedHashSet以及TreeSet。
Queue:和List、Set时同一级别的,都是继承Collection接口,它是一个队列,特点是先进先出(FIFO),它和堆栈一样,也是一种运算受限的线性表,后面学习的消息中间件中使用到的就是Queue。
(2)Map集合
- Map集合是一个键值对集合,存储键、值之间的映射。Key无序且唯一,Value不要求有序且允许重复。 - - Map没有继承与Collection接口,它是独立接口从Map集合中检索元素时,只要给出键对象就会返回对一你个的值对象。
- Map接口常用的实现类有:HashMap、TreeMap、Hashtable、LinkedHashMap、ConcurrentHashMap。
5.2 集合框架的底层数据结构
(1)List接口
- ArrayList:底层是Object数组
- Vector:底层也是Object数组
- LinkedList:底层使用的是双向循环列表
(2)Set接口
- HashSet(无序且唯一):底层是基于HashMap实现的,采用的是HashMap来保存元素;
- LinkedHashSet:它继承于HashSet,其内部是通过LinkedHashMap来实现的。
- TreeSet(有序、唯一):底层使用红黑树(自平衡的排序二叉树)。
(3)Map接口
- HashMap:JDK1.8之前采用的是数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)且数组长度小于64时,首先会进行扩容,否则将链表转化为红黑树,以减少搜索时间;
- LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑;
- HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
- TreeMap: 红黑树(自平衡的排序二叉树);
5.3 集合框架的特点
(1)List 有序,元素可重复
ArrayList
底层数据结构是数组,查询快,增删慢、线程不安全,效率高Vector
底层数据结构是数组,查询快,增删慢、线程安全,效率低LinkedList
底层数据结构是链表,查询慢,增删快、线程不安全,效率高
(2)Set 无序,唯一
HashSet
底层数据结构是哈希表。(无序,唯一)
如何来保证元素唯一性?
1.依赖两个方法:hashCode()和equals()LinkedHashSet
底层数据结构是链表和哈希表。(FIFO插入有序,唯一)
1.由链表保证元素有序
2.由哈希表保证元素唯一TreeSet
底层数据结构是红黑树。(唯一,有序)
1.如何保证元素排序的呢?
自然排序
比较器排序
2.如何保证元素唯一性的呢?
根据比较的返回值是否是0来决定
5.4 Java中有那些集合是线程安全的呢?
(1)Vector:比ArrayList多了个同步机制(线程安全);
(2)Hashtable:底层的方法或变量使用sycnhronized关键字修饰;
(3)ConcurrentHashMap
(4)Statck:堆栈类,先进后出;
(5)Enumeration:枚举,相当于迭代器。
5.5 List、Set、Map三者之间的区别是什么?
- List是一个有序容器、元素可重复,可以插入多个null元素,元素都有索引;
- Set是一个无序容器、元素不重复,只允许插入一个null值,并且必须保证元素唯一性;
- Map是一个键值对集合,里面存储着key-value之间的映射,Key无序且唯一,Value不要求有序且允许重复;
List和Set接口是继承于Collection接口,而Map是一个独立的接口,没有继承Collection接口。
Collection接口
List接口
一、ArrayList
1、ArrayList的简介
- ArrayList是List接口的实现类,底层使用数组的数据结构进行存储,他其实就是一个动态数组,当我们使用它来进行基本数据类型的存储时,只能存储基本数据类型的包装类,因为它底层实现的是数组对象Object[] elementData,因此不能进行基本数据类型的存储。
它有以下几个特点: - 查询效率高、增删效率低、线程不安全。但是使用频率高。
(1)为什么它的查找效率高呢?
答:因为ArrayList的底层是以数组实现,是一种随机访问模式,ArrayList实现了RandomAccess接口,因此查询的时候很快。
(2)为什么增删的效率低呢?
答:当增加/删除元素的时候,需要做一次数组拷贝的操作,如果元素比较多就比较耗性能。
(3)应用场景:适合使用在顺序添加、随机访问的场景。
2、ArrayLisy的默认长度以及扩容机制
(1)通过看ArrayList的源码可以知道ArrayList的默认长度为10(DEFAULT_CAPACITY = 10),如图源码所示:
它可以通过构造方法初始化的时候指定底层数组的大小,开始的时候默认是空数组,也就是长度为0,只有当我们去调用add方法添加数据时才会分配默认值10。
(2)然后我们都知道,数组的长度是有限的,当插入元素到一定程度的时候,就会进行扩容,打比方说我们现在有一个长度为10的数组,现在我们要新增一个元素,但是发现已经装不下了,这个时候会进行以下步骤:
- 第一步:重新定义一个长度为10+10/2的数组(定义一个原数组容量的1.5倍),也就是新增一个容量为15的数组;
- 第二步:将原数组中的数据原封不动的复制到新数组中,相当于对数组进行了拷贝,这个时候再把指向原数组的地址换到新数组。
以上两步就是ArrayList的扩容。
3、ArrayList在JDK1.7和JDK1.8版本初始化的时候有什么区别?
ArrayList在JDK1.7 之前初始化时会调用this(10)才是真正的容量为10,JDK1.7之后本身就默认走了空数组,只有第一次调用add()方法时容量才会变成10。
4、ArrayList(int initialCapacity) 会不会初始化数组大小?
会初始化数组大小,但是List的大小没有变,因为List的大小返回的时size的。
5、ArrayList常用的方法有哪些?
boolean add(E e)
将指定的元素添加到此列表的尾部。void add(int index, E element)
将指定的元素插入此列表中的指定位置。boolean addAll(Collection c)
按照指定 collection 的迭代器所返回的元素顺序,将该 collection 中的所有元素添加到此列表的尾部。boolean addAll(int index, Collection c)
从指定的位置开始,将指定 collection 中的所有元素插入到此列表中。void clear()
移除此列表中的所有元素。Object clone()
返回此 ArrayList 实例的浅表副本。boolean contains(Object o)
如果此列表中包含指定的元素,则返回 true。void ensureCapacity(int minCapacity)
如有必要,增加此 ArrayList 实例的容量,以确保它至少能够容纳最小容量参数所指定的元素数。E get(int index)
返回此列表中指定位置上的元素。int indexOf(Object o)
返回此列表中首次出现的指定元素的索引,或如果此列表不包含元素,则返回 -1。boolean isEmpty()
如果此列表中没有元素,则返回 trueint lastIndexOf(Object o)
返回此列表中最后一次出现的指定元素的索引,或如果此列表不包含索引,则返回 -1。E remove(int index)
移除此列表中指定位置上的元素。boolean remove(Object o)
移除此列表中首次出现的指定元素(如果存在)。protected void removeRange(int fromIndex, int toIndex)
移除列表中索引在 fromIndex(包括)和 toIndex(不包括)之间的所有元素。E set(int index, E element)
用指定的元素替代此列表中指定位置上的元素。int size()
返回此列表中的元素数。Object[] toArray()
按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组。T[] toArray(T[] a)
按适当顺序(从第一个到最后一个元素)返回包含此列表中所有元素的数组;返回数组的运行时类型是指定数组的运行时类型。void trimToSize()
将此 ArrayList 实例的容量调整为列表的当前大小。
6、ArrayList初始化过程源码分析
1 | /** |
总结:以上为第一次调用add方法时ArrayList底层所做的事情,下面详细总结一下:
当我们新创建一个ArrayList,没有赋初值,那么底层就调用无参构造方法,去创建一个Object数组对象,这个数组对象的默认长度为0,当我们第一次调用add方法去添加元素的时候,首先会进行一次容量的检测ensureCapacityInternal(size + 1);检测容量不够用的话就会进行扩容,将容量变成默认长度10(private static final int DEFAULT_CAPACITY = 10;),
这也就是为什么我们说的ArrayList的默认长度为10,然后扩容时是扩容为原数组的1.5倍,如果当前长度为10,需要扩容时容量就会计算为15(oldCapacity + (oldCapacity >> 1);)
7、ArrayList部分方法源码分析
1 | /** 方法的源码分析 |
二、LinkedList
1、LinkedList的简介
LinkedList类继承了AbstractSequentialList抽象类,同时继承了List、Deque、Clonable、Serializable接口,它可以被当做堆栈、队列或者双端队列进行操作。
LinkedList底层采用的是双向链表的数据结构进行存储,节点用静态内部类Node,它增删元素效率比较高,但是结构比较复杂。
源码如下:
1
2
3
4
5
6
7
8
9
10 private static class Node {
E item;
Nodenext; //后继节点
Nodeprev; //前驱节点
Node(Node prev, E element, Node next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
添加以下代码时的结构图:
2、LinkedList和ArrayList之间的区别是什么?
- 数据结构:ArrayList底层采用动态数组的数据结构,LinkedList底层采用双向链表的数据结构;
- 随机访问效率:ArrayList的随机访问效率比LinkedList要高,因为LinkedList是线性的数据存储方式,查询时需要移动指针从前往后依次查找;
- 增删效率:在非首尾的增删操作,LinkedList的效率要比ArrayList高,因为ArrayList的增删操作会影响数组内其他元素的下标。
- 内存空间:LinkedList要比ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,另一个指向后一个元素。
- 两者都是线程不安全的。
双向链表也叫双链表,它的每个数据节点中都有两个指针,分别指向直接前驱和直接后驱。因此从双向链表中任何一个节点开始都可以很方便的访问它的前驱节点和后驱节点。
三、Vector
Vector的底层数据结构和ArrayList一样是动态数组,区别在于Vector是线程安全的,它继承了AbstractList类。
Vector的默认长度为10,初始化未设置初始值时,会调用无参构造方法,然后使用this(10),直接将容量赋值为10,然后当容量不足时会进行扩容,扩容后容量为原来的两倍。10–>20
1 | public Vector() { |
3、ArrayList和Vector的区别
相同点:两者都实现了List接口,都是有序集合,底层数据结构相同,并且方法类似。
不同点:
- 线程安全:Vector使用了synchronized关键字来实现线程同步,是线程安全的,而ArrayList是非线程安全的;
- 性能:ArrayList在性能方面要由于Vector;
- 扩容:ArrayList和Vector都会根据实际需求动态的进行扩容,Vector每次扩容会增加1倍,而ArrayList增加0.5倍。
- 容量初始化方式:ArrayList在创建时是一个空数组,在第一次调用add方法时才初始化容量为10,而LinkedList在创建对象的时候就直接初始化容量为10。
- 版本不一样,Vector时JDK1.0的,ArrayList时JDK1.2的版本。
4、ArrayList、LinkedList、Vector三者之间的对比
- Vector是线程安全的容器,但是性能别ArrayList差;
- LinkedList的插入数据的速度较快;
- Vector和ArrayList的底层都是使用动态数组实现的。
Set接口
1、HashSet的工作原理
HashSet是基于HashMap实现的,底层数据结构是哈希表,主结构数组,HashSet的值存放在HashMap的key上,HashMap的Value统一为PRESENT,因此HashSet是一个无序集合,且里面的元素唯一,允许插入null元素,但不允许有重复的值。HashSet基本上都是直接调用底层的HashMap的相关方法来实现的。
2、HashSet是如何保证数据不重复的?
当调用HashSet中的add()方法添加元素时,首先会判断元素是否存在,而判断元素是否存在不仅仅要比较hash值同时还需要结合equals()方法进行比较。当调用HashSet中的add方法时会间接的使用HashMap中的put方法,我们都知道HashMap的键是唯一的,不允许重复,而从HashSet的源码可以知道添加的元素就是作为HashMap的Key,并且当HashMap中的Key/Value相同时新的Value会替换掉就的Value,HashMap比较Key是否相等时先是比较HashCode然后在比较equals,因此保证了数据不重复。
以下为HashSet的源码:
1 | private static final Object PRESENT = new Object(); |
附重点:
(1)hashcode()和equals()的一些注意点
- 如果两个对象相等,那么它们的hashcode一定相等,且对两个对象的equals方法返回为true;
- 如果两个对象有相同的hashcode,但它们不一定是相等的,equals方法返回不一定为true;
- 当equals方法被覆盖(重写)时,hashcode方法也异地你个要被覆盖(重写);
- hashCode方法的默认行为是对堆上的对象产生独特值,如果没重写hashCode,则该class的两个对象无论如何都不会相等,即使这两个对象指向相同的数据。
(2)==和equlas的对比
- ==判断两个变量或者实例是不是指向同一个内存空间,而equals方法是比较两个变量或者实例所指向的呢内存空间的值是不是相同的;
- ==是对内存地址进行比较,而equals方法是对字符串的内容进行比较;
- ==比较的是引用是否相同,而equals方法比较的是值是否相同。
3、HashSet的一些常用方法介绍
- add 添加一个元素
- clear 请发出整个HashSet中的元素
- contains 判断集合中是否包含某个元素
- remove 删除指定的元素
- size 返回集合的大小
- isEmpty 判断是否为空
4、HashSet和HashMap的区别
- 实现接口:HashMap实现了Map接口,而HashSet实现的是Set接口;
- 存储对比:HashMap存储的是键值对,而HashSet仅仅存储对象;
- 添加元素方式:HashMap调用put方法向Map中添加元素,而HashSet调用的是add方法向Set中添加元素;
- 效率:HashMap相对于HashSet来说较快,因为HashMap是使用唯一的键获取对象。
- 获取Hashcode的方式:HashMap使用的是Key计算hashcode,而HashSet是通过成员对象来计算hashcode,对于两个对象的hashcode可能相同,所以使用equals方法来判断对象是否相等。
5、HashSet的源码分析
HashSet的源码只有短短的300行,现在我们来看一下HashSet的构造方法和成员变量,源码如下:
1 | // HashSet 真实的存储元素结构 |
- 通过HashSet的构造参数我们可以看出每个构造方法都调用了对应的HashMap的构造方法,因此我们可以知道HashSet的默认初始化容量为16(源码中是1<<4),负载因子默认为0.75f,和HashMap的一样。
- 我们都知道Set集合是不允许存储重复的元素的,又由构造参数得出HashSet的底层存储结构为HashMap,那么从源码中可以得知,实现这不可重复的属性是由HashMap中存储键值对的Key来实现。
6、LinkedHashSet的简介
LinkedHashSet继承于HashSet,底层采用的链表+哈希表的数据结构,FIFO插入元素,是一个有序容器,且元素唯一,可以容纳null元素。
7、LinkedHashSet源码分析
上面HashSet源码的构造方法中有一个default权限的构造方法,该构造方法内部调用的是LinkedHashMap的构造方法,而LinkedHashMap比HashMap多了一个维护双向链表添加元素时保持的顺序。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32 // dummy 参数没有作用这里可以忽略
HashSet(int initialCapacity, float loadFactor, boolean dummy) {
map = new LinkedHashMap<>(initialCapacity, loadFactor);
}
//调用 LinkedHashMap 的构造方法,该方法初始化了初始起始容量,以及加载因子,
//accessOrder = false 即迭代顺序不等于访问顺序
public LinkedHashMap(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor);
accessOrder = false;
}
LinkedHashSet的构造方法一共有四个,统一调用了父类HashSet的 HashSet(int initialCapacity, float loadFactor, boolean dummy)构造方法。
//初始化 LinkedHashMap 的初始容量为诶 16 加载因子为 0.75f
public LinkedHashSet() {
super(16, .75f, true);
}
//初始化 LinkedHashMap 的初始容量为 Math.max(2*c.size(), 11) 加载因子为 0.75f
public LinkedHashSet(Collection<? extends E> c) {
super(Math.max(2*c.size(), 11), .75f, true);
addAll(c);
}
//初始化 LinkedHashMap 的初始容量为参数指定值 加载因子为 0.75f
public LinkedHashSet(int initialCapacity) {
super(initialCapacity, .75f, true);
}
//初始化 LinkedHashMap 的初始容量,加载因子为参数指定值
public LinkedHashSet(int initialCapacity, float loadFactor) {
super(initialCapacity, loadFactor, true);
}
以上就是LinkedHashSet的源码,从源码中就可以知道它的实现完全依赖于LinkedHashMap内部的数据结构。
7、TreeSet的简介
TreeSet是一个key唯一,有序(升序)容器,底层采用的是红黑树的数据结构,它是基于TreeMap实现的,不能存储null元素,同时也不支持同步,而且TreeMap中的key实际上就是一个TreeSet。使用TreeSet要求使用内部比较器或者外部比较器。
四、Collections工具类的部分方法介绍
- Collections.sort(list):如果list集合是字符串就会按照英文字母升序排序,如果是Integer类型就会按照数字大小排序;
- Collections.addAll(list,elements):一次性添加多个元素;
- Collections.binarySearch(list,element):二分查找,返回元素所在索引,element表示要查找的元素,使用前需要进行排序
- Collections.copy(list1,list2):将list1中的元素全部拷贝到list2集合,前提是list2集合的长度要大于或等于list1的长度,如果定义了泛型,那么集合的类型就需要一致
- Collections.fill(list,element):将element元素对list集合进行元素的填充,填充之后全部的元素都是element
- Collections.max(list):返回list集合中最大的元素
- Collections.min(list):返回list集合中最小的元素
- Collections.reverse(list):将list中的元素进行逆序排序
- Collections.synchronizedList(list):将集合转换成线程同步
Map接口
1、HashMap的简介
HashMap是我们常见的数据结构,在JDK1.7之前它是由数组+链表组成的数据结构,数组中每个地方都存储了Key-Value这样的实例,它的数据结构是一个Entry节点,在JDK1.8之后,HashMap的结构就变成了数组+链表+红黑树这么一个数据结构,把原来的Entry节点变成了Node节点,当链表长度大于8且数组长度大于64时会自动转化为红黑树。
(1)当我们使用put方法往HashMap中加入元素的时候,HashMap会利用Hash算法将Key的HashCode重新hash,并计算出当前对象元素在数组中的下标,然后将其存储进去;
(2)在存储时如果出现hash值相同的Key,这个时候就会使用equals()方法去比较它们的Key是否相同,如果Key相同,那么就进行值的覆盖,如果Key不同,那么就会将当前的Key-Value放入到链表当中。
(3)然后说到插入链表的方式,在JDK1.7之前采用的是头插法,意思就是新来的值会取代原有的值,原来的值就被顺推到链表中去;在JDK1.8之后采用的是尾插法,意思就是新来的值会往后添加到链表中去,当链表长度大于8且数组长度大于64时会自动转化为红黑树。

2、HashMap的扩容机制
HashMap的底层是使用数组进行存储的,我们都知道数组的容量是有限的,数据的多次插入,到达一定数量之后就会进行扩容,也就是resize,那什么时候resize呢?首先得有两个因素:Capacity(HashMap当前的长度,默认初始容量为16)和LoadFactor(负载因子,0.75f),要怎么理解呢?就比如说当前的数组大小为100,当你存进第76个元素的时候,判断发现需要进行resize了,也就是需要进行扩容了,HashMap的扩容不是简单扩大点容量就行了,它分为以下两步:
第一步:先去创建一个新的Entry空数组,长度为原来数组的两倍;
第二步:进行rehash,遍历原来的Entry数组,把所有的Entry重新Hash到新数组中;
整个过程就是HashMap的扩容。
3、聊一聊JDK1.7之前的头插法和JDK1.8之后的尾插法?
- JDK1.7之前使用的是头插法,就是在往链表中插入元素的时候,新来的值会取代原有的值,原有的值就会顺推到链表中去;
缺点:不好的地方在于头插法在数组进行扩容的时候,原有链表中的顺序有所改变,扩容之后重新Hash,可能会导致扩容转移后的前后链表顺序倒置,在转移的过程中修改了原有链表中的节点引用关系,这样的话在多线程操作下就会造成死循环,然后当我们使用get去取值的时候就会进入死循环。 - JDK1.8之后,插入数据的方式就变成了尾插入,使用尾插入在相同的情况下会将元素往后添加,这样就不会出现以上的情况,在扩容时会保持链表元素原有的顺序,就不会出现链表成环的问题。
4、JDK1.7是头插法,JDK1.8是尾插法,那头插法的时候,它会有死循环,这是线程不安全的原因之一吗? 那JDK1.8之后它的线程就是安全的吗?
不是的,那也不是线程安全的,因为1.8采用的是尾插法,但是没有改变它原来就是数据插入这么一个顺序,所以在这不会出现一个链表循环的这么一个过程。
5、HashMap的线程不安全,在日常开发中,你是怎么去保证他线程安全的?
一般可以使用想ConcurrentHashMap这种线程安全的一个集合容器。
6、HashMap是如何解决哈希冲突的?
7、为什么String和包装类适合当HashMap中的Key?
- String和包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少哈希碰撞的几率;
- 两者都是被final关键字修饰,即不变性,保证了Key不可被轻易更改,这样就不会有hash不同的情况出现;
- 两者内部都重写了equals方法和hashCode方法,符合HashMap的规范;
8、如果要使用对象作为HashMap中的Key,应当如何处理?
如果使用对象作为Key,那么这个对象就需要重写equals方法和hashCode方法,原因如下:
- 重写equals方法是因为需要计算存储数据的存储位置;
- 重写hashCode方法是为了保证Key在哈希表中的唯一性。
9、HashMap的默认初始化长度是16,为什么是16而不是8,32呢?
源码中写的值是1<<4,这是由于位运算性能好,直接操作内存而不需要进行进制转换,要知道计算机可是以二进制的形式做数据存储的,至于为什么是16的话,我们在创就创建HashMap的时候,阿里巴巴规范插件会提醒我们最好赋初值,而且最好是2的次幂,这样是为了位运算的方便,位运算比算数计算的效率高多了,之所以选16是为了服务将Key映射到index的算法,通过Key的HashCode值去做位运算,Hash算法的结果是均匀的,主要还是为了实现均匀分布。
10、那线程安全的,还有像HashTable啊,或者说我给他加Synchronized,或者Lock,或者用Collection.Synchronized都对他进行一个同步的操作,为什么你选择了ConcurrentHashMap?
HashTable虽然是线程安全的,但是其底层方法基本上都是使用了synchronized关键字修饰,效率低;
而ConcurrentHashMap它的并发度更高,并且它的数据结构在JDK1.8之后和HashMap一样变成了数组+链表+红黑树,它只会锁住我们目前获取到的那个Entry所在的那个节点的值,并且在上锁的时候它使用了CAS + Synchronized,再加上JDK1.6之后对Sychronized进行了一个优化和升级的过程,所以它的效率是更高的,也就是支持的并法度是更高的。
11、简单介绍一下锁升级的过程。
在锁对象的对象头里面有一个threadid字段,第一次访问的时候这个字段是为空的,然后JVM让其持有偏向锁,并且将这个字段设置为其线程id,再次进入的时候会先判断threadid是否与其线程id一致,如果一致就可以直接使用该对象,如果不一致则将偏向锁升级为轻量级锁,通过自旋一定次数来获取锁,执行一定次数之后如果还没有正常获取到想要使用的对象,此时就将轻量级锁升级为重量级锁,此过程就构成了Synchronized锁的升级。
锁的升级的目的:锁升级是为了减低了锁带来的性能消耗。在 Java 6 之后优化 synchronized 的实现方式,使用了偏向锁升级为轻量级锁再升级到重量级锁的方式,从而减低了锁带来的性能消耗。
12、HashMap和Hashtable的区别是什么?
- 线程安全:HashMap是非线程安全的,Hashtable是线程安全的,因为Hashtable的底层使用了synchronized关键字修饰;
- 效率:由于Hashtable的线程安全底层加了synchronized,所以HashMap的效率要比Hashtable高。
- 初始化容量以及扩容大小:HashMap的初始化容量为16,每次扩容时,容量会变成原来的2倍,而Hashtable的初始化容量为11,每次扩容都变为原来的2n+1。
- 底层数据结构:HashMap当链表长度大于阈值8的时候就会将链表转化为红黑树,而Hashtable没有这个机制。
- 是否允许null值:HashMap允许键和值为null,但是只允许一个null的Key,而Hashtable不允许非null的键和值存在。
- 实现方式不一样:Hashtable继承了Dictionary类(JDK1.0),而HashMap继承的是AbstractMap类;
- 迭代器不同:HashMap中的迭代器是fail-fast(快速识别机制),而Hashtable不是。
13、怎么决定是使用HashMap还是TreeMap?
- 当对数据进行插入、删除、定位查找等操作的时候,可以优先考虑使用HashMap;
- 而如果是要对一个有序的Key集合进行遍历的时候,这个时候使用TreeMap就更好一些。
14、Hashmap中的链表大小超过八个时会自动转化为红黑树,当删除小于六时重新变为链表,为啥呢?
根据泊松分布,在负载因子默认为0.75的时候,单个hash槽内元素个数为8的概率小于百万分之一,所以将7作为一个分水岭,等于7的时候不转换,大于等于8的时候才进行转换,小于等于6的时候就化为链表。
15、说一下关于ConcurrentHashMap的并法度、性能问题、数据操作,还有数据结构是什么样的么?(重点理解)
首先ConcurrentHashMap的底层是基于数组+链表组成的,不过JDK1.7和JDK1.8中具体实现稍微有点不一样,首先说一下它在1.7中的数据结构吧。
(1)JDK1.7的时候由Segment数组和HashEntry组成,和HashMap一样为数组+链表,至于并法度问题,
那是因为ConcurrentHashMap采用了分段锁技术,其中Segment继承了ReentrantLock,不会像Hasntable那样不管是put还是get操作都需要做同步处理,理论上ConcurrentHashMap支持CurrentLevel(也就是Segment数组数量)的线程并发,每当一个线程占用锁访问一个Segment时,不会影响到其他的Segment,也就是说当容量是16的话,它的并法度就是16,可以同时允许16个线程操作16个Segment并且还是线程安全的。
(2)关于它put操作:源码中它是先定位到Segment然后再进行put操作,首先第一步的时候尝试获取锁,如果获取失败肯定就有其他线程存在竞争,于是就利用自旋获取锁,简单来说就是第一步:尝试自选获取锁;第二:如果重试的次数达到了最大的扫描次数就改为阻塞锁获取,保证能够获取成功。
然后get的逻辑就比较简单了,只要将键通过Hash之后定位到具体的Segment,再通过一次Hash定位到具体的元素上,由于HashEntry中的值属性是用volatile关键词修饰的,保证了内存可见性,所以每次获取时都是最新值,ConcurrentHashMap的get方法是非常高效的,因为整个过程都不需要加锁。
(3)JDK1.7虽然支持每个Segment并发访问,但是还是存在一些问题,因为基本上还是数组+链表的方式,所以我们去查询的时候还得遍历数组,这样会导致效率很低,这个和JDK1.7的HashMap是存在一样的问题,所以在JDK1.8的时候完全优化了,JDK1.8的时候就抛弃了Segment分段锁,而是采用了CAS+Synchronized来保证并发安全性,跟HashMap很像,也把之前的HashEntry改成了Node,但是作用不变,把值和next采用了volatile去修饰,保证了可见性,同时引入了红黑树,在链表大于一定值的时候会转换(默认值是8),这就是它的存取操作。
16、ConcurrentHashMap在进行put操作的时候还是比较复杂的,大致分为以下步骤:
(1)根据key计算出HashCode;
(2)判断是否需要进行初始化;
(3)即为当前Key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功;
(4)如果当前位置的哈希码等于Moved等于-1的话,就需要进行扩容;
(5)如果都不满足,则利用Synchronized锁写入数据;
(6)如果数量还是大于Treeify_Threshold就要转换成红黑树;
17、HashMap和ConcurrentHashMap的区别是什么?
(1)线程安全:ConcurrenrHashMap是线程安全的,HashMap是非线程安全的;
(2)HashMap的键值对允许有null,但是ConcurrentHashMap都不允许。
18、HashTable和ConcurrentHashMap的区别是什么?
(1)底层数据结构:ConcurrenrHashMap在JDK1.7 之前采用的是分段的数组+链表,JDK1.8之后采用的是数据+链表/红黑树;而HashTable在JDK1.8之前都是采用的数组+链表的形式,数组采用的是HashMap的主体,链表主要是为了解决哈希冲突而存在的;
(2)实现线程安全的方式:① 在JDK1.7的时候,ConcurrentHashMap(分段锁) 对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。) 到了 JDK1.8 的时候已经摒弃了Segment的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后 对 synchronized锁做了很多优化) 整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到 Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本;②Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
19、为什么重写equals方法的时候需要重写hashCode方法?使用HashMap举个例子不?
因为在java中,所有的对象都是继承于Object类。Ojbect类中有两个方法equals、hashCode,这两个方法都是用来比较两个对象是否相等的。
在未重写equals方法我们是继承了object的equals方法,那里的 equals是比较两个对象的内存地址,显然我们new了2个对象内存地址肯定不一样
对于值对象,==比较的是两个对象的值
对于引用对象,比较的是两个对象的地址
在HashMap中是通过key的hashCode去寻找元素索引下标index的,加入某个index为2的是一个链表,我们去get的时候是根据key去hash然后计算出index,重写了equals方法后,去找链表中的元素的时候是找不到的,因此我们重写了equals方法建议一定要重写hashCode方法,以此保证相同的对象返回相同的hash值,不同的对象返回不同的值,不然一个链表的hashCode都一样的,就乱套了。
20、为什么HashTable不允许键值为null?
因为Hashtable使用的是安全失败机制(fail-safe),这种机制会使你此次读到的数据不一定是最新的数据。如果你使用的是null,就会使你无法判断对应的key是不存在还是为空,因为你无法再调用一次contain(key)来对key是否存在进行判断,ConcurrentHashMap同理。
泛型
1、什么是泛型?
- Java泛型设计规则:只要在编译期间没有出现警告,那么运行期间就不会出现ClassCastException异常;
- 泛型:把类型明确的工作推迟到创建对象或者是调用方法的时候才去明确的特殊的类型。
参数化类型:
- 把类型当做是参数一样进行传递;
- <数据类型>只能是引用类型;
相关术语
- ArrayList
中的E称为类型参数变量; - ArrayList
中的Integer称为实际类型参数; - 整个ArrayList
称为泛型类型; - 整个ArrayList
称为参数化的类型ParaneterizedType;
2、为什么需要泛型
没有泛型的时候:
Collection、Map集合对元素的类型是没有任何限制的,假设一个Collection集合中装载的全是一个Person对象,但是外面把Pig对象存储也到集合中,这个样是没有任何语法错误的,但是把对象扔进集合中,集合是不知道元素的类型是什么样的,仅仅知道是Object类型的,因此使用get()的时候,返回的是Object。获取该对象的时候还需要进行强制类型转换。有泛型以后:
限制了存储对象的类型,代码变得更加简洁,因为获取元素的时候不需要进行强制类型转换了;
程序更加健壮,因为只要编译时期没有警告,那么运行时期就不会出现ClassCastException异常;
代码的可读性和稳定性更强,因为载编写集合的时候就限制的类型。
2.1 有了泛型后使用增强for遍历集合
由于我们在创建集合的时候,明确了集合的类型,因此我们可以使用增强for来遍历集合
//创建一个集合对象
ArrayList
list.add(“hello”);
list.add(“world”);
list.add(“oldou”);
//由于明确了类型.我们可以增强for进行遍历
for (String str : list) {
System.out.println(str);
}
3、泛型的用法
3.1 泛型类
- 定义:将泛型定义在类上就是泛型类,载使用该类的时候,类型才能确定下来。
- 优点:当用户明切了类型,这个类就代表着什么类型,就不用再担心强转以及运行时转化异常的问题了。
泛型定义代码示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15/*
1:把泛型定义在类上
2:类型变量定义在类上,方法中也可以使用
*/
public class ObjectTool<T> {
private T obj;
public T getObj() {
return obj;
}
public void setObj(T obj) {
this.obj = obj;
}
} - 当用户想要使用哪种类型的时候,就在创建的时候指定类型,使用的时候该类就会自动转换成用户想要的使用类型。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public static void main(String[] args) {
//创建对象并指定元素类型
ObjectTool<String> tool = new ObjectTool<>();
tool.setObj(new String("钟福成"));
String s = tool.getObj();
System.out.println(s);
//创建对象并指定元素类型
ObjectTool<Integer> objectTool = new ObjectTool<>();
/**
* 如果我在这个对象里传入的是String类型的,它在编译时期就通过不了了.
*/
objectTool.setObj(10);
int i = objectTool.getObj();
System.out.println(i);
}3.2 泛型方法
将泛型定义在方法上就叫泛型方法,泛型是先定义后使用的,泛型方法定义如下所示:泛型类时拥有泛型这个特性的类,由于它本质上还是一个Java类,所以它是可以被继承的。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15//定义泛型方法..
public <T> void show(T t) {
System.out.println(t);
}
使用时传递进来的是什么类型,返回值就是什么类型的。
public static void main(String[] args) {
//创建对象
ObjectTool tool = new ObjectTool();
//调用方法,传入的参数是什么类型,返回值就是什么类型
tool.show("hello");
tool.show(12);
tool.show(12.5);
}
而被继承分为两种情况: - 子类明切泛型类的类型参数变量
- 子类不明切泛型类的类型参数变量
(1)子类明切泛型类的类型参数变量
- 泛型接口
1
2
3
4
5
6
7/*
把泛型定义在接口上
*/
public interface Inter<T> {
public abstract void show(T t);
} - 泛型接口的实现类(2)子类不明确泛型类的类型参数变量
1
2
3
4
5
6
7
8
9
10
11/**
* 子类明确泛型类的类型参数变量:
*/
public class InterImpl implements Inter<String> {
@Override
public void show(String s) {
System.out.println(s);
}
} - 当子类不明切泛型类的类型参数变量时,外界使用子类的时候,需要传递类型参数变量进来,在实现类上需要定义处类型参数变量。
1
2
3
4
5
6
7
8
9
10
11
12
13/**
* 子类不明确泛型类的类型参数变量:
* 实现类也要定义出<T>类型的
*
*/
public class InterImpl<T> implements Inter<T> {
@Override
public void show(T t) {
System.out.println(t);
}
} - 测试代码:需要注意的是:
1
2
3
4
5
6
7
8
9
10public static void main(String[] args) {
//测试第一种情况
//Inter<String> i = new InterImpl();
//i.show("hello");
//第二种情况测试
Inter<String> ii = new InterImpl<>();
ii.show("100");
} - 实现类重写父类的方法时,返回值的类型要和父类一致。
- 类上生命的泛型追非静态成员有效。
3.3 通配符?
除了使用
- 通配符的出现时喂了指定泛型中的类型范围。
通配符有一下三种形式: - <?> 被称为无限定通配符;
- <? extends T> 被称为有上限通配符;
- <? super T> 被称为又下限通配符;
3.4 类型擦除
泛型时Java1.5版本才引进的概念,在此之前是没有泛型的概念的,但显然泛型代码能够很好的和之前版本的代码兼容,这是因为泛型信息只存在于代码的编译阶段,在进入JVM之前,与泛型想改的信息都会被擦除掉,这就叫做类型擦除。
首先来个小题目:
(1)判断一下以下代码的输出情况:
1 | List<String> l1 = new ArrayList<String>(); |
输出结果为true,这是因为List
(2)类型String和Integer去哪了呢?
是由于泛型转译
3.5 泛型中需要注意的地方
泛型类或泛型方法中是不能直接使用8种基本的数据类型的,而是使用它们的包装类;
例如:Listlist = new ArrayList<>(); Java中不能创建具体类型的泛型数组