https://www.codenong.com/cs106124420/ p.s.文章Java环境是基于windows平台上的,所有程序建议在命令行验证,原因无它:Eclipse或IDEA在编译或运行时,会默认增加编译、运行时参数,会影响代码效果。最后文章基于本人理解,如有差错或者不足之处,欢迎指正!
一、Java程序乱码踩坑
大家写java程序,肯定或多或少有一些关于字符编码方面的问题,尤其是乱码,今天我们就一个由浅入深,来解析一些我们编写Java程序常遇到的坑和要注意的知识点。
首先,相信大家编写Java程序,都是写完源文件之后,编译运行,这年头加上IDE一些的智能化,相信大家也很少了解Java程序在编译运行过程中到底有些什么内容是跟字符编码有关。在这里,我一个简单的程序来举例。
class TestOne {
public static void main(String[] args) {
System.out.println("hello world");
System.out.println("你好世界");
}
}
大家写完直接用IDE编译运行一套连招下来,结果哗哗的出现在IDE的控制台界面,完事,这是一种情况,但是有人坚信没有自己手动编译和运行的程序不完整,不用记事本写程序的程序员不是好程序员,这种写完之后在cmd环境中用javac和java命令编译整个源文件然后运行,然后结果傻了。
明明网上的Java入门教程都是说一套javac和java命令组合下来,然后就是程序完美呈现,虽然这里确实没有任何“毛病”,但是这很明显乱码了,然后网上找回答,说是源文件编码问题,改成Windows平台默认的就好了或者指定编译时的javac的编码参数。根据这些看了一下自己的源文件字符编码格式是UTF-8。
二、Java编译相关知识点
问题是知道了这些,也不明白上面的那个解释,这里就借用RednaxelaFX大神的解释和一张图来说明:
从Java源码文件到Java Class文件,中间会经过Java源码编译器(例如javac或ECJ)的编译。也就是说,是Java源码编译器负责将Java源码文件的编码转换为最终的UTF-8。导致乱码的不是Java源码编译器的“编码”(写出UTF-8)的过程,而是“解码”(读入Java源码内容)的过程。以javac为例,它有一个参数可以指定输入的Java源码文件的编码:
关键在于“如果不指定encoding,则使用平台默认的转换器”。在简体中文的Windows上,平台默认编码会是GBK,那么javac就会默认假定输入的Java源码文件是以GBK编码的。javac能够正确读取文件内容并将其中的字符串以UTF-8输出到Class文件里,就跟自己写个程序以GBK读文件以UTF-8写文件一样。如果实际输入的确实是GBK编码(GBK兼容ASCII编码)的文件,那么一切都会正常。但如果实际输入的是别的编码的文件,例如超过了ASCII范围的UTF-8,那javac读进来的内容就会出问题,就“乱码”了。
另外为了大家更好的理解这里,我们附带下面这样一些知识:
内码 :简单来说,某种语言运行时,其char和string在内存中的编码方式。
外码 :除了内码,皆是外码。
要注意的是,源代码编译产生的目标代码文件(可执行文件或class文件)中的编码方式属于外码。
先看一下内码:
JVM中内码采用UTF16。早期,UTF16采用固定长度2字节的方式编码,两个字节可以表示65536种符号(其实真正能表示要比这个少),足以表示当时unicode中所有字符。但是随着unicode中字符的增加,2个字节无法表示所有的字符,UTF16采用了2字节或4字节的方式来完成编码。Java为应对这种情况,考虑到向前兼容的要求,Java用一对char来表示那些需要4字节的字符。所以大家这里也应该理解为什么java中的char是占用两个字节,只不过有些字符需要两个char来表示。
详细了解:
https://docs.oracle.com/javase/tutorial/i18n/text/unicode.html http://www.zhihu.com/question/27562173
外码:
由上面概念可以知道:Java的源文件和class文件编码方式都属于外码,另外Java源文件是任意字符编码的,这个是可以由我们指定的,就像上面那个记事本的UTF-8,GBK编码等等,而Java的class文件里存储的字符串是UTF-8编码的。(具体是一种modified UTF-8,请参考JVM规范:Chapter 4. The class File Format
看完这些,想必也知道对于这种乱码问题怎么解决,就是两方法:
- 改变 .java文件编码格式为Windows上平台默认编码GBK
- 指定javac编译命令参数: javac -encoding UTF-8 xx.java
方法一:其实这里javac TestOne.java在Windows默认情况下就是java -encoding GBK TestOne.java
一般弄到这里,大部分关于编译时的乱码问题基本上大家都清楚了,当然有些甚至javac编译都通不过,如下:
这些报错很好解释,就是编译.java源文件时,编译器编码参数设置和.java源文件不一样产生的,修改为统一格式就行。这类错误也经常出现在跨平台上:
三、深入理解 jvm的 file.encoding 参数
上面我们讲的主要是编译时指定参数不当不当可能导致程序乱码的产生,前面我们提到了一张图,其实那种图只是java源文件编译然后加载进内存(JVM)运行的过程,至于,JVM中还有一些关于字符编码操作的没有列出来,如IO操作的等等,这些具体就是指Java程序运行过程中存在的一些问题。
重点:下面我讲详细介绍这个file.encoding,网上现在关于这个真的是太杂了,而且都没怎么解释的清楚。它这个是 jvm 的启动参数之一,它是设置用于Java程序的文件的编码格式,它与JVM用到的UTF-16,以及java源文件用到的不同编码格式不一样。
可能看到上面都不清楚上面意思,我们通过Java源码来理解:
查找 java 源码,只有四个类调用了 file.encoding 这个属性,分别是:
在java.nio.Charset.defaultCharset() 中
java.net.URLEncoder的静态构造方法, 影响到的方法 java.net.URLEncoder.encode(String):这是Web环境中最常遇到的编码使用
com.sun.org.apache.xml.internal.serializer.Encoding的getMimeEncoding方法: 影响对无编码设置的xml文件的读取
javax.print.DocFlavor类的静态构造方法:影响打印的编码
这个后面三个都好理解,我们着重讲一下前面的第一个,在java.nio.Charset.defaultCharset()源码中:
/**
* Returns the default charset of this Java virtual machine.
*
* <p> The default charset is determined during virtual-machine startup and
* typically depends upon the locale and charset of the underlying
* operating system.
*
* @return A charset object for the default charset
*
* @since 1.5
*/
public static Charset defaultCharset() {
if (defaultCharset == null) {
synchronized (Charset.class) {
String csn = AccessController.doPrivileged(
new GetPropertyAction("file.encoding"));
Charset cs = lookup(csn);
if (cs != null)
defaultCharset = cs;
else
defaultCharset = forName("UTF-8");
}
}
return defaultCharset;
}