为 class 类型指定泛型
Specifying generics for a class type
我有一个主要处理配置类型转换的方法,但是在指定通用类型(例如 List
)后,如何处理特定类型就成了问题。在理想世界中,诸如使用类型见证之类的东西:
List<String> someVal = MyConfig.SOME_VAL.<List<String>>.as(List.class);'
(完整的 as
代码):
/**
* Attempts to return the {@link Config} value as a casted type. If the
* value cannot be casted it will attempt to return the default value. If
* the default value is inappropriate for the class, the method will
* throw a {@link ClassCastException}.
*
* @since 0.1.0
* @version 0.1.0
*
* @param <T> The type of the casting class
* @param c The class type to cast to
* @return A casted value, or {@code null} if unable to cast. If the passed
* class parameter is of a primitive type or autoboxed primitive,
* then a casted value of -1 is returned, or {@code false} for
* booleans. If the passed class parameter is for {@link String},
* then {@link Object#toString()} is called on the value instead
*/
default public <T> T as(Class<T> c) {
Validate.notNull(c, "Cannot cast to null");
Validate.isTrue(Primitives.unwrap(c) != void.class, "Cannot cast to a void type");
Object o = this.get();
if (o == null) {
T back = Reflections.defaultPrimitiveValue(c);
if (back != null) { //catch for non-primitive classes
return back;
}
}
if (c == String.class) {
return (T) String.valueOf(o);
}
if (c.isInstance(o)) {
return c.cast(o);
}
if (c.isInstance(this.getDefault())) {
return c.cast(this.getDefault());
}
throw new ClassCastException("Unable to cast config value");
}
所以基本上这给我留下了一个分为两部分的问题:为什么类型见证不能用于 class 上的泛型(例如 List(raw)
-> List<String>
) ,以及如何支持检索具有通用边界的 class 而不进行无关的转换?第一点让我特别困惑,因为这是完全合法的:
List<String> test = new ArrayList<>();
test = MyConfig.FRIENDLY_MOBS.as(test.getClass());
尽管它返回原始类型的列表
那一行真的很糟糕(类型擦除/原始类型),因为没有检查 Collection 类型是否真的包含字符串。
test = MyConfig.FRIENDLY_MOBS.as(test.getClass());
我认为最简单的解决方案是编写一个 as 方法,它接受集合类型的 class 对象和元素 class。请参阅以下示例(在静态范围内,因此您必须对其进行调整):
static List<String> words = Arrays.asList("Hello", "Bonjour", "engine");
static public <E, Coll extends Collection<E>> Coll as(Class<? extends Collection> collClass, Class<E> elemClass) {
if (collClass.isInstance(words)) {
Collection coll = collClass.cast(words);
for (Object o : coll)
if (!elemClass.isInstance(o))
throw new ClassCastException("BAM");
return (Coll)coll;
}
return null;
}
现在发现以下行为:
final List<String> list = as(List.class, String.class); // works
final List<String> list = as(List.class, Integer.class); // compile error: type check
final List<Integer> list = as(List.class, String.class); // compile error: type check
final List<Integer> list = as(List.class, Integer.class); // ClassCastException
至于其他尝试:iirc Jackson 有一些神奇的 TypeToken 东西可以捕获 List 等类型。它以某种方式滥用了 Enum 我认为...
您关于类型见证的想法确实是可行的方法,但您需要一个更好的类型见证,它不仅可以捕获原始类型(这里 List
),还可以捕获它的泛型参数。这在 Java 中并不容易,因为在大多数地方,由于类型擦除,泛型参数在运行时不可用。 Java 的反射 API 使用作为 Type
的子接口的接口作为泛型类型的运行时表示,但这些不适合您的目的,因为它们不提供任何编译时类型信息.
然而,有一个技巧可以实现你想要的。
这个技巧基于这样一个事实,即如果 class(示例:MyClass
)继承自泛型类型(示例:List<String>
),则不会进行类型擦除。您可以在运行时检索 MyClass
从 List<String>
继承的信息(使用方法 Class.getGenericSuperclass()
)。
然而,Subclassing 我们想要传递的实际类型将非常不灵活(例如,这不适用于 final classes)。因此,我们创建了一个特殊的 class(通常称为 TypeToken
),我们可以从中继承。 TypeToken
class 有一个泛型参数,在 subclass 中我们指定我们想要传递的类型作为这个参数。当然,为每个要传递的不同值创建一个特殊的 class 通常会很麻烦,但幸运的是我们可以使用匿名 classes 使这变得容易。
综合起来,解决方案可能如下所示。
我们类型标记的定义:
public abstract class TypeToken<T> {}
方法定义as
:
public <T> T as(TypeToken<T> typeToken) {
Type targetType = typeToken.getClass().getGenericSuperclass();
// use targetType here, e.g.
if (targetType instanceof ParameterizedType) { ... }
并调用它:
List<Integer> list = MyConfig.SOME_VAL.as(new TypeToken<List<String>>() {});
注意声明匿名子class并避免类型擦除的{}
。
更好的方法是使用现有的 class 作为类型标记,例如 class TypeToken
of the great Guava library (if you do not know this library yet, also look at what else it offers and consider using it!). This class also provides additional helper methods that make it easer to use the token in the as
method (directly using Type
instances can be difficult). The Guava wiki has more information on its TypeToken
class.
如果你担心创建太多 classes,你当然可以很容易地为 TypeToken<List<String>>
等常见情况提供一些默认实例。Guava 的 TypeToken
也有一个of(Class<T>)
方法可用于非泛型类型,因此 subclasses 将仅限于实际需要的情况。
其他项目也使用这个技巧,例如 Guice class TypeLiteral
(explanation), Gson (TypeToken
), and Jackson (TypeReference
)。所以我不会太担心 subclasses 的数量,因为它们不会弄乱你的源代码。
我有一个主要处理配置类型转换的方法,但是在指定通用类型(例如 List
)后,如何处理特定类型就成了问题。在理想世界中,诸如使用类型见证之类的东西:
List<String> someVal = MyConfig.SOME_VAL.<List<String>>.as(List.class);'
(完整的 as
代码):
/**
* Attempts to return the {@link Config} value as a casted type. If the
* value cannot be casted it will attempt to return the default value. If
* the default value is inappropriate for the class, the method will
* throw a {@link ClassCastException}.
*
* @since 0.1.0
* @version 0.1.0
*
* @param <T> The type of the casting class
* @param c The class type to cast to
* @return A casted value, or {@code null} if unable to cast. If the passed
* class parameter is of a primitive type or autoboxed primitive,
* then a casted value of -1 is returned, or {@code false} for
* booleans. If the passed class parameter is for {@link String},
* then {@link Object#toString()} is called on the value instead
*/
default public <T> T as(Class<T> c) {
Validate.notNull(c, "Cannot cast to null");
Validate.isTrue(Primitives.unwrap(c) != void.class, "Cannot cast to a void type");
Object o = this.get();
if (o == null) {
T back = Reflections.defaultPrimitiveValue(c);
if (back != null) { //catch for non-primitive classes
return back;
}
}
if (c == String.class) {
return (T) String.valueOf(o);
}
if (c.isInstance(o)) {
return c.cast(o);
}
if (c.isInstance(this.getDefault())) {
return c.cast(this.getDefault());
}
throw new ClassCastException("Unable to cast config value");
}
所以基本上这给我留下了一个分为两部分的问题:为什么类型见证不能用于 class 上的泛型(例如 List(raw)
-> List<String>
) ,以及如何支持检索具有通用边界的 class 而不进行无关的转换?第一点让我特别困惑,因为这是完全合法的:
List<String> test = new ArrayList<>();
test = MyConfig.FRIENDLY_MOBS.as(test.getClass());
尽管它返回原始类型的列表
那一行真的很糟糕(类型擦除/原始类型),因为没有检查 Collection 类型是否真的包含字符串。
test = MyConfig.FRIENDLY_MOBS.as(test.getClass());
我认为最简单的解决方案是编写一个 as 方法,它接受集合类型的 class 对象和元素 class。请参阅以下示例(在静态范围内,因此您必须对其进行调整):
static List<String> words = Arrays.asList("Hello", "Bonjour", "engine");
static public <E, Coll extends Collection<E>> Coll as(Class<? extends Collection> collClass, Class<E> elemClass) {
if (collClass.isInstance(words)) {
Collection coll = collClass.cast(words);
for (Object o : coll)
if (!elemClass.isInstance(o))
throw new ClassCastException("BAM");
return (Coll)coll;
}
return null;
}
现在发现以下行为:
final List<String> list = as(List.class, String.class); // works
final List<String> list = as(List.class, Integer.class); // compile error: type check
final List<Integer> list = as(List.class, String.class); // compile error: type check
final List<Integer> list = as(List.class, Integer.class); // ClassCastException
至于其他尝试:iirc Jackson 有一些神奇的 TypeToken 东西可以捕获 List
您关于类型见证的想法确实是可行的方法,但您需要一个更好的类型见证,它不仅可以捕获原始类型(这里 List
),还可以捕获它的泛型参数。这在 Java 中并不容易,因为在大多数地方,由于类型擦除,泛型参数在运行时不可用。 Java 的反射 API 使用作为 Type
的子接口的接口作为泛型类型的运行时表示,但这些不适合您的目的,因为它们不提供任何编译时类型信息.
然而,有一个技巧可以实现你想要的。
这个技巧基于这样一个事实,即如果 class(示例:MyClass
)继承自泛型类型(示例:List<String>
),则不会进行类型擦除。您可以在运行时检索 MyClass
从 List<String>
继承的信息(使用方法 Class.getGenericSuperclass()
)。
Subclassing 我们想要传递的实际类型将非常不灵活(例如,这不适用于 final classes)。因此,我们创建了一个特殊的 class(通常称为 TypeToken
),我们可以从中继承。 TypeToken
class 有一个泛型参数,在 subclass 中我们指定我们想要传递的类型作为这个参数。当然,为每个要传递的不同值创建一个特殊的 class 通常会很麻烦,但幸运的是我们可以使用匿名 classes 使这变得容易。
综合起来,解决方案可能如下所示。
我们类型标记的定义:
public abstract class TypeToken<T> {}
方法定义as
:
public <T> T as(TypeToken<T> typeToken) {
Type targetType = typeToken.getClass().getGenericSuperclass();
// use targetType here, e.g.
if (targetType instanceof ParameterizedType) { ... }
并调用它:
List<Integer> list = MyConfig.SOME_VAL.as(new TypeToken<List<String>>() {});
注意声明匿名子class并避免类型擦除的{}
。
更好的方法是使用现有的 class 作为类型标记,例如 class TypeToken
of the great Guava library (if you do not know this library yet, also look at what else it offers and consider using it!). This class also provides additional helper methods that make it easer to use the token in the as
method (directly using Type
instances can be difficult). The Guava wiki has more information on its TypeToken
class.
如果你担心创建太多 classes,你当然可以很容易地为 TypeToken<List<String>>
等常见情况提供一些默认实例。Guava 的 TypeToken
也有一个of(Class<T>)
方法可用于非泛型类型,因此 subclasses 将仅限于实际需要的情况。
其他项目也使用这个技巧,例如 Guice class TypeLiteral
(explanation), Gson (TypeToken
), and Jackson (TypeReference
)。所以我不会太担心 subclasses 的数量,因为它们不会弄乱你的源代码。