普通流(Stream<T>)和原始类型特化流(IntStream, LongStream, DoubleStream)的区别

一、例题:计算商品总价(单价 × 数量)

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():

java 复制代码
List<Double> prices = products.stream()
    .mapToDouble(Product::getPrice)
    .boxed()
    .collect(Collectors.toList());
相关推荐
wuminyu5 小时前
JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码
java·linux·c语言·jvm·c++
txxzjmzlh6 小时前
SpringBoot 配置⽂件
java·spring boot·spring
Wang's Blog6 小时前
Java 项目实战: 交友项目 - 搜索附近的功能概述和分析
java·开发语言·交友
骑着蜗牛撵大象3276 小时前
大模型 Agent 的沙箱护栏:gVisor 隔离、出口白名单与审计闭环
java·开发语言·jvm·安全·agent沙箱·gvisor·microvm
独泪了无痕6 小时前
Hutool之LocalDateTimeUtil:让时间间隔操作如虎添翼
java
三无少女指南8 小时前
从这里开始:Java 后端与 AI 工程的三条阅读路线
java·学习路线·ai工程
香蕉大香蕉(06敬畏市场)8 小时前
MySQL 单点详解(07)
java·数据库·mysql·ai·博客·八股
nice_lcj5208 小时前
深入理解 Java 反射:从 Class 对象到方法调用(含性能实测 + 迷你 IOC 实战 + 面试考点)
java·开发语言·后端·面试
骑着蜗牛撵大象3279 小时前
2026 后端 AI 工程化:Spring AI 管调用、MCP 管工具、Agent 内嵌管自治
java·人工智能·后端·spring·ai·可观测性·mcp
tryxr9 小时前
JVM 垃圾回收机制
java·jvm·gc·垃圾回收