Java集合框架

本文转载自JavaGuide,地址:https://github.com/Snailclimb/lavaGuide,作者:SnailClimb

一、ArrayList和LinkedList异同

1、线程安全性

ArrayList和LinkedList都是不同步的,不保证线程安全。

2、底层数据结构

ArrayList底层使用的是Object数组;LinkedList底层使用的是双向链表数据结构(JDK 1.6之前为循环链表,之后则取消了循环)

public class ArrayList extends AbstractList
        implements List, RandomAccess, Cloneable, java.io.Serializable
{
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
    transient Object[] elementData; // non-private to simplify nested class access
}
public class LinkedList
    extends AbstractSequentialList
    implements List, Deque, Cloneable, java.io.Serializable
{
    transient int size = 0;
    transient Node first;
    transient Node last;
    //插入两个节点,生成双向链表
    private static class Node {
        E item;
        Node next;
        Node prev;
        Node(Node prev, E element, Node next) {
            this.item = element;
            this.next = next;
            this.prev = prev;
        }
    }
}

3、插入删除是否受元素位置的影响

(1)ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。
比如:执行add(Ee)方法的时候,ArrayList会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是0(1)。
但是如果指定位置 i 插入和删除元素的话【add (int index, E element)】时间复杂度就为O(n-i),因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。

(2)LinkedList采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似0 (1)而数组为近似O (n)。

4、是否支持快速随机访问

快速随机访问就是通过元素的序号快速获取元素对象【对应于get (int index)方法】。
LinkedList因为是双向链表,因此需要从头/尾部开始查找元素,不支持高效的随机元素访问

public class LinkedList{
    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }
    Node node(int index) {
        // assert isElementIndex(index);
        if (index < (size >> 1)) {
            Node x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
}

而ArrayList是数组支持随机访问。

public class ArrayList {
    @SuppressWarnings("unchecked")
    E elementData(int index) {
        return (E) elementData[index];
    }
    public E get(int index) {
        rangeCheck(index);
        return elementData(index);
    }
}

5、内存空间占用

ArrayList的空间浪费主要体现在在lis例表的结尾会预留一定的容量空间

而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList多的空间(因为要存放直接后继和直接前驱以及数据)。

二、ArrayList与Vector的区别

Vector是ArrayList的线程安全的版本
首先,可以看到他们底层都是数组;
其次,Vector是线程安全的,ArrayList不保证线程安全
最后,可以看到Vector和ArrayList的扩容不太一样。

public class Vector{
    protected Object[] elementData;
    protected int elementCount;
    protected int capacityIncrement;
    public Vector(int initialCapacity, int capacityIncrement) {
        super();
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        this.elementData = new Object[initialCapacity];
        this.capacityIncrement = capacityIncrement;
    }
    //如果capacityIncrement大于0,则扩容为原来的容量加上这个capacityIncrement的值
    //如果capacityIncrement不大于0,则扩容为2倍
    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);
    }
    //Vector是线程安全的,这里仅截取add方法的源码来看一下
    public void add(int index, E element) {
        insertElementAt(element, index);
    }
    public synchronized void insertElementAt(E obj, int index) {
        modCount++;
        if (index > elementCount) {
            throw new ArrayIndexOutOfBoundsException(index
                                                     + " > " + elementCount);
        }
        ensureCapacityHelper(elementCount + 1);
        System.arraycopy(elementData, index, elementData, index + 1, elementCount - index);
        elementData[index] = obj;
        elementCount++;
    }
}
public class ArrayList{
    transient Object[] elementData; // non-private to simplify nested class access
    private int size;
    //扩容为原来的1.5倍
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        // minCapacity is usually close to size, so this is a win:
        elementData = Arrays.copyOf(elementData, newCapacity);
    }

    public void add(int index, E element) {
        rangeCheckForAdd(index);

        ensureCapacityInternal(size + 1);  // Increments modCount!!
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
}

三、HashMap的底层实现

JDK1.8之前HashMap底层是数组和链表结合在一起使用也就是链表散列。JDK1.8引入红黑树

1、概述

Java为数据结构中的映射定义了一个接口java.util.Map,此接口主要有四个常用的实现类,分别是HashMap、Hashtable、LinkedHashMap和TreeMap,类继承关系如下图

类的继承图

