1. 다형성과 타입 캐스팅

header

자바의 객체 지향 프로그래밍 핵심은 클래스입니다.
그 중에서 생성자(Constructor)와 상속(Inheritance)은 반드시 이해해야 하는 중요한 개념입니다.

1. 생성자(Constructor)

생성자 개념

객체 생성 시 호출되는 특별한 메서드이며, 주요 특징은 다음과 같습니다.

  • 클래스 이름과 동일
  • 반환 타입 없음
  • new 키워드와 함께 호출
  • 생성자를 작성하지 않으면 컴파일러가 기본 생성자를 자동으로 제공
public class Hello {
    int message;
    // 기본 생성자
    Hello() {
    }
}

생성자 오버로딩

매개변수의 개수와 타입이 다른 생성자를 여러 개 선언 가능합니다.
이를 생성자 오버로딩이라고 합니다.

class Person {
    String name;
    int age;

    Person() {}

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

    Person(String name, int age) { 
        this(name); // this()로 다른 생성자 호출
        this.age = age; 
    }

    // 생성자으 매개변수 이름이 직관적이지 않다.
    // Person(String a, int b) {
    //     name = a;
    //     age = b;
    // }
}

this와 this()

  • this: 현재 객체(자기 자신)를 가리키는 참조 변수
  • this(): 같은 클래스의 다른 생성자를 호출
class Student {
    String name;
    int score;

    Student(String name) {
        this.name = name;
    }

    Student(String name, int score) {
        this(name);   // 다른 생성자 호출
        this.score = score;
    }
}

2. 상속

부모 클래스의 필드와 메서드를 자식 클래스가 물려받는 것을 의미하며, 다음과 같은 장점이 있습니다.

  • 코드 재사용성 향상
  • 중복 제거
class A {
    void hello() { System.out.println("Hello"); }
}

class B extends A {
    void hi() { System.out.println("Hi"); }
}

B b = new B();
b.hello(); // 부모 메서드 사용 가능

문제 1: Person 클래스 생성자와 필드 초기화

Person 클래스를 작성하고, 이름과 나이를 입력받아 필드를 초기화하는 생성자를 만들어 보세요.
생성된 객체의 이름과 나이를 출력하세요.

조건 및 힌트

  1. 생성자는 클래스 이름과 같아야 합니다.
  2. this 키워드를 사용하여 필드를 초기화하세요.
