자바 상속

Tina Jeong·2021년 1월 21일
0

Re-자바

목록 보기
9/16

상속


실생활에서의 상속은 사망 후에 집문서, 빚 등의 재산을 넘겨받는 것을 의미한다. 자바에서의 상속은 부모 클래스의 필드, 메소드, 내부 클래스등의 클래스 멤버를 그대로 물려받는 것을 의미한다. 상속의 가장 큰 이유는 코드 재사용성이다. 부모 클래스의 필드, 메소드, 내부 클래스를 자식 클래스에서 재정의할 필요없이 그대로 사용할 수 있다.

지난 포스팅에서 LG 그램을 예시로 들었는데, 노트북은 PC의 일종이므로 공통된 요소를 묶어 부모 클래스로 만들고, LG 그램과 맥북 프로가 PC를 상속받도록 했다.
상속 UML

이제 그램과 맥북 프로는 코드를 재타이핑할 필요 없이 PC의 모든 필드와 메소드를 사용할 수 있으며, 그램의 부가기능인 램 최적화와 맥북의 부가기능인 에어드랍 기능만 추가해서 사용할 수 있게 되었다.

용어 정리

  • 부모 클래스 aka super class, parent class
  • 자식 클래스 aka subclass, derived class, extended class, child class

extends

상속 관계에 대한 명시는 extends 키워드를 이용한다. [자식클래스] extends [부모클래스]의 구조로 사용한다. implements 키워드는 아래 interface 부분 참고.

public class LgGram extends PC {
...
}

super 키워드


super 키워드는 자식 클래스에서 부모 클래스의 필드와 메소드 등을 접근하기 위한 키워드이다.

BigDecimal usedRamPercent;
public LgGram() {
        super.name="default";
        this.usedRamPercent= BigDecimal.ZERO;
}

super.name은 PC 클래스에 선언된 name을 참조하고 있다. 그런 맥락에서 this.name으로 써줘도 무관하지만, 명확히 하려고 super를 써주었다. 사실 지금은 이름이 동일한 지역 변수가 있는 것도 아니고, 부모 멤버 변수 이름과 동일한 자식 멤버 변수(후자는 부모 변수가 자식 클래스에서 숨겨지면서 혼란을 유발하니 비추👎) 가 있는 것도 아니다. 이처럼 변수 구분이 명확한 경우에는 this나 super를 써주지 않아도 된다.

public LgGram() {
        name = "default";
        usedRamPercent = BigDecimal.valueOf(0.0);
}

이때 LgGram()에서 부모 클래스의 no-argument 생성자인 PC()를 호출하지 않는데, 자바 컴파일러가 자동으로 super()를 삽입해서 부모 생성자를 호출한다. 그래서 PC 클래스에는 no-argument 생성자를 반드시 선언해줘야 한다. 반드시 불러서 쓸거니까. 안 그러면 컴파일 에러가 발생한다.

public class PC {
    int numOfFiles;
    boolean flag;
    String name;

    public PC() {
    
    }
...    

또, 생성자는 클래스 멤버는 아니지만 클래스의 이름과 동일한 일종의 메소드이므로 super(numOfFiles, name)로 생성자를 호출할수 있으며, 해당 호출을 통해 멤버변수를 초기화한다.

public LgGram(int numOfFiles, String name) {
        super(numOfFiles, name);
        if (numOfFiles <= 100) usedRamPercent = BigDecimal.valueOf(0.001 * numOfFiles);
        else usedRamPercent = BigDecimal.valueOf(0.2);
}

overriding


hiding

📌 OOP의 information hiding과 완전히 다른 내용이다.

부모 클래스와 자식클래스에 signature(동일한 이름의 메소드, 동일한 파라미터 타입)가 동일한 static 메소드가 있을 경우 자식의 메소드가 숨겨지는데, 이를 hiding이라고 한다. hiding은 객체지향 및 상속과도 거리가 있으므로 비추한다.

public class Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Animal");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Animal");
    }
}

public class Cat extends Animal {
    public static void testClassMethod() {
        System.out.println("The static method in Cat");
    }
    public void testInstanceMethod() {
        System.out.println("The instance method in Cat");
    }

    public static void main(String[] args) {
        Cat myCat = new Cat();
        Animal myAnimal = myCat;
        Animal.testClassMethod();
        myAnimal.testInstanceMethod();
    }
}

결과

The static method in Animal
The instance method in Cat

overloading

오버로딩은 같은 클래스내에서 signature를 다르게 하여 사용하는 것이다. 자바에서는 리턴타입을 오버로딩의 범위에 두지 않는다. 즉, 리턴타입이 다른 오버로딩은 허용하지 않는다.

