内部类平常用的还是挺多的,因此研究和总结内部类就显得非常重要。内部类,顾名思义就是将一个类的定义放在另一个类的内部进行,这就是内部类。内部类较难理解,但是功能强大,理解和运用内部类对提高编码水平有非常大的帮助。

初次邂逅—内部类存在的缘由

举一个经常看到的例子:

1
2
3
4
5
6
7
/**外部类**/
public class Outer {
/**内部类(内部是相对于外部而言)**/
class Inner{
//doSomething
}
}

现在问题来了,为何将一个类定义在某个类的内部,难道这不违反程序设计的单一职责原则吗?一个类定义在一个java源文件中不香吗?因此在使用内部类之前,了解使用内部类的理由显得尤为重要,个人觉得学知识带着目的来学,印象可能更深一些,毕竟有实际的例子来辅助记忆。

曾经读过一本书《Think in java》(像java一样思考),记得里面有一句关于内部类的话:”使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响”。当然这句话现在几乎成了劝人学内部类的定理。其实这句话透露出一些信息:1、内部类也可以继承某个类或实现某个接口;2、可以突破Java中类的单继承“限制”。

看一个例子,来加深对上述论据的理解。定义两个类ClassA和ClassB,很显然ClassC只能实现其中的任意一个类(此处继承ClassA),但是当你在ClassC的内部定义一个内部类InnerClassC,让它继承ClassB时,你会发现在内部类InnerClassC中是可以同时访问到ClassA和ClassB类中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ClassA {
public void classAmethod(){
System.out.println("classAmethod");
}
}

public class ClassB {
public void classBmethod(){
System.out.println("classBmethod");
}
}

public class ClassC extends ClassA {
class InnerClassC extends ClassB{
public InnerClassC(){
classAmethod();
classBmethod();
}
}
}

所以使用内部类最大的优点就在于,它能解决多重继承的问题。如果开发者不需要解决多重继承这一问题,那么可以使用其他方式,杀鸡焉用牛刀?

当然内部类的优点不仅仅只有上述一点,《Think in java》这本书中还列举了5点作为补充:1、内部类可以有多个实例,每个实例都有自己的状态信息,且与其他外围对象的信息相互独立。2、在单个外围类中,可以让多个内部类以不同的方式继承同一个类或者实现同一个接口。3、内部类对象的创建并不一定依赖于外围类对象。4、内部类并没有令人迷惑的“is-a”关系(继承关系),它就是一个独立的实体。5、内部类提供了更好的封装,除了该外围类,其他类都不能访问。

相识—内部类基础

接下来介绍内部类的基础知识,同时对前面的例子进行更深层次的分析与研究。来看一段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
//ClassA.class
public class ClassA {
private String Aname;
//getter和setter方法
public void classAmethod(){
System.out.println("classAmethod");
}
}

//ClassB.class
public class ClassB {
private String Bname;
//getter和setter方法
public void classBmethod(){
System.out.println("classBmethod");
}
}

//ClassC.class
public class ClassC extends ClassA {
private String Cname;
//getter和setter方法
class InnerClassC extends ClassB{
public InnerClassC(){
Cname = "I am innerClassC";
System.out.println(getAname());
System.out.println(getBname());
System.out.println(getCname());
}

public void show(){
System.out.println("Cname:"+getCname());
}
}

public static void main(String[] args){
ClassC c = new ClassC();
ClassC.InnerClassC ic = c.new InnerClassC();
ic.show();
}
}

运行结果为:

1
2
3
4
null
null
I am innerClassC
Cname:I am innerClassC

分析一下上述代码的含义:定义了两个类ClassA和ClassB,且均在内部设置了Xname属性及对应方法和一个classXmethod方法。接着再定义了一个ClassC类,它作为外部类继承ClassA类,同时内部也设置了Xname属性及对应方法。ClassC内部类InnerClassC继承了ClassB类,并定义了一个无参的构造方法。在这个无参的构造方法中,可以访问外部ClassC类的属性,哪怕是private修饰的。

