1. Java
中的List
、Set
、Map
有何区别?
Java
中的集合主要分为三类:List
(列表)、Set
(集合)、Map
(键值对), 三者的关系如下所示:
以下是Collection
的源码:
public interface Collection<E> extends Iterable<E> {
int size();
boolean isEmpty();
boolean contains(Object o);
Iterator<E> iterator();
Object[] toArray();
<T> T[] toArray(T[] a);
boolean add(E e);
boolean remove(Object o);
boolean containsAll(Collection<?> c);
boolean addAll(Collection<? extends E> c);
boolean removeAll(Collection<?> c);
boolean retainAll(Collection<?> c);
void clear();
boolean equals(Object o);
int hashCode();
default boolean removeIf(Predicate<? super E> filter) {
Objects.requireNonNull(filter);
boolean removed = false;
final Iterator<E> each = iterator();
while (each.hasNext()) {
if (filter.test(each.next())) {
each.remove();
removed = true;
}
}
return removed;
}
...
}
List
中的元素以线性方式存储,是有顺序的,值可以重复;Map
中的元素是一种键和值映射的集合,存储是无序的,每一个元素都是一个键值对,其中,键不允许重复,值可以重复;Set
中的元素是无序的,值不允许重复。但元素在集合中的位置是由元素的hashCode
决定,即位置是固定的(但是这个位置不是用户可以控制的,所以对于用户来说Set
中的元素还是无序的)
2 ArrayList
和LinkedList
的区别?
ArrayList
是基于数组的数据结构,LinkedList
是基于链表的数据结构;ArrayList
适用于查询操作;LinkedList
适用于插入和删除操作;
3 请说一下HashMap
与Hashtable
的区别
Hashtable
是个过时的集合类,存在于Java API
中很久了。在Java 4
中被重写了,实现了Map
接口,所以自此以后也成了Java
集合框架中的一部分。
3.1 父类不同
Hashtable
是基于陈旧的Dictionary
类的,HashMap
是Java 1.2
引进的,继承了抽象类AbstractMap
实现了Map
接口:
public class Hashtable<K,V> extends Dictionary<K,V>
implements Map<K,V>, Cloneable, java.io.Serializable { }
public class HashMap<K,V> extends AbstractMap<K,V> implements Map<K,V>, Cloneable, Serializable { }
3.2 线程安全不一样
Hashtable
中的方法是同步的,而HashMap
中的方法在默认情况下是非同步的。在多线程并发的环境下,可以直接使用Hashtable
,但是要使用HashMap
的话就需要增加同步处理:
3.3 允不允许null
值
Hashtable
中,key
和value
都不允许出现null
值,否则会抛出NullPointerException
异常。而在HashMap
中,null
可以作为键,这样的键只有一 个;可以有一个或多个键所对应的值为null
。当get()
方法返回null
值时,即可以表示HashMap
中没有该键,也可以表示该键所对应的值为null
。因此,在HashMap
中不能由get()
方法来判断HashMap
中是否存在某个键, 而应该用containsKey()
方法来判断。
3.4 遍历方式的内部实现上
不同Hashtable
、HashMap
都使用了Iterator
。而由于历史原因,Hashtable
还使用了Enumeration
的方式 。
3.5 哈希值的使用不同
Hashtable
直接使用对象的hashCode
。而HashMap
重新计算hash
值。
3.6 内部实现方式的数组的初始大小和扩容的方式不一样
HashTable
中的hash
数组初始大小是11
,增加的方式是old * 2 + 1
。HashMap
中hash
数组的默认大小是16
,而且一定是2
的指数。
4 ArrayList
的扩容机制
先来看看构造方法,以下是无参数构造方法:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;
// Constructs an empty list with an initial capacity of ten.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
}
这是无参数的构造方法,是对elementData
(元素的数组)进行赋值DEFAULTCAPACITY_EMPTY_ELEMENTDATA
(空数组,我们暂时叫做默认空数组),明明是空数组,但是注释确实创建容量是10
。
再来看带参数的构造方法:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA;
} else {
throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);
}
}
}
再看另一个构造方法:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
}
就是直接将集合转为数组赋值给elementData
,同时对size
赋值,并且如果size
不等于0
时,c.toArray might (incorrectly) not return Object[] (see 6260652)
;等于0
时,直接赋值空的数组。
接着看add
方法,先看一个参数的:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
public boolean add(E e) {
ensureCapacityInternal(size + 1); // Increments modCount!!
elementData[size++] = e;
return true;
}
}
就是ensureCapacityInternal(size + 1)
并且将e
添加到size++
的位置,我们来看ensureCapacityInternal(size + 1)
方法:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
}
DEFAULT_CAPACITY = 10
。也就是当空参数时创建ArrayList
时,minCapacity
与DEFAULT_CAPACITY
的最大值,显然创建无参数构造方法时minCapacity = 0
。这时结果= 10
,然后看ensureExplicitCapacity(minCapacity)
:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
}
显然调用无参数构造方法时minCapacity - elementData.length > 0
是成立的,我们再看grow(minCapacity)
:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
}
oldCapacity
是元素个数newCapacity
是oldCapacity + oldCapacity / 2
,即oldCapacity
的1.5
倍。当1.5
倍的元素个数小于minCapacity
时,newCapacity = minCapacity
。显然创建无参数ArrayList
就是这种情况,这就解决了开头的第一个问题。调用无参数构造方法创建的是10
个元素的长度。
我们再看当1.5
倍元素个数大于MAX_ARRAY_SIZE
时,newCapacity = hugeCapacity(minCapacity)
:
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable {
private static int hugeCapacity(int minCapacity) {
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE :
MAX_ARRAY_SIZE;
}
}
到这里我们可以总结一下了:无参构造方法调用add
之后创建的是10
个长度的数组。有参数则直接创建指定长度的数组。扩容时,小于MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8
一次扩容1.5
倍,超过则直接Integer.MAX_VALUE
。
5 HashMap
的实现原理
HashMap
实际上是一个“链表散列”的数据结构,即数组和 链表的结合体。 它是基于哈希表的Map
接口的非同步实 。
- 数组:存储区间连续,占用内存严重,寻址容易,插入删除困难;
- 链表:存储区间离散,占用内存比较宽松,寻址困难,插入删除容易;
HashMap
综合应用了这两种数据结构,实现了寻址容易,插入删除也容易;
例如我们以下图为例,看一下HashMap
的内部存储结构:
关于HashMap
的存取过程,可参照下图:
- 判断键值对数组
table[i]
是否为空或为null
,否则执行resize()
进行扩容; - 根据键值
key
计算hash
值得到插入的数组索引i
,如果table[i]==null
,直接新建节点添加,转向6
,如果table[i]
不为空,转向3
; - 判断
table[i]
的首个元素是否和key
一样,如果相同直接覆盖value
,否则转向4
,这里的相同指的是hashCode
以及equals
; - 判断
table[i]
是否为treeNode
,即table[i]
是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5
; - 遍历
table[i]
,判断链表长度是否大于8
,大于8
的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key
已经存在直接覆盖value
即可; - 插入成功后,判断实际存在的键值对数量
size
是否超多了最大容量threshold
,如果超过,进行扩容。
6 LinkedHashMap
的工作原理和使用方式
查看LinkedHashMap
源码发现是继承HashMap
实现Map
接口。也就是HashMap
的方法LinkedMap
都有。LinkHashMap
与HashMap
的主要区别是:LinkedHashMap
是有序的,HashMap
是无序的。 LinkedHashMap
通过维护一个双向链表实现有序,也正是因为要维护这个链表,内存上有更大的开销。
线性结构分为顺序结构和链表结构:
- 顺序结构:在内存中是一块完整有序内存。所以我们在查询的时候时候直接索引
index
,便可找到要查询的数据,速度非常快,缺点是插入删除慢。有点类似班级排队时(一列纵队),每个人都知道自己在第几个位置。老师只要说第三个位置,那这个同学立马知道老师要找的是自己。这时候要插入一个同学到第二个位置,所以之前第二个位置开始往后的每个同学的位置都要+1
。所以比较慢; - 链表结构:通过结点头记录该结点的上一个结点和下 一个下一个结点(就是传统的双链表,单链表就是只 记录下一个结点,循环链表就是最后一个结点的下一 个结点指向第一个结点)。正是因为这种关系,所以 链表结构不需要一块完整的内存,而且插入删除相对快,但是查询相对慢。但是因为要维护结点头,所以 内存开销相对大一点。有点类似于班级排队时,每个 人虽然不知道自己的位置,但是知道自己前面是谁和后面是谁。当要插入一个同学
b
时到c
前面时,只要c
同学记住自己之前是a
,现在换成b
。b
记住自己前面是a
,后面是c
。所以想对来说插入很快。删除类似。但 是当老师按位置查询时,就要先从第一个开始计数, 知道找到老师要找的数字。所以查询慢。
7 ConcurrentHashMap
的理解
并发集合常见的有ConcurrentHashMap
、ConcurrentLinkedQueue
、ConcurrentLinkedDeque
等。并发集合位于java.util.concurrent
包下 ,是jdk1.5
之后才有 的。
在Java
中有普通集合、同步(线程安全)的集合、并发集合。普通集合通常性能最高,但是不保证多线程的安全性和并发的可靠性。线程安全集合仅仅是给集合添加了synchronized
同步锁,严重牺牲了性能,而且对并发的效率就更低了,并发集合则通过复杂的策略不仅保证了多线程的安全又提高的并发时的效率ConcurrentHashMap
是线程安全的HashMap
的实现,默认构造同样有initialCapacity
和loadFactor
属性,不过还多了一个concurrencyLevel
属性,三属性默认值分别为16
、0.75
及16
其内部使用锁分段技术,维持这锁Segment
的数组, 在Segment
数组中又存放着Entity[]数
组,内部hash
算法将数据较均匀分布在不同锁中。
put
操作:并没有在此方法上加上synchronized
,首先对key.hashcode
进行hash
操作,得到key
的hash
值。hash
操作的算法和map
也不同,根据此hash
值计算并获取其对应的数组中的Segment
对象(继承自ReentrantLock
),接着调用此Segment
对象的put
方法来完成当前操作。ConcurrentHashMap
基于concurrencyLevel
划分出了多个Segment
来对key-value
进行存储,从而避免每次put
操作都得锁住整个数组。在默认的情况下,最佳情况下可 允许16
个线程并发无阻塞的操作集合对象,尽可能地减少并发时的阻塞现象。
get(key)
首先对key.hashCode
进行hash
操作,基于其值找到对应的Segment
对象,调用其get
方法完成当前操作。而Segment
的 get
操作首先通过hash
值和对象数组大小减1
的值进行按位与操作来获取数组上对应位置的HashEntry
。
在这个步骤中,可能会因为对象数组大小的改变,以及数组上对应位置的HashEntry
产生不一致性,那么ConcurrentHashMap
是如何保证的?
对象数组大小的改变只有在put
操作时有可能发生,由于HashEntry
对象数组对应的变量是volatile
类型的,因此可以保证如 HashEntry
对象数组大小发生改变,读操作可看到最新的对象数组大小。
在获取到了HashEntry
对象后,怎么能保证它及其next
属性构成的链表上的对象不会改变呢?
这点ConcurrentHashMap
采用了一个简单的方式,即HashEntry
对象中的hash
、key
、next
属性都是final
的,这也就意味着没办法插入一个HashEntry
对象到基于next
属性构成的链表中间或末尾。这样就可以保证当获取到HashEntry
对象后,其基于next
属性构建的链表是不会发生变化的。
ConcurrentHashMap
默认情况下采用将数据分为16
个段进行存储,并且16
个段分别持有各自不同的锁Segment
,锁仅用于put
和 remove
等改变集合对象的操作,基于volatile
及HashEntry
链表的不变性实现了读取的不加锁。这些方式使得ConcurrentHashMap
能够保持极好的 并发支持,尤其是对于读远比插入和删除频繁的Map
而言,而它采用的这些方法也可谓是对于Java
内存模型、并发机制深刻掌握的体现。
总结:ConcurrentHashMap
比起HashMap
,是线程安全的,比起Hashtable
是高效的。