6.4 static과 final 키워드 심화
static과 final은 자바에서 가장 자주 쓰이는 키워드 중 하나입니다. 각각의 의미와 용도를 정확히 이해하면 더 깔끔하고 효율적인 코드를 작성할 수 있습니다.
1. static 키워드
static이 붙으면 해당 멤버는 클래스에 속하는 것이 됩니다. 인스턴스(객체)를 만들지 않아도 사용할 수 있습니다.
1.1 static 변수 (클래스 변수)
static 변수는 모든 인스턴스가 하나의 값을 공유합니다. JVM에서 클래스가 로딩될 때 딱 한 번만 메모리에 올라갑니다.
public class Counter {
static int count = 0; // 모든 객체가 공유하는 변수
String name; // 각 객체마다 별도로 갖는 변수
Counter(String name) {
this.name = name;
count++; // 객체가 생성될 때마다 공유 카운터 증가
}
}
public class StaticTest {
public static void main(String[] args) {
Counter c1 = new Counter("첫 번째");
Counter c2 = new Counter("두 번째");
Counter c3 = new Counter("세 번째");
System.out.println(Counter.count); // 3 (클래스명으로 직접 접근!)
System.out.println(c1.count); // 3 (인스턴스로도 접근 가능, 비권장)
}
}
언제 쓰나? 모든 객체가 공유해야 하는 상태(생성된 객체 수, 공통 설정값 등)에 사용합니다.
1.2 static 메서드
객체 없이 호출할 수 있는 메서드입니다. this 키워드를 사용할 수 없고, 인스턴스 변수에도 접근할 수 없습니다.
public class MathHelper {
// static 메서드: 객체 없이 클래스명으로 바로 호출
public static int add(int a, int b) {
return a + b;
}
public static double circleArea(double radius) {
return Math.PI * radius * radius;
}
}
// 객체 생성 없이 사용!
int result = MathHelper.add(10, 20); // 30
double area = MathHelper.circleArea(5); // 78.53...
언제 쓰나? 유틸리티 메서드(특정 상태 없이 입력만으로 결과를 내는 순수 함수),
main()메서드처럼 인스턴스에 독립적인 동작에 사용합니다.
1.3 static 초기화 블록
복잡한 static 변수 초기화가 필요할 때 사용합니다. 클래스가 JVM에 처음 로딩될 때 딱 한 번 실행됩니다.
public class Config {
static final String DB_URL;
static final int MAX_CONNECTIONS;
// static 초기화 블록
static {
System.out.println("설정값 로딩 중...");
DB_URL = System.getenv("DB_URL") != null
? System.getenv("DB_URL")
: "jdbc:mysql://localhost:3306/mydb";
MAX_CONNECTIONS = 10;
System.out.println("설정 완료!");
}
}
1.4 static import
자주 쓰는 static 멤버를 클래스명 없이 사용할 수 있습니다.
import static java.lang.Math.*; // Math의 모든 static 멤버 임포트
import static java.util.Collections.*;
// 이제 Math. 없이 바로 사용 가능
double result = sqrt(pow(3, 2) + pow(4, 2)); // Math.sqrt, Math.pow 대신
System.out.println(result); // 5.0
2. final 키워드
final은 "더 이상 변경할 수 없다"는 의미입니다.
2.1 final 변수 (상수)
한 번 값이 할당되면 다시는 변경할 수 없습니다. 관례적으로 상수는 대문자와 밑줄로 이름을 짓습니다.
public class Constants {
// static final 조합: 진정한 의미의 상수
public static final double PI = 3.14159265358979;
public static final int MAX_SIZE = 100;
public static final String APP_NAME = "My Java App";
}
// 사용
System.out.println(Constants.PI); // 3.14159265358979
// Constants.PI = 3.14; // ❌ 컴파일 에러! 변경 불가
지역 변수에서의 final:
public void processData(final int size) {
final int limit = size * 2; // 한 번 설정 후 변경 불가
// limit = 100; // ❌ 컴파일 에러
// 람다식 안에서 외부 변수를 쓰려면 effectively final이어야 함
Runnable r = () -> System.out.println("크기: " + limit); // OK
}
2.2 final 메서드
자식 클래스에서 오버라이딩(재정의)을 금지합니다.
class Animal {
final void breathe() {
System.out.println("산소를 마십니다."); // 모든 동물의 공통 행동, 변경 불가
}
void sound() {
System.out.println("...");
}
}
class Dog extends Animal {
// breathe()를 오버라이딩하려 하면 컴파일 에러!
// @Override void breathe() { } // ❌
@Override
void sound() { // sound()는 오버라이딩 가능
System.out.println("멍멍!");
}
}
2.3 final 클래스
상속 자체를 금지합니다. String, Integer 등 자바 핵심 클래스들이 final인 이유가 바로 이것입니다 — 누군가 상속해서 동작을 변경하는 것을 막아 보안과 불변성을 보장합니다.
final class ImmutablePoint {
private final int x;
private final int y;
ImmutablePoint(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() { return x; }
public int getY() { return y; }
}
// class SpecialPoint extends ImmutablePoint { } // ❌ 컴파일 에러!
static과 final의 실전 활용 패턴:
-
상수 클래스 패턴: 관련 상수들을 하나의
final class에 모아둡니다.public final class HttpStatus {
public static final int OK = 200;
public static final int NOT_FOUND = 404;
public static final int SERVER_ERROR = 500;
private HttpStatus() {} // 인스턴스 생성 방지
} -
람다와 effectively final: 변수에
final을 명시하지 않아도 람다 안에서 사용되는 지역 변수는 사실상 final(effectively final) 이어야 합니다. 즉, 한 번 할당 후 값이 바뀌지 않으면 컴파일러가 자동으로 허용합니다. -
성능 팁: JVM은
static final상수를 컴파일 시점에 인라인(inline)할 수 있어서,static final int MAX = 100처럼 기본 타입 상수는 특히 빠릅니다.