Web/Java

[Java] 상속

 

학습할 것 (필수)

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

 

 

자바 상속의 특징

 

 

상속

상속이란, 기존의 클래스를 재사용하여 새로운 클래스를 작성하는 것입니다.

 

 

구현방법

자바에서 상속을 구현하는 방법은 간단합니다.

새로 작성하고자 하는 클래스의 이름뒤에 상속받고자 하는 클래스의 이름을 'extends'와 함께 써주면 됩니다.

 

class Chiled extends Parent {
    // ...
}

 

이 두 클래스는 상속관계에 있다고 하며,

 

상속해주는 클래스 (Parent) 를

  • 부모(parent)클래스
  • 상위(super)클래스
  • 기반(base)클래스

라고 합니다.

 

상속 받는 클래스 (Child)를

  • 자식(child)클래스
  • 하위(sub)클래스
  • 파생된(derived)클래스

라고 합니다.

 

 

 

class Parent { }
class Child extends Parent { }

 

 

 

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

 

class Parent {
    int age;
}

class Child extends Parent { }

 

클래스 클래스의 멤버
Parent age
Child age

 

 

 

 

 

이번엔 반대로 자손인 Child클래스에 새로운 멤버로 play() 메서드를 추가해봅니다.

 

class Parent {
    int age;
}

class Child extends Parent {
    void play () {}
}

클래스 클래스의 멤버
Parent age
Child age, play()

 

 

 

 

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

 

 

 

상속의 특징

  1. 생성자와 초기화 블럭은 상속되지 않는다. 멤버만 상속된다.
  2. 자손 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.

 

 

 

자바에서의 상속

자바 상속의 특징으로는 3가지 정도가 있습니다.

 

  1. 자바에서는 다중상속을 지원하지 않는다.
    (따라서 extends 뒤에는 단 하나의 부모클래스만 올 수 있다.)
  2. 자바에서는 상속의 횟수에 제한을 두지 않는다.
  3. C++의 경우에는 최상위 클래스가 없지만 자바에서 최상위 클래스는 Object클래스 이다.
    (Object 클래스만이 유일하게 super class를 가지지 않으며 자바의 모든 클래스들은 Object 클래스의 자손이라고 볼 수 있다.)

 

 

 

 

super 키워드

super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수 입니다.

 

클래스편에서 다루었던 this와 유사합니다.

 

2021.07.08 - [Web/Java] - [Java 스터디] 5. 클래스

 

[Java 스터디] 5. 클래스

목차 (클릭시 해당 목차로 이동) 클래스 정의하는 방법 클래스 객체를 정의해 놓은 것 객체의 설계도 또는 틀 이라고 이해하면 됩니다. *객체 클래스에 정의된 내용대로 메모리에 생성된 것 클래

ksabs.tistory.com

 

 

 

Parent에도 멤버변수 x가 있고,

Parent를 상속받는 Child에도 멤버변수 x가 있습니다.

 

이때 Child에서 x, this.x, super.x 를 모두 출력해봅니다.

package javastudy.ch6;

public class UseSuper {
    public static void main(String[] args) {
        Child c = new Child();
        c.method();
    }
}

class Parent {
    int x = 10;
}

class Child extends Parent {
    int x = 20;
    
    void method() {
        System.out.println("x = " + x);
        System.out.println("this.x = " + this.x);
        System.out.println("super.x = " + super.x);
    }
}

 

 

x, this.x 는 Child에있는 x를 가리키고

super.x는 부모클래스인 Parent의 x를 가리킵니다.

 

 

 

this, super 와 static method

this와 마찬가지로 super도 자신이 속한 인스턴스의 주소가 지역변수로 저장되기 때문에,

static메서드에서는 사용할 수 없고 인스턴스 메서드에서만 사용할 수 있습니다.

 

 

 

 

메소드 오버라이딩

 

오버라이딩

조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 오버라이딩이라고 합니다.

