Java8 新特性
Java8(JDK8.0)较 JDK7.0 有很多变化或者说是优化,比如 interface 里可以有静态方法和默认方法,并且可以有方法体 ,这一点就颠覆了之前的认知;java.util.HashMap
数据结构里增加了红黑树 ;还有众所周知的 Lambda 表达式 等等
一、Interface
interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。
为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。
default
修饰的方法,是普通实例方法,可以用this
调用,可以被子类继承、重写。
static
修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface
调用。
我们来看一个实际的例子。
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 interface InterfaceNew { static void sm () { System.out.println("interface提供的方式实现" ); } static void sm2 () { System.out.println("interface提供的方式实现" ); } default void def () { System.out.println("interface default方法" ); } default void def2 () { System.out.println("interface default2方法" ); } void f () ; } public interface InterfaceNew1 { default void def () { System.out.println("InterfaceNew1 default方法" ); } }
如果有一个类既实现了 InterfaceNew
接口又实现了 InterfaceNew1
接口,它们都有def()
,并且 InterfaceNew
接口和 InterfaceNew1
接口没有继承关系的话,这时就必须重写def()
。不然的话,编译的时候就会报错。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{ public static void main (String[] args) { InterfaceNewImpl interfaceNew = new InterfaceNewImpl (); interfaceNew.def(); } @Override public void def () { InterfaceNew1.super .def(); } @Override public void f () { } }
在 Java 8 中,接口和抽象类有什么区别的?
interface 和 class 的区别主要有:
接口多实现,类单继承
接口的方法是 public abstract 修饰,变量是 public static final 修饰。 abstract class 可以用其他修饰符
interface 的方法是更像是一个扩展插件。而 abstract class 的方法是要继承的。
开始我们也提到,interface 新增default
和static
修饰的方法,为了解决接口的修改与现有的实现类不兼容的问题,并不是为了要替代abstract class
。在使用上,该用 abstract class 的地方还是要用 abstract class,不要因为 interface 的新特性而将之替换。
记住接口永远和类不一样
二、函数式接口:functional interface
定义 :也称 SAM 接口,即 Single Abstract Method interfaces,有且只有一个抽象方法,但可以有多个非抽象方法的接口 。
在 java 8 中专门有一个包放函数式接口java.util.function
,该包下的所有接口都有 @FunctionalInterface
注解 。
在其他包中也有函数式接口,其中一些没有@FunctionalInterface
注解,但是只要符合函数式接口的定义就是函数式接口,与是否有@FunctionalInterface
注解无关,注解只是在编译时起到强制规范定义的 作用。其在 Lambda 表达式中有广泛的应用。
三、Lambda 表达式
本质是接口的实例(==对象==)
Lambda表达式是否有返回值,取决于(@Override)函数本身
使用 Lambda 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程 。
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中
Lambda 表达式的本质:整个 Lambda 表达式代表一个函数式接口的对象
使用情境:当需要对一个==函数式接口==实例化时,或方法形参中需要函数式接口的对象时,我们使用Lambda表达式来代替匿名实现类
1. 语法格式
**->
:**Lambda 操作符或箭头操作符
左侧:Lambda 表达式的形参列表——接口中 抽象方法的形参列表
**右侧:**Lambda 体——实现的抽象方法的方法体 ,也即 Lambda 表达式要执行的功能
1 2 3 (parameters) -> {statements;}; 或 (parameters) -> expression;
语法格式一:抽象方法没有形参,无返回值
1 2 Runnable r = () -> {System.out.println("It's a lambda function!" );};
语法格式二:抽象方法有一个参数,无返回值
1 2 Consumer<String> con = (String str) -> {System.out.println(str);};
语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
1 2 Consumer<String> con = (str) -> {System.out.println(str);};
语法格式四:Lambda 操作符左边若只有一个参数时, 参数的小括号可以省略
1 2 Consumer<String> con = str -> {System.out.println(str);};
语法格式五:抽象方法需要两个或以上的参数,多条执行语句,并且可以有返回值(标准写法)
1 2 3 4 5 Comparator<Integer> com = (o1, o2) -> { System.out.println("实现函数式接口方法" ); return Integer.compare(o1, o2); };
语法格式六:当 Lambda 体只有一条执行语句(可能是 return 语句), return与大括号都可以省略
1 2 Comparator<Integer> com = (o1, o2) -> Integer.compare(o1, o2);
1 2 3 4 Comparator<Integer> com1 = (o1, o2) -> Integer.compare(o1, o2); Comparator<Integer> com2 = Integer::compare;
1.1 总结
2. Lambda 实战
2.1 替代匿名内部类
过去给方法传动态参数的唯一方法是使用内部类(匿名实现类 )。比如
1.Runnable
接口
1 2 3 4 5 6 7 8 9 10 11 12 new Thread (new Runnable () { @Override public void run () { System.out.println("The runable now is using!" ); } }).start(); new Thread (() -> System.out.println("It's a lambda function!" )).start();1234567891011
2.Comparator
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 List<Integer> list = Arrays.asList(1 , 2 , 3 ); Collections.sort(list, new Comparator <Integer>() { @Override public int compare (Integer o1, Integer o2) { return Integer.compare(o1, o2); } }); Collections.sort(list, (Integer o1, Integer o2) -> Integer.compare(o1, o2)); Comparator<Integer> comperator = (Integer o1, Integer o2) -> Integer.compare(o1, o2); Collections.sort(list, comperator); 12345678910111213141516
3.Listener
接口
1 2 3 4 5 6 7 8 9 10 11 12 13 JButton button = new JButton ();button.addItemListener(new ItemListener () { @Override public void itemStateChanged (ItemEvent e) { e.getItem(); } }); button.addItemListener(e -> e.getItem());
4.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口 ,也就是说只要方法的参数是函数式接口的对象,就可以使用 Lambda 表达式 。
1 2 3 4 5 6 @FunctionalInterface public interface Comparator <T>{}@FunctionalInterface public interface Runnable {}
我们自定义一个函数式接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @FunctionalInterface public interface LambdaInterface { void f () ; } public class LambdaClass { public static void forEg () { lambdaInterfaceDemo(()-> System.out.println("自定义函数式接口" )); } static void lambdaInterfaceDemo (LambdaInterface i) { System.out.println(i); } }
2.2 集合迭代:forEach()方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void lamndaFor () { List<String> strings = Arrays.asList("1" , "2" , "3" ); for (String s : strings) { System.out.println(s); } strings.forEach((s) -> System.out.println(s)); strings.forEach(System.out::println); Map<Integer, String> map = new HashMap <>(); map.forEach((k,v)->System.out.println(v)); }
2.3 方法引用
2.3.1 方法引用
⭐不需要写参数名称
1 Consumer<String> con2 = Person::Print;
方法引用是一种更加紧凑,易读的 Lambda 表达式。即方法引用就是 Lambda 表达式,本质上代表一个函数式接口的对象
**情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用了一个已存在的方法,而不执行任何其它操作 ,可以考虑使用方法引用
要求:函数式接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致(针对情况1和2)
格式
:使用操作符
将类名(或对象引用)与方法名分隔开来,整个表达式代表一个
函数式接口的对象
注意:::
右边的方法必须是左边的类或对象所在类中声明的方法
主要使用情况
:
情况1: 对象引用::实例(非静态)方法
情况2: 类名::静态方法名
情况3: 类名::(非静态)方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test void methodReferenceTest () { Consumer<String> con1 = s -> System.out.println(s); Consumer<String> con2 = Person::static_Print; Consumer<String> con3 = System.out::println; Person p1 = new Person (12 , "Tms" ); Consumer<String> con4 = p1::unstatic_Print; Consumer<String> con5 = s -> p1.unstatic_Print(s); }
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 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 public class MethodRefTest { @Test public void test1 () { Consumer<String> con1 = str -> System.out.println(str); con1.accept("Beijing" ); Consumer<String> con2 = System.out::println; con2.accept("Shanghai" ); } @Test public void test2 () { Employee emp = new Employee (1001 , "Tom" , 23 , 5600 ); Supplier<String> sup1 = () -> emp.getName(); System.out.println(sup1.get()); Supplier<String> sup2 = emp::getName; System.out.println(sup2.get()); } @Test public void test3 () { Comparator<Integer> com1 = (t1, t2) -> Integer.compare(t1, t2); System.out.println(com1.compare(12 , 3 )); Comparator<Integer> com2 = Integer::compare; System.out.println(com2.compare(12 , 3 )); } @Test public void test4 () { Function<Double, Long> func1 = d -> Math.round(d); System.out.println(func1.apply(2.4 )); Function<Double, Long> func2 = Math::round; System.out.println(func2.apply(2.4 )); } @Test public void test5 () { Comparator<String> com1 = (t1, t2) -> t1.compareTo(t2); System.out.println(com1.compare("123" , "456" )); Comparator<String> com2 = String::compareTo; System.out.println(com2.compare("123" , "456" )); } @Test public void test6 () { BiPredicate<String, String> pre1 = (t1, t2) -> t1.equals(t2); System.out.println(pre1.test("123" , "123" )); BiPredicate<String, String> pre2 = String::equals; System.out.println(pre2.test("123" , "123" )); } @Test public void test7 () { Employee emp = new Employee (1001 , "Tom" , 23 , 5600 ); Function<Employee, String> func1 = e -> e.getName(); System.out.println(func1.apply(emp)); Function<Employee, String> func2 = Employee::getName; System.out.println(func2.apply(emp)); } }
2.3.2 构造器引用
Person::new
1 2 3 4 5 6 Function<Integer, Person> func2 = Person::new ; Person(int age) { this .age=age; }
**情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用某个类的构造器来new对象,而不执行其它操作 ,可以使用构造器引用
要求:函数式接口的抽象方法的参数列表与构造器的参数列表一致,且抽象方法的返回值即为构造器对应类的对象
格式 :使用操作符 ::
将类名与new分隔开来,整个表达式代表一个函数式接口的对象
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 类名::new 1 public class ConstructorRefTest { @Test public void test1 () { Supplier<Employee> sup = new Supplier <Employee>() { @Override public Employee get () { return new Employee (); } }; Supplier<Employee> sup1 = () -> new Employee (); Supplier<Employee> sup2 = Employee::new ; } @Test public void test2 () { Function<Integer, Employee> func1 = id -> new Employee (id); Function<Integer, Employee> func2 = Employee::new ; System.out.println(func1.apply(1001 ).getId()); } @Test void FunctionTest () { Function<Integer, Person> func = (i) -> new Person (i); Function<Integer, Person> func2 = Person::new ; } @Test public void test3 () { BiFunction<Integer, String, Employee> func1 = (id, name) -> new Employee (id, name); BiFunction<Integer, String, Employee> func2 = Employee::new ; System.out.println(func2.apply(1001 , "Tom" )); } }
2.3.3 数组引用
格式 :Xxx[]::new
可以将数组看成是一个特殊的类,则数组引用的写法与构造器引用类似
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @Test public void test4 () { Function<Integer, String[]> func1 = length -> new String [length]; String[] arr1 = func1.apply(5 ); System.out.println(Arrays.toString(arr1)); Function<Integer, String[]> func2 = String[]::new ; String[] arr2 = func2.apply(5 ); System.out.println(Arrays.toString(arr2)); }
2.4 访问变量
1 2 3 4 int i = 0 ;Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i); 123
lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。
四、Stream
1. Stream API的概述
Stream 关注的是对数据的运算,与CPU打交道;Collection 关注的是数据的存储,与内存打交道
java 新增了 java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如 java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何增删改查
Stream
是数据渠道,用于操作数据源(Collection
、Array
等)所生成的元素序列。它可以检索(Retrieve)和逻辑处理集合数据、包括过滤、切片、映射、排序、归约 等操作,类似 Sql 语句
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 的特性: - `Stream` 不保存数据 - `Stream` 不改变数据源。相反,他们会返回一个持有结果的新 `Stream` - 通过链式编程,使得 `Stream` 可以方便地对遍历处理后的数据进行再处理 - `Stream` 操作是**延迟执行**的,只有执行终止操作时,才会执行中间操作链,并返回结果 - 一个 `Stream` 只能经历一次 **实例化 —> 中间操作 —> 终止操作** 的过程,即当终止操作执行后该 `stream` 就关闭了,此时再通过这个 `stream` 调用方法会报异常 - `Stream` 的方法参数都是**函数式接口类型**,所以一般和 Lambda 配合使用 ### 2. 操作Stream的三个步骤 1. **实例化 `Stream` 类对象:**根据某个数据源(如 `Collection`、`Array` 等),获取一个流 2. **中间操作(链式编程):**一系列中间操作,声明如何对数据源的数据进行处理 3. **终止操作:**一旦执行终止操作,才会执行中间操作链,并返回结果。之后,不会再被使用 ![在这里插入图片描述](https://img-blog.csdnimg.cn/0f7d8f781f974b76b351c06add8e0275.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBATWVyc2lraXRv,size_20,color_FFFFFF,t_70,g_se,x_16#pic_center) ### 3. 实例化Stream对象 #### 3.1 方式一:通过集合 1. `default Stream<E> stream()`:返回一个**串行流(顺序流)** 2. `default Stream<E> parallelStream()`:返回一个**并行流** ```java @Test public void test1() { List<Employee> employees = EmployeeData.getEmployees(); //1. default Stream<E> stream():返回一个串行流(顺序流) Stream<Employee> stream = employees.stream(); //2. default Stream<E> parallelStream():返回一个并行流 Stream<Employee> employeeStream = employees.parallelStream(); }
3.2 方式二:通过数组
Java8 中 Arrays 工具类的静态方法 stream() 可以获取数组流:static <T> Stream<T> stream(T[] array)
1 2 3 4 5 6 7 8 9 10 11 @Test public void test2 () { int [] arr = new int []{1 , 2 , 3 }; IntStream stream = Arrays.stream(arr); Employee[] arr1 = new Employee []{new Employee (), new Employee ()}; Stream<Employee> stream1 = Arrays.stream(arr1); } 12345678910
3.3 方式三:通过Stream类的静态方法of()
Stream类的静态方法 of()
,通过显示值创建一个流,可以接收任意数量的参数:
public static<T> Stream<T> of(T... values)
1 2 3 4 5 6 @Test public void test3 () { Stream<Integer> integerStream = Stream.of(1 , 2 , 3 ); } 12345
3.4 方式四:创建无限流(造数据)
Stream类的静态方法 iterate()
或 generate()
,创建无限流:
迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)
生成:public static<T> Stream<T> generate(Supplier<T> s)
1 2 3 4 5 6 7 8 9 10 @Test public void test4 () { Stream.iterate(0 , t -> t + 2 ).limit(6 ).forEach(System.out::println); Stream.generate(Math::random).limit(6 ).forEach(System.out::println); } 123456789
4. Stream的中间操作
多个中间操作可以连接起来(链式编程 )形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理 ,称为**“惰性求值”**
中间操作涉及到的方法的返回值类型仍为 Stream
4.1 筛选与切片
Stream<T> filter(Predicate<? super T> predicate)
:从流中筛选某些元素,保留满足指定判定条件的元素
Stream<T> limit(long maxSize)
:截断流,返回一个元素数量不超过maxSize的流,按照数据源的顺序截断
Stream<T> skip(long n)
:返回一个跳过了前n个元素的流。若原始流中的元素不足n个,则返回一个空流。与limit(n)互补
Stream<T> distinct()
:通过流所生成元素的 hashCode()
和 equals()
去除重复元素,返回一个没有重复元素的流
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 @Test public void test1 () { List<Employee> list = EmployeeData.getEmployees(); Stream<Employee> stream = list.stream(); stream.filter(e -> e.getSalary() > 7000 ).forEach(System.out::println); list.stream().limit(3 ).forEach(System.out::println); list.stream().skip(3 ).forEach(System.out::println); list.stream().distinct().forEach(System.out::println); }
4.2 映射
<R> Stream<R> map(Function<? super T, ? extends R> mapper)
:接收一个函数(通常写成Lambda表达式)作为参数,该函数会被应用到流的每个元素上,映射成新的元素。返回映射操作后的流
<R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)
:接收一个函数(通常写成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 33 34 35 36 37 @Test public void tese2 () { List<String> list = Arrays.asList("aa" , "bb" , "cc" , "dd" ); list.stream().map(str -> str.toUpperCase()).forEach(System.out::println); List<Employee> employees = EmployeeData.getEmployees(); employees.stream().map(Employee::getName).filter(name -> name.length() > 3 ).forEach(System.out::println); Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest1::fromStringToStream); streamStream.forEach(s -> s.forEach(System.out::println)); Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream); characterStream.forEach(System.out::println); } public static Stream<Character> fromStringToStream (String str) { ArrayList<Character> list = new ArrayList <>(); for (Character c : str.toCharArray()) { list.add(c); } return list.stream(); }
4.3 排序
Stream<T> sorted()
:返回一个新的流,其中元素按自然顺序排序(自然排序)。元素所在类必须实现Comparable接口
Stream<T> sorted(Comparator com)
:返回一个新的流,其中元素按定制顺序排序(定制排序)。需要传入Comparator接口的实现类对象(使用Lambda表达式)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Test public void sortTest () { List<Integer> list = Arrays.asList(12 , 43 , 65 , 34 , 87 , 0 , -98 , 7 ); List<Person> pers = List.of(Person.persons); list.stream().sorted().forEach(System.out::println); pers.stream().sorted((e1, e2) -> { int ageValue = Integer.compare(e1.getAge(), e2.getAge()); if (ageValue != 0 ) return ageValue; else return e1.getName().compareTo(e2.getName()); }).forEach(System.out::println); }
5. Stream的终止操作
终止操作会从流的流水线生成结果
终止操作涉及到的方法的返回值类型可以是任何不是 Stream
的类型 ,例如:List、Integer,甚至是 void
流进行了终止操作后,不能再次使用
5.1 匹配与查找
boolean allMatch(Predicate<? super T> predicate)
:检查当前流中是否所有元素都满足指定判定条件
boolean anyMatch(Predicate<? super T> predicate)
:检查当前流中是否至少有一个元素满足指定判定条件
boolean noneMatch(Predicate<? super T> predicate)
:检查当前流中是否没有元素满足指定判定条件
Optional<T> findFirst()
:返回当前流的第一个元素
Optional<T> findAny()
:返回当前流中的任意元素
long count()
:返回当前流中元素总数
Optional<T> max(Comparator<? super T> comparator)
:返回当前流中按照定制排序顺序的最大元素。需要传入Comparator接口的实现类对象(使用Lambda表达式)
Optional<T> min(Comparator<? super T> comparator)
:返回当前流中按照定制排序顺序的最小元素。需要传入Comparator接口的实现类对象(使用Lambda表达式)
void forEach(Consumer<? super T> action)
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 54 55 56 57 58 :内部迭代 - 使用 Collection 接口需要自行使用迭代器迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了 ```java @Test public void test1() { List<Employee> employees = EmployeeData.getEmployees(); //1. boolean allMatch(Predicate<? super T> predicate):检查是否所有元素都满足指定判定条件 //练习1:是否所有员工的年龄都大于18岁 boolean allMatch = employees.stream().allMatch(employee -> employee.getAge() > 18); System.out.println(allMatch); //false //2. boolean anyMatch(Predicate<? super T> predicate):检查是否至少有一个元素满足指定判定条件 //练习2:是否存在员工的工资大于10000 boolean anyMatch = employees.stream().anyMatch(employee -> employee.getSalary() > 10000); System.out.println(anyMatch); //false //3. boolean noneMatch(Predicate<? super T> predicate):检查是否没有元素满足指定判定条件 //练习3:是否没有员工姓雷 boolean noneMatch = employees.stream().noneMatch(employee -> employee.getName().startsWith("雷")); System.out.println(noneMatch); //false //4. Optional<T> findFirst():返回第一个元素 //练习4:返回年龄最小的员工的信息 Optional<Employee> first = employees.stream() .sorted((employee1, employee2) -> Integer.compare(employee1.getAge(), employee2.getAge())).findFirst(); System.out.println(first); //5. Optional<T> findAny():返回当前流中的任意元素 Optional<Employee> any = employees.parallelStream().findAny(); System.out.println(any); //6. long count():返回当前流中元素总数 //练习5:返回工资大于5000的员工个数 long count = employees.stream().filter(employee -> employee.getSalary() > 5000).count(); System.out.println(count); //7.Optional<T> max(Comparator<? super T> comparator):返回当前流中按照定制排序顺序的最大元素 //练习6:返回所有员工的最高工资 Optional<Double> maxSalary = employees.stream().map(employee -> employee.getSalary()) .max((s1, s2) -> Double.compare(s1, s2)); System.out.println(maxSalary); //8. Optional<T> min(Comparator<? super T> comparator):返回当前流中按照定制排序顺序的最小元素 //练习7:返回工资最低的员工 Optional<Employee> min = employees.stream() .min((employee1, employee2) -> Double.compare(employee1.getSalary(), employee2.getSalary())); System.out.println(min); //9. void forEach(Consumer<? super T> action):内部迭代 employees.stream().forEach(System.out::println); //使用集合的遍历操作 employees.forEach(System.out::println); }
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 @Test void test1 () { List<Person> pers = List.of(Person.persons); pers.forEach(System.out::println); System.out.println(pers.stream().allMatch(e -> e.getAge() > 23 )); System.out.println(pers.stream().anyMatch(e -> e.getAge() > 30 )); System.out.println(pers.stream().noneMatch(e -> e.getAge() > 30 )); System.out.println(pers.stream().noneMatch(e -> e.getName().startsWith("董" ))); Optional<Person> op = pers.stream().findFirst(); System.out.println(op); } @Test void t2 () { List<Person> pers = List.of(Person.persons); long cnt = pers.stream().filter(e -> e.getAge() > 20 ).count(); Stream<Integer> stream2 = pers.stream().map(Person::getAge); Optional maxAge = stream2.max(Integer::compare); System.out.println(maxAge); Optional<Person> minAgePerson = pers.stream().min((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())); System.out.println("数据中年龄最小的人 :" + minAgePerson); }
5.2 归约
T reduce(T identity, BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为泛型类型 T
。identity表示初始值
Optional<T> reduce(BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为Optional<T>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test public void test2 () { List<Integer> list = Arrays.asList(1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ); Integer sum = list.stream().reduce(0 , Integer::sum); System.out.println(sum); List<Employee> employees = EmployeeData.getEmployees(); Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce((s1, s2) -> s1 + s2); System.out.println(salarySum); }
Mine
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test void test3 () { List<Integer> list = Arrays.asList(1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 , 9 , 10 ); Integer sum = list.stream().reduce(0 , Integer::sum); System.out.println(sum); List<Person> pers = List.of(Person.persons); Stream<Integer> AgeStream = pers.stream().map(Person::getAge); var sumAge = AgeStream.reduce(0 , Integer::sum); System.out.println("员工的总年龄" + sumAge); Optional ageOptional = pers.stream().map(Person::getAge).reduce((p1, p2) -> p1 + p2); System.out.println("员工的总年龄" + ageOptional); }
5.3 收集
<R, A> R collect(Collector<? super T, A, R> collector)
:将流转换为其他形式。接收一个 Collector 接口的实现类对象,用于给Stream中元素做汇总的方法
Collector 接口中方法的实现决定了如何对流执行收集的操作(如收集到 List、 Set、Map)
另外, Collectors 工具类中提供了很多静态方法,可以方便地创建常见收集器实例,常见的有:Collectors.toList()
、Collectors.toSet()
、Collectors.toCollection()
等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Test void test4 () { List<Person> pers = List.of(Person.persons); List<Person> list = pers.stream().filter((e) -> e.getAge() > 23 ).collect(Collectors.toList()); list.forEach(System.out::println); System.out.println("Set*******************" ); Set<Person> set = pers.stream().filter((e) -> e.getAge() > 23 ).collect(Collectors.toSet()); set.forEach(System.out::println); System.out.println("Collection*******************" ); Collection<Person> coll = pers.stream().filter((e) -> e.getAge() > 23 ).collect(Collectors.toList()); set.forEach(System.out::println); }
6. Stream的延迟执行
**在执行中间操作(返回值类型为 Stream
的方法)时,并不立即执行,而是等执行终止操作(返回值类型为非 Stream
的方法)后才执行。**因为拿到 Stream
并不能直接用,而是需要处理成一个常规类型。这里的 Stream
可以想象成是二进制流,拿到也看不懂。
我们下面分解一下 filter
方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 @Test public void laziness () { List<String> strings = Arrays.asList("abc" , "def" , "gkh" , "abc" ); Stream<Integer> stream = strings.stream().filter(new Predicate () { @Override public boolean test (Object o) { System.out.println("Predicate.test 执行" ); return true ; } }); System.out.println("count 执行" ); stream.count(); } count 执行 Predicate.test 执行 Predicate.test 执行 Predicate.test 执行 Predicate.test 执行
按执行顺序应该是先打印 4 次「Predicate.test
执行」,再打印「count
执行」。实际结果恰恰相反。说明 filter 中的方法并没有立刻执行,而是等调用count()
方法后才执行。
上面都是串行 Stream
的实例。并行 parallelStream
在使用方法上和串行一样。主要区别是 parallelStream
可多线程执行,是基于 ForkJoin
框架实现的,有时间大家可以了解一下 ForkJoin
框架和 ForkJoinPool
。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。
1 2 3 4 5 6 7 8 9 10 11 @Test public void parallelStreamTest () { List<Integer> numbers = Arrays.asList(1 , 2 , 5 , 4 ); numbers.parallelStream() .forEach(num->System.out.println(Thread.currentThread().getName()+">>" +num)); } main>>5 ForkJoinPool.commonPool-worker-2 >>4 ForkJoinPool.commonPool-worker-11 >>1 ForkJoinPool.commonPool-worker-9 >>2
从结果中我们看到,forEach
用到的是多线程。
五、Optional
在阿里巴巴开发手册关于 Optional 的介绍 中这样写到:
防止 NPE,是程序员的基本修养,注意 NPE 产生的场景:
1) 返回类型为基本数据类型,return 包装数据类型的对象时,自动拆箱有可能产生 NPE。
反例:public int f() { return Integer 对象}, 如果为 null,自动拆箱抛 NPE。
2) 数据库的查询结果可能为 null。
3) 集合里的元素即使 isNotEmpty,取出的数据元素也可能为 null。
4) 远程调用返回对象时,一律要求进行空指针判断,防止 NPE。
5) 对于 Session 中获取的数据,建议进行 NPE 检查,避免空指针。
6) 级联调用 obj.getA().getB().getC();一连串调用,易产生 NPE。
正例:使用 JDK8 的 Optional 类来防止 NPE 问题。
他建议使用 Optional
解决 NPE(java.lang.NullPointerException
)问题,它就是为 NPE 而生的 ,其中可以包含空值或非空值。下面我们通过源码逐步揭开 Optional
的红盖头。
假设有一个 Zoo
类,里面有个属性 Dog
,需求要获取 Dog
的 age
。
1 2 3 4 5 6 7 8 class Zoo { private Dog dog; } class Dog { private int age; }
传统解决 NPE 的办法如下:
1 2 3 4 5 6 7 8 9 Zoo zoo = getZoo();if (zoo != null ){ Dog dog = zoo.getDog(); if (dog != null ){ int age = dog.getAge(); System.out.println(age); } }
层层判断对象非空,有人说这种方式很丑陋不优雅,我并不这么认为。反而觉得很整洁,易读,易懂。你们觉得呢?
Optional
是这样的实现的:
1 2 3 4 Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).ifPresent(age -> System.out.println(age) );
是不是简洁了很多呢?
1. 理解Optional是一个容器类
Optional<T>
类(java.util.Optional) 是一个容器类,它可以保存类型T的值,代表这个值存在。或者仅仅保存null,表示这个值不存在。原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常
2. 如何创建一个 Optional 对象
上例中Optional.ofNullable
是其中一种创建 Optional 对象的静态方法。我们先看一下它的含义和其他创建 Optional 的源码方法。
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 private static final Optional<?> EMPTY = new Optional <>();private final T value;public static <T> Optional<T> ofNullable (T value) { return value == null ? empty() : of(value); } public static <T> Optional<T> empty () { Optional<T> t = (Optional<T>) EMPTY; return t; } public static <T> Optional<T> of (T value) { return new Optional <>(value); } private Optional (T value) { this .value = Objects.requireNonNull(value); } public static <T> T requireNonNull (T obj) { if (obj == null ) throw new NullPointerException (); return obj; }
ofNullable
方法和of
方法唯一区别就是当 value 为 null 时,ofNullable
返回的是EMPTY
,of 会抛出 NullPointerException
异常。如果需要把 NullPointerException
暴漏出来就用 of
,否则就用 ofNullable
。
1 2 3 4 5 6 7 @Test void test5 () { Person p1 = new Person (17 , "Tom" ); p1 = null ; Optional<Person> optionalPerson = Optional.ofNullable(p1); System.out.println(optionalPerson); }
3. map()相关方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public <U> Optional<U> map (Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } } 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()
和 flatMap()
有什么区别的?
1.参数不一样,map
的参数上面看到过,flatMap
的参数是这样
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class ZooFlat { private DogFlat dog = new DogFlat (); public DogFlat getDog () { return dog; } } class DogFlat { private int age = 1 ; public Optional<Integer> getAge () { return Optional.ofNullable(age); } } ZooFlat zooFlat = new ZooFlat ();Optional.ofNullable(zooFlat).map(o -> o.getDog()).flatMap(d -> d.getAge()).ifPresent(age -> System.out.println(age) );
2.flatMap()
参数返回值如果是 null 会抛 NullPointerException
,而 map()
返回EMPTY
。
4. 判断 value 是否为 null
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public boolean isPresent () { return value != null ; } public void ifPresent (Consumer<? super T> consumer) { if (value != null ) consumer.accept(value); }
5. 获取Optional容器中的对象 value
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 public T orElseGet (Supplier<? extends T> other) { return value != null ? value : other.get(); } public T orElse (T other) { return value != null ? value : other; } public <X extends Throwable > T orElseThrow (Supplier<? extends X> exceptionSupplier) throws X { if (value != null ) { return value; } else { throw exceptionSupplier.get(); } } public T get () { if (value == null ) { throw new NoSuchElementException ("No value present" ); } return value; }
6. 过滤值
1 2 3 4 5 6 7 8 9 10 11 12 public Optional<T> filter (Predicate<? super T> predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this ; else return predicate.test(value) ? this : empty(); }
7. 小结
看完 Optional
源码,Optional
的方法真的非常简单,值得注意的是如果坚决不想看见 NullPointerException
,就不要用 of()
、 get()
、flatMap(..)
。最后再综合用一下 Optional
的高频方法。
1 2 Optional.ofNullable(zoo).map(o -> o.getDog()).map(d -> d.getAge()).filter(v->v==1 ).orElse(3 );
六、Date-Time API
这是对java.util.Date
强有力的补充,解决了 Date 类的大部分痛点:
非线程安全
时区处理麻烦
各种格式化、和时间计算繁琐
设计有缺陷,Date 类同时包含日期和时间;还有一个 java.sql.Date,容易混淆。
我们从常用的时间实例来对比 java.util.Date 和新 Date 有什么区别。用java.util.Date
的代码该改改了。
1. java.time 主要类
java.util.Date
既包含日期又包含时间,而 java.time
把它们进行了分离
1 2 3 4 LocalDateTime.class LocalDate.class LocalTime.class
2. 格式化
Java 8 之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public void oldFormat () { Date now = new Date (); SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" ); String date = sdf.format(now); System.out.println(String.format("date format : %s" , date)); SimpleDateFormat sdft = new SimpleDateFormat ("HH:mm:ss" ); String time = sdft.format(now); System.out.println(String.format("time format : %s" , time)); SimpleDateFormat sdfdt = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" ); String datetime = sdfdt.format(now); System.out.println(String.format("dateTime format : %s" , datetime)); }
Java 8 之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void newFormat () { LocalDate date = LocalDate.now(); System.out.println(String.format("date format : %s" , date)); LocalTime time = LocalTime.now().withNano(0 ); System.out.println(String.format("time format : %s" , time)); LocalDateTime dateTime = LocalDateTime.now(); DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss" ); String dateTimeStr = dateTime.format(dateTimeFormatter); System.out.println(String.format("dateTime format : %s" , dateTimeStr)); }
3. 字符串转日期格式
Java 8 之前:
1 2 3 4 5 6 Date date = new Date ("2021-01-26" );SimpleDateFormat sdf = new SimpleDateFormat ("yyyy-MM-dd" );Date date1 = sdf.parse("2021-01-26" );
Java 8 之后:
1 2 3 4 5 6 7 8 9 LocalDate date = LocalDate.of(2021 , 1 , 26 );LocalDate.parse("2021-01-26" ); LocalDateTime dateTime = LocalDateTime.of(2021 , 1 , 26 , 12 , 12 , 22 );LocalDateTime.parse("2021-01-26 12:12:22" ); LocalTime time = LocalTime.of(12 , 12 , 22 );LocalTime.parse("12:12:22" );
Java 8 之前 转换都需要借助 SimpleDateFormat
类,而Java 8 之后 只需要 LocalDate
、LocalTime
、LocalDateTime
的 of
或 parse
方法。
4. 日期计算
下面仅以一周后日期 为例,其他单位(年、月、日、1/2 日、时等等)大同小异。另外,这些单位都在 java.time.temporal.ChronoUnit 枚举中定义。
Java 8 之前:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public void afterDay () { SimpleDateFormat formatDate = new SimpleDateFormat ("yyyy-MM-dd" ); Calendar ca = Calendar.getInstance(); ca.add(Calendar.DATE, 7 ); Date d = ca.getTime(); String after = formatDate.format(d); System.out.println("一周后日期:" + after); String dates1 = "2021-12-23" ; String dates2 = "2021-02-26" ; SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd" ); Date date1 = format.parse(dates1); Date date2 = format.parse(dates2); int day = (int ) ((date1.getTime() - date2.getTime()) / (1000 * 3600 * 24 )); System.out.println(dates2 + "和" + dates2 + "相差" + day + "天" ); }
Java 8 之后:
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 public void pushWeek () { LocalDate localDate = LocalDate.now(); LocalDate after = localDate.plus(1 , ChronoUnit.WEEKS); LocalDate after2 = localDate.plusWeeks(1 ); System.out.println("一周后日期:" + after); LocalDate date1 = LocalDate.parse("2021-02-26" ); LocalDate date2 = LocalDate.parse("2021-12-23" ); Period period = Period.between(date1, date2); System.out.println("date1 到 date2 相隔:" + period.getYears() + "年" + period.getMonths() + "月" + period.getDays() + "天" ); long day = date2.toEpochDay() - date1.toEpochDay(); System.out.println(date2 + "和" + date2 + "相差" + day + "天" ); }
5. 获取指定日期
除了日期计算繁琐,获取特定一个日期也很麻烦,比如获取本月最后一天,第一天。
Java 8 之前:
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 void getDay () { SimpleDateFormat format = new SimpleDateFormat ("yyyy-MM-dd" ); Calendar c = Calendar.getInstance(); c.set(Calendar.DAY_OF_MONTH, 1 ); String first = format.format(c.getTime()); System.out.println("first day:" + first); Calendar ca = Calendar.getInstance(); ca.set(Calendar.DAY_OF_MONTH, ca.getActualMaximum(Calendar.DAY_OF_MONTH)); String last = format.format(ca.getTime()); System.out.println("last day:" + last); Calendar currCal = Calendar.getInstance(); Calendar calendar = Calendar.getInstance(); calendar.clear(); calendar.set(Calendar.YEAR, currCal.get(Calendar.YEAR)); calendar.roll(Calendar.DAY_OF_YEAR, -1 ); Date time = calendar.getTime(); System.out.println("last day:" + format.format(time));
Java 8 之后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void getDayNew () { LocalDate today = LocalDate.now(); LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth()); LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth()); LocalDate nextDay = lastDayOfThisMonth.plusDays(1 ); LocalDate lastday = today.with(TemporalAdjusters.lastDayOfYear()); LocalDate lastMondayOf2021 = LocalDate.parse("2021-12-31" ).with(TemporalAdjusters.lastInMonth(DayOfWeek.SUNDAY)); }
java.time.temporal.TemporalAdjusters
里面还有很多便捷的算法,这里就不带大家看 Api 了,都很简单,看了秒懂。
6. JDBC 和 java8
现在 jdbc 时间类型和 java8 时间类型对应关系是
Date
—> LocalDate
Time
—> LocalTime
Timestamp
—> LocalDateTime
而之前统统对应 Date
,也只有 Date
。
7. 时区
时区:正式的时区划分为每隔经度 15° 划分一个时区,全球共 24 个时区,每个时区相差 1 小时。但为了行政上的方便,常将 1 个国家或 1 个省份划在一起,比如我国幅员宽广,大概横跨 5 个时区,实际上只用东八时区的标准时即北京时间为准。
java.util.Date
对象实质上存的是 1970 年 1 月 1 日 0 点( GMT)至 Date 对象所表示时刻所经过的毫秒数。也就是说不管在哪个时区 new Date,它记录的毫秒数都一样,和时区无关。但在使用上应该把它转换成当地时间,这就涉及到了时间的国际化。java.util.Date
本身并不支持国际化,需要借助 TimeZone
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 Date date = new Date ();SimpleDateFormat bjSdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );bjSdf.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai" )); System.out.println("毫秒数:" + date.getTime() + ", 北京时间:" + bjSdf.format(date)); SimpleDateFormat tokyoSdf = new SimpleDateFormat ("yyyy-MM-dd HH:mm:ss" );tokyoSdf.setTimeZone(TimeZone.getTimeZone("Asia/Tokyo" )); System.out.println("毫秒数:" + date.getTime() + ", 东京时间:" + tokyoSdf.format(date)); System.out.println(date);
在新特性中引入了 java.time.ZonedDateTime
来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ZonedDateTime zonedDateTime = ZonedDateTime.now();System.out.println("当前时区时间: " + zonedDateTime); ZoneId zoneId = ZoneId.of(ZoneId.SHORT_IDS.get("JST" ));ZonedDateTime tokyoTime = zonedDateTime.withZoneSameInstant(zoneId);System.out.println("东京时间: " + tokyoTime); LocalDateTime localDateTime = tokyoTime.toLocalDateTime();System.out.println("东京时间转当地时间: " + localDateTime); ZonedDateTime localZoned = localDateTime.atZone(ZoneId.systemDefault());System.out.println("本地时区时间: " + localZoned); 当前时区时间: 2021 -01 -27T14:43 :58.735 +08:00 [Asia/Shanghai] 东京时间: 2021 -01 -27T15:43 :58.735 +09:00 [Asia/Tokyo] 东京时间转当地时间: 2021 -01 -27T15:43 :58.735 当地时区时间: 2021 -01 -27T15:53 :35.618 +08:00 [Asia/Shanghai]
8. 小结
通过上面比较新老 Date
的不同,当然只列出部分功能上的区别,更多功能还得自己去挖掘。总之 date-time-api 给日期操作带来了福利。在日常工作中遇到 date 类型的操作,第一考虑的是 date-time-api,实在解决不了再考虑老的 Date。