Java 黑马程序员学习笔记(进阶篇20)

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

③ 合并流stream1stream2,并将合并后的每个元素转换为Actor对象(Actor类包含String nameint 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 表达式中调用的是某个类的静态方法。

示例:用Integercompare静态方法排序
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)
  • Lambdaname -> 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); 
    }
}

解析

  • Comparatorcompare方法有两个参数:(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")。构造方法需要将该字符串拆分,分别为nameage属性赋值(提示:可使用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类编写一个带参构造方法,参数为nameage,用于初始化属性。

③ 为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));
    }
}
相关推荐
腻害兔2 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社3 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
克里斯蒂亚诺更新3 小时前
ruoyi角色权限的对应关系
笔记
萧瑟余晖4 小时前
JDK 26 新特性详解
java·开发语言
马优晨5 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
a1117766 小时前
2FA 验证码生成器(github登录验证 app)
笔记·学习
我的xiaodoujiao6 小时前
API 接口自动化测试详细图文教程学习系列32--Allure测试报告2
python·学习·测试工具·pytest
维天说7 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
破碎的南瓜7 小时前
sqli--sql注入过关笔记(1,2,3,4,5,9)
数据库·笔记·sql
Zane19948 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端