2008年12月24日星期三

万年历

年末休假中,总想找点什么事情做做,lp说要买个挂历,我说我给你做吧,因为很早以前就见过别人写过很不错的万年历,觉着挺有意思的,曾经试图仿着写一个Java版本的农历,不过一直都没有做起来。

既然有几天假期,于是又开始蠢蠢欲动了,于是上网去找相关的资料,看看有没有现成的咚咚,我可不想重新发明轮子。

网上有很多的万年历,但是真正作者却很少,因为很多人都是抄来抄去。之前我看过一个叫做知来者的万年历,写得很好,精确度很高,还是难得的开放源代码的程序,但是作者却神龙不见首尾,连个联系方式都失效了。这次又找到了他的一个blog,虽然也有一年多没更新了,不过还是知道了两点,一是他还是移动万年历MobCal的真正作者,二是他的email地址,虽然我没有和他打过交道,但是就目前国内的IT圈子里,一个认真做事的开放源代码程序员是值得敬佩的。

另外就是很顺利地在一个农历论坛找到了一个很不错JavaScript版本的叫做寿星万年历,作者许剑伟,似乎是福建莆田十中的一个中学教师。这个程序算法是基于天文算法做出来的历法,准确性很高,还能计算日月食等等天文相关的数据。
仔细读了一下这位许老师的一些帖子发现,他是一个非常能钻研的人,做这个程序之前,他并没有很多天文历法方面的知识,然而通过一段时间的努力,他阅读了大量的天文资料,做出了实实在在的成果,并且翻译了《天文算法》这本书,这一切,都是在一年之内完成的。

回想一下自己在这一年,没有什么长进,一直都以很忙做为借口,想做的事却一直没有动手去做,实在是惭愧。“做到”这两个字,是当前的我最需要注意的。

2008年12月13日星期六

关于字节流的一个争论(二)

上次简单说了一下问题,这次就说说争论的过程,也算是一个影响失败的教训。

话说英国人收到了我开的bug之后,第二天就给我回信了,我还正高兴呢!心想这老外的效率真高,可打开邮件我就傻眼了,人家效率是高,可惜这个效率是把皮球踢回来的效率,不是解决问题的效率。
在邮件里,英国人首先指出我提供的例子不符合Java序列化的要求,让我去读一下Java序列化的规范,因为我在writeObject()里面少调了defaultWriteObject或writeFields(),blabla了一大堆,朋友们哪,这就是踢皮球的第一计,叫做围魏救赵。不过这个还是容易对付的,于是直接了当地和他们说,我给你的只是一段例子,为了展示问题而已,写不写这个不影响问题的展现,请解释问题吧。

很快,英国人的第二封邮件过来了,还是让我看Java序列化的规范,这次的理由是按照Java序列化的规范,读写必须一致,也就是说任何一次写数据,都必须要有同类型的一次读操作。我已开始觉得这个说法挺合理的,也仔细想就犯了一个错误,没有在第一时间反驳其说法的不严谨,导致了后面的长期扯皮。
在此之后,又有几个来回,最终对方直接就忽略掉我的邮件,这个事情就这样不了了之了。

2008年12月11日星期四

关于字节流的一个争论(一)

最近一段时间,和一个英国人争论了很久,其实来回也没几封邮件,但是对方每次回都要隔一两天,所以就拖了很久,而且最终也没有解决,看来我的影响力(Influence)还是很需要提高的。

问题其实很简单,我们的代码里需要序列化一个数据对象,序列化前它是很多个字节数组(byte array),这个是因为这个对象类似ByteArrayOutputStream,它是慢慢变大的,一开始的时候我们并不知道它有多大,如果用单个数组,在变大的过程中需要不断申请新的大块内存,可能会导致内存不够的错误。因为我们关心的是数据,反序列化之后则只需要恢复成一个数组就好了。

所以我们的主要逻辑就类似于下面这样的伪代码:
class ValueObject {
private transient List<byte[]> values;
private transient byte[] data;
private void writeObject(ObjectOutputStream output) {
output.writeInt(totalLength); // write out the total length of bytes
for (byte[] value : values) {
output.write(value, 0, value.length);
}
}

private void readObject(ObjectInputStream input) {
int length = input.readInt();
data = new byte[length];
input.read(data, 0, length);
}
}

换句话说,我们的主要想法就是写的时候分开写,读的时候一次读,这个逻辑本来是没有问题的,因为Java里面的流(stream)指的就是字节流,比如OutputStream里面明确申明:
This abstract class is the superclass of all classes representing an output stream of bytes.
既然是流,我怎么往里面写数据,另外一头怎么读应该任意,只要我读写的总字节数对上了就OK,而且大部分I/O类也都是这么实现的,所以这段代码一直都能很好地工作,似乎从来没有问题。

然而,我们还是碰到了一个问题,当我们的对象通过RMI-IIOP传输的时候,竟然导致了一个错误。经过测试,发现了一个奇怪的现象,那就是IIOP的I/O流和普通的流的行为是不一致的,具体就是当数组比较大的时候,当你写出一个字节数组,那么对应地必须读一个字节数组,写两次就得读两次,而且这个问题只在字节数组比较大的时候才会出现问题,小数组是没有问题的。

为什么会有这么奇怪的问题呢?仔细读过这方面的实现代码CDRInputStream后发现,IIOP是把数据分区块(block)传输的,比如这一头做了很多次写,如果数据不多的话(小于等于区块大小),但是传输是一个区块过去的,而对于字节数组,如果大于区块大小,就会是一个单独的区块。问题在于,区块是有边界标志的,读的时候,应该检查边界标志,事实上CDRInputStream也是这么做了,然而不幸的是,当用户读一个字节数组的时候,它假定用户指定的长度刚好就是区块的大小,并不判断是否超出当前区块,问题就出现了。

IT民工看到这儿,基本上立刻意识到,这不就是一个简单的错误么?检查一下边界就好了。于是我立刻给我们公司负责JDK ORB的组开了一个bug,要求他们解决这个问题,我以为他们应该很容易改掉,可是没有想到,这个问题竟然也成了一个扯皮的问题,最后竟然没法解决了!

具体过程有点复杂,下次再写。