Jaki jest najbliższy substytut wskaźnika funkcji w Javie?


299

Mam metodę, która ma około dziesięciu wierszy kodu. Chcę stworzyć więcej metod, które robią dokładnie to samo, z wyjątkiem niewielkich obliczeń, które zmienią jeden wiersz kodu. Jest to idealna aplikacja do przekazywania wskaźnika funkcji w celu zastąpienia tej jednej linii, ale Java nie ma wskaźników funkcji. Jaka jest moja najlepsza alternatywa?


5
Java 8 będzie miała wyrażenia Lambda . Możesz przeczytać więcej o wyrażeniach lambda tutaj .
Marius

4
@Marius Nie do końca uważam, że wyrażenia lambda liczą się jako wskaźniki funkcji. Z ::drugiej strony operator ...
Facet z

Przepraszamy za późny komentarz;) - zwykle nie potrzebujesz do tego wskaźnika funkcji. Wystarczy użyć metody szablonu! ( en.wikipedia.org/wiki/Template_method_pattern )
isnot2bad

2
@ isnot2bad - patrząc na ten artykuł, wydaje się to przesadą - bardziej złożone niż podane tutaj odpowiedzi. W szczególności metoda szablonów wymaga utworzenia podklasy dla każdego alternatywnego obliczenia. Nie widzę OP, który stwierdził coś, co wymaga podklas ; chce po prostu stworzyć kilka metod i udostępnić większość implementacji. Jak pokazuje zaakceptowana odpowiedź, można to łatwo zrobić „na miejscu” (wewnątrz każdej metody), nawet przed Java 8 z lambdami.
ToolmakerSteve,

@ToolmakerSteve Akceptowane rozwiązanie wymaga również od klasy według obliczeń (nawet jeśli jest to tylko anonimowa klasa wewnętrzna). A wzorzec metody szablonu można również zrealizować przy użyciu anonimowych klas wewnętrznych, więc nie różni się on tak bardzo od przyjętego rozwiązania dotyczącego narzutu (wcześniejszego niż Java 8). Jest to raczej kwestia wzorca użytkowania i szczegółowych wymagań, których nie znamy. Doceniam przyjętą odpowiedź i chciałem tylko dodać inną możliwość do przemyślenia.
isnot2bad

Odpowiedzi:


269

Anonimowa klasa wewnętrzna

Załóżmy, że chcesz przekazać funkcję z Stringparametrem, który zwraca an int.
Najpierw musisz zdefiniować interfejs z funkcją jako jego jedynym elementem, jeśli nie możesz ponownie użyć istniejącego.

interface StringFunction {
    int func(String param);
}

Metoda, która pobiera wskaźnik, zaakceptowałaby StringFunctioninstancję w następujący sposób:

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

I nazwano by tak:

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

EDYCJA: W Javie 8 można to nazwać wyrażeniem lambda:

ref.takingMethod(param -> bodyExpression);

13
Nawiasem mówiąc, jest to przykład „Command Patern”. en.wikipedia.org/wiki/Command_Pattern
Ogre Psalm33

3
@Ogre Psalm33 Ta technika może być również wzorem strategii, w zależności od tego, jak go używasz. Różnica między wzorcem strategii a wzorcem poleceń .
Rory O'Kane,

2
Oto implementacja zamykająca dla Java 5, 6 i 7 mseifed.blogspot.se/2012/09/... Zawiera wszystko, o co można prosić ... Myślę, że jest niesamowita!
mmm

@SecretService: Ten link jest martwy.
Lawrence Dol

@LawrenceDol Tak, to prawda. Oto tablica klasy, której używam. pastebin.com/b1j3q2Lp
mmm

32

Dla każdego „wskaźnika funkcji” stworzyłbym małą klasę funktorów, która implementuje twoje obliczenia. Zdefiniuj interfejs, który będą implementować wszystkie klasy, i przekaż instancje tych obiektów do swojej większej funkcji. Jest to kombinacja „ wzorca poleceń ” i „ wzorca strategii ”.

Przykład @ sblundy jest dobry.


28

Gdy istnieje wstępnie zdefiniowana liczba różnych obliczeń, które można wykonać w tej jednej linii, użycie wyliczenia jest szybkim, ale jasnym sposobem na wdrożenie wzorca strategii.

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

Oczywiście deklaracja metody strategii, a także dokładnie jedna instancja każdej implementacji są zdefiniowane w jednej klasie / pliku.


24

Musisz stworzyć interfejs zapewniający funkcje, które chcesz przekazać. na przykład:

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

}

