Java基础学习——第十六章 Java8新特性
Java基础学习——第十六章 Java8 新特性
Java8(JDK8.0)较 JDK7.0 有很多变化或者说是优化,比如 interface 里可以有静态方法和默认方法,并且可以有方法体,这一点就颠覆了之前的认知;java.util.HashMap
数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等
一、Interface
interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。
为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。
default
修饰的方法,是普通实例方法,可以用this
调用,可以被子类继承、重写。static
修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface
调用。
我们来看一个实际的例子。
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()
。不然的话,编译的时候就会报错。
public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{public static void main(String[] args) {InterfaceNewImpl interfaceNew = new InterfaceNewImpl();interfaceNew.def();}@Overridepublic void def() {InterfaceNew1.super.def();}@Overridepublic 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 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中
Lambda 表达式的本质:整个 Lambda 表达式代表一个函数式接口的对象
使用情境:当需要对一个函数式接口实例化时,或方法形参中需要函数式接口的对象时,我们使用Lambda表达式来代替匿名实现类
1. 语法格式
- **
->
:**Lambda 操作符或箭头操作符 - 左侧:Lambda 表达式的形参列表——接口中抽象方法的形参列表
- **右侧:**Lambda 体——实现的抽象方法的方法体,也即 Lambda 表达式要执行的功能
(parameters) -> {statements;}; 或
(parameters) -> expression;
语法格式一:抽象方法没有形参,无返回值
Runnable r = () -> {System.out.println("It's a lambda function!");};
语法格式二:抽象方法有一个参数,无返回值
Consumer<String> con = (String str) -> {System.out.println(str);};
语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
Consumer<String> con = (str) -> {System.out.println(str);};
语法格式四:Lambda 操作符左边若只有一个参数时, 参数的小括号可以省略
Consumer<String> con = str -> {System.out.println(str);};
语法格式五:抽象方法需要两个或以上的参数,多条执行语句,并且可以有返回值(标准写法)
Comparator<Integer> com = (o1, o2) -> {System.out.println("实现函数式接口方法");return Integer.compare(o1, o2);
};
语法格式六:当 Lambda 体只有一条执行语句(可能是 return 语句), return与大括号都可以省略
Comparator<Integer> com = (o1, o2) -> Integer.compare(o1, o2);
1.1 总结
- **左侧:**① Lambda 表达式的形参列表的参数类型可以省略(类型推断);② 如果只有一个形参,则小括号可以省略
- **右侧:**如果 Lambda 体只有一条执行语句(可能是 return 语句),则 return 与大括号都可以省略
2. Lambda 实战
2.1 替代匿名内部类
过去给方法传动态参数的唯一方法是使用内部类(匿名实现类)。比如
1.Runnable
接口
//Runnable接口的匿名实现类的匿名对象
//不使用lambda表达式
new Thread(new Runnable() {@Overridepublic void run() {System.out.println("The runable now is using!");}
}).start();//使用lambda表达式
new Thread(() -> System.out.println("It's a lambda function!")).start();
2.Comparator
接口
List<Integer> list = Arrays.asList(1, 2, 3);//不使用lambda表达式
Collections.sort(list, new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return Integer.compare(o1, o2);}
});//使用lambda表达式
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);
3.Listener
接口
JButton button = new JButton();//不使用lambda表达式
button.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {e.getItem();
}
});//lambda
button.addItemListener(e -> e.getItem());
4.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口,也就是说只要方法的参数是函数式接口的对象,就可以使用 Lambda 表达式。
@FunctionalInterface
public interface Comparator<T>{}@FunctionalInterface
public interface Runnable{}
我们自定义一个函数式接口
@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()方法
void lamndaFor() {List<String> strings = Arrays.asList("1", "2", "3");//传统foreachfor (String s : strings) {System.out.println(s);}//Collection:public void forEach(Consumer<? super E> action)//lambdastrings.forEach((s) -> System.out.println(s));//方法引用strings.forEach(System.out::println);//map:default void forEach(BiConsumer<? super K, ? super V> action)Map<Integer, String> map = new HashMap<>();map.forEach((k,v)->System.out.println(v));
}
2.3 方法引用
2.3.1 方法引用
方法引用是一种更加紧凑,易读的 Lambda 表达式。即方法引用就是 Lambda 表达式,本质上代表一个函数式接口的对象
- **情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用了一个已存在的方法,而不执行任何其它操作,可以考虑使用方法引用
- 要求:函数式接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致(针对情况1和2)
- 格式:使用操作符
::
将类名(或对象引用)与方法名分隔开来,整个表达式代表一个函数式接口的对象- 注意:
::
右边的方法必须是左边的类或对象所在类中声明的方法
- 注意:
类名(或对象引用)::方法名
- 主要使用情况:
- 情况1:
对象引用::实例(非静态)方法
- 情况2:
类名::静态方法名
- 情况3:
类名::实例(非静态)方法
- 情况1:
public class MethodRefTest {//情况一:对象引用::实例(非静态)方法//Consumer<T>中的void accept(T t)//PrintStream中的void println(Xxx x)@Testpublic void test1() {//Lambda表达式Consumer<String> con1 = str -> System.out.println(str);con1.accept("Beijing");//方法引用Consumer<String> con2 = System.out::println;con2.accept("Shanghai");}//Supplier<T>中的T get()//Employee中的String getName()@Testpublic void test2() {Employee emp = new Employee(1001, "Tom", 23, 5600);//Lambda表达式Supplier<String> sup1 = () -> emp.getName();System.out.println(sup1.get());//方法引用Supplier<String> sup2 = emp::getName;System.out.println(sup2.get());}//情况二:类名::静态方法名//Comparator<T>中的int compare(T t1,T t2)//Integer中的int compare(T t1,T t2)@Testpublic void test3() {//Lambda表达式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));}//Function<T, R>中的R apply(T t)//Math中的Long round(Double d)@Testpublic void test4() {//Lambda表达式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));}//情况三:类名::实例(非静态)方法//Comparator<T>中的int compare(T t1,T t2)//String中的int t1.compareTo(t2)@Testpublic void test5() {//Lambda表达式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"));}//BiPredicate<T, U>中的boolean test(T t, U u);//String中的boolean t1.equals(t2)@Testpublic void test6() {//Lambda表达式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"));}//Function<T, R>中的R apply(T t)//Employee中的String getName();@Testpublic void test7() {Employee emp = new Employee(1001, "Tom", 23, 5600);//Lambda表达式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 构造器引用
-
**情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用某个类的构造器来new对象,而不执行其它操作,可以使用构造器引用
-
要求:函数式接口的抽象方法的参数列表与构造器的参数列表一致,且抽象方法的返回值即为构造器对应类的对象
-
格式:使用操作符
::
将类名与new分隔开来,整个表达式代表一个函数式接口的对象
类名::new
public class ConstructorRefTest {//构造器引用//Supplier<T>中的T get()@Testpublic void test1(){//匿名实现类Supplier<Employee> sup = new Supplier<Employee>() {@Overridepublic Employee get() {return new Employee();}};//Lambda表达式Supplier<Employee> sup1 = () -> new Employee();//构造器引用Supplier<Employee> sup2 = Employee::new;}//Function<T, R>中的R apply(T t)@Testpublic void test2(){//Lambda表达式Function<Integer, Employee> func1 = id -> new Employee(id);//构造器引用Function<Integer, Employee> func2 = Employee::new;System.out.println(func1.apply(1001).getId());}//BiFunction<T, U, R>中的R apply(T t,U u)@Testpublic void test3(){//Lambda表达式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
-
可以将数组看成是一个特殊的类,则数组引用的写法与构造器引用类似
public class ArrayRefTest {//数组引用//Function<T, R>中的R apply(T t)@Testpublic void test4() {//Lambda表达式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 访问变量
int i = 0;
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i);
//i =3;
lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。
四、Stream
1. Stream API的概述
Stream 关注的是对数据的运算,与CPU打交道;Collection 关注的是数据的存储,与内存打交道
- java 新增了
java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何增删改查 Stream
是数据渠道,用于操作数据源(Collection
、Array
等)所生成的元素序列。它可以检索(Retrieve)和逻辑处理集合数据、包括过滤、切片、映射、排序、归约等操作,类似 Sql 语句Stream
的特性:Stream
不保存数据Stream
不改变数据源。相反,他们会返回一个持有结果的新Stream
- 通过链式编程,使得
Stream
可以方便地对遍历处理后的数据进行再处理 Stream
操作是延迟执行的,只有执行终止操作时,才会执行中间操作链,并返回结果- 一个
Stream
只能经历一次 实例化 —> 中间操作 —> 终止操作 的过程,即当终止操作执行后该stream
就关闭了,此时再通过这个stream
调用方法会报异常 Stream
的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用
2. 操作Stream的三个步骤
- **实例化
Stream
类对象:**根据某个数据源(如Collection
、Array
等),获取一个流 - **中间操作(链式编程):**一系列中间操作,声明如何对数据源的数据进行处理
- **终止操作:**一旦执行终止操作,才会执行中间操作链,并返回结果。之后,不会再被使用
3. 实例化Stream对象
3.1 方式一:通过集合
default Stream<E> stream()
:返回一个串行流(顺序流)default Stream<E> parallelStream()
:返回一个并行流
@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)
@Test
public void test2() {int[] arr = new int[]{1, 2, 3};//static <T> Stream<T> stream(T[] array):返回一个流IntStream stream = Arrays.stream(arr);Employee[] arr1 = new Employee[]{new Employee(), new Employee()};Stream<Employee> stream1 = Arrays.stream(arr1);
}
3.3 方式三:通过Stream类的静态方法of()
-
Stream类的静态方法
of()
,通过显示值创建一个流,可以接收任意数量的参数:public static<T> Stream<T> of(T... values)
@Test
public void test3() {//public static<T> Stream<T> of(T... values):返回一个流Stream<Integer> integerStream = Stream.of(1, 2, 3);
}
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)
@Test
public void test4() {//1. 迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)//遍历前10个偶数Stream.iterate(0, t -> t + 2).limit(6).forEach(System.out::println);//2. 生成:public static<T> Stream<T> generate(Supplier<T> s)Stream.generate(Math::random).limit(6).forEach(System.out::println);
}
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()
去除重复元素,返回一个没有重复元素的流
@Test
public void test1() {List<Employee> list = EmployeeData.getEmployees();Stream<Employee> stream = list.stream();//1. Stream<T> filter(Predicate<? super T> predicate):从流中筛选某些元素,保留满足指定判定条件的元素//筛选员工表中薪资大于7000的员工信息// stream.filter(new Predicate<Employee>() {// @Override// public boolean test(Employee e) {// return e.getSalary() > 7000;// }// }).forEach(System.out::println);//也可以写成Lambda表达式stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println); //forEach()是一个终止操作//2. Stream<T> limit(long maxSize):截断流,返回一个元素数量不超过maxSize的流,按照数据源的顺序截断//一个 `Stream` 只能进行一次完整的操作,即当终止操作执行后就关闭了,此时再使用这个 `stream` 会报错//stream.limit(3).forEach(System.out::println);//此时应当重新创建一个Stream对象list.stream().limit(3).forEach(System.out::println);//3. Stream<T> skip(long n):返回一个跳过了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补list.stream().skip(3).forEach(System.out::println);//4. Stream<T> distinct():通过流所生成元素的 hashCode() 和 equals() 去除重复元素,返回一个没有重复元素的流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表达式)作为参数,将流中的每个元素都换成另一个流,然后把所有流连接成一个流
@Test
public void tese2() {List<String> list = Arrays.asList("aa", "bb", "cc", "dd");//1. <R> Stream<R> map(Function<? super T, ? extends R> mapper)//接收一个函数(通常写成Lambda表达式)作为参数,该函数会被应用到流的每个元素上,映射成新的元素。返回映射操作后的流list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);//练习1:获取姓名长度大于3的员工的姓名List<Employee> employees = EmployeeData.getEmployees();//先调用map()将流中所有元素(Employee对象)映射为对应的name属性,然后调用filter()筛选仅保留名字长度大于3的员工//Stream的中间操作采用链式编程,最后通过forEach()执行终止操作//Stream<String> nameStream = employees.stream().map(employee -> employee.getName());//nameStream.filter(name -> name.length() > 3).forEach(System.out::println);//写成链式,并使用方法引用employees.stream().map(Employee::getName).filter(name -> name.length() > 3).forEach(System.out::println);//练习2:将list中的每个字符串元素中的每个字符单独打印出来//使用map()Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest1::fromStringToStream); //Stream里面套了StreamstreamStream.forEach(s -> s.forEach(System.out::println));//2. <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)//接收一个函数(通常写成Lambda表达式)作为参数,将流中的每个元素都换成另一个流,然后把所有流连接成一个流Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream);characterStream.forEach(System.out::println);
}//将字符串中的多个字符构成的集合转换成对应的Stream对象
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表达式)
@Test
public void test4() {List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);List<Employee> employees = EmployeeData.getEmployees();//1. `Stream<T> sorted()`:返回一个新的流,其中元素按自然顺序排序(自然排序)。元素所在类必须实现Comparable接口//此处按照Integer内部定义的compareTo方法进行自然排序list.stream().sorted().forEach(System.out::println);//2. `Stream<T> sorted(Comparator com)`:返回一个新的流,其中元素按定制顺序排序(定制排序)employees.stream().sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).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)
:内部迭代- 使用 Collection 接口需要自行使用迭代器迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了
@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:是否存在员工的工资大于10000boolean 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);
}
5.2 归约
T reduce(T identity, BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为泛型类型T
。identity表示初始值Optional<T> reduce(BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为Optional<T>
@Test
public void test2() {//1. reduce(T iden, BinaryOperator b)//练习1:计算1-10的自然数的和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);//2. Optional<T> reduce(BinaryOperator<T> accumulator)//练习2:计算所有员工的工资总和List<Employee> employees = EmployeeData.getEmployees();//Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce(Double::sum);Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce((s1, s2) -> s1 + s2);System.out.println(salarySum);
}
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()
等
@Test
public void test3() {List<Employee> employees = EmployeeData.getEmployees();//<R, A> R collect(Collector<? super T, A, R> collector)://练习1:查找工资大于6000的员工,结果返回为一个List或SetList<Employee> employeeList = employees.stream().filter(employee -> employee.getSalary() > 6000).collect(Collectors.toList());employeeList.forEach(System.out::println);Set<Employee> employeeSet = employees.stream().filter(employee -> employee.getSalary() > 6000).collect(Collectors.toSet());employeeSet.forEach(System.out::println);
}
6. Stream的延迟执行
**在执行中间操作(返回值类型为 Stream
的方法)时,并不立即执行,而是等执行终止操作(返回值类型为非 Stream
的方法)后才执行。**因为拿到 Stream
并不能直接用,而是需要处理成一个常规类型。这里的 Stream
可以想象成是二进制流,拿到也看不懂。
我们下面分解一下 filter
方法。
@Test
public void laziness(){List<String> strings = Arrays.asList("abc", "def", "gkh", "abc");Stream<Integer> stream = strings.stream().filter(new Predicate() {@Overridepublic 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
。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。
@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
。
class Zoo {private Dog dog;
}class Dog {private int age;
}
传统解决 NPE 的办法如下:
Zoo zoo = getZoo();
if(zoo != null){Dog dog = zoo.getDog();if(dog != null){int age = dog.getAge();System.out.println(age);}
}
层层判断对象非空,有人说这种方式很丑陋不优雅,我并不这么认为。反而觉得很整洁,易读,易懂。你们觉得呢?
Optional
是这样的实现的:
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 的源码方法。
/**
* Common instance for {@code empty()}. 全局EMPTY对象
*/
private static final Optional<?> EMPTY = new Optional<>();/**
* Optional维护的值
*/
private final T value;/**
* 如果value是null就返回EMPTY,否则就返回of(T)
*/
public static <T> Optional<T> ofNullable(T value) {return value == null ? empty() : of(value);
}
/**
* 返回 EMPTY 对象
*/
public static<T> Optional<T> empty() {Optional<T> t = (Optional<T>) EMPTY;return t;
}
/**
* 返回Optional对象
*/
public static <T> Optional<T> of(T value) {return new Optional<>(value);
}
/**
* 私有构造方法,给value赋值
*/
private Optional(T value) {this.value = Objects.requireNonNull(value);
}
/**
* 所以如果of(T value) 的value是null,会抛出NullPointerException异常,这样貌似就没处理NPE问题
*/
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
。
3. map()相关方法
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值
*/
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));}
}
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值,如果参数值返回null会抛 NullPointerException
*/
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
的参数是这样
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
/**
* value是否为null
*/
public boolean isPresent() {return value != null;
}
/**
* 如果value不为null,执行consumer接口的实现类实现的accept()方法
*/
public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);
}
5. 获取Optional容器中的对象 value
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
* 如果value != null 返回value,否则返回other的执行结果
*/
public T orElseGet(Supplier<? extends T> other) {return value != null ? value : other.get();
}/**
* 如果value != null 返回value,否则返回T
*/
public T orElse(T other) {return value != null ? value : other;
}/**
* 如果value != null 返回value,否则抛出参数返回的异常
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {if (value != null) {return value;} else {throw exceptionSupplier.get();}
}
/**
* value为null抛出NoSuchElementException,不为空返回value。
*/
public T get() {if (value == null) {throw new NoSuchElementException("No value present");}return value;
}
6. 过滤值
/**
* 1. 如果是empty返回empty
* 2. predicate.test(value)==true 返回this,否则返回empty
*/
public Optional<T> filter(Predicate<? super T> predicate) {Objects.requireNonNull(predicate);if (!isPresent())return this;elsereturn predicate.test(value) ? this : empty();
}
7. 小结
看完 Optional
源码,Optional
的方法真的非常简单,值得注意的是如果坚决不想看见 NPE
,就不要用 of()
、 get()
、flatMap(..)
。最后再综合用一下 Optional
的高频方法。
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
把它们进行了分离
LocalDateTime.class //日期+时间 format: yyyy-MM-ddTHH:mm:ss.SSS
LocalDate.class //日期 format: yyyy-MM-dd
LocalTime.class //时间 format: HH:mm:ss
2. 格式化
Java 8 之前:
public void oldFormat(){Date now = new Date();//format yyyy-MM-dd HH:mm:ssSimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");String date = sdf.format(now);System.out.println(String.format("date format : %s", date));//format HH:mm:ssSimpleDateFormat sdft = new SimpleDateFormat("HH:mm:ss");String time = sdft.format(now);System.out.println(String.format("time format : %s", time));//format yyyy-MM-dd HH:mm:ssSimpleDateFormat 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 之后:
public void newFormat(){//format yyyy-MM-ddLocalDate date = LocalDate.now();System.out.println(String.format("date format : %s", date));//format HH:mm:ssLocalTime time = LocalTime.now().withNano(0);System.out.println(String.format("time format : %s", time));//format yyyy-MM-dd HH:mm:ssLocalDateTime 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 之前:
//已弃用
Date date = new Date("2021-01-26");
//替换为
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = sdf.parse("2021-01-26");
Java 8 之后:
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 之前:
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 + "天");//结果:2021-12-23和2021-12-23相差300天
}
Java 8 之后:
public void pushWeek(){//一周后的日期LocalDate localDate = LocalDate.now();//方法1LocalDate after = localDate.plus(1, ChronoUnit.WEEKS);//方法2LocalDate 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() + "天");//打印结果是 “date1 到 date2 相隔:0年9月27天”//这里period.getDays()得到的天是抛去年月以外的天数,并不是总天数//如果要获取纯粹的总天数应该用下面的方法long day = date2.toEpochDay() - date1.toEpochDay();System.out.println(date2 + "和" + date2 + "相差" + day + "天");//打印结果:2021-12-23和2021-12-23相差300天
}
5. 获取指定日期
除了日期计算繁琐,获取特定一个日期也很麻烦,比如获取本月最后一天,第一天。
Java 8 之前:
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 之后:
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());//2021年最后一个周日,如果用Calendar是不得烦死。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
。
//北京时间:Wed Jan 27 14:05:29 CST 2021
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));//如果直接print会自动转成当前时区的时间
System.out.println(date);
//Wed Jan 27 14:05:29 CST 2021
在新特性中引入了 java.time.ZonedDateTime
来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId
。
//当前时区时间
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);// ZonedDateTime 转 LocalDateTime
LocalDateTime localDateTime = tokyoTime.toLocalDateTime();
System.out.println("东京时间转当地时间: " + localDateTime);//LocalDateTime 转 ZonedDateTime
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。
Java基础学习——第十六章 Java8新特性
Java基础学习——第十六章 Java8 新特性
Java8(JDK8.0)较 JDK7.0 有很多变化或者说是优化,比如 interface 里可以有静态方法和默认方法,并且可以有方法体,这一点就颠覆了之前的认知;java.util.HashMap
数据结构里增加了红黑树;还有众所周知的 Lambda 表达式等等
一、Interface
interface 的设计初衷是面向抽象,提高扩展性。这也留有一点遗憾,Interface 修改的时候,实现它的类也必须跟着改。
为了解决接口的修改与现有的实现不兼容的问题。新 interface 的方法可以用default
或 static
修饰,这样就可以有方法体,实现类也不必重写此方法。
一个 interface 中可以有多个方法被它们修饰,这 2 个修饰符的区别主要也是普通方法和静态方法的区别。
default
修饰的方法,是普通实例方法,可以用this
调用,可以被子类继承、重写。static
修饰的方法,使用上和一般类静态方法一样。但它不能被子类继承,只能用Interface
调用。
我们来看一个实际的例子。
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()
。不然的话,编译的时候就会报错。
public class InterfaceNewImpl implements InterfaceNew , InterfaceNew1{public static void main(String[] args) {InterfaceNewImpl interfaceNew = new InterfaceNewImpl();interfaceNew.def();}@Overridepublic void def() {InterfaceNew1.super.def();}@Overridepublic 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 表达式可以使代码变的更加简洁紧凑。让 java 也能支持简单的函数式编程。
Lambda 表达式是一个匿名函数,java 8 允许把函数作为参数传递进方法中
Lambda 表达式的本质:整个 Lambda 表达式代表一个函数式接口的对象
使用情境:当需要对一个函数式接口实例化时,或方法形参中需要函数式接口的对象时,我们使用Lambda表达式来代替匿名实现类
1. 语法格式
- **
->
:**Lambda 操作符或箭头操作符 - 左侧:Lambda 表达式的形参列表——接口中抽象方法的形参列表
- **右侧:**Lambda 体——实现的抽象方法的方法体,也即 Lambda 表达式要执行的功能
(parameters) -> {statements;}; 或
(parameters) -> expression;
语法格式一:抽象方法没有形参,无返回值
Runnable r = () -> {System.out.println("It's a lambda function!");};
语法格式二:抽象方法有一个参数,无返回值
Consumer<String> con = (String str) -> {System.out.println(str);};
语法格式三:数据类型可以省略,因为可由编译器推断得出,称为“类型推断”
Consumer<String> con = (str) -> {System.out.println(str);};
语法格式四:Lambda 操作符左边若只有一个参数时, 参数的小括号可以省略
Consumer<String> con = str -> {System.out.println(str);};
语法格式五:抽象方法需要两个或以上的参数,多条执行语句,并且可以有返回值(标准写法)
Comparator<Integer> com = (o1, o2) -> {System.out.println("实现函数式接口方法");return Integer.compare(o1, o2);
};
语法格式六:当 Lambda 体只有一条执行语句(可能是 return 语句), return与大括号都可以省略
Comparator<Integer> com = (o1, o2) -> Integer.compare(o1, o2);
1.1 总结
- **左侧:**① Lambda 表达式的形参列表的参数类型可以省略(类型推断);② 如果只有一个形参,则小括号可以省略
- **右侧:**如果 Lambda 体只有一条执行语句(可能是 return 语句),则 return 与大括号都可以省略
2. Lambda 实战
2.1 替代匿名内部类
过去给方法传动态参数的唯一方法是使用内部类(匿名实现类)。比如
1.Runnable
接口
//Runnable接口的匿名实现类的匿名对象
//不使用lambda表达式
new Thread(new Runnable() {@Overridepublic void run() {System.out.println("The runable now is using!");}
}).start();//使用lambda表达式
new Thread(() -> System.out.println("It's a lambda function!")).start();
2.Comparator
接口
List<Integer> list = Arrays.asList(1, 2, 3);//不使用lambda表达式
Collections.sort(list, new Comparator<Integer>() {@Overridepublic int compare(Integer o1, Integer o2) {return Integer.compare(o1, o2);}
});//使用lambda表达式
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);
3.Listener
接口
JButton button = new JButton();//不使用lambda表达式
button.addItemListener(new ItemListener() {
@Override
public void itemStateChanged(ItemEvent e) {e.getItem();
}
});//lambda
button.addItemListener(e -> e.getItem());
4.自定义接口
上面的 3 个例子是我们在开发过程中最常见的,从中也能体会到 Lambda 带来的便捷与清爽。它只保留实际用到的代码,把无用代码全部省略。那它对接口有没有要求呢?我们发现这些匿名内部类只重写了接口的一个方法,当然也只有一个方法须要重写。这就是我们上文提到的函数式接口,也就是说只要方法的参数是函数式接口的对象,就可以使用 Lambda 表达式。
@FunctionalInterface
public interface Comparator<T>{}@FunctionalInterface
public interface Runnable{}
我们自定义一个函数式接口
@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()方法
void lamndaFor() {List<String> strings = Arrays.asList("1", "2", "3");//传统foreachfor (String s : strings) {System.out.println(s);}//Collection:public void forEach(Consumer<? super E> action)//lambdastrings.forEach((s) -> System.out.println(s));//方法引用strings.forEach(System.out::println);//map:default void forEach(BiConsumer<? super K, ? super V> action)Map<Integer, String> map = new HashMap<>();map.forEach((k,v)->System.out.println(v));
}
2.3 方法引用
2.3.1 方法引用
方法引用是一种更加紧凑,易读的 Lambda 表达式。即方法引用就是 Lambda 表达式,本质上代表一个函数式接口的对象
- **情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用了一个已存在的方法,而不执行任何其它操作,可以考虑使用方法引用
- 要求:函数式接口的抽象方法的参数列表和返回值类型,必须与方法引用的方法的参数列表和返回值类型保持一致(针对情况1和2)
- 格式:使用操作符
::
将类名(或对象引用)与方法名分隔开来,整个表达式代表一个函数式接口的对象- 注意:
::
右边的方法必须是左边的类或对象所在类中声明的方法
- 注意:
类名(或对象引用)::方法名
- 主要使用情况:
- 情况1:
对象引用::实例(非静态)方法
- 情况2:
类名::静态方法名
- 情况3:
类名::实例(非静态)方法
- 情况1:
public class MethodRefTest {//情况一:对象引用::实例(非静态)方法//Consumer<T>中的void accept(T t)//PrintStream中的void println(Xxx x)@Testpublic void test1() {//Lambda表达式Consumer<String> con1 = str -> System.out.println(str);con1.accept("Beijing");//方法引用Consumer<String> con2 = System.out::println;con2.accept("Shanghai");}//Supplier<T>中的T get()//Employee中的String getName()@Testpublic void test2() {Employee emp = new Employee(1001, "Tom", 23, 5600);//Lambda表达式Supplier<String> sup1 = () -> emp.getName();System.out.println(sup1.get());//方法引用Supplier<String> sup2 = emp::getName;System.out.println(sup2.get());}//情况二:类名::静态方法名//Comparator<T>中的int compare(T t1,T t2)//Integer中的int compare(T t1,T t2)@Testpublic void test3() {//Lambda表达式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));}//Function<T, R>中的R apply(T t)//Math中的Long round(Double d)@Testpublic void test4() {//Lambda表达式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));}//情况三:类名::实例(非静态)方法//Comparator<T>中的int compare(T t1,T t2)//String中的int t1.compareTo(t2)@Testpublic void test5() {//Lambda表达式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"));}//BiPredicate<T, U>中的boolean test(T t, U u);//String中的boolean t1.equals(t2)@Testpublic void test6() {//Lambda表达式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"));}//Function<T, R>中的R apply(T t)//Employee中的String getName();@Testpublic void test7() {Employee emp = new Employee(1001, "Tom", 23, 5600);//Lambda表达式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 构造器引用
-
**情境:**对于某个 Lambda 表达式,当其 Lambda 体仅调用某个类的构造器来new对象,而不执行其它操作,可以使用构造器引用
-
要求:函数式接口的抽象方法的参数列表与构造器的参数列表一致,且抽象方法的返回值即为构造器对应类的对象
-
格式:使用操作符
::
将类名与new分隔开来,整个表达式代表一个函数式接口的对象
类名::new
public class ConstructorRefTest {//构造器引用//Supplier<T>中的T get()@Testpublic void test1(){//匿名实现类Supplier<Employee> sup = new Supplier<Employee>() {@Overridepublic Employee get() {return new Employee();}};//Lambda表达式Supplier<Employee> sup1 = () -> new Employee();//构造器引用Supplier<Employee> sup2 = Employee::new;}//Function<T, R>中的R apply(T t)@Testpublic void test2(){//Lambda表达式Function<Integer, Employee> func1 = id -> new Employee(id);//构造器引用Function<Integer, Employee> func2 = Employee::new;System.out.println(func1.apply(1001).getId());}//BiFunction<T, U, R>中的R apply(T t,U u)@Testpublic void test3(){//Lambda表达式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
-
可以将数组看成是一个特殊的类,则数组引用的写法与构造器引用类似
public class ArrayRefTest {//数组引用//Function<T, R>中的R apply(T t)@Testpublic void test4() {//Lambda表达式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 访问变量
int i = 0;
Collections.sort(strings, (Integer o1, Integer o2) -> o1 - i);
//i =3;
lambda 表达式可以引用外边变量,但是该变量默认拥有 final 属性,不能被修改,如果修改,编译时就报错。
四、Stream
1. Stream API的概述
Stream 关注的是对数据的运算,与CPU打交道;Collection 关注的是数据的存储,与内存打交道
- java 新增了
java.util.stream
包,它和之前的流大同小异。之前接触最多的是资源流,比如java.io.FileInputStream
,通过流把文件从一个地方输入到另一个地方,它只是内容搬运工,对文件内容不做任何增删改查 Stream
是数据渠道,用于操作数据源(Collection
、Array
等)所生成的元素序列。它可以检索(Retrieve)和逻辑处理集合数据、包括过滤、切片、映射、排序、归约等操作,类似 Sql 语句Stream
的特性:Stream
不保存数据Stream
不改变数据源。相反,他们会返回一个持有结果的新Stream
- 通过链式编程,使得
Stream
可以方便地对遍历处理后的数据进行再处理 Stream
操作是延迟执行的,只有执行终止操作时,才会执行中间操作链,并返回结果- 一个
Stream
只能经历一次 实例化 —> 中间操作 —> 终止操作 的过程,即当终止操作执行后该stream
就关闭了,此时再通过这个stream
调用方法会报异常 Stream
的方法参数都是函数式接口类型,所以一般和 Lambda 配合使用
2. 操作Stream的三个步骤
- **实例化
Stream
类对象:**根据某个数据源(如Collection
、Array
等),获取一个流 - **中间操作(链式编程):**一系列中间操作,声明如何对数据源的数据进行处理
- **终止操作:**一旦执行终止操作,才会执行中间操作链,并返回结果。之后,不会再被使用
3. 实例化Stream对象
3.1 方式一:通过集合
default Stream<E> stream()
:返回一个串行流(顺序流)default Stream<E> parallelStream()
:返回一个并行流
@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)
@Test
public void test2() {int[] arr = new int[]{1, 2, 3};//static <T> Stream<T> stream(T[] array):返回一个流IntStream stream = Arrays.stream(arr);Employee[] arr1 = new Employee[]{new Employee(), new Employee()};Stream<Employee> stream1 = Arrays.stream(arr1);
}
3.3 方式三:通过Stream类的静态方法of()
-
Stream类的静态方法
of()
,通过显示值创建一个流,可以接收任意数量的参数:public static<T> Stream<T> of(T... values)
@Test
public void test3() {//public static<T> Stream<T> of(T... values):返回一个流Stream<Integer> integerStream = Stream.of(1, 2, 3);
}
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)
@Test
public void test4() {//1. 迭代:public static<T> Stream<T> iterate(final T seed, final UnaryOperator<T> f)//遍历前10个偶数Stream.iterate(0, t -> t + 2).limit(6).forEach(System.out::println);//2. 生成:public static<T> Stream<T> generate(Supplier<T> s)Stream.generate(Math::random).limit(6).forEach(System.out::println);
}
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()
去除重复元素,返回一个没有重复元素的流
@Test
public void test1() {List<Employee> list = EmployeeData.getEmployees();Stream<Employee> stream = list.stream();//1. Stream<T> filter(Predicate<? super T> predicate):从流中筛选某些元素,保留满足指定判定条件的元素//筛选员工表中薪资大于7000的员工信息// stream.filter(new Predicate<Employee>() {// @Override// public boolean test(Employee e) {// return e.getSalary() > 7000;// }// }).forEach(System.out::println);//也可以写成Lambda表达式stream.filter(e -> e.getSalary() > 7000).forEach(System.out::println); //forEach()是一个终止操作//2. Stream<T> limit(long maxSize):截断流,返回一个元素数量不超过maxSize的流,按照数据源的顺序截断//一个 `Stream` 只能进行一次完整的操作,即当终止操作执行后就关闭了,此时再使用这个 `stream` 会报错//stream.limit(3).forEach(System.out::println);//此时应当重新创建一个Stream对象list.stream().limit(3).forEach(System.out::println);//3. Stream<T> skip(long n):返回一个跳过了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补list.stream().skip(3).forEach(System.out::println);//4. Stream<T> distinct():通过流所生成元素的 hashCode() 和 equals() 去除重复元素,返回一个没有重复元素的流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表达式)作为参数,将流中的每个元素都换成另一个流,然后把所有流连接成一个流
@Test
public void tese2() {List<String> list = Arrays.asList("aa", "bb", "cc", "dd");//1. <R> Stream<R> map(Function<? super T, ? extends R> mapper)//接收一个函数(通常写成Lambda表达式)作为参数,该函数会被应用到流的每个元素上,映射成新的元素。返回映射操作后的流list.stream().map(str -> str.toUpperCase()).forEach(System.out::println);//练习1:获取姓名长度大于3的员工的姓名List<Employee> employees = EmployeeData.getEmployees();//先调用map()将流中所有元素(Employee对象)映射为对应的name属性,然后调用filter()筛选仅保留名字长度大于3的员工//Stream的中间操作采用链式编程,最后通过forEach()执行终止操作//Stream<String> nameStream = employees.stream().map(employee -> employee.getName());//nameStream.filter(name -> name.length() > 3).forEach(System.out::println);//写成链式,并使用方法引用employees.stream().map(Employee::getName).filter(name -> name.length() > 3).forEach(System.out::println);//练习2:将list中的每个字符串元素中的每个字符单独打印出来//使用map()Stream<Stream<Character>> streamStream = list.stream().map(StreamAPITest1::fromStringToStream); //Stream里面套了StreamstreamStream.forEach(s -> s.forEach(System.out::println));//2. <R> Stream<R> flatMap(Function<? super T, ? extends Stream<? extends R>> mapper)//接收一个函数(通常写成Lambda表达式)作为参数,将流中的每个元素都换成另一个流,然后把所有流连接成一个流Stream<Character> characterStream = list.stream().flatMap(StreamAPITest1::fromStringToStream);characterStream.forEach(System.out::println);
}//将字符串中的多个字符构成的集合转换成对应的Stream对象
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表达式)
@Test
public void test4() {List<Integer> list = Arrays.asList(12, 43, 65, 34, 87, 0, -98, 7);List<Employee> employees = EmployeeData.getEmployees();//1. `Stream<T> sorted()`:返回一个新的流,其中元素按自然顺序排序(自然排序)。元素所在类必须实现Comparable接口//此处按照Integer内部定义的compareTo方法进行自然排序list.stream().sorted().forEach(System.out::println);//2. `Stream<T> sorted(Comparator com)`:返回一个新的流,其中元素按定制顺序排序(定制排序)employees.stream().sorted((e1, e2) -> Integer.compare(e1.getAge(), e2.getAge())).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)
:内部迭代- 使用 Collection 接口需要自行使用迭代器迭代,称为外部迭代。相反,Stream API 使用内部迭代——它帮你把迭代做了
@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:是否存在员工的工资大于10000boolean 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);
}
5.2 归约
T reduce(T identity, BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为泛型类型T
。identity表示初始值Optional<T> reduce(BinaryOperator<T> accumulator)
:将流中元素反复结合起来,得到一个值,返回值类型为Optional<T>
@Test
public void test2() {//1. reduce(T iden, BinaryOperator b)//练习1:计算1-10的自然数的和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);//2. Optional<T> reduce(BinaryOperator<T> accumulator)//练习2:计算所有员工的工资总和List<Employee> employees = EmployeeData.getEmployees();//Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce(Double::sum);Optional<Double> salarySum = employees.stream().map(Employee::getSalary).reduce((s1, s2) -> s1 + s2);System.out.println(salarySum);
}
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()
等
@Test
public void test3() {List<Employee> employees = EmployeeData.getEmployees();//<R, A> R collect(Collector<? super T, A, R> collector)://练习1:查找工资大于6000的员工,结果返回为一个List或SetList<Employee> employeeList = employees.stream().filter(employee -> employee.getSalary() > 6000).collect(Collectors.toList());employeeList.forEach(System.out::println);Set<Employee> employeeSet = employees.stream().filter(employee -> employee.getSalary() > 6000).collect(Collectors.toSet());employeeSet.forEach(System.out::println);
}
6. Stream的延迟执行
**在执行中间操作(返回值类型为 Stream
的方法)时,并不立即执行,而是等执行终止操作(返回值类型为非 Stream
的方法)后才执行。**因为拿到 Stream
并不能直接用,而是需要处理成一个常规类型。这里的 Stream
可以想象成是二进制流,拿到也看不懂。
我们下面分解一下 filter
方法。
@Test
public void laziness(){List<String> strings = Arrays.asList("abc", "def", "gkh", "abc");Stream<Integer> stream = strings.stream().filter(new Predicate() {@Overridepublic 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
。这里可以简单的理解它是通过线程池来实现的,这样就会涉及到线程安全,线程消耗等问题。下面我们通过代码来体验一下并行流的多线程执行。
@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
。
class Zoo {private Dog dog;
}class Dog {private int age;
}
传统解决 NPE 的办法如下:
Zoo zoo = getZoo();
if(zoo != null){Dog dog = zoo.getDog();if(dog != null){int age = dog.getAge();System.out.println(age);}
}
层层判断对象非空,有人说这种方式很丑陋不优雅,我并不这么认为。反而觉得很整洁,易读,易懂。你们觉得呢?
Optional
是这样的实现的:
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 的源码方法。
/**
* Common instance for {@code empty()}. 全局EMPTY对象
*/
private static final Optional<?> EMPTY = new Optional<>();/**
* Optional维护的值
*/
private final T value;/**
* 如果value是null就返回EMPTY,否则就返回of(T)
*/
public static <T> Optional<T> ofNullable(T value) {return value == null ? empty() : of(value);
}
/**
* 返回 EMPTY 对象
*/
public static<T> Optional<T> empty() {Optional<T> t = (Optional<T>) EMPTY;return t;
}
/**
* 返回Optional对象
*/
public static <T> Optional<T> of(T value) {return new Optional<>(value);
}
/**
* 私有构造方法,给value赋值
*/
private Optional(T value) {this.value = Objects.requireNonNull(value);
}
/**
* 所以如果of(T value) 的value是null,会抛出NullPointerException异常,这样貌似就没处理NPE问题
*/
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
。
3. map()相关方法
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值
*/
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));}
}
/**
* 如果value为null,返回EMPTY,否则返回Optional封装的参数值,如果参数值返回null会抛 NullPointerException
*/
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
的参数是这样
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
/**
* value是否为null
*/
public boolean isPresent() {return value != null;
}
/**
* 如果value不为null,执行consumer接口的实现类实现的accept()方法
*/
public void ifPresent(Consumer<? super T> consumer) {if (value != null)consumer.accept(value);
}
5. 获取Optional容器中的对象 value
/**
* Return the value if present, otherwise invoke {@code other} and return
* the result of that invocation.
* 如果value != null 返回value,否则返回other的执行结果
*/
public T orElseGet(Supplier<? extends T> other) {return value != null ? value : other.get();
}/**
* 如果value != null 返回value,否则返回T
*/
public T orElse(T other) {return value != null ? value : other;
}/**
* 如果value != null 返回value,否则抛出参数返回的异常
*/
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {if (value != null) {return value;} else {throw exceptionSupplier.get();}
}
/**
* value为null抛出NoSuchElementException,不为空返回value。
*/
public T get() {if (value == null) {throw new NoSuchElementException("No value present");}return value;
}
6. 过滤值
/**
* 1. 如果是empty返回empty
* 2. predicate.test(value)==true 返回this,否则返回empty
*/
public Optional<T> filter(Predicate<? super T> predicate) {Objects.requireNonNull(predicate);if (!isPresent())return this;elsereturn predicate.test(value) ? this : empty();
}
7. 小结
看完 Optional
源码,Optional
的方法真的非常简单,值得注意的是如果坚决不想看见 NPE
,就不要用 of()
、 get()
、flatMap(..)
。最后再综合用一下 Optional
的高频方法。
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
把它们进行了分离
LocalDateTime.class //日期+时间 format: yyyy-MM-ddTHH:mm:ss.SSS
LocalDate.class //日期 format: yyyy-MM-dd
LocalTime.class //时间 format: HH:mm:ss
2. 格式化
Java 8 之前:
public void oldFormat(){Date now = new Date();//format yyyy-MM-dd HH:mm:ssSimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");String date = sdf.format(now);System.out.println(String.format("date format : %s", date));//format HH:mm:ssSimpleDateFormat sdft = new SimpleDateFormat("HH:mm:ss");String time = sdft.format(now);System.out.println(String.format("time format : %s", time));//format yyyy-MM-dd HH:mm:ssSimpleDateFormat 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 之后:
public void newFormat(){//format yyyy-MM-ddLocalDate date = LocalDate.now();System.out.println(String.format("date format : %s", date));//format HH:mm:ssLocalTime time = LocalTime.now().withNano(0);System.out.println(String.format("time format : %s", time));//format yyyy-MM-dd HH:mm:ssLocalDateTime 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 之前:
//已弃用
Date date = new Date("2021-01-26");
//替换为
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
Date date1 = sdf.parse("2021-01-26");
Java 8 之后:
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 之前:
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 + "天");//结果:2021-12-23和2021-12-23相差300天
}
Java 8 之后:
public void pushWeek(){//一周后的日期LocalDate localDate = LocalDate.now();//方法1LocalDate after = localDate.plus(1, ChronoUnit.WEEKS);//方法2LocalDate 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() + "天");//打印结果是 “date1 到 date2 相隔:0年9月27天”//这里period.getDays()得到的天是抛去年月以外的天数,并不是总天数//如果要获取纯粹的总天数应该用下面的方法long day = date2.toEpochDay() - date1.toEpochDay();System.out.println(date2 + "和" + date2 + "相差" + day + "天");//打印结果:2021-12-23和2021-12-23相差300天
}
5. 获取指定日期
除了日期计算繁琐,获取特定一个日期也很麻烦,比如获取本月最后一天,第一天。
Java 8 之前:
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 之后:
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());//2021年最后一个周日,如果用Calendar是不得烦死。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
。
//北京时间:Wed Jan 27 14:05:29 CST 2021
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));//如果直接print会自动转成当前时区的时间
System.out.println(date);
//Wed Jan 27 14:05:29 CST 2021
在新特性中引入了 java.time.ZonedDateTime
来表示带时区的时间。它可以看成是 LocalDateTime + ZoneId
。
//当前时区时间
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);// ZonedDateTime 转 LocalDateTime
LocalDateTime localDateTime = tokyoTime.toLocalDateTime();
System.out.println("东京时间转当地时间: " + localDateTime);//LocalDateTime 转 ZonedDateTime
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。