2006年07月06日

        思考1 : 大局上面仍然有另一个大局
  思考2 : 公平永远有不同角度的公平
  思考3 : 这个游戏只有站在切换器旁边的人可以决定结果

  有一群小朋友在外面玩 而那个地方有两条铁轨,
  一条还在使用, 一条已经停用
  只有一个小朋友选择在停用的铁轨上玩
  其它的小朋友全都在仍在使用的铁轨上玩

  很不巧,火车来了(而且理所当然的往上面有很多小孩的,仍在使用的铁轨上行驶)

  而你正站在铁轨的切换器旁,因此你能让火车转往停用的铁轨
  这样的话你就可以救了大多数的小朋友; 但是那名在停用铁轨上的小朋友将被牺牲
  你会怎么办?

  据说大多数人会选择救多一些的人,换句话说,牺牲那名在停用铁轨上玩的小孩…

  但是这又引出另一个问题
  那一名选择停用铁轨的小孩显然是做出正确决定
  脱离了他的朋友而选择了安全的地方
  而他的朋友们则是无知或任性的选择在不该玩耍的地方玩
  为什么做出正确抉择的人要为了大多数人的无知而牺牲呢?

  [响应与挑战]

  这篇文章蛮发人深省的,看完了感触很深!
  我们常被教育要顾全大局,但公平吗?

  似乎当大家都做的理所当然的时候,
  我们就必须随波逐流,否则就会被放逐而不容于世,
  如渔父中那位老翁劝屈原所说的:
  世人皆浊,何不淈其泥而扬其波?
  众人皆醉,何不餔其糟而歠其醨?
  何故深思高举,自令放为?

  当一个人太坚持自己是"对"的,
  最后的下场可能就是被牺牲的可怜鬼!
  又有谁会为他掬一把同情之泪? 只会嘲笑他的愚蠢!

  我们已经进了社会,学习的就是圆滑的做人,
  当你是主管, 就像文章中那位切换轨道的人,
  内心的正义与现实冲突时, 你会如何抉择呢?

  不过- 换另一个角度,如不选择切换轨道
  因为,那群小朋友一定知道那是活的轨道
  所以,当他们听到火车的声音时,就会知道要跑!

  但若,将轨道切换后那个乖小孩必定惨死,
  因为,他从来没想过火车还会开到废轨道上
  所以,你认为呢?

  或许这样的想法与理念,
  到了人老时才会发现
  自己成为一个不断被牺牲的可怜鬼,
  但这个社会又为什么要把是与非颠倒来又颠倒去呢?

  另外,再想想,一条铁道会被停止使用,自由它的道理。
  是否代表着这个铁道本身有问题,未经验证就使用它会遇到潜藏的危机呢?

  如果切过去之后,被牺牲的就不只是一个或一群小孩了,
  而是整车的乘客呢?

2006年05月07日

在动物界,澳洲袋鼠彼此争斗时就有个不成文的规则,即身材高大的袋鼠只能用上肢,而体形较小的才可以手脚并用。

中央电视台只有<天气预报>是真实的

在你遇见他之前,你没有想过会曾经和他在一起,在你失去他以后,你也不会想到还会有谁会再来到你的生命中。就像我们上了大学会怀念高中的生活,而大学毕业却更怀念大学的美好。人的生活就是从一个驿站到下一个驿站,从一处风景走向下一处风景,虽然我们希望能有个人与我们共同走下去,可失去了他,你也许才会得到新的开始。

一个男人的二十几岁是一生中最暗淡的时光,要什么没什么;所以一定要珍惜在这段时间内深爱你的女人,因为她是在用她一生中最美丽的岁月去陪伴你度过这段最暗淡的时光。

补充三,非处,你会说。“你都不是处男,凭什么要求别人是处女”。
那么男人同样可以问一个问题“你自己都没有房子,凭什么要求你的老公要有房子呢”
你可能会说,因为我是女人,你是男人,所以你要有房子
那男人也可以说,因为我是男人,你是女人,所以你必须是处女

第一,女孩如果把第一次给了一个男孩,有两种情况,第一种,她很爱这个男孩。我相信大家都听过,忘掉一个人也许要一辈子这句话。何况是这个把自己从女孩变成女人的男人。我相信一个女孩爱一个男孩爱到可以把自己最宝贵的贞操都交给他的程度,她一辈子也忘不了这个男孩。所以,我不会娶这个女孩,因为我还没有伟大到那种可以容忍自己的老婆一辈子记着别的男人。第二种,这个女孩不爱这个男孩。连自己最宝贵的东西,都随便给了一个自己不爱的人的女孩。你会要吗?

2006年04月16日

Commons是Apache开放源代码组织中的一个Java子项目,该项目主要涉及一些开发中常用的模块,例如文件上传、命令行处理、数据库连接池、XML配置文件处理等。这些项目集合了来自世界各地软件工程师的心血,其性能、稳定性等方面都经受得住实际应用的考验。有效地利用这些项目将会给开发带来显而易见的效果。Fileupload就是其中用来处理HTTP文件上传的子项目。本文主要介绍如何使用Fileupload来处理浏览器提交到服务器的文件信息。

  为了让首次接触Fileupload的开发人员能够更直观的理解该项目,我们将实现一个简单的文件上传功能,并一步步介绍开发步骤,以及详细的代码。

  环境准备

  1. 下载并安装Tomcat(已经有很多关于Tomcat安装以及使用的文章,在这里不再介绍);

  2. 下载File upload的jar包commons-fileupload-1.0-beta-1.jar,并将该文件拷贝到{$TOMCAT}/common/lib目录下(其中{$TOMCAT}为Tomcat的安装目录);

  3. 由于Fileupload子项目同时要用到另外一个项目commons-Beanutils,所以必须下载Beanutils,并将解压后的文件commons-beanutils.jar拷贝到{$TOMCAT}/common/lib目录下。

  开发文件上传页面

  文件上传的界面如图1所示。为了增加效率我们设计了三个文件域,同时上传三个文件。

  图1 文件上传界面

  页面的HTML代码如下:


<html>
<head>
<title>文件上传演示</title>
</head>
<body bgcolor=“#FFFFFF”text=“#000000” leftmargin=“0”topmargin=“40”marginwidth=“0” marginheight=“0”>
<center>
<h1>文件上传演示</h1>
<form name=“uploadform”method=“POST” action=“save.jsp”ENCTYPE=“multipart/form-data”>
 <table border=“1”width=“450”cellpadding=“4” cellspacing=“2”bordercolor=“#9BD7FF”>
 <tr><td width=“100%”colspan=“2”>
 文件1:<input name=“file1”size=“40”type=“file”>
 </td></tr>
 <tr><td width=“100%”colspan=“2”>
 文件2:<input name=“file2”size=“40”type=“file”>
 </td></tr>
 <tr><td width=“100%”colspan=“2”>
 文件3:<input name=“file3”size=“40”type=“file”>
 </td></tr>
 </table>
 <br/><br/>
 <table>
 <tr><td align=“center”><input name=“upload” type=“submit”value=“开始上传”/></td></tr>
 </table>
</form>
</center>
</body>
</html>

  代码中要特别注意的是黑体处。必须保证表单的ENCTYPE属性值为multipart/form-data,这样浏览器才能正确执行上传文件的操作。

  处理上传文件信息

  由于本文主要是讲述如何使用Commons-fileupload,所以为了便于修改、调试,上传文件的保存使用一个JSP文件来进行处理。我们将浏览器上传来的所有文件保存在一个指定目录下并在页面上显示所有上传文件的详细信息。保存页面处理结果见图2所示。

  图2 保存页面

  下面来看看save.jsp的代码:


<%
/**
 * 演示文件上传的处理
 * @author <a href=“mailto:winter.lau@163.com”>Winter Lau</a>
 * @version $Id: save.jsp,v 1.00 2003/03/01 10:10:15
 */
%>
<%@ page language=“java”contentType=“text/html;charset=GBK”%>
<%@ page import=“java.util.*”%>
<%@ page import=“org.apache.commons.fileupload.*”%>
<html>
<head>
<title>保存上传文件</title>
</head>
<%
 String msg = “”;
 FileUpload fu = new FileUpload();
 // 设置允许用户上传文件大小,单位:字节
 fu.setSizeMax(10000000);
 // maximum size that will be stored in memory?
 // 设置最多只允许在内存中存储的数据,单位:字节
 fu.setSizeThreshold(4096);
 // 设置一旦文件大小超过getSizeThreshold()的值时数据存放在硬盘的目录
 fu.setRepositoryPath(“C:\\TEMP”);
 //开始读取上传信息
 List fileItems = fu.parseRequest(request);
%>
<body bgcolor=“#FFFFFF”text=“#000000” leftmargin=“0”topmargin=“40”marginwidth=“0” marginheight=“0”>
<font size=“6”color=“blue”>文件列表:</font>
<center>
<table cellpadding=0 cellspacing=1 border=1 width=“100%”>
<tr>
<td bgcolor=“#008080”>文件名</td>
<td bgcolor=“#008080”>大小</td>
</tr>
<%
 // 依次处理每个上传的文件
 Iterator iter = fileItems.iterator();
 while (iter.hasNext()) {
  FileItem item = (FileItem) iter.next();
  //忽略其他不是文件域的所有表单信息
  if (!item.isFormField()) {
   String name = item.getName();
   long size = item.getSize();
   if((name==null||name.equals(“”)) && size==0)
   continue;
%>
<tr>
<td><%=item.getName()%></td>
<td><%=item.getSize()%></td>
</tr>
<%
   //保存上传的文件到指定的目录
   name = name.replace(‘:’,‘_’);
   name = name.replace(‘\\’,‘_’);
   item.write(“F:\\”+ name);
  }
 }
%>
</table>

<br/><br/>
<a href=“upload.html”>返回上传页面</a>
</center>
</body>
</html>

  在这个文件中需要注意的是FileUpload对象的一些参数值的意义,如下面代码所示的三个参数sizeMax、sizeThreshold、repositoryPath:


FileUpload fu = new FileUpload();
// 设置允许用户上传文件大小,单位:字节
fu.setSizeMax(10000000);
// maximum size that will be stored in memory?
// 设置最多只允许在内存中存储的数据,单位:字节
fu.setSizeThreshold(4096);
// 设置一旦文件大小超过getSizeThreshold()的值时数据存放在硬盘的目录
fu.setRepositoryPath(“C:\\TEMP”);

  这3个参数的意义分别为:

  SizeMax 用来设置上传文件大小的最大值,一旦用户上传的文件大小超过该值时将会抛出一个FileUploadException异常,提示文件太大;

  SizeThreshold 设置内存中缓冲区的大小,一旦文件的大小超过该值的时候,程序会自动将其它数据存放在repositoryPath指定的目录下作为缓冲。合理设置该参数的值可以保证服务器稳定高效的运行;

  RepositoryPath 指定缓冲区目录。

  使用注意事项

  从实际应用的结果来看该模块能够稳定高效的工作。其中参数SizeThreshold的值至关重要,设置太大会占用过多的内存,设置太小会频繁使用硬盘作为缓冲以致牺牲性能。因此,设置该值时要根据用户上传文件大小分布情况来设定。例如大部分文件大小集中在100KB左右,则可以使用100KB作为该参数的值,当然了再大就不合适了。使用commons-fileupload来处理HTTP文件上传的功能模块很小,但是值得研究的东西很多。

2006年03月07日

ISO8859_1:西欧语系的字符集

new String(request.getparameter(name).getBytes("ISO8859_1"),"GBK");

2006年01月18日
Input和Output
stream代表的是任何有能力产出数据的数据源,或是任何有能力接收数据的接收源。在Java的IO中,所有的stream(包括Input和Out stream)都包括两种类型:

1 以字节为导向的stream

以字节为导向的stream,表示以字节为单位从stream中读取或往stream中写入信息。以字节为导向的stream包括下面几种类型:

input stream:

1) ByteArrayInputStream:把内存中的一个缓冲区作为InputStream使用

2) StringBufferInputStream:把一个String对象作为InputStream

3) FileInputStream:把一个文件作为InputStream,实现对文件的读取操作

4) PipedInputStream:实现了pipe的概念,主要在线程中使用

5) SequenceInputStream:把多个InputStream合并为一个InputStream

Out stream:

1) ByteArrayOutputStream:把信息存入内存中的一个缓冲区中

2) FileOutputStream:把信息存入文件中

3) PipedOutputStream:实现了pipe的概念,主要在线程中使用

4) SequenceOutputStream:把多个OutStream合并为一个OutStream

2 以Unicode字符为导向的stream

以Unicode字符为导向的stream,表示以Unicode字符为单位从stream中读取或往stream中写入信息。以Unicode字符为导向的stream包括下面几种类型:

Input Stream:

1) CharArrayReader:与ByteArrayInputStream对应

2) StringReader:与StringBufferInputStream对应

3) FileReader:与FileInputStream对应

4) PipedReader:与PipedInputStream对应

Out Stream:

1) CharArrayWrite:与ByteArrayOutputStream对应

2) StringWrite:无与之对应的以字节为导向的stream

3) FileWrite:与FileOutputStream对应

4) PipedWrite:与PipedOutputStream对应

以字符为导向的stream基本上对有与之相对应的以字节为导向的stream。两个对应类实现的功能相同,字是在操作时的导向不同。如CharArrayReader:和ByteArrayInputStream的作用都是把内存中的一个缓冲区作为InputStream使用,所不同的是前者每次从内存中读取一个字节的信息,而后者每次从内存中读取一个字符。

3 两种不现导向的stream之间的转换

InputStreamReader和OutputStreamReader:把一个以字节为导向的stream转换成一个以字符为导向的stream。

stream添加属性

1 “为stream添加属性”的作用

运用上面介绍的Java中操作IO的API,我们就可完成我们想完成的任何操作了。但通过FilterInputStream和FilterOutStream的子类,我们可以为stream添加属性。下面以一个例子来说明这种功能的作用。

如果我们要往一个文件中写入数据,我们可以这样操作:

FileOutStream fs = new FileOutStream(“test.txt”);

然后就可以通过产生的fs对象调用write()函数来往test.txt文件中写入数据了。但是,如果我们想实现“先把要写入文件的数据先缓存到内存中,再把缓存中的数据写入文件中”的功能时,上面的API就没有一个能满足我们的需求了。但是通过FilterInputStream和FilterOutStream的子类,为FileOutStream添加我们所需要的功能。

2 FilterInputStream的各种类型

用于封装以字节为导向的InputStream

1) DataInputStream:从stream中读取基本类型(int、char等)数据。

2) BufferedInputStream:使用缓冲区

3) LineNumberInputStream:会记录input stream内的行数,然后可以调用getLineNumber()和setLineNumber(int)

4) PushbackInputStream:很少用到,一般用于编译器开发

用于封装以字符为导向的InputStream

1) 没有与DataInputStream对应的类。除非在要使用readLine()时改用BufferedReader,否则使用DataInputStream

2) BufferedReader:与BufferedInputStream对应

3) LineNumberReader:与LineNumberInputStream对应

4) PushBackReader:与PushbackInputStream对应

3 FilterOutStream的各种类型

用于封装以字节为导向的OutputStream

1) DataIOutStream:往stream中输出基本类型(int、char等)数据。

2) BufferedOutStream:使用缓冲区

3) PrintStream:产生格式化输出

用于封装以字符为导向的OutputStream

1) BufferedWrite:与对应

2) PrintWrite:与对应

RandomAccessFile

1) 可通过RandomAccessFile对象完成对文件的读写操作

2) 在产生一个对象时,可指明要打开的文件的性质:r,只读;w,只写;rw可读写

3) 可以直接跳到文件中指定的位置

I/O应用的一个例子

import java.io.*;
public class TestIO{
public static void main(String[] args)
throws IOException{
//1.以行为单位从一个文件读取数据
BufferedReader in =
new BufferedReader(
new FileReader("F:\\nepalon\\TestIO.java"));
String s, s2 = new String();
while((s = in.readLine()) != null)
s2 += s + "\n";
in.close();

//1b. 接收键盘的输入
BufferedReader stdin =
new BufferedReader(
new InputStreamReader(System.in));
System.out.println("Enter a line:");
System.out.println(stdin.readLine());

//2. 从一个String对象中读取数据
StringReader in2 = new StringReader(s2);
int c;
while((c = in2.read()) != -1)
System.out.println((char)c);
in2.close();

//3. 从内存取出格式化输入
try{
DataInputStream in3 =
new DataInputStream(
new ByteArrayInputStream(s2.getBytes()));
while(true)
System.out.println((char)in3.readByte());
}
catch(EOFException e){
System.out.println("End of stream");
}

//4. 输出到文件
try{
BufferedReader in4 =
new BufferedReader(
new StringReader(s2));
PrintWriter out1 =
new PrintWriter(
new BufferedWriter(
new FileWriter("F:\\nepalon\\ TestIO.out")));
int lineCount = 1;
while((s = in4.readLine()) != null)
out1.println(lineCount++ + ":" + s);
out1.close();
in4.close();
}
catch(EOFException ex){
System.out.println("End of stream");
}

//5. 数据的存储和恢复
try{
DataOutputStream out2 =
new DataOutputStream(
new BufferedOutputStream(
new FileOutputStream("F:\\nepalon\\ Data.txt")));
out2.writeDouble(3.1415926);
out2.writeChars("\nThas was pi:writeChars\n");
out2.writeBytes("Thas was pi:writeByte\n");
out2.close();
DataInputStream in5 =
new DataInputStream(
new BufferedInputStream(
new FileInputStream("F:\\nepalon\\ Data.txt")));
BufferedReader in5br =
new BufferedReader(
new InputStreamReader(in5));
System.out.println(in5.readDouble());
System.out.println(in5br.readLine());
System.out.println(in5br.readLine());
}
catch(EOFException e){
System.out.println("End of stream");
}

//6. 通过RandomAccessFile操作文件
RandomAccessFile rf =
new RandomAccessFile("F:\\nepalon\\ rtest.dat", "rw");
for(int i=0; i<10; i++)
rf.writeDouble(i*1.414);
rf.close();

rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "r");
for(int i=0; i<10; i++)
System.out.println("Value " + i + ":" + rf.readDouble());
rf.close();

rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "rw");
rf.seek(5*8);
rf.writeDouble(47.0001);
rf.close();

rf = new RandomAccessFile("F:\\nepalon\\ rtest.dat", "r");
for(int i=0; i<10; i++)
System.out.println("Value " + i + ":" + rf.readDouble());
rf.close();
}
}



关于代码的解释(以区为单位):

1区中,当读取文件时,先把文件内容读到缓存中,当调用in.readLine()时,再从缓存中以字符的方式读取数据(以下简称“缓存字节读取方式”)。

1b区中,由于想以缓存字节读取方式从标准IO(键盘)中读取数据,所以要先把标准IO(System.in)转换成字符导向的stream,再进行BufferedReader封装。

2区中,要以字符的形式从一个String对象中读取数据,所以要产生一个StringReader类型的stream。

4区中,对String对象s2读取数据时,先把对象中的数据存入缓存中,再从缓冲中进行读取;对TestIO.out文件进行操作时,先把格式化后的信息输出到缓存中,再把缓存中的信息输出到文件中。

5区中,对Data.txt文件进行输出时,是先把基本类型的数据输出屋缓存中,再把缓存中的数据输出到文件中;对文件进行读取操作时,先把文件中的数据读取到缓存中,再从缓存中以基本类型的形式进行读取。注意in5.readDouble()这一行。因为写入第一个writeDouble(),所以为了正确显示。也要以基本类型的形式进行读取。

6区是通过RandomAccessFile类对文件进行操作。
2006年01月16日
J2EE 应用程序中的数据管理和数据持久性

英文原文

英文原文

内容:


Java 对象序列化
Java 数据库连接(JDBC)
从 Java 代码调用存储过程
包装
参考资料
作者简介
对本文的评价
相关内容:
J2EE 探险者:持久数据管理,第 2 部分
IBM developer kits for the Java platform (downloads)

JDBC 存储提供了多种数据管理的可能

级别: 中级

G.V.B. SubrahmanyamSubrahmanyam.vb.gampa@citigroup.com) , 顾问, Citigroup Technologies
Shankar Itchapurapushankar.i@polaris.co.in) , 顾问, Citigroup Technologies

2004 年 7 月

本文分析了在 Java 平台上可用的两个数据管理策略:Java 对象序列化和 Java 数据库连接(JDBC)。尽管本质上这两种数据管理策略并不存在孰优孰劣的问题,但在管理企业信息系统时,JDBC 轻而易举地得以胜出。在本文中,Java 开发人员 G.V.B. Subrahmanyam 和 Shankar Itchapurapu 对序列化和 JDBC都进行了介绍,并通过讨论和实例来向您展示了 JDBC 是您的最佳选择的原因。

当您正在建立企业信息系统时,需要确保以某种有效的方式存储、检索和显示企业数据。对于所有业务而言,数据都是独一无二的最大资产。所有软件系统都涉及数据,因此,数据的重要性是无论如何强调都不过分的。

应用程序的数据管理功能包括四个基本操作,通常也需要对企业数据执行这四个操作,它们是:建立、检索、更新删除(即 CRUD)。管理在企业系统的数据涉及在很长一段时间范围之内,始终如一地、成功地执行 CRUD 操作,而不必频繁地更改实际执行这些操作的代码。换句话说,管理数据意味着开发稳健的、可扩展和可维护的软件系统,以确保成功地进行 CRUD 操作,在软件的生命期中能够以一致的方式执行操作。

本文讨论了 J2EE 中的两种可用数据管理策略:Java 对象序列化和 Java 数据库连接(JDBC)。我们将查看这两种方法的优缺点。这两种数据管理策略实质上不存在孰优孰劣。在特定实现中,策略的可用性取决于项目的范围(出现在系统环境中的活动的活动范围),系统的上下文(驱动系统/子系统运行时的值的集合),以及其他的外部因素。然而,Java 序列化并不适合于企业系统,其数据需要用一种定义良好的结构(如RDBMS)来组织。我们首先将快速浏览 Java 对象序列化,然后查看 JDBC 更重要的一些方面,从而了解后者是如何实现前者所缺乏的一些关键特性的。

本文并不打算对 Java 对象序列化或者 JDBC 进行全面介绍。有关这两项技术的更多信息,请回顾参考资料小节。

Java 对象序列化
对象序列化是最简单的 Java 持久性策略。对象序列化是一个将对象图平面化为一个字节的线性序列的过程。对象图是作为对象继承、关联和聚合的结果而实现的一些关系式。对象的非暂态实例属性以字节的形式被写入到持久存储中。实例属性的值就是执行时间序列化时内存中的值。如果一个 Java 对象是可序列化的,那么它至少必须实现 java.io.Serializable 接口,该接口具有如下所示的结构:

package java.io; public interface Serializable {}

您可以看到,java.io.Serializable 接口并没有声明任何方法。它是一个记号或者标记接口。它告诉 Java 运行时环境,该实现类是可序列化的。列表 1 显示实现该接口的一个示例类。

列表 1. MySerializableObject.java

import java.io.Serializable; public class MySerializableObject extends MySuperClass implements Serializable { private String property1 = null; private String property2 = null; public String getProperty1() { return property1; } public void setProperty1(String val) { property1 = val; } public String getProperty2() { return property2; } public void setProperty2(String val) { property2 = val; } private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject (getProperty1 ()); out.writeObject (getProperty2 ()); } private void readObject (ObjectInputStream in) throws IOException, ClassNotFoundException { setProperty1 ((String) in.readObject ()); setProperty2 ((String) in.readObject ()); } }

无需自己实现 writeObject(...)readObject(...) 方法来执行序列化;Java 运行时环境具有使这些方法可用的默认实现。然而,您可以重写这些方法,提供如何存储对象状态的您自己的实现。

关于序列化,您需要记住一些要点。首先,在序列化期间,整个对象图(即,所有父类和被引用类)都将被序列化。其次, Serializable 类的所有实例变量自身都应该是可序列化的,除非已经特别声明它们为暂态,或者已经重写 writeObject(...)readObject(...) 来只序列化那些可序列化的实例变量。如果违反了后一规则,在运行时将出现一个异常。

每个后续 J2SE 版本都对对象序列化系统进行少量的增加。J2SE 1.4 也相应地向 ObjectOutputStream and ObjectInputStream 增加 writeUnshared() and readUnshared()方法。通常,一个序列化的流只包含任何给定对象的一个序列化实例,并且共享对该对象引用的其他对象可以对它进行后向引用。通常期望序列化一个对象独立于其他对象可能维护的任何引用。非共享的读写方法允许对象作为新的、独一无二的对象被序列化,从而获得一个类似于对象克隆但开销更少的效果。

Java 对象序列化存在的问题
序列化涉及到将对象图从内存具体化到持久存储(例如硬盘)中。这涉及到大量 I/O 开销。通常,对应用程序而言,序列化并不是最佳选择:

  • 管理几十万兆字节的存储数据
  • 频繁地更新可序列化对象

对存储企业数据而言,序列化是一个错误选择,因为:

  • 序列化的字节流只对 Java 语言是可读的。这是一个重大缺陷,因为企业系统通常是异构的,许多应用程序要与其他应用程序共同处理相同的数据。

  • 对象检索涉及大量的 I/O 开销。

  • 没有一个用来从序列化对象图中检索获取数据的查询语言。

  • 序列化没有内置的安全机制。

  • 序列化本身并不提供任何事务控制机制,因此不能在那些需要并发访问从而不使用辅助 API 的应用程序中使用它。

Java 数据库连接(JDBC)
Java 数据库连接(JDBC)是一个标准的 API,它使用 Java 编程语言与数据库进行交互。诸如 JDBC 的调用级接口是编程接口,它们允许从外部访问 SQL 命令来处理和更新数据库中的数据。通过提供与数据库连接的库例程,它们允许将 SQL 调用集成到通用的编程环境中。特别是,JDBC 有一个使接口变得极其简单和直观的例程的丰富收集。

在下面几个小节中,我们将查看通过 JDBC 与数据库连接所涉及的一些步骤。我们将特别关注与 Java 对象序列化相比,JDBC 是如何成为一个企业数据管理策略的。

建立一个数据库连接
在利用 JDBC 做任何其他事情之前,需要从驱动程序供应商那里获取数据库驱动程序,并且将该库添加到类路径中。一旦完这项工作,就可以在 Java 程序中使用类似于下面所示的代码来实现实际的连接。

Class.forName(); Java.sql.Connection conn = DriverManager.getConnection();

Java 对象序列化并不需要这个该步骤,因为使用序列化来执行持久性操作并不需要 DBMS。 序列化是一个基于文件的机制;因此,在序列化一个对象之前,需要在目标文件系统中打开一个 I/O 流。

创建 JDBC Statement 和 PreparedStatement
可以用 JDBC Statement 对象将 SQL 语句发送到数据库管理系统(DBMS),并且不应该将该对象与 SQL 语句混淆。 JDBC Statement 对象是与打开连接有关联,而不是与任何单独的 SQL 语句有关联。可以将 JDBC Statement 对象看作是位于连接上的一个通道,将一个或多个(您请求执行的)SQL 语句传送给 DBMS。

为了创建 Statement 对象,您需要一个活动的连接。通过使用我们前面所创建的 Connection 对象 con——下面的代码来完成这项工作。

Statement stmt = con.createStatement();

到目前为止,我们已经有了一个 Statement 对象,但是还没有将对象传递到 DBMS 的 SQL 语句。

当数据库接收到语句时,数据库引擎首先会分析该语句并查找句法错误。一旦完成对语句的分析,数据库就必须计算出执行它的最有效方法。在计算上,这可能非常昂贵。数据库会检查哪些索引可以提供帮助,如果存在这样的索引的话,或者检查是否应该完全读取表中的所有行。数据库针对数据进行统计,找出最佳的执行方式。一旦创建好查询计划,数据库引擎就可以执行它。

生成这样一个计划会占用 CPU 资源。理想情况是,如果我们两次发送相同的语句到数据库,那么我们希望数据库重用第一个语句的访问计划,我们可以使用 PreparedStatement 对象来获得这种效果。

这里有一个主要的特性是,将 PreparedStatement 与其超类 Statement 区别开来:与 Statement 不同,在创建 PreparedStatement 时,会提供一个 SQL 语句。然后了立即将它发送给 DBMS,在那里编译该语句。因而, PreparedStatement 实际上是作为一 个通道与连接和被编译的 SQL 语句相关联的。

那么,它的优势是什么呢?如果需要多次使用相同的查询或者不同参数的类似查询,那么利用 PreparedStatement,语句,只需被 DBMS 编译和优化一次即可。与使用正常的 Statement 相比,每次使用相同的 SQL 语句都需要重新编译一次。

还可以通过 Connection 方法创建PreparedStatement 。下面代码显示了如何创建一个带有三个输入参数的参数化了的 SQL 语句。

PreparedStatement prepareUpdatePrice = con.prepareStatement( "UPDATE Sells SET price = ? WHERE bar = ? AND beer = ?");

注意,Java 序列化不支持类似于 SQL 的查询语言。使用 Java 序列化访问对象属性的惟一途径就是反序列化该对象,并调用该对象上的 getter/accessor 方法。反序列化一个完整的对象在计算上可能很昂贵,尤其是在程序的生命期中,应用程序需要重复执行它。

在执行 PreparedStatement 之前,需要向参数提供值。通过调用 PreparedStatement 中定义的 setXXX() 方法可以实现它。最常使用的方法是 setInt()setFloat()setDouble(),以及 setString()。每次执行已准备的声明之前,都需要设置这些值。

执行语句和查询
执行 JDBC 中的 SQL 语句的方式是根据 SQL 语句的目的而变化的。DDL(数据定义语言)语句(例如表建立和表更改语句)和更新表内容的语句都是通过使用 executeUpdate() 执行的。列表 2 中包含 executeUpdate() 语句的实例。

列表 2. 实际运行中的 executeUpdate()

Statement stmt = con.createStatement(); stmt.executeUpdate("CREATE TABLE Sells " + "(bar VARCHAR2(40), beer VARCHAR2(40), price REAL)" ); stmt.executeUpdate("INSERT INTO Sells " + "VALUES ('Bar Of Foo', 'BudLite', 2.00)" ); String sqlString = "CREATE TABLE Bars " + "(name VARCHAR2(40), address VARCHAR2(80), license INT)" ; stmt.executeUpdate(sqlString);

我们将通过先前插入的参数值(如上所示)执行 PreparedStatement ,然后在这之上调用 executeUpdate(),如下所示:

int n = prepareUpdatePrice.executeUpdate() ;

相比之下,查询期望返回一个行作为它的结果,并且并不改变数据库的状态。这里有一个称为 executeQuery() 的相对应的方法,它的返回值是 ResultSet 对象,如列表 3 所示。

列表 3. 执行一个查询

String bar, beer ; float price ; ResultSet rs = stmt.executeQuery("SELECT * FROM Sells"); while ( rs.next() ) { bar = rs.getString("bar"); beer = rs.getString("beer"); price = rs.getFloat("price"); System.out.println(bar + " sells " + beer + " for " + price + " Dollars."); }

由于查询而产生的行集包含在变量 rs 中,该变量是 ResultSet 的一个实例。集合对于我们来说并没有太大用处,除非我们可以访问每一个行以及每一个行中的属性。ResultSet 提供了一个光标,可以用它依次访问每一个行。光标最初被设置在正好位于第一行之前的位置。每个方法调用都会导致光标向下一行移动,如果该行存在,则返回 true,或者如果没有剩余的行,则返回 false

我们可以使用适当类型的 getXXX() 来检索某一个行的属性。在前面的实例中,我们使用 getString()getFloat() 方法来访问列值。注意,我们提供了其值被期望用作方法的参数的列的名称;我们可以指定用列号来代替列名。检索到的第一列的列号为 1,第二列为 2,依次类推。

在使用 PreparedStatement 时,可以通过先前插入的参数值来执行查询,然后对它调用 executeQuery(),如下所示:

ResultSet rs = prepareUpdatePrice.executeQuery() ;

关于访问 ResultSet 的注释
JDBC 还提供一系列发现您在结果集中的位置的方法:getRow()isFirst()isBeforeFirst()isLast(),以及 isAfterLast()

这里还有一些使可滚动光标能够自由访问结果集中的任意行的方法。在默认情况下,光标只向前滚动,并且是只读的。在为 Connection 创建 Statement 时,可以将 ResultSet 的类型更改为更为灵活的可滚动或可更新模型,如下所示:

Statement stmt = con.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ResultSet rs = stmt.executeQuery("SELECT * FROM Sells");

不同的类型选项: TYPE_FORWARD_ONLYTYPE_SCROLL_INSENSITIVETYPE_SCROLL_SENSITIVE 。可以通过使用 CONCUR_READ_ONLYCONCUR_UPDATABLE 选项来选择光标是只读的还是可更新的。对于默认光标,可以使用 rs.next() 向前滚动它。对于可滚动的光标,您有更多的选项,如下所示:

rs.absolute(3); // moves to the third retrieved row rs.previous(); // moves back one row in the retrieved result set rs.relative(2); // moves forward two rows in the retrieved result set rs.relative(-3); // moves back three rows in the retrieved result set

对于可滚动光标的工作方式,这里有更多的详细描述。尽管可滚动光标对于特定应用程序是有用的,但是它导致极大的性能损失,所以应该限制和谨慎使用。可以在 参考资料小节中找到关于可滚动 ResultSet 的更多信息。

在序列化中不存在与 JDBC 的 ResultSet 相对应的机制。序列化和 JDBC 观察底层的数据的角度不同。JDBC (通常)假定底层数据是关系型结构的;而序列化假定底层数据是一个对象图。两种技术的底层数据结构存在显著差异。JDBC 的 Set 结构并不能自然地映射到序列化的对象图结构,反之亦然。当通过使用序列化语义将一个 Java 对象持久化时,数据的底层结构变成了一个字节流,该字节流展示了已经序列化了的核心对象的各种内部对象之间的关联。

JDBC 中的 ResultSet 导航是从一个 Set 元素移动到其他元素的过程,而在对象序列化中,这是不可能的,因为序列化涉及到对象关联,而不是将一组行封装到一个实体集合中。因此,Java 对象序列化无法向您提供用这种方式访问数据单独某个部分的能力。

事务
JDBC 允许将 SQL 语句组合到单独一个事务中。因此,我们可以通过使用 JDBC 事务特性来确保 ACID 属性。

Connection 对象执行事务控制。当建立连接时,在默认情况下,连接是自动提交模式下。这意味着每个 SQL 语句自身都被看作是一个事务,并且一完成执行就会被提交。

可以用以下方法开启或关闭自动提交模式:

con.setAutoCommit(false) ; con.setAutoCommit(true) ;

一旦关闭了自动提交,除非通过调用 commit() 显式地告诉它提交语句,否则无法提交 SQL 语句(即,数据库将不会被持久地更新)。在提交之前的任何时间,我们都可以调用 rollback() 回滚事务,并恢复最近的提交值(在尝试更新之前)。

我们还可以设置期望的事务隔离等级。例如,我们可以将设置事务隔离等级为 TRANSACTION_READ_COMMITTED,这使得在提交值之前,不允许对它进行访问。并且禁止脏读。在 Connection 接口中为隔离等级提供了五个这样的值。默认情况下,隔离等级是可序列化的。JDBC 允许我们发现数据库所设置的是什么事务隔离等级(使用 ConnectiongetTransactionIsolation() 方法)以及设置适当的等级(使用 ConnectionsetTransactionIsolation() 方法)。

回滚通常与 Java 语言的异常处理能力结合在一起使用。这种结合为处理数据完整性提供一个简单高效的机制。在下一节中,我们将研究如何使用 JDBC 进行错误处理。

注意,Java 对象序列化并不直接支持事务管理。如果您正在使用序列化,则将需要借助其他的 API,例如 JTA,来获得这个效果。然而,为了获得事务隔离的效果,可以选择在执行一个更新操作时同步该序列化对象,如下所示:

Synchronized(my_deserialized_object) { //Perform the updates etc... }

利用异常处理错误
软件程序中总是出现一些错误。通常,数据库程序是关键性应用程序,而且适当地捕获和处理错误是有必要的。程序应该恢复并且让数据库处于某种一致的状态下。将回滚与 Java 异常处理程序结合使用是达到这种要求的一种简便方法。

访问服务器(数据库)的客户(程序)需要能够识别从服务器返回的所有错误。JDBC 通过提供两种等级的错误条件来访问这种信息:SQLExceptionSQLWarningSQLException 是 Java 异常,它(如果未被处理)将会终止该应用程序。SQLWarningSQLException 的子类,但是它们代表的是非致命错误或意想不到的条件,因此,可以忽略它们。

在 Java 代码中,希望抛出异常或者警告的语句包含于 try 块中。如果在 try 块中的语句抛出异常或者警告,那么可以在对应的某个 catch 语句中捕获它。每个捕获语句都指出了它准备捕获的异常。

换句话说,如果数据类型是正确的,但是数据库大小超出其空间限制并且不能建立一个新表,则可能会抛出一个异常。 可以从 ConnectionStatement,以及 ResultSet 对象中获取 SQLWarning。每个对象都只是存储最近 SQLWarning。因此,如果通过 Statement 对象执行其他语句,则将放弃所有早期的警告。列表 4 举例说明了 SQLWarning 的使用。

列表 4. 实际运行中的 SQLWarnings

ResultSet rs = stmt.executeQuery("SELECT bar FROM Sells") ; SQLWarning warn = stmt.getWarnings() ; if (warn != null) System.out.println("Message: " + warn.getMessage()) ; SQLWarning warning = rs.getWarnings() ; if (warning != null) warning = warning.getNextWarning() ; if (warning != null) System.out.println("Message: " + warn.getMessage()) ;

实际上,SQLWarning 在某种程度上比 SQLException 更为罕见。最常见的是 DataTruncation 警告,它表示在从数据库读或写数据时存在问题。

Java 并没有提供序列化所使用的特定的异常类。使用序列化时发生的大多数异常都与执行的 I/O 操作有关,因此,在这些情况中 I/O 异常类将满足要求。

批处理
JDBC 2.0 提供一个用于批处理的强大API。批处理允许积累一组 SQL 语句,并且将它们一起发送并处理。一个典型的批处理就是银行应用程序,该应用程序每隔一刻钟就要更新许多账号。在减少从 Java 代码到数据库的往返次数方面,批处理是一个强大功能。

Statement 接口提供 addBatch(String) 方法,将 SQL 语句添加到一个批处理中。一旦已经将所有的 SQL 语句都增加到该批处理中,就可以使用 executeBatch() 方法一起执行它们。

然后,用executeBatch() 方法执行 SQL 语句,并返回 int 值的一个数组。该数组包含受每条语句影响的行数。将 SELECT 语句或者其他返回 ResultSet 的 SQL 语句放在一个批处理中会导致 SQLException

列表 5 中列出了利用 java.sql.Statement 进行批处理的一个简单实例。

列表 5. 实际运行中的批处理

Statement stmt = conn.createStatement(); stmt.insert("DELETE FROM Users"); stmt.insert("INSERT INTO Users VALUES('rod', 37, 'circle')"); stmt.insert("INSERT INTO Users VALUES('jane', 33, 'triangle')"); stmt.insert("INSERT INTO Users VALUES('freddy', 29, 'square')"); int[] counts = stmt.executeBatch();

在您不知道特定语句将运行的次数时,批处理是一个处理 SQL 代码的好方法。例如,如果在不使用批处理的情况下试图插入 100 条记录,那么性能可能会受到影响。如果编写一个脚本,增加 10000 条记录,那么情况会变得更糟。添加批处理可以帮助提高性能,后者甚至能够提高代码的可读性。

Java 对象序列化并不支持批处理。通常,会在某个对象的范围(联系图)上运用序列化,在这种情况下,批处理没有意义。因此,批处理在数据更新的定时和分组方面为您提供一定的灵活性,而这些对于序列化来说不一定是可用的。

从 Java 代码调用存储过程
存储过程是一组 SQL 语句,它们建立了一个逻辑单元,并执行特定任务。可以用存储过程来封装一个操作或者查询的集合,这些操作或查询都将在一个数据库服务器上执行。存储过程是在数据库服务器中被编译和存储的。因此,每次调用存储过程时,DBMS 都将重用已编译的二进制代码,因此执行速度会更快。

JDBC 允许您从 Java 应用程序中调用数据库存储过程。第一步是创建 CallableStatement 对象。与 StatementPreparedStatement 对象一样,这项操作是用一个打开的 Connection 对象完成的。CallableStatement 对象包含对存储过程的调用;但它并不包含存储过程自身。列表 6 中的第一行代码使用 con 连接建立了对存储过程 SHOW_ACCOUNT 的调用。波形括号中括住的部分是存储过程的转义语法。当驱动程序遇到 {call SHOW_ACCOUNT} 时,它将该转义语法翻译成数据库所使用的本地 SQL,从而调用名为 SHOW_ACCOUNT 的存储过程。

列表 6. 实际运行中的存储过程

CallableStatement cs = con.prepareCall("{call SHOW_ACCOUNT(?)}"); cs.setInt(1,myaccountnumber); ResultSet rs = cs.executeQuery();

假设 Sybase 中的存储过程 SHOW_ACCOUNT 包含列表 7 中所示的代码。

Listing 7. SHOW_ACCOUNT stored procedure

CREATE PROCEDURE SHOW_ACCOUNT (@Acc int) AS BEGIN Select balance from USER_ACCOUNTS where Account_no = @Acc END

ResultSet rs 看起来类似于:

balance ---------------- 12000.95

注意,用来执行 cs 的方法是 executeQuery(),由于 cs 调用的存储过程只包含一个查询,所以只产生一个结果集。如果该过程只包含一个更新或者一个 DDL 语句,则将使用 executeUpdate() 方法。然而,有时候存在存储过程包含多个 SQL 语句的情况,在这种情况下,它将产生多个结果集、多个更新计数,或者结果集和更新计数的某种结合。因此,应该使用 execute() 方法执行 CallableStatement

CallableStatement 类是 PreparedStatement 的子类,因此 CallableStatement 对象可以接受与 PreparedStatement 对象相同的参数。而且,CallableStatement 对象可以接受输出参数,并将该参数用于输入和输出。INOUT 参数和 execute() 方法通常很少使用。要想处理 OUT 参数,需要通过使用 registerOutParameter(int, int) 方法将 OUT 参数注册到存储过程。

举例说明,我们假设 GET_ACCOUNT 过程包含列表 8 中的代码。

列表 8. GET_ACCOUNT

CREATE PROCEDURE GET_ACCOUNT (@Acc int, @balance float OUTPUT) AS BEGIN Select @balance = balance from USER_ACCOUNTS where Account_no = @Acc END

在这个实例中,参数 balance 被声明是一个 OUT 参数。现在,调用该过程的 JDBC 代码如列表 9 所示。

列表 9. 调用一个存储过程的 JDBC 代码

CallableStatement csmt = con.prepareCall("{GET_ACCOUNT(?,?)"); csmt.setInt(1,youraccountnumber); csmt.registerOutParamter(2,java.sql.Types.FLOAT); csmt.execute();

正使用 Java 序列化时,并不需要访问任何外部的系统,如 DBMS。换句话说,序列化是一个纯 Java 语言现象,它不涉及执行一个外部环境中的已编译代码。因此,在序列化中不存在与 CallableStatement 对象相对应的机制。这意味着您不能将数据处理转移到外部系统或者组件中,尽管这些系统或者组件可能更适合它。

包装
在读完本文之后,我们希望您赞同:对于数据管理和持久化而言, JDBC 是比 Java 对象序列化要好得多的方法。

JDBC 是一个用来访问数据存储的极好的 API。 JDBC 最好的东西是它提供单一的 API 集合来访问多种数据源。用户只需要学习一个 API 集合,就可以访问任何数据源,这些数据源可以是关系型的、层次型的或者任何其他格式。您需要的只是一个 JDBC 驱动程序,用它连接到目标数据源。JDBC 做了大量工作,将所有技术细节都封装到了一个实现软件包中,从而将程序员从供应商特定的桎梏中解放出来。

表 1 对比了 JDBC 和 Java 对象序列化的各种特性。

表 1. JDBC 对 Java 序列化

对象序列化 JDBC
数据管理 使用文件系统存储序列化对象格式。这些系统中不包括特定的数据管理系统。序列化对象(存储在普通文件中的)通常是以自己的特殊方式通过底层 OS 来管理的。 使用一个 EAI/数据库来存储数据。EAI 或者数据库具有一个用来管理数据源中的数据指定的数据库管理系统(DBMS)。JDBC 是将请求发送到 DBMS 的 JVM 和 DBMS 之间的接口。JDBC 自身并不具有任何数据管理功能。
数据结构 底层的数据结构是一个对象图。序列化将 Java 对象的状态写入到文件系统。 底层的数据结构可以是关系型的、层次型的,或者是网络形状。但是数据的逻辑视图通常是一个表。
数据定义 数据定义涉及到使用序列化语义建立一个可序列化对象并持久化该对象。 数据定义涉及到在目标数据存储中建立必要的表,并且提供实体集之间的域级关系的明确定义。这一般是通过使用目标 DBMS 所提供的软件来完成的。
数据检索 数据检索涉及反序列化对象,并使用访问者方法读取对象的属性。 DBMS 提供一个特殊的数据子语言来检索数据。通过 JDBC API 可以将以这种数据子语言编写的语句传递给目标数据源。DBMS 负责验证、执行和返回该语句的结果。
安全 没有可以使用的定义良好的安全机制。然而,底层的 OS 可以提供已序列化文件的安全。 DBMS 提供一个广泛的安全特性集合。它可以完成认证和授权的工作。在可以访问或者操作 DBMS 上的数据之前,JDBC API 需要给目标 DBMS发送证书。
事务 没有可以使用的特定的事务控制机制。通过使用其他的 J2EE API,例如 JTA 或者 JTS,可以在程序上维护事务。 DBMS 提供复杂的事务管理。JDBC API 提供有用的方法来提交和回滚事务。
并发控制 没有可以使用的特定的并发控制机制。不过,通过使用 Java 语言中的同步技术可以获得并发控制的效果。 DBMS 提供多种等级的事务隔离。可以使用 JDBC API 方法来选择一个特定等级的隔离。

Java 对象序列化和 JDBC 是 Java 技术领域中许多数据持久化机制中的两种。在需要在多个 JVM 之间以 Java 语言特定格式共享数据(例如用 RMI 的按值传递机制共享数据)时,序列化最适合不过。然而,Java 序列化并不适用于企业数据,需要以一种定义良好的结构对这些数据进行组织。在这样的企业系统中,需要在多个系统和子系统之间共享数据,而这些系统并不一定都与 Java 语言兼容。在这种情况中,对象序列化根本不能工作。

JDBC 提供一个公用 API来访问异构的数据存储。它是 JVM 和目标 DBMS 之间的粘合剂。它提供了一个使用 Java 平台访问数据存储和维护企业数据的纲领性方法。然而,执行 CRUD 操作所需的所有代码都是由开发人员编写。

为了在企业环境中最有效地使用 JDBC,架构设计人员需要分析其企业中的数据,并开发一个用于数据持久性的框架。由于使用 JDBC 持久化数据的机制与系统想要解决的商业问题无关,因此强烈建议将数据持久性层与应用程序的商业逻辑相分离。设计模式对设计这种框架非常有帮助。

参考资料

作者简介
G.V.B. Subrahmanyam 博士拥有技术领域的硕士学位和 IIT Kharagpur 授予的博士学位,还有 BITS, Pilani 授予的软件系统硕士学位。可以通过 Subrahmanyam.vb.gampa@citigroup.com 联系他。


Shankar Itchapurapu 拥有计算机应用方面的硕士学位。可以通过 shankar.i@polaris.co.in 联系他。

2006年01月13日

1. package bookstore.servlet;
2. …
3. public class LoginCheckFilter
4. extends HttpServlet implements Filter
5. {
6.  …
7.  public void doFilter(ServletRequest request, ServletResponse response
8.      , FilterChain filterChain)
9.  {
10.  try
11.  {
12.   //进行请求和响应的类型转换
13.   HttpServletRequest httpRequest = (HttpServletRequest) request;
14.   HttpServletResponse httpResponse = (HttpServletResponse) response;
15.
16.   boolean isValid = true;
17.   String uriStr = httpRequest.getRequestURI().toUpperCase();
18.   if (uriStr.indexOf("LOGIN.JSP") == -1 &&
19.     uriStr.indexOf("SWITCH.JSP") == -1 &&
20.     httpRequest.getSession().getAttribute("ses_userBean") == null)
21.   {
22.    isValid = false;
23.   }
24.   if (isValid)
25.   {
26.    filterChain.doFilter(request, response);
27.   } else
28.   {
29.    httpResponse.sendRedirect("/webModule/login.jsp");
30.   }

31.
32.  } catch (ServletException sx)
33.  {
34.   filterConfig.getServletContext().log(sx.getMessage());
35.  } catch (IOException iox)
36.  {
37.   filterConfig.getServletContext().log(iox.getMessage());
38.  }
39. }
40. …
41. }

我在一个生物技术企业工作了四年,之前是做市场的,最近一年被老板调到了人力资源部当经理。一年的人事工作经历使我对人性有了更深入的认识,对中国人(包括自己在内)的坏毛病有颇多感慨和无奈。之所以放大说是中国人的劣根性,是因为我相信我下面说的很多特性在国人身上是普遍存在的,发生的几率要高于那些比我们好的国家。
  我是一个中国人,并不想贬低自己的民族,但我认为我们民族经过这一百年来的动荡,特别是十年文革,教育的确是被歪曲和延误了,国民整体素质处于一个很低的水平。我在下面所发表的言论,既是在揭中国人的伤疤,也是在揭自己的伤疤,但我相信一个人或者一个民族,只有勇于正视自己的缺点和毛病,才有改进和强大的机会。

  一、人人相轻

  中国人不是文人相轻,而是人人相轻,只要想轻视别人,总有相轻的理由。比如北京人轻视外地人,上海人轻视外地人,城里人轻视农村人,南方人轻视北方人,有钱人轻视穷人,开车的轻视走路的,走路的轻视扫路的,吃饭的轻视做饭的……就是不会相互尊重。

  在企业里面,就表现为硕士轻视本科,本科轻视大专,大专轻视中专,名校轻视非名校(靠!中国有什么名校?),干部轻视职员,职员轻视工人。更搞笑的是学理科的轻视学文科的,学文科的轻视学理科的,市场部的轻视技术部的,技术部的轻视市场部的。这不是随口乱掰,我就常听到“他们技术部的水平不行,解决不了什么质量问题”、“他们市场部的人员素质太低了,基本的产品知识都不具备”……这样的废话加屁话。都是一个公司的,别人不行要伸手帮忙,站在那里说风凉话能解决什么问题呢?

  说句老实话,在一个公司里面,都是出来打工的,谁比谁高多少呢?何况大家捧着的是一个饭碗。都是中国人,美国人把咱大使馆说炸就炸了,日本人就是不还钓鱼岛,连香港人都说咱们是“大圈仔”,我们还有什么理由去轻视自己的同胞?一个缺乏同情心的民族绝对不会是一个伟大的民族。我每次看见那些吃饱了腆着肚子趾高气昂地骂服务生的人,以及我们公司那些拿着几千块RMB(折合几百美金)的伪白领,以为自己忽然中产了,整个一不知道天高地厚的傻样,就觉得这个国家没什么希望。

  我记得以前读书的时候,每次大考,统计总分要精确到小数点后两位,然后依分数排名,根据排名自己挑座位,于是坐前面的就轻视坐后面的,老师还要说“你们坐前面的不要到后面去玩啊!”,估计中国人爱轻视别人的坏毛病就是那时候养成的。

  二、缺乏团队精神

  人人相轻,自然学不会相互合作。加之私心重、视野窄、眼光短,所以中国人在企业里面非常缺乏团队精神。

  我最近在公司推行绩效考核,有些部门经理不爽了,因为他们一算,自己的奖金要变少,还要被公司考核,于是背后说坏话的也有,开会大吵大闹的也有,不闻不问的也有,种种姿态,不一而足。有同事问我:“不至于那么严重吧,不就是搞绩效考核吗?一个制度而已”。制度本身倒不复杂,但是损害了某些人的个人利益,于是这个事情就变得复杂了。这些经理不会说自己的奖金变少了,而会说本部门的奖金变少了,本部门的风险变大了,或者挑起部门员工对制度的敌意,来对我施加压力。所以一个很简单的事情,就变得非常复杂了。

  中国人很少会把团队利益放在个人利益之上。其实在一个企业,团队利益和个人利益是一起的,公司好了大家都好,公司垮了,个人也拿不了几个月薪水。老外很崇尚个人价值,但在企业和组织里面非常遵循个体服从整体的准则,这就是对企业的正确理解。所以中国的职业经理人其实很不职业,就是没有团队精神,把个人或者部门凌驾于整个组织之上。开会讲话都是“我们市场部”、“他们技术部”、“他们物流部”、“他们财务部”,听起来不象是一个公司的,象有仇。我记得有次一个经理为他部门员工薪酬的事情问我“你们公司……”,我当时反问了一句“我们是谁?公司是谁?”他一下子楞住了。

  美国人在自家小孩读幼儿园的第一天,回来问的是“你今天为别的小朋友做了什么?”、“你为老师做了什么?”……这就是从小培养合作意识、团队精神。我估计中国的父母可能问的是“你今天喝了牛奶没有?”(担心自家小孩没喝到),“你今天在幼儿园乖吗?”(担心不乖被人打)……所以中国人从小被教育的是强调利己,而不是强调合作。NBA那个嘉得乐饮料的广告语“我有,我可以”被国内企业大肆抄袭,于是“我选择,我喜欢”、“我运动,我快乐”之类的东西到处泛滥,其实这里面就隐含着一种很突出“自我”的思想。我不明白为什么我们中国人老爱做些纠枉过正的事情,要么灭绝人性的搞共产主义,要么把西方的个人价值观夸张到极端自私的地步。一个社会也好,一个企业一个组织也好,应该是我为人人,人人为我。不合作,就是不利己,都强调自己,漠视别人,这个国家不会进步,一打仗大家又要做亡国奴。

  缺乏团队精神,企业内耗就多了,在我们公司,有40%的工作时间是去解决内耗的,因为部门间的摩擦太多,个人间的摩擦太多。所以我就感慨,老外几万人的公司都管得好,咱们中国企业百来号人就象一盘散沙,这不是一个管理制度或者管理手段的问题,而是一个文化的问题。中国人的历史就是这样的,老爱自己内部起哄,一跟外人打就完了。私心太重,就不会顾全大局,不顾全大局,就学不会妥协,不会妥协,就天天吵架,你争我斗,企业就在这样的内耗中完蛋了。

  三、疑心大,不诚信

  做人事经理免不了经常和人沟通,我就发现我们公司的人与人之间特别不坦诚,大家总是相互猜疑,经常听到这样的话“我知道他是这样看我的……”、“他肯定在老板面前说了我的坏话……”、“这个事情我不好说,不想惹麻烦……”,人前不说真话,人后乱说坏话。于是,企业的市场问题、生产问题变成了人际关系的问题,简单的问题搞复杂了。

  中国人从小就被教育不要信任别人,到了读中学的时候就会耍政治手腕了,刚才还在一起踢球,转身就找老师打小报告。我的初中班主任就每天轮流安排人写纪律监察报告,中国人活得不阳光,就是这样被教化出来的。

  不讲诚信也是从小养成的坏毛病。我妈妈从小教育我不准撒谎,但她自己却没有做到,邻居来借油明明有说没有,答应小学毕业跟我买辆自行车结果没买,经常把公家的电池拿到自己家用……。所以中国人说谎跟玩似的,因为家庭教育跟学校教育都没上好这一课。进了企业,就是对同事不讲诚信,对老板不讲诚信,对客户不讲诚信。我刚做人事经理的时候,很多人跟我说,人事经理就是老板的传声筒,做这个职位只有死路一条,千万不要做啊!我做了一年,发现其实老板没什么大问题,而是他们天生的爱猜疑老板,又不当着老板的面说实话。所以自己营造一个幻象,自己又信得不得了。企业里面的人际关系矛盾都是这样造成的。

  我们跟老外打交道,有问题他们会当面指出,不管多难堪,但这并不妨碍他吃饭的时候跟你谈笑风生。所以老外开会,会上可能有10种声音,但会后只有1种声音;中国人开会,会上没人说话,但会后可能有10种声音。我们老板开会结束时通常会问“大家还有什么意见?”全体沉默。一出会议室,跑到自己办公室门一关就开始开部门小会了,靠。

  无论在一个社会或是企业里面,诚信度越低,运行成本越高。中国人只信任跟自己有血缘关系的人,很难相信别人,其实是我们社会不够文明的一个表现。

  四、蔑视制度

  当人事经理的第一天,老板就跟我说:你最大的任务就是把公司的管理制度化。起初还不大理解,后来明白了老板的苦心,公司的各种制度不少,就是基本上没人遵守。这里面有两个问题:一是制度设计本身有缺陷,二是员工意识里根本就没有对制度的概念。

  中国人很聪明,但不知怎么把“制度”这个东西(包括制度的设计和遵守)总是搞不好。我是学法律的,我一直认为美国今天之所以这么强大,就是立国时把管理国家的体系和制度设计好了,大家可以安心搞建设。西方人的制度设计有时候是可以用“精妙”形容的,而且对制度的执行在我们看来近乎呆板,而中国人的聪明之处则是在于不管什么制度,都可以把它回避、歪曲、改造,直到这个制度等于没有。

  我上任后订了一个考勤制度,规定迟到一次扣10元,第二次40元,累积三次计旷工一天(因为公司的迟到现象很严重)。结果制度出来后,我一看有的员工迟到三次了,想着旷工罚款太重,心一软,就对员工说:“到了第三次迟到就补请一个事假吧,事假总比旷工好,下次不要迟到了”(这是我率先违反制度)。结果有的员工下个月仍然迟到三次,刚开始请迟到后事假,后来请病假(因为病假扣的钱更少),后来每次迟到都请病假,到后来连请假条也没有了,打个电话就完事……我痛定思痛,反思洪水泛滥起因是自己放闸,下了一个通知:“以后迟到一律不准事后补假”。不准事后请假,迟到的员工就把请假条的时间提前一天,反正经理们不管。我那时想到了《鹿鼎记》里面康熙对韦小宝说的一句话:“鳌拜逼朕一步,朕就要退一步,朕实在是退无可退了啊!”。最后实在没辙,宣布“迟到一律不准请假”。实施的当月有个女职员迟到三次,我通知她被记旷工了,她委屈得快要哭起来:“我从小就没有旷过课,现在居然被记旷工,你可以问××经理我那天迟到是因为……”,最后一句是“公司讲不讲人性化管理?!”我坚持不为所动,心想自己就是太讲人性,所以酿成如此大错。

  一个考勤制度执行都如此艰难,其它的制度就不用多说了。我上任以来推行制度化管理,其中的辛酸不足为外人道。很多员工暗地里说我是老板的监工,为了讨好老板不惜牺牲群众利益,真是比杜娥还冤。企业从40人变到200人,管理半径变大,价值观的冲突变多,没有统一的制度就会变成一盘散沙。可是我们的经理们凭感觉管理惯了,用制度管理别人不习惯,用制度约束自己不习惯,员工被制度管理更加不习惯,所以上下一心蔑视制度。

  我妈妈最小的一个弟弟,就是我的小舅,十八九岁的时候在外面混,经常惹事生非,三年之内被警察抓了9次,平均一年三次,然后我妈妈次次都把他成功地营救出来了。只要他一出事,我妈妈就会到处找关系(我认为她在那个城市简直有一个关系宝库),比如哪个的爱人是刑警队的,哪个的姐夫是公安局的,备好礼送过去,我那个混江湖的小舅就得意洋洋地出来了。所以我很小就有这样一个概念,办什么事都要找关系,有关系犯法了也不怕。

  前年我那个小舅被判了7年,出来后40岁,这辈子估计基本废掉了。我想就是他因为以前在我妈妈的包屁下,习惯性地蔑视国家法律制度。所以说,制度决定习惯,习惯决定性格,性格决定命运。

  五、政治敏感度太高

  我在公司跟员工谈话,结尾通常会说:“今天我跟你谈话的意思只是这个事情本身,没有别的意思”,听起来有点绕口。为什么要这么说?因为他们非常敏感。你说他哪些方面需要改进,他会联想到公司是否想炒他;你问他们部门的工作量是否饱和,他会联想到公司是否想炒他;你问他最近有没有继续进修的打算,他会联想到公司是否想炒他。他可能根本不在意你跟他谈话的内容,而是花很长时间来琢磨为什么要炒他。

  中国企业的内耗多,有个原因是说实话的成本太高。大家喜欢猜来猜去,相互间不信任,本来只是工作上的问题,非要上升到政治的高度,所以都不说实话。比如我对一个经理说“你处理这件事情有问题”,他可能会联想到我不喜欢他这个人,有意针对他。然后他会思考我为什么不喜欢他,是不是上次请客没有叫我?最后一定会找出一个理由来,于是误解就造成了。

  有个故事说,一个人去找邻居借斧头,可是他觉得邻居与他有些矛盾,不知道会不会借给他,所以边走边想,越想越气,最后跑到邻居的门口说:“你不用借斧头给我了!我才不会求你!”

  我就是一个典型的特“含蓄”的人,有事爱闷在心里不直接说,自以为这是顾及别人情绪,是一种修养,其实很误事。我曾经不喜欢我的一个下属到了极点,有段时间我每天都想炒掉他,而且这个想法象条毒蛇一样越缠越紧。但我强迫自己做了两件事:第一是站在他的角度来看我有什么问题;第二是坦诚地跟他交换意见。结果两人一摊开说,就那么点事,大家还有继续合作的机会,结果我们又共事到今天。

  所以我现在强迫自己说实话,说出来至少还有消除误解的机会,不说连机会都没有了。

  中国人的政治敏感度太高,多半是文革那会遗留下来的,再就是东方人特有的含蓄。不是说含蓄不好,非要学老外在大街上裸奔,但是含蓄得过了头,就显得有些小气和阴暗了。其实相互不信任会活得很累,自己累,别人也累。哪里有那么多的弦外之音?就事论事就完了。

  谈恋爱可以把简单的事情搞复杂一点,千转百回都行,办企业也这样,就会影响效率。中国人在企业里面,怕着怕那,提防心太强,往往把简单的事情搞复杂了。其实说穿了,人都很简单,都是吃五谷杂粮长大了,哪有那么可怕?都是你怕我,我怕你,相互间怕出来的。

  一个企业里面的政治气味太浓,跟老板也有关系。如果老板的控制欲太强,且以支配比他学历高的职业经理人为乐,那这个企业就极有可能成为清宫戏里的朝廷,明争暗斗,不亦乐乎。中国的民营企业搞着搞着就这样了,所以搞不长。

  没有一个环境是完全纯净的,发生政治行为也很正常,有人的地方就会有政治,但要控制在一个适当的程度。政治行为太泛滥了,就会损害诚信。

  六、犯“君子”错误

  这个世界上真正的坏人不多,就象真正的好人不多一样。但中国人很喜欢把“好人”与“坏人”这个本身就很模糊的道德标准去评判一个人的企业行为。公司要炒人,就会有员工说:“他人很好,公司为什么要炒掉他?”

  拜托,如果只有“坏人”才能被炒,请告诉我“坏人”在哪里?

  我从不认为我们公司的员工中有坏人,我只评判他是不是合格的企业人,如果他搞婚外情或者同性恋,那是他的价值观和性取向的问题,并不能以此判断他对公司的价值。如果对公司没有价值,雷锋我也不会要。

  我在公司的绩效考核制度中规定,每个部门每年必须有5%的员工被评为不合格,实际上我最初定的是10%,但后来所有的经理都反对,只好降低标准。即使是5%,经理们也不愿执行,他们对我说:“如果我的部门员工都合格,你一定要弄出个5%,怎么办?我只好安排员工轮流做庄了”。他们说得理直气壮,因为觉得自己是君子,对得起身边的兄弟们。

  我的回答是:“GE公司的淘汰率是20%,你认为我们公司的员工都比GE的员工优秀?”

  真正的错事10件中有9件是君子犯的,比如毛泽东与文革,斯大林与大萧反,小人并没有多少犯错的机会。中国人往往给“君子”一个错误的定义,然后用它来掩盖事实真相。如果一个经理在符合组织利益的前提下做“君子”,与员工讲情义,这绝对是一件好事,但如果是违背组织利益去对员工做人情,那么这个“君子”不仅毫无价值,简直形同犯罪。

  比如法律是最低的道德标准,但它是一条明确的线,你可以在这条线上做得更好,但你不能在线下。所以老外讲“法理情”,把法律摆在第一位,但并不是我们在中学课本中学到的“腐朽的资本主义社会里,只有赤裸裸的金钱关系,没有温情……”,他们只是先把人性定为“恶”,再用法律和制度来预防;中国人讲“情理法”,先把人性定为“善”,出了事再事后惩罚,结果法律没有遵守,人情味也越来越淡薄,医院可以看着病人死,行人可以站在大街上看着歹徒杀人,

  老外可以实行弹性的工作时间制,因为他们的员工主动性和自律性比咱们强,“领老板的薪水对老板负责”是基本的职业道德,就象在国外有的街道,红绿灯由司机自己按,因为遵守制度已经融入他们每个人的血脉中;要是在国内企业搞弹性工作时间,我相信90%的企业会死得很惨。中国的司机连红灯都敢闯,你叫他自己按红绿灯,他会一直按绿灯到自己不开车的那一天。

  国内企业为什么很难做好绩效考核,因为中国人喜欢做烂好人,不愿对别人作负面评价,所以绩效考核搞不下去。其实在当“君子”的背后,掩藏的本质是我们的经理人缺乏自信,害怕对下属作负面评价会引起下属反击而已。

  七、推卸责任

  我们公司的经理总抱怨老板不授权,权力太小,无法管理员工。可是遇到真正麻烦的时候,他们会把问题往老板那一交:“你看怎么办?”

  这些经理不会去想,他拿的薪水比员工多,权力比员工大,那么问题就应该到他为止,不然老板要你做经理干什么?可是他们总是把权力与责任分开,权力就是拿的钱多,管的人多,没想过其实权力和责任是对等的,你有多少权力,就要负起多少责任。

  在我们公司,人事和财务工作不好做,因为这两个部门代表公司行使职权,最容易被经理们“转手”责任。当你正常过问他们事务的时候,经理们会很反感,认为你触犯了他的一亩三分地,挑战了他的权力;可是一碰到员工要加薪、预算被削减这样的事情,他们就会说:“你加薪我是同意的,可是人事部不同意!”、“花这个钱我是同意的,可是财务部不同意!”。其实决定是我们跟他们一起下的,但出现问题的时候他们不去与员工沟通,把责任和矛盾推卸到我们头上。

  推卸责任的一个潜在心理意识是,看不见自己的问题。中国有句古训:“知天知地知彼易,知己难”,意思是人可以知道除自己以外的任何事情,就是不可自知,说得真好。所以我们公司搞培训的时候,大家群情激昂,有如醍醐灌顶,可是一回到工作中,该犯的错继续犯。因为培训那会老师讲的问题他全分析到别人头上去了,所以出了问题自然是别人的责任。

  破坏环境是中国企业最推卸责任的做法。企业以牺牲环境为代价得到1块钱的利润,也许我们后代用100块钱的代价也不能弥补。所以老外推行ISO14000(环境管理体系)认证,表面上是一种标准,其实就是企业对保护环境的一种承诺,是企业所应承担的社会责任感。我们的企业自己对社会推卸责任,怎么去要求员工对企业负起责任?

  八、缺乏包容性

  有句话说一个人的成就有多大,取决于他的胸怀有多大。做了人事经理后,我对这句话的感受尤为深切。

  我们公司有个部门经理,在公司创立初期为公司做了很大贡献,公司也一直努力想培养他。但他的心眼特别小,私心特别重,毫无包容精神,这是一个很要命的缺点。他几乎永远站在自己的立场去理解任何事情,比如,他认定他的上级(总监)不如他,但年终奖比他高,令他无法容忍,所以他经常跑到老板那去说上级的坏话。我跟他说,别人能做你的上级,肯定有他的长处,即使别人有问题,你也应该与他达成谅解和共识,原因很简单:你们是为一个目标工作,而且他是你的上级。可是一直到今天,他还在固执地寻找一切机会攻击他的上级。组织行为学里面有句话说“屁股决定大脑”,就是本位主义,他的大脑就完全被他的屁股(个人立场)控制了。

  我曾经跟老板开玩笑,评价他为“武功尽失,经脉全废”,意思是基本失去教育意义,无可救药。无论他的工作热情有多高,能力有多强,他不可能走到更高的管理岗位,这就是“性格决定命运”。我甚至断定他在生活中也不会取得成功,至少有一个论据可以证明:他33岁了,至今还没有女朋友。

  与自己不喜欢或不喜欢自己的人相处,是对胸怀的一个极大的考验。做大事的人的胸怀都是被反对者撑大的,就象李敖所说“男人的胸怀是被女人撑大的”一样。摩托罗拉的总裁高尔文喜欢驾船航海,万科的总裁王石喜欢登山,那都是练胸怀去了,人面对大海和高山的时候,心胸自然开阔,连心思都要透亮些。所以我总劝员工在工作之外多想想生活,多见见世面,多长长见识。老窝在办公室那点地方,做手头那点事情,怎么大气得起来?有点事就急了。

  我们搞计划生育,人口是控制住了,但另一方面,独生子会从小失去考验自己包容性的机会。人要在一个环境中才能碰到矛盾,而人一生中要不断地碰到矛盾,没有包容精神,一碰到不利自己的事情就跳,怎么跟别人合作?怎么解决矛盾?所以中国人缺乏团队精神,也和包容性有关。

  九、缺乏文化性

  把包容性再延展开来说,就是文化性。人类创造的文化包括科技文化和人文文化,它们分别发展着工具理性和价值理性,我这里说的是后一种。

  我曾经看到这样一个案例:一个中国人在一家国内的跨国公司工作,有一个到海外出任分公司CEO的机会,结果公司把机会给了一个他认为专业技能、学历背景都不如自己的老外。他去问老板,老板说:因为公司觉得那个老外有更高的人文修养和更开放的心态,而到一个不同的国家,面临不同的文化和价值观发生冲突的时候,需要他把各种文化和价值观糅合在一起,去实现公司的目标,这远比技能重要。

  这个案例给了我很深的启示。

  我始终认为,中国过了“五四”运动以后就基本没有文化了,到了文革就更加把以前的文化都丢了。其实中国的儒家文化有很多好的东西,结果我们没有发扬,却被新加坡发扬了,被韩国发扬了,最坏的是被小日本发扬了。

  也许中国人穷怕了,好不容易赶上改革开放,所以功利得有点过了头。我周围的很多职业经理人用各种证书、MBA学历把自己武装到牙齿,恨不得一个个都变成经济动物,谈起工作都是专家,就是不会与人相处。前几天我跟一个公司的同事聊天,他说大学毕业后6年时间里,他没有读过一本小说。

  中国人喜欢形式主义,以为发扬文化就是上硬件,比如搞几个艺术节,修几座古庙,找几个和尚念念经。人民到了放长假的时候在人山人海里遛一圈,就以为自己文化了。其实文化不是这些物化的东西,它是一种精神的力量,是以人为载体的。穷不是不要文化的借口,因为没有文化会更穷。中国的企业做不长,做不强,技术和管理是表象,真正的原因是缺乏企业家精神和企业文化。别人搞了一百多年市场经济和企业,那种文化传统和底蕴是一种气质,不是画个浓妆就学得会的。现在国内有些企业一进去要军训,要把企业编的文化手册倒背如流,那不是企业文化,是受迫性洗脑。

  跟中国的员工谈文化素养,谈人性关爱,他们多半以为你有病。他们会说,公司的氛围不好,沟通不通畅,执行力不强,但不会去想这是文化的原因。中国的企业家一有钱就忘本,就嚣张,要写书,要设论坛,要开名车,住豪宅,包二奶,骂警察,就是没想过回馈社会,也是缺乏文化性。

  学历和技能是衡量一个人的硬件标准,但真正决定一个人命运的是他的软件,是一种性格和态度,是文化。所以老外招聘员工的时候,强调“沟通能力”、“团队精神”、“心理承受能力”等这些东西,就是他们更注重一个人内在的素质,这才是决定个人价值的关键。

  结束语:文章既来自生活,又超越生活,大家不必以此文来质疑我作为一名人事经理的心态。既是网络,大家各取所需,不必望文生义。说到对本文所列问题的解决方案,我想,认识问题是解决问题的第一步,也许答案就隐藏在问题之中。最后,我以我非常喜欢的一段话结尾,与大家共勉:

  “谁都不是一座岛屿,自成一体;每个人都是那广袤大陆的一部分。如果海浪冲刷掉一个土块,欧洲就少了一点;如果一个海角,如果你朋友或你自己的庄园被冲掉,也是如此。任何人的死亡都使我受到损失,因为我包孕在人类之中。所以别去打听丧钟为谁而鸣,它为你敲响。”

  ——约翰.堂恩

2006年01月06日

  abstract class和interface是Java语言中对于抽象类定义进行支持的两种机制,正是由于这两种机制的存在,才赋予了Java强大的面向对象能力。abstract class和interface之间在对于抽象类定义的支持方面具有很大的相似性,甚至可以相互替换,因此很多开发者在进行抽象类定义时对于abstract class和interface的选择显得比较随意。其实,两者之间还是有很大的区别的,对于它们的选择甚至反映出对于问题领域本质的理解、对于设计意图的理解是否正确、合理。本文将对它们之间的区别进行一番剖析,试图给开发者提供一个在二者之间进行选择的依据。

理解抽象类

abstract class和interface在Java语言中都是用来进行抽象类(本文中的抽象类并非从abstract class翻译而来,它表示的是一个抽象体,而abstract class为Java语言中用于定义抽象类的一种方法,请读者注意区分)定义的,那么什么是抽象类,使用抽象类能为我们带来什么好处呢?

在面向对象的概念中,我们知道所有的对象都是通过类来描绘的,但是反过来却不是这样。并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。抽象类往往用来表征我们在对问题领域进行分析、设计中得出的抽象概念,是对一系列看上去不同,但是本质上相同的具体概念的抽象。比如:如果我们进行一个图形编辑软件的开发,就会发现问题领域存在着圆、三角形这样一些具体概念,它们是不同的,但是它们又都属于形状这样一个概念,形状这个概念在问题领域是不存在的,它就是一个抽象概念。正是因为抽象的概念在问题领域没有对应的具体概念,所以用以表征抽象概念的抽象类是不能够实例化的。

在面向对象领域,抽象类主要用来进行类型隐藏。我们可以构造出一个固定的一组行为的抽象描述,但是这组行为却能够有任意个可能的具体实现方式。这个抽象描述就是抽象类,而这一组任意个可能的具体实现则表现为所有可能的派生类。模块可以操作一个抽象体。由于模块依赖于一个固定的抽象体,因此它可以是不允许修改的;同时,通过从这个抽象体派生,也可扩展此模块的行为功能。熟悉OCP的读者一定知道,为了能够实现面向对象设计的一个最核心的原则OCP(Open-Closed Principle),抽象类是其中的关键所在。

从语法定义层面看abstract class和interface

在语法层面,Java语言对于abstract class和interface给出了不同的定义方式,下面以定义一个名为Demo的抽象类为例来说明这种不同。

使用abstract class的方式定义Demo抽象类的方式如下:

abstract class Demo {
abstract void method1();
abstract void method2();



使用interface的方式定义Demo抽象类的方式如下:

interface Demo {
void method1();
void method2();

}

在abstract class方式中,Demo可以有自己的数据成员,也可以有非abstarct的成员方法,而在interface方式的实现中,Demo只能够有静态的不能被修改的数据成员(也就是必须是static final的,不过在interface中一般不定义数据成员),所有的成员方法都是abstract的。从某种意义上说,interface是一种特殊形式的abstract class。

对于abstract class和interface在语法定义层面更多的细节问题,不是本文的重点,不再赘述,读者可以参阅参考文献〔1〕获得更多的相关内容。

从编程层面看abstract class和interface

从编程的角度来看,abstract class和interface都可以用来实现"design by contract"的思想。但是在具体的使用上面还是有一些区别的。

首先,abstract class在Java语言中表示的是一种继承关系,一个类只能使用一次继承关系。但是,一个类却可以实现多个interface。也许,这是Java语言的设计者在考虑Java对于多重继承的支持方面的一种折中考虑吧。

其次,在abstract class的定义中,我们可以赋予方法的默认行为。但是在interface的定义中,方法却不能拥有默认行为,为了绕过这个限制,必须使用委托,但是这会 增加一些复杂性,有时会造成很大的麻烦。

在抽象类中不能定义默认行为还存在另一个比较严重的问题,那就是可能会造成维护上的麻烦。因为如果后来想修改类的界面(一般通过abstract class或者interface来表示)以适应新的情况(比如,添加新的方法或者给已用的方法中添加新的参数)时,就会非常的麻烦,可能要花费很多的时间(对于派生类很多的情况,尤为如此)。但是如果界面是通过abstract class来实现的,那么可能就只需要修改定义在abstract class中的默认行为就可以了。

同样,如果不能在抽象类中定义默认行为,就会导致同样的方法实现出现在该抽象类的每一个派生类中,违反了"one rule,one place"原则,造成代码重复,同样不利于以后的维护。因此,在abstract class和interface间进行选择时要非常的小心。

从设计理念层面看abstract class和interface

上面主要从语法定义和编程的角度论述了abstract class和interface的区别,这些层面的区别是比较低层次的、非本质的。本小节将从另一个层面:abstract class和interface所反映出的设计理念,来分析一下二者的区别。作者认为,从这个层面进行分析才能理解二者概念的本质所在。

前面已经提到过,abstarct class在Java语言中体现了一种继承关系,要想使得继承关系合理,父类和派生类之间必须存在"is a"关系,即父类和派生类在概念本质上应该是相同的(参考文献〔3〕中有关于"is a"关系的大篇幅深入的论述,有兴趣的读者可以参考)。对于interface 来说则不然,并不要求interface的实现者和interface定义在概念本质上是一致的,仅仅是实现了interface定义的契约而已。为了使论述便于理解,下面将通过一个简单的实例进行说明。

考虑这样一个例子,假设在我们的问题领域中有一个关于Door的抽象概念,该Door具有执行两个动作open和close,此时我们可以通过abstract class或者interface来定义一个表示该抽象概念的类型,定义方式分别如下所示:

使用abstract class方式定义Door:

abstract class Door {
abstract void open();
abstract void close();
}


使用interface方式定义Door:

interface Door {
void open();
void close();
}


其他具体的Door类型可以extends使用abstract class方式定义的Door或者implements使用interface方式定义的Door。看起来好像使用abstract class和interface没有大的区别。

如果现在要求Door还要具有报警的功能。我们该如何设计针对该例子的类结构呢(在本例中,主要是为了展示abstract class和interface反映在设计理念上的区别,其他方面无关的问题都做了简化或者忽略)?下面将罗列出可能的解决方案,并从设计理念层面对这些不同的方案进行分析。

解决方案一:

简单的在Door的定义中增加一个alarm方法,如下:

abstract class Door {
abstract void open();
abstract void close();
abstract void alarm();
}


或者

interface Door {
void open();
void close();
void alarm();
}


那么具有报警功能的AlarmDoor的定义方式如下:

class AlarmDoor extends Door {
void open() { … }
void close() { … }
void alarm() { … }
}


或者

class AlarmDoor implements Door {
void open() { … }
void close() { … }
void alarm() { … }


这种方法违反了面向对象设计中的一个核心原则ISP(Interface Segregation Priciple),在Door的定义中把Door概念本身固有的行为方法和另外一个概念"报警器"的行为方法混在了一起。这样引起的一个问题是那些仅仅依赖于Door这个概念的模块会因为"报警器"这个概念的改变(比如:修改alarm方法的参数)而改变,反之依然。

解决方案二:

既然open、close和alarm属于两个不同的概念,根据ISP原则应该把它们分别定义在代表这两个概念的抽象类中。定义方式有:这两个概念都使用abstract class方式定义;两个概念都使用interface方式定义;一个概念使用abstract class方式定义,另一个概念使用interface方式定义。

显然,由于Java语言不支持多重继承,所以两个概念都使用abstract class方式定义是不可行的。后面两种方式都是可行的,但是对于它们的选择却反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理。我们一一来分析、说明。

如果两个概念都使用interface方式来定义,那么就反映出两个问题:1、我们可能没有理解清楚问题领域,AlarmDoor在概念本质上到底是Door还是报警器?2、如果我们对于问题领域的理解没有问题,比如:我们通过对于问题领域的分析发现AlarmDoor在概念本质上和Door是一致的,那么我们在实现时就没有能够正确的揭示我们的设计意图,因为在这两个概念的定义上(均使用interface方式定义)反映不出上述含义。

如果我们对于问题领域的理解是:AlarmDoor在概念本质上是Door,同时它有具有报警的功能。我们该如何来设计、实现来明确的反映出我们的意思呢?前面已经说过,abstract class在Java语言中表示一种继承关系,而继承关系在本质上是"is a"关系。所以对于Door这个概念,我们应该使用abstarct class方式来定义。另外,AlarmDoor又具有报警功能,说明它又能够完成报警概念中定义的行为,所以报警概念可以通过interface方式定义。如下所示:

abstract class Door {
abstract void open();
abstract void close();
}
interface Alarm {
void alarm();
}
class AlarmDoor extends Door implements Alarm {
void open() { … }
void close() { … }
void alarm() { … }
}


这种实现方式基本上能够明确的反映出我们对于问题领域的理解,正确的揭示我们的设计意图。其实abstract class表示的是"is a"关系,interface表示的是"like a"关系,大家在选择时可以作为一个依据,当然这是建立在对问题领域的理解上的,比如:如果我们认为AlarmDoor在概念本质上是报警器,同时又具有Door的功能,那么上述的定义方式就要反过来了。

结论

abstract class和interface是Java语言中的两种定义抽象类的方式,它们之间有很大的相似性。但是对于它们的选择却又往往反映出对于问题领域中的概念本质的理解、对于设计意图的反映是否正确、合理,因为它们表现了概念间的不同的关系(虽然都能够实现需求的功能)。这其实也是语言的一种的惯用法,希望读者朋友能够细细体会。




深入理解abstract class和interface
转自: Matrix-与Java共舞

2005年12月23日
form不可以嵌套,但是可以采取一些办法回避这一问题,如下例子你试试看
<form action="" method=post name=theform>
  <select name=myselect>
    <option value="0">0</option>
    :
    :
  </select>
<input type=text name=mytext>
<input type=checkbox name=mycheckbox>
<input type=button value=提交到页面1 onclick="javascript:act1()">
<input type=button value=提交到页面2 onclick="javascript:act2()">
<input type=button value=提交到页面2 onclick="javascript:act3()">
</form>
<script language=javascript>
function act1()
{
  theform.action="页面1.asp";
//如果想带参数,还可以像下面这样写
//  theform.action="页面1.asp?myvalue=10";
  :
//这里可以给form表单的元素赋值
  :
  theform.submit
}
function act1()
{
  theform.action="页面2.asp";
  :
//这里可以给form表单的元素赋值
  :
  theform.submit
}
function act1()
{
  theform.action="页面3.asp";
  :
//这里可以给form表单的元素赋值
  :
  theform.submit
}
</script>