简单记录之Java集合源码解析(基于JDK1.8)

话不多说,先上图
简单记录之Java集合源码解析(基于JDK1.8)_第1张图片

简单介绍

Java集合可以分为两大类,一类是以Collection为根接口实现的,另一类就是以Map为根接口实现的,Collection是单列集合,就是元素是单一的值,而Map是双列集合,存储的元素是键值对。OK,下面就来分析

Map

HashMap

数组+链表(红黑树) 的存储结构
扩容机制:数组的扩容是size大于阈值(容量x负载因子),数组扩容为原来的两倍,链表节点个数大于8转为红黑树
关于添加元素的详细过程可以移步我的这篇文章简单记录一下HashMap和ConcurrentHashMap

LinkedHashMap

LinkedHashMap可以保证遍历的顺序和插入元素的数据是一致的,那么问题来了,hash值的计算不是随机的吗,是怎么做到元素的有序的?从名字也可以看出来,linkedHashMap在HashMap的基础上,维护着一个双向链表。下面就来一步步分析,先来看看LinkedHashMap的类图和定义的方法
简单记录之Java集合源码解析(基于JDK1.8)_第2张图片
从类图中可以看到LinkedHashMap是继承自HashMap的,但在LinkedHashMap中却没有重写put方法,但是重写了节点定义的方法,即newNode()和newTreeNode(),这是形成链表的关键。因此LinkedHashMap添加元素的操作调用的是HashMap的put方法。

//这是HashMap的put方法中的片段,LinkedHashMap与HashMap的区别就发生在newNode()方法中
 if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
 	//这是LinkedHashMap中的newNode()方法
 	Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }

	//linkNodeLast()方法,将节点加入到链表尾部
    private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
        	//双向链表的操作
            p.before = last;
            last.after = p;
        }
    }

从代码可知,每一个加入到LinkedHashMap中的元素,不仅是存储在了HashMap中数组+链表的结构中,同时也成为了双向链表中的一个节点,并记录下了head和tail节点,因此在遍历的时候,可以通过双线链表从head遍历到tail。更详细的解析大家可以看看这篇文章LinkedHashMap源码解析

HashTable

HashTale其实和HashMap一样,唯一的区别就是HashTable的所有方法都加上了同步关键字synchronized

TreeMap

TreeMap采用的是红黑树的结构,因此和HashMap的无序存储不一样,TreeMap节点是有序存储的,对于基本类型的包装类来说,排序规则默认是从小到大。对于非基本类型的包装类和更改排序规则后面一起说。这里就简单介绍一下:

//TreeMap的put()方法的核心部分
		int cmp;
        Entry<K,V> parent;
		Comparator<? super K> cpr = comparator;
		do {
             parent = t;
             cmp = cpr.compare(key, t.key);
             if (cmp < 0)
                t = t.left;
             else if (cmp > 0)
                t = t.right;
             else
               return t.setValue(value);
          } while (t != null);
         Entry<K,V> e = new Entry<>(key, value, parent);
         if (cmp < 0)
            parent.left = e;
         else
            parent.right = e;
        //进行调整为一颗红黑树
        fixAfterInsertion(e);
        size++;

从源码可以看到,经过比较会返回一个值cmp,如果cmp<0,那么就会把要加入的key继续去和左子树去比较;cmp>0,就去和右子树比较;相等的话就直接替换value,并返回旧值。直到t为叶子节点,结束循环,根据cmp的值将key加入到左子树还是右子树。并通过fixAfterInsertion(e)方法,调整为一颗红黑树。

Collection

Set

我们常用的Set集合,实际上底层用的就是Map,大家可以通过查看HashSet、LinkedHashSet的源码,可以看到,在构造函数里都是实例化的HashMap和LinkedHashMap。因为Map是基于Key-Value的双列集合,而且Key是不能重复的,所以Set将Map的Value设置为静态的object,就构成了Set集合,也使得HashSet的值是不能重复的。

