본문 바로가기
IT 이야기/Java

[Java Study] 6일차 상속(super, Dynamic Method Dispatch)

by Dblog 2020. 12. 25.
728x90

학습할 것

  • 자바 상속의 특징
  • super 키워드
  • 메소드 오버 라이딩
  • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
  • 추상 클래스
  • final 키워드
  • Object 클래스

상속

객체 지향 프로그래밍(Object Oriented Programing)에서 상속은 자식 클래스가 부모의 클래스의 기능을 받아서 사용하는 것이며 자식 클래스는 언제든지 부모 클래스가 될 수 있습니다.

부모 클래스의 변수, 함수, 등을 그대로 사용할 수 있으며 오버 라이딩 개념을 활용해 재활용할 수 있습니다.

 

자바 상속의 특징

Java에서 상속을 사용하면 

  • 기존에 작정된 클래스 재활용
  • 부모의 멤버 변수, 함수를 자식 클래스에서 따로 정의하지 않아도 사용할 수 있음
  • 클래스 간 계층 관계를 구성함으로써 다형성 토대 마련

등의 장점을 가지게 됩니다.

상속에서 부모 클래스를 super class, 자식 클래스를 sub class라 부릅니다. super 클래스 즉, 부모클래스를 상속하게 되면 공통된 특징을 가지는 메소드. 변수는 선언이 불필요하기 때문에 코드의 재활용성이 좋아지고 클래스가 간결해져 코드의 가독성이 좋아집니다.

 

Java에서 클래스를 상속받는 방법은 아래 두 가지 키워드를 사용해서 상속받습니다.

  • extends

상속의 가장 대표적인 키워드입니다. implements와 가장 큰 다른 점은 오버 라이딩을 해도 되고 안 해도 된다는 점입니다. 또한 다중 상속을 사용할 수 없고 interface를 상속 할 수 없습니다. 

extends는 일반 클래스와 abstract 클래스라는 추상 클래스 상속에 사용되며 자신의 클래스에 부모 클래스의 기능을 활용하는 특징을 가지고 있습니다. 

더보기

extends 는 반드시 class를 상속받아야 하며 부모 클래스의 메소드를 필요한 것만 오버라이딩 할 수 있습니다.

public class Root {
	void run() {};
	void stop() {};
}


public class Child  extends Root{
	@Override
	void run() {
		super.run();
	}
}

 

  • implements

implements는 extends와 다르게 선언된 함수는 자식 클래스에 모두 선언해야 합니다. 또한 implements는 다중 상속이 가능합니다. 

implements는 인터페이스 상속에 사용되며 interface가 class를 사용하면 implements를 사용할 수 없습니다. 또한 implements는 여러 개를 상속받을 수 있으며 상속받은 자식 클래스는 부모 클래스의 내용을 모두 사용해야 합니다.

더보기

implements는 class를 상속 받을수 없고 상속받은 부모 클래스의 모든 메소드를 정의 해야 합니다.
하지만 다중 상속을 사용할 수 있습니다.

public interface Roots {
	void run();
	void stop();
}

public interface Roots2 {
	void runable();
	void stopable();
}

public class Child  implements Roots, Roots2{
	@Override
	public void run() {
	}
	@Override
	public void runable() {
	}
	
	@Override
	public void stop() {
	}
	@Override
	public void stopable() {
	}
}

 

 

super 키워드

Java에는 super 키워드와 super() 메소드가 있습니다.

  • super

super키워드는 부모 클래에서 상속받은 멤버를 참조할 때 사용하는 참조 변수로 사용됩니다. 

참조에 사용되다 보니 자신의 클래스를 의미하는 this와 헷갈리는 부분이 좀 있었습니다. 헷갈리는 부분은 아래 코드에서 확인할 수 있었습니다.

더보기

예제 코드

public class Root {
	protected int root_val = 3;
	
	void run() {};
	void stop() {};
}

public class Child  extends Root{
	int root_val = 0;
	
	@Override
	void run() {
		super.run();
	}
	
	void print() {
		System.out.println(root_val);
		System.out.println(this.root_val);
		System.out.println(super.root_val);
	}
	
	public static void main(String[] args) {
		Child ch = new Child();
		ch.print();
	}
}

 

결과

0
0
3

 

 

  • super()

부모 클래스의 생성자를 호출하는 메소드 입니다.

