학습 자료

static 과 final — 객체 없이 존재하는 것


Article

책이 몇 권 만들어졌는지 세고 싶다. 생성자에서 하나씩 올리면 될 것 같다.

class Book {
    int count = 0;

    Book() {
        count++;
    }
}
new Book();
new Book();
Book b = new Book();

System.out.println(b.count);
콘솔
  • 1

세 권을 만들었는데 1 이다.

당연하다. 11강에서 봤듯 필드는 객체마다 하나씩 생긴다. count 도 책마다 하나씩 있다. 각자 자기 것을 0 에서 1 로 올렸을 뿐이다.

세 권이 함께 쓰는 값이 필요하다.

static — 객체가 아니라 클래스에 붙는다

필드 앞에 static 을 붙인다.

class Book {
    static int total = 0;
    int mine = 0;

    Book() {
        total++;
        mine++;
    }
}
new Book();
new Book();
new Book();

System.out.println(Book.total);
콘솔
  • 3

Book.total 이다. b.total 이 아니라 클래스 이름으로 꺼낸다.

static 이 붙은 것은 객체에 속하지 않는다. 클래스에 딱 하나만 있다. 객체를 하나도 안 만들어도 존재한다.

static 필드는 하나, 인스턴스 필드는 객체마다

new Book() 을 세 번 한 뒤

Book.total
3
클래스에 하나뿐 — 셋이 함께 올렸다
book1.mine
1
book2.mine
1
book3.mine
1
static공유하고, 그냥 필드는 각자 갖는다.

static 이 아닌 필드를 인스턴스 필드라고 부른다. 인스턴스는 객체의 다른 이름이다.

객체 없이 부르는 메서드

메서드에도 붙는다. static 메서드는 객체를 만들지 않고 부른다.

class MathUtil {
    static int square(int n) {
        return n * n;
    }
}
System.out.println(MathUtil.square(5));   // 25

new MathUtil() 을 하지 않았다. 필요가 없다. square 는 객체의 상태를 전혀 쓰지 않기 때문이다. 넣은 값만으로 답이 나온다.

이런 도구 메서드에 static 을 붙인다. Math.round(), Arrays.toString(), Integer.parseInt() 가 전부 그렇다. 9강에서 Arrays.toString(a) 를 쓸 때 new Arrays() 를 하지 않았던 이유다.

2강의 System.out

2강에서 이런 말을 흘려뒀다.

System 을 객체로 만든 적이 없는데 System.out 을 바로 꺼내 쓴 게 이상하지 않은가?

이제 답할 수 있다. outSystem 클래스의 static 필드다. 클래스에 하나뿐이고, 객체 없이 클래스 이름으로 꺼낼 수 있다.

System.out.println("안녕") 을 다시 읽어보자. System 클래스에서 static 필드 out 을 꺼내고, 그 out 객체에게 println 을 시킨다. 마지막 printlnstatic 이 아니다.

main 이 static 인 이유

2강에서 미뤄둔 마지막 단어다. 열한 강의를 미뤘다.

JVM 이 프로그램을 시작할 때를 생각해보자. Hello.class 를 읽고 main 을 찾아 실행해야 한다. 그런데 mainstatic 이 아니라면?

객체가 있어야 부를 수 있다. 그럼 JVM 은 new Hello() 를 해야 한다. 그런데 생성자에 매개변수가 있으면? 무슨 값을 넣어야 하나. 생성자가 private 이면?

JVM 은 객체를 만들지 않고 부를 방법이 필요했다. 그래서 mainstatic 이다.

java Hello 를 쳤을 때 — 이제 전부 읽힌다
  1. JVM 이 Hello.class 를 읽는다1강
  2. main 을 찾는다2강 — 약속된 이름
  3. 객체 없이 부른다static 이라서 가능하다
  4. public 이라 밖에서 부를 수 있다12강

2강의 그 다섯 단어가 이제 전부 설명됐다. public(12강) · static(여기) · void(10강) · main(2강) · String[] args(9강).

10강의 그 에러

10강 끝에서 만난 에러를 다시 본다.

public class Main {
    void hi() { }

    public static void main(String[] args) {
        hi();
    }
}
터미널
  • error: non-static method hi() cannot be referenced from a static context

이제 읽힌다. mainstatic 이라 객체 없이 돌고 있다. 그런데 hi() 는 인스턴스 메서드라 객체가 있어야 한다.

main 에게는 그 객체가 없다. this 가 없다. 그래서 부를 수 없다.

고치는 방법은 두 가지다.

static void hi() { }              // hi 도 static 으로
new Main().hi();                  // 객체를 만들어서 부른다

10강에서 모든 메서드에 static 을 붙였던 것이 첫 번째 방법이었다.

반대 방향은 자유롭다. 인스턴스 메서드는 static 인 것을 얼마든지 쓸 수 있다. static 은 언제나 거기 있으니까.

