[Live Study 6주차] 상속

이호석·2022년 7월 26일
0

목표

자바의 상속에 대해 학습

학습할 것

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



1. 자바 상속의 특징

상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것이다. 상속을 통해서 클래스를 작성하면 적은 양의 코드로 새로운 클래스를 작성할 수 있고, 코드를 공통적으로 관리할 수 있어 프로그램의 생산성 및 유지보수에 기여를 한다.

상속은 extends라는 키워드로 이루어지게 된다. 간단한 예시를 보면

class Car {
	private int gear;
    private int speed;
    private int motor;
    
    public Car() {
    }
    public Car(int gear, int speed, int motor) {
    	this.gear = gear;
        this.speed = speed;
        this.motor = motor;
    }
    
    public void basicStatus() {
        System.out.println("Car status");
        System.out.println("gear = " + gear);
        System.out.println("speed = " + speed);
        System.out.println("motor = " + motor);
    }
    
}

class SportsCar extends Car {
	private int booster;
    
    public SportsCar() {
    }
    
    public SportsCar(int gear, int speed, int motor, int booster) {
    	super(gear, speed, motor);
        this.booster = booster;
    }
    
    public void sportsStatus() {
        System.out.println("SportsCar status");
        System.out.println("booster = " + booster);
    }
}

Car 클래스는 자동차의 기본적인 기능들을 필드로 가지고 있습니다. SportsCar는 Car에 포함되며 부가적으로 booster라는 기능을 가지고 있기에 Car 클래스를 상속받고, 자신의 부가적인 기능을 더하여 클래스를 구성합니다.

따라서 만약 SportsCar newCar = new SportsCar() 인스턴스가 존재하면 상위 클래스의 메소드인 basicStatus()sportsStatus()를 모두 호출할 수 있다.


1-1. 하위 클래스에서 할 수 있는 일

이렇게 Car 클래스는 상위 클래스라고 말하며 상위 클래스인 Car 클래스에서 파생된 클래스를 하위 클래스라고 말합니다.

하위 클래스는 생성자와 초기화 블럭은 상속되지 않고, 상위 클래스의 멤버들 중 public, Protected 접근제어자가 붙은 멤버들만 접근할 수 있습니다. 따라서 하위 클래스의 멤버의 개수는 상위 클래스보다 항상 같거나 많습니다.

추가로 하위 클래스는 상위 클래스의 멤버들을 사용할 수 있는것과 더불어 일반 클래스 같이 자신의 필드 및 메소드를 정의하고 사용할 수 있다. 다만 아래와 같은 기능도 추가적으로 사용 가능하다.

  • 상속된 필드를 바로 사용할 수 있다.(public, protected)
  • 상속된 메소드는 바로 사용할 수 있다.(public, protected)
  • 상속된 메소드를 오버라이딩 할 수 있다.
  • super 키워드를 이용해 상위 클래스의 생성자를 하위 클래스의 생성자에 작성할 수 있다.

1-2. 상위 클래스의 private 멤버

상위 클래스의 private멤버들은 일반적으로 하위 클래스에 사용할 수 없습니다.
하지만 필드의 경우, getter and setter와 같이 public한 메소드를 통해 필드에 접근할 수 있습니다.


1-3. 다중 상속의 금지

자바에서는 다중 상속을 금지하고 있습니다.
두 개 이상의 클래스를 상속받을 수 없는 이유는 여러 클래스의 필드를 상속하게 될 때, 서로 다른 클래스에서 동일한 필드를 인스턴스화 하면 구분할 수 없고, 이 필드를 사용하는 메소드 또는 생성자의 우선순위를 결정할 수 없기 떄문에 다중 상속을 금지하고 있습니다.



2. super 키워드

위의 Car 클래스를 조금 더 확장시켜 보자

class Car {
    private int gear;
    private int speed;
    private int motor;

    public Car() {
    }
    public Car(int gear, int speed, int motor) {
        this.gear = gear;
        this.speed = speed;
        this.motor = motor;
    }

    public int getGear() {
        return gear;
    }

    public int getSpeed() {
        return speed;
    }

    public int getMotor() {
        return motor;
    }

    public void printStatus() {
        System.out.println("Car status");
        System.out.println("gear = " + getGear());
        System.out.println("speed = " + getSpeed());
        System.out.println("motor = " + getMotor());
    }

