web-dev-qa-db-fra.com

Comment puis-je traiter les avertissements de distribution non contrôlés?

Eclipse me prévient du formulaire suivant:

Type safety: transtypage non contrôlé d'objet à HashMap

Cela provient d'un appel à une API sur laquelle je n'ai aucun contrôle sur lequel retourne Object:

HashMap<String, String> getItems(javax.servlet.http.HttpSession session) {
  HashMap<String, String> theHash = (HashMap<String, String>)session.getAttribute("attributeKey");
  return theHash;
}

J'aimerais éviter les avertissements Eclipse, si possible, car ils indiquent théoriquement au moins un problème de code potentiel. Je n'ai cependant pas encore trouvé le moyen d'éliminer celui-ci. Je peux extraire la ligne impliquée dans une méthode par elle-même et ajouter @SuppressWarnings("unchecked") à cette méthode, limitant ainsi l'impact d'un bloc de code dans lequel j'ignore les avertissements. De meilleures options? Je ne veux pas désactiver ces avertissements dans Eclipse.

Avant d’arriver au code, c’était plus simple, mais cela provoquait toujours des avertissements:

HashMap getItems(javax.servlet.http.HttpSession session) {
  HashMap theHash = (HashMap)session.getAttribute("attributeKey");
  return theHash;
}

Le problème était ailleurs lorsque vous avez essayé d'utiliser le hash, vous auriez des avertissements:

HashMap items = getItems(session);
items.put("this", "that");

Type safety: The method put(Object, Object) belongs to the raw type HashMap.  References to generic type HashMap<K,V> should be parameterized.
562
skiphoppy

Sensationnel; Je pense avoir compris la réponse à ma propre question. Je ne suis pas sûr que ça en vaut la peine! :)

Le problème est que la distribution n'est pas vérifiée. Donc, vous devez vérifier vous-même. Vous ne pouvez pas simplement vérifier un type paramétré avec instanceof, car les informations sur le type paramétré ne sont pas disponibles au moment de l'exécution, elles ont été effacées lors de la compilation.

Cependant, vous pouvez effectuer une vérification sur chaque élément du hachage, avec instanceof, et créer ainsi un nouveau hachage de type sûr. Et vous ne provoquerez aucun avertissement.

Grâce à mmyers et à Esko Luontola, j'ai paramétré le code que j'ai écrit à l'origine ici. Il peut donc être encapsulé dans une classe d'utilitaires quelque part et utilisé pour n'importe quelle HashMap paramétrée. Si vous voulez mieux comprendre et que vous n'êtes pas très familier avec les génériques, je vous encourage à consulter l'historique des modifications de cette réponse.

public static <K, V> HashMap<K, V> castHash(HashMap input,
                                            Class<K> keyClass,
                                            Class<V> valueClass) {
  HashMap<K, V> output = new HashMap<K, V>();
  if (input == null)
      return output;
  for (Object key: input.keySet().toArray()) {
    if ((key == null) || (keyClass.isAssignableFrom(key.getClass()))) {
        Object value = input.get(key);
        if ((value == null) || (valueClass.isAssignableFrom(value.getClass()))) {
            K k = keyClass.cast(key);
            V v = valueClass.cast(value);
            output.put(k, v);
        } else {
            throw new AssertionError(
                "Cannot cast to HashMap<"+ keyClass.getSimpleName()
                +", "+ valueClass.getSimpleName() +">"
                +", value "+ value +" is not a "+ valueClass.getSimpleName()
            );
        }
    } else {
        throw new AssertionError(
            "Cannot cast to HashMap<"+ keyClass.getSimpleName()
            +", "+ valueClass.getSimpleName() +">"
            +", key "+ key +" is not a " + keyClass.getSimpleName()
        );
    }
  }
  return output;
}

C'est beaucoup de travail, peut-être pour très peu de récompense ... Je ne suis pas sûr de l'utiliser ou non. J'aimerais savoir si les gens pensent que cela en vaut la peine ou pas. J'apprécierais également les suggestions d'amélioration: puis-je faire autre chose en plus de lancer AssertionErrors? Y a-t-il quelque chose de mieux que je pourrais lancer? Dois-je en faire une exception cochée?

