Java 内部类详解

Java 内部类详解


一、什么是内部类?

在 Java 中,内部类(Inner Class) 就是定义在另一个类内部的类。

java 复制代码
public class OuterClass {        // 外部类(外层的类)
    
    class InnerClass {           // 内部类(定义在外部类里面)
        
    }
}

简单来说:

  • 外部类:包含其他类的那个类
  • 内部类:被包含在另一个类里面的类

二、为什么需要内部类?

在讲具体分类之前,我们先理解内部类存在的意义:

1. 逻辑上的归属关系

有些类,它的存在只对某一个类有意义,没有必要单独写成一个独立的类文件。

比如:汽车有发动机,发动机只属于汽车,我们可以把发动机类写在汽车类内部。

2. 访问权限的便利性

内部类可以直接访问外部类的所有成员 ,包括 private 修饰的私有成员,这是普通类做不到的。

3. 实现更好的封装

把只在某个类内部使用的类,隐藏在外部类里面,外界无法直接访问,提高了封装性。


三、内部类的四种分类

Java 内部类分为四种,这是面试中最常被问到的知识点:

类型 定义位置 关键特点
成员内部类 类的内部,方法的外部 最普通的内部类
静态内部类 类的内部,用 static 修饰 不依赖外部类实例
局部内部类 方法内部 只在方法内有效
匿名内部类 方法内部,没有名字 一次性使用

四、成员内部类(Member Inner Class)

4.1 基本定义

成员内部类是最基础的内部类,定义在外部类的成员位置(和成员变量、成员方法平级)。

java 复制代码
public class Outer {
    
    private String name = "外部类的name";
    private int age = 18;
    
    // 成员内部类
    class Inner {
        private String name = "内部类的name";
        
        public void show() {
            // 内部类可以直接访问外部类的私有成员
            System.out.println("外部类的age: " + age);
            
            // 如果内部类和外部类有同名变量,用 外部类名.this.变量名 区分
            System.out.println("外部类的name: " + Outer.this.name);
            System.out.println("内部类的name: " + name);
        }
    }
    
    public void test() {
        // 外部类访问内部类,需要创建内部类对象
        Inner inner = new Inner();
        inner.show();
    }
}

4.2 如何创建成员内部类的对象

这是面试常考点!成员内部类的对象创建方式比较特殊:

java 复制代码
public class Test {
    public static void main(String[] args) {
        
        // 第一步:创建外部类对象
        Outer outer = new Outer();
        
        // 第二步:通过外部类对象创建内部类对象
        Outer.Inner inner = outer.new Inner();
        
        // 调用内部类方法
        inner.show();
    }
}

⚠️ 注意:成员内部类依赖于外部类的实例,必须先有外部类对象,才能创建内部类对象。

4.3 成员内部类的访问规则总结

java 复制代码
public class Outer {
    private int x = 10;
    
    class Inner {
        private int x = 20;
        
        public void method() {
            int x = 30;
            
            System.out.println(x);           // 30,局部变量
            System.out.println(this.x);      // 20,内部类的成员变量
            System.out.println(Outer.this.x);// 10,外部类的成员变量
        }
    }
}

五、静态内部类(Static Inner Class)

5.1 基本定义

静态内部类就是用 static 关键字修饰的内部类。

java 复制代码
public class Outer {
    
    private String name = "外部类name";
    private static int count = 100;  // 外部类静态变量
    
    // 静态内部类
    static class StaticInner {
        
        public void show() {
            // 静态内部类 不能 直接访问外部类的非静态成员
            // System.out.println(name); // ❌ 编译错误!
            
            // 静态内部类 可以 直接访问外部类的静态成员
            System.out.println(count);  // ✅ 可以访问
        }
    }
}

5.2 如何创建静态内部类的对象

静态内部类不需要外部类的实例,可以直接创建:

java 复制代码
public class Test {
    public static void main(String[] args) {
        
        // 直接创建静态内部类对象,不需要先创建外部类对象
        Outer.StaticInner inner = new Outer.StaticInner();
        inner.show();
    }
}

5.3 静态内部类 vs 成员内部类 对比

对比项 成员内部类 静态内部类
是否需要外部类实例 ✅ 需要 ❌ 不需要
能否访问外部类非静态成员 ✅ 能 ❌ 不能
能否访问外部类静态成员 ✅ 能 ✅ 能
自身能否有静态成员 ❌ 不能(JDK16之前) ✅ 能

六、局部内部类(Local Inner Class)

6.1 基本定义

局部内部类定义在方法内部,就像局部变量一样,只在该方法内有效。

java 复制代码
public class Outer {
    
    public void method() {
        
        int num = 10;  // 局部变量
        
        // 局部内部类,定义在方法内部
        class LocalInner {
            public void show() {
                // 可以访问外部类的成员
                // 可以访问所在方法的局部变量(必须是 final 或 effectively final)
                System.out.println("num = " + num);
            }
        }
        
        // 在方法内部创建并使用局部内部类
        LocalInner localInner = new LocalInner();
        localInner.show();
    }
}

6.2 关于 effectively final

局部内部类访问方法中的局部变量时,该变量必须是 final事实上不可变的(effectively final)。

java 复制代码
public void method() {
    int a = 10;    // 没有被修改过,是 effectively final ✅
    int b = 20;
    b = 30;        // 被修改了,不是 effectively final ❌
    
    class LocalInner {
        public void show() {
            System.out.println(a);  // ✅ 可以访问
            // System.out.println(b);  // ❌ 编译错误
        }
    }
}

💡 为什么有这个限制?

局部变量存在于栈内存中,方法执行完就销毁了。但内部类对象可能还存活着。为了保证内部类访问的变量值是一致的,Java 要求该变量不能被修改。


七、匿名内部类(Anonymous Inner Class)⭐

7.1 这是最重要的内部类!

匿名内部类是没有名字的内部类,它在定义的同时就创建了对象。

匿名内部类必须继承一个类 或者实现一个接口

7.2 基本语法

java 复制代码
new 父类/接口() {
    // 重写方法
    // 类体内容
};

7.3 具体示例

场景:有一个接口

java 复制代码
// 定义一个接口
interface Animal {
    void speak();
}

传统方式(需要单独定义实现类):

java 复制代码
// 需要专门写一个实现类
class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("汪汪汪");
    }
}

// 使用
Animal animal = new Dog();
animal.speak();

匿名内部类方式(直接在使用的地方定义):

java 复制代码
// 不需要单独定义实现类,直接用匿名内部类
Animal animal = new Animal() {
    @Override
    public void speak() {
        System.out.println("汪汪汪");
    }
};

animal.speak();

7.4 匿名内部类的本质

java 复制代码
Animal animal = new Animal() {
    @Override
    public void speak() {
        System.out.println("汪汪汪");
    }
};

这行代码做了三件事:

  1. 定义了一个类,这个类实现了 Animal 接口
  2. 创建了这个类的对象
  3. 把对象赋值给 animal 变量

这个类没有名字,所以叫"匿名"内部类。

7.5 匿名内部类的常见使用场景

场景1:作为方法参数传递

java 复制代码
public class Test {
    
    // 方法接收一个 Runnable 接口
    public static void doSomething(Runnable r) {
        r.run();
    }
    
    public static void main(String[] args) {
        // 传入匿名内部类
        doSomething(new Runnable() {
            @Override
            public void run() {
                System.out.println("执行任务");
            }
        });
    }
}

场景2:排序时传入比较器

java 复制代码
import java.util.*;

public class Test {
    public static void main(String[] args) {
        List<String> list = Arrays.asList("banana", "apple", "cherry");
        
        // 使用匿名内部类实现 Comparator 接口
        Collections.sort(list, new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                return s1.compareTo(s2);
            }
        });
        
        System.out.println(list); // [apple, banana, cherry]
    }
}

7.6 匿名内部类 vs Lambda 表达式

在 Java 8 之后,函数式接口(只有一个抽象方法的接口)的匿名内部类可以用 Lambda 表达式简化:

java 复制代码
// 匿名内部类写法
Runnable r1 = new Runnable() {
    @Override
    public void run() {
        System.out.println("Hello");
    }
};

// Lambda 表达式写法(更简洁)
Runnable r2 = () -> System.out.println("Hello");

⚠️ Lambda 只能替代函数式接口的匿名内部类,普通类或有多个方法的接口不能用 Lambda。


八、内部类的完整总结

复制代码
内部类
├── 成员内部类   → 定义在类中,需要外部类实例才能创建
├── 静态内部类   → 用 static 修饰,不需要外部类实例
├── 局部内部类   → 定义在方法中,只在方法内有效
└── 匿名内部类   → 没有名字,定义时立即创建对象,最常用

九、面试题与回答话术


面试题 1:Java 内部类有哪几种?分别有什么特点?

回答话术:

Java 内部类分为四种:

