Skip to content

集合排序与不可变集合 ​

学习目标 ​

  • 用 Comparator.comparing、thenComparing 和 reversed 组合排序规则。
  • 区分原地排序、排序副本、不可变集合和只读包装。
  • 知道同步包装、并发集合和不可变集合分别解决什么问题。

核心知识点 ​

专业术语 ​

  • Comparator:定义两个对象相对顺序的比较器。
  • 稳定排序:比较结果相等时保留原有相对顺序。
  • 不可变集合:创建后不能通过任何 API 改变容器结构。
  • 只读视图:当前引用不能修改,但源集合变化仍会反映出来。
  • 并发集合:为多线程访问提供特定的线程安全语义,不等于业务事务。

白话解释与边界 ​

list.sort 和 Collections.sort 都是原地排序;如果不想改原列表,先复制再排序。List.of、Set.of、Map.of 和 copyOf 是不可修改结果,Collections.unmodifiableXxx 是只读包装视图。并发集合解决多个线程对容器的协调访问,不会让跨多个容器或多个步骤的业务逻辑自动原子化。

常用用法 ​

Comparator.comparing:按对象字段排序 ​

comparingInt、comparingLong、comparingDouble 避免手动拆箱比较;字符串等引用字段可使用 Comparator.comparing。

java
// 语义:comparingInt、comparingLong、comparingDouble 避免手动拆箱比较。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record User(String name, int age) {}

public class ComparingDemo {
    public static void main(String[] args) {
        List<User> users = new ArrayList<>(List.of(
            new User("Bob", 30), new User("Alice", 20)));
            // 两个 User 元素分别为 Bob/30 和 Alice/20;外层 List 按该顺序初始化。
        users.sort(Comparator.comparingInt(User::age));
        // 关键变化:users.sort(Comparator.comparingInt(User::age));;按 Comparator.comparingInt(User::age) 重排 users,返回 void;集合顺序更新。
        System.out.println(users);
        // 输出:[User[name=Alice, age=20], User[name=Bob, age=30]]
    }
}

thenComparing:增加次级排序 ​

先比较主键,主键相等时再比较次键;先调用 reversed 再 thenComparing 可让主键降序而次键保持自然顺序。

java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

record Score(String name, int value) {}

public class ThenComparingDemo {
    public static void main(String[] args) {
        List<Score> scores = new ArrayList<>(List.of(
            new Score("Bob", 90), new Score("Alice", 90)));
            // 两个 Score 元素分别为 Bob/90 和 Alice/90;外层 List 按该顺序初始化。
        scores.sort(Comparator.comparingInt(Score::value)
            .reversed()
            .thenComparing(Score::name));
            // 关键变化:scores.sort(Comparator.comparingInt(Score::value) .reversed() .thenComparing(Score::name));;按 Comparator.comparingInt(Score::value) .reversed() .thenComparing(Score::name) 重排 scores,返回 void;集合顺序更新。
            // 输入:.reversed() .thenComparing(Score::name));;当前 Comparator 先调用无参 reversed,再调用 thenComparing(Score::name)。
            // 关键变化:.reversed() .thenComparing(Score::name));;先将按 value 的 Comparator 反向,再追加 Score::name 次级比较;返回组合 Comparator。
            // 关键变化:.thenComparing(Score::name));;为当前 Comparator 增加 Score::name 次级比较;返回组合 Comparator。
        System.out.println(scores);
        // 输出:[Score[name=Alice, value=90], Score[name=Bob, value=90]]
    }
}

Comparator.nullsFirst:把空值排在前面 ​

nullsFirst 用包装比较器明确把 null 放在所有非空值之前。

java
// 语义:nullsFirst 用包装比较器明确把 null 放在所有非空值之前。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class NullsFirstDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("b", null, "a"));
        list.sort(Comparator.nullsFirst(Comparator.naturalOrder()));
        // 关键变化:list.sort(Comparator.nullsFirst(Comparator.naturalOrder()));;按 Comparator.nullsFirst(Comparator.naturalOrder()) 重排 list,返回 void;集合顺序更新。
        System.out.println(list);
        // 输出:[null, a, b]
    }
}