* override 사전적 의미 : ~위에 덮어쓰다.

 

 

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 문자열이 반환된다는 것을 기대하고 있을 것입니다.

 

그렇지만, 3차원 좌표를 다루는 Point3D클래스에서는 getLocation()을 한다면 z까지 포함한 x, y, z 문자열이 반환되어야 합니다.

 

그래서 같은 이름으로된 메소드를 다르게 작성하여 getLocation()을 하면 한 점의 좌표를 반환받도록 기대하게 할 수 있습니다.

 

 

 

 

오버라이딩의 조건

자손 클래스에서 오버라이딩 하는 메서드는 조상 클래스의 메서드와

  1. 이름이 같아야 한다.
  2. 매개변수가 같아야 한다.
  3. 반환타입이 같아야 한다.

한마디로 선언부가 모두 일치해야합니다.

 

 

 

다만 접근 제어자와 예외는 제한된 조건 하에서만 다르게 변경할 수 있습니다.

 

 

1. 접근 제어자를 조상클래스와 메서드보다 좁은 범위로 변경할 수 없다.

 

접근 제어자의 범위를 넓은 순으로 나열하면

public > protected > (default) > private

 

그러면 조상 클래스가 public으로 선언되어 있다면 자식 클래스의 오버라이딩된 메서드는 private으로 선언할 수 없고 public으로만 선언이 가능합니다.

(상식 선에서 생각하면 쉽습니다.)

 

 

 

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

 

주의해야할 점은 throws 뒤에 붙은 Exception들의 개수만을 따지는 것이 아닙니다.

 

여기서 자식클래스인 Child에서 오버라이딩한 parentMethod()의 예외는 개수가 1개처럼 보이지만, 사실 Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있도록 선언한 것입니다.

 

그래서 자식클래스의 오버라이딩 메소드의 예외가 더 많이 선언되어 불가능한 메서드입니다.

class Parent {
    void parentMethod() throws IOException, SQLException {
        ...
    }
}

class Child extends Parent {
    void parentMethod() throws Exception {
        ...
    }
}

 

 

 

3. 인스턴스메서드를 static메서드로 또는 그 반대로 변경할 수 없다.

 

상식적으로 당연한 이야기 입니다.

static 메서드는 클래스 로딩시점에서 이미 인스턴스화 되는 메서드이기 때문이므로 오버라이딩으로 변경할 수 없습니다.

 

 

 

 

 

오버로딩 vs 오버라이딩

 

오버로딩과 오버라이딩 단어가 비슷하게 생겨서 혼동하기 쉽습니다.

 

하지만 의미만 잘 알고있다면 헷갈릴 일이 거의 없습니다.

 

오버로딩은 상속과 관계가 없습니다.

그냥 이름만 같고 다른 메서드를 정의하는 것입니다.

 

오버라이딩은 상속관계에서만 등장합니다.

상속받은 메서드의 내용만 변경합니다. 반환타입, 이름, 매개변수, 예외 등 선언부는 완전 동일해야합니다.

 

오버로딩과 오버라이딩의 공통점 메서드들의 이름이 같다는 점입니다.

 

 

 

 

 

 

다이나믹 메서드 디스패치

 

 

다형성 Polymorphism

자바에서 다형성은 동일한 네이밍을 가지고 여러 형태의 액션을 취하는 테크닉을 의미합니다.

 

오버로딩과 오버라이딩을 통해 다형성을 실현하고 있습니다.

 

오버로딩을 통해 컴파일 시간 다형성을 실현할 수 있고,

오버라이딩을 통해 실행시간 다형성을 실현할 수 있습니다.

 

 

 

다이나믹 메서드 디스패치 (Dynamic method dispatch)

 

다이나믹 메서드 디스패치는 실행시간(runtime)에 실현하는 다형성을 통해 어떤 메서드를 호출할지 결정하는 것을 말합니다.

