ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [ch7 자바 객체] 상속과 오버라이딩(overriding), super 알아보기
    프로그래밍 언어/JAVA 2022. 6. 16. 23:22
    상속이란?

     

    기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해서 클래스를 작성하다면 보다 적은 양의 코드로 새로운 클래스를 작성할 수 있고 코드를 공통적으로 관리할 수 있기 때문에 코드의 추가 및 변경이 매우 용이하다. 

     

    이러한 특징은 코드의 재사용성을 높이고 코드의 중복을 제거하여 프로그램의 생산성과 유지보수에 크게 기여한다. 

    자바에서 상속을 구현하는 방법을 아주 간단한데, 새로 작성하고자 하는 클래스의 이름 뒤에 상속받고자 하는 클래스의 이름을 키워드 'extends'와 함께 써주면 된다. 예를 들어 새로 작성하려는 클래스의 이름이 Child고 상속받고자 하는 기존의 클래스 이름이 Parent라면 아래와 같이 하면 된다.

    cclass Parent{ } 
    class Child extend Parent{ ... }

     

     

    이 두 클래스는 서로 상속 관계에 있다고 하며, 상속해주는 클래스를 '조상 클래스'라 하고 상속 받는 클래스를 '자손 클래스'라 한다.  클래스는 타원으로 클래스 간의 상속 관계는 화살표로 표시했다. 아래와 같은 클래스 간의 상속관계를 그림으로 표현한 것을 상속계층도(class hierarchy)라고 한다. 

     

     

     

    자손 클래스는 조상 클래스의 모든 멤버를 상속받기 때문에, Child 클래스는 Parent 클래스의 멤버들을 포함한다고 할 수 있다. 만일 Parent 클래스에 age라는 정수형 변수를 멤버변수로 추가하면, 자손 클래스는 조상의 멤버를 모두 상속받기 때문에, Child 클래스는 자동적으로 age라는 멤버변수가 추가된 것과 같은 효과를 얻는다. 

     

     

    Child 클래스에 새로운 코드가 추가되어도 조상인 Parent 클래스는 아무런 영향도 받지 않는다. 즉) 조상 클래스가 변경되면 자동적으로 영향을 받게 되지만, 자손 클래스가 변경되는 것은 조상 클래스에 아무런 영향을 주지 않는다.

    그래서 상속을 받는다는 것은 조상 클래스를 확장(extend)한다는 의미로 해석할 수도 있으며 이것이 상속에 사용되는 키워드가 'extends'인 이유기도 하다.

     

    - 자손 클래스는 조상 클래스의 모든 멤버를 상속받는다.
    (단, 생성자와 초기화 블럭은 상속되지 않는다.)
    - 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 만다.

     


     

    ※ 예제 1번 ※

    package ch7;
    
    class Tv{
    	boolean power;
    	int channel;
    	
    	void power() { power = !power;}
    	void channelUp() {channel++; }
    	void channelDown() {channel--; }
    }
    
    class SmartTv extends Tv{ //SmartTv에는 캡션(자막)을 보여주는 기능을 추가
    	boolean caption;
    	void displayCaption(String text) {
    		if(caption) { //만약 캡션 상태가 true라면 text를 보여준다.
    			System.out.println(text);
    		}
    	}
    	
    }
    
    public class Ex7_1 {
    	public static void main(String[] args) {
    		SmartTv stv = new SmartTv();
    		stv.channel = 10;
    		stv.channelUp();
    		System.out.println(stv.channel);
    		stv.displayCaption("hello, world");
    		stv.caption=true; //기능을 off에서 on으로 만든다. boolean형 변수의 초기값은 false이기 때문에
    		stv.displayCaption("hello, world");
    
    	}
    
    }

    예제 1번 출력 값

    Tv 클래스로부터 상속받고 기능을 추가하여 SmartTv 클래스를 작성하였다. 자손 클래스의 인스턴스를 생성하면 조상 클래스의 멤버도 함께 생성되기 때문에 따로 조상 클래스의 인스턴스를 생성하지 않고도 조상 클래스의 멤버들을 사용할 수 있다. 

     


     

     

    클래스들 간의 관계 - 포함관계

     

    상속이외에도 클래스를 재사용하는 또 다른 방법이 있는데, 그것은 클래스 간에 '포함(composite)'관계를 맺어 주는 것이다. 클래스 간의 포함관계를 맺어 주는 것은 한 클래스의 멤버변수로 다른 클래스 타입의 참조변수를 선언하는 것을 뜻한다. 원(Circle)을 표현하기  위한 Circle클래스와 좌표상의 한 점을 다루기 위한 Point 클래스가 각각 있다고 가정해보자

     

     

     

    아래와 같이 한 클래스를 작성하는 데 다른 클래스를 멤버변수로 선언하여 포함시키는 것은 좋은 생각이다. 하나의 거대한 클래스를 작성하는 것보다 단위별로 여러 개의 클래스를 작성한 다음, 이 단위 클래스들을 포함관계로 재사용하면 보다 간결하고 손쉽게 클래스를 작성할 수 있다.

     

     

    클래스 간의 관계 결정

     

    클래스를 작성하는데 있어 상속관계를 맺을 건지, 포함관계를 맺을 건지 혼돈스러울 수 있다. 위에서 설며안 Circle 클래스 경우, Point 클래스를 포함하는 대신 상속관계를 맺어주면 아래와 같다.

     

     

    두 경우를 비교해 보면 Circle 클래스를 작성하는데 있어 Point 클래스를 포함시키거나 상속 받도록 하는 것은 큰 차이가 없어 보인다. 그럴때는 문장을 만들어 보면 된다.

     

    원(Circle)은 점(Point) 이다 - Circle is a Point.
    원(Circle)은 점(Point) 을 가지고 있다. - Circle has a Point.

     

    원은 원점(Point)와 반지름으로 구성되므로 위의 두 문장을 비교해 보면 첫 번째 문장보다 두 번째 문장이 더 옳은 것을 알 수 있다. 이처럼 클래스를 가지고 문장을 만들었을 때 '~은 ~이다'라는 문장이 성립하면, 서로 상속 관계를 맺어주고, '~은 ~을 가지고 있다'라는 문장이 성립하면 포함관계를 맺어주면 된다. 따라서 Circle 클래스와 Point 클래스는 상속관계 보다 포함관계를 맺어주는 것이 더 옳다. 

     

    또 다른 예를 들어보자면 Car 클래스와 SportsCar 클래스는 'SportsCar는 Car이다.'와 같이 문장을 만드는 것이 더 옳기 때문에 두 클래스는 Car 클래스를 조상으로 하는 상속관계를 맺어 주어야 한다. 

     

     

     

    단일 상속(single inheritance)

     

    또 다른 객체지향언어인 C++에서는 여러 조상 클래스로부터 사옥받는 것이 가능한 '다중상속(multiple inheritance)'을 허용하지만 자바에서는 단일 상속만을 허용한다. 그래서 둘 이상의 클래스를 상속받을 수 없다. 

    class TvDVD extends Tv, DVD{ .. } //에러. 조상은 하나만 가능하다.

    단일 상속이 하나의 조상 클래스만을 가질 수 있기 때문에 다중상속에 비해 불편한 점도 있지만, 클래스 간의 관계가 보다 명확해지고 코드를 더욱 신뢰할 수 있게 만들어 준다는 점에서 다중상속보다 유리하다. 

     

     

     

    Object 클래스 - 모든 클래스의 조상

     

    Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상 클래스이다. 다른 클래스로부터 상속 받지 않는 모든 클래스들은 자동적으로 Object클래스로부터 상속받게 한다. 만약 Tv를 상속받는 SmartTv클래스가 있다고 가정했을 때 상속계층도를 따라 조상클래스, 조사을래스를 찾아 올라가다 보면 최상위 조상을 Object 클래스일 것이다. 

     

     

    이처럼 모든 상속계층도의 최상위에는 Object 클래스가 위치한다. 그래서 자바의 모든 클래스들은 Object클래스의 멤버들을 상속받기 때문에 toString(), equals(Object o) 같은 메서드들을 사용할 수 있는 것이다.

     

     

    오버라이딩(overriding)

     

    조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것이 오버라이딩이라고 한다. 상속받은 메서드를 그대로 사용하기도 하지만, 자손 클래스 자신에 맞게 변경해야하는 경우가 많다.

    예를들어) 2차워 좌표계의 한 점을 표현하기 위한 Point 클래스가 있을 떄, 이를 조상으로 하는 Point3D클래스, 3차원 좌표계의 한 점을 표현하기 위한 클래스를 새로 작성하였다고 하자 

     

    class Point{
    	int x;
    	int y;
    	
    	String getLocation() {
    		return "x: " + x + ", y:" + y;
    	}
    }
    
    class Point3D extends Point{
    	int z;
    	
    	String getLocation() {
    		return "x: " + x + ", y:" + y + ", z: " +z;
    	}
    }

     

    Point클래스의 getLocation()은 한 점의 x, y 좌표를 문자열로 반환하도록 작성되었다. 이 두 클래스는 서로 상속관계에 있으므로 Point3D클래스는 Point 클래스로부터 getLocation()을 상속받지만 Point3D 클래스에는 맞지 않는다. 그래서 이 메서드를 Point3D 클래스 자신에 맞게 z축의 좌표값도 포함하여 반환하도록 오버라이딩하였다. 

     

     

    오버라이딩 조건

     

    오버라이딩은 메서드의 내용만을 새로 작성하는 것이므로 메서드의 선언부(메서드 이름, 매개변수, 반환타입)는 조상의 것과 완전히 일치해야 한다. 다만 접근 제어자(access modifier)와 예외(exception)는 제한된 조건 하에서만 다르게 변경할 수 있다. 

    1. 접근 제어자는 조상 클래스의 메서드보다 좁은 범위로 변경할 수 없다.
    : 만일 조상 클래스에 정의된 메서드의 접근 제어자가 protected라면, 이를 오버라이딩하는 자손 클래스의 메서드는 접근 제어자가 protected나 public이어야 한다. 대부분의 경우 같은 범위에 접근 제어자로 사용한다. 뒤 포스팅에서 다루겠지만 접근 제어자의 접근 범위를 넓은 것 부터 나열하면 public, protected, (default), private이다. 

    2. 조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

     

     

    오버로딩 vs 오버라이딩

     

    자바를 공부하면서 초반엔 오버로딩과 오버라이딩이 헷갈렸었는데 차이는 명백하다. 오버로딩은 기존에 없는 새로운 메서드를 추가하는 것이고, 오버라이딩은 조상으로부터 상속받은 메서드의 내용을 변경하는 것이다.

    오버로딩 (overloading) : 기존에 없는 새로운 메서드를 정의하는 것(new)
    오버라이딩 (overriding) : 상속받은 메서드의 내용을 변경하는 것 (change, modify)

    아래의 예제를 보고 오버로딩과 오버라이딩을 구별해보자.

     

    이렇게 코드가 있을 때 Child 클래스에서는 상속받은 Parent 클래스에 메서드인 parentMethod를 오버라이딩한 것이고 만들어진 메서드에 매개변수를 추가해서 parentMethod(int i) {}를 만들어 오버로딩한 것이다.

     

     

     

    참조변수 super

     

    super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다. 멤버변수와 지역변수의 이름이 같을 때 this를붙여서 구별했듯이 상속받은 멤버와 자신의 멤버와 이름이 같을 때는 super를 붙여서 구별할 수 있다. 

     

    ※ 예제 2번 ※

    package ch7;
    
    public class Ex7_2 {
    	public static void main(String[] args) {
    		Child c = new Child();
    		c.method();
    		
    	}
    }
    
    class Parent{
    	int x = 10; //super.x
    }
    
    class Child extends Parent{
    	int x =20; //this.x
    	void method() {
    		System.out.println("x= "+x);
    		System.out.println("this.x= "+this.x);
    		System.out.println("super.x= "+super.x);
    	}
    }

    예제 2번 출력 값

     

    예제 2번에서는 Child 클래스는 조상인 Parent 클래스로부터 x를 상속받는데, 자신의 멤버인 x와 이름이 같아서 이 둘을 구분할 방법이 필요하다. 이럴 때 사용하는 것이 super이다. 모든 인스턴스 메서드에는 this와 super가 지역변수로 존재하는데, 이 들에는 자신이 속한 인스턴스의 주소가 자동으로 저장된다. 조상의 멤버와 자신의 멤버를 구별하는데 사용된다는 점만 제외하면 this와 super의 근본은 같다.

     

     

    super() - 조상의 생성자

     

    this()처럼 super()도 생성자이다. this()는 같은 클래스의 다른 생성자를 호출하는데 사용되지만, super()는 조상의 생성자를 호출하는데 사용된다. 

     

    ※ 예제 3번 ※

    package ch7;
    
    
    
    class Point {
    	int x,y;
    	
    	Point(int x, int y){
    		//멤버변수인 x를 (this.x)로 매개변수로 받은 x를 x로 나타냈다.
    		this.x = x; 
    		this.y= y;
    	}
    }
    
    class Point3D extends Point{
    	int z;
    	
    	Point3D(int x, int y , int z){
    		super(x,y); //조상클래스의 생성자인 Point(int x, int y)를 호출
    		this.z = z; //자신의 멤버를 초기화한다.
    	}
    	
    }
    
    public class Ex7_3 {
    
    	public static void main(String[] args) {
    		Point3D p = new Point3D(1,2,3);
    		System.out.println("x="+p.x+", y="+p.y+", z="+p.z);
    
    	}
    
    }

    예제 3번 출력 값

    만약 Point3D(int x, int y , int z) 생성자에서 this.x=x; this.y=y;로 초기화해도 되지만 조상의 멤버는 조상의 생성자를 통해 초기화하도록 작성하는 것이 바람직하므로 super(x,y)로 작성하였다.

     

     

     

    super 총 정리

     

     예제 4번  

    package ch7;
    
    public class Ex7_4 {
    
    	public static void main(String[] args) {
    		MyPoint3D d3 = new MyPoint3D(1,2,3);
    		System.out.println(d3.x+" "+d3.y+" "+d3.r);
    
    	}
    
    }
    
    
    class Point1{
    	int x;
    	int y;
    	
    	Point1(int x, int y){
    		this.x  = x;
    		this.y = y;
    	}
    	
    	public String print() {
    		return "x: " + x +", y: " +y;
    	}
    	
    }
    
    class MyPoint3D extends Point1{
    	
    	int r;
    	MyPoint3D(int x, int y, int r){
    		this.x=x;
    		this.y=y;
    		this.r = r;
    	}
    	
    	public String print() {
    		return "x: " + x +", y: " +y +", r" +r;
    	}
    }

    예제 4번 출력 값

     

     

    위의 예제 4번처럼 작성하면 에러가 난다. 

    그 이유는 생성자의 첫 줄은 부모의 다른 생성자를 호출해야 한다. 즉) Point1 클래스는 Object의 생성자, MyPoint3D는 Point1의 생성자를 말이다. 하지만 현재 MyPoint3D에는 super나 this같은 생성자 호출이 없다. 그래서 컴파일러가 자동으로 super.Point() 라는 생성자 호출을 넣어주는데, Point 클래스를 살펴보면 기본 생성자가 없는 걸 알 수 있다. 만약 Point1의 생성자 Point(int x, int y)가 없다면 자동으로 Point1(){} 라는 기본 생성자가 생성되어 에러 없이 작동하는 걸 확인할 수 있는데 이미 Point1 클래스에 매개변수가 2개 있는 생성자를 만들어두었기 때문에 기본 생성자는 생성되지 않는다. 따라서 기본 생성자를 추가해주면 에러없이 작동하는 걸 확인할 수 있다. 

     

     


     

    ※ 예제 5번 ※

    package ch7;
    
    public class Ex7_4 {
    
    	public static void main(String[] args) {
    		MyPoint3D d3 = new MyPoint3D(1,2,3);
    		System.out.println(d3.x+" "+d3.y+" "+d3.r);
    
    	}
    
    }
    
    
    class Point1{
    	int x;
    	int y;
    	
    	
    	Point1(){} //기본 생성자 추가
    	
    	Point1(int x, int y){
    		this.x  = x;
    		this.y = y;
    	}
    	
    	public String print() {
    		return "x: " + x +", y: " +y;
    	}
    	
    }
    
    class MyPoint3D extends Point1{
    	
    	int r;
    	MyPoint3D(int x, int y, int r){
    		this.x=x;
    		this.y=y;
    		this.r = r;
    	}
    	
    	public String print() {
    		return "x: " + x +", y: " +y +", r" +r;
    	}
    }

    예제 5번 출력 값

    반응형

    댓글

Designed by Tistory.