Trong Java, String.length()
dùng để trả về số lượng ký tự trong chuỗi, trong khi String.getBytes().length
dùng để trả về số lượng byte cần để biểu diễn chuỗi với mã hóa được chỉ định. Theo mặc định, mã hóa sẽ là giá trị của thuộc tính hệ thống file.encoding, tên mã hóa cũng có thể được đặt thủ công bằng cách gọi System.setProperty("file.encoding", "XXX")
. Ví dụ, UTF-8, Cp1252. Trong nhiều trường hợp, String.length()
sẽ trả về cùng một giá trị với String.getBytes().length
, nhưng trong một số trường hợp thì không.
String.length()
là số lượng đơn vị mã UTF-16 cần thiết để biểu diễn chuỗi. Có nghĩa là, đó là số lượng giá trị char
được sử dụng để biểu diễn chuỗi (do đó bằng với toCharArray().length
). Điều này thường giống như số lượng ký tự unicode (điểm mã) trong chuỗi - ngoại trừ khi sử dụng các ký tự thay thế UTF-16. Vì char
là 2 byte trong Java, nên String.getBytes().length
sẽ gấp 2 lần String.length()
nếu mã hóa là UTF-16.
String.getBytes().length
là số lượng byte cần thiết để biểu diễn chuỗi trong mã hóa mặc định của nền tảng. Ví dụ: nếu mã hóa mặc định là UTF-16, nó sẽ chính xác gấp đôi giá trị được trả về bởi String.length()
. Đối với UTF-8, hai độ dài có thể không giống nhau.
Mối quan hệ giữa hai độ dài này rất đơn giản nếu chuỗi chỉ chứa các mã ASCII vì chúng sẽ trả về cùng một giá trị. Nhưng đối với các ký tự không phải ASCII, mối quan hệ sẽ phức tạp hơn một chút. Ngoài các chuỗi ASCII, String.getBytes().length
có thể dài hơn, vì nó đếm các byte cần thiết để biểu diễn chuỗi, trong khi length()
đếm các đơn vị mã 2 byte.
Tiếp theo, chúng ta sẽ lấy mã hóa UTF-8 làm ví dụ để minh họa mối quan hệ này.
try{ System.out.println("file.encoding = "+System.getProperty("file.encoding")); char c = 65504; System.out.println("c = "+c); String s = new String(new char[]{c}); System.out.println("s = "+s); System.out.println("s.length = "+s.length()); byte[] bytes = s.getBytes("UTF-8"); System.out.println("bytes = "+Arrays.toString(bytes)); System.out.println("bytes.length = "+bytes.length); } catch (Exception ex){ ex.printStackTrace(); }
Hãy xem đầu ra trước tiên:
file.encoding = UTF-8 c = ï¿ s = ï¿ s.length = 1 bytes = [-17, -65, -96] bytes.length = 3
Từ đầu ra, chúng ta có thể thấy String.getBytes()
trả về ba byte. Điều này xảy ra như thế nào? Vì c
là 65504, ở dạng thập lục phân là 0xFFE0 (1111 1111 1110 0000). Dựa trên định nghĩa UTF-8, chúng ta có thể thấy:
Bits | Đầu tiên | Cuối cùng | Bytes | Byte 1 | Byte 2 | Byte 3 | Byte 4 | Byte 5 | Byte 6 |
---|---|---|---|---|---|---|---|---|---|
7 | U+0000 | U+007F | 1 | 0xxxxxxx |
|||||
11 | U+0080 | U+07FF | 2 | 110xxxxx |
10xxxxxx |
||||
16 | U+0800 | U+FFFF | 3 | 1110xxxx |
10xxxxxx |
10xxxxxx |
|||
21 | U+10000 | U+1FFFFF | 4 | 11110xxx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
||
26 | U+200000 | U+3FFFFFF | 5 | 111110xx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
|
31 | U+4000000 | U+7FFFFFFF | 6 | 1111110x |
10xxxxxx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
10xxxxxx |
Lưu ý: Thông số kỹ thuật ban đầu bao gồm các số lên đến 31 bit (giới hạn ban đầu của Bộ ký tự toàn cầu). Vào tháng 11 năm 2003, UTF-8 bị hạn chế bởi RFC 3629 ở mức U+10FFFF, để phù hợp với các ràng buộc của mã hóa ký tự UTF-16. Điều này đã loại bỏ tất cả các chuỗi 5 và 6 byte, và khoảng một nửa số chuỗi 4 byte.
Cần ba byte để lưu trữ giá trị 65504 này trong UTF-8. Ba byte đó là:
11101111 10111111 10100000
Giá trị nguyên của ba byte này ở dạng bù hai là:
-17 -65 -96
Đó là lý do tại sao chúng ta nhận được đầu ra ở trên.
Tiếp theo, hãy xem việc triển khai JDK của quá trình chuyển đổi này. Nó nằm trong lớp sun.nio.cs.UTF8.java
trong Java 8.
public int encode(char[] sa, int sp, int len, byte[] da) { int sl = sp + len; int dp = 0; int dlASCII = dp + Math.min(len, da.length); // Vòng lặp tối ưu hóa chỉ ASCII while (dp < dlASCII && sa[sp] < '\u0080') da[dp++] = (byte) sa[sp++]; while (sp < sl) { char c = sa[sp++]; if (c < 0x80) { // Có tối đa bảy bit da[dp++] = (byte)c; } else if (c < 0x800) { // 2 byte, 11 bit da[dp++] = (byte)(0xc0 | (c >> 6)); da[dp++] = (byte)(0x80 | (c & 0x3f)); } else if (Character.isSurrogate(c)) { if (sgp == null) sgp = new Surrogate.Parser(); int uc = sgp.parse(c, sa, sp - 1, sl); if (uc < 0) { if (malformedInputAction() != CodingErrorAction.REPLACE) return -1; da[dp++] = repl; } else { da[dp++] = (byte)(0xf0 | ((uc >> 18))); da[dp++] = (byte)(0x80 | ((uc >> 12) & 0x3f)); da[dp++] = (byte)(0x80 | ((uc >> 6) & 0x3f)); da[dp++] = (byte)(0x80 | (uc & 0x3f)); sp++; // 2 ký tự } } else { // 3 byte, 16 bit da[dp++] = (byte)(0xe0 | ((c >> 12))); da[dp++] = (byte)(0x80 | ((c >> 6) & 0x3f)); da[dp++] = (byte)(0x80 | (c & 0x3f)); } } return dp; }
Trước Java 8, mã nằm trong sun.io.CharToByteUTF8.java
.
public int convert(char[] input, int inOff, int inEnd, byte[] output, int outOff, int outEnd) throws ConversionBufferFullException, MalformedInputException { char inputChar; byte[] outputByte = new byte[6]; int inputSize; int outputSize; charOff = inOff; byteOff = outOff; if (highHalfZoneCode != 0) { inputChar = highHalfZoneCode; highHalfZoneCode = 0; if (input[inOff] >= 0xdc00 && input[inOff] <= 0xdfff) { // Đây là chuỗi UTF16 hợp lệ. int ucs4 = (highHalfZoneCode - 0xd800) * 0x400 + (input[inOff] - 0xdc00) + 0x10000; output[0] = (byte)(0xf0 | ((ucs4 >> 18)) & 0x07); output[1] = (byte)(0x80 | ((ucs4 >> 12) & 0x3f)); output[2] = (byte)(0x80 | ((ucs4 >> 6) & 0x3f)); output[3] = (byte)(0x80 | (ucs4 & 0x3f)); charOff++; highHalfZoneCode = 0; } else { // Đây là chuỗi UTF16 không hợp lệ. badInputLength = 0; throw new MalformedInputException(); } } while(charOff < inEnd) { inputChar = input[charOff]; if (inputChar < 0x80) { outputByte[0] = (byte)inputChar; inputSize = 1; outputSize = 1; } else if (inputChar < 0x800) { outputByte[0] = (byte)(0xc0 | ((inputChar >> 6) & 0x1f)); outputByte[1] = (byte)(0x80 | (inputChar & 0x3f)); inputSize = 1; outputSize = 2; } else if (inputChar >= 0xd800 && inputChar <= 0xdbff) { // điều này nằm trong UTF-16 if (charOff + 1 >= inEnd) { highHalfZoneCode = inputChar; break; } // kiểm tra ký tự tiếp theo có hợp lệ không char lowChar = input[charOff + 1]; if (lowChar < 0xdc00 || lowChar > 0xdfff) { badInputLength = 1; throw new MalformedInputException(); } int ucs4 = (inputChar - 0xd800) * 0x400 + (lowChar - 0xdc00) + 0x10000; outputByte[0] = (byte)(0xf0 | ((ucs4 >> 18)) & 0x07); outputByte[1] = (byte)(0x80 | ((ucs4 >> 12) & 0x3f)); outputByte[2] = (byte)(0x80 | ((ucs4 >> 6) & 0x3f)); outputByte[3] = (byte)(0x80 | (ucs4 & 0x3f)); outputSize = 4; inputSize = 2; } else { outputByte[0] = (byte)(0xe0 | ((inputChar >> 12)) & 0x0f); outputByte[1] = (byte)(0x80 | ((inputChar >> 6) & 0x3f)); outputByte[2] = (byte)(0x80 | (inputChar & 0x3f)); inputSize = 1; outputSize = 3; } if (byteOff + outputSize > outEnd) { throw new ConversionBufferFullException(); } for (int i = 0; i < outputSize; i++) { output[byteOff++] = outputByte[i]; } charOff += inputSize; } return byteOff - outOff; }
Đoạn mã này đang làm những gì được mô tả trong bảng trên.
Đối với bộ ký tự khác, bạn có thể làm theo cách tương tự để tìm ra các byte là gì khi gọi String.getBytes()
.
Một điều cần lưu ý từ bài viết này là hãy cẩn thận khi cố gắng gọi String.getBytes().length
và mong đợi nó giống với String.length()
, đặc biệt là khi có các thao tác byte cấp thấp trong ứng dụng của bạn, ví dụ: mã hóa và giải mã dữ liệu.