6회차. 상속

KIMA·2023년 1월 23일
0
post-thumbnail

목표

자바의 상속에 대해 학습하기

학습할 것

상속이란?

: 부모 클래스(= 상위 클래스, 기반 클래스)가 자식 클래스(= 하위 클래스, 파생 클래스)에게 자신의 멤버를 물려주는 행위이다.

  • 자식 클래스는 부모 클래스로부터 물려받은 멤버에 더해 자신만의 새로운 멤버를 추가할 수 있다.
  • 장점 : 기존의 클래스를 재사용하여 새로운 클래스를 작성하면, 유지보수가 용이하다.

💡 여기서 멤버란 필드와 메서드를 의미한다. 생성자와 초기화 블록은 자식으로 상속되어 호출되지 않고 부모의 것을 호출한다.

자바 상속의 특징

  • extends 키워드를 사용하여 상속한다.
    class 자식클래스명 extends 부모 클래스 {...}
  • 다중 상속이 불가능하다.
    • 다증 상속시, 컴파일 에러(Class cannot extend multiple classes)가 발생한다.
  • 부모 메소드를 자식 클래스에서 오버라이딩 가능하다.
    : 부모 클래스의 모든 메소드가 자식 클래스에 맞게 설계될 수 없기 때문에 자식 클래스에서 부모 메소드를 수정해서 사용할 수 있다.
  • 부모의 private 멤버는 상속이 불가능하다.
    : private 접근 제어자는 오직 선언된 클래스 내부에서만 접근이 가능하다.
  • final 클래스는 상속할 수 없고, final 메소드는 오버라이딩이 불가능하다.
    : final 키워드는 수정이 불가능함을 의미하므로, 수정이 가능한 상속과 오버라이딩이 불가능하다.
  • 모든 클래스는 java.lang.Object의 자식/자손 클래스이다.
    : 자바의 최상위 클래스는 java.lang.Object이며, 클래스 선언시 다른 클래스를 상속받지 않으면 암시적으로 java.lang.Object를 상속받는다.

super 키워드

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

  • 상속받은 멤버와 자신의 멤버의 이름이 같을 때 구분을 위해 super 키워드를 붙인다.
  • super는 부모 객체를 참조하므로 static 메소드안에서 사용할 수 없다.

super()

: 부모 클래스의 생성자를 호출하는 코드이다.

  • 자식 클래스의 생성자 블록 최상단에서 자신의 다른 생성자나 부모 클래스의 생성자를 호출하지 않으면 super()가 암묵적으로 호출된다.
    : 자식 객체를 생성하면 부모 객체가 먼저 생성되고 자식 객체는 그 다음에 생성되기 때문이다.
    • 따라서, 부모 클래스는 기본 생성자가 반드시 존재해야함을 기억하자.

🤔 암묵적으로 호출된다?
: 컴파일 시점에 자동으로 코드가 추가되는 것을 의미한다.

메소드 오버라이딩

: 부모 클래스로부터 상속받은 메소드의 내용을 재정의하는 것

  • 방법 : 자식 클래스에서 오버라이딩할 메소드위에 @Override를 선언한다.

@Override의 역할

  • 첫째, 해당 메소드가 부모의 메소드와 선언부가 완전히 일치한지 확인해주는 역할을 한다.

    💡 메소드 선언부
    : 이름, 매개변수, 반환타입

  • 둘째, 접근 제어자와 예외는 다음의 조건하에서만 다르게 변경할 수 있다.
    • 접근 제어자는 부모 클래스의 메소드보다 좁은 범위로 변경할 수 없다.
      • 대부분의 경우는 같은 범위의 접근 제어자를 사용한다.
    • 부모 클래스의 메소드보다 많은 수의 예외를 선언할 수 없다.
      • 부모 클래스의 메소드보다 예외 개수가 같거나 적더라도 더 큰 범위의 Exception을 선언한다면 허용되지 않는다.
        • 예) 부모 : IOException, 자식 : Exception ❌
    • 인스턴스 메서드를 static 메서드로 또는 그 반대로 변경할 수 없다.

예제

// 부모 클래스 : 2차원 좌표
class Point {
  int x;
  int y;

  String getLocation() {
    return "x: " + x + ", y: " + y;
  }
}

// 자식 클래스 : 3차원 좌표
class Point3D extends Point {
  int z;

  @Override
  String getLocation() {
    return "x: " + x + ", y: " + y + ", z: " + z;
  }
}

