Java——集合篇

目录

一、单列集合汇总 

1、List, Set, Queue, Map 的区别

2、HashSet、LinkedHashSet 和 TreeSet 的异同

3、ArrayList 和 Array(数组)的区别?

4、ArrayList 与 LinkedList 区别?(⭐⭐⭐⭐⭐)

5、ArrayList底层原理及扩容机制(⭐⭐⭐⭐)

6、Comparable 和 Comparator 的区别

7、ArrayList list=new ArrayList(10) 中的list扩容几次(⭐⭐)

8、如何实现数组和List之间的转换(⭐⭐)

 二、双列集合汇总

1、为什么要使用HashMap,底层原理是什么?(⭐⭐⭐⭐⭐)

2、HashMap 中 hash 值的作用?

3、HashMap 中的数组长度为什么必须是 2 的幂次方?(⭐⭐⭐)

4、JDK 1.7中HashMap 为什么会形成死循环?(⭐⭐⭐)

5、HashMap和Hashtable的区别

6、HashSet 如何检查重复

7、HashMap 为什么线程不安全?

8、HashMap 常见的遍历方式?

9、ConcurrentHashMap 和 Hashtable 的区别

10、JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

11、HashMap的put方法的具体流程。(⭐⭐⭐⭐⭐)

12、HashMap的扩容机制(⭐⭐⭐⭐)

13、hashMap的寻址算法(⭐⭐⭐⭐)

三、其他

1、为什么数组的索引从0开始?


                                              摘自javaguide的集合总体框架图:        


一、单列集合汇总 

1、List, Set, Queue, Map 的区别

  • List:底层基于object[]数组,存储的元素有序、可重复。

  • Set:底层基于HashMap实现,存储的元素无序,不可重复。

  • Queue:单端队列,存储的元素有序、可重复。

  • Map:使用键值对(key-value)存储,key 是无序的、不可重复的。

2、HashSet、LinkedHashSet 和 TreeSet 的异同

  • 三者都是Set 接口的实现类,都能保证元素唯一,并且都不是线程安全的。
  • 三者主要区别在于底层实现的数据结构不同:
    • HashSet底层基于哈希表,元素无序不可重复
    • LinkedHashSet底层基于链表和哈希表,元素具有唯一性和有序性(元素顺序满足FIFO)
    • TreeSet底层基于红黑树,支持对元素自定义排序规则。

3、ArrayList 和 Array(数组)的区别?

  • Array的大小固定;ArrayList可以动态扩容。
  • ArrayList允许使用泛型确保类型安全;Array不行。
  • ArrayList具备基本的增删改查操作;Array只能下标进行查询,没有动态增删改元素的能力。
  • Array既可以存储基本数据类型也可以存储对象;ArrayList只能存储对象,对于基本数据类型,需要将其转化为对应的包装类。

4、ArrayList 与 LinkedList 区别?(⭐⭐⭐⭐⭐)

  • 底层数据结构:ArrayList底层使用object[]数组,LinkedList底层使用双向链表。
  • 是否支持快速随机访问:ArrayList支持,LinkedList不支持。
  • 插入和删除是否受元素位置的影响:
    • ArrayList添加元素时默认添加至列表尾部,此时时间复杂度为O(1);但如果在指定位置添加和删除元素时,时间复杂度为O(n)。
    • LinkedList在头尾插入和删除时时间复杂度为O(1);但如果在指定位置插入删除元素时间复杂度为O(n)。

5、ArrayList底层原理及扩容机制(⭐⭐⭐⭐)

ArrayList三个构造函数:

  • ArrayList() 默认创建长度为0的数组。

  • ArrayList(int initialCapacity) 创建指定容量的数组。

  • ArrayList(Collection<? extends E> c) 使用集合c的大小作为数组容量。

ArrayList底层实现:

  • 底层数据结构:
    • 底层采用动态数组object[]实现。
  • 初始容量:
    • ArrayList初始容量为0,第一次添加数据时扩容为10。
  • 扩容逻辑:
    • 首先会创建一个新的数组,长度为原始数组的1.5倍(使用位运算),然后使用Arrays.copy方法将老数组的元素copy到新数组中,再将需要添加的新元素添加到新数组中

