Backend/Java

상속

AIHYEONJI 2025. 5. 19. 14:07

1. 상속

 

자바에서 상속(Inheritance)은 기존 클래스의 속성과 메서드를 새로운 클래스가 물려받아 재사용할 수 있게 해주는 객체지향의 핵심 개념 중 하나입니다. 상속을 사용하면 공통된 코드를 중복 없이 여러 클래스에서 사용할 수 있어 코드의 재사용성과 확장성이 높아집니다. 상속은 extends 키워드를 사용해 구현하며, 기존 클래스는 부모 클래스(슈퍼클래스), 새로 정의한 클래스는 자식 클래스(서브클래스)라고 부릅니다. 자식 클래스는 부모의 기능을 그대로 사용할 수 있을 뿐만 아니라, 필요에 따라 기능을 확장하거나 재정의(오버라이딩)할 수 있습니다.

 

class 부모클래스 {
    // 공통 속성 및 동작 정의
}

class 자식클래스 extends 부모클래스 {
    // 부모의 기능을 상속받고 추가적인 기능 정의
}
class Animal {
    public void eat() {
        System.out.println("먹는다.");
    }
}

class Dog extends Animal {
    public void bark() {
        System.out.println("멍멍 짖는다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();
        dog.eat();  // 부모의 메서드 사용
        dog.bark(); // 자신의 메서드 사용
    }
}

 

1. 메서드 오버라이딩

자식 클래스는 부모 클래스의 메서드를 그대로 사용할 수도 있고, 필요에 따라 재정의(override)하여 자신에게 맞게 바꿀 수도 있습니다.

class Animal {
    public void sound() {
        System.out.println("동물이 소리를 낸다.");
    }
}

class Cat extends Animal {
    // 어노테이션 : 자바가 해석이 될 때, 오버라이드된 것을 컨파일러에게 지시를 내릴 때
    @Override
    public void sound() {
        System.out.println("야옹");
    }
}

 

2. 자바에서의 상속 제약 사항

- 자바는 단일 상속만 지원합니다. 즉, 한 클래스는 하나의 부모 클래스만 상속받을 수 있습니다.

class A {}
class B {}
class C extends A, B {} // ❌ 컴파일 에러

 

- final 클래스는 상속할 수 없습니다.

// final이 붙은 클래스는 상속을 못하게 한다.
final class Car {}
class SportsCar extends Car {} // ❌ 에러 발생

 

- 생성자는 상속되지 않습니다. 생성자는 자식 클래스가 직접 정의해야 하며, 부모 생성자를 호출하려면 super()를 사용합니다.

 

3. super 키워드

super는 부모 클래스의 필드나 메서드에 접근할 때 사용합니다. 또한 생성자 안에서 부모 생성자 호출에도 사용됩니다.

class Parent {
    public Parent(String msg) {
        System.out.println("부모 생성자: " + msg);
    }
}

class Child extends Parent {
    public Child() {
        super("안녕하세요");  // 부모 생성자 호출
        System.out.println("자식 생성자");
    }
}
class Person {
    String name;

    public Person(String name) {
        this.name = name;
    }

    public void greet() {
        System.out.println("안녕하세요, 저는 " + name + "입니다.");
    }
}

class Student extends Person {
    String school;

    public Student(String name, String school) {
        super(name); // 부모 생성자 호출
        this.school = school;
    }

    @Override
    public void greet() {
        super.greet(); // 부모 메서드 호출
        System.out.println("학교는 " + school + "입니다.");
    }
}

 

2. final

final은 자바에서 "변경할 수 없음"을 의미하는 키워드입니다. 변수, 메서드, 클래스에 모두 사용할 수 있으며, 각각의 의미가 조금씩 다릅니다.

 

1. final 변수: 값 변경 불가 (상수)

final을 변수에 사용하면, 초기화된 이후 값 변경이 불가능한 상수가 됩니다. 선언과 동시에 값을 설정하거나, 생성자에서 한 번만 초기화 가능하며 관례상 대문자로 작성합니다. (MAX_SIZE, PI 등)

final int MAX = 100;
MAX = 200; // ❌ 에러! 값을 바꿀 수 없음

 

2. final 메서드: 재정의(오버라이딩) 불가

final을 메서드에 사용하면, 자식 클래스에서 그 메서드를 오버라이딩할 수 없습니다. 보안성 유지하며 중요한 로직이 변경되지 않도록 보호합니다.

class Parent {
    public final void show() {
        System.out.println("부모 클래스 메서드");
    }
}

class Child extends Parent {
    // public void show() {} // ❌ 오류: final 메서드는 재정의할 수 없음
}

 

3. final 클래스: 상속 불가

final을 클래스에 사용하면, 그 클래스를 다른 클래스가 상속받을 수 없습니다. 클래스를 변경하거나 확장하지 못하도록 막고 싶을 때 사용합니다. String, Integer 같은 핵심 라이브러리 클래스들이 final로 선언되어 있습니다.

final class Animal {
    public void sound() {
        System.out.println("동물 소리");
    }
}

// class Dog extends Animal {} // ❌ 오류: final 클래스는 상속 불가

 

종합예제

// 부모 클래스
class Animal {
    String name;

