Containers and Generic⚓︎
约 2187 个字 254 行代码 预计阅读时间 14 分钟
Array⚓︎
- 数组是一种特殊类型对象,能够存储固定数量的元素
- 
数组应当通过 new在堆上动态分配或聚合初始化(aggregate initialization) 被创建
- 
数组内所有元素类型相同 
- 
数组是一个对象 
- 
所有数组都有一个内部成员属性 length,表示数组大小
- 
合法下标值(valid subscript value) - 编译器会检查是否用的是合法下标值,无论用于右值还是左值
- 一旦程序运行过程中存在非法下标,就可能会出现数据或代码遭破坏,或程序中止 (abort) 的情况
- 所以 Java 有责任确保程序在运行时只使用合法下标值
- 关键概念:通过边界检查确保索引用于指代范围内的数组元素
 
- 
for-each 语法: 
- 
数组变量 - 是指向数组对象的指针
- 所以 Java 允许数组间的赋值
 
- 
对象数组 - 数组可以将对象的引用(references to objects)(句柄 (handle))作为元素存储
- 
注意:实例化一个对象数组仅预留了存储引用的空间,所以必须要单独实例化每个元素中要存储的对象,比如以下声明: 
- 
对象数组中的 for-each class Value { int i; } Value[] a = new Value[5]; for ( int i = 0; i < a.length; i++ ) a[i] = i; for ( var x: a ) x.i++; for ( var x: a ) System.out.println(x.i);- var可自动推断类型(和 C++ 的- auto类似)
 
 
Collections⚓︎
容器对象(collection object) 是指能存储任意数量的其他对象的对象。常用的容器对象包括:
- List:以特定顺序存储元素
- Set:不允许有重复元素
- Map:一组“键 - 值”对象对
容器类是泛型类(generic class)。
要定义一个容器类的变量,我们需要指定两种类型:
- 集合本身的类型(此处为 ArrayList)
- 存储在集合中的元素类型(此处为 String)
由于 Java 有类型推断功能,所以无需在 new 中重复表述元素类型:
import java.util.ArrayList;
public class Notebook {
    private ArrayList<String> notes = new ArrayList();
}
相应的对象结构如下:
Iteration⚓︎
在容器对象上迭代:
- 
索引 
- 
for-each 
- 
迭代器(iterator) public void listNotes() { Iterator<String> it = notes.iterator(); while (it.hasNext()) { System.out.println(it.next()); } }- 迭代器是一种对象,它提供了遍历容器对象中所有元素的功能
- 此时客户端程序员无需了解或关心该被迭代序列的底层结构
- 迭代器的工作流程:- 通过调用名为 iterator()的方法,向容器请求一个迭代器;这个迭代器将在首次调用其next()方法时,准备好返回序列中的第一个元素
- 使用 next()获取序列中的下一个对象
- 要想检查序列中是否还有更多对象,使用 hasNext()方法
- 利用 remove()移除迭代器最后返回的元素
 
- 通过调用名为 
 
