Java 设计模式最佳实践:1~5(2)

简介: Java 设计模式最佳实践:1~5(2)

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 -> zf :: (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 世界中需要考虑这个建议的原因在于可变代码有许多可移动的部分;它太复杂,不容易理解和修复。促进不变性简化了代码,并允许开发人员专注于流,而不是关注一段代码可能产生的副作用。最糟糕的副作用是,一个地方的微小变化可能会在另一个地方产生灾难性的结果(蝴蝶效应)。可变代码有时很难并行化,并且常常使用不同的锁。

函子

函子允许我们对给定的容器应用函数。他们知道如何从包装对象中展开值,应用给定的函数,并返回另一个包含结果/转换包装对象的函子。它们很有用,因为它们抽象了多种习惯用法,如集合、FuturePromise)和Optional。下面的代码演示了 Java 中的Optional函子的用法,其中Optional可以是一个给定的值,这是将函数应用于现有的包装值(5Optional的结果):

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 包含了StreamCompletableFuture和已经出现的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方法来使用。它静态地提供了minBymaxBy方法,返回两个元素中的较小值/较大值。
  • 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(...),可以构造各种类型的流,例如装箱原始类型(IntegerLongDouble)或其他类。为原始类型调用它的结果是以下特定流:IntStreamLongStreamDoubleStream。这些专用流类可以使用它们的静态方法之一来构造流,例如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 个元素。

下面的代码显示了peeklimitskip方法的用法。它计算出商务旅行折合成欧元的费用。第一笔和最后一笔费用与业务无关,因此需要过滤掉(也可以使用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 还引入了dropWhiletakeWhile。它们的目的是让开发人员更好地处理无限流。在下面的代码中,我们将使用它们将打印的数字限制在 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()只针对IntStreamLongStreamDoubleStream
  • 布尔值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

相关文章
|
2天前
|
设计模式 消息中间件 算法
【实习总结】Java学习最佳实践!
【实习总结】Java学习最佳实践!
22 3
|
1天前
|
设计模式 SQL 安全
Java一分钟之-设计模式:单例模式的实现
【5月更文挑战第16天】本文介绍了单例模式的四种实现方式:饿汉式(静态初始化)、懒汉式(双检锁)、静态内部类和枚举单例,以及相关问题和解决方法。关注线程安全、反射攻击、序列化、生命周期和测试性,选择合适的实现方式以确保代码质量。了解单例模式的优缺点,谨慎使用,提升设计效率。
16 3
|
2天前
|
设计模式 Java
【JAVA基础篇教学】第十四篇:Java中设计模式
【JAVA基础篇教学】第十四篇:Java中设计模式
|
2天前
|
SQL 设计模式 Java
Java编码规范与最佳实践
Java编码规范与最佳实践
23 0
|
2天前
|
设计模式 算法 Java
设计模式在Java开发中的应用
设计模式在Java开发中的应用
18 0
|
2天前
|
设计模式 前端开发 Java
19:Web开发模式与MVC设计模式-Java Web
19:Web开发模式与MVC设计模式-Java Web
24 4
|
2天前
|
设计模式 存储 前端开发
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
18:JavaBean简介及其在表单处理与DAO设计模式中的应用-Java Web
26 4
|
2天前
|
设计模式 缓存 监控
JAVA设计模式之结构型模式
结构模型:适配器模型、桥接模型、过滤器模型、组合模型、装饰器模型、外观模型、享受元模型和代理模型。
22 3
|
2天前
|
Java 开发者
Java中的异常处理:从基本概念到最佳实践
【4月更文挑战第30天】 在Java编程中,异常处理是确保程序健壮性和稳定性的关键机制。本文将深入探讨Java异常处理的基本概念,包括异常的分类、异常的抛出与捕获,以及如何有效地使用异常来增强代码的可读性和可维护性。此外,我们还将讨论一些关于异常处理的最佳实践,以帮助开发者避免常见的陷阱和误区。
|
2天前
|
设计模式 算法 Java
Java基础教程(19)-设计模式简述
【4月更文挑战第19天】设计模式是软件设计中反复使用的代码设计经验,旨在提升代码的可重用性、可扩展性和可维护性。23种模式分为创建型、结构型和行为型三类。创建型模式如工厂方法、抽象工厂、建造者、原型和单例,关注对象创建与使用的分离。结构型模式涉及对象组合,如适配器、装饰器、外观等,增强结构灵活性。行为型模式专注于对象间职责分配和算法合作,包括责任链、命令、观察者等。设计模式提供标准化解决方案,促进代码交流和复用。