Zephyrnet-logo

Gids voor interfaces in Java

Datum:

Introductie

interfaces in Java zijn een van de basisconcepten van objectgeoriënteerd programmeren die er vaak naast worden gebruikt klassen en abstracte lessen. Een interface vertegenwoordigt een referentietype, wat betekent dat het in wezen slechts een specificatie is waaraan een bepaalde klasse die het implementeert, moet gehoorzamen. Interfaces kunnen bevatten: Slechts constanten, methodehandtekeningen, standaardmethoden en statische methoden. Standaard staan ​​interfaces alleen het gebruik toe van: public specificeerder, in tegenstelling tot klassen die ook de . kunnen gebruiken protected en private bestekschrijvers.

In deze handleiding bekijken we interfaces in Java - hoe ze werken en hoe ze te gebruiken. We zullen ook alle concepten behandelen die u mogelijk moet begrijpen wanneer u met interfaces in Java werkt. Na het lezen van deze handleiding zou u een uitgebreid begrip moeten hebben van Java-interfaces.

Methode-lichamen bestaan ​​alleen voor standaard- en statische methoden. Maar zelfs als ze toestaan ​​dat een lichaam aanwezig is in een interface, is dit over het algemeen geen goede gewoonte, omdat het tot veel verwarring kan leiden en de code minder leesbaar kan maken. Interfaces kunnen niet worden geïnstantieerd - ze kunnen alleen worden geïmplementeerd door klassen, of uitgebreid door andere interfaces.

Waarom interfaces gebruiken?

We zouden al moeten weten dat Java-klassen overerving ondersteunen. Maar als het gaat om meerdere erfenissen, Java-klassen ondersteunen het gewoon niet, in tegenstelling tot bijvoorbeeld C#. Om dit probleem op te lossen gebruiken we interfaces!

Klassen verlengen andere klassen en interfaces kunnen ook verlengen andere interfaces, maar alleen een klasse gereedschap een interface. Interfaces helpen ook bij het bereiken van absolute abstractie wanneer nodig.

Interfaces zorgen ook voor: losse koppeling. Losse koppeling in Java vertegenwoordigt een situatie waarin twee componenten een lage afhankelijkheid van elkaar hebben - de componenten zijn onafhankelijk van elkaar. De enige kennis die een klasse heeft over de andere klasse, is wat de andere klasse heeft blootgelegd via zijn interfaces in losse koppeling.

Opmerking: Losse koppeling is wenselijk omdat het modularisatie en testen eenvoudiger maakt. Hoe meer gekoppelde klassen zijn, hoe moeilijker het is om ze individueel te testen en te isoleren van de effecten van andere klassen. Een ideale staat van klassenrelaties omvat: losse koppeling en hoge cohesie – ze kunnen volledig worden gescheiden, maar bieden elkaar ook extra functionaliteit. Hoe dichter de elementen van een module bij elkaar liggen, hoe groter de samenhang. Hoe dichter uw architectuur bij deze ideale staat komt, hoe gemakkelijker het zal zijn om uw systeem te schalen, te onderhouden en anderszins te testen.

Hoe interfaces in Java te definiëren

Het definiëren van interfaces is helemaal niet zo moeilijk. In feite is het vergelijkbaar met het definiëren van een klasse. In het belang van deze handleiding zullen we een eenvoudige Animal interface, en implementeer het vervolgens in verschillende klassen:

public interface Animal {
    public void walk();
    public void eat();
    public void sleep();
    public String makeNoise();
}

We kunnen ervoor zorgen dat het een verscheidenheid aan verschillende methoden heeft om verschillende gedragingen van dieren te beschrijven, maar de functionaliteit en het punt blijven hetzelfde, ongeacht hoeveel variabelen of methoden we toevoegen. Daarom houden we het eenvoudig met deze vier methoden.

Deze eenvoudige interface definieert een aantal gedragingen van dieren. In meer technische termen hebben we de methoden gedefinieerd die moeten worden gevonden binnen de specifieke klassen die deze interface implementeren. Laten we een maken Dog klasse die onze . implementeert Animal interface:

public class Dog implements Animal{
    public String name;

    public Dog(String name){
        this.name = name;
    }
}

Het is een eenvoudige klasse die maar één variabele heeft name. Het sleutelwoord implements sta ons toe uitvoeren de Animal interface binnen onze Dog klas. We kunnen het echter niet zo laten. Als we probeerden het programma te compileren en uit te voeren met de implementatie van de Dog klasse als deze, we krijgen een fout in de trant van:

