Rheinwerk Computing < openbook >


 
Inhaltsverzeichnis
Materialien
Vorwort
1 Java ist auch eine Sprache
2 Imperative Sprachkonzepte
3 Klassen und Objekte
4 Arrays und ihre Anwendungen
5 Der Umgang mit Zeichenketten
6 Eigene Klassen schreiben
7 Objektorientierte Beziehungsfragen
8 Ausnahmen müssen sein
9 Geschachtelte Typen
10 Besondere Typen der Java SE
11 Generics<T>
12 Lambda-Ausdrücke und funktionale Programmierung
13 Architektur, Design und angewandte Objektorientierung
14 Java Platform Module System
15 Die Klassenbibliothek
16 Einführung in die nebenläufige Programmierung
17 Einführung in Datenstrukturen und Algorithmen
18 Einführung in grafische Oberflächen
19 Einführung in Dateien und Datenströme
20 Einführung ins Datenbankmanagement mit JDBC
21 Bits und Bytes, Mathematisches und Geld
22 Testen mit JUnit
23 Die Werkzeuge des JDK
A Java SE-Module und Paketübersicht
Stichwortverzeichnis


Download:

- Listings, ca. 2,7 MB


Buch bestellen
Ihre Meinung?



Spacer
<< zurück
Java ist auch eine Insel von Christian Ullenboom

Einführung, Ausbildung, Praxis
Buch: Java ist auch eine Insel


Java ist auch eine Insel

Pfeil 11 Generics<T>
Pfeil 11.1 Einführung in Java Generics
Pfeil 11.1.1 Mensch versus Maschine – Typprüfung des Compilers und der Laufzeitumgebung
Pfeil 11.1.2 Raketen
Pfeil 11.1.3 Generische Typen deklarieren
Pfeil 11.1.4 Generics nutzen
Pfeil 11.1.5 Diamonds are forever
Pfeil 11.1.6 Generische Schnittstellen
Pfeil 11.1.7 Generische Methoden/Konstruktoren und Typ-Inferenz
Pfeil 11.2 Umsetzen der Generics, Typlöschung und Raw-Types
Pfeil 11.2.1 Realisierungsmöglichkeiten
Pfeil 11.2.2 Typlöschung (Type Erasure)
Pfeil 11.2.3 Probleme der Typlöschung
Pfeil 11.2.4 Raw-Type
Pfeil 11.3 Die Typen über Bounds einschränken
Pfeil 11.3.1 Einfache Einschränkungen mit extends
Pfeil 11.3.2 Weitere Obertypen mit &
Pfeil 11.4 Typparameter in der throws-Klausel *
Pfeil 11.4.1 Deklaration einer Klasse mit Typvariable <E extends Exception>
Pfeil 11.4.2 Parametrisierter Typ bei Typvariable <E extends Exception>
Pfeil 11.5 Generics und Vererbung, Invarianz
Pfeil 11.5.1 Arrays sind kovariant
Pfeil 11.5.2 Generics sind nicht kovariant, sondern invariant
Pfeil 11.5.3 Wildcards mit ?
Pfeil 11.5.4 Bounded Wildcards
Pfeil 11.5.5 Bounded-Wildcard-Typen und Bounded-Typvariablen
Pfeil 11.5.6 Das LESS-Prinzip
Pfeil 11.5.7 Enum<E extends Enum<E>> *
Pfeil 11.6 Konsequenzen der Typlöschung: Typ-Token, Arrays und Brücken *
Pfeil 11.6.1 Typ-Token
Pfeil 11.6.2 Super-Type-Token
Pfeil 11.6.3 Generics und Arrays
Pfeil 11.6.4 Brückenmethoden
Pfeil 11.7 Zum Weiterlesen
 

Zum Seitenanfang

11.6    Konsequenzen der Typlöschung: Typ-Token, Arrays und Brücken * Zur vorigen ÜberschriftZur nächsten Überschrift

Die Typlöschung ist im Allgemeinen kein so großes Problem, doch in speziellen Situationen ist es lästig, dass der Typ nicht zur Laufzeit vorliegt.

 

Zum Seitenanfang

11.6.1    Typ-Token Zur vorigen ÜberschriftZur nächsten Überschrift

Wir haben zum Beispiel gesehen, dass, wenn eine Rakete mit der Typvariablen T deklariert wurde, dieses T nicht wirklich wie in einem Makro durch das Typargument ersetzt wird, sondern dass in der Regel nur einfach Object eingesetzt wird:

class Rocket<T> {

T newRocketContent() { return new T(); } // inline image Compilerfehler

}

Aus new T() macht die Typlöschung also new Object(), und das ist nichts wert. Doch wie kann dennoch ein Typ erzeugt werden und der Typ T zur Laufzeit vorliegen?

Hier lässt sich ein Trick nutzen, nämlich ein Class-Objekt für den Typ einzusetzen.

Typargument

Class-Objekt repräsentiert Typargument

String

String.class

Integer

Integer.class

Tabelle 11.12    Transfer der Typargumente durch Class-Objekte

Dieses Class-Objekt, das nun den Typ repräsentiert, heißt Typ-Token (engl. type token). Es kommt uns natürlich entgegen, dass Class selbst als generischer Typ deklariert ist und zwei interessante Methoden ebenfalls »generifiziert« wurden.

[zB]  Beispiel

Erfrage die Class-Objekte:

Class<String> clazz1 = String.class;

String newInstance = clazz1.getConstructor().newInstance();

Class<? extends String> clazz2 = newInstance.getClass();

System.out.println( clazz1.equals( clazz2 ) ); // true

Zunächst ist da die Methode getConstructor(), die über das Class-Objekt den parameterlosen Konstruktor heraussucht. Über das Constructor-Objekt erzeugt dann newInstance() ein neues Exemplar mit dem Typ, den das Class-Objekt repräsentiert:

Mit einem gegebenen Objekt lässt sich mit getClass() das zugehörige Class-Objekt zur Klasse erfragen:

class java.lang.Object
  • final native Class<?> getClass()

    Liefert Class-Objekt.

[»]  Hinweis

Die Rückgabe Class<?> bei getClass() ist unschön, insbesondere die allgemeine Wildcard. Sie verhindert, dass sich Folgendes schreiben lässt:

Class<String> clazz = "ARTE".getClass();  // inline image Compilerfehler "Type mismatch"

Stattdessen muss es so heißen:

Class<? extends String> clazz = "ARTE".getClass();

Da Object nicht generisch deklariert ist, ist es kein Wunder, dass getClass() keine genaueren Angaben machen kann.

Lösungen mit dem Typ-Token

Um das Typ-Token einzusetzen, muss das Class-Objekt mit als Argument in einem Konstruktor oder einer Methode übergeben werden. So lässt sich etwa eine newInstance()-Methode nachbauen, die die geprüften Exceptions fängt und im Fehlerfall als RuntimeException meldet.

Gut zu sehen ist, wie sich der Typ des Class-Objekts auf die Rückgabe überträgt:

public static <T> T newInstance( Class<T> type ) {

try {

return type.getConstructor().newInstance();

}

catch ( ReflectiveOperationException e ) {

throw new RuntimeException( e );

}

}

Die ReflectiveOperationException ist die Oberklasse von ClassNotFoundException, IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchFieldException, NoSuchMethodException. Es ist praktisch, diesen Basistyp anzugeben, weil das Schreibarbeit spart – bei Reflection kann immer eine Menge schiefgehen, und es gibt unzählige geprüfte Ausnahmen.

 

Zum Seitenanfang

11.6.2    Super-Type-Token Zur vorigen ÜberschriftZur nächsten Überschrift

