본문 바로가기
공부기록/자바

Chapter 07. 상속

by 루팽 2023. 2. 6.

상속 개념

상속(Inheritance)은 부모가 자식에게 물려주는 행위를 말함

객체 지향 프로그램에서도 부모 클래스의 필드와 메소드를 자식 클래스에게 물려줄 수 있음

상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기에 중복되는 코드를 줄여 개발 시간을 단축시켜 줌

 

아래의 코드에서 자식 클래스(B)에서 처음부터 필드와 메소드 4개를 작성하는 것보다 field1과 method1을 부모 클래스(A)에서 상속받고 field2와 method2만 추가하는 것이 보다 효율적

public class A {
	int field1;
	void method1() { ... }
}

//===다른 클래스===

public class B extends A {
	String field2;
	void method2() { ... }
}

//A로부터 물려받은 필드와 메소드
B b = new B();
b.field1 = 10;
b.method1();

//B가 추가한 필드와 메소드
b.field2 = "홍길동";
b.method2();

 

상속의 또 다른 이점은 클래스의 수정을 최소화할 수 있다는 것

부모 클래스를 수정하면 모든 자식 클래스에 수정 효과를 가져옴

 

클래스 상속

자식 클래스를 선언할 때 어떤 부모로부터 상속받을 것인지 결정하고, 부모 클래스를 extends 뒤에 기술함

다른 언어와 달리 자바는 다중 상속을 허용하지 않음

따라서 extends 뒤에는 단 하나의 부모 클래스만이 와야 함

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

 //다중상속x!
//public class 자식클래스 extends 부모클래스1, 부모클래스2 {
//}

 

package dev_java.this_is_java.ch07;

public class Phone {
  // 필드 선언
  public String model;
  public String color;

  // 메소드 선언
  public void bell() {
    System.out.println("벨이 울립니다.");
  }

  public void sendVoice(String message) {
    System.out.println("자신: " + message);
  }

  public void receiveVoice(String message) {
    System.out.println("상대: " + message);
  }

  public void hangUp() {
    System.out.println("전화를 끊습니다.");
  }
}

 

package dev_java.this_is_java.ch07;

public class SmartPhone extends Phone {
  // 필드 선언
  public boolean wifi;

  // 생성자 선언
  public SmartPhone(String model, String color) {
    this.model = model; // Phone으로부터 상속받은 필드
    this.color = color;
  }

  // 메소드 선언
  public void setWifi(boolean wifi) {
    this.wifi = wifi;
    System.out.println("와이파이 상태를 변경했습니다.");
  }

  public void internet() {
    System.out.println("인터넷에 연결합니다.");
  }
}

 

package dev_java.this_is_java.ch07;

public class SmartPhoneExample {
  public static void main(String[] args) {
    // SmartPhone 객체 생성
    SmartPhone myPhone = new SmartPhone("갤럭시", "은색");

    // Phone으로부터 상속받는 필드 읽ㄱ
    System.out.println("모델: " + myPhone.model);
    System.out.println("색상: " + myPhone.color);

    // SmartPhone의 필드 읽기
    System.out.println("와이파이 상태: " + myPhone.wifi);

    // Phone으로부터 상속받은 메소드 호출
    myPhone.bell();
    myPhone.sendVoice("여보세요.");
    myPhone.receiveVoice("안녕하세요! 저는 홍길동입니다.");
    myPhone.sendVoice("네, 반갑습니다.");
    myPhone.hangUp();

    // SmartPhone의 메소드 호출
    myPhone.setWifi(true);
    myPhone.internet();
  }
}

// 모델: 갤럭시
// 색상: 은색
// 와이파이 상태: false
// 벨이 울립니다.
// 자신: 여보세요.
// 상대: 안녕하세요! 저는 홍길동입니다.
// 자신: 네, 반갑습니다.
// 전화를 끊습니다.
// 와이파이 상태를 변경했습니다.
// 인터넷에 연결합니다.

 

부모 생성자 호출

자바에서 자식 객체를 생성하면 부모 객체가 먼저 생성된 다음 자식 객체가 생성됨

자식클래스 변수 = new 자식클래스();

 

모든 객체는 생성자를 호출해야만 생성되는데 부모 객체도 예외는 아님

부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출됨

//자식 생성자 선언
public 자식클래스() {
	super();
	...
}

 

super()는 컴파일 과정에서 자동 추가되는데, 이것이 부모의 기본 생성자를 호출함

만약 부모 클래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생

부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 개발자는 아래와 같이 super(매개값) 코드를 직접 넣어야 함

이 코드는 매개값의 타입과 개수가 일치하는 부모 생성자를 호출함

//자식 생성자 선언
public 자식클래스() {
	super(매개값);
	...
}

 

부모 클래스가 기본 생성자를 가지고 있는 경우

package dev_java.this_is_java.ch07;

public class Phone {
  // 필드 선언
  public String model;
  public String color;

  // 기본 생성자 선언
  public Phone() {
    System.out.println("Phone() 생성자 실행");
  }
}

 

package dev_java.this_is_java.ch07;

public class SmartPhone extends Phone {
  // 자식 생성자 선언
  public SmartPhone(String model, String color) {
    super(); // 생략 가능(컴파일 시 자동 추가됨)
    this.model = model;
    this.color = color;
    System.out.println("SmartPhone(String model, String color) 생성자 실행");
  }
}

 

package dev_java.this_is_java.ch07;

public class SmartPhoneExample {
  public static void main(String[] args) {
    // SmartPhone 객체 생성
    SmartPhone myPhone = new SmartPhone("갤럭시", "은색");

    // Phone으로부터 상속받은 필드 읽기
    System.out.println("모델: " + myPhone.model);
    System.out.println("색상: " + myPhone.color);
  }
}

// Phone() 생성자 실행
// SmartPhone(String model, String color) 생성자 실행
// 모델: 갤럭시
// 색상: 은색

 

부모 클래스가 매개변수를 갖는 생성자가 있는 경우

package dev_java.this_is_java.ch07;

public class Phone {
  // 필드 선언
  public String model;
  public String color;

  // 매개변수를 갖는 생성자 선언
  public Phone(String model, String color) {
    this.model = model;
    this.color = color;
    System.out.println("Phone(String model, String color) 생성자 실행");
  }
}

 

package dev_java.this_is_java.ch07;

public class SmartPhone extends Phone {
  // 자식 생성자 선언
  public SmartPhone(String model, String color) {
    super(model, color); // 반드시 작성해야 함
    System.out.println("SmartPhone(String model, String color) 생성자 실행");
  }
}

 

package dev_java.this_is_java.ch07;

public class SmartPhoneExample {
  public static void main(String[] args) {
    // SmartPhone 객체 생성
    SmartPhone myPhone = new SmartPhone("갤럭시", "은색");

    // Phone으로부터 상속받은 필드 읽기
    System.out.println("모델: " + myPhone.model);
    System.out.println("색상: " + myPhone.color);
  }
}

// Phone(String model, String color) 생성자 실행
// SmartPhone(String model, String color) 생성자 실행
// 모델: 갤럭시
// 색상: 은색

 

메소드 재정의

부모 클래스의 메소드가 자식 클래스가 사용하기에 적합하지 않다면 자식 클래스에서 재정의해서 사용해야함

이것을 메소드 오버라이딩(Overriding)이라고 함

 

메소드 오버라이딩

메소드 오버라이딩은 상속된 메소드를 자식 클래스에서 재정의하는 것을 말함

메소드가 오버라이딩되었다면 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용됨

 

메소드를 오버라이딩할 때 주의해야 할 점

부모 메소드의 선언부(리턴타입, 메소드 이름, 매개변수)와 동일해야 함

접근 제한을 더 강하게 오버라이딩 할 수 없음(public → private으로 변경 불가)

새로운 예외를 throws할 수 없다

package dev_java.this_is_java.ch07;

public class Caculator {
  // 메소드 선언
  public double areaCircle(double r) {
    System.out.println("Caculator 객체의 areaCircle() 실행");
    return 3.14159 * r * r;
  }
}

 

package dev_java.this_is_java.ch07;

public class Computer extends Caculator {
  // 메소드 오버라이딩
  @Override // 컴파일 시 정확히 오버라이딩이 되었는지 체크해줌(생략 가능)
  public double areaCircle(double r) {
    System.out.println("Computer 객체의 areaCircle() 실행");
    return Math.PI * r * r;
  }
}

 

package dev_java.this_is_java.ch07;

public class ComputerExample {
  public static void main(String[] args) {
    int r = 10;

    Caculator caculator = new Caculator();
    System.out.println("원의 면적: " + caculator.areaCircle(r));
    System.out.println();

    Computer computer = new Computer();
    System.out.println("원의 면적: " + computer.areaCircle(r));
  }
}

// Caculator 객체의 areaCircle() 실행
// 원의 면적: 314.159

// Computer 객체의 areaCircle() 실행
// 원의 면적: 314.1592653589793

 

@Override 어노테이션

