Interfaces in Java

Java-interfaces verschillen van klassen, en het is belangrijk om te weten hoe u hun speciale eigenschappen in uw Java-programma's kunt gebruiken. Deze tutorial introduceert het verschil tussen klassen en interfaces, en leidt je vervolgens door voorbeelden die demonstreren hoe je Java-interfaces declareert, implementeert en uitbreidt.

Je leert ook hoe de interface is geëvolueerd in Java 8, met de toevoeging van standaard- en statische methoden, en in Java 9 met de nieuwe privémethoden. Deze toevoegingen maken interfaces nuttiger voor ervaren ontwikkelaars. Helaas vervagen ze ook de lijnen tussen klassen en interfaces, waardoor het programmeren van interfaces nog verwarrender wordt voor Java-beginners.

download Download de code Download de broncode voor voorbeeldtoepassingen in deze tutorial. Gemaakt door Jeff Friesen voor JavaWorld.

Wat is een Java-interface?

Een interface is een punt waar twee systemen elkaar ontmoeten en op elkaar inwerken. U kunt bijvoorbeeld een automaatinterface gebruiken om een ​​item te selecteren, ervoor te betalen en een item voor eten of drinken te ontvangen. Vanuit een programmeerperspectief zit een interface tussen softwarecomponenten. Bedenk dat een methode-header (methode naam, parameterlijst, enzovoort) interface zit tussen externe code die de methode aanroept en de code binnen de methode die zal worden uitgevoerd als resultaat van de aanroep. Hier is een voorbeeld:

System.out.println(average(10, 15)); double average(double x, double y) // interface between average(10, 15) call and return (x + y) / 2; { return (x + y) / 2; }

Wat vaak verwarrend is voor Java-beginners, is dat klassen ook interfaces hebben. Zoals ik heb uitgelegd in Java 101: Klassen en objecten in Java, is de interface het deel van de klasse dat toegankelijk is voor code die zich daarbuiten bevindt. De interface van een klasse bestaat uit een combinatie van methoden, velden, constructors en andere entiteiten. Overweeg lijst 1.

Lijst 1. De accountklasse en zijn interface

class Account { private String name; private long amount; Account(String name, long amount) { this.name = name; setAmount(amount); } void deposit(long amount) { this.amount += amount; } String getName() { return name; } long getAmount() { return amount; } void setAmount(long amount) { this.amount = amount; } }

De Account(String name, long amount)constructeur en de void deposit(long amount), String getName(), long getAmount()en void setAmount(long amount)methoden vormen de Accountinterface van klasse: ze zijn toegankelijk voor externe code. De private String name;en de private long amount;velden zijn niet toegankelijk.

Meer over Java-interfaces

Wat kunt u doen met interfaces in uw Java-programma's? Krijg een overzicht met Jeff's zes rollen van de Java-interface.

De code van een methode, die de interface van de methode ondersteunt, en dat deel van een klasse dat de interface van de klasse ondersteunt (zoals privévelden) staat bekend als de implementatie van de methode of klasse . Een implementatie moet worden verborgen voor externe code, zodat deze kan worden gewijzigd om aan veranderende vereisten te voldoen.

Wanneer implementaties worden blootgelegd, kunnen onderlinge afhankelijkheden tussen softwarecomponenten ontstaan. Methodecode kan bijvoorbeeld afhankelijk zijn van externe variabelen en de gebruikers van een klasse kunnen afhankelijk worden van velden die verborgen hadden moeten zijn. Deze koppeling kan tot problemen leiden wanneer implementaties moeten evolueren (misschien moeten blootgestelde velden worden verwijderd).

Java-ontwikkelaars gebruiken de interfacetaalfunctie om klasse-interfaces te abstraheren, waardoor klassen van hun gebruikers worden ontkoppeld . Door te focussen op Java-interfaces in plaats van op klassen, kunt u het aantal verwijzingen naar klassenamen in uw broncode minimaliseren. Dit vergemakkelijkt het overschakelen van de ene klasse naar de andere (misschien om de prestaties te verbeteren) naarmate uw software ouder wordt. Hier is een voorbeeld:

List names = new ArrayList() void print(List names) { // ... }

In dit voorbeeld wordt een namesveld gedeclareerd en geïnitialiseerd waarin een lijst met stringnamen is opgeslagen. Het voorbeeld beschrijft ook een print()methode voor het afdrukken van de inhoud van een lijst met strings, misschien één string per regel. Om het kort te houden, heb ik de implementatie van de methode weggelaten.

Listis een Java-interface die een opeenvolgende verzameling objecten beschrijft. ArrayListis een klasse die een array-gebaseerde implementatie van de ListJava-interface beschrijft. Een nieuw exemplaar van de ArrayListklasse wordt opgehaald en toegewezen aan Listvariabele names. ( Listen ArrayListworden opgeslagen in het standaardpakket van de klassenbibliotheek java.util.)

Hoeksteunen en generieken

De punthaken ( <en >) maken deel uit van Java's generieke functieset. Ze geven aan dat nameseen lijst met strings wordt beschreven (alleen strings kunnen in de lijst worden opgeslagen). Ik zal generieke geneesmiddelen introduceren in een toekomstig Java 101-artikel.

Wanneer er interactie is met de clientcode names, worden die methoden aangeroepen die worden gedeclareerd door Listen die worden geïmplementeerd door ArrayList. De klantcode heeft geen directe interactie met ArrayList. Als gevolg hiervan zal de clientcode niet breken wanneer een andere implementatieklasse, zoals LinkedList, vereist is:

List names = new LinkedList() // ... void print(List names) { // ... }

Omdat het print()type methode parameter is List, hoeft de implementatie van deze methode niet te veranderen. Als het type echter was geweest ArrayList, zou het type moeten worden gewijzigd in LinkedList. Als beide klassen hun eigen unieke methoden zouden declareren, moet u mogelijk print()de implementatie aanzienlijk wijzigen .

Door los te koppelen Listvan ArrayListen LinkedListkun je code schrijven die immuun is voor wijzigingen in de klasse-implementatie. Door Java-interfaces te gebruiken, kunt u problemen vermijden die zouden kunnen ontstaan ​​door het vertrouwen op implementatieklassen. Deze ontkoppeling is de belangrijkste reden om Java-interfaces te gebruiken.

Java-interfaces declareren

U declareert een interface door vast te houden aan een klasse-achtige syntaxis die bestaat uit een koptekst gevolgd door een hoofdtekst. De koptekst bestaat minimaal uit een trefwoord interfacegevolgd door een naam die de interface identificeert. Het lichaam begint met een open beugel en eindigt met een gesloten beugel. Tussen deze scheidingstekens bevinden zich constante en methodheader-declaraties:

interface identifier { // interface body }

Volgens afspraak wordt de eerste letter van de naam van een interface in hoofdletters weergegeven en worden daaropvolgende letters in kleine letters (bijvoorbeeld Drawable). Als een naam uit meerdere woorden bestaat, is de eerste letter van elk woord hoofdletters (zoals DrawableAndFillable). Deze naamgevingsconventie staat bekend als CamelCasing.

Listing 2 declareert een interface met de naam Drawable.

Listing 2. Een voorbeeld van een Java-interface

interface Drawable { int RED = 1; int GREEN = 2; int BLUE = 3; int BLACK = 4; int WHITE = 5; void draw(int color); }

Interfaces in Java's standaard klassenbibliotheek

Als naamgevingsconventie eindigen veel interfaces in Java's standaard klassenbibliotheek met het in staat achtervoegsel. Voorbeelden hiervan zijn Callable, Cloneable, Comparable, Formattable, Iterable, Runnable, Serializable, en Transferable. Het achtervoegsel is echter niet verplicht; de standaard klasse bibliotheek bevat de interfaces CharSequence, ClipboardOwner, Collection, Executor, Future, Iterator, List, Mapen vele anderen.

Drawabledeclareert vijf velden die kleurconstanten identificeren. Deze interface declareert ook de koptekst voor een draw()methode die moet worden aangeroepen met een van deze constanten om de kleur te specificeren die wordt gebruikt om een ​​omtrek te tekenen. (Het gebruik van integer-constanten is geen goed idee, omdat elke integer-waarde kan worden doorgegeven draw(). In een eenvoudig voorbeeld volstaan ​​ze echter.)

Field and method header defaults

Fields that are declared in an interface are implicitly public final static. An interface's method headers are implicitly public abstract.

Drawable identifies a reference type that specifies what to do (draw something) but not how to do it. Implementation details are consigned to classes that implement this interface. Instances of such classes are known as drawables because they know how to draw themselves.

Marker and tagging interfaces

An interface with an empty body is known as a marker interface or a tagging interface. The interface exists only to associate metadata with a class. For example, Cloneable (see Inheritance in Java, Part 2) implies that instances of its implementing class can be shallowly cloned. When Object's clone() method detects (via runtime type identification) that the calling instance's class implements Cloneable, it shallowly clones the object.

Implementing Java interfaces

A class implements an interface by appending Java's implements keyword followed by a comma-separated list of interface names to the class header, and by coding each interface method in the class. Listing 3 presents a class that implements Listing 2's Drawable interface.

Listing 3. Circle implementing the Drawable interface

class Circle implements Drawable { private double x, y, radius; Circle(double x, double y, double radius) { this.x = x; this.y = y; this.radius = radius; } @Override public void draw(int color) { System.out.println("Circle drawn at (" + x + ", " + y + "), with radius " + radius + ", and color " + color); } double getRadius() { return radius; } double getX() { return x; } double getY() { return y; } }

Listing 3's Circle class describes a circle as a center point and a radius. As well as providing a constructor and suitable getter methods, Circle implements the Drawable interface by appending implements Drawable to the Circle header, and by overriding (as indicated by the @Override annotation) Drawable's draw() method header.

Listing 4 presents a second example: a Rectangle class that also implements Drawable.

Listing 4. Implementing the Drawable interface in a Rectangle context

class Rectangle implements Drawable { private double x1, y1, x2, y2; Rectangle(double x1, double y1, double x2, double y2) { this.x1 = x1; this.y1 = y1; this.x2 = x2; this.y2 = y2; } @Override public void draw(int color) { System.out.println("Rectangle drawn with upper-left corner at (" + x1 + ", " + y1 + ") and lower-right corner at (" + x2 + ", " + y2 + "), and color " + color); } double getX1() { return x1; } double getX2() { return x2; } double getY1() { return y1; } double getY2() { return y2; } }

Listing 4's Rectangle class describes a rectangle as a pair of points denoting the upper-left and lower-right corners of this shape. As with Circle, Rectangle provides a constructor and suitable getter methods, and also implements the Drawable interface.

Overriding interface method headers

The compiler reports an error when you attempt to compile a non-abstract class that includes an implements interface clause but doesn't override all of the interface's method headers.

An interface type's data values are the objects whose classes implement the interface and whose behaviors are those specified by the interface's method headers. This fact implies that you can assign an object's reference to a variable of the interface type, provided that the object's class implements the interface. Listing 5 demonstrates.

Listing 5. Aliasing Circle and Rectangle objects as Drawables

class Draw { public static void main(String[] args) { Drawable[] drawables = new Drawable[] { new Circle(10, 20, 15), new Circle(30, 20, 10), new Rectangle(5, 8, 8, 9) }; for (int i = 0; i < drawables.length; i++) drawables[i].draw(Drawable.RED); } }

Because Circle and Rectangle implement Drawable, Circle and Rectangle objects have Drawable type in addition to their class types. Therefore, it's legal to store each object's reference in an array of Drawables. A loop iterates over this array, invoking each Drawable object's draw() method to draw a circle or a rectangle.

Assuming that Listing 2 is stored in a Drawable.java source file, which is in the same directory as the Circle.java, Rectangle.java, and Draw.java source files (which respectively store Listing 3, Listing 4, and Listing 5), compile these source files via either of the following command lines:

javac Draw.java javac *.java

Run the Draw application as follows:

java Draw

You should observe the following output:

Circle drawn at (10.0, 20.0), with radius 15.0, and color 1 Circle drawn at (30.0, 20.0), with radius 10.0, and color 1 Rectangle drawn with upper-left corner at (5.0, 8.0) and lower-right corner at (8.0, 9.0), and color 1

Note that you could also generate the same output by specifying the following main() method:

public static void main(String[] args) { Circle c = new Circle(10, 20, 15); c.draw(Drawable.RED); c = new Circle(30, 20, 10); c.draw(Drawable.RED); Rectangle r = new Rectangle(5, 8, 8, 9); r.draw(Drawable.RED); }

Zoals u kunt zien, is het vervelend om herhaaldelijk de draw()methode van elk object aan te roepen . Bovendien voegt dit extra bytecode toe aan Drawhet klassebestand van. Door te denken aan Circleen Rectangleals Drawables, kunt u een array en een eenvoudige lus gebruiken om de code te vereenvoudigen. Dit is een bijkomend voordeel van het ontwerpen van code om interfaces te verkiezen boven klassen.

Voorzichtigheid!