Programing

할당이 마지막 작업 인 경우에도 최종 변수를 catch에서 다시 할당 할 수 있습니까?

lottogame 2020. 12. 31. 07:54
반응형

할당이 마지막 작업 인 경우에도 최종 변수를 catch에서 다시 할당 할 수 있습니까?


나는 여기에서

final int i;
try { i = calculateIndex(); }
catch (Exception e) { i = 1; }

i제어가 catch 블록에 도달하면 이미 할당되었을 수 없습니다. 그러나 Java 컴파일러는 동의하지 않으며 주장 the final local variable i may already have been assigned합니다.

여기에 여전히 미묘한 부분이 있습니까, 아니면 잠재적 재 할당을 식별하기 위해 Java 언어 사양에서 사용하는 모델의 약점입니까? 내 주된 걱정은과 같은 Thread.stop()것인데, 이로 인해 예외가 "허공에서"던져 질 수 있지만 할당 후에 어떻게 던져 질 수 있는지는 아직 알 수 없습니다. 이것은 분명히 try-block 내에서 가장 마지막 작업입니다. .

위의 관용구가 허용된다면 많은 방법을 더 간단하게 만들 것입니다. 이 사용 사례는 Maybe 모나드 를 지속적으로 사용하는 Scala와 같은 언어에서 최고 수준의 지원을 제공합니다 .

final int i = calculateIndex().getOrElse(1);

나는이 사용 사례는 하나 특별한 경우 허용하는 매우 좋은 동기 부여의 역할을 생각 i입니다 확실히 할당되지 않은 캐치 블록 내에서합니다.

최신 정보

어떤 생각을 한 후에 이것이 JLS 모델의 약점 일 뿐이라는 것을 훨씬 더 확신합니다. "제시된 예에서 i제어가 캐치 블록에 도달 할 때 확실히 할당되지 않음 "공리를 선언하면 다른 공리와 충돌하지 않습니다. 또는 정리. 컴파일러는 icatch 블록에 할당되기 전에 읽기를 허용하지 않으므로 할당되었는지 여부 i를 관찰 할 수 없습니다.


JLS 사냥 :

최종 변수가 할당 직전에 할당되지 않은 경우 (§16)가 아니면 최종 변수가 할당되면 컴파일 타임 오류입니다.

Quoth 장 16 :

V는 다음 조건이 모두 유지되는 경우 catch 블록 전에 확실히 할당되지 않습니다.

V는 try 블록 후에 확실히 할당되지 않습니다.
V는 try 블록에 속한 모든 return 문 앞에 확실히 할당되지 않습니다.
V는 try 블록에 속하는 throw e 형식의 모든 문에서 e 다음에 할당되지 않습니다.
V는 try 블록에서 발생하는 모든 assert 문 후에 확실히 할당되지 않습니다.
V는 try 블록에 속하고 break 대상이 try 문을 포함 (또는 포함)하는 모든 break 문 앞에 확실히 할당되지 않습니다.
V는 try 블록에 속하고 continue 대상이 try 문을 포함하는 모든 continue 문 앞에 할당되지 않았습니다.

Bold는 내 것입니다. try블록 i할당 여부 불분명합니다 .

또한 예에서

final int i;
try {
    i = foo();
    bar();
}
catch(Exception e) { // e might come from bar
    i = 1;
}

굵은 텍스트는 실제 잘못된 할당 이 불법이되는 것을 방지 하는 유일한 조건 i=1입니다. 따라서 이것은 원본 게시물에 코드를 허용하기 위해 "확실히 할당되지 않음"이라는 더 미세한 조건이 필요하다는 것을 증명하기에 충분합니다.

이 조건을 다음으로 대체하도록 사양이 수정 된 경우

catch 블록이 확인되지 않은 예외를 포착하면 V는 try 블록 후에 확실히 할당되지 않습니다.
V는 catch 블록이 검사되지 않은 예외를 포착하는 경우 catch 블록에서 포착 한 유형의 예외를 던질 수있는 마지막 문 앞에 확실히 할당되지 않습니다.

그렇다면 귀하의 코드가 합법적이라고 생각합니다. (내 임시 분석을 최대한 활용하십시오.)

나는 이것에 대해 JSR을 제출했는데 무시 당할 것으로 예상했지만 어떻게 처리되는지 궁금했습니다. 기술적으로 팩스 번호가 필요한 분야이다, 나는 그것을하지 않습니다 희망 내가 + 1-000-000-000가 입력 한 경우 많은 피해를 입 힙니다.


