存档

‘java基础’ 分类的存档

彻底理解JAVA的编码转换问题

2009年9月16日

最近在写一些JNI接口的时候,碰到了一系列的编码转换问题,进行了一番总结,总算是彻底搞懂了。

在JAVA中处理字符串编码,我们可以理解为JVM里面的形态和外面的形态2种。比如:

首先,我们来探讨下编译期的编码问题:

  1. public class Main 
  2. {
  3.     String testStr = "你好";
  4. }

假设该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已经进行了转码。

上面是将中文写在代码里面,如果从文件中读入中文呢,我们是将再来看个例子:

  1. BufferedReader reader = new BufferedReader(new FileReader("test.txt"));
  2. 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用正确的解码方法去解码字符串。于是,代码改为:

  1. BufferedReader reader = new BufferedReader(new InputStreamReader(
  2.                 new FileInputStream("test.txt"), "UTF-8"));
  3. 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基础

打印GB2312编码中的所有汉字

2009年6月30日
  1. for(int j=0xB0; j<0xF8; j++)
  2.         {
  3.             for(int i=0xA1;i<0xFF;i++)
  4.             {
  5.                 byte[] bytes = new byte[2];
  6.                 bytes[0] = (byte)j;
  7.                 bytes[1] = (byte)i;
  8.                 System.out.println(new String(bytes));
  9.             }
  10.         }

java基础

利用NIO非阻塞方式制作浏览器代理

2009年6月28日

下面代码的大致思路是:传入一个socketServer得到的socket的,然后再建立一条到其他代理服务器的socket的,接着就是按照顺序来回倒数据,只要读入的数据不是-1,那么就有可能再读到数据,这2条通路的任何一条通路读入数据为-1,那么就结束。其次还有退出循环的可能就是,在读数据的时候发生异常,有2种情况会产生这种异常,要么浏览器认为没有数据可以读了,要么代理服务器那里认为没有数据可以写过来了,那么它们就会close它们各自的socket,此时我们再尝试读socket,就会报错:socket已经被断开。

  1. import java.io.IOException;
  2. import java.net.InetSocketAddress;
  3. import java.nio.ByteBuffer;
  4. import java.nio.channels.SocketChannel;
  5. import java.nio.charset.Charset;
  6.  
  7. /**
  8. * 提供一种非阻塞方式的网络连接,性能提高
  9. */
  10. public class SocketNioHandler {
  11.  
  12.     private InetSocketAddress endpoint;
  13.  
  14.     public SocketNioHandler() {
  15.     }
  16.  
  17.     public SocketNioHandler(InetSocketAddress endpoint) {
  18.         this.endpoint = endpoint;
  19.     }
  20.  
  21.     /**
  22.      * 此方法是线程安全的
  23.      */
  24.     @Override
  25.     public boolean process(SocketChannel socket) {
  26.         SocketChannel outSocket = null;
  27.         ByteBuffer buff = ByteBuffer.allocate(1024);
  28.  
  29.         try {
  30.             outSocket = SocketChannel.open(endpoint);
  31.             outSocket.configureBlocking(false);
  32.  
  33.             while (true) {
  34.  
  35.                 if (socket.read(buff) < 0) {
  36.                     break;
  37.                 } else {
  38.                     buff.flip();
  39.                     if (buff.limit() > 10)
  40.                         System.out.println(Charset.defaultCharset()
  41.                                 .decode(buff));
  42.                     buff.rewind();
  43.                     while (buff.remaining() != 0) {
  44.                         outSocket.write(buff);
  45.                     }
  46.                     buff.clear();
  47.                 }
  48.  
  49.                 if (outSocket.read(buff) < 0) {
  50.                     break;
  51.                 } else {
  52.                     buff.flip();
  53.                     while (buff.remaining() != 0) {
  54.                         socket.write(buff);
  55.                     }
  56.                     buff.clear();
  57.                 }
  58.  
  59.                 try {
  60.                     Thread.sleep(1);
  61.                 } catch (InterruptedException e) {
  62.                     ;
  63.                 }
  64.             }
  65.  
  66.         } catch (IOException e) {
  67.             ;
  68.         } finally {
  69.             try {
  70.                 socket.close();
  71.             } catch (IOException e) {
  72.                 e.printStackTrace();
  73.             }
  74.             try {
  75.                 outSocket.close();
  76.             } catch (IOException e) {
  77.                 e.printStackTrace();
  78.             }
  79.  
  80.         }
  81.  
  82.         return true;
  83.     }
  84.  
  85. }

