Klasy i metody abstrakcyjne

Klasy i metody abstrakcyjne to sposób na wejście jeszcze wyżej w filozofię programowania obiektowego. W sposób najprostszy abstrakcje te powodują, że wprost nie da się powołać z nich obiektów. Te struktury są jedynie „zarysem”, pewną „ideą” jaką programista zamierzył w stosunku do klasy, czy do metody. Dopiero na podstawie tej idei należy wypełnić ją treścią, a wtedy dopiero można tę treść przywołać „do życia” w postaci instancji klasy potomnej, czyli obiektu. Celem tego mechanizmu jest niejako przygotowanie standardu zachowującego np. nazewnictwo metod w ramach takiej klasy. Dzięki czemu, użytkownik tej klasy, znając ten standard z innych podobnych klas, nie zastanawia się nad dokumentacją klasy, a może ją wprost użytkować. Powołanie klasy potomnej na skutek dziedziczenia pozwala na zdefiniowanie kodu (ciała metod) w tych wirtualnych szablonach. Po ich zdefiniowaniu, można już pokusić się do powołania instancji klasy.

Prześledźmy przypadek klasy abstrakcyjnej Pojazd zamieszczonej poniżej:

 abstract class Pojazd {
     String VIN;
     String color;
     String name;

    abstract boolean testEngine();
    abstract boolean testTireCompression();
    
    abstract boolean setVIN(String VIN);
    abstract boolean setColor(String color);
    abstract boolean steName(String name);

    abstract String getVIN();
    abstract String getColor();
    abstract String getName();
}

Próba powołania „do życia” takiej klasy spełznie na niczym, zresztą widać opisy nagłówkowe, ale nie ma treści. Wiemy jednak teraz, że kiedy będziemy konstruować klasę opartą na takim szablonie jakim jest klasa abstrakcyjna, to musimy zdefiniować wszystkie te elementy, które w niej są nagłówkowo wymienione, czyli:

public class KlasyWirtualne {

  
    public static void main(String[] args) {
  
            Samochod sam=new Samochod();
            System.out.println("Kolor samochodu to: "+sam.getColor());
    }
    
}


 abstract class Pojazd {
     String VIN;
     String color;
     String name;

    abstract boolean testEngine();
    abstract boolean testTireCompression();
    
    abstract boolean setVIN(String VIN);
    abstract boolean setColor(String color);
    abstract boolean setName(String name);

    abstract String getVIN();
    abstract String getColor();
    abstract String getName();
}

class Samochod extends Pojazd {

    @Override
    boolean testEngine() {
       return true;
    }

    @Override
    boolean testTireCompression() {
       return true;   
    }

    @Override
    boolean setVIN(String VIN) {
       return true;
    }

    @Override
    boolean setColor(String color) {
       return true;
    }

    @Override
    boolean setName(String name) {
        return true;
    }

    @Override
    String getVIN() {
        if (VIN!=null) return VIN; else return "no VID defined";
    }

    @Override
    String getColor() {
        if (color!=null) return color; else return "no color defined";    
    }

    @Override
    String getName() {
        if (name!=null) return name; else return "no Name defined";
    }
}

Jak widać powyżej udało nam się odziedziczyć z klasy Pojazd i zdefiniować klasę Samochód wraz ze zdefiniowaniem wszystkich niezbędnych 1czyli przynajmniej tych wymienionych w definicji klasy abstrakcyjnej metod. Jednakże nie jest koniecznym aby tworzyć klasę abstrakcyjną w pełni abstrakcyjną… jeśli wiemy co powinno się znaleźć w niektórych metodach to jak najbardziej możemy w definicji klasy abstrakcyjnej je umieścić. Takim przykładem jest poniższy kod, będący modyfikacją poprzedniego.

