Gränssnitt i Java

Java-gränssnitt skiljer sig från klasser, och det är viktigt att veta hur man använder deras speciella egenskaper i dina Java-program. Denna handledning introducerar skillnaden mellan klasser och gränssnitt och guidar dig sedan genom exempel som visar hur du deklarerar, implementerar och utökar Java-gränssnitt.

Du lär dig också hur gränssnittet har utvecklats i Java 8, med tillägg av standard- och statiska metoder, och i Java 9 med de nya privata metoderna. Dessa tillägg gör gränssnitt mer användbara för erfarna utvecklare. Tyvärr suddar de också linjerna mellan klasser och gränssnitt, vilket gör gränssnittsprogrammering ännu mer förvirrande för Java-nybörjare.

ladda ner Skaffa koden Ladda ner källkoden till exempel applikationer i denna handledning. Skapad av Jeff Friesen för JavaWorld.

Vad är ett Java-gränssnitt?

Ett gränssnitt är en punkt där två system möts och interagerar. Du kan till exempel använda ett varuautomatgränssnitt för att välja en artikel, betala för den och ta emot en mat- eller dryckeartikel. Ur ett programmeringsperspektiv sitter ett gränssnitt mellan programvarukomponenter. Tänk på att ett metodhuvud (metodnamn, parameterlista och så vidare) gränssnitt sitter mellan extern kod som anropar metoden och koden inom den metod som kommer att köras som ett resultat av samtalet. Här är ett exempel:

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; }

Vad som ofta är förvirrande för nybörjare i Java är att klasser också har gränssnitt. Som jag förklarade i Java 101: Klasser och objekt i Java är gränssnittet den del av klassen som är tillgänglig för kod utanför den. Klassens gränssnitt består av en kombination av metoder, fält, konstruktörer och andra enheter. Överväg att lista 1.

Listning 1. Kontoklassen och dess gränssnitt

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; } }

Den Account(String name, long amount)konstruktör och void deposit(long amount), String getName(), long getAmount(), och void setAmount(long amount)metoder utgör Accountklassen gränssnitt: de är tillgängliga för extern kod. Den private String name;och private long amount;fält är otillgängliga.

Mer om Java-gränssnitt

Vad kan du göra med gränssnitt i dina Java-program? Få en översikt med Jeffs sex roller i Java-gränssnittet.

En metodkod, som stöder metodens gränssnitt, och den del av en klass som stöder klassens gränssnitt (t.ex. privata fält) kallas metodens eller klassens implementering . En implementering bör döljas från extern kod så att den kan ändras för att möta utvecklande krav.

När implementeringar exponeras kan ömsesidigt beroende mellan programvarukomponenter uppstå. Metodkod kan till exempel förlita sig på externa variabler och användare av en klass kan bli beroende av fält som borde ha döljts. Denna koppling kan leda till problem när implementeringar måste utvecklas (kanske exponerade fält måste tas bort).

Java-utvecklare använder gränssnittsspråkfunktionen för att abstrahera klassgränssnitt, vilket kopplar bort klasser från sina användare. Genom att fokusera på Java-gränssnitt istället för klasser kan du minimera antalet referenser till klassnamn i din källkod. Detta underlättar byte från en klass till en annan (kanske för att förbättra prestanda) när din programvara mognar. Här är ett exempel:

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

Det här exemplet deklarerar och initialiserar ett namesfält som lagrar en lista med strängnamn. Exemplet förklarar också en print()metod för att skriva ut innehållet i en lista med strängar, kanske en sträng per rad. För korthet har jag utelämnat metodens implementering.

Listär ett Java-gränssnitt som beskriver en sekventiell samling av objekt. ArrayListär en klass som beskriver en arraybaserad implementering av ListJava-gränssnittet. En ny instans av ArrayListklassen erhålls och tilldelas Listvariabeln names. ( Listoch ArrayListlagras i standardklassbibliotekets java.utilpaket.)

Vinkelfästen och generika

Vinkelfästena ( <och >) är en del av Java: s generiska funktionsuppsättning. De anger som namesbeskriver en lista med strängar (endast strängar kan lagras i listan). Jag introducerar generika i en framtida Java 101-artikel.

När klientkoden interagerar med nameskommer den att åberopa de metoder som deklareras av Listoch som implementeras av ArrayList. Klientkoden kommer inte att interagera direkt med ArrayList. Som ett resultat bryts inte klientkoden när en annan implementeringsklass LinkedListkrävs, till exempel:

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

Eftersom print()metodparametertypen är Listbehöver inte metodens implementering ändras. Men om typen hade varit ArrayList, måste typen ändras till LinkedList. Om båda klasserna skulle deklarera sina egna unika metoder kan du behöva ändra print()implementeringen avsevärt .

Koppla Listfrån ArrayListoch LinkedListlåter dig skriva kod som är immun mot klassimplementeringsändringar. Genom att använda Java-gränssnitt kan du undvika problem som kan uppstå genom att förlita sig på implementeringsklasser. Denna frikoppling är den främsta anledningen till att använda Java-gränssnitt.

Förklarar Java-gränssnitt

Du förklarar ett gränssnitt genom att följa en klasslik syntax som består av en rubrik följt av en kropp. I åtminstone består rubriken av nyckelord interfaceföljt av ett namn som identifierar gränssnittet. Kroppen börjar med en öppen stagkaraktär och slutar med en nära stag. Mellan dessa avgränsare finns konstanta och metodhuvuddeklarationer:

interface identifier { // interface body }

Enligt konvention är den första bokstaven i ett gränssnittsnamn versaler och efterföljande bokstäver är lägre (t.ex. Drawable). Om ett namn består av flera ord, verseras den första bokstaven i varje ord (t.ex. DrawableAndFillable). Denna namnkonvention kallas CamelCasing.

Listing 2 förklarar ett gränssnitt med namnet Drawable.

Listing 2. Ett exempel på Java-gränssnitt

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

Gränssnitt i Javas standardklassbibliotek

Som en namngivningskonvention slutar många gränssnitt i Java: s standardklassbibliotek med kapacitetssuffixet . Som exempel kan nämnas Callable, Cloneable, Comparable, Formattable, Iterable, Runnable, Serializable, och Transferable. Suffixet är dock inte obligatoriskt; standardklassbibliotek innehåller gränssnitt CharSequence, ClipboardOwner, Collection, Executor, Future, Iterator, List, Mapoch många andra.

Drawableförklarar fem fält som identifierar färgkonstanter. Detta gränssnitt förklarar också rubriken för en draw()metod som måste anropas med en av dessa konstanter för att specificera färgen som används för att rita en kontur. (Att använda heltalskonstanter är inte en bra idé eftersom valfritt heltal kan överföras till draw(). De räcker dock i ett enkelt exempel.)

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); }

Som du kan se är det tråkigt att upprepade gånger åberopa varje objekts draw()metod. Dessutom lägger du till detta extra bytecode till Drawklassfilen. Genom att tänka på Circleoch Rectanglesom Drawables kan du använda en matris och en enkel slinga för att förenkla koden. Detta är en ytterligare fördel med att designa kod för att föredra gränssnitt framför klasser.

Varning!