Java-Stream流(一)

本文详细解析了Java 8中的流(Stream)概念,解释其与IO流的区别,展示了如何使用流进行数据处理,包括过滤、映射、排序等操作,并介绍了流的并行处理优势。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

什么是流?和IO流有关系吗? Java在1.8版本引入了流的概念,这个流可不是IO流的流,第一次学的时候我就很容易把这个流和IO流想到一块去,学的时候总是想着它和IO流是不是有什么关系呢?可是学到了后来发现.......什么关系都没有,Java中的流和IO流是截然不同的两个概念,根本不搭边。

那么什么是流呢?

直接上一大堆概念什么的各位可能会没有耐心看,很罗嗦,所以还是先上代码吧:

public class Dish{ private final String name; //菜名 private final boolean vegetarian; //是否为蔬菜 private final int calories; //包含卡路里含量 private final Type type; //菜的类型 public enum Type{MEAT ,FISH ,OTHER};

    public Dish(String name, boolean vegetarian, int calories, Type type) {
        this.name = name;
        this.vegetarian = vegetarian;
        this.calories = calories;
        this.type = type;
    }

    public String getName() {
        return name;
    }

    public boolean isVegetarian() {
        return vegetarian;
    }

    public int getCalories() {
        return calories;
    }

    public Type getType() {
        return type;
    }
}
复制代码

public class collect {

private static List<Dish> menu = Arrays.asList(
        new Dish("pork" ,false ,800 , Dish.Type.MEAT),
        new Dish("beef" ,false ,700 , Dish.Type.MEAT),
        new Dish("chicken" ,false ,400 , Dish.Type.MEAT),
        new Dish("french froes" ,true ,530 ,Dish.Type.OTHER),
        new Dish("rice" ,true ,350 , Dish.Type.OTHER),
        new Dish("season fruit" ,true ,120 ,Dish.Type.OTHER),
        new Dish("pizza" ,true ,550 , Dish.Type.OTHER),
        new Dish("prawns" ,false ,300 , Dish.Type.FISH),
        new Dish("salmon" ,false ,450 , Dish.Type.FISH)
);



public static void main(String[] args){
    ToListCollector<Dish> collector = new ToListCollector<Dish>();
    menu.stream()
            .forEach(n -> {System.out.println(n.getName());});
}
复制代码

} 为了方便,我直接引用了《Java8实战》这本书中的例子,那么运行结果是什么呢?

pork beef chicken french froes rice season fruit pizza prawns salmon 依次打印出了每道菜的名称,我们在上面看到,List类型的对象调用了一个名为 stream() 的方法,然后接着调用了 foreach() 方法,从 foreach() 方法中传入的参数来看,这个方法的形参应该是一个函数式接口(如果对函数式接口和Lambda表达式有疑惑,请看我写的Java行为参数化帖子,有三篇),再看看我传入的Lambda表达式,就应该知道这个函数式接口应该就是Consumer,它接受一个泛型参数,没有返回值。我在Lambda的方法体中调用的形参的 getName() 方法并打印输出,然后程序就打印出了ArrayList中的每个元素的 name 属性。

这里有两个疑问点:stream() 方法和 foreach() 方法

其实 stream() 方法就是将ArrayList中的元素转换为流,而 foreach() 方法就是对流中每一个元素进行处理,处理操作就是你传进去的Lambda(方法引用也可以)。Java大部分的集合框架都带有一个 stream() 方法用来将集合中的元素转为流,那这个流到底是什呢?你可以把他理解为集合的另一种表现方式,但这种表述不准确。流是一次性消费的,就是说你在调用了集合的 stream() 方法后产生一个流,然后对流进行处理操作,操作完之后这个流就消失了。

那么 foreach() 方法相比大家也有所体会了吧,它就是将流中的元素代入一个 Consumer 的 accept() 方法中,然后对元素的操作是根据你传入的Lambda表达式或者方法引用决定。

这样一看,流就像是将集合中的元素进行了一次遍历并对其中的元素进行一定的操作,但是它更简便、更高效。为什么说它更简便呢?因为它可以并行处理。一般我们在处理一个集合中的元素时,为了充分利用CPU,会加入并行操作,但是多个线程对一个集合进行操作的时候会产生线程安全问题,那么我们首先想到的就是加锁,但是加锁的代码片段都是串行处理,那么对这一个集合进行并行操作的时候加锁与单线程处理效率并没有太大的提升,就比如我下面写的这个例子:

public class collect {

    private static List<Dish> menu = Arrays.asList(
            new Dish("pork" ,false ,800 , Dish.Type.MEAT),
            new Dish("beef" ,false ,700 , Dish.Type.MEAT),
            new Dish("chicken" ,false ,400 , Dish.Type.MEAT),
            new Dish("french froes" ,true ,530 ,Dish.Type.OTHER),
            new Dish("rice" ,true ,350 , Dish.Type.OTHER),
            new Dish("season fruit" ,true ,120 ,Dish.Type.OTHER),
            new Dish("pizza" ,true ,550 , Dish.Type.OTHER),
            new Dish("prawns" ,false ,300 , Dish.Type.FISH),
            new Dish("salmon" ,false ,450 , Dish.Type.FISH)
    );

    static int index;

    public static void main(String[] args){
        //在不借助流的情况下对集合元素进行并行处理
        for(int i = 0 ; i < 10 ; i++){
            new Thread(()->{
                //首先要做的就是遍历这个集合,为了避免每个线程对元素的重复操作,
                //我在上面定义了一个静态变量作为下标来标识操作到了第几个元素
                
                    //例如 我们要筛选处蔬菜并打印它的名字,在这里menu是一个成员变量,要加锁。
                    synchronized (menu.getClass()) {
                        if(index < menu.size()) {
                            if (menu.get(index).isVegetarian()) {
                                System.out.println(menu.get(index).getName());
                                index++;
                            } else {
                                index++;
                            }
                        }
                    }
            }).start();
        }
    }
}
复制代码

在上面的代码中我们看到,线程中的代码几乎都被加锁,所以创建的这几个线程几乎是串行运行的,根本没有充分发挥多线程的优势。那么,我们借助 stream 流再来试一试:

public class collect {

private static List<Dish> menu = Arrays.asList(
        new Dish("pork" ,false ,800 , Dish.Type.MEAT),
        new Dish("beef" ,false ,700 , Dish.Type.MEAT),
        new Dish("chicken" ,false ,400 , Dish.Type.MEAT),
        new Dish("french froes" ,true ,530 ,Dish.Type.OTHER),
        new Dish("rice" ,true ,350 , Dish.Type.OTHER),
        new Dish("season fruit" ,true ,120 ,Dish.Type.OTHER),
        new Dish("pizza" ,true ,550 , Dish.Type.OTHER),
        new Dish("prawns" ,false ,300 , Dish.Type.FISH),
        new Dish("salmon" ,false ,450 , Dish.Type.FISH)
);

static int index;

public static void main(String[] args){
    menu.parallelStream()
            .filter(Dish::isVegetarian)
            .forEach(n -> System.out.println(n.getName()));
    }
}
复制代码

看看这代码,是不是很简洁,这就是流的多线程处理,上面的代码中名为 menu 的集合首先调用了一个叫 paralleStream() 的方法,它和 stream() 方法的区别就是调用它之后对流的处理都是并行的,它会将流中的元素进行分段,然后分发到子线程中处理,后面的文章中我会继续讲解关于流并行的知识,在这里就是要告诉大家流的引入搭配上Lambda表达式极大的简化了我们的编程。

在调用 paralleStream() 方法后我又调用了一个 fileter() 方法,它的功能就像它的名字一样,是一个过滤器,它接受一个 Predict函数式接口,这个接口接受一个泛型参数并返回一个布尔值,流在调用这个方法后会将所有返回 false 的元素剔除出去,只留下返回 true 的元素交给下一个方法操作。

流的终端操作和中间操作 在上一个例子中我调用了 filter() 和 foreach() 方法,一个对流中元素过滤,一个将流中的元素打印输出,这两个操作其实是两种性质的操作,filter() 是中间操作,就是流在经过这个方法后会接着生成一个流,也就是将流中原来的元素筛选过后生成一个新流交给下一个方法处理;foreach() 方法是一个终端操作,经过它的流在操作处理过后不会返回流,终端操作标志着这一个流的结束。

那么Java还为我们提供了哪些中间操作和终端操作呢?我将它们应用在一个例子中然后一一讲解:

public class collect {

private static List<Dish> menu = Arrays.asList(
        new Dish("pork" ,false ,800 , Dish.Type.MEAT),
        new Dish("beef" ,false ,700 , Dish.Type.MEAT),
        new Dish("chicken" ,false ,400 , Dish.Type.MEAT),
        new Dish("french froes" ,true ,530 ,Dish.Type.OTHER),
        new Dish("rice" ,true ,350 , Dish.Type.OTHER),
        new Dish("season fruit" ,true ,120 ,Dish.Type.OTHER),
        new Dish("pizza" ,true ,550 , Dish.Type.OTHER),
        new Dish("prawns" ,false ,300 , Dish.Type.FISH),
        new Dish("salmon" ,false ,450 , Dish.Type.FISH)
);

static int index;

public static void main(String[] args){
    List<Integer> list = menu.stream()
            .filter(m -> !m.isVegetarian())
            .map(Dish::getCalories)
            .sorted(Comparator.comparingInt(n->n))
            .limit(4)
            .skip(1)
            .collect(Collectors.toList());
}
复制代码

} 这段代码的功能是什么呢?

就是筛选出所有不是蔬菜的食物并将 filter() 产生的流中的所有Dish对象映射成它们的卡路里,现在流中保存的不再是Dish对象,而是Interger对象;然后用 sorted() 方法对流中的卡路里进行排序,生成一条int值从小到大的流;用 limit() 方法截取流的前4个元素;用 skip() 方法跳过流中的前一个元素;通过 collect() 方法,在里面传入 Collectors.toList() 方法将流中的元素保存到一个 List 对象中。

1.filter()方法:中间操作

此方法接受一个 Predict 函数式接口,也就是说你可以传入一个Lambda表达式或者方法引用,Predict 接口中的抽象方法为

boolean test(T t); 此方法返回一个布尔值,只有返回true值的元素才会被加入到新的流中传递给下一个方法。

2.map()方法:中间操作

此方法接受一个 Function<? super T ,? extends R> 函数式接口,意在将传入的元素映射为其他类型的元素,Function中的抽象方法为

R apply(T t); apply方法接受一个泛型类型的值返回一个泛型类型的值,在本例中我们传入了Dish类型的元素并调用的Dish对象的 getCalories() 方法获得一个整形值返回。

3.sorted()方法:中间操作

此方法接受一个 Compator<? super T>类型的函数式接口,意在对流中的元素进行排序,排序规则根据传入的Compator类型Lambda表达式或者方法引用而定,Compator中的抽象方法为

int compare(T o1, T o2); compare方法接受两个泛型参数,在这里就是传入流中的两个元素,然后根据自己的逻辑对他们进行比较大小,就像我刚才调用的Compator.comparingInt() ,它的内部调用其实就是这个:

public static int compare(int x, int y) {
    return (x < y) ? -1 : ((x == y) ? 0 : 1);
}
复制代码

4.limit()方法:中间操作

这个方法接受一个long型值,意为截断,也就是将流的前 n(传入limit方法的long值) 个元素抽取出来生成一个新流传给下一个方法。

5.skip()方法:中间操作

这个方法接受一个long型值,意为跳过,也就是将流的前 n 个元素跳过,将剩下的元素抽取出来生成一个新流传给下一个方法。

6.collect()方法:终端操作

这个方法很重要,意为收集器,在后免得帖子中我会重点讲解收集器,在这里我们只介绍它的基本用法。它接受一个Collector<? super T ,A ,R>函数式接口,它会把流中的元素全部收集起来放到一个容器里去,这个容器由自己来定义。在上面我们写到的Collectors.toList()是Java自带的方法,会将所有元素收集到一个List中去。

以上就是对流的一些基本操作,大家也可以跟着源码去深入学习

流和集合是分开工作的 为什么这样说呢?在上面的例子中大家也看到了,我们在使用流的时候都是对集合中元素进行操作的,并且因为函数式接口的产生,这些操作都非常方便,那么我们就可以说集合是用来保存元素的,而流是用来操作元素的。

转载于:https://juejin.im/post/5c39aef46fb9a049ac7962d8

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值