Java8 函数式【1】:一文读懂逆变
Java8 函数式【1】:一文读懂逆变
禁止转载
- pure function
- 协变
- 逆变
Java8 引入了函数式接口,从此方法传参可以传递函数了,有人说:
不就是传一个方法吗,语法糖!
lambda表达式?语法糖!
你是否认为协变和逆变只是定义了集合间的关系,如果你的回答是是,那么这篇文章会改变你原有的看法。
尝试回答一下问题:
- 函数和方法有区别吗?
- 协变是什么,有什么使用场景
- 逆变是什么,有什么使用场景
一、纯函数—没有副作用
纯函数的执行不会带来对象内部参数、方法参数、数据库等的改变,这些改变都是副作用。比如Integer::sum是一个纯函数,输入为两个int,输出为两数之和,两个输入量不会改变,在Java 中可以申明为final int类型。
副作用的执行
Java对于不变类的约束明显不足,比如final array只能保证引用的指向不变,array内部的值还是可以改变的,如果存在第二个引用指向相同的array,那么将无法保证array不可变;标准库中的collection常用的还是属于可变muttable类型,可变类型在使用时很便利。
在函数式思想下,函数是一等公民,函数是有值的,比如Integer::sum就是函数类型BiFunction<Integer, Integer, Integer>的一个值,没有副作用的函数保证了函数可以看做一个黑盒,一个固定的输入便有固定的输出。
那么Java中对象的方法是纯函数吗?
大多数时候不是。对象的方法受到对象的状态影响,如果对象的状态不发生改变,同时不对外部产生影响(比如打印字符串),可以看做纯函数。注意:Java中的final类无法保证内部参数状态不发生改变。
那么纯函数的限制这么多,究竟有什么用呢?相信看到最后你会懂的。
本文之后讨论的函数都默认为纯函数。
二、协变—更抽象的继承关系
协变和逆变描述了继承关系的传递特性,协变比逆变更好理解,逆变我放到后面说。
学习这些概念前先不要考虑Java对泛型的实现,它会影响你的正确认识。当你对这些概念理解了之后,再往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简洁一些,也是静态强类型语言。你可以使用网络上的在线运行环境运行scastie.scala-lang.org。
// scala 变量名在前,类型在后,函数返回类型在括号后,可以省略
class Fruit(val name: String) {}
class Apple extends Fruit("苹果") {}
class Orange extends Fruit("橙子") {}
// 榨汁机,T表示泛型,<:表示匹配上界(榨汁机只能榨果汁),-T 表示T支持逆变
class Juicer[-T <: Fruit] {
def accept(fruit: T) = s"${fruit.name}汁"
}
val appleJuicer: Juicer[Apple] = Juicer[Fruit]()
println(appleJuicer.accept(Apple()))
// 编译不通过,因为appleJuicer的类型是Juicer[Apple]
// 虽然声明appleJuicer时传递的值是水果榨汁机,但是编译器只做类型检查,Juicer[Apple]类型不能接受其他水果
println(appleJuicer.accept(Orange()))
榨汁机 is-a 榨苹果汁机,因为榨汁机可以榨苹果。
逆变难以理解的点就在于逆变考虑的是函数的功能,而不是函数具体的参数。
参数传参原则上都可以支持逆变,因为对于纯函数而言,参数值并不可变。
再举一个例子,Java8 中stream的map方法需要的参数就是一个函数:
// map方法声明
<R> Stream<R> map(Function<? super T, ? extends R> mapper);
// 此时方法的参数就是T,我们传递的mapper的入参可以为T的父类, 因为mapper支持参数逆变
// 如下程序可以运行
// 你可以对任意一个Stream<T>流使用map(Object::toString),因为在Java中所有类都继承自Object。
Stream.of(1, 2, 3).map(Object::toString).forEach(System.out::println);
问题可以再复杂一点,如果函数的参数为集合类型,还可以支持逆变吗?
当然可以,如前所述,逆变考虑的是函数的功能,传入一个更为一般的函数也可以处理具体的问题。
// Scala中可以使用 ::: 运算符合并两个List, 下一行是List中对方法:::的声明
// def ::: [B >: A](prefix: List[B]): List[B]
// 这个方法在Java很难实现,你可以看看ArrayList::addAll的参数, 然后想想曲线救国的方案,下一篇文章我会详细讨论
// usage
val list: List[Fruit] = List(Apple()) ::: (List(Fruit("水果")))
println(list)
// output: List(Playground$Apple@74046e99, Playground$Fruit@8f0fecd)
总结一下:函数的入参可以支持逆变,即参数的继承关系和函数的继承关系相反,逆变的函数更通用。
不知道你是否还记得Effective Java中提出的PECS法则,Provider-extends, Consumer-super,这个法则和本文所说的协变、逆变又有什么关系呢?
其实PECS法则从使用角度实现了许多泛型方法,下一篇文章再详细说吧。
Java8 函数式【1】:一文读懂逆变的更多相关文章
- 一文读懂HTTP/2及HTTP/3特性
摘要: 学习 HTTP/2 与 HTTP/3. 前言 HTTP/2 相比于 HTTP/1,可以说是大幅度提高了网页的性能,只需要升级到该协议就可以减少很多之前需要做的性能优化工作,当然兼容问题以及如何 ...
- 一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现
一文读懂AI简史:当年各国烧钱许下的愿,有些至今仍未实现 导读:近日,马云.马化腾.李彦宏等互联网大佬纷纷亮相2018世界人工智能大会,并登台演讲.关于人工智能的现状与未来,他们提出了各自的观点,也引 ...
- 一文读懂高性能网络编程中的I/O模型
1.前言 随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力.本文(和下篇<高性能网络编程(六):一文读懂高性能网络编程中的线程模型>)旨在为大家提供有用的 ...
- 从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路
本文原作者阮一峰,作者博客:ruanyifeng.com. 1.引言 HTTP 协议是最重要的互联网基础协议之一,它从最初的仅为浏览网页的目的进化到现在,已经是短连接通信的事实工业标准,最新版本 HT ...
- 一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm)
一文读懂 深度强化学习算法 A3C (Actor-Critic Algorithm) 2017-12-25 16:29:19 对于 A3C 算法感觉自己总是一知半解,现将其梳理一下,记录在此,也 ...
- [转帖]MerkleDAG全面解析 一文读懂什么是默克尔有向无环图
MerkleDAG全面解析 一文读懂什么是默克尔有向无环图 2018-08-16 15:58区块链/技术 MerkleDAG作为IPFS的核心数据结构,它融合了Merkle Tree和DAG的优点,今 ...
- [转帖]一文读懂 HTTP/2
一文读懂 HTTP/2 http://support.upyun.com/hc/kb/article/1048799/ 又小拍 • 发表于:2017年05月18日 15:34:45 • 更新于:201 ...
- [转帖]从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路
从HTTP/0.9到HTTP/2:一文读懂HTTP协议的历史演变和设计思路 http://www.52im.net/thread-1709-1-2.html 本文原作者阮一峰,作者博客:r ...
- 一文读懂HDMI和VGA接口针脚定义
一文读懂HDMI和VGA接口针脚定义 摘自:http://www.elecfans.com/yuanqijian/jiekou/20180423666604.html HDMI概述 HDMI是高清 ...
随机推荐
- 排序算法详解(java代码实现)
排序算法大致分为内部排序和外部排序两种 内部排序:待排序的记录全部放到内存中进行排序,时间复杂度也就等于比较的次数 外部排序:数据量很大,内存无法容纳,需要对外存进行访问再排序,把若干段数据一次读 ...
- 99乘法表 java for循环
public static void main(String[] args) { //0-100的奇数和偶数和 int jsum=0; int osum=0; for (int i = 0; i &l ...
- ElasticSearch7.3学习(二十三)----RestHighLevelClient Java api实现match_all、ids、match、term、multi_match、bool、filter、sort等不同的搜索方式
1.数据准备 首先创建book索引 PUT /book/ { "settings": { "number_of_shards": 1, "number ...
- 我用13行摆烂了你的oj4
13行代码AC oj4是怎么回事呢?13行代码AC oj4相信大家都很熟悉,但是13行代码AC oj4是怎么回事呢,下面就让小编带大家一起了解吧.13行代码AC oj4,其实就是13行代码AC oj4 ...
- 试驾 Citus 11.0 beta
https://www.citusdata.com/blog/2022/03/26/test-drive-citus-11-beta-for-postgres/ Citus 11.0 beta 的最大 ...
- 好客租房14-在jsx中使用javascript表达式的注意点
注意点 单大括号中可以使用任意的表达式 jsx自身也是js表达式 注意:js中的对是一个例外 写在style样式中 //导入react import React from "reac ...
- 随笔总结:8086CPU的栈顶超界问题
我们学习编程都知道栈的超界限问题是非常严重的问题,他可能会覆盖掉其他数据,并且我们不知道这个数据是我们自己保存的用于其他用途的数据还是系统的数据,这样常常容易引发一连串的问题. 在学习汇编的时候,我们 ...
- 探究MySQL中SQL查询的成本
成本 什么是成本,即SQL进行查询的花费的时间成本,包含IO成本和CPU成本. IO成本:即将数据页从硬盘中读取到内存中的读取时间成本.通常1页就是1.0的成本. CPU成本:即是读取和检测是否满足条 ...
- git指令使用
仓库为空,本地创建git项目之后提交到仓库中1.创建项目文件夹(本地git仓库)2.在项目文件夹中右键:选择Git Bash3.初始化项目:git init -- 会出现一个.git的隐藏文件夹4.将 ...
- 8条github使用小技巧
1 简介 作为全球最大的开源及私有软件项目托管社区平台,github可以显著地帮助从事编程相关工作的人员提升自己的技术水平,也是费老师我日常最常浏览学习的技术类网站. github为了使得其使用更加便 ...