Java8新特性.docx
Java8新特性Java8新特性 一 lambda Java8中最重要的特性之一就是引入了lambda表达式。这能够使你的代码更加简练,并允许你将行为传递到各处。一段时间以来,Java因为自身的冗长和缺少函数式编程的能力而受到批评。随着函数式编程变得越来越流行和有价值,Java也在努力接受函数式编程。否则,Java将会变得没有价值。 Java8在使世界上最受欢迎的编程语言之一在接纳函数式编程的过程中向前迈了一大步。为了支持函数式编程,编程语言必须将函数作为第一类对象。在Java8之前,如果没有使用一个匿名内部类模板是没法写出清晰的函数式代码的。随着lambda表达式的引入,函数已经成为第一类对象,并能够像其它变量一样被到处传递。 lambda表达式允许你定义一个不与标识符绑定的匿名函数。你可以像编程语言中的其它概念一样使用它们,比如变量的声明。当一个编程语言需要支持高阶函数时,就需要用到lambda表达式。高阶函数是指以其它函数作为参数或者返回函数作为结果的函数。 这一节的代码在ch02包中 现在,随着在Java8中引进了lambda表达式,Java已经支持高阶函数。让我来看一个lambda表达式的典型例子Collections类中的sort方法。sort方法有两种变体一种以一个List作为参数,另一个以List和Comparator作为参数。如下面的代码块所示,第二种sort方法是一个接受lambda表达式的高阶函数的例子。 List<String> names = Arrays.asList("shekhar", "rahul", "sameer"); Collections.sort(names, (first, second) -> first.length - second.length); 1 2 上面的代码将姓名链表按照元素的长度进行排序。该程序的输出如下所示。 rahul, sameer, shekhar 1 上面代码块中的表达式(first, second) -> first.length - second.length是一个Comparator<String>类型的lambda表达式。 (first, second)是比较器Comparator的compare方法。 first.length - second.length 是用来比较两个名字长度的方法实体。 ->是lambda操作符,用来将参数和方法体分离开。 在我们继续深挖Java8的lambda表达式之前,让我们来看看lambda的历史来理解为什么会存在lambda。 lambda的历史 lambda表达式源自演算。演算由Alonzo Church在将带有函数的符号计算进行公式化时提出。演算是具有图灵完备性的,它通过数学形式来展现计算过程。图灵完备性表示你可以通过lambda表达任何的数学计算。 演算成为了函数式编程语言的一个坚实的理论基础。很多有名的函数式编程语言,像Haskell和Lisp都是构建在演算的基础上的。高阶函数的概念,比如接受其他函数为输入的函数也来自演算。 演算的核心概念是表达式。一个lambda表达式可以表示为如下形式: <expression> := <variable> | <function>| <application> 1 variable变量就是类似x,y,z的占位符,它们用来表示具体的像1,2之类的值,或者lambda方法。 functrion这是一个匿名的方法定义,它需要一个变量,并产生另一个lambda表达式。例如,x.x*x是一个用来计算数的平方的方法。 application这是将具体的参数应用在函数上的行为。假设你想得到10的平方,那么在演算中你会写一个平方函数x.x*x,并把10代入。这个函数应用将得到(x.x*x) 10 = 10*10 = 100。你不仅仅能够代入简单的像10一样的值,你可以将一个函数代入另一个函数来得到一个新的函数。例如,(x.x*x) (z.z+10)将会生成一个函数z.(z+10)*(z+10)。现在,你可以用这个函数得到一个数加上10以后的平方。这是一个高阶函数的例子。 现在你理解了演算和它在函数式编程语言中的影响。让我们来学习它是如何在Java8中实现的。 在Java8之前传递行为的方式 在Java8之前,唯一能够用来传递行为的方式是通过匿名类。假设你想要在用户完成注册的同时在另一个线程中给该用户发送一封邮件。在Java8之前,你会写出类似下面的代码。 sendEmail(new Runnable Override public void run System.out.println("Sending email."); ); sendEmail方法拥有如下的方法签名。 public static void sendEmail(Runnable runnable) 1 上面提到的代码的问题不仅仅是我们需要封装我们的行为,如将run方法直接放在一个对象中,更严重的问题是它丢失了程序员的意图,如将行为传递到sendEmail方法中。如果你使用过Guava类库,你肯定感受到了编写匿名类的痛苦。一个简单的用来过滤所有任务的标题中有lambda的例子如下所示。 Iterable<Task> lambdaTasks = Iterables.filter(tasks, new Predicate<Task> Override public boolean apply(Task task) return input.getTitle.contains("lambda"); ); 有了Java8的Stream API,你可以在不使用像Guava一样的第三方库的情况下写出上面提及的代码。我们将在第三章中讲解Stream,敬请期待。 Java8 lambda表达式 在Java8中,我们将使用lambda表达式写出如下的代码。这与我们上面提及过的代码段相同。 sendEmail( -> System.out.println("Sending email."); 1 上面的展示的代码非常简练,也没有污染程序员想要传递的行为。用来表示这个lambda表达式没有参数,像Runnable接口中的run方法就没有任何参数。->是将参数和用来打印出Sending email的方法主体分隔开的lambda操作符。 让我再来看看Collections.sort这个例子来了解lambda表达式是如何使用参数的。为了使名字能够按照它们的长度进行排列,我们向排序方法传入了一个Comparator。该Comparator如下所示。 Comparator<String> comparator = (first, second) -> first.length - second.length; 1 我们编写的lambda表达式与Comparator接口中的compare方法相关联。compare方法的签名如下。 int compare(T o1, T o2); 1 T是传给Comparator接口的类型参数。由于我们是对一组表示名字的字符串进行操作,所以这个例子中它将是字符串类型的。在lambda表达式中我们不需要特意提供该类型字符串。javac编译器会从上下文中推断出它的类型信息。由于我们在给一组字符串排序,Java编译器会推测出两个参数都应该是字符串,而compare方法只标明需要T这一种类型。像这样通过上下文推断类型的行为称作类型推断。Java8优化了Java原有的类型推断机制,使得它更具有鲁棒性,并能够更好地支持lambda表达式。javac会在后台寻找与你lambda表达式相关的信息,并使用该信息来找到参数正确的类型。 在大多数情况下,javac会从上下文中推断出类型。如果由于上下文缺失或不完整导致代码不能进行编译,它也就不能推断出类型。例如如果我们将String的类型信息从Comparator中移除,那么代码会像下面一样编译失败。 Comparator comparator = (first, second) -> first.length - second.length; / compilation error - Cannot resolve method 'length' 1 lambda表达式是如何在Java8中工作的? 你也许已经发现lambda表达式是与上面例子中的Comparator类似的一些接口。你不能对任意的接口使用lambda表达式。只有那些除了Object的方法外只定义了唯一抽象方法的接口可以使用lambda表达式。这一类的接口被称作函数式接口,它们可以通过FunctionalInterface注解来进行注解。如下所示,Runnable接口就是一个函数式接口。 FunctionalInterface public interface Runnable public abstract void run; FunctionalInterface注解不是强制需要的,它能够帮助其他工具知道这个接口是一个函数式接口,以此展现出有意义的行为。如果你试图编译一个有FunctionalInterface的接口,而该接口有多个抽象方法,那么编译器将会抛出一个发现多个没有重写的抽象方法的异常。同样的,如果你对一个没有任何方法的接口添加FunctionalInterface的注解,比如一个标记接口,那么你将会得到一条没有找到目标方法的的消息。 让我们来解答一个你也许会想到的最重要的问题。Java8中的lambda表达式是仅仅针对匿名类的语法糖吗,或者说函数式接口是如何转换为字节码的?简单的答案是不是。Java8不使用匿名内部类主要有两个原因: 性能开销:如果lambda表达式是通过使用匿名类来实现的,那么每一个lambda表达式都要在磁盘上产生一个文件。如果这些类在JVM启动时被加载,那么JVM的启动时间将会增加,因为所有的类在使用前都要进行加载和验证。 未来改变的可能性:如果Java8的设计者从开始就使用了匿名类,那么这将限制lambda表达式的实现方式在将来的变化。 使用invokedynamic Java8设计者决定使用在Java7中添加的invokedynamic指令来在运行时推迟编译策略的执行。当javac编译代码的时候,它会捕捉到lambda表达式并生成一个invokedynamic的调用。当invokedynamic命令被调用时,它会返回一个lambda要转化的函数式接口的实例。例如,我来查看Collections.sort的字节码,它如下所示。 public static void main(java.lang.String); Code: 0: iconst_3 1: anewarray #2 / class java/lang/String 4: dup 5: iconst_0 6: ldc #3 / String shekhar 8: aastore 9: dup 10: iconst_1 11: ldc #4 / String rahul 13: aastore 14: dup 15: iconst_2 16: ldc #5 / String sameer 18: aastore 19: invokestatic #6 / Method java/util/Arrays.asList:(Ljava/lang/Object;)Ljava/util/List; 22: astore_1 23: invokedynamic #7, 0 / InvokeDynamic #0:compare:Ljava/util/Comparator; 28: astore_2 29: aload_1 30: aload_2 31: invokestatic #8 / Method java/util/Collections.sort:(Ljava/util/List;Ljava/util/Comparator;)V 34: getstatic #9 / Field java/lang/System.out:Ljava/io/PrintStream; 37: aload_1 38: invokevirtual #10 / Method java/io/PrintStream.println:(Ljava/lang/Object;)V 41: return 该字节码有意思的地方在第23行23: invokedynamic #7, 0 / InvokeDynamic #0:compare:Ljava/util/Comparator;,也就是生成一个invokedynamic的地方。 第二步是将lambda表达式的主体部分转化成通过invokedynamic指令调用的方法。这一步让JVM实现者能够自由地选取他们自己的策略。我省略了这个话题相关的内容,你可以在 匿名类 vs lambda 让我们通过比较匿名类和lambda表达式来比较它们的不同。 在匿名类中,this表示匿名类自己,而在lambda表达式中,this表示包含了lambda表达式的类。 你可以在匿名类这个封闭类中隐藏变量。在lambda表达式中这么做时将产生一个编译错误。 lambda表达式的类型是由上下文决定的,而匿名类的类型是由你创建匿名类时指定的。 我需要自己编写函数式接口吗? Java8默认提供了好多函数式编程接口来供你在代码中使用。它们在java.util.function包中。让我们看一下其中的一部分。 java.util.function.Predicate 这个函数式接口被用来定义某些情形的检查,类似于断言。Predicate接口有一个叫做test的方法,它以泛型T为参数,返回一个布尔值。举例来说,如果我们想从一串名字中找到所有以s开头的名字,那么我们将向下面这样使用Predicate。 Predicate<String> namesStartingWithS = name -> name.startsWith("s"); 1 java.util.function.Consumer 这个函数式接口被用来执行一些不用产生输出的动作。Comsumer接口有一个以泛型T为参数且没有返回值的accept方法。比如将一条给定的信息通过邮件发出。 Consumer<String> messageConsumer = message -> System.out.println(message); 1 java.util.function.Function Function<String, String> toUpperCase = name -> name.toUpperCase; 1 java.util.function.Supplier 这个函数式接口不需要任何参数,却会产生一个值。这可以被用来像下面这样生成唯一标志码。 Supplier<String> uuidGenerator= -> UUID.randomUUID.toString; 1 我们将在这一系列教程中涉及更多的函数式接口。 方法引用 有时候你会创建一些只调用特定方法的lambda表达式,比如Function<String, Integer> strToLength = str -> str.length;。这个lambda只在String对象上调用length方法。这种情况可以通过使用方法引用来简化成Function<String, Integer> strToLength = String:length;。这可以被看做是只调用单个方法的lambda表达式的简化标记。在该表达式String:length中,String是目标引用,:是分隔符,length是在目标引用中将会被调用的方法。你在静态方法和实例方法中都可以使用方法引用。 静态方法引用 假设我们要找到一串数中最大的一个,那么我们可以写一个像Function<List<Integer>, Integer> maxFn = Collections:max这样的方法引用。max是Collections类中一个以list为参数的静态方法。然后你可以像maxFn.apply(Arrays.asList(1, 10, 3, 5)这样来调用。上面的lambda表达式是与Function<List<Integer>, Integer> maxFn = (numbers) -> Collections.max(numbers);等价的。 实例方法引用 这是一类为实例方法使用的方法引用,比如在String:toUpperCase在String引用上调用了toUpperCase方法。你也可以对有参数的方法使用方法引用,像BiFunction<String, String, String> concatFn = String:concat。concatFn可以像concatFn.apply("shekhar", "gulati")这样被调用。concat方法是字符串对象的需要一个参数的方法,形式为"shekhar".concat("gulati")。 练习>>写自己的lambda 让我们看一下下面的代码,并把我们学的应用起来。 public class Exercise_Lambdas public static void main(String args) List<Task> tasks = getTasks; List<String> titles = taskTitles(tasks); for (String title : titles) System.out.println(title); public static List<String> taskTitles(List<Task> tasks) List<String> readingTitles = new ArrayList<> for (Task task : tasks) if (task.getType = TaskType.READING) readingTitles.add(task.getTitle); return readingTitles; 上面的代码首先从一个工具方法getTasks中获取所有的任务。我们对getTasks方法的内部实现不感兴趣。getTasks方法可以从web、数据库或者内存中来获取任务。一旦你有了任务,我们过滤出所有的阅读任务并抽取出这些任务的标题。我们将抽取的标题存入一个链表并最终返回所有的阅读标题。 让我们从最简单的重构开始通过方法引用在链表上使用foreach方法。 public class Exercise_Lambdas public static void main(String args) List<Task> tasks = getTasks; List<String> titles = taskTitles(tasks); titles.forEach(System.out:println); public static List<String> taskTitles(List<Task> tasks) List<String> readingTitles = new ArrayList<> for (Task task : tasks) if (task.getType = TaskType.READING) readingTitles.add(task.getTitle); return readingTitles; 用Predicate<T>来过滤我们的任务。 public class Exercise_Lambdas public static void main(String args) List<Task> tasks = getTasks; List<String> titles = taskTitles(tasks, task -> task.getType = TaskType.READING); titles.forEach(System.out:println); public static List<String> taskTitles(List<Task> tasks, Predicate<Task> filterTasks) List<String> readingTitles = new ArrayList<> for (Task task : tasks) if (filterTasks.test(task) readingTitles.add(task.getTitle); return readingTitles; 用Function<T,R>来从我们的任务中抽取标题。 public class Exercise_Lambdas public static void main(String args) List<Task> tasks = getTasks; List<String> titles = taskTitles(tasks, task -> task.getType = TaskType.READING, task -> task.getTitle); titles.forEach(System.out:println); public static <R> List<R> taskTitles(List<Task> tasks, Predicate<Task> filterTasks, Function<Task, R> extractor) List<R> readingTitles = new ArrayList<> for (Task task : tasks) if (filterTasks.test(task) readingTitles.add(extractor.apply(task); return readingTitles; 对提取器使用方法引用。 public static void main(String args) List<Task> tasks = getTasks; List<String> titles = filterAndExtract(tasks, task -> task.getType = TaskType.READING, Task:getTitle); titles.forEach(System.out:println); List<LocalDate> createdOnDates = filterAndExtract(tasks, task -> task.getType = TaskType.READING, Task:getCreatedOn); createdOnDates.forEach(System.out:println); List<Task> filteredTasks = filterAndExtract(tasks, task -> task.getType = TaskType.READING, Function.identity); filteredTasks.forEach(System.out:println); 我们也可以通过编写我们自己的函数式接口,这样可以清楚地描述开发者的意图。我们可以创建一个继承于Function接口的TaskExtractor接口。该接口的输入类型被限定为Task,输出类型由lambda的实现决定。这样由于输入类型始终是Task,开发者只需要关注返回值的类型, public class Exercise_Lambdas public static void main(String args) List<Task> tasks = getTasks; List<Task> filteredTasks = filterAndExtract(tasks, task -> task.getType = TaskType.READING, TaskExtractor.identityOp); filteredTasks.forEach(System.out:println); public static <R> List<R> filterAndExtract(List<Task> tasks, Predicate<Task> filterTasks, TaskExtractor<R> extractor) List<R> readingTitles = new ArrayList<> for (Task task : tasks) if (filterTasks.test(task) readingTitles.add(extractor.apply(task); return readingTitles; interface TaskExtractor<R> extends Function<Task, R> static TaskExtractor<Task> identityOp return t -> t; 二 Stream API 我们通过学习lambda表达式,了解了如何能够在不创建额外类的情况下传递行为来帮助我们编写出简洁精练的代码。lambda表达式是一种通过使用函数式接口让开发者能够快速表达他们的想法的语言概念。设计API的时候将lambda,也就是那些使用了函数式接口的流畅的API记在脑子中,我们才能真正体验到lambda的强大,。 在Java8中引进的Stream API是使用lambda的API之一。就像SQL如何帮助你在数据库中形象地查询数据,Stream在Java集合计算上提供了一个形象的声明式的高层抽象来表示计算。形象的意思是指开发者只要写他们想写的,而不是关注他们该如何来写。在这一章中,我们将讨论对一个新的数据处理API的需求、Collection和Stream的区别,和如何在你的应用中使用Stream API。 这一节的代码在ch03包中 为什么我们需要一个新的数据处理抽象 在我的观点中,主要有两个原因: Collection API没有提供高层的概念来查询数据,所以开发者被迫为琐碎的工作编写很多重复的代码。 对Collection的并行操作在语言支持方面受到了限制。只能让开发者用Java的并发机制来让数据并行处理快速而有效。 在Java8之前的数据处理 看下面的代码并试图说出它的作用。 public class Example1_Java7 public static void main(String args) List<Task> tasks = getTasks; List<Task> readingTasks = new ArrayList<> for (Task task : tasks) if (task.getType = TaskType.READING) readingTasks.add(task); Collections.sort(readingTasks, new Comparator<Task> Override public int compare(Task t1, Task t2) return t1.getTitle.length - t2.getTitle.length; ); for (Task readingTask : readingTasks) System.out.println(readingTask.getTitle); 上面的代码将阅读任务按照它们标题的长度进行排序后输出。Java7的开发者成天要写这类的代码。为了写一个这样简单的程序,我们编写了15行代码。上面提到的代码的最大问题不是开发者要编写的代码的数量,而是它丢失了开发者的意图,也就是过滤阅读任务,根据标题长度排序,和转换成字符串列表。 Java8中的数据处理 如下所示,上述的代码可以通过Java8的Stream API简化。 public class Example1_Stream public static void main(String args) List<Task> tasks = getTasks; List<String> readingTasks = tasks.stream .filter(task -> task.getType = TaskType.READING) .sorted(t1, t2) -> t1.getTitle.length - t2.getTitle.length) .map(Task:getTitle) .collect(Collectors.toList); readingTasks.forEach(System.out:println); 上面的代码构建了一个由许多流式操作组成的管道流,下面对其一一讲解。 stream: 通过在一个原始的集合上调用stream方法来创建一个流式管道流,而tasks就是List<Task>类型的。 filter(Predicate): 这个操作从流中抽取符合断言的判定条件的元素。一旦你有了一个数据流,你可以在其上不调用或者多次调用中间操作。lambda表达式task -> task.getType = TaskType.READING定义了一个断言来过滤所有的阅读任务。该lambda表达式的类型为java.util.function.Predicate<Task>。 sorted(Comparator):这个操作返回一个根据由lambda表达式定义的比较器进行排序后的元素组成的数据流。在上面的例子中,这个比较器是(t1, t2) -> t1.getTitle.length - t2.getTitle.length 。 map(Function 为什么Java8编写的代码更好 我认为Java8的代码更好的理由如下: Java8的代码清晰地展现出开发者的意图,如过滤、排序等。 开发者通过Stream API的形式能够在一个高层的抽象上来表现出他们想要做什么,而不是他们如何来作。 Stream API为数据处理提供了一个统一的语言。现在当程序员讨论到数据处理时,它们将会有共同的词汇。当两个开发者谈论到filter方法时,你可以肯定他们都在使用一个数据过滤操作。 处理数据时不需要重复的代码。用户不需要写专门的for循环,也不用创建临时集合来存储数据。所有的工作都可以通过Stream API来完成。 Stream不会修改你原来的集合它们是免于变化的。 什么是Stream? Stream是一些数据上的