public class OverloadingTest {
    public void method1() {

    }
    public void method1(int a) {

    }
    public void method1(String a) {

    }
    public void method1(int a, int b) {

    }
    public int method1() { //error
        return 0;
    }
}

overriding

오버라이딩은 상속 관계에서 등장하는 용어로, 자식 클래스가 부모 클래스의 동일한 signature의 메소드를 재구현해서 사용하는 것이다. 오버로딩과 무관한 개념이다.

예시로 정리한다. PC를 상속받는 LgGram 클래스에서 super.turnOn()을 통해 부모 클래스의 메소드를 호출하고, 추가적인 작업을 진행하는 식으로 오버라이딩하고 있다. 아니면 super.turn()라인을 지우고 자식 클래스만의 turnOn()을 구현할 수도 있다. 즉, 자식클래스에서는 오버라이딩을 통해 부모 클래스에서 수행하던 A 작업에 B작업을 추가 진행할 수도 있고, A작업은 빼고 C작업을 진행할수도 있다.

@Override
void turnOn() {
        super.turnOn();
        usedRamPercent = usedRamPercent.add(BigDecimal.valueOf(0.2));
}

이때 @Override 어노테이션을 써주었는데, 해당 어노테이션을 쓰지 않아도 에러는 나지 않는다. 하지만, @Override를 써주면 해당 메소드 시그니처가 부모 클래스에 없는 경우 컴파일러가 에러를 발생시킨다. 가독성과 안전한 코딩을 위해 써주는 것이 좋다.

Method does not override method from its superclass

abstract


abtract는 추상 메소드나 추상클래스를 선언할 때 쓰이는 키워드이다. 추상 클래스는 추상 메소드와 필드, 일반메소드를 멤버로 가질 수 있으며, static 필드, static 메소드도 작성 가능하다. 추상 클래스와 추상 메소드는 상속받은 자식 클래스가 메소드를 구현하기 위한 의도로 작성된다.

📌 추상 메소드는 자식 클래스에서 구현되지 않을수도 있지만, 그런 경우에는 반드시 자식 클래스 또한 추상 클래스여야 한다.

public abstract class GraphicObject {
   // declare fields
   // declare nonabstract methods
   abstract void draw();
}

interface


먼저 interface는 class가 아니다. 또 일반적인 상속의 개념과 거리가 있는데, 객체간 작동 방식의 통일을 위해 사용한다. 추상 클래스는 객체간 연관성이 깊은 반면 interface는 관련성이 깊지 않은 객체 간 인터페이스의 통일을 위해 사용한다. interface는 추상 클래스와 다르게 public 상수와 public 메소드 signature, default method, static method만 소유할 수 있다. interface의 자식 클래스는 반드시 interface의 메소드를 구현해야 한다. 설사 자식 클래스가 추상 클래스라 할지라도.

아래는 오라클 튜토리얼 문서의 예시 코드이다.

public interface OperateCar {

   // constant declarations, if any

   // method signatures
   
   // An enum with values RIGHT, LEFT
   int turn(Direction direction,
            double radius,
            double startSpeed,
            double endSpeed);
   int changeLanes(Direction direction,
                   double startSpeed,
                   double endSpeed);
   int signalTurn(Direction direction,
                  boolean signalOn);
   int getRadarFront(double distanceToCar,
                     double speedOfCar);
   int getRadarRear(double distanceToCar,
                    double speedOfCar);
         ......
   // more method signatures
}

📌 java 8부터 default method와 static method를 사용할 수 있으며, java 9부터 private method를 사용할 수 있다.

implements

위 예시에서 볼 수 있듯이 인터페이스는 implements를 통해 상속 받으며 다중상속이 가능하기 때문에 아래의 형태를 여러번 목격했을 것이다.

class Subclass extends Superclass implements InterfaceA,InterfaceB {
...
}

abstract class vs interface


추상 클래스와 인터페이스의 공통점

  • instantiation이 불가하다.
  • 선언만 있고 구현부가 없다.
  • 자식 클래스가 메서드의 구체적인 동작을 구현한다. 특히 interface는 예외 없이 반드시 구현해야 한다.

추상 클래스와 인터페이스의 차이점

추상 클래스와 인터페이스의 가장 큰 차이점은 객체 간 연관성 유무이다.

  • 추상 클래스는 추상 메서드를 자식 클래스가 구체화하여 그 기능을 확장하는 데 목적이 있다.
  • interface는 말 그대로 인터페이스 통일에 주안점을 둔다. 연관성이 없는 클래스 끼리 기능을 각각 구현할 필요가 있는 경우에 사용한다.
  • 추상 클래스는 클래스이지만 인터페이스는 클래스가 아니다.
  • 추상 클래스는 단일 상속이지만 인터페이스는 다중 상속이 가능하다.
  • 추상 클래스는 접근 제한자를 자유롭게 사용할 수 있지만 인터페이스는 모두 public이다.

