[Java 8] (5) 使用Lambda表达式进行设计

时间:2021-10-29 06:03:37

使用Lambda表达式进行设计

在前面的几篇文章中,我们已经见识到了Lambda表达式是怎样让代码变的更加紧凑和简洁的。

这一篇文章主要会介绍Lambda表达式怎样改变程序的设计。怎样让程序变的更加轻量级和简洁。

怎样让接口的使用变得更加流畅和直观。

使用Lambda表达式来实现策略模式

如果如今有一个Asset类型是这种:

public class Asset {
public enum AssetType { BOND, STOCK };
private final AssetType type;
private final int value;
public Asset(final AssetType assetType, final int assetValue) {
type = assetType;
value = assetValue;
}
public AssetType getType() { return type; }
public int getValue() { return value; }
}

每个资产都有一个种类。使用枚举类型表示。

同一时候资产也有其价值,使用整型类型表示。

假设我们想得到全部资产的价值。那么使用Lambda能够轻松实现例如以下:

public static int totalAssetValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(Asset::getValue)
.sum();
}

尽管上述代码可以非常好的完毕计算资产总值这一任务。可是细致分析会发现这段代码将下面三个任务给放在了一起:

  1. 怎样遍历
  2. 计算什么
  3. 怎样计算

怎样如今来了一个新需求。要求计算Bond类型的资产总值。那么非常直观的我们会考虑将上段代码复制一份然后有针对性地进行改动:

public static int totalBondValues(final List<Asset> assets) {
return assets.stream()
.mapToInt(asset ->
asset.getType() == AssetType.BOND ? asset.getValue() : 0)
.sum();
}

而唯一不同的地方,就是传入到mapToInt方法中的Lambda表达式。当然,也能够在mapToInt方法之前加入一个filter,过滤掉不须要的Stock类型的资产,这种话mapToInt中的Lambda表达式就能够不必改动了。

public static int totalBondValues(final List<Asset> assets) {
return assets.stream()
.filter(asset -> asset.getType == AssetType.BOND)
.mapToInt(Asset::getValue)
.sum();
}

这样尽管实现了新需求,可是这样的做法明显地违反了DRY原则。

我们须要又一次设计它们来增强可重用性。 在计算Bond资产的代码中,Lambda表达式起到了两个作用:

  1. 怎样遍历
  2. 怎样计算

当使用面向对象设计时,我们会考虑使用策略模式(Strategy Pattern)来将上面两个职责进行分离。可是这里我们使用Lambda表达式进行实现:

public static int totalAssetValues(final List<Asset> assets, final Predicate<Asset> assetSelector) {
return assets.stream().filter(assetSelector).mapToInt(Asset::getValue).sum();
}

重构后的方法接受了第二个參数,它是一个Predicate类型的函数式接口。非常显然它的作用就是来指定怎样遍历。这实际上就是策略模式在使用Lambda表达式时的一个简单实现,由于Predicate表达的是一个行为。而这个行为本身就是一种策略。这样的方法更加轻量级,由于它没有额外创建其它的接口或者类型,仅仅是重用了Java 8中提供的Predicate这一函数式接口而已。

比方,当我们须要计算全部资产的总值时,传入的Predicate能够是这种:

System.out.println("Total of all assets: " + totalAssetValues(assets, asset -> true));

因此,在使用了Predicate自后,就将“怎样遍历”这个任务也分离出来了。因此。任务之间不再纠缠在一起,实现了单一职责的原则,自然而然就提高了重用性。

使用Lambda表达式实现组合(Composition)

在面向对象设计中,一般觉得使用组合的方式会比使用继承的方式更好,由于它降低了不必要的类层次。事实上,使用Lambda表达式也可以实现组合。

比方,在以下的CalculateNAV类中,有一个用来计算股票价值的方法:

public class CalculateNAV {
public BigDecimal computeStockWorth(final String ticker, final int shares) {
return priceFinder.apply(ticker).multiply(BigDecimal.valueOf(shares));
}
//... other methods that use the priceFinder ...
}

由于传入的ticker是一个字符串。代表的是股票的代码。而在计算中我们显然须要的是股票的价格,所以priceFinder的类型非常easy被确定为Function。因此我们能够这样声明CalculateNAV的构造函数:

private Function<String, BigDecimal> priceFinder;
public CalculateNAV(final Function<String, BigDecimal> aPriceFinder) {
priceFinder = aPriceFinder;
}

实际上,上面的代码使用了一种设计模式叫做“依赖倒转原则(Dependency Inversion Principle)”。使用依赖注入的方式将类型和详细的实现进行关联。而不是直接将实现写死到代码中,从而提高了代码的重用性。

为了測试CalculateNAV。能够使用JUnit:

public class CalculateNAVTest {
@Test
public void computeStockWorth() {
final CalculateNAV calculateNAV = new CalculateNAV(ticker -> new BigDecimal("6.01"));
BigDecimal expected = new BigDecimal("6010.00");
assertEquals(0, calculateNAV.computeStockWorth("GOOG", 1000).compareTo(expected));
}
//...
}

当然,也能够使用真实的Web Service来得到某仅仅股票的价格:

public class YahooFinance {
public static BigDecimal getPrice(final String ticker) {
try {
final URL url = new URL("http://ichart.finance.yahoo.com/table.csv?s=" + ticker);
final BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()));
final String data = reader.lines().skip(1).findFirst().get();
final String[] dataItems = data.split(",");
return new BigDecimal(dataItems[dataItems.length - 1]);
} catch(Exception ex) {
throw new RuntimeException(ex);
}
}
}

这里想说明的是在Java 8中。BufferedReader也有一个新方法叫做lines,目的是得到包括全部行数据的一个Stream对象。非常明显这也是为了让该类和函数式编程可以更好的融合。

另外想说明的是是Lambda表达式和异常之间的关系。非常明显,当使用getPrice方法时,我们能够直接传入方法引用:YahooFinance::getPrice

可是假设在调用此方法期间发生了异常该怎样处理呢?上述代码在发生了异常时,将异常包装成RuntimeException并又一次抛出。

这样做是由于仅仅有当函数式接口中的方法本身声明了会抛出异常时(即声明了throws
XXX),才干够抛出受检异常(Checked Exception)。

而显然在Function这一个函数式接口的apply方法中并未声明能够抛出的受检异常。因此getPrice本身是不能抛出受检异常的,我们能够做的就是将异常封装成执行时异常(非受检异常)。然后再抛出。

使用Lambda表达式实现装饰模式(Decorator
Pattern)

装饰模式本身并不复杂。可是在面向对象设计中实现起来并不轻松。由于使用它须要设计和实现较多的类型,这无疑添加了开发者的负担。

比方JDK中的各种InputStream和OutputStream。在其上有各种各样的类型用来装饰它,所以最后I/O相关的类型被设计的有些过于复杂了。学习成本较高,要想正确而高效地使用它们并不easy。

使用Lambda表达式来实现装饰模式,就相对地easy多了。在图像领域。滤镜(Filter)实际上就是一种装饰器(Decorator)。我们会为一幅图像添加各种各样的滤镜。这些滤镜的数量是不确定的,顺序也是不确定的。

比方下面代码为摄像机对色彩的处理进行建模:

@SuppressWarnings("unchecked")
public class Camera {
private Function<Color, Color> filter;
public Color capture(final Color inputColor) {
final Color processedColor = filter.apply(inputColor);
//... more processing of color...
return processedColor;
}
//... other functions that use the filter ...
}

眼下仅仅定义了一个filter。我们能够利用Function的compose和andThen来进行多个Function(也就是filter)的串联操作:

default <V> Function<V, R> compose(Function<? super V, ?

extends T> before) {
Objects.requireNonNull(before);
return (V v) -> apply(before.apply(v));
} default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
Objects.requireNonNull(after);
return (T t) -> after.apply(apply(t));
}

能够发现,compose和andThen方法的差别只在于串联的顺序。

使用compose时。传入的Function会被首先调用;使用andThen时,当前的Function会被首先调用。

因此,在Camera类型中,我们能够定义一个方法用来串联不定数量的Filters:

public void setFilters(final Function<Color, Color>... filters) {
filter = Arrays.asList(filters).stream()
.reduce((current, next) -> current.andThen(next))
.orElse(color -> color);
}

前面介绍过,因为reduce方法返回的对象是Optional类型的,因此当结果不存在时,须要进行特别处理。

以上的orElse方法在结果不存在时会被调用来得到一个替代方案。那么当setFilters方法没有接受不论什么參数时,orElse就会被调用,color
-> color
的意义就是直接返回该color,不作不论什么操作。

实际上。Function接口中也定义了一个静态方法identity用来处理须要直接返回自身的场景:

static <T> Function<T, T> identity() {
return t -> t;
}

因此能够将上面的setFilters方法的实现改进成以下这样:

public void setFilters(final Function<Color, Color>... filters) {
filter = Arrays.asList(filters).stream()
.reduce((current, next) -> current.andThen(next))
.orElseGet(Function::identity);
}

orElse被替换成了orElseGet,两者的定义例如以下:

public T orElse(T other) {
return value != null ? value : other;
} public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}

前者当value为空时会直接返回传入的參数other,而后者则是通过调用调用Supplier中的get方法来得到要返回的对象。这里又出现了一个新的函数式接口Supplier:

@FunctionalInterface
public interface Supplier<T> {
T get();
}

它不须要不论什么參数。直接返回须要的对象。这一点和类的无參构造函数(有些情况下也被称为工厂)有些类似。

说到了Supplier,就不能不提和它相对的Consumer函数接口,它们正好是一种互补的关系。Consumer中的accept方法会接受一个參数,可是没有返回值。

如今。我们就能够使用Camera的滤镜功能了:

final Camera camera = new Camera();
final Consumer<String> printCaptured = (filterInfo) ->
System.out.println(String.format("with %s: %s", filterInfo,
camera.capture(new Color(200, 100, 200)))); camera.setFilters(Color::brighter, Color::darker);
printCaptured.accept("brighter & darker filter");

在不知不觉中。我们在setFilters方法中实现了一个轻量级的装饰模式(Decorator Pattern)。不须要定义不论什么多余的类型,仅仅须要借助Lambda表达式就可以。

了解接口的default方法

在Java 8中,接口中也可以拥有非抽象的方法了,这是一个很重大的设计。

那么从Java编译器的角度来看,default方法的解析有下面几个规则:

  1. 子类型会自己主动拥有父类型中的default方法。
  2. 对于接口中实现的default方法,该接口的子类型可以对该方法进行覆盖。
  3. 类中的详细实现以及抽象的声明,都会覆盖全部实现接口类型中出现的default方法。
  4. 当两个或者两个以上的default方法实现中出现了冲突时。实现类须要解决这个冲突。

举个样例来说明以上的规则:

public interface Fly {
default void takeOff() { System.out.println("Fly::takeOff"); }
default void land() { System.out.println("Fly::land"); }
default void turn() { System.out.println("Fly::turn"); }
default void cruise() { System.out.println("Fly::cruise"); }
}
public interface FastFly extends Fly {
default void takeOff() { System.out.println("FastFly::takeOff"); }
}
public interface Sail {
default void cruise() { System.out.println("Sail::cruise"); }
default void turn() { System.out.println("Sail::turn"); }
}
public class Vehicle {
public void turn() { System.out.println("Vehicle::turn"); }
}

