2010-01-29 48 views
16

Gần đây tôi đã nhận ra rằng tôi không hiểu rõ quá trình mã hóa chuỗi của Java.Từ khi biên dịch sang thời gian chạy, mã hóa chuỗi Java thực sự hoạt động như thế nào

Xét đoạn mã sau:

public class Main 
{ 
    public static void main(String[] args) 
    { 
     System.out.println(java.nio.charset.Charset.defaultCharset().name()); 
     System.out.println("ack char: ^"); /* where^= 0x06, the ack char */ 
    } 
} 

Kể từ khi ký tự điều khiển là interpreted differently between windows-1252 and ISO-8859-1, tôi đã chọn các ack char để thử nghiệm.

Tôi hiện đang biên dịch mã hóa với các tệp mã hóa khác nhau, UTF-8, windows-1252ISO-8859-1. Cả hai biên dịch cho cùng một điều, byte-mỗi-byte như được xác minh bởi md5sum.

sau đó tôi chạy chương trình:

$ java Main | hexdump -C 
00000000 55 54 46 2d 38 0a 61 63 6b 20 63 68 61 72 3a 20 |UTF-8.ack char: | 
00000010 06 0a            |..| 
00000012 

$ java -Dfile.encoding=iso-8859-1 Main | hexdump -C 
00000000 49 53 4f 2d 38 38 35 39 2d 31 0a 61 63 6b 20 63 |ISO-8859-1.ack c| 
00000010 68 61 72 3a 20 06 0a        |har: ..| 
00000017 

$ java -Dfile.encoding=windows-1252 Main | hexdump -C 
00000000 77 69 6e 64 6f 77 73 2d 31 32 35 32 0a 61 63 6b |windows-1252.ack| 
00000010 20 63 68 61 72 3a 20 06 0a      | char: ..| 
00000019 

Nó một cách chính xác kết quả đầu ra 0x06 không có vấn đề mã hóa đang được sử dụng.

Ok, nó vẫn xuất kết quả giống nhau 0x06, được hiểu là chữ [ACK] có thể in bằng mã windows-1252.

Điều đó dẫn tôi đến một vài câu hỏi:

  1. Sản bảng mã/charset của file Java được biên soạn dự kiến ​​sẽ được trùng với charset mặc định của hệ thống, theo đó nó được biên soạn? Hai người luôn luôn đồng nghĩa?
  2. Biểu diễn được biên dịch dường như không phụ thuộc vào bộ ký tự biên dịch thời gian, đây có phải là trường hợp không?
  3. Điều này có nghĩa là các chuỗi trong các tệp Java có thể được diễn giải khác nhau trong thời gian chạy nếu chúng không sử dụng các ký tự chuẩn cho bộ ký tự/ngôn ngữ hiện tại?
  4. Tôi nên biết gì về chuỗi ký tự và chuỗi ký tự trong Java?
+0

Nó không rõ ràng những gì bạn có nghĩa là "biên dịch nó với mã hóa tệp khác nhau". Bạn có nghĩa là bạn lưu tệp trong các mã hóa khác nhau, sau đó biên dịch từng tệp đó bằng cách sử dụng nút chuyển mã hóa sang javac không? Nếu vậy, làm thế nào để bạn biết những gì rác ngẫu nhiên là cuộn lên trong các tập tin nguồn sau khi lưu chúng trong những mã hóa? Bạn không thể đặt một ký tự điều khiển theo nghĩa đen vào nguồn của bạn và mong đợi nó tồn tại tuần tự hóa với các ký tự được mã hóa. –

+0

Một tệp không có gì hơn một luồng byte. Vì vậy, tôi tham chiếu đến các chuỗi chứa 'char' có thể được diễn giải khác nhau, hoặc ở thời gian chạy hoặc tại thời gian biên dịch, bằng cách giả sử tập tin được mã hóa trong các bộ ký tự khác nhau. –

+0

Để rõ ràng về bước biên dịch, tôi đã sử dụng thuộc tính mã hóa của mặt trời để đặt bộ ký tự tại thời gian biên dịch: 'javac -encoding windows-1252 Main.java', với bộ mã hóa phù hợp. –

Trả lời

19
  1. file nguồn có thể ở bất kỳ mã hóa
  2. Bạn cần phải nói với trình biên dịch mã hóa các tập tin nguồn (ví dụjavac -encoding...); cách khác, nền tảng mã hóa được giả
  3. Trong binaries tập tin lớp, literals chuỗi được lưu trữ như (sửa đổi) UTF-8, nhưng trừ khi bạn làm việc với bytecode, điều này không quan trọng (xem JVM spec)
  4. Strings trong Java là UTF -16, luôn luôn (xem Java language spec)
  5. các System.outPrintStream sẽ làm thay đổi chuỗi từ UTF-16 đến byte trong hệ thống mã hoá trước khi viết họ stdout

ghi chú:

3

Nếu bạn biên dịch với các mã hóa khác nhau, các mã hóa này chỉ ảnh hưởng đến tệp nguồn của bạn. Nếu bạn không có bất kỳ ký tự đặc biệt nào bên trong các nguồn của bạn, sẽ không có sự khác biệt trong mã byte kết quả.

Để chạy, bộ mã mặc định của hệ điều hành được sử dụng. Điều này độc lập với bộ ký tự mà bạn đã sử dụng để biên dịch.

1

Erm dựa trên thisthis ký tự điều khiển ACK giống hệt nhau trong cả hai mã hóa. Sự khác biệt liên kết mà bạn chỉ ra là nói về cách DOS/Windows thực sự có biểu tượng cho hầu hết các ký tự điều khiển trong Windows-1252 (như các ký tự Heart/Club/Spade/Diamond và simileys) trong khi ISO-8859 thì không.

+0

Bạn là đúng, ack char là 0x06 trong cả hai mã hóa. Có lẽ tôi đã thất bại, nhưng tôi đã cố gắng đưa ra một kịch bản mà trong đó nó sẽ được diễn giải khác nhau dựa trên bộ ký tự hiện tại. Bài đăng trên blog của @ McDowell thực hiện công việc tốt hơn nhiều để thể hiện những gì tôi đã cố gắng thực hiện. –

13

Một bản tóm tắt của "những gì biết" về mã hóa chuỗi trong Java:

  • Một ví dụ String, trong ký ức, là một dãy số gồm 16-bit " đơn vị mã ", Java xử lý như char giá trị. Về mặt khái niệm, các đơn vị mã này mã hóa một chuỗi các "điểm mã", trong đó một điểm mã là "số được gán cho một ký tự đã cho theo tiêu chuẩn Unicode". Các điểm mã nằm trong khoảng từ 0 đến một chút nhiều hơn một triệu, mặc dù chỉ có khoảng 100 ngàn điểm được xác định cho đến nay. Các điểm mã từ 0 đến 65535 được mã hóa thành một đơn vị mã duy nhất, trong khi các điểm mã khác sử dụng hai đơn vị mã. Quá trình này được gọi là UTF-16 (aka UCS-2). Có một vài sự tinh tế (một số điểm mã không hợp lệ, ví dụ: 65535 và có khoảng 2048 điểm mã trong 65536 đầu tiên được đặt trước chính xác cho việc mã hóa các điểm mã khác).
  • Các trang mã và các trang tương tự không ảnh hưởng đến cách Java lưu trữ các chuỗi trong RAM. Đó là lý do tại sao "Unicode" bắt đầu bằng "Uni". Miễn là bạn không thực hiện I/O với các chuỗi của mình, bạn đang ở trong thế giới Unicode, nơi mọi người sử dụng cùng một ánh xạ các ký tự cho các điểm mã.
  • Bộ ký tự có hiệu lực khi mã hóa chuỗi thành byte hoặc giải mã chuỗi từ byte. Trừ khi được chỉ định rõ ràng, Java sẽ sử dụng một bộ ký tự mặc định phụ thuộc vào "locale" của người dùng, một khái niệm tổng hợp mờ về những gì làm cho một máy tính ở Nhật Bản nói tiếng Nhật. Khi bạn in ra một chuỗi với System.out.println(), JVM sẽ chuyển đổi chuỗi thành thứ gì đó phù hợp cho bất kỳ nơi nào các ký tự đó đi, thường có nghĩa là chuyển đổi chúng thành byte bằng cách sử dụng bộ ký tự phụ thuộc vào ngôn ngữ hiện tại (hoặc JVM đoán ngôn ngữ hiện tại).
  • Một ứng dụng Java là trình biên dịch Java. Trình biên dịch Java cần phải giải thích nội dung của các tệp nguồn, ở mức hệ thống, chỉ là một chuỗi các byte. Trình biên dịch Java sau đó chọn một bộ ký tự mặc định cho điều đó, và nó làm như vậy tùy thuộc vào miền địa phương hiện tại, giống như Java sẽ làm, bởi vì trình biên dịch Java được viết bằng Java. Trình biên dịch Java (javac) chấp nhận cờ dòng lệnh (-encoding) có thể được sử dụng để ghi đè lựa chọn mặc định đó.
  • Trình biên dịch Java tạo các tệp lớp không phụ thuộc vào miền địa phương. Chuỗi ký tự kết thúc trong các tệp lớp đó với (loại) mã hóa UTF-8, bất kể bộ mã nào mà trình biên dịch Java được sử dụng để diễn giải các tệp nguồn. Ngôn ngữ trên hệ thống mà trình biên dịch Java chạy tác động đến cách mã nguồn được giải thích, nhưng một khi trình biên dịch Java đã hiểu rằng chuỗi của bạn có chứa mã số 6, thì điểm mã này là những gì sẽ thực hiện theo cách của nó tới các tệp lớp và không ai khác. Lưu ý rằng các điểm mã 0 đến 127 có cùng mã hóa theo UTF-8, CP-1252 và ISO-8859-1, do đó những gì bạn có được là không có gì lạ.
  • Mặc dù vậy String trường hợp không phụ thuộc vào bất kỳ loại mã hóa nào, miễn là chúng vẫn còn trong RAM, một số thao tác bạn có thể muốn thực hiện trên chuỗi là phụ thuộc vào miền địa phương. Đây không phải là câu hỏi về mã hóa; nhưng một miền địa phương cũng xác định một "ngôn ngữ" và nó như vậy sẽ xảy ra rằng các khái niệm của chữ hoa và chữ thường phụ thuộc vào ngôn ngữ được sử dụng. Nghi ngờ thông thường đang gọi "unicode".toUpperCase(): điều này mang lại "UNICODE" ngoại trừ nếu ngôn ngữ hiện tại là Thổ Nhĩ Kỳ, trong trường hợp này bạn nhận được "UNİCODE" ("I" có dấu chấm). Giả định cơ bản ở đây là nếu miền địa phương hiện tại là Thổ Nhĩ Kỳ thì dữ liệu mà ứng dụng đang quản lý có lẽ là văn bản tiếng Thổ Nhĩ Kỳ; cá nhân, tôi thấy giả định này là tốt nhất có vấn đề. Nhưng vì vậy nó được.

Trong điều khoản thực tế, bạn nên chỉ định mã hóa rõ ràng trong mã của mình, ít nhất là hầu hết thời gian. Không gọi số String.getBytes(), gọi String.getBytes("UTF-8"). Sử dụng mã hóa mặc định, phụ thuộc vào ngôn ngữ là tốt khi nó được áp dụng cho một số dữ liệu được trao đổi với người dùng, chẳng hạn như tệp cấu hình hoặc thông báo để hiển thị ngay lập tức; nhưng ở nơi khác, tránh phương pháp phụ thuộc vào miền địa phương bất cứ khi nào có thể.

Trong số các phần phụ thuộc vào miền địa phương khác của Java, có các lịch. Có toàn bộ múi giờ kinh doanh, mà phụ thuộc vào "múi giờ", mà nên liên quan đến vị trí địa lý của máy tính (và đây không phải là một phần của "locale" stricto sensu ...). Ngoài ra, vô số ứng dụng Java bí ẩn thất bại khi chạy ở Bangkok, bởi vì ở một miền địa phương Thái, Java mặc định theo lịch Phật giáo theo năm hiện tại là 2553.

Như một quy luật, giả sử rằng thế giới là rộng lớn (nó là!) và giữ cho mọi thứ chung chung (không làm bất cứ điều gì phụ thuộc vào một bộ ký tự cho đến thời điểm cuối cùng, khi I/O phải thực sự được thực hiện).

Các vấn đề liên quan