这是一个新的工程。我们先创建一个 `User` 类,添加两个属性:`name` 和 `age`,加上构造函数,并重写 `toString` 方法。这是一个非常简单的 POJO 类。
在 `main` 函数中,我们创建一个 `List<User>`,使用 `new ArrayList<>()` 实例化。然后创建两个用户:`tom`(年龄 11)和 `jerry`(年龄也设为 11),将它们添加到列表中,最后用增强 for 循环遍历并打印。
代码如下:
public class Main {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
User tom = new User("tom", 11);
User jerry = new User("jerry", 11);
userList.add(tom);
userList.add(jerry);
for (User user : userList) {
System.out.println(user);
}
}
}
这段代码运行正常。但现在我们提出第一个问题:**为什么我们可以把 `userList` 放在增强 for 循环中,而不能把单个 `User` 对象放进去?**
比如我们写:
for (Object i : tom) { ... } // 编译错误
这会报错。为什么?我们平时可能没想过这个问题。
接着看第二个问题:我们现在有一个需求——如果 `userList` 中有用户的年龄是 11,就向列表中再添加一个新用户 `"张三"`。代码如下:
for (User user : userList) {
if (user.getAge() == 11) {
userList.add(new User("zhangsan", 3));
}
}
for(User user :userList){
System.out.println(user);
}
但运行后抛出了 `ConcurrentModificationException` 异常。逻辑看似合理,为什么会出错?
如果我们换一种写法:
public class Main {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
User tom = new User("tom", 11);
User jerry = new User("jerry", 11);
userList.add(tom);
userList.add(jerry);
long count = userList.stream().filter(user -> user.getAge() == 11).count();
for (long i = 0; i < count; i++) {
userList.add(new User("zhangsan", 3));
}
for (User user : userList) {
System.out.println(user);
}
}
}
这次却成功了。为什么同样是“遍历 + 修改集合”,一个报错,一个不报错?
带着这两个问题,我们进入今天的设计模式实战——**迭代器模式**。
---
我们先解决第一个问题:为什么 `List` 可以用于增强 for 循环,而 `User` 不行?
从面向对象的角度思考:既然有些类可以被遍历,有些不能,那是否可以抽象出一个“可被遍历”的能力?Java 中确实有这样的接口——`Iterable<T>`。
只要一个类实现了 `Iterable<T>` 接口,就可以放在增强 for 循环中。`List` 继承自 `Collection`,而 `Collection` 又继承自 `Iterable`,所以 `ArrayList` 等集合天然支持增强 for 循环。
我们可以通过反编译字节码来验证这一点。当我们写:
List <User> userList =new ArrayList<>();
for (User user : userList) {
System.out.println(user);
}
编译器实际将其翻译为:
Iterator<User> iterator = userList.iterator();
while (iterator.hasNext()) {
User user = iterator.next();
System.out.println(user);
}
也就是说,增强 for 循环的本质是:**调用对象的 `iterator()` 方法获取迭代器,然后用 `hasNext()` 和 `next()` 遍历元素**。
public class Main {
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
for (User user : userList) {
System.out.println(user);
// a b c
}
Iterator<User> iterator = userList.iterator();
while (iterator.hasNext()) {
User user = iterator.next();
System.out.println(user);
// a b c
}
}
}
那么,只要我们的 `User` 类实现 `Iterable<String>` 接口,理论上也可以被遍历。这样解决空指针问题了,迭代器究极迭代什么呢?Iterable和Iterator(里面有hasNext和next和remove和默认实现default)
我们尝试让 `User` 实现 `Iterable<String>`:
public class User implements Iterable<String> {
private String name;
private int age;
@Override
public Iterator<String> iterator() {
return new UserIte();
}
public User(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
class UserLite implements Iterable<String> {
@Override
public boolean hasNext(){
return false;
}
@Override
public String next(){
return "";
}
}
user里面有两个属性,只要我们把两个属性走完了。因此我们设立一个count=2
class UserLite implements Iterator<String> {
int count = 2;
@Override
public boolean hasNext() {
return count > 0;
}
@Override
public String next() {
count--;
if (count == 1) {
return User.this.name;
}
if (count == 0) {
return User.this.age + "";
}
throw new NoSuchElementException();
}
}
现在我们main里面创建一个 `User` 对象:
User zhangsan = new User("张三", 25);
for (String s : zhangsan) {
System.out.println(s);
}
运行成功!输出 `"张三"` 和 `"25"`。说明我们通过实现 `Iterable` 和 `Iterator`,让一个普通类也具备了可迭代的能力。
---
接下来解决第二个问题:**为什么边遍历边修改集合会抛 `ConcurrentModificationException`?**
我们再次查看字节码或源码。`ArrayList` 的 `iterator()` 方法返回一个内部类 `Itr`,它维护了一个 `cursor`(游标)和一个 `expectedModCount`。
`expectedModCount` 是在创建迭代器时,将当前 `modCount`(修改次数)复制过来的。每次调用 `next()` 时,都会检查当前 `modCount` 是否等于 `expectedModCount`。如果不等,说明集合在遍历过程中被外部修改了,就会抛出 `ConcurrentModificationException`。
这也是为什么我们在遍历时调用 `list.add()` 会失败。
而当我们使用 `stream().count()` 时,是在遍历之前就完成了统计,没有在迭代过程中修改集合,所以不会触发异常。
如果我们真的想在遍历中修改,可以在 `add` 后立即 `break`,确保不再调用 `next()`:
public class Main {
public static void main(String[] args) {
List<User> list = new ArrayList<>();
list.add(new User("tom", 11));
list.add(new User("jerry", 12));
Iterator<User> iterator = list.iterator();
while (iterator.hasNext()) {
User user = iterator.next();
if (user.getAge() == 12) {
list.add(new User("zhangsan", 3));
break;
}
}
for (User user : list) {
System.out.println(user);
}
}
}
这样就不会抛异常,因为 `next()` 不会再被调用,`checkForComodification` 也不会执行。
---
现在我们进入实战:假设有一个文件 `demo.user`,内容如下:
[alice,10]
[boy,16]
[candy,15]
[oiioi,3]
[tom,2]
[jemmy,1]
每行表示一个用户,格式为 `[name,age]`。我们需要解析这个文件,生成多个 `User` 对象进行处理。
常规做法是:
public class Main {
public static void main(String[] args) throws IOException {
List<User> userList = new ArrayList<>();
readFiles((User user) -> {
System.out.println(user);
userList.add(user);
});
}
private static void readFiles(Consumer<User> userConsumer) throws IOException {
List<String> lines = Files.readAllLines(new File("demo.user").toPath());
for (String line : lines) {
String midString = line.substring(1, line.length() - 1);
String[] split = midString.split(",");
String name = split[0];
int age = Integer.parseInt(split[1]);
User user = new User(name, age);
userConsumer.accept(user);
}
}
}
但如果文件很大,一次性读入内存可能导致 OOM。而且如果我们要多次处理这些用户,每次都要重新读文件。
我们可以用**迭代器模式**重构:让文件本身成为一个可迭代对象。
我们创建一个类 `UserFile`,实现 `Iterable<User>`:
public class UserFile implements Iterable<User> {
private final File file;
@Contracture(true)
public UserFile(File file) {
this.file = file;
}
@Override
public Iterator<User> iterator() {
return null;
}
}
class UserFileIterator implements Iterator<User> {
List<User> userList = loadUsersFromFile();
private List<User> loadUsersFromFile() {
try {
return Files.readAllLines(file.toPath()).stream().map(String line -> {
String midString = line.substring(1, line.length() - 1);
String[] split = midString.split(",");
return new User(split[0], Integer.parseInt(split[1]));
}).collect(Collectors.toList());
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public boolean hasNext() {
return cursor!=userList.size();
}
@Override
public User next() {
if(cursor>=userList.size()){
throw new NoSuchElementException();
}
int currentIndex = cursor;
cursor++;
return userList.get(currentIndex);
}
}
```
使用方式:
```java
UserFile userFile = new UserFile(Paths.get("demo.user"));
for (User user : userFile) {
System.out.println(user);
}
```
这样我们就实现了“文件即集合”的抽象,用户无需关心数据来源。
---
最后留一个思考题:
如果文件非常大,上述实现仍然会将所有数据加载到内存中。你能否修改 `UserFileIterator`,使其**分批读取**,比如每次只读一行,用完即弃,从而节省内存?提示:不要一次性 `Files.lines().collect()`,而是在 `hasNext()` 和 `next()` 中动态读取。