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. }

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月10日

package demo;

abstract class ModelA{
    abstract void decrease(int decraseCash);
}

class UserA extends ModelA{
    public int cash = 100;
   
    public void decrease(int decraseCash){
        this.cash = this.cash – decraseCash;
    }
   
}

class UserB extends ModelA{
    public int cash = 200;
   
    public void decrease(int decreaseCash){
        this.cash = this.cash – decreaseCash;
    }
}

class eOne{
    private java.util.Vector users = new java.util.Vector();
   
    public void addUser(ModelA a){
        this.users.add(a);
    }
   
    public void decrease(int number){
        java.util.Iterator i = this.users.iterator();
        while(i.hasNext()){
            ((ModelA)i.next()).decrease(number);
        }
    }
}

class Demo{
    public static void main(String args[]){
        UserA a1 = new UserA();
        UserA a2 = new UserA();
        UserB b1 = new UserB();
        UserB b2 = new UserB();
        eOne e = new eOne();
        e.addUser(a1);
        e.addUser(b1);
        e.decrease(50);
        System.out.println(a1.cash);
        System.out.println(a2.cash);
        System.out.println(b1.cash);
        System.out.println(b2.cash);
    }
}

F1 Help

ctrl + F2 结束程序

F3 查找下一个

shift + F3 反向查找下一个

ctrl + F 查找

ctrl + p 路径查找

ctrl + F4 运行到当前位置

F5 设置断点

F7 跟入

F8 单步

F9 运行

ctrl + F9 编译工程

shift + F9 调试模式运行

ctrl + shift + F9 编译当前类

ctrl + H 显示本类成员

ctrl + J 显示模板

ctrl + shift + c 自动完成错误捕捉代码

ctrl + Enter /mouseClick 当前关键字追踪

ctrl + shit + 数字(0-9) 设置/去除标签

ctrl + 数字(0-9) 返回标签位置

ctrl + alt + -> / <- 返回最近访问点

ctrl + -> / <- 光标跳过当前单位词

ctrl + shift + -> / <- 选择单位词

ctrl + 上下方向键 滚动屏幕

ctrl + home/end/pageup/pagedown

shift +盘方向键/home/end/pageup/pagedown 选择

ctrl + e 增量式查找

ctrl + w 捕捉离光标最近的单词

ctrl + shift + h 参数查找

ctrl + alt + space class insight

ctrl + F4 关闭当前类

ctrl + shift + F4 关闭(显示选择)

ctrl + B 切换窗体

ctrl + F6 切换窗体

ctrl + alt + p/c/m/z/s 视图开关

ctrl + g 到指定行

Tab 格式化宿进

ctrl + / 注释/去除注释选择行

ctrl + shift + I 宿进

ctrl + shift + U 反向宿进

2005年12月07日
Db Name: Microsoft SQL Server (6.5, 7, 2000 and 2005) and Sybase (10, 11, 12).
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: jTDS
Home Page: http://jtds.sourceforge.net/
JDBC Ver: 1.0.2
Download: http://sourceforge.net/project/showfiles.php?group_id=33291
Conn Code:
  Class.forName("net.sourceforge.jtds.jdbc.Driver ");
  Connection con = DriverManager.getConnection("jdbc:jtds:sqlserver://host:port/database","user","password");
