简介
字符串是咱们日常编码过程中应用到最多的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。
先看下后果:
123456789101180908592843917379
发现没有转换胜利。
尽管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/最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!
欢送关注我的公众号:「程序那些事」,懂技术,更懂你!