欢迎跳转到本文原文链接 Backend_Notes
内部类(inner class)是定义在另一个类中的类,需要使用内部类的原因有三个:
- 内部类方法可以访问该类定义所在作用域中的数据,包括私有的数据
- 内部类可以对同一个包中的其他类隐藏起来
- 当想要定义一个回调函数且不想编写大量代码时,使用匿名(anonymous)内部类比较便捷
使用内部类访问对象状态
我们可以看以下的例子:
public class InnerClassTest2 {
public static void main(String[] args) {
Student student = new Student("Alice", 18);
student.test();
}
}
class Student {
private String name;
private int age;
public Student(String name, int age) {
this.name = name;
this.age = age;
}
public void test() {
InnerClass innerClass = new InnerClass();
innerClass.print();
}
public class InnerClass {
public void print() {
System.out.println("name: " + name + ", age: " + age);
}
}
}
// output
// name: Alice, age: 18
观察上述代码的输出结果可知,内部类可以访问该类定义所在作用域中的数据,包括私有数据。内部类的对象有一个隐式引用,它引用了实例化该内部对象的外围类对,通过这个指针,可以访问外围类对象的全部状态,但是 static
内部类没有这种附加指针。
外围类的引用在内部类的定义中是不可见的,我们可以将这个引用称为 outer
,外围类的引用在构造器中设置,编译期修改了所有内部类的构造器,添加了一个外围类引用的构造函数,若没有定义该构造器,则编译器会为这个类生成一个默认的构造器,代码如下:
public class InnerClass {
// automatically gengerated code
public InnerClass(Student student) {
outer = student;
}
public void print() {
System.out.println("name: " + name + ", age: " + age);
}
}
当我们调用 student.test()
方法时,编译器会将 this
引用传递给当前的语音时钟构造器。
InnerClass innerClass = new InnerClass(this); // parameter automatically added
注意:
InnerClass 可以声明为私有的,这样只有 Student 的方法可以构造 InnerClass 对象,只有内部类可以是私有类,其他常规类只可以是包可见性或公有可见性。
内部类的特殊语法规则
上文提到内部类有一个外围类的引用 outer
,使用该引用的表达式为: OuterClass.this
,上述代码我们可以改为:
public class InnerClass {
public void print() {
System.out.println("name: " + Student.this.name + ", age: " + Student.this.age);
}
}
反过来我们也可以使用如下语法来更明确地编写内部对象的构造器:
outerObject.new InnerClass(construction parameters)
public void test() {
InnerClass innerClass = this.new InnerClass();
innerClass.print();
}
若内部类 Innerclass 是公有的,此时在外围类的作用域外可以这样创建内部类,在外围类的作用域之外,可以这样引用内部类: OuterClass.InnerClass
Student.InnerClass inner = student.new InnerClass();
注意:内部类不能有静态变量和静态方法。
局部内部类
我们也可以使用局部内部类,代码如下:
public class InnerClassTest3 {
public static void main(String[] args) {
Person p = new Person("Alice", 12);
p.test();
}
}
class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void test() {
class InnerClass {
public void print() {
System.out.println("name: " + name + ", age: " + age);
System.out.println("name: " + Person.this.name + ", age: " + Person.this.age);
}
}
InnerClass innerClass = new InnerClass();
innerClass.print();
}
}
// output
// name: Alice, age: 12
// name: Alice, age: 12
局部类不能使用 public 或 private 访问说明符进行声明,它的作用域被限定在声明这个局部类的块中,局部类有一个优势:即对外部世界可以完全隐藏起来,即时是 Person 类中的其他代码也不能访问它。除了 test 方法,没有任何方法知道它的存在
由外部方法访问变量
局部类不仅能够访问他们的外部类,还可以访问局部变量,不过这些局部变量必须事实上为 final, 这说明它们一旦被赋值就不会被改变,来看代码示例:
public class InnerClassTest4 {
public static void main(String[] args) {
Person2 p = new Person2("Alice", 12);
p.test("hello");
}
}
class Person2 {
private String name;
private int age;
public Person2(String name, int age) {
this.name = name;
this.age = age;
}
public void test(String parameter) {
String world = "world";
class InnerClass {
public void print() {
System.out.println(parameter);
System.out.println(world);
}
}
InnerClass innerClass = new InnerClass();
innerClass.print();
}
}
// output
// hello
// world
注意:
- 为了使局部内部类中的方法可以正常使用,parameter 会使用 test 方法的局部域进行备份
- 在 JDK8 之前,必须把局部类访问的局部变量声明为 final,JDK8 之后不用,但是通过反编译可以发现,编译器自动给变量加上了 final
匿名内部类
将局部内部类再进一步,此时我们可以使用匿名内部类,通用的语法格式为:
new SuperType(construction parameters) {
// inner class methods and data
}
SuperType
可以是接口,那么内部类就实现这个接口, 也可以是类,那么内部类就要扩展(继承)它。由于构造器的名字必须和类名相同,而匿名类没有类名,所以,匿名类没有构造器,只能使用超类的构造器,尤其当内部类实现接口的时候不能有任何构造参数。
匿名类一般用在事件监听器与其他回调,如:
class TalkingClock
{
/**
* Starts the clock.
* @param interval the interval between messages (in milliseconds)
* @param beep true if the clock should beep
*/
public void start(int interval, boolean beep)
{
ActionListener listener = new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
}
};
Timer t = new Timer(interval, listener);
t.start();
}
}
JDK8 后,我们更多地使用 lambda 表达式,上述代码可以转化为:
class TalkingClock
{
/**
* Starts the clock.
* @param interval the interval between messages (in milliseconds)
* @param beep true if the clock should beep
*/
public void start(int interval, boolean beep)
{
Timer t = new Timer(interval, event -> {
System.out.println("At the tone, the time is " + new Date());
if (beep) Toolkit.getDefaultToolkit().beep();
});
t.start();
}
}
双括号初始化(double brace initialization)
利用内部类可以实现集合的初始化,一般初始化集合时会使用如下代码:
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
test(list);
如果集合以后不会用到,我们可以使用匿名列表:
print(new ArrayList<Integer>(){{
add(1);
add(2);
add(3);
}});
注意这里用到了双括号,外层括号表示是建立了一个 ArrayList 的匿名类,里面一层括号为对象构造块,进行初始化。
静态内部类
有时候只是想要把一个类隐藏在一个类中,不需要内部类引用外部对象,此时可以使用静态内部类,取消外部对象的引用。
public class StaticInnerClassTest2 {
public static void main(String[] args) {
double[] nums = {1.1, 2.2, 3.3, 4.4, 5.5};
ArrayAlgo.Pair p = ArrayAlgo.minmax(nums);
System.out.println("Max: " + p.getSecond() + ", Min: " + p.getFirst());
}
}
class ArrayAlgo {
public static class Pair {
private double first;
private double second;
public Pair(double first, double second) {
this.first = first;
this.second = second;
}
public double getFirst() {
return first;
}
public double getSecond() {
return second;
}
}
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);
}
}
// output
// Max: 5.5, Min: 1.1
注意:
- 与常规内部类不同,静态内部类可以有静态域和静态方法
- 声明在接口中的内部类自动成为 static 和 public
Reference
1.Java核心技术卷一