[WikiDyd] [TitleIndex] [WordIndex

Rozszerzanie klas, klasy abstrakcyjne, interfejsy

[Java] : K. Arnold, J. Gosling: Java, WNT 1999

Przeanalizujmy kod klasy implementującej listę liniową zawierającą dowolne obiekty:

   1 class List3 {
   2 
   3    private class LNode {
   4       Object item;
   5       LNode next;
   6 
   7       LNode( Object item ) {
   8          this.item= item;
   9          this.next= null;
  10       }
  11 
  12       LNode( Object item, LNode next ) {
  13          this.item= item;
  14          this.next= next;
  15       }
  16 
  17       void insertAfter( Object item ) {
  18          next= new LNode( item, next );
  19       }
  20    }
  21 
  22    private LNode head;
  23    private int size;
  24    private LNode iterator;
  25     
  26    List3() {
  27       head= null;
  28       size= 0;
  29    }
  30 
  31    public void insertAtFront( Object newItem ) {
  32       head= new LNode( newItem, head );
  33       size++;
  34    }
  35 
  36    public void insertAtEnd( Object newItem ) {
  37       size++;
  38       if( head == null ) {
  39          head= new LNode( newItem, head );
  40       } else {
  41          LNode p= head;
  42          while( p.next != null )
  43             p= p.next;
  44          p.next= new LNode( newItem, p.next );
  45       }
  46    }
  47 
  48    public Object removeFirst( ) { // zwraca usuniety element
  49       if( head != null ) {
  50          Object tmp= head.item;
  51          head= head.next;
  52          size--;
  53          return tmp;
  54       } else {
  55          return null;  // lista jest pusta i nie ma co zwrocic
  56                     // to rozwiazanie nie jest dobre, ale poprawimy
  57                     // je pozniej
  58       }
  59    }
  60 
  61    public Object removeLast( ) { // zwraca usuniety element
  62       if( head != null ) {
  63          size--;
  64          if( head.next == null )
  65             return this.removeFirst();
  66          else {
  67             LNode p= head;
  68             while( p.next.next != null )
  69                p= p.next;
  70             Object tmp= p.next.item;
  71             p.next= null;
  72             return tmp;
  73          }
  74       } else {
  75          return null;  // lista jest pusta i nie ma co zwrocic
  76                     // to rozwiazanie nie jest dobre, ale poprawimy
  77                     // je pozniej (zastosujemy wyjątki)
  78       }
  79    }
  80 
  81    public void resetIterator() {
  82       iterator= head;
  83    }
  84 
  85    public boolean isNext() {
  86       return iterator != null;
  87    }
  88 
  89    public Object getNext() {
  90       if( isNext() ) {
  91          Object tmp= iterator.item;
  92          iterator= iterator.next;
  93          return tmp;
  94       } else
  95          return null; // to rozwiazanie tez nie jest dobre i tez
  96                    // poprawimy to pozniej (zastosujemy wyjątki)
  97    }
  98  
  99    public String toString( ) {
 100       StringBuffer b= new StringBuffer( "( " );
 101 
 102       for( LNode p=head; p != null; p= p.next ) 
 103          b.append( p.item + " " );
 104       b.append( " )" );
 105       return b.toString();
 106    }
 107 
 108    public static void main( String [] args ) {
 109       List3 l= new List3();
 110 
 111       for( int i=1; i <= 4; i++ )
 112          l.insertAtEnd( new Integer( i ) );
 113 
 114       System.out.println( l );
 115 
 116       for( double d=1; d <= 6; d++ )
 117          l.insertAtFront( new Double( d ) );
 118 
 119       System.out.println( l );
 120 
 121       l.resetIterator();
 122       while( l.isNext() ) {
 123          Object o= l.getNext(); 
 124          if( o instanceof Double )
 125             System.out.println( "Double: " + o );
 126          else if( o instanceof Integer )
 127             System.out.println( "Integer: " + o );
 128          else
 129             System.out.println( "other: " + o );
 130       }
 131 
 132 
 133       for( int i=1; i<=3; i++ ) {
 134          l.removeFirst();
 135          l.removeLast();
 136       }
 137       System.out.println( l );
 138       List3 ll= new List3();
 139       for( int i=10; i < 40; i+= 10 )
 140          ll.insertAtEnd( new Integer( i ) );
 141       l.insertAtFront( ll );
 142       System.out.println( l );
 143       l.resetIterator();
 144       while( l.isNext() ) {
 145          Object o= l.getNext(); 
 146          if( o instanceof Double )
 147             System.out.println( "Double: " + o );
 148          else if( o instanceof Integer )
 149             System.out.println( "Integer: " + o );
 150          else
 151             System.out.println( "other: " + o );
 152       }
 153    }
 154 }

Załóżmy, że chcemy zmienić klasę List3 tak, aby efektywniej dodawać element na końcu. W tym celu chcielibyśmy dodać dodatkowe pole -- wskaźnik na ostatni element.

Można to zrobić bez modyfikacji klasy List3 -- tworząc nową klasę, która dziedziczy wszystkie właściwości List3 i dodaje nową funkcjonalność:

   1 class TList extends List3 {
   2     /* pola head, size i iterator sa dziedziczone po List3 */
   3     LNode tail;
   4     ...

To oznacza, że TList zachowuje się tak, jak List3, ale posiada dodatkowe pole: tail.

Mówimy, że TList jest "klasą pochodną" albo "pod-klasą", a List3 jest "nad-klasą" (ang. subclass i superclass).

Klasa pochodna może: 1. dodawać nowe pola 2. deklarować nowe metody 3. zastępować metody istniejące w nad-klasie nowymi implementacjami (nazywamy to przesłanianiem metod).

Spróbujmy tego trzeciego: możemy w TList zastąpić metodę insertAtEnd przez znacznie efektywniejszą funkcję:

   1    public void insertAtEnd( Object newItem ) {
   2       if( head == null ) {
   3          head= new LNode( newItem, head );
   4          size= 1;
   5          tail= head;
   6       } else {
   7          tail.next= new LNode( newItem, tail.next );
   8          tail= tail.next;
   9       }
  10    }

Oczywiście metody insertAtFront, removeFirst, removeLast, i konstruktor tez wymagają re-implementacji, ale toString, resetIterator, isNext czy getNext mogą pozostać takie, jak w List3.

Rozszerzanie a konstruktor

Jeżeli nie zaimplementujemy odpowiedniego konstruktora dla TList, to przy tworzeniu obiektu takiego typu będzie wołany konstruktor List3. Niestety, nie może on inicjować poprawnie pola tail, bo takowego w List3 nie ma. Na szczęście możemy zasłonić ten konstruktor przez zdefiniowanie konstruktora TList:

   1    public TList() {
   2       size= 0;
   3       head= null;
   4       tail= null;
   5    }

Problem pojawi się, jeśli potem dodamy jakieś pole do List3 i zapomnimy o uaktualnieniu powyższego konstruktora. Aby uniknąć takich sytuacji możemy zmodyfikować konstruktor TList:

   1    public Tlist() {
   2       super();
   3       tail= null;
   4    }

"super()" oznacza tu wywołanie bezargumentowego konstruktora klasy nadrzędnej (a więc List3). Instrukcja "super();" musi być pierwszą instrukcją konstruktora.

Oczywiście nie musimy wywoływać konstruktora bezparametrowego -- jeśli nadklasa ma inne konstruktory, to możemy użyć jednego z nich.

Jeżeli pierwszą instrukcją konstruktora klasy pochodnej nie jest wywołanie żadnego konstruktora nadklasy, to automatycznie wywoływany jest jej bezargumentowy konstruktor. Jeśli nadklasa nie ma takowego, to konstruktor podklasy musi jawnie wywołać jeden z konstruktorów z argumentami.

Wywoływanie metod z nadklasy

Czasem dobrze jest zasłonić jakąś metodę nadklasy, ale móc wywołać także tę zasłoniętą: np. przy tworzeniu metody insertAtFront w TList wygodnie jest posłużyć się metodą z List3. Można to zrobić tak:

   1    public void insertAtFront( Object newItem ) {
   2       super.insertAtFront( newItem );
   3       if( size== 1 )
   4          tail= head;
   5    }

"super" pełni tu więc rolę podobną do "this", ale odnosi się do odpowiedniego obiektu nadklasy.

W odróżnieniu od wywołania konstruktora, wywołanie metody z nadklasy może wystąpić w dowolnym miejscu.

Atrybut "protected"

Stwierdzenie "można to zrobić bez modyfikacji klasy List3" występujące nieco wyżej na tej stronie, nie było prawdziwie. W rzeczywistości, aby klasa TList miała dostęp do pól klasy List3, należy te pola zadeklarować jako "protected" a nie "private":

   1 class List3 {
   2 
   3    protected class LNode {
   4       Object item;
   5       LNode next;
   6 
   7       LNode( Object item ) {
   8          this.item= item;
   9          this.next= null;
  10       }
  11 
  12       LNode( Object item, LNode next ) {
  13          this.item= item;
  14          this.next= next;
  15       }
  16 
  17       void insertAfter( Object item ) {
  18          next= new LNode( item, next );
  19       }
  20    }
  21 
  22    protected LNode head;
  23    protected int size;
  24    protected LNode iterator;
  25  ...

protected oznacza "pomiędzy public a `package'" -- pole takie jest dostępne nie tylko dla metod danej klasy, ale także dla metod klas pochodnych. Pola "prywatne" nie są dla takich klas dostępne.

Jeśli pozostawimy LNode, head, size i iterator jako "private", to zdefiniowane dla TList metody insertAtEndi i insertAtFront nie będą miały do nich dostępu i kompilator nie skompiluje takiego kodu.

Jeżeli tworzymy klasę, którą ktoś pewnego dnia będzie chciał rozszerzyć, to dobrze jest deklarować pola i metody wewnetrzne jako "protected".

Atrybut "final"

Słowa kluczowego final możemy używać do tworzenia:

Hierarchie klas

Klasa pochodna może mieć klasy pochodne, itd, itd. Na szczycie całej hierarchii klas stoi klasa Object:

       Object
      /     \
 String     List3
           /    \
        DList  TList 
                 /
               OrderedTList

Dlatego właśnie item w List3 jest typu Object: może on wskazywać na obiekt dowolnej klasy.

Dynamiczne dopasowanie metod

Zauważmy, że każdy obiekt typu TList jest też obiektem typu List3, ale nie każdy obiekt typu List3 jest TList:

  List3 l3= new TList();  // tak można
  TList tl= new List3();  // tego kompilator nie przepusci

A co się dzieje, jeśli zrobimy tak:

  List3 l3= new TList(); 
  l3.insertAtEnd( o );

otóż w drugiej instrukcji wołamy metodę TList.insertAtEnd( ) - Java automatycznie znajduje odpowiednią metodę, biorąc pod uwagę faktyczny typ obiektu W CZASIE WYKONYWANIA PROGRAMU.

I to jest miłe - bo np. jeżeli mamy kod, który korzysta z List3 i chcemy wymienić je na (efektywniejsze) TList, to wystarczy wymienić tylko nazwy konstruktorów list:

mamy

   1 class HuffmanCoder {
   2    List3 lista_robocza;
   3 
   4    HuffmanCoder( ) {
   5       lista_robocza= new List3();
   6 
   7    /*
   8        Tutaj tysiące linii kodu,
   9        kilkaset wywołań  insertAtEnd, 
  10        insertAtFront i innych
  11    */
  12 }

zmieniamy

   1 class HuffmanCoder {
   2    List3 lista_robocza;
   3 
   4    HuffmanCoder( ) {
   5       lista_robocza= new TList(); // TYLKO TE JEDNA LINIE !!
   6 
   7    /*
   8        Tutaj tysiące linii kodu,
   9        kilkaset wywołań  insertAtEnd, 
  10        insertAtFront i innych
  11    */
  12 }

I już HuffmanCoder posługuje się WSZĘDZIE TList.

Jeżeli mamy metodę, która posługuje się obiektem (obiektami) typu List3, ale ich nie tworzy, to taka metoda działa bez żadnych zmian także na obiektach TList.

UWAGA1: dynamiczne dopasowanie dotyczy tylko metod. Pola są dopasowane statycznie.

Przykład:

   1 class Podstawa {
   2    protected String n= "Podstawa";
   3 
   4    public String toString( ) {
   5       return n;
   6    }
   7 }
   8 
   9 class Rozszerzenie extends Podstawa {
  10    protected String n= "Rozszerzenie";
  11 
  12    public String toString( ) {
  13       return n;
  14    }
  15 
  16    public static void main( String [] args ) {
  17       Rozszerzenie r= new Rozszerzenie();
  18       Podstawa p= r;
  19 
  20       System.out.println( "r: " + r );
  21       System.out.println( "p: " + p );
  22       System.out.println( "r.n: " + r.n );
  23       System.out.println( "p.n: " + p.n );
  24    }
  25 }

UWAGA2: załóżmy, że w klasie TList zdefiniowaliśmy nową metodę cleanList(). Nie możemy oczywiście wywoływać cleanList dla obiektów typu List3,' ale też nie możemy wywoływać tej metody dla zmiennych typu List3 wskazującej na obiekt typu TList:

   TList tl= new TList();
   tl.cleanList();        // tak ok
   List3 l3= new Tlist();
   l3.cleanList();        // blad kompilacji

Jest tak dlatego, że nie każdy obiekt typu List3 jest TList, a obiekt typu List3 nie ma metody cleanList(), a więc nie można tu użyć dynamicznego dopasowania.

Jeszcze zabawniej robi się przy podstawianiu zmiennych:

   List3 l3;
   TList tl= new TList();

   l3= tl;         // ok               (kazdy TList jest List3)
   tl= l3;         // blad kompilacji  (nie kazdy ... )
   tl= (TList) l3; // ok, bo l3 jest TList
   l3= new List3();
   tl= (TList) l3; // RU-TIME ERROR: ClasCastException

Wróćmy teraz do kodu klasy List3: załóżmy, że włożyliśmy na listę trochę Integer(ów) i przypomnijmy, że getNext() zwraca obiekt.

Kod:

      l.resetIterator();
      while( l.isNext() ) {
         int x= l.getNext().intValue();   // blad kompilacji
         ...
      }

nie skompiluje się, gdyż nie każdy Object jest Integer, a Object nie ma zdefiniowanej metody intValue. Ale ma np. metodę toString, więc kod

      l.resetIterator();
      while( l.isNext() ) {
         String s= l.getNext().toString(); 
         ...
      }

da się skompilować.

Operator instanceof zaprezentowany w funkcji main klasy List3 pozwala stwierdzić, czy rzutowanie jest bezpieczne:

   if( s instanceof TList ) {
      t= (TList) s;
      ...

Klasy abstrakcyjne

Klasa abstrakcyjna to taka, która służy tylko do rozszerzania:

   1 public abstract class List {
   2    int size;
   3 
   4    public int length() {
   5       return size;
   6    }
   7 
   8    public abstract void insertAtFront( Object item ); // metoda abstrakcyjna
   9 
  10 }
  11 
  12 public abstract class Element {
  13    int node1, node2;
  14 
  15    public abstract double voltageOn();
  16 
  17    public abstract double currentThrough();
  18 }

Jeżeli choć jedna metoda w klasie jest zadeklarowana jako abstrakcyjna, to klasa też musi być zadeklarowana jako abstrakcyjna.

Nie można tworzyc obiektów należących do klas abstrakcyjnych: można zadeklarować zmienną typu List, ale nie można utworzyć obiektu typu List:

   List mojaLista;          // to jest ok
   mojaLista= new List();   // blad kompilacji

Możemy jednak stworzyć klasy pochodne od List, np.:

   1 public class SList extends List {
   2    // dziedziczymy size
   3    protected LNode head;
   4    
   5    SList() {
   6       head= null;
   7       size= 0;
   8    }
   9 
  10    // dziedziczymy length
  11 
  12    public void insertAtFront( Object item ) {
  13       head= new LNode( item, head );
  14       size++;
  15    }
  16 }

UWAGA: musimy zaimplementować metodę insertAtFront - kompilator nie pozwoli inaczej. Klasy nie-abstrakcyjne nie moga zawierać abstrakcyjnych metod.

Teraz możemy już zrobić tak:

   List mojaLista;          // to jest ok
   mojaLista= new SList();  // i to także

Po co stosujemy klasy abstrakcyjne?

  public void listSorter( List l ) { ... }

        --------------
        | Aplikacja  |
        --------------
        | listSorter |
        --------------
        | ATD List   |
        --------------

Interfejsy

W Javie występuje jeszcze coś przypominającego klasy abstrakcyjne, ale trochę innego:

   1 public interface Comparable {   // zdef. w java.lang
   2    public int compareTo( Object o );
   3 }

Iterfejs nie może zawierać implementacji żadnych metod ani pól innych niż stałe wspólne (final static).

Klasa może pochodzić od tylko jednej nadklasy ale może implementować wiele interfejsów.

   1 public interface Inversible {
   2    public void inverse();
   3 }
   4 
   5 public class SList extends List implements Comparable, Inversible {
   6   // to co poprzednio
   7 
   8   public int compareTo( Object o ) {
   9      // o ma byc lista, trzebo go porownac z this
  10      // i zwrocic -1|0|1
  11   }
  12 
  13   public void inverse() {
  14      // trzeba - np. odwrocic kolejnosc elementow
  15   }
  16 }

Teraz np.

  Comparable c= new Slist();
  Inversible i= (Inversible) c;  // uwaga: nie kazde Comparable jest Inversible

Interfejscy możemy rozszerzać dodając nowe metody lub kombinować:

public interface ComparableAndInversible extends Comparable, Inversible {}

Comparable

Comparable to standardowy interfejs. Jeżeli klasa implementuje Comparable, to tablice takich obiektów możemy sortować funkcją sort z java.util

public static void sort( Object[] a ); // sa rozne wersje

W Javie 1.5 interfejs przedefiniowano za pomocą generics:

public interface Comparable<T>

Np.

class String  implements Comparable<String> ...

Enumeration i Iterator

Enumeration to standardowy (ale przestarzały - patrz niżej) interfejs zdefiniowany w java.util

   1 public interface Enumeration {
   2    public boolean hasMoreElements();
   3    public Object nextElement();
   4 }

(Coś podobnego zrobiliśmy już, inaczej nazywając funkcje, w klasie List3)

Wyobraźmy sobie to samo za pomocą Enumeration:

   1 public class SListEnum implements Enumeration {
   2    private LNode n;
   3 
   4    SListEnum( SList l ) {
   5       n= l.head;
   6    }
   7 
   8    public boolean hasMoreElements() {
   9       return n != null;
  10    }
  11 
  12    public Object nextElement() {
  13       Object tmp= n.item;
  14       n= n.next;
  15       return tmp;
  16    }
  17 }

Jeżeli chcemy iterować po liscie, to robimy tak:

   SList sl= new SList();
   // wypelniamy sl
   SListEnum esl= new SListEnum( sl );
   while( el.hasMoreElements() ) {
      Object o= el.nextElement();
      // i obrabiamy o
   }

Zaletą interfejsu jest to, że możemy przxy jego pomocy iterować po obiekcie, nie wnikając w jego strukturę:

 for (Enumeration e = v.elements() ; e.hasMoreElements() ;) {
         System.out.println(e.nextElement());
 }

Funkcjonalność oferowana przez Enumeration została ponownie zaimplementowana w Javie 1.5 z użyciem szablonów (generics) w postaci Interfejsu Iterator.


2015-09-23 06:44