Java 8 в Android со Stream API и лямбдами

9.01.2015 / 14:32 от aNNiMON
Android

Как вы знаете, Android основан на Java 6. Google не спешит добавлять поддержку новых версий Java, поэтому приходится мечтать даже об использовании try-with-resources, multi-catch из Java 7, не говоря уже о Lambda Expressions и Stream API.
   Но если чего-то сильно захотеть, то никакие преграды не остановят, ведь так? Если мы не можем запустить код из Java 8 в Android-приложениях, то почему бы не преобразовать его в Java 6 совместимый? Именно это и делает проект Retrolambda финского разработчика Esko Luontola.


Retrolambda
   На странице проекта есть описание и короткое видео, демонстрирующее, как работает преобразование.
Вкратце, Retrolambda анализирует байт-код скомпилированных классов, ищет вызовы лямбда-выражений, ссылок на методы и прочие нововведения и заменяет их на те аналоги, которые уже присутствуют в указанной версии Java. То есть, лямбды заменятся анонимными классами, multi-catch (если мы компилируем для Java 6) заменится на несколько одинарных вызовов catch и т.д.
   Это даёт нам массу безграничных возможностей. Теперь мы можем писать меньше кода, а на этапе компиляции он автоматически адаптируется под наши нужды.

   Пример:
   В Android очень долго вешать обработчики на кнопки или другие элементы. Настолько этот процесс утомителен, что разработчики стараются упростить это использованием аннотаций или автоматическим генерированием кода. В Java 8 всё становится проще, если использовать лямбды:
  1. mButton.setOnClickListener(v -> v.setBackgroundColor(0xFF00FF00));
При сборке, Retrolambda заменит байткод, соответствующий вызову лямбда-выражения, на вызов анонимного класса, что будет равносильно:
  1. mButton.setOnClickListener(new View.OnClickListener() {
  2.     @Override
  3.     public void onClick(View v) {
  4.         v.setBackgroundColor(0xFF00FF00);
  5.     }
  6. });
Так зачем писать больше?

   Надеюсь, к этому моменту я заинтересовал вас в использовании Java 8 в Android. Теперь поговорим о том, как всё это дело настроить для работы.

Существует плагин для Maven и для Gradle.
Если для сборки вы используете Maven, добавьте в pom.xml следующий код:
  1. <plugin>
  2.     <groupId>net.orfjackal.retrolambda</groupId>
  3.     <artifactId>retrolambda-maven-plugin</artifactId>
  4.     <version>1.8.1</version>
  5.     <executions>
  6.         <execution>
  7.             <goals>
  8.                 <goal>process-main</goal>
  9.                 <goal>process-test</goal>
  10.             </goals>
  11.         </execution>
  12.     </executions>
  13. </plugin>
Дополнительную информацию можно найти здесь

Для Gradle всё так же просто — в build.gradle добавьте:
  1. buildscript {
  2.   repositories {
  3.      mavenCentral()
  4.   }
  5.   dependencies {
  6.      classpath 'me.tatarka:gradle-retrolambda:2.5.0'
  7.   }
  8. }
  9.  
  10. repositories {
  11.   mavenCentral()
  12. }
  13.  
  14. apply plugin: 'me.tatarka.retrolambda'

Для Android Studio нужно также указать версию Java. Всё в том же build.gradle укажите:
  1. android {
  2.   compileOptions {
  3.     sourceCompatibility JavaVersion.VERSION_1_8
  4.     targetCompatibility JavaVersion.VERSION_1_8
  5.   }
  6. }

Подробное описание приведено на странице проекта плагина Gradle.

После этого можно приступать к использованию всех вкусностей Java 8 (и Java 7) в своих приложениях. Поддерживаются:
  - (Java 8) лямбда-выражения: i -> i * i;
  - (Java 8) ссылки на методы: Integer::compare;
  - (Java 7) try-with-resources: try (InputStream is = ...) { .. };
  - (Java 7) multi-catch: try { .. } catch (NullPointerException | IOException ex) { .. };
  - (Java 7) switch для строк: switch (str) { case "add": ... case "remove": ... }

Автор обещает в скором времени добавить поддержку статических методов и методов по умолчанию в интерфейсах.

ВАЖНО: Retrolambda изменяет только языковые возможности, стандартные классы из Java 8 использовать не получится.


Stream API
   Ввиду того, что с Java 8 API в Android работать нельзя, но очень хочется использовать Stream API и другие полезные классы, почему бы нам не переписать всё нужное под Java 6?
   В библиотеке Lightweight-Stream-API как раз это и сделано. Мы имеем:
  - (Java 8) легковесный вариант Stream API (реализованный на итераторах, без параллельной обработки);
  - (Java 8) функциональный интерфейс (интерфейсы Predicate, Function, Consumer и прочие);
  - (Java 8) класс Optional;
  - (Java 7) класс Objects.

   Библиотека занимает порядка 30 килобайт, есть версии для Java ME, так что при желании, можно использовать в любой версии Java. Скачать можно на этой странице.

   Пример:
   Допустим есть список линий, а нам нужно отобрать из него 7 красных перпендикулярных прямых линий, вывести информацию о каждой, отсортировать и сохранить их в списке.
Как бы мы это делали:
  1. List<Lines> red7lines = new ArrayList<>();
  2. for (Line line : allLines) {    
  3.     if (line.getColor() == RED) {
  4.         System.out.println(line);
  5.         red7lines.add(line);
  6.     }
  7.     if (red7lines.size() >= 7) break;
  8. }
  9. Collections.sort(red7lines);

А вот как это делается с лямбдами и Lightweight-Stream-API:
  1. List<Lines> red7lines = Stream.of(allLines)
  2.         .filter(line -> line.getColor() == RED)
  3.         .limit(7)
  4.         .peek(System.out::println)
  5.         .sorted()
  6.         .collect(Collectors.toList());

Операции над Stream:
  - filter(Predicate p) - фильтрует данные по заданному критерию p;
  - map(Function f) - применяет функцию к каждому элементу списка. Например, map(p -> p + 1) - увеличивает значение каждого элемента на 1, map(p -> Integer.toHexString(p)) - преобразовывает числа в строку в HEX;
  - flatMap(Function f) - как и map, применяет функцию, но работает с другим Stream, упаковывая его в один (без примера не обойтись, будет ниже);
  - distinct() - отбирает дублирующиеся значения;
  - sorted() / sorted(Comparator c) - сортирует данные;
  - peek(Consumer c) - для каждого элемента вызывает заданный обработчик. Пригодится для вывода элементов перед помещением в список, как было показано выше;
  - limit(long max) - ограничивает набор элементов;
  - skip(long n) - пропускает заданное количество элементов;
  - forEach(Consumer c) - работает, как и peek, но после forEach больше нельзя обращаться к Stream - то есть это окончательная операция;
  - reduce(identity, BiFunction f) / reduce(BiFunction f) - применяет функцию с двумя аргументами для получения результата (пример ниже);
  - collect(Supplier s, BiConsumer acc) / collect(Collector c) - собирает элементы в заданный контейнер. Можно применять как для сбора элементов в список/множество, сбора символов в строку, так и для подсчёта среднего значения в числовых элементах;
  - min(Comparator c) / max(Comparator c) - находит минимальное/максимальное значение;
  - count() - возвращает количество элементов;
  - anyMatch(Predicate p) - возвращает true, если хотя бы один элемент соответствует критерию;
  - allMatch(Predicate p) - возвращает true, если все элементы соответствуют критерию;
  - noneMatch(Predicate p) - возвращает true, если все элементы НЕ соответствуют критерию;
  - findFirst() - возвращает первый элемент в Stream.

Несколько примеров самых важных функций:
  1. Stream.ofRange(0, 10).map(p -> p * p)
Возводит в квадрат каждое число от 0 до 9 включительно.

  1. Stream.ofRange(0, 10).map(p -> String.format("%d ^ 2 = %d", p, p * p))
Преобразует числа в строки вида: "1 ^ 2 = 1", "2 ^ 2 = 4", "3 ^ 2 = 9", ..., "9 ^ 2 = 81"

  1. Stream.ofRange(2, 10).flatMap(
  2.     i -> Stream.ofRange(2, 10).map(
  3.         j -> String.format("%d*%d=%d ", i, j, (i*j))
  4.     )
  5. ).forEach(System.out::println);
Выводит таблицу умножения:
2*2=4, 2*3=6, ...
3*2=6, 3*3=9, ...
...
9*2=18, 9*3=27, ...
Иными словами, у нас были числа {2,3,4,5,6,7,8,9}, а мы получили {2[2],2[3],...,2[9],3[1],3[2],...,3[9],...,9[8],9[9]} — вот это и есть flatMap, он "упаковывает" второй Stream в первый.

  1. Stream.ofRange(0, 100).reduce(0, (i,j) -> i + j)
Суммирует все числа от 0 до 100. Вызов будет происходить так:
0,0 -> 0
  0,1 -> 1
   1,2 -> 3
    3,3 -> 6
     6,4 -> 10
      10,5 -> 15

Точно так же, можно сделать умножение:
  1. Stream.of(4,6,8,10).reduce(10, (i,j) -> i * j)
10,4 -> 40
  40,6 -> 240
   240,8 -> 1920
    1920,10 -> 19200

Если в reduce первый аргумент не указывать, то за начальный аргумент принимается первый элемент в стриме (при этом функция не применяется)
  1. Stream.of("a","b","c").reduce((i,j) -> String.format("%s..%s", i, j));
??,"a" -> "a" (функция не применяется)
"a","b" -> "a..b"
  "a..b","c" -> "a..b..c"


Пример для Android
   Для демонстрации возможностей Retrolambda и Lightweight-Stream-API создадим небольшое приложение. Из файла будет считываться набор слов [фраза - перевод] и выводиться в список. Далее с этим списком мы будем производить различные операции с использованием Stream API.

   После создания проекта в Android Studio, кидаем Lightweight-Stream-API.jar в папку app/libs, открываем app/build.gradle и дописываем необходимые зависимости. Пример файла build.gradle

   Данные будем грузить из файла words.txt в папке assets. Фразы отделены друг от друга символом табуляции. Пример содержимого файла:
baffled сбитый с толку
bedrock основа
beet свёкла
brittle хрупкий
burn out выгореть, сгореть дотла
bushes кусты
be yourself; everyone else is already taken. oscar wilde. Будь собой, все остальные роли уже разобраны. Оскар Уальд.

Сначала прочитаем все строки в файле.
  1. final List<String> lines = new ArrayList<>();
  2. try (final InputStream is = context.getAssets().open("words.txt");
  3.      final InputStreamReader isr = new InputStreamReader(is, "UTF-8");
  4.      final BufferedReader reader = new BufferedReader(isr)) {
  5.     String line;
  6.     while ( (line = reader.readLine()) != null ) {
  7.         lines.add(line);
  8.     }
  9. } catch (IOException e) {
  10.     Log.e("Java 8 Example", "Utils.readWords", e);
  11. }
Здесь мы используем try-with-resources, который в обычном случае разрешено использовать только для minSdkVersion >= 19.
Далее, преобразуем список строк в список классов Word.
  1. return Stream.of(lines)
  2.         .map(str -> str.split("\t"))
  3.         .filter(arr -> arr.length == 2)
  4.         .map(arr -> new Word(arr[0], arr[1]))
  5.         .collect(Collectors.toList(new Word[0]));
Вот как будет выглядеть преобразование на примере одной строки:
str = "bedrock основа" -> str.split("\t")
  arr = {"bedrock", "основа"} -> отбираем элементы у которых длина массива = 2
   arr = {"bedrock", "основа"} -> new Word(arr[0], arr[1])
     собираем всё в список

Затем полученный список возвращается и заполняется адаптер:
  1. mAdapter = new WordAdapter(this, Utils.readWords(this));

Есть три кнопки: Distinct, Sort, Info.
Distinct убирает дубликаты в списке.
  1. findViewById(R.id.distinct).setOnClickListener(v -> {
  2.     Stream.of(mAdapter.getCurrentList())
  3.             .distinct()
  4.             .collect(Utils.collectAdapter(mAdapter));
  5. });
Sort, понятно, сортирует.
  1. findViewById(R.id.sort).setOnClickListener(v -> {
  2.     Stream.of(mAdapter.getCurrentList())
  3.             .sorted()
  4.             .collect(Utils.collectAdapter(mAdapter));
  5. });
Info показывает информацию о количестве элементов всего в адаптере и в ListView.
  1. findViewById(R.id.info).setOnClickListener(v -> {
  2.     long all = mAdapter.getWords().size();
  3.     long list = mListView.getCount();
  4.     String text = String.format("%d items all\n%d items in list", all, list);
  5.     Toast.makeText(getApplicationContext(), text, Toast.LENGTH_SHORT).show();
  6. });

Основные действия происходят по нажатию кнопки Go
  1. findViewById(R.id.go).setOnClickListener(v -> {
  2.     final int index = mActionSpinner.getSelectedItemPosition();
  3.     if (index != Spinner.INVALID_POSITION) {
  4.         action(actions[index]);
  5.     }
  6. });
У нас будут команды "filter 1", "add index", "group" и т.д. На основе этих команд мы будем выполнять нужные операции. Здесь удобно использовать switch для строк:
  1. switch (action) {
  2.     case "filter 1": /* ... */ break;
  3.     case "filter 2+":/* ... */ break;
  4.     case "filter %N words": /* ... */ break;
  5.     case "add index": /* ... */ break;
  6. }

Теперь об основных операциях.
Если нам нужно выбрать только те фразы, которые состоят из одного слова, то разбиваем строку по пробелу и проверяем полученный размер массива:
  1. stream.filter(p -> p.getWord().split(" ").length == 1)

Отобрать 2 и более строк:
  1. stream.filter(p -> p.getWord().split(" ").length >= 2)
stream_api_android_1.png

Отобразить длину строки перевода:
  1. stream.map(p -> p.setWord( String.valueOf(p.getTranslate().length()) ))
  stream_api_android_4.png

Вывести, содержится ли подстрока в заданном наборе:
  1. stream.map(p -> print(p.getWord().contains("ok") ? "yes" : "no"))
stream_api_android_3.png

Добавить порядковый номер каждому элементу:
  1. Stream.ofRange(0, mAdapter.getCount())
  2.     .map(i -> String.format("%d. %s", i+1, mAdapter.getItem(i).getWord()))
  3.     .map(str -> new Word(str, ""));
Здесь мы создаём стрим от 0 до [кол-во элементов], затем добавляем это (число+1) к заданному элементу.
stream_api_android_5.png
Выглядит так:
  1. 0 -> "1. " + getItem(0)
  2.  1 -> "2. " + getItem(1)
  3.   2 -> "3. " + getItem(2)

Сгруппировать по первому символу:
  1. Stream.ofRange('a', 'z'+1)
  2.     .map(i -> String.valueOf((char) i.shortValue()))
  3.     .flatMap(s -> Stream.of(mAdapter.getWords())
  4.             .filter(w -> w.getWord().startsWith(s))
  5.             .limit(5))
  6.     .map(w -> new Word(String.valueOf(w.getWord().charAt(0)), w.getWord()));
stream_api_android_7.png
Создаём стрим от кода символа 'a' до 'z' включительно, переводим символ в строку, применяем flatMap к 5 фразам, начинающимся с заданной буквы, выводим [буквы, фраза]
'a' -> "a" -> [фразы -> фразы, начинающиеся на "a" -> 5 фраз, начинающихся на "a"]
'b' -> "b" -> [фразы -> фразы, начинающиеся на "b" -> 5 фраз, начинающихся на "b"]
и т.д.


   Без лямбд и Stream API, пришлось бы писать много вложенных циклов, сравнений и присвоений, а так, получается очень компактно.
stream_api_android_2.png stream_api_android_6.png

Исходный код проекта: GitHub или Java8StreamExample.zip
Приложение: Java8StreamExample.apk
Библиотека Lightweight-Stream-API: GitHub | версия для Java ME
Retrolambda: https://github.com/orfjackal/retrolambda
retrolambda, stream api, java 8
+9   10   1
12576
Похожие статьи
Многопоточность в Java. Основы
Паттернология. Система команд
Java ME to Android

  © aNNiMON (Melnik Software)
Онлайн: 10 (3/7)
 
Яндекс.Метрика