Java四大内部类用法精讲
1. 初识内部类
内部类是定义在另一个类中的类。一个类的内部完全嵌套了另一个类的结构,嵌套的类被称为内部类,被嵌套的类称为外部类。
特点:可以直接访问私有属性、并且可以体现类与类之间的包含关系。
所以一个类除了有属性(成员变量)、方法(成员方法)、构造器(构造函数)、代码块(普通代码块,静态代码块),还有一个内部类,共同构成类的五大成员
1.1. 使用内部类的原因
1、内部类可以对同一个包中的其他类隐藏
类只有两个访问权限:public(public 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修饰符,因为局部变量也能使用final
2、可以直接访问外部类的所有成员,包括私有的
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 ArrayList<String>(), Runnable { ... } // 编译报错6、极易引发内存泄漏
匿名内部类会隐式持有外部类的引用(OuterClass.this)。如果将该匿名类对象交给一个生命周期很长的对象(如全局集合、长时间运行的线程),则外部类对象将无法被 GC 回收,导致内存泄漏。
import java.util.ArrayList; import java.util.List; public class MemoryLeakDemo { private static List<Runnable> 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、变量名冲突时的显式引用(外部类名.this)
5. 静态内部类
静态内部类就是用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();