Java将方法作为参数传递

400

我正在寻找一种方法来通过引用传递一个方法。我知道Java不会将方法作为参数传递,但是我想找到一个替代方法。

有人告诉我接口是传递方法参数的替代方法,但我不明白接口如何充当方法的引用。如果我理解正确的话,接口只是一组未定义的抽象方法。我不想每次都需要定义接口,因为多个不同的方法可能会调用相同的方法,并且具有相同的参数。

我的目标类似于这样:

public void setAllComponents(Component[] myComponentArray, Method myMethod) {
    for (Component leaf : myComponentArray) {
        if (leaf instanceof Container) { //recursive call if Container
            Container node = (Container) leaf;
            setAllComponents(node.getComponents(), myMethod);
        } //end if node
        myMethod(leaf);
    } //end looping through components
}

被调用的方式如下:

setAllComponents(this.getComponents(), changeColor());
setAllComponents(this.getComponents(), changeSize());

2
目前我的解决方案是传递一个额外的参数,并在内部使用 switch case 选择适当的方法。然而,这种解决方案并不利于代码重用。 - Mac
2
请参考此答案 https://dev59.com/M37aa4cB1Zd3GeqPq30-#22933032,了解类似问题的解决方法。 - Tomasz Gawel
18个回答

285

编辑:从Java 8开始,Lambda表达式是一个不错的解决方案,正如其他答案所指出的那样。下面的答案适用于Java 7及更早版本...


查看一下命令模式

// NOTE: code not tested, but I believe this is valid java...
public class CommandExample 
{
    public interface Command 
    {
        public void execute(Object data);
    }

    public class PrintCommand implements Command 
    {
        public void execute(Object data) 
        {
            System.out.println(data.toString());
        }    
    }

    public static void callCommand(Command command, Object data) 
    {
        command.execute(data);
    }

    public static void main(String... args) 
    {
        callCommand(new PrintCommand(), "hello world");
    }
}

编辑:正如Pete Kirkham指出的那样,还有另一种方法可以使用访问者模式来实现。访问者模式需要更多的操作——您的节点都需要具有一个acceptVisitor()方法,但如果需要遍历更复杂的对象图,则值得考虑该方法。


3
@Mac - 很好!在没有一流方法的语言中,这种方法一再出现作为模拟它们的事实标准方式,因此值得记住。 - Dan Vinton
9
这是访问者模式(将对集合进行迭代的操作与应用于集合中每个成员的函数分离),而不是命令模式(将方法调用的参数封装到对象中)。您特别没有封装参数 - 它由访问者模式的迭代部分提供。 - Pete Kirkham
1
不需要 accept 方法,除非你将访问者模式与双重分派结合使用。如果你有一个单态访问者,那么它就是你上面所写的代码。 - Pete Kirkham
1
在Java 8中,可以像ex.operS(String :: toLowerCase,“STRING”)一样。请参阅精美的文章:http://www.studytrails.com/java/java8/Java8_Lambdas_FunctionalProgramming.jsp - Zon
4
这段话的含义是:这段代码可以被看作是策略模式(Strategy pattern),因为它封装了一个算法但可以接收参数。不过,当遍历容器中的所有叶子节点时,就会想起访问者模式(Visitor pattern)——这是访问者模式的传统用法。总之,你的实现非常好,可以被视为策略模式或访问者模式。 - ToolmakerSteve
显示剩余3条评论

116
在Java 8中,你现在可以使用Lambda表达式和方法引用更轻松地传递方法。首先,一些背景知识:函数式接口是只有一个抽象方法的接口,虽然它可以包含任意数量的默认方法(在Java 8中新引入)和静态方法。如果你不使用Lambda表达式,那么一个Lambda表达式可以快速实现抽象方法,避免所有不必要的语法。

没有Lambda表达式:

obj.aMethod(new AFunctionalInterface() {
    @Override
    public boolean anotherMethod(int i)
    {
        return i == 982
    }
});

使用lambda表达式:
obj.aMethod(i -> i == 982);

