List(有序,可重复)
ArrayList
底层数据结构是数组,查询快,增删慢(因为:增删后涉及到其他数据的位移)
线程不安全,效率高
Vector
底层数据结构是数组,查询快,增删慢(因为:增删后涉及到其他数据的位移)
线程安全,效率低
LinkedList
底层数据结构是双向链表,查询慢,增删快
线程不安全,效率高
Set(无序,唯一)
HashSet
底层数据结构是哈希表。
哈希表依赖两个方法:hashCode()和equals()
执行顺序:
首先判断hashCode()值是否相同
是:继续执行equals(),看其返回值
是true:说明元素重复,不添加
是false:就直接添加到集合
否:就直接添加到集合
最终:
自动生成hashCode()和equals()即可
LinkedHashSet
底层数据结构由链表和哈希表组成。
由链表保证元素有序。
由哈希表保证元素唯一。
TreeSet
底层数据结构是红黑树。(是一种自平衡的二叉树)
如何保证元素唯一性呢?
根据比较的返回值是否是0来决定
如何保证元素的排序呢?
两种方式
自然排序(元素具备比较性)
让元素所属的类实现Comparable接口
比较器排序(集合具备比较性)
让集合接收一个Comparator的实现类对象
A:Map集合的数据结构仅仅针对键有效,与值无关。
B:存储的是键值对形式的元素,键唯一,值可重复。
HashMap
底层数据结构是:
jdk1.8以下:(数组+单向链表)哈希表
jdk1.8+:(数组+[单向链表 / 红黑树])哈希表,根据情况会选择链表和红黑树之间进行转换
线程不安全,效率高
哈希表依赖两个方法:hashCode()和equals()
执行顺序:
首先判断hashCode()值是否相同
是:继续执行equals(),看其返回值
是true:说明元素重复,不添加
是false:就直接添加到集合
否:就直接添加到集合
最终:
自动生成hashCode()和equals()即可
LinkedHashMap
底层数据结构由链表和哈希表组成。
由链表保证元素有序。
由哈希表保证元素唯一。
Hashtable
底层数据结构是哈希表。线程安全,效率低
哈希表依赖两个方法:hashCode()和equals()
执行顺序:
首先判断hashCode()值是否相同
是:继续执行equals(),看其返回值
是true:说明元素重复,不添加
是false:就直接添加到集合
否:就直接添加到集合
最终:
自动生成hashCode()和equals()即可
TreeMap
底层数据结构是红黑树。(是一种自平衡的二叉树)
如何保证元素唯一性呢?
根据比较的返回值是否是0来决定
如何保证元素的排序呢?
两种方式
自然排序(元素具备比较性)
让元素所属的类实现Comparable接口
比较器排序(集合具备比较性)
让集合接收一个Comparator的实现类对象
1、数组结构
数组结构我想不必多说了
数组是最最基本的数据结构,很多语言都内置支持数组。数组是使用一块连续的内存空间保存数据,保存的数据的个数在分配内存的时候就是确定的
2、链表结构
单向链表
双向链表
3、二叉树结构
4、散列表结构(哈希表)
是否是键值对象形式:
是:Map
键是否需要排序:
是:TreeMap
否:HashMap
不知道,就使用HashMap。
否:Collection
元素是否唯一:
是:Set
元素是否需要排序:
是:TreeSet
否:HashSet
不知道,就使用HashSet
否:List
要安全吗:
是:Vector
否:ArrayList或者LinkedList
增删多:LinkedList
查询多:ArrayList
不知道,就使用ArrayList
不知道,就使用ArrayList
Collection:
add()
remove()
contains()
iterator()
size()
遍历:
增强for
迭代器
|--List
get()
遍历:
普通for
|--Set
Map:
put()
remove()
containskey(),containsValue()
keySet()
get()
value()
entrySet()
size()
遍历:
根据键找值
根据键值对对象分别找键和值
面试常问特点:
1、底层结构
- jdk1.8以下:HashMap的底层是:数组+链表(单向链表)
- jdk1.8+:HashMap的底层是:数组+[链表(单向链表) / 红黑树 ]
2、线程不安全(put方法没有加锁)
3、初始化默认大小:16 【1 << 4】
4、扩容
扩容触发机制:
- 当前存储过的键值对的数量【即HashMap中的一个size属性】必须大于等于阈值(threshold)【注意:阈值=数组length*加载因子】;
- 当前加入的数据是否发生hash冲突
加载因子:0.75 当元素存储(使用)到达现有数组的75%的时候进行扩容(例如:下标100的数组,任意75个下标存储后就会扩容)
扩大容量:每次在原基础乘以2,扩容后为原来的2倍
/** * The default initial capacity - MUST be a power of two. */ static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16 /** * The load factor used when none specified in constructor. */ static final float DEFAULT_LOAD_FACTOR = 0.75f; /** * The next size value at which to resize (capacity * load factor). * @serial */ // If table == EMPTY_TABLE then this is the initial capacity at which the // table will be created when inflated. int threshold; /** * Constructs an empty HashMap with the default initial capacity * (16) and the default load factor (0.75). */ public HashMap() { this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); }
准确点来说是 Entry[]数组 (根据Entry的内部结构不同,来断定具体是什么数据结构)
那么我们能不能综合两者的特性,做出一种寻址容易,插入删除也容易的数据结构?答案是肯定的,这就是我们要提起的哈希表,哈希表有多种不同的实现方法,我接下来解释的是最常用的一种方法——拉链法,我们可以理解为“链表的数组”
问:数组我们都知道,但什么是Entry数组呢?
答:就是一个数组里放的都是Entry
问:那这里的Entry又是什么呢?
答:
注意:每个集合类里面的存放实现都有可能是不一样的(比如 TreeSet 里面也有 Entry,但是和 HashMap 的 Entry 内部结构实现是不同的)
HashMap 的 Entry 就是我们说的链表结构 (单向链表)
下图中我们可以看到,在HashMap里面有个静态类 Entry
也是 key value 形式的; 里面有 next 属性,类型也是Entry
这就是一个 Entry 里面嵌套了另一个 Entry(典型的链表结构)
注意:我们可以看到这个 Entry类里面只有一个 Entry
next 属性,只有下一个,所以这是一个 单向链表 HashMap 结构 ,里面有个Entry(这是用来存放数据的)
内部类 Entry 结构
下面是HashMap的结构图,结合上面的代码就很好理解了吧
数据都在这里面存放
首先执行初始化 :初始化会加载一些配置信息
初始化完成,如果在调用put方法时,发现table里面没有任何数据,那么会调用 inflateTable(int toSize)
(1)首先拿到我们的 key 算出对应的 HashCode【line:492】
(2)根据 key 和 table.length 调用 indexFor() 来计算出一个数值 i,这个数值是 table 数组的下标【line:493】
(3)重点来了,我们需要比较这个 key 是否已经存在,如果存在则给对应的 value 重新赋值【line:494~502】
(4)如果该数组下标内没有找到对应的 key,我们则调用 addEntry() 方法进行添加【line:505】
addEntry()方法如下:
大概看一下我们就可以理解,有兴趣的同学可以看一下 (红色区域为添加,蓝色区域后面会讲到)
链表的添加方式:每次都是在最外层创建一个新的链表点,然后把旧的链表插入新的点,所以最外层的(顶层的)永远是最新的
(5)get(key)方法如何实现的呢?
如果你认真的跟着我上面的步骤理解了 put(key,value)方法,我想不用看源码也能大概说出来是如何实现的
亮点,我们也附带学习了遍历链表的方法
扩容必须同时满足两个条件:
- 存放新值的时候,发现当前已有键值对(key-value)元素的个数(size)必须大于等于阈值(阈值=加载因子*当前数组长度)
- 存放新值的时候,当前新值数据key发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
在put()方法中有调用addEntry()方法,这个方法里面是具体的存值,在存值之前还有判断是否需要扩容
判断扩容条件:(size >= threshold) && (null != table[bucketIndex])【line:878】
如果下面两个条件都发生,那么就扩容
- 判断当前个数是否大于等于阈值(size >= threshold)
- 当前存放是否发生哈希碰撞(null != table[bucketIndex])
扩容调用方法:resize(int) 【line:879】
扩容大小为原先数组大小的两倍
阈值:threshold
threshold 就是所说的阈值,它是一个全局变量,决定了数组是否进行扩容的判断条件之一【上图中 line:878】
阈值根据加载因子和数组大小决定的
默认情况下: 阈值=加载因子 * 当前数组大小
HashMap的构造函数有4个:
下图中的3个构造函数可以看到都是调用了同一个构造函数 public HashMap (int initialCapacity, float loadFactor)
代码中选中的是加载因子
public HashMap (int initialCapacity, float loadFactor)
如果需要扩容,调用扩容的方法:resize(int)
总结:
HashMap的扩容需要同时满足两个条件:
- 存放新值的时候,发现当前已有键值对(key-value)元素的个数(size)必须大于等于阈值(阈值=加载因子*当前数组长度)
- 存放新值的时候,当前新值数据key发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
因为上面这两个条件,所以存在下面这些情况
- 就是hashmap在存值的时候(默认大小为16,负载因子0.75,阈值12),可能达到最后存满16个值的时候,再存入第17个值才会发生扩容现象,因为前16个值,每个值在底层数组中分别占据一个位置,并没有发生hash碰撞。【key不触发hash碰撞】
- 当然也有可能存储更多值(超多16个值,最多可以存26个值)都还没有扩容。原理:前11个值全部hash碰撞,存到数组的同一个位置(这时元素个数小于阈值12,不会扩容),后面所有存入的15个值全部分散到数组剩下的15个位置(这时元素个数大于等于阈值,但是每次存入的元素并没有发生hash碰撞,所以不会扩容),前面11+15=26,所以在存入第27个值的时候才同时满足上面两个条件,这时候才会发生扩容现象。
变化如下:
- 数据存储结构进行改变
- 对数组下标的定位生成,做了改动,使之更具有分散性(减少经常向同一个下标存储的情况)
- 加入了链表和红黑树相互转变的机制(减少链的深度)
transient Node[] table;
static class Node implements Map.Entry {
final int hash;
final K key;
V value;
Node next;
Node(int hash, K key, V value, Node next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
public final boolean equals(Object o) {
if (o == this)
return true;
if (o instanceof Map.Entry) {
Map.Entry,?> e = (Map.Entry,?>)o;
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
TreeNode
/**
* Entry for Tree bins. Extends LinkedHashMap.Entry (which in turn
* extends Node) so can be used as extension of either regular or
* linked node.
*/
static final class TreeNode extends LinkedHashMap.Entry {
TreeNode parent; // red-black tree links
TreeNode left;
TreeNode right;
TreeNode prev; // needed to unlink next upon deletion
boolean red;
TreeNode(int hash, K key, V val, Node next) {
super(hash, key, val, next);
}
......
}
使用树型结构,而不去使用单向链表
当节点超过8时,从链表转变为红黑树结构,从而减小链的深度
当节点小于6时,从红黑树转变为链表结构
/** * The bin count threshold for using a tree rather than list for a * bin. Bins are converted to trees when adding an element to a * bin with at least this many nodes. The value must be greater * than 2 and should be at least 8 to mesh with assumptions in * tree removal about conversion back to plain bins upon * shrinkage. */ static final int TREEIFY_THRESHOLD = 8; /** * The bin count threshold for untreeifying a (split) bin during a * resize operation. Should be less than TREEIFY_THRESHOLD, and at * most 6 to mesh with shrinkage detection under removal. */ static final int UNTREEIFY_THRESHOLD = 6;
put方法的改变
(1)根据情况判断是否进行初始化【line:628~629】
(2)计算出数组的下标,取出数据存入变量p,判断是否为null,如果为null则直接生成新节点存入,跳至661【line:630~631】
(3)如果该下标中存在数据则进入else【line:632~660】
(1、下标中先取链表的 顶层 Node节点,判断key是否相同。相同则直接进入line:653进行数据替换【line:634~636】
(2、判断该下标Node节点类型是否为TreeNode(树结构),如果是则使用内部类TreeNode的putTreeVal进行存储【line:637~638】
(3、如果传入的key不是 顶层 Node节点,数组下标节点也不是TreeNode(树结构),那么就对该下标下的Node节点进行循环遍历【line:639~652】
(4、对value值进行替换处理
LinkedHashMap 继承了 HashMap 所以很多方法都是继承来的,但是 LinkedHashMap 又是有序的,我们可以看到构造器中有个 accessOrder 参数,这个就是来控制有序的。
面试常问特点:
1、底层结构
jdk1.8以下:HashMap的底层是:数组+链表(单向链表)
jdk1.8+:HashMap的底层是:数组+[链表(单向链表) / 红黑树 ]
2、线程安全(put方法加了synchronized修饰)
3、初始化默认大小:11
4、扩容
扩容触发机制:数据大小【size()】大于等于阈值【count >= threshold】
加载因子:0.75
扩大容量:每次原大小乘以2再加1【(old.length << 1)+1】
/** * Constructs a new, empty hashtable with a default initial capacity (11) * and load factor (0.75). */ public Hashtable() { this(11, 0.75f); }
关于2n+1的扩展,在hashtable选择用取模的方式来进行,那么尽量使用素数、奇数会让结果更加均匀一些,具体证明,可以看看已经证明这点的技术文章
关于hash,hashmap用2的幂,主要是其还有一个hash过程即二次hash,不是直接用key的hashcode,这个过程打散了数据
总体就是一个减少hash冲突,并且找索引效率还要高,实现都是要考量这两因素的
1、添加方法和HashMap几乎是一样的
(1)用 key 算出对应的 HashCode【line:465】
(2)根据 HashCode和 table.length 计算出 table 数组的下标【line:466】
(3)循环遍历table下标中的entry链表,比较这个 key 是否已经存在,如果存在则给对应的 value 重新赋值【line:468~475】
(4)如果该数组下标内没有对应的key,我们则调用 addEntry() 方法进行添加【line:477】
2、扩容方法 rehash()
1、了解结构
TreeMap 有一个 Entry
Entry 类型又有三个 Entry 类型的 left、right、parent 属性,和自己的 key、value属性;(用来存放 树结构 左、右、父的对象数据,还有自己的值;color 属性是当前树节点的颜色)
由此可见 Entry 类型是个 红黑树 的结构,而 TreeMap 里面存储的是 Entry 结果自然就是红黑树
2、内部类 Entry 结构
面试常问特点:
1、底层结构:数组
2、线程不安全(add方法没有加锁)
3、初始化默认大小:10
4、扩容
扩容触发机制:当存储第11个数据时,11超过了默认的10,就会触发扩容
扩大容量:每次在原基础上增加0.5倍,扩容后为原来的1.5倍
/** * Default initial capacity. */ private static final int DEFAULT_CAPACITY = 10; /** * Shared empty array instance used for empty instances. */ private static final Object[] EMPTY_ELEMENTDATA = {}; /** * Constructs an empty list with an initial capacity of ten. */ public ArrayList() { super(); this.elementData = EMPTY_ELEMENTDATA; }
ensureCapacityInternal() 方法
(1)在调用Add方法时,会先通过 ensureCapacityInternal() 方法确保当前ArrayList维护的数组具有存储新元素的能力【Line:440】
(2)ensureCapacityInternal() 判断ArrayList默认的元素存储数据是否为空,为空则设置最小要求的存储能力为必要存储的元素和默认存储元素个数的两个数据之间的最大值,然后调用ensureExplicitCapacity方法实现这种最低要求的存储能力【Line:208】
(3)如果最低要求的存储能力 > ArrayList 已有的存储能力,这就表示ArrayList的存储能力不足,因此需要调用 grow();方法进行扩容 【Line:215】
扩容方法源码(扩容后为原来的1.5倍)
grow()方法
(1)数字转换为二进制,使用向右位移符 >> 移动所有二进制数,实现除法(源码中 oldCapacity >> 1表示所有二进制向右移动1位,表示除以2,移出去的数直接忽略)
(2)新size = 旧的size + 扩大的size(Line:236)
(3)创建一个新的数组
(4)通过Arrays.copyOf方法,将原数组的数据复制到新数组
总结:
举例说明:添加20个元素到ArrayList中
当第一次插入元素时才分配10(默认)个对象空间。之后扩容会按照1.5倍增长。
也就是当添加第11个数据的时候,Arraylist继续扩容变为10*1.5=15;
当添加第16个数据时,继续扩容变为15 * 1.5 =22个;
面试常问特点:
1、底层结构:数组
2、线程安全(add方法添加synchronized锁)
3、初始化默认大小:10
扩容触发机制:当存储第N+1个数据时,N+1超过了先前的数组最大个数N,就会触发扩容
扩大容量:每次在原基础上增加1倍,也就是总大小为原来的2倍
/** * Constructs an empty vector so that its internal data array * has size {@code 10} and its standard capacity increment is * zero. */ public Vector() { this(10); }
ensureCapacityHelper() 方法
基本上ArrayList一样
扩容方法源码(扩容后为原来的2倍)
grow()方法
(1)(capacityIncrement > 0) ? capacityIncrement : oldCapacity(大多情况会返回 oldCapacity)
(2)新size = 旧的size + 旧的size(Line:256)
(3)创建一个新的数组
(4)通过Arrays.copyOf方法,将原数组的数据复制到新数组
总结:
举例说明:添加40个元素到 Vector 中
当第一次插入元素时才分配10(默认)个对象空间。之后扩容会按照2倍增长。
也就是当添加第11个数据的时候,Vector 继续扩容变为10*2=20;
当添加第21个数据时,继续扩容变为20 * 2 =40个;
1、了解结构。我们进入类中,看到它的变量
LinkedList 有两个 Node 类型的 first、last属性,和自己的size;(用来存放第一个和最后一个,还有总大小的值)
Node 类型又有两个 Node 类型的 next、prev属性,和自己的 item 属性;(用来存放 前一个和后一个对象数据,还有自己的值)
由此可见 Node 类型是个 双向链表 的结构,而 LinkedList 里面存储的是 Node 结果自然就是 双向链表
2、内部类 Node 结构