【JAVA进化论】LV3-2:内部类

你知道吗?java里允许一个类的声明放在另外一个类的域里面(再次套娃),如果一个类被声明在了另外一个类里面,那么它就是一个内部类,内部类按照自己的修饰符和出现的区域不同,又分成了以下种类:

图1

如图,我们把包含内部类的普通类叫做外部类。内部类又按照静态非静态做了区分,非静态内部类又细分了三类。下面,让我们来走进内部类。

⚠️ 注意:本节内容默认你已经对类有很深入的理解,因此本节的例子类命名不再像前几节那么“讲究”,为了方便说名问题,我们把内部类以InnerA/B/C的形式命名,外部类以OuterA/B/C的方式命名。

一、静态内部类

1.1:静态内部类的定义&特性

静态内部类是内部类的一个大分类,它和非静态内部类有着很大的差别。静态内部类的定义方式如下:

代码块1
1
2
3
4
5
6
7
8
public class OuterA {

//在OtherA这个类的域内,定义另外一个使用static修饰的类,这个类就被称作OtherA的静态内部类
public static class InnerA {

}

}

静态内部类有哪些特性呢?

事实上静态内部类没有任何与其他类不同的特性,你只需要把它当成平时的类即可,只不过它被定义在另外一个类的域里而已,它的实例化方式需要这样做:

代码块2
1
2
3
4
public static void main(String[] args) {
OuterA outerA = new OuterA(); //外部类的实例化跟普通类没有任何区别
OuterA.InnerA innerA = new OuterA.InnerA(); //内部类的实例化方式
}

此外,普通类的定义里,只能把类定义成public类或者干脆不写权限修饰符,且public修饰的类被称为公开类,要求每个java文件里最多只能存在一个公开类,且类名必须要和java文件名一致,但是在静态内部类的世界,是允许加任意权限修饰符的,且限制跟之前讲的那张表中一致:

代码块3
1
2
3
4
5
6
7
public class OuterA {

//静态内部类的访问权限修饰符可以是public、protected、default、private里的任意一个
private static class InnerA {

}
}

图2

但是改成protected或default后,可以引用并访问。还有一点,外部类是可以无视静态内部类里资源的访问权限修饰符的,也就是说,外部类甚至可以访问静态内部类的private级别的内容。

静态内部类没什么好说的,你可以理解只是一个类被定义在了另外一个类里头,此外它跟普通类的用法没有任何区别。

1.2:静态内部类的意义

如果你认真看了1.1,你会发现,既然这东西跟普通类没什么区别,为什么它还要存在呢?是的,确实定义一个普通类避免了静态内部类那么奇怪的实例化语法,更加利于理解,静态内部类存在就只是java支持这种写法而已,一般情况下,当类B仅会被类A使用,不存在别的地方使用它的情况,这时就建议将类B定义为类A的静态内部类使用,这时访问权限建议设置为private ←这是较官方的说法,其实java支持这种规则,在实际开发中往往有更多种多样的写法,比如某些情况下封装成一个负责收集外部类各种属性的内部类:

代码块4
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
public class Student {

private int id;
private int age;
private String name;

private Student(int id, int age, String name) {
this.id = id;
this.age = age;
this.name = name;
}

public static Builder newBuilder() {
return new Builder(); //定义一个可以返回内部类对象的外部静态方法
}

public static class Builder {
private int id;
private int age;
private String name;

public Builder id(int id) {
this.id = id;
return this;
}

public Builder age(int age) {
this.age = age;
return this;
}

public Builder name(String name) {
this.name = name;
return this;
}

public Student build() {
return new Student(id, age, name); //静态内部类可以调用外部类的private方法
}
}
}

上面的Builder内部类负责给它的外部类收集属性值,最终通过build构建一个外部类实例,然后Student的构造器被我们搞成private了,因此Student不可以通过构造器的方式进行实例化了,但是却可以通过Builder这个内部类实例化:

代码块5
1
2
3
public static void main(String[] args) {
Student student = Student.newBuilder().id(1).age(12).name("sssss").build(); //←链式调用赋值
}

如果一个对象属性过多,相比那种定义一堆的set、get方法的方式给属性赋值,不如使用这种链式调用的方式来的简洁。

注意,举这个例子的目的不是让你一定要用静态内部类搞这种事情,而是要告诉你,java支持了这种定义方式,你可以利用这种定义方式,来做你自己认为正确的类拆分。

二、非静态内部类

2.1:成员内部类

先来看看成员内部类怎么定义:

代码块6
1
2
3
4
5
6
7
8
public class OuterB {

//在OtherB这个类的域内,定义另外一个非静态的类,这个类就被称作OtherA的成员内部类
public class InnerB {

}

}

成员内部类跟静态内部类就完全不一样了,来看看它是怎么完成实例化的:

代码块7
1
2
3
4
public static void main(String[] args) {
OuterB outerB = new OuterB(); //同样的,外部类的实例化跟普通类没有任何区别
OuterB.InnerB innerB = outerB.new InnerB(); //成员内部类必须依靠外部类的引用变量完成初始化
}

ok,现在来介绍下成员内部类的特性:

  1. 成员内部类不允许拥有非final的静态属性,不允许拥有任意形式的静态方法。
  2. 成员内部类必须通过外部类的引用变量通过new关键词去实例化(必须是外部类本类的引用变量才行,外部类的父类引用变量实例化内部类是不允许的)
  3. 成员内部类可以访问任意外部类的资源(无视访问权限修饰符的访问)

成员内部类的定义跟静态内部类一样拥有权限修饰符,我们一般建议设置成private的,由于非静态内部类的特性,它可以被外部类new出来之后操作外部类里面任意资源,因此它常被用来隔离一些外部类的复杂操作,例如一个外部类过于庞大,功能点很多,你就可以按照不同的功能点设置不同的成员内部类进行逻辑归类。

2.2:局部内部类

局部内部类就是定义在一个方法或者一个作用域里面的类,定义如下:

代码块8
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
public class OuterC {

static {
//静态块里定义的局部内部类
class InnerC1 {

}
InnerC1 innerC1 = new InnerC1(); //只能在静态块内使用
}

public void method() {
final int a = 1;
final int b = 14;
class InnerC2 { //方法体里定义的局部内部类
private int a = 2; //自己内部也可以定义与同域内命名相同的变量
public void m() {
a = 12;
System.out.println(a); //默认使用的a是它自己的
System.out.println(this.a); //跟上面一致,上面只是隐式的this.a,我们发现,如果自己的成员变量跟外面同域内声明的变量命名一致,则再也无法指定外部域里那个变量了
System.out.println(b); //局部内部类可以使用其外面的同域内的数据,但是要求数据必须声明成final修饰的常量
}
}
InnerC2 innerC2 = new InnerC2(); //上面是局部内部类的声明,完成后在下面即可实例化后使用
innerC2.m();
}

public static void method2() {
class InnerC3 { //静态方法体里定义的局部内部类

}
}
}

来总结下局部内部类的特性:

  1. 无法访问任何外部类的非静态的资源
  2. 不允许定义访问权限修饰符
  3. 作为定义在某个作用域内的局部内部类,它可以访问同域的变量,但是同域变量必须加上final关键词
  4. 允许在任意局部域内定义,定义后即可在下方直接使用

目前针对局部内部类做下了解即可,真正开发中极少用到。

2.3:匿名内部类

匿名内部类即无名称的内部类,我们在前面学过抽象类和接口,它们是无法被实例化的,但是我们现在这样做:

step1:定义一个接口,让它拥有两个方法,a和b

代码块9
1
2
3
4
public interface A {
void a();
void b();
}

step2:定义一个抽象类,让它拥有抽象方法c

代码块10
1
2
3
public abstract class B {
abstract void c();
}

step3:定义一个普通的类,让其拥有一个接受A和B类型参数的方法,然后触发abc方法

代码块11
1
2
3
4
5
6
7
public class C {
public void testInner(A a, B b) {
a.a();
a.b();
b.c();
}
}

ok,我们前面讲过,接口和抽象类都是无法被直接实例化的,所以我们在调用testInner这个方法的时候一定是传的A的实现类和B的子类的对象,但我现在告诉你,我没有实现A的类,也没有B的子类,该传什么呢?好的,我们开始引入匿名内部类:

代码块12
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args){
C c = new C();
c.testInner(new A() { //这里new了A,下方是对其的实现代码,这个代码块就是一个匿名内部类,这个类实现了A接口,属于A接口的实现类,这段代码作为匿名内部类被new了出来,赋给了testInner的第一个参数
@Override
public void a() {
System.out.println("实现了A接口的匿名内部类a方法逻辑实现");
}

@Override
public void b() {
System.out.println("实现了A接口的匿名内部类b方法逻辑实现");
}
}, new B() { //同样的,这里new了B,下方是对其抽象方法的重写,这个代码块也是一个匿名内部类,这个类继承了B,属于B的子类,这段代码作为匿名内部类被new了出来,赋给了testInner的第二个参数
@Override
void c() {
System.out.println("实现了B抽象类的匿名内部类c方法逻辑实现");
}
});
}

输出如下:

代码块13
1
2
3
实现了A接口的匿名内部类a方法逻辑实现
实现了A接口的匿名内部类b方法逻辑实现
实现了B抽象类的匿名内部类c方法逻辑实现

可以看到,匿名内部类里的实现逻辑已被成功触发。

这是在干什么呢?为什么好好的实现类和子类你不写,非要用匿名内部类写成这样?其实这是一种偷懒的写法,有些类你不想专门给它写实现和子类的话,别的地方又需要用到它们的对象,那就可以利用匿名内部类来“偷懒”