为何内部类可以访问外部类的所有属性(包括私有)?原因在于创建某个外部类的内部类对象时,此时内部类对象必定会捕获一个指向该外部类对象的引用,只要内部类在访问外部类的成员时,就会使用这个引用来选择外部类的成员。说白了,就是这里内部类的创建需要依赖外部类对象(注意是这里,不是所有后续会说明)。

继续回到代码中,在ClassC外部类中定义了一个main方法。在这个main方法中首先实例化一个外部类对象,接着使用ClassC.InnerClassC ic = c.new InnerClassC()来实例化内部类对象。注意看这个内部类对象的引用为ClassC.InnerClassC,对象类型都得依赖外部类,且使用了外部类对象的new方法来创建内部类对象。

仔细查看内部类InnerClassC无参构造方法可知,居然可以直接使用getAname()getCname方法,这都是外部类的。按照常理开发者都习惯使用this来代指本类,super代指父类。尝试将InnerClassC内部类无参构造方法中的代码修改为如下:

1
2
3
4
5
6
public InnerClassC(){
Cname = "I am innerClassC";
System.out.println(this.getAname());
System.out.println(this.getBname());
System.out.println(this.getCname());
}

可是IDEA出现错误提示:

再次验证了前面的论述:这里的getAname()getCname方法都是外部类的,而它又可以直接使用,不需要显式调用。说明内部类中存在对外部类对象的一个隐式引用,其实就是ClassC.this

因此当开发者需要在内部类中生成一个外部类的引用时,使用ClassC.this即可。

内部类初学者都有一个疑问,这个包含内部类的java源文件在编译成class文件后,其中的内部类还存在吗?这个问题今天有必要验证一下。进入到ClassC类所在的文件夹,使用javac *.java命令进行编译(注意不能只单纯编译ClassC文件,其中包含了其他类的引用,需要同时编译):

发现问题了,这个ClassC类居然编译出两个文件。查看一下这个ClassC$InnerClassC字节码文件:

再来查看ClassC字节码文件:

可以看到这两个class文件已经不再是一个类了,是具有联系的两个对象。

间接说明内部类是个编译时的概念,一旦编译成功后,就与外部类属于两个完全不同但具有一定联系的类。

相知—内部类分类

内部类一共分为4种,分别是:成员内部类、静态内部类、方法内部类和匿名内部类。

成员内部类

成员内部类,顾名思义,内部类作为一个成员而存在于外部类中。它是最普通的内部类,正如前面所见,它可以访问外部类的所有成员属性和方法,哪怕是private修饰的。但是外部类想要访问内部类的成员属性和方法,则需要通过内部类的实例才能进行访问。

现在有一个问题,在成员内部类中是否能使用static关键词?这个static关键词非常魔性,后续会专门出一篇文章来聊聊它。

答案是不能,IDEA提示成员内部类中不能包含任何静态声明。其实很好理解,原因在于成员属性或者方法都是依赖于对象而存在,而静态属性或者方法并不是,它与类有关。

举一个比较典型的成员内部类实例来加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
public class Outer{
private int outerVar = 1;
private int commonVar = 2;
private static int outStaticVar = 3;
//getter和setter方法
/**成员方法**/
public void outerMethod(){
System.out.println("outerMethod");
}
/**静态方法**/
public static void outerStaticMethod(){
System.out.println("outerStaticMethod");
}

/**内部类**/
public class Inner{
/**成员属性**/
private int commonVar = 102;

/**无参构造方法**/
public Inner(){};
/**成员方法,用来访问外部类的属性和方法**/
public void show(){
//当内部类和外部类属性同名时,默认调用的是内部类的属性
System.out.println("内部类的commonVar属性值:"+commonVar);
//当内部类和外部类属性同名时,获取同名外部类属性(外部类名.this.同名属性)
System.out.println("外部类的commonVar属性值:"+Outer.this.commonVar);
//内部类访问外部类的成员属性
System.out.println("外部类的outerVar属性值:"+outerVar);
//内部类访问外部类的静态属性
System.out.println("外部类的outStaticVar属性值:"+outStaticVar);
//内部类访问外部类的成员方法
outerMethod();
//内部类访问外部类的静态方法
outerStaticMethod();
}
}

public static void main(String[] args){
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.show();
}
}

