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

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

相关推荐
235162 小时前
【JVM】Java为啥能跨平台?JDK/JRE/JVM的关系?
java·开发语言·jvm·spring boot·后端·spring·职场和发展
courtfu2 小时前
Plugin ‘mysql_native_password‘ is not loaded`
java·后端
枫子有风2 小时前
Go语言流程控制
android·java·golang
上进小菜猪3 小时前
测试自动化Replay:让数据库迁移测试回归真实场景的一把“利器”
后端
Python私教3 小时前
FastAPI × SQLAlchemy 2.0 Async:从“能跑”到“可压测”的完整工程实践
后端
小裕哥略帅3 小时前
订单管理--实时算出在途数量、收货数量、到货数量、已发货数量和未发货数量
java·开发语言
Python私教3 小时前
FastAPI × Loguru:从“能跑”到“可运维”的日志实战
后端
Dxxyyyy3 小时前
零基础学JAVA--Day27(注释+异常+异常处理方法)
java·开发语言
Craaaayon4 小时前
如何选择两种缓存更新策略(写缓存+异步写库;写数据库+异步更新缓存)
java·数据库·redis·后端·缓存·mybatis