由于Stackoverflow鼓励自我回答,因此我尝试回应自己。
编码是将数据从一种格式转换为另一种格式的过程,此响应我详细介绍了String编码在Java中的工作方式(您可能需要阅读这篇文章,以更全面地了解文本结尾编码)。
介绍
字符串编码/解码是将byte []转换为String的过程,反之亦然。
乍一看,您可能会认为没有问题,但是如果我们对过程进行更深入的了解,可能会出现一些问题。 在最低级别上,信息以字节为单位存储/传输
:文件是字节序列,网络通信通过发送和接收字节来完成。因此,每次您想要读取或写入具有简单可读内容的文件时,或者每次您提交Web表单/阅读Web页面时,都会进行基础编码操作。让我们从Java中的基本String编码操作开始;从字节序列创建字符串。下面的代码将byte
[](字节可能来自文件或套接字)转换为String。
byte[] stringInByte=new byte[]{104,101,108,108,111}; String simple=new String(stringInByte); System.out.println("simple=" + simple);//prints simple=hello到目前为止,一切都很“简单”。字节的值取自此处,它显示了一种将字母和数字映射到字节的方法。让我们通过一个简单的要求使样本复杂化:byte
[]包含€(欧元)符号;糟糕,ascii表中没有欧元符号。
这可以粗略地概括为问题的核心,人类可读的字符(以及其他一些必要的字符,例如回车符,换行符等)大于256,即不能仅用一个字节来表示。如果由于某种原因您必须坚持使用单字节表示形式(例如,由于历史原因,第一个编码表仅使用7个字节,则出于空间限制的原因,如果磁盘上的空间有限并且您只为英语人群编写文本文档,则不会需要包含带有重音符号(例如è,ì)的意大利语字母,那么您就很难选择要代表的字符。
选择一种编码就是选择字节和字符之间的映射。
回到欧元示例,并坚持一个字节->映射ISO8859-15编码表的一个字符具有€符号;代表字符串“ hello€”的字节序列如下
byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164};您如何“告诉” Java用于转换的编码?字符串具有构造函数
String(byte[] bytes, String charsetName)
这样就可以指定“映射”。如果使用不同的字符集,则会得到不同的输出结果,如下所示:
byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164}; String simple1=new String(stringInByte1,"ISO8859-15"); System.out.println("simple1=" + simple1); //prints simple1=hello € String simple2=new String(stringInByte1,"ISO8859-1"); System.out.println("simple2=" + simple2); //prints simple1=hello ¤因此,这解释了为什么您读取某些字符并读取不同的字符的原因,用于写入的编码(从String到byte [])与用于读取的编码(从byte
[]到String)不同。同一字节可能以不同的编码映射到不同的字符,因此某些字符可能“看起来很奇怪”。
这些是理解String编码所需的基本概念。让我们把事情复杂化一点。为了实现已创建的多字节编码,可能需要在一个文本文档中表示超过256个符号。
使用多字节编码时,不再有一个字节- >一个字符映射,但是有字节序列->一个字符映射
UTF-8是最著名的多字节编码之一;UTF-8是一种可变长度编码,有些字符用一个字节表示,而另一些字符用一个以上字节表示;
UTF-8与某个一字节编码(例如us7ascii或ISO8859-1)重叠; 它可以看作是一个字节编码的扩展。
让我们来看第一个示例的UTF-8实际应用
byte[] stringInByte=new byte[]{104,101,108,108,111}; String simple=new String(stringInByte); System.out.println("simple=" + simple);//prints simple=hello String simple3=new String(stringInByte, "UTF-8"); System.out.println("simple3=" + simple3);//also this prints simple=hello如您所见,尝试执行该代码将打印出问候,即,表示UTF-8和ISO8859-1中的问候的字节是相同的。
但是,如果您尝试使用带有€符号的样本,则会得到一个?
byte[] stringInByte1=new byte[]{104,101,108,108,111,32,(byte)164}; String simple1=new String(stringInByte1,"ISO8859-15"); System.out.println("simple1=" + simple1);//prints simple1=hello String simple4=new String(stringInByte1, "UTF-8"); System.out.println("simple4=" + simple4);//prints simple4=hello ?表示无法识别字符,并且存在错误。 请注意,即使转换期间发生错误,您也不会例外。
不幸的是,在处理无效字符时,并非所有java类的行为都相同。让我们看看处理xml时会发生什么。
管理XML
在进行示例之前,值得记住的是,在Java中InputStream / OutputStream的读/写字节和Reader / Writer的读/写字符。
让我们尝试以一些不同的方式读取xml的字节序列,即读取文件以获取String与读取文件以获取DOM。
//Create a xml file String xmlSample="<?xml version="1.0" encoding="UTF-8"?>n<specialchars>àèìòù€</specialchars>"; try(FileOutputStream fosXmlFileOutputStreame= new FileOutputStream("test.xml")) { //write the file with a wrong encoding fosXmlFileOutputStreame.write(xmlSample.getBytes("ISO8859-15")); } try ( FileInputStream xmlFileInputStream= new FileInputStream("test.xml"); //read the file with the encoding declared in the xml header InputStreamReader inputStreamReader= new InputStreamReader(xmlFileInputStream,"UTF-8"); ) { char[] cbuf=new char[xmlSample.length()]; inputStreamReader.read(cbuf); System.out.println("file read with UTF-8=" + new String(cbuf)); //prints //file read with UTF-8=<?xml version="1.0" encoding="UTF-8"?> //<specialchars>������</specialchars> } File xmlFile = new File("test.xml"); documentBuilderFactory dbFactory = documentBuilderFactory.newInstance(); documentBuilder dBuilder = dbFactory.newdocumentBuilder(); document doc = dBuilder.parse(xmlFile); //throwscom.sun.org.apache.xerces.internal.impl.io.MalformedByteSequenceException:3字节UTF-8序列的无效字节2
在第一种情况下,结果是一些奇怪的字符,但没有异常,在第二种情况下,您得到一个异常(无效序列…。)发生异常是因为您正在读取UTF-8序列的三个字节的字符字节的值无效(因为使用UTF-8编码字符的方式)。
棘手的部分是,由于UTF-8与其他编码重叠,所以3字节UTF-8序列的无效字节2出现了“随机”异常(即,仅对于字符由一个以上字节表示的消息),因此在生产中在环境中,错误可能很难跟踪和重现。
利用所有这些信息,我们可以尝试回答以下问题:
为什么在读取/处理xml文件时出现y字节UTF-8序列无效字节x异常?
因为用于写入的编码(上述测试案例中的ISO8859-15)与用于读取的编码(上述测试案例中的UTF-8)不匹配;不匹配可能有一些不同的原因:
- 您在字节和char之间进行了一些错误的转换:例如,如果您正在使用InputStream读取文件并将其转换为Reader并将Reader传递给xml库,则必须按照以下代码指定字符集名称(即,您必须知道用于保存文件的编码)
try ( FileInputStream xmlFileInputStream= new FileInputStream("test.xml");//this is the reader for the xml library (DOM4J, JDOM for example) //UTF-8 isthe file encoding if you specify a wrong encoding or you do not apsecify anyencoding you may face Invalid byte x of y-byte UTF-8 sequence ExceptionInputStreamReader inputStreamReader= newInputStreamReader(xmlFileInputStream,"UTF-8"); )您将InputStream直接传递到xml库,但是该文件的文件不正确(例如,在第一个管理xml的示例中,标头声明UTF-8,但实际编码为ISO8859-15。 只需将文件还不够;必须使用标头中使用的编码保存文件。
您正在使用创建时未指定编码器的阅读器读取文件,并且平台编码与文件编码不同:
FileReader fileReader=new FileReader("text.xml");
这导致一个方面,至少对我来说,这是java中大多数String编码问题的根源:使用默认平台编码
您打电话的时候
"Hello €".getBytes();
在不同的操作系统上可以获得不同的结果;这是因为在Windows上,默认编码为Windows-1252,而在Linux上,默认编码为UTF-8;€char的编码方式不同,因此您不仅获得了不同的字节,而且还获得了不同的数组大小:
String helloEuro="hello €"; //prints hello euro byte[] size in iso8859-15 = 7 System.out.println("hello euro byte[] size in iso8859-15 = " + helloEuro.getBytes("ISO8859-15").length); //prints hello euro byte[] size in utf-8 = 9 System.out.println("hello euro byte[] size in utf-8 = " + helloEuro.getBytes("UTF-8").length);在遇到编码问题时,首先要使用String.getBytes()或新的String(byte [] …)而不指定编码是首先要做的检查
第二个是检查您是使用FileReader还是FileWriter读取或写入文件;在这两种情况下,文档均指出:
此类的构造函数假定默认字符编码和默认字节缓冲区大小是可接受的
与String.getBytes()一样,使用读取器/写入器在不同平台上读取/写入相同文件且未指定字符集可能会由于不同的默认平台编码而导致字节序列不同
正如javadoc建议的那样,解决方案是使用OutputStreamReader / OutputStreamWriter,该输出将OutputStream
/ InputStream和一个字符集规范包装在一起。
关于某些xml库如何读取XML内容的最后说明:
如果您通过阅读器,则库将依赖阅读器进行编码(即,它不检查xml标头中的内容),并且与编码无关,因为它正在读取字符而不是字节。
如果传递InputStream或File库依赖于xml标头进行编码,则它可能会引发一些编码异常
数据库
处理数据库时可能会出现另一个问题。创建数据库时,它具有用于保存varchar和string列(作为clob)的编码属性。如果使用8位编码(例如ISO8859-15)创建数据库,则当您尝试插入编码不允许的字符时,可能会出现问题。db中保存的内容可能与Java级别指定的字符串不同,因为在Java中,字符串在UTF-16的内存中表示,比在数据库级别指定的字符串“宽”。最简单的解决方案是:使用UTF-8编码创建数据库。
网络, 这是一个很好的起点。
如果您觉得缺少什么,请随时在评论中要求更多。



