java的集合学习

不羁岁月 提交于 2021-01-19 07:14:17

       说到集合,通常初学者会想到数组,可以说数组和集合还是有着根本的区别。初学者不妨了解一下这些区别:

       一、数组声明了它容纳的元素的类型,而集合不声明,如

   int[] array = new int[3] 或者 int[] array = new int[]{1,2,3};

       二、数组是静态的,一个数组实例具有固定的大小,一旦创建了就无法改变容量了。而集合是可以动态扩展容量,可以根据需要动态改变大小,集合提供更多的成员方法,能满足更多的需求。

       三、数组的存放的类型只能是一种(基本类型/引用类型),集合存放的类型可以不是一种(不加泛型时添加的类型是Object)。

      四、数组是java语言中内置的数据类型,是线性排列的,执行效率或者类型检查都是最快的。

       让我们直接深入主题,所有集合都是位于java.util包下,他们主要可以由两个接口派生成的:Collection和Map,Collection和Map是Java集合的框架的根接口,Collection框架接口树形图如下图所示:

其中Set和List接口分别代表了无序集合和有序集合,Queue是java提供的队列的实现。

      而Map框架的树形图如图下所示:

      下面我们就来一一介绍这些集合。

Set

      HashSet类

         HashSet是Set接口典型的实现,我们一般要使用到Set集合的时候就用HashSet这个类,HashSet按Hash算法存储集合中的元素,具有很好的存取和查找性能。HashSet具有以下特点。

         1.无序,不会按照添加的顺序排列;

         2.线程不安全,多个线程操作hashSet会产生线程安全问题;

         3.无重复数据,添加多个相同的对象(equals()和hashCode()都相等),只保留一个值;

         4.集合元素可以是null;

       当向HashSet集合存入一个元素时,HashSet会根据这个元素的hashCode()的值来确定该元素在hashSet中的位置,如果两个元素通过equals()比较相等,但是hashCode()不相等,那么hashSet会将这两个元素保存在不同的位置。如果两个元素的hashCode()相等,但是equals()不相等,hashSet会用链式结构来保存多个元素。如果HashSet有两个以上的元素具有相同的hashCode()值,这会很影响HashSet的性能。 对于HashSet而言,它是基于HashMap实现的,HashSet底层使用HashMap来保存所有元素,因此HashSet 的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap的相关方法来完成。

    LinkedHashSet

      由上图可知linkedHashSet是hashSet的一个子类,和hashSet类似,也是通过hashCode()来确定元素的位置,唯一一点不同的是linkedHashSet使用链表来维护了元素的顺序,使元素插入linkedHashSet集合的顺序有保证,linkedHashSet的性能略低于HashSet,但是迭代访问它时会有很好的性能,因为它是有序的,底层用LinkedHashMap实现。

    TreeSet

      TreeSet是Set接口下的SortedSet的子类,TreeSet可以保证集合中的元素处于排序状态。和linkedHashSet相比较,treeSet采用红黑树的数据结构来存储集合元素。treeSet支持两种排序方式:自然排序和定制排序。默认为自然排序。(TreeSet不能添加null元素)

      自然排序:java提供一个接口comparable,该接口定义一个方法compareTo(Object object);该方法返回一个整数值。当想要把某个对象加入treeSet集合中时,这个对象必须需要实现comparable接口,treeSet会根据加入的对象和集合中对象做conpareTo()比较,如果等于0,则相等(不会做任何添加动作),如果小于0,则往前排,如大于0,则往后排,以此类推。

      定制排序:定制排序需要在创建TreeSet时,提供一个Comparator对象和TreeSet关联,由Comparator完成集合内部的排序逻辑。如:

Set<String> set = new TreeSet<>((o1,o2)->{
        String a = (String)o1;
        String b = (String)o2;
      return a.length()>b.length()?-1:a.length()<b.length()?-1:0;
});

     EnumSet

      EnumSet是一个专门为枚举设计的类,EnumSet中的元素必须是枚举类型,集合中的元素也是有序的,EnumSet以枚举值在Enum类内的定义顺序来决定集合元素的顺序。EnumSet内部是以位向量的形式存储的,这种存储形式非常的高效,紧凑,因为EnumSet的运行速度比较快,占用内存很小。

     Set的比较和分析

       HashSet和TreeSet时Set接口中两个典型的实现,总体上HashSet的性能要优于TreeSet(特别是添加和查询的操作),TreeSet额外的红黑树算法维护集合元素的次序。只有当需要使用到保证set次序时用TreeSet,否则都应该使用HashSet。HashSet的子类LinkedHashSet对于普通的插入删除的操作比HashSet慢一点,这是需要额外维护链表造成的,但是有了链表,linkedHashSet的遍历更快。EnumSet的性能时所有Set中最好的,但是EnumSet只能保存同一个枚举类的集合元素。(HashSet,TreeSet,EnumSet都不是线程安全的,多线程的时候谨慎)

List

      ArrayList

        ArrayList是我们工作中使用的比较多的一个集合,是一个可储存有序可重复元素的集合,一般用add方法将对象存进集合中,内部是用数组实现,当我们new ArrayList()时,实际上是new Object[10]来存储我们add的对象。一般面试中常问的Arraylist和Vector的区别:其实Vector是JKD1.2之前的集合,一些方法和之后的ArraysList都有重复,ArrayList是线程不安全的,Vector是线程安全的,线程不安全性能当然要高些。还有就是Vector下提供一个Stack子类,相当于栈的功能(后进先出)。

      Queue

         queue是相当于队列数据结构的集合,通常是先进先出操作顺序,一般使用Queue接口下面的PriorityQueue实现类。除此之外Queue还有一个接口Deque(双端队列),可以在对头和队尾双端进行操作,增加了很多灵活性。

       LinkedList

           LinkedList类是List接口下的一个实现类,同时也实现了Deque接口,这意味着LinkedList既可以通过索引来访问集合中的对象,又可以通过offer(),push(),peekFirst(),peekLast()等API实现压栈弹栈等功能,LinkedList和ArrayList  ArrayDeque的实现方式不同,ArrayList  ArrayDeque都是通过数组的形式来保存数据的,因此他们随机访问集合数据时性能很好;而LinkList是以链表的形式来保存元素,所以查找功能差一些,但是增删改的性能很好。

       List性能比较和分析

           List是一个线性表接口,ArrayList和LinkedList分别是数组的线性表和链的线性表。而Queue代表着队列,PriorityQueue是单端队列,而Deque是双端队列。一般来说,由于数组以一块连续内存区来保存所有数组元素,所以数组的随机访问性能最好,而内部以链表实现的集合对于增删的性能较好。但总体来说ArrayList的性能比LinkedList要好,这也是我们在工作中使用比较多的原因。如果有多个线程访问List集合元素,可以使用Collections将集合包装成线程安全集合。

Map

          Map用于保存具有映射关系的数据,一个key对应一个value,key不能重复(注意:实现类中有的key可以为null,比如hashmap,有的不能为null 如hashtable。)。其中可以将Map中所有的key看作一个set集合,value看作一个List集合,只不过key和value之间必须一一对应。

       HashMap

           在Jdk7的时候hashMap使用数组实现的,jdk8之后,改用了红黑树实现。hashMap也是我们工作中使用比较多的,他不能保证key-value的顺序,类似于hashSet,判断两个key相等的标准也是:两个key通过equals()方法比较返回true,两个key的hashcode值也想等。如果使用可变对象作为hashMap的key,并且修改了作为key的可变对象,也会出现程序再也无法精准访问map中被修改过的key。
源码很重要(原文:https://blog.csdn.net/goosson/article/details/81029729 ):
put方法:

//对外开发使用
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}
//存值的真正执行者
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    
    //定义一个数组,一个链表,n永远存放数组长度,i用于存放key的hash计算后的值,即key在数组中的索引        
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    
    //判断table是否为空或数组长度为0,如果为空则通过resize()实例化一个数组并让tab作为其引用,并且让n等于实例化tab后的长度        
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    
    //根据key经过hash()方法得到的hash值与数组最大索引做与运算得到当前key所在的索引值,并且将当前索引上的Node赋予给p并判断是否该Node是否存在
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);//若tab[i]不存在,则直接将key-value插入该位置上。
    
        //该位置存在数据的情况  
    else {
        Node<K,V> e; K k; //重新定义一个Node,和一个k
        
        // 该位置上数据Key计算后的hash等于要存放的Key计算后的hash并且该位置上的Key等于要存放的Key     
        if (p.hash == hash &&((k = p.key) == key || (key != null && key.equals(k))))
            e = p;    //true,将该位置的Node赋予给e
    else if (p instanceof TreeNode)  //判断当前桶类型是否是TreeNode
        //ture,进行红黑树插值法,写入数据
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);     
        else {    
        //false, 遍历当前位置链表
            for (int binCount = 0; ; ++binCount) {
                //查找当前位置链表上的表尾,表尾的next节点必然为null,找到表尾将数据赋给下一个节点
                if ((e = p.next) == null) {
                     p.next = newNode(hash, key, value, null);    //是,直接将数据写到下个节点
                    // 如果此时已经到第八个了,还没找个表尾,那么从第八个开始就要进行红黑树操作
            if (binCount >= TREEIFY_THRESHOLD - 1)
                        treeifyBin(tab, hash);    //红黑树插值具体操作
                        break;
                }
                //如果当前位置的key与要存放的key的相同,直接跳出,不做任何操作   
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //将下一个给到p进行逐个查找节点为空的Node
        p = e;
            }
        }
        //如果e不为空,即找到了一个去存储Key-value的Node 
    if (e != null) { // existing mapping for key
            V oldValue = e.value;    
        if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //当最后一次调整之后Size大于了临界值,需要调整数组的容量
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

       get()方法:

//对外公开方法
public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }
 
//实际逻辑控制方法
final Node<K,V> getNode(int hash, Object key) {
    //定义相关变量
    Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
    //保证Map中的数组不为空,并且存储的有值,并且查找的key对应的索引位置上有值
    if ((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
        // always check first node 第一次就找到了对应的值
    if (first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k))))
            return first;
    //判断下一个节点是否存在
    if ((e = first.next) != null) {
            //true,检测是否是TreeNode
            if (first instanceof TreeNode)
                return ((TreeNode<K,V>)first).getTreeNode(hash, key); //通过TreeNode的get方法获取值
            //否,遍历链表
        do {
        //判断下一个节点是否是要查找的对象
                if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            }while ((e = e.next) != null);
        }
    }//未找到,返回null
    return null;
}

