內部類並不常用,而且使用起來有一定的定式,比如在下面的InnterDemoByTrhead.java里,我們通過內部類的形式創建線程。
1 public class InnerDemoByThread { 2 public static void main(String[] args) { 3 // 實現runnable接口,創建10個線程並啟動 4 for(int threadCnt = 0;threadCnt<10;threadCnt++) 5 new Thread(new Runnable() { 6 public void run() { 7 for (int i = 0; i < 5; i++) { 8 //在每個線程里,輸出0到4 System.out.println(Thread.currentThread().getName()+":"+ i); 9 } 10 } 11 }).start();//這里的括號是和第5行對應,注意需要帶分號 12 } 13 }
在上述的第4行里,我們通過for循環創建了10個線程,在第5行里,我們通過new Runnable定義了線程內部的動作,具體而言,在第6到第10行的代碼里,定義了打印0到4的動作。這里第5行通過new Thread定義的類,是在第1行定義的InnerDemoByThread類的內部,所以叫內部類,這也是內部類典型的用法。
雖然內部類出現的機會不多,但其中有個非常重要的知識點:當方法的參數需要被內部類使用時,那么這個參數必須是final,否則會報語法錯誤。我們在講線程的時候,通過內部類比較了線程安全和不安全集合的表現。這里我們通過改寫這個案例,着重看下“內部類“和“final“的要點,請大家看下如下的InnerFinalDemo.java代碼。
1 import java.util.ArrayList; 2 import java.util.List; 3 public class InnerFinalDemo { 4 public static int addByThreads(final List list) { 5 // 創建一個線程組 6 ThreadGroup group = new ThreadGroup("Group"); 7 // 通過內部類的方法來創建多線程 8 Runnable listAddTool = new Runnable() { 9 public void run() {// 在其中定義線程的主體代碼 10 list.add("0"); // 在集合里添加元素 11 } 12 }; 13 // 啟動10個線程,同時向集合里添加元素 14 for (int i = 0; i < 10; i++) { 15 new Thread(group, listAddTool).start(); 16 } 17 while (group.activeCount() > 0) { 18 try { Thread.sleep(10); } 19 catch (InterruptedException e) 20 { e.printStackTrace(); } 21 } 22 return list.size(); // 返回插入后的集合長度 23 } 24 public static void main(String[] args) { 25 List list = new ArrayList(); 26 //很大可能返回10 27 System.out.println(addByThreads(list)); 28 } 29 }
這段代碼的邏輯是,在main函數的第25行里,我們創建了一個線程不安全的ArrayList類型的對象,並在第27行調用了addByThreads方法返回list的長度。在addByThreads方法里,我們在第14行里,通過for循環啟動了10個線程,在這10個線程的主體邏輯(第9行的run方法)里,我們在第10行通過list.add方法給集合對象添加元素。
從功能上講,第27行的打印語句能輸出10,因為雖然ArrayList是線程不安全對象,但僅僅是10個線程同時操作,不足以發生“線程搶占”的情況。
但本代碼的重點是內部類和final,在代碼第3行定義的addByThreads方法里,我們注意到參數list前一定得加final,否則會報語法錯誤。我們可以通過如下的思維步驟來理解這個要點。
第一,第3行的這個帶final的list對象從屬於外部的InnerFinalDemo類,並且,在第8到12行的內部類里,也會用到這個對象,也就是說,在外部類和內部類里,都會用到這個對象。
第二,外部類和內部類是平行的,內部類並不從屬於外部類,這句話隱藏的含義是,外部類有可能在內部類之前被回收。
那么如果我們不加final,一旦外部類在內部類之前被回收,那么外部類里所包含的list對象也會被回收,但這時,內部類尚未使用這個list。在這種情況下,一旦內部類使用了list,就會報空指針錯(因為這個對象已經隨着外部類被回收了)。
為了避免這種錯誤,在指定語法時就加上了“當方法的參數需要被內部類使用時,那么這個參數必須是final”這個規定。一旦在此類參數前加final,那么這個參數就是常量了,存儲的位置就不是“堆區”了,而是“常量池”,這樣即使外部類被先回收,那么由於這類參數(比如list)不存在於外部類所從屬的堆空間(而是常量池),所以會繼續存在,這樣內部類就能繼續使用。
一些資深的面試官不會面試內部類的細節語法(因為不常用,而且使用起來有定式),而會考察上述的“參數和final”的知識點,所以大家在被問及”對內部類的掌握程度“這類問題時,可以按如下的思路來敘述。
第一,無需敘述內部類中各種語法,事實上,內部類涉及到“如何定義”以及“內部類中對象的可見性”等問題,語法相對而言比較復雜,說起來不容易,而且即使說清楚了,也無法很好體現大家的能力。
第二,可以直接說,“當方法的參數需要被內部類使用時,那么這個參數必須是final”,同時解釋下原因。當面試官聽到這以后,一般就不再問內部類問題了,因為他會認為,候選人連這么“資深”的知識也知道,那么就沒必要再細問內部類的問題了。
第三,由於已經引出“垃圾回收”的話題,所以大家可以找機會進一步按本章給出的提示,展示在這方面的能力,這樣就有很大可能得到“Java Core方面比較資深”的評價。
上述敘述是針對jdk1.7以及之前版本的,如果是針對jdk1.8版本,不需要顯式地加final,但依然會被當常量管理,具體來講,該對象的引用無法指向新的內存空間。