상속

반응형
반응형

*원래 클래스 파트에서 소개를 해야되는데... 깜빡하고 하지 않해서 상속 파트에서 하게 되었습니다.
색칠된 부분은 상속파트에 밀접적으로 관련된 부분입니다.^^;;😃

시작하기 앞써..

더보기

자바는 객체지향언어이다. 객체지향은 캡슐화, 추상화, 상속성, 다형성의 특징을 가지고 있다.
상속을 알아보기전에 객체지향의 특징을 하나씩 살펴보도록 하자.

1. 캡슐화(Encapsulation)

캡슐화는 변수와 메소드를 하나의 클래스로 묶는것을 말한다.
마치 편의점에서 파는 도시락과 비슷하다.

편의점 도시락

그림에서 보는것과 같이 까만색 플라스틱 상자?가 클래스이구 밥과 반찬은 메소드 혹은 변수다.
우리는 개발자이기때문에 코딩을 해보자.

public class Lunch {
    String rice;
    String SideDish;
    
    public String getRice() {
        return rice;
    }

    public void setRice(String rice) {
        this.rice = rice;
    }

    public String getSideDish() {
        return SideDish;
    }

    public void setSideDish(String sideDish) {
        SideDish = sideDish;
    }
}

이렇게 작성하였다. 이게 바로 캡슐화이다.

하지만 위 코드의 문제점은 누가 밥과 반찬을 몰래 바꿀 수 있기 때문이다.
그러면 어떻게 해야할까?

정보은닉(Information Hiding)

바로 도시락에 뚜껑을 씌워야 한다. 그래야 도시락이 바뀌는것을 방지할 수 있다.(사실 바뀌는 것보다 훔쳐가는게 더 문제다.)

도시락 뚜껑

이제 이 도시락은 안전해졌다. 뚜겅을 덮었기 때문에 도시락의 내용물을 지킬 수 있게 되었다.
코드에서는 간단하다. 바로 private만 입력해주면 된다.

public class Lunch {
    private String rice;
    private String SideDish;
    
    public String getRice() {
        return rice;
    }

    public void setRice(String rice) {
        this.rice = rice;
    }

    public String getSideDish() {
        return SideDish;
    }

    public void setSideDish(String sideDish) {
        SideDish = sideDish;
    }
}

곧 학습하겠지만 private를 붙이게 되면 값을 마음대로 변경시키는 것이 불가능해진다.(물론, setXX으로 값 변경이 가능하지만...ㅎㅎ)

근데 위 사진처럼 도시락의 갯수가 하나일리는 없다. 
도시락의 갯수는 무수히 많다. 
일일이 도시락 종류 클래스를 늘리는것은 비효율적으로 느껴진다.

2.추상화(Abstraction)

바로 공통된 부분만 따로 관리하는 것이다. 다시 말하자면, 공통된 부분만 추출하면 된다.

여러개의 도시락

현재 사진을 보면 밥, 김치, 반찬1,반찬2의 종류만 달라지고 전체적인 반찬의 갯수는 5개로 같다.
자바에서는 보통 interface라는 추상 클래스를 관리되어진다.

이것을 코드로 구현하면 다음과 같다.

public interface Lunch {
    void addLunch(String rice, String kimchi, String ... sideDish);
}

참고로 ...는 가변 변수로 변수를 자유롭게 추가가 가능하다.

이제 이것으로 다시 도시락을 만들어 보자. 

public class FirstLunch implements Lunch{
    private String rice;
    private String kimChi;
    private String sideDish1;
    private String sideDish2;


    @Override
    public void addLunch(String rice, String kimchi, String... sideDish) {
        this.rice = rice;
        this.kimChi = kimchi;
        this.sideDish1 = sideDish[0];
        this.sideDish2 = sideDish[1];
    }
}

 이제부터 여려개의 도시락을 만들 수 있게 되었다.

public class SecondLunch implements Lunch{
    private String rice;
    private String kimChi;
    private String sideDish;
    
    @Override
    public void addLunch(String rice, String kimchi, String... sideDish) {
        this.rice = rice;
        this.kimChi = kimchi;
        this.sideDish = sideDish[0];
    }
}

예제에서는 메소드가 단 하나뿐이지만 갯수가 많아진다면 어떻게 될까?

여러개의 도시락

다시 사진을 보면 반찬이 들어가는 갯수가 등 5개로 일정하다. 공통된 부분이 존재한다. 특히 소시지 부분이 비슷한 부분이 존재한다.
소지지를 미리 만들면 좋지 않을까?

3. 상속성(Inheritance)

상속을 이용하면 이 문제를 해결 할 수 있다. 

 파란색 테두리의 도시락을 살펴보면 소시지의 종류가 같다는 것을 알 수 있다.