110
skiphoppy

La réponse évidente, bien sûr, est de ne pas faire la distribution non contrôlée.

Si c'est absolument nécessaire, essayez au moins de limiter la portée de l'annotation @SuppressWarnings. Selon son Javadocs , il peut utiliser des variables locales; De cette façon, cela n’affecte même pas toute la méthode.

Exemple:

@SuppressWarnings("unchecked")
Map<String, String> myMap = (Map<String, String>) deserializeMap();

Il n'y a aucun moyen de déterminer si la Map devrait avoir les paramètres génériques <String, String>. Vous devez savoir à l'avance quels paramètres doivent être (ou vous saurez quand vous obtenez un ClassCastException). C'est pourquoi le code génère un avertissement, car le compilateur ne peut pas savoir s'il est sécurisé.

519
Michael Myers

Malheureusement, il n'y a pas de grandes options ici. N'oubliez pas que le but de tout cela est de préserver la sécurité du type. " Java Generics " offre une solution pour traiter les bibliothèques héritées non génériques, et il en existe une en particulier appelée "technique de boucle vide" dans la section section 8.2. Fondamentalement, créez la distribution non sécurisée et supprimez l’avertissement. Parcourez ensuite la carte comme ceci:

@SuppressWarnings("unchecked")
Map<String, Number> map = getMap();
for (String s : map.keySet());
for (Number n : map.values());

Si un type inattendu est rencontré, vous obtiendrez une exception ClassCastException au moment de l'exécution, mais au moins cela se produira près de la source du problème.

147
Julien Chastang

Dans les préférences Eclipse, accédez à Java-> Compilateur-> Erreurs/Avertissements-> Types génériques et cochez la case Ignore unavoidable generic type problems.

Cela répond à l’esprit de la question, c’est-à-dire.

J'aimerais éviter les avertissements Eclipse ...

sinon l'esprit.

49
Dave

Vous pouvez créer une classe d’utilitaires comme celle-ci et l’utiliser pour supprimer l’avertissement non contrôlé.

public class Objects {

    /**
     * Helps to avoid using {@code @SuppressWarnings({"unchecked"})} when casting to a generic type.
     */
    @SuppressWarnings({"unchecked"})
    public static <T> T uncheckedCast(Object obj) {
        return (T) obj;
    }
}

Vous pouvez l'utiliser comme suit:

import static Objects.uncheckedCast;
...

HashMap<String, String> getItems(javax.servlet.http.HttpSession session) {
      return uncheckedCast(session.getAttribute("attributeKey"));
}

Un peu plus de discussion à ce sujet est ici: http://cleveralias.blogs.com/thought_spearmints/2006/01/suppresswarning.html

26
Esko Luontola

Ce truc est difficile, mais voici mes pensées actuelles:

Si votre API renvoie Object, vous ne pouvez rien faire. Quoi qu'il en soit, vous allez lancer l'objet à l'aveuglette. Vous laissez Java lancer ClassCastExceptions, ou vous pouvez vérifier vous-même chaque élément et lancer Assertions ou IllegalArgumentExceptions ou autres, mais ces contrôles runtime sont tous équivalents. Vous devez supprimer la distribution compilation non vérifiée, peu importe ce que vous faites au moment de l'exécution.

Je préférerais simplement que la machine virtuelle soit masquée et que la machine virtuelle Java effectue sa vérification d'exécution pour moi, car nous "savons" ce que l'API doit renvoyer et sommes généralement disposés à supposer que cette dernière fonctionne. Utilisez des génériques partout au-dessus de la distribution, si vous en avez besoin. Vous n'achetez rien, car vous disposez toujours de la distribution à simple insu, mais vous pouvez au moins utiliser des génériques à partir de ce moment pour que la JVM puisse vous aider à éviter les conversions à l'aveugle dans d'autres parties de votre code.

Dans ce cas particulier, on peut supposer que vous pouvez voir l'appel à SetAttribute et voir que le type entre en jeu. Il ne suffit donc pas de lui attribuer le même type à la sortie, ce qui n'est pas immoral. Ajoutez un commentaire référençant SetAttribute et faites-le avec.

