1. 絮絮叨叨
- 在学习JDK 8的lambda表达式这一新特性时,发现很多文章都是以匿名类作为切入点,从而体现lambda表达式的简洁,或者帮助我们理解lambda表达式的写法
- 在对lambda表达式语法不熟悉的情况下,自己总是尝试先用匿名类实现这个接口,然后再跟lambda表达式一一对应 😂
- 通过学习JDK 8的官方文档,发现原来我所认识的内部类也有这么多讲究:嵌套类、静态嵌套类、非静态嵌套类等
2. 嵌套类
-
所谓嵌套类(Nested Class),就是在一个类中定义的类
-
之前自己一直把这种类直接叫做内部类,原来JDK中的专业术语是嵌套类
class OuterClass { // 外部类 static class StaticNestedClass { // 这是静态嵌套类 // 内容省略 } class NonStaticNestedClass { // 这是非静态嵌套类,又称内部类 // 内容省略 } }
-
根据是否被static关键字修饰,嵌套类分为:静态嵌套类(static nested class)、非静态嵌套类(non-static nested class)
-
非静态嵌套类,又称内部类(inner class)
2.1 为什么要使用嵌套类?
- 这是对仅在一个地方使用的类的逻辑分类方法:
- 类B只在类A中使用,则类B应该被嵌入到类A中。
- 将类B这样的帮助类(helper class)进行嵌套定义,可以让包更加精简
- 嵌套类增强了封装性:
- 两个顶层类(top-level class)A和B,如果B需要访问A中的成员,则A中的成员不能使用private修饰
- 将类B嵌套在类A中,类B可以访问到类A中的private成员
类B类本身可以看做类A的成员,因此可以访问类A的private成员
- 同时,类B本身可以对外界隐藏
- 增加了代码的易读性和可维护性:将小的类嵌套到顶层类中,一般会在靠近使用嵌套类的地方定义嵌套类
2.2 嵌套类的一些说明
2.2.1 关于访问权限
- 嵌套类是上层类(enclosing class)的成员变量,可以使用public、protected、private和包权限进行修饰
- 外部类,只能使用public或包权限进行修饰
2.2.2 关于访问范围
- 内部类可以访问上层类的所有成员,静态成员或实例成员都可以直接访问,甚至private成员也可以访问
- 注意: 内部类中,不能定义静态成员(static final的成员变量除外)
- 静态嵌套类,只能直接访问上层类的静态成员;若想访问实例成员,需要通过上层类的实例对象进行访问
解读1:内部类可以访问上层类的所有成员
-
内部类访问上层类的静态成员,无需过多解释
-
内部类是上层类的实例成员,在使用时必须先实例化上层类,再实例化内部类
-
这时,内部类的实例对象当然可以访问上层类的实例成员
public class OuterClass { private static String staticOuterField = "static field in outer class"; private String outerField = "instance field in outer class"; public static void staticOuterMethod() {} public void outerMethod() {} class InnerClass{ public void method() { System.out.println(staticOuterField); System.out.println(outerField); staticOuterMethod(); outerMethod(); } } public static void main(String[] args) { // 内部类的实例化 OuterClass outerClass = new OuterClass(); OuterClass.InnerClass innerClass = outerClass.new InnerClass(); innerClass.method(); } }
静态嵌套类无法直接访问上层类的实例成员
-
静态嵌套类可以看做上层类的静态成员,因此可以直接访问上层类的静态成员
-
静态嵌套类在使用时无需初始化,除非主动实例化上层类,否则将不存在上层类的实例
-
因此,静态嵌套类无法直接访问上层类的实例成员
-
这时,静态嵌套类就像一个顶层类(所谓顶层类,自己的理解:跟上层类同一level的类)
public class OuterClass { private static String staticOuterField = "static field in outer class"; private String outerField = "instance field in outer class"; public static void staticOuterMethod() {} public void outerMethod() {} static class StaticNestedClass { public void method() { System.out.println(staticOuterField); staticOuterMethod(); OuterClass outerClass = new OuterClass(); System.out.println(outerClass.outerField); outerClass.outerMethod(); } } } // 其他类的main方法 public static void main(String[] args) { // 实例化静态嵌套类,并访问静态嵌套类的实例方法 OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass(); staticNestedClass.method(); }
2.3 Shadowing(阴影)
-
在学习类的继承时,super和this两个关键字,想必大家都不陌生
-
尤其是存在同名字段或方法时,可以通过super关键字访问父类的实例成员(实例变量、构造函数、实例方法),可以通过this关键字(或者省略this关键字)访问当前类的实例成员
-
在内部类中,很容易出现类型声明相同的情况。
-
例如,下面代码中的
x
,在上层类ShadowTest
中是一个成员变量,在内部类FirstLevel
中是一个成员变量,在内部类的方法methodInFirstLevel()
中是一个入参public class ShadowTest { public int x = 0; class FirstLevel { public int x = 1; void methodInFirstLevel(int x) { System.out.println("x = " + x); System.out.println("this.x = " + this.x); System.out.println("ShadowTest.this.x = " + ShadowTest.this.x); } } public static void main(String[] args) { ShadowTest st = new ShadowTest(); ShadowTest.FirstLevel fl = st.new FirstLevel(); fl.methodInFirstLevel(23); } }
-
后来者居上,x的作用域将会发生Shadowing(在我看来就是覆盖😂)
-
被shadowed的x,需要通过添加作用域前缀才能被识别
- x:表示方法入参x
- this.x:表示内部类的成员变量x
- ShadowTest.this.x:表示上层类的成员变量x
-
最终的执行结果如下:
3. 特殊的内部类
- 到目前为止,我们看到的内部类都是定义在一个类中的非静态嵌套类
- 内部类其实还有两种特殊的类型:
- 定义在方法体中的内部类,叫做局部类(local class)
- 定义在方法体中的、没有名字的内部类,叫做匿名类(anonymous class)
- 具体来说,是定义在语句块(block)中的类,语句块可能是方法体、for循环、if语句等
3.1 局部类
-
下面是一个局部类示例,通过创建局部类
PhoneNumberCheck
判断一个电话号码是否有效(11位的手机号才有效)public class LocalClassTest { private static final String regx = "[0-9]+"; public static void checkPhoneNumber(String phoneNumber1, String phoneNumber2) { // 定义电话号码的长度,jdk 8之前必须使用final修饰 int length = 11; // 定义局部类,判断电话号码 class PhoneNumberCheck{ private String phoneNumber; public PhoneNumberCheck(String phoneNumber) { this.phoneNumber = phoneNumber; } public boolean checkPhoneNumber() { // 电话号码要求为11位,只能包含数字;局部类可以直接访问上层类的成员 if (phoneNumber.replaceAll(regx, "").length() == 0 && phoneNumber.length() == length) { return true; } return false; } // jdk 8开始,允许访问方法的入参 public void printParameter() { System.out.println("phoneNumber1: " + phoneNumber1 + ", phoneNumber2: " + phoneNumber2); } } // 定义好局部类后,通过局部类判断电话号码是否有效 PhoneNumberCheck check = new PhoneNumberCheck(phoneNumber1); // 打印方法入参中的两个电话号码 check.printParameter(); // 校验电话号码是否有效 if (check.checkPhoneNumber()) { System.out.println("phoneNumber1 is valid!"); } else { System.out.println("phoneNumber1 is invalid! "); } check = new PhoneNumberCheck(phoneNumber2); if (check.checkPhoneNumber()) { System.out.println("phoneNumber2 is valid!"); } else { System.out.println("phoneNumber2 is invalid! "); } } public static void main(String[] args) { checkPhoneNumber("18380122330", "7283456"); } }
-
执行结果:
关于局部类的特殊说明
- 如果局部类定义在静态方法中,则局部类只能访问上层类的静态成员
- 例如,checkPhoneNumber()为静态方法,如果将regx定义为非静态变量,编译时会报错
- 局部类能访问上层语句块的实例成员,所以局部类必须是非静态的(不太理解😂)
- 接口可以看做特殊的抽象类,但接口本质上是静态的,所以不能在语句块中定义一个接口。
- JDK 8以来的变化
- final 或effectively final
- JDK 8以前,局部类要想访问上层语句块中的局部变量或参数,要求局部变量或参数必须是final类型(这里的参数,不知道是否为方法的入参)
- JDK 8开始,要求变量或参数是final或
effectively final
即可 - 所谓的effectively final是指,变量或参数的值一旦初始化,就从未改变过(隐式的final类型)
- JDK 8之后,如果局部类定义在方法体中,局部类可以访问方法体的入参
- final 或effectively final
3.2 匿名类
-
局部类属于类的定义,匿名类则是一个表达式,可以在定义类的同时实例化一个类
-
匿名类的定义类似于通过构造函数new一个对象,但在构造函数之后有一个代码块,这个代码块是匿名类的具体定义
interface HelloWorld { public void greet(String name); } // 在方法体中定义匿名类 HelloWorld frenchGreeting = new HelloWorld() { public void greet(String name) { System.out.println("Salut " + name); } };
对匿名类的特殊说明
- 匿名类不能访问上层作用域中的非final或非effectively final的变量
- 匿名类中可以定义字段、额外的方法(即使没有实现或重写超类的任何方法)、实例初始化、局部类,但是不能定义构造函数
- 匿名类,在Java的GUI编程中使用非常多