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

相关文章
|
5天前
|
存储 Java 关系型数据库
高效连接之道:Java连接池原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。频繁创建和关闭连接会消耗大量资源,导致性能瓶颈。为此,Java连接池技术通过复用连接,实现高效、稳定的数据库连接管理。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接池的基本操作、配置和使用方法,以及在电商应用中的具体应用示例。
20 5
|
3天前
|
存储 Java 关系型数据库
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践
在Java开发中,数据库连接是应用与数据交互的关键环节。本文通过案例分析,深入探讨Java连接池的原理与最佳实践,包括连接创建、分配、复用和释放等操作,并通过电商应用实例展示了如何选择合适的连接池库(如HikariCP)和配置参数,实现高效、稳定的数据库连接管理。
11 2
|
9天前
|
设计模式 Java 程序员
[Java]23种设计模式
本文介绍了设计模式的概念及其七大原则,强调了设计模式在提高代码重用性、可读性、可扩展性和可靠性方面的作用。文章还简要概述了23种设计模式,并提供了进一步学习的资源链接。
26 0
[Java]23种设计模式
|
10天前
|
XML JSON 监控
告别简陋:Java日志系统的最佳实践
【10月更文挑战第19天】 在Java开发中,`System.out.println()` 是最基本的输出方法,但它在实际项目中往往被认为是不专业和不足够的。本文将探讨为什么在现代Java应用中应该避免使用 `System.out.println()`,并介绍几种更先进的日志解决方案。
32 1
|
25天前
|
设计模式 监控 算法
Java设计模式梳理:行为型模式(策略,观察者等)
本文详细介绍了Java设计模式中的行为型模式,包括策略模式、观察者模式、责任链模式、模板方法模式和状态模式。通过具体示例代码,深入浅出地讲解了每种模式的应用场景与实现方式。例如,策略模式通过定义一系列算法让客户端在运行时选择所需算法;观察者模式则让多个观察者对象同时监听某一个主题对象,实现松耦合的消息传递机制。此外,还探讨了这些模式与实际开发中的联系,帮助读者更好地理解和应用设计模式,提升代码质量。
Java设计模式梳理:行为型模式(策略,观察者等)
|
2月前
|
存储 设计模式 安全
Java设计模式-备忘录模式(23)
Java设计模式-备忘录模式(23)
|
20天前
|
监控 Java 数据库连接
探索Java中的异常处理机制:最佳实践与常见误区
在Java编程世界里,异常处理是确保应用程序稳定性和健壮性的关键环节。本文深入探讨了Java异常处理的机制,包括异常的分类、异常处理的基本原则以及如何在实际开发中应用这些原则。文章还指出了常见的异常处理误区,并提供了最佳实践建议,帮助开发者避免这些陷阱。通过具体代码示例和情景分析,本文旨在提升读者对Java异常处理的理解和应用能力。
|
2月前
|
设计模式 存储 缓存
Java设计模式 - 解释器模式(24)
Java设计模式 - 解释器模式(24)
|
25天前
|
设计模式 Java
Java设计模式
Java设计模式
25 0
|
28天前
|
设计模式 Java
Java设计模式之外观模式
这篇文章详细解释了Java设计模式之外观模式的原理及其应用场景,并通过具体代码示例展示了如何通过外观模式简化子系统的使用。
26 0