github.com/whiteship/live-study/issues/6
목표
자바의 상속에 대해 학습하세요.
학습할 것
- 자바 상속의 특징
- super 키워드
- 메소드 오버라이딩
- 다이나믹 메소드 디스패치(Dynamic Method Dispatch)
- 추상 클래스
- final 키워드
- Object 클래스
- ※ ♥ 스터디원 참고 및 리뷰
자바 상속의 특징
🤔 상속(Inheritance)이란?
자바에서의 상속이란 부모 클래스의 변수와 메소드를 자식 클래스가 물려받아 사용하는 것을 말한다.
상속하는 법
public Class Parent{ .... }; // 부모 클래스
public Class Child extends parent { .... }; // 자식 클래스
extends 키워드를 사용하여 상속받을 부모 클래스를 정할 수 있다.
※ 참고로 extends가 붙어 있지 않은 클래스라도 컴파일러에 의해 Object 클래스(최상위 클래스)를 자동으로 상속 받는다.
상속의 특징
- 부모 클래스의 생성자와 초기화 블럭은 상속되지 않는다.
- 부모 클래스의 private 접근 제한을 갖는 변수와 메소드는 자식한테 물려주지 않는다.
- 자식 클래스의 멤버 개수는 조상 클래스보다 항상 같거나 많다.
- 자바에서는 다중 상속을 지원하지 않는다.
- 동일한 이름의 변수가 부모 클래스와 자식 클래스에 둘 다 존재할 경우 자식 클래스의 변수로 오버라이딩 된다.
- 자손 클래스의 인스턴스르 생성하면 조상 클래스의 멤버와 자손 클래스의 멤버가 합쳐진 하나의 인스턴스로 생성된다.
※추가 지식
어느정도 공부하다보면 다중상속이 왜안되는지 궁금하기 마련이다. 결론은 문법적인 한계 때문이다. 다중 상속의 가장 대표적인 문제로 꼽히는 다이아몬트 문제를 통해 알아보자.
만약, 자바에서 다중 상속을 허용한다고 가정하면, 위 그림과 같은 형태(다이아 몬드)로 상속이 가능해진다. 만약 이때, Person 클래스가 추상 클래스(Abstract Class)이고 Father 클래스나 Mother클래스가 Person에 대한 구현 객체라면 문제가 발생할 수 있다.
코드를 통해 알아보자.
1) Person 클래스
public abstract class Person {
public abstract void speak();
}
2) Father 클래스
public class Father extends Person {
@Override
public void speak(){
System.out.println("speak implementation of Father");
}
}
3) Mother 클래스
public class Mother extends Person {
@Override
public void speak(){
System.out.println("speak implementation of Mother");
}
}
4) Child 클래스
public class Child extends Father, Mother {
public void test(){
// calling super class method
speak();
}
}
위의 경우 Child의 내부의 test() 메소드를 실행하게 된다면 Child 객체는 상위 객체로부터 상속받은 speak()메소드를 실행해야 하는데 Abstract method로 정의된 speak()메소드를 Father 클래스에서도 구현했고, Mother 클래스에서도 구현했기에 어떤 speak()메소드를 호출해야 할지 자식은 알 수 없다.
자바에서 다중 상속이 필요할 경우 인터페이스를 이용해 해야한다.
(자세한건 8주차 과제 인터페이스에서..)
super 키워드
(1) super
super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다.
멤버 변수와 지역변수의 이름이 같을 때 this를 붙여서 구별했듯이 상속 받은 멤버와 자신의 멤버와 이름이 같을 때 super를 붙여서 구별할 수 있다.
※ this와 마찬가지로 super 역시 static 메서드에서는 사용할 수 없고 인스턴스 메서드에만 사용할 수 있다.
class SuperTest {
public static void main(String args[]) {
Child c = new Child();
c.method();
}
}
class Parent{
int x=10;
}
class Child extends Parent {
int x= 20;
void method() {
System.out.println("x=" + x); //20
System.out.println("this.x=" + this.x); //20
System.out.println("super.x=" + super.x); //10
}
}
부모 클래스와 자식 클래스의 멤버변수가 같더라도 this와 super를 이용해서 서로 구별할 수 있다.
class Point {
int x;
int y;
String getLocation() {
return "x : " + x + ", y :"+y;
}
}
class Point3D extends Point {
int z;
String getLocation() {
// return "x: " + x + ", y :"+ y + ", z : " + z;
return super.getLocation() + ", z :" + z; //조상의 메서드 호출
}
}
부모 클래스의 메서드를 자식 클래스에서 오버라이딩한 경우에도 super를 사용해서 조상 클래스의 메서드를 호출할 수 있다.
(2) super()
this()는 같은 클래스의 다른 생성자를 호출하는 데 사용 되지만, super()는 조상 클래스의 생성자를 호출하는데 사용된다.
자손 클래스의 인스턴스를 생성하면, 자손의 멤버와 조상의 멤버가 모두 합쳐진 하나의 인스턴스가 생성된다. 그래서 자손 클래스의 인스턴스가 조상 클래스의 멤버들을 사용할 수 있는 것이다. 이 때 조상 클래스 멤버의 초기화 작업이 수행되어야 하기 때문에 자손 클래스의 생성자에서 조상 클래스의 생성자가 호출되어야 한다.
생성자의 첫 줄에서 조상클래스의 생성자를 호출해야하는 이유는 자손 클래스의 멤버가 조상 클래스의 멤버를 사용할 수도 있으므로 조상의 멤버들이 먼저 초기화되어 있어야 하기 때문이다.
이와 같은 조상 클래스 생성자의 호출은 클래스의 상속관계를 모든 클래스의 최고 조상인 Object 클래스 까지 거슬러 올라가면서 계속 반복된다. 따라서 Object 클래스를 제외한 모든 클래스의 생성자는 첫 줄에 반드시 자신의 다른 생성자 또는 조상의 생성자를 호출해야 한다. 그렇지 않으면 컴파일러는 생성자의 첫 줄에 'super()'를 자동으로 추가한다.
class PointTest {
public static void main(String args[]) {
Point3D p3 = new Point3D(1,2,3);
}
}
class Point {
int x, y;
Point(int x, int y) {
//super(); //조상인 Object클래스의 생성자 Object()를 호출한다.
this.x = x;
this.y = y;
}
String getLocation() {
return "x :" + x + ", y : "+ y;
}
}
class Point3D extends Point {
int z;
Point3D(int x, int y, int z) {
//★ super(); // 컴파일 에러 발생
this.x = x;
this.y = y;
this.z = z ;
}
}
★ 표시한 부분에 다른 생성자를 호출하지 않기 때문에 컴파일러가 삽입해주지만 컴파일 에러가 발생한다.
왜냐하면 부모 클래스인 Point에 기본 생성자가 없기 때문이다.(생성자가 정의되어 있는 클래스에는 컴파일러가 기본 생성자를 자동적으로 추가하지 않는다.)
이 에러를 해결하려면 Point클래스에 기본 생성자를 만들던가, Point(int x, int y)를 호출하도록 변경하면 된다.
메소드 오버라이딩
오버라이딩이란 상속 관계에 있는 부모 클래스에서 이미 정의된 메소드를 자식 클래스에서 같은 시그니쳐를 갖는 메소드로 재정의하는 것이다. 즉, 메서드의 선언부가 기존메소드와 완전히(이름, 매개변수, 반환타입) 같아야 한다.
※ JDK1.5부터 '공변 반환타입'이 추가되어, 반환타입을 자손 클래스의 타입으로 변경하는 것은 가능하다.
오버라이딩의 조건은 다음과 같다.
1. 접근 제어자는 조상 클래스의 메서드 보다 좁은 범위로 변경할 수 없다.
2.조상 클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.
class Parent {
void parentMethod() throws IOException, SQLException {
...
}
}
class Child extends Parent {
void parentMethod() throws Exception { //Exception 불가능
...
}
}
위 코드는Exception은 모든 예외의 최고 조상이므로 가장 많은 개수의 예외를 던질 수 있기 때문에 잘못된 오버라이딩이다.
3.static 메서드는 오버라이딩이 불가능하다.
4.final로 선언된 메소드의 경우 오버라이딩이 불가능하다.
5.private로 접근 지정자를 사용하는 경우 오버라이딩이 불가능하다.
class Parent {
void display() { System.out.println("부모 클래스의 display() 메소드입니다."); }
}
class Child extends Parent {
@Override
void display() { System.out.println("자식 클래스의 display() 메소드입니다."); }
}
public class Inheritance{
public static void main(String[] args) {
Parent pa = new Parent();
pa.display(); //1."부모 클래스의 display() 메소드입니다."
Child ch = new Child();
ch.display(); //2."자식 클래스의 display() 메소드입니다."
Parent pc = new Child();
pc.display(); //3."자식 클래스의 display() 메소드입니다."
}
}
1번은 부모클래스의 display()가 호출
2번은 오버라이딩된 자식 클래스의 display()가 호출
3번은 다형성 개념으로 이것 또한 오버라이딩된 자식 클래스의 display()가 호출
※ 하이딩
static 메서드는 런타임시가 아닌 컴파일시에 생성되고 메모리에 적재 된다. 다형성이란 런타임시에 해당 메서드를 구현한 실제 객체를 찾아가서 호출한다. 그러나 static메서드는 컴파일시에 선언된 객체의 메서들를 찾아 호출하기 때문에 다형성이 적용되지 않는다.
class Parent {
static void display() { System.out.println("부모 클래스의 display() 메소드입니다."); }
}
class Child extends Parent {
//@Override 선언시 컴파일 발생
static void display() { System.out.println("자식 클래스의 display() 메소드입니다."); }
}
public class Inheritance{
public static void main(String[] args) {
Parent parent = new Parent();
Parent parent2 = new Child();
parent.display(); //부모 클래스의 display() 메소드입니다.
parent2.display(); //부모 클래스의 display() 메소드입니다.
}
}
런타임 시에 parent2가 실제로 참조하고 있는 클래스를 찾아가는 것이 아니라 컴파일시에 결정된 클래스를 찾아가서 해당 메서드를 호출하는 이와 같은 상황을 하이딩이라 한다.
※ 정적 메서드는 오버라이딩이 되지 않아서 @Override 어노테이션 선언시 컴파일 에러가 발생한다.
자바의 꽃인 다형성을 해치는 방법이기 때문에 쓰지말자!!
다이나믹 메소드 디스패치(Dynamic Method Dispatch)
메소드 디스패치란 프로그램이 어떤 메소드를 호출할 것인가를 결정하여 그것을 실행하는 과정을 말한다.
대표적으로 Static Dispatch와 Dynamic Dispatch가 있다.
정적 디스패치(Static Dispatch)
public class StaticDispatch {
static class Service {
void run() {
System.out.println("run");
}
void run(String msg) {
System.out.println(msg);
}
}
public static void main(String[] args) {
new Service().run(); //run 출력
}
}
위 프로그램을 실행하면 우리는 "run"이 출력되는 것을 실행시키지 않아도 알 수 있다.
즉, 정적 디스패치란 위와 같이 실행 시점이 아니라도 컴파일 시점에 어느 메소드로 호출이 일어날지 결정되는 것을 말한다.
동적 디스패치(Dynamic Dispatch)
public class DynamicDispatch {
static abstract class Service {
abstract void run();
}
static class MyService1 extends Service {
@Override
void run() {
System.out.println("1");
}
}
static class MyService2 extends Service {
@Override
void run() {
System.out.println("2");
}
}
public static void main(String[] args) {
Service svc = new MyService1();
svc.run();
}
}
위 프로그램을 실행하면 코드를 작성한 우리는 1이 출력될 것을 알지만 컴파일러는 스스로 결정을 하지 못해 컴파일시에는 알 수 없다. 런타임 시에 svc에 할당된 객체를 확인 후 결정하게 된다.
추상 클래스
🤔 추상화란 무엇일까?
자바에서의 추상화란 기존의 클래스에서 공통된 성질을 뽑아 조상 클래스를 만드는 것이다.
반대말인 구체화는 상속을 통해 클래스를 구현하고 확장하는 작업이다.
추상 클래스를 설계도에 비유하자면 미완성 설계도에 비유할 수 있다.
클래스가 미완성이라는 것은 미완성 메서드(추상 메서드)를 포함하고 있다는 의미이다.
미완성 설계도로 완성된 제품을 만들 수 없듯이 추상클래스로 인스턴스를 생성할 수 없다. 추상 클래스는 상속을 통해서 자손클래스에 의해서만 완성될 수 있다.
※추가 지식
추상(abstract)의 단어의 뜻은 무엇일까?
사물이 지니고 있는 여러가지 측면 가운데서 특정한 측면 만을 가려내어 포착하는 것이다.(위키백과)
세상에 존재하는 모든 객체를 인간이 구체적이고 상세하게 파악하기는 힘들다.
그래서 우리 인간들은 객체들에 대해서 공통점을 찾고 그 공통점대로 객체를 묶는 행위를 추상화, 즉 일반화 한다.
인간과 고래는 차이점이 많지만 그룹화하여 조금더 단순하게 볼 수 있다.
인간과 고래를 포유류라는 그룹에 속할 수 있고 참새와 닭은 조류라는 그룹으로 묶을 수 있다.
이와같이 그룹으루 분류하여 얻는 장점은
1. 객체간의 차이점은 무시하고 객체들 간의 공통점을 파악하기 쉽다.
2. 객체의 불필요한 세부사항을 제거함으로써 중요한 부분을 강조할 수 있다.
객체지향 프로그래밍에서는 복잡한 프로그래밍을 단순화하고 분류함으로써 유연한 관계를 만들어 낼 수 있다.
추상 클래스 사용법
abstract class 클래스이름 {
...
}
class 앞에 키워드 'abstract'를 붙이기만 하면 된다. 추상 클래스는 추상메서드를 포함하고 있다는 것을 제외하고 일반 클래스와 전혀 다르지 않다. 추상클래스에도 생성자가 있으며, 멤버변수와 메서드도 가질 수 있다.
※ 추상메서드가 없는 클래스라도 추상 클래스로 지정되면 인스턴스를 생성할 수 없다.
추상 메서드는 선언부만 작성하고 구현부는 작성하지 않은 채로 남겨준 것을 말한다.
그럼 추상메서드를 왜 사용할까? 메서드의 내용이 상속받는 자식 클래스에 따라 달라질 수 있기 때문이다.
abstract 리턴타입 메서드이름();
추상 클래스로부터 상속받는 자식클래스는 오버라이딩을 통해 조상인 추상클래스의 추상 메서드를 모두 구현해주어야 한다. 만일 조상으로부터 상속받은 추상 메서드 중 하나라도 구현하지 않는다면, 자손 클래스 역시 추상클래스로 지정해주어야 한다.
abstract class Player {
abstract void play(int pos); //추상메서드
abstract void stop(); //추상메서드
}
class AudioPlayer extends Player {
void play(int pos) { ... } //추상메서드를 구현
void stop() { ... } //추상메서드를 구현
}
abstract class AbstractPlayer extends Player {
void play(int pos) { ... } //추상메서드를 구현
}
※ 추상 메서드의 접근 지정자로 private은 사용할 수 없다.
추상 메소드의 사용 목적
혼자하는 코딩이 아닌 여러명이서 할 경우 제약이 필요하다. 추상 메소드와 같은 제약이 없으면 사용자에 따라 해당 메서드를 오버라이딩할 수도 있고 안할수도 있기 때문이다. 반드시 필요한 메서드라면 구현을 강제화하면 커뮤니케이션의 비용 낭비 없이 코드만으로도 대화할 수 있다.
final 키워드
final은 '마지막의' 또는 '변경될 수 없는'의 의미를 가지고 있다.
즉, 엔티티를
크게 클래스, 메서드, 멤버 및 지역변수에 사용될 수 있다.
final class
변경 및 확장이 될 수 없는 클래스이다. 따라서 final로 지정된 클래스는 다른 클래스의 조상이 될 수 없다.
대표적인 final Class로 String 클래스가 있다.
final method
변경될 수 없는 메서드이다. final로 지정된 메서드는 오버라이딩을 통해 재정의 될 수 없다.
final variables
변수 앞에 final이 붙으면, 값을 변경할 수 없는 상수가 된다.
final class FinalTest{ //조상이 될 수 없는 클래스
final int MAX_SIZE = 10; //값을 변경할 수 없는 멤버변수(상수)
final void getMaxSize(){ //오버라이딩 할 수 없는 메서드(변경불가)
final int LV = MAX_SIZE; //값을 변경할 수 없는 지역변수(상수)
return MAX_SIZE;
}
}
Object 클래스
Object클래스는 java.lang 패키지에 있으며 모든 자바 클래스의 최고 조상 클래스이다.
메소드
(1) public final native Class<?> getClass();
객체 자신의 클래스 정보를 담고 있는 Class인스턴스를 반환한다.
public static void main(String[] args) {
Person person = new Person("soap", 20);
Class cls = person.getClass();
System.out.println("getName() : " + cls.getName()); //해당 객체의 이름
System.out.println("getSuperclass() : " + cls.getSuperclass()); // 해당 객체의 상위 클래스 이름
System.out.println("getDeclaredFields() : " + cls.getDeclaredFields()[0]); //해당 객체의 선언된 필드 정보
}
(2) public native int hashCode();
JVM으로 부터 객체 자신의 고유값인 해시코드를 반환한다.
자바에서는 C/C++과 같이 객체의 주소값을 찾을 수 없기 때문에 이 해시코드를 사용한다.
public static void main(String[] args) {
Person person = new Person("soap", 20);
System.out.println("hashCode : " + person.hashCode());
}
(3) public boolean equals(Object obj)
객체 자신과 객체 obj가 같은 객체인지 알려준다.
//Object 클래스의 equals() 메서드
public boolean equals(Object obj) {
return (this == obj);
}
객체의 참조값을 가지고 비교하기 때문에 만약 객체안의 값이 같으면 같은 객체라고 인식하게 구현하기 위해선 equals를 오버라이딩 해야 한다.
public class Person {
private String name;
private int age;
...
@Override(IDE가 만들어줌)
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
public static void main(String[] args) {
Person person = new Person("soap", 1);
Person person2 = new Person("soap", 1);
if (person.equals(person2))
System.out.println("같음");
else {
System.out.println("다름");
}
System.out.println(person.hashCode());
System.out.println(person2.hashCode());
}
name과 age 둘다 같아야지 true를 리턴하게 equals()를 오버라이딩 했다.
person.equals(person2)결과는 true를 리턴하지만("같음") person과 person2의 해시코드는 다르다
개발자가 정의한 객체 비교시 반드시 equals()와 hashCode()를 함께 재정의 해야한다.
예를 들면 아래와 같은 부작용이 있을 수 있다.
equals만 재정의해서 어떤 두 객체가 같다고 했는데 hash를 사용하는 Collection(HashSet, HashMap,...)에 넣을 때는 같다고 생각하지 않아서 문제가 생길 수 있다.
Set<Person> hset = new HashSet<>();
Person person1 = new Person("jdk", 27);
Person person2 = new Person("jdk", 27);
System.out.println("person1 : "+person1.hashCode());//2018699554
System.out.println("person2 : "+person2.hashCode());//1311053135
System.out.println(person1.equals(person2));//true
hset.add(person1);
hset.add(person2);
System.out.println(hset.size());//2
person1과 person2는 해시코드가 다르기 때문에 중복을 자동으로 없애주는 Set에 넣었음에도 불구하고 set의 사이즈느 2가 나온다.
이런 문제를 모르고 코딩하다가는 나중에 꼬여버린다.
즉, equals로 같은 객체라면 반드시 hashCode도 같은 값이여야만 한다.
하지만 반대로 hashCode가 같은 값이더라도 equals로 같은 객체가 아닐 수 있다는 것을 유의해야 한다.
또한 아주 중요한 점이 같은 파라미터를 이용해야 한다는 것이다.
예를들어 equals를 판단하는 파라미터에는 name만 이용했는데 hashcode에서는 age를 이용한다든지 name과 age를 같이 사용해버린다든지 하면 부작용이 많이 일어날 수 있다.
결론적으로 반드시 같은 파라미터를 이용하면 될것이다.
@Override(IDE가 만들어줌)
public int hashCode() {
return Objects.hash(name, age);
}
hashCode는 메모리에서 가진 hash주소 값을 기본적으로 반환해준다.
기본적으로 hash는 다른 객체여도 같을 수가 있기 때문에 비교에 적합하지 않으나 hash함수를 쓰는 collection같은 객체가 있으므로 함께 사용하는 것으로 이해하도록 하자.
(4) protected native Object clone() throws CloneNotSupportedException;
객체 자신의 복사본을 반환한다. 리턴 타입이 Object 타입으로 선언되어 있으므로 clone() 메소드가 리턴하는 객체를 타입에 맞게 사용할려면 캐스트 연산을 해야 한다.
※
- 복제 가능한 클래스는 정해져 있어서 모든 클래스에 대해 호출할 수는 없다. 각 클래스의 API 규격서에서 Cloneable이라고 쓰여진 클래스만 가능하다.
- 복제 가능한 클래스를 직접 만들기 위해서는 Cloneable 인터페이스를 구현하거나 clone 메소드를 오버라이딩 하면 된다.
public static void main(String[] args) {
Person person = new Person("soap", 20);
//Object의 접근 지정자가 protected이기 때문에
//직접 정의한 클래스에서 사용하기 위해선 오버라이딩이 필요하다.
Person person2 = (Person) person.clone();
}
public class Person {
private String name;
private int age;
...
public Object clone() {// clone 메소드를 오버라이드
try {
return super.clone();
} catch (CloneNotSupportedException e) {
return null;
}
}
(5) public String toString()
객체 자신의 정보를 문자열로 반환한다.
//Object 클래스에 정의된 toString()
public String toString() {
return getClass().getName()+"@"+Integer.toHexString(hashCode());
}
public static void main(String[] args) {
Person person = new Person("soap", 20);
System.out.println(person.toString());
}
※ 보통 아래와 같이 클래스의 변수들을 출력하는 용도로 많이 사용한다.
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
(6) public final native void notify();
객체 자신을 사용하려고 기다리는 쓰레드를 하나만 깨운다.
(7) public final native void notifyAll();
객체 자신을 사용하려고 기다리는 모든 쓰레드를 깨운다.
(8) public final void wait() throws InterruptedException
public final native void wait(long timeoutMillis) throws InterruptedException;
public final void wait(long timeoutMillis, int nanos) throws InterruptedException {
다른 쓰레드가 notify()나 notifyAll()을 호출할 때까지 현재 쓰레드를 무한히 또는 지정된 시간동안 기다리게 한다.
(9) protected void finalize() throws Throwabl
객체가 소멸될 때 가비지 컬렉터에 의해 자동적으로 호출된다.(오버라이딩 해놓았을때)
객체가 소멸되는 시점에 특정한 동작을 수행해야할 때 사용한다.
※ finalize()는 지양해야 한다.
만일 finalize()메소드를 수행하는데 오랜 시간이 걸린다면 객체가 오랫동안 메모리에 점유하게 되고 이로 인해 OOME(Out of Memory Error)가 발생한다.
※ ♥ 스터디원 참고 및 리뷰
참고
final 키워드
blog.lulab.net/programming-java/java-final-when-should-i-use-it/
하이딩
inor.tistory.com/7
자바의 정석
더블 디스 패치
wonwoo.ml/index.php/post/1490
정적 동적 디스패치
feco.tistory.com/86
wonwoo.ml/index.php/post/1475
equals()와 hashCode()
jeong-pro.tistory.com/172
'Language > Java' 카테고리의 다른 글
[자바 스터디 8주차] 인터페이스 (4) | 2021.01.09 |
---|---|
[자바 스터디 7주차] 패키지 (0) | 2021.01.03 |
[자바 스터디 5주차] 클래스 (0) | 2020.12.19 |
switch문 동작방식 (0) | 2020.12.09 |
[자바 스터디 4주차] 제어문 (0) | 2020.12.02 |