Liigu peamise sisu juurde

Liidesed

Sissejuhatus

Abstraktne klass määrab osalise lähteplaani omavahel seotud klasside perekonnale. Mõnikord ei põhine tüüpide vaheline seos aga mitte nende päritolul, vaid võimekusel.

Näiteks: Dog oskab ujuda, Duck oskab ujuda, Robot oskab ujuda. Neil ei pruugi olla muud ühist, kuid nad kõik jagavad sama võimekust, milleks on ujumine. Nende jaoks ühise abstraktse klassi deklareerimine oleks vale, sest Robot ei ole loom.

Java pakub sellise seose jaoks liideseid. Liides määrab hulga meetodeid, mida klass peab implementeerima, ilma et ta midagi ütleks selle kohta, mis see klass on või kuidas seda tegema peaks. Sisuliselt on liides leping, mida iga implementeeriv klass peab täitma.

Liidese deklareerimine

Nagu eelnevalt kirjeldatud, kasutatakse liidest võimekuse kirjeldamiseks. Liides määrab, milliseid toiminguid klass peab toetama, kuid ei määra, kuidas need toimingud on realiseeritud.

Liidese deklareerimiseks kasutatakse võtmesõna interface:

interface Swimmable {
void swim(); // implicitly public and abstract
}

See määrab lepingu: iga klass, mis liidest Swimmable implementeerib, peab pakkuma meetodi swim() teostuse. Teostuse olemasolu tagatakse kompilaatori poolelt ehk meetodi realiseerimata jätmine lõppeb kompileerimisveaga.

Kõik liidese meetodid on public ja abstract. Neid modifikaatoreid ei pea eraldi kirjutama, kuid nende lisamine on lubatud.

Liidese implementeerimine

Liidese implementeerimiseks kasutatakse implements märksõna klassi deklaratsioonis. Sellega annab klass märku, et täidab liidese lepingut ning tagab, et kõik liidese meetodid saavad realiseeritud.

class Dog implements Swimmable {
private String name;

public Dog(String name) {
this.name = name;
}

@Override
public void swim() {
System.out.println(name + " is paddling");
}

// name field getter, setter...
}

class Robot implements Swimmable {
@Override
public void swim() {
System.out.println("Robot is swimming with propellers");
}
}

Kui nendest klassidest tehtud objekte salvestada liidese tüüpi muutujasse, käsitletakse neid liideste, mitte konkreetse klassi järgi:

Swimmable a = new Dog("Rex");
Swimmable b = new Robot();

a.swim(); // Rex is paddling
b.swim(); // Robot is swimming with propellers

Muutuja deklareerimine tüübiga Swimmable tähendab, et kood eeldab ainult ujumisvõime olemasolu. Konkreetne klass (Dog, Robot või mõni muu) ei ole oluline tingimusel, kui liidese leping on täidetud. See on sama polümorfismi põhimõte, mida kasutati abstraktsete klassidega, kuid siin põhineb see ühisel võimekusel, mitte ühisel päritolul.

hoiatus

Kui objektile viidetakse liidese tüübi kaudu, on kättesaadavad ainult liideses deklareeritud meetodid.

Swimmable s = new Dog("Rex");

s.swim(); // OK
// s.getName(); // COMPILER ERROR — meetod ei kuulu liidesesse Swimmable

Kuigi tegelik objekt on Dog, käsitleb kompilaator muutujat Swimmable tüübi järgi. See tagab, et kirjutatav kood keskendub ainult mingile kindlale eesmärgile, mis tuleneb antud liidesest. Kui on vaja kasutada klassispetsiifilisi meetodeid, tuleb muutuja tüüp teisendada vastava klassi tüüpi.

Mitme liidese implementeerimine

Klass saab laiendada ainult ühte ülemklassi, kuid implementeerida mitut liidest. See võimaldab klassil omandada mitu sõltumatut võimekust ning on Java viis toetada käitumise tasemel mitmikspärimist (multi-inheritance).

interface Swimmable {
void swim();
}

interface Flyable {
void fly();
}

class Duck implements Swimmable, Flyable {
@Override
public void swim() {
System.out.println("Duck is swimming");
}

@Override
public void fly() {
System.out.println("Duck is flying");
}
}

Objekti saab kasutada nii tema konkreetse klassi kui ka iga implementeeritud liidese kaudu:

Duck duck = new Duck();
duck.swim(); // Duck is swimming
duck.fly(); // Duck is flying

// Can also be used as either interface type
Swimmable s = new Duck();
Flyable f = new Duck();

See võimaldab käsitleda objekti vastavalt vajalikule võimekusele. Näiteks meetod, mis töötab Swimmable tüübiga, saab kasutada kõiki objekte, mis oskavad ujuda, sõltumata nende konkreetsest klassist.

info

Java standardteek kasutab liideseid laialdaselt mitme võimekuse kombineerimiseks. Näiteks klass ArrayList implementeerib mitu liidest:

public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
// ...
}

