web-dev-qa-db-fra.com

Java generics - Rendre générique pour étendre 2 interfaces

Comment faites-vous cela:

public class Frankenstein<T extends IHuman, IMonster>{
}

Sans faire

public interface Weirdo extends Ihuman, IMonster{
}

Modifier

Pourquoi ça ne marche pas?

public <T> void mapThis(
        Class<? extends MyClass<T>> key, Class<? extends T & IDisposable> value) {

}

Je reçois le message du compilateur marquant Class<? extends T & IDisposable> comme une erreur.

42
Ilya Gazman

Reimeus a déjà souligné que ce que vous demandez dans votre montage n'est pas possible. Je voudrais juste expliquer un peu pourquoi.

On pourrait penser que vous pourriez utiliser ce qui suit:

public <T, U extends T & IDisposable> void mapThis(
        Class<? extends MyClass<T>> key,
        Class<? extends U> value
) { ... }

En fait, c'est ce qui m'est venu à l'esprit lorsque j'ai vu ce post pour la première fois. Mais cela donne en fait une erreur de compilation:

une variable de type ne peut pas être suivie par d'autres bornes

Pour m'aider à expliquer pourquoi, je voudrais citer un Oracle Blogs post par Victor Rudometov à propos de cette erreur:

Ce fait n'est pas toujours clair, mais il est vrai. Le code suivant ne doit pas être compilé:

interface I {}

class TestBounds <U, T extends U & I> {

}

Étant donné que JLS Chapitre 4 Types, valeurs et variables section 4.4 Type Variables indique: " La limite se compose de soit une variable de type, soit un classe ou type d'interface T éventuellement suivi d'autres types d'interface I1 , ..., JEn. ". On peut donc utiliser T étend U, T étend SomeClass & I, mais pas T étend U & I. Cette règle s'applique à tous les cas, y compris les variables de type et les limites dans les méthodes et les constructeurs.

Les raisons de cette restriction sont explorées dans un article étroitement lié: Pourquoi ne puis-je pas utiliser un argument de type dans un paramètre de type à plusieurs bornes?

Pour résumer, la restriction a été imposée afin "d'empêcher certaines situations délicates de voir le jour" ( JLS §4.9 ).

Quel genre de situations délicates? ne réponse de Chris Povirk en décrit un:

[Une raison de la restriction est] la possibilité de spécifier des types illégaux. Plus précisément, l'extension d'une interface générique deux fois avec des paramètres différents. Je ne peux pas trouver un exemple non artificiel, mais:

/** Contains a Comparator<String> that also implements the given type T. */
class StringComparatorHolder<T, C extends T & Comparator<String>> {
  private final C comparator;
  // ...
}

void foo(StringComparatorHolder<Comparator<Integer>, ?> holder) { ... }

Maintenant, holder.comparator Est un Comparator<Integer> Et un Comparator<String>.

Chris indique également bogue Sun 4899305 , qui était un bogue contestant cette restriction de langue. Il a été fermé en tant que Won't Fix avec le commentaire suivant:

Si une variable de type pouvait être suivie de variables de type ou d'interfaces (éventuellement paramétrées), il y aurait probablement plus de variables de type mutuellement récursives, qui sont très difficiles à gérer. Les choses sont déjà compliquées quand une borne est simplement un type paramétré, par ex. <S,R extends Comparable<S>>. Par conséquent, les limites ne vont pas changer maintenant. Javac et Eclipse conviennent que S&T Et S&Comparable<S> Sont illégaux.

Voilà donc les raisons de la restriction. En ce qui concerne spécifiquement les méthodes génériques (ce qui concerne votre question), je voudrais en outre souligner que l'inférence de type rendrait théoriquement ces limites inutiles de toute façon.

Si nous réexaminons les paramètres de type déclarés dans la signature hypothétique ci-dessus:

<T, U extends T & IDisposable>

En supposant que l'appelant ne spécifie pas explicitement T et U, cela peut être réduit comme suit:

<T, U extends Object & IDisposable>

Ou juste ceci (différence subtile, mais c'est n autre sujet ):

<T, U extends IDisposable>

C'est parce que T n'a pas de limites, donc quel que soit le type d'arguments transmis, T peut toujours se résoudre en Object au minimum, et ainsi peut alors U.

Revenons en arrière et disons que T est borné:

<T extends Foo, U extends T & IDisposable>

Cela peut être réduit de la même manière (Foo peut être une classe ou une interface):

<T extends Foo, U extends Foo & IDisposable>

Sur la base de ce raisonnement, la syntaxe que vous essayez d'atteindre est inutile en ce qui concerne la limitation de l'appelant à des arguments plus spécifiques.

Addendum pré-Java 8:

Avant Java 8, il y a est un cas d'utilisation pour ce que vous essayez de faire. En raison d'une limitation de la façon dont le compilateur infère les paramètres de type de méthode générique, mon raisonnement ci-dessus pour sortir de la fenêtre. Prenez la méthode générique suivante:

class MyClass {
    static <T> void foo(T t1, T t2) { }
}

C'est une erreur courante pour les débutants d'essayer de créer une méthode qui prend deux paramètres du "même type". Bien sûr, c'est inutile à cause du fonctionnement de l'héritage:

MyClass.foo("asdf", 42); // legal

Ici, T est supposé être Object - cela correspond au raisonnement précédent sur la simplification des paramètres de type mapThis. Vous devez spécifier manuellement les paramètres de type afin de réaliser la vérification de type souhaitée:

MyClass.<String>foo("asdf", 42); // compiler error

Cependant, et voici où commence votre cas d'utilisation, c'est une autre affaire avec plusieurs paramètres de type avec des limites décalées:

class MyClass {
    static <T, U extends T> void foo(T t, U u) { }
}

Maintenant, cet appel erreurs:

MyClass.foo("asdf", 42); // compiler error

Les tables ont tourné - nous devons assouplir manuellement les paramètres de type pour le faire compiler:

MyClass.<Object, Object>foo("asdf", 42); // legal

Cela se produit en raison de la manière limitée dont le compilateur déduit les paramètres de type de méthode. Pour cette raison, ce que vous vouliez réaliser aurait en fait eu une application pour restreindre les arguments de l'appelant.

Cependant, ce problème semble avoir été corrigé dans Java 8, et MyClass.foo("asdf", 42) compile maintenant sans aucune erreur (merci à Regent de l'avoir signalé).

102
Paul Bellora

Je pensais juste partager mon hack que j'utilise dans ces situations (assez rares):

    /**
     * This is a type-checking method, which gets around Java's inability
     * to handle multiple bounds such as "V extends T & ContextAware<T>".
     *
     * @param value initial value, which should extends T
     * @param contextAware initial value, which should extend ContextAware<T>
     * @return a new proxy object
     */
    public T blah(T value, ContextAware<T> contextAware) {
        if (value != contextAware) {
            throw new IllegalArgumentException("This method expects the same object for both parameters.");
        }

        return blah(value);
    }

Donc, en exigeant le même objet pour chacune des limites que vous essayez de satisfaire, vous obtenez la vérification du type à la compilation et l'objet unique qui fait tout. Certes, c'est un peu idiot de passer le même objet pour chaque paramètre, mais je le fais dans mon code `` interne '' en toute sécurité et confortablement.

3
Ben