这篇文章将详细介绍 Java 的内部类知识 |
什么是内部类
在类中定义的类就是内部类
为什么要使用内部类
使用内部类最大的优点就在于它能够非常好的解决多重继承的问题,但是如果我们不需要解决多重继承问题,那么我们自然可以使用其他的编码方式,但是使用内部类还能够为我们带来如下特性:
- 内部类可以用多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立
- 在单个外围类中,可以让多个内部类以不同的方式实现同一个接口,或者继承同一个类
- 创建内部类对象的时刻并不依赖于外围类对象的创建
- 内部类并没有令人迷惑的“is-a”关系,他就是一个独立的实体
- 内部类提供了更好的封装,除了该外围类,其他类都不能访问
了解内部类
当我们在创建一个内部类的时候,它无形中就与外围类有了一种联系,依赖于这种联系,它可以无限制地访问外围类的元素:
// 外围类
public class OuterClass {
private String name ;
public String getName() {
return this.name;
}
// 创建内部类
public class InnerClass {
public InnerClass() {
name = "Jack";
}
public void display(){
System.out.println("name:" + getName());
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
innerClass.display();
}
}
>>>>>
name: Jack
从这个例子中我们可以看出:内部类 InnerClass 可以对外围类 OuterClass 的属性进行无缝的访问,尽管它是 private 修饰的。这是因为当我们在创建某个外围类的内部类对象时,此时内部类对象必定会捕获一个指向那个外围类对象的引用,只要我们在访问外围类的成员时,就会用这个引用来选择外围类的成员。
并且还看到了如何来引用内部类:OuterClasName.InnerClassName,同时如果我们需要创建某个内部类对象,必须要利用外部类的对象通过 .new 来创建内部类:OuterClass.InnerClass innerClass = outerClass.new InnerClass();
同时如果我们需要在内部类中生成对外部类对象的引用,可以使用 OuterClassName.this 来产生一个外部类的引用,当然这点其实在编译时期就知晓了,所以没有任何运行时的成本:
public class OuterClass {
public void display(){
System.out.println("OuterClass...");
}
public class InnerClass{
// 内部类返回外部类的引用
public OuterClass getOuterClass(){
return OuterClass.this;
}
}
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
innerClass.getOuterClass().display();
}
}
我们需要明确一点,内部类是个编译时的概念,一旦编译成功后,它就与外围类属于两个完全不同的类(当然他们之间还是有联系的)。对于一个名为OuterClass的外围类和一个名为InnerClass的内部类,在编译成功后,会出现这样两个class文件:OuterClass.class和OuterClass$InnerClass.class |
内部类的分类
内部类分为:成员内部类、静态内部类、局部内部类和匿名内部类
☕️ 成员内部类
成员内部类: 定义在类的内部,而且与成员方法、成员变量同级,即也是外围类的成员之一,因此,成员内部类与外围类是紧密关联的。
这里的紧密关联指的是:成员内部类的对象的创建必须依赖于外围类的对象(即没有外围类对象,就不可能创建成员内部类),因此,成员内部类有以下3个特点:
- 成员内部类可以访问外围类的所有成员,包括私有成员
- 成员内部类是不可以声明静态成员(包括静态变量、静态方法、静态成员类、嵌套接口),但有个例外:可以声明 static final 的变量, 这是因为编译器对 final 类型的特殊处理,是直接将值写入字节码
- 成员内部类对象都隐式地保存了一个引用,指向创建它的外部类对象;或者说,成员内部类的入口是由外围类的对象保持着(静态内部类的入口,则直接由外围类保持着)
成员内部类中的 this,new 关键字:
- 获取外部类对象:OuterClass.this
- 明确指定使用外部类的成员(当内部类与外部类的名字冲突时):OuterClass.this.成员名
- 其它类中创建内部类对象:外围类对象.new
成员内部类就像外围类的实例成员一样,一定要存在对象才能访问,所以成员内部类必须绑定一个外围类的对象,下面我们通过一个例子来总结一下:
//外围类
public class OuterClass {
public int aa; // 实例成员
private static float f = 1.5f; // private的静态成员
// 外围类非静态成员函数
public void initInnerClass() {
System.out.println("内部类的初始化方法");
}
// 外围类的成员方法中创建成员内部类对象
public void createInnerClass() {
InnerClass innerClass = new InnerClass();
}
//成员内部类
class InnerClass{
private double aa; // 与围类的变量aa的名字重复
public void InnerClass(){
// 明确指定两个aa的所属并直接调用外部类的静态成员变量
this.aa = OuterClass.this.aa + f;
initInnerClass();
}
}
}
//其他类
public class OtherClass{
public static void main(String[] args) {
// 其他类中创建成员内部类
OuterClass oc = new OuterClass();
// 使用外部类对象来创建内部类对象
OuterClass.InnerClass innerClass = oc.new InnerClass();
}
}
内部类的嵌套
- 成员内部类可以继续包含成员内部类,而且不管一个内部类被嵌套了多少层,它都能透明地访问它的所有外部类所有成员
- 成员内部可以继续嵌套多层的成员内部类,但无法嵌套静态内部类;静态内部类则都可以继续嵌套这两种内部类
比如:
// 成员内部类
public class InnerClass{
public InnerClass(){
initInnerClass();
}
// 成员内部类中的成员内部类
public class InnerInnerCalss2{
protected double aa = OuterClass.this.aa; // 访问最外层的外围类的成员变量
}
}
成员内部类的继承
在内部类的访问权限允许的情况下,成员内部类也是可以被继承的。由于成员内部类的对象依赖于外围类的对象,或者说,成员内部类的构造器入口由外围类的对象把持着。因此,继承了成员内部类的子类必须要与一个外围类对象关联起来。同时,子类的构造器是必须要调用父类的构造器方法,所以也只能通过父类的外围类对象来调用父类构造器:
class ChildClass extends OuterClass.InnerClass{
// 成员内部类的子类的构造器的格式
public ChildClass(OuterClass outerClass) {
outerClass.super(); // 通过外围类的对象调用父类的构造方法
}
}
☕️ 静态内部类
静态内部类: 在类中用 static 声明的内部类。因为是 static,所以不依赖于外围类对象实例而独立存在,静态内部类的可以访问外围类中的所有静态成员,包括 private 的静态成员,但是它不能使用任何外围类的非 static 成员变量和方法。
同时静态内部类可以说是所有内部类中独立性最高的内部类,其创建对象、继承(实现接口)、扩展子类等使用方式与外围类并没有多大的区别:
//外围类
public class OuterClass {
private static float f = 1.5f; // private 的静态成员
static void println() { // 外围类静态方法
System.out.println("这是静态方法");
}
// protected 的静态内部类
protected static class StaticInnerClass{
float a;
public StaticInnerClass() {
a = f; // 直接访问外围类的private静态变量
println(); // 直接调用外围类的静态方法
}
}
public static void main(String[] args) {
OuterClass.StaticInnerClass staticInnerClass = new OuterClass.StaticInnerClass();
}
}
从例子中可以看出,静态内部类可以直接访问外部类的静态方法和属性,但访问非静态成员则是非法的,同时创建静态内部类的语法和创建外部类的语法类似,不用再使用 OuterClass.new 的方式来创建静态内部类的实例对象。
☕️ 局部内部类
局部内部类: 就是在方法、构造器、初始化块中声明的类,在结构上类似于一个局部变量,因此局部内部类是不能使用访问修饰符。
局部内部类的两个访问限制:
- 局部内部类可以访问局部变量,但一旦访问则此局部变量已经是final了,不能再改变
- 如果局部内部类定义在实例环境中(构造器、对象成员方法、实例初始化块),则可以访问外围类的所有成员;但如果内部类定义在静态环境中(静态初始化块、静态方法),则只能访问外围类的静态成员
我们使用一个例子来体现一下:
public class OuterClass {
private int a = 21;
static {
//静态域中的局部内部类
class LocalClass1{
int z = a; // 错误,在静态的作用域中的局部内部类无法访问外围对象成员
}
}
{
// 实例初始化块中的局部内部类
class localClass2{
int z = a; // 正确,实例初始化块的局部内部类可以访问外围对象成员
}
}
public OuterClass(){
int x = 2;
final int y = 3;
x = 3; // 错误,编译无法通过,因为局部变量 x 已经是 final 类型
//构造器中的局部内部类
class localClass3{
int z = y; // 可以访问final的局部变量
int b = a; // 可以访问类的所有成员
int c = x; // 访问没有用 final 修饰的局部变量会使该变量变成 final 类型
}
}
}
局部内部类使用的形参必须为 final 的原因:
从上面的例子中我们可以看到,局部内部类访问外面的局部变量时,要么,该变量时 final 类型的,要么不是 final 类型但一旦被访问就会变成 final 类型的。为什么必须要 final 的呢?
首先我们知道在内部类编译成功后,它会产生一个 class 文件,该 class 文件与外部类并不是同一个 class 文件,仅仅只保留对外部类的引用。当外部类传入的参数需要被内部类调用时,从 java 程序的角度来看是直接被调用:
public class OuterClass {
public void display(final String name, String age){
class InnerClass{
void display(){
System.out.println(name);
}
}
}
}
但其实不是,在 java 编译之后内部类的实际的操作如下:
// 编译后的内部类代码
public class OuterClass$InnerClass {
public InnerClass(String name,String age){
this.InnerClass$name = name;
this.InnerClass$age = age;
}
public void display(){
System.out.println(this.InnerClass$name + "----" + this.InnerClass$age );
}
}
所以从上面代码来看,内部类并不是直接调用方法传递的参数,而是利用自身的构造器对传入的参数进行备份,自己内部方法调用的实际上时自己的属性而不是外部方法传递进来的参数。也就是说在内部类中对属性的改变并不会影响到外部的形参,而然这从程序员的角度来看这是不可行的,毕竟站在程序的角度来看这两个根本就是同一个,如果内部类该变了,而外部方法的形参却没有改变这是难以理解和不可接受的,所以为了保持参数的一致性,就规定使用final来避免形参的不改变。
简单理解就是:为了避免引用值发生改变,例如被外部类的方法修改等,而导致内部类得到的值不一致,于是用final来让该引用不可改变。所以如果定义了一个匿名内部类,并且希望它使用一个其外部定义的参数,那么编译器会要求该参数引用是final的。
☕️ 匿名内部类
匿名内部类: 与局部内部类很相似,只不过匿名内部类是一个没有给定名字的内部类,在创建这个匿名内部类后,便会立即创建并返回此内部类的一个对象引用。
作用:匿名内部类用于隐式继承某个类(重写里面的方法或实现抽象方法)或者实现某个接口。
匿名内部类的访问限制: 与局部内部类一样
匿名内部类的优点:编码方便快捷
匿名内部类的缺点:
- 只能继承一个类或实现一个接口,不能再继承其他类或其他接口
- 只能用于创建一次对象实例
匿名内部类创建格式如下:
new 父类构造器(参数列表) | 实现接口() {
// 匿名内部类的内部部分
}
在这里我们看到使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。同时它也是没有 class 关键字,这是因为匿名内部类是直接使用 new 来生成一个对象的引用,当然这个引用是隐式的。
下面的例子是我们创建线程时经常用到的匿名内部类的方式来快速地创建一个对象的例子:
class OuterClass {
private int x = 5;
void createThread() {
final int a = 10;
int b = 189;
// 匿名内部类继承 Thread 类,并重写 Run 方法
Thread thread = new Thread("thread-1") {
int c = x; // 访问成员变量
int d = a; // final的局部变量
int e = b; // 访问没有用final修饰的局部变量
@Override
public void run() {
System.out.println("这是线程thread-1");
}
};
// 匿名内部类实现 Runnable 接口
Runnable r = new Runnable() {
@Override
public void run() {
System.out.println("线程运行中");
}
};
}
}
对于匿名内部类的使用它是存在一个缺陷的,就是它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失,所以匿名内部类是不能够被重复使用。如果我们需要对内部类进行多次使用,建议重新定义类,而不是使用匿名内部类。
我们总结一下匿名内部类的一些注意点:
- 使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口
- 匿名内部类中是不能定义构造函数
- 匿名内部类中不能存在任何的静态成员变量和静态方法
- 匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法