Java内部类详解
是什么?
内部类顾名思义,即在类中定义的类,俗称套娃。
为什么?
内部类是一种非常有用的特性,因为它允许你把一些逻辑相关的类组织在一起,并控制位于内部的类的可见性。让代码更优雅简洁。
除此之外,内部类还能链接外部类。内部类对外围类的引用在内部类的构造器中设置。编译器修改了所有的内部类的构造器,添加一个外围类引用的参数。所以内部类拥有其外围类的所有元素的访问权。
package com.silentSunshine.common.innerclass;
/**
* @auther: zhouqichun
* @date: 2021/1/18 - 01 - 18 - 15:16
* @description: com.silentSunshine.common.innerclass
* @version: 1.0
*/
class OuterClass{
private String outer = "outer";
private int anInt = 1;
class InnerClass{
//内部类访问外围类字段
public void setAnInt(int anInt){
//由于[***属性屏蔽***](),可以这么访问外围类字段
OuterClass.this.anInt = anInt;
}
public void print(){
//直接访问外围类的字段
System.out.println(outer);
}
}
}
此外,当生成一个内部类对象时,此对象与制造它的外围对象之间就有了一种联系,所以它能访问其外围对象的所有成员,而不需要任何特殊条件。
package com.silentSunshine.common.innerclass;
/**
* @auther: zhouqichun
* @date: 2021/1/18 - 01 - 18 - 15:16
* @description: com.silentSunshine.common.innerclass
* @version: 1.0
*/
public class InnerClassTest {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
//获得内部类对象
//如果想从外部类的非静态方法之外在任意位置创建内部类对象时,
//类型必须指定为:***OutClassName.InnerClassName***
OuterClass.InnerClass innerClass = outerClass.inner();
System.out.println("before change, outClass.anInt:" + outerClass.getAnInt());
//使用内部类对象改变外围类对象的字段
innerClass.setAnInt(2);
//innerClass.outer; Error, 不能直接访问outerClass的outer字段,即使outer为public
System.out.println("after change by innerClass, outClass.anInt:" + outerClass.getAnInt());
}
}
class OuterClass{
public String outer = "outer";
private int anInt = 1;
//内部类的一个典型用法是,外部类有一个方法,该方法返回一个指向内部类的引用。
//该方法返回内部类的一个对象
public InnerClass inner(){
return new InnerClass();
}
class InnerClass{
public void setAnInt(int anInt){
OuterClass.this.anInt = anInt;
}
}
}
-
输出为
before change, outClass.anInt:1 after change by innerClass, outClass.anInt:2
使用.this和.new
如果你需要生成对外部类对象的引用,可以使用外部类的名字后面紧跟圆点和this
(即OutClass.this
)
有时你可能想要告知某些其他对象,去创建其某个内部类的对象。要实现此目的,你必须在new
表达式中提供对其他外部类对象的引用,这就需要使用.new
语法
要想直接创建内部类的对象,你不能按照你想象的方式,去引用外部类的名字,而是必须使用外部类的对象来创建该内部类对象,即OuterObject.new InnerClass()
内部类的优点可以总结为以下四点
- 1.每个内部类都能独立的继承一个接口的实现,所以无论外部类是否已经继承了某个(接口的)实现,对于内部类都没有影响。内部类使得多继承的解决方案变得完整。
- 2.方便将存在一定逻辑关系的类组织在一起,又可以对外界隐藏。
- 3.方便编写事件驱动程序。
- 4.方便编写线程代码。
怎么样?
内部类包括成员内部类、局部内部类、匿名内部类、静态内部类
成员内部类
是什么
定义在类中的普通类,它是非静态的,跟其他类差别不大,称之为***成员内部类***。
怎么样
-
code
package com.silentSunshine.common.innerclass; /** * @auther: zhouqichun * @date: 2021/1/18 - 01 - 18 - 8:31 * @description: com.silentSunshine.common.innerclass * @version: 1.0 */ public class MemberInnerClass { public static void main(String[] args) { //创建外部类对象 Outer outer = new Outer(); //创建内部类对象 Outer.Inner inner = new Outer().new Inner(); //外部类对象调用方法 outer.print(); //Outer outer.inner.yell("形式参数1"); //内部类对象调用方法 inner.print(); //Inner inner.yell("形式参数2"); } } class Outer{ private int anInt = 1; private String string = "Outer"; //在外部类中创建内部类对象 public Outer.Inner inner = new Inner(); //外部类方法 public void print(){ System.out.println("outer"); } class Inner{ private int anInt2 = 2; private String string = "Inner"; //内部类方法 public void print(){ System.out.println("inner"); } public void yell(String string){ //内部类直接调用外部类的属性 System.out.println(anInt); //内部类调用自身属性 System.out.println(anInt2); //如果外部类与内部类有名称相同的字段时,可以通过以下方法调用字段 //调用外部类字段 System.out.println(Outer.this.string); //调用内部类字段 System.out.println(this.string); //传入的参数 System.out.println(string); } } }
输出为
-
output
outer 1 2 Outer Inner 形式参数1 inner 1 2 Outer Inner 形式参数2 进程已结束,退出代码0
成员内部类的属性屏蔽:
1 public class ShadowTest {
2
3 public int x = 0;
4
5 class FirstLevel {
6
7 public int x = 1;
8
9 void methodInFirstLevel(int x) {
10 System.out.println("x = " + x);
11 System.out.println("this.x = " + this.x);
12 System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
13 }
14 }
15
16 public static void main(String... args) {
17 ShadowTest st = new ShadowTest();
18 ShadowTest.FirstLevel fl = st.new FirstLevel();
19 fl.methodInFirstLevel(23);
20 }
21 }
输出结果为:
x = 23
this.x = 1
ShadowTest.this.x = 0
这个实例中有三个变量x:1、ShadowTest
类的成员变量;2、内部类FirstLevel
的成员变量;3、内部类方法methodInFirstLevel
的参数。
methodInFirstLevel
的参数x
屏蔽了内部类FirstLevel
的成员变量,因此,在该方法内部使用x时实际上是使用的是参数x
,可以使用this
关键字来指定引用是成员变量x
:
System.out.println("this.x = " + this.x);
利用类名来引用其成员变量拥有最高的优先级,不会被其他同名变量屏蔽,如:
System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
局部内部类
是什么
在一个块中定义的类称为***局部类***。局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。
为什么
局部类有一个优势,即对外部世界可以完全的隐藏起来。
与其他内部类相比较,局部类还有一个优点。它们不仅能够访问包含它们的外部类(如果是在main方法),还可以访问局部变量。不过,那些局部变量必须事实上为final
。
怎么样
-
code
package com.silentSunshine.common.innerclass; /** * @auther: zhouqichun * @date: 2021/1/19 - 01 - 19 - 8:12 * @description: com.silentSunshine.common.innerclass * @version: 1.0 */ public class PartialClassTest { private static String string = "hello"; public static void main(String[] args) { //先定义在使用,如果在使用后面定义,会报错 int Apple = 1; //Apple = 2; Error //局部内部类:定义在块中,即定义在{}中 class PartialClass{ public void print(){ System.out.println("i Am A PartialClass"); //局部类引用局部变量,局部变量必须为final System.out.println("局部变量 Apple = "+Apple); //在main中可以访问外围类字段,不过必须为静态字段 System.out.println("PartialClassTest.string = " + string); } } //创建局部类对象 PartialClass partialClass = new PartialClass(); partialClass.print(); } }
-
输出为
i Am A PartialClass 局部变量 Apple = 1 PartialClassTest.string = hello
-
局部类不能用public
或private
访问说明符进行声明。它的作用域被限定在声明这个局部类的块中。
注意: 局部内部类就像是方法里面的一个局部变量一样,是不能有 public
、protected
、private
以及 static
修饰符的。
匿名内部类
是什么
为了只创建某个类的一个对象,而不想为这个类命名,这种类被称为***匿名内部类***。
为什么
匿名内部类应该是平时我们编写代码时用得最多的,在编写事件监听的代码时使用匿名内部类不但方便,而且使代码更加容易维护。多年来,Java程序员习惯的做法是用匿名内部类实现事件监听器和其他回调。如今最好还是使用lambda表达式。
怎么样
匿名内部类的语法格式:
- 操作符:
new
; - 一个要实现的接口或要继承的类;
- 一对括号,如果是匿名子类,与实例化普通类的语法类似,如果有构造参数,要带上构造参数;如果是实现一个接口,只需要一对空括号即可;
- 一段被"{}"括起来类声明主体;
- 末尾的";"号(因为匿名类的声明是一个表达式,是语句的一部分,因此要以分号结尾)。
下面这段代码是一段 Android 事件监听代码:
scan_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}});
history_bt.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}});
-
以上代码的lambda版本
onClickListener a = v->{// TODO Auto-generated method stub}; scan_bt.setOnClickListener(a.onClick()); a = v->{// TODO Auto-generated method stub}; history_bt.setOnClickListener(a.onClick());
这段代码为两个按钮设置监听器,这里面就使用了匿名内部类。这段代码中的:
new OnClickListener() {
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}}
就是匿名内部类的使用。代码中需要给按钮设置监听器对象,使用匿名内部类能够在实现父类或者接口中的方法情况下同时产生一个相应的对象,但是前提是这个父类或者接口必须先存在才能这样使用。当然像下面这种写法也是可以的,跟上面使用匿名内部类达到效果相同。
private void setListener(){
scan_bt.setOnClickListener(new Listener1());
history_bt.setOnClickListener(new Listener2());}
class Listener1 implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}}
class Listener2 implements View.OnClickListener{
@Override
public void onClick(View v) {
// TODO Auto-generated method stub
}}
这种写法虽然能达到一样的效果,但是既冗长又难以维护,所以一般使用匿名内部类的方法来编写事件监听代码。同样的,匿名内部类也是不能有访问修饰符和 static
修饰符的。
匿名内部类是唯一一种没有构造器的类。正因为其没有构造器,所以匿名内部类的使用范围非常有限,大部分匿名内部类用于接口回调。匿名内部类在编译的时候由系统自动起名为Outter$1.class
。一般来说,匿名内部类用于继承其他类或是实现接口,并不需要增加额外的方法,只是对继承方法的实现或是重写。
深入理解匿名内部类
匿名内部类与局部类对作用域内的变量拥有相同的的访问权限。
(1)、匿名内部类可以访问外部内的所有成员;
(2)、匿名内部类不能访问外部类未加final修饰的变量(注意:JDK1.8
即使没有用final
修饰也可以访问);
(3)、属性屏蔽,与成员内部类相同,匿名内部类定义的类型(如变量)会屏蔽其作用域范围内的其他同名类型(变量):
匿名内部类的属性屏蔽:
1 public class ShadowTest {
2 public int x = 0;
3
4 interface FirstLevel {
5 void methodInFirstLevel(int x);
6 }
7
8 FirstLevel firstLevel = new FirstLevel() {
9
10 public int x = 1;
11
12 @Override
13 public void methodInFirstLevel(int x) {
14 System.out.println("x = " + x);
15 System.out.println("this.x = " + this.x);
16 System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
17 }
18 };
19
20 public static void main(String... args) {
21 ShadowTest st = new ShadowTest();
22 ShadowTest.FirstLevel fl = st.firstLevel;
23 fl.methodInFirstLevel(23);
24 }
25 }
输出结果为:
x = 23
this.x = 1
ShadowTest.this.x = 0
(4)、匿名内部类中不能定义静态属性、方法,但可以有静态常量static final
;
1 public class ShadowTest {
2 public int x = 0;
3
4 interface FirstLevel {
5 void methodInFirstLevel(int x);
6 }
7
8 FirstLevel firstLevel = new FirstLevel() {
9
10 public int x = 1;
11
12 public static String str = "Hello World"; // 编译报错
13
14 public static void aa() { // 编译报错
15 }
16
17 public static final String finalStr = "Hello World"; // 正常
18
19 public void extraMethod() { // 正常
20 // do something
21 }
22 };
23 }
(5)、匿名内部类可以有常量属性(final
修饰的属性);
(6)、匿名内部内中可以定义属性,如上面代码中的代码:private int x = 1;
(7)、匿名内部内中可以可以有额外的方法(父接口、类中没有的方法);
(8)、匿名内部内中可以定义内部类;
(9)、匿名内部内中可以对其他类进行实例化。
静态内部类
是什么&为什么
有时候,使用内部类只是为了把一个类隐藏在另外一个类的内部,并不需要内部类引用外围类对象。为此,可以将内部类声明为static
,以便取消产生的引用。即***静态内部类***
静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static
成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static
成员就会产生矛盾,因为外部类的非static
成员必须依附于具体的对象。
怎么样
public class Test {
public static void main(String[] args) {
Outter.Inner inner = new Outter.Inner();
}}
class Outter {
public Outter() {
}
//静态内部类
static class Inner {
public Inner() {
}
}—-
注意事项
注意:
- 只有内部类可以声明为
static
,外围类不可以。 - 只有内部类可以是私有类,而常规类只可以具有包可见性,或公有可见性。
- 内部类中声明的所有静态域都必须是final,即必须为
static final
。 - 只有静态内部类可以允许有静态方法,但只能访问外围类的静态域和方法。
注释:在内部类不需要访问外围类对象的时候,应该使用静态内部类。有些程序员用嵌套类(nested class)表示静态内部类。
注释:与常规内部类不同,静态内部类可以有静态域和方法。
注释:声明在接口中的内部类自动成为static
和public
类。
深入理解
1.为什么成员内部类可以无条件访问外部类的成员?
在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是 Outter.java 的代码:
public class Outter {
private Inner inner = null;
public Outter() {
}
public Inner getInnerInstance() {
if(inner == null)
inner = new Inner();
return inner;
}
protected class Inner {
public Inner() {
}
}}
编译之后,出现了两个字节码文件:
反编译 Outter$Inner.class 文件得到下面信息:
E:\Workspace\Test\bin\com\cxh\test2>javap -v Outter$Inner
Compiled from "Outter.java"
public class com.cxh.test2.Outter$Inner extends java.lang.Object
SourceFile: "Outter.java"
InnerClass:
#24= #1 of #22; //Inner=class com/cxh/test2/Outter$Inner of class com/cxh/tes
t2/Outter
minor version: 0
major version: 50
Constant pool:
const #1 = class #2; // com/cxh/test2/Outter$Inner
const #2 = Asciz com/cxh/test2/Outter$Inner;
const #3 = class #4; // java/lang/Object
const #4 = Asciz java/lang/Object;
const #5 = Asciz this$0;
const #6 = Asciz Lcom/cxh/test2/Outter;;
const #7 = Asciz <init>;
const #8 = Asciz (Lcom/cxh/test2/Outter;)V;
const #9 = Asciz Code;
const #10 = Field #1.#11; // com/cxh/test2/Outter$Inner.this$0:Lcom/cxh/t
est2/Outter;
const #11 = NameAndType #5:#6;// this$0:Lcom/cxh/test2/Outter;
const #12 = Method #3.#13; // java/lang/Object."<init>":()V
const #13 = NameAndType #7:#14;// "<init>":()V
const #14 = Asciz ()V;
const #15 = Asciz LineNumberTable;
const #16 = Asciz LocalVariableTable;
const #17 = Asciz this;
const #18 = Asciz Lcom/cxh/test2/Outter$Inner;;
const #19 = Asciz SourceFile;
const #20 = Asciz Outter.java;
const #21 = Asciz InnerClasses;
const #22 = class #23; // com/cxh/test2/Outter
const #23 = Asciz com/cxh/test2/Outter;
const #24 = Asciz Inner;
{
final com.cxh.test2.Outter this$0;
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
Code:
Stack=2, Locals=2, Args_size=2
0: aload_0
1: aload_1
2: putfield #10; //Field this$0:Lcom/cxh/test2/Outter;
5: aload_0
6: invokespecial #12; //Method java/lang/Object."<init>":()V
9: return
LineNumberTable:
line 16: 0
line 18: 9
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 this Lcom/cxh/test2/Outter$Inner;
}
第11行到35行是常量池的内容,下面注意第38行的内容:
final com.cxh.test2.Outter this$0;
这行是一个指向外部类对象的指针,看到这里想必大家豁然开朗了。也就是说编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?下面接着看内部类的构造器:
public com.cxh.test2.Outter$Inner(com.cxh.test2.Outter);
从这里可以看出,虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的 Outter this&0
指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对 Outter this&0
引用进行初始化赋值,也就无法创建成员内部类的对象了。
2.为什么局部内部类和匿名内部类只能访问局部final变量?
想必这个问题也曾经困扰过很多人,在讨论这个问题之前,先看下面这段代码:
public class Test {
public static void main(String[] args) {
}
public void test(final int b) {
final int a = 10;
new Thread(){
public void run() {
System.out.println(a);
System.out.println(b);
};
}.start();
}}
这段代码会被编译成两个class
文件:Test.class
和Test1.class
。默认情况下,编译器会为匿名内部类和局部内部类起名为Outterx.class
(x为正整数)。
根据上图可知,test 方法中的匿名内部类的名字被起为 Test$1
。
上段代码中,如果把变量 a 和 b 前面的任一个 final 去掉,这段代码都编译不过。我们先考虑这样一个问题:
当 test 方法执行完毕之后,变量a的生命周期就结束了,而此时 Thread 对象的生命周期很可能还没有结束,那么在 Thread
的 run
方法中继续访问变量 a
就变成不可能了,但是又要实现这样的效果,怎么办呢?Java 采用了复制的手段来解决这个问题。将这段代码的字节码反编译可以得到下面的内容:
我们看到在 run
方法中有一条指令:
这条指令表示将操作数10压栈,表示使用的是一个本地局部变量。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。
下面再看一个例子:
public class Test {
public static void main(String[] args) {
}
public void test(final int a) {
new Thread(){
public void run() {
System.out.println(a);
};
}.start();
}}
反编译得到:
我们看到匿名内部类 Test$1
的构造器含有两个参数,一个是指向外部类对象的引用,一个是 int 型变量,很显然,这里是将变量 test 方法中的形参 a 以参数的形式传进来对匿名内部类中的拷贝(变量 a 的拷贝)进行赋值初始化。
也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
从上面可以看出,在 run 方法中访问的变量 a 根本就不是 test 方法中的局部变量 a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在 run 方法中访问的变量 a 和 test 方法中的变量 a 不是同一个变量,当在 run 方法中改变变量 a 的值的话,会出现什么情况?
对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java 编译器就限定必须将变量 a 限制为 final 变量,不允许对变量 a 进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用 final 进行限定了。
3.静态内部类有特殊的地方吗?
从前面可以知道,静态内部类是不依赖于外部类的,也就说可以在不创建外部类对象的情况下创建内部类的对象。另外,静态内部类是不持有指向外部类对象的引用的,这个读者可以自己尝试反编译 class 文件看一下就知道了,是没有 Outter this&0 引用的。
参考文献
JAVA匿名内部类(Anonymous Classes) - 无恨之都 - 博客园
Java 内部类详解 | 菜鸟教程
《Java核心思想》