Java【集合面试题】

目录

一、集合容器概述

1. 什么是集合

2. 集合的特点

3. 集合和数组的区别

4. 使用集合框架的好处

5. 常用的集合类有哪些?

6. List,Set,Map三者的区别?

7. 集合框架底层数据结构

8. 哪些集合类是线程安全的?

9. Java集合的快速失败机制 “fail-fast”?

10. 怎么确保一个集合不能被修改?

二、Collection接口

List接口

11. 迭代器 Iterator 是什么?

12. Iterator 怎么使用?有什么特点?

13. 如何边遍历边移除 Collection 中的元素?

14. Iterator 和 ListIterator 有什么区别?

15. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List遍历的最佳实践是什么?

16. 说一下 ArrayList 的优缺点

17. 如何实现数组和 List 之间的转换?

18. ArrayList 和 LinkedList 的区别是什么?

19. ArrayList 和 Vector 的区别是什么?

20. 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述

21. 多线程场景下如何使用 ArrayList?

22. 为什么 ArrayList 的 elementData 加上 transient 修饰?

23. List 和 Set 的区别

Set接口

24. 说一下 HashSet 的实现原理?

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

26. HashSet与HashMap的区别

三、Map接口

27. 什么是Hash算法

28. 什么是hash冲突?

29.如何解决hash冲突?

30. 说一下HashMap的实现原理?

31. HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现

32. 什么是红黑树

33. HashMap的put方法的具体流程?

34. HashMap的扩容操作是怎么实现的?

35. 能否使用任何类作为 Map 的 key?

36. 为什么HashMap中String、Integer这样的包装类适合作为Key?

37. 如果使用Object作为HashMap的Key,应该怎么办呢?

38. HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?

39. HashMap 的长度为什么是2的幂次方

40. HashMap 与 HashTable 有什么区别?

41. 什么是TreeMap 

42. 如何决定使用 HashMap 还是 TreeMap?

43. HashMap 和 ConcurrentHashMap 的区别

44. ConcurrentHashMap 和 Hashtable 的区别?

45. ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?

四、辅助工具类

46. Array 和 ArrayList 有何区别?

47. comparable 和 comparator的区别?

48. Collection 和 Collections 有什么区别?

49. TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?


一、集合容器概述


1. 什么是集合

  • 集合就是一个放数据的容器,准确的说是放数据对象引用的容器
  • 集合类存放的都是对象的引用,而不是对象的本身
  • 集合类型主要有3种:set(集)、list(列表)和map(映射)。


2. 集合的特点


集合的特点主要有如下两点:

  1. 集合用于存储对象的容器,对象是用来封装数据,对象多了也需要存储集中式管理。
  2. 和数组对比对象的大小不确定。因为集合是可变长度的。数组需要提前定义大小


3. 集合和数组的区别

  1. 数组是固定长度的;集合可变长度的。
  2. 数组可以存储基本数据类型,也可以存储引用数据类型;集合只能存储引用数据类型。
  3. 数组存储的元素必须是同一个数据类型;集合存储的对象可以是不同数据类型。


4. 使用集合框架的好处

  1. 容量自增长;
  2. 提供了高性能的数据结构和算法,使编码更轻松,提高了程序速度和质量;
  3. 可以方便地扩展或改写集合,提高代码复用性和可操作性。
  4. 通过使用JDK自带的集合类,可以降低代码维护和学习新API成本。


5. 常用的集合类有哪些?

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


6. List,Set,Map三者的区别?

在这里插入图片描述
        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


7. 集合框架底层数据结构


Collection
List

  • Arraylist: Object数组
  • Vector: Object数组
  • LinkedList: 双向循环链表

Set

  • HashSet(无序,唯一):基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSet 继承与 HashSet,并且其内部是通LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 Hashmap 实现一样,不过还是有一点点区别的。
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树。)


Map

         HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是

主要为了解决哈希冲突而存在的(“拉链法”解决冲突).JDK1.8以后在解决哈希冲突时有了较

大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间

        LinkedHashMap:LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加

了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的

操作,实现了访问顺序相关逻辑。

        HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的.

        TreeMap: 红黑树(自平衡的排序二叉树)


8. 哪些集合类是线程安全的?

  • Vector:就比Arraylist多了个 synchronized (线程安全),因为效率较低,现在已经不太建议使用。
  • hashTable:就比hashMap多了个synchronized (线程安全),不建议使用。
  • ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)

9. Java集合的快速失败机制 “fail-fast”?

是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生fail-fast 机制。


        例如:假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制。
        原因:迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。集合在被遍历期间如果内容发生变化,就会改变modCount的值。每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;否则抛出异常,终止遍历。


解决办法:

  • 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized。
  • 使用CopyOnWriteArrayList来替换ArrayList

