Galileo Computing <openbook>
Galileo Computing - Programming the Net
Galileo Computing - Programming the Net


Java ist auch eine Insel von Christian Ullenboom
Programmieren für die Java 2-Plattform in der Version 1.4
Buch: Java ist auch eine Insel - Zum Katalog
gp Kapitel 3 Klassen und Objekte
  gp 3.1 Objektorientierte Programmierung
    gp 3.1.1 Warum überhaupt OOP?
    gp 3.1.2 Modularität und Wiederverwertbarkeit
  gp 3.2 Klassen benutzen
    gp 3.2.1 Die Klasse Point
    gp 3.2.2 Etwas über die UML
    gp 3.2.3 Anlegen eines Exemplars einer Klasse
    gp 3.2.4 Zugriff auf Variablen und Methoden mit dem Punkt
    gp 3.2.5 Konstruktoren
    gp 3.2.6 Die null-Referenz
  gp 3.3 Mit Referenzen arbeiten
    gp 3.3.1 Zuweisungen bei Referenzen
    gp 3.3.2 Funktionen mit nicht-primitiven Parametern
    gp 3.3.3 Gleichheit von Objekten und die Methode equals()
  gp 3.4 Arrays
    gp 3.4.1 Deklaration von Arrays
    gp 3.4.2 Arrays mit Inhalt
    gp 3.4.3 Die Länge eines Arrays mit length
    gp 3.4.4 Zugriff auf die Elemente
    gp 3.4.5 Array-Objekte erzeugen
    gp 3.4.6 Fehler bei Arrays
    gp 3.4.7 Arrays mit nicht-primitiven Elementen
    gp 3.4.8 Arrays und Objekte
    gp 3.4.9 Initialisierte Array-Objekte
    gp 3.4.10 Mehrdimensionale Arrays
    gp 3.4.11 Die Wahrheit über die Array-Initialisierung
    gp 3.4.12 Arrays kopieren und füllen
    gp 3.4.13 Mehrere Rückgabeparameter
    gp 3.4.14 Parameter per Referenz übergeben
    gp 3.4.15 Der Einstiegspunkt für das Laufzeitsystem
    gp 3.4.16 Der Rückgabewert von main()

Kapitel 3 Klassen und Objekte

Nichts auf der Welt ist so gerecht verteilt wie der Verstand.
Jeder glaubt, er hätte genug davon.


Galileo Computing

3.1 Objektorientierte Programmierung  downtop

Da Java eine objektorientierte Programmiersprache ist, müssen die Konzepte dieses Paradigmas bekannt sein. Erstaunlicherweise sind dies nicht viele, denn objektorientiertes Programmieren (OOP) basiert nur auf einigen wenigen Ideen. Werden diese beachtet, wird OOP nicht zum Verhängnis und der Vorteil gegenüber modularem Programmieren kann ausgeschöpft werden. Bjarne Stroustrup (Schöpfer von C++, von seinen Freunden auch Stumpy genannt) sagte treffend über den Vergleich von C und C++: »C makes it easy to shoot yourself in the foot, C++ makes it harder, but when you do, it blows away your whole leg.«

Hinweis Herkunft der OO-Sprachen: Java ist natürlich nicht die erste OO-Sprache, auch nicht C++. Klassischerweise gelten Smalltalk und insbesondere Simula-67 als Säulen aller OO-Sprachen. Die eingeführten Konzepte sind bis heute aktuell, darunter die vier allgemein anerkannten Prinzipien der OOP: Abstraktion, Kapselung, Vererbung und Polymorphie.


Galileo Computing

3.1.1 Warum überhaupt OOP?  downtop

In der frühen Softwareentwicklung haben sich zwei Modelle herausgebildet, um Programme zu entwerfen: Top-Down- und Bottom-Up-Analyse. Beide beschreiben jeweils eine Möglichkeit, Software durch schrittweises Verfeinern zu entwerfen. Bei der Top-Down-Analyse steht das Gesamtprogramm im Mittelpunkt und es wird nach den Funktionen gefragt, um diese an der oberen Stelle benötigte Funktionalität implementieren zu können. Ein Beispiel: Es soll ein Fahrplanauskunftsprogramm geschrieben werden. An oberster Stelle verwenden wir drei Funktionen, eine Funktion, die das Programm initialisiert, eine, die die Bildschirmmaske aufbaut und eine, die die Benutzereingaben entgegennimmt. Anschließend modellieren wir diese drei Funktionen mittels weiterer Funktionen. In der Funktion »Initialisieren« beispielsweise durch Speicher beschaffen, Informationsdatei laden, Informationen in Datenstrukturen umsortieren. Jede dieser Funktionen wird weiter verfeinert, bis die gewünschte Funktionalität erreicht ist. Der Top-Down-Ansatz eignet sich somit nur für Systeme, deren untere Stufen nacheinander entwickelt werden. Denn ohne den unteren Teil ist das gesamte Programm nicht lauffähig. Sofort wird hierdurch folgendes Problem sichtbar: Es muss von vornherein klar sein, welche Funktionen die Software hat und alle Funktionen müssen bekannt sein. Eine Modularisierung in Teilaufgaben ist schwer möglich und daran krankt diese Analyse-Technik. Schauen wir uns deshalb den Bottom-Up-Entwurf an. Dieser Entwurf geht genau in entgegengesetzter Richtung vor. Wir entwickeln erst die Komponenten der unteren Stufe und vereinigen sie dann zu einem Modul höherer Abstraktion. Problem: Diese Technik eignet sich zur Entwicklung nur dann, wenn die unteren Stufen tatsächlich eigenständig lauffähig sind.

Beide Methoden sind nicht wirklich befriedigend und so wurden Mischformen geschaffen. Mit diesen die Softwareprodukte zu gliedern, zeigte ebenfalls nur durchschnittliche Ergebnisse. Objektorientierte Programmierung wird als Schlüssel zur zukünftigen Softwareentwicklung angesehen und erweitert die Leistungsfähigkeit der beiden Analyse-Techniken Bottom-Up und Top-Down.

Wird gefragt, welche Faktoren zum Umdenken von prozeduraler zur objektorientierten Programmierung geführt haben, so lassen sich im Wesentlichen drei Eigenschaften aufzählen:

gp  In einem Programm stehen die Daten im Vordergrund
gp  Funktionen sind kurzlebiger als Daten
gp  Jede Software ist unzähligen Änderungen unterworfen