final


해당 클래스는 상속을 해주는 super class가 될 수 없다는 뜻이다. class에 final 키워드를 넣는 경우는 거의 보지 못했지만, 상속이 불필요하다면 final을 써주는 게 맞다는 생각이 든다.

Object 클래스


java.lang.Object 클래스는 자바의 모든 클래스의 조상 클래스root class이다. 즉, 자바에서 유일하게 super class가 없고, 자바의 아담과 하와 같이 모든 클래스의 super class가 된다.

그래서, 자바의 모든 클래스는 Object 클래스의 메소드를 구현하게 되며, Object 클래스에는 다음의 메소드들이 포함되어 있다. 스레드 관련 메소드는 포함하지 않았다. 나중에 스레드를 다루면서 정리하기로!

접근제한자와 리턴 타입메소드 이름설명
protected Objectclone()해당 객체의 shallow copy된 객체를 만들어준다. 메모리 재할당 x
booleanequals(Object obj)객체의 contents가 동일한지 판별. 주소 비교 아님.
Class<?>getClass()해당 객체의 런타임 클래스를 반환
inthashCode()해당 객체의 해시 코드를 반환. 주소 아님.
StringtoString()해당 객체를 문자열 형식으로 변환해서 보여줌. ORM에서는 overriding해서 속성과 값을 보여주는데 많이 쓴다.

String.hashCode() 메소드 이슈

pigeonhole principle을 재고하면서..

📌 다음의 내용은 정리하다가 발견한 이슈인데, equals()메소드와 hashCode()를 적절하게 overriding한다면/또는 되어 있다면 걱정할 필요가 없다. 상속보다는 해시함수의 이슈라 너무 trivial하게 느껴진다면 넘어가도 좋다. 그렇지만 문제가 발생했을 때 알고 있다면 대응할 수 있다.
pigeon hole principle

String.hashCode()는 해시 관련 Collection에서 동일 원소인지 판별할 때 사용된다. 즉, String이 key값의 type인 경우에 hashCode가 동일하고, equals 메소드의 반환값이 true라면 동일한 key값으로 판별한다.

equals method로 true가 반환되면 보통 동일한 hashCode를 가진다. 그런데.. 보통이라니? hashCode가 equals의 결과와 같다는 보장이 없다고? 예시 코드가 있다.

String a = "Z@S.ME";
String b = "Z@RN.E";
if (a.hashCode() == b.hashCode()) {
	System.out.println("same hashcode");
} else {
	System.out.println("different hashcode");
}

결과는 same hashcode가 나온다. 그 이유는 String 클래스가 다음과 같은 식에 단순 대입해서 해시코드를 만들어내기 때문이다. 범위 차이가 나기에 int로 String을 모두 unique하게 표현할 수 없다.

s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1]
def: s[i]는 s라는 스트링의 i번째 원소를 ASCII code로 나타낸것,
n은 문자열의 길이, ^는 exponential

key의 타입이 String일 때 발생하는 이슈이므로, equals() 메소드와 hashCode()를 상황에 따라 모종의 기준을 가지고 오버라이딩하여 해결한다. 아래는 예시.

  • HashMap<String,?>인 경우 -> equals() 메소드가 HashMap의 value값도 비교하도록, hashCode() 메소드에 HashMap의 value값을 더해서 return하도록
  • custom data class -> equals() 메소드가 모든 멤버변수 값을 비교하도록, hashCode()의 해시 평션 결과값에 멤버변수값을 더해서 return하도록.

참고
Java in a Nutsell, 7th Edition
https://docs.oracle.com/javase/tutorial/java/IandI/subclasses.html
https://docs.oracle.com/javase/tutorial/java/IandI/override.html
https://docs.oracle.com/javase/tutorial/java/IandI/super.html
https://docs.oracle.com/javase/tutorial/java/IandI/abstract.html
https://docs.oracle.com/en/java/javase/15/docs/api/java.base/java/lang/Object.html
https://docs.oracle.com/javase/tutorial/java/IandI/createinterface.html
https://github.com/tinajeong/TIL/blob/master/2020-09/2020-09-30.md
https://brunch.co.kr/@mystoryg/133
https://blog.ggaman.com/916
https://nesoy.github.io/articles/2018-06/Java-equals-hashcode
이미지 출처:
https://www.thebusinesswomanmedia.com/never-pigeonhole-yourself/

계속해서 문서를 업데이트하고 있습니다. 언제든지 댓글피드백 남겨주세요. 😉

profile
Keep exploring, 계속 탐색하세요.

0개의 댓글