探究java.util集合容器的底层原理

java.util是包含集合框架、遗留的 collection 类、事件模型、日期和时间设施、国际化和各种实用工具类(字符串标记生成器、随机数生成器和位数组、日期Date类、堆栈Stack类、向量Vector类等)。集合类、时间处理模式、日期时间工具等各类常用工具包
在这里插入图片描述
java.util下的集合容器包含如图的几大类

set:无重复元素集合容器
List:允许有重复元素容器
Map:键值对

使用机制

在这里插入图片描述

Set

HashSet

HashSet的底层实现和HashMap有很多类似之处。
其抽象理解可以理解为,HashSet是HashMap中的键,之后进行遍历取出。
而HashMap本身的遍历其实违背了使用思想,而HashSet则很需要使用遍历。
HashSet的遍历:
1.iterator。
iterator的实现就是将容器的元素通过一张线性表串联起来,iterator就是指向表头的指针,具体为链表。
值得注意的是当HashSet改变时,需要重新申请iterator。
2.for(String s : hashmap) 通过for循环遍历。

当然对于碰撞和扩容,采取的方法同HashMap一样。
并且链表长度大于8时会自定转化为红黑树。

LinkedHashSet

LinkedHashSet继承了HashSet,而底层实现又和LinkedHashMap相似。
因为本身HashSet是无序的,散乱的数据。
而使用双向链表去维护,可以得到插入顺序查询顺序

TreeSet

按照前面的例子,多半TreeSet和TreeMap一样实现方式,通过红黑树,插入和查询控制在o(logn)
在看了源码后果然如此。

List

ArrayList

ArrayList不是线程安全的,在多线程时使用需要上锁
ArrayList是基于数组实现的添加和查询指定项很快,但是插入和删除效率不高

初始时容量为0,在第一次扩容为10;
之后每次在add内存不够时扩容1.5倍。
private void grow(int minCapacity) {
int oldCapacity = elementData.length; // 旧容量
int newCapacity = oldCapacity + (oldCapacity >> 1); // 新容量为旧容量的1.5倍
if (newCapacity - minCapacity < 0) // 新容量小于参数指定容量,修改新容量
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) // 新容量大于最大容量
newCapacity = hugeCapacity(minCapacity); // 指定新容量
// 拷贝扩容
elementData = Arrays.copyOf(elementData, newCapacity);
}

LinkedList

LinkedList不是线程安全的
LinkedList是基于链表实现的插入和删除很快,但是添加效率不高

Vector

Vector是线程安全的
Vector也是基于数组实现,机制与ArrayList类似

Map

HashMap

JAVA中常用HashMap,在使用上看,HashMap对应一系列键值对,每个键唯一标识一个值。
HashMap使用了Hashing(哈希)原理。

Hash是一种伟大的索引方法。
在这里插入图片描述
hash函数将键的集合映射到hash集合,每一个键的元素唯一对应一个hash元素,例如
如果hash函数是hash(x)=x-1, 那么给定一个5,hash(5)=4,那么将第4列指针指向5的存储。
如果hash函数是hash(x)=x%5, 那么给定一个5,hash(5)=0,那么将第0列指针指向5的存储。

冲突(碰撞)
对于hash(x)=x%5而言,x=10,x=5得到的hash值相同,hash(5)=0,hash(10)=0。
此时就发生了冲突(碰撞)。
假设此时已经将第0列的指针指向了10的存储,又来了一个5。
此时通常有两种处理方法。
1) 链接地址
将后来的hash(a)=0,用上一个hash(x)=0指针a的存储。
即将10的存储指向5的存储。
在这里插入图片描述

2) 开放地址
将后来的hash(a)=0,用下一个0+1的指针a的存储。
如果下一个地址有指向,则继续向下。
在这里插入图片描述
扩容
扩容就是在原有效率过低时,用一个新的hash代替原有的hash函数,使得hash集合更大。
然后将原来的hash表用新的hash函数算后,转存到新的hash表中。
hash的容量使用率在75%以内效率都会很高,超过75%后,效率会明显下降,所以当容量使用率达到75%时需要扩容。
HashMap第一次的容量为16,当存储量达16*0.75=12时,就会是HashMap扩容,使得容量翻倍。

好处
hash的好处,不必排序,不必搜索,只需要计算就可以查询到某个值。
同样的对于数学中的高维问题,可以将hash函数设为降维方法
比如对于一个六面体来说,普通的查询需要查询8个顶点坐标,才能确定一个六面体。
但是对于hash而言,可以设hash(六面体)=体积。
那么只需要对六面体做体积运算即可查询,大大的简化了查询。

