Updated:

상속 개념

  • 상속(Inheriance): 부모가 자식에게 물려주는 행위
    • 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킬 수 있다.

  • 자식 클래스에서 처음부터 필드와 메소드 4개를 작성하는 것보다는 field1과 method1을 부모 클래스에서 상속받고 field2와 method2만 추가 작성하는 것이 보다 효율적
  • 상속은 클래스의 수정을 최소화할 수 있다.
    • 부모 클래스를 수정하면 모든 자식 클래스에 수정 효과를 가져옴

클래스 상속

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

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

}
  • 자바는 다중 상속을 허용하지 않음. 즉, 여러 개의 부모 클래스를 상속할 수 없다.
package 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 ch07;

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

    // 생성자 선언
    public SmartPhone(String model, String color) {
        this.model = model;
        this.color = color;
    }

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

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

}
package 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();
    }
}

부모 생성자 호출

 위 코드에서 SmartPhone 객체만 생성되는 것처럼 보이지만, 사실은 부모인 Phone 객체가 먼저 생성되고 그 다음에 자식인 SmartPhone 객체가 생성된 것이다. 모든 객체는 생성자를 호출해야만 한다. 부모 객체도 예외는 아니다. 부모 생성자는 자식 생성자의 맨 첫 줄에 숨겨져 있는 super()에 의해 호출된다.

public 자식클래스(...) {
  super();
  ...
}
  • super(): 컴파일 과정에서 자동 추가되는데, 이것은 부모의 기본 생성자를 호출
    • 만약 부모 크래스에 기본 생성자가 없다면 자식 생성자 선언에서 컴파일 에러가 발생
  • super(매개값, …): 부모 클래스에 기본 생성자가 없고 매개변수를 갖는 생성자만 있다면 매개값을 넣어줘야 함

메소드 재정의

 부모 클래스의 모든 메소드가 자식 클래스에게 맞게 설계되어 있다면 가장 이상적이지만, 어떤 메소드는 자식 클래스가 사용하기에 적합하지 않을 수 있다. 이러한 메소드는 메소드 오버라이딩(Overriding)을 통해 자식 클래스에서 재정의해서 사용해야 한다.

  • 메소드 오버라이딩: 상속된 메소드를 자식 클래스에서 재정의하는 것
    • 메소드가 오버라이딩되었다면 해당 부모 메소드는 숨겨지고, 자식 메소드가 우선적으로 사용된다.
  • 메소드 오버라이딩은 다음과 같은 규칙에 주의해서 작성해야 한다.
    • 부모 메소드의 선언부(리턴 타입, 메소드 이름, 매개변수)와 동일해야 함
    • 접근 제한을 더 강하게 오버라이딩할 수 없음(public에서 private으로 변경 불가)
    • 새로운 예외를 throws할 수 없음
package ch07.exam01;

public class Calculator {

    public double areaCircle(double r) {
        System.out.println("Calculator 객체의 areaCircle() 실행");
        return 3.14159 * r * r;
    }
}
package ch07.exam01;

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

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

        Calculator calculator = new Calculator();
        System.out.println("원 면적: " + calculator.areaCircle(r));
        System.out.println();

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

 출력 결과는 다음과 같다.

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

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

 자바는 개발자의 실수를 줄여주기 위해 정확히 오버라이딩이 되었는지 체크해주는 @Override 어노테이션을 제공한다. @Override를 붙이면 컴파일 단계에서 정확히 오버라이딩 되었는지 체크하고, 문제가 있다면 컴파일 에러를 출력한다.

부모 메소드 호출

 메소드를 재정의하면, 부모 메소드는 숨겨지고 자식 메소드만 사용되기 때문에 부모 메소드의 일부만 변경된다 하더라고 중복된 내용을 자식 메소드고 가지고 있어야 한다. 이 문제는 자식 메소드 내에서 부모 메소드를 호출하는 자식 메소드와 부모 메소드의 공동 작업 처리 기법을 이용해 해결할 수 있다.

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

class Child extends Parent {
  @Override
  public void method() {
    super.method();
    // 작업 처리2
  }
}
  • super.method()의 위치는 작업 처리2 전후에 어디든지 올 수 있다. 우선 처리가 괴어야 할 내용을 먼저 작성하면 된다.
  • 이 방법은 부모 메소드를 재사용함으로써 자식 메소드의 중복 작업 내용을 없애는 효과를 가져온다.
