Schnittstellen in Java
Java-Schnittstellen unterscheiden sich von Klassen, und es ist wichtig zu wissen, wie ihre speziellen Eigenschaften in Ihren Java-Programmen verwendet werden. In diesem Lernprogramm wird der Unterschied zwischen Klassen und Schnittstellen vorgestellt. Anschließend werden Sie durch Beispiele geführt, die zeigen, wie Java-Schnittstellen deklariert, implementiert und erweitert werden.
Sie erfahren auch, wie sich die Benutzeroberfläche in Java 8 mit zusätzlichen und statischen Methoden und in Java 9 mit den neuen privaten Methoden entwickelt hat. Diese Ergänzungen machen Schnittstellen für erfahrene Entwickler nützlicher. Leider verwischen sie auch die Grenzen zwischen Klassen und Schnittstellen, was die Schnittstellenprogrammierung für Java-Anfänger noch verwirrender macht.
download Code abrufen Laden Sie den Quellcode herunter, zum Beispiel Anwendungen in diesem Tutorial. Erstellt von Jeff Friesen für JavaWorld.Was ist eine Java-Schnittstelle?
Eine Schnittstelle ist ein Punkt, an dem sich zwei Systeme treffen und interagieren. Sie können beispielsweise eine Verkaufsautomatenschnittstelle verwenden, um einen Artikel auszuwählen, dafür zu bezahlen und einen Lebensmittel- oder Getränkeartikel zu erhalten. Aus programmtechnischer Sicht befindet sich eine Schnittstelle zwischen Softwarekomponenten. Beachten Sie, dass sich zwischen dem externen Code, der die Methode aufruft, und dem Code innerhalb der Methode, die als Ergebnis des Aufrufs ausgeführt wird, eine Schnittstelle für den Methodenheader (Methodenname, Parameterliste usw.) befindet. Hier ist ein Beispiel:
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; }
Was für Java-Anfänger oft verwirrend ist, ist, dass Klassen auch Schnittstellen haben. Wie ich in Java 101 erklärt habe: Klassen und Objekte in Java, ist die Schnittstelle der Teil der Klasse, auf den Code außerhalb zugreifen kann. Die Schnittstelle einer Klasse besteht aus einer Kombination von Methoden, Feldern, Konstruktoren und anderen Entitäten. Betrachten Sie Listing 1.
Listing 1. Die Account-Klasse und ihre Schnittstelle
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; } }
Der Account(String name, long amount)
Konstruktor und die void deposit(long amount)
, String getName()
, long getAmount()
und void setAmount(long amount)
Methoden bilden die Account
Klasse Schnittstelle: sie zu externem Code zugänglich sind. Die Felder private String name;
und private long amount;
sind nicht zugänglich.
Weitere Informationen zu Java-Schnittstellen
Was können Sie mit Schnittstellen in Ihren Java-Programmen tun? Verschaffen Sie sich einen Überblick über Jeffs sechs Rollen der Java-Oberfläche.
Der Code einer Methode, der die Schnittstelle der Methode unterstützt, und der Teil einer Klasse, der die Schnittstelle der Klasse unterstützt (z. B. private Felder), werden als Implementierung der Methode oder Klasse bezeichnet . Eine Implementierung sollte vor externem Code verborgen sein, damit sie an sich ändernde Anforderungen angepasst werden kann.
Wenn Implementierungen verfügbar gemacht werden, können Abhängigkeiten zwischen Softwarekomponenten auftreten. Beispielsweise kann Methodencode auf externen Variablen beruhen und die Benutzer einer Klasse können von Feldern abhängig werden, die ausgeblendet werden sollten. Diese Kopplung kann zu Problemen führen, wenn sich Implementierungen entwickeln müssen (möglicherweise müssen exponierte Felder entfernt werden).
Java-Entwickler verwenden die Interface-Sprachfunktion, um Klassenschnittstellen zu abstrahieren und so Klassen von ihren Benutzern zu entkoppeln . Indem Sie sich auf Java-Schnittstellen anstelle von Klassen konzentrieren, können Sie die Anzahl der Verweise auf Klassennamen in Ihrem Quellcode minimieren. Dies erleichtert den Wechsel von einer Klasse zur anderen (möglicherweise zur Verbesserung der Leistung), wenn Ihre Software ausgereift ist. Hier ist ein Beispiel:
List names = new ArrayList() void print(List names) { // ... }
In diesem Beispiel wird ein names
Feld deklariert und initialisiert , in dem eine Liste von Zeichenfolgennamen gespeichert ist. Das Beispiel deklariert auch eine print()
Methode zum Ausdrucken des Inhalts einer Liste von Zeichenfolgen, möglicherweise eine Zeichenfolge pro Zeile. Der Kürze halber habe ich die Implementierung der Methode weggelassen.
List
ist eine Java-Schnittstelle, die eine sequentielle Sammlung von Objekten beschreibt. ArrayList
ist eine Klasse, die eine Array-basierte Implementierung der List
Java-Schnittstelle beschreibt. Eine neue Instanz der ArrayList
Klasse wird abgerufen und der List
Variablen zugewiesen names
. ( List
und ArrayList
werden im java.util
Paket der Standardklassenbibliothek gespeichert .)
Spitze Klammern und Generika
Die spitzen Klammern ( <
und >
) sind Teil des generischen Funktionsumfangs von Java. Sie geben an, dass names
eine Liste von Zeichenfolgen beschrieben wird (in der Liste können nur Zeichenfolgen gespeichert werden). Ich werde Generika in einem zukünftigen Java 101-Artikel vorstellen.
Wenn Clientcode mit interagiert names
, werden die Methoden aufgerufen, die von deklariert und von List
implementiert werden ArrayList
. Der Client-Code interagiert nicht direkt mit ArrayList
. Infolgedessen wird der Clientcode nicht unterbrochen, wenn eine andere Implementierungsklasse LinkedList
erforderlich ist , z.
List names = new LinkedList() // ... void print(List names) { // ... }
Da der print()
Methodenparametertyp lautet List
, muss sich die Implementierung dieser Methode nicht ändern. Wenn der Typ jedoch gewesen wäre ArrayList
, müsste der Typ in geändert werden LinkedList
. Wenn beide Klassen ihre eigenen eindeutigen Methoden deklarieren, müssen Sie möglicherweise die print()
Implementierung erheblich ändern .
Entkoppeln List
von ArrayList
und LinkedList
ermöglicht das Schreiben von Code, der gegen Änderungen bei der Klassenimplementierung immun ist. Durch die Verwendung von Java-Schnittstellen können Sie Probleme vermeiden, die durch die Verwendung von Implementierungsklassen entstehen können. Diese Entkopplung ist der Hauptgrund für die Verwendung von Java-Schnittstellen.
Java-Schnittstellen deklarieren
Sie deklarieren eine Schnittstelle, indem Sie sich an eine klassenähnliche Syntax halten, die aus einem Header gefolgt von einem Body besteht. Der Header besteht mindestens aus einem Schlüsselwort, interface
gefolgt von einem Namen, der die Schnittstelle identifiziert. Der Körper beginnt mit einer offenen Klammer und endet mit einer engen Klammer. Zwischen diesen Trennzeichen befinden sich Konstanten- und Methodenheaderdeklarationen:
interface identifier { // interface body }
Konventionell wird der erste Buchstabe des Namens einer Schnittstelle in Großbuchstaben und nachfolgende Buchstaben in Kleinbuchstaben geschrieben (z. B. Drawable
). Wenn ein Name aus mehreren Wörtern besteht, wird der erste Buchstabe jedes Wortes in Großbuchstaben geschrieben (z. B. DrawableAndFillable
). Diese Namenskonvention wird als CamelCasing bezeichnet.
Listing 2 deklariert eine Schnittstelle mit dem Namen Drawable
.
Listing 2. Ein Beispiel für eine Java-Schnittstelle
interface Drawable { int RED = 1; int GREEN = 2; int BLUE = 3; int BLACK = 4; int WHITE = 5; void draw(int color); }
Schnittstellen in der Standardklassenbibliothek von Java
As a naming convention, many interfaces in Java's standard class library end with the able suffix. Examples include Callable
, Cloneable
, Comparable
, Formattable
, Iterable
, Runnable
, Serializable
, and Transferable
. The suffix isn't mandatory, however; the standard class library includes the interfaces CharSequence
, ClipboardOwner
, Collection
, Executor
, Future
, Iterator
, List
, Map
and many others.
Drawable
declares five fields that identify color constants. This interface also declares the header for a draw()
method that must be called with one of these constants to specify the color used to draw an outline. (Using integer constants isn't a good idea because any integer value could be passed to draw()
. However, they suffice in a simple example.)
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 Drawable
s. 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); }
Wie Sie sehen, ist es mühsam, die draw()
Methode jedes Objekts wiederholt aufzurufen . Außerdem wird dadurch der Draw
Klassendatei zusätzlicher Bytecode hinzugefügt. Wenn Sie an Circle
und Rectangle
als Drawable
s denken , können Sie ein Array und eine einfache Schleife nutzen, um den Code zu vereinfachen. Dies ist ein zusätzlicher Vorteil des Entwurfs von Code, um Schnittstellen gegenüber Klassen zu bevorzugen.