6주차 : 상속

Joo·2023년 4월 14일
post-thumbnail

1. 상속

부모 클래스에서 사용하던 변수와 메소드(public, protected)를 자식 클래스에서 재사용하기 위해 사용

  • 자식 클래스의 객체를 생성하면 자동으로 부모의 생성자가 먼저 호출되고 자식의 생성자가 호출됨
    • 모든 클래스의 부모 클래스는 Object 클래스
    • 어떤 클래스의 객체를 생성하든 Object 클래스의 생성자가 호출된다. (순서는 다를 수 있음)
      • Child class → Parent class → Object class
  • 다중 상속 불가능
  • 목적
    • 코드를 재사용하여 중복된 코드를 작성하지 않아도 됨
    • 클래스 간 계층 구조를 분류할 수 있음
    • 유지보수가 쉬워짐
      • 부모 클래스에 필드를 추가하면 모든 자식 클래스에서도 필드가 추가됨

2. super 키워드

  • 자식 클래스에서 부모 클래스에 접근할 때 사용하는 키워드
    • 부모 클래스의 참조 변수
  • super.필드
    • 부모 클래스의 필드에 접근함
  • super.메소드
    • 부모 클래스의 메소드를 사용함
  • super()
    • 부모 클래스의 생성자를 명시적으로 지정
    • 자식 클래스 객체를 만들 때 자식 클래스 생성자 가장 첫 줄에 'super()'가 만들어짐
    • 따라서 부모 클래스기본 생성자가 없다면 에러가 남
      • 부모 클래스에 기본 생성자가 없는 경우
        1. 부모 클래스 기본 생성자를 만듦

        2. super(...)를 이용해 부모 클래스 생성자를 명시

          public class InheritanceClass {
              
              public static void main(String[] args) {
                  ChildClass child = new ChildClass();
              }
          }
          
          class ParentClass {
              
          //  public ParentClass() {
          //      System.out.println("ParentClass constructor");
          //  }
              public ParentClass(String name) {
                  System.out.println("Parent name is " + name);
              }
          }
          
          class ChildClass extends ParentClass {
              
              ChildClass() {
                  super("HI");        //부모 클래스의 생성자를 명시적으로 지정
                  System.out.println("ChildClass constructor");
              }
          }

          결과

          Parent name is HI
          ChildClass constructor

3. 메소드 오버라이딩

부모 클래스의 메소드와 동일한 메소드 시그니처, 리턴 타입을 갖는 메소드를 자식 클래스에서 재정의하는 것

리턴 타입은 메소드 시그니처에 포함되지 않는다!

💡 Overloading - 확장
→ 메소드 시그니처가 다른 서로 다른 메소드들을 만들어 확장함 (자식>부모)
Overriding - 덮어 씀
→ 메소드 시그니처가 같은 메소드를 만들어 부모 클래스의 기능은 무시하고 덮어 씀

3.1 접근제어자

  • 접근 제어자는 달라도 되지만 자식의 접근 제어자 범위가 더 커야함
    • 부모 - default

    • 자식 - default 이상 (public, protected, default)
      - 부모가 공개한다는데 자식이 더 좁은 범위만 보여준다고? 안됨!

      cf) 부모 클래스의 메소드가 private
      - 자식 클래스의 메소드는 어떤 접근 제어자를 써도 상관 없음
      - private이 가장 작은 범위의 접근 제어자이기 때문

3.2 예외 처리 시 주의사항

(1) 부모 클래스 메소드가 예외를 던지지 않는 경우

  • 오버라이딩 시 checked exception을 던질 수 없음
  • unchecked exception은 가능
  • ex
    package org.example;
    
    public abstract class Parent {
        public void myMethod(String a) {
        }
    }
    package org.example;
    
    public class Child extends Parent {
    //    불가능
    //    @Override
    //    public void myMethod(String a) throws Exception {
    //        super.myMethod(a);
    //    }
    
    //    가능
        @Override
        public void myMethod(String a) throws RuntimeException {
            super.myMethod(a);
        }
    }

