Recherche…


Introduction

Une interface est un type de référence, similaire à une classe, qui peut être déclaré à l'aide du mot-clé d' interface . Les interfaces ne peuvent contenir que des constantes, des signatures de méthode, des méthodes par défaut, des méthodes statiques et des types imbriqués. Les corps de méthodes n'existent que pour les méthodes par défaut et les méthodes statiques. Comme les classes abstraites, les interfaces ne peuvent pas être instanciées: elles ne peuvent être implémentées que par des classes ou étendues par d'autres interfaces. L'interface est un moyen courant d'obtenir une abstraction complète en Java.

Syntaxe

  • interface publique Foo {void foo (); / * toute autre méthode * /}
  • interface publique Foo1 étend Foo {void bar (); / * toute autre méthode * /}
  • la classe publique Foo2 implémente Foo, Foo1 {/ * implémentation de Foo et Foo1 * /}

Déclaration et implémentation d'une interface

Déclaration d'une interface utilisant le mot-clé interface :

public interface Animal {
    String getSound(); // Interface methods are public by default
}

Annulation de l'annotation

@Override
public String getSound() {
    // Code goes here...
}

Cela oblige le compilateur à vérifier que nous remplaçons et empêche le programme de définir une nouvelle méthode ou de fausser la signature de la méthode.

Les interfaces sont implémentées à l'aide du mot clé implements .

public class Cat implements Animal {

    @Override 
    public String getSound() {
        return "meow";
    }
}

public class Dog implements Animal {

    @Override
    public String getSound() {
        return "woof";
    }
}

