java基礎(八)——內部類

1.什麼是內部類?

        將一個類定義在另一個內的內部,也就是嵌套在另一個類裏面,這就是內部類(可以看下面的代碼,Inner類和StaticInner就是Outer的內部類)。其實也可以把內部類看做一個類的普通成員,類似成員變量,成員方法。


2.內部類長什麼樣子?怎麼定義?怎麼調用?

        內部類跟普通類一樣,只不過它是定義在另一個類的內部,可以用public, protected, default, private這幾個修飾符修飾(前面說了,可以把內部類當普通成員看待,那麼當然也能用這幾個修飾符了,可以參考另一篇文章java基礎(四)——訪問控制符)。看代碼

package com.xupk.inner.demo;


public class Outer {
	
	int i = 1;
	static int j = 10;
	
	//普通內部類可當做Outer類的普通成員
	class Inner{
	}
	
	//靜態內部類可當作Outer類的靜態類成員
	static class StaticInner{
		
	}
	
	//普通方法分別訪問兩個內部類,相當於普通方法訪問兩個類成員
	public void access(){
		i = 2;//普通變量
		j = 9;//靜態變量
		Inner i = new Inner();//對於普通內部類,可以直接訪問
		StaticInner s = new StaticInner();//普通方法肯定可以訪問到類成員
	}
	
	//靜態方法
	public static void main(String[] args) {
		Outer o = new Outer();
		
		int oi = o.i;//普通成員變量需要通過實例訪問
		Inner i2 = o.new Inner();//類的靜態方法訪問普通成員,必須通過類的實例來訪問
		/***
		 * 下面這語句會報錯:
		 * 沒有任何類型 Outer 的外層實例可訪問。必須用類型 Outer 的外層實例
		 * (例如,x.new A(),其中 x 是 Outer 的實例)來限定分配。
		 * Inner i = new Inner();//類的靜態方法不能直接訪問類的普通成員
		 */
		
		Outer.j = 8;//類成員可以直接訪問,不需要通過實例
		StaticInner i3 = new StaticInner();//類的靜態方法可以直接訪問類的靜態成員
	}

}

3.靜態內部類的加載順序是怎麼樣的?

        首先我們知道,類加載時有加載、驗證、準備、解析、初始化等階段;那麼初始化階段,就是按代碼出現的順序先執行類的靜態成員賦值語句和靜態代碼塊,然後再執行靜態方法。靜態內部類會在其外部類加載的時候也把它加載進去嗎?首先得了解一下,觸發類初始化的5個條件(可以參考:初探java虛擬機類加載機制(零)——概述),我們發現下面代碼中,並沒有觸發內部類的初始化,因爲它不滿足上面5個條件中的任意一個,所以,在外部類加載的時候,內部類並不會自己加載。看代碼示例:

package com.xupk.inner.demo;

public class ClzInitTest {
	
	private static int i = 10;
	static{
		System.out.println("=============outer static code start===================");
		System.out.println("outer class load……");
		System.out.println("i=:"+i);
		System.out.println("=============outer static code end===================");
	}
	
	
	//靜態內部類
	static class Inner{
		private static int i = 1;
		static{
			System.out.println("===================inner static code start================");
			System.out.println("inner class load……");
			System.out.println("i=:"+i);
			
		}
		
		static void init(){
			System.out.println("inner method load");
		}
		
		static{
			System.out.println("===================inner static code end==================");
		}
	}
	
	
	public static void main(String[] args) {
		//什麼也不做
	}

}
輸出如下:

=============outer static code start===================
outer class load……
i=:10
=============outer static code end===================
        可見,當外部類加載的時候,並沒有加載內部類。再回頭看看主動觸發類初始化的5個條件,我們調用一個靜態內部類的靜態方法,應該是能觸發內部類的初始化纔對。實驗一下,把上面的main方法改一下,看代碼:

public static void main(String[] args) {
		Inner.init();
	}
輸出如下:
=============outer static code start===================
outer class load……
i=:10
=============outer static code end===================
===================inner static code start================
inner class load……
i=:1
===================inner static code end==================
inner method load
        首先會加載外部類的靜態代碼塊,然後main方法調用了靜態內部類的靜態方法init(),所以會觸發Inner類的初始化,這時候初始化順序是跟普通類初始化順序一樣的:順序執行靜態代碼塊和靜態成員變量賦值語句,然後執行靜態方法。

        這裏有個典型的例子,就是單例模式的設計。JVM內部的機制能夠保證:當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,會觸發SingleTon的初始化,然後在該方法內訪問StaticClassLazy這個內部類的靜態變量instance時,會觸發這個內部類的初始化,所以JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢,這樣我們就能保證單例的安全實現。同時該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了低性能問題

package com.xupk.inner.demo;

public class SingleTon {
	
	//私有化構造方法
	private SingleTon(){
	}
	
	//利用類加載的線程是互斥的機制來保證加載這個內部類安全加載
	private static class StaticClassLazy{
		//類的靜態變量只加載一次來保證單例
		private static SingleTon instance = new SingleTon();
	}
	
	//外部獲取實例的方法
	public static SingleTon getInstance(){
		return StaticClassLazy.instance;
	}

}

4.在哪裏用到內部類?

        LinkedList裏面就使用了大量的內部類,比如Entry(jdk1.7後改爲Node了)。下面引用一篇文章來說明一下內部類的應用場景(轉自:http://blog.csdn.net/hivon/article/details/606312#insertcode)

場景一:當某個類除了它的外部類,不再被其他的類使用時
我們說這個內部類依附於它的外部類而存在,可能的原因有: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...
       這個方法是巧妙地使用了內部類,將具體類的實現和它的具體工廠類綁定起來,由具體類的實現者在這個內部類的具體工廠裏去產生一個具體類的對象,這當然容易得多。雖然需要每一個具體類都創建一個具體工廠類,但由於具體工廠類是一個內部類,這樣也不會隨着具體類的增加而不斷增加新的工廠類,使得代碼看起來很臃腫,這也是本方法不得不使用內部類的一個原因吧。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章