Статьи

Java 8 SE Необязательно: строгий подход

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

обзор

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

Все цитаты, которые не принадлежат кому-то еще, взяты из поста Стивена. Хотя это не является строго необходимым, я рекомендую сначала прочитать его. Но не забудьте вернуться!

Я создал три гистограммы, которые я представляю в этом посте: один и тот же пример в версии Стивена , моя основная версия и моя расширенная версия .

отказ

Стивен Колебурн — легенда Java. Цитирую сообщение Маркуса Эйзела « Герои Явы» о нем:

Стивен Колебурн является членом технического персонала OpenGamma. Он широко известен своей работой с открытым исходным кодом и его блогом. Он создал Joda-Time, который сейчас дорабатывается как JSR-310 / ThreeTen. Он участвует в дискуссиях о будущем Java, включая предложения для оператора Diamond для дженериков и замыканий FCM, которые близки к принятым изменениям в Java 7 и 8. Стивен часто выступает на конференциях, JavaOne Rock Star и Java Champion ,

Я имел удовольствие внести свой вклад в Альянс собственности Стивена, и это укрепило мое мнение о нем как о чрезвычайно компетентном разработчике и очень обдуманном человеке.

Все это говорит о том, что, если сомневаешься, доверься ему.

Тогда есть факт, что его подход основан на аксиоме, что Optional должен использоваться исключительно как тип возврата. Это абсолютно соответствует рекомендациям тех, кто представил класс в первую очередь. Цитируя Брайана Гетца :

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

[…] Вы почти никогда не должны использовать его в качестве поля чего-либо или параметра метода.

Так что, если сомневаетесь, доверяйте его мнению по моему.

соседство

Конечно, даже лучше, чем просто доверять кому-либо, — принять решение. Итак, вот мои аргументы в отличие от аргументов Стивена.

Основные пункты

Вот пять основных пунктов Стивена:

  1. Не объявляйте никакую переменную экземпляра типа Optional.
  2. Используйте null, чтобы указать необязательные данные в закрытой области видимости класса.
  3. Используйте Optional для получателей, которые обращаются к необязательному полю.
  4. Не используйте Optional в установщиках или конструкторах.
  5. Используйте Optional в качестве типа возврата для любых других методов бизнес-логики, которые имеют необязательный результат.

Вот мой:

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

Примеры

Давайте сравним примеры. Его это:

Address.java Стивен Коулборн

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Address {
 
    private final String addressLine;  // never null
    private final String city;         // never null
    private final String postcode;     // optional, thus may be null
 
    // constructor ensures non-null fields really are non-null
    // optional field can just be stored directly, as null means optional
    public Address(String addressLine, String city, String postcode) {
        this.addressLine = Preconditions.chckNotNull(addressLine);
        this.city = Preconditions.chckNotNull(city);
        this.postcode = postcode;
    }
 
    // normal getters
    public String getAddressLine() {
        return addressLine;
    }
 
    public String getCity() {
        return city;
    }
 
    // special getter for optional field
    public Optional<String> getPostcode() {
        return Optional.ofNullable(postcode);
    }
 
    // return optional instead of null for business logic methods that may not find a result
    public static Optional<Address> findAddress(String userInput) {
        return... // find the address, returning Optional.empty() if not found
    }
 
}

Мне нравится, что ни один потребитель этого класса не может получить ноль. Мне не нравится, как вам все еще приходится иметь дело с этим — в классе, но и без.

Это будет моя (базовая) версия:

Address.java By Me (Основная версия)

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
public class Address {
 
    // look ma, no comments required
 
    private final String addressLine;
    private final String city;
    private final Optional<String> postcode;
 
    // nobody has to look at this constructor to check which parameters are
    // allowed to be null because of course none are!
 
    public Address(String addressLine, String city, Optional<String> postcode) {
        this.addressLine = requireNonNull(addressLine,
                "The argument 'addressLine' must not be null.");
        this.city = requireNonNull(city,
                "The argument 'city' must not be null.");
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    // of course methods that might not have a result
    // return 'Optional' instead of null
 
    public static Optional<Address> findAddress(String userInput) {
        // find the address, returning Optional.empty() if not found
    }
 
    // getters are straight forward and can be generated
 
    public String getAddressLine() {
        return addressLine;
    }
 
    public String getCity() {
        return city;
    }
 
    // look how the field's type matches the getter's type;
    // nice for bean-based code/tools
 
    public Optional<String> getPostcode() {
        return postcode;
    }
 
}

Здесь просто нет нулей.

Различия

Ограниченная проблема

Внутри объекта разработчик все еще вынужден думать о нуле и управлять им, используя! = Нуль проверки. Это разумно, поскольку проблема нуля ограничена. Весь код будет написан и протестирован как единое целое (вы пишете тесты, не так ли?), Поэтому нулевые значения не вызовут много проблем.

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

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

Очевидное Необязательное Очевидное

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class Address {
 
    // look ma, no comments required
 
    private final String addressLine;
    private final String city;
    private Optional<String> postcode;
 
    // nobody has to look at these constructors to check which parameters are
    // allowed to be null because of course none are!
 
    public Address(String addressLine, String city, Optional<String> postcode) {
        this.addressLine = requireNonNull(addressLine,
                "The argument 'addressLine' must not be null.");
        this.city = requireNonNull(city,
                "The argument 'city' must not be null.");
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    public Address(String addressLine, String city, String postcode) {
        // use 'requireNonNull' inside Optional factory method
        // if you prefer a verbose exception message;
        // otherwise 'Optional.of(postcode)' suffices
        this(addressLine, city, Optional.of(
                requireNonNull(postcode,
                        "The argument 'postcode' must not be null.")));
    }
 
    public Address(String addressLine, String city) {
        this(addressLine, city, Optional.empty());
    }
 
    // now if some method needs to use the postcode,
    // we can not overlook the fact that it is optional
 
    public int comparePostcode(Address other) {
        // without Optionals we might overlook that the postcode
        // could be missing and do this:
        // return this.postcode.compareTo(other.postcode);
 
        if (this.postcode.isPresent() && other.postcode.isPresent())
            return this.postcode.get().compareTo(other.postcode.get());
        else if (this.postcode.isPresent())
            return 1;
        else if (other.postcode.isPresent())
            return -1;
        else
            return 0;
    }
 
    // of course methods that might not have a result
    // return 'Optional' instead of null
 
    public static Optional<Address> findAddress(String userInput) {
        // find the address, returning Optional.empty() if not found
    }
 
    // getters are straight forward and can be generated
 
    public String getAddressLine() {
        return addressLine;
    }
 
    public String getCity() {
        return city;
    }
 
    // look how the field's type matches the getter's type;
    // nice for bean-based code/tools
 
    public Optional<String> getPostcode() {
        return postcode;
    }
 
    // in case this 'Address' is mutable
    // (which it probably shouldn't be but let's presume it is)
    // you can decide whether you prefer a setter that takes an 'Optional',
    // a pair of methods to set an existing and an empty postcode, or both
 
    public void setPostcode(Optional<String> postcode) {
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    public void setPostcode(String postcode) {
        // again you might want to use 'requireNonNull'
        // if you prefer a verbose exception message;
        this.postcode = Optional.of(
                requireNonNull(postcode,
                        "The argument 'postcode' must not be null."));
    }
 
    public void setEmptyPostcode() {
        this.postcode = Optional.empty();
    }
 
}

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

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

Производительность

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

Для него это причина предпочитать ноль.

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

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

Кстати, тот же аргумент может быть использован для использования массивов вместо ArrayLists или char-массивов вместо строк. Я уверен, что никто не последует этому совету без значительного прироста производительности.

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

Сериализуемость

Хотя это второстепенный вопрос, следует отметить, что класс может быть Serializable, что невозможно, если какое-либо поле является необязательным (так как Optional не реализует Serializable).

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

удобство

[I] Мой опыт показывает, что наличие Optional на установщике или конструкторе раздражает вызывающих, так как они обычно имеют реальный объект. Принуждение вызывающей стороны к переносу параметра в Optional — раздражение, которое я предпочел бы не причинять пользователям. (т.е. удобство превосходит строгость при вводе)

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class Address {
 
    // look ma, no comments required
 
    private final String addressLine;
    private final String city;
    private Optional<String> postcode;
 
    // nobody has to look at these constructors to check which parameters are
    // allowed to be null because of course none are!
 
    public Address(String addressLine, String city, Optional<String> postcode) {
        this.addressLine = requireNonNull(addressLine,
                "The argument 'addressLine' must not be null.");
        this.city = requireNonNull(city,
                "The argument 'city' must not be null.");
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    public Address(String addressLine, String city, String postcode) {
        // use 'requireNonNull' inside Optional factory method
        // if you prefer a verbose exception message;
        // otherwise 'Optional.of(postcode)' suffices
        this(addressLine, city, Optional.of(
                requireNonNull(postcode,
                        "The argument 'postcode' must not be null.")));
    }
 
    public Address(String addressLine, String city) {
        this(addressLine, city, Optional.empty());
    }
 
    // now if some method needs to use the postcode,
    // we can not overlook the fact that it is optional
 
    public int comparePostcode(Address other) {
        // without Optionals we might overlook that the postcode
        // could be missing and do this:
        // return this.postcode.compareTo(other.postcode);
 
        if (this.postcode.isPresent() && other.postcode.isPresent())
            return this.postcode.get().compareTo(other.postcode.get());
        else if (this.postcode.isPresent())
            return 1;
        else if (other.postcode.isPresent())
            return -1;
        else
            return 0;
    }
 
    // of course methods that might not have a result
    // return 'Optional' instead of null
 
    public static Optional<Address> findAddress(String userInput) {
        // find the address, returning Optional.empty() if not found
    }
 
    // getters are straight forward and can be generated
 
    public String getAddressLine() {
        return addressLine;
    }
 
    public String getCity() {
        return city;
    }
 
    // look how the field's type matches the getter's type;
    // nice for bean-based code/tools
 
    public Optional<String> getPostcode() {
        return postcode;
    }
 
    // in case this 'Address' is mutable
    // (which it probably shouldn't be but let's presume it is)
    // you can decide whether you prefer a setter that takes an 'Optional',
    // a pair of methods to set an existing and an empty postcode, or both
 
    public void setPostcode(Optional<String> postcode) {
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    public void setPostcode(String postcode) {
        // again you might want to use 'requireNonNull'
        // if you prefer a verbose exception message;
        this.postcode = Optional.of(
                requireNonNull(postcode,
                        "The argument 'postcode' must not be null."));
    }
 
    public void setEmptyPostcode() {
        this.postcode = Optional.empty();
    }
 
}

Конечно, это плохо масштабируется со многими дополнительными полями. В этом случае шаблон строителя поможет.

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

С полем типа Optional сеттер может выглядеть так:

Перегруженные сеттеры, чтобы избежать создания опций

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
public class Address {
 
    // look ma, no comments required
 
    private final String addressLine;
    private final String city;
    private Optional<String> postcode;
 
    // nobody has to look at these constructors to check which parameters are
    // allowed to be null because of course none are!
 
    public Address(String addressLine, String city, Optional<String> postcode) {
        this.addressLine = requireNonNull(addressLine,
                "The argument 'addressLine' must not be null.");
        this.city = requireNonNull(city,
                "The argument 'city' must not be null.");
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    public Address(String addressLine, String city, String postcode) {
        // use 'requireNonNull' inside Optional factory method
        // if you prefer a verbose exception message;
        // otherwise 'Optional.of(postcode)' suffices
        this(addressLine, city, Optional.of(
                requireNonNull(postcode,
                        "The argument 'postcode' must not be null.")));
    }
 
    public Address(String addressLine, String city) {
        this(addressLine, city, Optional.empty());
    }
 
    // now if some method needs to use the postcode,
    // we can not overlook the fact that it is optional
 
    public int comparePostcode(Address other) {
        // without Optionals we might overlook that the postcode
        // could be missing and do this:
        // return this.postcode.compareTo(other.postcode);
 
        if (this.postcode.isPresent() && other.postcode.isPresent())
            return this.postcode.get().compareTo(other.postcode.get());
        else if (this.postcode.isPresent())
            return 1;
        else if (other.postcode.isPresent())
            return -1;
        else
            return 0;
    }
 
    // of course methods that might not have a result
    // return 'Optional' instead of null
 
    public static Optional<Address> findAddress(String userInput) {
        // find the address, returning Optional.empty() if not found
    }
 
    // getters are straight forward and can be generated
 
    public String getAddressLine() {
        return addressLine;
    }
 
    public String getCity() {
        return city;
    }
 
    // look how the field's type matches the getter's type;
    // nice for bean-based code/tools
 
    public Optional<String> getPostcode() {
        return postcode;
    }
 
    // in case this 'Address' is mutable
    // (which it probably shouldn't be but let's presume it is)
    // you can decide whether you prefer a setter that takes an 'Optional',
    // a pair of methods to set an existing and an empty postcode, or both
 
    public void setPostcode(Optional<String> postcode) {
        this.postcode = requireNonNull(postcode,
                "The argument 'postcode' must not be null.");
    }
 
    public void setPostcode(String postcode) {
        // again you might want to use 'requireNonNull'
        // if you prefer a verbose exception message;
        this.postcode = Optional.of(
                requireNonNull(postcode,
                        "The argument 'postcode' must not be null."));
    }
 
    public void setEmptyPostcode() {
        this.postcode = Optional.empty();
    }
 
}

Опять же, все значения NULL немедленно отвечают с исключением.

Фасоль

С другой стороны, этот подход приводит к объектам, которые не являются bean-компонентами.

Ага. Наличие поля типа Optional не страдает от этого.

Общность

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

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

Это великая цель для достижения! И, следуя совету Стивена, вы проделаете большую часть пути туда. Так что не принимайте моё несогласие как причину не использовать Optional хотя бы так много.

Все, что я говорю, — это то, что я вижу небольшую причину, чтобы даже не запретить ноль!

отражение

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

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

Что вы думаете?

Ссылка: Java 8 SE Необязательно, строгий подход нашего партнера JCG Николая Парлога в блоге CodeFx .