Liigu peamise sisu juurde

Abstraktne klass vs liides

Sissejuhatus

Nii abstraktsed klassid kui ka liidesed on Java keeles abstraktsiooni vahendid. Mõlemad võimaldavad kirjeldada lepingut, mida teised klassid peavad järgima. Sobiva lahenduse valik sõltub eelkõige sellest, millist suhet soovitakse tüüpide vahel modelleerida ning kas tegemist on olemusliku hierarhia või käitumusliku võimekusega.

Peamised erinevused

Abstraktne klassLiides
Märksõnaabstract classinterface
Märksõna millega koos kasutatakseextendsimplements
Võib sisaldada väljuJah (mis tahes nähtavusega)Ainult public static final konstandid
Võib sisaldada konstruktoreidJahEi
Võib sisaldada konkreetseid meetodeidJahAinult default või static kaudu
MitmikpärimineEi (ainult üks extends)Jah (klass võib implements mitut)
Tüübi tähendusKirjeldab, mida miski onKirjeldab, mida miski suudab teha

Millal kasutada abstraktset klassi

Kasuta abstraktset klassi, kui:

  • Alamklassid jagavad ühist identiteeti, tegemist on selge IS-A seosega.
  • Soovid jagada alamklasside vahel ühist olekut (välju) või baaskäitumist (konkreetseid meetodeid).
  • Vajad konstruktorit, et tagada ühtne initsialiseerimisloogika kõigis alamklassides.
// Animal is the right model here — Dog and Cat share state and behaviour
abstract class Animal {
private String name;

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

public String getName() { return name; }

public abstract void makeSound();
}

class Dog extends Animal {
public Dog(String name) { super(name); }

@Override
public void makeSound() { System.out.println(getName() + " says: Woof!"); }
}

Millal kasutada liidest

Kasuta liidest, kui:

  • Kirjeldad võimekust või rolli, mis võib kehtida omavahel mitteseotud klasside vahel.
  • Klass peab täitma rohkem kui ühte lepingut.
  • Jagatud olekut pole vaja, oluline on ainult käitumise kirjeldamine.
// Comparable capability — a Dog, a Temperature, and an Invoice can all be comparable
// They share nothing else
interface Comparable<T> {
int compareTo(T other);
}

Dog, Temperature või Invoice võivad kõik olla võrreldavad, kuid nad ei jaga ühist identiteeti ega olekut. Neid seob ainult võimekus võrrelda.

Liidesed võimaldavad ka mitme rolli kombineerimist ühes klassis:

// Multiple capabilities on one class
class Document implements Printable, Saveable, Searchable {
// ...
}

Siin ei ole tegemist pärilushierarhiaga, vaid erinevate võimekuste koondamisega ühte tüüpi.

Kumba siis täpselt kasutada?

Otsustamisel küsi endalt kaks küsimust:

  1. Kas tüüpide vahel on sisuline IS-A seos? Kui üks klass on olemuslikult teise erijuht, eelista abstraktset klassi. Kui seos kirjeldab pigem võimekust (“suudab teha X operatsiooni”), eelista liidest.

  2. Kas jagatud käitumine eeldab jagatud olekut (välju)? Kui jah, on vajalik abstraktne klass. Liidesed ei saa sisaldada isendivälju.

// Abstract class — shared state (balance, owner), shared logic (deposit validation)
abstract class Account {
protected double balance;

public void deposit(double amount) {
if (amount <= 0) throw new IllegalArgumentException("Amount must be positive");
balance += amount;
}

public abstract void withdraw(double amount);
}

// Interface — capability with no shared state
interface Transferable {
void transferTo(Account target, double amount);
}

class SavingsAccount extends Account implements Transferable {
@Override
public void withdraw(double amount) { ... }

@Override
public void transferTo(Account target, double amount) { ... }
}

Antud näites:

  • Account määrab ühise oleku ja baaskäitumise.
  • Transferable kirjeldab eraldiseisvat võimekust, mida erinevad kontotüübid võivad realiseerida.

Mõlema kombineerimine

Abstraktsed klassid ja liidesed täiendavad üksteist. Levinud muster on määratleda avalik leping liidesena ja pakkuda osa-implementatsiooni abstraktse klassi kaudu. See aitab hoida avaliku API selge ja stabiilsena. Väline kood sõltub ainult liidesest ning võimaldab samal ajal jagada ühiseid implementatsioonidetaile (baaskäitumist ja/või olekut) ilma neid avalikku lepingusse toomata.

// Public contract — anyone can implement this
interface Shape {
double area();
double perimeter();
String getColor();
}

// Partial implementation — handles color so subclasses don't have to
abstract class AbstractShape implements Shape {
private String color;

public AbstractShape(String color) {
this.color = color;
}

@Override
public String getColor() { return color; }

// area() and perimeter() are still left to concrete subclasses
}

class Circle extends AbstractShape {
private double radius;

public Circle(String color, double radius) {
super(color);
this.radius = radius;
}

@Override
public double area() { return Math.PI * radius * radius; }

@Override
public double perimeter() { return 2 * Math.PI * radius; }
}

Kood, mis töötab Shape-ga, näeb ainult liidest ning ei tea ega hooli AbstractShape-st ega Circle-st. Abstraktne klass on implementatsiooni detail.

double totalArea(List<Shape> shapes) {
return shapes.stream()
.mapToDouble(Shape::area)
.sum();
}

List<Shape> shapes = List.of(
new Circle("red", 1.0),
new Rectangle("blue", 2.0, 3.0)
);
System.out.println(totalArea(shapes));
nõuanne

Paljud Java standardteegi kollektsioonid järgivad seda mustrit. List on liides, AbstractList pakub osalist implementatsiooni, ArrayList ja LinkedList on konkreetsed klassid.