    public Animal(String name) {
        this.name = name;
    }

    public void eat() {
        System.out.println(name + "가 먹습니다.");
    }

    public final void sleep() {
        System.out.println(name + "는 조용히 잠듭니다. (이건 반드시 이 방식으로!)");
    }

    public void speak() {
        System.out.println(name + "가 소리를 냅니다.");
    }
}

// 자식 클래스
class Dog extends Animal {

    public Dog(String name) {
        super(name);
    }

    // 오버라이딩: speak() 메서드를 개성 있게 변경
    @Override
    public void speak() {
        System.out.println(name + "가 멍멍 짖습니다!");
    }

    // 오버로딩: bark() 여러 버전
    public void bark() {
        System.out.println("멍멍!");
    }

    public void bark(String target) {
        System.out.println(target + "를 보고 멍멍!");
    }

    public void bark(int times) {
        for (int i = 0; i < times; i++) {
            System.out.print("멍! ");
        }
        System.out.println();
    }
}
public class Main {
    public static void main(String[] args) {
        Dog rucy = new Dog("루시");

        // 상속받은 메서드
        rucy.eat();   // 루시가 먹습니다.

        // final 메서드 (오버라이딩 불가)
        rucy.sleep(); // 루시는 조용히 잠듭니다.

        // 오버라이딩된 메서드
        rucy.speak(); // 루시가 멍멍 짖습니다!

        // 오버로딩된 메서드들
        rucy.bark();               // 멍멍!
        rucy.bark("택배 아저씨");     // 택배 아저씨를 보고 멍멍!
        rucy.bark(3);              // 멍! 멍! 멍!
    }
}

 

3. Object 클래스

자바에서 Object 클래스는 모든 클래스의 최상위 부모 클래스(슈퍼클래스)로, 자바에 존재하는 모든 클래스는 Object를 직접 또는 간접적으로 상속받습니다. 이 클래스는 객체를 비교하거나 문자열로 표현하고, 복제하거나 쓰레기 수집 전에 정리하는 등 객체에 대한 기본적인 동작을 정의한 메서드들을 제공합니다. 대표적인 메서드로는 toString(), equals(), hashCode(), clone(), finalize() 등이 있으며, 개발자는 필요에 따라 이 메서드들을 오버라이딩하여 자신만의 방식으로 객체의 동작을 정의할 수 있습니다. Object는 자바의 클래스 계층 구조에서 기본 뼈대 역할을 하는 핵심 클래스입니다.

 

class Person {
    // 실제로는 class Person extends Object 와 같음
}

 

Object 클래스가 제공하는 주요 메서드

toString() 객체를 문자열로 표현 (기본: 클래스이름@해시값)
equals(Object obj) 두 객체가 같은지를 비교 (기본: 주소 비교)
hashCode() 객체의 해시코드 반환
getClass() 클래스 정보를 담은 Class 객체 반환
clone() 객체 복제 (implements Cloneable 필요)
finalize() 가비지 컬렉션 전에 호출 (거의 사용되지 않음)

 

@Override
public String toString() {
    return "이름: " + name + ", 나이: " + age;
}
class Person {
    String name;
    int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // toString() 오버라이딩
    @Override
    public String toString() {
        return "이름: " + name + ", 나이: " + age;
    }

    // equals() 오버라이딩
    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;                 // 같은 객체인지 비교
        if (obj == null || getClass() != obj.getClass()) return false; // 클래스 비교

        Person other = (Person) obj;
        return this.name.equals(other.name) && this.age == other.age;
    }
}

public class Main {
    public static void main(String[] args) {
        Person p1 = new Person("김사과", 20);
        Person p2 = new Person("반하나", 25);
        Person p3 = new Person("김사과", 20);

        System.out.println(p1);
        System.out.println(p1.equals(p2));
        System.out.println(p1.equals(p3));
    }
}

 

4. 업캐스팅

업캐스팅(Upcasting)은 자식 클래스 객체를 부모 클래스 타입으로 참조하는 것을 말합니다. 즉, 상속 관계에서 자식 클래스의 객체를 부모 클래스의 변수로 담는 것이다.

 

업캐스팅을 사용하는 이유

  1. 다형성(Polymorphism)을 구현하기 위해
    → 동일한 부모 타입으로 여러 자식 객체를 통일해서 다룰 수 있음
  2. 유지보수가 쉬운 코드 구조를 위해
    → 메서드 호출 시 동작은 실제 객체의 메서드(오버라이딩된 것)가 실행됨
  3. 코드 재사용성과 확장성 향상