정답 보기

  class Person {
      String name;
      int age;

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

  public class Main {
      public static void main(String[] args) {
          Person p = new Person("홍길동", 25);
          System.out.println("이름: " + p.name);
          System.out.println("나이: " + p.age);
      }
  }
  

3. 오버라이딩(Overriding)

부모 클래스의 메서드를 자식 클래스에서 재정의하는 것을 말하며, 아래 규칙을 따라야 합니다.

  • 메서드의 이름, 매개변수, 반환 타입이 동일해야 합니다.
  • 접근 제한자는 같거나 더 넓은 범위로만 변경 가능합니다.
    • defaultpublic (O)
    • publicprivate (X)
class Computer {
    void powerOn() {
        System.out.println("전원 켜짐");
    }
}

class Mac extends Computer {
    // 애노테이션 권장!
    @Override
    void powerOn() {
        super.powerOn(); // 부모 메서드 호출
        System.out.println("Boot MacOSX");
    }
}

4. Super 키워드

super부모 클래스의 멤버를 참조할 때 사용하는 키워드입니다.

class Parent {
    int value = 10;
}

class Child extends Parent {
    void show() {
        System.out.println(super.value);
    }
}

5. 접근제한자

제한자 적용 범위
public 모든 클래스에서 접근 가능
protected 같은 패키지 + 자식 클래스
default 같은 패키지 내부
private 같은 클래스 내부

6. final 클래스와 final 메서드

  • final class: 상속 불가
  • final method: 오버라이딩 불가
  • 생성자에는 final을 사용할 수 없습니다.
final class Car {}  // 더 이상 상속 불가

class Phone {
    final void call() {
        System.out.println("전화 걸기");
    }
}

문제 2. 생성자 오버로딩

Student 클래스를 작성하고, 기본 생성자와 이름만 받는 생성자, 이름과 점수를 모두 받는 생성자를 작성하세요. 생성자 오버로딩을 활용하세요.

조건 및 힌트

  1. 생성자는 매개변수 개수와 타입을 달리할 수 있습니다.
  2. this()를 이용해 생성자 간 호출을 해보세요.
정답 보기

    class Student {
        String name;
        int score;

        Student() {}

        Student(String name) {
            this.name = name;
        }

        Student(String name, int score) {
            this(name);       // 다른 생성자 호출
            this.score = score;
        }
    }

    public class Main {
        public static void main(String[] args) {
            Student s1 = new Student("철수");
            Student s2 = new Student("영희", 90);
            System.out.println(s1.name);
            System.out.println(s2.name + " - " + s2.score);
        }
    }
  

문제 3. 오버라이딩과 다형성

부모 클래스 Post에 share() 메서드를 정의하고, 자식 클래스 ImagePostVideoPost에서 오버라이딩하세요.
각각 “이미지 공유”, “비디오 공유”를 출력하세요.

조건 및 힌트

  1. extends 키워드를 사용해 상속하세요.
  2. @Override 애노테이션을 붙여주세요.
정답 보기

    class Post {
        void share() {
            System.out.println("게시물 공유");
        }
    }

    class ImagePost extends Post {
        @Override
        void share() {
            System.out.println("이미지 공유");
        }
    }

    class VideoPost extends Post {
        @Override
        void share() {
            System.out.println("비디오 공유");
        }
    }

    public class Main {
        public static void main(String[] args) {
            Post p1 = new ImagePost();
            Post p2 = new VideoPost();
            p1.share();  // 이미지 공유
            p2.share();  // 비디오 공유
        }
    }
  

문제 4. super 키워드로 부모 필드 접근하기

Parent 클래스의 value 필드를 Child 클래스에서 출력하도록 하세요.
super 키워드를 사용해야 합니다.

조건 및 힌트

  1. super는 부모 클래스의 필드나 메서드에 접근할 때 사용합니다.
  2. Child 클래스 안에서 super.value를 출력하세요.
정답 보기

    class Parent {
        int value = 100;
    }

    class Child extends Parent {
        void showValue() {
            System.out.println("부모 value = " + super.value);
        }
    }

    public class Main {
        public static void main(String[] args) {
            Child c = new Child();
            c.showValue();
        }
    }
  

7. 타입 변환

Java Object Type Casting
https://www.geeksforgeeks.org/java/class-type-casting-in-java/

타입 변환은 한 타입을 다른 타입으로 변환하는 것입니다. 자바의 클래스 타입 변환은 상속 관계에서 이루어지며, 두 가지 종류가 있습니다.

자동 타입 변환 (업캐스팅, Upcasting)

자식 객체를 부모 타입으로 변환하는 것을 말하며, 별도의 캐스팅 연산자 없이 자동으로 이루어집니다.

  • 부모클래스 변수 = new 자식클래스();
  • 부모클래스 변수 = 자식객체;

부모 타입으로 변환된 경우, 부모 클래스의 멤버만 접근 가능합니다. 단, 자식이 오버라이딩한 메서드는 자식의 메서드가 호출됩니다.

class Parent {
    void print() {
        System.out.println("부모 출력");
    }
}

class Child extends Parent {
    void print() {   // 오버라이딩
        System.out.println("자식 출력");
    }
    void childOnly() {
        System.out.println("자식 전용 메서드");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();   // 자동 타입 변환 (업캐스팅)
        p.print();                // "자식 출력" (오버라이딩된 메서드 실행)
        // p.childOnly();         // 부모 타입이라 접근 불가
    }
}

강제 타입 변환 (다운캐스팅, Downcasting)

부모 타입으로 변환된 객체를 다시 자식 타입으로 변환하는 것을 말하며, 자식 클래스의 멤버에 접근하기 위해 사용됩니다.

  • 일회성 변환: ((자식클래스) 부모타입참조).자식메서드();
  • 변수 저장 후 사용: 자식클래스 변수 = (자식클래스) 부모타입참조;
class Parent {
    void print() {
        System.out.println("부모 출력");
    }
}

class Child extends Parent {
    void print() {
        System.out.println("자식 출력");
    }
    void childOnly() {
        System.out.println("자식 전용 메서드");
    }
}

public class Main {
    public static void main(String[] args) {
        Parent p = new Child();   // 업캐스팅
        p.print();                // "자식 출력"

        // 1) 일회성 다운캐스팅
        ((Child)p).childOnly();   // "자식 전용 메서드"

        // 2) 변수에 할당
        Child c = (Child)p;
        c.childOnly();            // "자식 전용 메서드"
    }
}

문제 5: 기본 타입 변환 연습

Account(계좌) 클래스를 만들고, 이를 상속받는 SavingsAccount(저축예금) 클래스를 정의하세요.
SavingsAccount 객체를 생성하여 Account 타입으로 자동 변환(업캐스팅)한 뒤, 다시 SavingsAccount 타입으로 강제 변환(다운캐스팅)하여 저축예금에만 있는 메서드를 호출해보세요.

조건 및 힌트

  1. Account 클래스에는 showBalance() 메서드를, SavingsAccount 클래스에는 addInterest() 메서드를 추가하세요.
  2. instanceof 연산자를 사용하여 안전하게 다운캐스팅을 시도하세요.
정답 보기

    class Account {
        int balance = 10000;
        void showBalance() {
            System.out.println("잔액: " + balance + "원");
        }
    }

    class SavingsAccount extends Account {
        void addInterest() {
            System.out.println("이자가 추가되었습니다!");
        }
    }

    public class Main {
        public static void main(String[] args) {
            // 1. SavingsAccount 객체를 Account 타입으로 업캐스팅
            Account acc = new SavingsAccount();
            acc.showBalance(); // Account의 메서드 호출 가능

            // 2. instanceof로 타입 확인 후 안전하게 다운캐스팅
            if (acc instanceof SavingsAccount) {
                SavingsAccount sa = (SavingsAccount) acc;
                sa.addInterest(); // SavingsAccount 고유의 메서드 호출
            }
        }
    }
  

문제 6: 다형성을 활용한 객체 관리

다양한 상품(Product)을 관리하는 쇼핑몰 프로그램을 만드세요. Product를 부모 클래스로 하고, BookClothing을 자식 클래스로 정의하세요. Product 타입의 배열에 BookClothing 객체를 함께 저장하고, 반복문을 통해 각 상품의 정보를 출력하세요.

조건 및 힌트

  1. Product에는 showInfo() 메서드를 정의하고, 각 자식 클래스에서 이를 오버라이딩하세요.
  2. Book에는 readSample() 메서드를, Clothing에는 checkSize() 메서드를 추가하세요.
  3. 반복문 내에서 instanceof를 사용해 객체 타입을 확인하고, 다운캐스팅하여 각 클래스 고유의 메서드를 호출하세요.
정답 보기

    class Product {
        public void showInfo() {
            System.out.println("상품 정보를 표시합니다.");
        }
    }

    class Book extends Product {
        @Override
        public void showInfo() {
            System.out.println("책 정보를 표시합니다.");
        }

        public void readSample() {
            System.out.println("책 샘플을 읽어봅니다.");
        }
    }

    class Clothing extends Product {
        @Override
        public void showInfo() {
            System.out.println("의류 정보를 표시합니다.");
        }

        public void checkSize() {
            System.out.println("의류 사이즈를 확인합니다.");
        }
    }

    public class Main {
        public static void main(String[] args) {
            Product[] products = { new Book(), new Clothing() };

            for (Product p : products) {
                p.showInfo(); // 오버라이딩된 메서드 실행

                if (p instanceof Book) {
                    Book book = (Book) p;
                    book.readSample();
                } else if (p instanceof Clothing) {
                    Clothing clothing = (Clothing) p;
                    clothing.checkSize();
                }
            }
        }
    }
  

8. 다형성

다형성(Polymorphism)은 객체 지향 프로그래밍의 핵심 특징 중 하나로, 하나의 타입(부모)으로 다양한 형태의 객체(자식)를 다룰 수 있는 능력을 의미합니다.

앞서 학습한 타입 변환을 통해, 동일한 메서드를 호출하더라도 실제 참조하는 객체가 무엇인지에 따라 실행 결과가 달라집니다.

사용 사례

예를 들어, Employee(직원)라는 부모 클래스를 상속받는 여러 자식 클래스가 있을 때, Employee 타입 하나만으로 다양한 직급의 객체를 관리할 수 있습니다.

  • Manager 객체는 보고 기능을 수행
  • Engineer 객체는 개발 기능을 수행
  • Intern 객체는 보조 기능을 수행

이처럼 하나의 부모 타입 참조 변수로 다양한 자식 객체를 유연하게 처리할 수 있는 것이 다형성의 대표적인 활용 사례입니다.