다형성은 하나의 객체가 여러 유형으로 사용될 수 있다는 개념이에요. 자바에서는 상속을 기반으로 동작하며, 부모 클래스로 선언된 변수에 자식 클래스의 인스턴스를 할당할 수 있습니다. 이를 활용하면 코드의 재사용성과 유지보수성이 높아지고, 다양한 객체를 유연하게 사용할 수 있습니다.
비유: 부모 클래스를 "컵"이라고 생각해 봅시다. 컵은 "물"도, "주스"도, "커피"도 담을 수 있죠. 컵이 어떤 종류의 액체를 담고 있든지 컵으로써의 기능(잡고 마시는 기능)은 변하지 않습니다.
마찬가지로
Person타입의 변수는Student,Teacher,Employee객체를 담을 수 있습니다. 하지만Person의 기능을 따르는 방식으로 사용됩니다.
코드 예시
Person p; // p는 Person 타입이므로 하위 객체를 담을 수 있음
p = new Student("허현수", 17, "20160001");
p = new Teacher("허현준", 22, "Java Programming");
p = new Employee("허현정", 23, "교무처");
비유: 부모 클래스는 "TV 리모컨"이고, 자식 클래스는 "TV 기기"라고 생각해 봅시다.
리모컨으로 TV를 조작하면, 어떤 TV를 연결했느냐에 따라 TV의 화면이 달라지는 것처럼
자식 객체가 부모 변수에 저장되었을 때 부모가 가진 메서드를 자식이 재정의하면, 실행 시 자식 클래스의 메서드가 호출됩니다.
코드 예시
Person p = new Student("허현수", 17, "20160001");
System.out.println(p.getDetails()); // Student의 getDetails() 실행됨!
p = new Teacher("허현준", 22, "Java Programming");
System.out.println(p.getDetails()); // Teacher의 getDetails() 실행됨!
텍스트 그림
메모리 구조 (Virtual Method Invocation)
Person p 참조 변수 (Stack)
┌──────────────────────────┐
│ 참조값 → Student 객체 │
└──────────────────────────┘
Heap 메모리
┌──────────────────────────┐
│ Student 객체 │
│ getDetails() 재정의됨 │ <-- 호출
└──────────────────────────┘
비유: 다양한 종류의 물건을 담을 수 있는 "가방"이라고 생각해 봅시다.
보통 "가방" 안에는 책, 연필, 노트 같은 같은 종류의 물건이 들어갑니다. 하지만 가방을 범용적으로 만든다면 책, 연필뿐만 아니라 노트북, 물통, 간식도 넣을 수 있죠!
즉,
Person[]배열도 원래는Person객체만 담을 수 있지만, 다형성을 이용하면 다양한 객체를 담을 수 있습니다!
코드 예시
Person[] pArr = new Person[4];
pArr[0] = new Person("홍길동", 20);
pArr[1] = new Student("허현수", 17, "20160001");
pArr[2] = new Teacher("허현준", 22, "Java Programming");
pArr[3] = new Employee("허현정", 23, "교무처");
for(Person p : pArr) {
System.out.println(p.getDetails());
}
텍스트 그림
pArr 배열 (Person 타입 배열)
┌───────────────────┬───────────────────┬───────────────────┬───────────────────┐
│ pArr[0] (Person) │ pArr[1] (Student) │ pArr[2] (Teacher) │ pArr[3] (Employee) │
├───────────────────┼───────────────────┼───────────────────┼───────────────────┤
│ "홍길동", 20 │ "허현수", 17, 학번 │ "허현준", 22, 과목 │ "허현정", 23, 부서 │
└───────────────────┴───────────────────┴───────────────────┴───────────────────┘
비유: 영화관에서 티켓을 검사할 때를 생각해봅시다!
직원은 “티켓을 가진 사람”이면 누구든지 입장할 수 있도록 합니다.
그런데 영화 티켓을 가진 사람은 일반 관객, VIP, 특별 초대 손님일 수도 있죠!
즉, 다형적 인자를 이용하면 메서드 하나로 다양한 유형의 객체를 처리할 수 있습니다.
코드 예시
public static void printPersonInfo(Person p) {
System.out.println(p.getDetails()); // 다양한 객체를 처리 가능!
}
printPersonInfo(new Student("허현수", 17, "20001234"));
printPersonInfo(new Teacher("허현준", 22, "Java Programming"));
printPersonInfo(new Employee("허현정", 23, "교무처"));
instanceof (객체 타입 확인)💡 비유: 여러 종류의 동물을 관찰할 때, 동물마다 특징이 다르죠!
동물 중 일부는 ‘날개가 있다’ → 새일 가능성이 높음
동물 중 일부는 ‘땅을 파고 다닌다’ → 두더지일 가능성이 높음
이를 컴퓨터적으로 검사하는 방법이
instanceof입니다.
코드 예시
if (p instanceof Student) {
System.out.println("이 객체는 Student 타입입니다!");
} else if (p instanceof Teacher) {
System.out.println("이 객체는 Teacher 타입입니다!");
}
텍스트 그림
p가 참조하는 객체 타입 체크
p instanceof Student → true ✅
p instanceof Teacher → false ❌
Casting형변환은 부모 타입의 객체를 자식 타입으로 변환하여 자식 클래스의 메서드나 속성에 접근할 수 있도록 하는 과정이에요.
📌 코드 예시
Student stu = (Student) p; // Person 타입을 Student 타입으로 변환
stu.applyForClasses(); // Student 클래스에서 정의한 메서드 호출
📌 텍스트 그림
[ Person p ]
┌──────────┐
│ Student 객체 │ ← (Student) 형변환 가능 ✅
└──────────┘
💡 왜 형변환이 필요한가?
p가 Person 타입이기 때문에, Person이 제공하는 기능만 사용할 수 있어요.p가 실제로 Student 객체를 참조하고 있다면, Student의 기능을 사용하려면 형변환을 해야 합니다!instanceof와 함께 사용하는 형변환 (자바 12 이후)자바 12부터 instanceof를 사용할 때 형변환을 별도로 할 필요 없이 한 번에 처리할 수 있도록 변경되었습니다.
📌 기존 방식 (자바 12 이전)
if (p instanceof Student) {
Student stu = (Student) p; // 형변환 필요
stu.applyForClasses();
}
📌 새로운 방식 (자바 12 이후)
if (p instanceof Student stu) { // stu 변수를 자동으로 형변환하여 사용 가능!
System.out.println("************ Student Info ***********");
stu.applyForClasses(); // 형변환 없이 직접 메서드 호출 가능!
}
💡 이 방식의 장점
instanceof 검사와 형변환을 한 줄로 처리할 수 있어 코드가 더 깔끔해집니다.stu를 즉시 사용할 수 있습니다.📌 텍스트 그림
[ Person p ]
┌──────────┐
│ Student 객체 │ ← `instanceof Student stu`로 자동 형변환 ✅
└──────────┘
static 키워드는 객체 간 공유되는 변수와 메서드를 정의할 때 사용됩니다.📌 핵심 개념
일반 변수(non-static)와 static 변수 비교
일반 변수: 객체마다 각각 다른 값을 가짐
static 변수: 모든 객체가 공통된 값을 공유
코드 예시
package static_;
public class Count {
public int a = 0; // 일반 변수
public static int b = 0; // static 변수
}
static 변수 활용
package static_;
public class StaticVarExample {
public static void main(String[] args) {
Count c1 = new Count();
c1.a++; // 객체마다 별개의 변수 증가
c1.b++; // 모든 객체가 공유하는 static 변수 증가
Count c2 = new Count();
c2.a++; // c2의 인스턴스 변수 증가
c2.b++; // static 변수 증가 (모든 객체 공유)
Count.b++; // 클래스 이름으로 직접 접근 가능!
System.out.println("c1.b: " + c1.b);
System.out.println("c2.b: " + c2.b);
System.out.println("Count.b: " + Count.b);
}
}

