자바 공부 기록 2회독(3) - 2024.1.18

동준·2024년 1월 18일
0

개인공부(자바)

목록 보기
5/16

3. 다형성

다형성은 코드 중복을 방지하고 객체지향적 관점에서 재사용성과 유연성을 높이며 나아가 확장성을 고려할 수 있다.
자바에서의 다형성 구현의 대표적인 예시는 앞서 봤던 오버로딩 외에도 메서드 오버라이딩이 있으며, 메소드 오버라이딩의 실질적인 구현 방법인 상속인터페이스가 있다.

1) 상속

자바에서의 상속 키워드는 extends

사실 말이 상속이지, 기능 확장에 가깝다. 상속 받은 자식 클래스가 부모 클래스의 필드와 생성자 및 메소드를 포함해서 자신만의 필드, 생성자, 메소드를 가지기 때문에. 이는 인터페이스에서도 비슷한 맥락이다.

여담으로, 자바는 다중 상속이 안된다.

B extends A
// 부모 클래스 : A
// 자식 클래스 : B

// B extends A1, A2 // 불가능

앞서 말했던 자식 클래스는 본인의 것 외에도 부모의 것까지 사용할 수 있다. 즉, 부모의 코드를 중복해서 자식에게 작성하지 않고도 자식은 부모의 것을 사용할 수 있으므로 이 또한 다형성의 구현 방법이 된다.

(1) 메소드 오버라이딩

상속된 메소드의 자식 클래스에서의 재정의로 아래의 조건을 준수해야만 한다.

  • 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)와 동일해야 한다.
  • 접근 제한을 더 강하게 오버라이딩 할 수 없다.
  • 새로운 예외를 throws 할 수 없다.

어노테이션 적극 활용하기(어노테이션은 나중에 공부)

class Calculator {
	public double getArea(double r) {
    	return 3.14159 * r * r;
    }
}

class Computer extends Calculator {
	@Override // 컴파일 시, 오버라이딩 조건 준수했는지 체킹
	public double getArea(double r) {
 		return Math.PI * r * r;   
    }
}

메소드가 오버라이딩되면, 오버라이딩당한 부모의 원본 메소드는 숨겨지는 것이 기본이다. 하지만, super 키워드를 사용하면 본래 부모의 원본 메소드를 불러올 수 있다.

@Override
public void methodExample(int parameter) {
	super.methodExample(parameter); // 부모의 원본 메소드 호출
}

(2) 클래스와 메소드에서의 final

클래스에 final을 붙이면 더 이상 상속 불가능한 클래스가 된다. String 클래스를 보면 final이 붙여진 채로 선언되어 있다.

public final class String { // 상속 불가
	// ...

메소드에 final을 붙이면 더 이상 오버라이딩 불가능한 메소드가 된다. 즉, 자식 클래스에서 재정의가 불가능해진다.

public final 리턴타입 메소드(매개변수) { // 오버라이딩 불가
	// ...

(3) 상속에서의 다형성 실현

class A {
	method : a1, a2, ab
}

class B extends A {
	method : b1, b2, ab (@Override)
}

B 클래스는 A 클래스를 상속받으며 A 클래스의 메소드 ab를 재정의하고 있다.
그리고 다형성 구현할 때 뜻도 모르고 공식처럼 써먹은 내용... A a = new B();
이게 왜 다형성 실현인가 다시 또 공부를 해보니까

A a = new B(); // 자동 타입 변환

a.a1();
a.a2();
// a.b1(); 에러
// a.b2(); 에러
a.ab(); // 오버라이딩 된 메소드 실행

B b = (B) a; // 강제 타입 변환
b.b1();
b.b2();

자식 객체가 부모 타입으로 자동 변환되면 부모 타입에 선언된 필드와 메소드만 사용 가능하다는 제약 사항이 따르므로, 자식 객체에만 선언된 필드와 메소드를 사용하려면 강제 캐스팅이 필요하다.

또한, 자동 타입 변환 과정에서 자식 객체를 갖게 되면, 자식 객체에서 오버라이딩 된 메소드가 호출될 수 있다.

다형성 = 오버라이딩 + 자동 타입 변환

(4) instanceof 연산자

부모 클래스와 산하의 여러 자식 클래스들을 통해 다형성을 구현한 경우, 부모 클래스 타입의 객체 참조변수가 어떤 자식 클래스 내용을 가리키는지 혼동이 올 수 있다.

예를 들어서 어느 메소드의 매개변수에 부모 클래스가 인자로 들어가는 경우...

class A
class B extends A

public void example(A a) {
	// ...

example 메소드의 매개변수에 들어가는 객체 참조변수 a는 다형성을 실현한 B 클래스이거나 혹은 그냥 A 클래스일 수도 있다.

이걸 분간해주는 연산자가 바로 instanceof

boolean result = 객체 instanceof 타입 변수;
if (a instanceof B b) {
	b.method();
} else {
	a.method();
}

(5) 추상 클래스

// 추상 클래스
public abstract class Animal {
    public String name;

    public Animal(String name) {
        this.name = name;
    }

    public void breathe() {
        System.out.println("숨 쉼");
    }

	// 추상 메소드
    public abstract void sound();
}

// 상속1
public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}

// 상속2
public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    public void sound() {
        System.out.println("애용");
    }
}

추상 메소드를 상속한 클래스에서 구현해야 된다는 점은 인터페이스와 유사하다. 인터페이스와의 비교 내용은 인터페이스 공부하면서 같이 공부하기

public class Example {
    public static void main(String[] args) {
//        Animal animal = new Animal("동물"); // 불가능
        Animal animal = new Dog("진돌이");
        System.out.println("이름 : " + animal.name);
        animal.breathe();
        animal.sound();

        animal = new Cat("애용이");
        System.out.println("이름 : " + animal.name);
        animal.breathe();
        animal.sound();
    }
}

추상 클래스 자체를 객체 참조변수로 가지도록 생성할 수는 없다.

(6) 봉인된(sealed) 클래스

봉인된 클래스는 자바 15에서 도입된, 무분별한 자식 클래스 생성을 막기 위한 기능이다.

public sealed class Person permits Employee, Manager {
	// ...
}

sealed 키워드를 적용한 클래스는 무조건 permits 키워드로 자신을 상속받는 것이 가능한 자식 클래스를 지정해야 한다.
상속된 클래스에게는 final 키워드와 non-sealed 키워드 둘 중에 하나를 골라야 한다.

// 더 이상 상속 불가
public final class Employee extends Person {
	// ...
}

// 봉인 해제로 상속 연이어서 가능
public non-sealed class Manager extends Person {
	// ...
}

public class Director extends Manager {
	// ...
}
  • final : 더 이상 상속 불가
  • non-sealed : 봉인 해제, 연이은 상속 가능

2) 인터페이스

(1) 인터페이스에서의 다형성 실현

아까 상속에서 봤던 다형성 공식(?)

다형성 = 오버라이딩 + 자동 타입 변환

그대로 인터페이스에서도 똑같이 실현된다.

public interface A {
	void method();
}

public class B implements A {
	@Override
    public void method() {
    }
}
A a = new B();
a.method(); // 오버라이딩 된 메소드 실행

(2) 필드와 메소드

public interface Example {
    int constantFiled = 0; // 상수 필드(public)
    
    void abstractMethod(); // 추상 메소드(public)
    
    default void defaultMethod() {} // 디폴트 메소드
    
    static void staticPublicMethod() {} // 정적 메소드(public)

    private void privateMethod() {} // private 메소드
    
    private static void staticPrivateMethod() {} // private 정적 메소드
}

인터페이스에서 선언 가능한 필드와 메소드들 왜이리 많음
외운다기 보다는 써먹으면서 손에 익혀두기.

상수 필드

앞에 public static final이 숨겨져있는 셈
그리고 static이니까 바로 인터페이스로 접근해서 사용 가능

Example.constantField; // yeah~

추상 메소드

앞에 public abstract가 숨겨져있는 셈
아마 인터페이스를 써먹으면서 얘를 무진장 많이 써먹게 될 거야...
인터페이스를 구현한 클래스에서 오버라이딩해서 추상 메소드를 실체화한다.

Example a = new Child();
a.abstractMethod(); // 오버라이딩 된 메소드 실행

디폴트 메소드

그렇다고 인터페이스가 추상 메소드만 갖고 있는 건 아니고, 완전한 실행 코드를 지닌 디폴트 메소드를 선언할 수 있음
앞에 default를 꼭 명시해줄 것
당연한 거지만, 인터페이스로 다형성을 구현한 참조 변수를 통해 디폴트 메소드를 구현하면 인터페이스의 디폴트 메소드가 실행된다.

interface A : defaultMethod() {}
class B implements A

Example a = new Child();
a.defaultMethod(); // A에서 구현된 실행부로 메소드 실행

정적 메소드

인터페이스에서 구현한 정적 메소드니까 인터페이스를 통해 곧바로 접근이 가능하다.

interface A : staticMethod() {}

A.staticMethod();

private 메소드

보통 인터페이스 내부에서 중복 코드를 작성할 때 주로 쓰이며, 정적 메소드와 인스턴스 메소드 둘 다 작성이 가능하다.

public interface ExampleInterface {
    String constantFiled = "인터페이스의 상수 필드"; 
    // 상수 필드(public)

    void abstractMethod(); 
    // 추상 메소드(public)

    default void defaultMethod() {
        System.out.println("인터페이스의 디폴트 메소드 실행");
        privateMethod();
        staticPrivateMethod();
    } // 디폴트 메소드

    static void staticPublicMethod() {
        System.out.println("인터페이스의 정적 메소드 실행");
        staticPrivateMethod();
    } // 정적 메소드(public)

    private void privateMethod() {
        System.out.println("인터페이스의 프라이빗 메소드 실행");
    } // private 메소드

    private static void staticPrivateMethod() {
        System.out.println("인터페이스의 프라이빗 정적 메소드 실행");
    } // private 정적 메소드
}

(3) 다중 구현 및 상속

여러 인터페이스를 통한 구현이 가능하다. 또한, 상속도 가능하다.

public interface A extends O1 {
	// ...
}

public interface B  {
	// ...
}

public class C {
	// ...
}

public class D implements A, B extends C {
	// ...
}

인터페이스가 클래스와 차별화되는 점은, 인터페이스는 다중 상속이 가능하다.

public interface A extends B, C {
	// ...
}

(4) 기타 상속과의 유사점

  • 객체 타입 연산자 instanceof 사용 가능
  • sealed 키워드 사용 가능
public sealed interface InterfaceA permits InterfaceB {}
// permits 뒤에 붙은 인터페이스만 '상속'이 가능하다.

public non-sealed interface InterfaceB extends IntefaceA {}
// 봉인 해제된 InterfaceB는 다른 자식 인터페이스 만들 수 있음
// 상속받은 인터페이스는 sealed 혹은 non-sealed 키워드가 꼭 붙어야 함
// 상속과 다르게 final은 없음
profile
scientia est potentia / 벨로그 이사 예정...

0개의 댓글