Java8新特性简介
Java 8 (又称为 JDK 1.8) 是 Java 语言开发的一个主要版本。Java 8 是 Oracle 公司于 2014 年 3 月发布,可以看成是自 Java 5 以来最具革命性的版本。Java 8 为 Java 语言、编译器、类库、开发工具与 JVM 带来了大量新特性。
- 速度更快
- 代码更少(增加了新的语法 Lambda 表达式)
- 强大的 Stream API
- 优化 Fork/Join 框架,更加容易写并行代码
- 最大化减少空指针异常 Optional
- 时间 API
......
其中最为核心的是 Lambda 表达式和 Stream API。
哈希结构
Java8 优化了哈希算法,JDK7 之前是数组 + 链表,当两个不同对象经过哈希算法算出的下标相同时,则该数组下标处形成链表,旧的在链表最后面。但是这样产生很长的链表时,恰巧我们需要的对象在最后一个,则需要从头遍历到尾,效率低。
JDK8 变成了数组 + 链表 + 红黑树,当链表的长度超出 8 时,并且整个数组的长度超出 64 时,则链表转为红黑树,即一种二叉树,这样减少了从头遍历到尾的效率低问题,只需要在二叉树的一方遍历,找出对象即可,不需要像全部遍历到最后。
JVM方法区
永久代改为元空间,即最大内存变成电脑的最大内存。
因为永久代的垃圾回收条件苛刻,所以容易导致内存不足,而转为元空间,使用本地内存,极大减少了 OOM 异常,毕竟,现在电脑的内存足以支持 Java 的允许。
首先明确:只有 Hotspot 才有永久代。BEA JRockit、IBMJ9 等来说,是不存在永久代的概念的。原则上如何实现方法区属于虚拟机实现细节,不受《Java 虚拟机规范》管束,并不要求统一。
Hotspot 中方法区的变化:
| 版本 | 变化 | | ---------- | ----------------------------------------------- | | JDK1.6 及以前 | 有永久代,静态变量存储在永久代上 | | JDK1.7 | 有永久代,但已经逐步「去永久代」,字符串常量池,静态变量移除,保存在堆中 | | JDK1.8 | 无永久代,类型信息,字段,方法,常量保存在本地内存的元空间,但字符串常量池、静态变量仍然在堆中 |
为什么永久代要被元空间替代?
随着 Java8 的到来,HotSpot VM 中再也见不到永久代了。但是这并不意味着类的元数据信息也消失了。这些数据被移到了一个与堆不相连的本地内存区域,这个区域叫做元空间(Metaspace)。
由于类的元数据分配在本地内存中,元空间的最大可分配空间就是系统可用内存空间,这项改动是很有必要的,原因有:
- 为永久代设置空间大小是很难确定的
在某些场景下,如果动态加载类过多,容易产生 Perm 区的 OOM。比如某个实际 Web 工程中,因为功能点比较多,在运行过程中,要不断动态加载很多类,经常出现致命错误。
Exception in thread‘dubbo client x.x connector'java.lang.OutOfMemoryError:PermGen space
而元空间和永久代之间最大的区别在于:元空间并不在虚拟机中,而是使用本地内存。
因此,默认情况下,元空间的大小仅受本地内存限制。
对永久代进行调优是很困难的。
主要是为了降低 Full GC
有些人认为方法区(如 HotSpot 虚拟机中的元空间或者永久代)是没有垃圾收集行为的,其实不然。《Java 虚拟机规范》对方法区的约束是非常宽松的,提到过可以不要求虚拟机在方法区中实现垃圾收集。事实上也确实有未实现或未能完整实现方法区类型卸载的收集器存在(如 JDK11 时期的 ZGC 收集器就不支持类卸载)。
一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。但是这部分区域的回收有时又确实是必要的。以前 Sun 公司的 Bug 列表中,曾出现过的若干个严重的 Bug 就是由于低版本的 HotSpot 虚拟机对此区域未完全回收而导致内存泄漏。
方法区的垃圾收集主要回收两部分内容:常量池中废弃的常量和不在使用的类型。
Lambda表达式
为什么使用Lambda表达式
Lambda 是一个 匿名函数,我们可以把 Lambda 表达式理解为是 一段可以传递的代码(将代码像数据一样进行传递)。可以写出更简洁、更灵活的代码。作为一种更紧凑的代码风格,使 Java 的语言表达能力得到了提升。
从匿名类到 Lambda 的转换
// 原来的匿名内部类 public void test1(){ Comparator
由此我们可以知道,Lambada 表达式替换了匿名内部类,自动实现匿名内部类的唯一接口。
逐步优化代码示例(顺便了解 Stream API)
首先是没有优化的代码,也就是初学者经常使用的代码,获取年龄小于 35 的员工信息、获取公司中工资大于 5000 的员工信息
``` public class TestLambda { List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) ); // 需求:获取公司中年龄小于 35 的员工信息 public List filterEmployeeAge(List emps){ List list = new ArrayList<>();
for (Employee emp : emps) { if(emp.getAge() <= 35){ list.add(emp); }}return list;
} // 需求:获取公司中工资大于 5000 的员工信息 public List filterEmployeeSalary(List emps){ List list = new ArrayList<>();
for (Employee emp : emps) { if(emp.getSalary() >= 5000){ list.add(emp); }}return list;
}
} ```
优化方式一:策略设计模式 + 匿名内部类
首先创建策略设计模式接口
public interface MyPredicate
然后利用匿名内部类实现
``` public class TestLambda { List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) ); // 需求:获取公司中年龄小于 35 的员工信息 public void filterEmployeeAge(){ List list = filterEmployee(emps, new MyPredicate () { @Override public boolean test(Employee t) { return t.getId() <= 103; } });
for (Employee employee : list) { System.out.println(employee);}
} // 需求:获取公司中工资大于 5000 的员工信息 public void filterEmployeeSalary(){ List list = filterEmployee(emps, new MyPredicate () { @Override public boolean test(Employee t) { return t.getSalary() >= 5000; } });
for (Employee employee : list) { System.out.println(employee);}
} } ```
优化方式三:策略设计模式 + Lambda 表达式
``` public class TestLambda { List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) ); // 需求:获取公司中年龄小于 35 的员工信息 public void test6(){ List list = filterEmployee(emps, (e) -> e.getAge() <= 35); list.forEach(System.out::println);
System.out.println("------------------------------------------");List list2 = filterEmployee(emps, (e) -> e.getSalary() >= 5000);list2.forEach(System.out::println);
} } ```
优化方式四:Stream API
注意:不需要策略设计模式
``` public class TestLambda { List emps = Arrays.asList( new Employee(101, "张三", 18, 9999.99), new Employee(102, "李四", 59, 6666.66), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) ); public void test7(){ emps.stream() .filter((e) -> e.getAge() <= 35) .forEach(System.out::println);
System.out.println("----------------------------------------------");emps.stream() .map(Employee::getName) .limit(3) .sorted() .forEach(System.out::println);
} } ```
Lambda表达式语法
Lambda 表达式在 Java 语言中引入了一个新的语法元素和操作符。这个操作符为 ->
, 该操作符被称为 Lambda 操作符或箭头操作符。它将 Lambda 分为两个部分:
- 左侧:指定了 Lambda 表达式需要的所有参数
- 右侧:指定了 Lambda 体,即 Lambda 表达式要执行的功能
语法格式一:无参,无返回值,Lambda 体只需一条语句
public void test1(){ Runnable r1 = () -> System.out.println("Hello Lambda!"); }
语法格式二:Lambda 需要一个参数,并且无返回值
public void test2(){ Consumer
语法格式三:Lambda 只需要一个参数时,参数的小括号可以省略
public void test2(){ Consumer
语法格式四:Lambda 需要两个参数,并且有返回值
public void test3(){ Comparator
语法格式五:当 Lambda 体只有一条语句时,return 与大括号可以省略
public void test4(){ Comparator
语法格式六:数据类型可以省略,因为可由编译器推断得出,称为「类型推断」
public void test4(){ // 类型不省略 Comparator
类型推断
上述 Lambda 表达式中的参数类型都是由编译器推断得出的。Lambda 表达式中无需指定类型,程序依然可以编译,这是因为 javac 根据程序的上下文,在后台推断出了参数的类型。Lambda 表达式的类型依赖于上下文环境,是由编译器推断出来的。这就是所谓的「类型推断」。
函数式接口
什么是函数式接口
只包含一个抽象方法的接口,称为函数式接口。
你可以通过 Lambda 表达式来创建该接口的对象。(若 Lambda 表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。
我们可以在任意函数式接口上使用 @FunctionalInterface
注解,这样做可以检查它是否是一个函数式接口,同时 javadoc 也会包含一条声明,说明这个接口是一个函数式接口。
自定义函数式接口
使用 @FunctionalInterface
注解
@FunctionalInterface public interface MyFunction { public String getValue(String str); }
函数式接口中使用泛型
@FunctionalInterface public interface MyFunction2
作为参数传递Lambda表达式
这里面使用了自定义函数式接口的第一个例子。
public String toUpperString(MyFunction mf, String str) { return mf.getValue(str) }
作为参数传递 Lambda 表达式:
String newStr = toUpperString( (str) -> str.toUpperCase(), "abcdef"); System.out.println(newStr);
作为参数传递 Lambda 表达式:为了将 Lambda 表达式作为参数传递,接收 Lambda 表达式的参数类型必须是与该 Lambda 表达式兼容的函数式接口的类型。
四大核心函数式接口
这是 Java 为我们提供的四个函数式接口,所以简单的函数式接口不用自己写。
| 函数式接口 | 参数类型 | 返回类型 | 用途 | | --------------------- | ---- | ------- | ---------------------------------------------------------- | | Consumer
消费型接口 | T | void | 对类型为 T 的对象应用操作,包含方法 void accept(T t);
| | Supplier
供给型接口 | 无 | T | 返回类型为 T 的对象,包含方法 T get();
| | Function
函数型接口 | T | R | 对类型为 T 的对象应用操作,并返回结果。结果是 R 类型的对象。包含方法 R apply(T t);
| | Predicate
断言型接口 | T | boolean | 确定类型为 T 的对象是否满足某约束,并返回 boolean 值。包含方法 boolean test(T t);
|
消费型接口
Consumer
接口的方法
void accept(T t);
没有返回值,参数一去不复返
public void happy(double money, Consumer
供给型接口
Supplier
接口的方法
T get();
没有参数,返回值是泛型 T,也就是参数进入接口后,回来的还是自己(值可能发生改变)
// 产生指定个数的整数,并放入集合中 public List
函数型接口
Function
接口的方法
R apply(T t);
参数是 T,返回值是 R,也就是 T 参数进入接口后,处理成 R 的类型并返回
// 用于处理字符串 public String strHandler(String str, Function
断言型接口
Predicate
接口的方法
boolean test(T t);
对 T 参数进行处理后,返回类型是 boolean 值
// 将满足条件的字符串,放入集合中 public List
其他接口
| 函数式接口 | 参数类型 | 返回类型 | 用途 | | ----------------------------------- | ------ | ------ | --------------------------------------------------------- | | BiFunction
| 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 | 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 类型的函数 |
三大引用
方法引用
当要传递给 Lambda 体的操作,已经有实现的方法了,可以使用方法引用。
方法引用:使用操作符 ::
将方法名和对象或类的名字分隔开来。如下三种主要使用情况:
- 对象::实例方法
- 类::静态方法
- 类::实例方法
例 1(类::静态方法):
(x) -> System.out.println(x); // 等价于 System.out::println; public void test1(){ Consumer
自动读取参数列表,放到 ::
后面的方法里。
例 2(对象::实例方法):
Binaryoperator
自动读取参数列表,放到 ::
后面的方法里。
例 3(类::实例方法):
compare((x, y) -> x.equals(y), "abcdef", "abcdef" ); // 等价于 compare(String::equals, "abcdef", "abcdef" ); public void test5(){ BiPredicate
当 ::
前面是类型,后面是实例方法,则自动读取参数列表,第一个参数放到 ::
的前面,其他参数放到 ::
后面的方法里。
注意:
- 方法引用所引用的方法的参数列表与返回值类型,需要与函数式接口中抽象方法的参数列表和返回值类型保持一致
- 若 Lambda 的参数列表的第一个参数,是实例方法的调用者,第二个参数(或无参)是实例方法的参数时,格式:
ClassName::MethodName
,如例 3
构造器引用
格式:ClassName::new
与函数式接口相结合,自动与函数式接口中方法兼容。可以把构造器引用赋值给定义的方法,与构造器参数列表要与接口中抽象方法的参数列表一致。
public void test7(){ Function
数组引用
格式:ClassName[]::new
public void test8(){ Function
额外 - 遍历集合
利用引用来快速遍历 List、Set、Map 等集合的数据。
public void testCollection(){ //List: List
Stream API
了解Stream
Java8 中有两大最为重要的改变。第一个是 Lambda 表达式;另外一个则是 Stream API(java.util.stream.*)
。
Stream 是 Java8 中处理集合的关键抽象概念,它可以指定你希望对集合进行的操作,可以执行非常复杂的查找、过滤和映射数据等操作。
使用 Stream API 对集合数据进行操作,就类似于使用 SQL 执行的数据库查询。也可以使用 Stream API 来并行执行操作。简而言之,Stream API 提供了一种高效且易于使用的处理数据的方式。
什么是Stream
Stream 是流,是数据渠道,用于操作数据源(集合、数组等)所生成的元素序列。「集合讲的是数据,流讲的是计算」
注意:
- Stream 自己不会存储元素
- Stream 不会改变源对象。相反,他们会返回一个持有结果的新 Stream
- Stream 操作是延迟执行的。这意味着他们会等到需要结果的时候才执行
操作三个步骤
创建 Stream
一个数据源(如:集合、数组),获取一个流
中间操作
一个中间操作链,对数据源的数据进行处理
终止操作(终端操作)
一个终止操作,执行中间操作链,并产生结果
Stream创建
由集合创建流
Java8 中的 Collection 接口被扩展,提供了两个获取流的方法:
default Stream stream()
: 返回一个串行流(顺序流)default Stream parallelStream()
: 返回一个并行流
public void test1(){ // Collection 提供了两个方法 stream() 与 parallelStream() List
由数组创建流
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)
public void test1(){ // 通过 Arrays 中的 stream() 获取一个数组流 Integer[] nums = new Integer[10]; Stream
由值创建流
可以使用静态方法 Stream.of()
,通过显示值 创建一个流。它可以接收任意数量的参数
public static Stream of(T... values)
: 返回一个流
public void test1(){ // 通过 Stream 类中静态方法 of() Stream
由函数创建流:创建无限流
可以使用静态方法 Stream.iterate()
和 Stream.generate()
,创建无限流
- 迭代:
public static Stream iterate(final T seed, final UnaryOperator f)
- 生成:
public static Stream generate(Supplier s);
public void test1(){ // 创建无限流 // 迭代 Stream
Stream中间操作
多个中间操作可以连接起来形成一个流水线,除非流水线上触发终止操作,否则中间操作不会执行任何的处理。而在终止操作时一次性全部处理,称为「惰性求值」。
筛选与切片
| 方法 | 描述 | | ------------------- | ------------------------------------------------------- | | filter(Predicate p) | 接收 Lambda,从流中排除某些元素 | | distinct() | 筛选,通过流所生成元素的 hashCode()
和 equals()
去除重复元素 | | limit(long maxSize) | 截断流,使其元素不超过给定数量 | | skip(long n) | 跳过元素,返回一个扔掉了前 n 个元素的流。若流中元素不足 n 个,则返回一个空流。与 limit(n) 互补 |
代码示例:
public class TestStreamaAPI { List
映射
| 方法 | 描述 | | ------------------------------- | -------------------------------------------- | | map(Function f) | 接收一个函数作为参数,该函数会被应用到每个元素上,并将其映射成一个新的元素 | | mapToDouble(ToDoubleFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 DoubleStream | | mapToInt(ToIntFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 IntStream | | mapToLong(ToLongFunction f) | 接收一个函数作为参数,该函数会被应用到每个元素上,产生一个新的 LongStream | | flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都换成另一个流,然后把所有流连接成一个流 |
代码示例:
public class TestStreamAPI1 { List
如何区分 map
和 flatMap
?利用去重 distinct
来讲解
public void testMap(){ String[] words = new String[]{"Hello", "World"}; List
数组使用 distinct
去重是比较数组不同下标的元素,如 Hello 和 World 比较,而不是 Hello 和 World 自己内部比较去重。
flatMap
是把 Hello 和 World 分成一个字符串,即一个字符串就是一个流,然后去重,是可以把 l 去掉,而 map
是不同下标元素比较。
使用 flatMap
方法的效果是,各个数组并不是分别映射一个流,而是映射成流的内容,所有使用 map(Array::stream)
时生成的单个流被合并起来,即扁平化为一个流。
两个方法效果如图:
举例说明
有二箱鸡蛋,每箱 5 个,现在要把鸡蛋加工成煎蛋,然后分给学生。
map 做的事情:把二箱鸡蛋分别加工成煎蛋,还是放成原来的两箱,分给 2 组学生;
flatMap 做的事情:把二箱鸡蛋分别加工成煎蛋,然后放到一起【10 个煎蛋】,分给 10 个学生。
总结
数组去重复时,如果去重复的是不同下标的整体元素,则使用 map
,如果去重复的是每个元素自己的内容,则使用 flatMap
。
排序
| 方法 | 描述 | | ----------------------- | ---------------------- | | sorted() | 产生一个新流,其中按自然顺序排序(从小到大) | | sorted(Comparator comp) | 产生一个新流,其中按比较器顺序排序 |
代码示例:
``` public class TestStreamAPI1 { List emps = Arrays.asList( new Employee(102, "李四", 59, 6666.66), new Employee(101, "张三", 18, 9999.99), new Employee(103, "王五", 28, 3333.33), new Employee(104, "赵六", 8, 7777.77), new Employee(104, "赵六", 8, 7777.77), new Employee(104, "赵六", 8, 7777.77), new Employee(105, "田七", 38, 5555.55) ); @Test public void test2(){ emps.stream() .map(Employee::getName) .sorted() .forEach(System.out::println);
System.out.println("------------------------------------");emps.stream() .sorted((x, y) -> { if(x.getAge() == y.getAge()){ return x.getName().compareTo(y.getName()); }else{ return Integer.compare(x.getAge(), y.getAge()); } }).forEach(System.out::println);
} } ```
增量
| 方法 | 描述 | | ------------------------------------------------- | ------------------------------------------------------------------------- | | range(int startInclusive, int endExclusive) | 以 1 的增量步从 startInclusive 到 endExclusive 返回顺序的有序 IntStream,不包括endExclusive | | rangeClosed(int startInclusive, int endExclusive) | 以 1 的增量步从 startInclusive 到 endExclusive 返回顺序的有序 IntStream,包括endExclusive |
代码示例:
public class TestStreamAPI1 { @Test public void test3(){ IntStream.range(0, 10).forEach( i -> System.out.println(i)); } @Test public void test4(){ IntStream.rangeClosed(0, 10).forEach( i -> System.out.println(i)); } }
test3 方法会输出 0 到 9,而 test4 方法会输出 0 到 10。
Stream终止操作
终端操作会从流的流水线生成结果。其结果可以是任何不是流的值,例如:List、Integer,甚至是 void。
查找与匹配
| 方法 | 描述 | | ---------------------- | -------------------------------------------------------------------- | | allMatch(Predicate p) | 检查是否有匹配所有条件的元素 | | anyMatch(Predicate p) | 检查是否有至少匹配一个条件的元素 | | noneMatch(Predicate p) | 检查是否没有匹配所有条件的元素 | | findFirst() | 返回第一个元素 | | findAny() | 返回当前流中的任意元素 | | count() | 返回流中元素总数 | | max(Comparator c) | 返回流中最大值 | | min(Comparator c) | 返回流中最小值 | | forEach(Consumer c) | 内部迭代(使用 Collection 接口需要用户去做迭代,称为外部迭代。相反,Stream API 使用内部迭代,即它帮你把迭代做了) |
代码示例:
``` public class TestStreamAPI2 { // BUSY:忙碌,FREE:空闲,VOCATION:工作 List emps = Arrays.asList( new Employee(102, "李四", 59, 6666.66, Status.BUSY), new Employee(101, "张三", 18, 9999.99, Status.FREE), new Employee(103, "王五", 28, 3333.33, Status.VOCATION), new Employee(104, "赵六", 8, 7777.77, Status.BUSY), new Employee(104, "赵六", 8, 7777.77, Status.FREE), new Employee(104, "赵六", 8, 7777.77, Status.FREE), new Employee(105, "田七", 38, 5555.55, Status.BUSY) ); // 测试检查 match @Test public void test1(){ boolean bl = emps.stream() .allMatch((e) -> e.getStatus().equals(Status.BUSY)); System.out.println(bl); boolean bl1 = emps.stream() .anyMatch((e) -> e.getStatus().equals(Status.BUSY)); System.out.println(bl1); boolean bl2 = emps.stream() .noneMatch((e) -> e.getStatus().equals(Status.BUSY)); System.out.println(bl2); } // 测试 findFirst() 和 findAny() @Test public void test2(){ Optional op = emps.stream() .sorted((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary())) .findFirst();
System.out.println(op.get());System.out.println("--------------------------------");Optional op2 = emps.parallelStream() .filter((e) -> e.getStatus().equals(Status.FREE)) .findAny();System.out.println(op2.get());
} // 测试 max,min @Test public void test3(){ long count = emps.stream() .filter((e) -> e.getStatus().equals(Status.FREE)) .count();
System.out.println(count);Optional op = emps.stream() .map(Employee::getSalary) .max(Double::compare);System.out.println(op.get());Optional op2 = emps.stream() .min((e1, e2) -> Double.compare(e1.getSalary(), e2.getSalary()));System.out.println(op2.get());
} } ```
注意:流进行了终止操作后,则该流不能再次使用。
如下的 max 返回无效,因为 count 已经终止操作。
public void test4(){ Stream
归约
| 方法 | 描述 | | -------------------------------- | ------------------------------------- | | reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T,初始值是 iden | | reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。 返回 Optional
|
注:map 和 reduce 的连接通常称为 map-reduce 模式,因 Google 用它来进行网络搜索而出名。
代码示例:
``` public class TestStreamAPI3 { List emps = Arrays.asList( new Employee(102, "李四", 79, 6666.66, Status.BUSY), new Employee(101, "张三", 18, 9999.99, Status.FREE), new Employee(103, "王五", 28, 3333.33, Status.VOCATION), new Employee(104, "赵六", 8, 7777.77, Status.BUSY), new Employee(104, "赵六", 8, 7777.77, Status.FREE), new Employee(104, "赵六", 8, 7777.77, Status.FREE), new Employee(105, "田七", 38, 5555.55, Status.BUSY) ); @Test public void test1(){ List list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer sum = list.stream() .reduce(0, (x, y) -> x + y); // 0 是初始值
System.out.println(sum);
System.out.println("----------------------------------------");Optional op = emps.stream() .map(Employee::getSalary) .reduce(Double::sum);
System.out.println(op.get()); } } ```
收集
| 方法 | 描述 | | -------------------- | --------------------------------------------------- | | collect(Collector c) | 将流转换为其他形式。接收一个 Collector 接口的实现,用于给 Stream 中元素做汇总的方法 |
Collector 接口中方法的实现决定了如何对流执行收集操作(如收集到 List、Set、Map)。但是 Collectors 实用类提供了很多静态方法,可以方便地创建常见收集器实例,具体方法与实例如下表:
| 方法 | 返回类型 | 作用 | | ----------------- | --------------------- | --------------------------------------------------- | | toList | List | 把流中元素收集到 List | | toSet | Set | 把流中元素收集到 Set | | toCollection | Collection | 把流中元素收集到创建的集合 | | counting | Long | 计算流中元素的个数 | | summingInt | Integer | 对流中元素的整数属性求和 | | averagingInt | Double | 计算流中元素 Integer 属性的平均值 | | summarizingInt | IntSummaryStatistics | 收集流中 Integer 属性的统计值。如:平均值 | | joining | String | 连接流中每个字符串 | | maxBy | Optional | 根据比较器选择最大值 | | minBy | Optional | 根据比较器选择最小值 | | reducing | 归约产生的类型 | 从一个作为累加器的初始值开始,利用 BinaryOperator 与流中元素逐个结合,从而归约成单个值 | | collectingAndThen | 转换函数返回的类型 | 包裹另一个收集器,对其结果转换函数 | | groupingBy | Map > | 根据某属性值对流分组,属性为 K,结果为 T | | partitioningBy | Map > | 根据 true 或 false 进行分区 |
代码示例:
public class TestStreamAPI3 { List
并行流与串行流
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。这样一来,你就可以自动把给定操作的工作负荷分配给多核处理器的所有内核,让他们都忙起来。整个过程无需程序员显示实现优化。
Java 8 中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API 可以声明性地通过 parallel()
与 sequential()
在并行流与顺序流之间进行切换。
了解Fork/Join框架
Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。
Fork/Join框架与传统线程池的区别
采用 工作窃取模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线程队列中,然后再从另外一个随机线程的队列中偷一个并把它放在自己的队列中。即自己空闲时,就去偷取别线程的任务来执行。
相对于一般的线程池实现,Fork/Join 框架的优势体现在对其中包含的任务的处理方式上。在一般的线程池中,如果一个线程正在执行的任务由于某些原因无法继续运行,那么该线程会处于等待状态。而在 Fork/Join 框架实现中,如果某个子问题由于等待另外一个子问题的完成而无法继续运行。那么处理该子问题的线程会主动寻找其他尚未运行的子问题来执行。这种方式减少了线程的等待时间,提高了性能。
代码示例
Java8 已经优化了 Fork/Join 框架,因为 Java7 之前使用该框架代码会比较繁琐。
如下 Java7 前的 Fork/Join 框架简易代码,计算累加值:
public class ForkJoinCalculate extends RecursiveTask
计算出累加 10 亿的花费时间:
public class TestForkJoin { // Fork/Join 框架花费时间 @Test public void test1(){ long start = System.currentTimeMillis(); ForkJoinPool pool = new ForkJoinPool(); ForkJoinTask
而 Java8 之后,Fork/Join 框架的代码优化更加简洁,通过 parallel()
与 sequential()
在并行流与顺序流之间进行切换。如下:
public class TestForkJoin { @Test public void test3(){ long start = System.currentTimeMillis(); Long sum = LongStream.rangeClosed(0L, 10000000000L) .parallel() // 并行流 .sum(); System.out.println(sum); long end = System.currentTimeMillis(); System.out.println("耗费的时间为: " + (end - start)); // 花费时间:18926 } }
Optional API
Optional 类(java.util.Optional)是一个容器类,代表一个值存在或不存在,原来用 null 表示一个值不存在,现在 Optional 可以更好的表达这个概念。并且可以避免空指针异常。
常用方法:
| 方法 | 作用 | | ------------------------ | ----------------------------------------------- | | Optional.of(T t) | 创建一个 Optional 实例 | | Optional.empty() | 创建一个空的 Optional 实例 | | Optional.ofNullable(T t) | 若 t 不为 null,创建 Optional 实例,否则创建空实例 | | isPresent() | 判断是否包含值 | | orElse(T t) | 如果调用对象包含值,返回该值,否则返回 t | | orElseGet(Supplier s) | 如果调用对象包含值,返回该值,否则返回 s 获取的值 | | map(Function f) | 如果有值对其处理,并返回处理后的 Optional,否则返回 Optional.empty() | | flatMap(Function mapper) | 与 map 类似,要求返回值必须是 Optional |
代码示例
public class TestOptional { @Test public void test1(){ Optional
模拟实际场景
每个男生心里都可能有一个女神,也可能没有,可以利用 Optional 来解决空指针异常。
首先创建没有利用 Optional,而是 if 判断 null 的男生实体类:
public class Man { private Godness god; public Man() { } public Man(Godness god) { this.god = god; } public Godness getGod() { return god; } public void setGod(Godness god) { this.god = god; } @Override public String toString() { return "Man [god=" + god + "]"; } }
女神实体类(有名字):
public class Godness { private String name; public Godness() { } public Godness(String name) { this.name = name; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Godness [name=" + name + "]"; } }
模拟获取男生心里的女神名字:
public class TestOptional { @Test public void test5(){ Man man = new Man(); String name = getGodnessName(man); System.out.println(name); } // 需求:获取一个男人心中女神的名字 public String getGodnessName(Man man){ if(man != null){ Godness g = man.getGod(); if(g != null){ return g.getName(); } } return "冰糖"; // 默认值 } }
上面代码是我们习惯使用的多层 if 来解决 null 问题,那么如何利用 Optional 解决呢?
创建 Optional 的男生实体类:
``` // 注意:Optional 不能被序列化 public class NewMan { private Optional godness = Optional.empty(); // 必须给个值,否则默认为 null
private Godness god;
public Optional getGod(){ return Optional.of(god); } public NewMan() { } public NewMan(Optional godness) { this.godness = godness; } public Optional getGodness() { return godness; } public void setGodness(Optional godness) { this.godness = godness; } @Override public String toString() { return "NewMan [godness=" + godness + "]"; } } ```
模拟获取男生心里的女神名字:
public class TestOptional { // 运用 Optional 的实体类 @Test public void test6(){ Optional
接口中的默认方法与静态方法
接口中的默认方法
Java 8 中允许接口中包含具有具体实现的方法,该方法称为「默认方法」,默认方法使用 default 关键字修饰。
例如:
public interface MyFun { default String getName(){ return "哈哈哈"; } }
如果有一个类也定义了相同的方法,如:
public class MyClass { public String getName(){ return "嘿嘿嘿"; } }
再来一个类同时继承和实现 MyClass
和 MyFun
,该使用谁呢?使用类 MyClass
。
public class SubClass extends MyClass implements MyFun { } class Test { public static void main(String[] args) { SubClass sc = new SubClass(); System.out.println(sc.getName()); // 输出:嘿嘿嘿 } }
或者再多出一个有相同方法的接口
``` public interface MyInterface {
default String getName(){ return "呵呵呵"; } } ```
SubClass
类同时实现两个接口,那么该使用哪个的方法呢?需要重写方法来决定使用哪个接口的方法
public class SubClass implements MyFun, MyInterface { @Override public String getName() { return MyInterface.super.getName(); } }
接口默认方法的「类优先」原则
若一个接口中定义了一个默认方法,而另外一个父类或接口中 又定义了一个同名的方法时
- 选择父类中的方法。如果一个父类提供了具体的实现,那么接口中具有相同名称和参数的默认方法会被忽略
- 接口冲突。如果一个父接口提供一个默认方法,而另一个接口也提供了一个具有相同名称和参数列表的方法(不管方法是否是默认方法),那么必须覆盖该方法来解决冲突
接口中的静态方法
Java8 中,接口中允许添加静态方法。
例如:
``` public interface MyInterface {
public static void show(){ System.out.println("接口中的静态方法"); } } ```
通过 MyInterface.show
调用即可。
新时间日期API
新时间日期API出现原因
为什么 Java 8 要出现新的时间日期 API,以前的 API 有什么问题吗?
当然有,那就是线程不安全问题,当多个线程同时调用一个日期 API 时,会报错。
举个例子:
public class TestSimpleDateFormat { public static void main(String[] args) throws Exception { SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); Callable
上面代码创建 10 个线程,循环 10 次获取某个时间点,最终瞬间执行 100 次获取时间的操作,导致报错,因为传统时间 API 有线程不安全问题,那么 Java 8 以前如何解决呢?
利用 ThreadLocal
来加锁即可,如下:
public class TestSimpleDateFormat { public static void main(String[] args) throws Exception { // 解决多线程安全问题 Callable
Java 8 之后如何解决这个问题呢?
利用三大核心类的 LocalDate
可以更加简洁的替换 ThreadLocal
来解决日期线程不安全问题。如下:
``` public class TestSimpleDateFormat {
public static void main(String[] args) throws Exception {
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyyMMdd");Callable task = new Callable() {
@Override public LocalDate call() throws Exception { LocalDate ld = LocalDate.parse("20161121", dtf); return ld; }
};
ExecutorService pool = Executors.newFixedThreadPool(10);
List> results = new ArrayList<>();for (int i = 0; i < 10; i++) { results.add(pool.submit(task));}for (Future future : results) { System.out.println(future.get());}pool.shutdown();
} } ```
三大核心类
三大类 LocalDate
、LocalTime
、LocalDateTime
类的实例是 不可变的对象(线程安全),分别表示使用 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 | 判断是否是闰年 |
now()
示例:
LocalDate localDate = LocalDate.now(); LocalDate localDate = LocalTime.now(); LocalDateTime localDateTime= LocalDateTime.now();
of()
示例:
LocalDate localDate = LocalDate.of(2016, 10, 26); LocalTime localTime = LocalTime.of(02, 22, 56); LocalDateTime localDateTime = LocalDateTime.of(2016, 10, 26, 12, 10, 55);
演示三大类的常用 API
public class TestLocalDateTime { // LocalDateTime:相当于 LocalDate + LocalTime @Test public void test1() { LocalDateTime ldt = LocalDateTime.now(); System.out.println(ldt); LocalDateTime ld2 = LocalDateTime.of(2016, 11, 21, 10, 10, 10); // 直接指定年月日,时分秒 System.out.println(ld2); LocalDateTime ldt3 = ld2.plusYears(20); // 加 20 年 System.out.println(ldt3); LocalDateTime ldt4 = ld2.minusMonths(2); // 减 2 个月 System.out.println(ldt4); System.out.println(ldt.getYear()); // 获取年 System.out.println(ldt.getMonthValue()); // 获取月 System.out.println(ldt.getDayOfMonth()); // 获取日 System.out.println(ldt.getHour()); // 获取时 System.out.println(ldt.getMinute()); // 获取分 System.out.println(ldt.getSecond()); // 获取秒 } }
Instant时间戳
用于「时间戳」的运算。它是以 Unix 元年(传统的设定为 UTC 时区 1970 年 1 月 1 日午夜时分)开始所经历的描述进行运算。
代码示例:
public class TestLocalDateTime { // Instant : 时间戳。(使用 Unix 元年 1970 年 1 月 1 日 00:00:00 所经历的毫秒值) @Test public void test2() { Instant ins = Instant.now(); // 默认使用 UTC 时区,所以比中国时间少 8 小时 System.out.println(ins); OffsetDateTime odt = ins.atOffset(ZoneOffset.ofHours(8)); // 加 8 小时:中国时区 System.out.println(odt); System.out.println(ins.getNano()); // 获取纳秒 Instant ins2 = Instant.ofEpochSecond(5); // 获取秒 System.out.println(ins2); } }
Duration和Period
Duration
:用于计算两个「时间」间隔Period
:用于计算两个间隔
public class TestLocalDateTime { // Duration : 用于计算两个「时间」间隔 // Period : 用于计算两个「日期」间隔 @Test public void test3(){ Instant ins1 = Instant.now(); try { Thread.sleep(1000); } catch (InterruptedException e) { } Instant ins2 = Instant.now(); System.out.println("所耗费时间为:" + Duration.between(ins1, ins2)); // 计算时间 System.out.println("----------------------------------"); LocalDate ld1 = LocalDate.now(); LocalDate ld2 = LocalDate.of(2011, 1, 1); Period pe = Period.between(ld2, ld1); // 计算日期 System.out.println(pe.getYears()); // 获取年 System.out.println(pe.getMonths()); // 获取月 System.out.println(pe.getDays()); // 获取日 } }
日期的操纵
TemporalAdjuster
: 时间校正器。有时我们可能需要获取例如:将日期调整到「下个周日」等操作TemporalAdjusters
: 该类通过静态方法提供了大量的常用TemporalAdjuster
的实现
代码示例:
public class TestLocalDateTime { // TemporalAdjuster : 时间校正器 @Test public void test4(){ LocalDateTime ldt = LocalDateTime.now(); System.out.println(ldt); LocalDateTime ldt2 = ldt.withDayOfMonth(10); // 直接修改月份为 10 System.out.println(ldt2); LocalDateTime ldt3 = ldt.with(TemporalAdjusters.next(DayOfWeek.SUNDAY)); // 下周日 System.out.println(ldt3); // 自定义:下一个工作日 LocalDateTime ldt5 = ldt.with((l) -> { LocalDateTime ldt4 = (LocalDateTime) l; DayOfWeek dow = ldt4.getDayOfWeek(); // 获取日对象 if(dow.equals(DayOfWeek.FRIDAY)){ // 如果是周五 return ldt4.plusDays(3); // 加 3 天就是周一 }else if(dow.equals(DayOfWeek.SATURDAY)){ // 如果是周六 return ldt4.plusDays(2); // 加 2 天就是周一 }else{ // 如果是周日 return ldt4.plusDays(1); // 加 1 天就是周一 } }); System.out.println(ldt5); } }
解析与格式化
java.time.format.DateTimeFormatter
类:该类提供了三种格式化方法:
- 预定义的标准格式
- 语言环境相关的格式
- 自定义的格式
public class TestLocalDateTime { // DateTimeFormatter : 解析和格式化日期或时间 @Test public void test5(){ // DateTimeFormatter dtf = DateTimeFormatter.ISO_LOCAL_DATE; // 默认标准格式 DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy年MM月dd日 HH:mm:ss E"); // 自定义格式 LocalDateTime ldt = LocalDateTime.now(); String strDate = ldt.format(dtf); System.out.println(strDate); LocalDateTime newLdt = ldt.parse(strDate, dtf); // 转换为 LocalDateTime 标准格式 System.out.println(newLdt); } }
时区的处理
Java8 中加入了对时区的支持,带时区的时间为分别为:ZonedDate
、ZonedTime
、ZonedDateTime
。
其中每个时区都对应着 ID,地区 ID 都为「{区域}/{城市}」的格式 例如:Asia/Shanghai 等
ZoneId
:该类中包含了所有的时区信息getAvailableZoneIds()
:可以获取所有时区信息of(id)
:用指定的时区信息获取ZoneId 对象
代码示例:
public class TestLocalDateTime { // ZonedDate、ZonedTime、ZonedDateTime :带时区的时间或日期 @Test public void test6(){ Set
与传统日期处理的转换
| 类 | 传统方法 | 转换方法 | | ------------------------------------------------------------- | -------------------------------------- | --------------------------- | | java.time.Instant
VS java.util.Date
| Date.from(instant) | date.toInstant() | | java.time.Instant
VS java.sql.Timestamp
| Timestamp.from(instant) | timestamp.toInstant() | | java.time.ZonedDateTime
VS java.util.GregorianCalendar
| GregorianCalendar.from(zonedDateTim e) | cal.toZonedDateTime() | | java.time.LocalDate
VS java.sql.Time
| Date.valueOf(localDate) | date.toLocalDate() | | java.time.LocalTime
VS java.sql.Time
| Date.valueOf(localDate) | date.toLocalTime() | | java.time.LocalDateTime
VS java.sql.Timestamp
| Timestamp.valueOf(localDateTime) | timestamp.toLocalDateTime() | | java.time.ZoneId
VS java.util.TimeZone
| Timezone.getTimeZone(id) | timeZone.toZoneId() | | java.time.format.DateTimeFormatter
VS java.text.DateFormat
| formatter.toFormat() | 无 |
Try升级
Java 8 之前,我们写的 try-catch 结尾都要写 finally 来关闭资源,则 Java 8 升级了 try-catch 的用法,直接在 try 加个括号,里面填写对象的实例化即可自动关闭资源,对比代码如下:
// Java 8 之前 public void testTry1(){ InputStreamReader reader = null; try{ reader = new InputStreamReader(System.in); //读取数据的过程:略 reader.read(); }catch (IOException e){ e.printStackTrace(); }finally{ //资源的关闭操作 if(reader != null){ try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } } } // Java 8 public void testTry2(){ try(InputStreamReader reader = new InputStreamReader(System.in)){ // 读取数据的过程:略 reader.read(); }catch(IOException e){ e.printStackTrace(); } }
注意:java 8 中要求资源对象的实例化,必须放在 try 的一对 () 内完成。
Java 9 又进行了升级,对象的实例化可以放在外面,try 的一对 () 传入对象即可,如下:
public void testTry3(){ InputStreamReader reader = new InputStreamReader(System.in); try(reader){ //读取数据的过程:略 reader.read(); }catch(IOException e){ e.printStackTrace(); } }
重复注解与类型注解
Java 8 对注解处理提供了两点改进:
- 可重复的注解,JDK 8 之前只允许一个类、方法体等上方只出现一个注解
- 可用于类型的注解
代码示例
创建一个数组注解,用于开启可重复的注解:
@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE}) @Retention(RetentionPolicy.RUNTIME) publie @interface MyAnnotations { MyAnnotation[] value(); }
创建可重复的注解类:
@Repeatabie(MyAnnotations.class) // 开启使用重复注解 @Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, TYPE_PARAMETER}) @Retention(RetentionPolicy.RUNTIME) public @interface MyAnnotation { String value(); }
测试使用重复注解:
public class TestAnnotations { @MyAnnotation("Hello") @MyAnnotation("World") public void show() { } }
可用于类型的注解,即在参数或者属性上可以加入注解,类似于 Spring Boot 的 @RequestParam()
。
public class TestAnnotations { @MyAnnotation("Hello") @MyAnnotation("World") public void show(@MyAnnotation("kele") String str) { } }
来源地址:https://blog.csdn.net/zhiqi_l163991102/article/details/131414769