🤔 부모 클래스에 정의된 static 메소드를 자식 클래스에서 똑같은 이름의 static 메소드로 만드는 것을 오버라이딩이라고 볼 수 있을까?

  • 메소드를 오버라이딩하는 목적은 런타임 다형성이다.
  • 따라서 위의 질문은 각 클래스에 별개의 static 메소드를 같은 이름으로만 정의한 것일 뿐 오버라이딩은 아니고 hiding이라 불린다.
  • 그리고 각 static 메서드는 클래스 이름으로 구별되고 호출할 때는 참조변수.메소드()가 아닌 클래스.메소드()가 바람직하다.

다형성(Polymorphism)

: poly(= many) + morph(= form), 즉 동일한 네이밍을 가지고 여러 형태의 액션을 취하는 기술을 의미한다.

  • 컴파일 시간 다형성은 static 메소드 오버로딩을 통해 실현하고, 런타임 다형성은 인스턴스 메소드 오버라이딩을 통해 실현한다.
    • 런타임 다형성이 일반적으로 말하는 다형성이다.
    • 컴파일 시간 다형성은 스태틱 메소드 디스패치, 런타임 다형성은 다이나믹 메소드 디스패치라고도 불리며 뒤에서 살펴보자.

메소드 디스패치

: 어떤 메소드를 호출할지 결정해서 실제로 실행시키는 과정

스태틱 메소드 디스패치

: 컴파일 시점에서 컴파일러가 어떤 메소드를 호출할지 알고 있는 경우

  • 컴파일 시 생성된 바이트코드에도 정보가 남아있으며 애플리케이션 실행 전에 호출할 메소드 결정
  • 예시
    • 메소드를 오버로딩하면 매개변수 타입과 개수에 따라 어떤 메소드를 호출할지 알 수 있는 경우
    • 상위 클래스가 있더라도 하위 클래스로 선언을 하고 하위 클래스의 인스턴스 생성
abstract class  Animal {
  abstract void speak();
}

class Dog extends Animal {
  @Override
  void speak() {
    System.out.println("wal!");
  }
}

class Cat extends Animal {
  @Override
  void speak() {
    System.out.println("meow!");
  }
}
Dog dog = new Dog();
dog.speak();

다이나믹 메소드 디스패치

: 인터페이스나 추상 클래스에 정의된 추상 메소드를 호출하는 경우에 호출되는 메소드가 런타임 시에 결정되는 것

abstract class  Animal {
  abstract void speak();
}

class Dog extends Animal {
  @Override
  void speak() {
    System.out.println("wal!");
  }
}

class Cat extends Animal {
  @Override
  void speak() {
    System.out.println("meow!");
  }
}
void speakAnimal(Animal animal) {
  animal.speak(); // 어떤 클래스의 speak()가 호출될 지는 런타임시에 생성되는 Animal 구현 객체에 따라 결정된다. 
}
speakAnimal(new Dog());

추상 클래스

추상 클래스

: 추상 메서드를 하나 이상 포함하고 있는 클래스로서, 실체 클래스들의 공통적인 특성(필드, 메소드)를 추출해서 선언한 클래스이다.

  • 추상 메서드 : 선언부만 작성하고 구현부는 상속한 자식 클래스가 구현하도록하는 메서드이다.

  • 문법

    • 추상 클래스 : class 앞에 abstract 키워드를 붙인다.
    • 추상 메서드 : 리턴 타입 앞에 abstract 키워드를 붙이고 구현부는 적지 않고 ;로 끝낸다.
  • 목적

    • 실체 클래스들의 공통 필드와 메소드 이름을 통일하기 위해
    • 실체 클래스의 작성 시간 절약하기 위해
    • 추상 클래스에서 메소드 선언부만 적어두면, 실체 클래스가 없어도 코드를 먼저 작성할 수 있기 때문에 시간이 절약된다.
  • 추상 클래스로는 인스턴스를 생성할 수 없다.

  • 추상 클래스인 부모 클래스의 추상 메서드를 하나라도 구현하지 않는다면, 자식 클래스 역시 추상 클래스로 지정해야한다.

  • 예제

    abstract class  Animal {
      abstract void speak();
      
      void jump() { 
        System.out.println("jump!") 
      };
    }
    
    class Dog extends Animal {
      @Override
      void speak() {
        System.out.println("wal!");
      }
    }
    
    class Cat extends Animal {
      @Override
      void speak() {
        System.out.println("meow!");
      }
    }

final 키워드

: 변경될 수 없다는 의미를 가지고 있다.

  • 사용될 수 있는 곳과 final 의미
    • 클래스 - 상속 ❌
      • 예) String 클래스, Math 클래스
    • 메서드 - 오버라이딩 ❌
    • 변수 - 상수
      • final이 붙은 인스턴스 변수는 생성자에서 초기화할 수 있다.

Object 클래스