java基础

深入研究ReentrantLock(重入锁)之引出话题篇

2009年5月30日

一直以来都想好好研究下ReentrantLock,她的独到魅力令我屡试不爽,无奈网上实在是没有太多的资料可以参考,于是自己开始深入研究它的内部实现机制,经过数天的研究,终于有点心得体会升华了,记录之……

synchronized原语和ReentrantLock在一般情况下没有什么区别,但是在非常复杂的同步应用中,请考虑使用ReentrantLock,特别是遇到下面2种需求的时候。
1.某个线程在等待一个锁的控制权的这段时间需要中断
2.需要分开处理一些wait-notify,ReentrantLock里面的Condition应用,能够控制notify哪个线程
3.具有公平锁功能,每个到来的线程都将排队等候
下面细细道来……

先说第一种情况,ReentrantLock的lock机制有2种,忽略中断锁和响应中断锁,这给我们带来了很大的灵活性。比如:如果A、B2个线程去竞争锁,A线程得到了锁,B线程等待,但是A线程这个时候实在有太多事情要处理,就是一直不返回,B线程可能就会等不及了,想中断自己,不再等待这个锁了,转而处理其他事情。这个时候ReentrantLock就提供了2种机制,第一,B线程中断自己(或者别的线程中断它),但是ReentrantLock不去响应,继续让B线程等待,你再怎么中断,我全当耳边风(synchronized原语就是如此);第二,B线程中断自己(或者别的线程中断它),ReentrantLock处理了这个中断,并且不再等待这个锁的到来,完全放弃。(如果你没有了解java的中断机制,请参考下相关资料,再回头看这篇文章,80%的人根本没有真正理解什么是java的中断,呵呵)

这里来做个试验,首先搞一个Buffer类,它有读操作和写操作,为了不读到脏数据,写和读都需要加锁,我们先用synchronized原语来加锁,如下:

  1. public class Buffer {
  2.  
  3.     private Object lock;
  4.  
  5.     public Buffer() {
  6.         lock = this;
  7.     }
  8.  
  9.     public void write() {
  10.         synchronized (lock) {
  11.             long startTime = System.currentTimeMillis();
  12.             System.out.println("开始往这个buff写入数据…");
  13.             for (;;)// 模拟要处理很长时间
  14.             {
  15.                 if (System.currentTimeMillis()
  16.                         - startTime > Integer.MAX_VALUE)
  17.                     break;
  18.             }
  19.             System.out.println("终于写完了");
  20.         }
  21.     }
  22.  
  23.     public void read() {
  24.         synchronized (lock) {
  25.             System.out.println("从这个buff读数据");
  26.         }
  27.     }
  28. }

接着,我们来定义2个线程,一个线程去写,一个线程去读。

  1. public class Writer extends Thread {
  2.  
  3.     private Buffer buff;
  4.  
  5.     public Writer(Buffer buff) {
  6.         this.buff = buff;
  7.     }
  8.  
  9.     @Override
  10.     public void run() {
  11.         buff.write();
  12.     }
  13.  
  14. }
  15.  
  16. public class Reader extends Thread {
  17.  
  18.     private Buffer buff;
  19.  
  20.     public Reader(Buffer buff) {
  21.         this.buff = buff;
  22.     }
  23.  
  24.     @Override
  25.     public void run() {
  26.  
  27.         buff.read();//这里估计会一直阻塞
  28.  
  29.         System.out.println("读结束");
  30.  
  31.     }
  32.  
  33. }

好了,写一个Main来试验下,我们有意先去“写”,然后让“读”等待,“写”的时间是无穷的,就看“读”能不能放弃了。

  1. public class Test {
  2.     public static void main(String[] args) {
  3.         Buffer buff = new Buffer();
  4.  
  5.         final Writer writer = new Writer(buff);
  6.         final Reader reader = new Reader(buff);
  7.  
  8.         writer.start();
  9.         reader.start();
  10.  
  11.         new Thread(new Runnable() {
  12.  
  13.             @Override
  14.             public void run() {
  15.                 long start = System.currentTimeMillis();
  16.                 for (;;) {
  17.                     //等5秒钟去中断读
  18.                     if (System.currentTimeMillis()
  19.                             - start > 5000) {
  20.                         System.out.println("不等了,尝试中断");
  21.                         reader.interrupt();
  22.                         break;
  23.                     }
  24.  
  25.                 }
  26.  
  27.             }
  28.         }).start();
  29.  
  30.     }
  31. }

我们期待“读”这个线程能退出等待锁,可是事与愿违,一旦读这个线程发现自己得不到锁,就一直开始等待了,就算它等死,也得不到锁,因为写线程要21亿秒才能完成 T_T ,即使我们中断它,它都不来响应下,看来真的要等死了。这个时候,ReentrantLock给了一种机制让我们来响应中断,让“读”能伸能屈,勇敢放弃对这个锁的等待。我们来改写Buffer这个类,就叫BufferInterruptibly吧,可中断缓存。

  1. import java.util.concurrent.locks.ReentrantLock;
  2.  
  3. public class BufferInterruptibly {
  4.  
  5.     private ReentrantLock lock = new ReentrantLock();
  6.  
  7.     public void write() {
  8.         lock.lock();
  9.         try {
  10.             long startTime = System.currentTimeMillis();
  11.             System.out.println("开始往这个buff写入数据…");
  12.             for (;;)// 模拟要处理很长时间
  13.             {
  14.                 if (System.currentTimeMillis()
  15.                         - startTime > Integer.MAX_VALUE)
  16.                     break;
  17.             }
  18.             System.out.println("终于写完了");
  19.         } finally {
  20.             lock.unlock();
  21.         }
  22.     }
  23.  
  24.     public void read() throws InterruptedException {
  25.         lock.lockInterruptibly();// 注意这里,可以响应中断
  26.         try {
  27.             System.out.println("从这个buff读数据");
  28.         } finally {
  29.             lock.unlock();
  30.         }
  31.     }
  32.  
  33. }

当然,要对reader和writer做响应的修改

  1. public class Reader extends Thread {
  2.  
  3.     private BufferInterruptibly buff;
  4.  
  5.     public Reader(BufferInterruptibly buff) {
  6.         this.buff = buff;
  7.     }
  8.  
  9.     @Override
  10.     public void run() {
  11.  
  12.         try {
  13.             buff.read();//可以收到中断的异常,从而有效退出
  14.         } catch (InterruptedException e) {
  15.             System.out.println("我不读了");
  16.         }
  17.        
  18.         System.out.println("读结束");
  19.  
  20.     }
  21.  
  22. }
  23.  
  24. /**
  25. * Writer倒不用怎么改动
  26. */
  27. public class Writer extends Thread {
  28.  
  29.     private BufferInterruptibly buff;
  30.  
  31.     public Writer(BufferInterruptibly buff) {
  32.         this.buff = buff;
  33.     }
  34.  
  35.     @Override
  36.     public void run() {
  37.         buff.write();
  38.     }
  39.  
  40. }
  41.  
  42. public class Test {
  43.     public static void main(String[] args) {
  44.         BufferInterruptibly buff = new BufferInterruptibly();
  45.  
  46.         final Writer writer = new Writer(buff);
  47.         final Reader reader = new Reader(buff);
  48.  
  49.         writer.start();
  50.         reader.start();
  51.  
  52.         new Thread(new Runnable() {
  53.  
  54.             @Override
  55.             public void run() {
  56.                 long start = System.currentTimeMillis();
  57.                 for (;;) {
  58.                     if (System.currentTimeMillis()
  59.                             - start > 5000) {
  60.                         System.out.println("不等了,尝试中断");
  61.                         reader.interrupt();
  62.                         break;
  63.                     }
  64.  
  65.                 }
  66.  
  67.             }
  68.         }).start();
  69.  
  70.     }
  71. }

这次“读”线程接收到了lock.lockInterruptibly()中断,并且有效处理了这个“异常”。好奇的读者,肯定要探个究竟,为什么ReentrantLock能做到这点,接下来,我们去迷宫探险吧……

java基础 , , ,

对象序列化为何要定义serialVersionUID的来龙去脉

2009年5月5日

在很多应用中,需要对某些对象进行序列化,让它们离开内存空间,入住物理硬盘,以便长期保存。比如最常见的是Web服务器中的Session对象,当有10万用户并发访问,就有可能出现10万个Session对象,内存可能吃不消,于是Web容器就会把一些seesion先序列化到内存,等要用了,再还原到对象中,说白了,就是能将一个2进制文件变成内存中的对象。在JAVA中,要实现这种机制,只要实现Serializable接口就可以了,先看下面这个简单例子,serialVersionUID稍后引出。我们先定义一个简单的Person类,然后创建这个对象,最后序列化它到一个文件。

  1. import java.io.Serializable;
  2.  
  3. public class Person implements Serializable {
  4.    
  5.     private String name;
  6.    
  7.     public String getName() {
  8.         return name;
  9.     }
  10.     public void setName(String name) {
  11.         this.name = name;
  12.     }
  13. }
  1. import java.io.FileInputStream;
  2. import java.io.FileOutputStream;
  3. import java.io.ObjectInputStream;
  4. import java.io.ObjectOutputStream;
  5.  
  6. public class WhySerialversionUID {
  7.  
  8. public static void main(String[] args) throws Exception {
  9.  
  10. //这里是把对象序列化到文件       
  11. Person crab = new Person();
  12. crab.setName("Mr.Crab");
  13.  
  14. ObjectOutputStream oo = new ObjectOutputStream
  15.     (new FileOutputStream("crab_file"));
  16. oo.writeObject(crab);
  17. oo.close();
  18.  
  19. //这里是把对象序列化到文件,我们先注释掉,一会儿用
  20. //ObjectInputStream oi = new ObjectInputStream
  21. //    (new FileInputStream("crab_file"));
  22. //Person crab_back = (Person) oi.readObject();
  23. //System.out.println("Hi, My name is " + crab_back.getName());
  24. //oi.close();
  25.  
  26.     }
  27. }

运行完后,我们发现有了一个crab_file文件,这个文件就保存这crab对象在内存中的形态。同样,我们把这部分代码注释掉,运行下面那段还原代码,发现,crab_file文件可以被转化为一个对象。

一切都那么顺利,但是如果在序列化之后,Person这个类发生了改变呢?比如,多了一个成员变量。我们做如下试验,还是先将对象序列化到一个文件中,之后在Person这个类中添加一个成员变量,如下:

  1. import java.io.Serializable;
  2.  
  3. public class Person implements Serializable {
  4.    
  5.     private String name;
  6.     //添加这么一个成员变量
  7.     private String address;
  8.    
  9.     public String getName() {
  10.         return name;
  11.     }
  12.     public void setName(String name) {
  13.         this.name = name;
  14.     }
  15. }

之后,我们再去运行一下还原,就发现运行出错了,会报如下错误:
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基础