(2) 부모 클래스 메소드가 예외를 던지는 경우

  • checked exception을 던지는 경우
    • 오버라이딩 시 더 큰 범위의 예외를 던질 수 없음
    • ex
      • ThirdException → SecondException → FirstException → Exception

        package org.example;
        
        public abstract class Parent {
            public void myMethod(int a) throws SecondException {
                throw new SecondException();
            }
        }
        package org.example;
        
        public class Child extends Parent {
        //  큰 범위 예외 -> 불가능
            @Override
            public void myMethod(int a) throws FirstException {
            }
        
        //  같은 예외 -> 가능
            @Override
            public void myMethod(int a) throws SecondException {
            }
        
        //  작은 범위 예외 -> 가능
            @Override
            public void myMethod(int a) throws ThirdException {
            }
        
        //  예외x -> 가능
            @Override
            public void myMethod(int a) {
            }
        }
  • unchecked exception을 던지는 경우 (= 부모 클래스가 예외를 던지지 않는 경우)
    • 부모 클래스 메소드가 던지는 예외보다 커도 상관없음
    • checked exception은 던질수 없음!
    • ex
      • ThirdException → SecondException → FirstException → RuntimeException

        package org.example;
        
        public abstract class Parent {
            public void myMethod(int a) throws SecondException {
                throw new SecondException();
            }
        }
        package org.example;
        
        public class Child extends Parent {
        //  큰 범위 예외 -> 가능
            @Override
            public void myMethod(int a) throws FirstException {
            }
        
        //  같은 예외 -> 가능
            @Override
            public void myMethod(int a) throws SecondException {
            }
        
        //  작은 범위 예외 -> 가능
            @Override
            public void myMethod(int a) throws ThirdException {
            }
        
        //  예외x -> 가능
            @Override
            public void myMethod(int a) {
            }
            
        //  checked 예외 -> 불가능
            @Override
            public void myMethod(int a) throws Exception {
            }
        }

4. 메소드 디스패치

어떤 메소드를 실행할 지 결정하고 실제로 실행시키는 과정

4.1 정적 메소드 디스패치

컴파일 시 어떤 메소드가 실행될 지 결정

class Human {
		int a;

    public void printA() {
        System.out.println("사람 메소드 : " + a);
    }
}

class Parent extends Human {
    @Override
    public void printA() {
        System.out.println("부모 메소드 : " + a);
    }
}

class Child extends Human {
    @Override
    public void printA() {
        System.out.println("자식 메소드 : " + a);
    }
}

public class InheritanceTest {
    public static void main(String[] args) {
        Parent parent = new Parent();
        Child child1 = new Child();

        parent.printA(); // 부모 클래스 타입의 printA() 메소드
        child1.printA(); // 자식 클래스 타입의 printA() 메소드
    }
}

4.2 동적 메소드 디스패치

런타임 시 어떤 메소드가 실행될 지 결정

  • 컴파일 단계에서는 해당 메소드가 어떤 클래스 타입의 메소드인지 모름
  • 인터페이스, 추상 클래스, 메소드 오버라이딩
    • 컴파일 단계에서는 어떤 클래스, 구현체의 메소드인지 모름
    • 런타임 시 메소드의 클래스 타입이 정해져 호출되는 것을 다이나믹 메소드 디스패치라고 함
  • 메소드의 클래스 타입
    • 참조 변수 타입이 아니라 참조할 객체의 타입에 따름
public class InheritanceTest {
    public static void main(String[] args) {
        Human whoIsIt = new Child();

        whoIsIt.printA(); // 컴파일 시 (눈으로 보기에) 어떤 클래스 타입의 메소드인지 알 수 없음
				// 실행해보면 자식 클래스 타입의 printA() 메소드가 호출됨
    }
}

4.3 더블 디스패치

런타임 시 객체매개변수에 따라 호출할 메소드가 결정

방문자 패턴

