Collection和Map下的常用实现类

1,811 阅读12分钟

Collection和Map下的常用实现类

Collection

Collection 被List和Set接口继承,其主要的关系图为:

List

List下面的实现类元素都是有序的并且允许元素重复,常用实现类有三个:ArrayList ,Vector,LinkedList ,下面就从以下几个角度来对这几个常用实现类进行介绍 :

  • 底层存储数据的数据的结构
  • 扩容的机制
  • 常用的方法

ArrayList

1.ArrayList 的底层数据结构

private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
transient Object[] elementData;  
public ArrayList() {
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }//从ArrayList的构造器中可以看出,其底层的数据结构就是维护了一个Object[]的一个数组

2.ArrayList的扩容机制

  1. 无参构造器

    当调用其无参构造器的时候,只是初始化了一个 Object[]的一个空数组,那将来在存入数据的时候必然要对其进行扩容,扩容的过程是怎样的呢?直接看源码就就好了,这里用其add方法debug进行探究

    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确定一个最小的数组容量minCapacity,保证元素能够加进去
        elementData[size++] = e;
        return true;
    }//接着会进入ensureCapacityInternal()函数确定容量  
     //先进入calculateCapacity()函数计算容量然后决定扩不扩容
    private static int calculateCapacity(Object[] elementData, int minCapacity) {
        if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
            return Math.max(DEFAULT_CAPACITY, minCapacity);//第一次添加给数组一个默认的size为10
        }
        return minCapacity;//当添加的元素超过10的时候就会返回此时的minCapacity
    }  
     //如果minCapacity大于了此时的elementData.length时就会对其进行扩容
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
    }
    //这里开始对数组进行扩容
    private void grow(int minCapacity) {
        // overflow-conscious code
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + (oldCapacity >> 1);
        //从这里可以看出当数组存入的元素个数大于10之后,进行第一次扩容,每次扩容为上一次容量的1.5倍
        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);//这里的copyof()函数保证原来的数据不会被覆盖
    }
    

    总结一下,ArrayList的扩容机制为首先会给一个默认的size10,这里并不是调用构造器给的,而是在添加的时候,然后后面如果存入的元素大于了10,就按照1.5倍进行扩容。

  2. 有参构造器

    //调用有参构造器就会给定初始数组的size
    public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    //之后就会继续走无参构造器的那条线
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // 确定一个最小的数组容量minCapacity,保证元素能够加进去
        elementData[size++] = e;
        return true;
    
    

    总结一下,有参构造器就是在初始的时候就给了数组一个初始值,之后元素数量超过之后也是按照上一次容量的1.5倍进行扩容

3.常用的方法

package generic_exercise;

import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Comparator;

@SuppressWarnings({"all"})

public class ArrayListTest {
    public static void main(String[] args) {
        ArrayList list = new ArrayList<>();
        list.add(1);
        list.add("jack");
        ArrayList list1 = new ArrayList<>(10);
        list1.add(1);
        list1.add("jack");
        //增删改查
        //增加
        list.add(2);
        list.add(1, 3);//在指定索引的位置加元素,并不会造成替换
        System.out.println(list);
        list.addAll(list1);//直接在list后面加一个列表进去
        System.out.println(list);
        //删除
       list.remove(1);//按照索引进行删除
       list.remove("jack");//按照对象进行删除
       list.removeAll(list1);//直接删除一个指定的list
       list.clear();// 清空list
        //查
        list.contains("jack");//查找对象有没有
        System.out.println(list.containsAll(list1));//查找list中有没有list1这个list
        //改
        System.out.println(list);
        list.set(1, "tom");//替换指定索引上的元素
        System.out.println(list);
        //其他常用方法
        list.size();
        list.indexOf("jack");
        list.lastIndexOf("jack");
        System.out.println(list.subList(1, 3)); //在list中截取一个左闭右开的list
        list.add(3,"jacky");
        //排序
        list.subList(1,3).sort(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                String str1 = (String)o1;
                String str2 = (String)o2;
                return str2.length()-str1.length();
            }
        });//按照字符串的长度进行排序
        System.out.println(list.subList(1, 3));

        list.subList(1,4).sort(new Comparator() {
            @Override
            public int compare(Object o1, Object o2) {
                String str1 = (String)o1;
                String str2 = (String)o2;
                return str2.compareTo(str1);
            }
        });//说明一下compareto方法:1.取字符串的较小的长度从第一个字母开始比较,
           // 不一样就返回这两个字母的差值
           // 2.如果两个字符串前面都一样比如(jack,jacky),就返回两个字符串的长度差
        /*
        public int compareTo(String anotherString) {
        int len1 = value.length;
        int len2 = anotherString.value.length;
        int lim = Math.min(len1, len2);
        char v1[] = value;
        char v2[] = anotherString.value;

        int k = 0;
        while (k < lim) {
            char c1 = v1[k];
            char c2 = v2[k];
            if (c1 != c2) {
                return c1 - c2;
            }
            k++;
        }
        return len1 - len2;
    }
         */
        System.out.println(list.subList(1, 4));

    }