Mit einem Class-Objekt lässt sich gut ein Typ repräsentieren, allerdings gibt es ein Problem. Das Class-Objekt kann selbst keine generischen Typen darstellen:

Typargument

Class-Objekt repräsentiert Typargument

String

String.class

Integer

Integer.class

Rocket<String>

Rocket<String>.class Geht nicht! inline image

Tabelle 11.13    Ein »Class«-Objekt kann keinen generischen Typ beschreiben.

Der wirkliche Typ lässt sich nur mit viel Getrickse bestimmen und festhalten. Hier kommt die Reflection-API zum Einsatz, sodass nur kurz die Klasse und ein Beispiel vorgestellt werden sollen. Hier ist die Klasse:

Listing 11.25    src/main/java/com/tutego/insel/generic/TypeRef, TypeRef

public abstract class TypeRef<T> {



public final Type type;



protected TypeRef() {

ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass();

type = superclass.getActualTypeArguments()[0];

}

}

Und hier ist ein Beispiel, das eine anonyme Unterklasse erzeugt und so den Typ zugänglich macht:

Listing 11.26    src/main/java/com/tutego/insel/generic/TypeRefDemo, main()

TypeRef<Rocket<String>> ref1 = new TypeRef<>(){};

System.out.println( ref1.type ); // com.tutego.insel.generic.Rocket<java.lang.String>

TypeRef<Rocket<Byte>> ref2 = new TypeRef<>(){};

System.out.println( ref2.type ); // com.tutego.insel.generic.Rocket<java.lang.Byte>

Damit konnten wir das Typargument über java.lang.reflect.Type festhalten, und ref1 unterscheidet sich eindeutig von ref2.

Der Typ liegt jedoch nicht als Class-Objekt vor, und Operationen wie getConstructor(). newInstance() sind auf Type nicht möglich – die Schnittstelle deklariert überhaupt keine Methoden, sondern repräsentiert nur Typen.

 

Zum Seitenanfang

11.6.3    Generics und Arrays Zur vorigen ÜberschriftZur nächsten Überschrift

Die Typlöschung ist der Grund dafür, dass Arrays nicht so umgesetzt werden können, wie es sich der Entwickler denkt.[ 217 ](Bei Oracle (http://bugs.java.com/bugdatabase/view_bug.do?bug_id=4888066) ist dafür ein Bug gelistet. Suns Antwort auf die Bitte, ihn zu beheben, lautet lapidar: »Some day, perhaps, but not now.« ) Folgendes ergibt einen Compilerfehler:

class TwoBox<T> {

T[] array = new T[ 2 ]; // inline image Cannot create a generic array of T

T[] getArray() { return array; }

}

Der Grund für diesen Fehler ist dann gut zu erkennen, wenn wir überlegen, zu welchem Programmcode die Typlöschung führen würde:

class TwoBox {

Object[] array = new Object[ 2 ]; // (1)

Object[] getArray() { return array; }

}

Der Aufrufer würde nun die TwoBox parametrisiert verwenden wollen:

TwoBox<String> twoStrings = new TwoBox<String>();

String[] stringArray = twoStrings.getArray();

Denken wir an dieser Stelle wieder an die Typlöschung und an das, was der Compiler generiert:

TwoBox twoStrings = new TwoBox();

String[] stringArray = (String[]) twoStrings.getArray(); // (2)

Jetzt ist es auffällig: Während (1) ein Object-Array der Länge 2 aufbaut und auch getArray() dies als Object-Array nach außen gibt, castet (2) dieses Object-Array auf ein String-Array. Das geht aber nicht, denn diese beiden Typen sind nicht typkompatibel. Zwar kann natürlich ein Object-Array Strings referenzieren, aber das Array selbst als Objekt ist eben ein Object[] und kein String[].

Reflection hilft

Die Java-API bietet über Reflection wieder eine Möglichkeit, Arrays eines Typs zu erzeugen:

T[] array = (T[]) Array.newInstance( clazz, 2 );

Allerdings muss der Class-Typ clazz bekannt sein und als zusätzlicher Parameter übergeben werden. Die Syntax T.class ergibt einen Compilerfehler, denn über die Typlöschung wäre das ja sowieso immer Object.class, was den gleichen Fehler wie vorher zur Folge hätte und kein Fortschritt wäre.

 

Zum Seitenanfang

11.6.4    Brückenmethoden Zur vorigen ÜberschriftZur nächsten Überschrift

Aus der Tatsache, dass mit Generics übersetzte Klassen auf einer JVM lauffähig sein müssen, die kein generisches Typsystem besitzt, folgen diverse Hacks, die der Compiler zur Erhaltung der heiligen Kompatibilität vornimmt. Er fügt neue Methoden ein, sogenannte Brückenmethoden, damit der Bytecode nach der Typlöschung auch von älteren Programmen genutzt werden kann.

Brückenmethode wegen Typvariablen in Parametern

Starten wir mit der Schnittstelle Comparable, die generisch deklariert wurde:

public interface Comparable<T> { public int compareTo( T o ); }

Die bekannte Klasse Integer implementiert zum Beispiel diese Schnittstelle und kann somit sagen, wie die Ordnung zu einem anderen Integer-Objekt ist:

Listing 11.27    java/lang/Integer.java, Ausschnitt

public final class Integer extends Number implements Comparable<Integer> {



private final int value;



public Integer( int value ) { this.value = value; }



public int compareTo( Integer anotherInteger ) {

int thisVal = this.value;

int anotherVal = anotherInteger.value;

return ( thisVal < anotherVal ? -1 : (thisVal == anotherVal ? 0 : 1) );

}

...

}

Die Klasse Integer implementiert die Methode compareTo(…) mit dem Parametertyp Integer. Der Compiler wird also eine Methode mit der Signatur compareTo(Integer) erstellen. Doch damit beginnt ein Problem! Wir haben eine unbekannte Anzahl an Zeilen Quellcode, die sich auf eine Methode compareTo(Object) beziehen, denn vor Java 5 war die Signatur ja anders.

Damit es nicht zu Inkompatibilitäten kommt, setzt der Compiler einfach noch die Methode compareTo(Object) bei Integer dazu. Die Implementierung sieht so aus, dass sie einfach delegiert:

public int compareTo( Object anotherInteger ) {

return compareTo( (Integer) anotherInteger );

}

Brückenmethode wegen kovarianter Rückgabetypen

Wenn eine Methode überschrieben wird, so muss die Unterklasse die gleiche Signatur (also den gleichen Methodennamen und die gleiche Parameterliste) besitzen. Nehmen wir eine Klasse CloneableFont, die Font erweitert und die clone()-Methode aus Object überschreibt. Eine Klasse, die sich auch unter Java 1.4 übersetzen lässt, würde so aussehen:

Listing 11.28    src/main/java/com/tutego/insel/nongeneric/CloneableFont.java, CloneableFont

public class CloneableFont extends Font implements Cloneable {



public CloneableFont( String name, int style, int size ) {

super( name, style, size );

}



@Override public Object clone() {

return new Font( getAttributes() );

}

}

Im Bytecode der Klasse CloneableFont sind somit ein Konstruktor und eine Methode vermerkt.

Dazu kurz ein Blick auf die Ausgabe des Dienstprogramms javap, das die Signaturen anzeigt:

$ javap com.tutego.insel.nongeneric.CloneableFont

Compiled from "CloneableFont.java"

public class com.tutego.insel.nongeneric.CloneableFont extends java.awt.Font{

public com.tutego.insel.nongeneric.CloneableFont(java.lang.String, int, int);

public java.lang.Object clone();

}

Nehmen wir nun an, eine zweite Klasse ist Nutzer von CloneableFont:

Listing 11.29    src/main/java/com/tutego/insel/nongeneric/CloneableFontDemo.java, main()

CloneableFont font = new CloneableFont( "Arial", Font.BOLD, 12 );

Object font2 = font.clone();

Beim Aufruf font.clone() prüft der Compiler, ob die Methode clone() in CloneableFont aufrufbar ist, und trägt dann die exakte Signatur mit Rückgabe – das ist der entscheidende Punkt – in den Bytecode ein. Die Anweisung font.clone() sieht im Bytecode von CloneableFontDemo.class etwa so aus (disassembliert mit javap):

invokevirtual #23;

#23 ist ein Verweis auf die clone()-Methode von CloneableFont, und invokevirtual ist der Bytecodebefehl zum Aufruf der Methode. Hinter der 23 steckt eine JVM-Methodenkennung, die von javap so ausgegeben wird:

com/tutego/insel/nongeneric/CloneableFont.clone:()Ljava/lang/Object;

Im Bytecode steht exakt ein Verweis auf die Methode clone() mit dem Rückgabetyp Object.

Seit Java 5 ist eine kleine Änderung beim Überschreiben hinzugekommen. Wenn eine Unterklasse eine Methode überschreibt, kann sie den Rückgabetyp auf einen Untertyp präzisieren – das nennt sich kovariantes Überschreiben. Also kann clone() statt Object jetzt Font zurückgeben.

Gleicher Rückgabetyp wie die überschriebene Methode

Kovarianter Rückgabetyp

com.tutego.insel.nongeneric.CloneableFont

@Override public Object clone() {

 return new Font( getAttributes() );

}

com.tutego.insel.generic.CloneableFont

@Override public Font clone() {

 return new Font( getAttributes() );

}

Tabelle 11.14    Beispiel kovarianter Rückgabetypen

Da der Rückgabetyp der überschriebenen Methode nun nicht mehr Object, sondern Font ist, ändert sich auch der Bytecode von CloneableFont. Die Datei CloneableFont.class ist ohne kovariante Rückgabe 593 Byte groß und mit kovarianter Rückgabe 739 Byte. (Warum dieser satte Unterschied? Dazu gleich mehr im folgenden Abschnitt.)

Stellen wir uns nun Folgendes vor: Die erste Version von CloneableFont wurde lange vor der Existenz von Generics implementiert und konnte kein kovariantes Überschreiben nutzen. Die Klasse CloneableFont ist unglaublich populär, und die Methode clone() – die Object liefert – wird von vielen Stellen aufgerufen. Im Bytecode der nutzenden Klassen gibt es also immer einen Bezug zu der Methode mit der JVM-Signatur:

com/tutego/insel/nongeneric/CloneableFont.clone:()Ljava/lang/Object;

Bei einem Refactoring geht der Autor der Klasse CloneableFont über seine Klasse und sieht, dass er bei clone() die kovariante Rückgabe nutzen kann. Er korrigiert die Methode und setzt statt Object den Typ Font ein. Er compiliert die Klasse und setzt sie wieder in die Öffentlichkeit.

Nun stellt sich die Frage, was mit den Nutzern ist, also Klassen wie CloneableFontDemo, die nicht neu übersetzt werden, denn sie suchen eine Methode mit dieser JVM-Signatur:

com/tutego/insel/nongeneric/CloneableFont.clone:()Ljava/lang/Object;

Da clone() in CloneableFont in der aktuellen Version nun Font zurückgibt, müsste die JVM einen Fehler auslösen, denn der Bytecode ist ja anders, und die gefragte Methode mit der Rückgabe Object ist nicht mehr da. Das wäre ein riesiges Problem, denn so würden durch die Änderung des Autors alle nutzenden Klassen illegal, und die Projekte mit diesen Klassen ließen sich alle nicht mehr übersetzen. Doch es gibt keinen Anlass zu Panik. Es kommt nicht zu einem Compilerfehler, da der Compiler eine Hilfsmethode einfügt, die in der JVM-Signatur mit der Java 1.4-Variante von clone(), also mit der Rückgabe Object, übereinstimmt. Der Disassembler javap zeigt das gut:

$ javap com.tutego.insel.generic.CloneableFont

Compiled from "CloneableFont.java"

public class com.tutego.insel.generic.CloneableFont extends java.awt.Font{

public com.tutego.insel.generic.CloneableFont(java.lang.String, int, int);

public java.awt.Font clone();

public java.lang.Object clone() throws java.lang.CloneNotSupportedException;

}

Die clone()-Methode gibt es also zweimal! Interessant ist die Besonderheit, dass Dinge im Bytecode erlaubt sind, die im Java-Programmcode nicht möglich sind. In Java gehört der Rückgabetyp nicht zur Signatur, und der Java-Compiler erlaubt nicht zwei Methoden mit der gleichen Signatur. Im Bytecode allerdings gehört der Rückgabetyp schon dazu, und daher sind die Methoden dort erlaubt, da sie klar unterscheidbar sind.

Übersetzt ein aktueller Compiler die Klasse CloneableFont mit dem kovarianten Rückgabetyp bei clone(), so setzt er automatisch die Brückenmethode ein. Das erklärt auch, warum der Bytecode der Klassen mit kovarianten Rückgabetypen größer ist. So finden auch die alten Klassen, die auf die ursprüngliche clone()-Methode mit der Rückgabe Object compiliert sind, diese Methode, und es gibt keinen Laufzeitfehler.

Als Letztes muss noch geklärt werden, was der Compiler eigentlich genau für eine Brückenmethode generiert. Das ist einfach, denn in die Brückenmethode setzt der Compiler eine Weiterleitung:

@Override public Font clone() {

return new Font( getAttributes() );

}

@Override public Object clone() {

return (Font) clone(); // Vom Compiler in Bytecode generiert

}

Bleibt festzuhalten, dass auf Ebene der JVM kovariante Rückgabetypen nicht möglich sind und im Bytecode immer die Methode inklusive Rückgabetyp referenziert wird.

[»]  Hinweis

In Java sind alle Methoden einer Klasse, die sich auch im Bytecode befinden, über Reflection zugänglich. Lästig wäre es nun, wenn Tools Methoden sähen, die der Compiler eingeführt hat, weil dieser herumtricksen und Beschränkungen umschiffen musste. Die Brückenmethoden werden daher mit einem speziellen Flag markiert und als synthetische Methoden (engl. synthetic methods) gekennzeichnet. Das Flag lässt sich über Reflection mit isSynthetic() an den Field-Objekten erfragen.

Brückenmethode wegen einer Typvariablen in der Rückgabe

Werfen wir einen Blick auf ein ähnliches Szenario, bei dem der Rückgabetyp durch eine Typvariable einer generisch deklarierten Klasse bestimmt wird, und sehen wir uns an, welche Konsequenzen sich im Bytecode daraus ergeben.

Die Schnittstelle Iterator dient im Wesentlichen dazu, Datensammlungen nach ihren Daten zu fragen. Die beiden Spalten zeigen die Deklaration der Iterator-Schnittstelle unter Java 1.4 und seit Java 5:

Java 1.4

Java 5

public interface Iterator {

boolean hasNext();

Object next();

void remove();

}
public interface Iterator<E> {

boolean hasNext();

E next();

void remove();

}

Tabelle 11.15    Veränderung der Implementierung der Schnittstelle Iterator

So soll hasNext() immer true ergeben, wenn der Iterator weitere Daten liefern kann, und next() liefert schlussendlich das Datum selbst. In der Variante unter Java 5 ist der Rückgabetyp von next() durch die Typvariable bestimmt.

Warum Brückenmethoden benötigt werden, zeigt wieder ein Beispiel, in dem Entwickler mit Java 1.4 begannen und später ihr Programm mit Generics verfeinerten. Trotz der Änderung muss eine alte, mit Java 1.4 compilierte Version noch funktionieren.

Üblicherweise liefern Iteratoren alle Elemente einer Datenstruktur. Doch anstatt durch eine Datenstruktur zu laufen, soll unser Iterator, ein EndlessRandomIterator, unendlich viele Zufallszahlen liefern. Wir interessieren uns besonders für die Implementierung der Schnittstelle Iterator. Ohne Generics unter Java 1.4 sieht das so aus:

Listing 11.30    src/main/java/com/tutego/insel/nongeneric/EndlessRandomIterator.java, EndlessRandomIterator

public class EndlessRandomIterator implements Iterator {

@Override public boolean hasNext() { return true; }

@Override public Object next() { return Double.valueOf( Math.random() ); }

@Override public void remove() { throw new UnsupportedOperationException(); }

}

Ein Programm, das den EndlessRandomIterator nutzt, empfängt bei next() nun ein Double. Aber durch den Ausschluss der Kovarianz in Java 1.4 kann durch die Deklaration von Object next() in Iterator in unserer Klasse EndlessRandomIterator auch nur Object next() stehen. Ein Nutzer von next() wird also auf jeden Fall die Methode next() mit dem Rückgabetyp Object erwarten und so im Bytecode vermerken – die Begründung hatten wir schon bei der Kovarianz im vorangehenden Abschnitt aufgeführt.

Passen wir den EndlessRandomIterator mit Generics an, ändern sich drei Dinge:

  1. Erstens wird implements Iterator zu implements Iterator<Double>.

  2. Dann wird Object next() zu Double next().

  3. Drittens kann der Code im Rumpf von next() durch das Autoboxing etwas kürzer werden, nämlich return Math.random().

Der wichtige Punkt ist, dass sich der Rückgabetyp bei next() von Object in Double ändert. An der Stelle muss der Compiler mit einer Brückenmethode eingreifen, sodass im Bytecode wieder zwei next()-Methoden stehen: einmal Object next() und dann Double next(). Denn ohne Object next() würde der alte Programmcode, der Object next() erwartet, plötzlich nicht mehr laufen, und das wäre ein Bruch der Abwärtskompatibilität.

 


Ihre Meinung?

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an kommunikation@rheinwerk-verlag.de

<< zurück
 Zum Rheinwerk-Shop
Zum Rheinwerk-Shop: Java ist auch eine Insel Java ist auch eine Insel

Jetzt Buch bestellen


 Buchempfehlungen
Zum Rheinwerk-Shop: Captain CiaoCiao erobert Java

Captain CiaoCiao erobert Java




Zum Rheinwerk-Shop: Java SE 9 Standard-Bibliothek

Java SE 9 Standard-Bibliothek




Zum Rheinwerk-Shop: Algorithmen in Java

Algorithmen in Java




Zum Rheinwerk-Shop: Objektorientierte Programmierung

Objektorientierte Programmierung




 Lieferung
Versandkostenfrei bestellen in Deutschland, Österreich und in die Schweiz

InfoInfo



 

 


Copyright © Rheinwerk Verlag GmbH 2021

Für Ihren privaten Gebrauch dürfen Sie die Online-Version natürlich ausdrucken. Ansonsten unterliegt das Openbook denselben Bestimmungen, wie die gebundene Ausgabe: Das Werk einschließlich aller seiner Teile ist urheberrechtlich geschützt.

Alle Rechte vorbehalten einschließlich der Vervielfältigung, Übersetzung, Mikroverfilmung sowie Einspeicherung und Verarbeitung in elektronischen Systemen.

 

[Rheinwerk Computing]



Rheinwerk Verlag GmbH, Rheinwerkallee 4, 53227 Bonn, Tel.: 0228.42150.0, Fax 0228.42150.77, service@rheinwerk-verlag.de



Cookie-Einstellungen ändern