jdk8新特性学习的使用方法总结笔记
JDK8新特性使用方法总结 一、Lambda表达式 1、匿名内部类
定义:没有名字的内部类
使用场景:简化书写
;某个局部类你只需要使用一次
,就可以使用匿名内部类
前提:必须存在继承或实现关系时才能使用
例子:
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 public class Demo1 { public void test () { new Animal(){ @Override public void eat () { System.out.println("匿名内部类实现eat" ); } }.eat(); Dog d = new Dog(); d.eat(); } interface Animal { void eat () ; } class Dog implements Animal { @Override public void eat () { System.out.println("小狗吃肉" ); } } }
本质:在编译时生成一个Class 文件。XXXXX$1.class
2、需求分析 创建一个新的线程,指定线程要执行的任务
1 2 3 4 5 6 7 8 9 public void lambdaDemo1 () { new Thread(new Runnable() { @Override public void run () { System.out.println("新线程中执行的代码 : " +Thread.currentThread().getName()); } }).start(); System.out.println("主线程中的代码:" + Thread.currentThread().getName()); }
这里使用了匿名内部类,传入一个实现类,实现了run方法
3、Lambda表达式初体验 1 2 3 4 5 6 7 8 9 10 new Thread(new Runnable() { @Override public void run () { System.out.println("新线程中执行的代码 : " +Thread.currentThread().getName()); } }).start(); new Thread(() -> { System.out.println("新线程中执行的代码 : " +Thread.currentThread().getName()); }).start();
优点:简化了匿名内部类的使用
4、Lambda的语法规则 Lambda省去了面向对象的条条框框,Lambda的标准格式由3个部分组成:
1 2 3 (参数类型 参数名称) -> { 代码体; }
格式说明:
(参数类型 参数名称):参数列表
{代码体;} :方法体
-> :箭头
,分割参数列表和方法体
4.1 Lambda练习1 练习无参无返回值的Lambda
定义一个接口
1 2 3 public interface UserService { void show () ; }
然后创建主方法使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void main (String[] args) { goShow(new UserService() { @Override public void show () { System.out.println("show 方法执行了..." ); } }); System.out.println("----------" ); goShow(() -> { System.out.println("Lambda show 方法执行了..." ); }); } public static void goShow (UserService userService) { userService.show(); }
4.2 Lambda练习2 练习有参有返回值的Lambda
创建一个Person对象
1 2 3 4 5 6 7 8 @Data @AllArgsConstructor @NoArgsConstructor public class Person { private String name; private Integer age; private Integer height; }
我们在List集合中保存多个Person对象,然后对这些对象做根据age排序操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public static void main (String[] args) { List<Person> list = new ArrayList<>(); list.add(new Person("周杰伦" ,33 ,175 )); list.add(new Person("刘德华" ,43 ,185 )); list.add(new Person("周星驰" ,38 ,177 )); list.add(new Person("郭富城" ,23 ,170 )); System.out.println("------" ); Collections.sort(list,(Person o1, Person o2) -> { return o1.getAge() - o2.getAge(); }); for (Person person : list) { System.out.println(person); } }
我们发现在sort方法的第二个参数是一个Comparator接口的匿名内部类,且执行的方法有参数和返回值,那么我们可以改写为Lambda表达式
5、Lambda表达式的省略写法 在lambda表达式的标准写法基础上,可以使用省略写法的规则为:
小括号内的参数类型可以省略
如果小括号内有且仅有一个参数,则小括号可以省略
如果大括号内有且仅有一个语句,可以同时省略大括号,return 关键字及语句分号
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 public class LambdaDemo3 { public static void main (String[] args) { goStudent((String name,Integer age)->{ return name + age + " 6666 ..." ; }); goStudent((name,age)-> name + age + " 6666 ..." ); goOrder((String name)->{ System.out.println("--->" + name); return "666" ; }); goOrder(name -> "666" ); } public static void goStudent (StudentService studentService) { studentService.show("张三" ,22 ); } public static void goOrder (OrderService orderService) { orderService.show("李四" ); } }
6、Lambda表达式的使用前提
方法的参数必须为接口才能使用Lambda
接口中有且仅有一个抽象方法
7、Lambda和匿名内部类的比较
所需类型不一样
匿名内部类的类型可以是 类,抽象类,接口
Lambda表达式需要的类型必须是接口
抽象方法的数量不一样
匿名内部类所需的接口中的抽象方法的数量是随意的
Lambda表达式所需的接口中只能有一个抽象方法
实现原理不一样
匿名内部类是在编译后形成一个class
Lambda表达式是在程序运行的时候动态生成class
二、接口中新增的方法 1、JDK8中接口的新增 JDK8之前
1 2 3 4 interface 接口名 { 静态常量; public static final 抽象方法; public abstract }
JDK8之后对接口做了增加,接口中可以有默认方法和静态方法
1 2 3 4 5 6 interface 接口名 { 静态常量; public static final 抽象方法; public abstract 默认方法; public default 静态方法; public static }
2、默认方法 2.1 为什么要增加默认方法 在JDK8以前接口中只能有抽象方法和静态常量,会存在以下的问题: 如果接口中新增抽象方法,那么实现类都必须要抽象这个抽象方法,非常不利于接口的扩展的
2.2 默认方法的使用 默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
3、静态方法 JDK8中为接口新增了静态方法,作用也是为了接口的扩展
使用:接口中的静态方法在实现类中是不能被重写的,调用的话只能通过: 接口名.静态方法名
4、两者的区别
默认方法通过实例调用,静态方法通过接口名调用
默认方法可以被继承,实现类可以直接调用接口默认方法,也可以重写接口默认方法
静态方法不能被继承,实现类不能重写接口的静态方法,只能使用接口名调用
三、函数式接口 1、函数式接口的由来 Lambda表达式的使用前提是要有函数式接口,而Lambda表达式使用时不关心接口名、抽象方法名。只关心抽象方法的参数列表和返回值类型。因此为了让我们使用Lambda表达式更加方便,JDK8中提供了大量常用的函数式接口
2、函数式接口介绍 JDK8提供的函数式接口,主要是在 java.util.function 包中
2.1 Supplier 无参有返回值的接口,对于的Lambda表达式需要提供一个返回数据的类型
1 2 3 4 @FunctionalInterface public interface Supplier <T > { T get () ; }
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 / ** * Supplier 函数式接口的使用 */ public class SupplierTest { public static void main (String[] args) { fun1(()->{ int arr[] = {22 ,33 ,55 ,66 ,44 ,99 ,10 }; Arrays.sort(arr); return arr[arr.length-1 ]; }); } private static void fun1 (Supplier<Integer> supplier) { Integer max = supplier.get(); System.out.println("max = " + max); } }
2.2 Consumer 有参无返回值的接口,前面的Supplier接口是用来生产数据的,而Consumer接口是用来消费数据的,使用的时候需要输入数据
1 2 3 4 @FunctionalInterface public interface Consumer <T > { void accept (T t) ; }
使用:将输入的数据统一转换为小写输出
1 2 3 4 5 6 7 8 9 10 public class ConsumerTest { public static void main (String[] args) { test(msg -> { System.out.println(msg + "-> 转换为小写:" + msg.toLowerCase()); }); } public static void test (Consumer<String> consumer) { consumer.accept("Hello World" ); } }
2.3 Function 有参有返回值的接口,Function接口是根据一个类型的数据得到另一个类型的数据,前者称为前置条件,后者称为后置条件。有参数有返回值
1 2 3 4 @FunctionalInterface public interface Function <T , R > { R apply (T t) ; }
使用:传递进入一个字符串返回一个数字
1 2 3 4 5 6 7 8 9 10 11 12 public class FunctionTest { public static void main(String[] args) { test(msg ->{ return Integer.parseInt(msg); }); } public static void test(Function<String,Integer> function){ Integer apply = function.apply("666"); System.out.println("apply = " + apply); } }
2.4 Predicate 有参且返回值为Boolean的接口
1 2 3 4 @FunctionalInterface public interface Predicate <T > { boolean test (T t) ; }
使用:
1 2 3 4 5 6 7 8 9 10 11 12 public class PredicateTest { public static void main (String[] args) { test(msg -> { return msg.length() > 3 ; },"HelloWorld" ); } private static void test (Predicate<String> predicate,String msg) { boolean b = predicate.test(msg); System.out.println("b:" + b); } }
四、方法引用 1、为什么要用方法引用 1.1 lambda表达式冗余 在使用Lambda表达式的时候,也会出现代码冗余的情况,比如:用Lambda表达式求一个数组的和
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 public class FunctionRefTest01 { public static void main (String[] args) { printMax(a -> { int sum = 0 ; for (int i : a) { sum += i; } System.out.println("数组之和:" + sum); }); } / ** * 求数组中的所有元素的和 * @param a */ public void getTotal (int a[]) { int sum = 0 ; for (int i : a) { sum += i; } System.out.println("数组之和:" + sum); } private static void printMax (Consumer<int []> consumer) { int [] a= {10 ,20 ,30 ,40 ,50 ,60 }; consumer.accept(a); } }
1.2 解决方案 因为在Lambda表达式中要执行的代码和我们另一个方法中的代码是一样的,这时就没有必要重写一份逻辑了,这时我们就可以“引用”重复代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class FunctionRefTest02 { public static void main(String[] args) { // :: 方法引用是JDK8中的新语法 printMax(FunctionRefTest02::getTotal); } / ** * 求数组中的所有元素的和 * @param a */ public static void getTotal(int a[]){ int sum = 0; for (int i : a) { sum += i; } System.out.println("数组之和:" + sum); } private static void printMax(Consumer<int[]> consumer){ int[] a= {10,20,30,40,50,60}; consumer.accept(a); } }
:: 方法引用是JDK8中的新语法
2、方法引用的格式 符号表示: ::
符号说明:双冒号为方法引用运算符
应用场景:如果Lambda表达式所要实现的方案,已经有其他方法存在相同的方案,那么则可以使用方法引用
常见的引用方式:
instanceName::methodName 对象::方法名
ClassName::staticMethodName 类名::静态方法
ClassName::methodName 类名::普通方法
ClassName::new 类名::new 调用的构造器
TypeName[]::new String[]::new 调用数组的构造器
2.1 对象名::方法名 1 2 3 4 5 6 7 8 public static void main (String[] args) { Date now = new Date(); Supplier<Long> supplier = ()->{return now.getTime();}; System.out.println(supplier.get()); Supplier<Long> supplier1 = now::getTime; System.out.println(supplier1.get()); }
注意事项:
被引用的方法,参数要和接口中的抽象方法的参数一样
当接口抽象方法有返回值时,被引用的方法也必须有返回值
2.2 类名::静态方法名 1 2 3 4 5 6 7 8 9 10 ublic static void main (String[] args) { Supplier<Long> supplier1 = ()->{ return System.currentTimeMillis(); }; System.out.println(supplier1.get()); Supplier<Long> supplier2 = System::currentTimeMillis; System.out.println(supplier2.get()); }
2.3 类名::引用实例方法 类名引用实例方法是有前提的,实际上是拿第一个参数作为方法的调用者
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public static void main (String[] args) { Function<String,Integer> function = (s)->{ return s.length(); }; System.out.println(function.apply("hello" )); Function<String,Integer> function1 = String::length; System.out.println(function1.apply("hahahaha" )); BiFunction<String,Integer,String> function2 = String::substring; String msg = function2.apply("HelloWorld" , 3 ); System.out.println(msg); }
2.4 类名::构造器 1 2 3 4 5 6 7 8 public static void main (String[] args) { Supplier<Person> sup = ()->{return new Person();}; System.out.println(sup.get()); Supplier<Person> sup1 = Person::new ; System.out.println(sup1.get()); }
2.5 数组::构造器 1 2 3 4 5 6 7 8 9 10 11 12 public static void main (String[] args) { Function<Integer,String[]> fun1 = (len)->{ return new String[len]; }; String[] a1 = fun1.apply(3 ); System.out.println("数组的长度是:" + a1.length); Function<Integer,String[]> fun2 = String[]::new ; String[] a2 = fun2.apply(5 ); System.out.println("数组的长度是:" + a2.length); }
小结:方法引用是对Lambda表达式符合特定情况下的一种缩写方式,它使得我们的Lambda表达式更加的精简,也可以理解为lambda表达式的缩写形式,不过要注意的是方法引用只能引用已经存在的方法
五、Stream API 1、集合处理数据的弊端 当我们在需要对集合中的元素进行操作的时候,除了必需的添加,删除,获取外,最典型的操作就是集合遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public class StreamTest01 { public static void main (String[] args) { List<String> list = Arrays.asList("张三" ,"张三丰" ,"成龙" ,"周星驰" ); List<String> list1 = new ArrayList<>(); for (String s : list) { if (s.startsWith("张" )){ list1.add(s); } } List<String> list2 = new ArrayList<>(); for (String s : list1) { if (s.length() == 3 ){ list2.add(s); } } for (String s : list2) { System.out.println(s); } } }
上面的代码针对与我们不同的需求总是一次次的循环循环循环.这时我们希望有更加高效的处理方式,这时我们就可以通过JDK8中提供的Stream API来解决这个问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public class StreamTest02 { public static void main (String[] args) { List<String> list = Arrays.asList("张三" ,"张三丰" ,"成龙" ,"周星驰" ); list.stream() .filter(s->s.startsWith("张" )) .filter(s->s.length() == 3 ) .forEach(s->{ System.out.println(s); }); System.out.println("----------" ); list.stream() .filter(s->s.startsWith("张" )) .filter(s->s.length() == 3 ) .forEach(System.out::println); } }
上面的SteamAPI代码的含义:获取流,过滤张,过滤长度,逐一打印。代码相比于上面的写法更加简洁直观
2、Stream流的获取方式 2.1 根据Collection获取 集合有两个父接口,Collection接口和Map接口
首先,java.util.Collection 接口中加入了default方法 stream,也就是说Collection接口下的所有的实现都可以通过steam方法来获取Stream流
1 2 3 4 5 6 7 8 public static void main (String[] args) { List<String> list = new ArrayList<>(); list.stream(); Set<String> set = new HashSet<>(); set.stream(); Vector vector = new Vector(); vector.stream(); }
Map接口没有实现 stream,这时我们可以通过Map的keySet()、values()、entrySet()来获取流
1 2 3 4 5 6 public static void main (String[] args) { Map<String,Object> map = new HashMap<>(); Stream<String> stream = map.keySet().stream(); Stream<Object> stream1 = map.values().stream(); Stream<Map.Entry<String, Object>> stream2 = map.entrySet().stream(); }
2.2 通过Stream的of方法 在实际开发中我们不可避免的会操作到数组中的数据,所以Stream接口中提供了静态方法of
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 public class StreamTest05 { public static void main (String[] args) { Stream<String> a1 = Stream.of("a1" , "a2" , "a3" ); String[] arr1 = {"aa" ,"bb" ,"cc" }; Stream<String> arr11 = Stream.of(arr1); Integer[] arr2 = {1 ,2 ,3 ,4 }; Stream<Integer> arr21 = Stream.of(arr2); arr21.forEach(System.out::println); int [] arr3 = {1 ,2 ,3 ,4 }; Stream.of(arr3).forEach(System.out::println); } }
3、Stream常用方法介绍 Stream流模型的操作可以被分为两种:
方法名
方法作用
返回值类型
方法种类
forEach
逐一处理
void
终结
count
统计个数
long
终结
match
是否匹配指定的条件
boolean
终结
find
找到某些数据
Optional
终结
max、min
最大值、最小值
Optional
终结
reduce
求和、最大值、统计次数
T
终结
filter
过滤
Stream
函数拼接
limit
取用前几个
Stream
函数拼接
skip
跳过前几个
Stream
函数拼接
map
映射
Stream
函数拼接
sorted
排序
Stream
函数拼接
distinct
去重
Stream
函数拼接
concat
组合
Stream
函数拼接
终结方法: 返回值类型不再是 Stream 类型的方法,不再支持链式调用
非终结方法: 返回值类型仍然是 Stream 类型的方法,支持链式调用
链式调用就是调用完一个函数后还能再继续调用其它函数,这样大大减少了代码量
3.1 forEach forEach用来遍历流中的数据的
1 void forEach (Consumer<? super T> action) ;
该方法接收一个Consumer接口,会将每一个流元素交给函数处理
1 2 3 public static void main (String[] args) { Stream.of("a1" , "a2" , "a3" ).forEach(System.out::println); }
3.2 count Stream流中的count方法用来统计其中的元素个数的
该方法返回一个long值,代表元素的个数
1 2 3 4 public static void main (String[] args) { long count = Stream.of("a1" , "a2" , "a3" ).count(); System.out.println(count); }
3.3 filter 可以通过filter方法将一个流转换成另一个子集流
1 Stream<T> filter (Predicate<? super T> predicate) ;
该接口接收一个Predicate函数式接口参数作为筛选条件
1 2 3 4 5 public static void main (String[] args) { Stream.of("a1" , "a2" , "a3" ,"bb" ,"cc" ,"aa" ,"dd" ) .filter((s)->s.contains("a" )) .forEach(System.out::println); }
输出:
3.4 limit limit方法可以对流进行截取处理,截取前n个数据
1 Stream<T> limit (long maxSize) ;
参数是一个long类型的数值,如果集合当前长度大于参数就进行截取
1 2 3 4 5 public static void main (String[] args) { Stream.of("a1" , "a2" , "a3" ,"bb" ,"cc" ,"aa" ,"dd" ) .limit(3 ) .forEach(System.out::println); }
输出:
3.5 skip 如果希望跳过前面几个元素,可以使用skip方法获取一个截取之后的新流
操作:
1 2 3 4 5 public static void main (String[] args) { Stream.of("a1" , "a2" , "a3" ,"bb" ,"cc" ,"aa" ,"dd" ) .skip(3 ) .forEach(System.out::println); }
输出:
3.6 map 如果我们需要将流中的元素映射到另一个流中,可以使用map方法
1 <R> Stream<R> map (Function<? super T, ? extends R> mapper) ;
该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的数据
1 2 3 4 5 public static void main (String[] args) { Stream.of("1" , "2" , "3" ,"4" ,"5" ,"6" ,"7" ) .map(Integer::parseInt) .forEach(System.out::println); }
3.7 sorted 如果需要将数据排序,可以使用sorted方法
在使用时可以指定对应的排序规则
1 2 3 4 5 6 public static void main (String[] args) { Stream.of("1" , "3" , "2" ,"4" ,"0" ,"9" ,"7" ) .map(Integer::parseInt) .sorted((o1,o2)->o2-o1) .forEach(System.out::println); }
3.8 distinct 如果要去掉重复数据,可以使用distinct方法
使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public static void main(String[] args) { Stream.of("1", "3", "3","4","0","1","7") .map(Integer::parseInt) .sorted((o1,o2)->o2-o1) // 根据比较强指定排序规则,从大到小 .distinct() // 去掉重复的记录 .forEach(System.out::println); System.out.println("--------"); Stream.of( new Person("张三",18) ,new Person("李四",22) ,new Person("张三",18) ).distinct() .forEach(System.out::println); }
Stream流中的distinct方法对于基本数据类型是可以直接出重的,但是对于自定义类型,我们是需要重hashCode和equals方法来移除重复元素
3.9 match 如果需要判断数据是否匹配指定的条件,可以使用match相关的方法
1 2 3 boolean anyMatch (Predicate<? super T> predicate) ; boolean allMatch (Predicate<? super T> predicate) ; boolean noneMatch (Predicate<? super T> predicate) ;
使用:
1 2 3 4 5 6 7 8 public static void main (String[] args) { boolean b = Stream.of("1" , "3" , "3" , "4" , "5" , "1" , "7" ) .map(Integer::parseInt) .noneMatch(s -> s > 4 ); System.out.println(b); }
3.10 find 如果我们需要找到某些数据,可以使用find方法来实现
1 2 Optional<T> findFirst () ; Optional<T> findAny () ;
使用:
1 2 3 4 5 6 7 public static void main (String[] args) { Optional<String> first = Stream.of("1" , "3" , "3" , "4" , "5" , "1" ,"7" ).findFirst(); System.out.println(first.get()); Optional<String> any = Stream.of("1" , "3" , "3" , "4" , "5" , "1" ,"7" ).findAny(); System.out.println(any.get()); }
3.11 max和min 如果我们想要获取最大值和最小值,那么可以使用max和min方法
1 2 Optional<T> min (Comparator<? super T> comparator) ;Optional<T> max (Comparator<? super T> comparator) ;
使用:
1 2 3 4 5 6 7 8 9 10 public static void main (String[] args) { Optional<Integer> max = Stream.of("1" , "3" , "3" , "4" , "5" , "1" , "7" ) .map(Integer::parseInt) .max((o1,o2)->o1-o2); System.out.println(max.get()); Optional<Integer> min = Stream.of("1" , "3" , "3" , "4" , "5" , "1" , "7" ) .map(Integer::parseInt) .min((o1,o2)->o1-o2); System.out.println(min.get()); }
3.12 reduce 如果需要将所有数据归纳得到一个数据,可以使用reduce方法
1 T reduce (T identity, BinaryOperator<T> accumulator) ;
使用:
1 2 3 4 5 6 7 8 9 10 11 public static void main (String[] args) { Integer sum = Stream.of(4 , 5 , 3 , 9 ) .reduce(0 , Integer::sum); System.out.println(sum); Integer max = Stream.of(4 , 5 , 3 , 9 ) .reduce(0 , Math::max); System.out.println(max); }
3.13 map和reduce 在实际开发中我们经常会将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 25 26 27 28 public static void main (String[] args) { Integer sumAge = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ).map(Person::getAge) .reduce(0 , Integer::sum); System.out.println(sumAge); Integer maxAge = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ).map(Person::getAge) .reduce(0 , Math::max); System.out.println(maxAge); Integer count = Stream.of("a" , "b" , "c" , "d" , "a" , "c" , "a" ) .map(ch -> "a" .equals(ch) ? 1 : 0 ) .reduce(0 , Integer::sum); }
结果:
3.14 concat 如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat
1 2 3 4 5 6 public static void main (String[] args) { Stream<String> stream1 = Stream.of("a" ,"b" ,"c" ); Stream<String> stream2 = Stream.of("x" , "y" , "z" ); Stream.concat(stream1,stream2).forEach(System.out::println); }
3.15 综合案例 定义两个集合,然后在集合中存储多个用户名称。然后完成如下的操作:
第一个队伍只保留姓名长度为3的成员
第一个队伍筛选之后只要前3个人
第二个队伍只要姓张的成员
第二个队伍筛选之后不要前两个人
将两个队伍合并为一个队伍
根据姓名创建Person对象
打印整个队伍的Person信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void main (String[] args) { List<String> list1 = Arrays.asList("迪丽热巴" , "宋远桥" , "苏星河" , "老子" ,"庄子" , "孙子" , "洪七公" ); List<String> list2 = Arrays.asList("古力娜扎" , "张无忌" , "张三丰" , "赵丽颖" ,"张二狗" , "张天爱" , "张三" ); Stream<String> stream1 = list1.stream().filter(s -> s.length() == 3 ).limit(3 ); Stream<String> stream2 = list2.stream().filter(s -> s.startsWith("张" )).skip(2 ); Stream.concat(stream1,stream2) .map(Person::new ) .forEach(System.out::println); }
输出结果:
1 2 3 4 5 Person{name='宋远桥' , age=null , height=null } Person{name='苏星河' , age=null , height=null } Person{name='张二狗' , age=null , height=null } Person{name='张天爱' , age=null , height=null } Person{name='张三' , age=null , height=null }
4、Stream结果收集 4.1 结果收集到集合中 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void test01 () { List<String> list = Stream.of("aa" , "bb" , "cc" ,"aa" ).collect(Collectors.toList()); System.out.println(list); Set<String> set = Stream.of("aa" , "bb" , "cc" , "aa" ).collect(Collectors.toSet()); System.out.println(set); ArrayList<String> arrayList = Stream.of("aa" , "bb" , "cc" , "aa" ) .collect(Collectors.toCollection(ArrayList::new )); System.out.println(arrayList); HashSet<String> hashSet = Stream.of("aa" , "bb" , "cc" , "aa" ) .collect(Collectors.toCollection(HashSet::new )); System.out.println(hashSet); }
4.2 结果收集到数组中 1 2 3 4 public void test02 () { String[] strings = Stream.of("aa" , "bb" , "cc" , "aa" ).toArray(String[]::new ); System.out.println(Arrays.toString(strings)); }
4.3 对流中的数据做聚合计算 当我们使用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 40 41 42 43 44 45 46 47 48 49 50 51 52 53 public void test03 () { Optional<Person> maxAge = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ).collect(Collectors.maxBy((p1, p2) -> p1.getAge() - p2.getAge())); System.out.println("最大年龄:" + maxAge.get()); Optional<Person> minAge = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ).collect(Collectors.minBy((p1, p2) -> p1.getAge() - p2.getAge())); System.out.println("最新年龄:" + minAge.get()); Integer sumAge = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ) .collect(Collectors.summingInt(Person::getAge)); System.out.println("年龄总和:" + sumAge); Double avgAge = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ).collect(Collectors.averagingInt(Person::getAge)); System.out.println("年龄的平均值:" + avgAge); Long count = Stream.of( new Person("张三" , 18 ) , new Person("李四" , 22 ) , new Person("张三" , 13 ) , new Person("王五" , 15 ) , new Person("张三" , 19 ) ).filter(p -> p.getAge() > 18 ) .collect(Collectors.counting()); System.out.println("满足条件的记录数:" + count); }
4.4 对流中数据做分组操作 当我们使用Stream流处理数据后,可以根据某个属性将数据分组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public void test04 () { Map<String, List<Person>> map1 = Stream.of( new Person("张三" , 18 , 175 ) , new Person("李四" , 22 , 177 ) , new Person("张三" , 14 , 165 ) , new Person("李四" , 15 , 166 ) , new Person("张三" , 19 , 182 ) ).collect(Collectors.groupingBy(Person::getName)); map1.forEach((k,v)-> System.out.println( k + "\t" + v)); System.out.println("-----------" ); Map<String, List<Person>> map2 = Stream.of( new Person("张三" , 18 , 175 ) , new Person("李四" , 22 , 177 ) , new Person("张三" , 14 , 165 ) , new Person("李四" , 15 , 166 ) , new Person("张三" , 19 , 182 ) ).collect(Collectors.groupingBy(p -> p.getAge() >= 18 ? "成年" : "未成年" )); map2.forEach((k,v)-> System.out.println(k +" \t" + v)); }
输出结果:
1 2 3 4 5 李四 [Person{name='李四' , age=22 , height=177 }, Person{name='李四' , age=15 ,height=166 }] 张三 [Person{name='张三' , age=18 , height=175 }, Person{name='张三' , age=14 ,height=165 }, Person{name='张三' , age=19 , height=182 }] ----------- 未成年 [Person{name='张三' , age=14 , height=165 }, Person{name='李四' , age=15 ,height=166 }] 成年 [Person{name='张三' , age=18 , height=175 }, Person{name='李四' , age=22 ,height=177 }, Person{name='张三' , age=19 , height=182 }]
4.5 对流中的数据做分区操作 Collectors.partitioningBy会根据值是否为true,把集合中的数据分割为两个列表,一个true列表,一个false列表
1 2 3 4 5 6 7 8 9 10 public void test06 () { Map<Boolean, List<Person>> map = Stream.of( new Person("张三" , 18 , 175 ) , new Person("李四" , 22 , 177 ) , new Person("张三" , 14 , 165 ) , new Person("李四" , 15 , 166 ) , new Person("张三" , 19 , 182 ) ).collect(Collectors.partitioningBy(p -> p.getAge() > 18 )); map.forEach((k,v)-> System.out.println(k + "\t" + v)); }
输出结果:
1 2 false [Person{name='张三' , age=18 , height=175 }, Person{name='张三' , age=14 ,height=165 }, Person{name='李四' , age=15 , height=166 }]true [Person{name='李四' , age=22 , height=177 }, Person{name='张三' , age=19 ,height=182 }]
4.6 对流中的数据做拼接 Collectors.joining会根据指定的连接符,将所有的元素连接成一个字符串
1 2 3 4 5 6 7 8 9 10 11 12 13 public void test07 () { String s1 = Stream.of( new Person("张三" , 18 , 175 ) , new Person("李四" , 22 , 177 ) , new Person("张三" , 14 , 165 ) , new Person("李四" , 15 , 166 ) , new Person("张三" , 19 , 182 ) ).map(Person::getName) .collect(Collectors.joining()); System.out.println(s1); }
5、并行的Stream流 5.1 串行的Stream流 我们前面使用的Stream流都是串行,也就是在一个线程上面执行
1 2 3 4 5 6 7 8 public void test01 () { long count = Stream.of(5 ,6 ,8 ,3 ,1 ,6 ) .filter(s->{ System.out.println(Thread.currentThread() + "" + s); return s > 3 ; }).count(); System.out.println("count=" + count); }
输出:
1 2 3 4 5 6 7 Thread[main,5 ,main]5 Thread[main,5 ,main]6 Thread[main,5 ,main]8 Thread[main,5 ,main]3 Thread[main,5 ,main]1 Thread[main,5 ,main]6 count=4
5.2 并行流 parallelStream其实就是一个并行执行的流,它通过默认的ForkJoinPool,可以提高多线程任务的速度
5.2.1 获取并行流 我们可以通过两种方式来获取并行流。
通过Collection接口中的parallelStream方法来获取
通过已有的串行流转换为并行流(parallel)
实现:
1 2 3 4 5 6 7 public void test02 () { List<Integer> list = new ArrayList<>(); Stream<Integer> integerStream = list.parallelStream(); Stream<Integer> parallel = Stream.of(1 , 2 , 3 ).parallel(); }
5.2.2 并行流操作 1 2 3 4 5 6 7 8 public void test03 () { Stream.of(1 ,4 ,2 ,6 ,1 ,5 ,9 ) .parallel() .filter(s->{ System.out.println(Thread.currentThread() + " s=" +s); return s > 2 ; }).count(); }
效果:
1 2 3 4 5 6 7 Thread[main,5 ,main] s=1 Thread[ForkJoinPool.commonPool-worker-2 ,5 ,main] s=9 Thread[ForkJoinPool.commonPool-worker-6 ,5 ,main] s=6 Thread[ForkJoinPool.commonPool-worker-13 ,5 ,main] s=2 Thread[ForkJoinPool.commonPool-worker-9 ,5 ,main] s=4 Thread[ForkJoinPool.commonPool-worker-4 ,5 ,main] s=5 Thread[ForkJoinPool.commonPool-worker-11 ,5 ,main] s=1
5.3 并行流和串行流对比 我们通过for循环,串行Stream流,并行Stream流来对5亿个数字求和,来看消耗时间
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 public class Test03 { private static long times = 500000000 ; / ** * 普通for 循环 消耗时间:138ms */ @Test public void test01 () { System.out.println("普通for循环:" ); long res = 0 ; for (int i = 0 ; i < times; i++) { res += i; } } / ** * 串行流处理 消耗时间:203ms */ @Test public void test02 () { System.out.println("串行流:" ); LongStream.rangeClosed(0 ,times) .reduce(0 ,Long::sum); } / ** * 并行流处理 消耗时间:84ms */ @Test public void test03 () { System.out.println("并行流:" ); LongStream.rangeClosed(0 ,times) .parallel() .reduce(0 ,Long::sum); } }
我们可以看到parallelStream的效率是最高的。Stream并行处理的过程会分而治之,也就是将一个大的任务切分成了多个小任务,这表示每个任务都是一个线程操作
5.4 线程安全问题 在多线程的处理下,就有可能出现数据安全问题
1 2 3 4 5 6 7 8 9 10 11 public void test01 () { List<Integer> list = new ArrayList<>(); for (int i = 0 ; i < 1000 ; i++) { list.add(i); } List<Integer> listNew = new ArrayList<>(); list.parallelStream() .forEach(listNew::add); System.out.println(listNew.size()); }
可能报异常:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 java.lang.ArrayIndexOutOfBoundsException at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorI mpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorA ccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:598) .... Caused by: java.lang.ArrayIndexOutOfBoundsException: 366 at java.util.ArrayList.add(ArrayList.java:463)
针对这个问题,我们的解决方案有哪些呢?
加同步锁
使用线程安全的容器
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 public void test02 () { List<Integer> listNew = new ArrayList<>(); Object obj = new Object(); IntStream.rangeClosed(1 ,1000 ) .parallel() .forEach(i->{ synchronized (obj){ listNew.add(i); } }); System.out.println(listNew.size()); } public void test03 () { Vector v = new Vector(); Object obj = new Object(); IntStream.rangeClosed(1 ,1000 ) .parallel() .forEach(i -> v.add(i)); System.out.println(v.size()); }
六、Optional类 Optional类是解决空指针的问题
1、以前对null的处理 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 / ** * 根据Person对象 将name转换为大写并返回 */ public String getName (Person person) { if (person != null ){ String name = person.getName(); if (name != null ){ return name.toUpperCase(); }else { return null ; } }else { return null ; } }
2、Optional类 Optional是一个没有子类的工具类,Optional是一个可以为null的容器对象,它的主要作用就是为了避免Null检查,防止NullpointerException
3、Optional的基本使用 Optional对象的创建方式
1 2 3 4 5 6 7 8 9 10 11 12 public void test02 () { Optional<String> op1 = Optional.of("zhangsan" ); Optional<String> op3 = Optional.ofNullable("lisi" ); Optional<Object> op4 = Optional.ofNullable(null ); Optional<Object> op5 = Optional.empty(); }
4、Optional的常用方法
isPresent():判断是否包含值,包含值返回true,不包含值返回false
get():如果Optional有值则返回,否则会抛出NoSuchElementException异常。get()通常和isPresent方法一块使用
orElse(T t):如果调用对象包含值,就返回该值,否则返回t
orElseGet(Supplier s):如果调用对象包含值,就返回该值,否则返回 Lambda表达式的返回值
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 / ** * 根据Person对象 将name转换为大写并返回 * 通过Optional方式实现 */ public String getNameForOptional (Optional<Person> op) { if (op.isPresent()){ String msg = op.map(Person::getName) .map(String::toUpperCase) .orElse("空值" ); return msg; } return null ; } / ** * 根据Person对象 将name转换为大写并返回 */ public String getName (Person person) { if (person != null ){ String name = person.getName(); if (name != null ){ return name.toUpperCase(); }else { return null ; } }else { return null ; } }
七、新时间日期API 1、旧版日期时间的问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void test01 () throws Exception { Date date = new Date(2021 ,05 ,05 ); System.out.println(date); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd" ); for (int i = 0 ; i < 50 ; i++) { new Thread(()->{ try { System.out.println(sdf.parse("2021-05-06" )); } catch (ParseException e) { e.printStackTrace(); } }).start(); } }
设计不合理,在java.util和java.sql的包中都有日期类,java.util.Date同时包含日期和时间的,而java.sql.Date仅仅包含日期,此外用于格式化和解析的类在java.text包下
非线程安全,java.util.Date是非线程安全的,所有的日期类都是可变的,这是java日期类最大的问题之一
时区处理麻烦,日期类并不提供国际化,没有时区支持
2、新日期时间API介绍 JDK 8中增加了一套全新的日期时间API,这套API设计合理,是线程安全的。新的日期及时间API位于 java.time 包中,下面是一些关键类:
LocalDate:表示日期,包含年月日,格式为 2019-10-16
LocalTime:表示时间,包含时分秒,格式为 16:38:54.158549300
LocalDateTime:表示日期时间,包含年月日,时分秒,格式为 2018-09-06T15:33:56.750
DateTimeFormatter:日期时间格式化类。
Instant:时间戳,表示一个特定的时间瞬间
Duration:用于计算2个时间(LocalTime,时分秒)的距离
Period:用于计算2个日期(LocalDate,年月日)的距离
ZonedDateTime:包含时区的时间
Java中使用的历法是ISO 8601日历系统,它是世界民用历法,也就是我们所说的公历。平年有365天, 闰年是366天。此外Java 8还提供了4套其他历法,分别是:
ThaiBuddhistDate:泰国佛教历
MinguoDate:中华民国历
JapaneseDate:日本历
HijrahDate:伊斯兰历
2.1 日期时间的常见操作 LocalDate,LocalTime以及LocalDateTime的操作
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 public void test01 () { LocalDate date1 = LocalDate.of(2021 , 05 , 06 ); System.out.println("date1 = " +date1); LocalDate now = LocalDate.now(); System.out.println("now = " +now); System.out.println("年:" + now.getYear()); System.out.println("月:" + now.getMonth().getValue()); System.out.println("日:" + now.getDayOfMonth()); System.out.println("星期:" + now.getDayOfWeek().getValue()); } public void test02 () { LocalTime time = LocalTime.of(5 ,26 ,33 ,23145 ); System.out.println(time); LocalTime now = LocalTime.now(); System.out.println(now); System.out.println(now.getHour()); System.out.println(now.getMinute()); System.out.println(now.getSecond()); System.out.println(now.getNano()); }
2.2 日期时间的修改和比较 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 public void test01 () { LocalDateTime now = LocalDateTime.now(); System.out.println("now = " +now); LocalDateTime localDateTime = now.withYear(1998 ); System.out.println("now :" +now); System.out.println("修改后的:" + localDateTime); System.out.println("月份:" + now.withMonth(10 )); System.out.println("天:" + now.withDayOfMonth(6 )); System.out.println("小时:" + now.withHour(8 )); System.out.println("分钟:" + now.withMinute(15 )); System.out.println("两天后:" + now.plusDays(2 )); System.out.println("10年后:" +now.plusYears(10 )); System.out.println("6个月后 = " + now.plusMonths(6 )); System.out.println("10年前 = " + now.minusYears(10 )); System.out.println("半年前 = " + now.minusMonths(6 )); System.out.println("一周前 = " + now.minusDays(7 )); } public void test02 () { LocalDate now = LocalDate.now(); LocalDate date = LocalDate.of(2020 , 1 , 3 ); System.out.println(now.isAfter(date)); System.out.println(now.isBefore(date)); System.out.println(now.isEqual(date)); }
注意:在进行日期时间修改的时候,原来的 LocalDate 对象是不会被修改,每次操作都是返回了一个新的LocalDate对象,所以在多线程场景下是数据安全的
2.3 格式化和解析操作 在JDK8中我们可以通过 java.time.format.DateTimeFormatter
类可以进行日期的解析和格式化操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void test01 () {LocalDateTime now = LocalDateTime.now(); DateTimeFormatter isoLocalDateTime = DateTimeFormatter.ISO_LOCAL_DATE_TIME; String format = now.format(isoLocalDateTime); System.out.println("format = " + format); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyyMM-dd HH:mm:ss" ); String format1 = now.format(dateTimeFormatter); System.out.println("format1 = " + format1); LocalDateTime parse = LocalDateTime.parse("1997-05-06 22:45:16" ,dateTimeFormatter); System.out.println("parse = " + parse); }
2.4 计算日期时间差 JDK8中提供了两个工具类Duration/Period:计算日期时间差
Duration:用来计算两个时间差(LocalTime)
Period:用来计算两个日期差(LocalDate)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void test01 () { LocalTime now = LocalTime.now(); LocalTime time = LocalTime.of(22 , 48 , 59 ); System.out.println("now = " + now); Duration duration = Duration.between(now, time); System.out.println(duration.toDays()); System.out.println(duration.toHours()); System.out.println(duration.toMinutes()); System.out.println(duration.toMillis()); LocalDate nowDate = LocalDate.now(); LocalDate date = LocalDate.of(1997 , 12 , 5 ); Period period = Period.between(date, nowDate); System.out.println(period.getYears()); System.out.println(period.getMonths()); System.out.println(period.getDays()); }
2.5 日期时间的地区 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 public void test01 () { LocalDateTime now = LocalDateTime.now(); System.out.println("now = " + now); ZonedDateTime bz = ZonedDateTime.now(Clock.systemUTC()); System.out.println("bz = " + bz); ZonedDateTime now2 = ZonedDateTime.now(ZoneId.of("America/New_York" )); System.out.println("now2 = " + now2); }
JDK新的日期和时间的优势:
新版日期时间API中,日期和时间对象是不可变,操作日期不会影响原来的值,而是生成一个新的 实例
线程安全
加入对时区的支持