(1) HashMap:它根据键的hashCode值存储数据,大多数情况下可以直接定位到它的值,因而具有很快的访问速度,但遍历顺序却是不确定的。 HashMap最多只允许一条记录的键为null,允许多条记录的值为null
HashMap非线程安全,即任一时刻可以有多个线程同时写HashMap,可能会导致数据的不一致。如果需要满足线程安全,可以用 Collections的synchronizedMap方法使HashMap具有线程安全的能力,或者使用ConcurrentHashMap。

(2) Hashtable:Hashtable是遗留类,很多映射的常用功能与HashMap类似,不同的是它承自Dictionary类,并且是线程安全的,任一时间只有一个线程能写Hashtable,并发性不如ConcurrentHashMap,因为ConcurrentHashMap引入了分段锁。Hashtable不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。

(3) LinkedHashMap:LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。

(4) TreeMap:TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当用Iterator遍历TreeMap时,得到的记录是排过序的。如果使用排序的映射,建议使用TreeMap。在使用TreeMap时,key必须实现Comparable接口或者在构造TreeMap传入自定义的Comparator,否则会在运行时抛出java.lang.ClassCastException类型的异常。

2、HashMap的原理

HashMap通过key的hashCode经过扰动函数处理过后得到hash值,然后通过(n -1) & hash判断当前元素存放的位置(这里的n指的是数组的长度);
如果当前位置存在元素的话,就判断该元素与要存入的元素的hash值以及key是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。

(1)hash方法
所谓扰动函数指的就是HashMap的hash方法。使用hash方法也就是扰动函数是为了防止一些实现比较差的 hashCode()方法换句话说使用扰动函数之后可以减少碰撞。

JDK 1.8 HashMap 的 hash 方法源码:

    static final int hash(Object key) {
        int h;
        //key.hashCode():返回散列值也就是hashcode
        //>>>:无符号右移,忽略符号位,空位都以0补齐
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

无符号右移16位异或的目的
详见:https://www.cnblogs.com/zxporz/p/11204233.html
由于int型的key.hashCode()有32bit,因此右移16bit后,原来的高16位移动到低16位,前面的高位用0填充,因此异或实际上是,保留高位信息、低位信息变为高位与地位的信息混合

h^(h>>>16) 与 h对比:高位保留,低位变为高位与地位特征的混合

HashMap的初始信息槽位有16位,因此在进行(n-1)&hash后,高位信息直接会被屏蔽,这样相当于丢失了高位的信息,而如果进行了无符号右移16位异或,则确保低位中保留着低位和高位的混合特征,这样便减少了碰撞的概率。
(n-1)&hash后高位信息丢失

可以看到整个的流程如下:


hashmap计算存储位置

JDK 1.7 HashMap的hash方法源码

static int hash(int h) {
    //This function ensures that hashcodes that differ only by 
    // constant multiples at each bit position have a bounded 
    // number of collisions (approximately 8 at default load factor).
    h ^ =(h >>> 20) ^ (h >>> 12);
    return h ^ (h >>> 7) ^ (h >>> 4);
}

jDK 1.8的hash方法相比于jDK 1.7 hash方法更加简化,但是原理不变。

(2)槽位数使用

//JDK 1.7的源码,JDK1.8没有这个方法,但是实现原理一样
static int indexFor(int h, int length) {
    return h & (length-1);
}

得到hash值之后,需要计算存放的位置。HashMap的初始槽位长度为16,而后以2的倍数扩容。
由于采用按位与运算(&)代替取模运算(%),当 length = 2^n 时,a % length = a & (length - 1) 。

(3)拉链法
所谓"拉链法"就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后,在解决hash冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间

hashMap的结构实现:数组+链表+红黑树(JDK1.8增加了红黑树部分)

3、自定义实现HashMap的put、get方法

public class HashMap{
    private static Object[] map=new Object[1024];
    private static final int constant=7;//用于计算hashcode的常量

    public static void put(String key, Object o) {
        int index=hashcode(key);//计算hashcode
        //将key-value封装成对象,存入数组
        Node entry=new Node(key,o);
        //若当前的index位置为空,则创建新的链表,否则取出当前位置的链表,将结点加入到当前链表中
        @SuppressWarnings("unchecked")
        LinkedList> link=(map[index]==null)?
                new LinkedList<>():(LinkedList>)map[index];
        link.add(entry);
        map[index]=link;
    }
    
    public static Object get(String key) {
        int index=hashcode(key);//根据key获取hashcode
        if(map[index]!=null) {//不为空则取出链表,遍历
            @SuppressWarnings("unchecked")
            LinkedList> link=
                    (LinkedList>)map[index];
            for(Node node:link)
                if(node.key.equals(key)) return node.value;
        }
        return null;
    }
    
    public static int hashcode(String key) {
        int sum=0;
        if(key.length()==0) return 0;
        else {
            char[] ch=key.toCharArray();
            for(char c:ch) sum+=(int)c;
            sum*=constant;
        }
        return sum;
    }
    
    static class Node implements Map.Entry{
        final K key;
        V value;
        public Node(K key,V value) {
            this.key=key;
            this.value=value;
        }
        @Override
        public K getKey() {
            return key;
        }

        @Override
        public V getValue() {
            return value;
        }

        @Override
        public V setValue(V value) {
            V old=this.value;
            this.value=value;
            return old;
        }
        
    }
}

4、HashMap多线程操作导致死循环的问题

在多线程下,进行put操作会导致HashMap死循环,原因在于HashMap的扩容resize()方法。由于扩容是新建一个数组,复制原数据到数组。由于数组下标挂有链表,所以需要复制链表,但是多线程操作有可能导致环形链表。复制链表过程如下:

以下模拟2个线程同时扩容,假设,当前HashMap的空间为2 (临界值为1),hashcode分别为0和1,在散列地址0处有元素A和B,这时候要添加元素C,C经过hash运算,得到散列地址为1,这时候由于超过了临界值,空间不够,需要调用resize方法进行扩容,那么在多线程条件下,会出现条件竞争,模拟过翻下:

线程一:读取到当前的HashMap情况,在准备扩容时,线程二介入


线程一准备扩容

线程二:读取HashMap,进行扩容


线程二进行扩容

扩容后,链表中元素的顺序会变为相反。如下图,在加入11后开始扩容,5和9的顺序反转


扩容后链表可能会反转

线程一:继续执行


线程一继续扩容

这个过程为,先将A复制到新的hash表中,然后接着复制B到链头(A的前边:B.next=A),本来B.next=null,
到此也就结束了(跟线程二一样的过程),但是,由于线程二扩容的原因,将B.next=A,所以,这里继续复制A,让 A.next=B,由此,环开多链表出现:B.next=A;A.next=B

四、HashMap和Hashtable的区别

1、线程安全性

HashMap不保证线程安全,但Hashtable是线程安全的,其内部的方法基本上都是被关键字synchronized修饰

public class HashMap extends AbstractMap
    implements Map, Cloneable, Serializable {
    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;
    }
}
public class Hashtable
    extends Dictionary
    implements Map, Cloneable, java.io.Serializable {
    private transient int count;

    public synchronized int size() {
        return count;
    }
    public synchronized boolean isEmpty() {
        return count == 0;
    }
}

2、效率、继承的类不同

因为线程安全的问题,HashMap比HashTable效率要高。另外HashTable基本上被淘汰,如果要保证线程安全就使用ConCurrentHashMap.

HashMap继承自AbstractMap,HashTable继承在Dictionary类,两者都实现了Map接口

3、key-value为null的支持

HashMap中的key仅有一个可以为null,而value为null的记录可以有多条;
HashTable中key-value都不允许为null。这一点可以从HashTable的put源码中看出来。

    public synchronized V put(K key, V value) {
        // Make sure the value is not null
        if (value == null) {
            throw new NullPointerException();
        }
        // Makes sure the key is not already in the hashtable.
        Entry tab[] = table;

        //在此处计算key的hash值,如果此处key为null,则直接抛出空指针异常。
        int hash = key.hashCode();
        int index = (hash & 0x7FFFFFFF) % tab.length;
        @SuppressWarnings("unchecked")
        Entry entry = (Entry)tab[index];
        for(; entry != null ; entry = entry.next) {
            if ((entry.hash == hash) && entry.key.equals(key)) {
                V old = entry.value;
                entry.value = value;
                return old;
            }
        }

        addEntry(hash, key, value, index);
        return null;
    }

4、初始容量和扩容不一样

HashMap:默认大小为16,如果指定大小则会将其扩容为2的幂次方大小。每次扩容,容量都变为原来的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);
    }
    public HashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR);
    }
    /**
     * 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;
    }

HashTable:默认大小为11,如果指定容量则使用指定的大小,之后每次扩容变为原来的2n+1

    //指定参数的构造函数
    public Hashtable(int initialCapacity, float loadFactor) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        if (loadFactor <= 0 || Float.isNaN(loadFactor))
            throw new IllegalArgumentException("Illegal Load: "+loadFactor);

        if (initialCapacity==0)
            initialCapacity = 1;
        this.loadFactor = loadFactor;
        table = new Entry[initialCapacity];
        threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    }
    //扩容方式:2n+1
    protected void rehash() {
        int oldCapacity = table.length;
        Entry[] oldMap = table;

        // overflow-conscious code
        int newCapacity = (oldCapacity << 1) + 1;
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
                // Keep running with MAX_ARRAY_SIZE buckets
                return;
            newCapacity = MAX_ARRAY_SIZE;
        }
        Entry[] newMap = new Entry[newCapacity];

        modCount++;
        threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
        table = newMap;

        for (int i = oldCapacity ; i-- > 0 ;) {
            for (Entry old = (Entry)oldMap[i] ; old != null ; ) {
                Entry e = old;
                old = old.next;

                int index = (e.hash & 0x7FFFFFFF) % newCapacity;
                e.next = (Entry)newMap[index];
                newMap[index] = e;
            }
        }
    }

5、底层数据结构

HashMap和HashTable都是用数组加链表来存储数据
不同点:
JDK1.8之后,HashMap在解决Hash冲突时引入了红黑树(当链表长度大于阈值(默认为8)时)以减少搜索时间。而HashTable没有这样的机制。

五、HashSet和HashMap的区别

HashSet底层就是基于HashMap实现的。(HashSet的源码非常非常少,因为除了 clone()方法、writeObject()方法、readObject()方法是HashSet自己不得实现之外,其他方法都是直接调用HashMap中的方法。)

public class HashSet
    extends AbstractSet
    implements Set, Cloneable, java.io.Serializable
{
    private transient HashMap map;
    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    public HashSet() {
        map = new HashMap<>();
    }
    public boolean add(E e) {
        return map.put(e, PRESENT)==null;//调用HashMap的put方法
    }
}
HashMap和HashSet的区别

六、ConcurrentHashMap和Hashtable的区别

ConcurrentHashMap和Hashtable的区别主要体现在实现线程安全的方式上不同

1、底层数据结构

jDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,jDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
Hashtable和JDK1.8之前的HashMap的底层数据结构类似都是采用数组+链表的形式,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的。

2、实现线程安全的方式(重要)

在JDK1.7的时候,ConcurrentHashMap (分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作,(JDK1.6以后对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构, 但是已经简化了属性,只是为了兼容旧版本;

Hashtable(同一把锁):使用synchronized来保证线程安全, 效率非常低下,当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。

3、两者的对比图

图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html

HashTable全表锁

JDK1.7的时候,ConcurrentHashMap是由Segment数据结构和HashEntry数据结构组成。

首先将数据分为一段段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问

Segment实现了ReentrantLock,所以Segment是一种可重入锁,扮演锁的角色。HashEntry用于存储键值对数据。

    static class Segment extends ReentrantLock implements Serializable {
        final float loadFactor;
        Segment(float lf) { this.loadFactor = lf; }
    }

—个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap类似,是一种数组和链表结构,一个Segment包含一个HashEntry数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个 HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁

JDK1.7的ConcurrentHashMap

JDK1.8的ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构是数组+链表/红黑二叉树。

JDK1.8的ConcurrentHashMap(TreeBin:红黑树 Node:链表结点)

七、集合框架底层数据结构总结

图片来自:https://www.cnblogs.com/jing99/p/7057245.html

Java集合框架图
Java集合框架

1、Collection

(1)List

Arraylist: Object数组
Vector: Object数组
LinkedList:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)

(2)Set

HashSet (无序,唯一):基于HashMap实现的,底层采用HashMap来保存元素

LinkedHashSet: LinkedHashSet继承与HashSet,并且其内部是通过LinkedHashMap来实现的。有点类
似于LinkedHashMap,其内部是基于Hashmap实现一样,不过还是有一点点区别的。

TreeSet(有序,唯一):红黑树(自平衡的排序二叉树)

2、Map

HashMap: jDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。
JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间

LinkedHashMap: LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。
另外,LinkedHashMap在上面结构的基础上,増加了一条双向链表,使得上面的结构可以保证键值对的插入顺序。同时链表进行相应的操作,实现了访问顺糊关逻辑。

HashTable:数组+链表组成的,数组是HashMap的主体,链表则主要是为了解决哈希冲突而存在的

TreeMap: 红黑树(自平衡的排序二叉树)

你可能感兴趣的:(Java集合框架)