Implementation⚓︎
容器实现:实现集合接口的类通常以 <Implementation-style><Interface> 的形式命名。下表总结了常见容器的实现:
| Interface | Hash Table | Resizable Array | Balanced Tree | Linked List | Hash Table + Linked List | 
|---|---|---|---|---|---|
| Set | HashSet | TreeSet | LinkedHashSet | ||
| List | ArrayList | LinkedList | |||
| Deque | ArrayDeque | LinkedList | |||
| Map | HashMap | TreeMap | LinkedHashMap | 
Utilities⚓︎
Collections 类的静态成员:
- min(Collection),- max(Collection)
- reverse()
- copy(List dest, List src)
- fill(List list, Object o)
Lists⚓︎
常用的 List 容器有:
- List(接口- ) :以特定顺序维护元素
- ArrayList:使用数组实现,插入和删除偏慢
- LinkedList:插入和删除成本低,但随机访问慢
和其他相近容器的比较
Collection
├─ List <- 有序、可重复、带索引
│ ├─ ArrayList
│ ├─ LinkedList <- 同时也实现了 Deque
│ └─ Vector
│     └─ Stack <- Vector 的子类(古老、同步、官方不推荐)
├─Queue <- 一端进、一端出,典型 FIFO
│  ├─ Deque <- 双端队列,两头都能进出
│  │   ├─ ArrayDeque <- 数组实现,非同步,最快
│  │   └─ LinkedList <- 链表实现,也能当 List 用
│  │
│  ├─ PriorityQueue <- 按优先级出队,不是 FIFO
│  └─ ConcurrentLinkedQueue...(并发包里的实现)
ArrayList⚓︎
官方文档:https://docs.oracle.com/javase/8/docs/api/java/util/ArrayList.html
下面罗列 ArrayList 常用的 API:
- 
添加 
- 
删除 
- 
修改 
- 
查找 
- 
信息 
- 
容量 
- 
生成 
- 
迭代 
- 
流 
ArrayList v.s. Vector
对于新代码,推荐使用
ArrayList或CopyOnWriteArrayList/Collections.synchronizedList,而非Vector。
实际选型:
- 单线程 -> ArrayList
- 多线程且读远多于写 -> CopyOnWriteArrayList
- 多线程读写均衡 -> Collections.synchronizedList(new ArrayList<>())
- 老代码 / 需要与遗留 API 对接 -> 才继续用 Vector
Sets⚓︎
常用的 Set 容器有:
- Set(接口- ) :加入- Set的所有元素都是唯一的
- HashSet:对象必须定义- hashCode()
- TreeSet:由树结构支持的有序集合
例子
如何选择合适的 Sets?
- 日常去重:HashSet
- 去重 + 插入顺序:LinkedHashSet
- 去重 + 排序 / 区间: TreeSet(单线程)或ConcurrentSkipListSet(并发)
- 并发去重:首选 ConcurrentHashMap.newKeySet()(JDK8 + 工厂方法) ,或ConcurrentSkipListSet
- 读多写少且集合小:CopyOnWriteArraySet
- enum专用:- EnumSet.allOf(MyEnum.class)
如果要将用户自定义对象放入 Set 中,需注意:
- 
必须 - 
正确覆写 equals(),包括以下约定(Object规范节选) :- 自反:x.equals(x)必须true
- 对称:x.equals(y)<=>y.equals(x)
- 传递:x.equals(y) && y.equals(z)=>x.equals(z)
- 一致:多次调用结果不变
- 与 null比较必须返回false
 
- 自反:
- 
一致地覆写 hashCode()- 规则:相等对象必须返回相同哈希值,不相等对象尽量分散
- 一旦对象进入 HashSet,不要修改参与hash/equals的字段,否则“内存泄漏”式丢失对象
 
 
- 
- 
可选(若想让 TreeSet/ConcurrentSkipListSet能排序,以下两者二选一即可)- 
实现 Comparable<T>
- 
提供外部 Comparator
 
- 
完整示例
public final class MyKey implements Comparable<MyKey> {
    private final String name; // 参与 hash/equals/compare
    private final int age;
    public MyKey(String name, int age) {
        this.name = name;
        this.age = age;
    }
    @Override
    public boolean equals(Object o) { /* 见上 */ }
    @Override
    public int hashCode() { /* 见上 */ }
    @Override
    public int compareTo(MyKey o) { /* 见上 */ }
    }
