当前位置: 首页 > news >正文

Java四大内部类用法精讲

1. 初识内部类

内部类是定义在另一个类中的类。一个类的内部完全嵌套了另一个类的结构,嵌套的类被称为内部类,被嵌套的类称为外部类。

特点:可以直接访问私有属性、并且可以体现类与类之间的包含关系。

所以一个类除了有属性(成员变量)方法(成员方法)构造器(构造函数)代码块(普通代码块,静态代码块),还有一个内部类,共同构成类的五大成员

1.1. 使用内部类的原因

1、内部类可以对同一个包中的其他类隐藏

类只有两个访问权限:public(public class Test{}) 与缺省(class Test{}),在类是 public 时,就是都可以访问,缺省状态下只有同在一个包下才能访问,如图

同一个包下的 Test2 可以成功访问到 Test1,但是 Test3 就访问不到了

使用内部类之后,连同一个包下的类也无法访问这个内部类

2、内部类的方法可以访问到这个类的作用域中的数据,包括原本私有的数据

1.2. 内部类的分类

  1. 定义在外部类的局部位置上
    1. 局部内部类(有类名)
    2. 匿名内部类(没有类名)
  2. 定义在外部类的成员位置上
    1. 成员内部类(没有static修饰)
    2. 静态内部类(使用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 Name

5、如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用外部类名.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 Name

3. 匿名内部类

本质是类,该类没有显式的名字,名字由 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 playing

3.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、内部类可以被访问修饰符修饰

成员内部类与普通成员一样,可以使用privateprotectedpublic或默认(包可见)来控制访问范围。

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. 使用说明

  1. 可以直接访问外部类的所有静态成员,包括私有的,但不能直接访问非静态成员。
  2. 可以添加任意修饰符(public、protected、默认、private),因为它的地位就是一个成员。
  3. 作用域:同其他的成员,为整个整体。
  4. 静态内部类访问外部类:直接访问。
  5. 如果外部类和静态内部类的成员重名时,静态内部类访问时,默认遵循就近原则,如果想访问外部类的成员,则可以使用外部类.成员去访问
  6. 外部类访问静态内部类:创建对象,再访问
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();
http://www.jsqmd.com/news/1076027/

相关文章:

  • ESXi 8.0 U2 升级后 HPP 存储多路径策略失效完整修复教程
  • 2026 短视频链接解析工具深度评测,全平台适配少报错|纯自用无广碎碎念
  • ChatGPT Plus总在关键时刻受限?判断是否升级Pro的新方法
  • 2026年中国拼多多运营技能排行榜TOP10
  • Radeon GPU 加速大模型,Token 生成速度提升三倍
  • ROS回调式Action客户端:告别waitForResult阻塞
  • 认知科学与类脑计算 笔记草稿 非最终版
  • VPFAY是什么牌子?VPFAY(维帕菲神经酸)三合一配方介绍与产品详解
  • Input Leap:一套键盘鼠标,掌控多台电脑的数字魔法
  • 5分钟极速部署:SchoolCMS开源教务系统完整指南
  • 宇树科技内容编辑岗面试题库及核心题解析(完整版)
  • 端侧 AI 工作流融入,一周本地大模型使用复盘
  • GPT 到底是什么?从“聊天玩具“到“能干活的操作系统“——一篇把 GPT 讲清楚的长帖
  • 成都企业如何选择AI智能体服务商?选型指南
  • 锚定双碳热点,绿色智慧园区开启低碳运营新范式
  • 手把手搭建MCP模型协同服务器:MultiServerMCPClient实战指南
  • 终极静态代码分析工具TscanCode:免费、快速、准确的C++/C/Lua代码质量守护神
  • 【Java开发环境搭建终极指南】:20年资深架构师亲授IntelliJ IDEA零基础到生产就绪的7大关键步骤
  • 双碳目标下,数据中心企业如何重构绿色增长逻辑
  • 双指标Schatten拟范数:定义、因子化公式及其在优化中的应用
  • 量化模型怎么选,Q4 与 Q5 在 Ryzen AI 上的表现
  • FFmpeg 深度技术剖析:从入门到内核——音视频开发者的终极参考书
  • Java Selenium自动化测试实战:从环境搭建到框架设计与CI集成
  • 2026 年企业级大模型 API 中转服务选型参考:六大平台技术特性与企业适配性深度解析
  • C4D安装教程(附安装包)Cinema4D环境配置图文教程
  • 18VIN,0.4A,输出可调,稳压LDO,XZ6320
  • 1分钟极速安装:Windows上iPhone USB网络共享驱动终极指南
  • 本地大模型长文本处理,十万字小说一键总结
  • 连锁拓店 / 公装避坑指南②:预算坑
  • 无网环境下的生产力,飞机高铁也能跑大模型