[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

  • 最近解析 以太网 SOME/IP 报文时,涉及到解析数据类型为 String(标准的Unicode字符集编码的文本序列,且细分为 动态 String [存在 32 bit 的 StringFieldLength] 和 静态 String ) 的场景。

尤其是 Struct 嵌套 String 时,还涉及到 CPU 的内存对齐(alignment)问题。

  • 言归正传,本文对 Unicode 字符集 做个总结。
  • 不想看 Unicode 不同细分字符集的原理的,可直接跳过 【Unicode字符集原理剖析】章节
  • 不了解【大小端/字节序(Endian)】的朋友,可参考阅读此篇:
  • 关于 Unicode 字符集 与 BOM 的原理、技术实现/代码实现的帖子,全网比较缺乏。

此篇应该是相对详实且务实的一篇。(文末附有一个文本转Unicode字符集的工具类)

概述: Unicode字符集

字符编码

字符编码的定义

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

  • 字符编码是指将字符集合(如字母、数字、标点符号、汉字等文本序列)映射到数字(通常为字节序列)的一种方法。

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

常见的字符编码

  • ASCII:一种7位编码方式,主要用于表示英语字符。
  • ISO-8859-1:又称Latin-1,用于表示西欧语言字符。
  • GB2312/GBK:主要用于简体中文和繁体中文字符的编码。GBK是GB2312的扩展,能够表示更多中文字符。
  • Unicode:一种统一的字符集,旨在表示全球所有文字。
  • UTF-8:一种Unicode编码实现,使用1至4个字节表示一个字符,兼容ASCII,广泛应用于互联网。
  • UTF-16:另一种Unicode编码实现,通常使用2个或4个字节表示一个字符,在Java内部通常采用UTF-16来表示字符串。
  • ASCII编码

ASCII编码每个字母或符号占1byte(8bits),并且8bits的最高位是0。因此,ASCII能编码的字母和符号只有128个。
有一些编码把8bits最高位为1的后128个值也编码上,使得1byte可以表示256个值,但是这属于扩展的ASCII,并非标准ASCII
通常所说的标准ASCII只有前128个值!
ASCII编码几乎被世界上所有编码所兼容(UTF16和UTF32是个例外)。
因此,如果一个文本文档里面的内容全都由ASCII里面的字母或符号构成,那么不管你如何展示该文档的内容,都不可能出现乱码的情况。

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

  • 在Java中,字符串是以Unicode形式存储的。

具体来说,Java中的String对象内部使用UTF-16编码。
因此,在进行编码转换时,通常需要将字节数据按照源编码解码成Java内部的Unicode字符串,再按照目标编码转换成字节序列输出。

常见中文字符集编码的关系

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

GB18030 / GBK / GB2312 / ASCII / UTF-8

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

文本编码转换的常见问题

在实际应用中,字符编码转换可能会遇到以下问题:

  • 乱码问题: 如果输入数据按照错误的编码解码,或者输出数据采用错误的编码写入,会导致显示乱码。

常见场景是中文字符在UTF-8和GBK之间转换不当。

  • 数据丢失: 部分编码格式可能无法表示某些字符,转换过程中可能导致数据丢失或替换成占位符(例如“?”)。
  • 效率问题: 对于大文件或大量数据转换,编码转换的效率也需要考虑,尤其是涉及到网络传输或实时处理时。

Java JDK的字符编码转换API

Java为我们提供了丰富的API来处理编码转换,主要包括:

  • String.getBytes(String charsetName):可以将一个字符串按照指定的字符集转换成字节数组。
  • new String(byte[] bytes, String charsetName):可以将字节数组按照指定字符集解码成字符串。
  • java.nio.charset.Charset: 提供了对字符集对象的支持,可以通过Charset.forName("UTF-8")等方法获取字符集实例。
  • java.io.InputStreamReaderOutputStreamWriter: 可以在流操作中指定编码格式,从而实现文件的编码转换。

Unicode 字符集的 BOM := Byte Order Mark := 字符顺序标记

  • BOM(Byte Order Mark)在分析unicode之前,先把bom(byte order mark)说一下。

bomunicode字符顺序的标识符号,一般以魔数(magic code)的形式出现在以Unicode字符编码的文件的开始的头部,作为该文件的编码标识。

  • 举个很简单的例子:

在 windows 下新建一个文本文件,并另存为 utf8 的文件格式。
该文件里面没有任何内容,我们再用Hex Edit来查看该文件的二进制内容:

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

UTF-8

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

[计算机组成原理/Java] 字符集编码: Unicode 字符集(UTF8/UTF16/UTF32) / `BOM`(Byte Order Mark/字节序标记) / UnicodeTextUtils

UTF-8 with BOM

0xEF BB BF就是这个文件的bom, 这也就是标识该文件是以utf8为编码格式的。

带 BOM 的 Unicode 文本 vs. 不带 BOM 的 Unicode 文本

  • UTF-8UTF-16UTF-32 还区分带 BOM 的以及不带 BOM 的 Unicode 文本。
  • BOM 的全称为 byte-order mark,即字节顺序标记,它是插入到以UTF-8、UTF16或UTF-32编码Unicode文件开头的特殊标记。

这些标记对于 UTF-8 来说并不是必须的。所以,我们们可以将带有 BOMUTF-8 转换为 UTF-8

Unicode 字符集 BOM的对应关系

下面来看看字符编码与其bom的对应关系

字符编码 Bom (十六进制)
UTF-8 EF BB BF
UTF-16 (BE) 大端 FE FF
UTF-16 (LE) 小端 FF FE
UTF-32 (BE) 大端 00 00 FE FF
UTF-32 (LE) 小端 FF FE 00 00
GB-18030 84 31 95 33

Unicode字符集原理剖析

UTF-8编码剖析

  • Unicode编码以code point来标识每一个字符, code point 的范围是
    0x000000 – 0x10FFFF

也就是每一个字符的code point都落在这个范围
utf8一个字符可以用1-4字节来表示,可能有人会说这code point最大也就是0x10FFFF,为什么最大不是可以用三个字节表示呢?那是因为utf8有自己独特的表示格式,先来看看下面的对应关系:

字节数 字符code point位数 最小的code point 最大的code point 第一个字节 第二个字节 第三个字节 第四个字节
1 7 U+0000 U+007F 0XXXXXXX
2 11 U+0080 U+07FF 110XXXXX 10XXXXXX
3 16 U+0800 U+FFFF 1110XXXX 10XXXXXX 10XXXXXX
4 21 U+10000 U+10FFFF 11110XXX 10XXXXXX 10XXXXXX 10XXXXXX
  • 当某个字符的code point (cp简称) U+0000 <= cp <= U+007F 落在这个范围内

这时只需要一个字节来表示 0XXXXXXX,将该字符的code point (7位)填入X的位置,就可以得到该字符的utf8的编码后的格式了。
我们以小写字母a举个例子,a的code point是01100001, 经过utf8编码后 01100001(0x61)

  • 例如,中文汉字 code point 为 0x52A0 二进制格式 ‭0101 0010 1010 0000

按照上表中的规则,该字符需要用3个字节来表示
按照填充规则 ,第一个字节 1110XXXX -> 11100101 , 第二个字节10XXXXXX -> 10001010 , 第三个字节10XXXXXX -> 10100000

组合起来就是 11100101 10001010 10100000 := ‭HEX-> 0xE58AA0‬

UTF-16编码剖析

  • utf-16编码的单元是2个字节,也就是16位。

utf-16编码格式在程序内存里经常使用,因为它比较高效,
java中Character 字符用的就是utf-16编码格式
在早期的时候,世界上所有的字符都可以用两个字节标识,也就是code point范围 U+0000 – U+FFFF,这样utf-16就可以很好的表示了,而且也不用像utf8那样按照固定的模板组合,可以直接用字符的code point表示,非常高效。

但是随着时间的推移,所有字符远远不能用两个字节的code point 表示了,那为了兼容code point 超过U+FFFF的字符 就出现字符代理对(Surrogate pair), utf16就是使用代理对来表示code point 范围在 U+10000 -> U+10FFFF之间的字符,当然也就的使用四个字节来表示该字符了。
对于Surrogate pair 与code point 之间的对应关系算法,等会儿再说。
先来看下utf16对于code point 小与U+10000的字符表示,其实用的就是字符的code point表示,这里还区分了大小端的表示法。

  • 案例

还是来看中文汉字 code point 为 0x52A0, 推测一下:
如果用utf16大端存储,那就是0x52A0;
如果用utf16小端存储,那就是0xA052

UTF-32编码剖析

  • utf-32用4个字节表示一个字符
  • 直接用字符的code point表示,非常高效,不需要任何的转化操作
  • 占用的存储空间却是很大的,会有空间的浪费。
  • 例如:小写字母a

code point0x61
utf32表示就是大端 -> 0x00 00 00 61 ; 小端 -> 0x61 00 00 00
这样会造成存储空间的浪费,当然应用场景不同而已,当追求高效的转换而忽略存储空间的浪费这个问题,utf32编码格式是比较好的选择。
而utf8的原则是尽可能的节省存储空间,牺牲转化的效率,各有各的好处。

判别Unicode文本的字符集的方法(Java) 【废弃/不可靠】

亲测,此方法并可绝对可靠(尤其是结果为 UTF-8 的情况)。

    /**      * 获取 Unicode 文本的字符集      * @param textBytes      * @return      */     public static Charset getUnicodeTextCharset(byte[] textBytes){         String encoding = null;         int bomSize = 4;//BOM_SIZE;         byte bom[] = new byte[bomSize];         int n, unread;         //n = internalIn.read(bom, 0, bom.length);          //读取 bom         int off = 0;         int len = bom.length;         int pos = 0;          if (bom == null) {             throw new NullPointerException();         } else if (off < 0 || len < 0 || len > bom.length - off) {             throw new IndexOutOfBoundsException();         }         int avail = bom.length <= textBytes.length ? bom.length : textBytes.length ;//算 bom.length 与 textBytes.length 的最小值         if (avail > 0) {             System.arraycopy(textBytes, pos, bom, off, avail);         }          //判断 unicode 字符集         if ((bom[0] == (byte) 0x00) && (bom[1] == (byte) 0x00)                 && (bom[2] == (byte) 0xFE) && (bom[3] == (byte) 0xFF)) {             encoding = "UTF-32BE";             //unread = n - 4;         } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)                 && (bom[2] == (byte) 0x00) && (bom[3] == (byte) 0x00)) {             encoding = "UTF-32LE";             //unread = n - 4;         } else if ((bom[0] == (byte) 0xEF) && (bom[1] == (byte) 0xBB)                 && (bom[2] == (byte) 0xBF)) {             encoding = "UTF-8";//utf08 with bom             //unread = n - 3;         } else if ((bom[0] == (byte) 0xFE) && (bom[1] == (byte) 0xFF)) {             encoding = "UTF-16BE";             //unread = n - 2;         } else if ((bom[0] == (byte) 0xFF) && (bom[1] == (byte) 0xFE)) {             encoding = "UTF-16LE";             //unread = n - 2;         } else {             // Unicode BOM mark not found, unread all bytes             //defaultEncoding = defaultEncoding == null ? Charset.defaultCharset().name() : defaultEncoding;             //defaultEncoding = defaultEncoding == null ? null : defaultEncoding;             //encoding = defaultEncoding;             //unread = n;             encoding = "UTF-8";//默认: UTF-8 (without bom)         }         // System.out.println("read=" + n + ", unread=" + unread);         return Charset.forName(encoding);     } 

最佳实践

UnicodeTextUtils : Unicode文本处理工具类

UnicodeCharsetEnum

import com.xxx.sdk.pojo.text.enums.DigitalModeEnum;  /**  * Unicode 字符集  * @updateTime 2025.6.17 19:48  */ public enum UnicodeCharsetEnum {     UTF8_WITH_BOM("UTF8_WITH_BOM", "UTF-8 With BOM", "UTF-8"),     UTF8_WITHOUT_BOM("UTF8_WITHOUT_BOM", "UTF-8 Without BOM", "UTF-8"),      //小端     UTF16LE_WITH_BOM("UTF16LE_WITH_BOM", "UTF-16LE With BOM", "UTF-16LE"),     UTF16LE_WITHOUT_BOM("UTF16LE_WITHOUT_BOM", "UTF-16LE Without BOM", "UTF-16LE"),      //大端     UTF16BE_WITH_BOM("UTF16BE_WITH_BOM", "UTF-16BE With BOM", "UTF-16BE"),     UTF16BE_WITHOUT_BOM("UTF16BE_WITHOUT_BOM", "UTF-16BE Without BOM", "UTF-16BE"),      //小端     UTF32LE_WITH_BOM("UTF32LE_WITH_BOM", "UTF-32LE With BOM",  "UTF-32LE"),     UTF32LE_WITHOUT_BOM("UTF32LE_WITHOUT_BOM", "UTF-32LE Without BOM",  "UTF-32LE"),      //大端     UTF32BE_WITH_BOM("UTF32BE_WITH_BOM", "UTF-32BE With BOM", "UTF-32BE"),     UTF32BE_WITHOUT_BOM("UTF32BE_WITHOUT_BOM", "UTF-32BE Without BOM", "UTF-32BE");      private final String charsetCode;     private final String charsetName;     //java中定义的字符集     private final String javaCharset;       public final static String CODE_PARAM = "code";     public final static String NAME_PARAM = "name";      UnicodeCharsetEnum(String charsetCode, String charsetName, String javaCharset) {         this.charsetCode = charsetCode;         this.charsetName = charsetName;         this.javaCharset = javaCharset;     }      public static UnicodeCharsetEnum findByCharsetCode(String charsetCode) {         for (UnicodeCharsetEnum type : values()) {             if (type.getCharsetCode().equals(charsetCode)) {                 return type;             }         }         return null;     }      public static UnicodeCharsetEnum findByCharsetName(String charsetName) {         for (UnicodeCharsetEnum type : values()) {             if (type.getCharsetName().equals(charsetName)) {                 return type;             }         }         return null;     }       public String getCharsetName() {         return charsetName;     }      public String getCharsetCode() {         return charsetCode;     }      public String getJavaCharset() {         return javaCharset;     } } 

UnicodeTextUtils

import com.xxx.sdk.pojo.text.UnicodeCharsetEnum;  import java.io.UnsupportedEncodingException;  /**  * Unicode 文本处理工具类  * @updateTime 2025.6.17 19:47  */ public class UnicodeTextUtils {     /**      * 将指定文本转换为指定 Unicode 字符集的字节数组      * @param text Java 字符串      *     eg: "hello world!你好!"      * @param unicodeCharset      *     eg: UTF8_WITH_BOM      * @return 指定 Unicode 字符集的字节数组      * @usage String newText = new String( textToBytes(text="hello world!你好!", UTF8_WITH_BOM) ,  UTF8_WITH_BOM.charset)      */     public static byte [] textToBytes(String text, UnicodeCharsetEnum unicodeCharset) throws UnsupportedEncodingException {         byte [] textBytes = null;         switch (unicodeCharset) {             // UTF8 不涉及 字节序(大小端)问题 (每个文本字符的最小单元: 1 byte)             case UTF8_WITH_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-8"                 int bomLength = 3;                 byte [] textBytes2 = new byte [textBytes1.length + bomLength];//预留 3个字节,填充 bom                 System.arraycopy(textBytes1, 00, textBytes2, 0 + bomLength, textBytes1.length);                 textBytes2[0] = (byte)0xef;                 textBytes2[1] = (byte)0xbb;                 textBytes2[2] = (byte)0xbf;                  //text == newText == "hello world!你好!", newText == [ (byte)0xef, (byte)0xbb, (byte)0xbf, 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, -28, -67, -96, -27, -91, -67, 33 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes2;                 break;             }             case UTF8_WITHOUT_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-8"                  //text == newText == "hello world!你好!", newText == [ 104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100, 33, -28, -67, -96, -27, -91, -67, 33 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes1;                 break;             }              //UTF16 设计 字节序(大小端)问题 (每个文本字符的最小单元: 2 byte)             case UTF16LE_WITH_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-16LE"                 int bomLength = 2;                 byte [] textBytes2 = new byte [textBytes1.length + bomLength];//预留 2个字节,填充 bom                 System.arraycopy(textBytes1, 00, textBytes2, 0 + bomLength, textBytes1.length);                 textBytes2[0] = (byte)0xff;                 textBytes2[1] = (byte)0xfe;                 //text == newText == "hello world!你好!", newText == [ 0xff/-1, 0xfe/-2, 104, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 119, 0, 111, 0, 114, 0, 108, 0, 100, 0, 33, 0, 96, 79, 125, 89, 33, 0 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes2;                 break;             }             case UTF16LE_WITHOUT_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-16LE"                  //text == newText == "hello world!你好!", newText == [ 104, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 119, 0, 111, 0, 114, 0, 108, 0, 100, 0, 33, 0, 96, 79, 125, 89, 33, 0 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes1;                 break;             }              case UTF16BE_WITH_BOM : {                 //方法1                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-16BE"                 int bomLength = 2;                 byte [] textBytes2 = new byte [textBytes1.length + bomLength];//预留 2个字节,填充 bom                 System.arraycopy(textBytes1, 00, textBytes2, 0 + bomLength, textBytes1.length);                 textBytes2[0] = (byte)0xfe;                 textBytes2[1] = (byte)0xff;                  //方法2                 //byte [] textBytes2 = (new String( text )).getBytes( "UTF-16" );//仅适用于 utf16 BE with bom(0xfe = -2, 0xff=-1)                  //text == newText == "hello world!你好!", newText == [ 0xfe/-2, 0xff/-1, 0, 104, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 119, 0, 111, 0, 114, 0, 108, 0, 100, 0, 33, 79, 96, 89, 125, 0, 33 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes2;                 break;             }             case UTF16BE_WITHOUT_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-16BE"                  //text == newText == "hello world!你好!", newText == [ 0, 104, 0, 101, 0, 108, 0, 108, 0, 111, 0, 32, 0, 119, 0, 111, 0, 114, 0, 108, 0, 100, 0, 33, 79, 96, 89, 125, 0, 33 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes1;                 break;             }              //UTF32 设计 字节序(大小端)问题 (每个文本字符的最小单元: 4 byte)             case UTF32LE_WITH_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-32LE"                 int bomLength = 4;                 byte [] textBytes2 = new byte [textBytes1.length + bomLength];//预留 4个字节,填充 bom                 System.arraycopy(textBytes1, 00, textBytes2, 0 + bomLength, textBytes1.length);                 textBytes2[0] = (byte)0xff;                 textBytes2[1] = (byte)0xfe;                 textBytes2[2] = (byte)0x00;                 textBytes2[3] = (byte)0x00;                  //text == newText == "hello world!你好!", newText ==                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes2;                 break;             }             case UTF32LE_WITHOUT_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-32LE"                  //text == newText == "hello world!你好!", newText ==                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes1;                 break;             }              case UTF32BE_WITH_BOM : {                 //方法1                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-32BE"                 int bomLength = 4;                 byte [] textBytes2 = new byte [textBytes1.length + bomLength];//预留 2个字节,填充 bom                 System.arraycopy(textBytes1, 00, textBytes2, 0 + bomLength, textBytes1.length);                 textBytes2[0] = (byte)0x00;                 textBytes2[1] = (byte)0x00;                 textBytes2[2] = (byte)0xfe;                 textBytes2[3] = (byte)0xff;                  //text == newText == "hello world!你好!", newText ==                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes2;                 break;             }             case UTF32BE_WITHOUT_BOM : {                 byte [] textBytes1 = (new String( text )).getBytes( unicodeCharset.getJavaCharset() );//"UTF-32BE"                  //方法2                 //byte [] textBytes2 = (new String( text )).getBytes( "UTF-32" );//仅适用于 utf32 BE without bom(0x00, 0x00, 0xfe = -2, 0xff=-1)                  //text == newText == "hello world!你好!", newText == [ 0, 0, 0, 104, 0, 0, 0, 101, 0, 0, 0, 108, 0, 0, 0, 108, 0, 0, 0, 111, 0, 0, 0, 32, 0, 0, 0, 119, 0, 0, 0, 111, 0, 0, 0, 114, 0, 0, 0, 108, 0, 0, 0, 100, 0, 0, 0, 33, 0, 0, 79, 96, 0, 0, 89, 125, 0, 0, 0, 33 ]                 //String newText = new String( textBytes2, unicodeCharset.getJavaCharset() );                 textBytes = textBytes1;                 break;             }             default: {                 //do nothing                 break;             }         }          return textBytes;     } } 

UnicodeTextUtilsTest

package com.xxx.sdk.utils.text;   import com.xxx.sdk.pojo.text.UnicodeCharsetEnum; import com.xxx.sdk.utils.bytes.BytesUtils; import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.Test;  @Slf4j public class UnicodeTextUtilsTest {     @Test     public void textToBytesTest() throws Exception {         String text = "hello world!你好!";          //efbbbf68656c6c6f20776f726c6421e4bda0e5a5bd21         log.info( "UTF8_WITH_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF8_WITH_BOM) ));         //68656c6c6f20776f726c6421e4bda0e5a5bd21         log.info( "UTF8_WITHOUT_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF8_WITHOUT_BOM) ));          //fffe680065006c006c006f00200077006f0072006c0064002100604f7d592100         log.info( "UTF16LE_WITH_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF16LE_WITH_BOM) ));         //680065006c006c006f00200077006f0072006c0064002100604f7d592100         log.info( "UTF16LE_WITHOUT_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF16LE_WITHOUT_BOM) ));         //feff00680065006c006c006f00200077006f0072006c006400214f60597d0021         log.info( "UTF16BE_WITH_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF16BE_WITH_BOM) ));         //00680065006c006c006f00200077006f0072006c006400214f60597d0021         log.info( "UTF16BE_WITHOUT_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF16BE_WITHOUT_BOM) ));          //fffe000068000000650000006c0000006c0000006f00000020000000770000006f000000720000006c0000006400000021000000604f00007d59000021000000         log.info( "UTF32LE_WITH_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF32LE_WITH_BOM) ));         //68000000650000006c0000006c0000006f00000020000000770000006f000000720000006c0000006400000021000000604f00007d59000021000000         log.info( "UTF32LE_WITHOUT_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF32LE_WITHOUT_BOM) ));         //0000feff00000068000000650000006c0000006c0000006f00000020000000770000006f000000720000006c000000640000002100004f600000597d00000021         log.info( "UTF32BE_WITH_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF32BE_WITH_BOM) ));         //00000068000000650000006c0000006c0000006f00000020000000770000006f000000720000006c000000640000002100004f600000597d00000021         log.info( "UTF32BE_WITHOUT_BOM:{}", BytesUtils.bytesToHexString( UnicodeTextUtils.textToBytes(text, UnicodeCharsetEnum.UTF32BE_WITHOUT_BOM) ));     } } 

UTF-16 并不是一个完美的选择————没有完美的银弹字符集

  • UTF-16 存在几个方面的问题:

UTF-16 能表示的字符数有 6 万多,看起来很多,但是实际上目前 Unicode 5.0 收录的字符已经达到 99024 个字符,早已超过 UTF-16 的存储范围;这直接导致 UTF-16 地位颇为尴尬——如果谁还在想着只要使用 UTF-16 就可以高枕无忧的话,恐怕要失望了

  • UTF-16 存在大小端/字节序问题,这个问题在进行信息交换时特别突出——如果字节序未协商好,将导致乱码

如果协商好,但是双方一个采用大端一个采用小端,则必然有一方要进行大小端转换性能损失不可避免(大小端问题其实不像看起来那么简单,有时会涉及硬件、操作系统、上层软件多个层次,可能会进行多次转换)
另外,容错性低有时候也是一大问题——局部的字节错误,特别是丢失或增加可能导致所有后续字符全部错乱,错乱后要想恢复,可能很简单,也可能会非常困难。
这一点在日常生活里大家感觉似乎无关紧要,但是在很多特殊环境下却是巨大的缺陷.
目前支撑我们继续使用 UTF-16 的理由主要是考虑到它是双字节的,在计算字符串长度、执行索引操作时速度很快。
当然这些优点 UTF-32 都具有,但很多人毕竟还是觉得 UTF-32 太占空间了。

  • 反过来 UTF-8 也不完美,也存在一些问题:

文化上的不平衡——对于欧美地区一些以英语为母语的国家 UTF-8 简直是太棒了,因为它和 ASCII 一样,一个字符只占一个字节,没有任何额外的存储负担
但是对于中日韩等国家来说,UTF-8 实在是太冗余,一个字符竟然要占用 3多个字节,存储和传输的效率不但没有提升,反而下降了。
所以,欧美人民常常毫不犹豫的采用 UTF-8,而我们却老是要犹豫一会儿

  • 变长字节表示带来的效率问题——大家对 UTF-8 疑虑重重的一个问题就是在于其因为是变长字节表示。

因此,无论是计算字符数,还是执行索引操作效率都不高。
为了解决这个问题,常常会考虑把 UTF-8 先转换为 UTF-16 或者 UTF-32 后再操作,操作完毕后再转换回去。而这显然是一种性能负担

  • 当然,UTF-8 的优点也不能忘了:

字符空间足够大,未来 Unicode 新标准收录更多字符,UTF-8 也能妥妥的兼容,因此不会再出现 UTF-16 那样的尴尬
不存在大小端字节序问题,信息交换时非常便捷
容错性高,局部的字节错误(丢失、增加、改变)不会导致连锁性的错误,因为 UTF-8 的字符边界很容易检测出来,这是一个巨大的优点(正是为了实现这一点,咱们中日韩人民不得不忍受 3 字节 1 个字符的苦日子)

  • 那么到底该如何选择呢?

因为无论是 UTF-8 和 UTF-16/32 都各有优缺点,因此选择的时候应当立足于实际的应用场景。
例如在某网友的习惯中,存储在磁盘上或进行网络交换时都会采用 UTF-8,而在程序内部进行处理时则转换为 UTF-16/32。
对于大多数简单的程序来说,这样做既可以保证信息交换时容易实现相互兼容,同时在内部处理时会比较简单,性能也还算不错。
基本上只要你的程序不是 I/O 密集型的都可以这么干,当然这只是我粗浅的认识范围内的经验,很可能会被无情的反驳。
稍微再展开那么一点点……在一些特殊的领域,字符编码的选择会成为一个很关键的问题。特别是一些高性能网络处理程序里更是如此。
这时采用一些特殊的设计技巧,可以缓解性能和字符集选择之间的矛盾。
例如对于内容检测/过滤系统,需要面对任何可能的字符编码,这时如果还采用把各种不同的编码都转换为同一种编码后再处理的方案,那么性能下降将会很显著。
而如果采用多字符编码支持的有限状态机方案,则既能够无需转换编码,同时又能够以极高的性能进行处理。
当然如何从规则列表生成有限状态机,如何使得有限状态机支持多编码,以及这将带来哪些限制,已经又成了另外的问题了。

Y 推荐文献

BOM 将 a 编码 java.lang.StringUTF-16 little endian 的方法

public static byte[] encodeString(String message) {      byte[] tmp = null;     try {         tmp = message.getBytes("UTF-16LE");     } catch(UnsupportedEncodingException e) {         // should not possible         AssertionError ae =         new AssertionError("Could not encode UTF-16LE");         ae.initCause(e);         throw ae;     }      // use brute force method to add BOM     byte[] utf16lemessage = new byte[2 + tmp.length];     utf16lemessage[0] = (byte)0xFF;     utf16lemessage[1] = (byte)0xFE;     System.arraycopy(tmp, 0,                      utf16lemessage, 2,                      tmp.length);     return utf16lemessage; } 

这是一个老问题,但我仍然找不到适合我情况的可接受答案。基本上,Java 没有内置的带有 BOM 的 UTF-16LE 编码器。因此,您必须推出自己的实现。 2017-08-24T22:17:10.220

private byte[] encodeUTF16LEWithBOM(final String s) {     ByteBuffer content = Charset.forName("UTF-16LE").encode(s);     byte[] bom = { (byte) 0xff, (byte) 0xfe };     return ByteBuffer.allocate(content.capacity() + bom.length).put(bom).put(content).array(); } 

EncodingConverter.java

编码格式 描述 是否带BOM 字节顺序标记 (BOM) 字符编码特点 常见应用场景
ANSI 通常指操作系统默认的本地字符编码,基于系统语言环境(如Windows的GBK、ISO-8859-1等)。 使用单字节编码,字符集依赖操作系统语言,无法直接表示所有Unicode字符。 主要用于非Unicode编码的Windows环境下。
UTF-16 LE UTF-16编码,采用小端字节序(低位字节在前)。 0xFF 0xFE 每个字符占用2字节,支持全球所有Unicode字符。 常见于Windows系统、Java环境。
UTF-16 BE UTF-16编码,采用大端字节序(高位字节在前)。 0xFE 0xFF 每个字符占用2字节,支持全球所有Unicode字符。 主要用于一些特定硬件和平台,较少见。
UTF-8 可变长度的Unicode编码格式,向后兼容ASCII。 1到4个字节表示一个字符,广泛应用,兼容ASCII,节省空间。 网络传输、HTML、JSON、Web开发。
带BOM的UTF-8 UTF-8编码,带有字节顺序标记(BOM)。 0xEF 0xBB 0xBF 与普通UTF-8相同,但在文件开头加上字节顺序标记(BOM)。 有时用于明确标识编码格式,尤其在跨平台传输中。

X 参考文献

  • UTF-8UTF-16UTF-32 还区分带 BOM 的以及不带 BOM 的 Unicode 文本。
发表评论

评论已关闭。

相关文章