1.Java容器分类图
https://www.cnblogs.com/wishyouhappy/p/3669198.html
2. 容器接口
容器接口是容器的基础。使用接口可以将容器的实现与容器接口分开,因而可以使用相同的方法访问容器而不需关心容器具体的数据结构
- Collection: 集合的顶级接口
- Map接口: (存放键值对,Map中的值也可以是一个容器)
- Iterator接口: 使用集合的iterator()方法创建的迭代器对象,都是接口的子类型对象
List<String> list=new ArrayList<String>();
for (Iterator<String> iterator = list.iterator(); iterator.hasNext();)
{
String s = iterator.next();
System.out.print(s+" ");
}
3. 子接口
-
List存储元素是有顺序的,Set无序(Set的底层实现其实是Map,它是计算key的哈希值来确定元素在数组中的存放位置,所以无序)。这里的有序和无序不是指集合中的排序,而是是否按照元素添加的顺序来存储对象。
-
List可以包含重复元素,Set集合中不包含重复元素(Set接口相当于穿了马甲的Map接口,本质上Set接口的子类都是使用Map来存储元素的,都是将元素存储到Map接口的key中,key不能重复,Set中的value都是用一个空的Object对象)
-
List中有get()方法,Set中没有。
-
ListIterator: Iterator for List ,用来迭代List的迭代器
拓展问题:Iterator和ListIterator区别?
https://www.cnblogs.com/lijia0511/p/4960033.html
- ListIterator继承自Iterator,对其方法进行了拓展
- Iterator只能单向遍历,ListIterator可以双向遍历和修改元素
- Iterator可以遍历任何Collection,而ListIterator只能遍历List
4. 具体容器类
4.1.ArrayList
ArrayList(线程不安全)是一个动态数组,也是我们常用的集合,它允许任何元素的插入,甚至包括null。每一个ArrayList都有一个初始化的容量(10),该容量代表了数组的大小,随着容器中容量的不断增加,容器的大小也会随着增加。在每次向容器中增加元素(调用add方法)时,会进行容量检查,当快溢出时(比如初始为10,当添加第11个元素的时候),会进行扩容操作(原容量1.5倍)
/**
* 默认容量.
* 动态数组
*/
private static final int DEFAULT_CAPACITY = 10;
transient Object[] elementData;
//无参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
//指定容量
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);
}
}
ArrayList扩容机制
https://www.cnblogs.com/dengrongzhang/p/9371551.html
ArrayList扩容分为两步:1.扩容;2.复制
ArrayList扩容发生在add()方法调用的时候;当判断需要扩容时(使用ensureCapacityInternal()方法判断),会使用grow()方法,源码如下:
private void grow(int minCapacity) {
// 获取到ArrayList中elementData数组的内存空间长度
int oldCapacity = elementData.length;
// 扩容至原来的1.5倍
int newCapacity = oldCapacity + (oldCapacity >> 1);
// 再判断一下新数组的容量够不够,够了就直接使用这个长度创建新数组,
// 不够就将数组长度设置为需要的长度
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//若预设值大于默认的最大值检查是否溢出
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 调用Arrays.copyOf方法将elementData数组指向新的内存空间时newCapacity的连续空间
// 并将elementData的数据复制到新的内存空间
elementData = Arrays.copyOf(elementData, newCapacity);
}
新数组长度(newCapacity) = 原数组长度 + 原数组长度右移一位
从此方法中我们可以清晰的看出其实ArrayList扩容的本质就是计算出新的扩容数组的size后实例化,并将原有数组内容复制到新数组中去
4.2.Vector
转载:https://blog.csdn.net/yt_19940616/article/details/90183781
Vector与ArrayList一样,也是通过数组(elementData)实现的
protected Object[] elementData; //存放元素的数组
protected int elementCount; //已经放入数组的元素个数
protected int capacityIncrement; //数组的增长系数
Vector扩容分为自定义扩容和默认扩容
//增添
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
private void ensureCapacityHelper(int minCapacity) {
// overflow-conscious code
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//如果设置了增长系数,那么就增加设置的容量;否则,按照默认:扩容两倍
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
elementData = Arrays.copyOf(elementData, newCapacity);
}
使用构造方法来设置增长系数
//设置指定容量和指定增长系数
public Vector(int initialCapacity, int capacityIncrement) {
super();
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
this.elementData = new Object[initialCapacity];
this.capacityIncrement = capacityIncrement;
}
//设置指定容量但增长系数为0
public Vector(int initialCapacity) {
this(initialCapacity, 0);
}
//默认容量10
public Vector() {
this(10);
}
当容量不足以容纳当前的元素个数时,就看构造方法中传入的容量增长系数CapacityIncrement是否为0,如果不为0,就设置新的容量为 旧容量 + 容量增长量;如果为0,设置新的容量为旧的容量的2倍,如果设置后的容量还不够,则直接新的容量设置为 旧容量 + 传入参数所需要的容量 而后同样用Arrays.copyof()方法将元素拷贝到新的数组
4.3.LinkedList
转载:https://blog.csdn.net/bntX2jSQfEHy7/article/details/78138835
LinkedList(线程不安全)是由双向链表实现的,每一个对象包含数据的同时还包含指向链表中前一个与后一个元素的引用
LinkedList并没有用数组来存储数据元素,而是由一个个Node类型节点来储存数据,然后每个Node结点通过指向前后结点的next(上指针)和prev(下指针)指针将整个List串联起来(Node节点串联起来的链表)
linkedList 是一个双向链表,没有初始化大小,也没有扩容的机制,就是一直在前面或者后面新增就好
//初始位置为0
transient int size = 0;
//第一个构造方法是空实现,因为内部是链表,无需像ArrayLsi一样t实例化数组
public LinkedList() {
}
public LinkedList(Collection<? extends E> c) {
this();
//调用addAll()方法
addAll(c);
}
//增添元素调用linkLast方法
public boolean add(E e) {
linkLast(e);
return true;
}
//初始化了Node节点
void linkLast(E e) {
final Node<E> l = last;
final Node<E> newNode = new Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}
//Node类
private static class Node<E> {
E item; //数据本身
Node<E> next;//上指针,前一个元素的引用
Node<E> prev;//下指针,下一个元素的引用
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
拓展问题:ArrayList和LinkedList区别?
- 数据结构的实现: ArrayList是动态数组,LinkedList是双向链表
- 随机访问的效率: ArrayList查询效率高,LinkedList效率低
- 添加和删除的效率 ArrayList增添、删除效率低,LinkedList效率高
总结: 在开发中如果需要对list频繁的添加,删除,插入,那么用LinkedList是很好的。但是数据量不大,需要经常查询数据的时候,ArrayList更适合
链表既然是用指针的方式连起来,那么意味着我们寻找某个节点就需要从头开始遍历,直到找到这个元素为止,并不能提供随机访问
例如我们想找数组的第五个元素,那么只需xxx[4]即可得到,但是链表不行,只能从头开始遍历到第五个元素才行
LinkedList插入与删除图解
插入只需要将要插入位置的前结点的next指针指向新的数据结点(如下图插入工作图的2),将插入结点的prev指向前指针(如1处),将next指向下一个结点(如3处),将之前的后结点的prev指针指向插入指针(如4处)
4.4.hashSet
hashSet,它是基于hashMap实现的,hashSet的底层使用hashMap来保存所有元素,因此HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成
//其内部声明了一个HashMap
private transient HashMap<E,Object> map;
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
/**
* Constructs a new, empty set; the backing <tt>HashMap</tt> instance has
* default initial capacity (16) and load factor (0.75).
*/
创建hashMap实例
public HashSet() {
//hashMap进行初始化
map = new HashMap<>();
}
//调用了hashMap的put方法
//E表示任意类型,PRESENT是内部声明的Object对象
public boolean add(E e) {
//put()方法返回value,判断value是否为null
return map.put(e, PRESENT)==null;
}
总结:hashset的存储数据在hashmap中的形式是map.put(key,null),hashSet数据是不允许重复的原因,是因为hashMap的key唯一,
4.5.hashMap
https://www.cnblogs.com/chengxiao/p/6059914.html#t2
hashMap的主干是一个Entry[]数组.Entry是hashMap的基本组成单元,每一个Entry包含一个key-value键值对
transient Entry<K,V>[] table = (Entry<K,V>[]) EMPTY_TABLE;
Entry是HashMap用来封装key-value键值对的,主要包括如下属性和方法:
https://blog.csdn.net/strivenoend/article/details/80397825
static class Entry<K,V> implements Map.Entry<K,V> {
final K key;// map中key值,可以为null。
V value; // map中的value值,可以为null。
Entry<K,V> next;// 链表引用,防止key值不同,hash值相同。
int hash; // 每个key的hash值
// 构造函数
Entry(int h, K k, V v, Entry<K,V> n) {
value = v;
next = n;
key = k;
hash = h;
}
public final K getKey() {
return key;
}
public final V getValue() {
return value;
}
// 同一个key时,新值替换旧值,返回旧值
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// key值重写equals方法
public final boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry e = (Map.Entry)o;
Object k1 = getKey();
Object k2 = e.getKey();
if (k1 == k2 || (k1 != null && k1.equals(k2))) {
Object v1 = getValue();
Object v2 = e.getValue();
if (v1 == v2 || (v1 != null && v1.equals(v2)))
return true;
}
return false;
}
// 重写hashCode值
public final int hashCode() {
return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue());
}
public final String toString() {
return getKey() + "=" + getValue();
}
// 其他方法省略
}
hashMap的hash冲突
当程序执行put(key,value)时,会调用hash()得到,key的hash值,根据hash值来存储value. 当计算出的多个key的hash值相同时,我们称之为hash冲突,hashMap用链表来存储发生hash冲突key的value
hashMap中扰动处理
https://blog.csdn.net/zhang_zhenwei/article/details/103345480
将Hash值的右移16位并与原Hash值取异或运算(^),混合高16位和低16位的值,得到一个更加散列的低16位的Hash值
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
hashMap的扩容机制
- 默认大小为16,负载因子0.75,阈值12
/**
* The default initial capacity - MUST be a power of two.
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16
这个上边注释的意思是,默认初始容量-必须是2的幂。
作用:
1.提醒你这个容量就是2的幂,扩容方式也是2的幂。
2.二的幂是使得Key Hash算法后的值尽可能均匀的分布在Map对应的数组位置的合理位置
- 什么时候扩容?
1、 存放新值的时候当前已有元素的个数必须大于等于阈值(当前数组的长度乘以加载因子的值)
2、 存放新值的时候当前存放数据发生hash碰撞(当前key计算的hash值换算出来的数组下标位置已经存在值)
//加载因子默认0.75
static final float DEFAULT_LOAD_FACTOR = 0.75f;
- 1.7扩容源码全过程
put()—> addEntry()---->resize()—>transfer()
存储k-v—>具体存放数据的方法—>需要扩容时执行的方法—>原数组数据移到新数组
转载:https://www.cnblogs.com/yanzige/p/8392142.html
为什么扩容2倍
转载:https://blog.csdn.net/Apeopl/article/details/88918576
hashMap链表成环问题
图解:https://www.cnblogs.com/wen-he/p/11496050.html
在多个线程并发扩容时,会在执行transfer()方法转移键值对时,造成链表成环,导致程序在执行get操作时形成死循环
hashMap1.7与1.8的区别?
转载:http://www.codeceo.com/article/java-hashmap-concurrenthashmap.html(这个人讲的很好)
- 1.8引入了红黑树
当链表长度大于等于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;
- 1.8中用Node代替了1.7的Entry,实现Map.Entry
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
Node(int hash, K key, V value, Node<K,V> 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;
}
}
- 链表数据插入:1.8采用了尾插入法代替了1.7的头插入法,避免了链表成环问题
4.6.hashTable
hashTable中,保存实际数据的,依然是Entry数组,其数据结构和hashMap(1.7)是类似的
/*
* Hashtable bucket collision list entry
*/
private static class Entry<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Entry<K,V> next;
protected Entry(int hash, K key, V value, Entry<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
@SuppressWarnings("unchecked")
protected Object clone() {
return new Entry<>(hash, key, value,
(next==null ? null : (Entry<K,V>) next.clone()));
}
// Map.Entry Ops
public K getKey() {
return key;
}
public V getValue() {
return value;
}
public V setValue(V value) {
if (value == null)
throw new NullPointerException();
V oldValue = this.value;
this.value = value;
return oldValue;
}
public boolean equals(Object o) {
if (!(o instanceof Map.Entry))
return false;
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
return (key==null ? e.getKey()==null : key.equals(e.getKey())) &&
(value==null ? e.getValue()==null : value.equals(e.getValue()));
}
public int hashCode() {
return hash ^ Objects.hashCode(value);
}
public String toString() {
return key.toString()+"="+value.toString();
}
}
hashTable类继承自Dictionary类,实现了三个接口,分别是Map,Cloneable和java.io.Serializable
hashTable主要方法的源码实现逻辑,与hashMap中非常相似,有一点重大区别就是所有的操作都是通过synchronized锁保护的。只有获得了对应的锁,才能进行后续的读写等操作
hashTable中的主要方法,如put,get,remove和rehash等
转载:http://www.imooc.com/article/details/id/23015
拓展问题:hashTable与hashMap的区别?
- hashTable线程安全,hashMap线程不安全
- hashTable不可以存储null值,hashMap可以存储
// Make sure the value is not null
public synchronized V put(K key, V value) {
if (value == null) {
throw new NullPointerException();
}
- 初始容量不同:hashTable 11; hashMap 16
/**
* Constructs a new, empty hashtable with a default initial capacity (11)
* and load factor (0.75).
*/
public Hashtable() {
this(11, 0.75f);
}
4.7.ConcurrentHashMap
为什么使用ConcurrentHashMap?
转载:https://www.cnblogs.com/chengxiao/p/6842045.html
分段锁思想:
hashTable性能差主要是由于所有操作需要竞争同一把锁,而如果容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想。
ConcurrentHashMap的主干是个Segment[]数组,每一个Segment包含了一个HashEntry[]数组
final Segment<K,V>[] segments;
//继承了ReentrantLock(重入锁)保证了线程安全
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) { this.loadFactor = lf; }
}
//Segment构造方法
Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
this.loadFactor = lf;//负载因子
this.threshold = threshold;//阈值
this.table = tab;//主干数组即HashEntry数组
}
transient volatile HashEntry<K,V>[] table;
//hashEntry是用来存储key-value数据的
static final class HashEntry<K,V> {
final int hash;
final K key;
volatile V value;
volatile HashEntry<K,V> next;
//其他省略
}
//负载因子
private static final float LOAD_FACTOR = 0.75f;
//初始容量
private static final int DEFAULT_CAPACITY = 16;
ConcurrentHashMap分为了16段的Segment[]数组,Segment与hashMap结构类似,其内部的hashEntry与entry类似(都是用来存储Key-Value数据)
Segment[]数组初始化后不可扩容,hashEntry可以自动扩容,扩容和hashMap大致一样
ConcurrentHashMap扩容源码
转载:https://www.cnblogs.com/lfs2640666960/p/9621461.html
Java1.8的ConcurrentHashMap也引入了红黑树
转载:https://blog.csdn.net/wfg18801733667/article/details/56664734
一个table就是一个segment,table内部有链表和 红黑树