Dans l'exemple, les classes Cat et Dog doivent définir la méthode getSound() car les méthodes d'une interface sont intrinsèquement abstraites (à l'exception des méthodes par défaut).

Utiliser les interfaces

Animal cat = new Cat();
Animal dog = new Dog();

System.out.println(cat.getSound()); // prints "meow"
System.out.println(dog.getSound()); // prints "woof"

Implémentation de plusieurs interfaces

Une classe Java peut implémenter plusieurs interfaces.

public interface NoiseMaker {
    String noise = "Making Noise"; // interface variables are public static final by default

    String makeNoise(); //interface methods are public abstract by default
}

public interface FoodEater {
    void eat(Food food);
}

public class Cat implements NoiseMaker, FoodEater { 
    @Override
    public String makeNoise() {
        return "meow";
    }

    @Override
    public void eat(Food food) {
        System.out.println("meows appreciatively");
    }
}

Notez que la classe Cat doit implémenter les méthodes abstract héritées dans les deux interfaces. En outre, notez qu'une classe peut pratiquement implémenter autant d'interfaces que nécessaire (la limite de la JVM est limitée à 65 535 ).

NoiseMaker noiseMaker = new Cat(); // Valid
FoodEater foodEater = new Cat(); // Valid
Cat cat = new Cat(); // valid

Cat invalid1 = new NoiseMaker(); // Invalid
Cat invalid2 = new FoodEater(); // Invalid

Remarque:

  1. Toutes les variables déclarées dans une interface sont public static final
  2. Toutes les méthodes déclarées dans une interface sont public abstract (cette instruction est valide uniquement via Java 7. A partir de Java 8, vous êtes autorisé à avoir des méthodes dans une interface, qui ne doivent pas nécessairement être abstraites; ces méthodes sont appelées méthodes par défaut )
  3. Les interfaces ne peuvent être déclarées comme final
  4. Si plusieurs interfaces déclarent une méthode ayant une signature identique, elle est effectivement traitée comme une seule méthode et vous ne pouvez pas distinguer la méthode d'interface implémentée
  5. Un fichier InterfaceName.class correspondant serait généré pour chaque interface, lors de la compilation.

Extension d'une interface

Une interface peut étendre une autre interface via le mot extends clé extend.

public interface BasicResourceService {
    Resource getResource();
}

public interface ExtendedResourceService extends BasicResourceService {
    void updateResource(Resource resource);
}

Désormais, une classe implémentant ExtendedResourceService devra implémenter à la fois getResource() et updateResource() .

Extension de plusieurs interfaces

Contrairement aux classes, le mot extends clé extend peut être utilisé pour étendre plusieurs interfaces (séparées par des virgules), ce qui permet de combiner des interfaces dans une nouvelle interface.

public interface BasicResourceService {
    Resource getResource();
}

public interface AlternateResourceService {
    Resource getAlternateResource();
}

public interface ExtendedResourceService extends BasicResourceService, AlternateResourceService {
    Resource updateResource(Resource resource);
}

Dans ce cas, une classe implémentant ExtendedResourceService devra implémenter getResource() , getAlternateResource() et updateResource() .

Utiliser des interfaces avec des génériques

Supposons que vous souhaitiez définir une interface permettant de publier / consommer des données vers et depuis différents types de canaux (par exemple, AMQP, JMS, etc.), mais que vous souhaitiez pouvoir changer les détails d'implémentation ...

Définissons une interface IO de base pouvant être réutilisée sur plusieurs implémentations:

public interface IO<IncomingType, OutgoingType> {

    void publish(OutgoingType data);
    IncomingType consume();
    IncomingType RPCSubmit(OutgoingType data);

}

Maintenant, je peux instancier cette interface, mais comme nous n'avons pas d'implémentations par défaut pour ces méthodes, il faudra une implémentation lorsque nous l'instancierons:

    IO<String, String> mockIO = new IO<String, String>() {

        private String channel = "somechannel";

        @Override
        public void publish(String data) {
            System.out.println("Publishing " + data + " to " + channel);
        }

        @Override
        public String consume() {
            System.out.println("Consuming from " + channel);
            return "some useful data";
        }

        @Override
        public String RPCSubmit(String data) {
            return "received " + data + " just now ";
        }

    };

    mockIO.consume(); // prints: Consuming from somechannel
    mockIO.publish("TestData"); // Publishing TestData to somechannel
    System.out.println(mockIO.RPCSubmit("TestData")); // received TestData just now

Nous pouvons aussi faire quelque chose de plus utile avec cette interface, disons que nous voulons l'utiliser pour envelopper certaines fonctions de base de RabbitMQ:

public class RabbitMQ implements IO<String, String> {

    private String exchange;
    private String queue;

    public RabbitMQ(String exchange, String queue){
        this.exchange = exchange;
        this.queue = queue;
    }

    @Override
    public void publish(String data) {
        rabbit.basicPublish(exchange, queue, data.getBytes());
    }

    @Override
    public String consume() {
        return rabbit.basicConsume(exchange, queue);
    }

    @Override
    public String RPCSubmit(String data) {
        return rabbit.rpcPublish(exchange, queue, data);
    }

}

Disons que je veux utiliser cette interface IO maintenant pour compter les visites sur mon site depuis le dernier redémarrage du système et afficher ensuite le nombre total de visites - vous pouvez faire quelque chose comme ceci:

import java.util.concurrent.atomic.AtomicLong;

public class VisitCounter implements IO<Long, Integer> {

    private static AtomicLong websiteCounter = new AtomicLong(0);
    
    @Override
    public void publish(Integer count) {
        websiteCounter.addAndGet(count);
    }

    @Override
    public Long consume() {
        return websiteCounter.get();
    }

    @Override
    public Long RPCSubmit(Integer count) {
        return websiteCounter.addAndGet(count);
    }
    
}

Maintenant, utilisons le VisitCounter:

    VisitCounter counter = new VisitCounter();

    // just had 4 visits, yay
    counter.publish(4);
    // just had another visit, yay
    counter.publish(1);

    // get data for stats counter
    System.out.println(counter.consume()); // prints 5

    // show data for stats counter page, but include that as a page view
    System.out.println(counter.RPCSubmit(1)); // prints 6

Lorsque vous implémentez plusieurs interfaces, vous ne pouvez pas implémenter la même interface deux fois. Cela s'applique également aux interfaces génériques. Ainsi, le code suivant n'est pas valide et entraînera une erreur de compilation:

interface Printer<T> {
    void print(T value);
}

// Invalid!
class SystemPrinter implements Printer<Double>, Printer<Integer> {
    @Override public void print(Double d){ System.out.println("Decimal: " + d); }
    @Override public void print(Integer i){ System.out.println("Discrete: " + i); }
}

Utilité des interfaces

Les interfaces peuvent être extrêmement utiles dans de nombreux cas. Par exemple, disons que vous avez une liste d’animaux et que vous souhaitez parcourir la liste en imprimant chacun le son qu’ils font.

{cat, dog, bird}

Une façon d'y parvenir serait d'utiliser des interfaces. Cela permettrait d'appeler la même méthode sur toutes les classes

public interface Animal {
    public String getSound();
}

Toute classe qui implements Animal doit également avoir une méthode getSound() , mais toutes peuvent avoir des implémentations différentes

public class Dog implements Animal {
    public String getSound() {
        return "Woof";
    }
}

public class Cat implements Animal {
    public String getSound() {
        return "Meow";
    }
}

public class Bird implements Animal{
    public String getSound() {
        return "Chirp";
    }
}

Nous avons maintenant trois classes différentes, chacune ayant une méthode getSound() . Comme toutes ces classes implement l'interface Animal , qui déclare la méthode getSound() , toute instance d'un Animal peut être appelée sur getSound()

Animal dog = new Dog();
Animal cat = new Cat();
Animal bird = new Bird();

dog.getSound(); // "Woof"
cat.getSound(); // "Meow"
bird.getSound(); // "Chirp"

Parce que chacun de ces Animal est un Animal , nous pourrions même mettre les animaux dans une liste, les parcourir en boucle et imprimer leurs sons.

Animal[] animals = { new Dog(), new Cat(), new Bird() };
for (Animal animal : animals) {
    System.out.println(animal.getSound());
}

Comme l'ordre du tableau est Dog , Cat , puis Bird , "Woof Meow Chirp" sera imprimé sur la console.

Les interfaces peuvent également être utilisées comme valeur de retour pour les fonctions. Par exemple, retourner un Dog si l'entrée est "chien" , Cat si l'entrée est "chat" , et Bird si c'est "oiseau" , et imprimer le son de cet animal pourrait être fait en utilisant

public Animal getAnimalByName(String name) {
    switch(name.toLowerCase()) {
        case "dog":
            return new Dog();
        case "cat":
            return new Cat();
        case "bird":
            return new Bird();
        default:
            return null;
    }
}

public String getAnimalSoundByName(String name){
    Animal animal = getAnimalByName(name);
    if (animal == null) {
        return null;
    } else { 
        return animal.getSound();
    }
}

String dogSound = getAnimalSoundByName("dog"); // "Woof"
String catSound = getAnimalSoundByName("cat"); // "Meow"
String birdSound = getAnimalSoundByName("bird"); // "Chirp"
String lightbulbSound = getAnimalSoundByName("lightbulb"); // null

Les interfaces sont également utiles pour l'extensibilité, car si vous souhaitez ajouter un nouveau type d' Animal , vous n'avez rien à changer avec les opérations que vous effectuez.

Implémentation d'interfaces dans une classe abstraite

Une méthode définie dans une interface est par défaut public abstract . Lorsqu'une abstract class implémente une interface , toutes les méthodes définies dans l' interface ne doivent pas être implémentées par la abstract class . En effet, une class déclarée abstract peut contenir des déclarations de méthode abstraites. Il incombe donc à la première sous-classe concrète d’implémenter toutes abstract méthodes abstract héritées des interfaces et / ou de la abstract class .

public interface NoiseMaker {
    void makeNoise();
}

public abstract class Animal implements NoiseMaker {
    //Does not need to declare or implement makeNoise()
    public abstract void eat();
}

//Because Dog is concrete, it must define both makeNoise() and eat()
public class Dog extends Animal {
    @Override
    public void makeNoise() {
        System.out.println("Borf borf");
    }

    @Override
    public void eat() {
        System.out.println("Dog eats some kibble.");
    }
}

A partir de Java 8 en avant , il est possible pour une interface de déclarer par default les implémentations de méthodes qui signifie que la méthode ne sera pas abstract , donc des sous-classes concrètes ne seront pas contraints de mettre en œuvre la méthode , mais héritera de la default la mise en œuvre , sauf contrordre.

Méthodes par défaut

Introduites dans Java 8, les méthodes par défaut permettent de spécifier une implémentation dans une interface. Cela pourrait être utilisé pour éviter la classe typique "Base" ou "Abstract" en fournissant une implémentation partielle d'une interface et en limitant la hiérarchie des sous-classes.

Implémentation du modèle d'observateur

Par exemple, il est possible d'implémenter le modèle Observer-Listener directement dans l'interface, offrant plus de flexibilité aux classes d'implémentation.

interface Observer {
    void onAction(String a);
}

interface Observable{
    public abstract List<Observer> getObservers();

    public default void addObserver(Observer o){
        getObservers().add(o);
    }

    public default void notify(String something ){
        for( Observer l : getObservers() ){
            l.onAction(something);
        }
    }
}

Maintenant, n'importe quelle classe peut être rendue "Observable" simplement en implémentant l'interface Observable, tout en étant libre de faire partie d'une hiérarchie de classes différente.

abstract class Worker{
    public abstract void work();
}

public class MyWorker extends Worker implements Observable {

    private List<Observer> myObservers = new ArrayList<Observer>();
    
    @Override
    public List<Observer> getObservers() {
        return myObservers;
    }

    @Override
    public void work(){
        notify("Started work");

        // Code goes here...

        notify("Completed work");
    }
    
    public static void main(String[] args) {    
        MyWorker w = new MyWorker();
       
        w.addListener(new Observer() {
            @Override
            public void onAction(String a) {
                System.out.println(a + " (" + new Date() + ")");
            }
        });
        
        w.work();
    }
}

Problème de diamant

Le compilateur Java 8 est conscient du problème de diamant qui se produit lorsqu'une classe implémente des interfaces contenant une méthode avec la même signature.

Pour le résoudre, une classe d'implémentation doit remplacer la méthode partagée et fournir sa propre implémentation.

interface InterfaceA {
    public default String getName(){
        return "a";
    }
}

interface InterfaceB {
    public default String getName(){
        return "b";
    }
}

public class ImpClass implements InterfaceA, InterfaceB {

    @Override
    public String getName() {    
        //Must provide its own implementation
        return InterfaceA.super.getName() + InterfaceB.super.getName();
    }
    
    public static void main(String[] args) {    
        ImpClass c = new ImpClass();
        
        System.out.println( c.getName() );                   // Prints "ab"
        System.out.println( ((InterfaceA)c).getName() );     // Prints "ab"
        System.out.println( ((InterfaceB)c).getName() );     // Prints "ab"
    }
}

Il y a toujours le problème d'avoir des méthodes avec le même nom et les mêmes paramètres avec des types de retour différents, qui ne compileront pas.

Utiliser les méthodes par défaut pour résoudre les problèmes de compatibilité

Les implémentations de méthodes par défaut sont très pratiques si une méthode est ajoutée à une interface dans un système existant où les interfaces sont utilisées par plusieurs classes.

Pour éviter de casser le système entier, vous pouvez fournir une implémentation de méthode par défaut lorsque vous ajoutez une méthode à une interface. De cette façon, le système continuera à compiler et les implémentations réelles pourront être effectuées étape par étape.


Pour plus d'informations, voir la rubrique Méthodes par défaut .

Modificateurs dans les interfaces

Le Oracle Java Style Guide indique:

Les modificateurs ne doivent pas être écrits lorsqu'ils sont implicites.

(Voir Modifiers dans Oracle Official Code Standard pour le contexte et un lien vers le document Oracle réel.)

Ce guide de style s'applique particulièrement aux interfaces. Considérons l'extrait de code suivant:

interface I {
    public static final int VARIABLE = 0;

    public abstract void method();

    public static void staticMethod() { ... }
    public default void defaultMethod() { ... }
}

Les variables

Toutes les variables d'interface sont des constantes implicites avec des modificateurs implicites public (accessibles pour tous), static (accessibles par nom d'interface) et final (doivent être initialisées pendant la déclaration):

