¿Cuál es el sustituto más cercano para un puntero de función en Java?


299

Tengo un método que tiene aproximadamente diez líneas de código. Quiero crear más métodos que hagan exactamente lo mismo, excepto por un pequeño cálculo que cambiará una línea de código. Esta es una aplicación perfecta para pasar un puntero de función para reemplazar esa línea, pero Java no tiene punteros de función. ¿Cuál es mi mejor alternativa?


55
Java 8 tendrá expresiones Lambda . Puede leer más sobre las expresiones lambda aquí .
Marius

44
@ Mario No creo que las expresiones lambda cuenten como punteros de función. El ::operador, por otro lado ...
The Guy with The Hat

Perdón por el comentario tardío;) - por lo general, no necesita un puntero de función para eso. ¡Solo usa un método de plantilla! ( en.wikipedia.org/wiki/Template_method_pattern )
isnot2bad

2
@ isnot2bad - mirar ese artículo, parece excesivo - más complejo que las respuestas dadas aquí. Específicamente, el método de plantilla requiere crear una subclase para cada cálculo alternativo. No veo que OP haya declarado nada que requiera subclases ; simplemente quiere crear varios métodos y compartir la mayor parte de la implementación. Como muestra la respuesta aceptada, esto se hace fácilmente "en su lugar" (dentro de cada método), incluso antes de Java 8 con sus lambdas.
ToolmakerSteve

@ToolmakerSteve La solución aceptada también requiere una clase por cálculo (incluso si es solo una clase interna anónima). Y el patrón de método de plantilla también se puede realizar utilizando clases internas anónimas, por lo que no difiere mucho de la solución aceptada con respecto a la sobrecarga (antes de Java 8). Por lo tanto, se trata más del patrón de uso y los requisitos detallados, que no conocemos. Agradezco la respuesta aceptada y solo quería agregar otra posibilidad para pensar.
isnot2bad

Respuestas:


269

Clase interna anónima

Digamos que desea que se pase una función con un Stringparámetro que devuelve un int.
Primero debe definir una interfaz con la función como su único miembro, si no puede reutilizar una existente.

interface StringFunction {
    int func(String param);
}

Un método que toma el puntero solo aceptaría una StringFunctioninstancia como esta:

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

Y se llamaría así:

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

EDITAR: en Java 8, podría llamarlo con una expresión lambda:

ref.takingMethod(param -> bodyExpression);

13
Este es un ejemplo del "Comando Patern", por cierto. en.wikipedia.org/wiki/Command_Pattern
Ogre Psalm33

3
@Ogre Psalm33 Esta técnica también podría ser el Patrón de estrategia, dependiendo de cómo lo use. La diferencia entre el Patrón de estrategia y el Patrón de comando .
Rory O'Kane

2
Aquí hay una implementación de cierre para Java 5, 6 y 7 mseifed.blogspot.se/2012/09/... Contiene todo lo que uno podría pedir ... ¡Creo que es bastante increíble!
mmm

@SecretService: ese enlace está muerto.
Lawrence Dol

@LawrenceDol Sí, lo es. Aquí hay un pastebin de la clase que estoy usando. pastebin.com/b1j3q2Lp
mmm

32

Para cada "puntero de función", crearía una pequeña clase functor que implementa su cálculo. Defina una interfaz que implementarán todas las clases y pase instancias de esos objetos a su función más grande. Esta es una combinación del " patrón de comando " y el " patrón de estrategia ".

El ejemplo de @ sblundy es bueno.


28

Cuando hay un número predefinido de cálculos diferentes que puede hacer en esa línea, usar una enumeración es una forma rápida pero clara de implementar un patrón de estrategia.

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);
}

Obviamente, la declaración del método de estrategia, así como exactamente una instancia de cada implementación, se definen en una sola clase / archivo.


24

Debe crear una interfaz que proporcione las funciones que desea transmitir. p.ej:

/**
 * 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);

}

Luego, cuando necesite pasar una función, puede implementar esa interfaz:

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

Finalmente, la función de mapa usa la pasada en Function1 de la siguiente manera:

   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;
   }

A menudo puede usar Runnable en lugar de su propia interfaz si no necesita pasar parámetros, o puede usar varias otras técnicas para hacer que el parámetro cuente menos "fijo", pero generalmente es una compensación con la seguridad de tipo. (O puede anular el constructor para que su objeto de función pase en los parámetros de esa manera ... hay muchos enfoques, y algunos funcionan mejor en ciertas circunstancias).


44
Esta "respuesta" se refiere más al conjunto de problemas que al conjunto de soluciones.
tchrist

18

Referencias de métodos utilizando el ::operador

Puede usar referencias de métodos en argumentos de métodos donde el método acepta una interfaz funcional . Una interfaz funcional es cualquier interfaz que contiene un solo método abstracto. (Una interfaz funcional puede contener uno o más métodos predeterminados o métodos estáticos).

IntBinaryOperatorEs una interfaz funcional. Su método abstracto applyAsInt, acepta dos ints como sus parámetros y devuelve un int. Math.maxtambién acepta dos intsy devuelve un int. En este ejemplo, A.method(Math::max);hace parameter.applyAsIntenviar sus dos valores de entrada ay Math.maxdevolver el resultado de eso 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);
    }
}

En general, puedes usar:

method1(Class1::method2);

en vez de:

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

que es la abreviatura de:

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

Para obtener más información, consulte el operador :: (dos puntos) en Java 8 y la Especificación del lenguaje Java §15.13 .


15

También puede hacer esto (que en algunas ocasiones RARAS tiene sentido). El problema (y es un gran problema) es que pierde toda la seguridad de tipo de uso de una clase / interfaz y tiene que lidiar con el caso donde el método no existe.

Tiene el "beneficio" de que puede ignorar las restricciones de acceso y llamar a métodos privados (no se muestra en el ejemplo, pero puede llamar a métodos que el compilador normalmente no le permitiría llamar).

De nuevo, es raro que esto tenga sentido, pero en esas ocasiones es una buena herramienta.

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
Utilizo esto para el manejo de menús / GUI a veces porque la sintaxis del método es mucho más simple que la sintaxis anónima de clase interna. Está ordenado, pero está agregando la complejidad de la reflexión en la que algunas personas no quieren profundizar, así que asegúrese de hacerlo bien y tener errores de texto claros para cada posible condición de error.
Bill K

Puede escribir con seguridad usando genéricos y no necesita reflexión.
Luigi Plinge

1
¿No veo cómo usar genéricos y no usar reflexión le permitiría llamar a un método por un nombre contenido en una Cadena?
TofuBeer

1
@LuigiPlinge: ¿puede proporcionar un fragmento de código de lo que quiere decir?
ToolmakerSteve

13

Si tiene solo una línea que es diferente, puede agregar un parámetro como una marca y una instrucción if (marca) que llame a una línea u otra.


1
La respuesta de javaslook parece una forma más limpia de hacer esto, si hay más de dos variantes de cálculo. O si uno desea incrustar el código en el método, entonces una enumeración para los diferentes casos que maneja el método, y un interruptor.
ToolmakerSteve

1
@ToolmakerSteve cierto, aunque hoy usarías lambdas en Java 8.
Peter Lawrey


11

Nuevas interfaces funcionales de Java 8 y referencias de métodos utilizando el ::operador.

Java 8 puede mantener referencias de métodos (MyClass :: new) con punteros " @ Functional Interface ". No hay necesidad del mismo nombre de método, solo se requiere la misma firma de método.

Ejemplo:

@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();
    }
}

Entonces, ¿qué tenemos aquí?

  1. Interfaz funcional: esta es una interfaz, anotada o no con @FunctionalInterface , que contiene solo una declaración de método.
  2. Referencias de métodos: esto es solo una sintaxis especial, se ve así, objectInstance :: methodName , nada más y nada menos.
  3. Ejemplo de uso: solo un operador de asignación y luego una llamada al método de interfaz.

¡DEBE UTILIZAR INTERFACES FUNCIONALES PARA LOS ESCUCHADORES SOLO Y SOLO PARA ESO!

Debido a que todos los demás punteros de función son realmente malos para la legibilidad del código y para la capacidad de comprender. Sin embargo, las referencias directas de métodos a veces son útiles, con foreach, por ejemplo.

Existen varias interfaces funcionales predefinidas:

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);

Para versiones anteriores de Java, debe probar Guava Libraries, que tiene una funcionalidad y una sintaxis similares, como Adrian Petrescu ha mencionado anteriormente.

Para una investigación adicional, consulte Java 8 Cheatsheet

y gracias a The Guy with The Hat por el enlace §15.13 de la especificación del lenguaje Java .


77
" Porque todos los demás ... son realmente malos para la legibilidad del código " es una afirmación completamente infundada, y además errónea.
Lawrence Dol

9

La respuesta de @ sblundy es excelente, pero las clases internas anónimas tienen dos pequeños defectos, el principal es que tienden a no ser reutilizables y el secundario es una sintaxis voluminosa.

Lo bueno es que su patrón se expande en clases completas sin ningún cambio en la clase principal (la que realiza los cálculos).

Cuando crea una instancia de una nueva clase, puede pasar parámetros a esa clase que pueden actuar como constantes en su ecuación, por lo que si una de sus clases internas se ve así:

f(x,y)=x*y

pero a veces necesitas uno que sea:

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

y tal vez un tercio que es:

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

en lugar de crear dos clases internas anónimas o agregar un parámetro "paso a través", puede crear una sola clase ACTUAL que cree como:

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);

Simplemente almacenaría la constante en la clase y la usaría en el método especificado en la interfaz.

De hecho, si SABE que su función no se almacenará / reutilizará, puede hacer esto:

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

Pero las clases inmutables son más seguras: no puedo encontrar una justificación para hacer que una clase como esta sea mutable.

Realmente solo publico esto porque me avergüenzo cada vez que escucho una clase interna anónima: he visto un montón de código redundante que era "obligatorio" porque lo primero que hizo el programador fue permanecer en el anonimato cuando debería haber usado una clase real y nunca repensó su decisión.


¿Eh? OP está hablando de diferentes cálculos (algoritmos; lógica); Estás mostrando diferentes valores (datos). Muestra un caso específico en el que la diferencia puede incorporarse a un valor, pero esa es una simplificación injustificable del problema que se plantea.
ToolmakerSteve

6

Las bibliotecas de Google Guava , que se están volviendo muy populares, tienen un objeto genérico Function y Predicate que han trabajado en muchas partes de su API.


Esta respuesta sería más útil si proporcionara detalles del código. Tome el código que se muestra en la respuesta aceptada y muestre cómo se vería usando Function.
ToolmakerSteve

4

Suena como un patrón de estrategia para mí. Echa un vistazo a los patrones de Java fluffycat.com.


4

Una de las cosas que realmente extraño cuando programo en Java son las devoluciones de llamada de funciones. Una situación en la que la necesidad de estos seguía presentándose era en el procesamiento recursivo de las jerarquías donde desea realizar alguna acción específica para cada elemento. Como recorrer un árbol de directorios o procesar una estructura de datos. El minimalista dentro de mí odia tener que definir una interfaz y luego una implementación para cada caso específico.

Un día me pregunté por qué no. Tenemos punteros de método: el objeto Método. Con la optimización de los compiladores JIT, la invocación reflexiva ya no conlleva una gran penalización de rendimiento. Y además de, digamos, copiar un archivo de una ubicación a otra, el costo de la invocación del método reflejado es insignificante.

Mientras lo pensaba más, me di cuenta de que una devolución de llamada en el paradigma OOP requiere unir un objeto y un método: ingrese el objeto Callback.

Echa un vistazo a mi solución basada en la reflexión para Callbacks en Java . Gratis para cualquier uso.


4

Bien, este hilo ya es lo suficientemente viejo, así que muy probablemente mi respuesta no sea útil para la pregunta. Pero dado que este hilo me ayudó a encontrar mi solución, lo pondré aquí de todos modos.

Necesitaba usar un método estático variable con entrada y salida conocidas (ambas dobles ). Entonces, conociendo el paquete y el nombre del método, podría trabajar de la siguiente manera:

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

para una función que acepta un doble como parámetro.

Entonces, en mi situación concreta, lo inicialicé con

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

e invocado más tarde en una situación más compleja con

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);

donde la actividad es un valor doble arbitrario. Estoy pensando en hacer esto un poco más abstracto y generalizarlo, como lo ha hecho SoftwareMonkey, pero actualmente estoy bastante contento con la forma en que es. Tres líneas de código, sin clases e interfaces necesarias, eso no es tan malo.


gracias Rob por agregar el codedescuento, estaba demasiado impaciente y estúpido para encontrarlo ;-)
yogibimbi

4

Para hacer lo mismo sin interfaces para una variedad de funciones:

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
¿Por qué? Esto es más complicado que las soluciones propuestas anteriormente, que simplemente necesitan una definición de función anónima por alternativa. Por alternativa, crea una clase y una definición de función anónima. Peor aún, esto se hace en dos ubicaciones diferentes en el código. Es posible que desee proporcionar alguna justificación para utilizar este enfoque.
ToolmakerSteve


3

Wow, ¿por qué no simplemente crear una clase Delegate que no es tan difícil dado que ya lo hice para Java y usarla para pasar el parámetro donde T es el tipo de retorno? Lo siento, pero como programador de C ++ / C # en general solo estoy aprendiendo Java, necesito punteros de función porque son muy útiles. Si está familiarizado con alguna clase que se ocupe de la información del método, puede hacerlo. En las bibliotecas de Java eso sería java.lang.reflect.method.

Si siempre usa una interfaz, siempre tiene que implementarla. En el manejo de eventos, realmente no hay una mejor manera de registrar / anular el registro de la lista de manejadores, sino para los delegados a los que necesita pasar funciones y no el tipo de valor, lo que hace que una clase de delegado lo maneje para superar una interfaz.


No es una respuesta útil, a menos que muestre los detalles del código. ¿Cómo ayuda la creación de una clase Delegado? ¿Qué código se requiere por alternativa?
ToolmakerSteve

2

Ninguna de las respuestas de Java 8 ha dado un ejemplo completo y coherente, así que aquí viene.

Declare el método que acepta el "puntero de función" de la siguiente manera:

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

Llámalo proporcionando la función con una expresión lambda:

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


1

Desde Java8, puede usar lambdas, que también tienen bibliotecas en la API oficial de SE 8.

Uso: debe utilizar una interfaz con un solo método abstracto. Haga una instancia de él (es posible que desee usar el java SE 8 ya proporcionado) de esta manera:

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

Para obtener más información, consulte la documentación: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html


1

Antes de Java 8, el sustituto más cercano para la funcionalidad tipo puntero de función era una clase anónima. Por ejemplo:

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.
    }
});

Pero ahora en Java 8 tenemos una alternativa muy clara conocida como expresión lambda , que se puede usar como:

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

donde isBiggerThan es un método en CustomClass. También podemos usar referencias de métodos aquí:

list.sort(MyClass::isBiggerThan);
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.