Interfaces en Java

Les interfaces Java sont différentes des classes et il est important de savoir comment utiliser leurs propriétés spéciales dans vos programmes Java. Ce didacticiel présente la différence entre les classes et les interfaces, puis vous guide à travers des exemples montrant comment déclarer, implémenter et étendre des interfaces Java.

Vous apprendrez également comment l'interface a évolué en Java 8, avec l'ajout de méthodes par défaut et statiques, et en Java 9 avec les nouvelles méthodes privées. Ces ajouts rendent les interfaces plus utiles aux développeurs expérimentés. Malheureusement, ils brouillent également les lignes entre les classes et les interfaces, rendant la programmation d'interface encore plus déroutante pour les débutants en Java.

télécharger Obtenir le code Téléchargez le code source des exemples d'applications dans ce didacticiel. Créé par Jeff Friesen pour JavaWorld.

Qu'est-ce qu'une interface Java?

Une interface est un point où deux systèmes se rencontrent et interagissent. Par exemple, vous pouvez utiliser une interface de distributeur automatique pour sélectionner un article, le payer et recevoir un aliment ou une boisson. Du point de vue de la programmation, une interface se situe entre les composants logiciels. Considérez qu'une interface d'en-tête de méthode (nom de méthode, liste de paramètres, etc.) se situe entre le code externe qui appelle la méthode et le code dans la méthode qui sera exécutée à la suite de l'appel. Voici un exemple:

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

Ce qui est souvent déroutant pour les débutants en Java, c'est que les classes ont également des interfaces. Comme je l'ai expliqué dans Java 101: Classes et objets en Java, l'interface est la partie de la classe qui est accessible au code situé en dehors de celle-ci. L'interface d'une classe se compose d'une combinaison de méthodes, de champs, de constructeurs et d'autres entités. Considérez la liste 1.

Listing 1. La classe Account et son 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; } }

Le Account(String name, long amount)constructeur et void deposit(long amount), String getName(), long getAmount(), et les void setAmount(long amount)méthodes forment l' Accountinterface de la classe: ils sont accessibles à un code externe. Les champs private String name;et private long amount;sont inaccessibles.

En savoir plus sur les interfaces Java

Que pouvez-vous faire avec les interfaces de vos programmes Java? Obtenez un aperçu des six rôles de Jeff de l'interface Java.

Le code d'une méthode, qui prend en charge l'interface de la méthode, et la partie d'une classe qui prend en charge l'interface de la classe (comme les champs privés) est appelée implémentation de la méthode ou de la classe . Une implémentation doit être masquée du code externe afin de pouvoir être modifiée pour répondre aux exigences en constante évolution.

Lorsque les implémentations sont exposées, des interdépendances entre les composants logiciels peuvent survenir. Par exemple, le code de méthode peut s'appuyer sur des variables externes et les utilisateurs d'une classe peuvent devenir dépendants de champs qui auraient dû être masqués. Ce couplage peut conduire à des problèmes lorsque les implémentations doivent évoluer (peut-être que les champs exposés doivent être supprimés).

Les développeurs Java utilisent la fonctionnalité de langage d'interface pour abstraire les interfaces de classe, dissociant ainsi les classes de leurs utilisateurs. En vous concentrant sur les interfaces Java plutôt que sur les classes, vous pouvez réduire le nombre de références aux noms de classes dans votre code source. Cela facilite le passage d'une classe à une autre (peut-être pour améliorer les performances) à mesure que votre logiciel évolue. Voici un exemple:

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

Cet exemple déclare et initialise un nameschamp qui stocke une liste de noms de chaîne. L'exemple déclare également une print()méthode pour imprimer le contenu d'une liste de chaînes, peut-être une chaîne par ligne. Par souci de concision, j'ai omis l'implémentation de la méthode.

Listest une interface Java qui décrit une collection séquentielle d'objets. ArrayListest une classe qui décrit une implémentation basée sur un tableau de l' Listinterface Java. Une nouvelle instance de la ArrayListclasse est obtenue et affectée à la Listvariable names. ( Listet ArrayListsont stockés dans le java.utilpackage de la bibliothèque de classes standard .)

Équerres angulaires et génériques

Les chevrons ( <et >) font partie du jeu de fonctionnalités génériques de Java. Ils indiquent que namesdécrit une liste de chaînes (seules les chaînes peuvent être stockées dans la liste). Je présenterai les génériques dans un prochain article sur Java 101.

Lorsque le code client interagit avec names, il appellera les méthodes déclarées par Listet implémentées par ArrayList. Le code client n'interagira pas directement avec ArrayList. Par conséquent, le code client ne sera pas interrompu lorsqu'une classe d'implémentation différente, telle que LinkedList, est requise:

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

Étant donné que le print()type de paramètre de méthode est List, l'implémentation de cette méthode n'a pas à changer. Cependant, si le type avait été ArrayList, le type devrait être changé en LinkedList. Si les deux classes devaient déclarer leurs propres méthodes uniques, vous devrez peut-être modifier considérablement print()l'implémentation de.

Découplage Listde ArrayListet LinkedListvous permet d' écrire du code qui est à l' abri des changements de classe mise en œuvre. En utilisant les interfaces Java, vous pouvez éviter les problèmes qui pourraient résulter de l'utilisation des classes d'implémentation. Ce découplage est la principale raison d'utiliser les interfaces Java.

Déclaration des interfaces Java

Vous déclarez une interface en adhérant à une syntaxe de type classe qui se compose d'un en-tête suivi d'un corps. Au minimum, l'en-tête se compose d'un mot-clé interfacesuivi d'un nom qui identifie l'interface. Le corps commence par un caractère d'accolade ouverte et se termine par une accolade étroite. Entre ces délimiteurs se trouvent des déclarations d'en-tête de constante et de méthode:

interface identifier { // interface body }

Par convention, la première lettre du nom d'une interface est en majuscule et les lettres suivantes sont en minuscules (par exemple, Drawable). Si un nom se compose de plusieurs mots, la première lettre de chaque mot est en majuscule (par exemple DrawableAndFillable). Cette convention de dénomination est connue sous le nom de CamelCasing.

Le listing 2 déclare une interface nommée Drawable.

Listing 2. Un exemple d'interface Java

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

Interfaces dans la bibliothèque de classes standard de Java

En tant que convention de dénomination, de nombreuses interfaces de la bibliothèque de classes standard de Java se terminent par le suffixe able . Les exemples incluent Callable, Cloneable, Comparable, Formattable, Iterable, Runnable, Serializableet Transferable. Le suffixe n'est cependant pas obligatoire; la bibliothèque de classe standard comprend les interfaces CharSequence, ClipboardOwner, Collection, Executor, Future, Iterator, List, Mapet bien d' autres.

Drawabledéclare cinq champs qui identifient les constantes de couleur. Cette interface déclare également l'en-tête d'une draw()méthode qui doit être appelée avec l'une de ces constantes pour spécifier la couleur utilisée pour dessiner un contour. (L'utilisation de constantes entières n'est pas une bonne idée car n'importe quelle valeur entière peut être transmise draw(). Cependant, elles suffisent dans un exemple simple.)

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

Comme vous pouvez le voir, il est fastidieux d'invoquer à plusieurs reprises la draw()méthode de chaque objet . De plus, cela ajoute du bytecode supplémentaire au Drawfichier de classe de. En pensant à Circleet en Rectangletant que Drawables, vous pouvez exploiter un tableau et une simple boucle pour simplifier le code. C'est un avantage supplémentaire de la conception de code pour préférer les interfaces aux classes.

Mise en garde!