3

Sample code:

class Outer {
    public Integer i;

    Outer(Integer i) {
        this.i = i;
    }

    public int getVal() { return i; }
}

class MyClass {

    public Integer f(Outer o) { return o.getVal();};

    public void main() {

        MyClass g = new MyClass();

        List<Integer> l1 = Arrays.asList(new Outer(2)).stream().map(g::f).collect(Collectors.toList());
        List<Integer> l2 = Arrays.asList(new Outer(2)).stream().map(Outer::getVal).collect(Collectors.toList());
    }
}

Using either of the method references of

  1. Outer::instanceMethod that takes no argument and is basically a Supplier<T> functional interface. [1]

  2. MyClass::instanceMethod that takes an argument of type Outer and is a Function<T,R>functional interface. [1]

is valid. Then how does the map function know to apply the function in option (1) to the objects of the stream, but pass the stream objects to function in option (2)?

[1] https://docs.oracle.com/javase/8/docs/api/java/util/stream/package-summary.html

5
  • In each case, it's the only option which would compile. I am sure the JLS spells it out in detail though I am not sure where. Commented Jan 8, 2017 at 12:27
  • 1
    Note that outer::instanceMethod is not a Supplier in this case. It's a Function, too. The input of the function is this (i.e. the outer instance on which the method is called), the output is the value returned by the method. Commented Jan 8, 2017 at 12:35
  • @JBNizet I suspected this very behavior. For example, in Python, all class functions pass this as the first argument by default, so no function has 'zero' arguments. But where in the docs is this behavior/convention mentioned? Commented Jan 8, 2017 at 12:53
  • 1
    There, for example: docs.oracle.com/javase/tutorial/java/javaOO/… Commented Jan 8, 2017 at 13:06
  • As a side note, Arrays.asList(new outer(2)).stream() in a very convoluted way of creating a single element stream, wrapping the instance in an array, wrapping the array in a list, to eventually create a Stream from a List. Just use Stream.of(new outer(2)) instead. Commented Jan 9, 2017 at 11:28

1 Answer 1

4

First of all the map method does not know itself what to do with method references. That's the compiler's job. In both cases, map expects :

Function<? super PackageName.outer,? extends Integer>

For you particular question both method references according to the docs are a Reference to an instance method of a particular object

Regarding how the compiler deals with lambdas and method references and translates them to bytecode this document is highly recommended reading. The most relevant part to your question (emphasis mine to summarise):

When the compiler encounters a lambda expression, it first lowers (desugars) the lambda body into a method whose argument list and return type match that of the lambda expression, possibly with some additional arguments (for values captured from the lexical scope, if any.) At the point at which the lambda expression would be captured, it generates an invokedynamic call site, which, when invoked, returns an instance of the functional interface to which the lambda is being converted. This call site is called the lambda factory for a given lambda. The dynamic arguments to the lambda factory are the values captured from the lexical scope. The bootstrap method of the lambda factory is a standardized method in the Java language runtime library, called the lambda metafactory. The static bootstrap arguments capture information known about the lambda at compile time (the functional interface to which it will be converted, a method handle for the desugared lambda body, information about whether the SAM type is serializable, etc.)

Method references are treated the same way as lambda expressions, except that most method references do not need to be desugared into a new method; we can simply load a constant method handle for the referenced method and pass that to the metafactory

Instance-capturing method reference forms include bound instance method references (s::length, captured with reference kind invokeVirtual)

The bytecode for your 2 cases is :

  1. outer::instanceMethod

    // handle kind 0x5 : INVOKEVIRTUAL
    PackageName/outer.getVal()I, 
    (LPackageName/outer;)Ljava/lang/Integer;
    
  2. MyClass::instanceMethod

    // handle kind 0x5 : INVOKEVIRTUAL
    PackageName/MyClass.f(LPackageName/outer;)Ljava/lang/Integer;, 
    (LPackageName/outer;)Ljava/lang/Integer;
    

Note that, although the second line is more complicated in the second case, the last line is the same. In both cases the compiler just sees a function that takes an outer and returns an Integer. And that matches what map expects.

Method References are described in the language spec 15.13 Method Reference Expressions. The fact that the target reference of a method reference is an implicit first argument of the method is mentioned at 15.13.3 Run-Time Evaluation of Method References.

If the compile-time declaration is an instance method, then the target reference is the first formal parameter of the invocation method. Otherwise, there is no target reference

Sign up to request clarification or add additional context in comments.

2 Comments

I get it that both functions end up passing an outer type object as a parameter. What I wanted to know was whether this behavior is documented anywhere, i.e., for example, the getters of a class implicitly pass the object instance for which they are called. In other words, does the compiler interpret outer.getValue() as outer.getValue(outer this) in general?
You could say that the compiler does that in general. I'll add a reference to docs in my answer

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.