破坏式更新和函数式更新
什么是破坏式更新和函数式更新:
破坏式更新:
有一个方法,传入一个对象并返回结果。在方法结束之后传入的参数对象也被改变了,这就是破坏式更新。你不能保证调用这个方法之后后续是否还会使用传入的参数对象,因此破坏式更新在java的函数式编程中是不被提倡的。这也是另一种副作用。
函数式更新:
用函数式编程的方法解决问题,强调没有任何副作用
破坏式更新例子:
我们有一个类用来保存火车站点的票务信息(利用简单的单向链表),表示从A地到B地的火车旅行,旅途中我们需要换车,所以需要使用几个由onward字段串联在一起的TrainJourney对象,直达火车或者旅途的最后一段onward为null
class TrainJourney {
public int price;
public TrainJourney onward;
public TrainJourney (int price, TrainJourney t) {
this.price = price;
this.onward = t;
}
}
复制代码
假设我们有几个互相分隔的TrainJourney对象分别代表A到B,B到C的旅行。我们希望建立一段新的旅行,它能将两个TrainJourney对象串联起来(即从A到B到C) 首先我们采用的是传统命令式的方法:
public static TrainJourney link (TrainJourney a, TrainJourney b) {
if (a == null) {
return b;
}
TrainJourney t = a;
while (a.onward != null) {
t = a.onward;
}
t.onward = b;
return a;
}
复制代码
这个方法具体的执行应该不用多讲了,这里我们注意到的是t.onward = b;这个操作之后return的还是a,这就出现了一个问题,这里我们进行的操作是直接修改了参数a,也就是参数a在执行完这个方法之后原来的数据结构就被改变了。如果我们还用参数a,b传入这个方法,返回的数据和第一次便不一样了,这样就产生了副作用。这个缺陷我们需要克服。因此:
如果我们需要使用表示计算结果的数据结果,那么请创建它的一个副本而不要直接修改现存的数据结构。这个最佳的实践也适用于标准的面向对象程序设计。
public static TrainJourney link (TrainJourney a, TrainJourney b) {
if (a == null) {
return b;
}
TrainJourney t = new TrainJourney(a.price, a.onward);
TrainJourney t1 = a;
TrainJourney t2 = t;
while (t1.onward != null) {
t2.onward = new TrainJourney(t1.onward.price, t1.onward.onward);
t2 = t2.onward;
t1 = t1.onward;
}
t2.onward = b;
return t;
}
复制代码
上述代码就是我们修改之后的代码,但是我们可以看到while语句中多次使用了new关键字创建对象来复制链表。但是这种方法会导致过度的对象复制。这时候,如果我们采用函数式编程的方法:
public static TrainJourney append (TrainJourney a, TrainJourney b) {
return a == null ? b : new TrainJourney (a.price, append(a.onward, b));
}
复制代码
和上面一对比,函数式编程的优点显而易见
- 代码量大大减少
- 没有对象复制导致的开支,执行速度快
函数式编程的代码一大特点就是我们只需要编写操作的步骤(先做什么,后做什么),具体如何操作(先做什么的具体操作)不需要我们写。在上述的例子中,我们从代码能看到,我们先检查参数a是否为空,如果为空则返回b,如果不为空则返回一个新的TrainJourney对象,这个对象的票价是参数a的票价,onward为递归调用append函数返回的值,递归时的参数为参数a的onward和参数b,说起来很绕。我简单地理解为:
函数式编程的代码只保留流程,具体操作全部交给程序自行完成。是一个偷懒的过程
这段代码有一个特别的地方,它并未创建整个新 TrainJourney对象的副本——如果a是n个元素的序列,b是m个元素的序列,那么调用这个函 数后,它返回的是一个由n+m个元素组成的序列,这个序列的前n个元素是新创建的,而后m个元 素和TrainJourney对象b是共享的。
另一个例子:
先前我们使用的是链表的例子,现在我们试试其他数据格式,最常见的就是二叉树
class Tree {
public String key;
public int val;
public Tree left, right;
public Tree (String key, int newval, Tree l, Tree r) {
this.key = key;
this.val = newval;
this.left = l;
this.right = r;
}
}
复制代码
这时候,我们希望根据key更新二叉树的val,一般的写法如下:
public static Tree update (String key, int newval, Tree t) {
if (t == null) {
t = new Tree(key, newval, null, null);
} else if (key.equals(t.key)) {
t.val = newval;
} else if (key.compareTo(t.key) < 0) {
t.left = update (key, newval, t.left);
} else {
t.right = update (key, newval, t.right);
}
}
复制代码
但是这种方法都会对现有的树进行修改,这意味着使用树存放映射关系的所有用户都会感知到这些修改,即破坏了原来的数据结构。 那么函数式编程是怎么样的呢?
public static Tree append (String k, int newval, Tree t) {
return t == null ? new Tree (key, newval, null, null) :
k.equals(t.key) ?
new Tree (k, newval, t.left, t.right) :
k.compareTo(t.key) < 0 ?
new Tree (k, newval, append (k, newval, t.left), t.right) :
new Tree (k, newval, t.left, append (k, newval, t.right));
}
复制代码
这段代码中,我们只用一行语句进行条件判断,没有采用if-else-then是为了强调,该写法没有任何副作用。不过如果采用if-else-then语句也可以,在每一个条件判断之后都加上return.