黑马程序员技术交流社区

标题: Java 8 Lambda 表达式学习 [打印本页]

作者: jannnonx    时间: 2016-6-20 17:59
标题: Java 8 Lambda 表达式学习
lambda表达式,是一段可以传递的代码,可以被多次执行。在 java8 之前,如果我们想写一个简单的比较器 Compartor ,我们需要创建一个实现类或者一个匿名内部类类传入到需要比较的方法内当中。

在 java8 之前传递一段代码不是很容易,现在我们想要实现一个通过传递代码来检查某个字符串的长度是否小于另外一个字符串的长度。

(String first, String second) -> Integer.compare(first.length(), second.length());
上面这段代码就是 lambda 表达式,这个表达式不仅仅是一个简单的代码块,还必须指定传递给代码的所有变量。

Java 当中 lambda 表达式的格式是:参数、箭头(->)、以及一个表达式。如果负责计算的代码无法用一个表达式表示,可以使用 {} 括起来。

如果 lambda 没有参数,可以使用 () 来表示,如果 lambda 表达式的参数类型可以被推导,那么可以省略掉。

Comparator<String> comparator = (first, second) -> Integer.compare( first.length(), second.length());
上面的例子当中会推导出 first 和 second 的类型是 String ,因为表达式赋值给了一个字符串比较器。

注意,在 lambda 表达式当中只在某些分支有返回值是不合法的。

函数式接口

Java 当中有许多接口都需要封装代码块, Runnable 、 Compartor 等等。

对于只包含一个方法的接口,可以通过 lambda 表达式来创建该接口的对象,这种接口被称为函数式接口。

在 java.util.function 包下面提供了许多通用的函数式接口。

可以在任意函数式接口上面使用 @FunctionalInterface 来标识它是一个函数式接口,但是该注解不是强制的。

当 lambda 表达式被转换成一个函数式接口的实例时,需要注意处理检查时异常,如下代码。

Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };
如果不加 try catch 语句的话,这个赋值语句就会编译错误,因为 Runnable 的 run 方法是没有异常抛出的。

Callable 是可以抛出任何异常,并且有返回值,但是我们不想返回任何数据的时候可以如下定义:

Callable<Void> callable = () -> {
            System.out.println("xxx");
            return null;
        };
方法引用

有时候,我们想传递的代码已经有现成的实现了。例如,我们仅仅想点击按钮时候打印 event 对象,可以进行如下代码:

button.setOnAction(System.out::println);
表达式 System.out::println 是一个方法引用,等同于 lambda 表达式 x -> System.out.println(x);

:: 操作符将方法名和对象或类的名字分隔开。有以下三种主要的使用情况:

对象 :: 实例方法
类 :: 静态方法
类 :: 实例方法
前两种情况,方法引用相当于提供方法参数的 lambda 表达式。 System.out::println 等同于 x -> System.out.println(x);

Math::pow 等同于 (x, y) -> Math.pow(x, y);

第三种情况,第一个参数会成为执行方法的对象。例如, Sting::compareToIgnoreCase 等同于 (x, y) -> x.compareToIgnoreCase(y);

Comparator<String> comparator = String::compareTo;
当然还可以捕获 this 指针,this :: equals 相当于 x -> this.equals(x);

构造器引用

构造器引用与方法引类似,不同的是构造器引用使用的方法名是 new。例如,Buttton::new。

List<String> strings = new ArrayList<String>();
strings.add("a");
strings.add("b");
strings.add("c");
Stream<Button> stream = strings.stream().map(Button::new);
List<Button> buttons = stream.collect(Collectors.toList());
先不详细介绍 stream map collect 方法,主要看对于每个列表元素会调用 Button 的构造方法。虽然 Button 有多个构造器,但是会选择只有一个 String 参数的构造器。

数组类型的构造器引用,int[]::new 是一个含有一个参数的构造器引用,这个参数就是数组的长度,相当于 x -> new int[x]。

变量作用域

有如下代码:

public static void repeat(String string, int count) {
        Runnable runnable = () -> {
            for (int i = 0; i < count; i++) {
                System.out.println(this.toString());
                Thread.yield();
            }
        };
        new Thread(runnable).start();
    }
上面这段代码的两个参数没有设置成 final 的,这在 JDK7 之前是会编译错误的,同样在 java8 当中匿名内部类访问外部也不需要 final 来修饰。

分析下上面的代码,由于有 Thread.yield 所以可能其他线程占用 CPU 先执行,然后方法 repeat() 先反回了,才执行 runnable,那么这个时候 string 和 count 这 2 个参数怎么办?

首先一个 lambda 表达式需要有三个部分:

一段代码
参数
自由变量的值,这里的“自由”指的是那些不是传入表达式的参数并且没有在代码中定义的变量。
上面的那个例子当中有两个自由变量,string 和 count,lambad 表达式必须存放这两个变量的值。并且含有自由变量的代码块被称为闭包。

在 lambda 表达式当中被引用的变量的值不可以被更改,编译器会检查修改操作:

public void repeat(String string, int count) {
        Runnable runnable = () -> {
            for (int i = 0; i < count; i++) {
                string = string + "a";//编译出错
                System.out.println(this.toString());
            }
        };
        new Thread(runnable).start();
    }
在 lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量。

String first = "";
Comparator<String> comparator = (first, second) -> Integer.compare(first.length(),//编译会出错
                second.length());
lambda 表达式中使用 this 会引用创建该 lambda 表达式的方法的 this 参数,

public class Testmain2 {
    public static void main(String[] args) {
        Testmain2 testmain2 = new Testmain2();
        testmain2.method();
    }

    @Override
    public String toString() {
        return "aaaa";
    }

    public void method() {
        Runnable runnable = () -> {
            System.out.println(this.toString());
        };
        new Thread(runnable).start();
    }
}
上面的例子执行后会输出:aaaa。

默认方法

在集合库当中提供了一些函数表达式,例如 forEach 方法:

list.forEach(System.out::println);
由于集合的接口是之前定义的,新添加一个 forEach 方法会导致老的代码不兼容,但是 java8 当中是给接口设计成可以包含具体实现的默认方法来解决这个问题。

public interface Person {
    long getID();

    default String getName() {
        return "name";
    }
}
如果要实现 Person 接口,那么必须实现 getID 方法,getName 方法可以不实现。

如果一个接口定义了一个默认方法,而另外一个父类中又定义了同名的方法,那么如何选择?有以下规则:

选择父类中的方法,如果父类提供了具体的实现方式,那么接口中具有相同名称和参数的默认方法会被忽略。
接口冲突,如果需要实现两个接口,并且这两个接口有两个相同签名的默认方法,那么子类就需要覆盖重写这个方法。
如果一个子类继承了一个父类,并且实现了一个接口,并且父类和接口有相同签名的默认方法,那么之类继承父类当中的实现,类优先可以保持 java7 的兼容性。

注意,不能为 Object 中的方法重新定义个默认方法。

接口中的静态方法

java8 可以在接口当中添加静态方法,便于把一些工具方法加入到接口当中,所以类似一些, Collections 和 Paths 类比较尴尬。




欢迎光临 黑马程序员技术交流社区 (http://bbs.itheima.com/) 黑马程序员IT技术论坛 X3.2