방문자와 방문자 공간을 분리하여 방문 공간이 방문자를 맞이할 때 이 후에 대한 행동을 방문자에게 위임하는 패턴

  • FoodVisitor
    package org.example.double_dispatch;
    
    /**
     * 방문자 패턴
     * 방문자 구현체(apple, chicken)가 실제 로직을 수행함
     */
    public interface FoodVisitor {
        void printBadOrGood(Man man);
        void printBadOrGood(Woman woman);
    }
    • Apple
      public class Apple implements FoodVisitor {
          @Override
          public void printBadOrGood(Man man) {
              System.out.println("[bad] man eats " + this.getClass().getSimpleName());
          }
      
          @Override
          public void printBadOrGood(Woman woman) {
              System.out.println("[bad] woman eats " + this.getClass().getSimpleName());
          }
      }
    • Chicken
      public class Chicken implements FoodVisitor {
          @Override
          public void printBadOrGood(Man man) {
              System.out.println("[good] man eats " + this.getClass().getSimpleName());
          }
      
          @Override
          public void printBadOrGood(Woman woman) {
              System.out.println("[good] woman eats " + this.getClass().getSimpleName());
          }
      }
  • Human
    public interface Human {
            public void eat(FoodVisitor foodVisitor);
    //    public void eat(Chicken chicken);
    //    public void eat(Apple apple);
    }
    • Man
      public class Man implements Human {
      
      //    @Override
      //    public void eat(Chicken chicken) {
      //        System.out.println("[good] man eats " + chicken.getClass().getName());
      //    }
      //
      //    @Override
      //    public void eat(Apple apple) {
      //        System.out.println("[bad] man eats " + apple.getClass().getName());
      //    }
      
      //     만약 새로운 Food가 추가된다면?
      //     -> Man과 Woman 모두에 새로운 Food를 처리하는 로직을 추가해야 함
      //    @Override
      //    public void eat(FoodVisitor food) {
      //        if (food instanceof Chicken) {
      //            System.out.print("[good] ");
      //        }
      //
      //        if (food instanceof Apple) {
      //            System.out.print("[bad] ");
      //        }
      //
      //        System.out.println("man eats " + food.getClass().getSimpleName());
      //    }
      
          @Override
          public void eat(FoodVisitor foodVisitor) {
              // 이 후 로직은 foodVisitor(visitor)에게 맡김
              // **Food가 추가되어도 Human의 코드는 수정할 필요 없음**
              foodVisitor.printBadOrGood(this);
          }
      }
    • Women
      public class Woman implements Human {
      //    @Override
      //    public void eat(FoodVisitor food) {
      //        if (food instanceof Chicken) {
      //            System.out.print("[good] ");
      //        }
      //
      //        if (food instanceof Apple) {
      //            System.out.print("[bad] ");
      //        }
      //
      //        System.out.println("woman eat " + food.getClass().getSimpleName());
      //    }
      
          @Override
          public void eat(FoodVisitor foodVisitor) {
              foodVisitor.printBadOrGood(this);
          }
      }
  • DoubleDispatchMain
    public class DoubleDispatchMain {
        public static void main(String[] args) {
            Human human = new Man();
            FoodVisitor foodVisitor = new Chicken();
    
            /**
             * 정적 메소드 디스패치가 되면 안됨!
             * Human에서 Food를 받아야 함 -> 동적 디스패치 1
             * eat() 메소드에서 전달받은 food로 이 후 로직 수행해야 함
             * foodVisitor.printBadOrGood(this) -> 동적 디스패치 2
             */
            human.eat(foodVisitor);
        }
    }
    [good] man eats Chicken

5. 인터페이스

실제 코드는 작성하지 않더라도 어떤 메소드들이 있어야 하는지를 정의해 놓은 것

  • 내용(몸통, 중괄호)이 없는 메소드만으로 구성
  • .java 파일이며 컴파일하면 .class가 됨. 하지만 단독으로 쓸 수는 없음
  • 인터페이스에 정의된 필드는 모두 public static final 필드
  • (static이나) final 메소드가 선언되있으면 안 됨
    ⇒ 자바 8부터 static 메소드default 메소드가 추가되었음!
  • 클래스는 인터페이스를 구현(implements)할 수 있음
    • 여러 개의 인터페이스를 구현할 수도 있음
  • 인터페이스가 다른 인터페이스를 상속받을 수 있음 (extends)
    • 이 때는 다중 상속 가능

6. 추상 클래스 (abstract class)

일부 완성되어 있는 클래스

  • 추상 클래스를 상속받은 클래스반드시 추상 메소드를 구현해야 함
  • 추상 클래스를 인스턴스화, 즉 단독으로 객체를 만들 수 없음
    • 추상 클래스를 상속받고 추상 메소드를 구현한 클래스를 인스턴스화 해야함
      • 익명 클래스를 만들어서 객체를 생성할 수도 있음
  • 구현되어 있는 메소드가 있어도 상관 없음
  • static이나 final이 아닌 변수있어도 됨
  • static이나 final 메소드 있어도 됨