: 모든 클래스의 최고 조상 클래스

  • 클래스 선언 시 다른 클래스를 상속받지 않으면 Object 클래스를 암시적으로 상속한다.
  • Object 클래스의 멤버들은 모든 클래스에서 사용 가능하다.
    • 멤버변수는 없고 오직 11개의 메소드만 가지고 있다.
  • 이제부터, Object 클래스가 가진 11개의 메소드에 대해 알아보자.

boolean equals(Object obj)

: 인자로 넘겨받은 객체와 자신이 같은지 비교하는 메소드

  • 기본적으로 구현되어 있는 내용은 인자로 넘겨받은 객체와 자신의 참조변수 값이 같은지 비교한다.

    class Value {
      int value;
      
      Value(int value) {
        this.value = value;
      }
    }
    Value v1 = new Value(10);
    Value v2 = new Value(10);
    
    if(v1.equals(v2)) {
      System.out.println("v1과 v2는 같습니다.");
    } else {
      System.out.println("v1과 v2는 다릅니다.");
    }
    
    v1 = v2;
    
    if(v1.equals(v2)) {
      System.out.println("v1과 v2는 같습니다.");
    } else {
      System.out.println("v1과 v2는 다릅니다.");
    }

    v1과 v2는 다릅니다.
    v1과 v2는 같습니다.
  • 두 객체의 주소값이 아닌 객체가 가진 필드의 값을 비교하고 싶을 땐 equals()를 오버라이딩하여 사용한다.

    class Value {
      int value;
      
      Value(int value) {
        this.value = value;
      }
      
      public boolean equlas(Object obj) {
        if(obj instanceof Value) {
          return id == ((Value)obj).id;
        } else {
          return false;
        }
      }
    }
    Value v1 = new Value(10);
    Value v2 = new Value(10);
    
    if(v1 == v2) {
      System.out.println("v1과 v2는 같습니다.");
    } else {
      System.out.println("v1과 v2는 다릅니다.");
    }
    
    if(v1.equals(v2)) {
      System.out.println("v1과 v2는 같습니다.");
    } else {
      System.out.println("v1과 v2는 다릅니다.");
    }
    v1과 v2는 다릅니다.
    v1과 v2는 같습니다.

hashCode()

: 해싱 기법에 사용되는 해시함수를 구현한 것이다.

  • 해싱 : 데이터 관리 기법중 하나로, 찾고자 하는 값(key)을 해시 함수에 대입하여 값이 저장된 주소(= hash code, value)를 얻어 바로 값에 접근할 수 있는 기술이다.

    • 다량의 데이터를 저장하고 검색하는 데 유용하다.
  • 기본적으로 구현된 hashCode()는 객체의 주소값으로 해시코드를 만들어 반환한다.

    • 해싱기법을 사용하는 HashMap이나 HashSet과 같은 클래스는 두 객체를 비교하기 위해 먼저 hashCode()로 리턴된 해시코드로 비교하고 다르면 다른 객체, 같으면 equals()로 다시 비교한다.

    • 따라서, 클래스의 인스턴스 변수 값으로 두 객체의 같고 다름을 판단해야하는 경우에 같은 객체라면 hashCode()를 호출했을 때 결과값인 해시코드도 같아야하므로, equals()뿐만 아니라 hashCode()도 적절히 오버라이딩 해야한다.

      String str1 = new String("abc");
      String str2 = new String("abc");
      
      // String 클래스는 같은 값을 가진 객체가 같은 해시 코드를 갖도록 hashCode()를 오버라이딩 한 상태이다.
      System.out.println(str1.hashCode());
      System.out.println(str2.hashCode());
      
      // identityHashCode는 객체의 주소 값으로 해시 코드를 생성한다.
      System.out.println(System.identityHashCode(str1)); 
      System.out.println(System.identityHashCode(str2));
      96354
      96354
      
      999966131
      1989780873

toString()

: 인스턴스에 대한 정보를 문자열로 제공한다.

  • 기본적으로 Object 클래스의 toString()에 구현된 내용은 다음과 같다.

    public String toString() {
      return getClass().getName() + "@" + Integer.toHexString(hashCode());
    }
    • 16진수의 해시코드를 출력한다.
  • 인스턴스에 대한 정보는 대부분의 경우 인스턴스 변수에 저장된 값들을 문자열로 표현한다는 뜻이다.

    • 따라서, 클래스를 정의할 때 toString()을 오버라이딩하여 인스턴스 변수에 저장된 값들을 출력하도록 재정의해야한다.
    • String 클래스의 toString()String 인스턴스가 갖고 있는 문자열을 반환하도록 오버라이딩 되어있다.
    • Date 클래스의 toString()Date 인스턴스가 갖고 있는 날짜와 시간을 반환하도록 오버라이딩 되어있다.
    String str = new String("KOREA");
    java.util.Date today = new java.util.Date();
    
    // PrintStream 클래스의 출력 메소드(print, println 등)로 객체를 출력하면 컴파일러가 내부적으로 toString() 메소드를 호출한다.
    System.out.println(str); // 내부적으로 str.toString()이 호출된다.
    System.out.println(today); // 내부적으로 today.toString()이 호출된다.
    KOREA
    Tue Jan 24 11:01:32 KST 2023

