Java学习day042 lambda表达式(构造器引用、变量作用域、处理lambda表达式、再谈Comparator)

使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。

day042   lambda表达式(构造器引用、变量作用域、处理lambda表达式、再谈Comparator)


1.构造器引用

构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:

ArrayList<String>names=...;
Stream<Person>stream=names.stream().map(Person::new);
List<Person>people=stream.collect(Col1ectors.toList());

map方法会为各个列表元素调用Person(String)构造器。如果有多个Person构造器,编译器会选择有一个String参数的构造器,因为它从上下文推导出这是在对一个字符串调用构造器。

可以用数组类型建立构造器引用。例如,int[]::new是一个构造器引用,它有一个参数:即数组的长度。这等价于lambda表达式x->new int[x]。

Java有一个限制,无法构造泛型类型T的数组。数组构造器引用对于克服这个限制很有用。表达式new T[n]会产生错误,因为这会改为new Object[n]。对于开发类库的人来说,这是一个问题。例如,假设我们需要一个Person对象数组。Stream接口有一个toArray方法可以返回Object数组:

Object[] people=stream.toArray();

不过,这并不让人满意。用户希望得到一个Person引用数组,而不是Object引用数组。流库利用构造器引用解决了这个问题。可以把Person[]::newtoArray方法:

Person[] people=stream.toArray(Person[]::new);

toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。


2.变量作用域

通常,你可能希望能够在lambda表达式中访问外围方法或类中的变量。考虑下面这个例子:

public static void repeatMessage(String text,int delay)
{
    ActionListener listener = event ->
    {
        System.out.println(text);
        Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay,listener).start0;
}

来看这样一个调用:

repeatMessage("Hello",1000);//Prints Hello every 1,000 milliseconds

现在来看lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的。实际上,这是repeatMessage方法的一个参数变量。

如果再想想看,这里好像会有问题,尽管不那么明显。lambda表达式的代码可能会在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

要了解到底会发生什么,下面来巩固我们对lambda表达式的理解。lambda表达式有3个部分:

1)一个代码块;

2)参数;

3)自由变量的值,这是指非参数而且不在代码中定义的变量。

在我们的例子中,这个lambda表达式有1个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获(下面来看具体的实现细节。例如,可以把一个lambda表达式转换为包含一个方法的对象,这样自由变量的值就会复制到这个对象的实例变量中。)

关于代码块以及自由变量值有一个术语:闭包(closure)。如果有人吹嘘他们的语言有闭包,现在你也可以自信地说Java也有闭包。在Java中,lambda表达式就是闭包。

可以看到,lambda表达式可以捕获外围作用域中变量的值。在Java中,要确保所捕获的值是明确定义的,这里有一个重要的限制。在lambda表达式中,只能引用值不会改变的变量。例如,下面的做法是不合法的:

public static void countDown(int start,int delay)
{
    ActionListener listener=event->
        {
            start--;//Error:Can't mutate captured variable 
            System.out.println(start);
        };
    new Timer(delay,listener).start();
}

之所以有这个限制是有原因的。如果在lambda表达式中改变变量,并发执行多个动作时就会不安全。对于目前为止我们看到的动作不会发生这种情况,不过一般来讲,这确实是一个严重的问题。

另外如果在lambda表达式中引用变量,而这个变量可能在外部改变,这也是不合法的。例如,下面就是不合法的:

public static void repeat(String text,int count)
{
    for(int i=1;i<=count;i++)
    {
        ActionListener listener = event->
        {
            System.out.println(i+":"+text);
            //Error:Cannot refer to changing i 
        };
        new Timer(1000,listener).start();
    }
}

这里有一条规则:lambda表达式中捕获的变量必须实际上是最终变量(effectively final)实际上的最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text总是指示同一个String对象,所以捕获这个变量是合法的。不过,i的值会改变,因此不能捕获i。

lambda表达式的体与嵌套块有相同的作用域。这里同样适用命名冲突和遮蔽的有关规则。在lambda表达式中声明与一个局部变量同名的参数或局部变量是不合法的。

Path first=Paths.get("/usr/bin");
Couparator<String> comp = 
    (first,second)->first.length()-second.length();
    I//Error:Variable first already defined

在方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。

在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如,考虑下面的代码:

public class Application()
{
    public void init()
    {
        ActionListener listener = event->
            {
                System.out.printn(this.toString());
            ...
            }
        ...
    }
}

表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。在lambda表达式中,this的使用并没有任何特殊之处。lambda表达式的作用域嵌套在init方法中,与出现在这个方法中的其他位置一样,lambda表达式中this的含义并没有变化。


3.处理lambda表达式

到目前为止,你已经了解了如何生成lambda表达式,以及如何把lambda表达式传递到需要一个函数式接口的方法。下面来看如何编写方法处理lambda表达式。

使用lambda表达式的重点是延迟执行(deferred execution)。毕竟,如果想耍立即执行代码,完全可以直接执行,而无需把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因,如:

•在一个单独的线程中运行代码;

•多次运行代码;

•在算法的适当位置运行代码(例如,排序中的比较操作);

•发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);

•只在必要时才运行代码。

下面来看一个简单的例子。假设你想要重复一个动作n次。将这个动作和重复次数传递到一个repeat方法:

repeat(10,()->System.out.println("Hello,World!"));

要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口。下表列出了Java API中提供的最重要的函数式接口。在这里,我们可以使用Runnable接口:

public static void repeat(int n,Runnable action)
{
    for(int i=0;i< n;i++) action.run();
}

需要说明,调用action.runO时会执行这个lambda表达式的主体。

 

现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。为此,需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void:处理int值的标准接口如下:

public interface IntConsumer
{
    void accept(int value);
}

下面给出repeat方法的改进版本:

public static void repeat(int n,IntConsumer action)
{
    for(int i=0; i<n;i++) action.accept(i);
}

可以如下调用它:

repeat(10,i->System.out.println("Countdown:"+(9-i)));

下表列出了基本类型int、long和double的34个可能的规范。最好使用这些特殊化规范来减少自动装箱。出于这个原因,在上一节的例子中使用了IntConsumer而不是Consumer<lnteger>。

大多数标准函数式接口都提供了非抽象方法来生成或合并函数。例如,Predicate.isEqual(a)等同于a::equals,不过如果a为null也能正常工作。已经提供了默认方法and、or和negate来合并谓词。例如,Predicate.isEqua(a).or(Predicate.isEqual(b))就等同于x->a.equals(x)||b.equals(x)。

如果设计你自己的接口,其中只有一个抽象方法,可以用@FunctionalInterface注解来标记这个接口。这样做有两个优点。如果你无意中增加了另一个非抽象方法,编译器会产生一个错误消息。另外javadoc页里会指出你的接口是一个函数式接口。

并不是必须使用注解根据定义,任何有一个抽象方法的接口都是函数式接口。不过使用@FunctionalInterface注解确实是一个很好的做法。


4.再谈Comparator

Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。

静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。例如,假设有一个Person对象数组,可以如下按名字对这些对象排序:

Arrays.sort(people,Comparator.comparing(Person::getName));

与手动实现一个Compamtor相比,这当然要容易得多。另外,代码也更为清晰,因为显然我们都希望按人名来进行比较。

可以把比较器与thenComparing方法串起来。例如,

Arrays.sort(people,Comparator.comparing(Person::getlastName).thenConiparing(Person::getFirstName));

如果两个人的姓相同,就会使用第二个比较器。

这些方法有很多变体形式。可以为comparing和thenComparing方法提取的键指定一个比较器。例如,可以如下根据人名长度完成排序:

Arrays.sort(people,Comparator.companng(Person::getName,(s,t)->Integer.compare(s.1ength(),t.length())));

另外,comparing和thenComparing方法都有变体形式,可以避免int、long或double值的装箱。要完成前一个操作,还有一种更容易的做法:

Arrays.sort(people,Comparator.comparinglnt(p->p.getName()-length()));

如果键函数可以返回null,可能就要用到nullsFirst和nullsLast适配器。这些静态方法会修改现有的比较器,从而在遇到null值时不会抛出异常,而是将这个值标记为小于或大于正常值。例如,假设一个人没有中名时getMiddleName会返回一个null,就可以使用Comparator.comparing(Person::getMiddleName(),Comparator.nullsFirst(…))。

nullsFirst方法需要一个比较器,在这里就是比较两个字符串的比较器。naturalOrder方法可以为任何实现了Comparable的类建立一个比较器。在这里,Comparator.<String>naturalOrder()正是我们需要的。下面是一个完整的调用,可以按可能为null的中名进行排序。这里使用了一个静态导人java.util.C0mparator.*,以便理解这个表达式。注意naturalOrder的类型可以推导得出。

Arrays.sort(people,comparing(Person::getMiddleName,nullsFirst(naturalOrder())));

静态reverseOrder方法会提供自然顺序的逆序。要让比较器逆序比较,可以使用reversed实例方法。例如naturalOrder().reversed()等同于reverseOrder()。


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值