8.2 Die Klasse »ArrayList« 

ArrayList gehört zu den Klassen, die das Interface IList implementieren. Ein Objekt vom Typ ArrayList hat standardmäßig eine Kapazität von 0 Elementen. Fügen Sie das erste hinzu, wird die Kapazität auf 4 Elemente erhöht. Wird das fünfte Element hinzugefügt, verdoppelt sich die Kapazität automatisch, also auf acht Elemente. Grundsätzlich wird die Kapazität immer verdoppelt, wenn versucht wird, ein Element mehr hinzuzufügen, als es die aktuelle Kapazität erlaubt. Dabei werden die ArrayList-Elemente im Speicher umgeschichtet, was einen Leistungsverlust nach sich zieht, der umso größer ist, je mehr Elemente sich bereits in der ArrayList befinden.
Sie sollten daher von Anfang an der ArrayList eine angemessene Kapazität zubilligen. Das können Sie am besten machen, indem Sie den einfach parametrisierten Konstruktor aufrufen und diesem die gewünschte Anfangskapazität mitteilen. Eine weitere Möglichkeit bietet auch die Eigenschaft Capacity.
8.2.1 Einträge hinzufügen 

Mit der Methode Add können Objekte einer ArrayList-Instanz hinzugefügt werden. Da ArrayList eine 0-basierte Auflistung ist, wird das erste Element den Index 0 haben, das zweite den Index 1 usw. Sie haben mit der Add-Methode keinen Einfluss darauf, an welcher Position das Objekt in der Liste aufgenommen wird, denn es wird an das Listenende gesetzt. Wollen Sie wissen, welchen Index ein hinzugefügtes Objekt erhalten hat, brauchen Sie nur den Rückgabewert der Add-Methode auszuwerten.
ArrayList liste = new ArrayList(); int index = liste.Add("Conie");
Die Add-Methode ist sehr flexibel und definiert ihren Parameter vom Typ Object. Sie können also alles kunterbunt in die Liste packen, vom String über einen booleschen Wert, von einem Button- bis hin zu einem Circle-Objekt. Spätestens zu dem Zeitpunkt, zu dem Sie die einzelnen Elemente auswerten wollen, werden Sie in Schwierigkeiten geraten, wenn Sie nicht exakt wissen, welcher Typ sich hinter einem bestimmten Element verbirgt. Genau das ist auch der Nachteil der ArrayList. Seien Sie also vorsichtig mit den Elementen – oder nutzen Sie sofort das generische Gegenstück der ArrayList, die Klasse List<T>. Dieser werden wir uns noch später in diesem Kapitel widmen.
Über die Methode Add hinaus bietet ArrayList mit AddRange eine weitere, ähnliche Methode an, der Sie auch ein herkömmliches Array übergeben können:
ArrayList liste = new ArrayList(); int[] array = {0, 10, 22, 9, 45}; liste.AddRange(array);
Liegt das Array schon bei der Instanziierung von ArrayList vor, kann das Array auch direkt dem Konstruktor übergeben werden:
ArrayList arr = new ArrayList(intArr);
Collection-Initialisierer
Eine weitere Möglichkeit, einer ArrayList Elemente hinzuzufügen, sind Collection-Initialisierer. Mithilfe von Collection-Initialisierern kann man bei der Initialisierung eines Collection-Objekts elegant Elemente hinzufügen. Man verwendet geschweifte Klammern, in denen die einzelnen Elemente durch Kommata voneinander getrennt sind – was dann beispielsweise wie folgt aussieht:
ArrayList liste = new ArrayList() {"Aachen", "Bonn", "Köln", "Düsseldorf" };
Collection-Initialisierer verringern den Codierungsaufwand ein wenig, da nicht immer wieder die Add-Methode aufgerufen werden muss.
Einträge aus einer ArrayList löschen
Mit der Methode Clear können alle Elemente aus der ArrayList gelöscht werden. Die ArrayList wird danach, obwohl sie leer ist, ihre ursprüngliche Kapazität beibehalten, sie schrumpft also nicht.
Löschen Sie einzelne Elemente, bieten sich die Methoden Remove und RemoveAt an. Remove erwartet das zu löschende Objekt, RemoveAt den Index des zu löschenden Objekts. Beim Löschen ist ein ganz besonderes Verhalten auf der IList basierenden Auflistungen zu erkennen, das wir uns nun in einem Beispiel ansehen wollen.
// ------------------------------------------- // Beispiel: ...\Kapitel 8\ArrayListSample // ------------------------------------------- using System.Collections; class Program { static void Main(string[] args) { ArrayList liste = new ArrayList() {"Peter", "Andreas", "Conie", "Michael", "Gerd", "Freddy"}; PrintListe(liste); liste.Remove("Andreas"); Console.WriteLine("--- Element gelöscht ---"); PrintListe(liste); Console.ReadLine(); } static void PrintListe(IList liste) { foreach (string item in liste) Console.WriteLine("Index: {0,-3}{1}", liste.IndexOf(item), item); } }
Beachten Sie bitte bei diesem und allen anderen Beispielen zu den nichtgenerischen Auflistungsklassen, dass per Vorgabe der Namespace System.Collections.Generic mit using bekannt gegeben ist, wir aber für diese Klassen System.Collections benötigen.
Die Methode PrintListe sorgt für die Ausgabe der Elemente an der Konsole. Das Übergabeargument ist vom Typ IList definiert. Daher könnte die Methode von allen IList implementierenden Klassen benutzt werden, beispielsweise auch von jedem Array – vorausgesetzt, in der Auflistung werden Zeichenfolgen verwaltet.
Nach dem Füllen der Auflistung wird sofort der Inhalt auf der Konsole ausgegeben. Neben der Zeichenfolge wird dabei auch noch der aktuelle Index angezeigt, unter dem die Zeichenfolge eingetragen ist. Der aktuelle Index eines Elements lässt sich mit der Methode IndexOf unter Übergabe des Elements einfach ermitteln.
Nach der Ausgabe der Liste wird das sich an zweiter Position (Index = 1) befindliche Element mit Remove aus der Auflistung gelöscht. Die Ausgabe der aktualisierten Liste entlarvt das eben angedeutete ganz besondere Verhalten der indexbasierten Collections: Der Index, den das aus der Liste gelöschte Element innehatte, bleibt nicht leer. Stattdessen verschieben sich alle nachfolgenden Elemente in der Weise, dass kein leerer Index zurückbleibt (siehe auch Abbildung 8.1).
Abbildung 8.1 Ein Element aus der Auflistung löschen
Hinweis |
Sollte sich dasselbe Objekt mehrfach in der Liste befinden, wird nur das Objekt entfernt, das zuerst gefunden wird. Befände sich in dem oben erwähnten Beispiel der Name Andreas auch noch unter Index = 6, wird nur der Eintrag unter dem Index = 1 entfernt. Sie können doppelte Einträge in eine Liste vermeiden, wenn Sie vor dem Hinzufügen des Elements mit Contains prüfen, ob sich das Element eventuell bereits in der Liste befindet. |
Möchten Sie während eines Schleifendurchlaufs ein Element aus der Liste löschen, ist eine foreach-Schleife als Schleifenkonstrukt denkbar ungeeignet, denn die Methoden des Interface IEnumerator funktionieren nur dann, wenn sich die Liste während des Schleifendurchlaufs nicht verändert.
Eine Lösung in solchen Fällen ist die Verwendung der for- oder while-Schleife, z. B.:
for(int index = 0; index < liste .Count;index++) if( (string)liste[index] == "Andreas") liste.RemoveAt(index);
Auf ein Listenelement wird über seinen Index zugegriffen, indem der Index in eckige Klammern gesetzt wird. Da die Elemente als Object-Typen in die ArrayList eingetragen werden, ist eine Konvertierung in den passenden Typ notwenig, in unserem Code also in string.
8.2.2 Datenaustausch zwischen einem Array und einer ArrayList 

