Статьи

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

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

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

В этой статье мы рассмотрим два различных способа объявления неабстрактных методов в ваших интерфейсах (что было невозможно в более ранних версиях Java). Мы также ответим на вопрос: теперь, когда интерфейсы могут реализовывать методы, в чем именно разница между абстрактными классами и интерфейсами?

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

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

В последнем посте вы увидели, как можно использовать лямбда-выражения для удаления большого количества стандартного кода из ваших приложений Android. Однако, когда лямбда-выражение просто вызывает единственный метод, у которого уже есть имя, вы можете вырезать еще больше кода из своего проекта, используя ссылку на метод.

Например, это лямбда-выражение на самом деле просто перенаправляет работу на существующий метод handleViewClick :

1
2
3
4
5
6
7
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
  fab.setOnClickListener(view -> handleViewClick(view));
}
 
private void handleViewClick(View view) {
 
}

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

1
Object/Class/Type::methodName

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

1
2
3
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
  fab.setOnClickListener(this::handleViewClick);
}

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

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

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

1
(args) -> Class.staticMethod(args)

Затем вы можете превратить его в ссылку на метод:

1
Class::staticMethodName

Например, если у вас есть статический метод PrintMessage в классе MyClass , то ссылка на ваш метод будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
public class myClass {
   public static void PrintMessage(){
       System.out.println(«This is a static method»);
   }
 
}
   public static void main(String[] args) {
       Thread thread = new Thread(myClass::PrintMessage);
       thread.start();
   }
}

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

1
(arguments) -> containingObject.instanceMethodName(arguments)

С:

1
containingObject::instanceMethodName

Итак, если у вас было следующее лямбда-выражение:

1
MyClass.printNames(names, x -> System.out.println(x));

Тогда введение ссылки на метод даст вам следующее:

1
MyClass.printNames(names,System.out::println);

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

1
ContainingType::methodName

Ссылки на конструктор аналогичны ссылкам на методы, за исключением того, что вы используете ключевое слово new для вызова конструктора. Например, Button::new является ссылкой на конструктор для класса Button , хотя точный вызываемый конструктор зависит от контекста.

Используя ссылки на конструктор, вы можете включить:

1
(arguments) -> new ClassName(arguments)

В:

1
ClassName::new

Например, если у вас был следующий интерфейс MyInterface :

1
2
3
public Interface myInterface{
  public abstract Student getStudent(String name, Integer age);
}

Затем вы можете использовать ссылки на конструктор для создания новых экземпляров Student :

1
2
myInterface stu1 = Student::new;
Student stu = stu1.getStudent(«John Doe», 27);

Также возможно создавать ссылки на конструкторы для типов массивов. Например, ссылка на конструктор для массива int s — это int[]::new .

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

Каждый раз, когда вы добавляете метод в определение интерфейса, любые классы, которые реализуют этот интерфейс, внезапно пропускают реализацию. Например, если у вас был интерфейс ( MyInterface ), который использовался MyClass , то добавление метода в MyInterface бы совместимость с MyClass .

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

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

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

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

Фактически, введение методов по умолчанию стало причиной того, что Oracle смогла сделать такое большое количество дополнений к API Коллекций в Java 8.

Collection — это универсальный интерфейс, который используется во многих различных классах, поэтому добавление новых методов в этот интерфейс может привести к разрыву бесчисленных строк кода. Вместо того чтобы добавлять новые методы в интерфейс Collection и ломать каждый класс, производный от этого интерфейса, Oracle создал функцию метода по умолчанию, а затем добавил эти новые методы в качестве методов по умолчанию. Если вы посмотрите на новый метод Collection.Stream () (который мы подробно рассмотрим в третьей части), вы увидите, что он был добавлен как метод по умолчанию:

1
2
3
default Stream<E> stream() {
    return StreamSupport.stream(spliterator(), false);
  }

Создать метод по умолчанию очень просто — просто добавьте модификатор по default в сигнатуру вашего метода:

1
2
3
4
5
6
public interface MyInterface {
  void interfaceMethod();
  default void defaultMethod(){
      Log.i(TAG,»This is a default method”);
  }
}

Теперь, если MyClass использует MyInterface но не предоставляет собственную реализацию defaultMethod , он просто унаследует реализацию по умолчанию, предоставленную MyInterface . Например, следующий класс все еще будет компилироваться:

1
2
public class MyClass extends AppCompatActivity implements MyInterface {
}

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

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

Представьте, что в дополнение к MyInterface вас есть следующее:

1
2
3
4
5
6
public interface SecondInterface {
  void interfaceMethod();
  default void defaultMethod(){
      Log.i(TAG,»This is also a default method”);
  }
}

И MyInterface и SecondInterface содержат метод по умолчанию с одинаковой подписью ( defaultMethod ). Теперь представьте, что вы пытаетесь использовать оба этих интерфейса в одном классе:

1
2
public class MyClass extends AppCompatActivity implements MyInterface, SecondInterface {
}

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

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

1
2
3
4
5
public class MyClass extends AppCompatActivity implements MyInterface, SecondInterface {
  public void defaultMethod(){
        
  }
}

Другое решение — указать, какую версию defaultMethod вы хотите реализовать, используя следующий формат:

1
<interface name>.super.<method name>();

Поэтому, если вы хотите вызвать реализацию MyInterface#defaultMethod() , вы должны использовать следующее:

1
2
3
4
5
public class MyClass extends AppCompatActivity implements MyInterface, SecondInterface {
   public void defaultMethod(){
     MyInterface.super.defaultMethod();
    }
}

Подобно методам по умолчанию, статические методы интерфейса дают вам возможность определять методы внутри интерфейса. Однако, в отличие от методов по умолчанию, реализующий класс не может переопределить статические методы интерфейса.

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

Вы создаете статический метод, помещая ключевое слово static в начале сигнатуры метода, например:

1
2
3
4
5
public interface MyInterface {
   static void staticMethod(){
      System.out.println(«This is a static method»);
  }
}

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

1
2
3
4
5
6
public class MyClass extends AppCompatActivity implements MyInterface {
    public static void main(String[] args) {
        MyInterface.staticMethod();

Это также означает, что класс и интерфейс могут иметь статический метод с одинаковой сигнатурой. Например, использование MyClass.staticMethod и MyInterface.staticMethod в одном классе не приведет к ошибке во время компиляции.

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

  • Абстрактные классы могут иметь конечные, не финальные, статические и нестатические переменные, тогда как интерфейс может иметь только статические и конечные переменные.
  • Абстрактные классы позволяют объявлять поля, которые не являются статическими и конечными, тогда как поля интерфейса по своей природе являются статическими и конечными.
  • В интерфейсах все методы, которые вы объявляете или определяете как методы по умолчанию, по своей природе являются открытыми, тогда как в абстрактных классах вы можете определять публичные, защищенные и частные конкретные методы.
  • Абстрактные классы являются классами , и поэтому могут иметь состояние; интерфейсы не могут иметь связанное с ними состояние.
  • Вы можете определить конструкторы внутри абстрактного класса, что невозможно в интерфейсах Java.
  • Java позволяет расширять только один класс (независимо от того, является ли он абстрактным), но вы можете реализовать столько интерфейсов, сколько вам нужно. Это означает, что интерфейсы обычно имеют преимущество, когда вам требуется множественное наследование, хотя вам нужно остерегаться смертельного алмаза смерти !

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

Однако с введением повторяющихся аннотаций в Java 8 вы теперь можете использовать одну и ту же аннотацию столько раз, сколько захотите в одном и том же месте.

Чтобы ваш код оставался совместимым с более ранними версиями Java, вам нужно хранить повторяющиеся аннотации в аннотации контейнера.

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

  • Пометьте аннотацию, о которой идет @Repeatable мета-аннотацией @Repeatable (аннотацией, которая используется для аннотации). Например, если вы хотите, чтобы аннотация @ToDo , вы должны использовать: @Repeatable(ToDos.class) . Значение в скобках — это тип аннотации контейнера, которую компилятор в конце концов сгенерирует.
  • Объявите содержащий тип аннотации. У этого должен быть атрибут, который является массивом повторяющегося типа аннотации, например:
1
2
3
public @interface ToDos {
   ToDo[] value();
}

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

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

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

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

А пока посмотрите другие наши посты о разработке приложений на Java и Android!

  • Android SDK
    Java против Kotlin: стоит ли использовать Kotlin для разработки под Android?
  • Котлин
    Kotlin From Scratch: переменные, базовые типы и массивы
    Чике Мгбемена
  • Android SDK
    Введение в компоненты архитектуры Android
    Жестяная мегали