运行结果:

1
2
3
4
5
6
内部类的commonVar属性值:102
外部类的commonVar属性值:2
外部类的outerVar属性值:1
外部类的outStaticVar属性值:3
outerMethod
outerStaticMethod

既然是作为外部类的成员而存在,那么其他的类是否也能访问呢?定义一个Other类,里面的代码为:

1
2
3
4
5
6
7
public class Other {
public static void main(String[] args){
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.show();
}
}

肯定是没有问题,但是是否觉得代码重复太明显了,因此合适的做法是在Outer类中定义一个get方法用于获取其成员内部类,特别是在内部类构造方法无参的情况下:

1
2
3
4
//用于得到一个内部类对象
public Inner getInner(){
return new Inner();
}

当然有参构造方法也不麻烦,作为参数传进去即可。

成员内部类小结。通过前面的介绍,可以知道成员内部类具有以下5个特点:

  • 成员内部类作为外部类的成员而存在,可以被任意修饰符修饰;
  • 成员内部类可以直接访问外部类的所有属性和方法,哪怕是private/static修饰的;
  • 成员内部类依赖于外部类,因此内部类对象的创建必须依赖于外部类对象;
  • 成员内部类中不能包含任何静态声明;
  • 成员内部类可以与外部类属性、方法重名,但默认调用的是内部类属性/方法。如想访问外部类属性/方法,可以使用外部类名.this.同名属性来访问。

静态内部类

顾名思义,静态内部类就是被static修饰的内部类,既然被static修饰,说明它比较特殊。顺便提一下static可以修饰成员变量、方法、代码块及内部类,这一点后续会有文章进行介绍。前面也说过被static修饰的东西都不依赖对象而存在,而是依赖于类。

在前面介绍成员内部类时,说了这么一句话:成员内部类对象的创建需要依赖于外部类对象。但是现在要谈的静态内部类对象的创建就不依赖外部类对象,也就意味着静态内部类中不能使用外部类中任何非static修饰的变量/方法。

同样举一个比较典型的静态内部类实例来加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
public class Outer {
private int outerVar = 1;
private static int commonVar = 2;
private static int outStaticVar = 3;
//getter和setter方法
/**成员方法**/
public void outerMethod(){
System.out.println("outerMethod");
}
/**静态方法**/
public static void outerStaticMethod(){
System.out.println("outerStaticMethod");
}
/**静态代码块**/
static {
System.out.println("out static block");
}

/**静态内部类**/
public static class StaticInner{
/**成员属性**/
private int innerVar = 101;
/**静态属性**/
private static int commonVar = 102;
private static int innerStaticVar = 103;
/**静态代码块**/
static {
System.out.println("inner static block");
}
/**成员方法**/
public void show(){
//当静态内部类和外部类属性同名时(只能是static属性),默认调用内部类的静态属性
System.out.println("内部类的commonVar属性值:"+commonVar);
//当静态内部类和外部类属性同名时(只能是static属性),获取同名外部类属性(外部类名.同名属性)
System.out.println("外部类的commonVar属性值:"+OuterA.commonVar);
//静态内部类访问自身成员属性
System.out.println("内部类的innerStaticVar属性值:"+innerStaticVar);
//静态内部类访问外部类的静态属性
System.out.println("外部类的outStaticVar属性值:"+outStaticVar);
//静态内部类访问外部类的静态方法
outerStaticMethod();
}
/**静态方法**/
public static void innerStaticMethod(){
System.out.println("innerStaticMethod");
}
}
public static void main(String[] args){
System.out.println(StaticInner.innerStaticVar);
StaticInner.innerStaticMethod();
new StaticInner().show();
}
}

猜猜看运行结果:(仔细体会一些里面各个组件的执行顺序)

1
2
3
4
5
6
7
8
9
out static block
inner static block
103
innerStaticMethod
内部类的commonVar属性值:102
外部类的commonVar属性值:2
内部类的innerStaticVar属性值:103
外部类的outStaticVar属性值:3
outerStaticMethod

