最近在写一些JNI接口的时候,碰到了一系列的编码转换问题,进行了一番总结,总算是彻底搞懂了。
在JAVA中处理字符串编码,我们可以理解为JVM里面的形态和外面的形态2种。比如:
首先,我们来探讨下编译期的编码问题:
- public class Main
- {
- String testStr = "你好";
- }
假设该java文件是GBK编码的,我们用如下命令:
javac -encoding GBK Main.java
假设该java文件是UTF-8编码的,我们用如下命令:
javac -encoding UTF-8 Main.java
虽然2个Main.java文件编码不一样,但是编译好的class 2进制文件,是一模一样的。那么是不是编译成UNICODE编码了呢?我看了下2进制文件里面的“你好”并非UNICODE编码,估计是处于安全考虑,进行了一些转换。不过,有一点我们可以总结了:N份源文件有着不同的编码格式,它们编译出来的class文件,都是统一的。至于中文被编译成什么编码格式了,我们不得而知,但可以猜测是UNICODE的变型。
上面的str变量就是字符串的内在形态,”你好”就是外在形态。str对象的任何操作都是基于内在形态的,我们把外形形态的字符串放入String类操作的时候,就要负责编码的转换。你可以看出上面这一行代码的编码转换吗?其实是有的:这里的”你好”是GBK编码的,因为它是Main.java里面的文字,而Main.java是GBK的;但是str的内部编码可不是GBK哦,所以这里JVM已经进行了转码。
上面是将中文写在代码里面,如果从文件中读入中文呢,我们是将再来看个例子:
- BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
- System.out.println(reader.readLine());
上面这个例子是从文件test.txt中读入一行打印,会不会出现乱码呢?我们来看一些情况:
注:下面4个试验,是用Eclipse来完成的
1.test.txt是UTF-8的,代码文件是GBK的(乱码)
2.test.txt是GBK的,代码文件是UTF-8的(乱码)
3.test.txt是UTF-8的,代码文件是UTF-8的(正常)
4.test.txt是GBK的,代码文件是GBK的(正常)
先说,为何乱码,道理非常简单,因为没有人告诉FileReader以什么编码去读test.txt。那么FileReader到底是用什么编码在读test.txt呢?
在Eclipse中,Eclipse会根据源码的编码格式来读,所以只要源码即:Main.java和test.txt编码一致即可。
但是在,命令行中,是根据操作系统的默认编码来读的,感谢索尔的提醒。
那么,我们为了不乱码,就需要告诉JAVA,外面文件的编码格式是什么,让JVM用正确的解码方法去解码字符串。于是,代码改为:
- BufferedReader reader = new BufferedReader(new InputStreamReader(
- new FileInputStream("test.txt"), "UTF-8"));
- System.out.println(reader.readLine());
这里的UTF-8就是test.txt的编码格式。程序运行正常。
上面2个例子,一个是通过代码里面写中文,一个是通过读取文件获得中文,最终这些中文在JVM的运行时,是以什么形式储存在内存中的呢?答案是:UNICODE,2个字节。
现在我们考虑一个复杂的场景:我们这个系统是一个中间站,需要把A模块的GBK字符串送到B模块处理,B模块是用UTF-8处理这些字符串的,当B模块处理完后,我们接收这些字符串,然后转发给A模块。这里就有着几个转码的步骤:
1.先从A模块取下字符串,我们告诉String,getBytesFromA()返回的是GBK编码的字节序,执行完后,所有得到的字节被解析为GBK编码,再转码成UNICODE编码,存储在strFromA对象中
String strFromA = new String(getBytesFromA(), “GBK”);
2.将strFromA从UNICODE转码成 UTF-8 ,然后传入sendBytesToB
sendBytesToB(strFromA .getBytes(”UTF-8″));
3.B模块执行完后,我们用getBytesFromB()从B模块取回字符串,告诉String用UTF-8来读取这些字节,同样所有得到的字节被解析为UTF-8编码,再转码成UNICODE编码,存储在strFromB对象中
String strFromB = new String(getBytesFromB(), “UTF-8″);
4.将strFromB从UNICODE转码成 GBK ,然后传入sendBytesToA
sendBytesToA(strFromB .getBytes(”GBK”));
总结如下:
1.JAVA在运行时,所有的编码都为UNICODE,存在char类型中,2个字节。如果遇到3个字节的UNICODE,用2个char存
2.回想,一个程序在运行期,内存里面的字符串从哪里来?2个途径,1.写在源码里面;2.运行期从文件或者网络等读取。对于1,我们需要在编译期告诉javac encoding是什么;对于2,我们需要告诉String或者FileReader等,读进来的字符串是何编码。一旦进入内存,将全部转为UNICODE
3.在运行期的字符串,可以通过getBytes方法来转化任意编码,如果不指定,那么就是用默认的编码(Eclipse的默认编码是源文件的编码,命令行的默认编码是操作系统的编码)。比如,一个字符串要保存到文件中,如果要存成UTF-8,最好在witer中告诉JAVA,你要保存为UTF-8。
4.最后总结,大型项目,切记编码问题,检视所有的外部来源是否都被指定编码格式了
java基础
下面代码的大致思路是:传入一个socketServer得到的socket的,然后再建立一条到其他代理服务器的socket的,接着就是按照顺序来回倒数据,只要读入的数据不是-1,那么就有可能再读到数据,这2条通路的任何一条通路读入数据为-1,那么就结束。其次还有退出循环的可能就是,在读数据的时候发生异常,有2种情况会产生这种异常,要么浏览器认为没有数据可以读了,要么代理服务器那里认为没有数据可以写过来了,那么它们就会close它们各自的socket,此时我们再尝试读socket,就会报错:socket已经被断开。
- import java.io.IOException;
- import java.net.InetSocketAddress;
- import java.nio.ByteBuffer;
- import java.nio.channels.SocketChannel;
- import java.nio.charset.Charset;
-
- /**
- * 提供一种非阻塞方式的网络连接,性能提高
- */
- public class SocketNioHandler {
-
- private InetSocketAddress endpoint;
-
- public SocketNioHandler() {
- }
-
- public SocketNioHandler(InetSocketAddress endpoint) {
- this.endpoint = endpoint;
- }
-
- /**
- * 此方法是线程安全的
- */
- @Override
- public boolean process(SocketChannel socket) {
- SocketChannel outSocket = null;
- ByteBuffer buff = ByteBuffer.allocate(1024);
-
- try {
- outSocket = SocketChannel.open(endpoint);
- outSocket.configureBlocking(false);
-
- while (true) {
-
- if (socket.read(buff) < 0) {
- break;
- } else {
- buff.flip();
- if (buff.limit() > 10)
- System.out.println(Charset.defaultCharset()
- .decode(buff));
- buff.rewind();
- while (buff.remaining() != 0) {
- outSocket.write(buff);
- }
- buff.clear();
- }
-
- if (outSocket.read(buff) < 0) {
- break;
- } else {
- buff.flip();
- while (buff.remaining() != 0) {
- socket.write(buff);
- }
- buff.clear();
- }
-
- try {
- Thread.sleep(1);
- } catch (InterruptedException e) {
- ;
- }
- }
-
- } catch (IOException e) {
- ;
- } finally {
- try {
- socket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- try {
- outSocket.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
-
- }
-
- return true;
- }
-
- }
java基础
在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到内存,等要用了,再还原到对象中,说白了,就是能将一个2进制文件变成内存中的对象。在JAVA中,要实现这种机制,只要实现Serializable接口就可以了,先看下面这个简单例子,serialVersionUID稍后引出。我们先定义一个简单的Person类,然后创建这个对象,最后序列化它到一个文件。
- import java.io.Serializable;
-
- public class Person implements Serializable {
-
- private String name;
-
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
- import java.io.FileInputStream;
- import java.io.FileOutputStream;
- import java.io.ObjectInputStream;
- import java.io.ObjectOutputStream;
-
- public class WhySerialversionUID {
-
- public static void main(String[] args) throws Exception {
-
- //这里是把对象序列化到文件
- Person crab = new Person();
- crab.setName("Mr.Crab");
-
- ObjectOutputStream oo = new ObjectOutputStream
- (new FileOutputStream("crab_file"));
- oo.writeObject(crab);
- oo.close();
-
- //这里是把对象序列化到文件,我们先注释掉,一会儿用
- //ObjectInputStream oi = new ObjectInputStream
- // (new FileInputStream("crab_file"));
- //Person crab_back = (Person) oi.readObject();
- //System.out.println("Hi, My name is " + crab_back.getName());
- //oi.close();
-
- }
- }
运行完后,我们发现有了一个crab_file文件,这个文件就保存这crab对象在内存中的形态。同样,我们把这部分代码注释掉,运行下面那段还原代码,发现,crab_file文件可以被转化为一个对象。
一切都那么顺利,但是如果在序列化之后,Person这个类发生了改变呢?比如,多了一个成员变量。我们做如下试验,还是先将对象序列化到一个文件中,之后在Person这个类中添加一个成员变量,如下:
- import java.io.Serializable;
-
- public class Person implements Serializable {
-
- private String name;
- //添加这么一个成员变量
- private String address;
-
- public String getName() {
- return name;
- }
- public void setName(String name) {
- this.name = name;
- }
- }
之后,我们再去运行一下还原,就发现运行出错了,会报如下错误:
Exception in thread “main” java.io.InvalidClassException: Person; local class incompatible: stream classdesc serialVersionUID = 8383901821872620925, local class serialVersionUID = -763618247875550322
意思就是说,文件流中的class和classpath中的class,也就是修改过后的class,不兼容了,处于安全机制考虑,程序抛出了错误,并且拒绝载入。那么如果我们真的有需求要在序列化后添加一个字段或者方法呢?应该怎么办?那就是自己去指定serialVersionUID。之前,在我们的例子中,我们是没有指定serialVersionUID的,那么java编译器会自动给这个class进行一个摘要算法,类似于指纹算法,只要这个文件多一个空格,得到的UID就会截然不同的,可以保证在这么多类中,这个编号是唯一的。所以,我们添加了一个字段后,由于没有显指定serialVersionUID,编译器又为我们生成了一个UID,当然和前面保存在文件中的那个不会一样了,于是就出现了2个号码不一致的错误。因此,只要我们自己指定了serialVersionUID,就可以在序列化后,去添加一个字段,或者方法,而不会影响到后期的还原,还原后的对象照样可以使用,而且还多了方法可以用,呵呵。但是serialVersionUID我们怎么去生成呢?你可以写1,也可以写2,都无所谓,但是最好还是按照摘要算法,生成一个惟一的指纹数字,eclipse可以自动生成的,jdk也自带了这个工具。一般写法类似于
private static final long serialVersionUID = -763618247875550322L;
java基础