Die objektorientierte Programmierung versucht nun, diese drei Faktoren zu beachten. Stehen die Daten im Vordergrund, so müssen wir weniger in Funktionen denken, sondern in Objekten, die die Daten beschreiben. Damit Änderungen gut möglich sind, kapseln wir die Funktionen so weit von den Daten, dass sie allgemein angewendet werden können.

Wir sehen schon an dieser kurzen Beschreibung, dass ein Objekt immer im Mittelpunkt steht. Alles dreht sich um das Objekt, und dies bezeichnen wir als Objekt-Orientiert.


Galileo Computing

3.1.2 Modularität und Wiederverwertbarkeit  downtop

Die objektorientierte Programmierung stellt zwei Konzepte in den Mittelpunkt des Software-Entwurfs: Wiederverwendbarkeit (das Problem ist jedem bekannt: Programmieren wiederholt sich an allen Stellen und das Neuschreiben kann nicht vermieden werden) und Modularität. Bei der Wiederverwendung geht es darum, die Bausteine objektorientierter Systeme, die Klassen, zu nutzen. Daher wollen wir nun erst einmal bereits vorhandene Klassen verwenden. Im zweiten Schritt werden wir dann eigene Klassen programmieren. Anschließend kümmern wir uns um das Konzept der Modularität, nämlich wie Gruppen kooperierender Klassen gestaltet werden.


Galileo Computing

3.2 Klassen benutzen  downtop

Klassen sind das wichtigste Merkmal objektorientierter Programme. Eine Klasse beschreibt die Eigenschaften der Objekte und gibt somit den Bauplan an. Jedes Objekt ist ein Exemplar (auch »Instanz« oder »Ausprägung« genannt) einer Klasse.

Eine Klasse definiert

gp  Attribute (Variablen, auch »Felder« genannt)
gp  Operationen (Methoden , die Funktionen einer Klasse)
gp  weitere Klassen (innere Klassen)

Attribute und Operationen heißen auch Eigenschaften eines Objekts. Welche Eigenschaften eine Klasse tatsächlich besitzen soll, wird in der Analyse- und Design-Phase festgesetzt. Dies wird in diesem Buch kein Thema sein; für uns liegen die Klassenbeschreibungen schon vor.

Um sich einer Klasse zu nähern, können wir einen lustigen Ich-Ansatz verwenden, der auch in der Analyse- und Design-Phase eingesetzt wird. Bei diesem Ich-Ansatz versetzen wir uns in das Objekt und sagen »Ich bin...« für die Klasse und »Ich habe...« für die Attribute und »Ich kann...« für die Operationen. Meine Leser sollte dies einmal an den Klassen Mensch, Auto, Wurm und Kuchen testen.


Galileo Computing

3.2.1 Die Klasse Point  downtop

Bevor wir uns mit eigenen Klassen beschäftigen, wollen wir zunächst einige Klassen aus der Standardbibliothek kennen lernen. Eine dieser Klassen ist Point. Sie beschreibt einen Punkt in einer zweidimensionalen Ebene durch die Koordinaten x und y und bietet einige Funktionen an, mit denen sich Punkt-Objekte anlegen und verändern lassen. Testen wir einen Punkt wieder mit dem Objektansatz:

Klassenaname Ich bin ein Punkt.
Attribute Ich habe eine x- und y-Koordinate.
Operationen Ich kann mich verschieben und meine Position festlegen

Für die Darstellung einer Klasse lässt sich Programmcode verwenden, also eine Textform oder aber eine grafische Notation. Eine dieser grafischen Beschreibungsformen ist die UML. Grafische Abbildungen sind für Menschen deutlich besser zu verstehen und erhöhen die Übersicht.

Abbildung

Im ersten Abschnitt lassen sich die Attribute ablesen, im zweiten die Methoden. Das + davor zeigt an, dass die Eingeschafften öffentlich sind und jeder sie nutzen kann. Die Typenangabe ist umgekehrt wie in Java: Zuerst kommt der Name der Variable, dann der Typ beziehungsweise bei Methoden der Typ des Rückgabewerts.


Galileo Computing

3.2.2 Etwas über die UML  downtop

UML (Unified Modeling Language) ist mehr als nur eine Notation zur Darstellung von Klassen. Mit ihrer Hilfe lassen sich die Analyse und das Design im Softwareentwicklungsprozess beschreiben. Mittlerweile hat sich UML jedoch zu einer allgemeinen Notation für andere Beschreibungen entwickelt, zum Beispiel für Datenbanken oder Workflow-Anwendungen.

Vor UML waren andere Darstellungsvarianten wie OMT oder Booch verbreitet. Diese waren eng mit einer Methode verbunden, die einen Entwicklungsprozess und ein Vorgehensmodell beschrieben. Methoden versuchen eine Vorgehensweise beim Entwurf von Systemen zu beschreiben, etwa »erst Vererbung einsetzen und dann die Attribute finden« oder »erst die Attribute finden und dann mit Vererbung verfeinern«. Bekannte OO-Methoden sind etwa Shlaer/Mellor, Coad/Yourdon, Booch, OMT und OOSE/Objectory. Aus dem Wunsch heraus, OO-Methoden zusammenzufassen, ging UML hervor. Anfangs stand die Abkürzung noch für Unified Method. Die Urversion 0.8 war die erste Veröffentlichung im Jahre 1995. Die Initiatoren waren Jim Rumbaugh und Grady Booch. Später trat Ivar Jacobson dazu und die drei »Amigos« erweiterten UML, die in der Version 1.0 bei der Object Management Group (OMG) als Standardisierungsvorschlag eingereicht wurde. Die Amigos nannten UML nun »Unified Modeling Language«, was deutlich macht, dass UML keine Methode ist, sondern lediglich eine Modellierungssprache. Folgende Tabelle gibt eine Kurzübersicht über die Veränderungen von UML:

1994 Booch und Rumbaugh vereinigen ihre Ansätze OOAD und OMT.
1995 Unified Method in der Version 0.8. Jacobson bringt OOSE mit ein und UML wird vereinfacht und von einer einheitlichen Methode wird abgesehen.
1996 UML in der Version 0.9. Notation wird verfeinert unter anderem von vielen Unternehmen wie Oracle, Microsoft, Digital, HP.
1997 Versionen 1.0 und 1.1 erscheinen. UML wird bei der OMG eingereicht. Im September wird UML 1.1 zum Standard.
1998 Version 1.2 mit Detailverbesserungen und einigen Korrekturen.
1999 UML in der Version 1.3

Eine aktuelle Version des Standards lässt sich bei http://www.rational.com/uml beziehen.

Diagramme in UML