Java에서 super()는 생성자에서 항상 호출되고 선언하지 않으면 자동으로 호출합니다. 실질적으로 클래스에 부모 클래스가 상속되어 있지 않더라도 Object의 관점으로는 결국 Object를 상속받고 있기 때문에 일바적인 클래스 내부에 생성자에도 super()가 선언되어 있습니다.

결국 자식 클래스에서 super() 메소드를 호출하면 부모 클래스 부터 Object까지 모두 초기화 해주는 역할을 합니다.

더보기

예제 소스

public class Root {
	protected int root_val;
	
	void run() {};
	void stop() {};
	
	public Root() {
	}
	public Root(int x) {
		this.root_val = x;
	}
}




public class Child  extends Root{
	int root_val = 0;
	
	@Override
	void run() {
		super.run();
	}
	
	
	public Child() {
		super(3);  // 이 코드를 주석 처리하면 super.root_val == 0
		System.out.println(root_val);
		System.out.println(super.root_val);
	}
	
	public static void main(String[] args) {
		Child ch = new Child();
		
	}
}

결과

0
3

 

 

메소드 오버 라이딩

상속된 부모 클래스에서 정의된 메소드를 자식 클래스에서 다시 사용하고 정의하는 것입니다.

https://ko.wikipedia.org/wiki/%EB%A9%94%EC%86%8C%EB%93%9C_%EC%98%A4%EB%B2%84%EB%9D%BC%EC%9D%B4%EB%94%A9

 

메소드 오버라이딩 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 메소드 오버라이딩 그림. 메소드 오버라이딩,(method overriding)은 객체 지향 프로그래밍에서 서브클래스 또는 자식 클래스가 자신의 슈퍼클래스들 또는 부모 클래

ko.wikipedia.org

 

오버 라이딩 조건

  • 부모 클래스와 동일한 메소드 이름
  • 부모 클래스와 동일한 파라미터
  • 부모 클래스의 메소드 보다 좁거나 넓은 범위의 접근제어자를 선언할 수 없음

오버 라이딩 조건에 만족하지 않으면 기존의 메소드를 재사용하지 않고 새로운 메소드를 만들어 사용하게 됩니다.

더보기

예제 코드

public interface Roots {
	void run();
	void stop();
}

public interface Roots2 {
	void runable();
	void stopable();
}

public class Child  implements Roots, Roots2{
	@Override
	public void run() {
	}
	@Override
	public void runable() {
	}
	
	@Override
	public void stop() {
	}
	@Override
	public void stopable() {
	}
    
   	public void stop1(){}  // 상위 클래스에 없는 메소드, 새로 만듬
    
}

 

다이나믹 메소드 디스패치 (Dynamic Method Dispatch)


Method Dispatch
어떤 메소드를 호출할 것 인지 결정 후 그것을 실제로 실행하는 과정을 말합니다.

https://riptutorial.com/ko/java/topic/9204/%EB%8B%A4%EC%9D%B4%EB%82%B4%EB%AF%B9-%EB%A9%94%EC%86%8C%EB%93%9C-%EB%94%94%EC%8A%A4%ED%8C%A8%EC%B9%98

 

Java Language - 다이내믹 메소드 디스패치 | java Tutorial

java documentation: 다이내믹 메소드 디스패치

riptutorial.com

Dispatch는 Static, Dinamic으로 나뉩니다.

  • Static

프로그램이 실제로 시작하는 runtime이 되지 않아도 어떤 메소드가 실행될지 이미 알고 있는 코드입니다. 이에 대해서는 오버 로딩된 메소드도 결정적으로는 파라미터가 다르기 때문에 메소드의 이름이 같더라도 정확하게 어떤 메소드를 호출할지 컴파일러 수준에서도 알고 있는 Static Dispatch라고 할 수 있습니다. 

public Class Dispatch {
	static class Service {
    	void run (int number) {
        	System.out.println("run Service number : " + number);
        }
        
        void run (String msg) {
        	System.out.println("run msg : " + msg);
        }
        
    }
	
    public static void main(String[] args) {
    	new Service().run(1);
        new Service().run("run dispatch");
    }    
}


// refer https://www.youtube.com/watch?v=s-tXAHub6vg

 

 

  • Dinamic

static과는 다르게 아래 코드와 같은 경우 컴파일 시점에는 ser라는 객체가 어떤 run을 실행해야 할지 컴파일러는 알지 못합니다. 물론 코드를 읽을 수 있는 우리는 실행 결과로 run1이 실행되는 것을 new MyService1();로 알 수 있습니다.

하지만 이 run() 메소드는 런타임에서 어떤 메소드를 호출할 때 할당되어 있는 Object가 어떤 것 인가 를 보고  판단하게 됩니다.

public Class Dispatch {
	static abstract Service {
    	void run ();
    }
	
    static class MyService1 extends Service {
    	@Override
        void run() {
        	System.out.println("run1");
        }
    }
    
    static class MyService2 extends Service {
    	@Override
        void run() {
        	System.out.println("run2");
        }
    }
    
    public static void main(String[] args) {
    	Service ser = new MyService1();
        ser.run();
    }    
}


// refer https://www.youtube.com/watch?v=s-tXAHub6vg


추가예제

더보기

아래 경우를 보면 알 수 있듯 list에 해당하는 svc는 run이 list안에 있는 객체 상태에 따라서 어떤 run을 실행할지 결정은 런타임에 할 수 있습니다. 

public Class Dispatch {
	static abstract Service {
    	void run ();
    }
	
    static class MyService1 extends Service {
    	@Override
        void run() {
        	System.out.println("run1");
        }
    }
    
    static class MyService2 extends Service {
    	@Override
        void run() {
        	System.out.println("run2");
        }
    }
    
    public static void main(String[] args) {
    	List<Service> svc = Arrays.asList(new MyService1(), new MyService2());
        svc.forEach(s -> s.run);
    }    
}


// refer https://www.youtube.com/watch?v=s-tXAHub6vg

 

 

Double Dispatch 

Dinamic Dispatch Dispatch를 두 번 한다라 생각할 수 있습니다.

아래 코드의 경우 두 번째 for문, 혹은 두번째 forEach의 s에서 에러가 발생하는데 코드만 봤을 때는 발생하는 이유를 이해하기 어려웠습니다, 

토비님 유튜브 강의를 몇번이나 돌려보면서 간신히 조금 이해한 내용으로 적어 보자면

에러의 원인은 컴파일러는 static dispatch 관점에서 메서드를 보게 되는데 SNS 타입으로 정확히 어떤 postOn을 찍을지 확실하게 알아야 하지만 SNS의 관점에서 보았을때 내부에 어떤 파라미터가 있는지 정확히 알 수 없습니다

결국 컴파일러는 원하는 메소드를 정확히 찍어낼 수 없어서 에러를 찍어내게 됩니다.

public class Dispatch {
	interface Post { 
    	void postOn(FaceBook sns);
        void postOn(Twitter sns);
    }
    static class Text implements Post {
    	public void postOn(SNS sns){
            if (FaceBook sns)
            	System.out.println("text : " + sns);
            if (Twitter sns)
            	System.out.println("text : " + sns);
            
        }
    }
    static class Picture implements Post {
    	public void postOn(SNS sns){
    	    if (FaceBook sns)
            	System.out.println("text : " + sns);
            if (Twitter sns)
            	System.out.println("text : " + sns);
        }
    }
    
    interface SNS {}    
    static class FaceBook implements SNS {};
    static class Twitter implements SNS {};
	
    
    public static void main(String[] args) {
        List<Post> post = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new FaceBook(), new Twitter());
    	
       
        posts.forEach(p -> sns.forEach(p.postOn(s))); // 아래 2중 for문과 같은 내용
	    
        for(Post p : posts){
        	for(SNS s : sns) {
            	p.postOn(s);
            }
    	}
}

 

이런 경우를 해결하기 위해 필요한 방법이 double dispatch입니다. 결국 위의 에러를 해결하기 위해선 다형성을 2번 적용해야 하는데 위 경우에는 파라미터를 가지고 다형성을 적용하려 시도해서 생긴 에러입니다. 

이 에러를 수정하기 위해서 일단 파라미터 리시버라는 개념이 살짝 필요한 것 같습니다.  일단 java는 single dispatch 구조로 되어있는데 이는 리시버가 하나밖에 없다는 것입니다.

이 개념을 적용했을 때 위에 코드에서는 두 번째 p.postOn(s)를 받을 때 파라미터 s에서 다시 한번 dispatch가 일어나려 하기 때문에 에러가 발생하는 것입니다.

 

코드를 살짝 바꿔서 이번에는 파라미터로 넘어온 걸 메소드 호출에 리시버로 만들어 버리기 때문에 이전에 Text, Picture에 implements로 받은 Post에서 dispatch가 일어나지 않고 PonstOn에서 받은 sns에서 dispatch가 일어납니다.