10. 怎么确保一个集合不能被修改?


        可以使用 Collections. unmodifiableCollection(Collection c) 方法来创建一个只读集合,这样改变集合的任何操作都会抛出 Java. lang. UnsupportedOperationException 异常。
示例代码如下:

List<String> list = new ArrayList<>();
list. add("x");
Collection<String> clist = Collections. unmodifiableCollection(list);
clist. add("y"); // 运行时此行报错
System. out. println(list. size());


二、Collection接口


List接口


11. 迭代器 Iterator 是什么?

        Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。因为所有Collection接继承了Iterator迭代器

12. Iterator 怎么使用?有什么特点?

  • Iterator 使用代码如下:
	List<String> list = new ArrayList<>();
	Iterator<String> it = list. iterator();
	while(it. hasNext()){
		String obj = it. next();
		System. out. println(obj);
	}
  • Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改
    的时候,就会抛出 ConcurrentModificationException 异常。

13. 如何边遍历边移除 Collection 中的元素?


        边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法,如下:

Iterator<Integer> it = list.iterator();
	while(it.hasNext()){
		*// do something*
		it.remove();
	}


一种最常见的错误代码如下:

  

	for(Integer i : list){
		list.remove(i)
	}


        运行以上错误代码会报 ConcurrentModificationException 异常。这是因为当使用
foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list,但同时该 list 正在被
Iterator.remove() 修改。Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。


14. Iterator 和 ListIterator 有什么区别?


Iterator 可以遍历 Set 和 List 集合,而 ListIterator 只能遍历 List。
Iterator 只能单向遍历,而 ListIterator 可以双向遍历(向前/后遍历)。
ListIterator 实现 Iterator 接口,然后添加了一些额外的功能,比如添加一个元素、替换一个元
素、获取前面或后面元素的索引位置。

15. 遍历一个 List 有哪些不同的方式?每种方法的实现原理是什么?Java 中 List遍历的最佳实践是什么?


遍历方式主要有以下几种:

1.for循环遍历

通过for循环即可遍历List中的所有元素,基于计数器。在集合外部维护一个计数器,然后依次读取每一个位置的元素,当读取到最后一个元素后停止.代码如下:

List<String> list = new ArrayList<>();
for(int i = 0; i < list.size(); i++) {
    System.out.println(list.get(i));
}

2.foreach遍历

使用增强型for循环可以更加简洁地遍历List,foreach 内部也是采用了 Iterator 的方式实现,使用时不需要显式声明Iterator 或计数器。优点是代码简洁,不易出错;缺点是只能做简单的遍历,不能在遍历过程中操作数据集合,例如删除、替换。代码如下:

List<String> list = new ArrayList<>();
for (String s : list) {
    System.out.println(s);
}

3.迭代器遍历

使用迭代器也可以遍历List中的所有元素,代码如下:

List<String> list = new ArrayList<>();
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    System.out.println(iterator.next());
}

4.Lambda表达式遍历

Java 8引入了Lambda表达式,可以通过Lambda表达式更方便地遍历List中的所有元素,代码如下:

List<String> list = new ArrayList<>();
list.forEach(s -> System.out.println(s));

最佳实践:Java Collections 框架中提供了一个 RandomAccess 接口,用来标记 List 实现是否支
持 Random Access。

  • 如果一个数据集合实现了该接口,就意味着它支持 Random Access,按位置读取元素的平均时间复杂度为 O(1),如ArrayList。
  • 如果没有实现该接口,表示不支持 Random Access,如LinkedList。
  • 推荐的做法就是,支持 Random Access 的列表可用 for 循环遍历,否则建议用 Iterator 或foreach 遍历。

16. 说一下 ArrayList 的优缺点


ArrayList的优点如下:

  • ArrayList 底层以数组实现,是一种随机访问模式。ArrayList 实现了 RandomAccess 接口,因此查找的时候非常快。
  • ArrayList 在顺序添加一个元素的时候非常方便。


ArrayList 的缺点如下:

  • 删除元素的时候,需要做一次元素复制操作。如果要复制的元素很多,那么就会比较耗费性能。
  • 插入元素的时候,也需要做一次元素复制操作,缺点同上。


ArrayList 比较适合顺序添加、随机访问的场景。

17. 如何实现数组和 List 之间的转换?

  • 数组转 List:使用 Arrays. asList(array) 进行转换。
  • List 转数组:使用 List 自带的 toArray() 方法。


代码示例:

    // list to array
	List<String> list = new ArrayList<String>();
	list.add("123");
	list.add("456");
	list.toArray();
	// array to list
	String[] array = new String[]{"123","456"};
	Arrays.asList(array);

18. ArrayList 和 LinkedList 的区别是什么?

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