or
  Connection con = DriverManager.getConnection("jdbc:jtds:sybase://host:port/database","user","password");

  Db Name: Microsoft SQL Server 2000
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: SQL Server 2000 Driver for JDBC
Home Page: http://www.microsoft.com/china/sql/
JDBC Ver: (SQL Server 2000 Driver for JDBC)
Download: http://www.microsoft.com/china/sql/downloads/2000/jdbc.asp
Conn Code:
  Class.forName("com.microsoft.jdbc.sqlserver.SQLServerDriver");
  Connection conn = DriverManager.getConnection ("jdbc:microsoft:sqlserver://server1:1433","user","password");

  Db Name: Oracle
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: Connector/J
Home Page: http://www.oracle.com/technology/software/tech/java/
JDBC Ver: (based on Oracle)
Download: http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/
Conn Code:
  Class.forName("oracle.jdbc.driver.OracleDriver");
  Connection con = DriverManager.getConnection("jdbc:oracle:thin:@host:port:databse","user","password");

  Db Name: MySQL
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: Connector/J
Home Page: http://dev.mysql.com/
JDBC Ver: 3.1
Download: http://dev.mysql.com/downloads/
Conn Code:
  Class.forName("com.mysql.jdbc.Driver");
  Connection con = DriverManager.getConnection("jdbc:mysql://host:port/database","user","password");

  Db Name: Sybase
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: jConnect
Home Page: http://www.sybase.com.cn/cn/content/SDN/
JDBC Ver: 4.5/5.5
Download: http://www.sybase.com/detail_list?id=13&morenavId=8668&multi=true&SR=Y&show=1265
Conn Code:
Class.forName("com.sybase.jdbc2.jdbc.SybDriver").newInstance();
DriverManager.getConnection("jdbc:sybase:Tds:IP:2638?ServiceName="+database,"user","password");

  Db Name: Postgresql
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: PostgreSQL JDBC
Home Page: http://jdbc.postgresql.org/
JDBC Ver: 8.0 Build 310
Download: http://jdbc.postgresql.org/download.html
Conn Code:
  Class.forName("org.postgresql.Driver");
  Connection con = DriverManager.getConnection("jdbc:postgresql://host:port/database","user","password");

  Db Name: IBM Informix
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: Informix JDBC Driver (IBM Informix JDBC V3.0)
Home Page: http://www-306.ibm.com/software/data/informix/
JDBC Ver: 3.0.JC1
Download: http://www14.software.ibm.com/webapp/download/search.jsp?go=y&rs=ifxjdbc
Conn Code:
  Class.forName("com.informix.jdbc.IfxDriver").newInstance();
  Connection conn= DriverManager.getConnection("jdbc:informix-sqli://123.45.67.89:1533/myDB:INFORMIXSERVER=myserver;user=testuser;password=testpassword";);

  Db Name: IBM DB2
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
JDBC Name: DB2 JDBC Universal Driver
Home Page: http://www-306.ibm.com/software/data/db2/udb/
JDBC Ver:
Download: http://www-128.ibm.com/developerworks/db2/downloads/jcc/
Conn Code:
  Class.forName("com.ibm.db2.jdbc.app.DB2Driver ").newInstance();
  Connection conn= DriverManager.getConnection("jdbc:db2://localhost:5000/sample",user,password);

IBM AS400主机在用的JDBC语法
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  有装V4R4以上版本的Client Access Express
  可以在C:\Program Files\IBM\Client Access\jt400\lib
  找到 driver 档案 jt400.zip,并更改扩展名成为 jt400.jar
Conn Code:
  java.sql.DriverManager.registerDriver (new com.ibm.as400.access.AS400JDBCDriver ());
  Class.forName("com.ibm.as400.access.AS400JDBCConnection");
  con = DriverManager.getConnection("jdbc:as400://IP","user","password");

  Db Name: Ms Access or Ms Foxpro
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Conn Code:
  step 1: 配置 odbc (例如:myDb)
  step 2:
  String dbURL = "jdbc:odbc:myDb";
  Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
 Connection conn = DriverManager.getConnection(dbURL);
or
  String dbURL = "jdbc:odbc:driver={MicrosoftAccessDriver(*.mdb)};DBQ=dabaseName.mdb";
  Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");
 Connection conn = DriverManager.getConnection(dbURL);
2005年11月22日

J2EE 面试题综合
/**
 * By metaphy 2005-11-12
 * Version: 0.01
 * 注:题目答案来源于metaphy过去的知识或网络,metaphy不能保证其正确或完整性,仅供参考
*/
[基础问答]
1.下面哪些类可以被继承?
java.lang.Thread (T)
java.lang.Number (T)
java.lang.Double (F)
java.lang.Math  (F)
java.lang.Void  (F)
java.lang.Class  (F)
java.lang.ClassLoader (T)

