collection java集合框架&hashmap源码解析

     前言

1 接口和抽象类

2 集合框架类图

2.1 List:比较 ArrrayList、LinkedList 

2.2 Set:比较 TreeSet 、HashSet、LinkedHashSet

2.3 Map:比较 Hashtable、HashMap、TreeMap

3 HashMap 源码分析

3.1 HashMap 内部结构

3.2 来解释一下Node<k,v> 里的hash是什么 ? 

3.3 putVal——从hashMap 初始化、扩容、到树化

3.4 为什么hashmap的容量是2的幂数

 

前言

集合,它是数学中的一个基本概念,表示 由一个或多个确定的元素所构成的整体

由这个概念,我们其实可以理解,像 Integer、Boolen 这些基本类型的包装类,都没有整体的概念,所以并不属于 集合框架范畴,而 map、list、 queue 这些数据类型有集合的概念,所以属于 集合框架的范畴。

1 接口和抽象类

接口和抽象类是Java 面向对象设计的两个基础机制。

接口是对行为的抽象,主要达到 API 定义和实现分离的目的。抽象类 其目的主要是代码复用。Java 类实现 interface 使用 implements,继承 abstract class 则是使用 extends 关键字。

一个类只能extends一个父类,但可以implements多个接口。与此同时,一个接口则可以同时extends多个接口,却不能implements任何接口。因而,Java中的接口是支持多继承的。

接口、抽象类、类有如下的区别:

 

  1. 支持多重继承:接口支持;抽象类不支持;类不支持;
  2. 支持抽象函数:接口语义上支持;抽象类支持;类不支持;
  3. 允许函数实现:接口不允许;抽象类支持;类允许;
  4. 允许实例化:接口不允许;抽象类不允许;类允许;
  5. 允许部分函数实现:接口不允许;抽象类允许;类不允许。
  6. 定义的内容:接口中只能包括public函数以及public static final常量;抽象类与类均无任何限制。

使用时机:当想要支持多重继承,或是为了定义一种类型请使用接口;当打算提供带有部分实现的“模板”类,而将一些功能需要延迟实现请使用抽象类;当你打算提供完整的具体实现请使用类。

在Java 标准库里面,定义了非常多的接口,比如Java.util.List;在Collection框架中,很多通用的部分就被抽取成为抽象类,例如Java.util.AbstractList.

2 集合框架类图

   

(?:手画图有点Low~)

我们可以看到Java 集合框架, Collection 接口是所有集合的根,然后扩展三大类集合,分别是:List、Set、Queue,每种集合的通用逻辑,都被抽象到相应的抽象类中。

List: 有序集合

Set:不允许有重复的元素,这是和List 最明显的区别,用于保证元素唯一性的场合。

Queue:Java 提供的标准队列结构,它支持先入先出(FIFO),或者后入先出(LIFO)等特定行为。这里不包括BlockingQueue,BlockingQueue多用于并发场合,所以放在并发包。

2.1 List:比较 ArrrayList、LinkedList 

List 的框架单独画一下:

                  

这三者都 实现了集合框架中的List, 也就是所谓的有序集合。所以功能比较相近,比如都提供了按照位置进行查找、添加、删除等操作。但是因为具体的实际的区别(见集合框架类图),在使用、性能、线程安全等方面,又有一些区别。

1、底层实现:ArrayList、Vector 是通过数据来实现的; LinkedList 采用双链表来实现。

2、读写性能

ArrayList:

  • 查找——查找元素的时候要遍历数组,对于非null 的元素采用 equals的 方法
  • 删除——删除数组的时候不会缩小数组的容量
  • 插入——插入元素时如果 超过当前数据定义最大值时,数据要扩容,扩容的时候要进行大量的数组复制操作  

LinkedList:

  • 查找——遍历链表
  • 删除——遍历链表,找到要删除的元素
  • 插入——必须先建一个新的Entry对象,并更新相应元素的前后引用