즉, 소시지부분을 따로 제작하면 추후에 만들때 편하지 않을까?

하나만 만드는것은 굉장히 비효율적으로 느껴질 수 있겠지만, 설명을 위해 양해 부탁 드립니다.

이런식으로 코드를 짤 수 있습니다.

public class Sausage {
    private String sausage = "비엔나";
    

    public String getSausage() {
        return sausage;
    }
}
public class ThirdLunch extends Sausage implements Lunch{
    private String rice;
    private String kimChi;
    private String sausage;
    private String sideDish;

    @Override
    public void addLunch(String rice, String kimchi, String... sideDish) {
        this.rice = rice;
        this.kimChi = kimchi;
        this.sausage = super.getSausage();
        this.sideDish = sideDish[0];
    }
}

 

이제 비엔나소시지가 들어있는 도시락을 미리 만들 수 있게 되었습니다.


super에 대한 키워드는 추후에 설명하겠습니다.
하지만 이렇게 만들면 소시지 위치에는 비엔나 소시지만 들어가게 된다.
어떻게 하면 다른 소시지를 넣을 수 있을까?

4.다형성(Polymorphism)

이때 활용되는 것이 다형성이다.
다형성이란 다양하게 표현되는 것을 말한다.

여러개의 도시락

이 사진으로 예시로 들어보자면... 기본적으로 밥, 김치, 소시지, 기타 반찬이 들어있다. 
상속 예제에서는 "비엔나 소시지"로 예제를 만들었지만, 이 자리에 반드시 비엔나 소시지가 들어갈 이유도 없다.
다형성은 다양하게 만들 수 있는데, 오버라이딩같은 방법으로 다형성을 설정할 수 있다.

코드를 다음과 같이 수정 하면 된다.

public class ThirdLunch extends Sausage implements Lunch{
    private String rice;
    private String kimChi;
    private String sausage;
    private String sideDish;

    @Override
    public String getSausage() {
        this.sausage = "독일";
        return sausage;
    }

    @Override
    public void addLunch(String rice, String kimchi, String... sideDish) {
        this.rice = rice;
        this.kimChi = kimchi;
        this.sausage = super.getSausage();
        this.sideDish = sideDish[0];
    }
    
    
}

super.getSausage()라는 부분을 수정해도 되지만,
다양성을 설명하기 위해 일부러 이렇게 했다.

이제 비로소 각 도시락마다 서로 다른 소시지를 넣을 수 있게 되었다.

간단하게 객체지향에 대해 학습해봤다.

제목에서 예고한 것처럼 [상속]에 대해 더 자세히 살펴보자.

상속이란?

상속은 상위 객체에서 상태나 동작등을 물려받는다.
상위 객체는 주로 부모 객체라고 말하며,
물려받는 객체 즉, 하위 객체는 자식 객체라고 표현된다.

예를 들어, 프로그래밍 서적으로 예시를 들어보겠다.
다음과 같이 프로그래밍 서적이 준비되어 있다.

프로그래밍 서적

이들의 공통점은 프로그래밍 서적이다. 

상속

즉, 부모 클래스는 "프로그래밍 서적"이구 
자식 클래스는 위 서적들을 뜻한다.

"프로그래밍 서적" 클래스에 정의된 내용들은 하위 서적들에서도 사용 된다.

자바의 상속은 총 2가지 방법으로 나타낼 수 있다.

extends

일반적으로 상속이라고 한다면 이것을 뜻합니다, 다중 상속이 불가능하다는 특징을 가지고 있다.

다중 상속은 상속을 여러개 받는 행위를 뜻한다.

위 그림을 코드로 만들어 보면 다음과 같다.

public class ProgrammingBook {
}

public class EasyJavaWebDevelopment extends ProgrammingBook{
}

public class DesignPatterns extends ProgrammingBook {
}

public class JavaStandard extends ProgrammingBook{
}

public class SuccessfulProgrammingStudy extends ProgrammingBook{
}


대표로 EasyJavaWebDevelopment로 해보겠다.

간단히 

public class ProgrammingBook {

    public void hello() {
        System.out.println("안녕하세요.");
    }
}

자식 클래스에서는 위 메소드를 만들지 않는다.

public class EasyJavaWebDevelopment extends ProgrammingBook{
}

이제 사용해보겠다.

public class Client {
    public static void main(String[] args) {
        EasyJavaWebDevelopment book = new EasyJavaWebDevelopment();
        // ProgrammingBook book = new EasyJavaWebDevelopment();도 됨..
        book.hello();
    }
}

결과