Auflistungen zeichnen sich durch die beiden Interfaces IEnumerable und ICollection aus. Aus dem letztgenannten Interface stammt die Methode CopyTo, die es ermöglicht, die Einträge einer Auflistung in ein Array zu kopieren.
ArrayList liste = new ArrayList(); liste.Add("Anton"); liste.Add("Gustaf"); liste.Add("Fritz"); string[] arr = new string[10]; liste.CopyTo(arr, 3);
Der zweite Parameter von CopyTo gibt den Startindex im Array an, ab dem die Elemente der ArrayList in das Array kopiert werden. Das Array muss groß genug sein, um alle Elemente aufzunehmen, sonst wird eine Exception ausgelöst. Handelt es sich bei den zu kopierenden Einträgen um Objektreferenzen, werden nicht die Objekte, sondern nur die Referenzen kopiert. ArrayList überlädt CopyTo, sodass auch spezifizierte Teilbereiche der Liste kopiert werden können.
8.2.3 Die Elemente einer ArrayList sortieren 

Um die Mitglieder einer ArrayList zu sortieren, wird die Methode Sort aufgerufen. Sort ist mehrfach überladen. Wir wollen uns zunächst mit der parameterlosen Version beschäftigen.
Die parameterlose »Sort«-Methode
Um die Elemente einer ArrayList mit der parameterlosen Sort-Methode zu sortieren, müssen die Typdefinitionen der Elemente die Schnittstelle IComparable implementieren. Diese Schnittstelle beschreibt nur die Methode CompareTo:
public interface IComparable { int CompareTo(object obj); }
Eine Klasse, die IComparable implementiert, garantiert, dass die Methode CompareTo existiert. Darauf ist die Sort-Methode der ArrayList angewiesen. Aus der .NET-Dokumentation zu CompareTo können wir entnehmen, dass das Objekt, auf dem Sort aufgerufen wird, mit dem an den Parameter übergebenen Objekt verglichen wird. Als Resultat liefert der Methodenaufruf eines der drei folgenden Ergebnisse:
- < 0, wenn das aktuelle Objekt kleiner als das Objekt obj ist
- 0, wenn das aktuelle Objekt gleich dem Objekt obj ist
- > 0, wenn das aktuelle Objekt größer als das Objekt obj ist
Die Regel, nach der im deutschsprachigen Raum sortiert wird, vergleicht die Zeichen unter Berücksichtigung der Groß- und Kleinschreibung wie folgt:
1 < 2 ... < a < A < b < B < c < C ... < y < Y < z < Z
Alle Klassen, die IComparable implementieren, sind daher ohne weiteres Coding dazu geeignet, innerhalb einer ArrayList sortiert zu werden. Das trifft insbesondere auf die elementaren Datentypen wie string, int oder double zu. Somit ist es uns auch möglich, die Listenelemente aus dem Beispiel ArrayListSample durch den Aufruf von Sort unkompliziert sortieren zu lassen.
static void Main(string[] args) {
ArrayList liste = new ArrayList() {"Peter", "Andreas", "Conie",
"Michael", "Gerd", "Freddy"};
liste.Sort();
PrintListe(liste);
Console.ReadLine();
}
Die Ausgabe der sortierten Liste sehen Sie in Abbildung 8.2.
Abbildung 8.2 Sortierte Listenelemente
Eigene Klassen mit »IComparable«
Viele Klassen des .NET Frameworks implementieren die IComparable-Schnittstelle. Das folgende Beispielprogramm soll zeigen, wie Sie dieses Interface für eigene Klassen einsetzen können. Dabei werden wir der Einfachheit halber nur mit einer sehr einfachen Klasse arbeiten, die neben der Schnittstellenimplementierung nur einen Integer-Wert in der Eigenschaft Value beschreibt und einen parametrisierten Konstruktor.
public class Demo : IComparable { public int Value {get; set;} public Demo(int value) { Value = value; } public int CompareTo(object obj) { if (((Demo)obj).Value < Value) return 1; else if (((Demo)obj).Value == Value) return 0; return -1; } }
Die Klasse Demo implementiert IComparable. Daher sind Objekte dieses Typs darauf vorbereitet, in einer ArrayList sortiert zu werden. Die Sortierreihenfolge soll sich am Inhalt des Felds Value orientieren. Da CompareTo den Parameter vom Typ Object definiert, muss vor dem Zugriff auf das Feld Value eine Typkonvertierung erfolgen. Die Bildung des Rückgabewertes bedarf keiner weiteren Beschreibung. Bei aller Einfachheit sollten Sie sich jedoch darüber im Klaren sein, dass Sie die Entscheidung selbst treffen, unter welchen Umständen die zu vergleichenden Objekte als größer, gleich oder kleiner einzustufen sind.
Anmerkung |
So, wie das Vergleichskriterium in diesem Beispiel formuliert ist, könnten wir auch die Schnittstelle IComparable des Integers für unsere Zwecke nutzen, und mit return Value.CompareTo(((Demo)obj).Value); einen deutlich kürzeren Code erzeugen. Allerdings würde dann diesem Beispiel die Allgemeingültigkeit fehlen. |
Mit der Implementierung der Methode CompareTo können wir uns aber noch nicht zufriedengeben, da nicht alle denkbaren Szenarien von unserer Implementierung berücksichtigt werden. Sie könnten nämlich auch ein Objekt übergeben, das mit dem aktuellen nicht vergleichbar ist, beispielsweise:
Circle kreis = new Circle(5); Demo element = new Demo(8); int result = element.CompareTo(kreis); ...
Implementieren Sie die Methode CompareTo, sollten Sie diesen Fall ebenso berücksichtigen wie die Eventualität, dass das übergebene Objekt noch nicht initialisiert und daher null ist. Die Implementierung, die diese beiden Szenarien einbezieht, sieht wie folgt aus:
public int CompareTo(object obj) {
// prüfen, ob der Parameter ein null-Verweis ist
if (obj == null)
return 1;
// prüfen, ob beide Typen gleich sind
if (obj.GetType() != GetType())
throw new ArgumentException("Ungültiger Vergleich.");
if (((Demo)obj).Value < this.Value)
return 1;
else if (((Demo)obj).Value == this.Value)
return 0;
return -1;
}
Generell sollten Sie die Methode CompareTo der Schnittstelle IComparable wie gezeigt implementieren, um gegen alle unzulässigen Aufrufe gewappnet zu sein und als robust zu gelten. Es wird zuerst überprüft, ob dem Parameter null übergeben wurde. In diesem Fall sollte der Vergleich abgebrochen werden und als Resultat einen Wert größer 0 liefern. Unterscheiden sich die beiden Typen des anstehenden Vergleichs, wird die Ausnahme ArgumentException ausgelöst und muss vom Aufrufer behandelt werden.
Natürlich wollen wir nun auch testen, ob wir unser Ziel erreicht haben. Dazu dient der folgende Code:
// -------------------------------------------
// Beispiel: ...\Kapitel 8\IComparableSample
// -------------------------------------------
class Program {
static void Main(string[] args) {
Demo[] arr = new Demo[] { new Demo(12), new Demo(27),
new Demo(35), new Demo(3) };
ArrayList liste = new ArrayList();
liste.AddRange(arr);
liste.Sort();
foreach (Demo item in liste) {
Console.WriteLine("Index: {0} / Wert: {1}",
liste.IndexOf(item), item.Value);
}
Console.ReadLine();
}
}
An der Konsole werden die Werte der Felder in der Reihenfolge 3, 13, 27 und 35 ausgegeben. Der Vergleich und die anschließende Sortierung finden also wie erwartet statt.
Vergleichsklassen mit »IComparer«
Das Sortieren einer ArrayList mit der parameterlosen Sort-Methode gestattet nur ein Vergleichskriterium. Manchmal ist es aber erforderlich, unterschiedliche Sortierkriterien zu berücksichtigen. Nehmen wir zum Beispiel die Klasse Person, die die beiden Felder Name und Wohnort beschreibt:
class Person { public string Name {get; set;} public string Wohnort {get; set;} public Person(string name, string ort) { Name = name; Wohnort = ort; } }
Würde die Klasse die Schnittstelle IComparable implementieren, müsste die Entscheidung getroffen werden, nach welchem Feld Objekte dieser Klasse sortiert werden können. Nun sollen beide Möglichkeiten angeboten werden: sowohl die Sortierung nach Wohnort als auch nach Name.
Die Lösung des Problems führt über die Bereitstellung sogenannter Vergleichsklassen, die die Schnittstelle IComparer implementieren. Jede Vergleichsklasse beschreibt genau ein Vergleichskriterium. Wollen wir einen bestimmten Objektvergleich erzwingen, müssen wir der Sort-Methode mitteilen, welche Vergleichsklasse dafür bestimmt ist. Dafür stehen uns zwei Überladungen zur Verfügung, denen die Referenz auf ein Objekt übergeben wird, das die Schnittstelle IComparer implementiert:
public virtual void Sort(IComparer); public virtual void Sort(int, int, IComparer);
Mit der Überladung, die zwei Integer erwartet, kann der Startindex und die Länge des zu sortierenden Bereichs bestimmt werden. Bei sehr großen Auflistungen steigert das die Performance, da Sortiervorgänge sehr rechenintensiv sind.
Die Schnittstelle IComparer stellt die Methode Compare für den Vergleich zweier Objekte bereit:
int Compare(object x, object y);
Compare funktioniert ähnlich wie die weiter oben erörterte Methode CompareTo und gibt die folgenden Werte zurück:
- < 0, wenn das erste Objekt kleiner als das zweite Objekt ist
- 0, wenn das erste Objekt gleich dem zweiten Objekt ist
- > 0, wenn das erste Objekt größer als das zweite Objekt ist
Für die Klasse Person wollen wir nun die beiden Vergleichsklassen NameComparer und WohnortComparer entwickeln, die gemäß unserer Anforderung die Schnittstelle IComparer implementieren und nach Wohnort bzw. Name sortieren.
// Vergleichsklasse - Kriterium 'Wohnort' class WohnortComparer : IComparer { public int Compare(object x, object y) { // prüfen auf null-Übergabe if(x == null && y == null) return 0; if(x == null) return 1; if(y == null) return -1; // Typüberprüfung if(x.GetType() != y.GetType()) throw new ArgumentException("Ungültiger Vergleich"); // Vergleich return ((Person)x).Wohnort.CompareTo(((Person)y).Wohnort); } } // Vergleichsklasse - Kriterium 'Name' class NameComparer : IComparer { public int Compare(object x, object y) { // prüfen auf null-Übergabe if(x == null && y == null) return 0; if(x == null) return 1; if(y == null) return -1; // Typüberprüfung if(x.GetType() != y.GetType()) throw new ArgumentException("Ungültiger Vergleich"); // Vergleich return ((Person)x).Name.CompareTo(((Person)y).Name); } }
Die Implementierung ähnelt der der Methode CompareTo. Zuerst sollte wieder ein Vergleich mit null durchgeführt werden und anschließend eine Prüfung, ob beide Parameter denselben Typ beschreiben oder zumindest einen vergleichbaren Typ besitzen. Sollte keine Bedingung zutreffen, kann der Vergleich der Objekte erfolgen. Dabei unterstützt uns die Klasse String, die ihrerseits die IComparable-Schnittstelle implementiert und folglich die Methode CompareTo veröffentlicht.
Haben wir ein ArrayList-Objekt mit Person-Objekten gefüllt, steht es uns frei, welche Vergleichsklasse wir zur Sortierung der Objekte benutzen, denn beide sind auf dieselbe Schnittstelle zurückzuführen und gegenseitig austauschbar.
Natürlich können Sie auch jederzeit die Klasse Person um die Schnittstelle IComparer erweitern. Syntaktisch bereitet das zumindest bei einem erforderlichen Vergleichskriterium überhaupt kein Problem. Andererseits müssen Sie sich auch vor Augen halten, wie Sie die Sort-Methode aufrufen müssten:
liste.Sort(new Person());
Dieser Code suggeriert, dass wir es mit einem weiteren, neuen Person-Objekt zu tun haben, obwohl wir das Objekt doch eigentlich nur dazu missbrauchen, das Vergleichskriterium der Sort-Methode bekannt zu geben. Ähnlich schlecht les- und interpretierbarer Code wäre das Resultat, wenn wir mit
liste.Sort(person1);
irgendein bekanntes Person-Objekt übergeben. Daher sollten Sie von dieser Codeimplementierung Abstand nehmen.
Sehen wir uns nun das Beispielprogramm an, in dem die oben beschriebenen Vergleichskriterien benutzt werden.
// --------------------------------------------------------- // Beispiel: ...\Kapitel 8\IComparerSample // --------------------------------------------------------- class Program { static void Main(string[] args) { ArrayList arrList = new ArrayList(); // ArrayList füllen Person pers1 = new Person("Meier", "Berlin"); arrList.Add(pers1); Person pers2 = new Person("Arnhold", "Köln"); arrList.Add(pers2); Person pers3 = new Person("Graubär", "Aachen"); arrList.Add(pers3); // nach Wohnorten sortieren arrList.Sort(new WohnortComparer()); Console.WriteLine("Liste nach Wohnorten sortiert"); ShowSortedList(arrList); // nach Namen sortieren arrList.Sort(new NameComparer()); Console.WriteLine("Liste nach Namen sortiert"); ShowSortedList(arrList); } static void ShowSortedList(IList liste) { foreach(Person temp in liste) { Console.Write("Name = {0,-12}", temp.Name); Console.WriteLine("Wohnort = {0}", temp.Wohnort); } Console.WriteLine(); } }
Abbildung 8.3 Ausgabe des Beispiels »IComparerSample«
8.2.4 Sortieren von Arrays mit »ArrayList.Adapter« 

