深圳幻海软件技术有限公司 欢迎您!

java常用集合类

2023-07-18

目录一、集合1.1、集合概念1.2、集合特点1.3、常用的集合类1.4、集合和数组的区别1.5、List,Set,Map三者的区别?1.6、集合底层的数据结构二、Collection接口2.1、List2.1.1、list集合元素删除2.1.2、集合元素判断2.1.3、List是线程不安全的&nbs

目录

一、集合

1.1、集合概念

1.2、集合特点

1.3、常用的集合类

1.4、集合和数组的区别

1.5、List,Set,Map三者的区别?

1.6、集合底层的数据结构

二、Collection接口

2.1、List

2.1.1、list集合元素删除

2.1.2、集合元素判断

2.1.3、List是线程不安全的

 2.1.4、ArrayList的优缺点

2.1.5、ArrayList 和 LinkedList 的区别是什么?

2.2、set集合

2.2.1、HashSet如何检查重复?HashSet是如何保证数据不可重复的?

2.2.2、hashCode()与equals()的相关规定:

2.2.3、HashSet与HashMap的区别

2.3、map集合

2.3.1、HashMap的实现原理

2.3.2、HashMap在JDK1.7和JDK1.8中有哪些不同?

2.3.3、HashMap的扩容操作是怎么实现的?

2.3.4、ConcurrentHashMap 和 Hashtable 的区别?


一、集合

1.1、集合概念

  • 集合就是一个放数据的容器,准确的说是放数据对象引用的容器

  • 集合类存放的都是对象的引用,而不是对象的本身

  • 集合类型主要有3种:set(集)、list(列表)和map(映射)。

1.2、集合特点

①集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。

②和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小

1.3、常用的集合类

  • Map接口和Collection接口是所有集合框架的父接口:
  1. Collection接口的子接口包括:Set接口和List接口
  2. Map接口的实现类主要有:HashMap、TreeMap、Hashtable、ConcurrentHashMap以及Properties等
  3. Set接口的实现类主要有:HashSet、TreeSet、LinkedHashSet等
  4. List接口的实现类主要有:ArrayList、LinkedList、Stack以及Vector等

1.4、集合和数组的区别

①数组是固定长度的;集合可变长度的。

②数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。

③数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。

1.5、List,Set,Map三者的区别?

1、Java 容器分为 Collection 和 Map 两大类,Collection集合的子接口有Set、List、Queue三种子接口。我们比较常用的是Set、List,Map接口不是collection的子接口。

Collection集合主要有List和Set两大接口

        List:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

        Set:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

  • Map是一个键值对集合,存储键、值和之间的映射。 Key无序,唯一;value 不要求有序,允许重复。Map没有继承于Collection接口,从Map集合中检索元素时,只要给出键对象,就会返回对应的值对象。

    • Map 的常用实现类:HashMap、TreeMap、HashTable、LinkedHashMap、ConcurrentHashMap

1.6、集合底层的数据结构

1、Collection
        (1) List
                ①Arraylist: Object数组
                ②Vector: Object数组
                ③LinkedList:双向循环链表
        (2)Set
                ①HashSet(无序,唯一)︰基于 HashMap 实现的,底层采用HashMap来保存元素
                ②LinkedHashSet: LinkedHashSet继承与HashSet,并且其内部是通过LinkedHashMap来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于Hashmap 实现一样,不过还是有一点点区别的。
                ③TreeSet(有序,唯一):红黑树(自平衡的排序二叉树。)

2、Map
        ①HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的("拉链法"解决冲突) .JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
        ②LinkedHashMap: LinkedHashMap继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
        ③HashTable: 数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的
        ④TreeMap:红黑树(自平衡的排序二叉树)

二、Collection接口

1、接口可以被继承。

2、接口可以被多次实现

2.1、List

1、创建list

  1. //只能用list接口里的方法
  2. List list1 = new ArrayList()
  3. //可以用ArrayList里的所有方法,因为AnrayList是实现list接口,所以可以调用的方法,比第一种要多
  4. ArrayList list2 = new ArrayList();
  5. Student student = new Student();

