Guava 库为 Java 开发者提供了一系列强大的迭代器增强工具,它们简化了复杂迭代模式的实现。本文将深入探讨 Guava 的 PeekingIterator、AbstractIterator 和 AbstractSequentialIterator。
1. PeekingIterator:洞察先机
标准的 Iterator 接口仅提供 hasNext() 和 next() 方法,这在某些场景下显得力不从心。当需要"预读"下一个元素以做出决策,但又不想立即消耗它时,PeekingIterator 应运而生。
核心功能:
Iterators.peekingIterator(Iterator)方法用于将现有的Iterator包装成一个PeekingIterator。- 其核心方法是
peek(),允许在不推进迭代器的情况下,查看下一个调用next()时将返回的元素。
它带来的便利:
PeekingIterator 的 peek() 方法能够支持更灵活的迭代逻辑,尤其是在需要根据当前元素和"下一个"元素关系进行判断的场景中。它允许在一次迭代过程中完成复杂的逻辑判断和数据处理,避免了传统方法中可能出现的冗余 next() 调用或额外的状态变量管理。
示例:消除连续重复元素
java
public class PeekingIteratorExample {
public static void main(String[] args) {
List<Integer> source = Arrays.asList(1, 1, 2, 3, 3, 3, 4, 5, 5);
List<Integer> result = Lists.newArrayList();
PeekingIterator<Integer> iter = Iterators.peekingIterator(source.iterator());
while (iter.hasNext()) {
Integer current = iter.next(); // 获取当前元素
result.add(current); // 将当前元素添加到结果
// 利用 peek() 提前判断,如果下一个元素与当前元素重复,则跳过。
while (iter.hasNext() && iter.peek().equals(current)) {
iter.next(); // 跳过这个重复元素
}
}
System.out.println("Original: " + source);
System.out.println("Deduplicated: " + result); // Output: [1, 2, 3, 4, 5]
}
}
在此示例中,我们仅对 source 列表进行了一次逻辑遍历。PeekingIterator 使得我们能够在处理 current 元素的同时,预判并跳过所有后续的重复项,而无需将它们存储到任何中间结构或进行后续的去重处理。这简化了去重逻辑的实现。
注意事项: 由 Iterators.peekingIterator 返回的 PeekingIterator 不支持在调用 peek() 之后执行 remove() 操作。
2. AbstractIterator:一个方法定义迭代器
当需要实现一个自定义的 Iterator,例如对数据进行过滤、转换或从复杂数据源中提取时,手动管理 hasNext() 和 next() 的内部状态会引入大量样板代码。AbstractIterator 极大地简化了这一过程。
核心功能:
- 开发者只需实现一个方法:
computeNext()。此方法负责计算并返回序列中的下一个值。 - 当迭代序列完成时,
computeNext()应返回endOfData()以标记迭代结束。
它带来的便利:
AbstractIterator 采用惰性求值(lazy evaluation)机制。它确保 computeNext() 方法仅在真正需要下一个元素(即 next() 被调用时)才会被执行。这使得迭代器能够按需生成元素,避免了在迭代器未被完全消费时进行不必要的计算。同时,Guava 负责处理 hasNext() 和 next() 之间的状态同步,简化了自定义迭代器的实现。
示例:惰性过滤 Null 值
java
public class AbstractIteratorExample {
public static Iterator<String> skipNulls(final Iterator<String> in) {
return new AbstractIterator<String>() {
@Override
protected String computeNext() {
while (in.hasNext()) {
String s = in.next();
if (s != null) {
return s; // 找到非 null 元素,返回
}
}
return endOfData(); // 内部迭代器耗尽,返回 endOfData()
}
};
}
public static void main(String[] args) {
List<String> source = Arrays.asList("hello", null, "world", null, "guava", "java");
Iterator<String> filteredIterator = skipNulls(source.iterator());
System.out.println("Filtered elements (only consuming first 3):");
int count = 0;
while (filteredIterator.hasNext() && count < 3) { // 假设我们只消费前3个元素
System.out.println(filteredIterator.next());
count++;
}
// Output:
// hello
// world
// guava
}
}
在此示例中,即使 source 列表中有更多元素,我们只消费了前 3 个非空字符串。AbstractIterator 确保了 in.next() 和 if (s != null) 这样的操作仅执行了刚好足以找到这 3 个元素所需的最小次数。这种按需计算的模式,使得迭代器只在需要时才处理数据。
限制: AbstractIterator 继承自 UnmodifiableIterator,这意味着它禁止实现 remove() 方法。如果您的迭代器必须支持 remove(),则不应继承 AbstractIterator。
java
3. AbstractSequentialIterator
对于那些下一个值很容易根据前一个值计算出来的迭代器,AbstractSequentialIterator 提供了一种表达迭代的替代方式。
核心功能:
- 开发者实现的方法是
computeNext(T previous),它接受序列中的前一个值作为参数。 - 必须在构造函数中提供一个初始值(或者如果迭代器应立即终止,则传入
null)。 computeNext()方法约定,返回null意味着迭代结束。
它带来的便利:
AbstractSequentialIterator 适用于那些下一个值可以根据前一个值计算出来的序列。它通过**按需生成(on-demand generation)**机制,仅在 next() 被调用时才计算下一个元素。这使得它非常适合处理可能非常长甚至无限的序列,因为它避免了预先计算和存储整个序列。
示例:惰性计算 2 的幂次
java
public class AbstractSequentialIteratorExample {
public static void main(String[] args) {
// 从1开始生成2的幂次方,直到达到 1 << 30
Iterator<Integer> powersOfTwo = new AbstractSequentialIterator<Integer>(1) {
@Override
protected Integer computeNext(Integer previous) {
// 如果 previous 已经达到最大值,则返回 null 结束迭代
return (previous == 1 << 30) ? null : previous * 2;
}
};
System.out.println("Powers of two (only consuming first 7):");
int count = 0;
while (powersOfTwo.hasNext() && count < 7) { // 假设我们只消费前7个元素
System.out.println(powersOfTwo.next());
count++;
}
// Output: 1, 2, 4, 8, 16, 32, 64
}
}
在此示例中,我们仅消费了序列的前 7 个元素。AbstractSequentialIterator 确保了只有这 7 次乘法操作被执行,并且在内存中只维护了当前 previous 元素的状态。这种惰性生成方式,避免了对整个序列进行预计算和存储的需要。
关键限制: 由于返回 null 标志着迭代的结束,因此 AbstractSequentialIterator 不能用于实现一个可以合法返回 null 元素的迭代器。
笔者遇到一个按需深拷贝的需求,简单描述为:已获得对象a,需要深拷贝若干份,同时需要标记序号,a已经实现deepCopy方法。实现如下:
java
// 实现1
A a = getA();
boolean first = true;
for (var x: request.getXList()) {
// ...
if (a != null) {
if (first) {
x.setA(a);
first = false;
} else {
A newA = a.deepCopy();
newA.seq++;
x.setA(newA);
}
}
// ...
}
// 实现2,修改引用a
A a = getA();
for (var x: request.getXList()) {
// ...
if (a != null) {
x.setA(a);
a = a.deepCopy(); //会多复制一次
a.seq++;
}
// ...
}
// 新实现
A a = getA();
// copy iter
Iterator<A> aIter = new AbstractSequentialIterator<>(a) {
@Override
protected A computeNext(A prev) {
A result = prev.deepCopy();
result.seq++;
return result;
}
};
for (var x: request.getXList()) {
// ...
if (a != null) {
x.setA(aIter.next());
}
// ...
}
和原有实现相比,新实现抽取出了A的迭代逻辑,更好理解。
总结
Guava 的 PeekingIterator、AbstractIterator 和 AbstractSequentialIterator 为 Java 开发者提供了强大的工具,用于简化和增强迭代器的使用。它们通过以下方式提升代码的健壮性和可维护性:
- PeekingIterator: 允许在单次遍历中进行预判和复杂逻辑处理,减少了状态管理和冗余操作。
- AbstractIterator: 强制惰性求值,简化了自定义迭代器的实现,并按需处理数据。
- AbstractSequentialIterator: 实现按需生成序列,适用于处理长序列,避免了不必要的预计算和存储。
在处理数据流、构建数据管道或实现复杂业务逻辑时,合理利用这些迭代器,可以使代码更加清晰、灵活,并更好地管理资源。