前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Java集合总结

Java集合总结

作者头像
用户5325874
发布2020-01-16 17:28:09
6240
发布2020-01-16 17:28:09
举报

Java集合类主要有2大分支,Collection及Map。 Collection体系如下:

14199675-d6af9e43c7f05c92.png
14199675-d6af9e43c7f05c92.png

image.png

14199675-e27ddafbbb38d4a1.png
14199675-e27ddafbbb38d4a1.png

image.png

Map体系如下:

14199675-3556e8660f8f98e3.png
14199675-3556e8660f8f98e3.png

image.png

** 补充图**

14199675-ef6b57c581172fbf.png
14199675-ef6b57c581172fbf.png

image.png

1、List接口和Set接口都继承自Collection接口,Collection接口继承Iterable接口(Iterable有一个Iterator方法),即可迭代的;Collection只能存储引用类型,并且是单个存储; 2、List接口存储元素特点:有序(存进去什么顺序取出来还什么顺序),可重复;Set接口存储元素特点:无序,不可重复 3、实现List接口主要的类包括ArrayList,LinkedList,Vector;实现Set的主要类包括:hashSet,另外还有一个TreeSet接口继承它(自动排序)

List

1、ArrayList (1)ArrayList实现了List接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null元素,底层通过数组实现。除该类未实现同步外,其余跟Vector大致相同。每个ArrayList都有一个容量(capacity),表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

(2)特点: A、查询效率高,插入删除效率低。查找的话,直接通过下标可以查找到,所以效率快;插入删除的话,由于插入(删除)位置后面的元素都需要移动,所以效率较差。 B、size(), isEmpty(), get(), set()方法均能在常数时间内完成,add()方法的时间开销跟插入位置有关,addAll()方法的时间开销跟添加元素的个数成正比。其余方法大都是线性时间。 C、add(int index, E e)需要先对元素进行移动,然后完成插入操作,也就意味着该方法有着线性的时间复杂度。

14199675-556ee7a1782a96ea.png
14199675-556ee7a1782a96ea.png

image.png

D、数组扩容: 从上面介绍的向ArrayList中存储元素的代码中,我们看到,每当向数组中添加元素时,都要去检查添加后元素的个数是否会超出当前数组的长度,如果超出,数组将会进行扩容,以满足添加数据的需求。数组扩容通过一个公开的方法ensureCapacity(int minCapacity)来实现。在实际添加大量元素前,我也可以使用ensureCapacity来手动增加ArrayList实例的容量,以减少递增式再分配的数量。ArrayList默认扩容1.5倍

14199675-eca642474ef469af.png
14199675-eca642474ef469af.png

image.png

E、在源码中看到 int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 这里其实有点意思,数组的最大长度为整数最大值减8。网上看到有人解释: 数组作为一个对象,需要一定的内存存储对象头信息,对象头信息最大占用内存不可超过8字节 参考:https://blog.csdn.net/ChineseYoung/article/details/80787071

(3)数组默认长度: 10,表示底层数组的实际大小。容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java泛型只是编译器提供的语法糖,所以这里的数组是一个Object数组,以便能够容纳任何类型的对象。

14199675-46b43a101505b338.png
14199675-46b43a101505b338.png

image.png

(4)非线程安全。为追求效率,ArrayList没有实现同步(synchronized),如果需要多个线程并发访问,用户可以手动同步,也可使用Vector替代。

2、LinkedList (1)LinkedList同时实现了List接口和Deque接口,也就是说它既可以看作一个顺序容器,又可以看作一个队列(Queue),同时又可以看作一个栈(Stack)。这样看来,LinkedList简直就是个全能冠军。当你需要使用栈或者队列时,可以考虑使用LinkedList,一方面是因为Java官方已经声明不建议使用Stack类,更遗憾的是,Java里根本没有一个叫做Queue的类(它是个接口名字)。 (2)关于栈或队列,现在的首选是ArrayDeque(双端队列),它有着比LinkedList(当作栈或队列使用时)有着更好的性能。 A、ArrayDeque内部使用数组实现,并且是循环数组 B、LinkedList内部使用链表实现 具体原因参考:https://blog.csdn.net/weixin_35785121/article/details/52165457 LinkedList底层通过双向链表实现。LinkedList的实现方式决定了所有跟下标相关的操作都是线性时间,而在首段或者末尾删除元素只需要常数时间。为追求效率LinkedList没有实现同步(synchronized),如果需要多个线程并发访问,可以先采用Collections.synchronizedList()方法对其进行包装。

