Szukaj…


Wprowadzenie

Interfejs to typ odwołania, podobny do klasy, który można zadeklarować za pomocą słowa kluczowego 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:

  1. Wszystkie zmienne zadeklarowane w interfejsie są public static final
  2. 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 )
  3. Interfejsy nie mogą być deklarowane jako final
  4. 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
  5. 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

  1. Wszystkie metody, które nie zapewniają implementacji, są domyślnie public i abstract .
public abstract void method();
Java SE 8
  1. Wszystkie metody ze static lub default modyfikatorem muszą zapewniać implementację i są domyślnie public .
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> {

}


Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow