Java容器的常见问题

记录Java容器中的常见概念和原理

参考:

基础容器

ArrayList(动态数组)、LinkedList(带头结点的双向链表)

ArrayList

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable
  • 默认初始容量为 10;
  • 扩容机制:添加元素前,先检查是否需要扩容,一般扩为源数组的 1.5 倍 + 1;
  • 边界检查(即检查 ArrayList 的 Size):涉及到 index 的操作;
  • 调整数组容量(减少容量):将底层数组的容量调整为当前列表保存的实际元素的大小;
  • 在查找给定元素索引值等的方法中,源码都将该元素的值分为null和不为null两种情况处理;

    LinkedList

    LinkedList 不但实现了List接口,还实现了Dequeue接口。因此,LinkedList不但可以当做List来用,还可以当做Stack(push、pop、peek),Queue(offer、poll、peek)来使用。

public class LinkedList<E>
    extends AbstractSequentialList<E>
    implements List<E>, Deque<E>, Cloneable, java.io.Serializable

private static class Entry<E> {
    E element;
    Entry<E> next;
    Entry<E> previous;

    Entry(E element, Entry<E> next, Entry<E> previous) {
        this.element = element;
        this.next = next;
        this.previous = previous;
   }
}

ArrayList与LinkedList比较

  • ArrayList是基于数组的实现,LinkedList是基于带头结点的双向循环链表的实现;
  • ArrayList支持随机访问,LinkedList不支持;
  • LinkedList可作队列和栈使用,实现了Dequeue接口,而ArrayList没有;
  • ArrayList 寻址效率较高,插入/删除效率较低;LinkedList插入/删除效率较高,寻址效率较低

Fast-fail机制

Fail-Fast 是 Java 集合的一种错误检测机制,为了防止在某个线程在对Collection进行迭代时,其他线程对该Collection进行结构上的修改。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险,抛出 ConcurrentModificationException 异常。

Set (HashSet,LinkedHashSet,TreeSet)

Set不包含重复的元素,这是Set最大的特点,也是使用Set最主要的原因。常用到的Set实现有 HashSet,LinkedHashSet 和 TreeSet。一般地,如果需要一个访问快速的Set,你应该使用HashSet;当你需要一个排序的Set,你应该使用TreeSet;当你需要记录下插入时的顺序时,你应该使用LinedHashSet。

  • HashSet:委托给HashMap进行实现,实现了Set接口
    HashSet是采用hash表来实现的,其中的元素没有按顺序排列,add()、remove()以及contains()等方法都是复杂度为O(1)的方法。
