본문 바로가기
JAVA

[Java] 상속(Inheritance)

by 햄과함께 2020. 12. 24.
320x100

상속

class A {
  String name;
  void printName() { System.out.println(name) };
  
  // ...
}

class B {
  String name;
  void printName() { System.out.println(name) };
  
  // ...
}

class C {
  String name;
  void printName() { System.out.println(name) };
  
  // ...
}

같은 기능이나 비슷한 변수를 가지는 클래스가 많다고 할 때, 그 클래스들의 변수 이름이나 공통된 기능을 하는 함수의 구현이 달라질 때 모든 클래스들의 내용을 바꿔야 하는 단점이 있다. -> 유지보수가 어렵다.

 

class SuperClass {
  String name;
  void printName() { System.out.println(name); }
}

class A extends SuperClass {
  // ...
}

class B  extends SuperClass {
  // ...
}

class C extends SuperClass {
  // ...
}

 

이 때 동일한 변수, 함수들을 하나의 클래스로 만들고 이를 상속하는 함수들을 만들면 상속하는 함수만 변경하면 이를 상속받는 클래스들도 변경된 변수, 함수를 사용할 수 있어 유지보수가 수월해진다.

또한 중복코드도 줄일 수 있다.

class 자식클래스이름 extends 부모클래스이름 { 
  // ... 
}

extends 키워드를 사용해서 상속한다.

class Parent {
    int no;
}

class Child extends Parent {
    String name;
}


Parent p = new Parent();
p.no = 10;

Child child = new Child();
child.no = 12; // 부모 클래스 멤버변수 사용 가능
child.name = "child";

Super 키워드

super 키워드는 자식클래스에서 부모클래스의 필드, 함수를 사용할 때 사용된다.

package Parent;

public class Parent {

    public int no;

    protected void printNo() {
        System.out.println(no);
    }
}
package Child;

import Parent.Parent;

public class Child extends Parent {

    public String name;

    public void printParentNo() {
        super.printNo();
    }
}

서로 다른 패키지에 있지만 Child 클래스는 Parent 클래스를 상속하였고, Parent에 있는 printNo 함수는 protected 접근 제어자를 가지기 때문에 Child 클래스 내에서 printNo 함수에 접근 할 수 있다.

package test;

import org.junit.jupiter.api.Test;

import Child.Child;
import Parent.Parent;

public class HelloWorldTest {

    @Test
    void test() {
        Parent p = new Parent();
        p.no = 10;
//        p.printNo(); // error

        Child child = new Child();
        child.no = 12;
        child.name = "child";
        child.printParentNo(); // 12
    }
}

parent 객체의 printNo 함수는 protected 접근제어자를 가지기 때문에 HelloWorldTest 클래스에서는 parent 객체의 printNo 함수를 호출할 수 없다.

child 객체에서 내부에서 printNo 함수를 호출하는 printParentNo 함수는 public 접근제어자를 가지므로 호출이 가능하다.


super, this

public class Child extends Parent {

    public int no;
    public String name;

    public Child() {
        super.no = 10;
    }

    public void printNos() {
        System.out.println("parent : " + super.no);
        System.out.println("child : " + this.no);
        System.out.println("no : " + no);
    }
}

자식 클래스는 부모 클래스의 멤버변수와 동일한 이름의 멤버변수를 가질 수 있다.

 this  키워드는 객체 자기 자신을 가리키는 반면,  super  키워드를 사용하면 부모 클래스의 멤버 변수에 접근 가능하다.

@Test
void test() {
   Child child = new Child();
   child.no = 12;

   child.printNos();
}

// parent : 10
// child : 12
// no : 12

출력 확인 테스트

// ...
    public void printNos() {
        System.out.println("parent : " + super.no);
        System.out.println("child : " + this.no);
        System.out.println("no : " + this.no);
    }

bytecode를 디컴파일한 코드를 확인해보면 no를 사용한 코드는 this.no로 변환된다.


메소드 오버라이딩 (Method Overriding)

메소드 오버라이딩이란 부모클래스에 정의된 함수를 재정의하는걸 말한다.

재정의하고자 하는 함수와 선언은 동일하게 가야하므로 반환타입, 메소드명, 파라미터들은 모두 동일해야한다.

@Override
재정의하고자 하는 함수 {}

@Override  어노테이션을 사용하여 오버라이딩한다.


접근제어자

부모클래스에서 정의한 접근제어자보다 더 낮은 범위의 접근제어자로 변경가능하다.

접근제어자범위 : public > protected > default > private

class Parent {

    // ...
    
    // default 접근제어자
    void printNo() {
        System.out.println(no);
    }
}