놀랍게도 자식 클래스에는 메소드를 넣지도 않았는데, 부모 클래스에 넣은 메소드가 출력되었다.
이렇게 상속 되었다라고 할 수 있다.

implements

구성이라고도 불리며, 다중 상속이 가능하다는 특징을 가지고 있다.
jdk8이후부터 extends처럼 사용이 가능해졌습니다. (default, static)

위 코드를 다시 implements으로 수정해 보자.

public interface ProgrammingBook {
}

public class EasyJavaWebDevelopment implements ProgrammingBook{
}

public class DesignPatterns implements ProgrammingBook {
}

public class JavaStandard implements ProgrammingBook{
}

public class SuccessfulProgrammingStudy implements ProgrammingBook{
}

다중 상속이 가지는 문제점?

다중 상속이 가능해지면서 한 가지 걱정이 생겼다.
바로 다이아몬드 상속이다. 
다이아몬드 상속은

다이아몬드 상속

이말과 같다.
아버지는 할아버지의 자식이고,
나는 아버지의 자식인데,
할아버지는 나의 자식이다. 

어딘가 이상하다.

아버지클래스와 내 클래스 관계는 어떻게 되는 것일까?
아버지클래스가 내 클래스보다 상위 클래스인지? 자식 클래스인지 헷갈린다.
이러한 문제점을 다이아몬드 상속문제라고 부른다. 

 

자바8에서는 3가지 방법으로 이 문제를 해결 할 수 있다.

우선순위 1번. 클래스가 항상 이긴다.


인터페이스로 상속된 클래스보다  클래스로 상속된 클래스가 더 우선순위가 더 높다.

public interface A {
    default void print() {
        System.out.println("A가 이긴다.");
    }
}

public class B implements A{
    public void print() {
        System.out.println("B가 이긴다.");
    }
}

public class D extends B implements A{

    public static void main(String[] args) {
        new D().print();
    }
}

무엇이 출력될까?

B가 이긴것으로 확인 되었다. 일부러 B는 A를 상속 받았다.

우선 순위 2번. 자식 클래스가 이긴다.

public interface A {
    default void print() {
        System.out.println("A가 이긴다.");
    }
}

public interface C extends A {
    @Override
    default void print() {
        System.out.println("C가 이긴다.");
    }
}

public class D implements A,C{

    public static void main(String[] args) {
        new D().print();
    }
}

우선 순위3번. 명시적으로 호출한다.

public interface A {
    default void print() {
        System.out.println("A가 이긴다.");
    }
}

public interface C {
    default void print() {
        System.out.println("C가 이긴다.");
    }
}

public class D implements A,C{

    public static void main(String[] args) {
        new D().print();
    }
}

에러가 발생한다.

public class D implements A,C{
    @Override
    public void print() {
        A.super.print();
    }

    public static void main(String[] args) {
        new D().print();
    }
}

이런식으로 메소드를 오버라이딩해서 명시적으로 사용해야된다.

그러면 

계속 A가 져서 이번에는 강제로 이기게 해주었다.

지금까지 자바상속의 특징에 대해 알아보았다.

근데, 상위 클래스도 this처럼 조작하는 방법이 있을 까?

super

this을 이용해 클래스 정보를 알아내는 것 처럼,
super키워드를 이용하여 상위 클래스 정보를 가져 올 수 있다.

 super키워드는 언제 사용할까?

   - 부모 클래스의 인스턴스를 참조할때

public class ProgrammingBook  {
    String title = "basic";

}

public class EasyJavaWebDevelopment extends ProgrammingBook{

    String title = "쉽게 따라하는 자바 웹 개발";

	public String getTitle() {
        return super.title;
    }
    public static void main(String[] args) {
        System.out.println(new EasyJavaWebDevelopment().getTitle());
    }
}

// 무엇이 나올까요?

정답

 - 부모 클래스 메서드를 호출할때 사용된다.

public class ProgrammingBook  {
     String title = "basic";

     public void typing() {
         System.out.println("자바를 학습합시다.");
     }
}

public class DesignPatterns extends ProgrammingBook {

    public void talk() {
        super.typing();
        System.out.println("이것은 디자인패턴 책입니다.");
    }

    public static void main(String[] args) {
        new DesignPatterns().talk();
    }
}

// 무엇이 나올까요?

정답

- 부모 클래스의 생성자를 호출하는데 사용된다.

public class ProgrammingBook  {

     public ProgrammingBook() {
         System.out.println("기본적으로 알아야 할것!");
     }
}

public class JavaStandard extends ProgrammingBook{

    public JavaStandard() {
        super();
    }
}

// 정답은 무엇일까요?

정답

그냥 궁금해서 해보는것!

this를 통해 값을 받는 것이 아닌
super를 통해 값을 받는 것은 가능할까?

public class ProgrammingBook  {
     String title = "basic";

}

public class SuccessfulProgrammingStudy extends ProgrammingBook{
    public SuccessfulProgrammingStudy(String title) {
        super.title = title;
    }
    
    public static void main(String[] args) {
        SuccessfulProgrammingStudy study = new SuccessfulProgrammingStudy("성공하자!");
        System.out.println(study.title);
    }
}

코딩이 되네.... 신기하군...

상위 클래스에다 저장할 수 있군.

같은 부모를 클래스로 두고 있는 형제 클래스에서는 어떻게 될까?

DesignPatterns patterns = new DesignPatterns();
System.out.println(patterns.title);

안되네...;;

결국, this가 할수 있는건 super로도 가능하다는것 같다.
근데 언제 쓰냐...;;;ㅎㅎ;
차이점은 현재 클래스 조작이냐? 아니면 부모 클래스 조작이냐? 인것 같다.

super.super.super이런것도 가능한지 확인하고 싶다.

public class A {
    String title = "A";
}

public class B extends A{

    public B() {
        super.title = "B";
    }
}

public class C extends B{
    public C() {
        super.super.title = "C";
    }
}

// 확인결과 안됨... 1번만 사용가능하다.

왜 한번만 되는지 바이트 코드에는 답이 있을까 했는데... 

public class study.whiteship.homework6.il.A {
  java.lang.String title;

  public study.whiteship.homework6.il.A();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: aload_0
       5: ldc           #7                  // String A
       7: putfield      #9                  // Field title:Ljava/lang/String;
      10: return
}

잘 모르겠다.. 확실한건 한 번만 가능하다는 것!

메소드 오버라이딩 (오버로딩은 다른거임)

부모 클래스에 메소드를 정의하였다...
그런데,, 자식 클래스에서 메소드값을 변경해야 되는 경우도 있다. 
그럴때 사용되는 것이 오버라이딩 이다.

저는 이렇게 외웠습니다. 오버라이딩 => over + write -ing => 덮어쓰다.

따라서 오버라이딩을 사용하게 되면 부모 클래스에 정의된 내용을
자식 클래스에서 그 메소드를 덮어써서 사용이 가능하다.

public class ProgrammingBook  {
     String title = "basic";

     public void author() {
         System.out.println("작가 미상");
     }

}

public class EasyJavaWebDevelopment extends ProgrammingBook{

    @Override
    public void author() {
        System.out.println("백기선");
    }

    public static void main(String[] args) {
        new EasyJavaWebDevelopment().author();
    }
}

다이나믹 메소드 디스패치

 메소드 디스패치 : 어떤 메소드를 호출할 것인가를 결정하여 그것을 실행하는 과정이다.
라고 위키백과에 나와 있다.

정확히 잘 모르지만 일단 코딩해보자.

 

동적 디스패치 - 위키백과, 우리 모두의 백과사전

위키백과, 우리 모두의 백과사전. 디스패치는 어떤 메소드를 호출할 것인가를 결정하여 그것을 실행하는 과정이다. 동적 디스패치와 정적 디스패치가 있는데, 동적 디스패치(dynamic dispatch)는 메

ko.wikipedia.org

메소드 디스패치 종류

       
       - dynamic 메소드 디스패치 

정적으로 동작하냐 동적으로 동작하냐에 따라 결정된다.

이것을 어떻게 하면 이해할 수 있을까 고민했다.

- static 메소드 디스패치 

이 디스패치는 다음과 같다.

더보기


OO가게에서는 치킨과 피자를 판다. 

우리는 치킨과 피자를 주문하려고 한다.
메뉴는 하나 뿐이라면, 배달 오는 치킨과 피자는?

우리에게 메뉴가 하나밖에 없으니 선택권이 존재하지 않는다.
따라서 

위 제품이 배달온다.

 

static 메소드 디스패치도 위와 같은 방식으로 진행됩니다.
똑같이 해보겠습니다.

 

public class Store {
    public void make() {
        System.out.println("치킨과 피자를 배달한다.");
    }
}

public class Client {
    public static void main(String[] args) {
        new Store().make();
    }
}

Sore클래스에 있는 make함수가 전달 된다. 이 방식으로 사용하면, 컴파일할 때 호출한다는 것을 알 수 있다.

dynamic 메서드 디스패치 :
이 방식도 똑같이 설명하겠다.

더보기


OO가게에서는 치킨과 피자를 판다. 

 

우리는 치킨과 피자를 주문하려고 한다.

만약에 매운맛 을 주문했다면, 배달 오는 치킨과 피자는?

