Ръководство за наследяване в Java

1. Общ преглед

Един от основните принципи на обектно-ориентираното програмиране - наследяването - ни позволява да използваме повторно съществуващия код или да разширим съществуващ тип.

Просто казано, в Java клас може да наследи друг клас и множество интерфейси, докато интерфейсът може да наследи други интерфейси.

В тази статия ще започнем с необходимостта от наследяване, като преминем към начина, по който наследяването работи с класове и интерфейси.

След това ще разгледаме как имената на променливите / методите и модификаторите на достъпа влияят върху наследените членове.

И накрая ще видим какво означава наследяване на тип.

2. Необходимостта от наследяване

Представете си, че като производител на автомобили предлагате множество модели автомобили на своите клиенти. Въпреки че различните модели автомобили могат да предлагат различни функции като люк или бронирани прозорци, всички те ще включват общи компоненти и функции, като двигател и джанти.

Има смисъл да се създаде основен дизайн и да се разшири, за да се създадат техни специализирани версии, вместо да се проектира всеки модел автомобил поотделно, от нулата.

По подобен начин, с наследяване, можем да създадем клас с основни характеристики и поведение и да създадем неговите специализирани версии, чрез създаване на класове, които наследяват този основен клас. По същия начин интерфейсите могат да разширят съществуващите интерфейси.

Ще забележим използването на множество термини за препращане към тип, който се наследява от друг тип, по-специално:

  • базовият тип се нарича още супер или родителски тип
  • производен тип се означава като разширен, под или подчинен тип

3. Наследяване на класа

3.1. Разширяване на клас

Клас може да наследи друг клас и да дефинира допълнителни членове.

Нека започнем с дефиниране на базов клас Car :

public class Car { int wheels; String model; void start() { // Check essential parts } }

Класът ArmoredCar може да наследи членовете на класа Car, като използва ключовата дума extends в декларацията си :

public class ArmoredCar extends Car { int bulletProofWindows; void remoteStartCar() { // this vehicle can be started by using a remote control } }

Сега можем да кажем, че класът ArmoredCar е подклас на Car, а последният е суперклас на ArmoredCar.

Класовете в Java поддържат единично наследяване ; на ArmoredCar класа не може да се разшири множество класове.

Също така имайте предвид, че при липса на ключова дума extends , клас неявно наследява клас java.lang.Object .

Клас подклас наследява нестатичните защитени и публични членове от класа суперклас. Освен това членовете с достъп по подразбиране и пакет се наследяват, ако двата класа са в един и същ пакет.

От друга страна, частните и статичните членове на клас не се наследяват.

3.2. Достъп до родители от детски клас

За достъп до наследени свойства или методи можем просто да ги използваме директно:

public class ArmoredCar extends Car { public String registerModel() { return model; } }

Имайте предвид, че не се нуждаем от препратка към суперкласа за достъп до неговите членове.

4. Наследяване на интерфейса

4.1. Внедряване на множество интерфейси

Въпреки че класовете могат да наследят само един клас, те могат да реализират множество интерфейси.

Представете си, че ArmoredCar , който дефинирахме в предходния раздел, е необходим за супер шпионин. Така че компанията за производство на автомобили е помислила да добави летяща и плаваща функционалност:

public interface Floatable { void floatOnWater(); }
public interface Flyable { void fly(); }
public class ArmoredCar extends Car implements Floatable, Flyable{ public void floatOnWater() { System.out.println("I can float!"); } public void fly() { System.out.println("I can fly!"); } }

В горния пример забелязваме използването на ключовите думи реализира за наследяване от интерфейс.

4.2. Проблеми с множествено наследяване

Java позволява множествено наследяване с помощта на интерфейси.

До Java 7 това не беше проблем. Интерфейсите могат да дефинират само абстрактни методи, тоест методи без никакво изпълнение. Така че, ако даден клас е реализирал множество интерфейси с един и същ подпис на метода, това не е проблем. В крайна сметка внедряващият клас имаше само един метод за изпълнение.

Нека да видим как това просто уравнение се промени с въвеждането на методи по подразбиране в интерфейсите с Java 8.

Започвайки с Java 8, интерфейсите могат да изберат да дефинират реализации по подразбиране за своите методи (интерфейсът все още може да дефинира абстрактни методи). Това означава, че ако клас реализира множество интерфейси, които дефинират методи с един и същ подпис, дъщерният клас ще наследи отделни реализации. Това звучи сложно и не е позволено.

Java забранява наследяването на множество реализации на едни и същи методи, дефинирани в отделни интерфейси.

Ето пример:

public interface Floatable { default void repair() { System.out.println("Repairing Floatable object"); } }
public interface Flyable { default void repair() { System.out.println("Repairing Flyable object"); } }
public class ArmoredCar extends Car implements Floatable, Flyable { // this won't compile }

Ако искаме да приложим и двата интерфейса, ще трябва да заменим метода repair () .

Ако интерфейсите в предходните примери дефинират променливи с едно и също име, да кажем продължителност , не можем да ги осъществим, без да предшестваме името на променливата с името на интерфейса:

public interface Floatable { int duration = 10; }
public interface Flyable { int duration = 20; }
public class ArmoredCar extends Car implements Floatable, Flyable { public void aMethod() { System.out.println(duration); // won't compile System.out.println(Floatable.duration); // outputs 10 System.out.println(Flyable.duration); // outputs 20 } }

4.3. Interfaces Extending Other Interfaces

An interface can extend multiple interfaces. Here's an example:

public interface Floatable { void floatOnWater(); }
interface interface Flyable { void fly(); }
public interface SpaceTraveller extends Floatable, Flyable { void remoteControl(); }

An interface inherits other interfaces by using the keyword extends. Classes use the keyword implements to inherit an interface.

5. Inheriting Type

When a class inherits another class or interfaces, apart from inheriting their members, it also inherits their type. This also applies to an interface that inherits other interfaces.

This is a very powerful concept, which allows developers to program to an interface (base class or interface), rather than programming to their implementations.

For example, imagine a condition, where an organization maintains a list of the cars owned by its employees. Of course, all employees might own different car models. So how can we refer to different car instances? Here's the solution:

public class Employee { private String name; private Car car; // standard constructor }

Because all derived classes of Car inherit the type Car, the derived class instances can be referred by using a variable of class Car:

Employee e1 = new Employee("Shreya", new ArmoredCar()); Employee e2 = new Employee("Paul", new SpaceCar()); Employee e3 = new Employee("Pavni", new BMW());

6. Hidden Class Members

6.1. Hidden Instance Members

What happens if both the superclass and subclass define a variable or method with the same name? Don't worry; we can still access both of them. However, we must make our intent clear to Java, by prefixing the variable or method with the keywords this or super.

The this keyword refers to the instance in which it's used. The super keyword (as it seems obvious) refers to the parent class instance:

public class ArmoredCar extends Car { private String model; public String getAValue() { return super.model; // returns value of model defined in base class Car // return this.model; // will return value of model defined in ArmoredCar // return model; // will return value of model defined in ArmoredCar } }

A lot of developers use this and super keywords to explicitly state which variable or method they're referring to. However, using them with all members can make our code look cluttered.

6.2. Hidden Static Members

What happens when our base class and subclasses define static variables and methods with the same name? Can we access a static member from the base class, in the derived class, the way we do for the instance variables?

Let's find out using an example:

public class Car { public static String msg() { return "Car"; } }
public class ArmoredCar extends Car { public static String msg() { return super.msg(); // this won't compile. } }

No, we can't. The static members belong to a class and not to instances. So we can't use the non-static super keyword in msg().

Since static members belong to a class, we can modify the preceding call as follows:

return Car.msg();

Consider the following example, in which both the base class and derived class define a static method msg() with the same signature:

public class Car { public static String msg() { return "Car"; } }
public class ArmoredCar extends Car { public static String msg() { return "ArmoredCar"; } }

Here's how we can call them:

Car first = new ArmoredCar(); ArmoredCar second = new ArmoredCar();

For the preceding code, first.msg() will output “Car and second.msg() will output “ArmoredCar”. The static message that is called depends on the type of the variable used to refer to ArmoredCar instance.

7. Conclusion

В тази статия разгледахме основен аспект на езика Java - наследяването.

Видяхме как Java поддържа единично наследяване с класове и множествено наследяване с интерфейси и обсъдихме тънкостите на това как механизмът работи в езика.

Както винаги, пълният изходен код за примерите е достъпен в GitHub.