Java 字符编码

byte[]与String相互转换

这里强调byte数组是由无序的二进制序列组成的,比如bitmap转换的。
如果byte数组本身就是可以正常显示的String,通过toByteArray转换而来,则大概率不会出现此问题。

需求

通过文本传输协议传输一张图片
发送方:bitmap->byte[]->String,接收方还原

其中需要byte数组和String相互转换,即将无序的二进制组成的byte数组和String之间相互转换。
这里的转换需要考虑两点:正确、高效。

正确性:转换的String能正确还原byte数组
高效性:转换的String尽可能短

UTF_8

首先使用最常用的UTF_8,转换为String之后,无法正确还原byte数组

现象及其分析

test代码如下:

//原始byteArray,二进制排列无序,通过bitmap转换而来
val str = byteArray.toString(Charsets.UTF_8)
val byteArrayRevert = str.toByteArray(Charsets.UTF_8)

在这里插入图片描述
接下来需要分析为何转换为string之后再转回来就和原来不一样了。
这里需要了解下UTF_8的字符编码规则。

在这里插入图片描述
综合以上log中频繁出现的锟斤拷乱码以及UTF_8的编码格式。

初步怀疑:
无序的二进制byte数组中乱码的部分是不符合UTF_8的编码规则的,这个时候按照UTF_8编码规则,会将其转换成一个固定的错误码。
而包含这个错误码的string,在转换回byte数组的时候,就还原失败了。

代码验证

  1. 取一个汉字String(此处选择"严"),通过str.toByteArray(Charsets.UTF_8)转换为byte数组

已知"严"的unicode是4E25(1001110 00100101), 根据上表, 可以发现4E25处在第三行的范围内(0000 0800 - 0000 FFFF),
因此"严"的UTF-8编码需要三个字节, 即格式是"1110xxxx 10xxxxxx 10xxxxxx".
然后, 从"严"的最后一个二进制位开始, 依次从后向前填入格式中的x, 多出的位补0.
这样就得到了, "严"的UTF-8编码是 “11100100 10111000 10100101”, 转换成十六进制就是E4B8A5.

  1. 将此汉字的UTF-8编码破坏,使其不符合编码规则

11100100 10111000 10100101 e4b8a5 “严”
11100100 10111000 00100101 e4b825 破坏最后一字节的高位
11100100 00111000 10100101 e438a5 破坏中间一字节的高位

在这里插入图片描述
可以看到不符合编码规则的无法正常显示,byte[]对应为[-17,-65,-67],对应十六进制为 EF BF BD。
我们搜索这个十六进制字符,可以看到很多文章指出这个是无效码点值,如你应该记住的一个UTF-8字符「EF BF BD」
至此分析完毕。

另外,大名鼎鼎的锟斤拷就是使用GBKEF BF BD进行解码的结果。
彻底弄懂Java中的Unicode和UTF-8编码

ISO-8859-1

有文章说因为UTF-8是多字节的可变长编码,转而使用单字节编码ISO-8859-1,经过尝试并不能解决这个问题。
原因和UTF-8一样,均为码表中存在无效码点。
这里因为原始byte数组是无序二进制,那只要在byte的表示范围内,编码规则里存在无效码点,就可能出现无法还原的情况。

在这里插入图片描述
可以看出使用ISO-8859-1时,原先byte数组里的部分负值,都被转换成了十进制的63,即0x3f。
-119对应0x89,-106对应0x96。均为无效码点。
ISO-8859-1字符集
在这里插入图片描述

Base64

Base64-wiki

Base64(基底64)是一种基于64个可打印字符来表示二进制数据的表示方法。由于log(2)64=6,所以每6个位元为一个单元,对应某个可打印字符。3个字节相當於24个位元,对应于4个Base64单元,即3个字节可由4个可打印字符来表示。

是否能正确还原:
简单来说,Base64用6位bit对应64个常用可见字符。3个byte的数据就可以用4个Base64字符表示。
这样的对应关系是没有无效码点的,所以base64没有上述两个编码规范不能还原的问题。

效率:
使用Base64,用4个字符表示3个byte的数据,效率上有1/3的冗余。

测试代码如下:
可以看出8414个byte的数据编码后变成长度为11368的String。

//原始ByteArray -- Base64 String
val iconStringBase64 = Base64.encodeToString(iconByteArray, Base64.DEFAULT)
Log.d(testTag, "test, iconStringBase64[${iconStringBase64.length}]: " + iconStringBase64)
//Base64 String -- 传输的ByteArray
val iconByteArrayB64 = iconStringBase64.toByteArray(Charsets.UTF_8)
Log.d(testTag, "test, iconByteArrayB64[${iconByteArrayB64.size}]: " + Arrays.toString(iconByteArrayB64))

