Java新特性
Java新特性
Lambda表达式是Java8中新增的特性,lambda表达式是一个可传递的代码块,可以在以后执行一次或多次。在以前定义的方法中,只能将基本类型或者引用类型的变量作为方法参数,在Java 8以后可以将一个代码片段作为方法参数。
Lambda表达式入门
在集合中Java为开发者提供了遍历集合的简洁方式,如下例所示:
1 | package cn.bytecollege; |
在上面的示例中,调用了list对象的foreach方法,从程序可以看出,传入foreach的并不是一个变量,而是一段代码,这就是Lambda表达式。从上面的语法可以看出,Lambda表达式的主要作用就是代替匿名内部类的烦琐语法。
Lambda由3部分组成:
- 形参列表:形参列表允许省略形参的数据类型,如果形参列表中有且只有1个参数,可以省略形参列表的括号
- 箭头函数:->必须有横线和大于号组成
- 代码块。如果代码块只包含一条语句,Lambda表达式允许省略代码块的花括号。
下面,通过示例来学习Lambda的写法:
1 | //表达式只有1个参数 |
函数式接口
Lambda表达式的目标类型必须是函数式接口,所谓函数式接口代表只包含一个抽象方法的接口,函数式接口可以包含多个默认方法、类方法,但是只能声明一个抽象方法。
如果采用匿名内部类语法来创建函数式接口的实例,则只需要实现一个抽象方法,在这种情况下可采用Lambda表达式来创建对象。
注意:Java8 专门为函数式接口提供了@FunctionalInterface注解,该注解通常放在接口定义前,该注解对程序功能没有任何作用,它的作用是用于告诉编译器执行更严格的检查,检查该接口必须是函数式接口,否则编译器出错。
Lambda表达式的结果就是被作为对象,程序中晚期可以使用Lambda表达式进行赋值,例如在多线程Thread类的构造器中可以传入Runnable接口的子类对象。查看Runnable接口发现,该接口也被声明为一个函数式接口:
1 |
|
所以,就可以使用Lambda表达式来创建线程:
1 | package cn.bytecollege; |
Lambda 表达式实现的是匿名方法——因此它只能实现特定函数式接口中的唯一方法。这意味着 Lambda 表达式有如下两个限制。
- Lambda 表达式的目标类型必须是明确的函数式接口。
- Lambda 表达式只能为函数式接口创建对象。Lambda 表达式只能实现一个方法,因此它只能为 只有一个抽象方法的接口(函数式接口)创建对象。
下面定义一个函数式接口深入学习Lambda表达式
1 | package cn.bytecollege; |
定义一个方法,方法参数是Consumer接口:
1 | package cn.bytecollege; |
在上例中定义了一个函数式接口,在测试类的test方法传入了接口并调用了Consumer接口的add方法,需要注意的是,此时add方法并没有方法实现,在main方法中调用了test,并将一段代码(即add方法的实现)也就是lambda表达式当做参数传入了test方法。换句话说在上例中使用了lambda表达替代了烦琐的匿名内部类。对比下面的代码就可以看出Lambda表达式的独到之处。
1 | package cn.bytecollege; |
从前面的程序可以看出Lambda表达式的使用离不开函数式接口,通常函数式接口中有且只能有1个抽象方法,这样使用Lambda表达式时也就明确了是哪个抽象方法的实现,如果接口中出现了多个抽象方法,那么就不能在接口上使用@FunctionInterface注解,会编译出错。因此,Java 8在java.util.function包中预定义了大量函数式接口,通常情况下这些接口完全可以满足开发需要:
- XxxFunction∶ 这类接口中通常包含一个 apply()抽象方法,该方法对参数进行处理、转换(apply() 方法的处理逻辑由 Lambda 表达式来实现),然后返回一个新的值。该函数式接口通常用于对指定数据进行转换处理。
- XxxConsumer∶ 这类接口中通常包含一个 accept()抽象方法,该方法与 XxxFunction 接口中的 apply()方法基本相似,也负责对参数进行处理,只是该方法不会返回处理结果。
- XxxxPredicate∶这类接口中通常包含一个 test()抽象方法,该方法通常用来对参数进行某种判断 test()方法的判断逻辑由 Lambda 表达式来实现),然后返回一个 boolean 值。该接口通常用于判断参数是否满足特定条件,经常用于进行筛滤数据。
- XxxSupplier∶ 这类接口中通常包含一个 getAsXxx()抽象方法,该方法不需要输入参数,该方法会按某种逻辑算法(getAsXxx()方法的逻辑算法由 Lambda 表达式来实现)返回一个数据。综上所述,不难发现 Lambda 表达式的本质很简单,就是使用简洁的语法来创建函数式接口的实例——这种语法避免了匿名内部类的烦琐。
下面在程序中示范上述接口的使用:
1 | package cn.bytecollege.lambda; |
在上面的CastUtil类中定义了castToString,在该方法中第一个参数是一个Java 8 预定义的函数式接口,在方法内调用了Function接口的apply()方法,作用是将任意类型转换成String。但是此时这个方法并没有方法的实现,需要在调用此方法时传入方法的实现。
1 | package cn.bytecollege.lambda; |
在测试类中,调用了CastUtil的castToString()方法,并传入了Lambda表达式,以此Lambda表达式作为apply()方法的实现,在表达式中使用了String.valueOf()方法将对象转换成String类型。
方法引用与构造器引用
前面已经介绍过,如果Lambda 表达式的代码块只有一条代码,程序就可以省略 Lambda 表达式中代码块的花括号。不仅如此,如果Lambda 表达式的代码块只有一条代码,还可以在代码块中使用方法引用和构造器引用。
方法引用和构造器引用可以让 Lambda表达式的代码块更加简洁。方法引用和构造器引用都需要使用两个英文冒号。
Lambda 表达式支持如下表所示的几种引用方式。
引用类方法
下面的示例将演示类方法的引用,首先定义一个函数式接口,接口中定义抽象方法castToString(),该方法的作用是将一个对象转换成String对象。
1 | package cn.bytecollege.lambda; |
在String的学习中可以知道,String类有提供了类方法valueOf(Object o),该方法可以将任意对象转换成String类型,因此可以使用该方法作为Lambda表达式的实现代码:
1 | package cn.bytecollege.lambda; |
在上面的代码中,创建了Lambda表达式作为了Function接口中castToString()方法的实现。在Lambda表达式中调用了String.valueOf()方法来进行对象到字符串的转换,在代码第8行调用了function接口的castToString()方法,实际上调用了就是代码第5行创建的Lambda表达式。
上面的Lambda表达式的代码块只有一行调用类方法的代码,因此可以使用如下方法引用进行替换。代码如下:
1 | package cn.bytecollege.lambda; |
对于上面的类方法的引用,也就是调用了String类的valueOf()方法来实现Function函数式接口中唯一抽象方法。当调用castToString()方法时,调用参数将会传给String类的valueOf()类方法。
引用对象的实例方法
下面演示第二种方法引用,引用对象的实例方法,首先使用Lambda表达式创建一个Function接口的子类对象:
1 | Function<Object, String> function = o->o.toString(); |
上面的Lambda表达式只有一条语句,因此省略了该代码的花括号。
接下来程序调用function对象的castToString()方法:
1 | package cn.bytecollege.lambda; |
上面的程序调用了function对象的castToString()方法时,由于function对象是Lambda表达式创建,castToString()方法的执行体就是Lambda表达式的代码部分,因此上面的程序输出了100.
上面的Lambda表达式代码只有一行,且调用了对象的o的toString()实例方法。因此代码可以进行如下替换:
1 | package cn.bytecollege.lambda; |
上面的Lambda表达式的代码只有一条语句,因此省略了代码块的花括号;而且由于表达式实现的castToString方法需要返回值,因此Lambda表达会将这行代码的值作为返回值。此时就可以使用方法引用进行替换,直接引用Object的toString()方法作为Lambda表达式的代码块。其中Function接口的castToString方法有个参数,当执行Lambda表达式代码块时,会自动调用传入参数的toString()方法。
引用构造器
下面的实例将演示如何引用构造器,首先定义函数式接口:
1 | package cn.bytecollege.lambda; |
该函数式接口包含了一个get()抽象方法,该方法的作用是使用String对象生成一个StringBuilder对象,接着使用Lambda表达式创建一个MyInterface的对象:
1 | package cn.bytecollege.lambda; |
上面的代码调用了myInterface对象的get()方法时,由于该对象是Lambda表达式创建的,因此get()方法执行体就是Lambda表达式的代码块部分,即执行体就是执行new StringBuilder(a)语句,并将这条语句的值作为方法的返回值。因此上面代码中Lambda表达式的代码可以进行如下替换:
1 | package cn.bytecollege.lambda; |
对于上面的构造器引用,也就是调用StringBuilder类的构造方法来实现MyInteface函数式接口中唯一的抽象方法,当调用MyInterface接口的test()方法是,调用参数会传给StringBuilder构造器,从上面的程序中可以看出,调用myInterface对象的get()方法时,实际只传入了一个String类型的参数,这个String类型的参数会被传给StringBuilder的构造器。
Lambda表达式和匿名内部类的联系和区别
从前面介绍可以看出,Lambda 表达式是匿名内部类的一种简化,因此它可以部分取代匿名内部类的作用,Lambda 表达式与匿名内部类存在如下相同点。
- Lambda 表达式与匿名内部类一样,都可以直接访问”effectively final”的局部变量,以及外部 类的成员变量(包括实例变量和类变量)。
- Lambda 表达式创建的对象与匿名内部类生成的对象一样,都可以直接调用从接口中继承的默认 方法。
首先创建函数式接口:
1 | package cn.bytecollege.ano; |
1 | package cn.bytecollege.ano; |
创建测试类:
1 | package cn.bytecollege.ano; |
上面的程序使用Lambda表达式创建了一个Display接口的对象,Lambda表达式分别访问了外部类的实例变量,类变量从这些来看Lambda表达式的代码块和匿名内部类的方法体是相同的。
和匿名内部类相似,由于Lambda表达式访问了了book局部变量,因此该局部变量相当于有一个隐式的final修饰,不允许对book局部变量重新赋值。
当程序使用 Lambda 表达式创建了 Display 的对象之后,该对象不仅可调用接口中唯一的抽象方法,也可调用接口中的默认方法。
Lambda表达式与匿名内部类主要存在如下区别:
- 匿名内部类可以为任意接口创建实例——不管接口包含多少个抽象方法,只要匿名内部类实现 所有的抽象方法即可;但 Lambda 表达式只能为函数式接口创建实例。
- 匿名内部类可以为抽象类甚至普通类创建实例;但 Lambda 表达式只能为函数式接口创建实例。
- 匿名内部类实现的抽象方法的方法体允许调用接口中定义的默认方法;但 Lambda 表达式的代 码块不允许调用接口中定义的默认方法。
Stream
Java8 还新增了Stream、IntStream、LongStream、DoubleStream等流式API(注意:这里的Stream并不是IO中的Stream),这些API代表多个支持串行和并行聚集操作的元素。上面的4个接口中,Stream是一个通用的流接口,而IntStream、LongStream、DoubleStream则代表元素类型为int、long、double的流。
Java 8 还为上面每个流式 API 提供了对应的 Builder,例如 Stream.Builder、IntStream.Builder、 LongStream.Builder、DoubleStream.Builder,开发者可以通过这些 Builder 来创建对应的流
独立使用 Stream 的步骤如下∶
- 使用 Stream 或 XxxStream 的 builder()类方法创建该 Stream 对应的 Builder。
- 重复调用 Builder 的 add()方法向该流中添加多个元素。
- 调用 Builder 的 build()方法获取对应的 Stream。
- 调用 Stream 的聚集方法。
Stream提供了大量的方法进行聚集操作,这些方法可以是中间的,也可以是末端的。
- 中间方法∶中间操作允许流保持打开状态,并允许直接调用后续方法。也就是说中间方法可以连续调用。
- 末端方法; 末端方法是对流的最终操作。当对某个 Stream 执行末端方法后,该流将会被”消耗” 且不再可用。换句话说就是末端方法一旦调用后就会关闭流,再不能对流进行操作,否则会抛出异常。
下面先介绍Stream常用的中间方法:
- filter(Predicate predicate)∶ 过滤 Stream 中所有不符合 predicate 的元素。
- mapToXxx(ToXxxFunction mapper)∶使用 ToXxxFunction 对流中的元素执行一对一的转换,该方 法返回的新流中包含了ToXxxFunction 转换生成的所有元素。
- peek(Consumer action)∶ 依次对每个元素执行一些操作,该方法返回的流与原有流包含相同的元 素。该方法主要用于调试。
- distinct()∶该方法用于排序流中所有重复的元素(判断元素重复的标准是使用 equals()比较返回 true)。这是一个有状态的方法。
- sorted()∶该方法用于保证流中的元素在后续的访问中处于有序状态。这是一个有状态的方法。
- limit(long maxSize)∶ 该方法用于保证对该流的后续访问中最大允许访问的元素个数。这是一个 有状态的、短路方法。
下面简单介绍一下 Stream 常用的末端方法。
- forEach(Consumer action)∶ 遍历流中所有元素,对每个元素执行 action。
- toArray()∶将流中所有元素转换为一个数组。
- reduce()∶该方法有三个重载的版本,都用于通过某种操作来合并流中的元素。
- min()∶ 返回流中所有元素的最小值。
- max()∶ 返回流中所有元素的最大值。
- count()∶ 返回流中所有元素的数量。
- anyMatch(Predicate predicate):判断流中是否至少包含一个元素符合 Predicate 条件。
- allMatch(Predicate predicate):判断流中是否每个元素都符合 Predicate 条件。
- noneMatch(Predicate predicate)∶判断流中是否所有元素都不符合 Predicate 条件。
- findFirst()∶ 返回流中的第一个元素。
- findAny()∶返回流中的任意一个元素。
下面,通过示例来学习Stream的使用:
1 | package cn.bytecollege.stream; |
除此之外,Java8允许使用流式API来操作集合,这也是Stream的重要使用场景之一,Collection接口提供了stream()默认方法,该方法可以返回该集合对应的流,下面使用Stream来操作集合。
1 | package cn.bytecollege.stream; |
在上面的实例中首先获取了List的Stream,在代码20行对stream进行了过滤,筛选出了包含Java的字符串,然后调用了count()统计了过滤后的字符串个数。因为count()是末端方法,因此调用后再不能进行后续操作。代码22行是用limit取出了前3个元素,并对其进行了遍历。
需要注意的是,Stream对集合的操作并不影响List中保存的数据。