java知识系列之集合理论篇

本文主要综合介绍java集合中比较重要的一些概念和实现。如下图
在这里插入图片描述

基础部分

Collection

List

:接口实例存储的是有序的可以重复的元素。

  • ArrayList
    - 底层使用数组
    - 读取速度快,增删速度慢
    - 不是线程安全的,只能在单线程环境下,多线程环境下可以考虑用collections.synchronizedList(List l)函数返回一个线程安全的ArrayList类,也可以使用concurrent并发CopyOnWriteArrayList类。
    - 当容量不够时,当前容量*1.5+1
  • LinkedList
    • 底层使用双向链表数据结构
    • 读取速度慢,增删快
    • 线程不安全
    • implements List, Deque。实现List接口,能对它进行队列操作,即可以根据索引来随机访问集合中的元素。同时它还实现Deque接口,即能将LinkedList当作双端队列使用。自然也可以被当作"栈来使用"
  • Vector
    • 底层使用数组
    • 读取速度快,增删慢
    • 线程安全,效率低
    • 容量不够时,默认扩展一倍
    • Stack
      Stack是Vector提供的一个子类,用于模拟"栈"这种数据结构(LIFO后进先出

小结

  1. java提供的List就是一个"线性表接口",ArrayList(基于数组的线性表)、LinkedList(基于链的线性表)是线性表的两种典型实现
  2. Queue代表了队列,Deque代表了双端队列(既可以作为队列使用、也可以作为栈使用)
  3. 因为数组以一块连续内存来保存所有的数组元素,所以数组在随机访问时性能最好。所以的内部以数组作为底层实现的集合在随机访问时性能最好。
  4. 内部以链表作为底层实现的集合在执行插入、删除操作时有很好的性能
  5. 进行迭代操作时,以链表作为底层实现的集合比以数组作为底层实现的集合性能好

Set

:接口实例存储的是无序的,不重复的数据

  • HashSet:
    - 底层使用hash表实现
    - 存取速度快
    - 内部使用HashMap实现
    - 值得注意的是,HashSet集合判断两个元素相等的标准是两个对象通过equals()方法比较相等,并且两个对象的hashCode()方法的返回值相等
  • TreeSet: (父类为SortedSet,此接口主要用于排序操作,即实现此接口的子类都属于排序的子类)
    - 底层使用二叉树实现
    - 排序存储
    - 内部使用TreeMap的SortedSet实现
  • LinkedHashSet
    - 采用hash表存储,并用双向链表记录插入顺序
    - 内部使用LinkedHashMap实现
    - LinkedHashSet集合也是根据元素的hashCode值来决定元素的存储位置,但和HashSet不同的是,它同时使用链表维护元素的次序,这样使得元素看起来是以插入的顺序保存的。当遍历LinkedHashSet集合里的元素时,LinkedHashSet将会按元素的添加顺序来访问集合里的元素。 LinkedHashSet需要维护元素的插入顺序,因此性能略低于HashSet的性能,但在迭代访问Set里的全部元素时(遍历)将有很好的性能(链表很适合进行遍历)

Set判断两个对象相同不是使用"=="运算符,而是根据equals方法。

也就是说,我们在加入一个新元素的时候,如果这个新元素对象和Set中已有对象进行注意equals比较都返回false,则Set就会接受这个新元素对象,否则拒绝。

因为Set的这个制约,在使用Set集合的时候,应该注意两点:
- 为Set集合里的元素的实现类实现一个有效的equals(Object)方法、
- 对Set的构造函数,传入的Collection参数不能包含重复的元素

小结:

  1. HashSet的性能总是比TreeSet好(特别是最常用的添加、查询元素等操作),因为TreeSet需要额外的红黑树算法来维护集合元素的次序。只有当需要一个保持排序的Set时,才应该使用TreeSet,否则都应该使用HashSet
  2. 对于普通的插入、删除操作,LinkedHashSet比HashSet要略慢一点,这是由维护链表所带来的开销造成的。不过,因为有了链表的存在,遍历LinkedHashSet会更快
  3. EnumSet是所有Set实现类中性能最好的,但它只能保存同一个枚举类的枚举值作为集合元素
  4. HashSet、TreeSet、EnumSet都是"线程不安全"的,通常可以通过Collections工具类的synchronizedSortedSet方法来"包装"该Set集合。
    SortedSet s = Collections.synchronizedSortedSet(new TreeSet(…));

Queue

Queue用于模拟"队列"这种数据结构(先进先出 FIFO)。队列的头部保存着队列中存放时间最长的元素,队列的尾部保存着队列中存放时间最短的元素。新元素插入(offer)到队列的尾部,访问元素(poll)操作会返回队列头部的元素,队列不允许随机访问队列中的元素。结合生活中常见的排队就会很好理解这个概念。在两端出入的list,所以也可以使用数组或则链表实现

  • BlockingQueue
  • Deque:Deque接口代表一个"双端队列",双端队列可以同时从两端来添加、删除元素,因此Deque的实现类既可以当成队列使用、也可以当成栈使用
    • ArrayDeque: 是一个基于数组的双端队列,和ArrayList类似,它们的底层都采用一个动态的、可重分配的Object[]数组来存储集合元素,当集合元素超出该数组的容量时,系统会在底层重-==、-= -=分配一个Object[]数组来存储集合元素
    • LinkedList:
  • PriorityQueue
    PriorityQueue并不是一个比较标准的队列实现,PriorityQueue保存队列元素的顺序并不是按照加入队列的顺序,而是按照队列元素的大小进行重新排序,这点从它的类名也可以看出来

Map

Map用于保存具有"映射关系"的数据,因此Map集合里保存着两组值,一组值用于保存Map里的key,另外一组值用于保存Map里的value。key和value都可以是任何引用类型的数据。Map的key不允
许重复,即同一个Map对象的任何两个key通过equals方法比较结果总是返回false。
关于Map,我们要从代码复用的角度去理解,java是先实现了Map,然后通过包装了一个所有value都为null的Map就实现了Set集合
Map的这些实现类和子接口中key集的存储形式和Set集合完全相同(即key不能重复)
Map的这些实现类和子接口中value集的存储形式和List非常类似(即value可以重复、根据索引来查找)

  • HashMap
    - 键值不可重复,值可以重复
    - 底层使用hash表
    - 线程不安全
    - 允许一个且仅一个key为null,值可以多个null
  • TreeMap
    - 键不可以重复,值可以重复
    - 底层红黑树,一种近似平衡二叉树
    - 线程不安全
  • LinkedHashMap
    • 使用双向链表来维护key-value对的次序
  • WeakHashMap
    • WeakHashMap与HashMap的用法基本相似。区别在于,HashMap的key保留了对实际对象的"强引用",这意味着只要该HashMap对象不被销毁,该HashMap所引用的对象就不会被垃圾回收。
        但WeakHashMap的key只保留了对实际对象的弱引用,这意味着如果WeakHashMap对象的key所引用的对象没有被其他强引用变量所引用,则这些key所引用的对象可能被垃圾回收,当垃
        圾回收了该key所对应的实际对象之后,WeakHashMap也可能自动删除这些key所对应的key-value对
  • ConcurrentHashMap

小结

  1. HashMap和Hashtable的效率大致相同,因为它们的实现机制几乎完全一样。但HashMap通常比Hashtable要快一点,因为Hashtable需要额外的线程同步控制
  2. TreeMap通常比HashMap、Hashtable要慢(尤其是在插入、删除key-value对时更慢),因为TreeMap底层采用红黑树来管理key-value对
  3. 使用TreeMap的一个好处就是: TreeMap中的key-value对总是处于有序状态,无须专门进行排序操作

Dictionary

  • HashTable
    - 键值不可重复,值可以重复
    - 底层hash表
    - 线程安全,简单粗暴的synchronized使用
    - key value都不可以null

小结:

  1. Collection
    一组"对立"的元素,通常这些元素都服从某种规则
      1.1) List必须保持元素特定的顺序
      1.2) Set不能有重复元素
      1.3) Queue保持一个队列(先进先出)的顺序
  2. Map
    一组成对的"键值对"对象

使用场景

  1. 排序 SortedSet/TreeSet/PriorityQueue/
    • 自然排序
    • 定制排序
  2. 数组:array
  3. 链表:
  4. 栈:LinkedList,Stack,Deque
  5. 队列:Deque

二叉树:

  1. 一个节点下不能多余两个节点。
  2. 二叉树的存储过程:
    如果是第一个元素,那么直接存入,作为根节点,下一个元素进来是会跟节点比较,如果大于节点放右边的,小于节点放左边;等于节点就不存储。后面的元素进来会依次比较,直到有位置存储为止

ArrayList、LinkedList、Vector的底层实现和区别

  1. 从同步性来看,ArrayList和LinkedList是不同步的,而Vector是的。所以线程安全的话,可以使用ArrayList或LinkedList,可以节省为同步而耗费的开销。但在多线程下,有时候就不得不使用Vector了。当然,也可以通过一些办法包装ArrayList、LinkedList,使我们也达到同步,但效率可能会有所降低。
  2. 从内部实现机制来讲,ArrayList和Vector都是使用Object的数组形式来存储的。当你向这两种类型中增加元素的时候,如果元素的数目超出了内部数组目前的长度它们都需要扩展内部数组的长度,Vector缺省情况下自动增长原来一倍的数组长度,ArrayList是原来的50%,所以最后你获得的这个集合所占的空间总是比你实际需要的要大。如果你要在集合中保存大量的数据,那么使用Vector有一些优势,因为你可以通过设置集合的初始化大小来避免不必要的资源开销。
  3. ArrayList和Vector中,从指定的位置(用index)检索一个对象,或在集合的末尾插入、删除一个对象的时间是一样的,可表示为O(1)。但是,如果在集合的其他位置增加或者删除元素那么花费的时间会呈线性增长O(n-i),其中n代表集合中元素的个数,i代表元素增加或移除元素的索引位置,因为在进行上述操作的时候集合中第i和第i个元素之后的所有元素都要执行(n-i)个对象的位移操作。LinkedList底层是由双向循环链表实现的,LinkedList在插入、删除集合中任何位置的元素所花费的时间都是一样的O(1),但它在索引一个元素的时候比较慢,为O(i),其中i是索引的位置,如果只是查找特定位置的元素或只在集合的末端增加、移除元素,那么使用Vector或ArrayList都可以。如果是对其它指定位置的插入、删除操作,最好选择LinkedList。

HashMap和HashTable的底层实现和区别,两者和ConcurrentHashMap的区别。

  1. HashTable线程安全则是依靠方法简单粗暴的sychronized修饰,HashMap则没有相关的线程安全问题考虑

  2. Hashtable不允许key或者value使用null值,而HashMap可以。

  3. 在内部算法上,它们对key的hash算法和hash值到内存索引的映射算法不同。

  4. 在以前的版本貌似ConcurrentHashMap引入了一个“分段锁”的概念,具体可以理解为把一个大的Map拆分成N个小的HashTable,根据key.hashCode()来决定把key放到哪个HashTable中。在ConcurrentHashMap中,就是把Map分成了N个Segment,put和get的时候,都是现根据key.hashCode()算出放到哪个Segment中。

通过把整个Map分为N个Segment(类似HashTable),可以提供相同的线程安全,但是效率提升N倍。

哈希表如何保证元素唯一呢 ? 底层是依赖 hashCode 和 equals 方法.

HashMap的hashcode的作用?什么时候需要重写?如何解决哈希冲突?查找的时候流程是如何?

HashMap的实现原理
1. 简单说,HashMap就是将key做hash算法,然后将hash所对应的数据映射到内存地址,直接取得key所对应的数据。在HashMap中。底层数据结构使用的是数组,所谓的内存地址即数组的下标索引。HashMap的高性能需要保证以下几点:

    - hash算法必须高效
    - hash值到内存地址(数组索引)的算法是快速的
    - 根据内存地址(数组索引)可以直接取得对应的值
如何保证hash算法高效,hash算法有关的代码如下:
```
   int hash = hash(key.hashCode());
    public native int hashCode();
    static int hash(int h){
        h ^= (h >>> 20) ^ (h >>> 12);
        return h ^ (h >>> 7) ^ (h >>> 4);
    } 
```
第一行代码是HashMap用于计算key的hash值,它前后调用了Object类的hashCode()方法和HashMap的内部函数hash()。Object类的hashCode()方法默认是native的实现,可以认为不存在性能问题。而hash()函数的实现全部基于位运算,因此,也是高效的。

