웹사이트 검색

Java의 다중 상속


오늘은 Java의 다중 상속에 대해 살펴보겠습니다. 언젠가 나는 자바에서 컴포지션에 대한 글을 거의 쓰지 않았다. 이번 포스팅에서는 자바 다중 상속에 대해 알아보고 컴포지션과 상속을 비교해 보도록 하겠습니다.

Java의 다중 상속

Java의 다이아몬드 문제

package com.journaldev.inheritance;

public abstract class SuperClass {

	public abstract void doSomething();
}

ClassA.java

package com.journaldev.inheritance;

public class ClassA extends SuperClass{
	
	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of A");
	}
	
	//ClassA own method
	public void methodA(){
		
	}
}

ClassB.java

package com.journaldev.inheritance;

public class ClassB extends SuperClass{

	@Override
	public void doSomething(){
		System.out.println("doSomething implementation of B");
	}
	
	//ClassB specific method
	public void methodB(){
		
	}
}

이제 ClassC 구현이 아래와 같으며 ClassA와 ClassB를 모두 확장한다고 가정해 보겠습니다. ClassC.java

package com.journaldev.inheritance;

// this is just an assumption to explain the diamond problem
//this code won't compile
public class ClassC extends ClassA, ClassB{

	public void test(){
		//calling super class method
		doSomething();
	}

}

test() 메서드가 슈퍼클래스 doSomething() 메서드를 호출하고 있음에 유의하십시오. 이것은 컴파일러가 어떤 수퍼클래스 메서드를 실행할지 모르기 때문에 모호성을 초래합니다. 다이아몬드 모양의 클래스 다이어그램 때문에 Java에서 다이아몬드 문제라고 합니다. Java의 다이아몬드 문제는 Java가 클래스에서 다중 상속을 지원하지 않는 주된 이유입니다. 다중 클래스 상속과 관련된 위의 문제는 모든 클래스에 공통 메서드가 하나 이상 있는 클래스가 세 개뿐인 경우에도 발생할 수 있습니다.

Java 인터페이스의 다중 상속

내가 항상 다중 상속이 클래스에서는 지원되지 않지만 인터페이스에서는 지원된다고 말하는 것을 알아차렸을 것입니다. 단일 인터페이스는 여러 인터페이스를 확장할 수 있습니다. 아래는 간단한 예입니다. InterfaceA.java

package com.journaldev.inheritance;

public interface InterfaceA {

	public void doSomething();
}

InterfaceB.java

package com.journaldev.inheritance;

public interface InterfaceB {

	public void doSomething();
}

두 인터페이스가 동일한 메소드를 선언하고 있음을 주목하십시오. 이제 아래와 같이 두 인터페이스를 모두 확장하는 인터페이스를 가질 수 있습니다. InterfaceC.java

package com.journaldev.inheritance;

public interface InterfaceC extends InterfaceA, InterfaceB {

	//same method is declared in InterfaceA and InterfaceB both
	public void doSomething();
	
}

인터페이스는 메소드만 선언하고 실제 구현은 인터페이스를 구현하는 구체적인 클래스에 의해 수행되기 때문에 이것은 완벽하게 괜찮습니다. 따라서 Java 인터페이스의 다중 상속에는 어떤 종류의 모호성도 없습니다. 이것이 자바 클래스가 아래 예제와 같이 여러 인터페이스를 구현할 수 있는 이유입니다. InterfacesImpl.java

package com.journaldev.inheritance;

public class InterfacesImpl implements InterfaceA, InterfaceB, InterfaceC {

	@Override
	public void doSomething() {
		System.out.println("doSomething implementation of concrete class");
	}

	public static void main(String[] args) {
		InterfaceA objA = new InterfacesImpl();
		InterfaceB objB = new InterfacesImpl();
		InterfaceC objC = new InterfacesImpl();
		
		//all the method calls below are going to same concrete implementation
		objA.doSomething();
		objB.doSomething();
		objC.doSomething();
	}

}

수퍼 클래스 메서드를 재정의하거나 인터페이스 메서드를 구현할 때마다 메서드를 재정의할 때 항상 재정의 주석을 사용한다는 사실을 알고 계셨습니까?

구조용 구성

따라서 ClassA 함수 methodA()ClassB 함수 methodB()를 활용하려면 어떻게 해야 합니까? 코드>ClassC. 해결책은 컴포지션을 사용하는 데 있습니다. 다음은 컴포지션을 사용하여 클래스 메서드를 모두 활용하고 개체 중 하나에서 doSomething() 메서드를 사용하는 리팩터링된 ClassC 버전입니다. ClassC.java

package com.journaldev.inheritance;

public class ClassC{

	ClassA objA = new ClassA();
	ClassB objB = new ClassB();
	
	public void test(){
		objA.doSomething();
	}
	
	public void methodA(){
		objA.methodA();
	}
	
	public void methodB(){
		objB.methodB();
	}
}

구성 대 상속

Java 프로그래밍의 모범 사례 중 하나는 \상속보다 구성을 선호\하는 것입니다. 이 접근 방식을 선호하는 몇 가지 측면을 살펴보겠습니다.

  1. Suppose we have a superclass and subclass as follows: ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    	public void methodC(){
    	}
    }
    

    ClassD.java

    package com.journaldev.inheritance;
    
    public class ClassD extends ClassC{
    
    	public int test(){
    		return 0;
    	}
    }
    

    The above code compiles and works fine but what if ClassC implementation is changed like below: ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    	public void methodC(){
    	}
    
    	public void test(){
    	}
    }
    

    Notice that test() method already exists in the subclass but the return type is different. Now the ClassD won’t compile and if you are using any IDE, it will suggest you change the return type in either superclass or subclass. Now imagine the situation where we have multiple levels of class inheritance and superclass is not controlled by us. We will have no choice but to change our subclass method signature or its name to remove the compilation error. Also, we will have to make a change in all the places where our subclass method was getting invoked, so inheritance makes our code fragile. The above problem will never occur with composition and that makes it more favorable over inheritance.

  2. Another problem with inheritance is that we are exposing all the superclass methods to the client and if our superclass is not properly designed and there are security holes, then even though we take complete care in implementing our class, we get affected by the poor implementation of the superclass. Composition helps us in providing controlled access to the superclass methods whereas inheritance doesn’t provide any control of the superclass methods, this is also one of the major advantages of composition over inheritance.

  3. Another benefit with composition is that it provides flexibility in the invocation of methods. Our above implementation of ClassC is not optimal and provides compile-time binding with the method that will be invoked, with minimal change we can make the method invocation flexible and make it dynamic. ClassC.java

    package com.journaldev.inheritance;
    
    public class ClassC{
    
    	SuperClass obj = null;
    
    	public ClassC(SuperClass o){
    		this.obj = o;
    	}
    	public void test(){
    		obj.doSomething();
    	}
    	
    	public static void main(String args[]){
    		ClassC obj1 = new ClassC(new ClassA());
    		ClassC obj2 = new ClassC(new ClassB());
    		
    		obj1.test();
    		obj2.test();
    	}
    }
    

    Output of above program is:

    doSomething implementation of A
    doSomething implementation of B
    

    This flexibility in method invocation is not available in inheritance and boosts the best practice to favor composition over inheritance.

  4. Unit testing is easy in composition because we know what all methods we are using from superclass and we can mock it up for testing whereas in inheritance we depend heavily on superclass and don’t know what all methods of superclass will be used, so we need to test all the methods of superclass, that is an extra work and we need to do it unnecessarily because of inheritance.

여기까지가 자바의 다중 상속과 구성에 대한 간략한 설명이었습니다.