《疯狂java讲义》学习(51):Lambda表达式

版权声明:本文为博主原创文章,如若转载请注明出处 https://blog.csdn.net/tonydz0523/article/details/87858100

Lambda表达式

Lambda这个名字来源于学术界的λ演算,具体我们就不探讨了。理解Lambda表达式,我们需要先回顾一下接口、匿名内部类和代码传递。

通过接口传递代码

我们之前介绍过接口以及面向接口的编程,针对接口而非具体类型进行编程,可以降低程序的耦合性,提高灵活性,提高复用性。接口常被用于传递代码,比如,我们知道File有如下方法:

public File[] listFiles(FilenameFilter filter)

listFiles需要的其实不是FilenameFilter对象,而是它包含的如下方法:

boolean accept(File dir, String name);

或者说,listFiles希望接受一段方法代码作为参数,但没有办法直接传递这个方法代码本身,只能传递一个接口。
再如,类Collections中的很多方法都接受一个参数Comparator,比如:

public static <T> void sort(List<T> list, Comparator<? super T> c)

他们需要的也不是Comparator对象,而是它包含的如下方法:

int compare(T o1, T o2);

但是,没有办法直接传递方法,只能传递一个接口。
又如,异步任务执行服务ExecutorService,提交任务的方法有:

<T> Future<T> submit(Callable<T> task);
Future<? > submit(Runnable task);

Callable和Runnable接口也用于传递任务代码。
通过接口传递行为代码,就要传递一个实现了该接口的实例对象,在之前的章节中,最简洁的方式是使用匿名内部类,比如:

//列出当前目录下的所有扩展名为.txt的文件
File f = new File(".");
File[] files = f.listFiles(new FilenameFilter(){
    @Override
    public boolean accept(File dir, String name) {
        if(name.endsWith(".txt")){
            return true;
        }
        return false;
    }
});

将files按照文件名排序,代码为:

Arrays.sort(files, new Comparator<File>() {
    @Override
    public int compare(File f1, File f2) {
        return f1.getName().compareTo(f2.getName());
    }
});

提交一个最简单的任务,代码为:

ExecutorService executor = Executors.newFixedThreadPool(100);
executor.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("hello world");
    }
});

Lambda语法

Java 8提供了一种新的紧凑的传递代码的语法:Lambda表达式。对于前面列出文件的例子,代码可以改为:

File f = new File(".");
File[] files = f.listFiles((File dir, String name) -> {
    if(name.endsWith(".txt")) {
        return true;
    }
    return false;
});

可以看出,相比匿名内部类,传递代码变得更为直观,不再有实现接口的模板代码,不再声明方法,也没有名字,而是直接给出了方法的实现代码。Lambda表达式由->分隔为两部分,前面是方法的参数,后面{}内是方法的代码。上面的代码可以简化为:

扫描二维码关注公众号,回复: 5290125 查看本文章
File[] files = f.listFiles((File dir, String name) -> {
    return name.endsWith(".txt");
});

当主体代码只有一条语句的时候,括号和return语句也可以省略,上面的代码可以变为:

File[] files = f.listFiles((File dir, String name) -> name.endsWith(".txt"));

注意:没有括号的时候,主体代码是一个表达式,这个表达式的值就是函数的返回值,结尾不能加分号,也不能加return语句。
方法的参数类型声明也可以省略,上面的代码还可以继续简化为:

File[] files = f.listFiles((dir, name) -> name.endsWith(".txt"));

之所以可以省略方法的参数类型,是因为Java可以自动推断出来,它知道listFiles接受的参数类型是FilenameFilter,这个接口只有一个方法accept,这个方法的两个参数类型分别是File和String。这样简化下来,代码是不是简洁多了?
排序的代码用Lambda表达式可以写为:

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

提交任务的代码用Lambda表达式可以写为:

executor.submit(()->System.out.println("hello"));

参数部分为空,写为()。
当参数只有一个的时候,参数部分的括号可以省略。比如,File还有如下方法:

public File[] listFiles(FileFilter filter)

FileFilter的定义为:

public interface FileFilter {
    boolean accept(File pathname);
}

使用FileFilter重写上面的列举文件的例子,代码可以为:

File[] files = f.listFiles(path -> path.getName().endsWith(".txt"));

与匿名内部类类似,Lambda表达式也可以访问定义在主体代码外部的变量,但对于局部变量,也可只能访问final类型的变量,与匿名内部类的区别是,它不要求变量声明为final,但变量事实上不能被重新赋值。比如:

String msg = "hello world";
executor.submit(()->System.out.println(msg));

可以访问局部变量msg,但msg不能被重新赋值,如果这样写:

String msg = "hello world";
msg = "good morning";
executor.submit(()->System.out.println(msg));

Java编译器会提示错误。
这个原因与匿名内部类是一样的,Java会将msg的值作为参数传递给Lambda表达式,为Lambda表达式建立一个副本,它的代码访问的是这个副本,而不是外部声明的msg变量。如果允许msg被修改,则程序员可能会误以为Lambda表达式读到修改后的值,引起更多的混淆。
为什么非要建立副本,直接访问外部的msg变量不行吗?不行,因为msg定义在栈中,当Lambda表达式被执行的时候,msg可能早已被释放了。如果希望能够修改值,可以将变量定义为实例变量,或者将变量定义为数组,比如:

String[] msg = new String[]{"hello world"};
msg[0] = "good morning";
executor.submit(()->System.out.println(msg[0]));

从以上内容可以看出,Lambda表达式与匿名内部类很像,主要就是简化了语法,那它是不是语法,内部实现其实就是内部类?其实不是,Java会为每个匿名内部类生成一个类,但Lambda表达式不会。Lambda表达式通常比较短,为每个表达式生成一个类会生成大量的类,性能会受到影响。内部实现上,Java利用了Java 7引入的为支持动态类型语言引入的invokedynamic指令、方法句柄(method handle)等。
Lambda表达式不是匿名内部类,那它的类型到底是什么呢?是函数式接口。

函数式接口

Java 8引入了函数式接口的概念,函数式接口也是接口,但只能有一个抽象方法,前面提及的接口都只有一个抽象方法,都是函数式接口。之所以强调是“抽象”方法,是因为Java 8中还允许定义静态方法和默认方法。Lambda表达式可以赋值给函数式接口,比如:

FileFilter filter = path -> path.getName().endsWith(".txt");
FilenameFilter fileNameFilter = (dir, name) -> name.endsWith(".txt");
Comparator<File> comparator = (f1, f2) ->
                    f1.getName().compareTo(f2.getName());
Runnable task = () -> System.out.println("hello world");

如果看这些接口的定义,会发现它们都有一个注解@FunctionalInterface,比如:

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

@FunctionalInterface用于清晰地告知使用者这是一个函数式接口,不过,这个注解不是必需的,不加,只要只有一个抽象方法,也是函数式接口。但如果加了,而又定义了超过一个抽象方法,Java编译器会报错,这类似于我们之前介绍的Override注解。

预定义的函数式接口

Java 8定义了大量的预定义函数式接口,用于常见类型的代码传递,这些函数定义在包java.util.function下:
在这里插入图片描述
对于基本类型boolean、int、long和double,为避免装箱/拆箱,Java 8提供了一些专门的函数,比如,int相关的部分函数:
在这里插入图片描述
这些函数有什么用呢?它们被大量用于Java 8的函数式数据处理Stream相关的类中,即使不使用Stream,也可以在自己的代码中直接使用这些预定义的函数。我们看一些简单的示例,包括Predicate、Function和Consumer。

1. Predicate示例

为便于举例,我们先定义一个简单的学生类Student,它有name和score两个属性,如下所示

static class Student {
    String name;
    double score;
}

我们省略了构造方法和getter/setter方法。
有一个学生列表:

List<Student> students = Arrays.asList(new Student[] {
        new Student("zhangsan", 89d), new Student("lisi", 89d),
        new Student("wangwu", 98d) });

在日常开发中,列表处理的一个常见需求是过滤,列表的类型经常不一样,过滤的条件也经常变化,但主体逻辑都是类似的,可以借助Predicate写一个通用的方法,如下所示:

public static <E> List<E> filter(List<E> list, Predicate<E> pred) {
    List<E> retList = new ArrayList<>();
    for(E e : list) {
        if(pred.test(e)) {
            retList.add(e);
        }
    }
    return retList;
}

这个方法可以这么用:

//过滤90分以上的
students = filter(students, t -> t.getScore() > 90);

2. Function示例

列表处理的另一个常见需求是转换。比如,给定一个学生列表,需要返回名称列表,或者将名称转换为大写返回,可以借助Function写一个通用的方法,如下所示:

public static <T, R> List<R> map(List<T> list, Function<T, R> mapper) {
    List<R> retList = new ArrayList<>(list.size());
    for(T e : list) {
        retList.add(mapper.apply(e));
    }
    return retList;
}

