java集合底层实现原理

本文详细介绍了Java集合框架中的ArrayList、Vector、LinkedList、HashSet、LinkedHashSet、TreeSet、HashMap、LinkedHashMap和TreeMap的底层实现原理及线程安全性。ArrayList和Vector都是基于数组实现,但Vector是线程安全的;LinkedList通过链表实现,适合频繁插入和删除;HashSet基于HashMap,LinkedHashSet则基于LinkedHashMap以保持插入顺序;TreeSet通过TreeMap实现,保证排序;HashMap使用数组+链表/红黑树,线程不安全;而HashTable是线程安全的哈希表。

目录

一:Iterable接口

二.Collection接口

三:List接口

3.1 ArrayList类

3.1.1 介绍

3.1.2 底层

3.1.2 线程安全问题

3.2 Vector类

3.2.1 底层实现

3.2.2扩容

3.2.3 线程安全性

3.2.4 vector与ArrayList比较

3.3 LinkedList

3.3.1 底层实现

3.3.2 容量问题

3.3.3 使用问题

3.3.4 线程安全性

4. Set接口

4.1 HashSet

4.1.1 底层

4.2 LinkedHashSet类

4.2.1 底层实现

4.2.2 LInkedHashSet与HashSet区别

4.3 TreeSet

4.3.1 介绍    

4.3.2 底层

4.3.3 线程安全性

5. Map类

5.1 HashMap类

5.1.1 底层实现

5.1.2 初始容量

5.1.3 Hash数据结构

5.1.4 线程安全性

5.1.5 hashmap的扩容问题

5.2 LinkedHashMap类

5.3  TreeMap类

5.3.1 底层实现

5.3.2 介绍:

5.3.3 对象增加的过程

5.4 HashTable

5.4.1 底层实现

5.4.2 线程安全性

5.4.3 HashTable和hashMap的区别

5.4.3 HashMap,TreeMap, LinkedHashMap区别


一:Iterable接口

    定义了迭代集合的迭代方法

   Iterator对应方法及其描述

    1)hasNext(): 返回迭代器是否有更多元素

    2)next返回下一个位置元素

    3)remove() : 移除元素

【注意】:每次产生一个迭代器,他总是指向第一个元素的前面位置

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class review01 {
    public static void main(String[] args) {
       //虽然容量大小定义为2,但是随着元素加入,其容量也自动增长
        List<Integer> list1 = new ArrayList<Integer>(2);
        System.out.println(list1.size());   // 0
        list1.add(3);
        list1.add(5);
        list1.add(8);
        list1.add(1);
        Iterator iterator = list1.iterator();
        while(iterator.hasNext()){
            System.out.println(iterator.next()); // 3 5 8 1
        }
    }
}

二.Collection接口

定义了集合中通用方法

  • size()
  • isEmpty()
  • contains()
  • add()
  • addAll()
  • remove()
  • removeAll()
  • toArray()

【注意】:1. collection中是contains(),而map是containsKey()

                2.toArray()方法object[] arr = coll.toArray()

三:List接口

1)元素被添加到集合中以后,取出的时候是按照放入顺序的

2)List可以重复

3)存在下表,可以直接依靠下标取值

一些常用方法:

  • get()
  • set()
  • indexOf()
  • lastIndexOf()

3.1 ArrayList类

3.1.1 介绍

      ArrayList是List接口的可变数组的实现。实现了所有可选列表操作,并允许包括 null 在内的所有元素。

除了实现 List 接口外,此类还提供一些方法来操作内部用来存储列表的数组的大小。

     每个ArrayList实例都有一个容量,该容量是指用来存储列表元素的数组的大小。它总是至少等于列表的大小。

随着向ArrayList中不断添加元素,其容量也自动增长。自动增长会带来数据向新数组的重新拷贝,

因此,如果可预知数据量的多少,可在构造ArrayList时指定其容量。在添加大量元素前,

