Статьи

Java 8 для разработки под Android: потоковые API и библиотеки дат и времени

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

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

В этом последнем посте мы рассмотрим аннотации типов, функциональные интерфейсы и способы применения более функционального подхода к обработке данных с помощью нового Stream API Java 8.

Я также покажу вам, как получить доступ к некоторым дополнительным функциям Java 8, которые в настоящее время не поддерживаются платформой Android, с помощью библиотек Joda-Time и ThreeTenABP .

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

Аннотации не являются новой функцией (на самом деле они восходят к Java 5.0), но в предыдущих версиях Java аннотации можно было применять только к объявлениям.

С выпуском Java 8 теперь вы можете использовать аннотации везде, где вы использовали тип, включая приемники методов; выражения создания экземпляра класса; реализация интерфейсов; дженерики и массивы; спецификация throws и implements предложения; и тип литья.

К сожалению, хотя Java 8 позволяет использовать аннотации в большем количестве мест, чем когда-либо прежде, она не предоставляет аннотации, специфичные для типов.

Библиотека поддержки аннотаций Android обеспечивает доступ к некоторым дополнительным аннотациям, таким как @Nullable , @NonNull , и аннотациям для проверки типов ресурсов, таких как @DrawableRes , @DimenRes , @ColorRes и @StringRes . Однако вы также можете использовать сторонний инструмент статического анализа, такой как Checker Framework , который был разработан совместно со спецификацией JSR 308 (спецификация «Аннотации на типах Java»). Эта структура предоставляет свой собственный набор аннотаций, которые можно применять к типам, а также ряд «контролеров» (процессоров аннотаций), которые подключаются к процессу компиляции и выполняют определенные «проверки» для каждой аннотации типов, включенной в Checker Framework.

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

Stream API предлагает альтернативный подход «трубы и фильтры» к обработке коллекций.

До Java 8 вы манипулировали коллекциями вручную, как правило, перебирая коллекции и работая по очереди с каждым элементом. Это явное зацикливание потребовало большого количества шаблонной информации, плюс трудно понять структуру цикла for, пока вы не достигнете тела цикла.

Stream API дает вам возможность более эффективно обрабатывать данные, выполняя один прогон этих данных — независимо от объема обрабатываемых данных или от того, выполняете ли вы несколько вычислений.

В Java 8 каждый класс, который реализует java.util.Collection имеет метод stream который может преобразовывать его экземпляры в объекты Stream . Например, если у вас есть Array :

1
String[] myArray = new String[]{«A», «B», «C», «D»};

Затем вы можете преобразовать его в поток с помощью следующего:

1
Stream<String> myStream = Arrays.stream(myArray);

Stream API обрабатывает данные, передавая значения из источника через ряд вычислительных шагов, известных как потоковый конвейер . Потоковый конвейер состоит из следующего:

  • Источник, такой как Collection , массив или функция генератора.
  • Ноль или более промежуточных «ленивых» операций. Промежуточные операции не запускают обработку элементов, пока вы не вызовете терминальную операцию — вот почему они считаются ленивыми.   Например, вызов Stream.filter() для источника данных просто устанавливает конвейер потока; фильтрация фактически не происходит, пока вы не вызовете операцию терминала. Это позволяет объединить несколько операций вместе, а затем выполнить все эти вычисления за один проход данных. Промежуточные операции создают новый поток (например, filter создает поток, содержащий отфильтрованные элементы) без изменения источника данных, поэтому вы можете свободно использовать исходные данные в другом месте вашего проекта или создавать несколько потоков из одного и того же источника.
  • Терминальная операция, такая как Stream.forEach . Когда вы вызываете операцию терминала, все ваши промежуточные операции будут выполняться и создавать новый поток. Поток не способен хранить элементы, поэтому, как только вы вызываете терминальную операцию, этот поток считается «потребленным» и больше не может использоваться. Если вы хотите вернуться к элементам потока, вам нужно будет сгенерировать новый поток из исходного источника данных.

Существуют различные способы получения потока из источника данных, в том числе:

  • Stream.of()   Создает поток из отдельных значений:

1
Stream<String> stream = Stream.of(«A», «B», «C»);
  • IntStream.range() Создает поток из диапазона чисел:

1
IntStream i = IntStream.range(0, 20);
  • Stream.iterate() Создает поток путем многократного применения оператора к каждому элементу. Например, здесь мы создаем поток, в котором каждый элемент увеличивается на единицу:

1
Stream<Integer> s = Stream.iterate(0, n -> n + 1);

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

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

1
2
Stream<String> myNewStream =
           myStream.map(s -> s.toUpperCase());

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

1
2
List<String> number_string = numbers.stream()
      .limit(5)

Операция filter(Predicate<T>) позволяет определить критерии фильтрации с помощью лямбда-выражения. Это лямбда-выражение должно возвращать логическое значение, которое определяет, должен ли каждый элемент быть включен в результирующий поток. Например, если у вас есть массив строк и вы хотите отфильтровать все строки, содержащие менее трех символов, вы должны использовать следующее:

1
2
3
4
5
Arrays.stream(myArray)
      .filter(s -> s.length() > 3)
     .forEach(System.out::println);
      
}

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

1
2
3
4
5
List<Integer> list = Arrays.asList(10, 11, 8, 9, 22);
 
list.stream()
    .sorted()
    .forEach(System.out::println);

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

1
2
Stream.of(«a»,»b»,»c»,»d»,»e»)
  .forEach(System.out::print);

Чтобы выполнить поток параллельно, вам нужно явно пометить этот поток как параллельный, используя метод parallel() :

1
2
3
Stream.of(«a»,»b»,»c»,»d»,»e»)
  .parallel()
  .forEach(System.out::print);

Внутри параллельных потоков используется Fork / Join Framework, поэтому число доступных потоков всегда равно количеству доступных ядер в ЦП.

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

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

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

Операция Collect собирает все обработанные элементы в контейнер, такой как List или Set . Java 8 предоставляет служебный класс Collectors , поэтому вам не нужно беспокоиться о реализации интерфейса Collectors , а также о фабриках для многих распространенных сборщиков, включая toList() , toSet() и toCollection() .

Следующий код создаст List содержащий только красные фигуры:

1
2
3
shapes.stream()
       .filter(s -> s.getColor().equals(«red»))
       .collect(Collectors.toList());

Кроме того, вы можете собрать эти отфильтрованные элементы в Set :

1
.collect(Collectors.toSet());

Операция forEach() выполняет некоторые действия с каждым элементом потока, делая его API-интерфейс потока эквивалентом оператора for-each.

Если у вас был список items , вы можете использовать forEach для печати всех элементов, включенных в этот List :

1
items.forEach(item->System.out.println(item));

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

1
items.forEach(System.out::println);

Функциональный интерфейс — это интерфейс, который содержит ровно один абстрактный метод, известный как функциональный метод.

Концепция интерфейсов с одним методом не нова: Runnable , Comparator , Callable и OnClickListener являются примерами такого интерфейса, хотя в предыдущих версиях Java они назывались интерфейсами с одним абстрактным методом (интерфейсами SAM).

Это больше, чем просто изменение имени, поскольку есть некоторые заметные различия в том, как вы работаете с функциональными (или SAM) интерфейсами в Java 8, по сравнению с более ранними версиями.

До Java 8 вы обычно создавали функциональный интерфейс, используя громоздкую реализацию анонимного класса. Например, здесь мы создаем экземпляр Runnable используя анонимный класс:

1
2
3
4
5
Runnable r = new Runnable(){
           @Override
           public void run() {
               System.out.println(«My Runnable»);
           }};

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

1
Runnable r = () -> System.out.println(«My Runnable»);

Java 8 также представляет аннотацию @FunctionalInterface которая позволяет пометить интерфейс как функциональный интерфейс:

1
2
3
4
@FunctionalInterface
public interface MyFuncInterface {
 public void doSomething();
}

Для обеспечения обратной совместимости с более ранними версиями Java аннотация @FunctionalInterface является необязательной; однако рекомендуется убедиться, что вы правильно реализуете свои функциональные интерфейсы.

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

1
2
3
4
5
6
7
8
@FunctionalInterface
public interface MyFuncInterface {
    void doSomething();
 
//Define a second abstract method//
 
    void doSomethingElse();
}

И, если вы попытаетесь скомпилировать интерфейс @FunctionInterface который содержит ноль методов, вы столкнетесь с ошибкой Нет целевого метода найден .

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

Java 8 также добавила пакет java.util.function, который содержит множество функциональных интерфейсов. Стоит потратить время на ознакомление со всеми этими новыми функциональными интерфейсами, просто чтобы вы точно знали, что доступно из коробки.

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

Java 8 представила новый API даты и времени (JSR-310), который призван решить эти проблемы, но, к сожалению, на момент написания этого API не поддерживается на платформе Android. Однако сегодня вы можете использовать некоторые из новых функций «Дата и время» в своих проектах Android, используя стороннюю библиотеку.

В этом последнем разделе я покажу вам, как настроить и использовать две популярные сторонние библиотеки, которые позволяют использовать API даты и времени Java 8 на Android.

ThreeTen Android Backport (также известный как ThreeTenABP) является адаптацией популярного проекта ThreeTen backport , который обеспечивает реализацию JSR-310 для Java 6.0 и Java 7.0. ThreeTenABP предназначен для предоставления доступа ко всем классам API Date и Time (хотя и с другим именем пакета) без добавления большого количества методов в ваши проекты Android.

Чтобы начать использовать эту библиотеку, откройте файл build.gradle уровня модуля и добавьте ThreeTenABP в качестве зависимости проекта:

1
2
3
4
5
dependencies {
 
//Add the following line//
 
compile ‘com.jakewharton.threetenabp:threetenabp:1.0.5’

Затем вам нужно добавить оператор импорта ThreeTenABP:

1
import com.jakewharton.threetenabp.AndroidThreeTen;

Инициализируйте информацию о часовом поясе в вашем методе Application.onCreate() :

1
2
3
4
@Override public void onCreate() {
 super.onCreate();
 AndroidThreeTen.init(this);
}

ThreeTenABP содержит два класса, которые отображают два «типа» информации о времени и дате:

  • LocalDateTime , который хранит время и дату в формате 2017-10-16T13: 17: 57.138
  • ZonedDateTime , который осведомлен о часовом поясе и хранит информацию о дате и времени в следующем формате: 2011-12-03T10: 15: 30 + 01: 00 [Европа / Париж]

Чтобы дать вам представление о том, как вы будете использовать эту библиотеку для получения информации о дате и времени, давайте использовать класс LocalDateTime для отображения текущей даты и времени:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.jakewharton.threetenabp.AndroidThreeTen;
import android.widget.TextView;
import org.threeten.bp.LocalDateTime;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      AndroidThreeTen.init(getApplication());
      setContentView(R.layout.activity_main);
       
      TextView textView = new TextView(this);
      textView.setText(«Time: » + LocalDateTime.now());
      setContentView(textView);
 
    }
  }
Отображение даты и времени с помощью библиотеки ThreeTen Android Backport

Это не самый удобный способ отображения даты и времени! Чтобы разобрать эти необработанные данные в нечто более удобочитаемое человеком, вы можете использовать класс DateTimeFormatter и установить для него одно из следующих значений:

  • BASIC_ISO_DATE . Форматирует дату как 2017-1016 + 01.00
  • ISO_LOCAL_DATE . Форматирует дату как 2017-10-16
  • ISO_LOCAL_TIME . Форматирует время как 14: 58: 43.242
  • ISO_LOCAL_DATE_TIME . Форматирует дату и время как 2017-10-16T14: 58: 09.616.
  • ISO_OFFSET_DATE . Форматирует дату как 2017-10-16 + 01.00
  • ISO_OFFSET_TIME . Форматирует время как 14: 58: 56.218 + 01: 00
  • ISO_OFFSET_DATE_TIME . Форматирует дату и время как 2017-10-16T14: 5836.758 + 01: 00
  • ISO_ZONED_DATE_TIME . Форматирует дату и время как 2017-10-16T14: 58: 51.324 + 01: 00 (Европа / Лондон)
  • ISO_INSTANT . Форматирует дату и время как 2017-10-16T13: 52: 45.246Z
  • ISO_DATE . Форматирует дату как 2017-10-16 + 01: 00
  • ISO_TIME . Форматирует время как 14: 58: 40.945 + 01: 00
  • ISO_DATE_TIME . Форматирует дату и время как 2017-10-16T14: 55: 32.263 + 01: 00 (Европа / Лондон)
  • ISO_ORDINAL_DATE . Форматирует дату как 2017-289 + 01: 00
  • ISO_WEEK_DATE . Форматирует дату как 2017-W42-1 + 01: 00
  • RFC_1123_DATE_TIME . Форматирует дату и время как понедельник, 16 октября 2017 года 14: 58: 43 + 01: 00

Здесь мы обновляем наше приложение для отображения даты и времени с форматированием DateTimeFormatter.ISO_DATE :

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
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.jakewharton.threetenabp.AndroidThreeTen;
import android.widget.TextView;
 
//Add the DateTimeFormatter import statement//
 
import org.threeten.bp.format.DateTimeFormatter;
import org.threeten.bp.ZonedDateTime;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      AndroidThreeTen.init(getApplication());
      setContentView(R.layout.activity_main);
 
      TextView textView = new TextView(this);
 
  DateTimeFormatter formatter = DateTimeFormatter.ISO_DATE;
      String formattedZonedDate = formatter.format(ZonedDateTime.now());
      textView.setText(«Time: » + formattedZonedDate);
      setContentView(textView);
 
   }
}

Чтобы отобразить эту информацию в другом формате, просто замените DateTimeFormatter.ISO_DATE на другое значение. Например:

1
DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;

До Java 8 библиотека Joda-Time считалась стандартной библиотекой для обработки даты и времени в Java, так что новый API даты и времени в Java 8 фактически «в значительной степени опирается на опыт, полученный в проекте Joda-Time» .

Хотя веб-сайт Joda-Time рекомендует пользователям перейти на Java 8 Date and Time как можно скорее, поскольку Android в настоящее время не поддерживает этот API, Joda-Time по-прежнему является жизнеспособным вариантом для разработки Android. Однако обратите внимание, что Joda-Time имеет большой API и загружает информацию о часовом поясе, используя ресурс JAR, оба из которых могут повлиять на производительность вашего приложения.

Чтобы начать работу с библиотекой Joda-Time, откройте файл build.gradle уровня модуля и добавьте следующее:

1
2
3
4
5
dependencies {
  compile ‘joda-time:joda-time:2.9.9’

Библиотека Joda-Time имеет шесть основных классов даты и времени:

  • Instant : представляет точку на временной шкале; например, вы можете получить текущую дату и время, вызвав Instant.now() .
  • DateTime : универсальная замена классу JDK Calendar .
  • LocalDate : дата без времени или ссылки на часовой пояс.
  • LocalTime : время без даты или какой-либо ссылки на часовой пояс, например 14:00:00.
  • LocalDateTime : локальная дата и время, но без информации о часовом поясе.
  • ZonedDateTime : дата и время с часовым поясом.

Давайте посмотрим, как вы печатаете дату и время, используя Joda-Time. В следующем примере я повторно использую код из нашего примера ThreeTenABP, поэтому, чтобы сделать вещи более интересными, я также использую withZone для преобразования даты и времени в значение ZonedDateTime .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.widget.TextView;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
 
public class MainActivity extends AppCompatActivity {
 
  @Override
  protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.activity_main);
       
      DateTime today = new DateTime();
 
//Return a new formatter (using withZone) and specify the time zone, using ZoneId//
 
      DateTime todayNy = today.withZone(DateTimeZone.forID(«America/New_York»));
      TextView textView = new TextView(this);
      textView.setText(«Time: » + todayNy );
      setContentView(textView);
 
   }
}
Отображение даты и времени с использованием библиотеки Joda-Time

Вы найдете полный список поддерживаемых часовых поясов в официальных документах Joda-Time.

В этом посте мы рассмотрели, как создавать более надежный код с использованием аннотаций типов, и исследовали подход «трубы и фильтры» к обработке данных с помощью нового Stream API Java 8.

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

Чтобы подвести итоги, я показал вам, как получить доступ к некоторым дополнительным функциям Java 8, которые Android в настоящее время не поддерживает по умолчанию, используя проекты Joda-Time и ThreeTenABP.

Вы можете узнать больше о выпуске Java 8 на веб-сайте Oracle.

И пока вы здесь, ознакомьтесь с некоторыми другими нашими статьями о разработке Java 8 и Android!

  • Android SDK
    Java против Kotlin: стоит ли использовать Kotlin для разработки под Android?
  • Котлин
    Kotlin From Scratch: переменные, базовые типы и массивы
    Чике Мгбемена
  • Котлин
    Kotlin с нуля: больше веселья с функциями
    Чике Мгбемена
  • Android SDK
    Введение в компоненты архитектуры Android
    Жестяная мегали
  • Android SDK
    Совет: пишите более чистый код с помощью Kotlin SAM Conversions
    Ашраф Хатхибелагал