Android 开发者如何使用函数式编程 (二)
如果你没有读过第一部分,请到这里读:
在上一篇帖子中,我们学习了纯粹性*、副作用和排序**。在本部分中,我们将讨论不变性和并发。
不变性不变性是指一旦一个值被创建,它就不可以被修改。
假设我有一个像这样的 Car 类:
public final class Car { private String name; public Car(final String name) { this.name = name; public void setName(final String name) { this.name = name; public String getName() { return name;
因为它有一个 setter,我可以在创建之后修改车的名称:
Car car = new Car("BMW"); car.setName("Audi");
这个类不是不可变的。他在创建之后可以被改变。
我们把它变成不可变的。要做到这一点,我们必须:
把 name 变量设为 final。 移除 setter。 把这个类也设为 final,这样另一个类就不可以继承它并修改它的内容。public final class Car { private final String name; public Car(final String name) { this.name = name; public String getName() { return name;
如果现在有人需要创建一个新的 car,他们需要初始化一个新的对象。没有人可以在 car 被创建之后修改它。这个类现在是不可变的了。
但是 getName() 方法呢?它在把名称返回给外部世界对吧?如果有人在通过 getter 取得引用之后修改了 name 的值怎么办?
在 Java 中,string在默认情况下是不可变的。哪怕有人获得了对 name string 的引用并修改它,他们也只能得到 name string 的拷贝,原先的 string 保持不变。
但是可变的东西怎么办?比如一个 list?我们修改一下 Car 类,使它具有一个驾驶员的 list。
public final class Car { private final List String listOfDrivers; public Car(final List String listOfDrivers) { this.listOfDrivers = listOfDrivers; public List String getListOfDrivers() { return listOfDrivers;
在这种情况下,有人可以通过 getListOfDrivers() 方法取得我们内部 list 的一个引用,并修改这个 list。这样,我们的类就是可变的了。
要让它不可变,我们必须在 getter 中返回一个 list 的深度拷贝。这样,新的 list 就可以被调用者安全地修改。深度拷贝的含义是我们递归地复制所有依赖它的数据。例如,如果这是一个 Driver 类的 list而不是简单的 string 列表,我们就必须复制每一个Driver 对象。否则,我们就会创建一个新的 list,其内容是对原先 Driver 对象的引用,而这些对象是可变的。在我们的类中,由于这个 list 是由不可变的 string 组成的,我们可以这样创建一个深度拷贝:
public final class Car { private final List String listOfDrivers; public Car(final List String listOfDrivers) { this.listOfDrivers = listOfDrivers; public List String getListOfDrivers() { List String newList = new ArrayList (); for (String driver : listOfDrivers) { newList.add(driver); return newList;
现在这个类就是真正不可变的了。
并发好了,不可变是很酷,但为什么要用它?我们在第一部分中已经讨论过,纯函数让我们很容易地实现并发。而且,如果一个对象是不可变的,它就很容易在纯函数中使用,因为你不能通过改变它而造成副作用。
来看一个例子。假设我们在 Car 中添加一个 getNoOfDrivers 方法,并允许外部调用者修改 driver 的数量,从而使它可变:
public class Car { private int noOfDrivers; public Car(final int noOfDrivers) { this.noOfDrivers = noOfDrivers; public int getNoOfDrivers() { return noOfDrivers; public void setNoOfDrivers(final int noOfDrivers) { this.noOfDrivers = noOfDrivers;
假设有两个线程共享 Car 类的实例:Thread_1 和 Thread_2。Thread_1 需要基于 driver 的数量做一些计算,所以它调用了getNoOfDrivers()。同时 Thread_2 开始执行,并修改了 noOfDrivers 变量。Thread_1 并不知道这个改变,愉快地继续它的计算。这些计算是不对的,因为 Thread_2 已经修改了变量的状态,而 Thread_1 并不知道。
下面的流程图说明了这个问题:
这是一个名为“读-修改-写问题”的典型资源竞争。传统的解决方案是使用锁和互斥。这样,同时只有一个线程可以操纵共享数据,在操作结束之后才释放锁(在我们的例子中,Thread_1 将持有对 Car 的锁,直到它完成计算)。
这种基于锁的资源管理是很难以保证安全的。它会造成极其难以分析的并发 bug。许多程序员在面对死锁和活锁时会失去理智。
不可变性如何解决这个问题呢?我们再次把 Car 设为不可变:
public final class Car { private final int noOfDrivers; public Car(final int noOfDrivers) { this.noOfDrivers = noOfDrivers; public int getNoOfDrivers() { return noOfDrivers;
现在,Thread_1 可以放心地计算,因为 Thread_2 保证无法修改这个对象。如果 Thread_2 想要修改 Car,那么它将会创建它自己的拷贝,而 Thread_1 完全不会受到影响。不需要任何锁。
不可变性保证共享数据在默认状况下就是线程安全的。不应该被修改的东西是不能被修改的。
如果我们需要全局可变状态怎么办?要写出有用的应用,我们在很多情况下需要共享可变的状态。我们可能会真正需要更新 noOfDrivers ,并把改变反映到整个系统中去。我们在下一章讨论函数式架构时,将使用状态隔离处理这种情况,并把副作用推到系统的边缘。
持久数据结构不可变对象可能很好,但如果我们不加限制地使用它们,它们将会给垃圾回收器造成负担,从而导致性能问题。函数式编程向我们提供具有不可变性,并能最小化对象创建的数据结构。这些专门化的数据结构被称为持久数据结构。
持久数据结构在被修改时,总会保留自己之前的版本。这些数据结构实际上是不可变的。对它们的操作不会(可见地)更新数据结构,而是返回一个新的修改过的结构。
假设我们需要把这些 string 存储在内存中:reborn, rebate, realize, realizes, relief, red, redder。
我们可以分开储存它们,但这需要的内存超出必要的限度。如果仔细看的话,我们可以看到这些 string 有很多共同的字符,我们可以用一个 trie 树储存它们(并不是所有的 trie 树都是持久的,但它是我们用来实现持久数据结构的工具之一):
这是持久数据结构的基本工作原理。如果一个新的 string 被加入,我们就创建一个新的节点,并把它链接到正确的位置。如果一个使用这个结构的对象需要删除一个节点,我们只要停止引用它即可。然而,实际的节点不会被从内存中删除,这样副作用就可以被避免。这保证引用这个数据结构的其它对象可以继续使用它。如果没有其它对象引用它,我们可以回收整个结构以收回内存。
在 Java 中使用持久数据结构并不是一个激进的想法。Clojure 是一个函数式语言,它在 JVM 上运行,并有一整个标准库的持久数据结构。你可以在 Android 代码中直接使用 Clojure 的标准库,但它很大而且有很多方法。我找到了一个更好的替代方法:一个叫做 PCollections 的库。它有 427 个方法和 48Kb dex 文件大小 ,很适合我们的需要。
作为一个例子,这是我们使用 PCollections 创建并使用一个持久链表时的情形:
ConsPStack String list = ConsPStack.*empty*(); System.*out*.println(list); // [] ConsPStack String list2 = list.plus("hello"); System.*out*.println(list); // [] System.*out*.println(list2); // [hello] ConsPStack String list3 = list2.plus("hi"); System.*out*.println(list); // [] System.*out*.println(list2); // [hello] System.*out*.println(list3); // [hi, hello] ConsPStack String list4 = list3.minus("hello"); System.*out*.println(list); // [] System.*out*.println(list2); // [hello] System.*out*.println(list3); // [hi, hello] System.*out*.println(list4); // [hi]
可见,没有任何一个 list 是在原位被修改的。每次进行一个修改时,它都会返回一个新的拷贝。
PCollections 有一些标准持久数据结构。它们是针对多种不同的用例实现的,都很值得探索。他们都很适合与易用的 Java 的标准集合库一起使用。
持久数据结构的范围是很广泛的,而这一部分只是触及了冰山的一角。如果你对学习更多相关知识感兴趣,我强烈推荐 Chris Okasaki 的纯函数数据结构。
总结不可变性和纯粹性是帮助我们写出安全的并发代码的强力组合。现在我们已经学习了足够多的概念,我们可以在下一部分中看一看如何为 Android 应用设计函数式框架。
额外内容我在 Droidcon India 中做了一个关于不可变性和并发的报告。希望你们喜欢。
Code For Better 谷歌开发者之声——Android 中的 Volley 库 Volley是一个HTTP 库,它使 Android 应用程序的网络变得非常简单和快速。它由 Google 开发并在 2013 年 Google I/O 期间推出。它的开发是因为 Android SDK 中缺少能够在不影响用户体验的情况下工作的网络类。尽管 Volley 是 Android 开源项目 (AOSP) 的一部分,但 Google 在 2017 年 1 月宣布 Volley 将迁移到一个独立的库。它管理网络请求的处理和缓存,并节省开发人员一次又一次编写相同的网络调用/缓存代码的宝贵时间。Volley不适合大型下载或流式操作,因为 Volley 在解析期间将所有响应保存在内存中。
面向Android开发者的Dart学习教程 近日Google发布了Flutter2.0, 使用Flutter开发的App可以在不做修改的情况下发布到更多的主流平台;再加上早些时候Fuchsia也宣布将Dart作为主要的UI开发语言,如果未来你想
相关文章
- Android:使用ZXing生成二维码(支持加入Logo图案)
- android文件的写入与读取---简单的文本读写context.openFileInput() context.openFileOutput()
- Android面试题摘录
- 《Android 应用案例开发大全(第二版)》——6.2节Intent对象
- 2017.1-TOP5 Android开源库
- Android SurfaceView实战 打造抽奖转盘
- 国外创业者分享之Android 与 iOS:猜猜哪个能让开发者赚得更多
- 2023 年开发 Android 应用程序,每个 Android 开发者在 2023 年都应该知道的常见和新的最佳实践和建议
- Android事件机制之一:事件传递和消费
- 对 Android 开发者来说,Google Play 到底哪里不一样?
- 英特尔 Android* 开发者指南上的对等应用
- android 使用post 提交
- [Android] android:visibility属性应用