final — 한 번 정하면 못 바꾼다

final 은 재대입을 막는다.

final int x = 1;
x = 2;
터미널
  • error: cannot assign a value to final variable x

여기에 큰 함정이 하나 있다.

final int[] a = { 1, 2, 3 };
a[0] = 99;

System.out.println(a[0]);
콘솔
  • 99

final 인데 바뀌었다.

4강을 다시 떠올리자. 참조형 상자에 든 것은 주소다. final 은 그 상자를 잠근다. 상자에 든 주소를 못 바꾸게 할 뿐, 그 주소가 가리키는 저쪽의 내용은 잠그지 않는다.

a = new int[]{ 7, 8, 9 };   // 이건 막힌다 — 상자를 바꾸려 하니까
a[0] = 99;                  // 이건 된다 — 저쪽 내용을 바꿀 뿐이니까

10강에서 fill 은 되고 reassign 은 안 됐던 것과 정확히 같은 구조다.

static final — 상수

staticfinal 을 함께 붙이면 클래스에 하나뿐이고 바뀌지 않는 값이 된다. 상수다.

class Book {
    static final int MAX_PRICE = 1_000_000;
}

이름은 전부 대문자로 쓰고 단어 사이를 밑줄로 잇는다. 관례다. 5강에서 오버플로를 볼 때 쓴 Integer.MAX_VALUE 가 그랬다.

숫자 사이의 밑줄(1_000_000)은 자릿수를 읽기 쉽게 하는 표시일 뿐이다. 컴파일러는 무시한다.

enum — 정해진 몇 가지 중 하나

책의 상태를 나타낸다고 하자. 초안, 판매중, 품절 셋 중 하나다.

String 으로 하면 오타가 나고, int 로 하면 3 이 무슨 뜻인지 모른다. 이럴 때 enum 을 쓴다.

enum Status {
    DRAFT, PUBLISHED, SOLD_OUT
}
Status s = Status.PUBLISHED;

System.out.println(s);
System.out.println(s == Status.PUBLISHED);
System.out.println(Arrays.toString(Status.values()));
콘솔
  • PUBLISHED
  • true
  • [DRAFT, PUBLISHED, SOLD_OUT]

enum 의 각 값은 static final 인 객체 하나씩이다. 그래서 세상에 PUBLISHED 는 딱 하나뿐이고, == 로 비교해도 안전하다. 6강에서 String== 로 비교하면 안 된다고 했던 것과 대비된다.

7강의 switch 와 잘 어울린다.

String label = switch (s) {
    case DRAFT      -> "초안";
    case PUBLISHED  -> "판매중";
    case SOLD_OUT   -> "품절";
};

default 가 없는데 컴파일된다. 경우의 수가 셋뿐이라는 걸 컴파일러가 알기 때문이다. 나중에 Status 에 값을 하나 추가하면, 이 switch 는 컴파일 에러가 나서 고쳐야 할 곳을 알려준다.

enum 값 앞에 Status. 를 붙이지 않는 것에 주의하자. switch 안에서는 이미 무슨 타입인지 알고 있다.

정리하면

static 은 클래스에 하나, final 은 한 번 정하면 끝.

다시 짚어보기
  1. 01

    static 은 객체가 아니라 클래스에 붙는다

    객체를 몇 개 만들든 하나뿐이다. 클래스 이름으로 꺼낸다.

    System.out.println(Book.total);
  2. 02

    main 이 static 인 이유

    JVM 이 객체를 만들지 않고 불러야 하기 때문이다. 2강의 마지막 단어였다.

  3. 03

    static 은 인스턴스 것을 쓸 수 없다

    this 가 없어서다. 반대 방향은 자유롭다.

  4. 04

    final 은 상자를 잠근다. 내용이 아니다

    final 배열의 칸은 바꿀 수 있다. 4강의 값과 주소 이야기다.

    final int[] a = { 1, 2, 3 };
    a[0] = 99;              // 된다
    a = new int[]{ 7 };     // 안 된다
  5. 05

    정해진 몇 가지는 enum

    값마다 객체 하나. == 로 비교해도 안전하고, switch 가 빠짐없이 검사해준다.

이제 클래스를 만들고, 필드를 감추고, 객체 없이 존재하는 것까지 안다.

다음 강의는 이 시리즈에서 세 번이나 미뤄둔 질문에 답한다. 6강에서 처음 물었고, 9강에서 배열이 이상하게 굴 때 다시 물었고, 11강에서 우리 Bookfalse 를 낼 때 또 물었다.

.equals() 는 대체 누가 만든 메서드인가.

답을 알려면 자바의 모든 클래스가 몰래 물려받고 있는 부모를 만나야 한다.

static 과 final — 객체 없이 존재하는 것 — 디코드랩(DCODELAB)