应用程序也可以使用ensureCapacity操作来增加ArrayList实例的容量,这可以减少递增式再分配的数量。

3.1.2 底层

1)底层使用的数据结构    

ArrayList底层使用可变数组保存元素,其操作基本上是对数组的操作

private transient Object[] elementData;

2)构造方法

    提供了三种方式的构造器,可以构造一个默认初始容量为10的空列表,或者构造一个指定初始容量的空列表,

或者构造一个包含指定collection的元素的列表,这些元素按照该collection的迭代器返回它们的顺序排列。

public ArrayList() {  
    this(10);  
}  
  
public ArrayList(int initialCapacity) {  
    super();  
    if (initialCapacity < 0)  
        throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity);  
    this.elementData = new Object[initialCapacity];  
}  
  
public ArrayList(Collection<? extends E> c) {  
    elementData = c.toArray();  
    size = elementData.length;  
    // c.toArray might (incorrectly) not return Object[] (see 6260652)  
    if (elementData.getClass() != Object[].class)  
        elementData = Arrays.copyOf(elementData, size, Object[].class);  
}  

3)调整数组容量

每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,

数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。

在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。

  • 初始容量为10。
  • 当数组容量不够是自动扩容为以前的1.5倍
  • 数组最大容量为Integer.MAX_VALUE-8
  •  ArrayList还给我们提供了将底层数组的容量调整为当前列表保存的实际元素的大小的功能。

        它可以通过trimToSize方法来实现

public void trimToSize() {  
    modCount++;  
    int oldCapacity = elementData.length;  
    if (size < oldCapacity) {  
        elementData = Arrays.copyOf(elementData, size);  
    }  
}  

3.1.2 线程安全问题

    线程不安全

3.2 Vector类

3.2.1 底层实现

public synchronized boolean add(E e) {
        modCount++;
        ensureCapacityHelper(elementCount + 1);
        elementData[elementCount++] = e;
        return true;
}
public synchronized E get(int index) {
        if (index >= elementCount)
            throw new ArrayIndexOutOfBoundsException(index);

        return elementData(index);
}

可以看到vector都增加了syncronized关键字,也就是说,vector的实现与ArrayList类似,但是使用了synchronized进行同步

3.2.2扩容

protected int capacityIncrement;
// 这里是int类型,扩容的时候不是算比例,而是直接跟capacityIncrement相加。

1)vector的构造函数可以传入capacityIncrement参数,它的作用是在扩容数组时,使得数组容量增加

capacityIncreament个。如果这个参数的数值小于等于0,扩容时候每次令capacity为原来的两倍。

2)数组的最大容量为

private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

3.2.3 线程安全性

    线程安全

3.2.4 vector与ArrayList比较

    1)vector是同步的,因此开销就比ArrayList大,访问速度更慢,最好使用ArrayList而不是Vector,因为

同步操作完全可以有程序员自己来控制

    2)vector每次扩容请求其大小的2倍(也可以通过构造函数设置增长的容量),而ArrayList是1.5倍

3.3 LinkedList

3.3.1 底层实现

     LinkedList的底层是通过链表来实现的

     LinkedList类是List接口的实现类,它是一个集合,可以根据索引来随机访问集合中的元素,

还实现了Deque接口,它还是一个队列,可以被当成双端队列来使用。LinkedList的底层是通过链表

来实现的,因此它的随机访问速度是比较差的,但是它的删除,插入操作会很快。

3.3.2 容量问题

    linkedList没有长度的概念,所以不存在容量不足的问题,因此不需要提供初始化大小的构造方法。

3.3.3 使用问题

    LinkedList是一个功能强大的类,可以被当做LIst集合,双端队列,栈来使用。

3.3.4 线程安全性

    线程不安全,没有实现同步

4. Set接口

  • 插入无序
  • 元素不能重复
  • 底层均为Map集合实现

4.1 HashSet