HashSet—>HashMap

LinkedHashSet---->LinkedHashMap

TreeSet---->TreeMap

Queue

我们先来说说Queue——队列,因为List中会用到Queue。
队列的特性就是先进先出。在Queue这个接口中,定义了队列的基本操作函
add()/remove(),offer()/poll(),peek()查看对头元素。接下来就来说说它的子接口Deque

Deque

Deque也是一个接口,它继承自Queue,所以肯定对队列进行了一个扩展。没错,Deque是一个双端队列,可以在队头和队尾分别进行插入和删除操作,那么基于双端队列,即可以实现队列(先进先出),也可以实现栈(先进后出),LinkedList实现了Deque接口,使得LinkedList是一个双向链表也是一个双端队列,这个在List的部分说。

PriorityQueue

PriorityQueue——优先级队列,实现了Queue接口,从名字就可以看出,这个队列是存在优先级的,也就是说,它是排序的,而排序的规则,就是在TreeMap的源码中提到的Comparable或者CompaComparator,关于比较会在后面说。
那么,在什么时候进行排序的呢?通过阅读源码,可以看到,在插入的时候就会根据规则比较进行节点的排序,队头的元素总是最小的或者最大的
存储结构:利用数组实现的堆(完全二叉树),默认的初始容量是11,而因为优先级的存在,那么就是一个最大堆或者最小堆
扩容机制:原始容量小于64,则扩容一倍,大于64,就扩容50%

List

ArrayList

ArrayList可以说是平常使用最多的线性表了,他的底层的存储结构是数组,默认的初始容量是10,既然是数组就存在扩容机制了,他的扩容机制也比较特别:
size等于数组长度时,扩容50%

LinkedList

LinkedList即实现了List接口,也实现了Deque接口,使得LinkedList是一个双向链表也是一个双端队列

基于Comparable和Comparator的排序规则

首先我们要知道
Comparable是一个泛型接口,里面只定义了一个compareTo()方法

public interface Comparable<T> {
 public int compareTo(T o);
}

Comparator也是一个泛型接口,定义了许多方法(静态方法、default修饰的方法),但关于我们需要实现的比较方法就只有compare()

int compare(T o1, T o2);

在TreeMap和PriorityQueue中他们是如何使用的呢?如果加入集合的对象是非基本类型的包装类(如:Integer,其实Integer也是实现了Comparable接口),那么有两种情况可以进行排序:

  1. 像Integer一样,加入的对象实现了Comparable接口,并重写了compareTo(T o 比较的对象)方法
  2. 在实例化集合的时候,传入了比较器对象,即实现了Comparator接口并重写了compare(T o1加入的对象 ,T o2 比较的对象)方法的对象

然后,集合会先调用比较器对象的compare()方法,如果没有比较器对象,就会去调用加入集合的对象的compareTo(),这里会有一个强转的过程,然后集合会判断返回值的大小进行判断,如果是小于0,就排在左边(左子树),大于0就排在右边(右子树)。
下面就用compare(T o1加入的对象 ,T o2 比较的对象)举例子:

  1. 对于从小到大的排序,compare()内就是retrun(o1-o2),当结果小于0,即o1小的在左边,大的在右边。
  2. 对于从大到小的排序,compare()内就是retrun(o2-o1),当结果小于0,即o1>o2,o1还是排在o2左边;大于0,那么o1排在o2右边,最终给的结果就是大的在左边,小的在右边

写在最后

通过源码的分析,知道了各种集合的底层存储结构,分析他们的特点也就轻而易举了。基于数组的,查询快速,删除插入主要在元素移动的时候比较耗时,而且对于数组来说,初始化的时候指定一个大概的容量,避免以后扩容,扩容也是一个很耗时的操作,例如HashMap、LinkedHashMap、PriorityQueue、ArrayList;基于链表的,插入删除快速,查询主要在遍历元素上很耗时。

你可能感兴趣的:(Java)