なぜ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.class0x030x04


45
コンパイラーは、通常そうであるとしても、クラスファイルの生成において決定論的である必要はないことに注意してください。この質問を参照してください。デフォルトでは、jarファイル再現可能ではありません。つまり、同じコードをコンパイルしも、2つの異なるJARが生成されます。これは、ファイルの順序とタイムスタンプが一致しないためです。特定の構成で再現可能なビルドが可能です。
ジャコモアルゼッタ2018年

22
TutorialsPointは、「Javaは空白行を完全に無視する」と主張しています。Java言語仕様のセクション3.4は、そうではないと述べています。どちらを信じますか?...
skomisa 2018年

37
@skomisa仕様。
wizzwizz4 2018年

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
Java SE 11のJava言語仕様のセクション3.4(「ラインターミネータ」)からの回答の正式なサポート:「Javaコンパイラは次に、ラインターミネータを認識することにより、Unicode入力文字のシーケンスをラインに分割します... 定義されたラインラインターミネータによって、Javaコンパイラによって生成行番号を決定することができます
skomisa

4
これらの行番号の重要な用途の1つは、例外がスローされた場合です。スタックトレースの例外の行番号がわかります。
gparyani 2018年

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属性が存在する場合、それらは任意の順序で表示される可能性があります。

Code属性の属性テーブルには、ソースファイルの行ごとに複数のLineNumberTable属性がある場合があります。つまり、LineNumberTable属性は一緒にソースファイルの特定の行を表すことができ、ソース行と1対1である必要はありません。


57

「Javaが空白行を無視するという仮定は誤りです。メソッドの前の空行の数に応じて異なる動作をするコードスニペットを次に示します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に空の行が1つある場合は印刷され"bar"ます。

ランタイムの動作が異なるため、タイムスタンプやその他のメタデータに関係なく、.classファイルは異なる必要あります。

これは、Javaだけでなく、行番号付きのスタックフレームにアクセスできるすべての言語に当てはまります。

注:(-g:noneデバッグ情報なしで)コンパイルされている場合、行番号は含まれず、getLineNumber()常にが返され、-1プログラムは"bar"改行の数に関係なく常にを出力します。


11
印刷もできますException in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1
xehpuk 2018年

1
@xehpuk私が得ること-1ができる唯一の方法は-g:noneフラグを使用することでした。通常を使用してこの例外を取得する他の方法はありますjavacか?
Andrey Tyukin、2018年

3
-gオプションだけだと思います。また、の生成を妨げるものも-g:varsあります。-g:sourceLineNumberTable
xehpuk 2018年

14

マニフェストには、デバッグの行番号の詳細だけでなく、ビルドの日時も格納される場合があります。これは当然、コンパイルするたびに異なります。


14
C#にもこの問題があります。最近まで、コンパイラーは常に生成されたアセンブリに新しいGUIDを埋め込んでいたため、2つのビルドがバイナリーで同一ではないことが保証され、それらを区別できます!
Eric Lippert、2018年

3
@EricLippert 2つのビルドが生成された時間のみが異なる場合(つまり、同一のコードベース)、それらを同じものとして扱わないでください。最新のCI / CDビルドパイプライン(Jenkins、TeamCity、CircleCI)では、ビルドを区別する方法がありますが、アプリケーションの観点からは、同じコードベースで新しいバイナリをデプロイすることは役に立たないようです。
Dio Phung、2018年

2
@DioPhung逆です。2つの異なるビルドが同じGUIDを持つことは望ましくありません。これは、システムがどちらを使用するかを決定できる方法だからです。そのため、毎回新しいGUIDを生成するのが最も簡単です。そして、あなたはエリックが意図しない結果として説明する副作用を受け取ります。
グラハム、

3
@vikingsteve私が言ったように、2つの異なるビルドが同じGUIDで報告され、同じソフトウェアであるとシステムに報告されることは、あまり役に立ちません。これは、あらゆる種類のプロビジョニングスキームの完全な失敗の原因となるため、GUIDが複製されることは(妥当な確率で)ミッションクリティカルです。同じソースコードの2つの別々のビルドに異なるGUIDを使用することは、せいぜい些細なことです。したがって、ミッションクリティカルな障害シナリオに直面しても、少し役に立たないと思うことは実際にはわかりません。
グラハム、

4
@vikingsteveバイナリのコード部分は同じです(私が理解している場合、私はC#の開発者ではありません)。これは、バイナリに添付されているメタデータの一部です。
キャプテンマン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.