Статьи

Избавление от кода Boilerplate с помощью лямбда-выражений Java

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

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

фильтрация

Давайте рассмотрим существование интерфейса с именем Predicate, определенного следующим образом:

interface Predicate<T> {
   public boolean test(T t);
}

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

static <T> List<T> filter(Predicate<T> predicate, List<T> source) {
    List<T> destiny = new ArrayList<>();
    for(T item : source) {
      if(predicate.test(item)){
         destiny.add(item);
      }
    }
   return destiny;
}

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

List<Integer> numbers = asList(1,2,3,4,5,6,7,8,9);
List<Integer> onlyOdds = filter(new Predicate<Integer>(){
                           @Override
                           public boolean test(Integer n) {
                              return n % 2 != 0;
                           }
                          }, numbers);

Но рассмотрим весь шаблонный код, который был необходим здесь, чтобы просто сказать, что мы хотели бы принять значение n и проверить, является ли оно нечетным числом. Очевидно, это не выглядит хорошо.

В Java 8 мы могли бы избавиться от всего этого беспорядка, просто реализовав наш предикат с использованием лямбда-выражения следующим образом:

List<Integer> numbers = asList(1,2,3,4,5,6,7,8,9);
List<Integer> onlyOdds = filter(n –> n % 2 !=0, numbers);

Здесь лямбда-выражение будет оцениваться для экземпляра типа Predicate, его аргумент n соответствует аргументу, ожидаемому его тестом метода, а тело выражений представляет реализацию метода.

картографирование

Давайте теперь рассмотрим существование функции интерфейса, определенной следующим образом:

interface Function<T,R> {
   public R apply(T t);
}

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

static <T,R> List<R> map(Function<T,R> function, List<T> source){
   List<R> destiny = new ArrayList<>();
   for(T item : source) {
      R value = function.apply(item);
      destiny.add(value);
   }
   return destiny;
}

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

List<String> digits = asList("1","2","3","4","5","6","7","8","9");
List<Integer> numbers = map(new Function<String, Integer() {
                          @Override
                          public Integer apply(String digit) {
                            return Integer.valueOf(digit);
                          }
                        }digits);

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

List<String> digits = asList("1","2","3","4","5","6","7","8","9");
List<Integer> numbers = map(s –> Integer.valueOf(s), digits);

Это явно намного лучше. Еще раз, лямбда-выражение оценивает экземпляр типа Function <String, Integer>, где s представляет аргумент для своей функции apply, а тело лямбда-выражения представляет то, что функция вернула бы в своем теле.

Снижение

Давайте теперь рассмотрим существование интерфейса BinaryOperator, определенного следующим образом:

interface BinaryOperator<T> {
   public T apply(T left, T right);
}

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

static <T> T reduce(BinaryOperator<T> operator,T seed, List<T> source){
    for(T item: source) {
       seed = operator.apply(seed, item);
    }
    return seed;
}

Рассмотрим теперь, что у нас есть список чисел, и мы хотели бы получить для суммирования их всех. Еще раз, если мы намереваемся использовать этот код, как мы традиционно делали до выпуска Java 8, мы были бы вынуждены следовать подробному определению, как изложено ниже:

List<Integer> numbers = asList(1,2,3,4,5);
Integer sum = reduce(new BinaryOperator<Integer>() {
                     @Override
                     public Integer apply(Integer left, Integer right){
                        return left + right;
                     }
                    },0,numbers);

Этот код может быть значительно упрощен за счет использования лямбда-выражения следующим образом:

List<Integer> numbers = asList(1,2,3,4,5);
Integer sum = reduce( (x,y) –> x + y, 0, numbers);

Где (x, y) соответствуют двум аргументам слева и справа, которые получает функция apply, и тело выражений будет тем, что она вернет. Обратите внимание, что, поскольку в этом случае мы должны указать два аргумента, лямбда-выражения должны указывать их в скобках, в противном случае компилятор может определить, какие аргументы для лямбда-выражения, а какие — для метода Reduce.

потребляющий

Рассмотрим теперь существование функционального интерфейса Consumer, определяемого следующим образом:

interface Consumer<T> {
   public void accept(T t);
}

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

static <T> void forEach(Consumer<T> consumer, List<T> source) {
    for(T item: source) {
       consumer.accept(item);
    }
}

Посмотрите на весь стандартный код, необходимый для создания потребителя, чтобы просто напечатать все элементы списка:

List<Integer> numbers = asList(1,2,3,4,5);
forEach(new Consumer<Integer>(){
   @Override
   public void accept(Integer n) {
      System.out.println(n);
   }
},numbers);

Однако теперь мы можем использовать простое лямбда-выражение для реализации эквивалентной функциональности следующим образом:

List<Integer> numbers = asList(1,2,3,4,5);
forEach(n –> { System.out.println(n); }, numbers);

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

производства

Рассмотрим теперь существование функционального интерфейса поставщика следующим образом:

interface Supplier<T> {
   public T get();
}

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

public static int generateX() {
  return 0;
}
 
public static int generateY() {
   //some expensive calculation here
   return 1;
}
 
public static int calculate(Supplier<Integer> thunkOfX,
                            Supplier<Integer> thunkOfY) {
   int x = thunkOfX.get();
   if(x==0)
      return 0;
   else
      return x + thunkOfY.get();
}

Посредством передачи двух поставщиков здесь мы откладываем оценку x и y до необходимого. Как видите, если x равен 0, y никогда не нужен. Таким образом, используя эту идиому, мы не тратим много времени на дорогостоящие вычисления без необходимости.

До лямбда-выражений вызов вычисления подразумевал бы много стандартного кода следующим образом:

calculate(new Supplier<Integer>() {
  @Override
  public Integer get() {
     return generateX();
  }
},
new Supplier<Integer>() {
   @Override
   public Integer get() {
     return generateY();
   }
});

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

calculate( () –> generateX(), () –> generateY() );

Очевидно, это намного лучше.

Резюме Лямбда-синтаксиса

Итак, это разные способы определения лямбда-выражений:

Predicate<Integer> isOdd = n –> n % 2 == 0;
Function<String, Integer> atoi = s –> Integer.valueOf(s);
BinaryOperator<Integer> product = (x, y) –> x * y
Comparator<Integer> maxInt = (x,y) –> x > y ? x : y;
Consumer<String> printer = s –> { System.out.println(s); };
Supplier<String> producer = () –> "Hello World";
Runnable task = () –> { System.out.println("I am a runnable task");  };

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

int oddSum = asList("1","2","3","4","5").stream()
                                        .map(n –> Integer.valueOf(n))
                                        .filter(n –> n % 2 != 0)
                                        .reduce(0, (x,y) –> x + y); // 9