최종 정의가 잘못 되었습니까?


186

먼저 퍼즐 : 다음 코드는 무엇을 인쇄합니까?

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10);

    private static long scale(long value) {
        return X * value;
    }
}

대답:

0

아래 스포일러.


당신이 인쇄 할 경우 X크기 (길이) 및 재정에 X = scale(10) + 3의 인쇄가 될 것입니다 X = 0X = 3. 이는 X일시적으로 설정되어 0있고 나중에 설정되어 있음을 의미합니다 3. 이것은 위반입니다 final!

정적 수정자는 최종 수정 자와 함께 상수를 정의하는 데에도 사용됩니다. 최종 수정자는 이 필드 의 값을 변경할 수 없음을 나타냅니다 .

출처 : https://docs.oracle.com/javase/tutorial/java/javaOO/classvars.html [강조 추가]


내 질문 : 이것은 버그입니까? 인가 final잘못 정의?


. 여기에 내가 관심 오전 코드입니다 X: 두 개의 서로 다른 값을 할당 0하고 3. 나는 이것이 위반이라고 생각합니다 final.

public class RecursiveStatic {
    public static void main(String[] args) {
        System.out.println(scale(5));
    }

    private static final long X = scale(10) + 3;

    private static long scale(long value) {
        System.out.println("X = " + X);
        return X * value;
    }
}

이 질문은 Java 정적 최종 필드 초기화 순서 의 가능한 복제본으로 표시되었습니다 . 다른 질문은 초기화 순서를 다루고 내 질문은 태그 와 결합 된 순환 초기화를 다루기 때문에이 질문은 중복 되지 않는다고 생각합니다 final. 다른 질문만으로도 내 질문의 코드가 오류를 발생시키지 않는 이유를 이해할 수 없습니다.

때이 에르네스토가 얻을 수있는 출력을 조사하여 특히 분명하다 a태그입니다 final, 그는 다음과 같은 출력을 얻을 :

a=5
a=5

내 질문의 주요 부분과 관련이없는 것은 final무엇입니까? 변수 는 어떻게 변수를 변경합니까?


17
X멤버 를 참조하는이 방법은 수퍼 클래스 생성자가 완료되기 전에 서브 클래스 멤버를 참조하는 것과 같습니다 final. 이는의 정의가 아니라 문제입니다 .
daniu

4
JLS에서 :A blank final instance variable must be definitely assigned (§16.9) at the end of every constructor (§8.8) of the class in which it is declared; otherwise a compile-time error occurs.
Ivan Ivan

1
@Ivan, 이것은 상수가 아니라 인스턴스 변수에 관한 것입니다. 그러나 당신은 장을 추가 할 수 있습니까?
AxelH

9
참고로 : 프로덕션 코드에서이 작업을 수행하지 마십시오. 누군가가 JLS의 허점을 악용하기 시작하면 모든 사람에게 혼란 스러울 수 있습니다.
Zabuzard

13
참고로 C #에서도 이와 동일한 상황을 만들 수 있습니다. C #은 상수 선언의 루프가 컴파일 타임에 잡힐 것이라고 약속하지만 읽기 전용 선언 에 대해서는 그러한 약속을하지 않으며 실제로 필드의 초기 0 값이 다른 필드 초기화 프로그램에 의해 관찰되는 상황에 처할 수 있습니다. 그렇게 할 때 아프면 하지 마십시오 . 컴파일러는 저장하지 않습니다.
Eric Lippert

답변:


217

매우 흥미로운 발견. 이를 이해하려면 JLS (Java Language Specification)를 파헤쳐 야합니다 .

그 이유는 final하나의 할당 만 허용하기 때문입니다 . 그러나 기본값은 할당 이 없습니다 . 실제로 이러한 모든 변수 (클래스 변수, 인스턴스 변수, 배열 구성 요소)는 지정 전에 처음부터 기본값을 가리 킵니다 . 그런 다음 첫 번째 과제는 참조를 변경합니다.


클래스 변수 및 기본값

다음 예를 살펴보십시오.

private static Object x;

public static void main(String[] args) {
    System.out.println(x); // Prints 'null'
}

우리는 명시 적으로 값을 할당하지 않았다 x가 가리키는 불구하고 null, 그것의 기본 값입니다. §4.12.5와 비교하십시오 .

변수의 초기 값

