Lambda表达式 介绍Lambda表示 最初我们学习线程时, 是以下面方法创建并开启线程的
1 2 3 4 5 6 7 8 9 10 11 12 13 class MyThread implements Runnable { @Override public void run () { System.out.println("hello world" ); } } @Test public void test () { MyThread myThread = new MyThread (); Thread thread = new Thread (myThread); thread.start(); }
后来我们学会了匿名内部类, 代码精简了很多, 但还是很冗余
1 2 3 4 5 6 7 8 9 @Test public void test1 () { new Thread (new Runnable () { @Override public void run () { System.out.println("hello world" ); } }).start(); }
现在我们用标准格式的Lambda表达式来实现
1 2 3 4 5 6 @Test public void test3 () { new Thread (() -> { System.out.println("hello, world" ); }).start(); }
对上面的例子可以分析出几点内容:
Thread 类需要 Runnable 接口作为参数,其中的抽象 run 方法是用来指定线程任务内容的核心
为了指定 run 的方法体,不得不需要 Runnable 接口的实现类
为了省去定义一个 Runnable 实现类的麻烦,不得不使用匿名内部类
必须覆盖重写抽象 run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错
而实际上,似乎只有方法体才是关键所在。
Lambda表达式简化了匿名内部类的使用,语法更加简单。
Lambda的标准格式 Lambda省去面向对象的条条框框,Lambda的标准格式格式由3个部分组成
1 2 3 (参数类型 参数名称) -> { 代码体; }
格式说明:
(参数类型 参数名称) :参数列表
{代码体;} :方法体
-> :箭头,分隔参数列表和方法体
Lambda与匿名内部类的对比 从匿名内部类改成Lambda表达式可以总结为
去掉new和接口名().
去掉方法名, 只剩下 (参数类型 参数1, 参数类型 参数2)
最后加个 -> { 方法体 }
一开始不熟悉可以先把匿名表达式写出来, 再改成Lambda表达式.
无参数无返回值 匿名内部类
1 2 3 4 5 6 7 8 9 10 @Test public void test4 () { Runnable r1 = new Runnable () { @Override public void run () { System.out.println("我是匿名内部类" ); } }; r1.run(); }
Lambda
1 2 3 4 5 6 7 @Test public void test5 () { Runnable r2 = () -> { System.out.println("我是Lambda" ); }; r2.run(); }
有参数有返回值 例一 我们先新建一个Person类, 有name, age, height三个属性, 再创建一个有参构造器, 最后在重写toString方法.
匿名内部类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test5 () { ArrayList<Person> persons = new ArrayList <>(); persons.add(new Person ("刘德华" , 58 , 174 )); persons.add(new Person ("张学友" , 58 , 176 )); persons.add(new Person ("刘德华" , 54 , 171 )); persons.add(new Person ("黎明" , 53 , 178 )); Collections.sort(persons, new Comparator <Person>() { @Override public int compare (Person o1, Person o2) { return o1.getAge() - o2.getAge(); } }); for (Person person : persons) { System.out.println(person); } }
Lambda表达式
1 2 3 Collections.sort(persons, (Person o1, Person o2) -> { return o2.getAge() - o1.getAge(); });
例二 匿名内部类
1 2 3 4 5 6 7 8 9 10 @Test public void test6 () { List<Integer> list = Arrays.asList(11 , 22 , 33 , 44 ); list.forEach(new Consumer <Integer>() { @Override public void accept (Integer integer) { System.out.println(integer); } }); }
Lambda表达式
1 2 3 4 List<Integer> list = Arrays.asList(11 , 22 , 33 , 44 ); list.forEach((Integer integer) -> { System.out.println(integer); });
Lambda的省略格式 在Lambda标准格式的基础上,使用省略写法的规则为:
小括号内参数的类型可以省略
如果小括号内有且仅有一个参数,则小括号可以省略
如果大括号内有且仅有一个语句,可以同时省略大括号、return关键字及语句分号
标准格式
1 2 3 (int a) -> { return new Person (); }
省略格式
无参数无返回值 无参数, ()不能省略, 可以省略{}
1 2 3 4 5 6 7 8 9 Runnable r2 = () -> { System.out.println("我是Lambda标准格式" ); }; r2.run(); Runnable r3 = () ->System.out.println("我是Lambda省略格式" );r3.run();
一个参数一条语句 一个参数, ()可以省略, 只有方法体只有一条语句, 可以省略{}
1 2 3 4 5 6 7 List<Integer> list = Arrays.asList(11 , 22 , 33 , 44 ); list.forEach((Integer integer) -> { System.out.println(integer); }); list.forEach(integer -> System.out.println(integer));
多个参数一条return 多个参数, ()不能省, 方法体只有一条 return 语句, 可以省略 {}和return
1 2 3 4 5 6 Collections.sort(persons, (Person o1, Person o2) -> { return o2.getAge() - o1.getAge(); }); Collections.sort(persons, (o1, o2) -> o1.getAge() - o2.getAge());
Lambda的前提条件 Lambda的语法非常简洁,但是Lambda表达式不是随便使用的,使用时有几个条件要特别注意:
方法的参数或局部变量类型必须为接口 才能使用Lambda
接口中有且仅有 一个 抽象方法
我们可以先新建一个接口Swimming, 里面目前只有一个方法.
1 2 3 public interface Swimming { void goSwimming () ; }
接着我们定义一个参数列表是Swimming接口的函数test7
1 2 3 public static void test7 (Swimming swimming) { swimming.goSwimming(); }
然后在main函数中调用test7
1 2 3 public static void main (String[] args) { test7(() -> System.out.println("我去游泳了" )); }
运行结果如下
此时我们在Swimming接口中加入一个抽象方法run
1 2 3 4 public interface Swimming { void goSwimming () ; void goRunning () ; }
main函数马上爆红
函数式接口 函数式接口在Java中是指:有且仅有一个抽象方法的接口。
函数式接口,即适用于函数式编程场景的接口。而Java中的函数式编程体现就是Lambda,所以函数式接口就是可以适用于Lambda使用的接口。只有确保接口中有且仅有一个抽象方法,Java中的Lambda才能顺利地进行推导。
FunctionalInterface注解 与 @Override 注解的作用类似,Java 8中专门为函数式接口引入了一个新的注解: @FunctionalInterface 。
该注解可用于一个接口的定义上:
1 2 3 4 @FunctionalInterface public interface Operator { void myMethod () ; }
一旦使用该注解来定义接口,编译器将会强制检查该接口是否确实有且仅有一个抽象方法,否则将会报错。不过,即使不使用该注解,只要满足函数式接口的定义,这仍然是一个函数式接口,使用起来都一样
了解Lambda的实现原理 我们现在已经会使用Lambda表达式了。那么Lambda到底是如何实现的,现在我们就来探究Lambda表达式的底层实现原理。
准备工作 定义一个函数式接口
1 2 3 4 @FunctionalInterface interface Swimmable { public abstract void swimming () ; }
定义函数, 参数是接口
1 2 3 4 5 public class Demo04LambdaImpl { public static void goSwimming (Swimmable swimmable) { swimmable.swimming(); } }
反编译匿名内部类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public class Demo04LambdaImpl { public static void main (String[] args) { goSwimming(new Swimmable () { @Override public void swimming () { System.out.println("使用匿名内部类实现游泳" ); } }); } public static void goSwimming (Swimmable swimmable) { swimmable.swimming(); } }
我们可以看到匿名内部类会在编译后产生一个类: Demo04LambdaImpl$1.class
使用XJad反编译这个类,得到如下代码
1 2 3 4 5 6 7 8 9 10 11 12 package com.itheima.demo01lambda;import java.io.PrintStream;static class Demo04LambdaImpl$1 implements Swimmable { public void swimming () { System.out.println("使用匿名内部类实现游泳" ); } Demo04LambdaImpl$1 () { } }
反编译Lambda
Lambda和匿名内部类对比
所需的类型不一样
匿名内部类,需要的类型可以是类,抽象类,接口
Lambda表达式,需要的类型必须是接口
抽象方法的数量不一样
匿名内部类所需的接口中抽象方法的数量随意
Lambda表达式所需的接口只能有一个抽象方法
实现原理不同
匿名内部类是在编译后会形成class
Lambda表达式是在程序运行的时候动态生成class
JDK 8接口新增的两个方法 JDK 8接口增强介绍 JDK 8以前的接口
1 2 3 4 interface 接口名 { 静态常量; 抽象方法; }
JDK 8对接口的增强,接口还可以有默认方法 和静态方法
JDK 8的接口
1 2 3 4 5 6 interface 接口名 { 静态常量; 抽象方法; 默认方法; 静态方法; }
接口默认方法 接口引入默认方法的背景 在JDK 8以前接口中只能有抽象方法。存在以下问题:
如果给接口新增抽象方法,所有实现类都必须重写这个抽象方法。不利于接口的扩展.
例如,JDK 8 时在Map接口中增加了forEach方法, 如果所有的实现类都需要去实现这个方法,那么工程量时巨大的。
因此,在JDK 8时为接口新增了默认方法,效果如下
1 2 3 4 5 6 public interface Map <K, V> { ... default void forEach (BiConsumer<? super K, ? super V> action) { ... } }
接口中的默认方法实现类不必重写,可以直接使用,实现类也可以根据需要重写。这样就方便接口的扩展。
接口默认方法的定义格式 1 2 3 4 5 interface 接口名 { 修饰符 default 返回值类型 方法名() { 代码; } }
接口默认方法的使用 定义一个接口, 其中test1是抽象方法, test2是默认方法, test1实现类必须实现, test2可以重写也可以直接使用
1 2 3 4 5 6 interface AA { public abstract void test1 () ; public default void test02 () { System.out.println("AA test02" ); } }
定义两个实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class BB implements AA { @Override public void test1 () { System.out.println("BB test1" ); } } class CC implements AA { @Override public void test1 () { System.out.println("CC test1" ); } @Override public void test02 () { System.out.println("CC实现类重写接口默认方法" ); } }
调用两个实现类
1 2 3 4 5 6 7 BB b = new BB ();b.test02(); CC c = new CC ();c.test02();
接口静态方法 为了方便接口扩展,JDK 8为接口新增了静态方法。静态方法不能重写!!!
接口静态方法的定义格式 1 2 3 4 5 interface 接口名 { 修饰符 static 返回值类型 方法名() { 代码; } }
xxxxxxxxxx /** * 关掉tcp连接 */xnet_err_t xtcp_close(xtcp_t *tcp) { tcp_free(tcp); return XNET_ERR_OK;}c 直接使用接口名调用即可:接口名.静态方法名()
不能重写!!!!
定义一个有静态方法的接口
1 2 3 4 5 interface AAA { public static void test01 () { System.out.println("AAA 接口的静态方法" ); } }
调用
1 2 3 4 public static void main (String[] args) { AAA.test01(); }
接口默认方法和静态方法的区别
默认方法通过实例调用,静态方法通过接口名调用。
默认方法可以被继承,实现类可以直接使用接口默认方法,也可以重写接口默认方法。
静态方法不能被继承,实现类不能重写接口静态方法,只能使用接口名调用
如何选择呢?如果这个方法需要被实现类继承或重写,使用默认方法,如果接口中的方法不需要被继承就使用静态方
法
常用内置函数式接口 内置函数式接口来由来 我们知道使用Lambda表达式的前提是需要有函数式接口。而Lambda使用时不关心接口名,抽象方法名,只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda方便,JDK提供了大量常用的函数式接口。
常用内置函数式接口介绍 它们主要在 java.util.function 包中。下面是最常用的几个接口。
Supplier接口 Supplier意味着”供给” , 对应的Lambda表达式需要“对外提供 ”一个符合泛型类型的对象数据。
供给型接口,通过Supplier接口中的get方法可以得到一个值,无参有返回的接口。
1 2 3 4 @FunctionalInterface public interface Supplier <T> { public abstract T get () ; }
使用Lambda表达式返回数组元素最大值
使用 Supplier 接口作为方法参数类型,通过Lambda表达式求出 int 数组中的最大值,接口的泛型使用java.lang.Integer类。
1 2 3 4 5 6 7 8 9 10 11 12 13 public void printMax (Supplier<Integer> supplier) { Integer integer = supplier.get(); System.out.println(integer); } @Test public void test8 () { printMax(() -> { int [] arr = {5 , 7 , 4 , 10 }; Arrays.sort(arr); return arr[arr.length - 1 ]; }); }
最后输出:10.
分析下过程:
printMax方法需要传入一个Supplier<Integer>类型的参数, 并且调用了接口中的get方法得到了一个参数并将其打印出来.
test8方法调用了printMax方法, 在括号中用Lambda表达式重写了接口的get方法, 重写的结果是返回一个数组中的最大值.
相当于integer通过重写的方法得到了数组里的最大值.
接下来执行打印语句, 将最大值打印出来.
Consumer接口 1 2 3 4 @FunctionalInterface public interface Consumer <T> { public abstract void accept (T t) ; }
Consumer接口是消费 一个数据,其数据类型由泛型参数决定。
消费型接口,通过接口中的accept方法可以消费一个值,有参无返回的接口。
使用Lambda表达式将一个字符串转成大写和小写的字符串
Consumer消费型接口,可以拿到accept方法参数传递过来的数据进行处理, 有参无返回的接口(所以是accept有参数,)。基本使用如
1 2 3 4 5 6 7 public void conversion (Consumer<String> consumer) { consumer.accept("HELLO WORLD" ); } @Test public void test9 () { conversion(s -> System.out.println(s.toLowerCase())); }
默认方法:andThen 如果一个方法的参数和返回值全都是 Consumer 类型,那么就可以实现效果:消费一个数据的时候,首先做一个操作,然后再做一个操作,实现组合。而这个方法就是 Consumer 接口中的default方法 andThen 。下面是JDK的源代码:
1 2 3 4 default Consumer<T> andThen (Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; }
备注: java.util.Objects 的 requireNonNull 静态方法将会在参数为null时主动抛出NullPointerException 异常。这省去了重复编写if语句和抛出空指针异常的麻烦。
要想实现组合,需要两个或多个Lambda表达式即可,而 andThen 的语义正是“一步接一步”操作。例如两个步骤组合的情况
1 2 3 4 5 6 7 8 9 10 11 12 public void conversion (Consumer<String> c1, Consumer<String> c2) { String s = "HELLO WORLD" ; c1.andThen(c2).accept(s); } @Test public void test9 () { conversion(s -> System.out.println(s.toLowerCase()), s -> System.out.println(s.toUpperCase()) ); }
运行结果将会首先打印完全大写的HELLO,然后打印完全小写的hello。当然,通过链式写法可以实现更多步骤的组合。
Function接口 Function接口用来根据一个T类型的数据得到一个R类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值.
1 2 3 4 @FunctionalInterface public interface Function <T, R> { public abstract R apply (T t) ; }
使用Lambda表达式将字符串转成数字
将 String 类型转换为 Integer 类型
1 2 3 4 5 6 7 8 public void stringToInt (Function<String, Integer> function) { Integer apply = function.apply("1234" ); System.out.println(apply + "+" + apply.getClass()); } @Test public void Test10 () { stringToInt(s -> Integer.parseInt(s)); }
输出结果是: 1234+class java.lang.Integer
默认方法:andThen Function 接口中有一个默认的 andThen 方法,用来进行组合操作。JDK源代码如下
1 2 3 4 default <V> Function<V, R> compose (Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); }
该方法同样用于“先做什么,再做什么”的场景,和 Consumer 中的 andThen 差不多
例如:将字符串解析成数字, 将上一步得到的数字乘以10
1 2 3 4 5 6 7 8 9 public void handler (Function<String, Integer> fun1, Function<Integer, Integer> fun2) { Integer result = fun1.andThen(fun2).apply("66" ); System.out.println(result); } @Test public void Test11 () { handler(s -> Integer.parseInt(s), s -> 10 * s); }
输出: 660
Predicate接口 有时候我们需要对某种类型的数据进行判断,从而得到一个boolean值结果。这时可以使用java.util.function.Predicate<T> 接口。
1 2 3 4 @FunctionalInterface public interface Predicate <T> { public abstract boolean test (T t) ; }
使用Lambda判断一个人名如果超过3个字就认为是很长的名字
1 2 3 4 5 6 7 8 9 10 public void judge (Predicate<String> predicate, String name) { boolean result = predicate.test(name); if (result) { System.out.println("这是一个很长的名字" ); } } @Test public void test12 () { judge(s -> s.length() > 3 , "迪丽冷巴" ); }
默认方法:and 既然是条件判断,就会存在与、或、非三种常见的逻辑关系。其中将两个 Predicate 条件使用“与”逻辑连接起来实
现“并且 ”的效果时,可以使用default方法 and 。其JDK源码为
1 2 3 4 default Predicate<T> and (Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) && other.test(t); }
使用Lambda表达式判断一个字符串中即包含W也包含H
1 2 3 4 5 6 7 8 9 10 public void wh (Predicate<String> p1, Predicate<String> p2) { boolean result = p1.and(p2).test("HelloWorld" ); if (result) { System.out.println("该字符串包含W和H" ); } } @Test public void test13 () { wh(s -> s.contains("W" ), s -> s.contains("H" )); }
默认方法:or 与 and 的“与”类似,默认方法 or 实现逻辑关系中的“或 ”。JDK源码为
1 2 3 4 default Predicate<T> or (Predicate<? super T> other) { Objects.requireNonNull(other); return (t) -> test(t) || other.test(t); }
使用Lambda表达式判断一个字符串中包含W或者包含H
代码只需要将and修改为or即可,其他都不变
1 2 3 4 5 6 7 8 9 10 public void wh (Predicate<String> p1, Predicate<String> p2) { boolean result = p1.or(p2).test("HelloWorld" ); if (result) { System.out.println("该字符串包含W或H" ); } } @Test public void test13 () { wh(s -> s.contains("W" ), s -> s.contains("H" )); }
默认方法:negate negate方法是“非”(取反), JDK源代码为
1 2 3 default Predicate<T> negate () { return (t) -> !test(t); }
使用Lambda表达式判断一个字符串中不包含X
1 2 3 4 5 6 7 8 9 10 public void judge (Predicate<String> predicate) { boolean result = predicate.negate().test("HelloWorld" ); if (result) { System.out.println("字符串里不包含X" ); } } @Test public void test12 () { judge(s -> s.contains("X" )); }
方法引用 方法引用简化Lambda 如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引用”过去就好了
下面举例了使用Lambda表达式和方法引用来对一个数组求和.
lambda表达式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Demo11MethodRefIntro { public static void getMax (int [] arr) { int sum = 0 ; for (int n : arr) { sum += n; } System.out.println(sum); } private static void printMax (Consumer<int []> consumer, int [] arr) { consumer.accept(arr); } public static void main (String[] args) { int [] arr = {10 , 20 , 30 , 40 , 50 }; printMax(a -> getMax(a), arr); } }
如果我们在Lambda中所指定的功能,已经有其他方法存在相同方案,那是否还有必要再写重复逻辑?可以直接“引用”过去就好了
方法引用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public class Demo11MethodRefIntro { public static void getMax (int [] arr) { int sum = 0 ; for (int n : arr) { sum += n; } System.out.println(sum); } private static void printMax (Consumer<int []> consumer, int [] arr) { consumer.accept(arr); } public static void main (String[] args) { int [] arr = {10 , 20 , 30 , 40 , 50 }; printMax(Demo11MethodRefIntro::getMax, arr); } }
请注意其中的双冒号::写法,这被称为“方法引用 ”,是一种新的语法.
方法引用的格式 符号表示 : ::
符号说明 : 双冒号为方法引用运算符,而它所在的表达式被称为方法引用 。
应用场景 : 如果Lambda所要实现的方案 , 已经有其他方法存在相同方案,那么则可以使用方法引用。
常见引用方式 方法引用在JDK 8中使用方式相当灵活,有以下几种形式:
instanceName::methodName 对象::方法名
ClassName::staticMethodName 类名::静态方法
ClassName::methodName 类名::普通方法
ClassName::new 类名::new 调用的构造器
TypeName[]::new String[]::new 调用数组的构造器
对象名::引用成员方法 这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法,则可以通过对象名引用成员方法
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test01 () { Date now = new Date (); Supplier<Long> supp = () -> { return now.getTime(); }; System.out.println(supp.get()); Supplier<Long> supp2 = now::getTime; System.out.println(supp2.get()); }
方法引用的注意事项
被引用的方法,参数要和接口中抽象方法的参数一样
当接口抽象方法有返回值时,被引用的方法也必须有返回值
类名::引用静态方法 由于在 java.lang.System 类中已经存在了静态方法currentTimeMillis,所以当我们需要通过Lambda来调用该
方法时,可以使用方法引用 .
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test02 () { Supplier<Long> supp = () -> { return System.currentTimeMillis(); }; System.out.println(supp.get()); Supplier<Long> supp2 = System::currentTimeMillis; System.out.println(supp2.get()); }
类名::引用实例方法 Java面向对象中,类名只能调用静态方法,类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void test03 () { Function<String, Integer> f1 = (s) -> { return s.length(); }; System.out.println(f1.apply("abc" )); Function<String, Integer> f2 = String::length; System.out.println(f2.apply("abc" )); BiFunction<String, Integer, String> bif = String::substring; String hello = bif.apply("hello" , 2 ); System.out.println("hello -> " + hello); }
类名::new引用构造器 由于构造器的名称与类名完全一样。所以构造器引用使用 类名称::new 的格式表示。
首先定义一个简单得Person类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public class Person { private String name; private int age; private int height; public Person () { } public Person (String name, int age) { this .name = name; this .age = age; } public Person (String name, int age, int height) { this .name = name; this .age = age; this .height = height; } public String getName () { return name; } public void setName (String name) { this .name = name; } public int getAge () { return age; } public void setAge (int age) { this .age = age; } public int getHeight () { return height; } public void setHeight (int height) { this .height = height; } @Override public String toString () { return "Person{" + "name='" + name + '\'' + ", age=" + age + ", height=" + height + '}' ; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test04 () { Supplier<Person> sup = () -> { return new Person (); }; System.out.println(sup.get()); Supplier<Person> sup2 = Person::new ; System.out.println(sup2.get()); BiFunction<String, Integer, Person> fun2 = Person::new ; System.out.println(fun2.apply("张三" , 18 )); }
数组::new引用数组构造器 数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test05 () { Function<Integer, String[]> fun = (len) -> { return new String [len]; }; String[] arr1 = fun.apply(10 ); System.out.println(arr1 + ", " + arr1.length); Function<Integer, String[]> fun2 = String[]::new ; String[] arr2 = fun.apply(5 ); System.out.println(arr2 + ", " + arr2.length); }
总结 方法引用是对Lambda表达式符合特定情况下的一种缩写,它使得我们的Lambda表达式更加的精简,也可以理解为
Lambda表达式的缩写形式 , 不过要注意的是方法引用只能”引用”已经存在的方法!
Stream流 Stream流式思想概述 注意:Stream和IO流(InputStream/OutputStream)没有任何关系,请暂时忘记对传统IO流的固有印象!
Stream流式思想类似于工厂车间的“生产流水线 ”,Stream流不是一种数据结构 ,不保存数据,而是对数据进行加工处理 。Stream可以看作是流水线上的一个工序。在流水线上,通过多个工序让一个原材料加工成一个商品。
Stream API能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归约。
获取Stream流的两种方式 java.util.stream.Stream 是JDK 8新加入的流接口。
获取一个流非常简单,有以下几种常用的方式:
所有的 Collection 集合都可以通过 stream 默认方法获取流;
Stream 接口的静态方法 of 可以获取数组对应的流。
根据Collection获取流 首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流。
1 2 3 public interface Collection { default Stream<E> stream () ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 import java.util.*;import java.util.stream.Stream;public class Demo04GetStream { public static void main (String[] args) { List<String> list = new ArrayList <>(); Stream<String> stream1 = list.stream(); Set<String> set = new HashSet <>(); Stream<String> stream2 = set.stream(); Vector<String> vector = new Vector <>(); Stream<String> stream3 = vector.stream(); } }
java.util.Map 接口不是 Collection 的子接口,所以获取对应的流需要分key、value或entry等情况:
1 2 3 4 5 6 7 8 9 10 11 12 13 import java.util.HashMap;import java.util.Map;import java.util.stream.Stream;public class Demo05GetStream { public static void main (String[] args) { Map<String, String> map = new HashMap <>(); Stream<String> keyStream = map.keySet().stream(); Stream<String> valueStream = map.values().stream(); Stream<Map.Entry<String, String>> entryStream = map.entrySet().stream(); } }
Stream中的静态方法of获取流 由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法 of ,使用很简单:
!!!注意:基本数据类型的数组不行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import java.util.stream.Stream;public class Demo06GetStream { public static void main (String[] args) { Stream<String> stream6 = Stream.of("aa" , "bb" , "cc" ); String[] arr = {"aa" , "bb" , "cc" }; Stream<String> stream7 = Stream.of(arr); Integer[] arr2 = {11 , 22 , 33 }; Stream<Integer> stream8 = Stream.of(arr2); int [] arr3 = {11 , 22 , 33 }; Stream<int []> stream9 = Stream.of(arr3); } }
备注: of 方法的参数其实是一个可变参数,所以支持数组。
Stream常用API Stream流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:
终结方法 :返回值类型不再是 Stream 类型的方法,不再支持链式调用。本小节中,终结方法包括 count 和
forEach 方法。
非终结方法 :返回值类型仍然是 Stream 类型的方法,支持链式调用。(除了终结方法外,其余方法均为非终结
方法。)
备注:本小节之外的更多方法,请自行参考API文档。
Stream注意事项(重要)
Stream只能操作一次
Stream方法返回的是新的流
Stream不调用终结方法,中间的操作不会执行
Stream流的forEach方法 forEach 用来遍历流中的数据
1 void forEach (Consumer<? super T> action) ;
该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。例如:
1 2 3 4 5 6 7 8 9 10 11 @Test public void testForEach () { List<String> one = new ArrayList <>(); Collections.addAll(one, "迪丽热巴" , "宋远桥" , "苏星河" , "老子" , "庄子" , "孙子" ); one.stream().forEach(System.out::println); }
Stream流的count方法 Stream流提供 count 方法来统计其中的元素个数
该方法返回一个long值代表元素个数。基本使用:
1 2 3 4 5 6 @Test public void testCount () { List<String> one = new ArrayList <>(); Collections.addAll(one, "迪丽热巴" , "宋远桥" , "苏星河" , "老子" , "庄子" , "孙子" ); System.out.println(one.stream().count()); }
Stream流的filter方法 filter用于过滤数据,返回符合过滤条件的数据, 可以通过 filter 方法将一个流转换成另一个子集流。方法声明:
1 Stream<T> filter (Predicate<? super T> predicate) ;
该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。
Stream流中的filter方法基本使用的代码如:
1 2 3 4 5 6 @Test public void testFilter () { List<String> one = new ArrayList <>(); Collections.addAll(one, "迪丽热巴" , "宋远桥" , "苏星河" , "老子" , "庄子" , "孙子" ); one.stream().filter(s -> s.length() == 2 ).forEach(System.out::println); }
在这里通过Lambda表达式来指定了筛选的条件:姓名长度为2个字。
Stream流的limit方法 limit 方法可以对流进行截取,只取用前n个。方法:
1 Stream<T> limit (long maxSize) ;
参数是一个long型,如果集合当前长度大于参数则进行截取。否则不进行操作。基本使用:
1 2 3 4 5 6 @Test public void testLimit () {List<String> one = new ArrayList <>(); Collections.addAll(one, "迪丽热巴" , "宋远桥" , "苏星河" , "老子" , "庄子" , "孙子" ); one.stream().limit(3 ).forEach(System.out::println); }
Stream流的skip方法 如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:
如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。基本使用:
1 2 3 4 5 6 @Test public void testSkip () {List<String> one = new ArrayList <>(); Collections.addAll(one, "迪丽热巴" , "宋远桥" , "苏星河" , "老子" , "庄子" , "孙子" ); one.stream().skip(2 ).forEach(System.out::println); }
Stream流的map方法 如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:
1 <R> Stream<R> map (Function<? super T, ? extends R> mapper) ;
该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。
Stream流中的 map 方法基本使用的代码如:
1 2 3 4 5 6 @Test public void testMap () { Stream<String> original = Stream.of("11" , "22" , "33" ); Stream<Integer> result = original.map(Integer::parseInt); result.forEach(s -> System.out.println(s.getClass())); }
这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。
Stream流的sorted方法 如果需要将数据排序,可以使用 sorted 方法。方法签名:
1 2 Stream<T> sorted () ; Stream<T> sorted (Comparator<? super T> comparator) ;
Stream流中的 sorted 方法基本使用的代码如:
1 2 3 4 5 6 7 8 9 @Test public void testSorted () { Stream.of(33 , 22 , 11 , 55 ) .sorted() .sorted((o1, o2) -> o2 - o1) .forEach(System.out::println); }
这段代码中, sorted 方法根据元素的自然顺序排序,也可以指定比较器排序。
Stream流的distinct方法 如果需要去除重复数据(保留第一次出现的元素, 去除后面重复出现的元素),可以使用 distinct 方法。方法签名:
Stream流中的 distinct 方法基本使用的代码如:
1 2 3 4 5 6 @Test public void testDistinct () { Stream.of(22 , 33 , 22 , 11 , 33 ) .distinct() .forEach(System.out::println); }
如果是自定义类型如何是否也能去除重复的数据呢?
1 2 3 4 5 6 7 8 9 10 @Test public void testDistinct2 () { Stream.of( new Person ("刘德华" , 58 ), new Person ("张学友" , 56 ), new Person ("张学友" , 56 ), new Person ("黎明" , 52 )) .distinct() .forEach(System.out::println); }
结论: 自定义类型是根据对象的hashCode和equals来去除重复元素的。
Stream流的match方法 如果需要判断数据是否匹配指定的条件,可以使用 Match 相关方法。方法签名:
1 2 3 boolean allMatch (Predicate<? super T> predicate) ; boolean anyMatch (Predicate<? super T> predicate) ; boolean noneMatch (Predicate<? super T> predicate) ;
Stream流中的 Match 相关方法基本使用的代码如:
1 2 3 4 5 6 7 8 @Test public void testMatch () { boolean b = Stream.of(5 , 3 , 6 , 1 ) .noneMatch(e -> e < 0 ); System.out.println("b = " + b); }
Stream流的find方法 如果需要找到某些数据,可以使用 find 相关方法。方法签名:
1 2 Optional<T> findFirst () ; Optional<T> findAny () ;
这两个方法,绝大部分情况下,是完全相同的,但是在多线程的环境下,findAny和findFirst返回的结果 可能不一样。
Stream流中的 find 相关方法基本使用的代码如:
1 2 3 4 5 6 7 @Test public void testFind () { Optional<Integer> first = Stream.of(5 , 3 , 6 , 1 ).findFirst(); System.out.println("first = " + first.get()); Optional<Integer> any = Stream.of(5 , 3 , 6 , 1 ).findAny(); System.out.println("any = " + any.get()); }
Stream流的max和min方法 如果需要获取最大和最小值,可以使用 max 和 min 方法。方法签名:
1 2 Optional<T> max (Comparator<? super T> comparator) ; Optional<T> min (Comparator<? super T> comparator) ;
Stream流中的 max 和 min 相关方法基本使用的代码如:
1 2 3 4 5 6 7 @Test public void testMax_Min () { Optional<Integer> max = Stream.of(5 , 3 , 6 , 1 ).max((o1, o2) -> o1 - o2); System.out.println("first = " + max.get()); Optional<Integer> min = Stream.of(5 , 3 , 6 , 1 ).min((o1, o2) -> o1 - o2); System.out.println("any = " + min.get()); }
Stream流的reduce方法 Reduce 原意:减少,缩小.
根据指定的计算模型将Stream中的值计算得到一个最终结果, reduce 操作可以实现从Stream中生成一个值,其生成的值不是随意的,而是根据指定的计算模型。比如,之前提到count、min和max方法,因为常用而被纳入标准库中。事实上,这些方法都是reduce操作。
所以如果需要将所有数据归纳得到一个数据,可以使用 reduce 方法。方法签名:
1 T reduce (T identity, BinaryOperator<T> accumulator) ;
Stream流中的 reduce 相关方法基本使用的代码如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 @Test public void testReduce () { int reduce = Stream.of(4 , 5 , 3 , 9 ) .reduce(0 , (a, b) -> { System.out.println("a = " + a + ", b = " + b); return a + b; }); System.out.println("reduce = " + reduce); int reduce2 = Stream.of(4 , 5 , 3 , 9 ) .reduce(0 , (x, y) -> Integer.sum(x, y)); int reduce3 = Stream.of(4 , 5 , 3 , 9 ).reduce(0 , Integer::sum); System.out.println(reduce2 + "+" + reduce3); int max = Stream.of(4 , 5 , 3 , 9 ) .reduce(0 , (x, y) -> x > y ? x : y); System.out.println("max = " + max); }
Stream流的map和reduce组合使用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Test public void testMapReduce () { int reduceAge = Stream.of( new Person ("刘德华" , 58 ), new Person ("张学友" , 56 ), new Person ("郭富城" , 54 ), new Person ("黎明" , 52 )) .map(Person::getAge) .reduce(0 , (x, y) -> x > y ? x : y); System.out.println(reduceAge); int count = Stream.of(1 , 2 , 2 , 1 , 3 , 2 ) .map(i -> { if (i == 2 ) { return 1 ; } else { return 0 ; } }) .reduce(0 , Integer::sum); System.out.println("count = " + count); }
Stream流的mapToInt 如果需要将Stream中的Integer类型数据转成int类型,可以使用 mapToInt 方法。方法签名:
1 IntStream mapToInt (ToIntFunction<? super T> mapper) ;
Stream流中的 mapToInt 相关方法基本使用的代码如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void test1 () { Stream<Integer> stream = Arrays.stream(new Integer []{1 , 2 , 3 , 4 , 5 }); IntStream intStream = stream.mapToInt(Integer::intValue); int reduce = intStream .filter(i -> i > 3 ) .reduce(0 , Integer::sum); System.out.println(reduce); IntStream intStream1 = IntStream.rangeClosed(1 , 10 ); Stream<Integer> boxed = intStream1.boxed(); boxed.forEach(s -> System.out.println(s.getClass() + ", " + s)); }
Stream流的concat方法 如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :
1 static <T> Stream<T> concat (Stream<? extends T> a, Stream<? extends T> b)
备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的。
该方法的基本使用代码如:
1 2 3 4 5 6 7 @Test public void testContact () { Stream<String> streamA = Stream.of("张三" ); Stream<String> streamB = Stream.of("李四" ); Stream<String> result = Stream.concat(streamA, streamB); result.forEach(System.out::println); }
Stream综合案例 现在有两个 ArrayList 集合存储队伍当中的多个成员姓名,要求使用Stream流依次 进行以下若干操作步骤:
第一个队伍只要名字为3个字的成员姓名;
第一个队伍筛选之后只要前3个人;
第二个队伍只要姓张的成员姓名;
第二个队伍筛选之后不要前2个人;
将两个队伍合并为一个队伍;
根据姓名创建 Person 对象;
打印整个队伍的Person对象信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test () { List<String> one = Arrays.asList("迪丽热巴" , "宋远桥" , "苏星河" , "老子" , "庄子" , "孙子" , "洪七公" ); List<String> two = Arrays.asList("古力娜扎" , "张无忌" , "张三丰" , "赵丽颖" , "张二狗" , "张天爱" ,"张三" ); Stream<String> oneStream = one.stream().filter(s -> s.length() == 3 ).limit(3 ); Stream<String> twoStream = two.stream().filter(s -> s.startsWith("张" )).skip(2 ); concat(oneStream, twoStream).map(Person::new ).forEach(System.out::println); }
收集Stream流中的结果 对流操作完成之后,如果需要将流的结果保存到数组或集合中,可以收集流中的数据.
Stream流中的结果到集合中 Stream流提供 collect 方法,其参数需要一个 java.util.stream.Collector<T,A, R> 接口对象来指定收集到哪种集合中。java.util.stream.Collectors 类提供一些方法,可以作为 Collector接口的实例:
public static <T> Collector<T, ?, List<T>> toList() :转换为 List 集合。
public static <T> Collector<T, ?, Set<T>> toSet() :转换为 Set 集合。
下面是这两个方法的基本使用代码:
1 2 3 4 5 6 Stream<String> stream = Stream.of("aa" , "bb" , "cc" ); List<String> list = stream.collect(Collectors.toList()); Set<String> set = stream.collect(Collectors.toSet()); ArrayList<String> arrayList = stream.collect(Collectors.toCollection(ArrayList::new )); HashSet<String> hashSet = stream.collect(Collectors.toCollection(HashSet::new ));
!!!!注意: 上面的代码不能一起运行, 一起运行会报错, 因为stream一旦被收集到集合中后就不再是流了
Stream流中的结果到数组中 Stream提供 toArray 方法来将结果放到一个数组中,返回值类型是Object[]的:
其使用场景如:
1 2 3 4 5 6 7 8 9 10 11 12 @Test public void testStreamToArray () { Stream<String> stream = Stream.of("aa" , "bb" , "cc" ); String[] strings = stream.toArray(String[]::new ); for (String str : strings) { System.out.println(str); } }
对流中数据进行聚合计算 Collectors.maxBy/Collectors.minBy/Collectors.counting/Collectors.summingInt/Collectors.averagingInt
当我们使用Stream流处理数据后,可以像数据库的聚合函数一样对某个字段进行操作。比如获取最大值,获取最小值,求总和,平均值,统计数量。
定义一个Person流
1 2 3 4 5 Stream<Person> studentStream = Stream.of( new Person ("赵丽颖" , 58 , 95 ), new Person ("杨颖" , 56 , 88 ), new Person ("迪丽热巴" , 56 , 99 ), new Person ("柳岩" , 52 , 77 ));
求年龄最大/最小的一个Person
1 2 3 4 5 6 7 8 Optional<Person> collect = studentStream.collect(Collectors.maxBy((o1, o2) -> o1.getAge() - o2.getAge())); System.out.println(collect.get());
求年龄总和
1 2 3 int sumAge = studentStream.collect(Collectors.summingInt(s -> s.getAge()));System.out.println("sumAge = " + sumAge);
求平均年龄
1 2 3 double avgAge = studentStream.collect(Collectors.averagingInt(s -> s.getAge()));System.out.println("avgAge = " + avgAge);
求总人数
1 2 3 Long count = studentStream.collect(Collectors.counting());System.out.println("count = " + count);
对流中数据进行分组 Collectors.groupingBy
当我们使用Stream流处理数据后,可以根据某个属性将数据分组:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void testGroup () { Stream<Person> studentStream = Stream.of( new Person ("赵丽颖" , 52 , 95 ), new Person ("杨颖" , 56 , 88 ), new Person ("迪丽热巴" , 56 , 55 ), new Person ("柳岩" , 52 , 33 )); Map<String, List<Person>> map = studentStream .collect(Collectors.groupingBy(s -> s.getAge() > 55 ? "成年" : "未成年" )); map.forEach((k, v) -> { System.out.println(k + ": " + v); });
运行结果:
1 2 未成年: [Person{name='赵丽颖' , age=52 , height=95 }, Person{name='柳岩' , age=52 , height=33 }] 成年: [Person{name='杨颖' , age=56 , height=88 }, Person{name='迪丽热巴' , age=56 , height=55 }]
对流中数据进行多级分组 Collectors.groupingBy
Stream流也可以多级分组 , 只要返回的结果有多少个就会分成多少个以结果为key的Map集合.
1 2 3 4 5 6 7 8 9 10 11 12 13 Map<String, List<Person>> map = studentStream .collect(Collectors.groupingBy(s -> { if (s.getAge() > 50 && s.getAge() <53 ) { return "50-53" ; } else if (s.getAge() > 53 && s.getAge() <55 ) { return "54-55" ; } else { return "56-60" ; } })); map.forEach((k, v) -> { System.out.println(k + ": " + v); });
运行结果:
1 2 3 50 -53 : [Person{name='赵丽颖' , age=52 , height=95 }]56 -60 : [Person{name='杨颖' , age=56 , height=88 }, Person{name='迪丽热巴' , age=56 , height=55 }]54 -55 : [Person{name='柳岩' , age=54 , height=33 }]
对流中数据进行分区 Collectors.partitioningBy 会根据值是否为true,把集合分割为两个列表,一个true列表,一个false列表。
1 2 3 4 5 Map<Boolean, List<Person>> map = studentStream.collect(Collectors.partitioningBy(s -> s.getAge() > 55 )); map.forEach((k, v) -> { System.out.println(k + " == " + v); });
运行结果
1 2 false == [Person{name='赵丽颖' , age=52 , height=95 }, Person{name='柳岩' , age=54 , height=33 }]true == [Person{name='杨颖' , age=56 , height=88 }, Person{name='迪丽热巴' , age=56 , height=55 }]
对流中数据进行拼接 Collectors.joining 会根据指定的连接符,将所有元素连接成一个字符串。
1 2 3 4 5 String collect = studentStream .map(Person::getName) .collect(Collectors.joining(">_<" , "^_^" , "^v^" )); System.out.println(collect);
输出结果
1 ^_^赵丽颖>_<杨颖>_<迪丽热巴>_<柳岩^v^
并行的Stream流 串行的Stream流 目前我们使用的Stream流是串行的,就是在一个线程上执行。
1 2 3 4 @Test public void test0Serial () { Stream.of(4 , 5 , 3 , 9 , 1 , 2 , 6 ).forEach(s -> System.out.println(Thread.currentThread() + ", s = " + s)); }
运行结果
1 2 3 4 5 6 7 Thread[main,5 ,main], s = 4 Thread[main,5 ,main], s = 5 Thread[main,5 ,main], s = 3 Thread[main,5 ,main], s = 9 Thread[main,5 ,main], s = 1 Thread[main,5 ,main], s = 2 Thread[main,5 ,main], s = 6
并行的Stream流 parallelStream其实就是一个并行执行的流。它通过默认的ForkJoinPool,可能提高多线程任务的速度。
获取并行Stream流的两种方式 直接获取并行的流 1 2 3 ArrayList<Integer> list = new ArrayList <>(); Stream<Integer> stream = list.parallelStream();
将串行流转成并行流 1 2 3 ArrayList<Integer> list = new ArrayList <>(); Stream<Integer> stream = list.stream().parallel();
并行操作代码:
1 2 3 4 5 6 @Test public void test0Parallel () { Stream.of(4 , 5 , 3 , 9 , 1 , 2 , 6 ) .parallel() .forEach(s -> System.out.println(Thread.currentThread() + ", s = " + s)); }
输出结果
1 2 3 4 5 6 7 Thread[main,5 ,main], s = 1 Thread[main,5 ,main], s = 9 Thread[main,5 ,main], s = 4 Thread[ForkJoinPool.commonPool-worker-3 ,5 ,main], s = 2 Thread[ForkJoinPool.commonPool-worker-3 ,5 ,main], s = 3 Thread[ForkJoinPool.commonPool-worker-2 ,5 ,main], s = 6 Thread[ForkJoinPool.commonPool-worker-1 ,5 ,main], s = 5
并行和串行Stream流的效率对比 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class Demo06 { private static long times = 50000000000L ; private long start; @Before public void init () { start = System.currentTimeMillis(); } @After public void destory () { long end = System.currentTimeMillis(); System.out.println("消耗时间: " + (end - start)); } @Test public void parallelStream () { System.out.println("serialStream" ); LongStream.rangeClosed(0 , times) .parallel() .reduce(0 , Long::sum); } @Test public void serialStream () { System.out.println("serialStream" ); LongStream.rangeClosed(0 , times) .reduce(0 , Long::sum); } @Test public void forAdd () { System.out.println("forAdd" ); long result = 0L ; for (long i = 1L ; i < times; i++) { result += i; } } }
我们可以看到parallelStream的效率是最高的。
Stream并行处理的过程会分而治之,也就是将一个大任务切分成多个小任务,这表示每个任务都是一个操作。
parallelStream线程安全问题 我们运行下面代码
1 2 3 4 5 6 @Test public void parallelStreamNotice () { ArrayList<Integer> list = new ArrayList <>(1000 ); IntStream.rangeClosed(1 , 1000 ).parallel().forEach(list::add); System.out.println(list.size()); }
得到结果为: 913
我们明明是往集合中添加1000个元素,而实际上只有913个元素。
解决方法: 加锁、使用线程安全的集合或者调用Stream的 toArray() / collect() 操作就是满足线程安全的了。
Optional类 Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象。它的作用主要就是为了解决避免Null检查,防止NullPointerException。
Optional的基本使用 Optional类的创建方式 1 2 3 Optional.of(T t) : 创建一个 Optional 实例 Optional.empty() : 创建一个空的 Optional 实例 Optional.ofNullable(T t):若 t 不为 null ,创建 Optional 实例,否则创建空实例
1 2 3 4 5 6 7 Optional<String> userNameO = Optional.of("凤姐" ); Optional<String> userNameO = Optional.of(null ); Optional<String> userNameO = Optional.ofNullable(null ); Optional<String> userNameO = Optional.ofNullable("凤姐" ); Optional<String> userNameO = Optional.empty();
Optional类的常用方法 isPresent isPresent() : 判断是否包含值,包含值返回true,不包含值返回false
1 2 3 4 5 6 7 if (userNameO.isPresent()) { String userName = userNameO.get(); System.out.println("用户名为:" + userName); } else { System.out.println("用户名不存在" ); }
orElse orElse(T t) : 如果调用对象包含值,返回该值,否则返回参数t
1 2 3 4 5 6 7 @Test public void test03 () { Optional<String> userName = Optional.of("凤姐" ); String name = userName.orElse("如花" ); System.out.println(name); }
其他方法 1 2 3 get() : 如果Optional有值则将其返回,否则抛出NoSuchElementException orElseGet (Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值map(Function f): 如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()
Optional的高级使用 1 2 3 4 5 6 7 8 9 10 11 12 @Test public void test05 () { Person u = new Person ("hello world" , 18 ); Optional<Person> uO = Optional.of(u); System.out.println(getUpperCaseUserName(uO)); } public String getUpperCaseUserName (Optional<Person> uO) { return uO.map(u -> u.getName()) .map(name -> name.toUpperCase()) .orElse("null" ); }
运行结果: 如果Person的name为空的话, 打印null; Person的name有值的话, 会将小写转换成大写.
不再需要在转换前进行if(name != null)的判断了.
总结 Optional是一个可以为null的容器对象。orElse,ifPresent,ifPresentOrElse,map等方法避免对null的判断,写出更加优雅的代码。
新的日期和时间API 旧版日期时间 API 存在的问题
设计很差: 在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期。此外用于格式化和解析的类在java.text包中定义。
非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一。
时区处理麻烦:日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题。
新日期时间API介绍 JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time 包中,下面是一些关键类。
LocalDate(年月日) LocalDate :表示日期,包含年月日,格式为 2019-10-16
1 2 3 4 5 6 7 8 9 10 11 12 13 LocalDate fj = LocalDate.of(1985 , 9 , 23 );System.out.println("fj = " + fj); LocalDate nowDate = LocalDate.now();System.out.println("nowDate = " + nowDate); System.out.println("年: " + nowDate.getYear()); System.out.println("月: " + nowDate.getMonthValue()); System.out.println("日: " + nowDate.getDayOfMonth()); System.out.println("星期: " + nowDate.getDayOfWeek());
LocalTime(时分秒) LocalTime :表示时间,包含时分秒,格式为 16:38:54.158549300
1 2 3 4 5 6 7 8 9 10 11 12 13 LocalTime time = LocalTime.of(12 ,15 , 28 , 129_900_000 );System.out.println("time = " + time); LocalTime nowTime = LocalTime.now();System.out.println("nowTime = " + nowTime); System.out.println("小时: " + nowTime.getHour()); System.out.println("分钟: " + nowTime.getMinute()); System.out.println("秒: " + nowTime.getSecond()); System.out.println("纳秒: " + nowTime.getNano());
LocalDateTime(年月日时分秒) LocalDateTime :表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
1 2 3 4 5 6 7 8 9 10 11 12 13 14 LocalDateTime fj = LocalDateTime.of(1985 , 9 , 23 , 9 , 10 , 20 );System.out.println("fj = " + fj); LocalDateTime now = LocalDateTime.now();System.out.println("now = " + now); System.out.println(now.getYear()); System.out.println(now.getMonthValue()); System.out.println(now.getDayOfMonth()); System.out.println(now.getHour()); System.out.println(now.getMinute()); System.out.println(now.getSecond()); System.out.println(now.getNano());
其他API DateTimeFormatter :日期时间格式化类。
Instant:时间戳,表示一个特定的时间瞬间。
Duration:用于计算2个时间(LocalTime,时分秒)的距离
Period:用于计算2个日期(LocalDate,年月日)的距离
ZonedDateTime :包含时区的时间
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天,闰年是366天。此外Java 8还提供了4套其他历法,分别是:
ThaiBuddhistDate:泰国佛教历
MinguoDate:中华民国历
JapaneseDate:日本历
HijrahDate:伊斯兰历
日期时间的比较 1 2 3 4 5 6 7 8 @Test public void test06 () { LocalDate now = LocalDate.now(); LocalDate date = LocalDate.of(2018 , 8 , 8 ); System.out.println(now.isBefore(date)); System.out.println(now.isAfter(date)); }
JDK 8的时间格式化与解析 通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 @Test public void test04 () { LocalDateTime now = LocalDateTime.now(); DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); String format = now.format(formatter); System.out.println("format = " + format); LocalDateTime parse = LocalDateTime.parse("1985-09-23 10:12:22" , formatter); System.out.println("parse = " + parse); }
JDK 8的Instant类 Instant 时间戳/时间线,内部保存了从1970年1月1日 00:00:00以来的秒和纳秒。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Test public void test07 () { Instant now = Instant.now(); System.out.println("当前时间戳 = " + now); System.out.println(now.getNano()); System.out.println(now.getEpochSecond()); System.out.println(now.toEpochMilli()); System.out.println(System.currentTimeMillis()); Instant instant = Instant.ofEpochSecond(5 ); System.out.println(instant); }
JDK 8的计算日期时间差类 Duration/Period类: 计算日期时间差。
Duration:用于计算2个时间(LocalTime,时分秒)的距离
Period:用于计算2个日期(LocalDate,年月日)的距离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @Test public void test08 () { LocalTime now = LocalTime.now(); LocalTime time = LocalTime.of(14 , 15 , 20 ); Duration duration = Duration.between(time, now); System.out.println("相差的天数:" + duration.toDays()); System.out.println("相差的小时数:" + duration.toHours()); System.out.println("相差的分钟数:" + duration.toMinutes()); System.out.println("相差的秒数:" + duration.toSeconds()); LocalDate nowDate = LocalDate.now(); LocalDate date = LocalDate.of(1998 , 8 , 8 ); Period period = Period.between(date, nowDate); System.out.println("相差的年:" + period.getYears()); System.out.println("相差的月:" + period.getMonths()); System.out.println("相差的天:" + period.getDays()); }
JDK 8的时间校正器 有时我们可能需要获取例如:将日期调整到“下一个月的第一天”等操作。可以通过时间校正器来进行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test09 () { LocalDateTime now = LocalDateTime.now(); TemporalAdjuster firsWeekDayOfNextMonth = temporal -> { LocalDateTime dateTime = (LocalDateTime) temporal; LocalDateTime nextMonth = dateTime.plusMonths(1 ).withDayOfMonth(1 ); System.out.println("nextMonth = " + nextMonth); return nextMonth; }; LocalDateTime nextMonth = now.with(firsWeekDayOfNextMonth); System.out.println("nextMonth = " + nextMonth); }
JDK 8设置日期时间的时区 Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着 ID,ID的格式为 “区域/城市” 。例如 :Asia/Shanghai 等。
ZoneId:该类中包含了所有的时区信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 @Test public void test10 () { LocalDateTime now = LocalDateTime.now(); System.out.println("now = " + now); ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC()); System.out.println("bz = " + bz); ZonedDateTime now1 = ZonedDateTime.now(); System.out.println("now1 = " + now1); 19T16:19 :44.007153500 +08:00 [Asia/Shanghai] ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/Vancouver" )); System.out.println("now2 = " + now2); 07 :00 [America/Vancouver] }