2.抽象类和接口的区别
(1)接口可以被多重implements,抽象类只能被单一extends
(2)接口只有定义,抽象类可以有定义和实现
(3)接口的字段定义默认为:public static final, 抽象类字段默认是"friendly"(本包可见)

3.Hashtable的原理,并说出HashMap与Hashtable的区别
HashTable的原理:通过节点的关键码确定节点的存储位置,即给定节点的关键码k,通过一定的函数关系H(散列函数),得到函数值H(k),将此值解释为该节点的存储地址.
HashMap 与Hashtable很相似,但HashMap 是非同步(unsynchronizded)和可以以null为关键码的.

4.forward和redirect的区别
forward: an internal transfer in servlet
redirect: 重定向,有2次request,第2次request将丢失第一次的attributs/parameters等

5.什么是Web容器?
实现J2EE规范中web协议的应用.该协议定义了web程序的运行时环境,包括:并发性,安全性,生命周期管理等等.

6.解释下面关于J2EE的名词
(1)JNDI:Java Naming & Directory Interface,JAVA命名目录服务.主要提供的功能是:提供一个目录系统,让其它各地的应用程序在其上面留下自己的索引,从而满足快速查找和定位分布式应用程序的功能.
(2)JMS:Java Message Service,JAVA消息服务.主要实现各个应用程序之间的通讯.包括点对点和广播.
(3)JTA:Java Transaction API,JAVA事务服务.提供各种分布式事务服务.应用程序只需调用其提供的接口即可.
(4)JAF: Java Action FrameWork,JAVA安全认证框架.提供一些安全控制方面的框架.让开发者通过各种部署和自定义实现自己的个性安全控制策略.
(5)RMI:Remote Method Interface,远程方法调用

7.EJB是基于哪些技术实现的?并说 出SessionBean和EntityBean的区别,StatefulBean和StatelessBean的区别.
EJB包括Session Bean、Entity Bean、Message Driven Bean,基于JNDI、RMI、JAT等技术实现.
SessionBean在J2EE应用程序中被用来完成一些服务器端的业务操作,例如访问数据库、调用其他EJB组件.EntityBean被用来代表应用系统中用到的数据.对于客户机,SessionBean是一种非持久性对象,它实现某些在服务器上运行的业务逻辑;EntityBean是一种持久性对象,它代表一个存储在持久性存储器中的实体的对象视图,或是一个由现有企业应用程序实现的实体.
Session Bean 还可以再细分为 Stateful Session Bean 与 Stateless Session Bean .这两种的 Session Bean都可以将系统逻辑放在 method之中执行,不同的是 Stateful Session Bean 可以记录呼叫者的状态,因此通常来说,一个使用者会有一个相对应的 Stateful Session Bean 的实体.Stateless Session Bean 虽然也是逻辑组件,但是他却不负责记录使用者状态,也就是说当使用者呼叫 Stateless Session Bean 的时候,EJB Container 并不会找寻特定的 Stateless Session Bean 的实体来执行这个 method.换言之,很可能数个使用者在执行某个 Stateless Session Bean 的 methods 时,会是同一个 Bean 的 Instance 在执行.从内存方面来看, Stateful Session Bean 与 Stateless Session Bean 比较, Stateful Session Bean 会消耗 J2EE Server 较多的内存,然而 Stateful Session Bean 的优势却在于他可以维持使用者的状态.

8.XML的解析方法
Sax,DOM,JDOM

9.什么是Web Service?
Web Service就是为了使原来各孤立的站点之间的信息能够相互通信、共享而提出的一种接口。
Web Service所使用的是Internet上统一、开放的标准,如HTTP、XML、SOAP(简单对象访问协议)、WSDL等,所以Web Service可以在任何支持这些标准的环境(Windows,Linux)中使用。
注:SOAP协议(Simple Object Access Protocal,简单对象访问协议),它是一个用于分散和分布式环境下网络信息交换的基于XML的通讯协议。在此协议下,软件组件或应用程序能够通过标准的HTTP协议进行通讯。它的设计目标就是简单性和扩展性,这有助于大量异构程序和平台之间的互操作性,从而使存在的应用程序能够被广泛的用户访问。

