본문 바로가기

Backend/Java

[Java] 선언만 했는데 왜 어떤 변수는 0이고, 어떤 변수는 에러일까? #Java 변수 초기화 순서 #static final

Java를 공부하다 보면 static, final, 지역 변수, 인스턴스 변수, 초기화 순서가 자꾸 섞입니다.

각각 어려운 개념이라기보다, 아직 익숙하지 않아서 비슷한 용어가 한꺼번에 들어오는 느낌에 가깝습니다.

이번 글은 깊게 파고들기보다, 코드를 볼 때 바로 확인할 수 있도록 핵심만 짧게 정리해보려고 합니다.


변수는 어디에 선언되었는지가 먼저다

class Sample {
    static int sharedCount;
    int age;

    void run() {
        int localCount;
    }
}

 

세 변수는 모두 int이지만 성격이 다릅니다.

 

종류 선언 위치 관리 단위 기본값
지역 변수 메서드·블록 내부 메서드 호출 없음
인스턴스 변수 클래스 내부, static 없음 객체 있음
static 변수 클래스 내부, static 사용 클래스 있음

 

지역 변수는 직접 초기화하지 않으면 사용할 수 없습니다.

void run() {
    int value;
    // System.out.println(value); // 컴파일 오류
}

 

반면 인스턴스 변수와 static 변수에는 JVM이 타입에 맞는 기본값을 넣습니다.

class Sample {
    int number;          // 0
    boolean active;      // false
    String name;         // null
    static int count;    // 0
}

 

 

인스턴스 변수는 객체마다 따로 존재한다

class Person {
    int age;
}

Person p1 = new Person();
Person p2 = new Person();

p1.age = 20;
p2.age = 30;
Heap
├── Person Object 1
│   └── age = 20
└── Person Object 2
    └── age = 30

  

 

age는 객체마다 별도로 존재합니다.

 

객체 하나의 값을 바꿔도 다른 객체에는 영향을 주지 않습니다.

static 변수는 객체가 아니라 클래스에 묶인다

class Person {
    static int count;
    int age;
}
Person p1 = new Person();
Person p2 = new Person();

Person.count = 2;

 

 

countp1, p2가 각각 가지는 값이 아닙니다.

Person 클래스 단위로 하나가 관리됩니다.

Person Class
└── static count = 2

Heap
├── Person Object 1
│   └── age
└── Person Object 2
    └── age

 

객체를 통해 접근하는 문법도 가능하지만 비추..

 

p1.count = 3;

 

이 코드는 p1 객체의 값을 바꾸는 것처럼 보이지만 실제로는 Person.count를 변경합니다.

그래서 static 멤버는 클래스 이름으로 접근하는 편이 의도가 더 분명합니다.

 

Person.count = 3;

 

static 메서드는 인스턴스 없이 호출할 수 있다

class Calculator {
    static int add(int a, int b) {
        return a + b;
    }
}

int result = Calculator.add(10, 20);

 

static 메서드는 특정 객체의 상태를 기준으로 실행되지 않습니다.

따라서 인스턴스 필드나 인스턴스 메서드에 바로 접근할 수 없습니다.

 

class Sample {
    int value = 10;

    static void print() {
        // System.out.println(value); // 컴파일 오류
    }
}

 

왜냐하면 print()가 호출될 때 어떤 Sample 객체의 value를 사용해야 하는지 알 수 없기 때문입니다.

final은 값을 상수로 만드는 키워드일까?

final의 핵심은 한 번 대입한 대상을 다시 대입하지 못하게 하는 것입니다.

기본형 변수

final int number = 10;
// number = 20; // 컴파일 오류

참조형 변수

final List<String> names = new ArrayList<>();
names.add("Java");

// names = new ArrayList<>(); // 컴파일 오류

 

참조형 변수에 final을 붙이면 참조가 가리키는 객체를 바꿀 수 없습니다.

하지만 객체 내부 상태까지 자동으로 불변이 되는 것은 아닙니다.

final reference
      │ 재할당 불가
      ▼
ArrayList Object
      └── 내부 값 변경 가능

 

static final은 항상 컴파일 상수일까?

아닙니다.

static final int MAX_COUNT = 100;

 

기본형 또는 String이며, 컴파일 시점에 값이 확정되는 표현식이라면 컴파일 상수가 될 수 있습니다.

하지만 다음 값들은 런타임에 결정됩니다.

 

static final int TIMEOUT = Integer.parseInt("30");
static final LocalDate TODAY = LocalDate.now();

 

둘 다 재할당할 수는 없지만, 컴파일 시점에 값이 확정되는 상수와는 다릅니다.

초기화 순서는 어떻게 볼까?

전체 세부 과정은 ClassLoader 편에서 다시 살펴보겠지만, 지금은 크게 두 흐름만 기억하면 됩니다.

클래스 단위 초기화

class Sample {
    static int a = 10;

    static {
        a = 20;
    }
}

// 생성 순서
// 10
// 20

 

클래스가 초기화될 때 static 필드의 명시적 초기화와 static 초기화 블록이 소스에 작성된 순서대로 실행됩니다.

객체 단위 초기화

class Sample {
    int value = 10;

    {
        value = 20;
    }

    Sample() {
        value = 30;
    }
}

// 호출 및 출력 순서
// 10
// 20
// 30

 

객체를 생성할 때는 개념적으로 다음 순서로 이어집니다.

인스턴스 필드 기본값
→ 명시적 초기화와 인스턴스 초기화 블록
→ 생성자

 

상속 관계에서는 부모 객체 부분의 초기화가 먼저 진행된다는 점도 함께 기억해두면 좋습니다.

class Sample {

    static int a = print("static field");

    static {
        print("static block");
    }

    int value = print("instance field");

    {
        print("instance block");
    }

    Sample() {
        print("constructor");
    }

    static String print(String s) {
        System.out.println(s);
        return s;
    }
}

// 정리
클래스 로딩/초기화
    ↓
static 변수 초기화
    ↓
static block 실행
    ↓
(이후 new 할 때마다)
    ↓
instance 변수 초기화
    ↓
instance block 실행
    ↓
생성자 실행

 

한 번에 정리!

지역 변수
→ 메서드·블록 안에 선언
→ 기본값 없음
→ 사용 전에 직접 초기화

인스턴스 변수
→ Heap 객체마다 존재
→ 기본값 있음

static 변수
→ 클래스 단위로 하나만 관리
→ 객체 생성 전에도 클래스가 준비되면 사용 가능

final
→ 재대입 제한
→ 참조형 객체의 내부 상태까지 불변으로 만들지는 않음

 

이번 글에서는 제가 자주 헷갈리는 부분만 빠르게 정리했습니다.