목차
1. 자바 상속의 특징
2. super 키워드
3. 메소드 오버라이딩
4. 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
5. 추상 클래스
6. final 키워드
7. Object 클래스
상속은 자식 클래스가 부모 클래스의 프로퍼티와 메소드를 물려받는 것을 의미한다. (생성자는 상속하지 않는다)상속은 extends 키워드를 사용해 다른 클래스를 상속 받을 수 있다.
//부모 클래스를(Parent) 만들어 보자
public class Parent {
public String str = "Parent";
public void printStr(){
System.out.println(str);
}
}
// 다음과 같은 Parent 클래스를 상속 받은 Child 클래스를 생성해보자
public class Child extends Parent{
}
// Child 에는 아무런 코딩을 하지 않았다.
@Test
void childProperty(){
Child child = new Child();
assertThat(child.str).isEqualTo("Parent");
}
@Test
void childMethod(){
Child child = new Child();
child.printStr();
}
// 프로퍼티와 메소드를 부모에게 받아와서 그대로 적용할 수 있는 것을 알 수 있다.
자바는 다중상속을 지원하지 않는다. 즉, 자식 클래스는 하나의 부모만 가질 수 있다.
단, 자바에서 모든 클래스는 Object를 암묵적으로 상속받고 있다.
public class A {
public static void main(String args[]){
System.out.println(A instanceof Object) // --> true
}
}
자바에서 상속은 여러단계로 이루어질 수 있다. 즉 A 클래스를 B 클래스가 상속받고 B 클래스를 상속받은 C클래스를 받을 수 있다.
public class A {
public void grandParent(){
System.out.println("GrandParent");
}
}
public class B extends A {
public void parent(){
System.out.println("Parent");
}
}
public class C extends B {
public void child(){
System.out.println("Child");
}
}
public class Main {
public static void main(String[] args) {
A grandParent = new A();
grandParent.grandParent();
B parent = new B();
parent.grandParent();
parent.parent();
C child = new C();
child.grandParent();
child.parent();
child.child();
}
}
위 코드를 보면 최상위 클래스(A) 를 상속받은 B 클래스 와 B를 상속받은 C클래스가 사용되었다.
C에서는 B가 A를 상속받았기 때문에 A 에서 정의한 grandParent() 도 사용할 수 있다.
자바에서는 한 클래스를 여러 클래스가 상속 받을 수 있다. A 클래스를 상속하는 B 클래스와 C 클래스를 만들 수 있다.
public class A {
public void print(){
System.out.println("A");
}
}
public class B extends A {
}
public class C extends A {
}
public class Main {
public static void main(String[] args) {
B b = new B();
C c = new C();
b.print();
c.print();
}
}
super 키워드는 해당클래스의 바로 위의 부모클래스를 참조하는 키워드이다.
이 키워드를 통해서 부모의 프로퍼티나 메소드들을 적용시킬 수 있다.
public class Parent {
String str = "parent";
}
public class Child extends Parent{
String str = "child";
void printStr(){
System.out.println(str);
}
void printStrWithSuper(){
System.out.println(super.str);
}
public static void main(String[] args) {
Child child = new Child();
child.printStr(); // -> child
child.printStrWithSuper(); // -> parent
}
}
this 와 this() 가 그러하듯 super() 은 상위클래스의 생성자를 호출하는 메소드이다.
자식 클래스의 인스턴스는 부모 클래스의 모든 멤버까지도 포함하고 있어야한다. 그래서 super()를 명시적으로 호출하지 않더라도, 자바 컴파일러가 자동으로 추가하여 준다. 그리고 이렇게 추가된 super()를 따라서 현재 클래스 부터 최상위 클래스인 Object 클래스까지의 모든 생성자가 호출되게 된다.
public class Main {
static class Parent{
String str;
Parent(String param){
str = param;
}
}
static class Child extends Parent{
int a;
Child(String param){
super(param);
a = 10;
}
}
public static void main(String[] args) {
Child child = new Child("parent");
System.out.println(child.str); // -> parent
System.out.println(child.a); // -> 10
}
}
메소드 오버라이딩이란 부모 클래스에서 정의한 메소드를 자식 클래스가 같은 메소드 시그니쳐로 재정의 하는 것을 의미한다.
부모 클래스의 private 멤버를 제외한 모든 메소드를 상속받을 수 있고, 상속받은 메소드는 오버라이딩이 가능하다.
public class Main {
static class Parent{
public void print(){
System.out.println("this is parent method");
}
}
static class Child extends Parent{
public void print(){
System.out.println("this is child method");
}
}
static class WithOutOverridingChild extends Parent{
}
public static void main(String[] args) {
Child child = new Child();
child.print(); // -> this is child method
WithOutOverridingChild child2 = new WithOutOverridingChild();
child2.print(); // -> this is parent method
}
}
이렇게 자식클래스에서 재정의한 메소드는 부모 클래스에서 정의한 접근자보다 더 좁은 범위의 접근자로 변경할 수 없다. 또 한, 부모 클래스의 메소드 보다 더 큰 범위의 예외를 설정할 수 없다.
자바의 상속과 메소드 오버라이딩에 대해 알아봤으니, 다형성에 대해서도 알아보도록 하자
다형성은 하나의 객체가 여러 가지 타입을 가질 수 있는 것을 의미한다.
물론, 모든 타입을 가질 수 있다는 말은 아니고 부모 클래스 타입의 참조 변수로 자식 클래스 타입의 인스턴스를 참조할 수 있다.
public class Car {
void start(){
System.out.println("Car engine on");
}
}
public class FastCar extends Car{
void start(){
System.out.println("fast car engine on");
}
void goFast(){
System.out.println("speed up!!");
}
}
public class Main {
public static void main(String[] args) {
Car car = new Car();
Car fastCar = new FastCar();
FastCar fastCar1 = new FastCar();
car.start(); // -> Car engine on
fastCar.start(); // -> fast car engine on
// fastCar.goFast(); --> Car 클래스는 에는 goFast() 가 없다
fastCar1.goFast(); // -> speed up!!
}
}
메소드 오버라이딩을 통해 자식클래스에서 메소드를 재정의도 가능하고,
자식클래스의 인스턴스를 참조하는 변수의 타입을 부모클래스로 할 수 있다는 것을 알았다.
public class Car {
void start(){
System.out.println("Car engine on");
}
}
public class FastCar extends Car{
void start(){
System.out.println("fast car engine on");
}
void goFast(){
System.out.println("speed up!!");
}
}
public class Main {
public static void main(String[] args) {
Car fastCar = new FastCar();
fastCar.start();
}
}
fastCar.start(); 가 호출될때 "Car engine on" 을 출력해야할지, "fast car engine on"을 출력해야할지는 언제 결정되는 것일까?
이 결정은 런타임시에 결정된다. 이렇게 어떤 메소드가 사용될지를 런타임 시에 결정하는 것을 다이나믹 메소드 디스패치 (Dynamic Method Dispatch) 라고 한다.
앞서 살펴본 다형성의 핵심에는 바로 이 다이나믹 메소드 디스패치가 있는 것이다.
다형성이 사용된 경우가 아니라면, 일반적으로 컴파일시에 모든 것이 결정되어있는 상태이다. 이를 스태틱 메소드 디스패치라고 한다.
현대자동차, 기아자동차, 르노자동차, 벤츠자동차, 아우디자동차 와 같은 자동차들이 있다고 생각해보자. 이러한 자동차들은 모두 시동을 걸고, 달리고, 정지하는 것과 같은 자동차가 가지는 기능과 특성들을 가지고 있을 것이다.
그렇다면 우리는 위에서 설명한 다형성을 생각해봤을 때, 이 모든 자동차들의 상위에 자동차라는 클래스를 만들어 모든 자동차를 자동차라는 타입으로 관리할 수 있을 거라는 생각이 든다.
그런데 이 때 자동차라는 클래스는 추상적인 개념을 표현하기 위한 클래스이다.
바로 이럴 때 사용하는 클래스가 추상 클래스이다.
public abstract class Car {
public abstract void start();
public abstract void go();
public abstract void stop();
}
public class HyndaiCar extends Car{
public void start() {
System.out.println("Hyndai car start");
}
public void go() {
System.out.println("Hyndai car go");
}
public void stop() {
System.out.println("Hyndai car stop");
}
}
public class KiaCar extends Car{
public void start() {
System.out.println("KiaCar car start");
}
public void go() {
System.out.println("KiaCar car go");
}
public void stop() {
System.out.println("KiaCar car stop");
}
}
public class Main {
public static void main(String[] args) {
Car hCar = new HyndaiCar();
Car kCar = new KiaCar();
hCar.start(); // -> Hyndai car start
hCar.go(); // -> Hyndai car go
hCar.stop(); // -> Hyndai car stop
kCar.start(); // -> KiaCar car start
kCar.go(); // -> KiaCar car go
kCar.stop(); // -> KiaCar car stop
}
}
추상 메소드는 선언부만 존재하고 구현부는 존재하지 않는 메소드이다.
구현은 자식 클래스에서 담당하도록 한다
추상 메소드가 하나라도 있다면, 그 클래스는 추상 클래스이어야 한다.
추상클래스라고 해서 반드시 추상 메소드를 포함 해야하는 것은 아니다.
추상클래스에도 구현되있는 메소드를 사용할 수 있고, 일반적인 클래스와 동일하더라도 abstract 키워드를 붙이면 해당클래스는 추상클래스가 된다.
이러한 추상클래스는 인스턴스로 만들 수 없고, 이너클래스나 람다로 구현하는 것 역시, 추상클래스의 인스턴스를 만든 것 이 아니라 클래스 내부에서 상속하는 클래스를 만들고 그 클래스를 인스턴스화 한 것이다.
더 이상 상속을 하지 말아야할 때, final 키워드를 사용하면 해당 클래스를 상속하는 클래스를 생성할 수 없다.
final public class FinalClass {
}
다음과 같이 final 을 붙인 클래스를 상속하려고 하면
해당 클래스를 상속할 수 없다는 경고 문자와 함께 컴파일 에러가 발생한다.
이런 final 키워드는 클래스 뿐만 아니라 메소드나 변수에 사용해 메소드 오버라이딩을 제한하고 변수의 재할당을 제한할 수 도 있다.
final 키워드는 왜 사용하는 것일까?
조금 추상적인 이야기로 설명하는게 가장 잘 와닿을 것 같아, 추상적으로 설명하자면 우리가 갈 수 있는 길이 100가지가 있고, 그 길 중 90개의 길을 막아 10개의 길로 유도한다고 생각해보자. 10개의 길 중 랜덤한 길로 물건을 배송했는데 물건이 도착하지 않는다면, 우리는 10개의 길에서 어디가 잘못되었는지 찾아보면 된다. 그런데, 모든 길이 뚫려있다면 우리는 100개의 길을 모두 뒤져가며 어디가 잘못되었는지 찾아야한다.
final을 사용하면 상속을 제한할 수 있다. 이런 제한은 자유도를 떨어트리지만, 의도를 넣음으로써 사이드 이팩트를 줄이고, 리팩토링에 유리할 수 있다.
모든 클래스는 암묵적으로 Object 클래스를 상속하고 있다.
이런 Object 클래스는 어떤 필요에 의해 만들어졌을까?
By having the Object as the super class of all Java classes, without knowing the type we can pass around objects using the Object declaration.
Before generics was introduced, imagine the state of heterogeneous Java collections. A collection class like ArrayList allows to store any type of classes. It was made possible only by Object class hierarchy.
The other reason would be to bring a common blueprint for all classes and have some list of functions same among them. I am referring to methods like hashCode(), clone(), toString() and methods for threading which is defined in Object class.
출처 : https://javapapers.com/java/why-object-is-super-class-in-java/
예제코드 깃헙레포 : https://github.com/JadenKim940105/whiteship-study/tree/master/src/main/java/weeks6