当取得key的hash值后,需要通过hash值得到内存地址:
```
    int i = indexFor(hash, table.length);
    static int indexFor(int h, int length){
        return h & (length - 1);
    }
```
indexFor()函数通过将hash值和数组长度按位与直接得到数组索引。 
最后由indexFor()函数返回的数组索引直接通过数组下标便可取得对应的值,直接的内存访问速度也是相当的快,因此,可认为HashMap是高性能的。
Hash冲突

需要存放到HashMap中的两个元素1和2,通过hash计算后,发现对应在内存中的同一个地址,如何处理?

其实HashMap的底层实现使用的是数组,但是数组内的元素并不是简单的值。而是一个Entry类的对象。

HashMap的内部维护着一个Entry数组,每一个Entry表项包括key、value、next和hash几项。next部分指向另外一个Entry。进一步阅读HashMap的put()方法源码,可以看到当put()操作有冲突时,新的Entry依然会被安放在对应的索引下标内,并替换原有的值。同时为了保证旧值不丢失,会将新的Entry的next指向旧值。这便实现了在一个数组索引空间内存放多个值项。因此,如图3.12所示,HashMap实际上是一个链表的数组。

    public V put(K key, V value){
        if(key == null)
            return putForNullKey(value);
        int hash = hash(key.hashCode());
        int i = indexFor(hash, table.length);
        for(Entry<K, V> e = table[i]; e != null; e = e.next){
            Object k;
            //如果当前的key已经存在于HashMap中
            if(e.hash == hash && ((k = e.key) == key || key.equals(k)))
            {
                V oldValue = e.value;    //取得旧值
                e.value = value;
                e.recordAccess(this);
                return oldValue;     //返回旧值
            }
        }
        modCount++;
        addEntry(hash, key, value, i);    //添加当前的表项到i位置
        return null;
    }

    void addEntry(int hash, K key, V value, int bucketIndex){
        Entry<K,V> e = table[bucketIndex];
        //将新增元素放到i的位置,并让它的next指向旧的元素
        table[bucketIndex] = new Entry<K,V>(hash, key, value, e);
        if(size++ >= threshold){
            resize(2 * table.length);
        }
    }

基于HashMap的这种实现机制,只要hashCode和hash()方法实现的足够好,能够尽可能的减少冲突的产生,那么对HashMap的操作几乎等价于对数组的随机访问操作,具有很好的性能。但是,如果hashCode()或者hash()方法实现较差,在大量冲突产生的情况下,HashMap事实上就退化为几个链表,对HashMap的操作等价于遍历链表,此时性能很差。

容量参数

除hashCode()的实现外,影响HashMap性能的还有它的容量参数。和ArrayList和Vector一样,这种基于数组的结构,不可避免的需要在数组空间不足时,进行扩展。而数组的重组相对而言较为耗时,因此对其作一定了解有助于优化HashMap的性能。

其中initialCapacity指定了HashMap的初始容量,loadFactor指定了其负载因子。初始容量即数组的大小,HashMap会使用大于等于initialCapacity并且是2的指数次幂的最小的整数作为内置数组的大小。负载因子又叫填充比,它是介于0和1之间的浮点数,它决定了HashMap在扩容之前,其内部数组的填充度。默认情况下,HashMap初始大小为16,负载因子为0.75。

负载因子 = 元素个数/内部数组总大小

在实际使用中,负载因子也可以设置为大于1的数,但如果这样做,HashMap将必然产生大量冲突,因为这无疑是在尝试往只有10个口袋的包里放15件物品,必然有几只口袋要大于一个物件。因此,通常不会这么使用。

在HashMap内部,还维护了一个threshold变量,它始终被定义为当前数组总容量和负载因子的乘积,它表示HashMap的阈值。当HashMap的实际容量超过阈值时,HashMap便会进行扩容。因此,HashMap的实际容量超过阈值时,HashMap便会进行扩容。因此,HashMap的实际填充率不会超过负载因子。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值