禁止转载
重写了之前博客写的泛型相关内容,全部整合到这一篇文章里了,把坑都填了,后续不再纠结这些问题了。本文深度总结了函数式思想、泛型对在Java中的应用,解答了许多比较难的问题。
- 纯函数
- 协变
- 逆变
- 泛型通配符
- PECS法则
- 自限定
Part 1: 协变与逆变
Java8 引入了函数式接口,从此方法传参可以传递函数了,有人说这是语法糖。
实际上,这是编程范式的转换,思想体系的变化。
一、纯函数—没有副作用
纯函数的执行不会带来对象内部参数、方法参数、数据库等的改变,这些改变都是副作用。比如Integer::sum是一个纯函数,输入为两个int,输出为两数之和,两个输入量不会改变,在Java 中可以申明为final int类型。
副作用的执行
Java对于不变类的约束明显不足,比如final array只能保证引用的指向不变,array内部的值还是可以改变的,如果存在第二个引用指向相同的array,那么将无法保证array不可变;标准库中的collection常用的还是属于可变mutable类型,可变类型在使用时很便利。
在函数式思想下,函数是一等公民,函数是有值的,比如Integer::sum就是函数类型BiFunction的一个值,没有副作用的函数保证了函数可以看做一个黑盒,一个固定的输入便有固定的输出。
那么Java中对象的方法是纯函数吗?
大多数时候不是。对象的方法受到对象的状态影响,如果对象的状态不发生改变,同时不对外部产生影响(比如打印字符串),可以看做纯函数。
本文之后讨论的函数都默认为纯函数。
二、协变—更抽象的继承关系
协变和逆变描述了继承关系的传递特性,协变比逆变更好理解。
协变的简单定义:如果A是B的子类,那么F(A)是F(B) 的子类。F表示的是一种类型变换。
比如:猫是动物,表示为Cat < Animal,那么一群猫是一群动物,表示为List[Cat] < List[Aniaml]。
上面的关系很好理解,在面向对象语言中,is-a表示为继承关系,即猫是动物的子类(subtype)。
所以,协变可以这样表示:
A < B ⇒ F(A) < F(B)
在猫的例子中,F表示集合。
那么如果F是函数呢?
我们定义函数F=Provider,函数的类型定义包括入参和出参,简单地考虑入参为空,出参为Animal和Cat的情况。简单理解为方法F定义为获取猫或动物。
那么Supplier作用Cat和Animal上,原来的类型关系保持吗?
答案是保持,Supplier[Cat] < Supplier[Animal]。也就是说获取一只猫就是获取一只动物。转换成面向对象的语言,Supplier[Cat]是Supplier[Animal]的子类。
在面向对象语言中,子类关系常常表现为不同类型之间的兼容。也就是说传值的类型必须为声明的类型的子类。如下面的代码是好的- List[User] users = List(user1, user2)
- List[Animal] animals = cats
- Supplier[Animal] supplierWithAnimal = supplierWithCat
- // 使用Supplier[Animal],实际上得到的是Cat
- Animal animal = supplierWithAnimal.get()
复制代码 我们来看下某百科对于里氏替换原则(LSP)的定义:
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 里氏代换原则中说,任何父类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。里氏代换原则是对“开-闭”原则的补充。实现“开-闭”原则的关键步骤就是抽象化。而子类与父类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。
Animal animal = new Cat(”kitty”);
在UML图中,一般父类在上,子类在下。因此,子类赋值到父类声明的过程可以形象地称为向上转型。
总结一下:协变是LSP的体现,形象的理解为向上转型。
三、逆变—难以理解的概念
与协变的定义相反,逆变可以这样表示:
A < B ⇒ F(B) < F(A)
最简单的逆变类是Consumer[T],考虑Consumer[Fruit] 和 Consumer[Apple]。榨汁机就是一类Consumer,接受的是水果,输出的是果汁。我定义的函数accpt为了避免副作用,返回字符串,然后再打印。
下面我用scala写的示例,其比Java简洁一些,也是静态强类型语言。你可以使用网络上的 playground 运行(eg: scastie.scala-lang.org)。
[code]// scala 变量名在前,类型在后,函数返回类型在括号后,可以省略class Fruit(val name: String) {}class Apple extends Fruit("苹果") {}class Orange extends Fruit("橙子") {}// 榨汁机,T表示泛型, |