메뉴판에는 매운맛에 대한 치킨과 피자가 존재하지 않는다.
상위 메뉴와 동일하다고 했으니, 
배달 오는 치킨과 피자는 다음과 같다.

위 제품이 배달온다.

우리는 맛을 결정했을 뿐, 치킨과 피자는 결정하지 않았다.
근데 우리는 어떤 치킨과 피자가 배달 올지 정확히 알 고 있다. 
상위 메뉴를 보기전에는 어떤 치킨과 피자가 올지 고민 해야한다.
이것이 바로 dynamic 메소드 디스패치다.

dynamic 메소드 디스패치도 위 처럼 동작합니다.

public class Store {

    public void make() {
        System.out.println("치킨과 피자를 배달한다.");
    }
}

public class HotStore extends Store{
    @Override
    public void make() {
        System.out.println("매운 치킨과 피자를 배달한다.");
    }
}

public class NoHotStore extends Store{
    @Override
    public void make() {
        System.out.println("맵지 않은 치킨과 피자를 배달한다.");
    }
}

public class NoHotStore extends Store{
    @Override
    public void make() {
        System.out.println("맵지 않은 치킨과 피자를 배달한다.");
    }
}

public class Client {
    public static void main(String[] args) {
         Store hotStore = new HotStore();
        hotStore.make();
    }
}

 자료형이 Store에 있는 make메소드로 호출 하자.
그런데...
결과 값은 "치킨과 피자를 배달한다"가 아닌

가 등장한다. 어떻게 된것일까?
인스턴스를 확인하니 HotStore로 되어있다. 따라서 사진처럼 출력된다.
우리도 예제에서 약간의 고민을 했던것 처럼
이 방식을 이용하게 되면 런타임 할때 호출 된다는 것을 알 수 있습니다.

어째서 런타임을 사용하는 다이나믹 메소드 디스패치를 사용하는 것일까요? 컴파일시 결정되는 스태틱 메소드 디스패치를 이용하는것이 더 좋지 않을까요? 지금은 컴퓨터도 좋아졌구 IDE가 잘 되어있어서 굳이 할 필요가 없는 것일까요? 고민해보자.
자바가 느린 시절에는 최대한 인터페이스를 사용하지 않고, 클래스로만 구현하는 것을 선호했다고 한다.
 

추상 클래스

 위에서 implements에 대해 설명했지만... 추상 클래스에 대해 다시 설명하겠다.

 생각해봅시다.
 굳이 지금 구현할 필요가 있을까?
 다른 곳에서도 많이 사용될까? 잠깐 위 예제를 가지고 오겠다.

현재 이 예제에서는 Store클래스에서 make라는 메소드로 구현하고 있다.

public class Store {

    public void make() {
        System.out.println("치킨과 피자를 배달한다.");
    }
}

또, 이 메소드는 다른 클래스에서 오버라이드 되어 사용하고 있다.

public class HotStore extends Store{
    @Override
    public void make() {
        System.out.println("매운 치킨과 피자를 배달한다.");
    }
}

public class NoHotStore extends Store{
    @Override
    public void make() {
        System.out.println("맵지 않은 치킨과 피자를 배달한다.");
    }
}

public class NoHotStore extends Store{
    @Override
    public void make() {
        System.out.println("맵지 않은 치킨과 피자를 배달한다.");
    }
}

이 말은 굳이 Store클래스에서는 구현할 필요가 없다는 뜻이다.

이럴때 사용하는 것이 추상 클래스이다.

추상클래스는 2가지 방법으로 만들 수 있다.

interface

class => interface
extends => implements

public interface Store {
    void make();
}

public class HotStore implements Store{
    @Override
    public void make() {
        System.out.println("매운 치킨과 피자를 배달한다.");
    }
}

public class MiddleStore implements Store{
    @Override
    public void make() {
        System.out.println("중간맛 치킨과 피자를 배달한다.");
    }
}

public class NoHotStore implements Store{
    @Override
    public void make() {
        System.out.println("맵지 않은 치킨과 피자를 배달한다.");
    }
}

 

abstract class 

이 방법은 추상 메소드 와 구현 메소드를 동시에 설정하는 방법이다.
추상 메소드란 구현되지 않은 메소드를 뜻하며,
구현 메소드는 구현된 메소드를 뜻한다.

추상 메소드로 만들고 싶다면 abstract를 추가하면 된다.

public abstract class Store {
    abstract void make();
}

public class HotStore extends Store{
    @Override
    public void make() {
        System.out.println("매운 치킨과 피자를 배달한다.");
    }
}

