注意注意!!!前排提示!!!本篇文章過長,最好收藏下來慢慢看,如果你之前對內部類不是很熟悉,一次性看完,大概你會懵逼。。。
1. 內部類概述
一個類的定義放在另一個類的內部,這個類就叫做內部類。內部類是一種非常有用的特性,因為它允許你把一些邏輯相關的類組織在一起。
內部類大體上可以分為四種:
成員內部類,靜態內部類,局部內部類,匿名內部類
我們先來詳細的看一下這四種內部類。
2. 成員內部類
成員的內部類,就是最基礎的內部類,沒有那些花里胡哨的修飾:
//外部類
public class Outer {
private String a = "a";
public int i = 1;
//內部類
class Inner{
private String b = "b";
public String c = "c";
public int getInt(){
return i; // 內部類可以訪問外部類變量
}
private String getString(){
return a + b + c; // 內部類可以訪問外部類的private變量
}
}
public String getParam(){
Inner inner = new Inner();
inner.b = "bb"; // 外部類可以訪問內部類的private變量
inner.c = "cc";
return inner.getInt() + inner.getString();
}
}
//測試類
class Test {
public static void main(String[] args) {
Outer outer = new Outer();
System.out.println(outer.getParam()); // 輸出:1abbcc
Outer.Inner oi = outer.new Inner();
oi.c = "ccc";
//oi.b = "bbb"; 編譯失敗
System.out.println(oi.getInt()); // 輸出:1
//System.out.println(oi.getString()); 編譯失敗
}
}
從這段代碼中,我們總結一下普通內部類的要點:
- 內部類可以訪問外部類變量,包括私有變量
- 在外部類中使用內部類的方法需要new一個內部類的對象。
- 在外部類中可以訪問到內部類的任何變量,包括私有變量。
- 在其他類中創建內部類對象需要使用這樣的形式:
OuterClassName.InnerClassName name = new OuterClassName().new InnerClassName()。
- 在其他類中定義的內部類對象不能訪問內部類中的私有變量。
當然,除了以上知識點以外,在內部類中,可以通過【.this】訪問到外部類對象。
public class Outer {
private int num ;
public Outer(){}
public Outer(int num){
this.num = num;
}
private class Inner{
public Outer getTest2(){
return Outer.this; // Outer.this指的是外部類的對象
}
public Outer newTest2(){
return new Outer();
}
}
public static void main(String [] args){
Outer test = new Outer(5);
Outer.Inner inner = test.new Inner();
Outer o1 = inner.getTest2();
Outer o2 = inner.newTest2();
System.out.println(o1.num); // 5
System.out.println(o2.num); // 0
}
}
注意通過.this得到的對象,和通過new出來的對象的區別。使用.this后,得到時創建該內部類時使用的外圍類對象的引用,new則是創建了一個新的引用。
到這里了我們需要明確一點,內部類是個編譯時的概念,一旦編譯成功后,它就與外圍類屬于兩個完全不同的類(當然他們之間還是有聯系的)。對于一個名為OuterClass的外圍類和一個名為InnerClass的內部類,在編譯成功后,會出現這樣兩個class文件:OuterClass.class和OuterClass$InnerClass.class。
3. 內部類與向上轉型
到目前為止,你可能覺得內部類不過如此,沒什么新奇的地方,畢竟如果只是為了隱藏一個類,java本身已經有了很好的隱藏機制------只給某個類包訪問權限,而用不著把類創建為內部類。
但是,當一個內部類向上轉型為其基類,尤其是轉型為一個接口的時候,內部類就有了用武之地。這是因為這樣的內部類(某個接口的實現類)對于其他人來說完全不可見,并且不可用。所得到的只是指向基類或者接口的引用,所以能很方便的隱藏實現細節。
我們來看一段代碼:
//定義兩個接口
public interface Run {
void run();
}
public interface Eat {
void eat();
}
//外部類
public class Person {
//這里是private
private class PEat implements Eat {
@Override
public void eat() {
System.out.println("eat with mouse");
}
}
//這里是protected
protected class PRun implements Run{
@Override
public void run() {
System.out.println("run with leg");
}
}
public Eat howToEat(){
return new PEat(); //向上轉型
}
public Run houToRun(){
return new PRun(); //向上轉型
}
}
//測試類
class TestPerson{
public static void main(String[] args) {
Person p = new Person();
Eat e = p.howToEat();
Run r = p.houToRun();
e.eat();
r.run();
Person.PRun ppr = p.new PRun();
//Person.PEat ppe = p.new PEat(); 編譯失敗,因為PEat是private的
}
}
從這段代碼可以看出,PEat是private,所以除了Person(它的外部類),沒有人能訪問到它。PRun是protected,所以只有Person及其子類、還有與Person同一個包中的類能訪問PRun,其他類不能訪問。
這意味著,如果客戶端程序員想要了解或者訪問這些成員是要受到限制的。除此之外,private內部類也不可以被向下轉型,因為不能訪問他的名字。
所以,private內部類給類的設計者提供了一種途徑,通過這樣的方式可以完全阻止任何依賴于類的編碼,并且完全隱藏了實現的細節。此外,從客戶端程序員的角度看,由于不能訪問任何新增加的、原本不屬于公共接口的方法,所以擴展接口是沒有價值的。這也個java編譯器提供了更高效代碼的機會。
所以說,一般成員內部類,都會定義為private的。
普通的類(非內部類),不能聲明為private或protected,它們之恩給你被賦予public或者包訪問權限。
4. 靜態內部類(嵌套類)
如果不需要內部類對象與其外圍類對象之間又聯系,那么可以將內部類聲明為static。這就是靜態內部類,也被稱為嵌套類。
普通的內部類對象隱式的保存了一個指向它的外部類引用的變量,所以它可以無條件的使用外部類的變量。但是當內部類yogastatic修飾的時候,就不會有這個變量了。這意味著:
- 要創建嵌套類的對象,并不需要其外圍類的對象。
- 靜態內部類中不能訪問非靜態的外部類變量,但是尅訪問外部類的靜態變量。
除此之外,由于普通內部類的字段與方法,只能放在類的外部層次上,所以普通的內部類不能有static方法和static變量,也不能在普通內部類中再包含靜態內部類。但是靜態內部類可以包含所有這些東西:
public class Outer {
private int i = 1;
public static String str = "str";
static class StaClass implements inter{
private String s = "s";
static int j = 2;
static int getInt(){
//return i + j;
return j;
}
private String getString(){
return str + s;
}
@Override
public void inter() {
System.out.println("inter");
}
static class InStaClass{
int x = 4;
static int y = 5;
static int getInt(){
//return x; // x是非靜態變量 不可以在靜態方法中使用
return y;
}
}
}
public inter getInter(){
return new StaClass();
}
}
class Test{
public static void main(String[] args) {
int a = Outer.StaClass.getInt();
//Outer.StaClass.getString(); // getString()為非靜態方法,不能這樣調用
int b = Outer.StaClass.InStaClass.getInt();
System.out.println(a + "----" + b); // 輸出 2----5
//new Outer().new StaClass(); 編譯失敗 StaClass是靜態的
new Outer().getInter().inter(); // 輸出 inter
}
}
通過這段代碼,我們總結一下靜態內部類的要點:
- 在靜態內部類中可以存在靜態成員
- 靜態內部類只能訪問外圍類的靜態成員變量和方法,不能訪問外圍類的非靜態成員變量和方法
- 靜態內部類中的靜態方法可以通過【外部類.內部類.方法名】直接調用
- 靜態內部類在其他類中不能new出來。(new Outer().new StaClass()這樣是不行的)
- 但是在外部類中,可以new一個靜態內部類的對象。
- 靜態內部類中不能使用【.this】
5. 局部內部類
在一個方法里或者任意作用域里定義的內部類叫做局部內部類。
為什么要這么做呢?
如前所示,你實現了某類型的接口,于是你可以創建并返回對其的引用,你需要這樣的引用。
你要解決一個復雜的問題,想創建一個類來輔助你的解決方案,但是又不希望這個類是公共可用的。
聽起來有點費解,往下看你就明白了。
5.1 一個定義在方法中的類
public class Person {
public Eat howToEat(){
// 定義在方法中的類
class EatWithMouth implements Eat{
@Override
public void eat() {
System.out.println("eat with mouth");
}
}
// 向上轉型
return new EatWithMouth();
}
public static void main(String[] args) {
Eat e = new Person().howToEat();
e.eat(); // eat with mouth
}
}
EatWithMouth是方法howToEat中的類而不是Person中的類。你甚至可以在同一個子目錄下的任意一個類中給任意一個內部類起EatWithMouth這個名字,而不會有命名沖突。所以,在howToEat方法外的任何地方都不能訪問到EatWithMouth類。
當然,這并不意味著一旦howToEat方法執行完畢,EatWithMouth類就不能用了。
5.2 在任意作用域嵌入一個內部類
可以在任意作用域中嵌入內部類:
public class EveryBlock {
private String test(boolean b){
if (b){
class A{
private String a = "a";
String getString(){
return a;
}
}
A a = new A();
String s = a.getString();
return s;
}
//A a = new A(); 編譯失敗 超出作用域
return null;
}
public static void main(String[] args) {
EveryBlock eb = new EveryBlock();
System.out.println(eb.test(true)); // a
}
}
雖然類A在if語句中,但是這并不表明類A的創建時有條件的,它其實是和別的類一起編譯的。但是它在它定義的作用域之外的不可用的,除此之外與普通內部類一樣。
通過這樣的方式,就解決了上面提到的第二個問題:不希望這個類是公用的。
6. 匿名內部類
匿名內部類應該是使用的最多的了,尤其是在swing中。
先看一個例子:
public class OuterClass {
public InnerClass getInnerClass(final int num,String str2){
return new InnerClass(){
int number = num + 3;
public int getNumber(){
return number;
}
};//注意:分號不能省
}
public static void main(String[] args) {
OuterClass out = new OuterClass();
InnerClass inner = out.getInnerClass(2, "chengfan");
System.out.println(inner.getNumber());
}
}
interface InnerClass {
int getNumber();
}
這段代碼里有一段很奇怪的東西:
return new InnerClass(){
int number = num + 3;
public int getNumber(){
return number;
}
};
沒錯,就是它。InnerClass不是一個借口么,怎么還能new呢?聰明的你一定知道,這就是匿名內部類,事實上,這段代碼和下面的寫法是等價的:
public class OuterCla {
class InnerClassImpl implements InnerClass{
int number ;
public InnerClassImpl(int num){
number = num + 3;
}
public int getNumber(){
return number;
}
}
public InnerClass getInnerClass(final int num){
return new InnerClassImpl(2);
}
public static void main(String[] args) {
OuterCla out = new OuterCla();
InnerClass inner = out.getInnerClass(2);
System.out.println(inner.getNumber());
}
}
這段代碼你應該懂了。將兩段代碼一比較,你大概也清楚了,上面那樣寫,意思是創建了一個實現了InnerClass的匿名類的對象。
匿名類可以創建接口、抽象類、與普通類的對象。創建接口和抽象類時,必須實現接口中所有方法。 創建匿名類時,可以是無參的,也可以有參數的,但是如果這個參數要在匿名類中使用,參數必須是final的,如果不使用,可以不被final修飾(代碼中有體現)。
6.1 為什么必須是final呢?
首先我們知道在內部類編譯成功后,它會產生一個class文件,該class文件與外部類并不是同一class文件,僅僅只保留對外部類的引用。當外部類傳入的參數需要被內部類調用時,從java程序的角度來看是直接被調用:
public class OuterClass {
public void display(final String name,String age){
class InnerClass{
void display(){
System.out.println(name);
}
}
}
}
從上面代碼中看好像name參數應該是被內部類直接調用?其實不然,在java編譯之后實際的操作如下:
public class OuterClass$InnerClass {
public InnerClass(String name,String age){
this.InnerClass$name = name;
this.InnerClass$age = age;
}
public void display(){
System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );
}
}
所以從上面代碼來看,內部類并不是直接調用方法傳遞的參數,而是利用自身的構造器對傳入的參數進行備份,自己內部方法調用的實際上時自己的屬性而不是外部方法傳遞進來的參數。
直到這里還沒有解釋為什么是final?在內部類中的屬性和外部方法的參數兩者從外表上看是同一個東西,但實際上卻不是,所以他們兩者是可以任意變化的,也就是說在內部類中我對屬性的改變并不會影響到外部的形參,而然這從程序員的角度來看這是不可行的,畢竟站在程序的角度來看這兩個根本就是同一個,如果內部類該變了,而外部方法的形參卻沒有改變這是難以理解和不可接受的,所以為了保持參數的一致性,就規定使用final來避免形參的不改變。
簡單理解就是,拷貝引用,為了避免引用值發生改變,例如被外部類的方法修改等,而導致內部類得到的值不一致,于是用final來讓該引用不可改變。
故如果定義了一個匿名內部類,并且希望它使用一個其外部定義的參數,那么編譯器會要求該參數引用是final的。
6.2匿名內部類小結
- 匿名內部類是沒有訪問修飾符的。
匿名內部類中不能存在任何的靜態成員變量和靜態方法。
- new 匿名內部類,這個類首先是要存在的。如果我們將那個InnerClass接口注釋掉,就會出現編譯出錯。
- 當所在方法的形參需要被匿名內部類使用,那么這個形參就必須為final。
- 匿名內部類創建一個接口的引用時是沒有構造方法的。但是可以通過構造代碼塊來模擬構造器,像下面這樣:
public A getA(){
return new A(){
int num = 0;
String str;
{
str = "這是構造代碼塊!";
System.out.println("str 已經被初始化!");
}
};
}
- 但是當匿名內部類創建一個抽象類或者實體類的引用時,如果有必要,是可以定義構造函數的:
public class Outer {
public static void main(String[] args) {
Outer outer = new Outer();
Inner inner = outer.getInner("Inner", "gz");
System.out.println(inner.getName());
}
public Inner getInner(final String name, String city) {
return new Inner(name, city) {
private String nameStr = name;
public String getName() {
return nameStr;
}
};
}
}
abstract class Inner {
Inner(String name, String city) {
System.out.println(city);
}
abstract String getName();
}
//注意這里的形參city,由于它沒有被匿名內部類直接使用,而是被抽象類Inner的構造函數所使用,所以不必定義為final。
- 匿名內部類不能是抽象的,它必須要實現繼承的類或者實現的接口的所有抽象方法。
事實上,創建匿名內部類要寫的模板代碼太多了,java8中的lambda表達式能夠替代大部分的匿名類,優雅簡潔代碼少,所以建議大家學習java8,當然,匿名內部類的知識還是要掌握的。
7.內部類的繼承
內部類的繼承,是指內部類被繼承,普通類 extents 內部類。而這時候代碼上要有點特別處理,具體看以下例子:
public class InheritInner extends WithInner.Inner {
// InheritInner() 是不能通過編譯的,一定要加上形參
InheritInner(WithInner wi) {
wi.super();
}
public static void main(String[] args) {
WithInner wi = new WithInner();
InheritInner obj = new InheritInner(wi);
}
}
class WithInner {
class Inner {
}
}
可以看到子類的構造函數里面要使用父類的外部類對象.super();而這個對象需要從外面創建并傳給形參。
8. 多重繼承
內部類是除了接口外實現多重繼承的又一有利工具。
利用接口實現多重繼承我們都知道,就是一次性實現很多接口。那么,如何利用內部類實現多重繼承呢?
看代碼:
//父親
public class Father {
public int strong(){
return 9;
}
}
//母親
public class Mother {
public int kind(){
return 8;
}
}
//兒子
public class Son {
/**
* 內部類繼承Father類
*/
class Father_1 extends Father{
public int strong(){
return super.strong() + 1;
}
}
class Mother_1 extends Mother{
public int kind(){
return super.kind() - 2;
}
}
public int getStrong(){
return new Father_1().strong();
}
public int getKind(){
return new Mother_1().kind();
}
}
public class Test1 {
public static void main(String[] args) {
Son son = new Son();
System.out.println("Son 的Strong:" + son.getStrong());
System.out.println("Son 的kind:" + son.getKind());
}
}
//輸出
//Son 的Strong:10
//Son 的kind:6
兒子繼承了父親,變得比父親更加強壯,同時也繼承了母親,只不過溫柔指數下降了。這里定義了兩個內部類,他們分別繼承父親Father類、母親類Mother類,且都可以非常自然地獲取各自父類的行為,這是內部類一個重要的特性:內部類可以繼承一個與外部類無關的類,保證了內部類的獨立性,正是基于這一點,多重繼承才會成為可能。
9. 內部類的原理簡析
上面說過這樣兩點:
(1) 在外部類的作用范圍內可以任意創建內部類對象,即使內部類是私有的(私有內部類)。即內部類對包圍它的外部類可見。
(2) 在內部類中可以訪問其外部類的所有域,即使是私有域。即外部類對內部類可見。
問題來了:上面兩個特點到底如何辦到的呢?內部類的"內部"到底發生了什么?
其實,內部類是Java編譯器一手操辦的。虛擬機并不知道內部類與常規類有什么不同。 編譯器是如何瞞住虛擬機的呢?
我們用javac命令編譯一下下面的代碼:
class Outer{
//外部類私有數據域
private int data=0;
//內部類
class Inner{
void print(){
//內部類訪問外部私有數據域
System.out.println(data);
}
}
}
可以看到這樣的結果:
這里寫圖片描述
對內部類進行編譯后發現有兩個class文件:Outer.class 、和Outer$Inner.class 。這說明內部類Inner仍然被編譯成一個獨立的類(Outer$Inner.class),而不是Outer類的某一個域。 虛擬機運行的時候,也是把Inner作為一種常規類來處理的。
但問題又來了,即然是兩個常規類,為什么他們之間可以互相訪問私有域那(最開始提到的兩個內部類特點)?這就要問問編譯器到底把這兩個類編譯成什么東西了。
我們利用reflect反射機制來探查了一下內部類編譯后的情況:
//反編譯后的Outer$Inner
class Outer$Inner{
Outer$Inner(Outer,Outer$Inner); //包可見構造器
private Outer$Inner(Outer); //私有構造器將設置this$0域
final Outer this$0; //外部類實例域this$0
}
好了,現在我們可以解釋上面的第一個內部類特點了: 為什么外部類可以創建內部類的對象?并且內部類能夠方便的引用到外部類對象?
首先編譯器將外、內部類編譯后放在同一個包中。在內部類中附加一個包可見構造器。這樣, 虛擬機運行Outer類中Inner in=new Inner(); 實際上調用的是包可見構造: new Outer$Inner(this,null)。因此即使是private內部類,也會通過隱含的包可見構造器成功的獲得私有內部類的構造權限。
再者,Outer$Inner類中有一個指向外部類Outer的引用this$0,那么通過這個引用就可以方便的得到外部類對象中可見成員。但是Outer類中的private成員是如何訪問到的呢?這就要看看下面Outer.class文件中的秘密了。
class Outer{
static int access$0(Outer); //靜態方法,返回值是外部類私有域 data 的值。
}
現在可以解釋第二個特點了:為什么內部類可以引用外部類的私有域?
原因的關鍵就在編譯器在外圍類中添加了靜態方法access$0。 它將返回值作為參數傳遞給他的對象域data。這樣內部類Inner中的打印語句:System.out.println(data);
實際上運行的時候調用的是:System.out.println(this$0.access$0(Outer));
總結一下編譯器對類中內部類做的手腳吧:
- (1) 在內部類中偷偷摸摸的創建了包可見構造器,從而使外部類獲得了創建權限。
- (2) 在外部類中偷偷摸摸的創建了訪問私有變量的靜態方法,從而 使 內部類獲得了訪問權限。這樣,類中定義的內部類無論私有,公有,靜態都可以被包圍它的外部類所訪問。
反射的知識,只會會講,IDEA自帶將.class文件反射的功能,但是它的功能強大到可以把Outer類完整的還原。。。
10. 總結
本篇文章過長,總結完以后也是身心俱疲,所以就不多啰嗦了。文章有什么錯誤或者不足,請及時與我聯系。
轉載請注明出處:
本文地址:http://blog.csdn.net/qq_31655965/article/details/54988623
原創自csdn:http://blog.csdn.net/qq_31655965
博主:clever_fan
看完了,如果對你有幫助,隨手點個贊唄~~~
參考資料
《java編程思想》
《java核心技術》
http://android.blog.51cto.com/268543/384844/
http://www.iteye.com/topic/442435
http://www.cnblogs.com/chenssy/p/3389027.html
http://www.cnblogs.com/jiangao/archive/2012/02/23/2364119.html