2007年5月30日星期三
2007年5月18日星期五
2007年5月10日星期四
Java泛型编程
1、Java泛型
其实Java的泛型就是创建一个用类型作为参数的类。就象我们写类的方法一样,方法是这样的method(String str1,String str2 ),方法中参数str1、str2的值是可变的。而泛型也是一样的,这样写class Java_Generics<K,V>,这里边的K和V就象方法中的参数str1和str2,也是可变。下面看看例子:
//code list 1 import Java.util.Hashtable; class TestGen0<K,V>{ public Hashtable<K,V> h=new Hashtable<K,V>(); public void put(K k, V v) { h.put(k,v); } public V get(K k) { return h.get(k); } public static void main(String args[]){ TestGen0<String,String> t=new TestGen0<String,String>(); t.put("key", "value"); String s=t.get("key"); System.out.println(s); } } |
正确输出:value
这只是个例子(Java中集合框架都泛型化了,这里费了2遍事.),不过看看是不是创建一个用类型作为参数的类,参数是K,V,传入的“值”是String类型。这个类他没有特定的待处理型别,以前我们定义好了一个类,在输入输入参数有所固定,是什么型别的有要求,但是现在编写程序,完全可以不制定参数的类型,具体用的时候来确定,增加了程序的通用性,像是一个模板。
呵呵,类似C++的模板(类似)。
1.1. 泛型通配符
下面我们先看看这些程序:
//Code list 2 void TestGen0Medthod1(List l) { for (Object o : l) System.out.println(o); } |
看看这个方法有没有异议,这个方法会通过编译的,假如你传入String,就是这样List<String>。
接着我们调用它,问题就出现了,我们将一个List<String>当作List传给了方法,JVM会给我们一个警告,说这个破坏了类型安全,因为从List中返回的都是Object类型的,而让我们再看看下面的方法。
//Code list 3 void TestGen0Medthod1(List<String> l) { for (Object o : l) System.out.println(o); } |
因为这里的List<String>不是List<Object>的子类,不是String与Object的关系,就是说List<String>不隶属于list<Object>,他们不是继承关系,所以是不行的,这里的extends是表示限制的。
类型通配符是很神奇的,List<?>这个你能为他做什么呢?怎么都是“?”,它似乎不确定,他总不能返回一个?作为类型的数据吧,是啊他是不会返回一个“?”来问程序员的?JVM会做简单的思考的,看看代码吧,更直观些。
//code list 4 List<String> l1 = new ArrayList<String>(); li.add(“String”); List<?> l2 = l1; System.out.println(l1.get(0)); |
这段代码没问题的,l1.get(0)将返回一个Object。
1.2. 编写泛型类要注意:
1) 在定义一个泛型类的时候,在 “<>”之间定义形式类型参数,例如:“class TestGen<K,V>”,其中“K” , “V”不代表值,而是表示类型。
2) 实例化泛型对象的时候,一定要在类名后面指定类型参数的值(类型),一共要有两次书写。例如:
TestGen<String,String> t=new TestGen<String,String>();
3) 泛型中<K extends Object>,extends并不代表继承,它是类型范围限制。
2、泛型与数据类型转换
2.1. 消除类型转换
上面的例子大家看到什么了,数据类型转换的代码不见了。在以前我们经常要书写以下代码,如:
//code list 5 import Java.util.Hashtable; class Test { public static void main(String[] args) { Hashtable h = new Hashtable(); h.put("key", "value"); String s = (String)h.get("key"); System.out.println(s); } } |
这个我们做了类型转换,是不是感觉很烦的,并且强制类型转换会带来潜在的危险,系统可能会抛一个ClassCastException异常信息。在JDK5.0中我们完全可以这么做,如:
//code list 6 import Java.util.Hashtable; class Test { public static void main(String[] args) { Hashtable<String,Integer> h = new Hashtable<String,Integer> (); h.put("key", new Integer(123)); int s = h.get("key").intValue(); System.out.println(s); } } |
这里我们使用泛化版本的HashMap,这样就不用我们来编写类型转换的代码了,类型转换的过程交给编译器来处理,是不是很方便,而且很安全。上面是 String映射到String,也可以将Integer映射为String,只要写成HashTable<Integer,String> h=new HashTable<Integer,String>();h.get(new Integer(0))返回value。果然很方便。
2.2 自动解包装与自动包装的功能
从上面有没有看到有点别扭啊,h.get(new Integer(123))这里的new Integer(123);好烦的,在JDK5.0之前我们只能忍着了,现在这种问题已经解决了,请看下面这个方法。我们传入一个int这一基本型别,然 后再将i的值直接添加到List中,其实List是不能储存基本型别的,List中应该存储对象,这里编译器将int包装成Integer,然后添加到 List中去。接着我们用List.get(0);来检索数据,并返回对象再将对象解包装成int。恩,JDK5.0给我们带来更多方便与安全。
//Code list 7 public void autoBoxingUnboxing(int i) { ArrayList<Integer> L= new ArrayList<Integer>(); L.add(i); int a = L.get(0); System.out.println("The value of i is " + a); } |
2.3 限制泛型中类型参数的范围
也许你已经发现在code list 1中的TestGen<K,V>这个泛型类,其中K,V可以是任意的型别。也许你有时候呢想限定一下K和V当然范围,怎么做呢?看看如下的代码:
//Code list 8 class TestGen2<K extents String,V extends Number> { private V v=null; private K k=null; public void setV(V v){ this.v=v; } public V getV(){ return this.v; } public void setK(K k){ this.k=k; } public V getK(){ return this.k; } public static void main(String[] args) { TestGen2<String,Integer> t2=new TestGen2<String,Integer>(); t2.setK(new String("String")); t2.setV(new Integer(123)); System.out.println(t2.getK()); System.out.println(t2.getV()); } } |
上边K的范围是<=String ,V的范围是<=Number,注意是“<=”,对于K可以是String的,V当然也可以是Number,也可以是Integer,Float, Double,Byte等。看看下图也许能直观些请看上图A是上图类中的基类,A1,A2分别是A的子类,A2有2个子类分别是A2_1,A2_2。
然后我们定义一个受限的泛型类class MyGen<E extends A2>,这个泛型的范围就是上图中兰色部分。
这个是单一的限制,你也可以对型别多重限制,如下:
class C<T extends Comparable<? super T> & Serializable> |
我们来分析以下这句,T extends Comparable这个是对上限的限制,Comparable< super T>这个是下限的限制,Serializable是第2个上限。一个指定的类型参数可以具有一个或多个上限。具有多重限制的类型参数可以用于访问它的每个 限制的方法和域。
2.4. 多态方法
//Code list 9 class TestGen { <T extends Object> public static List<T> make(T first) { return new List<T>(first); } } |
2007年5月8日星期二
-
初衷
最早开始接触面向对象的设计应该是在今年6月份皖北项目中。在这之前我做的程序可以说完全谈不上设计,更谈不上面向对象。大多是一些jsp页面,将数据从一个地方存到另一个地方而已。皖北的情况有所不同,当时用户对业务有需求,不是能够用jsp页面或者以某种方式移动数据所能解决的。用户的要求可以被理解为建立一个树状的模型,模型中的每一个上级节点的值是它所对应的下级节点的值的合计。为了实现这个需求,开始了第一次面向对象设计的实践。
面向对象的程序设计的核心原则是“开—闭”原则。开闭原则讲的是系统应该对扩充开对修改闭。这很好理解,就如同我们盖楼房,大家都盖3层。如果我的设计考虑到了日后的扩充问题,我按5层的标准设计。可想而知,如果日后房子不够了要增加层数,我就直接在3层上面加几层就可以了。而其他的人可能要把楼拆了重盖。
面向对象设计的其他原则有:里氏代换,依赖倒转等。而“开-闭”原则则是这些原则的核心,其他的一系列的原则好像都是为了实现开闭而提出的。包括很多,比如在设计的时候要使对象尽可能的内聚,同时减少耦合;要适当的抽象,复用的部分被提取到抽象类中;要尽可能的针对接口编程,或者说针对变化编程。而且封装的意义也已经扩大了,封装不仅仅指数据的隐藏而是任何形式的隐藏,事实上封装的是变化,也就是说系统变化的部分将会被封装。这说起来很简单,而实际上要求我们改变我们分析问题的思路和看待问题的视角,而所作的一切都是为了和开闭原则靠拢,最终的目的是设计出灵活的健壮的易于理解并且高效的系统。
设计模式的提出,很大程度上也是为了实现这个目标。提到模式我想起了我放假回家时打麻将的经历,牌桌上的朋友其实也不比我聪明,学历也没我高,可是我就是打不过他们,他们对于麻将的技巧十分的娴熟,甚至可以通过手中的牌和打出的牌来推测各家手中的情况。同时他们也有很多术语,什么“夹三条”,“边二万”等等。失败的原因在于,我一年几乎只打一次麻将,而他们几乎天天都在牌桌上。他们对于麻将的认识和我相比是有很大的区别的,他们了解很多“模式”不需要计算,直接从手中的牌就能看出怎么打,而我却需要很多计算,才能决定如何出牌,最终失败成必然。人天生就具有认识模式的能力,模式是一种针对特定系统的高级的抽象,而对于模式的理解与运用,只有通过实践才能最终纯熟。
既然提到了模式,我还想谈谈上帝模式。其实也许程序设计的最高境界就是这个上帝模式。在我们的系统中,有一个全能的上帝,它可以完成系统所需要的所有任务。其他的程序只需要向他“祈祷”(发送一个请求)就可以得到所需要的结果。当然,世界上没有全能的上帝,程序里面也没有,我所知道的系统,没有一个是这样设计的,也无法这样设计。事实上,软件设计模拟的是人类的意识,特别是人类的认识活动,分析活动。系统必然会被分解,形成很多概念,或者说是实体,也就是对象。我们总会从中去寻找我们所熟悉的东西,然后通过类比,分析,归纳等方式最终认识一个系统。认识的过程是反复的渐进的,程序设计也一样,这就是为什么要提出重构的原因。
-
ETL中间表导入程序
-
概述
-
我们主要要讲的是这个简单的etl中间表导入程序。这是淮北项目数据分析系统的一部分。这个程序的目的是从一张定义好的数据库中的表里面讲数据按照某一个规则到如到相关的事实表和维表中去。涉及到的主要问题有:
-
事务操作。
-
同步的增量问题。
-
实现不同的插入数据库方法,需要适当的抽象。
-
日志。
-
以恰当的方式制定规则。
这些问题是设计时要考虑的。
下图是系统最高抽象的一个类图:
图1
系统设计整体分为三个部分,分别是:
-
数据抽取部分
数据抽取部分的功能是将数据用源数据库抽取出来,当然这里源数据库中的数据是按照我们的要求存放的,也就是说是一种清洗过的数据。通过数据的抽取,生成系统的元数据(MetaData)。这一部分对应的类就是Generater。
-
数据包装过滤部分
系统的数据核心是MetaData(元数据),图一中的Container接口其实是一个数据容器,它和它里面包装的数据一起实现了系统的元数据。
-
数据操作部分
系统的操作核心是类MetaTransformer(元数据转换器),它其实是一个非常简单的类,他所描述的对象的功能也十分的简单,它的作用仅仅是把数据和操作连接起来。SourceType(源类型)是对数据源类型的一种抽象操作。针对不同的数据源类型,产生不同的操作。这种类型的区分,实际上是由操作人员指定的。对程序来说,它将按照一个固定的规则来产生SourceType的实现者。
下面我们对上面几个类的相互依赖做一个分析。上面这个类图是用eclipse的一个免费插件euml2分析出来的,这个插件尤其擅长分析依赖关系。在图一里面,除了MetaTransformer这个类以外,都是单向的依赖关系。类之间没有出现循环依赖。MetaTransformer之所以和很多类有依赖关系,是因为它本身就是承担连接数据和操作的责任,必须把数据与操作连接起来,所以它必须和数据部分以及操作部分关联。没有循环依赖,其实是一个很重要的设计细则,这样也十分有利与单元测试。
-
MetaData (元数据)
图2
元数据中保存了操作时所需要的主要数据。这样设计的目的是将数据提取和数据插入两部分的功能分开。同时让他们相互独立。在设计的时候有一个原则就是要尽可能的使程序的耦合性降低内聚性增高。其实就是讲要将程序中能够独立出来的功能尽可能的独立出来,这样做也是满足开闭原则的必要条件,如果你不这样做,程序耦合度极高,牵一发而动全身是不可能满足开闭原则的。
元数据是整个系统的一个核心,所有的操作都会围绕着它来进行。从设计上来看这个元数据类,好像就是《重构》里面提到的一种叫“纯稚的数据类”的坏味道。但我却不得不这样做,否则我的数据就会被打散放在数据库里了。所以我就是需要这么一个纯稚的数据类,这样也方便我操作数据。MetaData和它的容器Container一起构成了系统逻辑上的数据部分。这里有几点要注意,MetaData要设计的尽量简洁,内容越少越好,最好只有变化的数据和它的一个标识。而且它应该采用单例模式,保证它是唯一的,在系统的运行期间只有一个实例存在。
另外它有可能会非常的巨大,如果这样可能会对系统的健壮性有影响。因此在构建的时候应该有一个限制,目前这一部分功能没有实现。
这里面还要提到一下这个Container的抽象,Container抽象的目的主要是考虑到性能的问题。目前使用的数据容器是用Vector来实现的,众所周知,Vector的效率比较低下,但是使用起来顺手,如果考虑到性能的要求的话,这个数据容器还可以考虑用其他的方式来实现。当然这种抽象也应该通过一种配置由工厂方法来生成子类。但是这个也没有实现,所以这个抽象是不完整的。
另外还要说明的一点,就是MetaData这个对象是可以序列化出来的。借助jdk的功能,这个对象可以序列化成一个xml文件。这个功能没有严格的设计,实现的方法十分简单就是使用jdk里面的java.beans.XMLDecoder和 java.beans.XMLEncoder这两个类,其中一个是编码,一个是解码。通过他们可以把对象生成一个xml文档。
-
Generater (生成器)
图3
生成器的作用是将数据从一个源按照指定的规则抽取出来。如果纯粹是针对数据库操作的,其实也没有抽象的必要,这样设计其实是考虑如果需要从其他的数据源抽取的时候可以直接增加一个子类而不必改程序其他的部分。当然要实现这个目的至少还要配备一个工厂方法,以一种方式(通常是配置或者用户界面的指定)来决定到底是什么数据源。这个工厂方法没有实现,因为觉得出现这种需求的情况可能性比较小。所以其实这个抽象是聋子的耳朵——纯粹的摆设而已,没有什么实际的意义。
生成器里面有一个比较重要的操作就是要实现增量查询。我采用的方法是将源表中的时间戳序列化出来,成一个xml文档。这样做的坏处是较为复杂,好处是有了一个将源表中需要的数据导出的方法。另外仅操作源和目的无关,有助于减少系统之间不必要的耦合性。这里面还有一些设计的细节,比如不同的数据库对日期的操作是不同的,这里需要区分数据库,区分的方法采用webber.core里面的方法。就是通过分析配置文件配置项来产生多态。这里不细说了。
-
MetaTransformer (元数据转换器)
图4
数据转换的核心就是SourceType这个类型。它被设计为一个接口。因为我虽然知道源数据的类型,但是不能保证源数据如同我们想象的那么单纯,它有可能会比较复杂和特殊。所以我做了一个接口,这样可以保证SourceType类型在使用的时候系统其他部分不会受SourceType内部变化的影响。如果我们需要为SourceType增加一种功能,可以写一个实现这个接口的类就可以了,而系统的其他部分则不会受到影响。当然,由于SourceType的工厂方法负责根据条件产生它的子类,所以这个工厂方法也会受到影响,但总的来说,变化是可以控制的。不会扩散到系统的其他部分。
具体看一下这个抽象层次的设计。有一个抽象类SourceTypeDim用来描述维的操作。因为根据需求源数据的存放有三种可能:它是事实(SourceTypeDimBase)、他是维度(SourceTypeDimNone)、他是具有父子关系语意的维度(SourceTypeDimClass)。注意,这里使用了面向对象设计原则里面的依赖倒转原则,就是说复用的部分放在了抽象类里面。也就是说,元数据存放的三种可能所对应的三种操作其公用的部分被放在了抽象类里面。这样做的好处是减少冗余的代码,坏处是提高了抽象和具体的耦合性。所以这里面就有一个抽象层次划分的问题。我的设计明显少一个层次。应该设计为两个平行的抽象类,一个代表没有语意的维度描述,一个代表有语意的维度描述。但是由于有语意的描述只有一种情况,所以就暂时没有这么做。
具体什么时候实例化什么子类是由一个简单工厂方法SourceTypeDimFactory(参看图一)来决定的。它会读取配置文件,根据配置文件里面的描述来决定实例化那一个子类。
工厂方法——以上老是提到工厂方法。工厂方法是设计模式中的一种对象构建型模式,就是为生成子类所准备的。工厂方法的好处是把子类的产生变成一种规则,以后我们只要通过这种规则来构建子类,就不用担心其他的问题了。
例如:
这是我用的方法,为SourceType这个类型实例化子类。我是通过制定了一个类名的规则来实例化子类的。这是一种偷懒的很不好的方法。比较好的方法应该是通过配置文件,采用注册的机制,安全的产生子类。我这样做是因为这个继承关系十分的简单,也就是说SourceType这个类型的产品十分单一,特别的简单,所以工厂也相对简单了。
-
系统的其他组成
系统的其他组成主要有:XML描述文件、系统配置文件(未实现)、日志系统、系统全局常量、工具箱。
XML描述文件,用来描述数据源的特征,包括提取的sql语句,以及字段的含义。
系统配置文件,用来实现系统的一些配置,比如Container使用哪个子类,日志是否需要在后台黑窗中显示等。
日志系统,使用jdk提供的日志类,为系统增加日志。
系统全局常量,作用和配置文件一样,由于没有实现配置文件,就用它替代了配置文件。
工具箱,包含了一些需要的工具。比如一些转换,计算等等。
-
小结
在这篇文档里面,我提出了我对于面向对象程序设计的一些想法,这些想法很多都是幼稚的,不成熟的。虽然我也读了很多关于设计方面的书籍,但是我觉得程序的设计始终都是一种需要实践的艺术,动手比什么都重要。设计的核心其实还是人本身。一个系统设计的好还是坏,关键在于设计者对于系统的理解程度,没有理解就淡不上分析更谈不上设计。
正如我前面所谈到的人的认识过程是一个渐进的过程,所以重构在设计里面占一个极大的分量。上述的程序实际就是经过多次重构才最终成型的,而且目前也有很多的不足。因为这是一个客观的情况,所以我们在设计的时候切忌对象间的耦合度过高。耦合度越低越利于重构。同时我们也不要妄想一下子设计出一个完美的系统,毕竟人也不是上帝。此外还要学会利用别人已经完成的功能,在我的程序里面XML解析,日志生成都是使用了别人的成果。不要想着大包大揽,毕竟站在巨人的肩膀上才更容易成功。
2007年5月7日星期一
昨天晚上老婆又痛哭流涕了。原因是我有的时候非常狠心。原来我对她的伤害她不曾忘记,她对于我的不满在不断的升级。生活就是要不断的谅解,人不能总生活在过去的阴影里面。谁没有犯过错误?这个世界上没有完美无缺的人,因此我们也不能要求完美无缺。我就懂得这一点,我从来不要求什么完美无缺的。不断撕扯过去的伤口,那么人永远也不能恢复健康。健康的人才是有用的人。
其实这也没有什么,我有的时候觉得自己很对不住我的老婆。主要原因就是我不够强势。
其实我老婆这个人是一个普普通通的人我喜欢。她很敏感,容易受到伤害。
原谅别人是一种美德。不能原谅的话就要离开。如同能活着就活着,不能的话就去死。
嗓子剧痛无比,晚上反反复复的做这一个同样的梦。为什么让我这么痛苦?我从来不曾伤害别人。当然我知道这个不是理由。不爱自己就是一种罪过。
男人不能没有女人,如同女人不能没有男人一样。这是一个什么道理呢?不知道。我也不懂。就觉得这是一个规律。