JAVA核心知识点--字符编码

目录

为什么要编码?

常用编码格式

ASCII码

ISO-8859-1

GB2312

GBK

UTF-16

UTF-8

在I/O操作中存在的编码

在内存操作中的编码

在Java Web中涉及的编解码

URL的编解码

HTTP Header的编解码

POST表单的编解码

HTTP BODY的编解码

在JS中的编码问题

外部引入JS文件 

JS的URL编码

 escape()

encodeURI()

encodeURIComponent()

Java与JS编解码问题

其他需要编码的地方


参考书籍:《深入分析Java Web技术内幕(修订版)》----许令波著 

编码问题一直在困扰着程序开发人员,这在Java中尤其突出,因为Java是跨平台语言,字符在不同平台之间进行传输时经常需要进行编码切换。  

为什么要编码?

众所周知,计算机其实是很笨的,它只识别数字0和1,所以,无论什么内容在计算机内最终都必须以01串的形式进行存储,字符也不例外。在计算机中存储信息的最小单元是1个字节,即8个bit,一个字节所能表示的字符个数最多为256个(0~255,二进制11111111=十进制255)。但是,人类要表示的符号太多了,用一个字节来表示显然是远远不够的,必须用更多的字节来表示,比如:用两个字节最多可以表示65535个字符,用4个字节表示的字符数可多达4294967295个。一个字符究竟应该用多少个字节来表示才合适呢?这就是编码所实现的功能,不同的编码使用不同的字节数或数值来表示字符。 

常用编码格式

ASCII码

由于计算机是美国人发明的,因此,最早只有128个字母被编码到计算机里,也就是大小写英文字母、数字和一些符号,这个编码表被称为ASCII(American Standard Code for Information Interchange,美国信息交换标准码),采用一个字节的低7位来表示,0~31是控制字符,如换行、回车、删除等,32~127是打印字符,包括那些可以通过键盘输入并且能够显示出来的字符,比如大写字母、小写字母、数字等。

ISO-8859-1

128个字符显然是不够用的,于是ISO组织在ASCII码基础上又制定了一系列标准来扩展ASCII编码,它们是ISO-8859-1至ISO-8859-15,其中ISO-8859-1涵盖了大多数西欧语言字符,所以应用得最广泛。ISO-8859-1仍然是单字节编码,它总共能表示256个字符。

GB2312

如果要处理中文的话,一个字节显然还是不够用,所以,中国制定了GB2312编码,用来把中文编进去。GB2312是双字节编码,总的编码范围是A1~F7,其中A1~A9是符号区,总共包含682个符号;B0~F7是汉字区,包含6763个汉字。

GBK

GBK全称是《汉字内码扩展规范》,是国家技术监督局为Windows 95所制定的新的汉字内码规范,它的出现是为了扩展GB2312,并加入更多的汉字。它的编码范围是8140~FEFE(去掉XX7F),总共有23940个码位,它能表示21003个汉字,它的编码是和GB2312兼容的,也就是说用GB2312编码的汉字可以用GBK来解码,并且不会有乱码。

UTF-16

说到UTF-16必须提到Unicode(Universal Code 统一码),全世界有上百种语言,日本把日文编到Shift_JIS里,韩国把韩文编到Euc-kr里,如此,假如各国都按照各国各自的标准来设计编码,在某些多语言混用的场景下,就会不可避免地出现编码冲突。为了解决这一问题,ISO提出了Unicode标准,它试图将世界上所有语言都统一到一套编码里。而实际上Unicode仅仅只是一个规范,它使用0~65535之间的数字来表示所有字符,其中0~127这128个数字表示的字符与ASCII完全一样。至于要把0~65535这些数字以怎样的形式转换为01串保存到计算机中,Unicode并未声明其具体的实现方式,于是就出现了UTF(Unicode Transformation Format),包括:UTF-16和UTF-8。

UTF-16具体定义了Unicode字符在计算机中的存取方法,它用两个字节来表示Unicode的转化格式,采用定长的表示方法,即不论什么字符都用两个字节表示,两个字节就是16个bit,所以叫UTF-16。

UTF-8

UTF-16统一采用两个字节来表示一个字符,虽然在表示上非常简单、方便,但是也有其缺点,有很大一部分字符用一个字节就可以表示的现在要用两个字节表示,存储空间放大了一倍,在现在的网络带宽还非常有限的情况下,这样会增大网络传输的流量,而且也没有必要。