19. ArrayList 和 Vector 的区别是什么?

  1. 这两个类都实现了 List 接口(List 接口继承了 Collection 接口),他们都是有序集合
  2. 线程安全:Vector 使用了 Synchronized 来实现线程同步,是线程安全的,而 ArrayList 是非线程安全的。
  3. 性能:ArrayList 在性能方面要优于 Vector。
  4. 扩容:ArrayList 和 Vector 都会根据实际的需要动态的调整容量,只不过在 Vector 扩容每次会增加 1 倍,而 ArrayList 只会增加 50%。
  5. Vector类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。Arraylist不是同步的,所以在不需要保证线程安全时时建议使用Arraylist。


20. 插入数据时,ArrayList、LinkedList、Vector谁速度较快?阐述

  • ArrayList和Vector 底层的实现都是使用数组方式存储数据。数组元素数大于实际存储的数据以便增加和插入元素,它们都允许直接按序号索引元素,但是插入元素要涉及数组元素移动等内存操作,所以索引数据快而插入数据慢
  • Vector 中的方法由于加了 synchronized 修饰,因此 Vector 是线程安全容器,但性能上较ArrayList差
  • LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快


21. 多线程场景下如何使用 ArrayList?


ArrayList 不是线程安全的,如果遇到多线程场景,可以通过 Collections 的 synchronizedList 方法将其转换成线程安全的容器后再使用。例如像下面这样:

    

List<String> synchronizedList = Collections.synchronizedList(list);
	synchronizedList.add("aaa");
	synchronizedList.add("bbb");
	for (int i = 0; i < synchronizedList.size(); i++) {
		System.out.println(synchronizedList.get(i));
	}


22. 为什么 ArrayList 的 elementData 加上 transient 修饰?

        transient 标注的属性序列化时会被jvm忽略,每次序列化时,先调defaultWriteObject() 方法序列化 ArrayList 中的非 transient 元素,然后遍历 elementData,只序列化已存入的元素,这样既加快了序列化的速度,又减小了序列化之后的文件大小。


23. List 和 Set 的区别

相同点:

  • 都是用来存放对象的集合。
  • 都实现了 Iterable 接口,可以使用 Iterator 进行遍历。
  • 都可以通过序列化和反序列化进行对象的存储和读取。

不同点:

  • List 是有序的集合,可以存储重复元素,可以通过下标(索引)访问元素;而 Set 是无序的集合,不能存储重复元素,不支持通过下标访问元素。
  • List 使用的是动态数组的数据结构,即底层实现是一个数组,可以根据需要对数组进行大小的调整;而 Set 是使用的哈希表的数据结构,即底层实现是一个哈希表,使用哈希函数来确定元素的索引位置,以实现高效的插入、删除和查找操作。
  • List 提供了多种访问和修改元素的方法,例如通过下标访问、添加、删除、替换元素等方法;而 Set 提供的操作主要是添加、删除和查找元素,不能通过索引进行操作。
  • Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
    List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变


Set接口


24. 说一下 HashSet 的实现原理?


        HashSet 是基于 HashMap 实现的,HashSet的值存放于HashMap的key上,HashMap的value统一为一个空的对象(private static final Object PRESENT = new Object();),因此 HashSet 的实现比较简单,相关 HashSet 的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet 不允许重复的值。


25. 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 )。

以下是HashSet 部分源码:
   

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


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

  • 如果两个对象相等,则hashcode一定也是相同的
  • hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值
  • 两个对象相等,对两个equals方法返回true
  • 两个对象有相同的hashcode值,它们也不一定是相等的
  • 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
  • hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。

==与equals的区别

  • ==是判断两个变量或实例是不是指向同一个内存空间 equals是判断两个变量或实例所指向的内存空间的值是不是相同
  • ==是指对内存地址进行比较 equals()是对字符串的内容进行比较

ps:所有当比较基本数据类型时,==和equals效果相同,因为基本数据类型时直接定位的,内存地址就是内容。


26. HashSet与HashMap的区别

HashMapHashSet
存储键值对仅存储对象
调用put()向map中添加元素调用add()方法向Set中添加元素
HashMap使用键(Key)计算HashcodeHashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap使用键(Key)计算HashcodeHashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false


三、Map接口


27. 什么是Hash算法

        哈希算法也称为散列算法,是将任意长度的消息压缩到某一固定长度的消息摘要(digest)的过程。哈希函数将一段任意长度的消息转换成一段固定长度的输出,该输出通常称为哈希值、哈希码或摘要。哈希函数可以将消息作为输入,并生成固定长度的哈希值,这个哈希值可以用于数据加密、数据完整性验证、数字签名等各种数据处理领域。

        哈希函数具有以下特点:

  1. 哈希函数通常需要满足输入域比输出域大得多,这样可以避免不同的输入产生相同的哈希值(即哈希冲突)。

  2. 采用哈希算法计算哈希值的数据被修改任意一位,其哈希值就会发生改变,这个特点保证了数据完整性。

  3. 分布式计算中,采用哈希算法可以轻松地实现负载均衡和故障转移等功能。

        哈希算法有多种不同的实现方式,例如MD5、SHA-1、SHA-256、GOST等算法。在实际应用中需要根据安全性、速度和长度等因素来选择不同的算法。

ps:

        在Java中的HashMap中,键值对的插入和查找都是基于哈希值进行的。哈希值是通过一个哈希函数计算得到的,在HashMap中,哈希函数是通过对key调用hashCode()方法计算得到的。hashCode()方法继承自Object类,其默认实现是根据对象的地址计算哈希值。因此,如果只是使用默认的哈希函数,很可能会出现哈希冲突,导致HashMap的性能下降,甚至失去了哈希表的优势。

        为了减少哈希冲突的发生,Java的HashMap采用了扰动函数(又称为扰动算法)。扰动函数可以将哈希值进行再次计算,将原始哈希值的高低位进行运算混合,使得即使输入的哈希值有很多重复,经过扰动函数之后得到的哈希值仍然具有较高的随机性,从而降低了哈希冲突的发生概率。HashMap中的扰动函数通过对原始哈希值进行右移、异或等位运算实现,以此生成最终的哈希值。

        综上所述,扰动函数是HashMap中哈希函数的一部分,通过对原始哈希值进行位运算混合,可以降低哈希冲突的概率,提高HashMap的性能。


28. 什么是hash冲突?

        哈希冲突(Hash Collision)是指两个或多个不同的输入数据在添加到哈希表中时,经过哈希函数计算后得到相同的哈希值

29.如何解决hash冲突?

        在 JDK1.7 和 JDK1.8 中,都采用了一些方法来减少哈希冲突,这里我们来逐一了解一下:

JDK 1.7 中的哈希冲突处理办法:

1.哈希码扰动处理

        在 JDK 1.7 中,哈希码是通过进行 9 次扰动处理之后(1.8两次),再乘以固定的质数生成的哈希值。这种扰动的处理可以将哈希码的分布更加均匀,从而避免哈希冲突的发生。但相比于hashCode返回的int类型,我们HashMap初始的容量大小DEFAULT_INITIAL_CAPACITY = 1 << 4 (即2的四次方16)要远小于int类型的范围,所以我们如果只是单纯的用hashCode取余来获取对应的bucket这将会大大增加哈希碰撞的概率,并且最坏情况下还会将HashMap变成一个单链表,所以我们还需要对hashCode作一定的优化

2.开放地址法

        JDK 1.7 中的散列表使用了一种开放地址法的方式来处理哈希冲突。在散列表中,如果发生了哈希冲突,就会使用单独的桶位来存储这些冲突的值。在存储数据的过程中,如果发现当前桶位已经被占用,就会寻找散列表中下一个可用的桶位来存储这个值。不过这种方式容易造成“聚集”等问题,导致查找性能和哈希表的时间复杂度变得非常低。

3.初始容量和负载因子

        在 JDK 1.7 中,散列表的初始容量和负载因子都可以通过构造方法进行设置,可以通过设置合适的值来减少哈希冲突,从而提高查找效率。但是,由于 JDK 1.7 中的数据结构设计存在缺陷,这些参数的设置可能会导致散列表的性能下降。

JDK 1.8 中的哈希冲突处理办法

1.随机哈希算法

        在 JDK 1.8 中,引入了一种随机哈希算法来替代传统的哈希算法。这种算法随机地选取了一个种子值,并使用一种局部散列的方法来进行扰动,最后得到哈希值。这种算法使得散列表的哈希值分布更加随机、更加均匀,从而降低了哈希冲突的发生概率。

2.桶位中使用链表或红黑树

        在 JDK 1.8 中,如果散列表中的元素数量达到了某个阈值,就会将存储在该桶内的元素组织成链表或者红黑树来进行存储。如果散列表中的元素数量比较小,桶内元素会使用链表的方式进行存储;如果数量比较大,就会使用红黑树的方式来进行存储。通过使用链表和红黑树,可以优化查找效率和性能。

3.动态调整容量

        在 JDK 1.8 中,散列表的容量可以动态调整,当散列表中元素总数达到阈值时,就会自动进行扩容和缩容。当散列表的元素总数超过负载因子与当前散列表容量的积时,就会进行扩容;当元素总数小于负载因子与散列表容量的积时,就会进行缩容。在动态调整容量时,会重新计算哈希值并将元素重新分配到桶位上,从而减少哈希冲突的发生。

总结

  • 简单总结一下HashMap主要是使用了哪些方法来有效解决哈希冲突的:
    • 链表法就是将相同hash值的对象组织成一个链表放在hash值对应的槽位;
    • 开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个可以使用
      的槽位。


30. 说一下HashMap的实现原理?


        HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

        HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

        HashMap 基于 Hash 算法实现的

当我们往HashMap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标存储时,如果出现hash值相同的key,此时有两种情况。
(1)如果key相同,则覆盖原始值;
(2)如果key不同(出现冲突),则将当前的key-value放入链表中
        获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。
        需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

31. HashMap在JDK1.7和JDK1.8中有哪些不同?HashMap的底层实现


在Java中,保存数据有两种比较简单的数据结构:数组和链表。

  • 数组的特点是:寻址容易,插入和删除困难;
  • 链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。

HashMap JDK1.8之前

JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组
中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。

在这里插入图片描述

HashMap JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8
时,将链表转化为红黑树,以减少搜索时间。

在这里插入图片描述

JDK1.7 VS JDK1.8 比较

不同JDK 1.7JDK 1.8
存储结构数组 + 链表数组 + 链表 + 红黑树
初始化方式单独函数: inflateTable()直接集成到了扩容函数 resize() 中
hash值计算方式扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
存放数据的规则无冲突时,存放数组;冲突时,存放链表无冲突时,存放数组;冲突 & 链表长度 <8:存放单链表;冲突 & 链表长度 > 8:树化并存放红黑树
扩容后存储位置的计算方式全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1))按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量)


32. 什么是红黑树

        红黑树(Red-Black Tree)是一种自平衡二叉查找树,可以保证树的高度不超过 2log(n+1),其中 n 表示红黑树中的节点个数,因此红黑树的插入、查找和删除的时间复杂度都稳定在 O(logn) 级别。红黑树之所以自平衡,是通过在插入、删除操作时进行节点左旋、右旋和改变颜色等方式来维持红黑树的五个性质,这些性质包括:

  1. 每个节点要么是红色,要么是黑色
  2. 根节点是黑色的
  3. 如果一个节点是红色的,则其子节点必须为黑色的
  4. 从任意一个节点到其每个叶子节点的所有路径都包含相同数目的黑色节点
  5. 新插入的节点为红色

        旋转操作是红黑树中的一种结构调整操作,主要包括左旋和右旋两种。以左旋为例,左旋操作可以将一个节点的右子节点提升为该节点的父节点,同时将该节点作为其右子节点的左子节点,以保持红黑树的平衡,而右旋则是将一个节点的左子节点提升为该节点的父节点,同时将该节点作为其左子节点的右子节点。

        而通过变色操作,则可以实现节点颜色的改变。在红黑树中,节点的颜色只能是红色或黑色,并且插入操作时需要将节点设置为红色。当出现颜色不符合规定的情况时,可以通过颜色的改变来进行调整。例如,一次插入操作可能导致某个节点同时存在两个红色节点,这时就需要将该节点的父节点和叔节点变成黑色,祖父节点变成红色,然后以祖父节点为支点进行左旋或右旋操作。

        总之,旋转和变色是红黑树中保持平衡的重要操作,通过这些操作可以调整树的结构,使得红黑树保持自平衡并满足红黑树的五个性质。

33. HashMap的put方法的具体流程?

        当我们put的时候,首先计算 key 的 hash 值,这里调用了 hash 方法, hash 方法实际是让key.hashCode() 与 key.hashCode()>>>16 进行异或操作,高16bit补0,一个数和0异或不变,所以 hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。按照函数注释,因为bucket数组大小是2的幂,计算下标 index = (table.length - 1) & hash ,如果不做 hash 处理,相当于散列生效的只有几个低 bit 位,为了减少散列的碰撞,设计者综合考虑了速度、作用、质量之后,使用高16bit和低16bit异或来简单处理减少碰撞,而且JDK8中用了复杂度 O(logn)的树结构来提升碰撞下的性能。

putVal方法执行流程图:

在这里插入图片描述

流程描述:

  1. 判断键值对数组table是否为空或为null,否则执行resize()进行扩容;
  2. 根据键值key计算hash值得到插入的数组索引i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③;
  3. 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
  4. 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
  5. 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
  6. 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。


