03、内部类

一、内部类(Inner Class) / 嵌套类(Nested Class)


1、定义

  • 将一个 定义在另一个里面一个方法里面,这样的类称为 内部类
    • 内部类 - 寄生类
    • 外部类 - 宿主类

2、内部类 的 类型

类型修饰符依赖外部实例访问外部成员作用域典型场景
静态嵌套类static静态成员全局工具类、辅助类
成员内部类所有成员外部类内部迭代器、紧密耦合逻辑
局部内部类方法内的 有效 final方法 / 代码块 临时逻辑封装
匿名内部类方法内的 有效 final单次使用回调、事件监听

3、内部类 的 修饰符

内部类类型允许的修饰符
静态内部类访问控制修饰符:public / protected / 默认 / private
非访问修饰符:static(必须)、final、abstract、strictfp
成员内部类访问控制修饰符:public / protected / 默认 / private
非访问修饰符:final、abstract、strictfp
局部内部类访问控制修饰符:无
非访问修饰符:final、abstract、strictfp
匿名内部类无修饰符

二、外部类 与 静态内部类、成员内部类 的相互访问关系


1、静态内部类

  • 外部类 访问 静态内部类 的 静态方法、静态属性:
    • 静态内部类.静态方法(); 静态内部类.静态属性; 静态内部类.静态常量 ;
  • 外部类 访问 静态内部类 的 成员方法、成员属性:
    • 静态内部类对象.成员方法(); 静态内部类对象.成员属性;

2、成员内部类

  • 外部类 访问 成员内部类 的 成员方法、成员属性:
    • 成员内部类对象.成员方法(); 成员内部类对象.成员属性;
  • 外部类 访问 成员内部类 的 静态常量:【 成员内部类不能有 静态方法、静态属性】
    • 成员内部类.静态常量 ;

三、局部内部类 和 匿名内部类 不能有访问控制修饰符


内部类类型是否有类名?作用域是否允许访问修饰符设计目的
静态嵌套类✔️全局(按修饰符限制✔️工具类、辅助类
成员内部类✔️外部类实例相关✔️紧密耦合外部实例的逻辑
局部内部类✔️方法/代码块内临时逻辑封装
匿名内部类定义的代码块内一次性实现接口或继承类

1、作用域的限制

  • 局部内部类匿名内部类 只能在定义它的方法代码块内部被实例化和使用
    • 即:作用域仅限于该方法代码块内部外部无法直接访问该类。
  • 因此,访问修饰符在此场景下没有意义。即便允许添加修饰符,也无法改变其作用域限制

2、Java 语言规范限制

  • 根据 Java 语法规则,局部内部类匿名内部类 不能声明任何访问修饰符

  • 局部内部类方法的局部成员,而不是类的成员,因此,不适用类成员的访问控制规则。
    • 若允许修饰符,会导致语法冗余和矛盾,因为,局部内部类作用域已经隐式受限。
  • 由于匿名内部类 没有显式的 类名,修饰符无法附加类声明上。
    • 因此,语法上直接禁止使用修饰符

3、设计哲学:简化与一致性

  • 简化语义
    • 局部内部类作用是辅助方法内部逻辑,无需暴露给外部。
    • 匿名内部类的设计目标是快速实现 接口 或 继承类专注于 一次性使用。无需暴露给外部。
    • 禁止修饰符可以避免开发者误用(如:声明 public 但实际无法全局访问)。
  • 一致性
    • Java 其它局部定义元素(如:局部变量)也不能使用访问修饰符
    • 局部内部类的规则与此一致。

四、成员、局部、匿名 内部类中为何不可以定义静态成员?


  • Java 16 之前的限制如下:
内部类类型允许的静态成员禁止的静态成员根本原因
成员内部类static final 常量普通 static 方法、变量依赖外部类实例,与静态成员的独立性矛盾
局部内部类static final 常量普通 static 方法、变量作用域临时性,无法保证类级别存在
匿名内部类static final 常量普通 static 方法、变量匿名性导致无法通过类名访问
静态内部类所有 static 成员独立于外部类实例,生命周期一致
  • Java 16 开始,就没有以上的限制了。
    • 如:匿名内部类 的代码示例
interface Workable{
    void work();
}
/**
 * 匿名内部类
 *      JDK 16 之前,匿名内部类 的 内部:
 *          可以有 static final 修饰的常量、初始化块、成员变量、成员方法。
 *          不能有 静态成员变量,静态初始化块、静态方法。
 *      JDK 16 开始,匿名内部类 的 内部:
 *          可以有 static final 修饰的常量、初始化块、成员变量、成员方法、静态成员变量,静态初始化块、静态方法。
 */
public class AnonymousInnerClass {
    public static void main(String[] args) {
        // 此处相当于:
            // 1,创建 Workable 的匿名的实现类。
            // 2,并创建匿名的实现类的实例。
        // 将实现类的实例赋给接口变量,典型的向上转型。
        Workable w = new Workable(){
            static final int B = 1000;

            static int aaa = 1000;

            // 由于匿名内部类没有类名,所以没有构造器。
            private int age = 20;

            static {
                System.out.println(aaa);
                test11();
            }

            {
                System.out.println("匿名内部类的初始化块!");
                test();
            }

            @Override
            public void work(){
                System.out.println("aaa");
            }

            public void test(){
                System.out.println("匿名内部类的 普通方法");
            }

            public static void test11(){
                System.out.println("匿名内部类的静态方法!!!");
            }
        };
    }
}

1、成员内部类:

  • 1、成员内部类
    • 默认持有外部类实例引用(OuterClass.this),其存在依赖于外部类实例
    • 理解:
      • 成员内部类作为外部类成员变量
        • 因此,要访问成员内部类,就必须先创建外部类实例
        • 利用外部类实例,才能访问成员内部类

  • 2、静态属性属于类级别
    • 静态属性在类加载时进行初始化,不依赖任何实例
  • 3、矛盾点:
    • 如果允许成员内部类定义普通静态成员,这些静态成员需要独立于外部类实例 存在。
    • 但是,要访问成员内部类,就必须先创建外部类实例
    • 这就导致逻辑冲突

  • 允许 static final 常量的原因:
    • static final 常量在编译期确定值,不依赖运行时状态。
      • 如: static final int MAX = 100 。
    • 因此,不会破坏成员内部类实例绑定逻辑。

2、局部内部类 (不存在 static 修饰的 局部内部类)

1、Java 语言规范的限制

  • Java语言规范(JLS)明确规定:
    • 局部内部类不能包含静态声明(static字段、方法或静态初始化块)。
    • 编译时常量除外,即: static final 常量

  • 设计目的
    • 避免复杂性。
    • 局部内部类的作用:封装与当前方法相关的逻辑,静态成员通常用于全局状态或工具方法。
      • 与局部作用域的设计初衷冲突。

2、隐式依赖外部实例

  • 如果局部内部类定义在非静态上下文中(如:实例方法)时:
    • 它会隐式持有 外部类的实例引用(即使未显式使用 Outer.this)。
    • 这使得局部内部类的实例与外部类实例绑定。
  • 静态成员不依赖实例
    • 它们独立于对象存在。
  • 若允许静态成员,会导致语义矛盾
    • 静态成员需要不依赖外部实例,但局部内部类本身却隐式依赖外部实例。

3、生命周期与作用域的不匹配

  • 作用域限制
    • 局部内部类作用域仅限于定义它的代码块
    • 局部内部类的存在依赖于外部方法的调用
      • 当方法执行结束后,局部内部类理论上可能不再可用
  • 静态成员属于类级别
    • 与类的生命周期绑定(类加载时初始化,程序结束时销毁)。
  • 如果允许局部内部类定义静态成员,会导致矛盾:
    • 1、静态成员需要在类加载时初始化,但局部内部类可能仅在方法调用时存在。
    • 2、若方法被多次调用,静态成员的状态可能被意外修改,引发不可预测的行为。

  • 允许 static final 常量的原因
    • static final 常量在编译期直接写入常量池,不依赖类加载过程,
    • 因此,即使局部内部类未被实例化,常量值依然有效

3、匿名内部类:

1、Java 语言规范的限制

  • Java语言规范(JLS)明确规定:
    • 匿名内部类不能包含静态声明( static 字段、方法或静态初始化块)。
    • 编译时常量 除外,即: static final 常量

  • 设计目的
    • 匿名内部类的核心用途是快速实现接口继承类,并封装与当前上下文相关的逻辑。
    • 静态成员通常用于全局共享状态或工具方法。
    • 这与匿名内部类的临时性、局部性设计目标冲突

2、隐式依赖外部实例

  • 如果匿名内部类定义在非静态上下文中(如:实例方法)时:
    • 它会隐式持有 外部类的实例引用(即使未显式使用 Outer.this)。
    • 这使得匿名内部类的实例与外部类实例绑定。
  • 静态成员不依赖实例
    • 它们独立于对象存在。
  • 若允许静态成员,会导致语义矛盾
    • 静态成员需要不依赖外部实例,但匿名内部类本身却隐式依赖外部实例。

3、匿名内部类的本质

  • 静态成员属于类级别
    • 与类的生命周期绑定(类加载时初始化,程序结束时销毁)。
  • 如果允许匿名内部类定义静态成员,会导致矛盾:
    • 1、静态成员属于类级别,但匿名内部类没有 显式的类名,无法通过类名直接访问静态成员。
    • 2、匿名内部类的实例可能被多次创建,静态成员的状态可能被意外共享污染
      • 如:在循环中。

五、成员、局部、匿名 内部类 中为何 可以定义静态常量?


1、静态常量 与 普通静态成员的对比

特性普通静态成员常量( static final
初始化时机类加载时初始化编译期确定值,直接嵌入 字节码
生命周期依赖依赖类的加载和卸载无依赖,与程序共存
状态管理风险可能被修改,需考虑线程安全不可变,天然线程安全
内部类中是否允许成员、局部、匿名 内部类 中 禁止允许(需满足常量条件)

2、常量的特性:编译期 确定 值

  • 常量static final 且初始化为字面量常量表达式)的编译期 就已确定,并被直接嵌入到字节码中。
  • 无需类加载过程:常量的值不依赖类的初始化或实例化,因此不会与内部类的生命周期冲突。

3、不可变性:避免状态管理问题

  • 常量不可修改
    • final 关键字确保其值不可变。因此,不存在多线程多次调用导致的共享状态问题
  • 静态成员的风险
    • 普通静态成员可能因状态变化引发问题,但常量天然线程安全无状态依赖

4、Java语言规范的特殊允许

  • 规范支持:Java语言规范(JLS)对常量有特殊豁免。
    • 只要满足以下条件,内部类可以声明 static final 常量:
      • 字段是 static final。
      • 初始化值为 常量表达式(如:字面量、算术表达式、其它常量等)。
void method() {
    class LocalInner {
        static final String GREETING = "Hello"; // 允许:字符串字面量
        static final int SUM = 1 + 2;           // 允许:常量表达式
    }
}

5、生命周期无关性

  • 局部内部类匿名内部类 的生命周期依赖于方法或代码块的执行。
    • 但常量的值在编译期已固定,无需依赖类的实例化或方法调用。
  • 成员内部类 的生命周期与外部类实例绑定,但常量的 独立于任何实例存在。

六、局部内部类 和 匿名内部类 只能访问局部 final 变量?


1、Java 8 的 effectively final

  • 从 Java 8 开始,语法进一步简化,即:effectively(有效的) final
    • 没有显式声明 final,只要变量在初始化未被修改,编译器会自动视为 final 变量
  • 示例:
void example() {
    final int x = 10;
    new Thread(() -> System.out.println(x)).start();
}

// effectively final : 就是将 上边 x 前边的 final 去掉了而已。

void example() {
    // effectively final(未再修改)
    int x = 10;
    // 合法
    new Thread(() -> System.out.println(x)).start();
}
  • 如果后边要修改 x 变量的值,就需要将 x 的重新 复制 一份,传递到匿名内部类中。
void example() {
    int x = 10;

    // 想要将 x 的值,传入子线程中,并且,主线程后边还会修改 x 的值,就必须将 x 的复制一份。
        // 即:实现 effectively final(未再修改)
    int finalX = x;
    new Thread(() -> System.out.println(finalX)).start();

    // 再次修改 x 的变量。
    x = 20;
}

2、局部变量 与 匿名内部类、局部内部类 的 生命周期 存在 冲突

  • 代码示例(错误示范):
    • 编译错误:Variable ‘count’ is accessed from within inner class, needs to be final or effectively final.
public class LifecycleConflictExample {
    public static void main(String[] args) {
        Runnable runnable = createRunnable();
        runnable.run(); // 预期输出 10,实际可能存在问题
    }

    public static Runnable createRunnable() {
        // 非 final 的局部变量
        int count = 10; 

        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("Count: " + count); // 编译错误!
            }
        };
        // 修改 局部变量 的 值。
        count = 20;
        return r;
    }
}

