Reference to an instance method of a particular object

狂风中的少年 提交于 2019-11-30 09:15:21
Jon Skeet

userList.forEach is expecting a Consumer<? extends User> - in other words, a method which accept a User reference and do something with it.

That could be:

  • A static method accepting a User parameter, in which case the parameter will be populated with the relevant element in the list on each iteration:

    staticMethod(userFromList)
    
  • An instance method (of any class) accepting a User parameter, provided with a specific instance to call it on - again, the parameter will be populated with the relevant element:

    target.instanceMethod(userFromList)
    
  • An instance method on User with no parameters, provided without a specific instance, which case the target of the method call will be the relevant element in the list on each iteration:

    userFromList.instanceMethod()
    

Because you've tried to specify a target and the method doesn't have any parameters, the forEach method has nothing it can do with each element - it can't pass it as an argument because the method doesn't have any parameters, and it can't use it as the method target because you've already specified one.

Your working code shows the third example. Here are two other methods to allow you to demonstrate the first two:

public class UserPrinter {
    private final String name;

    public UserPrinter(String name) {
        this.name;
    }

    public static void staticPrintUser(User user) {
        // Assuming you add a User.getName() method
        System.out.println("staticPrintUser: " + user.getName());
    }

    public void instancePrintUser(User user) {
        System.out.println("instancePrintUser (instance " + name + "): "
            + user.getName());
    }
}

Then:

userList.forEach(UserPrinter::staticPrintUser);    // equivalent to
//userList.forEach(p -> UserPrinter.staticPrintUser(p));
UserPrinter printer = new UserPrinter("my printer");
userList.forEach(printer::instancePrintUser);      // equivalent to
//userList.forEach(p -> printer.instancePrintUser(p));

If you really want to call printUser on the same User three times, ignoring the User in the list, you could use:

userList.forEach(ignored -> u1.printName());

Based on http://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html we know that method references are similar to following lambdas

method reference          ==> lambda
------------------------------------------------------------------------------------
object::method            ==> (Foo f, Bar b, Baz z) -> object.method(f,b,z)
SomeClass::staticMethod   ==> (Foo f, Bar b, Baz z) -> SomeClass.staticMethod(f,b,z)
SomeClass::instanceMethod ==> (Foo f, Bar b, Baz z) -> f.instanceMethod(b,z)
SomeClass::new            ==> (Foo f, Bar b, Baz z) -> new SomeClass(f,b,z)

So your code

userList.forEach(User::printName); // works

can be rewritten as

userList.forEach((User u) -> u.printName()); // works

which is OK because it means that in accept method of Consumer which this lambdas "implements" you will invoke printName() on each User passed to this method.

But in case of

userList.forEach(u1::printName); // compile error

this code represents following lambda

userList.forEach((User u) -> u1.printName(u)); // compile error
//                                       ^^^   // method doesn't accept User argument

so you are trying to invoke printName from instance held by u1 reference and pass each User from list as this method argument, but as you see

public void printName() 

can't accept instance of User as its argument, which is why you are seeing compile time error.

This

u1::printName

is a method reference to be invoked on the object referenced by ui. The compiler doesn't know how to interpret the argument it should pass to the Consumer lambda. Its best guess is that it should be passed as

u1.printName(arg);

but such a method doesn't exist.

The method reference

u1::printName

is essentially equivalent to this lambda:

() -> u1.printName()

This is because printName doesn't have any arguments. If you had a printNameWithWidth(int width) method, then u1::printNameWithWidth would be equivalent to

(int width) -> u1.printNameWithWidth(width)

But the point is that in neither case is a User one of the arguments, since you've already told it which User to use (i.e. u1). forEach doesn't like that. It needs a lambda (or the equivalent) with a User (or whatever other element type) as an argument.

This:

User::printName

is equivalent to

(User x) -> x.printName()

which is why it works.

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;

public class Testing {


    public static void main(String[] args) {

        List<B> list = new ArrayList<B>();
        B b1=new B(); b1.setName("Durgesh");
        B b2=new B(); b2.setName("Val");
        list.add(b1);list.add(b2);


        MyInterface<B> my1 = B :: printName;
        my1.dummyDisplay(b1,b2);


        MyInterface<B> my2 = (a,b) -> a.printName(b);
        my2.dummyDisplay(b1,b2);

    //  MyInterface<B> my3 = b1::printName;   //compilation error
    }

}

class B{
    String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public     void printName(B b) {
    System.out.println(this.name + b.name);
    }   
}

@FunctionalInterface
interface  MyInterface <T> {
    public void dummyDisplay(T s, T t);

}

Below lines of code works fine even though printName method of class B accepts only 1 parameter while method dummyDisplay accepts 2 parameters. It is because when we call dummyDisplay ( of functional interface ) method with 2 arguments, compiler uses one argument to invoke printName method and another argument is passed as an argument to printName method. That means (arg1).printName(arg2). Notice the usage of "this" keyword in the method printName.So always remember that in such kinds of method references, number of paramters of the method to be called(printName) should always be 1 less than the number of parameters used in method(dummyDisplay) of functional interface. Such kind of method reference is very frequently used when dealing with POJO classes where we use getters(no arg) of the classes given the functional interface(say Function/Consumer - having methods with 1 parameter).

    MyInterface<B> my1 = B :: printName;
    my1.dummyDisplay(b1,b2);

I hope you understand this concept.

Now coming to below lines of code. This code is just a replacement of method reference which we discussed just now. Here, since method declared in functional interface has 2 parameters, so we have to and had to use 2 arguments in lambda expression (in this case its a and b). Then a.printName(b) will be written as the definition of interface method (dummyDisplay). Its straight forward. Lambda expression can be used any where provided Functional interfaces(off course).

    MyInterface<B> my2 = (a,b) -> a.printName(b);
    my2.dummyDisplay(b1,b2);

Now coming to last piece of code. We get compilation error because compiler expects exactly the same number of parameters in printName method of class B which are there in the method of functional interface. Normally this kind of method reference is used just to call any random method of any class which accepts some parameters and does some processing on the accepted data. eg. say add/multiply/divide methods present in the class Calculate or the compare method of Comparator functional interface. In all these cases, method definition does not use "this" keyword. It simply accepts some parameters and performs some task on them. I hope you guys got something out of it.

MyInterface<B> my3 = b1::printName;   //compilation error

Having said this, Now lets come to your question,

userList.forEach(User::printName);

works fine because, forEach method internally calls a method accept(arg1) of Consumer interface and your user-defined method printName has no args. So as per my above explanation, it is correct and the compiler does not complaint.

and

userList.forEach(u1::printName);

gives compilation error, because you are using object u1 while referencing instance method printName . So compiler expects same number of parameters of printName method as that there are in accept method of Consumer interface. So it will try to find printName(User param1) from your class User. And since it is not found, compiler complaints about the same.

I hope this helps you guys. Also let me know if I have missed anything or if I have said some thing wrong.

I think it's the key point of usage of method references in Java. I had really difficulty in learning it. @Pshemo's answer is really nice source for this case. Besides, the following image excerpted from Java 8 in Action is helpful to remember.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!