这里需要注意的一点就是在静态内部类,准确来说是被static修饰的java组件(属性、方法、代码块,内部类)而言,其内部都不能使用this和super,因为它缺少一个隐式的this和super,被static修饰的组件与对象无关,仅与类有关。

同样试试其他的类是否可以访问某外部类中的静态内部类,定义一个Other类,里面的代码为:

1
2
3
4
5
6
7
8
public class Other {
public static void main(String[] args){
//访问某外部类中静态内部类的静态方法时,静态内部类被加载,请注意此时外部类未被加载
Outer.StaticInner.innerStaticMethod();
//访问某外部类中静态内部类的成员方法
new Outer.StaticInner().show();
}
}

运行结果为:

1
2
3
4
5
6
7
8
inner static block  >>>>>>>看到没有外部类的静态代码块没有被初始化
innerStaticMethod
内部类的commonVar属性值:102
out static block
外部类的commonVar属性值:2
内部类的innerStaticVar属性值:103
外部类的outStaticVar属性值:3
outerStaticMethod

请注意,当你直接访问静态内部类中的静态方法时,由于它不依赖与外部类,因此外部类是不会被调用的,可从外部类的静态代码块没有被初始化这一点得到证明。

静态内部类小结。通过前面的介绍,可以知道静态内部类具有以下5个特点:

  • 静态内部类内部可以包含任意信息;
  • 静态内部类只能访问外部类的static属性/方法;
  • 静态内部类可以独立存在,不依赖于其外部类;
  • 可通过外部类名.内部类名.static属性/方法来直接访问内部类的static属性/方法;
  • 静态内部类可以与外部类被static修饰的属性、方法重名,但默认调用的是内部类的属性/方法。如想访问外部类被static修饰的属性/方法,可以使用外部类名.同名属性来访问。

局部内部类

顾名思义,局部内部类就是定义在方法和作用域中,一般用来解决较为复杂的问题。既然是局部内部类,那么它就不能被任何访问修饰符修饰,且只能在该定义的方法或作用域中使用。注意局部内部类中不能使用static关键字。

同样举一个比较典型的局部内部类实例来加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
public class OuterB {
private int outerVar = 1;
private int commonVar = 2;
private static int outStaticVar = 3;
//getter和setter方法
/**成员方法**/
public void outerMethod(){
System.out.println("outerMethod");
}
/**静态方法**/
public static void outerStaticMethod(){
System.out.println("outerStaticMethod");
}

/**外部类的main方法**/
public static void main(String[] args){
OuterB outerB = new OuterB();
outerB.outerCreateMethod("1234");
}

/**成员方法,内部定义局部内部类**/
public void outerCreateMethod(String password){
/**方法内定义的变量**/
Boolean flag = true;

/**局部内部类,注意前面不能使用访问修饰符**/
class Inner{
/**局部内部类成员属性**/
private int innerVar = 101;
private int commonVar = 102;

/**局部内部类成员方法**/
public void show(){
//当局部内部类和外部类属性同名时,默认调用局部内部类的同名属性
System.out.println("局部内部类的commonVar属性值:"+commonVar);
//当局部内部类和外部类属性同名时,获取同名外部类属性(外部类名.this.同名属性)
System.out.println("外部类的commonVar属性值:"+OuterB.this.commonVar);
//局部内部类访问自身成员属性
System.out.println("局部内部类的innerVar属性值:"+innerVar);
//局部内部类访问外部类的成员属性
System.out.println("外部类的outerVar属性值:"+outerVar);
//局部内部类访问外部类的静态属性
System.out.println("外部类的outStaticVar属性值:"+outStaticVar);

//局部内部类访问定义它的方法内的参数
System.out.println("outerCreateMethod方法中传入的参数password:"+password);
//局部内部类访问定义它的方法内的局部变量
System.out.println("outerCreateMethod方法中定义的flag:"+flag);

//局部内部类访问外部类的静态方法
outerStaticMethod();
//局部内部类访问外部类的成员方法
outerMethod();
}
}

/**只能在局部内部类定义的方法内才能访问到它**/
Inner inner = new Inner();
System.out.println("局部内部类的commonVar属性值:"+inner.commonVar);
System.out.println("局部内部类的innerVar属性值:"+inner.innerVar);
inner.show();
}
}