@Override를 붙이면 컴파일 단계에서 정확히 오버라이딩이 되었는지 체크하고, 문제가 있다면 컴파일 에러를 출력함

 

부모 메소드 호출

메소드를 재정의하면, 부모 메소드는 숨겨지고 자식 메소드만 사용되기에 비록 부모 메소드의 일부만 변경된다 하더라도 중복된 내용을 자식 메소드도 가지고 있어야 함

만약 부모 메소드가 100줄의 코드를 갖고 있다면, 자식 메소드에서 1줄만 추가하고 싶더라도 100줄의 코드를 자식 메소드에서 다시 작성해야 함

이 문제는 자식 메소드와 부모 메서드의 공동 작업 처리 기법을 이용하면 쉽게 해결됨

자식 메소드 내에서 부모 메소드를 호출하는 것인데, super 키워드와 도트(.) 연산자를 사용하면 숨겨진 부모 메소드를 호출할 수 있음

아래의 코드에서 부모 메소드를 재사용함으로써 자식 메소드의 중복 작업 내용을 없애는 효과를 가져옴

class Parent {
	public void method() {
	//작업 처리1
	}
}

class Child extends Parent { //부모메소드 상속받음
	@Override
	void method() { //재정의된 메소드
	super.method(); //부모 메소드 호출
	//작업 처리2
	}
}

 

아래의 예제에서 Airplane의 fly() 메소드를 자식 클래스인 SupersonicAirplane에서 오버라이딩함

따라서 일반 비행 모드일 때는 Airplane의 fly()를 사용하고, 초음속 비행 모드일 때는 오버라이딩된 SupersonicAirplane의 fly()를 사용함

package dev_java.this_is_java.ch07;

public class Airplane {
  // 메소드 선언
  public void land() {
    System.out.println("착륙합니다.");
  }

  public void fly() {
    System.out.println("일반 비행합니다.");
  }

  public void takeOff() {
    System.out.println("이륙합니다.");
  }
}

 

package dev_java.this_is_java.ch07;

public class SupersonicAirplane extends Airplane {
  // 상수 선언
  public static final int NOMAL = 1;
  public static final int SUPERSONIC = 2;

  // 상태 필드 선언
  public int flyMode = NOMAL;

  // 메소드 재정의
  @Override
  public void fly() {
    if (flyMode == SUPERSONIC) {
      System.out.println("초음속 비행합니다.");
    } else {
      // Airplane 객체의 fly() 메소드 호출
      super.fly();
    }
  }
}

 

package dev_java.this_is_java.ch07;

public class SupersonicAirplaneExample {
  public static void main(String[] args) {
    SupersonicAirplane sa = new SupersonicAirplane();
    sa.takeOff();
    sa.fly();
    sa.flyMode = SupersonicAirplane.SUPERSONIC;
    sa.fly();
    sa.flyMode = SupersonicAirplane.NOMAL;
    sa.fly();
    sa.land();
  }
}

// 이륙합니다.
// 일반 비행합니다.
// 초음속 비행합니다.
// 일반 비행합니다.
// 착륙합니다.

 

final 클래스와 final 메소드

필드 선언 시에 final을 붙이면 초기값 설정 후 값을 변경할 수 없음

클래스와 메소드에도 final을 붙일 수 있는데 final 클래스와 final 메소드는 상속과 관련이 있음

 

final 클래스

클래스를 선언할 때 final 키워드를 class 앞에 붙이면 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 됨

즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없음

public final class 클래스 { ... }

 

대표적인 예가 String 클래스

String 클래스는 아래와 같이 선언되어 있음

public final class String { ... }

 

그렇기에 아래와 같이 자식 클래스를 만들 수 없음

// public final class String extend String{ ... } -> final 클래스는 상속 불가!

 

package dev_java.this_is_java.ch07;

public final class Member { //final 클래스 Member
}

 

package dev_java.this_is_java.ch07;

// public class VeryImportantPerson extends Member{ -> Member는 final 클래스임으로 상속 불가!
}

 

final 메소드

메소드를 선언할 때 final 키워드를 붙이면 이 메소드는 최종적인 메소드이므로 오버라이딩할 수 없는 메소드가 됨

즉 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final 메소드는 자식 클래스에서 재정의할 수 없음

public final 리턴타입 메소드(매개변수) { ... }

 

package dev_java.this_is_java.ch07;

public class Car {
  // 필드 선언
  public int speed;

  // 메소드 선언
  public void speedUp() {
    speed += 1;
  }

  // final 메소드로 선언
  public final void stop() {
    System.out.println("차를 멈춤");
    speed = 0;
  }
}

 

package dev_java.this_is_java.ch07;

public class SportsCar extends Car{
  @Override
  public void speedUp() {
    speed += 10;
  }

  // Car 클래스의 stop() 메소드를 final로 선언했기에
  // 자식 클래스인 SportCar에서 stop() 메소드를 오버라이딩 할 수 없음!
  // @Override
  // public void stop() {
  //   System.out.println("스포츠카를 멈충");
  //   speed = 0;
  // }
}

 

protected 접근 제한자

private → default(접근제한자가 붙지 않은 상태) → protected → public

 

접근 제한자 제한 대상 제한 범위
protected  필드, 생성자, 메소드 같은 패키지이거나, 자식 객체만 사용 가능
package dev_java.this_is_java.ch07;

public class A {
  // 필드 선언
  protected String field;

  // 생성자 선언
  protected A() {
  }

  // 메소드 선언
  protected void method() {
  }
}

 

package dev_java.this_is_java.ch07; //A와 같은 패키지에서 protected 접근 가능

public class B {
  // 메소드 선언
  public void method() {
    A a = new A();
    a.field = "value";
    a.method();
  }
}

 

package dev_java.this_is_java.ch07.package2; //A와 다른 패키지

public class C {
  // 메소드 선언
  // public void method() { //protected이기에 다른 패키지에서 접근 불가!
  //   A a = new A();
  //   a.field = "value";
  //   a.method();
  // }
}

 

package dev_java.this_is_java.ch07.package2; //A와 다른 패키지

import dev_java.this_is_java.ch07.A;

public class D extends A { // protected여도 상속을 통해서 접근 가능
  // 생성자 선언
  public D() {
    // A() 생성자 호출
    super(); // 자식 생성자에서 super()로 A 생성자 호출
  }

  // 메소드 선언
  public void method1() {
    // A 필드값 변경
    this.field = "value";
    // A 메소드 호출
    this.method();
  }

  // // 메소드 선언
  // public void method2() { // 직접 객체를 생성해서 사용하는 것은 안 됨!
  //   A a = new A(); // new 연산자를 사용해 생성자를 직접 호출할 수 없음
  //   a.field = "value";
  //   a.method();
  // }
}

 

타입 변환

기본 타입과 마찬가지로 클래스도 타입 변환이 있는데, 클래스의 타입 변환은 상속 관계에 있는 클래스 사이에서 발생함

 

자동 타입 변환

자동 타입 변환(Promotion)은 의미 그대로 자동적으로 타입 변환이 일어나는 것을 말함

아래와 같은 조건에서, 자식은 부모의 특징과 기능을 상속받기에 부모와 동일하게 취급될 수 있음

만약 고양이가 동물의 특징과 기능을 상속받았다면 ‘고양이는 동물이다'가 성립됨

부모타입 변수 = 자식타입 객체; //자동 타입 변환

//cat과 animal 변수는 타입만 다를 뿐, 동일한 Cat 객체를 참조함
Cat cat = new Cat();
Animal animal = cat; //Animal animal = new Cat()도 가능!

cat == animal //true

 

바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있음

package dev_java;

class A {
}

class B extends A {
}

class C extends A {
}

class D extends B {
}

class E extends C {
}

public class PromotionExample {
  public static void main(String[] args) {
    B b = new B();
    C c = new C();
    D d = new D();
    E e = new E();

    // 자동 타입 변환(상속관계에 있음)
    A a1 = b;
    A a2 = c;
    A a3 = d;
    A a4 = e;

    B b1 = d;
    C c1 = e;

    // 컴파일 에러, 상속관계에 있지 않음!
    // B b3 = e;
    // C c2 = d;

  }
}

 

부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능함

비록 변수는 자식 객체를 참조하지만 변수로 접근 가능한 멤버는 부모 클래스 멤버로 한정됨

그러나 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드대신 오버라이딩된 메소드가 호출됨

