1. Stream 流综合练习
(1) 题目 1:筛选并转换年龄符合条件的记录为 Map
现有一个ArrayList<String>
集合,其中存储的元素均为字符串,格式统一为 "姓名,年龄"(例如"zhangsan,23"
表示姓名为zhangsan
,年龄为 23)。
请使用 Java Stream 流完成以下操作:
① 从集合中筛选出年龄大于等于 24的元素;
② 将筛选后的元素转换为Map<String, Integer>
集合,其中:
- Map 的键(Key) 为元素中的 "姓名"(字符串类型);
- Map 的值(Value) 为元素中的 "年龄"(整数类型);
③ 打印转换后的Map
集合。
已知该ArrayList
中的元素固定为:["zhangsan,23","lisi,24","wangwu,25"]
。
java
package demo3;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
public class test3 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"zhangsan,23","lisi,24","wangwu,25");
Map<String, Integer> map = list.stream()
.filter(s -> Integer.parseInt(s.split(",")[1]) >= 24)
.collect(Collectors.toMap(
s -> s.split(",")[0],
s -> Integer.parseInt(s.split(",")[1])));
System.out.println(map);
}
}
(2) 题目 2:合并处理两个列表并转换为对象集合
现有两个ArrayList<String>
集合,分别存储男性和女性信息,元素格式均为 "姓名,年龄"(例如"星期日,22"
表示姓名为星期日
,年龄为 22)。
两个集合的具体元素如下:
manList
:["星期日,22","刃,24","饮月,23","白厄,30","瓦尔特,35","万敌,25"]
womanList
:["杨颖,26","镜流,28","星,5","黑天鹅,30","飞霄,100","杨幂,20"]
请使用 Java Stream 流完成以下操作:
① 处理manList
:
- 筛选出姓名长度为 3的元素;
- 仅保留前 2 个符合条件的元素,得到流
stream1
。
② 处理womanList
:
- 筛选出姓名以 "杨" 字开头的元素;
- 跳过第一个符合条件的元素,得到流
stream2
。
③ 合并流stream1
和stream2
,并将合并后的每个元素转换为Actor
对象(Actor
类包含String name
和int age
两个属性,且有对应的构造方法public Actor(String name, int age)
)。
④ 将所有Actor
对象收集到List<Actor>
集合中,并打印该集合。
java
package demo3;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class test4 {
public static void main(String[] args) {
ArrayList<String> manList = new ArrayList<>();
ArrayList<String> womanList = new ArrayList<>();
Collections.addAll(manList,"星期日,22","刃,24","饮月,23","白厄,30","瓦尔特,35","万敌,25");
Collections.addAll(womanList,"杨颖,26","镜流,28","星,5","黑天鹅,30","飞霄,100","杨幂,20");
Stream<String> stream1 = manList.stream()
.filter(s -> s.split(",")[0].length() == 3)
.limit(2);
Stream<String> stream2 = womanList.stream()
.filter(s -> s.split(",")[0].startsWith("杨"))
.skip(1);
/* Stream.concat(stream1, stream2).map(new Function<String, Actor>() {
@Override
public Actor apply(String s) {
String name = s.split(",")[0];
int age = Integer.parseInt(s.split(",")[1]);
return new Actor(name, age);
}
}).forEach(s -> System.out.println(s));*/
List<Actor> list = Stream.concat(stream1, stream2)
.map(s -> new Actor(s.split(",")[0], Integer.parseInt(s.split(",")[1])))
.collect(Collectors.toList());
System.out.println(list);
}
}
关键逻辑 1:关于 return new Actor(name, age);
------ 为什么必须返回?
map
方法的作用是将流中的元素转换为另一种类型 (这里是将 String
转换为 Actor
)。它依赖 Function
接口的 apply
方法来完成转换,而 Function
接口的定义是:
关键逻辑 2:函数式接口的抽象方法可以有返回值,也可以没有返回值(即返回void
)
函数式接口的核心是 "有且仅有一个抽象方法",但这个方法的返回值类型是灵活的 ------ 可以是任意类型(包括基本类型、对象类型),也可以是void
(无返回值)。
① 有返回值的函数式接口(如Function<T, R>
)
Function<T, R>
的抽象方法是:
java
R apply(T t); // 接收T类型参数,返回R类型结果
它的作用是 "将 T 类型转换为 R 类型",所以必须有返回值 (否则无法完成 "转换" 的功能)。你之前代码中用的map
方法就依赖Function
,所以apply
必须返回转换后的结果(比如Actor
对象)。
② 无返回值的函数式接口(如Consumer<T>
)
Consumer<T>
的抽象方法是:
java
void accept(T t); // 接收T类型参数,无返回值
它的作用是 "消费 T 类型数据"(比如打印、修改外部状态等),不需要返回值。例如forEach
方法就依赖Consumer
,只需执行操作,不用返回结果:
java
list.stream().forEach(s -> System.out.println(s)); // accept方法无返回值
2. 方法引用
(1) 什么是方法引用?
方法引用是Lambda 表达式的简化写法 。当 Lambda 表达式的主体仅包含一个方法调用时,就可以用方法引用替代 Lambda,让代码更简洁。
- 本质:复用已有的方法,作为函数式接口的抽象方法的实现。
- 语法 :
类名/对象名::方法名
(双冒号::
是方法引用的运算符)
(2) 常见方法引用类型(重点)
根据引用的方法类型不同,分为 4 类,结合函数式接口实例理解:
① 类::静态方法(引用类的静态方法)
场景:Lambda 表达式中调用的是某个类的静态方法。
示例:用Integer
的compare
静态方法排序
java
import java.util.Arrays;
public class MethodRefDemo2 {
public static void main(String[] args) {
Integer[] nums = {3, 1, 2};
// Lambda写法:调用Integer的静态方法compare
Arrays.sort(nums, (a, b) -> Integer.compare(a, b));
// 方法引用写法:类::静态方法(Integer是类,compare是静态方法)
Arrays.sort(nums, Integer::compare);
}
}
解析:
Arrays.sort
的第二个参数是Comparator
接口(函数式接口),抽象方法为int compare(T o1, T o2)
;- Lambda
(a,b) -> Integer.compare(a,b)
的主体是调用Integer
类的静态方法compare(a,b)
; - 方法引用
Integer::compare
直接复用静态方法,简化代码。
② 类::new(引用构造器,创建对象)
场景:Lambda 表达式的主体是调用某个类的构造器创建对象。
示例:将字符串转换为Person
对象
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
// 定义Person类
class Person {
private String name;
public Person(String name) { this.name = name; }
@Override
public String toString() { return "Person{name='" + name + "'}"; }
}
public class MethodRefDemo4 {
public static void main(String[] args) {
List<String> nameList = new ArrayList<>();
Collections.addAll(nameList, "张三", "李四");
// Lambda写法:调用Person的构造器创建对象
List<Person> personList1 = nameList.stream()
.map(name -> new Person(name))
.collect(Collectors.toList());
// 方法引用写法:类::new(Person是类,new是构造器)
List<Person> personList2 = nameList.stream()
.map(Person::new) // 等价于name -> new Person(name)
.collect(Collectors.toList());
System.out.println(personList2); // [Person{name='张三'}, Person{name='李四'}]
}
}
解析:
map
的参数是Function
接口,抽象方法为R apply(T t)
;- Lambda
name -> new Person(name)
的主体是调用Person
的构造器(参数为name
); - 方法引用
Person::new
直接复用构造器,简化对象创建逻辑。
③ 类::实例方法(引用类的实例方法,特殊场景)
场景:Lambda 的第一个参数是方法的调用者,第二个参数是方法的参数(或无参数)。
java
import java.util.Arrays;
public class MethodRefDemo3 {
public static void main(String[] args) {
String[] names = {"张三", "李四", "王五"};
// Lambda写法:调用第一个参数s1的compareTo方法,参数是s2
Arrays.sort(names, (s1, s2) -> s1.compareTo(s2));
// 方法引用写法:类::实例方法(String是类,compareTo是实例方法)
Arrays.sort(names, String::compareTo);
}
}
解析:
Comparator
的compare
方法有两个参数:(s1, s2)
;- Lambda 中
s1.compareTo(s2)
的调用者是第一个参数s1
,参数是第二个参数s2
; - 满足 "第一个参数是调用者,第二个参数是方法参数",因此可用
String::compareTo
替代。
④ 类型[ ]::new
数组的构造器需要接收一个int 类型的参数 (数组的长度),并返回一个对应类型的数组(如String[]
、Integer[]
)。因此,它只能匹配 **IntFunction<T[]>
** 函数式接口:
java
@FunctionalInterface
public interface IntFunction<T[]> {
T[] apply(int length); // 接收数组长度,返回T类型的数组
}
当 Lambda 表达式的逻辑是 "根据一个 int 类型的长度,创建对应类型的数组" 时,可直接用类型[]::new
替代。最典型的场景是 Stream 流的toArray
方法(将流转换为数组时)。
将 Stream 流中的元素转换为String[]
数组
java
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ArrayConstructorRef {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
Collections.addAll(list, "a", "b", "c");
// 1. Lambda表达式写法:根据长度创建String数组
String[] arr1 = list.stream()
.toArray(length -> new String[length]); // length是流中元素的数量
// 2. 数组构造方法引用写法:简化Lambda
String[] arr2 = list.stream()
.toArray(String[]::new); // 等价于length -> new String[length]
System.out.println(arr1.length); // 3
System.out.println(arr2.length); // 3
}
}
解析:
- 流中元素数量为 3,
toArray
方法会将这个数量(3)作为参数传给apply
方法; String[]::new
本质是引用String
数组的构造器,接收长度 3,创建new String[3]
数组;- 两种写法完全等价,但方法引用更简洁。
(3) 综合练习
题目 1:学生信息转换与输出
请完成以下编程任务:
① 定义一个Student
类,包含私有属性name
(字符串类型,存储姓名)和age
(整数类型,存储年龄)。
② 为Student
类编写一个构造方法,参数为String
类型的字符串,该字符串格式为 "姓名,年龄"(例如 "张无忌,15")。构造方法需要将该字符串拆分,分别为name
和age
属性赋值(提示:可使用split(",")
方法拆分字符串)。
③ 重写Student
类的toString()
方法,使其返回格式为"Student{name='XXX', age=XX}"
的字符串(其中 XXX 为姓名,XX 为年龄)。
④ 创建一个test2
类,在其main
方法中完成以下操作:
- 初始化一个
ArrayList<String>
集合,并向集合中添加以下元素:"张无忌,15"
、"周芷若,14"
、"赵敏,13"
、"张强,20"
、"张良,35"
。 - 使用 Stream 流将上述集合转换为
Student[]
数组(要求使用map(Student::new)
进行转换,并使用toArray(Student[]::new)
转换为数组)。 - 打印转换后的
Student
数组(使用Arrays.toString()
方法)。
预期输出 :[Student{name='张无忌', age=15}, Student{name='周芷若', age=14}, Student{name='赵敏', age=13}, Student{name='张强', age=20}, Student{name='张良', age=35}]
java
package demo1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
public class test2 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<String>();
Collections.addAll(list,"张无忌,15","周芷若,14","赵敏,13","张强,20","张良,35");
Student[] arr = list.stream().map(Student::new).toArray(Student[]::new);
System.out.println(Arrays.toString(arr));
}
}
关键逻辑 1:map(Student::new)
:转换元素类型
① map()
是流的中间操作,作用是将流中的每个元素按照指定规则转换为另一种类型。
② 这里的Student::new
是构造方法引用 ,等价于一个 lambda 表达式:(String s) -> new Student(s)
③ 意思是:对流中的每个字符串元素(比如"张无忌,15"
),调用Student
类的构造方法(参数为这个字符串),创建一个Student
对象。
④ 经过map()
操作后,流的类型从Stream<String>
(存储字符串)变成了Stream<Student>
(存储Student
对象)。
关键逻辑 2:toArray(Student[]::new)
:转换为数组
① toArray()
是流的终端操作,作用是将流中的元素收集到一个数组中。
② 这里的Student[]::new
是数组构造器引用 ,等价于一个 lambda 表达式:(int length) -> new Student[length]
③ 意思是:告诉toArray()
方法,需要创建一个Student
类型的数组,数组长度由流中元素的数量决定,最终将流中的Student
对象存入这个数组。
关键逻辑 3:为什么使用 Arrays.toString(arr)
在 Java 中,当我们直接打印一个数组(比如System.out.println(arr)
)时,得到的结果并不是数组中元素的具体内容,而是数组的 "内存地址标识"(类似[Ldemo1.Student;@1b6d3586
这样的字符串),这是因为数组的默认toString()
方法(继承自Object
类)就是这么实现的,它无法直观展示数组中的元素。
题目 2:提取学生姓名并转换为数组
请完成以下编程任务:
① 定义一个Student
类,包含私有属性name
(字符串类型,姓名)和age
(整数类型,年龄)。
② 为Student
类编写一个带参构造方法,参数为name
和age
,用于初始化属性。
③ 为Student
类编写getName()
方法(getter 方法),用于获取name
属性的值。
④ 创建test3
类,在其main
方法中完成以下操作:
- 初始化一个
ArrayList<Student>
集合,并添加 3 个Student
对象:new Student("zhangsan",23)
、new Student("lisi",24)
、new Student("wangwu",25)
。 - 使用 Stream 流将上述集合中的
Student
对象转换为只包含姓名的String[]
数组(要求使用map(Student::getName)
提取姓名,并使用toArray(String[]::new)
转换为数组)。 - 打印转换后的字符串数组(使用
Arrays.toString()
方法)。
预期输出 :[zhangsan, lisi, wangwu]
java
package demo1;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.function.Function;
public class test3 {
public static void main(String[] args) {
ArrayList<Student> list = new ArrayList<>();
list.add(new Student("zhangsan",23));
list.add(new Student("lisi",24));
list.add(new Student("wangwu",25));
String[] arr = list.stream().map(Student::getName).toArray(String[]::new);
System.out.println(Arrays.toString(arr));
}
}