Qual è il sostituto più vicino per un puntatore a funzione in Java?


299

Ho un metodo che è di circa dieci righe di codice. Voglio creare più metodi che facciano esattamente la stessa cosa, ad eccezione di un piccolo calcolo che cambierà una riga di codice. Questa è un'applicazione perfetta per passare un puntatore a funzione per sostituire quella riga, ma Java non ha puntatori a funzione. Qual è la mia migliore alternativa?


5
Java 8 avrà espressioni Lambda . Puoi leggere di più sulle espressioni lambda qui .
Marius,

4
@Marius Non credo che le espressioni lambda contino come puntatori a funzioni. L' ::operatore, d'altra parte ...
The Guy with The Hat

Ci scusiamo per il commento tardivo;) - di solito, non è necessario un puntatore a funzione per quello. Usa un metodo modello! ( en.wikipedia.org/wiki/Template_method_pattern )
isnot2bad

2
@ isnot2bad - guardando quell'articolo, sembra eccessivo - più complesso delle risposte fornite qui. In particolare, il metodo modello richiede la creazione di una sottoclasse per ogni calcolo alternativo. Non vedo OP che ha dichiarato tutto ciò che richiede sottoclassi ; vuole semplicemente creare diversi metodi e condividere gran parte dell'implementazione. Come mostra la risposta accettata, questo è facilmente fatto "sul posto" (all'interno di ogni metodo), anche prima di Java 8 con i suoi lambdas.
ToolmakerSteve

@ToolmakerSteve La soluzione accettata richiede anche la classe per calcolo (anche se è solo una classe interna anonima). E il modello di metodo del modello può anche essere realizzato usando classi interne anonime, quindi non differisce molto dalla soluzione accettata relativa al sovraccarico (prima di Java 8). Quindi è più una questione del modello di utilizzo e dei requisiti dettagliati, che non conosciamo. Apprezzo la risposta accettata e volevo solo aggiungere un'altra possibilità a cui pensare.
Isnot2bad,

Risposte:


269

Classe interna anonima

Supponi di voler passare una funzione con un Stringparametro che restituisce un int.
Per prima cosa devi definire un'interfaccia con la funzione come unico membro, se non puoi riutilizzarne una esistente.

interface StringFunction {
    int func(String param);
}

Un metodo che utilizza il puntatore accetterebbe l' StringFunctionistanza in questo modo:

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

E si chiamerebbe così:

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

EDIT: in Java 8, è possibile chiamarlo con un'espressione lambda:

ref.takingMethod(param -> bodyExpression);

13
Questo è un esempio del "Comando Patern", tra l'altro. it.wikipedia.org/wiki/Command_Pattern
Ogre Psalm33

3
@Ogre Psalm33 Questa tecnica potrebbe anche essere il modello di strategia, a seconda di come la usi. La differenza tra il modello di strategia e il modello di comando .
Rory O'Kane,

2
Ecco un'implementazione di chiusura per Java 5, 6 e 7 mseifed.blogspot.se/2012/09/… Contiene tutto ciò che si potrebbe chiedere ... Penso che sia davvero fantastico!
mmm

@SecretService: quel link è morto.
Lawrence Dol,

@LawrenceDol Sì, lo è. Ecco un pastebin della classe che sto usando. pastebin.com/b1j3q2Lp
mmm

32

Per ogni "puntatore a funzione", creerei una piccola classe di funzione che implementa il tuo calcolo. Definisci un'interfaccia che verranno implementate da tutte le classi e passa le istanze di quegli oggetti nella tua funzione più grande. Questa è una combinazione di " modello di comando " e " modello di strategia ".

L'esempio di @ sblundy è buono.


28

Quando esiste un numero predefinito di calcoli diversi che è possibile eseguire in quella riga, l'utilizzo di un enum è un modo rapido ma chiaro per implementare un modello di strategia.

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

Ovviamente, la dichiarazione del metodo di strategia, così come esattamente un'istanza di ogni implementazione, sono tutti definiti in una singola classe / file.


24

È necessario creare un'interfaccia che fornisca le funzioni che si desidera passare. per esempio:

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

}

