2.1. Klašu projektēšana

Ievads

Mērķi

  • Definēt jēdzienus mantošana (inheritance), polimorfisms (polymorphism), pārslogošana (overloading), pārdefinēšana (overriding) un virtuālo metožu izsaukšana (virtual method invocation)
  • Lietot piekļuves modifikatorus - protected un noklusēto jeb pakotņu modifikatoru
  • Aprakstīt konstruktoru un metožu pārslogošanu
  • Aprakstīt objekta konstruēšanas un inicializācijas operāciju

Javas programmā atpazīt šādas lietas:

  • Pārslogotas metodes un konstruktorus
  • this izmantošanu pārslogoto konstruktoru izsaukšanai
  • Pārdefinētas metodes
  • Virsklašu metožu izsaukšanu
  • Vecāka klases konstruktorus un to izsaukšanu
  • Kā Javas programmēšanā atbalsta objektu mantošanu?

Vienkāršā mantošana

Piemērs bez mantošanas

Klases 'Employee' diagramma
public class Employee {
    public String name = ""; 
    public double salary; 
    public Date birthDate;
    public String getDetails() { ... }
}
Klases 'Manager' diagramma
public class Manager {
    public String name = ""; 
    public double salary;
    public Date birthDate;
    public String department;
    public String getDetails() { ... }
}

Piemērs ar mantošanu

Klašu mantošanas diagramma
public class Employee {
    public String name = ""; 
    public double salary;
    public Date birthDate;
    public String getDetails() { ... }
}

public class Manager extends Employee {
    public String department;
}

Vienkāršā mantošana

  • Kad klase manto tikai no vienas klases, to sauc par vienkāršo mantošanu (single inheritance)
  • Interfeisi (interface) var nodrošināt daudzkāršās mantošanas priekšrocības bez tai raksturīgajiem trūkumiem.
  • Javas klases sintakse:

    modifier class name [extends superclass] declarations*

Mantošanas hierarhija

Klašu mantošanas koks

Piekļuves vadība

Mainīgie un metodes var atrasties vienā no 4 piekļuves līmeņiem: public, protected, noklusētajā (pakotnes) un private. Klases var būt vienīgi public vai noklusētajā (pakotnes) līmenī.

protected līmenis dod mainīgajam vai metode plašāku piekļuvi, nekā noklusētais līmenis. Abos gadījumos mainīgos vai metodes var izmantot tajā pašā pakotnē (package friendly). Toties protected, atšķirībā no noklusētā līmeņa, ļauj piekļuvi arī visiem, kuri no šīs klases manto - pat ja tie ir citā pakotnē.

Sk. visu 4 modifikatoru piekļuves ierobežojumu apkopojumu tabulā:

Piekļuves modificētājsPati klaseŠīs klases pakotneApakšklaseCitas klases
private
default
protected
public

Metožu pārdefinēšana (method overriding)

  • Apakšklase var mainīt uzvedību, kas mantota no vecāku klases
  • Apakšklase var veidot metodi ar citu funkcionalitāti, nekā mantotajai metodei, bet ar to pašu
    • Vārdu
    • Atgriešanās tipu
    • Argumentu sarakstu

Ievērojiet, ka ir iespējamas arī metodes ar to pašu vārdu un atšķirīgiem argumentu sarakstiem - tas nozīmē, ka šīm metodēm ir kopīgs vienīgi pārslogots (overloaded) nosaukums, bet tās ir citādi nesaistītas metodes. Ar objektorientāciju nesaistītajai metodes vārda pārslogošanai nav sakara ar objektorientētajai programmēšanai raksturīgo metodes pārdefinēšanu, kas - mantotās metodes uzvedības izmaiņu bērnu klasē).

Metožu pārdefinēšana

public class Employee {
protected String name;
protected double salary;
protected Date birthDate;

public String getDetails() {
    return "Name: " + name + "\n" +
        "Salary: " + salary;
    }
}


public class Manager extends Employee {
    protected String department;

    public String getDetails() {
        return "Name: " + name + "\n" +
            "Salary: " + salary + "\n" +
            "Manager of: " + department;
    }
}

Atslēgvārds super

  • super lieto klase, lai vērstos pie vecāku klases
  • super izmanto, lai vērstos pie vecāku klases locekļiem - gan datu atribūtiem, gan metodēm.
  • Izsauktajai uzvedībai nav jābūt implementētai vecāku klasē, tā var būt vēl augstāk klašu hierarhijā.
