首先說明這裡指的是Java中的String,雖然我已經決定要轉戰C/C++了,但因為今天碰到一個問題,還是來看看。 String的定義如下:
複製代碼代碼如下:
public final class String
{
private final char value[]; // 儲存的字串
private final int offset; // 開始的位置
private final int count; // 字元數目
private int hash; // 快取的hash值
.....
}
在Debug的時候可以看到保存的值如下:
需要說明一下的是:如果沒有呼叫過hashCode(),那麼hash的值為0。容易知道這裡的value也就是真正保存的字串的值(也就是「字串測試」)的char數組,而每個char的值是多少呢?很容易驗證:Unicode。
到這裡大家也就猜到我們常用的subString是怎麼實現的了:如果是讓我們實作的話讓new String使用相同的value(char陣列),只修改offset和count就可以了。這樣的話既省空間又快(不需要拷貝),而事實上也是這樣的:
複製代碼代碼如下:
public String substring(int beginIndex) {
return substring(beginIndex, count);
}
public String substring(int beginIndex, int endIndex) {
.....
return ((beginIndex == 0) && (endIndex == count)) ? this :
new String(offset + beginIndex, endIndex - beginIndex, value);
}
String(int offset, int count, char value[]) {
this.value = value;
this.offset = offset;
this.count = count;
}
既然是在討論字串,JVM預設使用的是什麼編碼呢?透過調試可以發現:
複製代碼代碼如下:
public static Charset defaultCharset() {
if (defaultCharset == null) {
synchronized (Charset.class) {
java.security.PrivilegedAction pa = new GetPropertyAction("file.encoding");
String csn = (String)AccessController.doPrivileged(pa);
Charset cs = lookup(csn);
if (cs != null)
defaultCharset = cs;
else
defaultCharset = forName("UTF-8");
}
}
其中defaultCharset的值可以通過:
-Dfile.encoding=utf-8
進行設定。當然如果你想設定為「abc」也可以,但會預設為UTF-8。可以透過System.getProperty("file.encoding")來看具體的值。看defaultCharset是為什麼呢?因為網路傳輸的過程應該都是byte數組,不同的編碼方式得到的byte數組可能是不相同的。所以,我們得知道編碼方式是怎麼得到的吧?具體得到byte數組的方法也就是我們下面重點要看的getBytes了,它最終要呼叫的是CharsetEncoder的encode方法,如下:
複製代碼代碼如下:
public final CoderResult encode(CharBuffer in, ByteBuffer out, boolean endOfInput) {
int newState = endOfInput ? ST_END : ST_CODING;
if ((state != ST_RESET) && (state != ST_CODING) && !(endOfInput && (state == ST_END)))
throwIllegalStateException(state, newState);
state = newState;
for (;;) {
CoderResult cr;
try {
cr = encodeLoop(in, out);
} catch (BufferUnderflowException x) {
throw new CoderMalfunctionError(x);
} catch (BufferOverflowException x) {
throw new CoderMalfunctionError(x);
}
if (cr.isOverflow())
return cr;
if (cr.isUnderflow()) {
if (endOfInput && in.hasRemaining()) {
cr = CoderResult.malformedForLength(in.remaining());
} else {
return cr;
}
}
CodingErrorAction action = null;
if (cr.isMalformed())
action = malformedInputAction;
else if (cr.isUnmappable())
action = unmappableCharacterAction;
else
assert false : cr.toString();
if (action == CodingErrorAction.REPORT)
return cr;
if (action == CodingErrorAction.REPLACE) {
if (out.remaining() < replacement.length)
return CoderResult.OVERFLOW;
out.put(replacement);
}
if ((action == CodingErrorAction.IGNORE) || (action == CodingErrorAction.REPLACE)) {
in.position(in.position() + cr.length());
continue;
}
assert false;
}
}
當然首先會根據所需的編碼格式選擇對應的CharsetEncoder,而最主要的是不同的CharsetEncoder實作了不同的encodeLoop方法。這裡可能會不懂為什麼這裡有個for(;;)?其實看CharsetEncoder所處的包(nio)和它的參數也就大概明白了:這個函數是可以處理流的(雖然我們這裡使用的時候不會循環)。
在encodeLoop方法中會將盡可能多的char轉換成byte,new String差不多就是上面的逆過程。
在實際的開發過程中常會遇到亂碼問題:
在上傳文件的時候取到文件名;
JS傳到後端的字串;
首先先嘗試下下面程式碼的運行結果:
複製代碼代碼如下:
public static void main(String[] args) throws Exception {
String str = "字串";
// -41 -42 -73 -5 -76 -82
printArray(str.getBytes());
// -27 -83 -105 -25 -84 -90 -28 -72 -78
printArray(str.getBytes("utf-8"));
// ???
System.out.println(new String(str.getBytes(), "utf-8"));
// 瀛涓?
System.out.println(new String(str.getBytes("utf-8"), "gbk"));
// 字元??
System.out.println(new String("瀛涓?".getBytes("gbk"), "utf-8"));
// -41 -42 -73 -5 63 63
printArray(new String("瀛涓?".getBytes("gbk"), "utf-8").getBytes());
}
public static void printArray(byte[] bs){
for(int i = 0; i < bs.length; i++){
System.out.print(bs[i] + " ");
}
System.out.println();
}
在程式中的註解中說明了輸出結果:
因為GBK中2個byte表示一個漢字,所以就有了6個byte;
因為UTF-8中3個byte表示一個漢字,所以就有了9個byte;
因為透過無法透過GBK產生的byte陣列再根據UTF-8的規則去產生字串,所以顯示???;
這是常常遇到亂碼的原因,GBK使用UTF-8產生的byte能產生字串;
雖然上面產生的是亂碼,但是電腦不這麼認為,所以還是能透過getBytes得到位元組數組,而這個數組中是utf-8是可以辨識的;
最後的兩個63(?)應該是encode填充的(或是位元組不夠直接填充的,這個地方沒有細看);
GBK和UTF-8對於因為字母和數字的編碼是相同的,所以在這幾種字符的處理上是不會出現亂碼的,但是他們對漢字的編碼確實不一樣的,這就是很多問題的起源,看下面程式碼:
new String(new String("我們".getBytes("UTF-8"), "GBK").getBytes("GBK"), "UTF-8);
顯然這段程式碼的結果是“我們”,但是對我們有什麼用?首先我們注意到:
new String("我們".getBytes("UTF-8"), "GBK");
這段程式碼的結果是亂碼,而且很多的亂碼都是「亂成這樣的」。但要記住:這裡的亂是對我們而言,對電腦來說無所謂“亂”與“不亂”,它在我們幾乎放棄的時候還能從亂碼中通過“getBytes("GBK")”得到它的“主心骨”,然後我們就可以用“主心骨”還原出原來的字串。
看起來上面的這段程式碼能解決「GBK」和「UTF-8」之間的亂碼問題,但這種解法也只限於一種特殊情況:所有連續漢字的個數都是偶數個!原因在上面已經說過了,這裡就不贅述了。
那要怎麼解決這個問題呢?
第一種解決方法:encodeURI為什麼要用這種方法呢?原因很簡單:GBK和UTF-8對於%、數字、字母的編碼是統一的,所以在傳輸encode之後的串可以100%保證在這兩種編碼下得到的是同一個東西,然後再decode得到字符串就可以。根據String的格式可以猜測encode和decode的效率是非常非常高的,所以這也算是很好的解決方法了。
第二種解決方法:統一編碼格式<BR>這邊使用的是Webx礦建,只需要將webx.xml中設定defaultCharset="UTF-8"就可以了。