Java Language
Interfejsy
Szukaj…
Wprowadzenie
interface
. Interfejsy mogą zawierać tylko stałe, sygnatury metod, metody domyślne, metody statyczne i typy zagnieżdżone. Ciała metod istnieją tylko dla metod domyślnych i metod statycznych. Podobnie jak klasy abstrakcyjne, interfejsy nie mogą być tworzone - mogą być implementowane tylko przez klasy lub rozszerzane o inne interfejsy. Interfejs jest powszechnym sposobem na osiągnięcie pełnej abstrakcji w Javie.
Składnia
- interfejs publiczny Foo {void foo (); / * wszelkie inne metody * /}
- interfejs publiczny Foo1 rozszerza Foo {void bar (); / * wszelkie inne metody * /}
- klasa publiczna Foo2 implementuje implementację Foo, Foo1 {/ * Foo i Foo1 * /}
Deklaracja i implementacja interfejsu
Deklaracja interfejsu przy użyciu słowa kluczowego interface
:
public interface Animal {
String getSound(); // Interface methods are public by default
}
Zastąp adnotację
@Override
public String getSound() {
// Code goes here...
}
Zmusza to kompilator do sprawdzenia, czy jesteśmy nadpisywani i uniemożliwia programowi zdefiniowanie nowej metody lub zepsucie podpisu metody.
Interfejsy są implementowane za pomocą słowa kluczowego implements
.
public class Cat implements Animal {
@Override
public String getSound() {
return "meow";
}
}
public class Dog implements Animal {
@Override
public String getSound() {
return "woof";
}
}
W tym przykładzie klasy Cat
i Dog
muszą zdefiniować getSound()
, ponieważ metody interfejsu są z natury abstrakcyjne (z wyjątkiem metod domyślnych).
Korzystanie z interfejsów
Animal cat = new Cat();
Animal dog = new Dog();
System.out.println(cat.getSound()); // prints "meow"
System.out.println(dog.getSound()); // prints "woof"
Implementowanie wielu interfejsów
Klasa Java może implementować wiele interfejsów.
public interface NoiseMaker {
String noise = "Making Noise"; // interface variables are public static final by default
String makeNoise(); //interface methods are public abstract by default
}
public interface FoodEater {
void eat(Food food);
}
public class Cat implements NoiseMaker, FoodEater {
@Override
public String makeNoise() {
return "meow";
}
@Override
public void eat(Food food) {
System.out.println("meows appreciatively");
}
}
Zwróć uwagę, jak klasa Cat
musi implementować odziedziczone metody abstract
w obu interfejsach. Ponadto zauważ, jak klasa może praktycznie zaimplementować tyle interfejsów, ile potrzeba (istnieje ograniczenie 65 535 z powodu ograniczenia JVM ).
NoiseMaker noiseMaker = new Cat(); // Valid
FoodEater foodEater = new Cat(); // Valid
Cat cat = new Cat(); // valid
Cat invalid1 = new NoiseMaker(); // Invalid
Cat invalid2 = new FoodEater(); // Invalid
Uwaga:
- Wszystkie zmienne zadeklarowane w interfejsie są
public static final
- Wszystkie metody zadeklarowane w metodach interfejsu są
public abstract
(ta instrukcja jest ważna tylko w Javie 7. W Javie 8 dozwolone są metody w interfejsie, które nie muszą być abstrakcyjne; takie metody są znane jako metody domyślne ) - Interfejsy nie mogą być deklarowane jako
final
- Jeśli więcej niż jeden interfejs deklaruje metodę, która ma identyczną sygnaturę, wówczas jest ona skutecznie traktowana jako jedna metoda i nie można odróżnić, która metoda interfejsu jest zaimplementowana
- Odpowiedni plik InterfaceName.class zostanie wygenerowany dla każdego interfejsu po kompilacji
Rozszerzanie interfejsu
Interfejs może rozszerzyć inny interfejs za pomocą słowa kluczowego extends
.
public interface BasicResourceService {
Resource getResource();
}
public interface ExtendedResourceService extends BasicResourceService {
void updateResource(Resource resource);
}
Teraz klasa implementująca ExtendedResourceService
będzie musiała zaimplementować zarówno getResource()
i updateResource()
.
Rozszerzanie wielu interfejsów
W przeciwieństwie do klas, słowo kluczowe extends
może być użyte do rozszerzenia wielu interfejsów (oddzielonych przecinkami), umożliwiając kombinacje interfejsów w nowym interfejsie
public interface BasicResourceService {
Resource getResource();
}
public interface AlternateResourceService {
Resource getAlternateResource();
}
public interface ExtendedResourceService extends BasicResourceService, AlternateResourceService {
Resource updateResource(Resource resource);
}
W takim przypadku klasa implementująca ExtendedResourceService
będzie musiała zaimplementować getResource()
, getAlternateResource()
i updateResource()
.
Używanie interfejsów z rodzajami
Powiedzmy, że chcesz zdefiniować interfejs, który pozwala publikować / pobierać dane do iz różnych typów kanałów (np. AMQP, JMS itp.), Ale chcesz mieć możliwość zmiany szczegółów implementacji ...
Zdefiniujmy podstawowy interfejs IO, który może być ponownie użyty w wielu implementacjach:
public interface IO<IncomingType, OutgoingType> {
void publish(OutgoingType data);
IncomingType consume();
IncomingType RPCSubmit(OutgoingType data);
}
Teraz mogę utworzyć instancję tego interfejsu, ale ponieważ nie mamy domyślnych implementacji dla tych metod, będzie potrzebować implementacji, gdy utworzymy instancję:
IO<String, String> mockIO = new IO<String, String>() {
private String channel = "somechannel";
@Override
public void publish(String data) {
System.out.println("Publishing " + data + " to " + channel);
}
@Override
public String consume() {
System.out.println("Consuming from " + channel);
return "some useful data";
}
@Override
public String RPCSubmit(String data) {
return "received " + data + " just now ";
}
};
mockIO.consume(); // prints: Consuming from somechannel
mockIO.publish("TestData"); // Publishing TestData to somechannel
System.out.println(mockIO.RPCSubmit("TestData")); // received TestData just now
Możemy również zrobić coś bardziej przydatnego z tym interfejsem, powiedzmy, że chcemy go użyć do zawarcia podstawowych funkcji RabbitMQ:
public class RabbitMQ implements IO<String, String> {
private String exchange;
private String queue;
public RabbitMQ(String exchange, String queue){
this.exchange = exchange;
this.queue = queue;
}
@Override
public void publish(String data) {
rabbit.basicPublish(exchange, queue, data.getBytes());
}
@Override
public String consume() {
return rabbit.basicConsume(exchange, queue);
}
@Override
public String RPCSubmit(String data) {
return rabbit.rpcPublish(exchange, queue, data);
}
}
Powiedzmy, że chcę teraz używać tego interfejsu IO do liczenia odwiedzin w mojej witrynie od ostatniego uruchomienia systemu, a następnie do wyświetlenia łącznej liczby odwiedzin - możesz zrobić coś takiego:
import java.util.concurrent.atomic.AtomicLong;
public class VisitCounter implements IO<Long, Integer> {
private static AtomicLong websiteCounter = new AtomicLong(0);
@Override
public void publish(Integer count) {
websiteCounter.addAndGet(count);
}
@Override
public Long consume() {
return websiteCounter.get();
}
@Override
public Long RPCSubmit(Integer count) {
return websiteCounter.addAndGet(count);
}
}
Teraz użyjmy licznika VisitCounter:
VisitCounter counter = new VisitCounter();
// just had 4 visits, yay
counter.publish(4);
// just had another visit, yay
counter.publish(1);
// get data for stats counter
System.out.println(counter.consume()); // prints 5
// show data for stats counter page, but include that as a page view
System.out.println(counter.RPCSubmit(1)); // prints 6
Podczas implementowania wielu interfejsów nie można dwukrotnie zaimplementować tego samego interfejsu. Dotyczy to również ogólnych interfejsów. Dlatego poniższy kod jest nieprawidłowy i spowoduje błąd kompilacji:
interface Printer<T> {
void print(T value);
}
// Invalid!
class SystemPrinter implements Printer<Double>, Printer<Integer> {
@Override public void print(Double d){ System.out.println("Decimal: " + d); }
@Override public void print(Integer i){ System.out.println("Discrete: " + i); }
}
Przydatność interfejsów
Interfejsy mogą być bardzo pomocne w wielu przypadkach. Załóżmy na przykład, że masz listę zwierząt i chcesz przeglądać listę, drukując dźwięk, który wydają.
{cat, dog, bird}
Jednym ze sposobów na to byłoby użycie interfejsów. Umożliwiłoby to wywołanie tej samej metody we wszystkich klasach
public interface Animal {
public String getSound();
}
Każda klasa implements Animal
również musi mieć w getSound()
metodę getSound()
, ale wszystkie mogą mieć różne implementacje
public class Dog implements Animal {
public String getSound() {
return "Woof";
}
}
public class Cat implements Animal {
public String getSound() {
return "Meow";
}
}
public class Bird implements Animal{
public String getSound() {
return "Chirp";
}
}
Mamy teraz trzy różne klasy, z których każda ma getSound()
. Ponieważ wszystkie te klasy implement
interfejs Animal
, który deklaruje getSound()
, każde wystąpienie Animal
może mieć getSound()
Animal dog = new Dog();
Animal cat = new Cat();
Animal bird = new Bird();
dog.getSound(); // "Woof"
cat.getSound(); // "Meow"
bird.getSound(); // "Chirp"
Ponieważ każde z nich jest Animal
, moglibyśmy nawet umieścić zwierzęta na liście, przejrzeć je i wydrukować ich dźwięki
Animal[] animals = { new Dog(), new Cat(), new Bird() };
for (Animal animal : animals) {
System.out.println(animal.getSound());
}
Ponieważ kolejność tablic to Dog
, Cat
, a następnie Bird
, na konsoli zostanie wydrukowany napis „Woof Meow Chirp” .
Interfejsy mogą być również używane jako wartość zwracana dla funkcji. Na przykład zwracanie Dog
jeśli dane wejściowe to „pies” , Cat
jeśli dane wejściowe to „kot” , i Bird
jeśli to „ptak” , a następnie wydrukowanie dźwięku tego zwierzęcia można wykonać za pomocą
public Animal getAnimalByName(String name) {
switch(name.toLowerCase()) {
case "dog":
return new Dog();
case "cat":
return new Cat();
case "bird":
return new Bird();
default:
return null;
}
}
public String getAnimalSoundByName(String name){
Animal animal = getAnimalByName(name);
if (animal == null) {
return null;
} else {
return animal.getSound();
}
}
String dogSound = getAnimalSoundByName("dog"); // "Woof"
String catSound = getAnimalSoundByName("cat"); // "Meow"
String birdSound = getAnimalSoundByName("bird"); // "Chirp"
String lightbulbSound = getAnimalSoundByName("lightbulb"); // null
Interfejsy są również przydatne do rozszerzania, ponieważ jeśli chcesz dodać nowy typ Animal
, nie musisz zmieniać niczego za pomocą wykonywanych na nich operacji.
Implementowanie interfejsów w klasie abstrakcyjnej
Metodą zdefiniowaną w interface
jest domyślnie public abstract
. Gdy abstract class
implementuje interface
, wszelkie metody zdefiniowane w interface
nie muszą być implementowane przez abstract class
. Jest tak, ponieważ class
która została zadeklarowana jako abstract
może zawierać deklaracje metody abstrakcyjnej. Dlatego obowiązkiem pierwszej konkretnej podklasy jest wdrożenie wszelkich metod abstract
odziedziczonych z dowolnych interfejsów i / lub abstract class
.
public interface NoiseMaker {
void makeNoise();
}
public abstract class Animal implements NoiseMaker {
//Does not need to declare or implement makeNoise()
public abstract void eat();
}
//Because Dog is concrete, it must define both makeNoise() and eat()
public class Dog extends Animal {
@Override
public void makeNoise() {
System.out.println("Borf borf");
}
@Override
public void eat() {
System.out.println("Dog eats some kibble.");
}
}
Począwszy od języka Java 8 interface
może deklarować default
implementacje metod, co oznacza, że metoda nie będzie abstract
, dlatego żadne konkretne podklasy nie będą zmuszone do implementacji metody, ale odziedziczą default
implementację, chyba że zostaną zastąpione.
Metody domyślne
Metody domyślne, wprowadzone w Javie 8, są sposobem na określenie implementacji w interfejsie. Można tego użyć, aby uniknąć typowej klasy „Base” lub „Abstract”, zapewniając częściową implementację interfejsu i ograniczając hierarchię podklas.
Implementacja wzorca obserwatora
Na przykład możliwe jest zaimplementowanie wzorca Observer-Listener bezpośrednio w interfejsie, zapewniając większą elastyczność klasom implementacji.
interface Observer {
void onAction(String a);
}
interface Observable{
public abstract List<Observer> getObservers();
public default void addObserver(Observer o){
getObservers().add(o);
}
public default void notify(String something ){
for( Observer l : getObservers() ){
l.onAction(something);
}
}
}
Teraz każdą klasę można uczynić „obserwowalną” po prostu poprzez implementację interfejsu obserwowalnego, pozostając jednocześnie częścią innej hierarchii klas.
abstract class Worker{
public abstract void work();
}
public class MyWorker extends Worker implements Observable {
private List<Observer> myObservers = new ArrayList<Observer>();
@Override
public List<Observer> getObservers() {
return myObservers;
}
@Override
public void work(){
notify("Started work");
// Code goes here...
notify("Completed work");
}
public static void main(String[] args) {
MyWorker w = new MyWorker();
w.addListener(new Observer() {
@Override
public void onAction(String a) {
System.out.println(a + " (" + new Date() + ")");
}
});
w.work();
}
}
Problem z diamentem
Kompilator w Javie 8 jest świadomy problemu z diamentem, który powstaje, gdy klasa implementuje interfejsy zawierające metodę o tej samej sygnaturze.
Aby go rozwiązać, klasa implementująca musi zastąpić metodę współużytkowaną i zapewnić własną implementację.
interface InterfaceA {
public default String getName(){
return "a";
}
}
interface InterfaceB {
public default String getName(){
return "b";
}
}
public class ImpClass implements InterfaceA, InterfaceB {
@Override
public String getName() {
//Must provide its own implementation
return InterfaceA.super.getName() + InterfaceB.super.getName();
}
public static void main(String[] args) {
ImpClass c = new ImpClass();
System.out.println( c.getName() ); // Prints "ab"
System.out.println( ((InterfaceA)c).getName() ); // Prints "ab"
System.out.println( ((InterfaceB)c).getName() ); // Prints "ab"
}
}
Nadal istnieje problem posiadania metod o tej samej nazwie i parametrach z różnymi typami zwracanych danych, których nie można skompilować.
Użyj domyślnych metod, aby rozwiązać problemy ze zgodnością
Domyślne implementacje metod są bardzo przydatne, jeśli metoda zostanie dodana do interfejsu w istniejącym systemie, w którym interfejsy są używane przez kilka klas.
Aby uniknąć rozbicia całego systemu, możesz podać domyślną implementację metody po dodaniu metody do interfejsu. W ten sposób system będzie się nadal kompilował, a rzeczywiste implementacje będą mogły być wykonywane krok po kroku.
Aby uzyskać więcej informacji, zobacz temat Metody domyślne .
Modyfikatory w interfejsach
Przewodnik po stylu Java firmy Oracle stanowi:
Modyfikatory nie powinny być zapisywane, gdy są niejawne.
(Zobacz Modyfikatory w Oracle Official Code Standard, aby uzyskać informacje na temat kontekstu i łącza do faktycznego dokumentu Oracle.)
Te wskazówki dotyczące stylu dotyczą w szczególności interfejsów. Rozważmy następujący fragment kodu:
interface I {
public static final int VARIABLE = 0;
public abstract void method();
public static void staticMethod() { ... }
public default void defaultMethod() { ... }
}
Zmienne
Wszystkie zmienne interfejsu są domyślnie stałymi z niejawnymi modyfikatorami public
(dostępnymi dla wszystkich), static
(są dostępne według nazwy interfejsu) i modyfikatorami final
(muszą być inicjowane podczas deklaracji):
public static final int VARIABLE = 0;
Metody
- Wszystkie metody, które nie zapewniają implementacji, są domyślnie
public
iabstract
.
public abstract void method();
- Wszystkie metody ze
static
lubdefault
modyfikatorem muszą zapewniać implementację i są domyślniepublic
.
public static void staticMethod() { ... }
Po zastosowaniu wszystkich powyższych zmian otrzymamy:
interface I {
int VARIABLE = 0;
void method();
static void staticMethod() { ... }
default void defaultMethod() { ... }
}
Wzmocnij parametry ograniczonego typu
Parametry typu ograniczona umożliwiają ustawienie ograniczeń dla argumentów typu ogólnego:
class SomeClass {
}
class Demo<T extends SomeClass> {
}
Ale parametr type może powiązać tylko jeden typ klasy.
Typ interfejsu można powiązać z typem, który już miał powiązanie. Osiąga się to za pomocą symbolu &
:
interface SomeInterface {
}
class GenericClass<T extends SomeClass & SomeInterface> {
}
To wzmacnia wiązanie, potencjalnie wymagające argumentów typu pochodzących z wielu typów.
Wiele typów interfejsów można powiązać z parametrem typu:
class Demo<T extends SomeClass & FirstInterface & SecondInterface> {
}
Należy jednak zachować ostrożność. Wiele powiązań interfejsu jest zwykle oznaką zapachu kodu , co sugeruje, że należy utworzyć nowy typ, który działa jak adapter dla innych typów:
interface NewInterface extends FirstInterface, SecondInterface {
}
class Demo<T extends SomeClass & NewInterface> {
}