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. **终止操作:**一旦执行终止操作,才会执行中间操作链,并返回结果。之后,不会再被使用  ### 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。