最近更新
加上了部分我自己对问题的理解,同时附件信息中贴上了一个我最近在公司内部分享的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 方法中,仍然需要根据上下文类型推断其目标类型。
以上就是我的部分理解,作者水平有限,也在不断学习中,难免会有纰漏,希望和大家一起进步。如果有什么地方我描述有误或者没写明白的,欢迎大家指出。
附件信息
参考文献
1、《实战·Java高并发程序设计》葛一鸣 郭超 著。第六章 Java8与高并发