猜一猜运行结果:

1
2
3
4
5
6
7
8
9
10
11
局部内部类的commonVar属性值:102
局部内部类的innerVar属性值:101
局部内部类的commonVar属性值:102
外部类的commonVar属性值:2
局部内部类的innerVar属性值:101
外部类的outerVar属性值:1
外部类的outStaticVar属性值:3
outerCreateMethod方法中传入的参数password:1234
outerCreateMethod方法中定义的flag:true
outerStaticMethod
outerMethod

通过上面的例子,大家都知道了,局部内部类只能在定义它的方法或者作用域内使用,同时局部内部类的前面不能有任何访问修饰符。除此之外,其余的用法和成员内部类非常相似,因此完全可以认为局部内部类只是把作用范围缩小了,缩到只能在定义它的方法或者作用域内生效的一个“特殊”的成员内部类。

不知你是否注意到局部内部类中这几行代码,它用于输出局部内部类定义所在方法的参数和局部变量(无法修改参数和变量的值),看到下面的写法,甚至觉得局部内部类可以直接访问方法内的局部变量和参数???没错,但是有条件。

1
2
3
4
//局部内部类访问定义它的方法内的参数
System.out.println("outerCreateMethod方法中传入的参数password:"+password);
//局部内部类访问定义它的方法内的局部变量
System.out.println("outerCreateMethod方法中定义的flag:"+flag);

什么条件呢?前面设置flag为true,但是后面你修改了,将其设置为false:

1
2
3
/**方法内定义的变量**/
Boolean flag = true;
flag = false;

这时候局部内部类抛出异常:

IDEA的提示是说”flag变量从内部类中访问,需要final或有效的final”。言外之意:如果局部内部类想访问定义它的方法内的局部变量,那么这个变量要么前面使用final修饰,要么第一次赋值后不再修改(引用类型是指向不能改变)。特别注意JDK1.8之前(不含JDK1.8)只能访问被final修饰的变量,也就是只有前一种方式,后面这种方式是后来添加的。对于这种特殊的变量限制,在局部内部类访问定义它的方法的参数时,也同样适用。

局部内部类小结。通过前面的介绍,可以知道局部内部类具有以下6个特点:

  • 局部内部类只能定义在方法或者作用域内;
  • 局部内部类前不能有任何访问修饰符;
  • 局部内部类中不能包含任何静态声明;
  • 当被定义的方法/作用域中的参数、变量前有final修饰或者参数、变量的值在赋值后不再修改时,局部内部类可以直接访问被定义的方法/作用域中的参数、变量,但是无法修改;
  • 局部内部类可以直接访问外部类的所有属性和方法,哪怕是private/static修饰的;
  • 局部内部类可以与外部类的属性、方法重名,但默认调用的是局部内部类的属性/方法。如想访问外部类的属性/方法,可以使用外部类名.this.同名属性来访问。

匿名内部类

顾名思义,匿名内部类就是没有名字的内部类,既然没有名字,那么就无法使用访问修饰符,也没有构造方法。匿名内部类用的比较多,因此对于它的研究就显得尤为重要。其实你完全可以认为匿名内部类是一个没有名字的局部内部类。

匿名内部类的创建格式为:

1
2
3
4
new 父类构造器(参数列表)|| 实现接口()  
{
//匿名内部类的类体部分
}

新手可能第一眼还没明白怎么回事,就创建了一个匿名内部类。从这个创建格式可以看出匿名内部类必须继承一个类或者实现一个接口。匿名内部类的声明不能使用class关键字,而是直接使用new来生成一个对象的隐式引用。

匿名内部类的用法较为特殊,举一个比较典型的实例来加深理解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package com.envy.inner;

