Java匿名内部类

在 Java 中,匿名内部类是一种「没有显式类名」的内部类,通常用于快速创建一个「仅使用一次」的类实例,尤其适合简化接口或抽象类的实现(避免单独定义一个完整的类文件)。它本质是「继承了某个类 / 实现了某个接口的子类匿名对象」,语法上与创建对象的语句紧密结合。

一、匿名内部类的核心特点

  1. 无类名:不需要用 class 关键字定义类名,直接在创建对象时编写类体。
  2. 仅用一次:通常只创建一个实例,不重复使用(若需多次使用,建议定义普通类 / 接口)。
  3. 必须依托父类 / 接口:要么继承一个普通类,要么实现一个接口(且只能继承一个类或实现一个接口,不能同时做两者)。
  4. 作用域局限:只能在创建它的代码块中使用(比如方法内、局部变量位置)。

二、匿名内部类的常见语法场景

匿名内部类的语法核心是:在 new 父类/接口() 后直接跟上 { 类体 },本质是「创建一个匿名子类的实例」并实现父类 / 接口的抽象方法。

场景 1:实现一个接口(最常用)

假设存在一个接口 Greeting,需要快速创建它的实现类实例,无需单独定义 GreetingImpl 类:

// 1. 定义一个接口
interface Greeting {
    void sayHello(String name); // 抽象方法
}

public class AnonymousDemo {
    public static void main(String[] args) {
        // 2. 匿名内部类:实现 Greeting 接口,同时创建实例
        Greeting chineseGreeting = new Greeting() {
            // 重写接口的抽象方法
            @Override
            public void sayHello(String name) {
                System.out.println("你好," + name + "!");
            }
        };

        // 3. 调用匿名内部类的方法
        chineseGreeting.sayHello("张三"); // 输出:你好,张三!


        // 再创建另一个不同实现的匿名内部类(仅需修改逻辑)
        Greeting englishGreeting = new Greeting() {
            @Override
            public void sayHello(String name) {
                System.out.println("Hello, " + name + "!");
            }
        };
        englishGreeting.sayHello("Alice"); // 输出:Hello, Alice!
    }
}
  • 这里没有定义 class ChineseGreeting implements Greeting,而是直接在 new Greeting() 后写类体,本质是「匿名子类实现了 Greeting 接口」。

场景 2:继承一个普通类 / 抽象类

假设存在一个抽象类 Animal,匿名内部类可以直接继承它并实现抽象方法:

// 1. 定义一个抽象类
abstract class Animal {
    private String name;

    public Animal(String name) {
        this.name = name;
    }

    // 抽象方法:子类必须实现
    abstract void makeSound();

    // 普通方法:可直接使用
    public String getName() {
        return name;
    }
}

public class AnonymousDemo2 {
    public static void main(String[] args) {
        // 2. 匿名内部类:继承 Animal 抽象类,同时创建实例(需传父类构造参数)
        Animal cat = new Animal("小花") {
            // 重写抽象方法 makeSound
            @Override
            void makeSound() {
                System.out.println(getName() + " 喵喵叫~");
            }
        };

        // 3. 调用方法
        cat.makeSound(); // 输出:小花 喵喵叫~


        // 继承普通类的匿名内部类(同理,可重写普通方法)
        class Person {
            public void eat() {
                System.out.println("人吃饭");
            }
        }

        Person student = new Person() {
            @Override
            public void eat() {
                System.out.println("学生吃食堂饭");
            }
        };
        student.eat(); // 输出:学生吃食堂饭
    }
}
  • 若父类(如 Animal)有构造方法,匿名内部类创建时必须通过 new 父类(参数) 传递构造参数(如 new Animal("小花"))。

场景 3:作为方法参数(简化代码)

匿名内部类常作为「一次性的方法参数」,比如 Swing 中的按钮点击事件、集合排序的 Comparator 等,避免单独定义类:

import java.util.Arrays;
import java.util.Comparator;

public class AnonymousDemo3 {
    public static void main(String[] args) {
        // 需求:对字符串数组按「长度倒序」排序
        String[] fruits = {"apple", "banana", "cherry", "date"};

        // 匿名内部类:作为 Comparator 接口的实现类,传给 sort 方法
        Arrays.sort(fruits, new Comparator<String>() {
            @Override
            public int compare(String s1, String s2) {
                // 按字符串长度倒序:s2.length() - s1.length()
                return Integer.compare(s2.length(), s1.length());
            }
        });

        // 打印排序结果
        System.out.println(Arrays.toString(fruits)); 
        // 输出:[banana, cherry, apple, date](长度:6、6、5、4)
    }
}
  • 若不用匿名内部类,需单独定义 class FruitComparator implements Comparator<String>,代码会更繁琐。

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

  1. 访问外部变量的限制
    匿名内部类若要访问外部方法的局部变量,该变量必须是 final 修饰的(Java 8+ 可省略 final,但变量仍需「实际不可变」),否则编译报错。

public class AnonymousDemo4 {
    public static void main(String[] args) {
        String prefix = "Hello"; // Java 8+ 可省略 final,但不能修改 prefix 的值
        
        Greeting greeting = new Greeting() {
            @Override
            public void sayHello(String name) {
                System.out.println(prefix + ", " + name); // 可访问外部变量 prefix
            }
        };
        
        // prefix = "Hi"; // 若修改,匿名内部类中访问会报错
        greeting.sayHello("Tom"); // 输出:Hello, Tom
    }
}

原因:匿名内部类是「值捕获」,外部变量若可变,内部类持有的值可能与外部不一致,违背线程安全和一致性。

        2. 不能定义静态成员
匿名内部类中不能定义静态变量、静态方法(static 修饰的成员),但可以定义静态常量(static final)。
错误示例:

Greeting greeting = new Greeting() {
    // 错误:匿名内部类不能有静态变量
    static String msg = "Hi"; 

    @Override
    public void sayHello(String name) {}
};

与 Lambda 表达式的区别
Java 8 引入的 Lambda 表达式看似和匿名内部类相似,但本质不同:

  • Lambda 只能用于「函数式接口」(仅一个抽象方法的接口),匿名内部类可用于接口、抽象类、普通类;
  • Lambda 是「函数式编程语法糖」,编译后生成 invokedynamic 指令,匿名内部类编译后会生成独立的 .class 文件(如 AnonymousDemo$1.class);
  • 若接口有多个抽象方法,只能用匿名内部类,不能用 Lambda。

函数式接口:有且仅有一个抽象方法的接口,接口上方可以加@FunctionalInterface注解

Stream API(流式编程)

在 Java 中,Stream API(流式编程) 是 Java 8 引入的核心特性之一,它本质是一套「处理集合 / 数组等数据源的抽象工具」—— 把数据封装成「流(Stream)」,通过链式调用的方式,对数据执行「过滤、转换、聚合、排序」等批量操作,最终得到想要的结果。

它的设计目标是简化集合数据处理的代码,同时支持「并行处理」(无需手动写多线程),让代码更简洁、易读、易维护。

一、Stream 的核心概念:不是集合,是「数据处理管道」

首先要明确:Stream 不是集合,它不存储数据,也不改变原始数据源(如集合、数组),而是像一条「管道」—— 从数据源(集合、数组、I/O 流等)中读取数据,经过一系列「中间操作」(如过滤、转换)处理后,通过「终端操作」(如收集结果、统计数量)输出最终结果,管道一旦执行终端操作就会关闭,无法重复使用。

举个直观的例子:
要从一个「学生列表」中,筛选出「年龄大于 18 岁的男生」,并提取他们的「姓名」,最后整理成一个新列表。
用传统 for 循环需要手动遍历、判断、添加;用 Stream 只需链式调用,逻辑一目了然:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

// 学生类
class Student {
    private String name;
    private int age;
    private String gender;

    // 构造器、getter 省略
    public Student(String name, int age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String getName() { return name; }
    public int getAge() { return age; }
    public String getGender() { return gender; }
}

public class StreamDemo {
    public static void main(String[] args) {
        // 原始数据源:学生列表
        List<Student> studentList = Arrays.asList(
            new Student("张三", 20, "男"),
            new Student("李四", 17, "男"),
            new Student("王五", 19, "女"),
            new Student("赵六", 21, "男")
        );

        // Stream 处理:筛选 -> 提取姓名 -> 收集成列表
        List<String> adultMaleNames = studentList
                .stream() // 1. 从集合获取流
                .filter(s -> s.getAge() > 18 && "男".equals(s.getGender())) // 2. 中间操作:筛选
                .map(Student::getName) // 3. 中间操作:提取姓名(方法引用简化)
                .collect(Collectors.toList()); // 4. 终端操作:收集结果

        System.out.println(adultMaleNames); // 输出:[张三, 赵六]
    }
}

二、Stream 的核心操作:中间操作 + 终端操作

Stream 的操作必须遵循「中间操作链式调用,终端操作收尾」的规则 —— 中间操作只「记录操作逻辑」,不实际执行(惰性求值);只有执行终端操作时,才会一次性触发所有中间操作,处理数据并返回结果。

1. 中间操作:记录逻辑,不执行

常见的中间操作有:

  • 过滤(filter):按条件保留元素,参数是「Predicate 函数式接口」(接收元素,返回 boolean)。
    例:filter(s -> s.getAge() > 18) —— 保留年龄大于 18 的元素。
  • 转换(map):将元素转换为另一种类型,参数是「Function 函数式接口」(接收元素,返回新类型)。
    例:map(Student::getName) —— 将 Student 对象转换为其姓名(String 类型)。
  • 去重(distinct):按元素的 equals() 方法去重,无参数。
    例:stream.distinct() —— 去除重复元素。
  • 排序(sorted):无参数时按元素的「自然顺序」排序(需实现 Comparable 接口);也可传入「Comparator 接口」自定义排序。
    例:sorted((s1, s2) -> s1.getAge() - s2.getAge()) —— 按年龄升序排序。
  • 限制数量(limit):保留前 N 个元素,参数是 long 类型。
    例:limit(2) —— 只保留前 2 个元素。
  • 跳过元素(skip):跳过前 N 个元素,参数是 long 类型。
    例:skip(1) —— 跳过第一个元素。
2. 终端操作:触发执行,返回结果

终端操作会消耗流,执行后流无法再使用,常见的终端操作有:

  • 收集(collect):将流中的元素收集为集合(如 List、Set、Map),参数是「Collector 接口」(Java 提供 Collectors 工具类封装常用实现)。
    例:collect(Collectors.toList()) —— 收集为 List;collect(Collectors.toSet()) —— 收集为 Set。
  • 统计数量(count):返回流中元素的个数,返回值是 long 类型。
    例:stream.filter(...).count() —— 统计筛选后元素的数量。
  • 遍历(forEach):遍历流中的每个元素,参数是「Consumer 函数式接口」(接收元素,无返回值)。
    例:stream.forEach(s -> System.out.println(s.getName())) —— 打印每个元素的姓名。
  • 查找(findFirst/findAny)findFirst 返回流中第一个元素(Optional 类型,避免空指针);findAny 返回流中任意一个元素(并行流中更高效)。
    例:stream.filter(...).findFirst().orElse(null) —— 找到第一个符合条件的元素,无则返回 null。
  • 判断(anyMatch/allMatch/noneMatch):判断流中元素是否满足条件,返回 boolean 类型。
    • anyMatch:是否存在至少一个元素满足条件;
    • allMatch:是否所有元素都满足条件;
    • noneMatch:是否所有元素都不满足条件。
      例:stream.anyMatch(s -> s.getAge() > 20) —— 判断是否有年龄大于 20 的学生。

三、Stream 的两种类型:串行流 vs 并行流

Stream 支持「串行处理」和「并行处理」,默认是串行流(单线程处理);若要启用并行处理,只需将 stream() 改为 parallelStream(),无需手动写多线程代码,Stream 会自动分配线程(基于 Fork/Join 框架)。

例:并行处理大集合,统计偶数的个数(效率比串行更高):

import java.util.Arrays;
import java.util.List;

public class ParallelStreamDemo {
    public static void main(String[] args) {
        // 大集合(模拟100万个数字)
        List<Integer> numList = Arrays.asList(1, 2, 3, ..., 1000000); // 省略初始化

        // 并行流:统计偶数个数
        long evenCount = numList
                .parallelStream() // 启用并行流
                .filter(num -> num % 2 == 0)
                .count();

        System.out.println("偶数个数:" + evenCount);
    }
}

注意:并行流虽高效,但需确保操作「线程安全」(如避免修改共享变量),且数据量较小时(如几百个元素),串行流反而更快(并行流的线程切换有开销)。

四、Stream 的数据源:不止是集合

Stream 的数据源可以是多种类型,除了常见的 Collection(如 List、Set),还可以是:

  • 数组:通过 Arrays.stream(数组) 获取流,例:Arrays.stream(new int[]{1,2,3})
  • I/O 流:通过 Files.lines(Path) 获取文件行的流,例:Files.lines(Paths.get("test.txt"))(每行是一个 String 元素);
  • 直接生成:通过 Stream.of(元素) 直接创建流(例:Stream.of("a", "b", "c")),或 Stream.generate(...) 生成无限流(需配合 limit 限制数量)。

 Lambda 表达式

函数式编程和面向对象的区别:

函数式编程不在乎是哪个对象实现具体功能,强调做什么,不是谁去做

lambda的省略规则

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