1. 程式人生 > >幕後英雄的用武之地——淺談Java內部類的四個應用場景

幕後英雄的用武之地——淺談Java內部類的四個應用場景

                                                                    幕後英雄的用武之地                                                         ——淺談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; import java.util.Map; public abstract class ShapeFactory { protected abstract Shape create(); private static Map factories = new HashMap(); public static void addFactory(String id,ShapeFactory f) {        factories.put(id,f); } public static final Shape createShape(String id) {        if(!factories.containsKey(id))         {                try                {                       Class.forName("polyFactory."+id);                }                catch(ClassNotFoundException e)                {                       throw new RuntimeException("Bad shape creation : "+id);                }         }         return ((ShapeFactory)factories.get(id)).create(); } } 不錯,正是模板方法模式的運用。這個類蠻簡單的:首先是一個create()方法,用來產生具體類的物件,留交各具體工廠實現去實現。然後是一個Map型別的靜態變數,用來存放具體工廠的實現以及他們的ID號。接著的一個方法使用來增加一個具體工廠的實現。最後一個靜態方法是用來獲取具體物件,裡面的那個Class.forName……的作用是呼叫以ID號為類名的類的一些靜態的東西。 下面,我們來看具體的類的實現: package polyFactory; public class Circle implements Shape { public void draw() {         // TODO Auto-generated method stub        System.out.println("the circle is drawing..."); } public void erase() {         // TODO Auto-generated method stub        System.out.println("the circle is erasing..."); } private static class Factory extends ShapeFactory {        protected Shape create()         {                return new Circle();         } } static {ShapeFactory.addFactory("Circle",new Factory());} } 這個類的其他的地方也平常得很。但就是後面的那個內部類Factory用得好。第一呢,這個類只做一件事,就是產生一個Circle物件,與其他類無關,就這一個條也就滿足了使用內部類的條件。第二呢,這個Factory類需要是靜態的,這也得要求它被使用內部類,不然,下面的ShapeFacotry.addFactory就沒辦法add了。而最後的那個靜態的語句塊是用來將具體的工廠類新增到抽象的工廠裡面去。在抽象工廠裡呼叫Class.forName就會執行這個靜態的語句塊了。 下面仍然是一個具體類: package polyFactory; public class Square implements Shape { public void draw() {         // TODO Auto-generated method stub        System.out.println("the square is drawing..."); } public void erase() {         // TODO Auto-generated method stub        System.out.println("the square is erasing..."); } private static class Factory extends ShapeFactory {        protected Shape create()         {                return new Square();         } } static {ShapeFactory.addFactory("Square",new Factory());} } 最後,我們來測試一下: String[] ids = new String[]{"Circle","Square","Square","Circle"};         for(int i=0;i<ids.length;i++)         {                Shape shape = ShapeFactory.createShape(ids[i]);                shape.draw();                shape.erase();         } 測試結果為: the circle is drawing... the circle is erasing... the square is drawing... the square is erasing... the square is drawing... the square is erasing... the circle is drawing... the circle is erasing...        這個方法是巧妙地使用了內部類,將具體類的實現和它的具體工廠類繫結起來,由具體類的實現者在這個內部類的具體工廠裡去產生一個具體類的物件,這當然容易得多。雖然需要每一個具體類都建立一個具體工廠類,但由於具體工廠類是一個內部類,這樣也不會隨著具體類的增加而不斷增加新的工廠類,使得程式碼看起來很臃腫,這也是本方法不得不使用內部類的一個原因吧。