class Child extends Parent {

    // ...
    
    @Override
    public void printNo() {
        System.out.println(super.no);
    }
}

부모클래스의 printNo 메소드의 접근제어자는 default이다.

따라서 자식클래스에서는 같거나 이보다 넓은 범위의 default, protected, public 접근제어자로 변경가능하다.


예외처리

함수들 중에 예외를 던지는 함수들이 있는데 이런 함수를 상속하는 경우, 던지는 예외를 더 상세하게 범위를 줄일 수 있다.

public abstract class ObjectStreamException extends IOException {}

public class InvalidObjectException extends ObjectStreamException {}

ObjectStreamException(IOException 상속), InvalidObjectException(ObjectStreamException 상속), IOException 3가지 Exception으로 테스트해보자.

public class Parent {
    // ...
    
    protected void printNo() throws ObjectStreamException {
        System.out.println(no);
    }
}

부모클래스 함수에서는 ObjectStreamException을 던진다. 

public class Child extends Parent {
    // ...
    
    @Override
    protected void printNo() throws InvalidObjectException {
        System.out.println(no);
    }
}

자식클래스에서는 이와 동일한 예외나 이보다 더 상세한 InvalidObjectException을 던질 수 있다.

만약 ObjectStreamException 보다 더 넓은 범위의 예외(ex, IOException)을 던지고자 할 때는 위와 같은 에러를 뱉는다.


메소드 디스패치 (Method Dispatch)

메소드 디스패치란 어떤 메소드를 호출할지 결정한 후 실행하는걸 의미한다.


Static Method Dispatch

컴파일 타임 시 알 수 있는 정보로 어떤 메서드를 호출할지 정해서 호출하는 방법을 정적 메서드 디스패치라 한다.

파라미터가 다른 메서드 오버로딩된 메서드도 컴파일 시 알 수 있으므로 Static Method Dispatch에 속한다.

class Member {
    int name;
    
    void printName() {}
    void printName(String name) { }
}

Member member = new Member();

// static method dispatch
member.printName();
member.printName("name");

Dynamic Method Dispath

다이나믹 메서드 디스패치란 상위 타입의 클래스 타입인 객체의 메서드가 호출될 때 어떤 메서드 구현체를 실행할지 결정 후 호출하는 것을 의미한다.

상세 호출 객체의 정보는 런타임시에서만 알 수 있으므로 바이트코드로는 알 수 없다.

this 키워드로 객체의 정보를 알아내 호출할 메서드를 선택한다.

abstract class Animal {

    abstract void print();
}

class Dog extends Animal {
    void print() { System.out.println("dog");}
}
class Cat extends Animal {
    void print() { System.out.println("cat");}
}

// dynamic method dispatch
Animal dog = new Dog();
dog.print();
        
Animal cat = new Cat();
cat.print();

final 키워드

final class 는 상속할 수 없는 클래스를 의미한다. (immutable class)

final 메서드는 오버라이딩을 할 수 없다.

final 변수는 get만 가능하고 set은 할 수 없다. = 값 변경이 불가능하다.

 

// HelloJava.java
public class HelloJava {

    String name = "HelloJava";

    public String getName() {
        return name;
    }
}

클래스, 메서드, 변수가 한 번 정의된 이후 변경이 불가능하게 하고 싶은 경우 final 키워드를 사용한다.

// HelloJava.class 디컴파일
public class HelloJava {
    final String name = "HelloJava";

    public HelloJava() {
    }

    public String getName() {
        return "HelloJava"; // 변수 이름이 아닌 변수 값을 사용한다.
    }
}

final 변수는 컴파일되면 변수가 아닌 변수 값을 대신 사용한다.

// HelloJava.class 디컴파일
public class HelloJava {
    String name = "HelloJava";

    public HelloJava() {
    }

    public String getName() {
        return this.name;
    }
}

final 변수가 아닌 경우 컴파일 결과 this.name 처럼 변수를 그대로 사용한다.


추상 클래스

추상 클래스란 추상적으로 존재하는 클래스를 의미한다. 

추상 클래스로는 객체를 생성할 수 없으며 추상클래스를 상속한 클래스만이 객체로 생성될 수 있다.

abstract class 추상클래스이름 { }
class 클래스이름 extends 추상클래스이름 { }

abstract 예약어를 사용하여 추상 클래스임을 나타낸다.

추상클래스를 상속하기 위해서 extends 예약어를 사용한다.

abstract class Animal {

    String name = "Animal";

    void printName() {}

    abstract void abstractFunction();
}

class Dog extends Animal {

