Java 每日一刊(第15期):内部类
文章目录
- 前言
- 内部类
- 成员内部类(Member Inner Class)
- 静态内部类(Static Nested Class)
- 局部内部类(Local Inner Class)
- 匿名内部类(Anonymous Inner Class)
- 内部类的详细对比
- 内部类字节码文件
- 文件命名规则
- 字节码结构上的区别
- 内部类的典型使用场景
- GUI 事件监听器
- 数据封装与实现隐藏
- 构建复杂对象(Builder 模式)
- 访问外部类的私有成员
- 封装特定行为的组合逻辑
- 本期小知识
前言
这里是分享 Java 相关内容的专刊,每日一更。
本期将为大家带来以下内容:
- 内部类
- 内部类的详细对比
- 内部类字节码文件
- 内部类的典型使用场景
内部类
Java 内部类是一种将类嵌套在另一个类内部的编程结构。内部类可以访问外部类的成员(包括私有成员),从而形成了类与类之间更紧密的关联。Java 内部类有四种主要类型:
- 成员内部类(Member Inner Class)
- 静态内部类(Static Nested Class)
- 局部内部类(Local Inner Class)
- 匿名内部类(Anonymous Inner Class)
成员内部类(Member Inner Class)
成员内部类是指在另一个类中定义的类,并且不使用 static
修饰。它作为外部类的一个成员,可以和外部类的成员变量、方法共存。
特点:
- 成员内部类拥有外部类的所有非静态成员的访问权,包括
private
成员。 - 成员内部类的实例与外部类的实例相关联,只有在 外部类的实例 存在的情况下才能创建成员内部类的实例。
使用场景: 适用于某些内部逻辑只和外部类强相关的情况。通过成员内部类,内部的逻辑更加封闭且紧密绑定外部类。
语法:
class Outer {private String name = "Outer";class Inner {void display() {System.out.println("Outer name: " + name);}}
}
实例化: 成员内部类需要通过外部类的对象进行实例化:
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.display();
静态内部类(Static Nested Class)
静态内部类是用 static
关键字修饰的内部类。由于它是静态的,它独立于外部类的实例存在。
特点:
- 静态内部类不能访问外部类的非静态成员,只能访问外部类的静态成员。
- 它的实例不依赖外部类的实例,可以直接创建。
使用场景: 当内部类逻辑与外部类没有强依赖关系时,可以使用静态内部类。典型场景包括实现工具类、嵌套数据结构等。
语法:
class Outer {private static String name = "Outer";static class StaticInner {void display() {System.out.println("Outer name: " + name);}}
}
实例化: 静态内部类可以直接通过类名实例化:
Outer.StaticInner inner = new Outer.StaticInner();
inner.display();
局部内部类(Local Inner Class)
局部内部类是定义在方法或代码块中的类,它的作用域仅限于方法或代码块内。
特点:
- 局部内部类可以访问方法中的局部变量,前提是这些变量必须被声明为
final
或“事实上是final
”。 - 它的生命周期与方法的执行周期一致,方法执行完毕后,局部内部类将不再可用。
使用场景: 局部内部类适用于仅在特定方法中使用,逻辑较为局限的场景,如处理复杂算法的临时类。
语法:
class Outer {void method() {final String greeting = "Hello";class LocalInner {void printGreeting() {System.out.println(greeting);}}LocalInner localInner = new LocalInner();localInner.printGreeting();}
}
注意: 由于局部内部类的作用范围仅限于定义它的代码块,因此它 只能 在方法或代码块内部使用。
匿名内部类(Anonymous Inner Class)
匿名内部类是一种特殊的内部类,它没有名字,通常用于简化代码,尤其是在需要临时实现一个接口或者继承一个类时。匿名内部类直接创建类的实例,并实现或继承该类。
特点:
- 匿名内部类是一次性的,不能重复使用。
- 可以继承一个类或实现一个接口。
- 它通常用于简化代码,在特定的上下文中快速定义并使用一个类。
使用场景: 常见于回调机制、事件监听器等场景,或者需要快速实现某个类或接口的地方。
语法:
interface Greeting {void sayHello();
}public class Test {public static void main(String[] args) {// 在这里,匿名类实现了 Greeting 接口,并在创建时就直接定义了它的行为。Greeting greeting = new Greeting() {@Overridepublic void sayHello() {System.out.println("Hello, World!");}};greeting.sayHello();}
}
此处有点难以理解,我们进行进一步解释:
new Greeting()
表示创建对象,可是 Greeting
是接口,所以单独使用 new Greeting()
是错误的 。我们仔细观察上面的代码,发现在 new Greeting()
的后面紧跟着一个 {}
。这个紧跟着的 {}
表示的是一个匿名类(没有名字的类)。为什么呢?
class Hello {}
上面是一个标准类的定义,有 class 关键字和类名,而前面的匿名类没有使用 class 关键字和类名,所以被叫做匿名类。
注意:只有在 创建对象时 紧跟着的 {}
才被称作匿名类,其他地方的 {}
并不是。
内部类的详细对比
特性 | 成员内部类(Member Inner Class) | 静态内部类(Static Nested Class) | 局部内部类(Local Inner Class) | 匿名内部类(Anonymous Inner Class) |
---|---|---|---|---|
类名 | 有类名 | 有类名 | 有类名 | 无类名 |
访问外部类成员 | 可以访问外部类的所有成员 | 只能访问外部类的静态成员 | 可以访问外部类的所有成员 | 可以访问外部类的所有成员 |
实例化方式 | 需要通过外部类实例 | 不需要外部类实例 | 在方法中实例化 | 创建时直接实例化 |
使用场景 | 用于强关联类 | 用于工具类或静态数据结构 | 局限于局部方法 | 快速实现接口或继承类 |
生命周期 | 与外部类的实例相同 | 与外部类无关 | 与方法的生命周期一致 | 生命周期短,随用随销毁 |
内部类字节码文件
Java 编译器在编译内部类时会生成与普通类不同的字节码文件。具体区别如下:
文件命名规则
普通类:外部类和普通类的 .class
文件直接使用类名命名,如 Outer.class
。
内部类:编译器为内部类生成的 .class
文件使用 外部类名$内部类名.class
的格式。例如:
- 对于成员内部类
Outer.Inner
,生成的字节码文件是Outer$Inner.class
。 - 对于匿名内部类,Java 编译器会生成类似
Outer$1.class
这样的文件,其中的数字表示匿名类出现的顺序。 - 对于局部内部类,它的
.class
文件命名也是基于外部类的名称,但同样会根据顺序编号来标识不同的局部类。
字节码结构上的区别
内部类访问外部类的成员:当内部类访问外部类的成员(尤其是私有成员)时,Java 编译器在生成字节码时会自动为内部类生成一个指向外部类的隐式引用。因此,内部类的构造函数中会隐含一个指向外部类的引用,这样才能在运行时通过这个引用访问外部类的成员。
例如,成员内部类 Inner
会持有对外部类 Outer
的引用(Outer.this
),这是字节码文件中的一个额外字段。
- 静态内部类与普通类没有区别:静态内部类由于没有外部类的隐式引用,因此它的
.class
文件与普通类的结构更为相似。 - 匿名内部类:匿名内部类的
.class
文件中不会有类的名称,它直接在生成的字节码中定义类的行为。此外,由于匿名内部类通常用来实现接口或继承类,它会包含接口或父类的方法实现。
内部类的典型使用场景
GUI 事件监听器
在 Java 的 GUI 编程中(如 Swing
、AWT
),事件监听器通常用匿名内部类来实现。事件监听器需要实现一个单一的方法,如按钮点击事件。
button.addActionListener(new ActionListener() {@Overridepublic void actionPerformed(ActionEvent e) {System.out.println("Button clicked!");}
});
使用匿名内部类可以避免定义一个新的类,并且保持代码的简洁和紧凑。
数据封装与实现隐藏
内部类经常用于隐藏一些不需要对外公开的实现细节。例如,在一个复杂的数据结构如 HashMap
中,内部使用了很多私有类(如 Entry
),这些类只为外部类服务,并不需要单独暴露给外部用户。
class HashMap {private static class Entry {final int key;String value;Entry next;Entry(int key, String value) {this.key = key;this.value = value;}}
}
通过将 Entry
作为静态内部类,HashMap
可以封装其内部实现,保证用户无法直接操作这些内部类的结构。
构建复杂对象(Builder 模式)
静态内部类常用于构建者模式(Builder Pattern),通过静态内部类实现链式调用以构建复杂对象。这种模式提高了代码的可读性和灵活性。
class Product {private String name;private int price;private Product(Builder builder) {this.name = builder.name;this.price = builder.price;}public static class Builder {private String name;private int price;public Builder setName(String name) {this.name = name;return this;}public Builder setPrice(int price) {this.price = price;return this;}public Product build() {return new Product(this);}}
}
访问外部类的私有成员
当内部类需要访问外部类的私有成员时,使用成员内部类是最直接的方式。由于内部类持有对外部类实例的隐式引用,它可以直接操作外部类的成员,无需通过 getter 或 setter 方法。
class Outer {private int data = 10;class Inner {void display() {// 直接访问外部类的私有成员System.out.println("Data from outer: " + data); }}
}
封装特定行为的组合逻辑
在面向对象设计中,有时内部类用于封装某些行为,它们仅仅是外部类逻辑的一部分。这种模式可以帮助保持外部类的清晰,而将复杂的逻辑隔离到内部类中。
class TaskManager {class Task {String name;Task(String name) {this.name = name;}void execute() {System.out.println("Executing task: " + name);}}void runTask(String name) {Task task = new Task(name);task.execute();}
}
本期小知识
由于 成员内部类 会持有外部类的引用,如果内部类对象的生命周期比外部类对象长,可能会导致 内存泄漏。