PDA

Archiv verlassen und diese Seite im Standarddesign anzeigen : [JAVA] Generics Problem mit Typsicherheit


Nasenbaer
2009-01-13, 14:41:33
Hi,
ich hab in etwa folgenden Code:


class Test<T>
{
private T val1;
private T val2;

public Test(T val1);
}

Test<Double> test1 = new Test<Double>(5.0);
Test<?> test2 = test1;

test2.setVal2(0.1); // The method setVal2(capture#1-of ?) in the type Test<capture#1-of ?> is not applicable for the arguments (double)


Mir ist klar, dass zur Compile-Zeit der Typ von test2 nur in diesem einfachen Beispiel ermittelbar wäre aber in meinem konkreten Problem wäre nicht mal das gegeben. Mir ist auch klar, dass der Compiler deshalb nicht wissen kann, dass setVal2() in dem Fall immer typsicher ist.

Aber mir ist nicht klar wie ich das Problem lösen kann.
Zur Erstellung von test1 ist val2 noch nicht verfügbar, weswegen ich keinen Konstruktor nehmen kann, der val2 gleich mit angibt.
Ich habe jetzt die Vermutung, dass ich mit diesem Code und der Voraussetzung, dass ich den Typ von test2 zur Compilezeit nicht kenne niemals setVal2() nutzen kann. Oder kann man da irgendwie per Casts das Problem lösen?

rad05
2009-01-13, 15:06:18
Du kannst einen Type-Cast machen, dann erhältst du aber beim Compilieren eine Warning, weil ja der Code ja nicht mehr für alle Typen funktioniert.

(Test<Double>)test2.setVal2(0.1);

Du müsstest also zuerst einen Typ-Prüfung machen, und dann den Cast, wenn die Prüfung positiv ausfällt.

Nasenbaer
2009-01-13, 15:22:46
Du kannst einen Type-Cast machen, dann erhältst du aber beim Compilieren eine Warning, weil ja der Code ja nicht mehr für alle Typen funktioniert.

(Test<Double>)test2.setVal2(0.1);

Du müsstest also zuerst einen Typ-Prüfung machen, und dann den Cast, wenn die Prüfung positiv ausfällt.
Für diese Beispiel geht das - das ist richtig. Aber wie ich geschrieben habe, weiß ich den Typ von test1 nicht in meinem konkreten Problem. Ist weiß nur, dass test1 ein Test<?> ist aber dorthin casten ist ja witzlos.

Ich weiß nur, dass der Wert den ich mit setVal2 setzen will in meinem Fall den richtigen Typ hat aber der Compiler weiß das ja nicht.

Nasenbaer
2009-01-13, 15:45:01
Ich habs jetzt erstmal so gelöst:


Test<?> test2 = test1;

Class<?> type = test2.getVal1().getClass();

if(type == Double.class)
{
((Test<Double>)test2).setVal2(0.1);
}
else if (type == Integer.class)
{
....


Das würde gehen, da die Anzahl der Typen die Vorkommen unter 10 liegt aber elegant finde ich so eine if-then-elseif-Kette trotzdem nicht.

rad05
2009-01-13, 15:48:22
Du kannst über Reflection auch feststellen, ob die Methode setVal2 für den Typ der Variable existiert. Das wäre dann universell, aber auch wesentlich aufwändiger.

Nasenbaer
2009-01-13, 15:53:09
Hmm wenn das soo aufwändig ist dann lohnt das wohl nicht. Ich lass das dann erstmal so.

rad05
2009-01-13, 16:20:04
Falls es dich interessiert, findest du unter folgendem Link mehr Information zu Reflections in Java:

http://java.sun.com/developer/technicalArticles/ALT/Reflection/

Nasenbaer
2009-01-13, 16:27:08
Danke für den Link. Schau ich mir mal an sobald ichs brauch. Habs jetzt noch anders lösen können.

Ich hatte im konkreten Beispiel das hier:

Test<? extends Serializable> implements Serializable
{
...
}

Test<?> test1 = bla; //bla ist auch Test<?>
Test<Serializable> test2 = (Test<Serializable>)test1;
test2.setVal2(2); // Oh wunder es geht so.


Das klappt dann natürlich da alle Typen, die ich bei setVal2() nutze auch immer Serializable sind.

Abnaxos
2009-01-13, 17:07:31
Erstmal: Typsicherheit zur Compile-Time gibt es in Java nicht.

Was du gerade herausgefunden hast, ist, warum du z.B. das nicht schreiben kannst:

Test<Double> test1 = new Test<Double>(0.5);
Test<Object> test2 = test1; // darst du nicht!

test2.setVal2(.1); // dafür ist jetzt das hier kein Problem mehr

Daher solltest du immer versuchen, den Typ-Parameter irgendwie durch den gesamten Code durchzuschleppen, z.B.:

<T> void setVal2(Test<T> target, T val2) {
target.setVal2(val2);
}

Das ist in diesem Beispiel natürlich höchst unschön, aber wir diskutieren hier ja auch anhand eines aufs Minimum reduzierten Beispiels. ;) In Code, der in der Praxis entsteht, ist sowas meist kein Problem.

Wenn du den Compiler-Fehler in diesem Beispiel entfernen willst, kannst du auch einfach folgendes machen:

test2.setVal2((Object).1);

bzw.

test2.setVal2((Double).1);

Das wird dir allerdings nicht garantieren, dass test2 auch wirklich ein Test<Double> ist, ist es das nicht, fliegt dir zur Laufzeit eine Exception um die Ohren. Das unangenehme daran: Diese ClassCastException tritt erst bei getVal2() auf, denn das Feld ist im Bytecode ein Object, erst bei

Double val2 = test1.getVal2();

wird versucht zu casten.

Ich hatte im konkreten Beispiel das hier:

Test<? extends Serializable> implements Serializable
{
...
}

Test<?> test1 = bla; //bla ist auch Test<?>
Test<Serializable> test2 = (Test<Serializable>)test1;
test2.setVal2(2); // Oh wunder es geht so.


Das klappt dann natürlich da alle Typen, die ich bei setVal2() nutze auch immer Serializable sind.
Vorsicht: Ist bla ein Test<Double>, wird das zu einer ClassCastException führen, da 2 ein int ist, das Autoboxing also einen Integer daraus baut. Und auch hier: Die Exception tritt erst bei getVal2() auf.

Coda
2009-01-13, 17:27:25
Erstmal: Typsicherheit zur Compile-Time gibt es in Java nicht.
Hrm? Natürlich gibt es die. Oder wie meinst du das?

Nasenbaer
2009-01-13, 17:27:29
Durch den Programmfluss ist aber klar, dass die Zuweisung per setVal2() immer den gleichen Typ hat wie test2.
Ist sicher nicht schön wenn man bestimmte Teile des Codes woanders verwenden will aber erstmal reichts.

Abnaxos
2009-01-13, 17:46:58
Hrm? Natürlich gibt es die. Oder wie meinst du das?
Nur Teilweise. Gäbe Generics schon in Java 1.0, hätten wir jetzt vollständige Typsicherheit zur Compile-Time. So aber konnte man die Generics nur mit Abstrichen in der Typsicherheit nachträglich einführen, Stichwort "Type Erasure". Nicht umsonst ist die ClassCastException neben der NullPointerException und evtl. noch der IllegalArgumentException die häufigste RuntimeException in Java.

Es hindert mich niemand daran, folgendes zu tun:

((Test)new Test<Double>(.5)).setVal2("Hello World");

OK, "es gibt keine Typsicherheit zur Compile-Time" ist etwas zu hart ausgedrückt ... ;) Es gibt nur eine eingeschränkte Typsicherheit zur Compile-Time in Java. Mit Generics ist es etwas besser geworden, aber eben nur etwas, dafür ist die Sprache erheblich komplizierter geworden und teilweise muss man um Einschränkungen herumwürgen, dass man nur noch den Kopf schütteln kann ...

Monger
2009-01-13, 23:33:59
An der Stelle sollte man vielleicht noch erwähnen, dass Java Generics auch Boundaries kennen (die mitunter höllisch kompliziert werden können ).

Also z.B.

public class Test <T extends Double>
{
// ...
}


Ist ein Weilchen her dass ich mich damit beschäftigt habe (.NET kennt diese Boundaries leider nicht :( ), und ich habs damals ehrlich gesagt nicht wirklich verstanden. Du kannst aber grundsätzlich Einschränkungen zu deinem Typus machen wie z.B. "ist eine Oberklasse von Double" oder "ist eine Unterklasse von Double". Gibt für beides Anwendungsfälle, und ist auch kombinierbar.

Und wenn du auf einen Typus die Boundaries anwendest die z.B. spezieller als Double oder irgendein Interface sind, kannst du somit auch natürlich alle Methoden ansprechen die diese Klasse erlaubt, ohne dass der Compiler meckert.

Aber auch die Java Gurus sagen: Generics sind kein vollwertiger Ersatz für Casts. Es gibt genügend Fälle wo du casten musst, und dann die Cast Warnung mit einem SuppressWarnings Attribut unterdrücken solltest. Da die Generics eigentlich nur syntaktischer Zucker für Casts sind, können sie auch nicht mehr als die.


OK, "es gibt keine Typsicherheit zur Compile-Time" ist etwas zu hart ausgedrückt ... ;) Es gibt nur eine eingeschränkte Typsicherheit zur Compile-Time in Java.
Du meinst wahrscheinlich zur Laufzeit, oder?

Abraxus
2009-01-13, 23:45:07
Ist ein Weilchen her dass ich mich damit beschäftigt habe (.NET kennt diese Boundaries leider nicht :( ), und ich habs damals ehrlich gesagt nicht wirklich verstanden. Du kannst aber grundsätzlich Einschränkungen zu deinem Typus machen wie z.B. "ist eine Oberklasse von Double" oder "ist eine Unterklasse von Double". Gibt für beides Anwendungsfälle, und ist auch kombinierbar.

http://msdn.microsoft.com/en-us/library/d5x73970(VS.80).aspx
.net kennt zwar keine Obergrenze, aber ich wüsste nicht welcher Anwendungsfall sowas vorrausetzt. Aber du wirst es mir sicherlich gleich mitteilen.

Monger
2009-01-14, 00:15:18
http://msdn.microsoft.com/en-us/library/d5x73970(VS.80).aspx

Ich muss zugeben, das wusste ich nicht. Gibt es das zufällig in VB.NET auch?


.net kennt zwar keine Obergrenze, aber ich wüsste nicht welcher Anwendungsfall sowas vorrausetzt. Aber du wirst es mir sicherlich gleich mitteilen.
*narf*

http://java.sun.com/docs/books/tutorial/extra/generics/morefun.html


In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T).

Abraxus
2009-01-14, 00:34:01
Ich muss zugeben, das wusste ich nicht. Gibt es das zufällig in VB.NET auch?
Bestimmt, frag mich aber nicht wie.

*narf*
http://java.sun.com/docs/books/tutorial/extra/generics/morefun.html

In general, if you have an API that only uses a type parameter T as an argument, its uses should take advantage of lower bounded wildcards (? super T). Conversely, if the API only returns T, you'll give your clients more flexibility by using upper bounded wildcards (? extends T).


Für die Fälle legt man einfach fest, dass der generische Typ zwei Interfaces implementieren muss. Ich denke mit der Lösung ist man auch flexibler, falls man mal den Vererbungsbaum umbauen möchte.