1.1 概述

Steam流可以对集合数组的遍历等操作进行优化

传统集合的多步遍历代码

  1. public class Demo01ForEach{
  2. public static void main(String[] args){
  3. List<String> list= new ArrayList<>();
  4. list.add("Crisp");
  5. list.add("Roderick");
  6. list.add("function");
  7. for(String name : list){
  8. System.out.println(name);
  9. }
  10. }
  11. }

循环遍历的弊端

Java 8的Lambda让我们更加专注于做什么,而不是怎么做,仔细体会上例代码,可以发现:

  • for循环的语法就是怎么做
  • for循环的循环体才是做什么

为什么使循环?因为要进行遍历,但是循环是遍历的唯一方式吗?遍历是值每一个元素逐一进行处理,而并不是从第一个到最后一个顺序处理的循环,前者是目的,后者是方式

如果希望对集合进行过滤:

对集合进行三次过滤:

1.筛选姓张的人

2.筛选名字有三个字的人

3.最后对结果进行打印

传统写法

  1. /*用传统对方式,遍历集合,对集合中对数据进行过滤*/
  2. public class Demo01List {
  3. public static void main(String[] args) {
  4. //创建一个list集合
  5. List<String> list=new ArrayList<>();
  6. list.add("张无忌");
  7. list.add("周芷若");
  8. list.add("赵敏");
  9. list.add("张强");
  10. list.add("张三丰");
  11. //对list集合对元素进行过滤
  12. List<String> listA=new ArrayList<>();
  13. for (String s : list) {
  14. if(s.startsWith("张")){
  15. //把以张开头对元素存储到A中
  16. listA.add(s);
  17. }
  18. }
  19. //对ListA集合进行过滤,只要姓名长度为3的存储到新集合
  20. List<String> listB=new ArrayList<>();
  21. for (String s : listA) {
  22. if(s.length()==3){
  23. listB.add(s);
  24. }
  25. }
  26. //遍历ListB集合
  27. for (String s : listB) {
  28. System.out.println(s);
  29. }
  30. }
  31. }

Stream流写法

  1. /*使用Stream流对方式,遍历集合,对集合中对数据进行过滤
  2. * 关注的是做什么,而不是怎么做*/
  3. public class Demo02Stream {
  4. public static void main(String[] args) {
  5. //创建一个list集合
  6. List<String> list=new ArrayList<>();
  7. list.add("张无忌");
  8. list.add("周芷若");
  9. list.add("赵敏");
  10. list.add("张强");
  11. list.add("张三丰");
  12. //过滤,姓为张
  13. //过滤,长度为3
  14. //遍历
  15. list.stream()
  16. .filter(name->name.startsWith("张"))
  17. .filter(name->name.length()==3)
  18. .forEach(name-> System.out.println(name));
  19. }
  20. }

1.2 流式思想概述

整体来看,流式思想类似于工厂车间的“生产流水线

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

Tips:“Stream流”知识一个集合元素的函数模型,不是集合也不是数据结构,本身不存储任何元素(或地址值)

Stream流式一个来自数据源的元素队列

数据源->(步骤一)->(步骤二)->(步骤三)->[结果]

Stream流操作有两个基本特征:

  • Pipelining:中间操作都会返回流对象本身。
  • 内部迭代:Stream提供了内部迭代的方式,流可以直接调用遍历方法

使用流一般分为三个步骤:获取一个数据源(source)—->数据转换 —->执行操作获取想要的结果

1.3 获取流

java.util.stream.Stream<T>式java8新加入的最常用的流接口(不是函数式接口)

获取方式:

  • 所有的Collection集合都可以通过stream默认方法获取流
  • Stream接口的静态方法of可以获取数组对应的流
  1. public class Demo01GetStream {
  2. public static void main(String[] args) {
  3. //把集合转换为Stream
  4. List<String> list = new ArrayList<>();
  5. Stream<String> stream1 = list.stream();
  6. //Set集合转换Stream
  7. Set<String> set = new HashSet<>();
  8. Stream<String> stream2 = set.stream();
  9. //Map集合转换Stream(Map不是单列集合所以要拆分)
  10. Map<String,String> map = new HashMap<>();
  11. //获取键,存储到一个Set集合中
  12. Set<String> keySet = map.keySet();
  13. Stream<String> stream3 = keySet.stream();
  14. //获取值,存储到Collection集合中
  15. Collection<String> values = map.values();
  16. Stream<String> stream4 = values.stream();
  17. //获取键值对(键与值的映射关系)
  18. Set<Map.Entry<String, String>> entries = map.entrySet();
  19. Stream<Map.Entry<String, String>> stream5 = entries.stream();
  20. //数组转换为Stream
  21. Stream<Integer> stream6 = Stream.of(1, 2, 3, 4, 5);
  22. //可变参数可以传递数组
  23. Integer[] arr = {1,2,3,4,5};
  24. Stream<Integer> stream7 = Stream.of(arr);
  25. String[] arr2 = {"a","b","cc"};
  26. Stream<String> stream8 = Stream.of(arr2);
  27. }
  28. }

1.4 常用方法

流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种

  • 延迟方法:返回值类型仍然是Stream接口自身类型的方法,因此支持链式调用(除了终结方法外,其余方法均为延迟方法)

  • 终结方法:返回值类习惯不再是Strean接口自身类型的方法,因此不再支持类似StringBuilder那样的链式调用。例如countforEach

逐一处理:forEach

虽然名字叫forEach,但是和for循环中的“for-each”昵称不同

  1. void forEach(Consumer<? super T> action);

方法接收一个Consumer接口函数,会将每一个流元素交给该函数进行处理

复习Consumer

  1. java.util.function.Consumer<T>接口是一个消费型接口
  2. Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据

基本使用

//forEach方法,用来遍历流中的数据(终结方法,遍历之后就不能继续调用Stream流中的其他方法)
public class Demo02forEach {
    public static void main(String[] args) {
        //获取Stream流
        Stream<String> stream = Stream.of("Crisp", "Roderick", "Doinb", "Message");
        //使用Stream流中的方法forEach对Stream流中的数据进行遍历
        stream.forEach((String name)->{
            System.out.println(name);
        });
        //accept方法,Lambda表达式
    }
}
Crisp
Roderick
Doinb
Message

过滤:filter

可以通过filter方法将一个流转换成另一个子集流。

Stream<T> filter(Predicate<? super T> predicate);

该接口接收一个Predicate函数式接口作为筛选条件

复习Predicate接口

其中唯一的抽象方法

boolean test(T t);

该方法会产生一个boolean值结果。如果为true,那么filter方法将会留用元素;否则,舍弃元素

基本使用

public class Demo03Filter {
    public static void main(String[] args) {
        //创建Stream流
        Stream<String> stream = Stream.of("张三丰", "张翠山", "张三", "李四", "赵敏");
        //过滤
        Stream<String> stream2 = stream.filter((String name)->{return name.startsWith("张");});
        //遍历
        stream2.forEach(name-> System.out.println(name));
    }
}
张三丰
张翠山
张三

Stream流属于管道流,只能被消费一次。第一个Stream流调用完毕方法,数据就会流转到下一个Stream上,而这时上一个Stream就关闭了,无法调用方法了

映射:map

如果需要将流中的元素映射到另一个流中,可以使用map方法

<R> Stream<R> map(Function<? super T, ? extends R> mapper);

该接口需要一个Function函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流

复习Function接口

抽象方法:R apply(T t)

这可以将一种T类型转换为R类型,这种转换的动作就称为“映射”

基本使用

public class Demo04Map {
    public static void main(String[] args) {
        //获取String类型的Stream流
        Stream<String> stream = Stream.of("1", "2", "3", "4");
        //使用map方法,把字符串类型的整数,转换(映射)为Integer类型的整数
        Stream<Integer> integerStream = stream.map((String s) -> {
            return Integer.parseInt(s);
        });
        //遍历
        integerStream.forEach(num-> System.out.println(num));
    }
}

统计个数:count

正如旧集合Collection中的size方法一样。该方法统计元素个数

long count();

基本使用

System.out.println(stream.count());

Tips:该方法为终结方法

取用前几个:limit

limit方法可以对流进行截取

Stream<T> limit(long maxSize);

基本使用

public class Demo05Limit {
    //Limit方法是一个延迟方法,只是对流中的元素进行截取,返回的是一个新的流
    public static void main(String[] args) {
        //获取流
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6);
        //截取
        Stream<Integer> stream1 = stream.limit(4);
        //遍历
        stream1.forEach(num-> System.out.print(num+" "));
    }
}
1 2 3 4

跳过前几个:skip

如果希望跳过前几个元素,可以使用skip方法获取一个截取之后的新流

Stream<T> skip(long n);

如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流

基本使用

public class Demo06Skip {
    public static void main(String[] args) {
        //获取流
        Stream<Integer> stream = Stream.of(1, 2, 3, 4, 5, 6, 7);
        //使用skip方法,跳过前3个获取新流
        Stream<Integer> stream1 = stream.skip(3);
        //遍历
        stream1.forEach(num-> System.out.print(num+" "));
    }
}
4 5 6 7

组合:concat

如果有两个流,希望合并成为一个流,那么可以使用Stream接口的静态方法concat

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)

Tips:这是一个静态方法,与java.lang.String当中的concat方法是不同的

基本使用

public class Demo07Concat {
    public static void main(String[] args) {
        //获取流
        Stream<String> stream = Stream.of("1", "2", "3");
        Stream<String> stream1 = Stream.of("Crisp", "Roderick");
        //组合
        Stream<String> concat = Stream.concat(stream, stream1);
        //遍历
        concat.forEach(s-> System.out.print(s+" "));
    }
}
1 2 3 Crisp Roderick

1.5 练习:集合元素处理

题目

现有两个ArrayList集合存储队伍当中的多个成员姓名,进行下列操作:

1.第一个队伍只要名字为3个字的成员姓名

2.第一个队伍筛选之后只要前三人

3.第二个队伍只要姓张的成员姓名

4.第二个队伍筛选之后不要前2人

5.将两个队伍合并为一个队伍

6.根据姓名创建`Person`对象

7.打印person对象信息
public class Test {
    public static void main(String[] args) {
        //第一支队伍
        ArrayList<String> one =new ArrayList<>();
        one.add("迪丽热巴");
        one.add("宋远桥");
        one.add("苏星河");
        one.add("石破天");
        one.add("臭豆腐");
        one.add("老子");
        //第二支队伍
        ArrayList<String> two = new ArrayList<>();
        two.add("古力娜扎");
        two.add("张无忌");
        two.add("赵丽颖");
        two.add("张三丰");
        two.add("尼古拉斯赵四");
        two.add("张天爱");
        two.add("张二狗");
        //1获取两个流
        Stream<String> streamOne = one.stream();
        Stream<String> streamTwo = two.stream();
        //进行第一个队伍的处理
        Stream<String> streamOneResult = streamOne.filter((String name) -> {
            return name.length() == 3;
        }).limit(3);
        //第二个队伍的处理
        Stream<String> streamTwoResult = streamTwo.filter((String name) -> {
            return name.startsWith("张");
        }).skip(2);

        //合并队伍
        Stream<String> concat = Stream.concat(streamOneResult, streamTwoResult);
        //Person类
        concat.map((String name)->{ return new Person(name);}).forEach(p-> System.out.println(p));
    }
}

二、方法引用

可以理解为Lambda表达式的另一种表现形式

类型 语法 对应的Lambda表达式
静态方法 类名::staticMehod (Args) -> 类名.staticMehtod(args)
实例方法 Inst::instMethod (args) -> inst.instMethod(args)
对象方法 类名::instMethod (Inst,args) -> 类名.instMethod(args)
构造方法 类名::new (args)->new 类名(args)

TIPS: args=参数

2.1 方法引用符

双冒号::为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以使用双冒号来引用该方法作为Lambda的替代者

  • Lambda表达式写法:s -> System.out.println(s);
  • 方法引用写法: System.out :: println

2.2 静态方法引用

有一个Person类:

@Data
public class Person {

    private String name;

    private Integer age;

    public static int compareByAge(Person a, Person b) {
        return a.age.compareTo(b.age);
    }

}

假设一个部门30人,为了根据年龄排序我们通常会写一个比较器:

Person[] rosterAsArray = new Person[30];
// 添加数组元素省略

class PersonAgeComparator implements Comparator<Person> {
    public int compare(Person a, Person b) {
        return a.getBirthday().compareTo(b.getBirthday());
    }
}

Arrays.sort(rosterAsArray, new PersonAgeComparator());

然而,Person类中已经有了一个静态方法比较器:compareByAge,:

//使用Lambda:
Person[] rosterAsArray = new Person[30];
//添加数组元素省略
Array.sort(rosterAsArray, (a,b) -> Person.compareByAge(a,b));

//使用方法引用
Arrays.sort(rosterAsArray, Person::compareByAge);

另外一个例子:

public class Test {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(82,22,34,50,9);
        list.sort(Integer::compare);
        System.out.println(list);
    }
}

2.3 示例方法引用

实例方法引用,顾名思义就是调用已经存在的实例的方法,与静态方法引用不同的是类要先实例化,静态方法引用类无需实例化,直接用类名去调用。

@Data
class User {

    private String name;
    private Integer age;

    public User(String name, Integer age) {
        this.name = name;
        this.age = age;
    }
}

public class TestInstanceReference {

    public static void main(String[] args) {

        TestInstanceReference test = new TestInstanceReference();
        User user = new User("欧阳峰",32);
        Supplier<String> supplier = () -> user.getName();
        System.out.println("Lambda表达式输出结果:" + supplier.get());

        Supplier<String> supplier2 = user::getName;
        System.out.println("实例方法引用输出结果:" + supplier2.get());
    }
}

2.4 方法引用对象

/*
* 通过对象名引用成员方法
* 使用前提是对象名已经存在,成员方法也是已经存在
* */
public class Demo02ObjectMethodReference {
    //定义方法
    public static void printString(Printable p){
        p.print("Hello");
    }

    public static void main(String[] args) {
        //调用printString方法,参数Printable是函数式接口
        printString((s)->{
            //创建对象
            Demo01Object obj=new Demo01Object();
            //调用对象的成员方法
            obj.printUpperCaseString(s);
        });
        /*
        * 使用方法引用
        * 对象已经存在:Demo01Object
        * 成员方法已经存在:printUpperCase
        * 可以使用对象名引用成员方法
        * */
        Demo01Object obj=new Demo01Object();
        printString(obj::printUpperCaseString);
    }
}

2.5 构造方法引用

注意:需要调用的构造器的参数列表要与函数式接口中抽象方法的参数列表保持一致。

如:要获取一个空的User列表:

Supplier<List<User>> userSupplier = () -> new ArrayList<>();
List<User> user = userSupplier.get();

Supplier<List<User>> userSupplier2 = ArrayList<User>::new; //构造方法引用写法
List<User> user2 = userSupplier.get();
PrintName("雨落",(String name)->{return new Person(name);});

//方法引用
printName("雨落",Person::new);

2.6 数组构造器引用

public static int[] createArray(int length,ArrayBuilder ab){
        return ab.builderArray(lenth);//另外创建的接口方法
}

//Lambda
int[] arr1 = createArray(10,(len)->{return new int[len];});
//方法引用 (已知数组长度)
int[] arr1 = createArray(10,int[]::new);