Методы по умолчанию
Многие языки программирования интегрируют функциональные выражения с их библиотеками коллекций. Это часто приводит к коду, который короче и проще для понимания, чем эквивалент, использующий циклы. Например, рассмотрим цикл:
for (int i = 0; i < strList.size(); i++)
System.out.println(strList.get(i));
Существует лучший способ. Авторы библиотеки могут предоставить метод forEach, который применяет функцию к каждому элементу. Тогда вы можете просто вызвать
strList.forEach(System.out::println);
Это нормально, если библиотека коллекций была разработана с нуля. Но библиотека коллекций Java была разработана много лет назад, и есть проблема. Если интерфейс Collection получает новые методы, такие как forEach, то каждая программа, которая определяет свой собственный класс, реализующий Collection, сломается, пока тоже не реализует этот метод. Это просто недопустимо в Java.
Проектировщики Java решили эту проблему раз и навсегда, позволив создавать методы интерфейса с конкретной реализацией (так называемые методы по умолчанию). Эти методы могут быть безопасно добавлены к существующим интерфейсам. В этом разделе мы рассмотрим методы по умолчанию в деталях. В Java 8 метод forEach был добавлен к интерфейсу Iterable, суперинтерфейсу Collection, используя механизм, который я опишу здесь. Рассмотрим такой интерфейс:
interface Person {
long getId();
default String getFirstName() { return "Jack"; }
}
Интерфейс имеет два метода: getId, это абстрактный метод, и метод по умолчанию getFirstName. Конкретный класс, реализующий интерфейс Person, должен, конечно, предоставить реализацию getId, но он может выбрать, оставить реализацию getFirstName или переопределить ее.
Методы по умолчанию кладут конец классическому паттерну предоставления интерфейса и абстрактного класса, который реализует все или почти все из его методов, такие, как Collection/AbstractCollection или WindowListener/WindowAdapter. Теперь вы можете просто реализовать методы в интерфейсе.
Что произойдет, если точно такой же метод определен как метод по умолчанию в одном интерфейсе, а затем снова в качестве метода суперкласса или другого интерфейса? Языки типа Скала и C++ имеют сложные правила разрешения таких неоднозначностей. К счастью, правила в Java гораздо проще. Вот они:
Родительские классы выиграют. Если суперкласс предоставляет конкретный метод, методы по умолчанию с тем же именем и типами параметров просто игнорируются.
Интерфейсы сталкиваются. Если супер интерфейс предоставляет метод по умолчанию, а другой интерфейс поставляет метод с тем же именем и типами параметров (по умолчанию или нет), то вы должны разрешить конфликт путем переопределения этого метода.
Давайте посмотрим на второе правило. Рассмотрим еще один интерфейс с методом getFirstName:
interface Naming {
default String getFirstName() { return getClass().getName() + "_" + hashCode(); }
}
Что произойдет, если вы создадите класс, реализующий оба?
class Student implements Person, Naming {
...
}
Класс наследует две противоречивые версии метода getFirstName, предоставляемые интерфейсами Person и Naming. Вместо выбора того или другого метода, компилятор Java сообщает об ошибке и оставляет программисту разрешение неоднозначности. Просто предоставьте метод getFirstName в классе Student. В этом методе вы можете выбрать один из двух конфликтующих методов, как показано ниже:
class Student implements Person, Naming {
public String getFirstName() { returnPerson.super.getFirstName(); }
...
}
Теперь предположим, что Naming интерфейс не содержит реализацию по умолчанию для getFirstName:
interface Naming {
String getFirstName();
}
Может ли класс Student унаследовать метод по умолчанию из интерфейса Person? Это могло бы быть разумным, но проектировщики Java приняли решение в пользу единообразия. Это не имеет значения, как два интерфейсы конфликтуют. Если хотя бы один интерфейс обеспечивает реализацию, компилятор сообщает об ошибке, и программист должен устранить неоднозначность.
Если ни один интерфейс не обеспечивает реализацию по умолчанию для общего метода, то мы находимся в пре-Java 8 ситуации и нет никакого конфликта. У класса реализации есть две возможности: реализовать метод или оставить его нереализованным. В последнем случае класс сам является абстрактным.
Мы только что обсудили конфликты имен между двумя интерфейсами. Теперь рассмотрим класс, расширяющий суперкласс и реализующий интерфейс, наследуя тот же метод от обоих. Например, предположим, что Person является классом и Student определяется как:
class Student extends Person implements Naming { ... }
В этом случае только метод суперкласса имеет значение, и любой метод по умолчанию из интерфейса просто игнорируется . В нашем примере Student наследует метод getFirstName от Person, и нет никакой разницы, обеспечивает ли интерфейс Naming реализацию по умолчанию для getFirstName или нет. Это правило «класс побеждает» в действии. Правило «класс побеждает» обеспечивает совместимость с Java 7. Если вы добавляете методы по умолчанию к интерфейсу, это не имеет никакого влияния на код, который работал до того, как появились методы по умолчанию. Но имейте в виду: вы не имеете права создавать метод по умолчанию, который переопределяет один из методов класса Object. Например, вы не можете определить метод по умолчанию для toString или equals, хотя это могло бы быть удобным для таких интерфейсов, как List. Как следствие правила о победе классов, такой метод никогда не может выиграть у Object.toString или Object.equals.
Do'stlaringiz bilan baham: |