前言
时至2025/1/20号,最新版本时Java23
有人可能会说,出就出嘛,我只用Java8,那你有没有思考过为什么我们那么喜欢Java8?不妨先来回顾一下Java历代重要版本
Java历代重要版本
正是有函数式编程和Stream API 才让我们记住了Java8
它们是如此的重要,所以我们有必要好好学习一下。
好处:
- 代码简洁
- 功能强大
- 并行处理
- 链式调用
- 延迟执行
第一章 道之伊始
宇宙初开之际,混沌之气笼罩着整个宇宙,一切模糊不清。
然后,盘古开天,女娲造人:日月乃出、星辰乃现,山川蜿蜒、江河奔流、生灵万物,欣欣向荣。此日月、星辰、山川、江河、生灵万物,谓之【对象】,皆随时间而化。
然而:日月之行、星汉灿烂、山川起伏、湖海汇聚,冥冥中有至理藏其中。名曰【道】,乃万物遵循之规律,亦谓之【函数】,它无问东西,亘古不变
作为设计宇宙洪荒的程序员
- 造日月、筑山川、划江河、开湖海、演化生灵万物、令其生生不息,则必用面向【对象】之手段
- 若定规则、求本源、追纯粹,论不变,则当选【函数】编程之思想
下面就让我们从【函数】开始。
什么是函数
什么是函数呢?函数即规则
数学上:
例如:
INPUT | f(x) | OUTPUT |
---|---|---|
1 | ? | 1 |
2 | ? | 4 |
3 | ? | 9 |
4 | ? | 16 |
5 | ? | 25 |
… | … | … |
- f ( x ) = x 2 f(x) = x^2 f(x)=x2 是一种规律, input 按照此规律变化为 output
- 很多规律已经由人揭示,例如 e = m ⋅ c 2 e = m \cdot c^2 e=m⋅c2
- 程序设计中更可以自己去制定规律,一旦成为规则的制定者,你就是神
大道无情
大道无情: 是指大道不带有任何个人主观色彩,不偏袒任何一方,只遵循自然法则和规律。
无情
何为无情:
- 只要输入相同,无论多少次调用,无论什么时间调用,输出相同。
public class Sample1 {
public static void main(String[] args){
System.out.println(square(10));
System.out.println(square(10));
System.out.println(square(10));
System.out.println(square(10));
System.out.println(square(10));
}
static int square(int x){
return x * x;
}
}
佛祖成道
例如
public class TestMutable {
public static void main(String[] args) {
System.out.println(pray("张三"));
System.out.println(pray("张三"));
System.out.println(pray("张三"));
}
static class Buddha {
String name;
public Buddha(String name) {
this.name = name;
}
}
static Buddha buddha = new Buddha("佛祖");
static String pray(String person) {
return (person + "向[" + buddha.name + "]虔诚祈祷");
}
}
以上 pray 的执行结果,除了参数变化外,希望函数的执行规则永远不变
张三向[佛祖]虔诚祈祷
张三向[佛祖]虔诚祈祷
张三向[佛祖]虔诚祈祷
然而,由于设计上的缺陷,函数引用了外界可变的数据,如果这么使用
buddha.name = "魔王";
System.out.println(pray("张三"));
结果就会是
张三向[魔王]虔诚祈祷
问题出在哪儿呢?函数的目的是除了参数能变化,其它部分都要不变,这样才能成为规则的一部分。佛祖要成为规则的一部分,也要保持不变
改正方法
static class Buddha {
final String name;
public Buddha(String name) {
this.name = name;
}
}
如果使用的是JDK16以上版本还可以使用record
record Buddha(String name) { }
- 不是说函数不能引用外界的数据,而是它引用的数据必须也能作为规则的一部分
- 让佛祖不变,佛祖才能成为规则
成员函数完整写法:
public String getName(Student this){
return this.name;
}
函数与方法
方法本质上也是函数。不过方法绑定在对象之上,它是对象个人法则
函数是
- 函数(对象数据,其它参数)
而方法是
- 对象数据.方法(其它参数)
不变的好处
只有不变,才能在滚滚时间洪流中屹立不倒,成为规则的一部分。
多线程编程中,不变意味着线程安全
合格的函数无状态
大道无形
大道无形意指最深刻的道理是无法形容的,不可言,不可喻
然而,无形的道怎么传播呢,若想让时间处处得闻大道,必须让道有形
函数化对象
函数本无形,也就是它代表的规则:位置固定、不能传播。
若要有形,让函数的规则能够传播,需要将函数化为对象。
public class MyClass {
static int add(int a, int b) {
return a + b;
}
}
与
interface Lambda {
int calculate(int a, int b);
}
Lambda add = (a, b) -> a + b; // 它已经变成了一个 lambda 对象
public static void main(String[] args){
System.out.println(Sample4.add(3,4));
System.out.println(add.caculate(5,6));
}
区别在哪?
- 前者是纯粹的一条两数加法规则,它的位置是固定的,要使用它,需要通过 MyClass.add 找到它,然后执行
- 而后者(add 对象)就像长了腿,它的位置是可以变化的,想去哪里就去哪里,哪里要用到这条加法规则,把它传递过去
- 接口的目的是为了将来用它来执行函数对象,此接口中只能有一个方法定义
函数化为对象做个比喻
- 之前是大家要统一去西天取经
- 现在是每个菩萨、罗汉拿着经书,入世传经
例如
public class Test {
interface Lambda extends Serializable{//可以被序列化为字节
int calculate(int a, int b);
}
static class Server {
public static void main(String[] args) throws IOException {
ServerSocket ss = new ServerSocket(8080);
System.out.println("server start...");
while (true) {
Socket s = ss.accept();
Thread.ofVirtual().start(() -> {
try {
ObjectInputStream is = new ObjectInputStream(s.getInputStream());
Lambda lambda = (Lambda) is.readObject();
int a = ThreadLocalRandom.current().nextInt(10);
int b = ThreadLocalRandom.current().nextInt(10);
System.out.printf("%s %d op %d = %d%n",
s.getRemoteSocketAddress().toString(), a, b, lambda.calculate(a, b));
} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}
});
}
}
}
static class Client1 {
public static void main(String[] args) throws IOException {
try(Socket s = new Socket("127.0.0.1", 8080)){
Lambda lambda = /*(Lambda & Serializable)*/ (a, b) -> a + b;
ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
os.writeObject(lambda);
os.flush();
}
}
}
static class Client2 {
public static void main(String[] args) throws IOException {
try(Socket s = new Socket("127.0.0.1", 8080)){
Lambda lambda = (Lambda & Serializable) (a, b) -> a - b;
ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
os.writeObject(lambda);
os.flush();
}
}
}
static class Client3 {
public static void main(String[] args) throws IOException {
try(Socket s = new Socket("127.0.0.1", 8080)){
Lambda lambda = (Lambda & Serializable) (a, b) -> a * b;
ObjectOutputStream os = new ObjectOutputStream(s.getOutputStream());
os.writeObject(lambda);
os.flush();
}
}
}
}
- 上面的例子做了一些简单的扩展,可以看到不同的客户端可以上传自己的计算规则
P.S.
- 大部分文献都说 lambda 是匿名函数,但我觉得需要在这个说法上进行补充
- 至少在 java 里,虽然 lambda 表达式本身不需要起名字,但不得提供一个对应接口嘛
行为参数化
已知学生类定义如下
static class Student {
private String name;
private int age;
private String sex;
public Student(String name, int age, String sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
public int getAge() {
return age;
}
public String getName() {
return name;
}
public String getSex() {
return sex;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", sex='" + sex + '\'' +
'}';
}
}
针对一组学生集合,筛选出男学生,下面的代码实现如何,评价一下
public static void main(String[] args) {
List<Student> students = List.of(
new Student("张无忌", 18, "男"),
new Student("杨不悔", 16, "女"),
new Student("周芷若", 19, "女"),
new Student("宋青书", 20, "男")
);
System.out.println(filter(students)); // 能得到 张无忌,宋青书
}
static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.sex.equals("男")) {
result.add(student);
}
}
return result;
}
如果需求再变动一下,要求找到 18 岁以下的学生,上面代码显然不能用了,改动方法如下
static List<Student> filter(List<Student> students) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (student.age <= 18) {
result.add(student);
}
}
return result;
}
System.out.println(filter(students)); // 能得到 张无忌,杨不悔
那么需求如果再要变动,找18岁以下男学生,怎么改?显然上述做法并不太好… 更希望一个方法能处理各种情况,仔细观察以上两个方法,找不同。
不同在于筛选条件部分:
student.sex.equals("男")
和
student.age <= 18
既然它们就是不同,那么能否把它作为参数传递进来,这样处理起来不就一致了吗?
static List<Student> filter(List<Student> students, ???) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (???) {
result.add(student);
}
}
return result;
}
它俩要判断的逻辑不同,那这两处不同的逻辑必然要用函数来表示,将来这两个函数都需要用到 student 对象来判断,都应该返回一个 boolean 结果,怎么描述函数的长相呢?
interface Lambda {
boolean test(Student student);
}
方法可以统一成下述代码
static List<Student> filter(List<Student> students, Lambda lambda) {
List<Student> result = new ArrayList<>();
for (Student student : students) {
if (lambda.test(student)) {
result.add(student);
}
}
return result;
}
好,最后怎么给它传递不同实现呢?
filter(students, student -> student.sex.equals("男"));
以及
filter(students, student -> student.age <= 18);
还有新需求也能满足
filter(students, student -> student.sex.equals("男") && student.age <= 18);
这样就实现了以不变应万变,而变换即是一个个函数对象,也可以称之为行为参数化
延迟执行
在记录日志时,假设日志级别是 INFO,debug 方法会遇到下面的问题:
- 本不需要记录日志,但 expensive 方法仍被执行了
static Logger logger = LogManager.getLogger();
public static void main(String[] args) {
System.out.println(logger.getLevel());
logger.debug("{}", expensive());
}
static String expensive() {
System.out.println("执行耗时操作");
return "结果";
}
改进方法1:
if(logger.isDebugEnabled())
logger.debug("{}", expensive());
显然这么做,很多类似代码都要加上这样 if 判断,很不优雅
改进方法2:
在 debug 方法外再套一个新方法,内部逻辑大概是这样:
public void debug(final String msg, final Supplier<?> lambda) {
if (this.isDebugEnabled()) {
this.debug(msg, lambda.get());
}
}
调用时这样:
logger.debug("{}", () -> expensive());
expensive() 变成了不是立刻执行,在未来 if 条件成立时才执行
函数对象的不同类型
Comparator<Student> c =
(Student s1, Student s2) -> Integer.compare(s1.age, s2.age);
BiFunction<Student, Student, Integer> f =
(Student s1, Student s2) -> Integer.compare(s1.age, s2.age);