在Java中,什么最替代函数指针?


299

我有一个大约十行代码的方法。我想创建更多功能完全相同的方法,只是进行少量计算会更改一行代码。这是用于传入函数指针以替换该行的完美应用程序,但是Java没有函数指针。我最好的选择是什么?


5
Java 8将具有Lambda表达式。您可以在此处阅读有关lambda表达式的更多信息。
Marius

4
@Marius我不太认为lambda表达式可以算作函数指针。另一方面::操作员...
帽子的家伙

抱歉,您的评论太晚了;)-通常,您不需要使用函数指针。只需使用模板方法!(en.wikipedia.org/wiki/Template_method_pattern
isnot2bad

2
@ isnot2bad-看那篇文章,似乎有些矫kill过正-比这里给出的答案更复杂。具体来说,模板方法需要为每个替代计算创建一个子类。我看不到OP声明了任何需要子类的内容;他只是想创建几种方法,并共享大部分实现。如公认的答案所示,即使在Java 8及其lambda之前,也可以很容易地“就地”(在每个方法内部)完成此操作。
ToolmakerSteve

@ToolmakerSteve接受的解决方案还需要对每个类进行计算(即使它只是一个匿名内部类)。而且模板方法模式也可以使用匿名内部类来实现,因此它与有关开销(在Java 8之前)的公认解决方案相差无几。因此,这更多是关于使用模式和详细要求的问题,我们不知道。我很欣赏被接受的答案,只想增加另一种可能的想法。
isnot2bad 2015年

Answers:


269

匿名内部阶级

假设您要传入的函数String返回一个参数int
首先,如果您不能重用现有的接口,则必须定义一个以该函数作为其唯一成员的接口。

interface StringFunction {
    int func(String param);
}

使用指针的方法将只接受StringFunction实例,如下所示:

public void takingMethod(StringFunction sf) {
   int i = sf.func("my string");
   // do whatever ...
}

会这样称呼:

ref.takingMethod(new StringFunction() {
    public int func(String param) {
        // body
    }
});

编辑:在Java 8中,您可以使用lambda表达式来调用它:

ref.takingMethod(param -> bodyExpression);

13
顺便说一下,这是“命令模式”的一个示例。en.wikipedia.org/wiki/Command_Pattern
食人魔Psalm33

3
@Ogre Psalm33此技术也可以是“策略模式”,具体取决于您如何使用它。策略模式与指挥模式的区别
罗里·奥肯

2
这是Java 5、6和7的闭包实现 mseifed.blogspot.se/2012/09/… 它包含了所有可能需要的内容……我认为它非常棒!
毫米

@SecretService:该链接已死。
劳伦斯·多尔

@LawrenceDol是的。这是我正在使用的类的pastebin。pastebin.com/b1j3q2Lp
mmm

32

对于每个“函数指针”,我将创建一个小的函子类来实现您的计算。定义所有类都将实现的接口,并将这些对象的实例传递到更大的函数中。这是“ 命令模式 ”和“ 策略模式 ”的组合。

@sblundy的例子很好。


28

如果可以在一行中完成预定义数量的不同计算,则使用枚举是一种实现策略模式的快速而清晰的方法。

public enum Operation {
    PLUS {
        public double calc(double a, double b) {
            return a + b;
        }
    },
    TIMES {
        public double calc(double a, double b) {
            return a * b;
        }
    }
     ...

     public abstract double calc(double a, double b);
}

显然,策略方法声明以及每个实现的确切一个实例都定义在单个类/文件中。


24

您需要创建一个接口,该接口提供您要传递的功能。例如:

/**
 * A simple interface to wrap up a function of one argument.
 * 
 * @author rcreswick
 *
 */
public interface Function1<S, T> {

   /**
    * Evaluates this function on it's arguments.
    * 
    * @param a The first argument.
    * @return The result.
    */
   public S eval(T a);

}

然后,当您需要传递函数时,可以实现该接口:

List<Integer> result = CollectionUtilities.map(list,
        new Function1<Integer, Integer>() {
           @Override
           public Integer eval(Integer a) {
              return a * a;
           }
        });

最后,map函数使用在Function1中传递的内容,如下所示:

   public static <K,R,S,T> Map<K, R> zipWith(Function2<R,S,T> fn, 
         Map<K, S> m1, Map<K, T> m2, Map<K, R> results){
      Set<K> keySet = new HashSet<K>();
      keySet.addAll(m1.keySet());
      keySet.addAll(m2.keySet());

      results.clear();

      for (K key : keySet) {
         results.put(key, fn.eval(m1.get(key), m2.get(key)));
      }
      return results;
   }

如果不需要传递参数,通常可以使用Runnable而不是自己的接口,或者可以使用各种其他技术来减少param的“固定”数量,但这通常是在类型安全性方面进行的权衡。(或者,您可以重写构造函数,以使函数对象以这种方式传递参数。.有很多方法,在某些情况下,某些方法效果更好。)


4
这个“答案”与问题集有关的多于解决方案集。
tchrist

18

使用::运算符的方法引用

您可以在方法参数中使用方法引用,其中方法接受功能接口。功能接口是仅包含一个抽象方法的任何接口。(功能接口可能包含一个或多个默认方法或静态方法。)

IntBinaryOperator是一个功能界面。其抽象方法applyAsInt接受2 int作为参数,并返回intMath.max也接受两个,int并返回int。在此示例中,A.method(Math::max);make parameter.applyAsInt将其两个输入值发送到Math.max并返回该结果Math.max

import java.util.function.IntBinaryOperator;

class A {
    static void method(IntBinaryOperator parameter) {
        int i = parameter.applyAsInt(7315, 89163);
        System.out.println(i);
    }
}
import java.lang.Math;

class B {
    public static void main(String[] args) {
        A.method(Math::max);
    }
}

通常,您可以使用:

method1(Class1::method2);

代替:

method1((arg1, arg2) -> Class1.method2(arg1, arg2));

它的缩写为:

method1(new Interface1() {
    int method1(int arg1, int arg2) {
        return Class1.method2(arg1, agr2);
    }
});

有关更多信息,请参见Java 8Java语言规范§15.13中的::(双冒号)运算符


15

您也可以执行此操作(在某些RARE场合很有意义)。问题(也是一个大问题)是,您失去了使用类/接口的所有类型安全性,并且必须处理该方法不存在的情况。

它确实具有“好处”,您可以忽略访问限制并调用私有方法(示例中未显示,但是您可以调用编译器通常不允许您调用的方法)。

同样,这在极少数情况下是有道理的,但在那些情况下,它是一个不错的工具。

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

class Main
{
    public static void main(final String[] argv)
        throws NoSuchMethodException,
               IllegalAccessException,
               IllegalArgumentException,
               InvocationTargetException
    {
        final String methodName;
        final Method method;
        final Main   main;

        main = new Main();

        if(argv.length == 0)
        {
            methodName = "foo";
        }
        else
        {
            methodName = "bar";
        }

        method = Main.class.getDeclaredMethod(methodName, int.class);

        main.car(method, 42);
    }

    private void foo(final int x)
    {
        System.out.println("foo: " + x);
    }

    private void bar(final int x)
    {
        System.out.println("bar: " + x);
    }

    private void car(final Method method,
                     final int    val)
        throws IllegalAccessException,
               IllegalArgumentException,
               InvocationTargetException
    {
        method.invoke(this, val);
    }
}

1
有时,我将其用于菜单处理/ GUI,因为方法语法比匿名内部类语法简单得多。这很整洁,但是您增加了一些人不想挖掘的反射的复杂性,因此请务必确保正确无误,并对每种可能的错误情况都具有清晰的文本错误。
比尔K

您可以使用泛型安全地键入内容,而无需反射。
路易吉·普林格

1
我看不到使用泛型而不使用反射将如何让您通过String中包含的名称调用方法?
TofuBeer 2011年

1
@LuigiPlinge-您能提供您所指的代码段吗?
制造商史蒂夫(Steve)2015年

13

如果只有一行不同,则可以添加参数,例如标志和调用一行或另一行的if(flag)语句。


1
如果有两个以上的计算变体,javaslook的答案似乎是一种更干净的方法。或者,如果希望将代码嵌入到方法中,则为该方法处理的不同情况提供一个枚举,并进行切换。
制造商

1
@ToolmakerSteve如此,虽然今天你这一点在Java 8.使用lambda表达式
彼得Lawrey


11

使用运算符的新Java 8 功能接口方法参考::

Java 8能够使用“ @ Functional Interface ”指针维护方法引用(MyClass :: new)。不需要相同的方法名称,只需要相同的方法签名。

例:

@FunctionalInterface
interface CallbackHandler{
    public void onClick();
}

public class MyClass{
    public void doClick1(){System.out.println("doClick1");;}
    public void doClick2(){System.out.println("doClick2");}
    public CallbackHandler mClickListener = this::doClick;

    public static void main(String[] args) {
        MyClass myObjectInstance = new MyClass();
        CallbackHandler pointer = myObjectInstance::doClick1;
        Runnable pointer2 = myObjectInstance::doClick2;
        pointer.onClick();
        pointer2.run();
    }
}

那么,我们这里有什么?

  1. 功能接口-这是用@FunctionalInterface注释或不注释的接口,该接口仅包含一个方法声明。
  2. 方法参考-这只是特殊的语法,看起来像objectInstance :: methodName,仅此而已。
  3. 使用示例-只是一个赋值运算符,然后是接口方法调用。

您应该仅对听众使用功能接口!

因为所有其他此类函数指针确实不利于代码可读性和理解能力。但是,直接方法引用有时会很方便,例如foreach。

有几个预定义的功能接口:

Runnable              -> void run( );
Supplier<T>           -> T get( );
Consumer<T>           -> void accept(T);
Predicate<T>          -> boolean test(T);
UnaryOperator<T>      -> T apply(T);
BinaryOperator<T,U,R> -> R apply(T, U);
Function<T,R>         -> R apply(T);
BiFunction<T,U,R>     -> R apply(T, U);
//... and some more of it ...
Callable<V>           -> V call() throws Exception;
Readable              -> int read(CharBuffer) throws IOException;
AutoCloseable         -> void close() throws Exception;
Iterable<T>           -> Iterator<T> iterator();
Comparable<T>         -> int compareTo(T);
Comparator<T>         -> int compare(T,T);

对于较早的Java版本,您应该尝试使用Guava库,它具有与Adrian Petrescu上面提到的功能和语法类似的功能。

有关其他研究,请查看Java 8备忘单

并感谢Java语言规范§15.13链接的帽子的家伙。


7
因为所有其他……对于代码的可读性来说真的很不好 ”是一个完全毫无根据的主张,而且是错误的。
劳伦斯·多尔2014年

9

@sblundy的答案很好,但是匿名内部类有两个小缺陷,主要的缺陷是它们往往不可重用,而次要的则是庞大的语法。

令人高兴的是,他的模式可以扩展为完整的类,而对主类(执行计算的那个类)没有任何更改。

当实例化一个新类时,您可以将参数传递到该类中,这些参数可以在您的方程式中充当常量-因此,如果您的一个内部类如下所示:

f(x,y)=x*y

但有时您需要的是:

f(x,y)=x*y*2

也许三分之一是:

f(x,y)=x*y/2

您可以创建一个实例化为单个的ACTUAL类,而不是创建两个匿名内部类或添加“ passthrough”参数。

InnerFunc f=new InnerFunc(1.0);// for the first
calculateUsing(f);
f=new InnerFunc(2.0);// for the second
calculateUsing(f);
f=new InnerFunc(0.5);// for the third
calculateUsing(f);

它只会将常量存储在类中,并在接口中指定的方法中使用它。

实际上,如果知道您的函数不会被存储/重用,则可以执行以下操作:

InnerFunc f=new InnerFunc(1.0);// for the first
calculateUsing(f);
f.setConstant(2.0);
calculateUsing(f);
f.setConstant(0.5);
calculateUsing(f);

但是不可变的类更安全-我无法提出使此类可变的理由。

我之所以只发布此帖子,是因为每当我听到匿名内部类时,我都会畏缩-我看到了很多“必需”的冗余代码,因为程序员要做的第一件事就是当他应该使用实际的类并且从不使用匿名类时就匿名了。重新考虑了他的决定。


??OP正在谈论不同的计算(算法;逻辑);您显示的是不同的(数据)。您确实显示了可以将差异合并到值中的特定情况,但这是对所提出问题的不合理简化。
制造商史蒂夫(Steve)2015年


4

在我看来,这是一种策略模式。查看fluffycat.com Java模式。


4

使用Java进行编程时,我真正想念的一件事是函数回调。需要这些内容不断显示的一种情况是递归处理层次结构,您要对每个项目执行一些特定操作。就像遍历目录树或处理数据结构一样。我内心的极简主义者讨厌必须为每种特定情况定义一个接口,然后定义一个实现。

有一天,我发现自己想知道为什么不呢?我们有方法指针-Method对象。通过优化JIT编译器,反射调用确实不再带来巨大的性能损失。除了将文件从一个位置复制到另一个位置之外,所反映的方法调用的代价也微不足道。

当我进一步思考时,我意识到OOP范式中的回调需要将对象和方法绑定在一起-输入Callback对象。

看看我基于Java的基于反射的回调解决方案。免费使用。


4

好的,这个线程已经足够老了,所以很可能我的回答对这个问题没有帮助。但是由于该线程帮助我找到了解决方案,所以无论如何我都会把它放在这里。

我需要使用具有已知输入和已知输出(均为double)的可变静态方法。因此,知道方法包和名称后,我可以按以下方式工作:

java.lang.reflect.Method Function = Class.forName(String classPath).getMethod(String method, Class[] params);

对于接受一个double作为参数的函数。

因此,在我的具体情况下,我使用

java.lang.reflect.Method Function = Class.forName("be.qan.NN.ActivationFunctions").getMethod("sigmoid", double.class);

然后在更复杂的情况下调用它

return (java.lang.Double)this.Function.invoke(null, args);

java.lang.Object[] args = new java.lang.Object[] {activity};
someOtherFunction() + 234 + (java.lang.Double)Function.invoke(null, args);

活动是任意的双精度值。我正在考虑像SoftwareMonkey所做的那样,将其抽象一些并加以概括,但是目前我对它的方式很满意。三行代码,不需要类和接口,这还不错。


感谢Rob添加了code降价促销,我太急躁和愚蠢而找不到它;-)
yogibimbi

