前言
在你立足处深挖下去,就会有泉水涌出!别管蒙昧者们叫嚷:“下边永远是地狱!”
博客主页:KC老衲爱尼姑的博客主页
博主的github,平常所写代码皆在于此
共勉:talk is cheap, show me the code
作者是爪哇岛的新手,水平很有限,如果发现错误,一定要及时告知作者哦!感谢感谢!
文章目录
- 函数式编程
- 概念
- 函数式接口
- 常见的函数式接口
- Lambda表达式
- 概述
- 示例一
- 示例二
- 示例三
- 示例四
- 示例五
- Lambda省略规则
- Stream流
- 常规操作
- 1. 创建流
- (1)单列集合:集合对象.stream()
- (2)数组:Arrays.stream(数组)或者使用Stream.of()来创建
- (3)双列集合:双列集合无法直接转成流必须转成单列集合在创建流
- 2. 中间操作
- filter
- map
- distinct
- sorted
- limit
- skip
- flatMap
- concat
- forEach
- count
- max&min
- collect
- 3. 查找与匹配
- anyMatch
- allMatch
- noneMatch
- findFirst
- reduce
- 4. 函数式接口中默认方法
- 1. and
- 2. or
- 3. negate
- 注意事项
- 高级用法
- 流元素类型转换
- 并行流
函数式编程
概念
面向对象是需要关注用什么对象去解决什么问头。而函数式编程,“它是一种使用函数进行编程的方式”,它关注是数据进行了什么操作。
函数式接口
要了解Lambda表达式,首先需要了解什么是函数式接口,函数式接口定义:一个接口有且只有一个抽象方法 。
注意:
-
如果一个接口只有一个抽象方法,那么该接口就是一个函数式接口
-
如果我们在某个接口上声明了 @FunctionalInterface 注解,那么编译器就会按照函数式接口的定义来要求该接口,这样如果有两个抽象方法,程序编译就会报错的。所以,从某种意义上来说,只要你保证你的接口中只有一个抽象方法,你可以不加这个注解。加上就会自动进行检测的。
常见的函数式接口
-
Consumer 消费接口
根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数进行消费。
-
Function 计算转换接口
根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数计算或转换,把结果返回
-
Predicate 判断接口
根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中对传入的参数条件判断,返回判断结果
-
Supplier 生产型接口
根据其中抽象方法的参数列表和返回值类型知道,我们可以在方法中创建对象,把创建好的对象返回
Lambda表达式
概述
Lambda表达式是Java SE 8中一个重要的新特性。lambda表达式允许你通过表达式来代替功能接口。 lambda表达式就和方法一样,它提供了一个正常的参数列表和一个使用这些参数的主体(body,可以是一个表达式或一个代码块)。 Lambda 表达式(Lambda expression),基于数学中的λ演算得名,也可称为闭包(Closure) 。
Lambda表达式的语法
基本语法: (parameters) -> expression 或 (parameters) ->{ statements; }
Lambda表达式由三部分组成:
-
paramaters:类似方法中的形参列表,这里的参数是函数式接口里的参数。这里的参数类型可以明确的声明也可不声明而由JVM隐含的推断。另外当只有一个推断类型时可以省略掉圆括号。
-
->:可理解为“被用于”的意思
-
方法体:可以是表达式也可以代码块,是函数式接口里方法的实现。代码块可返回一个值或者什么都不反回,这里的代码块块等同于方法的方法体。如果是表达式,也可以返回一个值或者什么都不反回。
Lambda基本使用
示例一
我们使用匿名内部类的方式创建并启动线程:
public class Demo {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Hello World");
}
}).start();
}
}
可以使用Lambda的格式对其进行修改。修改后如下
public class Demo {
public static void main(String[] args) {
new Thread(() -> System.out.println("Hello World")).start();
}
}
可以简化的匿名内部类必须是函数式接口,函数式接口就是被@FunctionalInterface修饰的接口,同时该接口只有一个抽象方法。
示例二
使用IntBinaryOperator接口计算两个数的和,还是先试用匿名内部类的方式
import java.util.function.IntBinaryOperator;
public class Demo2 {
public static int calculateNum(IntBinaryOperator operator){
int a = 10;
int b = 20;
return operator.applyAsInt(a, b);
}
public static void main(String[] args) {
int i = calculateNum(new IntBinaryOperator() {
@Override
public int applyAsInt(int left, int right) {
return left + right;
}
});
System.out.println(i);//30
}
}
Lambda写法:
public class Demo2 {
public static int calculateNum(IntBinaryOperator operator){
int a = 10;
int b = 20;
return operator.applyAsInt(a, b);
}
public static void main(String[] args) {
int i = calculateNum((left, right) -> left + right);
System.out.println(i);
}
}
示例三
使用IntPredicate接口判读数字是否是偶数,先使用匿名内部类的写法调用该方法。
import java.util.function.IntPredicate;
public class Demo3 {
public static void printNum(IntPredicate predicate){
int[] arr = {1,2,3,4,5,6,7,8,9,10};
for (int i : arr) {
if(predicate.test(i)){//判断是否是偶数
System.out.println(i);//是的话直接打印
}
}
}
public static void main(String[] args) {
//允许你定义并传递一个接受整数参数并返回布尔值的函数IntPredicate
printNum(new IntPredicate() {
@Override
public boolean test(int value) {
return value%2==0;
}
});
}
}
Lambda写法:
import java.util.function.IntPredicate;
public class Demo3 {
public static void printNum(IntPredicate predicate){
int[] arr = {1,2,3,4,5,6,7,8,9,10};
for (int i : arr) {
if(predicate.test(i)){//判断是否是偶数
System.out.println(i);//是的话直接打印
}
}
}
public static void main(String[] args) {
//允许你定义并传递一个接受整数参数并返回布尔值的函数IntPredicate
printNum(value -> value%2==0);
}
}
示例四
使用Function接口将字符串转成成Integer类型,IntConsumer,先使用匿名内部类的写法调用该方法。
import java.util.function.Function;
public class Demo4 {
public static <R> R typeConver(Function<String,R> function){
String str = "1235";
R result = function.apply(str);
return result;
}
public static void main(String[] args) {
Integer result = typeConver(new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.valueOf(s);
}
});
System.out.println(result);
}
}
Lambda写法:
import java.util.function.Function;
public class Demo4 {
//Function用于表示一个接受一个参数并返回一个结果的函数
public static <R> R typeConver(Function<String,R> function){
String str = "1235";
R result = function.apply(str);
return result;
}
public static void main(String[] args) {
Integer result = typeConver(s -> Integer.valueOf(s));
System.out.println(result);
}
}
示例五
使用IntConsumer接口打印数组,该接口用于接收一个整数并不做任何处理直接返回,先使用匿名内部类的写法调用该方法。
import java.util.function.IntConsumer;
public class Demo5 {
public static void foreachArr(IntConsumer consumer){
int[] arr = {1,2,3,4,5,6,7,8,9,10};
for (int i : arr) {
consumer.accept(i);
}
}
public static void main(String[] args) {
foreachArr(new IntConsumer() {
@Override
public void accept(int value) {
System.out.println(value);
}
});
}
}
Lambda写法:
import java.util.function.IntConsumer;
public class Demo5 {
public static void foreachArr(IntConsumer consumer){
int[] arr = {1,2,3,4,5,6,7,8,9,10};
for (int i : arr) {
consumer.accept(i);
}
}
public static void main(String[] args) {
foreachArr(value -> System.out.println(value));
}
}
通过上述案例我们可以得到下面的省略规则
Lambda省略规则
-
参数类型可以省略
-
方法体只有一句代码时大括号return和唯一一句代码的分号可以省略
-
方法只有一个参数时小括号可以省略
这些规则可以不记忆,因为idea非常强大,我们不必一步到位写出Lambda简化后的某些匿名内部类,可以先写出匿名内部类,然后用Alt+enter快捷键直接改成Lambda的版本。
Stream流
概述
Stream是Java8 引入的新成员,它可以被用于处理集合或数组进行链状式的操作,可以更加方便的对数组或集合进行操作。我们可以将流想象成工厂里面的流水线,工人只需不断对流水线上过来的东西进行一一系列的处理,当走完了这条线东西就出来了。Stream流处理集合或数组如下图所示
为了后面的类中的代码更加简洁,在此引入lombok
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.16</version>
</dependency>
</dependencies>
案例数组准备
我们准备两个类分别是作者和书籍
Author类
import lombok.Data;
import java.awt.print.Book;
import java.util.List;
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Author {
//id
private Long id;
//姓名
private String name;
//年龄
private Integer age;
//简介
private String intro;
//作品
private List<Book> books;
}
Book类
import lombok.Data;
@Data
@AllArgsConstructor
@NoArgsConstructor
@EqualsAndHashCode
public class Book {
//id
private Long id;
//书名
private String name;
//分类
private String category;
//评分
private Integer score;
//简介
private String intro;
}
构造数据
使用该方法构造出一个存储Author的List集合
private static List<Author> getAuthors() {
//数据初始化
Author author = new Author(1L,"蒙多",33,"一个从菜刀中明悟哲理的祖安人",null);
Author author2 = new Author(2L,"亚拉索",15,"狂风也追逐不上他的思考速度",null);
Author author3 = new Author(3L,"易",14,"是这个世界在限制他的思维",null);
Author author4 = new Author(3L,"易",14,"是这个世界在限制他的思维",null);
//书籍列表
List<Book> books1 = new ArrayList<>();
List<Book> books2 = new ArrayList<>();
List<Book> books3 = new ArrayList<>();
books1.add(new Book(1L,"刀的两侧是光明与黑暗","哲学,爱情",88,"用一把刀划分了爱恨"));
books1.add(new Book(2L,"一个人不能死在同一把刀下","个人成长,爱情",99,"讲述如何从失败中明悟真理"));
books2.add(new Book(3L,"那风吹不到的地方","哲学",85,"带你用思维去领略世界的尽头"));
books2.add(new Book(3L,"那风吹不到的地方","哲学",85,"带你用思维去领略世界的尽头"));
books2.add(new Book(4L,"吹或不吹","爱情,个人传记",56,"一个哲学家的恋爱观注定很难把他所在的时代理解"));
books3.add(new Book(5L,"你的剑就是我的剑","爱情",56,"无法想象一个武者能对他的伴侣这么的宽容"));
books3.add(new Book(6L,"风与剑","个人传记",100,"两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
books3.add(new Book(6L,"风与剑","个人传记",100,"两个哲学家灵魂和肉体的碰撞会激起怎么样的火花呢?"));
author.setBooks(books1);
author2.setBooks(books2);
author3.setBooks(books3);
author4.setBooks(books3);
List<Author> authorList = new ArrayList<>(Arrays.asList(author,author2,author3,author4));
return authorList;
}
常规操作
1. 创建流
(1)单列集合:集合对象.stream()
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();
}
}
(2)数组:Arrays.stream(数组)或者使用Stream.of()来创建
public class Demo6 {
public static void main(String[] args) {
Integer [] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream1 = Arrays.stream(arr);
Stream<Integer> arr1 = Stream.of(arr);
}
}
(3)双列集合:双列集合无法直接转成流必须转成单列集合在创建流
public class Demo6 {
public static void main(String[] args) {
Map<String,Integer> map = new HashMap<String,Integer>();
map.put("张三",20);
map.put("李四",10);
map.put("王五",28);
Stream<Map.Entry<String, Integer>> stream = map.entrySet().stream();
}
}
2. 中间操作
中间方法指的是调用完成后会返回新的Stream流,可以继续使用(支持链式编程)。
可以对流中的元素进行条件过滤,符合条件的继续留在流中
比如,打印出姓名长度大于1的作家的姓名
import java.util.*;
import java.util.stream.Stream;
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.stream().filter(author -> author.getName().length()>1)
.forEach(author -> System.out.println(author.getName()));
}
}
运行结果:
map
对元素进行加工,并返回对应的新流
比如:打印所有作家的姓名
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.stream().map(author -> author.getName())
.forEach(name -> System.out.println(name));
}
}
运行结果:
distinct
去除重复的元素
比如:打印所有作家的姓名,并且要求其中不能有重复元素。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.stream().distinct().forEach(Author-> System.out.println(Author.getName()));
}
}
运行结果:
sorted
可以对流中的元素进行排序。
比如:对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
//升序
authors.stream().sorted((o1,o2)->o1.getAge()-o2.getAge())
.forEach(author-> System.out.println(author.getAge()));
}
}
运行结果:
注意:如果调用空参的sorted()方法,需要流中的元素是实现了Comparable。
limit
可以设置流的最大长度,超出的部分将被抛弃。
比如:对流中的元素按照年龄进行降序排序,并且要求不能有重复的元素,然后打印其中年龄最大的两个作家的姓名。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.stream().distinct().sorted((o1, o2) -> o2.getAge()-o1.getAge())
.limit(2).forEach(author-> System.out.println(author.getName()));
}
}
运行结果:
skip
跳过流中的前n个元素,返回剩下的元素
比如:打印除了年龄最大的作家外的其他作家,要求不能有重复元素,并且按照年龄降序排序。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.stream().distinct().sorted((o1, o2) -> o2.getAge()-o1.getAge())
.skip(1).
forEach(author-> System.out.println(author.getName()));
}
}
运行结果:
flatMap
map只能把一个对象转换成另一个对象来作为流中的元素。而flatMap可以把一个对象转换成多个对象作为流中的元素。
例一:打印所有书籍的名字。要求对重复的元素进行去重。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
//将每个作者的作品提取出来转换成新的流
authors.stream().flatMap(author->author.getBooks().stream())
.distinct().forEach(book-> System.out.println(book.getName()));
}
}
运行结果:
concat
合并a和b两个流为一个流
比如,将两个整数流合并成一个流并输出
public class Demo6 {
public static void main(String[] args) {
Stream<Integer> stream1 = Stream.of(1, 2, 3);
Stream<Integer> stream2 = Stream.of(4, 5, 6);
Stream<Integer> combinedStream = Stream.concat(stream1, stream2);
combinedStream.forEach(System.out::println); // 输出 1, 2, 3, 4, 5, 6
}
}
- 终结操作
forEach
对流中的元素进行遍历操作,我们通过传入的参数去指定对遍历到的元素进行什么具体操作。
比如:输出所有作家的名字
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.stream().map(author->author.getName()).distinct().forEach(name -> System.out.println(name));
}
}
运行结果:
count
可以用来获取当前流中元素的个数。
比如:打印这些作家的所出书籍的数目,注意删除重复元素。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
long count = authors.stream().flatMap(author -> author.getBooks().stream()).distinct().count();
System.out.println(count);
}
}
运行结果:
max&min
可以用来或者流中的最值。
比如:分别获取这些作家的所出书籍的最高分和最低分并打印。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Optional<Integer> max = authors.stream().flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.max((score1, score2) -> score1 - score2);
Optional<Integer> min = authors.stream().flatMap(author -> author.getBooks().stream())
.map(book -> book.getScore())
.min((score1, score2) -> score1 - score2);
System.out.println("max:"+max.get());
System.out.println("min:"+min.get());
}
}
运行结果;
collect
把当前流转换成一个集合。
比如:获取一个存放所有作者名字的List集合
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
List<String> collect = authors.stream().map(author -> author.getName())
.distinct()
.collect(Collectors.toList());
System.out.println(collect);
}
}
比如:获取一个所有书名的Set集合。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Set<Book> collect = authors.stream().flatMap(author -> author.getBooks().stream())
.collect(Collectors.toSet());
System.out.println(collect);
}
}
运行结果:
比如:获取一个Map集合,map的key为作者名,value为List
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Map<String, List<Book>> map = authors.stream().distinct().
collect(Collectors.toMap(author -> author.getName(), author -> author.getBooks()));
System.out.println(map);
}
}
运行结果:
3. 查找与匹配
anyMatch
可以用来判断是否有任意符合匹配条件的元素,结果为boolean类型。
比如:判断是否有年龄在30以上的作家
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
boolean b = authors.stream().anyMatch(author -> author.getAge() > 30);
System.out.println(b);//true
}
}
allMatch
可以用来判断是否都符合匹配条件,结果为boolean类型。如果都符合结果为true,否则结果为false。
比如:判断是否所有的作家都是成年人
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
boolean b = authors.stream().allMatch(author -> author.getAge() >=18);
System.out.println(b);//false/`
}
noneMatch
可以判断流中的元素是否都不符合匹配条件。如果都不符合结果为true,否则结果为false
比如:判断作家是否都没有超过100岁的。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
boolean b = authors.stream().noneMatch(author -> author.getAge() > 100);
System.out.println(b);//true
}
}
findFirst
获取流中的第一个元素。
比如:获取一个年龄最小的作家,并输出他的姓名。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Optional<Author> first = authors.stream().sorted((o1, o2) -> o1.getAge() - o2.getAge())
.findFirst();
first.ifPresent(author -> System.out.println(author.getName()));
}
}
reduce
该方法能对流中的数据按照我们所指定的方式计算,我们需要初始化一个值,它就会按照我们指定的计算方式依次拿流中的元素和初始值进行计算.计算结果在和流后面的元素进行计算。
比如,使用reduce计算所有作者年龄的和
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Integer reduce = authors.stream()
.distinct()
.map(author -> author.getAge())
.reduce(0, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer integer, Integer integer2) {
return integer+integer2;
}
});
System.out.println(reduce);
}
}
因为是累加所以初始值设定为0.
使用reduce求所有作者中年龄的最小值
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Integer min = authors.stream()
.map(author -> author.getAge())
.reduce(Integer.MAX_VALUE, new BinaryOperator<Integer>() {
@Override
public Integer apply(Integer result, Integer element) {
return result > element ? element : result;
}
});
System.out.println(min);
}
}
4. 函数式接口中默认方法
1. and
我们在使用Predicate接口时候可能需要进行判断条件的拼接。而and方法相当于是使用&&来拼接两个判断条件
例如:打印作家中年龄大于17并且姓名的长度大于1的作家。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();
stream.filter(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getAge()>17;
}
}.and(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getName().length()>1;
}
})).forEach(author-> System.out.println(author));
}
}
2. or
我们在使用Predicate接口时候可能需要进行判断条件的拼接。而or方法相当于是使用||来拼接两个判断条件。
例如:打印作家中年龄大于17或者姓名的长度小于2的作家
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();
stream.filter(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getAge()>17;
}
}.or(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getName().length()<2;
}
})).forEach(author-> System.out.println(author));
}
}
3. negate
Predicate接口中的方法。negate方法相当于是在判断添加前面加了个! 表示取反
例如:打印作家中年龄不大于17的作家。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
Stream<Author> stream = authors.stream();
stream.filter(new Predicate<Author>() {
@Override
public boolean test(Author author) {
return author.getAge()>17;
}
}.negate()).forEach(author-> System.out.println(author));
}
}
注意事项
- 对流进行操作如果没有终结操,那么所有的中间过程都不会i进行操作。
- 一旦已经经过了最终的操作,那么这个流就不能再使用。
- 我们对流进行了很多中间操作,但是不会影响原来的数据。
高级用法
流元素类型转换
由于Java中很多的Stream的方法都用到 了泛型,所以方法的参数和返回值基本都是引用类型。当我们使用的时候就会涉及大量的自动装箱和自动拆箱,比如说我们对流中数据进行普通的计算(加减乘除)就会涉及到自动拆箱,返回的时候就要自动装箱,所以这就造成了一定的时间消耗,虽然时间很短,但是面对流大量的数据时候,这个时间就非常的耗时了。所以为了能对这部分时间消耗进行优化,Stream还提供了很多专门针对基本数据类型的方法。这些方法都会将流中的元素转成基本数据类型流。
例如:mapToInt,mapToLong,mapToDouble,flatMapToInt,flatMapToDouble等。
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
long start1 =System.currentTimeMillis();
authors.stream()
.map(author -> author.getAge())
.map(age -> age + 10)
.filter(age->age>18)
.map(age->age+2)
.forEach(System.out::println);
long end1 =System.currentTimeMillis();
System.out.println("转换前:"+(end1-start1));//50
long start2 =System.currentTimeMillis();
authors.stream()
.mapToInt(author -> author.getAge())
.map(age -> age + 10)
.filter(age->age>18)
.map(age->age+2)
.forEach(System.out::println);
long end2 =System.currentTimeMillis();
System.out.println("转换后:"+(end2-start2));//4
}
}
对上述的代码计算其运行时间可知,使用了mapToInt的程序比没有使用的程序运行效率高了10倍左右。
并行流
当流中有大量的元素的时候,我们可以使用并行流去提高操作效率。所谓的并行流就是充分的利用多核CPU,将任务分配给多个线程去 执行。Stream流中提供了方法正好支持并行操作。
- parallel
paralle方法可以将普通的顺序流转成并行流,适合用于在现有流的基础上进行转换。
public class Demo6 {
public static void main(String[] args) {
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
Integer sum = stream.parallel()
//打印线名称
.peek(new Consumer<Integer>() {
@Override
public void accept(Integer num) {
System.out.println(num+Thread.currentThread().getName());
}
})
.filter(num -> num > 5)
.reduce((result, ele) -> result + ele)
.get();
System.out.println(sum);
}
}
- parallelStream
parallelStream方法也可以得到并行流,更适合从集合开始获取并行流
public class Demo6 {
public static void main(String[] args) {
List<Author> authors = getAuthors();
authors.parallelStream()
.map(author -> author.getAge())
.map(age -> age + 10)
.filter(age->age>18)
.map(age->age+2)
.forEach(age -> System.out.println(age));
}
}
各位看官如果觉得文章写得不错,点赞评论关注走一波!谢谢啦!。