public class MiddleStore extends Store{
    @Override
    public void make() {
        System.out.println("중간맛 치킨과 피자를 배달한다.");
    }
}

public class NoHotStore extends Store{
    @Override
    public void make() {
        System.out.println("맵지 않은 치킨과 피자를 배달한다.");
    }
}

다만 주의해야될 점은 interface처럼 implements가 아닌 extends을 사용하게 된다.
이 키워드를 사용하는 이유는 구현과 추상을 동시에 사용하는 클래스가 필요했기 때문인데,
자바8이후 부터는 interface에서도 구현과 추상을 동시에 할 수 있다.(static,default)

 추상 클래스자체로 인스턴스로 만들시(? 그걸 인스턴스로 만든다고 해야하는지는 잘 모르겠으나..), 익명 클래스로 호출된다. (추후에 배우는 람다와 관련 있다.)

final 키워드

final : 최후의

final class => 최후의 클래스
final method => 최후의 메소드
final variable => 최후의 변수

"최후" 라는 건 마지막이라는 뜻이다. 
자바에서는 클래스, 메소드,변수는 생성이 된다.

즉, 만들어지자 마자 끝이라는 이야기다.
그럼 하나씩 살펴보면서 어떤것이 강제되는지 알아보자.

finalClass 

간단하게 코드를 만들었다.

public final class FinalClass {
}

인스턴스 여러개 생성해보자.

전부 다른걸로 봐서 인스턴스로는 자유롭게 만들 수 있을 것 같다.

이번에는 다른 클래스에 상속시켜보겠다.

확인 결과 상속은 되지 않다.(반대 가능.)

finalMethod 

오버로딩

되는 군요.... 이로서 2개의 메소드는 서로 다른 메소드라는 것이 증명되었다.
오버로딩와 final은 상관없다 ㅜㅜ

오버라이딩 

오버라이딩은 되지않는다. ㅜㅜ

finalVariable

    1. 변수 변경
   

변경 안됨

static은 main에 출력하기 위해 추가

  2. 다른 클래스에서 사용하기

 public static void main(String[] args) {
    FinalVariable variable = new FinalVariable();
    System.out.println(variable.variable);
}

 

  가능한것 불가능한것
finalClass 인스턴스 생성, 상속 당하기 상속 하기
finalMethod 오버로딩 오버라이딩
finalVariable 다른 클래스에서 사용 가능! 값 변경

 

더 있는 지는 모르겠지만,,, 이게 최선!.

Object 클래스

object클래스는 자바에서 모든 클래스의 부모 클래스

public class Servant{

}

==

public class Servant extends Object{
   
}

=> extends Object는 생략되어있다는 뜻이다.

상속이 되있다는 뜻은 
정의되있는 메소드 오버라이딩을 할 수 있다는 뜻이다

어떤것이 있는지 확인 해보자

재활용!

 

객체에는 "지문"과 같은 hashcode라는 것을 가지고 있다.
hashcode는 객체들이 서로다른지 확인할때 사용된다.

hashcode()

이 메소드는 객체가 어떤 지문을 가지고 있는지 알려준다.
리턴은 int로 된다.

그런데 한 가지 의문이 있습니다. 강제로 객체를 2개 생성하고 해시코드값을 강제로 변경시키고
객체들이 서로 같은지 확인하는 코드를 작성한다면,

Servant servant1 = new Servant();
Servant servant2 = new Servant();

System.out.println(servant1 == servant2);

사람으로 친다면 지문이 같습니다. 하지만 지문이 같다고 해서 같은 사람일까?
물론, 지문이 같을리 없겠지만... 

하지만 이 두 객체는 지문이 같으므로 지문여부를 체크해보면,

System.out.println(servant1.hashCode() == servant2.hashCode());

이 값은 true를 리턴한다.

근데 이 두 객체를 같게 만들고 싶다.
이럴때 사용되는 것이 바로

equlas()

입니다. 그러면 이렇게 작성하면 될까?

@Override
    public boolean equals(Object obj) {
    return this == obj;
}
    
    

또는

@Override
   public boolean equals(Object obj) {
   return this.equals(obj);
}

아쉽지만 이 두개모두 false를 리턴합니다. 왜냐하면 어디를 찾아도 이 두객체가 같다는 증거?를 못찾았기 때문이다.
태생부터 다른 객체인데 같을리가 없죠. 그러면 어떻게 해야할까?

지문(hashcode값)이 같으면 같다고 해보자.
이제부터 지문이 같은 객체는 같은 객체라고 정의하자.

@Override
public boolean equals(Object obj) {
    return this.hashCode() == obj.hashCode();
 }

이제 hashcode값이 같다면 같은 객체!

