Java 클래스가 빈 줄과 다르게 컴파일되는 이유는 무엇입니까?


207

다음 Java 클래스가 있습니다

public class HelloWorld {
  public static void main(String []args) {
  }
}

이 파일을 컴파일하고 결과 클래스 파일에서 sha256을 실행하면

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

다음으로 클래스를 수정하고 다음과 같이 빈 줄을 추가했습니다.

public class HelloWorld {

  public static void main(String []args) {
  }
}

다시 동일한 결과를 얻을 것으로 예상되는 출력에서 ​​sha256을 실행했지만 대신

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

이 TutorialsPoint 기사 에서 다음 을 읽었습니다 .

주석이 포함 된 공백 만 포함 된 행을 빈 행이라고하며 Java는이를 완전히 무시합니다.

그래서 내 질문은 Java가 빈 줄을 무시하기 때문에 컴파일 된 바이트 코드가 두 프로그램 모두 다른 이유는 무엇입니까?

즉, 바이트 에서의 차이 HelloWorld.class0x03바이트로 대체됩니다 0x04.


45
컴파일러는 보통 클래스 파일을 생성하더라도 클래스 파일을 생성 할 때 결정적 일 필요는 없습니다. 이 질문을 참조하십시오 . 기본적으로 Jar 파일은 재현 할 수 없습니다 . 즉 , 동일한 코드를 컴파일해도 두 개의 다른 JAR이 생성됩니다. 파일의 순서와 타임 스탬프가 일치하지 않기 때문입니다. 특정 구성으로 재현 가능한 빌드가 가능합니다.
Giacomo Alzetta

22
TutorialsPoint는 빈 줄 을 "자바 무시합니다"라고 주장합니다 . Java 언어 사양의 섹션 3.4에 달리 명시되어 있습니다. 어느 것을 믿어야합니까? ...
skomisa

37
@skomisa 사양입니다.
wizzwizz4

4
@GiacomoAlzetta 단일 바이트 코드 파일에 대해 지정된 바이트 코드 형식조차 없습니다. 예를 들어 멤버의 순서는 지정되어 있지 않으므로 컴파일러가 Set내부에서 무작위로 새로운 불변의을 사용하면 각 실행마다 다른 순서를 생성 할 수 있습니다. 또한 컴파일 타임을 포함하는 사용자 정의 속성을 추가 할 수 있습니다. 그리고 등등…
Holger

15
@DioPhung 다른 교훈을 배운 : tutorialspoint 좋은 자습서에 대한 신뢰할 수있는 원본 아닙니다
jwenting

답변:


331

기본적으로 줄 번호는 디버깅을 위해 유지되므로 소스 코드를 변경 한 경우 메소드가 다른 줄에서 시작되고 컴파일 된 클래스가 차이를 반영합니다.


11
또한 OP에서보고 한 바이트가 다른 이유를 설명합니다 end-of-transmission. ASCII 코드 4 end-of-text를 나타내고 ASCII 코드 3을 나타냅니다
Ferrybig

160
이것을 실험적으로 증명하기 위해 -g:none컴파일 할 때 플래그를 사용하여 OP 소스의 클래스 파일 해시를 비교하고 (모든 디버깅 정보를 제거하고 여기 참조 ) 두 시나리오에서 동일한 해시를 얻었습니다.
캡틴 맨

14
3.4 (에서 답의 형식적인 지원에서 "행 종결" 의) 자바 SE (11)에 대한 Java 언어 사양 : 줄 끝을 인식하여 선에 유니 코드 입력 문자의 순서가 ... 분할 옆에 "A 자바 컴파일러 라인 정의 줄 단위 종결 자는 Java 컴파일러에 의해 생성 된 줄 번호를 결정할 수 있습니다 " .
skomisa

4
이 행 번호의 중요한 용도 중 하나는 예외가 발생하는 것입니다. 스택 추적에서 예외의 줄 번호를 알려줄 수 있습니다.
gparyani

114

javap -v자세한 정보를 출력하는 을 사용하여 변경 사항을 볼 수 있습니다 . 이미 언급 한 바와 같이 차이점은 줄 번호에 있습니다.

$ javap -v HelloWorld.class > with-line.txt
$ javap -v HelloWorld.class > no-line.txt
$ diff -C 1 no-line.txt with-line.txt
*** no-line.txt 2018-10-03 11:43:32.719400000 +0100
--- with-line.txt       2018-10-03 11:43:04.378500000 +0100
***************
*** 2,4 ****
    Last modified 03-Oct-2018; size 373 bytes
!   MD5 checksum 058baea07fb787bdd81c3fb3f9c586bc
    Compiled from "HelloWorld.java"
--- 2,4 ----
    Last modified 03-Oct-2018; size 373 bytes
!   MD5 checksum 435dbce605c21f84dda48de1a76e961f
    Compiled from "HelloWorld.java"
***************
*** 50,52 ****
        LineNumberTable:
!         line 3: 0
        LocalVariableTable:
--- 50,52 ----
        LineNumberTable:
!         line 4: 0
        LocalVariableTable:

보다 정확하게 클래스 파일은 LineNumberTable섹션 에서 다릅니다 .

LineNumberTable 특성은 Code 특성 (§4.7.3)의 특성 테이블에있는 선택적 가변 길이 특성입니다. 디버거가 코드 배열의 어느 부분을 원본 소스 파일의 주어진 줄 번호에 해당하는지 확인하기 위해 사용할 수 있습니다.

Code 속성의 속성 테이블에 여러 LineNumberTable 속성이있는 경우 순서에 상관없이 나타날 수 있습니다.

코드 속성의 속성 테이블에서 소스 파일의 라인 당 둘 이상의 LineNumberTable 속성이있을 수 있습니다. 즉, LineNumberTable 속성은 소스 파일의 주어진 행을 함께 나타낼 수 있으며 소스 행과 일대일 일 필요는 없습니다.


57

"자바는 빈 줄을 무시 한다 " 는 가정 이 잘못되었습니다. 다음은 메서드 앞의 빈 줄 수에 따라 다르게 동작하는 코드 스 니펫입니다 main.

class NewlineDependent {

  public static void main(String[] args) {
    int i = Thread.currentThread().getStackTrace()[1].getLineNumber();
    System.out.println((new String[]{"foo", "bar"})[((i % 2) + 2) % 2]);
  }
}

빈 줄이 없으면 main인쇄 "foo"되지만 빈 줄이 하나 있으면 main인쇄 "bar"됩니다.

런타임 동작이 다르므로 타임 스탬프 또는 기타 메타 데이터에 관계없이 .class파일 이 달라야 합니다 .

Java뿐만 아니라 줄 번호로 스택 프레임에 액세스 할 수있는 모든 언어에 적용됩니다.

참고 : -g:none디버깅 정보없이 컴파일 하면 줄 번호가 포함되지 않고 getLineNumber()항상을 반환 -1하며 "bar"줄 바꿈 수에 관계없이 프로그램이 항상 인쇄 합니다.


11
인쇄 할 수도 있습니다 Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk

1
@xehpuk 내가 얻을 수있는 유일한 방법 -1-g:none깃발 을 사용하는 것 입니다. 일반을 사용 하여이 예외를 얻는 다른 방법이 javac있습니까?
Andrey Tyukin

3
나는 -g옵션으로 만 추측합니다 . 이 또한의 -g:vars-g:source의 생성을 방지 LineNumberTable.
xehpuk

14

디버깅을위한 행 번호 세부 정보뿐만 아니라 매니페스트에도 빌드 시간과 날짜가 저장 될 수 있습니다. 컴파일 할 때마다 자연스럽게 다릅니다.


14
C #에도이 문제가 있습니다. 최근까지 컴파일러는 생성 된 어셈블리에 항상 새로운 GUID를 포함 시켰으므로 두 빌드가 이진 동일 하지 않다는 것을 보장 할 수 있습니다.
Eric Lippert

3
@EricLippert 두 빌드가 생성 된 시간 (예 : 동일한 코드베이스) 만 다른 경우, 동일한 빌드로 취급해서는 안됩니까? 최신 CI / CD 빌드 파이프 라인 (Jenkins, TeamCity, CircleCI)을 사용하면 빌드를 구별 할 수있는 방법이 있지만 애플리케이션 관점에서 동일한 코드 기반으로 최신 바이너리를 배포하는 것은 유용하지 않은 것 같습니다.
Dio Phung

2
@DioPhung 그것은 다른 방법입니다. 두 개의 서로 다른 빌드가 동일한 GUID를 갖기를 원하지 않습니다 . 시스템이 어떤 것을 사용할지 결정할 수 있기 때문입니다. 따라서 매번 새로운 GUID를 생성하는 것이 가장 쉽습니다. 그러면 Eric이 의도하지 않은 결과로 설명하는 부작용이 생깁니다.
Graham

3
@vikingsteve 내가 말했듯이, 두 개의 서로 다른 빌드가 동일한 GUID로보고되는 것이 훨씬 덜 도움이되고, 그런 다음 동일한 소프트웨어 인 것으로 시스템에보고됩니다. 이로 인해 모든 종류의 프로비저닝 체계가 완전히 실패 할 수 있으므로 GUID가 절대로 복제되지 않는 것이 중요합니다 (합리적 확률 내에서!). 동일한 소스 코드의 두 개의 개별 빌드에 대해 서로 다른 GUID를 갖는 것이 가장 성가신 일입니다. 따라서 미션 크리티컬 실패 시나리오에 직면했을 때 약간 도움이되지 않는다고 생각하는 것은 실제로 파악되지 않습니다.
Graham

4
@vikingsteve 바이너리 의 코드 부분은 여전히 ​​동일합니다 (이해하고 있다면 C # 개발자가 아닙니다), 바이너리에 첨부 된 메타 데이터 일뿐입니다.
캡틴 맨
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.