34. HashMap的扩容操作是怎么实现的?

        在 JDK1.8 中,HashMap的扩容操作相较于 JDK1.7 在实现上有了优化。

        当 HashMap 中的元素个数超过了负载因子(默认为 0.75)与当前桶大小的乘积时,触发扩容操作。具体来说,扩容操作的步骤如下:

  1. 判断是否需要进行 resize 操作,如果不需要,则直接返回。
    • resize 操作可以分为两种情况:创建一个新数组,或将链表转为红黑树。
    • 由于在 resize 操作中创建新数组的时机和增加新键值对的时机不一致,因为 resize 操作的触发只与键值对数量有关,因此 resize 操作可能会在没有新键值对加入的情况下触发。
  2. 根据原数组长度计算新数组长度,新数组的长度始终为原数组长度的两倍。
  3. 将原数组中的每个元素都重新计算 hash 值,并映射到新数组的对应位置上(因为扩容后,每个位置都多一个位置,根据 hash 值模新数组长度得到的下标要么不变,要么加上数组长度,即 (n - 1) & hash 改为 (n - 1) & (hash + n),其中 n 为新数组长度)
  4. 在 put 操作时,当桶中元素个数超过 8 个并且当前桶数组容量不小于 64 时会将链表转为红黑树,提高查找效率。在进行扩容操作时,如果发现原数组中某个桶中的元素类型为链表,则将链表中的节点转为红黑树节点。
  5. 将新数组作为底层数组,更新 HashMap 的相关属性。

        需要注意的是,在 JDK1.8 中,当链表中的元素个数超过 8 个时,会将链表转成红黑树,目的是为了优化查找效率。而在扩容过程中,如果原数组中某个桶中有多个节点,它们的顺序仍然会发生变化。这是因为扩容后新数组的容量是原数组容量的两倍,相当于每个位置都多了一位,依然与上面所述的方法相同。

        另外,JDK1.8 中,HashMap 的数组变量不再是直接存放链表的表头,而是存放一个Node类型的对象。如果该下标处没有元素,是一个空节点(null值),否则这个节点中存储着该链表的表头。这个表头节点使用与之前的链表节点相同的Node类型对象,只不过它的实例变量中存放的是链表的第一个元素以及它们的映射信息。由于数组中保存的是节点,因此可以更方便的操作链表或红黑树。


35. 能否使用任何类作为 Map 的 key?


可以使用任何类作为 Map 的 key,然而在使用之前,需要考虑以下几点:

  1. * 如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
  2. * 类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
  3. * 如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
  4. * 用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

36. 为什么HashMap中String、Integer这样的包装类适合作为Key?

  • 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  • 内部已重写了 equals() 、 hashCode() 等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue的过程),不容易出现Hash值计算错误的情况;

ps:

在Java中,字符串(String)和整数类型的包装类(如Integer、Long等)被称为不可变类(immutable class)。所谓不可变类,是指一旦对象创建之后,就不能再改变该对象的内容,即该对象的状态不可被修改。

对于String类来说,当一个字符串被创建后,它的内容就不允许被修改,任何对字符串的操作都会返回一个新的String对象。这是因为String类的内部实现采用了字符数组(char[])来存储字符串内容,而字符数组是不可变的。而对于整数类型的包装类,每个对象都封装了一个固定的数值,当需要改变该数值时,必须重新创建一个新的对象。这也保证了包装类的内容不会被修改。

不可变类的设计具有以下优点:

  1. 线程安全:不可变类的对象是线程安全的,因为它们无法被修改,不需要同步控制。

  2. 缓存优化:不可变类的对象可以被缓存以提高性能,因为它们的内容不会发生改变。

  3. 易于设计和维护:不可变类的代码更容易编写、调试和维护,因为不需要考虑对象状态的变化。

因此,Java中的String、Integer等包装类默认是不可变类。同时,这些类的不可变特性也使得它们适合作为哈希表中的键,保证了键的唯一性。

37. 如果使用Object作为HashMap的Key,应该怎么办呢?


答:重写 hashCode() 和 equals() 方法

  • 重写 hashCode() 是因为需要计算存储数据的存储位置,需要注意不要试图从散列码计算中排除掉一个对象的关键部分来提高性能,这样虽然能更快但可能会导致更多的Hash碰撞;
  • 重写 equals() 方法,需要遵守自反性、对称性、传递性、一致性以及对于任何非null的引用值x,x.equals(null)必须返回false的这几个特性,目的是为了保证key在哈希表中的唯一性;


38. HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?


答: hashCode() 方法返回的是int整数类型,其范围为-(2 ^ 31)~(2 ^ 31 - 1),约有40亿个映射空
间,而HashMap的容量范围是在16(初始化默认值)~2 ^ 30,HashMap通常情况下是取不到最
大值的,并且设备上也难以提供这么多的存储空间,从而导致通过 hashCode() 计算出的哈希值可
能不在数组大小范围内,进而无法匹配存储位置;
那怎么解决呢?
        HashMap自己实现了自己的 hash() 方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
        在保证数组长度为2的幂次方的时候,使用 hash() 运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,三来解决了“哈希值与数组大小范围不匹配”的问题;


