2017织梦网站怎么做seo,网站建设平台开发,品牌代理网,巴彦淖尔seo1.Java String与byte[]互相转换存在的问题
java中#xff0c;按照byte[] 》string 》byte[]的流程转换后#xff0c;byte数据与最初的byte不一致。
多说无益#xff0c;上代码#xff0c;本地macos机器执行#xff0c;统一使用的UTF-8编码。
import java.nio.charset.S…1.Java String与byte[]互相转换存在的问题
java中按照byte[] 》string 》byte[]的流程转换后byte数据与最初的byte不一致。
多说无益上代码本地macos机器执行统一使用的UTF-8编码。
import java.nio.charset.StandardCharsets;
import java.util.Arrays;public class StringConversionExample {public static void main(String[] args) {
byte[] original2 new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
byte[] transformed2 new String(original2).getBytes();
System.out.println(Arrays.toString(original2));
System.out.println(Arrays.toString(transformed2));
System.out.println(Arrays.equals(original2, transformed2));
}
}执行结果
[-17, -113, -1]
[-17, -65, -67, -17, -65, -67]
false
发现问题了没有byte转成string后再转回byte竟然最初的数据不一致代码开发中肯定是与预期不一致容易写出bug。
2.首先熟悉相关字符、字符集、字符编码与字符概念
了解了大概的问题我们带着疑问先了解一下相关的概念。
计算机里的unicode编码和UTF-8的关系-CSDN博客
字符编码和字符集到底有什么区别Unicode和UTF-8是什么关系-CSDN博客
主要理解一下下面几个概念的区别
字符就是我们看到的一个字母或一个汉字、一个标点符号都叫字符。如汉字“一”就是一个字符。字符码在指定的字符集中一个字符对应唯一一个数字这个数字就叫字符码。如上边的字符“一”在 Unicode 字符集中对应的字符码为 \u4e00。字符集规定了字符和字符码之间的对应关系。字符编码规定了一个字符码在计算机中如何存储比如UTF-8。
3.java中byte[]与string互转出现问题的表面原因是什么我们控制变量做了一个对比实验
首先看下如下代码:
byte[] original1 new byte[]{(byte)0xef, (byte)0x8f, (byte)0x8f};
byte[] transformed1 new String(original1).getBytes();
System.out.println(Arrays.toString(original1));
System.out.println(Arrays.toString(transformed1));
System.out.println(Arrays.equals(original1, transformed1));
它的执行结果是
[-17, -113, -113]
[-17, -113, -113]
true
这两个字节数组内容是完全相等的第一个byte array在经过到String的转换再到bytes的转换后内容保持不变。
再看如下代码
byte[] original2 new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
byte[] transformed2 new String(original2).getBytes();
System.out.println(Arrays.toString(original2));
System.out.println(Arrays.toString(transformed2));
System.out.println(Arrays.equals(original2, transformed2));
它的执行结果是
[-17, -113, -1]
[-17, -65, -67, -17, -65, -67]
false
这一次两个byte array的结果不一样了且结果差异很大。
这两段代码的唯一区别是original1的最后一个字节值是0x8f 而original2的最后一个字节值为0xff。
所以0xff是导致byte[]与string互转出现问题的表面原因。
4.0xff为什么会导致出现问题
1了解java byte与string的转换原理
上面的示例代码的执行环境中系统默认的字符集是UTF-8所以字节到字符的转换会按 UTF-8编码来进行转换。
2UTF-8可变长度字符编码
UTF-8Unicode Transformation Format-8是一种变长编码方案多位元组序列用于将Unicode字符集中的字符编码为字节序列以便在计算机系统中存储和传输。
UTF-8的定义和使用如下
1. 字符编码范围UTF-8可以表示Unicode字符集中的所有字符包括各种语言的字符、符号、标点符号等。
2. 变长编码UTF-8使用变长编码方式根据字符的不同范围使用不同长度的字节表示字符。具体规则如下 - 对于ASCII字符Unicode码范围为U0000至U007F使用一个字节表示其最高位为0。 - 对于非ASCII字符使用多个字节表示。UTF-8的编码规则如下 - 对于2字节字符使用两个字节表示其最高位为110。 - 对于3字节字符使用三个字节表示其最高位为1110。 - 对于4字节字符使用四个字节表示其最高位为11110。 - 对于更高范围的字符UTF-8可以使用最多6个字节进行表示。
3. 兼容性UTF-8编码方案保持了与ASCII字符集的兼容性。ASCII字符在UTF-8中仍然使用一个字节表示因此任何以ASCII编码的文本都是有效的UTF-8文本。
4. 字节顺序UTF-8不涉及字节顺序问题因为它是一种字节序列的编码方式而不是多字节字符的编码方式。因此字节顺序对UTF-8没有影响。
在使用UTF-8时常见的操作包括将字符串从编码为UTF-8字节序列以及将UTF-8字节序列解码为字符串。编程语言通常提供了相应的API和函数来进行这些操作例如Java中的getBytes()方法和new String()构造函数以及Go语言中的[]byte类型和string()类型转换。
总结UTF-8是一种用于将Unicode字符编码为字节序列的编码方案它具有变长编码、兼容性和广泛支持的特点。使用UTF-8可以在计算机系统中存储和传输各种语言的文本数据并且保持与ASCII字符集的兼容性。
3UTF-8可变长度字符编码的官方说法
这里引用一下更为官方的解释https://zh.wikipedia.org/wiki/UTF-8#UTF-8%E7%9A%84%E7%B7%A8%E7%A2%BC%E6%96%B9%E5%BC%8F 设计UTF-8的理由 UTF-8的设计有以下的多字符组序列的特质 单字节字符的最高有效比特永远为0。多字节序列中的首个字符组的几个最高有效比特决定了序列的长度。最高有效位为110的是2字节序列而1110的是三字节序列如此类推。多字节序列中其余的字节中的首两个最高有效比特为10。 UTF-8的这些特质保证了一个字符的字节序列不会包含在另一个字符的字节序列中。这确保了以字节为基础的部分字符串比对sub-string match方法可以适用于在文字中搜索字或词。有些比较旧的可变长度8位编码如Shift JIS没有这个特质故字符串比对的算法变得相当复杂。虽然这增加了UTF-8编码的字符串的信息冗余但是利多于弊。另外资料压缩并非Unicode的目的所以不可混为一谈。即使在发送过程中有部分字节因错误或干扰而完全丢失还是有可能在下一个字符的起点重新同步令受损范围受到限制。 另一方面由于其字节序列设计如果一个疑似为字符串的序列被验证为UTF-8编码那么我们可以有把握地说它是UTF-8字符串。一段两字节随机序列碰巧为合法的UTF-8而非ASCII的几率为32分1。对于三字节序列的几率为256分1对更长的序列的几率就更低了。 UTF-8的编码方式 UTF-8是UNICODE的一种变长度的编码表达方式《一般UNICODE为双字节指UCS2》它由肯·汤普逊Ken Thompson于1992年建立现在已经标准化为RFC 3629。UTF-8就是以8位为单元对UCS进行编码而UTF-8不使用大尾序和小尾序的形式每个使用UTF-8存储的字符除了第一个字节外其余字节的头两个比特都是以10开始使文字处理器能够较快地找出每个字符的开始位置。 但为了与以前的ASCII码兼容ASCII为一个字节因此UTF-8选择了使用可变长度字节来存储Unicode 注意不论是Unicode (Table 3.7) [12]还是ISO 10646 (10.2 UTF-8) [13]目前都只规定了最高码位是0x10FFFF的字符的编码。下表中表示大于0x10FFFF的UTF-8编码是不符合标准的。 Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 ) 码点的位数码点起值码点终值字节序列Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6 7U0000U007F10xxxxxxx11U0080U07FF2110xxxxx10xxxxxx16U0800UFFFF31110xxxx10xxxxxx10xxxxxx21U10000U1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx26U200000U3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx31U4000000U7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx 在ASCII码的范围用一个字节表示超出ASCII码的范围就用字节表示这就形成了我们上面看到的UTF-8的表示方法这样的好处是当UNICODE文件中只有ASCII码时存储的文件都为一个字节所以就是普通的ASCII文件无异读取的时候也是如此所以能与以前的ASCII文件兼容。大于ASCII码的就会由上面的第一字节的前几位表示该unicode字符的长度比如110xxxxx前三位的二进制表示告诉我们这是个2BYTE的UNICODE字符1110xxxx是个三位的UNICODE字符依此类推xxx的位置由字符编码数的二进制表示的位填入。越靠右的x具有越少的特殊意义。只用最短的那个足够表达一个字符编码数的多字节串。注意在多字节串中第一个字节的开头1的数目就是整个串中字节的数目。 从这里可以得出需要看byte是否符合UTF-8标准编码需要先把16进制byte转为二进制然后参考是否与UTF-8设计规范一致参阅Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 )。
4示例中的(byte)0xef, (byte)0x8f, (byte)0xff为什么不是UTF-8标准编码
4.1先补充一个知识点十六进制转二进制
计算方式一
将十六进制数 0xef 转换为其对应的二进制表示形式可以按照以下步骤进行
1. 将十六进制数 0xef 拆分为两个十六进制位0xe 和 0xf。 2. 将每个十六进制位转换为对应的四位二进制数。 - 0xe 转换为二进制为 1110。 - 0xf 转换为二进制为 1111。 3. 将两个四位二进制数连接起来得到八位二进制数。 - 11101111
因此十六进制数 0xef 转换为二进制为 11101111。
计算方式二
如果你觉得方式一太复杂可以先拆分为e、f然后使用网站快速转换在线进制转换 计算方式三
查表先转为10进制然后手动计算二进制。
4.1.1如下图0xEF的十进制为239 将十进制数 239 转换为二进制数可以使用逐次除以 2 的方法将每次的余数记录下来直到商为 0。这个过程如下所示
4.1.2转换步骤 将十进制数 239 除以 2 239 ÷ 2 119余数 1记录余数1 将商 119 除以 2 119 ÷ 2 59余数 1记录余数1 将商 59 除以 2 59 ÷ 2 29余数 1记录余数1 将商 29 除以 2 29 ÷ 2 14余数 1记录余数1 将商 14 除以 2 14 ÷ 2 7余数 0记录余数0 将商 7 除以 2 7 ÷ 2 3余数 1记录余数1 将商 3 除以 2 3 ÷ 2 1余数 1记录余数1 将商 1 除以 2 1 ÷ 2 0余数 1记录余数1
4.1.3汇总余数
将上述步骤中的余数从下到上即从最后一步到第一步排列起来就得到了二进制数
从下到上余数依次是11101111
4.1.4结论
十进制数 239 转换为二进制数是 11101111。
4.1.5验证 4.2根据上面的计算方式计算出本次的16进制对应的二进制
下面是 (byte)0xef, (byte)0x8f, (byte)0xff 这三个字节的 8 位二进制表示形式
1. (byte)0xef 对应的 8 位二进制数为11101111 2. (byte)0x8f 对应的 8 位二进制数为10001111 3. (byte)0xff 对应的 8 位二进制数为11111111
4.3查看二进制是否符合UTF-8的编码规范
这些二进制数表示了各自字节的位模式然后查看Unicode 和 UTF-8 之间的转换关系表。
0xef对应11101111所以属于转换关系表的字节序列3也就是说的UTF-8的三字节字符。剩下的两位字符需要对应Byte 2和Byte 3应该以10xxxxxx格式开头。
0x8f为10001111符合。
0xff为11111111不符合。
发现这些字节序列并不符合 UTF-8 编码规范所以它们并不是有效的 UTF-8 编码。
可以回答第4章最初的问题因为(byte)0xef, (byte)0x8f, (byte)0xff不符合三字节字符对应的转换关系表规范所以不是UTF-8标准编码。 Unicode 和 UTF-8 之间的转换关系表 ( x 字符表示码点占据的位 ) 码点的位数码点起值码点终值字节序列Byte 1Byte 2Byte 3Byte 4Byte 5Byte 6 7U0000U007F10xxxxxxx11U0080U07FF2110xxxxx10xxxxxx16U0800UFFFF31110xxxx10xxxxxx10xxxxxx21U10000U1FFFFF411110xxx10xxxxxx10xxxxxx10xxxxxx26U200000U3FFFFFF5111110xx10xxxxxx10xxxxxx10xxxxxx10xxxxxx31U4000000U7FFFFFFF61111110x10xxxxxx10xxxxxx10xxxxxx10xxxxxx10xxxxxx
5不是UTF-8标准编码跟java转换有什么关系
经过上面一串的解释你应该会得出一个结论因为(byte)0xef, (byte)0x8f, (byte)0xff中的(byte)0xff不是标准的UTF-8字符编码所以转换过程有问题。它活该呀谁让它不标准呢。
但是还有一个疑问没解决那就是为什么它不标准java就转换不正确
让我们回到最初的java代码 public static void main(String[] args) {
byte[] original2 new byte[]{(byte)0xef, (byte)0x8f, (byte)0xff};
byte[] transformed2 new String(original2).getBytes();
System.out.println(Arrays.toString(original2));
System.out.println(Arrays.toString(transformed2));
System.out.println(Arrays.equals(original2, transformed2));
}
其中转换逻辑使用到的关键java代码
new String(original2).getBytes();
分析String源码
简单的从 String 的构造方法可以看出没有设置编码的情况下会采用系统默认编码macOS上的为 UTF-8解码过程中使用到一个关键的CharsetDecoder来看一下CharsetDecoder 的构造方法
代码位置JDK 1.8java.lang.String#String(byte[], int, int, java.nio.charset.Charset)636行、611行
JDK 1.8java.nio.charset.CharsetDecoder#CharsetDecoder(java.nio.charset.Charset, float, float)234行
// java.lang.String#String(byte[], int, int, java.nio.charset.Charset)653行// decode using CharsetDecoderint en scale(length, cd.maxCharsPerByte());cd.onMalformedInput(CodingErrorAction.REPLACE).onUnmappableCharacter(CodingErrorAction.REPLACE);// CodingErrorAction.REPLACE)参数/*** Action indicating that a coding error is to be handled by dropping the* erroneous input, appending the coders replacement value to the output* buffer, and resuming the coding operation.*/public static final CodingErrorAction REPLACE new CodingErrorAction(REPLACE);// java.nio.charset.CharsetDecoder#CharsetDecoder(java.nio.charset.Charset, float, float)234行/*** Initializes a new decoder. The new decoder will have the given* chars-per-byte values and its replacement will be the* string code#92;uFFFD/code.** param cs* The charset that created this decoder** param averageCharsPerByte* A positive float value indicating the expected number of* characters that will be produced for each input byte** param maxCharsPerByte* A positive float value indicating the maximum number of* characters that will be produced for each input byte** throws IllegalArgumentException* If the preconditions on the parameters do not hold*/protected CharsetDecoder(Charset cs,float averageCharsPerByte,float maxCharsPerByte){this(cs,averageCharsPerByte, maxCharsPerByte,\uFFFD);}
其中比较关键的是 CodingErrorAction.REPLACEjavadoc中的解释 /*** Action indicating that a coding error is to be handled by dropping the* erroneous input, appending the coders replacement value to the output* buffer, and resuming the coding operation.*/public static final CodingErrorAction REPLACE new CodingErrorAction(REPLACE);
进行string转byte时针对错误部分的byte(byte)0xff会被java使用\uFFFD进行替换。所以再将byte转回string时自然无法准备恢复最初的数据了。
6使用java代码验证是否使用\uFFFD替换了错误部分的byte
package com.shopee.mmdb.chunnel.store.kafka;import java.nio.charset.Charset;
import java.util.Arrays;public class StringConversionExample {public static void main(String[] args) {byte[] original2 new byte[] {(byte) 0xef, (byte) 0x8f, (byte) 0xff};byte[] transformed2 new String(original2).getBytes();System.out.println(Arrays.toString(original2));System.out.println(Arrays.toString(transformed2));System.out.println(Arrays.equals(original2, transformed2));String unicodeString \uFFFD;Charset charset Charset.forName(UTF-8);byte[] byteArr unicodeString.getBytes(charset);System.out.println(Arrays.toString(byteArr));}
}
运行结果显而易见\uFFFD就是 [-17, -65, -67]。
[-17, -113, -1]
[-17, -65, -67, -17, -65, -67]
false
[-17, -65, -67]
7解决问题
既然是UTF-8编码不标准的问题那么可以通过单字节编码模式获取到正确的结果操作如下:
byte[] newBytes new String(bytes, StandardCharsets.ISO_8859_1).getBytes(StandardCharsets.ISO_8859_1)// 完整代码
package com.shopee.mmdb.chunnel.store.kafka;import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;public class StringConversionExample {public static void main(String[] args) {byte[] original2 new byte[] {(byte) 0xef, (byte) 0x8f, (byte) 0xff};byte[] transformed2 new String(original2, StandardCharsets.ISO_8859_1).getBytes(StandardCharsets.ISO_8859_1);
// byte[] transformed2 new String(original2).getBytes();System.out.println(Arrays.toString(original2));System.out.println(Arrays.toString(transformed2));System.out.println(Arrays.equals(original2, transformed2));String unicodeString \uFFFD;Charset charset Charset.forName(UTF-8);byte[] byteArr unicodeString.getBytes(charset);System.out.println(Arrays.toString(byteArr));}
}
执行结果
[-17, -113, -1]
[-17, -113, -1]
true
[-17, -65, -67]
5.java byte转string问题的总结
java中通过 byte[] 转换为 String 时可能因为一些编码规则比如UTF-8造成部分被替换反向转换为 byte[] 后和之前不同在转换时可以通过指定 StandardCharsets.ISO_8859_1 等单字节编码来解决问题
这里引入另一个思考的问题一个 String 转换为 byte[] 后再转换为 String 会有问题么答案是不会因为转换为 byte[] 的字节编码是符合 UTF-8 的
6.那么golang中是否也存在该问题呢
1看goalng的例子
同样的使用不标准的UTF-8编码 0xef, 0x8f, 0xffgolang执行 byte 转 string然后string再转回byte。
package mainimport (fmtreflectunicode/utf8
)func main() {original2 : []byte{0xef, 0x8f, 0xff}transformed2 : []byte(string(original2))fmt.Println(Original2:, original2)fmt.Println(Transformed2:, transformed2)fmt.Println(Are equal:, reflect.DeepEqual(original2, transformed2))fmt.Println(Original2 string:, string(original2))if utf8.Valid(original2) {fmt.Println(Original is valid UTF-8)} else {fmt.Println(Original is not valid UTF-8)}
}运行结果byte经过一通转换后又回到了最初的byte数据完全一致。
所以在golang里并没有复现出java里的byte转换后数据最终被篡改的问题。
Original2: [239 143 255]
Transformed2: [239 143 255]
Are equal: true
Original2 string:
Original is not valid UTF-82golang里为什么byte转string不会出现数据被篡改的情况
基本信息和源码
GO的字符串内存中string用UTF-8编码
go sdk 1.21.5的string源码如下
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string
同时根据golang官方博客https://blog.golang.org/strings的原文
Go source code is always UTF-8.
A string holds arbitrary bytes.
A string literal, absent byte-level escapes, always holds valid UTF-8 sequences.
大致意思如下
go中的代码总是用UTF-8编码并且字符串能够存储任何字节没有经过字节级别的转义那么字符串是一个标准的utf8序列
JAVA的字符串内存中string使用的UTF-16编码
JDK 1.8源码java.lang.String#String(byte[])
value StringUTF16.toBytes(ca, 0, caLen);
可能到这里你就会急了“你上面JAVA长篇大论一直给我讲UTF-8不标准字符导致了java的转换问题我刚接受这个概念。怎么到这里又变成了java使用的UTF-16golang反倒使用的UTF-8并且golang还没有出问题”。
很好我接着解释。
JAVA转换路径
1、输入非标准UTF-8字符 》
2、根据用户显式指定字符编码集或者Macos机器默认内置的字符编码UTF-8通知程序本次输入的byte数组是UTF-8编码规则 》
3、JAVA调用decodeUTF8_UTF16方法把byte[]转换为UTF-16 》
4、方法decodeWithDecoder使用\uFFFD替换掉不标准的字符并从byte[]转为char[] 》
5、最终使用方法StringUTF16.toBytes(ca, 0, caLen)把char[]再次转为UTF16并设置到string的private final byte[] value;里
// jdk 1.8 string类 573行方法路径java.lang.String#decodeUTF8_UTF16
dp decodeUTF8_UTF16(bytes, offset, sl, dst, dp, true);
GOLANG转换路径
1、根据传入的内存大小判断是否需要分配重新分配内存 2、构建stringStruct分类长度和内存空间; 3、赋值[]byte里面的数据到新构建stringStruct的内存空间中。
查看源码 src/runtime/string.go
type stringStruct struct {str unsafe.Pointerlen int
}func slicebytetostring(buf *tmpBuf, b []byte) (str string) {l : len(b)if l 0 {return }if l 1 {stringStructOf(str).str unsafe.Pointer(staticbytes[b[0]])stringStructOf(str).len 1return}var p unsafe.Pointer// 判断传入的缓冲区大小决定是否重新分配内存if buf ! nil len(b) len(buf) {p unsafe.Pointer(buf)} else {// 重新分配内存p mallocgc(uintptr(len(b)), nil, false)}// 将输出的str转化成stringStruct结构// 并且赋值stringStructOf(str).str pstringStructOf(str).len len(b)// 将[]byte中的内容复制到内存空间p中memmove(p, (*(*slice)(unsafe.Pointer(b))).array, uintptr(len(b)))return
}// 转换成
func stringStructOf(sp *string) *stringStruct {return (*stringStruct)(unsafe.Pointer(sp))
}
JAVA与GOLANG转换区别的对比 基本存储逻辑的区别: Java: String 是用 UTF-16 编码存储的字符序列设计上是用于表示文本数据的类。byte[] 是原始的字节数据通常与 String 之间的转换涉及字符编码。Go: string 总是 UTF-8 不代表一定编码的字节序列视图表示不可变的字节数据。它更接近于一个只读的 []byte在内部没有进行复杂的编码处理。 对无效编码的处理: Java: 在将 byte[] 转换为 String 时如果字节序列不符合字符集例如 UTF-8的要求Java 会用替代字符 \uFFFD 来表示解码失败的字节。这是为了保证转换结果在文本上是合理的。Go: 将 []byte 转换为 string 时不会检查字节序列的有效性。无论字节是否符合 UTF-8 规范转换后的 string 都会直接包含这些原始字节不会进行替换。 设计上的考虑: Java: 强调国际化和多语言支持String 是表示人类可读文本的类必须处理和转换字符集。Java 期望转换过程中得到有效的字符因此必须处理无效字节序列替换或者抛出异常。Go: 设计上强调简单和直接string 只是字节序列的不可变视图不做额外的编码检查和处理。这使得处理二进制数据和文本时更灵活但要求开发者自己确保字节序列的有效性。总结 Java: 强调对字符集和编码的处理String 是用来表示文本的类在转换时需要处理不合法的字节。Go: string 是不可变的字节序列视图直接允许 []byte 转换为 string无论字节是否构成有效的 UTF-8 字符。 想看更加详细对比请参考另外一篇文章Java 和 Go 中的 byte 和 String 转换-CSDN博客
最终结论
java底层存储string使用byte字节序列严格限制合法的UTF-16编码所以为了强调String的有效性做了严格限制比如不合法的byte会被替换掉。
golang底层存储string使用byte字节序列默认总表示UTF-8编码的字符也可以不是不做额外的字符合法性校验。
所以Java里使用new String(new byte[]{new byte[] {(byte) 0xef, (byte) 0x8f, (byte) 0xff}})会导致数据被篡改。golang里使用string([]byte{0xef, 0x8f, 0xff})不会导致数据被篡改所以golang不存在该问题。
7.JAVA和Golang String分析过程中的细节补充
1UTF-8和UTF-16什么区别
UTF-8: 以可变长度编码字符具有较好的 ASCII 兼容性和网络传输效率非常适合以英语为主的文本处理和互联网应用。UTF-16: 适合需要处理大量非 ASCII 字符的场景虽然占用存储空间较大但在处理字符时较为高效适合在内存中存储文本和内部字符处理。UTF16详细设计信息参考https://zh.wikipedia.org/wiki/UTF-16
更多对比参考文章编码规则UTF-8 和 UTF-16的区别-CSDN博客
2为什么Java默认使用UTF-16Golang默认使用UTF-8呢
Java 选择 UTF-16:
历史背景: Java 诞生时 Unicode 设计目标主要是 16 位编码UTF-16 是当时处理多语言的最佳选择。效率考虑: 固定长度的 2 字节编码简化了字符处理逻辑特别是在需要高效定位和操作字符时。兼容性需求: 与 Windows 系统的兼容性需求使得 UTF-16 在跨平台应用中具有优势。
Go 选择 UTF-8:
创始人背景Go 语言的3位创始人其中有2位同时也是UTF-8 字元编码的创始人。
互联网标准: Go 语言诞生在互联网时代UTF-8 已成为网络传输和文件存储的标准。简单高效: Go 的设计强调简单和高效UTF-8 作为默认编码简化了字符串处理特别是在处理 ASCII 兼容文本时。全球化支持: UTF-8 能够支持全球所有字符适应了现代互联网应用的需求。
总结
Java 和 Go 在处理字符编码方面的选择反映了它们各自的历史背景和设计哲学。Java 的 UTF-16 选择了稳定和兼容而 Go 的 UTF-8 则倾向于现代互联网应用的灵活性和效率。
更多原因参考另外一篇文章为什么Java默认使用UTF-16Golang默认使用UTF-8呢-CSDN博客
8.Golang中String与Byte、Rune的关系
1有必要重温一下最基本的字符编码概念上面已经讲过了
字符串字符串是由一个或多个字符组成的序列。它可以包含字母、数字、符号和其他特殊字符。
UnicodeUnicode 是一个字符集它为世界上几乎所有的字符都分配了一个唯一的标识符码点。每个字符都有一个对应的 Unicode 码点用 U 加上一个十六进制数字表示例如 U0041 表示字符 A。
UTF-8UTF-8 是一种变长编码方案用于在计算机中表示 Unicode 字符。它使用 8 位字节来编码字符根据字符的码点范围使用不同长度的字节序列来表示字符。UTF-8 可以准确地表示各种语言字符、符号和表情等它是最常用的字符编码方案之一。
Unicode 和 UTF-8 的关系是Unicode 定义了字符集和字符的标识符码点而 UTF-8 是一种编码方案用于在计算机中表示 Unicode 字符。UTF-8 编码将 Unicode 码点转换为字节序列并且可以通过解码将字节序列转换回字符。
字符Unicode 码点UTF-8 编码十六进制UTF-8 编码二进制爱U7231E788B1[11100111 10001000 10110001]AU004141[01000001]9U003939[00111001]
讲解可变长字符三字节的爱字的转换过程
查询Unicode表找到Unicode 码点: U7231 十六进制数十六进制数转换为二进制: 0111 0010 0011 0001根据 UTF-8 编码规则U7231 需要 3 字节。UTF-8 编码格式 1110xxxx 10xxxxxx 10xxxxxx将二进制数字按位插入格式按从右向左依次填充这个格式中的 x多出的 x 用 0 补上 1110xxxx 1110011110xxxxxx 1000100010xxxxxx 10110001二进制每4位一组根据下面的对照表转十六进制最终得到E7 88 B1
需要进一步详细重温原理可以看本文的第2个章节【2.首先熟悉相关字符、字符集、字符编码与字符概念】。
2String、Byte和Rune代码中的基本概念
详细讲解版本
StringGo 语言中的字符串是一个只读的字节切片。默认为UTF-8的字节序列所以string底层转字节时是默认按照UTF-8编码转换。字节转string时也默认按照UTF-8解码如果这组字节不是UTF-8个格式显示string的时候则会乱码。
[]rune可以把字符串转换为一个rune数组即unicode数组
一个rune 4个字节32 bit一个rune就表示一个Unicode字符每个Unicode字符在内存中是以utf-8的形式存储Unicode字符输出[]rune会把每个UTF-8转换为Unicode后再输出
[]byte可以把字符串转换为一个byte数组
一个byte 1个字节8 bit为了对比rune所以这句话看起来有点废话输出[]byte, 会按字符串在内存中实际存储形式(UTF-8)输出Unicode字符按[]byte输出就会把UTF-8的每个字节单个输出
ASCIIAmerican Standard Code for Information Interchange 是一种字符编码标准用于表示英文字符及一些控制字符。
7 位编码标准 ASCII 使用 7 位来编码字符可以表示 128 个字符从 0 到 127。8 位扩展扩展 ASCII 使用 8 位可以表示 256 个字符从 0 到 255包括标准 ASCII 和额外的字符。标准 ASCII 字符的Unicode范围是U0000 到 U007F 使用单个字节表示256个字符正好可以使用1个字节表示8bit也就是2的8次方256个字符全部可以存下。
非ASCII非 ASCII 字符指的是那些超出了标准 ASCII 范围的字符。非 ASCII 字符的编码值通常在 128 以上要更多的比特来编码以表示更广泛的字符集。
截至 Unicode 15.0最新版本已定义的字符总数为 149,186 个 。其中128 个是标准 ASCII 字符不包括拓展。因此非 ASCII 字符的数量是 149,186 - 128 149,058 个。非 ASCII 字符的Unicode范围是 U0080 到 U10FFFF最大的Unicode为U10FFFF二进制需要4个字节才能表示。需要使用可变的多字节表示从 2 到 4 个字节所以
查看Go源码byte和rune的定义分别是type byte uint8和type rune int32。
// string is the set of all strings of 8-bit bytes, conventionally but not
// necessarily representing UTF-8-encoded text. A string may be empty, but
// not nil. Values of string type are immutable.
type string string// byte is an alias for uint8 and is equivalent to uint8 in all ways. It is
// used, by convention, to distinguish byte values from 8-bit unsigned
// integer values.
type byte uint8// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.
type rune int32
byte uint8范围是0-2552的8次方1字节只能够表示ASCII Unicode。
Rune int324字节的rune完全兼容所有的unicode包括最大的Unicode U10FFFF。
区别总结
特性stringbyte[]rune数据类型不可变的字符串可变的字节切片可变的 rune字符切片单位UTF-8 字符串单个字节Unicode 码点字符主要用途存储和操作文本数据操作原始二进制数据或单字节字符处理和操作 Unicode 字符可变性不可变可变可变访问方式按字节或字符rune访问按字节访问按字符rune访问示例Hello, 世界[]byte{H, e, l, l, o}[]rune{H, 世}
简单总结版本 string 定义string 类型表示一个不可变的字节序列文本字符串。特点 每个字符可能是一个或多个字节。在 Go 中string 是 UTF-8 编码的字节序列这意味着它可以包含多字节字符。操作string 可以使用内置的字符串操作函数如 len、 等。 byte 定义byte 是一个 8 位无符号整数类型本质上等同于 uint8。特点 byte 类型通常用于表示原始的字节数据。处理单字节字符如 ASCII 字符或原始的二进制数据时非常有用。操作byte 数组[]byte可以进行字节级别的操作如读取、写入和修改。 rune 定义rune 是一个 32 位有符号整数类型本质上等同于 int32。特点 rune 表示一个 Unicode 码点可以表示世界上所有的字符。使用 rune 处理多字节字符和非 ASCII 字符时更方便。操作rune 切片[]rune可以方便地处理字符串中的 Unicode 字符。
存储同一个字符串时byte和rune的代码表现
包含非ascii码的字符串
package mainimport (fmtunicode/utf8unsafe
)func main() {c : go语言s_rune_c : []rune(c)s_byte_c : []byte(c)fmt.Println(s_rune_c) // [103 111 35821 35328] fmt.Println(s_byte_c) // [103 111 232 175 173 232 168 128]fmt.Println(utf8.RuneCountInString(c)) //4fmt.Println(len(c)) //8fmt.Println(len(s_rune_c)) //4
}
汉字占3个字节可变长字节存储unicode所以转换的[]byte长度为8由于已经转换为[]rune所以长度为4utf8.RuneCountInString()获取UTF-8编码字符串的长度所以跟[]rune一致
想了解更多它们的区别可以参考另外一篇文章golang string、byte[]以及rune的基本概念用法以及区别-CSDN博客
3String、Byte和Rune代码的用法示例和转换注意事项
示例与转换
1. string 与 []byte 的转换
string 转 []byte将字符串转换为字节数组可以用于对原始数据进行处理或传输。[]byte 转 string将字节数组转换回字符串用于将处理后的数据重新组装为字符串。
package mainimport (fmt
)func main() {// string 转换为 []bytestr : Hello, 世界byteArray : []byte(str)fmt.Printf(string to []byte: %v\n, byteArray)fmt.Printf(string to []byte: % X\n, byteArray) // 用 16 进制显示字节// []byte 转换为 stringnewStr : string(byteArray)fmt.Printf([]byte to string: %s\n, newStr)
}输出
string to []byte: [72 101 108 108 111 44 32 228 184 150 231 149 140]
string to []byte: 48 65 6C 6C 6F 2C 20 E4 B8 96 E7 95 8C
[]byte to string: Hello, 世界注意事项
在 string 转 []byte 时字符可能会被分割成多个字节。在 []byte 转 string 时确保字节数组是合法的 UTF-8 编码否则会出现乱码或非法字符。
2. string 与 []rune 的转换
string 转 []rune将字符串转换为 rune 数组方便逐字符处理特别是对于非 ASCII 字符。[]rune 转 string将 rune 数组转换回字符串用于重新组合字符。
package mainimport (fmt
)func main() {// string 转换为 []runestr : Hello, 世界runeArray : []rune(str)fmt.Printf(string to []rune: %v\n, runeArray)fmt.Printf(string to []rune: %c\n, runeArray) // 用字符显示每个 rune// []rune 转换为 stringnewStr : string(runeArray)fmt.Printf([]rune to string: %s\n, newStr)
}输出
string to []rune: [72 101 108 108 111 44 32 19990 30028]
string to []rune: [H e l l o , 世 界]
[]rune to string: Hello, 世界注意事项
rune 表示的是 Unicode 码点一个 rune 可以表示一个完整的字符。string 转 []rune 时每个字符都转换为对应的 Unicode 码点即使是多字节字符。[]rune 转 string 时字符会被正确地重新组装为字符串。
3. []byte 与 []rune 的转换
[]byte 转 []rune首先需要将 []byte 转换为 string然后再将 string 转换为 []rune。[]rune 转 []byte将 []rune 转换为 string然后再将 string 转换为 []byte。
package mainimport (fmt
)func main() {// []byte 转换为 []runebyteArray : []byte(Hello, 世界)str : string(byteArray) // 中间转换为 stringruneArray : []rune(str) // 再转换为 []runefmt.Printf([]byte to []rune: %v\n, runeArray)fmt.Printf([]byte to []rune: %c\n, runeArray)// []rune 转换为 []bytenewStr : string(runeArray) // 中间转换为 stringnewByteArray : []byte(newStr) // 再转换为 []bytefmt.Printf([]rune to []byte: %v\n, newByteArray)fmt.Printf([]rune to []byte: % X\n, newByteArray) // 用 16 进制显示字节
}输出
[]byte to []rune: [72 101 108 108 111 44 32 19990 30028]
[]byte to []rune: [H e l l o , 世 界]
[]rune to []byte: [72 101 108 108 111 44 32 228 184 150 231 149 140]
[]rune to []byte: 48 65 6C 6C 6F 2C 20 E4 B8 96 E7 95 8C注意事项
由于 byte 只表示单个字节因此在转换到 rune 之前需要经过 string 以处理多字节字符。直接将 []rune 转换为 []byte 是不可行的因为 rune 是 32 位而 byte 是 8 位需要中间通过 string 处理字符的编码。
总结与注意事项
string 是 UTF-8 编码的字节序列适合用于表示和处理常规的文本数据。byte 类型用于处理原始字节数据适合低级别的数据操作和处理单字节字符。rune 类型用于处理 Unicode 码点适合表示和操作多字节的字符特别是非 ASCII 字符。转换时需要注意字符编码确保在从一种表示形式转换到另一种时不会丢失数据或出现乱码尤其是在处理非 ASCII 字符时。
9.不符合UTF-8的Byte转换示例
Java
代码
package com.xxx;import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;public class StringConversionExample {public static void main(String[] args) {byte[] original2 new byte[] {(byte) 0xef, (byte) 0x8f, (byte) 0xff};// 解决办法使用单字节
// byte[] transformed2 new String(original2, StandardCharsets.ISO_8859_1).getBytes(StandardCharsets.ISO_8859_1);byte[] transformed2 new String(original2).getBytes();System.out.println(Arrays.toString(original2));System.out.println(Arrays.toString(transformed2));System.out.println(Arrays.equals(original2, transformed2));String unicodeString \uFFFD;Charset charset Charset.forName(UTF-8);byte[] byteArr unicodeString.getBytes(charset);System.out.println(Arrays.toString(byteArr));}
}
输出
[-17, -113, -1]
[-17, -65, -67, -17, -65, -67]
false
[-17, -65, -67]
Golang
代码
package mainimport (fmtreflectunicode/utf8
)func main() {original2 : []byte{0xef, 0x8f, 0xff}transformed2 : []byte(string(original2))fmt.Println()fmt.Println(byte byte demo)fmt.Println(Original byte:, original2)fmt.Println(Transformed byte:, transformed2)fmt.Println(Byte are equal:, reflect.DeepEqual(original2, transformed2))fmt.Println(Original byte string:, string(original2))fmt.Println(Transformed byte string:, string(transformed2))if utf8.Valid(original2) {fmt.Println(Original is valid UTF-8)} else {fmt.Println(Original is not valid UTF-8)}fmt.Println()fmt.Println(rune rune demo)originalRune : []rune{0xef, 0x8f, 0xff}transformedRune : []rune(string(originalRune))fmt.Println(Original rune:, originalRune)fmt.Println(Transformed rune:, transformedRune)fmt.Println(Rune are equal:, reflect.DeepEqual(originalRune, transformedRune))fmt.Println(Original rune string:, string(originalRune))fmt.Println(Transformed rune string:, string(transformedRune))fmt.Println()fmt.Println(next rune to byte demo)originalRune2 : []rune{0xef, 0x8f, 0xff}transformedRune2 : []byte(string(originalRune2))fmt.Println(Original rune:, originalRune2)fmt.Println(Transformed byte:, transformedRune2)fmt.Println(Rune byte are equal:, reflect.DeepEqual(originalRune2, transformedRune2))fmt.Println(Original rune string:, string(originalRune2))fmt.Println(Transformed byte string:, string(transformedRune2))fmt.Println()fmt.Println(next byte to rune demo)originalRune3 : []byte{0xef, 0x8f, 0xff}transformedRune3 : []rune(string(originalRune3))fmt.Println(Original byte:, originalRune3)fmt.Println(Transformed rune:, transformedRune3)fmt.Println(Byte Rune are equal:, reflect.DeepEqual(originalRune3, transformedRune3))fmt.Println(Original byte string:, string(originalRune3))fmt.Println(Transformed rune string:, string(transformedRune3))fmt.Println()fmt.Println(打印替换字符的byte和rune二进制格式)fmt.Println([]byte(string(\uFFFD)))fmt.Println([]rune(string(\uFFFD)))
}输出
byte byte demo
Original byte: [239 143 255]
Transformed byte: [239 143 255]
Byte are equal: true
Original byte string:
Transformed byte string:
Original is not valid UTF-8rune rune demo
Original rune: [239 143 255]
Transformed rune: [239 143 255]
Rune are equal: true
Original rune string: ïÿ
Transformed rune string: ïÿnext rune to byte demo
Original rune: [239 143 255]
Transformed byte: [195 175 194 143 195 191]
Rune byte are equal: false
Original rune string: ïÿ
Transformed byte string: ïÿnext byte to rune demo
Original byte: [239 143 255]
Transformed rune: [65533 65533 65533]
Byte Rune are equal: false
Original byte string:
Transformed rune string: 打印替换字符的byte和rune二进制格式
[239 191 189]
[65533]这里的代码中输入都是byte[]0xef, 0x8f, 0xff非标准的UTF-8变长字节转换过程会出现字符串显示乱码。
相关链接
1本文原理参考文档
Java里byte[]和String转换不一致的坑
Java byte[]和String转换问题
关于Java中bytes到String的转换
深入剖析go中字符串的编码问题——特殊字符的string怎么转byte
【Golang】深究字符串——从byte rune string到Unicode与UTF-8
2容易忽视的类似问题与本文的问题相似但有些区别也很有趣
Redis数据迁移过程使用jedis客户端需要注意区分string和byte命令转换字符编码不一致的问题使用不当会导致丢数据_redis数据转编议-CSDN博客