小言_互联网的博客

Java8新特性-Lambda表达式

256人阅读  评论(0)

简介

Lambda表达式(也称闭包),是Java8中最受期待和欢迎的新特性之一。Lambda表达式本质是一个匿名函数,但是它并不是匿名类的语法糖,它让 Java 开始走向函数式编程,其实现原理区别于一般的匿名类中的匿名函数。在Java语法层面Lambda表达式允许函数作为一个方法的参数(函数作为参数传递到方法中),或者把代码看成数据。Lambda表达式可以简化函数式接口的使用。函数式接口就是一个只有一个抽象方法的普通接口,像这样的接口就可以使用Lambda表达式来简化代码的编写。

使用Lambda表达式的优缺点

引入Lambda表达式的初衷:如果一个接口只包含一个方法,那么匿名类的语法会变得十分笨拙和不清楚,产生大量的模板代码,归结一下就是:代码冗余是匿名类的最大弊端。在编程的时候,我们很多时候希望把功能作为参数传递到另一个方法,Lambda表达式就是为此而生。

优点

  • 使用Lambda表达式可以简化接口匿名内部类的代码,可以减少类文件的生成,同时引入了强大的类型推断和方法引用特性,简单的功能甚至可以一行代码解决,解放匿名类的束缚。

  • 把功能作为参数向下传递,为函数式编程提供了支持,让 Java 开始走向函数式编程。

缺点

使用Lambda表达式会减弱代码的可读性,而且Lambda表达式的使用局限性比较强,只能适用于接口只有一个抽象方法时使用,不方便调试。

基本概念

函数式接口

  • 有且只有一个抽象方法的接口被为函数式接口
  • 只有函数式接口,才可以转换为lambda表达式
  • 接口默认方法必须予以实现,它们不是抽象方法
  • 函数式接口可以显式的被@FunctionalInterface所表示,当被标识的接口不满足规定时,编译器会提示报错

接口默认方法

接口默认方法的含义可以见Java官方教程中对应的章节,在文末的参考资料可以查看具体的链接:

Default methods enable you to add new functionality to the interfaces of your libraries and ensure binary compatibility with code written for older versions of those interfaces.

简单来说就是:默认方法允许你在你的类库中向接口添加新的功能,并确保新增的默认方法与这些接口的较早版本编写的代码二进制兼容

接口默认方法(下称默认方法)通过default关键字声明,可以直接在接口中编写方法体。也就是默认方法既声明了方法,也实现了方法。这一点很重要,在默认方法特性出现之前,Java编程语言规范中,接口的本质是抽象方法的集合,而自默认方法特性出现之后,接口的本质也改变了。默认方法的一个例子如下:

public interface DefaultMethod {
   

    default void defaultVoidMethod() {
   

    }

    default String sayHello(String name) {
   
        return String.format("%s say hello!", name);
    }

    static void main(String[] args) throws Exception {
   
        class Impl implements DefaultMethod {
   

        }
        DefaultMethod defaultMethod = new Impl();
        System.out.println(defaultMethod.sayHello("thinkwon"));
    }
}

如果继承一个定义了默认方法的接口,那么可以有如下的做法:

  • 完全忽略父接口的默认方法,那么相当于直接继承父接口的默认方法的实现(方法继承)。
  • 重新声明默认方法,这里特指去掉default关键字,用public abstract关键字重新声明对应的方法,相当于让默认方法转变为抽象方法,子类需要进行实现(方法抽象)。
  • 重新定义默认方法,也就是直接覆盖父接口中的实现(方法覆盖)。

结合前面一节提到的函数式接口,这里可以综合得出一个结论:函数式接口,也就是有且仅有一个抽象方法的接口,可以定义0个或者N(N >= 1)个默认方法

这一点正是Stream特性引入的理论基础。举个例子:

@FunctionalInterface
public interface CustomFunctionalInterface {
   

    public abstract void process();

    default void defaultVoidMethod() {
   

    }

    default String sayHello(String name) {
   
        return String.format("%s say hello!", name);
    }
}

这里说点题外话。

