小胖從官網出發,研究下為什么我們需要些內部類,內部類的區別和聯系。
思考三個問題:
(1)為什么需要內部類?靜態內部類和非靜態內部類有什么區別;
(2)為什么內部類可以無條件訪問外部類成員;
(3)為什么jdk1.8之前,局部內部類和匿名內部類訪問局部變量或者方法參數需要加final修飾符?
1. 官網閱讀:
1.1 為什么需要內部類
It is a way of logically grouping classes that are only used in one place: If a class is useful to only one other class, then it is logical to embed it in that class and keep the two together. Nesting such "helper classes" makes their package more streamlined.
簡化包配置:如果一個類只對另一個類有用,將他們嵌套在一起是合理的。嵌套一些“有幫助的類”可以使得包更加簡化
It increases encapsulation: Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.
增加了封裝:兩個頂級類A和B,B需要訪問A中聲明為private
的成員。
It can lead to more readable and maintainable code: Nesting small classes within top-level classes places the code closer to where it is used.
易讀和可維護:在頂級類中嵌套小類會使代碼更接近于使用它的位置。
1.2 為什么Java內部類設計為靜態和非靜態
Nested classes are divided into two categories: static and non-static. Nested classes that are declared static are called static nested classes. Non-static nested classes are called inner classes.
嵌套類一般分為兩類:靜態和非靜態。聲明static
的嵌套類稱為靜態嵌套類
。非靜態嵌套類稱為內部類
。
A static nested class interacts with the instance members of its outer class (and other classes) just like any other top-level class. In effect, a static nested class is behaviorally a top-level class that has been nested in another top-level class for packaging convenience.
靜態嵌套類就像任何頂級類一樣 與 其外部類(其他的類)的實例成員交互。事實上,靜態內部類在行為上就是一個頂級類,它嵌套在一個頂級類中以方便打包。
As with class methods and variables, a static nested class is associated with its outer class. And like static class methods, a static nested class cannot refer directly to instance variables or methods defined in its enclosing class: it can use them only through an object reference.
和靜態方法一樣,靜態嵌套類不能直接引用封閉類中定義的實例變量和方法。只能通過對象的引用來使用它們。
靜態方法引用對象:
public class TestNest {
private String abc;
public String getAbc() {
return abc;
}
public static String nestSta() {
TestNest testNest = new TestNest();
return testNest.getAbc();
}
}
As with instance methods and variables, an inner class is associated with an instance of its enclosing class and has direct access to that object's methods and fields. Also, because an inner class is associated with an instance, it cannot define any static members itself.
內部類可以直接訪問該對象的字段和方法,由于內部類和實例相關聯,因此無法定義任何的靜態成員變量。
那我們怎么理解呢?
靜態內部類就是一個獨立的類。為什么使用靜態內部類呢?
比如A,B兩個類,B有點特殊,雖然可以單獨存在,但只能被A使用。那么此時應該怎么辦?把B并入到A里面,復雜性提高,搞的A違反單一職責。如果B獨立,又可以被其他類依賴,不符合設計本意,不如將其變成A.B。其他類就不能使用B了。
而相比起來,非靜態的才是真正的內部類,對其外部類有一個引用。
1.3 序列化
Serialization of inner classes, including local and anonymous classes, is strongly discouraged. When the Java compiler compiles certain constructs, such as inner classes, it creates synthetic constructs;.....However, synthetic constructs can vary among different Java compiler implementations, which means that .class files can vary among different implementations as well. Consequently, you may have compatibility issues if you serialize an inner class and then deserialize it with a different JRE implementation.
強烈建議不要對內部類進行序列化。java編譯器編譯某些構造(如內部類)時,他會創建“合成構造”。合成構造在不同的java編譯器中變化。序列化內部類,然后使用不同的JRE實現反序列化,則可能存在兼容的問題。
2. 實戰內部類
2.1 成員內部類
類的成員
無條件訪問外部類
依賴外部類
多種訪問權限
2.1.1 內部類的特點
成員內部類中不能定義靜態變量
public class Outer {
private String name;
public Outer(String name) {
this.name = name;
}
//外部類使用內部類的方法
public void write() {
Inner inner = new Inner();
inner.say();
System.out.println(inner.getInnnerName());
}
//成員內部類
class Inner {
private String InnnerName;
public String getInnnerName() {
return InnnerName;
}
public void setInnnerName(String innnerName) {
InnnerName = innnerName;
}
public void say() {
System.out.println(name);
}
}
}
成員內部類相當于類的成員變量,可以無條件訪問外部類的成員。但不過要注意的是,成員內部類和外部內部類擁有同名的成員方法或者方法時,要顯式的聲明,否則默認情況下訪問的是內部類成員。
外部類.this.成員變量
外部類.this.成員方法
反編譯后的class文件:
public class Outer {
private String name;
public Outer(String name) {
this.name = name;
}
public void write() {
Outer.Inner inner = new Outer.Inner();
inner.say();
System.out.println(inner.getInnnerName());
}
class Inner {
private String InnnerName;
Inner() {
}
public String getInnnerName() {
return this.InnnerName;
}
public void setInnnerName(String innnerName) {
this.InnnerName = innnerName;
}
public void say() {
System.out.println(Outer.this.name);
}
}
}
外部類想訪問內部類的成員必須先創建一個內部類的對象,再通過指向這個對象的引用來訪問。
public void wirte() {
Inner inner = new Inner();
inner.say();
System.out.println(i + ":" + name);
}
于是外部類就可以訪問內部類的所有成員了!
2.1.2 如何創建內部類
我們知道,成員內部類是依賴于外部類而存在的。也就是說,想要創建成員內部類,前提是有一個外部類對象。
方式一:
Outer outer = new Outer();
Inner inner = outer.new Inner();
方式二:
Outter.Inner inner1 = outter.getInnerInstance();
//getInnerInstance()方法
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
2.1.3 成員內部類權限問題
成員內部類可以擁有public
、default
、protected
、private
權限。
-
private
只能在外部類中被訪問。 -
default
同一個包下訪問。 -
protected
同一個包下或繼承外部類的情況下。 -
public
任何地方。
而外部類只有public
和default
兩種訪問權限。
2.1.4 小結
成員內部類是依附于外部類存在的,并且和外部類的一個成員變量有相似之處。內部類可以無條件訪問外部類的成員、外部類需要內部類的引用才能訪問、四種訪問權限。
請注意(下面兩個內部類):jdk版本是1.8,看起來似乎編譯器取消了沒有聲明為
final
的變量或參數也可以在局部內部類和匿名內部類被訪問。但事實上是Java8引入了effectively final
概念,被默認成為了final類型。
2.2 局部內部類
方法或作用域內
局部變量
注意:局部內部類和成員內部類的區別就是局部內部類的訪問僅限于方法內或者該作用域內。不能定義靜態變量。
class Outer {
Object method() {
int locvar = 1;
System.out.println("1111");
class Inner {
void displayLocvar() {
System.out.println("locvar = " + locvar);
}
}
Object in = new Inner();
return in;
}
}
注意:局部內部類更像一個局部變量,是沒有訪問修飾符的。
2.3 匿名內部類
一般來說,匿名內部類用于繼承其他類或是實現接口,并不需要增加額外的方法,只是對繼承的方法的實現或者重寫。
匿名類的格式:
new Thread(new Runnable() {
@Override
public void run() {
//TODO
}
});
通過new XXX(){};
的方式創建了一個只能使用一次子類。
--
為什么叫做匿名內部類呢?
匿名內部類只能使用一次。他通常用來簡化代碼。但是使用匿名內部類還有一個前提條件:必須繼承一個父類(抽象類或具體類
)或是實現一個接口。
對于這個問題,首先我們應該明確的一點是對于匿名內部類,它可能引用三種外部變量:
- 外部類的成員變量(所有的變量);
- 外部方法或作用域內的局部變量;
- 外部方法的參數;
實際上,只有第一種變量不需要聲明為final。
原因:
首先,在這里提出,網上的答案基本是:局部變量聲明周期和局部類的聲明周期不同。會導致內部類失去引用造成錯誤!!!等等,一個變量加上final就可以延長生命周期了嗎?那加上final豈不是會造成內存短暫泄露?
正解:匿名內部類和所有類一樣,也是有自己的構造函數的,只不過這個構造函數是隱式的。
加入final修飾符是為了保持內部和外部的數據的一致性。
編譯前:
public class Outer {
String string = "";
void outerTest(final char ch){
final Integer integer = 1;
Inner inner = new Inner() {
void innerTest() {
System.out.println(string);
System.out.println(ch);
System.out.println(integer);
}
};
}
public static void main(String[] args) {
new Outer().outerTest(' ');
}
class Inner {
}
}
編譯后:
class Outer$1extends Outer.Inner
{
Outer$1(Outer paramOuter, char paramChar, Integer paramInteger)
{
super(paramOuter);
}
void innerTest()
{
System.out.println(this.this$0.string);
System.out.println(this.val$ch);
System.out.println(this.val$integer);
}
}
匿名內部類之所以可以訪問局部變量,是因為在底層將這個局部變量的值傳入了匿名內部類中,并且以匿名內部類的成員變量存在,這個值的傳遞過程是通過匿名內部類的構造器完成的。
我們可以看到匿名內部類引用的局部變量
和方法參數
以及外部類的引用
都會被當做構造函數的參數。但是外部類的成員變量
是通過外部類的引用來訪問的。
那么為什么匿名內部類訪問外部類的成員變量,無需final修飾呢?
因為非靜態內部類的對象保存了外部類對象的引用,因此內部類對外部類成員變量的修改都會真實的反應到外部類實例本身,所以不需要final修飾。
需要引入兩個知識點:
- 值傳遞和引用傳遞:基本類型作為參數傳遞時,傳遞的是值的引用,無論怎么改變這個拷貝,原值是不會改變的;當對象作為參數傳遞時,傳遞是對象引用的拷貝,無論怎么改變新引用的指向,原引用是不會改變的(當然通過新引用改變對象的內容,那么改變就是確確實實發生了)。
- final作用:被final修飾基本類型變量,不可更改其值;當被final修飾引用變量,不可改變其指向,只能改變對象的內容。
于是,假設允許不對局部變量加final,當匿名內部類里面嘗試改變外部基本類型的值的時候,或者改變外部引用變量的指向的時候,表面上看起來是成功了,但是實際上并不會影響到外部的變量。
所以java就一刀切,強制加上了final修飾。
2.4 靜態內部類
我們上面知道,靜態內部類是一個獨立的類,不需要依賴外部類也能存在的。所以靜態內部類不能使用外部類非static成員變量或者方法。
因為外部類的非靜態成員必須依附于具體的對象。
靜態內部類的創建方法:
外部類.內部類 引用名=new 外部類.內部類();
public static void main(String[] args) {
//靜態內部類的創建方法:
Outer.Inner in = new Outer.Inner();
in.say();
}
3. 問題解答
看到這里,我相信大家應該心里對問題也有了自己的答案。
靜態內部類是不依附與外部類存在的。而非靜態內部類就是外部類的一個成員,是需要依附于外部類。
非靜態內部類中含有構造函數,構造函數中會將外部類的引用傳入,所以,內部類可以無條件訪問外部類成員。
為什么使用final和生命周期是無關的,主要是java為了保持內部和外部變量的統一。
4. 內部類常見面試題
- 根據注釋填寫(1),(2),(3)處的代碼
public class Test{
public static void main(String[] args){
// 初始化Bean1
(1)
bean1.I++;
// 初始化Bean2
(2)
bean2.J++;
//初始化Bean3
(3)
bean3.k++;
}
class Bean1{
public int I = 0;
}
static class Bean2{
public int J = 0;
}
}
class Bean{
class Bean3{
public int k = 0;
}
}
我們可以知道,成員內部類,必須先產生外部類的實例化對象,才能產生內部類的實例化對象。而靜態內部類不需要產生實例化對象即可產生內部類的實例化對象。
創建靜態內部類:
外部類類名.內部類類名 xxx=new 外部類類名.內部類類名();
創建成員內部類:
外部類類名.內部類類名 xxx=外部類對象名.new 內部類類名();
因此,(1),(2),(3)處的代碼分別為:
Test test = new Test();
Test.Bean1 bean1 = test.new Bean1();
Test.Bean2 b2 = new Test.Bean2();
Bean bean = new Bean();
Bean.Bean3 bean3 = bean.new Bean3();
2.下面這段代碼的輸出結果是什么?
public class Test {
public static void main(String[] args) {
Outter outter = new Outter();
outter.new Inner().print();
}
}
class Outter
{
private int a = 1;
class Inner {
private int a = 2;
public void print() {
int a = 3;
System.out.println("局部變量:" + a);
System.out.println("內部類變量:" + this.a);
System.out.println("外部類變量:" + Outter.this.a);
}
}
}
輸出答案
3 2 1
總結:內部類和外部類變量的訪問權限問題:
- 非靜態內部類依賴于外部類對象的創建,所以,非靜態類中不能定義靜態變量。
- 非靜態內部類的構造方法中,含有外部類的引用。可以直接使用所有的外部類成員。
- 外部類不能直接使用非靜態內部類的成員。除非創建內部類對象。
- 可以把靜態內部類看做一個
獨立的靜態類
,所以不能直接使用一個類的實例成員。 -
匿名類必須繼承一個類(
抽象類或具體類
)或者實現一個接口
。new XXX(){};
就是一個內部類。 - 只含有
private
構造方法的類不能被繼承,所以可以使用protected
修飾類,以達到讓子類繼承的目的,此時,使用匿名內部類的new XXX(){};
的方式就可以創建出一個XXX的子類對象。