java设置字符串编码(java字符串指定编码)
简介
字符串是我们日常编码过程中使用到最多的java类型了。全球各个地区的语言不同,即使使用了unicode也会因为编码格式的不同采用不同的编码方式,如utf-8,utf-16,utf-32等。
我们在使用字符和字符串编码的过程中会遇到哪些问题呢?一起来看看吧。
使用变长编码的不完全字符来创建字符串
在java中string的底层存储char[]是以utf-16进行编码的。
注意,在jdk9之后,string的底层存储已经变成了byte[]。
stringbuilder和stringbuffer还是使用的是char[]。
那么当我们在使用inputstreamreader,outputstreamwriter和string类进行string读写和构建的时候,就需要涉及到utf-16和其他编码的转换。
我们来看一下从utf-8转换到utf-16可能会遇到的问题。
先看一下utf-8的编码:
utf-8使用1到4个字节表示对应的字符,而utf-16使用2个或者4个字节来表示对应的字符。
转换起来可能会出现什么问题呢?
public string readbytewrong(inputstream inputstream) throws ioexception {
byte[] data = new byte[1024];
int offset = 0;
int bytesread = 0;
string str="";
while ((bytesread = inputstream.read(data, offset, data.length - offset)) != -1) {
str += new string(data, offset, bytesread, "utf-8");
offset += bytesread;
if (offset >= data.length) {
throw new ioexception("too much input");
}
}
return str;
}
上面的代码中,我们从stream中读取byte,每读一次byte就将其转换成为string。很明显,utf-8是变长的编码,如果读取byte的过程中,恰好读取了部分utf-8的代码,那么构建出来的string将是错误的。
我们需要下面这样操作:
public string readbytecorrect(inputstream inputstream) throws ioexception {
reader r = new inputstreamreader(inputstream, "utf-8");
char[] data = new char[1024];
int offset = 0;
int charread = 0;
string str="";
while ((charread = r.read(data, offset, data.length - offset)) != -1) {
str += new string(data, offset, charread);
offset += charread;
if (offset >= data.length) {
throw new ioexception("too much input");
}
}
return str;
}
我们使用了inputstreamreader,reader将会自动把读取的数据转换成为char,也就是说自动进行utf-8到utf-16的转换。
所以不会出现问题。
char不能表示所有的unicode
因为char是使用utf-16来进行编码的,对于utf-16来说,u+0000 to u+d7ff 和 u+e000 to u+ffff,这个范围的字符,可以直接用一个char来表示。
但是对于u+010000 to u+10ffff是使用两个0xd800–0xdbff和0xdc00–0xdfff范围的char来表示的。
这种情况下,两个char合并起来才有意思,单独一个char是没有任何意义的。
考虑下面的我们的的一个substring的方法,该方法的本意是从输入的字符串中找到第一个非字母的位置,然后进行字符串截取。
public static string substringwrong(string string) {
char ch;
int i;
for (i = 0; i < string.length(); i += 1) {
ch = string.charat(i);
if (!character.isletter(ch)) {
break;
}
}
return string.substring(i);
}
上面的例子中,我们一个一个的取出string中的char字符进行比较。如果遇到u+010000 to u+10ffff范围的字符,就可能报错,误以为该字符不是letter。
我们可以这样修改:
public static string substringcorrect(string string) {
int ch;
int i;
for (i = 0; i < string.length(); i += character.charcount(ch)) {
ch = string.codepointat(i);
if (!character.isletter(ch)) {
break;
}
}
return string.substring(i);
}
我们使用string的codepointat方法,来返回字符串的unicode code point,然后使用该code point来进行isletter的判断就好了。
注意locale的使用
为了实现国际化支持,java引入了locale的概念,而因为有了locale,所以会导致字符串在进行转换的过程中,产生意想不到变化。
考虑下面的例子:
public void touppercasewrong(string input){
if(input.touppercase().equals("joker")){
system.out.println("match!");
}
}
我们期望的是英语,如果系统设置了locale是其他语种的话,input.touppercase()可能得到完全不一样的结果。
幸好,touppercase提供了一个locale的参数,我们可以这样修改:
public void touppercaseright(string input){
if(input.touppercase(locale.english).equals("joker")){
system.out.println("match!");
}
}
同样的, dateformat也存在着问题:
public void getdateinstancewrong(date date){
string mystring = dateformat.getdateinstance().format(date);
}
public void getdateinstanceright(date date){
string mystring = dateformat.getdateinstance(dateformat.medium, locale.us).format(date);
}
我们在进行字符串比较的时候,一定要考虑到locale影响。
文件读写中的编码格式
我们在使用inputstream和outputstream进行文件对写的时候,因为是二进制,所以不存在编码转换的问题。
但是如果我们使用reader和writer来进行文件的对象,就需要考虑到文件编码的问题。
如果文件是utf-8编码的,我们是用utf-16来读取,肯定会出问题。
考虑下面的例子:
public void fileoperationwrong(string inputfile,string outputfile) throws ioexception {
bufferedreader reader = new bufferedreader(new filereader(inputfile));
printwriter writer = new printwriter(new filewriter(outputfile));
int line = 0;
while (reader.ready()) {
line++;
writer.println(line + ": " + reader.readline());
}
reader.close();
writer.close();
}
我们希望读取源文件,然后插入行号到新的文件中,但是我们并没有考虑到编码的问题,所以可能会失败。
上面的代码我们可以修改成这样:
bufferedreader reader = new bufferedreader(new inputstreamreader(new fileinputstream(inputfile), charset.forname("utf8")));
printwriter writer = new printwriter(new outputstreamwriter(new fileoutputstream(outputfile), charset.forname("utf8")));
通过强制指定编码格式,从而保证了操作的正确性。
不要将非字符数据编码为字符串
我们经常会有这样的需求,就是将二进制数据编码成为字符串string,然后存储在数据库中。
二进制是以byte来表示的,但是从我们上面的介绍可以得知不是所有的byte都可以表示成为字符。如果将不能表示为字符的byte进行字符的转化,就有可能出现问题。
看下面的例子:
public void convertbigintegerwrong(){
biginteger x = new biginteger("1234567891011");
system.out.println(x);
byte[] bytearray = x.tobytearray();
string s = new string(bytearray);
bytearray = s.getbytes();
x = new biginteger(bytearray);
system.out.println(x);
}
上面的例子中,我们将biginteger转换为byte数字(大端序列),然后再将byte数字转换成为string。最后再将string转换成为biginteger。
先看下结果:
1234567891011
80908592843917379
发现没有转换成功。
虽然string可以接收第二个参数,传入字符编码,目前java支持的字符编码是:ascii,iso-8859-1,utf-8,utf-8be, utf-8le,utf-16,这几种。默认情况下string也是大端序列的。
上面的例子怎么修改呢?
public void convertbigintegerright(){
biginteger x = new biginteger("1234567891011");
string s = x.tostring(); //转换成为可以存储的字符串
byte[] bytearray = s.getbytes();
string ns = new string(bytearray);
x = new biginteger(ns);
system.out.println(x);
}
我们可以先将biginteger用tostring方法转换成为可以表示的字符串,然后再进行转换即可。
我们还可以使用base64来对byte数组进行编码,从而不丢失任何字符,如下所示:
public void convertbigintegerwithbase64(){
biginteger x = new biginteger("1234567891011");
byte[] bytearray = x.tobytearray();
string s = base64.getencoder().encodetostring(bytearray);
bytearray = base64.getdecoder().decode(s);
x = new biginteger(bytearray);
system.out.println(x);
}
本文的代码:
learn-java-base-9-to-20/tree/master/security
本文已收录于
http://www.flydean.com/java-security-code-line-string/最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!