Maps⚓︎
常用的 Map 容器有(和 Set 类似
- Map(接口)
- HashMap
- TreeMap
例子
HashMap <String, String> phoneBook = new HashMap<String, String>();
phoneBook.put("Charles Nguyen", "(531) 9392 4587");
phoneBook.put("Lisa Jones", "(402) 4536 4674");
phoneBook.put("William H. Smith", "(998) 5488 0123");
String phoneNumber = phoneBook.get("Lisa Jones");
System.out.println(phoneNumber);
哈希 (hashing) 和哈希码 (hash code):
- 键需要有 hashCode()方法
- 要想将用户自定义类作为 HashMap的键,必须覆写equals()和hashCode()
选择 Map 类
| 实现类 | 底层结构 | Key 可为 null | 有序性 | 备注 | 
|---|---|---|---|---|
| HashMap | 数组 + 链表 + 红黑树(JDK8+) | 允许一个 | 无顺序 | 默认首选,O(1) | 
| LinkedHashMap | 继承 HashMap,再串一条双向链表 | 允许 | 插入顺序或访问顺序(LRU) | 适合做缓存 | 
| TreeMap | 红黑树 | 不可 | 键自然顺序或自定义 Comparator | O(log n),支持区间查询 | 
| 实现类 | 同步方式 | Key 可为 null | 并发度 | 备注 | 
|---|---|---|---|---|
| Hashtable | 全表锁 | 不可 | 低 | 上古类,别用 | 
| Collections.synchronizedMap(new HashMap<>()) | 包装后全表锁 | 不可 | 低 | 简单兼容旧代码 | 
| ConcurrentHashMap | 分段锁(JDK7)→桶级锁 / 无锁(JDK8+) | 不可 | 高 | 读写并行,默认首选 | 
| 实现类 | 特点 | 
|---|---|
| WeakHashMap | 键是弱引用,GC 自动清理,适合做“键生命周期随对象”的缓存 | 
| IdentityHashMap | 用 ==比较键,而不是equals,用于图算法、对象拓扑复制 | 
| EnumMap | 键必须是同一 enum类型,内部用数组,比HashMap快且内存省 | 
tldr 版:
- 单线程 / 常规:默认 HashMap
- 需要插入 / 访问顺序:LinkedHashMap
- 需要排序 / 区间: TreeMap(单线程)或ConcurrentSkipListMap(并发)
- 并发:首选 ConcurrentHashMap;别再new Hashtable()
Generic⚓︎
关于泛型(generic):
- 泛型提供了一种向编译器传递容器类型信息的方式,以便进行类型检查
- 除了枚举、匿名内部类和 Exception类之外,所有类型都可以拥有泛型参数
泛型类(generic class) 必须指定两种类型:集合本身的类型(此处为 ArrayListString
将类型作为参数:
- T是占位符,生成对象时传具体类型
- 实际上 Java 常用占位符为 E而非T(E代表 element)
多类型参数:类型参数数量不限制,按位置对应
接口可能也会用到泛型类型:
interface Codec<T> {
    T decode(String s);
}
enum IntCodec implements Codec<Integer> {
    INSTANCE;
    public Integer decode(String s) {
        return Integer.valueOf(s);
    }
}
定义一个简单的泛型类:
public interface List<E> {
    void add(E x);
    Iterator<E> iterator();
}
public interface Iterator<E> {
    E next();
    boolean hasNext();
}
泛型类里的静态方法:
- 方法前写 <T>,编译器帮你推
public static <T> List<T> asList(T... a) {
    return Arrays.asList(a);
}
// 类型推断
List<String> names = asList("A", "B");
例子
public final class Box<T> {
    private T value;
    public Box(T value) { this.value = value; }
    public T get() { return value; }
    public void set(T value){ this.value = value;}
    // 静态泛型工厂,方便创建
    public static <E> Box<E> of(E value) { return new Box<>(value); }
    public static void main(String[] args) {
        Box<String> sBox = Box.of("Hello");
        System.out.println(sBox.get()); // Hello
        Box<Integer> iBox = new Box<>(42);
        iBox.set(iBox.get() + 1);
        System.out.println(iBox.get()); // 43
    }
}
Java 的泛型方式:
- 代码没有多个副本
- 泛型类型声明一次性编译,并转换为一个单独的类文件
- 泛型声明有形式类型参数
- 为形式类型参数使用简洁(如果可能的话,单字符)且富有启发性的名称
泛型与子类型 (subtyping):如果 Foo 是 Bar 的一个子类型(子类或子接口G 是某种泛型类型声明,那么 G<Foo> 并不是 G<Bar> 的子类型。
List<String> ls = new ArrayList<String>();
List<Object> lo = ls;
lo.add(new Object());
String s = ls.get(0);
通配符(wildcard):
void printCollection(Collection<?> c) {
    for (Object e : c) {
        System.out.println(e);
    }
}
Collection<?> c = new ArrayList<String>();
c.add(new Object());
例子
以下代码实现 Shape 类及其子类:
public abstract class Shape {
    public abstract void draw(Canvas c);
}
public class Circle extends Shape {
    private int x, y, radius;
    public void draw(Canvas c) { ... }
}
public class Rectangle extends Shape {
    private int x, y, width, height;
    public void draw(Canvas c) { ... }
}
// To draw them on a canvas
public void draw(Shape s) {
    s.draw(this);
}
public void drawAll(List<Shape> shapes) {
    for (Shape s: shapes) {
        s.draw(this);
    }
}
// Bounded wildcards
public void drawAll(List<? extends Shape> shapes) { ...}
double sum(List<? extends Number> nums) { /* 只读 */ }
void fill(List<? super Integer> ints) { /* 只写 */ }
extends v.s. super
extends 只能 " 读 ",super 只能 " 写”(相对变量而言)
| 界限 | 英文含义 | 能放什么 | 能拿什么 | 典型场景 | 
|---|---|---|---|---|
| ? extends Integer | 上界通配符 | 什么也不能放(除 null) | 只能读出 Integer或其子类 | 消费数据,如求和 | 
| ? super Integer | 下界通配符 | 可以写 Integer及其子类 | 只能拿出 Object(丧失具体类型) | 生产数据,如填充 | 
运行时擦除:
- 泛型只存在于编译期
- 编译后擦除到 Object或通配上限
例子
对于:
OpenJDK 24 的 javap -p -v ( 或 -c -v )为了可读性 ,把擦除后的Object / Number又显示成原泛型符号 T,但 Signature 属性 Object 和字节码指令里仍然是 Object。
泛型不能做的事:
- new T():擦除后无构造
- T.class:无具体类型信息
- List<int>:基本类型不能用
评论区