(dynamic은 runtime과 같은 의미로 쓰입니다.)

 

 

오버라이딩이 실행시간 다형성을 실현할 수 있는 이유는 클래스의 주소를 담는 참조변수에 어떤 객체가 담길지는 실행시간에 결정되기 때문입니다.

 

 

 

Super 클래스를 상속받는 Sub1, Sub2 클래스가 있다고 생각해봅니다.

 

 

 

Super 타입의 참조변수에 new Super()를 넣는 것은 당연히 가능합니다.

 

Super 타입에 new Sub1() 를 넣는 것이 가능할까요?

가능합니다.

 

Sub1은 Super타입의 모든 변수와 데이터를 가지고 있으므로 다형성을 통해 자동으로 Super 타입으로 형변환이 됩니다.

이것을 upcasting이라고 합니다.

(업캐스팅 upcasting : 상속관계에서 상위 객체로 자동 형변환이 되는 것)

 

 

package javastudy.ch6;


class Super {
    void print() {
        System.out.println("super's print");
    }
}

class Sub1 extends Super{
    @Override
    void print() {
        System.out.println("sub1's print");
    }
}

class Sub2 extends Super{
    @Override
    void print() {
        System.out.println("sub2's print");
    }
}


public class DynamicMethodDispatch {
    public static void main(String[] args) {
        Super reference = new Super(); // 1)
        reference.print();
        reference = new Sub1(); // 2)
        reference.print();
        reference = new Sub2(); // 3)
        reference.print();
    }
}

 

 

 

그렇다면 처음에 Super 타입으로 선언된 참조변수 reference는 아래 그림과 같은 순서로 주소를 담게됩니다.

(주소를 담는다 = 해당 class가 있는 메모리 영역의 주소를 담는다 = 해당 클래스를 가리킨다)

 

 

 

 

그래서 아래와 같은 결과가 나옵니다.

 



 

따라서 오버라이딩으로 다이나믹 디스패치를 적용해 실행시간에 어떤 메서드가 실행될지 정해줄 수 있다는 것을 알 수 있습니다.

 

 

 

 

추상클래스

 

클래스가 설계도라면, 추상클래스는 미완성 설계도라고 할 수 있습니다.

 

클래스가 미완성이라는 것은 미완성(추상) 메서드 하나 이상을 포함하고 있다는 의미입니다.

 

클래스 앞에 abstract를 붙여주면 됩니다.

abstract class Player {
    ...
}

 

 

추상메서드

메서드는 선언부와 구현부로 이루어 집니다.

 

추상메서드는 선언부만 있는 메서드를 말합니다.

즉, 설계는 해놓고 실제 수행될 내용은 작성되지 않았기 때문에 미완성 메서드라고 하는 것입니다.

 

 

추상메서드 역시 메서드 앞에 abstract를 붙여줍니다.

abstract class Player {
    abstract void play(int pos);
    abstract void stop();
}

 

 

추상메서드가 필요한 이유

실제 작업내용인 구현부가 없는 메서드가 무슨 의미가 있을까 싶기도 하겠지만, 메서드를 작성할 때 실제 작업내용인 구현부보다 더 중요한 부분이 선언부 입니다.

 

메서드의 이름과 메서드의 작업에 필요한 매개변수, 그리고 작업의 결과로 어떤 타입의 값을 반환할 것인가를 결정하는 것은 쉽지 않은 일입니다.

 

메서드를 사용하는 쪽에서는 메서드가 실제로 어떻게 구현되어있는지 몰라도 메서드의 이름과 매개변수, 리턴타입, 즉 선언부만 알고 있으면 되므로 내용이 없을 지라도 추상메서드를 사용하는 코드를 작성하는 것이 가능하며, 실제로는 자손클래스에 구현된 완성된 메서드가 호출되도록 할 수 있습니다.

 

 

 

 

 

 

추상클래스와 인터페이스(interface)

 