In UML werden unterschiedliche Diagramme definiert, die die unterschiedlichen Sichten auf die Software beschreiben. Für die einzelnen Phasen im Softwareentwurf sind unterschiedliche Diagrammtypen wichtig. Wir wollen kurz drei Diagramme und ihr Einsatzgebiet besprechen.

Use-Cases

Die Use-Cases-Diagramme entstehen meist während der Anforderungsphase und beschreiben die Geschäftsprozesse, in dem die Interaktion von Personen oder von bereits existierenden Programmen mit dem System dargestellt werden. Die handelnden Personen oder aktiven Systeme werden Aktoren genannt. Ein Use-Case beschreibt dann eine Interaktion mit dem System. Dazu werden die Aktoren als lustige Männchen gemalt (wobei die Geschlechter nicht zu erkennen sind) und die einzelnen Anwendungsfälle (Use-Cases) als Ellipsen.

Klassendiagramme

Für die statische Sicht auf einen Programmentwurf sind Klassendiagramme einer der wichtigsten Diagrammtypen. Sie sind besonders interessant, da Hilfsprogramme aus diesen Diagrammen automatisch Teile des Quellcodes erzeugen können. Die Diagramme stellen zum einen die Elemente der Klasse dar, zum anderen die Beziehungen der Klassen untereinander. Die Diagramme werden öfters in diesem Buch eingesetzt. Klassen werden als Rechteck dargestellt und die Beziehungen zwischen den Klassen durch Linien angedeutet.

Interaktionsdiagramme

Der Begriff umfasst zwei Unterdiagramme zur Darstellung der zeitlichen Abläufe eines Systems, die Sequenzdiagramme und die Kollaborationsdiagramme. Damit wird im Gegensatz zum Klassendiagramm das dynamische Verhalten von Objekten dargestellt.


Galileo Computing

3.2.3 Anlegen eines Exemplars einer Klasse  downtop

Von der Klasse Point werden zur Laufzeit Exemplare erzeugt, die Point-Objekte. Eine Klasse beschreibt also, wie ein Objekt aussehen soll. In einer Mengen- bzw. Element-Beziehung ausgedrückt entsprechen Objekte den Elementen und Klassen den Mengen, in denen die Objekte als Elemente enthalten sind.

Wir verbinden nun einen Variablennamen mit der Klasse und definieren bzw. deklarieren somit eine Variable, die eine Referenz auf ein Point-Objekt (ein Element der Klasse Point) erzeugt.

Beispiel Definieren einer Variablen p vom Typ Point.
Point p;

Vergleichen wir dies mit der bereits bekannten Deklaration einer Variablen für einen ganzzahligen Wert

int i;,

so können wir uns dies gut merken, denn links steht in beiden Fällen der Typ und rechts der Name der Variablen.

Im oberen Beispiel deklarieren wir eine Variable p und teilen dem Compiler mit, dass diese Variable Referenzen auf Objekte vom Typ Point erzeugen soll. Falls es sich bei p um eine Objekt- oder Klassenvariable handelt, wird p anfangs mit der Null-Referenz initialisiert, die auf kein Objekt verweist; als lokale Variable hätte p keinen vordefinierten Wert.

Durch die Deklaration einer Variablen mit dem Namen einer Klasse als Typ wird noch kein Exemplar erzeugt. Dazu müssen wir mit dem new-Operator explizit ein Objekt erzeugen. Hinter dem new-Operator folgt immer der Name der Klasse von der ein Exemplar erzeugt werden soll und ein Paar Klammern. Wir werden später sehen, dass hier ein spezieller Methodenaufruf (Konstruktoraufruf) stattfindet, bei dem wir auch Werte übergeben können.

Beispiel Anlegen eines Objekts und speichern der Referenz in einer Variablen.
p = new Point();

Das tatsächliche Objekt wird erst dynamisch, also zur Laufzeit, mit new erzeugt. Damit stellt das System Speicher für ein Point-Objekt bereit und speichert eine Referenz auf diesen reservierten Speicherblock in der Variablen p ab.

Die Deklaration der Variablen p und die separate Erzeugung eines Exemplars der Klasse Point lassen sich, wie bei der Deklaration primitiver Datentypen, auch kombinieren.

Beispiel Deklaration mit Initialisierung
double pi = 3.1415926535;
Point p = new Point();


Galileo Computing

3.2.4 Zugriff auf Variablen und Methoden mit dem Punkt  downtop

Die in einer Klasse definierten Variablen werden »Objektvariablen« (auch »Exemplar-, Instanz- oder Ausprägungsvariablen«) genannt. Wird ein Objekt erschaffen, dann erhält es seinen eigenen Satz von Objektvariablen . Sie bilden einen Zustand.

Ist das Objekt angelegt, wird auf die Methoden oder Variablen mit einem Punkt zugegriffen. Der Punkt (auch »Selektor« genannt) steht zwischen der Referenz und der Objekt-Eigenschaft.

Beispiel Folgende Zeilen erzeugen ein Point-Objekt, speichern eine Referenz auf dieses Objekt mit der Variablen p und weisen den Objektvariablen x und y Werte zu.
Point p = new Point();
p.x = 12;
p.y = 45;

Der Typ links vom Punkt muss immer eine Referenz sein. Im Prinzip funktioniert auch Folgendes:

new Point().x = 1;

Dies ist allerdings unsinnig, da zwar das Objekt erzeugt und ein Attribut gesetzt wird, anschließend aber der Garbage-Collector das Objekt wieder wegräumt. Für einen Methodenaufruf kann dies schon sinnvoller sein.

Ein Methodenaufruf gestaltet sich genau so einfach wie eine Objekterzeugung. Hinter der Referenz und dem Punkt folgt der Methodenname. Das nachfolgende Beispiel ist lauffähig und bindet gleich noch die Point-Klasse aus dem Paket java.awt ein. Ein Paket ist eine Gruppe zusammengehöriger Klassen.

Listing 3.1   MyPoint.java
import java.awt.Point;

class MyPoint
{
  public static void main( String args[] )
  {
    Point p = new Point();

    p.x = p.y = 12;
    p.setLocation( -3, 2 );

    System.out.println( p.toString() );

//    alternativ
//    System.out.println( p );

  }
}

Die letzte Anweisung ist gültig, da println() bei einem Objekt automatisch die toString()-Methode aufruft.


Galileo Computing

3.2.5 Konstruktoren  downtop

