상속
- 상속은 무언가로 부터 물려 받다는 개념이다. 현실세계에서는 부모님으로부터 자식들이 재산을 물려받는 것과 비슷하다고 보면된다. 객체 지향에서의 상속은 inheritance(상속)가 아닌 extends (재사용과 확장)로 이해하는 것이 좋다.
- 상속은 코드 재사용성을 높이는 기능이지만 항상 최선은 아니다. 그러나 이펙티브 자바(서적)에서는 상속은 아래와 같은 경우를 보면 안전하다고 말한다
- 클래스가 상속을 위해 특별히 설계 되었거나 구조화가 잘 된 클래스를 확장의 목적으로 상속하는것이 안전하다
- 동일 개발자가 하위 클래스와 상위 클래스의 통제하는 경우 즉 같은 패키지내에서 상속하는 것은 안전하다
객체 지향의 상속에 대한 잘못된 예 - 계층도 / 조직도
- 위 그림은 현실 세계의 상속이다.
- 할아버지는 아버지에게 물려주고 아버지는 아들에게 물려준다
객체 지향의 상속에 대한 올바른 예 - 분류도
- 위 관계는 상속 관계가 아니다. 동물은 포유류와 조류의 부모가 아니다. 고래 또한 부모가 포유류가 될 수 없다
- 동물이란것을 조금 더 세분화하여 분류하는 것이다
- 즉, 객체지향에서의 상속은 상위 클래스의 특성을 하위 클래스에서 상속하고 필요한것을 부가하여 확장해서 사용할 수 있다고 보면된다
자바 상속의 특징
- 다중 상속을 지원하지 않는다. extends 뒤에는 하나의 부모 클래스만 올 수있다
- 상속의 횟수 제한이 없다
- 자바에서의 최상위 계층 부모 클래스는
java.lang.Object
이다. 즉 Object 클래스는 super class를 가지지 않는다 - 상속을 할때 상위 클래스는 물려줄 특성이 풍부하면 좋고 반대로 인터페이스는 구현을 강제할 메서드의 개수가 적을 수록 좋다.
상속을 왜하지? 사용하는 이유
- 상위 클래스의 공통적인 부분을 사용하기 위해서이다
- 유지보수가 쉽다
- 중복이 적어진다
- 통일성이 있다
- 확장성이 용이해진다
- 모듈화를 통해 재사용이 가능해진다
- 코드가 간결해진다
- 개발시간을 단축할 수 있다
상속의 단점
- 상속을 사용하면 기능 추가/ 변경시에 예외나 버그가 발생하게 되면 어디에서 발생한지 예측을 하기가 힘들다. 상속 구조가 복잡하면 더욱 예측하기 힘들어진다
- 불필요한 기능을 원치 않는다 해도 상속해야한다
- 자료구조에서 Stack은 Vector를 상속하고 있는데 Vector의 단점을 모두 상속 받는다.
- Vector 단점
- ArrayList를 상속 받아 사용하고 synchronized가 걸려있다(thread-safe)
- Vector 단점
- 자료구조에서 Stack은 Vector를 상속하고 있는데 Vector의 단점을 모두 상속 받는다.
- 캡슐화 원칙을 위반한다
- 상위 클래스의 구현이 하위 클래스에게 노출되는 상속은 캡슐화를 깨뜨린다. 캡슐화가 깨짐으로서 하위 클래스가 상위 클래스에게 강하게 결합, 의존하게 되고 강한 결합, 의존은 변화에 유연하게 대처하기 어려워진다.
상속은 언제 사용하면 좋을까?
- 상속은 하위 자료형이 확실한 경우에 사용하는것이 좋다. 즉 is-a 관계가 성립할때만 상속하는 것이 바람직하다
상속하는 방법
- 상속하는 방법을 알아보자
class 자식클래스이름 extend 부모클래스이름 {
...
}
적용 예제
public class Parent {
public Parent() {
System.out.println("Parent Constructor");
}
public void printName() {
System.out.println("Parent printName()");
}
}
public class Child extends Parent{
public Child() {
System.out.println("Child Constructor");
}
}
public class InheritancePrint {
public static void main(String[] args) {
Child child = new Child();
child.printName();
}
}
//결과
Parent Constructor
Child Constructor
Parent printName();
Parent
클래스를 생성하고 생성자와 메소드를 만든다.Child
클래스를 생성하고Parent
를 확장(상속)하고 생성자를 만든다.InheritancePrint
클래스를 생성하고 메인 메소드에서Child
클래스를 참조하여child.printName()
메소드를 호출Parent
클래스의 생성자가 먼저 생성 되는 것은Parent
클래스를 확장하고 있기 때문이다, 다음으로Child
클래스의 생성자가 생성된다.
public class ChildPrint extends Parent{
public ChildPrint() {
System.out.println("Child Constructor");
}
public void printAge() {
System.out.println("printAge() - 18 month");
}
}
public class Parent {
public void printName() {
System.out.println("Parent printName()");
}
}
//결과
Child Constructor
printName() - Parent
public class ParentArg {
public ParentArg(String name) {
System.out.println("ParentArg("+name+") Constructor");
}
public void printName() {
System.out.println("Parent printName()");
}
}
public class ChildArg extends ParentArg{
public ChildArg() {
System.out.println("Child Constructor");
}
}
실행시 오류가 발생한다 String name
의 매개변수를 지정을 안해줘서이다
이를 해결하기 위해서는 방법이 있다.
- 부모 클래스에 "매개 변수가 없는" 기본 생성자를 만든다.
- 자식 클래스에서 부모 클래스의 생성자를 명시적으로 지정하는
super()
를 사용한다.
super 키워드
super()
- 부모 클래스의 생성자를 호출
- 부모 클래스에 매개 변수가 있는 생성자만 있을 경우에 사용
메소드 처럼 사용하지 않고 super.printName()
로 사용하면 부모 클래스에 있는 printName()
이라는 메소드를 호출한다는 의미다.
실행한 예제가 실행오류가 발생하는 이유도 자식 클래스의 생성자에는 지정하지 않아도, 자식 클래스를 컴파일 할 때 자동으로 super()
라는 문장이 들어가기 때문이다. ParentArg
클래스를 고치지 않고, ChildArg
클래스만 고쳐서 컴파일 및 실행이 정상적으로 수행되도록 하려면 다음과 같이 ChildArg
클래스의 생성자를 변경하면 된다.
public class ChildArg extends ParentArg{
public ChildArg() {
super("ChildArg");
System.out.println("Child Constructor");
}
}
super("ChildeArg")
라고 지정하면, ParentArg
클래스의 생성자 중 String
타입을 매개변수로 받을 수 있는 생성자를 찾는다. String
을 매개 변수로 받는 생성자가 있기 때문에, 이 생성자가 호출된다. 그런데, 이 생성자처럼 참조 자료형을 매개 변수로 받는 생성자가 하나 더 있다면 어떻게 될까? 다음과 같이 InheritancePrint
클래스의 객체를 매개 변수로 받는 ParentArg
생성자가 하나 더 있는 경우를 생각해보자.
public class ParentArg {
public ParentArg(String name) {
System.out.println("ParentArg("+name+") Constructor");
}
public ParentArg(InheritancePrint obj){
System.out.println("ParentArg(InheritancePrint) Constructor");
}
public void printName() {
System.out.println("Parent printName()");
}
}
ChildArg
클래스의 생성자에서 super
에 null
을 넘겨주면 어떤 객체의 null
인지 알 수 가없다
public class ChildArg extends ParentArg{
public ChildArg() {
super(null);
System.out.println("Child Constructor");
}
}
null
로 넘길시 "reference to ParentArg is ambiguous"라는 메시지가 있다. 이 말을 번역하면 "ParentArg 클래스로의 참조가 매우 모호하다"라는 의미이다.
super()
를 사용하여 생성자를 호출 할 때에는 호출하고자 하는 생성자의 기본 타입을 넘겨주는 것이 좋다.
상속과 T 메모리 구조
- 상속을 사용할 때 메모리 구조가 어떻게 되는지 살펴보자
Animal
public class Animal {
public String name;
public void showName() {
System.out.println(name);
}
}
Dog
public class Dog extends Animal {
public String habit;
public void showHabit() {
System.out.println("bow! " + habit +" name = "+name);
}
}
Test
public class Test {
public static void main(String[] args) {
Dog milk = new Dog(); // 1번
milk.habit = "짖다";
milk.name = "우유";
milk.showName();
milk.showHabit();
Animal samoyed = new Dog(); //2번
samoyed.name = "사모예드";
s.showName();
}
}
1번까지 실행 했을 때의 메모리구조를 살펴보자
Dog
클래스의 인스턴스만Heap
영역에 생긴것이 아니라Animal
클래스의 인스턴스도 함께Heap
영역에 생긴다. 하위 클래스의 인스턴스가 생성될 때 상위 클래스의 인스턴스도 함께 생성이 된다.milk
객체 참조 변수는Dog
클래스를 가리키고 있다
2번까지 실행 했을 때의 메모리구조를 살펴보자
- 아래 그램을 보기전에 구조를 한번 생각해보는 것도 좋다
samoyed
객체 참조 변수가 가리키고 있는것은Dog
가 아닌Animal
인스턴스이다
1번 Dog milk = new Dog();
2번 Animal samoyed = new Dog();
- 이렇게 보면 차이점이 눈에 보일 것이다.
결론
- 상속은 강력한 도구이지만 캡슐화 원칙을 침해하므로 깨지기 쉬운 프로그램이 될 수 있다. 원치않은 기능도 상속 받아야 하기 때문에 설계를 할때 정밀하게 설계 해야한다. 상위 클래스와 하위 클래스가 is-a 관계가 성립할때만 사용하는 것이 좋고 is-a 관계가 성립하더라도 상위와 하위클래스가 다른 패키지에 있거나 상속을 위한 상위 클래스가 아니라면 하위 클래스는 깨지기 쉬워진다. 부모 클래스에서 상속해줄 기능이 많을 수록 좋다.
Object 클래스
자바에서 Object
클래스는 모든 클래스의 부모 클래스가 되는 클래스이다
자바의 모든 클래스는 Object
클래스의 모든 필드와 메소드를 상속 받게 된다
자바의 모든 클래스는 별도로 extends
키워드를 사용하여 Object
클래스의 상속을 명시하지 않아도 Object
클래스의 모든 멤버를 자유롭게 사용이 가능하다
자바의 모든 객체에서 toString()
이나 equals()
, clone()
같은 메소드를 사용할 수 있는 이유가 해당 메소드들이 Object
클래스의 메소드이기 때문이다
메소드 오버라이딩(method overring)
- 오버라이딩이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 시그니쳐를 갖는 메소드로 다시 정의한다
- 자바에서 자식 클래스는 부모 클래스의 private 멤버를 제외한 모든 메소드를 상속받는다 이렇게 상속받은 메소드는 그대로 사용해도 되고 재정의하여 사용할 수도 있다, 메소드 오버라이딩이란 상속받은 부모 클래스의 메소드를 재정의하여 사용하는 것을 의미한다
오버라이딩 조건
- 오버라이딩이란 메소드의 동작만을 재정의 하는 것이므로, 메소드의 선언부는 기존 메소드와 완전히 같아야한다. 하지만 메소드의 반환 타입은 부모 클래스의 반환 타입으로 타입 변환할 수 있는 타입이라면 변경할 수 있다.
- 부모 클래스의 메소드보다 접근 제어자를 더 좁은 범위로 변경할 수 없다
- 부모 클래스의 메소드보다 더 큰 범위의 예외를 선언할 수 없다.
메소드 오버라이딩
- 메소드 오버라이딩을 통해 상속받은 부모 클래스의 메소드를 자식 클래스에 직접 재정의할 수 있다.
간단한 예제
- 부모클래스인
Parent
클래스의display()
메소드를 자식 클래스인Child
클래스에서 오버라이딩하는 예제
class Parent {
void display() { System.out.println("Parent의 display() 메소드"); }
}
class Child extends Parent {
void display() { System.out.println("Child의 display() 메소드"); }
}
public class Test{
public static void main(String[] args) {
Parent pa = new Parent();
pa.display();
Child ch = new Child();
ch.display();
Parent pc = new Child();
pc.display(); // Child cp = new Parent();
}
}
결과
Parent의 display() 메소드
Child의 display() 메소드
Child의 display() 메소드
위의 예제에서 세 번째와 같은 인스턴스의 참조가 허용되는 이유는 바로 자바에서의 다형성(polymorphism) 때문입니다.
다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
- 재정의 된 메서드에 대한 호출이 컴파일 타임이 아닌 런타임에 해석되는 프로세스이다 오버라이드된 메소드가 참조에 의해 호출될 때 Java는 참조하는 객체 유형에 따라 실행할 메소드 버전을 판별한다. 이것은 런타임의 다형성으로 알려져있다.
왜 다이나믹 메소드 디스패치가 필요한가?
- 상속 / 다형성이 없다면 컴파일시 결정이 가능하고 프로그램을 컴파일 할 때 호출된 메소드를 알 수 있다.
- 상속 / 다형성으로는 구체적인 런타임 유형을 알지 못하므로 런타임 중에 호출 할 메소드를 "Dynamic"으로 결정해야한다.
- 다이나믹 디스패치가 없으면 가상 메소드의 의미가 없으며 추상화 및 캡슐화의 중심이다.
예제
Dog animal = new Dog(); //1번
Animal dog = new Dog(); //2번
- 1번에서 2번으로 변경할때 Dynamic Dispatch가 해당이된다
추상 클래스(abstract class)
- 자바에서는 하나 이상의 추상 메소드를 포함하는 클래스를 가리켜 추상 클래스(abstract class)라고 합니다
- 추상 클래스는 객체 지향 프로그래밍에서 중요한 특징인 다형성을 가지는 메소드의 집합을 정의할 수 있도록 해준다
- 즉, 반드시 사용되어야 하는 메소드를 추상 클래스에 추상 메소드로 선언해 놓으면, 이 클래스를 상속받는 모든 클래스에서는 이 추상 메소드를 반드시 재정의해야한다.
문법
abstract class 클래스이름 {
...
abstract 반환타입 메소드이름();
...
}
추상클래스의 목적
- 추상 메소드가 포함된 클래스를 상속받는 자식 클래스가 반드시 추상 메소드를 구현하도록 하기 위함
- 만약 일반 메소드로 구현한다면 사용자에 따라 해당 메소드를 구현할 수도 있고, 안 할 수도 있다
- 하지만 추상 메소드가 포함된 추상 클래스를 상속 받은 모든 자식 클래스는 추상 메소드를 구현해야만 인스턴스를 생성할 수 있으므로, 반드시 구현하게 된다.
- 동일한 부모를 가지는 클래스를 묶는 개념으로 상속을 받아서 기능을 확장시키는 것
추상클래스의 특징
- 추상 클래스는 new 연산자를 사용하여 객체를 생성할 수 없다
- 추상 클래스(부모)와 일반 클래스(자식)는 상속의 관계에 놓여있다.
- 추상 클래스는 새로운 일반 클래스를 위한 부모 클래스의 용도로만 사용된다.
- 일반 클래스들의 필드와 메소드를 통일하여 일반 클래스 작성 시 시간을 절약할 수 있다
- 추상 클래스는 단일 상속만 가능하고 일반 변수를 가질 수 있다.
abstract class Animal {
abstract void showhabit();
}
class Dog extends Animal {
void showhabit() {
System.out.println("짖어!");
}
}
public class Test {
public static void main(String[] args) {
// Animal animal = new Animal(); // 추상 클래스는 인스턴스를 생성할 수 없음.
Dog milk = new Dog();
milk.showhabit();
}
}
결과
짖어!
위의 예제에서 추상 클래스인 Animal 클래스는 추상 메소드인 showhabit() 메소드를 가지고 있다. Animal 클래스를 상속받는 자식 클래스인 Dog 클래스는 showhabit() 메소드를 오버라이딩해야 인스턴스를 사용할 수 있다.
추상 클래스에는 일반 메서드 구현이 가능하다
abstract class Animal {
public abstract void showhabit();
public void age(){
System.out.println("20살");
}
}
class Dog extends Animal {
void showhabit() {
System.out.println("짖어!");
}
}
public class Test {
public static void main(String[] args) {
Dog milk = new Dog();
milk.showhabit();
}
}
결과
짖어!
20살
클래스와 인터페이스 사이 관계 이해하기
클래스끼리 인터페이스 끼리 상속할 때는 extends, 클래스가 인터페이스를 상속 받을 때는 implements 키워드를 사용한다
추상 클래스와 인터페이스의 공통점
- 추상 클래스와 인터페이스는 선언만 있고 구현 내용은 없는 클래스이다.
(자바 8부터 인터페이스에서 default method로 구현이 가능하지만 일반적으로 구현은 없다.)
-
인스턴스화(객체를 생성 X)를 할 수 없다.
-
추상 클래스를 extends로 상속받은 자식들과 인터페이스를 implements하고 구현한 자식들만 객체를 생성할 수 있다.
(결국 자식 클래스가 무언가 반드시 구현하도록 위임해야할 때 사용한다.)
추상 클래스와 인터페이스의 차이점
- 추상 클래스(단일상속) / 인터페이스(다중상속)
- 추상 클래스의 목적은 상속을 받아서 기능을 확장시키는 것(부모의 유전자를 물려받는다)
- 인터페이스의 목적은 구현하는 모든 클래스에 대해 특정한 메서드가 반드시 존재하도록 강제하는 역할(부모로부터 유전자를 물려받는 것이 아니라 사교적으로 필요에 따라 결합하는 관계) 즉, 구현 객체가 같은 동작을 한다는 것을 보장하기 위함
Final 키워드
- final은 클래스, 메소드 , 변수에 선언할 수 있다.
Class에 final을 선언할때
- 클래스를 상속을 해줄 수 없다
- 이 클래스를 상속 받아서 내용을 변경 못하게 한다
Method에 final을 선언하는 이유
- 더이상 Overring을 할 수 없다
변수에 final을 선언할때
- 값이 변하지 않게 상수값을 지정
- 인스턴스 변수나 static으로 선언된 클래스 변수는 선언과 함게 값을 지정해야만한다
- 초기화를 무조건 해줘야한다
Reference
- 자바의신
- 스프링 입문을 위한 자바 객체 지향의 원리와 이해
- http://www.tcpschool.com/java
- https://en.wikipedia.org/wiki/Dynamic_dispatch
- https://riptutorial.com/ko/java/topic/9204/다이내믹-메소드-디스패치
'스터디 > LiveStudy' 카테고리의 다른 글
8주차 과제: 인터페이스 (0) | 2021.01.04 |
---|---|
7주차 과제: 패키지 (0) | 2020.12.30 |
5주차 과제: 클래스 (0) | 2020.12.19 |
4주차 과제: 제어문 (0) | 2020.12.09 |
2주차 자바 데이터 타입, 변수 그리고 배열 (0) | 2020.11.20 |