Java 中字符编码相关的问题解析

字符集与字符编码

严格来说,一个字符在计算机中的表示形式,与字符集字符编码两方面相关。字符集定义一个字符对应的码位,而字符编码则定义码位在计算机中如何由一个或几个byte组合起来表示。
但在很多地方(如Java、MySQL),并不严格区分字符集与字符编码。这其中一个原因,可能是大多数字符集只对应了一种字符编码,如US-ASCII、GBK编码等。Unicode字符集比较特殊,对应了多种编码方式,如UTF-8、UTF-16BE等。
也就是说,字符集和字符编码之间,是一对一或一对多的关系。
在Java中,Charset类直接用来表示各种字符编码的名称(因为字符编码肯定只对应一个字符集),如Java中Charset的名称包括UTF-8、UTF-16BE、UTF-32BE、US-ASCII、Big5、GB2312、GBK、GB18030等。而且一个Charset名称可能会有多个别名,如 UTF-8 的别名有 unicode-1-1-utf-8、UTF8,UTF-16 的别名有 UTF_16、unicode、utf16、UnicodeBig。

Java虚拟机(JVM)的默认字符集

Java中有个系统属性"file.encoding",从字面意思看就是制定文件的默认编码方式。该值依赖于所在地区与底层操作系统的字符集。我在我的 Windows 10 的 JDK8 上测试得到的"file.encoding"默认为UTF-16BE
Java类库中的Charset类有个静态方法defaultCharset(),用于获取JVM的默认字符集。该方法在JDK8中的实现逻辑如下:

    public static Charset defaultCharset() {
        if (defaultCharset == null) {
            synchronized (Charset.class) {
                String csn = AccessController.doPrivileged(
                    new GetPropertyAction("file.encoding"));
                Charset cs = lookup(csn);
                if (cs != null)
                    defaultCharset = cs;
                else
                    defaultCharset = forName("UTF-8");
            }
        }
        return defaultCharset;
    }

从代码中可以看出:

  • 如果指定了"file.encoding"系统属性,则JVM默认字符集就是"file.encoding"的指定值。
  • 如果没有指定"file.encoding"系统属性,则JVM默认字符集是"UTF-8"。
  • 该方法只会在第一次调用时通过"file.encoding"初始化默认字符集。后续如果改了"file.encoding",defaultCharset()的返回值不会改变。

Java中涉及编码的地方主要由3个(URL编码和Base64编码除外):String对象相关的编码、Java工程中文件存储的编码,以及ByteBuffer相关的编码。

String对象相关的编码

首先要明白,Java 中的字符char、字符数组char[],在内存中保存的都是字符在Unicode字符集中的码位(Code Point,或称码点),是一个两字节的整数。
通过查看String的源码,我们不难发现String的底层表示是char[]。即我们通过某种方法,在将一段文本S(如"时间的朋友")转换成String对象以后,String对象就会保存下S中的每个字符对应的Unicode码,将这些Unicode码组成一个char[]

String对象中与编码相关的方法有两类,一类是String传入byte[]的构造方法new String(),另一类是getBytes()方法。

new String()在以byte[]构造字符串时,可以指定字符集,如果未指定则用JVM默认字符集对字节数组进行解码,从而得到一个char[]数组。

getBytes()也可以指定字符集,如果不指定,则与new String()方法一样,根据JVM默认的字符集进行编码。

文件存储的编码

写入文件的是一个个byte,因此需要对一个个字节进行编码,将其表示成若干个byte写入文件中。

在Java的IO库中,有流(OutputStream、InputStream)和XX器(Writer、Reader)两类。前者流是直接操作byte的,输入输出都是byte,而Writer、Reader的内部处理则涉及char与byte的转换,因此需要指定字符集。

抽象类 Writer 有子类 OutputStreamWriter,在构造它的对象时可以指定一个Charset。如果未指定,则默认会采用JVM的默认字符集(也就是"file.encoding"),来进行char到byte的转换,从而写入文件中。
而 OutputStreamWriter 有子类 FileWriter,其构造方法没有指定Charset的参数,它是采用父类OutputStreamWriter的默认字符集(即"file.encoding")来进行写文件的。

抽象类 Reader 与 Writer 类似,其子类 InputStreamReader 的构造器也可以指定Charset,如果未指定则采用JVM的默认字符集(也就是"file.encoding"),来进行byte到char的转换,将数据从文件读入内存中。
InputStreamReader 的子类 FileReader 也类似,构造方法没有指定Charset的参数,采用的父类InputStreamReader的默认字符集(即"file.encoding")来进行读文件的。

ByteBuffer相关的编码

在调用 ByteBuffer 的asCharBuffer()方法时,ByteBuffer 默认按UTF-16BE的编码方式解析其内部的byte数组。
当然,是BIG_ENDIAN(即高位字节放在前面,默认) 还是 LITTLE_ENDIAN,可以通过ByteBuffer的order(ByteOrder.LITTLE_ENDIAN)进行设置。

因此,如果存入ByteBuffer的字符串对应的byte[]不是按UTF-16BE或UTF-16LE(此时需要显式设置ByteBuffer的order,这样在解码时才不会出错)编码得到的,那么在调用其asCharBuffer()时,就无法正确解码得到字符串。

在对ByteBuffer中的数据解码时,也可以直接通过其array()方法获取底层的byte数组,然后用new String()(指定解码的字符集),或者通过特定字符集的 Charset 对象的 decode(ByteBuffer) 方法,对 ByteBuffer 进行解码。

Java中的UTF-16与UTF-16BE,UTF-16LE

Java中的UTF-16默认会按BIG_ENDIAN进行编解码。
在Java中运行"abc".getBytes("UTF-16"),得到的数组是[-2, -1, 0, 97, 0, 98, 0, 99]。其中,前两个字节是BOM(Byte-Order Mark)0xFEFF,后面两个6个字节,两两一组(因为是BIG_ENDIAN,故高位为0),分别是a、b、c的 Unicode。

原文地址:https://www.cnblogs.com/haycheng/p/14340486.html