20
Dustin Getz

Dans le monde de la session HTTP, vous ne pouvez pas vraiment éviter le transtypage, car l'API est écrite de cette façon (prend et renvoie uniquement Object).

Avec un peu de travail, vous pouvez facilement éviter les acteurs non contrôlés, cependant. Cela signifie qu’il se transformera en une distribution traditionnelle donnant un ClassCastException là en cas d’erreur. Une exception non contrôlée pourrait se transformer en CCE à tout moment par la suite au lieu du moment de la diffusion (c’est la raison pour laquelle il s’agit d’un avertissement séparé).

Remplacez HashMap par une classe dédiée:

import Java.util.AbstractMap;
import Java.util.Collection;
import Java.util.HashMap;
import Java.util.Map;
import Java.util.Set;

public class Attributes extends AbstractMap<String, String> {
    final Map<String, String> content = new HashMap<String, String>();

    @Override
    public Set<Map.Entry<String, String>> entrySet() {
        return content.entrySet();
    }

    @Override
    public Set<String> keySet() {
        return content.keySet();
    }

    @Override
    public Collection<String> values() {
        return content.values();
    }

    @Override
    public String put(final String key, final String value) {
        return content.put(key, value);
    }
}

Puis lancez dans cette classe au lieu de Map<String,String> et tout sera vérifié à l’endroit exact où vous écrivez votre code. Pas d'inattendu ClassCastExceptions plus tard.

12
Joachim Sauer

Voici un raccourci exemple qui évite l'avertissement "distribution non contrôlée" en utilisant deux stratégies mentionnées dans d'autres réponses.

  1. Transmettez la classe du type d'intérêt en tant que paramètre à l'exécution (Class<T> inputElementClazz). Ensuite, vous pouvez utiliser: inputElementClazz.cast(anyObject);

  2. Pour le transtypage d'une collection, utilisez le caractère générique? au lieu d’un type générique T, vous devez reconnaître que vous ne savez vraiment pas quel type d’objet vous attend du code hérité (Collection<?> unknownTypeCollection). Après tout, voici ce que l'avertissement de "distribution non contrôlée" veut nous dire: nous ne pouvons pas être sûrs d'obtenir un Collection<T>, donc la chose honnête à faire est d'utiliser un Collection<?>. Si absolument nécessaire, une collection d'un type connu peut toujours être construite (Collection<T> knownTypeCollection).

Le code hérité interfacé dans l'exemple ci-dessous a un attribut "entrée" dans StructuredViewer (StructuredViewer est un widget d'arborescence ou de table, "entrée" étant le modèle de données qui le sous-tend). Cette "entrée" peut être n’importe quel type de Collection Java.

public void dragFinished(StructuredViewer structuredViewer, Class<T> inputElementClazz) {
    IStructuredSelection selection = (IStructuredSelection) structuredViewer.getSelection();
    // legacy code returns an Object from getFirstElement,
    // the developer knows/hopes it is of type inputElementClazz, but the compiler cannot know
    T firstElement = inputElementClazz.cast(selection.getFirstElement());

    // legacy code returns an object from getInput, so we deal with it as a Collection<?>
    Collection<?> unknownTypeCollection = (Collection<?>) structuredViewer.getInput();

    // for some operations we do not even need a collection with known types
    unknownTypeCollection.remove(firstElement);

    // nothing prevents us from building a Collection of a known type, should we really need one
    Collection<T> knownTypeCollection = new ArrayList<T>();
    for (Object object : unknownTypeCollection) {
        T aT = inputElementClazz.cast(object);
        knownTypeCollection.add(aT);
        System.out.println(aT.getClass());
    }

    structuredViewer.refresh();
}

Naturellement, le code ci-dessus peut générer des erreurs d’exécution si nous utilisons le code hérité avec des types de données incorrects (par exemple, si nous définissons un tableau comme "entrée" de StructuredViewer au lieu d’une collection Java).

Exemple d'appel de la méthode:

dragFinishedStrategy.dragFinished(viewer, Product.class);
11
StaticNoiseLog

