共计 5033 个字符,预计需要花费 13 分钟才能阅读完成。
简介
字符串是咱们日常编码过程中应用到最多的 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/
最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
欢送关注我的公众号:「程序那些事」, 懂技术,更懂你!