    public void basicStatus() {
        System.out.println("Car status");
        System.out.println("gear = " + getGear());
        System.out.println("speed = " + getSpeed());
        System.out.println("motor = " + getMotor());
    }

}

class SportsCar extends Car {
    private int booster;

    public SportsCar() {
    }

    public SportsCar(int gear, int speed, int motor, int booster) {
        super(gear, speed, motor);
        this.booster = booster;
    }
	
    @Override
    public void printStatus() {
        System.out.println("SportsCar status");
        System.out.println("gear = " + getGear());
        System.out.println("speed = " + getSpeed());
        System.out.println("motor = " + getMotor());
        System.out.println("booster = " + booster);
    }

    public void printTotalStatus() {
        super.printStatus();
        System.out.println();
        this.printStatus();

    }

    public void sportsStatus() {
        System.out.println("SportsCar status");
        System.out.println("booster = " + booster);
    }
}

클래스를 살펴보면 우선 SportsCar의 생성자에선 super()메소드가 printTotalStatus() 메소드에선 super 키워드를 사용하는걸 볼 수 있다.


2-1. super

super 키워드란 하위 클래스에서 상위 클래스로부터 상속받은 멤버를 참조하는데 사용되는 키워드 입니다. 예를 들어 this는 동일한 이름의 필드와 지역변수가 있을때 구분하기 위해서 사용됐다면
오버라이드한 메소드에서 상위 클래스와 하위 클래스의 메소드를 구분하기 위해 super 키워드가 사용된다.


2-2. super()

super() 메소드는 상위 클래스의 생성자를 호출한다. 따라서 반드시 생성자 안에서 super()를 호출해야 하며 하위 클래스가 상위 클래스의 멤버를 사용할 수 있으므로 생성자 내에서 첫 줄에 작성해야 한다.

class RunCar {
    public static void main(String[] args) {
        SportsCar sportsCar = new SportsCar(1, 2, 3, 4);
        sportsCar.newPrintStatus();
    }
}
[실행결과]
Car status
gear = 1
speed = 2
motor = 3

SportsCar status
gear = 1
speed = 2
motor = 3
booster = 4



3. 메소드 오버라이딩

상위 클래스로 부터 상속받은 메소드의 내용을 재정의 하는것을 말한다.
주로 상위 클래스의 메소드를 자식 클래스에 맞게 사용해야할 때 사용된다.
또한 @Override 어노테이션은 생략할 수 있지만, 생략하지 않는것을 권장합니다.
> @Override는 왜 쓰는걸까?

[Car Class]
    public void printStatus() {
        System.out.println("Car status");
        System.out.println("gear = " + getGear());
        System.out.println("speed = " + getSpeed());
        System.out.println("motor = " + getMotor());
    }
    
[SportsCar Class]
    @Override
    public void printStatus() {
        System.out.println("SportsCar status");
        System.out.println("gear = " + getGear());
        System.out.println("speed = " + getSpeed());
        System.out.println("motor = " + getMotor());
        System.out.println("booster = " + booster);
    }

위의 SportsCar 클래스의 printStatus() 메소드는 SportsCar에 추가된 booster 필드까지 출력할 수 있게 기존의 메소드를 오버라이딩 했다.

메소드 오버라이딩은 다음과 같은 규칙을 지켜야 합니다.

  • 기존 메소드와 이름이 같아야 함
  • 매개변수가 같아야 함
  • 반환 타입이 같아야함
  • 접근 제어자는 상위 클래스의 접근제어자의 범위보다 좁아질 수 없다.
  • 상위 클래스의 메소드 보다 많은 수의 예외를 선언할 수 없다.

실제 오버라이딩된 printStatus() 메소드를 호출한 결과는 아래와 같습니다.

class RunCar {
    public static void main(String[] args) {
        Car car = new SportsCar(1, 2, 3, 4);
        System.out.println("<<<Car>>>");
        car.printStatus();

        System.out.println();
        System.out.println("<<<SportsCar>>>");
        SportsCar newCar = (SportsCar) car;
        newCar.printStatus();
    }
}
[실행결과]
<<<Car>>>
SportsCar status
gear = 1
speed = 2
motor = 3
booster = 4

<<<SportsCar>>>
SportsCar status
gear = 1
speed = 2
motor = 3
booster = 4

위의 main함수를 보면 상위 클래스의 변수로 하위 클래스의 인스턴스를 참조할 수 있고(업캐스팅), 상위 클래스의 변수로 printStatus() 메소드를 호출하면 하위 클래스의 오버라이딩된 메소드가 호출되는 것을 알 수 있습니다.

즉, 상위 클래스의 참조변수를 통해 메소드를 호출하면, 실제 해당 메소드를 구현하고 있는 SportsCar 클래스 타입을 기준으로 찾아가 오버라이딩된 메소드를 실행하는 동적 바인딩이 발생합니다.

반대로 하위 클래스의 참조 변수로 상위 클래스의 인스턴스는 참조할 수 없다.
(하위 클래스에 구현된 멤버가 상위 클래스 보다 많거나 같으므로)

또한 이를 이용해 객체지향의 원리인 다형성을 실현합니다. 단지 Car타입의 참조변수로 Car 클래스를 상속받는 여러 하위 클래스들을 모두 참조할 수 있기 때문입니다.
즉, 하위 클래스의 호출 방식을 알지못해도 오버라이딩을 통해 접근할 수 있습니다.



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

오버라이드된 메소드가 상위 클래스의 참조를 통해 호출된다면 Java는 컴파일 타임에는 메소드의 버전(상위 or 하위 클래스)를 사용할지 결정할 수 없으므로 런타임 시간으로 이 결정을 미루게 됩니다. 이는 Java가 메소드 오버라이딩을 통해 런타임 다형성을 지원하는 방법 중 하나입니다.

따라서 상위 클래스에의해 재정의 되는 메소드가 포함되어 있다면 상위 클래스의 참조 변수를 통해 다른 유형의 객체를 참조할때 메소드의 서로 다른 버전이 실행된다. 이를 Dynamic Method Dispatch라 한다.

public class Dispatch {

	// 상위 클래스
    static class SuperClass {
        private String message;

        public String getMessage() {
            return message;
        }

        public SuperClass(String message) {
            this.message = message;
        }

        public SuperClass() {
            this.message = "[This is SuperClass]";
        }

        public void run() {
            System.out.println(getMessage() + " " + this.getClass());
        }
    }
    
	// 하위 클래스
    static class SubClass extends SuperClass {
        public SubClass() {
            super("[This is SubClass]");
        }

		@Override
        public void run() {
            System.out.println(getMessage() + " " + this.getClass());
        }
    }

    public static void main(String[] args) {
        SuperClass superClass1 = new SuperClass();
        superClass1.run();
        SuperClass superClass2 = new SubClass();
        superClass2.run();
    }
}

위는 간단한 동적 메소드 디스패치 테스트를 위한 SuperClass와 SubClass를보여준다.
SubClass는 SuperClass를 상속받고 있으며 내부에 run() 메소드를 오버라이딩 하고 있다.

main 함수를 보면 상위 클래스의 참조 변수를 통해 다른 유형의 객체를 참조할때 메소드의 서로 다른 버전이 실행되는것을 알 수 있다.
컴파일 타임에는 어떤 객체(Super or Sub)의 메소드를 실행하는지 판단하지 못해 런타임으로 미뤄지게 된다.


4-1. Double Dispatch

더블 디스패치는 메소드 디스패치를 두 번 사용하여 유연성을 높여준다. 이를 이용한 대표적인 패턴이 바로 Visitor Pattern이다.

SNS 플랫폼에 맞게 포스팅을 뿌리는 서비스를 개발하며 더블 디스패치를 이용해보자

  • 요구사항
    • SNS라는 도메인과 Post라는 서비스가 있다.
    • SNS의 구현체로는 현재 페이스북과 트위터가 있다.
    • Post는 SNS 객체를 받아서 포스트를 뿌린다.
    • Post의 구현체로는 현재 Text와 Picture가 있다.
// 인터페이스들
interface Post {
    void postOn(SNS sns);
}

interface SNS {
    void post(Post post);

}

// 구현체들
class Picture implements Post {

    @Override
    public void postOn(SNS sns) {
        sns.post(this);
    }
}

class Text implements Post {

    @Override
    public void postOn(SNS sns) {
        sns.post(this);
    }
}

class Video implements Post {

    @Override
    public void postOn(SNS sns) {
        sns.post(this);
    }
}

class Facebook implements SNS {
    @Override
    public void post(Post post) {
        System.out.println(post.getClass() + " by facebook");
    }

}

