Java Stream流及方法引用

1 Stream流

说道Stream便容易想到 I/O Stream, 而实际上, 在Java 8中, 得益于Lambda所带来的函数式编程 ,引入了全新的Stream概念, 用于解决已有集合类库既有的弊端

将 集合 , 数组转换为Stream流, 使用Stream流中的方法对数组, 集合 进行操作

循环遍历的弊端

for循环的语法就是 “怎么做” , for循环的循环体才是"做什么"; 循环并不是遍历的唯一方式, 遍历是指每一个元素注意进行处理, 而并不是从第一个到最后一个顺序处理的循环, 前者是目的, 后者是方式

如果希望对集合中的元素记性筛选过滤:

  1. 将集合A根据条件一过滤为子集B
  2. 然后再根据条件二过滤为子集C

Stream的更优写法

// 使用Stream的方式,比那里结合,对集合中的元素进行过滤
public class DemoStream{
    public class void main(String[] args){
        List<String> list = new ArrayList<>();
        list.add("张无忌");
        list.add("周芷若");
        list.add("赵敏");
        list.add("张三丰");
        
        // 对list集合中的元素进行过滤, 以张开头的元素, 存储到一个新的集合
        // 对listA集合进行过滤,名字长度为3的人, 存储到一个新集合中
        list.stream()
        		.filter(name -> name.startsWith("张"))
        		.filter(name -> name.length()==3)
        		.forEach(name -> System.out.println(name));
    }
}

流式思想的概述

Stream流其实是一个集合元素的函数模型, 它并不是集合, 也不是数据结构, 其本身并不存储任何元素(或其地址值)

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

  • 元素是特定类型的对象, 形成一个队列; Java中的stream并不会存储元素, 而是按需计算

  • 数据源 : 流的来源, 可以是集合,数组等, 和以前的Collection操作不同, Stream操作还有两个基础的特征

  • Pipelining :中间操作都会返回流对象对象, 这样多个操作可以串联成一个管道, 如同流式风格; 这样做可以对操作进行优化, 比如延迟执行和短路

  • 内部迭代 : 以前对集合遍历都是通过iterator或者增强for的方式, 显式的在集合外部进行迭代, 这就叫做外部迭代, Stream提供了内部迭代的方式, 流可以直接调用遍历方法

当使用一个流的时候, 通常包括这三个步骤, 获取一个数据源(source) -> 数据转换 -> 执行操作获取想要的结果, 每次转换原有stream对象不改变, 但会返回一个新的stream对象(可以有多次转换), 这就允许对其操作可以像链条一样排列, 变成一个管道

获取流

java.util.stream.Stream\<T> 是Java8新加入的最常用的接口(这并不是一个函数式接口)

获取一个流非常简单

  • 所有的Collection集合都可以通过stream默认方法获取流
  • Stream 接口的静态方法 of 可以获取数组对应的流, of方法的参数是一个可变参数

根据Collection,List获取流

public class DemoGetStream{
    public class void main(String[] args){
        // 把集合转换为Stream流
        List<String> list = new ArrayList<>();
        Stream<String> stream1 = list.stream();
		
		Set<String> set = new HashSet<>();
		Stream<String> stream2 = list.stream();
		
		Map<String,String> map = new HashMap();
		// 获取键, 存储到一个Set集合
		Set<String> keySet = map.keySet();
		Stream<String> stream3 = keySet.stream();
		
		// 获取值,存储到Collection结合
		Collection<String> values = map.values();
		Stream<String> stream4 = values.stream();
		
		// 获取键值对entrySet
		Set<Map.Entry<String,String>> entries = map.entrySet()
		Stream<Map.Entry<String,String>> stream5 = values.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);
        
        
    }
}

常用方法

nums.stream().filter(num -> num!=null).count()

// stream() 创建stream
// filter(num -> num!=null)  转换stream
// count()  聚合
  • 延迟方法 : 返回值类型仍然是 Stream 接口自身类型的方法, 因此支持链式调用(除了终结方法哎, 其余方法均为延迟方法)
  • 终结方法 : 返回值不再是Stream接口自身类型的方法, 因此不再支持类似StringBuilder那样的链式调用, 本小节中终结方法包括count() 和forEach()

逐一处理:forEach

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

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

public class DemoStreamForEach{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六", "田七");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	stream.forEach( name -> System.out.println(name));
    }
}

** 过滤: filter**

通过 filter() 可以将一个流转换成另外一个子集流

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

该接口接受一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件

public class DemoStreamFilter{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> stream = Stream.of("张三", "李四", "王五", "赵六", "田七");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	Stream<String> stream2 = stream.filter( (String name) -> {return name.startsWith("张");});
       	stream.forEach( name -> System.out.println(name));
    } 
}

映射:map

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

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

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

