상속(inheritance)는 부모가 자식에게 물려주는 행위를 말한다. 객체 지향 프로그래밍에서는 부모 클래스의 필드와 메서드를 자식 클래스에게 물려줄 수 있다.
-------parent------
| field1, method1 |
-------------------
^
|
|
|
------child--------
| field1, method1,|
| field2, method2 |
-------------------
상속은 이미 잘 개발된 클래스를 재사용해서 새로운 클래스를 만들기 때문에 중복되는 코드를 줄여 개발 시간을 단축시킨다.
상속 방법은 다음과 같다.
public class Parents {
String field1;
void method1() { }
}
public class Child extends Parents {
String field2;
void method2() { }
}
잘보면 자식이 부모 클래스 이름으로 extends
를 사용하고 있는 것을 볼 수 있다. 이는 자식이 상속을 결정한다는 의미로 현실 세계의 논리와는 약간 다르다. 자식이 상속을 결정하므로 위의 그림에서 화살표의 방향이 자식 -> 부모 방향으로 가는 것이다.
python, c++과 달리 자바는 다중 상속을 지원하지 않는다.
자식이 부모 객체를 상속한 상태에서 자식 객체를 생성하면 메모리에 다음과 같이 생성된 것이다.
---------heap----------
| ----Parent 객체----- |
| | field1, method1 | |
| ------------------- |
| ^ |
| | |
| | |
| -----Child 객체----- |
| | field2, method2 | |
| ------------------- |
-----------------------
Child c = new Child();
를 실행하면 Child
객체 뿐만 아니라, Parent
객체도 생성하여 지칭하고 있다는 것이다.
따라서, 자식 객체가 생성될 때에 부모 객체도 생성되어야 한다는 것인데, 부모 객체가 생성된다는 것은 부모 객체의 생성자도 어딘가에서는 실행이 되어야 한다는 것이다. 이는 사실, 컴파일 단계에서 자식 클래스인 Child
의 생성자에 super()
라는 코드를 추가하여 부모 객체의 생성자를 호출하는 것이다.
public class Phone {
public String model;
public String color;
public Phone() {
System.out.println("Phone 생성자 호출");
}
}
public class SmartPhone extends Phone{
public SmartPhone(String model, String color) {
// super() <-- compile 단계에 추가된다.
this.model = model;
this.color = color;
System.out.println("SmartPhone 생성자 호출");
}
}
결과는 다음과 같다.
Phone 생성자 호출
SmartPhone 생성자 호출
자식 객체인 SmartPhone
를 생성하는 생성자에서 compiler가 자동으로 super()
code를 넣어 실행하는 것이다. 따라서, 부모 객체인 Phone
의 생성자가 먼저 실행되는 것이다.
super()
를 명시적으로 적어주어 compiler가 자동으로 super()
를 추가시켜주지 않게 만들 수도 있다. 이는 부모 객체의 생성자가 단순히 super()
로 실행되는 기본 생성자가 아닌 경우이다.
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 생성자 호출");
}
}
부모 객체인 Phone
클래스의 생성자가 Phone(String model, String color)
으로 기본 생성자가 아닌 매개변수가 있는 생성자이다. 이러한 경우 super()
만으로는 해당 생성자가 호출되지 않기 때문에, 자식 객체인 SmartPhone
에서 이를 명시적으로 호출해주어야 한다. 만약 호출해주지 않으면 부모 클래스인 Phone
객체의 생성자로 무엇을 실행시켜줄 지 모르므로 compile 단계에서 에러가 발생한다.
Phone
class 코드를 위와 같이 바꾸고 SmartPhone
class로 돌아가보면 error가 생길 것이다.
There is no parameterless constructor available in 'Phone'
부모 클래스인 Phone
의 생성자를 명시적으로 적어달라는 이야기이다.
public class SmartPhone extends Phone{
public SmartPhone(String model, String color) {
super(model, color);
System.out.println("SmartPhone 생성자 호출");
}
}
다음과 같이 SmartPhone
클래스의 생성자에 부모 클래스인 Phone
생성자를 super
로 명시적으로 적어주어야 하는 것이다.
부모 클래스로부터 부여받은 메서드를 자식 클래스에서 다시 정의하는 것을 '메서드 오버라이드'라고 한다.
'메서드 오버라이드'는 상속된 메서드를 자식 클래스에서 재정의하는 것을 말한다. 메서드 오버라이딩되었다면 부모 메서드는 숨겨지고, 자식 메서드가 우선적으로 사용된다. 단, 메서드 오버라이드를 위해서는 다음의 조건을 만족해야한다.
public -> priavte
불가throw
할 수 없다.다음의 예제를 보도록 하자. 부모 객체인 Calculator
를 상속 받은 Computer
는 areaCircle
메서드를 오바리이딩하여 더 정확한 원의 넓이를 계산하고 있다.
public class Calculator {
public double areaCircle(double r) {
System.out.println("areaCircle 객체의 areaCircle 실행");
return 3.14 * r * r;
}
}
public class Computer extends Calculator{
@Override
public double areaCircle(double r) {
System.out.println("Computer 객체의 areaCircle 실행");
return Math.PI * r * r;
}
}
참고로 @Override
annotation은 생략이 가능하다. 단지, 해당 어노테이션이 있으면 컴파일 단계에서 정확히 오버라이딩 되었는지 체크하고 문제가 있다면 컴파일 에러를 출력한다.
public class Main {
public static void main(String[] args) {
int r = 10;
Calculator calculator = new Calculator();
System.out.println(calculator.areaCircle(r));
Computer computer = new Computer();
System.out.println(computer.areaCircle(r));
}
}
결과는 다음과 같다.
areaCircle 객체의 areaCircle 실행
314.0
Computer 객체의 areaCircle 실행
314.1592653589793
오버라이딩에 성공한 것을 볼 수 있다.
자식 클래스에서 부모 클래스의 메서드를 오버라이딩하면 부모 클래스의 메서드가 감춰진다. 만약, 부모 클래스의 메서드를 자식 클래스에서 호출하고 싶다면 super.method
를 사용하면 된다.
public class Computer extends Calculator{
@Override
public double areaCircle(double r) {
double parentRes = super.areaCircle(r);
System.out.println("Calculator 객체의 areaCircle 실행" + parentRes);
System.out.println("Computer 객체의 areaCircle 실행");
return Math.PI * r * r;
}
}
super.areaCircle(r);
를 통해서 부모 객체의 areaCircle
를 호출한 것을 볼 수 있다.
final 클래스와 final 메서드는 상속과 관련이 있다.
클래스를 선언할 때 final
키워드를 붙이면 최종적인 클래스이므로 더 이상 상속할 수 없는 클래스가 된다. 즉 final
클래스는 부모 클래스가 될 수 없다.
public final class 클래스 {}
대표적인 예가 String
클래스이다. String
클래스는 다음과 같이 선언되어 있다.
public final class String {}
그래서 다음과 같이 자식 클래스를 만들 수 없다.
public class NewString extends String {}
final
메서드는 이 메서드에 대해서 오버라이딩 할 수 없다는 것을 나타낸다. 즉, 부모 클래스를 상속해서 자식 클래스를 선언할 때, 부모 클래스에 선언된 final
메서드는 자식 클래스에서 재정의될 수 없다.
public final 리턴타입 메서드(매개변수) {}
다음 예제는 Car
클래스의 stop
메서드를 final
로 선언했기 때문에 자식 클래스인 SportsCar
에서 stop
메서드를 오버라이딩 할 수 없다.
public class Car {
public int speed;
public void speedUp() {
speed += 1;
}
public final void stop() {
System.out.println("차를 멈춤");
speed = 0;
}
}
public class SportsCar extends Car {
@Override
public void stop() {
speed += 10;
}
}
다음의 에러가 발생한다.
stop()' cannot override 'stop()' in 'Car'; overridden method is final
final
로 선언된 메서드를 오버라이딩 할 수 없다는 것이다.
protected
접근 제한자는 상송과 관련이 있다. public
과 default
접근 제한자 중간쯤이라고 생각하면 된다.
접근 제한자 | 제한 대상 | 제한 범위 |
---|---|---|
protected | 필드, 생성자, 메서드 | 같은 패키지이거나, 자식 객체만 사용 가능 |
protected
는 같은 패키지에서는 default
처럼 접근이 가능하나, 다른 패키지에서는 자식 클래스만 접근을 허용한다.
package1
를 만들고, A.java
, B.java
를 만들자.
package package1;
public class A {
protected String field;
protected A() {}
protected void method() {}
}
protected
는 같은 패키지에서는 default
와 같은 접근 제한이 가능하다고 했다. 따라서, 같은 패키지에서는 접근이 가능하다.
package package1;
public class B {
public void method() {
A a = new A();
a.field = "value";
a.method();
}
}
B
클래스는 A
클래스와 같은 패키지이기 때문에 A
클래스의 protected 생성자, 필드, 메서드에 모두 접근이 가능하다.
package2 패키지를 만들고 C.java
파일에 다음의 코드르 넣어보도록 하자.
package package2;
public class C {
public void method() {
A a = new A(); // error
a.field = "value"; // error
a.method(); // error
}
}
A
클래스를 가져올 수는 있지만 protected로 된 생성자를 호출할 수 없어 객체를 생성할 수도 없으며 field, method 모두 접근이 불가능하다. 이는 protected
접근 제한자가 default
처럼 동작하여 다른 패키지에서 사용할 수 없도록 하기 때문이다.
package2에 D.java
파일을 만들고 A
클래스를 상속해보도록 하자.
package package2;
import package1.A;
public class D extends A {
public D() {
super(); // protected 생성자 호출 가능
}
public void method1() {
this.field = "value"; // protected field 접근 가능
this.method(); // protected method 접근 가능
}
public void method2() {
A a = new A(); // 상속이 아닌 직접 접근은 불가
a.field = "value"; // 상속이 아닌 직접 접근은 불가
a.method(); // 상속이 아닌 직접 접근은 불가
}
}
다른 패키지이지만 상속한 다음 protected로 제한된 생성자, field, method에 접근이 가능한 것을 볼 수 있다. 단 이는 super
나 this
를 통해서 상속받은 부모 객체에 접근할 때만 가능한 것이지, new A();
와 같이 직접 접근하는 것은 불가능하다.
기본 타입 간의 변환이 가능한 것처럼 클래스도 클래스 타입 변환이 가능한데, 이는 상속 관계에 있는 클래스 사이에서 발생한다.
자동 타입 변환은 다음과 같은 조건에서 발생한다.
부모타입 변수 = 자식타입객체;
자식 객체는 부모 객체의 모든 특성을 상속 받았기 때문에 자식 객체는 부모 타입 변수에 들어갈 수 있다. 가령, 동물과 고양이 관계에서 '고양이는 동물이다'가 가능하다.
class Animal {
}
class Cat extends Animal {
}
Cat
은 Animal
클래스를 상속하므로 Animal
이 가진 field, method들을 모두 가지고 있다. 따라서, 다음과 같이 Animal
변수에 Cat
객체가 들어갈 수 있다.
Cat cat = new Cat();
Animal animal = cat;
메모리 상태를 표현하면 다음과 같다. cat
과 animal
변수는 타입만 다를 뿐 동일한 Cat
객체를 참조한다. 단지, animal
변수는 Cat
객체만의 field, method를 실행할 버튼이 없는 것일 뿐이다.
--Animal--
| |
----------
^
|
|
---Cat---
cat --------> | |
animal -----> | |
---------
상속 계층이 좀 더 깊은 경우, 바로 위의 부모가 아니라도 상속 계층에서 상위 타입이라면 자동 타입 변환이 일어날 수 있다.
A
/ \
B C
| |
D E
다음의 구조를 코드로 표현하면 다음과 같다.
B b = new B();
C c = new C();
D d = new D();
E e = new E();
A a1 = b; // ok
A a2 = c; // ok
A a3 = d; // ok
A a4 = e; // ok
단, 다음은 불가능하다.
B b1 = e; // 불가
C c1 = d; // 불가
이들은 상속 관계가 없기 때문이다.
부모 타입으로 자동 타입 변환된 이후에는 부모 클래스에서 선언된 필드와 메서드만 접근이 가능하다. 이는 부모 타입 변수의 버튼이 자식 클래스의 것들을 모르기 때문이다. 따라서, 부모 타입 변수가 자식 객체를 참조하고 있지만 접근 가능한 맴버와 메서드는 부모 클래스로 한정된다.
그러나, 자식 클래스에서 오버라이딩된 메서드가 있다면 부모 메서드 대신 오버라이딩된 메서드가 호출된다. 이는 다형성과 관련 있기 때문에 잘 알아두어야 한다.
public class Parent {
public void method1() {
System.out.println("Parent-method1");
}
public void method2() {
System.out.println("Parent-method2");
}
}
Parent
클래스를 상속하고 method2
를 오버라이딩하는 Child
클래스를 만들어보자.
public class Child extends Parent {
@Override
public void method2() {
System.out.println("Child-method2");
}
public void method3() {
System.out.println("Child-method3");
}
}
method2
를 오버라이딩한 것을 볼 수 있다. method3
는 Child
클래스에서만 사용할 수 있다.
public class Main {
public static void main(String[] args) {
Child child = new Child();
Parent parent = child;
parent.method1(); // Parent-method1
parent.method2(); // Child-method2
// parent.method3()
}
}
method1
는 오버라이딩되지 않았으므로 부모 클래스에서의 코드와 동일한 결과가 실행되었지만, method2
는 자식 클래스에서 오버라이딩되었으므로 자식 객체의 메서드가 호출되는 것이다.
자식 타입은 부모 타입으로 자동 변환되지만, 반대로 부모 타입은 자식 타입으로 자동 변환되지 않는다. 이는 부모 타입이 자식 타입의 특질(field, method)이 없기 때문이다.
그런데, '강제 타입 변환'을 사용하여 부모 타입 객체를 자식 타입 객체로 바꿀 수 있다.
자식타입 변수 = (자식타입) 부모타입객체;
그러나, '강제 타입 변환'을 사용한다고 해서 무조건 강제 타입 변환을 시킬 수 있는 것은 아니다. 이것이 가능한 경우는 자식 객체가 부모 타입으로 자동 변환된 후에 다시 자식 타입으로 변환해야할 때 '강제 타입 변환'을 사용할 수 있는 것이다.
Parent parent = new Child(); // 자동 타입 변환
Child child = (Child) parent; // 강제 타입 변환
즉 부모타입객체가 사실은 자식 객체를 가리키고 있을 때만 가능하다는 것이다.
public class Parent {
public String field1;
public void method1() {
System.out.println("Parent-method1");
}
public void method2() {
System.out.println("Parent-method2");
}
}
public class Child extends Parent {
public String field2;
public void method3() {
System.out.println("Child-method3");
}
}
public class Main {
public static void main(String[] args) {
Child child = new Child();
// 자동 타입 변환
Parent parent = child;
parent.field1 = "data";
parent.method1();
parent.method2();
/*
parent.field2 = "data2"; // 불가능
parent.method3(); // 불가능
* */
// 강제 타입 변환
Child child1 = (Child) parent;
child1.field2 = "data2";
child1.method3();
}
}
위의 예시를 보면 child
라는 Child
객체를 만들고 Parent
클래스를 가진 parent
에 넣어주어 자동 타입 변환이 발생하도록 하였다.
이후에 Child
클래스 타입을 가진 child1
변수를 만들고, parent
변수를 강제 타입 변환해주어 child
객체를 전달해준 것이다.
다형성은 사용 방법은 동일하지만, 실행 결과가 다양하게 나오는 성질을 말한다. 이는 객체의 사용방법(method)가 동일한데, 그 내부의 객체가 무엇이냐에 따라 결과가 다르게 나오는 것을 말한다.
이러한 특성을 가능하게 해주는 것이 바로 '메서드 오버라이딩'이다. 부모 클래스를 상속받은 자식 클래스에서 부모 클래스 메서드를 오버라이딩 한다음에 부모 클래스 변수 타입에 넣어주면, 부모 클래스 변수 타입은 내부적으로 메서드 오버라이딩을 완료한 자식 객체를 참조하므로, 자식 객체의 오버라이딩된 메서드를 호출하게 된다.
따라서, 다형성이라는 것은 '자동 타입 변환' + '메서드 오버라이딩' 으로 구성되었다고 할 수 있다.
다형성 = 자동 타입 변환 + 메서드 오버라이딩
이는 메서드 뿐만 아니라 field 역시도 동일한데, 부모 타입에서 호출하고 있는 field
는 동일하지만 자식 객체가 내부적으로 어떤 것이냐에 따라 다른 값이 들어갈 수 있다.
public class Car {
public Tire tire;
public void run() {
tire.roll();
}
}
Car
클레스는 field로 어떤 tire
객체를 가지고 있냐에 따라서 run
의 결과가 달라게 된다. 이것이 바로 field
다형성이다. 예제를 통해서 자세히 확인해보도록 하자.
public class Tire {
public void roll() {
System.out.println("회전");
}
}
public class BasicTire extends Tire{
@Override
public void roll() {
System.out.println("basic tire");
}
}
public class SnowTire extends Tire{
@Override
public void roll() {
System.out.println("snow tire");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car();
myCar.tire = new Tire();
myCar.run();
myCar.tire = new BasicTire();
myCar.run();
myCar.tire = new SnowTire();
myCar.run();
}
}
myCar
객체의 tire
field가 어떤 tire
객체이냐에 따라서 결과가 달라지는 것을 볼 수 있다. 반면에 myCar
에서 사용하고 있는 tire
와 run
은 달라진 결과에 달리 동일한 사용방법을 쓰고 있다. 이것이 바로 다형성이다.
다음과 같이 field말고도 매개변수를 통해서도 다형성을 쓸 수 있다. 메서드를 호출할 때 특정 매개변수의 타입이 클래스라면 해당 클래스의 자식 클래스를 매개변수로 넘겨 동일한 메서드를 실행해도 다른 결과를 내도록 할 수 있다.
public class Driver {
public void drive(Vehicle vehicle) {
vehicle.run();
}
}
Driver
의 매개변수로 Vehicle
클래스 타입의 객체가 필요한데, Vehicle
뿐만 아니라, Vehicle
을 상속한 자식 객체들도 넘겨줄 수 있다. 가령 Vehicle
을 상속한 Bus
라는 클래스를 만들어보자.
public class Vehicle {
public void run() {
System.out.println("차량이 달린다.");
}
}
public class Bus extends Vehicle {
public void run() {
System.out.println("Bus가 달린다.");
}
}
Bus
클래스는 Vehicle
을 상속했기 때문에 Vehicle
타입의 변수에 자동 타입 변환이 가능하다.
public class Main {
public static void main(String[] args) {
Driver driver = new Driver();
Bus bus = new Bus();
driver.drive(bus); // Bus가 달린다.
}
}
다형성을 활용하다보면 실제 변수에서 사용하고 있는 객체의 타입이 무엇인지 알고 싶을 때가 있다. 이때 사용하는 것이 instanceof
이다.
boolean res = 객체 instanceof 타입;
결과가 res
에 true
or false
로 나온다. 참고로, 부모, 자식 상속관계와 상관없이 딱 객체가 타입
이랑 일치하는 지 본다. 즉, 부모 타입 변수가 자식 객체를 참조하고 있다면, 내부의 자식 객체의 타입만이 true
로 나온다.
가장 많이 사용되는 곳이 강제 타입 변환하기 전에 사용되는데, '변수'가 Child
타입인지 여부를 instanceof
로 확인하여 강제 타입 변환을 실행하는 것이다.
public void method(Parent parent) {
if (parent instanceof Child) {
Child child = (Child) parent;
child.method3(); // Child 만의 메서드
}
}
위의 코드가 계속해서 관행으로 쓰여서, java 12부터는 instanceof
연산의 결과가 true
일 경우 다음과 같이 간단히 child
객체를 사용할 수 있게 되었다.
if (parent instanceof Child child) {
child.method3(); //Child 만의 메서드
}
맨 우측에 if
문 블럭 안에 쓸 child
변수를 선언하는 것이다. 단, true
일 때만 쓸 수 있다.
'추상(abstract)'은 실체 간에 공통되는 특성을 추출한 것을 말한다. 가령, 새, 곤충, 물고기 등의 공통점은 동물이라는 것이다. 동물은 실체들의 공통되는 특성을 가지고 있는 추상적인 것이라고 볼 수 있다.
객체를 생성할 수 있는 클래스를 '실체 클래스'라고 한다. 이 클래스들의 공통적인 필드나 메서드를 추출해서 선언한 클래스를 '추상 클래스'라고 한다. '추상 클래스'는 '실체 클래스'의 부모 역할을 한다. 따라서, '실체 클래스'는 '추상 클래스'를 상속해서 공통적인 필드나 메서드를 물려받을 수 있다.
Animal.class (추상 클래스)
^ (상속)
|
-----------------------------
| | |
Bird.class Insect.class Fish.class (실체 클래스)
추상 클래스는 실체 클래스의 공통되는 필드와 메서드를 추출해서 만들었기 때문에 new
연산자를 사용해서 객체를 직접 생성할 수 없다.
Animal animal = new Animal(); // error
따라서, 추상 클래스는 실체 클래스를 만들기 위한 부모 클래스로만 사용된다. 즉, 추상 클래스는 extends
뒤에만 올 수 있다.
class Fish extends Animal {
...
}
추상 클래스 선언은 클래스 선언에 abstract
를 붙이면 된다.
public abstract class 클래스명 {
//field
//생성자
//method
}
추상 클래스도 메서드, 필드를 선언할 수 있다. 그리고 자식 객체가 생성될 때 super
를 통해서 추상 클래스의 생성자가 호출되기 때문에 생성자도 반드시 있어야 한다. 다음의 예시를 보도록 하자.
public abstract class Phone {
String owner;
Phone(String owner) {
this.owner = owner;
}
void turnOn() {
System.out.println("ON");
}
void turnOff() {
System.out.println("OFF");
}
}
Phone
은 공통된 필드와 메서드를 정의해두었다. 자식 클래스는 추상 클래스인 Phone
을 상속하여 공통된 필드와 메서드를 사용할 수 있다.
public class SmartPhone extends Phone{
public SmartPhone(String owner) {
super(owner);
}
void internetSearch() {
System.out.println("Internet");
}
}
Phone
을 상속받고 super
를 통해서 Phone
추상 클래스의 생성자를 호출하고 있다.
public class Main {
public static void main(String[] args) {
// Phone phone = new Phone(); // 불가
SmartPhone smartPhone = new SmartPhone("홍길동");
smartPhone.turnOn(); // ON
smartPhone.internetSearch(); // Internet
smartPhone.turnOff(); // OFF
}
}
추상 클래스 Phone
의 field와 method에 접근이 가능한 것을 볼 수 있다.
추상 클래스는 공통된 부분들만 모아놓은 특수한 클래스라고 했다. 이러한 추상 클래스는 상속으로만 사용할 수 있는데, 일부 추상 클래스의 메서드들은 자식 클래스에서 상속하여 오버라이딩을 해야만 하는 경우도 있다. 이는 공통된 특징 중 몇몇 부분들은 각 자식마다 다르게 실행될 수 있는 부분들이 있기 때문이다.
가령, 동물은 소리를 내지만, 소리를 내는 방식과 소리는 동물마다 다 다르다. 따라서, 동물이 소리를 낸다는 메서드는 있지만, 그 구현은 자식에게 맡기는 것이다. 이렇게 자식이 공통된 특질을 구체적으로 구현하도록 강제하는 것이 바로 '추상 메서드'이다.
추상 메서드는 추상 클래스에서만 사용할 수 있고, 자식에게서 오버라이딩을 강제한다. 다음과 같이 일반적인 메서드에 abstract
를 붙이기만 하면 된다.
abstract 리턴타입 메서드명(매개변수, ...);
추상 메서드는 자식 클래스의 공통 메서드라는 것만 정의할 뿐, 실행 내용을 가지지 않는다(실행 블럭이 없다.). 다음은 Animal
추상 클래스에서 sound
추상 메서드를 선언한 것이다.
public abstract class Animal {
abstract void sound();
}
세부 메서드의 내용은 추상 클래스를 상속한 자식 클래스에서 반드시 오버라이딩하여 재정의해야하는 것이다.
Animal
(abstract sound())
^
|
|
---------------------
| |
Dog Cat
(sound()) 멍멍 (sound()) 야옹
public abstract class Animal {
public void breathe() {
System.out.println("숨을 쉰다.");
}
public abstract void sound();
}
추상 메서드인 sound
는 반드시 자식 클래스에서 오버라이딩해야한다.
public class Dog extends Animal {
@Override
public void sound() { // 오버라이딩을 안하면 에러가 발생
System.out.println("멍멍");
}
}
추상 클래스인 Animal
을 상속하고 추상 메서드인 sound
을 오버라이딩했다. 참고로 sound
오버라이딩 안하면 에러가 발생한다.
public class Cat extends Animal {
@Override
public void sound() { // 오버라이딩을 안하면 에러가 발생
System.out.println("야옹");
}
}
public class Main {
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.breathe();
animal.sound();
}
}
추상 클래스 덕분에 자식 클래스에서 공통된 특질인 breathe
를 가지게 되었고, 추상 메서드인 sound
를 강제로 자식 클래스가 오버라이딩하도록 하여 다형성을 구성할 수 있도록 해주었다.
기본적으로 final
클래스를 제외한 모든 클래스는 부모 클래스가 될 수 있다. 그러나 java 15부터는 무분별한 자식 클래스 생성을 방지하기 위해서 봉인된(sealed) 클래스가 도입되었다.
다음과 같이 Person
자식 클래스는 Employee
와 Manager
만 가능하고, 그 이외는 자식 클래스가 될 수 없도록 Person
을 봉인된 클래스로 선언할 수 있다.
public sealed class Person permits Employee, Manager {}
sealed
키워드를 사용하면 permits
키워드 위에 상속 가능한 자식 클래스들을 지정해야한다. 봉인된 Person
클래스를 상속하는 Employee
와 Manager
는 final
또는 non-sealed
키워드로 다음과 같이 선언하거나, sealed
키워드를 사용해서 또 다른 봉인 클래스로 선언해야한다.
왜냐하면 기껏 Person
을 봉인된 클래스로 만들어놨더니, Employee
나 Manager
가 상속 가능하면 Person
을 간접 상속하는 것과 동일하기 때문이다. 따라서 봉인된 클래스를 상속하는 자식 클래스들도 상속이 안되거나, 상속이 제한되거나 해야하는 것이다. 만약 이러한 제약조건을 풀고싶다면 non-sealed
를 붙이는 것이다.
public final class Employee extends Person {}
public non-sealed class Manager extends Person {}
final
로 된 Employee
는 더이상 상속이 불가능하다. non-sealed
로 된 Manager
는 다음과 같이 자식 클래스를 만들 수 있다.
public class Director extends Manager {}
정리하면 봉인된 클래스를 상속하는 자식 클래스는 다음 3가지 중 하나를 해야한다.
1. 상속을 불가능하게 만들어 봉인된 클래스의 후손을 만들지 못하도록 하기: final
2. 상속을 제한하게 만들어 봉인된 클래스의 후손을 제한하기: sealed ~ permits
3. 자식 클래스에서는 봉인된 클래스의 제약을 해소하기: non-sealed