39. HashMap 的长度为什么是2的幂次方

                在 Java 中,HashMap 的容量总是 2 的幂次方,即 2,4,8,16,32..... 等等。这是因为 HashMap 的 Hash 算法是基于取模运算,使用容量为 2 的幂次方可以保证在进行取模时,只要是质数,就可以得到较为均匀的散列值。而且,采用 2 的幂次方作为容量,也能方便地进行位运算,提高 HashMap 的查找效率。

         在保证数组长度为2的幂次方的时候,使用 hash() 运算之后的值与运算(&)(数组长度 - 1)来获取数组下标的方式进行存储,这样一来是比取余操作更加有效率,二来也是因为只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length

        另外,HashMap 的大小可以动态调整,当 HashMap 中的键值对数量超过了容量与负载因子(Load Factor)的乘积,即当表达式 size > capacity * loadFactor 为 true 时,就会触发扩容操作。在扩容时,HashMap 的容量将会扩大为原来的两倍。

        因此,HashMap 容量为 2 的幂次方的这个限制其实并不会影响 HashMap 的功能和性能。而且,这个约束条件的存在还有助于简化 HashMap 的实现和优化运行效率。

ps:

HashMap 的大小(Size)是指 Hash 表中键值对(Entry)的数量,而 HashMap 的容量(Capacity)则是指 Hash 表的大小(即该 HashMap 所能容纳的最大数量)。


40. HashMap 与 HashTable 有什么区别?

  1. 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap );
  2. 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用 ConcurrentHashMap );
  3. 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException。
  4. 初始容量大小和每次扩充容量大小的不同 :创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。
  6. 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代。


41. 什么是TreeMap 

  • TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
  • TreeMap基于红黑树(Red-Black tree)实现。该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
  • TreeMap是线程非线程安全的。


42. 如何决定使用 HashMap 还是 TreeMap?


对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。


43. HashMap 和 ConcurrentHashMap 的区别

  • ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护,相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好,而HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
  • HashMap的键值对允许有null,但是ConCurrentHashMap都不允许。


44. ConcurrentHashMap 和 Hashtable 的区别?


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 的数据结构,但是已经简化了属性,只是为了兼容旧版本;
② Hashtable(同一把锁) :使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。


两者的对比图:
1、HashTable:

在这里插入图片描述

2、 JDK1.7的ConcurrentHashMap:

在这里插入图片描述

3、JDK1.8的ConcurrentHashMap(TreeBin: 红黑二叉树节点 Node: 链表节点):

在这里插入图片描述

答:ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同
步,HashTable 考虑了同步的问题使用了synchronized 关键字,所以 HashTable 在每次同步执行
时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。


45. ConcurrentHashMap 底层具体实现知道吗?实现原理是什么?


JDK1.7

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段
数据时,其他段的数据也能被其他线程访问。
在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:
一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一
种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构
的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修
改时,必须首先获得对应的 Segment的锁。

该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当
锁的角色;
Segment 是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元
素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁。

在这里插入图片描述
JDK1.8

ConcurrentHashMap 的底层实现采用了与HashMap类似的思想,在哈希表桶中,依然存储的是键值对,但链表的结构发生了一些改变。JDK1.8 为了提高并发的效率,使用了 CAS 和 synchronized 实现了多个线程之间的并发访问,因此不再需要像 JDK1.7 中那样使用“分段锁”进行保护。同时,为了减少锁的竞争,JDK1.8 将哈希表桶的内部结构调整为数组 +链表 + 红黑树的结构,根据链表中元素的数量来决定是否将其转换为红黑树,在插入、查找等操作时,均按照此结构进行访问。需要注意的是,在 JDK1.8 中,ConcurrentHashMap 的主要数据结构是Node类,而不再使用Segment类。

在这里插入图片描述

总的来说,在 JDK1.7 中,ConcurrentHashMap 的并发访问性能比在 JDK1.8 中要差一些,但是在 JDK1.7 之后升级到 JDK1.8 时,ConcurrentHashMap 的主要优化点不在于多线程并发问题的 Segment 分割,而是在于哈希桶内部结构的优化。


四、辅助工具类


46. Array 和 ArrayList 有何区别?


Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList有。
对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。


47. comparable 和 comparator的区别?


ComparableComparator都是Java中用于排序的接口,二者的实现都可以用于对集合或数组进行排序。