Następnie, gdy musisz przekazać funkcję, możesz zaimplementować ten interfejs:

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

Wreszcie funkcja mapy korzysta z funkcji przekazanej w funkcji 1 w następujący sposób:

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

Często możesz użyć Runnable zamiast własnego interfejsu, jeśli nie musisz przekazywać parametrów, lub możesz użyć różnych innych technik, aby parametry były mniej „ustalone”, ale zwykle jest to kompromis z bezpieczeństwem typu. (Lub możesz przesłonić konstruktor, aby obiekt funkcji przekazywał parametry w ten sposób .. istnieje wiele podejść, a niektóre działają lepiej w pewnych okolicznościach.)


4
Ta „odpowiedź” dotyczy bardziej zestawu problemów niż zestawu rozwiązań.
tchrist

18

Odniesienia do metod za pomocą ::operatora

W argumentach metody można używać odwołań do metod, w których metoda akceptuje interfejs funkcjonalny . Interfejs funkcjonalny to dowolny interfejs, który zawiera tylko jedną metodę abstrakcyjną. (Interfejs funkcjonalny może zawierać jedną lub więcej metod domyślnych lub metod statycznych.)

IntBinaryOperatorto funkcjonalny interfejs. Jego abstrakcyjna metoda applyAsIntprzyjmuje dwa ints jako parametry i zwraca an int. Math.maxakceptuje również dwa intsi zwraca an int. W tym przykładzie A.method(Math::max);sprawia , że parameter.applyAsIntwysyła do niego dwie wartości wejściowe Math.maxi zwraca wynik 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);
    }
}

Ogólnie możesz użyć:

method1(Class1::method2);

zamiast:

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

co jest skrótem od:

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

Aby uzyskać więcej informacji, zobacz :: :: operator (dwukropek) w Javie 8 i specyfikacji języka Java §15.13 .


15

Możesz to również zrobić (co w niektórych rzadkich przypadkach ma sens). Problem (i jest to duży problem) polega na tym, że tracisz wszelkie bezpieczeństwo korzystania z klasy / interfejsu i musisz zająć się przypadkiem, w którym metoda nie istnieje.

Ma tę „zaletę”, że można zignorować ograniczenia dostępu i wywoływać metody prywatne (nie pokazano w tym przykładzie, ale można wywoływać metody, których kompilator normalnie nie pozwala na wywołanie).

Ponownie, jest to rzadki przypadek, który ma sens, ale przy takich okazjach jest to miłe narzędzie.

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
Czasami używam tego do obsługi menu / GUI, ponieważ składnia metody jest o wiele prostsza niż anonimowa składnia klasy wewnętrznej. Jest to schludne, ale dodajesz złożoność refleksji, w którą niektórzy ludzie nie chcą zagłębiać się, więc bądź pewien, że dobrze to rozumiesz i masz jasne, tekstowe błędy dla każdego możliwego błędu.
Bill K

Możesz to zrobić bezpiecznie, używając ogólnych i nie potrzebujesz refleksji.
Luigi Plinge,

1
Nie widzę, w jaki sposób używanie generycznych i niestosowanie refleksów pozwoliłoby ci wywołać metodę według nazwy zawartej w ciągu?
TofuBeer,

1
@LigiigiPlinge - czy możesz podać fragment kodu tego, co masz na myśli?
ToolmakerSteve,

13

Jeśli masz tylko jedną linię, która jest inna, możesz dodać parametr, taki jak flaga i instrukcja if (flag), która wywołuje jedną linię lub drugą.


1
Odpowiedź javaslook wydaje się czystszym sposobem na zrobienie tego, jeśli więcej niż dwa warianty obliczeń. Lub jeśli ktoś chce osadzić kod w metodzie, wówczas wyliczenie dla różnych przypadków obsługiwanych przez metodę oraz przełącznik.
ToolmakerSteve,

1
@ToolmakerSteve prawda, chociaż dziś używałbyś lambdas w Javie 8.
Peter Lawrey


11

Nowe interfejsy funkcjonalne Java 8 i odniesienia do metod za pomocą ::operatora.

Java 8 jest w stanie zachować odwołania do metod (MyClass :: new) za pomocą wskaźników „ @ Functional Interface ”. Nie ma potrzeby używania tej samej nazwy metody, wymagany jest tylko ten sam podpis metody.

Przykład:

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

Co tu mamy?

  1. Interfejs funkcjonalny - jest to interfejs, opatrzony adnotacjami lub nie z @FunctionalInterface , który zawiera tylko jedną deklarację metody.
  2. Odnośniki do metod - to tylko specjalna składnia, wygląda tak, objectInstance :: methodName , nic więcej.
  3. Przykład użycia - tylko operator przypisania, a następnie wywołanie metody interfejsu.