根据学生列表返回名称列表的代码为:

List<String> names = map(students, t -> t.getName());

将学生名称转换为大写的代码为:

students = map(students, t -> new Student(
    t.getName().toUpperCase(), t.getScore()));

3. Consumer示例

在上面转换学生名称为大写的例子中,我们为每个学生创建了一个新的对象,另一种常见的情况是直接修改原对象,通过代码传递,这时,可以用Consumer写一个通用的方法,比如:

public static <E> void foreach(List<E> list, Consumer<E> consumer) {
    for(E e : list) {
        consumer.accept(e);
    }
}

上面转换为大写的例子可以改为:

foreach(students, t -> t.setName(t.getName().toUpperCase()));

以上这些示例主要用于演示函数接口的基本概念,实际中可以直接使用流API。

方法引用

Lambda表达式经常用于调用对象的某个方法,比如:

List<String> names = map(students, t -> t.getName());

这时,它可以进一步简化,如下所示:

List<String> names = map(students, Student::getName);

Student::getName这种写法是Java 8引入的一种新语法,称为方法引用。它是Lambda表达式的一种简写方法,由::分隔为两部分,前面是类名或变量名,后面是方法名。方法可以是实例方法,也可以是静态方法,但含义不同。
我们看一些例子,还是以Student为例,先增加一个静态方法:

public static String getCollegeName(){
    return "Laoma School";
}

对于静态方法,如下两条语句是等价的:

1. Supplier<String> s = Student::getCollegeName;
2. Supplier<String> s = () -> Student.getCollegeName();

它们的参数都是空,返回类型为String。
而对于实例方法,它的第一个参数就是该类型的实例,比如,如下两条语句是等价的:

1. Function<Student, String> f = Student::getName;
2. Function<Student, String> f = (Student t) -> t.getName();

对于Student::setName,它是一个BiConsumer,即如下两条语句是等价的:

1. BiConsumer<Student, String> c = Student::setName;
2. BiConsumer<Student, String> c = (t, name) -> t.setName(name);

如果方法引用的第一部分是变量名,则相当于调用那个对象的方法。比如,假定t是一个Student类型的变量,则如下两条语句是等价的:

1. Supplier<String> s = t::getName;
2. Supplier<String> s = () -> t.getName();

下面两条语句也是等价的:

1. Consumer<String> consumer = t::setName;
2. Consumer<String> consumer = (name) -> t.setName(name);

对于构造方法,方法引用的语法是<类名>::new,如Student::new,即下面两条语句等价:

1. BiFunction<String, Double, Student> s = (name, score) -> new Student(name, score);
2. BiFunction<String, Double, Student> s = Student::new;

函数的复合

在前面的例子中,函数式接口都用作方法的参数,其他部分通过Lambda表达式传递具体代码给它。函数式接口和Lambda表达式还可用作方法的返回值,传递代码回调用者,将这两种用法结合起来,可以构造符合的函数,是程序简洁易读。
下面我们看一些例子,这些例子利用了Java 8对接口的增强,即静态方法和默认方法,并利用它们实现复合函数,包括Comparator接口和function包。

1. Comparator中的复合方法

Comparator接口定义了如下静态方法:

public static <T, U extends Comparable<? super U>> Comparator<T> comparing(
        Function<? super T, ? extends U> keyExtractor) {
    Objects.requireNonNull(keyExtractor);
    return (Comparator<T> & Serializable)
        (c1, c2) -> keyExtractor.apply(c1).compareTo(keyExtractor.apply(c2));
}

这个方法是什么意思?它用于构建一个Comparator,比如,在前面的例子中,对文件按照文件名排序的代码为:

Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName()));

使用comparing方法,代码可以简化为:

Arrays.sort(files, Comparator.comparing(File::getName));

这样,代码的可读性大大增强。comparing方法为什么能达到这个效果呢?它构建并返回了一个符合Comparator接口的Lambda表达式,这个Comparator接受的参数类型是File,它使用了传递过来的函数代码keyExtractor将File转换为String进行比较。像comparing这样使用复合方式构建并传递代码并不容易阅读,但调用者很方便,也很容易理解。
Comparator还有很多默认方法,我们看两个:

default Comparator<T> reversed() {
    return Collections.reverseOrder(this);
}
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);
    };
}

reversed返回一个新的Comparator,按原排序逆序排。thenComparing也返回一个新的Comparator,在原排序认为两个元素排序相同的时候,使用传递的Comparator other进行比较。
看一个使用的例子,将学生列表按照分数倒序排(高分在前),分数一样的按照名字进行排序:

students.sort(Comparator.comparing(Student::getScore)
                          .reversed()
                          .thenComparing(Student::getName));

2.function包中的复合方法

在java.util.function包的很多函数式接口里,都定义了一些复合方法,我们看一些例子。
Function接口有如下定义:

default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) {
    Objects.requireNonNull(after);
    return (T t) -> after.apply(apply(t));
}

现将T类型的参数转化为类型R,再调用after将R转换为V,最后返回类型V。
还有如下定义:

default <V> Function<V, R> compose(
        Function<? super V, ? extends T> before) {
    Objects.requireNonNull(before);
    return (V v) -> apply(before.apply(v));
}

对V类型的参数,先调用before将V转换为T类型,再调用当前的apply方法转换为R类型返回。
Consumer、Predicate等都有一些复合方法,他们大量用于函数式数据处理API中。

Java实例练习

加密解密的始祖——凯撒密码

凯撒密码作为一种最为古老的对称加密体制,在古罗马的时候都已经很流行,它的基本思想是:通过把字母移动一定的位数来实现加密和解密。例如,如果密钥是把明文字母的位数向后移动三位,那么明文字母B就变成了密文的E,依次类推,X将变成A,Y变成B,Z变成C。本实例我们就来举一个和凯撒密码相关的例子

1.

新建项目CaesarPassword,并在其中创建一个CaesarPassword.java文件。在该类的主方法中使用Encryption()方法将字符串中每个字符取出并进行移位加密,当需要解密时使用Decrypt()方法对其进行反方向移位操作。核心代码如下所示:

package CaesarPassword;

import java.util.Scanner;
public class CaesarPassword {
    public static void main(String args[]) throws Exception {
        System.out.println("[A 加密][J 解密],Please Choose One");
        Scanner c = new Scanner(System.in);    // 创建Scanner对象
        String s1 = c.nextLine();              // 获取本行的字符串
        if (s1.equalsIgnoreCase("A")) {        // 判断变量s1与A是否相等,忽略大小
            System.out.println("请输入明文:");
            Scanner sc = new Scanner(System.in);
            String s = sc.nextLine();
            System.out.println("请输入密钥:");
            Scanner sc1 = new Scanner(System.in);
            int key = sc1.nextInt();           // 将下一个输入项转换成int类型
            Encryption(s, key);                // 调用Encryption()方法
        } else if (s1.equalsIgnoreCase("J")) {
            System.out.println("请输入密文:");
            Scanner sc = new Scanner(System.in);
            String s = sc.nextLine();
            System.out.println("请输入密钥:");
            Scanner sc1 = new Scanner(System.in);
            int key = sc1.nextInt();
            Decrypt(s, key);                   // 调用Encryption()方法
        }
    }
    public static void Encryption(String str, int k) {      // 加密
        String string = "";
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c >= 'a' && c <= 'z')          // 如果字符串中的某个字符是小写字母
            {
                c += k % 26;                  // 移动key%26位
                if (c < 'a')
                c += 26;                  // 向左超界
                if (c > 'z')
                c -= 26;                  // 向右超界
            } else if (c >= 'A' && c <= 'Z')   // 如果字符串中的某个字符是大写字母
            {
                c += k % 26;
                if (c < 'A')
                c += 26;
                if (c > 'Z')
                c -= 26;
            }
            string += c;                       // 将加密后的字符连成字符串
        }
        System.out.println(str + " 加密后为:" + string);
    }
    public static void Decrypt(String str, int n) {         // 解密
        int k = Integer.parseInt("-" + n);
        String string = "";
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (c >= 'a' && c <= 'z')          // 如果字符串中的某个字符是小写字母
            {
                c += k % 26;                  // 移动key%26位
                if (c < 'a')
                c += 26;                  // 向左超界
                if (c > 'z')
                c -= 26;                  // 向右超界
            } else if (c >= 'A' && c <= 'Z')   // 如果字符串中的某个字符是大写字母
            {
                c += k % 26;
                if (c < 'A')
                c += 26;
                if (c > 'Z')
                c -= 26;
            }
            string += c;                       // 将解密后的字符连成字符串
        }
        System.out.println(str + " 解密后为:" + string);
    }
}

运行后,结果如下:

[A 加密][J 解密],Please Choose One
A
请输入明文:
ffzs
请输入密钥:
4
ffzs 加密后为:jjdw

猜你喜欢

转载自blog.csdn.net/tonydz0523/article/details/87858100