Java(五)

29 阅读5分钟

集合框架是业务代码的「容器」;Spring 里 List/DTO、Map 缓存、Set 去重无处不在。


1. 集合体系概览

概念

Collection
├── List   有序、可重复
├── Set    不重复
└── Queue  队列

Map      键值对,非 Collection 子接口

常用实现:ArrayList、LinkedList、HashSet、HashMap。

原理

接口定义契约,实现类决定时间复杂度与迭代顺序。编程时面向接口声明:List<String> list = new ArrayList<>();

完整示例

package com.example.demo;

import java.util.*;

public class CollectionOverview {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        Set<String> set = new HashSet<>();
        Map<String, Integer> map = new HashMap<>();

        list.add("a"); list.add("a");
        set.add("a"); set.add("a");
        map.put("a", 1); map.put("b", 2);

        System.out.println(list.size()); // 2
        System.out.println(set.size());  // 1
        System.out.println(map.get("a"));
    }
}

常见坑

  • 仍用原始类型 List list — 失去泛型检查(见泛型节)。
  • 在 foreach 里直接 list.remove() — 用 Iterator.remove() 或 removeIf。

2. List:ArrayList vs LinkedList

概念

  • ArrayList:动态数组,随机访问 O(1),中间插入删除 O(n)。
  • LinkedList:双向链表,头尾操作方便,随机访问 O(n)。

原理

绝大多数业务读多写少、按下标访问 → 默认 ArrayList。队列/Deque 场景可用 ArrayDeque(往往优于 LinkedList)。

完整示例

package com.example.demo;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListDemo {
    public static void main(String[] args) {
        List<Integer> nums = new ArrayList<>();
        nums.add(10);
        nums.add(20);
        nums.set(0, 15);
        System.out.println(nums.get(1));

        List<Integer> dequeAsList = new LinkedList<>();
        ((LinkedList<Integer>) dequeAsList).addFirst(1);
        ((LinkedList<Integer>) dequeAsList).addLast(2);
    }
}

练习

合并两个已排序 List<Integer> 为一个排序列表(可先分别排序再双指针 merge)。

完整例子

package com.example.demo;

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

public class MergeSortedLists {
    static List<Integer> merge(List<Integer> a, List<Integer> b) {
        List<Integer> out = new ArrayList<>();
        int i = 0, j = 0;
        while (i < a.size() && j < b.size()) {
            if (a.get(i) <= b.get(j)) out.add(a.get(i++));
            else out.add(b.get(j++));
        }
        while (i < a.size()) out.add(a.get(i++));
        while (j < b.size()) out.add(b.get(j++));
        return out;
    }

    public static void main(String[] args) {
        System.out.println(merge(List.of(1, 3, 5), List.of(2, 4, 6)));
    }
}

常见坑

  • Arrays.asList(arr) 返回固定大小列表,不能 add/remove。
  • List.of(...)(9+)不可变,不能 add。

3. Set:HashSet 基础

概念

HashSet:基于 HashMap,元素唯一,无序(迭代顺序不稳定)。
LinkedHashSet:插入顺序。TreeSet:排序,元素需 Comparable 或传入 Comparator。

原理

依赖元素的 hashCode 与 equals:相等对象必须相同 hash(不等 hash 也可能碰撞,靠 equals 链)。

完整示例

package com.example.demo;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class SetDemo {
    static class Tag {
        final String name;
        Tag(String name) { this.name = name; }
        @Override public boolean equals(Object o) {
            if (!(o instanceof Tag t)) return false;
            return Objects.equals(name, t.name);
        }
        @Override public int hashCode() { return Objects.hash(name); }
    }

    public static void main(String[] args) {
        Set<Tag> tags = new HashSet<>();
        tags.add(new Tag("java"));
        tags.add(new Tag("java"));
        System.out.println(tags.size()); // 1
    }
}

练习

去重:List<String> 去重保序 — 用 LinkedHashSet 或循环 + Set 判断。

完整例子

package com.example.demo;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

public class DedupList {
    static List<String> dedup(List<String> input) {
        return new ArrayList<>(new LinkedHashSet<>(input));
    }

    public static void main(String[] args) {
        System.out.println(dedup(List.of("a", "b", "a", "c", "b"))); // [a, b, c]
    }
}

常见坑

  • 未重写 equals/hashCode 的自定义类放进 HashSet 会「重复」插入。
  • 修改已放入 Set 的、参与 hash 的字段 — 破坏集合不变性,禁止。

4. Map:HashMap 基础

概念

键值对;键唯一。HashMap 平均 O(1) get/put(JDK 8+ 链表+红黑树)。

原理

键的 hashCode → 桶下标;冲突链/树;equals 判同键。
null 键:HashMap 允许一个 null key(ConcurrentHashMap 不允许)。

完整示例

package com.example.demo;

import java.util.HashMap;
import java.util.Map;

public class MapDemo {
    public static void main(String[] args) {
        Map<Long, String> users = new HashMap<>();
        users.put(1L, "alice");
        users.put(2L, "bob");

        if (users.containsKey(1L)) {
            System.out.println(users.get(1L));
        }

        for (Map.Entry<Long, String> e : users.entrySet()) {
            System.out.println(e.getKey() + " -> " + e.getValue());
        }

        users.putIfAbsent(3L, "carol");
        users.merge(1L, " (vip)", (oldV, append) -> oldV + append);
    }
}

