상속

dev_314·2022년 10월 10일
0

| 백기선님의 라이브 스터디를 참고하여 작성한 게시물입니다.

참고: Inheritance

자바의 상속

상속과 관련된 자바의 여러 특징을 알아보자

자바는 다중 상속을 지원하지 않는다.

참고: Java and Multiple Inheritance

여러 클래스를 상속했을 때 여러 문제들이 발생해서 자바는 다중 상속을 지원하지 않는다.

모든 클래스는 최상위 클래스 'Object'를 상속받는다

참고: How do all classes inherit from Object?

보통 클래스를 선언할 때 extends Object를 적지 않는다. 그럴때마다 자바 컴파일러가 자동으로 extends Object를 대신 적어준다.
Object 클래스는 유일하게 superclass가 없는 class이다.

바이트 코드에서 살펴보기


상위 클래스에 대한 정보가 기록되어 있다.

상위 클래스의 이름, 생성자, 데이터 타입등이 기록되어 있다.

상위 클래스의 생성자를 호출(super())하고, 변수를 초기화하는 것 같다.

상속과 Access modifier

아래와 같은 구조가 있다.

package test;


public class Parent {

    private int a;
    int c;
    protected int b;
    public int d;

    public static void main(String[] args) {
        Parent parent = new Parent();
        System.out.println(parent.a);
        System.out.println(parent.b);
        System.out.println(parent.c);
        System.out.println(parent.d);
    }
}

자기 자신에선 당연히 전부 접근 가능

package test;

public class Child1 extends Parent {
    public static void main(String[] args) {
        Child1 child1 = new Child1();
        System.out.println(child1.b); // protected
        System.out.println(child1.c); // default
        System.out.println(child1.d); // public
    }
}

같은 패키지에서 부모를 상속한 자식에서는 private은 상속 불가능

package test2;

import test.Parent;

public class Child2 extends Parent {

    public static void main(String[] args) {
        Child2 child2 = new Child2();
        System.out.println(child2.d); // public
        System.out.println(child2.b); // protected
    }
}

다른 패키지에서 부모를 상속한 자식에서는 private, default는 상속 불가능

super 키워드

참고: super와 super()

super, this

기본적으로 super는 상속한 클래스에 정의된 인스턴스, 메소드에 접근한다.
그런데 다음과 같은 특이사항이 있다.

class Parent {
	int p;
}

class Child extends Parent {

	void print() {
        System.out.println(super.p); // Parent에 정의된 p에 접근
		System.out.println(this.p); // Child에 정의된 p가 없으므로, Parent에 정의된 p에 접근
    }
}
class Parent {
	int p;
}

public class Child extends Parent {
    int p;

    public Child(int c) {
        super.p = c - 1;
        this.p = c - 2;
    }

    void print() {
        System.out.println(p); // 기본적으로 this에 먼저 접근
        System.out.println(super.p);
    }

    public static void main(String[] args) {
        Child child = new Child(10);
        child.print();
    }
}

super(), this()

public class Parent {
    public Parent() {
        System.out.println("parent constructor");
    }
}

public class Child extends Parent {
    int a;

    public Child() {
        System.out.println("child constructor");
    }

    public Child(int a) {
    }

    public static void main(String[] args) {
        Child child = new Child();
        Child child2 = new Child(10);

    }
}

지난 주에 공부했던 클래스의 생성자 법칙을 떠올려보면,

1. 생성자는 this() 또는 super()를 호출해야 한다.
2. this() 또는 super()를 호출하지 않으면 자바 컴파일러를 통해 자동으로 super()를 호출한다.

child는 다음 과정을 통해 초기화된다.

1. 생성자 Child()를 호출
2. this() 또는 super()를 호출하지 않음 -> 자동으로 super()호출
3. 생성자 Parent()를 호출 -> 내부적으로 Object의 생성자를 호출

child2는 다음 과정을 통해 초기화된다.

1. 생성자 Child(int a)를 호출
2. this() 또는 super()를 호출하지 않음 -> 자동으로 super()호출
3. 생성자 Parent()를 호출 -> 내부적으로 Object의 생성자를 호출

하나 이상의 생성자가 있으면, 컴파일러는 default constructor를 생성해 주지 않는다. 그러므로 다음과 같은 상황에 유의해야 한다.

class Parent {

    int a;

    Parent(int n) { a = n; }
}

 

class Child extends Parent {

    int b;

    Child() {
        super(); // Error: 실행할 생성자가 없음
        b = 20;
    }
}

메소드 오버라이딩

바이트 코드에서 살펴보기

다음 코드의 바이트 코드를 살펴보자

package inheritance;