See tähendab, et ArrayList objekt:

  • on kasutatav järjendina (toetab elementide lisamist, eemaldamist jne),
  • toetab kiiret juhuslikku ligipääsu (RandomAccess),
  • on kloonitav (Cloneable),
  • on serialiseeritav (Serializable - objekti saab teisendada baitide jadaks ja hiljem taastada algseks objektiks.).

Kõik need on eraldi võimekused, mis on kombineeritud liideste abil, ilma et oleks vaja mitut ülemklassi.

Liidese väljad

Lisaks meetoditele võib liides sisaldada ka välju. Need väljad ei kuulu konkreetsele objektile, vaid liidesele endale.

Kõik liideses deklareeritud väljad on automaatselt public, static ja final, mis tähendab, et need on konstandid:

interface Direction {
int NORTH = 0; // same as: public static final int NORTH = 0;
int EAST = 1;
int SOUTH = 2;
int WEST = 3;
}

Neid saab kasutada ilma liidest implementeerimata, viidates liidese nimele:

int dir = Direction.NORTH;
hoiatus

Kuna liidese väljad on alati konstandid, ei saa nende väärtust muuta. Neid ei tohi kasutada muutuva oleku jagamiseks objektide vahel.

Lisaks on liidese kasutamine konstantide hoidmiseks üldiselt halb praktika (constant interface antipattern). Liides määratleb lepingu selle kohta, mida klass suudab teha — see ei ole õige koht konstantide hoidmiseks. Konstantide jaoks kasuta enum-i või staatilist klassi:

// Preferred: enum
enum Direction { NORTH, EAST, SOUTH, WEST }

// Also fine: constants class
class Direction {
public static final int NORTH = 0;
public static final int EAST = 1;
public static final int SOUTH = 2;
public static final int WEST = 3;

private Direction() {} // prevent instantiation
}

Täielikum näide

Vaatleme joonistusrakendust, kus objektidel võivad olla erinevad võimekused, näiteks joonistamine ja suuruse muutmine. Need võimekused on modelleeritud eraldi liidestena:

interface Drawable {
void draw();
}

interface Resizable {
void resize(double factor);
}

class Circle implements Drawable, Resizable {
private double radius;

public Circle(double radius) {
this.radius = radius;
}

@Override
public void draw() {
System.out.println("Drawing circle with radius " + radius);
}

@Override
public void resize(double factor) {
radius *= factor;
}
}

class TextLabel implements Drawable {
private String text;

public TextLabel(String text) {
this.text = text;
}

@Override
public void draw() {
System.out.println("Drawing label: " + text);
}
// TextLabel is not resizable — it simply does not implement Resizable
}

Objekte saab käsitleda nende võimekuse järgi. Näiteks saab kõik joonistatavad objektid paigutada samasse kogusse:

List<Drawable> canvas = new ArrayList<>();
canvas.add(new Circle(5.0));
canvas.add(new TextLabel("Hello"));

for (Drawable d : canvas) {
d.draw();
}

// Drawing circle with radius 5.0
// Drawing label: Hello

Circle on nii Drawable kui ka Resizable, sest see implementeerib mõlemat liidest. TextLabel on ainult Drawable, sest selle suurust ei saa muuta.

Liideste kasutamine võimaldab kombineerida võimekusi paindlikult. See väldib vajadust luua kunstlikke ühiseid ülemklasse ning võimaldab käsitleda objekte nende käitumise, mitte nende tüübi järgi.

Tüüpiteisendus liidesega

Liidestega tüüpiteisendus toimib samadel põhimõtetel nagu pärilikkuse puhul. Objekt saab automaatselt määrata liidese tüüpi muutujale (upcast), kusjuures kättesaadavad on ainult selles liideses deklareeritud meetodid. Vajadusel saab teisenduse teha tagasi konkreetsemaks tüübiks, kuid see nõuab otsest teisendust ning ebaõige tüübi korral tekib käitusajal ClassCastException:

Circle circle = new Circle(5.0);

Drawable d = circle; // upcast — automatic, always safe
Resizable r = circle; // upcast to a second interface — also automatic

Circle c = (Circle) d; // explicit downcast back to concrete type
c.resize(2.0);

Resizable r2 = (Resizable) d; // cast between interface types — valid if object implements both

Täpsema selgituse ning instanceof-põhise ohutu teisenduse kohta loe tüüpiteisenduse peatükist.

Liideste laiendamine

Liidesed võivad laiendada teisi liideseid, kasutades extends märksõna. Alamliidest implementeeriv klass peab realiseerima kõik meetodid nii ülemliidesest kui ka oma liidesest:

interface Drawable {
void draw();
}

interface AnimatedDrawable extends Drawable {
void animate();
// also inherits: draw()
}

Klass, mis implementeerib AnimatedDrawable, peab realiseerima nii draw() kui ka animate() meetodid:

class Sprite implements AnimatedDrawable {
@Override
public void draw() {
System.out.println("Drawing sprite");
}

@Override
public void animate() {
System.out.println("Animating sprite");
}
}

See võimaldab liideseid hierarhiliselt kombineerida, luues järkjärgulisi võimekuste kihte.