java8
一、lambda
① 什么是lambda ?
Lambda表达式可以理解为是一段可以传递的代码(将代码像参数一样进行传递,称为行为参数化)。Lambda允许把函数作为一个方法的参数(函数作为参数传递进方法中)如下所示:
Comparator<Integer> comparable = (x,y) ->Integer.compare(x,y)
② lamda基础语法
在Java8中引入了一个新的操作符"->"该操作符成为lambda操作符,箭头将lambda表达式分为2部分:
(T … object) ->{ doSomething } [ () 里的为参数列表,每个参数以","分割,**{}**中的则为所需要执行功能 ,T表示类型]
**I.**左侧为lambda 表达式的参数列表
**II.**右侧为lambda 表达式所需要执行的功能,即 lambda体
什么东西可以省略不写?
I.Lambda表达式的参数列表的数据类型可以省略不写,因为在JVM编译器可以通过上下文推断出数据类型,即"类型推断"
Comparator<Integer> comparable = (Integer x, Integer y) -> {
System.out.println("x:" + x + ",y:" + y);
return Integer.compare(x, y);
};
//省略后
Comparator<Integer> comparable = ( x, y) -> {
System.out.println("x:" + x + ",y:" + y);
return Integer.compare(x, y);
};
**II.**参数只有一个时,可以省略(),lambda体只有一行时,可以省略{}
str -> System.out.println(str)
a.无参数无返回值
Runnable r1 = () ->{ System.out.println("这是一个无参无返回值的lambda表达式")};
b.有参无返回值
Consumer<String> consumer =(str) -> {System.out.println(str)};
c.无参有返回值
Supplier supplier=()-> {
String str = String.valueOf("这是一个无参,但有返回值的方法");
return str;
};
d.有参有返回值
Comparator<Integer> comparable = (x, y) -> {
System.out.println("x:" + x + ",y:" + y);
return Integer.compare(x, y);
};
e.参数只有一个,lambda体只有一行
x->System.out.println(x)
③ 为啥要用lambda?
举个例子,假设现在有一批学生,需要按照如下进行需求,进行过滤掉部分不满足条件的学生:
-
1.获取StuList中性别为女的学生
-
2.将StuList中年龄大于18的获取出来
后面要用到的student实体类
package com.example.java8.model; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import lombok.ToString; import lombok.experimental.Accessors; /** * 学生实体对象 * @author Y */ @Data @Accessors(chain=true) @AllArgsConstructor @NoArgsConstructor @ToString public class Student { /** * 姓名 */ private String name; /** * 性别 */ private String sex; /** * 年龄 */ private Integer age; /** * 比较年龄大小 * @param student1 学生1 * @param student2 学生2 * @return 学生1的年龄大于学生2的年龄则返回1,反之返回-1 相等返回0 */ public static int compareAsc(Student student1, Student student2) { return student1.getAge().compareTo(student2.getAge()); } /** * 比较年龄大小 * @param student1 学生1 * @param student2 学生2 * @return 学生12的年龄大于学生1的年龄则返回1,反之返回-1 相等返回0 */ public static int compareDesc(Student student1, Student student2) { return student2.getAge().compareTo(student1.getAge()); } }
先初始化一个学生List,用于后面要讲解的3种不同方式提供数据:
private static final String SEX_WOMEN = "女"; public static final Integer AGE_18 = 18; //初始化 private static List<Student> studentList = Arrays.asList( new Student("张三", "男", 16), new Student("李四", "男", 15), new Student("王五", "男", 17), new Student("张二麻子", "男", 18), new Student("大乔", "女", 18), new Student("小乔", "女", 17) );
基于传统的java代码编写(不含有设计模式、java8等等),方式一:
按照需求,分别写2个方法:按照性别过滤、按照年龄过滤
/** * 按照性别为女的过滤 与sex相同保留,反之丢弃 * @param studentList 要过滤的学生 * @param sex 性别 * @return 过滤完后的学生实体 */ private static List<Student> filterBySex(List<Student> studentList,String sex) { List<Student> resultList = new ArrayList<>(studentList.size()); for (Student student : studentList) { if (sex.equals(student.getSex())) { resultList.add(student); } } return resultList; } /** * 按照年龄过滤学生 --与age相等则保留 反之丢弃 * @param studentList 要过滤的学生实体 * @param age 过滤年龄 * @return 已经过滤后的学生实体 */ private static List<Student> filterByAge(List<Student> studentList,Integer age) { List<Student> resultList = new ArrayList<>(studentList.size()); for (Student student : studentList) { if (age.equals(student.getAge())) { resultList.add(student); } } return resultList; } //方式一: 按照需求定制化不同的方法 @Test private void test01(){ //打印性别为女的 System.out.println("方式一调用:"); for (Student filterBySex : filterBySex(studentList,SEX_WOMEN)) { System.out.println("性别为女的:" + filterBySex); } //打印年龄为18的 for (Student filterByAge : filterByAge(studentList,AGE_18)) { System.out.println("龄为18的:" + filterByAge); } }
基于匿名内部类进行编写:
新建一个用于条件判断的接口IMypredicate:
public interface IMypredicate {
/**
* 用于用户自定义过滤条件
*
* @param student 需要进行断言的学生
* @return 断言的结果
*/
boolean filter(Student student);
}
然后写一个filter方法,用于按用于自定义的predicate进行过滤studentList:
/**
* 自定义学生条件过滤方法
* @param studentList 要过滤的学生实体
* @param predicate 过滤条件
* @return 过滤完的学生
*/
private static List<Student> filter(List<Student> studentList, IMypredicate predicate) {
List<Student> resultList = new ArrayList<>(studentList.size());
for (Student student : studentList) {
if (predicate.filter(student)) {
resultList.add(student);
}
}
return resultList;
}
//方式二:
//提一个公有接口IMypredicate出来,定义一个方法filter,后面要按照某种条件直接实现内部类即可
@Test
private void test02(){
System.out.println("方式二调用:");
List<Student> filterBySex = filter(studentList, new IMypredicate() {
@Override
public boolean filter(Student student) {
return SEX_WOMEN.equals(student.getSex());
}
});
for (Student bySex : filterBySex) {
System.out.println("性别为女的:" + bySex);
}
//获取年龄为18的
List<Student> filterByAge = filter(studentList, new IMypredicate() {
@Override
public boolean filter(Student student) {
return AGE_18.equals(student.getAge());
}
});
for (Student byByAge : filterByAge) {
System.out.println("年龄为18的:" + byByAge);
}
}
基于lambda表达式:
//方式三:
//lambda表达式写法
@Test
private void test03() {
//方式三(lambda):
//过滤性别
filter(studentList, student -> SEX_WOMEN.equals(student.getSex()));
//获取年龄大于15
filter(studentList, student -> student.getAge() > 15);
//获取年龄大于15 且为女的学生
filter(studentList, student -> student.getAge() > 15 && student.getSex().equals(SEX_WOMEN));
}
二、函数式接口
① 函数式接口是什么?
a.只包含一个抽象方法的接口,称为函数式接口。
**b.**你可以通过Lambda表达式来创建该接口的对象。(若Lambda表达式抛出一个受检异常,那么该异常需要在目标接口的抽象方法上进行声明)。
c.我们可以在任意函数式接口上使用@FunctionalInterface注解,这样做可以检查它是否是一个函数式接口,同时javadoc也会包含一条声明,说明这个接口是一个函数式接口。
@FunctionalInterface
public interface MyNumber{
public double getValue();
}
② 四大核心函数式接口
Java内置四大核心函数式接口:
函数式接口 | 参数类型 | 返回类型 | 用途 |
---|---|---|---|
Consumer 消费型接口 | T | void | void accept(T t),接收一个参数进行消费,但无需返回结果 |
Supplier 提供型接口 | 无 | T | T get(),无参数,有返回值。 |
Function<T, R> 函数型接口 | T | R | 对类型为T的对象应用操作,并返回结果为R类型的对象:R apply(T t) |
Predicate 断言型接口 | T | boolean | 确定类型为T的对象是否满足某约束,并返回boolean值:boolean test(T t) |
Consumer 消费型接口
方法 | 备注 |
---|---|
void accept(T t) | 接受一个入参,并且无返回值 |
void accept(T t)
@Test
public void consumerTest(){
handle("2021",(x-> System.out.println(String.format("今年是%s年",x))) );
}
public <T> void handle(T value, Consumer<T> consumer) {
consumer.accept(value);
}
//输出结果 今年是2021年
Supplier供给型接口
方法 | 备注 |
---|---|
T get() | 无入参,但提供一个返回值 |
@Test
public void supplierTest(){
//调用方法 生成10个随机数
List<Integer> lists = handle(10, () -> (int) (Math.random()*100));
System.out.println(lists);
}
//返回一个长度为num 的List集合
public List<Integer> handle(int num,Supplier<Integer> supplier) {
List<Integer>lists=new ArrayList<>();
for (int i = 0; i < num; i++) {
lists.add(supplier.get());
}
return lists;
}
//输出[55, 33, 12, 79, 93, 46, 51, 44, 69, 0]
Function<T,R> 函数型接口
方法 | 备注 |
---|---|
R apply(T t) | 将参数T应用到函数中,返回一个R |
@Test
public void testFunction(){
//入参为String类型 返回参数为String类型
String s1 = handleFunction("\t\t\t张三、李四 ", (x) -> x.trim());
System.out.println(s1);//张三、李四
//入参为int类型 返回参数为String类型
String s = handleFunction(123456, x -> String.format("数字转为字符串:%d",x));
System.out.println(s); //数字转为字符串:123456
}
private <T,R> R handleFunction(T value, Function<T,R> function){
return function.apply(value);
}
Predicate断言型接口
方法 | 备注 |
---|---|
boolean test(T t) | 接受一个输入参数T,返回一个布尔值结果 |
@Test
public void testPredicate(){
//筛选年龄大于30的人
List<Student> valid = valid(studentList, stu -> stu.getAge() < 16);
valid.forEach(person -> System
.out
.println("姓名:"+person.getName()+
",年龄:"+person.getAge()));
}
//校验方法
private List<Student> valid(List<Student> studentList, Predicate<Student> predicate){
List<Student> stuList=new ArrayList<>();
for (Student student : studentList) {
if (predicate.test(student)){
stuList.add(student);
}
}
return stuList;
}
三、方法引用
当要传递给Lambda体的操作,已经有实现的方法了,可以使用方法引用!(实现抽象方法的参数列表,必须与方法引用放方法的参数列表保持一致)
方法引用: 使用操作符"::"将方法名和对象或类的名字分割开来.
主要有如下四种格式:
类型 | 语法 | Lambda表达式 |
---|---|---|
静态方法引用 | 类名::静态方法名 | (args) -> 类名.静态方法名(args) [注这个args可以为多个,在这只是泛指] |
实例方法引用 | 实例::实例方法名 | (args) -> 实例.实例方法名(args) |
对象方法引用 | 类名::对象方法名 | (args) -> 类名.对象方法名(args) |
构建方法引用 | 类名::new | (args) -> new 类名(args) |
① 对象::实例方法名
package com.example.java8;
import com.example.java8.model.Student;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import java.util.Arrays;
import java.util.List;
import java.util.function.Supplier;
@SpringBootTest
class FunctionTests {
@Test
public void testMethodReferences(){
Student student = studentList.get(0);
//无参时
//lambda写法
Supplier<String> supplierLambda=()->student.getName();
//方法引用写法
Supplier<String> supplier= student::getName;
System.out.println("student name:"+supplierLambda.get());
System.out.println("student name:"+supplier.get());
System.out.println("__________________________________________");
//2个参数时
//按照年龄从大到小的排序
//lambda格式:
studentList.sort((student1,student2)->student.compareDesc(student1,student2));
//方法引用格式:
studentList.sort(student::compareDesc);
for (Student stu : studentList) {
System.out.println(stu);
}
}
}
② 类::静态方法名
@Test
public void test02(){
//按照年龄从小往大的排序
//lambda格式:
studentList.sort((student1,student2)->Student.compareAsc(student1,student2));
//方法引用格式:
studentList.sort(Student::compareAsc);
for (Student stu : studentList) {
System.out.println(stu);
}
}
③ 类::实例方法名
这里备注一下,类无法::普通方法
@Test
public void test03() {
//得到学生名字并输出
//lambda:
studentList
.stream()
.map(student -> student.getName())
.forEach(name -> System.out.println(name));
//方法引用:
studentList
.stream()
.map(Student::getName)
.forEach(System.out::println);
}
**④ **类::new
@Test
public void test04() {
//如果studentList为null 则new一个List
//lambda:
List<Student> studentList = Optional.ofNullable(FunctionTests.studentList).orElseGet(() -> new ArrayList<>());
//方法引用:
studentList = Optional.ofNullable(FunctionTests.studentList).orElseGet(ArrayList::new);
}
四、 Stream流
① 什么是Stream(流)?
可以理解为是一个数据管道,用于操作数据所生成的元素序列
a. Stream自己不会存储元素(流是基于数据源的对象,它本身不存储数据元素,而是通过管道将数据源的元素传递给操作。)
b. Stream不会改变源对象。相反,他们会返回一个持有结果的Stream。
c. Stream操作是延迟执行的。这意味着他们会等到需要结果的时候才执行
② 流的三大操作
1.创建Stream流
2.通过Stream流对象执行中间操作
③.执行最终操作,得到结果
可以用生产玩具车的流水线举例:将原材料放到流水线上(创建流),流水线上的工人对这个原材料不断加工,如拼装轮子然后将这个玩具,传递给下一个流水线的工人进行贴图纸,然后再传递给下一个流水线的工人做其他操作(流的一系列中间操作,返回结果仍然是流),然后流水线到头,将玩具车装起来(流水线到头了,也就是我们说的终止操作,返回是一个不是流的实际的对象 或者void)
a.创建Stream
最常用的直接是集合创建流
/**
* 创建流
*/
@Test
public void testCreateStream() {
/*
集合流
- Collection.stream() 穿行流
- Collection.parallelStream() 并行流
*/
Stream<Student> stream = studentList.stream();
Stream<Student> parallelStream = studentList.parallelStream();
// 从数组创建
IntStream stream3 = Arrays.stream(new int[]{2, 3, 5});
//Stream 静态方法
//Stream.of(...)
Stream<Integer> stream4 = Stream.of(1, 2, 3);
}
b.中间操作
中间操作可以理解为对Stream流一系列操作,比如过滤、映射等,中间操作返回的结果都是Stream流,因此我们可以链式调用,但如果不执行终止操作,中间操作是不会执行任何操作的,可以理解为惰性求值:
下面的代码是用于检验流的懒加载,如果将**nameStream.collect(Collectors.toList())**这一行代码注释掉,你会发现,当整个方法运行完,都没有执行打印。原因也很简单,就是上面所诉说的,流在未执行终止操作前,不会进行任何操作。
/**
* 中间操作
*/
@Test
public void testStreamFeatures() {
//创建流
Stream<Student> stream = studentList.stream();
//过滤年龄小于15的
Stream<Student> ageStream = stream.filter(stu -> stu.getAge() > 15);
//使用map得到学生姓名,并打印学生信息
Stream<String> nameStream = ageStream.map(stu -> {
System.out.println(stu);
return stu.getName();
});
//执行终止操作
List<String> stuNameList = nameStream.collect(Collectors.toList());
}
常用的中间操作
方法 | 描述 |
---|---|
filter(Predicate predicate) | 接收一个函数作为参数,根据提供的筛选条件predicate进行过滤 |
map(Function f) | 接收一个函数作为参数,该函数会作用到每个元素上,映射成新的元素 |
flatMap(Function f) | 接收一个函数作为参数,将流中的每个值都转换成另外一个流,然后所有流连接为一个流 |
sorted() | 按照自然顺序进行排序,返回一个新流 |
sorted(Comparator comp) | 自定义排序顺序,返回一个新流 |
skip(long n) | 将前n个元素进行丢弃,返回一个新流 |
limit(long maxSize) | 截取到maSize个数的元素值,返回一个新流 |
-
map操作
将R类型的参数,转换为T类型(R与T可以一样),最终转换返回一个新的流
/** * 中间操作 map */ @Test public void mapTest() { Stream<Student> studentStream = studentList .stream(); //提取学生的姓名 将Student类型转换String类型 Stream<String> stringStream = studentList .stream() .map(Student::getName); //打印输出 stringStream.forEach(System.out::println); }
-
flatMap 操作
将R类型的参数转换为T类型,如果Stream流中的元素是被Stream流所包裹,会将这个元素取出来,重新构成一个新的Stream流(其实flatmap只是比map厉害一点可以多转一层)
@Test public void testFlatMap() { //创建一个字符串集合 List<String> list = Arrays.asList("AB", "CD", "EFG"); Stream<Character> stream1= list .stream() .flatMap(this::stringToChar); //打印 System.out.println("入参是String,出参是Stream<Character>:"); stream1.forEach(System.out::println); }
-
filter过滤操作
filter方法就是基于我们传入的Predicate ,进行过滤数据,过滤后会产生一个新的Stream,不会对源数据产生影响
@Test public void testFilter(){ //打印过滤前的学生集合长度 System.out.println("过滤前学生长度:"+studentList.size()); //过滤年龄小于16岁的学生 List<Student> filterList = studentList .stream() .filter(stu -> stu.getAge() > 15) .collect(Collectors.toList()); //打印过滤后的学生集合长度 System.out.println("过滤前学生长度:"+filterList.size()); System.out.println("过滤后,源数据源的长度:"+studentList.size()); }
-
sorted排序操作
@Test public void sortedTest() { //学生按照年龄从小到大的排序 System.out.println("按照年龄从小到大排序:"); studentList .stream() .sorted(Comparator.comparing(Student::getAge)) .forEach(System.out::println); //学生按照年龄从大到小的排序 System.out.println("按照年龄从大到小排序:"); studentList .stream() .sorted((stu1,stu2)->stu2.getAge().compareTo(stu1.getAge())) .forEach(System.out::println); }
-
skip 操作
skip用于跳过前n个元素,从第n+1个开始获取值
@Test public void skipTest() { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); list .stream() .skip(3) .forEach(System.out::println); }
-
limit 操作
截取第一个到第n个元素,n+1个元素及其以后的元素都抛弃
@Test public void limitTest() { List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9); list .stream() .limit(3) .forEach(System.out::println); }
c.终止操作
终止在从流的流水线中(中间操作)生成最终的结果,其结果可以是任何不是流的值,且执行过终止操作的流是不可再使用的
方法 | 简述 |
---|---|
allMatch(Predicate p) | 检查是否匹配所有元素 |
anyMatch(Predicate ) | 检查是否至少匹配一个元素 |
noneMatch(Predicate p) | 检查是否没有匹配所有元素 |
findFirst() | 返回第一个元素 |
findAny() | 返回当前流中的任意元素 |
count() | 返回流中元素总数 |
max(Comparator c) | 返回流中最大值 |
min(Comparator c) | 返回流中最小值 |
forEach(Consumer c) | stream API 使用内部迭代(默认做了外部迭代) |
@Test
public void testTermination() {
//allMatch 检查是否匹配所有元素
boolean allMatch = studentList
.stream()
.allMatch(e -> e.getName().equals("张三"));
System.out.println("allMatch:" + allMatch);
System.out.println("___________________________________________________________________");
boolean anyMatch = studentList
.stream()
.anyMatch(e -> e.getName().equals("李四"));
System.out.println("anyMatch:" + anyMatch);
System.out.println("___________________________________________________________________");
//noneMatch 检查是否没有匹配元素
boolean noneMatch = studentList
.stream()
.noneMatch(e -> e.getName().equals("java"));
System.out.println("noneMatch:" + noneMatch);
System.out.println("___________________________________________________________________");
//findFirst 返回第一个元素
Optional<Student> findAny = studentList
.stream()
.findAny();
System.out.println("findAny:" + findAny);
System.out.println("___________________________________________________________________");
//findAny 返回流数据中的任意元素
Optional<Student> findFirst = studentList
.stream()
.findFirst();
System.out.println("findFirst:" + findFirst);
System.out.println("___________________________________________________________________");
//count 返回流中总个数
long count = studentList
.stream()
.count();
System.out.println("count:" + count);
System.out.println("___________________________________________________________________");
//max 返回流中最大的一个值
Optional<Student> max = studentList
.stream()
.max(Comparator.comparing(Student::getAge));
System.out.println("max:" + max);
System.out.println("___________________________________________________________________");
//min 返回流中最小值
Optional<Student> min = studentList.stream().min(Comparator.comparing(Student::getAge));
System.out.println("min:" + max);
}
方法 | 简述 |
---|---|
reduce(T iden, BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 T |
reduce(BinaryOperator b) | 可以将流中元素反复结合起来,得到一个值。返回 Optional |
方法1返回的是一个对象T,而方法二返回的是Optional,原因是方法1给了一个初始值iden
@Test
public void test3() {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9);
Integer reduce = list.stream()
.reduce(45, Integer::sum);
Optional<Integer> reduce1 = list
.stream()
.reduce(Integer::sum);
}
- 收集器
方法 | 简述 |
---|---|
collect | 将流转换为其他形式。接收一个Collection接口的实现,用于Stream中元素做汇总的方法 |
collect常与Collectors进行搭配使用,Collectors中提供了大量的方法可以使用,详见下面表(只是常用的):
方法 | 简述 |
---|---|
Collector<T, ?, List> toList | 它将输入元素 List到一个新的 List 。 |
Collector<T, ?, Set> toSet | 将输入元素 Set到一个新的 Set 。 |
Collector<T, ?, Map<K,U>> toMap | 它将元素累加到一个 Map ,其键和值是将所提供的映射函数应用于输入元素的结果。 |
Collector<T, ?, C> toCollection | 按照遇到的顺序将输入元素累加到一个新的 Collection中 |
Collector<CharSequence, ?, String> joining | 返回一个 Collector ,将输入元素连接到一个 String ,按照顺序。 |
groupingBy | 返回Collector “由基团”上的类型的输入元件操作实现T ,根据分类功能分组元素,并且在返回的结果Map 。 |
- toList
@Test
public void toListTest(){
//跳过2个学生,收集一个新的学生集合
List<Student> students = studentList
.stream()
.skip(2)
.collect(Collectors.toList());
//打印学生信息
students.stream().forEach(System.out::println);
}
- toSet
@Test
public void toSetTest() {
//收集学生的年龄 转换为set
Set<Integer> ageSet = studentList
.stream()
.map(Student::getAge)
.collect(Collectors.toSet());
//打印年龄
ageSet.forEach(System.out::println);
}
-
tomap (F标识Function,BO标识BinaryOperator)
-
Collector toMap(F keyMapper,F valueMapper)
分别传入keyMapper、valueMapper的函数式方法,将执行后的结果放入Map中
@Test public void toMapTest() { //按照姓名转map Map<String, Integer> nameMap = studentList .stream() .collect( Collectors.toMap(Student::getName, Student::getAge) ); for (Map.Entry<String, Integer> map : nameMap.entrySet()) { System.out.println(map.getKey()+","+map.getValue()); } }
-
Collector toMap(F keyMapper,F valueMapper, BO mergeFunction)
分别传入keyMapper、valueMapper的函数式方法,将执行后的结果放入Map中,其中如果出现key值重复,则使用mergeFunction进行处理
@Test public void toMapTest() { //使用学生的年龄作为key值,key重复则保留第一个 Map<Integer, String> ageMap = studentList .stream() .collect( Collectors.toMap(Student::getAge, Student::getName, (v1, v2) -> v1) ); //打印map值 for (Map.Entry<Integer, String> map : ageMap.entrySet()) { System.out.println(map.getKey()+","+map.getValue()); } }
-
Collector toMap(F keyMapper, F valueMapper,BO mergeFunction,Supplier mapSupplier)
这个方法相比于上面方法多了一个mapSupplier,它可以用于选择哪个Map
@Test public void toMapTest() { LinkedHashMap<Integer, String> linkedHashMap = studentList .stream() .collect( Collectors.toMap(Student::getAge, Student::getName, (v1, v2) -> v2, LinkedHashMap::new) ); TreeMap<Integer, String> treeMap = studentList .stream() .collect( Collectors.toMap(Student::getAge, Student::getName, (v1, v2) -> v2, TreeMap::new) );
}
- ##### join操作 joining(CharSequence delimiter),将delimiter加入到流中的每个元素中间,拼接成一个字符串进行返回 ```java @Test public void joinTest(){ String str = studentList .stream() .map(Student::getName) //将 、 加入到每个元素中间 .collect(Collectors.joining("、")); System.out.println(str); }
-
-
groupingBy
根据某个属性值,对流进行分组,它有3个不同的方法:
-
groupingByConcurrent(Function)
基于传入的function执行得到相应的key,基于这个key值进行分组
-
groupingBy(Function, Collector)
基于传入的function执行得到相应的key,基于这个key值进行分组,collector则是基于已经分好组的value,再次做一些操作
-
groupingBy(Function, Supplier, Collector)
这个相比于上面方法,多了一个Supplier,它表示你分组使用什么样的容器存储,默认是HashMap
public void groupByTest() { //按照学生姓名进行分组 Map<String, List<Student>> nameMap = studentList .stream() .collect( //groupingByConcurrent(Function) Collectors.groupingBy(Student::getName)); //按照学生年龄进行分组.然后再统计每个组的个数 Map<Integer, Long> ageCountMap = studentList .stream() .collect( // groupingBy(Function, Collector) Collectors.groupingBy(Student::getAge, Collectors.counting())); //按照学生年龄进行分组.然后再统计每个组的个数 使用TreeMap进行存储 TreeMap<Integer, Long> ageCountTreeMap = studentList .stream() .collect( // groupingBy(Function, Collector) Collectors.groupingBy(Student::getAge, TreeMap::new, Collectors.counting()) ); for (Map.Entry<Integer, Long> integerLongEntry : ageCountMap.entrySet()) { System.out.println(integerLongEntry.getKey()+","+integerLongE ntry.getValue()); } } public void groupByTest() { //按照学生姓名进行分组 Map<String, List<Student>> nameMap = studentList .stream() .collect( //groupingByConcurrent(Function) Collectors.groupingBy(Student::getName)); //按照学生年龄进行分组.然后再统计每个组的个数 Map<Integer, Long> ageCountMap = studentList .stream() .collect(Collectors.groupingBy(Student::getAge, Collectors.counting())); //按照学生年龄进行分组.然后再统计每个组的个数 使用TreeMap进行存储 TreeMap<Integer, Long> ageCountTreeMap = studentList .stream() .collect( // groupingBy(Function, Collector) Collectors.groupingBy(Student::getAge, TreeMap::new, Collectors.counting()) ); for (Map.Entry<Integer, Long> integerLongEntry : ageCountMap.entrySet()) { System.out.println(integerLongEntry.getKey()+","+integerLongE ntry.getValue()); } }
-
五、Optional
Optional可以让避免空指针的代码简洁且具高可读
① 为何要用Optional?
新建person、car、Insurance 3个对象,用于空指针讲解
public class Person {
private Car car;
public Car getCar() { return car; }
}
public class Car {
private Insurance insurance;
public Insurance getInsurance() { return insurance; }
}
public class Insurance {
private String name;
public String getName() { return name; }
}
由上可知,三者的关系是:人类拥有汽车,汽车上绑定了保险,通过一下代码可知,很容易会产生空指针异常问题:
@Test
public void testNullPointerException() {
//nullDataPerson()直接返回一个null
Person person = nullDataPerson();
//获取保险的名称
String insuranceName = person.getCar().getInsurance().getName();
//打印名称
System.out.println("汽车保险的名称:"+insuranceName);
}
使用if避免空指针异常
@Test
public void testCheckNullPointerByIf() {
//nullDataPerson()直接返回一个null
Person person = nullDataPerson();
if (person != null) {
Car car = person.getCar();
if (car != null) {
Insurance insurance = car.getInsurance();
if (insurance != null) {
System.out.println("汽车保险的名称:" + insurance.getName());
}
}
}
}
这里先埋个坑,放在第五章第③节中的map方法中讲解.
② 创建Optional对象
-
创建一个空的Optional
Optional<Car> car = Optional.empty();
-
使用of创建Optional对象
注:使用of创建Optional对象时,这个obj一定不能为null,否则会报空指针
Optional<Person> person = Optional.of(obj);
-
使用ofNullable创建Optional对象
注:见名知意使用ofNullable创建,obj可以为null
Optional<Person> person = Optional.ofNullable(obj);
③ Optional中常用方法
方法 | 说明 |
---|---|
boolean isPresent() | 判断对象是否不为null |
void ifPresent(Consumer consumer) | 如果对象存在,则执行这个consumer |
T get() | 获取Optional中的值(若值为null,会抛一个空指针) |
T orElse(T other) | 当值为空时,使用other替代 |
T orElseGet(Supplier other) | 当值为空时,使用Supplier执行产生的对象进行替代 |
T orElseThrow (Supplier exceptionSupplier) | 当值为空时,使用Supplier抛一个异常出来,当值不为空时,返回值 |
Optional map(Function mapper) | 和stream一样,对对象进行映射成一个新的值,返回Optional |
-
用于模拟数据库查库的方法
/** * 用于测试使用 产生一个空的person * @return null */ private Person nullDataPerson() { return null; } /** * 用于测试使用 产生一个空的person * @return null */ private Person nullDataCar(String personName) { return new Person(personName,null); } private Person getPerson(){ System.out.println("调用getPerson获取一个person"); return new Person("person",null); } /** *返回一个全量数据的Person * @return 返回一个具有保险名称的person */ private Person nullDataCar() { return new Person("法外狂徒张三",new Car(new Insurance("太平洋车辆保险"))); }
-
isPresent()
判断对象是否为空,正常情况下都不会用这个东西,要不然和if没啥区别
@Test public void test03(){ //isPresent() 判断对象是否存在,如果存在着为true,反之false boolean present = Optional .ofNullable(nullDataPerson()) .isPresent(); }
-
void ifPresent(Consumer<? super T> consumer)
如果对象存在,则执行这个consumer进行一些操作
@Test public void test03(){ //如果对象存在 打印对象 Optional .ofNullable(nullDataCar("法外狂徒张三")) .ifPresent(System.out::print); }
-
T get()
获取被Optional所包裹的对象,如果被包裹的对象为null,则会抛出一个空指针异常
@Test public void test03(){ //get() 获取被Optional所包裹的对象 如果为null则抛空指针异常 Person person = Optional .ofNullable(nullDataCar("法外狂徒张三")) .get(); }
-
T orElse(T other)|T orElseGet(Supplier supplier )| T orElseThrow(Supplier supplier) throws X
orElse当对象为null时,会直接将other当做默认值,然后返回(如果other传入的是一个方法,即使值不为空,也会执行这个方法)
@Test public void test03(){ //获取学生的部门名称 Student stu = findOne("张三"); //有值情况下 Student student = Optional .ofNullable(stu) .orElse(getStudent()); //无值情况下 Student studentNull = Optional .ofNullable(findOneWithNull()) .orElse(getStudent()); } private Student getStudent(){ System.out.println("执行了getStudent方法"); return new Student().setName("默认值"); } /** * 模拟数据库查询操作 返回null * @return Student */ private Student findOneWithNull(){ return null; } private Student findOne(String name){ return new Student().setName(name); } //输出 //执行了getStudent方法 //执行了getStudent方法
orElseGet当对象为null时,会执行supplier产生一个值进行返回,如果值不为空,则不会执行supplier
@Test public void test05(){ //orElseGet(Supplier supplier ) person = Optional .ofNullable(nullDataCar("李四")) .orElse(this::getPerson); System.out.println("打印人名:"+person.getName()); }
orElseThrow当对象为null时,会执行supplier然后抛出一个异常,当值不为空时,返回值
@Test public void test06(){ Student student = Optional .ofNullable(stu) .orElseThrow(()->new ResourceNotFoundException("根据:"+name+"未找到学生实体")); }
-
Optional map(Function<? super T, ? extends U> mapper)
和stream一样,对对象进行映射成一个新的值,返回Optional,这个可以用来避免多次if判断导致代码冗余
@Test public void testMethod(){ String name = Optional .of(allDataPerson()) .map(Person::getCar) .map(Car::getInsurance) .map(Insurance::getName) .orElse("默认值"); System.out.println("打印名称:"+name); }
六、并行流
并行流就是把一个内容分成多个数据块,并用不同的现场处理每个数据块的流,如果是单核CPU,只会存在并发而不会并行。
Fork/Join 框架:
Fork/Join框架是将一个大任务划分为若干个子任务,各自执行,最后将执行结果进行汇总,然后得到大任务的结果的框架。
图片来源于:《java8实战》
工作窃取算法
分支/合并框架工程用一种称为工作窃取( work stealing)的技术来解决这个问题。在实际应用中,这意味着这些任务差不多被平均分配到ForkJoinPool中的所有线程上。每个线程都为分配给它的任务保存一个双向链式队列,每完成一个任务,就会从队列头上取出下一个任务开始执行。基于前面所述的原因,某个线程可能早早完成了分配给它的所有任务,也就是它的队列已经空了,而其他的线程还很忙。这时,这个线程并没有闲下来,而是随机选了一个别的线程,从队列的尾巴上“偷走”一个任务。这个过程一直继续下去,直到所有的任务都执行完毕,所有的队列都清空。这就是为什么要划成许多小任务而不是少数几个大任务,这有助于更好地在工作线程之间平衡负载。
工作窃取图解:
并行流内部使用了默认的ForkJoinPool( 7.2节会进一步讲到分支/合并框架),它默认的线 程 数 量 就 是 你 的 处 理 器 数 量 , 这 个 值 是 由 Runtime.getRuntime().availableProcessors()得到的。但 是 你 可 以 通 过 系 统 属 性 java.util.concurrent.ForkJoinPool.common.parallelism来改变线程池大小 .
并行流开启方式
以下2种都可以开启并行流
@Test
public void testMethod(){
//第一种 直接.parallelStream()
Stream<Student> studentStream = studentList
.parallelStream();
//第二种 .parallel()
Stream<Long> parallel = Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel();
}
并行流存在的问题
并行流一定比顺序流更快?
测量对前n个自然是求和函数的性能
首先准备以下3个方法:parallelSum并行迭代器求和、iterativeSum迭代器求和、parallelRangedSum 使用LongStream参数的数值求和
public long parallelSum(long n) {
return Stream.iterate(1L, i -> i + 1)
.limit(n)
.parallel()
.reduce(0L, Long::sum);
}
public long iterativeSum(long n) {
long result = 0;
for (long i = 1L; i <= n; i++) {
result += i;
}
return result;
}
public long parallelRangedSum(long n) {
return LongStream.rangeClosed(1, n)
.parallel()
.reduce(0L, Long::sum);
}
在提供一个用于加载以上三者不同求和方式的方法,用于测试3种方法求和耗时,这个方法会对方法采样十次,输出计算和、计算耗时
public void measureSumPerf(Function<Long, Long> adder, long n) {
for (int i = 0; i < 10; i++) {
long start = System.currentTimeMillis();
long sum = adder.apply(n);
long duration = (System.currentTimeMillis() - start) ;
System.out.println("sum: " + sum+",time:"+duration);
}
}
样本数据
以下是执行求和方法代码
@Test
public void testParallelSum() {
measureSumPerf(this::parallelSum, 10000000L);
}
@Test
public void testIterativeSum() {
measureSumPerf(this::iterativeSum, 10000000L);
}
@Test
public void testRangedSum() {
measureSumPerf(this::parallelRangedSum, 10000000L);
}
以下是上述3种不同方式求和耗时样本数据
parallelSum | iterativeSum | parallelRangedSum |
---|---|---|
1055 | 19 | 55 |
1576 | 10 | 1 |
435 | 10 | 2 |
281 | 9 | 2 |
213 | 11 | 1 |
331 | 1 | 5 |
223 | 9 | 5 |
145 | 11 | 3 |
173 | 12 | 5 |
327 | 3 | 2 |
通过以上数据发现,并行版本比顺序版本要慢很多,因此可以得出,盲目的使用并行流并不一定能够让程序更快,可能会让程序更慢,那么为啥上述程序并行流比顺序版本慢? 但使用parallelRangedSum 进行并行求和就会很快呢?
1.parallelSum使用的是Stream.iterate来产生数据,而生产的都是装箱数据,在计算的时候需要将数据进行拆箱,而iterativeSum产生的数据则不需要做任何装箱或者拆箱操作
2.iiterate很难分割成能够独立执行的小块,因为每次应用这个函数都要依赖前一次应用的结
果 (上述阐述过,并行流采用的是Fork/Join,需要将任务拆分再合并)
3.LongStream.rangeClosed直接产生原始类型的long数字,没有装箱拆箱的开销。
4.LongStream.rangeClosed会生成数字范围, 很容易拆分为独立的小块
并行流中的安全问题
提供一个用于测试的计算类Accumulator,add方法用于进行累加
public class Accumulator {
public long total = 0;
public void add(long value) {
total += value;
}
}
使用上述类,进行5次测试,观测一下每次计算的结果是否一致
@Test
public void testThread(){
for (int i = 0; i < 5; i++) {
System.out.println("打印每次计算和:"+sideEffectSum(100000L));
}
}
public long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream
.rangeClosed(1, n)
.parallel()
.forEach(accumulator::add);
return accumulator.total;
}
样本数据:
打印每次计算和:1356102899
打印每次计算和:1000593908
打印每次计算和:1206185264
打印每次计算和:1258260474
打印每次计算和:1486204215
明显发现每次执行的结果都不一样,原因也很简单,由于多个多线程同时访问累加器,执行total += value,在操作共享变量时,非原子性操作
正确使用并行流
1.确认数据量在使用并行流前,是否达到了并行流比顺序流快的标准线,如果不确定写demo验证,因为并行流在数据量未达到的情况下,可能比顺序流还慢(并行存在在额外的资源消耗)
2.留意操作的对象是否存在装箱、拆箱操作,因为会自动装箱与拆箱与大大降低性能,可以使用java8提供的( IntStream、LongStream、 DoubleStream)来避免这种操作,但凡有可能都应该用这些流
3.要考虑流背后的数据结构是否易于分解。例如, ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历
源 | 可分界性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
类型的long数字,没有装箱拆箱的开销。
4.LongStream.rangeClosed会生成数字范围, 很容易拆分为独立的小块
并行流中的安全问题
提供一个用于测试的计算类Accumulator,add方法用于进行累加
public class Accumulator {
public long total = 0;
public void add(long value) {
total += value;
}
}
使用上述类,进行5次测试,观测一下每次计算的结果是否一致
@Test
public void testThread(){
for (int i = 0; i < 5; i++) {
System.out.println("打印每次计算和:"+sideEffectSum(100000L));
}
}
public long sideEffectSum(long n) {
Accumulator accumulator = new Accumulator();
LongStream
.rangeClosed(1, n)
.parallel()
.forEach(accumulator::add);
return accumulator.total;
}
样本数据:
打印每次计算和:1356102899
打印每次计算和:1000593908
打印每次计算和:1206185264
打印每次计算和:1258260474
打印每次计算和:1486204215
明显发现每次执行的结果都不一样,原因也很简单,由于多个多线程同时访问累加器,执行total += value,在操作共享变量时,非原子性操作
正确使用并行流
1.确认数据量在使用并行流前,是否达到了并行流比顺序流快的标准线,如果不确定写demo验证,因为并行流在数据量未达到的情况下,可能比顺序流还慢(并行存在在额外的资源消耗)
2.留意操作的对象是否存在装箱、拆箱操作,因为会自动装箱与拆箱与大大降低性能,可以使用java8提供的( IntStream、LongStream、 DoubleStream)来避免这种操作,但凡有可能都应该用这些流
3.要考虑流背后的数据结构是否易于分解。例如, ArrayList的拆分效率比LinkedList高得多,因为前者用不着遍历就可以平均拆分,而后者则必须遍历
源 | 可分界性 |
---|---|
ArrayList | 极佳 |
LinkedList | 差 |
IntStream.range | 极佳 |
Stream.iterate | 差 |
HashSet | 好 |
TreeSet | 好 |
4.在中间、终止操作时要考虑是否存在不安全的线程操作,避免在浪费性能的基础上,产出错误的结果