Java8 函数式【1】:一文读懂逆变

Java8 函数式【1】:一文读懂逆变

禁止转载

  • pure function
  • 协变
  • 逆变

Java8 引入了函数式接口,从此方法传参可以传递函数了,有人说:
不就是传一个方法吗,语法糖!
lambda表达式?语法糖!
你是否认为协变和逆变只是定义了集合间的关系,如果你的回答是是,那么这篇文章会改变你原有的看法。
尝试回答一下问题:

  1. 函数和方法有区别吗?
  2. 协变是什么,有什么使用场景
  3. 逆变是什么,有什么使用场景

一、纯函数—没有副作用

纯函数的执行不会带来对象内部参数、方法参数、数据库等的改变,这些改变都是副作用。比如Integer::sum是一个纯函数,输入为两个int,输出为两数之和,两个输入量不会改变,在Java 中可以申明为final int类型。

副作用的执行

Java对于不变类的约束明显不足,比如final array只能保证引用的指向不变,array内部的值还是可以改变的,如果存在第二个引用指向相同的array,那么将无法保证array不可变;标准库中的collection常用的还是属于可变muttable类型,可变类型在使用时很便利。

在函数式思想下,函数是一等公民,函数是有值的,比如Integer::sum就是函数类型BiFunction

那么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是继承复用的基石,只有当子类可以替换掉父类,软件单位的功能不受到影响时,父类才能真正被复用,而子类也能够在父类的基础上增加新的行为。里氏代换原则是对”开-闭”原则的补充。实现”开-闭”原则的关键步骤就是抽象化。而子类与父类的继承关系就是抽象化的具体实现,所以里氏代换原则是对实现抽象化的具体步骤的规范。

Java8 函数式【1】:一文读懂逆变

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表示泛型,

榨汁机 is-a 榨苹果汁机,因为榨汁机可以榨苹果。

逆变难以理解的点就在于逆变考虑的是函数的功能,而不是函数具体的参数。

参数传参原则上都可以支持逆变,因为对于纯函数而言,参数值并不可变。

再举一个例子,Java8 中stream的map方法需要的参数就是一个函数:

// map方法声明
 Stream map(Function mapper);

// 此时方法的参数就是T,我们传递的mapper的入参可以为T的父类, 因为mapper支持参数逆变
// 如下程序可以运行
// 你可以对任意一个Stream流使用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法则从使用角度实现了许多泛型方法,下一篇文章再详细说吧。

Original: https://www.cnblogs.com/dahua-dijkstra/p/16198839.html
Author: 大华dijkstra
Title: Java8 函数式【1】:一文读懂逆变

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/713667/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • 使用6to5,让今天就来写ES6的模块化开发!

    http://es6rocks.com/2014/10/es6-modules-today-with-6to5/?utm_source=javascriptweekly&u…

    技术杂谈 2023年5月31日
    0129
  • dremio 对于parquet 文件的一些要求以及优化处理

    dremio 比较依赖parquet 存储格式,同时对于parquet 的处理进行了不少的优化 读parquet 文件 3.1.3 提供了支持非堆内存的操作,3.2 增强了对于云p…

    技术杂谈 2023年5月30日
    0165
  • PyQt5 窗口操作

    ################################ PyQt5中文网 – PyQt5全套视频教程 # https://www.PyQt5.cn/ # 主讲: 村长 #…

    技术杂谈 2023年5月31日
    093
  • Vue

    Vue 学习目标 前端知识体系 HTML(页面的结构)、CSS(表现层)、JavaScript(行为) HTML 就不说了。 CSS CSS层叠样式是一门标记语言,并不是编程语言,…

    技术杂谈 2023年7月11日
    077
  • manim 3.0优化

    1、注意不要在物体变换之后再添加其他相关物体,这样物体的初始化会在动画部分的后面 2、动画实现过程最主要还是物体的初始化,所以可以将动画部分和查看物体初始化部分分开(即将动画部分放…

    技术杂谈 2023年7月24日
    091
  • SQL–基础语句

    1 SELECT skucolor,clicks FROM shecharme;#SQL语句从Shecharme表&#x4E…

    技术杂谈 2023年7月24日
    081
  • golang 笔记

    for循环,一个key在一个map中,则一直迭代 go总是使用值传递,但是有些数据类型是引用类型,比如map, pointer, channel, slice是部分引用类型 在给函…

    技术杂谈 2023年7月11日
    084
  • CSRF攻击:陌生链接不要随便点

    中我们讲到了 XSS 攻击,XSS 的攻击方式是黑客往用户的页面中注入恶意脚本,然后再通过恶意脚本将用户页面的数据上传到黑客的服务器上,最后黑客再利用这些数据进行一些恶意操作。XS…

    技术杂谈 2023年5月31日
    0108
  • ADC平台与低代码开发

    1.什么是ADC ADC(Application Development Center)是一个低代码、多体验的开发平台,提供面向业务开发者的全场景开发平台,以及完整的资产生命周期工…

    技术杂谈 2023年5月30日
    0119
  • Dockerfile 使用 SSH docker build

    如果在书写 Dockerfile 时,有些命令需要使用到 SSH 连接,比如从私有仓库下载文件等,那么我们应该怎么做呢? Dockerfile 文件配置 为了使得 Dockerfi…

    技术杂谈 2023年7月10日
    0106
  • 关于bat获取当前时间的小BUG,求解决

    set filename=%date:~0,4%%date:~5,2%%date:~8,2%%time:~0,2%%time:~3,2%%time:~6,2%echo %filen…

    技术杂谈 2023年5月31日
    091
  • PDCA循环

    PDCA循环 近年来,软件项目的规模及其复杂性正在以空前的速度增长,互联网用户市场庞大,互联网公司和相应的软件产品层出不穷。快速响应需求变化往往是互联网行业的常态,软件产品的快速开…

    技术杂谈 2023年5月31日
    0117
  • 测试驱动开发(TDD)

    测试应用有很多方法,例如,黑盒测试、白盒测试、迭代测试等,然而,这些方法都是从宏观上描述测试的。为了在技术上保障测试的效果,Kent Beck(也是极限编程创始人)提出了在结果上进…

    技术杂谈 2023年5月31日
    0110
  • Upload 组件 报错 — [object File]

    博客园 :当前访问的博文已被密码保护 请输入阅读密码: Original: https://www.cnblogs.com/crazycode2/p/16538707.htmlAu…

    技术杂谈 2023年5月31日
    095
  • 通俗易懂讲枚举

    枚举使用关键字 enum 进行定义,每个元素都是一个实例,如下,FOO 和 BAR 都是一个 EnumClazz 实例。 public enum EnumClazz { FOO, …

    技术杂谈 2023年7月25日
    0102
  • hdu 1845

    一看题意就是二分匹配问题,建边是双向的,两个集合都是n个点 这题的图很特殊,每个点都要与三个点相连,在纸上画了六个点的图就感觉此图最大匹配肯定是六,除以2就是原图的匹配了,就感觉这…

    技术杂谈 2023年6月1日
    0112
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球