10

Java 8新增的lambda表达式毫无疑问是令人非常激动的,从此我们可以非常简洁的定义和使用代码块而不是用繁琐的匿名内部类来实现。而接口是lambda表达式的基础,要理解lambda表达式就要先理解接口的概念。

接口

Java中接口是对类行为的抽象。似乎继承也能做到这件事,它们的区别在于Java中类只能有一个父类,而接口是可以实现多个的。所以接口更倾向于类的一部分抽象,也就是行为的抽象,而不是类本身的抽象。

语法

要定义一个接口很简单,使用关键字interface后面再跟上接口名称就可以了。类可以用implements关键字来实现接口。

public interface A {
    void test();
}
  • 接口不允许有实例域,但可以有常量
  • 接口中的域都会自动声明为public static final
  • 接口中的方法都会自动声明为public
  • 接口中可以声明抽象方法,Java 8以后还可以声明静态方法和默认方法
// Java 8版本
public interface A {
    //常量
    String AUTHOR = "Yuicon";
    //抽象方法
    void test();
    //默认方法
    default void testDefault(){}
    //静态方法
    static void testStatic(){}
}

默认方法的冲突

如果一个类实现的接口中有签名相同的默认方法,那么就会有冲突的问题。在Java中解决这个问题有一些明确的规则:

  • 在超类中已有同签名的方法,就会忽略接口中的默认方法,也就是超类优先
  • 接口中默认方法和另一个默认方法或者抽象方法冲突的,必须要覆盖这个方法

lambda表达式

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。之所以会有这么一个特性,是因为原先在Java中传递一个代码块是非常繁琐的一件事情,必须要构建一个对象。比如常用的Runnable接口:

Runnable runnable = new Runnable() {
    @Override
    public void run() {
        System.out.println("我好麻烦");
    }
};

lambda版本就非常简洁:

Runnable runnable = () -> System.out.println("我很简洁");

是的,lambda版本只要一行就完成了任务。

语法

在我看来lambda表达式是一种语法糖,它提供了一种简洁、易懂的方式来实现只有一个抽象方法的接口。关键词是只有一个抽象方法的接口,比如这样一个接口:

@FunctionalInterface
public interface A {
    void test();
}

A a = () -> System.out.println("test");
a.test();

其中@FunctionalInterface注解是用来标记接口为函数式接口,去掉也不会影响功能,添加了这个注解后编译器会检查接口内是否只有一个抽象方法。

lambda表达式主要有以下要素:

  • 参数
  • 箭头 ->
  • 方法体
  • 自由变量

参数

lambda表达式的参数和普通方法的参数并无太大区别,主要的区别点有:

  • 如果参数的类型可以被编译器推导出来,那么可以省略参数类型
  • 如果参数的类型可以被编译器推导出来,而且只有一个参数,那么就可以省略括号
Consumer<String> consumer = s -> System.out.println(s); 

方法体

lambda表达式的方法体内只有一条语句的时候,可以不加大括号且无需指定返回值,编译器会自动推导。方法体内有多条语句的时候就需要加大括号并手动指定返回值,不过lambda表达式是没有自己的作用域的,这点需要注意。

Supplier<String> supplier = () -> {
    String s = "test";
    return s;
};

自由变量

自由变量是指非参数而且不在方法体内定义的变量,我们来看一个例子:

    public static void main(String[] args) {
        String test = "test";
        A a = () -> System.out.println(test);
        a.test();
    }

例子中的变量test就是一个自由变量,代码块a引用了外部方法的变量,这就是一个闭包了。lambda表达式会复制一份自由变量的值,对象的话就是复制一个引用,因此lambda表达式离开了原作用域也能正常使用自由变量。不过lambda表达式对自由变量是有要求的,自由变量必须是不可变的,原因是并发执行时不安全。以下代码是错误的:

for (int i = 0; i < 9; i++) {
    // error
    A a = () -> System.out.println(i);
}

方法引用

方法引用是语法糖的语法糖,顾名思义方法引用是引用已有方法的一个特性。它的形式如下:

@FunctionalInterface
public interface A {

    void test(String s);

}

A a = System.out::println;
a.test("test");

之所以说方法引用是语法糖的语法糖是因为A a = System.out::println;完全等价于A a = s -> System.out.println(s);,方法引用有5种情况:

  • object::instanceMethod
  • this::instanceMethod
  • super::instanceMethod // 超类方法
  • Class::staticMethod
  • Class::instanceMethod

前4种情况和lambda表达式是完全等价的,第5种情况比较特殊,第一个参数会成为方法的目标。比如String::compareToIgnoreCase等同于 (x, y)-> x.compareToIgnoreCase(y)

构造器引用

构造器引用是引用对象的构造器,用的是特殊的方法名new,使用形式为Object::new,使用方法和方法引用差不多。

常用函数式接口

JDK已经提供了常用的函数式接口基本上是不需要自己写函数式接口的。

后记

一周一篇是不可能一周一篇的,人懒起来就和咸鱼一样根本不会动弹。还好人是会变通的,上周少了这周补上不就行了!Java被人诟病繁琐不是一天两天了,在各种新生编程语言的追赶下Java也要加快自己的演进了,更改发布周期就是一个很好的信号。

参考资料:
《Java核心技术 卷1》


Yuicon
495 声望23 粉丝