综上,ArrayList 对于元素的增加和删除都会引起数组的内存分配空间变化,所以插入删除数据慢,但是检索速度快。LinkedList 是基于链表存放数据的,增加和删除元素的速度快,但是检索速度慢。

3、ArrayList 和 LinkedList 都是线程不安全的。但是在Collections 工具类中,提供了一系列的 sychronized 方法。

 

2.2 Set:比较 TreeSet 、HashSet、LinkedHashSet

set 不允许有重复的元素,这是和List 最明显的区别,用于保证元素唯一性的场合。

  • TreeSet:支持顺序访问,但是添加、删除等操作效率低
  • HashSet:不保证有序,利用哈希算法,如果哈希散列正常,可以提供 O(1) 的添加、删除
  • LinkedHashSet:内部构建了一个记录插入顺序的双向链表,因此提供了按照插入顺序遍历的能力。插入删除效率低于HashSet,因为维护链表有开销。

 

2.3 Map:比较 Hashtable、HashMap、TreeMap

Map的整体架构再单独画一下:

                           

  • Hashtable:线程安全的,本身是同步的,方法函数采用synchronized修饰。key/value 都不可以为Null。
  • hashMap:不是同步的,线程不安全,key/value 可以为Null。put 和 get 操作的时间复杂度是O(1)。如果HashMap需要同步:(1)可以从Collections的synchronizedMap方法;(2)使用ConcurrentHashMap 类。
  • LinkedHashMap:按照插入顺序排序
  • TreeMap:基于红黑树的一种提供顺序访问的Map,实现了sortedMap接口,按照key排序,put、get、remove 的操作都是O(log(n))的时间复杂度。

 

3 HashMap 源码分析

 一句话总结hashmap:HashMap基于哈希思想,实现对数据的读写。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。当两个不同的键对象的hashcode相同时,它们会储存在同一个bucket位置的链表中,可通过键对象的equals()方法用来找到键值对。如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。

3.1 HashMap 内部结构

HashMap 内部结构是由数据和链表实现的,数组被分为一个个桶(bucket),通过哈希值决定了键值对在这个数据的寻址;哈希值相同的键值对,则以链表的形式存储,如果链表大小超过阈值(TREEIFY_THRESHOLD, 8),链表就会被改造为树形结构。

                                               

从源码中,我们可以看到,HashMap 的初始化数组大小是16,链表的大小是8,负载系数(load factor)是0.75

public class HashMap<K,V> extends AbstractMap<K,V>
    implements Map<K,V>, Cloneable, Serializable {

    private static final long serialVersionUID = 362498820763181265L;

    /**
     * The default initial capacity - MUST be a power of two.
     */
    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    /**
     * The maximum capacity, used if a higher value is implicitly specified
     * by either of the constructors with arguments.
     * MUST be a power of two <= 1<<30.
     */
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * The load factor used when none specified in constructor.
     */
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    /**
     * 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;

    /**
     * The smallest table capacity for which bins may be treeified.
     * (Otherwise the table is resized if too many nodes in a bin.)
     * Should be at least 4 * TREEIFY_THRESHOLD to avoid conflicts
     * between resizing and treeification thresholds.
     */
    static final int MIN_TREEIFY_CAPACITY = 64;

    /**
     * Basic hash bin node, used for most entries.  (See below for
     * TreeNode subclass, and in LinkedHashMap for its Entry subclass.)
     */
    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;
        }
}

再来看一下,数组(Node<K, V>[] table)和链表在怎么定义的呢? 

transient Node<K,V>[] table;   //数组

static class Node<K,V> implements Map.Entry<K,V> {   //节点
        final int hash;   //hash值,并不是key的hashcode,原因是为了解决哈希碰撞
        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;
        }
}

3.2 来解释一下Node<K,V> 里的hash是什么 ? 

我们在put 数据的时候   putVal(hash(key), key, value, false, true);  可以看出来,hash 并不是key的hashcode,而是hashcode 值把高位移到低位,然后进行异或运算,这是因为有些数据计算出来的哈希值的差异主要集中在高位,而HashMap 里的哈希寻址是忽略容量以上的高位的,那么这种处理就可以有效避免类似情况下的哈希碰撞。

