每日Java集合面试系列(3):基础篇(List有几种排序方式、Comparable 和Comparator的区别、如何实现自定义排序、HashMap红黑树、hash、hashMap的get复杂度)

每日一句:业精于勤,荒于嬉;行成于思,毁于随

系列介绍

Java集合基础(1)
Java集合基础(2)

今天是Java集合基础的第三篇。

集合

1. Java 对 List 有几种排序方式?

Java 中对 List 排序主要有两种核心方式:

  • Collections.sort() 方法:这是传统的排序方式,适用于任何实现了 List 接口的集合。底层使用 TimSort(一种改进的归并排序),保证稳定排序(相等元素的相对顺序不变)和 O(n log n) 的时间复杂度。例如:
    List<Integer> list = Arrays.asList(3, 1, 2);
    Collections.sort(list); // 自然排序
    Collections.sort(list, Comparator.reverseOrder()); // 自定义排序
    
  • List.sort() 方法:从 Java 8 开始,List 接口提供了默认的 sort() 方法,可以直接调用,同样使用 TimSort。这种方式更面向对象和灵活:
    list.sort(Comparator.naturalOrder());
    

此外,还可以使用 Stream API 进行排序(详见第4点),但这本质上是将 List 转换为流操作,而非直接 List 排序方式。

2. Comparable 和 Comparator 的区别?

  • Comparable 接口(位于 java.lang 包)定义对象的自然排序顺序。类实现 Comparable 后必须重写 compareTo() 方法,允许对象直接比较。例如,StringInteger 等内置类都实现了 Comparable,用于默认排序。这是“内部比较器”。
  • Comparator 接口(位于 java.util 包)定义自定义排序顺序,是一个函数式接口,需实现 compare() 方法。它允许在不修改类代码的情况下定义多种排序规则。这是“外部比较器”,更灵活。例如:
    Comparator<String> byLength = (s1, s2) -> s1.length() - s2.length();
    

关键区别:

  • Comparable 用于定义默认排序,而 Comparator 用于临时或多种排序。
  • Comparable 需要修改类本身,而 Comparator 可以在类外部实现。
  • 从设计模式看,Comparable 是策略模式的一种体现,而 Comparator 更符合开放-封闭原则。

3. Collections.sort() 和 Arrays.sort() 的区别?

  • Collections.sort() 用于对 List 集合进行排序,底层使用 TimSort 算法(基于归并排序和插入排序),适用于对象集合,保证稳定排序。
  • Arrays.sort() 用于对数组进行排序,但根据数组类型不同采用不同算法:
    • 对于对象数组(如 Integer[]),使用 TimSort,保证稳定。
    • 对于基本类型数组(如 int[]),使用双轴快速排序(Dual-Pivot Quicksort),这不保证稳定,但性能更好(因为基本类型无需考虑相等对象的顺序)。
      性能考虑:Arrays.sort() 对基本类型排序更快,而 Collections.sort() 对集合操作更安全。底层实现上,Collections.sort() 最终会调用 Arrays.sort() 来处理底层数组。

4. Java 8 中如何用 Stream API 进行排序?

Java 8 的 Stream API 提供了 sorted() 方法进行排序,有两种形式:

  • 自然排序:要求元素实现 Comparable 接口。
    List<String> list = Arrays.asList("banana", "apple", "cherry");
    List<String> sortedList = list.stream().sorted().collect(Collectors.toList());
    
  • 自定义排序:使用 Comparator 参数。
    List<String> reverseSorted = list.stream().sorted(Comparator.reverseOrder()).collect(Collectors.toList());
    

对于复杂对象,可以使用 Comparator.comparing() 方法链:

List<Person> people = ...;
List<Person> sortedPeople = people.stream()
    .sorted(Comparator.comparing(Person::getName).thenComparing(Person::getAge))
    .collect(Collectors.toList());

Stream 排序是惰性的,需要终止操作(如 collect())来触发实际排序。它不会修改原集合,而是返回新集合。

5. TreeSet 和 TreeMap 的排序原理是什么?

  • TreeSetTreeMap 都是基于红黑树(Red-Black Tree)实现的,红黑树是一种自平衡的二叉搜索树(BST),通过颜色约束和旋转操作保持平衡,确保最坏情况下的操作时间复杂度为 O(log n)。
  • 排序原理
    • 元素(或键)必须实现 Comparable 接口,或者在构造时提供 Comparator,用于比较和排序。
    • 插入、删除和查找操作都按照红黑树的规则进行:比较后决定左/右子树,并通过旋转和变色维持平衡。
    • 迭代时,中序遍历会返回有序序列(升序或降序)。
      例如,TreeMapput() 方法会调用 compare()compareTo() 来定位键的位置,并调整树结构。

6. 如何实现自定义排序(如按多个字段排序)?

实现自定义排序通常使用 Comparator 接口,特别是通过方法引用和链式调用。对于多个字段排序,使用 thenComparing() 方法:

// 假设 Person 类有 name 和 age 字段
List<Person> people = Arrays.asList(
    new Person("Alice", 30),
    new Person("Bob", 25),
    new Person("Alice", 20)
);

// 按 name 升序,然后按 age 降序
people.sort(Comparator.comparing(Person::getName)
    .thenComparing(Person::getAge, Comparator.reverseOrder()));

// 或者使用 lambda
people.sort((p1, p2) -> {
    int nameCompare = p1.getName().compareTo(p2.getName());
    if (nameCompare != 0) return nameCompare;
    return Integer.compare(p2.getAge(), p1.getAge()); // 降序
});

这种方式简洁且易读,体现了 Java 8 函数式编程的优势。

HashMap

7. 为什么 JDK 1.8 引入红黑树优化链表?

在 JDK 1.8 之前,HashMap 的每个桶(bucket)只使用链表处理哈希冲突。在最坏情况下(如大量键哈希到同一桶),链表会变得很长,导致查找性能降为 O(n)。JDK 1.8 引入了红黑树优化:当桶中元素数量超过阈值(默认为 8)时,链表转换为红黑树;当元素减少到另一阈值(默认为 6)时,转换回链表。

  • 原因:红黑树保证了最坏情况下查找、插入和删除的时间复杂度为 O(log n),显著提升了性能,尤其是在哈希碰撞攻击场景下。
  • 权衡:红黑树占用更多内存,且转换需要开销,因此只在必要时触发。这体现了工程上的平衡,兼顾了平均性能和最坏性能。

8. HashMap 的 key 可以是 null 吗?value 呢?

  • KeyHashMap 允许一个 null 键。因为 HashMaphash() 方法对 null 键返回 0,所以可以存储在第 0 个桶中。
  • ValueHashMap 允许多个 null 值。因为值没有唯一性约束。
    注意:Hashtable 不允许 null 键或值,否则会抛出 NullPointerException。这是 HashMapHashtable 的一个重要区别。

9. HashMap 的 hash() 方法是如何计算的?

在 JDK 1.8 中,HashMaphash() 方法使用扰动函数来减少哈希冲突:

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
  • 步骤
    1. 计算键的 hashCode()(int 类型,32位)。
    2. 将哈希码右移 16 位(h >>> 16),得到高16位。
    3. 将原哈希码与高16位进行异或操作(^),这样混合了高低位信息,使哈希值更均匀。
  • 目的:减少哈希碰撞,因为 HashMap 使用 (n - 1) & hash 计算索引(其中 n 是桶数量),异操作后低位更随机,分布更均匀。

10. HashMap 的 get() 方法的时间复杂度是多少?

  • 平均情况:O(1)。假设哈希函数良好,键均匀分布,每个桶只有少量元素,直接通过索引访问。
  • 最坏情况:如果所有键都哈希到同一个桶中,则:
    • 在 JDK 1.8 之前(纯链表),时间复杂度为 O(n)。
    • 在 JDK 1.8 之后(链表转红黑树),时间复杂度为 O(log n)。
      因此,JDK 1.8 的优化显著改善了最坏情况性能。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值