Guava 迭代器增强类介绍

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: 实现按需生成序列,适用于处理长序列,避免了不必要的预计算和存储。

在处理数据流、构建数据管道或实现复杂业务逻辑时,合理利用这些迭代器,可以使代码更加清晰、灵活,并更好地管理资源。

相关推荐
杨充4 分钟前
9.重构十二式的实战
设计模式·开源·代码规范
济*沧*海6 分钟前
如何判断接口的返回值?
后端
糖果店的幽灵9 分钟前
langgraph的四种state解析
java·前端·javascript·langgraph
杨充12 分钟前
5.多用组合和少继承
后端
杨充15 分钟前
6.设计原则的全景图
设计模式·开源·全栈
杨充16 分钟前
4.接口而非实现编程
java·后端·架构
杨充17 分钟前
2.面向对象的特性
设计模式
杨充17 分钟前
7.SOLID原则案例汇
设计模式·开源·全栈
杨充18 分钟前
8.反模式与坏味道
设计模式·开源·代码规范
梦梦代码精22 分钟前
基于ThinkPHP6 + Vue3的家政预约系统全解析:从LBS定位到自动派单的完整实现
java·docker·开源·php·代码规范