Java8-Optional的do-while循环

11
我经常在我的一些项目中使用do-while-checkNextForNull-getNext的循环模式(不知道是否有官方名称)。但是在Java8中,使用Optional被视为优于在客户端代码中检查null引用的更清晰的代码。但是当在这种循环模式中使用Optional时,代码变得有点冗长和丑陋,但因为Optional有一些方便的方法,所以我希望存在比我下面想出的更干净的方式。
示例:
给定以下类。
class Item {
    int nr;

    Item(nr) {
        this.nr = nr;
        // an expensive operation
    }

    Item next() {
        return ...someCondition....
            ? new Item(nr + 1)
            : null;
    }
}

其中第一项始终具有nr==1,每个项都确定下一个项,并且您不希望创建不必要的新项。

我可以在客户端代码中使用以下循环do-while-checkNextForNull-getNext模式:

Item item = new Item(1);
do {
    // do something with the item ....
} while ((item = item.next()) != null);

使用Java8-Optional,给定的类变为:

class Item {
    ....

    Optional<Item> next() {
        return ...someCondition....
            ? Optional.of(new Item(nr + 1))
            : Optional.empty();
    }
}

然后,do-while-checkNextForNull-getNext 循环模式变得有些丑陋和冗长:

Item item = new Item(1);
do {
    // do something with the item ....
} while ((item = item.next().orElse(null)) != null);

orElse(null)) != null 这部分让人感到不舒服。

我已经寻找了其他类型的循环,但没有找到更好的解决方案。有更简洁的方法吗?

更新:

可以使用 for-each 循环,同时避免空引用(使用空引用被认为是一种不好的实践)。这个解决方案由 Xavier Delamotte 提出,并且不需要 Java8-Optional。

使用通用迭代器的实现:

public class Item implements Iterable<Item>, Iterator<Item> {
    int nr;

    Item(int nr) { 
        this.nr = nr;
        // an expensive operation
    }

    public Item next() {
        return new Item(nr + 1);
    }

    public boolean hasNext() {
        return ....someCondition.....;
    }

    @Override
    public Iterator<Item> iterator() {
        return new CustomIterator(this);
    }
}

并且

class CustomIterator<T extends Iterator<T>> implements Iterator<T> {
    T currentItem;
    boolean nextCalled;

    public CustomIterator(T firstItem) {
        this.currentItem = firstItem;
    }

    @Override
    public boolean hasNext() {
        return currentItem.hasNext();
    }

    @Override
    public T next() {
        if (! nextCalled) {
            nextCalled = true;
            return currentItem;
        } else {
            currentItem = currentItem.next();
            return currentItem;
        }
    }
}

那么客户端代码就变得非常简单和干净:

for (Item item : new Item(1)) {
    // do something with the item ....
}

虽然这可能被视为 Iterator 合同的违反,因为 new Item(1) 对象包含在循环中,而通常,for 循环将立即调用 next() 并跳过第一个对象。换句话说,对于第一个对象,next() 被违反了,因为它返回了第一个对象本身。


3
你真的需要使用这个模式吗?你的类是否可以实现 Iterable 接口并返回一个迭代器(Iterator)呢?这样,你只需要实现 hasNext()(即当前的布尔条件)和 next 方法,而不是仅仅实现 next() 方法。 - Xavier Delamotte
1
@XavierDelamotte同意,这似乎又是对Java8特性的过度使用。不过,这是一个好问题,楼主。 - drew moore
@XavierDelamotte Iterator 在基于 IO 的源上实现起来非常棘手。为了找出它是否有下一个元素,实际上必须读取并缓存下一个元素。只依赖单个方法的光标式习语实际上更受欢迎。例如,Spliterator 使用这种方法,并加入了额外的技巧。 - Marko Topolnik
@MarkoTopolnik确实如此。然而,Guava提供了使用AbstractIterator来简化这些迭代器的实现。https://code.google.com/p/guava-libraries/wiki/CollectionHelpersExplained#AbstractIterator - Xavier Delamotte
@XavierDelamotte 包装器仍然无法解决 hasNext 应该是一个没有延迟的无副作用方法的事实。特别要注意需要向客户端发出 I/O 错误信号所带来的困难。 - Marko Topolnik
@XavierDelamotte,我刚刚使用您的解决方案更新了OP。虽然在第一次迭代中可能违反了next()的合同。 - Devabc
5个回答

12

你可以像这样做:

Optional<Item> item = Optional.of(new Item(1));
do {
    Item value = item.get();
    // do something with the value ....
} while ((item = value.next()).isPresent());

或者(为了避免额外的变量):

Optional<Item> item = Optional.of(new Item(1));
do {
    // do something with item.get() ....
} while ((item = item.get().next()).isPresent());

在第一行中使用Optional的一个缺点是,即使循环体中始终存在一个项,它也暗示了null的可能性。但这是一个有趣的解决方案,我认为它确实比 orElse(null)) != null 更清晰。谢谢! - Devabc

6
在Java8中,使用Optional比在客户代码中检查null引用更加干净。不,实际上相反:Optional只有在帮助编写更清洁的代码时才可以使用。在这种情况下,如果您现有的习惯看起来很好,就不必感到压力去使用它。例如,以下是Optional的良好使用示例:
item.next().map(Object::toString).ifPresent(System.out::println);

由于你需要在第一个不存在的 Optional 上跳出循环,因此这并没有什么帮助。

然而,我认为你真正的兴趣更广泛:利用 Java 8 的功能来改进你的代码。你应该选择的抽象是流式操作 Stream:

itemStream(() -> new Item(1)).forEach(item -> { ... all you need ... });

自然地,现在你可以放手使用流处理:

itemStream(() -> new Item(1)).filter(item.nr > 3).mapToInt(Item::nr).sum();

这是如何构建流的方法:
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

public class ItemSpliterator extends Spliterators.AbstractSpliterator<Item>
{
  private Supplier<Item> supplyFirst;
  private Item lastItem;

  public ItemSpliterator(Supplier<Item> supplyFirst) {
    super(Long.MAX_VALUE, ORDERED | NONNULL);
    this.supplyFirst = supplyFirst;
  }

  @Override public boolean tryAdvance(Consumer<? super Item> action) {
    Item item;
    if ((item = lastItem) != null)
      item = lastItem = item.next();
    else if (supplyFirst != null) {
      item = lastItem = supplyFirst.get();
      supplyFirst = null;
    }
    else return false;
    if (item != null) {
      action.accept(item);
      return true;
    }
    return false;
  }

  public static Stream<Item> itemStream(Supplier<Item> supplyFirst) {
    return StreamSupport.stream(new ItemSpliterator(supplyFirst), false);
  }
}

通过这个,你离无缝并行计算的能力仅有一步之遥。由于你的项目流基本上是顺序的,我建议阅读我在博客文章中对此主题的讨论。


3

只需将循环支持添加到您的API中:

class Item {
    int nr;

    Item(int nr) {
        this.nr = nr;
        // an expensive operation
    }

    public void forEach(Consumer<Item> action) {
        for(Item i=this; ; i=new Item(i.nr + 1)) {
            action.accept(i);
            if(!someCondition) break;
        }
    }
    public Optional<Item> next() {
        return someCondition? Optional.of(new Item(nr+1)): Optional.empty();
    }
}

然后您可以通过lambda表达式进行迭代
    i.forEach(item -> {whatever you want to do with the item});

或方法引用
    i.forEach(System.out::println);

如果您想支持比forEach循环更复杂的操作,支持流是正确的选择。它类似于您的实现封装了如何迭代Item

我建议使用 for (Item i = this; i != null; i = i.next()) 替代。这样就可以成为适用于任何 Item 子类的模板方法(实际上,如果 Item 能够被抽象为一个接口方法,则可以作为默认接口方法的完美候选者)。 - Marko Topolnik
1
@Marko Topolnik:如果考虑到子类,那么这将是合理的。我无法从原始问题中推导出这一点。 - Holger

1
在这里提供另一种选择,自Java 9以来可用。
Stream.iterate(new Item(1), Item::hasNext, Item::next)
      .forEach(this::doSomething)

doSomething(Item item)是处理该项的方法。


这里有几个问题:
  1. 如果第一个元素的hasNext为false,则不会包括种子(即第一个元素)。
  2. 最后一个元素通常没有加入到流中,因为它的hasNext为false。
- Mohammad Adnan

0

由于这与某种设计相关,我想出了以下设计。

创建支持提供可选下一个的接口。

public interface NextProvidble<T> {

    Optional<T> next();
}

该项实现了NextProvidble接口。

public class Item implements NextProvidble<Item> {
    int nr;

    Item(int nr) {
        this.nr = nr;
        // an expensive operation
    }

    @Override
    public Optional<Item> next() {
        return /*...someCondition....*/ nr < 10 ? Optional.of(new Item(nr + 1)) : Optional.empty();
    }

    @Override
    public String toString() {
        return "NR : " + nr;
    }
}

这里我使用 /...someCondition..../ 作为 nr < 10。

以下是自定义 Do While 的新类。

public abstract class CustomDoWhile<T extends NextProvidble<T>> {

    public void operate(T t) {
        doOperation(t);
        Optional<T> next = t.next();
        next.ifPresent( nextT -> operate(nextT));
    }

    protected abstract void doOperation(T t);
}

现在你需要在客户端代码中完成的工作。

 new CustomDoWhile<Item>() {
            @Override
            protected void doOperation(Item item) {
                System.out.println(item.toString());
            }
        }.operate(new Item(1));

可能非常清晰。 请添加您的想法。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接