Mengapa kelas Java mengkompilasi secara berbeda dengan baris kosong?

207

Saya memiliki kelas Java berikut

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

Ketika saya mengkompilasi file ini dan menjalankan sha256 pada file kelas yang dihasilkan saya dapatkan

9c8d09e27ea78319ddb85fcf4f8085aa7762b0ab36dc5ba5fd000dccb63960ff  HelloWorld.class

Selanjutnya saya memodifikasi kelas dan menambahkan baris kosong seperti ini:

public class HelloWorld {

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

Sekali lagi saya menjalankan sha256 pada output mengharapkan untuk mendapatkan hasil yang sama tetapi saya malah mendapatkannya

11f7ad3ad03eb9e0bb7bfa3b97bbe0f17d31194d8d92cc683cfbd7852e2d189f  HelloWorld.class

Saya telah membaca artikel TutorialsPoint ini bahwa:

Baris yang hanya berisi ruang putih, mungkin dengan komentar, dikenal sebagai baris kosong, dan Java benar-benar mengabaikannya.

Jadi pertanyaan saya adalah, karena Java mengabaikan baris kosong mengapa bytecode yang dikompilasi berbeda untuk kedua program?

Yaitu perbedaan dalam HelloWorld.classsatu 0x03byte yang digantikan oleh 0x04byte.

KNejad
sumber
45
Perhatikan bahwa kompiler tidak wajib deterministik dalam menghasilkan file kelas, meskipun biasanya mereka. Lihat pertanyaan ini . File jar secara default tidak dapat direproduksi, bahkan mengkompilasi kode yang sama akan menghasilkan dua JAR yang berbeda. Itu karena urutan file dan stempel waktu tidak akan cocok. Build yang dapat diproduksi dimungkinkan dengan konfigurasi spesifik.
Giacomo Alzetta
22
TutorialsPoint mengklaim bahwa "Java benar-benar mengabaikan" baris kosong. Bagian 3.4 dari Spesifikasi Bahasa Jawa mengatakan sebaliknya. Yang mana yang harus dipercaya? ...
skomisa
37
@skomisa Spesifikasi.
wizzwizz4
4
@GiacomoAlzetta bahkan tidak ada bentuk bytecode yang ditentukan untuk file bytecode tunggal. Misalnya, urutan anggota tidak ditentukan, jadi jika kompiler menggunakan Sets abadi baru dengan pengacakan secara internal, itu bisa menghasilkan urutan yang berbeda pada setiap proses. Itu juga bisa menambahkan atribut khusus yang berisi waktu kompilasi. Dan seterusnya ...
Holger
15
@DioPhung pelajaran lain yang dipelajari: tutorialspoint bukan sumber yang dapat diandalkan untuk tutorial yang baik
jwenting

Jawaban:

331

Pada dasarnya, nomor baris disimpan untuk debugging, jadi jika Anda mengubah kode sumber Anda seperti yang Anda lakukan, metode Anda dimulai pada baris yang berbeda dan kelas yang dikompilasi mencerminkan perbedaannya.

Federico klez Culloca
sumber
11
Itu juga menjelaskan mengapa ini berbeda dalam Bytes yang dilaporkan oleh OP: end-of-transmissionsingkatan dari kode ASCII 4 dan end-of-textsingkatan dari kode ASCII 3
Ferrybig
160
Untuk membuktikan ini, saya membandingkan hash file kelas dari sumber OP menggunakan -g:noneflag saat mengkompilasi (yang menghapus semua informasi debug, lihat di sini ) dan mendapatkan hash yang sama di kedua skenario.
Kapten Man
14
Untuk mendukung jawaban Anda secara formal, dari bagian 3.4 ( "Line Terminators" ) dari Spesifikasi Bahasa Java untuk Java SE 11 : "Compiler Java selanjutnya membagi urutan karakter input Unicode ke dalam baris dengan mengenali terminator garis ... Baris yang ditentukan oleh terminator garis dapat menentukan nomor baris yang dihasilkan oleh kompiler Java " .
skomisa
4
Salah satu penggunaan penting dari nomor baris ini adalah jika pengecualian dilemparkan; itu bisa memberi tahu Anda nomor baris pengecualian dalam jejak tumpukan.
gparyani
114

Anda dapat melihat perubahan dengan menggunakan javap -vyang akan menampilkan informasi verbose. Seperti lainnya yang telah disebutkan perbedaannya adalah dalam jumlah baris:

$ 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:

Lebih tepatnya file kelas berbeda di LineNumberTablebagian ini:

Atribut LineNumberTable adalah atribut panjang variabel opsional dalam tabel atribut atribut Code (§4.7.3). Ini dapat digunakan oleh para debugger untuk menentukan bagian array kode mana yang sesuai dengan nomor baris yang diberikan dalam file sumber asli.

Jika beberapa atribut LineNumberTable hadir dalam tabel atribut atribut Kode, maka mereka dapat muncul dalam urutan apa pun.

Mungkin ada lebih dari satu atribut LineNumberTable per baris file sumber dalam tabel atribut atribut Kode. Yaitu, atribut LineNumberTable dapat bersama-sama mewakili baris yang diberikan file sumber, dan tidak perlu satu-ke-satu dengan baris sumber.

Karol Dowbecki
sumber
57

Asumsi bahwa "Jawa mengabaikan garis kosong" salah. Berikut ini cuplikan kode yang berperilaku berbeda tergantung pada jumlah baris kosong sebelum metode 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]);
  }
}

Jika tidak ada garis kosong sebelumnya main, ia mencetak "foo", tetapi dengan satu baris kosong sebelumnya main, ia mencetak "bar".

Karena perilaku runtime berbeda, .classfile harus berbeda, terlepas dari cap waktu atau metadata lainnya.

Ini berlaku untuk setiap bahasa yang memiliki akses ke frame tumpukan dengan nomor baris, tidak hanya untuk Java.

Catatan: jika dikompilasi dengan -g:none(tanpa informasi debug), maka nomor baris tidak akan disertakan, getLineNumber()selalu kembali -1, dan program selalu dicetak "bar", terlepas dari jumlah jeda baris.

Andrey Tyukin
sumber
11
Itu juga bisa mencetak Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: -1.
xehpuk
1
@xehpuk Satu-satunya cara saya bisa mendapatkan -1adalah menggunakan -g:nonebendera. Apakah ada cara lain untuk mendapatkan pengecualian ini menggunakan biasa javac?
Andrey Tyukin
3
Saya kira hanya dengan -gopsi. Ada juga -g:varsdan -g:sourceyang mencegah generasi LineNumberTable.
xehpuk
14

Selain detail nomor baris untuk debugging, manifes Anda juga dapat menyimpan waktu dan tanggal pembuatan. Ini tentu saja akan berbeda setiap kali Anda kompilasi.

Graham
sumber
14
C # memiliki masalah ini juga; sampai baru-baru ini kompiler selalu menyematkan GUID baru dalam rakitan yang dihasilkan sehingga Anda akan dijamin bahwa dua build tidak akan sama biner, sehingga Anda dapat membedakannya!
Eric Lippert
3
@ EricLippert jika dua build hanya berbeda dengan waktu yang dihasilkan (yaitu basis kode yang identik), tidakkah kita harus memperlakukannya sama? Dengan pipeline build CI / CD modern (Jenkins, TeamCity, CircleCI), kita akan memiliki cara untuk membedakan antara build, tetapi dari perspektif aplikasi, menggunakan biner baru dengan basis kode yang identik tampaknya tidak berguna.
Dio Phung
2
@DioPhung Ini sebaliknya. Anda tidak ingin dua build berbeda memiliki GUID yang sama, karena itulah cara sistem dapat memutuskan mana yang akan digunakan. Jadi paling mudah untuk menghasilkan GUID baru setiap kali; dan kemudian Anda mendapatkan efek samping yang dijelaskan Eric sebagai konsekuensi yang tidak diinginkan.
Graham
3
@ vikingsteve Seperti yang saya katakan, akan lebih kurang membantu jika dua build berbeda untuk dilaporkan dengan GUID yang sama, yang kemudian akan dilaporkan ke sistem sebagai perangkat lunak yang sama. Ini akan menyebabkan kegagalan total dari segala jenis skema penyediaan, jadi sangat penting bahwa GUID tidak pernah digandakan (dalam probabilitas yang masuk akal!). Memiliki GUID yang berbeda untuk dua build terpisah dari kode sumber yang sama adalah gangguan yang paling banyak. Jadi, dalam menghadapi skenario kegagalan misi-kritis, apa yang menurut Anda sedikit tidak membantu benar-benar tidak masuk akal.
Graham
4
@ vikingsteve Bagian kode dari biner masih sama (jika saya mengerti, saya bukan seorang C # dev), itu hanya beberapa metadata yang dilampirkan ke biner.
Kapten Man