package ch07.exam02;

public class Airplane {
    
    public void land() {
        System.out.println("착륙합니다.");
    }

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

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

public class SupersonicAirplane extends Airplane {
    public static final int NORMAL = 1;
    public static final int SUPERSONIC = 2;

    public int flyMode = NORMAL;

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

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.NORMAL;
        sa.fly();
        sa.land();
    }
}
이륙합니다.
일반 비행합니다.
초음속 비행합니다.
일반 비행합니다.
착륙합니다.

final 클래스와 final 메소드

  • final 클래스
    • 클래스를 선언할 때 final 키워드를 class 앞에 붙이면 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 된다. 즉, final 클래스는 부모 클래스가 될 수 없어 자식 클래스를 만들 수 없다.
    • public final class 클래스 { … }
  • final 메소드
    • 메소드를 선언할 때 final 키워드를 붙이면 이 메소드는 최종적인 메소드이므로 오버라이딩할 수 없는 메소드가 된다.
    • public final 리턴타입 메소드 ( 매개변수, … ) { … }

protected 접근 제한자

 protected 접근 제한자는 public과 default의 중간쯤에 해당하는 접근 제한을 하고, 상속과 관련이 있다.

  • 제한 대상: 필드, 생성자, 메소드
  • 제한 범위: 같은 패키지이거나, 자식 객체만 사용 가능

타입 변환

  • 자동 타입 변환(Promotion)
    • 부모타입 변수 = 자식타입객체;
    • 바로 위의 부모가 아니더라도 상속 계층에서 상위 타입이면 자동 타입 변환이 일어날 수 있다.
    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;
      }
    }
    
    • 부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에 선언된 필드와 메소드만 접근이 가능
    • 자식 클래스에서 오버라이딩된 메소드가 있다면 부모 메소드 대신 오버라이딩된 메소드가 호출(다형성과 관련이 있음)
    class Parent {
      void method1() { ... }
      void method2() { ... }
    }
    
    class Child extends Parent {
      void method2() { ... }  // overriding
      void method3() { ... }
    }
    
    class ChildExample {
      public static void main(String[] args) {
        Child child = new Child();
    
        Parent parent = child;
    
        parent.method1();
        parent.method2();
        parent.method3(); // 호출 불가능
      }
    }
    
  • 강제 타입 변환(Casting): 자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않는다.
    • 캐스팅 연산자 사용: 자식타입 변수 = (자식타입) 부모타입객체;
    • 자식 객체가 부모 타입으로 자동 변환된 후 다시 자식 타입으로 변환될 때 강제 타입 변환을 사용할 수 있으므로, 부모 타입 객체를 자식 타입으로 무조건 강제 변환할 수 있는 것은 아님.
    • 자식 객체가 부모 타입으로 자동 변환하면 부모 타입에 선언된 필드와 메소드만 사용 가능. 만약 자식 타입에 선언된 필드와 메소드를 꼭 사용해야 한다면 강제 타입 변환을 해서 다시 자식 타입으로 변환해야 함
    class Parent {
      String field1;
      void method1() { ... }
      void method2() { ... }
    }
    
    class Child extends Parent {
      String field2;
      void method3() { ... }
    }
    
    class ChildExample {
      public static void main(String[] args) {
        Parent parent = new Child();
        parent.field1 = "xxx";
        parent.method1();
        parent.method2();
    
        parent.field2 = "yyy" // 불가능
        parent.method3(); // 불가능
    
        Child chile = (Chile) parent;
        child.field2 = "yyy"; // 가능
        child.method3();  // 가능
      }
    }
    

다형성

다형성은 실행 방법은 동일하지만 실행 결과가 다양하게 나오는 성질을 말한다. 다형성을 구현하기 위해서는 자동 타입 변환과 메소드 재정의가 필요하다.

필드 다형성

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

package ch07.exam03;

public class Tire {
    public void roll() {
        System.out.println("회전합니다.");
    }
}
package ch07.exam03;

public class HankookTire extends Tire {

    @Override
    public void roll() {
        System.out.println("한국 타이어가 회전합니다.");
    }
}
package ch07.exam03;

public class KumhoTire extends Tire {

