Почему Java 8 необязательно не должны использоваться в аргументах


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

public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {
    // my logic
}

многие веб-страницы указывают необязательный не следует использовать в качестве аргументов метода. Имея это в виду, я мог бы использовать следующий метод подписи и добавьте ясный комментарий Javadoc, чтобы указать, что аргументы могут быть null, надеясь, что будущие сопровождающие будут читать Javadoc и поэтому всегда выполнять нулевые проверки перед использованием аргументов (решение 2):

public int calculateSomething(String p1, BigDecimal p2) {
    // my logic
}

в качестве альтернативы я мог бы заменить свой метод четырьмя общедоступными методами, чтобы обеспечить более приятный интерфейс и сделать его более очевидным p1 и p2 являются необязательными (решение 3):

public int calculateSomething() {
    calculateSomething(null, null);
}

public int calculateSomething(String p1) {
    calculateSomething(p1, null);
}

public int calculateSomething(BigDecimal p2) {
    calculateSomething(null, p2);
}

public int calculateSomething(String p1, BigDecimal p2) {
    // my logic
}

теперь я пытаюсь написать код класса, который вызывает этот кусок логики для каждый подход. Сначала я получаю два входных параметра из другого объекта, который возвращает Optionals и затем, я призываю calculateSomething. Поэтому, если используется решение 1, вызывающий код будет выглядеть следующим образом:

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1, p2);

если используется решение 2, вызывающий код будет выглядеть следующим образом:

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));

если решение 3 применяется, я мог бы использовать код выше или я мог бы использовать следующее (Но это значительно больше кода):

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();
int result;
if (p1.isPresent()) {
    if (p2.isPresent()) {
        result = myObject.calculateSomething(p1, p2);
    } else {
        result = myObject.calculateSomething(p1);
    }
} else {
    if (p2.isPresent()) {
        result = myObject.calculateSomething(p2);
    } else {
        result = myObject.calculateSomething();
    }
}

так что мой вопрос: Почему считается плохой практикой использовать Optionalкак аргументы метода (см. Решение 1)? это выглядит как наиболее читаемое решение для меня и делает его наиболее очевидным, что параметры могут быть пустыми/нулевыми для будущих сопровождающих. (Я знаю, что дизайнеры Optional предполагалось, что он будет использоваться только как тип возврата, но я не могу найти никаких логических причин не использовать его в этом сценарии).

18 232

18 ответов:

О, эти стили кодирования, которые должны быть приняты с небольшим количеством соли.

  1. (+) передача необязательного результата другому методу без какого-либо семантического анализа; оставляя это методу, все в порядке.
  2. ( -) использование необязательных параметров, вызывающих условную логику внутри методов, буквально контрпродуктивно.
  3. ( -) необходимость упаковать аргумент в необязательный, является неоптимальным для компилятора и делает ненужную упаковку.
  4. ( -) In сравнение с параметрами, допускающими обнуление, является более дорогостоящим.

В общем: необязательно объединяет два состояния, которые должны быть распутаны. Следовательно, лучше подходит для результата, чем для ввода, для сложности потока данных.

The лучший пост Я видел на эту тему было написано Даниил Ольшевский:

хотя может возникнуть соблазн рассмотреть необязательные для не обязательных параметров метода, такое решение бледнеет по сравнению с другими возможными альтернативами. Чтобы проиллюстрировать проблему, изучите следующее объявление конструктора:

public SystemMessage(String title, String content, Optional<Attachment> attachment) {
    // assigning field values
}

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

SystemMessage withoutAttachment = new SystemMessage("title", "content", Optional.empty());
Attachment attachment = new Attachment();
SystemMessage withAttachment = new SystemMessage("title", "content", Optional.ofNullable(attachment));

вместо того, чтобы обеспечить ясность, заводских методов Факультативного занятия только отвлекают читателя. Обратите внимание, что есть только один дополнительный но представьте себе, что их два или три. Дядя Боб определенно не гордился бы таким кодом

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

public SystemMessage(String title, String content) {
    this(title, content, null);
}

public SystemMessage(String title, String content, Attachment attachment) {
    // assigning field values
}

это изменение делает клиентский код намного проще и легче читать.

SystemMessage withoutAttachment = new SystemMessage("title", "content");
Attachment attachment = new Attachment();
SystemMessage withAttachment = new SystemMessage("title", "content", attachment);

нет почти никаких веских причин не использовать Optional как параметры. Аргументы против этого полагаются на аргументы от авторитета (см. Brian Goetz-его аргумент заключается в том, что мы не можем применять ненулевые опционы) или что Optional аргументы могут быть null (по существу тот же аргумент). Конечно, любая ссылка в Java может быть нулевой, нам нужно поощрять правила, применяемые компилятором, а не память программистов (что проблематично и не масштабируется).

