引言
内部类是定义在另一个类中的类,它曾经对于简洁地实现回调非常重要,不过今天lambda表达式在这方面可以做的更好。但内部类对于某些结构的构建还是很有用的。
普通内部类
常规的内部类有以下两个特点:
1.内部类可以对除了自己的外部类以外的其他类隐藏。
2.内部类方法可以访问其外部类的字段,包括私有字段(默认拥有一个对外部类的引用)。
public class InnerClassTest {
public static void main(String[] args) {
Outer outer = new Outer(1);
Outer.Inner inner = outer.new Inner();
inner.printA();
}
}
class Outer{
private int a;
public Outer(int a) {
this.a = a;
}
class Inner{
public void printA(){
System.out.println(a);
}
}
}
上面的例子中,我们可以看到,内部类Inner的方法printA中可以直接访问其外部类的实例字段。
而在main函数中我们发现,在调用这个内部类的方法之前有两步操作:
1.创建一个外部类对象。
2.使用这个外部类对象new一个内部类对象。
这也就可以看出内部类的另外一个特性:
一个内部类对象的存在必须基于一个外部类对象,而外部类对象是可以独立存在的。
备注:内部类不能有static方法,看似内部类定义一个只能访问外部类静态字段和静态方法的方法是符合逻辑的,但java的设计者认为这只是徒增语言的复杂性,并没有选择这么做。
私有的内部类
内部类与普通的类不同,它可以标记为private,这意味着只有它的外部类对象可以通过其方法去创建一个私有内部类。
public class InnerClassTest {
public static void main(String[] args) {
Outer outer = new Outer(1);
outer.useInnerPrintA();
}
}
class Outer{
private int a;
public Outer(int a) {
this.a = a;
}
public void useInnerPrintA(){
Inner inner = new Inner();
inner.printA();
}
private class Inner{
public void printA(){
System.out.println(a);
}
}
}
内部类的特殊语法规则
1.在内部类中调用外部类的字段和方法时,有一个更为完整的方法:OuterClass.this.parameter
当内部类和外部类中有重名变量时,这种方式也可以明确和区分两个同名的变量。
2. 可以使用一个完整版的构造器:outerObject.new InnerClass(construction parameters)
3.在内部类的作用域之外,还可以这样引用内部类成员:OuterClass.InnerClass.parameter
public class InnerClassTest {
public static void main(String[] args) {
Outer outer = new Outer(1);
// 使用格式 outerObject.new InnerClass(construction parameters)
Outer.Inner inner = outer.new Inner();
inner.printA();
// 使用格式 OuterClass.InnerClass.parameter 来引用内部类
System.out.println(Outer.Inner.a);
}
}
class Outer{
private int a;
public Outer(int a) {
this.a = a;
}
public void useInnerPrintA(){
// 用如下方式改写可以增加明确性
Inner inner = this.new Inner();
inner.printA();
}
class Inner{
static int a = 2;
public void printA(){
// 如果有重名变量,该名称会指向内部类变量
System.out.println(a);
// 使用 OuterClass.this.parameter 格式来指代外部对象
System.out.println(Outer.this.a);
}
}
}
局部内部类
当内部类只被某个方法使用了一次时,可以在这个方法内定义这个内部类,这样的内部类就称为局部内部类。
就像它的名字一样,局部内部类的作用域固定在这个方法内,因此不必也不能为其添加访问修饰符,如public、private。
class Outer{
private int a;
public Outer(int a) {
this.a = a;
}
public void useInnerPrintA(){
class Inner{ //局部内部类
public void printA(){
System.out.println(a);
}
}
Inner inner = new Inner();
inner.printA();
}
}
局部内部类可以像一般的内部类一样访问它外层的字段,这里不仅包括外部类的字段,也包括它所在的方法的局部变量,但需要注意,它访问的局部变量必须是事实最终变量才可以,也就在事实上是不会改变的变量。
class Outer{
private int a;
public Outer(int a) {
this.a = a;
}
public void useInnerPrintA(int b){
// 此处的b和c都是局部字段
// 若局部内部类引用了它们,则不可被更改
int c = 3;
class Inner{
public void printA(){
System.out.println(a);
System.out.println(b);
System.out.println(c);
a++; // 合法
b++; // 不合法
c++; // 不合法
}
}
Inner inner = new Inner();
inner.printA();
}
}
局部内部类和lambda表达式在这方面是一样的,笔者认为它们是近亲,它们在离开了其所在的方法体之后,该方法体所使用的栈内存被释放,但局部内部类和lambda表达式内部所使用过的这些局部字段会连同它们一起,被持久的保留在堆内存中,这些保留在堆内存中的它们可能会在此后任意的时刻被调用和访问。
上述案例中的变量a并不限制为事实最终变量,原因很明显,因为它本来就是外部类对象的实例字段,并不会因为方法体的结束而消亡,与上述过程不符。
public class TimerTest
{
public static void main(String[] args)
{
int start = 10;
// 此处的lambda表达式引用了外部字段start,不得被修改
var timer1 = new Timer(1000,
e -> System.out.println(start--)); //不合法
}
}
如果不是事实最终变量,那么在多线程环境下就会产生不可预测性和严重的安全性问题,所以java的设计者为了安全考虑,要求这些外部的局部字段必须是事实最终变量。
匿名内部类
你可能以前了解过匿名内部类,它往往是用于临时创建某个实现了特定接口的对象。但事实上,它既可以用于一个必须要实现某个或者某些方法的接口类、抽象类,也可以用于一个普通类。
基于类的方法重写
当你仅仅想要创建某个类的一个对象,并且希望它有特殊的动作时,你可以使用匿名内部类去完成这个动作,但特殊的动作一般仅限于对已有的方法进行重写才有意义。
你甚至可以为这个特殊的对象添加只属于它的字段和方法,但你根据stu这个变量去调用它们的时候却是做不到的,因为编译器只会将stu这个变量当做是一个普通的Student对象。(具体请见下方代码)
public class InnerClassTest {
public static void main(String[] args) {
Student stu = new Student(18) {
// 无意义的操作(可以通过编译,但不能使用)
String name = "Bob";
// 无意义的操作(可以通过编译,但不能使用)
public void print() {
System.out.println("haha");
}
@Override
public String toString() {
return "Student age:" + getAge();
}
};
System.out.println(stu);
//以下两句无法通过编译
System.out.println(stu.name);
stu.print();
}
}
class Student{
private int age;
public Student(int age) {
this.age = age;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
可以看到,在上述代码中,stu所指向的对象拥有仅属于它自己的toString方法;而新定义的print方法是不可用的,编译器仅将它当做是一个普通的Student对象,不能识别到这个新定义的方法,因此也就不能使用它。
基于接口的方法实现
在上面的情况之外,也是更通常的情况下,你的匿名内部类会建立在一个接口类的实现上,那么就要提供一个接口的空构造器,注意这里的小括号是不能缺省的:
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
public class InnerClassTest {
public static void main(String[] args) {
// 使用new构造了一个接口的对象,此处小括号不能省略
Timer timer = new Timer(1000, new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("每隔一秒执行一次");
}
});
timer.start();
while (true){}
}
}
上面这个例子你不需要了解它的全部细节,我们仅用它来展示一个基于接口实现的匿名内部类,你只需要大概的知道是什么意思就好了。
要求外部变量是事实最终变量
匿名内部类不仅跟lambda表达式是近亲,甚至几乎就是一个东西,所以跟上面所述的内容一样,它所引用的外部变量也要求是事实最终变量。
public class Example {
public void doSomething() {
int x = 10;
// 匿名内部类中引用 x,x 必须是事实上最终变量
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println(x);
}
};
x = 20; // 这里修改 x 的值,将导致编译错误
}
}
双括号初始化
尽管匿名类它本身不能持有构造器,因为它只是用于创建另外一个类的特殊对象,但却可以为其提供一个用于对象初始化的代码块,基础牢固的同学应该对类的静态代码块和普通代码块有所了解,添加普通代码块可以让这个类的对象初始化时执行一些语句:
List<String> list = new ArrayList<>() {
{
add("Tom");
add("Bob");
}
// 重写方法
};
你可以用这种奇特的方式去初始化这个对象,可简写为以下形式:
List<String> list = new ArrayList<>() {{add("Tom"); add("Bob");}};
但事实上并不建议大家用这种方式初始化一个对象,因为对不了解这种罕见语法的人而言,这种写法的可读性很差,虽然你可以写注释,但你绝对有更清晰的方式去做这件事。
静态内部类
内部类可以声明为static,静态的内部类相当于是一个嵌套的类,它跟普通类只有一个区别,就是它隐藏在了另外一个类的内部,它最大的好处就是避免声明一个跟其他类重名的类。
就跟上面说的一样,静态内部类跟写在外面的普通类并无差别,它既没有对其外部类的引用,还与常规内部类不同,可以有自己的静态字段和方法,就跟普通类完全一样。
声明和构造一个静态内部类也想当简单,格式如下:
OuterClass.InnerClass obj = new OuterClass.InnerClass();
下面给到一个《java 核心技术》一书中给到的静态内部类的使用例子,在这个例子中,我们创建了一个静态内部类Pair,用于存放一组数据,这个类名通常容易跟其他类重名,使用静态内部类就避免了这个情况:
/**
* This program demonstrates the use of static inner classes.
* @version 1.02 2015-05-12
* @author Cay Horstmann
*/
public class StaticInnerClassTest
{
public static void main(String[] args)
{
var values = new double[20];
for (int i = 0; i < values.length; i++)
values[i] = 100 * Math.random();
ArrayAlg.Pair p = ArrayAlg.minmax(values);
System.out.println("min = " + p.getFirst());
System.out.println("max = " + p.getSecond());
}
}
class ArrayAlg
{
/**
* A pair of floating-point numbers
*/
public static class Pair
{
private double first;
private double second;
/**
* Constructs a pair from two floating-point numbers
* @param f the first number
* @param s the second number
*/
public Pair(double f, double s)
{
first = f;
second = s;
}
/**
* Returns the first number of the pair
* @return the first number
*/
public double getFirst()
{
return first;
}
/**
* Returns the second number of the pair
* @return the second number
*/
public double getSecond()
{
return second;
}
}
/**
* Computes both the minimum and the maximum of an array
* @param values an array of floating-point numbers
* @return a pair whose first element is the minimum and whose second element
* is the maximum
*/
public static Pair minmax(double[] values)
{
double min = Double.POSITIVE_INFINITY;
double max = Double.NEGATIVE_INFINITY;
for (double v : values)
{
if (min > v) min = v;
if (max < v) max = v;
}
return new Pair(min, max);
}
}
下面的两条默认行为不一定是必须的,但既然有默认设定,说明正常情况下一般这样更有意义:
在接口中声明的内部类自动是public和static的。
类中声明的接口(包括记录类、枚举类)都自动是static的。
后记
这里可以提到一个词:闭包。
闭包是指一个连同了外部自由变量的代码块,java的内部类和lambda表达式就具备这样的特点,有兴趣的读者可以自行查阅其他资料了解闭包的概念,如果有人跟你炫耀他所写的语言中有闭包,那么你可以自信的告诉他,java语言也有闭包。