JavaRush /Blog Java /Random-PL /Dziedziczenie wielokrotne w Javie. Porównanie składu i dz...
HonyaSaar
Poziom 26
Москва

Dziedziczenie wielokrotne w Javie. Porównanie składu i dziedziczenia

Opublikowano w grupie Random-PL
Jakiś czas temu napisałem kilka postów na temat dziedziczenia, interfejsów i kompozycji w Javie. W tym artykule przyjrzymy się dziedziczeniu wielokrotnemu, a następnie dowiemy się o przewadze kompozycji nad dziedziczeniem.
Dziedziczenie wielokrotne w Javie.  Porównanie składu i dziedziczenia - 1

Dziedziczenie wielokrotne w Javie

Dziedziczenie wielokrotne to możliwość tworzenia klas z wieloma klasami nadrzędnymi. W przeciwieństwie do innych popularnych języków obiektowych, takich jak C++, Java nie obsługuje dziedziczenia wielokrotnego klas. Nie popiera tego ze względu na prawdopodobieństwo napotkania „problemu diamentu”, a zamiast tego woli zapewnić jakieś kompleksowe podejście do jego rozwiązania, wykorzystując najlepsze opcje, abyśmy mogli osiągnąć podobny wynik dziedziczenia.

„Problem diamentu”

Aby prościej zrozumieć problem diamentu, załóżmy, że w Javie obsługiwane jest dziedziczenie wielokrotne. W tym przypadku możemy otrzymać klasy o hierarchii pokazanej na poniższym rysunku. hierarchia klas diamentówZałóżmy, że SuperClassjest to klasa abstrakcyjna opisująca pewną metodę, a klasy ClassAi ClassBsą klasami rzeczywistymi. SuperClass.java
package com.journaldev.inheritance;
public abstract class SuperClass {
   	public abstract void doSomething();
}
ClassA.java
package com.journaldev.inheritance;
public class ClassA extends SuperClass{
    @Override
 public void doSomething(){
        System.out.println("Какая-то реализация класса A");
    }
  //собственный метод класса  ClassA
    public void methodA(){
    }
}
Załóżmy teraz, że klasa ClassCdziedziczy od ClassAi ClassBjednocześnie, a jednocześnie ma następującą implementację:
package com.journaldev.inheritance;
public class ClassC extends ClassA, ClassB{
    public void test(){
        //вызов метода родительского класса
        doSomething();
    }
}
Należy pamiętać, że metoda test()wywołuje metodę doSomething()klasy nadrzędnej, co będzie prowadzić do niejednoznaczności, ponieważ kompilator nie wie, która metoda nadklasy powinna zostać wywołana. Ze względu na kształt diagramu dziedziczenia klas w tej sytuacji, który przypomina zarys fasetowanego diamentu, problem nazywa się „Problemem Diamentu”. Jest to główny powód, dla którego Java nie obsługuje dziedziczenia wielokrotnego klas. Należy zauważyć, że ten problem z dziedziczeniem wielokrotnym klas może również wystąpić w przypadku trzech klas, które mają co najmniej jedną wspólną metodę.

Wielokrotne dziedziczenie i interfejsy

Być może zauważyłeś, że zawsze mówię „wielokrotne dziedziczenie nie jest obsługiwane między klasami”, ale jest obsługiwane między interfejsami. Prosty przykład pokazano poniżej: InterfaceA.java
package com.journaldev.inheritance;
public interface InterfaceA {

    public void doSomething();
}
InterfaceB.java
package com.journaldev.inheritance;

public interface InterfaceB {

    public void doSomething();
}
Zauważ, że oba interfejsy mają metodę o tej samej nazwie. Załóżmy teraz, że mamy interfejs, który dziedziczy z obu interfejsów. InterfaceC.java
package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

    //метод, с тем же названием описан в  InterfaceA и InterfaceB
    public void doSomething();
Tutaj wszystko jest idealne, gdyż interfejsy są jedynie rezerwacją/opisem metody, a implementacja samej metody będzie w konkretnej klasie, która te interfejsy implementuje, więc nie ma możliwości napotkania niejednoznaczności przy wielokrotnym dziedziczeniu interfejsów. Dlatego klasy w Javie mogą dziedziczyć z wielu interfejsów. Pokażmy to na poniższym przykładzie. InterfacesImpl.java
package com.journaldev.inheritance;

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

    @Override
    public void doSomething() {
        System.out.println("doSomething реализация реального класса ");
    }

    public static void main(String[] args) {
        InterfaceA objA = new InterfacesImpl();
        InterfaceB objB = new InterfacesImpl();
        InterfaceC objC = new InterfacesImpl();

        //все вызываемые ниже методы получат одинаковую реализацию конкретного класса

        objA.doSomething();
        objB.doSomething();
        objC.doSomething();
    }
}
Być może zauważyłeś, że za każdym razem, gdy zastępuję metodę opisaną w nadklasie lub w interfejsie, używam adnotacji @Override. Jest to jedna z trzech wbudowanych adnotacji Java i należy jej zawsze używać podczas zastępowania metod.