Werden Objekte mit dem new-Operator angelegt, so wird ein Konstruktor aufgerufen, eine Art Methode mit besonderer Signatur. Bei der Erschaffung eines Objekts sollen in der Regel die Objektvariablen initialisiert werden. Diese Initialisierung wird dazu in den Konstruktor gesetzt, um sicherzustellen, dass das neue Objekt einen sinnvollen Anfangszustand aufweist.

Ein Konstruktor ohne Argumente ist der Standard-Konstruktor (auch »Default-Konstruktor«, selten »No-Arg-Konstruktor« genannt).

Beispiel Folgende Zeilen erzeugen schlussendlich zwei Point-Objekte mit denselben Koordinaten. Die Variablen p und q referenzieren jedoch zwei völlig getrennte Objekte; lediglich die Belegung der x- und y-Koordinaten ist bei den beiden Objekten »zufällig« gleich.
Point p = new Point();
p.setLocation( 10, 10 );
Point q = new Point( 10, 10 );

Der erste Konstruktor ist der Standard-Konstruktor, der zweite ein parametrisierter Konstruktor.


Was bei new passiert

Ein Konstruktor-Aufruf wird bei der Erschaffung eines Objekts durch den new-Operator ausgelöst. So erzeugt

Point p = new Point();

ein Exemplar der Klasse Point. Die Laufzeitumgebung von Java reserviert so viel Speicher, dass ein Point-Objekt dort Platz hat. Anschließend ruft die Laufzeitumgebung den Konstruktor auf und gibt eine Referenz auf das Objekt zurück, die im obigen Beispiel der Variablen p zugewiesen wird. Kann das System nicht genügend Speicher bereitstellen, so wird der GC aufgerufen. Kann dieser keinen freien Platz besorgen, generiert die Laufzeitumgebung einen OutOfMemoryError.


Galileo Computing

3.2.6 Die null-Referenz  downtop

In Java gibt das spezielle Literal null, das anzeigt, dass eine Referenz auf kein Objekt verweist. Der Wert ist nur für Referenzen vorgesehen und kann nicht in einen primitiven Typ wie die Ganzzahl 0 umgewandelt werden. Die Null-Referenz ist typenlos, das heißt, sie kann jedem Objekt zugewiesen werden. Daher ist Folgendes gültig:

Point p = null;
String s = null;

Da sich hinter null kein Objekt verbirgt, ist es auch nicht möglich, eine Methode aufzurufen. Der Compiler kennt zwar den Typ jedes Objekts, weiß aber erst zur Laufzeit, was referenziert wird. Wird versucht über die Null-Referenz auf eine Eigenschaft eines Objekts zuzugreifen, dann wird eine NullPointerException ausgelöst.

Listing 3.2   NullPointer.java
import java.awt.Point;

public class NullPointer
{
  public static void main( String args[] )
  {
    Point p = null;
    String s = null;

    p.setLocation(1,2);     // (1)
    s.length();
  }
}

Das Programm bricht schon bei (1) mit folgender Ausgabe ab:

java.lang.NullPointerException
 at NullPointer.main(NullPointer.java:10)
 Exception in thread "main"

Wir wollen an dieser Stelle noch einmal auf die logischen Kurzschlussoperatoren und normalen logischen Operatoren kommen. Letztere werten Operanden nur solange von links nach rechts aus, bis der Wert der Operation feststeht. Es scheint auf den ersten Blick nicht viel auszumachen, ob alle Teilausdrücke ausgewertet werden oder nicht. Es ist aber in einigen Ausdrücken wichtig, wie das folgende Beispiel zeigt.

if ( p != null && p.x >= 10 )

Die Bedingung testet, ob p überhaupt auf ein Objekt verweist und ob die x-Koordinate mindestens 10 ist. Diese Schreibweise ist häufig und der Und-Operator zur Verknüpfung muss ein Kurzschluss-Operator sein, denn in diesem Fall kommt es ausdrücklich darauf an, dass die Koordinate nur dann bestimmt wird, wenn die Variable p überhaupt auf ein Point-Objekt verweist. Andernfalls bekämen wir eine NullPointerException, wenn jeder Teilausdruck ausgewertet würde und p gleich null ist.


Galileo Computing

3.3 Mit Referenzen arbeiten  downtop


Galileo Computing

3.3.1 Zuweisungen bei Referenzen  downtop

Eine Referenz erlaubt den Zugriff auf das referenzierte Objekt. Es kann durchaus mehrere Kopien dieser Referenz geben, die in Variablen mit unterschiedlichen Namen abgelegt sind. So wie ein Personen-Objekt von den Mitarbeitern als »Chefin« angesprochen wird, aber von ihrem Mann als »Schnuckiputzi«.

Wir wollen uns dies an einem Punkt-Objekt näher anschauen, das wir unter einem alternativen Variablennamen ansprechen können:

Point p = new Point();
Point q = p;

Ein Punkt-Objekt wird erzeugt und mit der Variablen p referenziert. Die zweite Zeile speichert nun dieselbe Referenz durch die Variable q. Danach verweisen p und q auf dasselbe Objekt.

Abbildung

Beispiel Das hat die Konsequenz, dass bei einer Änderung des Punkt-Objekts über die in der Variablen p gespeicherte Referenz die Änderung auch bei Zugriff über die Variable q beobachtet werden kann.
Point p = new Point();
Point q = p;

p.x = 10;
System.out.println( q.x );   // 10

q.y = 5;
System.out.println( p.y );   // 5

Was wir über die Variable p ändern, kann über die andere Variable q erfragt werden und umgekehrt.


Vergleich mit der Kopie von primitiven Werten

Primitive Variablen werden immer per Wert kopiert. Betrachten wir folgende Zeilen, dann ist leicht zu sehen, wie sich die Daten verändern. Zunächst deklarieren wir zwei Variablen:

int i = 2;
int j;

Anschließend weisen wir j den Wert von i zu. An dieser Stelle wird der Wert aus i ausgelesen und in j kopiert. Dabei ist es der Zuweisung ziemlich egal, woher der Wert kommt (er könnte beispielsweise auch die Rückgabe einer Funktion sein). Die Ausgabe gibt demnach 2 aus. Ändert sich nun das i und geben wir j aus, so ist die Ausgabe natürlich immer noch 2, da eine Änderung von j keine Änderung von i mit sich zieht. Wir könnten also sagen: »Kopierer haben keine Geschichte«.

j = i;
System.out.println( j );

i = 3;
System.out.println( j );

Galileo Computing

3.3.2 Funktionen mit nicht-primitiven Parametern  downtop