4

要在没有接口的情况下执行相同的操作,请执行以下操作:

class NameFuncPair
{
    public String name;                // name each func
    void   f(String x) {}              // stub gets overridden
    public NameFuncPair(String myName) { this.name = myName; }
}

public class ArrayOfFunctions
{
    public static void main(String[] args)
    {
        final A a = new A();
        final B b = new B();

        NameFuncPair[] fArray = new NameFuncPair[]
        {
            new NameFuncPair("A") { @Override void f(String x) { a.g(x); } },
            new NameFuncPair("B") { @Override void f(String x) { b.h(x); } },
        };

        // Go through the whole func list and run the func named "B"
        for (NameFuncPair fInstance : fArray)
        {
            if (fInstance.name.equals("B"))
            {
                fInstance.f(fInstance.name + "(some args)");
            }
        }
    }
}

class A { void g(String args) { System.out.println(args); } }
class B { void h(String args) { System.out.println(args); } }

1
为什么?这比以前提出的解决方案更为复杂,以前提出的解决方案每个替代方案仅需要一个匿名函数定义。您可以选择创建一个类和一个匿名函数定义。更糟糕的是,这是在代码中的两个不同位置完成的。您可能需要提供一些使用此方法的理由。
制造商史蒂夫(Steve)2015年