3、原因分析

  • 生命周期冲突
    • 方法 createRunnable() 执行完毕后,局部变量 count 的栈内存被释放
    • 但createRunnable()方法,返回的 Runnable 对象(匿名内部类实例)。
      • 可能在其他地方被调用,其生命周期远超 createRunnable() 方法。
        • 如:main 方法。

  • 数据不一致风险
    • 匿名内部类中访问的 count 实际上是编译器 自动生成副本
    • 允许修改原变量 count ,内部类副本的(初始值 10)与原变量不一致,导致逻辑混乱
      • 如:count = 20。

4、解决方案

  • 方案 1:使用 final 变量
public static Runnable createRunnable() {
    final int count = 10; // 声明为 final
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println("Count: " + count); // 合法
        }
    };
    // count = 20; // 禁止修改
    return r;
}

  • 方案 2:利用 effectively final(Java 8+)
public static Runnable createRunnable() {
    int count = 10; // effectively final(未再修改)
    Runnable r = new Runnable() {
        @Override
        public void run() {
            System.out.println("Count: " + count); // 合法
        }
    };
    // count = 20; // 若取消注释,会破坏 effectively final,导致编译错误
    return r;
}

  • 特殊场景:绕过引用修改
    • 即使变量是 final,仍可修改其指向对象内部状态
public static Runnable createRunnable() {
    final List<String> list = new ArrayList<>(); // final 引用
    Runnable r = new Runnable() {
        @Override
        public void run() {
            list.add("Hello"); // 合法:修改对象内容,而非引用本身
            System.out.println(list);
        }
    };
    // list = new ArrayList<>(); // 禁止修改引用
    return r;
}

七、为什么在 Java 中需要内部类?


  • Java 引入内部类,核心目的是通过逻辑分组增强封装简化协作,提供更灵活的代码组织方式。
    • 它在设计模式(如:工厂模式、迭代器模式)、GUI 编程、回调机制等场景中广泛应用。
  • 内部类是 Java 面向对象设计的重要补充。

1、增强封装性

  • 隐藏实现细节:内部类可以对外部完全隐藏,仅服务于其外部类。
    • 如:一个链表(LinkedList)的内部节点类(Node)通常不需要暴露给外部。
      • 通过内部类实现能更好地封装数据结构。
  • 访问控制:内部类可以声明为 private 或 protected ,限制其它类的访问。
    • 确保只有外部类或特定子类能使用它。

2、直接访问外部类成员

  • 成员内部类隐式持有外部类的引用。
    • 因此,可以直接访问外部类的所有成员(包括:私有属性和方法)。
      • 无需通过 getter 或 public 暴露字段。
  • 这在需要频繁交互的场景(如:迭代器实现)中非常方便。

3、代码组织与可读性

  • 逻辑分组:将紧密关联的类放在一起,提升代码的可维护性。
    • 如:一个 Car 类可以包含 Engine 、 Wheel 等内部类。
      • 明确表达它们属于 Car 的组成部分。
  • 减少命名冲突:内部类的作用域被限定在外部类内,避免了全局类名的重复。

4、简化回调与事件处理

  • 在需要快速实现接口(如:事件监听器)时,匿名内部类可以就地定义并实例化。
    • 尤其在 Java 8 之前广泛用于 GUI 事件处理(如:Swing)。
button.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("Button clicked!");
    }
});
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值