/* ---------------- Static utilities -------------- */

    /**
     * Computes key.hashCode() and spreads (XORs) higher bits of hash
     * to lower.  Because the table uses power-of-two masking, sets of
     * hashes that vary only in bits above the current mask will
     * always collide. (Among known examples are sets of Float keys
     * holding consecutive whole numbers in small tables.)  So we
     * apply a transform that spreads the impact of higher bits
     * downward. There is a tradeoff between speed, utility, and
     * quality of bit-spreading. Because many common sets of hashes
     * are already reasonably distributed (so don't benefit from
     * spreading), and because we use trees to handle large sets of
     * collisions in bins, we just XOR some shifted bits in the
     * cheapest possible way to reduce systematic lossage, as well as
     * to incorporate impact of the highest bits that would otherwise
     * never be used in index calculations because of table bounds.
     */
    static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

3.3 putVal——从hashMap 初始化、扩容、到树化

我们再来看一下 putVal 方法,其本身的逻辑非常集中,从hashMap 初始化、扩容、到树化,全部都和这个方法有关。

/**
     * Implements Map.put and related methods
     *
     * @param hash hash for key
     * @param key the key
     * @param value the value to put
     * @param onlyIfAbsent if true, don't change existing value
     * @param evict if false, the table is in creation mode.
     * @return previous value, or null if none
     */
    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

在putVal 方法中,  if (++size > threshold)   resize();  如果 size 大于 threshold (初始是16) 的时候,会resize(),也就是我们说的rehash。扩容主要的开销在于,需要将老的数组中的元素重新放置到新的数组中。

所以 hashMap的预设容量很重要,因为这直接决定了可用桶的大小,桶太多则浪费空间,桶太少,则resize过程 影响性能。那实际开发中应该如何设置呢? 我们知道:负载因子 * 容量 > 元素数量,所以预先设置的容量需要满足,大于“预估元素数量 / 负载因子”,同时它是 2 的幂数。负载因子的话,默认是0.75

 

3.4 为什么hashmap的容量是2的幂数

上面我们知道了hash 是什么,而HashMap 是根据key的hash 值 来决策 这个值到底放到哪个桶里面。 在 putVal 的方法中,我们也可以看到:

if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;

if ((p = tab[i = (n - 1) & hash]) == null)   
    tab[i] = newNode(hash, key, value, null);

n = (tab = resize()).length;  n 表示链表的长度, tab[i = (n - 1) & hash] 实际上是计算出,key 在 tab 中的索引位置。如果n 永远都是2 的幂数的话,那么n-1 就永远是 以连续 1的形式表示(比如: 00001111=31),所以 (n-1)& hash = 保留后x位置1。

例如:10110111 & 00001111 = 00000111(00000111为15), 即如果 大小为32的话,那hash为10110111的就应该放在15的桶里面。

这样做的好处是:

  • &运算速度快,至少比%取模运算块
  • 能保证 索引值 肯定在 capacity 中,不会超出数组长度
  • (n - 1) & hash,当n为2次幂时,会满足一个公式:(n - 1) & hash = hash % n

那么此时,你可能会问,如果我显示的指定hashmap 初始容量就不为2的幂数呢? 会怎么样? 

在我们初始化hashmap的时候,如下代码所示,有this.threshold = tableSizeFor(initialCapacity);  这个tableSizeFor方法 可以保证n 永远都是2的幂等。

public HashMap(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal initial capacity: " +
                                               initialCapacity);
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal load factor: " +
                                               loadFactor);
        this.loadFactor = loadFactor;
        this.threshold = tableSizeFor(initialCapacity);
    }

 /**
     * Returns a power of two size for the given target capacity.
     */
    static final int tableSizeFor(int cap) {
        int n = cap - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

 

 

以上,撸不下去了。?

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值