以下是Java教程中关于Lambda表达式的摘录

Syntax of Lambda Expressions

A lambda expression consists of the following:

  • A comma-separated list of formal parameters enclosed in parentheses. The CheckPerson.test method contains one parameter, p, which represents an instance of the Person class.

    Note: You can omit the data type of the parameters in a lambda expression. In addition, you can omit the parentheses if there is only one parameter. For example, the following lambda expression is also valid:

    p -> p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
    
  • The arrow token, ->

  • A body, which consists of a single expression or a statement block. This example uses the following expression:

    p.getGender() == Person.Sex.MALE 
        && p.getAge() >= 18
        && p.getAge() <= 25
    

    If you specify a single expression, then the Java runtime evaluates the expression and then returns its value. Alternatively, you can use a return statement:

    p -> {
        return p.getGender() == Person.Sex.MALE
            && p.getAge() >= 18
            && p.getAge() <= 25;
    }
    

    A return statement is not an expression; in a lambda expression, you must enclose statements in braces ({}). However, you do not have to enclose a void method invocation in braces. For example, the following is a valid lambda expression:

    email -> System.out.println(email)
    

Note that a lambda expression looks a lot like a method declaration; you can consider lambda expressions as anonymous methods—methods without a name.


以下是如何使用lambda表达式“传递方法”的方法:

以下是如何使用lambda表达式“传递方法”的方法:

interface I {
    public void myMethod(Component component);
}

class A {
    public void changeColor(Component component) {
        // code here
    }

    public void changeSize(Component component) {
        // code here
    }
}

class B {
    public void setAllComponents(Component[] myComponentArray, I myMethodsInterface) {
        for(Component leaf : myComponentArray) {
            if(leaf instanceof Container) { // recursive call if Container
                Container node = (Container)leaf;
                setAllComponents(node.getComponents(), myMethodInterface);
            } // end if node
            myMethodsInterface.myMethod(leaf);
        } // end looping through components
    }
}

class C {
    A a = new A();
    B b = new B();

    public C() {
        b.setAllComponents(this.getComponents(), component -> a.changeColor(component));
        b.setAllComponents(this.getComponents(), component -> a.changeSize(component));
    }
}

使用方法引用,可以再次缩短类C的代码:

class C {
    A a = new A();
    B b = new B();

    public C() {
        b.setAllComponents(this.getComponents(), a::changeColor);
        b.setAllComponents(this.getComponents(), a::changeSize);
    }
}

类A需要从接口继承吗? - Serob_b
1
@Serob_b 不需要。除非你想将其作为方法引用传递(参见 :: 运算符),否则 A 的值并不重要。a.changeThing(component) 可以更改为任何语句或代码块,只要它返回 void 即可。 - The Guy with The Hat

80
自Java 8以来,有一个docsFunction<T, R>接口,其中包含方法。
R apply(T t);

您可以使用它将函数作为参数传递给其他函数。T是函数的输入类型,R是返回类型。
在您的示例中,您需要传递一个以Component类型作为输入并返回Void的函数。在这种情况下,Function不是最佳选择,因为没有Void类型的自动装箱。您要查找的接口称为Consumer(docs),其方法为
void accept(T t);

它会看起来像这样:
public void setAllComponents(Component[] myComponentArray, Consumer<Component> myMethod) {
    for (Component leaf : myComponentArray) {
        if (leaf instanceof Container) { 
            Container node = (Container) leaf;
            setAllComponents(node.getComponents(), myMethod);
        } 
        myMethod.accept(leaf);
    } 
}

而你可以使用方法引用来调用它:

setAllComponents(this.getComponents(), this::changeColor);
setAllComponents(this.getComponents(), this::changeSize); 

假设您已在同一类中定义了changeColor()和changeSize()方法。

如果你的方法需要接受多个参数,你可以使用 BiFunction<T, U, R> - T 和 U 是输入参数类型,R 是返回类型。还有 BiConsumer<T, U>(两个参数,没有返回类型)。不幸的是,对于三个或更多的输入参数,你必须自己创建一个接口。例如:

public interface Function4<A, B, C, D, R> {

    R apply(A a, B b, C c, D d);
}

33

使用java.lang.reflect.Method对象并调用invoke方法。


15
我能看到没有任何问题。这个问题是将一个方法作为参数传递,这是一种非常有效的做法。这也可以包装成许多漂亮的模式,使它看起来更好。而且这是最通用的方式,不需要任何特殊的接口。 - Vinodh Ramasubramanian
3
你是否在JavaScript中使用过类型安全(type safety)?类型安全不是一个争议点。 - Danubian Sailor
14
当一种编程语言将类型安全性视为其最强组成部分之一时,类型安全性如何不成为一个论点呢?Java是一种强类型语言,而这种强类型正是你选择它而非其他编译语言的原因之一。 - Adam Parkin
25
核心反射功能最初是为基于组件的应用程序构建工具而设计的。通常情况下,在正常应用程序运行时不应通过反射访问对象。这就是 Java 创建者的思路;-) 《Effective Java第二版》第53条:优先使用接口而不是反射。 - Wilhem Meignan
10
不是 reflect 的合理使用。看到所有的顶票,我感到震惊。reflect 从未被设计为一种通用的编程机制;只有在没有其他简洁的解决方案时才使用它。 - ToolmakerSteve
显示剩余4条评论

24

首先定义一个接口,其中包含您想要作为参数传递的方法

public interface Callable {
  public void call(int param);
}

实现一个带有方法的类

class Test implements Callable {
  public void call(int param) {
    System.out.println( param );
  }
}

// 像这样调用

Callable cmd = new Test();

这允许你将cmd作为参数传递并调用在接口中定义的方法调用。

public invoke( Callable callable ) {
  callable.call( 5 );
}

5
Java已经为您定义了很多接口,因此您可能不需要自己创建接口:http://docs.oracle.com/javase/8/docs/api/java/util/function/package-summary.html - slim
@slim 有趣的观点,这些定义有多稳定?它们是打算像你建议的那样通常使用,还是可能会出现问题? - Manuel
1
@slim 实际上,文档已经回答了这个问题:“此包中的接口是 JDK 使用的通用函数接口,并可供用户代码使用。” - Manuel

24

虽然这对于Java 7及以下版本尚不适用,但我认为我们应该展望未来并至少认识到新版本中的一些变化,如Java 8所带来的更改

换句话说,这个新版本将lambda表达式和方法引用引入了Java(以及新的API),它们是解决这个问题的另一个有效方案。虽然它们仍然需要一个接口,但不会创建新对象,并且由于JVM的不同处理方式,额外的类文件不会污染输出目录。

lambda表达式和方法引用这两种方式都需要一个可用的接口,其签名使用单个方法:

public interface NewVersionTest{
    String returnAString(Object oIn, String str);
}

从现在开始,方法的名称并不重要。接受lambda表达式的地方也同样可以使用方法引用。例如,要在这里使用我们的签名:
public static void printOutput(NewVersionTest t, Object o, String s){
    System.out.println(t.returnAString(o, s));
}

这只是一个简单的接口调用,直到lambda函数被传递为止:
public static void main(String[] args){
    printOutput( (Object oIn, String sIn) -> {
        System.out.println("Lambda reached!");
        return "lambda return";
    }
    );
}

这将输出:
Lambda reached!
lambda return

方法引用是类似的。给定:

public class HelperClass{
    public static String testOtherSig(Object o, String s){
        return "real static method";
    }
}

和主函数:

public static void main(String[] args){
    printOutput(HelperClass::testOtherSig);
}

输出结果将是真实的静态方法方法引用可以是静态的、实例的、非静态的任意实例,甚至是构造函数。对于构造函数,类名后跟::new将被使用。 1 有些人认为这不是一个lambda,因为它具有副作用。然而,它更直观地展示了lambda的使用方式。

15