클래스 변수 , 인스턴스 변수 또는 배열 구성 요소 는 작성 될 때 기본값으로 초기화됩니다 ( §15.9 , §15.10.2 )

이것은 예제와 같이 이러한 종류의 변수에만 해당됩니다. 로컬 변수를 보유하지 않습니다. 다음 예를 참조하십시오.

public static void main(String[] args) {
    Object x;
    System.out.println(x);
    // Compile-time error:
    // variable x might not have been initialized
}

동일한 JLS 단락에서 :

지역 변수 ( §14.4는 , §14.14 )해야 명시 적으로 값을 주어 그것을 사용하기 전에 중 하나를 초기화 (에 의해, §14.4 ) 또는 할당 ( §15.26 명확한 할당 규칙을 (사용하여 검증 할 수있는 방법으로,) § 16 (확정 할당 ).


최종 변수

이제 우리 final§4.12.4에서을 살펴 봅니다 .

최종 변수

변수는 final 로 선언 될 수 있습니다 . 최종 변수는 할 수있다 한 번에 할당 . 할당 직전에 변수를 할당하지 않으면 최종 변수를 할당 하면 컴파일 타임 오류가 발생 합니다 ( §16 (고정 할당) ).


설명

이제 예제로 돌아와 약간 수정했습니다.

public static void main(String[] args) {
    System.out.println("After: " + X);
}

private static final long X = assign();

private static long assign() {
    // Access the value before first assignment
    System.out.println("Before: " + X);

    return X + 1;
}

출력

Before: 0
After: 1

우리가 배운 것을 기억하십시오. 메소드 내 assign에서 변수 X에 아직 값이 지정 되지 않았습니다. 따라서 클래스 변수 이기 때문에 기본값을 가리키며 JLS에 따르면 해당 변수는 항상 로컬 변수와 달리 항상 기본값을 가리 킵니다. 애프터 assign방법 변수는 X값을 할당 1및 때문에 final우리는 더 이상 변경할 수 없습니다. 따라서 다음으로 인해 작동하지 않습니다 final.

private static long assign() {
    // Assign X
    X = 1;

    // Second assign after method will crash
    return X + 1;
}

JLS의 예

@Andrew 덕분 에이 시나리오를 정확하게 설명하는 JLS 단락을 찾았습니다.

하지만 먼저 살펴 보겠습니다

private static final long X = X + 1;
// Compile-time error:
// self-reference in initializer

이 방법이 허용되지 않는 이유는 무엇입니까? §8.3.3을 살펴보십시오.필드가 아직 초기화되지 않은 경우 필드에 대한 액세스가 제한되는시기에 대해 하는 을 .

클래스 변수와 관련된 몇 가지 규칙이 나열되어 있습니다.

fclass 또는 interface에 선언 된 클래스 변수에 대한 간단한 이름으로 참조 C경우 다음과 같은 경우 컴파일 타임 오류입니다 .

  • 참조는 클래스 변수 이니셜 라이저 C또는 정적 이니셜 라이저 C( §8.7 )에 나타납니다 . 과

  • 참조는 f자체 선언자 의 이니셜 라이저 또는 선언자의 왼쪽에 나타납니다 f. 과

  • 참조는 할당 표현의 왼쪽에 있지 않습니다 ( §15.26 ). 과

  • 참조를 포함하는 가장 안쪽 클래스 또는 인터페이스는 C입니다.

그것은 간단 X = X + 1합니다. 그 규칙에 의해 잡히고 메소드 접근은 아닙니다. 심지어이 시나리오를 나열하고 예를 제공합니다.

메소드 별 액세스는이 방법으로 확인되지 않으므로 다음과 같이하십시오.

class Z {
    static int peek() { return j; }
    static int i = peek();
    static int j = 1;
}
class Test {
    public static void main(String[] args) {
        System.out.println(Z.i);
    }
}

출력을 생성합니다.

0

변수 초기화 i프로그램은 클래스 메소드 peek을 사용하여 변수 초기화 프로그램에 의해 초기화 j되기 전에 변수 값에 액세스 하기 j때문에 여전히 기본값을 갖습니다 ( §4.12.5 ).


1
@Andrew 예, 클래스 변수입니다. 감사합니다. 네, 그것은 같은 액세스를 제한 할 몇 가지 추가 - 규칙을가하지 않는다면 작업 : §8.3.3 . 클래스 변수에 지정된 네 가지 점 (첫 번째 항목)을 살펴보십시오 . OPs 예제의 메소드 접근 방식은 이러한 규칙에 따르지 않으므로 X메소드에서 액세스 할 수 있습니다 . 그다지 신경 쓰지 않을 것입니다. JLS가 작업을 세부적으로 정의하는 방법에 따라 다릅니다. 나는 그런 코드를 절대 사용하지 않을 것입니다 .JLS의 일부 규칙을 악용하는 것입니다.
Zabuzard

4
문제는 생성자에서 인스턴스 메소드를 호출 할 수 있다는 것입니다. 반면에, 유용하고 안전한 슈퍼를 호출하기 전에 지역 주민을 할당하는 것은 허용되지 않습니다. 그림을 이동.
Reinstate Monica

1
@Andrew 당신은 아마도 여기에서 실제로 언급 한 유일한 사람 일 것입니다 forwards references(JLS의 일부이기도합니다). 이 해답이 없으면 매우 간단합니다 stackoverflow.com/a/49371279/1059372
Eugene

1
"첫 번째 과제는 참조를 변경합니다." 이 경우 참조 유형이 아니라 기본 유형입니다.
fabian

1
이 답변은 조금 길다면 맞습니다. :-) tl; dr은 OP 가 JLS가 아니라 "[최종] ​​필드를 변경할 수 없다"고 언급 한 자습서 를 인용 한 것으로 생각합니다 . 오라클의 튜토리얼은 매우 훌륭하지만 모든 경우를 다루지는 않습니다. OP의 질문에 대해 우리는 final의 실제 JLS 정의로 이동해야합니다. 그리고 그 정의는 최종 필드의 값이 절대로 변하지 않을 수 있다고 주장하는 것이 아닙니다 (OP가 당연히 도전하고 있음).
yshavit

22

final과는 아무런 관련이 없습니다.

인스턴스 또는 클래스 수준이므로 아직 할당되지 않은 경우 기본값을 유지합니다. 이것이 0할당하지 않고 액세스 할 때 나타나는 이유 입니다.

X완전히 할당하지 않고 액세스 하면 기본값 인 long이 유지 0되므로 결과가 나타납니다.


3
까다로운 점은 값을 할당하지 않으면 기본값으로 할당되지 않지만 "최종"값을 할당하는 데 사용하면 다음과 같이됩니다.
AxelH

2
@ AxelH 나는 그것이 무엇을 의미하는지 봅니다. 그러나 그것이 그렇지 않으면 세계가 붕괴되는 방식입니다.).
Suresh Atta

20

버그가 아닙니다.

첫 번째 통화는시기 scale에서 호출

private static final long X = scale(10);

평가하려고합니다 return X * value. X아직 값이 지정되지 않았으므로 a의 기본값 long( 0) 이 사용됩니다 .

에 코드를 평가하여 그 라인 그래서 X * 100 * 10입니다 0.


8
나는 그것이 OP가 혼동하는 것이라고 생각하지 않습니다. 혼란스러운 것은 X = scale(10) + 3. X메소드에서 참조 할 때는 이므로 0. 그러나 나중에는 3입니다. 따라서 OP X는에 서로 다른 두 개의 다른 값이 할당되어 있다고 생각합니다 final.
Zabuzard

4
@Zabuza이는 "로 설명되지 않는 그것은 평가하려고합니다 return X * value. XA에 대한 기본 값을 사용하므로하지 값이 할당되어 아직 및 long0. "? 말했다되지 않은 X(가)하는 기본 값으로 할당되어 있지만 X(이 용어를 인용하지 마십시오)) 기본 값으로 "대체"입니다.
AxelH

14

전혀 버그가 아니며 단순히 불법 참조 형식 의 참조가 아니며 더 이상 아무것도 아닙니다.

String x = y;
String y = "a"; // this will not compile 


String x = getIt(); // this will compile, but will be null
String y = "a";

public String getIt(){
    return y;
}

사양에서 간단히 허용됩니다.

예를 들어, 이것이 정확히 일치하는 곳입니다.

private static final long X = scale(10) + 3;

이전에 언급 한 바와 같이 어떤 식 으로든 불법 참조 에 대한 전방 참조 를 수행 scale하지만 기본값 인을 얻을 수 있습니다 X. 다시, 이것은 스펙에 의해 허용되며 (더 정확하게 말하면 금지되지 않습니다), 잘 작동합니다.


좋은 대답입니다! 스펙 두 번째 경우를 컴파일 할 수 있는지 궁금 합니다. 최종 필드의 "일관되지 않은"상태를 보는 유일한 방법입니까?
Andrew Tobilko

이 역시 꽤 많은 시간 동안 나를 귀찮게했다 앤드류, 나는 그것 C ++를 생각하는 경향 AM 또는 (이 사실이 아닌 경우 아이디어) C는 그것을 않습니다
유진

@Andrew : 그렇지 않으면 Turing 불완전 성 정리를 해결하는 것이기 때문입니다.
Joshua

9
@Joshua : 나는 여러분이 여기서 (1) 정지 문제, (2) 결정 문제, (3) 고델의 불완전 성 정리, (4) 튜링-완전 프로그래밍 언어와 같은 여러 가지 개념을 혼합하고 있다고 생각합니다. 컴파일러 작성자는 "이 변수를 사용하기 전에 반드시이 변수가 지정 되었습니까?"라는 문제를 해결하려고 시도하지 않습니다. 그 문제는 중단 문제를 해결하는 것과 동일하기 때문에 완벽하게 해결할 수 없습니다.
Eric Lippert

4
@EricLippert : 하하 죄송합니다. 불완전한 불완전 성과 정지 문제는 내 마음에 같은 장소를 차지합니다.
Joshua

4

클래스 수준 멤버는 클래스 정의 내에서 코드로 초기화 될 수 있습니다. 컴파일 된 바이트 코드는 클래스 멤버를 인라인으로 초기화 할 수 없습니다. (인스턴스 멤버도 비슷하게 처리되지만 제공된 질문과 관련이 없습니다.)

다음과 같은 것을 쓸 때 :

public class Demo1 {
    private static final long DemoLong1 = 1000;
}

생성 된 바이트 코드는 다음과 유사합니다.

public class Demo2 {
    private static final long DemoLong2;

    static {
        DemoLong2 = 1000;
    }
}

초기화 코드는 클래스 초기화 프로그램이 클래스를 처음로드 할 때 실행되는 정적 초기화 프로그램 내에 배치됩니다. 이 지식을 통해 원본 샘플은 다음과 유사합니다.

public class RecursiveStatic {
    private static final long X;

    private static long scale(long value) {
        return X * value;
    }

    static {
        X = scale(10);
    }

    public static void main(String[] args) {
        System.out.println(scale(5));
    }
}
  1. JVM은 RecursiveStatic을 jar의 진입 점으로로드합니다.
  2. 클래스 로더는 클래스 정의가로드 될 때 정적 초기화기를 실행합니다.
  3. 이니셜 라이저는 함수 scale(10)를 호출 하여 static final필드 를 할당합니다 X.
  4. scale(long)함수는 클래스가 부분적으로 초기화되는 동안 초기화되지 않은 값을 읽고 Xlong 또는 0의 기본값입니다.
  5. 의 값 0 * 10이 지정되고 X클래스 로더가 완료됩니다.
  6. JVM 은 0을 리턴하는 0으로 scale(5)초기화 된 X값에 5를 곱한 공개 정적 void 기본 메소드 호출 을 실행합니다 .

정적 최종 필드 X는 한 번만 할당되므로 final키워드 가 보장 합니다. 할당에 3을 추가하는 후속 쿼리의 경우 위의 5 단계는 평가 0 * 10 + 3가 값 3이며 주 방법은 결과를 3 * 5값으로 인쇄합니다 15.


3

객체의 초기화되지 않은 필드를 읽으면 컴파일 오류가 발생합니다. 불행히도 Java의 경우 그렇지 않습니다.

나는 이것이 표준의 세부 사항을 모르지만 객체가 인스턴스화되고 구성되는 방법의 정의 내에서 이것이 사실 인 근본적인 이유가 "숨겨져"있다고 생각합니다.

어떤 의미에서 final은 정의 된 목적이이 문제로 인해 달성되지 않기 때문에 정의가 잘못되었습니다. 그러나 모든 수업이 제대로 작성되면이 문제가 발생하지 않습니다. 모든 필드는 항상 모든 생성자에서 설정되며 생성자 중 하나를 호출하지 않고는 객체가 생성되지 않습니다. 직렬화 라이브러리를 사용해야 할 때까지 자연스럽게 보입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.