UTF-8采用一种变长技术,每个编码区域有不同的字码长度。不同的字符可以由1~6个字节组成。

UTF-8有以下编码规则:

  • 如果是1个字节,最高位(第8位)为0,则表示这是1个ASCII字符(00~7F)。可见,所有ASCII编码已经是UTF-8了。
  • 如果是1个字节,以11开头,则连续的1的个数暗示这个字符的字节数,例如;110xxxxx代表它是双字节UTF-8字符的首字节。
  • 如果是1个字节,以10开始,表示它不是首字节,则需要向前查找才能得到当前字符的首字节。 

在I/O操作中存在的编码

InputStreamReader类可以将I/O操作中读取到的字节流转换为字符流,其部分源代码如下:  

package java.io;

import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import sun.nio.cs.StreamDecoder;

public class InputStreamReader extends Reader {

    private final StreamDecoder sd;

    /**
     * Creates an InputStreamReader that uses the default charset.
     *
     * @param  in   An InputStream
     */
    public InputStreamReader(InputStream in) {
	super(in);
        try {
	    sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
        } catch (UnsupportedEncodingException e) {
	    // The default encoding should always be available
	    throw new Error(e);
	}
    }

    /**
     * Creates an InputStreamReader that uses the named charset.
     *
     * @param  in
     *         An InputStream
     *
     * @param  charsetName
     *         The name of a supported
     *         {@link java.nio.charset.Charset </code>charset<code>}
     *
     * @exception  UnsupportedEncodingException
     *             If the named charset is not supported
     */
    public InputStreamReader(InputStream in, String charsetName)
        throws UnsupportedEncodingException
    {
	super(in);
	if (charsetName == null)
	    throw new NullPointerException("charsetName");
	sd = StreamDecoder.forInputStreamReader(in, this, charsetName);
    }

    /**
     * Creates an InputStreamReader that uses the given charset. </p>
     *
     * @param  in       An InputStream
     * @param  cs       A charset
     *
     * @since 1.4
     * @spec JSR-51
     */
    public InputStreamReader(InputStream in, Charset cs) {
        super(in);
	if (cs == null)
	    throw new NullPointerException("charset");
	sd = StreamDecoder.forInputStreamReader(in, this, cs);
    }

    /**
     * Creates an InputStreamReader that uses the given charset decoder.  </p>
     *
     * @param  in       An InputStream
     * @param  dec      A charset decoder
     *
     * @since 1.4
     * @spec JSR-51
     */
    public InputStreamReader(InputStream in, CharsetDecoder dec) {
        super(in);
	if (dec == null)
	    throw new NullPointerException("charset decoder");
	sd = StreamDecoder.forInputStreamReader(in, this, dec);
    }
}

InputStreamReader内部包含一个StreamDecoder实例引用,对具体字节到字符的解码实现,其实是由StreamDecoder来完成的,在StreamDecoder解码过程中必须由用户指定Charset编码格式,若用户未指定Charset,则将使用本地环境中的默认字符集,如在中文环境中将使用GBK编码。

写的情况也类似,OutputStreamWriter委托StreamEncoder将字符编码成字节。  

package java.io;

import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import sun.nio.cs.StreamEncoder;

public class OutputStreamWriter extends Writer {

    private final StreamEncoder se;

    /**
     * Creates an OutputStreamWriter that uses the named charset.
     *
     * @param  out
     *         An OutputStream
     *
     * @param  charsetName
     *         The name of a supported
     *         {@link java.nio.charset.Charset </code>charset<code>}
     *
     * @exception  UnsupportedEncodingException
     *             If the named encoding is not supported
     */
    public OutputStreamWriter(OutputStream out, String charsetName)
	throws UnsupportedEncodingException
    {
	super(out);
	if (charsetName == null)
	    throw new NullPointerException("charsetName");
	se = StreamEncoder.forOutputStreamWriter(out, this, charsetName);
    }

    /**
     * Creates an OutputStreamWriter that uses the default character encoding.
     *
     * @param  out  An OutputStream
     */
    public OutputStreamWriter(OutputStream out) {
	super(out);
	try {
	    se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
	} catch (UnsupportedEncodingException e) {
	    throw new Error(e);
        }
    }

    /**
     * Creates an OutputStreamWriter that uses the given charset. </p>
     *
     * @param  out
     *         An OutputStream
     *
     * @param  cs
     *         A charset
     *
     * @since 1.4
     * @spec JSR-51
     */
    public OutputStreamWriter(OutputStream out, Charset cs) {
	super(out);
	if (cs == null)
	    throw new NullPointerException("charset");
	se = StreamEncoder.forOutputStreamWriter(out, this, cs);
    }

