Java map 方法引用为何报 non-static method 错误?
结论
问题不在 static context,而在于泛型信息丢失,导致编译器无法推断方法引用的类型。
直接调用:
orderType.getOrderExtnTransList().stream()
得到的很可能是原始类型 Stream,或者元素类型被推断为 Object。OrderExtnTrans::toDto 引用的是 OrderExtnTrans 的非静态方法,相当于:
orderExtnTrans -> orderExtnTrans.toDto()
如果流中元素的编译期类型是 Object,编译器就无法确认参数是 OrderExtnTrans,自然也无法匹配这个方法引用。此时出现的 Non-static method cannot be referenced from a static context 容易让人误判问题所在。
第一种写法之所以能够编译,是因为这里发生了未经检查的类型转换:
List<OrderExtnTrans> list = orderType.getOrderExtnTransList();
编译器暂时接受了“列表中的元素都是 OrderExtnTrans”这一断言,后续得到的流也就成了 Stream<OrderExtnTrans>。但 unchecked 警告已经表明,编译器并没有验证这种类型转换是否安全。
为什么方法引用需要明确的元素类型
对于实例方法引用:
OrderExtnTrans::toDto
假设 toDto() 返回 DtoOrderExtnTrans,那么它可以理解为:
(OrderExtnTrans value) -> value.toDto()
因此,下面的代码需要 Stream<OrderExtnTrans>:
Stream<OrderExtnTrans> stream = ...;
List<DtoOrderExtnTrans> result = stream
.map(OrderExtnTrans::toDto)
.collect(Collectors.toList());
如果接收者使用了原始泛型类型,例如:
Order order; // Order 本身其实是泛型类,但这里省略了类型参数
那么在这个接收者上调用泛型方法时,返回类型也会被擦除。原本类似下面的返回值:
List<OrderExtnTrans> getOrderExtnTransList()
通过原始类型接收者调用后,可能退化为原始 List。这样一来:
order.getOrderExtnTransList().stream()
得到的也是原始 Stream,方法引用随之失去目标参数类型。
正确修改泛型边界
不要让 OrderType 以原始类型的形式继承或使用 Order,而应提供完整的泛型边界。
假设 Order 可以整理为:
public class Order<D extends DtoOrder<?>> implements Serializable {
@OneToMany(
fetch = FetchType.LAZY,
mappedBy = "order",
orphanRemoval = true
)
private List<OrderExtnTrans> orderExtnTransList = new ArrayList<>();
public List<OrderExtnTrans> getOrderExtnTransList() {
return orderExtnTransList;
}
}
DTO 可以保留具体的订单实体类型:
public class DtoOrder<O extends Order<?>> extends DtoEntity {
private List<DtoOrderExtnTrans> dtoOrderExtnTransList;
public DtoOrder(O order) {
super(order);
this.dtoOrderExtnTransList = order.getOrderExtnTransList()
.stream()
.map(OrderExtnTrans::toDto)
.collect(Collectors.toList());
}
}
具体类型继续传递类型参数:
public class OrderStex extends Order<DtoOrderStex> {
}
public class DtoOrderStex extends DtoOrder<OrderStex> {
public DtoOrderStex(OrderStex order) {
super(order);
}
}
实际需要多少个泛型参数,要看 Order、DtoOrder 和 OrderExtnTrans 的定义,但处理原则相同:
- 不要使用原始类型
Order - 不要使用原始类型
List - 在每一层继承关系中继续传递类型参数
- 让
getOrderExtnTransList()明确返回List<OrderExtnTrans>或对应的泛型类型
如果 transition 本身也是泛型类型
如果不同订单对应不同的 transition 子类型,也可以把元素类型纳入泛型:
public class Order<T extends OrderExtnTrans> implements Serializable {
private List<T> orderExtnTransList = new ArrayList<>();
public List<T> getOrderExtnTransList() {
return orderExtnTransList;
}
}
DTO 需要声明相同的类型关系:
public class DtoOrder<
T extends OrderExtnTrans,
O extends Order<T>> extends DtoEntity {
private List<DtoOrderExtnTrans> dtoOrderExtnTransList;
public DtoOrder(O order) {
super(order);
this.dtoOrderExtnTransList = order.getOrderExtnTransList()
.stream()
.map(OrderExtnTrans::toDto)
.collect(Collectors.toList());
}
}
如果 toDto() 的返回类型也会随 transition 子类变化,可以进一步定义类型契约:
public interface DtoConvertible<D> {
D toDto();
}
public class OrderExtnTrans
implements DtoConvertible<DtoOrderExtnTrans> {
@Override
public DtoOrderExtnTrans toDto() {
return new DtoOrderExtnTrans(this);
}
}
这样,转换关系由类型系统直接表达,不再依赖强制转换。
为什么临时变量不是可靠的修复
下面这段代码:
List<OrderExtnTrans> list = orderType.getOrderExtnTransList();
如果右侧实际返回原始 List,它等于告诉编译器:“请相信列表中的元素全是 OrderExtnTrans。”编译器只能给出 unchecked 警告,然后继续编译。
一旦列表中混入其他类型,异常可能会推迟到流处理时才出现:
ClassCastException
因此,忽略警告并不能真正解决问题。需要修改的是:
getOrderExtnTransList()
的声明、OrderType 的泛型边界,以及继承链中遗漏的类型参数。
临时排查方式
可以先改用显式 lambda,观察编译器实际推断出的参数类型:
orderType.getOrderExtnTransList()
.stream()
.map(value -> value.toDto())
.collect(Collectors.toList());
如果 value.toDto() 报告 Object 上不存在该方法,就能确认流的元素类型已经退化为 Object。
也可以临时指定参数类型:
orderType.getOrderExtnTransList()
.stream()
.map((OrderExtnTrans value) -> value.toDto())
.collect(Collectors.toList());
不过,如果上游仍是原始 Stream,这里通常还会出现 unchecked 警告。因此,这种写法适合定位问题,不适合作为最终方案。
强制转换也只能作为过渡措施:
@SuppressWarnings("unchecked")
List<OrderExtnTrans> transitions =
(List<OrderExtnTrans>) orderType.getOrderExtnTransList();
由于类型擦除,JVM 无法验证列表中每个元素的真实类型,所以这种转换仍然不安全。
需要重点检查的声明
依次检查以下位置是否使用了原始类型:
class OrderType extends Order
应根据实际定义改成类似:
class OrderType extends Order<SomeDtoType>
泛型边界也不能写成:
<T extends Order>
可以写成:
<T extends Order<?>>
或者提供具体的类型参数:
<T extends Order<SomeDtoType>>
getter 必须保留元素类型:
public List<OrderExtnTrans> getOrderExtnTransList() {
return orderExtnTransList;
}
不要声明为:
public List getOrderExtnTransList()
最终需要让下面这个表达式的编译期类型明确成为 Stream<OrderExtnTrans>:
orderType.getOrderExtnTransList().stream()
泛型信息完整后,原来的链式写法就能正常编译:
this.dtoOrderExtnTransList = orderType.getOrderExtnTransList()
.stream()
.map(OrderExtnTrans::toDto)
.collect(Collectors.toList());
如果使用 Java 16 或更高版本,也可以写成:
this.dtoOrderExtnTransList = orderType.getOrderExtnTransList()
.stream()
.map(OrderExtnTrans::toDto)
.toList();
需要注意,Stream.toList() 返回的列表不可修改。如果字段需要可变列表,应继续使用 Collectors.toList(),或者显式收集到指定集合中。
备注:内容仅供参考。