Comparator.nullsLast:把空值排在后面 ​

nullsLast 用包装比较器明确把 null 放在所有非空值之后。

java
// 语义:nullsLast 用包装比较器明确把 null 放在所有非空值之后。
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;

public class NullsLastDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("b", null, "a"));
        list.sort(Comparator.nullsLast(Comparator.naturalOrder()));
        // 关键变化:list.sort(Comparator.nullsLast(Comparator.naturalOrder()));;按 Comparator.nullsLast(Comparator.naturalOrder()) 重排 list,返回 void;集合顺序更新。
        System.out.println(list);
        // 输出:[a, b, null]
    }
}

reversed:反转比较器 ​

Comparator.reverseOrder 或 comparator.reversed 都可以表达降序;不要在比较器中返回 value1 - value2,整数溢出会造成错误顺序。

java
// 语义:Comparator.reverseOrder 或 comparator.reversed 都可以表达降序。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ReversedComparatorDemo {
    public static void main(String[] args) {
        List<Integer> values = new ArrayList<>(List.of(1, 3, 2));
        values.sort(Comparator.reverseOrder());
        // 关键变化:values.sort(Comparator.reverseOrder());;按 Comparator.reverseOrder() 重排 values,返回 void;集合顺序更新。
        System.out.println(values);
        // 输出:[3, 2, 1]
    }
}

list.sort:原地排序 ​

list.sort 会改变当前可变列表;List.of 返回的列表排序会抛 UnsupportedOperationException。

java
// 语义:list.sort 会改变当前可变列表。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class ListSortOverviewDemo {
    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("java", "c", "python"));
        names.sort(Comparator.comparingInt(String::length));
        // 关键变化:names.sort(Comparator.comparingInt(String::length));;按 Comparator.comparingInt(String::length) 重排 names,返回 void;集合顺序更新。
        System.out.println(names);
        // 输出:[c, java, python]
    }
}

复制后排序:保留原集合 ​

需要保留调用方列表时复制到 ArrayList;排序结果也可以用 Stream.sorted().toList() 得到不可修改结果。

java
// 语义:需要保留调用方列表时复制到 ArrayList。
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class SortedCopyDemo {
    public static void main(String[] args) {
        List<Integer> source = List.of(3, 1, 2);
        List<Integer> sorted = new ArrayList<>(source);
        sorted.sort(Comparator.naturalOrder());
        // 关键变化:sorted.sort(Comparator.naturalOrder());;按 Comparator.naturalOrder() 重排 sorted,返回 void;集合顺序更新。
        System.out.println(source + " / " + sorted);
        // 输出:[3, 1, 2] / [1, 2, 3]
    }
}

List.of:创建小型不可变列表 ​

List.of 返回拒绝 null 的不可修改列表,并保留参数顺序。

java
// 语义:List.of 返回拒绝 null 的不可修改列表,并保留参数顺序。
import java.util.List;

public class ListOfDemo {
    public static void main(String[] args) {
        List<String> list = List.of("a", "b");
        System.out.println(list);
        // 输出:[a, b]
    }
}

Set.of:创建小型不可变集合 ​

Set.of 返回拒绝 null 和重复元素的不可修改集合,遍历顺序不属于契约。

java
// 语义:Set.of 返回拒绝 null 和重复元素的不可修改集合,遍历顺序不属于契约。
import java.util.Set;

public class SetOfDemo {
    public static void main(String[] args) {
        Set<String> set = Set.of("java", "sql");
        System.out.println(set.contains("java"));
        // 输出:true
        System.out.println(set.size());
        // 输出:2
    }
}

Map.of:创建小型不可变映射 ​

Map.of 返回拒绝 null 和重复键的不可修改映射。

java
// 语义:Map.of 返回拒绝 null 和重复键的不可修改映射。
import java.util.Map;

public class MapOfDemo {
    public static void main(String[] args) {
        Map<String, Integer> map = Map.of("java", 95, "sql", 88);
        System.out.println(map.get("java"));
        // 输出:95
        System.out.println(map.size());
        // 输出:2
    }
}

List.copyOf:创建不可变列表副本 ​

List.copyOf 复制列表当前结构,源列表之后的结构变化不会反映到结果。

java
// 语义:List.copyOf 复制列表当前结构,源列表之后的结构变化不会反映到结果。
import java.util.ArrayList;
import java.util.List;

public class ListCopyOfDemo {
    public static void main(String[] args) {
        List<String> listSource = new ArrayList<>(List.of("a"));
        List<String> list = List.copyOf(listSource);
        listSource.add("b");
        System.out.println(list);
        // 输出:[a]
    }
}

Set.copyOf:创建不可变集合副本 ​

Set.copyOf 复制集合当前结构并拒绝 null,元素对象本身仍是浅引用。

java
// 语义:Set.copyOf 复制集合当前结构并拒绝 null,元素对象本身仍是浅引用。
import java.util.HashSet;
import java.util.Set;

public class SetCopyOfDemo {
    public static void main(String[] args) {
        Set<String> source = new HashSet<>(Set.of("a"));
        Set<String> snapshot = Set.copyOf(source);
        source.add("b");
        System.out.println(snapshot);
        // 输出:[a]
    }
}

Map.copyOf:创建不可变映射副本 ​

Map.copyOf 复制映射当前结构并拒绝 null,键和值对象本身仍是浅引用。

java
// 语义:Map.copyOf 复制映射当前结构并拒绝 null,键和值对象本身仍是浅引用。
import java.util.HashMap;
import java.util.Map;

public class MapCopyOfDemo {
    public static void main(String[] args) {
        Map<String, Integer> source = new HashMap<>(Map.of("a", 1));
        Map<String, Integer> snapshot = Map.copyOf(source);
        source.put("b", 2);
        System.out.println(snapshot);
        // 输出:{a=1}
    }
}

不常用但需要知道 ​

Collections.sort:旧式排序入口 ​

Collections.sort 仍兼容旧代码,现代代码通常直接使用 list.sort(comparator)。

java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class CollectionsSortDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>(List.of(3, 1, 2));
        Collections.sort(list);
        // 关键变化:Collections.sort(list);;按 list 重排 Collections,返回 void;集合顺序更新。
        System.out.println(list);
        // 输出:[1, 2, 3]
    }
}

Collections.binarySearch:有序列表二分查找 ​

列表必须按相同排序规则排好;未找到时返回负插入点编码,使用前先判断结果是否小于 0。

java
import java.util.Collections;
import java.util.List;

public class CollectionsBinarySearchDemo {
    public static void main(String[] args) {
        List<Integer> sorted = List.of(1, 3, 5, 7);
        System.out.println(Collections.binarySearch(sorted, 5));
        // 输出:2
    }
}

Collections.min:查找最小元素 ​

min 按自然顺序或指定比较器返回最小元素,空集合调用会抛 NoSuchElementException。

java
import java.util.Collections;
import java.util.List;

public class CollectionsMinDemo {
    public static void main(String[] args) {
        List<Integer> values = List.of(3, 1, 3);
        System.out.println(Collections.min(values));
        // 输出:1
    }
}

Collections.max:查找最大元素 ​

max 按自然顺序或指定比较器返回最大元素,空集合调用会抛 NoSuchElementException。

java
import java.util.Collections;
import java.util.List;

public class CollectionsMaxDemo {
    public static void main(String[] args) {
        List<Integer> values = List.of(3, 1, 3);
        System.out.println(Collections.max(values));
        // 输出:3
    }
}

Collections.frequency:统计元素出现次数 ​

frequency 按 equals 统计目标元素在集合中出现的次数。

java
import java.util.Collections;
import java.util.List;

public class CollectionsFrequencyDemo {
    public static void main(String[] args) {
        List<Integer> values = List.of(3, 1, 3);
        System.out.println(Collections.frequency(values, 3));
        // 输出:2
    }
}

Collections.unmodifiableXxx:动态只读包装 ​