이것은 다형성({Polymorphism)과 관련 있음

package dev_java.week4;

public class Parent {
  // 메소드 선언
  public void method1() {
    System.out.println("Parent-method1()");
  }

  // 메소드 선언
  public void method2() {
    System.out.println("Parent-method2()");
  }
}

 

package dev_java.week4;

public class Child extends Parent {
  // 메소드 오버라이딩
  @Override
  public void method2() {
    System.out.println("Child-method2()");
  }

  // 메소드 선언
  public void method3() {
    System.out.println("Child-method3()");
  }
}

 

package dev_java.week4;

public class ChildExample {
  public static void main(String[] args) {
    // 자식 객체 생성
    Child child = new Child();

    // 자동 타입 변환
    Parent parent = child;

    // 메소드 호출
    parent.method1();
    parent.method2();
    // parent.method3(); // 호출 불가능!
  }
}

// Parent-method1()
// Child-method2()

 

강제 타입 변환

자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않음

대신 아래와 같이 캐스팅 연산자로 강제 타입 변환(Casting)할 수 있음

그렇지만 부모 타입 객체를 자식 타입으로 무조건 강제 변환할 수 있는 건 아님

자식 객체가 부모 타입으로 자동 변환된 후 자식 타입으로 변환될 때 강제 타입 변환을 사용할 수 있음

자식타입 변수 = (자식타입) 부모타입객체; //캐스팅 연산자를 사용해 강제 타입 변환

Parent parent = new Child(); //자동 타입 변환
Child child = (Child)parent; //강제 타입 변환

 

자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용 가능하다는 제약사항이 따름

만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환해야 함

package ch07.sec07.exam03;

public class Parent {
	// 필드 선언
	public String field1;

	// 메소드 선언
	public void method1() {
		System.out.println("Parent-method1()");
	}

	// 메소드 선언
	public void method2() {
		System.out.println("Parent-method2()");
	}
}

 

package ch07.sec07.exam03;

public class Child {
	// 필드 선언
	public String field2;

	// 메소드 선언
	public void method3() {
		System.out.println("Child-method3()");
	}
}

 

package ch07.sec07.exam03;

public class ChildExample {
	public static void main(String[] args) {
		// 객체 생성 및 자동 타입 변환
		Parent parent = new Child();

		// Parent 타입으로 메소드 사용
		parent.field1 = "data1";
		parent.method1();
		parent.method2();
//		parent.field2 = "data2"; 불가능!
//		parent.method3(); 불가능!

		// 강제 타입 변환
		Child child = (Child) parent;

		// Child 타입으로 필드와 메소드 사용
		child.field2 = "data2";
		child.method3();
	}
}

 

다형성

사용 방법은 동일하지만 실행 결과가 다양하게 나오는 성질

다형성 구현을 위해선 자동 타입 변환과 메소드 재정의(오버라이딩)가 필요

 

필드 다형성

필드 타입(사용방법)은 동일하지만 대입되는 객체가 달라져서 실행 결과가 다양하게 나올 수 있는 것

public class Car {
	// 필드 선언
	public Tire tire;

	// 메소드 선언
	public void run() {
		tire.roll(); // tire 필드에 대입된 객체의 roll() 메소드 호출
	}
}

// Car 객체 생성
Car myCar = new Car();

// HankookTire 장착
myCar.tire = new HankookTire();
// KumhoTire 장착
myCar.tire = new KumhoTire();

myCar.run(); // 대입된(장작된) 타이어의 roll() 메소드 호출

 

package ch07.sec08.exam01;

public class Tire {
//	메소드 선언
	public void roll() {
		System.out.println("회전합니다.");
	}
}

 

package ch07.sec08.exam01;

public class HankookTire extends Tire{
//	메소드 재정의(오버라이딩)
	@Override
	public void roll() {
		System.out.println("한국 타이어가 회전합니다.");
	}
}

 

package ch07.sec08.exam01;

public class KumhoTire extends Tire{
//	메소드 재정의(오버라이딩)
	@Override
	public void roll() {
		System.out.println("금호 타이어가 회전합니다.");
	}
}

 

package ch07.sec08.exam01;

public class Car {
//	필드 선언
	public Tire tire;
	
//	메소드 선언
	public void run() {
//		tire 필드에 대입된 객체의 roll()메소드 호출
		tire.roll();
	}
}

 

package ch07.sec08.exam01;

public class CarExample {
	public static void main(String[] args) {
//		Car 객체 생성
		Car myCar = new Car();
		
//		Tire 객체 장착
		myCar.tire = new Tire();
		myCar.run(); // 회전합니다.
		
//		HankookTire 객체 장착
		myCar.tire = new HankookTire();
		myCar.run(); // 한국 타이어가 회전합니다.
		
//		KumhoTire 객체 장착
		myCar.tire = new KumhoTire();
		myCar.run(); // 금호 타이어가 회전합니다.
	}
}

 

매개변수 다형성

다형성은 필드보다 메소드를 호출할 때 많이 발생

메소드가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만 자식 객체를 제공할 수도 있음

아래의 코드에서 자식 객체가 run() 메소드를 재정의하고 있다면, 재정의된 run() 메소드가 호출됨 어떤 자식 객체가 제공되느냐에 따라 drive()의 실행결과 달라는데 이것이 매개변수의 다형성

public class Driver {
	public void drive(Vehicle vehicle) {
		vehicle.run();
	}
}

// 일반적으로 drive() 메소드를 호출하면 아래와 같이 Vehicle 객체를 제공
Driver driver = new Driver();
Vehicle vehicle = new Vehicle();
driver.drive(vehicle)

// 하지만 매개값으로 Vehicle의 자식 객체도 제공할 수 있음(자동 타입 변환)
Driver driver = new Driver();
Bus bus = new Bus();
// 자동 타입 변환 발생 Vehicle vehicle = bus;
driver.drive(bus) // Bus객체의 run() 호출

 

package ch07.sec08.exam02;

public class Vehicle {
//	메소드 선언
	public void run() {
		System.out.println("차량이 달립니다.");
	}
}

 

package ch07.sec08.exam02;

public class Bus extends Vehicle{
//	메소드 재정의(오버라이딩)
	@Override
	public void run() {
		System.out.println("택시가 달립니다.");
	}
}

 

package ch07.sec08.exam02;

public class Taxi extends Vehicle{
//	메소드 재정의(오버라이딩)
	@Override
	public void run() {
		System.out.println("버스가 달립니다.");
	}
}

 

package ch07.sec08.exam02;

public class Driver {
//	메소드 선언(클래스 타입의 매개변수를 가지고 있음)
	public void drive(Vehicle vehicle) {
		vehicle.run();
	}
}

 

package ch07.sec08.exam02;

public class DriverExample {
	public static void main(String[] args) {
//		Driver 객체 생성
		Driver driver = new Driver();
		
//		매개값으로 Bus 객체를 제공하고 driver() 메소드 호출
		Bus bus = new Bus();
		driver.drive(bus); // driver.drive(new Bus())와 동일
		// 택시가 달립니다.
		
//		매개값으로 Taxi 객체를 제공하고 driver() 메소드 호출
		Taxi taxi = new Taxi();
		driver.drive(taxi); // driver.drive(new Taxi())와 동일
		// 버스가 달립니다.
	}
}

 

객체 타입 확인

변수가 참조하는 객체의 타입을 확인하고자 할 때, instanceof 연산자 사용

좌항에 객체, 우항에 타입이 오는데, 좌항의 객체가 우항의 타입이면 true, 그렇지 않으면 false를 산출

boolean result = 객체 instanceof 타입;

 

아래의 코드는 Child 타입으로 강제 타입 변환하기 전에 매개값이 Child 타입인지 여부를 instanceof 연산자로 확인함

Child 타입이 아니라면 강제 타입 변환을 할 수 없기 때문

강제 타입 변환을 하는 이유는 Child 객체의 모든 멤버(필드, 메소드)에 접근하기 위해서

public void method(Parent parent) {
	if(parent instanceof Child) { // parent 매개변수가 참조하는 객체가 Child인지 조사
		Child child = (Child) parent;
		//child 변수 사용
	}
}

// Java12 부터는 instanceof 연산 결과가 true일 경우, 우측 타입 변수를 사용할 수 있기에 강제타입변환 필요없음
if(parent instanceof Child child) {
	// child 변수 사용
}

 

package ch07.sec09;

public class Person {
//	필드 선언
	public String name;
	
//	생성자 선언
	public Person(String name) {
		this.name = name;
	}
	
//	메소드 선언
	public void walk() {
		System.out.println("걷습니다.");
	}
}

 

package ch07.sec09;

public class Student extends Person{
//	필드 선언
	public int studentNo;
	
//	생성자 선언
	public Student(String name, int studentNo) {
		super(name);
		this.studentNo = studentNo;
	}
	
//	메소드 선언
	public void study() {
		System.out.println("공부를 합니다.");
	}
}

 

package ch07.sec09;

public class InstanofExample {
//	main() 메소드에서 바로 호출하기위해 정적 메소드 선언
	public static void personInfo(Person person) {
		System.out.println("name: " + person.name);
		person.walk();

//		person이 참조하는 객체가 Student 타입인지 확인
//		매개값이 Student인 경우에만 강제타입변환, 필드 메소드 사용
		/*if (person instanceof Student) {
//			Student 객체일 경우 강제 타입 변환
			Student student = (Student) person;
//			Student 객체만 가지고 있는 필드 및 메소드 사용
			System.out.println("studentNo: " + student.studentNo);
			student.study();
		}*/
		
//		person이 참조하는 객체가 Student 타입일 경우
//		student 변수에 대입, 타입 변환 발생(Java12부터 사용 가능)
		if(person instanceof Student) {
			Student student = (Student) person;
			System.out.println("studentNo:" + student.studentNo);
			student.study();
		}
	}

	public static void main(String[] args) {
//		Person 객체를 매개값으로 제공하고 personInfo() 메소드 호출
		Person p1 = new Person("홍길동");
		personInfo(p1);
//		name: 홍길동
//		걷습니다.
		
		System.out.println();
		
//		Student 객체를 매개값으로 제공하고 personInfo() 메소드 호출
		Person p2 = new Student("김길동", 10);
		personInfo(p2);
//		name: 김길동
//		걷습니다.
//		studentNo:10
//		공부를 합니다.
	}
}

 

추상

사전적 의미의 추상(abstract)은 실체 간에 공통되는 특성을 추출한 것을 말함

예를 들어 새, 곤충, 물고기 등의 공통점은 동물이고, 동물은 실체들의 공통되는 특성을 가지고 있는 추상적인 것

 

추상 클래스

객체를 생성할 수 있는 클래스를 실체 클래스라고 한다면, 이 클래스들의 공통적인 필드나 메소드를 추출해 선언한 클래스를 추상클래스라고 함

추상 클래스는 실체 클래스의 부모 역할을 함

따라서 실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있음

Animal.class // 추상클래스, 실체클래스에서 공통되는 필드와 메소드를 따로 선언

// 실체클래스, 추상클래스 Animal을 상속
Bird.class
Insect.class
Fish.class

 

추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기에 new 연산자를 사용해서 객체를 생성할 수 없음

추상 클래스는 새로운 실체 클래스를 만들기 위한 부모 클래스로만 사용됨

즉, 추상 클래스는 extends 뒤에만 올 수 있음

// Animal animal = new Animal(); -> 사용 불가!

class Fish extends Animal {
}

 

추상 클래스 선언

클래스 선언에 abstract 키워드를 붙이면 추상 클래스 선언

추상 클래스는 new 연산자를 이용해 객체를 직접 만들지 못하고 상속을 통해 자식 클래스만 만들 수 있음

public abstract class 클래스명 {
	// 필드
	// 생성자
	// 메소드
}

 

추상 클래스도 필드, 메소드를 선언할 수 있음

그리고 자식 객체가 생성될 때 super()로 추상 클래스의 생성자가 호출되기에 생성자도 반드시 있어야 함

아래의 코드는 모든 전화기의 공통 필드, 메소드만 뽑아서 추상 클래스 Phone으로 선언한 것

package ch07.sec10.exam01;

public class Phone {
//	필드 선언
	String owner;

//	생성자 선언
	Phone(String owner) {
		this.owner = owner;
	}

//	메소드 선언
	void turnOn() {
		System.out.println("폰 전원을 켭니다.");
	}

	void turnOff() {
		System.out.println("폰 전원을 끕니다.");
	}
}

 

새로운 전화기 클래스는 추상 클래스 Phone으로부터 공통 필드, 메소드를 물려받고 특화된 필드, 메소드 작성가능

아래의 코드는 Phone을 상속해서 SmartPhone을 설계한 것

package ch07.sec10.exam01;

public class SmartPhone extends Phone{
//	생성자 선언
	SmartPhone(String owner) {
		//Phone 생성자 호출
		super(owner);
	}
	
//	메소드 선언
	void internetSearch() {
		System.out.println("인터넷 검색을 합니다.");
	}
}

 

Phone 객체는 new 연산자로 직접 생성할 수는 없지만 자식 객체인 SmartPhone은 new 연산자로 객체 생성 가능

Phone에게 물려받은 turnOn(), turnOff() 메소드 호출도 가능

package ch07.sec10.exam01;

public class PhoneExample {
	public static void main(String[] args) {
//		Phone phone = new Phone(); -> 사용불가!
		
		SmartPhone smartPhone = new SmartPhone("홍길동");
		
		smartPhone.turnOn(); // Phone 메소드
		smartPhone.internetSearch();
		smartPhone.turnOff(); // Phone 메소드
		
//		폰 전원을 켭니다.
//		인터넷 검색을 합니다.
//		폰 전원을 끕니다.
	}
}

 

추상 메소드와 재정의

자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부(리턴타입, 메소드명, 매개변수)만 동일하고 실행 내용은 자식 클래스마다 달라야하는 경우가 많음

예를 들어 Animal 추상 클래스에서 sound()라는 메소드를 선언할 수 있지만, 실행 내용인 소리는 동물마다 다르기에 추상 클래스에서 통일하여 작성 부 ㄹ가능

이런 경우 추상 클래스는 아래와 같은 추상 메소드를 선언

일반 메소드 선언과 다르게 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호{}가 없음

abstract 리턴타입 메소드명(매개변수, ...);

 

추상 메소드는 자식 클래스의 공통 메소드라는 것만 정의할 뿐, 실행 내용을 가지지 않음

아래의 코드는 Animal 추상 클래스에서 sound() 추상 메소드를 선언한 것

public abstract class Animal {
	abstract void sound();
}

 

추상 메소드는 자식 클래스에서 반드시 재정의(오버라이딩)해서 실행 내용을 채워야 함

따라서 Animal 클래스를 상속하는 자식 클래스는 고유한 소리를 내도록 sound() 메소드를 반드시 재정의해야 함

package ch07.sec10.exam02;

public abstract class Animal {
//	메소드 선언
	public void breathe() {
		System.out.println("숨을 쉽니다.");
	}
	
//	추상 메소드 선언
	public abstract void sound();
}

 

package ch07.sec10.exam02;

public class Dog extends Animal{
//	추상 메소드 재정의
	@Override
	public void sound() {
		System.out.println("멍멍");
	}
}

 

package ch07.sec10.exam02;

public class Cat extends Animal{
//	추상 메소드 재정의
	@Override
	public void sound() {
		System.out.println("야옹");
	}
}

 

package ch07.sec10.exam02;

public class AbstractMethodExample {
	public static void main(String[] args) {
		Dog dog = new Dog();
		dog.sound(); // 멍멍
		
		Cat cat = new Cat();
		cat.sound(); // 야옹
		
//		매개변수의 다형성
		animalSound(new Dog()); // 자동 타입 변환
		animalSound(new Cat()); // 자동 타입 변환
//		멍멍
//		야옹
	}
	
	public static void animalSound(Animal animal) {
		animal.sound(); // 재정의된 메소드 호출
	}
}

 

봉인된 클래스

기본적으로 final 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있음

그러나 Java15부터 무분별한 자식 클래스 생성 방지를 위해 봉인된(sealed) 클래스 도입

아래와 같이 Person의 자식 클래스는 Employee와 Manager만 가능하고, 그 이외는 자식 클래스가 될 수 없도록 Person을 봉인된 클래스로 선언할 수 있음

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

 

sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정해야 함

봉인된 Person 클래스를 상속하는 Employee와 Manager는 final 또는 non-sealed 키워드로 아래와 같이 선언하거나, sealed 키워드를 사용해서 또 다른 봉인 클래스로 선언해야 함

public final class Employee extends Person { ... }
punlic non-sealed class Manager extends Person { ... }

 

final은 더 이상 상속할 수 없다는 뜻이고, non-sealed는 봉인을 해제한다는 뜻

따라서 Employee는 더 이상 자식 클래스를 만들 수 없지만, Manager는 아래와 같이 자식 클래스를 만들 수 있음

public class Director extends Manager { ... }

 

package ch07.sec11;

public sealed class Person permits Employee, Manager {
//	필드
	public String name;

//	메소드
	public void work() {
		System.out.println("하는 일이 결정되지 않았습니다.");
	}
}

 

package ch07.sec11;

public final class Employee extends Person {
	@Override
	public void work() {
		System.out.println("제품을 생산합니다.");
	}
}

 

package ch07.sec11;

public non-sealed class Manager extends Person {
	@Override
	public void work() {
		System.out.println("생산 관리를 합니다.");
	}
}

 

package ch07.sec11;

public class Director extends Manager {
	@Override
	public void work() {
		System.out.println("제품을 기획합니다.");
	}
}

 

package ch07.sec11;

public class SealedExample {
	public static void main(String[] args) {
		Person p = new Person();
		Employee e = new Employee();
		Manager m = new Manager();
		Director d = new Director();
		
		p.work(); // 하는 일이 결정되지 않았습니다.
		e.work(); // 제품을 생산합니다.
		m.work(); // 생산 관리를 합니다.
		d.work(); // 제품을 기획합니다.
	}
}

'공부기록 > 자바' 카테고리의 다른 글

Chapter 08. 인터페이스  (0) 2023.09.20
Chapter 06. 클래스  (0) 2022.12.18
Chapter 05. 참조 타입  (0) 2022.11.23
Chapter 04. 조건문과 반복문  (0) 2022.11.22
Chapter 03. 연산자  (0) 2022.11.21

댓글