Java

[Java] 클래스의 상속과 다형성 (꼭 알아야 하는 내용만 뽑아서 정리)

펭귄코기 2022. 9. 26. 16:55

상속

부모 클래스의 멤버 (필드, 메서드, 이너클래스) 를 내려받아

자식 클래스 내부에 포함시키는 자바의 문법 요소

 

상속의 개념을 이해하기 위해 예시를 들어보자

운동선수와 프로그래머가 있다고 가정을 한다

  운동선수 프로그래머
필드 이름, 나이, 선수 등번호 이름, 나이, 사원번호
메서드 먹기, 잠자기, 운동기술 먹기, 잠자기, 코딩하기

 

위 와 같이 나열을 해보면 둘다 운동선수, 프로그래머 이전에 사람이기 때문에

이름과 나이가 있고 먹고 자는 행위가 공통으로 있다는것을 볼 수 있다

 

이걸 상속으로 표현을 해보면 아래 와 같다

사람 운동선수 프로그래머
이름, 나이 선수 등번호 사원번호
먹기, 잠자기 운동기술 코딩하기

 

사람 클래스가 부모클래스가 되는것이고

운동선수, 프로그래머가 자식클래스가 되는것이다

 

두 자식클래스는 부모의 모든 멤버를 내려받기 때문에 본인들의 고유의

필드와 메서드만 구성하면 되는것이다

반대로 부모클래스는 자식클래스들의 공통적 특징을 모아 구성한 클래스라고 보면 된다

 

* UML 기호에서 화살표는 부모클래스를 향하게 그려야한다

(동물 <- 사람 <- 학생 <- 대학생)

 

상속의 장점

- 코드의 중복성 제거

- 다형적 표현이 가능 / 1개의 객체를 여러가지 모양으로 표현 가능한것을 다형성 이라 함

대학생은 사람이다 O, 사람은 대학생이다 X

 

상속 문법

상속할 때는 extends 키워드를 사용한다

자바의 클래스는 다중 상속이 불가능하다

이유는 부모클래스인 A클래스, B클래스 필드에 같은 이름으로 data 라는 필드가 있는데

자식클래스인 C클래스에서 필드를 모두 내려 받을때 모호성이 발생하기 때문에

여기서 부모가 여러명이면 안된다는것이고 자식은 여러명이라도 상관없다

 

class 자식클래스 extends 부모클래스 { }

 

이때 상속을 하면 부모의 필드, 메서드를 전부 내려 받는다는데 이게 가능한 이유는

자식클래스의 객체를 생성할때 자바 가상 머신이 제일 먼저 부모 클래스의 객체를 생성하기 때문이다

이후 자식 클래스에서 추가한 필드와 메서드가 객체에 추가 됨으로써 자식클래스가 완성되는 것이다

즉 자식클래스 객체의 내부에는 부모 클래스 객체가 포함돼 있으므로 자식클래스 객체에서

부모클래스의 멤버를 사용할 수 있는 것이다

 

생성자의 상속 여부

부모의 모든 멤버(필드, 메서드, 이너클래스) 를 다 내려받지만 생성자는 상속받지 않는다

이유는 생성자를 상속 받았다고 치더라도 A( ) { } 이런 모양을 나타낼텐데

생성자라 하기엔 클래스명과 이름이 다르고

메서드라 하기엔 리턴 타입이 없고

필드나 이너클래스라 하기엔 소괄호와 중괄호가 있다

이러한 이유로 생성자는 상속이 되지 않는 것이다

 

객체의 다형적 표현

A 클래스를 상속받는 B클래스를 만들때

 

A a1 = new A();		// A는 A다
A a2 = new B();		// B는 A다

 

객체의 타입 변환

기본 자료형 에서 범위가 좁은 쪽에서 넓은 쪽으로 캐스팅 하는것을 업캐스팅

범위가 넓은 쪽에서 좁은 쪽으로 캐스팅 하는것을 다운캐스팅

 

객체 에서는 자식 클래스에서 부모 클래스 쪽으로 캐스팅 하는것을 업캐스팅

부모 클래스에서 자식 클래스로 캐스팅 하는것을 다운캐스팅

 

객체는 항상 업캐스팅 할 수 있으므로 컴파일러가 자동으로 넣어준다