public static final int VARIABLE = 0;

Les méthodes

  1. Toutes les méthodes qui ne fournissent pas d'implémentation sont implicitement public et abstract .
public abstract void method();
Java SE 8
  1. Toutes les méthodes avec static modificateur static ou default doivent fournir une implémentation et sont implicitement public .
public static void staticMethod() { ... }

Une fois que toutes les modifications ci-dessus auront été appliquées, nous obtiendrons ce qui suit:

interface I {
    int VARIABLE = 0;
    
    void method();

    static void staticMethod() { ... }
    default void defaultMethod() { ... }
}

Renforcer les paramètres de type borné

Les paramètres de type lié vous permettent de définir des restrictions sur les arguments de type générique:

class SomeClass {

}

class Demo<T extends SomeClass> {

}

Mais un paramètre de type ne peut être lié qu'à un seul type de classe.

Un type d'interface peut être lié à un type qui a déjà une liaison. Ceci est réalisé en utilisant le symbole & :

interface SomeInterface {

}

class GenericClass<T extends SomeClass & SomeInterface> {

}

Cela renforce la liaison, nécessitant potentiellement des arguments de type à dériver de plusieurs types.

Plusieurs types d'interface peuvent être liés à un paramètre de type:

class Demo<T extends SomeClass & FirstInterface & SecondInterface> {

}

Mais devrait être utilisé avec prudence. Les liaisons d'interface multiples sont généralement un signe d' odeur de code , ce qui suggère qu'un nouveau type doit être créé, qui agit comme un adaptateur pour les autres types:

interface NewInterface extends FirstInterface, SecondInterface {

}

class Demo<T extends SomeClass & NewInterface> {

}


Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow