Java【Stream/MethodReference】

Java【Stream/MethodReference】


一、Stream流

java8之后,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

1.1 概述
传统集合的多步遍历

几乎所有的集合(Collection接口或Map接口等)都支持直接或间接的遍历操作。

循环遍历的弊端

Java8Lambda让我们更加专注于做什么,而不是怎么做。为什么要使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。

传统的集合遍历
public class Demo01List {
    public static void main(String[] args) {
        // 创建一个List集合,存储姓名
        List<String> list = new ArrayList<>();
        list.add("迪丽热巴");
        list.add("玛尔扎哈");
        list.add("赵无极");
        list.add("赵晓");
        list.add("赵匡胤");

        // 对集合中的元素进行过滤,只要"赵"开头的元素,并存储到一个新的集合中
        List<String> listA = new ArrayList<>();
        for (String s : list) {
            if(s.startsWith("赵")){
                listA.add(s);
            }
        }

        // 对listA集合进行过滤,取出姓名长度为2的人,并存储到一个新的集合中
        List<String> listB = new ArrayList<>();
        for (String s : listA) {
            if(s.length()==2) {
                listB.add(s);
            }
        }

        // 遍历listB集合
        for (String s : listB) {
            System.out.println(s);
        }
    }
}
Stream的更优写法

使用Stream流的方式遍历集合,对集合中的数据进行遍历。Stream流是1.8之后出现的,关注的是做什么而不是怎么做。

public class Demo02Stream {
    public static void main(String[] args) {
        // 创建一个List集合,存储姓名
        List<String> list = new ArrayList<>();
        list.add("迪丽热巴");
        list.add("玛尔扎哈");
        list.add("赵无极");
        list.add("赵晓");
        list.add("赵匡胤");

        // 对集合中的元素进行过滤,只要"赵"开头的元素,并存储到一个新的集合中
        // 对listA集合进行过滤,取出姓名长度为2的人,并存储到一个新的集合中
        // 遍历listB集合
        list.stream()
                .filter(name->name.startsWith("赵"))
                .filter(name-> name.length()==2)
                .forEach(name-> System.out.println(name));
    }
}
1.2 流式思想

整体来看,流式思想类似于工厂车间的”生产线“,流遵循了做什么而非怎么做的原则。流表面上看上去和集合很类似,都可以让我们转换和获取数据,但是它们之间存在着显著的差异:

  • 流并不存储其元素

  • 流的操作不会修改其数据源

  • **流的操作是尽可能惰性执行的。**这意味直至需要其结果时,操作才会执行。

当需要对多个元素进行操作(特别是多步操作)的时候,考虑到性能及便利性,我们首先应该拼接好一个”模型“步骤方案,然后按照方案去执行。

在这里插入图片描述

上图展示了过滤、映射、跳过、计数等多步操作,这是一个集合元素的处理方案,而方案就是一种函数模型。图中的每一个方框都是一个”流“,调用指定的方法,可以从一个流模型转换成另一个流模型,而数字3是最终结果。

需要注意的是,上面的filtermapskip都是在对函数模型进行操作,集合元素并没有真正被处理,只有当终结方法count方法执行的时候,整个模型才会按照指定策略执行操作,这也是Lambda延迟执行特点的体现。

使用流的三个基本步骤:获取一个数据源(source)->数据转换->执行操作获取想要的结果。每次转换原有的Stream对象不改变,返回一个新的Stream对象(允许多次转换),这就允许其操作像链条一样排列,变成一个管道。

1.3 获取流

java.util.stream.Stream<T>是Java 8 新加入的最常用的流接口。(这并不是一个函数式接口

获取一个流非常简单,有以下几种常用方式:

  • 所有的Collection集合都可以通过steam默认方法获取流
  • Stream接口的静态方法of可以获取数组对应的流。
根据Collection获取流
// 把集合转换为Stream流
List<String> list = new ArrayList<>();
Stream<String> stream = list.stream();

Set<String> set = new HashSet<>();
Stream<String> stream2 = set.stream();
根据Map获取流
Map<String,String> map = new HashMap<>();
// 获取键,存储到一个Set集合中
Set<String> keySet = map.keySet();
Stream<String> stream3 = keySet.stream();

// 获取值,存储到一个Collection集合中
Collection<String> value = map.values();
Stream<String> stream4 = value.stream();

// 获取键值对(EntrySet)
Set<Map.Entry<String,String>> entries = map.entrySet();
Stream<Map.Entry<String,String>> stream5 = entries.stream();
根据数据获取流
// 把数组转换为Stream流
Stream<Integer> stream6 = Stream.of(1,2,3,4,5);

// 可变参数传递数组
Integer[] arr = {1, 2, 3, 4, 5};
Stream<Integer> stream7 = Stream.of(arr);
1.4 常用方法

在这里插入图片描述

流模型的操作很丰富,这里介绍一些常用的API,具体可以分为两类:

延迟方法:返回值仍然是Stream类型,支持链式调用。

终结方法:返回值不再是Stream类型

逐一处理:forEach

forEach:用来遍历流中的数据

// 获取一个Stream流
Stream<String> stream = Stream.of("迪丽热巴", "玛尔扎哈", "打铁赵晓");
// 使用Stream流中的forEach方法对流进行遍历
stream.forEach(name -> System.out.println(name));// 简化写法
过滤:filter

Stream流中常用方法filter:用于对流中的数据进行过滤 Stream<T> filter(Predicate<? super T> predicate) filter方法的参数Predicate 是一个函数式接口,所以可以传递Lambda表达式,对元素进行过滤。

注意:泛型通配符的概念

泛型的上限限定: ? extends E 代表使用的泛型只能是E的子类/本身
泛型的下限限定: ? super E 代表使用的泛型只能是E的父类/本身
// 创建一个Stream流
Stream<String> stream = Stream.of("赵无极", "赵晓", "张三丰", "张无忌", "张全蛋");
// 对Stream中的元素进行过滤,只要姓赵的人
Stream<String> stream2 = stream.filter(name -> name.startsWith("赵"));
// 遍历流中的数据
stream2.forEach(name -> System.out.println(name));

/*
    Stream 流属于管道流,只能被消费(使用)一次
    第一个Stream流调用完毕方法,数据就会转到下一个Stream中
    第一个Stream流使用完毕,就会自动关闭,此时再调用它的方法,会报错
    java.lang.IllegalStateException: stream has already been operated upon or closed
 */
// 遍历Stream流,报错
stream.forEach(name -> System.out.println(name));
映射:map

如果要将流中的元素映射到另一个流中,可以使用map方法: <R> Stream<R> map(Function<? super T,? extends R> mapper),该接口需要一个Function函数式接口参数,可以将流中的T类型数据,转换成另一种R类型数据的流。

// 获取一个Stream类型的流
Stream<String> stream = Stream.of("1", "2", "3", "4", "5");
// 使用Stream流中的map方法,将字符串类型的整数映射为Integer类型
Stream<Integer> stream2 = stream.map(s -> Integer.parseInt(s));
// 遍历stream2
stream2.forEach(i-> System.out.println(i));
统计个数:count

Stream流中的常用方法count:用于统计Stream流中元素的个数

// 获取一个流
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
list.add(4);
list.add(5);
list.add(6);
list.add(7);
Stream<Integer> stream = list.stream();
// 调用count方法
long count = stream.count();
System.out.println(count);
取用前几个:limit

Stream流中的常用方法limit: 用于截取流中的元素 。可以对流中的数据元素进行截取,只取前n个。
Stream<T> limit(long maxSize) limit方法是一个延迟方法,对流中的元素进行截取返回一个新的流。

// 获取一个新的流
Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "灰太狼", "红太狼");
// 使用limit方法截取前三个元素
Stream<String> stream2 = stream.limit(3);
// 遍历stream2
stream2.forEach(name-> System.out.println(name));
跳过前几个:skip

Stream流中的常用方法:skip 用于跳过元素。 如果希望跳过前几个元素,可以使用skip方法来截取获得一个新的流 Stream<T> skip(long n)。如果流的长度大于n,则会返回一个截取后的新流,否则会返回一个长度为0的空流。

// 获取一个新的流
Stream<String> stream = Stream.of("美羊羊", "喜羊羊", "懒羊羊", "灰太狼", "红太狼");
// 使用skip方法截跳过三个元素
Stream<String> stream2 = stream.skip(3);
// 遍历stream2
stream2.forEach(name-> System.out.println(name));
组合:concat

concate:用于把流组合到一起static <T> Stream<T> concat(Stream<? extends T> a,Stream<? extends T> b)

// 创建一个Stream流
Stream<String> stream = Stream.of("黄通", "任登宝", "郭子瑞", "徐超", "戴维斯");
// 获取一个Stream流
String[] arr = {"赵无极", "赵敏", "赵雷", "赵晓"};
Stream<String> stream2 = Stream.of(arr);
// 使用concate方法组合两个Stream
Stream<String> concate = Stream.concat(stream,stream2);
// 遍历流
concate.forEach(name-> System.out.println(name));
1.5 练习:集合元素处理(传统方式)

要求

有两个ArrayList集合存储队伍当中的多个成员姓名,要求使用传统的for循环(或增强for循环)依次进行以下步骤:

1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。

2.第一个队伍筛选之后只要前3个人;存储到一个新集合中。

3.第二个队伍只要姓德的成员姓名;存储到一个新集合中。

4.第二个队伍帅选之后不要前两个人;存储到一个新集合中。

5.将两个队伍合并为一个队伍:存储到一个新集合中。

6.根据姓名创建Person对象;存储到一个新集合中。

7.打印整个队伍的Person对象信息。

// 第一个队伍
ArrayList<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("玛尔扎哈");
one.add("宋");
one.add("赵匡胤");
one.add("赵无极");
one.add("赵大晓");
one.add("老子");
one.add("星宿老怪");
one.add("任登宝");
one.add("赵雷");
// 1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
ArrayList<String> one1 = new ArrayList<>();
for (String name : one) {
    if(name.length() == 3) {
        // System.out.println(name);
        one1.add(name);
    }
}
// 2.第一个队伍筛选之后只要前3个人;存储到一个新集合中。
ArrayList<String> one2 = new ArrayList<>();
for (int i = 0; i < one1.size(); i++) {
    if(i<3) {
        String name = one1.get(i);
        // System.out.println(name);
        one2.add(name);
    }
}
// 第二个队伍
ArrayList<String> two = new ArrayList<>();
two.add("德罗巴");
two.add("德布劳内");
two.add("德约科维奇");
two.add("姆巴佩");
two.add("C罗");
two.add("德马科斯");
two.add("德尚");
two.add("尼古拉斯凯奇");
two.add("布莱恩特");
// 3.第二个队伍只要姓德的成员姓名;存储到一个新集合中。
ArrayList<String> two1 = new ArrayList<>();
for (String name : two) {
    if(name.startsWith("德")){
        two1.add(name);
    }
}
// 4.第二个队伍帅选之后不要前两个人;存储到一个新集合中。
ArrayList<String> two2 = new ArrayList<>();
for (int i = 0; i < two1.size(); i++) {
    if(i>=2) {
        String name = two1.get(i);
        two2.add(name);
    }
}
// 5.将两个队伍合并为一个队伍:存储到一个新集合中。
ArrayList<String> list = new ArrayList<>();
list.addAll(one2);
list.addAll(two2);
// 6.根据姓名创建Person对象;存储到一个新集合中。
ArrayList<Person> list_person = new ArrayList<>();
for (String name : list) {
    list_person.add(new Person(name));
}
// 7.打印整个队伍的Person对象信息。
for (Person person : list_person) {
    System.out.println(person);
}
1.6 练习:集合元素处理(Stream方式)

使用Stream流依次进行上题的步骤

// 第一个队伍
ArrayList<String> one = new ArrayList<>();
one.add("迪丽热巴");
one.add("玛尔扎哈");
one.add("宋");
one.add("赵匡胤");
one.add("赵无极");
one.add("赵大晓");
one.add("老子");
one.add("星宿老怪");
one.add("任登宝");
one.add("赵雷");

// 1.第一个队伍只要名字为3个字的成员姓名;存储到一个新集合中。
// 2.第一个队伍筛选之后只要前3个人;存储到一个新集合中。
Stream<String> stream1 = one.stream().filter(name->name.length()==3).limit(3);
// 第二个队伍
ArrayList<String> two = new ArrayList<>();
two.add("德罗巴");
two.add("德布劳内");
two.add("德约科维奇");
two.add("姆巴佩");
two.add("C罗");
two.add("德马科斯");
two.add("德尚");
two.add("尼古拉斯凯奇");
two.add("布莱恩特");
// 3.第二个队伍只要姓德的成员姓名;存储到一个新集合中。
// 4.第二个队伍帅选之后不要前两个人;存储到一个新集合中。
Stream<String> stream2 = two.stream().filter(name->name.startsWith("德")).skip(2);
// 5.将两个队伍合并为一个队伍:存储到一个新集合中。
// 6.根据姓名创建Person对象;存储到一个新集合中。
// 7.打印整个队伍的Person对象信息。
Stream.concat(stream1,stream2).map(name->new Person(name)).forEach(person -> System.out.println(person));
二、方法引用

方法引用是一种特殊类型的 Lambda 表达式。它们通常用于通过引用现有方法来创建简单的 Lambda 表达式。

2.1 冗余的Lambda场景及使用方法引用改进

首先要定义一个函数式接口Printable,然后定义方法并调用,参数该传递函数式接口。

// 定义一个方法,参数传递Printable接口,对字符串进行打印
public static void printString(Printable p) {
    p.print("HelloWorld");
}

public static void main(String[] args) {
    // 调用printString方法
    printString((s)->{
        System.out.println(s);
    });
    /*
        分析:
            Lambda表达式的目的,打印参数传递的字符串
            把参数s传递给System.out对象,调用out对象的println方法对字符串进行了输出
            注意:
                1、System.out对象已经存在
                2、println方法也是已经存在的
            所以我们可以使用方法引用来优化Lambda表达式
     */
    printString(System.out::println);
}
2.2 方法引用符

双冒号::引用运算符,所在的表达式被称为方法引用。如果Lambda要表达的函数方法已经存在于某个方法的实现中,那么就可以通过方法引用来简化。

语义分析
  • Lambda表达式写法:s->System.out.println(s); 拿到参数s之后再经Lambda之手传递给System.out.println方法来处理。
  • 方法引用写法:System.out::println 直接让System.out中的println方法来取代Lambda

注意Lambda中传递的参数一定是方法引用中的那个方法可以接受的类型,否则会抛出异常

推导与省略

函数式接口是Lambda的基础,而方法引用Lambda的进一步优化。使用这两种方式,都是可以根据“可推导就是可省略”的原则,无需指定参数类型,无需指定的重载形式,被自动推导出来。

2.3 通过对象名引用成员方法

除了函数式接口Printable,创建一个类,类中定义了成员方法。通过对象名引用成员方法,使用前提是对象名是已经存在的,成员方法也是已经存在的。

public class MethodRerObject {
    // 定义一个成员方法printUpperCaseString
    public void printUpperCaseString(String str){
        System.out.println(str.toUpperCase());
    }
}
public class Demo01ObjectMethodReference {
    // 定义一个方法,方法的参数传递Printable接口
    public static void printString(Printable p) {
        p.print("Hello");
    }
    public static void main(String[] args) {
        // 调用printString方法
        printString((s)->{
            // 创建对象
            MethodRerObject mro = new MethodRerObject();
            // 调用成员方法打印输出
            mro.printUpperCaseString(s);
        });
        /*
            使用方法引用优化Lambda
            对象时已经存在的MethodRerObject
            成员方法也是已经存在的printUpperCaseString方法
            所以可以方法引用
         */
        // 先创建对象
        MethodRerObject obj = new MethodRerObject();
        printString(obj::printUpperCaseString);
    }
}
2.4 通过类名引用静态方法

通过类名引用静态方法:类已经存在,静态成员方法也已经存在。

@FunctionalInterface
public interface Calcable {
    int calcAbs(int number);
}
public class Demo01StaticMethodReference {
    // 定义一个方法,方法的参数传递一个整数和一个函数式接口
    public static int method(int number, Calcable c) {
        return c.calcAbs(number);
    }

    public static void main(String[] args) {
        // 调用method
        int number = method(-10,(n) -> {
            return Math.abs(n);
        });
        System.out.println(number);
        /*
            使用方法引用优化:Math类是存在的,静态方法abs方法也是存在的
         */
        int number2 = method(-101,Math::abs);
        System.out.println(number2);
    }
}
2.5 通过super引用成员方法

通过super引用成员方法:super已经存在,父类的成员方法已经存在

public class Man extends Human {
    // 子类重写父类的成员方法
    @Override
    public void sayHello() {
        System.out.println("Hello 我是Man!");
    }
    // 定义一个方法参数传递函数式接口
    public void method(Greetable g) {
        g.greet();
    }
    public void show() {
         调用method方法,方法的参数Greetable是一个函数式接口,可传递Lambda表达式
        //method(()->{
        //    // 创建父类对象
        //    Human h = new Human();
        //    // 调用父类的sayHello方法
        //    h.sayHello();
        //});
        // 使用关键字super优化Lambda
        method(()->{super.sayHello();});

        // 使用方法引用:super已经存在,父类的成员方法sayHello已经存在
        method(super::sayHello);
    }
    public static void main(String[] args) {
        new Man().show();//Hello 我是Human!
    }
}
2.6 通过this引用成员方法

this使用方法引用来引用成员方法:this是已经存在的,成员方法是已经存在的

@FunctionalInterface
public interface Richable {
    void buy();
}
public class Husband {
    // 定义一个买房子的方法
    public void buyHouse() {
        System.out.println("北京二环内买一套四合院!");
    }

    // 定义一个结婚的方法,参数传递Richable接口
    public void marry(Richable r) {
        r.buy();
    }
    // 定义一个非常高兴的方法
    public void soHappy(){
        // 调用结婚的方法
        //marry(()->{
        //    // 使用this调用本类的成员方法
        //    this.buyHouse();
        //});

        // 使用方法引用优化:this是已经存在的,成员方法buyHouse是已经存在的
        marry(this::buyHouse);
    }

    public static void main(String[] args) {
        new Husband().soHappy();
    }
}
2.7 类的构造器引用

构造方法使用方法引用:类已知,构造方法已知

// 自己事先写个标准的Person类和函数式接口PersonBuilder
public class Demo {
    // 定义一个方法,传递String类型的姓名和函数式接口
    public static void printName(String name, PersonBuilder pb) {
        Person person = pb.buildPerson(name);
        System.out.println(person.getName());
    }

    public static void main(String[] args) {
        // 调用printName方法
        printName("赵晓",(name)->{
            return new Person(name);
        });

        /*
            使用方法引用优化:Person已知,构造方法new Person(String name)已知
         */
        printName("赵晓", Person::new);
    }
}
2.8 数组的构造器引用

数组构造器使用方法引用:已知创建的是int[],数组的长度已知,创建方法new已知

@FunctionalInterface
public interface ArrayBuilder {
    int[] buildArray(int length);
}
public class Demo {
    // 定义一个方法,参数传递一个整数和一个函数式接口,返回一个int类型数组
    public static int[] createArray(int length, ArrayBuilder ab){
        return ab.buildArray(length);
    }
    public static void main(String[] args) {
        // 调用createArray方法
        int[] array = createArray(10,(len)->{
            return new int[len];
        });
        System.out.println(array.length);

        // 使用方法引用优化:已知创建的是int[],数组的长度已知,创建方法new已知
        int[] arr = createArray(20,int[]::new);
        System.out.println(arr.length);
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值