幕后英雄的用武之地——浅谈Java内部类的四个应用场景
Java内部类是Java言语的一个很重要的概念,《Java编程思想》花了很大的篇幅来讲述这个概念。但是我们在实践中很少用到它,虽然我们在很多时候会被动的使用到它,但它仍然像一个幕后英雄一样,不为我们所知,不为我们所用。
本文不试图来讲述Java内部类的今生前世、来龙去脉,这些在网络上都已经汗牛充栋。如果读者想了解这些,可以在网络上搜索来学习。Java内部类总是躲在它的外部类里,像一个幕后英雄一样。但是幕后英雄也有用武之地,在很多时候,恰当的使用Java内部类能起到让人拍案叫绝的作用。本文试图谈一谈让这个幕后英雄也有用武之地的四个场景,希望引起大家对使用Java内部类的兴趣。
以下的文字,要求大家熟悉Java内部类的概念后来阅读。
场景一:当某个类除了它的外部类,不再被其他的类使用时
我们说这个内部类依附于它的外部类而存在,可能的原因有:1、不可能为其他的类使用;2、出于某种原因,不能被其他类引用,可能会引起错误。等等。这个场景是我们使用内部类比较多的一个场景。下面我们以一个大家熟悉的例子来说明。
在我们的企业级Java项目开发过程中,数据库连接池是一个我们经常要用到的概念。虽然在很多时候,我们都是用的第三方的数据库连接池,不需要我们亲自来做这个数据库连接池。但是,作为我们Java内部类使用的第一个场景,这个数据库连接池是一个很好的例子。为了简单起见,以下我们就来简单的模拟一下数据库连接池,在我们的例子中,我们只实现数据库连接池的一些简单的功能。如果想完全实现它,大家不妨自己试一试。
首先,我们定义一个接口,将数据库连接池的功能先定义出来,如下:
public interface Pool extends TimerListener
{
//初始化连接池
public boolean init();
//销毁连接池
public void destory();
//取得一个连接
public Connection getConn();
//还有一些其他的功能,这里不再列出
……
}
有了这个功能接口,我们就可以在它的基础上实现数据库连接池的部分功能了。我们首先想到这个数据库连接池类的操作对象应该是由Connection对象组成的一个数组,既然是数组,我们的池在取得Connection的时候,就要对数组元素进行遍历,看看Connection对象是否已经被使用,所以数组里每一个Connection对象都要有一个使用标志。我们再对连接池的功能进行分析,会发现每一个Connection对象还要一个上次访问时间和使用次数。
通过上面的分析,我们可以得出,连接池里的数组的元素应该是由对象组成,该对象的类可能如下:
public class PoolConn
{
private Connection conn;
private boolean isUse;
private long lastAccess;
private int useCount;
……
}
下面的省略号省掉的是关于四个属性的一些get和set方法。我们可以看到这个类的核心就是Connection,其他的一些属性都是Connection的一些标志。可以说这个类只有在连接池这个类里有用,其他地方用不到。这时候,我们就该考虑是不是可以把这个类作为一个内部类呢?而且我们把它作为一个内部类以后,可以把它定义成一个私有类,然后将它的属性公开,这样省掉了那些无谓的get和set方法。下面我们就试试看:
public class ConnectPool implements Pool
{
//存在Connection的数组
private PoolConn[] poolConns;
//连接池的最小连接数
private int min;
//连接池的最大连接数
private int max;
//一个连接的最大使用次数
private int maxUseCount;
//一个连接的最大空闲时间
private long maxTimeout;
//同一时间的Connection最大使用个数
private int maxConns;
//定时器
private Timer timer;
public boolean init()
{
try
{
……
this.poolConns = new PoolConn[this.min];
for(int i=0;i<this.min;i++)
{
PoolConn poolConn = new PoolConn();
poolConn.conn = ConnectionManager.getConnection();
poolConn.isUse = false;
poolConn.lastAccess = new Date().getTime();
poolConn.useCount = 0;
this.poolConns[i] = poolConn;
}
……
return true;
}
catch(Exception e)
{
return false;
}
}
……
private class PoolConn
{
public Connection conn;
public boolean isUse;
public long lastAccess;
public int useCount;
}
}
因为本文不是专题来讲述数据库连接池的,所以在上面的代码中绝大部分的内容被省略掉了。PoolConn类不大可能被除了ConnectionPool类的其他类使用到,把它作为ConnectionPool的私有内部类不会影响到其他类。同时,我们可以看到,使用了内部类,使得我们可以将该内部类的数据公开,ConnectionPool类可以直接操作PoolConn类的数据成员,避免了因set和get方法带来的麻烦。
上面的一个例子,是使用内部类使得你的代码得到简化和方便。还有些情况下,你可能要避免你的类被除了它的外部类以外的类使用到,这时候你却不得不使用内部类来解决问题。
场景二:解决一些非面向对象的语句块
这些语句块包括if…else if…else语句,case语句,等等。这些语句都不是面向对象的,给我们造成了系统的扩展上的麻烦。我们可以看看,在模式中,有多少模式是用来解决由if语句带来的扩展性的问题。
Java编程中还有一个困扰我们的问题,那就是try…catch…问题,特别是在JDBC编程过程中。请看下面的代码:
……
try
{
String[] divisionData = null;
conn = manager.getInstance().getConnection();
stmt = (OracleCallableStatement)conn.prepareCall("{ Call PM_GET_PRODUCT.HEADER_DIVISION(?, ?) }");
stmt.setLong(1 ,productId.longValue() );
stmt.registerOutParameter(2, oracle.jdbc.OracleTypes.CURSOR); ;
stmt.execute();
ResultSet rs = stmt.getCursor(2);
int i = 0 ;
String strDivision = "";
while( rs.next() )
{
strDivision += rs.getString("DIVISION_ID") + "," ;
}
int length = strDivision.length() ;
if(length != 0 )
{
strDivision = strDivision.substring(0,length - 1);
}
divisionData = StringUtil.split(strDivision, ",") ;
map.put("Division", strDivision ) ;
LoggerAgent.debug("GetHeaderProcess","getDivisionData","getValue + " + strDivision +" " + productId) ;
}catch(Exception e)
{
LoggerAgent.error("GetHeaderData", "getDivisionData",
"SQLException: " + e);
e.printStackTrace() ;
}finally
{
manager.close(stmt);
manager.releaseConnection(conn);
}
这是我们最最常用的一个JDBC编程的代码示例。一个系统有很多这样的查询方法,这段代码一般分作三段:try关键字括起来的那段是用来做查询操作的,catch关键字括起来的那段需要做两件事,记录出错的原因和事务回滚(如果需要的话),finally关键字括起来的那段用来释放数据库连接。
我们的烦恼是:try关键字括起来的那段是变化的,每个方法的一般都不一样。而catch和finally关键字括起来的那两段却一般都是不变的,每个方法的那两段都是一样的。既然后面那两段是一样的,我们就非常希望将它们提取出来,做一个单独的方法,然后让每一个使用到它们的方法调用。但是,try…catch…finally…是一个完整的语句段,不能把它们分开。这样的结果,使得我们不得不在每一个数据层方法里重复的写相同的catch…finally…这两段语句。
既然不能将那些讨厌的try…catch…finally…作为一个公用方法提出去,那么我们还是需要想其他的办法来解决这个问题。不然我们老是写那么重复代码,真是既繁琐,又不容易维护。
我们容易想到,既然catch…finally…这两段代码不能提出来,那么我们能不能将try…里面的代码提出去呢?唉哟,try…里面的代码是可变的呢。怎么办?
既然try…里面的代码是可变的,这意味着这些代码是可扩展的,是应该由用户来实现的,对于这样的可扩展内容,我们很容易想到用接口来定义它们,然后由用户去实现。这样以来我们首先定义一个接口:
public interface DataManager
{
public void manageData();
}
我们需要用户在manageData()方法中实现他们对数据层访问的代码,也就是try…里面的代码。
然后我们使用一个模板类来实现所有的try…catch…finally…语句的功能,如下:
public class DataTemplate
{
public void execute(DataManager dm)
{
try
{
dm.manageData();
}
catch(Exception e)
{
LoggerAgent.error("GetHeaderData", "getDivisionData","SQLException: " + e);
e.printStackTrace() ;
}
finally
{
manager.close(stmt);
manager.releaseConnection(conn);
}
}
}
这样,一个模板类就完成了。我们也通过这个模板类将catch…finally…两段代码提出来了。我们来看看使用了这个模板类的数据层方法是怎么实现的:
new DataTemplate().execute(new DataManager()
{
public void manageData()
{
String[] divisionData = null;
conn = manager.getInstance().getConnection();
stmt = (OracleCallableStatement)conn.prepareCall("{ Call PM_GET_PRODUCT.HEADER_DIVISION(?, ?) }");
stmt.setLong(1 ,productId.longValue() );
stmt.registerOutParameter(2, oracle.jdbc.OracleTypes.CURSOR); ;
stmt.execute();
ResultSet rs = stmt.getCursor(2);
int i = 0 ;
String strDivision = "";
while( rs.next() )
{
strDivision += rs.getString("DIVISION_ID") + "," ;
}
int length = strDivision.length() ;
if(length != 0 )
{
strDivision = strDivision.substring(0,length - 1);
}
divisionData = StringUtil.split(strDivision, ",") ;
map.put("Division", strDivision ) ;
LoggerAgent.debug("GetHeaderProcess","getDivisionData","getValue + " + strDivision +" " + productId) ;
}
});
注意:本段代码仅供思路上的参考,没有经过上机测试。
我们可以看到,正是这个实现了DataManager接口的匿名内部类的使用,才使得我们解决了对try…catch…finally…语句的改造。这样,第一为我们解决了令人痛苦的重复代码;第二也让我们在数据层方法的编码中,直接关注对数据的操作,不用关心那些必需的但是与数据操作无关的东西。
我们现在来回想一下Spring框架的数据层,是不是正是使用了这种方法呢?
场景之三:一些多算法场合
假如我们有这样一个需求:我们的一个方法用来对数组排序并且依次打印各元素,对数组排序方法有很多种,用哪种方法排序交给用户自己确定。
对于这样一个需求,我们很容易解决。我们决定给那些排序算法定义一个接口,具体的算法实现由用户自己完成,只要求他实现我们的接口就行。
public interface SortAlgor
{
public void sort(int[] is);
}
这样,我们再在方法里实现先排序后打印,代码如下:
public void printSortedArray(int[] is, SortAlgor sa)
{
……
sa.sort(is);
for(int i=0;i<is.length;i++)
{
System.out.print(is[i]+” “);
}
System.out.println();
}
客户端对上面方法的使用如下:
int[] is = new int[]{3,1,4,9,2};
printSortedArray(is, new SortAlgor()
{
public void sort(is)
{
int k = 0;
for(int i=0;i<is.length;i++)
{
for(int j=i+1;j<is.length;j++)
{
if(is[i]>is[j])
{
k = is[i];
is[i] = is[j];
is[j] = k;
}
}
}
}
});
这样的用法很多,我们都或多或少的被动的使用过。如在Swing编程中,我们经常需要对组件增加监听器对象,如下所示:
spinner2.addChangeListener(new ChangeListener()
{
public void stateChanged(ChangeEvent e)
{
System.out.println("Source: " + e.getSource());
}
}
);
在Arrays包里,对元素为对象的数组的排序:
Arrays.sort(emps, new Comparator(){
Public int compare(Object o1,Object o2)
{
return ((Employee)o1).getServedYears()-((Employee)o2).getServedYears();
}
});
这样的例子还有很多,JDK教会了我们很多使用内部类的方法。随时我们都可以看一看API,看看还会在什么地方使用到内部类呢?
场景之四:适当使用内部类,使得代码更加灵活和富有扩展性
适当的使用内部类,可以使得你的代码更加灵活和富有扩展性。当然,在这里头起作用的还是一些模式的运行,但如果不配以内部类的使用,这些方法的使用效果就差远了。不信?请看下面的例子:
我们记得简单工厂模式的作用就是将客户对各个对象的依赖转移到了工厂类里。很显然,简单工厂模式并没有消除那些依赖,只是简单的将它们转移到了工厂类里。如果有新的对象增加进来,则我们需要修改工厂类。所以我们需要对工厂类做进一步的改造,进一步消除它对具体类的依赖。以前我们提供过一个使用反射来消除依赖的方法;这里,我们将提供另外一种方法。
这种方法是将工厂进一步抽象,而将具体的工厂类交由具体类的创建者来实现,这样,工厂类和具体类的依赖的问题就得到了解决。而工厂的使用者则调用抽象的工厂来获得具体类的对象。如下。
我们以一个生产形体的工厂为例,下面是这些形体的接口:
package polyFactory;
public interface Shape
{
public void draw();
public void erase();
}
通过上面的描述,大家都可能已经猜到,这个抽象的工厂肯定使用的是模板方法模式。如下:
package polyFactory;
import java.util.HashMap;
im