Interfacce in Java

Le interfacce Java sono diverse dalle classi ed è importante sapere come utilizzare le loro proprietà speciali nei programmi Java. Questo tutorial introduce la differenza tra classi e interfacce, quindi guida l'utente attraverso esempi che dimostrano come dichiarare, implementare ed estendere le interfacce Java.

Imparerai anche come si è evoluta l'interfaccia in Java 8, con l'aggiunta di metodi predefiniti e statici, e in Java 9 con i nuovi metodi privati. Queste aggiunte rendono le interfacce più utili per sviluppatori esperti. Sfortunatamente, sfocano anche i confini tra classi e interfacce, rendendo la programmazione dell'interfaccia ancora più confusa per i principianti di Java.

download Ottieni il codice Scarica il codice sorgente per applicazioni di esempio in questo tutorial. Creato da Jeff Friesen per JavaWorld.

Cos'è un'interfaccia Java?

Un'interfaccia è un punto in cui due sistemi incontrano e interagiscono. Ad esempio, è possibile utilizzare l'interfaccia di un distributore automatico per selezionare un articolo, pagarlo e ricevere un alimento o una bevanda. Dal punto di vista della programmazione, un'interfaccia si trova tra i componenti software. Considera che un'interfaccia di intestazione del metodo (nome del metodo, elenco di parametri e così via) si trova tra il codice esterno che chiama il metodo e il codice all'interno del metodo che verrà eseguito come risultato della chiamata. Ecco un esempio:

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

Ciò che spesso confonde i principianti di Java è che anche le classi hanno interfacce. Come ho spiegato in Java 101: Classi e oggetti in Java, l'interfaccia è la parte della classe accessibile al codice che si trova al di fuori di essa. L'interfaccia di una classe è costituita da una combinazione di metodi, campi, costruttori e altre entità. Considera il listato 1.

Listato 1. La classe Account e la sua interfaccia

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

Il Account(String name, long amount)costruttore e il void deposit(long amount), String getName(), long getAmount(), e void setAmount(long amount)metodi costituiscono la Accountl'interfaccia di classe: sono accessibili a codice esterno. I campi private String name;e private long amount;sono inaccessibili.

Ulteriori informazioni sulle interfacce Java

Cosa puoi fare con le interfacce nei tuoi programmi Java? Ottieni una panoramica con i sei ruoli di Jeff dell'interfaccia Java.

Il codice di un metodo, che supporta l'interfaccia del metodo e quella parte di una classe che supporta l'interfaccia della classe (come i campi privati) è noto come implementazione del metodo o della classe . Un'implementazione dovrebbe essere nascosta dal codice esterno in modo che possa essere modificata per soddisfare i requisiti in evoluzione.

Quando le implementazioni sono esposte, possono sorgere interdipendenze tra i componenti software. Ad esempio, il codice del metodo può fare affidamento su variabili esterne e gli utenti di una classe possono diventare dipendenti da campi che avrebbero dovuto essere nascosti. Questo accoppiamento può portare a problemi quando le implementazioni devono evolversi (forse i campi esposti devono essere rimossi).

Gli sviluppatori Java utilizzano la funzionalità del linguaggio dell'interfaccia per astrarre le interfacce delle classi, separando così le classi dai loro utenti. Concentrandosi sulle interfacce Java invece che sulle classi, è possibile ridurre al minimo il numero di riferimenti ai nomi delle classi nel codice sorgente. Ciò facilita il passaggio da una classe all'altra (forse per migliorare le prestazioni) man mano che il software matura. Ecco un esempio:

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

Questo esempio dichiara e inizializza un namescampo che memorizza un elenco di nomi di stringhe. L'esempio dichiara anche un print()metodo per stampare il contenuto di un elenco di stringhe, forse una stringa per riga. Per brevità, ho omesso l'implementazione del metodo.

Listè un'interfaccia Java che descrive una raccolta sequenziale di oggetti. ArrayListè una classe che descrive un'implementazione basata su array Listdell'interfaccia Java. Una nuova istanza della ArrayListclasse viene ottenuta e assegnata alla Listvariabile names. ( Liste ArrayListsono archiviati nel java.utilpacchetto della libreria di classi standard .)

Parentesi angolari e generici

Le parentesi angolari ( <e >) fanno parte del set di funzioni generiche di Java. Indicano che namesdescrive un elenco di stringhe (solo le stringhe possono essere memorizzate nell'elenco). Introdurrò i generici in un futuro articolo di Java 101.

Quando il codice client interagisce names, richiamerà i metodi dichiarati da Liste implementati da ArrayList. Il codice client non interagirà direttamente con ArrayList. Di conseguenza, il codice client non si interromperà quando è richiesta una classe di implementazione diversa, ad esempio LinkedList:

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

Poiché il print()tipo di parametro del metodo è List, l'implementazione di questo metodo non deve cambiare. Tuttavia, se il tipo fosse stato ArrayList, il tipo dovrebbe essere cambiato in LinkedList. Se entrambe le classi dovessero dichiarare i propri metodi univoci, potrebbe essere necessario modificare in modo significativo print()l'implementazione di.

Il disaccoppiamento Listda ArrayListe LinkedListconsente di scrivere codice immune alle modifiche all'implementazione della classe. Utilizzando le interfacce Java, è possibile evitare problemi che potrebbero derivare dall'affidarsi alle classi di implementazione. Questo disaccoppiamento è il motivo principale per l'utilizzo delle interfacce Java.

Dichiarazione delle interfacce Java

Dichiari un'interfaccia aderendo a una sintassi di tipo classe che consiste in un'intestazione seguita da un corpo. L'intestazione è costituita almeno interfaceda una parola chiave seguita da un nome che identifica l'interfaccia. Il corpo inizia con una parentesi graffa aperta e termina con una parentesi graffa chiusa. Tra questi delimitatori ci sono costanti e dichiarazioni di intestazione del metodo:

interface identifier { // interface body }

Per convenzione, la prima lettera del nome di un'interfaccia è maiuscola e le lettere successive sono minuscole (ad esempio, Drawable). Se un nome è composto da più parole, la prima lettera di ogni parola è maiuscola (come DrawableAndFillable). Questa convenzione di denominazione è nota come CamelCasing.

Il Listato 2 dichiara un'interfaccia denominata Drawable.

Listato 2. Un esempio di interfaccia Java

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

Interfacce nella libreria di classi standard di Java

Come convenzione di denominazione, molte interfacce nella libreria di classi standard di Java terminano con il suffisso abile . Gli esempi includono Callable, Cloneable, Comparable, Formattable, Iterable, Runnable, Serializable, e Transferable. Tuttavia, il suffisso non è obbligatorio; la libreria di classi standard comprende le interfacce CharSequence, ClipboardOwner, Collection, Executor, Future, Iterator, List, Mape molti altri.

Drawabledichiara cinque campi che identificano le costanti di colore. Questa interfaccia dichiara anche l'intestazione di un draw()metodo che deve essere chiamato con una di queste costanti per specificare il colore utilizzato per disegnare un contorno. (L'uso di costanti intere non è una buona idea perché è possibile passare qualsiasi valore intero draw(). Tuttavia, sono sufficienti in un semplice esempio.)

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

Come puoi vedere, è noioso invocare ripetutamente il draw()metodo di ogni oggetto . Inoltre, così facendo si aggiunge un bytecode extra al Drawfile di classe di. Pensando a Circlee Rectanglecome Drawables, puoi sfruttare un array e un semplice ciclo per semplificare il codice. Questo è un ulteriore vantaggio derivante dalla progettazione del codice per preferire le interfacce alle classi.

Attenzione!