Dans Android Studio, si vous souhaitez désactiver l'inspection, vous pouvez utiliser:

//noinspection unchecked
Map<String, String> myMap = (Map<String, String>) deserializeMap();
8
Jan Moravec

Dans ce cas particulier, je ne stockerais pas les cartes directement dans la session HttpSession, mais plutôt une instance de ma propre classe, qui contient à son tour une carte (un détail d'implémentation de la classe). Ensuite, vous pouvez être sûr que les éléments de la carte sont du bon type.

Mais si vous voulez quand même vérifier que le contenu de la carte est de type correct, vous pouvez utiliser un code comme celui-ci:

public static void main(String[] args) {
    Map<String, Integer> map = new HashMap<String, Integer>();
    map.put("a", 1);
    map.put("b", 2);
    Object obj = map;

    Map<String, Integer> ok = safeCastMap(obj, String.class, Integer.class);
    Map<String, String> error = safeCastMap(obj, String.class, String.class);
}

@SuppressWarnings({"unchecked"})
public static <K, V> Map<K, V> safeCastMap(Object map, Class<K> keyType, Class<V> valueType) {
    checkMap(map);
    checkMapContents(keyType, valueType, (Map<?, ?>) map);
    return (Map<K, V>) map;
}

private static void checkMap(Object map) {
    checkType(Map.class, map);
}

private static <K, V> void checkMapContents(Class<K> keyType, Class<V> valueType, Map<?, ?> map) {
    for (Map.Entry<?, ?> entry : map.entrySet()) {
        checkType(keyType, entry.getKey());
        checkType(valueType, entry.getValue());
    }
}

private static <K> void checkType(Class<K> expectedType, Object obj) {
    if (!expectedType.isInstance(obj)) {
        throw new IllegalArgumentException("Expected " + expectedType + " but was " + obj.getClass() + ": " + obj);
    }
}
8
Esko Luontola

La fonction utilitaire Objects.Unchecked dans la réponse ci-dessus de Esko Luontola est un excellent moyen d’éviter l’encombrement du programme.

Si vous ne souhaitez pas utiliser SuppressWarnings sur une méthode complète, Java vous oblige à le placer sur une méthode locale. Si vous avez besoin d'une distribution sur un membre, cela peut conduire à un code comme celui-ci:

@SuppressWarnings("unchecked")
Vector<String> watchedSymbolsClone = (Vector<String>) watchedSymbols.clone();
this.watchedSymbols = watchedSymbolsClone;

L'utilisation de l'utilitaire est beaucoup plus propre et ce que vous faites est toujours évident:

this.watchedSymbols = Objects.uncheckedCast(watchedSymbols.clone());

REMARQUE: Je pense qu'il est important d'ajouter que parfois l'avertissement signifie vraiment que vous faites quelque chose de mal, comme:

ArrayList<Integer> intList = new ArrayList<Integer>();
intList.add(1);
Object intListObject = intList; 

 // this line gives an unchecked warning - but no runtime error
ArrayList<String> stringList  = (ArrayList<String>) intListObject;
System.out.println(stringList.get(0)); // cast exception will be given here

Ce que le compilateur vous dit, c'est que cette distribution ne sera PAS vérifiée au moment de l'exécution. Par conséquent, aucune erreur d'exécution ne sera générée jusqu'à ce que vous tentiez d'accéder aux données du conteneur générique.

7
Gonen I

La suppression des avertissements n'est pas une solution. Vous ne devriez pas faire un casting à deux niveaux dans une seule déclaration.

HashMap<String, String> getItems(javax.servlet.http.HttpSession session) {

    // first, cast the returned Object to generic HashMap<?,?>
    HashMap<?, ?> theHash = (HashMap<?, ?>)session.getAttribute("attributeKey");

    // next, cast every entry of the HashMap to the required type <String, String>
    HashMap<String, String> returingHash = new HashMap<>();
    for (Entry<?, ?> entry : theHash.entrySet()) {
        returingHash.put((String) entry.getKey(), (String) entry.getValue());
    }
    return returingHash;
}
5
abbas

Si je dois utiliser une API qui ne prend pas en charge Generics .., j'essaie d'isoler ces appels dans des routines d'encapsulation avec le moins de lignes possible. J'utilise ensuite l'annotation SuppressWarnings et j'ajoute également les transtypages de sécurité de type en même temps.

Ceci est juste une préférence personnelle pour garder les choses aussi propres que possible.

2
Fortyrunner

Prenez celui-ci, c'est beaucoup plus rapide que de créer un nouveau HashMap, s'il en existe déjà un, mais toujours sécurisé, car chaque élément est comparé à son type ...

@SuppressWarnings("unchecked")
public static <K, V> HashMap<K, V> toHashMap(Object input, Class<K> key, Class<V> value) {
       assert input instanceof Map : input;

       for (Map.Entry<?, ?> e : ((HashMap<?, ?>) input).entrySet()) {
           assert key.isAssignableFrom(e.getKey().getClass()) : "Map contains invalid keys";
           assert value.isAssignableFrom(e.getValue().getClass()) : "Map contains invalid values";
       }

       if (input instanceof HashMap)
           return (HashMap<K, V>) input;
       return new HashMap<K, V>((Map<K, V>) input);
    }
2
jfreundo

Si vous postez votre code, vous pouvez le deviner, mais vous avez peut-être fait quelque chose dans le style de

HashMap<String, Object> test = new HashMap();

qui produira l'avertissement quand vous devez faire

HashMap<String, Object> test = new HashMap<String, Object>();

il pourrait être intéressant de regarder

Génériques dans le Java Langage de programmation

si vous ne connaissez pas ce qui doit être fait.

2
Mark Davidson

J'ai peut-être mal compris la question (un exemple et quelques lignes voisines seraient bien Nice), mais pourquoi n'utilisez-vous pas toujours une interface appropriée (et Java5 +)? Je ne vois aucune raison pour laquelle vous voudriez un jour transtyper vers un HashMap au lieu d'un Map<KeyType,ValueType>. En fait, je ne peux pas imaginer aucun raison de définir le type d'une variable sur HashMap au lieu de Map.

Et pourquoi la source est-elle une Object? Est-ce un type de paramètre d'une collection héritée? Si tel est le cas, utilisez des génériques et spécifiez le type souhaité.

2
phihag

Voici une façon de gérer cela lorsque je remplace l'opération equals().

public abstract class Section<T extends Section> extends Element<Section<T>> {
    Object attr1;

    /**
    * Compare one section object to another.
    *
    * @param obj the object being compared with this section object
    * @return true if this section and the other section are of the same
    * sub-class of section and their component fields are the same, false
    * otherwise
    */       
    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            // this exists, but obj doesn't, so they can't be equal!
            return false;
        }

        // prepare to cast...
        Section<?> other;

        if (getClass() != obj.getClass()) {
            // looks like we're comparing apples to oranges
            return false;
        } else {
            // it must be safe to make that cast!
            other = (Section<?>) obj;
        }

        // and then I compare attributes between this and other
        return this.attr1.equals(other.attr1);
    }
}

