Tech, Tech

December 17, 2009

解决Eclipse 3.5新建GoogleAppEngine Project项目的问题

Filed under: Java — Tags: , , — Yurii @ 3:08 pm

在Eclipse 3.5下安装GoogleAppEngine Plugin之后,新建Web Application Project时,可能遇到这样的错误提示:

Problems encountered while setting project description.   Nature does not exist: com.google.gwt.eclipse.core.gwtNature.

解决办法是修改project目录下的.project文件,新增以下几行:

<natures>
	<nature>org.eclipse.jem.workbench.JavaEMFNature</nature>
	<nature>org.eclipse.jdt.core.javanature</nature>
	<nature>com.google.gwt.eclipse.core.gwtNature</nature>
	<nature>com.google.gdt.eclipse.core.webAppNature</nature>
	<nature>org.eclipse.wst.common.modulecore.ModuleCoreNature</nature>
	<nature>org.eclipse.wst.common.project.facet.core.nature</nature>
</natures>

November 30, 2009

判断中文词语相似程度的几点思考

Filed under: Programming — Tags: , , , — Yurii @ 8:23 pm

许多地方都需要判断两个词(字符串)是否“相似”(譬如提示、纠错),对英文单词来说,简单的编辑距离算法就可以获得不错的效果,但中文词语的情况则更为复杂,下面谈谈我的经验。

对于中文单词,仅仅通过编辑距离往往很难判断是否“相似”:郭德刚,到底是更像郭德纲,还是李德刚(它与两者的编辑距离都是2)?黄容,到底是更像黄蓉,还是黄奕(编辑距离都是1)?如果不考虑词语本身出现的概率和上下文,凭经验直观判断,我们似乎可以说,“郭德刚”(guodegang)更像“郭德纲”(guodegang),“黄容”(huangrong)更像“黄蓉”(huangrong),究其原因,就是读音相同。所以,最粗略的标准是:在仅考虑文本本身的情况下,中文词语的相似程度,通过拼音判断比字型判断更为可靠。

接下来细究拼音判断,假设我们仅仅把文字转换为拼音字符串,再计算这些字符串的编辑距离,仍然不够细致。郭德干(guodegan)当然比郭也纲(guoyegang)更像郭德刚(guodegang),虽然编辑距离都是1;看来比较好的办法是,不要把拼音当成普通的字符串,而是拆分成声母和韵母两部分,赋予不同的权重(声母的权重更高),再计算编辑距离。如果需要提供一定的模糊性,可以为平舌-卷舌、前鼻音-后鼻音设定相似规则。

如果我们再进一步,需要判断“郭德钢”和“郭德缸”,哪个距离“郭德纲”更近一点,单纯从拼音已经无能为力了,只能通过字型判断——程序如何实现呢?其实也有办法,中文字型的简单判断,可以用到四角号码:“缸”的四角号码是8171,“钢”的四角号码是8772,“纲”的四角号码是2712,“钢”和“纲”的相似表现在右上(7)、右下(2)的编号相似,对四位数字分别做类似异或的运算,就可以判断出哪两个字的形状更相似了。需要说明的是,这种办法的误差很大,一般只推荐在拼音无法区分、也无法根据语境判断的极端条件下进行。

补充:从汉字到拼音的转换有许多库,最简单的是pinyin4j,拆分声母韵母只需要自己包装一下就可以。从汉字到四角号码的映射没有现成的库,如果需要,可以根据Unihan.txt生成相应的数据文件,如果在线也可以调用这个服务

November 17, 2009

Opera在Ubuntu下的字体解决方案

Filed under: Linux — Tags: , , — Yurii @ 4:17 pm

Opera浏览器的速度很快,但在Ubuntu下中文字题总是非常难看,高高低低大大小小,解决的办法是这样的:

修改/usr/share/opera/ini/font.ini

在其中找到关于中文字体的一段:

family:Song|Songti|fangsong*=chinese-s try-first

将开头的两个字体改成自己喜欢的中文字体名称就可以了,我改成了

family:STHeiti|STHeiti|fangsong*=chinese-s try-first

效果

Ubuntu下Opera的字体问题

November 5, 2009

Ubuntu 9.10升级的若干问题

Filed under: Linux — Tags: , , , — Yurii @ 12:43 pm

