JavaRush /Java блог /Архив info.javarush /Никаких оправданий при использовании нулевых указателей в...
dio
16 уровень
Москва

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

Статья из группы Архив info.javarush
Тони Хоар ввел понятие нулего указателя в язык ALGOL W еще в 1965 году: "Просто это было легко реализовать". Через много лет Тони Хоар выразил сожаление в своей речи, названной "моя ошибка на миллиард долларов". К сожалению, подавляющее большинство языков программирования, которые были созданы десятилетиями позже, имели тот же недостаток и создатели языков программирования вместе с рядовыми разработчиками начали искать возможность обойти печально известный NullPointerException. Функциональные языки программирования, такие как Haskell и Scala обошли эту проблемы структурно, упаковав нулевые значения в монады. Другие, императивные языки программирования, такие как Groovy ввели нуль-безопасный оператор разыменования (оператор ?), для безопасной работы с потенциально пустыми переменными. Аналогичное решение, было предложено (а затем отклонено) для Java 7. Честно говоря, я не скучаю по нуль-безопасному оператору разыменования в Java хотя бы потому, что я представляю как большинство разработчиков начнут злоупотреблять им из соображений «на всякий случай». Не хочу углубляться в теорию категорий и объяснить, что такое монада, есть тонны очень хороших статей на эту тему. Предлагаю быстро реализовать собственную монаду, используя синтаксис лямбда выражений в 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 как об оболочке которой нет. Таким образом 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);
    }
}
Я также добавил описание некоторых удобных методов Some и None в экземпляры 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 имеет две конкретные реализации, одна None для пустых значений (то, что к чему мы привыкли, оперируя ошибочной моделью с печально известной нулевой ссылкой)

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 ... }
если ключ типа String, ассоциирован со значением типа String, где значение это строка описывающая положительное целое число, то нужно вернуть это число. Другими словами мы хотим пройти следующий тест:

@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"));
}
Если мы не можем рассчитывать на Option, пришлось бы написать следующий код:

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. FunctionUtils.stringToInt определяется как:

import static Option.*;
public class FunctionUtils {
    public static Option<Integer> stringToInt(String s) {
        try {
            return some(Integer.parseInt(s));
        } catch (NumberFormatException nfe) {
            return none();
        }
    }
}
Этот метод пытается конвертировать строку в число, в случае неудачи возвращает свойство None. Обратите внимание мы могли бы задать такое поведение при вызове flatMap() с использованием анонимного лямбда-выражения, но я советую разработать небольшую библиотеку вспомогательных функций, как начал делать здесь, с целью оптимального использования функционального программирования. Думаю сравнением двух методов readPositiveIntParam, я наглядно продемонстрировал как используя монаду Option, можно написать код свободный от NullPointerException, в целом же используя функциональное программирование, вы можете снизить цикломатическую сложность своего кода. Ссылка на оригинал Перевел
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