6

The other day I was playing around with an experiment and I had a for loop something like so:

for (Node node : children) {
  // do stuff with node ...
}

And then I changed it to do this:

for (Node node : children.stream().filter(n -> n.isCreated())) {
  // do stuff with node ...
}

And got this error: Can only iterate over an array or an instance of java.lang.Iterable. This reopens some old wounds for me. Here's what I find silly here. Stream (the class that doesn't implement Iterable) has an iterator() method. Am I alone in thinking this is a major oversight? Is there any logical reason why it shouldn't be declared implement Iterable? I get that functional programming is cool and all but it can't be that the reason is to try to encourage people switch all their for loops to forEach() calls.

So the second part of this annoyance is that even if you do this:

for (Node node : children.stream().filter(n -> n.isCreated()).iterator()) {
  // do stuff with node ...
}

You get the exact same error. This is an old annoyance for me because when they introduced the for(:) syntax, it never made sense that Iterator wasn't one of the things you could use along with Iterable and arrays. Now, it seems even more obnoxious given that all that would need to be done to make Iterator implement Iterable is add the following method to Iterator:

default Iterator<E> iterator() {
  return this;
}

It's easy enough to work around these issues with methods like this:

static <T> Iterable<T> iterable(final Stream<T> stream)
{
  return new Iterable<T>() {
    @Override
    public Iterator<T> iterator() {
      return stream.iterator();
    }
  };
}

static <T> Iterable<T> iterable(final Iterator<T> iterator)
{
  return new Iterable<T>() {
    @Override
    public Iterator<T> iterator() {
      return iterator;
    }
  };
}

Or with lambas:

Iterable<T> foo = () -> iterator;

But why? It just seems silly to have to do this. Is anyone aware of why this wasn't done or come up with any legitimate reasons for not making these two interfaces Iterable?

lennon310
  • 3,132
  • 6
  • 16
  • 33
JimmyJames
  • 24,682
  • 2
  • 50
  • 92
  • 2
    I don't know about Iterator, but a Stream is potentially infinite, so a naive iteration could run forever. Not implementing the Iterable interface prevents these kinds of errors. – hoffmale Jun 06 '17 at 17:13
  • @hoffmale I'm not following. Stream already declares an [`iterator`](https://docs.oracle.com/javase/8/docs/api/java/util/stream/BaseStream.html#iterator--) method (via BaseStream.) How would declaring it to implement the Iterable interface effect what you are referring to? – JimmyJames Jun 06 '17 at 17:20
  • possible duplicate of https://stackoverflow.com/q/20129762 – wlnirvana Jan 24 '21 at 02:35

1 Answers1

11

They would break the Liskov Substitution Principle implementing Iterable<>.

An important part of the Iterable<> contract that isn't describable in Java is that you can iterate multiple times. Neither Iterator<> nor Stream<> have similar restrictions, so perfectly acceptable Iterator<>s or Streams<> that generate values only once, when converted to Iterable<> by your proposed conversion, will break code that expects to be able to independently operate on the values multiple times.

E.g.

<T> Iterable<Pair<T, T>> crossProduct(Iterable<T> source)
{
    List<Pair<T, T>> result = new ArrayList<Pair<T, T>>();

    for (outer : source) {
        for (inner : source) {
            result.add(new Pair<T, T>(outer, inner));
        }
    }
}
Caleth
  • 10,519
  • 2
  • 23
  • 35
  • 1
    Definitely a valid point you make. I would probably accept this if you can conjure up some evidence that this is part of the contract. The javadoc comment for Iterable says: "Implementing this interface allows an object to be the target of the "for-each loop" statement. See For-each Loop" and that's it. What you say makes sense in practice except for the fact that I don't see how iterating multiple times is a requirement of the enhanced for loop. So if you are right, then it seems like a mistake to use it for the for-each loop. – JimmyJames Jun 06 '17 at 19:17
  • 1
    @JimmyJames - I'd say it's implicit in the description of the iterator method ("Returns an iterator over a set of elements of type T. Returns: an Iterator."). Note that it doesn't allow the implementation to restrict the number of times it is called. Yes, I agree that it is a mistake that for-each loops can't accept Iterators (although i think it would be a bad idea to allow them to do so and not provide a warning on any use of the iterator afterwards), but to make a once-only Iterable is definitely dubious, if not outright wrong. – Jules Jun 06 '17 at 21:11
  • (Which isn't to say that I don't have an IterableAdapter class in my standard utility library... I just feel dirty every time I use it) – Jules Jun 06 '17 at 21:14
  • @Jules Where did you get that description for the `iterable()` method? The one for JDK 8 says: "Returns an iterator over elements of type T." I'm pretty sure 'set' would be incorrect here. On the other hand `BaseStream` says: "Returns an iterator for the elements of this stream." which is hardly different aside from 'of this stream'. It also says "This is a terminal operation." which actually calls out that the iterator consumes the stream. But I'm still not seeing anything that says `Iterable` is implicitly reusable. Is it assumed because it was retrofitted onto `Collection`? – JimmyJames Jun 06 '17 at 21:33
  • 2
    There is a notable *absence* of language to the effect of "An `Iterable` should be operated on ... only once", as compared to `Stream`, which supports an "`Iterable` is minimal `Collection`" theory. Another point is the addition of `Iterator::forEachRemaining` in Java 8 – Caleth Jun 06 '17 at 21:51
  • @JimmyJames - that was from the JDK7 Javadocs (chosen simply because they're the highest Google search result for "iterable javadoc"). – Jules Jun 06 '17 at 22:56
  • @Caleth I think it's a little weak but I guess I'll buy the first point. I don't understand how the forEachRemaining is relevant, though. Can you clarify? – JimmyJames Jun 07 '17 at 13:49
  • @Jules Thanks for letting me know that I am not the only one doing these kooky things. – JimmyJames Jun 07 '17 at 13:50
  • You'd rewrite your consuming `for (item : iterator.toIterable()) { ... }` as `iterator.forEachRemaining(item => { ... })` and `stream.forEach(item => { ... })` – Caleth Jun 07 '17 at 13:54
  • 1
    It doesn't explain why for-each doesn't work on streams or iterators though. – user253751 Nov 24 '20 at 19:32
  • @user253751 `for (item : iterable)` is [defined by the language](https://docs.oracle.com/javase/specs/jls/se8/html/jls-14.html#jls-14.14.2) to only apply to `Iterable`s and arrays – Caleth Nov 24 '20 at 20:08