一、例题:计算商品总价(单价 × 数量)
1-1、方法一:
java
public static void test06(){
List<Product> productList = Arrays.asList(
new Product("Book", 20.0, 2),
new Product("Pen", 2.5, 4)
);
List<Double> sumPricePer = productList.stream()
.map(product -> product.getPrice() * product.getStock())
.collect(Collectors.toList());
System.out.println(sumPricePer.toString());
double sum = 0.0;
for (Double price : sumPricePer) {
sum += price;
}
System.out.println(sum);
}
1-2、方法二:
java
public static void test07(){
List<Product> productList = Arrays.asList(
new Product("Book", 20.0, 2),
new Product("Pen", 2.5, 4)
);
double sum = productList.stream()
.mapToDouble(product -> product.getPrice() * product.getStock())
.sum();
System.out.println(sum);
}
二、为什么 Stream<Integer> 没有 .sum()?
因为:
Stream<T>是泛型接口,设计目标是处理任意引用类型 (如String,Product,Integer)。- 它不知道
T是否是数字,更不知道如何对它求和。 - 所以
Stream<T>只提供通用操作:collect(),forEach(),filter()等,不提供sum()。
2-1、包装类本身能做运算吗?(脱离 Stream 的情况)
在普通 Java 代码中 ,包装类可以参与算术运算,因为 Java 会自动拆箱:
java
Integer a = 10;
Integer b = 20;
int c = a + b; // ✅ 自动拆箱:a.intValue() + b.intValue()
所以:在普通代码中,包装类可以做运算(靠自动拆箱)。
Java 需要包装类,是因为泛型、集合(如
List)、反射、序列化等机制只支持引用类型,不支持基本类型。
2-2、必须用包装类的场景
| 场景 | 说明 |
|---|---|
| 泛型类/方法 | Optional<Integer>, Comparator<Double> |
| 反射(Reflection) | Method.invoke() 返回 Object,只能是引用类型 |
| JSON/XML 序列化 | Jackson、Gson 等库处理的是对象,不是 int |
| 数据库 ORM(如 JPA) | 实体类字段通常是 Integer,因为可能为 NULL |
| 表示"缺失值" | Integer 可以为 null,而 int 必须有值(默认 0) |
示例:为什么集合(Collection)必须用包装类?
java
// ❌ 编译错误!泛型不支持基本类型
List<int> numbers = new ArrayList<>();
// ✅ 正确:必须用包装类
List<Integer> numbers = new ArrayList<>();
原因:
- Java 的泛型是通过类型擦除实现的 ,底层只认
Object。 - 而
int不是Object的子类,不能被当作对象处理。 Integer是Object的子类,可以放进List、Map、Set等。
所以:没有包装类,你就无法把数字存进
ArrayList、HashMap等常用集合!
2-3、Java 的"双轨制"类型系统
Java 有两类数据类型:
| 类型 | 示例 | 特点 |
|---|---|---|
| 基本类型(Primitive) | int, double, boolean |
存储在栈上,高效,无方法 |
| 引用类型(Reference) | Integer, Double, String |
存储在堆上,是对象,可为 null |
为什么不能统一成一种?
因为:
- 性能:基本类型快、省内存。
- 面向对象:Java 是 OOP 语言,希望"一切皆对象"。
于是 Java 采取了折中方案:保留基本类型保性能,引入包装类来"对象化"基本值。
自动装箱/拆箱:让两者无缝协作
为了不让开发者痛苦地手动转换,Java 5 引入了:
- 自动装箱(Autoboxing) :
Integer i = 100;→Integer.valueOf(100) - 自动拆箱(Unboxing) :
int x = i;→i.intValue()
那为什么不全用包装类?
因为:
- 性能开销 :每个
Integer都是对象,占用堆内存,触发 GC。 - 空指针风险 :
Integer a = null; int b = a;→NullPointerException - 比较陷阱:
java
Integer a = 128, b = 128;
System.out.println(a == b); // false!因为超出缓存范围(-128~127)
所以:能用基本类型就用基本类型,只有需要"对象特性"时才用包装类。
三、为什么要有原始类型特化流 IntStream / DoubleStream?
Java 的泛型 不能直接使用基本类型(如
int,double) ,只能用包装类(Integer,Double)。
如果对大量数字做运算,频繁装箱/拆箱会带来性能开销 和内存浪费。
为了解决这个问题,Java 8 引入了三个原始类型特化的 Stream:
| 原始类型 | 对应的 Stream 类型 |
|---|---|
int |
IntStream |
long |
LongStream |
double |
DoubleStream |
它们不涉及装箱/拆箱,性能更高,且提供专门的数值操作方法(如 sum(), average(), max() 等)。
四、中间操作的返回类型分类
4-1. 返回 Stream<T> 的中间操作(最常见)
这些操作保持流的"对象"性质,适用于任意类型:
java
Stream<String> s1 = list.stream()
.filter(x -> x.length() > 3) // Stream<T>
.map(String::toUpperCase) // Stream<T>
.peek(System.out::println) // Stream<T>
.distinct() // Stream<T>
.sorted() // Stream<T>
.limit(5) // Stream<T>
.skip(2); // Stream<T>
所有这些方法都返回
Stream<T>,所以可以链式调用。
4-2. 返回 原始类型流 的中间操作(特殊转换)
当你需要从对象流转为数值流时,使用以下方法:
| 方法 | 返回类型 | 说明 |
|---|---|---|
.mapToInt(ToIntFunction) |
IntStream |
把每个元素映射成 int |
.mapToLong(ToLongFunction) |
LongStream |
映射成 long |
.mapToDouble(ToDoubleFunction) |
DoubleStream |
映射成 double |
示例:
java
List<Product> products = ...;
// 转成 DoubleStream(用于价格计算)
DoubleStream ds = products.stream()
.mapToDouble(p -> p.getPrice()); // 返回 DoubleStream
// 转成 IntStream(用于库存)
IntStream is = products.stream()
.mapToInt(p -> p.getStock()); // 返回 IntStream
⚠️ 一旦变成
DoubleStream,你就不能再调用.map()(因为它是DoubleStream的方法,不是Stream的),而要用.mapToDouble()或.map()(但参数是DoubleUnaryOperator)。
4-3. 原始类型流自己的中间操作
DoubleStream 也有自己的中间操作,比如:
java
DoubleStream ds = products.stream()
.mapToDouble(p -> p.getPrice())
.filter(d -> d > 10.0) // DoubleStream
.map(d -> d * 1.1) // DoubleStream(注意:这里是 DoubleUnaryOperator)
.sorted(); // DoubleStream
但注意:这里的 .map() 接收的是 DoubleUnaryOperator(函数式接口:double → double),不是 Function<Double, Double>!
五、终端操作的区别
| 流类型 | 支持的终端操作(部分) |
|---|---|
Stream<T> |
collect(), forEach(), findFirst(), anyMatch()... |
IntStream |
sum(), average(), max(), min(), summaryStatistics()... |
LongStream |
同上 |
DoubleStream |
同上 |
重点:只有
IntStream/LongStream/DoubleStream才有.sum()方法!
六、类型转换图(简化版)
Stream<Product>
│
├─ .map(...) ────────────────→ Stream<R>
│
├─ .mapToInt(...) ───────────→ IntStream
├─ .mapToLong(...) ──────────→ LongStream
└─ .mapToDouble(...) ────────→ DoubleStream
│
├─ .sum() → int / long / double
└─ .boxed() → Stream<Integer> / Stream<Long> / Stream<Double>
💡 如果你想把
DoubleStream转回Stream<Double>,可以用.boxed():
javaList<Double> prices = products.stream() .mapToDouble(Product::getPrice) .boxed() .collect(Collectors.toList());