Статьи

Больше нет оправданий использованию пустых ссылок в Java 8


Тони Хоар представил нулевые ссылки в ALGOL W еще в 1965 году «просто потому, что это было так легко реализовать». После многих лет он сожалел о своем решении, называя это 
«моей ошибкой в ​​миллиард долларов» .

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

Функциональные языки, такие как Haskell или Scala, структурно решают эту проблему, заключая обнуляемые значения в монаду Option / Maybe. Другие императивные языки, такие как Groovy, ввели 
безопасный для разыменования оператор разыменования  (? Оператор) для безопасной навигации по значениям, которые могут быть потенциально нулевыми. 
подобная функция  была предложена (а затем отвергнута) как часть проекта Coin в Java 7.

Честно говоря, я не пропускаю нулевой безопасный оператор разыменования в Java, даже потому что могу представить, что большинство разработчиков начнут злоупотреблять им «просто в кейс». Более того, поскольку в следующей версии Java 8 будут лямбда-выражения, будет просто реализовать монаду Option, которая, как я надеюсь показать в оставшейся части статьи, является гораздо более мощной и гибкой конструкцией. 

Я не хочу углубляться в теорию категорий и объяснять, что такое монада, даже потому что уже есть тонны 
очень хороших
статей,
делающих это, Моя цель — быстро реализовать монаду Option с использованием синтаксиса лямбда-выражений Java 8, а затем показать, как ее использовать, на очень практичном примере. В Scala монада M — это любой класс, имеющий следующие 3 метода:

def map[B](f: A => B): M[B]
def flatMap[B](f: A => M[B]): M[B]
def filter(p: A => Boolean): M[A] 

В частности, вы можете рассматривать монаду Option как обертку вокруг, возможно, отсутствующего значения.
Таким образом, Опцию универсального типа A можно определить следующим образом:
import java.util.functions.Predicate;
public abstract class Option<A> {

    public static final None NONE = new None();

    public abstract <B> Option<B> map(Func1<A, B> f);

    public abstract <B> Option<B> flatMap(Func1<A, Option<B>> f);

    public abstract Option<A> filter(Predicate<? super A> predicate);

    public abstract A getOrElse(A def);

    public static <A> Some<A> some(A value) {
        return new Some(value);
    }

    public static <A> None<A> none() {
        return NONE;
    }

    public static <A> Option<A> asOption(A value) {
        if (value == null) return none(); else return some(value);
    }
} 

Я также добавил несколько удобных фабричных методов для конкретных и некоторых реализаций Option, которые я реализую позже.
Здесь Predicate — это единственный интерфейс метода, определенный в новом пакете java.util.functions:
public interface Predicate<T> {
    boolean test(T t);
}

это используется, чтобы определить, соответствует ли входной объект заданным критериям, в то время как Func1 является другим единственным интерфейсным методом:
public interface Func1<A1, R> {
    R apply(A1 arg1);
} 


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

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

public class None<A> extends Option<A> {

    None() { }

    @Override
    public <B> Option<B> map(Func1<A, B> f) {
        return NONE;
    }

    @Override
    public <B> Option<B> flatMap(Func1<A, Option<B>> f) {
        return NONE;
    }

    @Override
    public Option<A> filter(Predicate<? super A> predicate) {
        return NONE;
    }

    @Override
    public A getOrElse(A def) {
        return def;
    }
}


а другой оборачивает реально существующее значение:
public class Some<A> extends Option<A> {

    private final A value;

    Some(A value) {
        this.value = value;
    }

    @Override
    public <B> Option<B> map(Func1<A, B> f) {
        return some(f.apply(value));
    }

    @Override
    public <B> Option<B> flatMap(Func1<A, Option<B>> f) {
        return f.apply(value);
    }

    @Override
    public Option<A> filter(Predicate<? super A> predicate) {
        if (predicate.test(value)) return this; else return None.NONE;
    }

    @Override
    public A getOrElse(A def) {
        return value;
    }
} 

Теперь, чтобы попытаться применить Option на конкретном примере, предположим, что у нас есть Map <String, String>, представляющая набор именованных параметров с соответствующими значениями.
Мы хотим разработать метод
int readPositiveIntParam(Map<String, String> params, String name) { // TODO ... }


что, если значение, связанное с данным ключом, является строкой, представляющей положительное целое число, возвращает это целое число, но возвращает ноль во всех других случаях.
Другими словами, мы хотим, чтобы следующий тест прошел:
@Test
public void testMap() {
    Map<String, String> param = new HashMap<String, String>();
    param.put("a", "5");
    param.put("b", "true");
    param.put("c", "-3");

    // the value of the key "a" is a String representing a positive int so return it
    assertEquals(5, readPositiveIntParam(param, "a"));

    // returns zero since the value of the key "b" is not an int
    assertEquals(0, readPositiveIntParam(param, "b"));

    // returns zero since the value of the key "c" is an int but it is negative
    assertEquals(0, readPositiveIntParam(param, "c"));

    // returns zero since there is no key "d" in the map
    assertEquals(0, readPositiveIntParam(param, "d"));
}


Если мы не можем полагаться на наш Опцион, мы должны выполнить эту задачу с помощью чего-то подобного 
int readPositiveIntParam(Map<String, String> params, String name) { 
String value = params.get(name);
if (value == null) return 0;
int i = 0;
try {
i = Integer.parseInt(value);
} catch (NumberFormatException nfe) { }
if (i < 0) return 0;
return i;
}


слишком много условных веток и точек возврата, не так ли?
Используя монаду Option, мы можем достичь того же результата с помощью одного беглого выражения:
int readPositiveIntParam(Map<String, String> params, String name) {
    return asOption(params.get(name))
            .flatMap(FunctionUtils::stringToInt)
            .filter(i -> i > 0)
            .getOrElse(0);
}

где мы использовали вспомогательный статический метод FunctionUtils.stringToInt () в качестве литерала функции, с синтаксисом ::, также введенным в Java 8, определяемым как:
import static Option.*;
public class FunctionUtils {
    public static Option<Integer> stringToInt(String s) {
        try {
            return some(Integer.parseInt(s));
        } catch (NumberFormatException nfe) {
            return none();
        }
    }
}


Этот метод пытается преобразовать String в int и, если не может, возвращает None Option.
Обратите внимание, что мы могли бы также определить это поведение inline, при этом вызывая метод flatMap (), используя анонимное лямбда-выражение, но я советую разработать небольшую библиотеку служебных функций, как я начал здесь, чтобы использовать возможность повторного использования терки допускается функциональным программированием. Я думаю, что сравнение двух методов readPositiveIntParam, которые я предоставил, хорошо иллюстрирует, как широкое использование монады Option может, наконец, позволить нам полностью написать бесплатное программное обеспечение NullPointerException и, в целом, как большее использование функционального программирования может значительно снизить его цикломатическую сложность. ,