Java 中的未绑定通用 return 类型
Unbound generic return type in Java
我正在考虑构建 Java API 的一些想法,今天我花了一些时间思考这段代码:
public <T> T getField1(Class<?> type, Class<T> retvalType, String fieldName) {
Object retval = ...; // boring reflection code - value could be dog, could be duck
return retvalType.cast(retval); // cast to expected - exception is raised here
}
// usage - we manually repeat the LHS type as a parameter
int foo = getField(o, Integer.class, "foo");
这就是我通常编写 API 的方式,他认为指定 retvalType
会给我额外的类型安全性。但是真的吗?
因为涉及反射,retval类型未知。仅当 retvalType
是非泛型时,第二行的转换才能可靠地捕获类型不匹配。另一方面,即使我们不进行这种显式转换,Java 运行时在将其设置为变量时也会转换通用结果——换句话说,如果我们重写上面的代码会有什么缺点吗如:
public <T> T getField2(Class<?> type, String fieldName) {
return ...; // boring reflection code - the value type will be checked when we assign it
}
// usage - exception is raised at the next line if types don't match
int foo = getField(o, "foo");
后一种代码的优点是用法更紧凑,不用我们自己去重复。缺点是类型转换异常来自不明显的地方(在调用站点没有显式转换),但我开始认为这是可以的 - 通过指定类型文字,我们只重复我们已经知道的,但最终因为反射,程序的健全性还是不能保证。
谁能很好地论证为什么第一个片段比第二个片段更可取?
编辑 1:
一个论点是,通过将预期类型作为参数,我们可以在不更改 API 的情况下满足进行非平凡强制转换的需要。
例如,查找的字段可能是 Date
类型,但我们可能请求 long
,可以通过 date.getTime()
在内部进行转换。这对我来说仍然显得牵强,并且存在混合责任的风险。
另一方面,不传递类型限制了输入依赖性,这使得 API 对更改更加健壮。 (提示 'information hiding'、'instability metric' 等)。
在与@shmosel 的对话中发现,当 LHS 类型为泛型时,第二个版本所依赖的运行时转换不一定会发生。
例如,考虑代码:
class Foo<T> {
void a1(Object o) {
// forces to be explicit about the chance of heap polution
@SuppressWarning("unchecked")
Set<String> a = (Set<String>) getField1(o, Set.class, "foo");
T c = (T) getField1(o, Set.class, "foo"); // guaranteed not to compile
}
void a2(Object o) {
// implicit chance of heap polution in case of Set<Date>
Set<String> a = (Set<String>) getField2(o, "foo");
T c = getField2(o, "foo"); // will succeed even if foo is Date
}
}
换句话说,通过在 getField()
方法中进行强制转换,我们可以硬性保证该值是指定的 class,即使它未被使用或分配给对象变量(在动态语言和反射库中经常发生)。
我正在考虑构建 Java API 的一些想法,今天我花了一些时间思考这段代码:
public <T> T getField1(Class<?> type, Class<T> retvalType, String fieldName) {
Object retval = ...; // boring reflection code - value could be dog, could be duck
return retvalType.cast(retval); // cast to expected - exception is raised here
}
// usage - we manually repeat the LHS type as a parameter
int foo = getField(o, Integer.class, "foo");
这就是我通常编写 API 的方式,他认为指定 retvalType
会给我额外的类型安全性。但是真的吗?
因为涉及反射,retval类型未知。仅当 retvalType
是非泛型时,第二行的转换才能可靠地捕获类型不匹配。另一方面,即使我们不进行这种显式转换,Java 运行时在将其设置为变量时也会转换通用结果——换句话说,如果我们重写上面的代码会有什么缺点吗如:
public <T> T getField2(Class<?> type, String fieldName) {
return ...; // boring reflection code - the value type will be checked when we assign it
}
// usage - exception is raised at the next line if types don't match
int foo = getField(o, "foo");
后一种代码的优点是用法更紧凑,不用我们自己去重复。缺点是类型转换异常来自不明显的地方(在调用站点没有显式转换),但我开始认为这是可以的 - 通过指定类型文字,我们只重复我们已经知道的,但最终因为反射,程序的健全性还是不能保证。
谁能很好地论证为什么第一个片段比第二个片段更可取?
编辑 1:
一个论点是,通过将预期类型作为参数,我们可以在不更改 API 的情况下满足进行非平凡强制转换的需要。
例如,查找的字段可能是 Date
类型,但我们可能请求 long
,可以通过 date.getTime()
在内部进行转换。这对我来说仍然显得牵强,并且存在混合责任的风险。
另一方面,不传递类型限制了输入依赖性,这使得 API 对更改更加健壮。 (提示 'information hiding'、'instability metric' 等)。
在与@shmosel 的对话中发现,当 LHS 类型为泛型时,第二个版本所依赖的运行时转换不一定会发生。
例如,考虑代码:
class Foo<T> {
void a1(Object o) {
// forces to be explicit about the chance of heap polution
@SuppressWarning("unchecked")
Set<String> a = (Set<String>) getField1(o, Set.class, "foo");
T c = (T) getField1(o, Set.class, "foo"); // guaranteed not to compile
}
void a2(Object o) {
// implicit chance of heap polution in case of Set<Date>
Set<String> a = (Set<String>) getField2(o, "foo");
T c = getField2(o, "foo"); // will succeed even if foo is Date
}
}
换句话说,通过在 getField()
方法中进行强制转换,我们可以硬性保证该值是指定的 class,即使它未被使用或分配给对象变量(在动态语言和反射库中经常发生)。