    /**
     * Creates an OutputStreamWriter that uses the given charset encoder.  </p>
     *
     * @param  out
     *         An OutputStream
     *
     * @param  enc
     *         A charset encoder
     *
     * @since 1.4
     * @spec JSR-51
     */
    public OutputStreamWriter(OutputStream out, CharsetEncoder enc) {
	super(out);
	if (enc == null)
	    throw new NullPointerException("charset encoder");
	se = StreamEncoder.forOutputStreamWriter(out, this, enc);
    }
}

以下是使用InputStreamReader和OutputStreamWriter进行字节流到字符流的一个简单示例。 

public static void main(String[] args) throws IOException {

		String file = "d:/stream.txt";
		String charset = "UTF-8";
		String string = "这是要保存的中文字符";

		FileOutputStream fos = new FileOutputStream(file);
		OutputStreamWriter osw = new OutputStreamWriter(fos, charset);
		try {
			osw.write(string);

		} finally {
			osw.close();
		}

		FileInputStream fis = new FileInputStream(file);
		InputStreamReader isr = new InputStreamReader(fis, charset);
		StringBuffer sb = new StringBuffer();
		char[] buf = new char[64];
		int count = 0;
		try {
			while ((count = isr.read(buf)) != -1) {
				sb.append(buf, 0, count);
			}

		} finally {
			isr.close();
		}
		System.out.println(sb.toString());
	}

在内存操作中的编码

String类提供了转换到字节的方法,也支持将字节转换为字符串的构造函数。 

public static void main(String[] args) throws UnsupportedEncodingException {
		String s="这是一段中文字符串";
		byte[] bytes=s.getBytes("UTF-8");
		String string=new String(bytes,"UTF-8");
	}

Charset类提供encode()与decode(),分别对应char[]到byte[]的编码和byte[]到char[]的解码。

public static void main(String[] args) {
		Charset cs = Charset.forName("UTF-8");
		ByteBuffer byteBuffer = cs.encode("这是要编码的字符串");
		CharBuffer charBuffer = cs.decode(byteBuffer);
	}

ByteBuffer提供一种char和byte之间的软转换,它们之间转换不需要编码和解码,只是把一个16bit的char拆分为2个8bit的byte表示,它们的实际值并没有被修改,仅仅是数据的类型做了转换。

public static void main(String[] args) {
		ByteBuffer heapByteBuffer = ByteBuffer.allocate(1024);
		ByteBuffer buffer = heapByteBuffer.putChar('中');
		System.out.print(Integer.toBinaryString(buffer.get(0)) + " ");
		System.out.print(Integer.toBinaryString(buffer.get(1)));
	}

打印结果:

1001110 101101

编码问题(char-encoding-problem)典型示例:

public class EncodeTest {
	static String toHexString(byte[] bytes) {
		StringBuilder sb = new StringBuilder("");
		if (bytes == null || bytes.length == 0) {
			return null;
		}
		for (int i = 0; i < bytes.length; i++) {
			int v = bytes[i] & 0xFF;

			String hv = Integer.toHexString(v);
			sb.append(hv + " ");
		}
		return sb.toString();
	}

	static String toHexString(char[] chars) {
		StringBuilder sb = new StringBuilder("");
		if (chars == null || chars.length == 0) {
			return null;
		}
		for (int i = 0; i < chars.length; i++) {
			String hv = Integer.toHexString((int) chars[i]);
			sb.append(hv + " ");
		}
		return sb.toString();
	}

