Liigu peamise sisu juurde

Meetodite ülekirjutamine

Sissejuhatus

Objektorienteeritud programmeerimises on võimalik klasse üles ehitada teiste klasside põhjal läbi pärilikkuse. Alamklass pärib ülemklassi väljad ja meetodid ning võib omalt poolelt omadusi juurde lisada.

Meetodi ülekirjutamine (method overriding) on mehhanism, mis võimaldab alamklassil asendada vanemklassilt saadud meetodi oma versiooniga. Meetodi signatuur jääb samaks, kuid käitumine muutub.

See on üks OOP-i võimsamaid ideid. Erinevat tüüpi objektid saavad reageerida samale meetodikutsumisele omal viisil.

Kiire sissejuhatus pärimisse

Enne kui tutvume meetodite ülekirjutamisega lähemalt, vaatame, kuidas pärilikkus töötab. Sellest tuleb ka hilisemates artiklites täpsemalt juttu.

Javas saab klass laiendada teist klassi kasutades extends märksõna:

class Animal {
private String name;

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

public String getName() {
return name;
}

public void makeSound() {
System.out.println(name + " makes a sound");
}
}

class Dog extends Animal {

public Dog(String name) {
super(name); // calls the Animal constructor
}
}

Dog pärib Animal klassi kõik omadused, sealhulgas name välja ning getName() ja makeSound() meetodid. Kui makeSound() meetodit välja kutsuda, saame tulemuseks Rex makes a sound:

Dog dog = new Dog("Rex");
dog.makeSound(); // Rex makes a sound

super(name) rida Dog klassi konstruktoris käivitab Animal klassi konstruktori. See on vajalik, et ülemklassis olevad väljad saaksid endale väärtused korrektselt.

Meetodi ülekirjutamine

Animal klassi makeSound() meetod on liiga üldine. Antud juhul koer peaks haukuma, mitte lihtsalt mingit suvalist heli tegema. Meetodit üle kirjutades on võimalik seda saavutada ehk alamklass saab makeSound() meetodile enda implementatsiooni:

class Dog extends Animal {

public Dog(String name) {
super(name);
}

@Override // This indicates that the method is being overriden
public void makeSound() {
System.out.println(getName() + " says: Woof!");
}
}

class Cat extends Animal {

public Cat(String name) {
super(name);
}

@Override
public void makeSound() {
System.out.println(getName() + " says: Meow!");
}
}
Animal dog = new Dog("Rex");
Animal cat = new Cat("Whiskers");

dog.makeSound(); // Rex says: Woof!
cat.makeSound(); // Whiskers says: Meow!

Kuigi mõlemad muutujad on deklareeritud Animal tüübiga, sõltub käivitatav meetod objekti tegelikust tüübist. Seda nimetatakse dynamic dispatch-iks ehk Java otsustab jooksvalt, millist meetodi versiooni kutsuda.

@Override annotatsioon

Kas te märkasite, et meetodi üle kirjutamisel märgiti see meetod eraldi ära? Antud juhul siis makeSound() meetodi kohale kirjutati @Override annotatsioon. Selle annotatsiooni eesmärk on nii arendajale kui ka kompilaatorile märku anda, et antud meetod on mõeldud ülemklassist tuleneva meetodi ülekirjutamiseks.

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

Seda annotatsiooni otseselt kirjutama ei pea, kood kompileerub ja töötab ilusti ka ilma selleta. Hea tava (ning rangelt soovituslik) on ikkagi seda kasutada, kuna kompilaator on siis suuteline kindlaks tegema, et tõepoolest soovitakse midagi üle kirjutada. Lisaks aitab see siis kirjavigu vältida, näiteks:

@Override
public void makeSond() { // Typo! Compiler error: method does not override a method from its superclass
System.out.println("Woof!");
}

Ilma @Override annotatsioonita tekiks hoopis uus meetod, mitte ei kirjutata ülemklassist tulevat üle.

nõuanne

Kasuta alati @Override annotatsiooni!

Reeglid meetodite ülekirjutamisel

Meetodite ülekirjutamisele kehtivad ka kindlad reeglid.

Meetodi signatuur

