Java函数式编程-lambda表达式

一、为什么需要函数式编程

在传统面向对象编程中,我们习惯于描述"东西是什么":抽象数据

复制代码
// OO:关注数据和对象
class Dog {
    String name;
    void bark() {}
}

而函数式编程关注的是"做什么动作":抽象动作

复制代码
// FP:关注行为
list.stream()
    .filter(x -> x > 10)
    .map(x -> x * 2)
    .forEach(System.out::println);

OO(面向对象)  →  抽象数据
FP(函数式编程)→  抽象行为

函数式编程还解决了并发编程中最棘手的问题之一:可变共享状态

复制代码
多个线程同时修改同一块内存 → 谁赢了?没人知道

函数式的解决方案:
    函数永远不修改现有值
    只生成新值返回
    不对内存产生争用
    天然线程安全

二、Lambda 表达式

什么是最小可能语法

Lambda 的核心思想就是能省的全省,只保留最核心的两件事:

复制代码
1. 参数是什么
2. 拿参数做什么

对比普通方法和 Lambda:

复制代码
// 普通方法:什么都要写
public int add(int a, int b) {
    return a + b;
}

// Lambda:只留核心
(a, b) -> a + b

省略了什么:

复制代码
访问修饰符  ← 编译器推断
返回值类型  ← 编译器推断
方法名      ← 不需要,不会复用
参数类型    ← 编译器推断
大括号+return ← 单行时省略

Lambda 的省略规律

编译器会帮我们推断s是什么类型

复制代码
// 第一步:完整写法
(String s) -> {
    return s.toUpperCase();
};

// 第二步:省略参数类型
(s) -> {
    return s.toUpperCase();
};

// 第三步:单参数省略括号
s -> {
    return s.toUpperCase();
};

// 第四步:单行省略大括号和return
s -> s.toUpperCase();

// 第五步:只是调用现有方法,换成方法引用
String::toUpperCase;

三、四大核心函数式接口

学会这四个,Lambda 才算真正入门。

1. Consumer 消费者

复制代码
特征:有参数,无返回值
记忆:消费者只花钱不挣钱

Consumer<String> c = s -> System.out.println(s);
c.accept("hello");  // 打印 hello

// 最常见场景
list.forEach(s -> System.out.println(s));
list.forEach(System.out::println); // 方法引用简化

2. Supplier 供应者

复制代码
特征:无参数,有返回值
记忆:供应商只产出不接收

Supplier<String> s = () -> "hello";
String result = s.get();  // 返回 hello

// 常见场景:懒加载
Supplier<List<String>> supplier = () -> new ArrayList<>();
// 需要的时候才创建,不是立刻创建

3. Function 函数

复制代码
特征:有参数,有返回值
记忆:有输入有输出,负责转换数据

Function<String, Integer> f = s -> s.length();
Integer result = f.apply("hello");  // 返回 5

// 常见场景:数据转换
list.stream().map(s -> s.length());
list.stream().map(String::length); // 方法引用简化

4. Predicate 断言

复制代码
特征:有参数,返回boolean
记忆:专门用来判断条件

Predicate<String> p = s -> s.length() > 3;
boolean result = p.test("hello");  // 返回 true

// 常见场景:过滤数据
list.stream().filter(s -> s.length() > 3);

四个接口对比总结

复制代码
接口        参数    返回值      方法名
─────────────────────────────────────
Consumer    有      无          accept()
Supplier    无      有          get()
Function    有      有(任意)    apply()
Predicate   有      boolean     test()

如何选择接口

复制代码
看方法的参数和返回值,对号入座:

有参数  无返回值        →  Consumer
无参数  有返回值        →  Supplier
有参数  有返回值        →  Function
有参数  返回boolean    →  Predicate
无参数  无返回值        →  Runnable
两个参数               →  Bi开头的版本
                           BiConsumer
                           BiFunction
                           BiPredicate

实际例子:

复制代码
static void sendMsg(String msg) { }      // 有进无出 → Consumer
static String getName() { }              // 无进有出 → Supplier
static Integer strToInt(String s) { }    // 有进有出 → Function
static boolean isVip(String userId) { }  // 有进返回判断 → Predicate

// 对应写法
Consumer<String>           c = YourClass::sendMsg;
Supplier<String>           s = YourClass::getName;
Function<String, Integer>  f = YourClass::strToInt;
Predicate<String>          p = YourClass::isVip;

Lambda 是一个函数式接口的匿名实现

// 你写的

Consumer<String> c = s -> System.out.println(s);

// 编译器理解的

Consumer<String> c = new Consumer<String>() {

@Override

public void accept(String s) {

System.out.println(s);

}

};

// Lambda本质是:

// 实现了函数式接口的一个匿名对象

普通方法:

有名字

挂在类上

不能单独传递

匿名内部类:

没有名字

是一个对象

可以传递[多态]

但写法啰嗦

Lambda:

没有名字

是一个对象

可以传递

写法极度简洁

Lambda 不只是没有名字的方法,而是一个实现了函数式接口的匿名对象,只不过编译器帮你把所有废话都省掉了,看起来像一个方法,但本质是个对象


四、方法引用

是什么

复制代码
方法引用 = 把一个已经存在的方法直接当作Lambda来用
不用重新写逻辑,直接用 :: 指向已有方法

// Lambda:重新写了逻辑
list.forEach(s -> System.out.println(s));

// 方法引用:直接指向已有方法
list.forEach(System.out::println);

四种方法引用

1. 静态方法引用:类名::静态方法

复制代码
// Lambda
list.stream().map(s -> Integer.parseInt(s));

// 方法引用
list.stream().map(Integer::parseInt);

2. 绑定方法引用:对象::普通方法

复制代码
String prefix = "hello_";

// Lambda
list.stream().map(s -> prefix.concat(s));

// 方法引用(对象已经确定)
list.stream().map(prefix::concat);

3. 未绑定方法引用:类名::普通方法

复制代码
// Lambda
list.stream().map(s -> s.toUpperCase());
//                 ↑ s是流里的每个元素,调用时才知道是谁

// 方法引用
list.stream().map(String::toUpperCase);
//                 ↑ 流里每来一个元素,那个元素就成为调用对象

未绑定 = 对象还不知道是谁
        流里每来一个元素才完成绑定

4. 构造方法引用:类名::new

复制代码
// Lambda
list.stream().map(s -> new StringBuilder(s));

// 方法引用
list.stream().map(StringBuilder::new);

四种对比

复制代码
类型            语法              对象在哪里
────────────────────────────────────────────
静态方法引用    类名::静态方法     不需要对象
绑定方法引用    对象::普通方法     对象已经确定
未绑定方法引用  类名::普通方法     调用时传入
构造方法引用    类名::new         new时创建

能用方法引用的条件

复制代码
方法的参数和返回值要和函数式接口匹配

// Consumer:无参无返回值
void println(String s)   匹配

// Function<String,Integer>:接收String返回Integer
Integer parseInt(String s)   匹配
String toUpperCase()   不匹配

五、闭包

什么是闭包

复制代码
Lambda 使用了函数作用域之外的变量
这个 Lambda 就形成了闭包

通俗说:Lambda 把外部变量"包"进去了
离开了原来的方法,外部变量依然活着

public class Closure1 {
    int i;  // 成员变量

    IntSupplier makeFun(int x) {
        return () -> x + i++;
        //           ↑   ↑
        //           x   i
        //      方法参数  成员变量
        // Lambda把这两个外部变量都包进去了
    }
}

成员变量 vs 局部变量

复制代码
public class Closure1 {
    int i;  // 成员变量,在堆上

    IntSupplier makeFun(int x) {
        int local = 10;          // 局部变量,在栈上
        return () -> x + i++;    // i可以改,local不行
    }
}

为什么局部变量不能改:

复制代码
局部变量在栈上
方法执行完,栈帧销毁,局部变量就没了

Lambda可能在方法结束后还被调用
所以Java把局部变量复制了一份给Lambda

如果外面改了局部变量:
    外面的值变了
    Lambda里的副本没变
    两个值不一致,产生混乱

所以Java规定:局部变量不能被修改

为什么成员变量可以改:

复制代码
成员变量在堆上
Lambda通过对象引用访问
不需要复制副本
改了就是改了,没有副本问题
所以随便改

成员变量(堆上)  →  Lambda里随便改  
局部变量(栈上)  →  Lambda里不能改  

等同 final 效果

复制代码
// 明确写final
IntSupplier makeFun(final int x) {
    final int local = 10;
    return () -> x + local;
}

// 等同final效果:没写final,但从来没改过
IntSupplier makeFun(int x) {
    int local = 10;
    // local 和 x 从来没被重新赋值
    // Java认为它们等同于final
    return () -> x + local;  //  可以用
}

// 改了就报错
IntSupplier makeFun(int x) {
    int local = 10;
    local = 20;  // 改了!
    return () -> x + local;  //  编译报错
}

多个Lambda共享成员变量

复制代码
Closure1 c = new Closure1();
c.i = 0;

IntSupplier f1 = c.makeFun(10); // x=10
IntSupplier f2 = c.makeFun(20); // x=20

// f1和f2的x不同,但共享同一个i
System.out.println(f1.getAsInt()); // 10+0=10,i变成1
System.out.println(f2.getAsInt()); // 20+1=21,i变成2
System.out.println(f1.getAsInt()); // 10+2=12,i变成3

// f1改了i,f2看到的i也变了
// 因为指向的是同一个对象的同一个i

六、串起来用

复制代码
List<String> names = List.of("张三", "李四", "王五丰");

names.stream()
    .filter(s -> s.length() > 2)      // Predicate:筛选
    .map(s -> s + "先生")              // Function:转换
    .forEach(System.out::println);     // Consumer:消费

// 输出:王五丰先生

总结

复制代码
Lambda    →  用最少的语法写函数逻辑
方法引用  →  已有方法直接当Lambda用,更简洁
四大接口  →  Consumer/Supplier/Function/Predicate
             看签名选接口,签名决定一切
闭包      →  Lambda捕获外部变量
             成员变量随便改
             局部变量必须等同final

函数式编程的核心思想:函数只接收数据,返回新数据,不修改任何外部状态。 代码更可预测,bug 更好找,天然线程安全。

相关推荐
喵手1 小时前
Python爬虫实战:鸣枪起跑!深度抓取全国马拉松赛事报名情报!
爬虫·python·爬虫实战·马拉松·零基础python爬虫教学·采集马拉松赛事报名数据·马拉松数据采集
wefly20171 小时前
告别繁琐配置!m3u8live.cn让 M3U8 链接验证变得如此简单
开发语言·前端·python·django·flask·开发工具
0 0 01 小时前
CCF-CSP 37-4集体锻炼【C++】考点:数学(最大公因数gcd特性),常数优化
开发语言·c++·算法
Han.miracle1 小时前
Spring IoC 与 DI 思想及实践详解
java
伊珞_711 小时前
【雨云图】雨云图简介+简单数据python画图代码
开发语言·python
Irissgwe1 小时前
基础I/O
java·linux·前端
天若有情6731 小时前
【C++实用工具】RandEmmet:致敬Emmet的极简随机数生成器(附完整源码+GitHub)
开发语言·c++·github
木易 士心1 小时前
Java中 synchronized 和 volatile 详解
java·开发语言·jvm
小码狐1 小时前
Spring相关知识【知识整理】
java·后端·spring