하지만 다운 캐스팅은 개발자가 직접 명시적으로 넣어줘야한다

 

기본 자료형에서는 다운 캐스팅을 할 때 오차가 발생하긴 하지만 문법적으로 가능했다

객체에서는 다운 캐스팅 할 때 될수도 있고 아닐수도 있다

사람은 학생이다 라고 했을때 학생일 수도? 아닐수도? 있기 때문이다

 

캐스팅의 가능여부는

무슨 타입으로 선언돼 있는지가 중요하지 않고

어떤 생성자로 생성됐는지가 중요하다

 

class A { }
class B extends A { }
class C extends B { }

// 자동 타입 변환 (업캐스팅)
B b1 = new B( );
A a1 = (A) b1;		// 자동으로 컴파일러가 추가 해줌

C c2 = new C( );
B b2 = (B) c2;		// 자동으로 컴파일러가 추가 해줌
A a2 = (A) c2;		// 자동으로 컴파일러가 추가 해줌


// 수동 타입 변환 (다운캐스팅)
A a1 = new A( )
B b1 = (B) a1;		// 예외 발생 이유는 부모 생성자로 객체 생성했는데 자식으로 가려해서

A a2 = new B( );
B b2 = (B) a2;		// 가능하다 이유는 B생성자로 객체를 생성했기 때문에
C c2 = (C) a2;		// 예외 발생 16번째 줄과 동일한 이유

 

메모리로 이해를 더 해 보겠다

만약 A <- B <- C 상속 관계를 가지고 A a = new B( ) 로 예시를 들어 보자면

B( ) 생성자로 만들었기에 힙영역에 부모 A( ) 생성자를 먼저 부르고 B( ) 를 부른다

그리고 A a 로 A타입의 a 참조변수를 사용했는데 이때 A( ) 생성자를 가리키고 있다고 보면 된다

B b = (B) a; 이렇게 다운 캐스팅하면 B( ) 생성자를 가리키게 되는것이다

여기서 C c = (C) a; 를 하면 힙 영역에 C( ) 생성자를 만든적이 없기에 가리키지 못해 다운캐스팅을 못한다

 

선언 타입에 따른 차이점

아래 코드와 같이 클래스가 있다고 가정을 하고

B b = new B( ); 이렇게 B( ) 생성자로 B타입 참조변수로 객체를 생성하면

B클래스에 있는 멤버와 A클래스에 있는 멤버 모두 사용가능하다

A a = new B( ); 이렇게 B( ) 생성자로 A타입 참조변수로 객체를 생성하면

A클래스에 있는 멤버만 사용할 수 있다

 

이는 앞에서 메모리로 이해를 했을때 이해를 했다면 쉽게 이해가 된다

 

만약 안된다면 다른 예시를 들어 보자면

앞쪽에 A a 인 타입 A, 참조변수 a는 일반 TV의 리모컨이다

B( ) 생성자는 스마트 TV 라고 보면된다

그러면 스마트 TV에 있는 기능은 사용 못하지만 일반 TV에 있는 기능은 사용 가능한 것이다

B b인 타입 B, 참조변수 b는 스마트 TV의 리모컨이다

B( ) 생성자는 스마트 TV 이기에 모든 기능을 다 사용 할 수 있는것이다

 

class A {
	int m = 3;
    void abc() {
    	System.out.println("A");
    }
}

class B extends A {
	int n = 5;
    void bcd() {
    	System.out.println("B");
    }
}

 

캐스팅 가능 여부를 확인하기

참조변수 instanceof 타입
b instanceof A

// 캐스팅 가능 : true
// 캐스팅 불가능 : false

 

만약 A <- B <- C 상속 구조에서 C c = new C( ); 로 객체를 생성했을때

C에서 A, B로 다운 캐스팅 하는게 가능하다 그래서

c instanceof A, c instanceof B, c instanceof C 모두 다 true 가 나오게 된다

 

메서드 오버라이딩

부모 클래스에서 상속받은 메서드와 동일한 이름의 메서드를 재정의 하는것

부모의 메서드에 자신이 만든 메서드로 올라 타는 개념이다

그래서 원할 때 밑에 깔려 있는 부모 메서드를 호출 할 수 있는 것이다

 

메서드 오버라이딩 조건

- 부모 클래스의 메서드와 메서드명, 입력매개변수의 타입 과 개수, 리턴타입이 동일해야 한다

