java集合类应用实验总结_java集合类的总结

  • Post author:
  • Post category:java


1.数组、链表和哈希表的区别

数组顺序存储,查找快,插入和删除慢。

链表每个元素都用指针相连,查找慢,插入和删除快。

哈希表是数组合链表的结合体,根据key通过hash函数映射值,查找速度快。

set集合怎么保证元素唯一的呢?

通过hashcode方法和equals方法,添加元素的时候,先判断两个元素的hashcode方法,如果不一致则直接添加,无需在比较equals方法;如果hashcode值一致呢则开始比较equals方法。

如果比较是对象,则需要重写hashcode和equals方法。

2.set以及实现类

set是一个无序且不可重复的数据结构。其实现类有hashSet、treeSet和LinkedHashSet。

2.1.hashSet是由hash table(哈希表)组成的。其中的元素是无序的,内部使用hashmap存储数据。HashSet的add、remove、contains方法 的时间复杂度为常量O(1)。线程不安全,运行速度快;并且允许存储null。

2.2.treeSet是由treeMap构造的,而treeMap是由红黑树(自平衡的排序二叉树)构造的。因此是一个有序的set,如果插入的是自定义对象 需要让类实现 Comparable 接口并且必须要重写compareTo。因为对象里可能定义了多个变量,所以需要自定义比较器来实现这个顺序。线程不安全。

2.3.LinkedHashSet是一个哈希表和链表的结合,且是一个双向链表。和hashSet唯一的区别是LinkedHashSet内部使用的是LinkHashMap。这样做的意义或者好处就是LinkedHashSet中的元素顺序是可以保证的,也就是说遍历序和插入序是一致的。线程不安全。

3.list以及实现类

list是一个有序且可以重复的数据结构。

3.1. ArrayList

ArrayList底层依赖数组来实现,查询效率较高,增删效率较低

ArrayList中的元素有序、可重复、允许null值

ArrayList会进行扩容检测。当超过数组长度的时候,会自动进行扩容1.5倍。初始化时尽量指定初始容量,可避免频繁扩容,影响程序执行效率

线程不安全,适用于单线程环境。

3.2. Vector

Vector底层依赖数组来实现,查询效率较高,增删效率较低

Vector中的元素有序、可重复、允许null值,添加单个元素的话,只能添加到集合末尾

Vector会自动进行扩容。扩容后的容量由增量来决定,(2倍 or 原容量+增量)

大多数方法用关键字synchronized修饰,线程安全,因此效率很低,应用场景很少。

3.3. LinkedList

LinkedList底层依赖双向循环链表实现,增删效率较高,查询效率较低

LinkedList中的元素有序、可重复、允许null值

线程不安全,适用于单线程环境。

4.map以及实现类的总结

map是一种存储key-value的结构。

1、HashMap

底层由数组合链表组成,而JDK1.8中,HashMap采用位桶+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。线程不安全。当hashmap的容量超过初始容量的0.75时,将链表数组扩大2倍,把原链表数组的搬移到新的数组中,是为扩容。键值对可以设置为null。

put过程:

先通过hashcode方法,找到数组的位置,如果么有值直接添加,否则使用equals方法,有相等的直接覆盖,否则添加(头插法)。

①.判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;

②.根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;

③.判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;

④.判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤;

⑤.遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;

⑥.插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。

get过程:

先通过hashcode找到数组位置,在equals遍历。

通过在put方法上加锁可以使其为线程安全,synchronized里面放的应该是一个static的,volitile的对象。因为锁的是每一次插入的动作,必须多线程共享。

为什么会线程不安全,第一:是因为当两个线程同时对这个hashmap进行扩容(会新建数组,将原先的元素全部复制到新的数据结构里去),或出现循环的结果导致预想不到的结果,链表会形成闭环。

第二:当两个线程同时进行put操作,然后在同一个位置出现指针碰撞,都在链表的头部插入,会丢失一个数据。

2、hashtable

hashTable是线程安全的一个map实现类,它实现线程安全的方法是在各个方法上添加了synchronize关键字。但是现在已经不再推荐使用HashTable了,因为现在有了ConcurrentHashMap这个专门用于多线程场景下的map实现类,其大大优化了多线程下的性能。不可设置键值对为null。

3、ConcurrentHashMap

基本结构和hashmap一样。无序。

主要实现原理是segment段锁,它不再使用和HashTable一样的synchronize一样的关键字对整个方法进行枷锁,而是转而利用segment段落锁来对其进行加锁,以保证Map的多线程安全。各segment之间互不干扰,并发进行啊。每个segment都是一个hashmap。HashEntry 中的 value 变量 和 指向下一个节点的引用变量 next 都是使用 volatile 关键字进行修饰的,以此保证有序性和可见性。get() 方法不加锁,先找 segemnt 再找 桶put() 方法要加锁,插入之前先判断 count 与阈值的关系,有需要的话则进行扩容处理(桶的容量扩容两倍)。put、remove、replace、clear 操作都要加锁。size() 先尝试两次不加锁的统计各个 segment 中的 count,如果结果不一致,则采用对全部 segment 加锁的方式统计。

Java1.8时,采用了cas来实现。

算法思路:V是共享变量,我们拿着自己准备的这个E,去跟V去比较,如果E == V ,说明当前没有其它线程在操作,所以,我们把N 这个值 写入对象的 V 变量中。如果 E != V ,说明我们准备的这个E,已经过时了,所以我们要重新准备一个最新的E ,去跟V 比较,比较成功后才能更新 V的值为N。

使用 synchronized 和 volatile 关键字来保证线程安全

取消 segment ,当桶中 链表 长度大于 8 时,将链表转换为 RBTree

get()方法不加锁

put()方法:

如果桶是空的,则初始化桶,并采用 CAS 操作将元素放在链表的首位

如果桶非空,则通过使用 synchronized 锁住 链表的第一个节点(以此来锁住整个桶),再使用 Unsafe 类的 CAS 操作将元素放在链表的末尾

4、TreeMap

红黑树实现,默认按字典排序。

5、LinkedHashMap

有序,一个哈希表和双向链表的结合。

6、weakHashMap



版权声明:本文为weixin_39979167原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。