class Animal {
    public void sound() {
        System.out.println("동물이 소리를 냅니다.");
    }
}

class Dog extends Animal {
    @Override
    public void sound() {
        System.out.println("멍멍!");
    }

    public void wagTail() {
        System.out.println("꼬리를 흔듭니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Dog dog = new Dog();             // Dog 객체 생성
        Animal animal = dog;             // ✅ 업캐스팅 (자동)

        animal.sound();                  // 멍멍! (오버라이딩된 메서드 호출)
        // animal.wagTail();             // ❌ 컴파일 에러! Animal 타입에는 없음
    }
}

다운캐스팅과 비교

업캐스팅 자식 객체 → 부모 타입으로 참조 (자동)
다운캐스팅 부모 타입 참조 → 다시 자식 타입으로 형변환 (명시적)

 

Dog dog = new Dog();
Animal animal = dog;             // 업캐스팅 (자동)
Dog dog2 = (Dog) animal;         // 다운캐스팅 (명시적)
dog2.wagTail();             // 가능!

 

5. 이종모음

이종 모음(heterogeneous collection)은 서로 다른 타입의 객체들을 하나의 컬렉션에 담아 처리하는 구조를 의미합니다. 일반적으로 제네릭(Generic)을 사용하지 않거나, 상위 타입(예: Object)으로 업캐스팅하여 다양한 타입의 객체들을 하나의 리스트나 배열 등에 저장할 수 있습니다. 이 방식은 유연성을 제공하지만, 꺼낼 때 다시 원래 타입으로 다운캐스팅해야 하고, 타입 안정성이 떨어지며 런타임 오류의 위험이 있습니다. 이종 모음은 특수한 상황에서 사용되며, 자바에서는 보통 제네릭과 상속을 활용해 동종 모음(homogeneous collection)을 선호합니다.

 

class Book {
    String title;

    public Book(String title) {
        this.title = title;
    }

    public void read() {
        System.out.println("책 \"" + title + "\"을 읽습니다.");
    }
}

public class HeterogeneousArrayExample {
    public static void main(String[] args) {
        // Object 타입 배열로 이종 모음 구성
        Object[] items = new Object[3];

        items[0] = "안녕하세요";            // String
        items[1] = 100;                   // Integer (오토박싱)
        items[2] = new Book("자바의 정석"); // 사용자 정의 클래스

        // 배열 순회하며 타입 확인 및 처리
        for (Object obj : items) {
            if (obj instanceof String) {
                System.out.println("문자열: " + obj);
            } else if (obj instanceof Integer) {
                System.out.println("정수: " + obj);
            } else if (obj instanceof Book) {
                Book book = (Book) obj; // 다운캐스팅
                book.read();
            }
        }
    }
}

instanceof란?

instanceof는 객체가 특정 클래스의 인스턴스인지 확인하는 연산자입니다. 이 연산자는 객체가 어떤 클래스 또는 그 하위 타입으로부터 생성되었는지를 판별할 때 사용되며, 결과는 true 또는 false를 반환합니다.

객체 instanceof 클래스이름
  • 결과는 boolean 타입
  • 객체가 해당 클래스 또는 그 자식 클래스의 인스턴스이면 true, 아니면 false
  • 주로 다운캐스팅하기 전에 안전하게 타입 확인할 때 사용
  • 자바 16부터는 instanceof와 다운캐스팅을 한 줄로 작성할 수 있게 패턴 매칭이 도입
class Book {
    String title;

    public Book(String title) {
        this.title = title;
    }

    public void read() {
        System.out.println("책 \"" + title + "\"을 읽습니다.");
    }
}

public class HeterogeneousArrayExample {
    public static void main(String[] args) {
        // Object 타입 배열로 이종 모음 구성
        Object[] items = new Object[3];

        items[0] = "안녕하세요";            // String
        items[1] = 100;                   // Integer (오토박싱)
        items[2] = new Book("자바의 정석"); // 사용자 정의 클래스

        // 배열 순회하며 타입 확인 및 처리
        for (Object obj : items) {
            if (obj instanceof String str) {
                System.out.println("문자열: " + str);
            } else if (obj instanceof Integer num) {
                System.out.println("정수: " + num);
            } else if (obj instanceof Book book) {
                book.read();  // 자동으로 Book 타입으로 다운캐스팅됨
            }
        }
    }
}

'Backend > Java' 카테고리의 다른 글

String 클래스  (0) 2025.05.20
접근 제한자  (0) 2025.05.20
클래스와 객체  (1) 2025.05.19
메서드  (0) 2025.05.19
배열  (1) 2025.05.15