Java'daki bir işlev işaretçisi için en yakın alternatif nedir?


299

On satır kod hakkında bir yöntem var. Bir kod satırını değiştirecek küçük bir hesaplama dışında, aynı şeyi yapan daha fazla yöntem oluşturmak istiyorum. Bu, bir satırın yerini almak için bir işlev işaretçisini geçmek için mükemmel bir uygulamadır, ancak Java'nın işlev işaretçileri yoktur. En iyi alternatifim nedir?


5
Java 8'in Lambda İfadeleri olacaktır . Burada lambda ifadeleri hakkında daha fazla bilgi edinebilirsiniz .
Marius

4
@Marius Lamda ifadelerinin işlev göstergeleri olarak sayıldığını pek sanmıyorum. Öte yandan operatör, ...::
Guy Hat ile

Geç yorum için özür dilerim;) - genellikle bunun için bir işlev işaretçisine ihtiyacınız yoktur. Sadece bir şablon yöntemi kullanın! ( en.wikipedia.org/wiki/Template_method_pattern )
isnot2bad

2
@ isnot2bad - bu makaleye bakmak, aşırıya kaçmak gibi görünüyor - burada verilen cevaplardan daha karmaşık. Özellikle, şablon yöntemi her alternatif hesaplama için bir alt sınıfın oluşturulmasını gerektirir . OP'nin alt sınıf gerektiren bir şey ifade ettiğini görmüyorum ; sadece birkaç yöntem oluşturmak ve uygulamanın çoğunu paylaşmak istiyor . Kabul edilen cevabın gösterdiği gibi, bu, lambdaslarıyla Java 8'den önce bile (her yöntemin içinde) kolayca "yerinde" yapılır.
ToolmakerSteve

@ToolmakerSteve Kabul edilen çözüm, hesaplama başına sınıf gerektirir (yalnızca anonim bir iç sınıf olsa bile). Ve şablon yöntemi kalıbı anonim iç sınıflar kullanılarak da gerçekleştirilebilir, bu nedenle (Java 8'den önce) ek yük ile ilgili kabul edilen çözümden çok farklı değildir. Bu daha çok, bilmediğimiz kullanım şekli ve ayrıntılı gereksinimlerle ilgili bir soru. Kabul edilen cevabı takdir ediyorum ve sadece düşünmek için başka bir olasılık eklemek istedim.
isnot2bad

Yanıtlar:


269

İsimsiz iç sınıf

Diyelim ki bir işlev Stringdöndüren bir parametreyle bir işleve sahip olmak istiyorsunuz int.
Öncelikle, mevcut bir arabirimi yeniden kullanamıyorsanız, işleve sahip bir arabirimi tek üyesi olarak tanımlamanız gerekir.

interface StringFunction {
    int func(String param);
}

İşaretçiyi alan bir yöntem aşağıdaki StringFunctiongibi bir örneği kabul eder :

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

Ve şöyle denir:

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

DÜZENLEME: Java 8'de lambda ifadesiyle çağırabilirsiniz:

ref.takingMethod(param -> bodyExpression);

13
Bu arada "Komuta Paterni" nin bir örneği. en.wikipedia.org/wiki/Command_Pattern
Ogre Psalm33

3
@Ogre Psalm33 Bu teknik, onu nasıl kullandığınıza bağlı olarak Strateji Paterni de olabilir. Strateji Düzeni ile Komut Düzeni arasındaki fark .
Rory O'Kane

2
İşte Java 5, 6 için bir kapatma uygulamasıdır ve 7 mseifed.blogspot.se/2012/09/... Tüm biri sormak için içeriyor ... Ben oldukça harika olduğunu düşünüyorum!
mmm

@SecretService: Bu bağlantı öldü.
Lawrence Dol

@LawrenceDol Evet, öyle. İşte kullandığım sınıfın bir çöp kutusu. pastebin.com/b1j3q2Lp
mmm

32

Her bir "fonksiyon işaretçisi" için, hesaplamanızı uygulayan küçük bir functor sınıfı oluştururdum. Tüm sınıfların uygulayacağı bir arabirim tanımlayın ve bu nesnelerin örneklerini daha büyük işlevinize geçirin. Bu, " komut paterni " ve " strateji paterni " nin bir kombinasyonudur .

@ sblundy örneği iyi.


28

Bir satırda yapabileceğiniz önceden tanımlanmış sayıda farklı hesaplama olduğunda, bir numaralandırma kullanmak bir strateji modeli uygulamak için hızlı, ancak açık bir yoldur.

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

Açıkçası, strateji yöntemi beyanı ve her uygulamanın tam olarak bir örneği tek bir sınıfta / dosyada tanımlanmıştır.


24

İletmek istediğiniz işlevleri sağlayan bir arabirim oluşturmanız gerekir. Örneğin:

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

}

Ardından, bir işlevi geçirmeniz gerektiğinde, bu arabirimi uygulayabilirsiniz:

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

Son olarak, map işlevi, İşlev1'de iletilen aşağıdaki gibi kullanır:

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

Parametreleri iletmeniz gerekmiyorsa, genellikle kendi arabiriminiz yerine Runnable'ı kullanabilirsiniz veya parametre sayısını daha az "sabit" hale getirmek için çeşitli teknikler kullanabilirsiniz, ancak genellikle tür güvenliği olan bir ödünleşmedir. (Ya da işlev nesnenizin parametrelere bu şekilde geçmesi için yapıcıyı geçersiz kılabilirsiniz. Çok sayıda yaklaşım vardır ve bazıları belirli durumlarda daha iyi çalışır.)


4
Bu "cevap" , çözüm kümesinden daha çok sorun kümesiyle ilgilidir .
tchrist

18

::İşleci kullanarak yöntem başvuruları

Yöntemin işlevsel bir arabirimi kabul ettiği yöntem bağımsız değişkenlerinde yöntem başvurularını kullanabilirsiniz . İşlevsel arabirim, yalnızca bir soyut yöntem içeren herhangi bir arabirimdir. (İşlevsel bir arabirim bir veya daha fazla varsayılan yöntem veya statik yöntem içerebilir.)

IntBinaryOperatorişlevsel bir arayüzdür. Soyut yöntemi, applyAsIntiki ints'yi parametre olarak kabul eder ve bir int. Math.maxayrıca iki ints'yi kabul eder ve bir int. Bu örnekte, A.method(Math::max);yapar parameter.applyAsIntonun iki giriş değerlerini göndermek Math.maxve bunun sonucu döndüren 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);
    }
}

Genel olarak şunları kullanabilirsiniz:

method1(Class1::method2);

onun yerine:

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

kısaltması:

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

Daha fazla bilgi için bkz . Java 8'de :: (çift kolonlu ) operatörü ve Java Dil Spesifikasyonu §15.13 .


15

Bunu da yapabilirsiniz (bazı nadir durumlarda mantıklıdır). Sorun (ve büyük bir sorundur), bir sınıf / arabirim kullanmanın tüm tip güvenliğini kaybetmeniz ve yöntemin mevcut olmadığı durumla uğraşmanız gerektiğidir.

Erişim kısıtlamalarını göz ardı edebileceğiniz ve özel yöntemleri çağırabileceğiniz "yarar" vardır (örnekte gösterilmemiştir, ancak derleyicinin normalde aramanıza izin vermeyeceği yöntemleri çağırabilirsiniz).

Yine, bunun mantıklı olduğu nadir bir durumdur, ancak bu durumlarda sahip olmak güzel bir araçtır.

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
Bu bazen yöntem sözdizimi anonim iç sınıf sözdiziminden çok daha basit olduğu için menü işleme / GUI'ler için kullanın. Düzgün, ancak bazı insanların kazmak istemediği yansıma karmaşıklığını ekliyorsunuz, bu yüzden doğru yaptığınızdan ve olası her hata durumu için açık, metinsel hatalarınız olduğundan emin olun.
Bill K

Jenerikler kullanarak güvenli bir şekilde yazabilirsiniz ve düşünmenize gerek yoktur.
Luigi Plinge

1
Nasıl jenerik ve yansıma kullanarak bir dizede bulunan bir adla bir yöntem çağırmak için izin göremiyorum?
TofuBeer

1
@LuigiPlinge - ne demek istediğinizin kod parçacığını sağlayabilir misiniz?
ToolmakerSteve

13

Farklı bir satırınız varsa, bayrak ve bir satırı veya diğerini çağıran bir if (flag) ifadesi gibi bir parametre ekleyebilirsiniz.


1
javaslook'un yanıtı, ikiden fazla hesaplama varyantı varsa bunu yapmanın daha temiz bir yolu gibi görünüyor. Veya kodu yönteme gömmek isterse, yöntemin işlediği farklı durumlar için bir numaralandırma ve bir anahtar.
ToolmakerSteve

1
@ToolmakerSteve true, bugün Java 8'de lambdas kullanacaksınız
Peter Lawrey


11

Operatörü kullanarak yeni Java 8 Fonksiyonel Arayüzleri ve Yöntem Referansları:: .

Java 8, " @ Fonksiyonel Arayüz " işaretçileriyle yöntem referanslarını (MyClass :: new) koruyabilir . Aynı yöntem adına gerek yoktur, sadece aynı yöntem imzası gereklidir.

Misal:

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

Peki, burada ne var?

  1. İşlevsel Arabirim - bu, yalnızca bir yöntem bildirimi içeren @FunctionalInterface ile açıklamalı veya açıklanmamış arabirimdir .
  2. Yöntem Referansları - bu sadece özel bir sözdizimidir, şöyle görünür, objectInstance :: methodName , başka bir şey daha az.
  3. Kullanım örneği - sadece bir atama operatörü ve daha sonra arayüz yöntemi çağrısı.

DİNLEYİCİLER İÇİN SADECE VE YALNIZCA FONKSİYONEL ARAYÜZLERİ KULLANMALISINIZ!

Çünkü diğer tüm fonksiyon göstergeleri kod okunabilirliği ve anlama yeteneği açısından gerçekten kötüdür. Bununla birlikte, doğrudan yöntem referansları bazen örneğin foreach ile kullanışlı olabilir.

Önceden tanımlanmış birkaç Fonksiyonel Arayüz vardır:

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

Önceki Java sürümleri için, Adrian Petrescu'nun yukarıda belirttiği gibi benzer işlevselliğe ve sözdizimine sahip Guava Kütüphanelerini denemelisiniz.

Ek araştırmalar için Java 8 Cheatsheet'e bakın

Java Dil Şartnamesi §15.13 bağlantısı için Şapkalı Adam'a teşekkürler .


7
" Çünkü diğerleri ... kod okunabilirliği için gerçekten kötü " tamamen temelsiz bir iddia ve yanlış yanı sıra.
Lawrence Dol

9

@ sblundy'nin cevabı harika, ama anonim iç sınıfların iki küçük kusuru var, birincisi tekrar kullanılabilir olma eğiliminde olmaları ve ikincisi hantal bir sözdizimidir.

Güzel olan şey, deseninin ana sınıfta (hesaplamaları gerçekleştiren) herhangi bir değişiklik yapmadan tam sınıflara genişlemesidir.

Yeni bir sınıf oluşturduğunuzda, bu sınıfa denkleminizde sabit olarak işlev görebilen parametreler iletebilirsiniz - böylece iç sınıflarınızdan biri şöyle görünürse:

f(x,y)=x*y

ancak bazen aşağıdakilere ihtiyacınız vardır:

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

ve belki de üçte biri:

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

iki anonim iç sınıf yapmak veya bir "geçidi" parametresi eklemek yerine, aşağıdaki gibi somutlaştırdığınız tek bir ACTUAL sınıfı yapabilirsiniz:

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

Sadece sabiti sınıfta depolar ve arayüzde belirtilen yöntemde kullanır.

Aslında, işlevinizin saklanmayacağını / yeniden kullanılmayacağını BILIRSENIZ bunu yapabilirsiniz:

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

Ama değişmez sınıflar daha güvenlidir - böyle bir sınıfı değişebilir yapmak için bir gerekçe bulamam.

Ben gerçekten sadece bu sonrası anonim iç sınıf duyduğum zaman cringe çünkü - "Gerekli" bir sürü gereksiz kod gördüm çünkü programcı yaptığı ilk şey o gerçek bir sınıf kullanmalıydı zaman anonim gitmek oldu kararını yeniden düşündü.


Ha? OP farklı hesaplamalar hakkında konuşuyor (algoritmalar; mantık); farklı değerler (veriler) gösteriyorsunuz . Farkın bir değere dahil edilebileceği özel bir durum gösterirsiniz, ancak bu, ortaya çıkan sorunun haksız bir şekilde basitleştirilmesidir.
ToolmakerSteve

6

Çok popüler hale gelen Google Guava kütüphaneleri , API'larının birçok bölümünde çalıştıkları genel bir İşlev ve Tahmin nesnesine sahiptir.


Bu yanıt, kod ayrıntılarını verdiyse daha yararlı olacaktır. Kabul edilen cevapta gösterilen kodu alın ve İşlev'i kullanarak nasıl görüneceğini gösterin.
ToolmakerSteve

4

Bana bir strateji modeli gibi geliyor. Fluffycat.com Java kalıplarına göz atın.


4

Java'da programlama yaparken gerçekten özlediğim şeylerden biri işlev geri çağrıları. Bunların kendini sunmaya devam etmesinin gerekli olduğu bir durum, her bir öğe için belirli bir eylem gerçekleştirmek istediğiniz hiyerarşileri yinelemeli olarak işlemektir. Bir dizin ağacını yürümek veya bir veri yapısını işlemek gibi. İçimdeki minimalist, her bir özel durum için bir arayüz ve ardından bir uygulama tanımlamaktan nefret ediyor.

