写在前面:这是JavaSE系列的第9篇。说实话,内部类和枚举这两个知识点,我当时学的时候觉得特别"鸡肋"——感觉用不上,代码还变复杂了。但之后才发现,Android开发、Spring框架源码、各种设计模式里到处都是内部类和枚举的影子。今天我把这部分讲透,让你看懂框架源码不再懵。

在这里插入图片描述


一、为什么需要内部类?从一个Android按钮点击说起

假设你在写Android App,页面上有个按钮,点击后要弹个Toast提示。最简单的写法:

// Android开发中的典型场景
Button button = findViewById(R.id.button);

// 方式1:写一个单独的类(太麻烦,还要新建一个文件)
button.setOnClickListener(new MyClickListener());

// 方式2:匿名内部类(一行搞定,这才是Android开发的日常)
button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Toast.makeText(MainActivity.this, "点击了按钮", Toast.LENGTH_SHORT).show();
    }
});

内部类的核心价值

  1. 代码组织:把相关的类放在一起,逻辑更清晰
  2. 访问控制:内部类可以访问外部类的私有成员
  3. 简化代码:匿名内部类让代码更简洁(尤其是回调场景)

二、四种内部类详解

2.1 成员内部类:类的"成员"

成员内部类就像类的属性或方法一样,定义在类里面、方法外面。

class Outer {
    private String outerName = "外部类";
    private int num = 10;
    
    // 成员内部类
    class Inner {
        private String innerName = "内部类";
        private int num = 20;  // 和外部类同名变量
        
        public void innerMethod() {
            System.out.println("内部类的方法");
            
            // 访问外部类的成员(包括private)
            System.out.println("外部类的outerName:" + outerName);
            System.out.println("外部类的num:" + Outer.this.num);  // 10
            System.out.println("内部类的num:" + this.num);       // 20
        }
    }
}

如何创建成员内部类对象?

public class Test {
    public static void main(String[] args) {
        // 必须先有外部类对象,才能创建内部类对象
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        
        inner.innerMethod();
    }
}

踩坑提醒Outer.Inner这种写法只在Java中见过,看起来怪怪的。记住:成员内部类依赖于外部类实例,所以必须先new Outer()

2.2 静态内部类:不依赖外部类实例

静态内部类用static修饰,它不持有外部类的引用,可以独立存在。

class Outer {
    private static int staticNum = 100;
    private int instanceNum = 200;  // 非静态成员
    
    // 静态内部类
    static class StaticInner {
        public void method() {
            // 只能访问外部类的静态成员
            System.out.println("可以访问staticNum:" + staticNum);
            // System.out.println(instanceNum);  // ❌ 编译错误!
        }
    }
}

创建静态内部类对象

// 不需要外部类对象,直接创建
Outer.StaticInner inner = new Outer.StaticInner();
inner.method();

实际应用HashMap中的Node类就是静态内部类

// HashMap源码节选
public class HashMap<K,V> extends AbstractMap<K,V> {
    // Node是静态内部类,不依赖HashMap实例
    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;
        // ...
    }
}

2.3 局部内部类:方法里的"临时工"

局部内部类定义在方法内部,只在方法内有效,出了方法就访问不到了。

class Outer {
    private int num = 10;
    
    public void method() {
        final int localVar = 20;  // 局部变量(JDK 8+自动加final)
        
        // 局部内部类:只在method方法内有效
        class LocalInner {
            public void innerMethod() {
                System.out.println("可以访问外部类成员:" + num);
                System.out.println("可以访问局部变量:" + localVar);
            }
        }
        
        // 只能在方法内部创建对象
        LocalInner inner = new LocalInner();
        inner.innerMethod();
    }
    
    // LocalInner在这里访问不到!
}

经验之谈:局部内部类在实际开发中用得很少,了解即可。重点掌握成员内部类和静态内部类。

2.4 匿名内部类:最常用,必须掌握

匿名内部类是没有名字的内部类,通常用于一次性使用的场景,比如事件监听、线程创建、回调函数等。

2.4.1 为什么要用匿名内部类?

看一个对比:

// 不用匿名内部类:需要单独写一个类(代码分散,文件多)
class Bird implements Flyable {
    @Override
    public void fly() {
        System.out.println("鸟在飞");
    }
}
Flyable f = new Bird();

// 用匿名内部类:一步到位,代码紧凑
Flyable f2 = new Flyable() {
    @Override
    public void fly() {
        System.out.println("鸟在飞");
    }
};
2.4.2 匿名内部类的语法
// 语法:new 接口/抽象类() { 实现方法 }

// 实现接口
Runnable r = new Runnable() {
    @Override
    public void run() {
        System.out.println("线程运行中");
    }
};
new Thread(r).start();

// 继承抽象类
Animal a = new Animal() {
    @Override
    void cry() {
        System.out.println("喵喵喵");
    }
};
2.4.3 实际应用场景

场景1:Android按钮点击事件

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        // 处理点击
    }
});

// JDK 8+ 用Lambda简化
button.setOnClickListener(v -> {
    // 处理点击
});

场景2:创建线程

// 匿名内部类方式
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("子线程运行");
    }
}).start();

// Lambda简化
new Thread(() -> System.out.println("子线程运行")).start();

场景3:排序时的自定义比较器

List<Student> list = new ArrayList<>();

// 匿名内部类
Collections.sort(list, new Comparator<Student>() {
    @Override
    public int compare(Student s1, Student s2) {
        return s1.getAge() - s2.getAge();
    }
});

// Lambda简化
Collections.sort(list, (s1, s2) -> s1.getAge() - s2.getAge());

核心理解:匿名内部类本质上是一个没有名字、只用一次的类。它让代码更紧凑,但也会增加阅读难度。JDK 8引入的Lambda表达式就是匿名内部类的语法糖。


三、四种内部类对比表

类型 定义位置 是否需要外部类对象 能否访问外部类private 典型应用场景
成员内部类 类中,方法外 ✅ 需要 ✅ 可以 和外部类紧密关联的对象
静态内部类 类中,方法外,带static ❌ 不需要 ⚠️ 只能访问静态成员 工具类、数据结构节点(如HashMap.Node)
局部内部类 方法内部 ✅ 需要 ✅ 可以 几乎不用
匿名内部类 方法内部 ✅ 需要 ✅ 可以 事件监听、回调函数、线程创建

四、枚举(Enum):不只是定义常量

4.1 枚举的基本用法

枚举用于定义一组固定的、有限的常量

// 定义枚举
enum Season {
    SPRING, SUMMER, AUTUMN, WINTER
}

// 使用
public class Test {
    public static void main(String[] args) {
        Season s = Season.SPRING;
        System.out.println(s);  // SPRING
        
        // switch中使用枚举(最常用)
        switch (s) {
            case SPRING -> System.out.println("春暖花开");
            case SUMMER -> System.out.println("夏日炎炎");
            case AUTUMN -> System.out.println("秋高气爽");
            case WINTER -> System.out.println("冬雪皑皑");
        }
    }
}

4.2 枚举的构造方法和属性

枚举可以像类一样定义属性和构造方法:

enum Color {
    // 枚举常量,调用构造方法
    RED("红色", "#FF0000"),
    GREEN("绿色", "#00FF00"),
    BLUE("蓝色", "#0000FF");
    
    private final String chineseName;
    private final String hexCode;
    
    // 构造方法默认是private(不能改为public)
    private Color(String chineseName, String hexCode) {
        this.chineseName = chineseName;
        this.hexCode = hexCode;
    }
    
    public String getChineseName() {
        return chineseName;
    }
    
    public String getHexCode() {
        return hexCode;
    }
}

// 使用
Color c = Color.RED;
System.out.println(c.getChineseName());  // 红色
System.out.println(c.getHexCode());      // #FF0000

4.3 枚举的常用方法

// values():获取所有枚举值
Season[] seasons = Season.values();
for (Season s : seasons) {
    System.out.println(s);
}

// valueOf():根据字符串获取枚举值
Season s = Season.valueOf("SPRING");

// ordinal():获取枚举值的序号(从0开始)
System.out.println(Season.SPRING.ordinal());  // 0
System.out.println(Season.SUMMER.ordinal());  // 1

// name():获取枚举值的名称
System.out.println(Season.SPRING.name());  // "SPRING"