public class Employee {
    private String name;
    private double salary;
    private Date birthDate;

    public String getDetails() {
        return "Name: " + name + "\nSalary: " + salary;
    }
}

public class Manager extends Employee {
    private String department;

    public String getDetails() {
        // call parent method
        return super.getDetails() +                  
            "\nDepartment: " + department;
    }
}

Polimorfisms

  • Polimorfisms (polymorphism) ir Javas programmas mainīgo, metožu parametru, parametrizēto datu tipu (generics) utml. spēja pieņemt dažādas formas objektu instances.
  • Tomēr programmas izpildes laikā nekādas "daudzformības" nav - jebkuram uzkonstruētam objektam ir tikai viena, konkrēta forma un konkrēts izpildes laika tips.
  • References tipa mainīgais toties var attiekties uz dažādu formu (t.i. tipu) objektiem.

Sacīdami, ka pārvaldnieks ir darbinieks, t.i. Manager ir (IS AN) Employee, nozīmē, ka Manager'am piemīt visi tie paši atribūti un metodes, kas vecāku klasei. Tātad, katra operācija, kura ir atļauta uz Employee objekta ir atļauta arī uz Manager objekta.

Varētu šķist, ka radīt Manager'u un piešķirt tā referenci Employee'a tipa mainīgajam ir nemērķtiecīgi. Tomēr tas ir iespējams un ir iemesli, kāpēc to dara.

Employee employee = new Manager(); // pareizi

// neatļauts mēģinājums piešķirt "Manager" atribūtam vērtību
employee.department = "Sales";
// mainīgā tips ir "Employee", tāpēc šī darbība ir liegta pat tad,
// ja konkrētajam objektam šis atribūts piemīt

Virtuālo metožu izsaukumi

Virtuālās metodes izsaukuma piemērs:

Employee e = new Manager();
e.getDetails();

Vajadzīga tipu pārbaude gan kompilācijas laikā, gan izpildes laikā

Valodā C++ šādu uzvedību nodrošināja tikai tad, ja metode tika deklarēta kā "virtual". Tas ļauj reizēm ietaupīt resursus, bet pat daļēja atteikšanās no pārdefinēto metožu virtuālās uzvedības nav raksturīga objektorientācijas paradigmai.

Prasības metožu pārdefinēšanā

  • Pārdefinējošai metodei jābūt ar to pašu atgriešanās tipu, kurš piemīt pārdefinētajai metodei
  • Piekļuves līmenis nevar būt mazāk pieejams kā metodei, kuru pārdefinējam (piemēram, protected metodi var pārdefinēt ar public metodi, bet nevar pārdefinēt ar private metodi).

Šajā nepareizajā piemērā ir attēlotas absurdības, kuras varētu rasties, ja piekļuves tipu varētu sašaurināt.

public class Parent {
    public void doSomething() {}
}

public class Child extends Parent {
    private void doSomething() {}
}

public class UseBoth {
    public void doOtherThing() {
        Parent p1 = new Parent();
        Parent p2 = new Child(); 
        p1.doSomething();
        p2.doSomething();
    }
}

Heterogēnas kolekcijas

Vienāda tipu objektu kolekcijas sauc par homogēnām kolekcijām:

MyDate[]  dates = new MyDate[2];
dates[0] = new MyDate(22, 12, 1964);
dates[1] = new MyDate(22, 7, 1964);

Dažādu tipu objektu kolekcijas sauc par heterogēnām kolekcijām:

Employee [] staff = new Employee[1024];
staff[0] = new Manager();
staff[1] = new Employee();
staff[2] = new Engineer();

Polimorfi argumenti

Tā kā Manager ir Employee:

// metode Employee klasē
public TaxRate findTaxRate(Employee e) {
}

// Izsaukums varbūt kādā citā aplikācijas klasē
Manager m = new Manager();
TaxRate t = findTaxRate(m);

Operators instanceof

public class Employee extends Object
public class Manager extends Employee
public class Engineer extends Employee
----------------------------------------

public void doSomething(Employee e) {
     if (e instanceof Manager) {
// Apstrādā Manager'u
     } else if (e instanceof Engineer) {
          // Apstrādā Engineer'u
     } else {
          // Apstrādā citus Employee tipus
     }
}

Šāda tipa kods ir riskants, ja tas neņem vērā situācijas, kur objektu mantošanas hierarhija laika gaitā varētu paplašināties. Tādēļ labāk ir pēc iespējas izmantot standarto metožu pārdefinēšanas mehānismu, nevis vienā metodē šķirot gadījumus ar instanceof operatoru.