public class KlasyWirtualne {

  
    public static void main(String[] args) {
  
            Samochod sam=new Samochod();
            System.out.println("Kolor samochodu to: "+sam.getColor());
    }
    
}


 abstract class Pojazd {
     String VIN;
     String color;
     String name;

    abstract boolean testEngine();
    abstract boolean testTireCompression();
    
    abstract boolean setVIN(String VIN);
    abstract boolean setColor(String color);
    abstract boolean setName(String name);

    String getVIN() {
        if (VIN!=null) return VIN; else return "no VID defined";
    }

    String getColor() {
        if (color!=null) return color; else return "no color defined";    
    }

    String getName() {
        if (name!=null) return name; else return "no Name defined";
    }
}



class Samochod extends Pojazd {

    @Override
    boolean testEngine() {
       return true;
    }

    @Override
    boolean testTireCompression() {
       return true;   
    }

    @Override
    boolean setVIN(String VIN) {
       return true;
    }

    @Override
    boolean setColor(String color) {
       return true;
    }

    @Override
    boolean setName(String name) {
        return true;
    }
}

Jak widać to również działa. Stąd też możemy wyprowadzić definicję klasy abstrakcyjnej:

Klasą abstrakcyjną jest taka klasa, która2oznaczona selektorem abstract zawiera przynajmniej jeden nagłówek bez treści implementacyjnej. Posiada następujące cechy:

  • może zawierać metody abstrakcyjne3oznaczone selektorem abstract, czyli takie, które nie posiadają implementacji (ani nawet nawiasów klamrowych)
  • może zawierać stałe 4zmienne oznaczone jako public static final
  • może zawierać zwykłe metody, które niosą jakąś funkcjonalność, a klasy rozszerzające mogą ją bez problemu dziedziczyć
  • klasy rozszerzające klasę abstrakcyjną muszą stworzyć implementację dla metod oznaczonych jako abstrakcyjne w klasie abstrakcyjnej
  • metod abstrakcyjnych nie można oznaczać jako statyczne (nie posiadają implementacji)
  • nie da się tworzyć instancji klas abstrakcyjnych

Konkludując, stosowanie klas abstrakcyjnych pozwala na tworzenie klas potomnych o łudząco podobnej konstrukcji (zestaw metod i pól danych), a różniących się implementacją poszczególnych metod dopasowanych do konkretnych już zastosowań. Wpływa to znakomicie na przejrzystość kodu i jego uniwersalność.

Interfejsy

Interfejsy noszą pewne podobieństwo do klas abstrakcyjnych, ale są wyrażane jeszcze większym poziomem abstrakcji. Mianowicie interfejsy są pewnym szkieletem według którego będzie budowana następna generacja klasy. Nie zawierają niczego prócz podstawowych informacji o wymogach jakie trzeba spełnić by powołać w przyszłości instancję obiektu na podstawie klasy, która implementuje5Tak, implementuje. W dziedziczeniu mamy do czynienia z rozszerzaniem klasy (extends), a w interfejsach mamy do czynienia z jego implementacją (implements) interfejs. Przykładem deklaracji interfejsu jest poniższy kod:

public class Interfejsy {

   
    public static void main(String[] args) {

        Wulkan w=new Wulkan();
        w.start();
        w.stop();
    }    
}


interface Eksperyment {    
    boolean start();
    boolean stop();    
}


class Wulkan implements Eksperyment {
    
    @Override
public boolean start() {
        System.out.println("Eksperyment się zaczyna");
        return true;
    }
    
    @Override
public boolean stop() {
        System.out.println("Eksperyment się zakończył");
        return true;
    } 
}

Z tego przedstawionego listingu wynika olbrzymie podobieństwo do klas abstrakcyjnych, po co zatem istnieją interfejsy?! Zobaczmy zatem poniższy kod:

public class Interfejsy {

   
    public static void main(String[] args) {
   
        Wulkan w=new Wulkan();
        w.start();
        w.stop();
    }
    
}

interface Source {
    boolean start();
}

interface Eksperyment{    
    boolean start();
    boolean stop();    
}