4.1.1 底层

     底层基于HashMap

    //键
	private transient HashMap<e,object> map;	
    // 值
    private static final Object PRESENT = new Object();
		//构造
	public HashSet() {
        map = new HashMap&lt;&gt;();
    }

4.2 LinkedHashSet类

4.2.1 底层实现

    底层基于LinkedHashMap实现,通过LInkedHashMap中的方法实现了顺序存值

具体来说,LinkedHashSet,它继承HashSet,又基于LinkedHashMap来实现。

     LinkedHashSet底层使用LinkedHashMap来保存所有元素,它继承与HashSet,其所有的方法操作上又与HashSet相同

因此 LinkedHashSet 的实现上非常简单,只提供了四个构造方法,并通过传递一个标识参数,调用父类的构造器,

底层构造一个LinkedHashMap来实现,在相关操作上与父类HashSet的操作相同,直接调用父类HashSet的方法即可

4.2.2 LInkedHashSet与HashSet区别

    LInkedHashSet维护着一个运行与所有条目的双重链接列表,此链表定义了迭代顺序,该迭代

顺序可为插入顺序或者是访问顺序

4.3 TreeSet

4.3.1 介绍    

TreeSet实现了SortedSet接口,它是一个有序的集合类,TreeSet的底层是通过TreeMap实现的。

TreeSet并不是根据插入的顺序来排序,而是根据实际的值的大小来排序。TreeSet也支持两种排序方式:

  • 自然排序
  • 自定义排序

4.3.2 底层

构造方法源码解析

//相同包下可以访问的构造方法,将指定的m赋值为m 
TreeSet(NavigableMap<E,Object> m) {
    this.m = m;
}
//无参构造方法,创建一个空的TreeMap对象,并调用上面的构造方法
public TreeSet() {
    this(new TreeMap<E,Object>());
}
//指定比较器,并用指定的比较器创建TreeMap对象
public TreeSet(Comparator<? super E> comparator) {
    this(new TreeMap<>(comparator));
}
//将指定的集合C转化为TreeSet
public TreeSet(Collection<? extends E> c) {
    this();
    addAll(c);
}
//将SortedMap中的元素转化为TreeMap对象
public TreeSet(SortedSet<E> s) {
    this(s.comparator());
    addAll(s);
}

     通过构造方法,可以看出TreeSet的底层是用TreeMap实现的。在构造方法中会创建一个TreeMap实例,

用于存放元素,另外TreeSet是有序的,也提供了制定比较器的构造函数。,如果没有提供比较器,

则采用key的自然顺序进行比较大小,如果指定的比较器,则采用指定的比较器,进行key值大小的比较。

4.3.3 线程安全性

     线程不安全,TreeSet是非同步的。

5. Map类

介绍

  • 1)以键值对的形式存放数据
  • 2)定义了通用的方法
  • 3)不可重复

通用方法

  • size()
  • isEmpty()
  • containsKey()
  • containsValue()
  • get()
  • put()
  • remove()
  • keyset()
  • values()
  • entrySet()

Entry类

   是Map类的内部类,用来获取所有的键值

5.1 HashMap类

5.1.1 底层实现

    数组+链表   或者    红黑树

//保存的数组,初始化16个
transient Node<k,v>[] table;
//为entrySet和value提供一个缓存
transient Set<map.entry<k,v>&gt; entrySet;
//元素的数量
transient int size;
//初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 &lt;&lt; 4; // aka 16
//最大容量
static final int MAXIMUM_CAPACITY = 1 &lt;&lt; 30;
//数组递增的策略 当size &gt; capacity*loadFacotor的时候递增
final float loadFactor;

 数组和链表的结合体如下所示:

5.1.2 初始容量

  hash算法,保证hash值平均分布,只有当为16的时候才可以最大程度的保证平均分布

//hash算法,保证哈希值平均分布,只有当为16的时候才可以最大程度的保证平均分布
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h &gt;&gt;&gt; 16);
}