Ülekirjutatav meetod peab olema sama nime, parameetrite arvu/tüüpidega (ja järjekorraga) kui ülemklassis:

class Animal {
public void makeSound() { ... }
}

class Dog extends Animal {
@Override
public void makeSound() { ... } // OK — same signature

// public void makeSound(int volume) // This is overloading, not overriding
}

Meetodite ülelaadimise kohta saate uurida siit.

Tagastustüübid

Tagastustüüp peab olema kas ülemklassi või alamklassi tüüpi (ehk kitsendus teisisõnu).

class Animal {
public Animal create() {
return new Animal("generic");
}
}

class Dog extends Animal {
@Override
public Dog create() { // OK — Dog is a subtype of Animal
return new Dog("Rex");
}
}

Nähtavus

Uuel meetodil peab olema kas sama või leebem nähtavus kui ülemklassis oleval meetodil:

class Animal {
public void makeSound() { ... }
}

class Dog extends Animal {
@Override
private void makeSound() { ... } // Error: attempting to assign weaker access
}

final meetodid

Antud teemat käsitleti final võtmesõna peatükis. final-iga tähistatud meetodeid pole võimalik üle kirjutada:

class Animal {
public final String getName() {
return name;
}
}

class Dog extends Animal {
@Override
public String getName() { ... } // Error: cannot override final method
}

Staatilised meetodid

Staatilised meetodid kuuluvad klassile, mitte objektidele, seega nende puhul ülekirjutamine ei kehti. Kui alamklassi kirjutatakse staatiline meetod sama signatuuriga, varjab see ülemklassi meetodit. Seda võtet üldiselt välditakse, kuna see tekitab koodis rohkem segadust kui selgust.

Vanemklassi meetodi laiendamine super märksõnaga

Võimalik on ka vanemklassi meetodit laiendada ehk mitte täielikult ära asendada. Selleks on võimalik kasutada super märksõna, mis võimaldab alamklassi meetodis välja kutsuda ülemklassi meetodit. Näiteks:

class Employee {
private String name;
private double baseSalary;

public Employee(String name, double baseSalary) {
this.name = name;
this.baseSalary = baseSalary;
}

public double calculatePay() {
return baseSalary;
}

public String getName() {
return name;
}

public double getBaseSalary() {
return baseSalary;
}
}

class Manager extends Employee {
private double bonus;

public Manager(String name, double baseSalary, double bonus) {
super(name, baseSalary);
this.bonus = bonus;
}

@Override
public double calculatePay() {
return super.calculatePay() + bonus; // base salary + bonus
}
}
Employee emp = new Employee("Alice", 3000);
Manager mgr = new Manager("Bob", 3000, 500);

System.out.println(emp.calculatePay()); // 3000.0
System.out.println(mgr.calculatePay()); // 3500.0

super.calculatePay() kasutab Employee calculatePay() meetodit ning selle tulemusele liidetakse Manager objektile ette määratud väärtus juurde.

Levinumad ülekirjutatavad meetodid

Iga klass Javas põhineb Object klassist. See tähendab, et igal klassil on teatud hulk meetodeid, mis tulenevad Object klassist. Nendest meetoditest kolme kirjutatakse väga tihti üle ehk nendega tasuks tutvuda. Nendeks on: toString(), equals() ja hashCode().

toString() meetod

toString() meetod tagastab sõnelise esitluse. Vaikimisi Object klassist tulenev toString() meetod tagastab sellisel kujul sõne: Student@4e50df2e - klassi nimi ja mäluviide. Arendajale see väga palju ei ütle.

toString() meetodi ülekirjutamisega on võimalik tagastada objekti kohta sisukamat informatsiooni, näiteks:

class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Student{name='" + name + "', age=" + age + "}";
}
}
Student student = new Student("Alice", 20);
System.out.println(student); // Student{name='Alice', age=20}

toString() meetodit kutsutakse välja automaatselt, kui objekt edastatakse System.out.println() meetodile. Lisaks on see kasutusel ka sõnede kombineerimisel ("Hello " + student) või kus iganes Javal on objekti vaja sõnena.

equals() meetod

Eelnevalt peaksite tuttavad olema sellega, et objektide võrdlemiseks ei saa kasutada == operaatorit. See võrdleb objektide mäluviidet, mitte sisu:

Student s1 = new Student("Alice", 20);
Student s2 = new Student("Alice", 20);

System.out.println(s1 == s2); // false — two different objects in memory

Selleks, et objektide sisu võrrelda kasutatakse equals() meetodit. Vaikimisi equals() meetod kontrollib mäluviiteid, kuid seda on võimalik asendada järgnevalt:

class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
// Same reference — must be equal
if (this == obj) return true;

// Null or different type — cannot be equal
if (obj == null || getClass() != obj.getClass()) return false;

// Cast and compare fields
Student other = (Student) obj;
return age == other.age && name.equals(other.name);
}
}

equals() puhul tuleb järgida järgnevat struktuuri:

  • Esmalt kontrollida, kas objektid on samad (this == obj).
  • Järgnevalt teha tüübikontrolli (null-i ei saa võrrelda, objektid peavad samat tüüpi olema).
  • Viimasena viia läbi tüübiteisendus ning siis kontrollida sisu järgi.

Või kompaktsemal viisil (kui kasutate Java 16+):

@Override
public boolean equals(Object obj) {
// null check and type check in one statement
// Also assigns obj to other, no casting needed
if (obj instanceof Student other) {
return age == other.age && name.equals(other.name);
}
return false;
}

Mõlema puhul on võimalik nüüd võrrelda objekte nende sisu järgi:

Student s1 = new Student("Alice", 20);
Student s2 = new Student("Alice", 20);
Student s3 = new Student("Bob", 22);

System.out.println(s1.equals(s2)); // true — same name and age
System.out.println(s1.equals(s3)); // false — different data
hoiatus

equals() meetodi parameetris on tüübiks Objekt. See on õige ning seda asendada ei tohi, muidu tekiks ülekirjutamise asemel ülelaadimine. Seetõttu on ka tüübiteisendus ja kontroll tähtis, kuna sisu kontrollimiseks on meil vaja objekti liikmetele ka ligi pääseda. Ilma selleta pääseks ainult ligi Object klassi meetoditele. Eelneva Student klass näitel tekiks siis järgnev kompileerimisviga:

Cannot resolve symbol 'name'
Cannot resolve symbol 'age'

hashCode() meetod

hashCode() meetod tagastab täisarvu, mis toimib kui objekti identifikaatorina. Seda kasutatakse andmestruktuurides nagu HashMap ja HashSet, et võimaldada objektide kiire leidmine.

Räsiväärtuse puhul on kindel reegel: kui kaks objekti on equals() meetodi järgi võrdsed, siis need peavad tagastama sama räsiväärtuse. Kui seda reeglit rikutakse, ei ole võimalik tagada räsitabelil põhinevate andmestruktuuride korrektne töö.

hashCode() meetodit on võimalik järgnevalt üle kirjutada:

class Student {
private String name;
private int age;

public Student(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null || getClass() != obj.getClass()) return false;
Student other = (Student) obj;
return age == other.age && name.equals(other.name);
}

@Override
public int hashCode() {
int result = name.hashCode();
result = 31 * result + age;
return result;
}
}

Räsiväärtuse arvutamisel kasutatakse muutumatuid väärtusi ning need peaksid ühtima sellega, mida equals() meetodis samasuse kinnitamiseks kasutatakse. See tagab, et samade tunnustega objektid tagastaks ka sama räsiväärtuse. Arv 31 on kokkulepitud reegel, mis aitab räsiväärtusi võrdselt jaotada.

Samuti võib räsiväärtust arvutada ka järgnevalt, kasutades Objects.hash() meetodit:

@Override
public int hashCode() {
return Objects.hash(name, age);
}

Antud juhul kehtib sama reegel, et arvutada väärtus välja väljade peal, mis on equals() meetodis kasutusel ning mida pole võimalik muuta.

Meetodite automaatne koostamine

Enamus koodiredaktorid on suutelised eelnevaid meetodeid automaatselt koostama, näiteks:

  • IntelliJ IDEAs vajutage paremale hiireklahvile ning sealt valige Generate ja sealt equals() and hashCode().
  • Valige väljad, mida soovite kasutada ning laske IDEl automaatselt korrektne implementatsioon koostada.