- 부모 클래스의 메서드보다 접근 지정자의 범위가 같거나 넓어야 한다

 

class A {
	void print( ) {
    	System.out.println("A 클래스");
    }
}

class B extends A {
	void print ( ) {
    	System.out.println("B 클래스");
    }
}

//다형성일때 메서드 불러오는 값

A aa = new A();
aa.print();

B bb = new B();
bb.print();

A ab = new B();
ab.print();

// 이랬을때 아래와 같은 순으로 출력
A클래스
B클래스
B클래스

 

위에 코드를 보면 오버라이딩에 대해서는 쉽게 이해가 되는데 다형성일때 헷갈릴 수 있다

메모리에서 메서드는 클래스영역에 모이고 메서드명, 타입, 매개변수 다 같기에 공유하고있다

A( ) 생성자로 만들어서 A 타입으로 aa 참조변수로 출력하면 A( ) 생성자의 메서드를 출력하고

B( ) 생성자로 만들어서 B 타입으로 bb 참조변수로 출력하면 B( ) 생성자의 메서드를 출력한다

이때 B( ) 생성자로 만들어서 A타입으로 ab 참조변수로 출력하면 원래는 A( ) 생성자의 메서드를 출력하는데

메서드는 B객체의 print( ) 가 A 객체의 print( )를 오버라이딩 하고 있기때문에 B클래스가 출력된것이다

 

메서드 오버라이딩 쓰는 이유

부모클래스인 동물 클래스 와 자식클래스인 강아지, 고양이, 새 클래스가 있다고 가정하자

이때 강아지, 고양이, 새 가 동물을 상속받아서 있다고 하고 각각의 타입으로 선언해주면 아래와 같다

 

Animal aa = new Animal();
Bird bb = new Bird();
Cat cc = new Cat();
Dog dd = new Dog();

aa.cry();
bb.cry();
cc.cry();
dd.cry();

 

그러나 부모클래스 타입으로 선언을 해주면 아래와 같다

이렇게 해주면 부모 클래스 내부의 메서드만 사용할 수 있게 된다

 

Animal ab = new Bird();
Animal ac = new Cat();
Animal ad = new Dog();

ab.cry();
ac.cry();
ad.cry();

 

이렇게 모든 객체를 부모 타입 하나로 선언해 두면 뭐가 좋냐 하면

배열로 한번에 관리가 가능하는 장점이 있다

 

Animal[ ] animals = new Animal[ ] { new Bird(), new Cat(), new Dog() };
for (Animal animal : animals) {
	animal.cry( );
}
// 짹짹, 야옹, 멍멍

 

인스턴스 필드와 정적 멤버의 중복

인스턴스 필드랑 정적 필드, 정적 메서드가 오버라이딩이 될까를 생각해보면 안된다

 

인스턴스 필드는 힙 영역에 값이 저장되는게 이때 객체가 다르면

각각 다른 객체에 담기기 때문에 오버라이딩 되는게 아니라 애초에 따로 나누어져 있는것이다

 

정적 필드도 똑같다 클래스 영역에 A객체 따로 B객체 따로 생성이 되기 떄문이다

정적 필드가 값을 공유하는 것은 같은 객체에서 값을 새로 덮어쓰기 했을때 일어난다

 

정적 메서드도 똑같다 인스턴스 메서드는 동일한 공간에 동일한 이름의 메서드를 저장하기에 그런데

정적 메서드는 정적 필드와 마찬가지로 각자의 클래스 내부에 존재하기 때문이다

 

* 객체가 어떤 생성자로 생성됐는지, 나머지는 어떤 타입으로 선언 됐는지를 살펴보면 좋을듯 하다

 

super 키워드와 super( ) 메서드

먼저 this와 super의 차이를 간략하게 보자면

this는 자신의 객체, this( )는 자신의 생성자를 의미

super는 부모의 객체, super( )는 부모의 생성자를 의미

 

이어서 super 키워드를 보자면

필드명의 중복 또는 메서드 오버라이딩 으로 가려진 부모의 필드 또는 메서드를 호출하기 위해 사용한다

그렇게 해야하는 이유를 예시로 알아보겠다

 

클래스 A에는 abc( ) 메서드가 있고 상속받은 B 클래스에는 abc( ) 메서드를 오버라이딩하고