Objektu tipu pārveidojumi

  • Lietot instanceof, lai pārbaudītu objekta atbilstību dotajam tipam
  • Ar atklātu tipa pārveidojumu atjaunot pilnu objekta funkcionalitāti
  • Tipu pārveidojumi notiek saskaņā ar šādām vadlīnijām:
  • Pārveidojumi virzienā uz tipu hierarhijas augšu var būt apslēpti (implicit).
  • Pārveidojumos uz leju vienmēr pārveido uz apakšklasi - to pārbauda kompilators
  • Pārveidojamā objekta tipu vēlreiz pārbauda izpildes laikā un tad, ja pārveidojumu veikt nav iespējams, rodas izpildes laika kļūdas.

Metožu vārdu pārslogošana

Lietot sekojoši:

public void println(int i)
public void println(float f)
public void println(String s)
  • Metožu argumentu sarakstiem jābūt atšķirīgiem.
  • Atgriešanās tipi var atšķirties.

Argumentu sarakstiem parasti būtu jābūt "pietiekami" dažādiem, lai nerastos pārpratumi saistībā ar noklusētajiem, paplašinošajiem tipu pārveidojumiem, piemēram, no float uz double, jo tie var programmētājam radīt sajukumu.

Konstruktoru pārslogošana

Konstruktorus tāpat kā metodes var pārslogot. Piemērs:

public Employee(String name, double salary, Date DoB) { ... }

public Employee(String name, double salary) { ... }

public Employee(String name, Date DoB) { ... }
  • Argumentu sarakstiem jābūt dažādiem.
  • Var lietot this referenci konstruktora pirmajā rindiņā, lai izsauktu citu konstruktoru.
1   public class Employee {
2     private static final double BASE_SALARY = 15000.00;
3     private String name;
4     private double salary;
5     private Date   birthDate;
6  
7     public Employee(String name, double salary, Date DoB) {
8       this.name = name;
9       this.salary = salary;
10      this.birthDate = DoB;
11    }
12    public Employee(String name, double salary) {
13      this(name, salary, null);
14    }
15    public Employee(String name, Date DoB) {
16      this(name, BASE_SALARY, DoB);
17    }
18    public Employee(String name) {
19      this(name, BASE_SALARY);
20    }
21    // more Employee code...
22  }

Konstruktori netiek mantoti

  • Apakšklase manto visas metodes un mainīgos no vecāku klases.
  • Apakšklase nemanto vecāku klases konstruktorus.
  • Divi veidi, kā iekļaut klasē konstruktoru ir:
  • Lietot noklusēto konstruktoru
  • Atklāti uzrakstīt vienu vai vairākus konstruktorus

Vecāku klases konstruktoru izsaukšana

  • Lai izsauktu vecāku klases konstruktoru, pirmajā konstruktora rindiņā jāraksta super izsaukums.
  • Kurš vecāku klases konstruktors tiek izsaukts, nosaka argumenti super izsaukumā
  • Ja konstruktora pirmajā rindiņā nav ne this ne super izsaukuma, tad kompilators pievieno apslēptu izsaukumu: super(). Tas izsauc vecāku klasē bezargumentu konstruktoru (kurš varētu būt arī "noklusētais" konstruktors).
  • Ja kādas klases vecāku klasei ir sadefinēti konstruktori, bet nav bezargumentu konstruktora, kurš ir nepieciešams noklusētajam super() izsaukumam, tad rodas kompilācijas kļūda.

Vecāku klases konstruktoru izsaukšana

1   public class Employee {
2     private static final double BASE_SALARY = 15000.00;
3     private String name;
4     private double salary;
5     private Date   birthDate;
6  
7     public Employee(String name, double salary, Date DoB) {
8       this.name = name;
9       this.salary = salary;
10      this.birthDate = DoB;
11    }
12    public Employee(String name, double salary) {
13      this(name, salary, null);
14    }
15    public Employee(String name, Date DoB) {
16      this(name, BASE_SALARY, DoB);
17    }
18    public Employee(String name) {
19      this(name, BASE_SALARY);
20    }
21    // more Employee code...
22  }
1   public class Manager extends Employee {
2      private String department;
3  
4      public Manager(String name, double salary, String dept) {
5          super(name, salary);
6          department = dept;
7      }
8      public Manager(String n, String dept) {
9          super(name);
10          department = dept;
11      }
12      public Manager(String dept) { // Nepareizi, jo nav bezargumentu super()
13          department = dept;
14      }
15  }

Objektu konstruēšana un inicializācija: Padziļināts izklāsts:

  • Rezervē atmiņu un veic noklusēto inicializāciju (saliek mainīgajos nulles)
  • Instanču mainīgo inicializācijai rekursīvi lieto sekojošus soļus:
  • Savāc konstruktora parametrus
  • Ja ir atklāts this() izsaukums, tālāko konstruktoru izsauc rekursīvi un iet uz 5. soli
  • Rekursīvi izsauc atklāto vai slēpto super izsaukumu, izņemot, ja jākonstruē java.lang.Object instance.
  • Izpilda atklāti rakstītos instanču mainīgo inicializētājus.
  • Izpildīt tekošā konstruktora ķermeni

Konstruktoru un inicializācijas piemērs

public class Object {
   ...
   public Object() {}
   ...
}

public class Employee extends Object {
  private String name;
  private double salary = 15000.00;
  private Date   birthDate;

  public Employee(String n, Date DoB) {
    // implicit super();
    name = n;
    birthDate = DoB;
  }
  public Employee(String n) {
    this(n, null);
  }
}

public class Manager extends Employee {
   private String department;

   public Manager(String n, String d) {
       super(n);
       department = d;
   }
}

Konstruktoru un inicializācijas piemērs

Sākotnējā inicializācija
  • Rezervē atmiņu Manager objektam
  • Inicializē instanču mainīgos uz noklusētajām vērtībām (false, 0 vai null)
Izsauc konstruktoru Manager("Joe Smith", "Sales")
  • Piesaista konstruktora parametrus: n="Joe Smith", d="Sales"
  • (Šoreiz nav atklāta this() izsaukuma)
  • Izsauc super(n), t.i. Employee(String)
Izsauc konstruktoru Employee(String)
  • Piesaista konstruktora parametrus: n="Joe Smith"
  • Izsauc this(n, null), t.i. Employee(String, Date)
  • Piesaista konstruktora parametrus: n="Joe Smith", DoB=null
  • (Šoreiz nav atklāta this() izsaukuma)
  • Izsauc super() t.i. Object()
Izsauc konstruktoru Object()
  • (Nav jāpiesaista parametri)
  • (Nav this() izsaukuma)
  • (Nav super() izsaukuma jo Object ir sakne)
  • (Nav atklātas mainīgo inicializācijas klasē Object)
  • (Nav konstruktora ķermeņa ko izpildīt)
Atgriežas pie Employee
  • Atklātās Employee mainīgo inicializācijas: salary=15000.00;
  • Izpilda ķermeni: name="Joe Smith"; date=null;
  • Izpilda ķermeni: Employee(String) nav ķermeņa
Atgriežas pie Manager
  • (Klasē Manager nav atklātas mainīgo inicializācijas)
  • Izpilda ķermeni: department="Sales"

Inicializācijas procesa sekas

1  public class Employee extends Object {
2      private String  name;
3      private double  salary = 15000.00;
4      private Date      birthDate;
5      private String summary;
6  
7      public Employee(String n, Date DoB) {
8          name = n;
9          birthDate = DoB;
10          summary = getDetails();
11      }
12      public Employee(String n) {
13            this(n, null);
14      }
15  
16      public String getDetails() {
17          return "Name: " + name + "\nSalary: " + salary
18                        + "\nBirth Date: " + birthDate;
19      }
20  }
1  public class Manager extends Employee {
2      private String department;
3  
4      public Manager(String n, String d) {
5          super(n);
6          department = d;
7      }
8  
9      public String getDetails() {
10          return super.getDetails() + "\nDept: " + department;
11      }
12  }

Konstruējot Manager objektu, vadība nonāk Employee klases 10. rindiņā. Tur esošais getDetails() izsaukums noved pie Manager metodes getDetails() izsaukuma, lai gan department vēl nav Manager konstruktorā inicializēts. Šajā piemērā iekš "summary" iedrukās "null" norādītā departamenta vietā.

Lai no šādām situācijām izvairītos, var izmantot šādu ieteikumu: "Ja konstruktorā ir jāsauc metode, padariet šo metodi privātu".

Klase java.lang.Object

  • Object klase ir visu Javas klašu hierarhijas virsotne
  • Klases deklarācija bez extends konstrukcijas patiesībā ir Object apakšklase
