[Java] Day06 - 상속
2021-02-03 # Java

Day06 상속


Review day05


Q1. 초기화블록과 생성자 중 어느것이 먼저 실행되는가?


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private int number;
{
this.number = 10;
System.out.println("init block");
}

<br>

public Init(){
this.number = 100;
System.out.println("constructor");
}

main(){
Init init = new Init();
}

다음과 같은 코드를 살펴보면 init block –> constructor로 출력이 됨을 확인할 수 있다. 즉, 초기화블록이 먼저 실행되고 그다음 생성자가 실행되는 것이다. 그렇다면 왜 초기화블록을 사용하는가? 바로, static 변수에 관해서 초기화가 필요할 수 있기 때문이다.

참고로


1
2
3
public Init(int number){
this()
}

위와 같은 코드는 디폴트 생성자인 this()를 호출하여 생성자를 호출하는 것이다. this 자체는 생성자는 아니지만(인스턴스) 해당 클래스의 생성자를 호출할 수 있다. (생성자는 메서드가 아니다.)
모든 생성자에는 explicit하게 this();를 쓰지 않아도 implicit하게 this();가 호출이 된다.
모든 생성자에는 explicit하게 super();를 쓰지 않아도 implicit하게 super();이 호출이 된다. 이는 이후에 있을 상속을 살펴보며 다시 보겠다.

Q2. final은 언제 사용하는 것이 좋은가?

상속을 막을 때 final, 생성을 막을 때 abstract로 인지하자!
String은 final로 선언되어 있는 것을 확인할 수 있는데 이처럼 상속받은 클래스가 메소드들을 오버라이딩하게 된다면 클래스의 목적이 달라져서 상속자체를 막기위하여 final class를 정의하게 된다. 단, 정확히는 오버라이딩만 막으려면 메소드를 final로 정의하면된다.




Day06 상속

자바 상속의 특징


첫번째로 Single inheritance만 된다는 것이다. C++같은 언어는 다중상속을 허용한다.
여러 조상클래스로부터 상속받는 것이 가능하다는 것이다. 다중상속을 허용하면 여러 클래스로부터 상속받을 수 있기 때문에 복합적인 기능을 가진 클래스를 쉽게 작성할 수 있따는 장점이 있지만, 클래스간의 관계가 매우 복잡해진다는 것과 서로 다른 클래스로부터 상속받은 메서드나 멤버간의 이름이 같은 경우 구별할 수 있는 방법이 없다는 단점이 있다. 이에 따른 rule이 존재하여 어떤메서드가 어디서 상속되었는지 확인하지 못하면 찾기어려운 오류가 생기기 마련이다.

이러한 오류를 해결하기 위해 다중상속의 장점을 포기하고 단일상속만을 허용한다.

단일 상속이 하나의 조상클래스만을 가질 수 있기 때문에 다중상속에 비해 불편한 점이 있지만 클래스간의 관계가 더 명확해지고 코드를 신뢰할 수 있게 만들어준다는 점에서는 장점을 가지게 된다.

두번째 특징으로는 모든 클래스는 implicit하게 Object의 서브클래스라는 점이다. 이 부분은 잠시 후에 Object클래스를 다루면서 다시 얘기해보겠다.

super keyword


super는 자손 클래스에서 조상 클래스로부터 상속받은 멤버를 참조하는데 사용되는 참조변수이다.

더 간단하게 말하자면 상속받은 클래스가 조상 클래스의 멤버를 보기위해 앞에 붙여야하는 keyword인 것이다.

멤버변수와 지역변수간에 구별하기 위해 this keyword를 붙인것과 비슷한 이치이다.

Example


  1. 부모 클래스의 멤버 변수 접근 : super.멤버변수
  2. 부모 클래스의 멤버 메서드 접근 : super.멤버메서드(매개변수)
  3. 부모 클래스의 생성자 호출 : super(매개변수)

단 주의해야할 점은 반드시 자식 클래스의 생성자 첫 라인에서 부모 생성자를 호출해야한다는 점인데 쓰지 않아도 implicit하게 호출이 된다.

super()


super()는 부모클래스의 생성자를 호출할 때 사용된다. 이를 constructor chaining이라고 부른다.

