Szukaj…


Wprowadzenie

Dziedziczenie to podstawowa funkcja obiektowa, w której jedna klasa nabywa i rozszerza właściwości innej klasy, używając słowa kluczowego extends . Aby zapoznać się z interfejsami i implements słowa kluczowego, zobacz interfejsy .

Składnia

  • klasa Klasa B rozszerza klasę A {...}
  • klasa Klasa B implementuje interfejs A {...}
  • interfejs InterfaceB rozszerza interfejs A {...}
  • klasa ClassB rozszerza ClassA implementuje InterfaceC, InterfaceD {...}
  • Klasa abstrakcyjna AbstractClassB rozszerza klasę A {...}
  • Klasa abstrakcyjna AbstractClassB rozszerza AbstractClassA {...}
  • Klasa abstrakcyjna AbstractClassB rozszerza ClassA implementuje InterfaceC, InterfaceD {...}

Uwagi

Dziedziczenie jest często łączone z rodzajowymi, tak że klasa podstawowa ma jeden lub więcej parametrów typu. Zobacz Tworzenie klasy ogólnej .

Klasy abstrakcyjne

Klasa abstrakcyjna to klasa oznaczona abstract słowem kluczowym. W przeciwieństwie do klasy nieabstrakcyjnej może zawierać abstrakcyjne metody bez implementacji. Jednak ważne jest utworzenie klasy abstrakcyjnej bez metod abstrakcyjnych.

Nie można utworzyć instancji klasy abstrakcyjnej. Można go zaklasyfikować (rozszerzyć), o ile podklasa jest albo abstrakcyjna, albo implementuje wszystkie metody oznaczone jako abstrakcyjne przez superklasy.

Przykład klasy abstrakcyjnej:

public abstract class Component {
    private int x, y;
    
    public setPosition(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public abstract void render();
}

Klasa musi być oznaczona jako abstrakcyjna, jeśli ma co najmniej jedną metodę abstrakcyjną. Metoda abstrakcyjna to metoda, która nie ma implementacji. Inne metody można zadeklarować w klasie abstrakcyjnej, która ma implementację w celu zapewnienia wspólnego kodu dla dowolnych podklas.

Próba utworzenia tej klasy spowoduje błąd kompilacji:

//error: Component is abstract; cannot be instantiated   
Component myComponent = new Component();

Jednak klasa, która rozszerza Component i zapewnia implementację wszystkich swoich metod abstrakcyjnych, może być utworzona.

public class Button extends Component {

    @Override
    public void render() {
        //render a button
    }
}

public class TextBox extends Component {

    @Override
    public void render() {
        //render a textbox
    }
}

Instancje klas dziedziczenia można również rzutować jako klasę nadrzędną (normalne dziedziczenie) i zapewniają one efekt polimorficzny, gdy wywoływana jest metoda abstrakcyjna.

Component myButton = new Button();
Component myTextBox = new TextBox();

myButton.render(); //renders a button
myTextBox.render(); //renders a text box

Klasy abstrakcyjne a interfejsy

Klasy abstrakcyjne i interfejsy zapewniają sposób definiowania podpisów metod, jednocześnie wymagając klasy rozszerzającej / implementującej do zapewnienia implementacji.

Istnieją dwie kluczowe różnice między klasami abstrakcyjnymi a interfejsami:

  • Klasa może rozszerzać tylko jedną klasę, ale może implementować wiele interfejsów.
  • Klasa abstrakcyjna może zawierać pola instancji ( static ), ale interfejsy mogą zawierać tylko pola static .
Java SE 8

Metody zadeklarowane w interfejsach nie mogły zawierać implementacji, dlatego użyto klas abstrakcyjnych, gdy przydatne było zapewnienie dodatkowych metod, które implementacje nazywają metodami abstrakcyjnymi.

Java SE 8

Java 8 pozwala interfejsom zawierać domyślne metody , zwykle implementowane przy użyciu innych metod interfejsu , dzięki czemu interfejsy i klasy abstrakcyjne są równie wydajne pod tym względem.

Anonimowe podklasy klas abstrakcyjnych

Dla wygody java umożliwia tworzenie instancji anonimowych instancji podklas klas abstrakcyjnych, które zapewniają implementacje metod abstrakcyjnych po utworzeniu nowego obiektu. W powyższym przykładzie może to wyglądać następująco:

Component myAnonymousComponent = new Component() {
    @Override
    public void render() {
        // render a quick 1-time use component
    }
}

Dziedziczenie statyczne

Metoda statyczna może być dziedziczona podobnie jak normalne metody, jednak w przeciwieństwie do normalnych metod niemożliwe jest tworzenie metod „ abstrakcyjnych ” w celu wymuszenia zastąpienia metody statycznej. Pisanie metody z taką samą sygnaturą jak metoda statyczna w superklasie wydaje się być formą przesłonięcia, ale tak naprawdę po prostu tworzy nową funkcję, która ukrywa drugą.

public class BaseClass {
    
