F4DE F4DE
首页
技术
笔记
随笔
关于
友链
归档

F4DE

Web Security
首页
技术
笔记
随笔
关于
友链
归档
  • Java相关

    • Stream
      • Lambda Function
        • 一般形式
        • FunctionalInterface
        • 方法引用
      • Stream
        • Steam介绍
        • Intermediate operation
        • Ternimal operations
        • Other features
    • javassist
  • 笔记
  • Java相关
F4DE
2021-02-25

Stream

流(stream)是Java8中的一个重要特性,利用匿名函数和流可以极大简化一些繁琐的操作。

# Lambda Function

# 一般形式

lambda的一般形式,由两部分组成:

(par1, par2···) -> {
    // code block
    return xxx;
}

(par1, par2···)表示参数,类型可以省略,因为编译器可以自动推断出参数类型,{···}中间的内容表示方法体,就是要执行的代码。

如果方法体中只有一行return xxx的代码,那么完全可以更加简洁:

(par1, par2···) -> xxx;

比如下面的例子:

(s1, s2) -> {
    return s1.equals(s2);
}

// 等同于

(s1, s2) -> s1.equals(s2);

函数返回值的类型也是由编译器自动判断的。

# FunctionalInterface

由于lambda创建了一个匿名函数,我们可以把这个函数交给一个变量,然后利用这个变量去执行我们写好的函数,比如在python中,我们可以这样做:

In [1]: func = lambda x : x+2

In [2]: print(func(1))
3

但由于Java是强类型语言,在基本类型和引用类型中没有一个用来表示“这个变量是一个函数”的类型,但是Java8为我们提供一种函数接口,一个lambda表达式可以赋值给一个函数接口的对象,然后利用这个对象去调用lambda表达式。

我们可以使用注解@FunctionalInterface来定义一个函数接口,比如这样:

@FunctionalInterface
interface LambdaFunc {
    boolean strCompare(String s1, String s2);
}

public class Demo1 {
    public static void main(String[] args) {
        LambdaFunc test = (s1, s2) -> s1.equals(s2);

        System.out.println(test.strCompare("123", "456"));
    }
}

/*
false

Process finished with exit code 0
*/

一个函数接口具有一下特点:

  1. 函数接口是单(抽象)方法接口
  2. 接口中抽象方法的返回值就是我们所写的lambda表达式中函数体的返回值
  3. 接口中抽象方法的参数就是我们所写的lambda表达式中的参数
  4. 只有显式调用函数接口对象.抽象方法并传入相应的参数才会调用对应的lambda表达式

其实有些函数接口我们没有必要自己实现,Java8中的java.util.function包中给我们提供了很多的函数接口,比如下面这样:

public class Demo1 {
    public static void main(String[] args) {
        BiPredicate<String, String> function = (String s1, String s2) -> s1.equals(s2);

        System.out.println(function.test("123", "456"));
    }
}

/*
false

Process finished with exit code 0
*/

BiPredicate就是一个function包中内置的一个函数接口,它的抽象方法会接受两个参数,并且返回布尔值:

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210104112204.png

# 方法引用

上面的例子还可以进一步化简:

public class Demo1 {
    public static void main(String[] args) {
        BiPredicate<String, String> function = String::equals;

        System.out.println(function.test("123", "456"));
    }
}

/*
false

Process finished with exit code 0
*/

我们把(String s1, String s2) -> s1.equals(s2)简化成了String::equals,这种简化方式称作方法引用,所谓方法引用,就是某个方法的签名和接口的抽象方法签名恰好一致,那么就可以直接把方法引用作为lambda表达式传入接口。

s -> System.out.println(s)  <==>  System.out::println

# Stream

Java 8 STREAMS Tutorial - YouTube (opens new window)

# Steam介绍

Stream(流)是Java8中引入的全新API

Steam本身并不会保存数据,而是类似于一个比较高级的迭代器,数据源中的元素单向传递到流中,不可以往复,只能遍历一次,就像水流一样一去不复返。

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210107175021.png

在图中涉及到了两个名词:Intermediate operations和Ternimal operations,这是流中的两个重要操作。

Stream分为中间操作(Intermediate operations)和终端操作(Ternimal operations):

  • 中间操作用于对流进行过滤、转换、排序等,比如filter、map、sort,多个中间操作可以被串联起来,一个流经过中间操作之后还是一个流。
  • 终端操作用于将流转换为其他Java对象,比如forEach、collect、reduce等。

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210107175251.png

一个流中可以有零个或者多个中间操作,对于比较大的数据集:我们应该遵循的一个原则是先使用filter将其过滤,然后再使用其他的中间操作比如map、sort等。

一个流只能有一个终端操作,一些常用的终端操作作用如下:

  • forEach:对于对于每个流中的元素使用相同的函数
  • collect:将流中的元素汇总到一个集合里
  • ······

在使用stream的时候,基本会遵循下面三个步骤:

  1. 创建stream
  2. 进行流的转换,返回一个新的stream对象(链式操作)
  3. 将流转换为其他Java对象

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210107175912.png

一个流可以通过Collections、Lists、Sets、ints、longs、doubles、arrays、lines of file创建出来。

# Intermediate operation

intermediate ooperation(中间操作):一个流后面可以串联一个或多个中间操作。中间操作可以打开流,对流中的数据进行过滤、映射等,并返回一个新的流供之后操作实用。

# map

public class Demo1 {
    public static void main(String[] args) {

        Stream.of(1, 2, 3, 4, 5, 6).map(x -> x + 2).forEach(System.out::println);

    }
}

/*
3
4
5
6
7
8

Process finished with exit code 0
*/

中间操作map用于将一个对象转换为另一个对象,它的签名要求实现一个Function函数接口:

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210106122927.png

这个函数接口的静态方法是接受一个T类型的参数,然后返回一个R类型的对象:

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210106123014.png

对于上面的例子,我们做的操作就是把X + 2,所以T和R都是Integer。

public class Demo1 {
    public static void main(String[] args) {

        Stream.of(1, 2, 3, 4, 5, 6).map(String::valueOf).forEach(s -> System.out.println(s.getClass().getName()));

    }
}

/*
java.lang.String
java.lang.String
java.lang.String
java.lang.String
java.lang.String
java.lang.String

Process finished with exit code 0

对于这个例子来说,T就是Integer,R就是String。

# filter

public class Demo1 {
    public static void main(String[] args) {

        Stream.of("AI", "Ankit", "Kushal", "Bob", "Susan", "Hans").filter(s -> s.startsWith("A")).forEach(System.out::println);

    }
}

/*
AI
Ankit

Process finished with exit code 0

filter是一个过滤器,可以通过给定的条件把流中不符合条件的元素剔除掉,它的签名要求我们实现一个Predicate函数接口,这个函数接口的静态方法接收一个T类型的参数,而返回一个布尔值:

https://cdn.jsdelivr.net/gh/F4ded/blog-pic/blog/20210106123958.png

下一个例子,通过map将流中的每个元素首字母转换为小写,然后再用filter过滤出符合条件的元素:

public class Demo1 {
    public static void main(String[] args) {

        Stream.of("AI", "Ankit", "Kushal", "Bob", "Susan", "Hans").map(String::toLowerCase).filter(s -> s.startsWith("a")).forEach(System.out::println);

    }
}

/*
ai
ankit

Process finished with exit code 0

# distinct

对于流中的元素进行去重,不需要先把其转换为Set,只需要实用distinct中间操作即可:

public class Demo1 {
    public static void main(String[] args) {

        List<Integer> list = List.of(1, 2, 3, 4, 5, 1);
        list.stream().distinct().forEach(System.out::println);

    }
}

/*
1
2
3
4
5

# Ternimal operations

Ternimal operations(终端操作):终端操作是流的链式调用的最后一步,被终端操作处理过的流就会变成其他的Java对象。在一个流的链式调用流程中,终端操作只会被调用一次,调用之后流就会关闭。

# forEach

forEach是最常用的一个终端操作,它的参数是一个Consumer函数接口(一元),对流中的每一个元素执行相同的操作,最常见的就是打印流中的每个元素:

public class Demo1 {
    public static void main(String[] args) {
        Stream.of(1, 2, 3, 4, 5).map(x -> x + 1).forEach(System.out::println);
    }
}

/*
2
3
4
5
6

# reduce

reduce(聚合)操作可以通过聚合函数把流中的元素聚合成一个结果。

reduce方法传入的是一个BinaryOperator接口对象,它的apply方法是一个二元抽象方法,会把传入的两个参数进行运算,并返回运算的结果:

T apply(T t, T u);

下面的代码实用了聚合操作来进行求和:

public class Demo1 {
    public static void main(String[] args) {

        Integer integer = Stream.of(1, 2, 3, 4, 5).reduce(0, (sum, i) -> sum + i);
        System.out.println("integer = " + integer);

    }
}

/*
integer = 15

这样似乎不是很好理解,可以使用for循环来改写一下上面的代码:

public class Demo1 {
    public static void main(String[] args) throws Exception {
        int[] arrays = {1, 2, 3, 4, 5};
        int sum = 0;
        for (int i : arrays) {
            System.out.printf("sum = sum + i = %d + %d\n", sum, i);
            sum += i;
        }
        System.out.println(sum);
    }

值得注意的是,我们上面的代码在reduce方法中传入一个初始值0,这样一来,在第一轮中,0就会被作为函数(sum, i) -> sum + i中sum的初始值,而i的初始值就是Stream中的第一个元素1,执行完第一轮的结果为0 + 1 = 1,而这个结果又会作为第二轮中sum的初始值,i的值为Stream中的第二个元素2,如此达到了累加的效果。

sum = sum + i = 0 + 1 = 1
sum = sum + i = 1 + 2 = 3
sum = sum + i = 3 + 3 = 6
sum = sum + i = 6 + 4 = 10
sum = sum + i = 10 + 5 = 15
15

此外,reduce方法还可以不传入初始值,这样一来,第一轮中的sum的初始值就变为了Stream中的第一个元素1,i的初始值就变成了Stream中的第二个元素2;reduce操作返回的结果也变成了Optional<Integer>对象,需要多调用一个get方法才能得到运算的结果。

public class Demo1 {
    public static void main(String[] args) throws Exception {
        Optional<Integer> integer = Stream.of(1, 2, 3, 4, 5).reduce((sum, i) -> sum + i);
        System.out.println("integer = " + integer.get());
    }
}

/*
integer = 15

sum = sum + i = 1 + 2 = 3
sum = sum + i = 3 + 3 = 6
sum = sum + i = 6 + 4 = 10
sum = sum + i = 10 + 5 = 15
15

灵活使用reduce,可以达到一些神奇的效果,比如把配置文件中的key=value这种形式的字符串作为键值对储存在Map中:

public class Demo2 {
    public static void main(String[] args) {
        List<String> props = List.of("profile=native", "debug=true", "logging=warn", "interval=500");
        Map<String, String> propMap = props.stream().map(line -> {
            String[] split = line.split("=");
            return Map.of(split[0], split[1]);
        }).reduce(new HashMap<String, String>(), (map, kvmap) -> {
            System.out.println(kvmap.toString() + " -> HashMap");
            map.putAll(kvmap);
            return map;
        });

        propMap.forEach((key, value) -> {
            System.out.println(key + ": " + value);
        });
    }
}

{profile=native} -> HashMap
{debug=true} -> HashMap
{logging=warn} -> HashMap
{interval=500} -> HashMap
logging: warn
interval: 500
debug: true
profile: native

# Other features

Stream具有延时性:只有当终端操作存在的时候,中间的操作才会被执行。

public class Demo1 {
    public static void main(String[] args) {
        
        Stream.of("a1", "a2", "a3", "a4").filter(s -> {
            System.out.println("filter: " + s);
            return true;
        });

    }
}

// 不打印结果

public class Demo1 {
    public static void main(String[] args) {
        
        Stream.of("a1", "a2", "a3", "a4").filter(s -> {
            System.out.println("filter: " + s);
            return true;
        }).forEach(s -> System.out.println("forEach: " + s));

    }
}

/*
filter: a1
forEach: a1
filter: a2
forEach: a2
filter: a3
forEach: a3
filter: a4
forEach: a4

Process finished with exit code 0
*/

这样设计是出于性能的考虑,Stream垂直执行可以减少对每个元素的操作个数

public class Demo1 {
    public static void main(String[] args) {

        Stream.of("a1", "b1", "c1").map(x -> {
            System.out.println("map: " + x);
            return x.toUpperCase();
        }).anyMatch(x -> {
            System.out.println("anyMatch: " + x);
            return x.startsWith("B");
        });

    }
}

/*
map: a1
anyMatch: A1
map: b1
anyMatch: B1
*/

anyMatch()方法是一个聚合操作,和中间操作filter()类似,可以传入一个返回值为布尔类型的lambda表达式,当表达式返回值为True的时候就结束循环。

Stream是不能复用的,当调用聚合操作之后流就关闭了,此时如果再调用流则会抛出异常。

public class Demo1 {
    public static void main(String[] args) {

        Stream<String> stream = Stream.of("a1", "b1", "c1").map(x -> {
            System.out.println("map: " + x);
            return x.toUpperCase();
        });
        stream.anyMatch(s -> {
            System.out.println("Stream close");
            return true;
        });
        stream.filter(s -> true);

    }
}

/*
map: a1
Stream close
Exception in thread "main" java.lang.IllegalStateException: stream has already been operated upon or closed
  at java.util.stream.AbstractPipeline.<init>(AbstractPipeline.java:203)
  at java.util.stream.ReferencePipeline.<init>(ReferencePipeline.java:94)
  at java.util.stream.ReferencePipeline$StatelessOp.<init>(ReferencePipeline.java:618)
  at java.util.stream.ReferencePipeline$2.<init>(ReferencePipeline.java:163)
  at java.util.stream.ReferencePipeline.filter(ReferencePipeline.java:162)
  at steam.Demo1.main(Demo1.java:18)
*/

javassist

javassist→

最近更新
01
在Shiro中使用无CommonsCollections依赖的CommonsBeanUtils利用链
04-19
02
CommonsBeanUtils
04-19
03
基于Tomcat全局存储进行回显
04-16
更多文章>
Theme by Vdoing | Copyright © 2020-2021
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×