Lev
Lev

Reputation: 3924

Java method reference

I've some class with these methods:

public class TestClass
{

    public void method1()
    {
        // this method will be used for consuming MyClass1
    }

    public void method2()
    {
        // this method will be used for consuming MyClass2
    }
}

and classes:

public class MyClass1
{
}

public class MyClass2
{
}

and I want HashMap<Class<?>, "question"> where I would store (key: class, value: method) pairs like this ( class "type" is associated with method )

hashmp.add(Myclass1.class, "question");

and I want to know how to add method references to HashMap (replace "question").

p.s. I've come from C# where I simply write Dictionary<Type, Action> :)

Upvotes: 18

Views: 15354

Answers (9)

Bohemian
Bohemian

Reputation: 425398

To answer your direct question regarding using a Map, your proposed classes would be:

interface Question {} // marker interface, not needed but illustrative

public class MyClass1 implements Question {}

public class MyClass2 implements Question {}

public class TestClass {
    public void method1(MyClass1 obj) {
        System.out.println("You called the method for MyClass1!");
    }

    public void method2(MyClass2 obj) {
        System.out.println("You called the method for MyClass2!");
    }
}

Then your Map would be:

Map<Class<? extends Question>, Consumer<Question>> map = new HashMap<>();

and populated like this:

TestClass tester = new TestClass();
map.put(MyClass1.class, o -> tester.method1((MyClass1)o)); // cast needed - see below
map.put(MyClass2.class, o -> tester.method2((MyClass2)o));

and used like this:

Question question = new MyClass1();
map.get(question.getClass()).accept(question); // calls method1

The above works OK, but the problem is that there's no way to connect the type of the key of the map with the type of its value, ie you can't use generics to properly type the value of the consumer and so use a method reference:

map.put(MyClass1.class, tester::method1); // compile error

that's why you need to cast the object in the lambda to bind to the correct method.

There's also another problem. If someone creates a new Question class, you don't know until runtime that there isn't an entry in the Map for that class, and you have to write code like if (!map.containsKey(question.getClass())) { // explode } to handle that eventuality.

But there is an alternative...


There is another pattern that does give you compile time safety, and means you don't need to write any code to handle "missing entries". The pattern is called Double Dispatch (which is part of the Visitor pattern).

It looks like this:

interface Tester {
    void consume(MyClass1 obj);
    void consume(MyClass2 obj);
}

interface Question {
    void accept(Tester tester);
}

public class TestClass implements Tester {
    public void consume(MyClass1 obj) {
        System.out.println("You called the method for MyClass1!");
    }

    public void consume(MyClass2 obj) {
        System.out.println("You called the method for MyClass2!");
    }
}

public  class MyClass1 implements Question {
    // other fields and methods
    public void accept(Tester tester) {
        tester.consume(this);
    }
}
public  class MyClass2 implements Question {
    // other fields and methods
    public void accept(Tester tester) {
        tester.consume(this);
    }
}

And to use it:

Tester tester = new TestClass();
Question question = new MyClass1();
question.accept(tester);

or for many questions:

List<Question> questions = Arrays.asList(new MyClass1(), new MyClass2());
questions.forEach(q -> q.accept(tester));

This pattern works by putting a callback into the target class, which can bind to the correct method for handling that class for the this object.

The benefit of this pattern is if another Question class is created, it is required to implement the accept(Tester) method, so the Question implementer will not forget to implement the callback to the Tester, and automatically checks that Testers can handle the new implementation, eg

public class MyClass3 implements Question {
    public void accept(Tester tester) { // Questions must implement this method
        tester.consume(this); // compile error if Tester can't handle MyClass3 objects
    }
}

Also note how the two classes don't reference each other - they only reference the interface, so there's total decoupling between Tester and Question implementations (which makes unit testing/mocking easier too).

Upvotes: 2

Gee Bee
Gee Bee

Reputation: 1794

Your question

Given your classes with some methods:

public class MyClass1 {
    public void boo() {
        System.err.println("Boo!");
    }
}

and

public class MyClass2 {
    public void yay(final String param) {
        System.err.println("Yay, "+param);
    }
}

Then you can get the methods via reflection:

Method method=MyClass1.class.getMethod("boo")

When calling a method, you need to pass a class instance:

final MyClass1 instance1=new MyClass1();
method.invoke(instance1);

To put it together:

public class Main {
    public static void main(final String[] args) throws NoSuchMethodException, SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {
        final Map<Class<?>,Method> methods=new HashMap<Class<?>,Method>();
        methods.put(MyClass1.class,MyClass1.class.getMethod("boo"));
        methods.put(MyClass2.class,MyClass2.class.getMethod("yay",String.class));


        final MyClass1 instance1=new MyClass1();
        methods.get(MyClass1.class).invoke(instance1);

        final MyClass2 instance2=new MyClass2();
        methods.get(MyClass2.class).invoke(instance2,"example param");

    }
}

Gives:
Boo!
Yay, example param

Watch out for the following gotchas:

  • hardcoded method name as a string - this is very hard to avoid
  • it is reflection, so accessing to the metadata of the class in runtime. Prone to a lot of exceptions (not handled in the example)
  • you need to tell not only the method name, but the parameter types as well to access to one method. This is because method overloading is standard, and this is the only way to pick the right overloaded method.
  • watch out when calling a method with parameters: there is no compile time parameter type check.