NALEŻY KORZYSTAĆ Z INTERFEJSÓW FUNKCJONALNYCH TYLKO DLA SŁUCHAWCÓW I TYLKO DO TYCH!

Ponieważ wszystkie inne takie wskaźniki funkcji są bardzo złe dla czytelności kodu i zdolności do zrozumienia. Jednak czasami przydatne są bezpośrednie odniesienia do metod, na przykład foreach.

Istnieje kilka predefiniowanych interfejsów funkcjonalnych:

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

We wcześniejszych wersjach Java powinieneś wypróbować biblioteki Guava, które mają podobną funkcjonalność i składnię, jak wspomniano powyżej Adrian Petrescu.

Aby uzyskać dodatkowe badania, zobacz Arkusz Java 8

i dzięki The Guy with The Hat za link do specyfikacji języka Java §15.13 .


7
Ponieważ wszystkie inne ... są bardzo złe dla czytelności kodu ” to całkowicie bezpodstawne twierdzenie, a poza tym błędne.
Lawrence Dol

9

Odpowiedź @ sblundy jest świetna, ale anonimowe klasy wewnętrzne mają dwie małe wady, z których pierwszą jest to, że nie nadają się do ponownego użycia, a wtórna to nieporęczna składnia.

Zaletą jest to, że jego wzór rozszerza się na pełne klasy bez żadnych zmian w klasie głównej (tej, która wykonuje obliczenia).

Kiedy tworzysz nową klasę, możesz przekazać parametry do tej klasy, która może działać jako stałe w twoim równaniu - więc jeśli jedna z twoich klas wewnętrznych wygląda tak:

f(x,y)=x*y

ale czasami potrzebujesz takiego, który jest:

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

a może trzecia to:

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

zamiast tworzyć dwie anonimowe klasy wewnętrzne lub dodawać parametr „przejście”, możesz utworzyć jedną RZECZYWISTĄ klasę, którą tworzysz jako:

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

Po prostu zapisałby stałą w klasie i używałby jej w metodzie określonej w interfejsie.

W rzeczywistości, jeśli WIESZ, że twoja funkcja nie zostanie zapisana / ponownie użyta, możesz to zrobić:

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

Ale niezmienne klasy są bezpieczniejsze - nie mogę znaleźć uzasadnienia, aby uczynić taką klasę zmienną.

Naprawdę publikuję to tylko dlatego, że wzdrygam się za każdym razem, gdy słyszę anonimową klasę wewnętrzną - widziałem dużo zbędnego kodu, który był „wymagany”, ponieważ pierwszą rzeczą, którą zrobił programista, był anonimowy, gdy powinien był użyć prawdziwej klasy i nigdy przemyślał swoją decyzję.


Co? OP mówi o różnych obliczeniach (algorytmach; logice); wyświetlasz różne wartości (dane). Pokazujesz konkretny przypadek, w którym różnicę można włączyć do wartości, ale jest to nieuzasadnione uproszczenie stawianego problemu.
ToolmakerSteve,

6

Biblioteki Google Guava , które stają się bardzo popularne, mają ogólny obiekt Function i Predicate , który przepracowali w wielu częściach swojego API.


Ta odpowiedź byłaby bardziej przydatna, gdyby zawierała szczegóły kodu. Weź kod pokazany w zaakceptowanej odpowiedzi i pokaż, jak by to wyglądało za pomocą funkcji.
ToolmakerSteve,

4

Brzmi dla mnie jak wzorzec strategii. Sprawdź wzorce Java fluffycat.com.


4

Jedną z rzeczy, których naprawdę brakuje mi podczas programowania w Javie, są wywołania zwrotne funkcji. Jedną z sytuacji, w której potrzeba ich ciągłego prezentowania się była w rekurencyjnym przetwarzaniu hierarchii, w której chcesz wykonać określoną akcję dla każdego elementu. Jak chodzenie po drzewie katalogów lub przetwarzanie struktury danych. Minimalista we mnie nie lubi definiować interfejsu, a następnie implementacji dla każdego konkretnego przypadku.

Pewnego dnia zastanawiałem się, dlaczego nie? Mamy wskaźniki metod - obiekt Method. Dzięki optymalizacji kompilatorów JIT wywoływanie odblaskowe nie powoduje już znacznej utraty wydajności. A oprócz, powiedzmy, kopiowania pliku z jednej lokalizacji do drugiej, koszt wywołanej metody odbicia blednie.