在写这篇文章的时候,笔者想起了一个前同事说过的话,大意如下:在软件工程中,如果从零做起,任何新功能的开发都是十分简单的,困难的是在兼容所有历史功能的前提下进行新功能的迭代。试想一下,Java迭代到今天已经过去十多年了,Hotspot VM源码工程已经十分庞大(手动编译过OpenJDK Hotspot VM源码的人都知道过程的痛苦),任何新增的特性都要向前兼容,否则很多用了历史版本的Java应用会无法升级新的JDK版本。既要二进制向前兼容,又要迭代出新的特性,Java需要进行舍夺,默认方法就是一个例子,必须舍去接口只能定义抽象方法这个延续了多年在Java开发者中根深蒂固的概念,夺取了基于默认方法实现构筑出来的流式编程体系。笔者有时候也在思考:如果要我去开发Stream这个新特性,我会怎么做或者我能怎么做?

嵌套类(Nested Classes)

嵌套类(Nested Classes),简单来说就是:在一个类中定义另一个类,那么在类内被定义的那个类就是嵌套类,最外层的类一般称为封闭类(Enclosing Class)。嵌套类主要分为两种:静态嵌套类和非静态嵌套类,而非静态嵌套类又称为内部类(Inner Classes

// 封闭类
class OuterClass {
   
    ...
    // 静态嵌套类
    static class StaticNestedClass {
   
        ...
    }
    
    // 内部类
    class InnerClass {
   
        ...
    }
}

静态嵌套类可以直接使用封闭的类名称去访问例如:OuterClass.StaticNestedClass x = new OuterClass.StaticNestedClass();,这种使用形式和一般类实例化基本没有区别。

内部类实例的存在必须依赖于封闭类实例的存在,并且内部类可以直接访问封闭类的任意属性和方法,简单来说就是内部类的实例化必须在封闭类实例化之后,并且依赖于封闭类的实例,声明的语法有点奇特:

public class OuterClass {
   

    int x = 1;

    static class StaticNestedClass {
   

    }

    class InnerClass {
   
        // 内部类可以访问封闭类的属性
        int y = x;
    }

    public static void main(String[] args) throws Exception {
   
        OuterClass outerClass = new OuterClass();

        // 必须这样实例化内部类 - 声明的语法相对奇特
        OuterClass.InnerClass innerClass = outerClass.new InnerClass();

        // 静态嵌套类可以一般实例化,形式为:封闭类.静态嵌套类
        OuterClass.StaticNestedClass staticNestedClass = new OuterClass.StaticNestedClass();

        // 如果main方法在封闭类内,可以直接使用静态嵌套类进行实例化
        StaticNestedClass x = new StaticNestedClass();
    }
}

内部类中有两种特殊的类型:本地类(Local Classes)和匿名类(Anonymous Classes)。

本地类是一种声明在任意块(block)的类,例如声明在代码块、静态代码块、实例方法或者静态方法中,它可以访问封闭类的所有成员属性和方法,它的作用域就是块内,不能在块外使用。例如:

public class OuterClass {
   

    static int y = 1;
    
    {
       
        // 本地类A
        class A{
   
            int z = y;
        }
        A a = new A();
    }

    static {
   
        // 本地类B
        class B{
   
            int z = y;
        }
        B b = new B();
    }

    private void method(){
   
        // 本地类C
        class C{
   
            int z = y;
        }
        C c = new C();
    }
}

匿名类可以让代码更加简明,允许使用者在定义类的同时予以实现,匿名类和其他内部类不同的地方是:它是一种表达式,而不是类声明。例如:

public class OuterClass {
   

    interface In {
   

        void method(String value);
    }
    
    public void sayHello(){
   
        // 本地类 - 类声明
        class LocalClass{
   
            
        }
        
        // 匿名类 - 是一个表达式
        In in = new In() {
   
            
            @Override
            public void method(String value) {
   
                
            }
        };
    }
}

嵌套类的类型关系图如下:

Nested Classes
  - Static Nested Classes
  - None Nested Classes
    - Local Classes
    - Anonymous Classes
    - Other Inner Classes

使用Lambda表达式的前提

只适用于函数式接口,即接口有且只有一个抽象方法!!!

基础语法

在认识Lambda表达式基础语法之前,先来看一段用两种方式创建线程的代码

// 创建线程
// 匿名类
new Thread(new Runnable() {
   
    @Override
    public void run() {
   
        System.out.println("Hello!");
    }
}).start();

// Lambda 表达式
new Thread(() -> System.out.println("Hello!")).start();

Lambda 表达式的基础语法:Java8中引入了一个新的操作符 “->” 该操作符称为箭头操作符或 Lambda 操作符

箭头操作符将 Lambda 表达式拆分成两部分:

左侧:Lambda 表达式的参数列表

右侧:Lambda 表达式中所需实现的功能, 即 Lambda 体

Lambda表达式的重要特征

  • 可选参数类型声明: 不需要声明参数类型,编译器可以统一识别参数值。

    也就说(s) -> System.out.println(s)和 (String s) -> System.out.println(s)是一样的编译器会进行类型推断,所以不需要添加参数类型。

  • 可选的参数圆括号: 一个参数无需定义圆括号,但多个参数需要定义圆括号。例如:

    1. s -> System.out.println(s) 一个参数不需要添加圆括号。
    2. (x, y) -> Integer.compare(y, x) 两个参数添加了圆括号,否则编译器报错。
  • 可选的Lambda体大括号:如果主体包含了一个语句,就不需要使用大括号。

    1. s -> System.out.println(s) , 不需要大括号.
    2. (s) -> { if (s.equals(“s”)){ System.out.println(s); } }; 需要大括号
  • 可选的返回关键字: 如果Lambda体不加{ }就不用写return,Lambda体加上{ }就需要添加return。

Lambda体不加{ }就不用写return:

 Comparator<Integer> com = (x, y) -> Integer.compare(y, x); 

Lambda体加上{ }就需要添加return:

Comparator<Integer> com = (x, y) -> {
   
  	int compare = Integer.compare(y, x);
  	return compare;
}; 

目标类型与类型推断

先引入下面的一个场景:

// org.thinkwon.Runnable
@FunctionalInterface
public interface Runnable {
   

    void run();

    static void main(String[] args) throws Exception {
   
        java.lang.Runnable langRunnable = () -> {
   };
        org.thinkwon.Runnable customRunnable = () -> {
   };
        langRunnable.run();
        customRunnable.run();
    }
}

定义了一个和java.lang.Runnable完全一致的函数式接口org.thinkwon.Runnable,上面main()方法中,可以看到两个接口对应的Lambda表达式的方法体实现也是完全一致,但是很明显最终可以使用不同类型的接口去接收返回值,也就是这两个Lambda的类型是不相同的。而这两个Lambda表达式返回值的类型是我们最终期待的返回值类型(expecting a data type of XX),那么Lambda表达式就是对应的被期待的类型,这个被期待的类型就是Lambda表达式的目标类型

为了确定Lambda表达式的目标类型,Java编译器会基于对应的Lambda表达式,使用上下文或者场景进行综合推导,判断的一个因素就是上下文中对该Lambda表达式所期待的类型。因此,只能在Java编译器能够正确推断Lambda表达式目标类型的场景下才能使用Lambda表达式,这些场景包括:

  • 变量声明。
  • 赋值。
  • 返回语句。
  • 数组初始化器。
  • Lambda表达式函数体。
  • 条件表达式(condition ? processIfTrue() : processIfFalse())。
  • 类型转换(Cast)表达式。

Lambda表达式除了目标类型,还包含参数列表和方法体,而方法体需要依赖于参数列表进行实现,所以方法参数也是决定目标类型的一个因素

方法参数的类型推导的过程主要依赖于两个语言特性:重载解析(Overload Resolution)和参数类型推导(Type Argument Inference)。

原文:For method arguments, the Java compiler determines the target type with two other language features: overload resolution and type argument inference

重载解析会为一个给定的方法调用(Method Invocation)寻找最合适的方法声明(Method Declaration)。由于不同的声明具有不同的签名,当Lambda表达式作为方法参数时,重载解析就会影响到Lambda表达式的目标类型。编译器会根据它对该Lambda表达式所提供的信息的理解做出决定。

如果Lambda表达式具有显式类型(参数类型被显式指定),编译器就可以直接使用Lambda表达式的返回类型;如果Lambda表达式具有隐式类型(参数类型被推导而知),重载解析则会忽略Lambda表达式函数体而只依赖Lambda表达式参数的数量。

举个例子:

// 显式类型
Function<String, String> functionX = (String x) -> x;
// 隐式类型
Function<String, Integer> functionY = x -> Integer.parseInt(x);

如果在依赖于方法参数的类型推导最佳方法声明时存在二义性(Ambiguous),我们则需要:

  • 使用显式Lambda表达式(为参数p提供显式类型)以提供额外的类型信息,ps.stream().map((Person p) -> p.getName());
  • Lambda表达式转型为Function<Person, String>ps.stream().map((Function<Person, String>) p -> p.getName());
  • 为泛型参数R提供一个实际类型。ps.stream().<String>map(p -> p.getName());

举个例子:

public static void main(String[] args) {
   
    List<Person> ps = new ArrayList<Person>();
    Stream<String> names = ps.stream().map(p -> p.getName());
}

private static class Person {
   

    private final String name;

    public Person(String name) {
   
        this.name = name;
    }

    public String getName() {
   
        return name;
    }
}

作用域

关于作用域的问题记住几点即可:

  • <1>Lambda表达式内的this引用和封闭类的this引用相同。
  • <2>Lambda表达式基于词法作用域,它不会从超类中继承任何变量,方法体里面的变量和它外部环境的变量具有相同的语义。
  • <3>Lambda expressions close over values, not variables,也就是Lambda表达式对值类型封闭,对变量(引用)类型开放(这一点正好解释了Lambda表达式内部引用外部的属性的时候,该属性必须定义为final)。

对于第<1>点举个例子:

public class LambdaThis {
   

    int x = 1;

    public void method() {
   
        Runnable runnable = () -> {
   
            int y = this.x;
            y++;
            System.out.println(y);
        };
        runnable.run();
    }

    public static void main(String[] args) {
   
        LambdaThis lambdaThis = new LambdaThis();
        // 2
        lambdaThis.method();
    }

}

对于第<2>点举个例子:

public class LambdaScope {
   
    
    public void method() {
   
        int x = 1;
        Runnable runnable = () -> {
   
            // 编译不通过 - Lambda方法体外部已经定义了同名变量
            int x = 2;
        };
        runnable.run();
    }
}

对于第<3>点举个例子:

public class LambdaValue {
   

    public void method() {
   
        (final) int x = 1;
        Runnable runnable = () -> {
   
            // 编译不通过 - 外部值类型使用了final
            x ++;
        };
        runnable.run();
    }
}

public class LambdaValue {
   

    public void method() {
   
        (final) IntHolder holder = new IntHolder();
        Runnable runnable = () -> {
   
            // 编译通过 - 使用了引用类型
            holder.x++;
        };
        runnable.run();
    }

    private static class IntHolder {
   
        int x = 1;
    }
}

方法引用

方法引用(Method Reference)是用来直接访问类或者实例已经存在的方法或者构造方法。方法引用提供了一种引用而不执行方法的方式,它需要由兼容的函数式接口构成的目标类型上下文。计算时,方法引用会创建函数式接口的一个实例。

当Lambda表达式中只是执行一个方法调用时,不用Lambda表达式,直接通过方法引用的形式可读性更高一些。

作用

  • 方法引用的唯一用途是支持Lambda的简写。

  • 方法引用提高了代码的可读性,也使逻辑更加清晰。

组成

  • 使用::操作符将方法名和对象或类的名字分隔开。::是域操作符(也可以称作定界符、分隔符)。

常见的方法引用

方法引用 等价的Lambda表达式
String::valueOf x -> String.valueOf(x)
Object::toString x -> x.toString()
x::toString () -> x.toString()
ArrayList::new () -> new ArrayList<>()

方法引用的类型归结如下:

类型 例子
静态方法引用 ClassName::methodName
指定对象实例方法引用 instanceRef::methodName
特定类型任意对象方法引用 ContainingType::methodName
超类方法引用 supper::methodName
构造器方法引用 ClassName::new
数组构造器方法引用 TypeName[]::new

可见其基本形式是:方法容器::方法名称或者关键字

举一些基本的使用例子:

静态方法引用

public class StaticMethodRef {
   

    public static void main(String[] args) {
   
        Function<String, Integer> function = StaticMethodRef::staticMethod;
        // 等同于
        // Function<String, Integer> function1 = (String s) -> StaticMethodRef.staticMethod(s);
        Integer result = function.apply("10086");
        // 10086
        System.out.println(result);
    }

    public static Integer staticMethod(String value) {
   
        return Integer.parseInt(value);
    }

}

指定对象实例方法引用

public class ParticularInstanceRef {
   

    public Integer refMethod(String value) {
   
        return Integer.parseInt(value);
    }

    public static void main(String[] args) {
   
        ParticularInstanceRef ref = new ParticularInstanceRef();
        Function<String, Integer> function = ref::refMethod;
        // 等同于
        // Function<String,Integer> function1 = (String s) -> ref.refMethod(s);
        Integer result = function.apply("10086");
        // 10086
        System.out.println(result);
    }

}

特定类型任意对象方法引用

String[] stringArray = {
   "C", "a", "B"};
Arrays.sort(stringArray, String::compareToIgnoreCase);
// 等同于
// Arrays.sort(stringArray, (String s1, String s2) -> s1.compareToIgnoreCase(s2));
// [a, B, C]
System.out.println(Arrays.toString(stringArray));

超类方法引用

public class SupperRef {
   

    public static void main(String[] args) throws Exception {
   
        Sub sub = new Sub();
        // 10086
        System.out.println(sub.refMethod("10086"));
    }

    private static class Supper {
   
        private Integer supperRefMethod(String value) {
   
            return Integer.parseInt(value);
        }
    }

    private static class Sub extends Supper {
   
        private Integer refMethod(String value) {
   
            Function<String, Integer> function = super::supperRefMethod;
            // 等同于
            // Function<String,Integer> function1 = (String s) -> super.supperRefMethod(s);
            return function.apply(value);
        }
    }

}

构造器方法引用

public class ConstructorRef {
   

    public static void main(String[] args) {
   
        Function<String, Person> function = Person::new;
        // 等同于
        // Function<String,Person> function1 = (String s) -> new Person(s);
        Person person = function.apply("thinkwon");
        // doge
        System.out.println(person.getName());
    }

    private static class Person {
   

        private final String name;

        public Person(String name) {
   
            this.name = name;
        }

        public String getName() {
   
            return name;
        }
    }

}

数组构造器方法引用

Function<Integer, Integer[]> function = Integer[]::new;
// 等同于
// Function<Integer, Integer[]> function1 = (Integer i) -> new Integer[i];
Integer[] array = function.apply(10);
// [null, null, null, null, null, null, null, null, null, null]
System.out.println(Arrays.toString(array));

Java8内置的函数式接口

Java 8 在 java.util.function 包下定义了很多标准函数式接口,主要分为以下几类:

接口 参数 返回值 类别
Consumer T void 消费型接口
Supplier None T 供给型接口
Function T R 函数型接口
Predicate T boolean 断言型接口

消费型接口

Consumer 接口只有一个抽象方法 accept,参数列表只有一个泛型t,无返回值,重点在于内部消费

public class ConsumerTest {
   

    public static void main(String[] args) {
   
        test("hello", x -> System.out.println(x));
    }

    public static <T> void test(T t, Consumer<T> consumer) {
   
        consumer.accept(t);
    }

}

如果需要多个参数列表的话,可以考虑使用 ObjLongConsumer

供给型接口

Supplier 只有一个抽象方法 get,参数列表为空,有返回值,返回值得数据类型为T。

public class SupplerTest {
   

    public static List<Integer> supply(Integer num, Supplier<Integer> supplier) {
   
        List<Integer> list = new ArrayList<Integer>();
        for (int x = 0; x < num; x++) {
   
            list.add(supplier.get());
        }
        return list;
    }

    public static void main(String[] args) {
   
        List<Integer> list = supply(10, () -> (int) (Math.random() * 100));
        list.forEach(System.out::println);
    }

}

如果需要返回得数据为基本数据类型,可以考虑使用 LongSupplier

函数型接口

Function<T, R> 只有一个抽象方法名为 apply,参数列表只有一个参数为T,有返回值,返回值的数据类型为R。

public class FunctionTest {
   

    public static void main(String[] args) {
   
        String test = test("hello", x -> x.toUpperCase());
        System.out.println(test);
    }

    public static String test(String str, Function<String, String> function) {
   
        return function.apply(str);
    }

}

如果需要多个入参,然后又返回值的话,可以考虑 BiFunction

断言型接口

断言型又名判断型。 Predicate 只有一个抽象方法 test,参数列表只有一个参数为 T,有返回值,返回值类型为 boolean。

public class PredicateTest {
   

    public static List<String> filter(List<String> fruit, Predicate<String> predicate) {
   
        List<String> f = new ArrayList<>();
        for (String s : fruit) {
   
            if (predicate.test(s)) {
   
                f.add(s);
            }
        }
        return f;
    }

    public static void main(String[] args) {
   
        List<String> fruit = Arrays.asList("香蕉", "哈密瓜", "榴莲", "火龙果", "水蜜桃");
        List<String> newFruit = filter(fruit, (f) -> f.length() == 2);
        System.out.println(newFruit);
    }

}

转载:https://blog.csdn.net/ThinkWon/article/details/113764085
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场