练习

词频统计:字符串 "java is fun and java is popular" 拆词,Map<String,Integer> 计数。
Map cache:Map<Long, User> 模拟按 id 查用户,命中打 log、未命中「加载」再放入。

完整例子

package com.example.demo.exercise;

import java.util.HashMap;
import java.util.Map;

public class MapExercises {
    record User(Long id, String name) {}

    static Map<String, Integer> wordCount(String sentence) {
        Map<String, Integer> map = new HashMap<>();
        for (String w : sentence.toLowerCase().split("\\s+")) {
            map.merge(w, 1, Integer::sum);
        }
        return map;
    }

    static final Map<Long, User> cache = new HashMap<>();

    static User findById(Long id) {
        User hit = cache.get(id);
        if (hit != null) {
            System.out.println("cache hit");
            return hit;
        }
        System.out.println("load from db");
        User loaded = new User(id, "user-" + id);
        cache.put(id, loaded);
        return loaded;
    }

    public static void main(String[] args) {
        System.out.println(wordCount("java is fun and java is popular"));
        findById(1L);
        findById(1L); // 第二次应 hit
    }
}

常见坑

  • map.get(key) 不存在返回 null,与「值为 null」混淆 — 用 getOrDefault 或 containsKey。
  • 多线程共享 HashMap 需 ConcurrentHashMap,否则数据错乱。

5. equals 与 hashCode 契约

概念

若 a.equals(b),则 a.hashCode() == b.hashCode()。
重写 equals 必须重写 hashCode(IDE 可生成)。

原理

HashMap/HashSet 先用 hash 定位,再用 equals 精确比较。违反契约会导致「丢数据」「查不到」。

完整示例

package com.example.demo;

import java.util.Objects;

public class User {
    private final Long id;
    private String name;

    public User(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof User user)) return false;
        return Objects.equals(id, user.id);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

练习

故意只写 equals 不写 hashCode,把两个 equal 的 User 放入 HashSet,观察 size。

完整例子

package com.example.demo;

import java.util.HashSet;
import java.util.Objects;
import java.util.Set;

public class BrokenHashCodeDemo {
    static class User {
        final Long id;
        User(Long id) { this.id = id; }
        @Override public boolean equals(Object o) {
            return o instanceof User u && Objects.equals(id, u.id);
        }
        // 故意不重写 hashCode → 默认用对象身份
    }

    public static void main(String[] args) {
        Set<User> set = new HashSet<>();
        set.add(new User(1L));
        set.add(new User(1L));
        System.out.println(set.size()); // 常为 2(equals 为 true 但 hash 不同)
    }
}

常见坑

  • 用可变字段参与 equals/hashCode(如 name 会变)导致 Set/Map 行为诡异 — 常用不可变 id。
  • 继承体系下 equals 对称性破坏 — 考虑 getClass() 比较或组合模式。

6. 泛型入门(Generics)

概念

编译期类型参数:List<String> 只能放 String;避免强转与部分 ClassCastException。

原理

类型擦除:运行时泛型信息大多被擦掉,List<String> 与 List<Integer> 运行时都是 List。
因此不能 new T()、不能 instanceof List<String>(可 instanceof List<?>)。

完整示例

package com.example.demo;

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

public class GenericsDemo {
    static <T> T first(List<T> list) {
        return list.isEmpty() ? null : list.get(0);
    }

    static void printAll(List<?> list) {  // 通配符:只读场景
        for (Object o : list) {
            System.out.println(o);
        }
    }

    public static void main(String[] args) {
        List<String> names = new ArrayList<>();
        names.add("alice");
        String s = first(names);

        List<Integer> nums = List.of(1, 2, 3);
        printAll(nums);
    }
}

练习

写泛型方法 swap(List<T> list, int i, int j)。

完整例子

package com.example.demo;

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

public class GenericSwap {
    static <T> void swap(List<T> list, int i, int j) {
        T tmp = list.get(i);
        list.set(i, list.get(j));
        list.set(j, tmp);
    }

    public static void main(String[] args) {
        List<String> names = new ArrayList<>(List.of("a", "b", "c"));
        swap(names, 0, 2);
        System.out.println(names); // [c, b, a]
    }
}

常见坑

  • 原始类型与泛型混用产生 heap pollution 警告。
  • List<Object> 不能接收 List<String>(不变性)— 用 List<? extends Number> 等通配符(PECS 原则,进阶再学)。

7. 不可变集合(Java 9+)

概念

List.of()、Set.of()、Map.of() 创建不可变集合,适合常量配置。

完整示例

package com.example.demo;

import java.util.List;
import java.util.Map;

public class ImmutableCollections {
    static final List<String> ROLES = List.of("ADMIN", "USER");
    static final Map<String, Integer> ERROR_CODES = Map.of(
            "NOT_FOUND", 404,
            "BAD_REQUEST", 400
    );
}

常见坑

  • List.of(a, b, c) 不允许 null 元素。
  • 超过 10 个键值对用 Map.ofEntries。

本章小结

结构选用
默认列表ArrayList
去重HashSet / LinkedHashSet
键值HashMap
自定义键必写 equals + hashCode