Dass sich das gleiche Objekt unter zwei Namen (über zwei verschiedene Variablen) ansprechen lässt, werden wir auch bei Methoden beobachten können. Eine Funktion, die als Parameter eine Objektreferenz bekommt, bezieht sich genau auf das übergebene Objekt. Das heißt, die Funktion kann dieses Objekt mit den angebotenen Methoden ändern oder auf die Attribute zugreifen.

Listing 3.3   PointFunktion.java
import java.awt.Point;

public class PointFunktion
{
  static void foo( Point p )
  {
    p.x = 10;
  }

   public static void main( String args[] )
  {
    Point q = new Point( 0,0 );
    foo( q );
    System.out.println( q.x );  // 10
  }
}

Wieder ein alternatives Beispiel mit Zeichenketten

Dieses Beispiel ist insofern bedeutend, da es bewusst macht, dass es in Java keine Referenzsemantik für Objekte wie in C++ gibt. Die Referenzen werden wie primitive Werte kopiert. Daher hat auch die folgende Funktion keine Nebenwirkungen:

static void buuh( Point p )
{
  p = new Point();
}

Der lokale Parameter referenziert hier ein anderes Punkt-Objekt. Diese Änderung wird nicht nach außen sichtbar.

Eine Parameterübergabe per Referenz (Call-By-Reference), wie sie in C++ verwendet wird, gibt es in Java nicht. In C++ ließe sich das von Java benutzte Verfahren nur mittelbar nachbilden, indem überall explizit Zeiger auf Objekte (anstelle der Objekte selbst) verwendet würden. Wir werden gleich noch ein zweites Beispiel für die Auswirkungen der in Java allgegenwärtigen Objektreferenzen kennen lernen.


Galileo Computing

3.3.3 Gleichheit von Objekten und die Methode equals()  downtop

Die Zuweisung mit dem Gleichheitszeichen schafft eine zusätzliche Kopie einer Referenz auf ein bereits existierendes Objekt. Der Vergleichsoperator == ist für alle Datentypen so definiert, dass er die vollständige Übereinstimmung zweier Werte testet. Bei primitiven Datentypen ist das einfach einzusehen und bei Referenztypen ist dies im Prinzip genauso. Der Operator == testet bei Referenzen, ob diese übereinstimmen, also auf das gleiche Objekt verweisen. Demnach sagt der Test etwas über die Identität der referenzierten Objekte aus, aber nicht, ob zwei verschiedene Objekte möglicherweise den gleichen Inhalt haben.

Beispiel Drei unterschiedliche Punkte und die Bedeutung von ==
Point p = new Point();
p.x = 12;

Point q = p;

Point r = new Point();
r.x = 12;

if ( p == q )    // ist wahr, da p und q dasselbe Objekt referenzieren

if ( p == r )    // ist falsch, da p und r zwei verschiedene Punkt-Objekte
                 // referenzieren, die zufällig dieselben Koordinaten 
haben
  ...

Da p und q auf dasselbe Objekt verweisen, ergibt der Vergleich true. p und r referenzieren unterschiedliche Objekte, die aber zufälligerweise den gleichen Inhalt haben. Doch woher soll der Compiler wissen, wann zwei Punkt-Objekte inhaltlich gleich sind? Weil ein Punkt sich durch die Attribute x und y auszeichnet? Die Laufzeitumgebung könnte voreilig die Belegung jeder Objektvariablen vergleichen, doch das entspricht nicht immer einem korrekten Vergleich, so wie wir ihn uns wünschen. Ein Punkt-Objekt könnte etwa zusätzlich die Anzahl der Zugriffe zählen, die jedoch für einen Vergleich, der auf der Lage zweier Punkte basiert, nicht berücksichtigt werden darf.

Die Methode equals()

Die allgemein gültige Lösung ist, die Klasse festlegen zu lassen, wann Objekte gleich sind. Da Klassen Eigenschaften definieren, können nur sie Attribute für einen Gleichheitstest heranziehen. Dazu kann jede Klasse eine Methode equals() implementieren, die Exemplare dieser Klasse mit beliebigen anderen Objekten vergleichen kann, und true liefern, wenn die gewünschten Zustände (Objektvariablen) übereinstimmen.

Beispiel Zwei inhaltlich gleiche Punkt-Objekte verglichen mit == und equals().
Point a = new Point( 10, 10 );
Point b = new Point( 10, 10 );

if ( a == b )         // false
  ...

if ( a.equals(b) )    // true
  ...

Nur equals() testet in diesem Fall die inhaltliche Gleichheit.


Aus den unterschiedlichen Bedeutungen müssen wir demnach die Begriffe »Identität« und »Gleichheit« von Objekten sorgfältig unterscheiden. Daher noch einmal eine Zusammenfassung:

  Getestet mit Implementierung
Identität ==  
Gleichheit equals() Aus der Klasse Object geerbte Methode muss geeignet überschrieben werden.

Es gibt immer ein equals()

Glücklicherweise müssen wir als Programmierer nicht lange darüber nachdenken, ob eine Klasse eine equals()-Methode anbieten soll oder nicht. Jede Klasse besitzt sie, da die universelle Oberklasse Object sie vererbt. Eine Unterklasse, wie etwa Point, kann nun eine eigene Implementierung angeben. Sie muss es aber nicht.

Werfen wir einen Blick auf die equals()-Methode aus Point, um eine Vorstellung von der Arbeitsweise zu bekommen:

public boolean equals( Object obj )
{
  if ( obj instanceof Point ) {
    Point pt = (Point)obj;
    return (x == pt.x) && (y == pt.y);   // (*)
  }
  return super.equals(obj);
}

Obwohl bei diesem Beispiel für uns einiges neu ist, erkennen wir den Vergleich in der Zeile (*). Hier vergleicht das Point-Objekt seine eigenen Attribute mit den Attributen des Objekts, das als Parameter an equals() übergeben wurde.

Die Oberklasse Object und ihr equals()

Wenn eine Klasse keine equals()-Methode angibt, dann erbt sie eine Implementierung aus der Klasse Object, die wie folgt aussieht:

public boolean equals( Object obj )
{
  return ( this == obj );
}

Wir erkennen, dass hier die Gleichheit auf die Gleichheit der Referenzen abgebildet wird. Ein inhaltlicher Vergleich findet nicht statt.


Galileo Computing

3.4 Arrays  downtop

Ein Array (auch »Feld« oder »Reihung« genannt) ist ein spezieller Datentyp, der mehrere Werte, die über einen ganzzahligen Index angesprochen werden, zu einer Einheit zusammenfasst. Er ist vergleichbar mit einem Setzkasten, in dem die Plätze durchnummeriert sind. Jeder Platz (etwa für Schlümpfe) nimmt immer Werte des gleichen Typs auf (nur Schlümpfe und keine Pokemons). Normalerweise liegen die Plätze eines Arrays (seine Elemente) im Speicher hintereinander, doch dies ist ein Implementierungsdetail, welches in Java unwichtig ist.

