ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [live-study] 6: 상속
    개발/Java 2021. 1. 6. 16:02

    목표

    자바의 상속에 대해 학습하세요.

    학습할 것 (필수)

    • 자바 상속의 특징
    • super 키워드
    • 메소드 오버라이딩
    • 다이나믹 메소드 디스패치 (Dynamic Method Dispatch)
    • 추상 클래스
    • final 키워드
    • Object 클래스

    자바 상속의 특징

    상속이란?

    현실 세계의 상속은 부모가 가진 재산을 자식이 물려받는 것을 의미한다.
    자바에서 상속도 마찬가지로 상위 클래스의 필드와 메소드를 하위 클래스에서 물려받아 사용할 수 있게 해주는 기능을 말한다.

    자바의 상속은 "상속"보다 재사용확장에 더 가깝다.

    상속이라고 설명하면 다음과 같은 가계도를 떠올리기 쉽다.

    http://study.zum.com/book/11918

    자바의 상속은 가계도보단, 분류도를 떠올리는게 적절하다.

    https://velog.io/@hkoo9329/%EC%9E%90%EB%B0%94-extends-implements-%EC%B0%A8%EC%9D%B4

    위의 그림에서 생물은 동물과 식물의 상위 클래스라고 부르고, 동물과 식물은 생물의 하위 클래스라고 부른다.

    상위 클래스는 하위 클래스들의 공통된 특성을 가지고 있다.
    생물을 예를들면 생명이 존재한다, 호흡한다 등이 있을 것이다.

    하위 클래스에서는 상위클래스를 확장함으로써 각각의 하위 클래스를 분류할 수 있는 특성을 가지고 있다.

    즉, 하위클래스 is kind of a 상위클래스가 성립한다.

    ex. Animal(or Plant) is kind of a Life

    자바 상속의 사용

    위에서 자바의 상속은 확장에 가깝다고 했는데, 상속을 구현하는 키워드 역시 extends이다.

    public class Life {
        public void breathe() {
            System.out.println("호~흡~");
        }
    }
    
    public class Plant extends Life {
    }
    public class Animal extends Life {
    }
    
    public class Main() {
        public static void main(String[] args) {
            Life plant = new Plant();
            Life Animal = new Animal();
            plant.breathe(); // 호~흡~
            animal.breathe();  // 호~흡~
        }
    }

    Plant와 Animal이 Life를 상속받는다.
    이 둘은 아무것도 구현하지 않아도 기본적으로 breathe()를 물려받아 가지고 있다.

    자바 상속의 특징

    • 자바는 다중상속을 지원하지 않는다.
    • 만약 다중상속이 지원된다고 하자.
      • 인어공주를 구현하기 위해 사람과 물고기를 상속받았다.
      • 이 경우 호흡한다()를 실행했을 때, 사람처럼 코로 숨을 쉬어야 할까, 물고기 처럼 아가미로 숨을 쉬어야 할까?
      • 이러한 문제를 다이아몬드 문제라고 하는데, 이 때문에 자바에서는 안정성을 위해 다중 상속을 지원하지 않는다.

    인터페이스를 통해 다중 상속을 구현할 수 있는데, 인터페이스는 ~ is able to의 의미에 가깝다.
    즉, 하위클래스는 상위클래스의 분류다라는 의미가 아닌, 구현 클래스가 인터페이스를 구현할 수 있다. 라는 의미다.

    • 최상위 클래스인 java.lang.Object 클래스를 상속한다.
    • 상위 클래스의 생성자는 상속되지 않는다.

    super 키워드

    super키워드는 상위 클래스의 속성이나 메소드를 사용할 때 필요한 키워드이다.
    하위 클래스에서 상위 클래스의 속성이나 메소드를 사용할 땐 super.상위클래스의_속성_또는_메소드() 로 사용한다.

    super()로 상위 클래스의 생성자를 호출할 수도 있으며, 이는 반드시 첫번째 줄에 와야한다.

    • 자식 생성자의 부모 생성자 호출
      • 자식 객체를 생성하면 부모 객체ㅏ 먼저 생성된 뒤 자식 객체가 생성된다.
      • 즉, 자식 생성자의 첫줄에는 super();가 숨어있는 것과 같다.

    메소드 오버라이딩

    오버라이딩이란, 상위 클래스의 메소드를 그대로 사용하지 않고 새로운 내용으로 재정의하는 것을 말한다.
    예를 들면 식물과 동물은 호흡 방법이 다를 것이다.

    public class Life {
        public void breathe() {
            System.out.println("호~흡~");
        }
    }
    
    public class Plant extends Life {
        @Override
        public void breathe() {
            System.out.println("어딘가에 있을 숨구멍으로 쓰읍 하아");
        }
    }
    public class Animal extends Life {
        @Override
        public void breathe() {
            System.out.println("콧구멍이나 아가미등으로 움~ 파아~");
        }
    }

    상위 클래스를 재정의 하는 것이므로 메서드 시그니쳐가 동일해야 한다. (리턴 타입, 파라미터 타입, 메소드 이름 등)

    @Override

    • 컴파일 시점에 오버라이드가 되었는지 확인해주는 어노테이션이다.
      • @Override를 붙이고 오버라이드 하지 않으면 컴파일 에러가 발생한다.
    • 써주지 않아도 기능에는 상관없다
    • 그러나 명시해주는 것이 가독성을 향상시켜주고 실수를 줄일 수 있는 길이다. 붙이는게 좋다.

    메소드 디스패치 (Dynamic Method Dispatch)

    객체지향에서는 객체들간의 메세지 전송을 통해 협력을 만들어간다.
    메세지 전송은 곧 메소드의 호출을 의미하고, 어떤 메소드를 호출할지 결정하여 실제로 실행하는 과정을 메소드 디스패치라고 한다.
    정적 메소드 디스패치, 다이나믹 메소드 디스패치, 더블 메소드 디스패치가 존재한다.

    정적 메소드 디스패치 (Static Method Dispatch)

    정의만으로 컴파일 시점에 컴파일러가 어떤 메소드를 실행시켜야 할지 알 수 있는 경우를 말한다.

    class Tiger {
        public void growl() {
            System.out.println("으를렁");
        }
    }
    
    public class StaticMethodDispatch {
        public static void main(String[] args) {
            Tiger tiger = new Tiger();
            tiger.growl(); // 디스패치
        }
    }

    동적 메소드 디스패치 (Dynamic Method Dispatch)

    컴파일 시점에 컴파일러가 어떤 메소드를 실행시켜야 할지 알 수 없어서, 런타임 시점에 결정하는 경우를 말한다.

    interface Life {
        void breathe();
    }
    
    class Animal implements Life {
    
        @Override
        public void breathe() {
            System.out.println("동물호흡");
        }
    }
    
    class Plant implements Life {
    
        @Override
        public void breathe() {
            System.out.println("식물호흡");
        }
    }
    
    public class DynamicMethodDispatch {
        public static void main(String[] args) {
            Life plant = new Plant();
            plant.breathe(); // 디스패치
        }
    }

    런타임 전에는 객체 생성이 되지 않기 때문에 컴파일 시점에 plant 변수가 Plant 타입임을 알 수 없다.

    객체는 런타임에 호출된다.

    더블 메소드 디스패치 (Double Method Dispatch)

    Dynamic Method Dispatch가 두번 일어나는 것을 말한다.

    다음 코드는 모든 post를 모든 sns에 올리는 코드이다. (더블 메소드 디스패치 아님!)

    public class Dispatch {
        interface Post {
            void postOn(SNS sns);
        }
    
        static class Text implements Post {
    
            @Override
            public void postOn(SNS sns) {
                if (sns instanceof Facebook) {
                    System.out.println("text -> Facebook");
                }
                if (sns instanceof Instagram) {
                    System.out.println("text -> Instagram");
                }
                else throw new IllegalArgumentException();
            }
    
        }
    
        static class Picture implements Post {
    
            @Override
            public void postOn(SNS sns) {
                if (sns instanceof Facebook) {
                    System.out.println("picture -> Facebook");
                }
                if (sns instanceof Instagram) {
                    System.out.println("picture -> Instagram");
                }
                else throw new IllegalArgumentException();
            }
        }
    
        interface SNS {
        }
    
        static class Facebook implements SNS {
    
        }
    
        static class Instagram implements SNS {
        }
    
    
        public static void main(String[] args) {
            List<Post> post = Arrays.asList(new Picture(), new Text());
            List<SNS> sns = Arrays.asList(new Facebook(), new Instagram());
    
            post.forEach(p -> sns.forEach(s -> p.postOn(s)));
        }
    }
    1. p.postOn(s)에서 Dynamic Method Dispatch가 발생한다.
    2. Picture or Text 클래스 내부의 if문에서 매개값인 sns의 타입에 따라 로직을 처리한다.

    위 코드의 문제점

    • if문을 쓰게 되면, 새로운 SNS 구현 클래스가 생겼을 때 어떻게 될까?
      • Text, Picture 클래스에 if문이 점점 늘어난다. 이것을 깜빡할 가능성이 높다.
        • if (sns instanceof Twitter) ....
        • if문 작성을 깜빡하면 로직에 심각한 장애 발생한다.
      • 만약 Post의 구현 클래스가 Text, Picture 뿐만 아니라 더 다양하다면?
        • if문이 기하급수적으로 늘어난다.
      • SOLID의 OCP 위반이기도 하다.
    • instanceof는 안티 패턴으로 지적된다
    • 객체지향답게 바꿔야한다.

    if문을 사용하지 않기 위해 다음과 같이 바꿔본다.

    public class DoubleDispatch { 
        interface Post { 
            void postOn(Facebook sns); 
            void postOn(Instagram sns); 
        }
    
      static class Text implements Post {
          @Override
          public void postOn(Facebook sns) {
    
          }
    
          @Override
          public void postOn(Instagram sns) {
    
          }
      }
    
      static class Picture implements Post {
    
          @Override
          public void postOn(Facebook sns) {
    
          }
    
          @Override
          public void postOn(Instagram sns) {
    
          }
      }
      ... 이하 생략
    • post.forEach(p -> sns.forEach(s -> p.postOn(s))); 에서 컴파일 에러가 난다.
    • post.forEach(p -> sns.forEach((SNS)s -> p.postOn(s)));와 같다.
      • 파라미터로 타입을 결정하려 했기 때문에 에러가 발생한다.
      • 즉, 파라미터는 다이나믹 디스패치의 조건이 되지 않는데, postOn()의 파라미터를 동적으로 주었기 때문이다.
        • 파라미터 타입을 동적으로 결정하려면 스태틱 디스패칭이 되어야 한다. (p.postOn()에서 p의 타입이 Text나 Picture처럼 명확해야 함)

    더블 메소드 디스패치로 개선해보자.

    public class DoubleDispatch {
        interface Post {
            void postOn(SNS sns);
        }
    
        static class Text implements Post {
    
            @Override
            public void postOn(SNS sns) {
                sns.post(this); // 두번째 다이나믹 메소드 디스패치
            }
    
        }
    
        static class Picture implements Post {
    
            @Override
            public void postOn(SNS sns) {
                sns.post(this); // 두번째 다이나믹 메소드 디스패치
            }
        }
    
        interface SNS {
            void post(Text post);
            void post(Picture post);
        }
    
        static class Facebook implements SNS {
    
            @Override
            public void post(Text text) {
                System.out.println("text -> Facebook");
            }
    
            @Override
            public void post(Picture picture) {
                System.out.println("picture -> Facebook");
            }
        }
    
        static class Instagram implements SNS {
            @Override
            public void post(Text text) {
                System.out.println("text -> Instagram");
            }
    
            @Override
            public void post(Picture picture) {
                System.out.println("picture -> Instagram");
            }
        }
    
    
        public static void main(String[] args) {
            List<Post> post = Arrays.asList(new Picture(), new Text());
            List<SNS> sns = Arrays.asList(new Facebook(), new Instagram());
    
            post.forEach(p -> sns.forEach(s -> p.postOn(s))); // 첫번째 다이나믹 메소드 디스패치
        }
    }
    

    비즈니스 로직을 Post -> SNS로 옮겨준 것 뿐인데, 에러가 나지 않는 이유

    • s -> p.postOn(s);
      • postOn(SNS s)의 파라미터가 SNS로 일치하므로 Post 클래스에 대한 다이나믹 디스패칭이 일어난다. 문제 없음
    • public void postOn(SNS sns) { sns.post(this);}
      • 파라미터로 넘어온 sns를 메소드 호출의 리시버로 만듦.
      • sns에 대해서 다이나믹 디스패칭이 일어난다. 문제없음
    • 다이나믹 메소드 디스패치가 두번 일어났다. --> 더블 메소드 디스패치

    자바는 싱글 디스패치 언어다.
    = 리시버가 하나뿐이다. (싱글 리시버 구조)
    = 어떤 메소드를 고를까 결정하는데 사용되는 조건이 하나. (리시버 파라미터)
    파라미터는 컴파일 시점에 결정이 되어있어야만 한다.

    더블 메소드 디스패치의 장점

    • 새로운 SNS 타입이 추가되어도 Post 관련 코드에 손댈 필요가 없다.

      • 즉, 새로 추가하는 것이 자유롭다.
        • 의존하고 있는 코드에 직접적인 영향을 주지 않는다.
      • => 객체지향스럽다.
    • Visitor 패턴의 원조라고 할 수 있다. 참고

    추상 클래스

    • 추상화는 어떤 객체들이 가진 특성중 공통된 특성을 추출해서 모델링하는 것이다.
    • 자바에서는 클래스의 공통적인 특성을 추출해 선언한 클래스가 추상 클래스이다.
    • 객체를 직접 생성할 수 없다. (new 키워드 사용 불가)

    추상 클래스의 목적

    • 실체 클래스를 설계하는 사람이 여러명일 경우 필드와 메소드의 이름을 통일하기 위해 사용한다.
    • 실체 클래스 작성시에 구조를 확정지어 실수와 혼란을 방지해서 시간을 단축한다.

    추상 메소드와 오버라이딩

    • 여러명이 실체 클래스 작성시 메소드의 이름을 통일하기 위해 사용한다고 하였는데, 그래서 추상 메소드로 선언부만 작성한다.
    • 상속받은 하위 클래스에서는 선언부를 오버라이딩 하여 반드시 코드를 작성해야 한다.
      • 오버라이딩하지 않으면 컴파일 에러가 발생한다.

    구현

    추상 클래스와 추상 메소드 모두 abstract 키워드만 붙여주면 된다.

    public abstract class ExampleClass {
        public abstract void ExampleMethod();
    }
    
    public class TestSubClass extends ExampleClass {
        @Override
        public void ExampleMethod() {
            // ...
        }
    }

    final 키워드

    final 키워드는 클래스나 메소드에 선언하면 상속과 관련이 있다.

    • final 클래스는 최종적인 클래스가 되어 상속할 수 없는 클래스가 된다.
      • public final class ExampleClass { ... }
      • 즉 상위 클래스가 될 수 없고, 하위 클래스를 만들 수 없다.
    • final 메소드는 최종적인 메소드가 되어 오버라이딩 할 수 없는 메소드가 된다.
      • public final void exampleMethod() {...}

    final을 변수에 선언할 수도 있는데, 이 경우 변수를 초기화한 이후 수정이 불가능하다.

    Object 클래스

    • java.lang에 포함된 클래스로, 자바의 최상위 부모 클래스에 해당한다.
    • 즉 모든 자바 클래스는 java.lang.Object를 상속하고 있는 하위 클래스다.
    • 그러므로 Object 클래스의 모든 메소드와 변수는 다른 클래스에서도 사용할 수 있다.

    Object 클래스의 메소드

    equals()

    • 두 객체를 동등 비교할때 사용한다.
    • 논리적으로 동등하면 true를 리턴하는데, 논리적으로 동등하다는 것은 객체가 저장하고 있는 데이터가 동일한 것을 의미한다.
    • Object의 equals()를 그대로 사용하지 않고 하위 클래스에서 오버라이드하여 사용한다.
    • 예를 들어 String 객체의 경우, 객체의 주소를 비교하는게 아니라 객체의 문자열이 동등한지 확인한다.
      • String 클래스에서 euqals()를 재정의했기 때문에 가능한 일이다.
    public class Dollar {
        int amount;
    
        public Dollar(int amount) {
            this.amount = amount;
        }
    
        @Override
        public boolean equals(Object object) {
            if (object instanceof Dollar) {
                if (amount == ((Dollar) object).amount) {
                    return true;
                }
            }
            return false;
        }
    }
    class DollarTest {
    
        @Test
        void equalsTest() {
            assertTrue(new Dollar(5).equals(new Dollar(5))); 
            assertFalse(new Dollar(5).equals(new Dollar(15))); 
        }
    }

    hashCode()

    • 객체 해시코드는 객체를 식별할 하나의 정수값을 말한다.
    • hashCode() 메소드는 객체의 메모리 번지수를 이용해서 해시코드를 만들기 때문에 모든 객체마다 리턴값이 다르다.
    • 논리적 동등 비교시에 주로 사용된다.
      • HashSet, HashMap, Hashtable에서 사용된다.
      • 이 셋은 hashCode() 리턴값을 먼저 비교하고 참인 경우에 equals() 리턴값을 비교한다.

    equals() 예제의 Dollar를 key로 하여 HashMap에 넣고 다시 꺼내려고 하면 null이 나온다. 해시코드가 다르기 때문이다.

         @Test
        void hashCodeTest() {
            HashMap<Dollar, String> hashMap = new HashMap<Dollar, String>();
    
            hashMap.put(new Dollar(5), "숭잼");
    
            String value = hashMap.get(new Dollar(5));
    
            assertEquals("숭잼", value); // 실패.
        }

    HashMap에서 동일한 가치의 Dollar를 꺼내기 위해선 hashCode()를 오버라이딩 해야한다.

    ....
    public class Dollar {
        @Override
        public int hashCode() {
            return amount;
        }
    }

    같은 가치의 Dollar를 꺼내기 위해선 해시코드를 Dollar의 가치로 만들면 된다.

    결론 : 동일한 해시코드를 리턴하게 만들어서 실제로는 서로 다른 객체이지만, 같은 5달러이므로 HashMap의 key로 사용하게 되었다.

    toString()

    • 객체의 문자 정보를 리턴한다.
    • 오버라이딩 하지 않으면 "클래스명@{16진수 해시코드}" 를 리턴한다.
    • 오버라이딩하여 유의미한 정보를 리턴하도록 만들면 된다.
    public class Dollar {
        ...
        @Override
        public String toString() {
            return String.format("%d달러", amount);
        }
    }
    ...
        @Test
        void toStringTest() {
            assertEquals("5달러", new Dollar(5).toString());
        }

    clone()

    • 원본 객체의 필드값과 동일한 값을 가지는 새로운 객체를 생성하기 위해 사용한다.

    • 원본 객체를 보호하기 위해 사용된다.

      • 원본 객체를 넘겨주어 다른 영역에서 사용할 경우 원본 값이 훼손될 수 있다.
    • Cloneable 인터페이스를 구현해야 사용할 수 있다.

      • 메소드는 없지만, 복제한다는 것을 명시하기 위해 사용한다.
      • 구현하지 않으면 clone()메소드 호출시 CloneNotSupportedException 예외가 발생한다.
      • 예외처리가 필요하기 때문에 try - catch문 안에서 사용해야 한다.
    • 얕은 복제 (thin clone)

      • clone()의 기본은 얕은 복제이다.

      • 얕은 복제는 단순히 필드값을 복제한다.

        • 기본형은 값을 복사하고, 참조형은 객체의 번지수를 복사한다.

          public class Dollar implements Cloneable {
          int amount;
          ...
          public Dollar getDollar() {
          try {
            return (Dollar) clone();
          } catch (CloneNotSupportedException e) {
            return null;
          }
          }
          }
    ...
        @Test
        void cloneTest() {
            Dollar five = new Dollar(5);
            assertEquals(five.amount, five.getDollar().amount);
        }
    • 깊은 복제 (deep clone)
      • 얕은 복제와 달리 참조형 객체까지 새로 복제한다.
      • 직접 오버라이딩하여 구현해야 한다.
    public class Dollar implements Cloneable {
        String owner;
        int amount;
    
        public Dollar(String owner, int amount) {
            this.owner = owner;
            this.amount = amount;
        }
    
        public Object clone() throws CloneNotSupportedException {
            Dollar cloned = (Dollar) super.clone();
            cloned.owner = this.owner.toString();
            return (Object) cloned;
        }
    
        public Dollar copyDollar() {
            try {
                return (Dollar) clone();
            } catch (CloneNotSupportedException e) {
                return null;
            }
        }
    }
     @Test
        void cloneTest() {
            Dollar five = new Dollar("soongjamm", 5);
    
            // 깊은 복제
            Dollar copiedFive = five.copyDollar();
            copiedFive.owner = "new owner";
            assertNotEquals(five.owner, copiedFive.owner); // 통과
        }
    • 얕은 복제를 했다면 String 타입은 참조 객체가 되기때문에 copiedFive의 owner를 수정했을 때, 원본인 five의 owner도 바뀌었겠지만,
    • 깊은 복제를 했기 때문에 copiedFive의 owner역시 값만 복사된 새로운 참조 변수가 되어서, 이 값을 변경해도 원본인 five의 owner에 영향이 없다.

    finalize()

    • 힙 영역에 존재하지만 참조하지 않는 객체는 Garbage Collctor에 의해 자동적으로 소멸된다.
    • GC는 수집하기 직전 자동적으로 finalize()를 실행시키는데, 실행내용은 없다.
    • 소멸되기전에 데이터를 저장하거나 마지막으로 사용한 자원(데이터 연결, 파일 등)을 닫고싶다던가 하면 오버라이딩한다.

    GC는 메모리가 부족하거나 CPU가 한가할 때 JVM에 의해 자동적으로 실행된다.
    그렇기 때문에 finalize()가 정확히 언제 실행될지 시점은 알 수 없다.

    참고

    이것이 자바다
    객체지향의 원리와 이해
    https://defacto-standard.tistory.com/413
    https://www.notion.so/e5c33507880b4d098f83a2c4f8f02c04
    토비의 봄 1회 https://www.youtube.com/watch?v=s-tXAHub6vg
    https://ganghee-lee.tistory.com/9

    댓글

Designed by Tistory.