微信搜「烟雨星空」,白嫖更多好文。
如今 Oracle 官方每隔半年就会出一个 JDK 新版本。按时间来算的话,这个月就要出 JDK15 了。然而,大部分公司仍是在使用 JDK7 和 8 。javascript
以前去我朋友家,居然被嘲笑不会用 JDK8 。 不服气的我,回来以后,固然是重点学习之啊。html
话很少说,本文目录以下:java
目录:程序员
先看下 lambda 表达式是怎么定义的:算法
lambda 表达式是一个匿名函数。 lambda 表达式容许把一个函数做为参数进行传递。apache
可能刚看到这两句话时,不知道是什么意思。那么,对比一下 js 中的 setInterval 函数的用法,你就能找到一些感受了。编程
//每一秒执行一次匿名函数。(模拟时钟) setInterval(function() { console.log("当前时间为:" + new Date()); }, 1000);
如上,function(){}
这段,就是一个匿名函数,而且能够把它做为参数传递给 setInterval 函数。数组
这是由于,在 js 中,函数是一等公民。安全
然而,在 Java 中,对象才是一等公民。可是,到了 JDK8 咱们也能够经过 lambda 表达式表示一样的效果。微信
lambda 表达式语法以下:
(参数1,参数2) -> { 方法体 }
左边指定了 lambda 表达式所须要的全部参数,右边用来描述方法体。->
即为 lambda 运算符。
想一下,在以前咱们经过匿名内部类的方式来启动一个线程,是怎么作的?
public class LambdaTest { @Test public void test(){ new Thread(new Runnable() { @Override public void run() { System.out.println("线程运行..."); } }).start(); } }
如今,若把它改成用 lambda 表达式,则为,
public class LambdaTest { @Test public void test(){ // 一行搞定 new Thread(()->System.out.println("线程运行...")).start(); } }
能够发现,明显用 lambda 表达式,写法更简洁了。
其实,Lambda 表达式就是函数式编程的体现。(什么,你还不知道什么是函数式编程? 那还不赶快百度去。)
注意事项:
咱们知道,在 Java 的接口中,只能定义方法名,不能实现方法体的,具体的实现须要子类去作。
可是,到了 JDK8 就不同了。在接口中,也能够经过 default
关键字来实现方法体。
那么,就有小伙伴疑惑了。好端端的,为何要加入这个奇怪的功能呢,它有什么用?
固然是为了提升代码的重用性了。此外,接口的默认方法能够在不影响原来的继承体系的状况下,进行功能的拓展,实现接口的向下兼容。
我滴天,好抽象。那,就用实例来讲明一下吧。
假设各类动物的继承体系以下,
public interface Animal { //全部动物都须要吃东西,具体吃什么,让子类去实现 void eat(); } public class Bird implements Animal { @Override public void eat() { System.out.println("早起的鸟儿有虫吃!"); } } public class Cat implements Animal { @Override public void eat() { System.out.println("小猫爱吃鱼!"); } }
如今,须要对 Animal接口拓展功能了。动物不能只会吃东西吧,它也许会奔跑,也许会飞行。那么,我在接口中添加两个方法, run 和 fly 就能够了吧。
这样定义方法虽然是能够的,可是,问题就来了。接口中定义了方法,实现类就要实现它的全部方法。小猫会奔跑,可是不会飞啊。而小鸟会飞,你让它在地上跑不是委屈人家嘛。
因此,这个设计不是太合理。
此时,就能够在接口中定义默认方法。子类不须要实现全部方法,能够按需实现,或者直接使用接口的默认方法。
所以,修改 Animal 接口以下,把 run 和 fly 定义为默认方法,
public interface Animal { //全部动物都须要吃东西,具体吃什么,让子类去实现 void eat(); default void run(){ System.out.println("我跑"); } default void fly(){ System.out.println("我飞"); } } public class Main { public static void main(String[] args) { Bird bird = new Bird(); bird.fly(); Cat cat = new Cat(); cat.run(); } }
在 JDK8 的集合中,就对 Collection 接口进行了拓展,如增长默认方法 stream() 等。既加强了集合的一些功能,并且也能向下兼容,不会对集合现有的继承体系产生影响。
另外,在接口中也能够定义静态方法。这样,就能够直接经过接口名调用静态方法。(这也很正常,接口原本就不能实例化)
须要注意的是,不能经过实现类的对象去调用接口的静态方法。
public interface MyStaticInterface { static void method(){ System.out.println("这是接口的静态方法"); } } public class MyStaticInterfaceImpl implements MyStaticInterface { public static void main(String[] args) { //直接经过接口名调用静态方法,不能经过实现类的对象调用 MyStaticInterface.method(); } }
若是一个接口中只有一个抽象方法,则称其为函数式接口。可使用 @FunctionalInterface
注解来检测一个接口是否为函数式接口。
JDK提供了常见的最简单的四种函数式接口:(必须掌握哦)
我这里举例了它们的使用方法,
public class LambdaTest { @Test public void test2(){ //打印传入的 msg printMsg((s)-> System.out.println(s),"听朋友说「烟雨星空」公众号不只文章好看,还免费送程序员福利,我心动了"); } public void printMsg(Consumer<String> consumer,String msg){ //消费型,只有传入参数,没有返回值 consumer.accept(msg); } @Test public void test3(){ //返回一个 0~99 的随机数 Integer content = getContent(() -> new Random().nextInt(100)); System.out.println(content); } public Integer getContent(Supplier<Integer> supplier){ //供给型,传入参数为空,带返回值 return supplier.get(); } @Test public void test4(){ //传入一个字符串,而后把它都转换成大写字母。 System.out.println(transfer((str) -> str.toUpperCase(), "My wechat : mistyskys")); } public String transfer(Function<String,String> func,String str){ // 函数型,传入一个参数,对其进行处理以后,返回一个结果 return func.apply(str); } @Test public void test5(){ //定义一个list,用来作筛选 ArrayList<String> list = new ArrayList<>(); list.add("zhangsan"); list.add("lisi"); list.add("jerry"); list.add("tom"); //筛选出集合中,字符串长度大于 3 的,并加入到结果集。 List<String> filterResult = filter((str) -> str.length() > 3, list); System.out.println(filterResult.toString()); } public List<String> filter(Predicate<String> predicate, List<String> list){ List<String> result = new ArrayList<>(); for (String str : list) { //断言型,传入一个参数,并返回true或者false。 //这里的逻辑是,若断言为真,则把当前的字符串加入到结果集中 if(predicate.test(str)){ result.add(str); } } return result; } }
还有一些其余函数式接口,都在java.util.function
包下,能够自行查看。使用方法都是同样的,再也不赘述。
除此以外,JDK 中还有不少函数式接口,例如 Comparator.java
。只要类上边看到了 @FunctionalInterface
这个注解,你均可以使用 lambda 表达式来简化写法。
概念:方法引用是用来直接访问类或者实例的已经存在的方法或者构造方法。
这里强调一下已经存在的含义。由于,lambda表达式本质上就是一个匿名函数。咱们知道,函数就是作逻辑处理的:拿一些数据,去作一些操做。
若是,咱们发现有其余地方(类或者对象)已经存在了相同的逻辑处理方案,那么就能够引用它的方案,而没必要重复写逻辑。这就是方法引用。
其实方法引用就是一个lambda表达式的另一种更简洁的表达方式。也能够说是语法糖。
只不过,这里要求 lambda 表达式须要符合必定的要求。首先,方法体只有一行代码。其次,方法的实现已经存在。此时,就能够用方法引用替换 lambda 表达式。
方法引用的操做符为双冒号::
。
下边就以最简单的一个咱们很是常见的打印语句为例。
//遍历数组里边的元素,并打印,用lambda表达式 String[] arr = new String[]{"zhangsan","lisi"}; Arrays.asList(arr).forEach((s)-> System.out.println(s));
能够发现,lambda 表达式只有一行代码,且方法体逻辑为打印字符串。而打印字符串的方案,在 System.out 对象中已经存在方法 println() 了。
因此,此处 lambda 表达式能够用方法引用替换。
// 注意:方法引用中的方法名不可带括号。 Arrays.asList(arr).forEach(System.out::println);
方法引用有如下四种形式:
下边举例说明:
public class ReferTest { public static void main(String[] args) { //函数式接口的抽象方法的参数列表和返回值类型,必须与方法引用对应的方法参数列表和返回值类型保持一致(状况3除外,比较特殊)。 //======= 1.对象::实例方法 ========= // lambda 表达式 Consumer consumer1 = (s) -> System.out.println(s); consumer1.accept("hello world"); //方法引用。Consumer的accept方法,和System.out的println方法结构同样, //都是传入一个参数,无返回值。故能够用方法引用。 Consumer consumer2 = System.out::println; consumer2.accept("hello java"); //======= 2.类::静态方法 ========= Integer[] arr = new Integer[]{12,20,15}; List<Integer> list = Arrays.asList(arr); // lambda 表达式 Comparator<Integer> com1 = (o1, o2) -> Integer.compare(o1, o2); Collections.sort(list,com1); //方法引用。Comparator的compare方法,和Integer的compare静态方法结构同样, //都是传入两个参数,返回一个int值,故能够用方法引用。 Comparator<Integer> com2 = Integer::compare; Collections.sort(list,com2); //======= 3.类::实例方法 ========= // lambda表达式 Comparator<Integer> com3 = (o1, o2) -> o1.compareTo(o2); //方法引用。这种形式比较特殊,(o1, o2) -> o1.compareTo(o2) , //当第一个参数o1为调用对象,且第二个参数o2为须要引用方法的参数时,才可用这种方式。 Comparator<Integer> com4 = Integer::compareTo; //======= 4.类::new ========= // lambda表达式 Supplier<String> supplier1 = () -> new String(); //方法引用。这个就比较简单了,就是类的构造器引用,通常用于建立对象。 Supplier<String> supplier2 = String::new; } }
题外话:方法引用,有时候不太好理解,让人感受莫名其妙。因此,若是不熟悉的话,用 lambda 表达式彻底没有问题。就是习惯的问题,多写就有感受了。
Optional 类是一个容器类。在以前咱们一般用 null 来表达一个值不存在,如今能够用 Optional 更好的表达值存在或者不存在。
这样的目的,主要就是为了防止出现空指针异常 NullPointerException 。
咱们知道,像层级关系比较深的对象,中间的调用过程很容易出现空指针,以下代码。
User user = new User(); //中间过程,user对象或者address对象都有可能为空,从而产生空指针异常 String details = user.getAddress().getDetails();
其中,对象的关系以下,
// 地址信息类 public class Address { private String province; //省 private String city; //市 private String county; //县 private String details; //详细地址 public String getProvince() { return province; } public void setProvince(String province) { this.province = province; } public String getCity() { return city; } public void setCity(String city) { this.city = city; } public String getCounty() { return county; } public void setCounty(String county) { this.county = county; } public String getDetails() { return details; } public void setDetails(String details) { this.details = details; } } //用户类 public class User { private String name; private Address address; public String getName() { return name; } public void setName(String name) { this.name = name; } public Address getAddress() { return address; } public void setAddress(Address address) { this.address = address; } }
在 Optional 类出现以前,为了防止空指针异常,能够这样作。(每一层都添加判空处理)
private static String getUserAddr(User user){ if(user != null){ Address address = user.getAddress(); if(address != null){ return address.getDetails(); }else { return "地址信息未填写"; } }else { return "地址信息未填写"; } }
能够发现,代码冗长,还不利于维护,随着层级关系更深,将会变成灾难(是否依稀记得js的回调地狱)。
那么,有了 Optional 类,咱们就能够写出更优雅的代码,而且防止空指针异常。(后边就填坑)
下面,就一块儿领略一下 Optional 的魅力吧!
实际上,Optional 是对原值(对象)的一层包装,咱们看下 Optional 的源码就知道了。
它把真正须要操做的对象 T 封装成 value 属性。构造器私有化,并提供三种静态的建立 Optional 对象的方法。
public final class Optional<T> { //EMPTY 表明一个值为空的 Optional 对象 private static final Optional<?> EMPTY = new Optional<>(); //用 value 来表明包装的实际值 private final T value; //值为null的构造函数 private Optional() { this.value = null; } //要求值不为null的构造函数,不然抛出空指针异常,见requireNonNull方法 private Optional(T value) { this.value = Objects.requireNonNull(value); } /** 此为Objects类的requireNonNull方法 public static <T> T requireNonNull(T obj) { if (obj == null) throw new NullPointerException(); return obj; } */ // 1. 建立一个值为空的 Optional 对象 public static<T> Optional<T> empty() { @SuppressWarnings("unchecked") Optional<T> t = (Optional<T>) EMPTY; return t; } // 2. 建立一个值不为空的 Optional 对象 public static <T> Optional<T> of(T value) { return new Optional<>(value); } // 3. 建立一个值可为空的 Optional 对象 // 若是值 value 为空,则同1,若不为空,则同2 public static <T> Optional<T> ofNullable(T value) { return value == null ? empty() : of(value); } }
所以,当咱们十分肯定传入的user对象不为空时,能够用 Optional.of(user)方法。若不肯定,则用 Optional.ofNullable(user),这样在后续的操做中能够避免空指针异常(后续map说明)。
一、get方法
public T get() { //若是值为null,则抛出异常,不然返回非空值value if (value == null) { throw new NoSuchElementException("No value present"); } return value; }
二、isPresent方法
//判断值是否存在,若值不为空,则认为存在 public boolean isPresent() { return value != null; }
看到这,不知道有没有小伙伴和我当初有同样的疑惑。既然有判空方法 isPresent,还有获取对象的 get 方法。那开头的那个坑,是否是就能够改写为以下,
//注意此时user类型为Optional<User> private static String getUserAddr(Optional<User> user){ //若是user存在,则取address对象 if(user.isPresent()){ Address address = user.get().getAddress(); //把address包装成Optional对象 Optional<Address> addressOptional = Optional.ofNullable(address); //若是address存在,则取details地址信息 if(addressOptional.isPresent()){ return addressOptional.get().getDetails(); }else { return "地址信息未填写"; } }else{ return "地址信息未填写"; } }
这样看起来,好像功能也实现了。可是,咱们先不说代码并无简洁(反而更复杂了),实际上是陷入了一个怪圈了。
由于,if(user.isPresent()){}
和手动判空处理 if(user!=null){}
实质上是没有区别的。这就是受以前一直以来的代码思惟限制了。
因此,咱们不要手动调用 isPresent 方法 。
不要奇怪,isPresent 方法,实际上是为了 Optional 中的其余方法服务的(如map方法),本意并非为了让咱们手动调用。你会在后续多个方法中,见到 isPresent 的身影。
三、ifPresent
//传入一个消费型接口,当值存在时,才消费。 public void ifPresent(Consumer<? super T> consumer) { if (value != null) consumer.accept(value); }
与 isPresent 方法不一样, ifPresent 方法是咱们推荐使用的。
如能够这样判空,
Optional<User> user = Optional.ofNullable(new User()); user.ifPresent(System.out::println); //不要用下边这种 if (user.isPresent()) { System.out.println(user.get()); }
四、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(); }
这两个方法都是当值不存在时,用于返回一个默认值。如user对象为null时,返回默认值。
@Test public void test1(){ User user = null; System.out.println("orElse调用"); User user1 = Optional.ofNullable(user).orElse(createUser()); System.out.println("orElseGet调用"); User user2 = Optional.ofNullable(user).orElseGet(() -> createUser()); } private User createUser() { //此处打印,是为了查看orElse和orElseGet的区别 System.out.println("createUser..."); return new User(); } //打印结果 orElse调用 createUser... orElseGet调用 createUser...
以上是user为null时,两个方法是没有区别的。由于都须要建立user对象做为默认值返回。
可是,当user对象不为null时,咱们看下对比结果,
@Test public void test2(){ User user = new User(); System.out.println("orElse调用"); User user1 = Optional.ofNullable(user).orElse(createUser()); System.out.println("orElseGet调用"); User user2 = Optional.ofNullable(user).orElseGet(() -> createUser()); } //打印结果 orElse调用 createUser... orElseGet调用
能够发现,当user对象不为null时,orElse依然会建立User对象,而orElseGet不会建立。
因此,当 orElse() 方法传入的参数须要建立对象或者比较耗时的操做时,建议用 orElseGet()
五、orElseThrow
当值为null,能够返回自定义异常。
User user = null; Optional.ofNullable(user).orElseThrow(IllegalAccessError::new);
若user对象为null,则抛出非法访问。
这样,能够有针对的对特定异常作一些其余处理。由于,会抛出哪些异常的状况,是咱们可控的。
六、map
public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); //看到没,map内部会先调用isPresent方法来作判空处理。 //因此咱们不要本身去调用isPresent方法 if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } }
map相似 Stream 的 map方法。处理完以后,返回的仍是一个 Optional 对象,因此能够作链式调用。
User user = new User(); String name = Optional.of(user).map(User::getName) .orElse("佚名"); System.out.println(name);
如上,取出user对象的name值,若name为空,返回一个默认值“佚名”(神奇的名字)。
这里,直接调用map方法,就不须要对user对象进行预先判空了。由于在map方法里边,会调用isPresent方法帮咱们处理user为null的状况。
到这里,脑壳转圈快的小伙伴,是否是对开头的坑已经有启发了。
没错,咱们能够经过 Optional 的链式调用,经过 map,orElse 等操做改写。以下,
private static String getUserAddr1(Optional<User> user){ //先获取address对象 return user.map((u)->u.getAddress()) //再获取details值, .map(e -> e.getDetails()) //若detail为null,则返回一个默认值 .orElse("地址信息未填写"); }
中间全部可能出现空指针的状况,Optional都会规避。由于 value!=null
这个操做已经被封装了。并且在不一样的处理阶段,Optional 会自动帮咱们包装不一样类型的值。
就像上边的操做,第一个map方法包装了User类型的user对象值,第二个map包装了String类型的details值,orElse 返回最终须要的字符串。
七、flatMap
public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Objects.requireNonNull(mapper.apply(value)); } }
乍看这个方法和 map 没什么区别。其实,它们的区别就在于传入的 mapper参数的第二个泛型。
map第二个泛型为? extends U
,flatMap第二个泛型为Optional<U>
。
因此,map方法在最后,用方法Optional.ofNullable
包装成了 Optional 。可是,flatMap就须要咱们本身去包装 Optional 了。
下边就看下怎么操做 flatMap。
@Test public void test3(){ User user = new User(); String name = Optional.of(user).flatMap((u) -> this.getUserName(u)) .orElse("佚名"); System.out.println(name); } //把用户名包装成Optional<String>,做为 Function 接口的返回值,以适配flatMap private Optional<String> getUserName(User user){ return Optional.ofNullable(user.getName()); }
八、filter
public Optional<T> filter(Predicate<? super T> predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this; else return predicate.test(value) ? this : empty(); }
见名知意,filter 是用来根据条件过滤的,若是符合条件,就返回当前 Optional 对象自己,不然返回一个值为 null的 Optional 对象。
以下,过滤姓名为空的 user。
User user = new User(); //因为user没有设置 name,因此返回一个值为 null 的 optionalUser Optional<User> optionalUser = Optional.of(user).filter((u) -> this.getUserName(u).isPresent()); //因为值为 null,因此get方法抛出异常 NoSuchElementException optionalUser.get();
首先,什么是 Stream 流?
流 (Stream) 和 Java 中的集合相似。可是集合中保存的数据,而流中保存的是,对集合或者数组中数据的操做。
之因此叫流,是由于它就像一个流水线同样。从原料通过 n 道加工程序以后,变成可用的成品。
若是,你有了解过 Spark 里边的 Streaming,就会有一种特别熟悉的感受。由于它们的思想和用法如此类似。
包括 lazy 思想,都是在须要计算结果的时候,才真正执行。 相似 Spark Streaming 对 RDD 的操做,分为转换(transformation)和行动(action)。转换只是记录这些操做逻辑,只有行动的时候才会开始计算。
转换介绍:http://spark.apache.org/docs/...
对应的,Stream API 对数据的操做,有中间操做和终止操做,只有在终止操做的时候才会执行计算。
因此,Stream 有以下特色,
那么问题来了,既然 Stream 是用来操做数据的。没有数据源,你怎么操做,所以还要有一个数据源。
因而,stream操做数据的三大步骤为:数据源,中间操做,终止操做。
流的源能够是一个数组,一个集合,一个生成器方法等等。
一、使用 Collection 接口中的 default 方法。
default Stream<E> stream() //返回一个顺序流 default Stream<E> parallelStream() //返回一个并行流
此处,咱们也就明白了,为何 JDK8 要引入默认方法了吧。
因为 Collection 集合父接口定义了这些默认方法,因此像 List,Set 这些子接口下的实现类均可以用这种方式生成一个 Stream 流。
public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); list.add("zhangzan"); list.add("lisi"); list.add("wangwu"); //顺序流 Stream<String> stream = list.stream(); //并行流 Stream<String> parallelStream = list.parallelStream(); //遍历元素 stream.forEach(System.out::println); } }
二、 Arrays 的静态方法 stream()
static <T> Stream<T> stream(T[] array)
能够传入各类类型的数组,把它转化为流。以下,传入一个字符串数组。
String[] arr = {"abc","aa","ef"}; Stream<String> stream1 = Arrays.stream(arr);
三、Stream接口的 of() ,generate(),iterate()方法
注意,of() 方法返回的是有限流,即元素个数是有限的,就是你传入的元素个数。
而 generate(),iterate() 这两个方法,是无限流,即元素个数是无限个。
使用方法以下,
//of Stream<Integer> stream2 = Stream.of(10, 20, 30, 40, 50); stream.forEach(System.out::println); //generate,每一个元素都是0~99的随机数 Stream<Integer> generate = Stream.generate(() -> new Random().nextInt(100)); //iterate,从0开始迭代,每一个元素依次增长2 Stream<Integer> iterate = Stream.iterate(0, x -> x + 2);
四、IntStream,LongStream,DoubleStream 的 of、range、rangeClosed 方法
它们的用法都是同样,不过是直接包装了一层。
实际,of()方法底层用的也是 Arrays.stream()方法。
以 IntStream 类为例,其余相似,
IntStream intStream = IntStream.of(10, 20, 30); //从0每次递增1,到10,包括0,但不包括10 IntStream rangeStream = IntStream.range(0, 10); //从0每次递增1,到10,包括0和10 IntStream rangeClosed = IntStream.rangeClosed(0, 10);
一个流能够有零个或者多个中间操做,每个中间操做都会返回一个新的流,供下一个操做使用。
一、筛选与切片
常见的包括:
用法以下:
@Test public void test1(){ ArrayList<Employee> list = new ArrayList<>(); list.add(new Employee("张三",3000)); list.add(new Employee("李四",5000)); list.add(new Employee("王五",4000)); list.add(new Employee("赵六",4500)); list.add(new Employee("赵六",4500)); // filter,过滤出工资大于4000的员工 list.stream() .filter((e) -> e.getSalary() > 4000) .forEach(System.out::println); System.out.println("==============="); // limit,限定指定个数的元素 list.stream() .limit(3) .forEach(System.out::println); System.out.println("==============="); // skip,和 limit 正好相反,跳过前面指定个数的元素 list.stream() .skip(3) .forEach(System.out::println); System.out.println("==============="); // distinct,去重元素。注意自定义对象须要重写 equals 和 hashCode方法 list.stream() .distinct() .forEach(System.out::println); } // 打印结果: Employee{name='李四', salary=5000} Employee{name='赵六', salary=4500} Employee{name='赵六', salary=4500} =============== Employee{name='张三', salary=3000} Employee{name='李四', salary=5000} Employee{name='王五', salary=4000} =============== Employee{name='赵六', salary=4500} Employee{name='赵六', salary=4500} =============== Employee{name='张三', salary=3000} Employee{name='李四', salary=5000} Employee{name='王五', salary=4000} Employee{name='赵六', salary=4500}
二、映射
主要是map,包括:
用法以下:
@Test public void test2(){ int[] arr = {10,20,30,40,50}; // map,映射。每一个元素都乘以2 Arrays.stream(arr) .map(e -> e * 2) .forEach(System.out::println); System.out.println("==============="); //mapToInt,mapToDouble,mapToLong 用法都同样,不一样的是返回类型分别是 //IntStream,DoubleStream,LongStream. Arrays.stream(arr) .mapToDouble(e -> e * 2 ) .forEach(System.out::println); System.out.println("==============="); Arrays.stream(arr) .flatMap(e -> IntStream.of(e * 2)) .forEach(System.out::println); } //打印结果: 20 40 60 80 100 =============== 20.0 40.0 60.0 80.0 100.0 =============== 20 40 60 80 100
这里须要说明一下 map 和 flatMap。上边的例子看不出来它们的区别。由于测试数据比较简单,都是一维的。
其实,flatMap 能够把二维的集合映射成一维的。看起来,就像把二维集合压平似的。( flat 的英文意思就是压平)
如今给出这样的数据,若想返回全部水果单词的全部字母("appleorangebanana"),应该怎么作?
String[] fruits = {"apple","orange","banana"};
先遍历 fruits 数组拿到每一个单词;而后,对每一个单词切分,切分后仍是一个数组 。
注意,此时的数组是一个二维数组,形如 [["a","p","p","l","e"] , [],[]]。
因此须要进一步遍历,再遍历(遍历两次),以下
String[] fruits = {"apple","orange","banana"}; Stream.of(fruits).map((s) -> Stream.of(s.split(""))) .forEach(e -> e.forEach(System.out::print));
虽然也实现了需求,可是整个流程太复杂了,单 forEach 遍历就两次。
用 flatMap 能够简化这个过程,以下。其实,就是把中间的二维数组直接压平成一维的单个元素,减小遍历次数。
Stream.of(fruits).map(s -> s.split("")) .flatMap(e -> Stream.of(e)) .forEach(System.out::print);
还有一种写法,不用 map,直接 flatMap。
Stream.of(fruits).flatMap(s -> Stream.of(s.split(""))) .collect(Collectors.toList()) .forEach(System.out::print);
三、排序
排序有两个方法,一个是无参的,默认按照天然顺序。一个是带参的,能够指定比较器。
@Test public void test4(){ String[] arr = {"abc","aa","ef"}; //默认升序(字典升序) Stream.of(arr).sorted().forEach(System.out::println); System.out.println("====="); //自定义排序,字典降序 Stream.of(arr).sorted((s1,s2) -> s2.compareTo(s1)).forEach(System.out::println); }
一个流只会有一个终止操做。 Stream只有遇到终止操做,它的源才开始执行遍历操做。注意,在这以后,这个流就不能再使用了。
一、查找与匹配
因为上边 API 过于简单,再也不作例子。
二、规约
规约就是 reduce ,把数据集合到一块儿。相信你确定据说过 hadoop 的 map-reduce ,思想是同样的。
这个方法着重说一下,比较经常使用,有三个重载方法。
2.一、一个参数
Optional<T> reduce(BinaryOperator<T> accumulator);
传入的是一个二元运算符,返回一个 Optional 对象。
咱们须要看下 BinaryOperator 这个函数式接口的结构,否则后边就不懂了,也不知道怎么用。
//BinaryOperator继承自 BiFunction<T,T,T>,咱们发现它们的泛型类型都是T,彻底相同 public interface BinaryOperator<T> extends BiFunction<T,T,T> { } public interface BiFunction<T, U, R> { //传入 T 和 U,返回类型 R ,这就说明它们的参数类型能够彻底不相同,固然也能够彻底相同 //对应的它的子类 BinaryOperator 就是彻底相同的 R apply(T t, U u); }
使用方式以下,
Integer[] arr = {1,2,3,4,5,6}; Integer res1 = Stream.of(arr).reduce((x, y) -> x + y).get(); System.out.println(res1); // 结果:21
它表达的意思是,反复合并计算。如上,就是先计算1和2的和,而后计算结果3再和下一个元素3求和,依次反复计算,直到最后一个元素。
2.二、两个参数
T reduce(T identity, BinaryOperator<T> accumulator);
传入两个参数,第一个参数表明初始值,第二个参数是二元运算符。返回的类型是 T ,而不是 Optional。
以下,给一个 10 的初始值,依次累加,
Integer res2 = Stream.of(arr).reduce(10, (x, y) -> x + y); System.out.println(res2); // 结果:31
注意:accumulator 累加器函数须要知足结合律。如上,加法就知足结合律。
它的计算过程示意图能够用下图表示,
identity 先和 T1 作计算,返回值做为中间结果,参与下一次和 T2 计算,如此反复。
另外须要注意的时,源码中说明了一句,并不强制要求必定按顺序计算。
but is not constrained to execute sequentially.
也就是说,实际计算时有可能会和图中表示的计算顺序不太同样。好比 T1 先和 T3 运算,而后结果再和 T2 运算。
这也是为何它要求函数符合结合律,由于交换元素顺序不能影响到最终的计算结果。
2.三、三个参数
<U> U reduce(U identity, BiFunction<U, ? super T, U> accumulator, BinaryOperator<U> combiner);
这个参数有三个,比较复杂。咱们分析一下。
所以,咱们能够把 reduce 分为非并行和并行两种状况。
2.3.一、 非并行规约
非并行状况下,第三个参数不起做用,identity 表明的是初始值。
如下的计算,是初始化一个 list,并向其中添加流中的元素。
Integer[] arr = {1,2,3,4,5,6}; ArrayList<Integer> res = Stream.of(arr).reduce(Lists.newArrayList(0), (l, e) -> { l.add(e); return l; }, (l, c) -> { //结果不会打印这句话,说明第三个参数没有起做用 System.out.println("combiner"); l.addAll(c); return l; }); System.out.println(res); // [0, 1, 2, 3, 4, 5, 6]
2.3.二、并行规约
并行规约,用的是 fork-join 框架思想,分而治之。把一个大任务分红若干个子任务,而后再合并。
不了解 fork-join 的,能够看这篇文章介绍:fork-join框架分析
因此,这里的累加器 accumulator 是用来计算每一个子任务的。组合器 combiner 是用来把若干个子任务合并计算的。
下边用例子说明:
Integer res4 = Stream.of(1,2,3,4).parallel().reduce(1, (s,e) -> s + e, (sum, s) -> sum + s); System.out.println(res4); // 结果:14
奇了怪了,计算结果应该是 10 的,为何是 14 呢。
这里就要说明,这个 identity 初始值了。它是在每次执行 combiner 的时候,都会把 identity 累加上。
具体执行几回 combiner ,能够经过如下方式计算出来 。( c 并不能表明有几个执行子任务)
AtomicInteger c = new AtomicInteger(0); Integer res4 = Stream.of(1,2,3,4).parallel().reduce(1, (s,e) -> s + e, (sum, s) -> {c.getAndIncrement(); return sum + s;}); System.out.println(c); //3 System.out.println(res4); //14
c 为 3 表明执行了 3 次 combiner ,最后计算总结果时,还会再加一次初始值,因此结果为:
(1+2+3+4) + (3+1) * 1 = 14 // 1+2+3+4 为正常非并行结算的和,3+1 为总共计算了几回初始值。
咱们能够经过加大stream的数据量来验证猜测。从1 加到 100 。初始值为 2 。
AtomicInteger count = new AtomicInteger(0); int length = 100; Integer[] arr1 = new Integer[length]; for (int i = 0; i < length; i++) { arr1[i] = i + 1; } Integer res5 = Stream.of(arr1).parallel().reduce(2, (s,e) -> s + e, (sum, s) -> {count.getAndIncrement(); return sum + s;}); System.out.println(count.get()); //15 System.out.println(res5); //5082
即:
(1+...+100) + (15+1) * 2 = 5082
怎么正常使用?
那么,问题就来了。这个并行计算不靠谱啊,都把计算结果计算错了。
这是为何呢,是它的算法有问题么?
非也,实际上是咱们的用法姿式错了。能够看下源码中对 identity 的说明。
This means that for all u, combiner(identity, u) is equal to u.
意思是,须要每次 combiner 运算时,identity 的值保证 u == combiner(identity,u) 是一个恒等式。
那么,为了知足这个要求,此种状况只能让 identity = 0 。
故,改写程序以下,
//其余都不变,只有 identity 由 2 改成 0 AtomicInteger count = new AtomicInteger(0); int length = 100; Integer[] arr1 = new Integer[length]; for (int i = 0; i < length; i++) { arr1[i] = i + 1; } Integer res5 = Stream.of(arr1).parallel().reduce(0, (s,e) -> s + e, (sum, s) -> {count.getAndIncrement(); return sum + s;}); System.out.println(count.get()); //15 System.out.println(res5); //5050
固然,只要保证 identity 不影响这个恒等式就行。
好比,对于 set 集合会自动去重,这种状况下,也可使用并行计算,
//初始化一个set,而后把stream流的元素添加到set中, //须要注意:用并行的方式,这个set集合必须是线程安全的。不然会报错ConcurrentModificationException Set<Integer> res3 = Stream.of(1, 2, 3, 4).parallel().reduce(Collections.synchronizedSet(Sets.newHashSet(10), (l, e) -> { l.add(e); return l; }, (l, c) -> { l.addAll(c); return l; }); System.out.println(res3);
三、收集
收集操做,能够把流收集到 List,Set,Map等中。并且,Collectors 类中提供了不少静态方法,方便的建立收集器供咱们使用。
这里举几个经常使用的便可。具体的 API 能够去看 Collectors 源码(基本涵盖了各类,最大值,最小值,计数,分组等功能。)。
@Test public void test6() { ArrayList<Employee> list = new ArrayList<>(); list.add(new Employee("张三", 3000)); list.add(new Employee("李四", 5000)); list.add(new Employee("王五", 4000)); list.add(new Employee("赵六", 4500)); //把全部员工的姓名收集到list中 list.stream() .map(Employee::getName) .collect(Collectors.toList()) .forEach(System.out::println); //求出全部员工的薪资平均值 Double average = list.stream() .collect(Collectors.averagingDouble(Employee::getSalary)); System.out.println(average); }
JDK8 以前的时间 API 存在线程安全问题,而且设计混乱。所以,在 JDK8 就从新设计了一套 API。
以下,线程不安全的例子。
@Test public void test1() throws Exception{ SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); ExecutorService executorService = Executors.newFixedThreadPool(10); List<Future<Date>> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { Future<Date> future = executorService.submit(() -> sdf.parse("20200905")); list.add(future); } for (Future<Date> future : list) { System.out.println(future.get()); } }
屡次运行,就会报错 java.lang.NumberFormatException 。
接下来,咱们就学习下新的时间 API ,而后改写上边的程序。
它们都是不可变类,用法差很少。以 LocalDate 为例。
一、建立时间对象
LocalDate localDate1 = LocalDate.now(); System.out.println(localDate1); //2020-09-05 LocalDate localDate2 = LocalDate.of(2020, 9, 5); System.out.println(localDate2); //2020-09-05 LocalDate localDate3 = LocalDate.parse("2020-09-05"); System.out.println(localDate3); //2020-09-05
二、获取年月日周
LocalDate currentDate = LocalDate.now(); System.out.println(currentDate.getYear()); //2020 System.out.println(currentDate.getMonth()); // SEPTEMBER System.out.println(currentDate.getMonthValue()); //9 System.out.println(currentDate.getDayOfYear()); //249 System.out.println(currentDate.getDayOfMonth()); //5 System.out.println(currentDate.getDayOfWeek()); // SATURDAY
三、日期比较,先后或者相等
它们都返回的是布尔值。
LocalDate date1 = LocalDate.of(2020, 9, 5); LocalDate date2 = LocalDate.of(2020, 9, 6); System.out.println(date1.isBefore(date2)); //true System.out.println(date1.isAfter(date2)); //false System.out.println(date1.equals(date2)); //false System.out.println(date1.isLeapYear()); //true
四、日期加减
减法同理,
LocalDate nowDate = LocalDate.now(); System.out.println(nowDate); //2020-09-05 System.out.println(nowDate.plusDays(1)); //2020-09-06 System.out.println(nowDate.plusWeeks(1)); //2020-09-12 System.out.println(nowDate.plusMonths(1)); //2020-10-05 System.out.println(nowDate.plusYears(1)); //2021-09-05
Instant 表明的是到从 UTC 时区 1970年1月1日0时0分0秒开始计算的时间戳。
Instant now = Instant.now(); System.out.println(now.toString()); // 2020-09-05T14:11:07.074Z System.out.println(now.toEpochMilli()); // 毫秒数, 1599315067074
用于表示时间段 ,能够表示 LocalDateTime 和 Instant 之间的时间段,用 between 建立。
LocalDateTime today = LocalDateTime.now(); //今天的日期时间 LocalDateTime tomorrow = today.plusDays(1); //明天 Duration duration = Duration.between(today, tomorrow); //第二个参数减去第一个参数的时间差 System.out.println(duration.toDays()); //总天数,1 System.out.println(duration.toHours()); //小时,24 System.out.println(duration.toMinutes()); //分钟,1440 System.out.println(duration.getSeconds()); //秒,86400 System.out.println(duration.toMillis()); //毫秒,86400000 System.out.println(duration.toNanos()); // 纳秒,86400000000000
和时间段 Duration,可是 Period 只能精确到年月日。
有两种方式建立 Duration 。
LocalDate today = LocalDate.now(); //今天 LocalDate date = LocalDate.of(2020,10,1); //国庆节 //1. 用 between 建立 Period 对象 Period period = Period.between(today, date); System.out.println(period); // P26D //2. 用 of 建立 Period 对象 Period of = Period.of(2020, 9, 6); System.out.println(of); // P2020Y9M6D // 距离国庆节还有 0 年 0 月 26 天 System.out.printf("距离国庆节还有 %d 年 %d 月 %d 天" , period.getYears(),period.getMonths(),period.getDays());
ZoneId 表示不一样的时区。
Set<String> availableZoneIds = ZoneId.getAvailableZoneIds(); availableZoneIds.forEach(System.out::println); //打印全部时区 ZoneId of = ZoneId.of("Asia/Shanghai"); //获取亚洲上海的时区对象 System.out.println(of); System.out.println(ZoneId.systemDefault()); //当前时区为: Asia/Shanghai
JDK1.8 提供了线程安全的日期格式化类 DateTimeFormatter。
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 1. 日期时间转化为字符串。有两种方式 String format = dtf.format(LocalDateTime.now()); System.out.println(format); // 2020-09-05 23:02:02 String format1 = LocalDateTime.now().format(dtf); //实际上调用的也是 DateTimeFormatter 类的format方法 System.out.println(format1); // 2020-09-05 23:02:02 // 2. 字符串转化为日期。有两种方式,须要注意,月和日位数要补全两位 //第一种方式用的是,DateTimeFormatter.ISO_LOCAL_DATE_TIME ,格式以下 LocalDateTime parse = LocalDateTime.parse("2020-09-05T00:00:00"); System.out.println(parse); // 2020-09-05T00:00 //第二种方式能够自定义格式 LocalDateTime parse1 = LocalDateTime.parse("2020-09-05 00:00:00", dtf); System.out.println(parse1); // 2020-09-05T00:00
接下来,就能够把上边线程不安全的类改写为新的时间 API 。
@Test public void test8() throws Exception{ // SimpleDateFormat 改成 DateTimeFormatter DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd"); ExecutorService executorService = Executors.newFixedThreadPool(10); // Date 改成 LocalDate List<Future<LocalDate>> list = new ArrayList<>(); for (int i = 0; i < 10; i++) { //日期解析改成 LocalDate.parse("20200905",dtf) Future<LocalDate> future = executorService.submit(() -> LocalDate.parse("20200905",dtf)); list.add(future); } for (Future<LocalDate> future : list) { System.out.println(future.get()); } }
PS:若是本文对你有用,就请关注我,给我点赞吧。你的支持是我写做最大的动力 ~
首发于:JDK8新特性最全总结