    @Override
    public void roll() {
        System.out.println("금호 타이어가 회전합니다.");
    }
}
package ch07.exam03;

public class Car {

    public Tire tire;

    public void run() {
        tire.roll();
    }
}
package ch07.exam03;

public class CarExample {
    public static void main(String[] args) {
        Car myCar = new Car();

        myCar.tire = new Tire();
        myCar.run();

        myCar.tire = new HankookTire();
        myCar.run();

        myCar.tire = new KumhoTire();
        myCar.run();
    }
}
회전합니다.
한국 타이어가 회전합니다.
금호 타이어가 회전합니다.

매개변수 다형성

 다형성은 필드보다는 메소드를 호출할 때 많이 발생한다. 메소드가 클래스 타입의 매개변수를 가지고 있을 경우, 호출할 때 동일한 타입의 객체를 제공하는 것이 정석이지만 자식 객체를 제공할 수도 있다.

package ch07.exam04;

public class Vehicle {

    public void run() {
        System.out.println("차량이 달립니다.");
    }
}
package ch07.exam04;

public class Bus extends Vehicle {

    @Override
    public void run() {
        System.out.println("버스가 달립니다.");
    }
}
package ch07.exam04;

public class Taxi extends Vehicle {

    @Override
    public void run() {
        System.out.println("태시가 달립니다.");
    }
}
package ch07.exam04;

public class Driver {

    public void drive(Vehicle vehicle) {
        vehicle.run();
    }
}
package ch07.exam04;

public class DriverExample {
    public static void main(String[] args) {
        Driver driver = new Driver();

        Bus bus = new Bus();
        driver.drive(bus);

        Taxi taxi = new Taxi();
        driver.drive(taxi);
    }
}
버스가 달립니다.
태시가 달립니다.

객체 타입 확인

instanceof 연산자를 사용하면, 매개변수가 아니더라도 변수가 참조하는 객체의 타입을 확인할 수 있다.

  • boolean result = 객체 instanceof 타입;

 다음 코드는 Child 타입으로 강제 타입 변환하기 전에 Child 타입이 아니라면 강제 타입 변환을 할 수 없기 때문에 매개값이 Child 타입인지 여부를 instanceof 연산자로 확인한다. 강제 타입 변환을 하는 이유는 Child 객체의 모든 멤버(필드, 메소드)에 접근하기 위해서이다.

public void method(Parent parent) { // parent는 Child 객체
  if(parent instanceof Child) {
    Child chile = (Child) parent;
  }
}

 Java 12부터는 instanceof 연산의 결과가 true일 경우, 우측 타입 변수를 사용할 수 있기 때문에 강제 타입 변환이 필요 없다.

if(parent instanceof Child child) {
  // child 변수 사용
}

 다음은 personInfo() 메소드의 매개값으로 Student를 제공했을 경우에만 studentNo를 출력하고 study() 메소드를 호출한다.

package ch07.exam05;

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

        // person 이 참조하는 객체가 Student 타입인지 확인
        // person 이 참조하는 객체가 Student 타입일 경우 student 변수에 대입(타입 변환 발생)
        if (person instanceof Student student) {
            System.out.println("studentNo: " + student.studentNo);
            student.study();
        }
    }

    public static void main(String[] args) {
        Person p1 = new Person("홍길동");
        personInfo(p1);

        System.out.println();

        // Student 객체를 매개값으로 제공하고 personInfo() 메소드 호출
        Student p2 = new Student("깅길동", 10);
        personInfo(p2);
    }
}
package ch07.exam05;

public class Person {

    public String name;

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

    public void walk() {
        System.out.println("걷습니다.");
    }
}
package ch07.exam05;

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("공부를 합니다.");
    }
}

추상 클래스

  • 추상(abstract): 실체 간에 공통되는 특성을 추출한 것

 객체를 생성할 수 있는 클래스를 실체 클래스라고 한다면, 이 클래스들의 공통적인 필드나 메소드를 추출해서 선언한 클래스를 추상 클래스라고 한다. 추상 클래스는 실체 클래스의 부모 역할을 하므로 실체 클래스는 추상 클래스를 상속해서 공통적인 필드나 메소드를 물려받을 수 있다.

  • Bird, Insect, Fish와 같은 실체 클래스에서 공통되는 필드와 메소드를 따로 선언한 Animal 클래스를 만들 수 있고, 이것을 상속해서 실체 클래스를 만들 수 있다.

 추상 클래스는 실체 클래스의 공통되는 필드와 메소드를 추출해서 만들었기 때문에 new 연산자를 사용해서 객체를 직접 생성할 수 없다. 추상 클래스는 새로운 실체 클래스를 만들기 위한 부모 클래스로만 사용된다. 즉, 추상 클래스는 extends 뒤에만 올 수 있다.

class Fish extends Animal {
  ...
}

추상 클래스 선언

 클래스 선언 abstract 키워드를 붙이면 추상 클래스 선언이 된다. 추상 클래스는 new 연산자를 이용해서 객체를 직접 만들지 못하고 상속을 통해 자식 클래스만 만들 수 있다.

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

 추상 클래스도 필드, 메소드를 선언할 수 있다. 그리고 자식 객체가 생성될 때 super()로 추상 클래스의 생성자가 호출되기 때문에 생성자도 반드시 있어야 한다.

package ch07.exam06;

public abstract class Phone {
    // 필드 선언
    String owner;
    
    // 생성자 선언
    Phone(String owner) {
        this.owner = owner;
    }

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

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

public class SmartPhone extends Phone {
    // 생성자 선언
    SmartPhone(String owner) {
        super(owner);   // Phone 생성자 호출
    }

    // 메소드 선언
    void internetSearch() {
        System.out.println("인터넷 검색을 합니다.");
    }
}

 Phone 객체는 new 연산자로 직접 생성할 수는 없지만 자식 객체인 SmartPhone은 new 연산자로 객체 생성이 가능하고, Phone으로부터 물려받은 turnOn()과 turnOff() 메소드를 호출할 수 있다.

package ch07.exam06;

public class PhoneExample {
    public static void main(String[] args) {
        SmartPhone smartPhone = new SmartPhone("홍길동");

        smartPhone.turnOn();
        smartPhone.internetSearch();
        smartPhone.turnOff();
    }
}
폰 전원을 켭니다.
인터넷 검색을 합니다.
폰 전원을 끕니다.

추상 메소드와 재정의

 자식 클래스들이 가지고 있는 공통 메소드를 뽑아내어 추상 클래스로 작성할 때, 메소드 선언부(리턴타입, 메소드명, 매개변수)만 동일하고 실행 내용은 자식 클래스마다 달라야 하는 경우가 많다.
 예를 들어 동물은 소리를 내기 때문에 Animal 추상 클래스에서 sound()라는 메소드를 선언할 수 있지만, 실행 내용인 소리는 동물마다 다르기 때문에 추상 클래스에서 통일하여 작성할 수 없다.
 이런 경우를 위해서 추상 클래스는 다음과 같은 추상 메소드를 선언할 수 있다.

abstract 리턴타입 메소드명(매개변수, ...);
  • 일반 메소드 선언과 다르게 abstract 키워드가 붙고, 메소드 실행 내용인 중괄호가 없다.

 추상 메소드는 자식 클래스의 공통 메소드라는 것만 정의할 뿐, 실행 내용은 가지지 않는다. 추상 메소드는 자식 클래스에서 반드시 재정의(오버라이딩)해서 실행 내용을 채워야 한다.

package ch07.exam07;

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

    // 추상 메소드 선언
    public abstract void sound();
}
package ch07.exam07;

public class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍");
    }
}
package ch07.exam07;

public class Cat extends Animal {
    @Override
    public void sound() {
        System.out.println("야옹");
    }
}
package ch07.exam07;

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 클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 JAVA 15부터는 무분별한 자식 클래스 생성을 방지하기 위해 봉인된(sealed) 클래스가 도입되었다.

  • public sealed class Person permits Employee, Manager { … }
    • sealed 키워드를 사용하면 permits 키워드 뒤에 상속 가능한 자식 클래스를 지정해야 한다.
  • Employee와 Manager는 final 또는 non-sealed 키워드로 선언하거나 sealed 키워드를 사용해서 또 다른 봉인 클래스로 선언해야 한다.
    • public final class Employee extends Person { … }
    • public non-sealed class Manager extends Person { … }
      • non-sealed는 봉인을 해제한다는 뜻으로 자식 클래스를 만들 수 있다.

댓글남기기