函数式编程,Stream流

函数式编程

函数式接口

概念:

​ 函数式接口在Java指的是:有且仅有一个抽象方法的接口就称为函数式接口。

​ 函数式接口,适用于函数式编程,在Java当中的函数式编程体现在Lambda表达式,所以函数式接口就是用来服务Lambda表达式,只有确保接口当中有且仅有一个抽象方法,Java中的Lambda才能顺利进行推导。

备注:“语法糖"是指使用更加便利方便,但是原理不变的代码语法。就比如遍历集合时使用for-each语法,其实底层使用的是迭代器,这便是"语法糖”。

格式:

只有确保接口当中有且仅有一个抽象方法即可:

修饰符 interface InterfaceName{
    //只能定义一个抽象方法
    public abstract 返回值类型 方法名称(参数列表);
    //可以定义其他的非抽象的方法
}

示例:

public interface FunctionInterfaceOne {
    public abstract void show01();
    public default void show02(){
        ....
    }
    //void show03(); 有且仅有一个抽象方法,才称为函数式接口
}
@FunctionalInterface注解

@Override注解作用类似,Java 8 中专门为函数式接口引入的一个新注解@FunctionalInterface,该注解主要定义在接口上,一旦使用该注解,编译器将会强制检查该接口是不是一个函数式接口,该接口中是不是有且仅有一个抽象方法,如果不是,编译报错。

@FunctionalInterface
public interface FunctionInterfaceOne {
    //定义一个抽象的方法
    void method();
    //void show();
    public default void show02(){}
}
自定义函数式接口的用途

​ 对于自定义的函数式接口,一般用于方法的参数和返回值上。

函数式编程

​ 能够兼顾Java的面向对象的特性基础上,通过Lambda表达式与方法引用,为开发者打开函数式编程的大门。

Lambda表达式的延迟加载

​ 有些场景的代码运行执行后,结果不一定会被使用到,从而造成性能的浪费。而Lambda表达式是延迟执行的,正好可以解决此问题,提升性能。

@FunctionalInterface
public interface BuildLogMessage {
    //定义有且只有一个抽象方法
    String buildMessage();
}
public class Demo01Logger {
    public static void showLog(int level,BuildLogMessage log){
        if (level <= 3){
            System.out.println(log.buildMessage());
        }
    }
    public static void main(String[] args) {
        //定义一些日志信息
        String massage1 = "执行Mysql操作";
        String massage2 = "执行Java.exe操作";
        String massage3 = "执行tomcat.exe操作";
        //调用showLog方法,参数是一个函数式接口,可以使用Lambda表达式
        showLog(2,()->{
            return massage1 + massage2 + massage3;
        });
    }
}

备注:实际上使用内部类也可以达到这样的效果,只是将代码操作延迟到另外一个对象当中通过调用方法来完成。后面的代码执行取决于前面的条件的判断结果

使用Lanbda作为方法的参数和返回值

在Java当中,Lambda表达式是作为匿名内部类的替代品,如果一个方法的参数是一个函数式接口类型,那么可以使用Lambda表达式进行代替。

java.lang.Runnable接口,就是一个函数式接口,

代码如下:

public class Demo01Lambda {
    //定义一个方法,开启线程的方法
    public static void staticThread(Runnable r) {
        new Thread(r).start();
    }
    public static void main(String[] args) {
        startThread(()->{
            System.out.println("开启一个新线程,线程任务被执行");
        });
        //优化Lambda
        startThread(()->System.out.println("开启一个新线程,线程任务被执行"));
    }
}

如果一个方法的返回值类型是一个函数式接口,那么我们可以直接使用一个Lambda表达式。

java.util.Comparator 接口是一个函数式接口

代码如下:

public class Demo2Lambda {
    //定义一个方法,方法的返回值是一个函数式接口类型Comparator
    public static Comparator<String> createComparator(){
        //返回值是一个函数式接口
        return new Comparator() {
            @Override
            public int compare(String o1,String o2) {
                //自定义比较的规则,升序/降序
                return o1.length() - o2.length();//升序
            }
        }
        //使用Lambda表达式 字符串长度进行升序
        return (o1,o2) -> o1.length() - o2.length();
    }
    public static void main(String[] args){
     	String[] strs = {"aaa","a","abcdefg","gggg"};   
        Arrays.sort(strs,createComparator());
        System.out.println(Arrars.toString(strs));//a,aaa,gggg,abcdefg
    }
}