java: Dog is not abstract and does not override abstract method makeNoise() in Animal

Deze fout vertelt ons dat we dat niet hebben gedaan Volg de regels ingesteld door de interface die we hebben geïmplementeerd. Zoals het er nu uitziet, is onze Dog klasse Dan moet je definieer alle vier de methoden die zijn gedefinieerd in de Animal interface, zelfs als ze niets teruggeven en gewoon leeg zijn. In werkelijkheid willen we altijd dat ze iets doen en zullen we geen overbodige/klassespecifieke methoden in een interface definiëren. Als u geen geldige implementatie van een interfacemethode in een subklasse kunt vinden, moet deze niet in de interface worden gedefinieerd. Sla het in plaats daarvan over in de interface en definieer het als een lid van die subklasse. Als alternatief, als het een andere generieke functionaliteit is, definieer ander interface, die naast de eerste kan worden geïmplementeerd. Ons voorbeeld is een beetje vereenvoudigd, maar het punt blijft hetzelfde, zelfs in meer gecompliceerde programma's:

public class Dog implements Animal{
    public String name;

    public Dog(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void walk() {
        System.out.println(getName() + " is walking!");
    }

    public void eat() {
        System.out.println(getName() + " is eating!");
    }

    public void sleep() {
        System.out.println(getName() + " is sleeping!");
    }

    public String makeNoise() {
        return getName() + " says woof!";
    }
}

Zodra we onze interface binnen onze doelklasse hebben geïmplementeerd, kunnen we al deze methoden gebruiken zoals we gewoonlijk deden wanneer we gebruikten public methoden uit alle klassen:

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog("Shiba Inu");

        dog.eat();
        System.out.println(dog.makeNoise());
        dog.walk();
        dog.sleep();
    }
}

Dit geeft ons de output:

Shiba Inu is eating!
Shiba Inu says woof!
Shiba Inu is walking!
Shiba Inu is sleeping!

Meerdere overerving

Zoals we eerder hebben vermeld, gebruiken we interfaces om de problemen op te lossen die klassen hebben met overerving. Hoewel een klas niet meer dan één klas tegelijk kan verlengen, kan het wel meer dan één interface implementeren tegelijk. Dit wordt gedaan door simpelweg de namen van de interfaces te scheiden door een komma. Een situatie waarin een klasse meerdere interfaces implementeert, of een interface meerdere interfaces uitbreidt, wordt genoemd meervoudige overerving.

De vraag rijst natuurlijk: waarom wordt meervoudige overerving niet ondersteund in het geval van klassen, maar in het geval van interfaces? Het antwoord op die vraag is ook vrij eenvoudig: dubbelzinnigheid. Verschillende klassen kunnen dezelfde methoden anders definiëren, waardoor de consistentie over de hele linie wordt verpest. Terwijl er in het geval van interfaces geen dubbelzinnigheid is - de klasse die de interface implementeert zorgt voor de implementatie van de methoden.

Voor dit voorbeeld bouwen we voort op onze vorige Animal koppel. Laten we zeggen dat we een willen maken Bird klas. Vogels zijn natuurlijk dieren, maar onze Animal interface heeft geen methoden om een ​​vliegende beweging te simuleren. Dit kan eenvoudig worden opgelost door een toe te voegen fly() methode binnen de Animal interface, toch?

Nou ja, maar eigenlijk niet.

Aangezien we een oneindig aantal klassen met dierennaam kunnen hebben die onze interface uitbreiden, zouden we in theorie een methode moeten toevoegen die het gedrag van een dier simuleert als het eerder ontbreekt, zodat elk dier de fly() methode. Om dit te voorkomen, maken we gewoon een nieuwe interface met een fly() methode! Deze interface zou door alle vliegende dieren worden geïmplementeerd.

In ons voorbeeld, aangezien de vogel een methode nodig zou hebben die vliegen simuleert, en laten we zeggen klapperend met zijn vleugels, zouden we zoiets als dit hebben:

public interface Flying {
    public void flapWings();
    public void fly();
}

Nogmaals, een zeer eenvoudige interface. Nu kunnen we de Bird klasse zoals we eerder hebben besproken:

public class Bird implements Animal, Fly{
    public String name;

    public Bird(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void walk() {
        System.out.println(getName() + " is walking!");
    }

    public void eat() {
        System.out.println(getName() + " is eating!");
    }

    public void sleep() {
        System.out.println(getName() + " is sleeping!");
    }

    public String makeNoise() {
        return getName() + " says: caw-caw!";
    }

    public void fly() {
        System.out.println(getName() + " is flying!");
    }

    public void flapWings(){
        System.out.println(getName() + " is flapping its wings!");
    }
}

Laten we een maken Bird object binnen onze hoofdklasse en voer de resultaten uit zoals we eerder deden:

Bird bird = new Bird("Crow");
System.out.println(bird.makeNoise());
bird.flapWings();
bird.fly();
bird.walk();
bird.sleep();

Het geeft een eenvoudige uitvoer:

Crow says: caw-caw!
Crow is flapping its wings!
Crow is flying!
Crow is walking!
Crow is sleeping!

Bekijk onze praktische, praktische gids voor het leren van Git, met best-practices, door de industrie geaccepteerde normen en bijgevoegd spiekbriefje. Stop met Googlen op Git-commando's en eigenlijk leren het!

Opmerking: Er zullen gevallen zijn (vooral bij het implementeren van meerdere interfaces) waarin niet alle methoden die in alle interfaces zijn gedeclareerd, binnen onze klasse worden gedefinieerd, ondanks onze inspanningen. Als onze belangrijkste Animal interface had om welke reden dan ook een swim() methode, binnen onze Bird klasse zou die methode leeg blijven (of return null), zoals vogels voor het grootste deel niet zwemmen.

Interface-overerving

Net zoals wanneer we de eigenschappen van de ene klasse erven van een andere met behulp van extends, kunnen we hetzelfde doen met interfaces. Door de ene interface met de andere uit te breiden, elimineren we in feite de noodzaak voor een klasse om in sommige gevallen meerdere interfaces te implementeren. In onze Bird klasse voorbeeld, we hadden het zowel de Animal en Flying interfaces, maar dat is niet nodig. We kunnen onze Flying interface verlengen de Animal interface, en we krijgen dezelfde resultaten:

public interface Flying extends Animal {
    public void flapWings();
    public void fly();
}

En de Bird klasse:

public class Bird implements Fly{

}

De code van zowel de Flying interface en Bird klasse blijft hetzelfde, het enige dat verandert zijn enkele regels binnen beide:

  • Flying breidt zich nu uit Animal en
  • Bird implementeert alleen de Flying interface (en de Animal interface per extensie)

De Main methode die we gebruikten om te laten zien hoe we deze objecten kunnen instantiëren en gebruiken, blijft ook hetzelfde als voorheen.

Opmerking: Wanneer onze Flying interface uitgebreid de Animal interface, hoefden we niet alle methoden te definiëren die in de Animal interface - ze zullen standaard direct beschikbaar zijn, wat eigenlijk het punt is van het uitbreiden van twee interfaces.

Deze koppels Flying en Animal samen. Dit is misschien wat je wilt, maar misschien ook niet wat je wilt. Afhankelijk van uw specifieke gebruiksgeval, als u kunt garanderen dat welke vliegen dan ook een dier moeten zijn, is het veilig om ze aan elkaar te koppelen. Als je er echter niet zeker van bent dat wat vliegt een dier moet zijn - niet verlengen Animal Met Flying.

Interfaces versus abstracte klassen

Aangezien we interfaces in deze handleiding in overvloed hebben besproken, laten we snel vermelden hoe ze zich verhouden tot abstracte lessen, aangezien dit onderscheid veel vragen oproept en er overeenkomsten tussen zijn. Met een abstracte klasse kunt u een functionaliteit maken die subklassen kunnen implementeren of overschrijven. Een klas kan verlengen maar een abstracte klas tegelijk. In de onderstaande tabel zullen we een kleine vergelijking van beide maken en zowel de voor- als nadelen bekijken van het gebruik van zowel interfaces als abstracte klassen:

Interface Abstracte klasse
Kan alleen 'openbare' abstracte methoden hebben. Alles wat binnen een interface is gedefinieerd, wordt verondersteld 'openbaar' te zijn Kan `beschermde` en `openbare` methoden hebben
`abstract` trefwoord bij het declareren van methoden is optioneel Het `abstract` trefwoord bij het declareren van methoden is verplicht
Kan meerdere interfaces tegelijk uitbreiden Kan slechts één klas of een abstracte klas tegelijk verlengen
Kan meerdere interfaces erven, maar kan geen klasse erven Kan een klasse en meerdere interfaces erven
Een klasse kan meerdere interfaces implementeren Een klasse kan slechts één abstracte klasse erven
Kan constructors/destructors niet declareren Kan constructors/destructors declareren
Gebruikt om een ​​specificatie te maken waaraan een klasse moet voldoen door Wordt gebruikt om de identiteit van een klasse te definiëren

Standaardmethoden in interfaces

Wat gebeurt er als je een systeem maakt, het in productie laat gaan en vervolgens besluit dat je een interface moet updaten door een methode toe te voegen? Je moet alle klassen bijwerken die het ook implementeren - anders komt alles tot stilstand. Om ontwikkelaars toe te staan: -update interfaces met nieuwe methoden zonder bestaande code te breken, kunt u gebruiken verzuim methoden, waarmee u de limiet van het definiëren van methode-lichamen in interfaces kunt omzeilen.

Door default methoden, kunt u de hoofdtekst definiëren van een gemeenschappelijke nieuwe methode die in alle klassen moet worden geïmplementeerd, die vervolgens automatisch wordt toegevoegd als het standaardgedrag van alle klassen zonder ze te breken en zonder ze expliciet te implementeren. Dit betekent dat u interfaces kunt bijwerken die zijn uitgebreid met honderden klassen, zonder refactoring!

Opmerking: gebruik default methoden is bedoeld voor het bijwerken van bestaande interfaces om achterwaartse compatibiliteit te behouden, niet om vanaf het begin te worden toegevoegd. Als u zich in de ontwerpfase bevindt, gebruik dan geen default methoden – alleen wanneer u eerder onvoorziene functionaliteit toevoegt die u niet eerder had kunnen implementeren.

Stel dat uw klant super blij is met uw aanvraag, maar ze hebben zich gerealiseerd dat vogels niet alleen fly() en flapWings() naast de dingen die andere dieren doen. Zij ook dive()! Je hebt al een . geïmplementeerd Crow, Pidgeon, Blackbird en Woodpecker.

Refactoring is vervelend en moeilijk, en vanwege de architectuur die je hebt gemaakt, is het moeilijk om een dive() in alle vogels voordat de deadline aanbreekt. Je kunt een default void dive() methode in de Flying interface.

public interface Flying {
    public void flapWings();
    public void fly();
    default void dive() {System.out.println("The bird is diving from the air!"}
}

Nu, binnen onze Bird klasse, kunnen we de implementatie van de dive() methode, aangezien we het standaardgedrag ervan al in de interface hebben gedefinieerd:

public class Bird implements Fly{
    public String name;

    public Bird(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void fly() {
        System.out.println(getName() + " is flying!");
    }

    public void flapWings(){
        System.out.println("The " + getName() + " is flapping its wings!");
    }
}

A Bird instantie kan dive() nu, zonder enige refactoring van de Bird klasse, waardoor we de broodnodige tijd hebben om het op een gracieuze en niet-gehaaste manier te implementeren:

Bird bird = new Bird("Crow");
bird.dive();

Dit resulteert in:

The bird is diving from the air!

Statische methoden in interfaces

Eindelijk – we kunnen definiëren static methoden ook in interfaces! Aangezien deze niet tot een specifieke instantie behoren, kunnen ze niet worden overschreven en worden ze aangeroepen door ze vooraf te laten gaan door de interfacenaam.

Statische interfacemethoden worden gebruikt voor algemene hulpprogramma's/helpermethoden, niet voor het implementeren van specifieke functionaliteit. De ondersteuning is toegevoegd om te voorkomen dat er naast interfaces niet-instantiële hulpklassen zijn, en om de hulpmethoden van afzonderlijke klassen in interfaces te bundelen. In feite helpt het gebruik van statische methoden je om een ​​extra klassedefinitie te vermijden die een paar hulpmethoden zou bevatten. In plaats van een Animal interface en AnimalUtils als een helper-klasse – je kunt nu de helper-methoden bundelen uit de AnimalUtils klasse in statisch Animal werkwijzen.

Dit vergroot de samenhang in je architectuur, omdat je minder klassen hebt en degene die je wel hebt, meer lineair scheidbaar zijn.

Stel bijvoorbeeld dat u uw . wilt valideren Animal implementaties, wat de validatie ook zou betekenen voor uw specifieke toepassing (zoals controleren of een dier in een boek is ingeschreven). Je zou dit kunnen definiëren als een intrinsieke statische methode van alles Animals:

interface Animal {
    public void walk();
    public void eat();
    public void sleep();
    public String makeNoise();

