String.length() vs String.getBytes().length in Java

  Pi Ke        2015-04-01 22:22:23       115,576        0          English  简体中文  Tiếng Việt 

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 1Byte 2Byte 3Byte 4Byte 5Byte 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.

JAVA  UTF8  STRING  ENCODING  SAMPLE 

       

  RELATED


  0 COMMENT


No comment for this article.



  RANDOM FUN

Microsoft, do you have to do it like this?