hash函数的设计
某种程度上而言尽可能将大的集合转化为小的集合,使得查询更有效率。

在JDK中:
public class HashMap<K,V> extends AbstractMap<K,V>
可以看出HashMap继承AbstractMap。
public abstract class AbstractMap<K,V> implements Map<K,V>
而AbstractMap<K,V> 实现了 Map<K,V>接口
public interface Map<K, V>可以看出Map是一个接口。
其中K是Key键,V是Value值。
Map接口定义了:
int size();
boolean isEmpty();
boolean containsKey(Object key);
boolean containsValue(Object value);
V get(Object key);
V put(K key, V value);
V remove(Object key);

首先HashMap在实现中定义了
Node<K,V>,一个很简单的链表。
定义了:
final int hash; //哈希值
final K key; //键
V value; //值
Node<K,V> next; //下一个结点

定义了方法
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
这个方法定义了hash的计算。
Object 中定义为:public native int hashCode();
hashCode()是一个native方法,意味着方法的实现和硬件平台有关,默认实现和虚拟机有关,对于有些JVM,hashCode()返回的就是对象的地址,大多时候JVM根据一定的规则将与对象相关的信息(比如对象的存储地址,对象的字段等)映射成一个数值,并返回。

HashMap中定义了:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
而putVal则是将键值对保存,操作是:
根据hash(key) 寻找是否冲突
如果冲突,那么找到冲突的结点Node
遍历链表如果k-v存在,则修改v
不存在则找到最后一个结点即(Node.next==null) 使得next等于当前的key-value
如果不冲突,那么在hash表中加入该结点

对于get来说,就是很简单查询。
public V get(Object key) {
Node<K,V> e;
return (e = getNode(hash(key), key)) == null ? null : e.value;
}

在源码中可以看出,HashMap是通过连接地址来解决冲突(碰撞)的。
而当链表长度超过8时,HashMap会自动将链表转成红黑树。
在多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

Hashtable

Hashtable和HashMap类似,源码中可以发现主要差别在于Hashtable的实现之前加了synchronized。
也就是说Hashtable是线程安全的。

至于其他更多的区别,可以读一下源码解读Hashtable

ConcurrentHashMap

ConcurrentHashMap是Java中的一个线程安全且高效的HashMap实现。
基本内容与HashMap相似,其中区别的,ConcurrentHashMap是通过CAS+synchronized的加锁。
另外当ConcurrentHashMap扩容时,p<0,此时任何线程都不能读写。

ConcurrentHashMap比起Hashtable的效率更快的原因是,Hashtable是将整个共享区上锁,所有资源都由一把锁控制。
而ConcurrentHashMap是用分段锁控制,每把锁控制一部分资源,这样当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。其中的操作比较复杂,详细可以读一下:
ConcurrentHashMap源码解读

LinkedHashMap

LinkHashMap 保存了记录插入的顺序,用Iteraor遍历时先得到的肯定是先插入的,遍历时比HashMap慢,有HashMap的全部特性
LinkedHashMap的实现是对HashMap加了一个双向链表作为维护,可以获得插入顺序访问顺序
同样LinkedHashMap和HashMap一样没有锁保护,线程不安全。
关于LinkedHashMap的双向链表具体实现,见LinkedHashMap底层原理

TreeMap

TreeMap是一个自动升序的Map。
在源码中可以发现TreeMap继承AbstractMap<K,V>,实现接口NavigableMap<K,V>
而AbstractMap<K,V>实现了Map<K,V>,而NavigableMap<K,V>实现了SortedMap<K,V>,而SortedMap<K,V>继承了Map<K,V>。

TreeMap的实现是基于红黑树的。
红黑树是自平衡二叉树,其结点必须为红色或者黑色,查询效率很高。
对于自动升序的Map而言,建立红黑树,再通过红黑树去实现SortedMap<K,V>,TreeMap来使用SortedMap<K,V>的接口,可以达到自动升序的效果。

在这里插入图片描述

红黑树:
红黑树是计算机科学内比较常用的一种数据结构,它使得对数据的搜索,插入和删除操作都能保持在O(lgn)的时间复杂度。
红黑树本质上是一颗二叉搜索树,它满足二叉搜索树的基本性质——即树中的任何节点的值大于它的左子节点,且小于它的右子节点。
一颗红黑树必须满足以下几点条件:
规则1、根节点必须是黑色。
规则2、任意从根到叶子的路径不包含连续的红色节点。
规则3、任意从根到叶子的路径的黑色节点总数相同。

而对于一颗红黑树,除了根节点插入为黑色外,其余的插入均为红色,一旦出现连续的红色则意味着不平衡需要调整。
红黑树的调整是旋转+着色。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值