5.1.3 Hash数据结构

    1)在Java中所有的数据结构都可以使用数组和引用来实现。而hash也成散列,就是一个链表加数组实现

hash数据结构具有无序的特征,这里的无序指的是存入顺序和取出顺序不一样。

    2)hash负载因子:hash负载因子代表了hash 表的空间填充度,即负载因子越大,其对空间的使用率越高,但是

这也造成了查询速度慢,而负载因子越小,其查询速度越快,空间填充度越低。在使用过程中一般会保持一个平衡。

例如HashMap的负载因子初始化为0.75,保证了两者之间的权衡。

   3)hash表如何存取数据:hash表的每一次存储都会先调用一个hash函数,而这个hash函数最后运算的值就是

所存储数据的下标。即当需要查询数据的时候,仅仅只需要调用hash函数进行一次计算就可以得出该数据所在的下标。

5.1.4 HashMap中数据结构的实现

    解析HashMap中Hash表的实现原理:

    在HashMap初始化的时候,首先会给内部的负载因子赋值为0.75,然后创建对象,

注意此时的HashMap内部的Node数组并没有实例化。

    开始put数据,此时put方法会调用putVal()方法,但是在调用这个putVal方法之前,它首先通过hash算法计算了一次

这个key所对应的hash值,而在putVal()方法中,又将这个hash值通过和数组的容量-1进行&运算,

得出了在这个数组的容量范围内的一个index。此时,这个key所需要存储的index正式确定。

     确定key之后,需要判断该index下有没有值,如果有,判断新增的这个元素与现有的这个元素是否相同,如果相同,替换该值。

如果不同,遍历整个链表,判断这个链表中是否存在和新增元素相同的值,如果不存在则直接添加到链表尾部,如果存在,替换该值;

当然,如果此时链表中节点的个数大于或者等于8且数组的容量大于64的时候以后就将链表转化为红黑树。

    containsKey()方法的实现,就是直接通过hash方法计算出hash值,然后通过&运算,获取数组下标,判断

这个下标是否为该值,如果不是,则进行遍历链表或者红黑树。

hashmap存储的源代码

public V put(K key, V value) {  
    // HashMap允许存放null键和null值。  
    // 当key为null时,调用putForNullKey方法,将value放置在数组第一个位置。  
    if (key == null)  
        return putForNullKey(value);  
    // 根据key的keyCode重新计算hash值。  
    int hash = hash(key.hashCode());  
    // 搜索指定hash值在对应table中的索引。  
    int i = indexFor(hash, table.length);  
    // 如果 i 索引处的 Entry 不为 null,通过循环不断遍历 e 元素的下一个元素。  
    for (Entry<K,V> e = table[i]; e != null; e = e.next) {  
        Object k;  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {  
            V oldValue = e.value;  
            e.value = value;  
            e.recordAccess(this);  
            return oldValue;  
        }  
    }  
    // 如果i索引处的Entry为null,表明此处还没有Entry。  
    modCount++;  
    // 将key、value添加到i索引处。  
    addEntry(hash, key, value, i);  
    return null;  
}  

解释:当我们往HashMap中put元素的时候,先根据key的hashCode重新计算hash值,

根据hash值得到这个元素在数组中的位置(即下标),如果数组该位置上已经存放有其他元素了,

那么在这个位置上的元素将以链表的形式存放,新加入的放在链头,最先加入的放在链尾。

如果数组该位置上没有元素,就直接将该元素放到此数组中的该位置上。

hashmap读取

public V get(Object key) {  
    if (key == null)  
        return getForNullKey();  
    int hash = hash(key.hashCode());  
    for (Entry<K,V> e = table[indexFor(hash, table.length)];  
        e != null;  
        e = e.next) {  
        Object k;  
        if (e.hash == hash && ((k = e.key) == key || key.equals(k)))  
            return e.value;  
    }  
    return null;  
}  

 解释:从HashMap中get元素时,首先计算key的hashCode,找到数组中对应位置的某一元素,

