Interfețe (interface, implements) în Java

Moștenire, interfețe și polimorfism Java 1.0 (metode default și static: Java 8, metode private: Java 9)

O interfață (interface) descrie ce metode trebuie să aibă o clasă; clasa o promite cu implements și poate implementa mai multe interfețe.

Sintaxă
interface Plata {
    double suma();                  // abstractă: trebuie implementată

    default String descriere() {    // are implementare
        return "plată de " + suma() + " lei";
    }
}

class PlataCard implements Plata, Comparable<PlataCard> { ... }

Ce face Interfețe (interface, implements)

O interfață este un contract: o listă de metode, de obicei fără implementare. O clasă care scrie implements Plata trebuie să ofere toate metodele abstracte ale interfeței, altfel nu compilează (sau trebuie declarată abstract). Metodele din interfață sunt implicit public.

O clasă poate implementa oricâte interfețe (implements A, B, C), deși poate extinde o singură clasă. Din Java 8, interfețele pot avea metode default (cu implementare, moștenite de clase) și metode static; din Java 9 și metode private ajutătoare. Câmpurile dintr-o interfață sunt automat public static final (constante).

Interfața este și un tip: o variabilă Plata p poate ține orice obiect care o implementează. Biblioteca standard folosește peste tot interfețe: List, Comparable, Runnable, AutoCloseable. O interfață cu o singură metodă abstractă este funcțională și poate primi o lambda.

Exemple

Exemplul 1: O interfață, mai multe implementări

Plata cere metoda suma() și oferă gratuit descriere(). PlataRate își rescrie descrierea, iar codul din main lucrează doar cu tipul Plata.

interface Plata {
    double suma();

    default String descriere() {
        return getClass().getSimpleName() + ": " + suma() + " lei";
    }
}

class PlataCard implements Plata {
    final double valoare;
    PlataCard(double valoare) { this.valoare = valoare; }

    @Override
    public double suma() { return valoare; }
}

class PlataRate implements Plata {
    final double rata;
    final int luni;
    PlataRate(double rata, int luni) { this.rata = rata; this.luni = luni; }

    @Override
    public double suma() { return rata * luni; }

    @Override
    public String descriere() {
        return "Rate: " + luni + " x " + rata + " = " + suma() + " lei";
    }
}

public class Main {
    public static void main(String[] args) {
        Plata[] plati = {new PlataCard(249.99), new PlataRate(150, 12)};
        double total = 0;
        for (Plata p : plati) {
            System.out.println(p.descriere());
            total += p.suma();
        }
        System.out.println("Total: " + total + " lei");
    }
}

Rezultat

PlataCard: 249.99 lei
Rate: 12 x 150.0 = 1800.0 lei
Total: 2049.99 lei

Exemplul 2: O clasă care implementează două interfețe

Termostat este în același timp Pornibil și Masurabil. Constanta din interfață este static final, iar metoda static se apelează pe numele interfeței.

interface Pornibil {
    void porneste();
    void opreste();
}

interface Masurabil {
    double PRAG = 18.0;       // implicit public static final

    double valoare();

    static String unitate() {
        return "°C";
    }
}

class Termostat implements Pornibil, Masurabil {
    private boolean pornit;
    private final double temperatura;

    Termostat(double temperatura) { this.temperatura = temperatura; }

    @Override public void porneste() { pornit = true; }
    @Override public void opreste() { pornit = false; }
    @Override public double valoare() { return temperatura; }

    boolean pornit() { return pornit; }
}

public class Main {
    public static void main(String[] args) {
        Termostat t = new Termostat(16.5);
        if (t.valoare() < Masurabil.PRAG) {
            t.porneste();
        }
        System.out.println("temperatură: " + t.valoare() + Masurabil.unitate());
        System.out.println("centrala pornită: " + t.pornit());

        Pornibil p = t;
        p.opreste();
        System.out.println("după oprire: " + t.pornit());
    }
}

Rezultat

temperatură: 16.5°C
centrala pornită: true
după oprire: false

Exemplul 3: Interfețe din biblioteca standard: Comparable

Implementând Comparable<Produs>, produsele capătă o ordine naturală (după preț), iar Arrays.sort știe să le sorteze.

import java.util.Arrays;

class Produs implements Comparable<Produs> {
    final String nume;
    final double pret;

    Produs(String nume, double pret) {
        this.nume = nume;
        this.pret = pret;
    }

    @Override
    public int compareTo(Produs altul) {
        return Double.compare(pret, altul.pret);
    }

    @Override
    public String toString() {
        return nume + " (" + pret + " lei)";
    }
}

public class Main {
    public static void main(String[] args) {
        Produs[] cos = {
            new Produs("cafea", 39.9),
            new Produs("zahăr", 6.5),
            new Produs("ceai", 12.0),
        };
        Arrays.sort(cos);
        System.out.println(Arrays.toString(cos));
    }
}

Rezultat

[zahăr (6.5 lei), ceai (12.0 lei), cafea (39.9 lei)]

Greșeli frecvente

  • Implementezi metoda din interfață fără public: attempting to assign weaker access privileges; was public. Metodele de interfață sunt publice, deci și implementările trebuie să fie.
  • Uiți o metodă abstractă: Termostat is not abstract and does not override abstract method valoare() in Masurabil.
  • Scrii extends în loc de implements pentru o interfață în declarația unei clase (sau invers). Clasele implementează interfețe; o interfață extinde alte interfețe.

Comenzi înrudite

  • Clase abstracte (abstract): O clasă abstract nu poate fi instanțiată direct; poate avea metode abstracte, pe care subclasele concrete sunt obligate să le scrie.
  • Polimorfism: Polimorfismul înseamnă că același apel (f.arie()) execută metoda clasei reale a obiectului, chiar dacă variabila are tipul superclasei.
  • Expresii lambda: O lambda este o funcție scurtă, fără nume, scrisă ca parametri -> corp, folosită oriunde se așteaptă o interfață funcțională.
  • Moștenire (extends): Cu extends o clasă moștenește câmpurile și metodele altei clase și le poate extinde sau rescrie; Java permite o singură superclasă.
  • sealed, permits și non-sealed: O clasă sau interfață sealed își alege explicit subtipurile (permits); fiecare subtip este final, sealed sau non-sealed.