Gdy się nad tym zastanowiłem, zdałem sobie sprawę, że wywołanie zwrotne w paradygmacie OOP wymaga powiązania obiektu i metody razem - wprowadź obiekt wywołania zwrotnego.

Sprawdź moje rozwiązanie oparte na odbiciu dla wywołań zwrotnych w Javie . Bezpłatnie do dowolnego użytku.


4

OK, ten wątek jest już wystarczająco stary, więc bardzo prawdopodobne, że moja odpowiedź nie jest pomocna w przypadku pytania. Ale ponieważ ten wątek pomógł mi znaleźć moje rozwiązanie, i tak go tutaj opublikuję.

Musiałem użyć zmiennej metody statycznej ze znanym wejściem i znanym wyjściem (oba podwójne ). Zatem znając pakiet metod i nazwę, mógłbym pracować w następujący sposób:

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

dla funkcji, która przyjmuje jedno podwójne jako parametr.

Tak więc w mojej konkretnej sytuacji zainicjowałem go

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

i wywołał go później w bardziej złożonej sytuacji z

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

gdzie aktywność jest dowolną podwójną wartością. Zastanawiam się nad tym, by zrobić to nieco bardziej abstrakcyjnie i uogólnić, tak jak zrobiło to SoftwareMonkey, ale obecnie jestem wystarczająco zadowolony z tego, jaki jest. Trzy linie kodu, brak klas i interfejsów, to nie jest takie złe.


dzięki Rob za dodanie codeprzeceny, byłem zbyt niecierpliwy i głupi, aby go znaleźć ;-)
yogibimbi

4

Aby zrobić to samo bez interfejsów dla szeregu funkcji:

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
Dlaczego? Jest to bardziej skomplikowane niż wcześniej proponowane rozwiązania, które po prostu potrzebują jednej anonimowej definicji funkcji dla każdej alternatywy. Alternatywnie można utworzyć definicję klasy i anonimowej funkcji. Co gorsza, odbywa się to w dwóch różnych lokalizacjach w kodzie. Możesz podać uzasadnienie dla zastosowania tego podejścia.
ToolmakerSteve,


3

Wow, dlaczego po prostu nie stworzyć klasy Delegate, która nie jest wcale taka trudna, biorąc pod uwagę, że już to zrobiłem dla java i użyć jej do przekazania parametru, w którym T jest typem zwracanym. Przykro mi, ale jako programista C ++ / C #, ogólnie po prostu uczący się języka Java, potrzebuję wskaźników funkcji, ponieważ są one bardzo przydatne. Jeśli znasz dowolną klasę, która zajmuje się informacjami o metodzie, możesz to zrobić. W bibliotekach Java byłoby to java.lang.reflect.method.

Jeśli zawsze korzystasz z interfejsu, zawsze musisz go zaimplementować. W obsłudze zdarzeń naprawdę nie ma lepszego sposobu na rejestrację / wyrejestrowanie z listy programów obsługi, ale dla delegatów, w których należy przekazać funkcje, a nie typ wartości, tworząc klasę delegata do obsługi jej w celu zdeklasowania interfejsu.


Nie jest to przydatna odpowiedź, chyba że pokażesz szczegóły kodu. Jak pomaga utworzenie klasy Delegat? Jaki kod jest wymagany dla każdej alternatywy?
ToolmakerSteve,

2

Żadna z odpowiedzi Java 8 nie dała pełnego, spójnego przykładu, więc oto nadchodzi.

Zadeklaruj metodę, która akceptuje „wskaźnik funkcji” w następujący sposób:

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

Wywołaj go, podając funkcję z wyrażeniem lambda:

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


1

Od wersji Java8 możesz używać lambdas, które również mają biblioteki w oficjalnym API SE 8.

Zastosowanie: Musisz użyć interfejsu z tylko jedną metodą abstrakcyjną. Zrób jego instancję (możesz użyć już dostarczonej wersji java SE 8) w następujący sposób:

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

Aby uzyskać więcej informacji sprawdź dokumentację: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html


1

Przed Javą 8 najbliższym substytutem funkcji przypominającej wskaźnik funkcji była klasa anonimowa. Na przykład:

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

Ale teraz w Javie 8 mamy bardzo zgrabną alternatywę znaną jako wyrażenie lambda , której można użyć jako:

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

gdzie isBiggerThan jest metodą w CustomClass. Możemy również użyć referencji metod tutaj:

list.sort(MyClass::isBiggerThan);
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.