常用的函数式接口

JDK提供了大量常用的函数式接口,丰富Lambda表达式的使用场景。它们主要在java.util.function包中被提供。

Supplier接口

java.util.function.Supplier<T>接口,该接口有且仅有一个无参的方法,

T get()

用来获取一个泛型参数指定类型的对象数据。由于该接口是一个函数式接口,所以我们可以使用Lambda表达式来操作它。

Supplier< T>接口被称之为生产型接口,指定接口的泛型是什么类型,那么接口中的get()方法就会生产什么类型的数据。

public class Demo01Supplier {
    //定义一个方法,方法的参数传递一个Supplier<T>接口,泛型指定为Integer,get方法会返回一个int
    public static int getNum(Supplier<Integer> sup){
        return sup.get();
    }
    public static void main(String[] args) {
        int[] arr = {10,20,30,40,50};
        int maxNum = getNum(() -> {
            //求出最大值
            int max = arr[0];
            for (int i : arr) {
                if (max <= i) {
                    max = i;
                }
            }
            return max;
        });
        System.out.println(maxNum);
    }
}
Consumer接口

java.util.function.Consumer< T>接口刚好和Supplier接口相反,它不是用来生产一个数据,而是消费一个数据。数据的类型有泛型来指定。

accept(T t)方法

意思就是消费一个指定泛型的数据

代码如下:

    //定义一个方法,方法的参数传递一个Consumer<T>接口类型,传递一个字符串变量
    public static void consumer(String str,Consumer<String> con) {
        //使用消费型接口对象,消费传递的字符串
        con.accept(str);
    }
    public static void main(String[] args){
        //来调用消费方法consumer
        consumer("abcdefg", name->{
            //把里面的abcdefg字符串改为大写
            String str = name.toUpperCase();
            String s = new StringBuffer(str).reverse().toString();
            System.out.println(s);
        });
    }
默认方法:andThen()

如果一个方法的参数和返回值都是Consumer类型,那么就可以实现这样的效果:消费数据的时候,首先做一个消费的操作,在做一个消费的操作,实现组合,可以通过Consumer接口当中的默认方法andThen来实现。

public class Demo03ConsumerAndThen {
    //定义一个方法,方法的参数传递一个字符串和两个Consumer接口,Consumer接口的泛型指定为字符串
    public static <con2> void consumers(String str, Consumer<String> con1,Consumer<String> con2){
      /* con1.accept(str);
       con2.accept(str); */
       con1.andThen(con2).accept(str);
       //规则 con1连接con2,先执行con1消费数据,在执行con2消费数据.
    }
    public static void main(String[] args){
        //由于consumers方法的参数Consumer接口是一个函数式接口,可以使用Lambda表达式
        consumers("Java31-中国最棒-123456",(name1)->{
            //消费规则
            //截取传入的字符串
            String sub = name1.substring(0, 6);
            System.out.println(sub);
        },(name2)->{
            String[] strs = name2.split("-");
            System.out.println(Arrays.toString(strs));
        });
    }
}

通过查看源码得知:andThen方法不允许传入一个null对象,否则会抛出空指针异常。

要想把两次消费动作连接起来,需要传入两个Consumer接口,通过andThen()方法实现一步一步的执行消费动作。

练习:

​ 定义一个字符串数组,存储每一个人的信息 如:“张三,20,郑州市”,存储5个人的信息,

​ 使用Consumer接口,按照指定的格式进行打印输出:姓名:张三;年龄:20,地址:郑州市,

​ 要求将打印姓名的动作作为第一个Consumer接口的规则

​ 将打印年龄的动作作为第二个接口的规则,打印地址的动作作为第三个接口的规则

public class People {
    public static void consumer(String[] arr, Consumer<String> name, Consumer<String> age, Consumer<String> address) {
        for (String str : arr) {
            name.andThen(age).andThen(address).accept(str);
        }
    }
    public static void main(String[] args) {
        String[] arr = {"张三,20,郑州市", "李四,22,南阳市", "小丽,18,开封市", "小王,21,郑州市", "王2五,25,郑州市"};
       /* consumer(arr, (name) -> {
            String[] split = name.split(",");
            System.out.print("姓名:" + split[0] + ";");
        }, (age) -> {
            String[] split = age.split(",");
            System.out.print("年龄:" + split[1] + ";");
        }, (address) -> {
            String[] split = address.split(",");
            System.out.println("地址:" + split[2]);
        });*/
        consumer(arr, name -> System.out.print("姓名:" + name.split(",")[0] + ";"),
                age -> System.out.print("年龄:" + age.split(",")[1] + ";"),
                address -> System.out.println("地址:" + address.split(",")[0] + ";"));
    }
}

Stream流

​ 在Java1.8中,由于Lambda表达式这种函数式编程,JDK引入了一个全新的概念:Stream流。用于解决已有集合类库的一些弊端的。

传统从集合中获取需要的元素
public class Demo01Stream {
    public static void main(String[] args){
        //构建一个集合
        List<String> list = new ArrayList<String>();
        list.add("abc123");
        list.add("aaa22");
        list.add("bcd125");
        list.add("abcd120");
        list.add("bbb230");
        //需要字符串中包含数字1的元素取出来
        List<String> list2 = new ArrayList<>();
        for(String str : list){
            if (str.contains("1")) {
                list2.add(str);
            }
        }
        //需要集合当中字符串长度不能超过6个的元素
        List<String> list3 = new ArrayList<>();
        for(String str : list2){
            if(str.length() <= 6){
                list3.add(str);
            }
        }
        //遍历查看最终想要的元素集合
        for(String str : list3){
            System.out.println(str);
        }
    }
}

​ 当我们需要对集合当中的元素进行操作的时候,总是需要对集合循环遍历,再次循环遍历…

​ 将集合A中根据条件拿到子集和B,再将子集和B根据条件二筛选子集和C

​ 它只是用来找到你需要元素的一种方式,并不是目的。目的想要取出想要的元素并且循环展示打印出来。以往的方式就是每次循环都需要从头开始遍历,下一次还要从头遍历

Java 1.8可以使用Lambda表达式的衍生物Stream流来优化你集合遍历的方式。

Stream流的更优写法
public class Demo01Stream {
    public static void main(String[] args){
        //构建一个集合
        List<String> list = new ArrayList<String>();
        list.add("abc123");
        list.add("aaa22");
        list.add("bcd125");
        list.add("abcd120");
        list.add("bbb230");
        //获取Stream流
        Stream<String> stream = list.stream();
        //需要字符串中包含数字1的元素取出来
        Stream stream02 = stream.filter(str -> str.contains("1"));
        //需要集合当中字符串长度不能超过6个的元素
        Stream stream03 = stream02.filter(str -> str.length() <= 6);
        //遍历查看最终想要的集合元素 forEach(Consumer<? super T> action) 
        //借助于Consumer中的accept(T t) 打印输出
        stream03.forEach(str -> System.out.println(str));
        //优化
        list.stream().filter(str -> str.contains("1"))
            .filter(str -> str.length() <=6)
            .forEach(str -> System.out.println(str));
    }
}
对此流的每个元素执行操作

一般我们把流式思想称之为"生产流水线"

流式思想概述:

​ 整体来看,流式思想类似于工厂中的"生产流水线"。

​ 当需要对多个元素进行操作的时候,尤其是多步操作,考虑到性能以及便利性。首先需要考虑一个"模型"步骤方案。然后按照设计的步骤方案去执行。

​ 比如你对某种类型的商品进行操作,你需要进行过滤,映射,跳过,计数等操作,这也是我们对集合中的元素操作的步骤,这一套步骤我们称之为一种处理方法,而方案就是一种"函数模型"。

​ 方案中操作的每一个步骤,我们称之为一个"流",调用指定的API方法,从一个流中转换为另一个流。

​ 都有对应的API方法,filter,map,skip,count都是对函数模型进行操作。

​ 当我们使用一个流的时候,通常需要包含三个基本步骤:

​ ①:获取一个数据源;

​ ②:数据转换;

​ ③:执行操作获取想要的操作结果。

​ 每次转换原有的Stream对象,返回一个新的Stream对象。这样我们可以像链条一样进行操作。

