Java8新特性二
一、并行流与顺序流
1.概念
并行流就是把一个内容分成多个数据块,并用不同的线程分别处理每个数据块的流。
java8中将并行进行了优化,我们可以很容易的对数据进行并行操作。Stream API可以声明性地通过 paralle()
与 sequential()
在并行流与顺序流之间进行切换,并行流的底层其实就是Fork/Join框架的一个实现
2.Fork/Join框架
Fork/Join框架:就是在必要的情況下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行Join汇总
ForkJoin框架采用的是“工作窃取模式”,传统线程在处理任务时,假设有一个大任务被分解成了20个小任务,并由四个线程A,B,C,D处理,理论上来讲一个线程处理5个任务,每个线程的任务都放在一个队列中,当B,C,D的任务都处理完了,而A因为某些原因阻塞在了第二个小任务上,那么B,C,D都需要等待A处理完成,此时A处理完第二个任务后还有三个任务需要处理,可想而知,这样CPU的利用率很低。而ForkJoin采取的模式是,当B,C,D都处理完了,而A还阻塞在第二个任务时,B会从A的任务队列的末尾偷取一个任务过来自己处理,C和D也会从A的任务队列的末尾偷一个任务,这样就相当于B,C,D额外帮A分担了一些任务,提高了CPU的利用率
3. Fork/Join框架代码示例:
首先要编写一个ForkJoin的计算类继承RecursiveTask 并重写 T compute() 方法
/**
* forkjoin框架使用示例: 利用ForkJoin框架求一个区间段的和
*/
public class ForkJoinTest extends RecursiveTask<Long> {
private static final long serialVersionUID = -4665674866263069371L;
//计算的起始值
private Long start;
//计算的终止值
private Long end;
//做任务拆分时的临界值
private static final Long THRESHOLD = 100L;
public ForkJoinTest(Long start, Long end) {
this.start = start;
this.end = end;
}
/**
* 计算代码,当计算区间的长度大于临界值时,继续拆分,当小于临界值时,进行计算
*
* @return
*/
@Override
protected Long compute() {
Long length = this.end - this.start;
if (length > THRESHOLD) {
long middle = (start + end) / 2;
ForkJoinTest left = new ForkJoinTest(start, middle);
left.fork();//拆分子任务并加入到线程队列
ForkJoinTest right = new ForkJoinTest(middle + 1, this.end);
right.fork();
return left.join() + right.join();
} else {
long sum = 0;
for (long i = start; i <= end; i++) {
sum += i;
}
return sum;
}
}
}
测试类:
import org.junit.Test;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.stream.LongStream;
public class TestForkJoin {
@Test
public void test1(){
long start = System.currentTimeMillis();
//1.ForkJoin框架也需要一个ForkJoin池来启动
ForkJoinPool pool = new ForkJoinPool();
//2.创建一个ForkJoinTask,RecursiveTask也是继承自ForkJoinTask,所以我们new自己写的那个计算类
ForkJoinTask<Long> task = new ForkJoinTest(0L, 1000000L);
//3.执行计算
long sum = pool.invoke(task);
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗费的时间为: " + (end - start)); //5463
}
/**
* 测试用for循环计算
*/
@Test
public void test2(){
long start = System.currentTimeMillis();
long sum = 0L;
for (long i = 0L; i <= 1000000L; i++) {
sum += i;
}
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗费的时间为: " + (end - start)); //7610
}
/**
* 测试用并行流计算
*/
@Test
public void test3(){
long start = System.currentTimeMillis();
Long sum = LongStream.rangeClosed(0L, 1000000L).parallel().sum();
System.out.println(sum);
long end = System.currentTimeMillis();
System.out.println("耗费的时间为: " + (end - start)); //2813
}
}
这里我就不粘贴打印结果了,因为我上面拆分的数据太小,体现不出并行流的优势,forkjoin拆分合并任务也是需要时间的,对于计算量比较小的任务,拆分合并所花费的时间可能会大于计算时间,这时候用forkjoin拆分任务就会有点得不偿失了,如果你的电脑配置好,你可以测试更大的数据,运行结果更明显。
二、Optional类
1. 什么是Optional对象
Java 8中所谓的Optional对象,即一个容器对象,该对象可以包含一个null或非null值。如果该值不为null,则调用isPresent()方法将返回true,且调用get()方法会返回该值
2. Optional类常用方法
1.of(T value)方法
就是返回一个包含非空值的Optional对象
源码:
public static <T> Optional<T> of(T value) {
return new Optional<>(value);
}
注意:如果我们给of(null)
传参数为null,依然会抛出空指针异常,但是我们不用像以前一样,一步步的Debug去慢慢找抛出异常的代码;现在我们可以直接定位:就是Optional这一行of()参数为null造成的
2.ofNullable(T value)方法
返回一个可以包含空值的Optional对象
源码:
public static <T> Optional<T> ofNullable(T value) {
return value == null ? empty() : of(value);
}
3.ifPresent(Consumer<? super T> consumer)方法
当值不为null时,执行consumer
源码:
public void ifPresent(Consumer<? super T> consumer) {
if (value != null)
consumer.accept(value);
}
4.empty()方法
返回一个Optional实例,里面存放的value是null,源码如下:
public static<T> Optional<T> empty() {
@SuppressWarnings("unchecked")
Optional<T> t = (Optional<T>) EMPTY;
return t;
}
5.orElse(T other)方法
当值为null时返回传入的值,否则返回原值;
源码:
public T orElse(T other) {
return value != null ? value : other;
}
6.orElseGet(Supplier<? extends T> other)方法
功能与orElse(T other)类似,不过该方法可选值的获取不是通过参数直接获取,而是通过调用传入的Lambda表达式获取
源码:
public T orElseGet(Supplier<? extends T> other) {
return value != null ? value : other.get();
}
7.orElseThrow(Supplier<? extends X> exceptionSupplier)方法
当遇到值为null时,根据传入的Lambda表达式跑出指定异常
源码:
public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X {
if (value != null) {
return value;
} else {
throw exceptionSupplier.get();
}
}
8.filter(Predicate<? super T> predicate)方法
过滤符合条件的Optional对象,这里的条件用Lambda表达式来定义,
如果入参predicate对象为null将抛NullPointerException异常,
如果Optional对象的值为null,将直接返回该Optional对象,
如果Optional对象的值符合限定条件(Lambda表达式来定义),返回该值,否则返回空的Optional对象
源码如下:
public Optional<T> filter(Predicate<? super T> predicate) {
Objects.requireNonNull(predicate);
if (!isPresent())
return this;
else
return predicate.test(value) ? this : empty();
}
使用示例:
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//过滤值的长度小于3的Optional对象
Optional<String> less3 = test.filter((value) -> value.length() < 3);
//打印结果
System.out.println(less3.orElse("不符合条件,不打印值!"));
}
}
9.map(Function<? super T, ? extends U> mapper)方法
前面的filter方法主要用于过滤,一般不会修改Optional里面的值,map方法则一般用于修改该值,并返回修改后的Optional对象
如果入参mapper对象为null将抛NullPointerException异常,
如果Optional对象的值为null,将直接返回该Optional对象,
最后,执行传入的lambda表达式,并返回经lambda表达式操作后的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));
}
}
栗子:
package optional;
import java.util.Optional;
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//将值修改为大写
Optional<String> less3 = test.map((value) -> value.toUpperCase());
//打印结果 ABCD
System.out.println(less3.orElse("值为null,不打印!"));
}
}
10.flatMap(Function<? super T, Optional> mapper)方法
flatMap方法与map方法基本一致,唯一的区别是,
如果使用flatMap方法,需要自己在Lambda表达式里将返回值转换成Optional对象,
而使用map方法则不需要这个步骤,因为map方法的源码里已经调用了Optional.ofNullable方法;
源码:
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));
}
}
栗子:
public class Snippet
{
public static void main(String[] args)
{
Optional<String> test = Optional.ofNullable("abcD");
//使用flatMap,将值修改为大写
Optional<String> less3 = test.flatMap((value) -> Optional.ofNullable(value.toUpperCase()));
//使用map,将值修改为大写
//Optional<String> less3 = test.map((value) -> value.toUpperCase());
//打印结果 ABCD
System.out.println(less3.orElse("值为null,不打印!"));
}
}
最后举个栗子,我们结合实际来了解Optional
栗子: 每个男人心中都有自己的女神,苍老师也罢,波多老师也罢(可能这两位也是搞IT的大佬吧),也有男的没有女神,那我们想看一个男的的女神叫什么名字(可能会抛出空指针异常,这就是我们要解决的)
创建一个女神类:
//女神
public class Godness {
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 Man {
private Godness godness;
public Man(Godness godness) {
this.godness = godness;
}
public Man() {
}
public Godness getGodness() {
return godness;
}
public void setGodness(Godness godness) {
this.godness = godness;
}
}
测试:
//获取一个男人心中的女神
@Test
public void test1(){
Man man = new Man();//空指针异常,因为当前这个男的没女神
System.out.println(getGodname(man));
}
public String getGodname(Man man){
return man.getGodness().name;
}
运行上述代码抛出异常,因为我们创建了一个男人对象,但是这个男人没有女神(没有女神参数),所以在我们调用getGodname(Man man)中的man.getGodness().name,前面为null,会null.name空指针异常
所以我们要防止女神为null,我们可以把女神封装成Optional类型,所以改写男人类(Man):
public class newMan {
private Optional<Godness> godness = Optional.empty();
public newMan() {
}
public newMan(Optional<Godness> godness) {
this.godness = godness;
}
public Optional<Godness> getGodness() {
return godness;
}
public void setGodness(Optional<Godness> godness) {
this.godness = godness;
}
@Override
public String toString() {
return "newMan{" +
"godness=" + godness +
'}';
}
}
测试:
@Test
public void test2(){
Optional<Godness> o = Optional.ofNullable(new Godness("白豆腐"));
Optional<newMan> op = Optional.ofNullable(new newMan());
System.out.println(getGodnewName(op));
}
public String getGodnewName(Optional<newMan> newMan){
return newMan.orElse(new newMan())//如果传进来的男人为null,那我们就创建一个男的
.getGodness()//获取这个男人的女神
.orElse(new Godness("李诗诗"))//如果没有女神,为了防止null.name空指针异常,我们创建一个女神
.getName();//获取女神名字
}
注释在代码写的很清楚,所以我们可以看到Optional这个类很方便,解决了我们的空指针异常问题,即使出现空指针异常,我们也可以很快定位到代码
三、接口中的默认方法和静态方法
在Java8之前的版本中,接口中只能声明常量和抽象方法,接口的实现类中必须实现接口中所有的抽象方法。而在Java8中,接口中可以声明默认方法和静态方法
1.默认方法
Java 8中允许接口中包含具有具体实现的方法,该方法称为“默认方法”,默认方法使用 default 关键字修饰
例如,我们可以定义一个接口MyFun,其中,包含有一个默认方法getName,如下所示:
public interface MyFun {
default String getName(){
return "默认方法";
}
}
类优先”的原则
若一个接口中定义了一个默认方法,而另外一个父类或接口中又定义了一个同名的方法时,遵循如下的原则
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{
@Override
public String getName(){
return MyInterface.super.getName();
}
}
此时,MyClass类中的getName方法返回的是:interface
如果MyClass中的getName()方法覆盖的是MyFunction接口的getName()方法,如下所示:
public class MyClass{
@Override
public String getName(){
return MyFunction.super.getName();
}
}
此时,MyClass类中的getName方法返回的是:function
2.静态方法
在Java8中,接口中允许添加静态方法,使用方式接口名.方法名。例如MyFunction接口中定义了静态方法send()
public interface MyFunction{
default String getName(){
return "binghe";
}
static void send(){
System.out.println("Send Message...");
}
}
我们可以直接使用如下方式调用MyFunction接口的send静态方法
MyFunction.send();
3.新时间日期API
Java 8通过发布新的Date-Time API (JSR 310)来进一步加强对日期与时间的处理
在旧版的 Java 中,日期时间 API 存在诸多问题,其中有:
- 非线程安全 java.util.Date 是非线程安全的,所有的日期类都是可变的,这是Java日期类最大的问题之一
- 设计很差 Java的日期/时间类的定义并不一致,在java.util和java.sql的包中都有日期类,此外用于格式化和解析的类在java.text包中定义。java.util.Date同时包含日期和时间,而java.sql.Date仅包含日期,将其纳入java.sql包并不合理。另外这两个类都有相同的名字,这本身就是一个非常糟糕的设计
- 时区处理麻烦日期类并不提供国际化,没有时区支持,因此Java引入了java.util.Calendar和java.util.TimeZone类,但他们同样存在上述所有的问题
Java 8 在 java.time 包下提供了很多新的 API。以下为两个比较重要的 API:
Local(本地):
简化了日期时间的处理,没有时区的问题
Zoned(时区):
通过制定的时区处理日期时间
新的java.time包涵盖了所有处理日期,时间,日期/时间,时区,时刻(instants),过程(during)与时钟(clock)的操作
本地化日期时间 API
LocalDate/LocalTime 和 LocalDateTime 类可以在处理时区不是必须的情况。代码如下:
import java.time.LocalDate;
import java.time.LocalTime;
import java.time.LocalDateTime;
import java.time.Month;
public class Java8Tester {
public static void main(String args[]){
Java8Tester java8tester = new Java8Tester();
java8tester.testLocalDateTime();
}
public void testLocalDateTime(){
// 获取当前的日期时间
LocalDateTime currentTime = LocalDateTime.now();
System.out.println("当前时间: " + currentTime);
LocalDate date1 = currentTime.toLocalDate();
System.out.println("date1: " + date1);
Month month = currentTime.getMonth();
int day = currentTime.getDayOfMonth();
int seconds = currentTime.getSecond();
System.out.println("月: " + month +", 日: " + day +", 秒: " + seconds);
LocalDateTime date2 = currentTime.withDayOfMonth(10).withYear(2012);
System.out.println("date2: " + date2);
// 12 december 2014
LocalDate date3 = LocalDate.of(2014, Month.DECEMBER, 12);
System.out.println("date3: " + date3);
// 22 小时 15 分钟
LocalTime date4 = LocalTime.of(22, 15);
System.out.println("date4: " + date4);
// 解析字符串
LocalTime date5 = LocalTime.parse("20:15:30");
System.out.println("date5: " + date5);
}
}
结果:
当前时间: 2020-11-14T21:21:46.969
date1: 2020-11-14
月: NOVEMBER, 日: 14, 秒: 46
date2: 2012-11-10T21:21:46.969
date3: 2014-12-12
date4: 22:15
date5: 20:15:30
使用时区的日期时间API
如果我们需要考虑到时区,就可以使用时区的日期时间API:
import java.time.ZonedDateTime;
import java.time.ZoneId;
public class Java8Tester {
public static void main(String args[]){
Java8Tester java8tester = new Java8Tester();
java8tester.testZonedDateTime();
}
public void testZonedDateTime(){
// 获取当前时间日期
ZonedDateTime date1 = ZonedDateTime.parse("2015-12-03T10:15:30+05:30[Asia/Shanghai]");
System.out.println("date1: " + date1);
ZoneId id = ZoneId.of("Europe/Paris");
System.out.println("ZoneId: " + id);
ZoneId currentZone = ZoneId.systemDefault();
System.out.println("当期时区: " + currentZone);
}
}
结果:
$ javac Java8Tester.java
$ java Java8Tester
date1: 2015-12-03T10:15:30+08:00[Asia/Shanghai]
ZoneId: Europe/Paris
当期时区: Asia/Shanghai