//传输的ByteArray  -- Base64 String
val iconStringBase64Re = iconByteArrayB64.toString(Charsets.UTF_8)
Log.d(testTag, "test, iconStringBase64Re: " + iconStringBase64Re.length)
//Base64 String -- 原始的ByteArray
val iconByteArrayBase6Re = Base64.decode(iconStringBase64Re, Base64.DEFAULT)
Log.d(testTag, "test, iconByteArrayRe: " + iconByteArrayBase6Re.size)

在这里插入图片描述

json

kotlin中使用gson可以直接将ByteArray转换为String,转换的规则为将ByteArray以十进制数组的方式打印出来。
使用json的方式也能正确还原,因为对应关系是确定的。
只是效率上存在问题,一个byte的数据转换为json之后变成2-5个字符(1, 或者 -119,)转换后的String长度为byte数组发2-5倍。

测试代码:
byte数组长度8414,转换后String长度为30567。

//ByteArray -- json
val iconByteArrayJson = Gson().toJson(iconByteArray)
Log.d(testTag, "test, iconByteArrayJson[${iconByteArrayJson.length}]: " + iconByteArrayJson)

//ByteArray -- json -- ByteArray
val iconByteArrayJsonRe = iconByteArrayJson.toByteArray(Charsets.UTF_8)
Log.d(testTag, "test, iconByteArrayJsonRe: " + iconByteArrayJsonRe.size)

val gson = Gson()
val type = object : TypeToken<ByteArray>() {}.type
val byteArrayJsonRe = gson.fromJson<ByteArray>(iconByteArrayJson,type)
Log.d(testTag, "test, byteArrayJsonRe[${byteArrayJsonRe.size}]: " + Arrays.toString(byteArrayJsonRe))

在这里插入图片描述

总结

  • UTF_8 和 ISO-8859-1在处理有规则的字符转换而来的byte[]时,能正常和string之间正确来回转换
  • UTF_8 和 ISO-8859-1在处理无规则的byte[] (比如bitmap转换而来的),无法和和string之间正确来回转换
  • Bsae64 和 json都能在byte[]和string之间正确转换,Base64效率较高,byte[]和String长度比在3:4

一个字符的String.length()是多少

  • Java 以 UTF-16 作为内存的字符char存储格式
  • 超过2字节的字符char无法表示,但是可以用多个char表示
  • 一个char等于一个code unit,而不等于一个字符
  • String.length()返回的是code unit的个数,而不是字符的个数,也不是占用字节的个数
    Java中关于Char存储中文到底是2个字节还是3个还是4个?

UTF-16 表示字符非常方便,每两个字节表示一个字符,这个在字符串操作时就大大简化了操作, 这也是Java 以 UTF-16 作为内存的字符存储格式的一个很重要的原因。 这也是为什么 java字符占用两个字节的原因。

PS:所以大于两个字节的Unicode字符在java中没法用char存储。如果用String 存储的话获取string.length()应该是2。获取真实字符串长度string.codePoints().count();如:👹
From 一篇读懂Unicode,UCS-2,UTF-8,UTF-16

一个字符的String.length()是多少

字符编码

字符串和编码
java中的字符编码方式

映射字符到数字的表格,被称作字符集(Character Set),微软称其为代码页(Code Page),又叫内码。字符集里的每一个字符都有一个编号,即映射到的数字,被称作码点(Code Point)。

字符编码那些事儿

  • Unicode 是全球文字统一编码。
    它把世界上的各种文字的每一个字符指定唯一编码,实现跨语种、跨平台的应用。
    Unicode 只是一个符号集,它只规定了每个符号的二进制数,却没有规定这个二进制数应该如何存储。比如,汉字‘严’的 Unicode 是十六进制数4E25,转换成二进制数足足有15位(100111000100101),也就是说,这个符号的表示至少需要2个字节。表示其他更大的符号,可能需要3个字节或者4个字节,甚至更多。

  • UTF
    Unicode的实现方式不同于编码方式。一个字符的Unicode编码确定。但是在实际传输过程中,由于不同系统平台的设计不一定一致,以及出于节省空间的目的,对Unicode编码的实现方式有所不同。简而言之,为了存储优化。
    Unicode的实现方式称为Unicode转换格式(Unicode Transformation Format,简称为UTF)。

    • 可变长编码 UTF-8
      UTF-8编码把一个Unicode字符根据不同的数字大小编码成1-6个字节,常用的英文字母被编码成1个字节,汉字通常是3个字节,只有很生僻的字符才会被编码成4-6个字节。
  • 在计算机内存中,统一使用Unicode编码,当需要保存到硬盘或者需要传输的时候,就转换为诸如UTF-8等编码。

    • 内码:char或String在内存里使用的编码方式。
    • 外码:除了内码都可以认为是“外码”。(包括class文件的编码)

参考

字符编码笔记:ASCII,Unicode 和 UTF-8 阮一峰
关于JAVA字符编码:Unicode,ISO-8859-1,GBK,UTF-8编码及相互转换
原码,反码,补码相互转换在线计算器
查看字符编码
彻底弄懂Java中的Unicode和UTF-8编码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值