Kompozycja jako zbawienie

A co jeśli chcemy użyć methodA()klasy ClassAi funkcji methodB()klasy ClassBw ClassС? Rozwiązaniem może być kompozycja - przepisana wersja ClassC, która implementuje obie metody klas ClassA, a ClassBtakże posiada implementację doSomething()dla jednego z obiektów. ClassC.java
package com.journaldev.inheritance;

public class ClassC{

    ClassA objA = new ClassA();
    ClassB objB = new ClassB();

    public void test(){
        objA.doSomething();
    }

    public void methodA(){
        objA.methodA();
    }

    public void methodB(){
        objB.methodB();
    }
}

Skład czy dziedziczenie?

Dobrą praktyką programowania w języku Java jest wykorzystywanie kompozycji zamiast dziedziczenia. Przyjrzymy się niektórym aspektom przemawiającym za tym podejściem.
  1. Załóżmy, że mamy następującą kombinację klas rodzic-dziedzic:

    ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    public void methodC(){
      	}
    
    }

    ClassD.java

    package com.journaldev.inheritance;
    
    public class ClassD extends ClassC{
    
        public int test(){
            return 0;
        }
    }

    Powyższy kod kompiluje się i działa dobrze, ale co by było, gdyby ClassCzostał zaimplementowany inaczej:

    package com.journaldev.inheritance;
    
    public class ClassC{
    
        public void methodC(){
        }
    
        public void test(){
        }
    }

    Należy zauważyć, że metoda test()już istnieje w klasie potomnej, ale zwraca wynik innego typu. Teraz ClassD, jeśli używasz IDE, nie zostanie ono skompilowane. Zostaniesz poproszony o zmianę typu zwracanego w klasie potomnej lub nadklasie.

    Wyobraźmy sobie teraz sytuację, w której występuje wielopoziomowe dziedziczenie klas i nadklasa nie jest dostępna dla naszych zmian. Teraz, aby pozbyć się błędu kompilacji, nie pozostaje nam nic innego, jak zmienić sygnaturę lub nazwę metody podklasy. Będziemy musieli także wprowadzić zmiany we wszystkich miejscach, w których wywołano tę metodę. Zatem dziedziczenie sprawia, że ​​nasz kod jest kruchy.

    Opisany powyżej problem nigdy nie występuje w przypadku kompozycji, dlatego też jest ona preferowana w przypadku dziedziczenia.

  2. Następny problem z dziedziczeniem polega na tym, że udostępniamy klientowi wszystkie metody rodzica. A jeśli nadklasa nie jest zaprojektowana bardzo poprawnie i zawiera luki w zabezpieczeniach. Wtedy nawet jeśli w pełni zadbamy o bezpieczeństwo w implementacji naszej podklasy, nadal będziemy zdani na wadliwą implementację klasy nadrzędnej.

    Kompozycja pomaga nam w zapewnieniu kontrolowanego dostępu do metod nadklasy, podczas gdy dziedziczenie nie utrzymuje żadnej kontroli nad jej metodami. Jest to również jedna z głównych przewag kompozycji nad dziedziczeniem.

  3. Kolejną zaletą kompozycji jest to, że zwiększa elastyczność wywoływania metod. Implementacja klasy ClassCopisanej powyżej nie jest optymalna i wykorzystuje wczesne wiązanie z wywoływaną metodą. Minimalne zmiany pozwolą nam uelastycznić wywoływanie metod i pozwolić na późne wiązanie (wiązanie w czasie wykonywania).

    ClassC.java

    package com.journaldev.inheritance;
    public class ClassC{
        SuperClass obj = null;
        public ClassC(SuperClass o){
            this.obj = o;
        }
        public void test(){
            obj.doSomething();
        }
    
        public static void main(String args[]){
            ClassC obj1 = new ClassC(new ClassA());
            ClassC obj2 = new ClassC(new ClassB());
    
            obj1.test();
            obj2.test();
        }
    }

    Powyższy program wyświetli:

    doSomething implementation of A
    doSomething implementation of B

    Ta elastyczność w wywoływaniu metod nie jest widoczna w przypadku dziedziczenia, co sprawia, że ​​najlepszym podejściem jest kompozycja.

  4. Testowanie jednostkowe jest łatwiejsze w przypadku kompozycji, ponieważ wiemy, że dla wszystkich metod używanych w nadklasie możemy wykonać testy, natomiast w przypadku dziedziczenia jesteśmy w dużym stopniu zależni od nadklasy i nie wiemy, jak działają metody klasy nadrzędnej będzie użyty. Zatem ze względu na dziedziczenie będziemy musieli przetestować wszystkie metody nadklasy, co jest niepotrzebną pracą.

    W idealnym przypadku dziedziczenie powinno być stosowane tylko wtedy, gdy relacja „ is-a ” jest prawdziwa dla klas nadrzędnych i podrzędnych, w przeciwnym razie preferowana powinna być kompozycja.

Oryginalny artykuł
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION