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

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

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무엇입니까? 변수 는 어떻게 변수를 변경합니까?



답변

매우 흥미로운 발견. 이를 이해하려면 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 ).


답변

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

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

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


답변

버그가 아닙니다.

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

private static final long X = scale(10);

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

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


답변

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

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. 다시, 이것은 스펙에 의해 허용되며 (더 정확하게 말하면 금지되지 않습니다), 잘 작동합니다.


답변

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

다음과 같은 것을 쓸 때 :

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.


답변

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

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

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


답변