public class Parent {
    void overrideMe() {
    }
}

public class Child extends Parent {
    @Override
    void overrideMe() {
        super.overrideMe();
    }

    public static void main(String[] args) {
        new Child().overrideMe();
    }
}


크게 특이한 내용 없이, 부모 클래스의 메소드를 호출한다.

@Override

보통 IDE의 도움을 받기 때문에 오버라이딩을 할 때 실수할 일이 많지는 않다.
그렇다고 프로그래밍 언어 측면에서 실수 가능성이 사라진건 아니다.

자바5 부터 Annotation을 지원하기 시작했는데, Annotation 중 @Override를 심심치 않게 발견할 수 있다.

@Override 자바 컴파일러에게 해당 메소드는 Override된 것이야라고 알려주는 역할이다. 이를 통해 컴파일 타임에 잘못된 override를 검출할 수 있다.

Annotation에 대해선 추후에 자세히 다루도록하겠다.

final

참고
[Java] final 그게 뭔데, 어떻게 쓰는 건데
자바에서 final에 대한 이해

특정 메소드를 하위 클래스에서 오버라이딩 하지 못하게 하고싶다.
그런데 상속 계층이 복잡해지다 보면 뭐가 뭔지 구분이 안 되는 경우가 종종 있다.

메소드에 final 키워드를 붙이면 하위 클래스에서 override하지 못하는 메소드임을 자바 컴파일러에게 알려줄 수 있다.

final class MyFinal {} // 해당 클래스를 상속할 수 없음

class Parent {
	
    final void myMethod() {} // 해당 메소드를 하위 클래스에서 override할 수 없음
}

class Child extends Parent {

	@Override
    void myMethod() {} // Error
} 

그 외에도 variable에 사용하여 최초의 초기화 이후에는 값을 변경할 수 없는 변수임을 나타낸다.

class MyClass {
	final int a = 10; // 변경 불가능
    final boolean b; // 생성자 또는 초기화 블록등에서 반드시 초기화를 해줘야 한다 (blank final)
    
	MyClass(boolean b) {
    	this.b = b;
    }    
    
    void myMethod(fianl int a) { // 파라미터에도 final 키워드를 사용할 수 있다.
    	final int a = 10;
        a = 20; //Error
        
        final MyClass myClass = new MyClass(false);

		myClass.setB(true); // not Error
        myClass = new MyClass(true); // Error
    }
}

고정 불변하는 상수값을 표현하기 위해 static을 같이 사용하면 메모리 측면에서 이득이 있다고 한다.

static method는 override할 수 없다.

class Parent {
	static void print() {
    	System.out.println("parent");
    }
}

// case1: 자식에서 override 시도
class Child extends Parent {
	static void print() {
    	System.out.println("child");
    }
    
    public static void main(String[] agrs) {
    	Child.print(); // child
    }
}

// case2: static method가 상속이 안되는건 아니다. 단순히 override할 수 없을 뿐

class Child extends Parent {
    public static void main(String[] agrs) {
    	Child.print(); // parent
    }
}

부모의 static method와 자식의 static method는 별개로 인식한다.
@Override를 붙이면 컴파일 에러가 작동한다.

즉, static method는 override할 수 없다.

메소드 오버로딩이랑 맨날 헷갈림

오버로딩: 메소드 이름은 같으나 파라미터 개수, 타입이 다른 상황 / 리턴 타입은 영향 X

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

참고: Dynamic Method Dispatch or Runtime Polymorphism in Java

메소드 오버라이딩은 자바가 Runtime Polymorphism을 지원하는 방법 중 하나이다.
Dynamic method dispatch란 오버라이딩 된 메소드를 컴파일 타임이 아닌, 런타임에 결정(resolve)하는 메커니즘을 의미한다.

오버라이딩된 메소드가 superclass reference을 통해 호출될 때, 자바는 메소드를 호출한 객체의 타입에 따라, '호출 시점'에 상위 클래스, 하위 클래의 메소드중 어떤 메소드를 실행할지 결정한다.

이때 중요한 점은, 변수의 타입이 아닌, 인스턴스의 타입에 따라 실행할 메소드가 결정된다는 사실이다.

public class Parent {

    void print() {
        System.out.println("parent");
    }
}

class Child extends Parent {
    @Override
    void print() {
        System.out.println("child");
    }

    public static void main(String[] args) {
        Parent p1 = new Child(); // upcasting
        Parent p2 = new Parent();

        p1.print(); // child
        p2.print(); // parent
    }
}

상위클래스의 변수로 하위 클래스 객체를 가리킬(refer)수 있는데, 이를 upcasting이라고 한다.
자바는 이 특성을 이용해 override된 method를 resolve한다.

