Klasy abstrakcyjne i interfejsy – czy są potrzebne?

żródło: javatpoint.com

Klasy abstrakcyjne i interfejsy to jedna z podstaw programowania obiektowego. Prawidłowe się nimi posługiwanie odróżnia dobrego programistę od zwykłego klepacza kodu. Jest to też powód, dla którego często pytania o tę dziedzinę padają na rozmowach kwalifikacyjnych. Co nam dają te konstrukcje?

Klasa abstrakcyjna

Klasa abstrakcyjna, w dużym uproszczeniu, to klasa, która nie może mieć implementacji (nie można tworzyć obiektów jej typu). Można natomiast z niej dziedziczyć. Klasa abstrakcyjna jest pewnym uogólnieniem innych klas, lecz sama nie istnieje. Jako przykład można wykorzystać klasyczną już strukturę figura-koło/kwadrat/trójkąt. W tym wypadku każda ze szczegółowych figur (koło, kwadrat czy trójkąt) posiada takie informacje jak pole czy obwód. Jednakże tylko koło ma taką właściwość jak długość promienia, natomiast tylko wielokąty mają liczbę i ilość boków.

W tej sytuacji można wydzielić klasę abstrakcyjną Figura, która będzie miała abstrakcyjne metody obliczPole() oraz obliczObwod(), natomiast pola dotyczące długości boków / promienia będą wewnątrz poszczególnych klas.

Abstrakcyjne metody

Metody abstrakcyjne to takie, które nie mają implementacji w ciele klasy abstrakcyjnej. Służą jedynie temu, by zagwarantować implementację metody w klasie pochodnej, oraz aby jej kontrakt był spełniony.

abstract class Figura {

    abstract double obliczPole();

    abstract double obliczObwod();

}

Teraz, gdy będziemy implementować klasę pochodną, np. koło, kod powinien wyglądać tak:

import java.lang.Math;

class Kolo extends Figura {

    double dlugoscPromienia;

    double obliczPole(){
        return Math.PI*Math.pow(dlugoscPromienia, 2);
    }

    double obliczObwod(){
        return Math.PI*2*dlugoscPromienia;
    }

}

Oprócz nich, w klasach abstrakcyjnych mogą być umieszczone metody nieabstrakcyjne, z gotową implementacją.

abstract class Figura{

    abstract double obliczObwod();

    abstract double obliczPole();

    void czymJestem(){
        System.out.println("Jestem klasą abstrakcyjną Figura, dbam o prawidłowe kontrakty metod w klasach pochodnych");
    }
}

Te metody można swobodnie przesłaniać w klasach pochodnych, jak w przypadku normalnych metod, jednak nie jest to wymagane.

Interfejs

Interfejs jest często nazywany klasą czysto abstrakcyjną. Oznacza to, że wszystkie metody wewnątrz interfejsu są abstrakcyjne. Interfejsy tworzy się za pomocą słowa kluczowego interface:

interface ObliczaObwodOrazPole{

    double obliczObwod();
    double obliczPole();

}

Innymi słowy jest to po prostu zestaw metod bez implementacji, które określają jakie metody powinny być zaimplementowane w klasie, która korzysta z interfejsu.

Oprócz kontraktów metod w interfejsie można umieszczać stałe – pola niezmienne w trakcie działania programu. Wartości polom finalnym (od słowa kluczowego final, które określa stałą) można nadawać jedynie podczas inicjalizacji, lub wewnątrz konstruktoa. Tworzy się je za pomocą słowa kluczowego final przed typem pola, natomiast w interfejsie nie jest to konieczne – samo istnienie interfejsu determinuje, że każde pole jest stałą.

interface ObliczaObwodOrazPole{

    double LICZBA_PI = 3.14;

    double obliczObwod();
    double obliczPole();

}

Implementacja interfejsów

Aby tworzenie interfejsów miało jakikolwiek sens – należy je zaimplementować, tj. napisać klasę, w której zaimplementowane będą metody opisane w interfejsie.

import java.lang.Math;

class Kolo implements ObliczaObwodIPole{