저는 JVM이 슬프게도 정확하다고 생각합니다. 코드를 보면 직관적으로 정확하지만 IL을 보는 맥락에서는 의미가 있습니다. 대부분의 경우를 모방하는 간단한 run () 메서드를 만들었습니다 (여기에 간단한 설명).

0: aload_0
1: invokevirtual  #5; // calculateIndex
4: istore_1
5: goto  17
// here's the catch block
17: // is after the catch

따라서이를 테스트하기위한 코드를 쉽게 작성할 수는 없지만 컴파일되지 않기 때문에 메서드 호출, 값 저장 및 catch 후 건너 뛰기 작업은 세 가지 개별 작업입니다. 당신은 수있다 (그러나 않을 수 있음) 예외가 발생할 수있는 catch 블록에 입력 초래 4 단계와 5 단계이 사이에 (Thread.interrupt () 가장 좋은 예가 될 것 같다) 내가 설정되었습니다.

나는 확실히 당신은 의도적으로 스레드와 인터럽트의 톤 일어날 것을 만들 수 있지 않다 (컴파일러는 어쨌든 코드를 작성하지 않습니다)하지만 이론적으로 이렇게입니다 i가 설정 될 수 있다고, 당신은에 입력 할 수 있습니다 이 간단한 코드로도 예외 처리 블록.


그다지 깨끗하지 않습니다 (그리고 당신이 이미하고있는 것을 의심합니다). 그러나 이것은 단지 하나의 추가 라인을 추가합니다.

final int i;
int temp;
try { temp = calculateIndex(); }
catch (IOException e) { temp = 1; }
i = temp;

이것은 명확한 할당에 대한 현재의 규칙이 일관성을 깨지 않고는 완화 될 수 없다는 논문에 찬성하는 가장 강력한 주장 (A)과 내 반론 (B)을 요약 한 것입니다.

  • A : 바이트 코드 수준에서 변수에 대한 쓰기는 try-block 내의 마지막 명령어가 아닙니다. 예를 들어, 마지막 명령어는 일반적으로 goto예외 처리 코드를 뛰어 넘는 것입니다.

  • B : 그러나 규칙 이 catch-block 내에서 확실히 할당되지 않은i 것으로 명시되어 있으면 해당 값이 관찰되지 않을 수 있습니다. 관찰 할 수없는 값은 값이없는 것만 큼 좋습니다.

  • A : 컴파일러가 확실히 할당되지 않은i 것으로 선언하더라도 디버그 도구는 여전히 값을 볼 수 있습니다.

  • B : 사실, 디버그 도구는 항상 초기화되지 않은 지역 변수에 액세스 할 수 있으며, 일반적인 구현에서는 임의의 값을 갖습니다. 초기화되지 않은 변수와 실제 쓰기가 발생한 후 갑자기 초기화가 완료된 변수 사이에는 본질적인 차이가 없습니다. 여기서 고려하는 특별한 경우에 관계없이 도구는 항상 추가 메타 데이터를 사용하여 각 지역 변수에 대해 해당 변수가 확실히 할당 된 명령어 범위를 알고 실행이 범위 내에있는 동안 값을 관찰 할 수 있도록 허용해야합니다.

최종 결론 :

이 사양은 게시 된 예제를 컴파일 할 수 있도록보다 세분화 된 규칙을 지속적으로받을 수 있습니다.


할당이 try 블록의 마지막 작업 인 경우 catch 블록에 들어갈 때 변수가 할당되지 않았 음을 알고 있습니다. 그러나 "최종 작업"이라는 개념을 공식화하면 사양이 상당히 복잡해집니다. 중히 여기다:

try {
    foo = bar();
    if (foo) {
        i = 4;
    } else {
        i = 7;
    }
}

그 기능이 유용할까요? 최종 변수는 최대 한 번이 아니라 정확히 한 번 할당되어야하기 때문에 그렇게 생각하지 않습니다 . 귀하의 경우에는가 던져 지면 변수가 할당되지 않습니다 . 어쨌든 변수가 범위를 벗어난 경우에는 신경 쓰지 않을 수 있지만 항상 그런 것은 아닙니다 ( 동일하거나 주변의 try 문에서,를 잡는 다른 catch 블록이있을 수 있습니다 ). 예를 들어 다음을 고려하십시오.ErrorError

final int i;
try {
    try {
        i = foo();
    } catch (Exception e) {
        bar();
        i = 1;
    }
} catch (Throwable t) {
    i = 0;
}

맞습니다. 그러나 i를 할당 한 후 (예 : finally 절에서) bar () 호출이 발생하거나 close 메서드가 예외를 발생시키는 리소스와 함께 try-with-resources 문을 사용하는 경우에는 그렇지 않습니다.

이를 고려하면 사양이 훨씬 더 복잡해집니다.

마지막으로 간단한 해결 방법이 있습니다.

final int i = calculateIndex();

int calculateIndex() {
    try {
        // calculate it
        return calculatedIndex;
    } catch (Exception e) {
        return 0;
    }
}

그것은 내가 할당되었음을 명백하게합니다.

요컨대,이 기능을 추가하면 약간의 이익을 위해 사양에 상당한 복잡성이 추가 될 것이라고 생각합니다.


1   final int i;
2   try { i = calculateIndex(); }
3   catch (Exception e) { 
4       i = 1; 
5   }

OP는 이미 4 행에서 i가 이미 할당되었을 수 있다고 언급했습니다. 예를 들어 비동기 예외 인 Thread.stop ()을 통해 http://docs.oracle.com/javase/specs/jvms/se7/html/jvms-2.html#jvms-2.5를 참조 하십시오.

이제 4 행에 중단 점을 설정하고 1이 할당 되기 전에 변수 i의 상태를 관찰 할 수 있습니다 . 따라서 관찰 된 동작을 완화하면 Java ™ Virtual Machine Tool 인터페이스 에 위배됩니다.


javadoc을 찾아 보면 Exceptioni가 할당 된 직후에의 하위 클래스를 던질 수 없는 것 같습니다 . JLS 이론적 관점에서는 Errori가 할당 된 직후에 던져 질 수있는 것 같습니다 (예 :) VirtualMachineError.

catch 블록은 당신이 잡고있는 여부를 구별함으로써, 도달하면 컴파일러는 내가 이전에 설정 될 수 있는지 여부를 확인하기위한 JLS 요구 사항이 없다 보인다 Exception또는 Error/ Throwable, 그것은 JLS 모델의 약점의 의미는.

다음을 시도해보십시오. (컴파일 및 테스트 완료)

(정수 래퍼 유형 + finally + "Elvis"연산자는 null 여부를 테스트합니다) :

import myUtils.ExpressionUtil;
....
Integer i0 = null; 
final int i;
try { i0 = calculateIndex(); }   // method may return int - autoboxed to Integer!
catch (Exception e) {} 
finally { i = nvl(i0,1); }       


package myUtils;
class ExpressionUtil {
    // Custom-made, because shorthand Elvis operator left out of Java 7
    Integer nvl(Integer i0, Integer i1) { return (i0 == null) ? i1 : i0;}
}

이 모델이 생명의 은인 역할을하는 한 가지 상황이 있다고 생각합니다. 아래 주어진 코드를 고려하십시오.

final Integer i;
try
{
    i = new Integer(10);----->(1)
}catch(Exception ex)
{
    i = new Integer(20);
}

이제 줄 (1)을 고려하십시오. 대부분의 JIT 컴파일러는 다음 순서 (의사 코드)로 객체를 생성합니다.

mem = allocate();   //Allocate memory 
ctorInteger(instance);//Invoke constructor for Singleton passing instance.
i = mem;        //Make instance i non-null

그러나 일부 JIT 컴파일러는 순서가 잘못된 쓰기를 수행 합니다. 위의 단계는 다음과 같이 재정렬됩니다.

mem = allocate();   //Allocate memory 
i = mem;        //Make instance i non-null
ctorInteger(instance);  //Invoke constructor for Singleton passing instance.

이제 (1) 행에서 객체를 생성하는 동안 JIT수행 한다고 가정 합니다 out of order writes. 생성자를 실행하는 동안 예외가 발생했다고 가정합니다. 이 경우 catch블록 inot null. JVM이이 모달을 따르지 않으면이 경우 최종 변수가 두 번 할당 될 수 있습니다 !!!


그러나 i두 번 할당 될 수 있습니다.

    int i;
    try {
        i = calculateIndex();  // suppose func returns true
        System.out.println("i=" + i);
        throw new IOException();
    } catch (IOException e) {
        i = 1;
        System.out.println("i=" + i);
    }

산출

i=0
i=1

그리고 그것은 그것이 최종일 수 없다는 것을 의미합니다


OP의 질문에 기반한 응답 편집

이것은 실제로 주석에 대한 응답입니다.

당신이 한 모든 것은 짚맨 논쟁의 명확한 예를 작성하는 것입니다. 당신은 모든 호출 사이트에 대해 유효한 항상 하나의 기본값이 있어야한다는 암묵적인 가정을 대리로 도입하고 있습니다.

I believe that we are approaching the entire question from opposite ends. It seems that you are looking at it from the bottom up - literally from the bytecode and going up to the Java. If this is not true, you are looking at it from the "code" compliance to the spec.

Approaching this from the opposite direction, from the "design" down, I see problems. I think it was M. Fowler who collected various "bad smells" into the book: "Refactoring: Improving the Design of Existing Code". Here (and probably many, many other places) the "Extract Method" refactoring is described.

Thus, if I imagine a made-up version of your code without the 'calculateIndex' method, I might have something like this:

public void someMethod() {
    final int i;
    try {
        int intermediateVal = 35;
        intermediateVal += 56;
        i = intermediateVal*3;
    } catch (Exception e) {
        // would like to be able to set i = 1 here;
    }
}

Now, the above COULD have been refactored as originally posted with a 'calculateIndex' method. However, if the 'Extract Method' Refactoring defined by Fowler is completely applied, then one gets this [note: dropping the 'e' is intentional to differentiate from your method.]

public void someMethod() {
    final int i =  calculateIndx();
}

private int calculateIndx() {
    try {
        int intermediateVal = 35;
        intermediateVal += 56;
        return intermediateVal*3;
    } catch (Exception e) {
        return 1;  // or other default values or other way of setting
    }
}

So from the 'design' perspective the problem is the code you have. Your 'calculateIndex' method does NOT calculate the index. It only does sometimes. The rest of the time, the exception handler does the calculation.

Furthermore, this refactoring is far more accommodating to changes. For instance, if you have to change what I assumed was the default value of '1' to a '2', no big deal. However, as pointed out by the OP reply quoted, one cannot assume that there is only one default value. If the logic to set this grows to be only slightly complex it could still easily reside in the encapsulated exception handler. However, at some point, it too may need to be refactored into it's own method. Both cases still allow the encapsulated method to perform it's function and truly calculate the index.

In summary, when I get here and look at what I believe is the correct code, then there is no compiler issue for discussion. (I am most certain you will not agree: that is fine, I just want to be clearer about my viewpoint.) As for compiler warnings that come up for incorrect code, those help me realize in the first place that something is wrong. In this case, that refactoring is needed.


As per specs JLS hunting done by "djechlin", specs tells when is the variable definitely unassigned. So spec says that in those scenarios it is safe to allow the assignment.There can be scenarios other than the one mentioned in the specs in which case variable can still be unassigned and it will depend on compiler to make that intelligent decision if it can detect and allow an assignment.

Spec in no way mentions in the scenario specified by you, that compiler should flag an error. So it depends on compiler implementation of spec if it is intelligent enough to detect such scenarios.

Reference: Java Language Specification Definite Assignment section "16.2.15 try Statements"


I faced EXACTLY the same problem Mario, and read this very interresting discussion. I just solved my issue by that:

private final int i;

public Byte(String hex) {
    int calc;
    try {
        calc = Integer.parseInt(hex, 16);
    } catch (NumberFormatException e) {
        calc = 0;
    }
    finally {
      i = calc;
    }
}

@Joeg, I must admit that I liked a lot your post about design, especially that sentence: calculateIndx() calculates sometimes the index, but could we say the same about parseInt() ? Isn't that also the role of calculateIndex() to throw and thus not calculate the index when it is not possible, and then making it returning a wrong value (1 is arbitrary in your refactoring) is imho bad.

@Marko, I didn't understand your reply to Joeg about the AFTER line 4 and BEFORE line 5... I'm not strong enough yet in java world (25y of c++ but only 1 in java...), but I thing this case is one where the compiler is right : i could be initialized twice in Joeg's case.

[All what I'm saying is a very very humble opinion]

ReferenceURL : https://stackoverflow.com/questions/17075061/could-a-final-variable-be-reassigned-in-catch-even-if-assignment-is-last-operat

반응형