4.4 枚举实现接口:策略模式

枚举可以实现接口,每个枚举常量提供不同的实现:

interface Operation {
    double apply(double x, double y);
}

enum Calculator implements Operation {
    ADD {
        @Override
        public double apply(double x, double y) {
            return x + y;
        }
    },
    SUBTRACT {
        @Override
        public double apply(double x, double y) {
            return x - y;
        }
    },
    MULTIPLY {
        @Override
        public double apply(double x, double y) {
            return x * y;
        }
    },
    DIVIDE {
        @Override
        public double apply(double x, double y) {
            if (y == 0) throw new ArithmeticException("除数不能为0");
            return x / y;
        }
    };
}

// 使用
public class Test {
    public static void main(String[] args) {
        double result = Calculator.ADD.apply(10, 5);      // 15.0
        double result2 = Calculator.DIVIDE.apply(10, 2);  // 5.0
    }
}

经验之谈:这种写法在策略模式中非常优雅,比传统的if-else或switch清晰多了。

4.5 单例模式的最佳实现:枚举

// 饿汉式(传统写法)
class Singleton1 {
    private static final Singleton1 INSTANCE = new Singleton1();
    private Singleton1() {}
    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

// 枚举式(《Effective Java》推荐,最简洁、线程安全、防反射攻击)
enum Singleton {
    INSTANCE;
    
    public void doSomething() {
        System.out.println("单例方法");
    }
}

// 使用
Singleton.INSTANCE.doSomething();

为什么枚举是单例的最佳实现?

  1. 线程安全:枚举的实例化由JVM保证线程安全
  2. 防止反射攻击:反射无法创建枚举实例
  3. 防止序列化问题:枚举天然支持序列化
  4. 代码简洁:一行代码搞定

五、面试高频考点

考点1:内部类可以访问外部类的哪些成员?

// 成员内部类、局部内部类、匿名内部类:可以访问外部类的所有成员(包括private)
// 静态内部类:只能访问外部类的静态成员

考点2:为什么成员内部类不能定义静态成员?

class Outer {
    class Inner {
        // static int num = 10;  // ❌ 编译错误!
        // 原因:成员内部类依赖于外部类实例,而静态成员不依赖实例,逻辑矛盾
        
        static final int CONSTANT = 100;  // ✅ 常量可以(编译期确定)
    }
}

考点3:匿名内部类有什么限制?

// 1. 不能定义构造方法(因为没有类名)
// 2. 不能定义静态成员(除了常量)
// 3. 只能创建一个对象
// 4. 必须继承一个父类或实现一个接口

考点4:枚举的构造方法为什么是private?

// 枚举的实例是固定的、有限的,由JVM在类加载时创建
// 不允许外部通过new创建新的枚举实例
// 所以构造方法必须是private(其实不写也是private)

考点5:switch可以用哪些类型?

// JDK 5之前:byte、short、char、int
// JDK 5:增加枚举
// JDK 7:增加String
// JDK 17:增加模式匹配(预览特性)

六、总结

今天我们系统学习了内部类和枚举:

四种内部类

  • 成员内部类:依赖外部类实例,可访问所有成员
  • 静态内部类:不依赖实例,只能访问静态成员(HashMap.Node)
  • 局部内部类:方法内定义,几乎不用
  • 匿名内部类:最常用,用于事件监听、回调、线程创建

枚举的高级用法

  • 定义属性和构造方法
  • 实现接口(策略模式)
  • 单例模式的最佳实现

核心选择原则

  • 代码需要复用 → 命名内部类
  • 只用一次 → 匿名内部类
  • 一组固定常量 → 枚举

下一步预告
Day10我们将学习常用类和包装类——Object、String、Math、日期类、自动拆装箱等。这些是你每天写代码都会用到的工具类。


互动话题:你在看框架源码(比如Spring、Android)的时候,有没有遇到过看不懂的内部类用法?欢迎在评论区分享,我帮你分析!

如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连支持!这是【JavaSE全面教学】系列的第9篇,关注我看完整套教程 👇


参考资料


本文为【JavaSE全面教学】系列第9篇,持续更新中…

更多推荐