1. 不可变集合
① 不可变集合的创建方式
(1) List/Set/Map.of()
静态方法
① JDK 9 新增的of()
方法直接创建真正的不可变集合,原集合不存在,无法被修改,且有严格校验规则。
② 核心特点:
- 不允许添加
null
元素(否则抛NullPointerException
)。 Set/Map
不允许添加重复元素(否则抛IllegalArgumentException
)。- 集合大小固定,调用
add()
/remove()
/clear()
等修改方法直接报错。
java
import java.util.List;
import java.util.Map;
import java.util.Set;
public class ImmutableDemo2 {
public static void main(String[] args) {
// 1. 创建不可变List(支持1~10个元素,超过需用of(...)多参数)
List<String> immutableList = List.of("Java", "MySQL", "Redis");
// 2. 创建不可变Set(禁止重复元素)
// Set<String> immutableSet = Set.of("Java", "Java"); // 报错:重复元素
// 3. 创建不可变Map(支持1~10个键值对)
Map<String, Integer> immutableMap = Map.of("Java", 100, "MySQL", 90);
// 4. 尝试修改:直接报错(UnsupportedOperationException)
// immutableList.add("Spring");
// immutableMap.put("Redis", 85);
}
}
(2) Map.ofEntries()
(处理多键值对 Map)
当Map
的键值对超过 10 个时,Map.of()
参数不够用,需用Map.ofEntries()
配合Map.entry()
创建。
java
package demo1;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class test5 {
public static void main(String[] args) {
HashMap<String,String> hm = new HashMap<>();
hm.put("张三","南京");
hm.put("李四","北京");
hm.put("王五","上海");
hm.put("赵六","北京");
hm.put("孙七","深圳");
Set<Map.Entry<String, String>> entries = hm.entrySet();
Map.Entry[] arr1 = new Map.Entry[0];
Map.Entry[] arr2 = entries.toArray(arr1);
Map map = Map.ofEntries(arr2);
}
}
关键逻辑 1:toArray(new Map.Entry[0])
做了什么?
① toArray()
是 Collection
接口中的方法,作用是 将集合中的元素转换为数组。它有两个常用重载形式:
- 无参
toArray()
:返回Object[]
类型的数组(通用但需要强转,不够方便)。 - 带参
toArray(T[] a)
:传入一个指定类型的数组,返回 该类型的数组(更安全,无需强转)。
② 你的代码中用的是第二种:entries.toArray(new Map.Entry[0])
,具体逻辑如下:
- 传入的参数是
new Map.Entry[0]
:一个长度为 0 的Map.Entry
类型数组。这个参数的核心作用是 告诉方法 "我需要返回一个Map.Entry
类型的数组"(指定数组类型)。
- 方法执行后:会创建一个 新的
Map.Entry
数组 ,数组的长度等于entries
集合的大小(你的例子中是 5),数组中的元素就是entries
集合中的所有键值对(Entry
对象)。
简单说:这句话的作用是 把 entries
集合(包含所有键值对)转换成一个 Map.Entry
类型的数组。
关键逻辑 2:为什么参数是 new Map.Entry[0]
(长度为 0)?
这是 Java 中一种高效的写法:
- 当传入的数组长度 小于集合大小 时,
toArray
会自动创建一个 长度等于集合大小 的新数组(类型和传入的一致)。 - 若传入的数组长度 大于等于集合大小 ,则直接使用该数组(多余位置填
null
),但会浪费空间。
所以传一个长度为 0 的数组,既明确了返回类型,又不会浪费空间,是推荐的写法。
关键逻辑 3:Map.Entry
是什么?
① Map
是存储键值对(key-value
)的集合,但 Map
本身不能直接遍历键值对,而是通过 entrySet()
方法返回一个包含所有键值对的 Set
集合,这个集合中的每个元素都是 Map.Entry
类型的对象。
② Map.Entry
接口定义了操作键值对的方法,比如:
getKey()
:获取当前键值对的key
;getValue()
:获取当前键值对的value
;setValue(V value)
:修改当前键值对的value
(仅可变Map
支持)。
③ Map.Entry
数组是什么?
当我们用 toArray(new Map.Entry[0])
转换集合时,得到的就是一个 Map.Entry
类型的数组 。数组中的每个元素对应 Map
中的一个键值对,例如:
你的代码中 HashMap
有 5 个键值对,转换后的 Map.Entry
数组长度为 5,数组的第 0 个元素可能是 (张三, 南京)
,第 1 个是 (李四, 北京)
,以此类推。
2. Stream 流
① Stream 流的核心概念
(1) 本质:对集合 / 数组的元素进行 "流水线式处理" 的工具(不存储数据,也不改变原集合)。
(2) 特点:
- **一次性:**流只能被 "消费" 一次(执行终结操作后,流就关闭了)。
- **惰性执行:**中间操作不会立即执行,直到调用终结操作时才统一处理。
- **链式调用:**中间操作返回流对象,可连续调用多个方法。
② Stream 流的操作步骤
三步曲:获取流 → 中间操作(可多个) → 终结操作(仅一个)
java
// 伪代码示例
集合/数组.获取流()
.中间操作1() // 过滤、转换等
.中间操作2() // 排序、去重等
.终结操作(); // 遍历、收集结果等
③ 第一步:获取 Stream 流
根据数据源(集合 / 数组)的不同,获取流的方式也不同:
(1) 从单列集合获取流(List/Set)
调用集合的stream()
方法:
java
List<String> list = new ArrayList<>();
Stream<String> stream1 = list.stream(); // List获取流
Set<Integer> set = new HashSet<>();
Stream<Integer> stream2 = set.stream(); // Set获取流
(2) 从双列集合获取流(Map)
Map 没有直接的stream()
方法,需先转成单列集合(keySet()
/values()
/entrySet()
)再获取流:
java
Map<String, Integer> map = new HashMap<>();
// 方式1:获取key的流
Stream<String> keyStream = map.keySet().stream();
// 方式2:获取value的流
Stream<Integer> valueStream = map.values().stream();
// 方式3:获取键值对(Entry)的流
Stream<Map.Entry<String, Integer>> entryStream = map.entrySet().stream();
(3) 从数组获取流
java
Stream<Integer> numStream = Stream.of(1, 2, 3, 4);
④ 第二步:中间操作(流水线加工)
中间操作返回的是Stream
对象,支持链式调用,常用操作如下:
(1) 过滤(filter()
)
根据条件保留元素(参数是Predicate
函数式接口,接收元素返回布尔值)。
java
List<String> list = Arrays.asList("张三", "李四", "张三丰", "王五");
// 过滤出名字长度为2的元素
list.stream()
.filter(name -> name.length() == 2) // 中间操作:过滤
.forEach(System.out::println); // 终结操作:打印
// 输出:张三、李四、王五
(2) 限制数量(limit(n)
)
保留前n
个元素。
java
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream()
.limit(3) // 只保留前3个
.forEach(System.out::println); // 输出:1、2、3
(3) 跳过元素(skip(n)
)
跳过前n
个元素(若元素总数≤n,返回空流)。
java
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
list.stream()
.skip(2) // 跳过前2个
.forEach(System.out::println); // 输出:3、4、5
(4) 去重(distinct()
)
根据元素的equals()
方法去重(注意:自定义对象需重写equals()
和hashCode()
)。
java
ArrayList<String> list1 = new ArrayList<>();
Collections.addAll(list1,"张无忌","张无忌","张强","张三丰","谢广坤","星期日","张无忌","张强");
list1.stream().distinct().forEach(s -> System.out.println(s)); // 只保留一个张无忌
(5) 映射(map()
)
将元素转换为另一种类型(参数是Function
函数式接口,接收元素返回新元素)。
题目:提取人员信息中的年龄并打印
① 现有一个存储人员信息的 ArrayList 集合,集合中每个元素的格式为 "姓名 - 年龄"(例如 "张无忌 - 15" 表示姓名为张无忌,年龄为 15)。
② 请使用 Java 的 Stream 流,通过 map 操作提取每个元素中的年龄(转换为整数类型),并打印所有年龄。
③ 已知集合中的元素为:["张无忌-15", "周芷若-14", "赵敏-13", "张强-20", "张三丰-100", "张良-35"]
。
说明:
- 需使用 Stream 流的 map 方法实现字符串到整数的转换;
- 最终输出结果为每个人员的年龄(整数),每行打印一个年龄。
java
package demo2;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Function;
public class test6 {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
Collections.addAll(list,"张无忌-15","周芷若-14","赵敏-13","张强-20","张三丰-100","张良-35");
list.stream().map(new Function<String,Integer>(){
@Override
public Integer apply(String s) {
String[] arr = s.split("-");
String ageString = arr[1];
Integer age = Integer.parseInt(ageString); // 不太理解
return age;
}
}).forEach(s-> System.out.println(s));
System.out.println("---------------------");
list.stream()
.map(s->Integer.parseInt(s.split("-")[1])) // 不太理解
.forEach(s-> System.out.println(s));
}
}
第一部分:map
匿名内部类的实现(详细步骤)
java
list.stream().map(new Function<String,Integer>(){
@Override
public Integer apply(String s) {
// s 代表集合中的每个元素,比如 "张无忌-15"、"周芷若-14" 等
String[] arr = s.split("-"); // 步骤1:按 "-" 分割字符串
String ageString = arr[1]; // 步骤2:取分割后数组的第2个元素(年龄的字符串形式)
Integer age = Integer.parseInt(ageString); // 步骤3:把字符串转成整数
return age; // 返回转换后的年龄整数
}
})
关键逻辑 1:String[] arr = s.split("-")
① split("-")
是字符串的分割方法,作用是按 "-"
把字符串拆成一个数组。
② 例如:
- 当
s = "张无忌-15"
时,split("-")
会返回["张无忌", "15"]
(一个长度为 2 的数组); - 当
s = "张三丰-100"
时,返回["张三丰", "100"]
。
关键逻辑 2:String ageString = arr[1]
数组的索引从 0 开始:
arr[0]
是分割后的第一个元素(姓名,如"张无忌"
);arr[1]
是分割后的第二个元素(年龄的字符串形式,如"15"
、"100"
)。这行代码的作用是提取出年龄的字符串(比如"15"
)。
关键逻辑 3:Integer age = Integer.parseInt(ageString)
Integer.parseInt(字符串)
是将 "数字格式的字符串" 转换成 "整数" 的方法。例如:
- 把
"15"
转成整数15
; - 把
"100"
转成整数100
。这行代码完成了 "字符串→整数" 的类型转换。
第二部分:Lambda 简化版(浓缩步骤)
java
list.stream()
.map(s->Integer.parseInt(s.split("-")[1])) // 简化写法
.forEach(s-> System.out.println(s));
这行代码是上面匿名内部类的 简化版(利用 Lambda 表达式省略了冗余语法),逻辑完全一致,只是把三步合并成了一行:
s.split("-")[1]
:直接通过链式调用,先分割字符串,再取索引 1 的元素(年龄字符串,如"15"
);Integer.parseInt(...)
:直接把上面得到的年龄字符串转成整数;- 整体
s -> ...
是 Lambda 表达式,代替了Function
接口的匿名内部类实现。
⑤ 第三步:终结操作(获取结果)
终结操作会触发流的处理,并返回一个非Stream
的结果(如void
、long
、List
等),常用操作如下:
(1) 遍历(forEach()
)
逐个处理元素(参数是Consumer
函数式接口)。
- 作用:遍历流中每个元素,对元素执行自定义操作。
- 依赖接口 :
Consumer<T>
函数式接口(抽象方法void accept(T t)
,接收元素但无返回值)。
java
ArrayList<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.stream()
.forEach(s -> System.out.println(s.toUpperCase())); // 转大写后打印
// 输出:
// A
// B
// C
(2) 计数(count
)
返回流中元素的个数(long
类型)。
java
public class StreamCount {
public static void main(String[] args) {
List<Integer> numList = new ArrayList<>();
numList.add(1);
numList.add(2);
numList.add(3);
// 统计元素数量
long count = numList.stream().count();
System.out.println("元素个数:" + count); // 3
}
}
// 输出:元素个数:3
(3) toArray(IntFunction<A[]> generator)
推荐用法(带参数)
基础转换(直接将 List 元素转为 String 数组)
java
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class StreamToArrayLambda1 {
public static void main(String[] args) {
// 初始化ArrayList
List<String> list = new ArrayList<>();
list.add("Java");
list.add("Stream");
list.add("Lambda");
// 使用lambda形式的toArray转换为String数组
String[] arr = list.stream()
.toArray(value -> new String[value]); // 核心:根据长度创建String数组
// 打印数组(使用Arrays.toString())
System.out.println(Arrays.toString(arr)); // [Java, Stream, Lambda]
}
}
解释 :流中共有 3 个元素,value
的值就是 3,new String[3]
创建长度为 3 的数组,最终将元素填入数组。
(4) collect(收集流为集合)
java
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class StreamCollect {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张三");
list.add("李四");
list.add("张三");
list.add("王五");
// 收集为List(可重复)
List<String> listResult = list.stream()
.filter(name -> name.length() == 2) // 过滤出2字名
.collect(Collectors.toList());
System.out.println("List结果:" + listResult); // [张三, 李四, 张三, 王五]
// 收集为Set(去重)
Set<String> setResult = list.stream()
.collect(Collectors.toSet());
System.out.println("Set结果:" + setResult); // [张三, 李四, 王五]
}
}
// 输出:
// List结果:[张三, 李四, 张三, 王五]
// Set结果:[张三, 李四, 王五]
第一段代码:Map<String, Integer> map
java
Map<String, Integer> map = list.stream()
.filter(s -> "男".equals(s.split("-")[1])) // 过滤出性别为"男"的元素
.collect(Collectors.toMap(
// 第一个参数:keyMapper(键映射器)------ 从元素中提取Map的"键"
new Function<String, String>() {
@Override
public String apply(String s) {
return s.split("-")[0]; // 键 = 姓名(分割后索引0的值)
}
},
// 第二个参数:valueMapper(值映射器)------ 从元素中提取Map的"值"
new Function<String, Integer>() {
@Override
public Integer apply(String s) {
return Integer.parseInt(s.split("-")[2]); // 值 = 年龄(转换为Integer类型)
}
}
));
关于 Function
匿名内部类:
new Function<String, String>()
是显式实现 Function
接口的匿名内部类:
Function<T, R>
中的T
是输入类型(这里是流中的字符串String
);R
是输出类型(第一个Function
输出键的类型String
,第二个输出值的类型Integer
);apply
方法的作用:将输入的字符串s
转换为需要的键或值(这里就是提取姓名和年龄)。