텍스트 그림 – 메모리 구조
[ Heap 영역 (각 객체 별도 저장) ]
┌─────────────┬─────────────┐
│Count객체(c1)│Count 객체(c2)│
│ a = 1 │ a = 1 │
└─────────────┴─────────────┘
[ Static 영역 (모든 객체 공유) ]
┌───────────────┐
│ static int b = 3 │ <-- 모든 객체가 공유 ✅
└───────────────┘
결론: b는 모든 객체가 공유하는 값이므로 Count.b++를 호출하면 모든 객체에 영향을 줍니다!
📌 코드 예시
package static_;
public class Count {
public int a = 0;
public static int b = 0;
public static int doIt() {
// return ++a; // ❌ 오류 발생 (non-static 변수 접근 불가)
return ++b; // ✅ static 변수 접근 가능
}
}
📌 static 메서드 호출
package static_;
public class StaticMethodExample {
public static void main(String[] args) {
System.out.println("Count.doIt(): " + Count.doIt()); // 객체 없이 호출 가능
System.out.println("Count.doIt(): " + Count.doIt());
System.out.println("Count.doIt(): " + Count.doIt());
}
}
📌 텍스트 그림
[ Count 클래스 ]
┌───────────────────┐
│ static int b │
│ static doIt() { │
│ return ++b; │ ← static 변수만 접근 가능 ✅
│ } │
└───────────────────┘
static 메서드는 non-static 변수(a)를 직접 접근할 수 없습니다!
📌 코드 예시
package static_;
public class StaticInit {
static {
System.out.println("static initializer가 수행됨!");
}
public StaticInit() {
System.out.println("Constructor 호출됨!");
}
}
📌 static 블록 실행 확인
package static_;
public class StaticInitExample {
public static void main(String[] args) {
StaticInit s1 = new StaticInit(); // 객체 생성 시 생성자 실행
StaticInit s2 = new StaticInit();
System.out.println("main() 메서드 종료");
}
}
📌 출력 결과
static initializer가 수행됨! ✅ (클래스 로딩 시 한 번 실행)
Constructor 호출됨!
Constructor 호출됨!
main() 메서드 종료
💡 static 블록은 main()보다 먼저 실행되며, 클래스 로딩 시 한 번만 실행됩니다!
📌 코드 예시
package static_;
public class Singleton {
private static Singleton instance = new Singleton(); // static 인스턴스
private Singleton() {
System.out.println("instance created!");
}
public static Singleton getInstance() { // static 메서드
return instance; // 모든 요청에서 동일한 객체 반환
}
}
📌 싱글톤 패턴 실행
package static_;
public class SingletonExample {
public static void main(String[] args) {
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1);
System.out.println(s1 == s2); // true! ✅
System.out.println(s2);
}
}
📌 텍스트 그림
싱글톤 패턴 메모리 구조
Singleton 클래스 │ private static instance │ ← 모든 요청에서 같은 객체 반환 ✅
Singleton.getInstance() 호출
s1: ┌───────────┐
│ Singleton 객체 │ ← 동일한 객체! ✅
└───────────┘
s2: ┌───────────┐
│ Singleton 객체 │ ← 동일한 객체! ✅
└───────────┘
싱글톤 패턴을 사용하면 getInstance()를 호출할 때마다 동일한 객체를 반환합니다!