
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;
count는 p1, 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
→ 재대입 제한
→ 참조형 객체의 내부 상태까지 불변으로 만들지는 않음
이번 글에서는 제가 자주 헷갈리는 부분만 빠르게 정리했습니다.