Wednesday, April 12, 2023

Quiz yourself: Using the stream methods dropWhile and takeWhile in Java

Quiz Yourself, Oracle Java Certification, Oracle Java Tutorial and Materials, Java Guides, Java Learning, Java Skills, Java Jobs


Given the following method fragment

Collection<Integer> c = Set.of(1,2,3,4,5,6,7); // line 1
var r = c.stream()
  .dropWhile(e -> e < 5)
  .takeWhile(e -> e < 7)
  .count(); // line 2
System.out.println(r);

Which statement is correct? Choose one.

A. Line 1 will cause an error.

B. Replacing the second statement with the following code would produce the same output:

var r = c.stream()
   .takeWhile(e -> e < 7)
   .dropWhile(e -> e < 5)
   .count(); // line 2

C. Replacing the second statement with the following code would produce the same output:

var r = c.stream()
  .filter(e -> !(e < 5))
  .filter(e -> e < 7)
  .count(); // line 2

D. The code may print 0.

E. The code may print 7.

F.     The code always prints 2.

Answer. This question explores some foundational concepts for collections and streams and, in particular, the Stream API’s dropWhile and takeWhile methods. These methods were added in Java 9 and are legitimate territory for the Java 11 and Java 17 exams.

Consider the behavior of the dropWhile and takeWhile methods. Both take a single argument of type Predicate and both methods are intermediate operations that return a new stream.

The dropWhile method, when applied to an ordered stream, starts at the beginning of the stream and tests, in order, data items of the stream it’s reading from. If elements pass the test, they are dropped. So far, this behavior seems similar to that of the filter method, except the behavior removes items that pass the test rather than keeping items that pass the test. However, as soon as an item fails the test, dropWhile becomes a simple pass-through, and all subsequent items will continue down the stream without being tested. The documentation describes this as “dropping the longest prefix of elements.”

Therefore, if an ordered stream contains the following (with the parade of items progressing towards the right, so that 1 is the head of the stream)

10 9 8 7 6 5 4 3 2 1

and you apply dropWhile(e -> e < 5), the output of the dropWhile will be a stream such as the following

10 9 8 7 6 5

If you apply the same dropWhile to this sequence

10 9 8 7 6 5 4 3 2 1 10 9 8 7 6 5 4 3 2 1

the result will be a stream such as the following

10 9 8 7 6 5 4 3 2 1 10 9 8 7 6 5

Notice that the result still contains the second occurrences of 1 through 4 (and if more of these occurred later, they too would be present in the result).

The behavior of the takeWhile operation has parallels to this, except that the resulting stream ends as soon as any item in the stream fails the test specified in the predicate. So, if you start with this stream

10 9 8 7 6 5 4 3 2 1 10 9 8 7 6 5 4 3 2 1

and apply takeWhile(e -> e < 7) to it, the result is a stream like the following

6 5 4 3 2 1

Therefore, if you chain the dropWhile(e -> e < 5) and takeWhile(e -> e < 7) operations, and apply them to an ordered stream that looks like the following

7 6 5 4 3 2 1

the dropWhile(e -> e < 5) would yield

7 6 5

and then the takeWhile(e -> e < 7) would operate on that stream and produce

6 5

If you then count the elements, you’d get the value 2.

This is all very nice, except for two problems. First, the order of items drawn from most Set objects is not guaranteed and typically does not match the order in which the items were added to the set. The second problem is you don’t have an ordered stream and as mentioned, the discussion above applies only to ordered streams.

Consider why these two things are true. A set, from a math perspective, has the task of rejecting duplicate elements, but it does not have the task of maintaining user-defined order. (Note that some of Java’s Set implementations do maintain user-defined order, for example, the TreeSet. But the order in this case still isn’t the order in which items were added; it’s literally an ordering that applies to the elements.) The documentation for the Set interface describes the effect of Set.of and Set.copyOf with the following caveat:

The iteration order of set elements is unspecified and is subject to change.

Shortly, you’ll see why this is critical and contradicts the discussion above.

Some stream objects are considered to be ordered and others are not. If you have a source of data that has a meaningful (user-controllable) order, such as the elements of a List or the computations of Stream.iterate (which calculates the next element of a stream from the predecessor), the stream starts out ordered.

Meanwhile, the documentation for dropWhile states

If this stream is unordered, and some (but not all) elements of this stream match the given predicate, then the behavior of this operation is nondeterministic; it is free to drop any subset of matching elements (which includes the empty set).

The documentation for takeWhile has an equivalent statement.

In this quiz question, you have an unordered stream; therefore, you have no basis for making reliable predictions about what it will do. (If you try this code, it will behave consistently, but “It always works for me!” is not a sound way to create production quality code. You must be able to guarantee that it will still work when implementations change, and for that, you must take seriously the limitations called out in the documentation.)

If you don’t even know that this code will behave consistently, you cannot claim to have other code that will behave the same way, nor can you assert that it will always do anything in particular. For this reason, options B, C, and F are all incorrect.

This leaves you to consider whether line 1 compiles, and if so, what the code might possibly output.

Option A claims the code will not compile. There are three ways that might seem like a tempting option.

◉ Can you put primitives into the argument for Set.of? Yes, you can because they will be autoboxed to Integer type. Further, this is consistent with the variable type (Collection<Integer>) to which the result is assigned.

◉ The assignment of the Set.of<Integer> created the variable of type Collection<Integer>. This succeeds because Set is a subinterface of Collection, and the generic types are both Integer.

◉ There would be an exception at runtime if you call Set.of with duplicate values in the argument list. In this case, the arguments are all distinct, so there’s no problem.

From this you know that there are no errors, and option A is also incorrect.

Option D is correct. The documentation for takeWhile working on an unordered stream states “…it is free to take any subset of matching elements (which includes the empty set).” Therefore, it clearly is permissible for the operation to result in zero surviving elements, which would produce a count of 0.

If you ignore the restrictions of dropWhile and takeWhile and simply consider that you don’t know the iteration order of the elements taken from Set.of, you can still see how unexpected results might be achieved. Imagine that the stream elements from the set arrive in the following order:

6 5 4 3 2 1 7

In this case, the dropWhile(e -> e < 5) part will be false on the first element, and none of the elements will be dropped. After that, the takeWhile(e -> e < 7) part will also be false on the first element (which is still 7). Therefore, zero elements will proceed downstream to the count() operation, and therefore 0 would be printed. Because 7 can’t be printed, you can see that option E is incorrect.

Conclusion. The correct answer is option D.

Source: oracle.com

Related Posts

0 comments:

Post a Comment