然后通过key的equals方法在对应位置的链表中找到需要的元素。

5.1.4 线程安全性

      不安全

5.1.5 hashmap的扩容问题

     当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。

所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,

这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:

原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。

      那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小*loadFactor时,

就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,

数组大小为16,那么当HashMap中元素个数超过16*0.75=12的时候,就把数组的大小扩展为 2*16=32,

即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,

所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

5.2 LinkedHashMap类

     LinkedHashMap类可以顺序的输出用户所输入的数据。

     LinkedHashMap中定义了一个Entry类,继承了HashMap.Node节点类,额外定义了两个属性,before和after,还有

最重要的一个方法newNode,这个方法被LInkededHashMap重写,确定了顺序性

5.3  TreeMap类

5.3.1 底层实现

    红黑树

5.3.2 介绍:

  •     继承了NavigableMap接口,NavigableMap接口继承了SortedMap接口
  •     实现了Cloneable接口,可被克隆
  •     自然排序

5.3.3 对象增加的过程

    创建一个TreeMap,此时可以传入一个比较器,如果不传入按照默认的自然顺序进行比较

    put对象,首先,检查该root节点是否为null,如果为null,检查当前传入key是否为null,不为null的话,则

直接创建一个root节点。如果当前root节点有值,则通过二分查找,寻找当前可以进行添加的父节点,找到以后按照比较器规则进行

添加,添加之后,红黑树自动实现平衡。

5.4 HashTable

5.4.1 底层实现

     哈希表 + 链表

5.4.2 线程安全性

     安全

5.4.3 HashTable和hashMap的区别

5.4.3 HashMap,TreeMap, LinkedHashMap区别

一般情况下,我们用的最多的是HashMap,HashMap里面存入的键值对在取出的时候是随机的,

它根据键的HashCode值存储数据,根据键可以直接获取它的值,具有很快的访问速度。

在Map 中插入、删除和定位元素,HashMap 是最好的选择。
TreeMap取出来的是排序后的键值对。但如果您要按自然顺序或自定义顺序遍历键,那么TreeMap会更好。
LinkedHashMap 是HashMap的一个子类,如果需要输出的顺序和输入的相同,

那么用LinkedHashMap可以实现,它还可以按读取顺序来排列,像连接池中可以应用。

补充:

  • HashSet是通过HashMap实现的,TreeSet是通过TreeMap实现的,只不过Set用的只是Map的key
  • Map的key和Set都有一个共同的特性就是集合的唯一性.TreeMap更是多了一个排序的功能
  • hashCode和equal()是HashMap用的, 因为无需排序所以只需要关注定位和唯一性即可
    a. hashCode是用来计算hash值的,hash值是用来确定hash表索引的.
    b. hash表中的一个索引处存放的是一张链表, 所以还要通过equal方法循环比较链上的每一个对象 才可以真正定位到键值对应的Entry.
    c. put时,如果hash表中没定位到,就在链表前加一个Entry,如果定位到了,则更换Entry中的value,并返回旧value
  • 由于TreeMap需要排序,所以需要一个Comparator为键值进行大小比较.当然也是用Comparator定位的.
    a. Comparator可以在创建TreeMap时指定
    b. 如果创建时没有确定,那么就会使用key.compareTo()方法,这就要求key必须实现Comparable接口.
    c. TreeMap是使用Tree数据结构实现的,所以使用compare接口就可以完成定位了.

参考

https://my.oschina.net/onlyzuo/blog/4701582

https://www.iteye.com/category/101360

https://www.iteye.com/blog/zhangshixi-673143

https://www.jianshu.com/p/3b5e2677935d

https://www.iteye.com/blog/zhangshixi-672697

https://www.jianshu.com/p/3c3e592e92c3

 

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值