인터페이스는 일종의 추상클래스 입니다.

 

하지만 추상클래스와 달리 몸통을 갖춘 일반 메서드나 멤버변수를 구성원으로 가질 수 없습니다.

 

추상클래스가 '미완성 설계도' 라면,

인터페이스는 밑그림만 있는 '기본 설계도' 라고 볼 수 있습니다.

 

 

 

 

인터페이스의 작성

 

interface 인터페이스이름 {
    public static final 타입 상수이름 = 값;
    public abstract 메서드이름(매개변수 목록);
}

 

인터페이스 멤버의 제약사항

- 모든 멤버변수는 public static final이어야 하며, 이를 생략할 수 있다.
- 모든 메서드는 public abstract 이어야 하며, 이를 생략할 수 있다.
단, static 메서드와 디폴트 메서드는 예외

 

 

예시)

인터페이스에 정의된 모든 멤버에 예외없이 적용되는 사항이기 때문에 편의상 생략하는 경우가 많습니다.

아래 예시와 같이 생략된 제어자들은 컴파일 시에 컴파일러가 자동적으로 추가해줍니다.

package javastudy.ch6;

public interface InterfaceEx {
    public static final int SPADE = 4;
    final int DIAMOND = 3; // public static final int DIAMOND = 3;
    static int HEART = 2; // public static final int HEART = 2;
    int CLOVER = 1; // public static final int CLOVER = 1;
    
    public abstract String getCardNumber();
    String getCardKind(); // public abstract String getCardKind();
}



 

 

인터페이스의 구현

인터페이스도 추상클래스과 같이 그 자체로는 인스턴스를 생성할 수 없습니다.

 

인터페이스도 자신에 정의된 추상메서드의 몸통을 만들어주는 클래스를 작성해야합니다.

 

다만, 추상클래스가 상속을 받을 때 extends를 사용하지만 인터페이스는 구현한다는 의미의 implements를 사용합니다.

 

class 클래스이름 implements 인터페이스이름 {
    // 인터페이스에 정의된 추상메서드 모두를 구현해야 한다.
}

class Fighter implements Fightable {
    public void move(int x, int y) {/* 내용 생략 */}
    public void attack(Unit u) {/* 내용 생략 */}
}

 

 

 

인터페이스의 장점

 

- 개발시간을 단축시킬 수 있다.
- 표준화가 가능하다.
- 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.
- 독립적인 프로그래밍이 가능하다.

 

1. 개발시간을 단축시킬 수 있다.

일단 인터페이스가 작성되면, 이를 사용해서 프로그램을 작성하는게 가능합니다.

그래서 한쪽에서는 인터페이스를 작성하고, 한쪽에서 구현클래스를 작성하면 구현될때까지 기다리지 않아도 동시에 개발을 진행할 수 있습니다.

 

2. 표준화가 가능하다.

프로젝트에 사용되는 기본 틀을 인터페이스로 작성한 다음, 개발자들에게 구현하게 하면 보다 정형화된 개발이 가능합니다.

 

3. 서로 관계없는 클래스들에게 관계를 맺어 줄 수 있다.

서로 상속관계도 아닌 아무런 관계가 없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺어줄 수 있습니다.

 

이에 관해서 스타크래프트 테란의 유닛들을 예시로 이해하면 쉽습니다.

 

파란색으로 쳐진 유닛들은 SCV에 의해 repair가 가능한 유닛들입니다.

 

interface가 없다면 SCV class에서는 repair가 가능한 유닛들 별로 메소드를 따로 만들어야 할 것 입니다.

void repair(Tank t) {}
void repair(Dropship) {}
...

왜냐하면 GroundUnit와 AirUnit을 매개변수로 넣기에는 GroundUnit인 Marine은 repair가 가능하지 않기 때문입니다.

 

이 서로 상관없는 클래스들에게 관계를 맺어줄 수 있는 방법이 인터페이스 입니다. 

 