    double dlugoscPromienia;

    @Override
    public double obliczObwod(){
        return PI*2*dlugoscPromienia;
    }

    @Override
    public double obliczPole(){
        return PI*Math.pow(dlugoscPromienia, 2);
    }

}

Do czego służą interfejsy?

Interfejsy przede wszystkim w prosty sposób ułatwiają integrację różnych fragmentów kodu. Napisanie samego interfejsu umożliwia odseparowanie API (kontraktu metod) od ich implementacji, co z kolei pozwala na równoległe tworzenie kilku warstw aplikacji przez kilka zespołów. Pozwala to także na zmianę implementacji, bez zmian tej części aplikacji, która z tych implementacji korzysta.

Wieloimplementacja interfejsów (wielodziedziczenie?)

Klasy mogą implementować wiele interfejsów jednocześnie, w odróżnieniu od klas, gdzie można dziedziczyć tylko po jednej z nich. Umożliwia to zmniejszenie rozmiaru samego interfejsu – zbliżenie do spełnienia zasady segregacji interfejsów (więcej o zasadach we wpisie o pryncypiach SOLID). Dzięki temu możemy podzielić nasz interfejs ObliczaObwodOrazPole na dwa mniejsze: ObliczaObwod oraz ObliczaPole. Konstrukcja dwóch interfejsów i klasy wyglądałaby teraz następująco:

interface ObliczaObwod{

    double obliczObwod();

}

..........

interface ObliczaPole{

    double obliczPole();

}

..........

import java.lang.Math;

class Kolo implements ObliczaObwod, ObliczaPole{

    double dlugoscPromienia;

    @Override
    double obliczObwod(){
        return Math.PI*2*dlugoscPromienia;
    }

    @Override
    double obliczPole(){
        return Math.PI*Math.pow(dlugoscPromienia, 2);
    }

}

Inicjalizacja typu interfejsu

Instancję klasy można przypisać zarówno do zmiennej tej klasy, jak i dowolnego interfejsu, który ta klasa implementuje. Na przykład dozwolony jest następujący zapis:

class Aplikacja{

    public static void main(String[] args){

        ObliczaPole obliczaPole = new Kolo();
        ObliczaObwod obliczaObwod = new Kolo();
        Kolo kolo = new Kolo();

    }

}

Pomimo tego, że w trakcie wykonywania programu każdy z tych obiektów jest tego samego typu (Kolo), to podczas kompilacji wygląda to następująco:

  • obiekt obliczaPole może jedynie korzystać z metody obliczPole()
  • obiekt obliczaObwod może jedynie korzystać z metody obliczObwod()
  • obiekt kolo może korzystać zarówno z metody obliczPole jak również obliczObwod

Dzieje się tak dlatego, że kompilator “widzi” jedynie te elementy klasy, które są dostępne w deklarowanym typie, natomiast wirtualna maszyna widzi to, co jest w typie inicjalizowanym. Więcej o tym we wpisie na temat polimorfizmu.

Różnica między klasą abstrakcyjną a interfejsem

Metody domyślne

Od Javy 8 wprowadzono do interfejsu coś, co nazywa się metodami domyślnymi. W skrócie – jest to domyślna implementacja metody w danym interfejsie, która nie musi być implementowana w klasie implementującej interfejs. Jest to bardzo podobne działanie do tego, które zaobserwować można w przypadku nieabstrakcyjnych metod klasy abstrakcyjnej.

Czym się zatem różnią?

Największa różnica między klasami abstrakcyjnymi a interfejsami leży w dziedziczeniu – otóż klasę można dziedziczyć tylko jedną, a interfejsów można implementować nieskończoną ilość. Drugą różnicą jest specyfikacja dostępu – w klasach abstrakcyjnych poszczególne elementy klasy mogą mieć różne specyfikatory dostępu (public, package, protected – private nie ma sensu), natomiast w interfejsie nie zapisuje się specyfikatorów dostępu – wszystkie elementy mają dostęp publiczny.

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Wymagane pola są oznaczone *