public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable {
    static final long serialVersionUID = -5024744406713321676L;

    // 底层支持,HashMap可以代表一个HashMap,也可以代表一个LinkedHashMap
    private transient HashMap<E,Object> map;     

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();    // 傀儡对象
  • LinkedHashSet:是HashSet的子类,被委托给HashMap的子类LinkedHashMap进行实现,实现了Set接口
    LinkedHashSet继承于HashSet,利用下面的HashSet构造函数即可,注意到,其为包访问权限,专门供LinkedHashSet的构造函数调用。LinkedHashSet性能介于HashSet和TreeSet之间,是HashSet的子类,也是一个hash表,但是同时维护了一个双链表来记录插入的顺序,基本方法的复杂度为O(1)。
  • TreeSet:委托给TreeMap(TreeMap实现了NavigableSet接口)进行实现,实现了NavigableSet接口(扩展的 SortedSet)
    TreeSet是采用树结构实现(红黑树算法),元素是按顺序进行排列,但是add()、remove()以及contains()等方法都是复杂度为O(log (n))的方法,它还提供了一些方法来处理排序的set,如first()、 last()、 headSet()和 tailSet()等。此外,TreeSet不同于HashSet和LinkedHashSet,其所存储的元素必须是可排序的(元素实现Comparable接口或者传入Comparator),并且不能存放null值。
public class TreeSet<E> extends AbstractSet<E>
    implements NavigableSet<E>, Cloneable, java.io.Serializable
{
    /**
     * The backing map.
     */
    private transient NavigableMap<E,Object> m;   // 底层支持

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();

    // 默认构造函数
    public TreeSet() {
        this(new TreeMap<E,Object>());
    }

Map(HashMap, LinkedHashMap, TreeMap)

HashMap

  • 对NULL键的特别处理

    • HashMap 中可以保存键为NULL的键值对,且该键值对是唯一的。若再次向其中添加键为NULL的键值对,将覆盖其原值。此外,如果HashMap中存在键为NULL的键值对,那么一定在第一个桶中。
  • HashMap 中的哈希策略
    • 使用 hash() 方法用于对Key的hashCode进行重新计算,以防止质量低下的hashCode()函数实现。由于hashMap的支撑数组长度总是 2 的倍数,通过右移可以使低位的数据尽量的不同,从而使Key的hash值的分布尽量均匀;
    • 使用 indexFor() 方法进行取余运算,以使Entry对象的插入位置尽量分布均匀
  • HashMap 的底层数组长度为何总是2的n次方?
    • 构造函数及扩容策略
    // HashMap 的容量必须是2的幂次方,超过 initialCapacity 的最小 2^n
    int capacity = 1;
    while (capacity < initialCapacity)
      capacity <<= 1;

    *当底层数组的length为2的n次方时,h&(length - 1) 就相当于对length取模,而且速度比直接取模快得多,这是HashMap在速度上的一个优化

  • 初始容量16,两倍扩容,先添加再检查

    LinkedHashMap:HashMap + 带头结点的双向循环链表


TreeMap:红黑树的实现

  • 排序二叉树 Vs. 红黑树
    排序二叉树虽然可以快速检索,但在最坏的情况下:如果插入的节点集本身就是有序的,要么是由小到大排列,要么是由大到小排列,那么最后得到的排序二叉树将变成链表:所有节点只有左节点(如果插入节点集本身是大到小排列);或所有节点只有右节点(如果插入节点集本身是小到大排列)。在这种情况下,排序二叉树就变成了普通链表,其检索效率就会很差。
  • 红黑树
    为了改变排序二叉树存在的不足,Rudolf Bayer 与 1972 年发明了另一种改进后的排序二叉树:红黑树,他将这种排序二叉树称为“对称二叉 B 树”,而红黑树这个名字则由 Leo J. Guibas 和 Robert Sedgewick 于 1978 年首次提出。
    红黑树是一个更高效的检索二叉树,因此常常用来实现关联数组。典型地,JDK 提供的集合类 TreeMap 本身就是一个红黑树的实现。红黑树在原有的排序二叉树增加了如下几个要求:

    • 性质 1:每个节点要么是红色,要么是黑色;
    • 性质 2:根节点永远是黑色的;
    • 性质 3:所有的叶节点都是空节点(即 null),并且是黑色的;
    • 性质 4:每个红色节点的两个子节点都是黑色(从每个叶子到根的路径上不会有两个连续的红色节点);
    • 性质 5:从任一节点到其子树中每个叶子节点的路径都包含相同数量的黑色节点。
  • 红黑树的性质
    上面的性质3中指出:红黑树的每个叶子节点都是空节点,而且并叶子节点都是黑色。但 Java 实现的红黑树将使用null来代表空节点,因此遍历红黑树时将看不到黑色的叶子节点,反而看到每个叶子节点都是红色的。根据性质 5,红黑树从根节点到每个叶子节点的路径都包含相同数量的黑色节点,因此从根节点到叶子节点的路径中包含的黑色节点数被称为树的“黑色高度(black-height)”。性质 4 则保证了从根节点到叶子节点的最长路径的长度不会超过任何其他路径的两倍。
    假如有一棵黑色高度为 3 的红黑树:从根节点到叶节点的最短路径长度是 2,该路径上全是黑色节点(黑节点 - 黑节点 - 黑节点)。最长路径也只可能为 4,在每个黑色节点之间插入一个红色节点(黑节点 - 红节点 - 黑节点 - 红节点 - 黑节点),性质 4 保证绝不可能插入更多的红色节点。由此可见,红黑树中最长路径就是一条红黑交替的路径。由此我们可以得出结论:对于给定的黑色高度为 N 的红黑树,从根到叶子节点的最短路径长度为 N-1,最长路径长度为 2 * (N-1)。
  • 红黑树的查找、插入与删除操作
    由于红黑树只是一个特殊的排序二叉树,因此对红黑树上的只读操作与普通排序二叉树上的只读操作完全相同,只是红黑树保持了大致平衡,因此检索性能比排序二叉树要好很多。但在红黑树上进行插入操作和删除操作会导致树不再符合红黑树的特征,因此插入操作和删除操作都需要进行一定的维护,以保证插入节点、删除节点后的树依然是红黑树。
  • 红黑树和平衡二叉树
    我们知道,在AVL树中,任何节点的两个子树的最大高度差为1,所以它也被称为高度平衡树;而红黑树并不追求 完全平衡,它只要求大致地达到平衡要求,降低了对旋转的要求,从而提高了性能。实际上,由于红黑树的设计,任何不平衡都会在三次旋转之内解决。
    红黑树能够以O(log2 n) 的时间复杂度进行搜索、插入、删除操作。此外,由于它的设计,任何不平衡都会在三次旋转之内解决。当然,还有一些更好的,但实现起来更复杂的数据结构,能够做到一步旋转之内达到平衡,但红黑树能够给我们一个比较“便宜”的解决方案。红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
    红黑树并不是真正的平衡二叉树,但在实际应用中,红黑树的算法时间复杂度和AVL相同,但统计性能比AVL树更高。
    排序二叉树的深度直接影响了检索的性能,正如前面指出,当插入节点本身就是由小到大排列时,排序二叉树将变成一个链表,这种排序二叉树的检索性能最低:N 个节点的二叉树深度就是 N-1。
    红黑树通过上面这种限制来保证它大致是平衡的——因为红黑树的高度不会无限增高,这样保证红黑树在最坏情况下都是高效的,不会出现普通排序二叉树的情况。

容器中判断两个对象是否相等的步骤

  • 判断两个对象的hashCode是否相等:如果不相等,认为两个对象也不相等,完毕;如果相等,转入2);
  • 判断两个对象用equals运算是否相等:如果不相等,认为两个对象也不相等;如果相等,认为两个对象相等。

同步容器

主要参考:https://github.com/wangzhiwubigdata/God-Of-BigData#%E4%B8%89Java%E5%B9%B6%E5%8F%91%E5%AE%B9%E5%99%A8

  • List和Set
    JUC(java.util.concurrent)集合包中的List和Set实现类包括: CopyOnWriteArrayList CopyOnWriteArraySet ConcurrentSkipListSet

    • CopyOnWriteArrayList相当于线程安全的ArrayList,它实现了List接口。CopyOnWriteArrayList是支持高并发的。
    • CopyOnWriteArraySet相当于线程安全的HashSet,它继承于AbstractSet类。CopyOnWriteArraySet内部包含一个CopyOnWriteArrayList对象,它是通过CopyOnWriteArrayList实现的。
  • Map
    JUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。

    • ConcurrentHashMap是线程安全的哈希表(相当于线程安全的HashMap);它继承于AbstractMap类,并且实现ConcurrentMap接口。ConcurrentHashMap是通过“锁分段”来实现的,它支持并发。
    • ConcurrentSkipListMap是线程安全的有序的哈希表(相当于线程安全的TreeMap); 它继承于AbstractMap类,并且实现ConcurrentNavigableMap接口。ConcurrentSkipListMap是通过“跳表”来实现的,它支持并发。
    • ConcurrentSkipListSet是线程安全的有序的集合(相当于线程安全的TreeSet);它继承于AbstractSet,并实现了NavigableSet接口。ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,它也支持并发。
      JUC集合包中Map的实现类包括: ConcurrentHashMap和ConcurrentSkipListMap。
  • Queue
    JUC集合包中Queue的实现类包括: ArrayBlockingQueue, LinkedBlockingQueue, LinkedBlockingDeque, ConcurrentLinkedQueue和ConcurrentLinkedDeque。

    • ArrayBlockingQueue是数组实现的线程安全的有界的阻塞队列。
    • LinkedBlockingQueue是单向链表实现的(指定大小)阻塞队列,该队列按 FIFO(先进先出)排序元素。
    • LinkedBlockingDeque是双向链表实现的(指定大小)双向并发阻塞队列,该阻塞队列同时支持FIFO和FILO两种操作方式。
    • ConcurrentLinkedQueue是单向链表实现的无界队列,该队列按 FIFO(先进先出)排序元素。
    • ConcurrentLinkedDeque是双向链表实现的无界队列,该队列同时支持FIFO和FILO两种操作方式。

CopyOnWrite

CopyOnWrite 容器即写时复制的容器,从JDK1.5开始,Java并发包里提供了两个使用CopyOnWrite机制实现的并发容器 —— CopyOnWriteArrayList 和 CopyOnWriteArraySet,它们适用于 读操作远多于写操作 的并发场景中。关于写时复制容器,通俗的理解是,当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。这样做的好处是,我们可以对CopyOnWrite容器进行并发的读,而不需要加锁,因为当前容器不会添加任何元素。所以,CopyOnWrite容器也是一种 读写分离思想,读和写不同的容器。写时复制容器也存在一些缺点,比如:

  • 容器对象的复制需要一定的开销,如果对象占用内存过大,可能造成频繁的YoungGC和Full GC,对内存的消耗太大;
  • CopyOnWriteArrayList不能保证数据严格的实时一致性,只能保证最终一致性。

LinkedBlockingQueue

LinkedBlockingQueue是一个单向链表实现的阻塞队列。该队列按 FIFO(先进先出)排序元素,新元素插入到队列的尾部,并且队列获取操作会获得位于队列头部的元素。链接队列的吞吐量通常要高于基于数组的队列,但是在大多数并发应用程序中,其可预知的性能要低。
此外,LinkedBlockingQueue还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。

  • LinkedBlockingQueue继承于AbstractQueue,它本质上是一个FIFO(先进先出)的队列。
  • LinkedBlockingQueue实现了BlockingQueue接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。
  • LinkedBlockingQueue是通过单链表实现的: (01) head是链表的表头。取出数据时,都是从表头head处插入。 (02) last是链表的表尾。新增数据时,都是从表尾last处插入。(03) count是链表的实际大小,即当前链表中包含的节点个数。(04) capacity是列表的容量,它是在创建链表时指定的。 (05) putLock是插入锁,takeLock是取出锁;notEmpty是“非空条件”,notFull是“未满条件”。通过它们对链表进行并发控制。 LinkedBlockingQueue在实现“多线程对竞争资源的互斥访问”时,对于“插入”和“取出(删除)”操作分别使用了不同的锁。对于插入操作,通过“插入锁putLock”进行同步;对于取出操作,通过“取出锁takeLock”进行同步。 此外,插入锁putLock和“非满条件notFull”相关联,取出锁takeLock和“非空条件notEmpty”相关联。通过notFull和notEmpty更细腻的控制锁。

LinkedBlockingDeque

LinkedBlockingDeque是双向链表实现的双向并发阻塞队列。该阻塞队列同时支持FIFO和FILO两种操作方式,即可以从队列的头和尾同时操作(插入/删除);并且,该阻塞队列是支持线程安全。
此外,LinkedBlockingDeque还是可选容量的(防止过度膨胀),即可以指定队列的容量。如果不指定,默认容量大小等于Integer.MAX_VALUE。

  • LinkedBlockingDeque继承于AbstractQueue,它本质上是一个支持FIFO和FILO的双向的队列。
  • LinkedBlockingDeque实现了BlockingDeque接口,它支持多线程并发。当多线程竞争同一个资源时,某线程获取到该资源之后,其它线程需要阻塞等待。
  • LinkedBlockingDeque是通过双向链表实现的。
    • first是双向链表的表头, last是双向链表的表尾,count是LinkedBlockingDeque的实际大小,即双向链表中当前节点个数,capacity是LinkedBlockingDeque的容量,它是在创建LinkedBlockingDeque时指定的
    • lock是控制对LinkedBlockingDeque的互斥锁,当多个线程竞争同时访问LinkedBlockingDeque时,某线程获取到了互斥锁lock,其它线程则需要阻塞等待,直到该线程释放lock,其它线程才有机会获取lock从而获取cpu执行权。
    • notEmpty和notFull分别是“非空条件”和“未满条件”。通过它们能够更加细腻进行并发控制。

CopyOnWriteArraySet

CopyOnWriteArrayList

ConcurrentSkipListSet

ConcurrentSkipListMap

ConcurrentLinkedQueue

ConcurrentHashMap

ArrayBlockingQueue

原文地址:https://www.cnblogs.com/xlsryj/p/12149669.html

时间: 2024-10-08 19:04:47

Java容器的常见问题的相关文章

Java 容器 & 泛型:一、认识容器

Writer:BYSocket(泥沙砖瓦浆木匠) 微博:BYSocket 豆瓣:BYSocket 容器是Java语言学习中重要的一部分.泥瓦匠我的感觉是刚开始挺难学的,但等你熟悉它,接触多了,也就"顺理成章"地知道了.Java的容器类主要由两个接口派生而出:Collection和Map. 一.Collection vs Collections 首先,Collection 和 Collections 是两个不同的概念.之所以放在一起,是为了更好的比较.Collection是容器层次结构中

Java 容器

在实际问题中我们经常需要处理数据,单纯依靠数组来存储数据对开发来说非常困难,java提供了一套容器来方便我们编程.对java容器有一个整体的了解对我们来说非常重要,这样在需要特定容器时,不会手忙脚乱,本文主要介绍java的一些基本容器,而不要仅知道使用ArrayList.下面的图片是java中的简单容器分类   --图片来自网络 java定义了四种容器类型,List.Set.Queue和Map. 其中List.Set.Queue都实现了Collection接口,下面来看看这4中类型的容器. Co

java容器---集合总结

思考为什么要引入容器这个概念? Java有多种方式保存对象(应该是对象的引用),例如使用数组时保存一组对象中的最有效的方式,如果你想保存一组基本类型的数据,也推荐使用这种方式,但大家知道数组是具有固定尺寸的,你必须事先知道你要需要多少个对象.但是在一般的情况中,你在写程序中并不知道将需要多少个对象,或者是否需要更加复杂的方式来存储对象,因此数组尺寸固定很不爽! 为了解决上述问题,引入了容器的概念.容器提供了完善的方法来保存对象,你可以使用这些工具来解决数量惊人的问题.Java容器类可以自动调整自

Java 容器在实际web项目中应用

前言:在java开发中我们离不开集合数组等,在java中有个专有名词:"容器" ,下面会结合Thinking in Java的知识和实际开发中业务场景讲述一下容器在Web项目中的用法.可结合图片代码了解Java中的容器 备注 :这个地方 ,参考于朝向远方的博客Java容器详解,既然前人总结的这么好,我就直接拿来用,在这里更注重在实际开发中的例子,感谢那些总结的前辈们,辛苦了. 简单的数组例子 Thinking in Java 中并没有把数组归为Java的容器,实际上数组的确不是Java

Java 容器在实际项目中的应用

前言:在java开发中我们离不开集合数组等,在java中有个专有名词:"容器" ,下面会结合Thinking in Java的知识和实际开发中业务场景讲述一下容器在Web项目中的用法.可结合图片代码了解Java中的容器 备注 :这个地方 ,参考于朝向远方的博客Java容器详解,既然前人总结的这么好,我就直接拿来用,在这里更注重在实际开发中的例子,感谢那些总结的前辈们,辛苦了. 简单的数组例子 Thinking in Java 中并没有把数组归为Java的容器,实际上数组的确不是Java

Java面试题-Java容器

一.Java容器分类 Java容器划分为两个概念Collection.Map Collection: 一个独立元素的序列,这些元素都服从一条或多条规则.List必须按照插入的顺序保存元素,不关心是否重复:Set不能有重复元素:Queue一端插入一端输出.所有的Collection都可以用foreach语法遍历 实现:List:ArrayList.LinkedList:   Set:HashSet.TreeSet.LinkedHashSet   Map:HashMap.TreeMap.Linked

java容器集合类

容器就是容纳物品,放置物品的东西,对Java来说,一切皆是对象,他的容器就是能保存java的对象的类.由于数据容器中存放了我们随时可能需要使用到的对象引用,所以一般的数据容器要都要能能提供方便的查询.遍历.修改等基本接口功能. 早期的OOP语言都通过数组的方式来实现对引用集的集中管理和维护.但是数组方式下,数组大小需要提前被确定,并不允许修改大小,导致其作为一种灵活的数据容器的能力的功能大为下降. 为了方便的利用数据容器进行引用的管理,Java中提供了丰富的数据容器以满足程序员多样化的需求. j

java 容器Collection List Set Map概述

对JAVA的集合的理解是想对于数组 数组是大小固定的,并且同一个数组只能存放类型一样的数据(基本类型/引用类型) JAVA集合可以存储和操作数目不固定的一组数据. 所有的JAVA集合都位于 java.util包中! JAVA集合只能存放引用类型的的数据,不能存放基本数据类型. JAVA集合主要分为三种类型: Set(集) List(列表) Map(映射) Collection 接口 Collection是最基本的集合接口,声明了适用于JAVA集合(只包括Set和List)的通用方法. Set 和

Java 容器相关知识全面总结

Java实用类库提供了一套相当完整的容器来帮助我们解决很多具体问题.因为我本身是一名Android开发者,包括我在内很多安卓开发,最拿手的就是ListView(RecycleView)+BaseAdapter+ArrayList三剑客, 平时接触使用的容器也只有ArrayList和HashMap.导致对于整个Java容器体系的掌握和使用还停留在很浅的层面.省不足而思改进,那么跟着我来总结一下Java容器的相关知识吧. 结构 java容器类的继承结构 具体介绍 迭代器 Collection Lis