class Twitter implements SNS {
    @Override
    public void post(Post post) {
        System.out.println(post.getClass() + " by twitter");
    }
}

class Instagram implements SNS {
    @Override
    public void post(Post post) {
        System.out.println(post.getClass() + " by instagram");
    }
}

// 메인 클래스
public class DoubleDispatch {
    public static void main(String[] args) {
        List<Post> posts = Arrays.asList(new Text(), new Picture());
        List<SNS> sns = Arrays.asList(new Facebook(), new Twitter());

        posts.forEach(post -> sns.forEach(s -> post.postOn(s)));
    }
}

[실행결과]
class dispatch.Text by facebook
class dispatch.Text by twitter
class dispatch.Picture by facebook
class dispatch.Picture by twitter

Post interface는 postOn이라는 메소드를 이용해 게시물을 포스트하는데 매개인자로 SNS의 구현체를 넘겨주면 해당 SNS로 포스팅을 할 수 있다.

SNS 인터페이스는 post라는 메소드를 이용하는데 현재 Post의 구현체들을 매개인자로 받아 해당 SNS에 해당 카테고리의 Post(사진, 글)을 올리게 된다.

postOn() 메소드가 실행될때 해당 Post interface를 구현한 구현체의 메소드를 디스패치를 통해 런타임중 찾아가게 되며
postOn() 메소드 안에서 sns.post()를 실행할때 해당 SNS interface를 구현한 구현체의 메소드를 디스패치를 통해 런타임중 찾아가게 되어 두 번의 메소드 디스패치가 발생한다.


4-2. 새로운 요구사항

이것의 장점은 확장에는 열려있고 개방에는 닫혀있는 OCP의 원칙을 지킬 수 있다는 것이다.

예를들어 다음과 같은 새로운 요구사항이 들어온다고 가정해보자

  • Instagram으로 동영상을 포스팅하고 싶습니다!!

새로운 SNS 종류인 Instagram의 추가와 새로운 포스팅 방식인 Video가 추가 되었다. 더블 디스패치를 이용하면 유연하게 대처할 수 있다.

// SNS 구현체 Instagram 추가
class Instagram implements SNS {
    @Override
    public void post(Post post) {
        System.out.println(post.getClass() + " by instagram");
    }
}

위와 같이 새로운 구현체만 확장하여 추가하면 기존 코드의 변경은 없으므로 OCP를 지킬 수 있다.


5. 추상 클래스

추상 클래스는 추상적으로 선언되는 클래스이며 추상메소드를 포함할 수 있고 포함하지 않을수도 있다. 또한 추상클래스는 인스턴스화 할 수 없지만, 하위 클래스를 이용해 구현할 수 있다.

만약 추상 메소드가 하나라도 클래스에 존재한다면 해당 클래스는 반드시 추상 클래스로 선언해야 합니다.

[추상 메소드]
추상 메소드는 일반적인 메소드와 달리 abstract 키워드가 붙고, 구현이 없이 선언된 메소드 입니다.

// 추상 메소드
public abstract run();

따라서 만약 위의 run()이라는 추상메소드를 특정 클래스가 가지고 있게되면 해당 클래스는 반드시 추상 클래스가 되어야 합니다.

// 추상 클래스
public abstract class TestClass {
	public abstract void run();
    
    public void printThisClass() {
    	System.out.println(this.getClass);
    }
}

5-1. 추상 클래스의 인스턴스화

추상클래스로 선언한 클래스는 인스턴스화 할 수 없습니다. 따라서 해당 추상 클래스를 상속받아 하위 클래스를 인스턴스화 하여 해당 추상 클래스내의 멤버들을 이용할 수 있습니다.

다만, 하위 클래스가 인스턴스화 되기 위해선 추상 클래스내 모든 추상 메소드를 구현해야 합니다. 그렇지 않다면 하위 클래스도 abstract로 추상 클래스화 해야 합니다.

public abstract class TestClass {
    public abstract void run();

    public void printThisClass() {
        System.out.println(this.getClass());
    }
}

abstract class ImplementationClass1 extends TestClass {
    public abstract void jump();
}

class ImplementationClass2 extends TestClass {

    @Override
    public void run() {
        System.out.println("run!");
    }
}

ImplementationClass1은 추상 클래스를 상속받아 새로운 추상 메소드를 추가한 하위 추상 클래스이다.
ImplementationClass2는 추상 클래스를 상속받아 추상 메소드를 구현한 하위 클래스이다.



6. final 키워드

자바에서 final 키워드는 변수, 메소드, 클래스에서 사용될 수 있는 키워드이다.

6-1. 변수에서의 final

변수에 final 키워드를 붙이게 되면 해당 변수는 상수가 되어 다른 값으로 수정할 수 없게된다. 이는 기본형과 참조형에서 모두 적용되지만 참조형의 경우 참조하고 있는 인스턴스를 다른 인스턴스로 변경할 수 없지만, 인스턴스 내의 값은 변경할 수 있다.

final 변수를 초기화하기 위해서는 선언과 동시에 값을 초기화 할 수 있고, 초기화 블록(정적 초기화 블록), 생성자를 이용한 초기화를 이용해 선언과 초기화를 분리할 수 있다.

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;

public class KeywordTest {

    @Test
    void finalTest() {
        TestClass testClass = new TestClass();

        assertAll("all variable equal test",
                () -> assertEquals(testClass.AGE, 25),
                () -> assertEquals(testClass.A, 10),
                () -> assertEquals(testClass.B, 20),
                () -> assertEquals(TestClass.C, 30)
        );
    }

    static class TestClass {
        public final int AGE = 25; // 선언과 동시에 초기화
        public final int A;
        public final int B;
        public static final int C;

		// 생성자 초기화
        public TestClass() {
            this.A = 10;
        }

		// 초기화 블록
        {
            B = 20;
        }
		
        // 정적 초기화 블록
        static {
            C = 30;
        }
    }
}

6-2. 메소드에서의 final

메소드에 final 키워드를 붙이게 되면 해당 메소드는 오버라이딩 할 수 없는 최종 메소드가 되고, Object 클래스가 해당 작업을 수행한다.

주로 모든 하위 클래스에서 동일한 구현을 따라야 하는 메소드에 final 키워드를 사용해 메소드를 선언한다.

Class A {
	final void m1() {
    	System.out.println("This is final Method");
    }
}

class B extends A {
	void m1() {
    	// Compile Error
    }
}

클래스에서의 final

final 키워드로 클래스를 선언하면 final Class라 말하고 final Class는 확장(상속) 할 수 없습니다.

final Class는 확장할 수 없기 때문에 상속을 확실하게 방지할때 사용되고
(Integer, Float등과 같이 Wrapper Class는 final Class이다.)

또한 미리 정의된 String 클래스와 같이 변경할 수 없는 클래스를 만드는데 사용되기도 합니다.

final class A {
	...
}

class B extends A {
	// Compile Error
}



Object 클래스

Object 클래스는 java.lang 패키지에 포함되어 있다. 가장 큰 특징으로는 자바에 모든 클래스는 Object 클래스에서 직접 혹은 간접적으로 파생된 하위 클래스라는 점이다.

클래스가 드른 클래스를 확장하지 않는다면 Object 클래스의 직접적인 자식 클래스고 아니라면 간접적인 자식 클래스이다.

따라서 Object 클래스의 메소드들은 모든 자바 클래스에서 사용할 수 있고, 자바 프로그램에서 루트의 역할을 한다.

Object 클래스는 다음과 같이 11개의 메소드를 가지고 있다.

  • protected Object clone()
    해당 객체와 정확히 동일한 새 객체를 반환 한다.

  • String toString()
    객체의 String 표현을 제공, 객체를 String으로 변환하는데 사용된다. 기본 toString()의 동작은 클래스 이름을 출력, @출력, unsigned 16진수 표현으로 구성된 문자열을 반환함

  • int hashCode()
    모든 객체에 대해 JVM은 해시코드 고유번호를 생성함, 고유 개체에 대해 고유한 정수를 반환한다. 이 메소드는 개체의 내부 주소를 정수로 변환하여 반환한다. 하지만 hashCode() 메소드로 자바에서 객체의 주소를 찾는것은 불가능하므로 C/C++같은 네이티브 언어를 이용해 주소를 찾는다.

  • boolean equals(Object obj)
    주어진 객체와 this 객체를 비교한다.

  • Class<T\> getClass()
    this 객체의 클래스 객체를 반환해 객체의 실제 런터임 클래스를 가져온다.
    혹은 클래스의 메타데이터를 가져오는데도 사용된다.



References

profile
꾸준함이 주는 변화를 믿습니다.

0개의 댓글