上一篇响应式编程开源库 RxJava2——起源
)主要是对RxJava的来源,以及Observble和Observer和一些相关联的基本概念简介。上篇文章应该能基本了解观察者模式,接下来就需要更深入了解其他重要名词——函数式接口、默认方法、纯函数、函数的副作用、高阶函数、可变的和不可变的、函数式编程和 Lambda 表达式。
1.函数式接口
一言以蔽之,函数式接口是有且只有一个抽象方法的接口。换言之,任何拥有唯一抽象方法的接口都可以被称为函数式接口。可以用注解 @FunctionalInterface
标明,也可忽略注解。在 Java 8 里,我们可以在接口中定义一个包含方法体的方法,这个方法叫默认方法,如下面所示。
@FunctionalInterface
public interface Test {
void test();
default void test2(){
}
}
如果接口定义默认方法或者继承并重写 java.lang.Object 类里的任何方法。那个接口还是函数式接口,这是因为在Java中所有类都是 Object的子类的。
@FunctionalInterface
public interface Test {
void test();
default void test2(){
}
@Override
String toString();
}
所以函数式接口满足下列三个条件:
只有一个抽象方法 - 可以有默认方法 - 可以使用 java.lang.Object的方法。
在 Java 8 里有一个新的工具包 java.util.function。在这个包里,所有的接口都是函数式接口。当我们需要用到流(Stream) API 时,这个工具包很有用。
关于Java8中的Stream API额外提一下,它必须是在SDK版本大于24才能使用。这是一个强大的API,能将Java中所有实现Iterable接口的类转变为数据流,在Java中也就是所有集合,都能转变为数据流。并且能方便对数据流做出相应操作,例如将集合中的值转换并把流中的每个数据执行打印操作。
List<Integer> list=new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add(i);
}
list.stream().map(new Function<Integer, String>() {
@Override
public String apply(Integer integer) {
return integer+"转换int->String";
}
}).forEach(new Consumer<String>() {
@Override
public void accept(String s) {
Log(s);
}
});
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 0转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 1转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 2转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 3转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 4转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 5转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 6转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 7转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 8转换int->String
2019-05-09 18:03:39.494 30878-30878/com.example.studyrx I/Study RxJava: 9转换int->String
2.纯函数
首先我们要理解什么是函数。在Java中函数也叫方法,是具备特定功能的一段代码块,解决了重复性代码的问题,目的是为了提高程序的复用性和可读性。
现在忘掉这个概念,我们从纯数学的方面理解函数。
函数的定义:给定一个数集A,假设其中的元素为x。现对A中的元素x施加对应法则f,记作f(x),得到另一数集B。假设B中的元素为y。则y与x之间的等量关系可以用y=f(x)表示。函数概念含有三个要素:定义域A、值域C和对应法则f。其中核心是对应法则f,它是函数关系的本质特征。
一个根据输入决定输出的方程式,就是函数。如 f(x) = x+3。
数学中对于给定的输入值,会得到唯一的结果,这就叫纯函数。但是编程中的函数给同样的输入可以有不同的输出,因为它依赖于其它数值。比如我们计算汇率:
public float convertRMB2Dollar(int RMB){
return RMB*getTodayRateFromAPI();
}
所以编程语言中的纯函数是返回值由它的输入值决定,而且没有明显可见的副作用。
输入输出数据流全是显式(Explicit)的。 显式(Explicit)的意思是,函数与外界交换数据只有一个唯一渠道——参数和返回值。函数从函数外部接受的所有输入信息都通过参数传递到该函数内部。函数输出到函数外部的所有信息都通过返回值传递到该函数外部。
3.函数的副作用
任何不纯的函数叫非纯函数,它可能产生副作用。或者一些函数本身是纯函数(指对于给定的输入值可以得出相同的输出值),但是如果它在产生结果的时候与外界发生了数据交换,那么我们就不能说这是一个纯函数。
第一类非纯函数的典型就是 Random 函数。对于给定的一个输入值,它总是返回不同的结果。
第二类副作用的典型是 println() ,它是一个非纯的函数。因为它将输出值转去了输入输出设备(而不是作为函数返回值输出),所以产生了副作用。任何纯函数一旦用 println() 来注释打印,那它就不再是纯函数了。
纯函数:
public int squre(int x){
return x*x;
}
因为副作用而非纯的的函数:
public int squre(int x){
System.out.println(x*x);
return x*x;
}
非纯函数(与外界数据交换):
public void login(String username, String password, Callback c){
API.login(username, password, callback);
}
函数副作用指当调用函数时,除了返回函数值之外,还对主调用函数产生附加的影响。例如修改全局变量(函数外的变量)或修改参数。
4.Java 可变对象和不可变对象
不可变对象(Immutable Objects)即对象一旦被创建它的状态(对象的数据,也即对象属性值)就不能改变,反之即为可变对象(Mutable Objects)。
不可变对象的类即为不可变类(Immutable Class)。Java平台类库中包含许多不可变类,如String、基本类型的包装类、BigInteger和BigDecimal等。
不可变对象有很多优缺点:
构造、测试和使用都很简单
线程安全且没有同步问题,不需要担心数据会被其它线程修改
当用作类的属性时不需要保护性拷贝
可以很好的用作Map键值和Set元素
不可变对象最大的缺点就是创建对象的开销,因为每一步操作都会产生一个新的对象。
可以遵照以下几点来编写一个不可变类:
确保类不能被继承 - 将类声明为final, 或者使用静态工厂并声明构造器为private
声明属性为private 和 final
不要提供任何可以修改对象状态的方法 - 不仅仅是set方法, 还有任何其它可以改变状态的方法
如果类有任何可变对象属性, 那么当它们在类和类的调用者间传递的时候必须被保护性拷贝
下面举例感受可变和不可变:
int array []= {1,2,3,4,5};
for (int i = 0; i < array.length; i++) {
array[i] = array[i] * 2;
}
在 Java 或者命令式编程中,上面的代码是可变的。它改变了原本的数组值。但是在函数式编程里,如果我做了同样的事情,我总是获得与 2 相乘后的数值组成的新数组,而我原来的数据仍然保持不变。
Integer array []= {1,2,3,4,5};
Arrays.stream(array).map(v->v*2).forEach(i-> System.out.print(i+" "));
for (int i = 0; i < array.length; i++) {
System.out.print(array[i]+ " ");
}
那么可变和不可变有什么用,这里简单举例:
public static void main(String[] args) {
Integer array []= {1,2,3,4,5};
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < array.length; i++) {
array[i] = array[i]+1;
}
}
}).start();
for (int i = 0; i < array.length; i++) {
System.out.println(square(array[i]));
}
}
public static int square(int a){
return a*a;
}
在这段代码里我们使用了子线程对数组做+1操作,在主线程对数组做平方操作。作为开发者,我们知道这里的打印结果根本不可控制。例如我运行后是如下结果:
这时候数据是可变的。那么现在对数据的不可变性进行严格控制。会是什么结果?
public static void main(String[] args) {
Integer array []= {1,2,3,4,5};
new Thread(new Runnable() {
@Override
public void run() {
Observable.fromArray(array)
.map(integer -> integer+1)
.subscribe(integer -> {});
}
}).start();
Observable.fromArray(array)
.map(integer -> square(integer))
.subscribe(integer -> System.out.println(integer));
}
public static int square(int a){
return a*a;
}
这时候因为我的程序没有对数组做直接改变,而是拷贝了我的数据。这时候数组是不可变的。这就能避免很多问题,比如这里的线程安全问题,我们根本不用考虑。
5.高阶函数
在数学和计算机科学中,高阶函数是至少满足下列一个条件的函数:
- 接受一个或多个函数作为输入
- 输出一个函数
数学
在数学中它们叫算子(运算符)或泛函。微积分中的导数就是常见的例子,因为它映射一个函数到另一个函数。
计算机科学
在无类型 lambda演算,所有函数都是高阶的;在有类型 lambda演算(大多数函数式编程语言都从中演化而来)中,高阶函数一般是那些函数型别包含多于一个箭头的函数。在函数式编程中,返回另一个函数的高阶函数被称为柯里化的函数。
在很多函数式编程语言中能找到的 map 函数是高阶函数的一个例子。它接受一个函数 f 作为参数,并返回接受一个列表并应用 f 到它的每个元素的一个函数。
在编程中,拥有至少一个函数类型为参数的函数,或着返回一个函数的函数叫做高阶函数。函数式编程就是指这种高度抽象的编程范式。
上面所介绍的的纯函数,高阶函数,不可变的都是函数式范式。在面向对象编程时我们经常要管理对象的状态,但是在函数式程序里,我们有数据,管理好了不可变性,我们可以大胆地做运算。
进入Lambda表达式之前先复习下之前的内容:
函数式接口 - 有且仅有一个抽象方法的接口,可以有默认方法,可以包含Object的方法。
默认方法 - 在 Java 8 里,我们可以在接口中定义有方法体的方法,这些叫默认方法。
纯函数 - 一个函数的返回值仅由输入值决定,没有明显可见的副作用,函数与外界交换数据只有一个唯一渠道——参数和返回值。
高阶函数 - 拥有至少一个函数类型为参数的函数,或着返回一个函数的函数叫做高阶函数。
6.Lambda 表达式
在计算机编程中,lambda 表达式,也叫匿名函数,是指一类无需定义标识符(函数名)的函数或子程序。
我们对一个控件设置监听是这样写的:
text.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
}
});
而通过Lambda表达式书写就是:
text.setOnClickListener(v -> {
});
初学的话会很难理解,很难看懂。初学的时候真的很困惑,编译器是如何知道我这里在做什么。这里就是函数式接口的运用了,编译时接口中只有一个方法,Java 自动知道‘v'是一个 View。这个接口只有一个抽象方法,它的参数是一个 view,如下所示
public interface OnClickListener {
/**
* Called when a view has been clicked.
*
* @param v The view that was clicked.
*/
void onClick(View v);
}
所以可以简单理解为Lambda表达式等价于函数式接口。