public class OuterC {

public interface Fruit{
void eat();
};

public static Fruit getFruit(String description){
Boolean flag = true;
return new Fruit() {
@Override
public void eat() {
System.out.println("获取被定义类的参数信息:"+description);
System.out.println("获取被定义类的局部变量:"+flag);
}
//注意后面一行的分号不能少!
};

}

public static void main(String[] args){
/**外部类调用匿名内部类中的方法**/
OuterC.getFruit("真香").eat();
}
}

运行结果:

1
2
获取被定义类的参数信息:真香
获取被定义类的局部变量:true

从上面的代码中可以知道几点:1、匿名内部类必须继承一个类或者实现一个接口(只能二选一);2、匿名内部类没有类名,所以也没有构造方法;3、匿名内部类没有访问修饰符;4、使用匿名内部类时,new后面的类/接口必须存在,其次可能要重写new之后类/接口的某个或某些方法;5、匿名内部类访问方法参数/变量时,同样也存在和局部内部类一样的访问限制;6、匿名内部类不能是抽象类,因此必须实现继承类或者实现接口的所有抽象方法。

相思—访问限制思考

现在来思考一下为何在局部内部类(包含匿名内部类)中访问定义它的方法的参数或者局部变量时,会有一个访问限制?即要求这个局部变量(方法参数要)要么前面使用final修饰,要么第一次赋值后不再修改(引用类型是指向不能改变)。便于说明这里统一为必须使用final关键词修饰。

如果开发者不按照要求进行设置,那么强行访问IDEA就会提示图片所示异常:

便于研究,这里提供一个测试了,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class OuterD {
public interface Fruit{
void eat();
}

public Fruit getFruit(String name){
boolean flag = true;
return new Fruit() {
@Override
public void eat() {
System.out.println("parameter:"+name);

System.out.println("local variable:"+flag);
}
};
}
}

先尝试使用javac *.java命令编译一下这个测试类,由于其中包含一个接口,因此可以猜猜最后会编译出三个class字节码文件:

OuterD.class文件是外部类编译后生成的文件,查看一下其中的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.envy.test;

public class OuterD {
public OuterD() {
}

public OuterD.Fruit getFruit(final String var1) {
final boolean var2 = true;
return new OuterD.Fruit() {
public void eat() {
System.out.println("parameter:" + var1);
System.out.println("local variable:" + var2);
}
};
}

public interface Fruit {
void eat();
}
}

可以发现编译后外部类内部自动添加了其无参的构造方法,其次对于定义内部类的方法的参数和局部变量前面也都自动添加了final关键词,笔者此处使用的是java1.8,所以也就知道了,为何1.8及以后可以允许局部变量/方法参数第一次赋值后不再修改(引用类型是指向不能改变)也是可能的,其实编译后还是会添加final关键词,只是不再强制要求开发者添加了。且可以发现它通过使用外部类.接口名这种方式对外提供了内部类的访问方式。

再来查看一下OuterD$Fruit.class文件,可以发现其实它就是Fruit接口编译后的文件:

1
2
3
4
5
package com.envy.test;

public interface OuterD$Fruit {
void eat();
}

可以发现编译后在外部类文件中访问内部类信息都是通过.形式,但是在外部类文件外则是使用$,言外之意为只有某个类中包含内部类,编译后才会有$存在。

再来看这个OuterD$1.class文件,显然这个就是匿名内部类编译后的文件,因为它没有名字只能使用1来标识:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.envy.test;

import com.envy.test.OuterD.Fruit;

class OuterD$1 implements Fruit {
OuterD$1(OuterD var1, String var2, boolean var3) {
this.this$0 = var1;
this.val$name = var2;
this.val$flag = var3;
}

public void eat() {
System.out.println("parameter:" + this.val$name);
System.out.println("local variable:" + this.val$flag);
}
}

看到问题的关键了,这里居然存在一个有参的构造方法,第一个参数是var1(外部类对象),第二个参数是被定义方法传入的参数,第三个参数是被定义方法内定义的局部变量的值。

所以匿名内部类并不是直接调用被定义的方法内传递的参数或者局部变量,而是使用自己的构造函数对传入的参数进行了备份,匿名内部类内的方法调用的实际上是自己的属性,并不是外部传进来的参数或者局部变量。(eat方法就说明了这一点)

不过这个似乎好像和必须使用final关键词联系不大,其实不然,前面说了内部类中方法其实调用的是自己的属性,那么就可以对内部类的属性进行修改,而不会影响到外部被定义方法中的方法参数或者局部变量。按照常理来说的确是这样的,但是当笔者尝试在匿名内部类的方法中修改“方法参数或者局部变量”时,IDEA报错了:

其实从开发者角度来说,既然将形参var2赋值给了val$name属性,那么val$name其实就是var2,两者是同一个对象:

1
2
3
4
5
OuterD$1(OuterD var1, String var2, boolean var3) {
this.this$0 = var1;
this.val$name = var2;
this.val$flag = var3;
}

如果方法内的val$name属性发生了变化,相应的形参var2的值也应该跟着变化,但是往往形参var2的值并不会变,因此这就造成了严重的安全问题,所以才规定使用final关键词来规避这一问题的产生。

看到这里也就明白了其实就是一个拷贝问题,Java不存在指针,那么为避免拷贝的值发生变化(如某局部变量被修改了,而内部类拷贝的值没有跟着变化,这样就造成了内外值不一致的情况),于是引入final关键词来保证该值永远不会变化。

有人可能有这么一个疑问,明明在被定义方法内修改的局部变量是作为一个形参传入到匿名内部类之中,形参变了,它赋值给内部类中的属性,这个属性怎么不会跟着变化呢?

为了解决这个疑问,下面采用Debug来一步步进行调试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.envy.inner;

public class OuterD {
public interface Fruit{
void eat();
}

public Fruit getFruit(String name){
boolean flag = true;
return new Fruit() {
@Override
public void eat() {
System.out.println("parameter:"+name);
System.out.println("local variable:"+flag);
}
};
}

public static void main(String[] args){
OuterD outerD = new OuterD();
Fruit fruit= outerD.getFruit("envy");
fruit.eat();
}
}

执行顺序如下:

仔细看执行顺序,说明getFruit方法是在eat方法前执行的,也就是eat方法中是直接保存了getFruit方法中对象的值。请注意这个匿名内部类中的eat方法是可以被Fruit对象所调用,不过晚于getFruit方法的执行罢了,getFruit方法都执行完了,保存在栈中的局部变量的生命周期也结束了。

需要注意的是,只有匿名内部类访问的方法参数或者局部变量前才需要添加final或者首次赋值后不再进行修改,对于没有访问的就没有这个限制了:

相恋—没有构造方法也能初始化

在前面多次提到,由于匿名内部类没有名字,因此没有构造方法,那么它是否也能初始化呢?答案是肯定的,可以使用构造代码块!举一个比较典型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.envy.inner;

public class OuterE {

public interface Fruit{
double getFruitPrice();
String getFruitName();
};

public Fruit getFruit(final String name, final double price){
return new Fruit() {
String fruitName;
double fruitPrice;
{
fruitName = name;
fruitPrice = price;
}
public double getFruitPrice() {
return fruitPrice;
}
public String getFruitName(){
return fruitName;
}
};
}

public static void main(String[] args){
OuterE outerE = new OuterE();
Fruit fruit = outerE.getFruit("苹果",2.8);
System.out.println(fruit.getFruitPrice());
System.out.println(fruit.getFruitName());
}
}

运行结果:

1
2
2.8
苹果

请注意这里的Fruit接口中必须定义getFruitPricegetFruitName方法,否则你无法直接通过fruit.getFruitPrice()等形式来调用匿名内部类中属性的getter方法,因为匿名内部类没有名字,所以无法来调用它的方法,只能借助于它实现类的同名方法的实现来完成方法调用。

那么关于内部类就先介绍到这里,里面涉及到的知识点还是挺多的,需要好好复习和消化。

参考文章:详解内部类浅谈Java内部类,感谢大佬们的指点。