Ubuntu 9.10发布了,从11月1日开始升级、折腾,到现在才算差不多稳定,走过的弯路写在这里。

1.升级方法:我觉得比较合适的办法是下载Alternate CD,从本地升级,并在升级时选择不使用网络,这样能最大限度地保证升级的稳定性和时间。具体做法是:

下载http://releases.ubuntu.com/karmic/ubuntu-9.10-alternate-i386.iso
sudo mount -o loop ubuntu-9.10-alternate-i386.iso /media/cdrom0
sh /cdrom/cdromupgrade
选择不使用网络(等升级完成之后更新到最快的apt源再使用网络比较好),之后升级一路顺利

2.升级之后NetworkManager不能用了,网络完全断开,网线都不能用
查看/var/log/syslog发现错误是找不到 /usr/lib/NetworkManager/libnm-settings-plugin-keyfile .so(天知道为什么.之前有个空格!)
做一个符号链接 cd /usr/lib/NetworkManager/ && sudo mv libnm-settings-plugin-keyfile.so  ‘libnm-settings-plugin-keyfile .so’
之后发现NetworkManager启动了,但显示Device Not Mangered,所有网络仍然不可用
上网查发现需要修改/etc/NetworkManager/nm-system-settings.conf,把[ifupdown]中的managered=false改成managered=true
之后网络总算正常了

3.字体,之前我用的苹果黑体,升级之后发现字体全乱了,原来是要修改/etc/fonts/conf.avail/69-language-selector-zh-cn.conf
打开这个文件,在每一个<edit>的第一行添加<string>STHeiti</string>
重新启动X,发现字体正常了

4.Eclipse和QQ的异常
升级完成之后Eclipse许多按钮无法点击,QQ总是自动退出
原来是GDK变化的问题,网上已经有很多解决方案了,我本来希望更改/etc/profile之类将“GDK_NATIVE_WINDOWS=true”直接作为环境变量,但XWindow似乎不管这些,最后我用的办法是这样的(以QQ为例):
新建一个sh文件qq_patch.sh,内容如下

#! /bin/sh
export GDK_NATIVE_WINDOWS=true
/usr/bin/qq

我的QQ图标是放在屏幕最上方的panel中的,所以直接修改这个图标(也就是启动项)的属性,将Command改为qq_patch.sh(如果不在%PATH内,需要添加完整路径),Eclipse也是同样更改。

November 4, 2009

一个简单的拼写检查(关键词错误提示)程序

Filed under: Java, Programming — Tags: , — Yurii @ 1:09 pm

拼写检查和关键词错误提示,是目前常见的功能,拼写检查能保证用户输入单词的拼写准确,关键词错误提示则可以帮用户“确认”准确的信息。

拼写检查

我们多动一点脑筋就会发现,这两个功能的本质是差不多的:找到与某个字符串“相似”的其它字符串。以下我们来讲解这个处理过程的大致原理。

首先我们解决判断两个字符串“相似”程度的问题。很明显,字符串“correct”和“korrect”非常“相似”,而“correct”和“korect”尽管也“相似”,但“相似”程度就不及之前的例子。为了能够定量分析“相似程度”,我们引入了“编辑距离”的概念。
所谓编辑距离(editing distance),也叫列文斯坦距离(Levenshtein Distance),指的是从一个字符串“变化”到另一个字符串的操作步骤数,其中的“操作步骤”一般是这样定义的:每增、删、改一个字符,计为一个操作步骤。两个字符串之间的编辑距离越大,它们的相似程度就越低。
再来看之前的例子,从korrect到correct,需要把第一个字符k改成c,所以编辑距离为1;而从korect到correct,除了需要把开头的k变成c,还需要“增加”一个r,所以编辑距离为2,故而korect就没有korrect那么“类似”correct。
编辑距离的计算已经有成熟的算法,主要思想就是按照动态规划,按照bottom-up的方式算出,大致步骤如下:

1.设定n为字符串s的长度,m为字符串t的长度;如果n=0,则返回m并退出,如果m=0,则返回n并退出;否则,构建一个m行n列的矩阵(数组);
2.初始化矩阵,第一行依次设定为0…n,第一列依次设定为0…m;
3.依次检查s的各个字符(假设当前下标为i,1 <= i <= n);
4.依次检查t的各个字符(假设当前下标为j,1 <= i <= m);
5.如果s[i]等于t[j],则cost为0;否则为1;
6.将矩阵元素d[i,j]设定为以下三个数值的最小值:d[i-1,j]+1, d[i,j-1]+1, d[i-1, j-1]+cost
7.重复第3,4,5,6步,直到计算出矩阵元素全部,得到的d[n,m]为编辑距离。

