一、Stream介绍

  stream是流式处理的一个关键的抽象,包括Stream,IntStream,LongStream 和 DoubleStream等等,首先我们来看一下类之间的关系

java流式sql java流式处理_操作符

最上层的接口是AutoCloseable接口,因为我们知道流式处理会涉及到一些资源,所以为了能够被正确的释放,这里通过AutoCloseable接口来处理,就是在我们使用try-with-resources声明的时候,会自动的调用close()方法在最后的时候释放资源。

接着是BaseStream接口,这个也是IntStream,LongStream 等都会继承的一个接口。在这个接口里面定义了流操作的基本接口,支持顺序和并行聚合操作的元素序列。

可能有人会好奇,流和集合有什么区别呢?通过Java官网文档,我们了解到主要有一下几点:

  1. 没有存储:流本身不存储任何数据元素,通过数组,IO等获取到流对象,通过pipline的方式提供处理的计算处理。
  2. 纯功能性的:通过流对元素进行处理产生一个结果,但是并不对原始数据进行修改。
  3. 惰性寻找:许多流操作,如过滤、映射或重复删除,都可以惰性地实现,从而暴露了优化的机会,中间操作总是惰性的。
  4. 无限可能:集合的大小是有限的,而流则不需要,短路操作,如limit(n)或findFirst(),允许在有限的时间内完成对无限流的计算
  5. 消耗品:在流的生命周期中,流的元素只被访问一次。与迭代器一样,必须生成新的流来重新访问源的相同元素。

数据在流中被处理的操作叫做操作符,每一种操作符都是对数进行的一种操作,那么Stream中有哪些操作符呢?Stream的操作符大体上分为两种:中间操作符和终止操作符。

中间操作符:对于数据流来说,中间操作符在执行指定处理程序后,数据流依然可以传递给下一级的操作符,中间操作符包含8种(排除了parallel,sequential,这两个操作并不涉及到对数据流的加工操作):

  • map(mapToInt,mapToLong,mapToDouble) 转换操作符,把比如A->B,这里默认提供了转int,long,double的操作符。
  • flatmap(flatmapToInt,flatmapToLong,flatmapToDouble) 拍平操作比如把 int[]{2,3,4} 拍平 变成 2,3,4 也就是从原来的一个数据变成了3个数据,这里默认提供了拍平成int,long,double的操作符。
  • limit 限流操作,比如数据流中有10个 我只要出前3个就可以使用。
  • distint 去重操作,对重复元素去重,底层使用了equals方法。
  • filter 过滤操作,把不想要的数据过滤。
  • peek 挑出操作,如果想对数据进行某些操作,如:读取、编辑修改等。
  • skip 跳过操作,跳过某些元素。
  • sorted(unordered) 排序操作,对元素排序,前提是实现Comparable接口,当然也可以自定义比较器。

这也就是上面说的惰性寻找的中间操作。

终止操作符:数据经过中间加工操作,就轮到终止操作符上场了,终止操作符就是用来对数据进行收集或者消费的,数据到了终止操作这里就不会向下流动了,终止操作符只能使用一次。

  • collect 收集操作,将所有数据收集起来,这个操作非常重要,官方的提供的Collectors 提供了非常多收集器,可以说Stream 的核心在于Collectors。
  • count 统计操作,统计最终的数据个数。
  • findFirst、findAny 查找操作,查找第一个、查找任何一个 返回的类型为Optional。
  • noneMatch、allMatch、anyMatch 匹配操作,数据流中是否存在符合条件的元素 返回值为bool 值。
  • min、max 最值操作,需要自定义比较器,返回数据流中最大最小的值。
  • reduce 规约操作,将整个数据流的值规约为一个值,count、min、max底层就是使用reduce。
  • forEach、forEachOrdered 遍历操作,这里就是对最终的数据进行消费了。
  • toArray 数组操作,将数据流的元素转换成数组。

 Stream 的一系列操作必须要使用终止操作,否者整个数据流是不会流动起来的,即处理操作不会执行。

 

Talk is cheap , show me some code 

public static void main(String[] args) {
    Stream<String> stream = Stream.of("Str11111", "Str222", "Str3333333", "Str44", "Str5");
    stream.map(e->e.length()).forEach(e->System.out.println(e));
}

这里面我们通过Stream的静态方法of()构造了一个Stream对象,然后map就是中间操作符,是对数据进行处理的操作,这里是提取Stream中每个元素的字符长度,然后forEach是一个终止操作符,流中的数据处理到这里就终止了,这里通过输出流,将每个元素的长度打印出来。

这里是通过Stream的静态方法来获取一个Stream,其实在实际环境中我们还可以通过以下几种方式获得Stream:

  • 通过一个集合对象的stream() 和 parallelStream() 方法
  • 对于一个数组对象,可以通过 Arrays.stream()方法来获取
  • Stream的静态工厂方法,Stream.of等等
  • 可以通过文件的BufferReader.lines()方法获得
  • 通过Files类来获取,比如lines()方法
  • 想要获取一个随机的数字流,可以通过 Random.ints()
  • JDK中的许多其他流承载方法,包括 BitSet.stream(), Pattern.splitAsStream(java.lang.CharSequence) 和 JarFile.stream()

paralleStream()相比与stream()理论上是更快的并发执行,这个并发在底层使用的是ForkJoin。Java8为Forkjoin添加了一个通用的线程池,用于处理那些没有被显示提交到任何线程池中的任务。它是ForkjoinPool类型上的一个静态元素。它拥有的默认线程数量等于运行计算机上的处理器数量。



public class StreamTest { public static void main(String[] args) { List<String> list = new ArrayList<>(); for(int i=0;i<10;i++) { list.add(i + ""); } list.parallelStream().forEach(System.out::println); } }


比如这段代码,在forEach函数中会为每个元素的操作创建一个任务,添加到ForkjoinPool中执行。

其实在多线程的场景中我们更熟悉使用线程池,但是Forkjoin其实不仅仅是使用了多线层的思路,还有一个任务窃取的法宝,所以高并发的情况下其实会比单纯的多线程效果

更好,效率更高。

但是Forkjoin没有缺点吗?其实我们知道这里默认的ForkjoinPool里面的线程数是CPU和核心数,那么这里就要小心了,如果我们并发的任务是IO密集型的话,可能就会产生意外的效果了,如果任务的IO阻塞率比较高,那么这是所有的核心线程都会被这些任务所阻塞住,无法执行其他请求。所以我们在选择使用Forkjoin的时候需要考虑任务的类型,Forkjoin其实更加适合CPU密集型的独立任务,这样的效果才是最好的。不过如果在IO密集型的任务里面我们想要使用Forkjoin,那就设置一些核心线程数,不要把所有的核心任务队列都占满,间接影响到了其他任务的执行。


函数式接口是伴随着Stream的诞生而出现的,Java8中的Stream 作为函数式编程的一种具体实现,开发者无需关注怎么做,只需知道要做什么

各种操作符配合简洁明了的函数式接口给开发者带来了简单快速处理数据的体验,下面我们来介绍以下什么是函数式接口

什么是函数式接口?简单来说就是只有一个抽象函数的接口。为了使得函数式接口的定义更加规范,java8中提供了@FunctionalInterface 注解告诉编译器在编译器去检查函数式接口的合法性,以便在编译器在编译出错时给出提示。为了更加规范定义函数接口,给出如下函数式接口定义规则:

  • 有且仅有一个抽象函数
  • 必须要有@FunctionalInterface 注解
  • 可以有默认方法

还是看我们上面的例子,我们看以下map方法:

/**
     * Returns a stream consisting of the results of applying the given
     * function to the elements of this stream.
     *
     * <p>This is an <a href="package-summary.html#StreamOps">intermediate
     * operation</a>.
     *
     * @param <R> The element type of the new stream
     * @param mapper a <a href="package-summary.html#NonInterference">non-interfering</a>,
     *               <a href="package-summary.html#Statelessness">stateless</a>
     *               function to apply to each element
     * @return the new stream
     */
    <R> Stream<R> map(Function<? super T, ? extends R> mapper);

这里面的Function就是一个函数式接口

/**
 * Represents a function that accepts one argument and produces a result.
 *
 * <p>This is a <a href="package-summary.html">functional interface</a>
 * whose functional method is {@link #apply(Object)}.
 *
 * @param <T> the type of the input to the function
 * @param <R> the type of the result of the function
 *
 * @since 1.8
 */
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

函数式接口需要用 @FunctionalInterface 注解进行标注,这个注解的功能就是告诉其他人,这是一个函数式的接口,里面只能有一个抽象方法,为了防止其他人在开发过程中改变了函数的特性。

然后里面根据接口的功能定义相应的抽象方法可,这个apply 就是入参为T,然后返回值为R,这是一个很典型的使用场景。

在JDK中其实也为我们定义了很多函数式接口,我们在开发过程中都是可以直接使用的,下面列以下四大内置函数式接口:

Comsumer<T> : 消费型接口
@FunctionalInterface
public interface Consumer<T> {

    /**
     * Performs this operation on the given argument.
     *
     * @param t the input argument
     */
    void accept(T t);

可以看到抽象方法是void,就是没有任何返回值,只是在里面进行一些处理就好了,来看个例子:

@Test
    public void testConsumer() {
        syrup("syrup", (t) -> System.out.println("test-consumer-content:" + t));
    }

    public void syrup(String str, Consumer<String> consumer) {
        consumer.accept(str);
    }

这里定义syrup方法定义了一个consumer的函数式接口的参数,然后因为是消费型的接口,所以不需要任何的返回值,这里我们直接打印到控制台

Supplier<T> :供给型接口
@FunctionalInterface
public interface Supplier<T> {

    /**
     * Gets a result.
     *
     * @return a result
     */
    T get();
}

调用get()方法就会返回一个对应类型的对象,来看个例子:

@Test
    public void testSupplier() {
        List<Integer> syrupList = syrup(10, () -> new Random().nextInt(100));
    }

    public List<Integer> syrup(int genCount, Supplier<Integer> supplier) {
        List<Integer> reVal = new ArrayList<>();
        for(int i=0;i<genCount;i++) {
            reVal.add(supplier.get());
        }
        return reVal;
    }

这里还是syrup方法,第一个参数是我们要生成多少个整数,然后第二个参数是一个供给型的函数式接口,这里我们是想要返回一个Integer的List对象

因为Supplier的方法是不需要参数,然后会返回一个指定范型的对象,这里具体的返回值,由函数的实现来返回

然后我们在 testSupplier 方法中通过Random来返回一个100以内的随机数,最终返回一个Integer的List 对象。

Function<T, R> : 函数式接口
@FunctionalInterface
public interface Function<T, R> {

    /**
     * Applies this function to the given argument.
     *
     * @param t the function argument
     * @return the function result
     */
    R apply(T t);

入参是T,然后对t对象进行一些操作,然后给返回一个R,同样来看一个例子:

@Test
    public void testFunction() {
        Integer length = syrup("syurpStrContent", t -> t.length());
    }

    public Integer syrup(String str, Function<String,Integer> function) {
        return function.apply(str);
    }

这里我们定义了一个Function的入参,第一个参数是一个String ,第二个参数是返回值,这里我们是要返回字符串的长度

Predicate<T> : 断言型接口
@FunctionalInterface
public interface Predicate<T> {

    /**
     * Evaluates this predicate on the given argument.
     *
     * @param t the input argument
     * @return {@code true} if the input argument matches the predicate,
     * otherwise {@code false}
     */
    boolean test(T t);

入参为T,在函数中可以对t进行一些操作处理,然后会返回一个boolean类型的结果。

@Test
    public void testPredicate() {
        List<String> list = Arrays.asList("aaaaaa","dsdfdsfdsfds","dsfdsfdsf","sdfsddsf","2311231");
        List<String> syrup = syrup(list, t -> t.length() > 3);
    }

    public List<String> syrup(List<String> list, Predicate<String> predicate) {
        for(String str : list) {
            if(predicate.test(str)) {
                list.remove(str);
            }
        }
        return list;
    }

这里我们定义一个 Predicate 类型的入参,和一个String的List 对象,然后通过 Predicate 断言处理,过滤返回boolean的对象。

上面就是JDK提供的四种基本的函数式接口,同时每个函数式的接口都有一些相应的子接口。可以方便我们在开发中进行使用,如果这些函数都无法满足我们的需求,就可以自己定义了。

 下面看一下常用的操作

归约与收集

  归约:将流中的元素反复结合起来,得到一个新值。这个比较常见的场景就是我们想要统计一个集合中所有属性的总计值。

public class StreamTest {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        for(int i=0;i<10;i++) {
            list.add(i);
        }
        Integer reduce = list.stream().reduce(0, (x, y) -> x + y);
        System.out.println(reduce);
    }
}

这里的reduce方法的入参有两个,还有一个是只有第二个参数的:

T reduce(T identity, BinaryOperator<T> accumulator);

第一个叫做identity,这里我们可以理解为初始值,后面是一个BinaryOperator的函数式对象,我们看看这个类的来源是哪里

@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {

可以看到这个BinaryOperator是继承与Function,所以我们可以知道第一个和第二个T是入参的类型,第三个T是返回值的类型,这里我们传的都是Integer

所以上面的示例就是把0~9这10个数字相加,结果就是:45

  

  收集:将流转换成其他形式,参数是一个Collector接口的实现,用于给Stream中的元素做汇总的方法

public interface Collector<T, A, R> {}

这里可以看到Collector是一个接口,里面定义了一些抽象方法,不过这里不需要我们自己去实现Collector接口,JDK本身给我们提供了一个Collectors的工具类提供了很多静态方法。

这里我们可以先看一个简单的例子:

List<Integer> collect = list.stream().collect(Collectors.toList());

这里我们使用了Collectors.toList()方法,把stream处理之后的元素组装成一个集合。我们看一下toList()里面是怎么实现的:

public static <T>
    Collector<T, ?, List<T>> toList() {
        return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
                                   (left, right) -> { left.addAll(right); return left; },
                                   CH_ID);
    }

首先这个CollectorImpl是Collector的一个实现类:

static class CollectorImpl<T, A, R> implements Collector<T, A, R> {

然后它的构造方法是一个函数式的对象,首先是创建一个ArrayList对象,然后是调用其中的add方法,执行的逻辑是从流中依次读取原色,然后调用addAll方法,因为每个元素都是一个ArrayList嘛,遍历之后返回一个最终的List。