추가로 bcd( ) 메서드를 생성했다 여기서 bcd( ) 메서드안에 abc( ) 메서드를 호출하면

A클래스 abc( ) 메서드와 B클래스 abc( ) 메서드 중에 어디를 부를까?

인스턴스 메서드의 내부에서 모든 필드와 메서드 앞에 있는 객체를 생략하면

this 키워드가 추가된다 그래서 this.abc( ) 가 되는거고 B클래스의 메서드가 출력되는 것이다

 

class A {
	void abc() {
    	System.out.println("A 클래스의 abc()");
	}
}

class B extends A {
	void abc() {
    	System.out.println("B 클래스의 abc()");
    }
    void bcd() {
    	abc();		// this.abc();
    }
}

bb.bcd;		// B클래스의 abc()

 

이때 부모의 abc( ) 메서드로 부르고 싶으면 super를 쓰는것이다

super.abc( ); 이런식으로 말이다

 

부모 클래스의 생성자를 호출하는 super() 메서드

자식 객체를 생성하면 자식생성자가 아닌 부모 생성자가 먼저 생성되는것을 앞에서 배웠다

그 이유가 super( ) 때문이다 모든 생성자의 첫줄에는 반드시 this( ) 또는 super( ) 가 있어야한다

만약 아무것도 없으면 컴파일러가 반드시 super( ) 를 자동으로 생성해준다

 

이때 부모 클래스에 A(int a) 이렇게 생성자가 있는데 B에 아무것도 생성 안하면 어떻게 되는가?

오류가 발생한다 컴파일러는 기본 super( )를 생성했는데 A클래스에 기본 생성자가 없기 떄문이다

이때는 수동으로 super(3) 이런식으로 작성 해줘야한다

 

최상위 클래스 Object

자바의 모든 클래스는 Object 클래스를 상속받는다

즉 Object 클래스는 자바의 최상위 클래스이다

 

* 아무런 클래스로 상속하지 않으면 자동으로 extends Object를 삽입해 Object 클래스를 상속한다

 

System.out.println(new A())

 

그래서 위 와 같이 사용자가 직접 만든 클래스 타입도 출력할 수 있다

 

이는 println( ) 메서드의 입력매개변수를 보면 되는데

System.out.println(Object x) 를 보면 알 수 있다

 

기본 자료형 이외에 Object를 입력매개변수로 하는 println( ) 메서드를 오버로딩 해 놓은 것이다

이렇게 하면 사용자가 어떤 클래스 타입의 객체를 생성하더라도

다형성에 따라 Object 타입이라고 불릴 수 있으므로 입력매개변수로 모든 타입의 객체를 받을 수 있다

 

toString() : 객체 정보를 문자열로 출력

생략했을때는 자동으로 추가 된다

참조자료형들을 출력하면 패키지.클래스명@해시코드 식으로 출력이 될텐데

이는 참조 변수가 주소값을 가지고 있기 때문이고 이를 값을 얻기 위해

Object 클래스의 toString( ) 메서드를 사용한다

그러나 객체의 직관적인 정보를 얻지 못해서 보통 오버라이딩 해서 사용한다

 

class B {
	int a = 3;
    int b = 4;
    @override
    public String toString() {
    	return "필드값 : a = " +  a + " , b = " + b;
    }
}

 

equals (Object obj) : 스택 메모리의 값 비교

입력매개변수로 넘어온 객체와 자기 객체의 스택 메모리 변숫값을비교해

그 결과를 true 또는 false로 리턴하는 메서드이다

실제 데이터의 위치를 비교하는 것이다

이 또한 사용하려면 오버라이딩 해서 사용하면 된다

 

class B {
	String name;
    B(String name) {
    	this.name = name;
    }
    @override
    public boolean equals(Object obj){
    	if (obj instanceof B){
        	if (this.name == ((B) obj).name){
            	return true;
            }
        }
        return false;
    }
}

 

hashCode( ) : 객체의 위치와 연관된 값

객체의 위치와 관련된 값으로 실제 위치를 나타내는 값은 아니다

객체의 위칫값을 기준으로 생성된 고윳값 정도로 생각하는것이 적절하다

Hashtable, HashMap 등에서 동등 비교를 하고자할때는 hashCode() 까지 오버라이딩 해야한다