Adnotacja Java SafeVarargs, czy istnieje standard lub najlepsza praktyka?

175

Niedawno natknąłem się na @SafeVarargsadnotację java . Szukanie w Google, co sprawia, że ​​funkcja wariadyczna w Javie jest niebezpieczna, wprawiło mnie w zakłopotanie (zatrucie sterty? Usunięte typy?), Więc chciałbym wiedzieć kilka rzeczy:

  1. Co sprawia, że ​​zmienna funkcja Java jest niebezpieczna w tym @SafeVarargssensie (najlepiej wyjaśnione w formie szczegółowego przykładu)?

  2. Dlaczego ta adnotacja jest pozostawiona uznaniu programisty? Czy to nie jest coś, co kompilator powinien być w stanie sprawdzić?

  3. Czy jest jakiś standard, do którego należy się stosować, aby jego funkcja była rzeczywiście bezpieczna dla waragów? Jeśli nie, jakie są najlepsze praktyki, aby to zapewnić?

Oren
źródło
1
Czy widziałeś przykład (i wyjaśnienie) w JavaDoc ?
jlordo
2
Na swoim trzecim pytaniu, jeden praktyką jest zawsze pierwszy element i inne: retType myMethod(Arg first, Arg... others). Jeśli nie określisz first, dozwolona jest pusta tablica i możesz mieć metodę o tej samej nazwie z tym samym typem zwracania, która nie pobiera żadnych argumentów, co oznacza, że ​​maszyna JVM będzie miała trudności z określeniem, którą metodę należy wywołać.
fge
4
@jlordo Zrobiłem, ale nie rozumiem, dlaczego jest to podane w kontekście varargs, ponieważ można łatwo powtórzyć tę sytuację poza funkcją varargs (zweryfikowałem to, kompiluje się z oczekiwanymi ostrzeżeniami bezpieczeństwa typu i błędami w czasie wykonywania).
Oren

Odpowiedzi:

244

1) W Internecie i na StackOverflow jest wiele przykładów dotyczących konkretnego problemu z typami generycznymi i varargami. Zasadniczo dzieje się tak, gdy masz zmienną liczbę argumentów typu parametru typu:

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

W Javie varargs są cukrem składniowym, który podlega prostemu „przepisaniu” w czasie kompilacji: parametr varargs typu X...jest konwertowany na parametr typu X[]; i za każdym razem, gdy wywoływana jest ta metoda varargs, kompilator zbiera wszystkie „argumenty zmiennych”, które znajdują się w parametrze varargs, i tworzy tablicę tak samo jak new X[] { ...(arguments go here)... }.

Działa to dobrze, gdy typ varargs jest podobny do betonu String.... Kiedy jest to zmienna typu T..., taka jak , działa również, gdy Twiadomo, że jest konkretnym typem dla tego wywołania. np. jeśli powyższa metoda byłaby częścią klasy Foo<T>i masz Foo<String>odniesienie, to wywołanie foojej byłoby w porządku, ponieważ wiemy, że Tznajduje się Stringw tym punkcie kodu.

Jednak nie działa, gdy „wartość” Tjest parametrem innego typu. W Javie nie jest możliwe utworzenie tablicy składającej się z parametru typu type ( new T[] { ... }). Więc zamiast tego Java używa new Object[] { ... }(tutaj Objectjest górna granica T; gdyby górna granica była czymś innym, byłaby to zamiast Object), a następnie wyświetla ostrzeżenie kompilatora.

Więc co jest złego w tworzeniu new Object[]zamiast new T[]czy w czymkolwiek? Cóż, tablice w Javie znają swój typ komponentu w czasie wykonywania. Dlatego przekazany obiekt tablicy będzie miał nieprawidłowy typ komponentu w czasie wykonywania.

Prawdopodobnie najbardziej powszechne użycie varargs, po prostu do iteracji po elementach, nie stanowi problemu (nie przejmujesz się typem wykonawczym tablicy), więc jest to bezpieczne:

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

Jednak w przypadku wszystkiego, co zależy od typu składnika środowiska wykonawczego przekazanej tablicy, nie będzie to bezpieczne. Oto prosty przykład czegoś, co jest niebezpieczne i ulega awarii:

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");
  }
}

Problem polega na tym, że zależymy od typu argsbycia T[], aby zwrócić go jako T[]. Ale w rzeczywistości typ argumentu w czasie wykonywania nie jest wystąpieniem T[].

3) Jeśli twoja metoda ma argument typu T...(gdzie T jest dowolnym parametrem typu), to:

  • Bezpieczne: Jeśli twoja metoda zależy tylko od tego, że elementy tablicy są instancjami T
  • Niebezpieczne: jeśli zależy to od faktu, że tablica jest instancją T[]

Rzeczy, które zależą od typu środowiska wykonawczego tablicy, obejmują: zwrócenie jej jako typu T[], przekazanie go jako argumentu do parametru typu T[], pobranie typu tablicy przy użyciu .getClass(), przekazanie do metod, które zależą od typu środowiska wykonawczego tablicy, takich jak List.toArray()i Arrays.copyOf()itp.

2) Rozróżnienie, o którym wspomniałem powyżej, jest zbyt skomplikowane, aby można je było łatwo rozróżnić automatycznie.

newacct
źródło
7
teraz to wszystko ma sens. dzięki. więc żeby zobaczyć, że całkowicie cię rozumiem, powiedzmy, że mamy klasę, FOO<T extends Something>czy byłoby bezpiecznie zwrócić T [] metody varargs jako tablicę Somthings?
Oren
30
Warto zauważyć, że jeden przypadek, w którym jest (prawdopodobnie) zawsze poprawny w użyciu, @SafeVarargsto sytuacja, w której jedyną rzeczą, jaką robisz z tablicą, jest przekazanie jej do innej metody, która jest już tak opisana (na przykład: często piszę metody vararg, które istnieją, aby przekonwertować swój argument na listę przy użyciu Arrays.asList(...)i przekazać go do innej metody; taki przypadek zawsze można opatrzyć adnotacją, @SafeVarargsponieważ Arrays.asListma adnotację).
Jules,
3
@newacct Nie wiem, czy tak było w Javie 7, ale Java 8 nie zezwala, @SafeVarargschyba że metoda jest staticlub final, ponieważ w przeciwnym razie mogłaby zostać zastąpiona w klasie pochodnej czymś niebezpiecznym.
Andy
3
aw Javie 9 będzie to również dozwolone dla privatemetod, których również nie można przesłonić.
Dave L.
4

Aby uzyskać najlepsze praktyki, rozważ to.

Jeśli masz to:

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

Zmień to na to:

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
}

Zauważyłem, że zwykle dodam varargy tylko po to, aby było wygodniej dla moich rozmówców. Dla mojej wewnętrznej implementacji prawie zawsze wygodniej byłoby użyć pliku List<>. Tak więc, aby wrócić na Arrays.asList()barana i upewnić się, że nie ma możliwości wprowadzenia Heap Pollution, oto co robię.

Wiem, że to tylko odpowiada na twoje # 3. newacct udzielił świetnej odpowiedzi na punkty 1 i 2 powyżej, a nie mam wystarczającej reputacji, aby zostawić to w komentarzu. : P

Lodówka
źródło