Java匿名内部类、Stream AI、Lambda表达式
匿名内部类中不能定义静态变量、静态方法(
Java匿名内部类
在 Java 中,匿名内部类是一种「没有显式类名」的内部类,通常用于快速创建一个「仅使用一次」的类实例,尤其适合简化接口或抽象类的实现(避免单独定义一个完整的类文件)。它本质是「继承了某个类 / 实现了某个接口的子类匿名对象」,语法上与创建对象的语句紧密结合。
一、匿名内部类的核心特点
- 无类名:不需要用
class关键字定义类名,直接在创建对象时编写类体。 - 仅用一次:通常只创建一个实例,不重复使用(若需多次使用,建议定义普通类 / 接口)。
- 必须依托父类 / 接口:要么继承一个普通类,要么实现一个接口(且只能继承一个类或实现一个接口,不能同时做两者)。
- 作用域局限:只能在创建它的代码块中使用(比如方法内、局部变量位置)。
二、匿名内部类的常见语法场景
匿名内部类的语法核心是:在 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>,代码会更繁琐。
三、匿名内部类的注意事项
-
访问外部变量的限制
匿名内部类若要访问外部方法的局部变量,该变量必须是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的省略规则



更多推荐



所有评论(0)