SafeVarargs-Annotation in Java

Wer in Java häufig mit Generics arbeitet weiß, dass dabei häufiger sehr spezielle Warnungen auftreten, insbesondere im Bezug auf die Typsicherheit von Variablen und Parametern.
Wer beispielsweise einen Ellipsis-Operator (auch Varargs-Operator genannt) in der Methodensignatur einer generischen Methode verwendet, erhält die Warnung Type safety: Potential heap pollution via varargs parameter args.
Als Lösung wird von Eclipse vorgeschlagen: Add @SafeVarargs.
Was macht diese Annotation und wie ist sie einzusetzen?

Die Annotation unterdrückt die Warnung. Sonst macht sie nichts. Nur weil ein Java-Entwickler die Annotation über eine unsichere Methode schreibt, wird diese nicht automatisch sicher. Die Annotation ist vielmehr als Hinweis zu interpretieren, dass der Entwickler der Meinung ist, dass die Methode sicher sei (und es hoffentlich auch beweisen kann). Einfach eine SafeVarargs-Annotation über die Methode zu schreiben, um die Warnung loszuwerden, löst das Problem nicht. Stattdessen sollte die Variable dann besser mit dem Ellipsis-Operator besser mit @SuppressWarnings("unchecked") annotiert werden.

Aber wie lässt sich das eigentliche Problem der unsicheren Methode lösen?
Dafür ist ein wenig Hintergrundwissen zum Ellipsis-Operator nötig. Hierzu ein kurzes Codebeispiel:

Der Ellipsis-Operator

static void print(String...strings) {
  for(String s : strings) {
    System.out.println(s);
  }
}

Die Funktionsaufrufe können wie folgt aussehen:

  print("A");
  print("A", "B", "C");
  print(new String[] {"A","B","C"});

Die oben definierte Methode print(String... strings) lässt sich nun sowohl mit einem String-Parameter (Zeile 1), als auch mit mehreren (Zeile 2) oder gleich einem ganzen String-Array (Zeile 3) aufrufen.
Der Compiler sorgt anschließend dafür, dass an allen Stellen, an denen die Methode mit dem Ellipsis-Operator aufgerufen wird, ein Array vom Typ String erstellt wird und die Parameter in dieses Array verpackt werden. Anschließend wird das neu erstellte String-Array an die print-Methode übergeben.

Nun ein generisches Beispiel mit Ellipsis-Operator:

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

Dieses einfache Beispiel soll einfach ein Array vom Typ T zurückgeben, welches aus denen an die Methode übergebenen Parametern in args bestehen soll. Wer diese Methode nun etwa in sein Eclipse kopiert oder sie kompilieren lässt, erhält die oben genannte Warnung Type safety: Potential heap pollution via varargs parameter args.
Der Methodenaufruf:

String[] sArr = asArray("Hello", "World");

Dieser einfache Methodenaufruf funktioniert wie es zu erwarten ist. Der Compiler sieht, dass der Typ String zu sein hat, da die Parameter (“Hello” und “World”) vom Typ String sind und kann sie daher einfach in ein String-Array verpacken. Dieses String-Array wird dann an die asArray-Methode gegeben. Da der Compiler intern eine Methode mit dem Basistyp des Generics (hier Object, bei einer Signatur wie <T extends CharSequence> CharSequence) erstellt, wird dieses String[] dann an die Methode gegeben. Diese gibt das Array in unserem Fall einfach wieder zurück. Da der Generics ja wie oben beschrieben durch Object substituiert wurde, ist der Rückgabetyp der Methode Object[]. An der Aufruferstelle wird dieses Object[] dann wieder zurück auf String[] gecastet. Dies ist ohne Weiteres möglich, da es sich beim zurückgegebenen Array ja um das gleiche Array handelt, welches als Parameter in die Methode gegeben wurde und dieses ja vom Typ String[] war.

Das Problem

Zum Problem kommt es, sobald die asArray-Methode keinen fixen Typ (wie zum Beispiel “String”), sondern einen generischen Parameter erhält. Beispiel:

//unsicher
static <T> T[] asArray(T... args) {
  return args;
}

//Warnung, da unsichere Methode aufgerufen wird
static <T> T[] arrayOfTwo(T a, T b) {
  return asArray(a, b);
}

Aufruf:

String[] sArr = arrayOfTwo("Hello", "World");

Es wird also die Methode arrayOfTwo mit den Parametern “Hello” und “World” aufgerufen. Diese ruft dann die unsichere Methode asArray von oben auf, welche ein Array zurückgeben soll.
Da der generische Typ vom Compiler wegoptimiert wurde, wird hier ein Array vom Basistyp des Generics erstellt. In diesem Fall ist das Object. Wenn die Signatur ein <T extends CharSequence> erhalten würde, wäre es entsprechend CharSequence. Das bedeuted, dass der Compiler nun also ein Array vom Typ Object[] für den Aufruf von asArray in Zeile 8 generiert.

In dieses Object[]-Array werden dann die beiden Parameter a und b aus Zeile 7 eingefügt. Da die arrayOfTwo-Methode hier mit den Parametern “Hello” und “World” aufgerufen wurde, werden diese beiden Strings also in das Array eingefügt. Dies ist kein Problem, da String von Object erbt. Somit wird von der printTwo-Methode also ein “echtes” Object[] zurückgegeben, das intern eben kein String[] mehr ist.
Dadurch, dass auf Aufruferseite gecastet wird, kann der Compiler nicht ahnen, dass das Object[], welches im Bytecode zurückgegeben wird, sich nicht auf ein String[] zurückcasten lässt. Somit gibt es erst zur Laufzeit eine ClassCastException:
Exception in thread "main" java.lang.ClassCastException: java.base/[Ljava.lang.Object; cannot be cast to java.base/[Ljava.lang.String;.

Um diese ungewollte Exception zu verhindern, gibt es die Type-Safety-Warnung am Ellipsis-Operator der asArray-Methode (Zeile 2). Sogar, wenn diese Warnung unterdrückt ist, gibt es bei jedem Aufruf der asArray-Methode (zum Beispiel in Zeile 8) eine Warnung (Type safety: A generic array of T is created for a varargs parameter), die auf das Problem hinweist.

In welchen Fällen könnte denn ein Ellipseoperator mit einem generischen Typen verwendet werden und es trotzdem ein typsicheres Resultat geben?
Hier ein Beispiel:

@SafeVarargs //sicher
static <T> void print(T... args) {
  for (T t : args) {
    System.out.println(t);
  }
}

//sicher, da sichere Methode aufgerufen wird
static <T> void printTwo(T a, T b) {
  print(a, b);
}

Aufruf:
printTwo("Hello","World");

Hier erstellt der Compiler zwar ebenfalls ein Object[] für die Parameter in Zeile 10 und packt die Strings aus Zeile 9 hinein. Da die Methode jedoch nicht behauptet, ein Array T[] zurückzugeben und intern nicht versucht, args auf einen anderen Typ zu casten, kann diese Methode als sicher angesehen werden. Daher kann sie zu Recht mit @SafeVarargs annotiert werden.
Daraufhin verschwindet auch die Warnung beim Methodenaufruf in Zeile 10.

Es fällt auf, dass alle Methoden in den Beispielen statisch waren. Dies liegt daran, dass @SafeVarargs seit der Einführung in Java 7 nur auf Methoden verwendet werden kann, die static oder final sind. Dies gibt insofern Sinn, als dass public-, protected- oder package-Methoden überschrieben werden können und damit nicht mehr zwangsläufig typsicher wären. Seit Java 9 lassen sich auch private Instanzmethoden mit @SafeVarargs> annotieren:

class Main {
	// neu in Java 9
	@SafeVarargs
	private <T> void printPrivate(T... args) {
		for (T t : args) {
			System.out.println(t);
		}
	}

	public static void main(String[] args) {
		new Main().printPrivate("Hello", "World");
	}
}

Private Methoden können in Java schließlich auch nicht überschrieben werden.

Fazit

Zu merken ist also:

  • Wenn das Array, welches bei der Verwendung von Generics mit dem Ellipsis-Operator verwendet wird, zwingend vom Typ T sein muss, kann es bei verschachtelten Aufrufen mit generischen Typen zu ClassCastExceptions kommen und der Code ist unsicher.
  • Daher sollte darauf geachtet werden, das Array nicht außerhalb der Methode zu verwenden (etwa es per return zurück geben oder es an eine Callable geben).

  • Wenn nur die Elemente im Array vom Typ T sein müssen, der Typ des Arrays selbst aber irrelevant ist, ist der Code sicher und die Methode kann mit @SafeVarargs annotiert werden.

Weiterführende Links

[Open JDK JEP 213.1]
[@SafeVarargs JavaDoc]
[Stack Overflow Post about the issue]

Short URL for this post: https://wp.me/p4nxik-31G
This entry was posted in Did you know?, Java and Quality, Java Basics and tagged , , , . Bookmark the permalink.

Leave a Reply