Lambda表达式只是一颗语法糖?

JDK在不断升级过程中,要致力解决的问题之一就是让程序代码变得更加简洁。JDK8引入的Lambda表达式在简化程序代码方面大显身手,它用简明扼要的语法来表达某种功能包含的操作。在程序遍历访问集合中元素的场合,运用Lambda表达式可以大大简化操纵集合的程序代码。

Lambda表达式的基本用法

我们先看一个程序示例

public class SimpleTest {
    public static void main(String[] args) {
        String [] data={"Tom","Mike","Mary","Linda","Jack"};
        List<String> names=new ArrayList<>(data);

        //方式一:传统的遍历集合的方式
        for (String name : names) {
            System.out.println(name);
        }

        //方式二:使用Lambda表达式
        names.forEach((name)-> System.out.println(name));

        //方式三:使用Lambda表达式
        names.forEach(System.out::print);
    }
}

比较3种遍历集合的代码,不难发现,使用Lambda表达式可以简化程序代码。Lambda表达式的基本语法为:

(Type param1, Type param2,..., TypeN paramN)-> {
	statment1;
	statment2;
	//...
	return statmentM;
}

从Lambda表达式的基本语法我们可以看出,Lambda表达式可以理解为一段带有输入参数的可执行语句块,这种语法表达方式也可称为函数式表达。
上面示例的SimpleTest类中的方式二的Lambda表达式的完整语法应该是

(String name)->{
	System.out.println(name);
	return;
}

Lambda表达式还有各种简化版:
(1)参数类型可以省略。在绝大多数情况下,编译器都可以从上下文环境中聪明地推断出Lambda表达式的参数类型,例如,对于以上Lambda表达式,编译器能推断出name变量的类型为String,因此Lambda表达式可以简化为:

(name)->{
	System.out.println(name);
	return ;
}

(2)当Lambda表达式的参数个数只有一个时,可以省略小括号。以上Lambda表达式可以简化为:

name->{
	System.out.println(name);
	return;
}

(3)当Lambda表达式只包含一条语句时,可以省略大括号、语句结尾的分号。此外,当return语句没有返回值时也可以省略。以上Lambda表达式可以简化为:

name-> System.out.println(name);

(4)Lambda表达式中符号“->”后面也可以仅包含一个普通的表达式,语法为:

(Type1 param1,Type2 param2,...,TypeN paramN)->(expression)

例如:

(int a,int b)->(a*b+2)

示例SimpleTest类中的方式三还通过符号“::”来直接调用println()方法

name.forEach(System.out::println);

用Lambda代替内部类

Lambda表达式的一个重要用武之地是代替内部类。例如下面的示例的InnerTester类中,用3种方式创建了线程。其中方式二和方式三使用了Lambda表达式。

public class InnerTester {
    public static void main(String[] args) {
        //方式一:使用匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello World");
            }
        }).start();

        //方式二:使用Lambda表达式
        new Thread(()-> System.out.println("Hello World")).start();

        //方式三:使用Lambda表达式
        Runnable race=()-> System.out.println("Hello World");
        new Thread(race).start();
    }
}

方式二和方式三的Lambda表达式相当于创建了实现Runnable接口的匿名对象,由于Runnable接口的run()方法不带参数,因此,Lambda表达式的参数列表也相应为空“()”,Lambda表达式中符号“->”后面的可执行语句块相当于run()方法的方法体。

Lambda表达式和集合的forEach()方法

从JDK1.5开始,Java集合都实现了java.util.Iterable接口,它的forEach()方法能够遍历集合中的每个元素。forEach()方法的完整定义如下:

default void forEach(Consumer<? super T> action)

forEach()方法有一个Consumer接口类型的action参数,它包含了对集合中每个元素的具体操作行为。action参数所引用的Consumer实例必须实现Consumer接口的accept(T t)方法,在该方法中指定对参数t所执行的具体操作。
例如以下forEach()方法中Lambda表达式相当于Consumer类型的匿名对象,它指定对每个元素的操作为打印这个元素:

names.forEach((name)->System.out.println(name));

假定有一个Person类具有name属性和age属性,并且提供了相应的getName()、getAge()、setName()和setAge()方法,参见示例