interface Geolokalizacja {
    boolean Geolokalizuj(double xSzer,double yWys);
}


class Wulkan implements Eksperyment, Geolokalizacja, Source {    // <-- Tu jest ciekawie :-)
    
    @Override
public boolean start() {
        System.out.println("Eksperyment się zaczyna");
        return true;
    }
    
    @Override
public boolean stop() {
        System.out.println("Eksperyment się zakończył");
        return true;
    }

    @Override
    public boolean Geolokalizuj(double xSzer, double yWys) {
        System.out.println("Ustawiam geolokalizację miejsca eksperymentu");
        return true;
}
    
}

Jak widać interfejsy są sposobem by „zmusić” implementującego je do uzupełnienia kodu wymaganego kilkoma nawet interfejsami6Ma to pewne podobieństwo do… dziedziczenia wielobazowego, ale dziedziczeniem nie jest. Jest jednakże sposobem na implementację z kilku schematów łączących się w jedną strukturę zawierającą wszystkie potrzebne metody..

Czym zatem jest interfejs?! Definicja poniżej:

Interfejs jest strukturą, która zawiera listę metod jakie należy zaimplementować, oraz pola danych będących stałymi. Jest specyficznym spisem składników dla stworzenia kompletnej klasy.

Wszystkie metody interfejsu są domyślnie publiczne i abstrakcyjne, nie jest więc konieczne dopisywanie przed nimi tych słów kluczowych, jeśli o tym zapomnimy, nic się nie stanie.

Wszystkie pola interfejsu muszą być zadeklarowane jako publiczne, statyczne i finalne, czyli innymi słowy, w interfejsie możemy także umieszczać stałe pola. Nie muszą być jednak oznaczone słowami final i static, podobnie jak w przypadku metod są one tam dodawane automatycznie

  • Interfejs musi być utworzony przy użyciu słowa kluczowego interface
  • Interfejsy mogą być wykorzystywane polimorficznie7o tym następnym razem, tzn. można ich używać jako typu ogólniejszego klas, które go implementują.
  • Interfejs może rozszerzać (extends) tylko interfejsy (nawet kilka, co w przypadku klas jest zabronione).
  • Metody interfejsu nie mogą być zadeklarowane jako statyczne.
public class Interfejsy {

   
    public static void main(String[] args) {
   
        Wulkan w=new Wezuwiusz("40°49′16″N","14°25′32″E");
        w.start();
        w.stop();
        System.out.println(Source.ETYKIETA);
    }
    
}

interface Source {
 // public final String ETYKIETA="AutorProjektu";
    String ETYKIETA="AutorProjektu";
    boolean start();
}

interface Eksperyment extends Source{    
    @Override       
    boolean start();
    boolean stop();    
}

interface Geolokalizacja {
    String ETYKIETA="Wersja 1.01";
    boolean Geolokalizuj(String DlugGeo, String SzerGeo);
}


abstract class Wulkan implements Eksperyment, Geolokalizacja, Source {
    protected String DlugGeo, SzerGeo;
    @Override
public boolean start() {
        System.out.println("Eksperyment się zaczyna");
        return true;
    }
    
    @Override
public boolean stop() {
        System.out.println("Eksperyment się zakończył");
        return true;
    }

    @Override
public boolean Geolokalizuj(String DlugGeo, String SzerGeo) {
        System.out.println("Ustawiam geolokalizację miejsca eksperymentu");
        this.DlugGeo=DlugGeo; this.SzerGeo=SzerGeo;
        return true;
}
    
       abstract boolean Wybuch(); 
}


class Wezuwiusz extends Wulkan {
    Wezuwiusz(String DlugGeo,String SzerGeo) {
       this.DlugGeo=DlugGeo; this.SzerGeo=SzerGeo; 
    }
    @Override
    boolean Wybuch() {
        return false;
    }
    
}

Powyższy kod jest przykładem stopniowego rozbudowywania struktur o niezbędne metody i pola danych.