An alternative answer

I guess what you're looking for is a simple listener: i.e. a way to call a method from another class indirectly.

public class MyClass1 implements ActionListener {
    @Override
    public void actionPerformed(final ActionEvent e) {
        System.err.println("Boo!");
    }
}

and

public class MyClass2 implements ActionListener {
    @Override
    public void actionPerformed(final ActionEvent e) {
        System.err.println("Yay");
    }
}

using as:

public class Main {
    public static void main(final String[] args)  {
        final MyClass1 instance1=new MyClass1();
        final MyClass2 instance2=new MyClass2();

        final Map<Class<?>,ActionListener> methods=new HashMap<Class<?>,ActionListener>();

        methods.put(MyClass1.class,instance1);
        methods.put(MyClass2.class,instance2);



        methods.get(MyClass1.class).actionPerformed(null);
        methods.get(MyClass2.class).actionPerformed(null);
    }
}

This is called the listener pattern. I dared to reuse the ActionListener from Java Swing, but in fact you can very easily make your own listeners by declaring an interface with a method. MyClass1, MyClass2 will implement the method, and then you can call it just like a... method.

No reflection, no hardcoded strings, no mess. (The ActionListener allows passing one parameter, which is tuned for GUI apps. In my example I just pass null.)

Upvotes: 0

M A
M A

Reputation: 72884

You mention in the code comment that each method consumes an object of a certain type. Since this is a common operation, Java already provides you with a functional interface called Consumer that acts as a way to take an object of a certain type as input and do some action on it (two words so far that you already mentioned in the question: "consume" and "action").

The map can therefore hold entries where the key is a class such as MyClass1 and MyClass2, and the value is a consumer of objects of that class:

Map<Class<T>, Consumer<T>> consumersMap = new HashMap<>();

Since a Consumer is a functional interface, i.e. an interface with only one abstract method, it can be defined using a lambda expression:

Consumer<T> consumer = t -> testClass.methodForTypeT(t);

where testClass is an instance of TestClass.

Since this lambda does nothing but call an existing method methodForTypeT, you can use a method reference directly:

Consumer<T> consumer = testClass::methodForTypeT;

Then, if you change the signatures of the methods of TestClass to be method1(MyClass1 obj) and method2(MyClass2 obj), you would be able to add these method references to the map:

consumersMap.put(MyClass1.class, testClass::method1);
consumersMap.put(MyClass2.class, testClass::method2);

Upvotes: 3

Daniel Kaplan
Daniel Kaplan

Reputation: 67514

Now that Java 8 is out I thought I'd update this question with how to do this in Java 8.

package com.sandbox;

import java.util.HashMap;
import java.util.Map;

public class Sandbox {
    public static void main(String[] args) {
        Map<Class, Runnable> dict = new HashMap<>();

        MyClass1 myClass1 = new MyClass1();
        dict.put(MyClass1.class, myClass1::sideEffects);

        MyClass2 myClass2 = new MyClass2();
        dict.put(MyClass2.class, myClass2::sideEffects);

        for (Map.Entry<Class, Runnable> classRunnableEntry : dict.entrySet()) {
            System.out.println("Running a method from " + classRunnableEntry.getKey().getName());
            classRunnableEntry.getValue().run();
        }
    }

    public static class MyClass1 {
        public void sideEffects() {
            System.out.println("MyClass1");
        }
    }

    public static class MyClass2 {
        public void sideEffects() {
            System.out.println("MyClass2");
        }
    }

}

Upvotes: 15

Yegoshin Maxim
Yegoshin Maxim

Reputation: 881

Use interfaces instead of function pointers. So define an interface which defines the function you want to call and then call the interface as in example above. To implement the interface you can use anonymous inner class.

void DoSomething(IQuestion param) {
    // ...
    param.question();
}

Upvotes: 3

nd.
nd.

Reputation: 8942

While you can store java.lang.reflect.Method objects in your map, I would advise against this: you still need to pass the object that is used as the this reference upon invocation, and using raw strings for method names may pose problems in refactoring.

The cannonical way of doing this is to extract an interface (or use an existing one) and use anonymous classes for storing:

map.add(MyClass1.class, new Runnable() {
  public void run() {
    MyClass1.staticMethod();
  }
});

I must admit that this is much more verbose than the C#-variant, but it is Java's common practice - e.g. when doing event handling with Listeners. However, other languages that build upon the JVM usually have shorthand notations for such handlers. By using the interface-approach, your code is compatible with Groovy, Jython, or JRuby and it is still typesafe.

Upvotes: 2

Chandra Sekhar
Chandra Sekhar

Reputation: 19500

Method method = TestClass.class.getMethod("method name", type)

Upvotes: 3

Peter Lawrey
Peter Lawrey

Reputation: 533880

This is feature which is likely to be Java 8. For now the simplest way to do this is to use reflection.

public class TestClass {
    public void method(MyClass1 o) {
        // this method will be used for consuming MyClass1
    }

    public void method(MyClass2 o) {
        // this method will be used for consuming MyClass2
    }
}

and call it using

Method m = TestClass.class.getMethod("method", type);

Upvotes: 11

Related Questions