只读包装阻止通过 view 修改,但仍反映 source 的后续变化;需要稳定快照时使用 copyOf。

java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class UnmodifiableCollectionDemo {
    public static void main(String[] args) {
        List<String> source = new ArrayList<>(List.of("a"));
        List<String> view = Collections.unmodifiableList(source);
        source.add("b");
        System.out.println(view);
        // 输出:[a, b]
    }
}

Collections.synchronizedCollection:同步包装边界 ​

遍历同步包装集合时仍要按文档在包装对象上加锁;多步业务操作不自动成为原子事务。

java
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;

public class SynchronizedCollectionDemo {
    public static void main(String[] args) {
        Collection<String> values =
            Collections.synchronizedCollection(new ArrayList<>());
            // 关键变化:Collections.synchronizedCollection(new ArrayList<>());;调用 synchronizedCollection,实参为 new ArrayList<>();返回对底层 Collection 访问加同步包装的 Collection。
        values.add("java");
        synchronized (values) {
            System.out.println(values.size());
            // 输出:1
        }
    }
}

ConcurrentHashMap:并发键值更新 ​

ConcurrentHashMap 适合并发键值更新,但单键原子操作不能替代跨键事务。

java
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapBoundaryDemo {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
        counts.merge("java", 1, Integer::sum);
        // 关键变化:counts.merge("java", 1, Integer::sum);;向 counts 合并 "java", 1, Integer::sum,返回合并后的值;映射内容更新。
        System.out.println(counts.get("java"));
        // 输出:1
    }
}

CopyOnWriteArrayList:读多写少的并发列表 ​

CopyOnWriteArrayList 的写入会复制底层数组,适合读多写少的监听器列表。

java
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class CopyOnWriteArrayListDemo {
    public static void main(String[] args) {
        List<String> listeners = new CopyOnWriteArrayList<>();
        listeners.add("audit");
        System.out.println(listeners);
        // 输出:[audit]
    }
}

简单案例 ​

java
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;

public class SortSummaryDemo {
    public static void main(String[] args) {
        List<Integer> values = new ArrayList<>(List.of(3, 1, 2));
        values.sort(Comparator.naturalOrder());
        // 关键变化:values.sort(Comparator.naturalOrder());;按 Comparator.naturalOrder() 重排 values,返回 void;集合顺序更新。
        System.out.println(values);
        // 输出:[1, 2, 3]
    }
}

排序要先确认是否允许修改原列表;不允许时复制后排序。

易混点 ​

  • list.sort 和 Collections.sort 都是原地排序,而不是返回新列表;排序不可变列表会失败。
  • List.of 等工厂是不可变容器,unmodifiableList 只是动态只读视图。
  • copyOf 是浅结构复制,集合元素、Map 的键和值对象仍共享引用。
  • Comparator 比较结果必须符合传递性;比较器错误会让 TreeSet 去重和排序结果都不可信。

课后小问 ​

  1. 为什么排序前常常要复制 List? 答案:sort 会原地修改列表,复制后排序可以保留调用方传入的原始顺序。 解析:如果调用方已经明确允许修改,直接 sort 更简单;API 不应偷偷改变外部拥有的列表。

  2. unmodifiableList 和 List.copyOf 的主要区别是什么? 答案:前者是源集合的动态只读视图,后者是当前结构的不可修改副本。 解析:源集合后续 add 会反映到包装视图,但不会反映到 copyOf 结果;两者都不深复制元素。

本节小结 ​

  • Comparator 组合让主键、次键、升降序和 null 规则可读地表达。
  • 原地排序与复制排序应按调用方所有权决定。
  • of/copyOf 适合不可修改结果,unmodifiableXxx 适合只读视图。
  • 同步包装和并发集合有明确边界,不自动提供跨多步业务事务。

快速回顾 ​

  • 能用 comparing、thenComparing、reversed 组合排序。
  • 能区分原地排序、不可变副本和只读视图。
  • 能判断 null、重复元素和比较器一致性风险。
  • 能说出同步包装、CopyOnWriteArrayList、ConcurrentHashMap 的适用边界。