由于Java核心技術(shù)上的例子很不錯,本篇內(nèi)部類博客部分代碼以Java核心技術(shù)上的例子進行講解侧馅。
內(nèi)部類總覽
內(nèi)部類分為四種,分別是:
- 一般的內(nèi)部類(與方法在一級上的)
- 局部內(nèi)部類(在方法中的類)
- 匿名內(nèi)部類(沒有類名的類)
- 靜態(tài)內(nèi)部類(也稱為嵌套類)
一般的內(nèi)部類
下面是一個一般內(nèi)部類的代碼展示:
public class InnerClassTest {
public static void main(String[] args) {
TalkingClock clock = new TalkingClock(1000, true);
clock.start();
JOptionPane.showMessageDialog(null, "Quit Program?");
System.exit(0);
}
}
class TalkingClock {
private int interval;
private boolean beep;
public TalkingClock(int interval, boolean beep) {
this.interval = interval;
this.beep = beep;
}
public void start() {
ActionListener listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
//一般的內(nèi)部類
public class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
}
從上面我們可以看到在TimePrinter這個內(nèi)部類中的actionPerformed方法中引用了外部類TalkingClock的interval和beep域呐萌。所以說我們從這里可以看出一般的內(nèi)部類的一個特點就是可以訪問自身的數(shù)據(jù)域馁痴,也可以訪問創(chuàng)建它的外部類對象的數(shù)據(jù)域。
那么為了讓內(nèi)部類有這樣的特性肺孤,它是怎么做到的呢弥搞?其實,在內(nèi)部類中總有一個隱式的引用指向著創(chuàng)建它的外部類的對象
渠旁。就像下面的一樣:
光這么說可能你理解并不會太深刻,那么現(xiàn)在我們走向代碼里面去船逮。先來一張圖顾腊,再來一一解釋吧!
當然我是在生成字節(jié)碼的目錄下
挖胃,然后了解Linux的童鞋都知道ls是列出當然目錄下的可見的文件或者文件夾的命令
杂靶。然后這些字節(jié)碼有點多,但別去管其它的其它是一些無關(guān)緊要的類的字節(jié)碼酱鸭。我們直接看到TalkingClock.class和TalkingClock$TimePrinter.class吗垮,他們分別就是上面外部類和內(nèi)部類的字節(jié)碼啦!看到這是是不是感覺被騙了凹髓,emmm…內(nèi)部類其實就是編譯器的一個語法烁登,在編譯后的話沒有啥字節(jié)碼了,就是和泛型
蔚舀、foreach
等一樣的是一層編譯器語法糖饵沧。內(nèi)部類會被單獨的編譯出來锨络,然后通過$來連接外部類和內(nèi)部類成為內(nèi)部類的類名。
然后在后面我們通過javap -private <字節(jié)碼>
(這里我們看第一個javap得到的東西)用來顯示所有類和成員狼牺。我看的final TalkingClock this$0;
羡儿。this$0
這個變量是TalkingClock
類型的,沒錯是钥,其實這就是我們上面給出的圖中的outer那個變量掠归。這樣一切都明了了。之所以在內(nèi)部類中能夠去引用外部類的數(shù)據(jù)域就是因為我們的內(nèi)部類只用隱藏著一個外部類的引用悄泥。
然后虏冻,還想說的就是我們這樣一般的內(nèi)部類,可以是私有類码泞,但是常規(guī)類(也就是我們的外部類一樣的類)只能是包可見性或公有可見性兄旬。然后,也就是如果我們的內(nèi)部類對于其它類可見的時候余寥,我也可以來進行內(nèi)部類的創(chuàng)建领铐。但是得注意一點的是我們?nèi)绻谄渌愡M行一般內(nèi)部類的創(chuàng)建的時候,我們要借助于一個外部類對象進行創(chuàng)建
宋舷。也就是像下面一樣進行創(chuàng)建绪撵。
TalkingClock clock = new TalkingClock(1000,true);
TalkingClock.TimePrinter printer = clock.new TimePrinter();
雖然,基于Java 8
的Java核心技術(shù)書上在說內(nèi)部類中聲明的所有靜態(tài)域都必須是final以及可以聲明靜態(tài)方法祝蝠。但是筆者在Java 9上發(fā)現(xiàn)這樣的一般內(nèi)部類是不能進行靜態(tài)域或者是靜態(tài)方法的聲明的音诈,要聲明只能讓內(nèi)部類變成靜態(tài)內(nèi)部類才行。
通過前面的代碼我們可以發(fā)現(xiàn)一般的內(nèi)部類是能夠去訪問對應外部類的私有域的绎狭。它是怎么做到的呢税肪!我們看到第二個javap得到的內(nèi)容。我們發(fā)現(xiàn)其中有一個static boolean access$000(TalkingClock)
的方法遭顶。
我們在內(nèi)部類的用到了私有的beep芜飘,因此在外部類中這個自動生成的靜態(tài)方法來實現(xiàn)私有beep的訪問的。當然由于編譯器的不同方法名可能會不同蹦狂,如:access$0誓篱。
局部內(nèi)部類
為了后面好分析,先來看一波代碼吧:
public class InnerClassTest {
public static void main(String[] args) {
TalkingClock clock = new TalkingClock();
clock.start(1000, true);
JOptionPane.showMessageDialog(null, "Quit Program?");
System.exit(0);
}
}
class TalkingClock {
public void start(int interval,boolean beep) {
//局部內(nèi)部類
class TimePrinter implements ActionListener{
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
}
ActionListener listener = new TimePrinter();
Timer timer = new Timer(interval, listener);
timer.start();
}
}
從代碼我們可以看到凯楔,局部內(nèi)部類就是將內(nèi)部類放在了外部類的一個方法內(nèi)窜骄。然后得注意一點的就是局部內(nèi)部類不能夠用public或者是private訪問說明符進行聲明。它的作用于限定在聲明這個局部內(nèi)部類的塊中摆屯。因此這也是它的一個優(yōu)勢邻遏,那就是這個內(nèi)部類對外面的世界是完全隱藏的
在局部內(nèi)部類中實現(xiàn)外部類私有變量的訪問的方案是和上面一般的內(nèi)部類訪問私有變量是一樣的。但是如果一個局部內(nèi)部類對一個局部變量的訪問的話,那么這個局部內(nèi)部類中就會存儲一個局部變量的副本党远。就像下面一樣:
那么上面代碼的整個流程我們來解釋一下:
- 調(diào)用start方法
- 調(diào)用內(nèi)部類TimePrinter的構(gòu)造器削解,初始化listener變量
- 將listener傳給Timer構(gòu)造器,定時器開始計時沟娱,start方法結(jié)束氛驮。然后此時,start方法結(jié)束济似,beep參數(shù)不復存在
- 然后矫废,actionPerformed執(zhí)行if(beep)...
我們可以看到在start方法結(jié)束后,變量beep就不存在了砰蠢。
在Java 8之前
的話蓖扑,必須把局部內(nèi)部類定義為final的局部變量才行的。比如上面的start方法就會變成下面的樣子:
public void start(int interval,final boolean beep){
...
}
匿名內(nèi)部類
咱們依然先來看看代碼:
public class InnerClassTest {
public static void main(String[] args) {
TalkingClock clock = new TalkingClock();
clock.start(1000, true);
JOptionPane.showMessageDialog(null, "Quit Program?");
System.exit(0);
}
}
class TalkingClock {
public void start(int interval, boolean beep) {
//匿名內(nèi)部類
ActionListener listener = new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer timer = new Timer(interval, listener);
timer.start();
}
}
有上面的代碼我們可以看到台舱,匿名內(nèi)部類就是只用創(chuàng)建一個對象律杠,而不需要對其命名(就是沒有類名)的類。從形式上
來說就是一個構(gòu)造參數(shù)的閉小括號后面跟一個大括號竞惋,就是一個匿名內(nèi)部類柜去。其中大括號就是普通的類后面的大括號,里面可以寫自己的方法也可以重寫父類或者是接口中的方法拆宛。
由于構(gòu)造器的名字要和類名相同嗓奢,但是匿名內(nèi)部類沒有類名。所以浑厚,匿名類不能有構(gòu)造器股耽。
像上面ActionListener這樣只有一個方
法必須在實現(xiàn)它的類中進行重寫的接口
,我們叫做函數(shù)式接口
钳幅。實現(xiàn)這樣的接口的匿名內(nèi)部類物蝙,我們可以用lambada表達式來進行代替。就像下面這樣:
ActionListener listener = e -> {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
};
對于匿名內(nèi)部類敢艰,我們有一個雙括號初始化的技巧诬乞。比如像一個方法需要傳一個數(shù)組列表,但是這個數(shù)組列表無需再被用到盖矫,那么我們就可以用這個技巧。現(xiàn)在击奶,我們來看看代碼:
// 好久沒玩LOL辈双,邀請一波好友來玩下!
public class Test {
public static void main(String[] args) {
//沒有使用匿名內(nèi)部類
ArrayList<String> friends = new ArrayList();
friends.add("anriku");
friends.add("zzia");
friends.add("zxZhu");
inviteFriendToPlayLOL(friends);
//使用匿名內(nèi)部類柜砾,并用雙重括號初始化
inviteFriendToPlayLOL(new ArrayList<>() {{
add("Jay");
add("acemurder");
add("Mike");
}});
}
private static void inviteFriendToPlayLOL(List<String> friends) {
System.out.println(friends);
}
}
在大括號中的大括號湃望,叫做構(gòu)造塊
。構(gòu)造塊會在每一個對象構(gòu)造的時候進行調(diào)用。與靜態(tài)代碼塊不同的就是靜態(tài)代碼塊只會在一個類被加載的時候進行調(diào)用证芭。調(diào)用順序是這樣的:靜態(tài)塊>構(gòu)造塊>構(gòu)造方法
瞳浦。
靜態(tài)內(nèi)部類
如果我們的內(nèi)部類只是想完全的隱藏在一個類之中
,并不需要這個內(nèi)部類與外面的類打交道废士。那么我們可以將內(nèi)部類聲明為static(也只有內(nèi)部類能夠被static修飾)
叫潦,這樣的話內(nèi)部類會不會持有外部類的引用。這樣的話官硝,內(nèi)部類只能夠使用外部類的靜態(tài)變量或者是靜態(tài)方法了矗蕊。不能引用外部類的實例域或者是方法了。
現(xiàn)在我們來舉個栗子:
public class InnerClassTest {
public static void main(String[] args) {
double[] d = new double[20];
for (int i = 0;i < d.length;i++){
d[i] = 100*Math.random();
ArrayAlg.Pair p = ArrayAlg.minmax(d);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
}
class ArrayAlg{
//靜態(tài)內(nèi)部類
public static class Pair{
private double first;
private double second;
Pair(double first, double second) {
this.first = first;
this.second = second;
}
public double getFirst() {
return first;
}
public double getSecond() {
return second;
}
}
public static Pair minmax(double[] values){
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v:values){
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min,max);
}
}
我們?yōu)槭裁匆@樣寫呢氢架?其中的過程就是:
- 我們需要一個minmax用來將一個數(shù)組中的最大最小值比較出來傻咖。由于兩個方法的話就要進行兩次的遍歷比較。于是就想到用一個方法岖研,但是要同時返回最大和最小值卿操,那么我們就用Pair類來將兩個值連在一起。
- 但是如果單獨做一個類的話孙援,有兩個缺點:
- 以Pair為類名的類太多了
- 這個Pair類實際上只在ArrayAlg類中使用到
- 于是我們就讓其作為一個內(nèi)部類
- 再由于這個內(nèi)部類與外部沒有什么干系(也就是內(nèi)部類完全不需要訪問外圍對象)害淤,于是就讓其作為一個靜態(tài)內(nèi)部類
那么靜態(tài)內(nèi)部類是不是真的沒有去引用外部類對象呢,我們依然用javap命令去看一看:
從上面我們可以看到在這個內(nèi)部類中我們沒有看到之前外部類對象的引用赃磨。所以說這是真沒關(guān)系的筝家。
然后需要注意的一點就是,當外部類進行加載的時候邻辉,并且外部類沒有用到靜態(tài)內(nèi)部類的時候溪王,靜態(tài)內(nèi)部類是不會加載的。我們可以來看下測試代碼值骇。
//一個測試類莹菱,其中包括了一個靜態(tài)內(nèi)部類
public class StaticClassTest {
public static void test(){
System.out.println("Test");
}
static class InnerStaticClass{
static {
System.out.println("This is static block of InnerStaticClass");
}
}
}
public class Main{
public static void main(String[] args) {
StaticClassTest.test();
}
}
咱們來看一下代碼運行的結(jié)果:
沒錯吧,運行結(jié)果中沒有執(zhí)行靜態(tài)內(nèi)部類中的靜態(tài)代碼塊所需打印的東西吱瘩。
總結(jié)
今天道伟,我們學習了內(nèi)部類相關(guān)的東西。雖然還是有點復雜使碾,但是通過我們一步一步的分析蜜徽,我們揭開了其中神秘的東西。對其了解也是更上一層樓了吧票摇!
其中拘鞋,一般的內(nèi)部類
、局部內(nèi)部類
矢门、匿名內(nèi)部類
都會包含一個創(chuàng)建它的外部類的對象的引用盆色。但是靜態(tài)內(nèi)部類
不會持有這么一個變量灰蛙。
參考
- Java核心技術(shù) 卷一