Який найближчий замінник покажчика функції на Java?


299

У мене є метод, який становить близько десяти рядків коду. Я хочу створити більше методів, які роблять точно те саме, за винятком невеликого розрахунку, який змінить один рядок коду. Це ідеальний додаток для передачі вказівника функції для заміни цього рядка, але у Java немає функціональних покажчиків. Яка моя найкраща альтернатива?


5
У Java 8 будуть Lambda вирази . Детальніше про лямбдаські вирази ви можете прочитати тут .
Маріус

4
@Marius Я не думаю, що лямбда-вирази вважаються функціональними вказівниками. Оператор, з іншого боку ...::
Хлопець з Hat

Вибачте за пізній коментар;) - зазвичай для цього вам не потрібен вказівник функції. Просто використовуйте шаблон шаблону! ( en.wikipedia.org/wiki/Template_method_pattern )
isnot2bad

2
@ isnot2bad - дивлячись на цю статтю, здається, надмірність - складніша за відповіді, наведені тут. Зокрема, шаблонний метод вимагає створення підкласу для кожного альтернативного обчислення. Я не бачу, щоб ОП заявила нічого, що вимагає підкласів ; він просто хоче створити кілька методів і поділити більшу частину реалізації. Як показує прийнята відповідь, це легко зробити "на місці" (усередині кожного методу), навіть перед Java 8 зі своїми лямбдами.
ToolmakerSteve

@ToolmakerSteve Прийняте рішення також вимагає класу за розрахунком (навіть якщо це лише анонімний внутрішній клас). Шаблон методу шаблону також може бути реалізований за допомогою анонімних внутрішніх класів, тому він не сильно відрізняється від прийнятого рішення, що стосується накладних витрат (до Java 8). Тож це більше питання про модель використання та деталізовані вимоги, про які ми не знаємо. Я ціную прийняту відповідь і просто хотів додати ще одну можливість подумати.
isnot2bad

Відповіді:


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

EDIT: У Java 8 ви можете назвати це висловом лямбда:

ref.takingMethod(param -> bodyExpression);

13
Це, до речі, приклад «командного патерна». en.wikipedia.org/wiki/Command_Pattern
Psalm Ogre33

3
@Ogre Psalm33 Ця методика також може бути стратегічною схемою, залежно від того, як ви її використовуєте. Різниця між шаблоном стратегії та командним шаблоном .
Рорі О'Кайн

2
Ось реалізація закриття для Java 5, 6 та 7 mseifed.blogspot.se/2012/09/… Він містить все, про що можна було б попросити ... Я думаю, це досить приголомшливо!
ммм

@SecretService: Це посилання мертве.
Лоуренс Дол,

@LawrenceDol Так, так. Ось пастабін класу, який я використовую. pastebin.com/b1j3q2Lp
ммм

32

Для кожного "покажчика функції" я створив би невеликий клас функторів, який реалізує ваш обчислення. Визначте інтерфейс, який реалізуватимуть усі класи, і передайте екземпляри цих об'єктів у вашу більшу функцію. Це поєднання " командного шаблону " та " стратегії ".

Приклад @ sblundy - це добре.


28

Якщо є заздалегідь визначена кількість різних обчислень, які ви можете зробити в одному рядку, використання enum - це швидкий, але зрозумілий спосіб реалізації стратегії.

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 замість власного інтерфейсу, якщо вам не потрібно вводити параметри, або ви можете використовувати різні інші методи, щоб зробити параметр менш "фіксованим", але це, як правило, компроміс із безпекою типу. (Або ви можете перекрити конструктор, щоб ваш функціональний об’єкт передав парами таким чином. Є багато підходів, і деякі працюють краще за певних обставин.)


4
Ця "відповідь" стосується більше набору задач, ніж набору рішення.
tchrist

18

Посилання методу за допомогою ::оператора

Ви можете використовувати посилання методів в аргументах методів, де метод приймає функціональний інтерфейс . Функціональний інтерфейс - це будь-який інтерфейс, який містить лише один абстрактний метод. (Функціональний інтерфейс може містити один або кілька методів за замовчуванням або статичні методи.)

IntBinaryOperatorє функціональним інтерфейсом. Його абстрактний метод, applyAsIntприймає два intпараметри як свої параметри і повертає int. Math.maxтакож приймає два ints і повертає int. У цьому прикладі A.method(Math::max);змушує 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 8 та специфікація мови Java §15.13 .


15

Ви також можете зробити це (що в деяких РІДКІСНИХ випадках має сенс). Проблема (і це велика проблема) полягає в тому, що ви втрачаєте всю безпеку використання класу / інтерфейсу, і вам доведеться мати справу з тим випадком, коли методу не існує.