    static boolean checkBook(Animal animal, List book) {
        return book.contains(animal);
    }
}

De Dog definitie is hetzelfde als voorheen - u kunt deze methode niet overschrijven of anderszins wijzigen, en het behoort tot de Animal koppel. U kunt dan de interface gebruiken om te controleren of a Dog hoort bijvoorbeeld thuis in een arbitrageboek (zeg maar een register van huisdieren in een stad) via de Animal hulpprogramma methode:

Dog dog = new Dog("Shiba Inu");

boolean isInBook = Animal.checkBook(dog, new ArrayList());
System.out.println(isInBook);

isInBook = Animal.checkBook(dog, List.of(dog));
System.out.println(isInBook);

Functionele interfaces

Functionele interfaces zijn geïntroduceerd in Java 8, en ze vertegenwoordigen een interface die: slechts een enkele abstracte methode erin. U kunt uw eigen functionele interfaces definiëren, daar is de overvloed aan ingebouwde functionele interfaces in Java, zoals: Function, Predicate, UnaryOperator, BinaryOperator, Supplier, enzovoort, zijn zeer waarschijnlijk om direct aan uw behoeften te voldoen. Deze zijn allemaal te vinden in de java.util.function pakket. We zullen hier echter niet dieper op ingaan, omdat ze niet echt het hoofdonderwerp van deze gids zijn.

Als je een holistische, diepgaande en gedetailleerde gids voor functionele interfaces wilt lezen, lees dan onze "Gids voor functionele interfaces en Lambda-expressies in Java"!

Naamgevingsconventies voor interfaces

Dus, hoe noem je interfaces? Er is geen vaste regel en afhankelijk van het team waarmee u werkt, ziet u mogelijk verschillende conventies. Sommige ontwikkelaars prefixen interfacenamen met I, zoals IAnimal. Dit is niet erg gebruikelijk bij Java-ontwikkelaars en wordt voornamelijk overgedragen van ontwikkelaars die eerder in andere ecosystemen hebben gewerkt.

Java heeft een duidelijke naamgevingsconventie. Bijvoorbeeld, List is een interface terwijl ArrayList, LinkedList, etc. zijn implementaties van die interface. Bovendien beschrijven sommige interfaces de mogelijkheden van een klasse, zoals: Runnable, Comparable en Serializable. Het hangt vooral af van wat de bedoelingen van je interface zijn:

  • Als uw interface een generieke ruggengraat is voor een gemeenschappelijke klasse van klassen waarbij elke set vrij nauwkeurig kan worden beschreven door zijn familie - noem het als de familienaam, zoals: Set, en implementeer vervolgens a LinkedHashSet.
  • Als uw interface een generieke ruggengraat is voor een gemeenschappelijke klasse van klassen waarbij elke set kan niet redelijk nauwkeurig worden beschreven door zijn familie - noem het als de familienaam, zoals: Animal, en implementeer vervolgens a Bird, liever dan een FlyingAnimal (want dat is geen goede omschrijving).
  • Als je interface wordt gebruikt om de mogelijkheden van een klasse te beschrijven, noem het dan een vaardigheid, zoals: Runnable, Comparable.
  • Als uw interface wordt gebruikt om een ​​service te beschrijven, noem deze dan de service, zoals: UserDAO en implementeer vervolgens een UserDaoImpl.

Conclusie

In deze handleiding hebben we een van de belangrijkste basisconcepten voor objectgeoriënteerd programmeren in Java behandeld. We hebben uitgelegd wat interfaces zijn en hun voor- en nadelen besproken. We hebben ook laten zien hoe u ze definieert en gebruikt in een paar eenvoudige voorbeelden, waarbij meerdere overervingen en interface-overerving worden behandeld. We bespraken de verschillen en overeenkomsten tussen interfaces en abstracte klassen, standaard en statische methoden, naamgevingsconventies en functionele interfaces.

Interfaces zijn vrij eenvoudige structuren met een eenvoudig doel voor ogen, maar ze zijn een zeer krachtig hulpmiddel dat moet worden gebruikt wanneer de gelegenheid zich voordoet, zodat de code leesbaarder en duidelijker wordt.

spot_img

Laatste intelligentie

spot_img

Chat met ons

Hallo daar! Hoe kan ik u helpen?