public class DemoStreamMap{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> stream = Stream.of("1", "2", "3", "4", "5");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	Stream<String> stream2 = stream.map( (String s) -> {return Integer.parseInt(s);});
       	stream.forEach( i -> System.out.println(i));
    } 
}

统计个数 : count

正如集合Collection当中的size 方法一样, 流提供count方法来说数一数其中的元素个数

long count();

该方法返回一个long值代表元素个数(不再像旧集合那样是int值)

public class DemoStreamCount{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	Stream<String> result = stream.filter( (String s) -> {return s.startsWith("张");});
       	stream.forEach(result.count());
    } 
}

取用前几个 : limit

limit方法可以对流进行截取, 只取用前 n 个

Stream<T>  limit(long maxSize);
public class DemoStreamCount{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	Stream<String> result = original.limit(2); // 只取前2个
    } 
}

跳过前几个 : skip

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

Stream<T> skip(long n);  后面的参数为 几, 就跳过几个
public class DemoStreamCount{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	Stream<String> result = original.skip(2); // 只剩余最后一个 "周芷若"
    } 
}

组合 : concat

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

static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends> b)
public class DemoStreamCount{
    public class void main(String[] args){
        // 创建一个Stream流
       	Stream<String> streamA = Stream.of("张无忌");
       	Stream<String> streamB = Stream.of("张三丰");
       	// 使用stream流中的方法forEach对stream流中的数据进行遍历
       	Stream<String> result = Stream.concat(streamA,streamB); 
    } 
}

2 方法引用

冗余的Lambda场景

来看一个简单的函数式接口以应用Lambda表达式:

1 @FunctionalInterface
2 public interface Printable {
3     void print(String str);
4 }

在 Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:

1 public class Demo01PrintSimple {
2   private static void printString(Printable data) {
3     data.print("Hello, World!");
4   } 
5   public static void main(String[] args) {
6     printString(s ‐> System.out.println(s));
7   }
8 }

其中 printString 方法只管调用 Printable 接口的 print 方法,而并不管 print 方法的具体实现逻辑会将字符串打印到什么地方去。

而 main 方法通过Lambda表达式指定了函数式接口 Printable 的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它。

问题分析

这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是 System.out对象中的 println(String) 方法。

既然Lambda希望做的事情就是调用 println(String) 方法,那何必自己手动调用呢?

用方法引用改进代码

能否省去Lambda的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

1  public class DemoPrintRef {
2         private static void printString(Printable data) {
3             data.print("Hello, World!");
4         } 
5         public static void main(String[] args) {
6             printString(System.out::println);
7         }
8     }

请注意其中的双冒号 :: 写法,这被称为“方法引用”,而双冒号是一种新的语法。

方法引用符

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

语义分析

例如上例中, System.out 对象中有一个重载的 println(String) 方法恰好就是我们所需要的。那么对于printString 方法的函数式接口参数,对比下面两种写法,完全等效:

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

第一种语义是指:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理。

第二种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全样,而第二种方法引用的写法复用了已有方案,更加简洁

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

推导与省略

如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。

下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为int类型的参数:

1 @FunctionalInterface
2 public interface PrintableInteger {
3     void print(int str);
4 }

由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化:

1  public class DemoPrintOverload {
2         private static void printInteger(PrintableInteger data) {
3             data.print(1024);
4         } 
5         public static void main(String[] args) {
6             printInteger(System.out::println);
7         }
8     }

这次方法引用将会自动匹配到 println(int) 的重载形式

通过对象名引用成员方法

这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:

1 public class MethodRefObject {
2   public void printUpperCase(String str) {
3     System.out.println(str.toUpperCase());
4   }
5 }

函数式接口仍然定义为:

1 @FunctionalInterface
2 public interface Printable {
3   void print(String str);
4 }

那么当需要使用这个 printUpperCase 成员方法来替代 Printable 接口的Lambda的时候,已经具有了MethodRefObject 类的对象实例,则可以通过对象名引用成员方法,代码为:

1 public class DemoMethodRef {
2   private static void printString(Printable lambda) {
3     lambda.print("Hello");
4   } 
5   public static void main(String[] args) {
6     MethodRefObject obj = new MethodRefObject();
7     printString(obj::printUpperCase);
8   }
9 }

通过类名称引用静态方法

由于在 java.lang.Math 类中已经存在了静态方法 abs ,所以当我们需要通过Lambda来调用该方法时,有两种写法。首先是函数式接口:

1 @FunctionalInterface
2 public interface Calcable {
3   int calc(int num);
4 }

第一种写法是使用Lambda表达式:

1 public class DemoLambda {
2   private static void method(int num, Calcable lambda) {
3     System.out.println(lambda.calc(num));
4   } 
5   public static void main(String[] args) {
6     method(‐10, n ‐> Math.abs(n));
7   }
8 }

但是使用方法引用的更好写法是:

1 public class Demo06MethodRef {
2   private static void method(int num, Calcable lambda) {
3     System.out.println(lambda.calc(num));
4   }
5   public static void main(String[] args) {
6     method(‐10, Math::abs);
7   }
8 }

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: n -> Math.abs(n)
  • 方法引用: Math::abs

通过super引用成员方法

如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:

1 @FunctionalInterface
2 public interface Greetable {
3   void greet();
4 }

然后是父类 Human 的内容:

1 public class Human {
2   public void sayHello() {
3     System.out.println("Hello!");
4   }
5 }

最后是子类 Man 的内容,其中使用了Lambda的写法

 1 public class Man extends Human {
 2         @Override
 3         public void sayHello() {
 4             System.out.println("大家好,我是Man!");
 5         } 
 6         //定义方法method,参数传递Greetable接口
 7         public void method(Greetable g){
 8             g.greet();
 9         } 
10         public void show(){
11             //调用method方法,使用Lambda表达式
12             method(()‐>{
13                     //创建Human对象,调用sayHello方法
14                     new Human().sayHello();
15             });
16             //简化Lambda
17             method(()‐>new Human().sayHello());
18             //使用super关键字代替父类对象
19             method(()‐>super.sayHello());
20         }
21     }

但是如果使用方法引用来调用父类中的 sayHello 方法会更好,例如另一个子类 Woman :

 1 public class Man extends Human {
 2         @Override
 3         public void sayHello() {
 4             System.out.println("大家好,我是Man!");
 5         } 
 6         //定义方法method,参数传递Greetable接口
 7         public void method(Greetable g){
 8             g.greet();
 9         } 
10         public void show(){
11             method(super::sayHello);
12         }
13     }

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: () -> super.sayHello()
  • 方法引用: super::sayHello

通过this引用成员方法

this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。首先是简单的函数式接口:

1 @FunctionalInterface
2 public interface Richable {
3   void buy();
4 }

下面是一个丈夫 Husband 类:

1 public class Husband {
2   private void marry(Richable lambda) {
3     lambda.buy();
4   } 
5   public void beHappy() {
6     marry(() ‐> System.out.println("买套房子"));
7   }
8 }

开心方法 beHappy 调用了结婚方法 marry ,后者的参数为函数式接口 Richable ,所以需要一个Lambda表达式。
  但是如果这个Lambda表达式的内容已经在本类当中存在了,则可以对 Husband 丈夫类进行修改:

 1 public class Husband {
 2   private void buyHouse() {
 3     System.out.println("买套房子");
 4   } 
 5   private void marry(Richable lambda) {
 6     lambda.buy();
 7   } 
 8   public void beHappy() {
 9     marry(() ‐> this.buyHouse());
10   }
11 }

如果希望取消掉Lambda表达式,用方法引用进行替换,则更好的写法为:

 1 public class Husband {
 2   private void buyHouse() {
 3     System.out.println("买套房子");
 4   } 
 5   private void marry(Richable lambda) {
 6     lambda.buy();
 7   }
 8   public void beHappy() {
 9     marry(this::buyHouse);
10   }
11 }

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: () -> this.buyHouse()
  • 方法引用: this::buyHouse

类的构造器引用

由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:

 1 public class Person {
 2   private String name;
 3    public Person(String name) {
 4     this.name = name;
 5 } 
 6   public String getName() {
 7     return name;
 8 } 
 9   public void setName(String name) {
10     this.name = name;
11 }
12 }

然后是用来创建 Person 对象的函数式接口:

1 public interface PersonBuilder {
2   Person buildPerson(String name);
3 }

要使用这个函数式接口,可以通过Lambda表达式:

1 public class DemoLambda {
2   public static void printName(String name, PersonBuilder builder) {
3     System.out.println(builder.buildPerson(name).getName());
4   } 
5   public static void main(String[] args) {
6     printName("赵丽颖", name ‐> new Person(name));
7   }
8 }

但是通过构造器引用,有更好的写法:

1 public class Demo10ConstructorRef {
2   public static void printName(String name, PersonBuilder builder) {
3     System.out.println(builder.buildPerson(name).getName());
4   } 
5   public static void main(String[] args) {
6     printName("赵丽颖", Person::new);
7   }
8 }

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: name -> new Person(name)
  • 方法引用: Person::new

数组的构造器引用

数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时,需要一个函数式接口:

1 @FunctionalInterface
2 public interface ArrayBuilder {
3   int[] buildArray(int length);
4 }

在应用该接口的时候,可以通过Lambda表达式:

1 public class DemoArrayInitRef {
2   private static int[] initArray(int length, ArrayBuilder builder) {
3     return builder.buildArray(length);
4   } 
5   public static void main(String[] args) {
6     int[] array = initArray(10, length ‐> new int[length]);
7   }
8 }

但是更好的写法是使用数组的构造器引用 :

在这个例子中,下面两种写法是等效的:

  • Lambda表达式: length -> new int[length]
  • 方法引用: int[]::new
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值