функциональные языки программирования поощряют Optional параметры. Один из лучших способов использования этого-иметь несколько дополнительных параметров и использовать liftM2 чтобы использовать функцию, предполагающую, что параметры не пусты и возвращают необязательный (см. http://www.functionaljava.org/javadoc/4.4/functionaljava/fj/data/Option.html#liftM2-fj.F-). Java 8, к сожалению, реализовал очень ограниченную библиотеку, поддерживающую необязательный.

как Java-программисты мы должны использовать только null to взаимодействие с устаревшими библиотеками.

этот совет является вариантом правила" быть как можно более неспецифичным в отношении входов и как можно более конкретным в отношении выходов".

обычно, если у вас есть метод, который принимает простое ненулевое значение, вы можете сопоставить его с Optional, поэтому простая версия строго более неспецифична в отношении входов. есть куча возможных причин, почему вы хотели бы требовать Optional аргумент тем не менее:

  • вы хотите ваша функция должна использоваться в сочетании с другим API, который возвращает Optional
  • ваша функция должна возвращать что-то кроме пустой Optional Если заданное значение пустое
  • думаешь Optional настолько потрясающе, что тот, кто использует ваш API, должен быть обязан узнать об этом; -)

С Optional для одного, чтобы избежать возвращениеnull. Вполне можно пройти в null к способ.

хотя они еще не являются официальными, вы можете использовать стиль JSR-308 аннотации, чтобы указать, принимаете ли вы null значения в функцию. Обратите внимание, что у вас должен быть правильный инструментарий, чтобы фактически идентифицировать его, и он обеспечит больше статической проверки, чем принудительной политики выполнения, но это помочь.

public int calculateSomething(@NotNull final String p1, @NotNull final String p2) {}

Это кажется мне немного глупым, но единственная причина, о которой я могу думать, заключается в том, что аргументы объекта в параметрах метода уже являются необязательными - они могут быть нулевыми. Поэтому заставлять кого-то взять существующий объект и обернуть его в необязательный вид бессмысленно.

Это, как говорится, цепочка методов вместе, которые принимают / возвращают optionals-это разумная вещь, например, возможно, монада.

Я считаю, что необязательный должен быть монадой, и они не мыслимы в Java.

в функциональном программировании вы имеете дело с чистыми и более высокими функциями порядка, которые принимают и составляют свои аргументы только на основе их "типа бизнес-домена". Составление функций, которые питаются или о вычислении которых следует сообщать, в реальном мире (так называемые побочные эффекты) требует применения функций, которые заботятся об автоматической распаковке значений из монад представление внешнего мира (состояние, конфигурация, будущее, возможно, либо, писатель и т. д...); это называется подъем. Вы можете думать об этом как своего рода разделение.

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

еще одна причина, чтобы быть осторожным, когда пройти Optional в качестве параметра является то, что метод должен делать одну вещь... Если вы передаете Optional param вы можете сделать больше, чем одну вещь, это может быть похоже на передачу логического param.

public void method(Optional<MyClass> param) {
     if(param.isPresent()) {
         //do something
     } else {
         //do some other
     }
 }

Я думаю, это потому, что вы обычно пишете свои функции для управления данными, а затем поднимаете их до Optional используя map и похожие функции. Это добавляет значение по умолчанию Optional поведение к нему. Конечно, могут быть случаи, когда необходимо написать свою собственную вспомогательную функцию, которая работает на Optional.

Я считаю, что резонанс бытия заключается в том, что вы должны сначала проверить, является ли необязательным сам null, а затем попытаться оценить значение, которое он обертывает. Слишком много ненужных проверок.

Optionals не предназначены для этой цели, как хорошо объяснил Брайан Гетц.

вы всегда можете использовать @Nullable чтобы обозначить, что аргумент метода может быть null. Использование дополнительного не позволяет вам писать логику метода более аккуратно.

принятие необязательных параметров в качестве параметров вызывает ненужную обертку на уровне вызывающего абонента.

например в случае:

public int calculateSomething(Optional<String> p1, Optional<BigDecimal> p2 {}

предположим, что у вас есть две строки not-null (т. е. возвращается из какого-то другого метода):

String p1 = "p1"; 
String p2 = "p2";

вы вынуждены обернуть их в необязательный, даже если вы знаете, что они не пусты.

это становится еще хуже, когда вам приходится сочинять с другими "отображаемыми" структурами, т. е. Eithers:

Either<Error, String> value = compute().right().map((s) -> calculateSomething(
< here you have to wrap the parameter in a Optional even if you know it's a 
  string >));

ref:

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

ref. https://github.com/teamdigitale/digital-citizenship-functions/pull/148#discussion_r170862749

используя Optional в качестве аргументов метода также требуется null проверить на себе. Ненужная дублирующая логика. Например:-

void doSomething(Optional<ISOCode> isoOpt, Optional<Product> prodOpt){

    if(isoOpt != null){
       isoOpt.orElse(ISOCode.UNKNOWN);
    }

    if(prodOpt != null){
       prodOpt.orElseThrow(NullPointerException::new);
    }

    //more instructions
}

представьте, если количество растет. Если забыть нулевую проверку, возможно, NPE будет брошен во время выполнения.

кроме того, если ваш метод является общедоступным, клиент будет вынужден обернуть его параметры в Optionals, что будет обременительно, особенно если количество аргументов растет.

еще один подход, что вы можете сделать, это

// get your optionals first
Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();

// bind values to a function
Supplier<Integer> calculatedValueSupplier = () -> { // your logic here using both optional as state}

после того, как вы построили функцию(поставщик в этом случае), вы сможете передать это как любую другую переменную и сможете вызвать ее с помощью

calculatedValueSupplier.apply();

идея здесь заключается в том, есть ли у вас дополнительное значение или нет, будет внутренней деталью вашей функции и не будет в параметре. Функции мышления при размышлении о необязательном качестве параметра на самом деле очень полезная техника, которую я имею найдено.

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

сначала я также предпочел передать Optionals в качестве параметра, но если вы переключитесь с точки зрения API-дизайнера на точку зрения API-пользователя, вы увидите недостатки.

для вашего примера, где каждый параметр является необязательным, я бы предложил изменить метод расчета в собственный класс, как показано ниже:

Optional<String> p1 = otherObject.getP1();
Optional<BigInteger> p2 = otherObject.getP2();

MyCalculator mc = new MyCalculator();
p1.map(mc::setP1);
p2.map(mc::setP2);
int result = mc.calculate();

Я знаю, что этот вопрос больше касается мнения, а не твердых фактов. Но недавно я перешел от разработчика .net к java, поэтому я только недавно присоединился к необязательной стороне. кроме того, я бы предпочел заявить это как комментарий, но поскольку мой уровень точки не позволяет мне комментировать, я вынужден вместо этого поставить это в качестве ответа.

то, что я делал, что послужило мне хорошим эмпирическим правилом. Использовать варианты для возвращаемых типов, и использовать только Optionals как параметры, если мне требуется как значение необязательного, так и погода или нет, необязательный имел значение в методе.

Если я забочусь только о значении, я проверяю isPresent перед вызовом метода, если у меня есть какой-то журнал или другая логика в методе, которая зависит от того, существует ли значение, тогда я с радостью передам необязательный.

Это потому, что у нас разные требования к пользователю API и разработчику API.

разработчик несет ответственность за предоставление точной спецификации и правильного внедрения. Поэтому, если разработчик уже знает, что аргумент является необязательным, реализация должна правильно обращаться с ним, будь то null или необязательный. API должен быть максимально простым для пользователя, а null-самым простым.

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

прежде всего, если вы используете метод 3, Вы можете заменить эти последние 14 строк кода следующим:

int result = myObject.calculateSomething(p1.orElse(null), p2.orElse(null));

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

public int calculateSomething(String p1OrNull, BigDecimal p2OrNull)

таким образом, ясно, что допустимы нулевые значения.

использование p1.orElse(null) иллюстрирует, как многословный наш код получает при использовании Optional, что является частью того, почему я избегаю его. Необязательный был написан для функционального программирования. Потокам это нужно. Ваши методы, вероятно, никогда не должны возвращать необязательный, если это не необходимо использовать их в функциональном программировании. Есть такие методы, как Optional.flatMap() метод, который требует Ссылки на функцию, которая возвращает необязательно. Вот его подпись:

public <U> Optional<U> flatMap(Function<? super T, ? extends Optional<? extends U>> mapper)

так что обычно это единственная веская причина для написания метода, который возвращает необязательный. Но даже там этого можно избежать. Вы можете передать геттер, который не возвращает необязательный метод, например flatMap (), обернув его в другой метод, который преобразует функцию в правильный тип. Метод обертки выглядит следующим образом:

public static <T, U> Function<? super T, Optional<U>> optFun(Function<T, U> function) {
    return t -> Optional.ofNullable(function.apply(t));
}

Итак, предположим, у вас есть геттер такой: String getName()

вы не можете передать его в flatMap следующим образом:

opt.flatMap(Widget::getName) // Won't work!

но вы можете передать это так:

opt.flatMap(optFun(Widget::getName)) // Works great!

за пределами функционального программирования следует избегать дополнительных процедур.

Брайан Гетц сказал это лучше всего, когда он сказал это:

причина необязательный был добавлен в Java, потому что это:

return Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
    .stream()
    .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
    .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
    .filter(m -> Objects.equals(m.getReturnType(), returnType))
    .findFirst()
    .getOrThrow(() -> new InternalError(...));

чище, чем этот:

Method matching =
    Arrays.asList(enclosingInfo.getEnclosingClass().getDeclaredMethods())
    .stream()
    .filter(m -> Objects.equals(m.getName(), enclosingInfo.getName())
    .filter(m ->  Arrays.equals(m.getParameterTypes(), parameterClasses))
    .filter(m -> Objects.equals(m.getReturnType(), returnType))
    .getFirst();
if (matching == null)
  throw new InternalError("Enclosing method not found");
return matching;