区别如下:

  1. Comparable是在类的定义时就实现的接口,它提供了compareTo方法,使得自身可以比较大小。而Comparator是一个单独的比较器类,它实现了compare方法,可以用于比较多个类的实例。

  2. 如果一个对象需要自己的排序规则,则它应该实现Comparable接口,这样可以保证它的实例可以进行自然排序。如果需要用不同的排序规则对同一类型的对象进行排序,则可以实现一个Comparator接口来进行排序。

  3. 如果类已经实现了Comparable接口,那么可以利用Collections.sort()Arrays.sort()方法进行排序,这些方法将自动使用对象自身的排序规则。而如果没有实现Comparable接口,则必须通过自定义的Comparator类来进行排序,或者实现Comparable接口。

  4. 通常而言,Comparable适用于“内部比较”,即在对象内部实现比较规则;而Comparator适用于“外部比较”,即在排序时提供一种比较规则。也就是说,Comparable是使得一个类对于另一个类具有可比性,而Comparator则是针对某一属性或规则进行比较。

  • 下面是一个使用ComparableComparator的例子,来展示两者的区别:

  • public class Person implements Comparable<Person> {
        private String name;
        private int age;
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        // 实现Comparable接口的compareTo方法
        @Override
        public int compareTo(Person otherPerson) {
            // 按照年龄排序
            return this.age - otherPerson.age;
        }
    
        // 使用Comparator实现按照名字排序
        public static Comparator<Person> NameComparator = new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.name.compareTo(p2.name);
            }
        };
    
        public static void main(String[] args) {
            // 使用Comparable自然排序
            List<Person> personList = new ArrayList<>();
            personList.add(new Person("Tom", 20));
            personList.add(new Person("Jerry", 18));
            personList.add(new Person("Mike", 22));
            Collections.sort(personList);
            System.out.println(personList); // [Person [name=Jerry, age=18], Person [name=Tom, age=20], Person [name=Mike, age=22]]
    
            // 使用Comparator按照名字排序
            Collections.sort(personList, Person.NameComparator);
            System.out.println(personList); // [Person [name=Jerry, age=18], Person [name=Mike, age=22], Person [name=Tom, age=20]]
        }
    
        // 重写toString方法,方便输出查看
        @Override
        public String toString() {
            return "Person [name=" + name + ", age=" + age + "]";
        }
    }

总之,Comparable是对象内部定义的排序规则,Comparator是外部定义的排序规则。如果对象已经有了比较规则,则使用Comparable更加简便;如果需要多种排序规则,则使用Comparator更加灵活。


48. Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
  • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。


49. TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

TreeMapTreeSet在排序时,元素的比较取决于它们的排序键。在使用默认构造方法创建TreeMapTreeSet时,元素默认会按照自然排序(即实现了Comparable 接口,并重写了compareTo方法)进行排序。如果没有实现Comparable接口,则需要创建TreeMap 或 TreeSet时指定一个自定义的Comparator对象作为参数。

Collections工具类中的sort()方法对集合的排序方式跟TreeMapTreeSet类似,即默认使用元素的自然排序。同时,Collections工具类也提供了一个sort()方法,允许用户指定一个自定义的比较器对象。这个自定义的比较器可以是一个实现了Comparator接口的类,它会重写compare方法,用于实现自定义的比较规则。在这个方法中,我们可以指定任何我们想要的比较方式,不受元素本身是否实现Comparable接口的限制。

下面是一个示例,演示了如何使用默认排序和自定义排序对一个List集合进行排序:

public class SortExample {
    public static void main(String[] args) {
        // 使用默认排序
        List<Integer> list1 = new ArrayList<>();
        list1.add(5);
        list1.add(1);
        list1.add(3);
        list1.add(4);
        list1.add(2);

        System.out.println("Before Sorting: " + list1);
        Collections.sort(list1);
        System.out.println("After Sorting: " + list1);

        // 使用自定义的排序
        List<Person> list2 = new ArrayList<>();
        list2.add(new Person("Tom", 20));
        list2.add(new Person("Jerry", 18));
        list2.add(new Person("Mike", 22));
        list2.add(new Person("Andy", 19));
        list2.add(new Person("Alice", 21));

        Comparator<Person> comparator = new Comparator<Person>() {
            @Override
            public int compare(Person p1, Person p2) {
                return p1.getName().compareTo(p2.getName());
            }
        };

        System.out.println("Before Sorting: " + list2);
        Collections.sort(list2, comparator);
        System.out.println("After Sorting: " + list2);
    }
}

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person [name=" + name + ", age=" + age + "]";
    }
}

 在上面的示例中,我们使用了Collections工具类对一个List<Integer>和一个List<Person>进行了排序。在第一个集合中,我们使用默认排序对List进行排序,因为Integer实现了Comparable接口。在第二个集合中,我们自定义了一个Comparator对象,对Person对象按照名字进行排序。可以看到,无论是默认排序还是自定义排序,都可以通过sort()方法实现。

————————————————
版权声明:本文为CSDN博主「超级码里喵」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_30999361/article/details/124503952

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值