Ein herkömmliches Array bietet von Haus aus keine Möglichkeit, die in ihm enthaltenen Elemente zu sortieren. Es gibt dennoch einen Weg, der über die klassische Methode Adapter der Klasse ArrayList führt.
public static ArrayList Adapter(IList list);
Der Methode wird ein IList-Objekt übergeben. Der Zufall will es, dass ein klassisches Array diese Schnittstelle implementiert. Adapter legt einen Wrapper (darunter ist eine Klasse zu verstehen, die sich um eine andere legt) um das IList-Objekt. Der Rückgabewert ist die Referenz auf ein neues ArrayList-Objekt, mit dessen Methoden, unter anderem auch Sort, sich das IList-Objekt manipulieren lässt.
Wie Sie die Methode Adapter einsetzen können, möchte ich Ihnen an einem Beispiel zeigen. Dabei dient wieder die Klasse Person aus dem Beispiel IComparerSample als Grundlage. Zudem soll auch wieder die Möglichkeit eröffnet werden, entweder nach Namen oder Wohnort sortieren zu können. Dazu können Sie die ebenfalls vorhandenen Vergleichsklassen des Beispiels aus dem letzten Abschnitt wiederverwenden.
// ----------------------------------------------------------- // Beispiel: ...\Kapitel 8\ArrayListAdapterSample // ----------------------------------------------------------- class Program { static void Main(string[] args){ Person[] pers = new Person[3]; pers[0] = new Person("Peter", "Celle"); pers[1] = new Person("Alfred", "München"); pers[2] = new Person("Hugo", "Aachen"); ArrayList liste = ArrayList.Adapter(pers); // Sortierung nach Namen liste.Sort(new NameComparer()); Console.WriteLine("Sortiert nach den Namen:"); for (int index = 0; index < 3; index ++) if( liste[index] != null) Console.WriteLine(((Person)liste[index]).Name); // Sortierung nach Wohnort Console.WriteLine("\nSortiert nach dem Wohnort:"); liste.Sort(new WohnortComparer()); for (int index = 0; index < 3; index ++) if( liste[index] != null) Console.WriteLine(((Person)liste[index]).Wohnort); Console.ReadLine(); } }
Bei der Ausgabe der sortierten Listenelemente müssen wir ein wenig vorsichtiger sein. Denn im Gegensatz zur ArrayList, die uns garantiert, dass sich hinter jedem Index ein gültiges Objekt verbirgt, kann der Index in einem klassischen Array null sein. Der Versuch einer Ausgabe oder ganz allgemein des Zugriffs auf ein null-Element würde mit einer Ausnahme quittiert werden. Daher ist unbedingt darauf zu achten, vor der Ausgabe mit
if (liste[index] != null)
auf den Inhalt null zu prüfen.