泛型接口与实现类型转换问题:如何基于接口编程?
首先得明确:你的核心思路——基于ItemService接口编程、保留实现类可替换性——完全没问题!问题出在你用了不绑定的通配符,导致编译器无法追踪泛型类型之间的依赖关系。
为什么会报错?
当你声明ItemService<? extends Slicer, ? extends ItemSlice>时,编译器会把这两个?当成两个完全独立的未知类型:
- 第一个
?代表某个未知的Slicer子类(比如DynamoDbSlicer) - 第二个
?代表某个未知的ItemSlice子类(比如DynamoDbItemSlice)
但编译器不知道这两个未知类型其实是配对的——更关键的是,它不知道ItemSlice的next()方法返回的Slicer,正好就是ItemService需要的那个T类型参数。所以当你传入DynamoDbSlicer或者slice.next()的返回值时,编译器无法确认它们和那个未知的T是同一个类型,自然就报错了。
解决方案
这里有几种可行的方案,按实现复杂度从低到高排列:
方案1:明确声明接口的具体泛型类型
这是最简单直接的方式,完全符合“基于接口编程”的需求——你依然用ItemService接口引用实现类,只是明确了它的类型参数:
// 用具体的类型参数替代通配符 ItemService<DynamoDbSlicer, DynamoDbItemSlice> itemService = new DynamoDbItemService(); // 后续调用完全正常 ItemSlice slice = itemService.getItemHistory(itemId, DynamoDbSlicer.first(1)); slice = itemService.getItemHistory(itemId, (DynamoDbSlicer) slice.next());
这样编译器能清晰追踪到T是DynamoDbSlicer,E是DynamoDbItemSlice,而DynamoDbItemSlice.next()返回的正好是DynamoDbSlicer,完美匹配参数要求。如果后续要替换实现,只需要把类型参数换成对应实现的Slicer和ItemSlice子类即可。
方案2:用泛型方法捕获绑定的类型参数
如果需要在代码中保持一定的通用性(比如处理多种ItemService实现),可以通过泛型方法来让编译器自动捕获类型绑定关系:
// 泛型方法会自动绑定T和E的关系 private static <T extends Slicer, E extends ItemSlice> void processItemHistory(ItemService<T, E> service, String itemId) { // 注意:如果是通用场景,初始slicer的获取方式需要调整,这里只是示例 T initialSlicer = (T) DynamoDbSlicer.first(1); E slice = service.getItemHistory(itemId, initialSlicer); // 此时slice.next()的返回值可以安全转换为T slice = service.getItemHistory(itemId, (T) slice.next()); } // 调用时直接传入实现类,编译器自动推导类型 processItemHistory(new DynamoDbItemService(), itemId);
这种方式把类型绑定的工作交给了方法的泛型参数,编译器能保证T和E的一致性,以及slice.next()返回的Slicer就是T类型。
方案3:调整接口定义,强化类型绑定
如果可以修改ItemSlice和Slice接口的定义,可以让它们的泛型参数和ItemService的T直接绑定,从根源上避免类型不匹配:
// 调整Slice接口,让next()返回指定的Slicer子类 public interface Slice<T, S extends Slicer> { List<T> data(); S next(); } // 调整ItemSlice,绑定到具体的Slicer子类 public interface ItemSlice<S extends Slicer> extends Slice<Item, S> { } // 调整ItemService,确保E的next()返回的就是T public interface ItemService<T extends Slicer, E extends ItemSlice<T>> { E getItemHistory(final String id, final T slicer); } // 实现类保持不变 public class DynamoDbItemSlice implements ItemSlice<DynamoDbSlicer> { public ImmutableList<Item> data() {} public DynamoDbSlicer next() {} }
此时声明变量时,可以用更灵活的方式,同时保证类型安全:
ItemService<DynamoDbSlicer, ? extends ItemSlice<DynamoDbSlicer>> itemService = new DynamoDbItemService(); // 调用时无需强制转换,编译器能确认slice.next()返回的是DynamoDbSlicer ItemSlice<DynamoDbSlicer> slice = itemService.getItemHistory(itemId, DynamoDbSlicer.first(1)); slice = itemService.getItemHistory(itemId, slice.next());
总结
你的“基于接口编程、保留实现可替换性”的思路完全正确,只是通配符的使用方式破坏了泛型类型之间的绑定关系。上面的方案都能解决问题,其中方案1最适合你的场景——简单直接,既满足接口编程的需求,又能避免编译错误。
内容的提问来源于stack exchange,提问作者Stuart Leyland-Cole

