먼저 퍼즐 : 다음 코드는 무엇을 인쇄합니까?
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 = 0
후 X = 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을 살펴보십시오.필드가 아직 초기화되지 않은 경우 필드에 대한 액세스가 제한되는시기에 대해 하는 을 .
클래스 변수와 관련된 몇 가지 규칙이 나열되어 있습니다.
f
class 또는 interface에 선언 된 클래스 변수에 대한 간단한 이름으로 참조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 * 10
예 0 * 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));
}
}
- JVM은 RecursiveStatic을 jar의 진입 점으로로드합니다.
- 클래스 로더는 클래스 정의가로드 될 때 정적 초기화기를 실행합니다.
- 이니셜 라이저는 함수
scale(10)
를 호출 하여static final
필드 를 할당합니다X
. - 이
scale(long)
함수는 클래스가 부분적으로 초기화되는 동안 초기화되지 않은 값을 읽고X
long 또는 0의 기본값입니다. - 의 값
0 * 10
이 지정되고X
클래스 로더가 완료됩니다. - JVM 은 0을 리턴하는 0으로
scale(5)
초기화 된X
값에 5를 곱한 공개 정적 void 기본 메소드 호출 을 실행합니다 .
정적 최종 필드 X
는 한 번만 할당되므로 final
키워드 가 보장 합니다. 할당에 3을 추가하는 후속 쿼리의 경우 위의 5 단계는 평가 0 * 10 + 3
가 값 3
이며 주 방법은 결과를 3 * 5
값으로 인쇄합니다 15
.
답변
객체의 초기화되지 않은 필드를 읽으면 컴파일 오류가 발생합니다. 불행히도 Java의 경우 그렇지 않습니다.
나는 이것이 표준의 세부 사항을 모르지만 객체가 인스턴스화되고 구성되는 방법의 정의 내에서 이것이 사실 인 근본적인 이유가 “숨겨져”있다고 생각합니다.
어떤 의미에서 final은 정의 된 목적이이 문제로 인해 달성되지 않기 때문에 정의가 잘못되었습니다. 그러나 모든 수업이 제대로 작성되면이 문제가 발생하지 않습니다. 모든 필드는 항상 모든 생성자에서 설정되며 생성자 중 하나를 호출하지 않고는 객체가 생성되지 않습니다. 직렬화 라이브러리를 사용해야 할 때까지 자연스럽게 보입니다.