Java Language
Dziedzictwo
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 polastatic
.
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 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:
Animal
to klasa abstrakcyjna z wspólnymi atrybutami:name
ilifeExpectancy
i metodami abstrakcyjnymi:remember()
iprotectOwner()
.Dog
iCat
toAnimals
, które wdrożyły metodyremember()
iprotectOwner()
.Cat
może sięclimb()
aleDog
nie.Dog
możethink()
aleCat
nie. Te konkretne możliwości są dodawane doCat
andDog
poprzez wdrożenie.Man
nie jestAnimal
ale potrafiThink
,Learn
,Apply
iClimb
.Cat
nie jestMan
ale potrafi sięClimb
.Dog
nie jestMan
ale może sięLearn
Man
nie jest aniCat
aniDog
ale może mieć pewne zdolności dwóch ostatnich bez rozszerzaniaAnimal
,Cat
lubDog
. Odbywa się to za pomocą interfejsów.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 ...
- Chcesz dzielić kod między kilkoma blisko spokrewnionymi klasami.
- 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).
- Chcesz zadeklarować pola niestatyczne lub nie-końcowe.
Rozważ użycie interfejsów, jeśli ...
- Oczekujesz, że niepowiązane klasy zaimplementują Twój interfejs. Na przykład wiele niepowiązanych obiektów może implementować interfejs
Serializable
. - Chcesz określić zachowanie określonego typu danych, ale nie martwisz się, kto wdraża to zachowanie.
- 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