Java 设计模式最佳实践:1~5(1)https://developer.aliyun.com/article/1426755
高阶函数
高阶函数是可以将其他函数作为参数,创建并返回它们的函数。它们通过使用现有的和已经测试过的小函数来促进代码重用。例如,在下面的代码中,我们计算给定温度(华氏度)的平均值(摄氏度):
jshell> IntStream.of(70, 75, 80, 90).map(x -> (x - 32)*5/9).average(); $4 ==> OptionalDouble[25.5]
注意在高阶map
函数中使用 Lambda 表达式。相同的 Lambda 表达式可以在多个地方用于转换温度。
jshell> IntUnaryOperator convF2C = x -> (x-32)*5/9; convF2C ==> $Lambda$27/1938056729@4bec1f0c jshell> IntStream.of(70, 75, 80, 90).map(convF2C).average(); $6 ==> OptionalDouble[25.5] jshell> convF2C.applyAsInt(80); $7 ==> 26Function
组合
在数学中,函数是用一个函数的输出作为下一个函数的输入而组合起来的。同样的规则也适用于函数式编程,其中一阶函数由高阶函数使用。前面的代码已经包含了这样一个示例,请参见map
函数中的andThen
纯函数的用法。
为了使函数的组成更加直观,我们可以用andThen
方法重写转换公式:
jshell> IntUnaryOperator convF2C = ((IntUnaryOperator)(x -> x-32)).andThen(x -> x *5).andThen(x -> x / 9); convF2C ==> java.util.function.IntUnaryOperator$$Lambda$29/1234776885@dc24521 jshell> convF2C.applyAsInt(80); $23 ==> 26
柯里化
柯里化是将一个 n 元函数转化为一系列或一元函数的过程,它是以美国数学家 Haskell Curry 的名字命名的。形式g:: x -> y -> z
是f :: (x, y) -> z
的柯里化形式。对于前面给出的平方半径公式,f(x,y) = x2 + y2
,一个柯里化版本,不使用双函数,将使用apply
多次。一个函数的单一应用只会用一个值替换参数,正如我们前面看到的。下面的代码展示了如何创建一个双参数函数,对于n
个参数,Function
类的apply
函数将有 n 个调用:
jshell> Function<Integer, Function<Integer, Integer>> square_radius = x -> y -> x*x + y*y; square_radius ==> $Lambda$46/1050349584@6c3708b3 jshell> List<Integer> squares = Arrays.asList(new Tuple<Integer, Integer>(1, 5), new Tuple<Integer, Integer>(2, 3)).stream(). map(a -> square_radius.apply(a.y).apply(a.x)). collect(Collectors.toList()); squares ==> [26, 13]
闭包
闭包是实现词汇作用域的一种技术。词法范围允许我们访问内部范围内的外部上下文变量。假设在前面的例子中,y
变量已经被赋值。Lambda 表达式可以保持一元表达式,并且仍然使用y
作为变量。这可能会导致一些很难找到的 bug,如在下面的代码中,我们希望函数的返回值保持不变。闭包捕获一个对象的当前值,正如我们在下面的代码中看到的,我们的期望是,add100
函数总是将 100 添加到给定的输入中,但是它没有:
jshell> Integer a = 100 a ==> 100 jshell> Function<Integer, Integer> add100 = b -> b + a; add100 ==> $Lambda$49/553871028@eec5a4a jshell> add100.apply(9); $38 ==> 109 jshell> a = 101; a ==> 101 jshell> add100.apply(9); $40 ==> 110
在这里,我们期望得到 109,但是它用 110 回答,这是正确的(101 加 9 等于 110);我们的a
变量从 100 变为 101。闭包需要谨慎使用,而且,根据经验,使用final
关键字来限制更改。闭包并不总是有害的;在我们想要共享当前状态的情况下(并且在需要的时候能够修改它),闭包非常方便。例如,我们将在需要提供数据库连接(抽象连接)的回调的 API 中使用闭包;我们将使用不同的闭包,每个闭包提供基于特定数据库供应商设置的连接,通常从外部上下文中已知的属性文件读取。它可以用函数的方式实现模板模式。
不变性
在《Effective Java》中,Joshua Bloch 提出了如下建议:将对象视为不可变的。在 OOP 世界中需要考虑这个建议的原因在于可变代码有许多可移动的部分;它太复杂,不容易理解和修复。促进不变性简化了代码,并允许开发人员专注于流,而不是关注一段代码可能产生的副作用。最糟糕的副作用是,一个地方的微小变化可能会在另一个地方产生灾难性的结果(蝴蝶效应)。可变代码有时很难并行化,并且常常使用不同的锁。
函子
函子允许我们对给定的容器应用函数。他们知道如何从包装对象中展开值,应用给定的函数,并返回另一个包含结果/转换包装对象的函子。它们很有用,因为它们抽象了多种习惯用法,如集合、Future
(Promise
)和Optional
。下面的代码演示了 Java 中的Optional
函子的用法,其中Optional
可以是一个给定的值,这是将函数应用于现有的包装值(5
的Optional
的结果):
jshell> Optional<Integer> a = Optional.of(5); a ==> Optional[5]
现在我们将函数应用于值为 5 的包装整数对象,得到一个新的可选保持值 4.5:
jshell> Optional<Float> b = a.map(x -> x * 0.9f); b ==> Optional[4.5] jshell> b.get() $7 ==> 4.5
Optional
是一个函子,类似于 Haskell 的Maybe
(只是| Nothing
),它甚至有一个静态Optional.empty()
方法,返回一个没有值(Nothing
)的Optional
。
应用
应用添加了一个新级别的包装,而不是将函数应用于包装对象,函数也被包装。在下面的代码中,函数被包装在一个可选的。为了证明应用的一个用法,我们还提供了一个标识(所有内容都保持不变)选项,以防所需的函数(在我们的例子中是toUpperCase
)为空。因为没有语法糖来自动应用包装函数,所以我们需要手动执行,请参阅get().apply()
代码。注意 Java9 added 方法Optional.or()
的用法,如果我们的输入Optional
为空,它将延迟返回另一个Optional
:
jshell> Optional<String> a = Optional.of("Hello Applicatives") a ==> Optional[Hello Applicatives] jshell> Optional<Function<String, String>> upper = Optional.of(String::toUpperCase) upper ==> Optional[$Lambda$14/2009787198@1e88b3c] jshell> a.map(x -> upper.get().apply(x)) $3 ==> Optional[HELLO APPLICATIVES]
这是我们的应用,它知道如何将给定的字符串大写。让我们看看代码:
jshell> Optional<Function<String, String>> identity = Optional.of(Function.identity()) identity ==> Optional[java.util.function.Function$$Lambda$16/1580893732@5c3bd550] jshell> Optional<Function<String, String>> upper = Optional.empty() upper ==> Optional.empty jshell> a.map(x -> upper.or(() -> identity).get().apply(x)) $6 ==> Optional[Hello Applicatives]
前面的代码是我们的应用,它将标识函数(输出与输入相同)应用于给定的字符串。
单子
单子应用一个函数,将一个包装值返回给一个包装值。Java 包含了Stream
、CompletableFuture
和已经出现的Optional
等示例。flatMap
函数通过将给定的函数应用于邮政编码映射中可能存在或不存在的邮政编码列表来实现这一点,如下代码所示:
jshell> Map<Integer, String> codesMapping = Map.of(400500, "Cluj-Napoca", 75001, "Paris", 10115, "Berlin", 10000, "New York") codesMapping ==> {400500=Cluj-Napoca, 10115=Berlin, 10000=New York, 75001=Paris} jshell> List<Integer> codes = List.of(400501, 75001, 10115, 10000) codes ==> [400501, 75001, 10115, 10000] jshell> codes.stream().flatMap(x -> Stream.ofNullable(codesMapping.get(x))) $3 ==> java.util.stream.ReferencePipeline$7@343f4d3d jshell> codes.stream().flatMap(x -> Stream.ofNullable(codesMapping.get(x))).collect(Collectors.toList()); $4 ==> [Paris, Berlin, New York]
Haskell 使用以下单子(在其他函数式编程语言中导入)。它们对于 Java 世界也很重要,因为它们具有强大的抽象概念:
- 读取器单子允许共享和读取环境状态。它在软件的可变部分和不可变部分之间提供了边缘功能。
- 写入器单子用于将状态附加到多个写入器,非常类似于记录到多个写入器(控制台/文件/网络)的日志过程。
- 状态单子既是读取器又是写入器。
为了掌握函子、应用和单子的概念,我们建议您查阅这个页面和这个页面。在这个页面的 Cyclops React 库里也有一些函数式的好东西。
Java 函数式编程简介
函数式编程是基于流和 Lambda 表达式的,两者都是在 Java8 中引入的。像 RetroLambda 这样的库允许 Java8 代码在旧的 JVM 运行时运行,比如 Java5、6 或 7(通常用于 Android 开发)。
Lambda 表达式
Lambda 表达式是用于java.util.functions
包接口的语法。最重要的是:
BiConsumer
:一种使用两个输入参数而不返回结果的操作,通常用在forEach
映射方法中。支持使用andThen
方法链接BiConsumers
。BiFunction
:通过调用apply
方法,接受两个参数并产生结果的函数。BinaryOperator
:对同一类型的两个操作数进行的一种操作,产生与操作数类型相同的结果,通过调用其继承的apply
方法来使用。它静态地提供了minBy
和maxBy
方法,返回两个元素中的较小值/较大值。BiPredicate
:由两个参数(也称为谓词)组成的布尔返回函数,用于调用其test
方法。Consumer
:使用单个输入参数的操作。就像它的二进制对应项一样,它支持链接,并通过调用它的apply
方法来应用,如下面的示例所示,其中使用者是System.out.println
方法:
jshell> Consumer<Integer> printToConsole = System.out::println; print ==> $Lambda$24/117244645@5bcab519 jshell> printToConsole.accept(9) 9
Function
:接受一个参数并产生结果的函数。它转换输入,而不是变异。它可以通过调用其apply
方法直接使用,使用andThen
链接,使用compose
方法组合,如下面的示例代码所示。这样,我们的代码就可以通过在现有函数的基础上构造新函数来保持 DRY(缩写为不要重复):
jshell> Function<Integer, Integer> square = x -> x*x; square ==> $Lambda$14/1870647526@47c62251 jshell> Function<Integer, String> toString = x -> "Number : " + x.toString(); toString ==> $Lambda$15/1722023916@77caeb3e jshell> toString.compose(square).apply(4); $3 ==> "Number : 16" jshell> square.andThen(toString).apply(4); $4 ==> "Number : 16"
Predicate
:一个参数的布尔返回函数。在下面的代码中,我们将测试字符串是否完全小写:
jshell> Predicate<String> isLower = x -> x.equals(x.toLowerCase()) isLower ==> $Lambda$25/507084503@490ab905 jshell> isLower.test("lower") $8 ==> true jshell> isLower.test("Lower") $9 ==> false
Supplier
:这是一个值供应器:
jshell> String lambda = "Hello Lambda" lambda ==> "Hello Lambda" jshell> Supplier<String> closure = () -> lambda closure ==> $Lambda$27/13329486@13805618 jshell> closure.get() $13 ==> "Hello Lambda"
UnaryOperator
:作用于单个操作数的一种特殊函数,其结果与其操作数的类型相同;可以用Function
代替。
流
流是一个函数管道,用于转换而不是变异数据。它们有创造者、中间者和终端操作。要从流中获取值,需要调用终端操作。流不是数据结构,不能重复使用,一旦被使用,如果第二次收集,它将保持关闭状态,java.lang.IllegalStateException
异常:流已经被操作或关闭,将被抛出。
流创建操作
流可以是连续的,也可以是并行的。它们可以从Collection
接口、JarFile、ZipFile 或位集创建,也可以从 Java9 开始从Optional class stream()
方法创建。Collection
类支持parallelStream()
方法,该方法可以返回并行流或串行流。通过调用适当的Arrays.stream(...)
,可以构造各种类型的流,例如装箱原始类型(Integer
、Long
、Double
)或其他类。为原始类型调用它的结果是以下特定流:IntStream
、LongStream
或DoubleStream
。这些专用流类可以使用它们的静态方法之一来构造流,例如generate(...)
、of(...)
、empty()
、iterate(...)
、concat(...)
、range(...)
、rangeClosed(...)
或builder()
。通过调用lines(...)
方法可以很容易地从BufferedReader
对象获取数据流,该方法也以静态形式存在于Files
类中,用于从路径给定的文件获取所有行。Files
类提供了其他流创建者方法,如list(...)
、walk(...)
、find(...)
。
Java9 除了前面提到的Optional
之外,还添加了更多返回流的类,比如Matcher
类(results(...)
方法)或Scanner
类(findAll(...)
和tokens()
方法)。
流中间操作
中间流操作是延迟应用的;这意味着只有在终端操作被调用之后才进行实际调用。在下面的代码中,使用在网上使用随机生成的名称,一旦找到第一个有效名称,搜索将停止(只返回一个Stream
对象):
jshell> Stream<String> stream = Arrays.stream(new String[] {"Benny Gandalf", "Aeliana Taina","Sukhbir Purnima"}). ...> map(x -> { System.out.println("Map " + x); return x; }). ...> filter(x -> x.contains("Aeliana")); stream ==> java.util.stream.ReferencePipeline$2@6eebc39e jshell> stream.findFirst(); Map Benny Gandalf Map Aeliana Taina $3 ==> Optional[Aeliana Taina]
流中间操作包含以下操作:
sequential()
:将当前流设置为串行流。parallel()
:将当前流设置为可能的并行流。根据经验,对大型数据集使用并行流,并行化可以提高性能。在我们的代码中,并行操作会导致性能下降,因为并行化的成本大于收益,而且我们正在处理一些否则无法处理的条目:
jshell> Stream<String> stream = Arrays.stream(new String[] {"Benny Gandalf", "Aeliana Taina","Sukhbir Purnima"}). ...> parallel(). ...> map(x -> { System.out.println("Map " + x); return x; }). ...> filter(x -> x.contains("Aeliana")); stream ==> java.util.stream.ReferencePipeline$2@60c6f5b jshell> stream.findFirst(); Map Benny Gandalf Map Aeliana Taina Map Sukhbir Purnima $14 ==> Optional[Aeliana Taina]
unordered()
:无序处理输入。它使得序列流的输出顺序具有不确定性,并通过允许更有效地实现一些聚合函数(如去重复或groupBy
),从而提高并行执行的性能。onClose(..)
:使用给定的输入处理器关闭流使用的资源。Files.lines(...)
流利用它来关闭输入文件,比如在下面的代码中,它是自动关闭的,但是也可以通过调用close()
方法手动关闭流:
jshell> try (Stream<String> stream = Files.lines(Paths.get("d:/input.txt"))) { ...> stream.forEach(System.out::println); ...> } Benny Gandalf Aeliana Taina Sukhbir Purnima
filter(..)
:应用谓词过滤输入。map(..)
:通过应用函数来转换输入。flatMap(..)
:使用基于映射函数的流中的值替换输入。distinct()
:使用Object.equals()
返回不同的值。sorted(..)
:根据自然/给定比较器对输入进行排序。peek(..)
:允许使用流所持有的值而不更改它们。limit(..)
:将流元素截断为给定的数目。skip(..)
:丢弃流中的前 n 个元素。
下面的代码显示了peek
、limit
和skip
方法的用法。它计算出商务旅行折合成欧元的费用。第一笔和最后一笔费用与业务无关,因此需要过滤掉(也可以使用filter()
方法)。peek
方法是打印费用总额中使用的费用:
jshell> Map<Currency, Double> exchangeToEur = Map.of(Currency.USD, 0.96, Currency.GBP, 1.56, Currency.EUR, 1.0); exchangeToEur ==> {USD=0.96, GBP=1.56, EUR=1.0} jshell> List<Expense> travelExpenses = List.of(new Expense(10, Currency.EUR, "Souvenir from Munchen"), new Expense(10.5, Currency.EUR, "Taxi to Munich airport"), new Expense(20, Currency.USD, "Taxi to San Francisco hotel"), new Expense(30, Currency.USD, "Meal"), new Expense(21.5, Currency.GBP, "Taxi to San Francisco airport"), new Expense(10, Currency.GBP, "Souvenir from London")); travelExpenses ==> [Expense@1b26f7b2, Expense@491cc5c9, Expense@74ad ... 62d5aee, Expense@69b0fd6f] jshell> travelExpenses.stream().skip(1).limit(4). ...> peek(x -> System.out.println(x.getDescription())). ...> mapToDouble(x -> x.getAmount() * exchangeToEur.get(x.getCurrency())). ...> sum(); Taxi to Munich airport Taxi to San Francisco hotel Meal Taxi to San Francisco airport $38 ==> 92.03999999999999
除了前面介绍的Stream.ofNullable
方法外,Java9 还引入了dropWhile
和takeWhile
。它们的目的是让开发人员更好地处理无限流。在下面的代码中,我们将使用它们将打印的数字限制在 5 到 10 之间。移除上限(由takeWhile
设置)将导致无限大的递增数字打印(在某个点上,它们将溢出,但仍会继续增加–例如,在迭代方法中,使用x -> x + 100
):
jshell> IntStream.iterate(1, x-> x + 1). ...> dropWhile(x -> x < 5).takeWhile(x -> x < 7). ...> forEach(System.out::println);
输出是 5 和 6,正如预期的那样,因为它们大于 5,小于 7。
流终端操作
终端操作是遍历中间操作管道并进行适当调用的值或副作用操作。它们可以处理返回的值(forEach(...)
、forEachOrdered(...)
),也可以返回以下任意值:
- 迭代器(例如
iterator()
和spliterator()
方法) - 集合(
toArray(...)
、collect(...)
,使用集合toList()
、toSet()
、toColletion()
、groupingBy()
、partitioningBy()
或toMap()
) - 特定元素(
findFirst()
、findAny()
) - 聚合(归约)可以是以下任何一种:
- 算法:
min(...)
、max(...)
、count()
或sum()
、average()
、summaryStatistics()
只针对IntStream
、LongStream
、DoubleStream
。 - 布尔值:
anyMatch(...)
、allMatch(...)
和noneMatch(...)
。 - 自定义:使用
reduce(...)
或collect(...)
方式。一些可用的收集器包括maxBy()
、minBy()
、reducing()
、joining()
和counting()
。
面向对象设计模式的再实现
在本节中,我们将根据 Java8 和 Java9 中提供的新特性来回顾一些 GOF 模式。
单子
使用闭包和Supplier
可以重新实现单例模式。Java 混合代码可以利用Supplier
接口,比如在下面的代码中,单例是一个枚举(根据函数编程,singleton 类型是那些只有一个值的类型,就像枚举一样)。以下示例代码与第 2 章“创建模式”中的代码类似:
jshell> enum Singleton{ ...> INSTANCE; ...> public static Supplier<Singleton> getInstance() ...> { ...> return () -> Singleton.INSTANCE; ...> } ...> ...> public void doSomething(){ ...> System.out.println("Something is Done."); ...> } ...> } | created enum Singleton jshell> Singleton.getInstance().get().doSomething(); Something is Done.
构建器
Lombock 库将生成器作为其功能的一部分引入。只要使用@Builder
注解,任何类都可以自动获得对builder
方法的访问权,如 Lombock 示例代码在这个页面中所示:
Person.builder().name("Adam Savage").city("San Francisco").job("Mythbusters").job("Unchained Reaction").build();
其他 Java8 之前的实现使用反射来创建通用生成器。Java8+ 泛型构建器版本可以通过利用供应器和BiConsumer
组合来实现,如下代码所示:
jshell> class Person { private String name; ...> public void setName(String name) { this.name = name; } ...> public String getName() { return name; }} | replaced class Person | update replaced variable a, reset to null jshell> Supplier<Person> getPerson = Person::new getPerson ==> $Lambda$214/2095303566@78b66d36 jshell> Person a = getPerson.get() a ==> Person@5223e5ee jshell> a.getName(); $91 ==> null jshell> BiConsumer<Person, String> changePersonName = (x, y) -> x.setName(y) changePersonName ==> $Lambda$215/581318631@6fe7aac8 jshell> changePersonName.accept(a, "Gandalf") jshell> a.getName(); $94 ==> "Gandalf"
Java 设计模式最佳实践:1~5(3)https://developer.aliyun.com/article/1426753