Java四大内部类用法精讲
1. 初识内部类内部类是定义在另一个类中的类。一个类的内部完全嵌套了另一个类的结构嵌套的类被称为内部类被嵌套的类称为外部类。特点可以直接访问私有属性、并且可以体现类与类之间的包含关系。所以一个类除了有属性成员变量、方法成员方法、构造器构造函数、代码块普通代码块静态代码块还有一个内部类共同构成类的五大成员1.1. 使用内部类的原因1、内部类可以对同一个包中的其他类隐藏类只有两个访问权限publicpublic class Test{} 与缺省class Test{}在类是 public 时就是都可以访问缺省状态下只有同在一个包下才能访问如图同一个包下的 Test2 可以成功访问到 Test1但是 Test3 就访问不到了使用内部类之后连同一个包下的类也无法访问这个内部类2、内部类的方法可以访问到这个类的作用域中的数据包括原本私有的数据1.2. 内部类的分类定义在外部类的局部位置上局部内部类有类名匿名内部类没有类名定义在外部类的成员位置上成员内部类没有static修饰静态内部类使用static修饰2. 局部内部类定义在外部类的局部位置比如方法中代码块中并且拥有类名。2.1. 书写案例public class Outer { private String instanceVar 实例变量; private static String staticVar 静态变量; // 1. 构造器中的局部内部类 public Outer() { class ConstructorInner { void print() { // 可以访问实例成员和静态成员 System.out.println(构造器内部类 - instanceVar); System.out.println(构造器内部类 - staticVar); } } new ConstructorInner().print(); } // 2. 实例初始化块中的局部内部类 { class InstanceBlockInner { void print() { System.out.println(实例初始化块内部类 - instanceVar); } } new InstanceBlockInner().print(); } // 3. 静态初始化块中的局部内部类 static { class StaticBlockInner { void print() { // 只能访问静态成员不能访问非静态成员 System.out.println(静态初始化块内部类 - staticVar); } } new StaticBlockInner().print(); } public static void main(String[] args) { // 创建对象时会依次执行静态块 → 实例块 → 构造器 new LocalInnerInBlocks(); } }2.2. 使用说明1、不能添加访问修饰符因为它的地位就是一个局部变量局部变量是不能使用访问修饰符的。但是可以使用final修饰符因为局部变量也能使用final2、可以直接访问外部类的所有成员包括私有的public class Outer { // Outer类私有的成员变量 private int num 10; public void test() { class Inner { void show() { // 直接获取外部类私有的成员变量 System.out.println(num num); } } } }3、可以访问所在方法的局部变量但该局部变量必须是 final 或者是“事实最终”。什么是事实最终在 Java 8 之后只要局部变量在赋值后不再改变编译期会自动视为final在事实上这个变量就是一个常量。这是局部内部类最重要的特性也是 JDK 8 前后的一个分水岭JDK 8 之前局部变量必须显式声明为final。JDK 8 及之后局部变量隐式成为final即effectively final只要你不修改变量的值编译器就不会报错。public class Outer { public void test() { private int num 10; // 如果下面这行取消注释num 就不再是 effectively final编译报错 // num 11; class Inner { void show() { System.out.println(num num); } } new Inner().show(); } }4、在外部类的方法中创建内部类对象通过内部类对象访问内部类变量或者方法外部类的对象再调用该方法才能使得内部类的变量或者方法被使用案例代码如下public class Outer { public void test() { class Inner { private String name Inner Name; void show() { System.out.println(Inner Show ); } } // 在test方法中主动创建对象再操作变量与方法 Inner inner new Inner(); inner.show(); System.out.println(name inner.name); } public static void main(String[] args) { // 外部类对象调用该方法 Outer outer new Outer(); outer.test(); } } // 运行结果 // Inner Show // name Inner Name5、如果外部类和局部内部类的成员重名时默认遵循就近原则如果想访问外部类的成员则可以使用外部类名.this.成员去访问代码案例如下public class Outer { // 外部类的name变量 private String name Outer Name; public void test() { class Inner { // 内部类的name变量 private String name Inner Name; void showName() { System.out.println(My Name is name); System.out.println(My Outer Name is Outer.this.name); } } Inner inner new Inner(); inner.showName(); } public static void main(String[] args) { Outer outer new Outer(); outer.test(); } } // 运行结果 // My Name is Inner Name // My Outer Name is Outer Name3. 匿名内部类本质是类该类没有显式的名字名字由 JDK 底层命令不过被隐藏此名字命令规则是外部类名字$出现次序。它本质上是在new对象时直接对某个接口或父类进行临时的“子类实现”并立即生成一个该子类的实例。它主要用于一次性使用作用域内用完即弃的场景能极大简化代码书写。3.1. 书写案例1、实现接口的匿名内部类public interface USB { void iPone(String username); void fan(String username); } public class Main { public static void main(String[] args) { USB usb new USB() { Override public void iPone(String username) { System.out.println(username 用USB口给手机充电); } Override public void fan(String username) { System.out.println(username 用USB口给风扇充电); } }; usb.iPone(张三); usb.fan(李四); } } // 输出结果 // 张三用USB口给手机充电 // 李四用USB口给风扇充电2、继承普通类或抽象类的匿名内部类public class Father { public void play() { System.out.println(Father is playing); } } public class Main { public static void main(String[] args) { Father son new Father(){ Override public void play() { System.out.println(Son is playing); } }; son.play(); } } // 输出结果 // Son is playing3.2. 使用说明下面的案例会大量用到一个Runnable接口我先对这个接口做一个简要介绍FunctionalInterface public interface Runnable { void run(); // 无参数、无返回值、不抛受检异常 }它的核心职责就是封装一段可执行的代码块一个“任务”。它本身不是线程需要配合Thread类或线程池ExecutorService来驱动执行。1、匿名内部类可以访问外部类的成员变量也可以访问局部变量但局部变量必须是final或“事实最终”。public class Outer { private String outerField 外部类成员; public void show(final String param) { // 显式 final 或事实 final String localVar 局部变量; // 后续未修改即为事实 final // 匿名内部类中访问外部变量 Runnable r new Runnable() { Override public void run() { System.out.println(outerField); // 访问成员变量OK System.out.println(localVar); // 访问局部变量OK System.out.println(param); // 访问方法参数OK } }; r.run(); } }2、this关键字指向匿名内部类自身而非外部类如果想在匿名内部类中引用外部类对象需要使用外部类名.this这与局部内部类都是一致的public class Outer { String name Outer; public void test() { Runnable r new Runnable() { String name Inner; Override public void run() { System.out.println(this.name); // 输出Inner匿名类自己的 System.out.println(Outer.this.name); // 输出Outer外部类的 } }; r.run(); } }3、匿名内部类不能定义静态成员除了静态常量因为匿名类本身是实例化的没有独立的静态上下文。// 编译错误示例 Runnable r new Runnable() { // static int a 10; // 编译报错不允许静态成员 static final int B 20; // 允许编译期常量 Override public void run() {} };4、匿名内部类不能有显式构造方法因为它没有名字所以无法定义constructor。如果需要进行初始化操作可以使用实例初始化块{}来模拟构造器。// 模拟构造逻辑 abstract class Animal { abstract void eat(); } public class Test { public static void main(String[] args) { Animal dog new Animal() { // 实例初始化块相当于匿名类的构造器 { System.out.println(匿名类初始化块执行); // 可以在这里做赋值、校验等 } Override void eat() { System.out.println(狗吃骨头); } }; dog.eat(); } }5、类型受限只能继承一个父类或实现一个接口匿名内部类在new时就已经限定了类型无法再继承其他类。// 正确实现一个接口 Runnable r1 new Runnable() { public void run() {} }; // 正确继承一个类 Thread t new Thread() { public void run() {} }; // 错误无法同时继承类和实现接口语法不允许 // new ArrayListString(), Runnable { ... } // 编译报错6、极易引发内存泄漏匿名内部类会隐式持有外部类的引用OuterClass.this。如果将该匿名类对象交给一个生命周期很长的对象如全局集合、长时间运行的线程则外部类对象将无法被 GC 回收导致内存泄漏。import java.util.ArrayList; import java.util.List; public class MemoryLeakDemo { private static ListRunnable runnableList new ArrayList(); public void addTask() { // 匿名内部类持有 MemoryLeakDemo 实例的引用 Runnable task new Runnable() { Override public void run() { System.out.println(执行任务); } }; runnableList.add(task); // 任务列表是 static长期存活 } public static void main(String[] args) { MemoryLeakDemo demo new MemoryLeakDemo(); demo.addTask(); demo null; // 即使 demo 置为 null因为 runnableList 中的 task 持有 demo 引用demo 无法回收 } }避坑建议如果必须在长生命周期对象中使用考虑使用静态内部类不持有外部引用或在回调中使用弱引用WeakReference。7、作用域限制匿名内部类只能使用其父类/接口中已有的方法因为你无法向下转型转型为具体的匿名子类类型所以新增的方法外部无法调用。Runnable r new Runnable() { public void run() { System.out.println(run); } public void newMethod() { System.out.println(新方法); } // 新增方法 }; r.run(); // 可以 // r.newMethod(); // 编译报错Runnable 接口中没有此方法8、Lambda 表达式在 Java 8 之后如果匿名内部类只实现一个抽象方法即函数式接口可以用Lambda 表达式替代代码更简洁。// 匿名内部类写法 Runnable oldWay new Runnable() { Override public void run() { System.out.println(Hello); } }; // Lambda 写法更简洁 Runnable newWay () - System.out.println(Hello);注意Lambda 没有this污染问题它的this指向外部类且不会生成独立的 class 文件性能略优于匿名内部类。但当需要实现多个方法或继承父类时仍必须使用匿名内部类。补充这里说到的 this 污染问题是指上文提到的当你在匿名内部类里写this时它指向的是匿名类自己生成的对象而不是外部类的对象。Lambda 本质上不是一个类它不会生成独立的.class内部文件所以this在 Lambda 里保持了词法作用域即代码写在哪里this就是哪里的。4. 成员内部类又称实例内部类是定义在另一个类外部类的成员位置即与属性、方法平级且不加static修饰的非静态类。它本质上是外部类的一个实例成员因此必须依附于外部类的实例才能存在。4.1. 书写案例class OuterClass { // 外部类的成员变量 private String outerField 外部类字段; // 成员内部类定义与成员变量/方法平级 class InnerClass { // 内部类的成员变量 private String innerField 内部类字段; public void innerMethod() { System.out.println(内部类方法执行); } } }因为成员内部类属于外部类的实例所以不能直接用new OuterClass.InnerClass()这是静态内部类的方式而是必须先创建外部类对象再通过该对象去new内部类。public class Main { public static void main(String[] args) { // 1. 先创建外部类实例 OuterClass outer new OuterClass(); // 2. 通过外部类实例 .new 创建内部类实例 OuterClass.InnerClass inner outer.new InnerClass(); // 或者合并成一步不推荐可读性差 // OuterClass.InnerClass inner new OuterClass().new InnerClass(); inner.innerMethod(); } }4.2. 使用说明1、无条件访问外部类所有成员包括私有内部类天然持有外部类当前实例的引用即外部类名.this因此可以随意访问外部类的private字段和方法。class Outer { private String secret 私密数据; class Inner { public void showSecret() { // 直接访问外部类私有字段 System.out.println(访问外部私有数据: secret); } } }2、内部类中不能定义静态成员除编译期常量由于内部类本身属于对象不依附于类加载所以不能声明static方法或非final的静态变量。但可以定义static final常量在编译期就确定的字面量。class Outer { class Inner { // 允许编译期常量基本类型或String字面量 static final int MAX_COUNT 100; // 编译报错静态方法不能定义在非静态内部类中 // static void staticMethod() {} // 编译报错非final的静态变量 // static int count 10; } }3、内部类可以被访问修饰符修饰成员内部类与普通成员一样可以使用private、protected、public或默认包可见来控制访问范围。class Outer { // 私有的内部类只能在Outer内部使用 private class PrivateInner { } // 公开的内部类任何地方都能用 public class PublicInner { } }4、变量名冲突时的显式引用外部类名.this5. 静态内部类静态内部类就是用static修饰的成员内部类且不依赖外部类实例5.1. 书写案例class Outer { private static String staticField 外部静态字段; private String instanceField 外部实例字段; // 静态内部类 static class StaticInner { private String innerMsg 内部类消息; public void show() { // 只能直接访问外部类的静态成员 System.out.println(staticField); } } }静态内部类不依附于外部类对象所以直接new即可就像使用一个顶层的独立类只是类名前多了个外部类前缀。public class Main { public static void main(String[] args) { // 直接实例化无需外部类实例 Outer.StaticInner inner new Outer.StaticInner(); inner.show(); } }5.2. 使用说明可以直接访问外部类的所有静态成员包括私有的但不能直接访问非静态成员。可以添加任意修饰符(public、protected、默认、private)因为它的地位就是一个成员。作用域同其他的成员为整个整体。静态内部类访问外部类直接访问。如果外部类和静态内部类的成员重名时静态内部类访问时默认遵循就近原则如果想访问外部类的成员则可以使用外部类.成员去访问外部类访问静态内部类创建对象再访问public class OuterClass { private static String name Tom; static class innerClass{ public void getName(){ System.out.println(name); } } public void showName(){ innerClass innerClass new innerClass(); innerClass.getName(); } }5.3. 经典的应用场景Builder建造者模式这是静态内部类最深入人心的用途。在类内部定义一个同名的Builder静态内部类优雅地构建复杂对象。public class User { private String name; private int age; private User(Builder builder) { this.name builder.name; this.age builder.age; } // 静态内部类 Builder public static class Builder { private String name; private int age; public Builder setName(String name) { this.name name; return this; } public Builder setAge(int age) { this.age age; return this; } public User build() { return new User(this); } } } // 客户端调用极其优雅 User user new User.Builder() .setName(张三) .setAge(25) .build();