我上次查看时,Java本身不能做到你想要的功能;你必须使用“变通方法”来解决这些限制。就我所看到的,接口是一种替代方案,但不是一个好的替代方案。也许告诉你这件事的人是指这样的东西:

public interface ComponentMethod {
  public abstract void PerfromMethod(Container c);
}

public class ChangeColor implements ComponentMethod {
  @Override
  public void PerfromMethod(Container c) {
    // do color change stuff
  }
}

public class ChangeSize implements ComponentMethod {
  @Override
  public void PerfromMethod(Container c) {
    // do color change stuff
  }
}

public void setAllComponents(Component[] myComponentArray, ComponentMethod myMethod) {
    for (Component leaf : myComponentArray) {
        if (leaf instanceof Container) { //recursive call if Container
            Container node = (Container) leaf;
            setAllComponents(node.getComponents(), myMethod);
        } //end if node
        myMethod.PerfromMethod(leaf);
    } //end looping through components
}

然后您将使用以下方式调用:

setAllComponents(this.getComponents(), new ChangeColor());
setAllComponents(this.getComponents(), new ChangeSize());

11

如果这些方法不需要返回任何值,您可以使它们返回Runnable对象。

private Runnable methodName (final int arg) {
    return (new Runnable() {
        public void run() {
          // do stuff with arg
        }
    });
}

然后像这样使用:

private void otherMethodName (Runnable arg){
    arg.run();
}

简单明了的方法是这样调用的:otherMethodName(methodName(5)); - Noor Hossain
如果您不需要返回类型,我想这个方法应该是可行的。否则,您需要使用如Arvid Kumar Avinash所回答的接口。 - mirzak

9

Java-8及以后版本

从Java 8开始,您可以使用Lambda表达式实现函数接口(即仅有一个抽象方法的接口)的抽象方法,并将其作为参数传递给方法。

@FunctionalInterface
interface ArithmeticFunction {
    public int calcualate(int a, int b);
}

public class Main {
    public static void main(String args[]) {
        ArithmeticFunction addition = (a, b) -> a + b;
        ArithmeticFunction subtraction = (a, b) -> a - b;

        int a = 20, b = 5;

        System.out.println(perform(addition, a, b));
        // or
        System.out.println(perform((x, y) -> x + y, a, b));

        System.out.println(perform(subtraction, a, b));
        // or
        System.out.println(perform((x, y) -> x - y, a, b));
    }

    static int perform(ArithmeticFunction function, int a, int b) {
        return function.calcualate(a, b);
    }
}

输出:

25
25
15
15

在线演示

方法引用中了解更多信息。


6

我没有找到足够明确的示例来说明如何将java.util.function.Function用作简单方法的参数函数。这里有一个简单的例子:

import java.util.function.Function;

public class Foo {

  private Foo(String parameter) {
    System.out.println("I'm a Foo " + parameter);
  }

  public static Foo method(final String parameter) {
    return new Foo(parameter);
  }

  private static Function parametrisedMethod(Function<String, Foo> function) {
    return function;
  }

  public static void main(String[] args) {
    parametrisedMethod(Foo::method).apply("from a method");
  }
}

基本上,您有一个带有默认构造函数的 Foo 对象。一个 method 作为参数从类型为 Function<String, Foo>parametrisedMethod 调用。

  • Function<String, Foo> 表示该函数以 String 为参数并返回一个 Foo
  • Foo::Method 对应于像 x -> Foo.method(x); 的 lambda。
  • parametrisedMethod(Foo::method) 可以看作是 x -> parametrisedMethod(Foo.method(x))
  • .apply("from a method") 基本上就是执行 parametrisedMethod(Foo.method("from a method"))

然后,它将在输出中返回:

>> I'm a Foo from a method

这个示例应该是可以直接运行的,然后您可以尝试使用不同的类和接口来尝试更复杂的操作。


在Android中使用apply调用需要最低API 24。 - Inès Belhouchet
@InesBelhouchet 或者使用 Java 8+ API 解糖:https://developer.android.com/studio/write/java8-support - pallgeuer

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接