public abstract class AbsClass {
    int a;

    public AbsClass(int a) {
        this.a = a;
    }

    public void printA() {
        System.out.println(a);
    }
}

public static void main(String[] args) {

    /**
     * 1. 추상 클래스는 단독으로 인스턴스화 할 수 없음
     * -> 익명 클래스를 만들고 객체를 생성함
     * 2. 추상 클래스에 추상 메소드가 없어도 에러는 안남
     * 3. 추상 메소드가 있다면 그 메소드는 무조건 오버라이딩 해야함
     */
    AbsClass absClass = new AbsClass(10) {
        @Override
        public void printA() {
            super.printA();
        }
    };
}

인터페이스 & 추상 클래스

  • 사용 목적
    • 개발자의 역량에 따른 변수, 메소드 선언의 격차를 줄일 수 있음
    • 공통적인 인터페이스와 추상 클래스를 선언해 놓으면 선언과 구현을 구분할 수 있음
  • 인터페이스를 사용하는 경우
    • 서로 관련이 없는 클래스라도 공통된 행위를 하는 경우
      • 같은 인터페이스를 구현한 클래스는 같은 동작을 할 것으로 예상
  • 추상 클래스를 사용하는 경우
    • 서로 관련이 있는 클래스를 하나의 타입으로 묶고 싶은 경우
    • 공통된 대부분의 필드나 메소드는 구현해놓고 조금씩 다른 부분을 확장하고 싶은 경우
    • 인스턴스 변수를 제공하고 싶은 경우 ↔ 인터페이스 필드는 상수만 가능함

7. final 키워드

  • class
    • public final class FinalClass
    • final 클래스는 더 이상 상속해 줄 수 없음
    • 더 이상 확장하면 안되는 클래스에 사용 (ex. String)
  • method
    • public final void finalMethod
    • 더 이상 오버라이딩 할 수 없음 → 누군가 메소드의 기능을 바꿀 수 없음
  • 변수
    • 바꿀 수 없는 변수 → 상수
    • 선언과 함께 초기화를 해야함
    • 매개 변수 & 지역 변수는 반드시 선언할 때 초기화 할 필요 없음
      • 매개 변수

        • 이미 넘어올 때 초기화 되어서 넘어옴
      • 지역 변수
        - 선언된 중괄호 내에서만 참조되므로

        → 단, 둘 다 메소드 내부에서 다시 값을 할당하면 안 됨

        public void test(final int b) {
                final int a;
                a = 1;
        
                System.out.println(a);
        
                a = 2;  //불가
                b = 1;  //불가
        }
    • 참조 자료형 변수
      • 마찬가지로 final로 선언하면 두 번 이상 값을 할당하거나 생성자를 사용해 초기화할 수 없음
      • 객체의 인스턴스 변수클래스 변수바꿀 수 있음
        • 객체는 final이지만 객체의 필드는 final이 아니기 때문에

8. Object 클래스

8.1 java.lang.Object 클래스

  • 모든 자바 클래스의 부모 클래스

  • 클래스의 기본적인 행동을 정의해 놓은 클래스

  • 필드는 가지지 않으며 11개의 메소드로만 구성

  • 메소드 종류

    • 객체를 처리하기 위한 메소드
      • equals()
        • 메소드를 호출한 객체와 매개변수로 넘겨받은 객체가 같은지 비교
      • getClass()
        • Class 클래스의 객체를 리턴 - 리플렉션, 20장
      • hashCode()
        • 객체의 해시코드 값을 리턴
          • 해시코드
            • 객체의 메모리 주소를 16진수로 표현한 것
      • toString()
        • 객체를 문자열로 표현한 것
    • 쓰레드를 처리하기 위한 메소드
      • notify()
        • 객체의 모니터에 대기하고 있는 단일 쓰레드를 깨움
      • notifyAll()
        • 객체의 모니터에 대기하고 있는 모든 쓰레드를 깨움
      • wait()
        • 다른 쓰레드가 현재 객체에 대한 notify(), notifyAll()을 호출할 때까지 현재 쓰레드가 대기함
        • 매개변수로 밀리초 시간을 받을 수 있음 (1/1000초)