(3)查询效率比较低,插入、删除效率比较高。查询的时候,每次都需要从头开始查询,查询效率O(N),但是插入(删除)只需要影响到插入位置前后的元素,因此插入(删除)效率较高

3、Vector (1)和ArrayList一样,底层使用数组实现 (2)vector是线程安全的,效率受到影响。 (3)vector在多线程环境下也会受到线程安全问题。比如说,一个线程去删除i位置上的元素,另外一个线程去拿i位置上的元素,就会报异常。 (4)默认长度:10

4、Stack Stack是继承自Vector的,所以用法啊,线程安全什么的跟Vector都差不多,只是有几个地方需要注意:

(1)add()和push(),stack是将最后一个element作为栈顶的,所以这两个方法对stack而言是没什么区别的,但是,它们的返回值不一样,add()返回boolean,就是添加成功了没有;push()返回的是你添加的元素。为了可读性以及将它跟栈有一丢丢联系,推荐使用push。

(2)peek()和pop(),这两个方法都能得到栈顶元素,区别是peek()只是读取,对原栈没有什么影响;pop(),从字面上就能理解,出栈,所以原栈的栈顶元素就没了。

5、List、ArrayList , LinkedList , Vector的对比 ArrayList和Vector本质都是用数组实现的,而LinkedList是用双链表实现的;所以,Arraylist和Vector在查找效率上比较高,增删效率比较低;LinkedList则正好相反。ArrayList是线程不安全的,Vector是线程安全的,效率肯定没有ArrayList高了。实际中一般也不怎么用Vector,可以自己做线程同步,也可以用Collections配合ArrayList实现线程同步。

6、适用场景 (1)查询数据,适用ArrayList (2)插入、删除适用LinkedList (3)保证数据安全用vector (4)因为ArrayList在内存不够的时候,默认是原来的1.5倍,vector默认是原来的2倍,因此,当元素数目越来越大,扩展次数多的时候,使用vector比较有优势。

Set

1、HashSet (1)不能保证元素的排列顺序,顺序有可能发生变化 (2)不是同步的 (3)集合元素可以是null,但只能放入一个null

2、TreeSet TreeSet是SortedSet接口的唯一实现类,TreeSet可以确保集合元素处于排序状态。TreeSet支持两种排序方式,自然排序和定制排序,其中自然排序为默认的排序方式。向TreeSet中加入的应该是同一个类的对象。 TreeSet判断两个对象不相等的方式是两个对象通过equals方法返回false,或者通过CompareTo方法比较没有返回0 自然排序是根据集合元素的大小,以升序排列,如果要定制排序,应该使用Comparator接口,实现 int compare(T o1,T o2)方法。

(1)TreeSet 是二叉树实现的,TreeSet中的数据是自动排好序的,不允许放入null值。

(2)HashSet 是哈希表实现的,HashSet中的数据是无序的,可以放入null,但只能放入一个null,两者中的值都不能重复,就如数据库中唯一约束。

(3)HashSet要求放入的对象必须实现HashCode()方法,放入的对象,是以hashcode码作为标识的,而具有相同内容的 String对象,hashCode是一样,所以放入的内容不能重复。但是同一个类的对象可以放入不同的实例 。

Map

14199675-f83e7734c12badb2.png
14199675-f83e7734c12badb2.png

image.png

1、HashMap (1)HashMap的结构:HashMap采用了链地址法,也就是数组+链表的方式处理hash冲突(HashMap主要作用是解决hash冲突)。

HashMap的主干是一个Entry数组。Entry是HashMap的基本组成单元,每一个Entry包含一个key-value键值对。

14199675-ff9631ddb353c883.png
14199675-ff9631ddb353c883.png

image.png

简单来说,HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的,如果定位到的数组位置不含链表(当前entry的next指向null),那么对于查找,添加等操作很快,仅需一次寻址即可;如果定位到的数组包含链表,对于添加操作,其时间复杂度依然为O(1),因为最新的Entry会插入链表头部,急需要简单改变引用链即可,而对于查找操作来讲,此时就需要遍历链表,通过key对象的equals方法逐一比对查找。所以,性能考虑,HashMap中的链表出现越少,性能才会越好。

将对向放入到HashMap或HashSet中时,有两个方法需要特别关心:A、hashCode()和equals()。hashCode()方法决定了对象会被放到哪个bucket里,当多个对象的哈希值冲突时,equals()方法决定了这些对象是否是“同一个对象”。所以,如果要将自定义的对象放入到HashMap或HashSet中,需要@Override hashCode()和equals()方法。 B、插入使用头插法

(2)get() get(Object key)方法根据指定的key值返回对应的value,该方法调用了getEntry(Object key)得到相应的entry,然后返回entry.getValue()。因此getEntry()是算法的核心。 算法思想是首先通过hash()函数得到对应bucket的下标,然后依次遍历冲突链表,通过key.equals(k)方法来判断是否是要找的那个entry。

14199675-a1d2eccbe580e592.png
14199675-a1d2eccbe580e592.png

image.png

(3)put( ) 当数组长度为2的n次幂的时候,不同的key算得得index相同的几率较小,那么数据在数组上分布就比较均匀,也就是说碰撞的几率小,相对的,查询的时候就不用遍历某个位置上的链表,这样查询效率也就较高了。 当程序试图将一个key-value对放入HashMap中时,程序首先根据该 key 的 hashCode() 返回值决定该 Entry 的存储位置:如果两个 Entry 的 key 的 hashCode() 返回值相同,那它们的存储位置相同。如果这两个 Entry 的 key 通过 equals 比较返回 true,新添加 Entry 的 value 将覆盖集合中原有 Entry 的 value,但key不会覆盖。如果这两个 Entry 的 key 通过 equals 比较返回 false,新添加的 Entry 将与集合中原有 Entry 形成 Entry 链,而且新添加的 Entry 位于 Entry 链的头部(头插法,形成hash桶)

(4)HashMap的resize(rehash) 当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,因为数组的长度是固定的。所以为了提高查询的效率,就要对HashMap的数组进行扩容,数组扩容这个操作也会出现在ArrayList中,这是一个常用的操作,而在HashMap数组扩容之后,最消耗性能的点就出现了:原数组中的数据必须重新计算其在新数组中的位置,并放进去,这就是resize。 那么HashMap什么时候进行扩容呢?当HashMap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,这是一个折中的取值。也就是说,默认情况下,数组大小为16,那么当HashMap中元素个数超过160.75=12的时候,就把数组的大小扩展为 2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知HashMap中元素的个数,那么预设元素的个数能够有效的提高HashMap的性能。

(5)HashMap的性能参数 有两个参数可以影响HashMap的性能:初始容量(inital capacity,初始为16)和负载系数(load factor,初始为0.75)。初始容量指定了初始table的大小,负载系数用来指定自动扩容的临界值。当entry的数量超过capacity*load_factor时,容器将自动扩容并重新哈希。对于插入元素较多的场景,将初始容量设大可以减少重新哈希的次数。