public class Employee {
    ...
}

ir tas pats, kas

public class Employee extends Object {
    ...
}

Operators == un equals metode

  • == operators nosaka, vai divas references ir identiskas (t.i. attiecas uz to pašu objektu).
  • equals metode nosaka, vai objekti ir zināmā nozīmē "vienādi", bet tie var būt divi atsevišķi objekti
  • Klasē Object dotā equals metodes implementācija lieto == operatoru.
  • Lietotāja definētajās klasēs var pārdefinēt equals metodi, lai izveidotu konkrētajam objektu tipam piemērotu vienādības pārbaudi.
  • Atbilstoši izmaiņām kādas klases equals metodē, vajadzētu pamainīt arī hashCode metodi, kuru lieto java.util.HashTable u.c. datu struktūras (ir prasība - ja divi objekti ir vienādi saskaņā ar savu equals() metodi, tad arī hešvērtības tiem ir vienādi int mainīgie).

Piemērs ar equals

1  public class MyDate {
2    private int day;
3    private int month;
4    private int year;
5  
6    public MyDate(int day, int month, int year) {
7      this.day   = day;
8      this.month = month;
9      this.year  = year;
10    }
11  
12    public boolean equals(Object o) {
13      boolean result = false;
14      if ( (o != null) && (o instanceof MyDate) ) {
15        MyDate d = (MyDate) o;
16        if ( (day == d.day) && (month == d.month)
17                        && (year == d.year) ) {
18          result = true;
19        }
20      }
21      return result;
22    }
23  
24    public int hashCode() {
25      return (   (new Integer(day).hashCode())
26                            ^ (new Integer(month).hashCode())
27                            ^ (new Integer(year).hashCode())
28                          );
29    }
30  }

Piemērs ar equals

1  class TestEquals {
2    public static void main(String[] args) {
3      MyDate  date1 = new MyDate(14, 3, 1976);
4      MyDate  date2 = new MyDate(14, 3, 1976);
5  
6      if ( date1 == date2 ) {
7          System.out.println("date1 un  date2 ir identiski");
8      } else {
9          System.out.println("date1 un date2 nav identiski");
10     }
11  
12     if ( date1.equals(date2) ) {
13         System.out.println("date1 un date2 ir vienādi");
14     } else {
15         System.out.println("date1 un date2 nav vienādi");
16     }
17  
18     System.out.println("piešķir date2 = date1;");
19     date2 = date1;
20  
21     if ( date1 == date2 ) {
22         System.out.println("date1 un date2 ir identiski");
23     } else {
24         System.out.println("date1 un date2 nav identiski");
25     }
26   }
27 }

Izvade:

date1 un date2 nav identiski
date1 un date2 ir vienādi
piešķir date2 = date1;
date1 un date2 ir identiski

Metode toString

  • Pārveido objektu par String.
  • Lieto stringu konkatenācijās
  • Pārdefinējiet šo metodi, lai piedāvātu informāciju par saviem objektiem lasāmā veidā
  • Vienkāršie tipi (int, char, boolean, utml.) pārveidojas par String, lietojot iesaiņotājklases (java.lang.Integer, java.lang.Character, java.lang.Boolean) statisko metodi toString.

Iesaiņotājklases (wrapper classes)

Uzlūko vienkāršos datu tipus kā objektus

Vienkāršais datu tipsIesaiņotājklase
booleanjava.lang.Boolean
bytejava.lang.Byte
charjava.lang.Character
shortjava.lang.Short
intjava.lang.Integer
longjava.lang.Long
floatjava.lang.Float
doublejava.lang.Double
int pInt = 500;
Integer wInt = new Integer(pInt);
int p2 = wInt.intValue();

Jautājumi paškontrolei

  • Definēt jēdzienus mantošana, polimorfisms, pārslogošana, pārdefinēšana un virtuālās metodes izsaukums
  • Prast lietot piekļuves modificētājus - protected un noklusēto ("pakotnes līmeņa") modificētāju
  • Aprakstīt konstruktoru un metožu pārdefinēšanu
  • Aprakstīt pilnu objekta konstrukcijas un inicializācijas gaitu

Javas programmā prast atpazīt:

  • Pārslogotas metodes un konstruktorus
  • "this" lietojumu visu pārslogoto konstruktoru izsaukšanai
  • Pārdefinētas metodes
  • Virsklases metožu izsaukumus
  • Vecāku klases konstruktorus
  • Vecāku klases konstruktoru izsaukumus