JDK1.8 新特性总结
- Lambda 表达式
1.1 Lambda 表达式是什么?
Lambda 表达式有两个特点:一是匿名函数,二是可传递。
匿名函数的应用场景是:通常是在需要一个函数,但是又不想费神去命名一个函数的场合下使用 Lambda 表达式。lambda 表达式所表示的匿名函数的内容应该是很简单的,如果复杂的话,干脆就重新定义一个函数了,使用 lambda 就有点过于执拗了。
可传递使用场景是:就是将 Lambda 表达式传递给其他的函数,它当做参数,Lambda 作为一种更紧凑的代码风格,使 Java 的语言表达能力得到提升。
lambda 表达式专门针对只有一个方法的接口(即函数式接口)
1.2 Lambda 表达式语法
Lambda 表达式在 Java 语言中引入了一个新的语法元素和操作符。这个操作符为 ”- >”,该操作符被称为 Lambda 操作符或箭头操作符,它将 Lambda 分为两个部分:
左侧:指定了 Lambda 表达式所需要的所有参数 右侧:指定了 Lambda 体,即 Lambda 表达式所要执行的功能
([参数可选,…]) -> {
}
常见的语法格式
语法格式一:无参,无返回值,Lambda 体只需要一条语句
Runnable r = () -> System.out.println(“Hello Lambda!”);
语法格式二:Lambda 需要一个参数
Consumer<String> con = (x) -> System.out.println(x);
语法格式三:Lambda 只需要一个参数时,参数的小括号可以省略
Consumer<String> con = x -> System.out.println(x);
语法格式四:Lambda 需要两个参数,并且有返回值
Comparator<Integer> com = (x, y) -> {System.out.println(“ 函数式接口 ”); return Integer.compare(x, y); };
语法格式五:当 Lambda 体只有一条语句时,return 与大括号可以省略
Comparator<Integer> com = (x, y) -> Integer.compare(x, y);
语法格式六:数据类型可以省略,因为可由编译器推断得出,称为类型推断
BinaryOperator<Long> operator = (Long x, Long y) -> {System.out.println(“ 实现函数接口方法 ”); return x + y; };
1.3 Lambda 表达式实战
实战 1:线程
public class Test {
public static void main(String[] args) {
// Java8 之前:
new Thread(new Runnable() {
public void run() {
System.out.println(“hello world”);
}
}).start();
// Java8 方式:
new Thread(() -> System.out.println(“hello world”)).start();
}
}
实战 2:集合元素的遍历
public class Test2 {
public static void main(String[] args) {
// Java8 之前:
List<String> list1 = Arrays.asList(“a”, “b”, “c”, “d”);
for (String str : list1) {
System.out.println(str);
}
// Java 8 之后:
List list2 = Arrays.asList(“a”, “b”, “c”, “d”);
list2.forEach(n -> System.out.println(n));
// 使用 Java 8 的方法引用更方便,方法引用由:: 双冒号操作符标示,
list2.forEach(System.out::println);
}
}
实战 3:map 函数
map 函数可以说是函数式编程里最重要的一个方法了。map 的作用是将一个对象变换为另外一个。
public class Test3 {
public static void main(String[] args) {
map();
}
public static void map() {
List<Double> cost = Arrays.asList(10.0, 20.0, 30.0);
cost.stream().map(x -> x + x * 0.05).forEach(x -> System.out.println(x));
}
}
实战 4:reduce 函数
map 的作用是将一个对象变为另外一个,而 reduce 实现的则是将所有值合并为一个
public class Test4 {
public static void main(String[] args) {
mapReduce();
}
public static void mapReduce() {
List<Double> cost = Arrays.asList(10.0, 20.0, 30.0);
double allCost = cost.stream().map(x -> x + x * 0.05).reduce((sum, x) -> sum + x).get();
System.out.println(allCost);
}
}
实战 5:过滤
public class Test5 {
public static void main(String[] args) {
filter();
}
public static void filter() {
List<Double> cost = Arrays.asList(10.0, 20.0, 30.0, 40.0);
List<Double> filteredCost = cost.stream().filter(x -> x > 25.0).collect(Collectors.toList());
filteredCost.forEach(x -> System.out.println(x));
}
}
实战 6:Predicate 过滤
public class Test6 {
public static void filter(List<String> languages, Predicate<String> condition) {
languages.stream().filter(x -> condition.test(x)).forEach(x -> System.out.println(x + ” “));
}
public static void main(String[] args) {
List<String> languages = Arrays.asList(“Java”, “Python”, “scala”, “Shell”, “R”);
System.out.println(“Language starts with J: “);
filter(languages, x -> x.startsWith(“J”));
System.out.println(“\nLanguage ends with a: “);
filter(languages, x -> x.endsWith(“a”));
System.out.println(“\nAll languages: “);
filter(languages, x -> true);
System.out.println(“\nNo languages: “);
filter(languages, x -> false);
System.out.println(“\nLanguage length bigger three: “);
filter(languages, x -> x.length() > 4);
}
}
实战 7:比较
首先看看在老版本的 Java 中是如何比较字符串的:
List<String> names = Arrays.asList(“peter”, “anna”, “mike”, “xenia”);
Collections.sort(names, new Comparator<String>() {
@Override
public int compare(String a, String b) {
return b.compareTo(a);
}
});
只需要给静态方法 Collections.sort 传入一个 List 对象以及一个比较器来按指定顺序排列。通常做法都是创建一个匿名的比较器对象然后将其传递给 sort 方法。
在 Java 8 中你就没必要使用这种传统的匿名对象的方式了,Java 8 提供了更简洁的语法,lambda 表达式:
Collections.sort(names, (String a, String b) -> {
return b.compareTo(a);
});
看到了吧,代码变得更段且更具有可读性,但是实际上还可以写得更短:
Collections.sort(names, (String a, String b) -> b.compareTo(a));
对于函数体只有一行代码的,你可以去掉大括号 {} 以及 return 关键字,但是你还可以写得更短点:
Collections.sort(names, (a, b) -> b.compareTo(a));
Java 编译器可以自动推导出参数类型,所以你可以不用再写一次类型。
1.4 Lambda 作用域
在 lambda 表达式中访问外层作用域和老版本的匿名对象中的方式很相似。你可以直接访问标记了 final 的外层局部变量,或者实例的字段以及静态变量。
示例:
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert(“123”);
System.out.println(converted); // 123
1.4.1 访问局部变量
我们可以直接在 lambda 表达式中访问外层的局部变量:
final int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
但是和匿名对象不同的是,这里的变量 num 可以不用声明为 final,该代码同样正确:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
stringConverter.convert(2); // 3
不过这里的 num 必须不可被后面的代码修改(即隐性的具有 final 的语义),例如下面的就无法编译:
int num = 1;
Converter<Integer, String> stringConverter =
(from) -> String.valueOf(from + num);
num = 3;
在 lambda 表达式中试图修改 num 同样是不允许的。
1.4.2 访问对象字段与静态变量
和本地变量不同的是,lambda 内部对于实例的字段以及静态变量是即可读又可写。该行为和匿名对象是一致的:
class Lambda4 {
static int outerStaticNum;
int outerNum;
void testScopes() {
Converter<Integer, String> stringConverter1 = (from) -> {
outerNum = 23;
return String.valueOf(from);
};
Converter<Integer, String> stringConverter2 = (from) -> {
outerStaticNum = 72;
return String.valueOf(from);
};
}
}
1.4.3 访问接口的默认方法
还记得第一节中的 formula 例子么,接口 Formula 定义了一个默认方法 sqrt 可以直接被 formula 的实例包括匿名对象访问到,但是在 lambda 表达式中这个是不行的。Lambda 表达式中是无法访问到默认方法的,以下代码将无法编译:
Formula formula = (a) -> sqrt(a * 100);
Built-in Functional Interfaces
- 函数式接口
简单来说就是只定义了一个抽象方法的接口(Object 类的 public 方法除外),就是函数式接口,并且还提供了注解:@FunctionalInterface
Lambda 表达式是如何在 java 的类型系统中表示的呢?
每一个 lambda 表达式都对应一个类型,通常是接口类型。而“函数式接口”是指仅仅只包含一个抽象方法的接口,每一个该类型的 lambda 表达式都会被匹配到这个抽象方法。因为 默认方法 不算抽象方法,所以你也可以给你的函数式接口添加默认方法。
我们可以将 lambda 表达式当作任意只包含一个抽象方法的接口类型,确保你的接口一定达到这个要求,你只需要给你的接口添加 @FunctionalInterface 注解,编译器如果发现你标注了这个注解的接口有多于一个抽象方法的时候会报错的。
示例如下:
@FunctionalInterface
interface Converter<F, T> {
T convert(F from);
}
Converter<String, Integer> converter = (from) -> Integer.valueOf(from);
Integer converted = converter.convert(“123”);
System.out.println(converted); // 123
需要注意如果 @FunctionalInterface 如果没有指定,上面的代码也是对的。
译者注 将 lambda 表达式映射到一个单方法的接口上,这种做法在 Java 8 之前就有别的语言实现,比如 RhinoJavaScript 解释器,如果一个函数参数接收一个单方法的接口而你传递的是一个 function,Rhino 解释器会自动做一个单接口的实例到 function 的适配器,典型的应用场景有 org.w3c.dom.events.EventTarget 的 addEventListener 第二个参数 EventListener。
常见的四大函数式接口
1 Consumer 接口
从字面意思上我们就可以看得出啦,consumer 接口
就是一个消费型的接口,通过传入参数,然后输出值,就是这么简单,Java8 的一些方法看起来很抽象,其实,只要你理解了就觉得很好用,并且非常的简单。
我们下面就先看一个例子,然后再来分析这个接口。
1.1 Consumer 实例
/**
* consumer 接口测试
*/
@Test
public void test_Consumer() {
//① 使用 consumer 接口实现方法
Consumer<String> consumer = new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
};
Stream<String> stream = Stream.of(“aaa”, “bbb”, “ddd”, “ccc”, “fff”);
stream.forEach(consumer);
System.out.println(“********************”);
//② 使用 lambda 表达式,forEach 方法需要的就是一个 Consumer 接口
stream = Stream.of(“aaa”, “bbb”, “ddd”, “ccc”, “fff”);
Consumer<String> consumer1 = (s) -> System.out.println(s);//lambda 表达式返回的就是一个 Consumer 接口
stream.forEach(consumer1);
// 更直接的方式
//stream.forEach((s) -> System.out.println(s));
System.out.println(“********************”);
//③ 使用方法引用,方法引用也是一个 consumer
stream = Stream.of(“aaa”, “bbb”, “ddd”, “ccc”, “fff”);
Consumer consumer2 = System.out::println;
stream.forEach(consumer);
// 更直接的方式
//stream.forEach(System.out::println);
}
输出结果
1.2 实例分析
① consumer
接口分析
在代码①中,我们直接创建 Consumer
接口,并且实现了一个名为 accept
的方法,这个方法就是这个接口的关键了。
我们看一下 accept
方法;这个方法传入一个参数,不返回值。当我们发现 forEach
需要一个 Consumer
类型的参数的时候,传入之后,就可以输出对应的值了。
② lambda 表达式作为 consumer
Consumer<String> consumer1 = (s) -> System.out.println(s);//lambda 表达式返回的就是一个 Consumer 接口
在上面的代码中,我们使用下面的 lambda
表达式作为 Consumer
。仔细的看一下你会发现,lambda
表达式返回值就是一个 Consumer
;所以,你也就能够理解为什么 forEach
方法可以使用 lamdda 表达式作为参数了吧。
③ 方法引用作为 consumer
Consumer consumer2 = System.out::println;
在上面的代码中,我们用了一个 方法引用 的方式作为一个 Consumer,同时也可以传给 forEach
方法。
1.3 其他 Consumer 接口
除了上面使用的 Consumer 接口,还可以使用下面这些 Consumer 接口。IntConsumer、DoubleConsumer、LongConsumer、BiConsumer
,使用方法和上面一样。
1.4 Consumer 总结
看完上面的实例我们可以总结为几点。
① Consumer 是一个接口,并且只要实现一个 accept
方法,就可以作为一个 “消费者” 输出信息。② 其实,lambda 表达式、方法引用的返回值都是 Consumer 类型,所以,他们能够作为 forEach
方法的参数,并且输出一个值。
2 Supplier 接口
Supplier 接口是一个 供给型 的接口,其实,说白了就是一个 容器,可以用来存储数据,然后可以供其他方法使用的这么一个接口,是不是很明白了,如果还是不明白,看看下面的例子,一定彻底搞懂!
2.1 Supplier 实例
**
* Supplier 接口测试,supplier 相当一个容器或者变量,可以存储值
*/
@Test
public void test_Supplier() {
//① 使用 Supplier 接口实现方法, 只有一个 get 方法,无参数,返回一个值
Supplier<Integer> supplier = new Supplier<Integer>() {
@Override
public Integer get() {
// 返回一个随机值
return new Random().nextInt();
}
};
System.out.println(supplier.get());
System.out.println(“********************”);
//② 使用 lambda 表达式,
supplier = () -> new Random().nextInt();
System.out.println(supplier.get());
System.out.println(“********************”);
//③ 使用方法引用
Supplier<Double> supplier2 = Math::random;
System.out.println(supplier2.get());
}
2.2 实例分析
① Supplier 接口分析
Supplier<Integer> supplier = new Supplier<Integer>() {
@Override
public Integer get() {
// 返回一个随机值
return new Random().nextInt();
}
};
看一下这段代码,我们通过创建一个 Supplier 对象,实现了一个 get
方法,这个方法无参数,返回一个值;所以,每次使用这个接口的时候都会返回一个值,并且保存在这个接口中,所以说是一个 容器。
② lambda 表达式作为 Supplier
//② 使用 lambda 表达式,
supplier = () -> new Random().nextInt();
System.out.println(supplier.get());
System.out.println(“********************”);
上面的这段代码,我们使用 lambda 表达式 返回一个 Supplier 类型的接口,然后,我们调用 get
方法就可以获取这个值了。
③ 方法引用作为 Supplier
//③ 使用方法引用
Supplier<Double> supplier2 = Math::random;
System.out.println(supplier2.get());
方法引用也是返回一个 Supplier 类型的接口。
2.3 Supplier 实例 2
我们看完第一个实例之后,我们应该有一个了解了,下面再看一个。
/**
* Supplier 接口测试 2,使用需要 Supplier 的接口方法
*/
@Test
public void test_Supplier2() {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5);
// 返回一个 optional 对象
Optional<Integer> first = stream.filter(i -> i > 4)
.findFirst();
//optional 对象有需要 Supplier 接口的方法
//orElse,如果 first 中存在数,就返回这个数,如果不存在,就放回传入的数
System.out.println(first.orElse(1));
System.out.println(first.orElse(7));
System.out.println(“********************”);
Supplier<Integer> supplier = new Supplier<Integer>() {
@Override
public Integer get() {
// 返回一个随机值
return new Random().nextInt();
}
};
//orElseGet,如果 first 中存在数,就返回这个数,如果不存在,就返回 supplier 返回的值
System.out.println(first.orElseGet(supplier));
}
输出结果
代码分析
Optional<Integer> first = stream.filter(i -> i > 4)
.findFirst();
使用这个方法获取到一个 Optional 对象,然后,在 Optional 对象中有 orElse 方法 和 orElseGet 是需要一个 Supplier 接口的。
//optional 对象有需要 Supplier 接口的方法
//orElse,如果 first 中存在数,就返回这个数,如果不存在,就放回传入的数
System.out.println(first.orElse(1));
System.out.println(first.orElse(7));
System.out.println(“********************”);
Supplier<Integer> supplier = new Supplier<Integer>() {
@Override
public Integer get() {
// 返回一个随机值
return new Random().nextInt();
}
};
//orElseGet,如果 first 中存在数,就返回这个数,如果不存在,就返回 supplier 返回的值
System.out.println(first.orElseGet(supplier));
- orElse:如果 first 中存在数,就返回这个数,如果不存在,就放回传入的数
- orElseGet:如果 first 中存在数,就返回这个数,如果不存在,就返回 supplier 返回的值
2.4 其他 Supplier 接口
除了上面使用的 Supplier 接口,还可以使用下面这些 Supplier 接口。IntSupplier、DoubleSupplier、LongSupplier、BooleanSupplier
,使用方法和上面一样。
2.5 Supplier 总结
① Supplier 接口可以理解为一个容器,用于装数据的。② Supplier 接口有一个 get
方法,可以返回值。
3 Predicate 接口
Predicate 接口是一个谓词型接口,其实,这个就是一个类似于 bool 类型的判断的接口,后面看看就明白了。
3.1 Predicate 实例
/**
* Predicate 谓词测试,谓词其实就是一个判断的作用类似 bool 的作用
*/
@Test
public void test_Predicate() {
//① 使用 Predicate 接口实现方法, 只有一个 test 方法,传入一个参数,返回一个 bool 值
Predicate<Integer> predicate = new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
if(integer > 5){
return true;
}
return false;
}
};
System.out.println(predicate.test(6));
System.out.println(“********************”);
//② 使用 lambda 表达式,
predicate = (t) -> t > 5;
System.out.println(predicate.test(1));
System.out.println(“********************”);
}
输出结果
3.2 实例分析
① Predicate 接口分析
//① 使用 Predicate 接口实现方法, 只有一个 test 方法,传入一个参数,返回一个 bool 值
Predicate<Integer> predicate = new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
if(integer > 5){
return true;
}
return false;
}
};
这段代码中,创建了一个 Predicate
接口对象,其中,实现类 test
方法,需要传入一个参数,并且返回一个 bool
值,所以这个接口作用就是 判断!
System.out.println(predicate.test(6));
再看,调用 test 方法,传入一个值,就会返回一个 bool 值。
② 使用 lambda 表达式作为 predicate
//② 使用 lambda 表达式,
predicate = (t) -> t > 5;
System.out.println(predicate.test(1));
System.out.println(“********************”);
lambda 表达式返回一个 Predicate
接口,然后调用 test
方法!
3.3 Predicate 接口实例 2
/**
* Predicate 谓词测试,Predicate 作为接口使用
*/
@Test
public void test_Predicate2() {
//① 将 Predicate 作为 filter 接口,Predicate 起到一个判断的作用
Predicate<Integer> predicate = new Predicate<Integer>() {
@Override
public boolean test(Integer integer) {
if(integer > 5){
return true;
}
return false;
}
};
Stream<Integer> stream = Stream.of(1, 23, 3, 4, 5, 56, 6, 6);
List<Integer> list = stream.filter(predicate).collect(Collectors.toList());
list.forEach(System.out::println);
System.out.println(“********************”);
}
这段代码,首先创建一个 Predicate 对象,然后实现 test
方法,在 test 方法中做一个判断:如果传入的参数大于 5,就返回 true,否则返回 false;
Stream<Integer> stream = Stream.of(1, 23, 3, 4, 5, 56, 6, 6);
List<Integer> list = stream.filter(predicate).collect(Collectors.toList());
list.forEach(System.out::println);
这段代码调用 Stream
的 filter
方法,filter
方法需要的参数就是 Predicate 接口,所以在这里只要 大于 5 的数据 就会输出。
3.4 Predicate 接口总结
① Predicate 是一个谓词型接口,其实只是起到一个判断作用。② Predicate 通过实现一个 test
方法做判断。
4 Function 接口
Function 接口是一个功能型接口,它的一个作用就是转换作用,将输入数据转换成另一种形式的输出数据。
4.1 Function 接口实例
/**
* Function 测试,function 的作用是转换,将一个值转为另外一个值
*/
@Test
public void test_Function() {
//① 使用 map 方法,泛型的第一个参数是转换前的类型,第二个是转化后的类型
Function<String, Integer> function = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();// 获取每个字符串的长度,并且返回
}
};
Stream<String> stream = Stream.of(“aaa”, “bbbbb”, “ccccccv”);
Stream<Integer> stream1 = stream.map(function);
stream1.forEach(System.out::println);
System.out.println(“********************”);
}
输出结果
4.2 代码分析
① Function 接口分析
//① 使用 map 方法,泛型的第一个参数是转换前的类型,第二个是转化后的类型
Function<String, Integer> function = new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return s.length();// 获取每个字符串的长度,并且返回
}
};
这段代码创建了一个 Function
接口对象,实现了一个 apply
方法,这个方法有一个输入参数和一个输出参数。其中,泛型的第一个参数是转换前的类型,第二个是转化后的类型。
在上面的代码中,就是 获取字符串的长度,然后将每个字符串的长度作为返回值返回。
② 重要应用 map 方法
Stream<String> stream = Stream.of(“aaa”, “bbbbb”, “ccccccv”);
Stream<Integer> stream1 = stream.map(function);
stream1.forEach(System.out::println);
在 Function
接口的重要应用不得不说 Stream
类的 map
方法了,map
方法传入一个 Function
接口,返回一个转换后的 Stream
类。
4.3 其他 Function 接口
除了上面使用的 Function 接口,还可以使用下面这些 Function 接口。IntFunction、DoubleFunction、LongFunction、ToIntFunction、ToDoubleFunction、DoubleToIntFunction 等等,使用方法和上面一样。
4.4 Function 接口总结
① Function 接口是一个功能型接口,是一个转换数据的作用。② Function 接口实现 apply
方法来做转换。
- 方法与构造函数引用
若 lambda 体中的内容有方法已经实现了,那么可以使用“方法引用”也可以理解为方法引用是 lambda 表达式的另外一种表现形式并且其语法比 lambda 表达式更加简单
3.1 方法引用
方法引用其实是 lambda 表达式的部分的简化,也就是为了简化 lambda 表达式而存在的感觉,下面我们还讲讲怎么使用 方法引用。
三种表现形式:
- 对象::实例方法名
- 类::静态方法名
- 类::实例方法名(lambda 参数列表中第一个参数是实例方法的调用 者,第二个参数是实例方法的参数时可用)
示例 1
public void test() {
/**
* 注意:
*1.lambda 体中调用方法的参数列表与返回值类型,要与函数式接口中抽象方法的函数列表和返回值类型保持一致!
* 2. 若 lambda 参数列表中的第一个参数是实例方法的调用者,而第二个参数是实例方法的参数时,可以使用 ClassName::method
*
*/
Consumer<Integer> con = (x) -> System.out.println(x);
con.accept(100);
// 方法引用 - 对象:: 实例方法
Consumer<Integer> con2 = System.out::println;
con2.accept(200);
// 方法引用 - 类名:: 静态方法名
BiFunction<Integer, Integer, Integer> biFun = (x, y) -> Integer.compare(x, y);
BiFunction<Integer, Integer, Integer> biFun2 = Integer::compare;
Integer result = biFun2.apply(100, 200);
// 方法引用 - 类名:: 实例方法名
BiFunction<String, String, Boolean> fun1 = (str1, str2) -> str1.equals(str2);
BiFunction<String, String, Boolean> fun2 = String::equals;
Boolean result2 = fun2.apply(“hello”, “world”);
System.out.println(result2);
}
示例 2
@Test
public void test_method_reference() {
// 使用 lambda 表达式
Stream.of(“A”, “BB”, “CCC”, “DDDD”, “FFFFF”)
.map(s -> s.length()) //lambda
.forEach((x) -> {
System.out.println(x);
});
// 使用静态方法引用
Stream.of(“A”, “BB”, “CCC”, “DDDD”, “FFFFF”)
.map(String::length) // 静态方法引用
.forEach((x) -> {
System.out.println(x);
});
// 使用实例方法引用
Stream.of(
new ClassMate(“1”, “ 欧阳思海 ”),
new ClassMate(“2”, “sihai”)
).map(ClassMate::getName)// 实例方法引用
.forEach(x -> {
System.out.println(x);
});
}
在第一个测试中,我们用的是 lambda 表达式来 获取每个字符串的长度。
s -> s.length()
在第二个测试中,我们使用的是静态方法引用来 获取每个字符串的长度。
String::length
在第三个测试中,我们使用的是实例方法引用。
ClassMate::getName
解释 ① map 方法是映射的意思。② forEach 方式是遍历每一个元素。③ ClassMate 是一个包含 id 和 name 的简单 po 类。
通过上面这个例子,基本上我们就知道怎么使用方法引用了。下面我们进行一个小的总结。
总结 ① 使用方法
类名:: 方法名
② 方法可以是:静态方法,实例方法
3.2 构造器引用
在上面我们讲了方法引用的基本使用方法,其实除了方法引用以外,还有构造函数引用,回想一下,以前我们创建对象是怎么做?是不是需要 new 一个对象呢,那么现在用构造函数引用又是怎么做的呢?
格式:ClassName::new
示例 1
public void test2() {
// 构造方法引用 类名::new
Supplier<Employee> sup = () -> new Employee();
System.out.println(sup.get());
Supplier<Employee> sup2 = Employee::new;
System.out.println(sup2.get());
// 构造方法引用 类名::new(带一个参数)
Function<Integer, Employee> fun = (x) -> new Employee(x);
Function<Integer, Employee> fun2 = Employee::new;
System.out.println(fun2.apply(100));
}
示例 2
/**
* @return void
* @Author ouyangsihai
* @Description 构造函数引用测试
* @Date 10:23 2019/5/14
* @Param []
**/
@Test
public void test_method_reference2() {
// 使用 lambda 表达式
Stream.of(“A”, “BB”, “CCC”, “DDDD”, “FFFFF”)
.map(s -> new ClassMate(s)) //lambda
.collect(Collectors.toList());
// 使用构造函数引用
Stream.of(“A”, “BB”, “CCC”, “DDDD”, “FFFFF”)
.map(ClassMate::new) // 构造函数引用, 由上下文决定用哪一个构造函数
.collect(Collectors.toList());
}
① 第一个我们使用的是 lambda 表达式进行创建对象的 s -> new ClassMate(s)
。② 第二个我们使用的是 构造函数引用 创建对象的 ClassMate::new
。③ 我们发现构造函数引用:类名::new
,然后对于使用哪一个构造函数是由上下文决定的,比如有一个参数和两个参数和无参数的构造函数,会自动确定用哪一个。
3.3 数组引用
格式:Type[]::new
public void test(){
// 数组引用
Function<Integer, String[]> fun = (x) -> new String[x];
Function<Integer, String[]> fun2 = String[]::new;
String[] strArray = fun2.apply(10);
Arrays.stream(strArray).forEach(System.out::println);
}
- 接口的默认方法和静态方法
4.1 默认方法
默认方法很简单,用 default
声明即可。
@FunctionalInterface
public interface FunctionalInterfaceTest {
// 继承接口后,又加了新的抽象方法,这个接口就不再是函数式接口
void test(String s);
// 默认方法
default String getStr(){
return null;
}
}
① 在接口中添加了一个默认方法。并且实现了方法。
4.2 静态方法
默认方法很简单,用 static
声明即可。
@FunctionalInterface
public interface FunctionalInterfaceTest {
// 继承接口后,又加了新的抽象方法,这个接口就不再是函数式接口
void test(String s);
// 静态方法
static String getStr2(){
return null;
}
// 错误用法
default static String getStr3(){
return null;
}
}
① 实现的静态方法,用 static
声明。② 注意 不能 同时使用 default 和 static 声明。
- 流操作 -Stream
1 理论
1.1 初识 Stream
@Before
public void init() {
random = new Random();
stuList = new ArrayList<Student>() {
{
for (int i = 0; i < 100; i++) {
add(new Student(“student” + i, random.nextInt(50) + 50));
}
}
};
}
public class Student {
private String name;
private Integer score;
//—–getters and setters—–
}
// 1 列出班上超过 85 分的学生姓名,并按照分数降序输出用户名字
@Test
public void test1() {
List<String> studentList = stuList.stream()
.filter(x->x.getScore()>85)
.sorted(Comparator.comparing(Student::getScore).reversed())
.map(Student::getName)
.collect(Collectors.toList());
System.out.println(studentList);
}
列出班上分数超过 85 分的学生姓名,并按照分数降序输出用户名字,在 java8 之前我们需要三个步骤:
1)新建一个 List<Student> newList,在 for 循环中遍历 stuList,将分数超过 85 分的学生装入新的集合中
2)对于新的集合 newList 进行排序操作
3)遍历打印 newList
这三个步骤在 java8 中只需要两条语句,如果紧紧需要打印,不需要保存新生产 list 的话实际上只需要一条,是不是非常方便。
1.2.stream 的特性
我们首先列出 stream 的如下三点特性,在之后我们会对照着详细说明
1.stream不存储数据
2.stream不改变源数据
3.stream的延迟执行特性
通常我们在数组或集合的基础上创建 stream,stream 不会专门存储数据,对 stream 的操作也不会影响到创建它的数组和集合, 对于 stream 的聚合、消费或收集操作只能进行一次,再次操作会报错,如下代码:
@Test
public void test1(){
Stream<String> stream = Stream.generate(()->”user”).limit(20);
stream.forEach(System.out::println);
stream.forEach(System.out::println);
}
程序在正常完成一次打印工作后报错。
stream 的操作是延迟执行的,在列出班上超过 85 分的学生姓名例子中,在 collect 方法执行之前,filter、sorted、map 方法还未执行,只有当 collect 方法执行时才会触发之前转换操作
看如下代码:
public boolean filter(Student s) {
System.out.println(“begin compare”);
return s.getScore() > 85;
}
@Test
public void test() {
Stream<Student> stream = Stream.of(stuArr).filter(this::filter);
System.out.println(“split————————————-“);
List<Student> studentList = stream.collect(toList());
}
我们将 filter 中的逻辑抽象成方法,在方法中加入打印逻辑,如果 stream 的转换操作是延迟执行的,那么 split 会先打印,否则后打印,代码运行结果为
可见 stream 的操作是延迟执行的。
TIP**:**
当我们操作一个流的时候,并不会修改流底层的集合(即使集合是线程安全的),如果想要修改原有的集合,就无法定义流操作的输出。
由于 stream 的延迟执行特性,在聚合操作执行前修改数据源是允许的。
List<String> wordList;
@Before
public void init() {
wordList = new ArrayList<String>() {
{
add(“a”);
add(“b”);
add(“c”);
add(“d”);
add(“e”);
add(“f”);
add(“g”);
}
};
}
/**
* 延迟执行特性,在聚合操作之前都可以添加相应元素
*/
@Test
public void test() {
Stream<String> words = wordList.stream();
wordList.add(“END”);
long n = words.distinct().count();
System.out.println(n);
}
最后打印的结果是 8
如下代码是错误的
/**
* 延迟执行特性,会产生干扰
* nullPointException
*/
@Test
public void test2(){
Stream<String> words1 = wordList.stream();
words1.forEach(s -> {
System.out.println(“s->”+s);
if (s.length() < 4) {
System.out.println(“select->”+s);
wordList.remove(s);
System.out.println(wordList);
}
});
}
结果报空指针异常
2 流的创建
2.1 流的创建方法
既然需要聊聊流的操作,那么,首先还是先看看怎么创建流。
创建流的方法有三种,分别是:Stream.of()、Stream.iterate()、Stream.generate(),然后,分别看一下这三个方法的声明。
static <T> Stream<T> of(T… values)
static <T> Stream<T> iterate(T seed, UnaryOperator<T> f)
static <T> Stream<T> generate(Supplier<T> s)
Stream.of()
:参数很简单,就是一系列的泛型参数。Stream.iterate()
:第一个参数是一个初始值,第二个参数是一个操作。Stream.generate()
:参数就是一个 Supplier 的供给型的参数。
2.2 流的创建方法举例
@Test
public void testCreateStream() {
// 利用 Stream.of 方法创建流
Stream<String> stream = Stream.of(“hello”, “world”, “Java8”);
stream.forEach(System.out::println);
System.out.println(“##################”);
// 利用 Stream.iterate 方法创建流
Stream.iterate(10, n -> n + 1)
.limit(5)
.collect(Collectors.toList())
.forEach(System.out::println);
System.out.println(“##################”);
// 利用 Stream.generate 方法创建流
Stream.generate(Math::random)
.limit(5)
.forEach(System.out::println);
System.out.println(“##################”);
// 从现有的集合中创建流
List<String> strings = Arrays.asList(“hello”, “world”, “Java8”);
String string = strings.stream().collect(Collectors.joining(“,”));
System.out.println(string);
}
在上面的例子中,Stream.of()
方法的参数是几个字符串,Stream.iterate()
方法的第一个参数是初始值 10,第二个参数是在 10 的基础上每次加 1 的操作,Stream.generate()
的参数是用 Random 方法产生随机数。
2.3 流的创建总结
流的创建有三种方法,分别是Stream.of()、Stream.iterate()、Stream.generate(),这几个都是 Stream
类的静态方法,所以,使用起来非常的方便。
3 流的操作
在上一节中,我们知道怎么创建流了,接下来,我们就看看对流可以进行哪些操作,使用了 Stream
流之后,是否会比 Java8
之前方便很多呢?
stream 的操作是延迟执行的,在列出班上超过 85 分的学生姓名例子中,在 collect 方法执行之前,filter、sorted、map 方法还未执行,只有当 collect 方法执行时才会触发之前转换操作
3.1 装箱流
在处理对象流的时候,可以利用 Collectors
类的 静态方法 转换为集合,例如,将字符串流转换为 List<String>
,这种方式是没有问题的。
但是,如果遇到 double 流想要转换为 List 时,这是就会报错。
DoubleStream.of(1.0, 2.0, 3.0)
.collect(Collectors.toList());// 错误的写法
这种方式就是错误的,编译是不能通过的。
别慌,对于这种问题,有 3 种比较好的解决方法。
利用 boxed 方法
利用 boxed
方法,可以将 DoubleStream
转换为 Stream<Double>
,例如;
DoubleStream.of(1.0, 2.0, 3.0)
.boxed()
.collect(Collectors.toList());
这样就解决了上面的问题。
利用 mapToObj 方法
利用 mapToObj
方法也可以实现上面的功能,另外,也提供了 mapToInt、mapToLong、mapToDouble
等方法将基本类型流转换为相关包装类型。
DoubleStream.of(1.0, 2.0, 3.0)
.mapToObj(Double::valueOf)
.collect(Collectors.toList());
collect 方法
一般情况下,我们利用 collect
方法的时候,都是用于将 流的数据收集为基本类型的集合,例如;
stream.collect(Collectors.toList())
然而,collect
方法其实还有一种更加一般化的形式,如下;
<R> R collect(Supplier<R> supplier,
ObjIntConsumer<R> accumulator,
BiCnsumer<R,R> combiner)
上面这种方法的 第一个参数是一个供给器,相当于初始化一个容器,第二个参数是累加器,相当于给初始化的容器赋值,第三个参数是组合器,相当于将这些元素全部组合到一个容器。
下面,我们通过一个简单的例子来看看到底是怎么使用的!
List<Double> list = DoubleStream.of(1.0, 2.0, 3.0)
.collect(ArrayList<Double>::new, ArrayList::add, ArrayList::addAll);
上面的例子我们可以看到,第一个参数:使用一个 静态方法 初始化一个 List
容器,第二个参数:使用静态方法 add
,添加元素,第三个参数:使用静态方法 addAll
,用于联合所有的元素。
从最后的返回值为 List<Double>
,我们也可以看出,全部组合成一个初始化的 List
集合中了。
3.2 字符串与流之间的转换
这一小节主要讲解一下字符串与流之间的转换,将 String 转为流 有两种方法,分别是 java.lang.CharSequence
接口定义的默认方法 chars
和 codePoints
,而将 流转为字符串 就是我们前面已经讲解到的方法 collect
。
@Test
public void testString2Stream() {
String s = “hello world Java8”.codePoints()// 转换成流
.collect(StringBuffer::new,
StringBuffer::appendCodePoint,
StringBuffer::append)// 将流转换为字符串
.toString();
String s1 = “hello world Java8”.chars()// 转换成流
.collect(StringBuffer::new,
StringBuffer::appendCodePoint,
StringBuffer::append)// 将流转换为字符串
.toString();
}
在上面的例子中,先用chars
和 codePoints
方法转换为流,然后都是利用 collect
方法再转回字符串。
3.3 流的映射 map 与 flatMap
流的映射是什么意思呢,我们先将一个在 Java8 之前的例子,我们常常需要将一个集合的对象的某一个字段取出来,然后再存到另外一个集合中,这种场景我们在 Java8 之前我们会这样实现。
@Test
public void testList() {
List<Person> list = new ArrayList<>();
List<Friend> friends = new ArrayList<>();
friends.add(new Friend(“Java5”));
friends.add(new Friend(“Java6”));
friends.add(new Friend(“Java7”));
Person person = new Person();
person.setFriends(friends);
list.add(person);
List<String> strings = new ArrayList<>();
for(Person p : list){
strings.add(p.getName());
}
}
是不是这样很麻烦,这也就是以前大家一直所说的 Python 用一招,Java 需要用花招!
但是,Java8
却改变了这种现实,我们来看一看怎么使用 map
和 flatMap
。
首先,我们先看一下这俩个方法的 声明;
<R> Stream<R> map(Function<? super T,? extends R> mapper)
<R> Stream<R> flatMap(Function<? super T,? extends Stream<? extends R>> mapper)
接下来,我们用这两个方法改写上面的方式,先看看 map
方法;
@Test
public void testMapAndFlatMap() {
List<Person> list = new ArrayList<>();
List<Friend> friends = new ArrayList<>();
friends.add(new Friend(“Java5”));
friends.add(new Friend(“Java6”));
friends.add(new Friend(“Java7”));
Person person = new Person();
person.setFriends(friends);
list.add(person);
// 映射出名字
List<String> strings = list.stream().map(Person::getName).collect(Collectors.toList());
}
通过使用 map
方法,参数给定 Person::getName
映射出 name
,然后再用 collect
收集到 List
中,就完成了上面的负责的操作,是不是很舒服。
但是,如果我们用 map 方法想要映射出 friends
属性,会遇到一个问题;
// 映射出朋友
List<List<Friend>> collect = list.stream().map(Person::getFriends).collect(Collectors.toList());
我们发现,上面的返回值是 List<List<Friend>>
,这种形式集合里面还包着集合,处理有点麻烦,但是,不是还有另外 flatMap
没有使用吗,这个方法正好能够解决这个问题。
List<Friend> collect1 = list.stream().flatMap(friend -> friend.getFriends().stream()).collect(Collectors.toList());
发现,这个方法的返回值是 List<Friend>
,正如我们看到的,flatMap
的方法能够“展平”包裹的流,这就是 map
和 flatMap
的区别。
3.4 流的连接
流的连接有两种方式,如果是 两个流的连接 ,使用 Stream.concat
方法,如果是 三个及三个以上的流的连接,就使用 Stream.flatMap
方法。
@Test
public void testConcatStream() {
// 两个流的连接
Stream<String> first = Stream.of(“sihai”, “sihai2”, “sihai3”);
Stream<String> second = Stream.of(“sihai4”, “sihai5”, “sihai6”);
Stream<String> third = Stream.of(“siha7”, “sihai8”, “sihai9”);
Stream<String> concat = Stream.concat(first, second);
// 多个流的连接
Stream<String> stringStream = Stream.of(first, second, third).flatMap(Function.identity());
}
4 流的规约操作
流的规约操作几种类型,这里都讲一下。
内置的规约操作
基本类型流都有内置的规约操作。包括average、count、max、min、sum、summaryStatistics,前面的几个方法相信不用说了,summaryStatistics
方法是前面的几个方法的结合,下面我们看看他们如何使用。
@Test
public void testReduce1() {
String[] strings = {“hello”, “sihai”, “hello”, “Java8”};
long count = Arrays.stream(strings)
.map(String::length)
.count();
System.out.println(count);
System.out.println(“##################”);
int sum = Arrays.stream(strings)
.mapToInt(String::length)
.sum();
System.out.println(sum);
System.out.println(“##################”);
OptionalDouble average = Arrays.stream(strings)
.mapToInt(String::length)
.average();
System.out.println(average);
System.out.println(“##################”);
OptionalInt max = Arrays.stream(strings)
.mapToInt(String::length)
.max();
System.out.println(max);
System.out.println(“##################”);
OptionalInt min = Arrays.stream(strings)
.mapToInt(String::length)
.min();
System.out.println(min);
DoubleSummaryStatistics statistics = DoubleStream.generate(Math::random)
.limit(1000)
.summaryStatistics();
System.out.println(statistics);
}
就是这么简单!
基本的规约操作
基本的规约操作是利用前面讲过的 reduce
方法实现的,IntStream
接口定义了三种 reduce
方法的重载形式,如下;
OptionalInt reduce(IntBinaryOperator op)
int reduce(int identity, IntBianryOperator op)
<U> U reduce(U identity,
BiFunction<U,? super T,U> accumulator,
BianryOperator<U> combiner)
上面的 identity
参数就是初始化值的意思,IntBianryOperator
类型的参数就是操作,例如 lambda
表达式;BianryOperator<U> combiner
是一个组合器,在前面有讲过。
下面我们通过一个例子来讲解一下。
@Test
public void testReduce2() {
int sum = IntStream.range(1, 20)
.reduce((x, y) -> x + y)
.orElse(0);
System.out.println(sum);
System.out.println(“##################”);
int sum2 = IntStream.range(1, 20)
.reduce(0, (x, y) -> x + 2 * y);
System.out.println(sum2);
System.out.println(“##################”);
int sum3 = IntStream.range(1, 20)
.reduce(0, Integer::sum);
System.out.println(sum3);
}
例子中的第一个是 1 到 20 累加 的操作,第二个以 0 为初始值,然后 2 倍累加,第三个是 以 0 为初始值,累加。
流的计数
流的数量统计有两种方法,分别是 Stream.count()
方法和 Collectors.counting()
方法。
@Test
public void testStatistics() {
// 统计数量
String[] strings = {“hello”, “sihai”, “hello”, “Java8”};
long count = Arrays.stream(strings)
.count();
System.out.println(count);
System.out.println(“##################”);
Long count2 = Arrays.stream(strings)
.collect(Collectors.counting());
System.out.println(count2);
}
5 流的查找与匹配
流的查找
流的查找 Stream 接口提供了两个方法 findFirst
和 findAny
。
findFirst
方法返回流中的 第一个 元素的 Optional
,而 findAny
方法返回流中的 某个 元素的 Optional
。
我们来看一个例子。
String[] strings = {“hello”, “sihai”, “hello”, “Java8”};
Optional<String> first = Arrays.stream(strings)
.findFirst();
System.out.println(first.get());
System.out.println(“##################”);
Optional<String> any = Arrays.stream(strings).findAny();
System.out.println(any.get());
System.out.println(“##################”);
流的匹配
流的匹配 Stream 接口提供了三个方法,分别是 anyMatch
(任何一个元素匹配,返回 true)、allMatch
(所有元素匹配,返回 true)、noneMatch
(没有一个元素匹配,返回 true)。
boolean b = Stream.of(1, 2, 3, 4, 5, 10)
.anyMatch(x -> x > 5);
System.out.println(b);
System.out.println(“##################”);
boolean b2 = Stream.of(1, 2, 3, 4, 5, 10)
.allMatch(x -> x > 5);
System.out.println(b2);
System.out.println(“##################”);
boolean b3 = Stream.of(1, 2, 3, 4, 5, 10)
.noneMatch(x -> x > 5);
System.out.println(b3);
6 并行流和串行流
在 jdk1.8 新的 stream 包中针对集合的操作也提供了并行操作流和串行操作流。并行流就是把内容切割成多个数据块,并且使用多个线程分别处理每个数据块的内容。Stream api 中声明可以通过 parallel()与 sequential()方法在并行流和串行流之间进行切换。jdk1.8 并行流使用的是 fork/join 框架进行并行操作
可以将普通顺序执行的流转变为并行流,只需要调用顺序流的 parallel() 方法即可,如 Stream.iterate(1, x -> x + 1).limit(10).parallel()。
6.1 并行流的执行顺序
我们调用 peek 方法来瞧瞧并行流和串行流的执行顺序,peek 方法顾名思义,就是偷窥流内的数据,peek 方法声明为 Stream<T> peek(Consumer<? super T> action); 加入打印程序可以观察到通过流内数据,见如下代码:
public void peek1(int x) {
System.out.println(Thread.currentThread().getName() + “:->peek1->” + x);
}
public void peek2(int x) {
System.out.println(Thread.currentThread().getName() + “:->peek2->” + x);
}
public void peek3(int x) {
System.out.println(Thread.currentThread().getName() + “:->final result->” + x);
}
/**
* peek,监控方法
* 串行流和并行流的执行顺序
*/
@org.junit.Test
public void testPeek() {
Stream<Integer> stream = Stream.iterate(1, x -> x + 1).limit(10);
stream.peek(this::peek1).filter(x -> x > 5)
.peek(this::peek2).filter(x -> x < 8)
.peek(this::peek3)
.forEach(System.out::println);
}
@Test
public void testPeekPal() {
Stream<Integer> stream = Stream.iterate(1, x -> x + 1).limit(10).parallel();
stream.peek(this::peek1).filter(x -> x > 5)
.peek(this::peek2).filter(x -> x < 8)
.peek(this::peek3)
.forEach(System.out::println);
}
串行流打印结果如下:
并行流打印结果如下:
咋看不一定能看懂,我们用如下的图来解释
我们将 stream.filter(x -> x > 5).filter(x -> x < 8).forEach(System.out::println)的过程想象成上图的管道,我们在管道上加入的 peek 相当于一个阀门,透过这个阀门查看流经的数据,
1)当我们使用顺序流时,数据按照源数据的顺序依次通过管道,当一个数据被 filter 过滤,或者经过整个管道而输出后,第二个数据才会开始重复这一过程
2)当我们使用并行流时,系统除了主线程外启动了七个线程(我的电脑是 4 核八线程)来执行处理任务,因此执行是无序的,但同一个线程内处理的数据是按顺序进行的。
6.2 sorted()、distinct()等对并行流的影响
sorted()、distinct()是元素相关方法,和整体的数据是有关系的,map,filter 等方法和已经通过的元素是不相关的, 不需要知道流里面有哪些元素,并行执行和 sorted 会不会产生冲突呢?
结论:1. 并行流和排序是不冲突的,2. 一个流是否是有序的,对于一些 api 可能会提高执行效率,对于另一些 api 可能会降低执行效率
3. 如果想要输出的结果是有序的,对于并行的流需要使用 forEachOrdered(forEach 的输出效率更高)
我们做如下实验:
/**
* 生成一亿条 0 -100 之间的记录
*/
@Before
public void init() {
Random random = new Random();
list = Stream.generate(() -> random.nextInt(100)).limit(100000000).collect(toList());
}
/**
* tip
*/
@org.junit.Test
public void test1() {
long begin1 = System.currentTimeMillis();
list.stream().filter(x->(x > 10)).filter(x->x<80).count();
long end1 = System.currentTimeMillis();
System.out.println(end1-begin1);
list.stream().parallel().filter(x->(x > 10)).filter(x->x<80).count();
long end2 = System.currentTimeMillis();
System.out.println(end2-end1);
long begin1_ = System.currentTimeMillis();
list.stream().filter(x->(x > 10)).filter(x->x<80).distinct().sorted().count();
long end1_ = System.currentTimeMillis();
System.out.println(end1-begin1);
list.stream().parallel().filter(x->(x > 10)).filter(x->x<80).distinct().sorted().count();
long end2_ = System.currentTimeMillis();
System.out.println(end2_-end1_);
}
可见,对于串行流.distinct().sorted()方法对于运行时间没有影响,但是对于串行流,会使得运行时间大大增加,因此对于包含 sorted、distinct()等与全局数据相关的操作,不推荐使用并行流。
6.3 ForkJoin 框架
Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分 (fork) 成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。关键字:递归分合、分而治之。采用“工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线 程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中 相对于一般的线程池实现,fork/join 框架的优势体现在对其中包含的任务的 处理方式上. 在一般的线程池中, 如果一个线程正在执行的任务由于某些原因 无法继续运行, 那么该线程会处于等待状态. 而在 fork/join 框架实现中, 如果 某个子问题由于等待另外一个子问题的完成而无法继续运行. 那么处理该子 问题的线程会主动寻找其他尚未运行的子问题来执行. 这种方式减少了线程 的等待时间, 提高了性能.。
/**
* 要想使用 Fark—Join,类必须继承
* RecursiveAction(无返回值)
* Or
* RecursiveTask(有返回值)
*
*/
public class ForkJoin extends RecursiveTask<Long> {
/**
* 要想使用 Fark—Join,类必须继承 RecursiveAction(无返回值)或者
* RecursiveTask(有返回值)
*
* @author Wuyouxin
*/
private static final long serialVersionUID = 23423422L;
private long start;
private long end;
public ForkJoin() {
}
public ForkJoin(long start, long end) {
this.start = start;
this.end = end;
}
// 定义阙值
private static final long THRESHOLD = 10000L;
@Override
protected Long compute() {
if (end – start <= THRESHOLD) {
long sum = 0;
for (long i = start; i < end; i++) {
sum += i;
}
return sum;
} else {
long middle = (end – start) / 2;
ForkJoin left = new ForkJoin(start, middle);
// 拆分子任务,压入线程队列
left.fork();
ForkJoin right = new ForkJoin(middle + 1, end);
right.fork();
// 合并并返回
return left.join() + right.join();
}
}
/**
* 实现数的累加
*/
@Test
public void test1() {
// 开始时间
Instant start = Instant.now();
// 这里需要一个线程池的支持
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoin(0L, 10000000000L);
// 没有返回值 pool.execute();
// 有返回值
long sum = pool.invoke(task);
// 结束时间
Instant end = Instant.now();
System.out.println(Duration.between(start, end).getSeconds());
}
/**
* java8 并行流 parallel()
*/
@Test
public void test2() {
// 开始时间
Instant start = Instant.now();
// 并行流计算 累加求和
LongStream.rangeClosed(0, 10000000000L).parallel()
.reduce(0, Long :: sum);
// 结束时间
Instant end = Instant.now();
System.out.println(Duration.between(start, end).getSeconds());
}
@Test
public void test3(){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream().forEach(System.out::print);
list.parallelStream()
.forEach(System.out::print);
}
展示多线程的效果:
@Test
public void test(){
// 并行流 多个线程执行
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
.forEach(System.out::print);
//
System.out.println(“=========================”);
numbers.stream()
.sequential()
.forEach(System.out::print);
}
7 stream vs spark rdd
最初看到 stream 的一个直观感受是和 spark 像,真的像
val count = sc.parallelize(1 to NUM_SAMPLES).filter {_ =>
val x = math.random
val y = math.random
x*x + y*y < 1}.count()println(s”Pi is roughly ${4.0 * count / NUM_SAMPLES}”)
以上代码摘自 spark 官网,使用的是 scala 语言,一个最基础的 word count 代码,这里我们简单介绍一下 spark,spark 是当今最流行的基于内存的大数据处理框架,spark 中的一个核心概念是 RDD(弹性分布式数据集),将分布于不同处理器上的数据抽象成 rdd,rdd 上支持两种类型的操作 1)Transformation(变换)2)Action(行动),对于 rdd 的 Transformation 算子并不会立即执行,只有当使用了 Action 算子后,才会触发。
ForkJoin 框架 Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分 (fork) 成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。关键字:递归分合、分而治之。采用“工作窃取”模式(work-stealing):当执行新的任务时它可以将其拆分分成更小的任务执行,并将小任务加到线 程队列中,然后再从一个随机线程的队列中偷一个并把它放在自己的队列中 相对于一般的线程池实现,fork/join 框架的优势体现在对其中包含的任务的 处理方式上. 在一般的线程池中, 如果一个线程正在执行的任务由于某些原因 无法继续运行, 那么该线程会处于等待状态. 而在 fork/join 框架实现中, 如果 某个子问题由于等待另外一个子问题的完成而无法继续运行. 那么处理该子 问题的线程会主动寻找其他尚未运行的子问题来执行. 这种方式减少了线程 的等待时间, 提高了性能.。
/**
* 要想使用 Fark—Join,类必须继承
* RecursiveAction(无返回值)
* Or
* RecursiveTask(有返回值)
*
*/
public class ForkJoin extends RecursiveTask<Long> {
/**
* 要想使用 Fark—Join,类必须继承 RecursiveAction(无返回值)或者
* RecursiveTask(有返回值)
*
* @author Wuyouxin
*/
private static final long serialVersionUID = 23423422L;
private long start;
private long end;
public ForkJoin() {
}
public ForkJoin(long start, long end) {
this.start = start;
this.end = end;
}
// 定义阙值
private static final long THRESHOLD = 10000L;
@Override
protected Long compute() {
if (end – start <= THRESHOLD) {
long sum = 0;
for (long i = start; i < end; i++) {
sum += i;
}
return sum;
} else {
long middle = (end – start) / 2;
ForkJoin left = new ForkJoin(start, middle);
// 拆分子任务,压入线程队列
left.fork();
ForkJoin right = new ForkJoin(middle + 1, end);
right.fork();
// 合并并返回
return left.join() + right.join();
}
}
/**
* 实现数的累加
*/
@Test
public void test1() {
// 开始时间
Instant start = Instant.now();
// 这里需要一个线程池的支持
ForkJoinPool pool = new ForkJoinPool();
ForkJoinTask<Long> task = new ForkJoin(0L, 10000000000L);
// 没有返回值 pool.execute();
// 有返回值
long sum = pool.invoke(task);
// 结束时间
Instant end = Instant.now();
System.out.println(Duration.between(start, end).getSeconds());
}
/**
* java8 并行流 parallel()
*/
@Test
public void test2() {
// 开始时间
Instant start = Instant.now();
// 并行流计算 累加求和
LongStream.rangeClosed(0, 10000000000L).parallel()
.reduce(0, Long :: sum);
// 结束时间
Instant end = Instant.now();
System.out.println(Duration.between(start, end).getSeconds());
}
@Test
public void test3(){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream().forEach(System.out::print);
list.parallelStream()
.forEach(System.out::print);
}
展示多线程的效果:
@Test
public void test(){
// 并行流 多个线程执行
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
numbers.parallelStream()
.forEach(System.out::print);
//
System.out.println(“=========================”);
numbers.stream()
.sequential()
.forEach(System.out::print);
}
8 流的总结
这篇文章主要讲解了流的一些操作,包括下面几个方面。
- 流的创建方法。
- 流的系列操作,包括装箱流、字符串与流之间的转换、流和映射 map 和 flatMap、流的连接。
- 流的规约操作
- 流的查找与匹配
6. Date API
LocalDate | LocalTime | LocalDateTime
6.1 优点
- 之前使用的 java.util.Date 月份从 0 开始,我们一般会 + 1 使用,很不方便,java.time.LocalDate 月份和星期都改成了 enum
- java.util.Date 和 SimpleDateFormat 都不是线程安全的,而 LocalDate 和 LocalTime 和最基本的 String 一样,是不变类型,不但线程安全,而且不能修改。
- java.util.Date 是一个“万能接口”,它包含日期、时间,还有毫秒数,更加明确需求取舍
- 新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用 java.util.Date 配合 Calendar 要写好多代码,而且一般的开发人员还不一定能写对
6.2 示例
6.2.1 LocalDate
public static void localDateTest() {
// 获取当前日期, 只含年月日 固定格式 yyyy-MM-dd 2018-05-04
LocalDate today = LocalDate.now();
// 根据年月日取日期,5 月就是 5,
LocalDate oldDate = LocalDate.of(2018, 5, 1);
// 根据字符串取:默认格式 yyyy-MM-dd,02 不能写成 2
LocalDate yesteday = LocalDate.parse(“2018-05-03”);
// 如果不是闰年 传入 29 号也会报错
LocalDate.parse(“2018-02-29”);
}
6.2.2 LocalDate 常用转化
/**
* 日期转换常用, 第一天或者最后一天 …
*/
public static void localDateTransferTest(){
//2018-05-04
LocalDate today = LocalDate.now();
// 取本月第 1 天:2018-05-01
LocalDate firstDayOfThisMonth = today.with(TemporalAdjusters.firstDayOfMonth());
// 取本月第 2 天:2018-05-02
LocalDate secondDayOfThisMonth = today.withDayOfMonth(2);
// 取本月最后一天,再也不用计算是 28,29,30 还是 31:2018-05-31
LocalDate lastDayOfThisMonth = today.with(TemporalAdjusters.lastDayOfMonth());
// 取下一天:2018-06-01
LocalDate firstDayOf2015 = lastDayOfThisMonth.plusDays(1);
// 取 2018 年 10 月第一个周三 so easy?:2018-10-03
LocalDate thirdMondayOf2018 = LocalDate.parse(“2018-10-01”).with(TemporalAdjusters.firstInMonth(DayOfWeek.WEDNESDAY));
}
6.2.3 LocalTime
public static void localTimeTest(){
//16:25:46.448(纳秒值)
LocalTime todayTimeWithMillisTime = LocalTime.now();
//16:28:48 不带纳秒值
LocalTime todayTimeWithNoMillisTime = LocalTime.now().withNano(0);
LocalTime time1 = LocalTime.parse(“23:59:59”);
}
6.2.4 LocalDateTime
public static void localDateTimeTest(){
// 转化为时间戳 毫秒值
long time1 = LocalDateTime.now().toInstant(ZoneOffset.of(“+8”)).toEpochMilli();
long time2 = System.currentTimeMillis();
// 时间戳转化为 localdatetime
DateTimeFormatter df= DateTimeFormatter.ofPattern(“YYYY-MM-dd HH:mm:ss.SSS”);
System.out.println(df.format(LocalDateTime.ofInstant(Instant.ofEpochMilli(time1),ZoneId.of(“Asia/Shanghai”))));
}
Java 8 在包 java.time 下包含了一组全新的时间日期 API。新的日期 API 和开源的 Joda-Time 库差不多,但又不完全一样,下面的例子展示了这组新 API 里最重要的一些部分:
6.2.5 Clock 时钟
Clock 类提供了访问当前日期和时间的方法,Clock 是时区敏感的,可以用来取代 System.currentTimeMillis() 来获取当前的微秒数。某一个特定的时间点也可以使用 Instant 类来表示,Instant 类也可以用来创建老的 java.util.Date 对象。
Clock clock = Clock.systemDefaultZone();
long millis = clock.millis();
Instant instant = clock.instant();
Date legacyDate = Date.from(instant); // legacy java.util.Date
6.2.6 Timezones 时区
在新 API 中时区使用 ZoneId 来表示。时区可以很方便的使用静态方法 of 来获取到。时区定义了到 UTS 时间的时间差,在 Instant 时间点对象到本地日期对象之间转换的时候是极其重要的。
System.out.println(ZoneId.getAvailableZoneIds());
// prints all available timezone ids
ZoneId zone1 = ZoneId.of(“Europe/Berlin”);
ZoneId zone2 = ZoneId.of(“Brazil/East”);
System.out.println(zone1.getRules());
System.out.println(zone2.getRules());
// ZoneRules[currentStandardOffset=+01:00]
// ZoneRules[currentStandardOffset=-03:00]
LocalTime 本地时间
LocalTime 定义了一个没有时区信息的时间,例如 晚上 10 点,或者 17:30:15。下面的例子使用前面代码创建的时区创建了两个本地时间。之后比较时间并以小时和分钟为单位计算两个时间的时间差:
LocalTime now1 = LocalTime.now(zone1);
LocalTime now2 = LocalTime.now(zone2);
System.out.println(now1.isBefore(now2)); // false
long hoursBetween = ChronoUnit.HOURS.between(now1, now2);
long minutesBetween = ChronoUnit.MINUTES.between(now1, now2);
System.out.println(hoursBetween); // -3
System.out.println(minutesBetween); // -239
LocalTime 提供了多种工厂方法来简化对象的创建,包括解析时间字符串。
LocalTime late = LocalTime.of(23, 59, 59);
System.out.println(late); // 23:59:59
DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedTime(FormatStyle.SHORT)
.withLocale(Locale.GERMAN);
LocalTime leetTime = LocalTime.parse(“13:37”, germanFormatter);
System.out.println(leetTime); // 13:37
LocalDate 本地日期
LocalDate 表示了一个确切的日期,比如 2014-03-11。该对象值是不可变的,用起来和 LocalTime 基本一致。下面的例子展示了如何给 Date 对象加减天 / 月 / 年。另外要注意的是这些对象是不可变的,操作返回的总是一个新实例。
LocalDate today = LocalDate.now();
LocalDate tomorrow = today.plus(1, ChronoUnit.DAYS);
LocalDate yesterday = tomorrow.minusDays(2);
LocalDate independenceDay = LocalDate.of(2014, Month.JULY, 4);
DayOfWeek dayOfWeek = independenceDay.getDayOfWeek();
System.out.println(dayOfWeek); // FRIDAY 从字符串解析一个 LocalDate 类型和解析 LocalTime 一样简单:
DateTimeFormatter germanFormatter =
DateTimeFormatter
.ofLocalizedDate(FormatStyle.MEDIUM)
.withLocale(Locale.GERMAN);
LocalDate xmas = LocalDate.parse(“24.12.2014”, germanFormatter);
System.out.println(xmas); // 2014-12-24
LocalDateTime 本地日期时间
LocalDateTime 同时表示了时间和日期,相当于前两节内容合并到一个对象上了。LocalDateTime 和 LocalTime 还有 LocalDate 一样,都是不可变的。LocalDateTime 提供了一些能访问具体字段的方法。
LocalDateTime sylvester = LocalDateTime.of(2014, Month.DECEMBER, 31, 23, 59, 59);
DayOfWeek dayOfWeek = sylvester.getDayOfWeek();
System.out.println(dayOfWeek); // WEDNESDAY
Month month = sylvester.getMonth();
System.out.println(month); // DECEMBER
long minuteOfDay = sylvester.getLong(ChronoField.MINUTE_OF_DAY);
System.out.println(minuteOfDay); // 1439
只要附加上时区信息,就可以将其转换为一个时间点 Instant 对象,Instant 时间点对象可以很容易的转换为老式的 java.util.Date。
Instant instant = sylvester
.atZone(ZoneId.systemDefault())
.toInstant();
Date legacyDate = Date.from(instant);
System.out.println(legacyDate); // Wed Dec 31 23:59:59 CET 2014
格式化 LocalDateTime 和格式化时间和日期一样的,除了使用预定义好的格式外,我们也可以自己定义格式:
DateTimeFormatter formatter =
DateTimeFormatter
.ofPattern(“MMM dd, yyyy – HH:mm”);
LocalDateTime parsed = LocalDateTime.parse(“Nov 03, 2014 – 07:13”, formatter);
String string = formatter.format(parsed);
System.out.println(string); // Nov 03, 2014 – 07:13
和 java.text.NumberFormat 不一样的是新版的 DateTimeFormatter 是不可变的,所以它是线程安全的。关于时间日期格式的详细信息:http://download.java.net/jdk8/docs/api/java/time/format/DateTimeFormatter.html
7 Optional
在我们的开发中,NullPointerException 可谓是随时随处可见,为了避免空指针异常,我们常常需要进行一些防御式的检查,所以在代码中常常可见 if(obj != null) 这样的判断。幸好在 JDK1.8 中,java 为我们提供了一个 Optional 类,Optional 类能让我们省掉繁琐的非空的判断。下面先说一下 Optional 中为我们提供的方法
下面我们写几个例子来具体看一下每个方法的作用:
7.1 of
// 创建一个值为张三的 String 类型的 Optional
Optional<String> ofOptional = Optional.of(“ 张三 ”);
// 如果我们用 of 方法创建 Optional 对象时,所传入的值为 null,则抛出 NullPointerException 如下图所示
Optional<String> nullOptional = Optional.of(null);
7.2 ofNullable
// 为指定的值创建 Optional 对象,不管所传入的值为 null 不为 null,创建的时候都不会报错
Optional<String> nullOptional = Optional.ofNullable(null);
Optional<String> nullOptional = Optional.ofNullable(“lisi”);
7.3 empty
// 创建一个空的 String 类型的 Optional 对象
Optional<String> emptyOptional = Optional.empty();
7.4 get
如果我们创建的 Optional 对象中有值存在则返回此值,如果没有值存在,则会抛出
NoSuchElementException 异常。小 demo 如下:
Optional<String> stringOptional = Optional.of(“ 张三 ”);
System.out.println(stringOptional.get());
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.get());
7.5 orElse
如果创建的 Optional 中有值存在,则返回此值,否则返回一个默认值
Optional<String> stringOptional = Optional.of(“ 张三 ”);
System.out.println(stringOptional.orElse(“zhangsan”));
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElse(“ 李四 ”));
7.6 orElseGet
如果创建的 Optional 中有值存在,则返回此值,否则返回一个由 Supplier 接口生成的值
Optional<String> stringOptional = Optional.of(“ 张三 ”);
System.out.println(stringOptional.orElseGet(() -> “zhangsan”));
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElseGet(() -> “orElseGet”));
7.7 orElseThrow
Optional<String> stringOptional = Optional.of(“ 张三 ”);
System.out.println(stringOptional.orElseThrow(CustomException::new));
Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.orElseThrow(CustomException::new));
private static class CustomException extends RuntimeException {
private static final long serialVersionUID = -4399699891687593264L;
public CustomException() {
super(“ 自定义异常 ”);
}
public CustomException(String message) {
super(message);
}
}
如果创建的 Optional 中有值存在,则返回此值,否则抛出一个由指定的 Supplier 接口生成的异常
7.8 filter
如果创建的 Optional 中的值满足 filter 中的条件,则返回包含该值的 Optional 对象,否则返回一个空的 Optional 对象
Optional<String> stringOptional = Optional.of(“zhangsan”);
System.out.println(stringOptional.filter(e -> e.length() > 5).orElse(“ 王五 ”));
stringOptional = Optional.empty();
System.out.println(stringOptional.filter(e -> e.length() > 5).orElse(“lisi”));
注意 Optional 中的 filter 方法和 Stream 中的 filter 方法是有点不一样的,Stream 中的 filter 方法是对一堆元素进
行过滤,而 Optional 中的 filter 方法只是对一个元素进行过滤,可以把 Optional 看成是最多只包含一个元素
的 Stream。
7.9 map
如果创建的 Optional 中的值存在,对该值执行提供的 Function 函数调用
//map 方法执行传入的 lambda 表达式参数对 Optional 实例的值进行修改, 修改后的返回值仍然是一个 Optional 对象
Optional<String> stringOptional = Optional.of(“zhangsan”);
System.out.println(stringOptional.map(e -> e.toUpperCase()).orElse(“ 失败 ”));
stringOptional = Optional.empty();
System.out.println(stringOptional.map(e -> e.toUpperCase()).orElse(“ 失败 ”));
7.10 flagMap
如果创建的 Optional 中的值存在,就对该值执行提供的 Function 函数调用,返回一个 Optional 类型的值,否则就返回一个空的 Optional 对象.flatMap 与 map(Funtion)方法类似,区别在于 flatMap 中的 mapper 返回 值必须是 Optional,map 方法的 mapping 函数返回值可以是任何类型 T。调用结束时,flatMap 不会对结果用 Optional 封装。
//map 方法中的 lambda 表达式返回值可以是任意类型,在 map 函数返回之前会包装为 Optional。
// 但 flatMap 方法中的 lambda 表达式返回值必须是 Optionl 实例
Optional<String> stringOptional = Optional.of(“zhangsan”);
System.out.println(stringOptional.flatMap(e -> Optional.of(“lisi”)).orElse(“ 失败 ”));
stringOptional = Optional.empty();
System.out.println(stringOptional.flatMap(e -> Optional.empty()).orElse(“ 失败 ”));
7.11 ifPresent
//ifPresent 方法的参数是一个 Consumer 的实现类,Consumer 类包含一个抽象方法,该抽象方法对传入的值进行处理,只处理没有返回值。
Optional<String> stringOptional = Optional.of(“zhangsan”);
stringOptional.ifPresent(e-> System.out.println(“ 我被处理了。。。”+e));
7.12 实际线上使用示例
String testDetail = Optional._ofNullable_(studentTask).map(StudentTask::getTestDetail).orElse(“”); if (StringUtils._isBlank_(testDetail)) {return; }
List<StudentTaskResultVo> studentTaskResultVos = JsonUtils._fromJson_(testDetail, new TypeToken<List<StudentTaskResultVo>>() {}.getType()); Map<Long, StudentTaskResultVo> mapResults = new HashMap<>(); studentTaskResultVos.forEach(s -> mapResults.put(s.getTaskQuestionId(), s)); taskQuestions.forEach(t -> { t.setStudentAnswer(Optional._ofNullable_(mapResults.get(t.getId())).map(StudentTaskResultVo::getStudentAnswer).orElse(null)); t.setDoQuestionResult(Optional._of_(mapResults.get(t.getId())).map(StudentTaskResultVo::getDoQuestionResult).orElse(null)); });
Optional._ofNullable_(initRedisQueueListeners()).orElse(Collections._emptyList_()).forEach(l->executor.execute(l));
Optional._ofNullable_(task).orElseThrow(() -> new BaseException(“task not exists”));
Integer code = Optional._ofNullable_(jsonObject).map((a) -> a.get(“code”)).map((c) -> (Integer) c).orElse(0); String msg = Optional._ofNullable_(jsonObject).map((a) -> a.get(“msg”)).map(m -> (String) m).orElse(“”); String data = Optional._ofNullable_(jsonObject).map((a) -> a.get(“data”)).map(b -> jsonObject.getString(“data”)).orElse(“”);
Optional._ofNullable_(classIds).filter(arr -> arr.length >= 1).orElseThrow(() -> new BaseException(“*_至少选择 1 个班级 ”**)); Optional._ofNullable_(classIds).filter(arr -> arr.length <= 5).orElseThrow(() -> new BaseException(“** 最多同时布置 5 个班级 ”**)); Optional._ofNullable*(gradeId).filter(arr -> arr >= 1 && arr <= 6).orElseThrow(() -> new BaseException(“** 该年级暂无练习 ”**));
List<ClassInfo> classInfos = classInfoService.getByIds(Arrays._asList_(classIds)); Optional._ofNullable_(classInfos).filter(arr -> CollectionUtils._isNotEmpty_(arr)).orElseThrow(() -> new BaseException(“** 暂无班级管理权限 ”**));
JSONObject jsonObject = restTemplate.postForObject(url, new HttpEntity<>(params, headers), JSONObject.class); String code = Optional._ofNullable_(jsonObject).map((a) -> a.getString(“code”)).orElseThrow(RuntimeException::new); if (!code.equals(“1”)) throw new RuntimeException(jsonObject.toJSONString()); String data = Optional._ofNullable_(jsonObject).map((a) -> a.getString(“data”)).orElseThrow(RuntimeException::new);