Java 学习企划——Stream 流部分

在这里插入图片描述
大家好,从我开始写博客也过去半年多了,c 站陪我走过了学习 Java 最艰苦的那段时光,也非常荣幸写的博客能得到这么多人的喜欢。
某一天当我开始学习分布式的时候突然想到这可能是补充 Java 知识拼图的最后几块部分了,为了将前面的知识更好的精进,我准备在完成分布式学习后将 Java 按照学习路线的顺序整理属于我和大家的博客上去,顺便写一下这一路的经验和感受,这篇博客算是一个先导吧,如果对我这个企划感兴趣的朋友可以关注我一下,我们重新起航,一起拥抱更好的未来!

01. 什么是 Stream 流?

💡 Stream 是 Java 8 中引入的一个新的抽象概念,它提供了一种更为便捷、高效的方式来处理 集合 数据。Stream 可以让开发者以声明式的方式对集合进行操作,而不需要显式地使用循环或者条件语句。

<1> 初识 stream 流

🍀 给一个 List<String> 链表,包含六个数字字符串,请找出以数字 1 开头并且长度为 3 的字符串,并且将其 输出 出来。

❓ 看到这道题目,第一想法肯定是借助两次遍历将符合这两种情况的字符串分别筛选成新的集合,再将其打印出来,来看一下代码实现。

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("123");
        list.add("12");
        list.add("15");
        list.add("266");
        list.add("171");
        list.add("13");

        List<String> list1 = new ArrayList<>();
        for (String s : list) {
            if (s.startsWith("1")) {
                list1.add(s);
            }
        }

        List<String> list2 = new ArrayList<>();
        for (String s : list1) {
            if (s.length() == 3) {
                list2.add(s);
            }
        }

        for (String s : list2) {
            System.out.println(s);
        }
    }
} 

那如果使用 Stream 流会有什么效果呢?

public class Main {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();

        list.add("123");
        list.add("12");
        list.add("15");
        list.add("266");
        list.add("171");
        list.add("13");

        list.stream().filter(x -> x.startsWith("1")).filter(x -> x.length() == 3).forEach(x -> System.out.println(x));
    }
}

💡 大家看着这段代码来尝试体会一下 Stream 流处理集合数据的方式:

将集合中的数据想象成一些商品,将它们放到一个流水线上。

这时候老板突然提要求了:不是从 1 开始的我们不要。
那就加一道工序,把从 1 开始的全部筛出来。
此时你看老板眉头紧锁又在思考怎么筛,你抓紧把筛出来的部分 再放到流水线 上,静候老板的指示。

老板一拍脑门:长度不是 3 的也不要了,说完就走了,聪明的你立马就懂了,这是最后一道工序,于是筛选完最后一次之后就将其产出了(输出)。

<2> stream 的思想

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

💡 Stream 的操作可以链接在一起形成一个流水线。这个流水线包括一系列中间操作和一个终端操作

  • 所谓的中间操作就是筛选完了之后再将其放到流水线上。
  • 而终端操作就是直接结束流程,产出商品了。
List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
// 中间操作:过滤出偶数
Stream<Integer> evenNumbersStream = numbers.stream().filter(n -> n % 2 == 0);
// 终端操作:打印偶数
evenNumbersStream.forEach(System.out::println);

🍀 空口无凭没有说服力,直接来看一个中间操作的返回值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

🍀 再来看终端操作的返回值:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

一个返回的是一个新的 Stream 流,另一个则是 void

02. 得到 stream 流

💡 要想在流水线上操纵商品,得先将其放到流水线上,这里先来看 Stream 能操纵什么元素,以及提供了怎样的 API 来得到 Stream 流。

获取方式方法名说明
单列集合stream()Collection 提供的默认方法
双列集合无法直接使用,需要将其转为单列集合
数组Arrays.stream()使用 Arrays 工具类提供的 static 方法
一些零散数据Stream.of()Stream 接口中的 static 方法

🍀 单列集合可以通过父类 Collection 提供的接口默认实现方法来直接得到 stream

list.stream();

🍀 双列集合,例如 HashMap,这时候就只能将其转化为单列集合,回顾一下,对于 Map 可以使用 keySet() 方法得到 keySet 集合,使用 values 得到 value 的实现 Collection 接口的集合。

public class Main {
    public static void main(String[] args) {
        Map<String, Integer> map = new HashMap<>();

        map.put("aaa", 1);
        map.put("bbb", 2);
        map.put("ccc", 3);

        map.keySet().stream().filter(x -> x.startsWith("a")).forEach(x -> System.out.println(x));
        System.out.println("---------");
        map.values().forEach(x -> System.out.println(x));
    }
}
aaa
---------
1
3
2