第一种是成员内部类 ,定义在外部类的成员位置,可以访问外部类的所有成员包括私有成员。创建成员内部类的对象时,必须先创建外部类的对象,语法是 外部类对象.new 内部类()

第二种是静态内部类 ,用 static 修饰,不依赖外部类的实例,可以直接通过 new 外部类.内部类() 创建。静态内部类不能直接访问外部类的非静态成员,只能访问静态成员。

第三种是局部内部类,定义在方法内部,只在该方法内有效。局部内部类访问方法中的局部变量时,该变量必须是 final 或 effectively final。

第四种是匿名内部类,没有名字,必须继承一个类或实现一个接口,在定义的同时创建对象。常用于事件监听、回调、排序等场景,在 Java 8 之后函数式接口的匿名内部类可以用 Lambda 表达式替代。


面试题 2:成员内部类和静态内部类有什么区别?

回答话术:

主要有以下几点区别:

  1. 依赖关系:成员内部类依赖外部类的实例,必须通过外部类对象来创建;静态内部类不依赖外部类实例,可以直接创建。

  2. 访问权限:成员内部类可以访问外部类的所有成员,包括非静态成员;静态内部类只能访问外部类的静态成员,不能直接访问非静态成员。

  3. 使用场景:如果内部类需要访问外部类的实例成员,用成员内部类;如果内部类不需要访问外部类的实例成员,用静态内部类,这样可以避免内存泄漏,因为静态内部类不持有外部类的引用。


面试题 3:匿名内部类是什么?有什么使用限制?

回答话术:

匿名内部类是没有名字的内部类,它在定义的同时就创建了对象。匿名内部类必须继承一个类或者实现一个接口。

使用限制主要有:

  1. 匿名内部类没有名字,所以不能定义构造方法
  2. 匿名内部类不能是 abstract 的,因为创建的同时就要实例化
  3. 匿名内部类不能定义静态成员(JDK16之前)
  4. 访问外部方法的局部变量时,该变量必须是 final 或 effectively final

常见使用场景是作为方法参数传递,比如线程的 Runnable、排序的 Comparator、各种事件监听器等。


面试题 4:为什么局部内部类访问局部变量时,局部变量必须是 final 的?

回答话术:

这是因为生命周期不一致的问题。局部变量存储在栈内存中,当方法执行完毕后,栈帧销毁,局部变量也就消失了。但是内部类创建的对象存储在堆内存中,它的生命周期可能比方法更长。

为了解决这个问题,Java 会把局部变量的值拷贝一份到内部类中。如果允许局部变量被修改,那么内部类中拷贝的值和外部的值就会不一致,导致数据不同步的问题。所以 Java 要求这个变量必须是 final 或 effectively final(事实上不可变的),来保证两份数据的一致性。


面试题 5:内部类会导致内存泄漏吗?

回答话术:

非静态内部类(成员内部类、匿名内部类)会持有外部类实例的引用,这在某些场景下可能导致内存泄漏。

最典型的例子是在 Android 开发中,如果在 Activity 中定义了一个非静态的 Handler 匿名内部类,Handler 持有 Activity 的引用,如果消息队列中还有未处理的消息,Activity 想要销毁时就无法被垃圾回收,从而导致内存泄漏。

解决方案是使用静态内部类,静态内部类不持有外部类的引用,不会导致这个问题。如果静态内部类确实需要访问外部类的成员,可以通过**弱引用(WeakReference)**来持有外部类对象。

相关推荐
枫叶丹42 小时前
【HarmonyOS 6.0】ArkUI 状态管理进阶:深入理解 @Consume 装饰器默认值特性
开发语言·华为·harmonyos
Flittly2 小时前
【SpringAIAlibaba新手村系列】(7)结构化输出与对象映射
java·spring boot·agent
Chase_______2 小时前
【Python 基础】第4章:函数模块与包完全指南(函数/模块/包)
开发语言·python
众创岛2 小时前
测试失败时自动截图并附加到 Allure 报告
开发语言·python
csbysj20202 小时前
SQL CREATE DATABASE 指令详解
开发语言
我命由我123452 小时前
React - useEffect、useRef、Fragment
开发语言·前端·javascript·react.js·前端框架·ecmascript·js
未来龙皇小蓝2 小时前
Java安全通信:RSA签名 + AES混合加密详解
java·开发语言·安全·web安全
XDOC2 小时前
使用docx4j将Word文档转换为PDF
java
heimeiyingwang2 小时前
【架构实战】混沌工程:让系统更健壮的实践
开发语言·架构·php