2、list集合元素添加

  1. List<String> list = new ArrayList<>();
  2. //下面这样写会有数组扩容的操作,会消耗性能
  3. list.add("asa" );
  4. list.add ( "qqq");
  5. //如果确定了list里面要放的元紊,建议这么写,性能比较高,没有扩容操作,但是不能再使用add添加新元素
  6. List<String> list1 = Arrays.asList( "asa" , "qqq"");
  7. list1.add( "222"");
  8. System.out.println(list1.toString());

3、数组与List如何转换

①数组转 List:使用Arrays. asList(array)进行转换。

②List转数组:使用List自带的 toArray)方法。

代码实例:

  1. // list to array
  2. List<String> list = new ArrayList<String>();
  3. list.add("123");
  4. list.add("456");
  5. list.toArray();
  6. // array to list
  7. String[] array = new String[]{"123","456"};
  8. Arrays.asList(array);

2.1.1、list集合元素删除

        从下面结果可知集合中的元素并没有全部删除。

  1. public class ListDemo {
  2. public static void main(String[] args) {
  3. List<String> list=new ArrayList<>();
  4. list.add("hello");
  5. list.add("hello");
  6. list.add("hello");
  7. list.add("hello");
  8. for (int i = 0; i < list.size(); i++) {
  9. if("hello".equals(list.get(i))){
  10. list.remove(i);
  11. }
  12. }
  13. System.out.println(list);
  14. }
  15. }

结果:[hello, hello]

原因如下图:

1、解决方法

①倒序删除

  1. public class ListDemo {
  2. public static void main(String[] args) {
  3. List<String> list=new ArrayList<>();
  4. list.add("hello");
  5. list.add("hello");
  6. list.add("hello");
  7. list.add("hello");
  8. for (int i = list.size(); i >=0; i--) {
  9. if("hello".equals(list.get(i))){
  10. list.remove(i);
  11. }
  12. }
  13. System.out.println(list);
  14. }
  15. }

结果是【】

②迭代器删除

利用iterator自带的remove方法进行删除,由于iterator.remove()方法删除时会自动进行数组下进行移位操作。

  1. public static void iterator(ArrayList<Long> list) {
  2. Iterator<Long> it = list.iterator();
  3. while (it.hasNext()) {
  4. Long a = it.next();
  5. if (a == 3) {
  6. it.remove();
  7. }
  8. }
  9. System.out.println(list);

③lambada表达式删除

  1. public static void lambda(ArrayList<Long> list) {
  2. ArrayList<Long> delList = new ArrayList<>();
  3. list.stream().forEach(vo -> {
  4. if (vo == 3) {
  5. delList.add(vo);
  6. }
  7. });
  8. list.removeAll(delList);
  9. System.out.println(list);
  10. }

2.1.2、集合元素判断

1、判断是否为空

  1. List<String> list=new ArrayList<>();
  2. //使用工具类进行判断不容易出现空指针,下面是字符串的工具类
  3. System.out.println(Strutil.isBlank("hello"));
  4. //集合判断是否是空集合,下面是集合的工具类
  5. System.out.println(CollectionUtil.isEmpty(list));

2.1.3、List是线程不安全的

①线程类

  1. public class MyThread implements Runnable{
  2. ArrayList<String> aaa;
  3. public MyThread(ArrayList<String> aaa){
  4. this.aaa=aaa;
  5. }
  6. @Override
  7. public void run() {
  8. for (int i = 0; i < 10000; i++) {
  9. aaa.add("aaa"+i);
  10. System.out.println(Thread.currentThread().getName()+"在第"+i+ aaa.get(i));
  11. }
  12. }
  13. }

②测试类

  1. public class Test {
  2. public static void main(String[] args) {
  3. ArrayList<String> list=new ArrayList<>();
  4. MyThread m1=new MyThread(list);
  5. MyThread m2=new MyThread(list);
  6. new Thread(m1).start();
  7. new Thread(m2).start();
  8. }
  9. }

③结果:对应的元素添加不一致

 2.1.4、ArrayList的优缺点

①ArrayList的优点如下:
        ArrayList底层以数组实现,是一种随机访问模式。ArrayList 实现了RandomAccess接口,因此查找的时候非常快。
        ArrayList在顺序添加一个元素的时候非常方便。
②ArrayList 的缺点如下:
        删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能,插入元素的时候,也需要做一次元素复制操作,缺点同上。
③ArrayList 比较适合顺序添加、随机访问的场景。

2.1.5、ArrayList 和 LinkedList 的区别是什么?

①数据结构实现: ArrayList是动态数组的数据结构实现,而LinkedList 是双向链表的数据结构实现。

②随机访问效率: ArrayList 比 LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
③增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList效率要高,因为ArrayList增删操作要影响数组内的其他数据的下标。
④内存空间占用: LinkedList 比 ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
⑤线程安全: ArrayList和LinkedList都是不同步的,也就是不保证线程安全;
⑥综合来说,在需要频繁读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList。
⑦LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
 

2.2、set集合

List是有序可重复的,set是无序不重复的。

代码演示:

  1. public class ListDemo {
  2. public static void main(String[] args) {
  3. List<String> list=new ArrayList<>();
  4. list.add("qqq");
  5. list.add("qqq");
  6. list.add("qqq");
  7. Set<String> set=new HashSet<>();
  8. set.add("www");
  9. set.add("www");
  10. set.add("www");
  11. System.out.println(list);
  12. System.out.println(set);
  13. }
  14. }

结果如下图

1、HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为present,因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成,HashSet 不允许重复的值。

2、hashcode就是一个int的数。

        两个对象的hashcode如果相等,这两个不一定相等,因为可能存在hash碰撞。但是两个对象的hashcode如果不相等,那么这两个对象绝对不相等。

  • hashmap判断两个key是否相等
    • 先判断这两个对象的hashcode是否相等,如果相等,再去调用equals。如果不相等,就直接认为这两个对象不相等。好处就是:equals需要比对太多元素性能较差,但Hashcode直接比较,性能较高一些。

3,两个对象的equals相等,那么这两个对象一定相等。

2.2.1、HashSet如何检查重复?HashSet是如何保证数据不可重复的?

①向HashSet 中add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。
②HashSet中的add )方法会使用HashMap 的put()方法。
③HashMap的 key是唯一的,由源码可以看出 HashSet添加进去的值就是作为HashMap 的key,并且在HashMap中如果K/V相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复(HashMap比较key是否相等是先比较hashcode 再比较equals ) 。

部分源码展示:

  1. private static final Object PRESENT = new Object();
  2. private transient HashMap<E,Object> map;
  3. public HashSet() {
  4. map = new HashMap<>();
  5. }
  6. public boolean add(E e) {
  7. // 调用HashMap的put方法,PRESENT是一个至始至终都相同的虚值
  8. return map.put(e, PRESENT)==null;
  9. }

2.2.2、hashCode()与equals()的相关规定

①如果两个对象相等,则hashcode一定也是相同的
        hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值

②两个对象相等,对两个equals方法返回true
③两个对象有相同的hashcode值,它们也不一定是相等的。

④综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
⑤hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

2.2.3、HashSet与HashMap的区别

2.3、map集合

2.3.1、HashMap的实现原理

1、HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和nul键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
2、HashMap的数据结构:在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个"链表散列"的数据结构,即数组和链表的结合体。
3、HashMap基于Hash 算法实现的
        ①当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
        ②存储时,如果出现hash值相同的key,此时有两种情况。

  • 如果key相同,则覆盖原始值;
  • 如果key不同(出现冲突),则将当前的key-value放入链表中

4、获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
5、理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
6、需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

2.3.2、HashMap在JDK1.7和JDK1.8中有哪些不同?

 1、红黑树

红黑树是一种特殊的二叉查找树。红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)。

