Java8 lambda表达式,Comparator.comparing().thenComparing()报错

  • Post author:
  • Post category:java


最近更新

加上了部分我自己对问题的理解,同时附件信息中贴上了一个我最近在公司内部分享的PPT,里面详细讲述了函数式接口、Lambda表达式、Stream、Optional等JDK8的新特性。有需求的同学可以下载参考一下。


引子

在对集合进行操作的时候,我喜欢用Java8的新特性——Stream/lambda表达式等。最近,在项目中就碰到了一个怪异的问题,这里记录下来,并将解决方式分享给大家。

问题描述

报错信息:

Cannot resolve method ‘xxx’

Non-static method cannot be referenced from a static context

问题复现:

首先,Book实体的定义:

@Data   // lombok 的注解
public class Book {
    /**
     * 文章字数
     */
    private Integer wordCount;  
    /**
     * 出版日期
     */
    private Date publishDate;
}

现在我们有一个需求,需要对一个 List<Book> bookList 进行排序,然后返回。

排序规则:

先按照 文章字数 倒序,再按照  出版日期 升序 排序。

这个需求很简单,怎么实现我不管。于是我们有了以下方法:

public List<Book> test1(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> -book.getWordCount()).thenComparing(Book::getPublishDate));
    return bookList;
}

这样,就会报上面提到的那两个错误,如下图:

同时,经过我测试,下面这四种情况也是有问题的(测试 是否是 减号(“-“)造成的问题):

public List<Book> test2(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> -book.getWordCount()).thenComparing(book -> book.getPublishDate));
    return bookList;
}

public List<Book> test3(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> book.getPublishDate()).thenComparing(book -> -book.getWordCount()));
    return bookList;
}

public List<Book> test4(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> book.getPublishDate()).thenComparing(book -> book.getWordCount()));
    return bookList;
}

public List<Book> test5(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> -book.getPublishDate()).thenComparing(book -> -book.getWordCount()));
    return bookList;
}

如图:

下面两种情况是没问题的:

public List<Book> test6(List<Book> bookList) {
    bookList.sort(Comparator.comparing(Book::getPublishDate).thenComparing(book -> -book.getWordCount()));
    return bookList;
}

public List<Book> test7(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> -book.getWordCount()));
    return bookList;
}

如下图:

那这就奇怪了,难道,我们在需要使用Comparator.comparing().thenComparing()进行两次优先级排序的时候,只能先使用 方法引用(就是 :: 双冒号语法,如上图我标 红色横线 的地方),然后再使用 普通 -> 的方式?那这样第一次排序岂不是只能升序?肯定是不可能的。

我们注意到,book -> -book.getWordCount() 这个地方的报错信息为:Cannot resolve method ‘getWordCount()’ 。我们查看 book 引用的类型信息:

我们可以看到,book 的类型为 Object。这说明,在这里,book 引用类型没有自动推断出来。要解决这个问题,很简单,我们手动为其指定类型即可,如下:

public List<Book> test6(List<Book> bookList) {
    bookList.sort(Comparator.comparing((Book book) -> -book.getWordCount()).thenComparing(Book::getPublishDate));
    return bookList;
}

如下图,我们可以看到问题已经被解决,并且lambda链后方的问题也一并解决了。

注意图中标价的部分,我们手动指定了 book 的引用类型。

至于 其他 test 方法中的错误,也都可以通过 手动指定book 引用类型来解决。

原因探究

至此,该问题我们已经解决。但是,为什么出现这个错误呢?

首先,我们必须先明确几个知识点:

1、只定义了一个抽象方法的接口称为函数式接口。可以有多个默认实现的方法,但是只能有一个抽象方法。注意Object类实现的方法。

2、Lambda表达式的类型,也称为 目标类型,是


由编译器从根据其上下文推断出来


的。Lambda表达式的类型,必须是函数式接口。

3、对于

方法引用

,如果使用的是


静态方法


,或者


调用目标明确


,那么


流内的元素将会自动作为参数使用


;如果使用的是

静态方法

,或者


不存在调用目标


,那么


流内元素将会自动作为调用目标


对于上面提到的第一点,比较好理解。问题是,第二点该如何理解呢?这里,谈一下我的理解:

我们常见的三种引用类型:

  • 静态方法引用     ClassName::methodName
  • 实例上的实例方法引用     instanceReference::methodName
  • 类型上的实例方法引用     ClassName::methodName

对于前两种——静态方法引用 和 实例上的实例方法引用,其调用目标明确:

  • 静态方法引用:静态方法,通过类名 去调用,调用目标即 :: 前指定的类名;
  • 实例上的实例方法引用:实例方法,需要通过 对象实例 去调用,调用目标即 :: 前的实例对象;

而实例上的实例方法不一样,实例方法,需要通过 实例对象 去调用,而 ClassName::methodName 语法中,调用对象并不明确,只约束了调用方法的对象,是 :: 前的类的对象实例,但是并不知道具体是 哪一个对象。所以,在此时,流中的元素,就会作为调用对象。 既然,流中元素作为调用对象,那么对象本身就携带了类型信息了。这里,需要一点理解。

我们看上面具体的例子:

public List<Book> test2(List<Book> bookList) {
    bookList.sort(Comparator.comparing(book -> -book.getWordCount()).thenComparing(book -> book.getPublishDate));
    return bookList;
}

public List<Book> test6(List<Book> bookList) {
    bookList.sort(Comparator.comparing(Book::getPublishDate).thenComparing(book -> -book.getWordCount()));
    return bookList;
}

如下图:

首先,通过源码,我们可以知道, comparing 和 thenComparing 两个方法的返回类型都是 Comparator<T> ,Comparator是一个函数式接口;

对于 test2 这个例子,我是这么理解的:

前面我们提到,Lambda表达式根据其上下文信息推断Lambda表达式的目标类型。那么,在test2例子中,comparing返回的目标类型推断,需要其上下文信息,而这里,仅仅只能得知其上文期待的类型信息,而不能得知其下文 thenComparing 实际会返回的类型,所以,这里,在comparing方法这里,返回的目标类型推断失败。这里,注意理解。如果各位朋友有更好的描述方式,欢迎交流。

而对于test6这个例子,首先  Book::getPublistDate 是一个  类型上的实例方法引用,上面我们讲过,这种方法引用,流中元素将会自动作为调用方。也就是说,在这里,流中元素 Book对象 在调用方法时,同时携带了自身的类型信息 (对象类型为 Book类型),就相当于我们上面 手动限定了 引用类型是 Book 了。所以,comparing 方法在执行时,通过方法引用 限定了 对象类型,那么其目标类型也就确定了,在comparing方法执行完后,其返回的 Comparator<T> 类型也对应得到了;不过在 thenComparing 方法中,仍然需要根据上下文类型推断其目标类型。

以上就是我的部分理解,作者水平有限,也在不断学习中,难免会有纰漏,希望和大家一起进步。如果有什么地方我描述有误或者没写明白的,欢迎大家指出。

附件信息

下载链接:

遇见Lambda——Java8新特性.pptx

参考文献

1、《实战·Java高并发程序设计》葛一鸣 郭超 著。第六章 Java8与高并发



版权声明:本文为Zereao原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。