    @Override
    void abstractFunction() {

    }
}

메서드는 일반 메서드와 abstract 예약어를 앞에 붙인 추상메서드를 가질 수 있다.

추상클래스를 상속한 클래스에서는 추상메서드는 반드시 오버라이딩하여야 한다.


인터페이스

 

변수

interface IAnimal {
    public static final String name = "IAnimal";
}

멤버변수는 public 접근제어자만 가질 수 있다. 접근제어자를 명시하지 않으면 public 접근제어자를 가진다.

인터페이스는 final 변수만을 가진다. 따라서 초기화 값을 명시해줘야한다.

Cannot assign a value to final variable 'name'

인터페이스에서 정의한 name 변수를 변경하려고 하면 위와 같은 에러가 발생한다. 인터페이스 정의한 변수는 final 변수이기 때문에 변수 값을 변경할 수 없다.

class Dog implements IAnimal {

    @Override
    public void publicPrint() {
        System.out.println(name); // possible
    }
}


Dog dog = new Dog();
dog.name; // impossible

또한 해당 인터페이스를 구현한 구현클래스에서는 해당 변수에 접근가능하지만 외부에서 구현클래스의 객체에서의 접근은 불가능하다.

 

메서드

interface IAnimal {
    private void privatePrint() {}
    default void defaultPrint() {}
    void publicPrint();
}

메서드는 private, default, public 접근제어자를 가질 수 있다. (protected 불가능) 접근제어자를 명시하지 않으면 public 접근제어자를 가진다.

  인터페이스 구현클래스
private 함수 선언/정의. 오버라이딩 불가능
default 함수 선언/정의. 오버라이딩 가능
public 함수 선언. 오버라이딩 가능

인터페이스의 메서드는 final 메서드가 될 수 없다.

추상클래스 vs 인터페이스

  추상클래스 인터페이스
상속 예약어 extends implements
상속 가능 클래스 수 1개 1개이상 (다중상속 가능)
public 메서드 함수 정의 가능. (선택) 함수 정의 불가능. 반드시 오버라이딩 해야함.

Object 클래스

java.lang.Object

Object 클래스는 모든 클래스들의 최상위 클래스이다.

다른 클래스들을 상속받고 있지 않은 클래스들은 Object 클래스의 상속이 생략되어 있다.

함수 설명
Object clone() 객체의 복사본을 만들고 반환한다.
boolean equals(Object obj) 기본 구현은 this == obj 로, 주소를 확인한다.
객체의 동등비교 시 사용된다.
void finalize() 해당 자원이 더 이상 사용된다고 판단되지 않을 때 GC에서 호출한다.
성능이슈, 데드락 등의 문제를 발생시킬 수 있고 java9부터 deprecated 되었다.
Class<?> getClass() 런타임 클래스를 반환한다.
int hashCode() 객체의 hashcode 값을 반환한다. 
만약 동일한 객체라면 반드시 같은 hashcode를 가져야한다. 다른 객체이지만 동일한 hashcode 값을 가질 수도 있다.
HashMap, HashSet 과 에서는 key값을 hashcode로 사용한다.
만약 equals 함수를 재정의 하고 hashCode 함수를 재정의하지 않았다면 hashcode를 키 값으로 하는 클래스들이 정상 작동하지 않을 수 있다. (equals, hashCode 함수는 같이 재정의 되어야 한다.)
void notify() 이 개체의 모니터에서 대기중인 스레드 중 임의의 스레드 한 개를 깨운다.
void notifyAll() 이 개체의 모니터에서 대기중인 모든 스레드를 깨운다.
String toString() 객체의 문자열 표현을 반환한다.
기본 구현 : getClass().getName() + "@" + Integer.toHexString(hashCode())
void wait([long timeout][, int nanos]) 호출한 스레드가 notify() 나 notifyAll() 메서드로 깨울 때까지 대기하거나
timeout, nanos 시간까지 아무도 깨우지 않는다면 스스로 깨어난다.

참고

www.tcpschool.com/java/java_inheritance_overriding 

stackoverflow.com/questions/56139760/why-is-the-finalize-method-deprecated-in-java-9

www.youtube.com/watch?v=s-tXAHub6vg

320x100

'JAVA' 카테고리의 다른 글

[Java] 데이터, 변수, 배열  (0) 2020.12.27
[Java] JVM  (0) 2020.12.27
[Java] 이진검색트리(BST) 구현  (0) 2020.12.19
[Java] 클래스, 객체  (0) 2020.12.19
[Java] 연결리스트(Linked List), Stack, Queue 구현  (0) 2020.12.12

댓글