이때 SNS 자기 자신을 post에 넘겨버리기 때문에 이번엔 정확히 어떤 sns를 찍어야 하는지 컴파일러가 확실히 알 수 있게 됩니다.

또한 아래처럼 바꾸게 되면 새로운 클래스를 추가할 때 추가하기 더욱 쉽기도 합니다.

public class Dispatch {
	interface Post { 
    	void postOn(SNS sns);
    }
    static class Text implements Post {
    	public void postOn(SNS sns){
        	sns.post(this);
        }
    }
    static class Picture implements Post {
    	public void postOn(SNS sns){
        	sns.post(this);
        }
    }
    
    interface SNS {
    	void post(Text post);
        void post(Picture post);
    }   
    
    static class FaceBook implements SNS {
    	public void post(Text post){ System.out.println("text - FaceBook"); }
        public void post(Picture post){ System.out.println("Picture - FaceBook"); }
    };
    static class Twitter implements SNS {
    	public void post(Text post){ System.out.println("text - Twitter"); }
        public void post(Picture post){ System.out.println("Picture - Twitter"); }
    };
    static class Google implements SNS {
    	public void post(Text post){ System.out.println("text - Google"); }
        public void post(Picture post){ System.out.println("Picture - Google"); }
    };
	
    
    public static void main(String[] args) {
        List<Post> post = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new FaceBook(), new Twitter(), new Google());
    	
       
        posts.forEach(p -> sns.forEach(p.postOn(s))); // 아래 2중 for문과 같은 내용
	    
        for(Post p : posts){
        	for(SNS s : sns) {
            	p.postOn(s);
            }
    	}
}

 

추상 클래스

인터페이스의 역할을 하며 구현체도 가지고 있는 특이한 클래스입니다. 요즘에는 인터페이스로 많이 대체되어 잘 쓰이고 있지는 않다고 합니다.

추상 클래스를 정의하고 사용하는 방법

  • 클래스 앞에 abstract 키워드를 사용하여 정의
  • 추상 메소드 사용 가능
  • 인스턴스 생성 불가

인스턴스와 다른 것 중 가장 크게 보이는 것은 메소드안에 구현체가 있다는 것입니다. 하지만 인스턴스를 생성할 수 없고 상속받아서 사용해야 하는 단점 때문에 대부분 인터페이스를 사용하는 것 같습니다.

 

 

final 키워드

C 계열 언어에서 사용하는 const와 같은 역할을 합니다. '상수'라 불리며 변수를 선언하는 동시에 값을 초기화하며 프로그램이 실행되는 중간에는 값을 수정할 수 없습니다. 

일반적으로 final 변수는 프로그램 전체에서 사용되는 경우가 많아 static으로 사용되는 경우가 많습니다.

더보기
private final int consts = 3;

 

Object 클래스

모든 클래스의 부모 클래스를 따라가다 보면 만날 수 있는 최상위 클래스입니다. 모든 클래스는 결국 Object를 상속받게 됩니다. 

형태는 아래 예시 코드와 같습니다.

package org.opentutorials.javatutorials.progenitor;
 
class O {}
package org.opentutorials.javatutorials.progenitor;
 
class O extends Object {}

 

Object 클래스도 다른 클래스와 같이 메소드를 가지고 있고 당연하게 메소드를 오버 라이딩할 수 있습니다. 가장 흔히 사용하는 toStirng() 함수 또한 object에 포함되어있는 메소드 입니다.

https://docs.oracle.com/javase/7/docs/api/java/lang/Object.html

 

Object (Java Platform SE 7 )

Called by the garbage collector on an object when garbage collection determines that there are no more references to the object. A subclass overrides the finalize method to dispose of system resources or to perform other cleanup. The general contract of fi

docs.oracle.com

 

추가 공부..

토비님 강의를 듣다가 너무 좋은 내용을 알려주셔서 까먹을까 봐 따로 적어 놓습니다.

Method Signature

  • name
  • parameter type

signature가 같으면 메소드를 오버라이딩 할 수 있고 또한 signature가 같은 메소드를 두 개 이상 선언하여 사용할 수 없습니다. 추가로 return 타입은 메소드의 signature로 볼 수 없습니다. 

String hello (String msg) { return "";};
List hello (String msg) { return null;};

// return 타입이 다르지만 signature가 모두 같기 때문에 에러가 발생

 

Method Type

  • return type
  • method type parameter
  • method argument type
  • exception

Method Type이 일치하면 Method Reference에서 사용할 수 있습니다. 

 

 

참고 문헌

728x90

댓글