Возвращает первое ненулевое значение

У меня есть ряд функций:

String first(){}
String second(){}
...
String default(){}

Каждый может вернуть нулевое значение, кроме значения по умолчанию. каждая функция может принимать разные параметры. Например, сначала можно не принимать аргументов, второй может принимать значение String, третий может принимать три аргумента и т.д.. Я хотел бы сделать что-то вроде:

ObjectUtils.firstNonNull(first(), second(), ..., default());

Проблема состоит в том, что из-за вызова функции это требует высокой оценки. Где я хотел бы выйти раньше, сказать после второй функции (потому что вызовы функций могут быть дорогими, думаю, API-вызовы и т.д.). На других языках вы можете сделать что-то похожее на это:

return first() || second() || ... || default()

В Java я знаю, что могу сделать что-то вроде:

String value;
if (value = first()) == null || (value = second()) == null ...
return value;

Это не очень читаемая ИМО из-за всех ошибок == null. ObjectUtils.firstNonNull() сначала создает коллекцию, а затем выполняет итерацию, что нормально, если функция оценивается лениво.

Предложения? (кроме того, чтобы сделать кучу ifs)

Ответы

Ответ 1

String s = Stream.<Supplier<String>>of(this::first, this::second /*, ... */)
                 .map(Supplier::get)
                 .filter(Objects::nonNull)
                 .findFirst()
                 .orElseGet(this::defaultOne);

Он останавливается на первом ненулевом значении или устанавливает значение, которое возвращается из defaultOne. Пока вы остаетесь последовательным, вы в безопасности. Конечно, для этого требуется Java 8 или новее.

Причина, по которой он останавливается при первом вводе ненулевого значения, объясняется тем, как Stream обрабатывает каждый шаг. map является промежуточной операцией поэтому filter. findFirst с другой стороны является операция короткого замыкания терминала. Таким образом, он продолжается со следующим элементом, пока он не будет соответствовать фильтру. Если ни один элемент не совпадает с пустым необязательным, возвращается и поэтому вызывается orElseGet -supplier.

this::first и т.д. - это только ссылки на методы. Если они статические, замените его на YourClassName::first и т.д.

Вот пример, если подпись ваших методов будет отличаться:

String s = Stream.<Supplier<String>>of(() -> first("takesOneArgument"),
                                       () -> second("takes", 3, "arguments")
                                   /*, ... */)
                 .map(Supplier::get)
                 .filter(Objects::nonNull)
                 .findFirst()
                 .orElseGet(this::defaultOne);

Обратите внимание, что Supplier оценивается только при вызове get на нем. Таким образом, вы получаете свое ленивое поведение оценки. Параметры метода в вашем выражении поставщика-лямбда должны быть окончательными или фактически окончательными.

Ответ 2

Это можно сделать довольно чисто с потоком Suppliers.

Optional<String> result = Stream.<Supplier<String>> of(
     () -> first(), 
     () -> second(),
     () -> third() )
  .map( x -> x.get() )
  .filter( s -> s != null)
  .findFirst(); 

Причина этого в том, что, несмотря на появление, все исполнение управляется findFirst(), который вытягивает элемент из filter(), который лениво вытаскивает элементы из map(), который вызывает get() для обработки каждого нажатия. findFirst() перестанет вытягиваться из потока, когда один элемент прошел фильтр, поэтому последующие поставщики не будут иметь get().

Хотя я лично считаю, что декларативный стильный стиль Stream и более выразительный, вам не нужно использовать Stream для работы с Supplier, если вам не нравится стиль:

Optional<String> firstNonNull(List<Supplier<String>> suppliers {
    for(Supplier<String> supplier : suppliers) {
        String s = supplier.get();
        if(s != null) {
            return Optional.of(s);
        }
    }
    return Optional.empty();
}

Должно быть очевидно, что вместо возврата Optional вы можете в равной степени вернуть String, либо возвращая значение null (yuk), либо строку по умолчанию, либо бросая исключение, если вы исчерпаете опции из списка.

Ответ 3

Он не читается, потому что вы имеете дело с кучей отдельных функций, которые не выражают какого-либо соединения друг с другом. Когда вы пытаетесь соединить их, отсутствие направления очевидно.

Вместо этого попробуйте

 public String getFirstValue() {
      String value;
      value = first();
      if (value != null) return value;
      value = second();
      if (value != null) return value;
      value = third();
      if (value != null) return value;
      ...
      return value;
 }

Будет ли это долго? Вероятно. Но вы применяете код поверх интерфейса, который не подходит для вашего подхода.

Теперь, если вы можете изменить интерфейс, вы можете сделать интерфейс более дружелюбным. Возможным примером может быть то, что этапы являются объектами ValueProvider.

public interface ValueProvider {
    public String getValue();
}

И тогда вы можете использовать его как

public String getFirstValue(List<ValueProvider> providers) {
    String value;
    for (ValueProvider provider : providers) {
       value = provider.getValue();
       if (value != null) return value;
    }
    return null;
}

И существуют различные другие подходы, но они требуют реструктуризации кода, чтобы быть более объектно-ориентированным. Помните, что только потому, что Java является объектно-ориентированным языком программирования, это не означает, что он всегда будет использоваться объектно-ориентированным способом. Список методов first()... last() очень не объектно-ориентирован, потому что он не моделирует a List. Несмотря на то, что имена методов являются выразительными, List имеет на нем методы, которые позволяют легко интегрироваться с инструментами типа for и Iterators.

Ответ 4

Если вы используете java 8, вы можете преобразовать эти вызовы функций в lambdas.

public static<T> T firstNonNull(Supplier<T> defaultSupplier, Supplier<T>... funcs){
    return Arrays.stream(funcs).filter(p -> p.get() != null).findFirst().orElse(defaultSupplier).get();
}

Если вы не хотите, чтобы общая реализация и использовала ее только для String, продолжайте и просто замените T на String:

public static String firstNonNull(Supplier<String> defaultSupplier, Supplier<String>... funcs){
    return Arrays.stream(funcs).filter(p -> p.get() != null).findFirst().orElse(defaultSupplier).get();
}

И затем назовите его так:

firstNonNull(() -> getDefault(), () -> first(arg1, arg2), () -> second(arg3));

P.S. btw default - зарезервированное ключевое слово, поэтому вы не можете использовать его как имя метода:)

EDIT: хорошо, лучший способ сделать это - вернуть Необязательный, тогда вам не нужно передавать поставщик по умолчанию по-разному:

@SafeVarargs
public static<T> Optional<T> firstNonNull(Supplier<T>... funcs){
    return Arrays.stream(funcs).filter(p -> p.get() != null).map(s -> s.get()).findFirst();
}

Ответ 5

Если вы хотите упаковать его в метод утилиты, вам придется перевернуть каждую функцию во что-то, что отменит выполнение. Возможно, что-то вроде этого:

public interface Wrapper<T> {
    T call();
}

public static <T> T firstNonNull(Wrapper<T> defaultFunction, Wrapper<T>... funcs) {
    T val;
    for (Wrapper<T> func : funcs) {
       if ((val = func.call()) != null) {
           return val;
       }
    }
    return defaultFunction.call();
}

Вы можете использовать java.util.concurrent.Callable вместо определения своего собственного класса Wrapper, но тогда вам придется иметь дело с исключением, которое объявляется Callable.call().

Затем это можно вызвать с помощью:

String value = firstNonNull(
    new Wrapper<>() { @Override public String call() { return defaultFunc(); },
    new Wrapper<>() { @Override public String call() { return first(); },
    new Wrapper<>() { @Override public String call() { return second(); },
    ...
);

В Java 8, как указывает @dorukayhan, вы можете отказаться от определения своего собственного класса Wrapper и просто использовать интерфейс Supplier. Кроме того, вызов можно сделать гораздо более четко с помощью лямбда:

String value = firstNonNull(
    () -> defaultFunc(),
    () -> first(),
    () -> second(),
    ...
);

Вы также можете (как предлагает @Oliver Charlesworth) использовать ссылки на методы как сокращения для лямбда-выражений:

String value = firstNonNull(
    MyClass::defaultFunc,
    MyClass::first,
    MyClass::second,
    ...
);

Я имею в виду два мнения, которые более читабельны.

В качестве альтернативы вы можете использовать одно из потоковых решений, предложенное многими другими.

Ответ 6

Просто создайте класс с одной функцией следующим образом:

class ValueCollector {
  String value;
  boolean v(String val) { this.value = val; return val == null; }
}

ValueCollector c = new ValueCollector();
if c.v(first()) || c.v(second()) ...
return c.value;

Ответ 7

Вы можете выполнить это через отражение:

public Object getFirstNonNull(Object target, Method... methods) {
    Object value = null;
    for (Method m : methods) {
        if ( (value = m.invoke(target)) != null) {
            break;
        }
    }
    return value;
}