目录
一、Lambda表达式
Lambda作为函数式编程中的基础部分,在其他编程语言(例如:Scala)中早就广为使用,但在Java领域中发展较慢,直到java8才开始支持Lambda。
Lambda表达式格式:参数->表达式 或 (参数1,参数2,...)->{表达式1; 表达式2; ...},当参数为单个时,()可以省略;同样地当表达式为单个时,{}可以省略;参数可以写类型,也可以不写(不写的话,编译器编译时可自动推断参数的类型)。
Lambda表达式本质上是一个匿名方法,其底层还是通过invokedynamic指令生成匿名类来实现的,它提供了简单的语法和写作方式,通过Lambda表达式替代函数式接口,使代码变得更紧凑简洁,举几个例子感受下:
/** 创建线程 */
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Java8之前常用的创建线程方式");
}
});
Thread thread2 = new Thread(() -> System.out.println("使用Lambda表达式简化线程创建"));
/** 定义集合的比较器Comparator */
List<Integer> list = Arrays.asList(new Integer[]{15, 30, 25, 56, 66, 42, 19});
Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o1.compareTo(o2);
}
});
Collections.sort(list, ((o1, o2) -> o1.compareTo(o2)));
Lambda表达式的简洁优势从上面例子中体现的淋漓尽致,这对提升Java程序代码的优雅性是重要的一步。此外,Lambda表达式还给Java带来了闭包功能,换句话说就是Lambda表达中可以访问域外的局部变量(但要注意不能在lambda表达式内部修改定义在域外的局部变量!!),在此之前的内部类也是Java闭包功能的体现,关于函数闭包和回调有时间再专门整理分析吧。
Java8之前,我们进行接口编程的话,大致步骤如下:定义一个接口 -- 使用new方式创建接口对象 -- 重写接口方法 -- 通过接口实例调用接口方法,举个例子:
public class XxxTest {
/** 接口编程,实现两个int整数求和 */
public static void main(String[] args) {
Func func = new Func() {
@Override
public int add(int m, int n) {
return m + n;
}
};
System.out.println(func.add(5, 6));
}
}
// 自定义接口
interface Func{
int add(int a, int b);
}
上面的示例中,通过"new 接口名称(){} "的方式,本质上是一种匿名内部类的实现。当有了Lambda表达式之后,如何来简化接口编程呢?演示如下:(这里顺带测试了一下变量作用域的问题)
public class XxxTest {
/** Lambda表达式简化接口编程,实现两个int整数求和 */
public static void main(String[] args) {
int a; // test
// Lambda 表达式当中不允许声明一个与局部变量同名的参数或者局部变量
// Func f = (a, b) -> a + b; // 编译报错!!!
Func f = (c, b) -> c + b;
System.out.println(f.add(1, 3));
}
}
// 自定义接口
interface Func{
int add(int a, int b);
}
两行代码轻松搞定,简化接口编程 —— 这就是Lambda表达式的最直接应用了。而实际中Lambda表达式最重要的应用,当属Java集合通过stream流 + Lambda表达式操作集合元素了,这一点在后面的Stream API使用中能很好的体现出来。
最后值得一提的是,Lambda表达式还可以通过方法引用、构造器引用等方式继续简化!!!
二、方法引用、构造器引用
方法引用简化Lambda表达式的规则如下:
- objectName::instanceMethod 即 对象::实例方法
- ClassName::staticMethod 即 类::静态方法
- ClassName::instanceMethod 即 类::实例方法
构造器引用简化Lambda表达式的规则如下:
- ClassName::new 即 类::new
这里简化规则很清晰,不再举例说明了。
三、函数式接口
函数式接口(Functional Interface)指的是有且仅有一个抽象方法、可有多个非抽象方法的接口。上面示例定义的Func接口就是一个的函数式接口,接口里只定义了一个add()抽象方法,为了满足javadoc规范,最好在Func接口上使用@FunctionalInterface注解,来标记该接口是一个函数式接口。Java8之前,JDK有哪些函数式接口和抽象方法呢,在此盘点一下:
- java.lang.Runnable —— public abstract void run();
- java.util.concurrent.Callable —— V call() throws Exception;
- java.security.PrivilegedAction
- java.util.Comparator —— int compare(T o1, T o2);
- java.io.FileFilter —— boolean accept(File pathname);
- java.nio.file.PathMatcher
- java.lang.reflect.InvocationHandler —— public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
- java.beans.PropertyChangeListener
- java.awt.event.ActionListener
- javax.swing.event.ChangeListener
本文一开始举例用的就是Runnable接口和Comparator接口!!Java8时,在java.util.function包下新增了许多函数式接口,在此也来盘点下,其中最核心的函数式接口及抽象方法有:(其余的函数式接口可参考API文档)
- Consumer<T>:消费型接口 —— void accept(T t)
- Function<T,R>:函数型接口 —— R apply(T t)
- Predicate<T>:断定型接口 —— boolean test(T t)
- Supplier<T>:供应型接口 —— T get()
下面通过demo演示下如何使用这些函数式接口:
1、消费型接口:Consumer<T> void accept(T t)
属于有参数,无返回值:
@Test
void test() {
Consumer consumer = name -> System.out.println(name + ",欢迎访问我的博客!");
consumer.accept("weixiangxiang");
// 指定T类型
Consumer<String> consumer1 = (String name) -> System.out.println(name + ",欢迎访问我的CSDN博客!");
consumer1.accept("xxwei3");
}
2、供应型接口:Supplier<T> T get()
属于无参数,有返回值:
@Test
void test() {
Supplier supplier = ()-> 6;
System.out.println(supplier.get());
// 指定T类型
Supplier<Double> supplier1 = ()-> 3.14;
System.out.println(supplier1.get());
}
3、函数型接口:Function<T,R> R apply(T t)
属于有参数,有返回值:
@Test
void test() {
Function<Integer, Integer> function = (a) -> a * 2;
System.out.println(function.apply(5));
}
4、断定型接口:Predicate<T> boolean test(T t)
@Test
void test() {
List<Integer> list = Arrays.asList(0, 1, 2, 3, 4, 5, 6, 7, 8, 9);
eval(list, n -> n % 2 == 0); // 输出全部偶数
eval(list, n -> n >= 5); // 输出>=5的数字
}
public static void eval(List<Integer> list, Predicate<Integer> predicate) {
for (Integer n : list) {
if (predicate.test(n)) {
System.out.println(n + " ");
}
}
}
四、接口方法的默认实现
接口是一种面向抽象的编程技术,Java8之前的接口方法是不能有任何实现逻辑的,如果需要修改该接口的方法,则实现了该接口的类也要全部同步修改,这不仅违反了面向对象里的开闭原则(对扩展开放且对修改关闭),而且涉及到JDK众多实现类的修改也很头疼,因此Java8开始引入了接口方法的默认实现,可以很好的解决接口的修改与现有的实现不兼容的问题!!!
接口方法默认实现的定义格式有:(有默认方法 和 静态方法的默认实现)
- default 返回值 方法名(参数列表){ 实现逻辑;}
- 权限修饰符 static 返回值 方法名(参数列表){ 实现逻辑;}
以函数式接口Comparator为例,compare()方法是一个抽象方法,thenComparing()方法是默认方法且带有默认实现,comparing()方法是静态方法且带有默认实现,部分源码如下:
@FunctionalInterface
public interface Comparator<T> {
// 抽象方法
int compare(T o1, T o2);
// 静态方法(带有默认实现)
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));
}
// 默认方法(带有默认实现)
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);
};
}
}
正是因为接口引入了方法的默认实现,丰富了函数式接口的功能,使得实现类有更多的选择。值得注意的是,静态方法的调用使用 接口名.静态方法 即可(如Comparator.comparing(Integer::new))。
当一个类实现了多个函数式接口,且多个函数式接口存在同名的默认方法时,调用默认方法该如何区分呢,通过 接口名.super.默认方法 即可(Comparator.super.thenComparing(...);)。
小结
Java8是一个重大变化的版本,首次引入Lambda表达式替代函数式接口,极大的简化了Java程序代码,通过方法引用和构造器引用还可以继续简化Lambda表达式,这一切似乎开始改变着我们对Java代码臃肿而繁杂的印象。而函数式接口在Java8之前就已存在了,在使用时感觉很不友好和优雅,Java8之后又新增了许多重要的函数式接口,同时伴随着Lambda表达式和Stream流的到来,改变了Java代码的书写方式也提升着程序处理数据的性能。另外,接口编程中的接口方法通过引入默认实现,解决了修改接口与现有实现不兼容的问题,这也是一个令人欣喜地变化。
自从sun公司被Oracle收购后,Java迎来了诸多方面的改变,让我们感受最直接的是Java版本迭代周期明显的比以前缩短了,因此还有人调侃说Java8刚学完Java快到16了,比如Java8引入了Lambda表达式,Java10开始支持局部变量的类型推断(即var变量),在Java11中则对var变量进行了增强,支持在Lambda表达式参数声明时使用var变量!当今时代,科技日新月异,今天用的东西可能明天就是过时的了,唯有不断保持学习和进步才能让自己立于不败之地,而这一切的变化,都值得我们去学习和掌握,不积硅步无以至千里,点滴付出终将有所收获,共同进步吧 ~