破坏式更新和函数式更新
什么是破坏式更新和函数式更新:
破坏式更新:
有一个方法,传入一个对象并返回结果。在方法结束之后传入的参数对象也被改变了,这就是破坏式更新。你不能保证调用这个方法之后后续是否还会使用传入的参数对象,因此破坏式更新在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.