Java 생성자, 상속, 실행 순서 완전 정복
Java 생성자의 동작 원리부터 상속 관계에서의 실행 순서까지, 헷갈리는 개념을 코드와 다이어그램으로 정리합니다.
생성자란?
생성자(Constructor)는 객체가 만들어질 때 딱 한 번 자동으로 호출되는 특수 메서드다. 목적은 하나 — 객체의 초기 상태를 설정하는 것.
비유: 생성자는 새 건물을 지을 때 쓰는 설계도 겸 인테리어 공사다. 건물(객체)이 완성되는 순간 공사(생성자)가 실행되고, 이후엔 일반 메서드로만 상태를 바꿀 수 있다.
class Car {
String model;
int speed;
// 생성자: 클래스 이름과 동일, 반환 타입 없음
Car(String model, int speed) {
this.model = model; // this = 지금 만들어지는 이 객체
this.speed = speed;
}
}
Car myCar = new Car("Tesla", 100);
// new → 메모리 할당 → 생성자 호출 → 객체 완성
생성자의 규칙
| 규칙 | 설명 |
|---|---|
| 클래스 이름과 동일 | Car 클래스의 생성자는 Car() |
| 반환 타입 없음 | void도 쓰지 않는다 |
new 키워드로만 호출 |
일반 메서드처럼 직접 호출 불가 |
| 여러 개 가능 | 매개변수가 다르면 여러 개 선언 가능 (오버로딩) |
기본 생성자 vs 매개변수 생성자
기본 생성자 (Default Constructor)
생성자를 하나도 안 만들면 컴파일러가 자동으로 빈 생성자를 추가한다.
class Dog {
String name;
// 생성자를 선언하지 않음
}
// 컴파일러가 이걸 자동으로 추가:
// Dog() {}
Dog d = new Dog(); // 가능
주의: 매개변수 있는 생성자를 하나라도 직접 만들면, 기본 생성자는 자동 추가되지 않는다.
class Dog {
String name;
Dog(String name) { // 직접 선언
this.name = name;
}
}
Dog d1 = new Dog("바둑이"); // OK
Dog d2 = new Dog(); // 컴파일 에러! 기본 생성자 없음
기본 생성자도 필요하다면 직접 추가해야 한다.
class Dog {
String name;
Dog() { this.name = "이름없음"; } // 직접 추가
Dog(String name) { this.name = name; }
}
this 와 this()
this — 자기 자신을 가리키는 참조
class Person {
String name;
int age;
Person(String name, int age) {
// 매개변수 name과 필드 name이 이름 충돌
this.name = name; // this.name = 필드, name = 매개변수
this.age = age;
}
}
this() — 같은 클래스의 다른 생성자 호출
생성자 안에서 다른 생성자를 재사용할 수 있다. 반드시 첫 번째 줄에 있어야 한다.
class Person {
String name;
int age;
String job;
Person(String name, int age) {
this.name = name;
this.age = age;
this.job = "무직";
}
Person(String name, int age, String job) {
this(name, age); // 위의 생성자를 재사용 (첫 줄이어야 함)
this.job = job;
}
}
new Person("김철수", 30, "개발자") 호출 시:
→ Person(name, age, job) 진입
→ this(name, age) → Person(name, age) 먼저 실행
→ job = "개발자" 설정
상속 (Inheritance)
상속이란?
부모 클래스의 필드와 메서드를 자식 클래스가 물려받는 것. 공통 코드를 중복 없이 재사용하고, 기능을 확장할 수 있다.
비유: 부모님의 재산(코드)을 자식이 물려받되, 자식은 자신만의 재산을 추가할 수 있다.
// 부모 클래스
class Animal {
String name;
Animal(String name) {
this.name = name;
System.out.println("Animal 생성자: " + name);
}
void breathe() {
System.out.println(name + " 숨 쉬는 중");
}
}
// 자식 클래스 — extends로 상속
class Dog extends Animal {
String breed;
Dog(String name, String breed) {
super(name); // 부모 생성자 호출 (반드시 첫 줄)
this.breed = breed;
System.out.println("Dog 생성자: " + breed);
}
void bark() {
System.out.println(name + " 왈왈!"); // 부모 필드 사용 가능
}
}
Dog dog = new Dog("바둑이", "진돗개");
출력:
Animal 생성자: 바둑이 ← 부모 먼저
Dog 생성자: 진돗개 ← 자식 나중
상속 관계 구조
Animal
┌──────────────┐
│ name │
│ breathe() │
└──────┬───────┘
│ extends
┌──────▼───────┐
│ Dog │
│ breed │ ← Dog만의 추가 필드
│ breathe() │ ← Animal에서 상속
│ bark() │ ← Dog만의 추가 메서드
└──────────────┘
Java 상속의 제약
- 단일 상속만 가능:
extends는 하나만 (다중 상속 불가) private멤버는 상속되지 않음 (존재하지만 직접 접근 불가)final클래스는 상속 불가 (String이 대표적 예)
super 와 super()
super() — 부모 생성자 호출
자식 생성자의 첫 번째 줄에 위치해야 한다.
명시적으로 쓰지 않으면 컴파일러가 super() (부모 기본 생성자 호출)를 자동 삽입한다.
class Animal {
String name;
// 기본 생성자 없음! 매개변수 생성자만 있음
Animal(String name) {
this.name = name;
}
}
class Dog extends Animal {
Dog() {
// 컴파일러가 super() 삽입 시도 → Animal에 기본 생성자 없어서 에러!
}
}
부모가 기본 생성자가 없으면 자식은 반드시 super(인자)를 명시해야 한다.
class Dog extends Animal {
Dog(String name) {
super(name); // 명시적으로 부모 생성자 호출
}
}
super — 부모의 필드/메서드에 접근
자식과 부모에 같은 이름이 있을 때 구분하는 용도.
class Animal {
String name = "동물";
void speak() {
System.out.println("...");
}
}
class Dog extends Animal {
String name = "개"; // 부모와 같은 이름
void showNames() {
System.out.println(name); // "개" (자식)
System.out.println(super.name); // "동물" (부모)
}
@Override
void speak() {
super.speak(); // 부모 메서드 호출
System.out.println("왈왈!"); // 추가 동작
}
}
메서드 오버라이딩 (Overriding)
부모에게 물려받은 메서드를 자식이 재정의하는 것.
class Shape {
double area() {
return 0;
}
}
class Circle extends Shape {
double radius;
Circle(double radius) {
this.radius = radius;
}
@Override // 오버라이딩 명시 (생략 가능하지만 붙이는 게 좋다)
double area() {
return Math.PI * radius * radius;
}
}
class Rectangle extends Shape {
double width, height;
Rectangle(double width, double height) {
this.width = width;
this.height = height;
}
@Override
double area() {
return width * height;
}
}
오버라이딩 규칙
| 항목 | 규칙 |
|---|---|
| 메서드 이름 | 동일해야 함 |
| 매개변수 | 동일해야 함 |
| 반환 타입 | 동일하거나 자식 타입(공변 반환) |
| 접근 제어자 | 부모보다 좁아질 수 없음 (public → private 불가) |
| 예외 | 부모보다 넓은 예외 추가 불가 |
오버로딩 vs 오버라이딩 — 헷갈리지 않기
class Calculator {
// 오버로딩(Overloading): 같은 클래스, 같은 이름, 다른 매개변수
int add(int a, int b) { return a + b; }
double add(double a, double b) { return a + b; }
// → 컴파일 시점에 어떤 메서드 쓸지 결정 (정적 바인딩)
}
class SmartCalculator extends Calculator {
// 오버라이딩(Overriding): 부모 메서드를 자식이 재정의
@Override
int add(int a, int b) {
System.out.println("더하기 실행");
return a + b;
}
// → 런타임에 어떤 메서드 쓸지 결정 (동적 바인딩)
}
초기화 블록 (Initializer Block)
생성자 외에 객체/클래스를 초기화하는 두 가지 블록이 있다.
인스턴스 초기화 블록 {}
모든 생성자가 공통으로 실행해야 할 코드를 넣는다. 생성자보다 먼저 실행된다.
class Server {
String host;
int port;
long startTime;
// 인스턴스 초기화 블록 — 모든 생성자 실행 전에 항상 실행
{
startTime = System.currentTimeMillis();
System.out.println("서버 초기화 블록 실행");
}
Server() {
this.host = "localhost";
this.port = 8080;
}
Server(String host, int port) {
this.host = host;
this.port = port;
}
}
static 초기화 블록 static {}
클래스가 JVM에 처음 로딩될 때 딱 한 번 실행된다. static 변수 초기화에 사용.
class DatabaseConfig {
static String url;
static String driver;
// static 초기화 블록 — 클래스 로딩 시 1회 실행
static {
System.out.println("DB 설정 로딩");
url = "jdbc:mysql://localhost:3306/mydb";
driver = "com.mysql.jdbc.Driver";
}
}
// DatabaseConfig.url 처음 접근하는 순간 static 블록 실행
System.out.println(DatabaseConfig.url);
실행 순서 — 핵심
이게 가장 헷갈리는 부분이다. 상속 관계에서 객체 생성 시 순서를 정확히 알아야 한다.
단일 클래스의 초기화 순서
1. static 변수 초기화 (클래스 로딩 시 1회)
2. static 초기화 블록 (클래스 로딩 시 1회)
───── 이하는 new 할 때마다 ─────
3. 인스턴스 변수 기본값 설정 (int → 0, String → null...)
4. 인스턴스 변수 선언부 초기화
5. 인스턴스 초기화 블록 {}
6. 생성자
class Example {
static int staticVar = 10; // 1. static 변수
static {
System.out.println("static 블록"); // 2. static 블록
}
int instanceVar = 20; // 4. 인스턴스 변수
{
System.out.println("인스턴스 블록"); // 5. 인스턴스 블록
}
Example() {
System.out.println("생성자"); // 6. 생성자
}
}
// new Example() 시 출력:
// static 블록 ← 클래스 최초 로딩 시 1회
// 인스턴스 블록 ← new 할 때마다
// 생성자 ← new 할 때마다
상속 관계의 실행 순서
핵심 규칙: 항상 부모부터 완전히 초기화된 후 자식이 초기화된다.
1. 부모 static 변수 & static 블록
2. 자식 static 변수 & static 블록
───── 이하는 new 할 때마다 ─────
3. 부모 인스턴스 변수 초기화
4. 부모 인스턴스 블록
5. 부모 생성자
6. 자식 인스턴스 변수 초기화
7. 자식 인스턴스 블록
8. 자식 생성자
코드로 확인:
class Animal {
static { System.out.println("1. Animal static 블록"); }
String name;
{ System.out.println("3. Animal 인스턴스 블록"); }
Animal(String name) {
this.name = name;
System.out.println("4. Animal 생성자: " + name);
}
}
class Dog extends Animal {
static { System.out.println("2. Dog static 블록"); }
String breed;
{ System.out.println("5. Dog 인스턴스 블록"); }
Dog(String name, String breed) {
super(name); // 부모 생성자 호출 → 3, 4번 실행
this.breed = breed;
System.out.println("6. Dog 생성자: " + breed);
}
}
// new Dog("바둑이", "진돗개") 실행 시 출력:
// 1. Animal static 블록 ← 클래스 로딩 시 1회
// 2. Dog static 블록 ← 클래스 로딩 시 1회
// 3. Animal 인스턴스 블록
// 4. Animal 생성자: 바둑이
// 5. Dog 인스턴스 블록
// 6. Dog 생성자: 진돗개
3단계 상속의 실행 순서
class A {
A() { System.out.println("A 생성자"); }
}
class B extends A {
B() {
super(); // 컴파일러가 자동 삽입
System.out.println("B 생성자");
}
}
class C extends B {
C() {
super(); // 컴파일러가 자동 삽입
System.out.println("C 생성자");
}
}
new C();
// 출력:
// A 생성자 ← 최상위 부모부터
// B 생성자
// C 생성자 ← 자식이 마지막
new C() 호출
→ C() 진입
→ super() → B() 진입
→ super() → A() 진입
→ "A 생성자" 출력
→ A() 완료
→ "B 생성자" 출력
→ B() 완료
→ "C 생성자" 출력
→ C() 완료
오버라이딩과 실행 순서 — 주의할 함정
부모 생성자에서 오버라이딩된 메서드를 호출하면 예상과 다른 결과가 나올 수 있다.
class Parent {
int value = 10;
Parent() {
print(); // 이 시점에 자식 오버라이딩 메서드가 호출됨!
}
void print() {
System.out.println("Parent.value = " + value);
}
}
class Child extends Parent {
int value = 99; // 자식의 value
Child() {
super(); // 부모 생성자 호출
}
@Override
void print() {
// 부모 생성자 실행 시점에 자식 value는 아직 초기화 안 됨!
System.out.println("Child.value = " + value);
}
}
new Child();
// 출력: Child.value = 0 ← 99가 아님!
// 이유: 부모 생성자 실행 시점엔 자식 필드가 아직 기본값(0)
생성자에서 오버라이딩 가능한 메서드를 호출하지 않는 것이 안전하다.
전체 흐름 요약 다이어그램
new Dog("바둑이", "진돗개") 호출
│
├── [클래스 첫 로딩 시]
│ ├── Animal static 변수/블록
│ └── Dog static 변수/블록
│
└── [매번 new 할 때]
├── Dog() 생성자 진입
│ └── super("바둑이") 호출
│ ├── Animal 인스턴스 변수 초기화
│ ├── Animal 인스턴스 블록
│ └── Animal("바둑이") 생성자 실행 ✓
├── Dog 인스턴스 변수 초기화
├── Dog 인스턴스 블록
└── Dog("바둑이", "진돗개") 나머지 실행 ✓
정리
| 개념 | 핵심 한 줄 |
|---|---|
| 생성자 | new 시 자동 호출, 객체 초기 상태 설정 |
| 기본 생성자 | 직접 생성자 없으면 자동 추가. 하나라도 만들면 자동 추가 안 됨 |
| this() | 같은 클래스의 다른 생성자 호출. 반드시 첫 줄 |
| super() | 부모 생성자 호출. 반드시 첫 줄. 없으면 컴파일러가 자동 삽입 |
| 오버라이딩 | 부모 메서드를 자식이 재정의. 런타임에 실제 타입으로 결정 |
| 인스턴스 블록 | 모든 생성자 실행 전에 먼저 실행 |
| static 블록 | 클래스 로딩 시 딱 1회 실행 |
| 실행 순서 | static → 부모 인스턴스 블록 → 부모 생성자 → 자식 인스턴스 블록 → 자식 생성자 |
| 주의 | 생성자에서 오버라이딩 가능한 메서드 호출 금지 — 자식 필드 미초기화 상태 |