Stream流和以往的Collection集合有所不同。Stream操作有两个基础的特征:

  • 中间操作都会返回流对象本身,这样多个操作就可以串联成一个管道,如同流式风格,对中间的操作进行优化,比如可以进行延迟执行和短路。
  • 内部迭代:以前对集合遍历都是通过迭代器(iterator)或增强for循环,显示的在集合外部进行迭代,这叫外部迭代。Stream流提供了内部迭代的方法,这个流可以直接调用遍历的方法。
    • Stream流其实是一个集合元素的函数模型,它并不是集合,也不是数据结构。其本身并不存储任何元素(地址值)

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

  • 元素式特定类型的对象,形成一个队列。Java当中的Stream并不会存储元素,而是按需计算。
  • 数据源:流的来源,可以使集合,也可以是数组等容器。
获取流对象

java.util.stream.Stream< T >是JDK1.8引入的新特性,较为常用的接口(本身并不是函数式接口)

获取一个流对象,由以下常用方法

  • 所有的Collection集合都可以通过stream()默认方法来获取

  • Stream接口里面还有静态方法of()也可以获取stream流

方法
Stream< T >stream()
static Stream< T >of(T t)
of(T ...values)
根据Collection集合获取流对象

只要是Collection集合的实现类或者子类都可以使用stream()方法获取流对象

import java.util.*;
import java.util.stream.Stream;

public class Demo01GetStream {
    public static void main(String[] args) {
        //把集合转换为Stream流
        List<Object> list = new ArrayList<>();
        Stream<Object> stream1 = list.stream();
		//
        HashSet<Integer> set = new HashSet<>();
        Stream<Integer> stream2 = set.stream();
		//
        HashMap<String,String> map = new HashMap<>();
        Set<String> keySet = map.keySet();
        Stream<String> Stream3 = keySet.stream();
		//
        Collection<String> values = map.values();
        Stream<String> stream4 = values.stream();
        //entry(键与值的映射)
        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, 6);
        stream6.filter(num -> num>3).filter(num -> num % 2 == 0).forEach(num -> System.out.println(num));
        //可变参数是一个数组
        String[] arr = {"a","b","c","d"};
        Stream<String> stream7 = Stream.of(arr);

    }
}

​ Stream流属于管道流,每次只能被消费一次,第一个Stream流调用完毕后,数据就会被转移到下一个Stream上,而这时第一个Stream流已经使用完毕,就会关闭了,不能再次调用,如果强制调用,程序就会抛出:java.lang.IllegalStateException: stream has already been operated upon or closed

Stream流中的常用方法

流模型中的操作很多,大致上可以把其中的API方法分为两部分:

  • 延迟方法:返回值类型都是Stream流接口本身,因此可以支持链式操作
  • 终结方法:返回值就不是Stream流接口本身,因此不能再进行链式操作。比如:count()方法和forEach()方法
返回值方法
voidforEach(Consumer< T > consumer) :借助于该函数式接口中的方法accept()方法,Consumer< T >是一个消费性接口,用来消费一个指定泛型的数据(遍历流元素)
Stream< T >filter(Predicate<? super T> predicate) : 返回由此给定谓词匹配的此流的元素组成的流(返回包含指定元素的流)
< R > Stream< R >map(Function<? super T,? extends R> mapper) : 返回由给定函数应用于此流的元素的结果组成的流。(可以将当前流中的T数据转换成另外一种R类型的数据)
longcount() : 返回此流中的元素数
Stream< T >limit(long maxSize) 返回由此流的元素组成的流,截短长度不能超过maxSize(截取流,返回取得流中的前几个元素的流)
Stream< T >skip(long n) 在丢弃流的第n元素后,返回由该流的n元素组成的流。(跳过前几个元素,取用后几个元素)
Stream< T >concat(Stream<? extends T> a, Stream<? extends T> b) 创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。(把两个流合并成一个)
forEach()方法
 public class TestForEach {
    public static void main(String[] args) {
        //1.获取一个数据源
        Stream<String> stream = Stream.of("aa","abb","ccc","ddd");
        //2.转换数据
        //3.执行操作获取想要的结果
        stream.forEach(str -> {
            if (str.contains("a")){
                System.out.println(str);
            }
        });
    }
}
//展示结构 aa abb
过滤:filter()

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

Stream<T> filter(Predicate<? super T> predicate) 返回由此给定微词匹配的此流的元素组成的流
//借助于Predicate函数式接口当中的抽象方法test(T t) 对数据进行过滤

该方法接收一个函数式接口Predicate,可以使用Lambda表达式进行条件的筛选。

Predicate接口

java.util.stream.Predicate 函数式接口,其中唯一的抽象方法

