内部类

一、先想明白:Java 为什么要设计「内部类」?(核心痛点)

你可以先记住:内部类的唯一核心价值,是让「两个强关联的类」绑定得更紧密,同时隐藏细节、简化代码

举个生活化例子:

  • 你有一部手机(外部类 Phone),手机里有一个电池(内部类 Battery)。
  • 电池永远依附于手机,不会单独存在;电池需要知道手机的型号、电压(访问外部类成员);你也不想让别人直接操作电池(封装)。
  • 如果把 Battery 做成外部类,要么需要传 Phone 对象才能访问手机参数,要么代码散落各处,耦合度高。
  • 做成内部类,既天然能访问手机的所有信息,又能把电池的逻辑封装在手机内部,代码更「抱团」。

工程里的核心痛点:

  • 避免创建大量「只服务于一个类」的零散工具类(比如 Order 订单的 OrderItem 订单项、User 用户的 Address 地址);
  • 简化「一次性使用的接口实现」(比如按钮点击事件、线程任务),不用单独写一个实现类。

二、逐类讲透:4 种内部类(从易到难,只讲核心,不堆概念)

1. 成员内部类(最基础:依附外部类对象)
定义

直接写在外部类里(和成员变量、方法同级),没有 static 修饰,是「对象级」的内部类 ——必须先有外部类对象,才能创建内部类对象

极简代码(手机 + 电池)
// 外部类:手机(必须先new Phone,才能new Battery)
public class Phone {
    // 外部类私有成员:手机型号(内部类能直接访问)
    private String model = "华为Mate60";
    
    // 成员内部类:电池(依附于Phone对象)
    public class Battery {
        private int capacity = 5000; // 电池容量
        
        // 内部类方法:访问外部类成员
        public void showInfo() {
            // 直接用外部类的private变量model,不用getter/setter
            System.out.println(model + "的电池容量:" + capacity + "mAh");
        }
    }

    // 测试:外部类中创建内部类
    public static void main(String[] args) {
        // 第一步:创建外部类对象(先有手机)
        Phone myPhone = new Phone();
        // 第二步:通过外部类对象创建内部类对象(再装电池)
        Phone.Battery myBattery = myPhone.new Battery();
        // 调用内部类方法
        myBattery.showInfo(); // 输出:华为Mate60的电池容量:5000mAh
    }
}
工程场景
  • 订单系统:Order(订单)类里定义 OrderItem(订单项)成员内部类 —— 订单项必须属于某个订单,且需要访问订单的订单号、用户 ID 等信息;
  • 容器类:HashMap 里的 Node 节点(JDK 源码里就是成员内部类)—— 节点只属于 HashMap,需要访问 HashMap 的负载因子、容量等参数。
核心特点
  • 内部类持有外部类的「对象引用」(底层是 外部类名.this),所以能直接访问外部类的所有成员(哪怕是 private);
  • 外部类要访问内部类成员,必须先 new 内部类对象;
  • 创建语法:外部类名.内部类名 变量名 = new 外部类名().new 内部类名();
  • 可以用 private 修饰(比如把 Battery 设为 private,就只能在 Phone 内部使用,完全隐藏)。
2. 静态内部类(次常用:不依附外部类对象)
定义

在成员内部类基础上加 static 修饰,是「类级」的内部类 ——不需要外部类对象,直接就能创建

极简代码(工具箱 + 螺丝刀)
// 外部类:工具箱(静态内部类不依附于工具箱对象)
public class ToolBox {
    // 外部类静态常量:品牌(所有工具箱都一样)
    public static final String BRAND = "博世";
    
    // 静态内部类:螺丝刀(通用工具,不依附某个工具箱)
    public static class Screwdriver {
        private String type = "十字";
        
        public void work() {
            // 只能访问外部类的static成员(BRAND),不能访问非static成员
            System.out.println(BRAND + "牌" + type + "螺丝刀");
        }
    }

    // 测试:直接创建静态内部类
    public static void main(String[] args) {
        // 无需new ToolBox,直接new静态内部类(核心区别!)
        ToolBox.Screwdriver sd = new ToolBox.Screwdriver();
        sd.work(); // 输出:博世牌十字螺丝刀
    }
}
工程场景
  • 业务层:UserService 类里定义 UserQueryParam(用户查询参数)静态内部类 —— 查询参数是通用的,不需要依附某个 UserService 实例;
  • JDK 源码:ArrayList 里的 Itr 迭代器(静态内部类)—— 迭代器是操作 ArrayList 的工具,不依赖某个 ArrayList 实例。
核心特点
  • 不持有外部类对象引用,只能访问外部类的 static 成员;
  • 创建语法:外部类名.内部类名 变量名 = new 外部类名.内部类名();
  • 工程中优先用静态内部类(减少依赖,更灵活)。
3. 局部内部类(几乎不用:仅方法内有效)

4. 匿名内部类(高频:一次性使用的接口实现)

匿名内部类

一、什么是匿名内部类?

匿名内部类(Anonymous Inner Class)是 Java 中一种没有类名的内部类,它是局部内部类的一种特殊形式,通常用于快速创建一个类的实例,且这个类只需要使用一次的场景。

核心特点:

  1. 没有类名,无法单独定义,必须在创建对象的同时定义。
  2. 必须继承一个父类,或者实现一个接口(二者只能选其一)。
  3. 匿名内部类是一次性的,定义后立即创建实例,无法重复使用。
  4. 匿名内部类不能有构造方法(因为没有类名,构造方法需要和类名一致)。

二、为什么要用匿名内部类?

当你需要创建一个类的实例,且这个类只使用一次,不需要复用,使用匿名内部类可以:

  • 简化代码,避免定义多余的类文件。
  • 快速实现接口 / 继承父类,省去单独写子类的步骤。

三、匿名内部类的语法格式

// 方式1:继承一个父类
new 父类名(构造参数) {
    // 匿名内部类的成员变量和方法(可以重写父类方法)
};

// 方式2:实现一个接口
new 接口名() {
    // 必须实现接口中的所有抽象方法
};

四、实战示例(从简单到复杂)

示例 1:实现接口的匿名内部类(最常用)

假设我们有一个简单的接口,需要快速创建实例并调用方法:

// 定义一个接口
interface Greeting {
    void sayHello();
}

public class AnonymousDemo {
    public static void main(String[] args) {
        // 传统方式:需要创建一个实现类
        // class GreetingImpl implements Greeting {
        //     @Override
        //     public void sayHello() {
        //         System.out.println("Hello World!");
        //     }
        // }
        // Greeting greeting = new GreetingImpl();
        
        // 匿名内部类方式:无需定义实现类,直接创建实例
        Greeting greeting = new Greeting() {
            // 实现接口的抽象方法
            @Override
            public void sayHello() {
                System.out.println("Hello 匿名内部类!");
            }
        };
        
        // 调用方法
        greeting.sayHello(); // 输出:Hello 匿名内部类!
    }
}

示例 2:继承父类的匿名内部类

// 定义一个父类
class Animal {
    String name;
    public Animal(String name) {
        this.name = name;
    }
    public void eat() {
        System.out.println(name + "在吃食物");
    }
}

public class AnonymousDemo2 {
    public static void main(String[] args) {
        // 匿名内部类继承Animal,并重写eat方法
        Animal cat = new Animal("小猫") {
            // 重写父类方法
            @Override
            public void eat() {
                System.out.println(name + "在吃鱼");
            }
        };
        
        cat.eat(); // 输出:小猫在吃鱼
    }
}
示例 3:匿名内部类的常见场景(事件监听)

在 Swing/Android 开发中,匿名内部类常用于事件处理(比如按钮点击):

import javax.swing.JButton;
import javax.swing.JFrame;

public class AnonymousDemo3 {
    public static void main(String[] args) {
        JFrame frame = new JFrame("按钮示例");
        JButton button = new JButton("点击我");
        
        // 给按钮添加点击事件,使用匿名内部类实现ActionListener接口
        button.addActionListener(e -> {
            // JDK8+ 可以用Lambda表达式简化(本质是匿名内部类的语法糖)
            System.out.println("按钮被点击了!");
        });
        
        frame.add(button);
        frame.setSize(300, 200);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setVisible(true);
    }
}

五、匿名内部类的注意事项

访问外部变量:匿名内部类可以访问外部的局部变量,但这个变量必须是 final(JDK8+ 后可以省略 final,但编译器会自动隐式添加,变量仍不能被修改)。

public class Test {
    public static void main(String[] args) {
        int num = 10; // 隐式final,不能修改
        Greeting greeting = new Greeting() {
            @Override
            public void sayHello() {
                System.out.println("num = " + num); 
                // num = 20; // 报错:无法修改final变量
            }
        };
    }
}

没有构造方法:匿名内部类没有类名,因此无法定义构造方法,如果需要初始化,可以用初始化块

只能使用一次:匿名内部类的实例创建后,无法通过类名再次创建实例,适合一次性使用的场景。

Lambda 表达式替代:JDK8 之后,对于只有一个抽象方法的接口(函数式接口),可以用 Lambda 表达式简化匿名内部类的写法(如示例 3),代码更简洁。

总结

  1. 匿名内部类是无类名、一次性使用的内部类,必须继承父类或实现接口,常用于简化单次使用的类实例创建。
  2. 核心语法是 new 父类/接口() { 重写方法 },无需单独定义子类 / 实现类。
  3. 注意匿名内部类访问外部局部变量时,变量需为 final(JDK8+ 隐式 final),且无构造方法,可通过初始化块完成初始化。

LANBDA表达式代替匿名构造类

一、Lambda 表达式是什么?

Lambda 表达式是 Java 8 引入的核心特性,本质是简化版的匿名内部类专门用来简化只有一个抽象方法的接口(称为函数式接口)的实现。它的核心思想是 “关注做什么,而不是怎么做”,让代码更简洁、更易读。

1. 先看一个对比(匿名内部类 vs Lambda)

假设我们有一个函数式接口(只有一个抽象方法):

// 函数式接口:只有一个抽象方法
@FunctionalInterface // 注解可选,但推荐加,编译器会检查是否符合函数式接口规范
interface Calculator {
    int calculate(int a, int b);
}

用匿名内部类实现

public class Test {
    public static void main(String[] args) {
        // 匿名内部类实现Calculator接口
        Calculator add = new Calculator() {
            @Override
            public int calculate(int a, int b) {
                return a + b;
            }
        };
        System.out.println(add.calculate(10, 20)); // 输出30
    }
}

用 Lambda 表达式实现

public class Test {
    public static void main(String[] args) {
        // Lambda简化:省去类结构,只保留参数和逻辑
        Calculator add = (int a, int b) -> { return a + b; };
        System.out.println(add.calculate(10, 20)); // 输出30
        
        // 进一步简化(Lambda的语法糖)
        Calculator multiply = (a, b) -> a * b; // 省略参数类型+省略大括号和return(单行逻辑)
        System.out.println(multiply.calculate(10, 20)); // 输出200
    }
}

从对比能明显看到:Lambda 把匿名内部类的 “模板代码”(new 接口() { @Override ... })全部省略,只保留核心的参数业务逻辑

2. Lambda 的核心语法

Lambda 的完整语法格式:

(参数列表) -> { 方法体 }

语法简化规则(核心)

  1. 参数列表:如果参数类型可以被编译器推断(比如上面的a,b),可以省略类型;如果只有一个参数,括号也可以省略(如(s) -> s.length() 可简化为 s -> s.length())。
  2. 方法体:如果只有一行代码,大括号{}return(如果有返回值)可以一起省略;如果有多行代码,必须保留大括号和return(如果有返回值)。
  3. 无参数:用空括号表示,比如() -> System.out.println("Hello Lambda")

示例:不同场景的 Lambda 写法

// 1. 无参数,无返回值
Runnable runnable1 = () -> System.out.println("执行任务");

// 2. 一个参数,省略括号和类型,无返回值
Consumer<String> consumer1 = s -> System.out.println("输入:" + s);

// 3. 多个参数,省略类型,有返回值(单行)
Comparator<Integer> comparator1 = (a, b) -> a - b;

// 4. 多个参数,有返回值(多行)
Calculator divide = (a, b) -> {
    if (b == 0) {
        throw new IllegalArgumentException("除数不能为0");
    }
    return a / b;
};

二、Lambda 的核心前提:函数式接口

Lambda 只能用于函数式接口(只有一个抽象方法的接口),这是 Lambda 的使用前提,一定要记住!

Java 8 已经为我们内置了大量常用的函数式接口,放在java.util.function包下,工程中几乎不需要自己定义,常用的有:

二、Lambda 的核心前提:函数式接口

Lambda 只能用于函数式接口(只有一个抽象方法的接口),这是 Lambda 的使用前提,一定要记住!

Java 8 已经为我们内置了大量常用的函数式接口,放在java.util.function包下,工程中几乎不需要自己定义,常用的有:

接口名 抽象方法 用途
Runnable void run() 无参数、无返回值的执行逻辑
Consumer<T> void accept(T t) 接收一个参数、无返回值(消费数据)
Supplier<T> T get() 无参数、返回一个值(提供数据)
Function<T,R> R apply(T t) 接收 T 类型参数,返回 R 类型值(转换数据)
Predicate<T> boolean test(T t) 接收一个参数,返回布尔值(判断条件)
Comparator<T> int compare(T o1, T o2) 比较两个对象

三、工程实践中 Lambda 的核心应用场景

Lambda 在实际项目中不是 “花里胡哨” 的语法糖,而是能大幅简化代码、提升开发效率的工具,核心应用场景如下:

1. 集合操作(Stream 流)

这是 Lambda 最常用的场景!Java 8 的 Stream 流结合 Lambda 可以极简地实现集合的过滤、映射、排序、统计等操作,替代传统的 for 循环。

传统 for 循环 vs Stream+Lambda需求:从用户列表中筛选出年龄大于 18 的用户,提取姓名并按字母排序。

// 准备数据
List<User> userList = Arrays.asList(
    new User("张三", 20),
    new User("李四", 17),
    new User("王五", 25)
);

// 传统方式(繁琐)
List<String> nameList = new ArrayList<>();
for (User user : userList) {
    if (user.getAge() > 18) {
        nameList.add(user.getName());
    }
}
Collections.sort(nameList);

// Stream+Lambda(简洁)
List<String> nameListLambda = userList.stream()
    .filter(user -> user.getAge() > 18) // 过滤:Predicate接口
    .map(User::getName) // 映射:Function接口(方法引用,Lambda的简化)
    .sorted() // 排序:Comparator接口
    .collect(Collectors.toList());

System.out.println(nameListLambda); // 输出[张三, 王五]
2. 线程创建

替代new Thread(new Runnable(){...})的繁琐写法:

// 传统方式
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }
}).start();

// Lambda方式
new Thread(() -> System.out.println("线程执行:" + Thread.currentThread().getName())).start();
3. 事件处理(Swing/Android 等 GUI 开发)

在 GUI 开发中,按钮点击、事件监听等场景,Lambda 能简化匿名内部类的写法:

// Swing按钮点击事件(传统)
JButton button1 = new JButton("点击");
button1.addActionListener(new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {
        System.out.println("按钮被点击了");
    }
});

// Lambda方式
button1.addActionListener(e -> System.out.println("按钮被点击了"));

四、Lambda 的注意事项(工程中避坑)

  1. Lambda 中访问外部变量时,变量必须是final有效 final(即变量值不会被重新赋值),这和匿名内部类的规则一致。
  1. Lambda 没有自己的thisthis指向的是 Lambda 所在的外部类对象(而匿名内部类的this指向自身)。
  2. 不要过度使用 Lambda:如果逻辑复杂(超过 3 行),建议抽成独立方法,避免 Lambda 嵌套导致代码可读性下降。

总结

  1. Lambda 核心:是函数式接口的简化写法,语法为(参数) -> { 逻辑 },核心是 “简化匿名内部类,关注业务逻辑”。
  2. 使用前提:必须基于函数式接口(只有一个抽象方法的接口),优先使用java.util.function包下的内置接口。
  3. 工程应用:最常用在 Stream 流操作、线程创建、事件监听、回调函数等场景,能大幅简化代码,提升可读性和开发效率。

记住:Lambda 不是 “必须用”,但在合适的场景下使用,能让你的代码从 “臃肿的模板代码” 变成 “简洁的业务逻辑”,这也是现代 Java 开发的主流写法。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