有了编辑距离,我们就可以判断出与某个字符串“相似”的字符串了;但是仅有编辑距离还不够,在拼写检查和关键词错误提示时,我们需要迅速“找出”与某个字符串在一定编辑距离之内的其它字符串。所有“备选”的字符串可以首先准备好,报存在一个词典内,但词典的查询却非常麻烦:以简单顺序保存需要遍历所有元素才能得到结果;散列保存也无法找到合适的散列值(因为编辑距离是根据两个字符串的具体情况临时计算出来的)。看来,需要用到别的办法。

我们仔细观察编辑距离就会发现,它具有以下几个特性:
ED(s, s) = 0
ED(s, t) = ED(t, s)
ED(s, t) + ED(t, u) >= ED(s, u),其实这也很好理解,如果从s到t需要x步,从t到u需要y步,那么从s到u最多需要x+y步(首先进行x步变换成为t,然后进行y步变换成为u)。既然满足三角不等式,就可以用数据结构BKTree来解决。
BKTree的结构非常简单,从名字看出,它也是一棵树,包含父结点、子结点、边几种元素,其中,一个父结点可以包含多个子结点,父子结点之间的距离,就是父结点所对应字符串与子结点所对应字符串的编辑距离。

按照BKTree的特性,我们可以很方便地构建出BKTree:假设需要插入字符串s:

1.将根结点R设为当前结点N;
2.对比s与N对应字符串Ns的编辑距离,若为0,表示此字符串s已经存在,退出;
3.若编辑距离为d,则查找当前结点的子结点中,与当前结点距离为d的那个子结点C。如果存在,则将C设为N,返回第2步;否则,在当前结点新建一个子结点Cn,它对应的字符串就是s,退出;

BKTree构建完成之后,就可以进行查找了。假设对于字符串s,需要查找与其编辑距离<=d的字符串,步骤是:

1.将根结点R设为当前结点N;
2.计算N对应的字符串Ns与s的距离ED(Ns, s),赋值为m;
3.如果m <= d,将Ns加入结果;
4.遍历N的所有子结点,如果某个子结点C与N的距离k满足 (m – d) <= k <= (m + d);则将该子结点记录下来,依次重复2,3,4步,直到结束退出;

这思路很好理解:
如果ED(s, Ns) <= d,Ns当然在结果之中;
对于N的某个子结点C来说,设ED(s, Cs) = x,若x <= d;
按照三角不等式,则m + d >= m + x = ED(Ns, s)  + ED(s, Cs) >= ED(Ns, Cs) = k,所以 k <= m + d;另一方面,k + x = ED(Ns, Cs) + ED(Cs, s) >= ED(Ns, s) = m,也就是 k > = m – x >= m – d。

这样,基本的数据结构就确立下来。如果我们按照前期分析(根据出现次数等),给不同的字符串加上权重,就可以实现一个简单的关键词提示了。

需要补充的是,构建BKTree是比较消耗时间的操作,许多时候我们将构建好的BKTree保存下来,以后直接载入就可以了。但是在Java语言中,如果父结点中“子结点的指针”是原始的引用,序列化时可能会导致StackOverflow的异常(实际上,类似树的数据结构可能经常遇到这样的问题),最好的办法是改变树的存储方式:把结点存储在一个顺序表中,每个结点对应一个编号,“子结点的指针”就成了“子结点对应编号”,如此就不会有问题了。

参考:

怎样写一个拼写检查器发掘Java Serialization API中的秘密

October 29, 2009

制作Ubuntu USB启动光盘的办法

Filed under: Linux — Tags: — Yurii @ 4:27 pm

对与没有光驱的笔记本来说,通过U盘安装安装Ubuntu似乎是最简单的方法了。只是,我们需要首先制作一个可以启动Ubuntu的U盘。上网查了许多办法,不是太麻烦就是不可行。最后找到了一个简单的:

找一台安装了Ubuntu的机器,先下载想要安装的Ubuntu的ISO文件(我这里是ubuntu-9.04-desktop-i386.iso),将格式化为Fat16或者Fat32格式的U盘插上,执行以下命令:

sudo apt-get install syslinux mtools
wget -O isotostick.sh http://www.ymer.org/files/isotostick-jj.sh
chmod +x isotostick.sh
sudo ./isotostick.sh ubuntu-9.04-desktop-i386.iso /dev/sdb1

注意,这里的/dev/sdb1可能随机器不同有所变化,具体请输入fdisk -l查看自己机器的情况,替换/dev/sdb1。

如果使用Fat16文件系统,可能会出现“operation not permitted”的错误,这是因为它不支持符号链接。不过可以完全忽略,因为这个错误并不会引起安装异常。

October 3, 2009

发掘Java Serialization API中的秘密

Filed under: Java — Tags: — Yurii @ 8:03 am

发掘Java Serialization API中的秘密

作者 Todd Greanier
翻译 Yurii
原文地址 http://java.sun.com/developer/technicalArticles/Programming/serialization/
转载请注明来源 http://www.luanxiang.org/tech/archives/43.html

我们都知道,在Java中,我们能在内存里创建可重用的对象。然而,这些对象只有在Java虚拟机运行时才能存在。如果我们创建的对象能摆脱虚拟机的限制就好了,对吗?对了,依靠对象的序列化(serialization),你就能把对象固化(flatten),用各种神奇的方式重用。

序列化一个对象,就是把这个对象的状态存储到一系列字节中的过程,将来可以从这些字节还原出这个对象。Java Serialization API为处理对象序列化的开发人员提供了一套标准机制。如果你理解了这些类和方法,就会发现API并不复杂,而且方便使用。

本文介绍如何持久化自己的Java对象,从基础开始,前进到更高级的概念。我们会学到不同的序列化方法——使用缺省协议,定制缺省协议,编制自己的协议——我们会详细检查从对象缓存、版本控制以及性能因素等等持久化方案中产生的问题。

看完本文,你会清晰认识这套强大的Java API,尽管你之前可能并没什么了解。

开始:缺省的序列化机制

从基础开始。要序列化一个Java对象,我们必须有一个能持久的对象。如果一个对象实现了java.io.Serializable接口,它就是可以序列化的;这个接口向下层API说明,此对象可以转换为一系列字节固化下来(flatten),将来再从中取出。

下面这个类能够持久化,我们用它来说明序列化机制

10 import java.io.Serializable;
20 import java.util.Date;
30 import java.util.Calendar;
40 public class PersistentTime implements Serializable
50 {
60 private Date time;
70
80 public PersistentTime()
90 {
100      time = Calendar.getInstance().getTime();
110    }
120
130    public Date getTime()
140    {
150      return time;
160    }
170  }

我们看到,与通常使用的类的唯一区别就在于第40行实现了java.io.Serializable接口。Serializable接口仅仅是是一个用于标记的接口,不需要执行任何方法——序列化机制依靠它来验证类是否能持久化。这样,我们得到了序列化的第一条规则:

规则1:欲持久化的对象必须实现Serializable接口,或者继承实现了此接口的类。

下一步就是真正来序列化这个对象了。它是由java.io.ObjectOutputStream这个类完成的。这个类是一个过滤流——它是包装好的低端字节流(也就是节点流node stream),为我们处理序列化协议。节点流可以写到文件,也可以通过Socket传输。也就是说,我们可以很容易地把固化的对象通过网络传输,在网络的另一端恢复出来。

来看看存储PersistentTime对象的代码:

10 import java.io.ObjectOutputStream;
20 import java.io.FileOutputStream;
30 import java.io.IOException;
40 public class FlattenTime
50 {
60 public static void main(String [] args)
70 {
80 String filename = “time.ser”;
90 if(args.length > 0)
100     {
110       filename = args[0];
120     }
130     PersistentTime time = new PersistentTime();
140     FileOutputStream fos = null;
150     ObjectOutputStream out = null;
160     try
170     {
180       fos = new FileOutputStream(filename);
190       out = new ObjectOutputStream(fos);
200       out.writeObject(time);
210       out.close();
220     }
230     catch(IOException ex)
240     {
250       ex.printStackTrace();
260     }
270   }
280 }

真正的步骤是在第200行,我们调用了ObjectOutputStream.writeObject()方法,它启动了序列化机制,对象被固化了(存到了文件里)

下面的代码用来从文件中恢复对象:

10 import java.io.ObjectInputStream;
20 import java.io.FileInputStream;
30 import java.io.IOException;
40 import java.util.Calendar;
50 public class InflateTime
60 {
70 public static void main(String [] args)
80 {
90 String filename = “time.ser”;
100     if(args.length > 0)
110     {
120       filename = args[0];
130     }
140   PersistentTime time = null;
150   FileInputStream fis = null;
160   ObjectInputStream in = null;
170   try
180   {
190     fis = new FileInputStream(filename);
200     in = new ObjectInputStream(fis);
210     time = (PersistentTime)in.readObject();
220     in.close();
230   }
240   catch(IOException ex)
250   {
260     ex.printStackTrace();
270   }
280   catch(ClassNotFoundException ex)
290   {
300     ex.printStackTrace();
310   }
320   // print out restored time
330   System.out.println(”Flattened time: ” + time.getTime());
340   System.out.println();
350      // print out the current time
360   System.out.println(”Current time: ” + Calendar.getInstance().getTime());
370 }
380}

在上面的代码中,对象的重建是在第210行进行的,通过调用ObjectInputStream.readObject()方法。它读入我们之前序列化时存储的一系列字节,重建与之前一样的对象。因为readObject()可以读取任何序列化的对象,所以必须进行类型转换。因此,进行重建的系统必须能够访问到类的文件。也就是说,对象所属的类的文件,以及对象的方法,都没有保存下来,保存的只是对象的状态。

之后,在第360行,我们调用了getTime()方法来得到被序列化对象被固化的时间。对比两个时间,就可以证明序列化机制确实如期望的工作了。

不可序列化的对象

Java序列化的基本机制很容易使用的,但是只了解这一点是不够的。上面提到过,只有标记了Serializable的对象才可以持久化。java.lang.Object对象没有实现这个接口。所以,并不是所有的Java对象都可以自动序列化的。好消息是,大多数对象——譬如AWT和Swing GUI组件,字符串和数组——都可以序列化。

另一方面,诸如Thread、OutputStream及其子类和Socket等系统级别的类,是无法序列化的。实际上,序列化它们也没有意义。例如,在这台电脑的JVM中运行的线程,会使用这台电脑的内存。把它持久化,再到另一个JVM中运行,毫无意义。关于java.lang.Object没有实现Serializable接口的另一点重要的是,你所创建的任何仅仅继承Object(而不是其它可序列化的类)的类,都是不能序列化的,除非你自己(就像上面的例子一样)实现这个接口。

这样就产生了一个问题:如果我们的类包含Thread实例怎么办?我们能不能持久化这样的对象?答案是肯定的,前提是,我们把自己的意图明确告诉序列化机制,把这个类的Thread对象标记为临时的(transient)。

假设我们要创建表征动物的类。下面省略了关于动物的代码,只给出了我们使用的类:

10 import java.io.Serializable;
20 public class PersistentAnimation implements Serializable, Runnable
30 {
40 transient private Thread animator;
50 private int animationSpeed;
60 public PersistentAnimation(int animationSpeed)
70 {
80 this.animationSpeed = animationSpeed;
90 animator = new Thread(this);
100     animator.start();
110   }
120       public void run()
130   {
140     while(true)
150     {
160       // do animation here
170     }
180   }
190 }

创建PersistentAnimation类的实例,同时也就创立animator线程(Yurii注:这种方法是不值得推荐的,具体请参考Java Concurrency In Practice一书),线程会如预想的工作。在第40行,我们标记了这个线程,告诉序列化机制,这个字段(field)不应该与对象的其它字段(此处是animationSpeed)同时保存下来。底线是:任何字段,如果不能序列化,或者不希望序列化,都应该标记为transient。序列化不关心private之类的访问限定——只要字段没有标记为transient,就会作为对象的持久状态,都会被持久化。

因此,我们得到另一条规则。下面是关于持久化对象的两条规则:

* 规则1:要持久化的对象必须实现Serializable接口,或者继承实现了此接口的类

* 规则2:要持久化的类必须把所有不用序列化的字段标记为transient。

定制缺省协议

现在来看进行序列化的第二种办法:定制缺省协议。尽管上面的代码说明了怎样序列化包含线程的对象,但我们重建这个对象时,仍然会遇到问题。要记住,如果我们创建了一个新的对象,对象的构造函数只有在这个类的实例创建时调用。记住了这一点,我们再来看看animation的代码。首先,我们创建一个PersistentAnimation对象,它启动了animation线程。然后,我们序列化这个对象:

PersistentAnimation animation = new PersistentAnimation(10);
FileOutputStream fos = …
ObjectOutputStream out = new ObjectOutputStream(fos);
out.writeObject(animation);

看来没问题,直到我们调用readObject()方法重建这个对象为止。记得吗?只有创建新的实例,才会调用构造函数。这里并没有创建新的实例,只是恢复之前持久化的对象。这样,animation对象只在第一次创建的时候运行了。这样看来,持久化似乎没什么用,对吗?

嘿,不过有好消息。我们有办法让对象照期望的办法来工作:我们可以让animation在对象恢复之后重新启动。这样,我们可以创建一个辅助方法startAnimation()来完成现有构造函数的工作。我们可以在构造函数中调用这个方法,也可以在读入对象之后调用。这不错,但是把事情搞复杂了。现在,任何希望使用animation对象的人都必须记得,必须在正常的反序列化(deserialization)过程之后调用这个方法。Java Serialization API希望给开发人员提供无缝的方案,但这个办法做不到。

不过,这里倒是有个不错的办法。通过使用序列化机制内建的特性,开发人员必须在类文件内部提供两个办法,为普通的序列化过程中多加一些处理。这两个方法是:

* private void writeObject(ObjectOutputStream out) throws IOException;

* private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException;

请注意,因为两个方法不是继承(inherited),也不是重用(overridden),也不是重载(overloaded),(而且必须)被声明为private。奥妙就在于,虚拟机在调用相应的方法时,会自动检查是否声明了这两个方法。只要需要,虚拟机就能调用private方法,哪怕其它对象都不能。这样就保存了类的完整性,序列化协议也可以正常工作。序列化协议通常就是用这种办法工作的,调用ObjectOutputStream.writeObject()或者是ObjectInputStream.readObject()。因而,即使某个类提供了这些特殊的专用的private方法,其对象的序列化也和其它普通对象的序列化没什么不同。

综上,我们来看看改进的PersistentAnimation,它包含了这两个方法,容许我们控制反序列化(deserialization)过程,下面是构造函数伪代码:

10 import java.io.Serializable;
20 public class PersistentAnimation implements Serializable, Runnable
30 {
40 transient private Thread animator;
50 private int animationSpeed;
60 public PersistentAnimation(int animationSpeed)
70 {
80 this.animationSpeed = animationSpeed;
90 startAnimation();
100   }
110       public void run()
120   {
130     while(true)
140     {
150       // do animation here
160     }
170   }
180   private void writeObject(ObjectOutputStream out) throws IOException
190   {
200     out.defaultWriteObject();
220   }
230   private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException
240   {
250     // our “pseudo-constructor”
260     in.defaultReadObject();
270     // now we are a “live” object again, so let’s run rebuild and start
280     startAnimation();
290
300   }
310   private void startAnimation()
320   {
330     animator = new Thread(this);
340     animator.start();
350   }
360 }

请注意两个新的private方法的第一行。它们的任务正如它们的名字——对固化的字段执行缺省的读写操作,这两点很重要,因为我们没有替换缺省的程序,只是向其中增添了内容。调用ObjectOutputStream.writeObject()就会启动缺省的序列化协议。首先检查对象,确保它实现了Serializable接口,然后检查是否提供了这些缺省的private方法。如果是,stream类就被作为参数传递过去,根据用途,交出代码的控制权。

这些private方法可以用来定制在序列化过程中进行任何需要的步骤。加密应该加到输出中,而解密应该加到输入(请注意,字节是直接读写的,没有进行任何处理)。也可以用来向流中加入其它的数据,可能是公司的验证代码。这里具备无限的可能。

停止这样的序列化!

OK,关于序列化过程,我们已经了解了不少,现在再多看一点。如果你希望创建这样的类:父类可以序列化,但是子类不能序列化?你不能取消对某个接口的实现,所以如果父类实现了Serializable接口,子类就会实现(假设满足了上面两条规则)。

10 private void writeObject(ObjectOutputStream out) throws IOException
20 {
30 throw new NotSerializableException(”Not today!”);
40 }
50 private void readObject(ObjectInputStream in) throws IOException
60 {
70 throw new NotSerializableException(”Not today!”);
80 }

读写这个对象的任何尝试,都会抛出异常。请记住,因为这些方法是private的,除非能接触到源代码,否则没法修改它们——Java不容许重写(override)这些方法。

创建自己的协议:Externalizable接口

如果错过了序列化的第三个选项,我们的讨论就不够完整:通过Externalizable接口创建自己的协议。我们可以实现Externalizable,而不是Serializable,Externalizable包含两个方法:

* public void writeExternal(ObjectOutput out) throws IOException;

* public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;

重写这两个方法就可以提供自己的协议。它与前面看到的序列化做法不同,这里一切都得自己来。也就是说,整个协议都得自己动手。尽管这办法更麻烦,但也是操控程度最高的办法。举个这样序列化的例子:通过Java程序读写PDF文件。如果你知道该怎么读写PDF(也就是那一系列字节),就可以通过writeExternal和readExternal方法,提供专门针对PDF的协议。

和以前一样,无论一个类是如何实现Externalizable的,使用上都没有差别。只需要调用writeObject()和readObject(),然后,就会自动调用这些方法了。

陷阱

不熟悉序列化机制的开发人员可能觉得有些问题非常奇怪。当然,本文就是为此而作——让你明白。所以,我们来看看这些陷阱,看我们是否能明白,它们为什么会出现,如何解决。

在流中缓存对象

首先来看这样的情况,把对象写如流中,然后再写一次。缺省情况下,ObjectOutputStream会保存写入它的对象的引用。也就是说,如果写入对象的状态被多次写入,新的状态就不会保存!下面的代码说明了这个问题:

10 ObjectOutputStream out = new ObjectOutputStream(…);
20 MyObject obj = new MyObject(); // must be Serializable
30 obj.setState(100);
40 out.writeObject(obj); // saves object with state = 100
50 obj.setState(200);
60 out.writeObject(obj); // does not save new object state

有两种办法控制这个情况。第一,你可以确定,在一个写入调用之后关闭整个流,确保新的对象每次只写入一次。第二,你可以调用ObjectOutputStream.reset()方法,它会告诉流清理其所保存的对引用的缓存,这样所有的新的写入调用都会实际写入进去。但也要小心,reset会刷新整个object的缓存,因此所有写入的对象都可以重新写入。

版本控制

现在来看第二个陷阱,设想,你创建了一个类,实例化了,然后写入到对象流。固化的对象在文件系统中保存了一段时间。之后,你更新了类文件,可能增加了新的字段。这时候再读入之前固化的对象,会发生什么?

坏消息是,会抛出异常——具体说就是java.io.InvalidClassException——因为所有能够持久化的类都自动赋予了一个独特的标识符。如果标识符不等于固化对象的标识符,就会抛出异常。但是,如果你真正思考这个问题,我仅仅加了一个字段,为什么就要抛出异常?这个字段不能被设定为缺省值,下次序列化时再写入吗?

确实,我们可以做到,但是需要一点代码来实现。所有类的标识符,都保存在一个叫serialVersionUID的字段中。如果你希望控制版本,你就可以自己提供serialVersionUID字段,确保你修改类文件时它不会发生变化。你可以使用JDK发布版带的一个工具来检查缺省的值(缺省情况下,就是object的hash值)。

下面的例子说明了使用serialver来查看Baz类的缺省值。

> serialver Baz
> Baz: static final long serialVersionUID = 10275539472837495L;

把返回的数值拷贝出来,贴到自己的代码里(在Windows窗口里,你可以用–show参数运行这个工具,简化拷贝-粘贴流程)现在,如果你对Baz的类文件进行了任何修改,只要确保version ID没有发生变化,程序就可以正常运行。

只要对源代码的更改保证了兼容性,版本控制就能带来巨大的好处。兼容的修改包括添加或去除方法或者字段。不兼容的改变包含更改对象的层级结构,或者是去掉对Serializable接口的实现。在Java Serialization Specification中,给出了兼容和不兼容更改的完整列表。

性能问题

第三个陷阱:缺省的机制尽管易于使用,性能却不是最好的。我把Date对象写入到一个文件1000次,重复这个过程100次。写入一个Date对象的平均时间是115ms。然后我手动输出Date对象,通过标准的I/O,运行同样多的次数,平均时间是52ms。几乎节省了一半的时间!通常,会在易用性和性能之间进行权衡,序列化也不例外。如果速度是你的应用程序的主要目标,你可能希望自己定制一个协议。

另一个考虑是关于之前提到的事实,在输出流中缓存的对象引用。这样,如果流没有关闭,系统可能不能对写入流的对象进行垃圾收集。故而,最好的办法仍然与其它I/O操作一样,就是在流操作完成之后,尽快关闭这些流。

结论

Java中的序列化很有吸引力,而且同样易于使用。了解实现序列化的三种不同方式,有助于按照自己的意愿整理API。在本文中,我们看到了许多序列化机制,我希望它有助于澄清事实,而不是相反。给出所有代码的底线,就是在熟悉API的前提下维持常识。本文为理解Java序列化API提供了坚实的基础,不过我仍然推荐读者细读规范,发现更多详尽的细节。

August 19, 2009

为Ant输出的Jar文件添加Debug信息

Filed under: Java — Yurii @ 12:19 pm

现在许多开发团队都使用ANT来实现自动化的集成构建/打包/发布,ANT的诸多好处这里就不多说了,但ANT打包出来的jar文件没有包含Source信息,如果在运行时出现异常(当然,如果测试环境完善,这样的几率很小),输出的Exception信息只能看到class名,而不包含line number(只显示Unknown Source),非常不方便调试:

java.lang.NullPointerException

at com.xxx.xxx.ServerThread.run(Unknown Source)
at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:885)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:907)
at java.lang.Thread.run(Thread.java:619)

要解决这个问题,只需要在ANT的javac task之后添加两个参数即可(我从网上查了几份英文的资料,似乎都不对)

<javac srcdir=”${src}” destdir=”${build}” encoding=”utf-8″ debug=”on” debuglevel=”lines,vars,source” classpathref=”classpath”/>


July 27, 2009

Ubuntu中Perl Locale Warning的处理

Filed under: Uncategorized — Tags: — Yurii @ 12:01 pm

在Ubuntu中安装和更新软件时,有可能会遇到下面的错误:

perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
LANGUAGE = (unset),
LC_ALL = “”,
LC_TIME = “en_US.UTF-8″,
LC_CTYPE = “zh_CN.UTF-8″,
LC_MONETARY = “en_US.UTF-8″,
LC_COLLATE = “en_US.UTF-8″,
LC_ADDRESS = “en_US.UTF-8″,
LC_TELEPHONE = “en_US.UTF-8″,
LC_MESSAGES = “en_US.UTF-8″,
LC_NAME = “en_US.UTF-8″,
LC_MEASUREMENT = “en_US.UTF-8″,
LC_IDENTIFICATION = “en_US.UTF-8″,
LC_NUMERIC = “en_US.UTF-8″,
LC_PAPER = “en_US.UTF-8″,
LANG = “en_US.UTF-8″

按照提示,这是因为Locale设定错误所导致的,输入locale -a,可以看到当前机器的Locale设定:

locale: Cannot set LC_CTYPE to default locale: No such file or directory
C
en_US.utf8
POSIX

比较之后我们发现, LC_CTYPE被设定为zh_CN.utf8,而当前并未安装,所以产生了问题。解决办法有两个:

  1. 在/etc/profile文件里设定LC_CTYPE为en_US.utf8
  2. 安装zh_CN.utf8,输入sudo apt-get install language-pack-zh即可。

March 24, 2009

SubEclipse的中文问题

Filed under: Misc — Tags: , , , — Yurii @ 3:37 pm

SubEclipse是Eclipse下主要的Subversion插件,但它有个问题:在一般系统上总是会显示中文界面(我在Windows和Linux下都是如此)。Eclipse本来习惯英文界面,忽然蹦出一大堆中文,还真是不适应。

要设定SubEclipse的语言,需要在Eclipse启动时,添加参数

-nl en_US

譬如,在我的Ubuntu下,是这样修改快捷方式的(Windows下照样修改)。

Older Posts »

Powered by WordPress