상속 대 구성 : 선택 방법

상속과 구성은 개발자가 클래스와 개체 간의 관계를 설정하는 데 사용하는 두 가지 프로그래밍 기술입니다. 상속은 한 클래스에서 다른 클래스를 파생시키는 반면, 컴포지션은 클래스를 부분의 합으로 정의합니다.

상속 관계에서 부모 또는 수퍼 클래스를 변경하면 코드가 손상 될 위험 이 있으므로 상속을 통해 생성 된 클래스와 객체는 밀접하게 결합 됩니다. 컴포지션을 통해 생성 된 클래스와 객체는 느슨하게 결합되어 있으므로 코드를 손상시키지 않고 구성 요소 부분을 더 쉽게 변경할 수 있습니다.

느슨하게 결합 된 코드는 더 많은 유연성을 제공하기 때문에 많은 개발자는 구성이 상속보다 더 나은 기술이라는 것을 배웠지 만 사실은 더 복잡합니다. 프로그래밍 도구를 선택하는 것은 올바른 주방 도구를 선택하는 것과 비슷합니다. 야채를 자르기 위해 버터 나이프를 사용하지 않으며 모든 프로그래밍 시나리오에 대해 구성을 선택해서는 안됩니다. 

이 Java Challenger에서는 상속과 구성의 차이점과 어떤 것이 프로그램에 맞는지 결정하는 방법을 배웁니다. 다음으로 Java 상속의 중요하지만 도전적인 몇 가지 측면 인 메서드 재정의, super키워드 및 형식 캐스팅을 소개합니다. 마지막으로, 결과물이 무엇인지 결정하기 위해 한 줄씩 상속 예제를 통해 학습 한 내용을 테스트합니다.

Java에서 상속을 사용하는 경우

객체 지향 프로그래밍에서 우리는 자식과 부모 클래스 사이에 "is a"관계가 있음을 알 때 상속을 사용할 수 있습니다. 몇 가지 예는 다음과 같습니다.

  • 사람 인간입니다.
  • 고양이 동물입니다.
  • 자동차   차량입니다.

각각의 경우에 하위 또는 하위 클래스는 상위 또는 수퍼 클래스 의 특수 버전입니다. 코드 재사용의 예는 수퍼 클래스에서 상속됩니다. 이 관계를 더 잘 이해하려면 잠시 시간을내어 다음에서 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및  LivingRooma의 구성 개체를 House

코드 받기

이 Java Challenger의 예제에 대한 소스 코드를 가져옵니다. 예제를 따르는 동안 자체 테스트를 실행할 수 있습니다.

상속 vs 구성 : 두 가지 예

다음 코드를 고려하십시오. 이것은 상속의 좋은 예입니까?

 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만드는 소리를 전문화하고 싶을 수 있습니다 .

 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 상속의 예입니다. 먼저 클래스를 확장 하여 AnimalCat클래스 를 만듭니다 . 다음으로, 우리는 우선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

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 상속 문제를 해결하십시오!

상속에 대한 몇 가지 중요한 개념을 배웠으므로 이제 상속 문제를 시도해 볼 때입니다. 시작하려면 다음 코드를 살펴보십시오.