简体   繁体   English

Java如何使用Set参数化通用方法?

[英]Java how to parametrize a generic method with a Set?

I have a method with such signature: 我有这样的签名的方法:

private <T> Map<String, byte[]> m(Map<String, T> data, Class<T> type)

When I invoke like this for example it is working fine: 例如,当我这样调用时,它运行良好:

Map<String, String> abc= null;
m(abc, String.class);

But when my parameter T is a Set it doesn't work: 但是,当我的参数T是Set时,它不起作用:

Map<String, Set<String>> abc= null;
m(abc, Set.class);

Is there a way to make it work? 有办法使它起作用吗?

You're going to have to do something really ugly, using an unchecked cast like this: 您将必须使用如下所示的未经检查的演员表来做一些非常丑陋的事情:

m(abc, (Class<Set<String>>) (Class<?>) Set.class);

This comes down to type-erasure. 这归结为类型擦除。 At runtime Class<Set<String>> is the same as Class<Set<Integer>> , because we don't have reified generics, and so there is no way to know that what you have is a class for a "Set of strings" vs. a class for a "Set of integers". 在运行时, Class<Set<String>>Class<Set<Integer>> ,因为我们没有统一的泛型,因此无法知道您拥有的是“ Set of字符串”和“整数集”的类。

I asked a related question some time ago that should also give you some pointers: 不久前我问了一个相关的问题,该问题也应该给您一些提示:

IMO this confusion is due to the fact the generics were bolted on after the fact, and aren't reified. IMO之所以感到困惑,是因为事实是,泛型是在事实发生之后才被螺栓固定的,因此并未得到具体化。 I think it's a failing of the language when the compiler tells you that the generic types don't match, but you don't have an easy way of even representing that particular type. 当编译器告诉您泛型类型不匹配时,我认为这是一种语言的失败,但是您甚至没有简单的方法来表示该特定类型。 For example, in your case you end up with the compile-time error: 例如,在您的情况下,您最终会遇到编译时错误:

        m(abc, Set.class);
        ^
  required: Map<String,T>,Class<T>
  found: Map<String,Set<String>>,Class<Set>
  reason: inferred type does not conform to equality constraint(s)
    inferred: Set
    equality constraints(s): Set,Set<String>
  where T is a type-variable:
    T extends Object declared in method <T>m(Map<String,T>,Class<T>)

Now it would be perfectly reasonable for you to think "Oh, I should use Set<String>.class then", but that is not legal. 现在,您认为“哦,那我应该使用Set<String>.class ”将是完全合理的,但这是不合法的。 This is abstraction leakage from the implementation of generics in the language, specifically that they are subject to type-erasure. 这是从泛型语言实现中的抽象泄漏,特别是泛型易受类型擦除。 Semantically, Set<String>.class represents the runtime class instance of a set of strings. 在语义上, Set<String>.class表示一组字符串的运行时类实例。 But actually at runtime we cannot represent the runtime class of a set of strings, because it is indistinguishable from a set that contains objects of any other type. 但是实际上,在运行时我们不能表示一组字符串的运行时类,因为它与包含任何其他类型的对象的集合是无法区分的。

So we have a runtime semantic that is at odds with compile-time semantic, and knowing why Set<T>.class isn't legal requires knowing that generics are not reified at runtime. 因此,我们有一个与编译时语义不一致的运行时语义,要知道Set<T>.class 为什么不合法,就需要知道泛型在运行时不会得到验证。 This mismatch is what leads to weird workarounds like these. 这种不匹配是导致出现此类奇怪变通办法的原因。

What compounds the problem is that class instances also ended up being conflated with type-tokens. 使问题复杂化的是,类实例最终也被与类型令牌混合在一起。 Since you do not have access to the type of the generic parameter at runtime, the work around has been to pass in an argument of type Class<T> . 由于您在运行时无法访问通用参数的类型,因此解决方法是传入Class<T>类型的参数。 On the surface this works great because you can pass in things like String.class (which is of type Class<String> ) and the compiler is happy. 从表面上看,这非常String.class ,因为您可以传递String.classClass<String>东西(类型为Class<String> ),并且编译器很高兴。 But this method breaks down in your case: what if T itself represents a type with its own generic-type parameter? 但是这种方法在您的情况下很糟糕:如果T 本身代表具有自己的泛型类型参数的类型怎么办? Now using classes as type-tokens is not useful because there is no way to distinguish between Class<Set<String>> and Class<Set<Integer>> because fundamentally, they are both Set.class at runtime and so share the same class instance. 现在将类用作类型标记没有用,因为无法区分Class<Set<String>>Class<Set<Integer>>因为从根本上说,它们在运行时都是Set.class ,因此共享同一个类实例。 So IMO, using a class as a runtime type-token doesn't work as a general solution. 因此,将类用作运行时类型令牌的IMO不能用作常规解决方案。

Due to this shortcoming in the language, there are some libraries that make it very easy to retrieve the generic type-information. 由于该语言的缺点,因此有些库使检索通用类型信息非常容易。 In addition they also provide classes are better at representing the "type" of something: 此外,它们还提供了更擅长表示事物“类型”的类:

From what I see, there are two potential solutions to this problem in which both have their respective limitations. 从我的角度来看,有两个潜在的解决方案,它们都有各自的局限性。

The first solution relies on the fact that java's type erasure is complete , meaning that types for any parametrized types are erased regardless of "depth". 第一个解决方案依赖于Java的类型擦除已完成的事实,这意味着无论“深度”如何,所有参数化类型的类型都将被擦除。 For example: a Map<String, Set<String> will get reduced to Map<String, Set> and then Map<Object, Object> meaning that whilst type information is hard to obtain, it technically isn't needed during runtime given that any object can be inserted into the Map (given that it passes all class casts). 例如: Map<String, Set<String>将被简化为Map<String, Set> ,然后是Map<Object, Object>这意味着尽管很难获取类型信息,但是从技术上讲,在运行时不需要它任何对象都可以插入Map中(前提是它可以通过所有类强制转换)。

With this, we can create a relatively "ugly" (compared to the second solution) method of obtaining runtime type information through an instance present in the map. 这样,我们可以创建一个相对“丑陋”(与第二种解决方案相比)的方法,该方法通过映射中存在的实例获取运行时类型信息。 By doing so, regardless of how many sets you embed and what the resultant "type" is present after erasure, we can guarantee that an instance of it will be insertable back into the original map. 这样,无论您嵌入多少集以及擦除后出现的结果“类型”如何,我们都可以保证可以将其实例重新插入到原始映射中。

Demonstrated below: 如下所示:

// Java 7 approach
private <T> Map<String, byte[]> m(Map<String, T> data){
    Class valueType = null;

    Iterator<T> valueIterator = data.values().iterator();

    while(valueIterator.hasNext()){
        T nextCandidate = valueIterator.next();

        if(nextCandidate != null){
            valueType = nextCandidate.getClass();
            break;
        }
    }

    if(valueType == null){
        // No instance present, fail
        return null;
    }

    // Create a new instance
    T obj = (T) valueType.newInstance(); // Exception handling not shown

    // Rest of code here

    return null;
}

as seen, the type information is extracted directly from the first non-null value present within the map. 如图所示,类型信息是直接从映射中存在的第一个非空值中提取的。 Under java 8 we can do better using streams: 在Java 8下,我们可以更好地使用流:

// Java 8 approach
private <T> Map<String, byte[]> m(Map<String, T> data){
    // Note: use findFirst() for more consistent behaviour
    Optional<T> optInstance = data.values().stream().filter(Objects::nonNull).findAny();

    if(!optInstance.isPresent()){
        // No instance present, fail
        return null;
    }

    Class valueType = optInstance.get().getClass();

    // Create a new instance
    T obj = (T) valueType.newInstance(); // Exception handling not shown

    // Rest of code here

    return null;
}

However, this solution has a couple of limitations. 但是,此解决方案有两个限制。 As stated, the map has to contain at least one non-null value for the operation to be successful. 如上所述,该映射必须包含至少一个非null值才能使操作成功。 And secondly, this solution doesn't take account of subclassing of the declared type (? extends T) on specific elements which may provide to be problematic if you have elements of different classes (eg TreeSet and HashSet within the same map). 其次,此解决方案未考虑特定元素上声明类型的子类化(? extends T) ,如果您具有不同类的元素(例如,同一映射中的TreeSetHashSet (? extends T) ,可能会带来问题。

The second issue can be solved easily by dealing with type information on a key-value pair basis rather on a "whole" map basis though this comes at the cost of "knowing" the type information for all elements within the map. 通过以键-值对为基础而不是以“整个”映射为基础处理类型信息,可以轻松解决第二个问题,尽管这是以“知道”映射中所有元素的类型信息为代价的。 Alternatively, more complex solutions such as devising the most specific common superclass to all non-null values within the map can also be used, but for all intents and purposes, this becomes more of a guesstimate solution than a real one. 或者,也可以使用更复杂的解决方案,例如为映射中的所有非空值设计最特定的公共超类,但是出于所有意图和目的,这比实际解决方案更像是一种猜测性解决方案。


The second solution to this problem is, in my opinion, a lot cleaner but poses additional complexity to the caller. 我认为,解决此问题的第二种方法是清洁得多,但给调用者带来了额外的复杂性。 This approach follows a more functional approach and can be applied if there are only a limited number of type-dependent operations within the method. 此方法遵循功能更强的方法,并且如果该方法中仅存在有限数量的类型相关操作,则可以应用此方法。 Following your proposed case of instantiation of the generic type T, we can modify the method as follows: 根据您提出的实例化通用类型T的实例,我们可以对方法进行如下修改:

private <T> Map<String, byte[]> m(Map<String, T> data, Callable<T> creator){
    // Create a new instance
    T obj = creator.call(); // Exception handling not shown

    // Rest of code here

    return null;
}

and called as follows: 并调用如下:

Map<String, Set<String>> data = new HashMap<>();

// Instantiation method set to new HashSet (thanks to bayou.io for HashSet::new)
m(data, HashSet::new); // Note: replace with anonymous inner class for java 7

in this case, the type information (which is present at the level of the caller) can be bypassed by having the caller provide the type-dependent functionality required. 在这种情况下,可以通过让呼叫者提供所需的与类型有关的功能来绕过类型信息(存在于呼叫者级别)。 The example provides a basic HashSet creation for all values but more complex instantiation rules can be defined on a per-element basis. 该示例为所有值提供了基本的HashSet创建,但是可以在每个元素的基础上定义更复杂的实例化规则。

The downside to this approach is that it provides complexity to the caller and can be very bad if this were to be an external API function (though the use of private within your original method suggests otherwise). 这种方法的缺点是,它给调用者带来了复杂性,如果这是一个外部API函数,则可能会非常糟糕(尽管在原始方法中使用private会另行说明)。 Java 7 and below also causes quite a bit of boilerplate anonymous inner class code to pop up making caller-side code harder to read. Java 7及更低版本还导致弹出很多样板化匿名内部类代码,从而使调用方代码更难以阅读。 Additionally, if most of your method requires type-information to be present then this solution is less feasible as well (since you'd be reprogramming most of your method on a per-type basis, defeating the point of using generics). 另外,如果您的大多数方法都要求提供类型信息,那么该解决方案也不太可行(因为您将在每个类型的基础上对大多数方法进行重新编程,这使使用泛型的观点大打折扣)。


In all, I'd personally prefer to use the second approach if possible, only using the first approach if deemed infeasible. 总之,如果可能的话,我个人更愿意使用第二种方法,仅在认为不可行时才使用第一种方法。 The gist of the solutions I'm getting at here is to not rely on type information when dealing with generics or at least set a bound such that you get functionality you require without ugly hacks. 我在这里遇到的解决方案的要点是,在处理泛型时不要依赖类型信息,或者至少要设置一个界限,以使您获得所需的功能而不会受到难看的黑客攻击。 In the case where type-dependent operations have to be performed, have the caller provide the functionality for that (through Callables, Runnables or some FunctionalInterface of your creation). 在必须执行与类型相关的操作的情况下,请调用方为此提供功能(通过Callables,Runnables或创建的某些FunctionalInterface )。

If type information is absolutely critical for some reason not made apparent, I suggest reading this article to stop type erasure altogether, allowing type information to be present directly from within the method. 如果由于某种原因而导致类型信息绝对关键,但我不建议您阅读这篇文章以完全停止类型擦除,允许类型信息直接从方法中显示。

The following signature works with super keyword. 以下签名与super关键字一起使用。 (I tested with Java7) (我使用Java7进行了测试)

private <T> Map<String, byte[]> m(Map<String, T> data, Class<? super T> type)

Map<String, Set<String>> abc = null;
m(abc, Set.class);

This is subtyping for generics. 这是泛型的子类型。

You'd need to do it like : 您需要这样做:

Map<String, Set> abc = null;  //gives a compiler warning
m(abc, Set.class)

The issue is that if you want T to be captured to Set<String> , there will be no way to express Class<T> since there's no such thing as Set<String>.class , just Set.class . 问题是,如果您希望将T捕获到Set<String> ,则将无法表达Class<T>因为没有诸如Set<String>.class这样的东西,只有Set.class

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM