Set 数据类型

除了以前学的 List,Java 中还支持Set

Set 不支持索引,但是你可以确定某个元素是否在 set 中

1
2
3
4
Set<String> s = new HashSet<>();
s.add("Tokyo");
s.add("Lagos");
System.out.println(s.contains("Tokyo"));

Exceptions

如果你的代码有哪些操作是不能做的,你可以选择 throw 一个 exception,这会在报错信息中显示,例如:

1
2
3
4
public void add(T x) {
if (x == null) {
throw new IllegalArgumentException("can't add null");
}

让数据类型支持 for 循环迭代 & Iterators

在课堂上我们创建了一个 ArraySet,但目前无法使用 for 循环对其进行遍历,因此需要引入 iterator 这一机制。

要使一个类支持 for 循环,需要满足以下几点:

  1. 在该类中实现 iterator() 方法,并让该方法返回 Iterator<T> 类型
  2. 实现一个 Iterator<T> 类型的类,该类必须实现 hasNext()next() 方法
  3. 该类需要 implements Iterable<T>

考虑以下完整的可迭代 ArraySet 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.util.Iterator;  

public class ArraySet<T> implements Iterable<T> {
private T[] items;
private int size;

public ArraySet() {
items = (T[]) new Object[100];
size = 0;
}

public boolean contains(T x) {
for (int i = 0; i < size; i += 1) {
if (items[i].equals(x)) {
return true;
}
}
return false;
}

public void add(T x) {
if (x == null) {
throw new IllegalArgumentException("can't add null");
}
if (contains(x)) {
return;
}
items[size] = x;
size += 1;
}

public Iterator<T> iterator() {
return new ArraySetIterator();
}

private class ArraySetIterator implements Iterator<T> {
private int wizPos;

public ArraySetIterator() {
wizPos = 0;
}

public boolean hasNext() {
return wizPos < size;
}

public T next() {
T returnItem = items[wizPos];
wizPos += 1;
return returnItem;
}
}

public static void main(String[] args) {
ArraySet<Integer> aset = new ArraySet<>();
aset.add(5);
aset.add(23);
aset.add(42);

for (int i : aset) {
System.out.println(i);
}
}

这里的 ArraySetIterator 可以理解为一个“巫师”,用于顺序访问 ArraySet 中的元素:

  • 初始位置 wizPos = 0
  • 调用 hasNext() 用于判断是否还有元素
  • 调用 next() 时:
    • 返回当前位置的元素
    • 然后将指针移动到下一个位置

Pasted image 20260404141300

上图展示了迭代的内部过程。本质上,for-each 循环只是对 Iterator 的一种语法封装。

如果 ArraySet<T>implements Iterable<T>,则无法使用 for 循环,只能手动编写类似上图中的迭代代码。这种支持被称为 iterable


Iterator 和 Iterable 的区别

  • Iterator 是执行遍历的游标对象
  • Iterable 则是能够生成一个 Iterator、从而实现对自身内容进行遍历的对象。
    • 例如:一个本身不能被遍历的对象,通过生成一个 iterator 就可以实现遍历

打印数据类型里的内容 & toString

当调用 System.out.println(x) 时,Java 会自动调用 x.toString() 方法。

ArrayList 这样的类都已经重写了 toString(),因此可以直接 println()。而我们之前实现的 ArraySet 如果直接使用 println(),会打印对象的内存地址(类似 ArraySet@1a2b3c)。因此需要在类中重写该方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  @Override
public String toString() {
StringBuilder returnSB = new StringBuilder("{");
if (size == 0) {
return "{}";
}
for (int i = 0; i < size - 1; i += 1) {
returnSB.append(items[i].toString());
returnSB.append(", ");
}
returnSB.append(items[size - 1]);
returnSB.append("}");
return returnSB.toString();
}

public static void main(String[] args) {
ArraySet<Integer> aset = new ArraySet<>();
/* 省略内容 */
System.out.println(aset);

StringBuilder 类用于创建和操作可变字符串。与 String 不同,StringBuilder 可以在原对象上进行修改,而不会产生新的对象。

这里使用 StringBuilder 而不是 String 的原因是 String 每次拼接都会创建新对象,时间复杂度高

因此常见模式是:用 StringBuilder 构建,最后调用 toString() 转换为 String


判断数据类型里的内容是否相等 & equals

==.equals() 在 Java 中的行为完全不同:

  • == 比较的是两个变量是否指向同一个对象(内存地址)
    • 即使两个 Array 内容完全相同,只要不是同一个对象,也会返回 false
  • .equals(Object o) 是定义在 Object 类中的方法,很多类会重写它,让他比较内容是否相等(逻辑相等)

在这里引入 instanceof 这样一个新语法:

1
2
3
4
5
6
7
@Override  
public boolean equals(Object o) {
if (o instanceof Dog uddaDog) { // 假设前面定义了 Dog 类
return this.size == uddaDog.size;
}
return false;
}
1
o instanceof Dog uddaDog

上面这行代码实际上做了三件事:

  1. 判断 o动态类型是否是 Dog(或其子类)
  2. 如果是,则自动完成类型转换,并将其赋值给静态类型为 DoguddaDog 变量
  3. 如果 o 是 null,则 instanceof 会直接返回 false,不会报错

额外:手动实现 instanceof 的功能

这部分最新版课堂没交,却在 Project1 中的 equals 方法中用到了,我们要手动实现 instanceof 的功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
public boolean equals(Object other) {  
/* ... */
// 假设前面定义了一个 Deque 类(或者接口)
Deque<?> o;

try {
o = (Deque<?>) other;
} catch (ClassCastException e) {
return false;
}

/* ... 省略部分和上面一样的判断逻辑 ... */
}