3

哇,为什么不创建一个Delegate类,考虑到我已经为java做过的事,就不那么难了,并使用它来传递T为返回类型的参数。很抱歉,但是作为一个通常只学习Java的C ++ / C#程序员,我需要函数指针,因为它们非常方便。如果您熟悉处理方法信息的任何类,则可以这样做。在Java库中,它将是java.lang.reflect.method。

如果您始终使用接口,则必须始终实现它。在事件处理中,实际上没有比从处理程序列表进行注册/注销更好的方法了,但是对于需要传递函数而不是值类型的委托,则使委托类可以处理超出接口的委托。


除非您显示代码详细信息,否则不是有用的答案。创建Delegate类有什么帮助?每个替代方案需要什么代码?
制造商史蒂夫(Steve)2015年

2

Java 8的答案中没有一个给出完整的,有凝聚力的示例,因此它来了。

声明接受“函数指针”的方法,如下所示:

void doCalculation(Function<Integer, String> calculation, int parameter) {
    final String result = calculation.apply(parameter);
}

通过为函数提供一个lambda表达式来调用它:

doCalculation((i) -> i.toString(), 2);


1

从Java8开始,您可以使用lambda,后者在官方SE 8 API中也具有库。

用法: 您只需要使用一个接口使用一种抽象方法。创建它的一个实例(您可能要使用已经提供的一个Java SE 8),如下所示:

Function<InputType, OutputType> functionname = (inputvariablename) {
... 
return outputinstance;
}

有关更多信息,请查阅文档:https : //docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html


1

在Java 8之前,类似于类函数指针的功能的最近替代品是一个匿名类。例如:

Collections.sort(list, new Comparator<CustomClass>(){
    public int compare(CustomClass a, CustomClass b)
    {
        // Logic to compare objects of class CustomClass which returns int as per contract.
    }
});

但是现在在Java 8中,我们有一个非常整洁的替代方案,称为lambda表达式,可以将其用作:

list.sort((a, b) ->  { a.isBiggerThan(b) } );

其中isBiggerThan是中的方法CustomClass。我们也可以在这里使用方法引用:

list.sort(MyClass::isBiggerThan);
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.