int newCapacity = oldCapacity + (oldCapacity >> 1);

6、Comparable 和 Comparator 的区别

        Comparable和Comparator都是接口,都可以用来进行比较、排序,可以将Comparable理解为“内部比较器”,Comparator理解为“外部比较器”

  • 实现方式
    • Comparable可以直接在需要进行排序的实体类中实现,重写compateTo方法即可。
    • Comparator需要另外创建一个实现Comparator接口的实现类来作为“比较器”,并在排序时将比较器作为参数传入。
  • 各自的优缺点
    • Comparable 实现比较简单,但是需要修改源代码。
    • Comparator需要新建比较器类,较为复杂,但是不需要修改源代码,并且新建的比较器类可以供多个对象排序使用。

具体参考这篇文章 Comparable和Comparator区别

7、ArrayList list=new ArrayList(10) 中的list扩容几次(⭐⭐)

        该语句声明和实例了一个 ArrayList,指定了容量为 10,未进行扩容。

8、如何实现数组和List之间的转换(⭐⭐)

  • 数组转List:使用Arrays工具类的asList方法。
  • List转数组:使用List的toArray方法。

数组转List后,如果修改了数组内容,list受影响吗?

  • 受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,会传入数组对象的引用。

List转数组后,如果修改了List内容,数组受影响吗?

  • 不受影响,调用toArray方法之后,底层会进行数组的拷贝,和原来List的元素就没关系了。


 二、双列集合汇总

1、为什么要使用HashMap,底层原理是什么?(⭐⭐⭐⭐⭐)

        关键点:①底层数据结构 ②工作方式

        HashMap是一个集合了查询效率和增删效率的容器,内部存的都是一个个键值对,可以通过访问键值对其进行访问和修改。

  • jdk1.8以前:
    • HashMap底层采用数组+链表的结构。
    • 添加数据时,会计算key值对应的哈希值,以定位key存放的位置,然后判断该位置的key是否相同,相同则直接覆盖;不相同即发生哈希冲突,会采用头插法将元素插入链表中。
  • jdk1.8之后,HashMap底层采用数组+链表+红黑树的结构,发生哈希冲突时采用尾插法将元素插入链表中,并且当链表长度大于阈值(默认为8)时,数组长度大于64时,会将链表改为红黑树进行存储。

2、HashMap 中 hash 值的作用?

        HashMap 中的 hash 值是由hash函数产生的,所有键值对存放的位置都是由 hash值和(length-1) 与运算得到的(length必须为2的幂次方,此时与运算等价于对length-1取模)。因此,简单来说hash值就是用来定位某键值对在HashMap中存放的位置的

3、HashMap 中的数组长度为什么必须是 2 的幂次方?(⭐⭐⭐)

        length为2的幂次方可以确保(length-1)的二进制低位都是1,此时hash&(length-1) 等价于 hash%(length-1) ,并且位运算的效率较高。

4、JDK 1.7中HashMap 为什么会形成死循环?(⭐⭐⭐)

        JDK 1.7中在链表中添加元素的方式是头插法当两个线程同时对HashMap进行扩容操作时,可能会形成环形链表,产生死循环。JDK 1.7中采用了尾插法来避免链表导致,从而避免产生环形链表。

        具体来说:HashMap扩容时,会将旧HashMap的数据移植到 扩容的新HashMap中,而由于链表的插入方式是头插法,a->b->c 会变成 c->b->a ,旧线程仍然认为a节点后面是b,而b节点后面已经是a了,这里就会产生死循环。

5、HashMap和Hashtable的区别

  • 线程安全与效率
    • HashMap线程不安全,但效率相对较高。
    • Hashtable线程安全,效率相对较低。
  • 对 Null 的支持:
    • HashMap可以存储null值,键只能存一个(对应的key为0),值可以存多个。
    • Hashtable不可以存储null键和null值。
  • 初始容量大小和每次扩容大小:
    • 不指定容量:
      • HashMap默认初始化大小为 16,之后每次扩充,容量变为原来的 2 倍。
      • Hashtable默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。
    • 指定容量
      • HashMap会将指定容量扩充为 2 的幂次方大小,即HashMap总是使用 2 的幂作为哈希表的大小。
      • Hashtable直接使用指定的容量。
  • 扩容方式不同:当容量不足时要进行resize方法,而resize有两个步骤:
    • ①扩容:两者扩容大小不一样。
    • ②rehash:两者都会重新计算hash值,而两者计算hash的值的方式也不同。(如下代码)
//hashMap计算hash值
static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }
//而hashtable直接使用hashcode值作为最终的hash值
  • 底层数据结构:
    • JDK1.8 以后的 HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时 ,将链表转化为红黑树以减少搜索时间。
    • Hashtable只使用链表解决哈希冲突。

6、HashSet 如何检查重复

        当我们将对象加入HashSet时,HashSet会计算该对象的hashcode值,并与HashSet中其他对象作比较:若没有hashcode相同的对象,则该对象不重复,允许加入;若有hashcode相同的对象,还需要使用equals()方法检查两对象是否真的相同,如果相同则不允许加入该对象。

ps:

  • hashcode是某个对象的哈希值,相同对象的hashcode值一定相同,不同对象的hashcode值也有可能相同(即哈希冲突)。

7、HashMap 为什么线程不安全?

        jdk1.8之前HashMap存在死循环数据丢失的问题。而数据丢失是所有版本都存在的问题,主要是由于并发情况下多线程同时进行put操作,并发生了哈希冲突,此时线程A在判断完哈希冲突之后阻塞了,线程B将数据插入,然后线程B苏醒之后就会将线程A的数据覆盖掉。

8、HashMap 常见的遍历方式?

主要有四种遍历方式:

  1. 使用迭代器(Iterator)的方式进行遍历。

            //使用迭代器(Iterator)EntrySet 的方式进行遍历.
            Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
            while(iterator.hasNext()){
                Map.Entry<Integer, String> entry = iterator.next();
                System.out.println(entry.getKey());
                System.out.println(entry.getValue());
            }
    
            //使用迭代器(Iterator)KeySet 的方式进行遍历;
            Iterator<Integer> iterator1 = map.keySet().iterator();
            while (iterator1.hasNext()){
                Integer next = iterator1.next();
                System.out.println(next);
                System.out.println(map.get(next));
            }
  2. 使用 For Each的方式进行遍历。

            //使用 For Each EntrySet 的方式进行遍历;
            for(Map.Entry<Integer,String> entry : map.entrySet()){
                System.out.println(entry.getKey());
                System.out.println(entry.getValue());
            }
    
            //使用 For Each KeySet 的方式进行遍历;
            for(Integer key:map.keySet()){
                System.out.println(key);
                System.out.println(map.get(key));
            }
  3. 使用 Lambda 表达式的方式进行遍历。

            map.forEach((key,value)->{
                System.out.println(key);
                System.out.println(value);
            });
  4. 使用 Streams API 的方式进行遍历。

        map.entrySet().stream().forEach((entry)->{
            System.out.println(entry.getKey());
            System.out.println(entry.getValue());
        });

        从性能上来说,迭代器是遍历是最快的,使用entryset比使用keyset要快

9、ConcurrentHashMap 和 Hashtable 的区别

        实现线程安全的方式不一样:

  • Hashtable几乎在所有添加、删除等方法都加了synchronized同步锁,相当于给哈希表加了一个大锁,多线程访问的时候,大量线程会被阻塞,效率低下。
  • jdk1.8之前的ConcurrentHashMap采用的是分段锁的思想,将哈希数组切割成若干个segment,每个segment包含n个键值对。 每一个segment都进行加锁,不同的segment不会有锁竞争,因此比Hashtable效率要高。
  • jdk1.8之后的ConcurrentHashMap锁粒度更细,没有采用分段锁的策略,而是在元素的节点上采用 CAS + synchronized 操作来保证并发的安全性,仅在链表或红黑树的首节点进行加锁,只要hash值不冲突,就不会产生并发,大大提高了效率。