Method overriding


오버라이딩이란 조상 클래스로부터 상속받은 메서드의 내용을 변경하는 것을 말한다.
그에 따른 조건은 다음과 같이 세개가 있다.

  1. 이름이 같아야한다.
  2. 매개변수가 같아야한다.
  3. 반환타입이 같아야한다.

즉, 선언부가 같아야한다는 것이다. 다만 access modifierexception은 제한된 조건 하에서만 다르게 변경할 수 있다.

  1. access modifier(접근제어자)는 조상클래스의 메서드보다 좁은 범위로 변경할 수 없다.
  • 만일 조상클래스에 접근제어자가 protected라면 오버라이딩할 시 protected나 public이어야한다. 대부분의 경우에는 같은 범위의 접근 제어자를 사용한다. 접근범위의 순서는 public, protected, (default), private이다.
  1. 조상클래스의 메서드보다 많은 수의 예외를 선언할 수 없다.

또한, 인스턴스메서드를 static메서드로, static메서드를 인스턴스메서드로 변경할 수 없다.

조상 클래스에 정의된 static메서드를 자손 클래스에서 똑같은 이름의 static메서드로 정의할 순 있지만 이는 각 클래스에 별개의 static메서드를 정의한 것일 뿐 오버라이딩한 것이 아니다.

static멤버들은 자신들이 정의된 클래스에 묶여있다고 생각하면 좋다.

Example

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public 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;
}
}

위처럼 상속을 받은 후에 원래 정의되어있던 메서드를 위의 규칙에 따라 재정의하는 것이 오버라이딩(overriding)이다.

Dynamic Method Dispatch


Runtime polymorphism. “같은 클래스를 상속하고 있는 여러 클래스 중 어느 서브클래스를 사용할 것인가”를 런타임 시점까지 미룸으로서, 클래스 재사용성을 높이는 테크닉이다.

Visitor Pattern


오른쪽에는 Composite 패턴으로 구현 된 File과 Directory로 이루어진 데이터 구조가 있다.

다만, 방문자를 수용하기 위해 Element 인터페이스를 상속받아서 accept() 메서드를 각각 구현하고 있으며 각 element의 경로를 구하는 연산 부분이 방문자에서 이루어진다.

왼쪽에는 방문자로 데이터 구조를 방문하면서 필요한 연산을 수행한다. 각 element에 접근하기 위한 visit메서드를 오버라이딩 및 오버로딩을 하고 있다.

예제

1
2
3
4
5
6
7
8
9
10
11
12
public interface Element {
public abstract void accept(Visitor v);
}

public abstract class Entry implements Element {
String name;
public Entry(String name)
{
this.name = name;
}
public abstract void add(Entry entry);
}

방문자를 수용하기 위한 accept() 메서드를 정의하는 인터페이스 Element와 File과 Directory가 공통적으로 구현 해야 할 인터페이스를 정의하는 상위 클래스 Entry를 정의한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class File extends Entry {
public File(String name)
{
super(name);
}
public void add(Entry entry){}
public void accept(Visitor v)
{
v.visit(this);
}
}

public class Directory extends Entry {

ArrayList<Entry> directory = new ArrayList(); //자식 객체를 담기 위한 ArrayList

public Directory(String name)
{
super(name);
}
public void add(Entry entry) //자식 객체 추가
{
directory.add(entry);
}
public void accept(Visitor v)
{
v.visit(this); //어느 visit() 메서드를 호출할지 결정납니다.
}
}

데이터 구조에 해당하는 File과 Directory이다.

앞서 Composite패턴에서는 경로를 구하는 연산을 이곳에서 이루어졌던 반면에 지금은 방문자를 수용하는 accept() 메서드를 구현하고 있다.

accept() 내용은 매개변수로 받은 Visitor 객체를 통하여 자기 자신을 인자로 하는 visit() 메서드를 호출 하고 있다.

살펴 볼 것은 visit() 메서드는 File과 Directory에서 자기 자신을 인자로 해서 호출이 이루어지고 있다.

이때 File에 대한 visit() 메서드가 호출이 될지 Directory에 대한 visit()가 호출이 될지 결정이 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class Visitor {

public abstract void visit(File file);
public abstract void visit(Directory directory);
}

public class ViewVisitor extends Visitor {

private String Path="";
public void visit(File file)
{
System.out.println(Path + "/"+file.name);
}
public void visit(Directory dic)
{
Path = Path + "/" + dic.name;
System.out.println(Path);
for(int i=0; i<dic.directory.size();i++)
{
dic.directory.get(i).accept(this);
}
}

}

Visitor 클래스는 방문자들이 데이터 구조를 방문하기 위한 인터페이스를 정의하고 있다.

ViewVisitor 클래스는 데이터 구조를 방문하면서 각 Element의 현재 경로를 출력해주는 역할을 한다.

visit() 라는 같은 이름으로 하나는 Directory를 인자로 갖는 다른 하나는 File를 인자로 갖는 메서드를 오버로딩이 되어 있는 상태이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Main {

public static void main(String argsp[])
{
Directory root = new Directory("root");
Directory bin = new Directory("bin");
Directory Lkt = new Directory("Lkt");
File file1 = new File("file1");
File file2 = new File("file2");
File file3 = new File("file3");
File file4 = new File("file4");

root.add(file1);
bin.add(file2);
bin.add(file3);
Lkt.add(file4);
root.add(Lkt);
root.add(bin);

root.accept(new ViewVisitor()); //경로 출력
}
}

이처럼 Visitor 패턴은 데이터 구조와 연산을 분리함으로써 데이터 구조를 변경하지 않으면서 새로운 연산을 쉽게 할 수 있다.

만약 새로운 연산을 추가하고자 한다면 새로운 visit() 메서드를 구현하면 된다.

Abstract class


추상클래스는 인스턴스로 생성할 수 없고 상속을 통해서 자손클래스에 의해 완성될 수 있는 클래스이다.

추상클래스 자체로는 클래스로서의 역할이 부락능하지만, 새로운 클래스를 작성하는데 있어서 바탕이 되는 조상클래스로의 중요한 의미가 있다. 클래스의 틀을 제공해준다고 보면 좋다.

1
2
3
4
abstract class Player{
abstract void play(int pos);
abstract void stop();
}

위의 예제처럼 선언부만 정의되어있고 구현부는 정의되어있지 않은 특징을 지닌다. 진짜 말그대로 틀만 잡아준 것이다.

단, 추상메서드를 구현해놨다면 자손클래스에서는 반드시 구현을 시켜야한다. 그렇지 않으면 상속받은 클래스 또한 추상클래스로 정의된다.

위처럼 컴파일 에러가 나게된다.

Object class


아까 말했듯 Object클래스는 모든 클래스 상속계층도의 최상위에 있는 조상클래스이다.

다른 클래스로부터 상속 받지 않는 우리가 생각하는 기존의 최상위 조상클래스는 자동적으로 Object클래스로부터 상속받게 함으로써 이것을 가능하게 한다.

실제로 위의 예시의 Player클래스를 Player instanceof Object문은 True가 나온다.

Object클래스는 위와 같은 메서드들이 정의되어있으며 모든 클래스들은 위의 메서드들을 사용할 수 있다.


toString()


기본동작 : 객체의 해시코드를 출력한다.
Override 목적 : 객체의 정보를 문자열 형태로 표현하고자 할 때
toString() 의 원형은 아래와 같다.

1
getClass().getName() + '@' + Integer.toHexString(hashCode())

equals()

기본동작 : ‘==’ 연산 결과 반환

override 목적 : 물리적으로 다른 메모리에 위치하는 객체여도 논리적으로 동일함을 구현하기 위함.

equals()를 사용해 두 객체의 동일함을 논리적으로 override할 수 있다.

‘물리적 동일함’ - 객체가 메모리에서 같은 주소값을 갖는 것을 의미

‘논리적 동일함’ - 물리적으로는 다른 위치에 있지만 같은 id의 회원객체, 같은 id의 주문객체와 같이 도메인을 구분할 수 있는 고유한 값 등에 의해 동일한 것을 의미


hashCode()

해시코드란, jvm이 인스턴스를 생성할 때 메모리 주소를 변환해서 부여하는 코드.

