web-dev-qa-db-fra.com

Vous évitez l'attribution non contrôlée dans une carte avec plusieurs types de valeur?

Je rencontre des problèmes avec un avertissement dans Java 7:

Unchecked assignment: 'Java.lang.Class' to 'Java.lang.Class<T>'

Je l'obtiens sur la ligne Class<T> type = typeMap.get(key); dans la fonction get ci-dessous.

Fondamentalement, ce que j'essaie de faire ici, c'est que je veux stocker un tas de paires clé/valeur de types inconnus (mais tous sont des descendants d'Object à l'exception de null), mais ne pas perdre le type. J'ai donc créé une classe avec le contenu suivant en utilisant des génériques. Il a deux cartes (une pour stocker les données et une pour stocker le type de classe:

    private Map<String, Object>  dataMap = new HashMap<>();
    private Map<String, Class> typeMap = new HashMap<>();

    public  <T> void put(String key, T instance){
        dataMap.put(key, instance);
        if (instance == null){
            typeMap.put(key,null);
        }
        else {
            typeMap.put(key, instance.getClass());
        }
    }

    public <T> T get(String key){
        Class<T> type = typeMap.get(key);
        if (type == null){
            return null;
        }
        return type.cast(dataMap.get(key));
    }

Cela fonctionne très bien, mais l'avertissement me dérange. Existe-t-il un moyen pour que Java effectue cette conversion sans se plaindre (autre que la supprimer)? Ou existe-t-il une meilleure façon d'accomplir ce que j'essaie de faire? Que diriez-vous de Java 8 car je n'ai pas encore vraiment eu l'occasion d'y plonger?

Merci!

28
irotsoma

La raison pour laquelle vous avez montré que ce n'est pas sûr est qu'avec cette affectation:

Class<T> type = typeMap.get(key);

T n'a rien à voir avec le Class récupéré de la carte. T est toujours déduit du contexte environnant de l'appel à get. Par exemple, je peux faire cette séquence d'appels:

// T is inferred from the arguments as String (which is fine)
example.put("k", "v");
// T is inferred from the return value target type as Integer
Integer i = example.get("k");

Dans la méthode get, String.class Est correctement récupéré de la mappe de type, mais une conversion non vérifiée est effectuée en Class<Integer>. L'appel à type.cast(...) n'est pas lancé, car la valeur extraite de la carte de données est un String. Une conversion implicite vérifiée puis en fait arrive à la valeur de retour , en la castant à Integer et un ClassCastException est lancé.

Cette étrange interaction est due à effacement de type .


Ainsi, lorsque nous stockons plusieurs types dans une même structure de données, il existe un certain nombre de façons de l'aborder, selon nos besoins.

1. Nous pouvons renoncer aux vérifications de compilation s'il n'y a aucun moyen de les effectuer.

Stocker le Class est inutile pour la plupart ici car, comme je l'ai montré ci-dessus, il n'effectue pas de validation utile. Nous pourrions donc repenser la carte selon les lignes suivantes:

class Example {
    private final Map<String, Object> m = new HashMap<>();

    void put(String k, Object v) {
        m.put(k, v);
    }

    Object getExplicit(String k) {
        return m.get(k);
    }

    @SuppressWarnings("unchecked")
    <T> T getImplicit(String k) {
        return (T) m.get(k);
    }
}

getExplicit et getImplicit font la même chose mais:

String a = (String) example.getExplicit("k");
// the generic version allows an implicit cast to be made
// (this is essentially what you're already doing)
String b = example.getImplicit("k");

Dans les deux cas, nous comptons simplement sur notre propre conscience de programmeur pour ne pas commettre d'erreurs.

Supprimer les avertissements n'est pas nécessairement mauvais, il est juste important de comprendre ce qu'ils signifient réellement et quelles sont les implications.

2. Passez un Class à get pour que la valeur renvoyée soit valide.

C'est comme ça que je l'ai vu faire typiquement.

class Example {
    private final Map<String, Object> m = new HashMap<>();

    void put(String k, Object v) {
        m.put(k, v);
    }

    <T> T get(String k, Class<T> c) {
        Object v = m.get(k);
        return c.isInstance(v) ? c.cast(v) : null;
    }
}

example.put("k", "v");
// returns "v"
String s = example.get("k", String.class);
// returns null
Double d = example.get("k", Double.class);

Mais, bien sûr, cela signifie que nous devons passer deux paramètres à get.

3. Paramétrez les touches.

C'est un roman mais plus avancé et il peut être plus pratique ou non.

class Example {
    private final Map<Key<?>, Object> m = new HashMap<>();

    <V> Key<V> put(String s, V v) {
        Key<V> k = new Key<>(s, v);
        put(k, v);
        return k;
    }

    <V> void put(Key<V> k, V v) {
        m.put(k, v);
    }

    <V> V get(Key<V> k) {
        Object v = m.get(k);
        return k.c.isInstance(v) ? k.c.cast(v) : null;
    }

    static final class Key<V> {
        private final String k;
        private final Class<? extends V> c;

        @SuppressWarnings("unchecked")
        Key(String k, V v) {
            // this cast will always be safe unless
            // the outside world is doing something fishy
            // like using raw types
            this(k, (Class<? extends V>) v.getClass());
        }

        Key(String k, Class<? extends V> c) {
            this.k = k;
            this.c = c;
        }

        @Override
        public int hashCode() {
            return k.hashCode();
        }

        @Override
        public boolean equals(Object o) {
            return (o instanceof Key<?>) && ((Key<?>) o).k.equals(k);
        }
    }
}

Donc par exemple:

Key<Float> k = example.put("k", 1.0f);
// returns 1.0f
Float f = example.get(k);
// returns null
Double d = example.get(new Key<>("k", Double.class));

Cela pourrait avoir un sens si les entrées sont connues ou prévisibles afin que nous puissions avoir quelque chose comme:

final class Keys {
    private Keys() {}
    static final Key<Foo> FOO = new Key<>("foo", Foo.class);
    static final Key<Bar> BAR = new Key<>("bar", Bar.class);
}

Ensuite, nous n'avons pas à construire un objet clé chaque fois qu'une récupération est effectuée. Cela fonctionne très bien, en particulier pour ajouter un typage fort aux scénarios de type chaîne.

Foo foo = example.get(Keys.FOO);

4. N'ayez pas de carte, n'importe quel type d'objet peut être inséré, utilisez un polymorphisme quelconque.

Lorsque c'est possible et pas trop encombrant, c'est une bonne option. S'il y a un comportement commun pour lequel les différents types sont utilisés, faites-en une interface ou une superclasse afin que nous n'ayons pas à utiliser la conversion.

Un exemple simple pourrait ressembler à ceci:

// bunch of stuff
Map<String, Object> map = ...;

// store some data
map.put("abc", 123L);
map.put("def", 456D);

// wait awhile
awhile();

// some time later, consume the data
// being particular about types
consumeLong((Long) map.remove("abc"));
consumeDouble((Double) map.remove("def"));

Et nous pourrions plutôt remplacer quelque chose comme ceci:

Map<String, Runnable> map = ...;

// store operations as well as data
// while we know what the types are
map.put("abc", () -> consumeLong(123L));
map.put("def", () -> consumeDouble(456D));

awhile();

// consume, but no longer particular about types
map.remove("abc").run();
map.remove("def").run();
34
Radiodef

Vous essayez d'affecter un élément de type Class à une variable de type Class<T>. Bien sûr, c'est une tâche non contrôlée. Vous semblez implémenter une carte hétérogène. Java (et tout autre langage fortement typé) n'a aucun moyen d'exprimer le type de valeur de votre carte d'une manière sûre de type statique.

En effet, les types d'éléments ne sont connus qu'au moment de l'exécution. Attendre du compilateur qu'il vérifie statiquement les choses qui ne sont pas encore connues en demande trop. Le compilateur ne peut même pas faire d'inférence de type statique raisonnable, donc s'attendre à ce qu'il prédise l'avenir de l'inférence de type dynamique est vraiment un tronçon.

4
Judge Mental