Friday, February 10, 2023

Quiz yourself: Handling side effects in Java

Oracle Java, Oracle Java Tutorial and Materials, Oracle Java Prep, Oracle Java Preparation, Oracle Java Certification, Oracle Java Learning, Oracle Java Guides

This question exemplifies a style that’s popular with test creators. It’s less popular with candidates.


Imagine that your colleague is prototyping new business logic that must work in a multithreaded application and has created the following class:

class MyRunnable implements Runnable {
    public void run() {
        synchronized (MyRunnable.class) {
            System.out.print("hello ");
            System.out.print("bye ");
        }
    }
}

To test the class, your colleague wrote the following method and then invoked the method, passing a Stream object containing two MyRunnable instances:

public static void testMyRunnable(Stream<Runnable> s) {
    s.map(
        i -> {
            new Thread(new MyRunnable()).start();
            return i;
        }
    ).count();
}

A. The output will be exactly hello bye hello bye.
B. The output will always start with hello followed by either hello or bye.
C. No output will be produced.
D. None of the above.

Which statement is correct? Choose one.


Answer. This question exemplifies a style that’s popular with test creators, but perhaps it’s less popular with candidates. The setup makes the question appear to be on one topic, when in fact it’s really about something else. In this case, the question probably appears to be about threading and mutual exclusion using synchronization. It’s really about the Stream API.

Oracle Java, Oracle Java Tutorial and Materials, Oracle Java Prep, Oracle Java Preparation, Oracle Java Certification, Oracle Java Learning, Oracle Java Guides
Look at the method and its invocation. The test method receives a Stream as an argument, calls a map() operation on that stream, and then executes the count() terminal operation on the resulting stream. You know from the question that the Stream argument has two items in it, so the count() method must return 2.

Here is the detail that matters most: If the Stream object is one for which the size is known without having to draw elements to exhaustion, the count() method might actually return that size without ever processing the body of the stream. Indeed, the documentation for the count() method states the following:

An implementation may choose to not execute the stream pipeline (either sequentially or in parallel) if it is capable of computing the count directly from the stream source. In such cases no source elements will be traversed and no intermediate operations will be evaluated. Behavioral parameters with side-effects, which are strongly discouraged except for harmless cases such as debugging, may be affected.

In other words, if the argument stream has a known size, there will be no output at all. If, however, the argument stream has a size that is not known until it runs, some output will be produced.

The side effects of printing “hello ” and “bye ” are therefore not impossible but are also not guaranteed. Options A, B, and C are therefore incorrect, and option D must be the correct answer.

To dig deeper, let’s investigate this idea of a stream having a known or unknown element count. The following streams have exactly two elements:

List.of(1, 3).stream()
Stream.of(1, 3)

However, because some of the elements might be removed, the following stream has an element count that must be determined dynamically:

List.of(1, 3).stream.filter(x -> 3 * Math.random())

Given that this kind of side effect can be ignored—the documentation calls it elided—how should you write code intended to be used in the map method and related methods? The guidance is that the operations passed as arguments to the methods of a stream should generally be pure functions. A key (but not the only) feature of a pure function in programming (as distinct from mathematical theory) is that it does not have observable side effects. (Printing a message is typically considered to be a visible side effect, though logging messages might not be considered visible. It’s complicated and what’s visible depends a bit on perspective.)

On this topic, the documentation has more to offer.

The eliding of side-effects may also be surprising. With the exception of terminal operations forEach and forEachOrdered, side-effects of behavioral parameters may not always be executed when the stream implementation can optimize away the execution of behavioral parameters without affecting the result of the computation.

As mentioned earlier, this question looks as if it’s about synchronization. So, in the interest of completeness, consider how this aspect will behave if the map method’s argument is invoked with each element of the stream.

The body of the run() method is synchronized on the java.lang.Class object that describes MyRunnable in the running VM (that is, MyRunnable.class). This is, in effect, a static element and, therefore, no matter how many instances of this particular MyRunnable class might exist, only one thread can be in the process of executing the sequence of print statements. That tells you that if any thread manages to print “hello ” it must continue to print “bye ” before any other thread can print anything. This would mean that, if the stream actually processed its elements through the map operation, the output would be as shown in option A.

Conclusion. The correct answer is option D.

Source: oracle.com

Related Posts

0 comments:

Post a Comment