Vector

Vector 和 ArrayList 差不多,主要区别就在于Vector的方法中有synchronized关键字修饰,它是线程安全的,如果是多线程的情况下,优先使用Vector。

1.Vector 的底层数据结构

 this.elementData = new Object[initialCapacity];

从这里可以看出Vector的底层维护的依然是一个Object[ ]数组。

2.Vector的扩容机制

  1. 无参构造器

      public Vector(int initialCapacity, int capacityIncrement) {
            super();
            if (initialCapacity < 0)
                throw new IllegalArgumentException("Illegal Capacity: "+
                                                   initialCapacity);
            this.elementData = new Object[initialCapacity];
            this.capacityIncrement = capacityIncrement;
        }
    
    public Vector(int initialCapacity) {
            this(initialCapacity, 0);
        }
    
    public Vector() {
            this(10);
    }//构造器重载
    

    从构造器可以看出来,其构造器重载,调用无参构造器的时候,此时会给底层的数组一个默认的size(10),当超过这个size时也会进行扩容:

    private void ensureCapacityHelper(int minCapacity) {    
        if (minCapacity - elementData.length > 0)
            grow(minCapacity);
    }
    private void grow(int minCapacity) {   
        int oldCapacity = elementData.length;
        int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                         capacityIncrement : oldCapacity);//无参构造器每次2倍扩容
        if (newCapacity - minCapacity < 0)
            newCapacity = minCapacity;
        if (newCapacity - MAX_ARRAY_SIZE > 0)
            newCapacity = hugeCapacity(minCapacity);
        elementData = Arrays.copyOf(elementData, newCapacity);
    }
    
    
  2. 有参构造器

    有参构造器和无参构造器没有太大区别,只是给数组的初始值不同而已,除此之外,其扩容方式从上面源代码可以看出实际上有两种方式,一是不指定构造器capacityIncrement,那每次就按照2倍扩容,指定capacityIncrement,每次就在上一次的基础上加capacityIncrement个就可以了

总结一下,Vector调用构造器就给底层的数组一个默认的size,而ArrayList则是在添加元素的时候给一个size,默认值都是10,Vector扩容可以是2倍也可以指定参数增长,ArrayList只能是1.5倍进行扩容。

3.常用方法

Vector 的常用方法与ArrayList 基本差不多,这里就不在赘述了,其中有element的方法暂时还没有接触到,后面接触到了进行补充

LinkedList

1.底层存储数据的数据的结构

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

LinkedList 底层维护的是一个双向链表,链表组成就是由LinkedList 中的一个内部类 Node 结点通过 Node next Node prev 还有 Node last,Node first结合成的,下面用一个图来说明:

2.扩容的机制

LinkedList 从源码分析,它是不需要进行扩容的,每次增加一个数据它就会new一个Node来保存数据,同时通过游标的移动来更换双向链表的头部和尾巴,下面就以增加一个数据为例进行分析:

void linkLast(E e) {
    final Node<E> l = last;
    final Node<E> newNode = new Node<>(l, e, null);
    last = newNode;
    if (l == null)
        first = newNode;
    else
        l.next = newNode;
    size++;
    modCount++;
}

从上面的源码以及游标的移动可以知道,LinkedList 默认增加数据是在双向链表的尾部后面进行添加的

3.常用的方法

常用方法和前面的ArrayList没有什么太大的区别,这里就不用在进行探究了。

ArrayList VS Vector VS LinkedList

  • ArrayList,Vector ,LinkedList其三者都实现了List接口,从而其元素都是有序的都支持索引,ArrayList,Vector 底层维护的是一个object[ ]数组,LinkedList底层维护的是一个双向链表,就数组来说的话,其内存为连续的地址,大小固定,从而不适合进行数据的动态存储,但是查询的速度会比较快,双向链表代销可变,适合动态存储数据以及对数据进行删除和添加,但是查询只能按照游标的方向进行查询,因此查询会比较慢
  • ArrayList,Vector 的get()和set()方法性能优于LinkedList,LinkedList的remove() 和add()方法优于ArrayList,Vector
  • ArrayList,LinkedList没有synchronized修饰,线程不安全,Vector线程安全,但是导致效率略低于ArrayList

Set

Set接口下面的实现类主要有三个, HashSet,LinkedHashSet ,TreeSet ,这三个实现类里面的元素都是不允许重复的,这是因为这

HashSet

1.底层存储数据的数据的结构

public HashSet() {
    map = new HashMap<>();
}//底层是一个hashmap
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;
    }//Node里面有key,value,hash,Node<K,V> next四个属性

 transient Node<K,V>[] table;//数据存储在一个由Node构成的table表中

从 HashSet 的构造器可以看出,其实其底层为HashMap,HashMap底层维护的是一个哈希表即(数组+链表+红黑树),所以其底层的数据的结构也为哈希表

2.扩容的机制

从上述的源码可以看出在调用HashSet的构造器的时候并没有给底层的数组进行size的分配,所以在第一次添加元素的时候就要对其进行扩容,具体的扩容从下面的源码进行分析:

//首先创建一个HashMap
public HashSet() {
    map = new HashMap<>();
}
//调用map的put方法,这里是单列数据,所以PRESENT是一个 Object PRESENT = new Object(),用来占位
 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }
//在调用putVal方法之前先算一下key的hash值,为后面将其转换为Node<K,V>[] table这个数组的下标做准备
  public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }
//key的计算,会调用这个传进来的key的key.hashCode()方法再异或其无符号右移16得到key的hash值(注意这里传进来的对象如果重写了hashCode()方法会进行动态绑定的
   static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//然后调用这个putVal方法
 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;//第一次添加数据会调用resize()方法进行扩容,将数组的size设置为16
     //阈值为0.75*16=12走的下面代码
        
     /*
        final Node<K,V>[] resize() {
        Node<K,V>[] oldTab = table;
        int oldCap = (oldTab == null) ? 0 : oldTab.length;
        int oldThr = threshold;
        int newCap, newThr = 0;  
        else {               
            newCap = DEFAULT_INITIAL_CAPACITY;//DEFAULT_INITIAL_CAPACITY =16
            newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);//DEFAULT_LOAD_FACTOR=0.75
        }     
     
     */
     /*
        if (++size > threshold)
            resize();
        下一次如果需要扩容的话(扩容与否取决于上一次扩容的阈值),那就size*2,阈值*2 按照(16(12)->32(24)->..)
        进行走的下面代码:
          else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
                newThr = oldThr << 1;      
       
     */
          
        if ((p = tab[i = (n - 1) & hash]) == null)//计算当前数据需要存放在table表中的索引,如果当前索引位         置下没有存放Node,下面就直接创建一个Node,直接存放即可
            tab[i] = newNode(hash, key, value, null);
        else {/*果有Node的话(说明当前传进来的对象的hash值和在这个索引位置上的Node中存的对象hash值一样),那
           就进行进一步的比较
           */                      
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;//hash值相同并且对象的内容也相同就走e!=null,不进行添加
            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) 
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { 
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }//如果传进来的数据是双列的话那就将Value替换为当前对象的Value
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

简单梳理一下HashSet的扩容机制,在调用HashSet的无参构造器的时候会创建一个HashMap,是一个 Node<K,V>[] table,table这个数组中就是一个个Node来存放数据,Node之间又会形成链甚至可以树化,第一次存放数据时,会给table数组一个默认的size(16),还有一个阈值16*0.75=12,第二次存放数据时,那么这个时候就会通过传入的对象的key来计算hash值继而确定将这个对象存在table数组中的索引,如果hash值和上个存入对象的一样那就再比较内容(equals()),内容一样则放弃存储(双列数据会替换value),如果内容不一样将挂在这个索引下的Node后面,形成链,如果开始的hash值不一样,那就直接new Node 放在table[]数组中,当size大于12时,table就会2倍扩容,阈值也是二倍增加,当table数组size大于64然后一条链的长度大于8,那这条链就会树化。

3.常用的方法

常用方法就是add(),remove(),size(),iterator(),contain(),不支持索引。

LinkedHashSet

LinkedHashSet 是HashSet的一个实现子类,与HashSet不同的是其元素的存取顺序一致

1.底层数据结构

从上述图片可以看出LinkedHashSet底层维护的是一个LinkedHashMap,和之前HashSet不同,维护的是一个HashMap,HashMap的table数组中存的是Node,而LinkedHashSet 的table数组中存的是一个Entery,还可以看到Entery是LinkedHashMap的一个内部类,Node里面可以存Entry说明了LinkedHashMap 中的Entry 继承了HashMap中的Node,除此之外,这个Entry属性里面有before和after所以其实LinkedHashSet 和LinkedHashMap底层的数据结构应该是一个数组加双向链表的形式,构成了双向链表所以看起来数据是有序的,结构如下图:

2.扩容机制

//调用无参构造器,table数组初始化size(16),负载因子为0.75
public LinkedHashSet() {
    super(16, .75f, true);
}    
//增加数据仍然和HashSet一致,后面分析就和前面一样了
 public boolean add(E e) {
        return map.put(e, PRESENT)==null;
    }


3.常用方法

LinkedHashSet 常用方法和HashSet是差不多的,不再赘述。

TreeSet

1.底层数据结构

可以看到TreeSet底层维护的是一个TreeMap,TreeMap 底层维护的是一个红黑树,以Entry为结点的,TreeSet在添加数据的时候就可以用其带有比较器的构造器,从而实现添加有序,至于比较器前面已经说过了,添加有序取出自然有序,值得注意的是添加元素的时候,依然不可以重复,这个重复的标准由比较器指定。

2.扩容机制

这里底层是红黑树,所以就不存在需要去扩容了。

3.常用方法

常用方法和前面的HashSet差不多,只不过其可以有顺序,所以多了一些有序的方法比如first(),last(),floor(),ceiling()等等。

Map

Map 接口下面常用的实现类就有 HashMap,HashTable,TreeMap,LinkedHashMap,properties,继承关系如下:

对于HashMap,TreeMap,LinkedHashMap,在前面分析HashSet,LinkedHashSet,TreeSet都分析了底层的数据结构了在这里就不再赘述了,但是前面没有说Map下面数据的组织形式,下面用一张图来进行说明:

其中可以通过keySet,values,entrySet 对Map进行遍历,下面就重点说一说 HashTable

HashTable

HashTable 是线程安全的,其中的元素(key,value)不允许为null,HashMap中的key允许有一个为null,value可以有任意多个null,其他用法和HashMap差不多。

1.底层数据结构

可以看到它 的底层数据结构和HashMap比较相似,Entry是其的一个内部类,相当于HashMap中的Node,并且这个Entry实现了Map.Entry的接口。

2.扩容方式

这里扩容的话可以看一下源码:

//首先会给这个数组一个默认的size(11),负载因子0.75
public Hashtable() {
    this(11, 0.75f);
}
//如果超过了负载的值就会进行扩容
  protected void rehash() {
        int oldCapacity = table.length;
        Entry<?,?>[] oldMap = table;

        int newCapacity = (oldCapacity << 1) + 1;//扩容2倍+1
        if (newCapacity - MAX_ARRAY_SIZE > 0) {
            if (oldCapacity == MAX_ARRAY_SIZE)
       

3.常用方法

常用方法和HashMap差不多,就不赘述了。

Properties

Properties主要用于配置文件的一个读取,后面IO流学了继续补充

集合的选型