Cela semble fonctionner dans Java 8 (même compilé avec -Xlint:unchecked)

1
Jim Daehn

Presque tous les problèmes informatiques peuvent être résolus en ajoutant un niveau d'indirection *, ou quelque chose du genre.

Introduisez donc un objet non générique de niveau supérieur à un Map. Sans contexte, cela ne semblera pas très convaincant, mais enfin:

public final class Items implements Java.io.Serializable {
    private static final long serialVersionUID = 1L;
    private Map<String,String> map;
    public Items(Map<String,String> map) {
        this.map = New.immutableMap(map);
    }
    public Map<String,String> getMap() {
        return map;
    }
    @Override public String toString() {
        return map.toString();
    }
}

public final class New {
    public static <K,V> Map<K,V> immutableMap(
        Map<? extends K, ? extends V> original
    ) {
        // ... optimise as you wish...
        return Collections.unmodifiableMap(
            new HashMap<String,String>(original)
        );
    }
}

static Map<String, String> getItems(HttpSession session) {
    Items items = (Items)
        session.getAttribute("attributeKey");
    return items.getMap();
}

* Sauf trop de niveaux d'indirection.

1

Deux manières, l’une qui évite complètement la balise, l’autre en utilisant une méthode utilitaire coquine mais agréable.
Le problème est celui des collections pré-génériques ...
Je crois que la règle de base est la suivante: "jette les objets une chose à la fois". Ce que cela signifie lorsque vous essayez d'utiliser des classes brutes dans un monde générique, c'est que, parce que vous ne savez pas ce qu'il y a dans cette carte < ?,?> (et la JVM pourrait même s’apercevoir que ce n’est même pas une carte!), il est évident que vous ne pouvez pas la lancer. Si vous aviez Map <String,?> Map2 puis HashSet <String> keys = (HashSet <String>), map2.keySet () ne vous avertit pas, même si cela constitue un "acte de foi" pour le compilateur (car il pourrait s’avérer être un TreeSet) ... mais ce n’est qu’un célibataire acte de foi.

PS: à mon objection, itérer "comme si c'était ennuyeux" et comme "prenant du temps" était la réponse "sans douleur, sans gain": une collection générique contient forcément Map.Entry <String, String > s, et rien d'autre. Vous devez payer pour cette garantie. Lors de l'utilisation systématique de génériques, ce paiement prend en belle forme la conformité au code, pas le temps machine!
Une école de pensée pourrait dire que vous devez définir les paramètres d’Eclipse pour faire de telles erreurs de distribution non vérifiées, plutôt que des avertissements. Dans ce cas, vous devrez utiliser mon premier moyen.

package scratchpad;

import Java.util.HashMap;
import Java.util.Iterator;
import Java.util.Map;
import Java.util.Vector;

public class YellowMouse {

    // First way

    Map<String, String> getHashMapStudiouslyAvoidingSuppressTag(HttpSession session) {
      Map<?, ?> theHash = (Map<?, ?>)session.getAttribute("attributeKey");

      Map<String, String> yellowMouse = new HashMap<String, String>();
      for( Map.Entry<?, ?> entry : theHash.entrySet() ){
        yellowMouse.put( (String)entry.getKey(), (String)entry.getValue() );
      }

      return yellowMouse;
    }


    // Second way

    Map<String, String> getHashMapUsingNaughtyButNiceUtilityMethod(HttpSession session) {
      return uncheckedCast( session.getAttribute("attributeKey") );
    }


    // NB this is a utility method which should be kept in your utility library. If you do that it will
    // be the *only* time in your entire life that you will have to use this particular tag!!

    @SuppressWarnings({ "unchecked" })
    public static synchronized <T> T uncheckedCast(Object obj) {
        return (T) obj;
    }


}
0
mike rodent

Si vous êtes sûr que le type renvoyé par session.getAttribute () est HashMap, vous ne pouvez pas transtyper avec ce type exact, mais vous ne vous fiez qu'à la vérification du générique HashMap.

HashMap<?,?> getItems(javax.servlet.http.HttpSession session) {  
    HashMap<?,?> theHash = (HashMap<?,?>)session.getAttribute("attributeKey");
    return theHash;
} 

Eclipse surprendra alors les avertissements, mais cela peut bien sûr entraîner des erreurs d’exécution difficiles à déboguer. J'utilise cette approche dans des contextes non critiques uniquement.

0
Lukas Normantas

Il suffit de le vérifier avant de le lancer.

Object someObject = session.getAttribute("attributeKey");
if(someObject instanceof HashMap)
HashMap<String, String> theHash = (HashMap<String, String>)someObject;  

Et pour ceux qui le demandent, il est assez courant de recevoir des objets dont vous n'êtes pas sûr du type. De nombreuses implémentations "SOA" héritées transmettent divers objets auxquels vous ne devriez pas toujours faire confiance. (Les horreurs!)

EDIT Nous avons modifié l'exemple de code une fois pour qu'il corresponde aux mises à jour de l'affiche. Après quelques commentaires, je constate qu'instanceof ne fonctionne pas bien avec les génériques. Cependant, modifier la vérification pour valider l'objet externe semble bien fonctionner avec le compilateur en ligne de commande. Exemple révisé maintenant affiché.

0
Rick