Bir gün kendimi neden olmasın diye merak ettim? Yöntem işaretçilerimiz var - Yöntem nesnesi. JIT derleyicilerini optimize ederken, yansıtıcı çağırma artık gerçekten büyük bir performans cezası taşımaz. Ve örneğin, bir dosyayı bir konumdan diğerine kopyalamanın yanı sıra, yansıtılan yöntem çağırma maliyetinin önemi önemsizdir.

Daha fazla düşündüğüm gibi, OOP paradigmasındaki bir geri çağrının bir nesneyi ve yöntemi birbirine bağlamayı gerektirdiğini fark ettim - Geri Arama nesnesini girin.

Java'da Geri Aramalar için yansıma tabanlı çözümüme göz atın . Herhangi bir kullanım için ücretsiz.


4

Tamam, bu konu zaten yeterince eski, bu yüzden muhtemelen cevabım soru için yararlı değil. Ama bu konu benim çözümümü bulmama yardım ettiğinden, yine de buraya koyacağım.

Bilinen giriş ve bilinen çıkış (her ikisi de çift ) ile değişken bir statik yöntem kullanmak gerekiyordu . Böylece, yöntem paketini ve adını bilerek, aşağıdaki gibi çalışabilirim:

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

bir çifte parametre olarak kabul eden bir işlev için.

Böylece, somut durumumda

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

ve daha sonra daha karmaşık bir durumda

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

burada aktivite keyfi bir çifte değerdir. SoftwareMonkey'in yaptığı gibi belki de bunu biraz daha soyut ve genelleştirmeyi düşünüyorum, ama şu anda olduğu gibi yeterince mutluyum. Üç satır kod, sınıf ve arayüz gerekmez, bu çok kötü değil.


codeMarkdown eklediğiniz için teşekkürler Rob , ben sabırsız ve aptalca bulmak için ;-)
yogibimbi

4

Aynı işlevi bir dizi işlev için arabirim olmadan yapmak için:

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
Neden? Bu, alternatif başına bir anonim işlev tanımına ihtiyaç duyan, daha önce önerilen çözümlerden daha karmaşıktır. Alternatif olarak, bir sınıf ve anonim işlev tanımı oluşturursunuz. Daha da kötüsü, bu kodda iki farklı yerde yapılır. Bu yaklaşımı kullanmak için bir gerekçe sunmak isteyebilirsiniz.
ToolmakerSteve


3

Vay canına, neden sadece Java için yaptığım göz önüne alındığında bu kadar zor olmayan bir Delegate sınıfı oluşturmuyoruz ve T dönüş türü olduğu parametreye geçmek için kullanıyorum. Üzgünüm ama genel olarak sadece java öğrenme bir C ++ / C # programcı olarak, çok kullanışlı oldukları için fonksiyon işaretçileri gerekir. Metot Bilgisi ile ilgilenen herhangi bir sınıfa aşina iseniz bunu yapabilirsiniz. Java kütüphanelerinde java.lang.reflect.method olur.

Her zaman bir arabirim kullanıyorsanız, onu her zaman uygulamanız gerekir. Olay işleme sırasında, işleyiciler listesinden kaydolmak / kayıttan çıkmak için daha iyi bir yol yoktur, ancak değer türünü değil, işlevleri girmeniz gereken temsilciler için, sınıflar için bir arabirim işlemek üzere bir temsilci sınıfı yapmak.


Kod ayrıntılarını göstermediğiniz sürece yararlı bir yanıt değildir. Bir Delegate sınıfı oluşturmak nasıl yardımcı olur? Alternatif başına hangi kod gerekir?
ToolmakerSteve

2

Java 8 cevaplarının hiçbiri tam ve uyumlu bir örnek vermedi, işte geliyor.

"İşlev işaretçisini" kabul eden yöntemi aşağıdaki gibi bildirin:

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

Fonksiyona lambda ifadesi sağlayarak çağırın:

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


1

Java8'den bu yana, resmi SE 8 API'sında da kütüphaneleri olan lambdas'ı kullanabilirsiniz.

Kullanım: Sadece bir soyut yöntemle bir arayüz kullanmanız gerekir. Bunun bir örneğini yapın (zaten sağlanan bir java SE 8'i kullanmak isteyebilirsiniz) şöyle:

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

Daha fazla bilgi için belgelere göz atın: https://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html


1

Java 8'den önce, işlev işaretçisi benzeri işlevsellik için en yakın değişiklik anonim bir sınıftı. Örneğin:

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

Ama şimdi Java 8'de lambda ifadesi olarak bilinen çok temiz bir alternatifimiz var :

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

Burada isBiggerThan, içindeki bir yöntemdir CustomClass. Burada yöntem referanslarını da kullanabiliriz:

list.sort(MyClass::isBiggerThan);
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.