对于规则1,FastFly和Fly接口可以说明。FastFly尽管覆盖了Fly中的takeOff方法,可是它同一时候也继承了Fly中的其他三个方法。

对于规则2,假设有不论什么接口继承了FastFly或者不论什么类型实现了FastFly,那么该子类型中的takeOff方法是来自于FastFly,而非Fly接口。

public class SeaPlane extends Vehicle implements FastFly, Sail {
private int altitude;
//...
public void cruise() {
System.out.print("SeaPlane::cruise currently cruise like: ");
if(altitude > 0)
FastFly.super.cruise();
else
Sail.super.cruise();
}
}

以上的SeaPlane继承了Vehicle类型,同一时候实现了FastFly和Sail接口。因为这两个接口中都定义了cruise方法,依据规则4,会发生冲突。因此须要SeaPlane来解决这个冲突。而解决的办法就是又一次定义这种方法。可是,父类型中的方法还是可以被调用的。正如上述代码会首先推断高度。然后调用对应父类的方法。

实际上,turn方法也存在于FastFly和Sail类型中。该方法之所以不存在冲突得益于Vehicle方法覆盖了接口中的default方法。

因此依据规则3,turn方法就不存在冲突了。

或许有人会认为在Java 8中,接口类型更像是一个抽象类,由于它不只可以拥有抽象方法。如今它还可以有详细实现。可是这样的说法还是过于片面了,毕竟接口还是不可以拥有状态的,而抽象类则可以持有状态。

同一时候。从继承的角度来看,一个类可以实现多个接口,而一个类至多只能继承一个类。

使用Lambda表达式创建更流畅的API

比方。如今有一个用于发送邮件的类型和它的使用方式:

public class Mailer {
public void from(final String address) { /*... */ }
public void to(final String address) { /*... */ }
public void subject(final String line) { /*... */ }
public void body(final String message) { /*... */ }
public void send() { System.out.println("sending..."); }
//...
} Mailer mailer = new Mailer();
mailer.from("build@agiledeveloper.com");
mailer.to("venkats@agiledeveloper.com");
mailer.subject("build notification");
mailer.body("...your code sucks...");
mailer.send();

这样的代码我们差点儿每天都要碰到。可是当中的坏味道你发现了吗?不认为mailer实例出现的太频繁了吗?对于这样的情况。我们能够用法链模式(Method Chaining Pattern)来改进:

public class MailBuilder {
public MailBuilder from(final String address) { /*... */; return this; }
public MailBuilder to(final String address) { /*... */; return this; }
public MailBuilder subject(final String line) { /*... */; return this; }
public MailBuilder body(final String message) { /*... */; return this; }
public void send() { System.out.println("sending..."); }
//...
} new MailBuilder()
.from("build@agiledeveloper.com")
.to("venkats@agiledeveloper.com")
.subject("build notification")
.body("...it sucks less...")
.send();

这样就流畅多了,代码也更加简练。

可是上述代码还是存在问题:

  1. newkeyword的使用添加了噪声
  2. 新创建的对象的引用能够被保存,意味着该对象的生命周期不可预測

这次我们让Lambda也參与到改进的过程中:

public class FluentMailer {
private FluentMailer() {}
public FluentMailer from(final String address) { /*... */; return this; }
public FluentMailer to(final String address) { /*... */; return this; }
public FluentMailer subject(final String line) { /*... */; return this; }
public FluentMailer body(final String message) { /*... */; return this; }
public static void send(final Consumer<FluentMailer> block) {
final FluentMailer mailer = new FluentMailer();
block.accept(mailer);
System.out.println("sending...");
}
//...
} FluentMailer.send(mailer ->
mailer.from("build@agiledeveloper.com")
.to("venkats@agiledeveloper.com")
.subject("build notification")
.body("...much better..."));

最重要的改进就是让类型的构造函数声明成私有的。这就防止了在类外部被显式的实例化,控制了实例的生命周期。然后send方法被重构成了一个static方法,它接受一个Consumer类型的Lambda表达式来完毕对新创建的实例的操作。

此时。新创建的实例的生命周期很明白:即当send方法调用结束之后。该实例的生命周期也就随之结束了。这样的模式也被直观地称为“Loan Pattern”。即首先得到它,操作它。最后归还它。

处理异常

前文中提到过,假设函数接口的方法本身未定义能够被抛出的受检异常,那么在使用该接口时是无法处理可能存在的受检异常的,比方典型的IOException这类,在进行文件操作的时候必需要处理:

public class HandleException {
public static void main(String[] args) throws IOException {
List<String> paths = Arrays.asList("/usr", "/tmp");
paths.stream()
.map(path -> new File(path).getCanonicalPath())
.forEach(System.out::println);
// 以上代码不能通过编译。由于没有处理可能存在的IOException
}
} // ... unreported exception IOException; must be caught or declared to be thrown
// .map(path -> new File(path).getCanonicalPath())
// ^

这是由于在map方法须要的Function函数接口中,并没有声明不论什么能够被抛出的受检异常。这里我们有两个选择:

  1. 在Lambda表达式内处理受检异常
  2. 捕获该受检异常并又一次以非受检异常(如RuntimeException)的形式抛出

使用第一个选择时。代码是这种:

paths.stream()
.map(path -> {
try {
return new File(path).getCanonicalPath();
} catch(IOException ex) {
return ex.getMessage();
}
})
.forEach(System.out::println);

使用第二个选择时。代码是这种:

paths.stream()
.map(path -> {
try {
return new File(path).getCanonicalPath();
} catch(IOException ex) {
throw new RuntimeException(ex);
}
})
.forEach(System.out::println);

在单线程环境中,使用捕获受检异常并又一次抛出非受检异常的方法是可行的。

可是在多线程环境这样用,就存在一些风险。

多线程环境中。Lambda表达式中发生的错误会被自己主动传递到主线程中。

这会带来两个问题:

  1. 这不会停止其它正在并行运行的Lambda表达式。
  2. 假设有多个线程抛出了异常,在主线程中却仅仅能捕获到一个线程中的异常。

    假设这些异常信息都非常重要的话,那么更好的方法是在Lambda表达式中就进行异常处理并将异常信息作为结果的一部分返回到主线程中。

实际上,另一种方法。就是依据异常处理的须要定义我们自己的函数接口,比方:

@FunctionalInterface
public interface UseInstance<T, X extends Throwable> {
void accept(T instance) throws X;
}

这种话,不论什么使用UseInstance类型的Lambda表达式就行抛出各种异常了。

[Java 8] (5) 使用Lambda表达式进行设计的更多相关文章

  1. Java 终于有 Lambda 表达式啦~Java 8 语言变化&mdash&semi;&mdash&semi;Lambda 表达式和接口类更改【转载】

    原文地址 en cn 下载 Demo Java™ 8 包含一些重要的新的语言功能,为您提供了构建程序的更简单方式.Lambda 表达式 为内联代码块定义一种新语法,其灵活性与匿名内部类一样,但样板文件 ...

  2. Java核心技术-接口、lambda表达式与内部类

    本章将主要介绍: 接口技术:主要用来描述类具有什么功能,而并不给出每个功能的具体实现.一个类可以实现一个或多个接口. lambda表达式:这是一种表示可以在将来的某个时间点执行的代码块的简洁方法. 内 ...

  3. Java函数式编程和lambda表达式

    为什么要使用函数式编程 函数式编程更多时候是一种编程的思维方式,是种方法论.函数式与命令式编程的区别主要在于:函数式编程是告诉代码你要做什么,而命令式编程则是告诉代码要怎么做.说白了,函数式编程是基于 ...

  4. Java基础教程:Lambda表达式

    Java基础教程:Lambda表达式 本文部分内容引用自OneAPM:http://blog.oneapm.com/apm-tech/226.html 引入Lambda Java 是一流的面向对象语言 ...

  5. java函数式编程之lambda表达式

    作为比较老牌的面向对象的编程语言java,在对函数式编程的支持上一直不温不火. 认为面向对象式编程就应该纯粹的面向对象,于是经常看到这样的写法:如果你想写一个方法,那么就必须把它放到一个类里面,然后n ...

  6. 最全最强 Java 8 - 函数编程(lambda表达式)

    Java 8 - 函数编程(lambda表达式) 我们关心的是如何写出好代码,而不是符合函数编程风格的代码. @pdai Java 8 - 函数编程(lambda表达式) 简介 lambda表达式 分 ...

  7. Java 8:掌握 Lambda 表达式

    本文将介绍 Java 8 新增的 Lambda 表达式,包括 Lambda 表达式的常见用法以及方法引用的用法,并对 Lambda 表达式的原理进行分析,最后对 Lambda 表达式的优缺点进行一个总 ...

  8. Upgrading to Java 8——第一章 Lambda表达式

    第一章 Lambda表达式 Lamada 表达式是Java SE 8中最重要的新特性,长期以来被认为是在Java中缺失的特性,它的出现使整个java 语言变得完整.至少到目前,在这节中你将学习到什么是 ...

  9. JDK8新特性01 Lambda表达式01&lowbar;设计的由来

    1.java bean public class Employee { private int id; private String name; private int age; private do ...

随机推荐

  1. 数据类型int、bigint、smallint 和 tinyint范围

      bigint 从 -2^63 (-9223372036854775808) 到 2^63-1 (9223372036854775807) 的整型数据(所有数字).存储大小为 8 个字节. int ...

  2. 《从零开始做一个MEAN全栈项目》(2)

    欢迎关注本人的微信公众号"前端小填填",专注前端技术的基础和项目开发的学习.   上一节简单介绍了什么是MEAN全栈项目,这一节将简要介绍三个内容:(1)一个通用的MEAN项目的技 ...

  3. C&num; DataGridView控件清空数据完美解决方法

    C# DataGridView控件绑定数据后清空数据在清除DataGridview的数据时: 1.DataSource为NULL(DataGridView.DataSource= null;)这样会将 ...

  4. U盘安装CentOS无法进入Centos系统解决办法

    转自:http://blog.sina.com.cn/s/blog_3feedf320101idlu.html     目前使用U盘安装系统逐渐因为它的便捷而受到人们的欢迎,但是使用U盘来安装Cent ...

  5. java-成员变量的属性与成员函数的覆盖

    java中在多态的实现是通过类的继承或者接口的实现来完成的. 在类继承或者接口实现过程中就会涉及到成员属性以及成员函数的重写,需要注意的是,成员函数的重写是直接覆盖父类的(继承中),但是成员变量重写是 ...

  6. Linux怪哉ntfs

    http://www.linuxidc.com/Linux/2013-08/88721.htm

  7. hdu 4640 Island and study-sister(状态压缩dp)

    先处理前两个学长到达各个点所需要的最少时间,在计算前两个学长和最后一个学长救出所有学妹的最少时间. #include<stdio.h> #include<string.h> # ...

  8. spring通过注解依赖注入和获取xml配置混合的方式

    spring的xml配置文件中某个<bean></bean>中的property的用法是什么样的? /spring-beans/src/test/java/org/spring ...

  9. ajax 实现异步请求

    ajax实现异步请求: function onclicks() { $.ajax( { url:'../hhh/columnSearch.do',// 跳转到 action // data: {tab ...

  10. Spring Boot 日志配置方法&lpar;超详细&rpar;

    默认日志 Logback : 默认情况下,Spring Boot会用Logback来记录日志,并用INFO级别输出到控制台.在运行应用程序和其他例子时,你应该已经看到很多INFO级别的日志了. 从上图 ...