Jedes Array beinhaltet Werte eines bestimmten Datentyps. Dies können sein:

gp  Elementare Datentypen wie int, byte, long, usw.
gp  Referenzen auf Objekte
gp  Referenzen auf andere Arrays, um mehrdimensionale Arrays zu realisieren

Galileo Computing

3.4.1 Deklaration von Arrays  downtop

Eine Array-Variablendeklaration ähnelt einer gewöhnlichen Deklaration, nur dass nach dem Datentyp oder den Variablen die Zeichen »[« und »]« gesetzt werden müssen. Uns ist es freigestellt, welche Schreibweise wir wählen. Hauptsache, es werden überhaupt Klammern gesetzt. Wie bei der gesamten Programmierung, so sollte auch hier konsistent vorgegangen werden – einmal so, einmal so, behindert die schnelle Wahrnehmung von Programmquelltext.

int schachfeld[];
int [] auchSchach;
Point punkte[];

Die Klammern [ ] vor oder hinter die Variable setzen?

So ganz ohne Unterschied ist die Deklaration nicht. Das zeigt sich spätestens, wenn mehr als eine Variable deklariert wird. Die Klammern können einerseits Teil des Typs sein, andererseits Teil der Variablen. Sind sie Teil des Typs, so sind alle deklarierten Variablen ein Feld. Es entspricht demnach

int[]   prims, matrix[], 3dmatrix[][];

der Deklaration

int prims[], matrix[][], 3dmatrix[][][];

Hier ist doppelt Vorsicht geboten, denn der eine oder andere wollte vielleicht nur

int []prims, i;

schreiben, um auszudrücken, dass i eine normale Ganzzahlvariable ist. Stattdessen würde der Compiler jedoch annehmen, dass i ein Feld ist und eine Zuweisung der Art i=2 gnadenlos ablehnen. Es ist aber nicht direkt ersichtlich, wo der Fehler liegt. Ich empfehle daher, die Klammern hinter den Bezeichner zu setzen und bestenfalls in jede Zeile nur eine Deklaration setzen. (Aus diesem Grunde ist es auch ungünstig, unter C(++) das Sternchen für den Zeiger direkt an den Datentyp zu packen. ) Das beugt Fehlern vor. Nach reiner Java-Lehre jedenfalls, gehören die Klammern hinter den Typbezeichner, so hat es Gosling gewollt.


Galileo Computing

3.4.2 Arrays mit Inhalt  downtop

Die bisherigen Deklarationen von Array-Variablen erzeugen noch lange kein Array-Objekt, das die einzelnen Array-Elemente aufnehmen kann. Wenn allerdings die Einträge direkt mit Werten belegt werden sollen, so gibt es in Java eine Abkürzung, die ein Array-Objekt anlegt und zugleich mit Werten belegt.

Beispiel Wertebelegung eines Felds
int primiMäuschen[] = { 1, 2, 3, 5, 7, 7+4, 
};

String substantive[] = {
  "Haus",
  "Maus",
  translator.toGerman("dog"),
    new Point().toString()
};

In diesem Fall wird ein Feld mit passender Größe angelegt und die Elemente in das Feld kopiert, die in der Aufzählung genannt sind. Innerhalb der Aufzählung kann als Letztes ein Komma stehen, wie die Aufzählung bei primiMäuschen demonstriert.

Es ist nicht möglich (wie in C(++)) das Feld mit einer bestimmten Größe zu initialisieren, ohne gleichzeitig die Werte aller Elemente aufzuzählen.

Beispiel Folgende Zeile ist in Java falsch und führt zu einem Compilerfehler:
int einhundertElemente[100];   // Compilerfehler

Wir müssen das über eine explizite Objekt-Erzeugung lösen, wie wir etwas später mit einem new-Operator sehen werden,

Strings und keine Arrays

Ein Array von Char-Zeichen ist nicht mit einem String vergleichbar. Die Klasse String bietet jedoch einen Konstruktor an, sodass aus einem Feld mit Zeichen ein String-Objekt erzeugt werden kann. Alle Zeichen des Felds werden kopiert, sodass anschließend Feld und String keine Verbindung mehr besitzen. Das bedeutet, falls sich das Feld ändert, ändert sich der String nicht automatisch mit. Das kann er auch nicht, da Strings unveränderlich sind.

Beispiel Mit der Methode toCharArray() können wir einen String in ein char-Feld konvertieren.
char umlaut[] = "aeiouäöü".toCharArray();


Galileo Computing

3.4.3 Die Länge eines Arrays mit length  downtop

Die Anzahl der Elemente, folglich die Länge des Arrays, ist für jedes Array-Objekt in der frei zugänglichen Objektvariablen length gespeichert. length ist eine public final int Variable, deren Wert entweder positiv oder Null ist.

Beispiel Ein Feld und Ausgabe der Länge
int primiMäuschen[] = { 1, 2, 3, 5, 7, 7+4, 
};
System.out.println( primiMäuschen.length );

Feldlängen sind final

Das Attribut length eines Felds ist nicht nur öffentlich (public) und vom Typ int, sondern natürlich auch final. Schreibzugriffe sind nicht gestattet. (Was sollten sie bewirken? Eine dynamische Vergrößerung des Feldes?) Ein Schreibzugriff führt zu einem Übersetzungsfehler.


Galileo Computing

3.4.4 Zugriff auf die Elemente  downtop

Die Anzahl der Elemente, die ein Array aufnehmen kann, wird auch als Größe bzw. Länge bezeichnet. In Java beginnt ein Array, ähnlich wie in C(++), bei 0 (und nicht bei einer frei wählbaren Untergrenze wie in PASCAL). Die Größe lässt sich später nicht mehr ändern. Da die Elemente eines Arrays ab 0 nummeriert werden, ist der letzte gültige Index um 1 kleiner als die Länge des Felds. Der Zugriff auf die Elemente eines Felds erfolgt mit Hilfe der eckigen Klammern [], die hinter die Referenz an das Array-Objekt gesetzt werden. Bei einem Array a der Länge n ist der gültige Bereich somit a[0] bis a[n-1].

Beispiel Greife auf das erste und letzte Zeichen aus dem Feld zu
char name[] = "ABC".toCharArray[];
char first = name[0];
char last  = name[name.length-1];