HashMap 包含如下几个构造器: HashMap():构建一个初始容量为 16,负载因子为 0.75 HashMap。 HashMap(int initialCapacity):构建一个初始容量为 initialCapacity,负载因子为 0.75 的 HashMap。 HashMap(int initialCapacity, float loadFactor):以指定初始容量、指定的负载因子创建一个 HashMap。 HashMap的基础构造器HashMap(int initialCapacity, float loadFactor)带有两个参数,它们是初始容量initialCapacity和加载因子loadFactor。 initialCapacity:HashMap的最大容量,即为底层数组的长度。 loadFactor:负载因子loadFactor定义为:散列表的实际元素数目(n)/ 散列表的容量(m)。 负载因子衡量的是一个散列表的空间的使用程度,负载因子越大表示散列表的装填程度越高,反之愈小。对于使用链表法的散列表来说,查找一个元素的平均时间是O(1+a),因此如果负载因子越大,对空间的利用更充分,哈希冲突多,然而后果是查找效率的降低;如果负载因子太小,哈希冲突少,那么散列表的数据将过于稀疏,对空间造成严重浪费。 HashMap的实现中,通过threshold字段来判断HashMap的最大容量: threshold = (int)(capacity * loadFactor) 结合负载因子的定义公式可知,threshold就是在此loadFactor和capacity对应下允许的最大元素数目,超过这个数目就重新resize,以降低实际的负载因子。默认的的负载因子0.75是对空间和时间效率的一个平衡选择。当容量超出此最大容量时, resize后的HashMap容量是容量的两倍:

(6)Fail-Fast机制 java.util.HashMap不是线程安全的,因此如果在使用迭代器的过程中有其他线程修改了map,那么将抛出ConcurrentModificationException,这就是所谓fail-fast策略。 这一策略在源码中的实现是通过modCount域,modCount顾名思义就是修改次数,对HashMap内容的修改都将增加这个值,那么在迭代器初始化过程中会将这个值赋给迭代器的expectedModCount。

在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map (注意到modCount声明为volatile,保证线程之间修改的可见性。)

迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。

2、LinkedHashMap (1)继承自HashMap (2)能够按插入的顺序进行遍历。 (3)内部使用双向链表实现。默认按插入元素的顺序排序,也可以更换成按照访问顺序排序。

3、TreeMap 红黑树算法的实现,说下红黑树的几种特征 (1)颜色特征:节点永远是红色或者黑色 (2)根特征:根节点永远是黑色 (3)外部特征:扩充外部节点都是 空的 黑色节点 (4)内部特征:“红色节点”的两个子节点都是 黑色的 (5)深度特征:任何节点到其子孙外部节点的路径,都包含相同数目的“黑色”节点。

4、WeakHashMap 对内部数据使用弱引用。如果内部的键值对在其他地方没有强引用引用着它,当系统内存不够的情况下,系统会自动清除该键值对。可以作为简单缓存表的解决方案(假如直接使用HashMap来存键值对,该键值对不会被GC回收)

5、HashMap和Hashtable对比 (1)HashMap不是线程安全的;HashTable是线程安全的,其线程安全是通过Sychronized实现。由于上述原因,HashMap效率高于HashTable。 (2)HashMap的键可以为null(key和value都可以),HashTable不可以(key和value都不可以)。 (3)多线程环境下,通常也不是用HashTable,因为效率低。HashMap配合Collections工具类使用实现线程安全。同时还有ConcurrentHashMap可以选择,该类的线程安全是通过Lock的方式实现的,所以效率高于Hashtable。 (4)Hashtable多了一个elements()方法,返回一个Enumeration对象,但是不能用iterator遍历。 (5)Hashtable数组默认大小为11,Hashmap为16,且一定是2的指数。 (6)Hashtable基于Dictionary类,Hashmap基于AbstractMap类。、

6、ConcurrentHashMap高并发原理总结 HashMap是线程不安全的,ConcurrentHashMap是线程安全的。

(1)采用锁分段技术 HashEntry为基本键值对存储单元,构成HashEntry数组。Segement继承自可重入锁ReentrantLock,充当了锁角色。ConcurrentHashMap将数组分段,每段由一个segment以及它的锁负责。减小锁的粒度能让并发性能更好。当一个线程获得一个segment锁、能够访问其中一段数据的时候,其他分段也能被其他线程正常访问,是互不干扰的。

(2)读写分离 读操作get不需要加锁,写操作put需要在对应的segment加锁。

(3)HashEntery 对象部分属性设为final 降低处理链表时的复杂性,减少了锁相关的操作。

(4)HashEntry 类的 value 域被声明为 Volatile 型 保证了能被多个线程读,而且不会读到过期数据。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • List
  • Set
  • Map
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档