聂振宇,加入去哪儿网技术团队,目前在平台事业部/基础架构部,对并发编程,构建高并发系统很感兴趣。
引子
在我们平常写 Java 代码的过程中,总会或多或少的使用到 Java 泛型。但 Java 泛型有着各种各样的限制,运行时还会进行类型擦除,在需要重度使用泛型编程的场合,那酸爽,真是一言难尽。 作为曾经在 C++ 模版元编程中翱翔(挣扎)过的人,曾经多次痛苦发出“这也不行?”,“这样都不行?”,“到底还能干啥?”的感慨。不过工作还是要继续,虽然 Java 泛型不够完美,我们还是要尽量寻找到它能够发光发热的地方。 这篇文章将分享一些在 Java 泛型上探索(折腾)的经验,希望能对大家使用 Java 泛型起到一些帮助,有什么问题也欢迎指正。
场景
考虑这样一个简化的场景,应用会不断接收到 byte 数组,每个数组第一个 byte 表示数据类型,剩下的部分是 json 序列化后的数据,我们需要根据数据类型的不同来做一些对应的处理。
解决方案
初始方案
通过对场景的一顿分析,我们写出了如下的代码(考虑到版面问题,省去了异常处理和一些显而易见的代码):
@Service
public class ProcessorService {
@Resource
private List<CodeProcessor> codeProcessors;
private Map<Integer, CodeProcessor> processorMapping;
@PostConstruct
public void init() {
this.processorMapping = map(codeProcessors);
}
public void process(byte[] bytes) throws Exception {
int code = bytes[0];
ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes, 1, bytes.length - 1);
processorMapping.get(code).process(inputStream);
}
}
public interface CodeProcessor {
int code();
void process(InputStream inputStream) throws Exception;
}
public abstract class AbstractCodeProcessor<T> {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final int code;
private final TypeReference<T> typeReference;
protected AbstractCodeProcessor(int code, TypeReference<T> typeReference) {
this.code = code;
this.typeReference = typeReference;
}
@Override
public int code() {
return code;
}
@Override
public final void process(InputStream inputStream) throws Exception {
T data = MAPPER.readValue(inputStream, typeReference);
doProcess(data);
}
protected abstract void doProcess(T data);
}
@Service
public class ListStringProcessor extends AbstractCodeProcessor<List<String>> {
private static final int LIST_STRING_CODE = 0;
public ListStringProcessor() {
super(0, new TypeReference<List<String>>() {});
}
@Override
protected void doProcess(List<String> data) {
// do something
}
}
乍一眼看上去还行,每次如果有新的解析类型需要支持,不需要修改任何原有代码,只用照着 ListStringProcessor 的写法添加一个继承自 AbstractCodeProcessor 的解析类,比如 class PersonProcessor extends AbstractCode Processor<Person>,系统就自动能支持新的 Person 类型,似乎到这里为止也还不错。 但是,ListStringProcessor 里面的 new TypeReference<List<String>> 有些刺眼,泛型 List<String>在类定义上就有,这里又多写了一遍,有那么点重复代码的感觉。 出于对代码更高的追求(瞎折腾),我们决定除掉它。
第一次改进
想要去掉new TypeReference<list<String>>,就需要通过代码获取 ListStringProcessor 的泛型信息。我们无奈的发现,Java 的泛型参数信息对于类本身而言是没办法进行获取的。 不过我们没有放弃,再仔细查看api发现,有两个叫getGenericSuperclass和getGenericInterfaces的方法,如果父类或者接口是泛型的,那么可以把它们的返回值转成 ParameterizedType 对象,然后通过ParameterizedType的getActualTypeArguments方法获取泛型参数(那对于没有定义父类的类,我们就不能获取到泛型参数信息吗?没错,好像只能放弃治疗)。 泛型参数获取到之后,我们会发现 getActualTypeArguments() 返回的类型是 Type[],通过查找 jackson api,我们看到这个 Type 对象也可以起到类似 TypeReference 的作用,重写后代码如下:
public abstract class TypeCapture<T> {
protected final Type captureType() {
Type superclass = getClass().getGenericSuperclass();
return ((ParameterizedType) superclass).getActualTypeArguments()[0];
}
}
public abstract class AbstractCodeProcessor<T> extends TypeCapture {
private static final ObjectMapper MAPPER = new ObjectMapper();
private final int code;
private final JavaType type;
protected AbstractCodeProcessor(int code) {
this.code = code;
this.type = MAPPER.constructType(captureType());
}
@Override
public int code() {
return code;
}
@Override
public final void process(InputStream inputStream) throws Exception {
T data = MAPPER.readValue(inputStream, type);
doProcess(data);
}
protected abstract void doProcess(T data);
}
public class ListStringProcessor extends AbstractCodeProcessor<List<String>> {
public ListStringProcessor() {
super(0);
}
@Override
protected void doProcess(List<String> data) {
// do something
}
}
经过这次修改,我们去掉了碍眼的 new TypeReference<List<String>>,可以看到,现在一点儿重复代码都没有了。 但是,我们又发现了一个新的问题,如果像下面这么写的话,是没有办法获取到泛型参数的。
public class WrongProcessor<T> extends AbstractCodeProcessor<T> {}
CodeProcessor processor = new WrongProcessor<String>();
对于上面的 WrongProcessor<String>类型对象来说,由于泛型擦除,它相当于是 WrongProcessor<Object> 对象,type Capture 方法的结果会是通配符 T。而且这个问题在应用启动时不会出现,并且显而易见会在序列化时出现问题。我们能不能在程序启动时就发现这个问题呢?
第二次改进
通过 debug captureType 方法返回的通配符 T,我们发现它属于 TypeVariable 接口,这个接口专门用来描述通配符。如果我们想要检测的话,貌似在 captureType 函数里面判断一下就可以了。这样子应用启动过程中实例化对象,构造函数就会抛出异常,防止错误代码启动。代码如下:
public abstract class TypeCapture<T> {
protected final Type captureType() {
Type superclass = getClass().getGenericSuperclass();
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
if (type instanceof TypeVariable) {
throw new IllegalStateException("illegal type: " + type);
}
return type;
}
}
到这里似乎终于结束了,但是我们还要继续精益求精(瞎折腾)。
最终版
仔细查看 jdk 源码和文档,可以发现,getActualTypeArguments 方法返回的类型 Type 有这么几个继承它的接口或类。而刚刚出问题的 TypeVariable 只是其中的一种。 Class:具体类 ParameterizedType:带有泛型参数的类型 GenericArrayType:泛型数组 TypeVariable:通配符 WildcardType:泛型参数带有?的情况 想要检查出各种各样的问题,那么就得把这几种情况全都考虑到: 对于 Class 类型,代表检查通过; 对于 TypeVariable 和 WildcardType,代表检查失败; 对于 ParameterizedType,我们可以用 getRawType 获取它的不带泛型参数的类型(List<T>中的List),用 getActualTypeArguments 获取它的泛型参数类型,需要注意的是,getRawType和getActualTypeArguments返回的也是 Type,需要做递归处理; 对于 GenericArrayType,我们可以用 getGenericComponentType 获取它的数组类型,和 ParameterizedType 一样,这也是个 Type,需要做递归处理。 经过仔细分析后,最终版本如下:
public abstract class TypeCapture<T> {
protected final Type captureType() {
Type superclass = getClass().getGenericSuperclass();
Type type = ((ParameterizedType) superclass).getActualTypeArguments()[0];
check(type);
return type;
}
private void check(Type type) {
if (type instanceof Class) {
return;
} else if (type instanceof ParameterizedType) {
checkParameterizedType((ParameterizedType) type);
} else if (type instanceof GenericArrayType) {
check(((GenericArrayType) type).getGenericComponentType());
} else {
throw new IllegalStateException("illegal type: " + type);
}
}
private void checkParameterizedType(ParameterizedType type) {
check(type.getRawType());
Type[] actualTypeArguments = type.getActualTypeArguments();
if (actualTypeArguments != null) {
for (Type actualTypeArgument : actualTypeArguments) {
check(actualTypeArgument);
}
}
}
}
结束语
折腾到这里,总算有了一个较为完善且优雅的解决方案。Java 泛型的限制很多,但只要我们愿意精益求精(折腾),还是能做出一些有用的东西来。