Annotation Java SafeVarargs, existe-t-il une norme ou une meilleure pratique?

175

J'ai récemment rencontré l' @SafeVarargsannotation java . Rechercher sur Google ce qui rend une fonction variadique dangereuse en Java m'a laissé plutôt confus (empoisonnement de tas? Types effacés?), Alors j'aimerais savoir quelques choses:

  1. Qu'est-ce qui rend une fonction Java variadique dangereuse dans le @SafeVarargssens du terme (de préférence expliqué sous la forme d'un exemple détaillé)?

  2. Pourquoi cette annotation est-elle laissée à la discrétion du programmeur? N'est-ce pas quelque chose que le compilateur devrait être capable de vérifier?

  3. Y a-t-il une norme à laquelle il faut adhérer pour s'assurer que sa fonction est vraiment sans danger? Sinon, quelles sont les meilleures pratiques pour le garantir?

Oren
la source
1
Avez-vous vu l'exemple (et l'explication) dans JavaDoc ?
jlordo
2
Pour votre troisième question, une pratique est d'avoir toujours un premier élément et d' autres: retType myMethod(Arg first, Arg... others). Si vous ne spécifiez pas first, un tableau vide est autorisé, et vous pouvez très bien avoir une méthode du même nom avec le même type de retour ne prenant aucun argument, ce qui signifie que la JVM aura du mal à déterminer quelle méthode doit être appelée.
fge
4
@jlordo Je l'ai fait, mais je ne comprends pas pourquoi c'est donné dans le contexte varargs car on peut facilement replacer cette situation en dehors d'une fonction varargs (vérifié, compile avec les avertissements de sécurité de type attendus et les erreurs à l'exécution) ..
Oren

Réponses:

244

1) Il existe de nombreux exemples sur Internet et sur StackOverflow concernant le problème particulier des génériques et des varargs. En gros, c'est quand vous avez un nombre variable d'arguments d'un type paramètre de type:

<T> void foo(T... args);

En Java, les varargs sont un sucre syntaxique qui subit une simple "réécriture" à la compilation: un paramètre varargs de type X...est converti en un paramètre de type X[]; et chaque fois qu'un appel est fait à cette méthode varargs, le compilateur collecte tous les "arguments variables" qui vont dans le paramètre varargs, et crée un tableau comme new X[] { ...(arguments go here)... }.

Cela fonctionne bien lorsque le type varargs est comme du béton String.... Quand il s'agit d'une variable de type comme T..., cela fonctionne également quand Test connu pour être un type concret pour cet appel. Par exemple, si la méthode ci-dessus faisait partie d'une classe Foo<T>, et que vous avez une Foo<String>référence, alors l'appeler fooserait acceptable car nous savons que Tc'est Stringà ce point dans le code.

Cependant, cela ne fonctionne pas lorsque la "valeur" de Test un autre paramètre de type. En Java, il est impossible de créer un tableau d'un composant de type paramètre de type ( new T[] { ... }). Donc, Java utilise à la place new Object[] { ... }(voici Objectla limite supérieure de T; s'il y avait quelque chose de différent, ce serait celle-là au lieu de Object), puis vous donne un avertissement du compilateur.

Alors, qu'est-ce qui ne va pas avec la création new Object[]au lieu de new T[]ou quoi que ce soit? Eh bien, les tableaux en Java connaissent leur type de composant au moment de l'exécution. Ainsi, l'objet tableau transmis aura le mauvais type de composant lors de l'exécution.

Pour probablement l'utilisation la plus courante de varargs, simplement pour parcourir les éléments, ce n'est pas un problème (vous ne vous souciez pas du type d'exécution du tableau), donc c'est sûr:

@SafeVarargs
final <T> void foo(T... args) {
    for (T x : args) {
        // do stuff with x
    }
}

Cependant, pour tout ce qui dépend du type de composant d'exécution du tableau passé, ce ne sera pas sûr. Voici un exemple simple de quelque chose qui n'est pas sûr et qui plante:

class UnSafeVarargs
{
  static <T> T[] asArray(T... args) {
    return args;
  }

  static <T> T[] arrayOfTwo(T a, T b) {
    return asArray(a, b);
  }

  public static void main(String[] args) {
    String[] bar = arrayOfTwo("hi", "mom");
  }
}

Le problème ici est que nous dépendons du type d' argsêtre T[]pour le renvoyer sous la forme T[]. Mais en fait, le type de l'argument à l'exécution n'est pas une instance de T[].

3) Si votre méthode a un argument de type T...(où T est n'importe quel paramètre de type), alors:

  • Sûr: Si votre méthode dépend uniquement du fait que les éléments du tableau sont des instances de T
  • Unsafe: si cela dépend du fait que le tableau est une instance de T[]

Les choses qui dépendent du type d'exécution du tableau incluent: le renvoyer en tant que type T[], le passer en tant qu'argument à un paramètre de type T[], obtenir le type de tableau à l'aide .getClass(), le transmettre à des méthodes qui dépendent du type d'exécution du tableau, comme List.toArray()et Arrays.copyOf(), etc.

2) La distinction que j'ai mentionnée ci-dessus est trop compliquée pour être facilement distinguée automatiquement.

newacct
la source
7
eh bien maintenant, tout a du sens. Merci. donc juste pour voir que je vous comprends complètement, disons que nous avons une classe FOO<T extends Something>serait-il prudent de renvoyer le T [] d'une méthode varargs sous forme de tableau de Somthings?
Oren
30
Il peut être intéressant de noter qu'un cas où il est (vraisemblablement) toujours correct d'utiliser @SafeVarargsest lorsque la seule chose que vous faites avec le tableau est de le passer à une autre méthode qui est déjà si annotée (par exemple: je me retrouve fréquemment à écrire des méthodes vararg qui existent pour convertir leur argument en liste en utilisant Arrays.asList(...)et le passer à une autre méthode; un tel cas peut toujours être annoté avec @SafeVarargscar il Arrays.asListcontient l'annotation).
Jules
3
@newacct Je ne sais pas si c'était le cas dans Java 7, mais Java 8 ne le permet pas à @SafeVarargsmoins que la méthode ne soit staticou final, car sinon, elle pourrait être remplacée dans une classe dérivée avec quelque chose de dangereux.
Andy
3
et dans Java 9, il sera également autorisé pour les privateméthodes qui ne peuvent pas non plus être remplacées.
Dave L.
4

Pour les meilleures pratiques, considérez ceci.

Si vous avez ceci:

public <T> void doSomething(A a, B b, T... manyTs) {
    // Your code here
}

Changez-le en ceci:

public <T> void doSomething(A a, B b, T... manyTs) {
    doSomething(a, b, Arrays.asList(manyTs));
}

private <T> void doSomething(A a, B b, List<T> manyTs) {
    // Your code here
}

J'ai constaté que je n'ajoute généralement que des varargs pour le rendre plus pratique pour mes appelants. Il serait presque toujours plus pratique pour mon implémentation interne d'utiliser un fichier List<>. Donc, pour Arrays.asList()me servir et m'assurer qu'il n'y a aucun moyen que je puisse introduire Heap Pollution, c'est ce que je fais.

Je sais que cela ne répond qu'à votre # 3. newacct a donné une excellente réponse pour les # 1 et # 2 ci-dessus, et je n'ai pas assez de réputation pour laisser cela comme un commentaire. : P

Frigo
la source