Skip to content

集合设计原则:框架背后的设计哲学

接口与实现分离

Java 集合框架最核心的原则:用接口声明变量,用实现类创建对象

java
// ✅ 面向接口编程,灵活切换实现
List<String> list = new ArrayList<>();
// 哪天要改成 LinkedList?只需改这一行
List<String> list = new LinkedList<>();

// ❌ 直接用实现类声明,失去了灵活性
ArrayList<String> list = new ArrayList<>();

这个原则的威力在于:当需求变化时,你只需要改一行代码。

记住这个原则

变量类型用接口(ListSetMap),创建对象用具体实现(ArrayListHashSetHashMap)。

为什么 HashMap 比 Hashtable 更推荐

java
// ❌ 遗留代码风格
Hashtable<String, Integer> old = new Hashtable<>();

// ✅ 现代代码风格
Map<String, Integer> modern = new HashMap<>();

两者功能几乎一样,但 HashMap 变量类型是 Map 接口,可以随时替换成 TreeMap(需要排序)或 ConcurrentHashMap(需要并发),而 Hashtable 变量类型是具体类,改起来要大改。


泛型:编译期的安全网

没有泛型的时代是什么样子?

java
// JDK 1.4:无泛型,到处都要强制类型转换
List list = new ArrayList();
list.add("hello");
String s = (String) list.get(0); // 需要强制转换

// JDK 5+:泛型,类型安全
List<String> list = new ArrayList<>();
list.add("hello");
String s = list.get(0); // 无需转换,编译时就检查

泛型的设计哲学:错误在编译期发现,而不是在运行时。一个 ClassCastException 可能让你的程序在凌晨 3 点崩溃,而泛型让这些错误在 javac 阶段就被揪出来。

java
// 编译错误,而不是运行时崩溃
List<String> list = new ArrayList<>();
list.add(123); // 编译不通过:类型不兼容

可选操作:接口的边界

有些操作在某些实现中不可行,Java 用「可选操作」模式来处理这个问题:

java
public interface List<E> {
    // 所有实现都要支持的方法
    int size();
    boolean isEmpty();

    // 可选操作(UnsupportedOperationException)
    default void sort(Comparator<? super E> c) {
        throw new UnsupportedOperationException();
    }
}

Arrays.asList() 返回的列表不支持增删操作:

java
List<String> fixed = Arrays.asList("a", "b", "c");
fixed.add("d"); // UnsupportedOperationException
fixed.remove(0); // UnsupportedOperationException
fixed.set(0, "x"); // 这个可以!

这就是「固定大小列表」和「可变列表」的区别——Arrays.asList() 返回的底层就是原数组,不是真正独立的列表。


视图与快照

集合框架中有两种数据提供方式:视图(view)和快照(snapshot)。

java
Map<String, Integer> map = new HashMap<>();
map.put("a", 1);
map.put("b", 2);

// 视图:底层共享,修改会影响原 Map
Set<String> keys = map.keySet();
keys.remove("a"); // map 里 "a" 也被删了!
System.out.println(map); // {b=2}

// 快照:独立副本
Set<String> snapshot = new HashSet<>(map.keySet());
snapshot.remove("b"); // 不影响 map
System.out.println(map); // {b=2}

keySet()values()entrySet() 返回的都是视图,不是副本。

面试常问的坑

不要在遍历 Map 的同时修改它(ConcurrentModificationException),但可以通过迭代器的 remove() 来安全删除:

java
Iterator<String> it = map.keySet().iterator();
while (it.hasNext()) {
    String key = it.next();
    if (key.equals("b")) {
        it.remove(); // ✅ 安全删除
    }
}

算法复用:Collections 工具类

CollectionsArrays 提供了一套通用的算法,让所有集合都能复用:

java
import java.util.*;

public class AlgorithmReuseDemo {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);

        // 排序
        Collections.sort(numbers);
        System.out.println("排序后: " + numbers);

        // 反转
        Collections.reverse(numbers);
        System.out.println("反转后: " + numbers);

        // 洗牌
        Collections.shuffle(numbers);
        System.out.println("洗牌后: " + numbers);

        // 二分查找(必须先排序)
        int index = Collections.binarySearch(numbers, 5);
        System.out.println("5 的位置: " + index);

        // 不可变视图
        List<Integer> unmodifiable = Collections.unmodifiableList(numbers);
        // unmodifiable.add(10); // UnsupportedOperationException
    }
}

JDK 8+ 的 Stream 也能复用

java
import java.util.*;
import java.util.stream.*;

public class StreamAlgorithmDemo {

    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 1, 4, 1, 5, 9, 2, 6);

        // 过滤 + 映射 + 收集
        List<Integer> evens = numbers.stream()
            .filter(n -> n % 2 == 0)
            .sorted()
            .collect(Collectors.toList());
        System.out.println("偶数排序: " + evens);

        // 统计
        IntSummaryStatistics stats = numbers.stream()
            .mapToInt(Integer::intValue)
            .summaryStatistics();
        System.out.println("统计: " + stats.getMax() + ", " + stats.getMin() + ", " + stats.getAverage());
    }
}

线程安全的演进

Java 集合的线程安全经历了三次重大演进:

java
// 1. 遗留 synchronized(低效,全局锁)
Map<String, Integer> v1 = new Hashtable<>();
Map<String, Integer> v2 = Collections.synchronizedMap(new HashMap<>());

// 2. 并发包(高效,粒度锁 / CAS)
Map<String, Integer> v3 = new ConcurrentHashMap<>();

// 3. 读多写少(Copy-On-Write)
List<String> v4 = new CopyOnWriteArrayList<>();
Set<String> v5 = new CopyOnWriteArraySet<>();

性能对比

java
import java.util.*;
import java.util.concurrent.*;

public class ConcurrencyPerformanceDemo {

    public static void main(String[] args) throws InterruptedException {
        int threads = 100;
        int ops = 100000;

        // synchronizedMap:全局锁
        Map<Integer, Integer> syncMap = Collections.synchronizedMap(new HashMap<>());
        long syncTime = benchmark(syncMap, threads, ops);

        // ConcurrentHashMap:分段锁/CAS
        Map<Integer, Integer> concurrentMap = new ConcurrentHashMap<>();
        long concurrentTime = benchmark(concurrentMap, threads, ops);

        System.out.println("synchronizedMap: " + syncTime + " ms");
        System.out.println("ConcurrentHashMap: " + concurrentTime + " ms");
        System.out.println("提升: " + (double) syncTime / concurrentTime + "x");
    }

    static long benchmark(Map<Integer, Integer> map, int threads, int ops)
            throws InterruptedException {
        Thread[] ts = new Thread[threads];
        long start = System.nanoTime();
        for (int i = 0; i < threads; i++) {
            final int base = i * ops;
            ts[i] = new Thread(() -> {
                for (int j = 0; j < ops; j++) {
                    map.put(base + j, j);
                }
            });
            ts[i].start();
        }
        for (Thread t : ts) t.join();
        return (System.nanoTime() - start) / 1_000_000;
    }
}

典型结果:ConcurrentHashMap 比 synchronizedMap 快 5-10 倍。


相关链接

基于 VitePress 构建