道之伊始
宇宙初开之际,混沌之气笼罩着整个宇宙,一切模糊不清。
然后,盘古开天,女娲造人:日月乃出、星辰乃现,山川蜿蜒、江河奔流、生灵万物,欣欣向荣。此日月、星辰、山川、江河、生灵万物,谓之【对象】,皆随时间而化。
然而:日月之行、星汉灿烂、山川起伏、湖海汇聚,冥冥中有至理藏其中。名曰【道】,乃万物遵循之规律,亦谓之【函数】,它无问东西,亘古不变
作为设计宇宙洪荒的程序员
- 造日月、筑山川、划江河、开湖海、演化生灵万物、令其生生不息,则必用面向【对象】之手段
- 若定规则、求本源、追纯粹,论不变,则当选【函数】编程之思想
下面就让我们从【函数】开始。
一、什么是函数
什么是函数呢?函数即规则
数学上:
例如:
INPUT | f(x) | OUTPUT |
---|---|---|
1 | ? | 1 |
2 | ? | 4 |
3 | ? | 9 |
4 | ? | 16 |
5 | ? | 25 |
... | ... | ... |
- $f(x) = x^2$ 是一种规律, input 按照此规律变化为 output
- 很多规律已经由人揭示,例如 $e = m \cdot c^2$
- 程序设计中更可以自己去制定规律,一旦成为规则的制定者,你就是神
二、大道无情
无情
何为无情:
- 只要输入相同,无论多少次调用,无论什么时间调用,输出相同。
三、函数与方法
方法本质上也是函数。不过方法绑定在对象之上,它是对象个人法则
函数是
- 函数(对象数据,其它参数)
而方法是
- 对象数据.方法(其它参数)
四、不变的好处
只有不变,才能在滚滚时间洪流中屹立不倒,成为规则的一部分。
多线程编程中,不变意味着线程安全
五、合格的函数无状态
大道无形
函数化对象
函数本无形,也就是它代表的规则:位置固定、不能传播。
若要有形,让函数的规则能够传播,需要将函数化为对象。
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 对象
区别在哪?
- 前者是纯粹的一条两数加法规则,它的位置是固定的,要使用它,需要通过 MyClass.add 找到它,然后执行
- 而后者(add 对象)就像长了腿,它的位置是可以变化的,想去哪里就去哪里,哪里要用到这条加法规则,把它传递过去
- 接口的目的是为了将来用它来执行函数对象,此接口中只能有一个方法定义
函数化为对象做个比喻
- 之前是大家要统一去西天取经
- 现在是每个菩萨、罗汉拿着经书,入世传经
例如
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 表达式本身不需要起名字,但不得提供一个对应接口嘛
六、行为参数化
已知学生类定义如下
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);二. 函数编程语法