繁体   English   中英

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

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

我有这样的签名的方法:

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

例如,当我这样调用时,它运行良好:

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

但是,当我的参数T是Set时,它不起作用:

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

有办法使它起作用吗?

您将必须使用如下所示的未经检查的演员表来做一些非常丑陋的事情:

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

这归结为类型擦除。 在运行时, Class<Set<String>>Class<Set<Integer>> ,因为我们没有统一的泛型,因此无法知道您拥有的是“ Set of字符串”和“整数集”的类。

不久前我问了一个相关的问题,该问题也应该给您一些提示:

IMO之所以感到困惑,是因为事实是,泛型是在事实发生之后才被螺栓固定的,因此并未得到具体化。 当编译器告诉您泛型类型不匹配时,我认为这是一种语言的失败,但是您甚至没有简单的方法来表示该特定类型。 例如,在您的情况下,您最终会遇到编译时错误:

        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>)

现在,您认为“哦,那我应该使用Set<String>.class ”将是完全合理的,但这是不合法的。 这是从泛型语言实现中的抽象泄漏,特别是泛型易受类型擦除。 在语义上, Set<String>.class表示一组字符串的运行时类实例。 但是实际上,在运行时我们不能表示一组字符串的运行时类,因为它与包含任何其他类型的对象的集合是无法区分的。

因此,我们有一个与编译时语义不一致的运行时语义,要知道Set<T>.class 为什么不合法,就需要知道泛型在运行时不会得到验证。 这种不匹配是导致出现此类奇怪变通办法的原因。

使问题复杂化的是,类实例最终也被与类型令牌混合在一起。 由于您在运行时无法访问通用参数的类型,因此解决方法是传入Class<T>类型的参数。 从表面上看,这非常String.class ,因为您可以传递String.classClass<String>东西(类型为Class<String> ),并且编译器很高兴。 但是这种方法在您的情况下很糟糕:如果T 本身代表具有自己的泛型类型参数的类型怎么办? 现在将类用作类型标记没有用,因为无法区分Class<Set<String>>Class<Set<Integer>>因为从根本上说,它们在运行时都是Set.class ,因此共享同一个类实例。 因此,将类用作运行时类型令牌的IMO不能用作常规解决方案。

由于该语言的缺点,因此有些库使检索通用类型信息非常容易。 此外,它们还提供了更擅长表示事物“类型”的类:

从我的角度来看,有两个潜在的解决方案,它们都有各自的局限性。

第一个解决方案依赖于Java的类型擦除已完成的事实,这意味着无论“深度”如何,所有参数化类型的类型都将被擦除。 例如: Map<String, Set<String>将被简化为Map<String, Set> ,然后是Map<Object, Object>这意味着尽管很难获取类型信息,但是从技术上讲,在运行时不需要它任何对象都可以插入Map中(前提是它可以通过所有类强制转换)。

这样,我们可以创建一个相对“丑陋”(与第二种解决方案相比)的方法,该方法通过映射中存在的实例获取运行时类型信息。 这样,无论您嵌入多少集以及擦除后出现的结果“类型”如何,我们都可以保证可以将其实例重新插入到原始映射中。

如下所示:

// 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;
}

如图所示,类型信息是直接从映射中存在的第一个非空值中提取的。 在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;
}

但是,此解决方案有两个限制。 如上所述,该映射必须包含至少一个非null值才能使操作成功。 其次,此解决方案未考虑特定元素上声明类型的子类化(? extends T) ,如果您具有不同类的元素(例如,同一映射中的TreeSetHashSet (? extends T) ,可能会带来问题。

通过以键-值对为基础而不是以“整个”映射为基础处理类型信息,可以轻松解决第二个问题,尽管这是以“知道”映射中所有元素的类型信息为代价的。 或者,也可以使用更复杂的解决方案,例如为映射中的所有非空值设计最特定的公共超类,但是出于所有意图和目的,这比实际解决方案更像是一种猜测性解决方案。


我认为,解决此问题的第二种方法是清洁得多,但给调用者带来了额外的复杂性。 此方法遵循功能更强的方法,并且如果该方法中仅存在有限数量的类型相关操作,则可以应用此方法。 根据您提出的实例化通用类型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;
}

并调用如下:

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

在这种情况下,可以通过让呼叫者提供所需的与类型有关的功能来绕过类型信息(存在于呼叫者级别)。 该示例为所有值提供了基本的HashSet创建,但是可以在每个元素的基础上定义更复杂的实例化规则。

这种方法的缺点是,它给调用者带来了复杂性,如果这是一个外部API函数,则可能会非常糟糕(尽管在原始方法中使用private会另行说明)。 Java 7及更低版本还导致弹出很多样板化匿名内部类代码,从而使调用方代码更难以阅读。 另外,如果您的大多数方法都要求提供类型信息,那么该解决方案也不太可行(因为您将在每个类型的基础上对大多数方法进行重新编程,这使使用泛型的观点大打折扣)。


总之,如果可能的话,我个人更愿意使用第二种方法,仅在认为不可行时才使用第一种方法。 我在这里遇到的解决方案的要点是,在处理泛型时不要依赖类型信息,或者至少要设置一个界限,以使您获得所需的功能而不会受到难看的黑客攻击。 在必须执行与类型相关的操作的情况下,请调用方为此提供功能(通过Callables,Runnables或创建的某些FunctionalInterface )。

如果由于某种原因而导致类型信息绝对关键,但我不建议您阅读这篇文章以完全停止类型擦除,允许类型信息直接从方法中显示。

以下签名与super关键字一起使用。 (我使用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);

这是泛型的子类型。

您需要这样做:

Map<String, Set> abc = null;  //gives a compiler warning
m(abc, 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