Java Language
interfacce
Ricerca…
introduzione
interface
parola chiave. Le interfacce possono contenere solo costanti, firme dei metodi, metodi predefiniti, metodi statici e tipi nidificati. I corpi dei metodi esistono solo per metodi predefiniti e metodi statici. Come le classi astratte, le interfacce non possono essere istanziate: possono essere implementate solo da classi o estese da altre interfacce. L'interfaccia è un modo comune per ottenere la completa astrazione in Java.
Sintassi
- interfaccia pubblica Foo {void foo (); / * altri metodi * /}
- l'interfaccia pubblica Foo1 estende Foo {void bar (); / * altri metodi * /}
- classe pubblica Foo2 implementa Foo, Foo1 {/ * implementazione di Foo e Foo1 * /}
Dichiarazione e implementazione di un'interfaccia
Dichiarazione di un'interfaccia che utilizza la parola chiave interface
:
public interface Animal {
String getSound(); // Interface methods are public by default
}
Sostituisci annotazione
@Override
public String getSound() {
// Code goes here...
}
Questo costringe il compilatore a controllare che stiamo eseguendo l'override e impedisce al programma di definire un nuovo metodo o rovinare la firma del metodo.
Le interfacce sono implementate usando la parola chiave implements
.
public class Cat implements Animal {
@Override
public String getSound() {
return "meow";
}
}
public class Dog implements Animal {
@Override
public String getSound() {
return "woof";
}
}
Nell'esempio, le classi Cat
e Dog
devono definire il metodo getSound()
poiché i metodi di un'interfaccia sono intrinsecamente astratti (ad eccezione dei metodi predefiniti).
Utilizzando le interfacce
Animal cat = new Cat();
Animal dog = new Dog();
System.out.println(cat.getSound()); // prints "meow"
System.out.println(dog.getSound()); // prints "woof"
Implementazione di più interfacce
Una classe Java può implementare più interfacce.
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");
}
}
Si noti come la classe Cat
deve implementare i metodi abstract
ereditati in entrambe le interfacce. Inoltre, nota come una classe può praticamente implementare tutte le interfacce necessarie (c'è un limite di 65.535 a causa della Limitazione JVM ).
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
Nota:
- Tutte le variabili dichiarate in un'interfaccia sono
public static final
- Tutti i metodi dichiarati in un'interfaccia metodi sono
public abstract
(Questa dichiarazione è valida solo tramite Java 7. Da Java 8, è possibile avere metodi in un'interfaccia, che non devono essere astratti, tali metodi sono noti come metodi predefiniti ) - Le interfacce non possono essere dichiarate
final
- Se più di una interfaccia dichiara un metodo che ha la stessa firma, allora efficacemente viene trattato come un solo metodo e non è possibile distinguere da quale metodo di interfaccia è implementato
- Un file InterfaceName.class corrispondente verrà generato per ogni interfaccia, dopo la compilazione
Estendere un'interfaccia
Un'interfaccia può estendere un'altra interfaccia tramite la parola chiave extends
.
public interface BasicResourceService {
Resource getResource();
}
public interface ExtendedResourceService extends BasicResourceService {
void updateResource(Resource resource);
}
Ora una classe che implementa ExtendedResourceService
dovrà implementare sia getResource()
che updateResource()
.
Estensione di più interfacce
A differenza delle classi, la parola chiave extends
può essere utilizzata per estendere più interfacce (separate da virgole) consentendo combinazioni di interfacce in una nuova interfaccia
public interface BasicResourceService {
Resource getResource();
}
public interface AlternateResourceService {
Resource getAlternateResource();
}
public interface ExtendedResourceService extends BasicResourceService, AlternateResourceService {
Resource updateResource(Resource resource);
}
In questo caso, una classe che implementa ExtendedResourceService
dovrà implementare getResource()
, getAlternateResource()
e updateResource()
.
Utilizzo delle interfacce con Generics
Supponiamo che tu voglia definire un'interfaccia che consenta la pubblicazione / il consumo di dati da e verso diversi tipi di canali (ad es. AMQP, JMS, ecc.), Ma tu vuoi essere in grado di cambiare i dettagli di implementazione ...
Definiamo un'interfaccia IO di base che può essere riutilizzata in più implementazioni:
public interface IO<IncomingType, OutgoingType> {
void publish(OutgoingType data);
IncomingType consume();
IncomingType RPCSubmit(OutgoingType data);
}
Ora posso creare un'istanza di questa interfaccia, ma dal momento che non abbiamo implementazioni predefinite per questi metodi, avrà bisogno di un'implementazione quando la istanziamo:
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
Possiamo anche fare qualcosa di più utile con quell'interfaccia, diciamo che vogliamo usarlo per avvolgere alcune funzioni base di 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);
}
}
Diciamo che voglio usare questa interfaccia IO ora come un modo per contare le visite al mio sito web dal mio ultimo riavvio del sistema e quindi essere in grado di visualizzare il numero totale di visite - puoi fare qualcosa del genere:
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);
}
}
Ora usiamo il 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
Quando si implementano più interfacce, non è possibile implementare la stessa interfaccia due volte. Questo vale anche per le interfacce generiche. Pertanto, il seguente codice non è valido e genererà un errore di compilazione:
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à delle interfacce
Le interfacce possono essere estremamente utili in molti casi. Ad esempio, supponiamo di avere una lista di animali e di voler scorrere l'elenco, ognuno dei quali stampa il suono che produce.
{cat, dog, bird}
Un modo per farlo sarebbe utilizzare le interfacce. Ciò consentirebbe lo stesso metodo per essere chiamato su tutte le classi
public interface Animal {
public String getSound();
}
Qualsiasi classe che implements Animal
deve avere anche un metodo getSound()
, tuttavia possono avere implementazioni diverse
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";
}
}
Ora abbiamo tre diverse classi, ognuna delle quali ha un metodo getSound()
. Poiché tutte queste classi implement
l'interfaccia Animal
, che dichiara il metodo getSound()
, qualsiasi istanza di un Animal
può avere getSound()
chiamato su di esso
Animal dog = new Dog();
Animal cat = new Cat();
Animal bird = new Bird();
dog.getSound(); // "Woof"
cat.getSound(); // "Meow"
bird.getSound(); // "Chirp"
Poiché ognuno di questi è un Animal
, potremmo persino inserire gli animali in un elenco, scorrerli tra loro e stampare i loro suoni
Animal[] animals = { new Dog(), new Cat(), new Bird() };
for (Animal animal : animals) {
System.out.println(animal.getSound());
}
Poiché l'ordine dell'array è Dog
, Cat
e quindi Bird
, "Woof Meow Chirp" verrà stampato sulla console.
Le interfacce possono anche essere utilizzate come valore di ritorno per le funzioni. Ad esempio, restituendo un Dog
se l'input è "dog" , Cat
se l'input è "cat" e Bird
se è "bird" , e quindi si può stampare il suono di quell'animale usando
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
Le interfacce sono anche utili per l'estensibilità, perché se si desidera aggiungere un nuovo tipo di Animal
, non è necessario modificare nulla con le operazioni eseguite su di esse.
Implementazione di interfacce in una classe astratta
Un metodo definito in interface
è di default public abstract
. Quando una abstract class
implementa interface
, i metodi definiti interface
non devono essere implementati dalla abstract class
. Questo perché una class
dichiarata abstract
può contenere dichiarazioni di metodi astratti. È quindi responsabilità della prima sottoclasse concreta implementare qualsiasi metodo abstract
ereditato da qualsiasi interfaccia e / o 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.");
}
}
Da Java 8 in poi è possibile che interface
dichiari implementazioni default
di metodi, il che significa che il metodo non sarà abstract
, quindi qualsiasi sottoclasse concreta non sarà forzata ad implementare il metodo ma erediterà l'implementazione default
meno che non venga sovrascritta.
Metodi predefiniti
Introdotto in Java 8, i metodi predefiniti sono un modo per specificare un'implementazione all'interno di un'interfaccia. Questo potrebbe essere usato per evitare la tipica classe "Base" o "Abstract" fornendo un'implementazione parziale di un'interfaccia e limitando la gerarchia delle sottoclassi.
Implementazione del modello di osservatore
Ad esempio, è possibile implementare il pattern Observer-Listener direttamente nell'interfaccia, fornendo maggiore flessibilità alle classi di implementazione.
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);
}
}
}
Ora, qualsiasi classe può essere resa "Osservabile" semplicemente implementando l'interfaccia Observable, pur essendo libera di far parte di una diversa gerarchia di classi.
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();
}
}
Problema del diamante
Il compilatore in Java 8 è a conoscenza del problema dei rombi causato da una classe che implementa interfacce contenenti un metodo con la stessa firma.
Per risolverlo, una classe di implementazione deve sovrascrivere il metodo condiviso e fornire la propria implementazione.
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"
}
}
C'è ancora il problema di avere metodi con lo stesso nome e parametri con diversi tipi di ritorno, che non verranno compilati.
Utilizzare i metodi predefiniti per risolvere i problemi di compatibilità
Le implementazioni del metodo predefinito sono molto utili se un metodo viene aggiunto a un'interfaccia in un sistema esistente in cui le interfacce vengono utilizzate da più classi.
Per evitare di suddividere l'intero sistema, è possibile fornire un'implementazione di metodo predefinita quando si aggiunge un metodo a un'interfaccia. In questo modo, il sistema sarà ancora compilato e le implementazioni effettive possono essere fatte passo dopo passo.
Per ulteriori informazioni, vedere l'argomento Metodi predefiniti .
Modificatori nelle interfacce
L'Oracle Java Style Guide afferma:
I modificatori non dovrebbero essere scritti quando sono impliciti.
(Vedi Modificatori in Oracle Official Code Standard per il contesto e un link al documento Oracle effettivo.)
Questo orientamento di stile si applica in particolare alle interfacce. Consideriamo il seguente frammento di codice:
interface I {
public static final int VARIABLE = 0;
public abstract void method();
public static void staticMethod() { ... }
public default void defaultMethod() { ... }
}
variabili
Tutte le variabili di interfaccia sono costanti implicite con modificatori public
impliciti (accessibili a tutti), static
(accessibili tramite nome interfaccia) e final
(devono essere inizializzati durante la dichiarazione):
public static final int VARIABLE = 0;
metodi
- Tutti i metodi che non prevedono l'implementazione sono implicitamente
public
eabstract
.
public abstract void method();
- Tutti i metodi con modificatore
static
odefault
devono fornire l'implementazione e sono implicitamentepublic
.
public static void staticMethod() { ... }
Dopo aver applicato tutte le modifiche precedenti, otterremo quanto segue:
interface I {
int VARIABLE = 0;
void method();
static void staticMethod() { ... }
default void defaultMethod() { ... }
}
Rafforza i parametri del tipo limitato
I parametri di tipo limitato consentono di impostare restrizioni sugli argomenti di tipo generico:
class SomeClass {
}
class Demo<T extends SomeClass> {
}
Ma un parametro di tipo può collegarsi solo a un singolo tipo di classe.
Un tipo di interfaccia può essere associato a un tipo che ha già un legame. Questo si ottiene usando il simbolo &
:
interface SomeInterface {
}
class GenericClass<T extends SomeClass & SomeInterface> {
}
Ciò rafforza il legame, potenzialmente richiedendo argomenti di tipo per derivare da più tipi.
Più tipi di interfaccia possono essere associati a un parametro di tipo:
class Demo<T extends SomeClass & FirstInterface & SecondInterface> {
}
Ma dovrebbe essere usato con cautela. I collegamenti di interfacce multiple sono di solito un segno di odore di codice , suggerendo che dovrebbe essere creato un nuovo tipo che funge da adattatore per gli altri tipi:
interface NewInterface extends FirstInterface, SecondInterface {
}
class Demo<T extends SomeClass & NewInterface> {
}