简介

字符串是咱们日常编码过程中应用到最多的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/

最艰深的解读,最粗浅的干货,最简洁的教程,泛滥你不晓得的小技巧等你来发现!

欢送关注我的公众号:「程序那些事」,懂技术,更懂你!