8.2 toString()

  • 객체를 출력하는 메소드
  • '패키지.클래스@해시코드값' 리턴함
    • getClass().getName() + ‘@’ + Integer.toHexString(hashCode())
  • 오버라이딩을 해서 객체를 쉽게 확인할 목적으로 사용
  • 자동 호출되는 경우
    • println() 메소드에 매개변수로 들어가는 경우

    • 객체에 더하기 연산을 하는 경우

      public class ToStringMethod {
      
          public static void main(String[] args) {
              Banana banana = new Banana();
              Apple apple = new Apple();
              System.out.println(banana);
              System.out.println("HI "+apple);
              System.out.println("Hello "+apple.toString());
          }
      }
      
      class Banana {}
      
      class Apple {
      
          public String toString() {
              return "I am Apple";
          }
      }

      결과

      chapter11.Banana@36baf30c
      HI I am Apple
      Hello I am Apple

  • 보통 DTO 클래스를 사용할 때 toString() 메소드를 오버라이딩 해놓는게 좋음
    • 그래야 객체의 내용을 확인하기 쉽기 때문

8.3 equals()

  • 객체가 같은지 다른지 비교하는 메소드
  • == & !=
    • 위 연산자는 기본 자료형에서만 사용해야함
    • 정확히는 참조자료형에 ==, != 사용할 순 있으나 이것은 두 객체의 주소값을 비교하는 것
  • 객체를 비교할 때는 equals() 메소드를 오버라이딩하여 사용해야 함
    • 그냥 사용하면 두 객체의 해시코드값을 리턴하므로 == 비교와 똑같음
    • equals() 메소드를 오버라이딩할 때 hashCode() 메소드도 같이 오버라이딩 해야함
      • equals() 메소드의 결과 true ⇒ hashCode() 메소드 결과 true → hashCode() 메소드도 같은 결과를 갖도록 오버라이딩 해야함 cf) 보통 IDE가 equals() & hashCode()를 자동으로 오버라이딩 해줌
  • 기능 위주 클래스에서는 굳이 오버라이딩할 필요 없음
  • 컬렉션을 사용할 경우 원하는 객체를 찾거나 지우려면 반드시 오버라이딩 해야함
import java.util.Objects;

class Banana {

    int count;

    Banana() {
    }

    Banana(int number) {
        count = number;
    }

		//cmd+N 단축키를 이용해 자동으로 만듦
    @Override
    public String toString() {
        return "Banana{" +
                "count=" + count +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Banana banana = (Banana) o;
        return count == banana.count;
    }

    @Override
    public int hashCode() {
        return Objects.hash(count);
    }
}
public class EqualsMethod {

    public static void main(String[] args) {
        Banana banana1 = new Banana(1);
        Banana banana5 = new Banana(5);
        Banana banana10 = new Banana(1);

        System.out.println(banana1);
        System.out.println(banana5);
        System.out.println(banana10);

        if (banana1.equals(banana5)) {
            System.out.println("same fruit");
        } else {
            System.out.println("different fruit");
        }

        if (banana1.equals(banana10)) {
            System.out.println("same fruit");
        } else {
            System.out.println("different fruit");
        }
    }
}

결과

Banana{count=1}
Banana{count=5}
Banana{count=1}
different fruit
same fruit

8.4 hashCode()

객체의 메모리 주소16진수로 변환하여 리턴하는 메소드

  • 두 객체가 동일하면 hashCode() 값이 똑같아야 함
  • 오버라이딩 규칙
    1. 자바 애플리케이션이 실행되는 동안 hashCode()는 항상 동일한 int 값을 리턴해야 함

    2. equals() 결과가 true인 경우 두 객체의 hashCode() 결과값은 항상 동일해야 함

      • equals() 메소드 오버라이딩 시 hashCode() 메소드도 같이 오버라이딩 해야하는 이유
    3. equals() 결과가 false라고 해서 무조건 hashCode() 결과값이 달라야 하는 것은 아님

      직접 오버라이딩 하는것은 권장하지 않음. IDE가 알아서 잘 해준다.

Reference

[Java] 상속

방문자 패턴

자바 상속

casting object

Seung's Story : 네이버 블로그

더블 디스패치

[Java] 더블 디스패치(Double Dispatch)

더블 디스패치

0개의 댓글