返回介绍

6.2 函数式编程基础

发布于 2024-08-21 22:20:21 字数 8368 浏览 0 评论 0 收藏 0

在正式进入函数式编程之前,有必要先了解一下Java 8为支持函数式编程所做的基础性的改进,这里,将简要介绍一下FunctionalInterface注释、接口默认方法和方法句柄。

6.2.1 FunctionalInterface注释

Java 8提出了函数式接口的概念。所谓函数式接口,简单来说,就是只定义了单一抽象方法的接口。比如下面的定义:

@FunctionalInterface
public static interface IntHandler{
  void handle(int i);
}

注释FunctionalInterface用于表明IntHandler接口是一个函数式接口,该接口被定义为只包含一个抽象方法handle(),因此它符合函数式接口的定义。如果一个函数满足函数式接口的定义,那么即使不标注为@FunctionalInterface,编译器依然会把它看做函数式接口。这有点像@Override注释,如果你的函数符合重载的要求,无论你是否标注了@Override,编译器都会识别这个重载函数,但一旦你进行了标注,而实际的代码不符合规范,那么就会得到一个编译错误。如图6.1所示,展示了一个不符合规范,却被标注为@FunctionalInterfacede接口。很显然,该IntHandler包含两个抽象方法,因此不符合函数式接口的要求,又因为IntHandler接口被标注为函数式接口,产生矛盾,故编译出错。

图6-1 不符合规范的函数式接口

这里需要强调的是,函数式接口只能有一个抽象方法,而不是只能有一个方法。这分两点来说明:首先,在Java 8中,接口运行存在实例方法(参见下节的“接口默认方法”),其次任何被java.lang.Object实现的方法,都不能视为抽象方法,因此,下面的NonFunc接口不是函数式接口,因为equals()方法在java.lang.Object中已经实现。

interface NonFunc {
  boolean equals(Object obj);
}

同理,下面实现的IntHandler接口符合函数式接口要求,虽然看起来它不像,但实际上它是一个完全符合规范的函数式接口。

@FunctionalInterface
public static interface IntHandler{
  void handle(int i);
  boolean equals(Object obj);
}

函数式接口的实例可以由方法引用或者lambda表达式进行构造,这个我们将在后面进一步举例说明。

6.2.2 接口默认方法

在Java 8之前的版本,接口只能包含抽象方法。但从Java 8之后,接口也可以包含若干个实例方法。这一改进使得Java 8拥有了类似于多继承的能力。一个对象实例,将拥有来自于多个不同接口的实例方法。

比如,对于接口IHorse,实现如下:

public interface IHorse{ void eat(); default void run(){ System.out.println("hourse run"); } }

在Java 8中,使用default关键字,可以在接口内定义实例方法。注意,这个方法并非抽象方法,而是拥有特定逻辑的具体实例方法。

所有的动物都能自由呼吸,所以,这里可以再定义一个IAnimal接口,它也包含一个默认方法breath()。

public interface IAnimal {
  default void breath(){
    System.out.println("breath");
  }
}

骡是马和驴的杂交物种,因此骡(Mule)可以实现为IHorse,同时骡也是动物,因此有:

public class Mule implements IHorse,IAnimal{ @Override public void eat() { System.out.println("Mule eat"); } public static void main(String[] args) { Mule m=new Mule(); m.run(); m.breath(); } }

注意上述代码中Mule实例同时拥有来自不同接口的实现方法。这在Java 8之前是做不到的。从某种程度上说,这种模式可以弥补Java单一继承的一些不便。但同时也要知道,它也将遇到和多继承相同的问题,如图6.2所示。如果IDonkey也存在一个默认的run()方法,那么同时实现它们的Mule,就会不知所措,因为它不知道应该以哪个方法为准。

图6-2 接口默认方法带来的多继承问题

增加一个IDonkey的实现:

public interface IDonkey{
  void eat();
  default void run(){
    System.out.println("Donkey run");
  }
}

修改骡Mule的实现如下,注意它同时实现了IHorse和IDonkey:

public class Mule implements IHorse,IDonkey,IAnimal{ @Override public void eat() { System.out.println("Mule eat"); } public static void main(String[] args) { Mule m=new Mule(); m.run(); m.breath(); } }

此时,由于IHorse和IDonkey拥有相同的默认实例方法,故编译器会抛出一个错误:

Duplicate default methods named run with the parameters () and () are inherited from the types
IDonkey and IHorse

为了让Mule同时实现IHorse和IDonkey,在这里,我们不得不重新实现一下run()方法,让编译器可以进行方法绑定。修改Mule的实现如下:

public class Mule implements IHorse,IDonkey,IAnimal{ @Override public void run(){ IHorse.super.run(); } @Override public void eat() { System.out.println("Mule eat"); } public static void main(String[] args) { Mule m=new Mule(); m.run(); m.breath(); } }

在这里,将Mule的run()方法委托给IHorse实现,当然,大家也可以有自己的实现。

接口默认实现对于整个函数式编程的流式表达非常重要。比如,大家熟悉的java.util.Comparator接口,它在JDK 1.2时就已经被引入,用于在排序时给出两个对象实例的具体比较逻辑。在Java 8中,Comparator接口新增了若干个默认方法,用于多个比较器的整合。其中一个常用的默认方法如下:

default Comparator<T> thenComparing(Comparator<? super T> other) {
  Objects.requireNonNull(other);
  return (Comparator<T> & Serializable) (c1, c2) -> {
    int res = compare(c1, c2);
    return (res != 0) ? res : other.compare(c1, c2);
  };
}

有了这个默认方法,在进行排序时,我们就可以非常方便地进行元素的多条件排序,比如,如下代码构造一个比较器,它先按照字符串长度排序,继而按照大小写不敏感的字母顺序排序。

Comparator<String> cmp = Comparator.comparingInt(String::length)
    .thenComparing(String.CASE_INSENSITIVE_ORDER);

6.2.3 lambda表达式

lambda表达式可以说是函数式编程的核心。lambda表达式即匿名函数,它是一段没有函数名的函数体,可以作为参数直接传递给相关的调用者。lambda表达式极大地增强了Java语言的表达能力。

下例展示了lambda表达式的使用,在forEach()函数中,传入的就是一个lambda表达式,它完成了对元素的标准输出操作。可以看到这段表达式并不像函数一样有名字,非常类似匿名内部类,它只是简单地描述了应该执行的代码段。

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6);
numbers.forEach((Integer value) -> System.out.println(value));

和匿名对象一样,lambda表达式也可以访问外部的局部变量,如下所示:

final int num = 2;
Function<Integer, Integer> stringConverter = (from) -> from * num;
System.out.println(stringConverter.apply(3));

上述代码可以编译通过,正常执行,并输出6。与匿名内部对象一样,在这种情况下,外部的num变量必须申明为final,这样才能保证在lambda表达式中合法的访问它。

但奇妙的是,对于lambda表达式而言,即使去掉上述的final定义,程序依然可以编译通过!但千万不要以为这样你就可以修改num的值了。实际上,这只是Java 8做了一个掩人耳目的小处理,它会自动地将在lambda表达式中使用的变量视为final。因此,下述代码是可以编译通过的:

int num = 2;
Function<Integer, Integer> stringConverter = (from) -> from * num;
System.out.println(stringConverter.apply(3));

但是,如果像下面这么写,就不行:

int num = 2;
Function<Integer, Integer> stringConverter = (from) -> from * num;
  num++;
System.out.println(stringConverter.apply(3));

上述的num++会引起一个编译错误:

Local variable num defined in an enclosing scope must be final or effectively final

6.2.4 方法引用

方法引用是Java 8中提出的用来简化lambda表达式的一种手段。它通过类名和方法名来定位到一个静态方法或者实例方法。

方法引用在Java 8中的使用非常灵活。总的来说,可以分为以下几种。

· 静态方法引用:ClassName::methodName

· 实例上的实例方法引用:instanceReference::methodName

· 超类上的实例方法引用:super::methodName

· 类型上的实例方法引用:ClassName::methodName

· 构造方法引用:Class::new

· 数组构造方法引用:TypeName[]::new

首先,方法引用使用“::”定义,“::”的前半部分表示类名或者实例名,后半部分表示方法名称。如果是构造函数,则使用new表示。

下例展示了方法引用的基本使用:

public class InstanceMethodRef {
  public static void main(String[] args) {
    List<User> users=new ArrayList<User>();
    for(int i=1;i<10;i++){
      users.add(new User(i,"billy"+Integer.toString(i)));
    }
    users.stream().map(User::getName).forEach(System.out::println);
  }
}

对于第1个方法引用“User::getName”,表示User类的实例方法。在执行时,Java会自动识别流中的元素(这里指User实例)是作为调用目标还是调用方法的参数。在“User::getName”中,显然流内的元素都应该作为调用目标,因此实际上,在这里调用了每一个User对象实例的getName()方法,并将这些User的name作为一个新的流。同时,对于这里得到的所有name,使用方法引用System.out::println进行处理。这里的System.out为PrintStream对象实例,因此,这里表示System.out实例的println方法,系统也会自动判断,流内的元素此时应该作为方法的参数传入,而不是调用目标。

一般来说,如果使用的是静态方法,或者调用目标明确,那么流内的元素会自动作为参数使用。如果函数引用表示实例方法,并且不存在调用目标,那么流内元素就会自动作为调用目标。

因此,如果一个类中存在同名的实例方法和静态函数,那么编译器就会感到很困惑,因为此时,它不知道应该使用哪个方法进行调用。它既可以选择同名的实例方法,将流内元素作为调用目标,也可以使用静态方法,将流元素作为参数。

请看下面的例子:

public class BadMethodRef {
  public static void main(String[] args) {
    List<Double> numbers=new ArrayList<Double>();
    for(int i=1;i<10;i++){
      numbers.add(Double.valueOf(i));
    }
    numbers.stream().map(Double::toString).forEach(System.out::println);
  }
}

上述代码试图将所有的Double元素转为String并将其输出,但是很不幸,在Double中同时存在以下两个函数:

public static String toString(double d)
public String toString()

此时,对函数引用的处理就出现了歧义,因此,这段代码在编译时就会抛出如下错误:

Ambiguous method reference: both toString() and toString(double) from the type Double are
eligible

方法引用也可以直接使用构造函数。首先,查看模型类User的定义:

public class User{
  private int id;
  private String name;

  public User(int id,String name){
    this.id=id;
    this.name=name;
  }
  //这里省略对字段的setter和getter
}

下面的方法引用调用了User的构造函数:

public class ConstrMethodRef {
  @FunctionalInterface
  interface UserFactory<U extends User> {
    U create(int id, String name);
  }

  static UserFactory<User> uf=User::new;

  public static void main(String[] args) {
    List<User> users=new ArrayList<User>();
    for(int i=1;i<10;i++){
      users.add(uf.create(i, "billy"+Integer.toString(i)));
    }
    users.stream().map(User::getName).forEach(System.out::println);
  }
}

在此,UserFactory作为User的工厂类,是一个函数式接口。当使用User::new创建接口实例时,系统会根据UserFactory.create()的函数签名来选择合适的User构造函数,在这里,很显然就是public User(int id,String name)。在创建UserFactory实例后,对UserFactory.create()的调用,都会委托给User的实际构造函数进行,从而创建User对象实例。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文