精华内容
下载资源
问答
  • Java8函数接口

    千次阅读 2020-09-20 01:21:16
    Java函数接口 函数接口概述 什么是函数接口函数接口就是只定义一个抽象方法的接口,但同时可以拥有默认方法,为了简化函数方法的实现可以使用lambda表达式,其基本语法如下: (parameters) -> expression #...

    Java函数接口

    函数接口概述

    什么是函数接口:函数接口就是只定义一个抽象方法的接口,但同时可以拥有默认方法,为了简化函数方法的实现可以使用lambda表达式,其基本语法如下:

    (parameters) -> expression # 控制流语句需要使用{}

    Java函数编程:原始类型转换成对应的引用类型,Java中的泛型只能绑定引用类型,这由泛型内部的实现方式造成的,Java中包括一个将原始基本类型转换成引用类型的机制,成为装箱,反过程为拆箱操作。IntPredicate等可以避免自动装箱操作,对于专门的输入参数类型的函数式接口,如DoublePredicate,IntConsumer,Function输出参数的变种为ToIntFunction和IntToDoubleFunction

    如下图的常见函数接口的表格(多看看函数接口源码熟练使用即可):

    Function接口

    1、Function 接口的作用

         代表一个方法,可以接收一个参数,并产生一个结果,泛型参数T代表输入参数类型,R代表产生出参J数类型

    2、Function 接口的实现

       (因为函数编程参数即是动作,接下来介绍的时候对Function称之为操作)。函数接口由@FcuntionalInterface注解修饰,并且只能有一个接口参数,但可以同时包括多个default函数。如下Function接口的源码:

    @FunctionalInterfacepublic interface Function<T, R> {    //  该函数标识对参数T使用Function进行处理,产生R类型的结果    R apply(T t);      // 例如 A.compose(B).apply(v);先对v使用B操作,再进行A操作。A为调用者    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {        Objects.requireNonNull(before);        return (V v) -> apply(before.apply(v));    }    // 例如 A.andThen(B).apply(v);对v先进行A操作,再进行B操作    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {        Objects.requireNonNull(after);        return (T t) -> after.apply(apply(t));    }    // 返回一个总是返回它的输入参数的Function,此时结果和输入参数均为T    static <T> Function<T, T> identity() {        return t -> t;    }}

    3、应用

    1、Function使用案例(基于Java8的网络爬虫):

        当使用Java中国的OkHttp网络框架和Jsoup框架处理网络爬虫的时候,常常会根据不同的平台对不同页面进行处理,所以为了规范化以及行为参数化处理,我这里以此为背景,写了一个Function类型的HTTP超文本标签id为某某某的属性解析器。

      // Function函数结构规范  private static String parserHtmlBy(Document document, Function<Document, String> idFunction) {      if (Objects.isNull(idFunction) || Objects.isNull(document)) {          return "";      }      // 此处使用了apply函数      return idFunction.apply(document);    }  public static void main(String[] args) throws IOException {      OkHttpClient client = new OkHttpClient();      String uri = "https://www.bilibili.com/";      Response httpBody = client.newCall(new Request.Builder().url(uri)              .addHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36")              .build()).execute();      if (!httpBody.isSuccessful()) {          throw new HttpRetryException("Http Request Exception", httpBody.code());      }      Document html = Jsoup.parse(httpBody.body().string());      // 不同的页面可能使用Function不一致,所以这里使用Function对函数进行封装      // 行为参数化,行为idFunction对html参数进行处理      String headWrapper = parserHtmlBy(html, document -> {          Element idToValue = html.getElementById("reportFirst1");          Map<String, String> attrs = Maps.newHashMap();          Optional.ofNullable(idToValue).ifPresent(t -> {              idToValue.attributes().forEach(attribute -> {                  attrs.put(attribute.getKey(), attribute.getValue());              });          });          return new GsonBuilder().create().toJson(attrs);      });      System.out.println("哔哩哔哩 " + headWrapper);  }

    2、关于compose,andThen,identity函数的研究(请看一下源码中的Doc分析)

    Function<Integer,Integer> multi = i->i*i;Function<Integer,Integer> resize = i->i*2;// 执行顺序,resize,multi。结果为16System.out.println(multi.compose(resize).apply(2));// 执行顺序,multi,resize。结果为8System.out.println(multi.andThen(resize).apply(2));// 直接输出原值。结果为2System.out.println(Function.identity().apply(2));

    Predicate接口

    1、predicate函数结构作用

          代表一个方法,可以接收一个参数,并根据某一规则判断,该参数是否符合该规则,根据规则过滤数据信息。

    2、predicate函数接口实现

      (因为函数编程参数即是动作,接下来介绍的时候对Predicate称之为操作)。函数接口由@FunctionalInterface注解修饰,并且只能有一个接口参数,但可以同时包括多个default函数。如下Predicate接口的源码:

    @FunctionalInterfacepublicinterface Predicate<T> {    // 判断传入的参数是否符合某种规则    boolean test(T t);    // 判断传入的参数是否符合某些规则    default Predicate<T> and(Predicate<? super T> other) {        Objects.requireNonNull(other);        return (t) -> test(t) && other.test(t);    }    // 判断传入的参数是否不符合某种规则    default Predicate<T> negate() {        return (t) -> !test(t);    }    // 判断传入的参数是否符合多种规则中的某一种    default Predicate<T> or(Predicate<? super T> other) {        Objects.requireNonNull(other);        return (t) -> test(t) || other.test(t);    }    // 判断两个对象是否相等,Predicate接口的静态方法    static <T> Predicate<T> isEqual(Object targetRef) {        return (null == targetRef)? Objects::isNull : object -> targetRef.equals(object);    }}

    3、predicate函数接口使用(选茶叶)

    public class TeaFilter {    @Data    @ToString    @AllArgsConstructor    static class Tea {        String type;        String price;    }    private static List<Tea> filter(List<Tea> teas, Predicate<Tea> t) {        List<Tea> suit = Lists.newLinkedList();        for (Tea tea : teas) {            // negate            if (t.test(tea)) {                suit.add(tea);            }        }        return suit;    }    public static void main(String[] args) {        List<Tea> teas = Arrays.asList(new Tea("龙绿茶", "100"),                new Tea("龙井", "1000"),                new Tea("信阳毛尖", "10000")        );        // test操作        List<Tea> tea = filter(teas, x -> x.getType().endsWith("毛尖"));        // or 操作        Predicate<Tea> teaOrPredicate = x -> x.getType().startsWith("龙");        Predicate<Tea> teaOrPredicateOther = x -> x.getPrice().compareTo("10") >= 0;        filter(teas, teaOrPredicate.or(teaOrPredicateOther)).forEach(x -> System.out.println(x.toString()));        System.out.println("==========");        // and 操作        Predicate<Tea> teaAndPredicate = x -> x.getType().startsWith("龙");        Predicate<Tea> teaAndPredicateOther = x -> x.getPrice().compareTo("1000") >= 0;        filter(teas, teaAndPredicate.and(teaAndPredicateOther)).forEach(x -> System.out.println(x.toString()));        // 比较对象isEqual,可用于对象比较,但是不建议        Predicate<Tea> isEqual = Predicate.isEqual(new Tea("普洱", "100"));        System.out.println(isEqual.test(new Tea("普洱", "1000")));    }}

    Suppiler接口

    1、Suppiler函数接口作用 

        Suppiler函数接口的作用,根据提供者返回结果,函数签名为 ()->T

    2、Suppiler函数接口源码

    // 返回一个提供者的结果@FunctionalInterfacepublic interface Supplier<T> {    // 返回一个结果    T get();}

    3、举例子(一个对象的某个字段的值的获取和默认值):

    publicstatic String suppiler(Supplier<String> suppiler, String defaultValue) {    String val = suppiler.get();    if (val == null || "".equals(val)) {        return defaultValue;    }    return val;}private static String sval(Supplier<String> suppiler) {    return suppiler(suppiler, "NA");}// 可以是任意一个引用类型,此时返回一个值public static void main(String[] args) {    System.out.println(sval(new Node("")::getName));}

    ​​​​​​​Consumer接口

    1、Consumer接口作用

       java.util.function.Consumer<T>定义了一个名叫accept的抽象方法,它接受泛型T的对象,没有返回(void),可以简单的理解为消费者。

    2、Consumer函数接口实现

    // 与其他函数接口不同的是,Consumer接口操作产生副作用// T 代表输入参数类型@FunctionalInterfacepublic interface Consumer<T> {    //  对给定参数执行操作,没有返回值    void accept(T t);    // 返回组成的{@code Consumer},依次执行此操作    // 操作,然后进行{@code after}操作。如果执行任何一个操作会引发异常,它将被中继给    // 组合操作。如果执行此操作会引发异常,{@code after}操作将不会执行。    default Consumer<T> andThen(Consumer<? super T> after) {        Objects.requireNonNull(after);        return (T t) -> {            accept(t);            after.accept(t);        };    }}

    3、Consumer函数接口使用

    public static void consumer(List<String> list, Consumer<String> consumer){    for(String s : list){        consumer.andThen(consumer).accept(s);    }}public static void main(String[] args) {    List<String> costMoney = new ArrayList<>();    costMoney.add("dollars"); // 美元    costMoney.add("yen");     // 日元    costMoney.add("RMB");     // 人民币    consumer(costMoney, System.out::println);}

     

    展开全文
  • 常用函数接口

    千次阅读 2018-05-13 22:27:34
    常用函数式接口JDK 8 中重要的函数接口接口 参数 返回 中文 示例 Supplier None T 提供者 工厂方法创建对象 ​ Consumer T void 消费者 输出一个值 ​ Predicate T boolean 谓语,顾名思义,中文中的‘是’与‘不是...

    常用函数式接口

    JDK 8 中重要的函数接口

    接口      参数          返回          中文          示例
    Supplier None T 提供者 工厂方法创建对象
    
    Consumer T void 消费者 输出一个值
    
    Predicate T boolean 谓语,顾名思义,中文中的‘是’与‘不是’是中文语法的谓语
    
    Function T R 函数 获得某个对象的名字
    
    BinaryOperator (T, T) T 二元操作符,二元(就是数学里二元一次方程那个二元,代表 2 个的意思),双重的。
                    即有两个操作数 例如求两个数的乘积(*)
        
    UnaryOperator T T 一元操作符,只有一个操作数 逻辑非(!)

    Supplier 接口

    Supplier 接口代表一个结果的提供者。
    
    Supplier 接口是用来生成数据的,数据的类型通过泛型参数给定。
    
    使用 get()方法获得返回值,不要求每次调用时都返回新的或不同的结果。

    接口的源码如下:

    @FunctionalInterface
    public interface Supplier {
     /**
    得到一个结果,返回 T 对象
     */
       T get();
      }

    Supplier 接口中的方法

    T get() 获得指定类型的数据

    将 Supplier 直接使用

     案例需求get()方法的基本使用,在 Supplier 接口中 get 方法返回一个字符串。 案例步骤:1) 使用 Lambda 创建 Supplier 对象,泛型类型为 String,方法体返回一个字符串,return 可以省略。2) 调用 Supplier 的 get()方法得到字符串,并打印输出

    public class DemoSupplier {
    
     public static void main(String[] args) {
    
     //使用 Lambda 创建 Supplier 对象
    
     Supplier supplier = () -> "Hello Java!";
    
     //输出它的值
    
     System.out.println(supplier.get());
    
     }
    }

    将 Supplier 使用生成对象

     案例需求:

    下面的例子演示了如何通过调用一个静态方法,生成一个员工对象返回。

    使用构造方法做为 Supplier 参数的引用。

     案例步骤:1) 在主类内部创建一个私有的静态 Employee 对象,重写 toString()方法,返回一个字符串:"我是员工"。

    2) 在 main 函数中创建一个 Supplier 对象,泛型类型是 Employee。使用 Lambda 传入 Supplier 对象,方法体实例化员工对象,省略 return 方法。

    3) 使用 supplier 对象的 get()方法得到员工对象

    4) 打印输出员工对象

    因为 Employee 对象是私有的,外部类无法直接实例化员工对象。

    调用 Supplier 的 get()方法来生成员工对象,这样做的目的是可以控制员工对象的生成方式,类似于工厂模式。

    //主类
    public class DemoSupplier {
    
     //员工类
     private static class Employee {//注意static
    
     @Override
    
     public String toString() {
    
     return "我是员工";
    
     }
    
     }
    
     public static void main(String[] args) {
    
     //使用 Lambda 传入 Supplier 对象,将生成一个员工对象//此时仅仅是实例化了接口并未执行里面代码
    
     Supplier supplier = ()->new Employee();
    
     //输出员工对象
    
     System.out.println(supplier.get());
    
     }
    }

    将 Supplier 做为方法的参数

     需求说明求数组中的最大值,使用 Supplier 接口作为方法参数类型,通过 Lambda 表达式求出 int 数组中的最大值。 需求分析1) 定义整型数组 int[] arr = {12,68,10,2,99,313,46};2) 创建静态方法 getMax():返回 int 类型,将 Supplier 做为参数,泛型类型为 Integer,方法体调用 get()方法返回值。3) 在 main 函数中调用 getMax()方法,使用 Lambda 传入 Supplier 对象,并且实现查找最大值的功能。4) 在 Lambda 表达式相当于方法体:遍历每个元素,比较大小,找出最大值。

    public class DemoSupplier {
     public static void main(String[] args) {
     int[] arr = {12, 68, 10, 2, 99, 313, 46};
     // 调用 getMax 方法获得最大值,Lambda 相当于方法体
     int num = getMax(() -> {
     int max = arr[0];
     for (int i = 1; i < arr.length; i++) {
     if (max < arr[i]) {
     max = arr[i];
     }
     }
     return max;
     });
     //输出最大值
     System.out.println("最大值是:" + num);
     }
     //使用 Supplier 做为参数
     public static int getMax(Supplier<Integer> supplier) {
     return supplier.get();
     }
    }

    Consumer 接口

    Consumer 接口代表接受单一的输入变量而且没有返回值的一类操作。
    
    它的作用和 Supplier 相反,是消费一个数据的,消费的数据类型需要通过泛型指定。

    它的源代码如下:

    
    @FunctionalInterface
    public interface Consumer<T>{
     //接受 t 对象,无返回值
     void accept(T t);
        
    //默认的组合方法,参数和返回值都是 Consumer 类型,先调用自己的 accept()方法,再调用参数的 accept()方法
     default Consumer<T>  andThen(Consumer<T>  after) {
     Objects.requireNonNull(after);
     return (T t) -> { accept(t); after.accept(t); };
     }
    }
    

    Consumer 接口中的方法

    void accept(T t) 接受对给定的参数进行操作。

    Consumer 接口中的默认方法:

    default Consumer<T>  andThen(Consumer<T>  after)然后
    
    如果一个方法的参数和返回值全都是 Consumer<T>  类型,那么就可以实现效果:
    
    消费一个数据的时候,首先做一个操作,然后再做另一个操作,两个操作依次执行,实现一种组合操作。
    
    而这个方法就是 Consumer 接口中的默认方法 andThen。

    直接使用 Consumer 对象

     实现步骤:1) 使用 Lambda 创建 Consumer 对象,直接打印传入的字符串数据。2) 调用 Consumer 的 accept()方法,在 accept()方法中传入一个字符串数据。

    public class DemoConsumer {
     public static void main(String[] args) {
     //创建 Consumer 对象,打印传入的变量 t
     Consumer consumer = t -> System.out.println(t);
     //调用 Consumer 中的方法
     consumer.accept("Hello Lambda");
     }
    }

    Consumer 做为参数

    List  Set 集合中遍历的 forEach 方法它的参数就是 Consumer,

    请看下面的代码: 案例需求:1) 创建一个数组,使用 Arrays.asList("孙悟空", "猪八戒", "白骨精", "嫦娥") 转成 List 对象。2) 使用 forEach 方法打印每一个元素,forEach 中使用 Lamba 表达式输出传入的字符串

    public class DemoForEach {
     public static void main(String[] args) {
     //将数组转成 List 对象
     List names = Arrays.asList("孙悟空", "猪八戒", "白骨精", "嫦娥");
     //打印每一个字符串,forEach 的参数就是 Consumer
     names.forEach(t -> System.out.println(t));
     }
    }

    分析 forEach()方法的源代码

    default void forEach(Consumer<T> action) {
     Objects.requireNonNull(action);//判断某一对象是否不为null
     for (T t : this) {
     action.accept(t);
     }
    }
    
    这是定义在 java.lang.Iterable 接口中的默认方法,
    
    参数就是 Consumer 对象,方法体内对当前集合使用 for遍历,this 就是集合对象。
    
    每次对一个元素调用 accept()方法。
    
    而我们外部调用的代码中对 accept()方法进行了实现,输出了每个元素。
    
    
    public static  T requireNonNull(T obj) 静态方法,JDK7 中新增的方法,判断传入的对象是否为 NULL,
    
    如果是 NULL 则抛出异常,不为 NULL 则返回对象本身。常用于方法或构造方法中传入对象参数的校验。

    default Consumer<T>  andThen(Consumer<? super T>  after) {
     Objects.requireNonNull(after); //判断 after 是否为 null
        
    //先调用自己的 accept()方法,再调用参数的 accept()方法
     return (T t) -> { accept(t); after.accept(t); };
    }
    
    要想实现组合,需要两个或多个 Lambda 表达式,而 andThen 的语义正是执行“一步接一步”操作。

    andThen 方法演示示例 案例需求:将字符串 Hello 首先打印大写的 HELLO,然后打印小写的 hello 实现步骤:1) 创建 Consumer 对象 c1,使用 Lambda 打印 s 对象的大写2) 创建 Consumer 对象 c2,使用 Lambda 打印 s 对象的小写3) c1 调用 andThen(c2)方法,再调用 accept("字符串"),完成依次的操作。

    public class DemoConsumer {
        public static void main(String[] args) {
            //打印大写
            Consumer<String> c1 = s -> System.out.println(s.toUpperCase());
            //打印小写
            Consumer<String> c2 = s-> System.out.println(s.toLowerCase());
            //调用方法
            c1.andThen(c2).accept("Hello Consumer");
        }
    }

    使用 Consumer 做为参数

     需求说明格式化打印信息,下面的字符串数组当中存有多条信息,

    请按照格式“姓名:XX。性别:XX。”的格式将信息打印出来。

    要求将打印姓名的动作作为第一个 Consumer 接口的 Lambda 实例,

    将打印性别的动作作为第二个Consumer 接口的 Lambda 实例,将两个 Consumer 接口按照顺序“拼接”到一起。以下数组共 5 个元素,每个元素包含 2 项信息用逗号分隔。String[] arr = { "张飞,男", "貂蝉,女", "曹操,男","孙尚香,女","小乔,女" };

     实现步骤1) 创建静态方法 printInfo(),有 3 个参数,

    第 1 个是需要打印的字符串数组,

    第 2 个是 Consumer用于打印姓名 name,

    第 3 个是 Consumer用于打印性别 gender。

    2) 在 printInfo 方法中遍历数组中每个元素,再调用 name.andThen(gender).accept(单个元素)

    3) 每调用一次 andThen()方法,在下面输出一行横线

    4) 在 main 函数中创建上面要遍历的数组

    5) 调用 printInfo 方法,传入 3 个参数,

    第 1 个参数是数组,

    第 2 个参数使用 Lambda 打印姓名,参数 s 表示数组中的每个元素。

    第 3 个参数使用 Lambda 打印性别。

    public class DemoConsumerPrint {
        public static void main(String[] args) {
            String[] arr = { "张飞,男", "貂蝉,女", "曹操,男","孙尚香,女"};
    
            //这里的 s 表示数组中的每个元素
            printInfo(arr, s ->{
                System.out.println("姓名:" + s.split(",")[0]);
            },s ->{
                System.out.println("性别:" + s.split(",")[1]);
            });
        }
    
        public static void printInfo(String[] arr, Consumer<String> name, Consumer<String> gender) {
            for (String s : arr) {
                name.andThen(gender).accept(s);
                System.out.println("------------------");
            }
        }
    }

    Predicate 接口

    Predicate 中文意思为谓语,"我是一个程序员""是""不是"就是谓语。
    它代表只有一个变量的函数,返回 boolean 类型。
    
    有时候我们需要进行某种判断,从而得到一个 boolean 值的结果。
    
    可以使用 java.util.function.Predicate接口。

    Predicate 的源码:

    @FunctionalInterface
    public interface Predicate<T> {
     boolean test(T t); //抽象方法,对 t 进行测试,返回 boolean 类型
    
    /**
    - 组合方法,将当前的谓语与另一个谓语进行短路的与操作,返回一个谓语对象
      */
       default Predicate<T> and(Predicate<? super T>  other) {
       Objects.requireNonNull(other); //判断 other 是否为空
       return (t) -> test(t) && other.test(t);
       }
        
       /**
    - 对当前的谓语进行逻辑非操作,返回一个谓语对象
      */
       default Predicate<T> negate() {
       return (t) -> !test(t);
       }
        
       /**
    - 组合方法,将当前的谓语与另一个谓语进行短路的或操作,返回一个谓语对象
      */
       default Predicate<T> or(Predicate<? super T> other) {
       Objects.requireNonNull(other);
       return (t) -> test(t) || other.test(t);
       }
        
       /**
    - 静态方法,判断 test(object)方法传入的对象是否与参数 targetRef 对象相等
      */
       static  Predicate<T> isEqual(Object targetRef) {
       return (null == targetRef)
       ? Objects::isNull
       : object -> targetRef.equals(object); /
       }
      }
    

    Predicate 接口中的方法

    boolean test(T t)  t 进行指定条件的测试,返回 boolean 类型

    test()方法演示

     案例需求:判断 test("字符串")方法给定的参数长度是否大于 5 案例步骤:1) 创建一个 Predicate 谓语对象,使用 Lambda 实现 boolean test(T t)方法2) 方法体的参数是 s,返回字符串的长度大于 5,省略 return 关键字。3) 两次调用 test()方法看运行结果,第 1 次使用字符串 Hello,第 2 次使用字符串 Predicate

    public class DemoPredicateTest {
    
     public static void main(String[] args) {
    
     //创建一个 Predicate 谓语对象,boolean test(T t)方法接收字符串类型,返回 boolean 类型
    
     Predicate<String> predicate = s -> s.length() > 5;
    
     //两次调用 test 方法看运行结果
    
     System.out.println("Hello 的长度是否大于 5:" + predicate.test("Hello"));
    
     System.out.println("Predicate 的长度是否大于 5:" + predicate.test("Predicate"));
    
     }
    }

    默认方法 and()

    既然是条件判断,就会存在与、或、非三种常见的逻辑关系。
    
    其中将两个 Predicate 条件使用“与”逻辑连接起来实现“并且”的效果时,可以使用 default 方法 and。
    
    这个默认方法接收一个 Predicate 参数,返回一个 Predicate参数。

    其 JDK 源码为:

    /**
    组合方法,将当前的谓语与另一个谓语进行短路的与操作,返回一个谓语对象
    */
    
    default Predicate<T> and(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) && other.test(t);
    }

    and 方法演示示例:

     案例需求:判断一个字符串是否包含指定的字符串:既包含大写“H”,又要包含大写“W” 案例步骤:1) 创建 2 个需要判断的字符串:s1="Hello world"和 s2="Hello World"2) 使用 Lambda 表达式,创建两个 Predicate 对象3) 判断字符串 s 是否包含 H4) 判断字符串 s 是否包含 W5) 调用 and 方法和 test 方法,分别输出 s1 和 s2 的结果

    public class DemoPredicateAnd {
    
     public static void main(String[] args){
    
     //创建 2 个需要判断的字符串
    
     String s1 = "Hello world";
    
     String s2 = "Hello World";
    
     // 使用 Lambda 表达式,创建两个 Predicate 对象
    
     //判断 s 是否包含 H
    
     Predicate<String> p1 = s -> s.contains("H");
    
     //判断 s 是否包含 W
    
     Predicate<String> p2 = s -> s.contains("W");
    
     //调用 and 方法
    
     System.out.println(s1 + "是否包含 H 和 W:" + p1.and(p2).test(s1));
    
     System.out.println(s2 + "是否包含 H 和 W:" + p1.and(p2).test(s2));
    
     }
    }

    默认方法 or()

     and 的“与”类似,默认方法 or 实现逻辑关系中的“或”操作。
    
    这个默认方法接收一个 Predicate 参数,返回一个 Predicate 参数。

    JDK 源码为:

    /**
    *组合方法,将当前的谓语与另一个谓语进行短路的或操作,返回一个谓语对象
    */
    default Predicate<T> or(Predicate<? super T> other) {
        Objects.requireNonNull(other);
        return (t) -> test(t) || other.test(t);
    }
    

    or 方法演示示例:

     案例需求:判断一个字符串的长度大于 10 或者小于 5 案例步骤:1) 创建三个字符串 s1,s2,s3 内容如下图2) 使用 Lambda 创建 2 个 Predicate 接口对象,第 1 个判断长度是否大于 10,每 2 个判断长度是否小于 53) 调用 or 和 test 方法输出每个字符串的测试结果

    public class DemoPredicateOr {
    
     public static void main(String[] args) {
    
     //创建三个字符串
    
     String s1 = "Hello World"; //大于 10
    
     String s2 = "Java"; //小于 5
    
     String s3 = "I am boy"; //既不大于 10,又不小于 5
    
     //使用 Lambda 创建 2 个 Predicate 接口对象
    
     Predicate<String> p1 = s -> s.length() > 10;
    
     Predicate<String> p2 = s -> s.length() < 5;
    
     //输出每个字符串的测试结果
    
     System.out.println(s1 + "=" + p1.or(p2).test(s1));
    
     System.out.println(s2 + "=" + p1.or(p2).test(s2));
    
     System.out.println(s3 + "=" + p1.or(p2).test(s3));
    
     }
    }

    默认方法 negate()

    “与”、“或”已经了解了,剩下的“非”(取反)也会简单。方法没有参数,返回值为 Predicate。

    默认方法 negate的 JDK 源代码为:

    /**
    *对当前的谓语进行逻辑非操作,返回一个谓语对象
    */
    
    default Predicate<T> negate() {
    
     return (t) -> !test(t);
    }
    
    从实现中很容易看出,它是执行了 test 方法之后,对结果 boolean 值进行“!”取反而已。
    
    要在 test 方法调用之前调用 negate 方法,正如 and  or 方法一样。

     案例需求:判断年龄是否小于 18 岁,将判断的结果取反。 案例步骤1) 创建 2 个整数类型的年龄,一个 25,一个 15 岁。2) 使用 Lambda 创建 1 个 Predicate,判断年龄小于 18 岁。3) 使用 nagate()取反以后再调用 test()方法,输出两个年龄的结果

    public class DemoPredicateNegate {
    
     public static void main(String[] args) {
    
     int age1 = 25; //25 岁
    
     int age2 = 15; //15 岁
    
     Predicate<Integer> predicate = (a) -> a < 18; //判断是否小于 18 岁
    
     System.out.println(age1 + "小于 18 岁,取反:" + predicate.negate().test(age1));
    
     System.out.println(age2 + "小于 18 岁,取反:" + predicate.negate().test(age2));
    
     }
    }

    静态方法 isEqual ()

    Predicate 中唯一的静态方法,方法的参数是两个 Object 类型,返回一个 Predicate 类型。
    
    作用:根据 Objects.equals(Object, Object)方法比较两个参数是否相等,
    一个对象通过 isEqual()传入,另一个对象通过 test()传入。
    
    
    java.util.Objects 类中的方法 说明
    public static boolean equals(Object a,Object b)
    
    作用:用于比较两个对象是否相等
    
    参数:a  b 是要比较的两个对象
    
    返回:如果两个对象相等,则返回 true,否则返回 false

    JDK 源代码为:

    /**
    *静态方法,判断 test(object)方法传入的对象是否与参数 targetRef 对象相等
    */
    static <T> Predicate<T> isEqual(Object targetRef) {
        return (null == targetRef)
                ? Objects::isNull
                : object -> targetRef.equals(object);
    }

     案例需求:比较两个字符串是否相等 案例步骤:1) 通过静态方法 isEqual("newboy"),直接返回 Predicate 对象2) 调用 Predicate 中的 test()方法传入另两个字符串分别比较

    public class DemoPredicateIsEqual {
    
     public static void main(String[] args) {
    
     //通过静态方法直接返回 Predicate 对象
    
     Predicate predicate = Predicate.isEqual("newboy");
    
     //调用 test()方法传入另两个字符串分别比较
    
     System.out.println("两个字符串是否相等:" + predicate.test("newboy"));
    
     System.out.println("两个字符串是否相等:" + predicate.test("NewBoy"));
    
     }
    
    }

    Predicate 的应用示例

     需求说明集合当中有多条“姓名+性别”的信息如下:"张飞,男", "貂蝉,女", "曹操,男","孙尚香,女","小乔,女"

    请通过 Predicate 接口的 and 组合方法,将符合要求的字符串筛选到集合 ArrayList 中,

    需要同时满足两个条件:1) 必须为女生 2) 姓名为两个字

     开发步骤:1) 创建第 1 个 Predicate 判断条件:使用逗号分隔的第 0 个元素姓名长度是 2

    2) 创建第 2 个 Predicate 判断条件:使用逗号分隔的第 1 个元素性别等于女

    3) 创建一个新的 List 集合,用于存储过滤以后符合条件的字符串

    4) 使用 List 中的 forEach(Lambda)遍历上面的原始 List 集合,

    使用 Predicate 中的 and 和 test 方法判断每个元素

    5) 两个条件都为真才添加到新的 List 集合中

    6) 创建第 1 个 Consumer 接口,输出使用逗号分隔的第 0 个元素姓名

    7) 创建第 2 个 Consumer 接口,输出使用逗号分隔的第 1 个元素性别

    8) 使用 List 中的 forEach(Lambda)遍历,输出过滤后的新的集合

    9) 使用 Consumer 接口中的 andThen 和 accept 方法,输出每一个元素

    public static void main(String[] args) {
    
        //从数组中创建一个 List 集合
    
        List<String> list = Arrays.asList("张飞,男", "貂蝉,女", "曹操,男","孙尚香,女","小乔,女");
    
        //创建第 1 个 Predicate 判断条件:使用逗号分隔的第 0 个元素姓名长度是 2
    
        Predicate<String> pname = s -> s.split(",")[0].length() ==2;
    
        //创建第 2 个 Predicate 判断条件:使用逗号分隔的第 1 个元素性别等于女
    
        Predicate<String> pgender = s-> s.split(",")[1].equals("女");
    
        //创建一个新的 List 集合
    
        List<String> infos = new ArrayList<>();
    
        //使用 Lamba 中的 forEach()遍历上面的 List 集合,使用 Predicate 中的 and 和 test 方法判断每个元素
    
        list.forEach(s -> {
    
            //两个都为真才添加到集合中
    
            if (pname.and(pgender).test(s)) {
    
                infos.add(s);
    
            }
    
        });
    
        //创建第 1 个 Consumer 接口,输出使用逗号分隔的第 0 个元素姓名
    
        Consumer<String> cname = s -> System.out.println("姓名:" + s.split(",")[0]);
    
        //创建第 2 个 Consumer 接口,输出使用逗号分隔的第 1 个元素性别
    
        Consumer<String> cgender = s -> System.out.println("性别:" + s.split(",")[1]);
    
        //使用 Lamba 中的 forEach()遍历,输出过滤后的集合
    
        infos.forEach(s -> {
            //使用 Consumer 接口中的 andThen 和 accept 方法,每输出一个元素隔一条线
            cname.andThen(cgender).accept(s);
            System.out.println("---------------");
    
        });
    }

    Function 接口

    Function接口:
            根据一个参数得到另一个参数值,前面称为计算的参数,后面称为计算的结果。
            有进有出,所以称为“函数 Function”。
            
    类似于数学中的函数,通过一个变量求出另一个变量的值。如:f(x) = 2x+3

    以下是它的 Java 源代码

    import java.util.Objects;
    
    /**
    代表通过一个变量求出另一个变量的结果的函数
    
    @param  输入给函数的变量
    
    @param  函数输出的结果
    */
    @FunctionalInterface
    public interface Function<T, R> {
    
     /**
    对给定的变量 t 进行计算,得到返回的结果 R
    */
     R apply(T t);
      
        
     /**
    默认组合方法,先计算当前函数,再计算传入的函数
    */
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }
        
     /**
    默认组合方法,先计算传入的函数,再计算当前函数
    */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
     /**
    静态方法:总是返回它的输入变量
    */
    static <T> Function<T, T> identity() {
        return t -> t;
    }
    }

    抽象方法:apply()

    是java.util.function.Function 接口中的方法 
    
    R apply(T t); 对给定的变量 t 进行计算,得到返回的结果 R

    apply 方法演示示例: 案例需求将 Integer 类型转换为 String 类型,并且输出转换以后字符串的长度。1) 创建一个 Function 对象,输入类型是整数,输出类型是字符串2) Lambda 表达式将一个整数 i 转成字符串3) 调用 apply(数字)方法得到转换后的字符串,再调用字符串的 length()方法得到长度,打印输出。4) 第 1 次转换 99 这个数字,第 2 次转换 1000 这个数字。

    public class DemoFunction {
    
     public static void main(String[] args) {
    
        //创建一个 Function 对象
    
        Function<Integer,String> converter = i -> Integer.toString(i);
    
        System.out.println("99 转成字符串的长度是:" + converter.apply(99).length());
    
        System.out.println("1000 转成字符串的长度是:" + converter.apply(1000).length());
     }
    }

    默认方法:andThen()

    Function 接口中有一个默认的 andThen 方法,用来进行组合操作。
    
    先计算当前函数,再计算传入的函数。两个函数依次执行。
    
    andThen 方法的参数是 Function 对象,返回一个 Function 对象。

    JDK 源代码如:

    /**
    默认组合方法,先计算当前函数,再计算传入的函数
    */
    
    default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
        Objects.requireNonNull(after);
        return (T t) -> after.apply(apply(t));
    }

    andThen 方法演示示例: 案例需求:连续进行两个操作:第 1 个操作是将字符串转换成为 int 数字,第 2 个操作将转换好的数字乘以 10。两个操作按照前后顺序组合到一起。1) 让用户从键盘输入 1 个数字,使用字符串接收。2) 创建第 1 个 Function 函数将字符串转成整数3) 创建第 2 个函数将整数乘以 10 返回4) 调用 andThen 方法和 apply,并且输出结果

    public class DemoFunctionAndThen {
        public static void main(String[] args) {
            //用户输入一个字符串
            System.out.println("请输入数字:");
            Scanner input = new Scanner(System.in);
            String str = input.nextLine();
            //第 1 个函数将字符串转成整数
            Function<String,Integer> f1 = s -> Integer.parseInt(s);
            //第 2 个函数将整数乘以 10 返回
            Function<Integer,Integer>  f2 = i -> i * 10;
            //调用 andThen 方法,并且输出结果
            System.out.println("转成整数并乘以 10 以后的结果是:" + f1.andThen(f2).apply(str));
        }
    }

    默认方法:compose()

    Function 中有一个与 andThen 非常类似的 compose 方法。
    
    中文是"组成"的意思,方法参数是 Function,返回值是 Function,
    
    先运行参数的 apply 方法,再调用自己的 apply 方法。

    其 JDK 源代码为:

    /**
    默认组合方法,先计算传入的函数,再计算当前函数
    */
    default <V> Function<V, R> compose(Function<? super V, ? extends T> before) {
        Objects.requireNonNull(before);
        return (V v) -> apply(before.apply(v));
    }
    
    结合 andThen 方法的 JDK 源码实现进行对比,会发现 compose 方法的参数 Lamda 将会先执行。
    
    所以二者只是先后顺序的不同而已。

    compose 方法的演示

     案例需求:创建两个函数对象:1 个将字符串转成大写,1 个将字符串转成小写。分别使用 andThen 和 compose 方法组合调用,查看不同的计算结果。 开发步骤:1) 创建第 1 个 Function,输入输出都是 String 类型,将字符串转成大写。2) 创建第 2 个 Function,输入输出都是 String 类型,将字符串转成小写。3) 调用第 1 个函数的 apply 方法,并且输出值4) 调用第 2 个函数的 apply 方法,并且输出值5) 调用 andThen 方法和 apply 方法查看运行结果6) 调用 compose 方法和 apply 方法查看运行结果

    public class DemoFunctionCompose {
        public static void main(String[] args) {
            Function<String,String> f1=s -> s.toUpperCase();
            Function<String,String> f2 = s -> s.toLowerCase();
            System.out.println("转成大写:" + f1.apply("Hello"));
            System.out.println("转成小写:" + f2.apply("Hello"));
            System.out.println("先转成大写,再转成小写:" + f1.andThen(f2).apply("Hello"));
            
            System.out.println("先转成小写,再转成大写:" + f1.compose(f2).apply("Hello"));
        }
    }

    Function 的应用示例 需求说明请使用 Function 进行函数拼接,按照顺序执行多个函数。操作依次为:1) 将字符串"赵丽颖,20"截取数字年龄部分,得到字符串;2) 将上一步的字符串转换成为 int 类型的数字;3) 将上一步的 int 数字累加 100,得到结果 int 数字。 开发步骤:1) 创建第 1 个 Function 对象,将字符串 20 取出,返回一个字符串2) 创建第 2 个 Function 对象,将字符串转成整数,返回整数3) 创建第 3 个 Function 对象,将整数加 100,返回计算结果4) 调用 andThen 方法 2 次,apply 方法应用字符串:"赵丽颖,20",输出结果 代码实现

    public class DemoFunctionApp {
        public static void main(String[] args) {
            //创建第 1 个 Function 对象,将字符串 20 取出,返回一个字符串
            Function<String,String> fun1 = s -> s.split(",")[1];
            //创建第 2 个 Function 对象,将字符串转成整数,返回整数
            Function<String,Integer> fun2 = s -> Integer.parseInt(s);
            //创建第 3 个 Function 对象,将整数加 100,返回计算结果
            Function<Integer,Integer> fun3 = num -> num + 100;
            //调用 andThen 方法 2 次,apply 方法应用字符串,输出结果
            System.out.println("计算结果:" + fun1.andThen(fun2).andThen(fun3).apply("赵丽颖,20"));
        }
    }

    BinaryOperator 接口

    BinaryOperator 表示对两个相同类型的操作数进行操作,产生相同类型的结果。

    接口中的方法

    static <T> BinaryOperator<T> maxBy(Comparator<? super T> comparator) 
    返回一个BinaryOperator ,它根据指定的Comparator返回两个元素中的较大Comparator   
    
    static <T> BinaryOperator<T> minBy(Comparator<? super T> comparator) 
    返回BinaryOperator返回根据指定的两个元件的较小的Comparator   
    
    这个接口中定义了两个静态方法,
    BiFunction 是用于定义两个操作符的函数接口。

    BinaryOperator 接口中的方法

    T apply(T t, T u); 从父接口 BiFunction 中继承下来的抽象方法,
    传入两个参数 t  u 进行函数计算,返回计算的结果。
    
    两个参数和返回值都是同一种类型。
    
    
    default <V> BiFunction<T,U,V> andThen(Function<? super R,? extends V> after)
    返回一个组合函数,首先将该函数应用于其输入,然后将after函数应用于结果。 
    如果任一函数的评估引发异常,则将其转发给组合函数的调用者。 
    
    参数类型 
    V - after函数的输出类型,以及组合函数 
    参数 
    after - 应用此函数后应用的功能 
    结果 
    一个组合函数首先应用此函数,然后应用 after函数 
    

    方法的演示:apply()

     案例需求:使用 BinaryOperator 接口的 apply()方法,计算 2 个整数的和,并且输出结果。 案例步骤:1) 创建类 Demo25BinaryOperator2) 创建 BinaryOperator 接口,使用 Lambda 实现方法,方法有两个参数,返回方法的计算结果。3) 调用 apply()方法传入实际的参数,打印计算结果。 案例代码:

    public class DemoBinaryOperator {
        public static void main(String[] args) {
    
            BinaryOperator<Integer> operator = (m, n) -> m + n;
    
            System.out.println("计算结果是:" + operator.apply(3, 5));
        }
    }

    静态方法

    
    public static  BinaryOperator minBy(Comparator comparator)
    通过后面的 Comparator 比较器判断,返回两个元素中较小的元素
    
    public static  BinaryOperator maxBy(Comparator comparator)
    通过后面的 Comparator 比较器判断,返回两个元素中较大的元素
    
    Comparator 接口中的静态方法说明
    naturalOrder() 按元素的自然排序的大小进行比较,返回一个 Comparator 对象
    reverseOrder() 按元素的倒序大小进行比较,返回一个 Comparator 对象

    BinaryOperator 接口做为方法参数

     案例需求有如下数组{2,1,3,5},对数组中的每个元素进行替换。替换算法如下:1) 第 0 个元素不变2) 第 0 个+第 1 个元素的结果代替第 1 个元素3) 第 1 个新元素+第 2 个元素的结果代替 2 个4) 第 2 个新元素+第 3 个元素的结果代替第 3 个5) 依次类推,直到所有的元素替换完成为止。

     案例步骤

    Arrays 类中的方法 说明
    void parallelPrefix(T[] array, BinaryOperator op)
    并行累积
    
    作用:对数组中每个元素使用指定的二元操作函数进行替换操作
    
    参数 1:要替换的数组
    
    参数 2:指定二元操作函数

    1) 创建 BinaryOperator对象,指定 2 个数的算法是 m+n2) 创建 Integer 类型的数组:{2,1,3,5}3) 输出操作前的数组4) 调用上面的 parallelPrefix()方法,将 BinaryOperator 做为参数传入5) 输出操作后的数组6) 如果使用不同的算法,则每个元素的替换的结果不同。如:换成两个数相乘。 案例代码

    public static void main(String[] args) {
        BinaryOperator<Integer> operator = (m,n) -> m+n;
        Integer [] arr = {2,1,3,5};
        System.out.println("操作前的数组:" + Arrays.toString(arr)) ;
        Arrays.parallelPrefix(arr,operator);
        System.out.println("操作后的数组:" + Arrays.toString(arr)) ;
    }

    静态方法的演示 案例需求比较两个整数,使用 minBy 静态方法找出最小值比较两个字符串,使用 maxBy 静态方法找出最大值 案例步骤1) 创建 BinaryOperator对象,使用 minBy()静态方法,按数字的正常大小进行比较。2) 输出最小值,调用 apply()方法,传入 2 个整数。3) 创建 BinaryOperator对象,使用 maxBy()静态方法,按字符串的大小进行比较。4) 输出最大值,调用 apply()方法,传入 2 个字符串:"ABCD","xyz" 案例代码

     public static void main(String[] args) {
     //naturalOrder()是 Comparator 中的静态方法,即按数字的正常大小进行比较
     BinaryOperator oper1 = BinaryOperator.minBy(Comparator.naturalOrder());
     System.out.println("最小值是:" + oper1.apply(3,5));
         
     //naturalOrder()是 Comparator 中的静态方法,即按字符串的正常大小进行比较
     BinaryOperator oper2 = BinaryOperator.maxBy(Comparator.naturalOrder());
     System.out.println("最大值是:" + oper2.apply("ABCD","xyz"));
    }

    UnaryOperator 接口

    UnaryOperator 表示对单个操作数的操作,该操作数生成与其操作数类型相同的结果。
    
    UnaryOperator 接口继承于 Function 接口,
    
    所以有 T apply(T t)抽象方法,与前面的 Function 接口中的 apply()方法相同。
    
    它的输入类型和返回类型是相同的类型。

    UnaryOperator 接口的源码

    package java.util.function;
    @FunctionalInterface
    public interface UnaryOperator extends Function {
     /**
    始终返回其输入参数的一元运算符
    */
     static  UnaryOperator identity() {
     return t -> t;
     }
    }

    方法的演示

    UnaryOperator 接口中的方法 说明
    
    T apply(T t); 
         Function 接口中继承下来的抽象方法,使用给定的参数应用此一元运算函数,返回另一个值。
        参数和返回值是同一种类型。
    
    static  UnaryOperator identity() 
        始终返回其输入参数的一元运算符也就是后续 apply()输入的是什么,就返回什么。

     案例步骤1) 使用 UnaryOperator.identity()静态方法创建 UnaryOperator对象2) 应用 apply()方法,输入字符串 abc,得到结果也是 abc。 案例代码

     public static void main(String[] args) {
     //创建一个 UnaryOperator对象,
     UnaryOperator operator = UnaryOperator.identity();
         
     //调用 apply()方法,输出参数的值
     System.out.println("输出与输出一样:" + operator.apply("abc"));
    }

    UnaryOperator 使用方法的参数

     案例需求:有一个整数的列表集合,将集合中每个元素乘以 2,再替换这个元素,输出替换前后的列表集合有一个字符串的列表集合,将集合中每个元素用它的大写进行替换。 案例步骤:

    ArrayList 中的方法 说明
    replaceAll(UnaryOperator operator) 使用一元操作函数的结果,替换列表中的每个元素

    1) 使用 Arrays.asList()创建一个整数列表2) 创建 UnaryOperator一元运算函数,指定运算表达式是 x*23) 调用 ArrayList 的 replaceAll()方法,把上面创建的一元运算函数做为参数传入4) 输出替换前后的列表5) 使用 Arrays.asList()创建一个字符串列表6) 这次直接在 replaceAll()方法中传入 Lambda 表达式,s.toUpperCase()7) 输出替换前后的列表 案例代码:

     public static void main(String[] args) {
     List nums = Arrays.asList(3, 10, 8, 2);
     System.out.println("替换前:" + nums);
     UnaryOperator oper = x -> x * 2;
     nums.replaceAll(oper);
     System.out.println("替换后:" + nums);
         
     List names = Arrays.asList("Jack","Rose","Tom","NewBoy");
     System.out.println("替换前:" + names);
     names.replaceAll(s -> s.toUpperCase());
     System.out.println("替换后:" + names);
    }

    常用的函数式接口小结

    Supplier 提供数据者 
        T get();没有传入参数,有结果。
    
    Consumer 消费数据者 
        void accept(T t); 传入数据,没有结果。 
        andThen()
        
    Predicate 谓语 
        boolean test(T t); 对传入的数据逻辑判断 
        and()
        or()
        negate()
        isEqual()
        
    Function 函数 
        R apply(T t); 传入一个变量返回计算结果 
        andThen()
        compose()
        identity()
        
        
    BinaryOperator 二元操作符 
        T apply(T t,T u); 传入两个参数返回一个结果 
        andThen()
        继承于 BiFunction
        
    UnaryOperator
        继承于 Function
        一元操作符 
        T apply(T t); 传入一个参数返回一个结果 
        andThen()
        compose()
        identity()

    展开全文
  • Java 8 函数接口详细教程

    千次阅读 2019-05-12 19:50:56
    Java 8 函数接口详细教程 本文介绍Java 8中各种函数接口,它们一般应用场景及标准API用法。 Lambdas 表达式 Java8 引入强大的新的语法升级是Lambdas表达式。Lambdas表达式是匿名函数,可视为语言的一等公民进行处理...

    Java 8 函数接口详细教程

    本文介绍Java 8中各种函数接口,它们一般应用场景及标准API用法。

    Lambdas 表达式

    Java8 引入强大的新的语法升级是Lambdas表达式。Lambdas表达式是匿名函数,可视为语言的一等公民进行处理,如可以作为方法的参数或返回值。

    Java 8 之前,我们通常为每种仅需封装单个功能场景要创建一个类。这导致很多不必要的模板代码来定义简单函数表达式。

    所有预定义的函数接口都在 java.util.function 包中。

    函数接口

    所有函数式接口建议增加 @FunctionalInterface注解进行标识。这不仅清晰表明该接口目的,也让编译器在注解接口不满足一定条件是生成错误。

    任何满足SAM(Single Abstract Method)接口是函数式接口。其实现可视为lambda表达式。

    注意Java 8 的缺省方法不是抽象的不算数,因此一个函数接口可能有多个缺省方法。你通过Function文档可以观察到该特点。

    Function<T,R> 接口

    最简单且常用的lambda表达式是Function接口,接收一个值并返回另一个值得方法。单参数函数通过Function接口表示,其返回值和输入参数被参数化:

    public interface Function<T, R> { … }
    

    Function类型其中一个用法是jdk标准库中的Map.computeIfAbsent 方法,其根据map的key返回一个值,但如果key不存在,则计算一个值返回。为了计算值,其传入Function实现:

    Map<String, Integer> nameMap = new HashMap<>();
    Integer value = nameMap.computeIfAbsent("John", s -> s.length());
    

    本例中,通过将函数应用于键计算值,即该键值对存入map中并从方法钓鱼总返回值。顺便说下,也可以通过匹配传入并返回值类型的方法引用代替lambda表达式。

    需说明的是,调用方法的对象隐式实际上是方法的第一个参数,也可以使用方法引用作为函数接口:

    Integer value = nameMap.computeIfAbsent("John", String::length);
    

    函数接口还包括缺省方法compose,其可以组合几个函数至一个函数,并顺序进行执行:

    Function<Integer, String> intToString = Object::toString;
    Function<String, String> quote = s -> "'" + s + "'";
     
    Function<Integer, String> quoteIntToString = quote.compose(intToString);
     
    assertEquals("'5'", quoteIntToString.apply(5));
    

    quoteIntToString 是组合函数,其中quote函数使用intToString 函数的执行结果。

    特殊的基本类型函数接口

    因为基本类型U币能作为泛型参数,jdk提供了常用基本类型的几个Function版本,double,int,long以及它们输入参数和返回值类型的不同组合:

    • IntFunction, LongFunction, DoubleFunction: 参数是特定类型,返回值类型是泛型
    • ToIntFunction, ToLongFunction, ToDoubleFunction: 返回值是特定类型,入参是泛型
    • DoubleToIntFunction, DoubleToLongFunction, IntToDoubleFunction, IntToLongFunction, LongToIntFunction, LongToDoubleFunction — 入参和返回值类型都是固定的,和其名称说明的一样

    对于jdk默认没有提供的类型,如一个函数带short类型,返回byte,但你可以自己进行实现:

    @FunctionalInterface
    public interface ShortToByteFunction {
     
        byte applyAsByte(short s);
     
    }
    

    下面写一个方法使用ShortToByteFunction接口定义的逻辑转换short数值至byte数组:

    public byte[] transformArray(short[] array, ShortToByteFunction function) {
        byte[] transformedArray = new byte[array.length];
        for (int i = 0; i < array.length; i++) {
            transformedArray[i] = function.applyAsByte(array[i]);
        }
        return transformedArray;
    }
    

    这里测试如何实现short数组至byte数组乘以2的转换:

    short[] array = {(short) 1, (short) 2, (short) 3};
    byte[] transformedArray = transformArray(array, s -> (byte) (s * 2));
     
    byte[] expectedArray = {(byte) 2, (byte) 4, (byte) 6};
    assertArrayEquals(expectedArray, transformedArray);
    

    二元函数接口

    带两个参数的lambda表达式,我们需要使用名称包含Bi关键字的函数接口:BiFunction, ToDoubleBiFunction, ToIntBiFunction, and ToLongBiFunction。

    BiFunction 两个参数和返回值都是泛型,而ToDoubleBiFunction和其他类似函数接口可以返回基本类型。

    使用二元lambda表示的典型示例是jdk中Map.replaceAll 方法,其使用计算值替换map中所有值。下面使用BiFunction实现接收key和被替换的值取计算新值并返回:

    Map<String, Integer> salaries = new HashMap<>();
    salaries.put("John", 40000);
    salaries.put("Freddy", 30000);
    salaries.put("Samuel", 50000);
     
    salaries.replaceAll((name, oldValue) -> 
      name.equals("Freddy") ? oldValue : oldValue + 10000);
    

    Supplier函数接口

    Supplier接口是另一个不带任何参数的特殊形式。典型用于延迟生成值。举例,定义double值得平方函数。其不接收一个值,而是Supperlier作为值:

    public double squareLazy(Supplier<Double> lazyValue) {
        return Math.pow(lazyValue.get(), 2);
    }
    

    该函数可以通过使用Supplier实现来延迟生成值。这对于生成值需要花费很多时间情况非常有用。下面使用Guava 的sleepUninterruptibly 的方法进行模拟:

    Supplier<Double> lazyValue = () -> {
        Uninterruptibles.sleepUninterruptibly(1000, TimeUnit.MILLISECONDS);
        return 9d;
    };
     
    Double valueSquared = squareLazy(lazyValue);
    

    另一个Supplier的使用场景是定义流的生成序列逻辑。为了演示,我们使用静态Stream.genernate方法创建斐波那契数值流:

    int[] fibs = {0, 1};
    Stream<Integer> fibonacci = Stream.generate(() -> {
        int result = fibs[1];
        int fib3 = fibs[0] + fibs[1];
        fibs[0] = fibs[1];
        fibs[1] = fib3;
        return result;
    });
    

    Stream.generate方法传入函数作为Supplier函数接口的实现。注意,要成为有用的生成器,Supplier函数接口通常需要某种外部状态。在本例中,它的状态由最后两个斐波那契数列数字组成。
    为了实现该状态,我们使用一个数组而不是一组变量,因为所有在lambda表达式里面使用的外部变量必须是final。

    其他特殊的Supplier 函数接口包括 BooleanSupplier, DoubleSupplier, LongSupplier 和 IntSupplier, 它们的返回类型都是相应的基本类型。

    Consumer 函数接口

    与Supplier相反,Consumer接口接收一个泛型参数但没有返回值。该函数是有副作用的代表(因为修改了参数,lambda表达式不能修改参数)。

    举例,对list中每个名称以输出至控制台的方式进行问候。lambda表达式传入 List.forEach方法实现Consumer函数接口:

    List<String> names = Arrays.asList("John", "Freddy", "Samuel");
    names.forEach(name -> System.out.println("Hello, " + name));
    

    也有特定版本的Consumer — DoubleConsumer, IntConsumer and LongConsumer,接收基本类型值作为参数。更有趣的是BiConsumer接口,其中一个应用场景是迭代map的entry:

    Map<String, Integer> ages = new HashMap<>();
    ages.put("John", 25);
    ages.put("Freddy", 24);
    ages.put("Samuel", 30);
     
    ages.forEach((name, age) -> System.out.println(name + " is " + age + " years old"));
    

    另外一组特殊BiConsumer 接口是 ObjDoubleConsumer, ObjIntConsumer, ObjLongConsumer ,分别接收两个参数,其中之一是泛型,另一个是基本类型。

    Predicate 函数接口

    在数学逻辑中,谓词是一个函数,它接收一个值并返回一个布尔值。Predicate函数接口是一个特殊函数接口,其接收一个泛型类型值,返回一个boolean。典型的应用场景是过滤集合的值:

    List<String> names = Arrays.asList("Angela", "Aaron", "Bob", "Claire", "David");
     
    List<String> namesWithA = names.stream()
      .filter(name -> name.startsWith("A"))
      .collect(Collectors.toList());
    

    在上面的代码中,我们使用流API过滤list的值名称以A开头的,Predicate实现即过滤逻辑代码。和前面示例一样,IntPredicate, DoublePredicate 和 LongPredicate 几个接口接收基础类型。

    Operator 函数接口

    Operator 接口是Function接口的特殊情况,接收和返回类型相同。UnaryOperator接口接收单个参数,其中一个应用是Collection Api的替换list中所有值,使用相同类型的计算值:

    List<String> names = Arrays.asList("bob", "josh", "megan");
    names.replaceAll(name -> name.toUpperCase());
    

    List.replaceAll函数返回void类型,因为其替换一定位置的值。为了实现该目的,用于转换list值的lambda必须返回与其入参类型相同的结果。这就是为什么UnaryOperator在这里很有用。当然也可以使用方法引用代替lambda:

    names.replaceAll(String::toUpperCase);
    

    BinaryOperator接口一个最有趣的用例是reduce操作。如计算整型集合值之和。使用stream api可以实现,但更通用的方式是使用reduce方法:

    List<Integer> values = Arrays.asList(3, 5, 8, 9, 12);
     
    int sum = values.stream().reduce(0, (i1, i2) -> i1 + i2);
    

    reduce方法接收一个初始累加值和BinaryOperator函数接口。该接口参数是相同类型的一对值,函数包括逻辑实现连接两者称为一个相同类型的值。传入函数必须具有结合性,即与值得计算顺序无关,如应满足下面条件:

    op.apply(a, op.apply(b, c)) == op.apply(op.apply(a, b), c)
    

    BinaryOperator 函数的结合性使得并行计算很容易。当然也针对基本类型的UnaryOperator 和 BinaryOperator,依次命名为 DoubleUnaryOperator, IntUnaryOperator, LongUnaryOperator, DoubleBinaryOperator, IntBinaryOperator 和 LongBinaryOperator。

    传统的函数接口

    不是所有的函数接口都来自Java 8 。很多之前版本的接口满足函数接口的条件则可以用作lambda表达式。典型的例子是并行API的 Runnable 和 Callable 接口。在Java 8 中这些接口使用@FunctionalInterface进行标记,这使得并发代码大大得到简化:

    Thread thread = new Thread(() -> System.out.println("Hello From Another Thread"));
    thread.start();
    

    总结

    本文我们描述Java Api提供可以作为lambda表达的不同函数式接口,并通过示例说明其应用场景。

    展开全文
  • 函数、方法和接口

    千次阅读 2019-08-24 16:50:58
    函数对应操作序列,是程序的基本组成元素。Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数函数式编程语言...

    8.25打卡学习记录

    1.4 函数、方法和接口

    函数对应操作序列,是程序的基本组成元素。Go语言中的函数有具名和匿名之分:具名函数一般对应于包级的函数,是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。方法是绑定到一个具体类型的特殊函数,Go语言中的方法是依托于类型的,必须在编译时静态绑定。接口定义了方法的集合,这些方法依托于运行时的接口对象,因此接口对应的方法是在运行时动态绑定的。Go语言通过隐式接口机制实现了面向对象模型

    Go语言程序的初始化和执行总是从main.main函数开始的。但是如果main包导入了其它的包,则会按照顺序将它们包含进main包里(这里的导入顺序依赖具体实现,一般可能是以文件名或包路径名的字符串顺序导入)。如果某个包被多次导入的话,在执行的时候只会导入一次。当一个包被导入时,如果它还导入了其它的包,则先将其它的包包含进来,然后创建和初始化这个包的常量和变量,再调用包里的init函数,如果一个包有多个init函数的话,调用顺序未定义(实现可能是以文件名的顺序调用),同一个文件内的多个init则是以出现的顺序依次调用(init不是普通函数,可以定义有多个,所以也不能被其它函数调·)。最后,当main包的所有包级常量、变量被创建和初始化完成,并且init函数被执行后,才会进入main.main函数,程序开始正常执行。下图是Go程序函数启动顺序的示意图:

    图 1-11 包初始化流程

    要注意的是,在main.main函数执行之前所有代码都运行在同一个goroutine,也就是程序的主系统线程中。因此,如果某个init函数内部用go关键字启动了新的goroutine的话,新的goroutine只有在进入main.main函数之后才可能被执行到。

    1.4.1 函数

    在Go语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名和匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例。当然,Go语言中每个类型还可以有自己的方法,方法其实也是函数的一种。

    // 具名函数
    func Add(a, b int) int {
    	return a+b
    }
    
    // 匿名函数
    var Add = func(a, b int) int {
    	return a+b
    }
    

    Go语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数

    // 多个参数和多个返回值
    func Swap(a, b int) (int, int) {
    	return b, a
    }
    
    // 可变数量的参数
    // more 对应 []int 切片类型
    func Sum(a int, more ...int) int {
    	for _, v := range more {
    		a += v
    	}
    	return a
    }
    

    当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果:

    func main() {
    	var a = []interface{}{123, "abc"}
    
    	Print(a...) // 123 abc
    	Print(a)    // [123 abc]
    }
    
    func Print(a ...interface{}) {
    	fmt.Println(a...)
    }
    

    第一个Print调用时传入的参数是a...,等价于直接调用Print(123, "abc")。第二个Print调用传入的是未解包的a,等价于直接调用Print([]interface{}{123, "abc"})

    不仅函数的参数可以有名字,也可以给函数的返回值命名:

    func Find(m map[int]int, key int) (value int, ok bool) {
    	value, ok = m[key]
    	return
    }
    

    如果返回值命名了,可以通过名字来修改返回值,也可以通过defer语句在return语句之后修改返回值:

    func Inc() (v int) {
    	defer func(){ v++ } ()
    	return 42
    }
    

    其中defer语句延迟执行了一个匿名函·,因为这个匿名函数捕获了外部函数的局部变量v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问

    闭包的这种引用方式访问外部变量的行为可能会导致一些隐含的问题:

    func main() {
    	for i := 0; i < 3; i++ {
    		defer func(){ println(i) } ()
    	}
    }
    // Output:
    // 3
    // 3
    // 3
    

    因为是闭包,在for迭代语句中,每个defer语句延迟执行的函数引用的都是同一个i迭代变量,在循环结束后这个变量的值为3,因此最终输出的都是3。

    修复的思路是在每轮迭代中为每个defer函数生成独有的变量。可以用下面两种方式:

    func main() {
    	for i := 0; i < 3; i++ {
    		i := i // 定义一个循环体内局部变量i
    		defer func(){ println(i) } ()
    	}
    }
    
    func main() {
    	for i := 0; i < 3; i++ {
    		// 通过函数传入i
    		// defer 语句会马上对调用参数求值
    		defer func(i int){ println(i) } (i)
    	}
    }
    

    第一种方法是在循环体内部再定义一个局部变量,这样每次迭代defer语句的闭包函数捕获的都是不同的变量,这些变量的值对应迭代时的值。第二种方式是将迭代变量通过闭包函数的参数传入,defer语句会马上对调用参数求值。两种方式都是可以工作的。不过一般来说,在for循环内部执行defer语句并不是一个好的习惯,此处仅为示例,不建议使用。

    Go语言中,如果以切片为参数调用函数时,有时候会给人一种参数采用了传引用的方式的假象:因为在被调用函数内部可以修改传入的切片的元素。其实,任何可以通过函数参数修改调用参数的情形,都是因为函数参数中显式或隐式传入了指针参数。函数参数传值的规范更准确说是只针对数据结构中固定的部分传值,例如字符串或切片对应结构体中的指针和字符串长度结构体传值,但是并不包含指针间接指向的内容。将切片类型的参数替换为类似reflect.SliceHeader结构体就很好理解切片传值的含义了:

    func twice(x []int) {
    	for i := range x {
    		x[i] *= 2
    	}
    }
    
    type IntSliceHeader struct {
    	Data []int
    	Len  int
    	Cap  int
    }
    
    func twice(x IntSliceHeader) {
    	for i := 0; i < x.Len; i++ {
    		x.Data[i] *= 2
    	}
    }
    

    因为切片中的底层数组部分是通过隐式指针传递(指针本身依然是传值的,但是指针指向的却是同一份的数据),所以被调用函数是可以通过指针修改掉调用参数切片中的数据。除了数据之外,切片结构还包含了切片长度和切片容量信息,这2个信息也是传值的。如果被调用函数中修改了LenCap信息的话,就无法反映到调用参数的切片中,这时候我们一般会通过返回修改后的切片来更新之前的切片。这也是为何内置的append必须要返回一个切片的原因。

    Go语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为Go语言运行时会根据需要动态地调整函数栈的大小。每个goroutine刚启动时只会分配很小的栈(4或8KB,具体依赖实现),根据需要动态调整栈的大小,栈最大可以达到GB级(依赖具体实现,在目前的实现中,32位体系结构为250MB,64位体系结构为1GB)。在Go1.4以前,Go的动态栈采用的是分段式的动态栈,通俗地说就是采用一个链表来实现动态栈,每个链表的节点内存位置不会发生变化。但是链表实现的动态栈对某些导致跨越链表不同节点的热点调用的性能影响较大,因为相邻的链表节点它们在内存位置一般不是相邻的,这会增加CPU高速缓存命中失败的几率。为了解决热点调用的CPU缓存命中率问题,Go1.4之后改用连续的动态栈实现,也就是采用一个类似动态数组的结构来表示栈。不过连续动态栈也带来了新的问题:当连续栈动态增长时,需要将之前的数据移动到新的内存空间,这会导致之前栈中全部变量的地址发生变化。虽然Go语言运行时会自动更新引用了地址变化的栈变量的指针,但最重要的一点是要明白Go语言中指针不再是固定不变的了(因此不能随意将指针保持到数值变量中,Go语言的地址也不能随意保存到不在GC控制的环境中,因此使用CGO时不能在C语言中长期持有Go语言对象的地址)。

    因为,Go语言函数的栈会自动调整大小,所以普通Go程序员已经很少需要关心栈的运行机制的。在Go语言规范中甚至故意没有讲到栈和堆的概念。我们无法知道函数参数或局部变量到底是保存在栈中还是堆中,我们只需要知道它们能够正常工作就可以了。看看下面这个例子:

    func f(x int) *int {
    	return &x
    }
    
    func g() int {
    	x = new(int)
    	return *x
    }
    

    第一个函数直接返回了函数参数变量的地址——这似乎是不可以的,因为如果参数变量在栈上的话,函数返回之后栈变量就失效了,返回的地址自然也应该失效了。但是Go语言的编译器和运行时比我们聪明的多,它会保证指针指向的变量在合适的地方。第二个函数,内部虽然调用new函数创建了*int类型的指针对象,但是依然不知道它具体保存在哪里。对于有C/C++编程经验的程序员需要强调的是:不用关心Go语言中函数栈和堆的问题,编译器和运行时会帮我们搞定;同样不要假设变量在内存中的位置是固定不变的,指针随时可能会变化,特别是在你不期望它变化的时候。

    1.4.2 方法

    方法一般是面向对象编程(OOP)的一个特性,在C++语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是Go语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。面向对象编程(OOP)进入主流开发领域一般认为是从C++开始的,C++就是在兼容C语言的基础之上支持了class等面向对象的特性。然后Java编程则号称是纯粹的面向对象语言,因为Java中函数是不能独立存在的,每个函数都必然是属于某个类的。

    面向对象编程更多的只是一种思想,很多号称支持面向对象编程的语言只是将经常用到的特性内置到语言中了而已。Go语言的祖先C语言虽然不是一个支持面向对象的语言,但是C语言的标准库中的File相关的函数也用到了的面向对象编程的思想。下面我们实现一组C语言风格的File函数:

    // 文件对象
    type File struct {
    	fd int
    }
    
    // 打开文件
    func OpenFile(name string) (f *File, err error) {
    	// ...
    }
    
    // 关闭文件
    func CloseFile(f *File) error {
    	// ...
    }
    
    // 读文件数据
    func ReadFile(f *File, offset int64, data []byte) int {
    	// ...
    }
    

    其中OpenFile类似构造函数用于打开文件对象,CloseFile类似析构函数用于关闭文件对象,ReadFile则类似普通的成员函数,这三个函数都是普通的函数。CloseFileReadFile作为普通函数,需要占用包级空间中的名字资源。不过CloseFileReadFile函数只是针对File类型对象的操作,这时候我们更希望这类函数和操作对象的类型紧密绑定在一起。

    Go语言中的做法是,将CloseFileReadFile函数的第一个参数移动到函数名的开头:

    // 关闭文件
    func (f *File) CloseFile() error {
    	// ...
    }
    
    // 读文件数据
    func (f *File) ReadFile(offset int64, data []byte) int {
    	// ...
    }
    

    这样的话,CloseFileReadFile函数就成了File类型独有的方法了(而不是File对象方法)。它们也不再占用包级空间中的名字资源,同时File类型已经明确了它们操作对象,因此方法名字一般简化为CloseRead

    // 关闭文件
    func (f *File) Close() error {
    	// ...
    }
    
    // 读文件数据
    func (f *File) Read(offset int64, data []byte) int {
    	// ...
    }
    

    将第一个函数参数移动到函数前面,从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给int这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

    方法是由函数演变而来,只是将函数的第一个对象参数移动到了函数名前面了而已。因此我们依然可以按照原始的过程式思维来使用方法。通过叫方法表达式的特性可以将方法还原为普通类型的函数:

    // 不依赖具体的文件对象
    // func CloseFile(f *File) error
    var CloseFile = (*File).Close
    
    // 不依赖具体的文件对象
    // func ReadFile(f *File, offset int64, data []byte) int
    var ReadFile = (*File).Read
    
    // 文件处理
    f, _ := OpenFile("foo.dat")
    ReadFile(f, 0, data)
    CloseFile(f)
    

    在有些场景更关心一组相似的操作:比如Read读取一些数组,然后调用Close关闭。此时的环境中,用户并不关心操作对象的类型,只要能满足通用的ReadClose行为就可以了。不过在方法表达式中,因为得到的ReadFileCloseFile函数参数中含有File这个特有的类型参数,这使得File相关的方法无法和其它不是File类型但是有着相同ReadClose方法的对象无缝适配。这种小困难难不倒我们Go语言码农,我们可以通过结合闭包特性来消除方法表达式中第一个参数类型的差异:

    // 先打开文件对象
    f, _ := OpenFile("foo.dat")
    
    // 绑定到了 f 对象
    // func Close() error
    var Close = func() error {
    	return (*File).Close(f)
    }
    
    // 绑定到了 f 对象
    // func Read(offset int64, data []byte) int
    var Read = func(offset int64, data []byte) int {
    	return (*File).Read(f, offset, data)
    }
    
    // 文件处理
    Read(0, data)
    Close()
    

    这刚好是方法值也要解决的问题。我们用方法值特性可以简化实现:

    // 先打开文件对象
    f, _ := OpenFile("foo.dat")
    
    // 方法值: 绑定到了 f 对象
    // func Close() error
    var Close = f.Close
    
    // 方法值: 绑定到了 f 对象
    // func Read(offset int64, data []byte) int
    var Read = f.Read
    
    // 文件处理
    Read(0, data)
    Close()
    

    Go语言不支持传统面向对象中的继承特性,而是以自己特有的组合方式支持了方法的继承。Go语言中,通过在结构体内置匿名的成员来实现继承:

    import "image/color"
    
    type Point struct{ X, Y float64 }
    
    type ColoredPoint struct {
    	Point
    	Color color.RGBA
    }
    

    虽然我们可以将ColoredPoint定义为一个有三个字段的扁平结构的结构体,但是我们这里将Point嵌入到ColoredPoint来提供XY这两个字段。

    var cp ColoredPoint
    cp.X = 1
    fmt.Println(cp.Point.X) // "1"
    cp.Point.Y = 2
    fmt.Println(cp.Y)       // "2"
    

    通过嵌入匿名的成员,我们不仅可以继承匿名成员的内部成员,而且可以继承匿名成员类型所对应的方法。我们一般会将Point看作基类,把ColoredPoint看作是它的继承类或子类。不过这种方式继承的方法并不能实现C++中虚函数的多态特性。所有继承来的方法的接收者参数依然是那个匿名成员本身,而不是当前的变量。

    type Cache struct {
    	m map[string]string
    	sync.Mutex
    }
    
    func (p *Cache) Lookup(key string) string {
    	p.Lock()
    	defer p.Unlock()
    
    	return p.m[key]
    }
    

    Cache结构体类型通过嵌入一个匿名的sync.Mutex来继承它的LockUnlock方法. 但是在调用p.Lock()p.Unlock()时, p并不是LockUnlock方法的真正接收者, 而是会将它们展开为p.Mutex.Lock()p.Mutex.Unlock()调用. 这种展开是编译期完成的, 并没有运行时代价.

    在传统的面向对象语言(eg.C++或Java)的继承中,子类的方法是在运行时动态绑定到对象的,因此基类实现的某些方法看到的this可能不是基类类型对应的对象,这个特性会导致基类方法运行的不确定性。而在Go语言通过嵌入匿名的成员来“继承”的基类方法,this就是实现该方法的类型的对象,Go语言中方法是编译时静态绑定的。如果需要虚函数的多态特性,我们需要借助Go语言接口来实现。

    1.4.3 接口

    Go语言之父Rob Pike曾说过一句名言:那些试图避免白痴行为的语言最终自己变成了白痴语言(Languages that try to disallow idiocy become themselves idiotic)。一般静态编程语言都有着严格的类型系统,这使得编译器可以深入检查程序员有没有作出什么出格的举动。但是,过于严格的类型系统却会使得编程太过繁琐,让程序员把大好的青春都浪费在了和编译器的斗争中。Go语言试图让程序员能在安全和灵活的编程之间取得一个平衡。它在提供严格的类型检查的同时,通过接口类型实现了对鸭子类型的支持,使得安全动态的编程变得相对容易。

    Go的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念,但Go语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。这种设计可以让你创建一个新的接口类型满足已经存在的具体类型却不用去破坏这些类型原有的定义;当我们使用的类型来自于不受我们控制的包时这种设计尤其灵活有用。Go语言的接口类型是延迟绑定,可以实现类似虚函数的多态功能。

    接口在Go语言中无处不在,在“Hello world”的例子中,fmt.Printf函数的设计就是完全基于接口的,它的真正功能由fmt.Fprintf函数完成。用于表示错误的error类型更是内置的接口类型。在C语言中,printf只能将几种有限的基础数据类型打印到文件对象中。但是Go语言灵活接口特性,fmt.Fprintf却可以向任何自定义的输出流对象打印,可以打印到文件或标准输出、也可以打印到网络、甚至可以打印到一个压缩文件;同时,打印的数据也不仅仅局限于语言内置的基础类型,任意隐式满足fmt.Stringer接口的对象都可以打印,不满足fmt.Stringer接口的依然可以通过反射的技术打印。fmt.Fprintf函数的签名如下:

    func Fprintf(w io.Writer, format string, args ...interface{}) (int, error)
    

    其中io.Writer用于输出的接口,error是内置的错误接口,它们的定义如下:

    type io.Writer interface {
    	Write(p []byte) (n int, err error)
    }
    
    type error interface {
    	Error() string
    }
    

    我们可以通过定制自己的输出对象,将每个字符转为大写字符后输出:

    type UpperWriter struct {
    	io.Writer
    }
    
    func (p *UpperWriter) Write(data []byte) (n int, err error) {
    	return p.Writer.Write(bytes.ToUpper(data))
    }
    
    func main() {
    	fmt.Fprintln(&UpperWriter{os.Stdout}, "hello, world")
    }
    

    当然,我们也可以定义自己的打印格式来实现将每个字符转为大写字符后输出的效果。对于每个要打印的对象,如果满足了fmt.Stringer接口,则默认使用对象的String方法返回的结果打印:

    type UpperString string
    
    func (s UpperString) String() string {
    	return strings.ToUpper(string(s))
    }
    
    type fmt.Stringer interface {
    	String() string
    }
    
    func main() {
    	fmt.Fprintln(os.Stdout, UpperString("hello, world"))
    }
    

    Go语言中,对于基础类型(非接口类型)不支持隐式的转换,我们无法将一个int类型的值直接赋值给int64类型的变量,也无法将int类型的值赋值给底层是int类型的新定义命名类型的变量。Go语言对基础类型的类型一致性要求可谓是非常的严格,但是Go语言对于接口类型的转换则非常的灵活。对象和接口之间的转换、接口和接口之间的转换都可能是隐式的转换。可以看下面的例子:

    var (
    	a io.ReadCloser = (*os.File)(f) // 隐式转换, *os.File 满足 io.ReadCloser 接口
    	b io.Reader     = a             // 隐式转换, io.ReadCloser 满足 io.Reader 接口
    	c io.Closer     = a             // 隐式转换, io.ReadCloser 满足 io.Closer 接口
    	d io.Reader     = c.(io.Reader) // 显式转换, io.Closer 不满足 io.Reader 接口
    )
    

    有时候对象和接口之间太灵活了,导致我们需要人为地限制这种无意之间的适配。常见的做法是定义一个含特殊方法来区分接口。比如runtime包中的Error接口就定义了一个特有的RuntimeError方法,用于避免其它类型无意中适配了该接口:

    type runtime.Error interface {
    	error
    
    	// RuntimeError is a no-op function but
    	// serves to distinguish types that are run time
    	// errors from ordinary errors: a type is a
    	// run time error if it has a RuntimeError method.
    	RuntimeError()
    }
    

    在protobuf中,Message接口也采用了类似的方法,也定义了一个特有的ProtoMessage,用于避免其它类型无意中适配了该接口:

    type proto.Message interface {
    	Reset()
    	String() string
    	ProtoMessage()
    }
    

    不过这种做法只是君子协定,如果有人刻意伪造一个proto.Message接口也是很容易的。再严格一点的做法是给接口定义一个私有方法。只有满足了这个私有方法的对象才可能满足这个接口,而私有方法的名字是包含包的绝对路径名的,因此只能在包内部实现这个私有方法才能满足这个接口。测试包中的testing.TB接口就是采用类似的技术:

    type testing.TB interface {
    	Error(args ...interface{})
    	Errorf(format string, args ...interface{})
    	...
    
    	// A private method to prevent users implementing the
    	// interface and so future additions to it will not
    	// violate Go 1 compatibility.
    	private()
    }
    

    不过这种通过私有方法禁止外部对象实现接口的做法也是有代价的:首先是这个接口只能包内部使用,外部包正常情况下是无法直接创建满足该接口对象的;其次,这种防护措施也不是绝对的,恶意的用户依然可以绕过这种保护机制。

    在前面的方法一节中我们讲到,通过在结构体中嵌入匿名类型成员,可以继承匿名类型的方法。其实这个被嵌入的匿名成员不一定是普通类型,也可以是接口类型。我们可以通过嵌入匿名的testing.TB接口来伪造私有的private方法,因为接口方法是延迟绑定,编译时private方法是否真的存在并不重要。

    package main
    
    import (
    	"fmt"
    	"testing"
    )
    
    type TB struct {
    	testing.TB
    }
    
    func (p *TB) Fatal(args ...interface{}) {
    	fmt.Println("TB.Fatal disabled!")
    }
    
    func main() {
    	var tb testing.TB = new(TB)
    	tb.Fatal("Hello, playground")
    }
    

    我们在自己的TB结构体类型中重新实现了Fatal方法,然后通过将对象隐式转换为testing.TB接口类型(因为内嵌了匿名的testing.TB对象,因此是满足testing.TB接口的),然后通过testing.TB接口来调用我们自己的Fatal方法。

    这种通过嵌入匿名接口或嵌入匿名指针对象来实现继承的做法其实是一种纯虚继承,我们继承的只是接口指定的规范,真正的实现在运行的时候才被注入。比如,我们可以模拟实现一个gRPC的插件:

    type grpcPlugin struct {
    	*generator.Generator
    }
    
    func (p *grpcPlugin) Name() string { return "grpc" }
    
    func (p *grpcPlugin) Init(g *generator.Generator) {
    	p.Generator = g
    }
    
    func (p *grpcPlugin) GenerateImports(file *generator.FileDescriptor) {
    	if len(file.Service) == 0 {
    		return
    	}
    
    	p.P(`import "google.golang.org/grpc"`)
    	// ...
    }
    

    构造的grpcPlugin类型对象必须满足generate.Plugin接口(在"github.com/golang/protobuf/protoc-gen-go/generator"包中):

    type Plugin interface {
    	// Name identifies the plugin.
    	Name() string
    	// Init is called once after data structures are built but before
    	// code generation begins.
    	Init(g *Generator)
    	// Generate produces the code generated by the plugin for this file,
    	// except for the imports, by calling the generator's methods
    	// P, In, and Out.
    	Generate(file *FileDescriptor)
    	// GenerateImports produces the import declarations for this file.
    	// It is called after Generate.
    	GenerateImports(file *FileDescriptor)
    }
    

    generate.Plugin接口对应的grpcPlugin类型的GenerateImports方法中使用的p.P(...)函数却是通过Init函数注入的generator.Generator对象实现。这里的generator.Generator对应一个具体类型,但是如果generator.Generator是接口类型的话我们甚至可以传入直接的实现。

    Go语言通过几种简单特性的组合,就轻易就实现了鸭子面向对象和虚拟继承等高级特性,真的是不可思议。

    感谢Go语言圣经这个只为记录学习!!!

    展开全文
  • 函数接口和lambda表达式

    千次阅读 2017-06-20 21:54:40
    函数接口和lambda表达式函数接口(Functional Interface) :​ 任何接口,如果只包含唯一 一个抽象方法,那么它就是一个FI。(之前它们被称为 SAM类型,即 单抽象方法类型(Single Abstract Method))。接口中的...
  • Lambda表达式及函数接口

    千次阅读 2019-11-26 08:48:46
    Lambda表达式及函数接口 1、函数式编程思想概述 在数学中,函数就是有输入量、输出量的一套计算方案,也就是“拿什么东西做什么事情”。相对而言,面向对象过 分强调“必须通过对象的形式来做事情”,而函数...
  • Java8常用的内置函数接口

    千次阅读 2018-07-29 11:02:12
    JDK 1.8 API中包含了很多内置的函数接口。有些是在以前版本的Java中大家耳熟能详的,例如Comparator接口,或者Runnable接口。对这些现成的接口进行实现,可以通过@FunctionalInterface 标注来启用Lambda功能支持。...
  • 函数接口为:int filter_ansi(char* gbk_string) 注:汉字的GBK编码范围是0×8140-0xFEFE   #include <iostream><br />using namespace std;   Filter_Ansii(const c
  • java中接口类似c++中纯虚函数

    千次阅读 2016-05-15 18:49:36
    Java提供了一种抽象方法的机制即接口,C++中叫纯虚函数。 包含抽象方法的类为抽象类,如果一个类有一个或多个的抽象方法,那么类一定要定义为抽象类。 子类需要继承接口类,且实现接口方法。 1、定义接口  ...
  • 希望本文能够成为Java8 Lambda...理解Functional Interface(函数接口,以下简称FI)是学习Java8 Lambda表达式的关键所在,所以放在最开始讨论。FI的定义其实很简单:任何接口,如果只包含唯一一个抽象方法,那
  • jdk8特性例子 内置函数接口

    千次阅读 2017-02-13 23:04:54
    这个接口包含不同的默认方法,将谓词组成复杂的逻辑组合。 public class PredicateTest { public static void main(String[] args) { Predicate<String> predicate = (s) -> s.length() > 0; // 检查...
  • Oracle官网JNI简介和接口函数分析

    千次阅读 2014-05-30 15:33:33
    目录   第一章 概述 5 JNI概述 5 历史背景 6 JDK1.0本地方法接口 6 ...JNI接口函数和指针 8 编译,加载和链接本地方法 9 解析本地方法名 10 本地方法参数 11 引用JAVA对象 13 全局和局部引用 13 实现
  • Kotlin的高级语言功能——Lambda和匿名函数、内联函数、扩展函数、高阶函数及协同和挂起的基本语法和使用
  • 使用lr测试json接口,向服务端发送json格式请求,接收处理返回响应数据。 主要用到函数:  1)web_custom_request  2)web_reg_save_param (此函数常用户处理动态参数,该方法在LoadRunner中被称为...
  • 函数原型&函数声明

    千次阅读 2019-02-20 09:53:36
    函数声明由函数返回类型、函数名和形参列表组成。形参列表必须包括形参类型,但是不必对形参命名。这三个元素被称为函数原型,函数原型描述了函数接口函数原型类似函数定义时的函数头,又称函数声明。为了能使...
  • Python 函数

    千次阅读 2017-05-31 18:47:59
    在 Python 中,函数是一组执行特定任务的相关语句。 函数有助于将程序分解成更小的块和模块化。随着我们的程序越来越大,函数使其更加有条理和可管理。 此外,还可以避免重复并使代码可重用。
  • 混淆矩阵可视化接口(子函数)

    千次阅读 2016-11-02 10:57:39
    numpy 1.11.0目前接口库@MrLevo520–数据转化接口仍在不断更新目的 将混淆矩阵可视化展现出来 准备工作 亲先安装numpy,matplotlib 接口代码新建一个confusion_matrix_png.py文件,输入如下代码# -*- coding: utf-...
  • 简而言之,OCI是由一组应用程序开发接口(API)组成的,ORACLE提供API的方式是提供一组库。在这组库中,包含了一系列的函数调用,如连接数据库、调用SQL等。 利用OCI,C程序访问ORACLE数据库的模式如图1所示。 ...
  • JDK8新特性2->接口组成与使用

    万次阅读 2020-04-13 11:25:19
    JDK8中新增了一系列的特性,接口组成也进行了一系列的变革,当然是在原有的基础上新增了静态方法和default方法, 一、接口组成 更新之前的组成:①静态常量 ②抽象方法 修饰词 interface 接口名{ 静态常量; ...
  • C++虚函数详解

    万次阅读 多人点赞 2019-04-08 19:31:35
    C++虚函数详解 前言 C++的特性使得我们可以使用函数继承的方法快速实现开发,而为了满足多态与...虚函数的实现是由两个部分组成的,虚函数指针与虚函数表。 虚函数指针 虚函数指针**(virtual function pointer)*...
  • Python 内置函数详解

    万次阅读 多人点赞 2019-11-13 17:21:35
    不过,在大家公认的所谓内置函数里面,有很多并不是真的函数,而是内置类,只是因为使用起来和真正的函数没有什么不同,所以也就约定俗成地统称为内置函数了。比如,我们常说的类型转换函数 int()、str()、float() ...
  • C++中的函数原型和函数定义

    千次阅读 热门讨论 2020-08-30 14:44:23
    函数声明由函数返回类型、函数名和形参列表组成。这三个元素被称为函数原型,函数原型描述了函数接口函数原型在c++中就是函数声明,因为它定义了函数名、参数个数、参数类型和返回值。定义是函数声明加上对这...
  • Python内置函数

    千次阅读 多人点赞 2019-05-10 15:10:36
    Python内置函数Python abs()函数Python all() 函数Python any() 函数Python basestring() 函数Python bin() 函数Python bool() 函数Python bytearray() 函数Python callable() 函数Python chr() 函数Python ...
  • 函数和代码复用

    万次阅读 多人点赞 2019-06-06 16:31:41
    1、关于递归函数的描述,以下选项中正确的是 A.包含一个循环结构 B.函数比较复杂 C.函数内部包含对本函数的再次调用 D.函数名称作为返回值 答案:D 答案解析:递归函数是指函数内部包含对本函数的再次调用。 ...
  • 将一些孤立的单通道数组合并成一个多通道数组,创建一个由多个单通道阵列组成的多通道阵列。 merge()函数的C++有两个版本: void merge(const Mat& mv, size_tcount, OutputArray dst); void merge...
  • 本文主要介绍类的友元函数、虚函数、静态成员、const对象和volatile对象以及指向类成员的指针。 友元函数 从之前的文章可知,当把类中的成员的访问权限定义为私有的或者保护的时,在类的外面,只能通过该类的...
  • POSIX函数

    千次阅读 2016-12-22 21:42:39
    该模块包含了定义在 IEEE 1003.1(POSIX.1)标准文档里的函数接口,通过其它手段无法访问。 警告: 通过POSIX函数,可以检索很多敏感数据,例如:posix_getpwnam()以及其它函数。当开启了安全模式,POSIX函数...
  • ,但是要讲解这一部分需要匿名内部类、lambda表达式以及函数接口的相关知识,本文将分为两篇文章来讲解上述内容,读者可以按需查阅。 Java 匿名内部类、lambda表达式与函数接口 Java Stream API 本文是该系列...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 328,751
精华内容 131,500
关键字:

组成的函数接口