美文网首页
Java中文乱码详解

Java中文乱码详解

作者: 上善若泪 | 来源:发表于2021-11-02 11:45 被阅读0次

    1 Java编码转换

    1.1 String转换图

    在这里插入图片描述

    图中详细描述了 字符串类String 与 文件File ,ByteBufferCharBufferbyte[] 数组char[]数组之间的互相转换

    1.2 String和Unicode编码

    StringUnicode编码关系:String类始终是以Unicode编码形式存储

    String.getBytes()的使用:如果不带字符集参数,就会依赖于JVM的字符集编码,LINUX上一般为UNICODE,WINDOWS下一般为GBK.(要想改变JVM缺省字符集编码,启动JVM时用选项-Dfile.encodeing=UTF-8。为了安全起见,建议始终带参数调用,例如:String s ; s.getBytes("UTF-8")

    Charset类非常好用,Charset.forName("编码格式"),同时返回一个实例对象如:charset,那么编码解码如下:

    • charset.encode是编码,即把String按你指定的字符集编码格式进行编码后输出字节数组。
    • charset.decode是解码,即把一个字节数组按你指定的字符集编码格式进行解码后输出成字符串。

    1.3 new String()的理解

    String newStr = new String(oldStr.getBytes(), "UTF-8");  
    

    java中的String类是按照unicode进行编码的,当使用String(byte[] bytes, String encoding)构造字符串时,encoding所指的是bytes中的数据是按照那种方式编码的,而不是最后产生的String是什么编码方式,换句话说,是让系统把bytes中的数据由encoding编码方式转换成unicode编码,因为String类始终是以Unicode编码形式存储。
    如果不指明,bytes的编码方式将由jdk根据操作系统决定。

    1.4 实际例子

    举例如下:

        String s = Charset.defaultCharset().displayName();
        String s1 = "我喜欢你,My Love";
       
        ByteBuffer bb1 = ByteBuffer.wrap(s1.getBytes("UTF-8"));
    
        for(byte bt:bb1.array()){
            System.out.printf("%x",bt);
        }
        //char[]用法
        char[] chArray={'I','L','o','v','e','你'};
    
        //CharBuffer用法
        CharBuffer cb = CharBuffer.wrap(chArray);
        //重新定位指针
        cb.flip();
    
        String s2= new String(chArray);
    
        //ByteBuffer用法
        ByteBuffer bb2 = Charset.forName("utf-8").encode(cb);
    
        // 利用Charset编码为指定字符集
    
        ByteBuffer bb3 = Charset.forName("utf-8").encode(s1);
    
        byte [] b   = bb3.array() ;
    
        // 利用Charset按指定字符集解码为字符串
        ByteBuffer bb4= ByteBuffer.wrap(b);
    
        String s2 = Charset.forName("utf-8").decode(bb4).toString();
    

    使用String构造方法和String.getBytes()做好中文字符转码

    @Test
     public void test() {
          String testStr = "中";
          try {
              // 得到指定编码的字节数组 字符串--->字节数组 6         byte[] t_iso = testStr.getBytes("ISO8859-1");
             byte[] t_gbk = testStr.getBytes("GBK");
             byte[] t_utf8 = testStr.getBytes("UTF-8");
             System.out.println("使用ISO解码..." + t_iso.length);
             System.out.println("使用GBK解码..." + t_gbk.length);
             System.out.println("使用UTF8解码..." + t_utf8.length);
             // 解码后在组装13         String ut_iso = new String(t_iso, "ISO8859-1");
             String ut_gbk = new String(t_gbk, "GBK");
             String ut_utf8 = new String(t_utf8, "UTF-8");
             System.out.println("使用ISO解码后再用ISO组装..." + ut_iso);
             System.out.println("使用GBK解码后再用GBK组装..." + ut_gbk);
             System.out.println("使用UTF8解码后再用UTF8组装..." + ut_utf8);
             // 有时候要求必须是iso字符编码类型
             // 可以先用GBK/UTF8编码后,用ISO8859-1组装成字符串,解码时逆向即可获得正确中文字符21         String t_utf8Toiso = new String(t_utf8, "ISO8859-1");
             // 将iso编码的字符串进行还原23         String ut_utf8Toiso = new String(t_utf8Toiso.getBytes("ISO8859-1"),"UTF-8");
             System.out.println("使用ISO组装utf8编码字符..." + t_utf8Toiso);
             System.out.println("使用ISO解码utf8编码字符..." + ut_utf8Toiso);
         } catch (UnsupportedEncodingException e) {
             e.printStackTrace();
         }
    }
    

    1.5 java编码转换过程

    我们总是用一个java类文件和用户进行最直接的交互(输入、输出),这些交互内容包含的文字可能会包含中文
    这些过程是从宏观上面来观察的,了解这个肯定是不行的,我们需要真正来了解java是如何来编码和被解码的

    1.5.1 编辑源文件

    当我们用编辑器编写java源文件,程序文件在保存时会采用操作系统默认的编码格式(一般我们中文的操作系统采用的是GBK编码格式)形成一个.java文件。java源文件是采用操作系统默认支持的file.encoding编码格式保存的。下面代码可以查看系统的file.encoding参数值。
    System.out.println(System.getProperty("file.encoding"));

    1.5.2 编译源文件

    当我们使用javac.exe编译我们的java文件时,JDK首先会确认它的编译参数encoding来确定源代码字符集,如果我们不指定该编译参数,JDK首先会获取操作系统默认的file.encoding参数,然后JDK就会把我们编写的java源程序从file.encoding编码格式转化为JAVA内部默认的UNICODE格式放入内存中

    JDK将上面编译好的且保存在内存中信息写入class文件中,形成.class文件。此时.class文件是Unicode编码的,也就是说我们常见的.class文件中的内容无论是中文字符还是英文字符,他们都已经转换为Unicode编码格式了。
    在这一步中对JSP源文件的处理方式有点儿不同:WEB容器调用JSP编译器,JSP编译器首先会查看JSP文件是否设置了文件编码格式,如果没有设置则JSP编译器会调用调用JDK采用默认的编码方式将JSP文件转化为临时的servlet类,然后再编译为.class文件并保持到临时文件夹中。

    1.5.3 运行编译类

    运行编译的类:在这里会存在一下几种情况
    直接在console上运行,JSP/Servlet类,java类与数据库之间。
    这三种情况每种情况的方式都会不同,

    1.5.3.1 Console上运行的类

    这种情况下,JVM首先会把保存在操作系统中的class文件读入到内存中,这个时候内存中class文件编码格式为Unicode,然后JVM运行它。如果需要用户输入信息,则会采用file.encoding编码格式对用户输入的信息进行编码同时转换为Unicode编码格式保存到内存中。程序运行后,将产生的结果再转化为file.encoding格式返回给操作系统并输出到界面去。整个流程如下:

    在这里插入图片描述

    在上面整个流程中,凡是涉及的编码转换都不能出现错误,否则将会产生乱码。

    1.5.3.2 JSPServlet类

    <%@ page language="java" 
    contentType="text/html; 
    charset=UTF-8" pageEncoding="GBK" %>
    

    在上面代码中有两个地方存在编码:pageEncodingcontentTypecharset。其中pageEncodingjsp文件本身的编码,而contentTypecharset是指服务器发送给客户端时的内容编码

    1. JVMJSP编译为.jsp文件。在这个过程中pageEncoding就起到作用了,JVM首先会获取pageEncoding的值,如果该值存在则采用它设定的编码来编译,否则则采用file.encoding编码来编译。
    2. JVM.java文件转换为.class文件。在这个过程就与任何编码的设置都没有关系了,不管JSP采用了什么样的编码格式都将无效。经过这个阶段后.jsp文件就转换成了统一的Unicode格式的.class文件了。
    3. 后台经过业务逻辑处理后将产生的结果输出到客户端。在这个过程中contentTypecharset就发挥了功效。如果设置了charset则浏览器就会使用指定的编码格式进行解码,否则采用默认的ISO-8859-1编码格式进行解码处理。
    1. 传输时的编码格式
      当用户请求Servlet时,WEB容器会调用它的JVM来运行Servlet。首先JVM会把servletclass加载到内存中去,内存中的servlet代码是Unicode编码格式的。然后JVM在内存中运行该Servlet,在运行过程中如果需要接受从客户端传递过来的数据(如表单和URL传递的数据),则WEB容器会接受传入的数据,在接收过程中如果程序设定了传入参数的的编码则采用设定的编码格式,如果没有设置则采用默认的ISO-8859-1编码格式,接收的数据后JVM会将这些数据进行编码格式转换为Unicode并且存入到内存中。运行Servlet后产生输出结果,同时这些输出结果的编码格式仍然为Unicode。紧接着WEB容器会将产生的Unicode编码格式的字符串直接发送置客户端,如果程序指定了输出时的编码格式,则按照指定的编码格式输出到浏览器,否则采用默认的ISO-8859-1编码格式。整个过程流程图如下:
      在这里插入图片描述

    1.5.3.4 数据库部分

    我们知道java程序与数据库的连接都是通过JDBC驱动程序来连接的,而JDBC驱动程序默认的是ISO-8859-1编码格式的,也就是说我们通过java程序向数据库传递数据时,JDBC首先会将Unicode编码格式的数据转换为ISO-8859-1的编码格式,然后在存储在数据库中,即在数据库保存数据时,默认格式为ISO-8859-1

    在这里插入图片描述

    1.6 java是如何编码解码的

    1.6.1 编码&解码

    在java中主要有四个场景需要进行编码解码操作:
    I/O操作,内存,数据库,javaWeb
    下面主要介绍前面两种场景,数据库部分只要设置正确编码格式就不会有什么问题,javaWeb场景过多需要了解URLgetPOST的编码,servlet的解码

    1.6.2 I/O操作

    所谓乱码问题无非就是转码过程中编码格式的不统一产生的,比如编码时采用UTF-8,解码采用GBK,但最根本的原因是字符到字节或者字节到字符的转换出问题了,而这中情况的转换最主要的场景就是I/O操作的时候。当然I/O操作主要包括网络I/O(也就是javaWeb)和磁盘I/O
    首先我们先看I/O的编码操作

    在这里插入图片描述
    InputStream为字节输入流的所有类的超类,Reader为读取字符流的抽象类。java读取文件的方式分为按字节流读取和按字符流读取,其中InputStreamReader是这两种读取方式的超类。

    1.6.3 按字节

    一般都是使用InputStream.read()方法在数据流中读取字节(read()每次都只读取一个字节,效率非常慢,我们一般都是使用read(byte[])),然后保存在一个byte[]数组中,最后转换为String。在我们读取文件时,读取字节的编码取决于文件所使用的编码格式,而在转换为String过程中也会涉及到编码的问题,如果两者之间的编码格式不同可能会出现问题。例如存在一个问题test.txt编码格式为UTF-8,那么通过字节流读取文件时所获得的数据流编码格式就是UTF-8,而我们在转化成String过程中如果不指定编码格式,则默认使用系统编码格式(GBK)来解码操作,由于两者编码格式不一致,那么在构造String过程肯定会产生乱码,如下

    File file = new File("C:\\test.txt");
            InputStream input = new FileInputStream(file);
            StringBuffer buffer = new StringBuffer();
            byte[] bytes = new byte[1024];
            for(int n ; (n = input.read(bytes))!=-1 ; ){
                buffer.append(new String(bytes,0,n));
            }
            System.out.println(buffer);
    
    输出结果:锘挎垜鏄?cm
    

    要想不出现乱码,在构造String过程中指定编码格式,使得编码解码时两者编码格式保持一致即可:
    buffer.append(new String(bytes,0,n,"UTF-8"));

    1.6.4 按字符

    其实字符流可以看做是一种包装流,它的底层还是采用字节流来读取字节,然后它使用指定的编码方式将读取字节解码为字符。在javaReader是读取字符流的超类。所以从底层上来看按字节读取文件和按字符读取没什么区别。在读取的时候字符读取每次是读取留个字节,字节流每次读取一个字节。

    1.6.5 字节&字符转换

    字节转换为字符一定少不了InputStreamReaderAPI解释如下:InputStreamReader是字节流通向字符流的桥梁:它使用指定的 charset读取字节并将其解码为字符。它使用的字符集可以由名称指定或显式给定,或者可以接受平台默认的字符集。 每次调用 InputStreamReader 中的一个read()方法都会导致从底层输入流读取一个或多个字节。要启用从字节到字符的有效转换,可以提前从底层流读取更多的字节,使其超过满足当前读取操作所需的字节。API解释非常清楚,InputStreamReader在底层读取文件时仍然采用字节读取,读取字节后它需要根据一个指定的编码格式来解析为字符,如果没有指定编码格式则采用系统默认编码格式。

    String file = "C:\\test.txt"; 
             String charset = "UTF-8"; 
             // 写字符换转成字节流
             FileOutputStream outputStream = new FileOutputStream(file); 
             OutputStreamWriter writer = new OutputStreamWriter(outputStream, charset); 
             try { 
                writer.write("我是 cm"); 
             } finally { 
                writer.close(); 
             }          
             // 读取字节转换成字符
             FileInputStream inputStream = new FileInputStream(file); 
             InputStreamReader reader = new InputStreamReader(inputStream, charset); 
             StringBuffer buffer = new StringBuffer(); 
             char[] buf = new char[64]; 
             int count = 0; 
             try { 
                while ((count = reader.read(buf)) != -1) { 
                    buffer.append(buf, 0, count); 
                } 
             } finally { 
                reader.close(); 
             }
             System.out.println(buffer);
    

    1.6.6 内存

    首先我们看下面这段简单的代码

    String s = "我是 cm"; 
    byte[] bytes = s.getBytes(); 
    String s1 = new String(bytes,"GBK"); 
    String s2 = new String(bytes);
    

    在这段代码中我们看到了三处编码转换过程(一次编码,两次解码)。先看String.getTytes():

    public byte[] getBytes() {
            return StringCoding.encode(value, 0, value.length);
        }
    

    内部调用StringCoding.encode()方法操作:

    static byte[] encode(char[] ca, int off, int len) {
            String csn = Charset.defaultCharset().name();
            try {
                // use charset name encode() variant which provides caching.
                return encode(csn, ca, off, len);
            } catch (UnsupportedEncodingException x) {
                warnUnsupportedCharset(csn);
            }
            try {
                return encode("ISO-8859-1", ca, off, len);
            } catch (UnsupportedEncodingException x) {
                // If this code is hit during VM initialization, MessageUtils is
                // the only way we will be able to get any kind of error message.
                MessageUtils.err("ISO-8859-1 charset not available: "  + x.toString());
                // If we can not find ISO-8859-1 (a required encoding) then things
                // are seriously wrong with the installation.
                System.exit(1);
                return null;
            }
        }
    

    encode(char[] paramArrayOfChar, int paramInt1, int paramInt2)方法首先调用系统的默认编码格式,如果没有指定编码格式则默认使用ISO-8859-1编码格式进行编码操作,进一步深入如下:String csn = (charsetName == null) ? "ISO-8859-1" : charsetName;
    同样的方法可以看到new String的构造函数内部是调用StringCoding.decode()方法:

    public String(byte bytes[], int offset, int length, Charset charset) {
            if (charset == null)
                throw new NullPointerException("charset");
            checkBounds(bytes, offset, length);
            this.value =  StringCoding.decode(charset, bytes, offset, length);
        }
    

    decode方法和encode对编码格式的处理是一样的。
    对于以上两种情况我们只需要设置统一的编码格式一般都不会产生乱码问题。

    1.6.7 编码&编码格式

    首先先看看java编码类图

    在这里插入图片描述
    首先根据指定的chart设置ChartSet类,然后根据ChartSet创建ChartSetEncoder对象,最后再调用 CharsetEncoder.encode对字符串进行编码,不同的编码类型都会对应到一个类中,实际的编码过程是在这些类中完成的。下面时序图展示详细的编码过程:
    在这里插入图片描述
    通过这编码的类图和时序图可以了解编码的详细过程。下面将通过一段简单的代码对ISO-8859-1、GBK、UTF-8编码
    public class Test02 {
        public static void main(String[] args) throws UnsupportedEncodingException {
            String string = "我是 cm";
            Test02.printChart(string.toCharArray());
            Test02.printChart(string.getBytes("ISO-8859-1"));
            Test02.printChart(string.getBytes("GBK"));
            Test02.printChart(string.getBytes("UTF-8"));
        }
        
        /**
         * char转换为16进制
         */
        public static void printChart(char[] chars){
            for(int i = 0 ; i < chars.length ; i++){
                System.out.print(Integer.toHexString(chars[i]) + " "); 
            }
            System.out.println("");
        }
        
        /**
         * byte转换为16进制
         */
        public static void printChart(byte[] bytes){
            for(int i = 0 ; i < bytes.length ; i++){
                String hex = Integer.toHexString(bytes[i] & 0xFF); 
                 if (hex.length() == 1) { 
                   hex = '0' + hex; 
                 } 
                 System.out.print(hex.toUpperCase() + " "); 
            }
            System.out.println("");
        }
    }
    -------------------------outPut:
    6211 662f 20 63 6d 
    3F 3F 20 63 6D 
    CE D2 CA C7 20 63 6D 
    E6 88 91 E6 98 AF 20 63 6D
    

    通过程序我们可以看到“我是 cm”的结果为:
    char[]:6211 662f 20 63 6d
    ISO-8859-1:3F 3F 20 63 6D
    GBK:CE D2 CA C7 20 63 6D
    UTF-8:E6 88 91 E6 98 AF 20 63 6D
    图如下:


    在这里插入图片描述

    1.7 javaWeb中的编码解码

    1.7.1 编码&解码

    用户想服务器发送一个HTTP请求,需要编码的地方有urlcookieparameter,经过编码后服务器接受HTTP请求,解析HTTP请求,然后对urlcookieparameter进行解码。在服务器进行业务逻辑处理过程中可能需要读取数据库、本地文件或者网络中的其他文件等等,这些过程都需要进行编码解码。当处理完成后,服务器将数据进行编码后发送给客户端,浏览器经过解码后显示给用户。在这个整个过程中涉及的编码解码的地方较多,其中最容易出现乱码的位置就在于服务器与客户端进行交互的过程。
    上面整个过程可以概括成这样,页面编码数据传递给服务器,服务器对获得的数据进行解码操作,经过一番业务逻辑处理后将最终结果编码处理后传递给客户端,客户端解码展示给用户。所以下面我就请求对javaweb的编码&解码进行阐述。

    客户端想服务器发送请求无非就通过四中情况:

    • URL方式直接访问。
    • 页面链接。
    • 表单get提交
    • 表单post提交

    1.7.2 URL方式

    对于URL,如果该URL中全部都是英文的那倒是没有什么问题,如果有中文就要涉及到编码了。如何编码?根据什么规则来编码?又如何来解码呢,首先看URL的组成部分:

    在这里插入图片描述
    在这URL中浏览器将会对pathparameter进行编码操作。为了更好地解释编码过程,使用如下URL
    http://127.0.0.1:8080/perbank/我是cm?name=我是cm
    将以上地址输入到浏览器URL输入框中,通过查看http 报文头信息我们可以看到浏览器是如何进行编码的。下面是IE、Firefox、Chrome三个浏览器的编码情况:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    可以看到各大浏览器对我是的编码情况如下:
    path部分 Query String
    Firefox E6 88 91 E6 98 AF E6 88 91 E6 98 AF
    Chrome E6 88 91 E6 98 AF E6 88 91 E6 98 AF
    IE E6 88 91 E6 98 AF CE D2 CA C7

    对于path部分Firefox、chrome、IE都是采用UTF-8编码格式,对于Query String部分Firefox、chrome采用UTF-8IE采用GBK。至于为什么会加上%,这是因为URL的编码规范规定浏览器将ASCII字符非 ASCII字符按照某种编码格式编码成16进制数字然后将每个16 进制表示的字节前加上%
    当然对于不同的浏览器,相同浏览器不同版本,不同的操作系统等环境都会导致编码结果不同,上表某一种情况,对于URL编码规则下任何结论都是过早的。由于各大浏览器、各个操作系统对URLURIQueryString编码都可能存在不同,这样对服务器的解码势必会造成很大的困扰,下面我们将tomcat,看tomcat是如何对URL进行解码操作的。
    解析请求的 URL 是在 org.apache.coyote.HTTP11.InternalInputBufferparseRequestLine 方法中,这个方法把传过来的 URLbyte[] 设置到 org.apache.coyote.Request的相应的属性中。这里的URL 仍然是 byte格式,转成 char 是在 org.apache.catalina.connector.CoyoteAdapterconvertURI方法中完成的:

    protected void convertURI(MessageBytes uri, Request request) 
                 throws Exception { 
                        ByteChunk bc = uri.getByteChunk(); 
                        int length = bc.getLength(); 
                        CharChunk cc = uri.getCharChunk(); 
                        cc.allocate(length, -1); 
                        String enc = connector.getURIEncoding();     //获取URI解码集
                        if (enc != null) { 
                            B2CConverter conv = request.getURIConverter(); 
                            try { 
                                if (conv == null) { 
                                    conv = new B2CConverter(enc); 
                                    request.setURIConverter(conv); 
                                } 
                            } catch (IOException e) {...} 
                            if (conv != null) { 
                                try { 
                                    conv.convert(bc, cc, cc.getBuffer().length - cc.getEnd()); 
                                    uri.setChars(cc.getBuffer(), cc.getStart(), cc.getLength()); 
                                    return; 
                                } catch (IOException e) {...} 
                            } 
                        } 
                        // Default encoding: fast conversion 
                        byte[] bbuf = bc.getBuffer(); 
                        char[] cbuf = cc.getBuffer(); 
                        int start = bc.getStart(); 
                        for (int i = 0; i < length; i++) { 
                            cbuf[i] = (char) (bbuf[i + start] & 0xff); 
                        } 
                        uri.setChars(cbuf, 0, length); 
        }
    

    从上面的代码可知,对URI的解码操作是首先获取Connector的解码集,该配置在server.xml

    <Connector URIEncoding="utf-8"  />
    

    如果没有定义则会采用默认编码ISO-8859-1来解析。
    对于Query String部分,我们知道无论我们是通过get方式还是POST方式提交,所有的参数都是保存在Parameters,然后我们通过request.getParameter,解码工作就是在第一次调用getParameter方法时进行的。在getParameter方法内部它调用org.apache.catalina.connector.RequestparseParameters 方法,这个方法将会对传递的参数进行解码。下面代码只是parseParameters方法的一部分:

              //获取编码
                 String enc = getCharacterEncoding();
                //获取ContentType 中定义的 Charset
                boolean useBodyEncodingForURI = connector.getUseBodyEncodingForURI();
                if (enc != null) {    //如果设置编码不为空,则设置编码为enc                parameters.setEncoding(enc);
                    if (useBodyEncodingForURI) {   //如果设置了Chartset,则设置queryString的解码为ChartSet                    parameters.setQueryStringEncoding(enc);    
                    }
                } else {     //设置默认解码方式                parameters.setEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
                    if (useBodyEncodingForURI) {
                        parameters.setQueryStringEncoding(org.apache.coyote.Constants.DEFAULT_CHARACTER_ENCODING);
                    }
                }
    

    从上面代码可以看出对query String的解码格式要么采用设置的ChartSet要么采用默认的解码格式ISO-8859-1。注意这个设置的ChartSet是在http Header中定义的ContentType,同时如果我们需要改指定属性生效,还需要进行如下配置:
    <Connector URIEncoding="UTF-8" useBodyEncodingForURI="true"/>
    上面部分详细介绍了URL方式请求的编码解码过程。其实对于我们而言,我们更多的方式是通过表单的形式来提交。

    1.7.3 表单GET

    我们知道通过URL方式提交数据是很容易产生乱码问题的,所以我们更加倾向于通过表单形式。当用户点击submit提交表单时,浏览器会更加设定的编码来编码数据传递给服务器。通过GET方式提交的数据都是拼接在URL后面来提交的,所以tomcat服务器在进行解码过程中URIEncoding就起到作用了。tomcat服务器会根据设置的URIEncoding来进行解码,如果没有设置则会使用默认的ISO-8859-1来解码。假如我们在页面将编码设置为UTF-8,而URIEncoding设置的不是或者没有设置,那么服务器进行解码时就会产生乱码。这个时候我们一般可以通过new String(request.getParameter(“name”).getBytes(“iso-8859-1″),”utf-8″)的形式来获取正确数据。

    1.7.4 表单POST

    对于POST方式,它采用的编码也是由页面来决定的即contentType。当我通过点击页面的submit按钮来提交表单时,浏览器首先会根据contentTypecharset编码格式来对POST表单的参数进行编码然后提交给服务器,在服务器端同样也是用contentType中设置的字符集来进行解码(这里与get方式就不同了),这就是通过POST表单提交的参数一般而言都不会出现乱码问题。当然这个字符集编码我们是可以自己设定的:request.setCharacterEncoding(charset)

    1.8 解决URL中文乱码问题

    1.8.1 javascript

    使用javascript编码不给浏览器插手的机会,编码之后再向服务器发送请求,然后在服务器中解码。在掌握该方法的时候,我们需要料及javascript编码的三个方法:escape()、encodeURI()、encodeURIComponent()。

    1.8.1.1 escape

    采用SIO Latin字符集对指定的字符串进行编码。所有非ASCII字符都会被编码为%xx格式的字符串,其中xx表示该字符在字符集中所对应的16进制数字。例如,格式对应的编码为%20。它对应的解码方法为unescape()

    在这里插入图片描述
    事实上escape()不能直接用于URL编码,它的真正作用是返回一个字符的Unicode编码值。比如上面我是cm的结果为%u6211%u662Fcm,其中对应的编码为6211的编码为662Fcm编码为cm
    注意,escape()不对+编码。但是我们知道,网页在提交表单的时候,如果有空格,则会被转化为+字符。服务器处理数据的时候,会把+号处理成空格。所以,使用的时候要小心

    1.8.1.2 encodeURI

    对整个URL进行编码,它采用的是UTF-8格式输出编码后的字符串。不过encodeURI除了ASCII编码外对于一些特殊的字符也不会进行编码如:! @ # $& * ( ) = : / ; ? + ‘

    1.8.1.3 encodeURIComponent()

    URI字符串采用UTF-8编码格式转化成escape格式的字符串。相对于encodeURIencodeURIComponent会更加强大,它会对那些在encodeURI()中不被编码的符号(; / ? : @ & = + $ , #)统统会被编码。但是encodeURIComponent只会对URL的组成部分进行个别编码,而不用于对整个URL进行编码。对应解码函数方法decodeURIComponent
    当然我们一般都是使用encodeURI方来进行编码操作。所谓的javascript两次编码后台两次解码就是使用该方法。javascript解决该问题有一次转码、两次转码两种解决方法

    1.8.1.4 一次转码

    javascript转码:

    var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm';
    window.location.href = encodeURI(url);
    
    转码后的URL:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%E6%88%91%E6%98%AFcm
    后台处理:
    String name = request.getParameter("name");
            System.out.println("前台传入参数:" + name);
            name  = new String(name.getBytes("ISO-8859-1"),"UTF-8");
            System.out.println("经过解码后参数:" + name);
    
    输出结果:
    前台传入参数:??????cm 
    经过解码后参数:我是cm
    

    1.8.1.5 二次转码

    var url = '<s:property value="webPath" />/ShowMoblieQRCode.servlet?name=我是cm';
    window.location.href = encodeURI(encodeURI(url));
    
    转码后的url:http://127.0.0.1:8080/perbank/ShowMoblieQRCode.servlet?name=%25E6%2588%2591%25E6%2598%25AFcm
    后台处理:
            String name = request.getParameter("name");
            System.out.println("前台传入参数:" + name);
            name  = URLDecoder.decode(name,"UTF-8");
            System.out.println("经过解码后参数:" + name);
    
    输出结果:
    前台传入参数:E68891E698AFcm 
    经过解码后参数:我是cm
    

    2 计算机编码历史

    2.1 认识字符集

    2.1.1 问题起源

    对于计算机而言,它仅认识两个01,不管是在内存中还是外部存储设备上,我们所看到的文字、图片、视频等等数据在计算机中都是已二进制形式存在的。不同字符对应二进制数的规则,就是字符的编码。字符编码的集合称为字符集。
    在早期的计算机系统中,使用的字符是非常少的,他们只包括26个英文字母、数字符号和一些常用符号,对于这些字符进行编码,用1个字节就足够了,但是随着计算机的不断发展,为了适应全世界其他各国民族的语言,这些少得可怜的字符编码肯定是不够的。于是人们提出了UNICODE编码,它采用双字节编码,兼容英文字符和其他国家民族的双字节字符编码。
    每个国家为了统一编码都会规定该国家/地区计算机信息交换用的字符集编码,为了解决本地字符信息的计算机处理,于是出现了各种本地化版本,引进LANG, Codepage 等概念。
    现在大部分具有国际化特征的软件核心字符处理都是以 Unicode 为基础的,在软件运行时根据当时的 Locale/Lang/Codepage 设置确定相应的本地字符编码设置,并依此处理本地字符。在处理过程中需要实现 Unicode 和本地字符集的相互转换。
    java内部采用的就是Unicode编码,所以在java运行的过程中就必然存在从Unicode编码与相应的计算机操作系统或者浏览器支持的编码格式相互转化的过程,这个转换的过程有一系列的步骤,如果某个步骤出现错误,则输出的文字就会是乱码。所以产生java乱码的问题就在于JVM与对应的操作系统/浏览器进行编码格式转换时出现了错误。

    其实解决 JAVA 程序中的汉字编码问题的方法往往很简单,但理解其背后的原因,定位问题,还需要了解现有的汉字编码和编码转换。

    2.1.2 常见字符编码

    计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。常见的字符编码主要包括:ASCII编码、GBK编码、Unicode

    2.2 字符编码详解;基础知识 + ASCII + GB*

    2.2.1 基础知识

    在了解各种字符集之前我们需要了解一些最基础的知识,如:编码、字符、字符集、字符编码基础知识。

    2.2.1.1 编码

    计算机中存储的信息都是用二进制表示的,我们在屏幕上所看到文字、图片等都是通过二进制转换的结果。编码是信息从一种形式或格式转换为另一种形式的过程,通俗点讲就是就是将我们看到的文字、图片等信息按照某种规则存储在计算机中,例如‘c’在计算机中怎么表达,‘陈’在计算机中怎么表达,这个过程就称之为编码。解码是编码的逆过程,它是将存储在计算机的二进制转换为我们可以看到的文字、图片等信息,它体现的是视觉上的刺激。
    n位二进制数可以组合成2的n次方个不同的信息,给每个信息规定一个具体码组,这种过程也叫编码。
    在编码和解码中,他们就如加密、解密一般,他们一定会遵循某个规则,即y = f(x),那么x = f(y);否则在解密过程就会导致‘a’解析成‘b’或者乱码。

    2.2.1.2 字符

    字符是可使用多种不同字符方案或代码页来表示的抽象实体,它是一个单位的字形、类字形单位或符号的基本信息,也是各种文字和符号的总称,包括各国家文字、标点符号、图形符号、数字等。
    字符是指计算机中使用的字母、数字、字和符号,包括:1、2、3、A、B、C、~!·#¥%……—*()——+等等。在 ASCII 编码中,一个英文字母字符存储需要1个字节。在 GB 2312 编码或 GBK 编码中,一个汉字字符存储需要2个字节。在UTF-8编码中,一个英文字母字符存储需要1个字节,一个汉字字符储存需要3到4个字节。在UTF-16编码中,一个英文字母字符或一个汉字字符存储都需要2个字节(Unicode扩展区的一些汉字存储需要4个字节)。在UTF-32编码中,世界上任何字符的存储都需要4个字节

    2.2.1.3 字符集

    字符是各种文字和符号的总称,而字符集则是多个字符的集合,字符集种类较多,每个字符集包含的字符个数不同。而计算机要准确的处理各种字符集文字,需要进行字符编码,以便计算机能够识别和存储各种文字。
    常见字符集名称:ASCII字符集、GB2312字符集、BIG5字符集、 GB18030字符集、Unicode字符集等

    2.2.1.4 字符编码

    计算机中的信息包括数据信息和控制信息,然而不管是那种信息,他们都是以二进制编码的方式存入计算机中,但是他们是怎么展示在屏幕上的呢?同时在展现过程中如何才能保证他们不出错?这个时候字符编码就起到了重要作用!字符编码是一套规则,一套建立在符合集合与数字系统之间的对应关系之上的规则,它是信息处理的基本技术。
    使用字符编码这套规则能够对自然语言的字符的一个集合(如字母表或音节表),与其他东西的一个集合(如号码或电脉冲)进行配对

    2.2.2 ASCII

    2.2.2.1 标准ASCII码

    ASCIIAmerican Standard Code for Information Interchange,是基于拉丁字母的一套电脑编码系统,主要用于显示现代英语和其他西欧语言。它是现今最通用的单字节编码系统。
    ASCII码使用指定的7位或者8位二进制数字组合表示128或者256种可能的字符。标准的ASCII编码使用的是7(2^7 = 128)位二进制数来表示所有的大小写字母、数字和标点符号已经一些特殊的控制字符,最前面的一位统一规定为0。其中0~31127(共33个)是控制字符或通信专用字符,32~126(共95个)是字符(32是空格),其中48~57为0到9十个阿拉伯数字,65~90为26个大写英文字母,97~122号为26个小写英文字母,其余为一些标点符号、运算符号等

    前面提过标准的ASCII码是使用七位来表示字符的,而最高位(b7)则是用作奇偶校验的。所谓奇偶校验,是指在代码传送过程中用来检验是否出现错误的一种方法,一般分奇校验和偶校验两种。
    奇校验规定:正确的代码一个字节中1的个数必须是奇数,若非奇数,则在最高位b7添1;
    偶校验规定:正确的代码一个字节中1的个数必须是偶数,若非偶数,则在最高位b7添1。

    2.2.2.2 扩展ASCII码

    标准的ASCII是用七位来表示的,那么它的缺陷就非常明显了:只能显示26个基本拉丁字母、阿拉伯数目字和英式标点符号,基本上只能应用于现代美国英语,对于其他国家,128个字符肯定不够。于是,这些欧洲国家决定利用字节中闲置的最高位编入新的符号,这样一来,可以表达的字符数最多就为256个,但是随着产生的问题也就来了:不同的国家有不同的字母,可能同一个编码在不同的国家所表示的字符不同。但是不管怎么样,在这些编码中0~127所表示的字符肯定是一样的,不一样的也只是128~255这一段。
    8位ASCII在欧洲国家表现的不尽人意,那么在其他国家就更加不用说了,我们拥有五千年历史文化的中华名族所包含的汉字多大10多万,不知道是多少个256。所以一个字节8位表示的256个字符肯定是不够的,那么两个字节呢?可能够了吧!我们常见的汉字就是用两个字节表示的,如GB2312。

    2.2.3 GB*编码

    为了显示中文,我们必须设计一套编码规则用于将汉字转换为计算机可以接受的数字系统的数。显示中文的常用字符编码有:GB2312、GBK、GB18030

    2.2.3.1 GB2312

    GB2312,用于汉字处理、汉字通信等系统之间的信息交换,通行于中国大陆。它的编码规则是:小于127的字符的意义与原来相同,但两个大于127的字符连在一起时,就表示一个汉字,前面的一个字节(他称之为高字节)从0xA1用到 0xF7,后面一个字节(低字节)从0xA10xFE,这样我们就可以组合出大约7000多个简体汉字了。在这些编码里,还把数学符号、罗马希腊的 字母、日文的假名们都编进去了,连在ASCII里本来就有的数字、标点、字母都统统重新编了两个字节长的编码,这就是常说的全角字符,而原来在127 号以下的那些就叫半角字符了
    虽然GB2312收录了这么多汉子,他所覆盖的使用率可以达到99%,但是对于那些不常见的汉字,例如人名、地名、古汉语,它就不能处理了,于是就有下面的GBKGB 18030的出现。

    2.2.3.2 GBK

    GBK,全称《汉字内码扩展规范》,由中华人民共和国全国信息技术标准化技术委员会1995年12月1日制订,也是汉字编码的标准之一。
    GBKGB2312的扩展,他向下与GB2312兼容,,向上支持ISO 10646.1国际标准,是前者向后者过渡过程中的一个承上启下的标准。同时它是使用双字节编码方案,其编码范围从8140FEFE(剔除xx7F),首字节在 81-FE之间,尾字节在 40-FE 之间,共23940个码位,共收录了21003个汉字。

    2.2.3.3 GB18030

    GB18030,全称:国家标准GB 18030-2005《信息技术 中文编码字符集》,是我国计算机系统必须遵循的基础性标准之一,GB18030有两个版本:GB18030-2000GB18030-2005
    GB18030-2000GBK的取代版本,仅规定了常用非汉字符号和27533个汉字(包括部首、部件等)的编码,它的主要特点是在GBK基础上增加了CJK统一汉字扩充A的汉字。
    GB18030-2005是全文强制性标准,市场上销售的产品必须符合,它是GB18030-2000的基础上增加了42711个汉字和多种我国少数民族文字的编码。
    GB18030标准采用单字节、双字节和四字节三种方式对字符编码
    UTF-8相同,采用多字节编码,每个字可以由1个、2个或4个字节组成。
    编码空间庞大,最多可定义161万个字符。
    支持中国国内少数民族的文字,不需要动用造字区。
    汉字收录范围包含繁体汉字以及日韩汉字

    2.2.4 Unicode编码

    正如前面前面所提到的一样,世界存在这么多国家,也存在着多种编码风格,像中文的GB232、GBK、GB18030,这样乱搞一套,虽然在本地运行没有问题,但是一旦出现在网络上,由于互不兼容,访问则会出现乱码。为了解决这个问题,伟大的Unicode编码腾空出世。

    2.2.4.1 Unicode

    Unicode又称为统一码、万国码、单一码,它是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。可以想象Unicode作为一个字符大容器,它将世界上所有的符号都包含其中,并且每一个符号都有自己独一无二的编码,这样就从根本上解决了乱码的问题。所以Unicode是一种所有符号的编码。
    Unicode伴随着通用字符集的标准而发展,同时也以书本的形式对外发表,它是业界的标准,对世界上大部分的文字系统进行了整理、编码,使得电脑可以用更为简单的方式来呈现和处理文字。Unicode至今仍在不断增修,迄今而至已收入超过十万个字符,它备受业界认可,并广泛地应用于电脑软件的国际化与本地化过程。
    我们知道Unicode是为了解决传统的字符编码方案的局限而产生的,对于传统的编码方式而言,他们都存在一个共同的问题:无法支持多语言环境,这对于互联网这个开放的环境是不允许的。而目前几乎所有的电脑系统都支持基本拉丁字母,并各自支持不同的其他编码方式。Unicode为了和它们相互兼容,其首256字符保留给ISO 8859-1所定义的字符,使既有的西欧语系文字的转换不需特别考量;并且把大量相同的字符重复编到不同的字符码中去,使得旧有纷杂的编码方式得以和Unicode编码间互相直接转换,而不会丢失任何信息

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

    Unicode编码是现在大部分计算机内部使用的编码格式

    Unicode是字符集,它主要有UTF-8、UTF-16、UTF-32三种实现方式。由于UTF-8是目前主流的实现方式,UTF-16、UTF-32相对而言使用较少,所以下面就主要介绍UTF-8

    2.2.4.2 UCS

    提到Unicode可能有必要了解下,UCSUCSUniversal Character Set,通用字符集),是由ISO制定的ISO 10646(或称ISO/IEC 10646)标准所定义的标准字符集。它包括了其他所有字符集,保证了与其他字符集的双向兼容,即,如果你将任何文本字符串翻译到UCS格式,然后再翻译回原编码,你不会丢失任何信息。
    UCS不仅给每个字符分配一个代码,而且赋予了一个正式的名字。表示一个UCSUnicode值的十六进制数通常在前面加上U+,例如U+0041代表字符A

    2.2.4.3 Little endian & Big endian

    由于各个系统平台的设计不同,可能会导致某些平台对字符的理解不同(比如字节顺序的理解)。这时将会导致同意字节流可能会被解释为不同的内容。如某个字符的十六进制为4E59,拆分为4E59,在MAC上读取时是欧诺个低位开始的,那么MAC在遇到该字节流时会被解析为594E,找到的字符为,但是在Windows平台是从高字节开始读取,为4E59,找到的字符为。也就是说在Windows平台保存的“乙”跑到MAC平台上就变成了“奎”。这样势必会引起混乱,于是在Unicode编码中采用了大头(Big endian)、小头(Little endian)两种方式来进行区分。即第一个字节在前,就是大头方式,第二个字节在前就是小头方式。那么这个时候就出现了一个问题:计算机怎么知道某个文件到底是采用哪种编码方式的呢?
    Unicode规范中定义,每一个文件的最前面分别加入一个表示编码顺序的字符,这个字符的名字叫做 零宽度非换行空格ZERO WIDTH NO-BREAK SPACE),用FEFF表示。这正好是两个字节,而且FF比FE大1。
    如果一个文本文件的头两个字节是FE FF,就表示该文件采用大头方式;如果头两个字节是FF FE,就表示该文件采用小头方式。

    2.2.4.4 BOM

    既然底层存储分为了大端和小端两种模式,那么假如我们现在有一个文件,计算机又是怎么知道当前是采用的大端模式还是小端模式呢?

    BOMbyte order mark(字节顺序标记),出现在文本文件头部。BOM 就是用来标记当前文件采用的是大端模式还是小端模式存储。我想这个大家应该都见过,平常在使用记事本保存文档的时候,需要选择采用的是大端还是小端:
    [图片上传失败...(image-9ef133-1649151484181)]

    UCS 编码中有一个叫做 Zero Width No-Break Space(零宽无间断间隔)的字符,对应的编码是 FEFFFEFF 是不存在的字符,正常来说不应该出现在实际数据传输中。
    但是为了区分大端模式和小端模式,UCS 规范建议在传输字节流前,先传输字符 Zero Width No-Break Space。而且根据这个字符的顺序来区分大端模式和小端模式。

    下表就是不同编码的 BOM
    [图片上传失败...(image-87c8a7-1649151484181)]
    有了这个规范,解析文件的时候就可以知道当前编码以及其存储模式了。注意这里 UTF-8 编码比较特殊,因为本身 UTF-8 编码有特殊的顺序格式规定,所以 UTF-8 本身并没有什么大端模式和小端模式的区别.

    根据 UTF-8 本身的特殊编码格式,在没有 BOM 的情况下也能被推断出来,但是因为微软是建议都加上 BOM,所以目前存在了带 BOM 的 UTF-8 文件和不带 BOM 的 UTF-8文件,这两种格式在某些场景可能会出现不兼容的问题,所以在平常使用中也可以稍微留意这个问题

    2.2.4.5 UTF-8

    UTF-8是一种针对Unicode的可变长度字符编码,可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度。它可以用来表示Unicode标准中的任何字符,且其编码中的第一个字节仍与ASCII兼容,这使得原来处理ASCII字符的系统无须或只须做少部份修改,即可继续使用。因此,它逐渐成为电子邮件、网页及其他存储或传送文字的应用中,优先采用的编码。
    UTF-8使用一到四个字节为每个字符编码,编码规则如下:

    • 对于单字节的符号,字节的第一位设为0,后面7位为这个符号的unicode码。因此对于英语字母,UTF-8编码和ASCII码是相同的。
    • 对于n字节的符号(n>1),第一个字节的前n位都设为1,第n+1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的unicode
    Unicode UTF-8
    0000 ~007F 0XXX XXXX
    0080 ~07FF 110X XXXX 10XX XXXX
    0800 ~FFFF 1110XXXX 10XX XXXX 10XX XXXX
    1 0000 ~1F FFFF 1111 0XXX 10XX XXXX 10XX XXXX 10XX XXXX
    20 0000 ~3FF FFFF 1111 10XX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX
    400 0000 ~7FFF FFFF 1111 110X 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX 10XX XXXX

    根据上面的转换表,理解UTF-8的转换编码规则就变得非常简单了:第一个字节的第一位如果为0,则表示这个字节单独就是一个字符;如果为1,连续多少个1就表示该字符占有多少个字节。
    以汉字为例,演示如何实现UTF-8编码。
    已知的unicode是4E25100 1110 0010 0101),根据上表,可以发现4E25处在第三行的范围内(0000 0800-0000 FFFF),因此UTF-8编码需要三个字节,即格式是1110xxxx 10xxxxxx 10xxxxxx。然后,从的最后一个二进制位开始,依次从后向前填入格式中的x,多出的位补0。这样就得到了,UTF-8编码是11100100 10111000 10100101,转换成十六进制就是E4B8A5

    2.2.4.6 Unicode与UTF-8之间的转换

    通过上面的例子我们可以看到”严”的Unicode码为4E25UTF-8编码为E4B8A5,他们两者是不一样的,需要通过程序的转换来实现,在Window平台最简单的直观的方法就是记事本

    在这里插入图片描述
    在最下面的编码(E)处有四个选项:ANSI、Unicode、Unicode big endian、UTF-8
    • ANSI:记事本的默认的编码方式,对于英文文件是ASCII编码,对于简体中文文件是GB2312编码。注意:不同 ANSI 编码之间互不兼容,当信息在国际间交流时,无法将属于两种语言的文字,存储在同一段ANSI编码的文本中
    • UnicodeUCS-2编码方式,即直接用两个字节存入字符的Unicode码。该方式是小头little endian方式。
    • Unicode big endianUCS-2编码方式,大头方式。
    • UTF-8:阅读上面(UTF-8)。
      实例:在记事本中输入”严”字,依次选择ANSI、Unicode、Unicode big endian、UTF-8四种编码风格,然后另存为,使用EditPlus文本工具使用”16进制查看器”进行查看,得到如下结果:
      在这里插入图片描述
    • ANSI:两个字节D1 CF正是”严”的GB2312编码。
    • Unicode:四个字节”FF FE 25 4E”,其中FF FE表示小头存储方式,真正的编码为”25 4E”。
    • Unicode big endian:四个字节”FE FF 4E 25″,”FE FF”表示大头存储方式,真正编码为”4E 25″。
    • UTF-8:编码是六个字节EF BB BF E4 B8 A5,前三个字节EF BB BF表示这是UTF-8编码,后三个E4B8A5就是的具体编码,它的存储顺序与编码顺序是一致的。

    点击了解源码反码补码和汉字与十六进制互转

    参考文献地址:
    Unicode维基百科:http://zh.wikipedia.org/wiki/Unicode
    Unicode百度百科:http://baike.baidu.com/view/40801.html
    字符编码笔记:ASCII,Unicode和UTF-8:http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
    UTF-8百度百科:http://baike.baidu.com/view/25412.html
    编码:http://baike.baidu.com/subview/237708/11062012.html(百度百科)
    字符:http://baike.baidu.com/view/263416.html(百度百科)
    字符集:http://baike.baidu.com/view/51987.html(百度百科)
    字符编码:http://baike.baidu.com/view/1204863.html(百度百科)
    字符集和字符编码:http://www.cnblogs.com/skynet/archive/2011/05/03/2035105.html(吴秦)
    ASCII:http://baike.baidu.com/view/15482.html
    GB2312:http://baike.baidu.com/view/443268.html
    GBK:http://baike.baidu.com/view/931619.html
    GB18030:http://baike.baidu.com/view/889058.html

    相关文章

      网友评论

          本文标题:Java中文乱码详解

          本文链接:https://www.haomeiwen.com/subject/tzmxzltx.html