1

JDK1.8新特性总结

  1. 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

  1. 函数式接口

简单来说就是只定义了一个抽象方法的接口(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);
}

输出结果

img

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));
}

输出结果

img

代码分析

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("********************");

}

输出结果

img

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);

这段代码调用 Streamfilter 方法,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("********************");

}

输出结果

img

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 方法来做转换。

  1. 方法与构造函数引用

若lambda体中的内容有方法已经实现了,那么可以使用“方法引用” 也可以理解为方法引用是lambda表达式的另外一种表现形式并且其语法比lambda表达式更加简单

3.1 方法引用

方法引用其实是 lambda 表达式的部分的简化,也就是为了简化 lambda 表达式而存在的感觉,下面我们还讲讲怎么使用方法引用

三种表现形式:

  1. 对象::实例方法名
  2. 类::静态方法名
  3. 类::实例方法名 (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);
}

  1. 接口的默认方法和静态方法

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 声明。

  1. 流操作-Stream

1 理论

1.1 初识Stream

image-20200608150326644

@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);
}

image-20200608152101310

程序在正常完成一次打印工作后报错。

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会先打印,否则后打印,代码运行结果为

image-20200608152142643

可见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);
}
});
}

结果报空指针异常

image-20200608152221817

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 接口定义的默认方法 charscodePoints ,而将流转为字符串就是我们前面已经讲解到的方法 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();
}

在上面的例子中,先用charscodePoints 方法转换为流,然后都是利用 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 却改变了这种现实,我们来看一看怎么使用 mapflatMap

首先,我们先看一下这俩个方法的声明

<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 的方法能够“展平”包裹的流,这就是 mapflatMap 的区别。

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);

}

img

例子中的第一个是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 接口提供了两个方法 findFirstfindAny

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);
}

串行流打印结果如下:

image-20200608153126534

并行流打印结果如下:

image-20200608153144322

咋看不一定能看懂,我们用如下的图来解释

image-20200608153157210

我们将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_);

}

image-20200608153309293

可见,对于串行流.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算子后,才会触发。

image-20200608153358140

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中为我们提供的方法

image-20200608155513796

下面我们写几个例子来具体看一下每个方法的作用:

7.1 of

//创建一个值为张三的String类型的Optional
Optional<String> ofOptional = Optional.of("张三");
//如果我们用of方法创建Optional对象时,所传入的值为null,则抛出NullPointerException如下图所示
Optional<String> nullOptional = Optional.of(null);

image-20200608155635916

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());

image-20200608155942069

Optional<String> emptyOptional = Optional.empty();
System.out.println(emptyOptional.get());

image-20200608160026069

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("李四"));

image-20200608160105214

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"));

image-20200608160136529

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);
}
}

image-20200608160205444

如果创建的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"));

image-20200608160322941

注意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("失败"));

image-20200608160354006

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("失败"));

image-20200608160433657

7.11 ifPresent

//ifPresent方法的参数是一个Consumer的实现类,Consumer类包含一个抽象方法,该抽象方法对传入的值进行处理,只处理没有返回值。
Optional<String> stringOptional = Optional.of("zhangsan");
stringOptional.ifPresent(e-> System.out.println("我被处理了。。。"+e));

image-20200608160501696

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);


成俊杰
1 声望1 粉丝