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 uitAnimal
enBird
implementeert alleen deFlying
interface (en deAnimal
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 Animal
s:
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 aLinkedHashSet
. - 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 aBird
, liever dan eenFlyingAnimal
(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 eenUserDaoImpl
.
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.