그런데 == 과 equals의 비교가 다르다.
이 둘이 어떤 역할을 하는지 알아볼 필요가 있을 것 같다.

== 

동일성 비교로, 같은 인스턴스임을 의미한다.
따라서 

Servant servant1 = new Servant();
Servant servant2 = new Servant();

System.out.println(servant1 == servant2);


이 두개의 인스턴스는 다르기 때문에 false를 리턴한다.

equals

@Override
public boolean equals(Object obj) {
    return this.hashCode() == obj.hashCode();
 }

다른 인스턴스에도 불구하고 가지고 있는 값이 같음을 의미하며 동등성을 의미한다.
해시코드값이 같다면  결과값을 true로 리턴한다.

결국,

System.out.println(servant1.equals(servant2));

의 값은 true가 된다.

toString()

우리는 객체마다 hashcode라는 지문을 받는걸로 알고 있다.
객체는 hashcode를 이용해서 주소값을 리턴한다.

System.out.println(servant1);

study.whiteship.homework6.finals.Servant@64

그런데 제가 정의한 hashcode와 값이 다르다.
저는 100을 정의했는데...

어떻게 만들었길래 100이 64가 되었을까?

public String toString() {
  return getClass().getName() + "@" + Integer.toHexString(hashCode());
}

Hex값으로 변경되는 것 같다.

Hex는 16진수입니다. 따라서 100을 16인수로 만들어 보겠다.
바로 64가 나온다.

이러한 규칙으로 만들게 되는데 이것을 "주소 값"이라고 부른다.

그렇다면, 우리는 이 메소드를 이용해서 무엇을 할 수 있을까?
이름에서 알 수 있다시피, String으로... 이다.
즉, 문자열로 만들어진다는 것을 알 수 있다,

그렇다는 이야기는 이 메소드를 오버라이딩해서 원하는 문자열로 바꿀 수 있다는 뜻이다.
바꿔보자.

@Override
public String toString() {
   return "열심히 공부합시다.";
}
System.out.println(servant1);

열심히 공부합시다.

 

이렇게 Object 메서드에 대해 학습했다.

hasecode - 객체의 지문을 정의할 수 있다.

- 지문이 같다고 해서 무조건 같은것은 아니다.
equals - 객체의 동등성을 비교한다.

- == 와 다른 메커니즘을 같는다.
toString - 주소값을 리턴한다.

- 새로운 문자열로 교체한다.

 

+) 더블 디스패치

이 방식은 디스패치가 2번일어나는 방식이다.

그렇기 위해서는 인터페이스가 2개 필요한가? (1개로도 가능할지런지 모르겠지만 제가 아는 선에서는 2개가 필요할 것 같다.)
그래야 다이나믹 디스패치가 2번일어날 수 있기 때문이다..

예제가 생각안나서 토비님 예제를 가져와서 설명하겠습니다. (45:41:~1:19:20)

 

게시글이 작성되는 post Interfsce
SNS 종류를 뜻하는 sns Interface

public interface Post {
}

public interface Sns {
}

게시글은 sns에서 작성되어지는데, 글로 작성하는 방법과 그림을 넣는 방법이 존재한다.

public interface Post {
    void postOn(Sns sns);
}

즉, Sns중 하나를 골라서 작성된다는 뜻!...

아, 글로 작성하는 방법과 그림을 넣는 방법을 빼먹었다..ㅎㅎ;

public class Text implements Post{
    @Override
    public void post(Sns sns) {
        
    }
}

public class Picture implements Post{
    @Override
    public void post(Sns sns) {

    }
}

이제 sns클래스를 만들어 봅자.
어떤 sns가 있을까?

페이스북, 트위터로 하자.

public class Twitter implements Sns{
}

public class FaceBook implements Sns{
}

모든 준비는 끝났다.

자 이제 대충 print를 만들고 실행해보겠다.

public interface Sns {
    void post(Post post);
}

public class Twitter implements Sns{
    @Override
    public void post(Post post) {
        System.out.print(post.getClass().getSimpleName());
    }
}

public class Text implements Post{
    @Override
    public void post(Sns sns) {
        System.out.println("text: "+sns.getClass().getSimpleName());
    }
}

Sns twitter = new Twitter();
twitter.postOn(new Text());

라고 입력했는데...  하나만 뜨는가... 이것은 다이나믹 디스패치가 1번만 일어나기 때문이라 해석된다.
왜냐하면 트위터에서 글로 작성된다고 만 했기 때문에 결과는 Text만 등장하게 되었다.

자 그래서 이렇게 수정했다.

원래는 sns에서 글을 작성하는 형태지만
이번에는 post에서 어떤 sns를 이용하는 형태로 출력해보겠다.

 new Text().post(new Twitter());

