简体   繁体   中英

Java double colon operator from compile time to byte code generation?

In this question the author uses the following example:

@Override
public final OptionalInt max() {
    return reduce(Math::max); //this is the gotcha line
}

So in this case it looks as if max() is a proxy for Math.max on the instance of this class. However there are no arguments passed to max, so does java 8 compile this to something like (Pseudo code):

@Override
public final OptionalInt max(Integer a, Integer b) {
      //If neither a or b are null
      return new OptionalInt.of(Math.max(a,b));
      //Otherwise return empty because we can't compare the numbers
      return OptionalInt.empty()
}

Also how would one write the javadoc for something like this?

So in this case it looks as if max() is a proxy for Math.max on the instance of this class. However there are no arguments passed to max, so does java 8 compile this to something like (Pseudo code):

 @Override public final OptionalInt max(Integer a, Integer b) { //If neither a or b are null return new OptionalInt.of(Math.max(a,b)); //Otherwise return empty because we can't compare the numbers return OptionalInt.empty() } 

Not quite :). Let's start by figuring out what the reduce operator actually does . The documentation explains that it performs a reduction on a sequence of numbers by applying an algorithm that is logically equivalent to the following:

public OptionalInt reduce(IntBinaryOperator op) {
    boolean foundAny = false;
    int result = 0;

    for (int element : [this stream]) {
        if (!foundAny) {
            foundAny = true;
            result = element;
        }
        else {
            result = op.applyAsInt(result, element);
        }
    }

    return foundAny ? OptionalInt.of(result)
                    : OptionalInt.empty();
}

Seems simple enough. If you can tell it how to take two numbers and 'reduce' or 'combine' them into one, then reduce knows how to extend that logic to reduce an entire sequence into a single number. It handles the edge cases and the aggregation for you. All it needs from you is a function that takes in two numbers and gives it one back. That function should conform to the functional interface IntBinaryOperator .

A functional interface is an interface that is meant to describe a single function. Specifically, it describes the argument types and the return type. The rest is largely superfluous. The signature for an IntBinaryOperator looks like this:

int applyAsInt(int left, int right);

You can provide a function that conforms to this specification in several ways. Prior to Java 8, you might have done something like this:

stream.reduce(
    new IntBinaryOperator() {
        public int applyAsInt(int a, int b) {
            return b > a ? b : a;
        }
    }
);

Java 8 gives us a shorthand form for functional interfaces called lambda expressions . These are a bit more concise, and while they are conceptually similar to anonymous inner classes, they're not quite the same thing.

stream.reduce((a, b) -> b > a ? b : a);

Both functions above are equivalent: they take in two numbers and return the larger of the two. As it turns out, every standard programming library has a function that does exactly the same thing. In Java, that function is Math.max . So rather than writing this logic myself, I can delegate to Math.max :

stream.reduce((a, b) -> Math.max(a, b));

But wait! All reduce wants is a function that takes two numbers and returns one. Math.max does that, so do I even need to wrap it in a lambda? It turns out I don't; I can tell reduce to just call Math.max directly:

stream.reduce(Math::max);

This says "I know you want a function, so I'm show you by name where to find one that's already been written". The compiler knows that Math.max conforms to the (int, int) -> int specification we need, so it emits some bytecode telling the VM how to 'bootstrap' it once it's needed. When the JVM hits your call to reduce , it calls a special method that generates a wrapper class implementing IntBinaryOperator that delegates to Math.max in its implementation of applyAsInt . It only performs this 'bootstrapping' step once. Since calling Math.max doesn't rely on anything other than the two numbers that get passed in, it can cache that implementation and use it the next time you wind up on this code path.

Pre Java 8, this would have been written as:

public MyMathInteface {
    OptionalInt max(Integer a, Integer b);
}

public static final MyMathInterface reducing = new MyMathInterface() {
    @Override
    public OptionalInt max(Integer a, Integer b) {
        return OptionalInt.of(Math.max(a, b));
    }
};

@Override
public final OptionalInt max() {
    return reduce(reducing);
}

Then reduce would be defined as:

public static OptionalInt reduce(MyMathInteface toReduce) {
    return toReduce.max(someValueA, someValueB);
}

So to answer your question, no arguments are passed to Math::max , because those values are retrieved by the reduce function. They could be constants or they could be retrieved from some other place.

In any case, the use of the max method in this way is called a method reference, that is where you do SomeObject::method . That :: operator creates a method reference. It returns a function, but does not call the function. The user (reduce) is responsible for calling the function.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM