Наследование против состава: как выбрать

Наследование и композиция - это два метода программирования, которые разработчики используют для установления отношений между классами и объектами. В то время как наследование наследует один класс от другого, композиция определяет класс как сумму его частей.

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

Поскольку слабосвязанный код предлагает большую гибкость, многие разработчики узнали, что композиция - лучший метод, чем наследование, но на самом деле все гораздо сложнее. Выбор инструмента для программирования аналогичен выбору правильного кухонного инструмента: вы не будете использовать нож для масла, чтобы нарезать овощи, и точно так же вы не должны выбирать композицию для каждого сценария программирования. 

В этом Java Challenger вы узнаете разницу между наследованием и композицией и узнаете, как решить, что подходит для вашей программы. Далее я познакомлю вас с несколькими важными, но сложными аспектами наследования Java: переопределением методов, superключевым словом и приведением типов. Наконец, вы протестируете то, что вы узнали, построчно просматривая пример наследования, чтобы определить, каким должен быть результат.

Когда использовать наследование в Java

В объектно-ориентированном программировании мы можем использовать наследование, когда знаем, что между дочерним и его родительским классом существует связь «есть». Вот несколько примеров:

  • Человек - это человек.
  • Кошка - это животное.
  • Автомобиль - это   средство передвижения.

В каждом случае дочерний класс или подкласс - это специализированная версия родительского или суперкласса. Наследование от суперкласса является примером повторного использования кода. Чтобы лучше понять эту взаимосвязь, уделите время изучению Carкласса, который наследуется от Vehicle:

 class Vehicle { String brand; String color; double weight; double speed; void move() { System.out.println("The vehicle is moving"); } } public class Car extends Vehicle { String licensePlateNumber; String owner; String bodyStyle; public static void main(String... inheritanceExample) { System.out.println(new Vehicle().brand); System.out.println(new Car().brand); new Car().move(); } } 

Когда вы думаете об использовании наследования, спросите себя, действительно ли подкласс является более специализированной версией суперкласса. В этом случае автомобиль - это тип транспортного средства, поэтому отношения наследования имеют смысл. 

Когда использовать композицию в Java

В объектно-ориентированном программировании мы можем использовать композицию в случаях, когда один объект «имеет» (или является частью) другой объект. Вот несколько примеров:

  • В автомобиле есть аккумулятор (аккумулятор - это часть автомобиля).
  • У человека есть сердце (сердце - это часть человека).
  • В доме есть гостиная (гостиная является частью дома).

Чтобы лучше понять этот тип отношений, рассмотрим состав House:

 public class CompositionExample { public static void main(String... houseComposition) { new House(new Bedroom(), new LivingRoom()); // The house now is composed with a Bedroom and a LivingRoom } static class House { Bedroom bedroom; LivingRoom livingRoom; House(Bedroom bedroom, LivingRoom livingRoom) { this.bedroom = bedroom; this.livingRoom = livingRoom; } } static class Bedroom { } static class LivingRoom { } } 

В этом случае, мы знаем , что дом имеет гостиную и спальню, так что мы можем использовать Bedroomи  LivingRoomобъекты в композиции а House

Получить код

Получите исходный код примеров в этом Java Challenger. Следуя примерам, вы можете запускать свои собственные тесты.

Наследование против композиции: два примера

Рассмотрим следующий код. Это хороший пример наследования?

 import java.util.HashSet; public class CharacterBadExampleInheritance extends HashSet { public static void main(String... badExampleOfInheritance) { BadExampleInheritance badExampleInheritance = new BadExampleInheritance(); badExampleInheritance.add("Homer"); badExampleInheritance.forEach(System.out::println); } 

В этом случае ответ отрицательный. Дочерний класс наследует множество методов, которые он никогда не будет использовать, в результате чего получается тесно связанный код, который одновременно сбивает с толку и трудно поддерживать. Если приглядеться, также становится ясно, что этот код не проходит проверку «is a».

Теперь давайте попробуем тот же пример с использованием композиции:

 import java.util.HashSet; import java.util.Set; public class CharacterCompositionExample { static Set set = new HashSet(); public static void main(String... goodExampleOfComposition) { set.add("Homer"); set.forEach(System.out::println); } 

Использование композиции для этого сценария позволяет  CharacterCompositionExampleклассу использовать только два HashSetметода без наследования всех из них. Это приводит к более простому и менее связанному коду, который будет легче понять и поддерживать.

Примеры наследования в JDK

В Java Development Kit есть множество хороших примеров наследования:

 class IndexOutOfBoundsException extends RuntimeException {...} class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {...} class FileWriter extends OutputStreamWriter {...} class OutputStreamWriter extends Writer {...} interface Stream extends BaseStream
    
      {...} 
    

Обратите внимание, что в каждом из этих примеров дочерний класс является специализированной версией своего родителя; например, IndexOutOfBoundsExceptionэто тип RuntimeException.

Переопределение метода с помощью наследования Java

Наследование позволяет нам повторно использовать методы и другие атрибуты одного класса в новом классе, что очень удобно. Но для того, чтобы наследование действительно работало, мы также должны иметь возможность изменять некоторые из унаследованного поведения в нашем новом подклассе. Например, мы можем выделить звук, который Catиздает a :

 class Animal { void emitSound() { System.out.println("The animal emitted a sound"); } } class Cat extends Animal { @Override void emitSound() { System.out.println("Meow"); } } class Dog extends Animal { } public class Main { public static void main(String... doYourBest) { Animal cat = new Cat(); // Meow Animal dog = new Dog(); // The animal emitted a sound Animal animal = new Animal(); // The animal emitted a sound cat.emitSound(); dog.emitSound(); animal.emitSound(); } } 

Это пример наследования Java с переопределением метода. Во- первых, мы простираться в Animalкласс , чтобы создать новый Catкласс. Затем мы переопределяем метод Animalкласса, emitSound()чтобы получить определенный звук Cat. Несмотря на то, что мы объявили тип класса как Animal, когда мы создадим его экземпляр, Catмы получим кошачье мяуканье. 

Переопределение метода - это полиморфизм

You might remember from my last post that method overriding is an example of polymorphism, or virtual method invocation.

Does Java have multiple inheritance?

Unlike some languages, such as C++, Java does not allow multiple inheritance with classes. You can use multiple inheritance with interfaces, however. The difference between a class and an interface, in this case, is that interfaces don't keep state.

If you attempt multiple inheritance like I have below, the code won't compile:

 class Animal {} class Mammal {} class Dog extends Animal, Mammal {} 

A solution using classes would be to inherit one-by-one:

 class Animal {} class Mammal extends Animal {} class Dog extends Mammal {} 

Another solution is to replace the classes with interfaces:

 interface Animal {} interface Mammal {} class Dog implements Animal, Mammal {} 

Using ‘super' to access parent classes methods

When two classes are related through inheritance, the child class must be able to access every accessible field, method, or constructor of its parent class. In Java, we use the reserved word super to ensure the child class can still access its parent's overridden method:

 public class SuperWordExample { class Character { Character() { System.out.println("A Character has been created"); } void move() { System.out.println("Character walking..."); } } class Moe extends Character { Moe() { super(); } void giveBeer() { super.move(); System.out.println("Give beer"); } } } 

In this example, Character is the parent class for Moe.  Using super, we are able to access Character's  move() method in order to give Moe a beer.

Using constructors with inheritance

When one class inherits from another, the superclass's constructor always will be loaded first, before loading its subclass. In most cases, the reserved word super will be added automatically to the constructor.  However, if the superclass has a parameter in its constructor, we will have to deliberately invoke the super constructor, as shown below:

 public class ConstructorSuper { class Character { Character() { System.out.println("The super constructor was invoked"); } } class Barney extends Character { // No need to declare the constructor or to invoke the super constructor // The JVM will to that } } 

If the parent class has a constructor with at least one parameter, then we must declare the constructor in the subclass and use super to explicitly invoke the parent constructor. The super reserved word won't be added automatically and the code won't compile without it.  For example:

 public class CustomizedConstructorSuper { class Character { Character(String name) { System.out.println(name + "was invoked"); } } class Barney extends Character { // We will have compilation error if we don't invoke the constructor explicitly // We need to add it Barney() { super("Barney Gumble"); } } } 

Type casting and the ClassCastException

Casting is a way of explicitly communicating to the compiler that you really do intend to convert a given type.  It's like saying, "Hey, JVM, I know what I'm doing so please cast this class with this type." If a class you've cast isn't compatible with the class type you declared, you will get a ClassCastException.

In inheritance, we can assign the child class to the parent class without casting but we can't assign a parent class to the child class without using casting.

Consider the following example:

 public class CastingExample { public static void main(String... castingExample) { Animal animal = new Animal(); Dog dogAnimal = (Dog) animal; // We will get ClassCastException Dog dog = new Dog(); Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); Animal anotherDog = dog; // It's fine here, no need for casting System.out.println(((Dog)anotherDog)); // This is another way to cast the object } } class Animal { } class Dog extends Animal { void bark() { System.out.println("Au au"); } } 

When we try to cast an Animal instance to a Dog we get an exception. This is because the Animal doesn't know anything about its child. It could be a cat, a bird, a lizard, etc. There is no information about the specific animal. 

The problem in this case is that we've instantiated Animal like this:

 Animal animal = new Animal(); 

Then tried to cast it like this:

 Dog dogAnimal = (Dog) animal; 

Because we don't have a Dog instance, it's impossible to assign an Animal to the Dog.  If we try, we will get a ClassCastException

In order to avoid the exception, we should instantiate the Dog like this:

 Dog dog = new Dog(); 

then assign it to Animal:

 Animal anotherDog = dog; 

In this case, because  we've extended the Animal class, the Dog instance doesn't even need to be cast; the Animal parent class type simply accepts the assignment.

Casting with supertypes

Можно объявить a Dogс супертипом Animal, но если мы хотим вызвать определенный метод из Dog, нам нужно будет привести его. В качестве примера, что, если бы мы хотели вызвать bark()метод? У Animalсупертипа нет способа точно узнать, какой экземпляр животного мы вызываем, поэтому нам нужно выполнить приведение Dogвручную, прежде чем мы сможем вызвать bark()метод:

 Animal dogWithAnimalType = new Dog(); Dog specificDog = (Dog) dogWithAnimalType; specificDog.bark(); 

Вы также можете использовать приведение без присвоения объекта типу класса. Этот подход удобен, когда вы не хотите объявлять другую переменную:

 System.out.println(((Dog)anotherDog)); // This is another way to cast the object 

Примите вызов Java-наследования!

Вы узнали несколько важных концепций наследования, так что теперь пора попробовать решить проблему наследования. Для начала изучите следующий код: