道之伊始
宇宙初开之际,混沌之气笼罩着整个宇宙,一切模糊不清。
然后,盘古开天,女娲造人:日月乃出、星辰乃现,山川蜿蜒、江河奔流、生灵万物,欣欣向荣。此日月、星辰、山川、江河、生灵万物,谓之【对象】,皆随时间而化。
然而:日月之行、星汉灿烂、山川起伏、湖海汇聚,冥冥中有至理藏其中。名曰【道】,乃万物遵循之规律,亦谓之【函数】,它无问东西,亘古不变
作为设计宇宙洪荒的程序员
- 造日月、筑山川、划江河、开湖海、演化生灵万物、令其生生不息,则必用面向【对象】之手段
- 若定规则、求本源、追纯粹,论不变,则当选【函数】编程之思想
下面就让我们从【函数】开始。
什么是函数
什么是函数呢?函数即规则
数学上:

例如:
INPUT |
f(x) |
OUTPUT |
1 |
? |
1 |
2 |
? |
4 |
3 |
? |
9 |
4 |
? |
16 |
5 |
? |
25 |
… |
… |
… |
- f(x)=x2 是一种规律, input 按照此规律变化为 output
- 很多规律已经由人揭示,例如 e=m⋅c2
- 程序设计中更可以自己去制定规律,一旦成为规则的制定者,你就是神
大道无情
无情
何为无情:
- 只要输入相同,无论多少次调用,无论什么时间调用,输出相同。
佛祖成道
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| 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 的执行结果,除了参数变化外,希望函数的执行规则永远不变
1 2 3
| 张三向[佛祖]虔诚祈祷 张三向[佛祖]虔诚祈祷 张三向[佛祖]虔诚祈祷
|
然而,由于设计上的缺陷,函数引用了外界可变的数据,如果这么使用
1 2
| buddha.name = "魔王"; System.out.println(pray("张三"));
|
结果就会是
问题出在哪儿呢?函数的目的是除了参数能变化,其它部分都要不变,这样才能成为规则的一部分。佛祖要成为规则的一部分,也要保持不变
改正方法
1 2 3 4 5 6 7
| static class Buddha { final String name;
public Buddha(String name) { this.name = name; } }
|
或
1
| record Buddha(String name) { }
|
- 不是说函数不能引用外界的数据,而是它引用的数据必须也能作为规则的一部分
- 让佛祖不变,佛祖才能成为规则
函数与方法
方法本质上也是函数。不过方法绑定在对象之上,它是对象个人法则
函数是
而方法是
不变的好处
只有不变,才能在滚滚时间洪流中屹立不倒,成为规则的一部分。
多线程编程中,不变意味着线程安全
合格的函数无状态
大道无形
函数化对象
函数本无形,也就是它代表的规则:位置固定、不能传播。
若要有形,让函数的规则能够传播,需要将函数化为对象。
1 2 3 4 5
| public class MyClass { static int add(int a, int b) { return a + b; } }
|
与
1 2 3 4 5
| interface Lambda { int calculate(int a, int b); }
Lambda add = (a, b) -> a + b;
|
区别在哪?
- 前者是纯粹的一条两数加法规则,它的位置是固定的,要使用它,需要通过 MyClass.add 找到它,然后执行
- 而后者(add 对象)就像长了腿,它的位置是可以变化的,想去哪里就去哪里,哪里要用到这条加法规则,把它传递过去
- 接口的目的是为了将来用它来执行函数对象,此接口中只能有一个方法定义
函数化为对象做个比喻
- 之前是大家要统一去西天取经
- 现在是每个菩萨、罗汉拿着经书,入世传经
例如
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| public class Test { interface Lambda { 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 表达式本身不需要起名字,但不得提供一个对应接口嘛
行为参数化
已知学生类定义如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| 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 + '\'' + '}'; } }
|
针对一组学生集合,筛选出男学生,下面的代码实现如何,评价一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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 岁以下的学生,上面代码显然不能用了,改动方法如下
1 2 3 4 5 6 7 8 9 10 11
| 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岁以下男学生,怎么改?显然上述做法并不太好… 更希望一个方法能处理各种情况,仔细观察以上两个方法,找不同。
不同在于筛选条件部分:
和
既然它们就是不同,那么能否把它作为参数传递进来,这样处理起来不就一致了吗?
1 2 3 4 5 6 7 8 9
| static List<Student> filter(List<Student> students, ???) { List<Student> result = new ArrayList<>(); for (Student student : students) { if (???) { result.add(student); } } return result; }
|
它俩要判断的逻辑不同,那这两处不同的逻辑必然要用函数来表示,将来这两个函数都需要用到 student 对象来判断,都应该返回一个 boolean 结果,怎么描述函数的长相呢?
1 2 3
| interface Lambda { boolean test(Student student); }
|
方法可以统一成下述代码
1 2 3 4 5 6 7 8 9
| 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; }
|
好,最后怎么给它传递不同实现呢?
1
| filter(students, student -> student.sex.equals("男"));
|
以及
1
| filter(students, student -> student.age <= 18);
|
还有新需求也能满足
1
| filter(students, student -> student.sex.equals("男") && student.age <= 18);
|
这样就实现了以不变应万变,而变换即是一个个函数对象,也可以称之为行为参数化
延迟执行
在记录日志时,假设日志级别是 INFO,debug 方法会遇到下面的问题:
- 本不需要记录日志,但 expensive 方法仍被执行了
1 2 3 4 5 6 7 8 9 10 11
| 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:
1 2
| if(logger.isDebugEnabled()) logger.debug("{}", expensive());
|
显然这么做,很多类似代码都要加上这样 if 判断,很不优雅
改进方法2:
在 debug 方法外再套一个新方法,内部逻辑大概是这样:
1 2 3 4 5
| public void debug(final String msg, final Supplier<?> lambda) { if (this.isDebugEnabled()) { this.debug(msg, lambda.get()); } }
|
调用时这样:
1
| logger.debug("{}", () -> expensive());
|
expensive() 变成了不是立刻执行,在未来 if 条件成立时才执行
函数对象的不同类型
1 2 3 4 5
| 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)
|
函数编程-语法