	public static void main(String[] args) {
		String string = "I am 李";
		// Unicode十进制数值为:    73       32       97       109      32       26446
		// Unicode十六进制字符串:  49       20       61       6d       20       674e
		// Unicode二进字符串:      01001001 00100000 01100001 01101101 00100000 0110011101001110
		try {
			byte[] iso8859 = string.getBytes("ISO-8859-1");
			byte[] gb2312 = string.getBytes("GB2312");
			byte[] gbk = string.getBytes("GBK");
			byte[] utf16 = string.getBytes("UTF-16");
			byte[] utf8 = string.getBytes("UTF-8");

			System.out.println(toHexString(string.toCharArray()));
			// 输出结果:49 20 61 6d 20 674e
			
			/**
			 * ISO-8859-1编码会将不支持的字符编码为3f,即"?"字符。
			 */
			System.out.println(toHexString(iso8859));
			// 输出结果:49 20 61 6d 20 3f
			
			/**
			 * GB2312字符集有一个从char到byte的码表,不同的字符编码就是从这个码表找到与每个字符对应的字节,然后拼装成byte数组。
			 */
			System.out.println(toHexString(gb2312));
			// 输出结果:49 20 61 6d 20 c0 ee
			
			/**
			 * GBK编码兼容GB2312编码,且GBK包含的汉字字符更多。
			 */
			System.out.println(toHexString(gbk));
			// 输出结果:49 20 61 6d 20 c0 ee
			
			/**
			 * UTF-16仅将字符的高位与低位进行拆分变成两个字节,特点是编码效率非常高,规则很简单
			 * 前面用两个字节来保存BYTE_ORDER_MARK值,用来区分是高位字节在前,或者低位字节在前。
			 */
			System.out.println(toHexString(utf16));
			// 输出结果:fe ff 0 49 0 20 0 61 0 6d 0 20 67 4e 
			
			/**
			 * UTF-8编码也不用查表,效率很高,变长存储节省空间。
			 */
			System.out.println(toHexString(utf8));
			// 输出结果:49 20 61 6d 20 e6 9d 8e 
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
}

在Java Web中涉及的编解码

URL的编解码

用户提交一个URL,在这个URL中可能存在中文,因此需要编码。

Apache Tomcat对URL的URI部分进行解码的字符集是在Connector的<Connector URIEncoding="UTF-8"/>中定义的,如果没有定义,那么将以默认编码ISO-8859-1解析。所以有中文URL时最好把URIEncoding设置成UTF-8编码。

CATALINA_HOME\conf\server.xml中修改Connector 配置如下:

<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8483"  URIEncoding="UTF-8"/>

URL中以Get方式请求的QueryString的解码是在request.getParameter()方法第一次被调用时进行的,解码字符集要么是Header中ContentType定义的Charset,要么是默认的ISO-8859-1,要使用ContentType中定义的编码,就要将Connector的<Connector  URIEncoding="UTF-8" useBodyEncodingForURI="true"/>中的useBodyEncodingForURI设置为true,对QueryString使用BodyEncoding解码。

CATALINA_HOME\conf\server.xml中修改Connector 配置如下:  

<Connector port="8080" protocol="HTTP/1.1"
               connectionTimeout="20000"
               redirectPort="8483"  URIEncoding="UTF-8" useBodyEncodingForURI="true"/>

HTTP Header的编解码

当客户端发起一个HTTP请求时,在Header中可能会传递其他参数,如Cookie、redirectPath等,对于这些参数Tomcat默认使用ISO-8859-1解码,并且不能设置成其他的解码格式。因此,如果你设置的Header中含有非ASCII字符,解码中肯定会有乱码。一个简单的解决办法:可以先将这些字符用org.apache.catalina.util.URLEncoder编码,再添加到Header中,使用这些项时再按照相应的字符集解码即可。

POST表单的编解码

POST表单提交的参数的解码也是在request.getParameter()方法第一次被调用时发生的,POST表单的参数传递方法与QueryString不同,它是通过HTTP的BODY传递到服务端的。当我们在页面上单击提交按钮时浏览器首先将根据ContentType的Charset编码格式对在表单中填入的参数进行编码,然后提交到服务器端,在服务器端同样也是用ContentType中的字符集进行解码的。所以通过POST表单提交的参数一般不会出现问题,而且这个字符集编码是我们自己设置的,可以通过request.setCharacterEncoding(charset)来设置。

注意:要在第一次调用request.getParameter()方法之前就设置request.setCharacterEncoding(charset),否则POST表单提交上来的数据可能出现乱码。

HTTP BODY的编解码

当用户请求的资源已经成功获取后,这些内容将通过Response返回给客户端浏览器,这个过程要先经过编码,再到浏览器进行解码。编解码字符集可以通过response.setCharacterEncoding来设置,它将会覆盖request.getCharacterencoding的值,并且通过Header的Content-Type返回客户端,浏览器接收到返回的Socket流时将通过Content-Type的charset来解码。如果返回的HTTP Header中Content-Type没有设置charset,那么浏览器将根据HTML的<meta HTTP-equiv="Content-Type" content="text/html; charset=GBK" />中的charset来解码。如果也没有定义,那么浏览器将使用默认的编码来解码。

访问数据库都是通过客户端JDBC驱动来完成的,用JDBC来存取数据时要和数据的内置编码保持一致,可以通过设置JDBC URL来指定,如MYSQL:url=“jdbc:mysql://localhost:3306/DB?useUnicode=true&characterEncoding=GBK”。

在JS中的编码问题

外部引入JS文件 

在一个单独的JS文件中包含字符串输入 的情况,如:

<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<script src="script.js" charset="gbk"></script>
</head>
</html>

如果引入的script.js脚本中有如下代码:

document.write("这是一段中文");

这时如果script没有设置cahrset,浏览器就会以当前这个页面的默认字符集解析这个JS文件。当script.js文件与当前页面的编码格式不一致时,就会出现乱码。

JS的URL编码

在JS中处理URL编码的函数有三个:escape()、encodeURI()和encodeURIComponent()。 

 escape()

这个函数是将ASCII字母、数字、标点符号(* + - . / @ _)之外的其他字符转化成Unicode编码值,并且在编码值前加上“%u”,通过unescape()函数解码,如图所示。  

注意:escape()和unescape()已经从ECMAScript V3 标准中删除了,URL的编码可以用encodeURI和encodeURIComponent来代替。

encodeURI()

与escap()相比,encodeURI()是真正的JS用来对URL编码的函数,它可以将整个URL中的字符(一些特殊字符除外,如:! # $ & ' ( ) * + , -  . / : ; = ? @ _ ~ 0-9 a-z A-Z)进行UTF-8编码,在每个码值前加上“%”,解码通过decodeURI()函数,如图所示。 

encodeURIComponent()

encodeURIComponent()这个函数比encodeURI()编码还要彻底,它除了对 ! ' ( ) * - . _ ~ 0-9 a-z A-Z这几个字符不编码,对其他所有字符都编码。这个函数通常用于将一个URL当作一个参数放在另一个URL中,解码通过decodeURIComponent()函数,如图所示。 

Java与JS编解码问题

在Java端处理URL编解码有两个类,分别是 java.net.URLEncoder和java.net.URLDecoder。这两个类可以将所有“%”加UTF-8码值用UFT-8解码,从而得到原始字符。Java端的URLEncoder和URLDecoder与前端JS对应的是encodeURIComponent和decodeURIComponent。

注意:可以用encodeURIComponent两次编码,如encodeURIComponent(encodeURIComponent(str)),这样在Java端通过request.getParameter()用GBK解码后取得的就是UTF-8编码的字符串,如果Java端需要使用这个字符串,则再用UTF-8解码一次;如果是将这个结果直接通过JS输出到前端,那么这个UTF-8字符串可以直接在前端正常显示。  

其他需要编码的地方

XML文件可以通过设置头来指定编码格式:  

<?xml version="1.0" encoding="UTF-8"?>

Velocity模板设置编码的格式如下:  

services.VelocityService.input.encoding=UTF-8

JSP设置编码的格式如下:  

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

 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
很抱歉,作为文本AI助手,我无法绘制思维导图。但是,我可以为您提供一个简要的java IO流知识点总结。 Java IO流是Java中用于处理输入和输出的机制。它提供了一种有效的方式来读取和写入数据,以及与文件、网络和其他设备进行交互。 Java IO流包括字节流和字符流两种类型。字节流以字节为单位进行操作,适用于处理二进制数据。而字符流以字符为单位进行操作,适用于处理文本数据。 常用的字节流包括InputStream和OutputStream类,用于读取和写入字节数据。常用的字符流包括Reader和Writer类,用于读取和写入字符数据。可以通过使用字节流和字符流的组合来实现不同类型数据的读写操作。 在Java IO流中,还有一些特殊的流,如缓冲流、转换流、对象流等。缓冲流提供了缓冲区来提高IO性能。转换流用于处理字符编码和解码。对象流用于对Java对象进行读写操作。 此外,Java IO流还包括一些常用的类和方法,如File类用于处理文件和目录,RandomAccessFile类用于对文件进行随机访问,FileInputStream和FileOutputStream类用于读写文件等。 通过组合和使用不同类型的流和类,您可以实现各种复杂的IO操作,如读写文件、网络通信、序列化对象等。 希望这个简要总结对您有所帮助。如果您有任何更具体的问题,请随时提问。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [万字长文+思维导图帮你梳理 Java IO 流,还学不会你来打我(值得收藏)](https://blog.csdn.net/a1405/article/details/116766237)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值