실제 메모리 주소값과는 별개의 값이며 실제 메모리 주소는 System 클래스의 identityHashCode()로 확인할 수 있다.

기본동작 : JVM이 부여한 코드값. 인스턴스가 저장된 가상머신의 주소를 10진수로 반환한다.

override 목적 : 두 개의 서로 다른 메모리에 위치한 객체가 동일성을 갖기 위함.

자바에서의 동일성

equals()의 반환값이 true, hashCode() 반환값이 동일함을 의미한다.

보통 equals()와 hashCode()는 함께 override 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class User{
int id;
String name;

public User(int id, String name){
this.id = id;
this.name = name;
}

public int getId(){
return id;
}

@Override
public boolean equals(Object obj){
if(obj instanceof User){
return this.getId() == ((User)obj).getId();
}else{
return false;
}

public static void main(String[] args) {

User user1 = new User(1001, "홍길동");
User user2 = new User(1001, "홍길동");

System.out.println("user1.equals(user2): " + user1.equals(user2));
System.out.println("user1.hashCode(): " + user1.hashCode());
System.out.println("user2.hashCode(): " + user2.hashCode());
}
}

위 main 메소드 실행 결과

  • equals 메소드를 overriding 하여 같음을 확인
  • hashCode는 별도 재정의 하지 않았음으로 아래와 같이 다르게 출력

user1.equals(user2): true
user1.hashCode(): 901506536
user2.hashCode(): 747464370

Process finished with exit code 0


clone()

해당 인스턴스를 복제하여 새로운 인스턴스를 반환한다.

but. 단지 필드의 값만 복사함으로, 필드의 값이 배열이나 인스턴스면 제대로 복제할 수 없다.

→ clone() 메소드를 오버라이딩하여 복제가 제대로 이루어지도록 재정의 해야 한다.

이러한 clone() 메소드는 데이터의 보호를 이유로 Cloneable 인터페이스를 구현한 클래스의 인스턴스만이 사용할 수 있다.

Clone 메소드는 신중하게 오버라이드하자. (feat. effective Java)

Cloneable 인터페이스는 복제를 허용하는 객체라는 것을 알리는 목적으로 사용하는 믹스인 인터페이스이다. (Mixin Interface)

  • 믹스인 인터페이스이기 때문에 자신이 clone method를 가지고 있는 것도 아니다.

  • Object의 clone은 Cloneable을 implements 하지 않으면 사용할 수 없다.

    Cloneable 을 impelemtns한 class에서 clone을 호출하면, 해당 객체의 복제본을 만들어 반환한다.

→ 복제 객체는 원본 객체와 같은 필드를 가지며 각 필드의 값도 복사된다.

but.

reference를 가진 녀석들은 Deep Copy 가 아닌 Soft Copy를 수행한다는 점에 주목해야한다.

clone 메소드는 또 다른 생성자와 같다.

clone 메소드가 원본 객체에 손상을 주지 않으면서 원본과 복제 객체 간의 상호 영향도 없도록 해야 한다.

→ 즉, reference의 경우 deep copy를 해주어야 한다는 것이다.

여기서 deep copy는 해당 객체의 clone을 호출해주는 것만으로 끝나는 것이 아니다.

  • Collection일 경우에는 Collection 안의 내용물들도 clone 을 해주는 진정한 Deep Copy가 되어야 한다.

finalize()

finalize() 메소드는 직접 호출하는 메소드가 아니라 객체가 HEAP 메모리에서 해제될 때 가비지 콜렉터가 호출하는 메소드이다.

이 메소드가 Override되어 있으면 가바지 콜렉터가 이 메소드를 호출하여 실행한다.

즉, finalize()는 객체가 해제될 때 리소스 해제, 소켓 close 등의 필요한 것들을 구현해주면 된다.




Reference

Java의 정석
https://www.notion.so/e5c33507880b4d098f83a2c4f8f02c04
https://github.com/ByungJun25/study/tree/main/java/whiteship-study/6week#Method-Overriding%EA%B3%BC-Hiding
https://lktprogrammer.tistory.com/58
https://blog.naver.com/swoh1227/222181505425
https://leemoono.tistory.com/20