resize()扩容方法:

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;    //未扩容时数组的容量
    int oldThr = threshold;
    int newCap, newThr = 0;//定义新的容量和临界值
    //当前Map容量大于零,非第一次put值
    if (oldCap > 0) {
        if (oldCap >= MAXIMUM_CAPACITY) {    //超过最大容量:2^30
            //临界值等于Integer类型的最大值 0x7fffffff=2^31-1
            threshold = Integer.MAX_VALUE;    
            return oldTab;
        }
        //当前容量在默认值和最大值的一半之间
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1;    //新临界值为当前临界值的两倍
    }
    //当前容量为0,但是当前临界值不为0,让新的容量等于当前临界值
    else if (oldThr > 0) 
        newCap = oldThr;
    //当前容量和临界值都为0,让新的容量为默认值,临界值=初始容量*默认加载因子
    else {
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //如果新的临界值为0
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE);
    }
    //临界值赋值
    threshold = newThr;
    //扩容table
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    table = newTab;
    if (oldTab != null) {
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;//此时newCap = oldCap*2
                else if (e instanceof TreeNode) //节点为红黑树,进行切割操作
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                else { //链表的下一个节点还有值,但节点位置又没有超过8
                    //lo就是扩容后仍然在原地的元素链表
                    //hi就是扩容后下标为  原位置+原容量  的元素链表,从而不需要重新计算hash。
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    //循环链表直到链表末再无节点
                    do {
                        next = e.next;
                        //e.hash&oldCap == 0 判断元素位置是否还在原位置
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    //循环链表结束,通过判断loTail是否为空来拷贝整个链表到扩容后table
                    if (loTail != null) {
                       loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;

}


       Hashtable

           hashtable总是被拿来和hashMap作比较,hashtable是很早就出现的类,他和hashMap最主要的区别就是他是线程安全的,并且不允许把null作为他的key和value,一般情况下如果多线程下要保证线程安全,可以用hashtable,否则都用hashMap。与hashMap一般,不要使用可变对象作为hashMap,hashtable的key,尽量不要在程序中修改作为key的可变对象。

        LinkedHashMap

            与linkedHashSet类是,HashMap也有一个LinkedHashMap子类,LinkedHashMap也是使用双向链表来维护key-value元素的次序的,只需要考虑key的次序,该链表负责维护Map的迭代顺序,迭代顺序与插入时的顺序保持一致。LinkedHashMap需要维护元素的插入顺序,因此性能略低于HashMap的性能;但是他是与链表来维护内部顺序,所以迭代访问map里面元素时有较好的性能。

        TreeMap

            与TreeSet一样,Set接口派生出SortedSet子接口,SortedSet有一个TreeSet实现类一样,Map接口也派生出一个SortedMap子接口,SortedMap也有一个TreeMap实现类。TreeMap就是一个红黑树数据结构,每个key-value对即作为红黑树的一个节点,TreeMap可以保证所有的key-value对处于有序状态。TreeMap也有两种排序方式:

           自然排序:TreeMap的所有key必须实现Comparable接口,而且所有的kye应该是同一个类的对象,否则会报错。

           定制排序:创建TreeMap时,传入一个Comparator对象对有个的key进行比较排序,采用定制排序时不要求key必须实现Comparable接口。

        WeakHashMap

           WeakHashMap与HashMap的用法基本相似,与HashMap不同的是hashMap的key保留了对实际对象的强引用,这意味着只要该HashMap对象不被销毁,该HashMap的key所引用的对象就不会被回收,hashMap也不会删除这些key所对应的key-value对;但WeakHashMap的key只保留了对实际对象的弱引用,这就意味着如果WeakHashMap对象的key所引用对象没有被其他强引用变量所引用,则这些key所引用的对象可能会被垃圾回收,WeakHashMap也可能删除这些key所对应的key-value对。

        IdentityHashMap

            IdentityHashmap和HashMap基本相似,但它在处理两个key相等时比较独特:在IdenityHashMap中,当且仅当两个key严格相等时(key1==key2)时,IdentityHashMap才认为两个key相等;但是HashMap只要key1和key2通过equals()方法比较返回true,且他们的hashcode值相等即可。

        EnumMap

            EnumMap是一个枚举Map,他的所有key必须是单个的枚举类的枚举值,创建EnumMap时必须显式或者隐式指定它对应的枚举类。特性如下:

        1.EnumMap在内部以数组形式保存,所以个他非常高效,紧凑。

        2.EnumMap根据key的自然顺序来维护key-value对的顺序。即枚举值在枚举类中定义的顺序。

        3.EnumMap不允许null作为key值,但是可以作为value值。

       Map性能分析 

          一般来说,程序应该多考虑使用HashMap,因为HashMap正是为了快讯查询设计的,即使对于线程来说,也没有必要使用HashTable,可以使用Collections 工具类把HashMap变成线程安全的。TreeMap通常比HashMap,Hashtable要慢,特别是插入,删除时更慢,但是TreeMap中key-value总处于有序状态,无需专门的排序操作,需要对key-value中的key进行排序时,可以使用TreeMap,LinkedHashMap性能比HashMap要慢一些,因为他需要维护链表来保持map中key-value时的添加顺序。IdentityHashMap也不常用,唯一和HashMap不同之处是采用==而不是equals()方法判断元素相等。EnumMap的性能最好,但它只能使用同一个枚举类的枚举作为key。

Hash的解读

         对于hashSet,hashMap,Hashtbale而言,他们采用hash算法决定几何中元素的存储位置,并通过hash算法来控制集合的大小。

        hash表里面可以存储元素的位置被称为“桶”,通常情况下,单个“桶”里面存储一个元素,此时有最好的性能:hash算法可以根据hashCode计算出“桶”的存储位置,并且取出数据。但是当发生hash冲突时,单个桶会有多个元素,这些元素以链表形式存储,想得到数据必须按hash出桶位置,然后顺序搜索才能找到数据。如图:                                                                       

                                                                          

         HashSet,HashMap,Hashtable都是使用hash算法来决定元素的存储,因为都包含如下元素:

         容量(capacity):hash表中桶的数量。

         初始化容量(initial capacity):创建hash表时桶的数量。hashMap和hashSet都允许在构造器中制定初始化容量。

         尺寸(size):当前hash表中记录的数量。

         负载因子(load factor):负载因子等于“siza/capacity”。负载因为为0,表示空的hash表,0.5表示半满的hash表。HashSet,HashMap,Hashtable的默认负载因子是0.75。

        (本文全手打,请忽略因为引文大小写。谢谢)

       

 

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!