![]() |
|
|
Primitive Elemente in den Collection-DatenstrukturenDie eingefügte Zahl durch das Integer-Objekt macht deutlich, dass als Elemente nur Objekte akzeptiert werden und keine primitiven Datentypen. Über das Boxing fügt jedoch Java 5 automatisch primitive Elemente ein, doch diese sind dann immer noch Wrapper-Objekte. Für performante Anwendungen ist es sinnvoll, eine komplette eigene Klasse für den speziellen Datentyp einzusetzen. So etwas muss nicht mehr selbst programmiert werden – die Lösung ist etwas GNU Trove: high performance collections for Java, unter http://trove4j.sourceforge.net/. Kopieren, Ausschneiden und EinfügenEine wichtige Methode, die ArrayList von AbstractList überschreibt, ist clone(). Damit lässt sich leicht eine Kopie der Liste erzeugen. Es entsteht so allerdings nur eine flache Kopie der Datenstruktur. Ein besonderer Witz ist removeRange(int, int). Die Methode wird zwar überschrieben, sie ist aber immer noch protected1 . Die API-Dokumentation beschreibt dazu, dass removeRange() nicht zur offiziellen Schnittstelle von Listen gehört, sondern für die Autoren neuer Listenimplementierungen gedacht ist. Beispielsweise kann removeRange() für ArrayList-Objekte deutlich effizienter implementiert werden als in der Klasse AbstractList. Das offizielle Idiom für die Funktionalität von removeRange() ist subList(from, to).clear(). Die subList()-Technik erschlägt gleich noch ein paar andere Operationen, für die es keine speziellen Range-Varianten gibt, zum Beispiel indexOf(), also die Suche in einem Teil der Liste. Listing 11.3 ArrayListDemo.java import java.util.*; public class ArrayListDemo { public static void main( String args[] ) { ArrayList<String> a = new ArrayList<String>(); for ( int i = 1; i <= 20; i++ ) a.add( "" + i ); a.subList( a.size()/2-3, a.size()/2+3 ).clear(); System.out.println( a ); // Iterator besorgen und Reihe ausgeben ListIterator it = a.listIterator( a.size() ); while( it.hasPrevious() ) System.out.print( it.previous() + " " ); System.out.println(); // Datenstruktur kürzen und klonen a.subList( 4, a.size() ).clear(); System.out.println( a ); } } Zusätzlich sehen wir hier noch einen ganz normalen ListIterator im Einsatz. Dann ist die Ausgabe Folgende: [1, 2, 3, 4, 5, 6, 7, 14, 15, 16, 17, 18, 19, 20] 20 19 18 17 16 15 14 7 6 5 4 3 2 1 [1, 2, 3, 4] 11.3.4 asList() und die »echten« Listen
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Beispiel Eine Anwendung von toArray(), die Punkte in ein Feld kopiert
ArrayList<Point> l = new ArrayList<Point>(); l.add( new Point(13,43) ); l.add( new Point(9,4) ); Object points[] = l. toArray(); |
Wir erhalten nun ein Feld mit Referenzen auf Point-Objekte. Doch wir können zum Beispiel nicht einfach points[1].x schreiben, um auf das Attribut des Point-Exemplars zuzugreifen, denn das Array points hat den deklarierten Elementtyp Object. Es fehlt die explizite Typumwandlung, und erst ((Point)points[1]).x ist korrekt. Doch spontan kommen wir sicherlich auf die Idee, einfach den Typ des Arrays auf Point zu ändern. In dem Array befinden sich ja nur Referenzen auf Point-Exemplare.
Point points[] = l.toArray(); // Vorsicht!
Jetzt wird der Compiler einen Fehler melden, da der Rückgabewert von toArray() ein Object[] ist. Spontan reparieren wir dies, indem wir eine Typumwandlung auf ein Point-Array an die rechte Seite setzen.
Point points[] = (Point[]) l.toArray(); // Gefährlich!
Jetzt haben wir zur Übersetzungszeit kein Problem mehr, aber zur Laufzeit wird es immer knallen, auch wenn sich im Array tatsächlich nur Point-Objekte befinden.
Diesen Programmierfehler müssen wir verstehen. Was wir falsch gemacht haben, ist einfach: Wir haben den Typ des Arrays mit den Typen der Array-Elemente durcheinander gebracht. Einem Array von Objekt-Referenzen können wir alles zuweisen.
Object os[] = new Object[3]; os[0] = new Point(); os[1] = "Trecker fahr’n"; os[2] = new Date();
Wir merken, dass der Typ des Arrays Object[] ist, und die Array-Elemente sind ebenfalls vom Typ Object. Hinter dem new-Operator, der das Array-Objekt erzeugt, steht der gemeinsame Obertyp für zulässige Array-Elemente. Bei Object[]-Arrays dürfen die Elemente Referenzen für beliebige Objekte sein. Klar ist, dass ein Array nur Objektreferenzen aufnehmen kann, die mit dem Typ für das Array selbst kompatibel sind, also sich auf Exemplare der angegebenen Klasse beziehen oder auf Exemplare von Unterklassen dieser Klasse.
/* 1 */ Object os[] = new Point[3]; /* 2 */ os[0] = new Point(); /* 3 */ os[1] = new Date(); // !! /* 4 */ os[2] = "Trecker fahr’n"; // !!
Zeile 3 und 4 sind vom Compiler erlaubt, führen aber zur Laufzeit zu einer java.lang.ArrayStoreException.
Kommen wir wieder zurück zur Methode toArray(). Da die auszulesende Datenstruktur alles Mögliche enthalten kann, muss also der Typ der Elemente Object sein. Wir haben gerade festgestellt, dass der Elementtyp des Array-Objekts, das die Methode toArray() als Ergebnis liefert, mindestens so umfassend sein muss. Da es einen allgemeineren (umfassenderen) Typ als Object nicht gibt, ist auch der Typ des Arrays Object[]. Dies muss so sein, auch wenn die Elemente einer Datenstruktur im Einzelfall einen spezielleren Typ haben. Einer allgemein gültigen Implementierung von toArray() bleibt gar nichts anderes übrig, als das Array vom Typ Object[] und die Elemente vom Typ Object zu erzeugen.
public Object[] toArray() { Object[] objs = new Object[size()]; Iterator it = iterator(); for (int i = 0; i < objs.length; i++) { objs[i] = it.next(); } return objs; }
Wenn sich auch die Elemente wieder auf einen spezielleren Typ konvertieren lassen, ist das jedoch bei dem Array-Objekt selbst nicht der Fall. Ein Array-Objekt mit Elementen vom Typ X ist nicht automatisch auch selbst vom Typ X[], sondern von einem Typ Y[], wobei Y eine (echte) Oberklasse von X ist.
Bevor wir nun eine Schleife mit einer Typumwandlung für jedes einzelne Array-Element schreiben oder eine Typumwandlung bei jedem Zugriff auf die Elemente vornehmen, sollten wir einen Blick auf die zweite toArray()-Funktion werfen. Sie akzeptiert als Parameter ein vorgefertigtes Array für das Ergebnis. Mit dieser Funktion lässt sich erreichen, dass das Ergebnis-Array von einem spezielleren Typ als Object[] ist.
Beispiel Wir fordern von der toArray()-Funktion ein Feld vom Typ Point.
ArrayList<Point> l = new ArrayList<Point>(); l.add( new Point(13,43)); l.add( new Point(9,4)); Point points[] = (Point[]) l. toArray( new Point[0]); |
Jetzt bekommen wir die Listen-Elemente in ein Array kopiert, und der Typ des Arrays ist, passend zu den aktuell vorhandenen Listen-Elementen, Point[].
Spannend ist die Frage, wie so etwas funktionieren kann. Dazu verwendet die Methode toArray(Object[]) die Technik Reflection, um dynamisch ein Array vom gleichen Typ wie das übergebene Array zu erzeugen. Wollten wir ein Array b vom Typ des Arrays a mit Platz für len Elemente anlegen, so schreiben wir
Object b[] = (Object[]) Array.newInstance (a.getClass().getComponentType(), len );
Mit a.getClass().getComponentType() erhalten wir ein Class-Objekt für den Elementtyp des Arrays, zum Beispiel das Class-Objekt Point.class für die Klasse Point. a.getClass() allein liefert ein Class-Objekt für das Array a, etwa ein Objekt, das den Typ Point[] repräsentiert. Array.newInstance(), eine statische Methode von java.lang.reflect.Array, konstruiert ein neues Array mit dem Elementtyp aus dem Class-Objekt und der angegebenen Länge. Nichts anderes macht auch ein new X[len], nur dass hier der Elementtyp zur Übersetzungszeit festgelegt werden muss. Da der Rückgabewert von newInstance() ein allgemeines Object ist, muss letztendlich noch die Konvertierung in ein passendes Array stattfinden.
Ist das übergebene Array so groß, dass es alle Elemente der Collection aufnehmen kann, kopiert toArray() die Elemente aus der Collection in das Feld. Oft wird aber dort ein new X[0] anzeigen, dass wir ein neu erzeugtes Array-Objekt wünschen. Im Übrigen entspricht natürlich toArray(new Object[0]) dem Aufruf von toArray(). Dennoch gibt die Java-Bibliothek zwei völlig getrennte Implementierungen an, da toArray()einfacher und effizienter zu implementieren ist.
Eine ArrayList beziehungsweise Vector muss zwei Größen verwalten: zum einen die Anzahl der gespeicherten Elemente nach außen, zum anderen die interne Größe des Felds. Ist die Kapazität des Felds größer als die Anzahl der Elemente, so können noch Elemente aufgenommen werden, ohne dass die Liste etwas unternehmen muss. Die Anzahl der Elemente in der Liste, die Größe, liefert die Methode size(), und die Kapazität des darunter liegenden Arrays liefert capacity().
Die Liste vergrößert sich automatisch, falls mehr Elemente aufgenommen werden als ursprünglich am Platz vorgesehen waren. Diese Operation heißt Resizing. Dabei spielt die Größe initialCapacity für effizientes Arbeiten eine wichtige Rolle. Sie sollte passend gewählt sein. Betrachten wir daher zunächst die Funktionsweise der Liste, falls das interne Array zu klein ist.
Wenn das Array zehn Elemente fasst, nun aber ein Elftes eingefügt werden soll, so muss das Laufzeitsystem einen neuen Speicherbereich reservieren und jedes Element des alten Felds in das neue kopieren. Dies kostet Zeit. Schon aus diesem Grund sollte der Konstruktor ArrayList(int initialCapacity)/Vector(int initialCapacity) gewählt werden, da dieser eine Initialgröße festsetzt. Das Wissen über unsere Daten hilft dann der Datenstruktur. Falls kein Wert voreingestellt wurde, so werden zehn Elemente angenommen. In vielen Fällen ist dieser Wert zu klein.
Nun haben wir zwar darüber gesprochen, dass ein neues Feld angelegt wird und die Elemente kopiert werden, haben aber nichts über die Größe des neuen Felds gesagt. Hier gibt es Strategien wie die »Verdopplungsmethode« beim Vector. Wird er vergrößert, so ist das neue Feld doppelt so groß wie das Alte. Dies ist eine Vorgehensweise, die für kleine und schnell wachsende Felder eine clevere Lösung darstellt, für große Felder aber schnell zum Verhängnis werden kann. Für den Fall, dass wir die Vergrößerung selbst bestimmen wollen, nutzen wir den Konstruktor Vector(int initialCapacity, int capacityIncrement), der die Verdopplung ausschaltet und eine fixe Vergrößerung befiehlt. Die ArrayList verdoppelt nicht, sie nimmt die neue Größe mal 1,5. Bei ihr gibt es leider auch den capacityIncrement im Konstruktor nicht.
Die interne Größe des Arrays kann mit ensureCapacity() geändert werden. Ein Aufruf von ensureCapacity(int minimumCapacity) bewirkt, dass die Liste insgesamt mindestens minimumCapacity Elemente aufnehmen kann, ohne dass ein Resizing nötig wird. Ist die aktuelle Kapazität der Liste kleiner als minimumCapacity, so wird mehr Speicher angefordert. Der Vektor verkleinert die aktuelle Kapazität nicht, falls sie schon höher als minimumCapacity ist. Um aber auch diese Größe zu ändern und somit ein nicht mehr wachsendes Vektor-Array so groß wie nötig zu machen, gibt es, ähnlich wie beim String mit Leerzeichen, die Methode trimToSize(). Sie reduziert die Kapazität des Vektors auf die Anzahl der Elemente, die gerade in der Liste sind. Mit size() lässt sich die Anzahl der Elemente in der Liste erfragen. Sie gibt die wirkliche Anzahl der Elemente zurück.
Bei der Klasse Vector lässt sich mit setSize(int newSize) auch die Größe der Liste verändern. Ist die neue Größe kleiner als die Alte, werden die Elemente am Ende des Vektors abgeschnitten. Ist newSize größer als die alte Größe, werden die neu angelegten Elemente mit null initialisiert.2 Vorsicht ist bei newSize=0 geboten, denn setSize(0) bewirkt das Gleiche wie removeAllElements().
Eine verkettete Liste hat neben den normalen Funktionen aus AbstractList noch weitere Hilfsmethoden, um leicht einen Stack oder eine Schlange zu programmieren. Es handelt sich dabei um die Funktionen addFirst(), addLast(), getFirst(), getLast(), removeFirst() und removeLast().
1 AbstractList ist removeRange() gültig mit einem ListIterator implementiert.
2 Zudem können null-Referenzen ganz normal als Elemente eines Vektors auftreten, bei den anderen Datenstrukturen gibt es Einschränkungen
| << zurück |
Copyright © Galileo Press GmbH 2004
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.