本篇博客你将学到:
1.Lambda表达式
2.Optional类,告别空指针异常
3.Stream流式处理
4.时间处理LocalDate、LocalTime、LocalDateTime、ZonedDateTime、Clock、Duration
5.重复注解
6.扩展注释
7.更好的类型推荐机制
8.参数名字保存在字节码中
9.异步调用 CompletableFuture
1. Lambda表达式
在JDK8之前,一个方法能接受的参数都是变量,例如: object.method(Object o)
,那么,如果需要传入一个动作呢?比如回调。那么你可能会想到匿名内部类。例如:
首先定义一个业务类:
public class Person {
public void create(String name, PersonCallback personCallback)
{
System.out.println("执行主业务方法");
personCallback.callback();
}
}
匿名内部类是需要依赖接口的,所以需要定义个接口:
@FunctionalInterface
public interface PersonCallback {
void callback();
}
编写测试类:
public class Test {
public static void main(String[] args) {
new Person().create("源码之路", new PersonCallback() {
@Override
public void callback() {
System.out.println("调用回调函数");
}
});
}
}
打印结果:
上面的PersonCallback其实就是一种动作,但是我们真正关心的只有callback方法里的内容而已,为了写callback里面的内容,我还要生成一个内部类并实现callback方法。在java8以前,这也是没办法的事情,因为一个方法传入的参数必须是java原生变量和对象,不能传递方法。java8改变了增加一个一种参数传递方式,那就是我们可以传递一个方法了,即Lambda表达式。我们对上述代码做如下修改:
public class Test {
public static void main(String[] args) {
new Person().create("源码之路",()->{
System.out.println("调用回调函数");
});
}
}
()->{ System.out.println("调用回调函数"); }
就是Lambda表达式啦,他是接口PersonCallback 的实现者,Lambda允许把函数作为一个方法的参数,一个lambda由用逗号分隔的参数列表、 –>符号、函数体符号、函数体三部分表示。对于上面的表达式,()为Lambda表达式的入参,这里为空,{ System.out.println("调用回调函数"); }为函数体,重点是这个表达式是没有名字的。
我们知道,当我们实现一个接口的时候,肯定要实现接口里面的方法,那么现在一个Lambda表达式应该也要遵循这一个基本准则,那么一个Lambda表达式它实现了接口里的什么方法呢?
答案是:一个Lambda表达式实现了接口里的有且仅有的唯一一个抽象方法
。那么对于这种接口就叫做函数式接口。再次强调,这个接口必须只能有且只有一个抽象方法。Lambda表达式其实完成了实现接口并且实现接口里的方法这一功能,也可以认为Lambda表达式代表一种动作。我们可以直接把这种特殊的动作进行传递。
我们现在在接口方法里传递一个参数:
@FunctionalInterface
public interface PersonCallback {
void callback(String name);
}
public class Test {
public static void main(String[] args) {
new Person().create("源码之路",(name)->{
System.out.println("调用回调函数:"+name);
});
}
}
由此可见,()就是用来传递参数列表的。对于上面的表达式我们还可以进行简化:
public class Test {
public static void main(String[] args) {
new Person().create("源码之路", name->System.out.println("调用回调函数:"+name) );
}
}
小括号()去掉了,这归功于Java8的类型推导机制。因为现在接口里只有一个方法,那么现在这个Lambda表达式肯定是对应实现了这个方法,既然是唯一的对应关系,那么入参肯定是String类型,所以可以简写,并且方法体只有唯一的一条语句,所以也可以简写,以达到表达式简洁的效果。也就是说java8能自动匹配你的参数和方法体中的内容,当然如果你的方法体有很多条语句的话,{}还是不能省略的。
是方法也是实例
考虑这么一种情况,PersonCallback 是一个接口,我要生成一个该接口的实现类的对象,传统做法是编写一个类实现这个接口,然后实现这个接口的方法,如:
public class PersonCallBackImpl implements PersonCallback {
@Override
public void callback(String name) {
System.out.println(name);
}
}
然后
PersonCallback per = new PersonCallBackImpl ();
per.callback("源码之路:");
就算是java8,工作中我们还是会这么用,因为上文说了,Lambda表达式要求你的接口只能有一个方法,而我们的业务接口往往有多个方法,不适合Lambda表达式。但是,如果我的接口就是一个方法,而我就是要实现一个实例且调用接口方法,上面写法很麻烦,换种写法看看:
PersonCallback personCallback = (name) ->{System.out.println(name);};
personCallback.callback("源码之路");
(name) ->{System.out.println(name);}
就是表示接口类型的对象。
有的读者包括我同事会说,我一直用实现类的方式,Lambda我不习惯,而且感觉可读性差,我不想学,我现在的够用了!OK,没人逼着你学新东西,但是作为我们IT人,就是要不断的学习不断地更新自己的技术栈,不然就要被淘汰,没办法,技术更新换代太快了。中国有句俗语,熟能生巧,IT也是,再难的技术,多研究几遍,也就那么回事。
()里就一个参数,如果我们加一个,或者不加会怎样:
报错了,其实上文讲过了,这是源于java8的类型推导机制,接口中唯一的方法只有一个参数,编译器很智能的推导出来了,而且一个参数的话可以省略小括号,无参或者多参都不能省略小括号。此外,不但能推导出方法参数的个数,还能推导出参数的类型。
看见没,编译器能推导出参数类型为String,如果传一个int型参数,就会报错。
1.1 函数式接口
一个接口里面只有一个抽象方法,这个接口就是函数式接口,这其实上面说的PersonCallback 就是函数式接口,这只是一种定义,不过函数式接口有个标志,就是@FunctionalInterface修饰了这个接口。
@FunctionalInterface
public interface PersonCallback {
void callback(String name);
}
这个注解起到了校验的作用,比如,我在上面的接口中再增加一个抽象方法就会编译错误:
有读者说了,要你这么说,我连PersonCallback也不想写,我每次都要定义一个接口才能使用Lambda表达式,比如我就想传递两个int类型数值a和b,然后计算a+b的值返回,那我该怎么做?
首先,定义一个函数式接口:
@FunctionalInterface
public interface Caculate {
int add(int a , int b);
}
使用,
Caculate caculate = (a,b)->{return a+b;};
caculate.add(1,2);
其实,JDK8中也增加了很多函数式接口,比如java.util.function包,比如这四个常用的接口:
Supplier 无参数,返回一个结果
Function 接受一个输入参数,返回一个结果
Consumer 接受一个输入参数,无返回结果
Predicate 接受一个输入参数,返回一个布尔值结果
我们可以直接使用,不需要自己去定义函数式接口,减少代码量:
Supplier:
package java.util.function;
@FunctionalInterface
public interface Supplier<T> {
T get();
}
public class Test {
public static void main(String[] args) {
Supplier<String> supplier = ()->{return "源码之路";};
String retu = supplier.get();
}
}
Function :
public class Test {
public static void main(String[] args) {
Function<Integer, String> function = (a)->{return a+b;};
String retu = function.apply(3);
}
}
Consumer
public class Test {
public static void main(String[] args) {
Consumer<String> consumer = (a)->{System.out.println(a);};
consumer.accept("源码之路");
}
}
public class Test {
public static void main(String[] args) {
Predicate<String> predicate = (a)->{
if(a.equals("源码之路")){
return true;}
else{return false;}
};
boolean isSuccess = predicate.test("源码之路");
}
}
Java8中提供给我们这么多函数式接口就是为了让我们写 中提供给我们这么多函数式接口就是为了让我们写Lambda表达式更加方便 表达式更加方便,当然遇到特殊情况,你还是需要定义你自己的函数式接口然后才能写对应的Lambda表达式。
Lambda表达式还有个技巧,就是如果你实现的接口需要返回数据,且你实现的方法体只有一行语句,则可以省略return关键字,大家自己调试,学技术要自己动手尝试。
假如一个接口有三个实现类:
public interface PersonInterface {
void getName();
}
public class YellowPerson implements PersonInterface {
@Override
public void getName() {
System.out.println("yellow");
}
}
public class WhitePerson implements PersonInterface {
@Override
public void getName() {
System.out.println("white");
}
}
public class BlackPerson implements PersonInterface {
@Override
public void getName() {
System.out.println("black");
}
}
现在我需要在PersonInterface接口中新增一个方法,那么势必它的三个实现类都需要做相应改动才能编译通过,这里我就不进行演示了,那么我们在最开始设计的时候,其实可以增加一个抽象类PersonAbstract,三个实现类改为继承这个抽象类,按照这种设计方法,对PersonInterface接口中新增一个方法是,其实只需要改动PersonAbstract类去实现新增的方法就好了,其他实现类不需要改动了:
public interface PersonInterface {
void getName();
void walk();
}
public abstract class PersonAbstract implements PersonInterface {
@Override
public void walk() {
System.out.println("walk");
}
}
public class BlackPerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("black");
}
}
public class WhitePerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("white");
}
}
public class YellowPerson extends PersonAbstract {
@Override
public void getName() {
System.out.println("yellow");
}
}
在Java8中支持直接在接口中添加已经实现了的方法,一种是Default方法(默认方法),一种Static方法(静态方法)。
1.2 接口的默认方法
在接口中用default修饰的方法称为默认方法 默认方法。接口中的默认方法一定要有默认实现(方法体),接口实现者可以继承它,也可以覆盖它。
default void testDefault(){
System.out.println("default");
};
public class Test {
public static void main(String[] args) {
PersonCallback personCallback = (name)->{System.out.println(name);};
personCallback.testDefault();
}
}
1.3 静态方法
在接口中用static修饰的方法称为静态方法 静态方法。
static void testStatic(){
System.out.println("static");
};
调用方式:
TestInterface.testStatic();
因为有了默认方法和静态方法,所以你不用去修改它的实现类了,可以进行直接调用。注意,默认方法和静态方法可以写多个,实现类只能调用默认方法不能调用静态方法,静态方法只能通过TestInterface.testStatic()的方式调用。
这也正对应了“迪米特法则”面向接口编程!
1.4 方法引用
有个函数式接口Consumer,里面有个抽象方法accept能够接收一个参数但是没有返回值,这个时候我想实现accept方法,让它的功能为打印接收到的那个参数,那么我可以使用Lambda表达式这么做:
Consumer<String> consumer = s -> System.out.println(s);
consumer.accept("ޮ源码之路");
但是其实我想要的这个功能PrintStream类(也就是System.out的类型)的println方法已经实现了,这一步还可以再简单点,如:
Consumer<String> consumer = System.out::println;
consumer.accept("ޮ源码之路");
打印结果:
这就是方法引用,方法引用方法的参数列表必须与函数式接口的抽象方法的参数列表保持一致,返回值不作要求。
引用方法
- 实例对象::实例方法名
- 类名::静态方法名
- 类名::实例方法名
实例对象::实例方法名
Consumer<String> consumer = System.out::println;
consumer.accept("ޮቲᘌ");
System.out代表的就是PrintStream类型的一个实例, println是这个实例的一个方法。
类名::静态方法名
Function<Long, Long> f = Math::abs;
Long result = f.apply(-3L);
Math是一个类而abs为该类的静态方法。 Function中的唯一抽象方法apply方法参数列表与abs方法的参数列表相同,都是接收一个Long类型参数。
类名::实例方法名
若Lambda表达式的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,就可以使用这种方法
BiPredicate<String, String> b = String::equals;
b.test("a", "b");
String是一个类而equals为该类的定义的实例方法。 BiPredicate中的唯一抽象方法test方法参数列表与equals方法的参数列表相同,都是接收两个String类型参数。
引用构造器
在引用构造器的时候,构造器参数列表要与接口中抽象方法的参数列表一致,格式为 类名::new。如:
Function<Integer, StringBuffer> fun = StringBuffer::new;
StringBuffer buffer = fun.apply(10);
Function接口的apply方法接收一个参数,并且有返回值。在这里接收的参数是Integer类型,与StringBuffer类的一个构造方法StringBuffer(int capacity)对应,而返回值就是StringBuffer类型。上面这段代码的功能就是创建一个Function实例,并把它apply方法实现为创建一个指定初始大小的StringBuffer对象。
引用数组
引用数组和引用构造器很像,格式为 类型[]::new,其中类型可以为基本类型也可以是类。如:
Function<Integer, int[]> fun = int[]::new;
int[] arr = fun.apply(10);
Function<Integer, Integer[]> fun2 = Integer[]::new;
Integer[] arr2 = fun2.apply(10);
2.Optional
空指针异常是导致Java应用程序失败的最常见原因,以前,为了解决空指针异常, Google公司著名的Guava项目引入了Optional类, Guava通过使用检查空值的方式来防止代码污染,它鼓励程序员写更干净的代码。受到Google Guava的启发, Optional类已经成为Java 8类库的一部分。
Optional实际上是个容器:它可以保存类型T的值,或者仅仅保存null。 Optional提供很多有用的方法,这样我们就不用显式进行空值检测。
我们先看Optional介绍,再来实战。
创建Optional对象的几个方法:
1. Optional.of(T value), 返回一个Optional对象, value不能为空,否则会出空指针异常
2. Optional.ofNullable(T value), 返回一个Optional对象, value可以为空
3. Optional.empty(),代表空
其他API:
1. optional.isPresent(),是否存在值(不为空)
2. optional.ifPresent(Consumer<? super T> consumer), 如果存在值则执行consumer
3. optional.get(),获取value
4. optional.orElse(T other),如果没值则返回other
5. optional.orElseGet(Supplier<? extends T> other),如果没值则执行other并返回
6. optional.orElseThrow(Supplier<? extends X> exceptionSupplier),如果没值则执行exceptionSupplier, 并抛出异常
那么,我们之前对于防止空指针会这么写:
public class Order {
String name;
public String getOrderName(Order order ) {
if (order == null) {
return null;
}
return order.name;
}
}
现在用Optional,会改成:
public class Order {
String name = "源码之路";
public String getOrderName(Order order ) {
// if (order == null) {
// return null;
// }
//
// return order.name;
Optional<Order> orderOptional = Optional.ofNullable(order);
if (!orderOptional.isPresent()) {
return null;
}
return orderOptional.get().name;
}
}
那么如果只是改成这样,实质上并没有什么分别,事实上isPresent() 与 obj != null 无任何分别,并且在使用get()之前最好都使用isPresent() ,比如下面的代码在IDEA中会有警告:'Optional.get()' without 'isPresent()' check。另外把 Optional 类型用作属性或是方法参数在 IntelliJ IDEA 中更是强力不推荐的。
对于上面的代码我们利用IDEA的提示可以优化成一行( 666!):
public class Order {
String name;
public String getOrderName(Order order ) {
// if (order == null) {
// return null;
// }
// return order.name;
return Optional.ofNullable(order).map(order1 -> order1.name).orElse(null);
}
}
这个优化过程中map()起了很大作用。
第一次用,感觉很抽象,难以理解,也不习惯这么用。我们看以下Optional的源码,大家就会理解很多:
public final class Optional<T> {
//EMPTY常量,即存放空值的Optional对象
private static final Optional<?> EMPTY = new Optional();
//被存放的值,可为null或非null值
private final T value;
//私有构造方法,创建一个包含空值的Optional对象
private Optional() {
this.value = null;
}
//私有构造方法,创建一个非空值的Optional对象
private Optional(T var1) {
this.value = Objects.requireNonNull(var1);
}
//这个方法很简单,作用是返回一个Optional实例,里面存放的value是null
public static <T> Optional<T> empty() {
Optional var0 = EMPTY;
return var0;
}
//很简单,就是返回一个包含非空值的Optional对象
public static <T> Optional<T> of(T var0) {
return new Optional(var0);
}
// 很简单,返回一个可以包含空值的Optional对象
public static <T> Optional<T> ofNullable(T var0) {
return var0 == null ? empty() : of(var0);
}
//得到Optional对象里的值,如果值为null,则抛出NoSuchElementException异常
public T get() {
if (this.value == null) {
throw new NoSuchElementException("No value present");
} else {
return this.value;
}
}
//很简单,判断值是否不为null
public boolean isPresent() {
return this.value != null;
}
// 当值不为null时,执行consumer
public void ifPresent(Consumer<? super T> var1) {
if (this.value != null) {
var1.accept(this.value);
}
}
/*
*看方法名就知道,该方法是过滤方法,过滤符合条件的Optional对象,这里的条件用Lambda表达式来定义,
*如果入参predicate对象为null将抛NullPointerException异常,
*如果Optional对象的值为null,将直接返回该Optional对象,
*如果Optional对象的值符合限定条件(Lambda表达式来定义),返回该值,否则返回空的Optional对象
*/
public Optional<T> filter(Predicate<? super T> var1) {
Objects.requireNonNull(var1);
if (!this.isPresent()) {
return this;
} else {
return var1.test(this.value) ? this : empty();
}
}
/**
* 前面的filter方法主要用于过滤,一般不会修改Optional里面的值,map方法则一般用于修改该值,并返回修改后的Optional对象
* 如果入参mapper对象为null将抛NullPointerException异常,
* 如果Optional对象的值为null,将直接返回该Optional对象,
* 最后,执行传入的lambda表达式,并返回经lambda表达式操作后的Optional对象
*/
public <U> Optional<U> map(Function<? super T, ? extends U> var1) {
Objects.requireNonNull(var1);
return !this.isPresent() ? empty() : ofNullable(var1.apply(this.value));
}
/**
* flatMap方法与map方法基本一致,唯一的区别是,
* 如果使用flatMap方法,需要自己在Lambda表达式里将返回值转换成Optional对象,
* 而使用map方法则不需要这个步骤,因为map方法的源码里已经调用了Optional.ofNullable方法;
*/
public <U> Optional<U> flatMap(Function<? super T, Optional<U>> var1) {
Objects.requireNonNull(var1);
return !this.isPresent() ? empty() : (Optional)Objects.requireNonNull(var1.apply(this.value));
}
//很简单,当值为null时返回传入的值,否则返回原值;
public T orElse(T var1) {
return this.value != null ? this.value : var1;
}
//功能与orElse(T other)类似,不过该方法可选值的获取不是通过参数直接获取,而是通过调用传入的Lambda表达式获取
public T orElseGet(Supplier<? extends T> var1) {
return this.value != null ? this.value : var1.get();
}
//当遇到值为null时,根据传入的Lambda表达式跑出指定异常
public <X extends Throwable> T orElseThrow(Supplier<? extends X> var1) throws Throwable {
if (this.value != null) {
return this.value;
} else {
throw (Throwable)var1.get();
}
}
public boolean equals(Object var1) {
if (this == var1) {
return true;
} else if (!(var1 instanceof Optional)) {
return false;
} else {
Optional var2 = (Optional)var1;
return Objects.equals(this.value, var2.value);
}
}
public int hashCode() {
return Objects.hashCode(this.value);
}
public String toString() {
return this.value != null ? String.format("Optional[%s]", this.value) : "Optional.empty";
}
}
使用案例
阅读完源码(很简单有没有),我们举几个使用例子来加深印象:
ifPresent
,当值不为null时,执行Lambda表达式
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcDef");
//值不为null,执行Lambda表达式,
test.ifPresent(name -> {
String s = name.toUpperCase();
System.out.println(s);
});
//打印ABCDEF
}
}
filter
,如果Optional对象的值符合限定条件(Lambda表达式来定义),返回该值,否则返回空的Optional对象
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//过滤值的长度小于3的Optional对象
Optional<String> less3 = test.filter((value) -> value.length() < 3);
//打印结果
System.out.println(less3.orElse("不符合条件,不打印值!"));
}
}
map
,如果Optional对象的值为null,将直接返回该Optional对象,否则,执行传入的lambda表达式,并返回经lambda表达式操作后的Optional对象
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//将值修改为大写
Optional<String> less3 = test.map((value) -> value.toUpperCase());
//打印结果 ABCD
System.out.println(less3.orElse("值为null,不打印!"));
}
}
orElseGet
,功能与orElse(T other)类似,不过该方法可选值的获取不是通过参数直接获取,而是通过调用传入的Lambda表达式获取
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable(null);
System.out.println(test.orElseGet(() -> "hello"));
//将打印hello
}
}
orElseThrow
,当遇到值为null时,根据传入的Lambda表达式跑出指定异常
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable(null);
//这里的Lambda表达式为构造方法引用
System.out.println(test.orElseThrow(NullPointerException::new));
//将打印hello
}
}
Optional总结:
使用 Optional 时尽量不直接调用 Optional.get() 方法, Optional.isPresent() 更应该被视为一个私有方法, 应依赖于其他像 Optional.orElse(), Optional.orElseGet(), Optional.map() 等这样的方法。
3. Stream
Java8中的Stream是对集合(Collection)对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作(aggregate operation),或者大批量数据操作 (bulk data operation)。 Stream API 借助于同样新出现的Lambda表达式,极大的提高编程效率和程序可读性。同时它提供串行和并行两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用 fork/join 并行方式来拆分任务和加速处理过程。通常编写并行代码很难而且容易出错, 但使用 Stream API 无需编写一行多线程的代码,就可以很方便地写出高性能的并发程序。
所以说,Java8中首次出现的java.util.stream是一个函数式语言+多核时代综合影响的产物。
但在当今这个数据大爆炸的时代,在数据来源多样化、数据海量化的今天,很多时候不得不脱离 RDBMS,或者以底层返回的数据为基础进行更上层的数据统计。而 Java 的集合 API 中,仅仅有极少量的辅助型方法,更多的时候是程序员需要用 Iterator 来遍历集合,完成相关的聚合应用逻辑。这是一种远不够高效、笨拙的方法。在Java7中,如果要找一年级的所有学生,然后返回按学生分数值降序排序好的学生ID的集合,我们需要这样写:
package stream;
//学生类
public class Student {
private Integer id;
private Grade grade;
private Integer score;
public Student(Integer id, Grade grade, Integer score) {
this.id = id;
this.grade = grade;
this.score = score;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public Integer getScore() {
return score;
}
public void setScore(Integer score) {
this.score = score;
}
}
//班级类
package stream;
public enum Grade {
FIRST, SECOND, THTREE
}
package stream;
import com.google.common.collect.Lists;
import java.util.*;
public class Test {
public static void main(String[] args) {
final Collection<Student> students = Arrays.asList(
new Student(1, Grade.FIRST, 60),
new Student(2, Grade.SECOND, 80),
new Student(3, Grade.FIRST, 100),
new Student(4, Grade.FIRST, 78),
new Student(5, Grade.FIRST, 92)
);
List<Student> gradeOneStudents = Lists.newArrayList();
for (Student student: students) {
if (Grade.FIRST.equals(student.getGrade())) {
gradeOneStudents.add(student);
}
}
Collections.sort(gradeOneStudents, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
return o2.getScore().compareTo(o1.getScore());
}
});
List<Integer> studentIds = new ArrayList<>();
for(Student t: gradeOneStudents){
studentIds.add(t.getId());
System.out.println("id:"+t.getId()+"——得分"+t.getScore());
}
}
}
打印结果:
而在 Java 8 使用 Stream,代码更加简洁易读;而且使用并发模式,程序执行速度更快,只需要将stram()变成parallelStream()即可。
public static void main(String[] args) {
final Collection< Student > students= Arrays.asList(
new Student(1, Grade.FIRST, 60),
new Student(2, Grade.SECOND, 80),
new Student(3, Grade.FIRST, 100),
new Student(4, Grade.FIRST, 78),
new Student(5, Grade.FIRST, 92)
);
List<Integer> studentIds = students.stream()
.filter(student -> student.getGrade().equals(Grade.FIRST))
.sorted(Comparator.comparingInt(Student::getScore))
.map(Student::getId)
.collect(Collectors.toList());
}
下面我们详解以下stream。
3.1 什么是stream
Stream 不是集合元素,它不是数据结构并不保存数据,它是有关算法和计算的,它更像一个高级版本的Iterator。
3.2 stream特点
- Iterator,用户只能显式地一个一个遍历元素并对其执行某些操作;Stream,用户只要给出需要对其包含的元素执行什么操作,比如 “过滤掉长度大于 10 的字符串”、 “获取每个字符串的首字母”等,Stream 会隐式地在内部进行遍历,做出相应的数据转换。
- Stream 就如同一个Iterator,单向,不可往复,数据只能遍历一次,遍历过一次后即用尽了,就好比流水从面前流过,一去不复返。
- Stream 可以并行化操作,Iterator只能命令式地、串行化操作。顾名思义,当使用串行方式去遍历时,每个 item 读完后再读下一个 item。而使用并行去遍历时,数据会被分成多个段,其中每一个都在不同的线程中处理,然后将结果一起输出。 Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架来拆分任务和加速处理过程。
3.3 stream的构成
当我们使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一
个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道,如下图所示。
3.3.1生成stream source的方式
-
从Collection 和数组生成:
- Collection.stream()
- Collection.parallelStream()
- Arrays.stream(T array)
- Stream.of(T t)
-
从BufferedReader
- java.io.BufferedReader.lines()
-
静态工厂
- java.util.stream.IntStream.range()
- java.nio.file.Files.walk()
自己构建
java.util.Spliterator-
其它
- Random.ints()
- BitSet.stream()
- Pattern.splitAsStream(java.lang.CharSequence)
- JarFile.stream()
3.3.2 stream的操作类型
- 中间操作(Intermediate Operation):一个流可以后面跟随零个或多个 intermediate操作。其目的主要是打开流,做出某种程度的数据映射/过滤,然后返回一个新的流,交给下一个操作使用。这类操作都是惰性化的(lazy),就是说,仅仅调用到这类方法,并没有真正开始流的遍历。
- 终止操作(Terminal Operation):一个流只能有一个 terminal 操作,当这个操作执行后,流就被使用“光”了,无法再被操作。所以这必定是流的最后一个操作。 Terminal 操作的执行,才会真正开始流的遍历,并且会生成一个结果。
中间操作(Intermediate Operation)又可以分为两种类型:
- 无状态操作(Stateless Operation):操作是无状态的,不需要知道集合中其他元素的状态,每个元素之间是相互独立的,比如map()、 filter()等操作。
- 有状态操作(Stateful Operation):有状态操作,操作是需要知道集合中其他元素的状态才能进行的,比如sort()、 distinct()。
终止操作(Terminal Operation)从逻辑上可以分为两种: - 短路操作(short-circuiting):短路操作是指不需要处理完所有元素即可结束整个过程
- 非短路操作(non-short-circuiting):非短路操作是需要处理完所有元素之后才能结束整个过程。
3.3.3 stream 的使用
简单说,对 Stream 的使用就是实现一个 filter-map-reduce 过程,产生一个最终结果,或者导致一个副作用。
构造流的几种常见方法:
// 1. Individual values
Stream stream = Stream.of("a", "b", "c");
// 2. Arrays
String [] strArray = new String[] {"a", "b", "c"};
stream = Stream.of(strArray);
stream = Arrays.stream(strArray);
// 3. Collections
List<String> list = Arrays.asList(strArray);
stream = list.stream();
需要注意的是,对于基本数值型,目前有三种对应的包装类型 Stream:
IntStream、 LongStream、 DoubleStream。当然我们也可以用 Stream、 Stream >、 Stream,但是 boxing 和unboxing 会很耗时,所以特别为这三种基本数值型提供了对应的 Stream。
Java 8 中还没有提供其它数值型 Stream,因为这将导致扩增的内容较多。而常规的数值型聚合运算可以通过上面三种 Stream 进行。
数值流的构造
IntStream.of(new int[]{1, 2, 3}).forEach(System.out::println);
IntStream.range(1, 3).forEach(System.out::println);
IntStream.rangeClosed(1, 3).forEach(System.out::println);
流转换成其他的数据结构
Stream<String> stream = Stream.<String>of(new String[]{"1", "2", "3"});
List<String> list1 = stream.collect(Collectors.toList());
List<String> list2 = stream.collect(Collectors.toCollection(ArrayList::new));
String str = stream.collect(Collectors.joining());
System.out.println(str);
注意,一个 Stream 只可以使用一次,上面的代码为了简洁而重复使用了数次。
流的典型用法
- map/flatmap
我们先来看 map。如果你熟悉 scala 这类函数式语言,对这个方法应该很了解,它的作用就是把 input Stream的每一个元素,映射成 output Stream 的另外一个元素。
Stream<String> stream = Stream.<String>of(new String[]{"a", "b", "c"});
stream.map(String::toUpperCase).forEach(System.out::println);
这段代码把所有的字母转换为大写。 map 生成的是个 1:1 映射,每个输入元素,都按照规则转换成为另外一个元素。还有一些场景,是一对多映射关系的,这时需要 flatMap:
Stream<List<Integer>> inputStream = Stream.of(
Arrays.asList(1),
Arrays.asList(2, 3),
Arrays.asList(4, 5, 6)
);
Stream<Integer> mapStream = inputStream.map(List::size);
Stream<Integer> flatMapStream = inputStream.flatMap(Collection::stream);
- filter
filter 对原始 Stream 进行某项测试,通过测试的元素被留下来生成一个新Stream。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).filter(n -> n<3).forEach(System.out::println);
将小于3的数字留下来。
- forEach
forEach 是 terminal 操作,因此它执行后,Stream 的元素就被“消费”掉了,你无法对一个 Stream 进行两次terminal 运算。下面的代码会报错。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Stream stream = Arrays.stream(nums);
stream.forEach(System.out::print);
stream.forEach(System.out::print);
相反,具有相似功能的 intermediate 操作 peek 可以达到上述目的。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Stream stream = Arrays.stream(nums);
stream.peek(System.out::println)
.peek(data-> System.out.println("重复利用:"+data))
.collect(Collectors.toList());
forEach 不能修改自己包含的本地变量值,也不能用 break/return 之类的关键字提前结束循环。下面的代码还是打印出所有元素,并不会提前返回。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).forEach(integer -> {
System.out.print(integer);
return;
});
forEach和常规和常规for循环的差异不涉及到性能,它们仅仅是函数式风格与传统循环的差异不涉及到性能,它们仅仅是函数式风格与传统Java风格的差别。风格的差别。
- reduce
这个方法的主要作用是把 Stream 元素组合起来。它提供一个起始值(种子),然后依照运算规则(BinaryOperator),和前面 Stream 的第一个、第二个、第 n 个元素组合。从这个意义上说,字符串拼接、数值的 sum、 min、 max、 average 都是特殊的 reduce。例如 Stream 的 sum 就相当于:
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Integer sum = Arrays.stream(nums).reduce(0, (integer, integer2) ->
integer+integer2);
System.out.println(sum);
也有没有起始值的情况,这时会把 Stream 的前面两个元素组合起来,返回的是 Optional。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
// 有初始值
Integer sum = Arrays.stream(nums).reduce(0, Integer::sum);
// 无初始值
Integer sum1 = Arrays.stream(nums).reduce(Integer::sum).get();
- limit / skip
limit 返回 Stream 的前面 n 个元素;skip 则是扔掉前 n 个元素。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).limit(3).forEach(System.out::print);
System.out.println();
Arrays.stream(nums).skip(2).forEach(System.out::print);
- sorted
对 Stream 的排序通过 sorted 进行,它比数组的排序更强之处在于你可以首先对 Stream 进行各类 map、 filter、limit、 skip 甚至 distinct 来减少元素数量后,再排序,这能帮助程序明显缩短执行时间。
Integer[] nums = new Integer[]{1,2,3,4,5,6};
Arrays.stream(nums).sorted((i1, i2) ->
i2.compareTo(i1)).limit(3).forEach(System.out::print);
System.out.println();
Arrays.stream(nums).sorted((i1, i2) ->
i2.compareTo(i1)).skip(2).forEach(System.out::print);
- min/max/distinct
Integer[] nums = new Integer[]{1, 2, 2, 3, 4, 5, 5, 6};
System.out.println(Arrays.stream(nums).min(Comparator.naturalOrder()).get());
System.out.println(Arrays.stream(nums).max(Comparator.naturalOrder()).get());
Arrays.stream(nums).distinct().forEach(System.out::print);
- match
Stream 有三个 match 方法,从语义上说:
-- allMatch:Stream 中全部元素符合传入的 predicate,返回 true
-- anyMatch:Stream 中只要有一个元素符合传入的 predicate,返回 true
-- noneMatch:Stream 中没有一个元素符合传入的 predicate,返回 true
它们都不是要遍历全部元素才能返回结果。例如 allMatch 只要一个元素不满足条件,就 skip 剩下的所有元素,返回 false。
Integer[] nums = new Integer[]{1, 2, 2, 3, 4, 5, 5, 6};
System.out.println(Arrays.stream(nums).allMatch(integer -> integer < 7));
System.out.println(Arrays.stream(nums).anyMatch(integer -> integer < 2));
System.out.println(Arrays.stream(nums).noneMatch(integer -> integer < 0));
用Collection来进行reduction操作:
java.util.stream.Collectors 类的主要作用就是辅助进行各类有用的 reduction 操作,例如转变输出为 Collection,把Stream 元素进行归组。
- groupingBy/partitioningBy
例如对上面的Student进行按年级进行分组:
final Collection<Student> students = Arrays.asList(
new Student(1, Grade.FIRST, 60),
new Student(2, Grade.SECOND, 80),
new Student(3, Grade.FIRST, 100)
);
// 按年级进行分组
students.stream().collect(Collectors.groupingBy(Student::getGrade)).forEach(((grade,
students1) -> {
System.out.println(grade);
students1.forEach(student ->
System.out.println(student.getId()+","+student.getGrade()+","+student.getScore()));
}));
打印结果:
例如对上面的Student进行按分数段进行分组:
students.stream().collect(Collectors.partitioningBy(student -> student.getScore()
<=60)).forEach(((grade, students1) -> {
System.out.println(grade);
students1.forEach(student ->
System.out.println(student.getId()+","+student.getGrade()+","+student.getScore()));
}));
parallelStream
parallelStream其实就是一个并行执行的流.它通过默认的ForkJoinPool,可以提高你的多线程任务的速度。
Arrays.stream(nums).parallel().forEach(System.out::print);
System.out.println(Arrays.stream(nums).parallel().reduce(Integer::sum).get());
System.out.println();
Arrays.stream(nums).forEach(System.out::print);
System.out.println(Arrays.stream(nums).reduce(Integer::sum).get());
parallelStream要注意的问题
parallelStream底层是使用的ForkJoin。而ForkJoin里面的线程是通过ForkJoinPool来运行的,Java 8为ForkJoinPool添加了一个通用线程池,这个线程池用来处理那些没有被显式提交到任何线程池的任务。它是ForkJoinPool类型上的一个静态元素。它拥有的默认线程数量等于运行计算机上的处理器数量,所以这里就出现了这个java进程里所有使用parallelStream的地方实际上是公用的同一个ForkJoinPool。 parallelStream提供了更简单的并发执行的实现,但并不意味着更高的性能,它是使用要根据具体的应用场景。如果cpu资源紧张parallelStream不会带来性能提升;如果存在频繁的线程切换反而会降低性能。
3.4 steam总结
- 不是数据结构,它没有内部存储,它只是用操作管道从 source(数据结构、数组、 generator function、IO channel)抓取数据。
- 它也绝不修改自己所封装的底层数据结构的数据。例如 Stream 的 filter 操作会产生一个不包含被过滤元素的新 Stream,而不是从 source 删除那些元素。
- 所有 Stream 的操作必须以 lambda 表达式为参数。
- 惰性化,很多 Stream 操作是向后延迟的,一直到它弄清楚了最后需要多少数据才会开始,Intermediate操作永远是惰性化的。
- 当一个 Stream 是并行化的,就不需要再写多线程代码,所有对它的操作会自动并行进行的。
4. Date/Time API
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理。对日期与时间的操作一直是Java程序员最痛苦的地方之一。标准的 java.util.Date以及后来的java.util.Calendar一点没有改善这种情况(可以这么说,它们一定程度上更加复杂)。
这种情况直接导致了Joda-Time——一个可替换标准日期/时间处理且功能非常强大的Java API的诞生。 Java 8新的Date-Time API (JSR 310)在很大程度上受到Joda-Time的影响,并且吸取了其精髓。
- LocalDate类
LocaleDate只持有ISO-8601格式且无时区信息的日期部分
LocalDate date = LocalDate.now(); // 当前日期
date = date.plusDays(1); // 增加一天
date = date.plusMonths(1); // 增加一个月
date = date.minusDays(1); // 减少一天
date = date.minusMonths(1); // 减少一个月
System.out.println(date);
- LocalTime类
LocaleTime只持有ISO-8601格式且无时区信息的时间部分
LocalTime time = LocalTime.now(); // 当前时间
time = time.plusMinutes(1); // 增加一分钟
time = time.plusSeconds(1); // 增加一秒
time = time.minusMinutes(1); // 减少一分钟
time = time.minusSeconds(1); // 减少一秒
System.out.println(time); // ׁ
- LocalDateTime类和格式化
LocaleDateTime把LocaleDate与LocaleTime的功能合并起来,它持有的是ISO-8601格式无时区信息的日期与时间。
LocalDateTime localDateTime = LocalDateTime.now();
System.out.println(localDateTime);
System.out.println(localDateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd
HH:mm:ss"))); // 2018-12-18 21:13:07 ᛔਧԎ໒ୗ
- ZonedDateTime类
如果你需要特定时区的日期/时间,那么ZonedDateTime是你的选择。它持有ISO-8601格式具具有时区信息的日期与时间。
final ZonedDateTime zonedDatetimeFromZone = ZonedDateTime.now( ZoneId.of(
"America/Los_Angeles" ) );
System.out.println(zonedDatetimeFromZone);
-Clock类
它通过指定一个时区,然后就可以获取到当前的时刻,日期与时间。 Clock可以替换System.currentTimeMillis()与TimeZone.getDefault()。
final Clock utc = Clock.systemUTC(); // 协调世界时,又称世界统一时间、世界标准时间、国际协调时间
final Clock shanghai = Clock.system(ZoneId.of("Asia/Shanghai")); // 上海
System.out.println(LocalDateTime.now(utc));
System.out.println(LocalDateTime.now(shanghai));
- Duration类
Duration使计算两个日期间的不同变的十分简单。
final LocalDateTime from = LocalDateTime.parse("2018-12-17 18:50:50", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
final LocalDateTime to = LocalDateTime.parse("2018-12-18 19:50:50", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
final Duration duration = Duration.between(from, to);
System.out.println("Duration in days: " + duration.toDays()); // 1
System.out.println("Duration in hours: " + duration.toHours()); // 25
5.重复注解
假设,现在有一个服务我们需要定时运行,就像Linux中的cron一样,假设我们需要它在每周三的12点运行一次,那我们可能会定义一个注解,有两个代表时间的属性。
@Retention(RetentionPolicy.RUNTIME)
public @interface Schedule {
int dayOfWeek() default 1; //周几
int hour() default 0; //几点
}
所以我们可以给对应的服务方法上使用该注解,代表运行的时间:
public class ScheduleService {
//每周三的12点执行
@Schedule(dayOfWeek = 3, hour = 12)
public void start() {
//执行服务
}
}
那么如果我们需要这个服务在每周四的13点也需要运行一下,如果是JDK8之前,那么...尴尬了!你不能像下面的代码,会编译错误
public class ScheduleService {
//jdk中两个相同的注解会报错
@Schedule(dayOfWeek = 3, hour = 12)
@Schedule(dayOfWeek = 4, hour = 13)
public void start() {
//执行服务
}
}
那么如果是JDK8,你可以改一下注解的代码,在自定义注解上加上@Repeatable元注解,并且指定重复注解的存储注解(其实就是需要需要数组来存储重复注解),这样就可以解决上面的编译报错问题。
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(value = Schedule.Schedules.class)
public @interface Schedule {
int dayOfWeek() default 1;
int hour() default 0;
@Retention(RetentionPolicy.RUNTIME)
@interface Schedules {
Schedule[] value();
}
}
同时,反射相关的API提供了新的函数getAnnotationsByType()来返回重复注解的类型。
添加main方法:
public static void main(String[] args) {
try {
Method method = ScheduleService.class.getMethod("start");
for (Annotation annotation : method.getAnnotations()) {
System.out.println(annotation);
}
for (Schedule s : method.getAnnotationsByType(Schedule.class)) {
System.out.println(s.dayOfWeek() + "|" + s.hour());
}
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
6.扩展注释
注解就相当于一种标记,在程序中加了注解就等于为程序加了某种标记。
JDK8之前的注解只能加在:
public enum ElementType {
/** Class, interface (including annotation type), or enum declaration */
TYPE, // 类,接口,枚举
/** Field declaration (includes enum constants) */
FIELD, // 类型变量
/** Method declaration */
METHOD, // 方法
/** Parameter declaration */
PARAMETER, // 方法参数
/** Constructor declaration */
CONSTRUCTOR, // 构造方法
/** Local variable declaration */
LOCAL_VARIABLE, // 局部变量
/** Annotation type declaration */
ANNOTATION_TYPE, // 注解类型
/** Package declaration */
PACKAGE // 包
}
JDK8中新增了两种:
- TYPE_PARAMETER,表示该注解能写在类型变量的声明语句中。
- TYPE_USE,表示该注解能写在使用类型的任何语句中
checkerframework中的各种校验注解,比如:@Nullable, @NonNull等等。
public class GetStarted {
void sample() {
@NonNull Object ref = null;
}
}
7.更好的类型推荐机制
直接看代码:
public class Value<T> {
public static<T> T defaultValue() {
return null;
}
public T getOrDefault(T value, T defaultValue) {
return value != null ? value : defaultValue;
}
public static void main(String[] args) {
Value<String> value = new Value<>();
System.out.println(value.getOrDefault("22", Value.defaultValue()));
}
}
上面的代码重点关注value.getOrDefault("22", Value.defaultValue()), 在JDK8中不会报错,那么在JDK7中呢?
答案是会报错: Wrong 2nd argument type. Found: 'java.lang.Object', required:
'java.lang.String' 。所以Value.defaultValue()的参数类型在JDK8中可以被推测出,所以就不必明确给出。
8.参数名字保存在字节码中
先来想一个问题:JDK8之前,怎么获取一个方法的参数名列表?
在JDK7中一个Method对象有下列方法:
Method.getParameterAnnotations()
获取方法参数上的注解
Method.getParameterTypes()
获取方法的参数类型列表
但是没有能够获取到方法的参数名字列表!
在JDK8中增加了两个方法:
Method.getParameters()
获取参数名字列表
Method.getParameterCount()
获取参数名字个数
用法:
public class ParameterNames {
public void test(String name, String address) {
}
public static void main(String[] args) throws Exception {
Method method = ParameterNames.class.getMethod("test", String.class,
String.class);
for (Parameter parameter : method.getParameters()) {
System.out.println(parameter.getName());
}
System.out.println(method.getParameterCount());
}
}
结果:
从结果可以看出输出的参数个数正确,但是名字不正确!需要在编译 编译时增加–parameters参数后再运行。
9. 异步调用 CompletableFuture
当我们Javer说异步调用时,我们自然会想到Future,比如:
public class FutureDemo {
/**
* 异步进行一个计算
* @param args
*/
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Integer> result = executor.submit(new Callable<Integer>() {
public Integer call() throws Exception {
int sum=0;
System.out.println("正在计算...");
for (int i=0; i<100; i++) {
sum = sum + i;
}
//模拟等待
Thread.sleep(TimeUnit.SECONDS.toSeconds(3));
System.out.println("算完了 ");
return sum;
}
});
try {
System.out.println("result:" + result.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("事情都做完了...");
executor.shutdown();
}
}
如果想实现异步计算完成之后,立马能拿到这个结果且继续异步做其他事情呢?这个问题就是一个线程依赖另外一个线程,这个时候Future就不方便,我们来看一下CompletableFuture的实现:
public class FutureDemo {
/**
* 异步进行一个计算
* @param args
*/
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
CompletableFuture result = CompletableFuture.supplyAsync(() -> {
int sum=0;
System.out.println("正在计算...");
for (int i=0; i<100; i++) {
sum = sum + i;
}
try {
Thread.sleep(TimeUnit.SECONDS.toSeconds(3));
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"算完了");
return sum;
}, executor).thenApplyAsync(sum -> {
System.out.println(Thread.currentThread().getName()+"打印"+sum);
return sum;
}, executor);
System.out.println("做其他事情...");
try {
System.out.println("result:" + result.get());
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("事情都做完了...");
executor.shutdown();
}
}
只需要简单的使用thenApplyAsync就可以实现了。
当然CompletableFuture还有很多其他的特性,我们下次单独开个专题来讲解。
9. Java虚拟机(JVM)新特性
PermGen空间被移除了,取而代之的是Metaspace(JEP 122)。 JVM选项-XX:PermSize与-XX:MaxPermSize分别被-XX:MetaSpaceSize与-XX:MaxMetaspaceSize所代替。虚拟机内容太多了,以后会出个专题,想深入了解虚拟机的读者可留言,看的人多我就抓紧更新。
`