10、JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同?

  • 线程安全实现方式:JDK 1.7采用分段锁,JDK 1.8锁粒度更细,对每个链表或红黑树的头节点加锁。
  • Hash 碰撞解决方法:JDK 1.7采用拉链法(头插法),JDK 1.8采用拉链法(尾插法)+红黑树。
  • 并发度:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。

11、HashMap的put方法的具体流程。(⭐⭐⭐⭐⭐)

  • 判断键值对数组是否为空,是的话就执行resize()方法进行扩容。
  • 根据键值key计算hash值得到数组索引。
  • 判断索引位置是否为空,是的话直接新建节点进行添加;不为空的话,判断索引位置的首个元素是否和key一样,一样就直接覆盖,不一样则:
    • 判断索引位置是否为treenode,即判断是否是红黑树,如果是红黑树,直接在树中插入键值对。
    • 遍历链表,如果链表长度大于8就转成红黑树,在树中执行插入操作;如果不是大于8,就在链表中执行插入;在遍历过程中判断key是否存在,存在就直接覆盖对应的value值。
  • 最后插入成功后,判断存在的键值对数量是否超过阈值,超过了需要进行扩容。

12、HashMap的扩容机制(⭐⭐⭐⭐)

  • 在初始化或元素达到阈值时,需要调用resize函数进行扩容,第一次数组长度初始化为16,扩容阈值为12(数组长度*0.75),每次扩容扩大为原来的两倍。
  • 扩容之后会创建一个新数组,遍历老数组,将元素移到新数组中:
    • 当前节点没有哈希冲突,直接对新数组的长度做模运算,插入当前节点。
    • 如果是红黑树,直接走红黑树的添加逻辑。
    • 如果是链表,需要遍历链表,(可能需要拆分链表),链表元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上。

13、hashMap的寻址算法(⭐⭐⭐⭐)

  • 先计算对象的哈希值,再进行二次哈希,即将哈希值右移16位再进行异或运算,可以使哈希分布更加均匀
  • 最后使用与运算( (capacity-1)  & hash))得到对象的索引。

三、其他

1、为什么数组的索引从0开始?

  • 数组在根据索引获取元素时,会使用寻址公式:数组首地址 + 索引 * 存储数据的类型大小
  • 如果索引从1开始算的话,寻址公式就变为:数组首地址 + (索引-1) * 存储数据的类型大小,底层多运算了一次减法操作,性能会降低。 

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
可以使用Java中的Stream API来实现这个功能。具体实现方法如下: 1. 使用Stream的map方法将集合中的对象转化成属性值的集合; 2. 使用Stream的distinct方法去重; 3. 使用Stream的count方法统计去重后的元素个数,如果为1则说明两个集合中的对象某个属性值全部相等,否则不相等。 示例代码如下: ```java public static boolean isPropertyEqual(List<Object> list1, List<Object> list2, String propertyName) { // 将集合中的对象转化成属性值的集合 List<Object> propertyList1 = list1.stream().map(obj -> { try { Field field = obj.getClass().getDeclaredField(propertyName); field.setAccessible(true); return field.get(obj); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); return null; } }).collect(Collectors.toList()); // 将集合中的对象转化成属性值的集合 List<Object> propertyList2 = list2.stream().map(obj -> { try { Field field = obj.getClass().getDeclaredField(propertyName); field.setAccessible(true); return field.get(obj); } catch (NoSuchFieldException | IllegalAccessException e) { e.printStackTrace(); return null; } }).collect(Collectors.toList()); // 去重 propertyList1 = propertyList1.stream().distinct().collect(Collectors.toList()); propertyList2 = propertyList2.stream().distinct().collect(Collectors.toList()); // 统计去重后的元素个数,如果为1则说明两个集合中的对象某个属性值全部相等,否则不相等 return propertyList1.size() == 1 && propertyList2.size() == 1 && propertyList1.get(0).equals(propertyList2.get(0)); } ``` 其中,list1和list2分别表示两个集合,propertyName表示要比较的属性名。返回值为一个布尔值,表示两个集合中的对象某个属性值是否全部相等。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值