Repairable이라는 인터페이스를 만들고 repair가 가능한 유닛들이 implements하면 됩니다.

interface Repairable {}

class SCV extends GroundUnit implements Repairable {...}

class Tank extends GroundUnit implements Repairable {...}

class Dropship extends AirUnit implements Repairable {...}

 

그리고 SCV에서는 Repairable 타입을 매개변수로 받으면 됩니다.

void repair(Repairable r) {}

 

 

 

 

4. 독립적인 프로그래밍이 가능하다.

위에 3번과 같이 서로 관계없는 클래스를 관계있게 만들어도 각자의 클래스에서 구현하거나 변경한 내용은 서로 영향을 미치지 않습니다.

그래서 서로 관계가 있지만 서로 독립적인 프로그래밍이 가능하도록 만들어 줍니다.

 

 

 

 

 

final

이제까지 설명중에 final 이라는 키워드가 자주 등장했습니다.

 

final은 제어자 입니다.

 

제어자

제어자는 접근제어자와 그 외의 제어자로 나눌 수 있습니다.

접근제어자 : public, protected, default, private
그 외 : static, final, abstract, native, transient, synchronized, volatile, strictfp

 

단, 접근 제어자는 한번에 4가지 중 하나만 선택하여 사용할 수 있고, 그 외의 제어자는 여러 제어자를 조합하여 사용하는 것이 가능합니다.

참고. 제어자들 간의 순서는 관계없지만 주로 접근 제어자를 제일 왼쪽에 놓는 경향이 있습니다.

 

 

 

final

final은 '변경될 수 없는' 이라는 의미를 가지고 있습니다.

제어자 대상 의미
final 클래스 다른 클래스의조상이 될 수 없다.
메서드 final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다.
멤버변수 값을 변경할 수 없는 상수가 된다.
지역변수

 

 

생성자를 이용한 final 멤버 변수의 초기화

final 변수는 일반적으로 선언과 초기화를 동시에 합니다.

하지만 인스턴스 변수의 경우 생성자에서 초기화 되도록 할 수 있습니다.

 

생성자에서 초기화된다면 생성자에서 받는 매개변수에 따라 어떤 값으로 초기화될 지 정해줄 수 있습니다.

Class Card{
    final int NUMBER;
    final String KIND;
    
    Card(String kind, int num) {
        KIND = kind;
        NUMBER = num;
    }
}

 

 

 

 

 

Object 클래스

 

Object클래스는 이미 언급했듯이 모든 클래스의 최고 조상 클래스입니다.

 

Object 클래스는 멤버변수는 없고 오직 11개의 메서드만 가지고 있습니다.

Object클래스의 메서드 설명
protected Object clone() 객체 자신의 복사본을 반환한다
public boolean equals(Object obj) 객체 자신과 객체 obj가 같은 객체인지 알려준다.(같으면 true)
protected void finalize() 객체가 소멸될 때 가비지 컬렉터에 의해 자동적으로 호출된다. 이 때 수행되어야하는 코드가 있을 때 오버라이딩한다.
public Class getClass() 객체 자신의 클래스 정보를 담고 있는 Class인스턴스를 반환한다.
public int hashCode() 객체 자신의 해시코드를 반환한다.
public String toString() 객체 자신의 정보를 문자열로 반환한다.
public void notify() 객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨운다.
public void notifyAll() 객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨운다.
public void wait()
public void wait(long timeout)
public void wait(long timeout, int nanos)
다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간(timeout, nanos)동안 기다리게 한다.

 

 

이 중에서 쓰레드와 관련된 메서드를 제외하고 중요한 메서드 4가지만 다뤄보겠습니다.

 

 

equals(Object obj)

 

실제 Object 클래스의 equals 메서드 입니다.

Object를 매개변수로 받아 this(자신)과 비교합니다.

클래스타입의 obj와 this 는 둘다 참조변수이기때문에 자신의 주소를 저장하고 있습니다.

 

