共计 24857 个字符,预计需要花费 63 分钟才能阅读完成。
1、Lambda 表达式
1.1、概述
首先,要想明白 Lambda 表达式就要先明白 函数式接口,所以,咱们先来了解一下什么是函数式接口吧!
所谓函数式接口就是 有且仅有一个抽象方法的接口
函数式接口就是适用于函数式编程场景的接口,java 中的函数式编程的体现就是 lambda!所以函数式接口 就是可以适用于 Lambda 使用的接口。
只有当接口中有且只有一个抽象方法的时候,Java 中的 lambda 表达式才能顺利推导!
也就是说在Java 中使用 Lambda 表达式必须符合函数式接口的规范
所以,使用 Lambda 接口的前提 是:
(1)Lambda 关联的接收对象必须是函数式接口。(也就是说方法的形参必须是接口)
(2)这个接口只能有一个抽象方法(函数式接口的规范)
1.2、函数式接口
/*
* 函数式接口:有且只有一个抽象方法!* @FunctionalInterface:用来检测该接口中是否是只有一个抽象方法,如果不止一个就报错!* */
@FunctionalInterface
public interface FunctionInter {public void method();
}
1.3、Lambda 表达式和匿名内部类
Lambda 表达式“本质上”是一个匿名内部类,只是二者体现形式不一样,但可以把 Lambda 表达式当做匿名内部类来理解!
1.3.1、匿名内部类
匿名内部类就是某个实现了接口的子类对象
不用匿名内部类
// 未用匿名内部类
public class MyComparator implements Comparator<Integer> {@Override
public int compare(Integer o1, Integer o2) {return o1-o2;
}
}
public static void main(String[] args) {Comparator c = new MyComparator();// 实现比较器接口,创建对象
TreeSet<Integer> set = new TreeSet<Integer>(c);
}
使用匿名内部类
public static void main(String[] args) {Comparator c = new MyComparator();// 实现比较器接口,创建对象
TreeSet<Integer> set = new TreeSet<Integer>(c);
// 使用匿名内部类
Comparator c2 = new Comparator<Integer>() {@Override
public int compare(Integer o1, Integer o2) {return o1-o2;
}
};// 匿名内部类实现比较器接口
TreeSet<Integer> set2 = new TreeSet<Integer>(c2);
}
1.3.2、Lambda 表达式
// 超简洁
TreeSet<Integer> set3 = new TreeSet<Integer>((o1,o2) ->o1-o2);
从上面的代码对比中,大家可以发现,lambda 表达式真的 超简洁!
1.4、Lambda 表达式详解
1.4.1、Lambda 表达式的标准写法
// 可以没有形参,如果有多个形参,那么用“,”隔开
// 方法体和形参之间用“->”连接
(参数类型 形参 1,参数类型 形参 2)->{方法体;}
代码演示一(无参无返回值)
public interface FlyAble {public void fly();
}
public class Test {public static void main(String[] args) {// 匿名内部类的写法
rocket(new FlyAble() {@Override
public void fly() {System.out.println("i can fly!-- 匿名内部类");
}
});
//lambda 表达式的写法
rocket(()->{System.out.println("i can fly!--Lambda 表达式");
});
}
public static void rocket(FlyAble f){f.fly();
}
}
lambda 表达式的本质就是重写接口中的方法
代码演示一(有参有返回值)
public interface FlyAble {public int fly(String demo2);
}
public static void main(String[] args) {// 匿名内部类的写法
rocket(new FlyAble() {@Override
public int fly(String name) {System.out.println("i can fly!-- 匿名内部类");
return 30;// 分行高度
}
},"小鸟");
//lambda 表达式的写法
rocket((String name)->{System.out.println("i can fly!--Lambda 表达式");
return 10000;
},"飞机");
}
public static void rocket(FlyAble f,String name){int height = f.fly(name);
System.out.println(name+"的分行高度是:"+height);
}
1.4.2、Lambda 表达式的省略写法
(1)小括号内参数的类型可以省略
(2)如果小括号内有且仅有一个参数,则小括号可以省略
(3)如果大括号内有且仅有一个语句,可以 同时 省略大括号、return 关键字及语句分号
例如:
public interface FlyAble {public void fly(String name);
}
public class Test {public static void main(String[] args) {//lambda 表达式的写法
rocket(name->System.out.println(name+"can fly!--Lambda 表达式"));
}
public static void rocket(FlyAble f){f.fly("bird");
}
}
2、JDK8 接口的方法增强
2.1、概述
JDK1.8 之前,接口中允许出现的成员有静态常量、抽象方法
//JDK1.8 以前
public interface InterA {// 静态常量
// 抽象方法
}
JDK1.8 之前只允许接口中出现抽象方法,但是在实际的使用过程中,发现这样会影响接口的扩展性。例如:当往一个接口中添加新的抽象方法时,原来实现该接口的类都会报错! 这样就显得“牵一发而动全身”!
为了解决这一弊端,JDK 在 1.8 版本中,对接口的功能进行了扩展!
JDK1.8 之后,接口中允许出现的成员有静态常量、抽象方法、默认方法 、 静态方法
//JDK1.8 以后
public interface InterB {// 静态常量
// 抽象方法
// 默认方法
// 静态方法
}
2.2、JDK1.8 接口新增方法种类
- 默认方法
- 静态方法
2.3、默认方法的定义和使用
2.3.1、默认方法的定义
默认方法定义在 接口中
他是个 有方法体 的方法
他定义的关键字是default
default 出现的 位置 在方法 返回值类型前面
定义格式如下:
interface InterA {public void methodA();
// 默认方法
public default void methodDef(){// 功能代码
}
}
2.3.2、默认方法的使用
(1)直接用
(2)重写
interface InterA {public void methodA();
public default void methodDef(){System.out.println("default ... method");
}
}
// 直接用
class A implements InterA{@Override
public void methodA() {}}
// 重写
class B implements InterA{@Override
public void methodA() { }
@Override
public void methodDef() {System.out.println("B ...default ... method");
}
}
class Test{public static void main(String[] args) {InterA a = new A();
a.methodDef();// 直接用
InterA b = new B();
b.methodDef();// 重写
}
}
运行结果:
2.4、静态方法的定义和使用
2.4.1、静态方法的定义
静态方法定义在 接口中
他是个 有方法体 的静态方法
定义格式如下:
interface InterD {// 定义静态方法
public static void methodSta(){// 方法体
}
public void methodA();
public default void methodDef(){System.out.println("default ... method");
}
}
从上面可以看出,接口中静态方法的定义和普通类中的静态方法的定义没啥区别!
2.4.2、静态方法的使用
注意:接口中的静态方法 只能通过接口名调用
2.5、接口中静态方法和默认方法的区别
1、默认方法通过实例调用,静态方法通过接口名调用
2、默认方法可以被继承,可以被重写
3、静态方法不能被继承,不能被重写,只能使用接口名调用静态方法
3、JDK 提供的常用内置函数式接口
3.1、为什么 JDK 要提供这些常用内置函数接口?
因为 Lambda 表达式不关心接口名称,只关心接口的形参列表及返回值,所以为了方便我们使用 Lambda 表达式进行编程(函数式编程),JDK 就提供了大量形参列表不同,返回值不同的函数式接口!
3.2、常用的函数式接口如下
(1)Supplier 接口:供给型接口
(2)Consumer 接口:消费型接口
(3)Function 接口:转换型接口
(4)Predicate 接口:判断型接口
3.2.1、Supplier 接口:供给型接口
@FunctionalInterface
public interface Supplier<T> {/**
* Gets a result.
*
* @return a result
*/
T get();
}
Supplier
供给型接口:通过 get 方法得到一个返回值,该返回值类型,通过泛型规定,是一个无参有返回值的接口!
案例:使用 Lambda 表达式返回数组元素的最小值!
public static void main(String[] args) {printMin(()->{int[] arr = {1,2,3,4,5};
Arrays.sort(arr);
return arr[0];
});
}
public static void printMin(Supplier<Integer> min){Integer minVlaue = min.get();
System.out.println(minVlaue);
}
3.2.2、Consumer 接口:消费型接口
@FunctionalInterface
public interface Consumer<T> {void accept(T t);
}
案例:把一个字符串中的字母全部转换成小写
public static void main(String[] args) {toLower((String str)->{String lower = str.toLowerCase();
System.out.println(lower);
});
}
public static void toLower(Consumer<String> consumer){consumer.accept("HelloWorld");
}
案例:把一个字符串既转换成大小,又转换成小写
public static void main(String[] args) {toLowerAndToUpper((String str)->{System.out.println(str.toLowerCase());
},
(String str)->{System.out.println(str.toUpperCase());
});
}
public static void toLowerAndToUpper(Consumer<String> consumer1,Consumer<String> consumer2){consumer1.accept("HelloWorld");
consumer2.accept("HelloWorld");
}
另一种写法
public static void main(String[] args) {toLowerAndToUpper((String str)->{System.out.println(str.toLowerCase());
},
(String str)->{System.out.println(str.toUpperCase());
});
}
public static void toLowerAndToUpper(Consumer<String> consumer1,Consumer<String> consumer2){consumer1.andThan(consumer2).accept("HelloWorld");
}
3.2.3、Function 接口:转换型接口
@FunctionalInterface
public interface Function<T, R> {R apply(T t);
}
案例:把字符串转换成一个整数并返回
public static void main(String[] args) {parseToInt((String str)->{return Integer.parseInt(str);
});
}
public static void parseToInt(Function<String,Integer> fun){Integer apply = fun.apply("10");
System.out.println(apply);
}
案例:把两个字符串转换成整数,并对这两个整数求和
public static void main(String[] args) {sum((String str)->{return Integer.parseInt(str);
},(String str)->{return Integer.parseInt(str);
});
}
public static void sum(Function<String,Integer> fun1,Function<String,Integer> fun2){Integer num1 = fun1.apply("10");
Integer num2 = fun1.apply("10");
System.out.println(num1+num2);
}
案例:传递两个参数,第一个参数是个字符串,要求把这个字符串转成数字;第二个是要求把转换之后的数字乘以 5
public static void main(String[] args) {mul((String str)->{return Integer.parseInt(str);
},(Integer i)->{return i*5;
});
}
public static void mul(Function<String,Integer> fun1,Function<Integer,Integer> fun2){Integer num1 = fun1.apply("10");
Integer num2 = fun2.apply(num1);
System.out.println(num2);
}
另外的实现方式
public static void main(String[] args) {mul((String str)->{return Integer.parseInt(str);
},(Integer i)->{return i*5;
});
}
public static void mul(Function<String,Integer> fun1,Function<Integer,Integer> fun2){fun1.andThen(fun2).apply("10");
}
3.2.4、Predicate 接口:判断型接口
@FunctionalInterface
public interface Predicate<T> {boolean test(T t);
}
案例:判断字符串的长度是否大于 3
public static void main(String[] args) {is3Length((String str)->{return str.length()>3;
});
}
public static void is3Length(Predicate<String> pre){boolean fbb = pre.test("fbbb");
System.out.println("长度是否是 3 个长度:"+fbb);
}
案例:判断字符串中是否既包含 W, 也包含 H
public static void main(String[] args) {wANDh((String str)->{return str.contains("w") && str.contains("h");
});
}
public static void wANDh(Predicate<String> pre){boolean rst = pre.test("wwaaah");
System.out.println("是否既包含 W 也包含 H:"+rst);
}
另外一种写法
public static void main(String[] args) {wANDh((String str)->{return str.contains("w");
},(String str)->{return str.contains("h");
});
}
public static void wANDh(Predicate<String> pre1,Predicate<String> pre2){boolean rst1 = pre1.test("hahwahah");
boolean rst2 = pre2.test("hahwahah");
boolean rst=rst1&&rst2;
System.out.println("是否既包含 W 也包含 H:"+rst);
}
另外一种写法
public static void main(String[] args) {wANDh((String str)->{return str.contains("w");
},(String str)->{return str.contains("h");
});
}
public static void wANDh(Predicate<String> pre1,Predicate<String> pre2){boolean rst = pre1.and(pre2).test("aaahhaaawww");
System.out.println("是否既包含 W 也包含 H:"+rst);
}
案例:使用 Lambda 表达式判断一个字符串中包含 W 或者 包含 H
public static void main(String[] args) {wORh((String str)->{return str.contains("w");
},(String str)->{return str.contains("h");
});
}
public static void wORh(Predicate<String> pre1,Predicate<String> pre2){boolean rst = pre1.or(pre2).test("aaahhaaawww");
System.out.println("是否包含 W 或包含 H:"+rst);
}
案例:使用 Lambda 表达式判断一个字符串中是否不包含 W
public static void main(String[] args) {isNotContain((String str)->{return str.contains("a");
});
}
public static void isNotContain(Predicate<String> pre1){boolean rst = pre1.negate().test("hahwahah");
System.out.println("是否不包含:"+rst);
}
negate()的作用就是对后面的 test 方法的结果取反。
4、Lambda 表达式的方法引用
定义:把方法中的代码像变量值一样传递(int a=10;int b=a)
把方法传递给抽象方法
4.1、Lambda 表达式的方法引用的作用是什么?
先来看一下 Lambda 表达式中 代码冗余 的场景
public class DemoReferenceMethod {public static void main(String[] args) {int[] arr = {1,2,3};
printSum(arr);// 普通方法调用
//lambda 方法调用
printSumLambda((Integer[] arr2)->{int sum=0;
for (int i : arr2) {sum+=i;}
System.out.println(sum);
});
}
public static void printSum(int[] arr){int sum=0;
for (int i : arr) {sum+=i;}
System.out.println(sum);
}
public static void printSumLambda(Consumer<int[]> con){int[] arr = {1,2,3};
con.accept(arr);
}
}
从上面代码中中可以看出,printSum 的方法内容和 printSumLambda 方法的 lambda 表达式的形参内容是一样的,所以这里存在了代码冗余。
printSum 的方法内容 =printSumLambda 方法的 lambda 表达式
将上面的内容用 方法引用 改进
//lambda 方法调用
printSumLambda((Integer[] arr2)->{int sum=0;
for (int i : arr2) {sum+=i;}
System.out.println(sum);
});
// 用方法引用改进上面的 lambda 表达式
printSumLambda(DemoReferenceMethod::printSum)
4.2、方法引用的格式
符号表示:::
符号解释:双冒号为 方法引用运算符 ,而它所在的表达式被称为 方法引用。
应用场景:如果 Lambda 所有实现的方案,已经有其他方法存在 相同方案,那么则可以使用方法引用。
4.3、常见引用方式
对象:: 方法名
类名:: 静态方法名
类名:: 方法名
类名::new(调用的构造器)
数组类型::new(调用数组的构造器)
4.3.1、对象:: 方法名
public static void main(String[] args) {Date d = new Date();
// 不使用方法引用
printTime(()->{return d.getTime();});
// 使用方法引用
printTime(d::getTime);
}
public static void printTime(Supplier<Long> supp){Long aLong = supp.get();
System.out.println(aLong);
}
public void test(){Date d = new Date();
Supplier<Long> aLong= d::getTime;
Long l=aLong.get();
System.out.println(l);
}
方法引用的注意事项:
A: 被引用的方法,参数要和接口中抽象方法的参数一样
B: 当接口抽象方法有返回值时,被引用的方法也必须有返回值
4.3.2、类名:: 静态方法名
public void test(){Supplier<Long> aLong= ()->{return System.currentTimeMillis();};
System.out.println(aLong.get());
Supplier<Long> aLong2= System::currentTimeMillis;
System.out.println(aLong2.get());
}
4.3.3、类名:: 方法名
public void test(){Function<String,Integer> fun1 = (String str)->{return str.length();}
System.out.println(fun1.apply("hello"));
// 方法引用
Function<String,Integer> fun2 = String::length;
System.out.println(fun2.apply("helloabc"));
// 方法引用
BiFunction(String,Integer,String) fun3 = String:substring;
String sub=fun3.apply("hello",2);
System.out.println(sub);
}
4.3.4、类名::new(引用构造器)
class Person{public Person(){}
public Person(String name,int age){this.name=name;
this.age=age;
}
private String name;
private int age;
public String getName(){return name;
}
public void setName(String name){this.name=name;
}
public void setAge(int age){this.age=age;
}
public int getAge(){return age;
}
}
public void test(){Supplier<Person> sup = ()->{return new Person();}
Person p = sup.get();
// 方法引用
Supplier<Person> sup2 = Person::new;
Person p2 = sup2.get();
// 方法引用
BiFunction(String,Integer,Person) fun3 = Person:new;
Person p3=fun3.apply("zs",2);
}
4.3.5、数组类型::new
public void test{Function<String[],Integer> fun = (len)->{return new String[len];
}
String[] strs1 = fun.apply(10);
Function<String[],Integer> fun2 = String[]::new
String[] strs2 = fun2.apply(10);
}
4.4、方法引用总结
方法引用是对 Lambda 表达式符合特定情况下的一种缩写,它使得我们的 Lambda 表达式更加的精简,也可以理解为 Lambda 表达式的缩写形式,不过要注意的是 方法引用只能引用已经存在的方法!
5、Stream 流
5.1、Stream 流思想概述
Stream 流式思想类似于工厂车间的“生产流水线”,Stream 流不是一种数据结构,不保存数据,而是对数据进行加工处理。Stream 可以看作是流水线上的一个工序。在流水线上,通过多个工序把一个原材料加工成一个商品。
通过使用 Stream 的 API,能让我们快速完成许多复杂的操作,如筛选、切片、映射、查找、去除重复,统计,匹配和归纳。
5.2、Stream 流思想的体现案例
/*
假设集合中有如下人物:张学友,周润发, 赵薇,张绍忠,张三丰。要求:1、拿到所有姓张的人物
2、拿到名字长度为 3 个字的
3、打印这些数据
实现:传统做法:遍历三次集合,实现规定的要求
流式做法:遍历一个,实现规定要求
*/
//1、传统做法
public class StreamDemo{public static void main(String[] args){ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list,"张无忌","周芷若","赵薇","张强","张三丰");
//1、拿到所有姓张的人物
ArrayList<String> zhang = new ArrayList<String>();
for(String name:list){if(name.startsWith("张")){zhang.add(name);
}
}
//2、拿到名字长度为 3 个字的
ArrayList<String> threeLength = new ArrayList<String>();
for(String name:zhang){if(name.length()==3){threeLength.add(name);
}
}
//3、打印这些数据
for(String name:threeLength){System.out.println(name);
}
}
}
//2、流式做法
public class StreamDemo2{public static void main(String[] args){ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list,"张无忌","周芷若","赵薇","张强","张三丰");
list.stream().filter(s->s.startsWith("张")).
filter(s->s.length()==3).
forEach(System.out::println);
}
}
打印结果:
5.3、获取 Stream 流的两种方式
(1)根据 Collection 获取流
(2)Stream 中的静态方法 of 获取流
5.3.1、根据 Collection 的 stream()方法获取流
5.3.2、根据 Stream 类的静态 of()方法
直接传入多个字符串
传入一个字符串数组
传入一个整数数组
5.5、Stream 的注意事项
1、Stream 只能操作一次
2、Stream 方法返回的是新的流
3、Stream 不调用终结方法,终结的操作不会执行
5.4、Stream 常用 API
Stream 的 API 分为两类:
1、终结方法—> 返回值不是 Stream 类型—> 不支持链式调用
2、非终结方法—> 返回值是 Stream 类型—> 支持链式调用
注意:concat 是 Stream 的静态方法
5.6、Stream 常用 API 演示
forEach: 逐个遍历
count: 逐个遍历
limit: 显示前几个
skip: 跳过前几个
map: 就是把一种类型的流映射成另外一种流
这里的 map 和集合中的 map 不是一个意思,这里的 map 只是把一种类型的值映射成另外一种类型的值,没有键值对!
把字符串转换成整数
sorted:对流中的数据进行排序
distinced: 对流中的数据进行去重
math: 元素匹配,有三种匹配情况
(1)allMatch(): 匹配所有
(2)noneMatch(): 判断是否是无匹配
(3)anyMatch(): 只要有一个匹配就行
find: 元素查找
查找第一个:
方式一:findFirst()
方式二:findAny()
max 和 min 方法,查找最大值和最小值
reduce: 对数据进行加工处理
(1)对数据进行求和
(2)找最大值
map 和 reduce 结合的练习
(1)求集合中 Person 对象的年龄总和
public static void main(String[] args) {// 求流中的人的年龄和
ArrayList<Person> list = new ArrayList<Person>();
Collections.addAll(list,new Person("张学友",18),
new Person("周杰伦",19),
new Person("周润发",20),
new Person("张学友",40));
Integer totalAge = list.stream().map((p) -> {return p.getAge();}).reduce(0, (x, y) -> {return x + y;
});
System.out.println(totalAge);
}
(2)求 Person 中年龄最大是多少
public static void main(String[] args) {// 求流中的人的最大年龄
ArrayList<Person> list = new ArrayList<Person>();
Collections.addAll(list,new Person("张学友",18),
new Person("周杰伦",19),
new Person("周润发",20),
new Person("张学友",40));
Integer max = list.stream().map((p) -> p.getAge()).reduce(0, Integer::max);
System.out.println(max);
}
(3)求字符串数组中”a“出现了多少次?
public static void main(String[] args) {// 求字符串数组中“a”出现了多少次
String[] str = {"a","b","c","a","b"};
Integer count = Stream.of(str).map((s) -> {if (s == "a") {return 1;
} else {return 0;
}
}).reduce(0, (x, y) -> {return x + y;
});
System.out.println(count);
}
Stream 流的 mapToInt 方法
如果需要将 Stream
InteStream 和 Stream 的继承体系
mapToInt 的基本用法
public static void main(String[] args) {IntStream intStream = Stream.of(1, 2, 3).mapToInt((i)->i.intValue());
intStream.forEach(System.out::println);
}
使用基本数据类型可以节省内存空间
收集 Stream 流的结果
(1)收集流的结果到集合中去
(2)收集流的结果到数组中去
收集流的结果到集合中去
public static
public static
public static <T, C extends Collection
收集结果到数组中去
对流中的数据进行聚合计算
(1)获取最大值
(2)获取最小值
(3)求总和
(4)求平均值
(5)分组
(6)多级分组
(7)分区
(8)拼接
获取最大值
求四大天王中年龄最大的
求四大天王中年龄最小的
求四大天王的年龄总和
求四大天王的平均年龄
对流中的数据进行分组
四大天王按照年龄进行分组
map 的便捷遍历方式
原理:
将年龄 19 岁以上(包括 19)分为一组,19 岁以下分为一组。
多级分组
先按性别进行分组,再按年龄进行分组
对流中的数据进行分区,true 为一个列表区,false 为一个列表区
对流中的数据进行拼接
6、并行的 Stream 流
串行的 Stream 流
这样单线程处理,如果数量大,cpu 核心多,势必会造成效率低下、资源浪费的情况!
6.1、并行的 Stream 流的获取方式
(1)通过集合直接获取并行流
(2)通过 Stream 对象的 parallel()方法将串行流转变成并行流
方式一:通过集合直接获取并行流
方式二:通过 Stream 对象的 parallel()方法将串行流转变成并行流
并行的 Stream 流和串行的 Stream 流的计算效率对比
需求:计算 8 亿的累加和
(1)for 循环
(2)串行流
(3)并行流
6.2、并行流的线程安全问题
线程安全问题现场
需求:把 1000 个数字用并行流存到集合中去。
线程安全问题的解决方案:
(1)加锁
(2)使用线程安全的集合
(3)使用串行流
把并行流转成串行流
(4)使用 Collectors 的 toList 方法 /Stream 的 toArray 方法
7、Fork/Join 框架
Fork/Join 框架是并行流 parallelStream 底层使用的技术,Fork/Join 框架是 JDK1.7 底层使用的技术。Fork/Join 框架可以将一个大任务拆分成很多个小任务来异步执行。
Fork/Join 框架主要包含三个模块:
(1):线程池,ForkJoinPool
(2):任务对象,ForkJoinTask
(3):执行任务的线程,ForkJoinWorkerThread
7.1、Fork/Join 框架原理 - 分治法
ForkJoinPool 主要用来使用 分治法(Divide-and-Conquer Algorithm)来解决问题。典型的应用比如快速排序算法,ForkJoinPool 需要使用相对少的线程来处理大量的任务。比如要对 1000 万个数据进行排序,那么会将这个任务分割成两个 500 万数据的合并任务和一个针对这两组 500 万数据的合并任务。以此类推,对于 500 万的数据也会做出同样的分割处理,到最后会设置一个阈值来规定当数据规模到多少时,停止这样的分割处理。比如,当元素的数量小于 10 时,会停止分割,转而使用插入排序对它们进行排序。那么到最后,所有的任务加起来会有大概 2000000+ 个。问题的关键在于,对于一个任务而言,只有当它所有的子任务完成之后,它才能够被执行(其实就是采用了递归算法)。
7.2、Fork/Join 原理 - 工作窃取法
当执行新的任务时 Fork/Join 框架会将任务拆分分成更小的任务执行,并将小任务加到线程队列中,当多个线程同时执行时,就总会有线程先执行完毕,有线程后执行完毕。先执行完毕的线程会从其它线程队列的末尾窃取任务来执行。为什么会从其它线程的末尾窃取了,因为如果从头部位置开始窃取,可能会遇到线程安全的问题。
7.3、ForkJoin 案例
需求:使用 Fork/Join 计算 1 -10000 的累加和,当一个任务的计算量大于 3000 时拆分任务,数量小于 3000 时计算。
package Test;
import java.util.List;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
public class Demo10 {public static void main(String[] args) {long startTime = System.currentTimeMillis();
ForkJoinPool pool = new ForkJoinPool();
SumRecursiveTask task = new SumRecursiveTask(1L,10000L);
Long rst = pool.invoke(task);
long endTime = System.currentTimeMillis();
System.out.println("结果是:"+rst);
System.out.println("计算时间是:"+(endTime-startTime));
}
}
class SumRecursiveTask extends RecursiveTask<Long>{private Long start;
private Long end;
private static final int THRESHOLD=3000;
public SumRecursiveTask(Long start, Long end) {this.start = start;
this.end = end;
}
@Override
protected Long compute() {Long length = end-start;// 计算任务长度
if(length<=THRESHOLD){// 如果在阈值范围内就进行计算
long sum=0;
for(long i=start;i<=end;i++){sum+=i;}
System.out.println("计算:start:"+start+"->"+end+"之间的值是:"+sum);
return sum;
}else{// 如果不在阈值范围内就继续拆分
long middle = (start+end)/2;
System.out.println("拆分:左边"+start+"->"+middle+"右边:"+(middle+1)+"->"+end);
// 递归调用
SumRecursiveTask left = new SumRecursiveTask(start,middle);
left.fork();
SumRecursiveTask right = new SumRecursiveTask(middle+1,end);
right.fork();
return left.join()+right.join();
}
}
}
8、Optional 的使用
我们之前写代码,要经常进行空值的判断,避免出现空指针异常。JDK8 针对这一情况推出了 Optional 来改进这一情况!
首先来看一下之前对 null 值的处理情况吧!
public static void main(String[] args) {Person p= new Person("张三",18);
p=null;
if(p!=null){System.out.println(p);
}else{System.out.println("查无此人");
}
}
8.1、Optional 类介绍
Optional 是一个没有子类的工具类,Optional 是一个可以为 null 的容器对象。它的 作用 主要就是为了解决避免 Null 检查,防止 NullPointerException。
8.2、Optional 类的创建方式:
Optional.of(T t): 创建一个 Optional 实例
Optional.empty(): 创建一个空的 Optional 实例
Optional.ofNullable(T t): 若 t 不为null, 创建 Optional 实例,否则创建空实例
//Optional.of(T t): 创建一个 Optional 实例
public static void main(String[] args) {Person p= new Person("张三",18);
Optional<Person> p1 = Optional.of(p);// 不能放空值
Person person = p1.get();// 获取对象
System.out.println(person);
}
public static void main(String[] args) {Person p= new Person("张三",18);
//Optional.ofNullable(T t): 若 t 不为 null, 创建 Optional 实例,否则创建空实例
Optional<Person> p1 = Optional.ofNullable(p);
Person person = p1.get();
System.out.println(person);
}
8.3、Optional 类的常用方法:
isPresent(): 判断是否包含值,包含值返回true, 不包含值返回false
get(): 如果 Optional 有值则将其返回,否则抛出 NoSuchElementException
orElse(T t): 如果调用对象包含值,返回该值,否则返回参数 t
orElseGet(Supplier s): 如果调用对象包含值,返回该值,否则返回 s 获取的值
map(Function f): 如果有值对其处理,并返回处理后的 Optional, 否则返回 Optional.empty()
9、JDK8 的时间和日期
9.1、旧版日期时间 API 存在的问题
1、设计很差:在 Java.util 和 java.sql 的包中都有日期类,java.util.Date 同时包含日期和时间,而 java.sql.Date 仅包含日期。此处用于格式化和解析的类在 java.text 包中定义。
2、非线程安全:java.util.Date 是非线程安全的,所有的日期类都是可变的,这是 java 日期类最大的问题之一。
3、时区处理麻烦:日期类并不提供国际化,没有时区支持。因此 Java 引入了 Java.util.Calendar 和 Java.util.TimeZone 类,但他们同样存在上述所有的问题。
Date 类的缺陷
public static void main(String[] args) throws Exception {Date d = new Date(1990,1,1);
System.out.println(d);
}
日期解析和格式化缺陷
线程不安全
public static void main(String[] args) throws Exception {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
for (int i=0;i<100;i++){new Thread(()->{Date d = null;
try {d = sdf.parse("2020-1-1");
} catch (ParseException e) {e.printStackTrace();
}
System.out.println(d.toLocaleString());
}).start();}
}
9.2、新版本日期时间 API 介绍
JDK8 中增加了一套全新的日期和时间 API, 这套 API 设计合理,是线程安全的。新的日期及时间 API 位于 java.time 包中,下面是一些关键类。
LocalDate: 表示日期,包含年月日,格式为 2019-10-16
LocalTime: 表示时间,包含时分秒,格式为 16:38:54 158549300
LocalDateTime: 表示日期时间,包含年月日,时分秒,格式为:2018-09-06T15:33:56.750
DateTimeFormatter: 日期时间格式化类。
Instant: 时间戳,表示一个特定的时间瞬间
Duration: 用于计算两个时间(LocalTime, 时分秒)的距离
Period: 用于计算两个日期(LocalDate, 年月日)的距离
ZonedDateTime: 包含时区的时间
9.3、Java 中的历法
java 中使用的历法是 ISO 8601 日历系统,它是世界民用历法,也就是我们所说的公历。平年有 365 天,闰年有 366 天。此外 Java 8 还提供了 4 套其他历法,分别是:
ThaiBuddhistDate: 泰国佛教历
MinguoDate: 中华民国历
JapaneseDate: 日本历
HijrahDate: 伊斯兰历
9.4、JDK8 的日期和时间类
LocalDate、LocalTime、LocalDateTime 类的实例是不可变的对象,分别表示使用 ISO-8601 日历系统的日期、时间、日期和时间。他们提供了简单的日期或时间,并不包含当前的时间信息,也不包含与时区相关的信息。
public static void main(String[] args) throws Exception {//LocalDate: 表示日期,年、月、日
// 创建指定日期
LocalDate fj = LocalDate.of(2020,1,1);
System.out.println(fj);//2020-01-01
// 创建当前日期
LocalDate now = LocalDate.now();
System.out.println(now);
// 获取日期信息
System.out.println(now.getYear());// 获取年
System.out.println(now.getMonthValue());// 获取月
System.out.println(now.getDayOfMonth());// 日
System.out.println(now.getDayOfWeek());// 周几
}
public static void main(String[] args) throws Exception {//LocalTime: 表示十分秒
// 得到当前时间对象
LocalTime lt = LocalTime.now();
System.out.println(lt);
int hour = lt.getHour();// 得到小时
int minute = lt.getMinute();// 得到分钟
int second = lt.getSecond();// 得到秒
int nano = lt.getNano();// 得到纳秒
System.out.println(hour);
System.out.println(minute);
System.out.println(second);
System.out.println(nano);
}
public static void main(String[] args) throws Exception {// 获取当前日期和时间
LocalDateTime ldt = LocalDateTime.now();
// 以下分别得到:年、月、日、时、分、秒、纳秒
int year = ldt.getYear();
int month = ldt.getMonthValue();
int day = ldt.getDayOfMonth();
int hour = ldt.getHour();
int minute = ldt.getMinute();
int second = ldt.getSecond();
int nano = ldt.getNano();
System.out.println(year);
System.out.println(month);
System.out.println(day);
System.out.println(hour);
System.out.println(minute);
System.out.println(second);
System.out.println(nano);
}
对日期的修改,使用 withAttribute 方法,该方法会 返回修改之后的日期时间对象,原来的日期时间对象不会发生改变。
修改日期
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);
// 修改年份
LocalDateTime ldt2 = ldt.withYear(2022);
System.out.println(ldt2);
}
增加和减少年份
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
System.out.println(ldt);
// 增加和减少年份
LocalDateTime localDateTime = ldt.plusYears(10);// 增加年份
System.out.println(localDateTime);
// 减少年份
LocalDateTime localDateTime1 = localDateTime.minusYears(10);
System.out.println(localDateTime1);
}
年份的判断
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
LocalDateTime ldt2 = LocalDateTime.of(2019, 10, 10, 10, 10, 10);
boolean after = ldt.isAfter(ldt2); // 是否在 ldt2 之后
System.out.println(after);
boolean before = ldt.isBefore(ldt2);// 是否在 ldt2 之前
System.out.println(before);
boolean equal = ldt.isEqual(ldt2);// 是否和 ldt2 相等
System.out.println(equal);
}
JDK 8 的时间格式化与解析
通过 java.time.format.DateTimeFormatter 类可以进行日期时间解析与格式化
JDK 自带的时间、日期格式化模板
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME;//jdk 自带的时间日期格式化模板
String format = isoDateTime.format(ldt);
System.out.println(format);
}
自定义时间、日期格式化模板
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter zdy = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒");// 自定义解析模板
String format = zdy.format(ldt);
System.out.println(format);
}
时间日期的解析
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter zdy = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒");// 自定义解析模板
LocalDateTime parse = LocalDateTime.parse("2020 年 01 月 04 日 10 时 30 分 05 秒", zdy);
System.out.println(parse);
}
public static void main(String[] args) throws Exception {LocalDateTime ldt = LocalDateTime.now();
DateTimeFormatter zdy = DateTimeFormatter.ofPattern("yyyy 年 MM 月 dd 日 HH 时 mm 分 ss 秒");// 自定义解析模板
for (int i=0;i<100;i++){new Thread(new Runnable() {@Override
public void run() {LocalDateTime parse = LocalDateTime.parse("2020 年 01 月 04 日 10 时 30 分 05 秒", zdy);
System.out.println(parse);
}
}
).start();}
}
Instant
Instant 时间戳 / 时间线,内部保存了从 1970 年 1 月 1 日 00:00:00 以来的秒和纳秒
public static void main(String[] args) throws Exception {Instant now = Instant.now();
System.out.println("当前时间戳 ="+now);
System.out.println(now.getNano());// 获取时间戳的纳秒值
System.out.println(now.getEpochSecond());
System.out.println(now.toEpochMilli());
System.out.println(System.currentTimeMillis());
}
Instant 的加操作
计算时间和日期差
1、Duration: 用于计算 2 个时间(LocalTime, 时分秒)的距离
2、Period: 用于计算 2 个日期(LocalDate, 年月日)的距离
JDK8 的时间矫正器
有时我们可能需要获得特定的日期。例如:将日期调整到“下个月的第一天”等操作。可以通过时间矫正器来进行。
TemporalAdjuster: 时间校正器
TemporalAdjusters: 该类通过静态方法提供了大量的常用 TemporalAdjuster 实现。
TemporalAdjuster: 时间校正器
//TemporaAdjuster: 时间矫正器
//TemporalAdjusters: 该类通过静态方法提供了大量的常用 TemporalAdjuster 的实现
public class Demo36 {public static void main(String[] args) throws Exception {LocalDateTime dateTime = LocalDateTime.now();
// 得到下个月的第一天
TemporalAdjuster firsWeekDayOfNextMonth = (Temporal temporal) -> {LocalDateTime ldt = (LocalDateTime) temporal;// 把 temporal 转换成 LocalDateTime 类型
LocalDateTime nextMonth = ldt.plusMonths(1).withDayOfMonth(1);// 调整时间,加一个月,加一个月后的第一天
System.out.println(nextMonth);
return nextMonth;// 返回调整之后的时间
};
}
}
public static void main(String[] args) throws Exception {LocalDateTime dateTime = LocalDateTime.now();
// 得到下个月的第一天
TemporalAdjuster firsWeekDayOfNextMonth = (Temporal temporal) -> {LocalDateTime ldt = (LocalDateTime) temporal;// 把 temporal 转换成 LocalDateTime 类型
LocalDateTime nextMonth = ldt.plusMonths(1).withDayOfMonth(1);// 调整时间,加一个月,加一个月后的第一天
System.out.println(nextMonth);
return nextMonth;// 返回调整之后的时间
};
LocalDateTime now = dateTime.with(firsWeekDayOfNextMonth);
// LocalDateTime now2 = (LocalDateTime)firsWeekDayOfNextMonth;
System.out.println(now);
}
结果:
JDK8 设置时间和日期的时区
Java8 中加入了对时区的支持,LocalDate、LocalTime、LocalDateTime 是不带时区的,带时区的日期时间类分别为:ZonedDate、ZonedTime、ZonedDateTime。
其中每个时区都对应着 ID,ID 的格式为“区域 / 城市”。例如:Asia/Shanghai 等。
Zoneld: 该类中包含了所有的时区信息
public static void main(String[] args) throws Exception {// 获得所有的时区 ID
// ZoneId.getAvailableZoneIds().forEach(System.out::println);
// 创建世界标准时间
final ZonedDateTime zonedDateTimeFromClock = ZonedDateTime.now(Clock.systemUTC());
System.out.println(zonedDateTimeFromClock);//2020-01-09T01:44:36.951697400Z
// 创建一个当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now);//2020-01-09T09:48:21.130519700
// 创建一个带时区的 ZonedDateTime
final ZonedDateTime zonedDateTimeFromZone = ZonedDateTime.now(ZoneId.of("Africa/Nairobi"));
System.out.println(zonedDateTimeFromZone);//2020-01-09T04:48:21.131519700+03:00[Africa/Nairobi]
}