🍀 数组,使用 Arrays 工具类提供的 stream 方法,相信经常刷算法的同学一定不陌生,在处理数组的时候有时可以使用这种方法来简化代码。

public class Main {
    public static void main(String[] args) {
        int[] nums = new int[]{1, 2, 3, 5};

        Arrays.stream(nums).filter(x -> x >= 2).forEach(x -> System.out.println(x));
    }
}
2
3
5

🍀 Stream 还可以处理一些一些零散的数据,但注意这些数据必须是同种类型的。

public class Main {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5).filter(x -> x >= 3).forEach(x -> System.out.println(x));
        System.out.println("---------------");
        Stream.of("1", "2", "3", "4", "5").filter(x -> x.startsWith("1")).forEach(x -> System.out.println(x));
    }
}
3
4
5
---------------
1

💡 注意:第四种方法中的 可变参数 可以处理一些数组类型,但必须是 引用数组

  • 这是一位内泛型在编译时会被擦除,这意味着泛型信息在运行时是不可用的。
  • 如果你传入一个数组作为泛型参数,编译器在编译时不会保留数组的元素类型信息,而只是将其视为一个 Object 类型的数组。当你尝试输出这个泛型数组时,只能获取到数组的地址而不是内容。
public class Main {
    public static void main(String[] args) {
        int[] nums = new int[] {1, 2, 3};
        Stream.of(nums).forEach(x -> System.out.println(x));
    }
}
[I@404b9385

03. Stream 流的中间方法

💡 中间方法返回的是一个新的 Stream 流;修改 Stream 流中的数据对原本的集合或者数组没有任何影响。除了比较特殊的 map() 方法,其他其实都是对数据的增和删。

名称说明
filter()过滤
limit()获取前几个元素
skip()跳过前几个元素,获取后面的
distinct()元素去重,依赖 hashCodeequals 方法
concat()将两个流合并为一个流
map()转换流中的数据类型

🍀 增和删的方法的含义比较明确也没有什么特别注意的地方,这里就直接上一个完整的代码演示和输出案例。

public class Main {
    public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);

        // 1. 使用 filter 过滤出偶数
        numbers.stream().filter(x -> x % 2 == 0).forEach(x -> System.out.print(x + " "));
        System.out.println("\n----------");
        // 2. 使用 limit 得到前三个元素
        numbers.stream().limit(3).forEach(x -> System.out.print(x + " "));
        System.out.println("\n----------");
        // 3. 使用 skip 方法跳过前三个元素
        numbers.stream().skip(3).forEach(x -> System.out.print(x + " "));
    }
}
2 4 6 8 10 
----------
1 2 3 
----------
4 5 6 7 8 9 10 
public class Main {
    public static void main(String[] args) {
        // 对两个链表进行合并和去重操作
        List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> list2 = Arrays.asList(4, 5, 6, 7, 8);

        Stream<Integer> concat = Stream.concat(list1.stream(), list2.stream());
        concat.distinct().forEach(x -> System.out.print(x + " "));
    }
}
1 2 3 4 5 6 7 8

💡 在 Java 中,Stream API 的 distinct() 方法底层是通过哈希集合(HashSet LinkedHashMap)来实现去除重复元素的功能的。

  • 具体来说,当调用 distinct() 方法时,Stream 会使用一个哈希集合来保存已经遇到过的元素。当遍历流中的元素时,每遇到一个新元素,就会将其添加到哈希集合中。如果集合中已经存在相同的元素,则不会重复添加。最终,返回的流中只包含不重复的元素。
  • 这里再啰嗦几句关于 equalshashCode 的重写问题,在默认情况下,也就是 Object 类提供的 default 方法,hashCode 方法默认是映射内存地址,equals 方法默认比较的也是内存地址。
    • 比如说我们想要实现一个 不允许存放相同元素的集合,那肯定是优先去比较 hashCode,而哈希映射得到的内容是有限的,如果碰到恰好相同的情况就会出现哈希冲突
    • 这时候再去调用 equals 方法去比较,一比较,好家伙,不一样,那就放进去吧,但此时可以看出来比较的是内存地址,两个对象的内存地址肯定是不相同的,此时做的就是无效的比较。
    • 那之重写 equals() 方法可行吗?因为在存入时候优先比较的是 hash 码,逻辑上的相等 equals 不等于 hash 相等,这时候就会出现问题。
    • 那只重写 hashCode() 呢?那这就和上面的哈希冲突情况相同了,哈希值相同的时候比较却发现不同(比较的是内存地址),这时候也会存入相同的元素。
      • 总结一下,其实重写两个方法就是对去重的两个阶段的逻辑达到统一,这样才不会出现某一阶段筛选漏掉的情况。

🍀 map 方法它接受一个 函数 作为参数,该函数会被应用到流中的每个元素上,从而将原始流中的元素映射成新的元素,最终返回一个包含映射结果的新流。

<R> Stream<R> map(Function<? super T, ? extends R> mapper)
public class Main {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("alice", "bob", "charlie");

        // 将每个名字转换为大写
        List<String> upperCaseNames = names.stream()
                               .map(name -> name.toUpperCase())
                               .collect(Collectors.toList());

        System.out.println(upperCaseNames); // 输出 [ALICE, BOB, CHARLIE]
    }
}

04. Stream 流的终端方法

💡 终端方法就是取出流水线中元素的操作,再取出的时候,可以选择采用何种方式包装。

名称说明
void forEach()遍历
long count()统计
toArray()包装到数组中
collect()包转到集合中

💡 可以看出返回值均不是 Stream,执行完终端操作流水线就被停掉了,无法继续使用。

🍀 forEach() 就是对流水线上现有的元素的一个遍历,传入一个函数来指定对每个元素的操作。

public class Main {
    public static void main(String[] args) {
        // 对两个链表进行合并和去重操作
        List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> list2 = Arrays.asList(4, 5, 6, 7, 8);
        Stream<Integer> concat = Stream.concat(list1.stream(), list2.stream());
        Stream<Integer> stream =  concat.distinct();
//            stream.forEach(new Consumer<Integer>() {
//            @Override
//            public void accept(Integer integer) {
//                System.out.println(integer);
//            }
//       	 });
        stream.forEach(x -> System.out.print(x + " "));
    }
}
1 2 3 4 5 6 7 8

🍀 count() 方法就是统计现有元素的总数,返回值为 long

public class Main {
    public static void main(String[] args) {
        // 对两个链表进行合并和去重操作
        List<Integer> list1 = Arrays.asList(1, 2, 3, 4, 5);
        List<Integer> list2 = Arrays.asList(4, 5, 6, 7, 8);
        Stream<Integer> concat = Stream.concat(list1.stream(), list2.stream());
        Stream<Integer> stream =  concat.distinct();
        System.out.println(stream.count());
    }
}
8

🍀 toArray() 方法可以选择是否传参,如果不传参返回的就是一个 Object[] 数组;但一般是会得到一个具体类型的数组,这时候就要进行传参操作了。

  • 传递的参数是一个函数,它的作用是产生一个指定类型的数组,toArray 方法的底层会依次将流里的元素放到这个数组中。

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

        String[] array = stream.toArray(new IntFunction<>() {
            @Override
            public String[] apply(int value) {
                return new String[value];
            }
        });
        //String[] array = stream.toArray(value -> new String[value]);

05. 收集方法 collect

💡 集合的种类有很多,同时也有一些注意事项,这里单独来讲解一下 collect 方法

  • 因为可转化的集合有很多,所以需要一个参数来指定选择的是哪种集合,这个参数就是 CollectorImpl
  • 系统提供了工具类 Collectors 来规划的创建 CollectorImpl

🍀 收集到 List 或者 Set

// 收集到 List
List<String> list = stream.collect(Collectors.toList());
// 收集到 Set
Set<String> set = stream.collect(Collectors.toSet());

🍀 Mapkeyvalue 两个字段,比较特殊。

  • Collectors.toMap() 需要传入两个函数作为参数

    • 第一个函数的两个泛型分别指定流中的数据类型和新 Map 集合中键的数据类型

      • 重写的方法中参数是流中的每个元素,返回值是放置到 Map 集合中的元素的形式。
    • 第二个函数中的两个泛型分别指定流中的数据类型和新 Map 集合中值的数据类型

比如说处理很多以 姓名-年龄 为格式的字符串,将其映射为姓名为键的 Map 集合可以通过如下的方式实现:

    Map<String, Integer> collect = list.stream().collect(Collectors.toMap(
            new Function<String, String>() {
        @Override
        public String apply(String s) {

            return s.split("-")[0];
        }
    }, new Function<String, Integer>() {
        @Override
        public Integer apply(String s) {
            return Integer.parseInt(s.split("-")[1]);
        }
    }));
  • 29
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

*Soo_Young*

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值