equals는 객체의 같고 다름을 주소를 비교해 결과로 얻습니다.

 

만약, 클래스의 인스턴스의 값으로 객체가 같은지를 판단해야하면 equals 메서드를 오버라이딩 하면 됩니다.

 

 

 

hashCode()

이 메서드는 해싱기법에 사용되는 해시함수를 구현한 것입니다.

Object에 정의된 hashCode()는 객체의 주소값으로 해시코드를 만들어 반환합니다.

 

Java 1.8에서는

equals 메서드가 같다고 판별한 두 객체의 hashCode 호출결과는 똑같은 Integer 값입니다.

하지만 equals 메서드가 다르다고 판별한 두 객체의 hashCode 호출결과는 반드시 다르지는 않습니다.

왜냐하면 64bit JVM에서는 8byte 주소값으로 해시코드(4 byte)를 만들기 때문에 해시코드가 중복될 수 있기 때문입니다.

 

만약, 클래스의 인스턴스의 값으로 객체가 같은지를 판단해야해 equals 메서드를 오버라이딩 했다면 hashCode 메서드도 적절히 오버라이딩 해야합니다. 같은 객체라면 hashCode 메서드를 호출했을 때의 결과값인 해시코드도 같아야 하기 때문입니다.

 

 

equals()를 재정의했다면 반드시 hashCode()도 재정의 해주어야 합니다.

 

2021.07.16 - [Web/Java] - [Java] HashSet과 HashMap에서 equals 오버라이딩시 hashCode도 재정의 해주어야 하는 이유

 

[Java] HashSet과 HashMap에서 equals 오버라이딩시 hashCode도 재정의 해주어야 하는 이유

자바의 최상위 클래스인 Object 클래스에는 equals과 hashCode 메서드가 있습니다. Object의 equals는 객체의 주소르 비교하여 같은 객체인지 확인합니다. 그렇다면, 만약에 이름과 나이가 같다면 "같다"

ksabs.tistory.com

 

 

 

toString()

 

실제 Object 클래스의 toString 코드

 

이 메서드는 인스턴스에 대한 정보를 문자열로 제공할 목적으로 정의한 것입니다.

클래스를 작성할 때 toString()을 오버라이딩 하지 않고 사용한다면 위의 내용이 그대로 사용되어 16진수의 hashCode가 출력됩니다.

 

 

 

 

clone()

이 메서드는 자신을 복제하여 새로운 인스턴스를 생성합니다.

 

하지만 단순히 인스턴스변수의 값만 복사하기 때문에 참조타입의 인스턴스 변수가 있는 클래스는 "복제"가 되지 않습니다.

참조타입의 인스턴스 변수는 주소를 저장하고 있기때문에 주소가 복사되기 때문에 복제된 인스턴스의 작업이 원래의 인스턴스에 영향을 미치게 됩니다.

 

예를들어 배열의 경우 clone 메서드를 오버라이딩하고, 새로운 배열을 생성해 배열의 내용을 복사하도록 해야합니다.

 

clone() 사용하기

1. 복제할 클래스가 Cloneable 인터페이스를 구현해야 한다.
2. clone()을 오버라이딩 하며 접근 제어자를 protected에서 public으로 변경한다.
(그래야만 상속관계가 없는 다른 클래스에서 clone() 호출가능)
3. 조상클래스의 clone()을 호출하는 코드가 포함된 try-catch문을 작성한다.

 

clone()을 사용하기 위해서는 Cloneable 인터페이스를 implements해야합니다.

왜냐하면 인스턴스의 데이터를 보호하기 위해 클래스 작성자가 복제를 허용(Cloneable을 implements)해야 하기 때문입니다.

 

 

Object의 clone에는 cloneNotSupportedException 을 throws 해줍니다.

그래서 사용할 때에도 clone()을 호출하는 코드가 포함된 try-catch문을 작성해야합니다.