【问题标题】:do-while with Java8-Optional使用 Java8 进行操作-可选
【发布时间】:2015-02-26 11:48:42
【问题描述】:

我在一些项目中经常使用 do-while-checkNextForNull-getNext 循环模式(不知道是否有正式名称)。但是在 Java8 中,使用 Optional 被认为是比在客户端代码中检查空引用更干净的代码。但是当在这种循环模式中使用 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 ....
}

虽然这可能被视为违反了迭代器协定,因为 new Item(1) 对象包含在循环中,而通常情况下,for 循环会立即调用 next() 并因此跳过第一个对象。换句话说:对于第一个对象,next() 被违反了,因为它返回了第一个对象本身。

【问题讨论】:

  • 你真的必须使用这种模式吗?你可以类实现Iterable 并给你一个Iterator 吗?您只需实现hasNext()(这是您当前的布尔条件)和next,而不是仅实现next()
  • @XavierDelamotte 同意,这听起来像是(又一次)过度使用 Java8 功能集。不过,这是个好问题,OP。
  • @XavierDelamotte Iterator 在基于 IO 的源上实现是出了名的尴尬。要确定它是否为hasNext,它实际上必须读取并缓存下一个元素。仅依赖单一方法的类似光标的习语实际上是首选。例如,Spliterator 使用了这种方法,但有一个额外的变化。
  • @MarkoTopolnik 确实如此。然而 Guava 提供使用 AbstractIterator 来简化此类迭代器的实现。 code.google.com/p/guava-libraries/wiki/…
  • @XavierDelamotte 包装器仍然无济于事,因为hasNext 应该是一种没有延迟的无副作用方法。尤其要注意向客户端发送 I/O 错误信号所带来的困难。

标签: java loops while-loop java-8 optional


【解决方案1】:

你可以这样做:

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 更干净。谢谢!
【解决方案2】:

在 Java8 中,使用 Optional 被认为是比在客户端代码中检查空引用更简洁的代码

不,反之亦然: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);
  }
}

有了这个,您离无缝并行计算的能力只有一步之遥。由于您的项目流基本上是连续的,我建议您查看我的blog post 关于这个主题。

【讨论】:

    【解决方案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 循环更复杂的操作,supporting streams 是正确的选择。相似之处在于您的实现封装了如何迭代 Items。

    【讨论】:

    • 我建议改为for (Item i = this; i != null; i = i.next())。然后这将成为任何 Item 子类合法的模板方法(事实上,如果 Item 可以抽象为一个接口方法,那么 default 接口方法的完美候选者)。
    • @Marko Topolnik:如果考虑到子类,那将是合理的。我无法从原始问题中得出这一结论。
    【解决方案4】:

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

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

    public interface NextProvidble<T> {
    
        Optional<T> next();
    }
    

    Item 实现 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

    Custom 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));
    

    可能很清楚。 请添加您的想法。

    【讨论】:

      【解决方案5】:

      在此处删除自 Java 9 以来可用的另一个替代方案。

      Stream.iterate(new Item(1), Item::hasNext, Item::next)
            .forEach(this::doSomething)
      

      doSomething(Item item) 是对项目执行某些操作的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2021-12-28
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2021-08-13
        • 1970-01-01
        • 1970-01-01
        • 2016-02-12
        相关资源
        最近更新 更多