public class Person {
    private String name;
    private int age;
    
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

下面的EachTester类创建了一个存放Person对象的List列表,并且在遍历这个列表时,指定了更加复杂的操作行为:先把每个Person对象的age属性加1,然后打印这个Person对象的信息。

public class EachTester {
    public static void main(String[] args) {
        List<Person> persons=new ArrayList<Person>(){
            {
                add(new Person("Tom",21));
                add(new Person("Mike",32));
                add(new Person("Linda",19));
            }
        };
        persons.forEach((Person p)->{//Lambda表达式相当于是consumer类型的匿名对象
            //指定对每个元素的具体操作
            p.setAge(p.getAge()+1);
            System.out.println(p.getName()+":"+p.getAge());
        });
    }
}

以上Lambda表达式相当于创建了一个Consumer类型的匿名对象,并实现了Consumer接口的accept(T t)方法,此处传给accept(T t)方法的参数为Person对象。在Lambda表达式中符号“->”后的可执行语句块相当于accept(T t)方法的方法体。

用Lambda表达式对集合进行排序

下面的示例SortTester类提供了3种为集合排序的方式。其中方式二和方式三都采用了Lambda表达式。

public class SortTester {
    public static void main(String[] args) {
        String[] data={"Tom","Mike","Mary","Linda","Jack"};
        List<String> names= Arrays.asList(data);
        
        //方式一:通过创建匿名的Comparator实例来排序
        Comparator<String> cp=new Comparator<String>(){
          @Override
          public int compare(String s1, String s2){
              return (s1.compareTo(s2));
          }  
        };
        Collections.sort(names,cp);
        
        //方式二:用Lambda表达式来排序
        Comparator<String> sortByName=(String s1,String s2)->(s1.compareTo(s2));
        Collections.sort(names,sortByName);
        
        //方式三:用Lambda表达式来排序
        Collections.sort(names,(String s1,String s2)->(s1.compareTo(s2)));
        
        names.forEach(System.out::println);
    }
}

以上方式二和方式三的Lambda表达式相当于创建了Comparator类型的匿名对象。由于Comparator接口的compare(T o1,T o2)方法有两个参数,所以Lambda表达式也有两个相应的参数(String s1,String s2),Lambda表达式中符号“->”后面的表达式“s1.compareTo(s2)”相当于compare(T o1,T o2)的方法体。

Lambda表达式和Stream API联合使用

Java的输入和输出中ByteArrayInputStream(字节输入流)采用了适配器模式,它为字节数组提供了流接口,使得程序可以按照操作流的方式来访问字节数组。从JDK8开始,专门抽象出了java.util.stream.Stream流接口,它可以充当Java集合的适配器,使得程序能够按照操作流的方式来访问集合中的元素。
Stream 接口提供了一组功能强大的操纵集合的方法:

  • filter(Predicate<?super T> predicate): 对集合中的元素进行过滤,返回包含符合条件的元素的流
  • forEach(Consumer<? super T> action): 遍历集合中的元素
  • limit(long maxSize) : 返回参数maxSize所指定个数的元素
  • max(Comparator<? super T> comparator): 根据参数指定的比较规则,返回集合中最大的元素
  • min(Comparator<? super T> cmparator): 根据参数指定的比较规则,返回集合中最小的元素
  • sorted(): 对集合中的元素自然排序
  • sorted(Comparator<? super T> comparator): 根据参数指定的比较规则,对集合中的元素排序
  • mapToInt(ToIntFunction<? super T> mapper): 把当前的流映射为int类型的流,返回一个IntStream对象。ToIntFunction接口类型的参数指定映射方式。ToIntFunction接口有一个返回值为int类型的applyAsInt(T value)方法,该方法把参数value映射为int类型的方式
  • mapToLong(ToLongFunction<? super T> mapper): 把当前的流映射为long类型的流,返回一个LongStream对象。ToLongFunction接口类型的参数指定映射方式。ToLongFunction接口有一个返回值为long类型的applyAsLong(T value)方法,该方法把参数value映射为long类型的方式
  • toArray(): 返回包含集合中所有元素的对象数组
    Collection接口的stream()方法返回一个Stream对象,程序可以通过这个Stream对象操纵集合中的元素。
    下面示例的ColTester类先创建了包含Person对象的ArrayList列表,接着调用它的stream()方法得到一个流,接着再对流中元素进行过滤和排序等操作。
public class ColTester {
    public static void main(String[] args) {
        List<Person> persons=new ArrayList<Person>(){
            { //匿名类初始化代码
                add(new Person("Tom", 21));
                add(new Person("Mike", 32));
                add(new Person("Linda", 19));
                add(new Person("Mary", 29));
            }
        };

        persons.stream()
                .filter(p -> p.getAge()>20) //过滤条件为年龄大于20
                .forEach(p -> System.out.println(p.getName()+":"+p.getAge()));

        persons.stream()
                .sorted((p1,p2)->(p1.getAge()-p2.getAge()))//按照年龄排序
                .limit(3)    //取出3个元素
                .forEach(p -> System.out.println(p.getName()+":"+p.getAge()));

        int maxAge=persons.parallelStream() //获得并行流
                            //把包含Person对象的流映射为保存其age属性的int类型流
                            .mapToInt(p -> p.getAge())
                            .max()
                            .getAsInt();
        System.out.println("Max Age:"+maxAge);

    }
}

在ColTester类的main()方法中,persons.stream()方法返回一个Stream对象,接下来调用Stream对象的filter()、sorted()和forEach()方法时,传入的都是Lambda表达式。
Stream接口的filter()方法的完整声明为:

Stream<T> filter(Predicate<? super T> predicate)

以上filter()方法有一个Predicate类型参数,用来指明过滤数据的条件。Predicate接口的test(T t)方法判断参数t是否符合过滤条件,如果符合就返回true,否则返回false。以上程序代码“filter(p->p.getAge()>20)”通过Lamda表达式创建了一个Predicate匿名对象,把它传给filter()方法。Lambda表达式中符合“->”后面的表达式“p.getAge()>20”相当于是test(T t)方法的方法体。
Collection接口的parallelStream()方法返回一个采用并行处理机制的Stream对象。当集合中有大批量数据时,为了提高处理集合中元素的效率,可以调用此方法来得到Stream对象,它的内部实现会开启多个线程来并发处理数据。
在ColTester类的main()方法中,还调用persons.parallelStream()方法得到一个并行流,接下来再调用流的mapToInt()方法,把当前存放Person对象的流映射为存放其age属性的int类型的流(即IntStream类型对象),然后再调用IntStream流的max()方法返回最大值,该返回值是空指针安全的OptionalInt类型的对象,再调用OptionalInt对象的getAsInt()方法得到OptionalInt对象所包装的int基本类型的值。

Lambda表达式可操纵的变量作用域

Lambda表达式可以访问它所属的外部类的变量,包括外部类的实例变量、静态变量和局部变量。此外,Lambda表达式还可以引用this关键字,this关键字实际上引用的是外部类的实例。下面示例ScopeTester演示了Lambda表达式对各种变量及this关键字的引用

public class ScopeTester {
    int var1=0; //实例变量
    public void test(){
        String [] data={"Tom","Mike","Mary"};
        List<String> names= Arrays.asList(data);

        char var2=',';
        //以下这行代码编译出错,不允许改变var2最终变量的值
        //var2='';

        //使用lambda表达式
        names.forEach((name)->{
            var1++;   //访问并修改实例变量var1
            //通过this访问实例变量var1,访问局部变量var2
            System.out.println(this.var1+":"+name+var2);
        });
    }

    public static void main(String[] args) {
        new ScopeTester().test();
    }
}

在以上示例中,var1变量时ScopeTester类的实例变量,var2变量时test()方法的局部变量,在Lambda表达式中可以直接访问这两个变量,还可以通过“this.var1”的方式来访问var1实例变量。
值得注意的是,在Lambda表达式中访问的局部变量必须符合以下两个条件之一:

  • 条件一: 最终局部变量,即用final修饰的局部变量
  • 条件二: 实际上最终局部变量,即虽然没有被final修饰,但在程序中不会改变局部变量的值
    例如以上ScopeTester类中的“var2=‘ ’;”语句试图修改var2局部变量的值,这会导致编译错误。因为Lambda表达式会访问var2局部变量,所以编译器不允许修改var2变量的值。

Lambda表达式中的方法引用

下面的两种Lambda表达式是等价的:

names.forEach((name)->System.out.println(name));
或者:
names.forEach(System.out::println);

在编译器能根据上下文来推断Lambda表达式的参数的场合,可以在Lambda表达式中省略参数,直接通过“::”符合来引用方法。方法引用的语法格式有以下3种:

第一种方式:objectName::instanceMethod   //引用实例方法
第二种方式:ClassName::staticMethod      //引用静态方法
第三种方式:ClassName::instanceMethod    //引用实例方法

下面举例说明:

x->System.out.println(x)    等同于   System.out::println   //引用实例方法
(x,y)->Math.max(x,y)        等同于   Math::max             //引用静态方法
x->x.toLowerCase()          等同于   String::toLowerCase   //引用实例方法   

对于第三种方式,为什么可以通过类名来访问实例方法呢?这是因为聪明的编译器会根据上下文来推断到底引用哪个对象的实例方法。
在Lambda表达式中,对构造方法的应用的语法如下:

ClassName::new

例如,以下两种Lambda表达式等价:

x->new BigDecimal(x) 等同于: BigDecimal::new

函数式接口(FunctionalInterface)

在JDK8中定义了一个Annotation类型的函数式接口FunctionalInterface:

public @interface FunctionalInterface

Lambda表达式只能赋值给声明为函数式接口的Java类型的变量。上面示例中的Consumer、Runnable、Comparator、Predicate接口都标注为函数式接口,因此可以接受Lambda表达式,例如,一下代码把Lambda表达式赋值给一个Runnable类型的变量,这是合法的:

Runnable race=()->System.out.println("Hello World");

String类没有标注为函数式接口。以下代码试图把Lambda表达式赋值给一个String类型的变量,这是非法的,会导致编译错误:

String str=()->{return "hello".toUpperCase();};

只要查阅Java API文档,就能了解一个java类是否被标注为函数式接口。例如,在Java API文档中,Runnable接口的声明如下,由此可以看出它被标注为函数式接口:

@FunctionalInterface
public interface Runnable

Java语法糖

语法糖(syntactic sugar)也称为糖衣语法,指在计算机语言中添加的某种语法,这种语法虽然没有显著地增加语言的功能,但是能方便程序员编程,提高程序的可读性,简化程序,减少程序出错的机会。
Java语言在不断发展的过程中,也陆陆续续地加入了一些语法糖。Lambda表达式就是一颗典型的语法糖。除了Lambda表达式,Java语言中常用的语法糖还包括以下几种:
(1)泛型与类型擦除: Java语言中的泛型只在Java源代码中存在,在编译后的字节码中,会被替换成原生类型,并且在相应的地方自动加入强制类型转换代码。对于运行中的Java程序来说,ArrayList<Integer> 和ArrayList<String>是同一个类。这种在编译时去除泛型的过程称为类型擦除。所以说泛型技术实际上是Java语言的一颗语法糖。
(2)自动装箱和拆箱:自动装箱和拆箱实现了基本数据类型与包装数据类型之间的隐式转换。
(3)foreach循环语句:foreach语句从JDK5开始映入的,可以简化遍历数组和集合的代码。
(4)方法的数目可变参数:可变参数可以简化对方法的定义和调用
(5)枚举类型:有助于更方便地访问类型相同的一组常量
(6)断言语句:方便对程序的调试
(7)switch表达式支持枚举类型和字符串
(8)自动释放try语句中打开的资源

总结

JDK8引入的Lambda表达式,它本质只是一颗让编程人员更加得心应手的“语法糖”,它只存在于Java源代码中,由编译器把它转换为常规的Java类代码。Lambda表达式优点类似于方法,由参数列表和一个使用这些参数的主体(可以是一个表达式或者一个代码块)组成。
Lambda表达式于Stream API 联合使用,可以方便地操纵集合,完成对集合中元素的过滤和排序等操作。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章