Über den Typ des Index

Innerhalb der eckigen Klammern steht ein Ganzzahl-Ausdruck, der sich zur Laufzeit berechnen lassen muss. long-Werte sowie Gleitkommazahlen sind nicht möglich. Bei Long-Werten wäre der Wertebereich zu groß, denn ein int-Index erlaubt ja schon mehr als 2,4 Milliarden Elemente. Bei Gleitkommazahlen bliebe die Frage nach der Zugriffstechnik. Hier müssten wir den Wert auf ein Intervall runterrechnen.

Beispiel Liegt etwa eine Fließkommazahl f im Intervall von 0 bis 1 und haben die Werte eine Genauigkeit von einem Tausendstel, so ließe sich für ein Array a mit 1000 Elementen für eine Ausgabe schreiben:
f = 0.01;                 // im Intervall von 0 
bis 1
out( a[(int)(f*1000 )] )

Index vom Typ char ist auch ein int

Der Index eines Felds muss von einem Typ sein, der ohne Verlust auf int konvertierbar ist. Dazu gehören byte, short, und char.

Beispiel Das Zeichen c soll in eine Java-Unicode-Zeichenfolge der Form \uxxxx umgewandelt werden.
public static String charToUnicodeEscape( char c 
)
{
  char chars[] = { '\\', 'u',
    hexchars[c >> 12 & 0xf], hexchars[c >> 8 & 0xf],
    hexchars[c >> 4 & 0xf], hexchars[c & 0xf] };
  return new String( chars, 0, 6 );
}
private static final char hexchars[] = {
  '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
  'A', 'B', 'C', 'D', 'E', 'F'
};

Genau genommen haben wir es auch hier mit Indexwerten vom Typ int zu tun, weil mit den char-Werten vorher noch gerechnet wird.

Obwohl es Hexzeichen-Felder schon in anderen Klassen gibt (etwa in Properties), sind diese oft privat. Wenn wir ein eigenes Array in der Klasse definieren, hat das zusätzlich den Vorteil, dass keine eventuell unerwünschten Abhängigkeiten zu anderen Klassen entstehen.


Galileo Computing

3.4.5 Array-Objekte erzeugen  downtop

Ein Array muss mit dem new-Operator unter Angabe einer festen Größe erzeugt werden. Das Anlegen der Variablen alleine erzeugt noch kein Feld mit einer bestimmten Länge. In Java ist das Anlegen des Felds genauso dynamisch wie die Objekterzeugung. Dies drückt auch der new-Operator aus. Die Länge des Feldes wird in eckigen Klammern angegeben. Hier kann ein beliebiger Integer-Wert stehen, auch eine Variable.

Beispiel Die zweite Zeile erzeugt ein Array-Objekt für 100 Elemente.
int arrayOfInts[];
arrayOfInts = new int[100];

Die Felder mit den primitiven Werten sind mit 0, 0.0 oder false initialisiert.


Die Deklaration ist auch zusammen mit der Zuweisung möglich.

Beispiel Deklaration eines 10-elementigen Felds und Initialisierung des Elemente
double x[] = new double[10];    // dann gilt für 
die Indexwerte 0 <= x <= 9
for ( int i = 0; i < 10; i++ )
  x[i] = 2*i;

Beispiel Günstig ist ein Index vom Typ char, der automatisch zum int konvertiert wird, zum Beispiel als Laufvariable, wenn Felder von Zeichenketten generiert werden.
char alphabet[] = new char['z'-'a'+1];
for( char c = 'a'; c <= 'z'; c++ )
  alphabet[c-'a'] = c;

Das ist auch eine elegante und schnelle Möglichkeit, Strings zu erzeugen, denn die Zeichenkette kann mit einem String(char [])-Konstruktor in ein String-Objekt umgewandelt werden:

String result = new String( alphabet );

Galileo Computing

3.4.6 Fehler bei Arrays  downtop

Beim Zugriff auf ein Array-Element können Fehler auftreten. Zunächst einmal kann das Array-Objekt fehlen, sodass die Referenzierung fehlschlägt. Etwa in dem folgenden Fall, bei dem der Compiler den Fehler nicht bemerkt:

int feld[];
feld[1] = 1;

Die Strafe ist eine NullPointerException.

Der zweite und dritte Fehler liegt im Index begründet. Dieser könnte negativ sein oder über der maximalen Länge liegen. Jeder Zugriff auf das Feld wird zur Laufzeit getestet. Auch bei Operationen, die für den Compiler entscheidbar wären, wird dieser Weg eingeschlagen, etwa bei den nachfolgenden Zeilen:

int feld[] = new int[100];
feld[100] = 100;

Hier könnte der Compiler theoretisch Alarm schlagen, was aber kaum ein Compiler bisher macht, denn der Zugriff auf Elemente mit einem ungültigen Index ist syntaktisch und statisch semantisch völlig in Ordnung.

Ist der Index negativ10  oder zu groß, dann hagelt es eine IndexOutOfBoundException. Wird diese nicht abgefangen, bricht das Laufzeitsystem das Programm mit einer Fehlermeldung ab.

Index und das Inkrement

Wir haben beim Inkrement schon ein Phänomen wie i=i++ betrachtet. Ebenso ist auch die Anweisung bei einem Feldzugriff zu behandeln.

a[i] = i++;

Bei der Position a[i] wird i gesichert und anschließend die Zuweisung gemacht. Wenn wir darum eine Schleife konstruieren, erweitern wir dies zu einer Initialisierung:

int is[] = new int[4];

int i = 0;
while ( i < is.length )
  is[i] = i++;

Die Ausgabe ergibt 0, 1, 2 und 3.


Galileo Computing

3.4.7 Arrays mit nicht-primitiven Elementen  downtop

Der Datentyp der Array-Elemente muss nicht zwingend ein primitiver sein. Auch ein Array von Objektreferenzen kann deklariert werden. Dieses Array besteht dann nur aus Referenzen auf die eigentlichen Objekte, die in dem Array abgelegt werden sollen. Die Größe des Arrays im Speicher errechnet sich demnach aus der Länge des Felds multipliziert mit dem Speicherbedarf einer Referenz. Nur das Array-Objekt selbst wird angelegt, nicht aber die Objekte, die das Array aufnehmen soll. Dies lässt sich einfach damit begründen, dass der Compiler auch gar nicht wüsste, welchen Konstruktor er aufrufen sollte.

Beispiel Ein nicht-primitives Feld mit fünf Punkt-Objekten
Point punkte[] = new Point[5];