Quindi, quando è necessario passare una funzione, è possibile implementare tale interfaccia:

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

Infine, la funzione map utilizza il passato in Function1 come segue:

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

È spesso possibile utilizzare Runnable anziché la propria interfaccia se non è necessario passare parametri, oppure è possibile utilizzare varie altre tecniche per rendere il conteggio dei parametri meno "fisso", ma di solito è un compromesso con la sicurezza del tipo. (Oppure puoi sovrascrivere il costruttore affinché il tuo oggetto funzione passi i parametri in quel modo .. ci sono molti approcci e alcuni funzionano meglio in determinate circostanze.)


4
Questa "risposta" riguarda più il set di problemi che il set di soluzioni.
tchrist

18

Riferimenti a metodi mediante l' ::operatore

È possibile utilizzare i riferimenti di metodo negli argomenti del metodo in cui il metodo accetta un'interfaccia funzionale . Un'interfaccia funzionale è qualsiasi interfaccia che contiene solo un metodo astratto. (Un'interfaccia funzionale può contenere uno o più metodi predefiniti o metodi statici.)

IntBinaryOperatorè un'interfaccia funzionale. Il suo metodo astratto applyAsInt, accetta due ints come parametri e restituisce un int. Math.maxaccetta anche due se intrestituisce un int. In questo esempio, A.method(Math::max);rende parameter.applyAsIntinvia i suoi due valori di ingresso per Math.maxe restituire il risultato di tale 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);
    }
}

In generale, puoi usare:

method1(Class1::method2);

invece di:

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

che è l'abbreviazione di:

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

Per ulteriori informazioni, consultare l' operatore :: (doppio punto) in Java 8 e Java Language Specification §15.13 .


15

Puoi anche farlo (che in alcune occasioni RARE ha senso). Il problema (ed è un grosso problema) è che perdi tutta la sicurezza tipica dell'uso di una classe / interfaccia e devi affrontare il caso in cui il metodo non esiste.

Ha il "vantaggio" che puoi ignorare le restrizioni di accesso e chiamare metodi privati ​​(non mostrati nell'esempio, ma puoi chiamare metodi che il compilatore normalmente non ti lascerebbe chiamare).

Ancora una volta, è un caso raro che questo abbia un senso, ma in quelle occasioni è uno strumento piacevole da avere.

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
Lo uso per la gestione dei menu / le GUI a volte perché la sintassi del metodo è molto più semplice della sintassi anonima della classe interna. È pulito, ma stai aggiungendo la complessità della riflessione in cui alcune persone non vogliono approfondire, quindi assicurati di aver capito bene e di avere chiari errori testuali per ogni possibile condizione di errore.
Bill K,

Puoi farlo in modo sicuro usando i generici e non hai bisogno di riflessione.
Luigi Plinge,

1
Non riesco a vedere come usare generici e non usare reflection ti lascerebbe chiamare un metodo con un nome contenuto in una stringa?
TofuBeer,

1
@LuigiPlinge - puoi fornire uno snippet di codice di cosa intendi?
ToolmakerSteve

13

Se hai solo una linea diversa, puoi aggiungere un parametro come un flag e un'istruzione if (flag) che chiama una linea o l'altra.


1
La risposta di javaslook sembra un modo più pulito per farlo, se più di due varianti di calcolo. O se si desidera incorporare il codice nel metodo, quindi un enum per i diversi casi gestiti dal metodo e un interruttore.
ToolmakerSteve

1
@ToolmakerSteve true, anche se oggi useresti lambdas in Java 8.
Peter Lawrey,


11

Nuove interfacce funzionali e riferimenti ai metodi Java 8 mediante l' ::operatore.

Java 8 è in grado di mantenere i riferimenti ai metodi (MyClass :: new) con puntatori " @ Functional Interface ". Non è necessario lo stesso nome del metodo, è richiesta solo la stessa firma del metodo.

Esempio:

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

Quindi, cosa abbiamo qui?

  1. Interfaccia funzionale: si tratta di un'interfaccia, annotata o meno con @FunctionalInterface , che contiene solo una dichiarazione del metodo.
  2. Riferimenti ai metodi - questa è solo una sintassi speciale, assomiglia a questa, objectInstance :: methodName , niente di più niente di meno.
  3. Esempio di utilizzo: solo un operatore di assegnazione e quindi interfaccia con il metodo di chiamata.

DOVREI UTILIZZARE LE INTERFACCE FUNZIONALI SOLO PER GLI ASCOLTI E SOLO PER QUESTO!

Perché tutti gli altri puntatori di funzioni di questo tipo sono davvero dannosi per la leggibilità del codice e per la capacità di comprensione. Tuttavia, a volte i riferimenti ai metodi diretti sono utili, ad esempio foreach.

Esistono diverse interfacce funzionali predefinite:

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

Per le versioni precedenti di Java, dovresti provare Guava Libraries, che ha funzionalità e sintassi simili, come Adrian Petrescu ha menzionato sopra.

Per ulteriori ricerche, consultare il foglio informativo di Java 8

e grazie al link The Guy with The Hat for the Java Language Specification §15.13 .


7
" Perché tutti gli altri ... sono davvero dannosi per la leggibilità del codice " è un'affermazione completamente priva di fondamento, e sbagliata, inoltre.
Lawrence Dol,

9

La risposta di @ sblundy è ottima, ma le classi interne anonime hanno due piccoli difetti, il principale è che tendono a non essere riutilizzabili e il secondario è una sintassi voluminosa.

La cosa bella è che il suo modello si espande in classi complete senza alcun cambiamento nella classe principale (quella che esegue i calcoli).

Quando crei un'istanza di una nuova classe puoi passare parametri in quella classe che può agire come costanti nella tua equazione, quindi se una delle tue classi interne appare così:

f(x,y)=x*y

ma a volte hai bisogno di uno che sia:

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

e forse un terzo che è:

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

piuttosto che creare due classi interne anonime o aggiungere un parametro "passthrough", puoi creare una singola classe ACTUAL che crei un'istanza come:

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

Memorizzerebbe semplicemente la costante nella classe e la userebbe nel metodo specificato nell'interfaccia.

In effetti, se SAPETE che la vostra funzione non sarà memorizzata / riutilizzata, potreste farlo:

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

Ma le classi immutabili sono più sicure: non riesco a trovare una giustificazione per rendere una classe come questa mutevole.

Pubblica questo post solo perché sento rabbrividire ogni volta che sento una classe interna anonima - Ho visto un sacco di codice ridondante che era "Richiesto" perché la prima cosa che il programmatore ha fatto è diventata anonima quando avrebbe dovuto usare una classe reale e mai ripensò alla sua decisione.


Eh? OP sta parlando di diversi calcoli (algoritmi; logica); stai mostrando valori (dati) diversi. Si mostra un caso specifico in cui la differenza può essere incorporata in un valore, ma si tratta di una semplificazione ingiustificabile del problema posto.
ToolmakerSteve

6

Le librerie di Google Guava , che stanno diventando molto popolari, hanno un oggetto generico di funzione e predicato che hanno lavorato in molte parti della loro API.


Questa risposta sarebbe più utile se fornisse i dettagli del codice. Prendi il codice mostrato nella risposta accettata e mostra come apparirebbe usando la funzione.
ToolmakerSteve

4

Mi sembra un modello strategico. Dai un'occhiata ai modelli Java di fluffycat.com.


4

Una delle cose che mi manca davvero quando programmo in Java sono i callback di funzioni. Una situazione in cui la necessità di questi continuava a presentarsi era nell'elaborazione ricorsiva di gerarchie in cui si desidera eseguire alcune azioni specifiche per ciascun elemento. Come camminare su un albero di directory o elaborare una struttura di dati. Il minimalista dentro di me odia dover definire un'interfaccia e quindi un'implementazione per ogni caso specifico.

Un giorno mi sono ritrovato a chiedermi perché no? Abbiamo puntatori a metodi: l'oggetto Method. Con l'ottimizzazione dei compilatori JIT, l'invocazione riflessiva in realtà non comporta più un'enorme penalità di prestazione. E oltre a, per esempio, copiare un file da una posizione a un'altra, il costo dell'invocazione del metodo riflesso impallidisce.

Mentre ci pensavo di più, mi sono reso conto che un callback nel paradigma OOP richiede l'associazione di un oggetto e un metodo insieme: immettere l'oggetto Callback.

Scopri la mia soluzione basata sulla riflessione per Callbacks in Java . Gratuito per qualsiasi uso.


4

OK, questo thread è già abbastanza vecchio, quindi molto probabilmente la mia risposta non è utile per la domanda. Ma poiché questo thread mi ha aiutato a trovare la mia soluzione, la metterò comunque qui.

Avevo bisogno di usare un metodo statico variabile con input noto e output noto (entrambi doppi ). Quindi, conoscendo il pacchetto e il nome del metodo, ho potuto lavorare come segue:

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

per una funzione che accetta un doppio come parametro.

Quindi, nella mia situazione concreta, l'ho inizializzato con

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

e lo ha invocato più tardi in una situazione più complessa 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);

dove l'attività è un doppio valore arbitrario. Sto forse pensando di fare un po 'di più in astratto e di generalizzarlo, come ha fatto SoftwareMonkey, ma attualmente sono abbastanza contento di come è. Tre righe di codice, senza classi e interfacce necessarie, non è poi così male.


grazie Rob per aver aggiunto il codemarkdown, ero troppo impaziente e stupido per trovarlo ;-)
Yogibimbi,

4

Per fare la stessa cosa senza interfacce per una serie di funzioni:

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
Perché? Questo è più complicato delle soluzioni precedentemente proposte, che richiedono semplicemente una definizione di funzione anonima per alternativa. In alternativa, si crea una classe e una definizione di funzione anonima. Peggio ancora, questo viene fatto in due diverse posizioni nel codice. Potresti voler fornire una giustificazione per l'utilizzo di questo approccio.
ToolmakerSteve


3

Caspita, perché non creare semplicemente una classe delegata che non è poi così difficile dato che l'ho già fatto per Java e usarla per passare il parametro dove T è il tipo restituito. Mi dispiace, ma come programmatore C ++ / C # in generale solo imparando Java, ho bisogno di puntatori di funzione perché sono molto utili. Se hai familiarità con qualsiasi classe che si occupa delle informazioni sul metodo, puoi farlo. Nelle librerie Java che sarebbe java.lang.reflect.method.

Se usi sempre un'interfaccia, devi sempre implementarla. Nella gestione degli eventi non esiste davvero un modo migliore per registrare / annullare la registrazione dall'elenco dei gestori, ma per i delegati in cui è necessario passare funzioni e non il tipo di valore, rendendo una classe delegata per gestirla per surclassare un'interfaccia.


Non è una risposta utile, a meno che non mostri i dettagli del codice. In che modo la creazione di una classe delegata aiuta? Quale codice è richiesto per alternativa?
ToolmakerSteve

2

Nessuna delle risposte di Java 8 ha fornito un esempio completo e coerente, quindi ecco che arriva.

Dichiarare il metodo che accetta il "puntatore a funzione" come segue:

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

Chiamalo fornendo alla funzione un'espressione lambda:

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


1

Da Java8, puoi usare lambdas, che ha anche librerie nell'API SE 8 ufficiale.

Utilizzo: è necessario utilizzare un'interfaccia con un solo metodo astratto. Creane un'istanza (potresti voler usare quella java SE 8 già fornita) in questo modo:

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

Per ulteriori informazioni, consulta la documentazione: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html


1

Prima di Java 8, il sostituto più vicino alla funzionalità simile a un puntatore a funzione era una classe anonima. Per esempio:

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

Ma ora in Java 8 abbiamo un'alternativa molto chiara conosciuta come espressione lambda , che può essere usata come:

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

dove isBiggerThan è un metodo in CustomClass. Possiamo anche usare i riferimenti ai metodi qui:

list.sort(MyClass::isBiggerThan);
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.