Він має "перевагу", що ви можете ігнорувати обмеження доступу та викликати приватні методи (не показано в прикладі, але ви можете викликати методи, які компілятор, як правило, не дозволяє вам дзвонити).

Знову ж, це рідкісний випадок, коли це має сенс, але в тих випадках це гарний інструмент.

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 іноді, оскільки синтаксис методу набагато простіший, ніж анонімний синтаксис внутрішнього класу. Це акуратно, але ви додаєте складність роздумів, яку деякі люди не хочуть копати, тому будьте чорт упевнений, що ви правильно це зрозуміли та маєте чіткі текстові помилки для кожного можливого стану помилок.
Білл К

Ви можете робити це безпечно, використовуючи дженерики, і вам не потрібно роздумів.
Луїджі Плінге

1
Я не бачу, як використання генерики та нерефлексія дозволить вам викликати метод ім'ям, що міститься в рядку?
TofuBeer

1
@LuigiPlinge - чи можете ви надати фрагмент коду того, що ви маєте на увазі?
ToolmakerSteve

13

Якщо у вас є лише один рядок, який відрізняється, ви можете додати такий параметр, як прапор та оператор if (flag), який викликає один або інший рядок.


1
Відповідь javaslook видається чистішим способом зробити це, якщо більше двох варіантів розрахунку. Або якщо хочеться вставити код у метод, то перерахунок для різних випадків, якими обробляється метод, і перемикач.
ToolmakerSteve

1
@ToolmakerSteve правда, хоча сьогодні ви б використовували лямбди на Java 8.
Peter Lawrey

12

Вам також може бути цікаво дізнатися про роботу, яка триває для Java 7 із закриттям:

Який поточний стан закриття в Java?

http://gafter.blogspot.com/2006/08/closures-for-java.html
http://tech.puredanger.com/java7/#closures


+1 за корисні посилання, хоча я вважаю, що додавати до Java закриття зовсім не корисно.
mikera

11

Нові функціональні інтерфейси та довідники Java 8 за допомогою ::оператора.

Java 8 може підтримувати посилання на методи (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. Приклад використання - просто оператор призначення та виклик методу інтерфейсу.

ВИ ВИКОРИСТОВУЄТЕ ФУНКЦІОНАЛЬНІ ІНТЕРФЕЙСИ ДЛЯ СПИСОКІВ ЛИШЕ ТА ТІЛЬКИ ДЛЯ ТОГО!

Тому що всі інші такі покажчики функцій справді погані для читабельності коду та здатності розуміти. Однак прямі посилання на методи іноді бувають корисними, наприклад, з передбаченням.

Є кілька заздалегідь заданих функціональних інтерфейсів:

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, які мають схожі функціональні можливості та синтаксис, про які Адріан Петреску згадував вище.

Для додаткових досліджень подивіться на шаблоні Java 8

і спасибі Хлопець із Шапочкою за специфікацію мови Java §15.13 .


7
" Тому що всі інші ... справді погані для читабельності коду " - це абсолютно необґрунтоване твердження, і помилково, до того ж.
Лоуренс Дол

9

@ відповідь sblundy чудова, але анонімні внутрішні класи мають два невеликі недоліки, головним є те, що вони, як правило, не підлягають повторному використанню, а вторинний - об'ємний синтаксис.

Приємно те, що його малюнок розширюється на повні класи без будь-якої зміни головного класу (того, який виконує обчислення).

Коли ви створюєте інстанціювання нового класу, ви можете передавати параметри до цього класу, які можуть діяти як константи у вашому рівнянні - тож якщо один із ваших внутрішніх класів виглядає так:

f(x,y)=x*y

але іноді вам потрібен такий:

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

а може і третя, що:

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

замість створення двох анонімних внутрішніх класів або додавання параметра "прохідний", ви можете зробити один клас АКТУАЛЬНИЙ, який ви інстанціюєте як:

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

Але незмінні класи безпечніші - я не можу придумати виправдання, щоб зробити клас на зразок цього змінного.

Я дійсно розміщую це лише через те, що клацаю щоразу, коли чую анонімний внутрішній клас - я бачив багато зайвого коду, який був "Потрібен", тому що перше, що робив програміст, - це анонімно, коли він повинен був використовувати фактичний клас і ніколи переосмислив своє рішення.


Так? ОП говорить про різні обчислення (алгоритми; логіка); ви показуєте різні значення (дані). Ви показуєте конкретний випадок, коли різницю можна включити у значення, але це невиправдане спрощення поставленої проблеми.
ToolmakerSteve

6

Бібліотеки Google Guava , які стають дуже популярними, мають загальний об'єкт функцій та предикатів, який вони працювали у багатьох частинах API.


Ця відповідь була б кориснішою, якби вона містила детальну інформацію про код. Візьміть код, показаний у прийнятій відповіді, і покажіть, як це виглядатиме за допомогою функції.
ToolmakerSteve

4

Мені це звучить як шаблон стратегії. Ознайомтеся з шаблонами Java fluffycat.com.


4

Одне з речей, які мені дуже не вистачає при програмуванні на Java, - це зворотні функції функцій. Одна з ситуацій, коли потреба в цих постійно себе представляла, полягала в рекурсивній обробці ієрархій, де потрібно виконати певні дії для кожного елемента. Як і ходіння по дереву каталогів, або обробка структури даних. Мінімаліст всередині мене ненавидить визначати інтерфейс, а потім реалізацію для кожного конкретного випадку.

Одного разу мені стало цікаво, чому ні? У нас є покажчики методу - об'єкт Method. Оптимізуючи компілятори JIT, відбивна виклик справді більше не приносить великого покарання за продуктивність. Крім того, що, скажімо, копіювання файлу з одного місця в інше, вартість виклику методу, що відображається, зменшується.

Коли я детальніше подумав про це, я зрозумів, що зворотний виклик у парадигмі OOP вимагає зв’язування об'єкта та методу разом - ввести об'єкт зворотного виклику.

Ознайомтеся з моїм рішенням, що базується на зворотах на Java . Безкоштовно для будь-якого використання.


4

ОК, ця тема вже досить стара, тому, напевно, моя відповідь не корисна для запитання. Але оскільки ця нитка допомогла мені знайти рішення, я все-таки викладу його тут.

Мені потрібно було використовувати змінний статичний метод з відомим входом і відомим виходом (обидва подвійні ). Тож, знаючи пакет методу та назву, я міг би працювати так:

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

для функції, яка приймає один параметр як параметр.

Отже, у своїй конкретній ситуації я ініціалізував це

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, але наразі я досить задоволений таким, яким він є. Три рядки коду, не потрібні класи та інтерфейси, це не дуже погано.


дякую Робу за те, що він додав 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
Чому? Це складніше, ніж запропоновані раніше рішення, для яких просто потрібно одне анонімне визначення функції на альтернативу. Як альтернатива, ви створюєте визначення класу та анонімного визначення функції. Гірше, це робиться в двох різних місцях у коді. Ви можете надати певне обґрунтування для використання цього підходу.
ToolmakerSteve

3

Перевірте лямбдай

http://code.google.com/p/lambdaj/

і зокрема його нову функцію закриття

http://code.google.com/p/lambdaj/wiki/Закриття

і ви знайдете дуже зрозумілий спосіб визначення покажчика закриття чи функціонування без створення безглуздого інтерфейсу або використання некрасивих внутрішніх класів


3

Нічого собі, чому б просто не створити клас Delegate, який не все так важко, враховуючи те, що я вже робив для Java, і використовувати його для передачі в параметр, де T - тип повернення. Мені дуже шкода, але як програміст C ++ / C # взагалі просто навчаюсь Java, мені потрібні функціональні вказівники, оскільки вони дуже зручні. Якщо ви знайомі з будь-яким класом, який займається інформацією про метод, ви можете це зробити. У бібліотеках Java, яка буде java.lang.reflect.method.

Якщо ви завжди використовуєте інтерфейс, вам завжди доведеться його реалізувати. У поводженні з подіями насправді немає кращого способу реєстрації / відреєстрації зі списку обробників, але для делегатів, де вам потрібно передати функції, а не тип значення, зробивши клас делегата для обробки цього інтерфейсу для ауткласів.


Не корисна відповідь, якщо ви не покажете деталі коду. Як допомагає створити клас «Делегат»? Який код необхідний для альтернативи?
ToolmakerSteve

2

Жоден з відповідей Java 8 не дав повного, згуртованого прикладу, тому ось він іде.

Заявіть метод, який приймає "покажчик функції" таким чином:

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

Назвіть його, надавши функції з виразом лямбда:

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

1

Якщо хтось бореться, щоб передати функцію, яка приймає один набір параметрів для визначення її поведінки, а інший набір параметрів, на яких потрібно виконати, як-от схеми:

(define (function scalar1 scalar2)
  (lambda (x) (* x scalar1 scalar2)))

див. функцію передачі з поведінкою, визначеною параметрами на Java


1

З Java8 ви можете використовувати лямбда, які також мають бібліотеки в офіційному API SE 8.

Використання: Вам потрібно використовувати інтерфейс лише з одним абстрактним методом. Зробіть його примірник (можливо, ви хочете використовувати один 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 у нас є дуже акуратна альтернатива, відома як лямбда-вираз , яку можна використовувати як:

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

де isBiggerThan - метод в CustomClass. Тут також можна використовувати посилання на методи:

list.sort(MyClass::isBiggerThan);
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.