文章目录
1.Java8新特性概述
java8的新特性有函数式接口、接口的默认方法与静态方法、Lambda表达式、方法引用与构造器引用、Stream API、新时间日期API、减少空指针异常Optional、支持重复注解、Fork/Join 框架等。
java8的优点如下:速度更快、代码更少(增加了新的语法Lambda表达式)、强大的Stream API、便于并行、最大化减少空指针异常Optional。
2. java8接口的变更
2.1接口中的默认方法
Java 8中允许接口中包含具有具体实现的方法,该方法称为“默认方法”,默认方法使用 default 关键字修饰 。
例如,我们可以定义一个接口MyFunction,其中,包含有一个默认方法getName,如下所示。
public interface MyFunction<T>{
T get(Long id);
default String getName(){
return "binghe";
}
}
2.1.1.默认方法的原则
在Java8中,默认方法具有“类优先”的原则。若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时,遵循如下的原则。
1.选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略。
例如,现在有一个接口为MyFunction,和一个类MyClass,如下所示。
MyFunction接口
public interface MyFunction{
default String getName(){
return "MyFunction";
}
}
MyClass类
public class MyClass{
public String getName(){
return "MyClass";
}
}
此时,创建SubClass类继承MyClass类,并实现MyFunction接口,如下所示。
public class SubClass extends MyClass implements MyFunction{
}
接下来,我们创建一个SubClassTest类,对SubClass类进行测试,如下所示。
public class SubClassTest{
@Test
public void testDefaultFunction(){
SubClass subClass = new SubClass();
System.out.println(subClass.getName());
}
}
运行上述程序,会输出字符串:MyClass。
2.接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法), 那么必须覆盖该方法来解决冲突。
例如,现在有两个接口,分别为MyFunction和MyInterface,各自都有一个默认方法getName(),如下所示。
MyFunction接口
public interface MyFunction{
default String getName(){
return "function";
}
}
MyInterface接口
public interface MyInterface{
default String getName(){
return "interface";
}
}
实现类MyClass同时实现了MyFunction接口和MyInterface接口,由于MyFunction接口和MyInterface接口中都存在getName()默认方法,所以,MyClass必须覆盖getName()方法来解决冲突,如下所示。
public class MyClass implements MyFunction,MyInterface{
@Override
public String getName(){
return MyInterface.super.getName();
}
}
此时,MyClass类中的getName方法返回的是:interface。
增加默认方法的目的主要是为了在接口上向后兼容。让类中重写方法的优先级高于默认方法能简化很多继承问题。
三定律:对默认方法的工作原理,特别是在多重继承下的行为还没有把握,遵循如下三条简单的定律
- 类胜于接口。如果在继承链中有方法体或抽象的方法声明,那么就可以忽略接口中定义的方法。
- 子类胜于父类。如果一个接口继承了另一个接口,且两个接口都定义了一个默认方法,那么子类中定义的方法胜出。
- 没有规则三。如果上面两条规则不适用,子类要么需要实现该方法,要么将该方法声明为抽象方法。
其中第一条规则是为了让代码向后兼容。
2.2.接口中的静态方法
在Java8中,接口中允许添加静态方法,使用方式接口名.方法名。例如MyFunction接口中定义了静态方法send()。
public interface MyFunction{
default String getName(){
return "hello";
}
static void send(){
System.out.println("Send Message...");
}
}
我们可以直接使用如下方式调用MyFunction接口的send静态方法。
MyFunction.send();
2.3.函数式接口
Lambda表达式需要函数式接口的支持,所以,我们有必要来说说什么是函数式接口。
- 只有一个抽象方法的接口
- 函数式接口上可以使用 @FunctionalInterface 注解也可以不使用
- 函数式接口里可以定义默认方法:默认方法有方法体,不是抽象方法。
- 函数式接口里可以定义静态方法:静态方法也不是抽象方法,是一个有具体方法实现的方法
- 函数式接口里可以定义Object里的public方法(改成抽象方法):虽然它们是抽象方法,却不需要覆盖重写,因为所有接口的实现类都是Object类的子类,而在Object类中有这些方法的具体的实现。
2.3.1.函数式接口与lambda表达式的简单使用示例
例如下面使用函数式接口和Lambda表达式实现对字符串的处理功能。
定义函数式接口MyFunc,如下所示
@FunctionalInterface
public interface MyFunc <T> {
public T getValue(T t);
}
接下来,定义一个操作字符串的方法,其中参数为MyFunc接口实例和需要转换的字符串。
public String handlerString(MyFunc<String> myFunc, String str){
return myFunc.getValue(str);
}
接下来,我们对自定义的函数式接口进行测试,此时我们传递的函数式接口的参数为Lambda表达式,并且将字符串转化为大写。
@Test
public void test6(){
String str = handlerString((s) -> s.toUpperCase(), "hello world");
System.out.println(str);
}
也可以截取字符串的某一部分,如下所示:
@Test
public void test7(){
String str = handlerString((s) -> s.substring(0,4), "hello world");
System.out.println(str);
}
通过handlerString(MyFunc myFunc, String str)方法结合Lambda表达式对字符串进行任意操作。
注意:作为参数传递 Lambda 表达式:为了将 Lambda 表达式作为参数传递,接收Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型 。
2.3.2.四大核心函数式接口
函数式接口 | 参数类型 | 返回类型 | 使用场景 |
---|---|---|---|
Consumer消费型接口 | T | void | 对类型为T的对象应用操作,接口定义的方法:void accept(T t) |
Supplier供给型接口 | 无 | T | 返回类型为T的对象,接口定义的方法:T get() |
Function<T,R>函数式接口 | T | R | 对类型为T的对象应用操作,并R类型的返回结果。接口定义的方法:R apply(T t) |
Predicate断言型接口 | T | boolean | 确定类型为T的对象是否满足约束条件,并返回boolean类型的数据。接口定义的方法:boolean test(T t) |
2.3.2.1.Consumer接口
使用示例:
public void handlerConsumer(Integer number, Consumer<Integer> consumer){
consumer.accept(number);
}
@Test
public void test1(){
this.handlerConsumer(10000, (i) -> System.out.println(i));
}
2.3.2.2. Supplier接口
使用示例:
public List<Integer> getNumberList(int num, Supplier<Integer> supplier){
List<Integer> list = new ArrayList<>();
for(int i = 0; i < num; i++){
list.add(supplier.get())
}
return list;
}
@Test
public void test2(){
List<Integer> numberList = this.getNumberList(10, () -> new Random().nextInt(100));
numberList.stream().forEach(System.out::println);
}
2.3.2.3. Function接口
使用示例:
public String handlerString(String str, Function<String, String> func){
return func.apply(str);
}
@Test
public void test3(){
String str = this.handlerString("hello", (s) -> s.toUpperCase());
System.out.println(str);
}
2.3.2.4. Predicate接口
使用示例:
public List<String> filterString(List<String> list, Predicate<String> predicate)
{
List<String> strList = new ArrayList<>();
for(String str : list){
if(predicate.test(str)){
strList.add(str);
}
}
return strList;
}
@Test
public void test4(){
List<String> list = Arrays.asList("Hello", "Lambda", "binghe", "lyz", "World");
List<String> strList = this.filterString(list, (s) -> s.length() >= 5);
strList.stream().forEach(System.out::println);
}
2.4.其他函数接口
函数式接口 | 参数类型 | 返回类型 | 使用场景 |
---|---|---|---|
BiFunction(T, U, R) | T, U | R | 对类型为T,U的参数应用操作,返回R类型的结果。接口定义的方法:R apply(T t, U u) |
UnaryOperator(Function子接口) | T | T | 对类型为T的对象进行一 元运算, 并返回T类型的 结果。 包含方法为 T apply(T t) |
BinaryOperator (BiFunction 子接口) | T, T | T | 对类型为T的对象进行二 元运算, 并返回T类型的 结果。 包含方法为 T apply(T t1,T t2) |
BiConsumer<T, U> | T, U | void | 对类型为T, U 参数应用 操作。 包含方法为 void accept(T t, U u) |
ToIntFunction | T | int | 计算int值的函数 |
ToLongFunction | T | long | 计算long值的函数 |
ToDoubleFunction | T | double | 计算double值的函数 |
IntFunction | int | R | 参数为int 类型的函数 |
LongFunction | long | R | 参数为 long类型的函数 |
DoubleFunction | double | R | 参数为double类型的函数 |
3.lambda表达式
Lambda表达式是一个匿名函数,本质上是对接口的实现,Lambda是一段可以传递的代码(能够做到将代码像数据一样进行传递)。
匿名内部类,例如,我们使用匿名内部类比较两个Integer类型数据的大小。
Comparator<Integer> com = new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
};
在上述代码中,我们使用匿名内部类实现了比较两个Integer类型数据的大小。
接下来,我们就可以将上述匿名内部类的实例作为参数,传递到其他方法中了,如下所示。
TreeSet<Integer> treeSet = new TreeSet<>(com)
使用Lambda表达式完成两个Integer类型数据的比较。
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
Java 8以前匿名内部类需要引用它所在方法里的变量时,需要将变量声明为final。将变量声明为final,意味着不能为其重复赋值。同时也意味着在使用final变量时,实际上是在使用赋给该变量的一个特定的值。Java 8 虽然放松了这一限制,可以引用非final变量,但是该变量在既成事实上必须是final。虽然无需将变量声明为final,但在Lambda表达式中,也无法用作非终态变量。如果坚持用作非终态变量,编译器就会报错。
既成事实上的final是指只能给该变量赋值一次。换句话说,Lambda表达式引用的是值,而不是变量。
3.1.Lambda表达式的语法
Lambda表达式在Java语言中引入了 “->” 操作符, “->” 操作符被称为Lambda表达式的操作符或者箭头操作符,它将Lambda表达式分为两部分:
- 左侧部分指定了Lambda表达式需要的所有参数。
Lambda表达式本质上是对接口的实现,Lambda表达式的参数列表本质上对应着接口中方法的参数列表。 - 右侧部分指定了Lambda体,即Lambda表达式要执行的功能。
Lambda体本质上就是接口方法具体实现的功能。
常用的Lambda表达式的语法总结如下。
1.语法格式一:无参,无返回值,Lambda体只有一条语句
Runnable r = () -> System.out.println("Hello Lambda");
2.语法格式二:Lambda表达式需要一个参数,并且无返回值
Consumer<String> func = (s) -> System.out.println(s);
3.语法格式三:Lambda只需要一个参数时,参数的小括号可以省略
Consumer<String> func = s -> System.out.println(s);
4.语法格式四:Lambda需要两个参数,并且有返回值
BinaryOperator<Integer> bo = (a, b) -> {
System.out.println("函数式接口");
return a + b;
};
5.语法格式五:当Lambda体只有一条语句时,return和大括号可以省略
BinaryOperator<Integer> bo = (a, b) -> a + b;
6.语法格式六:Lambda表达式的参数列表的数据类型可以省略不写,因为JVM编译器能够通过上下文推断出数据类型,这就是“类型推断”
BinaryOperator<Integer> bo = (Integer a, Integer b) -> {
return a + b;
};
等同于
BinaryOperator<Integer> bo = (a, b) -> {
return a + b;
};
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。 Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。 Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的“类型推断”。
4.方法引用
当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!这里需要注意的是:实现抽象方法的参数列表,必须与方法引用方法的参数列表保持一致!
方法引用就是操作符“::”将方法名和对象或类的名字分隔开来。
有如下三种使用情况:
- 对象::实例方法
- 类::静态方法
- 类::实例方法
我们可以列举几个示例:
(x) -> System.out.println(x);
//等同于:
System.out::println
BinaryOperator<Double> bo = (x, y) -> Math.pow(x, y);
//等同于
BinaryOperator<Double> bo = Math::pow;
注意: 当需要引用方法的第一个参数是调用对象,并且第二个参数是需要引用方法的第二个参数(或无参数)时ClassName::methodName 。
5.构造器引用
格式如下所示:
ClassName::new
与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致!
Function<Integer, MyClass> fun = (n) -> new MyClass(n);
//等同于
Function<Integer, MyClass> fun = MyClass::new;
6. 数组引用
格式如下所示。
type[]::new
Function<Integer, Integer[]> fun = (n) -> new Integer[n];
//等同于
Function<Integer, Integer[]> fun = Integer[]::new;
7.Stream API概述
Stream 是 Java8 (java.util.stream.*)中处理集合的关键抽象概念,可以执行非常复杂的查找、过滤和映射数据等操作。就类似于使用SQL 执行的数据库查询。也可以使用Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式 。 Stream 是用函数式编程方式在集合类上进行复杂操作的工具。
流是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。“集合讲的是数据,流讲的是计算! ”
- Stream 自己不会存储元素。
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新Stream。
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行。
7.1Stream操作的三个步骤
- 创建 Stream:一个数据源(如: 集合、数组), 获取一个流。
- 中间操作:一个中间操作链,对数据源的数据进行处理。
- 终止操作(终端操作):一个终止操作,执行中间操作链,并产生结果 。
Stream里的中间操作的方法返回的Stream对象不是一个新集合,而是创建新集合的配方。最终不产生新集合的方法叫作惰性求值方法;像终止操作最终会从Stream产生值的方法叫作及早求值方法。
判断一个操作是惰性求值还是及早求值很简单:只需看它的返回值。如果返回值是Stream,那么是惰性求值;如果返回值是另一个值或为空,那么就是及早求值。使用这些操作的理想方式就是形成一个惰性求值的链,最后用一个及早求值的操作返回想要的结果,这正是它的合理之处。整个过程和建造者模式有共通之处。建造者模式使用一系列操作设置属性和配置,最后调用一个build方法,这时,对象才被真正创建。
7.1.1创建Stream
7.1.1.1.获取Stream
Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
- default Stream stream() : 返回一个顺序流
- default Stream parallelStream() : 返回一个并行流
7.1.1.2.由数组创建Stream
Java8 中的 Arrays 的静态方法 stream() 可以获取数组流:
- static Stream stream(T[] array): 返回一个流
重载形式,能够处理对应基本类型的数组:
- public static IntStream stream(int[] array)
- public static LongStream stream(long[] array)
- public static DoubleStream stream(double[] array)
可以通过下面的代码示例来使用Arrays类的stream()方法来创建Stream流
Integer[] nums = new Integer[]{1,2,3,4,5,6,7,8,9};
Stream<Integer> numStream = Arrays.stream(nums);
7.1.1.3.由值创建流
可以使用静态方法 Stream.of(), 通过显示值创建一个流。它可以接收任意数量的参数。
- public static Stream of(T t)
- public static Stream of(T… values) : 返回一个流
可以看到,在Stream类中,提供了两个of()方法,一个只需要传入一个泛型参数,一个需要传入一个可变泛型参数。
我们可以使用下面的代码示例来使用of方法创建一个Stream流。
Stream<String> strStream = Stream.of("a", "b", "c");
7.1.1.4.由函数创建流
由函数创建流可以创建无限流。可以使用静态方法 Stream.iterate() 和Stream.generate(), 创建无限流 。
-
迭代:public static Stream iterate(final T seed, final UnaryOperator f)
-
生成:public static Stream generate(Supplier s)
iterate()方法主要是使用“迭代”的方式生成无限流,而generate()方法主要是使用“生成”的方式生成无限流。
我们可以使用下面的代码示例来使用这两个方法生成Stream流 -
迭代
Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 2);
intStream.forEach(System.out::println);
运行上述代码,会在终端一直输出偶数,这种操作会一直持续下去。如果我们只需要输出10个偶数,该如何操作呢?其实也很简单,使用Stream对象的limit方法进行限制就可以了,如下所示:
Stream<Integer> intStream = Stream.iterate(0, (x) -> x + 2);
intStream.limit(10).forEach(System.out::println);
- 生成
Stream.generate(() -> Math.random()).forEach(System.out::println);
上述代码同样会一直输出随机数,如果我们只需要输出5个随机数,则只需要使用limit()方法进行限制即可。
Stream.generate(() -> Math.random()).limit(5).forEach(System.out::println);
7.1.1.5 创建空流
在Stream类中提供了一个empty()方法,如下所示。
public static<T> Stream<T> empty() {
return StreamSupport.stream(Spliterators.<T>emptySpliterator(), false);
}
我们可以使用Stream类的empty()方法来创建一个空Stream流,如下所示。
Stream<String> empty = Stream.empty();
7.1.2.Stream的中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理!而在终止操作时一次性全部处理,称为“惰性求值”。 Stream的中间操作是不会有任何结果数据输出的。
Stream的中间操作在整体上可以分为:筛选与切片、映射、排序。接下来,我们就分别对这些中间操作进行简要的说明。
7.1.2.1.筛选与切片
方法 | 描述 | 用法 |
---|---|---|
filter(Predicate p) | 接收lambda,从流中排除某些元素 | list.stream().filter((e) -> e.getAge() > 30); |
distinct() | 筛选,通过流所生成元素的hashCode()和equals()去除重复元素 | list.stream().distinct().forEach(System.out :: println); |
limit(long maxSize) | 截断流,使其元素不超过给定数量 | list.stream().filter((e) -> e.getAge() >30 ).limit(2).forEach(System.out :: println); |
skip(long n) | 跳过元素,返回一个扔掉了前n个元素的流。若流中元素不足n个,则返回一个空流。与limit(n)互补 | list.stream().skip(2).forEach(System.out :: println); |
7.1.2.2.映射
方法 | 描述 |
---|---|
map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素 |
mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream。 |
mapTolnt(TolntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream。 |
mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream。 |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 |
1.map()方法
接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素。我们可以按照如下方式使用map()方法
//将流中每一个元素都映射到map的函数中,每个元素执行这个函数,再返回
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
list.stream().map((e) -> e.toUpperCase()).forEach(System.out::printf);
//获取Person中的每一个人得名字name,再返回一个集合
List<String> names = this.list.stream().map(Person :: getName).collect(Collectors.toList());
2.flatMap()
接收一个函数作为参数,将流中的每个值都换成另 一个流,然后把所有流连接成一个流。测试flatMap()方法的所有代码。
/**
* flatMap —— 接收一个函数作为参数,将流中的每个值都换成一个流,然后把所有流连接成一个流
*/
@Test
public void testFlatMap () {
StreamAPI_Test s = new StreamAPI_Test();
List<String> list = Arrays.asList("aaa", "bbb", "ccc", "ddd");
list.stream().flatMap((e) -> s.filterCharacter(e)).forEach(System.out::println);
//如果使用map则需要这样写
list.stream().map((e) -> s.filterCharacter(e)).forEach((e) -> {
e.forEach(System.out::println);
});
}
/**
* 将一个字符串转换为流
*/
public Stream<Character> filterCharacter(String str){
List<Character> list = new ArrayList<>();
for (Character ch : str.toCharArray()) {
list.add(ch);
}
return list.stream();
}
/**
*包含多个列表的Stream
*/
List<Integer> together = Stream.of(asList(1, 2), asList(3, 4))
.flatMap(numbers -> numbers.stream())
.collect(toList());
assertEquals(asList(1, 2, 3, 4), together);
其实map方法就相当于Collection的add方法,如果add的是个集合得话就会变成二维数组,而flatMap 的话就相当于Collection的addAll方法,参数如果是集合得话,只是将2个集合合并,而不是变成二维数组。
7.1.2.3.排序
方法 | 描述 |
---|---|
sorted() | 产生一个新流,其中按自然顺序排序 |
sorted(Comparator comp) | 产生一个新流,其中按比较器顺序排序 |
从上述表格可以看出:sorted有两种方法,一种是不传任何参数,叫自然排序,还有一种需要传Comparator 接口参数,叫做定制排序。
我们也可以按照如下方式来使用Stream的sorted()方法。
// 自然排序
List<Employee> persons = list.stream().sorted().collect(Collectors.toList());
//定制排序
List<Employee> persons1 = list.stream().sorted((e1, e2) -> {
if (e1.getAge() == e2.getAge()) {
return 0;
} else if (e1.getAge() > e2.getAge()) {
return 1;
} else {
return -1;
}
}).collect(Collectors.toList());
7.1.3 Stream的终止操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如: List、Integer,Double、String等,甚至是 void 。
在Java8中,Stream的终止操作可以分为:查找与匹配、规约和收集。接下来,我们就分别简单说明下这些终止操作。
7.1.3.1.查找与匹配
方法 | 描述 |
---|---|
allMatch(Predicate p) | 检查是否匹配所有元素 |
anyMatch(Predicate p) | 检查是否至少匹配一个元素 |
noneMatch(Predicate p) | 检查是否没有匹配所有元素 |
findFirst() | 返回第一个元素 |
findAny() | 返回当前流中的任意元素 |
count() | 返回流中元素总数 |
max(Comparator c) | 返回流中最大值 |
min(Comparator c) | 返回流中最小值 |
forEach(Consumer c) | 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API使用内部迭代–它帮你把迭代做了) |
1.allMatch()
boolean match = employees.stream().allMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);
注意:使用allMatch()方法时,只有所有的元素都匹配条件时,allMatch()方法才会返回true。
2.anyMatch()方法
boolean match = employees.stream().anyMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);
注意:使用anyMatch()方法时,只要有任意一个元素符合条件,anyMatch()方法就会返回true。
3.noneMatch()方法
boolean match = employees.stream().noneMatch((e) -> Employee.Stauts.SLEEPING.equals(e.getStauts()));
System.out.println(match);
注意:使用noneMatch()方法时,只有所有的元素都不符合条件时,noneMatch()方法才会返回true。
4.findFirst()方法
Optional<Employee> op = employees.stream().sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())).findFirst();
System.out.println(op.get());
5.findAny()方法
Optional<Employee> op = employees.stream().filter((e) -> Employee.Stauts.WORKING.equals(e.getStauts())).findFirst();
System.out.println(op.get());
6.count()方法
long count = employees.stream().count();
System.out.println(count);
7.max()方法
Optional<Employee> op = employees.stream().max((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));
System.out.println(op.get());
8.min()方法
Optional<Double> op = employees.stream().map(Employee::getSalary).min(Double::compare);
System.out.println(op.get());
9.forEach()方法
forEach()方法表示内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反, Stream API 使用内部迭代)。示例:
employees.stream().forEach(System.out::println);
7.1.3.2. 规约
方法 | 描述 |
---|---|
reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T |
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 Optional < T> |
reduce操作可以实现从一组值中生成一个值。在上述例子中用到的count、min和max方
法,因为常用而被纳入标准库中。事实上,这些方法都是reduce操作。
示例:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10);
Integer sum = list.stream().reduce(0, (acc, element) -> acc + element);
System.out.println(sum);
System.out.println("----------------------------------------");
Optional<Double> op = employees.stream().map(Employee::getSalary).reduce(Double::sum);
System.out.println(op.get());
通过reduce操作对Stream中的数字求和。 Lambda表达式的返回值是最新的acc,是上一轮acc的值和当前元素相加的结果。以0作起点——一个空Stream的求和结果,每一步都将Stream中的元素累加至accumulator,遍历至Stream中的最后一个元素时,accumulator的值就是所有元素的和。
我们也可以搜索employees列表中“张”出现的次数。
Optional<Integer> sum = employees.stream()
.map(Employee::getName)
.flatMap(TestStreamAPI1::filterCharacter)
.map((ch) -> {
if(ch.equals('六'))
return 1;
else
return 0;
}).reduce(Integer::sum);
System.out.println(sum.get());
注意:上述例子使用了硬编码的方式来累加某个具体值,大家在实际工作中再优化代码。
7.1.3.3. 收集
方法 | 描述 |
---|---|
collect(Collector c) | 将流转换成其他形式。接收一个 Collector接口的实现,用于给Stream中元素做汇总的方法 |
我们可以通过类似如下示例来使用collect方法。
Optional<Double> max = employees.stream()
.map(Employee::getSalary)
.collect(Collectors.maxBy(Double::compare));
System.out.println(max.get());
Optional<Employee> op = employees.stream()
.collect(Collectors.minBy((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(op.get());
Double sum = employees.stream().collect(Collectors.summingDouble(Employee::getSalary));
System.out.println(sum);
Double avg = employees.stream().collect(Collectors.averagingDouble(Employee::getSalary));
System.out.println(avg);
Long count = employees.stream().collect(Collectors.counting());
System.out.println(count);
System.out.println("--------------------------------------------");
DoubleSummaryStatistics dss = employees.stream()
.collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(dss.getMax());
Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、 Set、 Map)。 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例, 具体方法与实例如下表
方法 | 返回类型 | 作用 | 用法 |
---|---|---|---|
toList | List< T> | 把流中的元素收集到List | List< Employee> emps= list.stream().collect(Collectors.toList()); |
toSet | Set< T> | 把流中的元素收集到Set | Set< Employee> emps= list.stream().collect(Collectors.toset()); |
toCollection | Collection< T> | 把流中的元素收集到创建的集合中 | Collection< Employee>emps=list.stream().collect(Collectors.toCollection(ArrayList::new)); |
counting | Long | 计算流中的元素的个数 | long count = list.stream().collect(Collectors.counting()); |
summingInt | Integer | 对流中元素的整数属性求和 | inttotal=list.stream().collect(Collectors.summingInt(Employee::getSalary)); |
averaginglnt | Double | 计算流中元素Integer属性的平均值 | double avg= list.stream().collect(Collectors.averagingInt(Employee::getSalary)); |
summarizingInt | IntSummaryStatistics | 收集流中Integer属性的统计值。如:平均值 | IntSummaryStatistics iss= list.stream().collect(Collectors.summarizingInt(Employee::getSalary)); |
joining | String | 连接流中每个字符串 | String str= list.stream().map(Employee::getName).collect(collectors.joining()); |
maxBy | Optional< T> | 根据比较器选择最大值 | Optional< Emp>max= list.stream().collect(Collectors.maxBy(comparingInt(Employee::getSalary)))) |
minBy | Optional< T> | 根据比较器选择最小值 | Optional< Emp> min = list.stream().collect(Collectors.minBy(comparingInt(Employee::getSalary))); |
reducing | 归约产生的类型 | 从一个作为累加器的初始值开始,利用BinaryOperator与流中元素逐个结合,从而归约成单个值 | inttotal=list.stream().collect(Collectors.reducing(0, Employee::getSalar, integer:.sum)); |
collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果转换函数 | inthow= list.stream().collect(Collectors.collectingAndThen(Collectors.tolist(), List::size)); |
groupingBy | Map<K, List< T>> | 根据某属性值对流分组,属性为K,结果为V | Map<Emp.Status, List< Emp>> map= list.stream().collect(Collectors.groupingBy(Employee::getstatus)); |
partitioningBy | Map<Boolean, List< T>> | 根据true或false进行分区 | Map<Boolean,List< Emp>>vd= list.stream().collect(Collectors.partitioningBy(Employee::getManage)); |
public void test4(){
Optional<Double> max = emps.stream().map(Employee::getSalary).collect(Collectors.maxBy(Double::compare));
System.out.println(max.get());
Optional<Employee> op = emps.stream().collect(Collectors.minBy((e1, e2) ->
Double.compare(e1.getSalary(), e2.getSalary())));
System.out.println(op.get());
Double sum = emps.stream().collect(Collectors.summingDouble(Employee::getSalary));
System.out.println(sum);
Double avg = emps.stream().collect(Collecors.averagingDouble(Employee::getSalary));
System.out.println(avg);
Long count = emps.stream().collect(Collectors.counting());
DoubleSummaryStatistics dss = emps.stream().collect(Collectors.summarizingDouble(Employee::getSalary));
System.out.println(dss.getMax());
除了collection中可以统计一些max,min,sum,avg信息,还可以通过summaryStatistics方法可获得Stream的一些统计信息。
IntSummaryStatistics summaryStatistics = Stream.of(1, 2, 3).mapToInt((i) -> i).summaryStatistics();
System.out.println("max:" + summaryStatistics.getMax());
System.out.println("min:" + summaryStatistics.getMin());
System.out.println("sum:" + summaryStatistics.getSum());
System.out.println("average:" + summaryStatistics.getAverage());
这里用到了流与数值流直接的转换mapToInt,类似的方法还有mapToDouble、mapToLong。对应获得的数值流还提供了一些额外的方法,就像上面获取不同统计信息的方法一样。
7.2流中调试
使用流时,调试可能会变得更加复杂,因为迭代已交由类库控制,而且很多流操作是惰性求值的。现在可以使用forEach方法打印出流中的值,这同时会触发求值过程。但是这样的操作有个缺点:我们无法再继续操作流了,流只能使用一次。如果我们还想继续,必须重新创建流。
7.2.1解决方案:peak
peek:生成一个相同的Stream,并提供一个消费函数,当新Stream中的元素被消费(执行操作)时,该消费函数会在此之前先执行。
Stream.of(1, 2).peek(i -> System.out.println("peekCall:" + i)).forEach(System.out::println);
流有一个方法让你能查看每个值,同时能继续操作流。这就是peek方法。下面使用peek方法重写了前面的例子,输出流中的值,同时避免了重复的流操作。
Set<String> nationalities
= album.getMusicians()
.filter(artist -> artist.getName().startsWith("The"))
.map(artist -> artist.getNationality())
.peek(nation -> System.out.println("Found nationality: " + nation))
.collect(Collectors.<String>toSet());
7.2.2 在流中间设置断点
记录日志这是peek方法的用途之一。为了像调试循环那样一步一步跟踪,可在peek方法
中加入断点,这样就能逐个调试流中的元素了。
此时,peek方法可知包含一个空的方法体,只要能设置断点就行。有一些调试器不允在
空的方法体中设置断点,此时,我将值简单地映射为其本身,这样就有地方设置断点了,
虽然这样做不够完美,但只要能工作就行。
7.3并行流与串行流
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
Java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。
LongStream.rangeClosed(0, 10000000L).parallel().reduce(0, Long::sum);
Stream API 可以声明性地通过 parallel() 与sequential() 在并行流与顺序流之间进行切换。为了发挥并行流框架的优势,写代码时必须遵守一些规则和限制。
- 比如调用reduce方法,初始值可以为任意值,为了让其在并行化时能工作正常,初值必须为组合函数的恒等值。拿恒等值和其他值做reduce操作时,其他值保持不变。比如,使用reduce 操作求和,组合函数为(acc, element) -> acc + element,则其初值必须为0, 因为任何数字加0,值不变。
- reduce 操作的另一个限制是组合操作必须符合结合律。这意味着只要序列的值不变,组合操作的顺序不重要。
使用parallel方法能轻易将流转换为并行流。还有一个叫sequential的方法。在要对流求
值时,不能同时处于两种模式,要么是并行的,要么是串行的。如果同时调用了parallel
和sequential 方法,最后调用的那个方法起效。
7.3.1 for循环和Stream操作的性能对比
- 针对简单的操作,比如基础类型的遍历,使用for循环性能要明显高于串行Stream操作。但Stream的并行操作随着服务器的核数增加,会优于for循环。
- 针对复杂操作,串行Stream性能与for循环不差上下,但并行Stream的性能已经是无法匹敌了。
- 特别是针对一个集合进行多层过滤并归约操作,无论从写法上或性能上都要明显优于for循环。
用一句话来说就是:简单操作for循环即可,复杂操作首推Stream。
8.Optional类介绍
一个容器对象,它可以或可能不包含非空值。用于尽量避免空指针异常。
通过源代码会发现,它并没有实现java.io.Serializable接⼝口,因此应避免在类属性中使⽤用,防⽌止意想不不到的问题。
除了了Optional类之外,还扩展了了⼀一些常⽤用类型的Optional对象,比如:OptionalDouble、OptionalInt、OptionalLong。⽤用法基本上相似。
- Optional.of(T t) : 创建一个 Optional 实例
- Optional.empty() : 创建一个空的Optional 实例
- Optional.ofNullable(T t):若 t 不为 null,创建 Optional实例,否则创建空实例
- isPresent() : 判断是否包含值
- ifPresent(): 如果一个值存在,调用指定的值的指定的用户,否则什么都不做。
- orElse(T t) : 如果调用对象包含值,返回该值,否则返回t
- orElseGet(Supplier s) :如果调用对象包含值,返回该值,否则返回 s 获取的值
- map(Function f):如果有值对其处理,并返回处理后的Optional,否则返回Optional.empty()
- flatMap(Function mapper):与 map 类似,要求返回值必须是Optional
Optional.ofNullable(son).map(Person::getParent).map(Person::getUsername).orElse("--");
8.1 创建Optional类
(1)使用empty()方法创建一个空的Optional对象:
Optional<String> empty = Optional.empty();
(2)使用of()方法创建Optional对象:
String name = "helloWorld";
Optional<String> opt = Optional.of(name);
assertEquals("Optional[helloWorld]", opt.toString());
传递给of()的值不可以为空,否则会抛出空指针异常。例如,下面的程序会抛出空指针异常。
String name = null;
Optional<String> opt = Optional.of(name);
如果我们需要传递一些空值,那我们可以使用下面的示例所示。
String name = null;
Optional<String> opt = Optional.ofNullable(name);
使用ofNullable()方法,则当传递进去一个空值时,不会抛出异常,而只是返回一个空的Optional对象,如同我们用Optional.empty()方法一样。
8.2.isPresent
我们可以使用这个isPresent()方法检查一个Optional对象中是否有值,只有值非空才返回true。
Optional<String> opt = Optional.of("binghe");
assertTrue(opt.isPresent());
opt = Optional.ofNullable(null);
assertFalse(opt.isPresent());
在Java8之前,我们一般使用如下方式来检查空值。
if(name != null){
System.out.println(name.length);
}
在Java8中,我们就可以使用如下方式来检查空值了。
Optional<String> opt = Optional.of("helloWord");
opt.ifPresent(name -> System.out.println(name.length()));
8.3.ifPresent
判断并执⾏行行操作。可对值进⾏行行判断然后打印,接收参数为Consumer<? super T>函数式接⼝口。
Optional.of("helloWorld").ifPresent(System.out::println);
8.4.orElse和orElseGet
(1)orElse
orElse()方法用来返回Optional对象中的默认值,它被传入一个“默认参数‘。如果对象中存在一个值,则返回它,否则返回传入的“默认参数”。
String nullName = null;
String name = Optional.ofNullable(nullName).orElse("hello");
assertEquals("hello", name);
(2)orElseGet
与orElse()方法类似,但是这个函数不接收一个“默认参数”,而是一个函数接口。
String nullName = null;
String name = Optional.ofNullable(nullName).orElseGet(() -> "hello");
assertEquals("hello", name);
(3)二者有什么区别?
要想理解二者的区别,首先让我们创建一个无参且返回定值的方法。
public String getDefaultName() {
System.out.println("Getting Default Name");
return "hello";
}
接下来,进行两个测试看看两个方法到底有什么区别。
String text;
System.out.println("Using orElseGet:");
String defaultText = Optional.ofNullable(text).orElseGet(this::getDefaultName);
assertEquals("hello", defaultText);
System.out.println("Using orElse:");
defaultText = Optional.ofNullable(text).orElse(getDefaultName());
assertEquals("hello", defaultText);
在这里示例中,我们的Optional对象中包含的都是一个空值,让我们看看程序执行结果:
Using orElseGet:
Getting default name...
Using orElse:
Getting default name...
两个Optional对象中都不存在value,因此执行结果相同。
那么,当Optional对象中存在数据会发生什么呢?我们一起来验证下。
String name = "hello001";
System.out.println("Using orElseGet:");
String defaultName = Optional.ofNullable(name).orElseGet(this::getDefaultName);
assertEquals("hello001", defaultName);
System.out.println("Using orElse:");
defaultName = Optional.ofNullable(name).orElse(getDefaultName());
assertEquals("hello001", defaultName);
运行结果如下所示。
Using orElseGet:
Using orElse:
Getting default name...
可以看到,当使用orElseGet()方法时,getDefaultName()方法并不执行,因为Optional中含有值,而使用orElse时则照常执行。所以可以看到,当值存在时,orElse相比于orElseGet,多创建了一个对象。如果创建对象时,存在网络交互,那系统资源的开销就比较大了,这是需要我们注意的一个地方。
如果计算备选值在计算上太过繁琐,即可使用orElseGet方法。该方法接受一个
Supplier 对象,只有在Optional对象真正为空时才会调用。
8.5.orElseThrow
orElseThrow()方法当遇到一个不存在的值的时候,并不返回一个默认值,而是抛出异常。
String nullName = null;
String name = Optional.ofNullable(nullName).orElseThrow( IllegalArgumentException::new);
orElseThrow()⽅方法与get()⽅方法类似,当值为null时调⽤用会抛NullPointerException异常,但该方法可以指定抛出的异常类型。
Optional.empty().orElseThrow(()-> new RuntimeException("抛出自定义异常!"));
8.6.get
get()方法表示是Optional对象中获取值。
Optional<String> opt = Optional.of("binghe");
String name = opt.get();
assertEquals("binghe", name);
使用get()方法也可以返回被包裹着的值。但是值必须存在。当值不存在时,会抛出一个NoSuchElementException异常。
Optional<String> opt = Optional.ofNullable(null);
String name = opt.get();
8.7.filter
接收一个函数式接口,当符合接口时,则返回一个Optional对象,否则返回一个空的Optional对象。
String name = "hello";
Optional<String> nameOptional = Optional.of(name);
boolean isHello = nameOptional.filter(n -> "hello".equals(name)).isPresent();
assertTrue(isHello );
boolean isHello001 = nameOptional.filter(n -> "hello001".equals(name)).isPresent();
assertFalse(isHello001);
使用filter()方法会过滤掉我们不需要的元素。
接下来,我们再来看一例示例,例如目前有一个Person类,如下所示。
public class Person{
private int age;
public Person(int age){
this.age = age;
}
//省略get set方法
}
例如,我们需要过滤出年龄在25岁到35岁之前的人群,那在Java8之前我们需要创建一个如下的方法来检测每个人的年龄范围是否在25岁到35岁之前。
public boolean filterPerson(Peron person){
boolean isInRange = false;
if(person != null && person.getAge() >= 25 && person.getAge() <= 35){
isInRange = true;
}
return isInRange;
}
看上去就挺麻烦的,我们可以使用如下的方式进行测试。
assertTrue(filterPerson(new Peron(18)));
assertFalse(filterPerson(new Peron(29)));
assertFalse(filterPerson(new Peron(16)));
assertFalse(filterPerson(new Peron(34)));
assertFalse(filterPerson(null));
如果使用Optional,效果如何呢?
public boolean filterPersonByOptional(Peron person){
return Optional.ofNullable(person)
.map(Peron::getAge)
.filter(p -> p >= 25)
.filter(p -> p <= 35)
.isPresent();
}
使用Optional看上去就清爽多了,这里,map()仅仅是将一个值转换为另一个值,并且这个操作并不会改变原来的值。
8.8.map
如果有值对其处理,并返回处理后的Optional,否则返回 Optional.empty()。
List<String> names = Arrays.asList("hello001", "hello002", "", "hello003", "", "hello004");
Optional<List<String>> listOptional = Optional.of(names);
int size = listOptional.map(List::size).orElse(0);
assertEquals(6, size);
在这个例子中,我们使用一个List集合封装了一些字符串,然后再把这个List使用Optional封装起来,对其map(),获取List集合的长度。map()返回的结果也被封装在一个Optional对象中,这里当值不存在的时候,我们会默认返回0。如下我们获取一个字符串的长度。
String name = "hello";
Optional<String> nameOptional = Optional.of(name);
int len = nameOptional.map(String::length()).orElse(0);
assertEquals(6, len);
我们也可以将map()方法与filter()方法结合使用,如下所示。
String password = " password ";
Optional<String> passOpt = Optional.of(password);
boolean correctPassword = passOpt.filter(
pass -> pass.equals("password")).isPresent();
assertFalse(correctPassword);
correctPassword = passOpt
.map(String::trim)
.filter(pass -> pass.equals("password"))
.isPresent();
assertTrue(correctPassword);
上述代码的含义就是对密码进行验证,查看密码是否为指定的值。
8.9.flatMap
与 map 类似,要求返回值必须是Optional。
假设我们现在有一个Person类。
public class Person {
private String name;
private int age;
private String password;
public Optional<String> getName() {
return Optional.ofNullable(name);
}
public Optional<Integer> getAge() {
return Optional.ofNullable(age);
}
}
public Optional<String> getPassword() {
return Optional.ofNullable(password);
}
// 忽略get set方法
接下来,我们可以将Person封装到Optional中,并进行测试,如下所示。
Person person = new Person("hello", 18);
Optional<Person> personOptional = Optional.of(person);
Optional<Optional<String>> nameOptionalWrapper = personOptional.map(Person::getName);
Optional<String> nameOptional = nameOptionalWrapper.orElseThrow(IllegalArgumentException::new);
String name1 = nameOptional.orElse("");
assertEquals("hello", name1);
String name = personOptional
.flatMap(Person::getName)
.orElse("");
assertEquals("hello", name);
注意:方法getName返回的是一个Optional对象,如果使用map,我们还需要再调用一次get()方法,而使用flatMap()就不需要了。
8.10.使用误区
关于使用Optional的误区有以下:
- 正确的使用创建方法,不确定是否为null时尽量选择ofNullable方法。
- 避免用在成员变量上(原因上面已经提到);
- 避免直接调用Optional对象的get和isPresent方法;
最后一条可能难理解,试想一下如果先用isPresent方法获得是否存在,然后决定是否调用get方法和之前的ifelse判断并无二致。
9.新时间日期API
9.1.本地时间——使用 LocalDate、 LocalTime、 LocalDateTime
LocalDate、 LocalTime、 LocalDateTime 类的实例是不可变的对象,分别表示使用 ISO-8601日历系统的日期、时间、日期和时间。它们提供了简单的日期或时间,并不包含当前的时间信息。也不包含与时区相关的信息。
注: ISO-8601日历系统是国际标准化组织制定的现代公民的日期和时间的表示法
方法描述
方法 | 描述 |
---|---|
now() | 静态方法,根据当前时间创建对象 |
of() | 静态方法,根据指定日期/时间创建 对象 |
plusDays, plusWeeks, plusMonths, plusYears | 向当前 LocalDate 对象添加几天、 几周、 几个月、 几年 |
minusDays, minusWeeks, minusMonths, minusYears | 从当前 LocalDate 对象减去几天、 几周、 几个月、 几年 |
plus, minus | 添加或减少一个 Duration 或 Period |
withDayOfMonth, withDayOfYear, withMonth, withYear | 将月份天数、 年份天数、 月份、 年 份 修 改 为 指 定 的值 并 返 回 新 的 LocalDate 对象 |
getDayOfMonth | 获得月份天数(1-31) |
getDayOfYear | 获得年份天数(1-366) |
getDayOfWeek | 获得星期几(返回一个 DayOfWeek 枚举值) |
getMonth | 获得月份, 返回一个 Month 枚举值 |
getMonthValue | 获得月份(1-12) |
getYear | 获得年份 |
until | 获得两个日期之间的 Period 对象, 或者指定ChronoUnits 的数字 |
isBefore, isAfter | 比较两个 LocalDate |
isLeapYear | 判断是否是闰年 |
示例代码如下:
// 获取当前系统时间
LocalDateTime localDateTime1 = LocalDateTime.now();
System.out.println(localDateTime1);
// 运行结果:2024-08-26T16:02:02.391979
// 指定日期时间
LocalDateTime localDateTime2 = LocalDateTime.of(2024, 10, 27, 13, 45,10);
System.out.println(localDateTime2);
// 运行结果:2024-10-27T13:45:10
LocalDateTime localDateTime3 = localDateTime1
// 加三年
.plusYears(3)
// 减三个月
.minusMonths(3);
System.out.println(localDateTime3);
// 运行结果:2027-05-26T16:02:43.919851600
System.out.println(localDateTime1.getYear());
// 运行结果:2024
System.out.println(localDateTime1.getMonthValue()); // 运行结果:8
System.out.println(localDateTime1.getDayOfMonth()); // 运行结果:26
System.out.println(localDateTime1.getHour());
// 运行结果:16
System.out.println(localDateTime1.getMinute());
System.out.println(localDateTime1.getSecond());
LocalDateTime localDateTime4 = LocalDateTime.now();
System.out.println(localDateTime4);
// 运行结果:2
// 运行结果:43
// 2024-08-26T16:02:43.919851600
LocalDateTime localDateTime5 = localDateTime4.withDayOfMonth(10);
System.out.println(localDateTime5);
// 2024-08-10T16:02:43.919851600
9.2.Instant时间戳
用于“时间戳”的运算。它是以Unix元年(传统的设定为UTC时区1970年1月1日午夜时分)开始所经历的描述进行运算 。
示例代码如下所示。
Instant instant1 = Instant.now();
System.out.println(instant1);
// 运行结果:2024-08-26T08:07:55.284336100Z
// 默认获取UTC时区
// 偏移量运算
OffsetDateTime offsetDateTime = instant1.atOffset(ZoneOffset.ofHours(8));
System.out.println(offsetDateTime);
// 运行结果:2024-08-26T16:07:55.284336100+08:00
// 获取时间戳
System.out.println(instant1.toEpochMilli());
// 运行结果:1724659675284
// 以Unix元年为起点,进行偏移量运算
Instant instant2 = Instant.ofEpochSecond(60);
System.out.println(instant2);
// 运行结果:1970-01-01T00:01:00Z
9.3. Duration 和 Period
Duration:用于计算两个“时间”间隔。
Period:用于计算两个“日期”间隔。
Instant instant_1 = Instant.now();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
Instant instant_2 = Instant.now();
Duration duration = Duration.between(instant_1, instant_2);
System.out.println(duration.toMillis());
// 运行结果:1000
LocalTime localTime_1 = LocalTime.now();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
LocalTime localTime_2 = LocalTime.now();
System.out.println(Duration.between(localTime_1, localTime_2).toMillis());
// 运行结果:1000
LocalDate localDate_1 = LocalDate.of(2018,9, 9);
LocalDate localDate_2 = LocalDate.now();
Period period = Period.between(localDate_1, localDate_2);
System.out.println(period.getYears());
// 运行结果:5
System.out.println(period.getMonths());
System.out.println(period.getDays());
// 运行结果:11
// 运行结果:17
9.4.日期的操纵
TemporalAdjuster : 时间校正器。有时我们可能需要获取例如:将日期调整到“下个周日”等操作。
TemporalAdjusters : 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现。
例如获取下个周日,如下所示:
LocalDate nextSunday = LocalDate.now().with(TemporalAdjusters.next(DayOfWeek.SUNDAY));
完整的示例代码如下所示。
LocalDateTime localDateTime1 = LocalDateTime.now();
System.out.println(localDateTime1);
// 2024-08-26T16:29:02.176744400
// 获取这个第一天的日期
System.out.println(localDateTime1.with(TemporalAdjusters.firstDayOfMonth()));
// 2024-08-01T16:29:02.176744400
// 获取下个周末的日期
System.out.println(localDateTime1.with(TemporalAdjusters.next(DayOfWeek.SUNDAY))
);
// 2024-09-01T16:29:02.176744400
// 自定义:下一个工作日
LocalDateTime localDateTime2 = localDateTime1.with(l -> {
LocalDateTime localDateTime = (LocalDateTime) l;
DayOfWeek dayOfWeek = localDateTime.getDayOfWeek();
if (dayOfWeek.equals(DayOfWeek.FRIDAY)) {
return localDateTime.plusDays(3);
} else if (dayOfWeek.equals(DayOfWeek.SATURDAY)) {
return localDateTime.plusDays(2);
} else {
return localDateTime.plusDays(1);
}
});
System.out.println(localDateTime2);
9.5. 解析与格式化
java.time.format.DateTimeFormatter 类:该类提供了三种格式化方法:
- 预定义的标准格式
- 语言环境相关的格式
- 自定义的格式
示例代码如下所示。
DateTimeFormatter dateTimeFormatter1 = DateTimeFormatter.ISO_DATE;
LocalDateTime localDateTime = LocalDateTime.now();
String strDate1 = localDateTime.format(dateTimeFormatter1);
System.out.println(strDate1);
// 运行结果:2024-08-26
// Date -> String
DateTimeFormatter dateTimeFormatter2 = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String strDate2 = dateTimeFormatter2.format(localDateTime);
System.out.println(strDate2);
// 运行结果:2024-08-26 16:33:03
// String -> Date
LocalDateTime localDateTime1 = localDateTime.parse(strDate2,dateTimeFormatter2);
System.out.println(localDateTime1);
9.6.时区的处理
Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDate、 ZonedTime、
ZonedDateTime。
其中每个时区都对应着 ID,地区ID都为 “{区域}/{城市}”的格式,例如 : Asia/Shanghai等。
- ZoneId:该类中包含了所有的时区信息
- getAvailableZoneIds() : 可以获取所有时区时区信息
- of(id) : 用指定的时区信息获取 ZoneId 对象
示例代码如下所示。
// 获取所有的时区
Set<String> set = ZoneId.getAvailableZoneIds();
System.out.println(set);
// 通过时区构建LocalDateTime
LocalDateTime localDateTime1 =
LocalDateTime.now(ZoneId.of("America/El_Salvador"));
System.out.println(localDateTime1);
// 以时区格式显示时间
LocalDateTime localDateTime2 = LocalDateTime.now();
ZonedDateTime zonedDateTime1 =
localDateTime2.atZone(ZoneId.of("Africa/Nairobi"));
System.out.println(zonedDateTime1);
10. JDK8 HashMap
- JDK7 HashMap结构为数组+链表(发生元素碰撞时,会将新元素添加到链表开头)
- JDK8 HashMap结构为数组+链表+红黑树(发生元素碰撞时,会将新元素添加到链表末尾,当HashMap总容量大于等于64,并且某个链表的大小大于等于8,会将链表转化为红黑树(注意:红黑树是二叉树的一种))
JDK8 HashMap重排序:如果删除了HashMap中红黑树的某个元素导致元素重排序时,不需要计算待重排序的元素的HashCode码,只需要将当前元素放到(HashMap总长度+当前元素在HashMap中的位置)的位置即可。
11.Fork/Join 框架
11.1简单概述
Fork/Join 框架: 就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。
11.2.Fork/Join 框架与传统线程池的区别
采用 “工作窃取”模式(work-stealing):
当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中。
相对于一般的线程池实现,fork/join框架的优势体现在对其中包含的任务的处理方式上.在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态.而在fork/join框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行.那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行.这种方式减少了线程的等待时间,提高了性能。
11.3Fork/Join框架实例
使用Fork/Join框架实现累加和的示例程序:
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.Future;
import java.util.concurrent.RecursiveTask;
public class ForkJoinTaskExample extends RecursiveTask<Integer> {
public static final int threshold = 2;
private int start;
private int end;
public ForkJoinTaskExample(int start, int end) {
this.start = start;
this.end = end;
}
@Override
protected Integer compute() {
int sum = 0;
//如果任务足够小就计算任务
boolean canCompute = (end - start) <= threshold;
if (canCompute) {
for (int i = start; i <= end; i++) {
sum += i;
}
} else {
// 如果任务大于阈值,就分裂成两个子任务计算
int middle = (start + end) / 2;
ForkJoinTaskExample leftTask = new ForkJoinTaskExample(start, middle);
ForkJoinTaskExample rightTask = new ForkJoinTaskExample(middle +1, end);
// 执行子任务
leftTask.fork();
rightTask.fork();
// 等待任务执行结束合并其结果
int leftResult = leftTask.join();
int rightResult = rightTask.join();
// 合并子任务
sum = leftResult + rightResult;
}
return sum;
}
public static void main(String[] args) {
ForkJoinPool forkjoinPool = new ForkJoinPool();
//生成一个计算任务,计算1+2+3+4
ForkJoinTaskExample task = new ForkJoinTaskExample(1, 100);
//执行一个任务
Future<Integer> result = forkjoinPool.submit(task);
try {
log.info("result:{}", result.get());
} catch (Exception e) {
log.error("exception", e);
}
}
}
12.注解
12.1.注解基本概念
注解就相当于一种标记,在程序中加了注解就等于为程序加了某种标记。所有的注解类型都继承自这个普通的接口(java.lang.annotation.Annotation)。
标记可以加在包、类、字段、方法,方法参数以及局部变量上。可以同时存在多个注解。
定义注解需要的基础注解信息如下所示。
@SuppressWarnings("deprecation") //编译器警告过时(source阶段)
@Deprecated
//过时(Runtime阶段)
@Override
//重写(source阶段)
@Retention(RetentionPolicy.RUNTIME)
//保留注解到程序运行时。(Runtime阶段)
@Target({ElementType.METHOD,ElementType.TYPE})
//标记既能定义在方法上,又能定义在类、接口、枚举上等。
12.1.1元注解
元注解是用于修饰其它注解的注解
- @Documented:指定了被修饰的注解是可以Javadoc等工具文档化
- @Retention:指定了被修饰的注解的生命周期
- @Target:指定被修饰的注解的作用范围
- @Inherited:指定了被修饰的注解修饰程序元素的时候是可以被子类继承的
@Documented:用于指定被该元注解修饰的注解类将被javadoc工具提取成文档。默认情况下,javadoc是 不包括注解的,但是加上了这个注解生成的文档中就会带着注解了。定义为@Documented 的注解必须设置Retention值为RUNTIME。
@Retention
只能用于修饰一个 Annotation 定义, 用于指定该 Annotation 可以保留多长时间,@Rentention 包含一个 RetentionPolicy 类型的成员变量, 使用 @Rentention 时必须为该 value 成员变量指定值(值有三种)。
RetentionPolicy.SOURCE: 编译器使用后,直接丢弃这种策略的注释。
RetentionPolicy.CLASS: 编译器将把注解记录在 class 文件中 . 当运行 Java 程序时 , JVM 不会保留注解。 这是默认值。
RetentionPolicy.RUNTIME: 编译器将把注解记录在 class 文件中 . 当运行 Java 程序时 , JVM 会保留注解 . 程序可以 通过反射获取该注解。
注意: 我们常用的定义即是RetentionPolicy.RUNTIME,因为我们使用反射来实现的时候是需要从JVM中获取class类对象并操作类对象的。
三个生命周期是 源码阶段 - > class类对象阶段 - > Runtime运行时阶段。
@Target
用于修饰注解的注解,用于指定被修饰的注解能用于修饰哪些程序元素。@Target也包含一个名为value的成员变量。
ElementType.ANNOTATION_TYPE 用于描述注解
ElementType.CONSTRUCTOR 用于描述构造方法
ElementType.FIELD 用于描述属性
ElementType.LOCAL_VARIABLE 用于描述局部变量
ElementType.METHOD 用于描述方法
ElementType.PACKAGE 用于描述包
ElementType.PARAMETER 用于描述方法内的参数
ElementType.TYPE 用于描述类型进行注解,比如类、接口、枚举
@Inherited
被它修饰的Annotation将具有继承性。如果某个类使用了被@Inherited修饰的Annotation,则其子类将自动具有该注解。
12.1.2. 自定义注解
格式:public @Interface 注解名 {
属性列表/无属性
}
注解体中的属性要求(返回值类型):基本数据类型、String类型、、Class类型、枚举类型、注解、以上类型的数组。
注解的属性也叫做成员变量。注解只有成员变量,没有方法。
注意:
- 在这里不能有void的无返回值类型和以上类型以外的类型
- 定义的属性,在使用时需要给注解中的属性赋值
- 如果定义属性时,使用default关键字给属性默认初始化值,则使用注解时可以不为属性赋值,它取的是默认值。如果为它再次传入值,那么就发生了对原值的覆盖。
- 如果只有一个属性需要赋值,并且属性的名称为value,则赋值时value可以省略,可以直接定义值
- 数组赋值时,值使用{}存储值。如果数组中只有一个值,则可以省略{ }
12.1.3.注解的提取
注解通过反射获取。首先可以通过 Class 对象的 isAnnotationPresent() 方法判断它是否应用了某个注解。然后通过 getAnnotation() 方法来获取 Annotation 对象,或者是 getAnnotations() 方法。如果获取到的 Annotation 如果不为 null,则就可以调用它们的属性方法了。比如
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
public int id()default -1;
public String msg()default "Hi";
}
@TestAnnotation
public class TestAn {
public static void main(String[] args) {
boolean hasAnnotation = TestAn.class.isAnnotationPresent(TestAnnotation.class);
if (hasAnnotation) {
TestAnnotation testAnnotation = TestAn.class.getAnnotation(TestAnnotation.class);
System.out.println("id:" + testAnnotation.id());
System.out.println("msg:" + testAnnotation.msg());
}
}
}
12.2. Java8中的注解
对于注解(也被称做元数据),Java 8 主要有两点改进:类型注解和重复注解。
12.2.1.类型注解
1)Java 8 的类型注解扩展了注解使用的范围。
在java 8之前,注解只能是在声明的地方所使用,java8开始,注解可以应用在任何地方。如:
创建类实例
new @Interned MyObject();
类型映射
myString = (@NonNull String) str;
implements 语句中
class UnmodifiableList<T> implements@Readonly List<@Readonly T> { ... }
throw exception声明
void monitorTemperature() throws@Critical TemperatureException { ... }
注意:
在Java 8里面,当类型转化甚至分配新对象的时候,都可以在声明变量或者参数的时候使用注解。Java注解可以支持任意类型。
类型注解只是语法而不是语义,并不会影响java的编译时间,加载时间,以及运行时间,也就是说,编译成class文件的时候并不包含类型注解。
2)新增ElementType.TYPE_USE 和ElementType.TYPE_PARAMETER(在Target上)
新增的两个注释的程序元素类型 ElementType.TYPE_USE 和 ElementType.TYPE_PARAMETER用来描述
注解的新场合。
- ElementType.TYPE_PARAMETER 表示该注解能写在类型变量的声明语句中。
- ElementType.TYPE_USE 表示该注解能写在使用类型的任何语句中(例如:声明语句、泛型和强制转换语句中的类型)。
例如,下面的示例。
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@interface MyAnnotation {}
3)类型注解的作用
类型注解被用来支持在Java的程序中做强类型检查。配合第三方插件工具Checker Framework(注:此插件so easy,这里不介绍了),可以在编译的时候检测出runtime error(例如:UnsupportedOperationException;NumberFormatException;NullPointerException异常等都是runtime error),以提高代码质量。这就是类型注解的作用。
注意:使用Checker Framework可以找到类型注解出现的地方并检查。
例如下面的代码。
import checkers.nullness.quals.*;
public class TestDemo{
void sample() {
@NonNull Object my = new Object();
}
}
使用javac编译上面的类:(当然若下载了Checker Framework插件就不需要这么麻烦了)
javac -processor checkers.nullness.NullnessChecker TestDemo.java
上面编译是通过的,但若修改代码:
@NonNull Object my = null;
但若不想使用类型注解检测出来错误,则不需要processor,正常javac TestDemo.java是可以通过编译
的,但是运行时会报 NullPointerException 异常。
为了能在编译期间就自动检查出这类异常,可以通过类型注解结合 Checker Framework 提前排查出来
错误异常。
注意java 5,6,7版本是不支持注解@NonNull,但checker framework 有个向下兼容的解决方案,就是将
类型注解@NonNull 用/**/注释起来。
import checkers.nullness.quals.*;
public class TestDemo{
void sample() {
/*@NonNull*/ Object my = null;
}
}
这样javac编译器就会忽略掉注释块,但用checker framework里面的javac编译器同样能够检测出@NonNull错误。
通过 类型注解 + checker framework 可以在编译时就找到runtime error。
12.2.2 重复注解
允许在同一声明类型(类,属性,或方法)上多次使用同一个注解。
Java8以前的版本使用注解有一个限制是相同的注解在同一位置只能使用一次,不能使用多次。
Java 8 引入了重复注解机制,这样相同的注解可以在同一地方使用多次。重复注解机制本身必须用 @Repeatable 注解。
实际上,重复注解不是一个语言上的改变,只是编译器层面的改动,技术层面仍然是一样的。
例如,我们可以使用如下示例来具体对比Java8之前的版本和Java8中的注解。
1)自定义一个包装类Hints注解用来放置一组具体的Hint注解
@interface MyHints {
Hint[] value();
}
@Repeatable(MyHints.class)
@interface Hint {
String value();
}
使用包装类当容器来存多个注解(旧版本方法)
@MyHints({@Hint("hint1"), @Hint("hint2")})
class Person {}
使用多重注解(新方法)
@Hint("hint1")
@Hint("hint2")
class Person {}
2)完整类测试如下所示。
public class RepeatingAnnotations {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Filters {
Filter[] value();
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Repeatable(Filters.class)
public @interface Filter {
String value();
}
@Filter("filter1")
@Filter("filter2")
public interface Filterable {
public static void main(String[] args) {
for (Filter filter : Filterable.class.getAnnotationsByType(Filter.class)) {
System.out.println(filter.value());
}
}
}
分析:
注释Filter被@Repeatable( Filters.class )注释。Filters 只是一个容器,它持有Filter, 编译器尽力向程序员隐藏它的存在。通过这样的方式,Filterable接口可以被Filter注释两次。
另外,反射的API提供一个新方法getAnnotationsByType() 来返回重复注释的类型(注意
Filterable.class.getAnnotation( Filters.class )将会返回编译器注入的Filters实例。
3)java 8之前也有重复使用注解的解决方案,但可读性不好。
public @interface MyAnnotation {
String role();
}
public @interface Annotations {
MyAnnotation[] value();
}
public class RepeatAnnotationUseOldVersion {
@Annotations({@MyAnnotation(role="Admin"),@MyAnnotation(role="Manager")})
public void doSomeThing(){
}
}
Java8的实现方式(由另一个注解来存储重复注解,在使用时候,用存储注解Authorities来扩展重复注解),可读性更强。
@Repeatable(Annotations.class)
public @interface MyAnnotation {
String role();
}
public @interface Annotations {
MyAnnotation[] value();
}
public class RepeatAnnotationUseOldVersion {
@MyAnnotation(role="Admin")
@MyAnnotation(role="Manager")
public void doSomeThing(){
}
}