Java8 新特性
来源:互联网 发布:c语言学徒招聘 编辑:程序博客网 时间:2024/06/14 10:42
一、lambda表达式
什么是lambda?
lambda表达式是一段可以传递的代码,它的核心思想是将面向对象中的传递数据变成传递行为。 我们回顾一下在使用java8之前要做的事,之前我们编写一个线程时是这样的:
Runnable r = new Runnable() { @Override public void run() { System.out.println("do something."); }}
也有人会写一个类去实现Runnable接口,这样做没有问题,我们注意这个接口中只有一个run方法, 当把Runnable对象给Thread对象作为构造参数时创建一个线程,运行后将输出do something.。 我们使用匿名内部类的方式实现了该方法。
这实际上是一个代码即数据的例子,在run方法中是线程要执行的一个任务,但上面的代码中任务内容已经被规定死了。 当我们有多个不同的任务时,需要重复编写如上代码。
设计匿名内部类的目的,就是为了方便 Java 程序员将代码作为数据传递。不过,匿名内部 类还是不够简便。 为了执行一个简单的任务逻辑,不得不加上 6 行冗繁的样板代码。那如果是lambda该怎么做?
Runnable r = () -> System.out.println("do something.");
嗯,这代码看起来很酷,你可以看到我们用()和->的方式完成了这件事,这是一个没有名字的函数,也没有人和参数,再简单不过了。
使用->将参数和实现逻辑分离,当运行这个线程的时候执行的是->之后的代码片段,且编译器帮助我们做了类型推导; 这个代码片段可以是用{}包含的一段逻辑。下面一起来学习一下lambda的语法。
基础语法
在lambda中我们遵循如下的表达式来编写:
expression = (variable) -> action
- variable: 这是一个变量,一个占位符。像x,y,z,可以是多个变量;
- action: 这里我称它为action, 这是我们实现的代码逻辑部分,它可以是一行代码也可以是一个代码片段。
可以看到Java中lambda表达式的格式:参数、箭头、以及动作实现,当一个动作实现无法用一行代码完成,可以编写 一段代码用{}包裹起来。
ambda表达式可以包含多个参数,例如:
int sum = (x, y) -> x + y;
这时候我们应该思考这段代码不是之前的x和y数字相加,而是创建了一个函数,用来计算两个操作数的和。 后面用int类型进行接收,在lambda中为我们省略去了return。函数式接口
函数式接口是只有一个方法的接口,用作lambda表达式的类型。前面写的例子就是一个函数式接口,来看看jdk中的Runnable源码:
@FunctionalInterfacepublic interface Runnable { /** * When an object implementing interface <code>Runnable</code> is used * to create a thread, starting the thread causes the object's * <code>run</code> method to be called in that separately executing * thread. * <p> * The general contract of the method <code>run</code> is that it may * take any action whatsoever. * * @see java.lang.Thread#run() */ public abstract void run();}
这里只有一个抽象方法run,实际上你不写public abstract也是可以的,在接口中定义的方法都是public abstract的。 同时也使用注解@FunctionalInterface告诉编译器这是一个函数式接口,当然你不这么写也可以,标识后明确了这个函数中 只有一个抽象方法,当你尝试在接口中编写多个方法的时候编译器将不允许这么干。
尝试函数式接口
我们来编写一个函数式接口,输入一个年龄,判断这个人是否是成人。
public class FunctionInterfaceDemo { @FunctionalInterface interface Predicate<T> { boolean test(T t); } /** * 执行Predicate判断 * @param age 年龄 * @param predicate Predicate函数式接口 * @return 返回布尔类型结果 */ public static boolean doPredicate(int age, Predicate<Integer> predicate) { return predicate.test(age); } public static void main(String[] args) { boolean isAdult = doPredicate(20, x -> x >= 18); System.out.println(isAdult); }}
从这个例子我们很轻松的完成 是否是成人 的动作,其次判断是否是成人,在此之前我们的做法一般是编写一个 判断是否是成人的方法,是无法将 判断 共用的。而在本例只,你要做的是将 行为 (判断是否是成人,或者是判断是否大于30岁) 传递进去,函数式接口告诉你结果是什么。
实际上诸如上述例子中的接口,伟大的jdk设计者为我们准备了java.util.function包
- BiConsumer
- BiFunction
- BinaryOperator
- BiPredicate
- BooleanSupplier
- Consumer
- DoubleBinaryOperator
- DoubleConsumer
- DoubleFunction
- DoublePredicate
- DoubleSupplier
- DoubleToIntFunction
- DoubleToLongFunction
- DoubleUnaryOperator
- Function
- IntBinaryOperator
- IntConsumer
- IntFunction
- IntPredicate
- IntSupplier
- IntToDoubleFunction
- IntToLongFunction
- IntUnaryOperator
- LongBinaryOperator
- LongConsumer
- LongFunction
- LongPredicate
- LongSupplier
- LongToDoubleFunction
- LongToIntFunction
- LongUnaryOperator
- ObjDoubleConsumer
- ObjIntConsumer
- ObjLongConsumer
- Predicate
- Supplier
- ToDoubleBiFunction
- ToDoubleFunction
- ToIntBiFunction
- ToIntFunction
- ToLongBiFunction
- ToLongFunction
- UnaryOperator
我们前面写的Predicate函数式接口也是JDK种的一个实现,他们大致分为以下几类:
消费型接口示例
public static void donation(Integer money, Consumer<Integer> consumer){ consumer.accept(money); }public static void main(String[] args) { donation(1000, money -> System.out.println(String.format("消费%d元", money))) ;}
供给型接口示例
public static List<Integer> supply(Integer num, Supplier<Integer> supplier){ List<Integer> resultList = new ArrayList<Integer>() ; for(int x=0;x<num;x++) resultList.add(supplier.get()); return resultList ;}public static void main(String[] args) { List<Integer> list = supply(10,() -> (int)(Math.random()*100)); list.forEach(System.out::println);}
函数型接口示例
转换字符串为Integer
public static Integer convert(String str, Function<String, Integer> function) { return function.apply(str);}public static void main(String[] args) { Integer value = convert("28", x -> Integer.parseInt(x));}
断言型接口示例
筛选出只有2个字的水果
public static List<String> filter(List<String> fruit, Predicate<String> predicate){ List<String> f = new ArrayList<>(); for (String s : fruit) { if(predicate.test(s)){ f.add(s); } } return f;}public static void main(String[] args) { List<String> fruit = Arrays.asList("香蕉", "哈密瓜", "榴莲", "火龙果", "水蜜桃"); List<String> newFruit = filter(fruit, (f) -> f.length() == 2); System.out.println(newFruit);}
默认方法
在Java语言中,一个接口中定义的方法必须由实现类提供实现。但是当接口中加入新的API时, 实现类按照约定也要修改实现,而Java8的API对现有接口也添加了很多方法,比如List接口中添加了sort方法。 如果按照之前的做法,那么所有的实现类都要实现sort方法,JDK的编写者们一定非常抓狂。
幸运的是我们使用了Java8,这一问题将得到很好的解决,在Java8种引入新的机制,支持在接口中声明方法同时提供实现。 这令人激动不已,你有两种方式完成 1.在接口内声明静态方法 2.指定一个默认方法。
我们来看看在JDK8中上述List接口添加方法的问题是如何解决的
default void sort(Comparator<? super E> c) { Object[] a = this.toArray(); Arrays.sort(a, (Comparator) c); ListIterator<E> i = this.listIterator(); for (Object e : a) { i.next(); i.set((E) e); }}
翻阅List接口的源码,其中加入一个默认方法default void sort(Comparator
List<Integer> list = Arrays.asList(2, 7, 3, 1, 8, 6, 4);list.sort(Comparator.naturalOrder());System.out.println(list);
Comparator.naturalOrder()是一个自然排序的实现,这里可以自定义排序方案。你经常看到使用Java8操作集合的时候可以直接foreach的原因也是在Iterable接口中也新增了一个默认方法:forEach,该方法功能和 for 循环类似,但是允许 用户使用一个Lambda表达式作为循环体。
二、Stream API
: Stream是Java8种处理集合的抽象概念, 它可以指定你希望对集合的操作,但是执行操作的时间交给具体实现来决定。
为什么需要Stream?
Java语言中集合是使用最多的API,几乎每个Java程序都会用到集合操作, 这里的Stream和IO中的Stream不同,它提供了对集合操作的增强,极大的提高了操作集合对象的便利性。
集合对于大多数编程任务而言都是基本的,为了解释集合是怎么工作,我们想象一下当下最火的外卖APP, 当我们点菜的时候需要按照距离、价格、销量等进行排序后筛选出自己满意的菜品。 你可能想选择距离自己最近的一家店铺点菜,尽管用集合可以完成这件事,但集合的操作远远算不上完美。
假如让你编写上面示例中的代码,你可能会写出如下:
// 店铺属性public class Property { String name; // 距离,单位:米 Integer distance; // 销量,月售 Integer sales; // 价格,这里简单起见就写一个级别代表价格段 Integer priceLevel; public Property(String name, int distance, int sales, int priceLevel) { this.name = name; this.distance = distance; this.sales = sales; this.priceLevel = priceLevel; } // getter setter 省略}
我想要筛选距离我最近的店铺,你可能会写下这样的代码:
public static void main(String[] args) { Property p1 = new Property("叫了个鸡", 1000, 500, 2); Property p2 = new Property("张三丰饺子馆", 2300, 1500, 3); Property p3 = new Property("永和大王", 580, 3000, 1); Property p4 = new Property("肯德基", 6000, 200, 4); List<Property> properties = Arrays.asList(p1, p2, p3, p4); Collections.sort(properties, (x, y) -> x.distance.compareTo(y.distance)); String name = properties.get(0).name; System.out.println("距离我最近的店铺是:" + name);}
这里也使用了部分lambda表达式,在Java8之前你可能写的更痛苦一些。 要是要处理大量元素又该怎么办呢?为了提高性能,你需要并行处理,并利用多核架构。 但写并行代码比用迭代器还要复杂,而且调试起来也够受的!
但Stream中操作这些东西当然是非常简单的,小试牛刀:
// Stream操作String name2 = properties.stream() .sorted(Comparator.comparingInt(x -> x.distance)) .findFirst() .get().name;System.out.println("距离我最近的店铺是:" + name);
新的API对所有的集合操作都提供了生成流操作的方法,写的代码也行云流水,我们非常简单的就筛选了离我最近的店铺。 在后面我们继续讲解Stream更多的特性和玩法。
外部迭代和内部迭代
当你处理集合时,通常会对它进行迭代,然后处理返回的每个元素。比如我想看看月销量大于1000的店铺个数。
使用for循环进行迭代
int count = 0;for (Property property : properties) { if(property.sales > 1000){ count++; }}
上面的操作是可行的,但是当每次迭代的时候你需要些很多重复的代码。将for循环修改为并行执行也非常困难, 需要修改每个for的实现。
从集合背后的原理来看,for循环封装了迭代的语法糖,首先调用iterator方法,产生一个Iterator对象, 然后控制整个迭代,这就是外部迭代。迭代的过程通过调用Iterator对象的hasNext和next方法完成。
使用迭代器进行计算
int count = 0;Iterator<Property> iterator = properties.iterator();while(iterator.hasNext()){ Property property = iterator.next(); if(property.sales > 1000){ count++; }}
而迭代器也是有问题的。它很难抽象出未知的不能操作;此外它本质上还是串行化的操作,总体来看使用 for循环会将行为和方法混为一谈。
另一种办法是使用内部迭代完成,properties.stream()该方法返回一个Stream而不是迭代器。
使用内部迭代进行计算
long count2 = properties.stream() .filter(p -> p.sales > 1000) .count();
上述代码是通过Stream API完成的,我们可以把它理解为2个步骤:
- 找出所有销量大于1000的店铺
- 计算出店铺个数
为了找出销量大于1000的店铺,需要先做一次过滤:filter,你可以看看这个方法的入参就是前面讲到的Predicate断言型函数式接口, 测试一个函数完成后,返回值为boolean。 由于Stream API的风格,我们没有改变集合的内容,而是描述了Stream的内容,最终调用count()方法计算出Stream 里包含了多少个过滤之后的对象,返回值为long。
创建Stream
你已经知道Java8种在Collection接口添加了Stream方法,可以将任何集合转换成一个Stream。 如果你操作的是一个数组可以使用Stream.of(1, 2, 3)方法将它转换为一个流。
也许有人知道JDK7中添加了一些类库如Files.readAllLines(Paths.get(“/home/biezhi/a.txt”))这样的读取文件行方法。 List作为Collection的子类拥有转换流的方法,那么我们读取这个文本文件到一个字符串变量中将变得更简洁:
String content = Files.readAllLines(Paths.get("/home/biezhi/a.txt")).stream() .collect(Collectors.joining("\n"));
这里的collect是后面要讲解的收集器,对Stream进行了处理后得到一个文本文件的内容。
JDK8也为我们提供了一些便捷的Stream相关类库:
- AbstractPipeline
- AbstractShortCircuitTask
- AbstractSpinedBuffer
- AbstractTask
- BaseStream
- Collector
- Collectors
- DistinctOps
- DoublePipeline
- DoubleStream
- FindOps
- ForEachOps
- IntPipeline
- IntStream
- LongPipeline
- LongStream
- MatchOps
- Node
- Nodes
- PipelineHelper
- ReduceOps
- ReferencePipeline
- Sink
- SliceOps
- SortedOps
- SpinedBuffer
- Stream
- StreamOpFlag
- Streams
- StreamShape
- StreamSpliterators
- StreamSupport
- TerminalOp
- TerminalSink
- Tripwire
创建一个流是很简单的,下面我们试试用创建好的Stream做一些操作吧。
流操作
java.util.stream.Stream中定义了许多流操作的方法,为了更好的理解Stream API掌握它常用的操作非常重要。 流的操作其实可以分为两类:处理操作、聚合操作。
- 处理操作:诸如filter、map等处理操作将Stream一层一层的进行抽离,返回一个流给下一层使用。
- 聚合操作:从最后一次流中生成一个结果给调用方,foreach只做处理不做返回。
filter
filter看名字也知道是过滤的意思,我们通常在筛选数据的时候用到,频率非常高。 filter方法的参数是Predicate predicate即一个从T到boolean的函数。
筛选出距离我在1000米内的店铺
properties.stream().filter(p -> p.distance < 1000)
筛选出名称大于5个字的店铺
properties.stream().filter(p -> p.name.length() > 5);
map
有时候我们需要将流中处理的数据类型进行转换,这时候就可以使用map方法来完成,将流中的值转换为一个新的流。
列出所有店铺的名称
properties.stream().map(p -> p.name);
传给map的lambda表达式接收一个Property类型的参数,返回一个String。 参数和返回值不必属于同一种类型,但是lambda表达式必须是Function接口的一个实例。
flatMap
有时候我们会遇到提取子流的操作,这种情况用的不多但是遇到flatMap将变得更容易处理。
例如我们有一个List
List<List<String>> lists = new ArrayList<>(); lists.add(Arrays.asList("apple", "click")); lists.add(Arrays.asList("boss", "dig", "qq", "vivo")); lists.add(Arrays.asList("c#", "biezhi"));
要做的操作是获取这些数据中长度大于2的单词个数
lists.stream() .flatMap(Collection::stream) .filter(str -> str.length() > 2) .count();
在不使用flatMap前你可能需要做2次for循环。这里调用了List的stream方法将每个列表转换成Stream对象, 其他的就和之前的操作一样。
max和min
Stream中常用的操作之一是求最大值和最小值,Stream API 中的max和min操作足以解决这一问题。
我们需要筛选出价格最低的店铺:
Property property = properties.stream() .max(Comparator.comparingInt(p -> p.priceLevel)) .get();
查找Stream中的最大或最小元素,首先要考虑的是用什么作为排序的指标。 以查找价格最低的店铺为例,排序的指标就是店铺的价格等级。
为了让Stream对象按照价格等级进行排序,需要传给它一个Comparator对象。 Java8提供了一个新的静态方法comparingInt,使用它可以方便地实现一个比较器。 放在以前,我们需要比较两个对象的某项属性的值,现在只需要提供一个存取方法就够了。
收集结果
通常我们处理完流之后想查看一下结果,比如获取总数,转换结果,在前面的示例中你发现调用了 filter、map之后没有下文了,后续的操作应该调用Stream中的collect方法完成。
获取距离我最近的2个店铺
List<Property> properties = properties.stream() .sorted(Comparator.comparingInt(x -> x.distance)) .limit(2) .collect(Collectors.toList());
获取所有店铺的名称
List<String> names = properties.stream() .map(p -> p.name) .collect(Collectors.toList());
获取每个店铺的价格等级
Map<String, Integer> map = properties.stream() .collect(Collectors.toMap(Property::getName, Property::getPriceLevel));
所有价格等级的店铺列表
Map<Integer, List<Property>> priceMap = properties.stream() .collect(Collectors.groupingBy(Property::getPriceLevel));
并行数据处理
并行和并发
并发是两个任务共享时间段,并行则是两个任务在同一时间发生,比如运行在多核CPU上。 如果一个程序要运行两个任务,并且只有一个CPU给它们分配了不同的时间片,那么这就是并发,而不是并行。
并行化是指为缩短任务执行时间,将一个任务分解成几部分,然后并行执行。
这和顺序执行的任务量是一样的,区别就像用更多的马来拉车,花费的时间自然减少了。 实际上,和顺序执行相比,并行化执行任务时,CPU承载的工作量更大。
数据并行化是指将数据分成块,为每块数据分配单独的处理单元。
还是拿马拉车那个例子打比方,就像从车里取出一些货物,放到另一辆车上,两辆马车都沿着同样的路径到达目的地。
当需要在大量数据上执行同样的操作时,数据并行化很管用。 它将问题分解为可在多块数据上求解的形式,然后对每块数据执行运算,最后将各数据块上得到的结果汇总,从而获得最终答案。
人们经常拿任务并行化和数据并行化做比较,在任务并行化中,线程不同,工作各异。 我们最常遇到的JavaEE应用容器便是任务并行化的例子之一,每个线程不光可以为不同用户服务, 还可以为同一个用户执行不同的任务,比如登录或往购物车添加商品。
Stream并行流
流使得计算变得容易,它的操作也非常简单,但你需要遵守一些约定。默认情况下我们使用集合的stream方法 创建的是一个串行流,你有两种办法让他变成并行流。
- 调用Stream对象的parallel方法
- 创建流的时候调用parallelStream而不是stream方法
我们来用具体的例子来解释串行和并行流
串行化计算
筛选出价格等级小于4,按照距离排序的2个店铺名
properties.stream() .filter(p -> p.priceLevel < 4) .sorted(Comparator.comparingInt(Property::getDistance)) .map(Property::getName) .limit(2) .collect(Collectors.toList());
调用 parallelStream 方法即能并行处理
properties.parallelStream() .filter(p -> p.priceLevel < 4) .sorted(Comparator.comparingInt(Property::getDistance)) .map(Property::getName) .limit(2) .collect(Collectors.toList());
读到这里,大家的第一反应可能是立即将手头代码中的stream方法替换为parallelStream方法, 因为这样做简直太简单了!先别忙,为了将硬件物尽其用,利用好并行化非常重要,但流类库提供的数据并行化只是其中的一种形式。
我们先要问自己一个问题:并行化运行基于流的代码是否比串行化运行更快?这不是一个简单的问题。 回到前面的例子,哪种方式花的时间更多取决于串行或并行化运行时的环境。
三、函数式编程
我们不断的提及函数式这个名词,它指的是lambda吗?如果是这样,采用函数式编程能为你带来什么好处呢?
函数式的思考
命令式编程
一般我们实现一个系统有两种思考方式,一种专注于如何实现,比如下厨做菜,通常按照自己熟悉的烹饪方法:首先洗菜, 然后切菜,热油,下菜,然后…… 这看起来像是一系列的命令合集。对于这种”如何做”式的编程风格我们称之为命令式编程, 它的特点非常像工厂的流水线、计算机的指令处理,都是串行化、命令式的。
CookingTask cookingTask = new CookingTask();cookingTask.wash();cookingTask.cut();cookingTask.deepFry();cookingTask.fried();...
声明式编程
还有一种方式你关注的是要做什么,我们如果用lambda和函数式来解决上述问题应该是这样的:
public class CookingDemo { public void doTask(String material, Consumer<String> consumer) { consumer.accept(material); } public static void main(String[] args) { CookingDemo cookingDemo = new CookingDemo(); cookingDemo.doTask("蔬菜", material -> System.out.println("清洗" + material)); cookingDemo.doTask("蔬菜", material -> System.out.println(material + "切片")); cookingDemo.doTask("食用油", material -> System.out.println(material + "烧热")); cookingDemo.doTask("", material -> System.out.println("炒菜")); }}
这里我们将烹饪的实现细节交给了函数库,它最大的优势在于你读起来就像是在问题陈述,采用这种方式我们很快可以理解它的功能, 当你在烹饪流程中添加其他步骤也变得非常简单,你只需要调用doTask方法将材料传递进去处理,比如在食用油烧热前我要打个鸡蛋
cookingDemo.doTask("鸡蛋", material -> System.out.println(material + "打碎搅拌均匀"));
而不用再编写一个处理鸡蛋的方法。
什么是函数式编程
对于“什么是函数式编程”这一问题最简化的回答是“它是一种使用函数进行编程的方式”。 每个人的理解都是不同的,其核心是:在思考问题时,使用不可变值和函数,函数对一个值进行处理,映射成另一个值。
不同的语言社区往往对各自语言中的特性孤芳自赏。现在谈Java程序员如何定义函数式编程还为时尚早, 但是,这根本不重要!我们关心的是如何写出好代码,而不是符合函数式编程风格的代码。
我们想象一下设计一个函数,输入一个字符串类型和布尔类型参数,输出一个整形参数。
int pos = 0;public Integer foo(String str, boolea flag){ if(flag && null != str){ pos++; } return pos;}
这个例子有输入也有输出,同时每次调用也可能会更行外部的变量值,这样的函数我们称之为是有副作用的函数。
在函数式编程的上下文中,一个“函数”对应于一个数学函数:它接受零个或多个参数,生成一个或多个结果,并且不会有任何副作用。 你可以把它看成一个黑盒,它接收输入并产生一些输出,像下面的函数
public Integer foo(String str, boolea flag){ if(flag && null != str){ return 1; } return 0;}
这种类型的函数和你在Java编程语言中见到的函数之间的区别是非常重要的(我们无法想象,log或者sin这样的数学函数会有副作用)。 尤其是,使用同样的参数调用数学函数,它所返回的结果一定是相同的。这里,我们暂时不考虑Random.nextInt这样的方法
函数的副作用
当谈论“函数式”时,我们想说的其实是“像数学函数那样——没有副作用”。由此,编程上的一些精妙问题随之而来。 我们的意思是,每个函数都只能使用函数和像if-then-else这样的数学思想来构建吗? 或者,我们也允许函数内部执行一些非函数式的操作,只要这些操作的结果不会暴露给系统中的其他部分? 换句话说,如果程序有一定的副作用,不过该副作用不会为其他的调用者感知,是否我们能假设这种副作用不存在呢? 调用者不需要知道,或者完全不在意这些副作用,因为这对它完全没有影响。
当我们希望能界定这二者之间的区别时,我们将第一种称为纯粹的函数式编程,后者称为函数式编程。
在编程实战中我们很难用Java语言以纯粹的函数式来完成一个程序的,因为很多老的代码包括标准库的函数都是有副作用的 (调用Scanner.nextLine就有副作用,它会从一个文件中读取一行, 通常情况两次调用的结果完全不同)。你希望为你的系统 编写接近纯函数式的实现,需要确保你的代码没有副作用。假设这样一个函数或者方法,它没有副作用,进入方法体执行时会对一个字段的值加一, 退出方法体之前会对该字段减一。对一个单线程的程序而言,这个方法是没有副作用的,可以看作函数式的实现。
我们构建函数式的准则是,被称为“函数式”的函数或方法都只能修改局部变量,除此之外,它引用的对象都应该是final的。 所有的引用类型字段都指向不可变对象。
四、在 Java 中提升函数以更好地“函数式”编程
Java8中的Stream和Optional给我们带来了函数式编程的乐趣,但Java仍然缺少很多函数编程的关键特性。Lambda表达式、Optional和Stream只是函数式编程的冰山一角。这也导致了varvr和functionlajava这些类库的出现,他们都源于Haskell这个纯函数式编程语言。
如果想要更加地“函数式”编程,那么首先要注意的是不要过早的中断monad(一种设计模式,表示将一个运算过程通过函数拆解成互相连接的多个步骤。只要提供下一步运算所需的函数,整个运算就会自动进行下去, Optional、Stream都是monad),比如,很多人经常会在还不需要的时候就调用了Optional.get()和Stream.collect()提前终止monad。本文主要讲述如何通过提升方法来使得代码更”函数式”。
假设有一个接口可以对数字进行计算。
public interface Math { int multiply(int a, int b); double divide(int a, int b); ..}
我们要使用这个接口来对使用Optional做包装的数字做计算。
public interface NumberProvider { Optional<Integer> getNumber();}
接着我们来实现一个方法能够返回两个数字相除的结果,结果用Optional包装。如果这两个数字有一个为空则返回空Optional。如下:
public Optional<Double> divideFirstTwo(NumberProvider numberProvider, Math math) { Optional<Integer> first = numberProvider.getNumber(); Optional<Integer> second = numberProvider.getNumber(); if(first.isPresent() && second.isPresent()) { double result = math.divide(first.get(), second.get()); return Optional.of(result); } else { return Optional.empty(); }}
上面的代码非常不优雅,有大量的代码都是在做Optional的包装和解包装。可以让上面的代码变得更加“函数式”,如下:
public Optional<Double> divideFirstTwo(NumberProvider numberProvider, Math math) { return numberProvider.getNumber() .flatMap(first -> numberProvider.getNumber() .map(second -> math.divide(first, second)));}
这样代码少了很多,也优雅了很多。先调用第一个Optional的flatMap,再在lambda中调用第二个Optional的map,进一步可以抽取出一个提升方法:
public interface Optionals { static <R, T, Z> BiFunction<Optional<T>, Optional<R>, Optional<Z>> lift( BiFunction<? super T, ? super R, ? extends Z> function) { return (left, right) -> left.flatMap(leftVal -> right.map( rightVal -> function.apply(leftVal, rightVal))); }}
如上,可知这个方法提升能够提升任何具有两个Optional参数、一个Optional结果的函数,使得被提升的函数具有Optional的一个特性:如果一个参数是空的,那么结果就是空的。如果JDK抽取flatMap和map到一个公共接口,如Monad,那么我们可以为Java Monad的每一个实例(Stream、Optional、自己的实现类)实现一个公共的提升函数。但现实是我们不得不为每一个实例都复制粘贴上面的代码。最终的divideFirstTwo代码如下:
import static com.ps.functional.monad.optional.Optionals.lift;...public Optional<Double> divideFirstTwo(NumberProvider numberProvider, Math math) { return lift(math::divide).apply(numberProvider.getNumber(), numberProvider.getNumber());}
- java8 新特性
- java8新特性 ---译
- Java8新特性教程
- Java8新特性学习
- JAVA8新特性
- Java8新特性详解
- JAVA8的新特性
- java8 新特性
- java8新特性
- java8 新特性
- JAVA8新特性一览
- Java8 新特性学习
- Java8新特性 Stream
- java8新特性
- java8新特性
- java8新特性
- java8新特性学习
- JAVA8新特性
- table tr th td 去除默认的边距,间距方法
- nodejs基础: 如何升级Noejs版本
- S3FD: Single Shot Scale-invariant Face Detector
- opencv学习笔记之seq
- hdu-6201 transaction transaction transaction
- Java8 新特性
- destoon短信接口更换成和其他运营商通道并存
- JS中日期转换
- JCTools
- Python如何从相对路径下import
- 聊天机器人5步重塑酒店业
- ArcEingine属性表读取并加载到GridView
- 【Python】Matplotlib画图(七)——线的颜色、点的形状
- 9月8日云栖精选夜读:杭城上演阿里巴巴“春运”大片……