clone()

: 자신을 복제하여 새로운 인스턴스를 생성한다.

  • 목적 : 원래의 인스턴스는 보존하고 clone()을 이용해서 새로운 인스턴스를 생성하여 작업을 하면 작업 이전의 값이 보존되므로, 작업에 실패한다면 원래의 상태로 되돌릴 수 있다.

  • 기본적으로 Object 클래스의 clone()은 단순히 인스턴스 변수의 값만 복사한다. 즉, 얕은 복사를 한다.

    protected Object clone() throws CloneNotSupportedException;
    • 따라서, 인스턴스 변수가 레퍼런스 타입이라면 복제된 인스턴스에서 수정할 경우 원래의 인스턴스도 같이 수정된다.
      • 예를 들어, 배열의 경우 복제된 인스턴스도 같은 배열의 주소를 가지고 있기 때문에 복제된 인스턴스 작업이 원래의 인스턴스에 영향을 미치게 된다.
  • 깊은 복사를 위해서는 clone()을 오버라이딩해야한다.

    • 배열, ArrayList, LinkedList, HashSet, TreeSet, HashMap, TreeMap, Calendar, Date와 같은 클래스들은 clone()을 오버라이딩하여 깊은 복사를 지원한다.
    • clone()을 오버라이딩하면서 접근제어자를 protected에서 public으로 변경해야만 상속관계가 없는 다른 클래스에서도 clone()을 호출할 수 있다.
  • clone()을 호출하기위해서는 클래스가 Cloneable인터페이스를 반드시 구현해야 한다.

    • 만약, 구현 하지 않고 clone()을 호출하면 CloneNotSupportedException 예외가 발생한다.
    • 인스턴스의 데이터를 보호하기위한 용도로 Cloneable인터페이스가 구현되어 있다는 것은 클래스 작성자가 복제를 허용한다는 뜻이다.
    • Cloneable인터페이스는 멤버가 존재하지 않는 비어있는 인터페이스이다.
  • 예제

    class Circle implements Cloneable {
      Point p;
      double r;
      
      Circle(Point p, double r) {
        this.p = p;
        this.r = r;
      }
    
      @Override
      public Circle clone() {
        Object obj = null;
        try {
          obj = super.clone();
        } catch(CloneNotSupportedException e) {}
        
        Circle c = (Circle) obj;
        c.p = new Point(this.p.x, this.p.y);
        
        return c;
      }
    }
    • clone()의 반환 타입이 부모인 Object 클래스의 clone() 반환 타입인 Object가 아닌 이유는 공변 반환 타입(covariant return type)을 지원하기 때문이다.

    🤔 공변 반환 타입(covariant return type)이란?
    : 오버라이딩할 때 조상 메서드의 반환타입을 자손 클래스의 타입으로 변경을 허용하는 것

    • 장점 : 번거로운 형변환이 줄어든다.
    • jdk 1.5버전부터 지원한다.

getClass()

: 자신이 속한 클래스의 Class 객체를 반환하는 메서드

💡 Class 객체
: 클래스의 모든 정보(클래스에 정의된 멤버의 이름이나 개수 등)를 담고 있다.

  • Class 객체로 객체를 생성하고 메서드를 호출하는 등 보다 동적인 코드를 작성할 수 있다.
    • 자세한 내용은 Reflection API를 검색해보자.
  • 클래스 당 1개만 존재한다.
  • 클래스 파일이 클래스 로더에 의해서 메모리에 올라갈 때, 자동으로 생성된다.
    • 기존에 생성된 Class 객체가 메모리에 존재하는지 확인하고, 있으면 Class 객체의 참조를 반환하고 없으면 클래스 패스에 지정된 경로를 따라서 클래스 파일을 찾아 Class 객체로 변환한다.
  • 클래스의 정보가 필요할 때, Class 객체에 대한 참조를 얻는 방법
    • 첫째, 생성된 객체로부터 얻는다.
      Class<클래스명> classObject = new 클래스명().getClass();
    • 둘째, 클래스 리터럴(*.class)로부터 얻는다.
      Class<클래스명> classObject = 클래스명.class;
    • 셋째, 클래스 이름으로부터 얻는다.
      Class<클래스명> classObject = Class.forName(클래스명);
      • 특정 클래스 파일(예> 데이터베이스 드라이버)를 메모리에 올릴 때 사용한다.

Reference

profile
안녕하세요.

0개의 댓글