java string的一些细节剖析
程序员文章站
2024-02-09 18:30:52
首先说明这里指的是java中的string,虽然我已经决定转战c/c++了,但是因为今天碰到一个问题,还是来看一下。string的定义如下: 复制代码 代码如下: publ...
首先说明这里指的是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的效率是非常非常高的,所以这也算是一种很好的解决方法了。
第二种解决方法:统一编码格式
这边使用的是webx矿建,只需要将webx.xml中设置defaultcharset="utf-8"就可以了。
复制代码 代码如下:
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的效率是非常非常高的,所以这也算是一种很好的解决方法了。
第二种解决方法:统一编码格式
这边使用的是webx矿建,只需要将webx.xml中设置defaultcharset="utf-8"就可以了。