post에 있는 print코드를 sns쪽으로 옮겼다.

public class Text implements Post{
    @Override
    public void post(Sns sns) {
        sns.postOn(this);
    }
}

@Override
public void postOn(Post post) {
   System.out.println(this.getClass().getSimpleName()+":"+ post.getClass().getSimpleName());
}

근데 생각해보자.. 
this를 지운다면 어떻게 될까?

놀랍게도 되지 않는것을 확인 할 수 있다.

public class Text implements Post{
    @Override
    public void postOn(Sns sns) {
      
    }
}

이러한 코딩의 장점은 바로 확장성이다.
예를들어 인스타그램을 추가한다고 해보자.

그러면

public class Instagram implements Sns{
    @Override
    public void postOn(Post post) {
        System.out.println(post.getClass().getSimpleName()+"-"+this.getClass().getSimpleName());
    }
}

이거만 추가하고 설정해주면...

new Text().post(new Instagram());
new Picture().post(new Instagram());

놀랍게도 

정상적으로 나오는것을 확인 할 수 있다.

현재 코드들은 수정할것이 많아 보입니다. 수정하고 전체 코드를 올려보겠다.

public interface Post {
    default void post(Sns sns) {
        sns.post(this);
    }
}

public interface Sns {
    default void postOn(Post post) {
        System.out.println(post.getClass().getSimpleName()+"-"+this.getClass().getSimpleName());
    }
}

public class Text implements Post{

}

public class Picture implements Post{

}

public class Twitter implements Sns{


}

public class FaceBook implements Sns{

}

public class Instagram implements Sns{
}

이렇게 수정할 수 있습니다. 페이스북을 실행시켜보겠다.

new Text().post(new FaceBook());
new Picture().post(new FaceBook());

 

놀랍게도 정상적으로 나온다는것을 확인 할 수 있다.

그런데 이게 왜 디스패치가 2번 일어난것일까?

public interface Post {
    default void post(Sns sns) {
        sns.post(this);
    }
}

public interface Sns {
    default void postOn(Post post) {
        System.out.println(post.getClass().getSimpleName()+"-"+this.getClass().getSimpleName());
    }
}

실행 코드를 다시 작성하면,

Post text = new Text();
Sns faceBook = new FaceBook();
text.post(faceBook);

으로 고칠 수 있죠..
처음에는 post에서 디스패치가 발생한다. 그 결과 text라는 결과를 받게 된다.(1번)
이제 메소드를 타고 가면 

public interface Post {
    default void post(Sns sns) {
        sns.postOn(this);
    }
}

가 존재합니다.
그런데 여기에서는 어떤 sns인지 모르는 상태이다.
어떤 sns인지는 모르겠지만... 아무튼 이 포스팅 방식을 따른다. 결국 여기서도 디스패치가 1번더 일어나게 된다..

더블 디스패치는 디자인 패턴에서 비지터 패턴이라고 부른다고 한다.

[토비님의 강의를 보면서 제 나름대로 코드를 수정했습니다.
 1. 토비님께서는 PostOn을 text,picture방식으로 나눠서 코딩하셨지만, 하나로 합칠 수 있을 것 같아, Post로 작성하였습니다.
 2. 상속받은 클래스에 그 내용을 구현하셨지만, 이것도 하나로 합칠 수 있을 것 같아 interface의 default키워드를 이용해 합쳤습니다.]

결과적으로 2번 디스패치가 일어난것 같으니 더블 디스패치라 생각한다.

참고
goodgid.github.io/Java-8-Default-Method/   
www.javacodemonk.com/diamond-problem-of-inheritance-in-java-8-88faf6c9  
www.javatpoint.com/super-keyword   
ko.wikipedia.org/wiki/동적_디스패치   
blog.weirdx.io/post/3113  
www.youtube.com/watch?v=GfYg3imRZsc
do-study.tistory.com/62?category=804294    
www.youtube.com/watch?v=s-tXAHub6vg 45:41:~1:19:20

'프로그래밍 언어 > 자바' 카테고리의 다른 글

패키지  (0) 2020.12.30
Failed to execute goal org.apache.maven.plugins:maven-surefire-plugin:2.22.2:test  (0) 2020.12.29
상속  (1) 2020.12.24
클래스  (0) 2020.12.15
큐 구현(설명 x)  (0) 2020.12.08
스택 구현(설명x)  (0) 2020.12.07

댓글(1)

  • 해커
    2021.08.31 01:07

    인터페이스는 '상속' 보다는'구현'으로 좀 수정해주세요... 용어에서 혼동이 올 수 있기 때문에...

Designed by JB FACTORY