目录
三、较Java7而言,更加推荐使用Java8 ==》为什么要使用Java8
至此,Lambda表达式、函数式接口、方法引用我们已经学会了。
一、前言~写给初学Java8人的文章
本文综合了下述资料,整理成一份关于Java 8新特性的参考教材,希望你有所收获。
https://www.runoob.com/java/java8-new-features.html
https://blog.csdn.net/yczz/article/details/50896975
尚硅谷Java8视频
本人不是照抄照搬,而是结合自己在学习中的感受和理解,以更易懂、简介的方式去诠释Java8新特性,也算是记录自己的学习过程。
希望对你有帮助!
二、内容简介
本篇文章将从以下八个方面,从最简单的Lambda表达式到其他特性的学习
1、Lambda表达式
2、函数式接口
3、方法引用和构造器引用
4、Stream API
5、接口中的默认方法与静态方法
6、Optional类
7、新时间日期API
8、其他新特性
【提醒】在记得刚毕业一年时,去企业面试会被问到这样的几个问题?
1)有没有使用过JAVA8?
2)你觉得java8的优点是什么?
3)java的某个特性的用法?例如:Lambda、Stream、Option
4)什么是内部迭代或者外部迭代?.....
学完这篇文章你就会知道这些问题的答案。对这些问题更加深刻的理解,深有感受!
三、较Java7而言,更加推荐使用Java8 ==》为什么要使用Java8
说明:我没接触过java9,乃至更高版本的新特性,就java7和java8对比而已,代码整洁,开发更快,并行,空指针异常,这几个方面的感受最深。
原因如下:(面试题答案)
1、速度更快 --》 增加了红黑树,优化了hash,java7时,哈希表是有数组+链表组成,经由hash计算数组的位置,然后调用equal判断是否存在,如果不存在,将是以链表的形式存储。在java8时,如果链表的长度 > 8,以及数组的长度大于64时,将不再以链表的形式存储,而变成了红黑树,这样在查找的时候速度更快 ==》 这段话,有兴趣的可以看看,如果面试遇到的话,可以讲一下,java8中增加了红黑树,红黑树的用途在哪,快在哪
2、代码更少 -》 使用Lambda表达式
3、强大的Stram API -》提供了对数据源、数组、集合的,类似于SQL的数据操作API
4、便于并行 -》在java7时支持并行开发,Fork/Join框架,拆分合并,需要使用实现RecursiveTask接口,编程很复杂,而且开发代码难以被理解,所以很少用,而在java8中提供了parallel()和sequential()方法,很容易的对数据进行并行操作
5、最大化减少空指针异常 Optional -》 下面介绍Optional类
以上1~5点的红色部分,可以稍微了解一下,如果在面试时,问到类似的问题,可以用得上哦。
四、Lambda表达式
1、概念
在Java8中引入一个新的操作符号“->”,该符号称之为:箭头符号或Lambda操作符,改操作符将Lambda表达式拆分成两部分。
左侧是Lambda表达式的参数列表
右侧是Lambda表达式的执行体,具体的操作实现部分
2、基本语法
(parameters) -> expression
或
(parameters) ->{ statements; }
3、Lambda表达式的特点
说明parameters表示参数列表,->表示连接符,{}表示内部是方法体
3.1、= 右边的类型会根据左边的函数式接口类型自动推断;
3.2、如果形参列表为空,只需保留();
3.3、如果形参只有1个,()可以省略,只需要参数的名称即可;
3.4、如果执行语句只有1句,且无返回值,{}可以省略,若有返回值,则若想省去{},则必须同时省略return,且执行语句也保证只有1句
3.5、形参列表的数据类型会自动推断;
3.6、lambda不会生成一个单独的内部类文件;
3.7、lambda表达式若访问了局部变量,则局部变量必须是final的,若是局部变量没有加final关键字,系统会自动添加,此后在修改该局部变量,会报错。
【备注】上述的7点描述了Lambda表达式各种简写的方式
4、Lambda格式 ~ 对上述的特点进行解读
语法格式一:无参数,无返回值
() -> System.out.println("Hello Lambda!");
语法格式二:有一个参数,并且无返回值
(x) -> System.out.println(x)
语法格式三:若只有一个参数,小括号可以省略不写
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);
语法格式六:支持指定参数的数据类型
(Integer x, Integer y) -> Integer.compare(x, y);
5、入门案例
5.1 demo1
当某些函数,只被使用一次的,则推荐使用Lambda表达式,无须声明一个函数,这就是Java8的好处
package com.dgut.edu.cn.zhenghongbin.jdk8.TestMethodRef;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
public class TestLambda001 {
public static void main(String[] args) {
// 第一种格式: 无参数、无返回值
Runnable r = () -> System.out.print("Hello World");
r.run();
System.out.println("\n格式语法1end...");
List<String> stringList = Arrays.asList("a", "b", "c");
// 语法格式二:有一个参数,并且无返回值
stringList.forEach((str) -> System.out.print(str));
System.out.println("\n格式语法2end...");
// 语法格式三:若只有一个参数,小括号可以省略不写
stringList.forEach( str -> System.out.print(str));
System.out.println("\n格式语法3end...");
// 语法格式四:有两个以上的参数,有返回值,并且 Lambda 体中有多条语句
Comparator<Integer> com = (x, y) -> {
System.out.println("函数式接口");
return Integer.compare(x, y);
};
System.out.println(com.compare(1, 2));
System.out.println("\n格式语法4end...");
// 语法格式五:若 Lambda 体中只有一条语句, return 和 大括号都可以省略不写
stringList.forEach( str -> System.out.print(str));
System.out.println("\n格式语法5end...");
// 语法格式六:支持指定参数的数据类型
System.out.println("格式语法6end...");
stringList.forEach( (String str) -> System.out.print(str));
}
}
执行结果:
自此,Lambda表达式基本就学完,特别的简单。
5.1 demo2 ~ 匿名内部内到Lambda表达式
5.1.1 线程实现到Lambda表达式
// 匿名内部类
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("tttttttttttttttttttt");
}
};
runnable.run();
// lambda表达式
Runnable r1 = () ->System.out.println("dddddd");
r1.run();
5.1.2 数据类型比较接口到Lambda表达式
// 匿名内部类
Comparator comparatorUtil = new Comparator<String>() {
@Override
public int compare( String a, String b) {
return Integer.compare(a.length(), b.length());
}
};
System.out.println(comparatorUtil.compare("aaa", "bbb"));
// lambda表达式
Comparator<String> comparator2 = (x, y) -> Integer.compare(x.length(), y.length());
System.out.println(comparator2.compare("ccc","ddd"));
从上述的两个案例中,可以得到:函数接口
Lambda的设计者们为了让现有的功能与Lambda表达式良好兼容,考虑了很多方法,于是产生了函数接口这个概念。函数接口指的是只有一个函数的接口,这样的接口可以隐式转换为Lambda表达式。java.lang.Runnable和java.util.concurrent.Callable是函数式接口的最佳例子。在实践中,函数式接口非常脆弱:只要某个开发者在该接口中添加一个函数,则该接口就不再是函数式接口进而导致编译失败。为了克服这种代码层面的脆弱性,并显式说明某个接口是函数式接口,Java 8 提供了一个特殊的注解@FunctionalInterface(Java 库中的所有相关接口都已经带有这个注解了):
5.1 demo3 练习Lambda表达式
public class Employee {
private int id;
private String name;
private int age;
private double salary;
public Employee() {
}
public Employee(String name) {
this.name = name;
}
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public Employee(int id, String name, int age, double salary) {
this.id = id;
this.name = name;
this.age = age;
this.salary = salary;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
public String show() {
return "测试方法引用!";
}
public void getLength(Employee e){
System.out.println(e.getName().length());
}
public void getLength2(){
System.out.println(2);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + id;
result = prime * result + ((name == null) ? 0 : name.hashCode());
long temp;
temp = Double.doubleToLongBits(salary);
result = prime * result + (int) (temp ^ (temp >>> 32));
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Employee other = (Employee) obj;
if (age != other.age)
return false;
if (id != other.id)
return false;
if (name == null) {
if (other.name != null)
return false;
} else if (!name.equals(other.name))
return false;
if (Double.doubleToLongBits(salary) != Double.doubleToLongBits(other.salary))
return false;
return true;
}
@Override
public String toString() {
return "Employee [id=" + id + ", name=" + name + ", age=" + age + ", salary=" + salary + "]";
}
}
练习题目
//需求:获取公司中年龄小于 35 的员工信息
@Test
public void test2(){
List<Employee> filterEmployee = new ArrayList<>();
emps.forEach( (e) -> {
if(e.getAge() < 35){
filterEmployee.add(e);
}
});
System.out.println("=========打印=============");
filterEmployee.forEach(System.out::println);
}
//需求:获取公司中工资大于 5000 的员工信息
@Test
public void test3(){
List<Employee> filterEmployee = new ArrayList<>();
emps.forEach(e -> {
if( e.getSalary() > 5000){
filterEmployee.add(e);
}
});
System.out.println("=========打印=============");
filterEmployee.forEach(System.out::println);
}
6、Lambda表达式注意要点
变量作用域
lambda 表达式只能引用标记了 final 的外层局部变量,这就是说不能在 lambda 内部修改定义在域外的局部变量,否则会编译错误。
一开始在学习Lambda表达式的时候,会有一个想法,就是如何去改变Lambda表达式外部的一个值。
在Lambda表示中声明,只能去修改定义在域外的使用final修饰的局部变量,而final修饰的局部变量不可变,因此是不推荐的。
如果需要改变,应该使用以下的方式。
// 需求:判断员工的工资是否大于5000,返回boolean
这个需求需要
下面注释的代码是错误的,错误提示如下图。
@Test
public void test5(){
boolean isMoreThan = false;
List<Employee> moreThanEmployee = new ArrayList<>();
emps.forEach( e -> {
if(e.getSalary() > 5000) {
moreThanEmployee.add(e);
}
});
if(moreThanEmployee.size() == emps.size()){
isMoreThan = true;
}
System.out.println("isMoreThan is : " + isMoreThan);
}
错误代码:
总结来说:一般使用Lambda表达式不来改变外部局部变量的值,这点是要注意的。
这是我学习Lambda表达式来最大的疑问,希望对你们有帮助。
在上述中,提到一个概念叫函数式接口,接下来就来讲解一下什么是函数式接口!
五、函数式接口
定义:只包含一个抽象方法的接口,成为函数式接口
特点:在接口上有注解@FunctionInterfaca注解
其实在jdk8之前已经有很多函数式接口
java.lang.Runnable
java.util.concurrent.Callable
java.security.PrivilegedAction
java.util.Comparator
java.io.FileFilter
java.nio.file.PathMatcher
java.lang.reflect.InvocationHandler
java.beans.PropertyChangeListener
java.awt.event.ActionListener
javax.swing.event.ChangeListener
JDK 1.8 新增加的函数接口
这里举例四个最常用的函数式接口
1、为什么学习函数式接口呢?
1)为了让现有的功能与Lambda表达更好的兼容,代码更少
2)更好的学习Stream API ,在Stream API中,有大量的函数式接口入参,如果不明白这些概念的话,将很难使用Stream API
所以,了解常用的函数式接口非常之有用
2、Java 内置四大核心函数式接口
除此之外,还有20来个其他接口,具体使用到再看API即可
https://www.runoob.com/java/java8-functional-interfaces.html
3、入门案例
3.1 Consumer<T> : 消费型接口、接口只有一个参数,且无返回值
void accept(T t);
List<String> str = Arrays.asList("aa", "bb", "cc", "dd", "ee");
@Test
public void test1(){
happy(str, (m)-> m.forEach(System.out::println));
happy2("a", (m)-> System.out.println(m));
}
/**
* 消费型接口
*/
public void happy(List<String> stringList, Consumer<List<String>> consumer){
consumer.accept(stringList);
}
public void happy2(String str, Consumer<String> consumer){
consumer.accept(str);
}
【理解】在一开始接触消费型接口的话,非常不能理解。就上面的例子来说,如果说,要实现对参数的打印的话,为什么不直接在方法里面操作,而定义一个消费型接口函数,将具体的实现放入到具体的调用中呢。
其实,最主要的目的还是实现和定义分开,在happy方法中的第二个参数,说白了就是一个接口定义,没有具体的实现,而将具体的实现(多样化)放入到调用中,所以称之为内置函数式接口。
换一种理解来说,如果说我定义一个操作的接口,然后有两个具体操作的类型去实现这个接口,然后在调用的时候传入具体的实现类,也可以实现上述的效果,这样的话会多增加两个实现类。而采用函数式方法编程,代码更加整洁。
如果上述代码,不能理解的话,个人建议,从lambda表达式的匿名内部类到lambda表达式、还有上述的两个案例,手动敲一遍,就会知道它的好处的。
【扩展】学习这个是为了更好的去使用Java8的API,更好的使用Lambda表达式
例如:在Stream().generate()的入参中要求传入一个Suppiler参数,按照以前的编程思想,需要写一个类去实现Supplier接口传入,这样就很麻烦,如果使用Lambda就很方法了,直接写实现即可。
下面的四个方法按照上面的理解
3.2 Supplier<T> 供给型接口,接口返回一个任意范型的值,和Function接口不同的是该接口没有任何参数
T get()
@Test
public void test22(){
List<Integer> numList = getNumList2(5, ()->(int)(Math.random() * 100));
numList.forEach(System.out::println);
}
public List<Integer> getNumList2(int num,Supplier<Integer> supplier){
List<Integer> integersList = new ArrayList<>();
for (int i = 0; i < num; i++){
integersList.add(supplier.get());
}
return integersList;
}
案例2
@Test
public void test222(){
List<Integer> numList = getNumList(5, 10 , ()->(int)(Math.random() * 100));
numList.forEach(System.out::println);
}
/**
* Supplier<T> 供给型接口
* T get()
*/
//需求:产生指定个数的整数,并放入集合中
public List<Integer> getNumList(int min, int max, Supplier<Integer> supplier){
List<Integer> integersList = new ArrayList<>();
for (; min < max; min++){
integersList.add(supplier.get());
}
return integersList;
}
复杂Demo,结合业务
List<Employee> emps = Arrays.asList(
new Employee(101, "张三", 18, 9999.99),
new Employee(102, "李四", 59, 6666.66),
new Employee(103, "王五", 28, 3333.33),
new Employee(104, "赵六", 8, 7777.77),
new Employee(105, "田七", 38, 5555.55)
);
@Test
public void test2222(){
List<Integer> numList = getNumList(5, 10, ()-> emps.get(0).getId());
numList.forEach(System.out::println);
}
/**
* Supplier<T> 供给型接口
* T get()
*/
//需求:产生指定个数的整数,并放入集合中
public List<Integer> getNumList(int min, int max, Supplier<Integer> supplier){
List<Integer> integersList = new ArrayList<>();
for (; min < max; min++){
integersList.add(supplier.get());
}
return integersList;
}
结合Stream API更好
@Test
public void test2(){
List<Integer> numList = getNumList(5, 10, ()-> emps.stream().limit(1).map(Employee::getId).collect(Collectors.toList()).get(0));
numList.forEach(System.out::println);
}
3.3 Function<T, R> 函数式接口。接口有一个参数并且返回一个结果,并附带了一些可以和其他函数组合的默认方法(compose, andThen):
R applay(T)
@Test
public void test3(){
int bbb = strHandler("bbb", str -> str.length() + 100);
System.out.println(bbb);
String s = strList("aaa", "bbb", (x, y) -> x + y);
System.out.println(s);
strList2("aaa", 10, (x, y) ->{
List<String> stringList = new ArrayList<>();
for (int i = 0 ; i < 10; i++){
stringList.add(x + i);
}
return stringList;
}).forEach(System.out::println);
}
/**
* 函数式接口
* Function<T, R>
* applay
*/
//需求:用于处理字符串
public int strHandler(String str, Function<String, Integer> function){
return function.apply(str);
}
【注意】如果说是:需要两个入参的话,这时候可以参考一下子类
@Test
public void test3(){
int bbb = strHandler("bbb", str -> str.length() + 100);
System.out.println(bbb);
String s = strList("aaa", "bbb", (x, y) -> x + y);
System.out.println(s);
strList2("aaa", 10, (x, y) ->{
List<String> stringList = new ArrayList<>();
for (int i = 0 ; i < 10; i++){
stringList.add(x + i);
}
return stringList;
}).forEach(System.out::println);
}
public int strHandler2(String str, String str2, BiFunction<String, String, Integer> function){
return function.apply(str, str2);
}
/**
* 函数式接口
* Function<T, R>
* applay
*/
//需求:用于处理字符串
public int strHandler(String str, Function<String, Integer> function){
return function.apply(str);
}
public List<String> strList2(String str, int number, BiFunction<String , Integer , List<String>> biFunction){
return biFunction.apply(str, number);
}
public String strList(String str, String str2, BiFunction<String , String , String > biFunction){
return biFunction.apply(str, str2);
}
4.4 Predicate<T> 断言型接口。接口只有一个参数,返回boolean类型。该接口包含多种默认方法来将Predicate组合成其他复杂的逻辑(比如:与,或,非):
boolean test(T t);
@Test
public void test4(){
filterStr(emps, (e)-> e.getAge() > 55).forEach(System.out::println);
}
/**
* 断言型接口
* Predicate<T>
* boolean test(T t)
*/
//需求:将满足条件的字符串,放入集合中
public List<Employee> filterStr(List<Employee> employees, Predicate<Employee> predicate){
List<Employee> employeeList = new ArrayList<>();
employees.forEach(e->{
if(predicate.test(e)){
employeeList.add(e);
}
});
return employeeList;
}
自此,Java8四大内置核心函数式接口已经学会了,其他的子接口看具体的API,参考上面的案例写写一些Demo
【学习心得】
初学习上述四大内置接口时,觉得很绕,不习惯。这时不妨多敲几遍上述的代码,自己模拟上述的案例,不看源码,写出来,慢慢就能体会到里面的内涵了。
等你深有体会之后,在去使用Stream API更能理解
例如:Stream.generate(Supplier<T> s):参数:提供型接口,因此需要写入提供型接口的实现。例如:取员工中年龄大于30岁的、
emp.stream().filter(Predicate<T> predicate):参数断言型接口,引入需要写入返回boolean的接口实现,例如:emp.getId() == 101
emp.steam().limit(1).map(Function f):参数函数型接口,因此就知道需要写入一个函数式接口实现
......
六、方法引用和构造器引用
使用操作符“::”将方法名和对象或类的名字分割开来
6.1 为什么使用方法引用和构造器引用?
方法引用使得开发者可以直接引用现存的方法、Java类的构造方法或者实例对象。方法引用和Lambda表达式配合使用,使得java类的构造方法看起来紧凑而简洁,没有很多复杂的模板代码。
用代码演示:
静态方法不说,如果是要使用非静态方法的话,那么需要new一个实例,然后用该实例.invoke某个方法。为了让代码更加简洁,就不需要创建改对象了,直接使用方法引用。
6.2 方法引用格式
1、对象:: 实例方法名
2、类:: 静态方法名
3、类: 实例方法名
【特别特别提醒一点】格式1和2很好理解,唯一很难理解的是:3
查了很多资料,看到这句话:
详细看看下面的案例
6.3 入门学习案例
1、对象::实例方法格式
在Lambda表达式中。
对象调用实例方法 ==》 (x, y) -> 对象.invokemethodName (param1, param2)
简写为: 对象::methodName
特备注意的一点是:lambda表达式中的左侧参数列表,必须跟对象方法的参数列表匹配上,因此可简写成方法引用
public class TestMethodRef {
/**
* 内部接口定义
*/
public interface FunctionOperate{
String toUpperCase(String str);
int sum(int a , int b);
}
/**
* 实现类
*/
public class BizFunctionOperate implements FunctionOperate{
@Override
public int sum(int a, int b) {
return a + b;
}
@Override
public String toUpperCase(String str) {
System.out.println(str.toUpperCase());
return str.toUpperCase();
}
}
@Test
public void test001(){
List<String> stringList = Arrays.asList("aa", "bb", "cc", "dd");
TestMethodRef.BizFunctionOperate bizFunctionOperate = new TestMethodRef.BizFunctionOperate();
// 如果不使用方法引用
stringList.forEach(x -> bizFunctionOperate.toUpperCase(x));
// 如果使用方法引用
stringList.forEach(bizFunctionOperate::toUpperCase);
}
}
2、类::静态方法名
同样:若lambda表达式中的左侧参数列表,必须跟对象方法的参数列表匹配上,因此可简写成方法引用
@Test
public void test002(){
List<String> stringList = Arrays.asList("aa", "bb", "cc", "dd");
// 如果不使用方法引用
stringList.forEach((x)-> System.out.println(x));
// 如果使用方法引用
stringList.forEach(System.out::println);
}
说白了,跟实例::实例方法格式一样
3、 类:: 实例方方
【注意】特定条件:
也就是:它不是瞎用的
最长犯的一个错
举个例子:
class Car {
//Supplier是jdk1.8的接口,这里和lamda一起使用了
public static void collide(final Car car) {
System.out.println("Collided " + car.toString());
}
public void follow(final Car another) {
System.out.println("Following the " + another.toString());
}
public void repair() {
System.out.println("Repaired " + this.toString());
}
}
@Test
public void test001(){
List<String> stringList = Arrays.asList("aa", "bb", "cc", "dd");
TestMethodRef.BizFunctionOperate bizFunctionOperate = new TestMethodRef.BizFunctionOperate();
// 如果不使用方法引用
stringList.forEach(x -> bizFunctionOperate.toUpperCase(x));
// 如果使用方法引用
stringList.forEach(bizFunctionOperate::toUpperCase);
List<Employee> employeeLists = Arrays.asList(new Employee("小郑")
, new Employee("小杰"), new Employee("莎莎"));
// 如果不使用方法引用
employeeLists.forEach(employee -> employee.getLength2());
// 如果使用方法引用
employeeLists.forEach(Employee::getLength2);
}
从上述的例子中可以看出,如果第一个参数是调用对象,并且第二个参数是需要引 用方法的第二个参数(或无参数)时 才可以引用
细细品上面这句话。
【注意】这些概念一定要好好理解,无论是自己写代码还是理解别人的代码都很有帮助的哦
6.4 构造器引用 :构造器的参数列表,需要与函数式接口中参数列表保持一致!
格式
类名::new
案例
/**
* 构造器引用
*/
@Test
public void test2(){
Function<String, Employee> fun = Employee::new;
fun.apply("a");
fun.apply("b");
BiFunction<String, Integer, Employee> fun2 = Employee::new;
}
public interface LambdaTest5 {
abstract String creatString(char[] c);
}
public class Main {
public static void main(String[] args) {
LambdaTest5 lt5 = String::new;
System.out.println(lt5.creatString(new char[]{'1','2','3','a'}));
}
}
项目举例
6.5 数据引用
//数组引用
@Test
public void test8(){
Function<Integer, String[]> fun = (args) -> new String[args];
String[] strs = fun.apply(10);
System.out.println(strs.length);
System.out.println("--------------------------");
Function<Integer, Employee[]> fun2 = Employee[] :: new;
Employee[] emps = fun2.apply(20);
System.out.println(emps.length);
}
其实跟创建一个数据一样的。只是更加简洁罢了
至此,Lambda表达式、函数式接口、方法引用我们已经学会了。
个人学习建议:
看完整篇文章,可能会觉得很多概念性的东西,很多人抱着就是要学习一下Java8的新语法、看看Stream流时如何操作数据的、Optional如何使用案例。
其实不然,个人觉得更重要的是概念性的东西,而不是API如何使用,况且你去阅读大神的源码时,很多代码都会看到方法引用和函数式接口编程和lambda表达式,只有理解了它的本质概念性的精华部分,才能更好的用好API。
接下来更多的是API的案例,不会有这么多的概念咯
接下来学习Stream API,由于篇幅过长,另起一篇文章,有兴趣可以看看哦,希望对你们有帮助,谢谢
流的创建
中间操作
终止操作
....
https://blog.csdn.net/xiaozhegaa/article/details/105907130