boolean test(T t) 
该方法返回布尔类型值,代表指定的条件是否满足,如果条件满足返回true,那么Stream流的方法filter()将集合或数组其中的元素保留下来,如果不满足返回false,将元素过滤
public static void main(String[] args) {
    //1.准备一个数据源
    //获取该数据源
    String[] arr = {"小孙","小王","小赵","老王","涂少","老刘"};
    //2.数据转换
    //使用Stream流中的方法filter,对姓涂的过滤掉
    Stream<String> stream = Stream.of(arr);
    Stream<String> stream2 = stream.filter(name -> !name.contains("涂"));
    Stream<String> stream3 = stream2.filter(name -> name.startsWith("小"));
    stream3.forEach(System.out::println);
    /*stream.filter(name -> !name.contains("涂"))
            .filter(name -> name.startsWith("小"))
            .forEach(name-> System.out.println(name));*/
}
映射:map()

如果你需要将流中的数据映射到另一个流中,

< R > Stream< R >  map(Function<? super T,? extends  R> mapper)  :  返回由给定函数应用于此流的元素的结果组成的流。 

该方法接收一个函数式接口Function作为方法参数:可以将当前流中的T数据转换成另外一种R类型的数据。

Function接口

java.util.stream.Function 函数式接口。

R apply(T t) 
可以将一种T类型的数据转换成R类型的数据,那么这种转换的动作,我们称为映射
public static void main(String[] args) {
    //1.准备一个数据源
    //获取数据源
    //把String字符串的整数-->int类型的整数
    Stream<String> stream = Stream.of("123", "124", "125", "126");
    //2.数据转换 把字符串类型的数据转化成int类型的数据, 由于Function式一个函数式接口
    //apply(T t)
    //Integer.valueOf() 字符转换为Integer类型
    Stream<Integer> stream2 = stream.map(str -> Integer.valueOf(str));
    //遍历
    stream2.forEach(num -> System.out.println(num));
}
统计个数:count()

可以向Collection集合当中的size()一样,统计流中的元素个数,通过count()方法来实现

//返回此流中的元素数
long count();

该方法返回一个long类型的值代表流中的元素个数(区别于size()返回值int值)

public static void main(String[] args){
    Stream<Integer> stream = Stream.of(1,2,3,4,5,6);
    //统计个数
    long count = stream.count();
    System.out.println(count);//6
}
取用流中前几个:limit()

limit()方法可以对流中的数据进行限制—>截取操作,需要一个参数,设定取用流中前几个数

limit(long maxSize)  返回由此流的元素组成的流,截短长度不能超过maxSize

参数是一个long类型的,截取的长度不能超过流中最大元素个数:否则不进行操作。

public static void main(String[] args) {
    //准备一个数据源
    //获取数据源
    Stream<Integer> stream = Stream.of(12,13,14,15,16,20,30);
    //想要截取流中的前五个元素
    Stream<Integer> stream2 = stream.count(5);
    //查看流中的元素个数
    System.out.println(stream2.count());//5
}
跳过前几个:skip()

如果你希望跳过前几个元素,取用后几个元素,可以使用skip()方法

skip(long n)  在丢弃流的第一个n元素后,返回由该流的n元素组成的流。 

如果流中的个数小于n,将会得到一个长度为0的空流,反之流中的个数大于n则会跳过前n个元素

public static void main(String[] args) {
    String[] source = {"123","124","125","126","abc","abd","ase"};
    Stream<String> stream = Stream.of(source);
    //跳过前3个元素
    Stream<String> stream2 = stream.skip(4);
    /*long count = stream2.count();
    System.out.println(count);*/
    stream2.forEach(str -> System.out.println(str));
}
组合:concat()

如果由两个流,希望合并成一个流,那么可以使用concat()静态方法

concat(Stream<? extends T> a, Stream<? extends  T> b)  创建一个懒惰连接的流,其元素是第一个流的所有元素,后跟第二个流的所有元素。 
public static void main(String[] args){
    //准备连个数据源
    //获取两次数据源
    Stream<Integer> stream1 = Stream.of(12,13,14,15);
    Stream<Integer> stream2 = Stream.of(1,2,3,4,5,6);
    //把两个流合并成一个流
    Stream<Integer> stream3 = Stream.concat(stream1,stream2);
    //展示结构
    stream3.forEach(str -> System.out.println(str));
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值