Hier wird Platz für 5 Referenzen auf Punkte gemacht, aber kein einziges Point-Objekt angelegt. Später würde das Feld etwa mit point[0] = new Point() gefüllt. Standardmäßig werden die Array-Elemente mit der Null-Referenz initialisiert.


Beispiel Fünf Punkte werde mit willkürlichen Werten erzeugt. Die Zufallszahlen werden dabei mit Hilfe der mathematischen Funktion Math.random() erzeugt.
Point punkte[] = new Point[5];

for ( int i=0; i<punkte.length; i++ )
  punkte[i] = new Point( (int)(Math.random()*100), (int)(Math.random()*100) 
);

for (int i = 0; i < punkte.length; i++)
  System.out.println( punkte[i] );

Die Ausgabe erzeugt zum Beispiel Folgendes:

java.awt.Point[x=59,y=77]
java.awt.Point[x=47,y=86]
java.awt.Point[x=18,y=71]
java.awt.Point[x=55,y=97]
java.awt.Point[x=12,y=70]


Galileo Computing

3.4.8 Arrays und Objekte  downtop

Wir haben gesehen, dass Arrays Objekte sind, die geordnete Elemente enthalten. Trotz der weitgehenden Übereinstimmung mit gewöhnlichen Objekten, sollten wir die Unterschiede nicht verschweigen:

gp  Mit dem Operator [] kann auf Array-Elemente über ihren ganzzahligen Index zugegriffen werden. Dieser Operator wird bei anderen Objekten nicht angeboten.
gp  Eine spezielle Form des new-Operators erzeugt ein Exemplar der Array-Klasse.
gp  Eine entsprechende Array-Klasse wird automatisch generiert, wenn ein Array-Typ deklariert wird.

Galileo Computing

3.4.9 Initialisierte Array-Objekte  downtop

Wenn wir in Java ein Array-Objekt erzeugen und gleich mit Werten initialisieren wollen, dann schreiben wir etwa

int primi[] = { 2, 5, 7, 11, 13 };

Wollen wir uns erst nach der Variablendeklaration für die Feldinhalte interessieren und sie gegebenenfalls auch ändern, schlägt ein Versuch wie der Folgende fehl:

int primi[];
primi = { 2, 5, 7, 11, 13 };

Besonders ärgerlich wird dies bei der Parameterübergabe. So scheitert der folgende praktische Funktionsaufruf:

ausgleichsgerade( { 1.23, 4.94, 9.33, 3.91, 6.34 
} );

Das Einzige, was spontan zur Lösung dieses Problems beiträgt, ist die Einführung einer neuen Variablen:

int primi[];
int tmpprimi[] = { 2, 5, 7, 11, 13 };
primi = tmpprimi;

Ein Feld ohne Namen

Glücklicherweise gibt eine Variante des new-Operators, der durch ein Paar eckiger Klammern erweitert wird, in denen die Initialwerte des Arrays folgen. Die Größe des Arrays entspricht genau der Anzahl der Werte.

Für die oberen Beispiele ergibt sich folgenden Schreibweise:

int primi[];
primi = new int[]{ 2, 5, 7, 11, 13 };
ausgleichsgerade( new double[]{ 1.23, 4.94, 9.33, 3.91, 6.34 } );

Da, wie im zweiten Beispiel, ein initialisiertes Feld mit Werten gleich an die Funktion übergeben wird und keine zusätzliche Variable benutzt wird, wird diese Art der Arrays »anonyme Arrays« genannt. An sich gibt es auch sonst anonyme Arrays, wie new int[2000].length zeigt. Doch in diesem Fall wird das Feld nicht mit Werten initialisiert.

Variable Parameterlisten

Mit dem Konzept der initialisierten Array-Objekte lässt sich eine variable Parameterliste nachstellen. Wir schreiben einfach eine Methode, die ein Feld verlangt und übergeben dann beim Aufruf ein entsprechend vorbelegtes Array-Objekt.

Listing 3.4   VariableParameter.java
public class VariableParameter
{
  static double sum( double values[] )
  {
    double sum = 0;

    for ( int i = 0; i < values.length; i++ )
      sum += values[i];

    return sum;
  }

  public static void main( String args[] )
  {
    System.out.println( sum(new double[]{ 1.5, 2.5 }) );
  }
}

Galileo Computing

3.4.10 Mehrdimensionale Arrays  downtop

Java realisiert mehrdimensionale Arrays durch Arrays von Arrays. Sie können etwa für die Darstellung von mathematischen Matrizen oder Rasterbildern Verwendung finden. Ein zweidimensionales Feld mit dem Platz für Reihen von 8 Elementen definiert sich einfach über folgende Zeile:

int A[][] = new int[4][8];

Zwei alternative Deklarationen sind

int[][] A = new int[4][8];   // Der Typ von A ist 
zweidimensionales Array
                               // mit Elementtyp int
int[] A [] = new int[4][8];

Einzelne Elemente werden mit A[i][j] angesprochen.11  Der Zugriff erfolgt mit so vielen Klammerpaaren, wie die Dimension des Arrays angibt. Obwohl mehrdimensionale Arrays im Prinzip Arrays mit Arrays als Elementen sind, lassen sie sich leicht deklarieren.

Beispiel Der Aufbau von zweidimensionalen Feldern ist vergleichbar mit einer Matrix beziehungsweise Tabelle. Dann lässt sich der Eintrag im Feld a[x][y] in folgender Tabelle ablesen.
a[0][0]  a[0][1]  a[0][2]  a[0][3]  a[0][4]  a[0][5] 
 ...
a[1][0]  a[1][1]  a[1][2]  a[1][3]  a[1][4]  a[1][5]
a[2][0]  a[2][1]  a[2][2]  a[2][3]  a[2][4]  a[2][5]
...

Nicht-rechteckige Felder

Da in Java mehrdimensionale Arrays als Arrays von Arrays implementiert sind, müssen diese nicht zwingend rechteckig sein. Jede Zeile im Feld kann eine eigene Größe haben.

Beispiel Ein dreieckiges Array mit Zeilen der Länge 1, 2 und 3.
int m[][] = new int[3][];
for ( int i=0; i<3; i++ )
  m[i] = new int[i+1];

Der Vergleich von

int m[][] = new int [3][4];
int m[][] = new int [3][];

zeigt, dass im ersten Fall die passenden Unterfelder automatisch erzeugt werden. Dies ist im zweiten Fall nicht so. Hier müssen wir selber die Unterfelder initialisieren, bevor wir auf die Elemente zugreifen.

Ebenso wie bei eindimensionalen Feld