① 红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色[注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点! ]。


②如果一个结点是红色的,则它的子结点必须是黑色的。


③每个结点到叶子结点NIL所经过的黑色结点的个数一样的。[确保没有一条路径会比其他路径长出俩倍,所以红黑树是相对接近平衡的二叉树的!]


④红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。为什么呢?道理很简单,添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。

2.3.3、HashMap的扩容操作是怎么实现的?

  • 在jdk1.8中,resize方法是在hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容;
  • 每次扩展的时候,都是扩展2倍;
  • 扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
  • 在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值值(第一次为12),这个时候在扩容的同时也会伴随的桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为O,重新进行hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
     
  1. final Node<K,V>[] resize() {
  2. Node<K,V>[] oldTab = table;//oldTab指向hash桶数组
  3. int oldCap = (oldTab == null) ? 0 : oldTab.length;
  4. int oldThr = threshold;
  5. int newCap, newThr = 0;
  6. if (oldCap > 0) {//如果oldCap不为空的话,就是hash桶数组不为空
  7. if (oldCap >= MAXIMUM_CAPACITY) {//如果大于最大容量了,就赋值为整数最大的阀值
  8. threshold = Integer.MAX_VALUE;
  9. return oldTab;//返回
  10. }//如果当前hash桶数组的长度在扩容后仍然小于最大容量 并且oldCap大于默认值16
  11. else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
  12. oldCap >= DEFAULT_INITIAL_CAPACITY)
  13. newThr = oldThr << 1; // double threshold 双倍扩容阀值threshold
  14. }
  15. // 旧的容量为0,但threshold大于零,代表有参构造有cap传入,threshold已经被初始化成最小2的n次幂
  16. // 直接将该值赋给新的容量
  17. else if (oldThr > 0) // initial capacity was placed in threshold
  18. newCap = oldThr;
  19. // 无参构造创建的map,给出默认容量和threshold 16, 16*0.75
  20. else { // zero initial threshold signifies using defaults
  21. newCap = DEFAULT_INITIAL_CAPACITY;
  22. newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
  23. }
  24. // 新的threshold = 新的cap * 0.75
  25. if (newThr == 0) {
  26. float ft = (float)newCap * loadFactor;
  27. newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
  28. (int)ft : Integer.MAX_VALUE);
  29. }
  30. threshold = newThr;
  31. // 计算出新的数组长度后赋给当前成员变量table
  32. @SuppressWarnings({"rawtypes","unchecked"})
  33. Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];//新建hash桶数组
  34. table = newTab;//将新数组的值复制给旧的hash桶数组
  35. // 如果原先的数组没有初始化,那么resize的初始化工作到此结束,否则进入扩容元素重排逻辑,使其均匀的分散
  36. if (oldTab != null) {
  37. // 遍历新数组的所有桶下标
  38. for (int j = 0; j < oldCap; ++j) {
  39. Node<K,V> e;
  40. if ((e = oldTab[j]) != null) {
  41. // 旧数组的桶下标赋给临时变量e,并且解除旧数组中的引用,否则就数组无法被GC回收
  42. oldTab[j] = null;
  43. // 如果e.next==null,代表桶中就一个元素,不存在链表或者红黑树
  44. if (e.next == null)
  45. // 用同样的hash映射算法把该元素加入新的数组
  46. newTab[e.hash & (newCap - 1)] = e;
  47. // 如果e是TreeNode并且e.next!=null,那么处理树中元素的重排
  48. else if (e instanceof TreeNode)
  49. ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
  50. // e是链表的头并且e.next!=null,那么处理链表中元素重排
  51. else { // preserve order
  52. // loHead,loTail 代表扩容后不用变换下标,见注1
  53. Node<K,V> loHead = null, loTail = null;
  54. // hiHead,hiTail 代表扩容后变换下标,见注1
  55. Node<K,V> hiHead = null, hiTail = null;
  56. Node<K,V> next;
  57. // 遍历链表
  58. do {
  59. next = e.next;
  60. if ((e.hash & oldCap) == 0) {
  61. if (loTail == null)
  62. // 初始化head指向链表当前元素e,e不一定是链表的第一个元素,初始化后loHead
  63. // 代表下标保持不变的链表的头元素
  64. loHead = e;
  65. else
  66. // loTail.next指向当前e
  67. loTail.next = e;
  68. // loTail指向当前的元素e
  69. // 初始化后,loTail和loHead指向相同的内存,所以当loTail.next指向下一个元素时,
  70. // 底层数组中的元素的next引用也相应发生变化,造成lowHead.next.next.....
  71. // 跟随loTail同步,使得lowHead可以链接到所有属于该链表的元素。
  72. loTail = e;
  73. }
  74. else {
  75. if (hiTail == null)
  76. // 初始化head指向链表当前元素e, 初始化后hiHead代表下标更改的链表头元素
  77. hiHead = e;
  78. else
  79. hiTail.next = e;
  80. hiTail = e;
  81. }
  82. } while ((e = next) != null);
  83. // 遍历结束, 将tail指向null,并把链表头放入新数组的相应下标,形成新的映射。
  84. if (loTail != null) {
  85. loTail.next = null;
  86. newTab[j] = loHead;
  87. }
  88. if (hiTail != null) {
  89. hiTail.next = null;
  90. newTab[j + oldCap] = hiHead;
  91. }
  92. }
  93. }
  94. }
  95. }
  96. return newTab;
  97. }

2.3.4、ConcurrentHashMap 和 Hashtable 的区别?

1、ConcurrentHashMap和Hashtable 的区别主要体现在实现线程安全的方式上不同。

  • 底层数据结构:JDK1.7的ConcurrentHashMap底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable和JDK1.8之前的HashMap 的底层数据结构类似都是采用数组+链表的形式,数组是HashMap 的主体,链表则是主要为了解决哈希冲突而存在的;
  • 实现线程安全的方式:
    • 在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。)到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(JDK1.6以后对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本;
    • 2.② Hashtable(同一把锁):使用synchronized来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
文章知识点与官方知识档案匹配,可进一步学习相关知识
算法技能树首页概览49291 人正在系统学习中