优势:
(1).跨平台;
(2).SOAP协议是基于XML和HTTP这些业界的标准的,得到了所有的重要公司的支持。
(3).由于使用了SOAP,数据是以ASCII文本的方式而非二进制传输,调试很方便;并且由于这样,它的数据容易通过防火墙,不需要防火墙为了程序而单独开一个“漏洞”。
(4).此外,WebService实现的技术难度要比CORBA和DCOM小得多。
(5).要实现B2B集成,EDI比较完善与比较复杂;而用WebService则可以低成本的实现,小公司也可以用上。
(6).在C/S的程序中,WebService可以实现网页无整体刷新的与服务器打交道并取数。
缺点:
(1).WebService使用了XML对数据封装,会造成大量的数据要在网络中传输。
(2).WebService规范没有规定任何与实现相关的细节,包括对象模型、编程语言,这一点,它不如CORBA。

10.多线程有几种实现方法,都是什么?同步有几种实现方法,都是什么?
答:多线程有两种实现方法,分别是继承Thread类与实现Runnable接口
同步的实现方面有两种,分别是synchronized,wait与notify

11.JSP中动态INCLUDE与静态INCLUDE的区别?
动态INCLUDE用jsp:include动作实现
<jsp:include page="included.jsp" flush="true"/>
它总是会检查所含文件中的变化,适合用于包含动态页面,并且可以带参数
静态INCLUDE用include伪码实现,定不会检查所含文件的变化,适用于包含静态页面
<%@ include file="included.htm" %>
 


[Java编程与程序运行结果]
1.Java编程,打印昨天的当前时刻
public class YesterdayCurrent{
  public void main(String[] args){
    Calendar cal = Calendar.getInstance();
    cal.add(Calendar.DATE, -1);
    System.out.println(cal.getTime());
  }
}

2.文件读写,实现一个计数器
  public int getNum(){
        int i = -1;
        try{
            String stri="";
            BufferedReader in = new BufferedReader(new FileReader(f));
            while((stri=in.readLine())!=null){
                i = Integer.parseInt(stri.trim());
            }
            in.close();
        }catch(Exception e){
            e.printStackTrace();
        }
        return i;
    }
    public void setNum(){
        int i = getNum();
        i++;       
        try{
            PrintWriter out=new PrintWriter(new BufferedWriter(new FileWriter(f,false))); 
            out.write(String.valueOf(i));            //可能是编码的原因,如果直接写入int的话,将出现java编码和windows编码的混乱,因此此处写入的是String
            out.close() ;
        }catch(Exception e){
            e.printStackTrace();
        }
    }
3. 指出下面程序的运行结果:
class A{
    static{
        System.out.print("1");
    }
    public A(){
        System.out.print("2");
    }
}
class B extends A{
    static{
        System.out.print("a");
    }
    public B(){
        System.out.print("b");
    }  
}
public class Hello{
    public static void main(String[] ars){
        A ab = new B(); //执行到此处,结果: 1a2b
 ab = new B(); //执行到此处,结果: 1a2bab
    }
}
注:类的static 代码段,可以看作是类首次加载(被虚拟机加载)执行的代码,而对于类的加载,首先要执行其基类的构造,再执行其本身的构造
4.写一个Singleton模式的例子
public class Singleton{
 private static Singleton single = new Singleton();
 private Singleton(){}
 public Singleton getInstance(){
  return single;
 }
}

[数据库]
1.删除表的重复记录
如果记录完全相同才算重复记录,那么:  (sql server2000下测试通过)
select distinct * into #tmpp from tid
delete from tid     
insert into tid select * from #tmpp
drop table #tmpp
如果有id主键(数字,自增1的那种),那么:(sql server2000下测试通过)
delete from tableA where id not in
(select id = min(id) from tableA group by name)

2.delete from tablea & truncate table tablea的区别
truncate 语句执行速度快,占资源少,并且只记录页删除的日志;
delete 对每条记录的删除均需要记录日志