|
2006年7月19日
何去何从?有点迷茫,是沉寂在温柔之乡还是去迎接狂风暴雨! 看来自己还是没有自信!bs..........myself
2007最新骗局:
建行一同志转送: 今天经过一栋大楼门口,门口有一提款机。有一个老伯,一直
看著我走过他身边,突然叫住我。他说他不识字,拿一张提款卡要我帮他在大楼门口
的自动提款机取钱。我回答我无法帮你取,叫警卫帮你。结果,他就回答我说不用了
,继续找其他路人帮他取钱。朋友们要记住---取款机可是有摄影机耶。万一他说我抢
劫或是偷他的提款卡,甚至他的卡片是偷来的,帮他领钱会在提款机留下影像,绝对
会让你百口莫辩!我会警惕 ! 是因为已有同事上当,目前仍官司缠身。显然这是诈骗
集团在找替身了! 请用力传出去~~~ 骗案真是层出不穷,一不小心就会踏入陷阱,真是
令人防不胜防!
提醒各位朋友在外多小心!
(2)一业主,家中突然断电,看到窗户外别人家里都有电,就出门查看自家电
表箱,打开门就被刀子顶着了——持刀入室抢劫伤人家里突然断电,不要贸然就开门
查看,有猫眼的多观察一会门外动静,没猫眼的也隔着门静听一段时间,没有异常响
动再开门。
(3)各位女同胞们注意了!这是最新骗局
女同胞请注意 男同胞请叫自己的朋友注意 新出的情况,女性朋友要特别注意啦:
一位上班的小姐在下班回家的路上看到一个小孩子一直哭,很可怜 ,然后就过去问那
小朋友怎么了.小朋友就跟那个小姐说: 我迷路了,可以请你带我回家吗?然后拿一张
纸条给她看, 说那是他家地址.然后她就笨笨的带小孩子去了.一般人都有同情心,然后
带到那个所谓小孩子的家里以后,她一按铃,门铃像是有高压电,就失去知觉了.隔天醒
来就被脱光光在一间空屋里,身边什么都没有了,她甚至连犯人长啥样子都没看见.所以
,现在人犯案都是利用同情心啊,如果遇到类似这种的,千万别带他去,要带就带他到派
出所去好了,走丢的小孩放到派出所一定没错啦,请通知身边所有女性,为了广大女士
的安全,看完后麻烦给转发给所有人。
(4)今天遇到讨饭新招,大家注意提防了~~
今天在家休息,有人按门铃,开门一看,是个50来岁的老妇女,手里拿了2包喜糖,我
还以为是邻居来分喜糖的,结果一开口,听得出不是本地人,她说什么这2包糖给我们
的,图个喜气,要换一点钱给她,后面还说了一大堆不知道什么,我也没听清楚,感
觉就是不对,吓的马上关门,晕!这年头,还有这么讨钱的一刚。
(5)转发:大家注意了!到自动取款机取钱时一定要倍加小心!!!!!
昨晚在工行自动取款机取钱时,后面来了个老妇女,问我能不能取钱,还说
什么取款机有个键可能坏了,旁边不知什么时候来了个小女孩,一直想我身边跻,我
也没在意,小孩子淘气嘛,可是过分的是她竟然把手朝出钞口放,准备拿我的钱了,
我感觉不对劲了,立即把她推到一边,等着把钱取出来。之后我想了一下,她们俩给
我设了个套:老妇女负责和我瞎聊,吸引我的注意力,小女孩趁我不注意时抢走我的
钱!如果我不防备的话,钱说不定就被抢走了,这样的话,我就进套了:(一则我立
即去追小女孩,去追回我的钱,可是谁又会相信一个小女孩能抢我一个大人的钱呢?
更可怕的是站在我后面的老妇女将会取光我卡中所有的钱,因为我的卡还在取款机里
面;二则我不立即去追小女孩,等拿到卡再追,到那时小女孩就无影无踪了,钱也就
没了啊:(她们真的很聪明,很可耻的!!!)
这是我的亲身经历,希望大家以后取钱时一定要警惕起来,注意观察周围的所有人,
并转告周围的家人、同事、朋友,让坏蛋分子没有可乘之机!!!!又出现骗局新招
!!
(6)我父母都退休在家。昨天上午,来一陌生中年人,说自己摩托车油开没了,加油
站太远,摩托车又太重推不动,所以想问我父母要一个可乐瓶去买汽油,刚开口就说
实在不行就出2、3元买一个空瓶好了。我母亲就拿了个空瓶给他,别说他还真从口袋
里掏出钱来,不过是几张百元大钞,还让我父母找钱。我母亲顿生警觉,说算了,不
过是一个空瓶而已。他10元钱买下来,只不过还是那张百元大钞。好在我母
亲尚未龙钟,也不是那种爱贪小便宜的人
女性朋友一定要认真看完,注意自我安全啊,现在万恶的社会。。。。朋友发给我一
篇报道,现转给各位看看 ,出门在外,千万小心,小心千万。。。
(7)一对新婚夫妇到巴黎度蜜月。在巴黎,妻子在一间时尚服装店试衣服 ,身为丈夫
就在试衣间外等候。但等候多时却不见妻子走出来 ,紧张的丈夫要求店员帮忙到里头
查看 ,却意外发现试衣间空无一人。丈夫以为妻子开玩笑作弄人 ,要他紧张.于是回到
酒店等她回来。几小时后却不见妻子的踪影,才知事态严重。丈夫赶忙报警 ,并到巴黎
所有服装店和医院询问妻子下落。三星期过去了,妻子犹如从人间蒸发,音讯全无,伤
心的丈夫只能收拾包袱回到 ** 。由于无法从绝望中振作,丈夫无心工作,甚至独自生
活 ,决定把自己放逐,流浪到各地方。几年后 ,他心血来潮到巴厘岛,在一破旧的屋子
参观一畸形秀 ( freak show ) 。他见到一脏生锈的铁笼里,有一女人四肢全无,身躯,
包括脸部,犹如破布般残破 ,充满疤痕。她在地上扭曲着 ,并发出有如野兽般的呻吟声
。突然间男人惊恐地发出尖叫声。他从那毫无人样的女人脸上见到,他再熟悉不过,属
于他新婚不久就告失踪的妻子脸上的红色胎记。
(8)另一版本则发生在上海。几年前一女通知公安她的表妹在上海市集购物时无故失
踪,可是遍寻不着 ,直到五年后一友人撞见这表妹在泰国曼谷街道上行乞。恐怖的是她
不知何故没了双手双脚,身子被铁链绑在灯柱旁。
(9)这是在某一对夫妇去香港游玩时发生的故事。一对夫妻不知不觉走入了全香港治
安最坏的地区的一家精品店里 ? 妻子对店里的衣服样式十分喜欢 ,随后就进入试衣间
试衣。可是,先生在外头等了又等,却不见妻子出来。由于实在是等太久了 ,所以先生
开门 进去找她,可是试衣间里早已空空如也。他吃惊地向店员询问妻子到哪里去了,可
是店员们却好象是串通好了一样,都说没有看见,并坚持根本没有象他妻子这样的人来
过店里。因此他只好请当地的警察协助搜索这家精品店 ,可是却一无所获。后来他又
一个人找了一段时间 ,直到他的签证到期。最后不得已他就在找不到妻子的情况下回
国了。之后经过了一年 … 他向公司请了一段长假,再一次回到香港去找他的妻子。他
带着妻子的相片走遍香港的大街小巷,但这次仍是一点线索也没有。终于假期就要结束
了,他身心疲惫地开始考虑要回国的时候 ,有一天无意间经过了一间珍奇小屋。小屋的
看板上写着 : 达磨(不倒翁) 虽然他对珍奇事物并不感兴趣 ,但由于连日疲劳他想
让自己改变一下心情 , 加上看板上写着 达磨的文字也引起他的兴趣。最后他决定
进去瞧瞧。但是他不该进去的!因为珍奇小屋里面展出一件令他惨不忍睹的东 … 小屋
里的舞台上有一位手脚都被切断的全裸女性被当成花瓶一样摆在那里 ! 这位女性的舌
头已经被拔掉了,不断发出奇怪的呻吟声。看到这么恶心的东西真令他恨不得马上拔腿
就跑 ,但不知为什么他心里感受到一股奇怪的气氛 ,于是他又重新仔细看那女人的面
孔 … 没错!这女人正是他一年前失踪的妻子。后来,他向当地的黑道支付庞大的赎金
换取妻子的剩下的躯干。但一切都太迟了,他可怜的妻子早就已经疯了。现在她还住在
国内某家医院,继续不断地发出奇怪的呻吟声 …
(10)最近有人告诉我,他的朋友在晚上听到门口有婴儿在哭,不过当时已很晚了,
而且她认为这件事很奇怪,于是她打电话给警察。警察告诉她∶ 「无论如何,绝对不
要开 门。」这位女士表示那声音听起来象是婴儿爬到窗户附近哭,她担心婴儿会爬到
街上,被车子碾过。警察告诉她∶我们已派人前往,无论如何不能开门。警方认为这
是一个连续杀人犯,利用婴儿哭声的录音带,诱使女性以为有人在外面遗弃婴儿,她
们出门察看。虽然尚未证实此事,但是警方已接到许多女性打电话来说,他们晚上独
自在家时,听到门外有婴儿的哭声,请将这个消息传给其他人,不要因为听到婴儿的
哭声而开门。
请严肃看待这贴子!有这么离谱!小心为妙!!!
以前听说大活人现场失踪,后来被卖到马戏团、被卖器官什么的,只当是天方夜谭。
结果真的看到发生在真实中的恐怖故事。
(11)事情是同事群发邮件告知的。她的朋友,简称小a吧,上周和两个女孩子,简称
小b和小c,去逛罗湖商业城。罗湖商业城是深圳假货集散地,龙蛇混杂,紧靠深圳火
车站和香港的罗湖口岸,人流量非常大。话说小c内急,就去上卫生间,小a和小b在洗
手间外面等。等了很久很久,还是不见小c出来,两个人有点奇怪了。于是两个人进去
催她。谁知道进去一看,人影全无。两个人倒竖一口冷气,打手机也没人接。一个大
活人,难道就这么活不见人死不见尸的失踪了?于是赶紧报警。 警察来了,问情事情
经过以后,说了一句令人无比毛骨悚然的话,“你们有没有看见其他可疑的人进去?
”,两个人再三回忆,没有。因为不可能带着一个活生生的100多斤的人出来,而她们
不注意。这时候小a突然想起来,其间有个清洁工打扮的人推着一辆清洁小车进去、接
着又出来…… 警察告诉他们,这种事情已经不是第一次发生,现在深圳警方初步怀疑
一个犯罪团伙,有组织地在管理疏松的低档商业城,利用人们,尤其是女性对清洁工
没有防范意识的心理,进行有组织地绑架、贩卖人体器官犯罪。别忘了,罗湖商业城
离香港和深圳火车站有多么地近。现在已经几天过去了,那个可怜的小c姑娘,仍是活
不见人死不见尸。我的同事说,小a,也就是我同事的朋友,仍在等小c的消息。但是
很可能,也许如果幸运的话,活着的小c会被扔在哪个角落,只是失去了她的肾,但是
,更有可能的是,也许再过几天或者几个月、几年,小c的头颅和躯体、四肢会在深圳
的城乡结合部的垃圾堆被人发现。如果看到这个发生在身边的活生生的恐怖事件,请
转告身边的女性亲友,一定小心防范清洁工打扮的人,因为他/她很可能会趁你不注意
把你敲晕,放进清洁车拉走,接下来等着你的是无比恐怖的活人分尸。
远程方法调用入门指南(Java RMI Tutorial)
远程方法调用入门指南
Copyright © 2005 Stephen Suen. All rights reserved.
Java 远程方法调用(Remote Method Invocation, RMI)使得运行在一个 Java 虚拟机(Java Virtual Machine, JVM)的对象可以调用运行另一个 JVM 之上的其他对象的方法,从而提供了程序间进行远程通讯的途径。RMI 是 J2EE 的很多分布式技术的基础,比如 RMI-IIOP 乃至 EJB。本文是 RMI 的一个入门指南,目的在于帮助读者快速建立对 Java RMI 的一个感性认识,以便进行更深层次的学习。事实上,如果你了解 RMI 的目的在于更好的理解和学习 EJB,那么本文就再合适不过了。通过本文所了解的 RMI 的知识和技巧,应该足够服务于这个目的了。
本文的最新版本将发布在程序员咖啡馆网站上(建设中)。欢迎订阅我们的邮件组,以获得关于本文的正式发布及更新信息。
全文在保证完整性,且保留全部版权声明(包括上述链接)的前提下可以在任意媒体转载——须保留此标注。
我们知道远程过程调用(Remote Procedure Call, RPC)可以用于一个进程调用另一个进程(很可能在另一个远程主机上)中的过程,从而提供了过程的分布能力。Java 的 RMI 则在 RPC 的基础上向前又迈进了一步,即提供分布式 对象间的通讯,允许我们获得在远程进程中的对象(称为远程对象)的引用(称为远程引用),进而通过引用调用远程对象的方法,就好像该对象是与你的客户端代码同样运行在本地进程中一样。RMI 使用了术语"方法"(Method)强调了这种进步,即在分布式基础上,充分支持面向对象的特性。
RMI 并不是 Java 中支持远程方法调用的唯一选择。在 RMI 基础上发展而来的 RMI-IIOP(Java Remote Method Invocation over the Internet Inter-ORB Protocol),不但继承了 RMI 的大部分优点,并且可以兼容于 CORBA。J2EE 和 EJB 都要求使用 RMI-IIOP 而不是 RMI。尽管如此,理解 RMI 将大大有助于 RMI-IIOP 的理解。所以,即便你的兴趣在 RMI-IIOP 或者 EJB,相信本文也会对你很有帮助。另外,如果你现在就对 API 感兴趣,那么可以告诉你,RMI 使用 java.rmi 包,而 RMI-IIOP 则既使用 java.rmi 也使用扩展的 javax.rmi 包。
本文的随后内容将仅针对 Java RMI。
在学习 RMI 之前,我们需要了解一些基础知识。首先需要了解所谓的分布式对象(Distributed Object)。分布式对象是指一个对象可以被远程系统所调用。对于 Java 而言,即对象不仅可以被同一虚拟机中的其他客户程序(Client)调用,也可以被运行于其他虚拟机中的客户程序调用,甚至可以通过网络被其他远程主机之上的客户程序调用。
下面的图示说明了客户程序是如何调用分布式对象的:
从图上我们可以看到,分布式对象被调用的过程是这样的:
-
客户程序调用一个被称为 Stub (有时译作存根,为了不产生歧义,本文将使用其英文形式)的客户端代理对象。该代理对象负责对客户端隐藏网络通讯的细节。Stub 知道如何通过网络套接字(Socket)发送调用,包括如何将调用参数转换为适当的形式以便传输等。
-
Stub 通过网络将调用传递到服务器端,也就是分布对象一端的一个被称为 Skeleton 的代理对象。同样,该代理对象负责对分布式对象隐藏网络通讯的细节。Skeleton 知道如何从网络套接字(Socket)中接受调用,包括如何将调用参数从网络传输形式转换为 Java 形式等。
-
Skeleton 将调用传递给分布式对象。分布式对象执行相应的调用,之后将返回值传递给 Skeleton,进而传递到 Stub,最终返回给客户程序。
这个场景基于一个基本的法则,即行为的定义和行为的具体实现相分离。如图所示,客户端代理对象 Stub 和分布式对象都实现了相同的接口,该接口称为远程接口(Remote Interface)。正是该接口定义了行为,而分布式对象本身则提供具体的实现。对于 Java RMI 而言,我们用接口(interface)定义行为,用类(class)定义实现。
RMI 的底层架构由三层构成:
-
首先是 Stub/Skeleton 层。该层提供了客户程序和服务程序彼此交互的接口。
-
然后是远程引用(Remote Reference)层。这一层相当于在其之上的 Stub/Skeleton 层和在其之下的传输协议层之前的中间件,负责处理远程对象引用的创建和管理。
-
最后是传输协议(Transport Protocol) 层。该层提供了数据协议,用以通过线路传输客户程序和远程对象间的请求和应答。
这些层之间的交互可以参照下面的示意图:
和其它分布式对象机制一样,Java RMI 的客户程序使用客户端的 Stub 向远程对象请求方法调用;服务器对象则通过服务器端的 Skeleton 接受请求。我们深入进去,来看看其中的一些细节。
注意: 事实上,在 Java 1.2 之后,RMI 不再需要 Skeleton 对象,而是通过 Java 的反射机制(Reflection)来完成对服务器端的远程对象的调用。为了便于说明问题,本文以下内容仍然基于 Skeleton 来讲解。
当客户程序调用 Stub 时,Stub 负责将方法的参数转换为序列化(Serialized)形式,我们使用一个特殊的术语,即编列(Marshal)来指代这个过程。编列的目的是将这些参数转换为可移植的形式,从而可以通过网络传输到远程的服务对象一端。不幸的是,这个过程没有想象中那么简单。这里我们首先要理解一个经典的问题,即方法调用时,参数究竟是传值还是传引用呢?对于 Java RMI 来说,存在四种情况,我们将分别加以说明。
-
对于基本的原始类型(整型,字符型等等),将被自动的序列化,以传值的方式编列。
-
对于 Java 的对象,如果该对象是可序列化的(实现了 java.io.Serializable 接口),则通过 Java 序列化机制自动地加以序列化,以传值的方式编列。对象之中包含的原始类型以及所有被该对象引用,且没有声明为 transient 的对象也将自动的序列化。当然,这些被引用的对象也必须是可序列化的。
-
绝大多数内建的 Java 对象都是可序列化的。 对于不可序列化的 Java 对象(java.io.File 最典型),或者对象中包含对不可序列化,且没有声明为 transient 的其它对象的引用。则编列过程将向客户程序抛出异常,而宣告失败。
-
客户程序可以调用远程对象,没有理由禁止调用参数本身也是远程对象(实现了 java.rmi.Remote 接口的类的实例)。此时,RMI 采用一种模拟的传引用方式(当然不是传统意义的传引用,因为本地对内存的引用到了远程变得毫无意义),而不是将参数直接编列复制到远程。这种情况下,交互的双方发生的戏剧性变化值得我们注意。参数是远程对象,意味着该参数对象可以远程调用。当客户程序指定远程对象作为参数调用服务器端远程对象的方法时,RMI 的运行时机制将向服务器端的远程对象发送作为参数的远程对象的一个 Stub 对象。这样服务器端的远程对象就可以回调(Callback)这个 Stub 对象的方法,进而调用在客户端的远程对象的对应方法。通过这种方法,服务器端的远程对象就可以修改作为参数的客户端远程对象的内部状态,这正是传统意义的传引用所具备的特性。是不是有点晕?这里的关键是要明白,在分布式环境中,所谓服务器和客户端都是相对的。被请求的一方就是服务器,而发出请求的一方就是客户端。
在调用参数的编列过程成功后,客户端的远程引用层从 Stub 那里获得了编列后的参数以及对服务器端远程对象的远程引用(参见 java.rmi.server.RemoteRef API)。该层负责将客户程序的请求依据底层的 RMI 数据传输协议转换为传输层请求。在 RMI 中,有多种的可能的传输机制,比如点对点(Point-to-Point)以及广播(Multicast)等。不过,在当前的 JMI 版本中只支持点对点协议,即远程引用层将生成唯一的传输层请求,发往指定的唯一远程对象(参见 java.rmi.server.UnicastRemoteObject API)。
在服务器端,服务器端的远程引用层接收传输层请求,并将其转换为对远程对象的服务器端代理对象 Skeleton 的调用。Skeleton 对象负责将请求转换为对实际的远程对象的方法调用。这是通过与编列过程相对的反编列(Unmarshal)过程实现的。所有序列化的参数被转换为 Java 形式,其中作为参数的远程对象(实际上发送的是远程引用)被转换为服务器端本地的 Stub 对象。
如果方法调用有返回值或者抛出异常,则 Skeleton 负责编列返回值或者异常,通过服务器端的远程引用层,经传输层传递给客户端;相应地,客户端的远程引用层和 Stub 负责反编列并最终将结果返回给客户程序。
整个过程中,可能最让人迷惑的是远程引用层。这里只要明白,本地的 Stub 对象是如何产生的,就不难理解远程引用的意义所在了。远程引用中包含了其所指向的远程对象的信息,该远程引用将用于构造作为本地代理对象的 Stub 对象。构造后,Stub 对象内部将维护该远程引用。真正在网络上传输的实际上就是这个远程引用,而不是 Stub 对象。
在 RMI 的基本架构之上,RMI 提供服务与分布式应用程序的一些对象服务,包括对象的命名/注册(Naming/Registry)服务,远程对象激活(Activation)服务以及分布式垃圾收集(Distributed Garbage Collection, DGC)。作为入门指南,本文将指介绍其中的命名/注册服务,因为它是实战 RMI 所必备的。其它内容请读者自行参考其它更加深入的资料。
在前一节中,如果你喜欢刨根问底,可能已经注意到,客户端要调用远程对象,是通过其代理对象 Stub 完成的,那么 Stub 最早是从哪里得来的呢?RMI 的命名/注册服务正是解决这一问题的。当服务器端想向客户端提供基于 RMI 的服务时,它需要将一个或多个远程对象注册到本地的 RMI 注册表中(参见java.rmi.registry.Registry API)。每个对象在注册时都被指定一个将来用于客户程序引用该对象的名称。客户程序通过命名服务(参见 java.rmi.Naming API),指定类似 URL 的对象名称就可以获得指向远程对象的远程引用。在 Naming 中的 lookup() 方法找到远程对象所在的主机后,它将检索该主机上的 RMI 注册表,并请求所需的远程对象。如果注册表发现被请求的远程对象,它将生成一个对该远程对象的远程引用,并将其返回给客户端,客户端则基于远程引用生成相应的 Stub 对象,并将引用传递给调用者。之后,双方就可以按照我们前面讲过的方式进行交互了。
注意: RMI 命名服务提供的 Naming 类并不是你的唯一选择。RMI 的注册表可以与其他命名服务绑定,比如 JNDI,这样你就可以通过 JNDI 来访问 RMI 的注册表了。
理论离不开实践,理解 RMI 的最好办法就是通过例子。开发 RMI 的分布式对象的大体过程包括如下几步:
-
定义远程接口。这一步是通过扩展 java.rmi.Remote 接口,并定义所需的业务方法实现的。
-
定义远程接口的实现类。即实现上一步所定义的接口,给出业务方法的具体实现逻辑。
-
编译远程接口和实现类,并通过 RMI 编译器 rmic 基于实现类生成所需的 Stub 和 Skeleton 类。
RMI 中各个组件之间的关系如下面这个示意图所示:
回忆我们上一节所讲的,Stub 和 Skeleton 负责代理客户和服务器之间的通讯。但我们并不需要自己生成它们,相反,RMI 的编译器 rmic 可以帮我们基于远程接口和实现类生成这些类。当客户端对象通过命名服务向服务器端的 RMI 注册表请求远程对象时,RMI 将自动构造对应远程对象的 Skeleton 实例对象,并通过 Skeleton 对象将远程引用返回给客户端。在客户端,该远程引用将用于构造 Stub 类的实例对象。之后,Stub 对象和 Skeleton 对象就可以代理客户对象和远程对象之间的交互了。
我们的例子展现了一个简单的应用场景。服务器端部署了一个计算引擎,负责接受来自客户端的计算任务,在服务器端执行计算任务,并将结果返回给客户端。客户端将发送并调用计算引擎的计算任务实际上是计算指定精度的 π 值。
定义远程接口与非分布式应用中定义接口的方法没有太多的区别。只要遵守下面两个要求:
注意: 在 Java 1.2 之前,上面关于抛出异常的要求更严格,即必须抛出 java.rmi.RemoteExcption,不允许类似 java.io.IOException 这样的超类。现在之所以放宽了这一要求,是希望可以使定义既可以用于远程对象,也可以用于本地对象的接口变得容易一些(想想 EJB 中的本地接口和远程接口)。当然,这并没有使问题好多少,你还是必须声明异常。不过,一种观点认为这不是问题,强制声明异常可以使开发人员保持清醒的头脑,因为远程对象和本地对象在调用时传参的语意是不同的。本地对象是传引用,而远程对象主要是传值,这意味对参数内部状态的修改产生的结果是不同的。
对于第一个要求,java.rmi.Remote 接口实际上没有任何方法,而只是用作标记接口。RMI 的运行环境依赖该接口判断对象是否是远程对象。第二个要求则是因为分布式应用可能发生任何问题,比如网络问题等等。
例 1
列出了我们的远程接口定义。该接口只有一个方法:executeTask() 用以执行指定的计算任务,并返回相应的结果。注意,我们用后缀 Remote 表明接口是远程接口。
例 1. ComputeEngineRemote 远程接口
package rmitutorial;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface ComputeEngineRemote extends Remote {
public Object executeTask(Task task) throws RemoteException;
}
例 2
列出了计算任务接口的定义。该接口也只有一个方法:execute() 用以执行实际的计算逻辑,并返回结果。注意,该接口不是远程接口,所以没有扩展 java.rmi.Remote 接口;其方法也不必抛出 java.rmi.RemoteException 异常。但是,因为它将用作远程方法的参数,所以扩展了 java.io.Serializable 接口。
例 2. Task 接口
package rmitutorial;
import java.io.Serializable;
public interface Task extends Serializable {
Object execute();
}
接下来,我们将实现前面定义的远程接口。例 3给出了实现的源代码。
例 3. ComputeEngine 实现
package rmitutorial;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class ComputeEngine extends UnicastRemoteObject
implements ComputeEngineRemote {
public ComputeEngine() throws RemoteException {
super();
}
public Object executeTask(Task task) throws RemoteException {
return task.execute();
}
}
类 ComputeEngine 实现了之前定义的远程接口,同时继承自 java.rmi.server.UnicastRemoteObject 超类。UnicastRemoteObject 类是一个便捷类,它实现了我们前面所讲的基于 TCP/IP 的点对点通讯机制。远程对象都必须从该类扩展(除非你想自己实现几乎所有 UnicastRemoteObject 的方法)。在我们的实现类的构造函数中,调用了超类的构造函数(当然,即使你不显式的调用这个构建函数,它也一样会被调用。这里这样做,只是为了突出强调这种调用而已)。该构造函数的最重要的意义就是调用 UnicastRemoteObject 类的 exportObject() 方法。导出(Export)对象是指使远程对象准备就绪,可以接受进来的调用的过程。而这个过程的最重要内容就是建立服务器套接字,监听特定的端口,等待客户端的调用请求。
为了让客户程序可以找到我们的远程对象,就需要将我们的远程对象注册到 RMI 的注册表。这个过程有时被称为"引导"过程(Bootstrap)。我们将为此编写一个独立的引导程序负责创建和注册远程对象。例 4 给出了引导程序的源代码。
例 4. 引导程序
package rmitutorial;
import java.rmi.Naming;
import java.rmi.RMISecurityManager;
public class Bootstrap {
public static void main(String[] args) throws Exception {
String name = "ComputeEngine";
ComputeEngine engine = new ComputeEngine();
System.out.println("ComputerEngine exported");
Naming.rebind(name, engine);
System.out.println("ComputeEngine bound");
}
}
可以看到,我们首先创建了一个远程对象(同时导出了该对象),之后将该对象绑定到 RMI 注册表中。Naming 的 rebind() 方法接受一个 URL 形式的名字作绑定之用。其完整格式如下:
protocol://host:port/object
其中,协议(Protocol)默认为 rmi;主机名默认为 localhost;端口默认为 1099。注意,JDK 中提供的默认 Naming 实现只支持 rmi 协议。在我们的引导程序里面只给出了对象绑定的名字,而其它部分均使用缺省值。
例 5
给出了我们的客户端程序。该程序接受两个参数,分别是远程对象所在的主机地址和希望获得的 π 值的精度。
例 5. Client.java
package rmitutorial;
import java.math.BigDecimal;
import java.rmi.Naming;
public class Client {
public static void main(String args[]) throws Exception {
String name = "rmi://" + args[0] + "/ComputeEngine";
ComputeEngineRemote engineRemote =
(ComputeEngineRemote)Naming.lookup(name);
Pi task = new Pi(Integer.parseInt(args[1]));
BigDecimal pi = (BigDecimal)(engineRemote.executeTask(task));
System.out.println(pi);
}
}
例 6. Pi.java
package rmitutorial;
import java.math.*;
public class Pi implements Task {
private static final BigDecimal ZERO =
BigDecimal.valueOf(0);
private static final BigDecimal ONE =
BigDecimal.valueOf(1);
private static final BigDecimal FOUR =
BigDecimal.valueOf(4);
private static final int roundingMode =
BigDecimal.ROUND_HALF_EVEN;
private int digits;
public Pi(int digits) {
this.digits = digits;
}
public Object execute() {
return computePi(digits);
}
public static BigDecimal computePi(int digits) {
int scale = digits + 5;
BigDecimal arctan1_5 = arctan(5, scale);
BigDecimal arctan1_239 = arctan(239, scale);
BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
arctan1_239).multiply(FOUR);
return pi.setScale(digits,
BigDecimal.ROUND_HALF_UP);
}
public static BigDecimal arctan(int inverseX,
int scale) {
BigDecimal result, numer, term;
BigDecimal invX = BigDecimal.valueOf(inverseX);
BigDecimal invX2 =
BigDecimal.valueOf(inverseX * inverseX);
numer = ONE.divide(invX, scale, roundingMode);
result = numer;
int i = 1;
do {
numer =
numer.divide(invX2, scale, roundingMode);
int denom = 2 * i + 1;
term =
numer.divide(BigDecimal.valueOf(denom),
scale, roundingMode);
if ((i % 2) != 0) {
result = result.subtract(term);
} else {
result = result.add(term);
}
i++;
} while (term.compareTo(ZERO) != 0);
return result;
}
}
编译我们的示例程序和编译其它非分布式的应用没什么区别。只是编译之后,需要使用 RMI 编译器,即 rmic 生成所需 Stub 和 Skeleton 实现。使用 rmic 的方式是将我们的远程对象的实现类(不是远程接口)的全类名作为参数来运行 rmic 命令。参考下面的示例:
E:\classes\rmic rmitutorial.ComputeEngine
编译之后将生成 rmitutorial.ComputeEngine_Skel 和 rmitutorial.ComputeEngine_Stub 两个类。
远程对象的引用通常是通过 RMI 的注册表服务以及 java.rmi.Naming 接口获得的。远程对象需要导出(注册)相应的远程引用到注册表服务,之后注册表服务就可以监听并服务于客户端对远程对象引用的请求。标准的 Sun Java SDK 提供了一个简单的 RMI 注册表服务程序,即 rmiregistry 用于监听特定的端口,等待远程对象的注册,以及客户端对这些远程对象引用的检索请求。
在运行我们的示例程序之前,首先要启动 RMI 的注册表服务。这个过程很简单,只要直接运行 rmiregistry 命令即可。缺省的情况下,该服务将监听 1099 端口。如果需要指定其它的监听端口,可以在命令行指定希望监听的端口(如果你指定了其它端口,需要修改示例程序以适应环境)。如果希望该程序在后台运行,在 Unix 上可以以如下方式运行(当然,可以缺省端口参数):
$ rmiregistry 1099 &
在 Windows 操作系统中可以这样运行:
C:\> start rmiregistry 1099
我们的 rmitutorial.Bootstrap 类将用于启动远程对象,并将其绑定在 RMI 注册表中。运行该类后,远程对象也将进入监听状态,等待来自客户端的方法调用请求。
$ java rmitutorial.Bootstrap
ComputeEngine exported
ComputeEngine bound
启动远程对象后,打开另一个命令行窗口,运行客户端。命令行的第一个参数为 RMI 注册表的地址,第二个参数为期望的 π 值精度。参考下面的示例:
$ java rmitutorial.Client localhost 50
3.14159265358979323846264338327950288419716939937511
在演示示例程序时,我们实际上是在同一主机上运行的服务器和客户端,并且无论是服务器和客户端所需的类都在相同的类路径上,可以同时被服务器和客户端所访问。这忽略了 Java RMI 的一个重要细节,即动态类装载。因为 RMI 的特性(包括其它几个特性)并不适用于 J2EE 的 RMI-IIOP 和 EJB 技术,所以,本文将不作详细介绍,请读者自行参考本文给出的参考资料。不过,为了让好奇的读者不至于过分失望,这里简单介绍一下动态类装载的基本思想。
RMI 运行时系统采用动态类装载机制来装载分布式应用所需的类。如果你可以直接访问应用所涉及的所有包括服务器端客户端在内的主机,并且可以把分布式应用所需的所有类都安装在每个主机的 CLASSPATH 中(上面的示例就是极端情况,所有的东西都在本地主机),那么你完全不必关心 RMI 类装载的细节。显然,既然是分布式应用,情况往往正相反。对于 RMI 应用,客户端需要装载客户端自身所需的类,将要调用的远程对象的远程接口类以及对应的 Stub 类;服务器端则要装载远程对象的实现类以及对应的 Skeleton 类(Java 1.2 之后不需要 Skeleton 类)。RMI 在处理远程调用涉及的远程引用,参数以及返回值时,可以将一个指定的 URL 编码到流中。交互的另一端可以通过 该 URL 获得处理这些对象所需的类文件。这一点类似于 Applet 中的 CODEBASE 的概念,交互的两端通过 HTTP 服务器发布各自控制的类,允许交互的另一端动态下载这些类。以我们的示例为例,客户端不必部署 ComputeEngine_Stub 的类文件,而可以通过服务器端的 HTTP 服务器获得类文件。同样,服务器端也不需要客户端实现的定制任务 Pi 的类文件。
注意,这种动态类装载将需要交互的两端加载定制的安全管理器(参见 java.rmi.RMISecurityManager API),以及对应的策略文件。
-
The Java™ Tutorial Trail:RMI
-
David Flanagan, Jim Farley, William Crawford and Kris Magnusson, 1999, ISBN 1-56592-483-5E, O'Reilly, Java™ Enterprise in a Nutshell
-
Ed Roman, Scott Ambler and Tyler Jewell 2002, ISBN 0-471-41711-4, John Wiley &Sons, Inc., Matering Enterprise JavaBeans™ , Second Edition
正则表达式(转载)
关键词: 正则表达式 模式匹配 Javascript
关键字:正则表达式 模式匹配 Javascript 摘要:收集一些常用的正则表达式。 正则表达式用于字符串处理,表单验证等场合,实用高效,但用到时总是不太把握,以致往往要上网查一番。我将一些常用的表达式收藏在这里,作备忘之用。本贴随时会更新。 匹配中文字符的正则表达式: [\u4e00-\u9fa5] 匹配双字节字符(包括汉字在内):[^\x00-\xff] 应用:计算字符串的长度(一个双字节字符长度计2,ASCII字符计1) String.prototype.len=function(){return this.replace([^\x00-\xff]/g,"aa").length;} 匹配空行的正则表达式:\n[\s| ]*\r 匹配HTML标记的正则表达式:/<(.*)>.*<\/\1>|<(.*) \/>/ 匹配首尾空格的正则表达式:(^\s*)|(\s*$) String.prototype.trim = function() { return this.replace(/(^\s*)|(\s*$)/g, ""); } 利用正则表达式分解和转换IP地址: 下面是利用正则表达式匹配IP地址,并将IP地址转换成对应数值的Javascript程序: function IP2V(ip) { re=/(\d+)\.(\d+)\.(\d+)\.(\d+)/g //匹配IP地址的正则表达式 if(re.test(ip)) { return RegExp.$1*Math.pow(255,3))+RegExp.$2*Math.pow(255,2))+RegExp.$3*255+RegExp.$4*1 } else { throw new Error("Not a valid IP address!") } } 不过上面的程序如果不用正则表达式,而直接用split函数来分解可能更简单,程序如下: var ip="10.100.20.168" ip=ip.split(".") alert("IP值是:"+(ip[0]*255*255*255+ip[1]*255*255+ip[2]*255+ip[3]*1)) 匹配Email地址的正则表达式:\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)* 匹配网址URL的正则表达式:http://([\w-]+\.)+[\w-]+(/[\w- ./?%&=]*)? 利用正则表达式去除字串中重复的字符的算法程序:[注:此程序不正确,原因见本贴回复]
var s="abacabefgeeii" var s1=s.replace(/(.).*\1/g,"$1") var re=new RegExp("["+s1+"]","g") var s2=s.replace(re,"") alert(s1+s2) //结果为:abcefgi
我原来在CSDN上发贴寻求一个表达式来实现去除重复字符的方法,最终没有找到,这是我能想到的最简单的实现方法。思路是使用后向引用取出包括重复的字符,再以重复的字符建立第二个表达式,取到不重复的字符,两者串连。这个方法对于字符顺序有要求的字符串可能不适用。 得用正则表达式从URL地址中提取文件名的javascript程序,如下结果为page1 s="http://www.9499.net/page1.htm" s=s.replace(/(.*\/){0,}([^\.]+).*/ig,"$2") alert(s) 利用正则表达式限制网页表单里的文本框输入内容: 用正则表达式限制只能输入中文:onkeyup="value=value.replace(/[^\u4E00-\u9FA5]/g,'')" onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\u4E00-\u9FA5]/g,''))" 用正则表达式限制只能输入全角字符: onkeyup="value=value.replace(/[^\uFF00-\uFFFF]/g,'')" onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\uFF00-\uFFFF]/g,''))" 用正则表达式限制只能输入数字:onkeyup="value=value.replace(/[^\d]/g,'') "onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\d]/g,''))" 用正则表达式限制只能输入数字和英文:onkeyup="value=value.replace(/[\W]/g,'') "onbeforepaste="clipboardData.setData('text',clipboardData.getData('text').replace(/[^\d]/g,''))"
摘要: javascript小技巧
事件源对象
event.srcElement.tagName event.srcElement.type
捕获释放 event.srcElement.setCapture(); event... 阅读全文
Java Excel是一开放源码项目,通过它Java开发人员可以读取Excel文件的内容、创建新的Excel文件、更新已经存在的Excel文件。使用该API非Windows操作系统也可以通过纯Java应用来处理Excel数据表。因为是使用Java编写的,所以我们在Web应用中可以通过JSP、Servlet来调用API实现对Excel数据表的访问。
现在发布的稳定版本是V2.0,提供以下功能:
从Excel 95、97、2000等格式的文件中读取数据; 读取Excel公式(可以读取Excel 97以后的公式); 生成Excel数据表(格式为Excel 97); 支持字体、数字、日期的格式化; 支持单元格的阴影操作,以及颜色操作; 修改已经存在的数据表; 现在还不支持以下功能,但不久就会提供了:
不能够读取图表信息; 可以读,但是不能生成公式,任何类型公式最后的计算值都可以读出; 应用示例
1 从Excel文件读取数据表
Java Excel API既可以从本地文件系统的一个文件(.xls),也可以从输入流中读取Excel数据表。读取Excel数据表的第一步是创建Workbook(术语:工作薄),下面的代码片段举例说明了应该如何操作:(完整代码见ExcelReading.java)
import java.io.*; import jxl.*; … … … … try { //构建Workbook对象, 只读Workbook对象 //直接从本地文件创建Workbook //从输入流创建Workbook InputStream is = new FileInputStream(sourcefile); jxl.Workbook rwb = Workbook.getWorkbook(is); } catch (Exception e) { e.printStackTrace(); }
一旦创建了Workbook,我们就可以通过它来访问Excel Sheet(术语:工作表)。参考下面的代码片段:
//获取第一张Sheet表 Sheet rs = rwb.getSheet(0);
我们既可能通过Sheet的名称来访问它,也可以通过下标来访问它。如果通过下标来访问的话,要注意的一点是下标从0开始,就像数组一样。
一旦得到了Sheet,我们就可以通过它来访问Excel Cell(术语:单元格)。参考下面的代码片段:
//获取第一行,第一列的值 Cell c00 = rs.getCell(0, 0); String strc00 = c00.getContents();
//获取第一行,第二列的值 Cell c10 = rs.getCell(1, 0); String strc10 = c10.getContents();
//获取第二行,第二列的值 Cell c11 = rs.getCell(1, 1); String strc11 = c11.getContents();
System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType()); System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType()); System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());
如果仅仅是取得Cell的值,我们可以方便地通过getContents()方法,它可以将任何类型的Cell值都作为一个字符串返回。示例代码中Cell(0, 0)是文本型,Cell(1, 0)是数字型,Cell(1,1)是日期型,通过getContents(),三种类型的返回值都是字符型。
如果有需要知道Cell内容的确切类型,API也提供了一系列的方法。参考下面的代码片段:
String strc00 = null; double strc10 = 0.00; Date strc11 = null;
Cell c00 = rs.getCell(0, 0); Cell c10 = rs.getCell(1, 0); Cell c11 = rs.getCell(1, 1);
if(c00.getType() == CellType.LABEL) { LabelCell labelc00 = (LabelCell)c00; strc00 = labelc00.getString(); } if(c10.getType() == CellType.NUMBER) { NmberCell numc10 = (NumberCell)c10; strc10 = numc10.getValue(); } if(c11.getType() == CellType.DATE) { DateCell datec11 = (DateCell)c11; strc11 = datec11.getDate(); }
System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType()); System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType()); System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());
在得到Cell对象后,通过getType()方法可以获得该单元格的类型,然后与API提供的基本类型相匹配,强制转换成相应的类型,最后调用相应的取值方法getXXX(),就可以得到确定类型的值。API提供了以下基本类型,与Excel的数据格式相对应,如下图所示:
每种类型的具体意义,请参见Java Excel API Document。
当你完成对Excel电子表格数据的处理后,一定要使用close()方法来关闭先前创建的对象,以释放读取数据表的过程中所占用的内存空间,在读取大量数据时显得尤为重要。参考如下代码片段:
//操作完成时,关闭对象,释放占用的内存空间 rwb.close();
Java Excel API提供了许多访问Excel数据表的方法,在这里我只简要地介绍几个常用的方法,其它的方法请参考附录中的Java Excel API Document。
Workbook类提供的方法
1. int getNumberOfSheets() 获得工作薄(Workbook)中工作表(Sheet)的个数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); int sheets = rwb.getNumberOfSheets();
2. Sheet[] getSheets() 返回工作薄(Workbook)中工作表(Sheet)对象数组,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); Sheet[] sheets = rwb.getSheets();
3. String getVersion() 返回正在使用的API的版本号,好像是没什么太大的作用。
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); String apiVersion = rwb.getVersion();
Sheet接口提供的方法
1) String getName() 获取Sheet的名称,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); jxl.Sheet rs = rwb.getSheet(0); String sheetName = rs.getName();
2) int getColumns() 获取Sheet表中所包含的总列数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); jxl.Sheet rs = rwb.getSheet(0); int rsColumns = rs.getColumns();
3) Cell[] getColumn(int column) 获取某一列的所有单元格,返回的是单元格对象数组,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); jxl.Sheet rs = rwb.getSheet(0); Cell[] cell = rs.getColumn(0);
4) int getRows() 获取Sheet表中所包含的总行数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); jxl.Sheet rs = rwb.getSheet(0); int rsRows = rs.getRows();
5) Cell[] getRow(int row) 获取某一行的所有单元格,返回的是单元格对象数组,示例子:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); jxl.Sheet rs = rwb.getSheet(0); Cell[] cell = rs.getRow(0);
6) Cell getCell(int column, int row) 获取指定单元格的对象引用,需要注意的是它的两个参数,第一个是列数,第二个是行数,这与通常的行、列组合有些不同。
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile)); jxl.Sheet rs = rwb.getSheet(0); Cell cell = rs.getCell(0, 0);
2 生成新的Excel工作薄
下面的代码主要是向大家介绍如何生成简单的Excel工作表,在这里单元格的内容是不带任何修饰的(如:字体,颜色等等),所有的内容都作为字符串写入。(完整代码见ExcelWriting.java)
与读取Excel工作表相似,首先要使用Workbook类的工厂方法创建一个可写入的工作薄(Workbook)对象,这里要注意的是,只能通过API提供的工厂方法来创建Workbook,而不能使用WritableWorkbook的构造函数,因为类WritableWorkbook的构造函数为protected类型。示例代码片段如下:
import java.io.*; import jxl.*; import jxl.write.*; … … … … try { //构建Workbook对象, 只读Workbook对象 //Method 1:创建可写入的Excel工作薄 jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile));
//Method 2:将WritableWorkbook直接写入到输出流 /* OutputStream os = new FileOutputStream(targetfile); jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(os); */ } catch (Exception e) { e.printStackTrace(); }
API提供了两种方式来处理可写入的输出流,一种是直接生成本地文件,如果文件名不带全路径的话,缺省的文件会定位在当前目录,如果文件名带有全路径的话,则生成的Excel文件则会定位在相应的目录;另外一种是将Excel对象直接写入到输出流,例如:用户通过浏览器来访问Web服务器,如果HTTP头设置正确的话,浏览器自动调用客户端的Excel应用程序,来显示动态生成的Excel电子表格。
接下来就是要创建工作表,创建工作表的方法与创建工作薄的方法几乎一样,同样是通过工厂模式方法获得相应的对象,该方法需要两个参数,一个是工作表的名称,另一个是工作表在工作薄中的位置,参考下面的代码片段:
//创建Excel工作表 jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0);
"这锅也支好了,材料也准备齐全了,可以开始下锅了!",现在要做的只是实例化API所提供的Excel基本数据类型,并将它们添加到工作表中就可以了,参考下面的代码片段:
//1.添加Label对象 jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell"); ws.addCell(labelC);
//添加带有字型Formatting的对象 jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true); jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf); jxl.write.Label labelCF = new jxl.write.Label(1, 0, "This is a Label Cell", wcfF); ws.addCell(labelCF);
//添加带有字体颜色Formatting的对象 jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false, Underlinestyle.NO_UNDERLINE, jxl.format.Colour.RED); jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc); jxl.write.Label labelCFC = new jxl.write.Label(1, 0, "This is a Label Cell", wcfFC); ws.addCell(labelCF);
//2.添加Number对象 jxl.write.Number labelN = new jxl.write.Number(0, 1, 3.1415926); ws.addCell(labelN);
//添加带有formatting的Number对象 jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##"); jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf); jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN); ws.addCell(labelNF);
//3.添加Boolean对象 jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false); ws.addCell(labelB);
//4.添加DateTime对象 jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date()); ws.addCell(labelDT);
//添加带有formatting的DateFormat对象 jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss"); jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df); jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF); ws.addCell(labelDTF);
这里有两点大家要引起大家的注意。第一点,在构造单元格时,单元格在工作表中的位置就已经确定了。一旦创建后,单元格的位置是不能够变更的,尽管单元格的内容是可以改变的。第二点,单元格的定位是按照下面这样的规律(column, row),而且下标都是从0开始,例如,A1被存储在(0, 0),B1被存储在(1, 0)。
最后,不要忘记关闭打开的Excel工作薄对象,以释放占用的内存,参见下面的代码片段:
//写入Exel工作表 wwb.write();
//关闭Excel工作薄对象 wwb.close();
这可能与读取Excel文件的操作有少少不同,在关闭Excel对象之前,你必须要先调用write()方法,因为先前的操作都是存储在缓存中的,所以要通过该方法将操作的内容保存在文件中。如果你先关闭了Excel对象,那么只能得到一张空的工作薄了。
3 拷贝、更新Excel工作薄
接下来简要介绍一下如何更新一个已经存在的工作薄,主要是下面二步操作,第一步是构造只读的Excel工作薄,第二步是利用已经创建的Excel工作薄创建新的可写入的Excel工作薄,参考下面的代码片段:(完整代码见ExcelModifying.java)
//创建只读的Excel工作薄的对象 jxl.Workbook rw = jxl.Workbook.getWorkbook(new File(sourcefile));
//创建可写入的Excel工作薄对象 jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile), rw); //读取第一张工作表 jxl.write.WritableSheet ws = wwb.getSheet(0);
//获得第一个单元格对象 jxl.write.WritableCell wc = ws.getWritableCell(0, 0); //判断单元格的类型, 做出相应的转化 if(wc.getType() == CellType.LABEL) { Label l = (Label)wc; l.setString("The value has been modified."); }
//写入Excel对象 wwb.write();
//关闭可写入的Excel对象 wwb.close();
//关闭只读的Excel对象 rw.close();
之所以使用这种方式构建Excel对象,完全是因为效率的原因,因为上面的示例才是API的主要应用。为了提高性能,在读取工作表时,与数据相关的一些输出信息,所有的格式信息,如:字体、颜色等等,是不被处理的,因为我们的目的是获得行数据的值,既使没有了修饰,也不会对行数据的值产生什么影响。唯一的不利之处就是,在内存中会同时保存两个同样的工作表,这样当工作表体积比较大时,会占用相当大的内存,但现在好像内存的大小并不是什么关键因素了。
一旦获得了可写入的工作表对象,我们就可以对单元格对象进行更新的操作了,在这里我们不必调用API提供的add()方法,因为单元格已经于工作表当中,所以我们只需要调用相应的setXXX()方法,就可以完成更新的操作了。
尽单元格原有的格式化修饰是不能去掉的,我们还是可以将新的单元格修饰加上去,以使单元格的内容以不同的形式表现。
新生成的工作表对象是可写入的,我们除了更新原有的单元格外,还可以添加新的单元格到工作表中,这与示例2的操作是完全一样的。
最后,不要忘记调用write()方法,将更新的内容写入到文件中,然后关闭工作薄对象,这里有两个工作薄对象要关闭,一个是只读的,另外一个是可写入的。
以上摘自IBM网站
使用JXL读取Excel表格,拷贝、更新Excel工作薄
|
xymiser 原创 (参与分:41669,专家分:2761) 发表:2006-01-18 22:11 版本:1.0 阅读:1666次 |
|
/** * <p>读取Excel表格,拷贝、更新Excel工作薄 </p> * <p>Description: 可以读取Excel文件的内容,更新Excel工作薄 * </p> * <p>Copyright: Copyright (c) Corparation 2005</p> * <p>程序开发环境为eclipse</p> * @author Walker * @version 1.0 */ package cn.com.yitong.xls;
import java.io.File; import java.io.FileInputStream; import java.io.InputStream; import java.util.Vector;
import cn.com.yitong.ChartImg; import cn.com.yitong.VireObj; import cn.com.yitong.platform.log.YTLogger;
import jxl.CellType; import jxl.Workbook; import jxl.format.CellFormat; import jxl.format.Colour; import jxl.format.UnderlineStyle; import jxl.write.Formula; import jxl.write.Label; import jxl.write.Number; import jxl.write.WritableCell; import jxl.write.WritableCellFormat; import jxl.write.WritableFont; import jxl.write.WritableImage; import jxl.write.WritableSheet; import jxl.write.WritableWorkbook; import jxl.write.WriteException; import jxl.write.biff.RowsExceededException;
public class XLSDemo { private static final int TITLE_LENGTH = 7; private static final int SHEET_WIDTH = 32; private static final int SHEET_HEIGHT = 116; /** * 创建Excel */ private void makeXls() { Workbook workbook = null; try { // 构建Workbook对象, 只读Workbook对象 // 直接从本地文件创建Workbook, 从输入流创建Workbook InputStream ins = new FileInputStream("D:/Workspace/testproj/source.xls"); workbook = Workbook.getWorkbook(ins);
// 利用已经创建的Excel工作薄创建新的可写入的Excel工作薄 File outFile = new File("D:/Workspace/testproj/test.xls"); WritableWorkbook wwb = Workbook.createWorkbook(outFile, workbook); // 读取第一张工作表 WritableSheet dataSheet = wwb.getSheet(0); // 设置冻结单元格 dataSheet.getSettings().setVerticalFreeze(7); dataSheet.getSettings().setHorizontalFreeze(2); // 测试模拟数据 Vector vecData = new Vector(); for(int i = 0; i < 50; i ++) { VireObj obj = new VireObj(); obj.setOrgNo("00" + i + "0"); obj.setOrgName("机构" + (i + 1)); obj.setOpenAcc((int)(100 * Math.random())); obj.setDestoryAcc((int)(10 * Math.random())); obj.setTotalAcc((int)(500 * Math.random())); obj.setMonthInCount((int)(500 * Math.random())); obj.setMonthInMoney(500 * Math.random()); obj.setMonthOutCount((int)(500 * Math.random())); obj.setMonthOutMoney(500 * Math.random()); vecData.add(obj); } // 插入数据 insertData(wwb, dataSheet, vecData); // 插入模拟图像数据 Vector vecImg = new Vector(); for(int i = 0; i < 3; i ++) { ChartImg img = new ChartImg(); img.setImgTitle("图像" + (i + 1)); img.setImgName("D:/Workspace/testproj/images/barchart.png"); vecImg.add(img); } // 插入图表 insertImgsheet(wwb, vecImg); //写入Excel对象 wwb.write(); wwb.close(); } catch (Exception e) { YTLogger.logDebug(e); } finally { // 操作完成时,关闭对象,释放占用的内存空间 workbook.close(); } } /** * 插入数据 * @param wwb WritableWorkbook : 工作簿 * @param dataSheet WritableSheet : 工作表 * @throws RowsExceededException * @throws WriteException */ private void insertData(WritableWorkbook wwb, WritableSheet dataSheet, Vector vecData) throws RowsExceededException, WriteException { // 获得标题单元格对象 modiStrCell(dataSheet, 2, 0, "工商银行江苏省分行 个人网上银行业务种类/开销户明细报表(2005-12)", null); // 修改数据单元格数据 for(int i = 0; i < vecData.size(); i ++) { VireObj obj = (VireObj)vecData.get(i); modiStrCell(dataSheet, 0, TITLE_LENGTH + i, obj.getOrgNo(), null); modiStrCell(dataSheet, 1, TITLE_LENGTH + i, obj.getOrgName(), null); modiNumCell(dataSheet, 2, TITLE_LENGTH + i, obj.getOpenAcc(), null); modiNumCell(dataSheet, 3, TITLE_LENGTH + i, obj.getDestoryAcc(), null); modiNumCell(dataSheet, 4, TITLE_LENGTH + i, obj.getTotalAcc(), null); modiNumCell(dataSheet, 5, TITLE_LENGTH + i, obj.getMonthInCount(), null); modiNumCell(dataSheet, 6, TITLE_LENGTH + i, obj.getTotalInMoney(), null); modiNumCell(dataSheet, 7, TITLE_LENGTH + i, obj.getMonthOutCount(), null); modiNumCell(dataSheet, 8, TITLE_LENGTH + i, obj.getMonthOutMoney(), null); } // 删除空行 for (int j = vecData.size() + TITLE_LENGTH; j < SHEET_HEIGHT; j++) { dataSheet.removeRow(vecData.size() + TITLE_LENGTH); } // 插入公式 for(int i = 2; i < SHEET_WIDTH; i ++) { modiFormulaCell(dataSheet, i, vecData.size() + TITLE_LENGTH, 8, vecData.size() + TITLE_LENGTH, null); } }
/** * 修改字符单元格的值 * @param dataSheet WritableSheet : 工作表 * @param col int : 列 * @param row int : 行 * @param str String : 字符 * @param format CellFormat : 单元格的样式 * @throws RowsExceededException * @throws WriteException */ private void modiStrCell(WritableSheet dataSheet, int col, int row, String str, CellFormat format) throws RowsExceededException, WriteException { // 获得单元格对象 WritableCell cell = dataSheet.getWritableCell(col, row); // 判断单元格的类型, 做出相应的转化 if (cell.getType() == CellType.EMPTY) { Label lbl = new Label(col, row, str); if(null != format) { lbl.setCellFormat(format); } else { lbl.setCellFormat(cell.getCellFormat()); } dataSheet.addCell(lbl); } else if (cell.getType() == CellType.LABEL) { Label lbl = (Label)cell; lbl.setString(str); } else if (cell.getType() == CellType.NUMBER) { // 数字单元格修改 Number n1 = (Number)cell; n1.setValue(42.05); } } /** * 修改数字单元格的值 * @param dataSheet WritableSheet : 工作表 * @param col int : 列 * @param row int : 行 * @param num double : 数值 * @param format CellFormat : 单元格的样式 * @throws RowsExceededException * @throws WriteException */ private void modiNumCell(WritableSheet dataSheet, int col, int row, double num, CellFormat format) throws RowsExceededException, WriteException { // 获得单元格对象 WritableCell cell = dataSheet.getWritableCell(col, row); // 判断单元格的类型, 做出相应的转化 if (cell.getType() == CellType.EMPTY) { Number lbl = new Number(col, row, num); if(null != format) { lbl.setCellFormat(format); } else { lbl.setCellFormat(cell.getCellFormat()); } dataSheet.addCell(lbl); } else if (cell.getType() == CellType.NUMBER) { // 数字单元格修改 Number lbl = (Number)cell; lbl.setValue(num); } else if (cell.getType() == CellType.LABEL) { Label lbl = (Label)cell; lbl.setString(String.valueOf(num)); } } /** * 修改公式单元格的值 * @param dataSheet WritableSheet : 工作表 * @param col int : 列 * @param row int : 行 * @param startPos int : 开始位置 * @param endPos int : 结束位置 * @param format * @throws RowsExceededException * @throws WriteException */ private void modiFormulaCell(WritableSheet dataSheet, int col, int row, int startPos, int endPos, CellFormat format) throws RowsExceededException, WriteException { String f = getFormula(col, row, startPos, endPos); // 插入公式(只支持插入,不支持修改) WritableCell cell = dataSheet.getWritableCell(col, row); if (cell.getType() == CellType.EMPTY) { // 公式单元格 Formula lbl = new Formula(col, row, f); if(null != format) { lbl.setCellFormat(format); } else { lbl.setCellFormat(cell.getCellFormat()); } dataSheet.addCell(lbl); } else if (cell.getType() == CellType.STRING_FORMULA) { YTLogger.logWarn("Formula modify not supported!"); } } /** * 得到公式 * @param col int : 列 * @param row int : 行 * @param startPos int : 开始位置 * @param endPos int : 结束位置 * @return String * @throws RowsExceededException * @throws WriteException */ private String getFormula(int col, int row, int startPos, int endPos) throws RowsExceededException, WriteException { char base = 'A'; char c1 = base; StringBuffer formula = new StringBuffer(128); // 组装公式 formula.append("SUM("); if (col <= 25) { c1 = (char) (col % 26 + base); formula.append(c1).append(startPos).append(":") .append(c1).append(endPos).append(")"); } else if (col > 25) { char c2 = (char) ((col - 26) / 26 + base); c1 = (char) ((col - 26) % 26 + base); formula.append(c2).append(c1).append(startPos).append(":") .append(c2).append(c1).append(endPos).append(")"); }
return formula.toString(); } /** * 插入图表工作表 * @param wwb WritableWorkbook : 工作簿 * @param vecImg Vector : 图像链表 * @throws RowsExceededException * @throws WriteException */ private void insertImgsheet(WritableWorkbook wwb, Vector vecImg) throws RowsExceededException, WriteException { // 插入图像 WritableSheet imgSheet; if((wwb.getSheets()).length < 2) { imgSheet = wwb.createSheet("图表", 1); } else { imgSheet = wwb.getSheet(1); } for (int i = 0; i < vecImg.size(); i++) { ChartImg chart = (ChartImg) vecImg.get(i); // 插入图像标题 Label lbl = new Label(0, 2 + 20 * i, chart.getImgTitle()); WritableFont font = new WritableFont(WritableFont.ARIAL, WritableFont.DEFAULT_POINT_SIZE, WritableFont.NO_BOLD, false, UnderlineStyle.NO_UNDERLINE, Colour.DARK_BLUE2); WritableCellFormat background = new WritableCellFormat(font); background.setWrap(true); background.setBackground(Colour.GRAY_25); imgSheet.mergeCells(0, 2 + 20 * i, 9, 2 + 20 * i); lbl.setCellFormat(background); imgSheet.addCell(lbl); // 插入图像单元格 insertImgCell(imgSheet, 2, 4 + 20 * i, 8, 15, chart.getImgName()); } }
/** * 插入图像到单元格(图像格式只支持png) * @param dataSheet WritableSheet : 工作表 * @param col int : 列 * @param row int : 行 * @param width int : 宽 * @param height int : 高 * @param imgName String : 图像的全路径 * @throws RowsExceededException * @throws WriteException */ private void insertImgCell(WritableSheet dataSheet, int col, int row, int width, int height, String imgName) throws RowsExceededException, WriteException { File imgFile = new File(imgName); WritableImage img = new WritableImage(col, row, width, height, imgFile); dataSheet.addImage(img); } /** * 测试 * @param args */ public static void main(String[] args) { XLSDemo demo = new XLSDemo(); demo.makeXls(); } } |
|
jxl不错,简单易用
import jxl.*; import jxl.write.*; import java.io.*; import java.io.File.*; import java.util.*;
public class excel { public static void main(String[] args) {
String targetfile = "c:/out.xls";//输出的excel文件名 String worksheet = "List";//输出的excel文件工作表名 String[] title = {"ID","NAME","DESCRIB"};//excel工作表的标题
WritableWorkbook workbook; try { //创建可写入的Excel工作薄,运行生成的文件在tomcat/bin下 //workbook = Workbook.createWorkbook(new File("output.xls")); System.out.println("begin");
OutputStream os=new FileOutputStream(targetfile); workbook=Workbook.createWorkbook(os);
WritableSheet sheet = workbook.createSheet(worksheet, 0); //添加第一个工作表 //WritableSheet sheet1 = workbook.createSheet("MySheet1", 1); //可添加第二个工作 /* jxl.write.Label label = new jxl.write.Label(0, 2, "A label record"); //put a label in cell A3, Label(column,row) sheet.addCell(label); */
jxl.write.Label label; for (int i=0; i<title.length; i++) { //Label(列号,行号 ,内容 ) label = new jxl.write.Label(i, 0, title[i]); //put the title in row1 sheet.addCell(label); }
//下列添加的对字体等的设置均调试通过,可作参考用
//添加数字 jxl.write.Number number = new jxl.write.Number(3, 4, 3.14159); //put the number 3.14159 in cell D5 sheet.addCell(number);
//添加带有字型Formatting的对象 jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES,10,WritableFont.BOLD,true); jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf); jxl.write.Label labelCF = new jxl.write.Label(4,4,"文本",wcfF); sheet.addCell(labelCF);
//添加带有字体颜色,带背景颜色 Formatting的对象 jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL,10,WritableFont.BOLD,false,jxl.format.UnderlineStyle.NO_UNDERLINE,jxl.format.Colour.RED); jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc); wcfFC.setBackground(jxl.format.Colour.BLUE); jxl.write.Label labelCFC = new jxl.write.Label(1,5,"带颜色",wcfFC); sheet.addCell(labelCFC);
//添加带有formatting的Number对象 jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##"); jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf); jxl.write.Number labelNF = new jxl.write.Number(1,1,3.1415926,wcfN); sheet.addCell(labelNF);
//3.添加Boolean对象 jxl.write.Boolean labelB = new jxl.write.Boolean(0,2,false); sheet.addCell(labelB);
//4.添加DateTime对象 jxl.write.DateTime labelDT = new jxl.write.DateTime(0,3,new java.util.Date()); sheet.addCell(labelDT);
//添加带有formatting的DateFormat对象 jxl.write.DateFormat df = new jxl.write.DateFormat("ddMMyyyyhh:mm:ss"); jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df); jxl.write.DateTime labelDTF = new jxl.write.DateTime(1,3,new java.util.Date(),wcfDF); sheet.addCell(labelDTF);
//和宾单元格 //sheet.mergeCells(int col1,int row1,int col2,int row2);//左上角到右下角 sheet.mergeCells(4,5,8,10);//左上角到右下角 wfc = new jxl.write.WritableFont(WritableFont.ARIAL,40,WritableFont.BOLD,false,jxl.format.UnderlineStyle.NO_UNDERLINE,jxl.format.Colour.GREEN); jxl.write.WritableCellFormat wchB = new jxl.write.WritableCellFormat(wfc); wchB.setAlignment(jxl.format.Alignment.CENTRE); labelCFC = new jxl.write.Label(4,5,"单元合并",wchB); sheet.addCell(labelCFC); //
//设置边框 jxl.write.WritableCellFormat wcsB = new jxl.write.WritableCellFormat(); wcsB.setBorder(jxl.format.Border.ALL,jxl.format.BorderLineStyle.THICK); labelCFC = new jxl.write.Label(0,6,"边框设置",wcsB); sheet.addCell(labelCFC); workbook.write(); workbook.close(); }catch(Exception e) { e.printStackTrace(); } System.out.println("end"); Runtime r=Runtime.getRuntime(); Process p=null; //String cmd[]={"notepad","exec.java"}; String cmd[]={"C:\\Program Files\\Microsoft Office\\Office\\EXCEL.EXE","out.xls"}; try{ p=r.exec(cmd); } catch(Exception e){ System.out.println("error executing: "+cmd[0]); }
} }
Java中合并XML文档的设计与实现
作者: 凌宗虎 李先国
出处: 计算机与信息技术
责任编辑: 方舟
[ 2005-06-09 08:39 ]
摘 要:介绍了XML应用中合并XML 文档的方法与应用,在基于XML的应用中,有着广泛的应用前景。 关键词:XML文档 解析器 元素 在XML应用中,最常用也最实用的莫过于 XML文件的读写。由于XML语义比较严格,起始标记必须配对,所以合并XML文档并不像合并普通文件那样简单。在JAVA中,如何合并XML文档,下面介绍一种方法。 设计思想 应用 javax.xml.parsers包中的解析器解析得到两个XML文件的根元素,再采用 递归的方式逐一复制被合并文件的元素。 实现过程 为了读写XML文件,需要 导入如下JAVA包,"//"后为注释说明,笔者的环境是 JDK 1.3.1,在JDK 1.4.0中测试也通过。 Import java.io. *; //Java基础包,包含各种IO操作 Import java.util. *; //Java基础包,包含各种标准数据结构操作 Import javax.xml.parsers. *; //XML解析器接口 Import org.w3c.dom. *; //XML的DOM实现 import org.apache.crimson.tree.XmlDocument;//写XML文件要用到 Import javax.xml.transform. *; Import javax.xml.transform.dom. *; Import javax.xml.transform.stream. *; |
下面介绍合并XML文档的过程。先说明一下各个方法的作用。方法 is Merging()有两个 参数(分别是目标XML 文件名和被合并的XML文件名),调用JAVA的解析器,获得两个要合并的XML文档的Document结构和根元素,并调用方法duplicate()和方法write To()。当然,在XML文档的合并过程中,可以加入另外的一些判断条件,比如,当被合并XML文档不存在时,将如何处理,等等。 Private Boolean is Merging (String mainFileName, String sub Filename) throws Exception { Boolean isOver = false; DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); Document Builder db = null; Try { Db = dbf.newDocumentBuilder (); } Catch (ParserConfigurationException pce) { System.err.println(pce); //出现异常时,输出异常信息 } Document doc_main = null,doc_vice = null; //获取两个XML文件的Document。 Try { Doc_main = db.parse (mainFileName); Doc_vice = db.parse (sub Filename); } Catch (DOM Exception dom) { System.err.println (dom.getMessage ()); } Catch (Exception ioe) { System.err.println (ioe); } //获取两个文件的根元素。 Element root_main = doc_main.getDocumentElement (); Element root_vice = doc_vice.getDocumentElement (); //下面添加被合并文件根节点下的每个元素 Novelist message Items = root_vice.getChildNodes (); Int item_number = messageItems.getLength (); //如果去掉根节点下的第一个元素,比如<所属管理系统> ,那么i从3开始。否则i从1开始。 For (int i=1; i < item_number; i=i+2 ) { //调用dupliate(),依次复制被合并XML文档中根节点下的元素。 Element messageItem = (Element) messageItems.item (i); IsOver = dupliate (doc_main, root_main, messageItem); } //调用 write To(),将合并得到的Document写入目标XML文档。 Boolean isWritten = write To (doc_main, mainFileName); Return isOver && isWritten; } |
方法dupliate ()有三个参数(分别是目标XML文档的Document,目标XML文档中要添加节点的父节点和被合并XML文档的复制节点),采用递归的形式,将一个XML文档中的元素复制到另一个XML文档中。 Private Boolean dupliate (Document doc_dup, Element father, Element son) throws Exception { Boolean is done = false; String son_name = son.getNodeName (); Element sub ITEM = doc_dup.createElement (son_name); //复制节点的属性 If (son.hasAttributes ()){ NamedNodeMap attributes = son.getAttributes (); For (int i=0; i < attributes.getLength () ; i ++){ String attribute_name = attributes. Item (i). GetNodeName (); String attribute_value = attributes. Item (i). GetNodeValue (); SubITEM.setAttribute (attribute_name, attribute_value); } } Father.appendChild (sub ITEM); //复制节点的值 Text value son = (Text) son.getFirstChild (); String nodevalue_root = ""; If (value_son! = null && value_son.getLength () > 0) nodevalue_root = (String) value_son.getNodeValue (); Text valuenode_root = null; If ((nodevalue_root! = null)&&(nodevalue_root.length () > 0)) valuenode_root = doc_dup.createTextNode (nodevalue_root); If (valuenode_root! = null && valuenode_root.getLength () > 0) subITEM.appendChild (valuenode_root); //复制子结点 Novelist sub_messageItems = son.getChildNodes (); int sub_item_number = sub_messageItems.getLength(); if (sub_item_number < 2){ //如果没有子节点,则返回 Is done = true; } Else { For (int j = 1; j < sub_item_number; j=j+2) { //如果有子节点,则递归调用本方法 Element sub_messageItem = (Element) sub_messageItems.item (j); Is done = dupliate (doc_dup, subITEM, sub_messageItem); } } Return is done; } |
方法writeTo()有两个参数(分别是目标XML文档的Document和文件名),将所得目标XML文档写入文件。 Private Boolean write To (Document doc, String fileName) throws Exception { Boolean isOver = false; DOM Source doms = new DOM Source (doc); File f = new File (fileName); Stream Result sr = new Stream Result (f); Try { Transformer Factory tf=TransformerFactory.newInstance (); Transformer t=tf.newTransformer (); Properties properties = t.getOutputProperties (); Properties.setProperty (OutputKeys.ENCODING,"GB2312"); T.setOutputProperties (properties); T.transform (doms, sr); IsOver = true; } Catch (TransformerConfigurationException tce) { Tce.printStackTrace (); } Catch (Transformer Exception te) { Te.printStackTrace (); } Return isOver; } |
最后使用测试函数进行测试。对于两个已经存在的XML文件(比如,存在文件D:/a.xml和D:/b.xml,要将b.xml合并到a.xml中),可以测试如下: Public static void main (String [] args) throws Exception { Boolean is done = is Merging ("D:/a.xml","D:/b.xml"); If (is Done) System.out.println ("XML files have been merged."); Else System.out.println ("XML files have NOT been merged."); } | 总结 本文介绍了如何利用JAVA中的XML解析器,合并两个XML文档。当然,在合并的过程中,还可以加入其他的约束条件,比如要求过滤掉特定的元素等。另外,复制元素的插入位置也可以加以限制。
在应用中加入全文检索功能 ——基于Java的全文索引引擎Lucene简介
作者: 车东 Email: chedongATbigfoot.com/chedongATchedong.com
写于:2002/08 最后更新:
02/22/2006 14:42:55 Feed Back >> (Read this before you ask question)
版权声明:可以任意转载,转载时请务必以超链接形式标明文章原始出处和作者信息及本声明 http://www.chedong.com/tech/lucene.html
关键词:Lucene java full-text search engine Chinese word segment
内容摘要:
Lucene是一个基于Java的全文索引工具包。
-
基于Java的全文索引引擎Lucene简介:关于作者和Lucene的历史
-
全文检索的实现:Luene全文索引和数据库索引的比较
-
中文切分词机制简介:基于词库和自动切分词算法的比较
-
具体的安装和使用简介:系统结构介绍和演示
-
Hacking Lucene:简化的查询分析器,删除的实现,定制的排序,应用接口的扩展
-
从Lucene我们还可以学到什么
基于Java的全文索引/检索引擎——Lucene
Lucene不是一个完整的全文索引应用,而是是一个用Java写的全文索引引擎工具包,它可以方便的嵌入到各种应用中实现针对应用的全文索引/检索功能。
Lucene的作者:Lucene的贡献者Doug Cutting是一位资深全文索引/检索专家,曾经是V-Twin搜索引擎(Apple的Copland操作系统的成就之一)的主要开发者,后在Excite担任高级系统架构设计师,目前从事于一些INTERNET底层架构的研究。他贡献出的Lucene的目标是为各种中小型应用程序加入全文检索功能。
Lucene的发展历程:早先发布在作者自己的www.lucene.com,后来发布在SourceForge,2001年年底成为APACHE基金会jakarta的一个子项目:http://jakarta.apache.org/lucene/
已经有很多Java项目都使用了Lucene作为其后台的全文索引引擎,比较著名的有:
对于中文用户来说,最关心的问题是其是否支持中文的全文检索。但通过后面对于Lucene的结构的介绍,你会了解到由于Lucene良好架构设计,对中文的支持只需对其语言词法分析接口进行扩展就能实现对中文检索的支持。
全文检索的实现机制
Lucene的API接口设计的比较通用,输入输出结构都很像数据库的表==>记录==>字段,所以很多传统的应用的文件、数据库等都可以比较方便的映射到Lucene的存储结构/接口中。总体上看:可以先把Lucene当成一个支持全文索引的数据库系统。
比较一下Lucene和数据库:
Lucene |
数据库 |
索引数据源:doc(field1,field2...) doc(field1,field2...) \ indexer / _____________ | Lucene Index| -------------- / searcher \ 结果输出:Hits(doc(field1,field2) doc(field1...))
|
索引数据源:record(field1,field2...) record(field1..) \ SQL: insert/ _____________ | DB Index | ------------- / SQL: select \ 结果输出:results(record(field1,field2..) record(field1...))
|
Document:一个需要进行索引的“单元” 一个Document由多个字段组成 |
Record:记录,包含多个字段 |
Field:字段 |
Field:字段 |
Hits:查询结果集,由匹配的Document组成 |
RecordSet:查询结果集,由多个Record组成 |
全文检索 ≠ like "%keyword%"
通常比较厚的书籍后面常常附关键词索引表(比如:北京:12, 34页,上海:3,77页……),它能够帮助读者比较快地找到相关内容的页码。而数据库索引能够大大提高查询的速度原理也是一样,想像一下通过书后面的索引查找的速度要比一页一页地翻内容高多少倍……而索引之所以效率高,另外一个原因是它是排好序的。对于检索系统来说核心是一个排序问题。
由于数据库索引不是为全文索引设计的,因此,使用like "%keyword%"时,数据库索引是不起作用的,在使用like查询时,搜索过程又变成类似于一页页翻书的遍历过程了,所以对于含有模糊查询的数据库服务来说,LIKE对性能的危害是极大的。如果是需要对多个关键词进行模糊匹配:like"%keyword1%" and like "%keyword2%" ...其效率也就可想而知了。
所以建立一个高效检索系统的关键是建立一个类似于科技索引一样的反向索引机制,将数据源(比如多篇文章)排序顺序存储的同时,有另外一个排好序的关键词列表,用于存储关键词==>文章映射关系,利用这样的映射关系索引:[关键词==>出现关键词的文章编号,出现次数(甚至包括位置:起始偏移量,结束偏移量),出现频率],检索过程就是把模糊查询变成多个可以利用索引的精确查询的逻辑组合的过程。从而大大提高了多关键词查询的效率,所以,全文检索问题归结到最后是一个排序问题。
由此可以看出模糊查询相对数据库的精确查询是一个非常不确定的问题,这也是大部分数据库对全文检索支持有限的原因。Lucene最核心的特征是通过特殊的索引结构实现了传统数据库不擅长的全文索引机制,并提供了扩展接口,以方便针对不同应用的定制。
可以通过一下表格对比一下数据库的模糊查询:
|
Lucene全文索引引擎 |
数据库 |
索引 |
将数据源中的数据都通过全文索引一一建立反向索引 |
对于LIKE查询来说,数据传统的索引是根本用不上的。数据需要逐个便利记录进行GREP式的模糊匹配,比有索引的搜索速度要有多个数量级的下降。 |
匹配效果 |
通过词元(term)进行匹配,通过语言分析接口的实现,可以实现对中文等非英语的支持。 |
使用:like "%net%" 会把netherlands也匹配出来, 多个关键词的模糊匹配:使用like "%com%net%":就不能匹配词序颠倒的xxx.net..xxx.com |
匹配度 |
有匹配度算法,将匹配程度(相似度)比较高的结果排在前面。 |
没有匹配程度的控制:比如有记录中net出现5词和出现1次的,结果是一样的。 |
结果输出 |
通过特别的算法,将最匹配度最高的头100条结果输出,结果集是缓冲式的小批量读取的。 |
返回所有的结果集,在匹配条目非常多的时候(比如上万条)需要大量的内存存放这些临时结果集。 |
可定制性 |
通过不同的语言分析接口实现,可以方便的定制出符合应用需要的索引规则(包括对中文的支持) |
没有接口或接口复杂,无法定制 |
结论 |
高负载的模糊查询应用,需要负责的模糊查询的规则,索引的资料量比较大 |
使用率低,模糊匹配规则简单或者需要模糊查询的资料量少 |
全文检索和数据库应用最大的不同在于:让
最相关的
头100条结果满足98%以上用户的需求
Lucene的创新之处:
大部分的搜索(数据库)引擎都是用B树结构来维护索引,索引的更新会导致大量的IO操作,Lucene在实现中,对此稍微有所改进:不是维护一个索引文件,而是在扩展索引的时候不断创建新的索引文件,然后定期的把这些新的小索引文件合并到原先的大索引中(针对不同的更新策略,批次的大小可以调整),这样在不影响检索的效率的前提下,提高了索引的效率。
Lucene和其他一些全文检索系统/应用的比较:
|
Lucene |
其他开源全文检索系统 |
增量索引和批量索引 |
可以进行增量的索引(Append),可以对于大量数据进行批量索引,并且接口设计用于优化批量索引和小批量的增量索引。 |
很多系统只支持批量的索引,有时数据源有一点增加也需要重建索引。 |
数据源 |
Lucene没有定义具体的数据源,而是一个文档的结构,因此可以非常灵活的适应各种应用(只要前端有合适的转换器把数据源转换成相应结构), |
很多系统只针对网页,缺乏其他格式文档的灵活性。 |
索引内容抓取 |
Lucene的文档是由多个字段组成的,甚至可以控制那些字段需要进行索引,那些字段不需要索引,近一步索引的字段也分为需要分词和不需要分词的类型: 需要进行分词的索引,比如:标题,文章内容字段 不需要进行分词的索引,比如:作者/日期字段 |
缺乏通用性,往往将文档整个索引了 |
语言分析 |
通过语言分析器的不同扩展实现: 可以过滤掉不需要的词:an the of 等, 西文语法分析:将jumps jumped jumper都归结成jump进行索引/检索 非英文支持:对亚洲语言,阿拉伯语言的索引支持 |
缺乏通用接口实现 |
查询分析 |
通过查询分析接口的实现,可以定制自己的查询语法规则: 比如: 多个关键词之间的 + - and or关系等 |
|
并发访问 |
能够支持多用户的使用 |
|
关于亚洲语言的的切分词问题(Word Segment)
对于中文来说,全文索引首先还要解决一个语言分析的问题,对于英文来说,语句中单词之间是天然通过空格分开的,但亚洲语言的中日韩文语句中的字是一个字挨一个,所有,首先要把语句中按“词”进行索引的话,这个词如何切分出来就是一个很大的问题。
首先,肯定不能用单个字符作(si-gram)为索引单元,否则查“上海”时,不能让含有“海上”也匹配。
但一句话:“北京天安门”,计算机如何按照中文的语言习惯进行切分呢? “北京 天安门” 还是“北 京 天安门”?让计算机能够按照语言习惯进行切分,往往需要机器有一个比较丰富的词库才能够比较准确的识别出语句中的单词。
另外一个解决的办法是采用自动切分算法:将单词按照2元语法(bigram)方式切分出来,比如: "北京天安门" ==> "北京 京天 天安 安门"。
这样,在查询的时候,无论是查询"北京" 还是查询"天安门",将查询词组按同样的规则进行切分:"北京","天安安门",多个关键词之间按与"and"的关系组合,同样能够正确地映射到相应的索引中。这种方式对于其他亚洲语言:韩文,日文都是通用的。
基于自动切分的最大优点是没有词表维护成本,实现简单,缺点是索引效率低,但对于中小型应用来说,基于2元语法的切分还是够用的。基于2元切分后的索引一般大小和源文件差不多,而对于英文,索引文件一般只有原文件的30%-40%不同,
|
自动切分 |
词表切分 |
实现 |
实现非常简单 |
实现复杂 |
查询 |
增加了查询分析的复杂程度, |
适于实现比较复杂的查询语法规则 |
存储效率 |
索引冗余大,索引几乎和原文一样大 |
索引效率高,为原文大小的30%左右 |
维护成本 |
无词表维护成本 |
词表维护成本非常高:中日韩等语言需要分别维护。 还需要包括词频统计等内容 |
适用领域 |
嵌入式系统:运行环境资源有限 分布式系统:无词表同步问题 多语言环境:无词表维护成本 |
对查询和存储效率要求高的专业搜索引擎
|
目前比较大的搜索引擎的语言分析算法一般是基于以上2个机制的结合。关于中文的语言分析算法,大家可以在Google查关键词"wordsegment search"能找到更多相关的资料。
安装和使用
下载:http://jakarta.apache.org/lucene/
注意:Lucene中的一些比较复杂的词法分析是用JavaCC生成的(JavaCC:JavaCompilerCompiler,纯Java的词法分析生成器),所以如果从源代码编译或需要修改其中的QueryParser、定制自己的词法分析器,还需要从https://javacc.dev.java.net/下载javacc。
lucene的组成结构:对于外部应用来说索引模块(index)和检索模块(search)是主要的外部应用入口
org.apache.Lucene.search/ |
搜索入口 |
org.apache.Lucene.index/ |
索引入口 |
org.apache.Lucene.analysis/ |
语言分析器 |
org.apache.Lucene.queryParser/ |
查询分析器 |
org.apache.Lucene.document/ |
存储结构 |
org.apache.Lucene.store/ |
底层IO/存储结构 |
org.apache.Lucene.util/ |
一些公用的数据结构 |
简单的例子演示一下Lucene的使用方法: 索引过程:从命令行读取文件名(多个),将文件分路径(path字段)和内容(body字段)2个字段进行存储,并对内容进行全文索引:索引的单位是Document对象,每个Document对象包含多个字段Field对象,针对不同的字段属性和数据输出的需求,对字段还可以选择不同的索引/存储字段规则,列表如下:
方法 | 切词 | 索引 | 存储 | 用途 |
---|
Field.Text(String name, String value) | Yes | Yes | Yes | 切分词索引并存储,比如:标题,内容字段 | Field.Text(String name, Reader value) | Yes | Yes | No | 切分词索引不存储,比如:META信息, 不用于返回显示,但需要进行检索内容 | Field.Keyword(String name, String value) | No | Yes | Yes | 不切分索引并存储,比如:日期字段 | Field.UnIndexed(String name, String value) | No | No | Yes | 不索引,只存储,比如:文件路径 | Field.UnStored(String name, String value) | Yes | Yes | No | 只全文索引,不存储 |
public class IndexFiles { //使用方法:: IndexFiles [索引输出目录] [索引的文件列表] ... public static void main(String[] args) throws Exception { String indexPath = args[0]; IndexWriter writer; //用指定的语言分析器构造一个新的写索引器(第3个参数表示是否为追加索引) writer = new IndexWriter(indexPath, new SimpleAnalyzer(), false);
for (int i=1; i<args.length; i++) { System.out.println("Indexing file " + args[i]); InputStream is = new FileInputStream(args[i]);
//构造包含2个字段Field的Document对象 //一个是路径path字段,不索引,只存储 //一个是内容body字段,进行全文索引,并存储 Document doc = new Document(); doc.add(Field.UnIndexed("path", args[i])); doc.add(Field.Text("body", (Reader) new InputStreamReader(is))); //将文档写入索引 writer.addDocument(doc); is.close(); }; //关闭写索引器 writer.close(); } } 索引过程中可以看到: - 语言分析器提供了抽象的接口,因此语言分析(Analyser)是可以定制的,虽然lucene缺省提供了2个比较通用的分析器SimpleAnalyser和StandardAnalyser,这2个分析器缺省都不支持中文,所以要加入对中文语言的切分规则,需要修改这2个分析器。
- Lucene并没有规定数据源的格式,而只提供了一个通用的结构(Document对象)来接受索引的输入,因此输入的数据源可以是:数据库,WORD文档,PDF文档,HTML文档……只要能够设计相应的解析转换器将数据源构造成成Docuement对象即可进行索引。
- 对于大批量的数据索引,还可以通过调整IndexerWrite的文件合并频率属性(mergeFactor)来提高批量索引的效率。
检索过程和结果显示: 搜索结果返回的是Hits对象,可以通过它再访问Document==>Field中的内容。 假设根据body字段进行全文检索,可以将查询结果的path字段和相应查询的匹配度(score)打印出来, public class Search { public static void main(String[] args) throws Exception { String indexPath = args[0], queryString = args[1]; //指向索引目录的搜索器 Searcher searcher = new IndexSearcher(indexPath); //查询解析器:使用和索引同样的语言分析器 Query query = QueryParser.parse(queryString, "body", new SimpleAnalyzer()); //搜索结果使用Hits存储 Hits hits = searcher.search(query); //通过hits可以访问到相应字段的数据和查询的匹配度 for (int i=0; i<hits.length(); i++) { System.out.println(hits.doc(i).get("path") + "; Score: " + hits.score(i)); }; } } 在整个检索过程中,语言分析器,查询分析器,甚至搜索器(Searcher)都是提供了抽象的接口,可以根据需要进行定制。
Hacking Lucene 简化的查询分析器 个人感觉lucene成为JAKARTA项目后,画在了太多的时间用于调试日趋复杂QueryParser,而其中大部分是大多数用户并不很熟悉的,目前LUCENE支持的语法: Query ::= ( Clause )* Clause ::= ["+", "-"] [<TERM> ":"] ( <TERM> | "(" Query ")") 中间的逻辑包括:and or + - &&||等符号,而且还有"短语查询"和针对西文的前缀/模糊查询等,个人感觉对于一般应用来说,这些功能有一些华而不实,其实能够实现目前类似于Google的查询语句分析功能其实对于大多数用户来说已经够了。所以,Lucene早期版本的QueryParser仍是比较好的选择。 添加修改删除指定记录(Document) Lucene提供了索引的扩展机制,因此索引的动态扩展应该是没有问题的,而指定记录的修改也似乎只能通过记录的删除,然后重新加入实现。如何删除指定的记录呢?删除的方法也很简单,只是需要在索引时根据数据源中的记录ID专门另建索引,然后利用IndexReader.delete(Termterm)方法通过这个记录ID删除相应的Document。 根据某个字段值的排序功能 lucene缺省是按照自己的相关度算法(score)进行结果排序的,但能够根据其他字段进行结果排序是一个在LUCENE的开发邮件列表中经常提到的问题,很多原先基于数据库应用都需要除了基于匹配度(score)以外的排序功能。而从全文检索的原理我们可以了解到,任何不基于索引的搜索过程效率都会导致效率非常的低,如果基于其他字段的排序需要在搜索过程中访问存储字段,速度回大大降低,因此非常是不可取的。 但这里也有一个折中的解决方法:在搜索过程中能够影响排序结果的只有索引中已经存储的docID和score这2个参数,所以,基于score以外的排序,其实可以通过将数据源预先排好序,然后根据docID进行排序来实现。这样就避免了在LUCENE搜索结果外对结果再次进行排序和在搜索过程中访问不在索引中的某个字段值。 这里需要修改的是IndexSearcher中的HitCollector过程: ... scorer.score(new HitCollector() { private float minScore = 0.0f; public final void collect(int doc, float score) { if (score > 0.0f && // ignore zeroed buckets (bits==null || bits.get(doc))) { // skip docs not in bits totalHits[0]++; if (score >= minScore) { /* 原先:Lucene将docID和相应的匹配度score例入结果命中列表中: * hq.put(new ScoreDoc(doc, score)); // update hit queue * 如果用doc 或 1/doc 代替 score,就实现了根据docID顺排或逆排 * 假设数据源索引时已经按照某个字段排好了序,而结果根据docID排序也就实现了 * 针对某个字段的排序,甚至可以实现更复杂的score和docID的拟合。 */ hq.put(new ScoreDoc(doc, (float) 1/doc )); if (hq.size() > nDocs) { // if hit queue overfull hq.pop(); // remove lowest in hit queue minScore = ((ScoreDoc)hq.top()).score; // reset minScore } } } } }, reader.maxDoc()); 更通用的输入输出接口 虽然lucene没有定义一个确定的输入文档格式,但越来越多的人想到使用一个标准的中间格式作为Lucene的数据导入接口,然后其他数据,比如PDF只需要通过解析器转换成标准的中间格式就可以进行数据索引了。这个中间格式主要以XML为主,类似实现已经不下4,5个: 数据源: WORD PDF HTML DB other \ | | | / XML中间格式 | Lucene INDEX 目前还没有针对MSWord文档的解析器,因为Word文档和基于ASCII的RTF文档不同,需要使用COM对象机制解析。这个是我在Google上查的相关资料:http://www.intrinsyc.com/products/enterprise_applications.asp 另外一个办法就是把Word文档转换成text:http://www.winfield.demon.nl/index.html
索引过程优化
索引一般分2种情况,一种是小批量的索引扩展,一种是大批量的索引重建。在索引过程中,并不是每次新的DOC加入进去索引都重新进行一次索引文件的写入操作(文件I/O是一件非常消耗资源的事情)。 Lucene先在内存中进行索引操作,并根据一定的批量进行文件的写入。这个批次的间隔越大,文件的写入次数越少,但占用内存会很多。反之占用内存少,但文件IO操作频繁,索引速度会很慢。在IndexWriter中有一个MERGE_FACTOR参数可以帮助你在构造索引器后根据应用环境的情况充分利用内存减少文件的操作。根据我的使用经验:缺省Indexer是每20条记录索引后写入一次,每将MERGE_FACTOR增加50倍,索引速度可以提高1倍左右。
搜索过程优化
lucene支持内存索引:这样的搜索比基于文件的I/O有数量级的速度提升。 http://www.onjava.com/lpt/a/3273 而尽可能减少IndexSearcher的创建和对搜索结果的前台的缓存也是必要的。
Lucene面向全文检索的优化在于首次索引检索后,并不把所有的记录(Document)具体内容读取出来,而起只将所有结果中匹配度最高的头100条结果(TopDocs)的ID放到结果集缓存中并返回,这里可以比较一下数据库检索:如果是一个10,000条的数据库检索结果集,数据库是一定要把所有记录内容都取得以后再开始返回给应用结果集的。所以即使检索匹配总数很多,Lucene的结果集占用的内存空间也不会很多。对于一般的模糊检索应用是用不到这么多的结果的,头100条已经可以满足90%以上的检索需求。
如果首批缓存结果数用完后还要读取更后面的结果时Searcher会再次检索并生成一个上次的搜索缓存数大1倍的缓存,并再重新向后抓取。所以如果构造一个Searcher去查1-120条结果,Searcher其实是进行了2次搜索过程:头100条取完后,缓存结果用完,Searcher重新检索再构造一个200条的结果缓存,依此类推,400条缓存,800条缓存。由于每次Searcher对象消失后,这些缓存也访问那不到了,你有可能想将结果记录缓存下来,缓存数尽量保证在100以下以充分利用首次的结果缓存,不让Lucene浪费多次检索,而且可以分级进行结果缓存。
Lucene的另外一个特点是在收集结果的过程中将匹配度低的结果自动过滤掉了。这也是和数据库应用需要将搜索的结果全部返回不同之处。 我的一些尝试: - 支持中文的Tokenizer:这里有2个版本,一个是通过JavaCC生成的,对CJK部分按一个字符一个TOKEN索引,另外一个是从SimpleTokenizer改写的,对英文支持数字和字母TOKEN,对中文按迭代索引。
- 基于XML数据源的索引器:XMLIndexer,因此所有数据源只要能够按照DTD转换成指定的XML,就可以用XMLIndxer进行索引了。
- 根据某个字段排序:按记录索引顺序排序结果的搜索器:IndexOrderSearcher,因此如果需要让搜索结果根据某个字段排序,可以让数据源先按某个字段排好序(比如:PriceField),这样索引后,然后在利用这个按记录的ID顺序检索的搜索器,结果就是相当于是那个字段排序的结果了。
从Lucene学到更多 Luene的确是一个面对对象设计的典范 - 所有的问题都通过一个额外抽象层来方便以后的扩展和重用:你可以通过重新实现来达到自己的目的,而对其他模块而不需要;
- 简单的应用入口Searcher, Indexer,并调用底层一系列组件协同的完成搜索任务;
- 所有的对象的任务都非常专一:比如搜索过程:QueryParser分析将查询语句转换成一系列的精确查询的组合(Query),通过底层的索引读取结构IndexReader进行索引的读取,并用相应的打分器给搜索结果进行打分/排序等。所有的功能模块原子化程度非常高,因此可以通过重新实现而不需要修改其他模块。
- 除了灵活的应用接口设计,Lucene还提供了一些适合大多数应用的语言分析器实现(SimpleAnalyser,StandardAnalyser),这也是新用户能够很快上手的重要原因之一。
这些优点都是非常值得在以后的开发中学习借鉴的。作为一个通用工具包,Lunece的确给予了需要将全文检索功能嵌入到应用中的开发者很多的便利。 此外,通过对Lucene的学习和使用,我也更深刻地理解了为什么很多数据库优化设计中要求,比如: - 尽可能对字段进行索引来提高查询速度,但过多的索引会对数据库表的更新操作变慢,而对结果过多的排序条件,实际上往往也是性能的杀手之一。
- 很多商业数据库对大批量的数据插入操作会提供一些优化参数,这个作用和索引器的merge_factor的作用是类似的,
- 20%/80%原则:查的结果多并不等于质量好,尤其对于返回结果集很大,如何优化这头几十条结果的质量往往才是最重要的。
- 尽可能让应用从数据库中获得比较小的结果集,因为即使对于大型数据库,对结果集的随机访问也是一个非常消耗资源的操作。
参考资料: Apache: Lucene Project http://jakarta.apache.org/lucene/ Lucene开发/用户邮件列表归档 Lucene-dev@jakarta.apache.org Lucene-user@jakarta.apache.org The Lucene search engine: Powerful, flexible, and free http://www.javaworld.com/javaworld/jw-09-2000/jw-0915-Lucene_p.html Lucene Tutorial http://www.darksleep.com/puff/lucene/lucene.html Notes on distributed searching with Lucene http://home.clara.net/markharwood/lucene/ 中文语言的切分词 http://www.google.com/search?sourceid=navclient&hl=zh-CN&q=chinese+word+segment 搜索引擎工具介绍 http://searchtools.com/ Lucene作者Cutting的几篇论文和专利 http://lucene.sourceforge.net/publications.html Lucene的.NET实现:dotLucene http://sourceforge.net/projects/dotlucene/
Lucene作者Cutting的另外一个项目:基于Java的搜索引擎Nutch http://www.nutch.org/ http://sourceforge.net/projects/nutch/
关于基于词表和N-Gram的切分词比较 http://china.nikkeibp.co.jp/cgi-bin/china/news/int/int200302100112.html
2005-01-08 Cutting在Pisa大学做的关于Lucene的讲座:非常详细的Lucene架构解说
使用Windows操作系统的朋友对Excel(电子表格)一定不会陌生,但是要使用Java语言来操纵Excel文件并不是一件容易的事。在Web应用日益盛行的今天,通过Web来操作Excel文件的需求越来越强烈,目前较为流行的操作是在JSP或Servlet 中创建一个CSV (comma separated values)文件,并将这个文件以MIME,text/csv类型返回给浏览器,接着浏览器调用Excel并且显示CSV文件。这样只是说可以访问到 Excel文件,但是还不能真正的操纵Excel文件,本文将给大家一个惊喜,向大家介绍一个开放源码项目,Java Excel API,使用它大家就可
以方便地操纵Excel文件了。
Java Excel API简介
Java Excel是一开放源码项目,通过它Java开发人员可以读取Excel文件的内容、创建新的Excel文件、更新已经存在的Excel文件。使用该 API非Windows操作系统也可以通过纯Java应用来处理Excel数据表。因为是使用Java编写的,所以我们在Web应用中可以通过JSP、 Servlet来调用API实现对Excel数据表的访问。
现在发布的稳定版本是V2.0,提供以下功能:
从Excel 95、97、2000等格式的文件中读取数据;
读取Excel公式(可以读取Excel 97以后的公式);
生成Excel数据表(格式为Excel 97);
支持字体、数字、日期的格式化;
支持单元格的阴影操作,以及颜色操作;
修改已经存在的数据表;
现在还不支持以下功能,但不久就会提供了:
不能够读取图表信息;
可以读,但是不能生成公式,任何类型公式最后的计算值都可以读出;
应用示例
1、从Excel文件读取数据表
Java Excel API既可以从本地文件系统的一个文件(.xls),也可以从输入流中读取Excel数据表。读取Excel数据表的第一步是创建Workbook(术语:工作薄),下面的代码片段举例说明了应该如何操作:(完整代码见ExcelReading.java)
import java.io.*;
import jxl.*;
… … … …
try
{
//构建Workbook对象, 只读Workbook对象
//直接从本地文件创建Workbook
//从输入流创建Workbook
InputStream is = new FileInputStream(sourcefile);
jxl.Workbook rwb = Workbook.getWorkbook(is);
}
catch (Exception e)
{
e.printStackTrace();
}
一旦创建了Workbook,我们就可以通过它来访问Excel Sheet(术语:工作表)。参考下面的代码片段:
//获取第一张Sheet表
Sheet rs = rwb.getSheet(0);
我们既可能通过Sheet的名称来访问它,也可以通过下标来访问它。如果通过下标来访问的话,要注意的一点是下标从0开始,就像数组一样。
一旦得到了Sheet,我们就可以通过它来访问Excel Cell(术语:单元格)。参考下面的代码片段:
//获取第一行,第一列的值
Cell c00 = rs.getCell(0, 0);
String strc00 = c00.getContents();
//获取第一行,第二列的值
Cell c10 = rs.getCell(1, 0);
String strc10 = c10.getContents();
//获取第二行,第二列的值
Cell c11 = rs.getCell(1, 1);
String strc11 = c11.getContents();
System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType()); 如果仅仅是取得Cell的值,我们可以方便地通过getContents()方法,它可以将任何类型的Cell值都作为一个字符串返回。示例代码中Cell(0, 0)是文本型,Cell(1, 0)是数字型,Cell(1,1)是日期型,通过getContents(),三种类型的返回值都是字符型。
如果有需要知道Cell内容的确切类型,API也提供了一系列的方法。参考下面的代码片段:
String strc00 = null;
double strc10 = 0.00;
Date strc11 = null;
Cell c00 = rs.getCell(0, 0);
Cell c10 = rs.getCell(1, 0);
Cell c11 = rs.getCell(1, 1);
if(c00.getType() == CellType.LABEL)
{
LabelCell labelc00 = (LabelCell)c00;
strc00 = labelc00.getString();
}
if(c10.getType() == CellType.NUMBER)
{
NmberCell numc10 = (NumberCell)c10;
strc10 = numc10.getvalue();
}
if(c11.getType() == CellType.DATE)
{
DateCell datec11 = (DateCell)c11;
strc11 = datec11.getDate();
}
System.out.println("Cell(0, 0)" + " value : " + strc00 + "; type : " + c00.getType());
System.out.println("Cell(1, 0)" + " value : " + strc10 + "; type : " + c10.getType());
System.out.println("Cell(1, 1)" + " value : " + strc11 + "; type : " + c11.getType());
在得到Cell对象后,通过getType()方法可以获得该单元格的类型,然后与 API提供的基本类型相匹配,强制转换成相应的类型,最后调用相应的取值方法getXXX(),就可以得到确定类型的值。API提供了以下基本类型,与 Excel的数据格式相对应,如下图所示:
每种类型的具体意义,请参见Java Excel API document.
当你完成对Excel电子表格数据的处理后,一定要使用close()方法来关闭先前创建的对象,以释放读取数据表的过程中所占用的内存空间,在读取大量数据时显得尤为重要。参考如下代码片段:
//操作完成时,关闭对象,释放占用的内存空间
rwb.close();
Java Excel API提供了许多访问Excel数据表的方法,在这里我只简要地介绍几个常用的方法,其它的方法请参考附录中的Java Excel API document.
Workbook类提供的方法
1. int getNumberOfSheets()
获得工作薄(Workbook)中工作表(Sheet)的个数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
int sheets = rwb.getNumberOfSheets();
2. Sheet[] getSheets()
返回工作薄(Workbook)中工作表(Sheet)对象数组,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
Sheet[] sheets = rwb.getSheets();
3. String getVersion()
返回正在使用的API的版本号,好像是没什么太大的作用。
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
String apiVersion = rwb.getVersion();
Sheet接口提供的方法
1) String getName()
获取Sheet的名称,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
String sheetName = rs.getName();
2) int getColumns()
获取Sheet表中所包含的总列数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
int rsColumns = rs.getColumns();
3) Cell[] getColumn(int column)
获取某一列的所有单元格,返回的是单元格对象数组,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell[] cell = rs.getColumn(0);
4) int getRows()
获取Sheet表中所包含的总行数,示例:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
int rsRows = rs.getRows();
5) Cell[] getRow(int row)
获取某一行的所有单元格,返回的是单元格对象数组,示例子:
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell[] cell = rs.getRow(0);
6) Cell getCell(int column, int row)
获取指定单元格的对象引用,需要注意的是它的两个参数,第一个是列数,第二个是行数,这与通常的行、列组合有些不同。
jxl.Workbook rwb = jxl.Workbook.getWorkbook(new File(sourcefile));
jxl.Sheet rs = rwb.getSheet(0);
Cell cell = rs.getCell(0, 0); 2、生成新的Excel工作薄
下面的代码主要是向大家介绍如何生成简单的Excel工作表,在这里单元格的内容是不带任何修饰的(如:字体,颜色等等),所有的内容都作为字符串写入。(完整代码见ExcelW
riting.java)
与读取Excel工作表相似,首先要使用Workbook类的工厂方法创建一个可写入的工作薄(Workbook)对象,这里要注意的是,只能通过API提供的工厂方法来创建Workbook,而不能使用 WritableWorkbook的构造函数,因为类WritableWorkbook的构造函数为protected类型。示例代码片段如下:
import java.io.*;
import jxl.*;
import jxl.write.*;
… … … …
try
{
//构建Workbook对象, 只读Workbook对象
//Method 1:创建可写入的Excel工作薄
jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile));
//Method 2:将WritableWorkbook直接写入到输出流
/*
OutputStream os = new FileOutputStream(targetfile);
jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(os);
*/
}
catch (Exception e)
{
e.printStackTrace();
}
API提供了两种方式来处理可写入的输出流,一种是直接生成本地文件,如果文件名不带全路径的话,缺省的文件会定位在当前目录,如果文件名带有全路径的话,则生成的Excel文件则会定位在相应的目录;另外一种是将Excel对象直接写入到输出流,例如:用户通过浏览器来访问Web服务器,如果HTTP头设置正确的话,浏览器自动调用客户端的Excel应用程序,来显示动态生成的 Excel电子表格。
接下来就是要创建工作表,创建工作表的方法与创建工作薄的方法几乎一样,同样是通过工厂模式方法获得相应的对象,该方法需要两个参数,一个是工作表的名称,另一个是工作表在工作薄中的位置,参考下面的代码片段:
//创建Excel工作表
jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0);
"这锅也支好了,材料也准备齐全了,可以开始下锅了!",现在要做的只是实例化API所提供的Excel基本数据类型,并将它们添加到工作表中就可以了,参考下面的代码片段:
//1.添加Label对象
jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell");
ws.addCell(labelC);
//添加带有字型Formatting的对象
jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true);
jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
jxl.write.Label labelCF = new jxl.write.Label(1, 0, "This is a Label Cell", wcfF);
ws.addCell(labelCF);
//添加带有字体颜色Formatting的对象
jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false,
Underlinestyle.NO_UNDERLINE, jxl.format.Colour.RED);
jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
jxl.write.Label labelCFC = new jxl.write.Label(1, 0, "This is a Label Cell", wcfFC);
ws.addCell(labelCF);
//2.添加Number对象
jxl.write.Number labelN = new jxl.write.Number(0, 1, 3.1415926);
ws.addCell(labelN);
//添加带有formatting的Number对象
jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN);
ws.addCell(labelNF);
//3.添加Boolean对象
jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false);
ws.addCell(labelB);
//4.添加DateTime对象
jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date());
ws.addCell(labelDT);
//添加带有formatting的DateFormat对象
jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss");
jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df);
jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF);
ws.addCell(labelDTF); 这里有两点大家要引起大家的注意。第一点,在构造单元格时,单元格在工作表中的位置就已经确定了。一旦创建后,单元格的位置是不能够变更的,尽管单元格的内容是可以改变的。第二点,单元格的定位是按照下面这样的规律(column, row),而且下标都是从0开始,例如,A1被存储在(0, 0),B1被存储在(1, 0)。
最后,不要忘记关闭打开的Excel工作薄对象,以释放占用的内存,参见下面的代码片段:
//写入Exel工作表
wwb.write();
//关闭Excel工作薄对象
wwb.close();
这可能与读取Excel文件的操作有少少不同,在关闭Excel对象之前,你必须要先调用write()方法,因为先前的操作都是存储在缓存中的,所以要通过该方法将操作的内容保存在文件中。如果你先关闭了Excel对象,那么只能得到一张空的工作薄了。
3、拷贝、更新Excel工作薄
接下来简要介绍一下如何更新一个已经存在的工作薄,主要是下面二步操作,第一步是构造只读的Excel工作薄,第二步是利用已经创建的Excel工作薄创建新的可写入的Excel工作薄,参考下面的代码片段:(完整代码见ExcelModifying.java)
//创建只读的Excel工作薄的对象
jxl.Workbook rw = jxl.Workbook.getWorkbook(new File(sourcefile));
//创建可写入的Excel工作薄对象
jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(targetfile), rw);
//读取第一张工作表
jxl.write.WritableSheet ws = wwb.getSheet(0);
//获得第一个单元格对象
jxl.write.WritableCell wc = ws.getWritableCell(0, 0);
//判断单元格的类型, 做出相应的转化
if(wc.getType() == CellType.LABEL)
{
Label l = (Label)wc;
l.setString("The value has been modified.");
}
//写入Excel对象
wwb.write();
//关闭可写入的Excel对象
wwb.close();
//关闭只读的Excel对象
rw.close(); 之所以使用这种方式构建Excel对象,完全是因为效率的原因,因为上面的示例才是 API的主要应用。为了提高性能,在读取工作表时,与数据相关的一些输出信息,所有的格式信息,如:字体、颜色等等,是不被处理的,因为我们的目的是获得行数据的值,既使没有了修饰,也不会对行数据的值产生什么影响。唯一的不利之处就是,在内存中会同时保存两 个同样的工作表,这样当工作表体积比较大时,会占用相当大的内存,但现在好像内存的大小并不是什么关键因素了。
一旦获得了可写入的工作表对象,我们就可以对单元格对象进行更新的操作了,在这里我们不必调用API提供的add()方法,因为单元格已经于工作表当中,所以我们只需要调用相应的setXXX()方法,就可以完成更新的操作了。
尽单元格原有的格式化修饰是不能去掉的,我们还是可以将新的单元格修饰加上去,以使单元格的内容以不同的形式表现。
新生成的工作表对象是可写入的,我们除了更新原有的单元格外,还可以添加新的单元格到工作表中,这与示例2的操作是完全一样的。
最后,不要忘记调用write()方法,将更新的内容写入到文件中,然后关闭工作薄对象,这里有两个工作薄对象要关闭,一个是只读的,另外一个是可写入的。
jxl的一些总结
要往xls文件里面写入数据的时候需要注意的是第一要新建一个xls文件 OutputStream os=new FileOutputStream("c:\\excel2.xls");
再建完这个文件的时候再建立工作文件 jxl.write.WritableWorkbook wwb = Workbook.createWorkbook(new File(os));
如果这个文件已经存在,那么我们可以在这个文件里面加入一个sheet为了和以前的数据进行分开; jxl.write.WritableSheet ws = wwb.createSheet("Test Sheet 1", 0); 在createSheet方法里前面的参数是sheet名,后面是要操作的sheet号
接下来就可以往这个文件里面写入数据了
写入数据的时候注意的格式
(1)添加的字体样式 jxl.write.WritableFont wf = new jxl.write.WritableFont(WritableFont.TIMES, 18, WritableFont.BOLD, true); WritableFont()方法里参数说明: 这个方法算是一个容器,可以放进去好多属性 第一个: TIMES是字体大小,他写的是18 第二个: BOLD是判断是否为斜体,选择true时为斜体 第三个: ARIAL 第四个: UnderlineStyle.NO_UNDERLINE 下划线 第五个: jxl.format.Colour.RED 字体颜色是红色的
jxl.write.WritableCellFormat wcfF = new jxl.write.WritableCellFormat(wf);
jxl.write.Label labelC = new jxl.write.Label(0, 0, "This is a Label cell",wcfF); ws.addCell(labelC); 在Label()方法里面有三个参数 第一个是代表列数, 第二是代表行数, 第***要写入的内容 第四个是可选项,是输入这个label里面的样式 然后通过写sheet的方法addCell()把内容写进sheet里面。
(2)添加带有formatting的Number对象 jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##");
(3)添加Number对象 (3.1)显示number对象数据的格式
jxl.write.NumberFormat nf = new jxl.write.NumberFormat("#.##"); jxl.write.WritableCellFormat wcfN = new jxl.write.WritableCellFormat(nf);
jxl.write.Number labelNF = new jxl.write.Number(1, 1, 3.1415926, wcfN); ws.addCell(labelNF); Number()方法参数说明: 前两上表示输入的位置 第三个表示输入的内容
(4)添加Boolean对象 jxl.write.Boolean labelB = new jxl.write.Boolean(0, 2, false); ws.addCell(labelB);
(5)添加DateTime对象 jxl.write.DateTime labelDT = new jxl.write.DateTime(0, 3, new java.util.Date()); ws.addCell(labelDT); DateTime()方法的参数说明 前两个表示输入的位置 第三个表示输入的当前时间
(6)添加带有formatting的DateFormat对象 这个显示当前时间的所有信息,包括年月日小时分秒 jxl.write.DateFormat df = new jxl.write.DateFormat("dd MM yyyy hh:mm:ss"); jxl.write.WritableCellFormat wcfDF = new jxl.write.WritableCellFormat(df); jxl.write.DateTime labelDTF = new jxl.write.DateTime(1, 3, new java.util.Date(), wcfDF); ws.addCell(labelDTF);
(7)添加带有字体颜色Formatting的对象 jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL, 10, WritableFont.NO_BOLD, false,UnderlineStyle.NO_UNDERLINE, jxl.format.Colour.RED); jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc);
import="jxl.format.* jxl.write.WritableFont wfc = new jxl.write.WritableFont(WritableFont.ARIAL,20,WritableFont.BOLD,false,UnderlineStyle.NO_UNDERLINE,jxl.format.Colour.GREEN);
(8)设置单元格样式
jxl.write.WritableCellFormat wcfFC = new jxl.write.WritableCellFormat(wfc); wcfFC.setBackGround(jxl.format.Colour.RED);//设置单元格的颜色为红色 wcfFC = new jxl.write.Label(6,0,"i love china",wcfFC);
Jxl在写excel文件时使用的方法比较怪,也可以说jxl不支持修改excel文件。它的处理方式是每次打开旧excel文件,然后创建一个该excel文件的可写的副本,所有的修改都是在这个副本上做的。下面是一个例子。
Java中static、this、super、final用法 |
作者:未知 来源:未知 发布时间:2006-7-14 21:33:56 发布人:sany |
减小字体
增大字体
一、static 请先看下面这段程序: public class Hello{ public static void main(String[] args){ //(1) System.out.println("Hello,world!"); //(2) } } 看过这段程序,对于大多数学过Java 的从来说,都不陌生。即使没有学过Java,而学过其它的高级语言,例如C,那你也应该能看懂这段代码的意思。它只是简单的输出“Hello,world”,一点别的用处都没有,然而,它却展示了static关键字的主要用法。 在1处,我们定义了一个静态的方法名为main,这就意味着告诉Java编译器,我这个方法不需要创建一个此类的对象即可使用。你还得你是怎么运行这个程序吗?一般,我们都是在命令行下,打入如下的命令(加下划线为手动输入): javac Hello.java java Hello Hello,world! 这就是你运行的过程,第一行用来编译Hello.java这个文件,执行完后,如果你查看当前,会发现多了一个Hello.class文件,那就是第一行产生的Java二进制字节码。第二行就是执行一个Java程序的最普遍做法。执行结果如你所料。在2中,你可能会想,为什么要这样才能输出。好,我们来分解一下这条语句。(如果没有安装Java文档,请到Sun的官方网站浏览J2SE API)首先,System是位于java.lang包中的一个核心类,如果你查看它的定义,你会发现有这样一行:public static final PrintStream out;接着在进一步,点击PrintStream这个超链接,在METHOD页面,你会看到大量定义的方法,查找println,会有这样一行: public void println(String x)。 好了,现在你应该明白为什么我们要那样调用了,out是System的一个静态变量,所以可以直接使用,而out所属的类有一个println方法。 静态方法 通常,在一个类中定义一个方法为static,那就是说,无需本类的对象即可调用此方法。如下所示: class Simple{ static void go(){ System.out.println("Go..."); } } public class Cal{ public static void main(String[] args){ Simple.go(); } } 调用一个静态方法就是“类名.方法名”,静态方法的使用很简单如上所示。一般来说,静态方法常常为应用程序中的其它类提供一些实用工具所用,在Java的类库中大量的静态方法正是出于此目的而定义的。 静态变量 静态变量与静态方法类似。所有此类实例共享此静态变量,也就是说在类装载时,只分配一块存储空间,所有此类的对象都可以操控此块存储空间,当然对于final则另当别论了。看下面这段代码: class Value{ static int c=0; static void inc(){ c++; } } class Count{ public static void prt(String s){ System.out.println(s); } public static void main(String[] args){ Value v1,v2; v1=new Value(); v2=new Value(); prt("v1.c="+v1.c+" v2.c="+v2.c); v1.inc(); prt("v1.c="+v1.c+" v2.c="+v2.c); } } 结果如下: v1.c=0 v2.c=0 v1.c=1 v2.c=1 由此可以证明它们共享一块存储区。static变量有点类似于C中的全局变量的概念。值得探讨的是静态变量的初始化问题。我们修改上面的程序: class Value{ static int c=0; Value(){ c=15; } Value(int i){ c=i; } static void inc(){ c++; } } class Count{ public static void prt(String s){ System.out.println(s); } Value v=new Value(10); static Value v1,v2; static{ prt("v1.c="+v1.c+" v2.c="+v2.c); v1=new Value(27); prt("v1.c="+v1.c+" v2.c="+v2.c); v2=new Value(15); prt("v1.c="+v1.c+" v2.c="+v2.c); } public static void main(String[] args){ Count ct=new Count(); prt("ct.c="+ct.v.c); prt("v1.c="+v1.c+" v2.c="+v2.c); v1.inc(); prt("v1.c="+v1.c+" v2.c="+v2.c); prt("ct.c="+ct.v.c); } } 运行结果如下: v1.c=0 v2.c=0 v1.c=27 v2.c=27 v1.c=15 v2.c=15 ct.c=10 v1.c=10 v2.c=10 v1.c=11 v2.c=11 ct.c=11 这个程序展示了静态初始化的各种特性。如果你初次接触Java,结果可能令你吃惊。可能会对static后加大括号感到困惑。首先要告诉你的是,static定义的变量会优先于任何其它非static变量,不论其出现的顺序如何。正如在程序中所表现的,虽然v出现在v1和v2的前面,但是结果却是v1和v2的初始化在v的前面。在static{后面跟着一段代码,这是用来进行显式的静态变量初始化,这段代码只会初始化一次,且在类被第一次装载时。如果你能读懂并理解这段代码,会帮助你对static关键字的认识。在涉及到继承的时候,会先初始化父类的static变量,然后是子类的,依次类推。非静态变量不是本文的主题,在此不做详细讨论,请参考Think in Java中的讲解。 静态类 通常一个普通类不允许声明为静态的,只有一个内部类才可以。这时这个声明为静态的内部类可以直接作为一个普通类来使用,而不需实例一个外部类。如下代码所示: public class StaticCls{ public static void main(String[] args){ OuterCls.InnerCls oi=new OuterCls.InnerCls(); } } class OuterCls{ public static class InnerCls{ InnerCls(){ System.out.println("InnerCls"); } } } 输出结果会如你所料: InnerCls 和普通类一样。内部类的其它用法请参阅Think in Java中的相关章节,此处不作详解。 二、this & super 在上一篇拙作中,我们讨论了static的种种用法,通过用static来定义方法或成员,为我们编程提供了某种便利,从某种程度上可以说它类似于C语言中的全局函数和全局变量。但是,并不是说有了这种便利,你便可以随处使用,如果那样的话,你便需要认真考虑一下自己是否在用面向对象的思想编程,自己的程序是否是面向对象的。好了,现在开始讨论this&super这两个关键字的意义和用法。 在Java中,this通常指当前对象,super则指父类的。当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用this来实现这个目的,当然,this的另一个用途是调用当前对象的另一个构造函数,这些马上就要讨论。如果你想引用父类的某种东西,则非super莫属。由于this与super有如此相似的一些特性和与生俱来的某种关系,所以我们在这一块儿来讨论,希望能帮助你区分和掌握它们两个。 在一般方法中 最普遍的情况就是,在你的方法中的某个形参名与当前对象的某个成员有相同的名字,这时为了不至于混淆,你便需要明确使用this关键字来指明你要使用某个成员,使用方法是“this.成员名”,而不带this的那个便是形参。另外,还可以用“this.方法名”来引用当前对象的某个方法,但这时this就不是必须的了,你可以直接用方法名来访问那个方法,编译器会知道你要调用的是那一个。下面的代码演示了上面的用法: public class DemoThis{ private String name; private int age; DemoThis(String name,int age){ setName(name); //你可以加上this来调用方法,像这样:this.setName(name);但这并不是必须的 setAge(age); this.print(); } public void setName(String name){ this.name=name;//此处必须指明你要引用成员变量 } public void setAge(int age){ this.age=age; } public void print(){ System.out.println("Name="+name+" Age="+age);//在此行中并不需要用this,因为没有会导致混淆的东西 } public static void main(String[] args){ DemoThis dt=new DemoThis("Kevin","22"); } } 这段代码很简单,不用解释你也应该能看明白。在构造函数中你看到用this.print(),你完全可以用print()来代替它,两者效果一样。下面我们修改这个程序,来演示super的用法。 class Person{ public int c; private String name; private int age; protected void setName(String name){ this.name=name; } protected void setAge(int age){ this.age=age; } protected void print(){ System.out.println("Name="+name+" Age="+age); } } public class DemoSuper extends Person{ public void print(){ System.out.println("DemoSuper:"); super.print(); } public static void main(String[] args){ DemoSuper ds=new DemoSuper(); ds.setName("kevin"); ds.setAge(22); ds.print(); } } 在DemoSuper中,重新定义的print方法覆写了父类的print方法,它首先做一些自己的事情,然后调用父类的那个被覆写了的方法。输出结果说明了这一点: DemoSuper: Name=kevin Age=22 这样的使用方法是比较常用的。另外如果父类的成员可以被子类访问,那你可以像使用this一样使用它,用“super.父类中的成员名”的方式,但常常你并不是这样来访问父类中的成员名的。 在构造函数中 构造函数是一种特殊的方法,在对象初始化的时候自动调用。在构造函数中,this和super也有上面说的种种使用方式,并且它还有特殊的地方,请看下面的例子: class Person{ public static void prt(String s){ System.out.println(s); } Person(){ prt("A Person."); } Person(String name){ prt("A person name is:"+name); } } public class Chinese extends Person{ Chinese(){ super(); //调用父类构造函数(1) prt("A chinese.");//(4) } Chinese(String name){ super(name);//调用父类具有相同形参的构造函数(2) prt("his name is:"+name); } Chinese(String name,int age){ this(name);//调用当前具有相同形参的构造函数(3) prt("his age is:"+age); } public static void main(String[] args){ Chinese cn=new Chinese(); cn=new Chinese("kevin"); cn=new Chinese("kevin",22); } } 在这段程序中,this和super不再是像以前那样用“.”连接一个方法或成员,而是直接在其后跟上适当的参数,因此它的意义也就有了变化。super后加参数的是用来调用父类中具有相同形式的构造函数,如1和2处。this后加参数则调用的是当前具有相同参数的构造函数,如3处。当然,在Chinese的各个重载构造函数中,this和super在一般方法中的各种用法也仍可使用,比如4处,你可以将它替换为“this.prt”(因为它继承了父类中的那个方法)或者是“super.prt”(因为它是父类中的方法且可被子类访问),它照样可以正确运行。但这样似乎就有点画蛇添足的味道了。 最后,写了这么多,如果你能对“this通常指代当前对象,super通常指代父类”这句话牢记在心,那么本篇便达到了目的,其它的你自会在以后的编程实践当中慢慢体会、掌握。另外关于本篇中提到的继承,请参阅相关Java教程。 三、final final在Java中并不常用,然而它却为我们提供了诸如在C语言中定义常量的功能,不仅如此,final还可以让你控制你的成员、方法或者是一个类是否可被覆写或继承等功能,这些特点使final在Java中拥有了一个不可或缺的地位,也是学习Java时必须要知道和掌握的关键字之一。 final成员 当你在类中定义变量时,在其前面加上final关键字,那便是说,这个变量一旦被初始化便不可改变,这里不可改变的意思对基本类型来说是其值不可变,而对于对象变量来说其引用不可再变。其初始化可以在两个地方,一是其定义处,也就是说在final变量定义时直接给其赋值,二是在构造函数中。这两个地方只能选其一,要么在定义时给值,要么在构造函数中给值,不能同时既在定义时给了值,又在构造函数中给另外的值。下面这段代码演示了这一点: import java.util.List; import java.util.ArrayList; import java.util.LinkedList; public class Bat{ final PI=3.14; //在定义时便给址值 final int i; //因为要在构造函数中进行初始化,所以此处便不可再给值 final List list; //此变量也与上面的一样 Bat(){ i=100; list=new LinkedList(); } Bat(int ii,List l){ i=ii; list=l; } public static void main(String[] args){ Bat b=new Bat(); b.list.add(new Bat()); //b.i=25; //b.list=new ArrayList(); System.out.println("I="+b.i+" List Type:"+b.list.getClass()); b=new Bat(23,new ArrayList()); b.list.add(new Bat()); System.out.println("I="+b.i+" List Type:"+b.list.getClass()); } } 此程序很简单的演示了final的常规用法。在这里使用在构造函数中进行初始化的方法,这使你有了一点灵活性。如Bat的两个重载构造函数所示,第一个缺省构造函数会为你提供默认的值,重载的那个构造函数会根据你所提供的值或类型为final变量初始化。然而有时你并不需要这种灵活性,你只需要在定义时便给定其值并永不变化,这时就不要再用这种方法。在main方法中有两行语句注释掉了,如果你去掉注释,程序便无法通过编译,这便是说,不论是i的值或是list的类型,一旦初始化,确实无法再更改。然而b可以通过重新初始化来指定i的值或list的类型,输出结果中显示了这一点: I=100 List Type:class java.util.LinkedList I=23 List Type:class java.util.ArrayList 还有一种用法是定义方法中的参数为final,对于基本类型的变量,这样做并没有什么实际意义,因为基本类型的变量在调用方法时是传值的,也就是说你可以在方法中更改这个参数变量而不会影响到调用语句,然而对于对象变量,却显得很实用,因为对象变量在传递时是传递其引用,这样你在方法中对对象变量的修改也会影响到调用语句中的对象变量,当你在方法中不需要改变作为参数的对象变量时,明确使用final进行声明,会防止你无意的修改而影响到调用方法。 另外方法中的内部类在用到方法中的参变量时,此参变也必须声明为final才可使用,如下代码所示: public class INClass{ void innerClass(final String str){ class IClass{ IClass(){ System.out.println(str); } } IClass ic=new IClass(); } public static void main(String[] args){ INClass inc=new INClass(); inc.innerClass("Hello"); } } final方法 将方法声明为final,那就说明你已经知道这个方法提供的功能已经满足你要求,不需要进行扩展,并且也不允许任何从此类继承的类来覆写这个方法,但是继承仍然可以继承这个方法,也就是说可以直接使用。另外有一种被称为inline的机制,它会使你在调用final方法时,直接将方法主体插入到调用处,而不是进行例行的方法调用,例如保存断点,压栈等,这样可能会使你的程序效率有所提高,然而当你的方法主体非常庞大时,或你在多处调用此方法,那么你的调用主体代码便会迅速膨胀,可能反而会影响效率,所以你要慎用final进行方法定义。 final类 当你将final用于类身上时,你就需要仔细考虑,因为一个final类是无法被任何人继承的,那也就意味着此类在一个继承树中是一个叶子类,并且此类的设计已被认为很完美而不需要进行修改或扩展。对于final类中的成员,你可以定义其为final,也可以不是final。而对于方法,由于所属类为final的关系,自然也就成了final型的。你也可以明确的给final类中的方法加上一个final,但这显然没有意义。 下面的程序演示了final方法和final类的用法: final class final{ final String str="final Data"; public String str1="non final data"; final public void print(){ System.out.println("final method."); } public void what(){ System.out.println(str+"\n"+str1); } } public class FinalDemo { //extends final 无法继承 public static void main(String[] args){ final f=new final(); f.what(); f.print(); } } 从程序中可以看出,final类与普通类的使用几乎没有差别,只是它失去了被继承的特性。final方法与非final方法的区别也很难从程序行看出,只是记住慎用。 final在设计模式中的应用 在设计模式中有一种模式叫做不变模式,在Java中通过final关键字可以很容易的实现这个模式,在讲解final成员时用到的程序Bat.java就是一个不变模式的例子。如果你对此感兴趣,可以参考阎宏博士编写的《Java与模式》一书中的讲解。 到此为止,this,static,supert和final的使用已经说完了,如果你对这四个关键字已经能够大致说出它们的区别与用法,那便说明你基本已经掌握。然而,世界上的任何东西都不是完美无缺的,Java提供这四个关键字,给程序员的编程带来了很大的便利,但并不是说要让你到处使用,一旦达到滥用的程序,便适得其反,所以在使用时请一定要认真考虑。
|
Java中this、super用法简谈 通过用static来定义方法或成员,为我们编程提供了某种便利,从某种程度上可以说它类似于C语言中的全局函数和全局变量。但是,并不是说有了这种便利,你便可以随处使用,如果那样的话,你便需要认真考虑一下自己是否在用面向对象的思想编程,自己的程序是否是面向对象的。好了,现在开始讨论this&super这两个关键字的意义和用法。 在Java中,this通常指当前对象,super则指父类的。当你想要引用当前对象的某种东西,比如当前对象的某个方法,或当前对象的某个成员,你便可以利用this来实现这个目的,当然,this的另一个用途是调用当前对象的另一个构造函数,这些马上就要讨论。如果你想引用父类的某种东西,则非super莫属。由于this与super有如此相似的一些特性和与生俱来的某种关系,所以我们在这一块儿来讨论,希望能帮助你区分和掌握它们两个。 在一般方法中 最普遍的情况就是,在你的方法中的某个形参名与当前对象的某个成员有相同的名字,这时为了不至于混淆,你便需要明确使用this关键字来指明你要使用某个成员,使用方法是“this.成员名”,而不带this的那个便是形参。另外,还可以用“this.方法名”来引用当前对象的某个方法,但这时this就不是必须的了,你可以直接用方法名来访问那个方法,编译器会知道你要调用的是那一个。下面的代码演示了上面的用法: public class DemoThis{ private String name; private int age; DemoThis(String name,int age){ setName(name); //你可以加上this来调用方法,像这样:this.setName(name);但这并不是必须的 setAge(age); this.print(); br> } public void setName(String name){ this.name=name;//此处必须指明你要引用成员变量 } public void etAge(int age){ this.age=age; } public void print(){ System.out.println("Name="+name+" ge="+age); //在此行中并不需要用this,因为没有会导致混淆的东西 } public static void main(String[] args){ DemoThis dt=new DemoThis("Kevin","22"); 这段代码很简单,不用解释你也应该能看明白。在构造函数中你看到用this.print(),你完全可以用print()来代替它,两者效果一样。下面我们修改这个程序,来演示super的用法。 class Person{ public int c; private String name; private int age; protected void setName(String name){ this.name=name; } protected void setAge(int age){ this.age=age; } protected void print(){ System.out.println("Name="+name+" Age="+age); } } public class DemoSuper extends Person{ public void print(){ System.out.println("DemoSuper:"); super.print(); } public static void main(String[] args){ DemoSuper ds=new DemoSuper(); ds.setName("kevin"); ds.setAge(22); ds.print(); } } 在DemoSuper中,重新定义的print方法覆写了父类的print方法,它首先做一些自己的事情,然后调用父类的那个被覆写了的方法。输出结果说明了这一点: DemoSuper: Name=kevin Age=22
这样的使用方法是比较常用的。另外如果父类的成员可以被子类访问,那你可以像使用this一样使用它,用“super.父类中的成员名”的方式,但常常你并不是这样来访问父类中的成员名的。 在构造函数中构造函数是一种特殊的方法,在对象初始化的时候自动调用。在构造函数中,this和super也有上面说的种种使用方式,并且它还有特殊的地方,请看下面的例子:
class Person{
public static void prt(String s){ System.out.println(s); } Person(){ prt("A Person."); } Person(String name){ prt("A person name is:"+name);
} } public class Chinese extends Person{ Chinese(){ super(); //调用父类构造函数(1) prt("A chinese.");//(4) } Chinese(String name){ super(name);//调用父类具有相同形参的构造函数(2) prt("his name is:"+name); } Chinese(String name,int age){ this(name);//调用当前具有相同形参的构造函数(3) prt("his age is:"+age); } public static void main(String[] args){ Chinese cn=new Chinese(); cn=new Chinese("kevin"); cn=new Chinese("kevin",22); } } 在这段程序中,this和super不再是像以前那样用“.”连接一个方法或成员,而是直接在其后跟 上适当的参数,因此它的意义也就有了变化。super后加参数的是用来调用父类中具有相同形式的 构造函数,如1和2处。this后加参数则调用的是当前具有相同参数的构造函数,如3处。当然,在 Chinese的各个重载构造函数中,this和super在一般方法中的各种用法也仍可使用,比如4处,你 可以将它替换为“this.prt”(因为它继承了父类中的那个方法)或者是“super.prt”(因为它 是父类中的方法且可被子类访问),它照样可以正确运行。但这样似乎就有点画蛇添足的味道 了。 最后,写了这么多,如果你能对“this通常指代当前对象,super通常指代父类”这句话牢记在 心,那么本篇便达到了目的,其它的你自会在以后的编程实践当中慢慢体会、掌握。另外关于本 篇中提到的继承,请参阅相关Java教程。
泊船瓜洲
@ 2005-08-30 18:08
Java中的类反射机制 (转帖) 一、反射的概念 : 反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问、检测和修改它本身状态或行为的一种能力。这一概念的提出很快引发了计算机科学领域关于应用反射性的研究。它首先被程序语言的设计领域所采用,并在Lisp和面向对象方面取得了成绩。其中LEAD/LEAD++ 、OpenC++ 、 MetaXa和OpenJava等就是基于反射机制的语言。最近,反射机制也被应用到了视窗系统、操作系统和文件系统中。
反射本身并不是一个新概念,它可能会使我们联想到光学中的反射概念,尽管计算机科学赋予了反射概念新的含义,但是,从现象上来说,它们确实有某些相通之处,这些有助于我们的理解。在计算机科学领域,反射是指一类应用,它们能够自描述和自控制。也就是说,这类应用通过采用某种机制来实现对自己行为的描述(self-representation)和监测(examination),并能根据自身行为的状态和结果,调整或修改应用所描述行为的状态和相关的语义。可以看出,同一般的反射概念相比,计算机科学领域的反射不单单指反射本身,还包括对反射结果所采取的措施。所有采用反射机制的系统(即反射系统)都希望使系统的实现更开放。可以说,实现了反射机制的系统都具有开放性,但具有开放性的系统并不一定采用了反射机制,开放性是反射系统的必要条件。一般来说,反射系统除了满足开放性条件外还必须满足原因连接(Causally-connected)。所谓原因连接是指对反射系统自描述的改变能够立即反映到系统底层的实际状态和行为上的情况,反之亦然。开放性和原因连接是反射系统的两大基本要素。13700863760
Java中,反射是一种强大的工具。它使您能够创建灵活的代码,这些代码可以在运行时装配,无需在组件之间进行源代表链接。反射允许我们在编写与执行时,使我们的程序代码能够接入装载到JVM中的类的内部信息,而不是源代码中选定的类协作的代码。这使反射成为构建灵活的应用的主要工具。但需注意的是:如果使用不当,反射的成本很高。
二、Java中的类反射: Reflection 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序对自身进行检查,或者说“自审”,并能直接操作程序的内部属性。Java 的这一能力在实际应用中也许用得不是很多,但是在其它的程序设计语言中根本就不存在这一特性。例如,Pascal、C 或者 C++ 中就没有办法在程序中获得函数定义相关的信息。
1.检测类:
1.1 reflection的工作机制
考虑下面这个简单的例子,让我们看看 reflection 是如何工作的。
import java.lang.reflect.*; public class DumpMethods { public static void main(String args[]) { try { Class c = Class.forName(args[0]); Method m[] = c.getDeclaredMethods(); for (int i = 0; i < m.length; i++) System.out.println(m.toString()); } catch (Throwable e) { System.err.println(e); } } }
按如下语句执行:
java DumpMethods java.util.Stack
它的结果输出为:
public java.lang.Object java.util.Stack.push(java.lang.Object)
public synchronized java.lang.Object java.util.Stack.pop()
public synchronized java.lang.Object java.util.Stack.peek()
public boolean java.util.Stack.empty()
public synchronized int java.util.Stack.search(java.lang.Object)
这样就列出了java.util.Stack 类的各方法名以及它们的限制符和返回类型。
这个程序使用 Class.forName 载入指定的类,然后调用 getDeclaredMethods 来获取这个类中定义了的方法列表。java.lang.reflect.Methods 是用来描述某个类中单个方法的一个类。
1.2 Java类反射中的主要方法
对于以下三类组件中的任何一类来说 -- 构造函数、字段和方法 -- java.lang.Class 提供四种独立的反射调用,以不同的方式来获得信息。调用都遵循一种标准格式。以下是用于查找构造函数的一组反射调用:
l Constructor getConstructor(Class[] params) -- 获得使用特殊的参数类型的公共构造函数,
l Constructor[] getConstructors() -- 获得类的所有公共构造函数
l Constructor getDeclaredConstructor(Class[] params) -- 获得使用特定参数类型的构造函数(与接入级别无关)
l Constructor[] getDeclaredConstructors() -- 获得类的所有构造函数(与接入级别无关)
获得字段信息的Class 反射调用不同于那些用于接入构造函数的调用,在参数类型数组中使用了字段名:
l Field getField(String name) -- 获得命名的公共字段
l Field[] getFields() -- 获得类的所有公共字段
l Field getDeclaredField(String name) -- 获得类声明的命名的字段
l Field[] getDeclaredFields() -- 获得类声明的所有字段
用于获得方法信息函数:
l Method getMethod(String name, Class[] params) -- 使用特定的参数类型,获得命名的公共方法
l Method[] getMethods() -- 获得类的所有公共方法
l Method getDeclaredMethod(String name, Class[] params) -- 使用特写的参数类型,获得类声明的命名的方法
l Method[] getDeclaredMethods() -- 获得类声明的所有方法
1.3开始使用 Reflection:
用于 reflection 的类,如 Method,可以在 java.lang.relfect 包中找到。使用这些类的时候必须要遵循三个步骤:第一步是获得你想操作的类的 java.lang.Class 对象。在运行中的 Java 程序中,用 java.lang.Class 类来描述类和接口等。
下面就是获得一个 Class 对象的方法之一:
Class c = Class.forName("java.lang.String");
这条语句得到一个 String 类的类对象。还有另一种方法,如下面的语句:
Class c = int.class;
或者
Class c = Integer.TYPE;
它们可获得基本类型的类信息。其中后一种方法中访问的是基本类型的封装类 (如 Integer) 中预先定义好的 TYPE 字段。
第二步是调用诸如 getDeclaredMethods 的方法,以取得该类中定义的所有方法的列表。
一旦取得这个信息,就可以进行第三步了——使用 reflection API 来操作这些信息,如下面这段代码:
Class c = Class.forName("java.lang.String");
Method m[] = c.getDeclaredMethods();
System.out.println(m[0].toString());
它将以文本方式打印出 String 中定义的第一个方法的原型。
2.处理对象:
如果要作一个开发工具像debugger之类的,你必须能发现filed values,以下是三个步骤:
a.创建一个Class对象 b.通过getField 创建一个Field对象 c.调用Field.getXXX(Object)方法(XXX是Int,Float等,如果是对象就省略;Object是指实例).
例如: import java.lang.reflect.*; import java.awt.*;
class SampleGet {
public static void main(String[] args) { Rectangle r = new Rectangle(100, 325); printHeight(r);
}
static void printHeight(Rectangle r) { Field heightField; Integer heightvalue; Class c = r.getClass(); try { heightField = c.getField("height"); heightvalue = (Integer) heightField.get(r); System.out.println("Height: " + heightvalue.toString()); } catch (NoSuchFieldException e) { System.out.println(e); } catch (SecurityException e) { System.out.println(e); } catch (IllegalAccessException e) { System.out.println(e); } } }
三、安全性和反射: 在处理反射时安全性是一个较复杂的问题。反射经常由框架型代码使用,由于这一点,我们可能希望框架能够全面接入代码,无需考虑常规的接入限制。但是,在其它情况下,不受控制的接入会带来严重的安全性风险,例如当代码在不值得信任的代码共享的环境中运行时。
由于这些互相矛盾的需求,Java编程语言定义一种多级别方法来处理反射的安全性。基本模式是对反射实施与应用于源代码接入相同的限制:
n 从任意位置到类公共组件的接入
n 类自身外部无任何到私有组件的接入
n 受保护和打包(缺省接入)组件的有限接入
不过至少有些时候,围绕这些限制还有一种简单的方法。我们可以在我们所写的类中,扩展一个普通的基本类 java.lang.reflect.AccessibleObject 类。这个类定义了一种setAccessible方法,使我们能够启动或关闭对这些类中其中一个类的实例的接入检测。唯一的问题在于如果使用了安全性管理器,它将检测正在关闭接入检测的代码是否许可了这样做。如果未许可,安全性管理器抛出一个例外。
下面是一段程序,在TwoString 类的一个实例上使用反射来显示安全性正在运行:
public class ReflectSecurity {
public static void main(String[] args) {
try {
TwoString ts = new TwoString("a", "b");
Field field = clas.getDeclaredField("m_s1");
// field.setAccessible(true);
System.out.println("Retrieved value is " +
field.get(inst));
} catch (Exception ex) {
ex.printStackTrace(System.out);
}
}
}
如果我们编译这一程序时,不使用任何特定参数直接从命令行运行,它将在field .get(inst)调用中抛出一个 IllegalAccessException异常。如果我们不注释field.setAccessible(true)代码行,那么重新编译并重新运行该代码,它将编译成功。最后,如果我们在命令行添加了JVM参数-Djava.security.manager以实现安全性管理器,它仍然将不能通过编译,除非我们定义了ReflectSecurity类的许可权限。
四、反射性能: 反射是一种强大的工具,但也存在一些不足。一个主要的缺点是对性能有影响。使用反射基本上是一种解释操作,我们可以告诉JVM,我们希望做什么并且它满足我们的要求。这类操作总是慢于只直接执行相同的操作。
下面的程序是字段接入性能测试的一个例子,包括基本的测试方法。每种方法测试字段接入的一种形式 -- accessSame 与同一对象的成员字段协作,accessOther 使用可直接接入的另一对象的字段,accessReflection 使用可通过反射接入的另一对象的字段。在每种情况下,方法执行相同的计算 -- 循环中简单的加/乘顺序。
程序如下:
public int accessSame(int loops) {
m_value = 0;
for (int index = 0; index < loops; index++) {
m_value = (m_value + ADDITIVE_value) *
MULTIPLIER_value;
}
return m_value;
}
public int accessReference(int loops) {
TimingClass timing = new TimingClass();
for (int index = 0; index < loops; index++) {
timing.m_value = (timing.m_value + ADDITIVE_value) *
MULTIPLIER_value;
}
return timing.m_value;
}
public int accessReflection(int loops) throws Exception {
TimingClass timing = new TimingClass();
try {
Field field = TimingClass.class.
getDeclaredField("m_value");
for (int index = 0; index < loops; index++) {
int value = (field.getInt(timing) +
ADDITIVE_value) * MULTIPLIER_value;
field.setInt(timing, value);
}
return timing.m_value;
} catch (Exception ex) {
System.out.println("Error using reflection");
throw ex;
}
}
在上面的例子中,测试程序重复调用每种方法,使用一个大循环数,从而平均多次调用的时间衡量结果。平均值中不包括每种方法第一次调用的时间,因此初始化时间不是结果中的一个因素。下面的图清楚的向我们展示了每种方法字段接入的时间:
图 1:字段接入时间 :
我们可以看出:在前两副图中(Sun JVM),使用反射的执行时间超过使用直接接入的1000倍以上。通过比较,IBM JVM可能稍好一些,但反射方法仍旧需要比其它方法长700倍以上的时间。任何JVM上其它两种方法之间时间方面无任何显著差异,但IBM JVM几乎比Sun JVM快一倍。最有可能的是这种差异反映了Sun Hot Spot JVM的专业优化,它在简单基准方面表现得很糟糕。反射性能是Sun开发1.4 JVM时关注的一个方面,它在反射方法调用结果中显示。在这类操作的性能方面,Sun 1.4.1 JVM显示了比1.3.1版本很大的改进。
如果为为创建使用反射的对象编写了类似的计时测试程序,我们会发现这种情况下的差异不象字段和方法调用情况下那么显著。使用newInstance()调用创建一个简单的java.lang.Object实例耗用的时间大约是在Sun 1.3.1 JVM上使用new Object()的12倍,是在 IBM 1.4.0 JVM的四倍,只是Sun 1.4.1 JVM上的两部。使用Array.newInstance(type, size)创建一个数组耗用的时间是任何测试的JVM上使用new type[size]的两倍,随着数组大小的增加,差异逐步缩小。
结束语: Java语言反射提供一种动态链接程序组件的多功能方法。它允许程序创建和控制任何类的对象(根据安全性限制),无需提前硬编码目标类。这些特性使得反射特别适用于创建以非常普通的方式与对象协作的库。例如,反射经常在持续存储对象为数据库、XML或其它外部格式的框架中使用。 Java reflection 非常有用,它使类和数据结构能按名称动态检索相关信息,并允许在运行着的程序中操作这些信息。Java 的这一特性非常强大,并且是其它一些常用语言,如 C、C++、Fortran 或者 Pascal 等都不具备的。
但反射有两个缺点。第一个是性能问题。用于字段和方法接入时反射要远慢于直接代码。性能问题的程度取决于程序中是如何使用反射的。如果它作为程序运行中相对很少涉及的部分,缓慢的性能将不会是一个问题。即使测试中最坏情况下的计时图显示的反射操作只耗用几微秒。仅反射在性能关键的应用的核心逻辑中使用时性能问题才变得至关重要。
许多应用中更严重的一个缺点是使用反射会模糊程序内部实际要发生的事情。程序人员希望在源代码中看到程序的逻辑,反射等绕过了源代码的技术会带来维护问题。反射代码比相应的直接代码更复杂,正如性能比较的代码实例中看到的一样。解决这些问题的最佳方案是保守地使用反射——仅在它可以真正增加灵活性的地方 ——记录其在目标类中的使用。
利用反射实现类的动态加载
Bromon原创 请尊重版权
最近在成都写一个移动增值项目,俺负责后台server端。功能很简单,手机用户通过GPRS打开Socket与服务器连接,我则根据用户传过来的数据做出响应。做过类似项目的兄弟一定都知道,首先需要定义一个类似于MSNP的通讯协议,不过今天的话题是如何把这个系统设计得具有高度的扩展性。由于这个项目本身没有进行过较为完善的客户沟通和需求分析,所以以后肯定会有很多功能上的扩展,通讯协议肯定会越来越庞大,而我作为一个不那么勤快的人,当然不想以后再去修改写好的程序,所以这个项目是实践面向对象设计的好机会。
首先定义一个接口来隔离类:
package org.bromon.reflect;
public interface Operator
{
public java.util.List act(java.util.List params)
}
根据设计模式的原理,我们可以为不同的功能编写不同的类,每个类都继承Operator接口,客户端只需要针对Operator接口编程就可以避免很多麻烦。比如这个类:
package org.bromon.reflect.*;
public class Success implements Operator
{
public java.util.List act(java.util.List params)
{
List result=new ArrayList();
result.add(new String(“操作成功”));
return result;
}
}
我们还可以写其他很多类,但是有个问题,接口是无法实例化的,我们必须手动控制具体实例化哪个类,这很不爽,如果能够向应用程序传递一个参数,让自己去选择实例化一个类,执行它的act方法,那我们的工作就轻松多了。
很幸运,我使用的是Java,只有Java才提供这样的反射机制,或者说内省机制,可以实现我们的无理要求。编写一个配置文件emp.properties:
#成功响应
1000=Success
#向客户发送普通文本消息
2000=Load
#客户向服务器发送普通文本消息
3000=Store
文件中的键名是客户将发给我的消息头,客户发送1000给我,那么我就执行Success类的act方法,类似的如果发送2000给我,那就执行Load 类的act方法,这样一来系统就完全符合开闭原则了,如果要添加新的功能,完全不需要修改已有代码,只需要在配置文件中添加对应规则,然后编写新的类,实现act方法就ok,即使我弃这个项目而去,它将来也可以很好的扩展。这样的系统具备了非常良好的扩展性和可插入性。
下面这个例子体现了动态加载的功能,程序在执行过程中才知道应该实例化哪个类:
package org.bromon.reflect.*;
import java.lang.reflect.*;
public class TestReflect
{
//加载配置文件,查询消息头对应的类名
private String loadProtocal(String header)
{
String result=null;
try
{
Properties prop=new Properties();
FileInputStream fis=new FileInputStream("emp.properties");
prop.load(fis);
result=prop.getProperty(header);
fis.close();
}catch(Exception e)
{
System.out.println(e);
}
return result;
}
//针对消息作出响应,利用反射导入对应的类
public String response(String header,String content)
{
String result=null;
String s=null;
try
{
/*
* 导入属性文件emp.properties,查询header所对应的类的名字
* 通过反射机制动态加载匹配的类,所有的类都被Operator接口隔离
* 可以通过修改属性文件、添加新的类(继承MsgOperator接口)来扩展协议
*/
s="org.bromon.reflect."+this.loadProtocal(header);
//加载类
Class c=Class.forName(s);
//创建类的事例
Operator mo=(Operator)c.newInstance();
//构造参数列表
Class params[]=new Class[1];
params[0]=Class.forName("java.util.List");
//查询act方法
Method m=c.getMethod("act",params);
Object args[]=new Object[1];
args[0]=content;
//调用方法并且获得返回
Object returnObject=m.invoke(mo,args);
}catch(Exception e)
{
System.out.println("Handler-response:"+e);
}
return result;
}
public static void main(String args[])
{
TestReflect tr=new TestReflect();
tr.response(args[0],”消息内容”);
}
}
测试一下:java TestReflect 1000
这个程序是针对Operator编程的,所以无需做任何修改,直接提供Load和Store类,就可以支持2000、3000做参数的调用。
有了这样的内省机制,可以把接口的作用发挥到极至,设计模式也更能体现出威力,而不仅仅供我们饭后闲聊。
array(数组)和Vector是十分相似的Java构件(constructs),两者全然不同,在选择使用时应根据各自的功能来确定。
1、数组:Java arrays的元素个数不能下标越界,从很大程度上保证了Java程序的安全性,而其他一些语言出现这一问题时常导致灾难性的后果。 Array可以存放Object和基本数据类型,但创建时必须指定数组的大小,并不能再改变。值得注意的是:当Array中的某一元素存放的是Objrct reference 时,Java不会调用默认的构造函数,而是将其初值设为null,当然这跟Java对各类型数据赋默认值的规则是一样的,对基本数据类型同样适用。
2、Vector:对比于Array,当更多的元素被加入进来以至超出其容量时,Vector的size会动态增长,而Array容量是定死的。同时,Vector在删除一些元素后,其所有下标大于被删除元素的元素都依次前移,并获得新下标比原来的小了)。注意:当调用Vector的size()方法时,返回Vector中实际元素的个数。 Vector内部实际是以Array实现的,也通过元素的整数索引来访问元素,但它只能存放java.lang.Object对象,不能用于存放基本类型数据,比如要存放一个整数10,得用new Integer(10)构造出一个Integer包装类对象再放进去。当Vector中的元素个数发生变化时, 其内部的Array必须重新分配并进行拷贝,因此这是一点值得考虑的效率问题。 Vetor同时也实现了List接口,所以也可以算作Colletion了,只是它还特殊在:Vector is synchronized。即Vetor对象自身实现了同步机制。
3、ArrayList:实现了List接口,功能与Vetor一样,只是没有同步机制,当然元素的访问方式为从List中继承而来,可存放任何类型的对象。
4、HashMap:继承了Map接口,实现用Keys来存储和访问Values,Keys和Values都可以为空,它与Hashtable类的区别在于Hashtable类的Keys不能为null,并Hashtable类有同步机制控制,而HashMap类没有。 在Struts类库中实现了一个LableValueBean,用Lable(Key)来存储和访问Value,很方便。
本文提供一个项目中的错误实例,提供对其观察和分析,揭示出 Java语言实例化一个对象具体过程,最后总结出设计Java类的一个重要规则。通过阅读本文,可以使Java程序员理解Java对象的构造过程,从而设计出更加健壮的代码。本文适合Java初学者和需要提高的Java程序员阅读。 程序掷出了一个异常 作者曾经在一个项目里面向项目组成员提供了一个抽象的对话框基类,使用者只需在子类中实现基类的一个抽象方法来画出显示数据的界面,就可使项目内的对话框具有相同的风格。具体的代码实现片断如下(为了简洁起见,省略了其他无关的代码): public abstract class BaseDlg extends JDialog { public BaseDlg(Frame frame, String title) { super(frame, title, true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH); this.getContentPane().add(createClientPanel(), BorderLayout.CENTER); this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); }
private JPanel createHeadPanel() { ... // 创建对话框头部 }
// 创建对话框客户区域,交给子类实现 protected abstract JPanel createClientPanel();
private JPanel createButtonPanel { ... // 创建按钮区域 } } |
这个类在有的代码中工作得很好,但一个同事在使用时,程序却掷出了一个NullPointerException违例!经过比较,找出了工作正常和不正常的程序的细微差别,代码片断分别如下: 一、正常工作的代码: public class ChildDlg1 extends BaseDlg { JTextField jTextFieldName; public ChildDlg1() { super(null, "Title"); } public JPanel createClientPanel() { jTextFieldName = new JTextField(); JPanel panel = new JPanel(new FlowLayout()); panel.add(jTextFieldName); ... // 其它代码 return panel; } ... } ChildDlg1 dlg = new ChildDlg1(frame, "Title"); // 外部的调用 |
二、工作不正常的代码: public class ChildDlg2 extends BaseDlg { JTextField jTextFieldName = new JTextField(); public ChildDlg2() { super(null, "Title"); } public JPanel createClientPanel() { JPanel panel = new JPanel(new FlowLayout()); panel.add(jTextFieldName); ... // 其它代码 return panel; } ... } ChildDlg2 dlg = new ChildDlg2(); // 外部的调用 |
你看出来两段代码之间的差别了吗?对了,两者的差别仅仅在于类变量jTextFieldName的初始化时间。经过跟踪,发现在执行panel.add(jTextFieldName)语句之时,jTextFieldName确实是空值。 我们知道,Java允许在定义类变量的同时给变量赋初始值。系统运行过程中需要创建一个对象的时候,首先会为对象分配内存空间,然后在“先于调用任何方法之前”根据变量在类内的定义顺序来初始化变量,接着再调用类的构造方法。那么,在本例中,为什么在变量定义时便初始化的代码反而会出现空指针违例呢? 对象的创建过程和初始化 实际上,前面提到的“变量初始化发生在调用任何方法包括构造方法之前”这句话是不确切的,当我们把眼光集中在单个类上时,该说法成立;然而,当把视野扩大到具有继承关系的两个或多个类上时,该说法不成立。 对象的创建一般有两种方式,一种是用new操作符,另一种是在一个Class对象上调用newInstance方法;其创建和初始化的实际过程是一样的: 首先为对象分配内存空间,包括其所有父类的可见或不可见的变量的空间,并初始化这些变量为默认值,如int类型为0,boolean类型为false,对象类型为null; 然后用下述5个步骤来初始化这个新对象: 1)分配参数给指定的构造方法; 2)如果这个指定的构造方法的第一个语句是用this指针显式地调用本类的其它构造方法,则递归执行这5个步骤;如果执行过程正常则跳到步骤5; 3)如果构造方法的第一个语句没有显式调用本类的其它构造方法,并且本类不是Object类(Object是所有其它类的祖先),则调用显式(用super指针)或隐式地指定的父类的构造方法,递归执行这5个步骤;如果执行过程正常则跳到步骤5; 4)按照变量在类内的定义顺序来初始化本类的变量,如果执行过程正常则跳到步骤5; 5)执行这个构造方法中余下的语句,如果执行过程正常则过程结束。 这一过程可以从下面的时序图中获得更清晰的认识: 对分析本文的实例最重要的,用一句话说,就是“父类的构造方法调用发生在子类的变量初始化之前”。可以用下面的例子来证明: // Petstore.java class Animal { Animal() { System.out.println("Animal"); } } class Cat extends Animal { Cat() { System.out.println("Cat"); } } class Store { Store() { System.out.println("Store"); } } public class Petstore extends Store{ Cat cat = new Cat(); Petstore() { System.out.println("Petstore"); } public static void main(String[] args) { new Petstore(); } } |
运行这段代码,它的执行结果如下: Store Animal Cat Petstore 从结果中可以看出,在创建一个Petstore类的实例时,首先调用了它的父类Store的构造方法;然后试图创建并初始化变量cat;在创建cat时,首先调用了Cat类的父类Animal的构造方法;其后才是Cat的构造方法主体,最后才是Petstore类的构造方法的主体。 寻找程序产生例外的原因
现在回到本文开始提到的实例中来,当程序创建一个ChildDlg2的实例时,根据super(null, “Title”)语句,首先执行其父类BaseDlg的构造方法;在BaseDlg的构造方法中调用了createClientPanel()方法,
这个方法是抽象方法并且被子类ChildDlg2实现了,因此,实际调用的方法是ChildDlg2中的createClientPanel()方法(因为Java里面采用“动态绑定”来绑定所有非final的方法);createClientPanel()方法使用了ChildDlg2类的实例变量jTextFieldName,而此时ChildDlg2的变量初始化过程尚未进行,jTextFieldName是null值!所以,ChildDlg2的构造过程掷出一个NullPointerException也就不足为奇了。
再来看ChildDlg1,它的jTextFieldName的初始化代码写在了createClientPanel()方法内部的开始处,这样它就能保证在使用之前得到正确的初始化,因此这段代码工作正常。
解决问题的两种方式
通过上面的分析过程可以看出,要排除故障,最简单的方法就是要求项目组成员在继承使用BaseDlg类,实现createClientPanel()方法时,凡方法内部要使用的变量必须首先正确初始化,就象ChildDlg1一样。然而,把类变量放在类方法内初始化是一种很不好的设计行为,它最适合的地方就是在变量定义块和构造方法中。
在本文的实例中,引发错误的实质并不在ChildDlg2上,而在其父类BaseDlg上,是它在自己的构造方法中不适当地调用了一个待实现的抽象方法。
从概念上讲,构造方法的职责是正确初始化类变量,让对象进入可用状态。而BaseDlg却赋给了构造方法额外的职责。
本文实例的更好的解决方法是修改BaseDlg类:
public abstract class BaseDlg extends JDialog { public BaseDlg(Frame frame, String title) { super(frame, title, true); this.getContentPane().setLayout(new BorderLayout()); this.getContentPane().add(createHeadPanel(), BorderLayout.NORTH); this.getContentPane().add(createButtonPanel(), BorderLayout.SOUTH); }
/** 创建对话框实例后,必须调用此方法来布局用户界面 */ public void initGUI() { this.getContentPane().add(createClientPanel(), BorderLayout.CENTER); }
private JPanel createHeadPanel() { ... // 创建对话框头部 }
// 创建对话框客户区域,交给子类实现 protected abstract JPanel createClientPanel();
private JPanel createButtonPanel { ... // 创建按钮区域 } } |
新的BaseDlg类增加了一个initGUI()方法,程序员可以这样使用这个类:
ChildDlg dlg = new ChildDlg(); dlg.initGUI(); dlg.setVisible(true); |
总结
类的构造方法的基本目的是正确初始化类变量,不要赋予它过多的职责。
设计类构造方法的基本规则是:用尽可能简单的方法使对象进入就绪状态;如果可能,避免调用任何方法。在构造方法内唯一能安全调用的是基类中具有final属性的方法或者private方法(private方法会被编译器自动设置final属性)。final的方法因为不能被子类覆盖,所以不会产生问题。 | |
所以那些有子类实现的方法尽量不要放在父类的构造函数中,一定要注意设计好构造函数。
这个问题一直困扰我很久,一直没有完全的理解: abstractclass和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。
abstractclass和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于abstractclass和interface的选择显得比较随意。其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。理解抽象类 abstractclass和interface在Java语言中都是用来进行抽象类(本文中的抽象类并非从abstractclass翻译而来,它表示的是一个抽象体,而abstractclass为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为我们带来什么好处呢? 在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。 在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读者一定知道,为了能够实现面向对象设计的一个最核心的原则OCP(Open-ClosedPrinciple),抽象类是其中的关键所在。 从语法定义层面看abstractclass和interface 在语法层面,Java语言对于abstractclass和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。 使用abstractclass的方式定义Demo抽象类的方式如下: abstractclassDemo{ abstractvoidmethod1(); abstractvoidmethod2(); … } 使用interface的方式定义Demo抽象类的方式如下: interfaceDemo{ voidmethod1(); voidmethod2(); … } 在abstractclass方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是staticfinal的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstractclass。 从编程的角度来看,abstractclass和interface都可以用来实现"designbycontract"的思想。但是在具体的使用上面还是有一些区别的。 首先,abstractclass在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。 其次,在abstractclass的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会增加一些复杂性,有时会造成很大的麻烦。 在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstractclass或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstractclass来实现的,那么可能就只需要修改定义在abstractclass中的默认行为就可以了。 同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"onerule,oneplace"原则,造成代码重复,同样不利于以后的维护。因此,在abstractclass和interface间进行选择时要非常的小心。 从设计理念层面看abstractclass和interface 上面主要从语法定义和编程的角度论述了abstractclass和interface的区别,这些层面的区别是比较低层次的、非本质的。本小节将从另一个层面:abstractclass和interface所反映出的设计理念,来分析一下二者的区别。作者认为,从这个层面进行分析才能理解二者概念的本质所在。 前面已经提到过,abstarctclass在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"isa"关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"isa"关系的大篇幅深入的论述,有兴趣的读者可以参考)。对于interface来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。 考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstractclass或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示: 使用abstractclass方式定义Door: abstractclassDoor{ abstractvoidopen(); abstractvoidclose(); } 使用interface方式定义Door: interfaceDoor{ voidopen(); voidclose(); } 其他具体的Door类型可以extends使用abstractclass方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstractclass和interface没有大的区别。 | |
如果现在要求Door还要具有报警的功能。
我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstractclass和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。解决方案一: 简单的在Door的定义中增加一个alarm方法,如下: abstractclassDoor{ abstractvoidopen(); abstractvoidclose(); abstractvoidalarm(); } 或者 interfaceDoor{ voidopen(); voidclose(); voidalarm(); } 那么具有报警功能的AlarmDoor的定义方式如下: classAlarmDoorextendsDoor{ voidopen(){…} voidclose(){…} voidalarm(){…} } 或者 classAlarmDoorimplementsDoor{ voidopen(){…} voidclose(){…} voidalarm(){…} } 这种方法违反了面向对象设计中的一个核心原则ISP(InterfaceSegregationPriciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。 解决方案二: 既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstractclass方式定义;两个概念都使用interface方式定义;一个概念使用abstractclass方式定义,另一个概念使用interface方式定义。 显然,由于Java语言不支持多重继承,所以两个概念都使用abstractclass方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。我们一一来分析、说明。 如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。 如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstractclass在Java语言中表示一种继承关系,而继承关系在本质上是"isa"关系。所以对于Door这个概念,我们应该使用abstarctclass方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示: abstractclassDoor{ abstractvoidopen(); abstractvoidclose(); } interfaceAlarm{ voidalarm(); } classAlarmDoorextendsDoorimplementsAlarm{ voidopen(){…} voidclose(){…} voidalarm(){…} } 这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstractclass表示的是"isa"关系,interface表示的是"likea"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。 | |
好多同志对 iframe 是如何控制的,并不是十分了解,基本上还处于一个模糊的认识状态.
注意两个事项,ifr 是一个以存在的 iframe 的 ID 和 NAME 值:  document.getElementById(“ifr”);  window.frames[“ifr”];
要想使用iframe内的函数,变量就必须通过第二种方法.因为它取的是一个完整的DOM模型(不知道这样说对不对).第一种方法只是取出了一个OBJECT而已.
如果只想改变iframe的 src 或者 border , scrolling 等 attributes(与property不是一个概念,property是不能写在标签内的,比如:scrollHeight,innerHTML等),就需要用到第一种方法.
如果想取得iframe的页面(不是iframe本身),就需要使用第二种方法,因为它取得的是一个完整的DOM模型,比如想得到iframe的document.body的内容,就只能用第二种方法.
还要注意的是,如果在iframe的页面未完全装入的时候,调用iframe的DOM模型,会发生很严重的错误,所以,你要准备一个容错模式.
下面是示
网页头部标签meta详解
关键词: 收集meta资料
网页头部标签meta详解 网页头部有两个标签titel和meta, title比较简单,就是网页标题,meta的内容还是蛮多了,为了自己今后能够比较好的参考,我收集了些资料,也希望对大家有用。 一、meta标签的组成 meta标签共有两个属性,它们分别是http-equiv属性和name属性,不同的属性又有不同的参数值,这些不同的参数值就实现了不同的网页功能。 1、name属性 name属性主要用于描述网页,与之对应的属性值为content,content中的内容主要是便于搜索引擎机器人查找信息和分类信息用的。meat标签的name属性语法格式是:<meta name="参数" content="具体的参数值"> 。 其中name属性主要有以下几种参数: A、Keywords(关键字) 说明:keywords用来告诉搜索引擎你网页的关键字是什么。 举例:<meta name ="keywords" content="science, education,culture,politics,ecnomics,relationships, entertaiment, human"> B、description(网站内容描述) 说明:description用来告诉搜索引擎你的网站主要内容。 举例:<meta name="description" content="This page is about the meaning of science, education,culture."> C、robots(机器人向导) 说明:robots用来告诉搜索机器人哪些页面需要索引,哪些页面不需要索引。 content的参数有all,none,index,noindex,follow,nofollow。默认是all。 举例:<meta name="robots" content="none"> D、author(作者) 说明:标注网页的作者 举例:<meta name="author" content="***@yahoo.com.con"> 2、http-equiv属性 http-equiv顾名思义,相当于http的文件头作用,它可以向浏览器传回一些有用的信息,以帮助正确和精确地显示网页内容,与之对应的属性值为content,content中的内容其实就是各个参数的变量值。 meat标签的http-equiv属性语法格式是:<meta http-equiv="参数" content="参数变量值"> ;其中http-equiv属性主要有以下几种参数: A、Expires(期限) 说明:可以用于设定网页的到期时间。一旦网页过期,必须到服务器上重新传输。 用法:<meta http-equiv="expires" content="Fri, 12 Jan 2001 18:18:18 GMT"> 注意:必须使用GMT的时间格式。 B、Pragma(cache模式) 说明:禁止浏览器从本地计算机的缓存中访问页面内容。 用法:<meta http-equiv="Pragma" content="no-cache"> 注意:这样设定,访问者将无法脱机浏览。 C、Refresh(刷新) 说明:自动刷新并指向新页面。 用法:<meta http-equiv="Refresh" content="2;URL=http://phigo.bokee.com"> 注意:其中的2是指停留2秒钟后自动刷新到URL网址。 D、Set-Cookie(cookie设定) 说明:如果网页过期,那么存盘的cookie将被删除。 用法:<meta http-equiv="Set-Cookie" content="cookievalue=xxx; expires=Friday, 8-Aug -2008 18:18:18 GMT; path=/"> 注意:必须使用GMT的时间格式。 E、Window-target(显示窗口的设定) 说明:强制页面在当前窗口以独立页面显示。 用法:<meta http-equiv="Window-target" content="_top"> 注意:用来防止别人在框架里调用自己的页面。 F、content-Type(显示字符集的设定) 说明:设定页面使用的字符集。 用法:<meta http-equiv="content-Type" content="text/html; charset=gb2312"> G、Page-Exit /Page-Enter(进入页面、离开页面动画效果) 使用meta标签,我们还可以在进入网页或者离开网页的一刹那实现动画效果,我们只要在页面的html代码中的<head></head>标签之间添加如下代码就可以了: <meta http-equiv="Page-Enter" content="revealTrans(duration=5, transition=23)"> <meta http-equiv="Page-Exit" content="revealTrans(duration=5, transition=23)"> 一旦上述代码被加到一个网页中后,我们再进出页面时就会看到一些特殊效果,这个功能其实与FrontPage2000中的Format/Page Transition一样,但我们要注意的是所加网页不能是一个Frame页;Duration的值为网页动态过渡的时间,单位为秒。Transition是过渡方式,它的值为0到23,分别对应24种过渡方式。如下表: 0 盒状收缩 | 1 盒状放射 | 2 圆形收缩 | 3 圆形放射 | 4 由下往上 | 5 由上往下 | 6 从左至右 | 7 从右至左 | 8 垂直百叶窗 | 9 水平百叶窗 | 10 水平格状百叶窗 | 11垂直格状百叶窗 | 12 随意溶解 | 13从左右两端向中间展开 | 14从中间向左右两端展开 | 15从上下两端向中间展开 | 16从中间向上下两端展开 | 17 从右上角向左下角展开 | 18 从右下角向左上角展开 | 19 从左上角向右下角展开 | 20 从左下角向右上角展开 | 21 水平线状展开 | 22 垂直线状展开 | 23 随机产生一种过渡方式 |
我们已经知道,在 document 对象中有一个 cookie 属性。但是 Cookie 又是什么?“某些 Web 站点在您的硬盘上用很小的文本文件存储了一些信息,这些文件就称为 Cookie。”—— MSIE 帮助。一般来说,Cookies 是 CGI 或类似,比 HTML 高级的文件、程序等创建的,但是 javascript 也提供了对 Cookies 的很全面的访问权利。 我们先要学一学 Cookie 的基本知识。 每个 Cookie 都是这样的:<cookie名>=<值> <cookie名>的限制与 javascript 的命名限制大同小异,少了“不能用 javascript 关键字”,多了“只能用可以用在 URL 编码中的字符”。后者比较难懂,但是只要你只用字母和数字命名,就完全没有问题了。<值>的要求也是“只能用可以用在 URL 编码中的字符”。 每个 Cookie 都有失效日期,一旦电脑的时钟过了失效日期,这个 Cookie 就会被删掉。我们不能直接删掉一个 Cookie,但是可以用设定失效日期早于现在时刻的方法来间接删掉它。 每个网页,或者说每个站点,都有它自己的 Cookies,这些 Cookies 只能由这个站点下的网页来访问,来自其他站点或同一站点下未经授权的区域的网页,是不能访问的。每一“组”Cookies 有规定的总大小(大约 2KB 每“组”),一超过最大总大小,则最早失效的 Cookie 先被删除,来让新的 Cookie“安家”。 现在我们来学习使用 documents.cookie 属性。 如果直接使用 document.cookie 属性,或者说,用某种方法,例如给变量赋值,来获得 document.cookie 的值,我们就可以知道在现在的文档中有多少个 Cookies,每个 Cookies 的名字,和它的值。例如,在某文档中添加“document.write(document.cookie)”,结果显示: name=kevin; email=kevin@kevin.com; lastvisited=index.html 这意味着,文档包含 3 个 Cookies:name, email 和 lastvisited,它们的值分别是 kevin, kevin@kevin.com 和 index.html。可以看到,两个 Cookies 之间是用分号和空格隔开的,于是我们可以用 cookieString.split('; ') 方法得到每个 Cookie 分开的一个数组(先用 var cookieString = document.cookie)。 设定一个 Cookie 的方法是对 documents.cookie 赋值。与其它情况下的赋值不同,向 documents.cookie 赋值不会删除掉原有的 Cookies,而只会增添 Cookies 或更改原有 Cookie。赋值的格式: documents.cookie = 'cookieName=' + escape('cookievalue') + ';expires=' + expirationDateObj.toGMTString(); 是不是看到头晕了呢?cookieName 表示 Cookie 的名称,cookievalue 表示 Cookie 的值,expirationDateObj 表示储存着失效日期的日期对象名,如果不需要指定失效日期,则不需要第二行。不指定失效日期,则浏览器默认是在关闭浏览器(也就是关闭所有窗口)之后过期。 首先 escape() 方法:为什么一定要用?因为 Cookie 的值的要求是“只能用可以用在 URL 编码中的字符”。我们知道“escape()”方法是把字符串按 URL 编码方法来编码的,那我们只需要用一个“escape()”方法来处理输出到 Cookie 的值,用“unescape()”来处理从 Cookie 接收过来的值就万无一失了。而且这两个方法的最常用途就是处理 Cookies。其实设定一个 Cookie 只是“documents.cookie = 'cookieName=cookievalue'”这么简单,但是为了避免在 cookievalue 中出现 URL 里不准出现的字符,还是用一个 escape() 好。 然后“expires”前面的分号:注意到就行了。是分号而不是其他。 最后 toGMTString() 方法:设定 Cookie 的时效日期都是用 GMT 格式的时间的,其它格式的时间是没有作用的。 现在我们来实战一下。设定一个“name=rose”的 Cookie,在 3 个月后过期。 var expires = new Date(); expires.setTime(expires.getTime() + 3 * 30 * 24 * 60 * 60 * 1000); /* 三个月 x 一个月当作 30 天 x 一天 24 小时 x 一小时 60 分 x 一分 60 秒 x 一秒 1000 毫秒 */ documents.cookie = 'name=rose;expires=' + expires.toGMTString(); 为什么没有用 escape() 方法?这是因为我们知道 rose 是一个合法的 URL 编码字符串,也就是说,'rose' == escape('rose')。一般来说,如果设定 Cookie 时不用 escape(),那获取 Cookie 时也不用 unescape()。 再来一次:编写一个函数,作用是查找指定 Cookie 的值。 function getCookie(cookieName) { var cookieString = documents.cookie; var start = cookieString.indexOf(cookieName + '='); // 加上等号的原因是避免在某些 Cookie 的值里有 // 与 cookieName 一样的字符串。 if (start == -1) // 找不到 return null; start += cookieName.length + 1; var end = cookieString.indexOf(';', start); if (end == -1) return unescape(cookieString.substring(start)); return unescape(cookieString.substring(start, end)); } 这个函数用到了字符串对象的一些方法,如果你不记得了(你是不是这般没记性啊),请快去查查。这个函数所有的 if 语句都没有带上 else,这是因为如果条件成立,程序运行的都是 return 语句,在函数里碰上 return,就会终止运行,所以不加 else 也没问题。该函数在找到 Cookie 时,就会返回 Cookie 的值,否则返回“null”。 现在我们要删除刚才设定的 name=rose Cookie。 var expires = new Date(); expires.setTime(expires.getTime() - 1); documents.cookie = 'name=rose;expires=' + expires.toGMTString();
Java 多线程程序设计要点(synchronized)
Tag:JAVA基础
多线程程序设计要点: 1.多线程中有主内存和工作内存之分, 在JVM中,有一个主内存,专门负责所有线程共享数据;而每个线程都有他自己私有的工作内存, 主内存和工作内存分贝在JVM的stack区和heap区。 2.线程的状态有'Ready', 'Running', 'Sleeping', 'Blocked', 和 'Waiting'几个状态, 'Ready' 表示线程正在等待CPU分配允许运行的时间。 3.线程运行次序并不是按照我们创建他们时的顺序来运行的,CPU处理线程的顺序是不确定的,如果需要确定,那么必须手工介入,使用setPriority()方法设置优先级。 4.我们无从知道一个线程什么时候运行,两个或多个线程在访问同一个资源时,需要synchronized 5. 每个线程会注册自己,实际某处存在着对它的引用,因此,垃圾回收机制对它就“束手无策”了。 6. Daemon线程区别一般线程之处是:主程序一旦结束,Daemon线程就会结束。 7. 一个对象中的所有synchronized方法都共享一把锁,这把锁能够防止多个方法对通用内存同时进行的写操作。synchronized static方法可在一个类范围内被相互间锁定起来。 8. 对于访问某个关键共享资源的所有方法,都必须把它们设为synchronized,否则就不能正常工作。 9. 假设已知一个方法不会造成冲突,最明智的方法是不要使用synchronized,能提高些性能。 10. 如果一个\"同步"方法修改了一个变量,而我们的方法要用到这个变量(可能是只读),最好将自己的这个方法也设为 synchronized。 11. synchronized不能继承, 父类的方法是synchronized,那么其子类重载方法中就不会继承“同步”。 12. 线程堵塞Blocked有几个原因造成: (1)线程在等候一些IO操作 (2)线程试图调用另外一个对象的“同步”方法,但那个对象处于锁定状态,暂时无法使用。 13.原子型操作(atomic), 对原始型变量(primitive)的操作是原子型的atomic. 意味着这些操作是线程安全的, 但是大部分情况下,我们并不能正确使用,来看看 i = i + 1 , i是int型,属于原始型变量: (1)从主内存中读取i值到本地内存. (2)将值从本地内存装载到线程工作拷贝中. (3)装载变量1. (4)将i 加 1. (5)将结果给变量i. (6)将i保存到线程本地工作拷贝中. (7)写回主内存. 注意原子型操作只限于第1步到第2步的读取以及第6到第7步的写, i的值还是可能被同时执行i=i+1的多线程中断打扰(在第4步)。 double 和long 变量是非原子型的(non-atomic)。数组是object 非原子型。 14. 由于13条的原因,我们解决办法是:
class xxx extends Thread{ //i会被经常修改 private int i; public synchronized int read(){ return i;} public synchronized void update(){ i = i + 1;} .......... } 15. Volatile变量, volatile变量表示保证它必须是与主内存保持一致,它实际是"变量的同步", 也就是说对于volatile变量的操作是原子型的,如用在long 或 double变量前。 16. 使用yield()会自动放弃CPU,有时比sleep更能提升性能。 17. sleep()和wait()的区别是:wait()方法被调用时会解除锁定,但是我们能使用它的地方只是在一个同步的方法或代码块内。 18. 通过制造缩小同步范围,尽可能的实现代码块同步,wait(毫秒数)可在指定的毫秒数可退出wait;对于wait()需要被notisfy()或notifyAll()踢醒。 19. 构造两个线程之间实时通信的方法分几步: (1). 创建一个PipedWriter和一个PipedReader和它们之间的管道; PipedReader in = new PipedReader(new PipedWriter()) (2). 在需要发送信息的线程开始之前,将外部的PipedWriter导向给其内部的Writer实例out (3). 在需要接受信息的线程开始之前,将外部的PipedReader导向给其内部的Reader实例in (4). 这样放入out的所有东西度可从in中提取出来。 20. synchronized带来的问题除性能有所下降外,最大的缺点是会带来死锁DeadLock,只有通过谨慎设计来防止死锁,其他毫无办法,这也是线程难以驯服的一个原因。不要再使用stop() suspend() resume()和destory()方法 21. 在大量线程被堵塞时,最高优先级的线程先运行。但是不表示低级别线程不会运行,运行概率小而已。 22. 线程组的主要优点是:使用单个命令可完成对整个线程组的操作。很少需要用到线程组。 23. 从以下几个方面提升多线程的性能: 检查所有可能Block的地方,尽可能的多的使用sleep或yield()以及wait(); 尽可能延长sleep(毫秒数)的时间; 运行的线程不用超过100个,不能太多; 不同平台linux或windows以及不同JVM运行性能差别很大。
|
shiweifu 发表于 22:24:04
|
attachEvent() / addEventListener() 对象添加触发事件(转)
有时候当某一对象的某一事件被触发时,它所要执行的程序可能是一大串,有可能是要呼叫某一函数,也有可能同时又要呼叫另一函数。
document.getElementById("btn").onclick = method1; document.getElementById("btn").onclick = method2; document.getElementById("btn").onclick = method3; 如果这样写,那么将会只有medhot3被执行
在IE中使用addachEvent ,
var btn1Obj = document.getElementById("btn1"); //object.attachEvent(event,function); btn1Obj.attachEvent("onclick",method1); btn1Obj.attachEvent("onclick",method2); btn1Obj.attachEvent("onclick",method3); 执行顺序为method3->method2->method1
Mozilla系列中需要使用 addEventListener
var btn1Obj = document.getElementById("btn1"); //element.addEventListener(type,listener,useCapture); btn1Obj.addEventListener("click",method1,false); btn1Obj.addEventListener("click",method2,false); btn1Obj.addEventListener("click",method3,false); 执行顺序为method1->method2->method3
看看gmail的代码
var Ka=navigator.userAgent.toLowerCase(); var rt=Ka.indexOf("opera")!=-1; var r=Ka.indexOf("msie")!=-1&&(document.all&&!rt);
function Zl(a,b,c){if(r){a.attachEvent("on"+b,c)}else{a.addEventListener(b,c,false)}}
javascript事件列表解说
javascript事件列表解说 | 事件 | 浏览器支持 | 解说 | 一般事件 | onclick | IE3、N2 | 鼠标点击时触发此事件 | ondblclick | IE4、N4 | 鼠标双击时触发此事件 | onmousedown | IE4、N4 | 按下鼠标时触发此事件 | onmouseup | IE4、N4 | 鼠标按下后松开鼠标时触发此事件 | onmouseover | IE3、N2 | 当鼠标移动到某对象范围的上方时触发此事件 | onmousemove | IE4、N4 | 鼠标移动时触发此事件 | onmouseout | IE4、N3 | 当鼠标离开某对象范围时触发此事件 | onkeypress | IE4、N4 | 当键盘上的某个键被按下并且释放时触发此事件. | onkeydown | IE4、N4 | 当键盘上某个按键被按下时触发此事件 | onkeyup | IE4、N4 | 当键盘上某个按键被按放开时触发此事件 | 页面相关事件 | onabort | IE4、N3 | 图片在下载时被用户中断 | onbeforeunload | IE4、N | 当前页面的内容将要被改变时触发此事件 | onerror | IE4、N3 | 出现错误时触发此事件 | onload | IE3、N2 | 页面内容完成时触发此事件 | onmove | IE、N4 | 浏览器的窗口被移动时触发此事件 | onresize | IE4、N4 | 当浏览器的窗口大小被改变时触发此事件 | onscroll | IE4、N | 浏览器的滚动条位置发生变化时触发此事件 | onstop | IE5、N | 浏览器的停止按钮被按下时触发此事件或者正在下载的文件被中断 | onunload | IE3、N2 | 当前页面将被改变时触发此事件 | 表单相关事件 | onblur | IE3、N2 | 当前元素失去焦点时触发此事件 | onchange | IE3、N2 | 当前元素失去焦点并且元素的内容发生改变而触发此事件 | onfocus | IE3 、N2 | 当某个元素获得焦点时触发此事件 | onreset | IE4 、N3 | 当表单中RESET的属性被激发时触发此事件 | onsubmit | IE3 、N2 | 一个表单被递交时触发此事件 | 滚动字幕事件 | onbounce | IE4、N | 在Marquee内的内容移动至Marquee显示范围之外时触发此事件 | onfinish | IE4、N | 当Marquee元素完成需要显示的内容后触发此事件 | onstart | IE4、 N | 当Marquee元素开始显示内容时触发此事件 | 编辑事件 | onbeforecopy | IE5、N | 当页面当前的被选择内容将要复制到浏览者系统的剪贴板前触发此事件 | onbeforecut | IE5、 N | 当页面中的一部分或者全部的内容将被移离当前页面[剪贴]并移动到浏览者的系统剪贴板时触发此事件 | onbeforeeditfocus | IE5、N | 当前元素将要进入编辑状态 | onbeforepaste | IE5、 N | 内容将要从浏览者的系统剪贴板传送[粘贴]到页面中时触发此事件 | onbeforeupdate | IE5、 N | 当浏览者粘贴系统剪贴板中的内容时通知目标对象 | oncontextmenu | IE5、N | 当浏览者按下鼠标右键出现菜单时或者通过键盘的按键触发页面菜单时触发的事件 | oncopy | IE5、N | 当页面当前的被选择内容被复制后触发此事件 | oncut | IE5、N | 当页面当前的被选择内容被剪切时触发此事件 | ondrag | IE5、N | 当某个对象被拖动时触发此事件 [活动事件] | ondragdrop | IE、N4 | 一个外部对象被鼠标拖进当前窗口或者帧 | ondragend | IE5、N | 当鼠标拖动结束时触发此事件,即鼠标的按钮被释放了 | ondragenter | IE5、N | 当对象被鼠标拖动的对象进入其容器范围内时触发此事件 | ondragleave | IE5、N | 当对象被鼠标拖动的对象离开其容器范围内时触发此事件 | ondragover | IE5、N | 当某被拖动的对象在另一对象容器范围内拖动时触发此事件 | ondragstart | IE4、N | 当某对象将被拖动时触发此事件 | ondrop | IE5、N | 在一个拖动过程中,释放鼠标键时触发此事件 | onlosecapture | IE5、N | 当元素失去鼠标移动所形成的选择焦点时触发此事件 | onpaste | IE5、N | 当内容被粘贴时触发此事件 | onselect | IE4、N | 当文本内容被选择时的事件 | onselectstart | IE4、N | 当文本内容选择将开始发生时触发的事件 | 数据绑定 | onafterupdate | IE4、N | 当数据完成由数据源到对象的传送时触发此事件 | oncellchange | IE5、N | 当数据来源发生变化时 | ondataavailable | IE4、N | 当数据接收完成时触发事件 | ondatasetchanged | IE4、N | 数据在数据源发生变化时触发的事件 | ondatasetcomplete | IE4、N | 当来子数据源的全部有效数据读取完毕时触发此事件 | onerrorupdate | IE4、N | 当使用onBeforeUpdate事件触发取消了数据传送时,代替onAfterUpdate事件 | onrowenter | IE5、N | 当前数据源的数据发生变化并且有新的有效数据时触发的事件 | onrowexit | IE5、N | 当前数据源的数据将要发生变化时触发的事件 | onrowsdelete | IE5、N | 当前数据记录将被删除时触发此事件 | onrowsinserted | IE5、N | 当前数据源将要插入新数据记录时触发此事件 | 外部事件 | onafterprint | IE5、N | 当文档被打印后触发此事件 | onbeforeprint | IE5、N | 当文档即将打印时触发此事件 | onfilterchange | IE4、N | 当某个对象的滤镜效果发生变化时触发的事件 | onhelp | IE4、N | 当浏览者按下F1或者浏览器的帮助选择时触发此事件 | onpropertychange | IE5、N | 当对象的属性之一发生变化时触发此事件 | onreadystatechange | IE4、N | 当对象的初始化属性值发生变化时触发此事件 |
JavaScript 事件串联执行多个处理过程的方法
JavaScript 事件串联执行多个处理过程的方法
2006-01-04
@ 15:46:42 · 作者 andot · 归类于 JavaScript
以前写 JavaScript 程序时,事件都是采用
object
.
event
=
handler
;
的方式初始化。这种方式对于 Internet Explorer、Mozilla/Firefox 和 Opera 来说很通用。但是有一个问题就是,这种方式只能一个事件对应一个事件处理过程。如果希望一个事件可以依次执行多个处理过程就不好用了。
但是 Internet Explorer 从 5.0 开始提供了一个 attachEvent 方法,使用这个方法,就可以给一个事件指派多个处理过程了。attachEvent 对于目前的 Opera 也适用。但是问题是 Mozilla/Firefox 并不支持这个方法。但是它支持另一个 addEventListener 方法,这个方法跟 attachEvent 差不多,也是用来给一个事件指派多个处理过程的。但是它们指派的事件有些区别,在 attachEvent 方法中,事件是以 “on” 开头的,而在 addEventListener 中,事件没有开头的 “on”,另外 addEventListener 还有第三个参数,一般这个参数指定为 false 就可以了。
因此要想在你的程序中给一个事件指派多个处理过程的话,只要首先判断一下浏览器,然后根据不同的浏览器,选择使用 attachEvent 还是 addEventListener 就可以了。实例如下:
if
(
document
.
all
)
{
window
.
attachEvent
(
'
onload
'
,
handler1
)
;
window
.
attachEvent
(
'
onload
'
,
handler2
)
;
}
else
{
window
.
addEventListener
(
'
load
'
,
handler1
,
false
)
;
window
.
addEventListener
(
'
load
'
,
handler2
,
false
)
;
}
注意:attachEvent 所指派的多个过程的执行顺序是随机的,所以这几个过程之间不要有顺序依赖。另外 attachEvent 和 addEventListener 不仅仅适用于 window 对象,其他的一些对象也支持该方法。
在Java 2的Collections框架中,主要包括两个接口及其扩展和实现类:Collection接口和Map接口。两者的区别在于前者存储一组对象,后者则存储一些关键字/值对。
public interface java.util.Map {
//Altering Methods public Object put(Object key, Object value); public Object remove(Object key); public void putAll(java.util.Map); public void clear();
//Querying Methods public Object get(Object key); public int size(); public boolean isEmpty(); public boolean containsKey(Object); public boolean containsValue(Object); public boolean equals(Object);
//Viewing Methods public java.util.Set keySet(); //Gets keys public java.util.Collection values(); //Gets values public java.util.Set entrySet(); //Gets mappings
public static interface java.util.Map.Entry { //a map-entry (single key/value pair) public Object getKey(); //returns current entry key public Object getValue(); //returns current entry value public Object setValue(Object value); public boolean equals(Object); public int hashCode(); } } Map接口提供了方便易用的方法,通过这些方法可以查询、查看、修改当前Map的内容。注意对于Map接口的keySet()方法返回一个Set,Set是Collection接口的一个扩展,包含不重复的一组对象。因为Map中的key是不可重复的,所以得到所有key的keySet()方法返回一个Set对象。Map接口本身还包含了一个Map.Entry接口,一个Map.Entry就是Map中的一个关键字/值对。Map接口中的entrySet()方法就返回了一个集合对象,其中每一个元素都实现了Map.Entry接口。Map接口的get(Object key),put(Object key,Object value),和remove(Object key)方法都有同一个问题。他们的返回类型都是Object,当返回null时,可以猜测为调用那个方法前那个key不存在。但是只有在null不允许作为Map的值时可以这样猜测。所有Map接口的通用实现都允许null作为key或者value,这就说当返回一个null值,就可以意味着很多事情。只是因为通用实现允许null值,你不能下那个映射有null值的结论。如果你确知没有null值,那返回null值就意味着调用那个方法前,映射里并没有那个键。否则,你必须调用containsKey(Object key)来看看那个Key是否存在。
Hashtable
java.util.Hashtable实现了Map接口,在Hashtable中使用key对象的hashCode()作为对应的对象的相对存储地址,以便实现根据关键字快速查找对象的功能。所以只有一个实现了hashCode()和equals()方法的对象才可作为Hashtable的key。null值不能作为关键字或值。 public class java.util.Hashtable extends Dictionary implements Cloneable, Map, Serializable {
//Hashtable constructors //construct a default Hashtable with default capacity and load of 0.75 public Hashtable(); //construct a Hashtable with passed capacity and default load of 0.75 public Hashtable (int initialCapacity); //construct Hashtable with passed capacity and load public Hashtable(int initialCapacity, float load); //construct Hashtable with passed mapping public Hashtable(Map); //Hashtable specific methods //checks if Object is in Hashtable public boolean contains(Object); //returns Enumeration of elements in Hashtable public Enumeration elements(); //returns Enumeration of keys in hashtable public Enumeration keys(); //creates shallow copy of Hashtable(structure copied, but not key/values) public Object clone(); //prints out key/value pairs of Hashtable elements public String toString(); //reorganizes all elements in Hashtable, and increases Hashtable capacity protected void rehash(); //get Value from passed in key public Object get(Object); //insert key/value pair public Object put(Object key, Object value);
} Hashtable是Java 2集合框架推出之前的一个老的工具类,在新的Java 2集合框架下,已经被HashMap取代。Hashtable和HashMap的区别主要是前者是同步的,后者是快速失败机制保证不会出现多线程并发错误(Fast-Fail)。在初始化一个Hashtable时,可以指定两个参数:初始容量、负荷,这两个参数强烈的影响着Hashtable的性能。容量是指对象的个数,负荷是指散列表中的实际存储的对象个数和容量的比率。如果初始容量太小,那么Hashtable需要不断的扩容并rehash(),而这是很耗时的;如果初始容量太大,又会造成空间的浪费。负荷则相反,负荷太小会造成空间浪费,负荷太大又会耗时(因为这会造成较多的关键字的散列码重复,Hashtable使用一个链接表来存储这些重复散列码的对象)。容量的缺省值是11,负荷的缺省值是0.75,一般情况下你都可以使用缺省值来生成一个Hashtable。另外,在Hashtable中的大部分的方法都是同步的。
HashMap
HashMap基本实现了Map接口的全部方法。方法的签名大家看上面的Map接口。这儿主要说说几个Map接口中的方法。 按照集合框架的实现,哈希表是单链表作为元素的数组,有着同样索引值的两个或更多入口被一起链结到单链表中。哈希表声明如下: private Entry[] table; 组件类型Entry是Map.Entry接口的实现,Map.Entry声明于Map接口内。下边是Map.Entry接口的简化实现: private static class Entry implements Map.Entry{ int hashCode; Object key; Object value; Entry next;
Entry(int hashCode,Object key,Object value,Entry next){ This.hashCode=hashCode; This.key=key; This.value=value; This.next=next; } public Object getKey(){ return key; } public Object getValue(){ return value; } public Object setValue(Object value){ Object oldValue=this.value; This.value=value; Return oldValue; } }
SortedMap是Map接口的子接口,SortedMap的标准实现是TreeMap,实现了一个排序的Map。这儿不再做说明。本站翻译的《Java 规则》(Java Rules)中对Java2的集合框架有深入的研究。本文限于篇幅,只及皮毛。有兴趣的朋友,请参考Java2源码中的集合实现代码和API doc。
随着网络的发展,网速和机器速度的提高,越来越多的网站用到了丰富客户端技术。而现在Ajax则是最为流行的一种方式。JavaScript是一种解释型语言,所以能无法达到和C/Java之类的水平,限制了它能在客户端所做的事情,为了能改进他的性能,我想基于我以前给JavaScript做过的很多测试来谈谈自己的经验,希望能帮助大家改进自己的JavaScript脚本性能。
语言层次方面
循环
循环是很常用的一个控制结构,大部分东西要依靠它来完成,在JavaScript中,我们可以使用for(;;),while(),for(in)三种循环,事实上,这三种循环中for(in)的效率极差,因为他需要查询散列键,只要可以就应该尽量少用。for(;;)和while循环的性能应该说基本(平时使用时)等价。
而事实上,如何使用这两个循环,则有很大讲究。最后得出的结论是:
局部变量和全局变量
局部变量的速度要比全局变量的访问速度更快,因为全局变量其实是全局对象的成员,而局部变量是放在函数的栈当中的。
不使用Eval
使用eval相当于在运行时再次调用解释引擎对内容进行运行,需要消耗大量时间。这时候使用JavaScript所支持的闭包可以实现函数模版(关于闭包的内容请参考函数式编程的有关内容)
减少对象查找
因为JavaScript的解释性,所以a.b.c.d.e,需要进行至少4次查询操作,先检查a再检查a中的b,再检查b中的c,如此往下。所以如果这样的表达式重复出现,只要可能,应该尽量少出现这样的表达式,可以利用局部变量,把它放入一个临时的地方进行查询。
这一点可以和循环结合起来,因为我们常常要根据字符串、数组的长度进行循环,而通常这个长度是不变的,比如每次查询a.length,就要额外进行一个操作,而预先把var len=a.length,则就少了一次查询。
字符串连接
如果是追加字符串,最好使用s+=anotherStr操作,而不是要使用s=s+anotherStr。
如果要连接多个字符串,应该少使用+=,如
s+=a; s+=b; s+=c;
应该写成
s+=a + b + c;
而如果是收集字符串,比如多次对同一个字符串进行+=操作的话,最好使用一个缓存。怎么用呢?使用JavaScript数组来收集,最后使用join方法连接起来,如下
var buf = new Array(); for(var i = 0; i < 100; i++){ buf.push(i.toString()); } var all = buf.join("");
类型转换
类型转换是大家常犯的错误,因为JavaScript是动态类型语言,你不能指定变量的类型。
1. 把数字转换成字符串,应用"" + 1,虽然看起来比较丑一点,但事实上这个效率是最高的,性能上来说:
("" +) > String() > .toString() > new String()
这条其实和下面的“直接量”有点类似,尽量使用编译时就能使用的内部操作要比运行时使用的用户操作要快。
String()属于内部函数,所以速度很快,而.toString()要查询原型中的函数,所以速度逊色一些,new String()用于返回一个精确的副本。
2. 浮点数转换成整型,这个更容易出错,很多人喜欢使用parseInt(),其实parseInt()是用于将字符串转换成数字,而不是浮点数和整型之间的转换,我们应该使用Math.floor()或者Math.round()。
另外,和第二节的对象查找中的问题不一样,Math是内部对象,所以Math.floor()其实并没有多少查询方法和调用的时间,速度是最快的。
3. 对于自定义的对象,如果定义了toString()方法来进行类型转换的话,推荐显式调用toString(),因为内部的操作在尝试所有可能性之后,会尝试对象的toString()方法尝试能否转化为String,所以直接调用这个方法效率会更高
使用直接量
其实这个影响倒比较小,可以忽略。什么叫使用直接量,比如,JavaScript支持使用[param,param,param,...]来直接表达一个数组,以往我们都使用new Array(param,param,...),使用前者是引擎直接解释的,后者要调用一个Array内部构造器,所以要略微快一点点。
同样,var foo = {}的方式也比var foo = new Object();快,var reg = /../;要比var reg=new RegExp()快。
字符串遍历操作
对字符串进行循环操作,譬如替换、查找,应使用正则表达式,因为本身JavaScript的循环速度就比较慢,而正则表达式的操作是用C写成的语言的API,性能很好。
高级对象
自定义高级对象和Date、RegExp对象在构造时都会消耗大量时间。如果可以复用,应采用缓存的方式。
DOM相关
插入HTML
很多人喜欢在JavaScript中使用document.write来给页面生成内容。事实上这样的效率较低,如果需要直接插入HTML,可以找一个容器元素,比如指定一个div或者span,并设置他们的innerHTML来将自己的HTML代码插入到页面中。
对象查询
使用[“”]查询要比.items()更快,这和前面的减少对象查找的思路是一样的,调用.items()增加了一次查询和函数的调用。
创建DOM节点
通常我们可能会使用字符串直接写HTML来创建节点,其实这样做
-
无法保证代码的有效性
-
字符串操作效率低
所以应该是用document.createElement()方法,而如果文档中存在现成的样板节点,应该是用cloneNode()方法,因为使用createElement()方法之后,你需要设置多次元素的属性,使用cloneNode()则可以减少属性的设置次数——同样如果需要创建很多元素,应该先准备一个样板节点。
定时器
如果针对的是不断运行的代码,不应该使用setTimeout,而应该是用setInterval。setTimeout每次要重新设置一个定时器。
其他
脚本引擎
据我测试Microsoft的JScript的效率较Mozilla的Spidermonkey要差很多,无论是执行速度还是内存管理上,因为JScript现在基本也不更新了。但SpiderMonkey不能使用ActiveXObject
文件优化
文件优化也是一个很有效的手段,删除所有的空格和注释,把代码放入一行内,可以加快下载的速度,注意,是下载的速度而不是解析的速度,如果是本地,注释和空格并不会影响解释和执行速度。
总结
本文总结了我在JavaScript编程中所找到的提高JavaScript运行性能的一些方法,其实这些经验都基于几条原则:
-
直接拿手头现成的东西比较快,如局部变量比全局变量快,直接量比运行时构造对象快等等。
-
尽可能少地减少执行次数,比如先缓存需要多次查询的。
-
尽可能使用语言内置的功能,比如串链接。
-
尽可能使用系统提供的API,因为这些API是编译好的二进制代码,执行效率很高
同时,一些基本的算法上的优化,同样可以用在JavaScript中,比如运算结构的调整,这里就不再赘述了。但是由于JavaScript是解释型的,一般不会在运行时对字节码进行优化,所以这些优化仍然是很重要的。
当然,其实这里的一些技巧同样使用在其他的一些解释型语言中,大家也可以进行参考。
escape() & encodeURI() & encodeURIComponent()
escape() & encodeURI() & encodeURIComponent()这三个函数都可以用来对URI进行encode或过滤特殊字符(#/$&+=?/等)。我的经验是最好用encodeURIComponent()(需要IE 5.5以上,FireFox当然没问题),因为对UTF-8支持比较好,不会遇到中文乱码问题,否则还需要进行编码转换,很麻烦的。Terac Rome就是用的encodeURIComponent(),del.icio.us和365key用的是escape()。当然,有人这三个函数都不用,自己写过滤函数,我觉得那纯粹是浪费精力。
下面是MSDN上对这三个函数的解释:
escape(charString)
The escape method returns a string value (in Unicode format) that contains the contents of charstring. All spaces, punctuation, accented characters, and any other non-ASCII characters are replaced with %xx encoding, where xx is equivalent to the hexadecimal number representing the character. For example, a space is returned as "%20."
Characters with a value greater than 255 are stored using the %uxxxx format.
Note The escape method should not be used to encode Uniform Resource Identifiers (URI). Use encodeURI and encodeURIComponent methods instead.
http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthescape.asp
encodeURI(URIString)
The encodeURI method returns an encoded URI. If you pass the result to decodeURI, the original string is returned. The encodeURI method does not encode the following characters: ":", "/", ";", and "?". Use encodeURIComponent to encode these characters.
http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthfencodeuri.asp
encodeURIComponent(encodedURIString)
The encodeURIComponent method returns an encoded URI. If you pass the result to decodeURIComponent, the original string is returned. Because the encodeURIComponent method encodes all characters, be careful if the string represents a path such as /folder1/folder2/default.html. The slash characters will be encoded and will not be valid if sent as a request to a web server. Use the encodeURI method if the string contains more than a single URI component.
http://msdn.microsoft.com/library/en-us/script56/html/js56jsmthencodeuricomponent.asp
encodeURIComponent 方法返回一个已编码的 URI。如果将编码结果传递给 decodeURIComponent,则将返回初始的字符串。因为 encodeURIComponent 方法将对所有字符编码,请注意,如果该字符串代表一个路径,例如 /kyle/blogpost.asp,则其中的斜杠也将被编码,这样,当该字符串作为请求发送到 Web 服务器时它将是无效的。如果字符串中包含多个 URI 组件,请使用 encodeURI 方法进行编码。 【例子】encodeURIComponent('/?&神泥') 返回 %2F%3F%26%E7%A5%9E%E6%B3%A5
encodeURI 方法返回一个已编码的 URI。如果将编码结果传递给 decodeURI,则将返回初始的字符串。encodeURI 不对下列字符进行编码:“:”、“/”、“;”和“?”。请使用 encodeURIComponent 对这些字符进行编码。 【例子】encodeURI('/?&神泥') 返回 /?&%E7%A5%9E%E6%B3%A5
|