static binding

Static binding은 컴파일 타임에 바인딩이 이뤄지는 방식을 의미한다.
자바의 private, final, static 메소드와 varaible은 static binding 방식을 통해 컴파일 타임에 binding된다.

이처럼 동적으로 실행할 method가 resolve되는 특징은 일반(?) 메소드에만 적용된다.

public class Parent {

	int a = 10;

    void print() {
        System.out.println("parent");
    }
}

class Child extends Parent {

	int a = 20;
    int b = 30;

    @Override
    void print() {
        System.out.println("child");
    }

    public static void main(String[] args) {
        Parent p1 = new Child(); // upcasting
        Parent p2 = new Parent();

        p1.print(); // child
        p2.print(); // parent
        System.out.println(p1.a); // 10
        System.out.println(p2.a); // 10
    }
}

down casting

Parent p = new Child();
System.out.println(p.a); // 10
System.out.println(p.b); // Error : Parent타입 변수에는 b라는 varaible이 없다

// Down Casting 실시
Child c = (Child) p;
System.out.println(c.b); // 30

추상 클래스

추상 메소드를 하나라도 가지고 있는 클래스를 의미한다.
추상 메소드란 자식 클래스에서 반드시 오버라이딩해야만 사용할 수 있는 메소드를 의미한다.

public abstract class MyAbstract {

    abstract void myAbstractMethod(); // method body없이 header만 정의했다.

    void myNotAbstractMethod() { // abstract 클래스는 abstract method만 가지는건 아니다.
        ...
    }
}

class Child extends MyAbstract {

    @Override
    void myAbstractMethod() { // MyAbstract의 추상 메소드를 전부 override하지 않으면 컴파일 에러
		...
    }
}

인터페이스와의 차이점은 인터페이스 파트에서 다루도록 하겠다.

바이트 코드에서 살펴보기


일반적인 메소드와 달리 Code부분이 없다.
아마 인터페이스의 메소드도 비슷한 형태를 띠지 않을까 싶다. 인터페이스 파트에서 살펴보도록 하겠다.

Object 클래스

참고: Object 클래스

모든 클래스의 부모 클래스(최상위 클래스)

equals

참고:

잠깐 비교 연산자 (==)

연산자 파트 공부를 대충해서 지금 벌 받는다.

비교 연산자(==)는 primitive type data에 대해서는 값 비교, reference type data에 대해선 주소값 비교를 수행한다.

primitive data의 값 비교도 사실 주소값 비교에 기반한다.
constant pool에 값이 저장되어 있고, primitive data을 할당받은 변수들은 결국 동일한 constant pool주소를 가리키고 있으므로, ==연산을 수행해도 의도대로 작동한다.

반면 reference type data는 각 데이터가 heap 영역에 별도로 생성, 할당되므로 == 비교를 사용할 수 없다. 해도 되는데 의도대로 작동하진 않을 것이다.

다시 Object의 equals로

Object 클래스의 equals는 내부적으로 == 연산자를 사용한다. 즉, Object의 equals는 비교 연산자와 동일하게 작동한다.

public class Object {

	public boolean equals(Object obj) {
		return (this == obj);
    }
}

따로 equals를 override하지 않으면 Object의 equals를 사용한다.

String의 equals

스트링은 ==으로 비교하지 마라. 대신 equals로 비교하라

String 클래스는 equals를 override해서 주소값 비교가 아닌, 진짜 문자열 비교(?)를 수행한다.

	System.out.println("123".equals("123")); // String의 equals는 진짜 문자열 비교(?)를 수행 -> true
	System.out.println("123" == "123"); // constant pool의 동일한 주소를 가리킨다 -> true
        
	String s = new String("123"); // 변수 s는 heap 영역에 저장
	String s1 = new String("123"); // 변수 s1는 heap 영역에 저장
	System.out.println(s == s1); // s의 주소 != s1의 주소 -> false
	System.out.println(s.equals(s1)); // String의 equals는 진짜 문자열 비교(?)를 수행 -> true

hashcode

참고
[java] hashcode()와 equals() 메서드는 언제 사용하고 왜 사용할까?
Guide to hashCode() in Java

Object 클래스의 hashcode는 주소값을 hash한 결과인 int값을 반환하는 native로 작성된 메소드이다.

이걸 어디에 쓰나 할 수 있는데, 생각보다 많은 곳에 쓰이더라.

대표적으로 hashXXX(hashMap, hashSet, hashTable, ...)에서 쓰인다.

profile
블로그 이전했습니다 https://dev314.tistory.com/

0개의 댓글