    public static int num = 5;

    public static void sayHello() {
        System.out.println("Hello");
    }

    public static void main(String[] args) {
        BaseClass.sayHello();
        System.out.println("BaseClass's num: " + BaseClass.num);
            
        SubClass.sayHello();
        //This will be different than the above statement's output, since it runs
        //A different method
        SubClass.sayHello(true);
        
        StaticOverride.sayHello();
        System.out.println("StaticOverride's num: " + StaticOverride.num);
    }
}

public  class SubClass extends BaseClass {
    
    //Inherits the sayHello function, but does not override it   
    public static void sayHello(boolean test) {
        System.out.println("Hey");
    }
}

public static class StaticOverride extends BaseClass {

    //Hides the num field from BaseClass
    //You can even change the type, since this doesn't affect the signature
    public static String num = "test";
        
    //Cannot use @Override annotation, since this is static
    //This overrides the sayHello method from BaseClass
    public static void sayHello() {
        System.out.println("Static says Hi");
    }

}

Uruchomienie dowolnej z tych klas daje wynik:

Hello
BaseClass's num: 5
Hello
Hey
Static says Hi
StaticOverride's num: test

Zauważ, że w przeciwieństwie do zwykłego dziedziczenia, w dziedziczeniu statycznym metody nie są ukryte. Zawsze możesz wywołać podstawową sayHello za pomocą BaseClass.sayHello() . Ale klasy dziedziczą metody statyczne, jeśli w podklasie nie znaleziono żadnych metod o tej samej sygnaturze. Jeśli podpisy dwóch metod różnią się, obie metody można uruchomić z podklasy, nawet jeśli nazwa jest taka sama.

Pola statyczne ukrywają się w podobny sposób.

Użycie „ostatecznego” w celu ograniczenia dziedziczenia i zastąpienia

Klasy końcowe

W przypadku użycia w deklaracji class final modyfikator zapobiega zadeklarowaniu innych klas, które extend klasę. Klasa final jest klasą „liścia” w hierarchii klas spadkowych.

// This declares a final class
final class MyFinalClass {
    /* some code */
}

// Compilation error: cannot inherit from final MyFinalClass
class MySubClass extends MyFinalClass {
    /* more code */
}

Przypadki użycia dla klas końcowych

Klasy końcowe można łączyć z private konstruktorem w celu kontrolowania lub zapobiegania tworzeniu instancji klasy. Można to wykorzystać do stworzenia tak zwanej „klasy użytkowej”, która definiuje tylko elementy statyczne; tj. stałe i metody statyczne.

public final class UtilityClass {

    // Private constructor to replace the default visible constructor
    private UtilityClass() {}

    // Static members can still be used as usual
    public static int doSomethingCool() {
        return 123;
    }

}

Niezmienne klasy należy również zadeklarować jako final . (Niezmienna klasa to taka, której instancji nie można zmienić po ich utworzeniu; zobacz temat I mmutable Objects .) W ten sposób uniemożliwisz utworzenie mutowalnej podklasy niezmiennej klasy. Byłoby to sprzeczne z zasadą substytucji Liskowa, która wymaga, aby podtyp był posłuszny „umowie behawioralnej” swoich nadtypów.

Z praktycznego punktu widzenia uznanie niezmiennej klasy za final ułatwia rozumowanie o zachowaniu programu. Rozwiązuje również problemy związane z bezpieczeństwem w scenariuszu, w którym niezaufany kod jest wykonywany w bezpiecznym obszarze izolowanym. (Na przykład, ponieważ String jest zadeklarowany jako final , zaufana klasa nie musi się martwić, że może zostać oszukiwana, aby zaakceptować zmienną podklasę, którą niezaufany rozmówca mógłby następnie potajemnie zmienić).

Wadą klas final jest to, że nie działają one z niektórymi frameworkami, takimi jak Mockito. Aktualizacja: Mockito wersja 2 obsługuje teraz drwiny z końcowych klas.

Ostateczne metody

final modyfikator można również zastosować do metod, aby zapobiec ich zastąpieniu w podklasach:

public class MyClassWithFinalMethod {

    public final void someMethod() {
    }
}

public class MySubClass extends MyClassWithFinalMethod {

    @Override
    public void someMethod() { // Compiler error (overridden method is final)
    }
}

Ostateczne metody są zwykle używane, gdy chcesz ograniczyć to, co podklasa może zmienić w klasie bez całkowitego zakazania podklas.


final modyfikator można również zastosować do zmiennych, ale znaczenie zmiennej final dla zmiennych nie ma związku z dziedziczeniem.

Zasada substytucji Liskowa

Podstawialność jest zasadą w programowaniu obiektowym wprowadzonym przez Barbarę Liskov w wygłoszonym na konferencji w 1987 roku stwierdzeniu, że jeśli klasa B jest podklasą klasy A , to gdziekolwiek można się spodziewać A , można zamiast niej użyć B :

class A {...}
class B extends A {...}

public void method(A obj) {...}

A a = new B(); // Assignment OK
method(new B()); // Passing as parameter OK

Dotyczy to również sytuacji, gdy typ jest interfejsem, w którym nie ma potrzeby hierarchicznej relacji między obiektami:

interface Foo {
    void bar();
}

class A implements Foo {
    void bar() {...}
}

class B implements Foo {
    void bar() {...}
}

List<Foo> foos = new ArrayList<>();
foos.add(new A()); // OK
foos.add(new B()); // OK

Teraz lista zawiera obiekty, które nie należą do tej samej hierarchii klas.

Dziedzictwo

Przy użyciu słowa kluczowego extends wśród klas wszystkie właściwości nadklasy (znanej również jako klasa nadrzędna lub klasa podstawowa ) są obecne w podklasie (znanej również jako klasa potomna lub klasa pochodna )

public class BaseClass {

    public void baseMethod(){
        System.out.println("Doing base class stuff");
    }
}

public class SubClass extends BaseClass {

}

Przypadki SubClass odziedziczyły metodę wytworzono baseMethod() :

SubClass s = new SubClass();
s.baseMethod();  //Valid, prints "Doing base class stuff"

Dodatkową treść można dodać do podklasy. Pozwala to na dodatkową funkcjonalność w podklasie bez żadnych zmian w klasie podstawowej lub jakichkolwiek innych podklasach z tej samej klasy podstawowej:

public class Subclass2 extends BaseClass {

    public void anotherMethod() {
        System.out.println("Doing subclass2 stuff");
    }
}

Subclass2 s2 = new Subclass2();
s2.baseMethod(); //Still valid , prints "Doing base class stuff"
s2.anotherMethod(); //Also valid, prints "Doing subclass2 stuff" 

Pola są również dziedziczone:

public class BaseClassWithField {

    public int x;

}

public class SubClassWithField extends BaseClassWithField {

    public SubClassWithField(int x) {
        this.x = x; //Can access fields
    }
}

private pola i metody nadal istnieją w podklasie, ale nie są dostępne:

public class BaseClassWithPrivateField {

    private int x = 5;

    public int getX() {
        return x;
    }
}

public class SubClassInheritsPrivateField extends BaseClassWithPrivateField {

    public void printX() {
        System.out.println(x); //Illegal, can't access private field x
        System.out.println(getX()); //Legal, prints 5
    }
}

SubClassInheritsPrivateField s = new SubClassInheritsPrivateField();
int x = s.getX(); //x will have a value of 5.

W Javie każda klasa może obejmować maksymalnie jedną inną klasę.

public class A{}
public class B{}
public class ExtendsTwoClasses extends A, B {} //Illegal

Jest to znane jako wielokrotne dziedziczenie i chociaż w niektórych językach jest legalne, Java nie zezwala na to w przypadku klas.

W rezultacie każda klasa ma nierozgałęziony łańcuch rodowy klas prowadzący do Object , z którego wywodzą się wszystkie klasy.

Dziedziczenie i metody statyczne

W Javie klasa nadrzędna i podrzędna mogą mieć metody statyczne o tej samej nazwie. Ale w takich przypadkach implementacja metody statycznej w potomku ukrywa implementację klasy nadrzędnej, nie jest to przesłanianie metody. Na przykład:

class StaticMethodTest {

  // static method and inheritance
  public static void main(String[] args) {
    Parent p = new Child();
    p.staticMethod(); // prints Inside Parent
    ((Child) p).staticMethod(); // prints Inside Child
  }

  static class Parent {
    public static void staticMethod() {
      System.out.println("Inside Parent");
    }
  }

  static class Child extends Parent {
    public static void staticMethod() {
      System.out.println("Inside Child");
    }
  }
}

Metody statyczne są powiązane z klasą, a nie z instancją, a to wiązanie metody odbywa się w czasie kompilacji. Ponieważ w pierwszym wywołaniu staticMethod() , rodzic referencyjna klasa p użyto, Parent jest wersja staticMethod() jest wywoływana. W drugim przypadku, zrobiliśmy rzucania p do Child klasy Child „s staticMethod() wykonywane.

Zmienne cieniowanie

Zmienne są Zacieniowane, a metody NADMIERNE. Która zmienna zostanie użyta, zależy od klasy, dla której zmienna jest zadeklarowana. Wybór metody zależy od faktycznej klasy obiektu, do którego odwołuje się zmienna.

class Car {
    public int gearRatio = 8;

    public String accelerate() {
        return "Accelerate : Car";
    }
}

class SportsCar extends Car {
    public int gearRatio = 9;

    public String accelerate() {
        return "Accelerate : SportsCar";
    }

    public void test() {

    }


    public static void main(String[] args) {

        Car car = new SportsCar();
        System.out.println(car.gearRatio + "  " + car.accelerate());
        // will print out 8  Accelerate : SportsCar
    }
}

Zawężanie i poszerzanie odniesień do obiektów

Rzutowanie instancji klasy bazowej do podklasy, jak w: b = (B) a; nazywa się zawężaniem (gdy próbujesz zawęzić obiekt klasy bazowej do bardziej konkretnego obiektu klasy) i wymaga jawnego rzutowania typu.

Rzutowanie instancji podklasy do klasy bazowej jak w: A a = b; nazywa się poszerzeniem i nie wymaga rzutowania.

Aby to zilustrować, weź pod uwagę następujące deklaracje klas i kod testowy:

class Vehicle {
}

class Car extends Vehicle {
}

class Truck extends Vehicle {
}

class MotorCycle extends Vehicle {
}

class Test {

    public static void main(String[] args) {
    
        Vehicle vehicle = new Car();
        Car car = new Car();        
    
        vehicle = car; // is valid, no cast needed

        Car c = vehicle // not valid
        Car c = (Car) vehicle; //valid
    }
}

Instrukcja Vehicle vehicle = new Car(); jest prawidłową instrukcją Java. Każde wystąpienie Car jest również Vehicle . Dlatego przypisanie jest legalne bez potrzeby jawnego rzutowania tekstu.

Z drugiej strony, Car c = vehicle; nie jest poprawny. Statyczny typ zmiennej vehicle to Vehicle co oznacza, że może odnosić się do instancji Car , Truck , MotorCycle , or any other current or future subclass of Vehicle . (Or indeed, an instance of Pojazdu itself, since we did not declare it as an class.) The assignment cannot be allowed, since that might lead to abstrakcyjnej class.) The assignment cannot be allowed, since that might lead to referring to a samochodu referring to a instancji Ciężarówki.

Aby zapobiec tej sytuacji, musimy dodać jawną rzutowanie tekstu:

Car c = (Car) vehicle;

Rzutowanie mówi kompilatorowi, że spodziewamy się, że wartość vehicle będzie Car lub podklasą Car . W razie potrzeby kompilator wstawi kod, aby wykonać sprawdzenie typu w czasie wykonywania. Jeśli sprawdzenie się nie powiedzie, ClassCastException zostanie ClassCastException podczas wykonywania kodu.

Pamiętaj, że nie wszystkie rzutowania są prawidłowe. Na przykład:

String s = (String) vehicle;  // not valid

Kompilator Java wie, że instancja zgodna z typem Vehicle nie może być nigdy zgodna z String . Rzutowanie typu nigdy nie mogłoby się powieść, a JLS nakazuje, że spowoduje to błąd kompilacji.

Programowanie do interfejsu

Ideą programowania interfejsu jest oparcie kodu przede wszystkim na interfejsach i używanie konkretnych klas tylko w momencie tworzenia instancji. W tym kontekście dobry kod zajmujący się np. Kolekcjami Java będzie wyglądał mniej więcej tak (nie że sama metoda jest w ogóle przydatna, tylko ilustracja):

public <T> Set<T> toSet(Collection<T> collection) {
  return Sets.newHashSet(collection);
}

podczas gdy zły kod może wyglądać następująco:

public <T> HashSet<T> toSet(ArrayList<T> collection) {
  return Sets.newHashSet(collection);
}

Nie tylko ten pierwszy można zastosować do szerszego wyboru argumentów, jego wyniki będą bardziej kompatybilne z kodem dostarczanym przez innych programistów, którzy zasadniczo stosują się do koncepcji programowania interfejsu. Najważniejsze jednak powody, dla których warto skorzystać z tego pierwszego, to:

  • przez większość czasu kontekst, w którym wynik jest wykorzystywany, nie potrzebuje i nie powinien wymagać tak wielu szczegółów, jak zapewnia konkretna implementacja;
  • przestrzeganie interfejsu wymusza czystszy kod i mniej hacków, takich jak kolejna metoda publiczna dodawana do klasy obsługującej określony scenariusz;
  • kod jest bardziej testowalny, ponieważ interfejsy można łatwo wyśmiewać;
  • wreszcie koncepcja pomaga, nawet jeśli oczekuje się tylko jednej implementacji (przynajmniej dla testowalności).

Jak więc łatwo zastosować koncepcję programowania do interfejsu podczas pisania nowego kodu, mając na uwadze jedną konkretną implementację? Jedną z powszechnie używanych opcji jest kombinacja następujących wzorów:

  • programowanie do interfejsu
  • fabryka
  • budowniczy

Poniższy przykład oparty na tych zasadach jest uproszczoną i okrojoną wersją implementacji RPC napisaną dla wielu różnych protokołów:

public interface RemoteInvoker {
  <RQ, RS> CompletableFuture<RS> invoke(RQ request, Class<RS> responseClass);
}

Powyższy interfejs nie powinien być tworzony bezpośrednio przez fabrykę, zamiast tego otrzymujemy bardziej konkretne interfejsy, jeden do wywołania HTTP i jeden do AMQP, z których każdy ma fabrykę i konstruktor do konstruowania instancji, które z kolei są również instancjami powyższy interfejs:

public interface AmqpInvoker extends RemoteInvoker {
  static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
    return new AmqpInvokerBuilder(instanceId, factory);
  }
}

Instancje RemoteInvoker do użytku z AMQP mogą być teraz konstruowane tak łatwo (lub bardziej zaangażowane w zależności od konstruktora):

RemoteInvoker invoker = AmqpInvoker.with(instanceId, factory)
  .requestRouter(router)
  .build();

Wywołanie żądania jest tak proste, jak:

Response res = invoker.invoke(new Request(data), Response.class).get();

Ponieważ Java 8 pozwala na umieszczanie metod statycznych bezpośrednio w interfejsach, pośrednia fabryka stała się domyślna w powyższym kodzie zastąpionym przez AmqpInvoker.with() . W Javie przed wersją 8 ten sam efekt można osiągnąć dzięki wewnętrznej klasie Factory :

public interface AmqpInvoker extends RemoteInvoker {
  class Factory {
    public static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
      return new AmqpInvokerBuilder(instanceId, factory);
    }
  }
}

Odpowiednia instancja zmieniłaby się w:

RemoteInvoker invoker = AmqpInvoker.Factory.with(instanceId, factory)
  .requestRouter(router)
  .build();

Powyższy konstruktor może wyglądać tak (chociaż jest to uproszczenie, ponieważ pozwala ono zdefiniować do 15 parametrów odbiegających od domyślnych). Zauważ, że konstrukcja nie jest publiczna, więc jest szczególnie użyteczna tylko z powyższego interfejsu AmqpInvoker :

public class AmqpInvokerBuilder {
  ...
  AmqpInvokerBuilder(String instanceId, ConnectionFactory factory) {
    this.instanceId = instanceId;
    this.factory = factory;
  }

  public AmqpInvokerBuilder requestRouter(RequestRouter requestRouter) {
    this.requestRouter = requestRouter;
    return this;
  }

  public AmqpInvoker build() throws TimeoutException, IOException {
    return new AmqpInvokerImpl(instanceId, factory, requestRouter);
  }
}

Ogólnie rzecz biorąc, konstruktor można również wygenerować za pomocą narzędzia takiego jak FreeBuilder.

Wreszcie standardowa (i jedyna oczekiwana) implementacja tego interfejsu jest zdefiniowana jako klasa lokalna dla pakietu w celu wymuszenia użycia interfejsu, fabryki i konstruktora:

class AmqpInvokerImpl implements AmqpInvoker {
  AmqpInvokerImpl(String instanceId, ConnectionFactory factory, RequestRouter requestRouter) {
    ...
  }

  @Override
  public <RQ, RS> CompletableFuture<RS> invoke(final RQ request, final Class<RS> respClass) {
    ...
  }
}

Tymczasem wzorzec ten okazał się bardzo wydajny w opracowywaniu całego naszego nowego kodu, bez względu na to, jak prosta lub złożona jest jego funkcjonalność.

Klasa abstrakcyjna i użycie interfejsu: funkcja „Is-a” vs. „Has-a”

Kiedy stosować klasy abstrakcyjne: Aby zaimplementować to samo lub inne zachowanie wśród wielu powiązanych obiektów

Kiedy używać interfejsów: do realizacji umowy przez wiele niepowiązanych obiektów

Klasy abstrakcyjne tworzą „to” relacje, podczas gdy interfejsy zapewniają „ma możliwość”.

Można to zobaczyć w poniższym kodzie:

public class InterfaceAndAbstractClassDemo{
    public static void main(String args[]){
        
        Dog dog = new Dog("Jack",16);
        Cat cat = new Cat("Joe",20);
            
        System.out.println("Dog:"+dog);
        System.out.println("Cat:"+cat);
        
        dog.remember();
        dog.protectOwner();
        Learn dl = dog;
        dl.learn();
                
        cat.remember();
        cat.protectOwner();
        
        Climb c = cat;
        c.climb();
        
        Man man = new Man("Ravindra",40);
        System.out.println(man);
        
        Climb cm = man;
        cm.climb();
        Think t = man;
        t.think();
        Learn l = man;
        l.learn();
        Apply a = man;
        a.apply();
    }
}

abstract class Animal{
    String name;
    int lifeExpentency;
    public Animal(String name,int lifeExpentency ){
        this.name = name;
        this.lifeExpentency=lifeExpentency;
    }
    public abstract void remember();
    public abstract void protectOwner();
    
    public String toString(){
        return this.getClass().getSimpleName()+":"+name+":"+lifeExpentency;
    }
}
class Dog extends Animal implements Learn{
    
    public Dog(String name,int age){
        super(name,age);
    }
    public void remember(){
        System.out.println(this.getClass().getSimpleName()+" can remember for 5 minutes");
    }
    public void protectOwner(){
        System.out.println(this.getClass().getSimpleName()+ " will protect owner");
    }
    public void learn(){
        System.out.println(this.getClass().getSimpleName()+ " can learn:");
    }
}
class Cat extends Animal implements Climb {
    public Cat(String name,int age){
        super(name,age);
    }
    public void remember(){
        System.out.println(this.getClass().getSimpleName() + " can remember for 16 hours");
    }
    public void protectOwner(){
        System.out.println(this.getClass().getSimpleName()+ " won't protect owner");
    }
    public void climb(){
        System.out.println(this.getClass().getSimpleName()+ " can climb");
    }
}
interface Climb{
    void climb();
}
interface Think {
    void think();
}

interface Learn {
    void learn();
}
interface Apply{
    void apply();
}

class Man implements Think,Learn,Apply,Climb{
    String name;
    int age;

    public Man(String name,int age){
        this.name = name;
        this.age = age;
    }
    public void think(){
        System.out.println("I can think:"+this.getClass().getSimpleName());
    }
    public void learn(){
        System.out.println("I can learn:"+this.getClass().getSimpleName());
    }
    public void apply(){
        System.out.println("I can apply:"+this.getClass().getSimpleName());
    }
    public void climb(){
        System.out.println("I can climb:"+this.getClass().getSimpleName());
    }
    public String toString(){
        return "Man :"+name+":Age:"+age;
    }
}

wynik:

Dog:Dog:Jack:16
Cat:Cat:Joe:20
Dog can remember for 5 minutes
Dog will protect owner
Dog can learn:
Cat can remember for 16 hours
Cat won't protect owner
Cat can climb
Man :Ravindra:Age:40
I can climb:Man
I can think:Man
I can learn:Man
I can apply:Man

Kluczowe uwagi:

  1. Animal to klasa abstrakcyjna z wspólnymi atrybutami: name i lifeExpectancy i metodami abstrakcyjnymi: remember() i protectOwner() . Dog i Cat to Animals , które wdrożyły metody remember() i protectOwner() .

  2. Cat może się climb() ale Dog nie. Dog może think() ale Cat nie. Te konkretne możliwości są dodawane do Cat and Dog poprzez wdrożenie.

  3. Man nie jest Animal ale potrafi Think , Learn , Apply i Climb .

  4. Cat nie jest Man ale potrafi się Climb .

  5. Dog nie jest Man ale może się Learn

  6. Man nie jest ani Cat ani Dog ale może mieć pewne zdolności dwóch ostatnich bez rozszerzania Animal , Cat lub Dog . Odbywa się to za pomocą interfejsów.

  7. Mimo że Animal jest klasą abstrakcyjną, ma konstruktor, w przeciwieństwie do interfejsu.

TL; DR:

Klasy niezwiązane mogą mieć możliwości za pośrednictwem interfejsów, ale klasy pokrewne zmieniają zachowanie poprzez rozszerzenie klas podstawowych.

Zapoznaj się ze stroną dokumentacji Java, aby dowiedzieć się, którego użyć w konkretnym przypadku użycia.

Rozważ użycie klas abstrakcyjnych, jeśli ...

  1. Chcesz dzielić kod między kilkoma blisko spokrewnionymi klasami.
  2. Oczekujesz, że klasy, które rozszerzają twoją klasę abstrakcyjną, mają wiele wspólnych metod lub pól lub wymagają modyfikatorów dostępu innych niż publiczne (takie jak chronione i prywatne).
  3. Chcesz zadeklarować pola niestatyczne lub nie-końcowe.

Rozważ użycie interfejsów, jeśli ...

  1. Oczekujesz, że niepowiązane klasy zaimplementują Twój interfejs. Na przykład wiele niepowiązanych obiektów może implementować interfejs Serializable .
  2. Chcesz określić zachowanie określonego typu danych, ale nie martwisz się, kto wdraża to zachowanie.
  3. Chcesz skorzystać z wielokrotnego dziedziczenia typu.

Przesłanianie dziedziczenia

Przesłanianie w dziedziczeniu jest używane, gdy używasz już zdefiniowanej metody z superklasy w podklasie, ale w inny sposób niż sposób, w jaki metoda została pierwotnie zaprojektowana w superklasie. Przesłonięcie pozwala użytkownikowi ponownie wykorzystać kod, wykorzystując istniejący materiał i modyfikując go, aby lepiej odpowiadał potrzebom użytkownika.


Poniższy przykład pokazuje, w jaki sposób ClassB zastępuje funkcjonalność ClassA , zmieniając to, co jest wysyłane za pomocą metody drukowania:

Przykład:

public static void main(String[] args) {
    ClassA a = new ClassA();
    ClassA b = new ClassB();
    a.printing();
    b.printing();
}

class ClassA {
    public void printing() {        
        System.out.println("A");
    }
}

class ClassB extends ClassA {
    public void printing() {
         System.out.println("B");
    }
}

Wynik:

ZA

b



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow