Kapitel 4. Die Java-Sprache

Diese Arbeit wurde mithilfe von KI übersetzt. Wir freuen uns über dein Feedback und deine Kommentare: translation-feedback@oreilly.com

Als Menschen lernen wir die Feinheiten der gesprochenen Sprache durch Versuch und Irrtum. Wir lernen, wo wir das Subjekt im Verhältnis zum Verb setzen und wie wir mit Dingen wie Zeitformen und Plural umgehen. In der Schule lernen wir zwar fortgeschrittene Sprachregeln, aber schon die jüngsten Schüler/innen können ihren Lehrer/innen verständliche Fragen stellen. Computersprachen haben ähnliche Eigenschaften: Es gibt "Teile der Sprache", die wie zusammensetzbare Bausteine funktionieren. Es gibt Möglichkeiten, Fakten zu deklarieren und Fragen zu stellen. In diesem Kapitel befassen wir uns mit diesen grundlegenden Programmiereinheiten in Java. Da Versuch und Irrtum ein guter Lehrmeister sind, werden wir uns auch ansehen, wie du mit diesen neuen Einheiten spielen und deine Fähigkeiten üben kannst.

Da die Syntax von Java von C abgeleitet ist, stellen wir einige Vergleiche zu Funktionen dieser Sprache an, aber es sind keine Vorkenntnisse in C erforderlich. Kapitel 5 baut auf diesem Kapitel auf, indem es über die objektorientierte Seite von Java spricht und die Diskussion über die Kernsprache abschließt. In Kapitel 7 geht es um Generics und Records, Funktionen, die die Funktionsweise von Typen in der Sprache Java verbessern und es dir ermöglichen, bestimmte Arten von Klassen flexibler und sicherer zu schreiben.

Danach tauchen wir in die Java-APIs ein und sehen, was wir mit der Sprache machen können. Der Rest des Buches ist gefüllt mit kurzen Beispielen, die nützliche Dinge in einer Vielzahl von Bereichen tun. Wenn du nach diesen einführenden Kapiteln noch Fragen hast, hoffen wir, dass sie dir beantwortet werden, wenn du dir den Code ansiehst. Es gibt natürlich immer mehr zu lernen! Wir werden versuchen, dich auf weitere Ressourcen hinzuweisen, die dir helfen können, deine Java-Reise über die von uns behandelten Themen hinaus fortzusetzen.

Für Leserinnen und Leser, die gerade erst mit dem Programmieren beginnen, wird das Internet wahrscheinlich ein ständiger Begleiter sein. Viele, viele Websites, Wikipedia-Artikel, Blogbeiträge und, nun ja, die Gesamtheit von Stack Overflow können dir dabei helfen, bestimmte Themen zu vertiefen oder kleine Fragen zu beantworten, die auftauchen könnten. In diesem Buch geht es zum Beispiel um die Sprache Java und darum, wie du mit Java und den dazugehörigen Tools nützliche Programme schreiben kannst. Diese Programmiergrundlagen werden natürlich in unseren Diskussionen und Codebeispielen auftauchen, aber vielleicht gefällt dir der eine oder andere Hyperlink, der dir hilft, bestimmte Ideen zu festigen oder die Lücken zu füllen, die wir zwangsläufig lassen müssen.

Wie wir bereits erwähnt haben, werden dir viele Begriffe in diesem Kapitel nicht geläufig sein. Mach dir keine Sorgen, wenn du gelegentlich ein wenig verwirrt bist. Die schiere Breite von Java bedeutet, dass wir von Zeit zu Zeit Erklärungen oder Hintergrunddetails weglassen müssen. Wir hoffen, dass du im Laufe des Kurses die Gelegenheit haben wirst, einige dieser frühen Kapitel noch einmal zu lesen. Neue Informationen können ein bisschen wie ein Puzzlespiel funktionieren. Es ist einfacher, ein neues Teil einzubauen, wenn du bereits einige andere, verwandte Teile zusammengefügt hast. Wenn du einige Zeit mit dem Schreiben von Code verbracht hast und dieses Buch für dich mehr zu einem Nachschlagewerk und weniger zu einem Leitfaden wird, wirst du feststellen, dass die Themen in diesen ersten Kapiteln mehr Sinn ergeben.

Textkodierung

Java ist eine Sprache für das Internet. Da die Nutzerinnen und Nutzer in vielen verschiedenen Sprachen sprechen und schreiben, muss Java auch mit einer großen Anzahl von Sprachen umgehen können. Die Internationalisierung erfolgt über den Unicode-Zeichensatz, einen weltweiten Standard, der die Schriftzeichen der meisten Sprachen unterstützt.1 Die neueste Version von Java basiert auf dem Unicode 14.0 Standard, der mindestens zwei Bytes für die interne Darstellung jedes Symbols verwendet. Wie du dich vielleicht aus "Die Vergangenheit: Java 1.0-Java 20" weißt, bemüht sich Oracle, mit neuen Versionen des Unicode-Standards Schritt zu halten. Deine Java-Version enthält möglicherweise eine neuere Version von Unicode.

Java-Quellcode kann mit Unicode geschrieben und in einer beliebigen Anzahl von Zeichenkodierungen gespeichert werden. Das macht Java zu einer recht freundlichen Sprache für die Einbindung nicht-englischer Inhalte. Programmiererinnen und Programmierer können den umfangreichen Zeichensatz von Unicode nicht nur für die Anzeige von Informationen für den Benutzer, sondern auch für ihre eigenen Klassen-, Methoden- und Variablennamen verwenden.

Der Typ Java char und die Klasse String unterstützen von Haus aus Unicode-Werte. Intern wird der Text entweder in Zeichen- oder Byte-Arrays gespeichert, aber die Java-Sprache und die APIs machen dies für dich transparent, so dass du im Allgemeinen nicht darüber nachdenken musst. Unicode ist auch sehr ASCII-freundlich (ASCII ist die gängigste Zeichenkodierung für Englisch). Die ersten 256 Zeichen sind so definiert, dass sie mit den ersten 256 Zeichen des Zeichensatzes ISO 8859-1 (Latin-1) identisch sind, so dass Unicode mit den gängigsten englischen Zeichensätzen abwärtskompatibel ist. Außerdem behält eine der gängigsten Dateikodierungen für Unicode, UTF-8, die ASCII-Werte in ihrer Ein-Byte-Form bei. Diese Kodierung wird standardmäßig in kompilierten Java-Klassendateien verwendet, sodass die Speicherung von englischem Text kompakt bleibt.

Die meisten Plattformen können nicht alle derzeit definierten Unicode-Zeichen darstellen. Um das zu umgehen, können Java-Programme mit speziellen Unicode-Escape-Sequenzen geschrieben werden. Ein Unicode-Zeichen kann mit dieser Escape-Sequenz dargestellt werden:

\uxxxx

xxxx ist eine Folge von ein bis vier hexadezimalen Ziffern. Die Escape-Sequenz zeigt ein ASCII-kodiertes Unicode-Zeichen an. Dies ist auch die Form, die Java verwendet, um Unicode-Zeichen in einer Umgebung auszugeben (zu drucken), die sie sonst nicht unterstützt. Java verfügt über Klassen zum Lesen und Schreiben von Unicode-Zeichenströmen in bestimmten Kodierungen, einschließlich UTF-8.

Wie bei vielen langlebigen Standards in der Welt der Technik wurde auch bei Unicode ursprünglich so viel zusätzlicher Platz eingeplant, dass keine denkbare Zeichenkodierung jemals mehr als 64K Zeichen benötigen könnte. Seufz. Natürlich haben wir diese Grenze überschritten und einige UTF-32-Kodierungen sind im Umlauf. Vor allem die Emoji-Zeichen, die in vielen Messaging-Apps zu finden sind, werden über den Standardumfang der Unicode-Zeichen hinaus kodiert. (Der kanonische Smiley-Emoji hat zum Beispiel den Unicode-Wert 1F600.) Java unterstützt Multibyte-UTF-16-Escape-Sequenzen für solche Zeichen. Nicht jede Plattform, die Java unterstützt, unterstützt die Ausgabe von Emoji, aber du kannst die jshell starten, um herauszufinden, ob deine Umgebung Emoji-Zeichen anzeigen kann (siehe Abbildung 4-1).

ljv6 0401
Abbildung 4-1. Drucken von Emojis in der macOS Terminal App

Sei aber vorsichtig bei der Verwendung solcher Zeichen. Wir mussten einen Screenshot verwenden, um sicherzustellen, dass du die kleinen Süßen in jshell auf einem Mac sehen kannst. Du kannst jshell verwenden, um dein eigenes System zu testen. Du kannst eine minimale grafische Anwendung ähnlich unserer HelloJava Klasse aus "HelloJava" aufsetzen. Erstelle ein JFrame, füge ein JLabel hinzu und mache den Rahmen sichtbar:

jshell> import javax.swing.*

jshell> JFrame f = new JFrame("Emoji Test")
f ==> javax.swing.JFrame[frame0 ...=true]

jshell> f.add(new JLabel("Hi \uD83D\uDE00"))
$12 ==> javax.swing.JLabel[ ...=CENTER]

jshell> f.setSize(300,200)

jshell> f.setVisible(true)

Hoffentlich siehst du den Smiley, aber das hängt auch von deinem System ab. Abbildung 4-2 zeigt die Ergebnisse, die wir bei genau diesem Test unter macOS und Linux erhalten haben.

ljv6 0402
Abbildung 4-2. Testen der Emoji-Darstellung auf verschiedenen Systemen

Es ist nicht so, dass du Emoji in deinen Anwendungen nicht verwenden oder unterstützen kannst, du musst dir nur der Unterschiede in den Ausgabefunktionen bewusst sein. Sorge dafür, dass deine Nutzerinnen und Nutzer ein gutes Erlebnis haben, egal wo sie deinen Code ausführen.

Warnung

Wenn du die grafischen Komponenten aus dem Swing-Paket importierst, achte darauf, dass du das richtige Präfix javax und nicht das Standardpräfix java verwendest. Mehr über Swing erfährst du in Kapitel 12.

Kommentare

Da wir nun wissen, wie der Text unserer Programme gespeichert wird, können wir uns darauf konzentrieren, was wir speichern wollen! Programmierer fügen oft Kommentare in ihren Code ein, um komplexe Teile der Logik zu erklären oder um anderen Programmierern eine Anleitung zum Lesen des Codes zu geben. (Oft ist der "andere Programmierer" einige Monate oder Jahre später du selbst.) Der Text in einem Kommentar wird vom Compiler komplett ignoriert. Kommentare haben keinen Einfluss auf die Leistung oder Funktionalität deiner Anwendung. Aus diesem Grund sind wir große Fans davon, gute Kommentare zu schreiben. Java unterstützt sowohl Blockkommentare im C-Stil, die sich über mehrere Zeilen erstrecken können und durch /* und */ abgegrenzt werden, als auch Zeilenkommentare im C++-Stil, die durch // gekennzeichnet sind:

    /*  This is a
            multiline
                comment.    */

    // This is a single-line comment
    // and so // is this

Blockkommentare haben eine Anfangs- und Endsequenz und können große Textbereiche abdecken. Sie können jedoch nicht "verschachtelt" werden, d. h. du kannst keinen Blockkommentar innerhalb eines anderen Blockkommentars einfügen, ohne dass der Compiler etwas dagegen hat. Einzeilige Kommentare haben nur eine Startsequenz und werden durch das Ende einer Zeile abgegrenzt; zusätzliche // Indikatoren innerhalb einer einzelnen Zeile haben keine Wirkung. Zeilenkommentare sind nützlich für kurze Kommentare innerhalb von Methoden; sie stehen nicht im Konflikt mit Blockkommentaren. Größere Codeabschnitte, in denen einzeilige Kommentare vorkommen, kannst du trotzdem mit einem Blockkommentar umschließen. Dies wird oft als Auskommentieren eines Codeabschnitts bezeichnet - ein gängiger Trick zum Debuggen großer Anwendungen. Da der Compiler alle Kommentare ignoriert, kannst du Kommentare in Codezeilen oder um Codeblöcke herum einfügen, um zu sehen, wie sich ein Programm verhält, wenn der Code entfernt wird.2

Javadoc-Kommentare

Ein spezieller Blockkommentar, der mit /** beginnt, zeigt einen Doc-Kommentar an. Ein doc-Kommentar ist dafür gedacht, von automatischen Dokumentationsgeneratoren extrahiert zu werden, wie z. B. dem JDK-eigenen javadoc-Programm oder den kontextabhängigen Tooltips in vielen IDEs. Ein doc-Kommentar wird wie ein normaler Blockkommentar durch das nächste */ abgeschlossen. Innerhalb des doc-Kommentars werden Zeilen, die mit @ beginnen, als spezielle Anweisungen für den Dokumentationsgenerator interpretiert, die ihm Informationen über den Quellcode geben. Konventionell beginnt jede Zeile eines Dok-Kommentars mit einem *, wie im folgenden Beispiel gezeigt, aber das ist optional. Führende Abstände und die * in jeder Zeile werden ignoriert:

/**
 * I think this class is possibly the most amazing thing you will
 * ever see. Let me tell you about my own personal vision and
 * motivation in creating it.
 * <p>
 * It all began when I was a small child, growing up on the
 * streets of Idaho. Potatoes were the rage, and life was good...
 *
 * @see PotatoPeeler
 * @see PotatoMasher
 * @author John 'Spuds' Smith
 * @version 1.00, 19 Nov 2022
 */
class Potato { ... }

Das Kommandozeilenwerkzeug javadoc erstellt eine HTML-Dokumentation für Klassen, indem es den Quellcode liest und die eingebetteten Kommentare und @ Tags herauszieht. In diesem Beispiel erzeugen die Tags Autoren- und Versionsinformationen in der Klassendokumentation. Die @see Tags erzeugen Hypertext-Links zu der entsprechenden Klassendokumentation.

Der Compiler sieht sich auch die Doc-Kommentare an; insbesondere interessiert er sich für das Tag @deprecated, das bedeutet, dass die Methode für veraltet erklärt wurde und in neuen Programmen vermieden werden sollte. Die kompilierte Klasse enthält Informationen über veraltete Methoden, so dass der Compiler dich warnen kann, wenn du ein veraltetes Feature in deinem Code verwendest (auch wenn der Quelltext nicht verfügbar ist).

Die Kommentare von Doc können über Klassen-, Methoden- und Variablendefinitionen erscheinen, aber einige Tags gelten nicht für alle diese Definitionen. Das Tag @exception kann zum Beispiel nur auf Methoden angewendet werden. Tabelle 4-1 gibt einen Überblick über die in Doc-Kommentaren verwendeten Tags.

Tabelle 4-1. Doc-Kommentar-Tags
Tag Beschreibung Gilt für

@see

Zugehöriger Klassenname

Klasse, Methode oder Variable

@code

Inhalt des Quellcodes

Klasse, Methode oder Variable

@link

Assoziierte URL

Klasse, Methode oder Variable

@author

Name des Autors

Klasse

@version

Version string

Klasse

@param

Parametername und Beschreibung

Methode

@return

Beschreibung des Rückgabewerts

Methode

@exception

Name und Beschreibung der Ausnahme

Methode

@deprecated

Erklärt einen Artikel für veraltet

Klasse, Methode oder Variable

@since

Anmerkungen API-Version, als der Artikel hinzugefügt wurde

Variabel

Javadoc-Tags in Doc-Kommentaren stellen Metadaten über den Quellcode dar, d.h. sie fügen beschreibende Informationen über die Struktur oder den Inhalt des Codes hinzu, die streng genommen nicht Teil der Anwendung sind. Einige zusätzliche Tools erweitern das Konzept der Javadoc-Tags um andere Arten von Metadaten über Java-Programme, die mit dem kompilierten Code mitgeführt werden und von der Anwendung leichter verwendet werden können, um ihr Kompilierungs- oder Laufzeitverhalten zu beeinflussen. Die Java Annotations-Funktion bietet eine formalere und erweiterbare Möglichkeit, Java-Klassen, -Methoden und -Variablen Metadaten hinzuzufügen. Diese Metadaten sind auch während der Laufzeit verfügbar.

Anmerkungen

Das Präfix @ hat in Java eine weitere Funktion, die ähnlich wie Tags aussehen kann. Java unterstützt das Konzept der Anmerkungen als Mittel, um bestimmte Inhalte für eine besondere Behandlung zu markieren. Du wendest Annotationen auf Code außerhalb von Kommentaren an. Die Annotation kann Informationen enthalten, die für den Compiler oder deine IDE nützlich sind. Die Annotation @SuppressWarnings bewirkt zum Beispiel, dass der Compiler (und oft auch deine IDE) Warnungen über mögliche Probleme wie unerreichbaren Code ausblendet. Wenn du dich in "Advanced Class Design" mit der Erstellung interessanterer Klassen beschäftigst , kann es sein, dass deine IDE deinem Code die Annotation @Overrides hinzufügt. Diese Annotationen weisen den Compiler an, einige zusätzliche Prüfungen durchzuführen. Diese Prüfungen sollen dir helfen, gültigen Code zu schreiben und Fehler zu erkennen, bevor du (oder deine Benutzer) dein Programm ausführen.

Du kannst sogar eigene Anmerkungen erstellen, um mit anderen Tools oder Frameworks zu arbeiten. Eine ausführlichere Diskussion über Anmerkungen würde den Rahmen dieses Buches sprengen, aber wir wollten, dass du sie kennst, denn Tags wie @Overrides tauchen sowohl in unserem Code als auch in Beispielen oder Blogbeiträgen auf, die du online findest.

Variablen und Konstanten

Das Hinzufügen von Kommentaren zu deinem Code ist zwar wichtig, um lesbare und wartbare Dateien zu erstellen, aber irgendwann musst du anfangen, kompilierbare Inhalte zu schreiben. Programmieren ist die Kunst, diese Inhalte zu manipulieren. In fast jeder Sprache werden solche Informationen in Variablen und Konstanten gespeichert, damit der Programmierer sie leichter verwenden kann. Java hat beides. Variablen speichern Informationen, die du im Laufe der Zeit ändern und wiederverwenden willst (oder Informationen, die du im Voraus nicht kennst, wie z. B. die E-Mail-Adresse eines Benutzers). Konstanten speichern Informationen, die, nun ja, konstant sind. Beispiele für beide Elemente haben wir schon in unseren kleinen Startprogrammen gesehen. Erinnere dich an unser einfaches grafisches Label aus "HelloJava":

import javax.swing.*;

public class HelloJava {
  public static void main(String[] args) {
    JFrame frame = new JFrame("Hello, Java!");
    JLabel label = new JLabel("Hello, Java!", JLabel.CENTER);
    frame.add(label);
    frame.setSize(300, 300);
    frame.setVisible(true);
  }
}

In diesem Schnipsel ist frame eine Variable. Wir laden sie in Zeile 5 mit einer neuen Instanz der Klasse JFrame hoch. Dann können wir diese Instanz in Zeile 7 wiederverwenden, um unser Label hinzuzufügen. Wir verwenden die Variable erneut, um die Größe unseres Rahmens in Zeile 8 festzulegen und um ihn in Zeile 9 sichtbar zu machen. All diese Wiederverwendung ist genau das, was Variablen auszeichnet.

Zeile 6 enthält eine Konstante: JLabel.CENTER. Konstanten enthalten einen bestimmten Wert, der sich im Laufe deines Programms nie ändert. Informationen, die sich nicht ändern, mögen seltsam erscheinen - warum nicht einfach jedes Mal die Informationen selbst verwenden? Konstanten können einfacher zu benutzen sein als ihre Daten; Math.PI kann man sich wahrscheinlich leichter merken als den Wert 3.141592653589793, für den sie steht. Und da du den Namen der Konstanten in deinem eigenen Code auswählen kannst, besteht ein weiterer Vorteil darin, dass du die Informationen auf sinnvolle Weise beschreiben kannst. JLabel.CENTER mag zwar immer noch etwas undurchsichtig erscheinen, aberdas Wort CENTER gibt dir zumindest einen Hinweis darauf, was passiert.

Die Verwendung von benannten Konstanten macht auch spätere Änderungen einfacher. Wenn du z. B. die maximale Anzahl der verwendeten Ressourcen kodierst, ist es viel einfacher, diese Grenze zu ändern, wenn du nur den Anfangswert der Konstante ändern musst. Wenn du eine wörtliche Zahl wie 5 verwendest, müsstest du jedes Mal, wenn dein Code diese Höchstzahl überprüfen muss, alle Java-Dateien durchforsten, um jedes Vorkommen einer 5 aufzuspüren und sie ebenfalls zu ändern - sofern sich diese 5 tatsächlich auf das Ressourcenlimit bezieht. Diese Art des manuellen Suchens und Ersetzens ist fehleranfällig und nicht nur mühsam.

Weitere Details zu den Typen und Anfangswerten von Variablen und Konstanten werden wir im nächsten Abschnitt sehen. Wie immer kannst du die jshell benutzen, um einige dieser Details selbst zu entdecken! Aufgrund von Einschränkungen des Interpreters kannst du in der jshell keine eigenen Konstanten auf oberster Ebene deklarieren. Du kannst jedoch Konstanten verwenden, die für Klassen wie JLabel.CENTER definiert wurden, oder sie in deinen eigenen Klassen definieren.

Versuche, die folgenden Anweisungen in jshell einzugeben, um den Flächeninhalt eines Kreises mit Math.PI zu berechnen und in einer Variablen zu speichern. Diese Übung zeigt auch, dass die Neuzuweisung von Konstanten nicht funktioniert. (Und wieder müssen wir ein paar neue Konzepte einführen, wie die Zuweisung - das Einfügeneines Wertes in eine Variable - und den Multiplikationsoperator *. Wenn dir diese Befehle immer noch seltsam vorkommen, lies weiter. Wir werden im weiteren Verlauf dieses Kapitels auf alle neuen Elemente genauer eingehen).

jshell> double radius = 42.0;
radius ==> 42.0

jshell> Math.PI
$2 ==> 3.141592653589793

jshell> Math.PI = 3;
|  Error:
|  cannot assign a value to final variable PI
|  Math.PI = 3;
|  ^-----^

jshell> double area = Math.PI * radius * radius;
area ==> 5541.769440932396

jshell> radius = 6;
radius ==> 6.0

jshell> area = Math.PI * radius * radius;
area ==> 113.09733552923255

jshell> area
area ==> 113.09733552923255

Beachte den Compilerfehler, wenn wir versuchen, Math.PI auf 3 zu setzen. Du könntest radius und sogar area ändern, nachdem du sie deklariert und initialisiert hast. Aber Variablen können immer nur einen Wert speichern, also ist die letzte Berechnung das Einzige, was in der Variable area bleibt.

Typen

Das Typsystem einer Programmiersprache beschreibt, wie die Datenelemente (die Variablen und Konstanten, die wir gerade angesprochen haben) mit der Speicherung im Speicher verbunden sind und wie sie zueinander in Beziehung stehen. In einer statisch typisierten Sprache wie C oder C++ ist der Typ eines Datenelements ein einfaches, unveränderliches Attribut, das oft direkt einem zugrunde liegenden Hardware-Phänomen entspricht, z. B. einem Register oder einem Zeigerwert. In einer dynamisch typisierten Sprache wie Smalltalk oder Lisp können Variablen beliebige Elemente zugewiesen werden und ihren Typ während ihrer gesamten Lebensdauer effektiv ändern. In diesen Sprachen wird ein erheblicher Aufwand betrieben, um zu überprüfen, was zur Laufzeit passiert. Skriptsprachen wie Perl erreichen ihre Benutzerfreundlichkeit durch drastisch vereinfachte Typensysteme, in denen nur bestimmte Datenelemente in Variablen gespeichert werden können und Werte in einer gemeinsamen Darstellung, z. B. als Strings, vereinheitlicht werden.

Java kombiniert viele der besten Eigenschaften von statisch und dynamisch typisierten Sprachen. Wie in einer statisch typisierten Sprache hat jede Variable und jedes Programmierelement in Java einen Typ, der zur Kompilierungszeit bekannt ist, so dass das Laufzeitsystem normalerweise nicht die Gültigkeit von Zuweisungen zwischen Typen überprüfen muss, während der Code ausgeführt wird. Im Gegensatz zu den traditionellen Sprachen C oder C++ verwaltet Java auch Laufzeitinformationen über Objekte und nutzt diese, um ein wirklich dynamisches Verhalten zu ermöglichen. Java-Code kann zur Laufzeit neue Typen laden und sie vollständig objektorientiert verwenden, was Casting (Konvertierung zwischen Typen) und vollständige Polymorphie (Kombination von Merkmalen mehrerer Typen) ermöglicht. Java-Code kann auch seine eigenen Typen zur Laufzeit "reflektieren" oder untersuchen, was fortschrittliche Arten des Anwendungsverhaltens ermöglicht, z. B. Interpreter, die dynamisch mit kompilierten Programmen interagieren können.

Java-Datentypen lassen sich in zwei Kategorien einteilen. Primitive Typen stehen für einfache Werte, die in der Sprache eine eingebaute Funktionalität haben; sie repräsentieren Zahlen, boolesche (wahr oder falsch) Werte und Zeichen. Referenztypen (oder Klassentypen) umfassen Objekte und Arrays; sie werden Referenztypen genannt, weil sie auf einen großen Datentyp "verweisen", der "per Referenz" übergeben wird, wie wir gleich noch erklären werden. Generische Typen sind Referenztypen, die einen bestehenden Typ verfeinern und gleichzeitig die Kompilierzeit-Typensicherheit gewährleisten. In Java gibt es zum Beispiel die Klasse List, die eine Reihe von Elementen speichern kann. Mit Hilfe von Generika kannst du ein List<String> erstellen, das ein List ist, das nur Strings enthalten kann. Oder wir könnten eine Liste von JLabel Objekten mit List<JLabel> erstellen. In Kapitel 7 werden wir noch viel mehr über Generics erfahren.

Primitive Typen

Zahlen, Zeichen und boolesche Werte sind grundlegende Elemente in Java. Im Gegensatz zu einigen anderen (vielleicht reineren) objektorientierten Sprachen sind sie keine Objekte. Für Situationen, in denen es wünschenswert ist, einen primitiven Wert wie ein Objekt zu behandeln, bietet Java "Wrapper"-Klassen. (Dazu später mehr.) Der große Vorteil der Behandlung von primitiven Werten als etwas Besonderes ist, dass der Java-Compiler und die Java-Laufzeitumgebung ihre Implementierung leichter optimieren können. Primitive Werte und Berechnungen können nach wie vor auf die Hardware abgebildet werden, so wie es in Sprachen auf niedrigerer Ebene schon immer der Fall war.

Ein wichtiges Merkmal der Portabilität von Java ist, dass die primitiven Typen genau definiert sind. Du musst dir zum Beispiel keine Gedanken über die Größe von int auf einer bestimmten Plattform machen; es ist immer eine 32-Bit-Zahl mit Vorzeichen. Die "Größe" eines numerischen Typs legt fest, wie groß (oder wie genau) der Wert ist, den du speichern kannst. Der Typ byte ist zum Beispiel ein 8-Bit-Wert mit Vorzeichen zum Speichern kleiner Zahlen von -128 bis 127.3 Der bereits erwähnte Typ int kann die meisten numerischen Anforderungen erfüllen und speichert Werte zwischen (ungefähr) +/- zwei Milliarden. Tabelle 4-2 fasst die primitiven Typen von Java und ihre Fähigkeiten zusammen.

Tabelle 4-2. Java primitive Datentypen
Typ Definition Ungefähre Reichweite oder Genauigkeit

boolean

Logischer Wert

true oder false

char

16-Bit, Unicode-Zeichen

64K Zeichen

byte

8-Bit, vorzeichenbehaftete Ganzzahl

-128 bis 127

short

16-Bit, vorzeichenbehaftete Ganzzahl

-32.768 bis 32.767

int

32-Bit, vorzeichenbehaftete Ganzzahl

-2,1e9 bis 2,1e9

long

64-Bit, vorzeichenbehaftete Ganzzahl

-9,2e18 bis 9,2e18

float

32-Bit, IEEE 754, Fließkommazahl

6-7 signifikante Nachkommastellen

double

64-Bit, IEEE 754

15 signifikante Nachkommastellen

Hinweis

Denjenigen unter euch, die sich mit C auskennen, fällt vielleicht auf, dass die primitiven Typen wie eine Idealisierung der C-Skalar-Typen auf einem 32-Bit-Rechner aussehen, und das stimmt auch. So sollen sie auch aussehen. Die Java-Entwickler haben ein paar Änderungen vorgenommen, z. B. die Unterstützung von 16-Bit-Zeichen für Unicode und die Abschaffung von Ad-hoc-Zeigern. Aber im Großen und Ganzen stammen die Syntax und die Semantik der primitiven Java-Typen von C ab.

Aber warum gibt es überhaupt Größen? Auch hier geht es wieder um Effizienz und Optimierung. Die Anzahl der Tore bei einem Fußballspiel liegt selten im einstelligen Bereich - sie würde in eine byte Variable passen. Die Anzahl der Fans, die das Spiel sehen, würde jedoch eine größere Variable erfordern. Die Gesamtsumme des Geldes, das die Fans bei allen Fußballspielen in allen WM-Ländern ausgeben, müsste noch größer sein. Indem du die richtige Größe wählst, gibst du dem Compiler die beste Chance, deinen Code zu optimieren, so dass deine Anwendung schneller läuft, weniger Systemressourcen verbraucht oder beides.

Einige wissenschaftliche oder kryptografische Anwendungen erfordern es, dass du sehr große (oder sehr kleine) Zahlen speicherst und bearbeitest, wobei Genauigkeit wichtiger ist als Leistung. Wenn du größere Zahlen brauchst, als die primitiven Typen bieten, kannst du dir die Klassen BigInteger und BigDecimal im Paket java.math ansehen. Diese Klassen bieten eine nahezu unendliche Größe oder Genauigkeit. (Wenn du diese großen Zahlen in Aktion sehen willst, verwenden wir BigInteger, um die Fakultät zu berechnen (siehe "Erstellen eines benutzerdefinierten Reduzierers") .

Fließkommagenauigkeit

Fließkomma Operationen in Java folgen der internationalen Spezifikation IEEE 754, was bedeutet, dass das Ergebnis von Fließkommaberechnungen normalerweise auf verschiedenen Java-Plattformen gleich ist. Java erlaubt jedoch eine erweiterte Genauigkeit auf Plattformen, die dies unterstützen. Dies kann zu extrem kleinen und undurchsichtigen Unterschieden in den Ergebnissen von hochpräzisen Operationen führen. Die meisten Anwendungen werden dies nicht bemerken, aber wenn du sicherstellen willst, dass deine Anwendung auf verschiedenen Plattformen genau die gleichen Ergebnisse liefert, kannst du das spezielle Schlüsselwort strictfp als Klassenmodifikator für die Klasse verwenden, die die Fließkommamanipulation enthält (wir behandeln Klassen in Kapitel 5). Der Compiler verbietet dann diese plattformspezifischen Optimierungen.

Deklaration und Initialisierung von Variablen

deklarierst du Variablen innerhalb von Methoden und Klassen mit einem Typnamen, gefolgt von einem oder mehreren durch Komma getrennten Variablennamen. Zum Beispiel:

    int foo;
    double d1, d2;
    boolean isFun;

Du kannst eine Variable optional mit einem Ausdruck des entsprechenden Typs initialisieren, wenn du sie deklarierst:

    int foo = 42;
    double d1 = 3.14, d2 = 2 * 3.14;
    boolean isFun = true;

Variablen, die als Mitglieder einer Klasse deklariert sind, werden auf Standardwerte gesetzt, wenn sie nicht initialisiert sind (siehe Kapitel 5). In diesem Fall werden numerische Typen standardmäßig auf den entsprechenden Wert Null gesetzt, Zeichen auf das Null-Zeichen (\0) und boolesche Variablen auf den Wert false. (Referenztypen erhalten ebenfalls einen Standardwert, null, aber dazu gleich mehr in "Referenztypen") .

Lokale Variablen, die innerhalb einer Methode deklariert werden und nur für die Dauer eines Methodenaufrufs existieren, müssen dagegen explizit initialisiert werden, bevor sie verwendet werden können. Wie wir sehen werden, setzt der Compiler diese Regel durch, so dass keine Gefahr besteht, sie zu vergessen.

Integer-Literale

Ganzzahl Literale können in binär (Basis 2), oktal (Basis 8), dezimal (Basis 10) oder hexadezimal (Basis 16) angegeben werden. Binäre, oktale und hexadezimale Basen werden vor allem dann verwendet, wenn es um Daten auf niedriger Ebene in Dateien oder im Netzwerk geht. Sie stellen nützliche Gruppierungen einzelner Bits dar: 1, 3 bzw. 4 Bits. Dezimalwerte haben keine solche Zuordnung, aber sie sind für die meisten numerischen Informationen viel menschenfreundlicher. Eine dezimale Ganzzahl wird durch eine Folge von Ziffern angegeben, die mit einem der Zeichen 1-9 beginnt:

    int i = 1230;

Eine binäre Zahl wird durch die führenden Zeichen 0b oder 0B (Null "b") bezeichnet, gefolgt von einer Kombination aus Nullen und Einsen:

    int i = 0b01001011;            // i = 75 decimal

Oktalzahlen unterscheiden sich von Dezimalzahlen durch eine einfache führende Null:

    int i = 01230;             // i = 664 decimal

Eine Hexadezimalzahl wird durch die führenden Zeichen 0x oder 0X (Null "x") bezeichnet, gefolgt von einer Kombination aus Ziffern und den Zeichen a-f oder A-F, die die Dezimalwerte 10-15 darstellen:

    int i = 0xFFFF;            // i = 65535 decimal

Ganzzahlige Literale sind vom Typ int, es sei denn, sie haben ein L angehängt, das angibt, dass sie als long Wert erzeugt werden sollen:

    long l = 13L;
    long l = 13;           // equivalent: 13 is converted from type int
    long l = 40123456789L;
    long l = 40123456789;  // error: too big for an int without conversion

(Der Kleinbuchstabe l funktioniert auch, sollte aber vermieden werden, weil er oft wie die Zahl 1 aussieht).

Wenn ein numerischer Typ in einer Zuweisung oder einem Ausdruck verwendet wird, der einen "größeren" Typ mit einem größeren Bereich enthält, kann er in den größeren Typ verschoben werden. In der zweiten Zeile des vorherigen Beispiels hat die Zahl 13 den Standardtyp int, wird aber für die Zuweisung an die Variable long in den Typ long überführt.

Bestimmte andere numerische und vergleichende Operationen führen ebenfalls zu dieser Art von arithmetischem Aufstieg, ebenso wie mathematische Ausdrücke, die mehr als einen Typ beinhalten. Wenn du zum Beispiel einen Wert von byte mit einem Wert von int multiplizierst, macht der Compiler aus byte zuerst einen Wert von int:

    byte b = 42;
    int i = 43;
    int result = b * i;  // b is promoted to int before multiplication

Du kannst niemals den umgekehrten Weg gehen und einen numerischen Wert einem Typ mit einem kleineren Bereich zuweisen, ohne einen expliziten Cast durchzuführen, eine spezielle Syntax, mit der du dem Compiler genau sagen kannst, welchen Typ du brauchst:

    int i = 13;
    byte b = i;        // Compile-time error, explicit cast needed
    byte b = (byte) i; // OK

Der Cast in der dritten Zeile ist die (byte) Phrase vor unserer Variablen i. Konvertierungen von Fließkomma- in Ganzzahltypen erfordern immer einen expliziten Cast, da es zu einem Verlust an Genauigkeit kommen kann.

Zu guter Letzt kannst du deinen numerischen Literalen ein wenig Formatierung hinzufügen, indem du das Zeichen "_" (Unterstrich) zwischen den Ziffern verwendest. Wenn du besonders große Ziffernfolgen hast, kannst du sie wie in den folgenden Beispielen aufteilen:

    int RICHARD_NIXONS_SSN = 567_68_0515;
    int for_no_reason = 1___2___3;
    int JAVA_ID = 0xCAFE_BABE;
    long grandTotal = 40_123_456_789L;

Unterstriche dürfen nur zwischen den Ziffern erscheinen, nicht am Anfang oder Ende einer Zahl oder neben dem L long integer signifier. Probiere ein paar große Zahlen in jshell aus. Beachte, dass du eine Fehlermeldung bekommst, wenn du versuchst, einen long Wert ohne den L Signifier zu speichern. Du kannst sehen, dass die Formatierung nur zu deiner Bequemlichkeit dient. Sie wird nicht gespeichert; nur der tatsächliche Wert wird in deiner Variablen oder Konstante gespeichert:

jshell> long m = 41234567890;
|  Error:
|  integer number too large
|  long m = 41234567890;
|           ^

jshell> long m = 40123456789L;
m ==> 40123456789

jshell> long grandTotal = 40_123_456_789L;
grandTotal ==> 40123456789

Probiere ein paar andere Beispiele aus. Es kann nützlich sein, ein Gefühl dafür zu bekommen, was du lesenswert findest. Es kann dir auch helfen, herauszufinden, welche Arten von Aktionen und Castings es gibt oder welche du brauchst. Es geht nichts über ein unmittelbares Feedback, das dir diese Feinheiten verdeutlicht!

Fließkomma-Literale

Fließkommawerte können in dezimaler oder wissenschaftlicher Notation angegeben werden. Fließkomma-Literale sind vom Typ double, es sei denn, ihnen wird ein f oder F angehängt, was bedeutet, dass es sich um einen Wert mit geringerer Genauigkeit handelt float. Genau wie bei ganzzahligen Literalen kannst du den Unterstrich verwenden, um Fließkommazahlen zu formatieren - aber auch hier nur zwischen den Ziffern. Du kannst sie nicht am Anfang, am Ende, neben dem Dezimalpunkt oder neben dem F Zeichen der Zahl platzieren:

    double d = 8.31;
    double e = 3.00e+8;
    float f = 8.31F;
    float g = 3.00e+8F;
    float pi = 3.1415_9265F;

Zeichenliterale

Ein literaler Zeichenwert kann entweder als Zeichen in einfachen Anführungszeichen oder als escapte ASCII- oder Unicode-Sequenz, ebenfalls in einfachen Anführungszeichen, angegeben werden:

    char a = 'a';
    char newline = '\n';
    char smiley = '\u263a';

Meistens hast du es mit Zeichen zu tun, die in einem String zusammengefasst sind, aber es gibt auch Situationen, in denen einzelne Zeichen nützlich sind. Wenn du zum Beispiel Tastatureingaben in deiner Anwendung verarbeitest, kann es sein, dass du einzelne Tastendrücke char auf einmal verarbeiten musst.

Referenztypen

In einer objektorientierten Sprache wie Java erstellst du neue, komplexe Datentypen aus einfachen Primitiven, indem du eine Klasse erstellst. Jede Klasse dient dann als ein neuer Typ in der Sprache. Wenn wir zum Beispiel in Java eine neue Klasse mit dem Namen Car erstellen, erstellen wir damit implizit auch einen neuen Typ namens Car. Der Typ eines Elements bestimmt, wie es verwendet wird und wo es zugewiesen werden kann. Wie bei den Primitiven kann ein Element des Typs Car im Allgemeinen einer Variablen des Typs Car zugewiesen oder als Argument an eine Methode übergeben werden, die einen Car Wert annimmt.

Ein Typ ist nicht nur ein einfaches Attribut. Klassen können Beziehungen zu anderen Klassen haben, und das gilt auch für die Typen, die sie repräsentieren. Alle Klassen in Java existieren in einer Eltern-Kind-Hierarchie, in der eine Kindklasse oder Unterklasse eine spezielle Art ihrer Elternklasse ist. Die entsprechenden Typen haben die gleiche Beziehung, wobei der Typ der Kindklasse als Untertyp der Elternklasse angesehen wird. Da Kindklassen die gesamte Funktionalität ihrer Elternklassen erben, ist ein Objekt des Kindtyps in gewisser Weise äquivalent zum Elterntyp oder eine Erweiterung desselben. Ein Objekt des Kindtyps kann anstelle eines Objekts des Elterntyps verwendet werden.

Wenn du zum Beispiel eine neue Klasse Dog erstellst, die Animal erweitert, wird der neue Typ Dog als Subtyp von Animal betrachtet. Objekte des Typs Dog können dann überall dort verwendet werden, wo ein Objekt des Typs Animal verwendet werden kann; ein Objekt des Typs Dog kann einer Variablen des Typs Animal zugewiesen werden. Dies wird als Subtyp-Polymorphismus bezeichnet und ist eines der wichtigsten Merkmale einer objektorientierten Sprache. In Kapitel 5 werden wir uns näher mit Klassen und Objekten befassen.

Primitive Typen werden in Java "nach Wert" verwendet und weitergegeben. Das heißt, wenn ein primitiver Wert wie int einer Variablen zugewiesen oder als Argument an eine Methode übergeben wird, wird sein Wert kopiert. Auf Referenztypen (Klassentypen) wird dagegen immer "per Referenz" zugegriffen. Eine Referenz ist ein Handle oder ein Name für ein Objekt. Eine Variable eines Referenztyps ist ein "Zeiger" auf ein Objekt ihres Typs (oder eines Subtyps, wie oben beschrieben). Wenn du die Referenz einer Variablen zuweist oder sie an eine Methode weitergibst, wird nur die Referenz kopiert, nicht das Objekt, auf das sie zeigt. Eine Referenz ist wie ein Zeiger in C oder C++, mit dem Unterschied, dass ihr Typ streng erzwungen ist. Der Referenzwert selbst kann nicht explizit erstellt oder geändert werden. Um einer Variablen vom Typ Referenz einen Referenzwert zu geben, musst du ein entsprechendes Objekt zuweisen.

Lass uns ein Beispiel durchspielen. Wir deklarieren eine Variable vom Typ Car, genannt myCar, und weisen ihr ein entsprechendes Objekt zu:4

    Car myCar = new Car();
    Car anotherCar = myCar;

myCar ist eine Variable vom Typ Referenz, die einen Verweis auf das neu erstellte Car Objekt enthält. (Kümmere dich vorerst nicht um die Details der Objekterstellung; auch das werden wir in Kapitel 5 behandeln.) Wir deklarieren eine zweite Variable vom Typ Car, anotherCar, und weisen sie demselben Objekt zu. Jetzt gibt es zwei identische Referenzen: myCar und anotherCar, aber nur eine tatsächliche Car Objektinstanz. Wenn wir den Zustand des Car Objekts selbst ändern, sehen wir den gleichen Effekt, wenn wir es mit beiden Referenzen betrachten. Wir können ein wenig hinter die Kulissen schauen, indem wir das mit jshell ausprobieren:

jshell> class Car {}
|  created class Car

jshell> Car myCar = new Car()
myCar ==> Car@21213b92

jshell> Car anotherCar = myCar
anotherCar ==> Car@21213b92

jshell> Car notMyCar = new Car()
notMyCar ==> Car@66480dd7

Beachte das Ergebnis der Erstellung und der Zuweisungen. Hier kannst du sehen, dass Java-Referenztypen mit einem Zeigerwert (21213b92, die rechte Seite von @) und ihrem Typ (Car, die linke Seite von @) kommen. Wenn wir ein neues Car Objekt erstellen, notMyCar, erhalten wir einen anderen Zeigerwert. myCar und anotherCar zeigen auf dasselbe Objekt; notMyCar zeigt auf ein zweites, separates Objekt.

Typen ableiten

Moderne Versionen von Java haben die Fähigkeit, Variablentypen in vielen Situationen abzuleiten, kontinuierlich verbessert. Seit Java 10 kannst du das Schlüsselwort var in Verbindung mit der Deklaration und Initiierung einer Variablen verwenden und dem Compiler erlauben, den richtigen Typ zu ermitteln:

jshell> class Car2 {}
|  created class Car2

jshell> Car2 myCar2 = new Car2()
myCar2 ==> Car2@728938a9

jshell> var myCar3 = new Car2()
myCar3 ==> Car2@6433a2

Beachte die (zugegebenermaßen hässliche) Ausgabe, wenn du myCar3 in jshell erstellst. Obwohl wir den Typ nicht explizit angegeben haben, wie wir es bei myCar2 getan haben, kann der Compiler den korrekten Typ leicht erkennen, und wir erhalten tatsächlich ein Car2 Objekt.

Übergabe von Referenzen

Objektreferenzen werden auf die gleiche Weise an Methoden übergeben. In diesem Fall würden entweder myCar oder anotherCar als äquivalente Argumente für eine hypothetische Methode, genannt myMethod(), in unserer hypothetischen Klasse dienen:

    myMethod(myCar);

Ein wichtiger, aber manchmal verwirrender Unterschied ist, dass die Referenz selbst ein Wert (eine Speicheradresse) ist. Dieser Wert wird kopiert, wenn du ihn einer Variablen zuweist oder ihn in einem Methodenaufruf übergibst. In unserem vorherigen Beispiel ist das Argument, das an eine Methode übergeben wird (eine lokale Variable aus Sicht der Methode), eigentlich eine dritte Referenz auf das Car Objekt, zusätzlich zu myCar und anotherCar.

Die Methode kann den Zustand des Car Objekts durch diese Referenz verändern, indem sie die Methoden des Car Objekts aufruft oder seine Variablen verändert. Allerdings kann myMethod() nicht die Vorstellung des Aufrufers von der Referenz auf myCar ändern: Das heißt, die Methode kann die myCar des Aufrufers nicht so ändern, dass sie auf ein anderes Car Objekt zeigt; sie kann nur ihre eigene Referenz ändern. Das wird noch deutlicher, wenn wir später über Methoden sprechen.

Referenztypen verweisen immer auf Objekte (oder null), und Objekte werden immer von Klassen definiert. Wenn du eine Instanz- oder Klassenvariable bei ihrer Deklaration nicht initialisierst, weist der Compiler ihr wie bei nativen Typen den Standardwert null zu. Wie bei nativen Typen werden auch lokale Variablen, die einen Referenztyp haben, nicht standardmäßig initialisiert, so dass du deinen eigenen Wert setzen musst, bevor du sie benutzt. Zwei spezielle Arten von Referenztypen - Arrays und Interfaces - legen den Typ des Objekts, auf das sie verweisen, jedoch auf eine etwas andere Weise fest.

Arrays in Java sind eine interessante Art von Objekten, die automatisch erstellt werden, um eine Sammlung von Objekten eines anderen Typs, dem sogenannten Basistyp, zu speichern. Ein einzelnes Element in einem Array hat diesen Basistyp. (Ein Element eines Arrays vom Typ int[] ist also ein int, und ein Element eines Arrays vom Typ String[] ist ein String.) Durch die Deklaration eines Arrays wird implizit ein neuer Klassentyp erstellt, der als Container für seinen Basistyp dient, wie du später in diesem Kapitel sehen wirst.

Schnittstellen sind ein bisschen raffinierter. Eine Schnittstelle definiert eine Reihe von Methoden und gibt dieser Reihe einen entsprechenden Typ. Auf ein Objekt, das die Methoden der Schnittstelle implementiert, kann sowohl mit dem Typ der Schnittstelle als auch mit seinem eigenen Typ Bezug genommen werden. Variablen und Methodenargumente können genau wie andere Klassentypen als Schnittstellentypen deklariert werden, und jedes Objekt, das die Schnittstelle implementiert, kann ihnen zugewiesen werden. Das macht das Typensystem flexibler und ermöglicht es Java, die Grenzen der Klassenhierarchie zu überschreiten und Objekte zu erstellen, die tatsächlich viele Typen haben. Wir werden Schnittstellen auch in Kapitel 5 behandeln.

Generische Typen oder parametrisierte Typen, wie wir bereits erwähnt haben, sind eine Erweiterung der Java-Klassensyntax, die eine zusätzliche Abstraktion in der Art und Weise ermöglicht, wie Klassen mit anderen Java-Typen arbeiten. Generische Typen ermöglichen es dem Programmierer, eine Klasse zu spezialisieren, ohne den Code der Klasse zu ändern. Wir behandeln Generics ausführlich in Kapitel 7.

Ein Wort über Strings

Strings in Java sind Objekte; sie sind also ein Referenztyp. String Objekte haben jedoch eine besondere Hilfe vom Java-Compiler, die sie eher wie primitive Typen aussehen lässt. Literale String-Werte im Java-Quellcode, also eine Reihe von Zeichen oder Escape-Sequenzen zwischen doppelten Anführungszeichen, werden vom Compiler in String Objekte umgewandelt. Du kannst ein String Literal direkt verwenden, es als Argument an Methoden übergeben oder es einer Variablen vom Typ String zuweisen:

    System.out.println("Hello, World...");
    String s = "I am the walrus...";
    String t = "John said: \"I am the walrus...\"";

Das Symbol + in Java ist überladen und kann sowohl mit Strings als auch mit regulären Zahlen arbeiten. Überladen ist ein Begriff, der in Sprachen verwendet wird, die es ermöglichen, denselben Methodennamen oder dasselbe Operatorsymbol zu verwenden, wenn man mit unterschiedlichen Datentypen arbeitet. Bei Zahlen führt + die Addition durch. Bei Zeichenketten führt + die Verkettung durch, was Programmierer das Zusammenfügen zweier Zeichenketten nennen. Während Java das beliebige Überladen von Methoden erlaubt (mehr unter "Methodenüberladung"), ist + einer der wenigen überladenen Operatoren in Java:

    String quote = "Fourscore and " + "seven years ago,";
    String more = quote + " our" + " fathers" +  " brought...";

    // quote is now "Fourscore and seven years ago,"
    // more is now " our fathers brought..."

Java erstellt ein einzelnes String Objekt aus den verketteten String-Literalen und liefert es als Ergebnis des Ausdrucks. (Mehr zu String in Kapitel 8.)

Aussagen und Ausdrücke

Java-Anweisungen erscheinen innerhalb von Methoden und Klassen. Sie beschreiben alle Aktivitäten in einem Java-Programm. Variablendeklarationen und Zuweisungen, wie die im vorherigen Abschnitt, sind Anweisungen, ebenso wie grundlegende Sprachstrukturen wie if/then-Bedingungen und Schleifen. (Mehr zu diesen Strukturen später in diesem Kapitel.) Hier sind einige Anweisungen in Java:

    int size = 5;
    if (size > 10)
      doSomething();
    for (int x = 0; x < size; x++) {
      doSomethingElse();
      doMoreThings();
    }

Ausdrücke erzeugen Werte; Java wertet einen Ausdruck aus, um ein Ergebnis zu erhalten. Dieses Ergebnis kann dann als Teil eines anderen Ausdrucks oder in einer Anweisung verwendet werden. Methodenaufrufe, Objektzuweisungen und natürlich auch mathematische Ausdrücke sind Beispiele für Ausdrücke:

    // These are all valid Java expressions
    new Object()
    Math.sin(3.1415)
    42 * 64

Einer der Grundsätze von Java ist es, die Dinge einfach und konsistent zu halten. Wenn es keine anderen Einschränkungen gibt, werden Auswertungen und Initialisierungen in Java immer in der Reihenfolge durchgeführt, in der sie im Code erscheinen - von links nach rechts und von oben nach unten. Diese Regel wird bei der Auswertung von Zuweisungsausdrücken, Methodenaufrufen und Array-Indizes angewendet, um nur einige Beispiele zu nennen. In einigen anderen Sprachen ist die Reihenfolge der Auswertung komplizierter oder sogar von der Implementierung abhängig. Java beseitigt diese Gefahr, indem es genau und einfach definiert, wie der Code ausgewertet wird.

Das bedeutet aber nicht, dass du anfangen solltest, obskure und verworrene Anweisungen zu schreiben. Sich auf die Reihenfolge der Auswertung von Ausdrücken auf komplexe Weise zu verlassen, ist eine schlechte Programmiergewohnheit, selbst wenn es funktioniert. Sie führt zu Code, der schwer zu lesen und schwerer zu ändern ist.

Aussagen

In jedem Programm sind es die Anweisungen, die den eigentlichen Zauber ausmachen. Anweisungen helfen uns, die Algorithmen zu implementieren, die wir am Anfang dieses Kapitels erwähnt haben. Sie helfen nicht nur, sie sind sogar die eigentliche Programmierzutat, die wir verwenden; jeder Schritt in einem Algorithmus entspricht einer oder mehreren Anweisungen. Anweisungen erfüllen in der Regel eine von vier Aufgaben:

  • Eingabe sammeln, um sie einer Variablen zuzuweisen

  • Ausgabe schreiben (auf dein Terminal, auf JLabel, etc.)

  • Eine Entscheidung darüber treffen, welche Anweisungen ausgeführt werden sollen

  • Wiederhole eine oder mehrere andere Aussagen

Anweisungen und Ausdrücke in Java erscheinen innerhalb eines Codeblocks. Ein Codeblock enthält eine Reihe von Anweisungen, die von einer offenen geschweiften Klammer ({) und einer geschlossenen geschweiftenKlammer (}) umgeben sind. Die Anweisungen in einem Codeblock können Variablendeklarationen und die meisten der anderen Arten von Anweisungen und Ausdrücken enthalten, die wir bereits erwähnt haben:

  {
    int size = 5;
    setName("Max");
    // more statements could follow...
  }

In gewissem Sinne sind Methoden nur Codeblöcke, die Parameter annehmen und mit ihrem Namen aufgerufen werden können - eine hypothetische Methode setUpDog() könnte zum Beispiel so beginnen:

  setUpDog(String name) {
    int size = 5;
    setName(name);
    // do any other setup work ...
  }

Die Deklarationen von Variablen haben in Java einen Geltungsbereich. Sie sind auf den sie umschließenden Codeblock beschränkt, d.h. du kannst eine Variable außerhalb des nächsten Satzes geschweifter Klammern weder sehen noch verwenden:

    {
      // Scopes are like Vegas...
      // What's declared in a scope, stays in that scope
      int i = 5;
    }

    i = 6;  // Compile-time error, no such variable i

Auf diese Weise kannst du Codeblöcke verwenden, um Anweisungen und Variablen beliebig zu gruppieren. Die häufigste Verwendung von Codeblöcken ist jedoch die Definition einer Gruppe von Anweisungen zur Verwendung in einer bedingten oder iterativen Anweisung.

if/else-Bedingungen

Eines der Schlüsselkonzepte in der Programmierung ist der Begriff "Entscheidung". "Existiert diese Datei?" oder "Hat der Benutzer eine WiFi-Verbindung?" sind Beispiele für Entscheidungen, die Computerprogramme und Apps ständig treffen. Java verwendet die beliebte if/else Anweisung für viele dieser Arten von Entscheidungen.5 Java definiert eine if/else Klausel wie folgt:

    if (condition)
      statement1;
    else
      statement2;

Auf Englisch könnte man die Anweisung if/else so lesen: "Wenn die Bedingung wahr ist, führe statement1 aus. Andernfalls führe statement2 aus."

condition ist ein boolescher Ausdruck und muss in Klammern gesetzt werden. Ein boolescher Ausdruck wiederum ist entweder ein boolescher Wert (true oder false) oder ein Ausdruck, der zu einem dieser Werte ausgewertet wird.6 Zum Beispiel ist i == 0 ein boolescher Ausdruck, der prüft, ob die Ganzzahl i den Wert 0 enthält:

// filename: ch04/examples/IfDemo.java
    int i = 0;
    // you can use i now to do other work and then
    // we can test it to see if anything has changed
    if (i == 0)
      System.out.println("i is still zero");
    else
      System.out.println("i is most definitely not zero");

Das gesamte vorangegangene Beispiel ist selbst eine Anweisung und könnte in einer anderen if/else Klausel verschachtelt werden. Die if Klausel kann zwei verschiedene Formen annehmen: einen "Einzeiler" oder einen Block. Wir werden dieses Muster auch bei anderen Anweisungen wie den im nächsten Abschnitt besprochenen Schleifen sehen. Wenn du nur eine Anweisung auszuführen hast (wie die einfachen println() -Aufrufe im vorherigen Schnipsel), kannst du diese einzelne Anweisung nach dem if -Test oder nach dem else -Schlüsselwort einfügen. Wenn du mehr als eine Anweisung ausführen musst, verwendest du einen Block. Die Blockform sieht wie folgt aus:

    if (condition) {
      // condition was true, execute this block
      statement;
      statement;
      // and so on...
    } else {
      // condition was false, execute this block
      statement;
      statement;
      // and so on...
    }

Hier werden alle eingeschlossenen Anweisungen im Block ausgeführt, egal welche Verzweigung genommen wird. Wir können diese Form verwenden, wenn wir mehr tun müssen, als nur eine Meldung zu drucken. Wir können zum Beispiel sicherstellen, dass eine andere Variable, vielleicht j, nicht negativ ist:

// filename: ch04/examples/IfDemo.java
    int j = 0;
    // you can use j now to do work like i before,
    // then make sure that work didn't drop
    // j's value below zero
    if (j < 0) {
      System.out.println("j is less than 0! Resetting.");
      j = 0;
    } else {
      System.out.println("j is positive or 0. Continuing.");
    }

Beachte, dass wir geschweifte Klammern für die if Klausel mit zwei Anweisungen und für die else Klausel mit einem einzigen println() Aufruf verwendet haben. Du kannst immer einen Block verwenden, wenn du willst. Wenn du aber nur eine Anweisung hast, ist der Block mit seinen geschweiften Klammern optional.

Switch-Anweisungen

Viele Sprachen unterstützen eine "eine von vielen"-Bedingung, die auch als Switch- oder Case-Anweisung bekannt ist. Bei einer Variablen oder einem Ausdruck bietet eine switch Anweisung mehrere Optionen, die übereinstimmen könnten. Und wir meinen wirklich "könnten". Ein Wert muss mit keiner der switch Optionen übereinstimmen; in diesem Fall passiert nichts. Wenn der Ausdruck mit einer case übereinstimmt, wird dieser Zweig ausgeführt. Wenn mehr als eine case übereinstimmen würde, gewinnt die erste Übereinstimmung.

Die gebräuchlichste Form der Java-Anweisung switch nimmt eine ganze Zahl (oder ein Argument vom Typ numerisch, das automatisch in einen Ganzzahlentyp umgewandelt werden kann) oder eine Zeichenkette und wählt zwischen einer Reihe von alternativen, konstanten case Verzweigungen aus:7

    switch (expression) {
      case constantExpression :
        statement;
      [ case constantExpression :
        statement;  ]
      // ...
      [ default :
        statement;  ]
    }

Der case-Ausdruck für jede Verzweigung muss zur Kompilierzeit einen anderen konstanten Integer- oder String-Wert ergeben. Strings werden mit der Methode String equals() verglichen, auf die wir in Kapitel 8 näher eingehen werden.

Du kannst einen optionalen default Fall angeben, um nicht übereinstimmende Bedingungen abzufangen. Bei der Ausführung findet der Schalter einfach die Verzweigung, die seinem bedingten Ausdruck entspricht (oder die Standardverzweigung) und führt die entsprechende Anweisung aus. Aber das ist noch nicht das Ende der Geschichte. Die Anweisung switch führt die Verzweigungen nach der übereinstimmenden Verzweigung weiter aus, bis sie auf das Ende des Schalters oder eine spezielle Anweisung namens break trifft. Hier sind ein paar Beispiele:

// filename: ch04/examples/SwitchDemo.java
    int value = 2;

    switch(value) {
      case 1:
        System.out.println(1);
      case 2:
        System.out.println(2);
      case 3:
        System.out.println(3);
    }
    // prints both 2 and 3

Üblicher ist die Verwendung von break, um jeden Zweig zu beenden:

// filename: ch04/examples/SwitchDemo.java
    int value = GOOD;

    switch (value) {
      case GOOD:
        // something good
        System.out.println("Good");
        break;
      case BAD:
        // something bad
        System.out.println("Bad");
        break;
      default:
        // neither one
        System.out.println("Not sure");
        break;
    }
    // prints only "Good"

In diesem Beispiel wird nur eine Verzweigung -GOOD, BAD oder die Standardverzweigung - ausgeführt. Das "Weitergehen"-Verhalten von switch ist gerechtfertigt, wenn du mehrere mögliche Fallwerte mit derselben(n) Anweisung(en) abdecken willst, ohne dass du einen Haufen Code duplizieren musst:

// filename: ch04/examples/SwitchDemo.java
    int value = MINISCULE;
    String size = "Unknown";

    switch(value) {
      case MINISCULE:
      case TEENYWEENY:
      case SMALL:
        size = "Small";
        break;
      case MEDIUM:
        size = "Medium";
        break;
      case LARGE:
      case EXTRALARGE:
        size = "Large";
        break;
    }

    System.out.println("Your size is: " + size);

In diesem Beispiel werden die sechs möglichen Werte effektiv in drei Fälle gruppiert. Und diese Gruppierungsfunktion kann jetzt direkt in Ausdrücken erscheinen. Java 12 bot Switch-Ausdrücke als Vorschaufunktion, die mit Java 14 verfeinert und dauerhaft gemacht wurde.

Anstatt die Größennamen im obigen Beispiel auszudrucken, könnten wir zum Beispiel unsere Größenbezeichnung direkt einer Variablen zuweisen:

// filename: ch04/examples/SwitchDemo.java

    int value = EXTRALARGE;

    String size = switch(value) {
      case MINISCULE, TEENYWEENY, SMALL -> "Small";
      case MEDIUM -> "Medium";
      case LARGE, EXTRALARGE -> "Large";
      default -> "Unknown";
    }; // note the semicolon! It completes the switch statement

    System.out.println("Your size is: " + size);
    // prints "Your size is Large"

Beachte die neue "Pfeil"-Syntax (ein Bindestrich gefolgt von einem Größer-als-Symbol). Du verwendest immer noch getrennte case Einträge, aber mit dieser Ausdruckssyntax werden die Groß- und Kleinbuchstaben in einer durch Komma getrennten Liste angegeben und nicht als separate, kaskadierende Einträge. Zwischen der Liste und dem Wert, der zurückgegeben werden soll, wird dann -> verwendet. Diese Form macht den Ausdruck switch etwas kompakter und (hoffentlich) lesbarer.

do/while-Schleifen

Das andere wichtige Konzept zur Steuerung, welche Anweisung als nächstes ausgeführt wird(Kontrollfluss oder Flow of Control in der Programmiersprache), ist die Wiederholung. Computer sind wirklich gut darin, Dinge immer und immer wieder zu tun. Die Wiederholung eines Codeblocks wird mit einer Schleife durchgeführt. In Java gibt es eine Reihe von verschiedenen Schleifenanweisungen. Jede Art von Schleife hat Vor- und Nachteile. Schauen wir uns diese verschiedenen Typen jetzt an.

Die iterativen Anweisungen do und while laufen so lange, wie ein boolescher Ausdruck (oft als Bedingung der Schleife bezeichnet) einen Wert true liefert. Die Grundstruktur dieser Schleifen ist sehr einfach:

    while (condition)
      statement; // or block

    do
      statement; // or block
    while (condition);

Eine while Schleife eignet sich perfekt, um auf eine externe Bedingung zu warten, wie z.B. den Erhalt einer neuen E-Mail:

    while(mailQueue.isEmpty())
      wait();

Natürlich muss diese hypothetische wait() Methode eine Begrenzung haben (typischerweise ein Zeitlimit, wie z.B. eine Sekunde warten), damit sie beendet wird und die Schleife eine weitere Chance bekommt, zu laufen. Aber sobald du eine E-Mail hast, willst du auch alle Nachrichten verarbeiten, die angekommen sind, nicht nur eine. Auch hier ist eine while Schleife perfekt. Du kannst einen Block von Anweisungen innerhalb geschweifter Klammern verwenden, wenn du mehr als eine Anweisung in deiner Schleife ausführen musst. Betrachte einen einfachen Countdown-Drucker:

// filename: ch04/examples/WhileDemo.java

    int count = 10;
    while(count > 0) {
      System.out.println("Counting down: " + count);
      // maybe do other useful things
      // and decrement our count
      count = count - 1;
    }
    System.out.println("Done");

In diesem Beispiel verwenden wir den Vergleichsoperator >, um unsere Variable count zu überwachen. Wir wollen weiterarbeiten, solange der Countdown positiv ist. Innerhalb des Schleifenkörpers geben wir den aktuellen Wert von count aus und verringern ihn dann um eins, bevor wir ihn wiederholen. Wenn wir count schließlich auf 0 reduzieren, hält die Schleife an, weil der Vergleich false zurückgibt.

Im Gegensatz zu while Schleifen, die ihre Bedingungen zuerst testen, führt eine do-while Schleife (oder häufiger nur eine do Schleife) ihren Anweisungskörper immer mindestens einmal aus. Ein klassisches Beispiel ist die Überprüfung der Eingaben eines Benutzers. Du weißt, dass du eine bestimmte Information brauchst, also forderst du diese Information im Schleifenkörper an. Die Bedingung der Schleife kann auf Fehler prüfen. Wenn es ein Problem gibt, beginnt die Schleife von vorne und fordert die Informationen erneut an. Dieser Vorgang kann so lange wiederholt werden, bis deine Anfrage fehlerfrei zurückkommt und du weißt, dass du gute Informationen hast.

    do {
      System.out.println("Please enter a valid email: ");
      String email = askUserForEmail();
    } while (email.hasErrors());

Auch hier wird der Körper einer do Schleife mindestens einmal ausgeführt. Wenn der Nutzer beim ersten Mal eine gültige E-Mail-Adresse angibt, wiederholen wir die Schleife einfach nicht.

Die for-Schleife

Eine weitere beliebte Schleifenanweisung ist die for Schleife. Sie eignet sich besonders gut zum Zählen. Die allgemeinste Form der for Schleife ist ebenfalls ein Überbleibsel aus der Sprache C. Sie kann etwas unübersichtlich aussehen, aber sie stellt eine ganze Menge Logik kompakt dar:

    for (initialization; condition; incrementor)
      statement; // or block

Der Abschnitt zur Variableninitialisierung kann Variablen deklarieren oder initialisieren, die auf den Geltungsbereich des for Körpers beschränkt sind. Die for Schleife beginnt dann eine mögliche Reihe von Runden, in denen die Bedingung zuerst geprüft und, wenn sie wahr ist, die Body-Anweisung (oder der Block) ausgeführt wird. Nach jeder Ausführung des Rumpfes werden die Inkrementor-Ausdrücke ausgewertet, um ihnen die Möglichkeit zu geben, Variablen zu aktualisieren, bevor die nächste Runde beginnt. Betrachte eine klassische Zählschleife:

// filename: ch04/examples/ForDemo.java

    for (int i = 0; i < 100; i++) {
      System.out.println(i);
      int j = i;
      // do any other work needed
    }

Diese Schleife wird 100 Mal ausgeführt und gibt dabei Werte von 0 bis 99 aus. Wir deklarieren und initialisieren eine Variable, i, auf Null. Mit der Bedingungsklausel prüfen wir, ob i kleiner als 100 ist. Wenn ja, führt Java den Hauptteil der Schleife aus. In der Inkrement-Klausel erhöhen wir i um eins. (Mehr über die Vergleichsoperatoren wie < und > sowie die Abkürzung ++ im nächsten Abschnitt "Ausdrücke") . Nachdem i inkrementiert wurde, kehrt die Schleife zurück, um die Bedingung zu überprüfen. Java wiederholt diese Schritte (Bedingung, Körper, Inkrement) so lange, bis i den Wert 100 erreicht.

Erinnere dich daran, dass die Variable j für den Block lokal ist (nur für Anweisungen innerhalb des Blocks sichtbar) und für den Code nach der for Schleife nicht mehr zugänglich ist. Wenn die Bedingung einer for Schleife bei der ersten Überprüfung false zurückgibt (zum Beispiel, wenn wir i in der Initialisierungsklausel auf 1.000 setzen), werden der Hauptteil und der Inkrementor-Abschnitt nie ausgeführt.

Du kannst mehrere kommagetrennte Ausdrücke in den Initialisierungs- und Inkrementierungsabschnitten der for Schleife verwenden. Zum Beispiel:

// filename: ch04/examples/ForDemo.java

    // generate some coordinates
    for (int x = 0, y = 10; x < y; x++, y--) {
      System.out.println(x + ", " + y);
      // do other stuff with our new (x, y)...
    }

Du kannst auch bestehende Variablen außerhalb des Bereichs der for Schleife innerhalb des Initialisierungsblocks initialisieren. Das könntest du tun, wenn du den Endwert der Schleifenvariable an anderer Stelle verwenden willst. Diese Praxis ist im Allgemeinen verpönt: Sie ist fehleranfällig und kann deinen Code schwer verständlich machen. Dennoch ist sie legal und du wirst vielleicht auf eine Situation stoßen, in der dieses Verhalten für dich am sinnvollsten ist:

    int x;
    for(x = 0; x < someHaltingValue; x++) {
      System.out.print(x + ": ");
      // do whatever work you need ...
    }
    // x is still valid and available
    System.out.println("After the loop, x is: " + x);

Du kannst den Initialisierungsschritt sogar ganz weglassen, wenn du mit einer Variablen arbeiten willst, die bereits einen guten Startwert hat:

    int x = 1;
    for(; x < someHaltingValue; x++) {
      System.out.print(x + ": ");
      // do whatever work you need ...
    }

Beachte, dass du immer noch das Semikolon brauchst, das normalerweise den Initialisierungsschritt von der Bedingung trennt.

Die erweiterte for-Schleife

Javas mit dem verheißungsvollen Namen "erweiterte for -Schleife" funktioniert wie die Anweisung foreach in einigen anderen Sprachen und iteriert über eine Reihe von Werten in einem Array oder einer anderen Art von Sammlung:

    for (varDeclaration : iterable)
      statement_or_block;

Die erweiterte Schleife for kann verwendet werden, um Schleifen über Arrays jeden Typs sowie über jede Art von Java-Objekt zu ziehen, das die Schnittstelle java.lang.Iterable implementiert. (Über Arrays, Klassen und Schnittstellen werden wir in Kapitel 5 mehr sagen.) Dazu gehören die meisten Klassen der Java Collections API (siehe Kapitel 7). Hier sind ein paar Beispiele:

// filename: ch04/examples/EnhancedForDemo.java

    int [] arrayOfInts = new int [] { 1, 2, 3, 4 };
    int total = 0;

    for(int i  : arrayOfInts) {
      System.out.println(i);
      total = total + i;
    }
    System.out.println("Total: " + total);

    // ArrayList is a popular collection class
    ArrayList<String> list = new ArrayList<String>();
    list.add("foo");
    list.add("bar");

    for(String s : list)
      System.out.println(s);

Auch in diesem Beispiel haben wir nicht über Arrays oder die Klasse ArrayList und ihre spezielle Syntax gesprochen. Was wir hier zeigen, ist die Syntax der erweiterten for Schleife, die sowohl über ein Array als auch über eine Liste von Stringwerten iteriert. Die Kürze dieser Form macht sie beliebt, wenn du mit einer Sammlung von Elementen arbeiten musst.

Pause/Fortsetzen

Die Java-Anweisung break und ihr Freund continue können auch verwendet werden, um eine Schleife oder eine bedingte Anweisung abzukürzen, indem sie aus ihr herausspringen. Die Anweisung break veranlasst Java, die aktuelle Schleifenanweisung (oder switch) anzuhalten und den Rest des Rumpfes zu überspringen. Java fährt mit der Ausführung des Codes fort, der nach der Schleife kommt. Im folgenden Beispiel geht die while Schleife endlos weiter, bis die watchForErrors() Methode true zurückgibt und eine break Anweisung auslöst, die die Schleife anhält und an der Stelle fortfährt, die mit "nach der while Schleife" markiert ist:

    while(true) {
      if (watchForErrors())
        break;
      // No errors yet so do some work...
    }
    // The "break" will cause execution to
    // resume here, after the while loop

Die Anweisung continue bewirkt, dass die Schleifen for und while zu ihrer nächsten Iteration übergehen, indem sie zu dem Punkt zurückkehren, an dem sie ihre Bedingung überprüfen. Das folgende Beispiel gibt die Zahlen 0 bis 9 aus, wobei die Zahl 5 übersprungen wird:

// filename: ch04/examples/ForDemo.java

    for (int i = 0; i < 10; i++) {
      if (i == 5)
        continue;
      System.out.println(i);
    }

Die Anweisungen break und continue sehen aus wie die Anweisungen in der Sprache C, aber die Java-Formulare haben die zusätzliche Fähigkeit, ein Label als Argument zu nehmen und mehrere Ebenen zum Bereich des gelabelten Punktes im Code zu springen. Diese Funktion ist im Java-Alltag nicht sehr verbreitet, kann aber in besonderen Fällen wichtig sein. So sieht das aus:

    labelOne:
      while (condition1) {
        // ...
        labelTwo:
          while (condition2) {
            // ...
            if (smallProblem)
              break; // Will break out of just this loop

            if (bigProblem)
              break labelOne; // Will break out of both loops
          }
        // after labelTwo
      }
    // after labelOne

Einschließende Anweisungen, wie Codeblöcke, Bedingungen und Schleifen, können mit Bezeichnern wie labelOne und labelTwo gekennzeichnet werden. In diesem Beispiel hat break oder continue ohne Argument die gleiche Wirkung wie die früheren Beispiele. Eine break bewirkt, dass die Verarbeitung an dem Punkt "nach labelTwo" fortgesetzt wird; eine continue bewirkt, dass die labelTwo Schleife sofort zu ihrem Bedingungstest zurückkehrt.

Wir könnten die Anweisung break labelTwo in der Anweisung smallProblem verwenden. Sie hätte die gleiche Wirkung wie eine normale break, aber break labelOne bricht, wie bei der Anweisung bigProblem zu sehen, aus beiden Ebenen aus und setzt an dem Punkt fort, der mit "nach labelOne" bezeichnet ist. In ähnlicher Weise würde continue labelTwo wie eine normale continue wirken, aber continue labelOne würde zum Test der labelOne Schleife zurückkehren. Mit den mehrstufigen Anweisungen break und continue entfällt die Hauptbegründung für die viel geschmähte Anweisung goto in C/C++.8

Es gibt ein paar Java-Anweisungen, auf die wir jetzt nicht eingehen werden. Die Anweisungen try, catch und finally werden für die Behandlung von Ausnahmen verwendet, wie wir in Kapitel 6 besprechen werden. Die Anweisung synchronized wird in Java verwendet, um den Zugriff auf Anweisungen zwischen mehreren Threads zu koordinieren; in Kapitel 9 wird die Synchronisierung von Threads besprochen .

Unerreichbare Anweisungen

Abschließend sollten wir erwähnen, dass der Java-Compiler unerreichbare Anweisungen als Compile-Time-Fehler kennzeichnet. Eine unerreichbare Anweisung ist eine, bei der der Compiler feststellt, dass sie nie aufgerufen wird. Natürlich werden viele Methoden oder Codeteile in deinem Programm vielleicht nie aufgerufen, aber der Compiler erkennt nur diejenigen, von denen er durch eine geschickte Überprüfung zur Kompilierzeit "beweisen" kann, dass sie nie aufgerufen werden. Eine Methode mit einer unbedingten return Anweisung in der Mitte führt beispielsweise zu einem Kompilierfehler, ebenso wie eine Methode mit einer Bedingung, von der der Compiler weiß, dass sie nie erfüllt wird:

    if (1 < 2) {
      // This branch always runs and the compiler knows it
      System.out.println("1 is, in fact, less than 2");
      return;
    } else {
      // unreachable statements, this branch never runs
      System.out.println("Look at that, seems we got \"math\" wrong.");
    }

Du musst die nicht erreichbaren Fehler korrigieren, bevor du die Kompilierung abschließen kannst. Glücklicherweise handelt es sich bei den meisten Fällen dieses Fehlers nur um Tippfehler, die leicht zu beheben sind. In dem seltenen Fall, dass diese Compilerprüfung einen Fehler in deiner Logik und nicht in deiner Syntax aufdeckt, kannst du den nicht ausführbaren Code jederzeit umstellen oder löschen.

Ausdrücke

Ein Ausdruck erzeugt ein Ergebnis, oder einen Wert, wenn er ausgewertet wird. Der Wert eines Ausdrucks kann ein numerischer Typ sein, wie bei einem arithmetischen Ausdruck; ein Referenztyp, wie bei einer Objektzuweisung; oder der spezielle Typ void, der der deklarierte Typ einer Methode ist, die keinen Wert zurückgibt. Im letzten Fall wird der Ausdruck nur nach seinen Nebeneffekten ausgewertet, d. h. nach der Arbeit, die er neben der Erzeugung eines Wertes leistet. Der Compiler kennt den Typ eines Ausdrucks. Der Wert, der zur Laufzeit erzeugt wird, hat entweder diesen Typ oder, im Falle eines Referenztyps, einen kompatiblen (zuweisbaren) Subtyp. (Mehr zu dieser Kompatibilität in Kapitel 5.)

Wir haben bereits einige Ausdrücke in unseren Beispielprogrammen und Codeschnipseln gesehen. Wir werden noch viele weitere Beispiele für Ausdrücke im Abschnitt "Zuweisung" sehen .

Betreiber

Mit Operatoren kannst du Ausdrücke auf verschiedene Arten kombinieren oder verändern. Sie "operieren" Ausdrücke. Java unterstützt fast alle Standardoperatoren aus der Sprache C. Diese Operatoren haben in Java den gleichen Vorrang wie in C, wie in Tabelle 4-3 dargestellt.9

Tabelle 4-3. Java-Operatoren
Vorrang Betreiber Operandentyp Beschreibung

1

++, -

Arithmetik

Inkrement und Dekrement

1

+, -

Arithmetik

Unäres Plus und Minus

1

~

Integral

Bitweises Komplement

1

!

Boolesche

Logisches Komplement

1

( type )

Jede

Guss

2

*, /, %

Arithmetik

Multiplikation, Division, Rest

3

+, -

Arithmetik

Addition und Subtraktion

3

+

String

String-Verkettung

4

<<

Integral

Linksverschiebung

4

>>

Integral

Rechtsverschiebung mit Vorzeichenerweiterung

4

>>>

Integral

Rechtsverschiebung ohne Verlängerung

5

<, <=, >, >=

Arithmetik

Numerischer Vergleich

5

instanceof

Objekt

Typenvergleich

6

==, !=

Primitiv

Gleichheit und Ungleichheit der Werte

6

==, !=

Objekt

Gleichheit und Ungleichheit des Bezugs

7

&

Integral

Bitweises UND

7

&

Boolesche

Boolesches UND

8

^

Integral

Bitweises XOR

8

^

Boolesche

Boolesches XOR

9

|

Integral

Bitweises ODER

9

|

Boolesche

Boolesches ODER

10

&&

Boolesche

Bedingtes UND

11

||

Boolesche

Bedingtes ODER

12

?:

N/A

Bedingter ternärer Operator

13

=

Jede

Aufgabe

Wir sollten auch beachten, dass der Prozent-Operator (%) kein Modulo-Operator im eigentlichen Sinne ist, sondern ein Rest-Operator, der auch einen negativen Wert haben kann. Versuche, mit einigen dieser Operatoren in jshell zu spielen, um ein besseres Gefühl für ihre Auswirkungen zu bekommen. Wenn du noch neu in der Programmierung bist, ist es besonders nützlich, dich mit den Operatoren und ihrer Rangfolge vertraut zu machen. Du wirst regelmäßig auf Ausdrücke und Operatoren stoßen, selbst wenn du ganz alltägliche Aufgaben in deinem Code ausführst:

jshell> int x = 5
x ==> 5

jshell> int y = 12
y ==> 12

jshell> int sumOfSquares = x * x + y * y
sumOfSquares ==> 169

jshell> int explicitOrder = (((x * x) + y) * y)
explicitOrder ==> 444

jshell> sumOfSquares % 5
$7 ==> 4

Java fügt auch einige neue Operatoren hinzu. Wie wir gesehen haben, kannst du den + Operator mit String Werten verwenden, um eine String-Verkettung durchzuführen. Da alle Integer-Typen in Java vorzeichenbehaftete Werte sind, kannst du den >> Operator verwenden, um eine rechts-arithmetische Verschiebung mit Vorzeichenerweiterung durchzuführen. Der >>> Operator behandelt den Operanden als eine Zahl ohne Vorzeichen10 und führt eine rechts-arithmetische Verschiebung ohne Vorzeichenerweiterung durch. AlsProgrammierer müssen wir die einzelnen Bits in unseren Variablen nicht mehr so oft manipulieren wie früher, daher wirst du diese Verschiebungsoperatoren wahrscheinlich nicht sehr oft sehen. Wenn sie in Beispielen zur Kodierung oder zum Parsen von Binärdaten auftauchen, die du im Internet liest, kannst du dir in der jshell ansehen, wie sie funktionieren. Diese Art von Spiel ist eine unserer Lieblingsanwendungen für jshell!

Aufgabe

Während das Deklarieren und Initialisieren einer Variable als Anweisung ohne resultierenden Wert betrachtet wird, ist die Zuweisung einer Variablen allein ein Ausdruck:

    int i, j;   // statement with no resulting value
    int k = 6;  // also a statement with no result
    i = 5;      // both a statement and an expression

Normalerweise verlassen wir uns auf die Zuweisung nur wegen ihrer Nebeneffekte, wie in den ersten beiden Zeilen oben, aber eine Zuweisung kann als Wert in einem anderen Teil eines Ausdrucks verwendet werden. Manche Programmierer nutzen diese Tatsache, um einen bestimmten Wert mehreren Variablen gleichzeitig zuzuweisen:

    j = (i = 5);
    // both j and i are now 5

Wenn du dich ausgiebig auf die Reihenfolge der Auswertung verlässt (in diesem Fall mit zusammengesetzten Zuweisungen), kann der Code undurchsichtig und schwer zu lesen sein. Wir empfehlen das nicht, aber diese Art der Initialisierung taucht in Online-Beispielen auf.

Der Nullwert

Der Ausdruck null kann einem beliebigen Referenztyp zugewiesen werden. Er bedeutet "keine Referenz". Eine null Referenz kann nicht verwendet werden, um irgendetwas zu referenzieren, und der Versuch, dies zu tun, erzeugt zur Laufzeit eine NullPointerException. Aus dem Kapitel "Referenztypen" weißt du, dass null der Standardwert ist, der nicht initialisierten Klassen- und Instanzvariablen zugewiesen wird; führe deine Initialisierungen durch, bevor du Variablen vom Referenztyp verwendest, um diese Ausnahme zu vermeiden.

Variabler Zugang

Der Punkt (.) Operator wird verwendet, um Mitglieder einer Klasse oder einer Objektinstanz auszuwählen. (Er kann den Wert einer Instanzvariablen (eines Objekts) oder einer statischen Variable (einer Klasse) abrufen.) Sie kann auch eine Methode angeben, die für ein Objekt oder eine Klasse aufgerufen werden soll:

    int i = myObject.length;
    String s = myObject.name;
    myObject.someMethod();

Ein referenzartiger Ausdruck kann in zusammengesetzten Auswertungen verwendet werden (mehrere Verwendungen der Punkt-Operation in einem Ausdruck), indem weitere Variablen oder Methoden für das Ergebnis ausgewählt werden:

    int len = myObject.name.length();
    int initialLen = myObject.name.substring(5, 10).length();

Die erste Zeile ermittelt die Länge unserer name Variablen, indem sie die length() Methode des String Objekts aufruft. Im zweiten Fall machen wir einen Zwischenschritt und fragen nach einer Teilzeichenkette des name Strings. Die Methode substring der Klasse String gibt ebenfalls eine Referenz String zurück, für die wir die Länge abfragen. Das Zusammenfassen von Operationen wie dieser wird auch als Verkettung von Methodenaufrufen bezeichnet. Eine verkettete Auswahloperation, die wir schon oft verwendet haben, ist der Aufruf der Methode println() für die Variable out der Klasse System:

    System.out.println("calling println on out");

Methodenaufruf

Methoden sind Funktionen, die sich innerhalb einer Klasse befinden und je nach Art der Methode über die Klasse oder ihre Instanzen zugänglich sind. Der Aufruf einer Methode bedeutet, dass die Anweisungen des Methodenkörpers ausgeführt werden, wobei alle erforderlichen Parametervariablen übergeben werden und möglicherweise ein Wert zurückgegeben wird. Ein Methodenaufruf ist ein Ausdruck, der zu einem Wert führt. Der Typ des Wertes ist der Rückgabetyp der Methode:

    System.out.println("Hello, World...");
    int myLength = myString.length();

Hier haben wir die Methoden println() und length() für verschiedene Objekte aufgerufen. Die Methode length() gibt einen Integer-Wert zurück; der Rückgabetyp von println() ist void (kein Wert). Es ist wichtig zu betonen, dass println() eine Ausgabe erzeugt, aber keinen Wert. Wir können diese Methode nicht einer Variablen zuweisen, wie wir es oben mit length() getan haben:

jshell> String myString = "Hi there!"
myString ==> "Hi there!"

jshell> int myLength = myString.length()
myLength ==> 9

jshell> int mistake = System.out.println("This is a mistake.")
|  Error:
|  incompatible types: void cannot be converted to int
|  int mistake = System.out.println("This is a mistake.");
|                ^--------------------------------------^

Methoden machen den Großteil eines Java-Programms aus. Du könntest zwar einige triviale Anwendungen schreiben, die ausschließlich in einer einzigen main() Methode einer Klasse bestehen, aber du wirst schnell feststellen, dass du die Dinge aufbrechen musst. Methoden machen deine Anwendung nicht nur besser lesbar, sie öffnen auch die Türen zu komplexen, interessanten und nützlichen Anwendungen, die ohne sie einfach nicht möglich sind. Schau dir doch mal unsere grafischen Hello World-Anwendungen in "HelloJava" an. Wir haben mehrere Methoden verwendet, die für die Klasse JFrame definiert wurden.

Das sind einfache Beispiele, aber in Kapitel 5 wirst du sehen, dass es etwas komplexer wird, wenn es Methoden mit demselben Namen, aber unterschiedlichen Parametertypen in derselben Klasse gibt, oder wenn eine Methode in einer Unterklasse neu definiert wird.

Anweisungen, Ausdrücke und Algorithmen

Stellen wir eine Sammlung von Anweisungen und Ausdrücken dieser verschiedenen Typen zusammen, um ein konkretes Ziel zu erreichen. Mit anderen Worten: Wir schreiben einen Java-Code, um einen Algorithmus zu implementieren. Ein klassisches Beispiel für einen Algorithmus ist das Verfahren von Euklid, um den größten gemeinsamen Nenner (GCD) zweier Zahlen zu finden. Es verwendet einen einfachen (wenn auch mühsamen) Prozess der wiederholten Subtraktion. Wir können Javas while Schleife, eine if/else Bedingung und einige Zuweisungen verwenden, um die Aufgabe zu erledigen:

// filename: ch04/examples/EuclidGCD.java

    int a = 2701;
    int b = 222;
    while (b != 0) {
      if (a > b) {
        a = a - b;
      } else {
        b = b - a;
      }
    }
    System.out.println("GCD is " + a);

Es ist nicht schick, aber es funktioniert - und es ist genau die Art von Aufgabe, die Computerprogramme gut erfüllen können. Das ist es, was du hier suchst! Nun, du bist wahrscheinlich nicht hier, um den größten gemeinsamen Nenner von 2701 und 222 (übrigens 37) zu finden, aber du bist hier, um die Lösungen für Probleme als Algorithmen zu formulieren und diese Algorithmen in ausführbaren Java-Code zu übersetzen.

Hoffentlich fügen sich langsam ein paar weitere Teile des Programmierpuzzles zusammen. Aber mach dir keine Sorgen, wenn diese Ideen noch nicht ganz klar sind. Der ganze Programmierprozess erfordert eine Menge Übung. In einer der Programmierübungen in diesem Kapitel sollst du versuchen, den obigen Codeblock in eine echte Java-Klasse innerhalb der Methode main() zu übertragen. Versuche, die Werte von a und b zu ändern. In Kapitel 8 werden wir uns mit der Umwandlung von Zeichenketten in Zahlen befassen, so dass du den GCD einfach ermitteln kannst, indem du das Programm erneut ausführst und der Methode main() zwei Zahlen als Parameter übergibst, wie in Abbildung 2-10 gezeigt, ohne neu zu kompilieren.

Objekt erstellen

Objekte in Java werden mit dem new Operator zugewiesen:

    Object o = new Object();

Das Argument für new ist der Konstruktor für die Klasse. Der Konstruktor ist eine Methode, die immer denselben Namen hat wie die Klasse. Der Konstruktor gibt alle erforderlichen Parameter an, um eine Instanz des Objekts zu erstellen. Der Wert des Ausdrucks new ist ein Verweis auf den Typ des erstellten Objekts. Objekte haben immer einen oder mehrere Konstruktoren, auch wenn sie nicht immer für dich zugänglich sind.

Die Objekterstellung wird in Kapitel 5 ausführlich behandelt. Für den Moment genügt es zu wissen, dass die Objekterzeugung auch eine Art von Ausdruck ist und dass das Ergebnis eine Objektreferenz ist. Eine kleine Besonderheit ist, dass die Bindung von new "enger" ist als die des Punktselektors (.). Ein beliebter Nebeneffekt dieses Details ist, dass du ein neues Objekt erstellen und eine Methode dafür aufrufen kannst, ohne das Objekt einer Variablen vom Typ Referenz zuzuweisen. Du brauchst zum Beispiel die aktuelle Uhrzeit, aber nicht den Rest der Informationen, die in einem Date Objekt enthalten sind. Du brauchst keinen Verweis auf das neu erstellte Datum zu behalten, sondern kannst das benötigte Attribut einfach durch Verkettung abrufen:

jshell> int hours = new Date().getHours()
hours ==> 13

Die Klasse Date ist eine Hilfsklasse, die das aktuelle Datum und die Uhrzeit darstellt. Hier erstellen wir eine neue Instanz von Date mit dem Operator new und rufen die Methode getHours() auf, um die aktuelle Stunde als Integer-Wert abzurufen. Die Objektreferenz Date lebt lange genug, um den Methodenaufruf getHours() zu bedienen, und wird dann losgeschnitten und schließlich in den Müll geworfen (siehe "Garbage Collection").

Das Aufrufen von Methoden aus einer neuen Objektreferenz ist eine Frage des Stils. Es wäre sicherlich übersichtlicher, eine Zwischenvariable vom Typ Date für das neue Objekt zu reservieren und dann dessen Methode getHours() aufzurufen. Es ist jedoch üblich, Operationen zu kombinieren, wie wir es getan haben, um die obigen Stunden zu erhalten. Wenn du Java lernst und dich mit seinen Klassen und Typen vertraut machst, wirst du wahrscheinlich einige dieser Muster übernehmen. Bis dahin solltest du dir aber keine Sorgen machen, dass dein Code "langatmig" ist. Klarheit und Lesbarkeit sind wichtiger als stilistische Schnörkel, wenn du dich durch dieses Buch arbeitest.

Der instanceof-Operator

Du verwendest den instanceof Operator, um den Typ eines Objekts zur Laufzeit zu bestimmen. Er prüft, ob ein Objekt vom gleichen Typ oder von einem Subtyp des Zieltyps ist. (Zu dieser Klassenhierarchie später mehr!) Das ist dasselbe wie die Frage, ob das Objekt einer Variablen des Zieltyps zugewiesen werden kann. Der Zieltyp kann eine Klasse, eine Schnittstelle oder ein Array sein. instanceof gibt einen boolean Wert zurück, der angibt, ob das Objekt dem Typ entspricht. Probieren wir es in jshell aus:

jshell> boolean b
b ==> false

jshell> String str = "something"
str ==> "something"

jshell> b = (str instanceof String)
b ==> true

jshell> b = (str instanceof Object)
b ==> true

jshell> b = (str instanceof Date)
|  Error:
|  incompatible types: java.lang.String cannot be converted to java.util.Date
|  b = (str instanceof Date)
|       ^-^

Beachte, dass der letzte instanceof Test einen Fehler zurückgibt. Mit seinem ausgeprägten Sinn für Typen kann Java oft unmögliche Kombinationen zur Kompilierzeit erkennen. Ähnlich wie bei unerreichbarem Code lässt dich der Compiler nicht weiterarbeiten, bis du das Problem behoben hast.

Der instanceof Operator meldet auch korrekt, ob das Objekt vom Typ eines Arrays ist:

    if (myVariable instanceof byte[]) {
      // now we're sure myVariable is an array of bytes
      // go ahead with your array work here...
    }

Es ist auch wichtig zu wissen, dass der Wert null nicht als Instanz einer Klasse angesehen wird. Der folgende Test gibt false zurück, unabhängig davon, welchen Typ du der Variablen gibst:

jshell> String s = null
s ==> null

jshell> Date d = null
d ==> null

jshell> s instanceof String
$7 ==> false

jshell> d instanceof Date
$8 ==> false

jshell> d instanceof String
|  Error:
|  incompatible types: java.util.Date cannot be converted to java.lang.String
|  d instanceof String
|  ^

null ist also nie eine "Instanz" einer Klasse, aber Java verfolgt trotzdem die Typen deiner Variablen und lässt dich nicht zwischen inkompatiblen Typen testen (oder casten).

Arrays

Ein Array ist ein spezieller Objekttyp, der eine geordnete Sammlung von Elementen enthalten kann. Der Typ der Elemente des Arrays wird als Basistyp des Arrays bezeichnet; die Anzahl der darin enthaltenen Elemente ist ein festes Attribut, das als Länge bezeichnet wird. Java unterstützt Arrays aller primitiven Typen und auch Referenztypen. Um ein Array mit dem Basistyp byte zu erstellen, kannst du zum Beispiel den Typ byte[] verwenden. Auf ähnliche Weise kannst du mit String[] ein Array mit dem Basistyp String erstellen.

Wenn du schon einmal in C oder C++ programmiert hast, sollte dir die grundlegende Syntax von Java-Arrays bekannt vorkommen. Du erstellst ein Array mit einer bestimmten Länge und greifst auf die Elemente mit dem Indexoperator [] zu. Im Gegensatz zu diesen Sprachen sind Arrays in Java jedoch echte, erstklassige Objekte. Ein Array ist eine Instanz einer speziellen Java array Klasse und hat einen entsprechenden Typ im Typsystem. Das bedeutet, dass du, um ein Array zu verwenden, wie bei jedem anderen Objekt auch, zuerst eine Variable des entsprechenden Typs deklarierst und dann den new Operator verwendest, um eine Instanz davon zu erstellen.

Array Objekte unterscheiden sich von anderen Objekten in Java in dreierlei Hinsicht:

  • Java erstellt implizit einen speziellen Array Klassentyp für uns, wenn wir einen neuen Typ von Array deklarieren. Es ist nicht unbedingt notwendig, diesen Prozess zu kennen, um Arrays zu verwenden, aber er wird später helfen, ihre Struktur und ihre Beziehung zu anderen Objekten in Java zu verstehen.

  • In Java können wir den [] Operator verwenden, um auf Array-Elemente zuzugreifen und sie zuzuweisen, so dass Arrays so aussehen, wie es viele erfahrene Programmierer erwarten. Wir könnten unsere eigenen Klassen implementieren, die sich wie Arrays verhalten, aber dann müssten wir uns mit Methoden wie get() und set() begnügen, anstatt die spezielle Notation [] zu verwenden.

  • Java bietet eine entsprechende Sonderform des new Operators, mit der wir eine Instanz eines Arrays mit einer bestimmten Länge mit der [] Notation konstruieren oder es direkt aus einer strukturierten Liste von Werten initialisieren können.

Arrays machen es einfach, mit zusammenhängenden Informationen zu arbeiten, z. B. mit den Textzeilen in einer Datei oder den Wörtern in einer dieser Zeilen. Wir verwenden sie häufig in Beispielen in diesem Buch; du wirst in diesem und den folgenden Kapiteln viele Beispiele für die Erstellung und Bearbeitung von Arrays mit der Notation [] sehen.

Array-Typen

Eine Array-Variable wird durch einen Basistyp, gefolgt von einer leeren Klammer, [], bezeichnet. Alternativ akzeptiert Java auch eine Deklaration im C-Stil, bei der die Klammern nach dem Array-Namen gesetzt werden.

Die folgenden Erklärungen sind gleichwertig:

    int[] arrayOfInts;   // preferred
    int [] arrayOfInts;  // spacing is optional
    int arrayOfInts[];   // C-style, allowed

In jedem Fall deklarieren wir arrayOfInts als ein Array mit ganzen Zahlen. Die Größe des Arrays spielt noch keine Rolle, da wir nur eine Variable vom Typ Array deklarieren. Wir haben noch keine Instanz der Klasse array oder die zugehörige Speicherung erstellt. Es ist nicht einmal möglich, die Länge eines Arrays anzugeben, wenn eine Variable vom Typ Array deklariert wird. Die Größe hängt ausschließlich von dem Array-Objekt selbst ab, nicht von der Referenz auf es.

Arrays von Referenztypen können auf die gleiche Weise erstellt werden:

    String[] someStrings;
    JLabel someLabels[];

Array-Erstellung und -Initialisierung

Du verwendest den new Operator, um eine Instanz eines Arrays zu erstellen. Nach dem new Operator geben wir den Basistyp des Arrays und seine Länge mit einem eingeklammerten ganzzahligen Ausdruck an. Mit dieser Syntax können wir Array-Instanzen mit tatsächlicher Speicherung für unsere kürzlich deklarierten Variablen erstellen. Da Ausdrücke erlaubt sind, können wir innerhalb der Klammern sogar ein wenig rechnen:

    int number = 10;
    arrayOfInts = new int[42];
    someStrings = new String[ number + 2 ];

Wir können auch die Schritte der Deklaration und Zuweisung des Arrays kombinieren:

    double[] someNumbers = new double[20];
    Component[] widgets = new Component[12];

Array-Indizes beginnen mit Null. So hat das erste Element von someNumbers[] den Index 0 und das letzte Element den Index 19. Nach der Erstellung werden die Array-Elemente selbst mit den Standardwerten für ihren Typ initialisiert. Für numerische Typen bedeutet das, dass die Elemente anfangs Null sind:

    int[] grades = new int[30];
    // first element grades[0] == 0
    // ...
    // last element grades[19] == 0

Die Elemente eines Arrays von Objekten sind Referenzen auf die Objekte - genau wie einzelne Variablen, auf die sie zeigen -, aber sie enthalten keine Instanzen der Objekte. Der Standardwert eines jeden Elements ist daher null, bis wir Instanzen der entsprechenden Objekte zuweisen:

    String names[] = new String[42];
    // names[0] == null
    // names[1] == null
    // ...

Das ist ein wichtiger Unterschied, der für Verwirrung sorgen kann. In vielen anderen Sprachen ist die Erstellung eines Arrays gleichbedeutend mit der Zuweisung von Speicherplatz für seine Elemente. In Java enthält ein neu zugewiesenes Array von Objekten eigentlich nur Referenzvariablen, jede mit dem Wert null.11 Das heißt aber nicht, dass ein leeres Array keinen Speicherplatz benötigt; der Speicher wird benötigt, um diese Referenzen (die leeren "Slots" im Array) zu speichern. Abbildung 4-3 zeigt das Array names aus dem vorherigen Beispiel.

ljv6 0403
Abbildung 4-3. Ein Java-Array

Wir bauen unsere names Variable als ein Array von Strings auf (String[]). Dieses spezielle String[] Objekt enthält vier Variablen vom Typ String. Den ersten drei Array-Elementen haben wir String Objekte zugewiesen. Das vierte hat den Standardwert null.

Java unterstützt das Konstrukt der geschweiften Klammern im C-Stil {}, um ein Array zu erstellen und seine Elemente zu initialisieren:

jshell> int[] primes = { 2, 3, 5, 7, 7+4 };
primes ==> int[5] { 2, 3, 5, 7, 11 }

jshell> primes[2]
$12 ==> 5

jshell> primes[4]
$13 ==> 11

Ein Array-Objekt des richtigen Typs und der richtigen Länge wird implizit erstellt, und die Werte der kommagetrennten Liste von Ausdrücken werden seinen Elementen zugewiesen. Beachte, dass wir hier weder das Schlüsselwort new noch den Array-Typ verwendet haben. Java schließt aus der Zuweisung auf die Verwendung von new.

Wir können die {} Syntax auch mit einem Array von Objekten verwenden. In diesem Fall muss jeder Ausdruck ein Objekt auswerten, das einer Variablen vom Basistyp des Arrays oder dem Wert null zugewiesen werden kann. Hier sind einige Beispiele:

jshell> String[] verbs = { "run", "jump", "hide" }
verbs ==> String[3] { "run", "jump", "hide" }

jshell> import javax.swing.JLabel

jshell> JLabel yesLabel = new JLabel("Yes")
yesLabel ==> javax.swing.JLabel...

jshell> JLabel noLabel = new JLabel("No")
noLabel ==> javax.swing.JLabel...

jshell> JLabel[] choices={ yesLabel, noLabel,
   ...> new JLabel("Maybe") }
choices ==> JLabel[3] { javax.swing.JLabel ... ition=CENTER] }

jshell> Object[] anything = { "run", yesLabel, new Date() }
anything ==> Object[3] { "run", javax.swing.JLabe ... 2023 }

Die folgenden Deklarations- und Initialisierungsanweisungen sind gleichwertig:

    JLabel[] threeLabels = new JLabel[3];
    JLabel[] threeLabels = { null, null, null };

Das erste Beispiel ist natürlich besser, wenn du eine große Anzahl von Objekten speichern musst. Die meisten Programmierer verwenden die geschweifte Klammer nur dann, wenn sie echte Objekte haben, die sie im Array speichern wollen.

Arrays verwenden

Die Größe eines Array-Objekts ist in der öffentlichen Variable length verfügbar:

jshell> char[] alphabet = new char[26]
alphabet ==> char[26] { '\000', '\000' ... , '\000' }

jshell> String[] musketeers = { "one", "two", "three" }
musketeers ==> String[3] { "one", "two", "three" }

jshell> alphabet.length
$24 ==> 26

jshell> musketeers.length
$25 ==> 3

length ist das einzige zugängliche Feld eines Arrays; es ist eine Variable, keine Methode wie in vielen anderen Sprachen. Glücklicherweise weist dich der Compiler darauf hin, wenn du versehentlich Klammern wie alphabet.length() verwendest, was jeder hin und wieder tut.

Der Array-Zugriff in Java ist genau wie der Array-Zugriff in vielen anderen Sprachen: Du greifst auf ein Element zu, indem du einen ganzzahligen Ausdruck zwischen Klammern hinter den Namen des Arrays setzt. Diese Syntax funktioniert sowohl für den Zugriff auf einzelne, vorhandene Elemente als auch für die Zuweisung neuer Elemente. So können wir unseren zweiten Musketier bekommen:

// remember the first index is 0!
jshell> System.out.println(musketeers[1])
two

Das folgende Beispiel erstellt ein Array von JButton Objekten mit dem Namen keyPad. Dann füllt es das Array mit Schaltflächen, wobei es unsere eckigen Klammern und die Schleifenvariable als Index verwendet:

    JButton[] keyPad = new JButton[10];
    for (int i=0; i < keyPad.length; i++)
      keyPad[i] = new JButton("Button " + i);

Erinnere dich daran, dass wir die erweiterte for Schleife auch verwenden können, um über Array-Werte zu iterieren. Hier verwenden wir sie, um alle Werte auszudrucken, die wir gerade zugewiesen haben:

    for (JButton b : keyPad)
      System.out.println(b);

Der Versuch, auf ein Element zuzugreifen, das außerhalb des Bereichs des Arrays liegt, erzeugt eine ArrayIndexOutOfBoundsException. Dies ist eine Art von RuntimeException, so dass du es entweder selbst abfangen und behandeln kannst, wenn du es wirklich erwartest, oder es ignorieren kannst, wie wir in Kapitel 6 besprechen werden. Hier ist ein Vorgeschmack auf die try/catch Syntax, die Java verwendet, um solch potenziell problematischen Code zu verpacken:

    String [] states = new String [50];

    try {
      states[0] = "Alabama";
      states[1] = "Alaska";
      // 48 more...
      states[50] = "McDonald's Land";  // Error: array out of bounds
    } catch (ArrayIndexOutOfBoundsException err) {
      System.out.println("Handled error: " + err.getMessage());
    }

Es ist eine gängige Aufgabe, einen Bereich von Elementen aus einem Array in ein anderes zu kopieren. Eine Möglichkeit, Arrays zu kopieren, ist die Low-Level-Methode arraycopy() der Klasse System:

    System.arraycopy(source, sourceStart, destination, destStart, length);

Im folgenden Beispiel wird die Größe des Arrays names aus einem früheren Beispiel verdoppelt:

    String[] tmpVar = new String [ 2 * names.length ];
    System.arraycopy(names, 0, tmpVar, 0, names.length);
    names = tmpVar;

Hier weisen wir eine temporäre Variable, tmpVar, als neues Array zu, das doppelt so groß ist wie names. Wir verwenden arraycopy(), um die Elemente von names in das neue Array zu kopieren. Zum Schluss weisen wir names ein temporäres Array zu. Wenn nach der Zuweisung des neuen Arrays an names keine Referenzen auf das alte Array mit den Namen mehr vorhanden sind, wird das alte Array beim nächsten Durchlauf in den Müll geworfen.

Vielleicht ist eine einfachere Möglichkeit, die gleiche Aufgabe zu erfüllen, die Verwendung der copyOf() oder copyOfRange() Methoden der Klasse java.util.Arrays zu verwenden:

jshell> byte[] bar = new byte[] { 1, 2, 3, 4, 5 }
bar ==> byte[5] { 1, 2, 3, 4, 5 }

jshell> byte[] barCopy = Arrays.copyOf(bar, bar.length)
barCopy ==> byte[5] { 1, 2, 3, 4, 5 }

jshell> byte[] expanded = Arrays.copyOf(bar, bar.length+2)
expanded ==> byte[7] { 1, 2, 3, 4, 5, 0, 0 }

jshell> byte[] firstThree = Arrays.copyOfRange(bar, 0, 3)
firstThree ==> byte[3] { 1, 2, 3 }

jshell> byte[] lastThree = Arrays.copyOfRange(bar, 2, bar.length)
lastThree ==> byte[3] { 3, 4, 5 }

jshell> byte[] plusTwo = Arrays.copyOfRange(bar, 2, bar.length+2)
plusTwo ==> byte[5] { 3, 4, 5, 0, 0 }

Die Methode copyOf() nimmt das ursprüngliche Array und eine Ziellänge an. Wenn die Ziellänge größer ist als die ursprüngliche Array-Länge, wird das neue Array auf die gewünschte Länge aufgefüllt (mit Nullen oder Nullen). Die Methode copyOfRange() nimmt einen Startindex (einschließlich) und einen Endindex (ausschließlich) sowie eine gewünschte Länge an, die gegebenenfalls ebenfalls aufgefüllt wird .

Anonyme Arrays

Oft ist es praktisch, Wegwerf-Arrays zu erstellen: Arrays, die nur an einer Stelle verwendet werden und auf die nirgendwo anders verwiesen wird. Solche Arrays brauchen keine Namen, weil du dich in diesem Kontext nie wieder auf sie beziehst. Du möchtest zum Beispiel eine Sammlung von Objekten erstellen, die du als Argument an eine Methode weitergibst. Es ist einfach genug, ein normales, benanntes Array zu erstellen, aber wenn du nicht wirklich mit dem Array arbeitest (wenn du das Array nur als Halter für eine Sammlung verwendest), solltest du diesen temporären Halter nicht benennen müssen. Java macht es einfach, "anonyme" (unbenannte) Arrays zu erstellen.

Angenommen, du musst eine Methode namens setPets() aufrufen, die ein Array von Animal Objekten als Argumente benötigt. Vorausgesetzt, dass Cat und Dog Unterklassen von Animal sind, kannst du setPets() mit einem anonymen Array folgendermaßen aufrufen:

    Dog pete = new Dog ("golden");
    Dog mj = new Dog ("black-and-white");
    Cat stash = new Cat ("orange");
    setPets (new Animal[] { pete, mj, stash });

Die Syntax von sieht ähnlich aus wie die Initialisierung eines Arrays in einer Variablendeklaration. Wir legen implizit die Größe des Arrays fest und füllen seine Elemente mit Hilfe der geschweiften Klammern aus. Da es sich jedoch nicht um eine Variablendeklaration handelt, müssen wir explizit den new Operator und den Array-Typ verwenden, um das Array-Objekt zu erstellen.

Mehrdimensionale Arrays

Java unterstützt mehrdimensionale Arrays in Form von Arrays aus anderen Arrays. Du erstellst ein mehrdimensionales Array mit einer C-ähnlichen Syntax, indem du mehrere Klammerpaare verwendest, eines für jede Dimension. Du verwendest diese Syntax auch, um auf Elemente an verschiedenen Positionen innerhalb des Arrays zuzugreifen. Hier ist ein Beispiel für ein mehrdimensionales Array, das ein hypothetisches Schachbrett darstellt:

    ChessPiece[][] chessBoard;
    chessBoard = new ChessPiece[8][8];
    chessBoard[0][0] = new ChessPiece.Rook;
    chessBoard[1][0] = new ChessPiece.Pawn;
    chessBoard[0][1] = new ChessPiece.Knight;
    // setup the remaining pieces

Abbildung 4-4 veranschaulicht das Array von Arrays, das wir erstellen.

ljv6 0404
Abbildung 4-4. Ein Array aus Arrays von Schachfiguren

Hier wird chessBoard als Variable vom Typ ChessPiece[][] (ein Array von ChessPiece Arrays) deklariert. Diese Deklaration erzeugt implizit auch den Typ ChessPiece[]. Das Beispiel veranschaulicht die spezielle Form des new Operators, der zur Erstellung eines mehrdimensionalen Arrays verwendet wird. Er erstellt ein Array aus ChessPiece[] Objekten und macht dann jedes Element wiederum zu einem Array aus ChessPiece Objekten. Dann indexieren wir chessBoard, um Werte für bestimmte ChessPiece Elemente anzugeben.

Natürlich kannst du auch Arrays mit mehr als zwei Dimensionen erstellen. Hier ist ein etwas unpraktisches Beispiel:

    Color [][][] rgb = new Color [256][256][256];
    rgb[0][0][0] = Color.BLACK;
    rgb[255][255][0] = Color.YELLOW;
    rgb[128][128][128] = Color.GRAY;
    // Only 16 million to go!

Wir können einen Teilindex eines mehrdimensionalen Arrays angeben, um ein Unterarray von Objekten des Typs Array mit weniger Dimensionen zu erhalten. In unserem Beispiel ist die Variable chessBoard vom Typ ChessPiece[][]. Der Ausdruck chessBoard[0] ist gültig und bezieht sich auf das erste Element von chessBoard, das in Java vom Typ ChessPiece[] ist. Wir können zum Beispiel unser Schachbrett Zeile für Zeile auffüllen:

    ChessPiece[] homeRow =  {
      new ChessPiece("Rook"), new ChessPiece("Knight"),
      new ChessPiece("Bishop"), new ChessPiece("King"),
      new ChessPiece("Queen"), new ChessPiece("Bishop"),
      new ChessPiece("Knight"), new ChessPiece("Rook")
    };

    chessBoard[0] = homeRow;

Wir müssen die Größen der Dimensionen eines mehrdimensionalen Arrays nicht unbedingt mit einer einzigen new Operation angeben. Die Syntax des new Operators erlaubt es uns, die Größen einiger Dimensionen nicht anzugeben. Mindestens die Größe der ersten Dimension (die wichtigste Dimension des Arrays) muss angegeben werden, aber die Größen der nachfolgenden, weniger wichtigen Dimensionen können unbestimmt gelassen werden. Wir können später geeignete Werte für den Array-Typ zuweisen.

Wir können eine vereinfachte Tafel mit booleschen Werten erstellen, die hypothetisch den Belegungsstatus eines bestimmten Feldes mit dieser Technik verfolgen könnte:

    boolean [][] checkerBoard = new boolean [8][];

Hier wird checkerBoard deklariert und erstellt, aber seine Elemente, die acht boolean[] Objekte der nächsten Ebene, bleiben leer. Bei dieser Art der Initialisierung ist checkerBoard[0] so lange null, bis wir explizit ein Array erstellen und es zuweisen, wie folgt:

    checkerBoard[0] = new boolean [8];
    checkerBoard[1] = new boolean [8];
    // ...
    checkerBoard[7] = new boolean [8];

Der Code der beiden vorherigen Schnipsel ist äquivalent zu:

    boolean [][] checkerBoard = new boolean [8][8];

Ein Grund, warum du die Dimensionen eines Arrays nicht angeben solltest, ist, dass du Arrays, die uns gegeben werden, später speichern kannst.

Da die Länge des Arrays nicht Teil seines Typs ist, müssen die Arrays im Schachbrettmuster nicht notwendigerweise die gleiche Länge haben, d.h. mehrdimensionale Arrays müssen nicht rechteckig sein. Betrachte das "dreieckige" Array mit ganzen Zahlen in Abbildung 4-5, bei dem Zeile eins eine Spalte hat, Zeile zwei zwei Spalten und so weiter.

ljv6 0405
Abbildung 4-5. Eine dreieckige Anordnung von Arrays

Die Übungen am Ende des Kapitels geben dir die Möglichkeit, dieses Array selbst einzurichten und zu initialisieren!

Typen und Klassen und Arrays, oh je!

Java hat eine Vielzahl von Typen zum Speichern von Informationen, von denen jeder seine eigene Art hat, wörtliche Bits dieser Informationen darzustellen. Im Laufe der Zeit wirst du dich mit ints, doubles, chars und Strings vertraut machen. Aber überstürze nichts - diese grundlegenden Bausteine sind genau das, was du mit jshell erforschen sollst. Es lohnt sich immer, zu überprüfen, ob du weißt, was eine Variable speichern kann. Vor allem bei Arrays lohnt es sich, ein wenig zu experimentieren. Du kannst die verschiedenen Deklarationstechniken ausprobieren und überprüfen, ob du weißt, wie du auf die einzelnen Elemente in ein- und mehrdimensionalen Strukturen zugreifen kannst.

Du kannst auch mit einfachen Flow-of-Control-Anweisungen in jshell spielen, z. B. mit unseren if Verzweigungen und while Schleifenanweisungen. Es erfordert ein wenig Geduld, gelegentlich einen mehrzeiligen Snippet einzugeben, aber wir können gar nicht genug betonen, wie nützlich Spiel und Übung sind, wenn du immer mehr Details von Java in dein Gehirn einbaust. Programmiersprachen sind sicherlich nicht so komplex wie menschliche Sprachen, aber sie haben dennoch viele Ähnlichkeiten. Du kannst dir Java genauso gut aneignen wie Englisch (oder die Sprache, in der du dieses Buch liest, wenn du eine Übersetzung hast). Du wirst ein Gefühl dafür bekommen, was der Code tun soll, auch wenn du die Einzelheiten nicht sofort verstehst.

Einige Teile von Java, wie arrays, sind definitiv voller Besonderheiten. Wir haben bereits festgestellt, dass Arrays Instanzen spezieller Array-Klassen in der Java-Sprache sind. Wenn Arrays Klassen haben, wo passen sie dann in die Klassenhierarchie und wie sind sie miteinander verbunden? Das sind gute Fragen, aber bevor wir sie beantworten können, müssen wir mehr über die objektorientierten Aspekte von Java sprechen. Das ist das Thema von Kapitel 5. Für den Moment kannst du davon ausgehen, dass Arrays in die Klassenhierarchie passen.

Fragen überprüfen

  1. Welches Textkodierungsformat wird von Java standardmäßig in kompilierten Klassen verwendet?

  2. Welche Zeichen werden verwendet, um einen mehrzeiligen Kommentar einzuschließen? Können diese Kommentare verschachtelt werden?

  3. Welche Schleifenkonstrukte unterstützt Java?

  4. Was passiert in einer Kette von if/else if Tests, wenn mehrere Bedingungen wahr sind?

  5. Wenn du die Gesamtkapitalisierung des US-Aktienmarktes (etwa 31 Billionen Dollar zum Ende des Finanzjahres 2022) in ganzen Dollars speichern möchtest, welchen primitiven Datentyp könntest du verwenden?

  6. Welchen Wert hat der Ausdruck 18 - 7 * 2?

  7. Wie würdest du ein Array erstellen, das die Namen der Wochentage enthält?

Code-Übungen

Für deine Programmierpraxis bauen wir auf zwei der Beispiele aus diesem Kapitel auf:

  1. Implementiere den Euklid'schen GCD-Algorithmus als vollständige Klasse mit dem Namen Euclid. Erinnere dich an die Grundlagen des Algorithmus:

        int a = 2701;
        int b = 222;
        while (b != 0) {
          if (a > b) {
            a = a - b;
          } else {
            b = b - a;
          }
        }
        System.out.println("GCD is " + a);

    Kannst du dir für deine Ausgabe vorstellen, dass du dem Benutzer neben dem gemeinsamen Nenner auch die Originalwerte von a und b anzeigen kannst? Die ideale Ausgabe würde etwa so aussehen:

    % java Euclid
    The GCD of 2701 and 222 is 37
  2. Versuche, das dreieckige Array aus dem vorherigen Abschnitt in einer einfachen Klasse oder in jshell zu erstellen. Hier ist eine Möglichkeit:

        int[][] triangle = new int[5][];
        for (int i = 0; i < triangle.length; i++) {
          triangle[i] = new int [i + 1];
          for (int j = 0; j < i + 1; j++)
            triangle[i][j] = i + j;
        }

    Erweitere nun diesen Code, um den Inhalt von triangle auf dem Bildschirm auszugeben. Erinnere dich daran, dass du den Wert eines Array-Elements mit derSystem.out.println() Methode drucken kannst:

        System.out.println(triangle[3][1]);

    Deine Ausgabe wird wahrscheinlich eine lange, vertikale Zahlenreihe wie diese sein:

    0
    1
    2
    2
    3
    4
    3
    4
    5
    6
    4
    5
    6
    7
    8

Fortgeschrittene Übungen

  1. Wenn du eine größere Herausforderung suchst, kannst du versuchen, die Ausgabe in einem visuellen Dreieck anzuordnen. Die obige Anweisung druckt ein Element allein in eine Zeile. Das eingebaute System.out Objekt hat eine weitere Ausgabemethode: print(). Diese Methode gibt keinen Zeilenumbruch aus, nachdem sie das übergebene Argument ausgegeben hat. Du kannst mehrere Aufrufe von System.out.print() miteinander verknüpfen, um eine Ausgabezeile zu erzeugen:

        System.out.print("Hello");
        System.out.print(" ");
        System.out.print("triangle!");
        System.out.println(); // We do want to complete the line
        // Output:
        // Hello triangle!

    Deine endgültige Ausgabe sollte ungefähr so aussehen:

    % java Triangle
    0
    1 2
    2 3 4
    3 4 5 6
    4 5 6 7 8

1 Weitere Informationen findest du auf der offiziellen Unicode-Website. Seltsamerweise ist eine der Schriften, die als "veraltet und archaisch" aufgelistet sind und derzeit nicht vom Unicode-Standard unterstützt werden, Javanisch - eine historische Sprache der Menschen auf der indonesischen Insel Java.

2 Die Verwendung eines Kommentars zum "Verstecken" von Code kann sicherer sein, als den Code einfach zu löschen. Wenn du den Code wiederhaben willst, nimmst du einfach die Begrenzungszeichen des Kommentars heraus.

3 Java verwendet eine Technik namens "Zweierkomplement", um ganze Zahlen zu speichern. Diese Technik verwendet ein Bit am Anfang der Zahl, um festzustellen, ob es sich um einen positiven oder negativen Wert handelt. Eine Besonderheit dieser Technik ist, dass der negative Bereich immer um eins größer ist.

4 Der vergleichbare Code in C++ würde lauten:
Car& myCar = *(new Car());
Car& anotherCar = myCar;

5 Wir sagen "populär", weil viele Programmiersprachen die gleiche bedingte Anweisung haben.

6 Das Wort "boolesch" stammt von dem englischen Mathematiker George Boole, der den Grundstein für die logische Analyse gelegt hat. Das Wort wird zu Recht groß geschrieben, aber viele Computersprachen haben einen "booleschen" Typ, der klein geschrieben wird - auch Java. Du wirst online immer beide Varianten sehen.

7 Wir werden hier nicht auf andere Formen eingehen, aber Java unterstützt auch die Verwendung von Aufzählungstypen und Klassenabgleich in switch Anweisungen.

8 Es gilt immer noch als schlechte Manier, zu benannten Etiketten zu springen.

9 Vielleicht erinnerst du dich an den Begriff " Vorrang" - und anseine nette Eselsbrücke "Bitte entschuldige meine liebe Tante Sally" - aus der Algebra in der Schule. Java wertet zuerst die (P)arenthesen aus, dann alle (E)xponenten, dann die (M)ultiplikation und (D)ivision und schließlich die (A)ddition und (S)ubtraktion.

10 Computer stellen ganze Zahlen auf eine von zwei Arten dar: vorzeichenbehaftete ganze Zahlen, die negative Zahlen zulassen, und vorzeichenlose, die dies nicht tun. Ein Byte mit Vorzeichen hat z.B. den Bereich -128...127. Ein Byte ohne Vorzeichen hat den Bereich 0...255.

11 Das Analogon in C oder C++ ist ein Array aus Zeigern. Allerdings sind Zeiger in C oder C++ selbst Zwei-, Vier- oder Acht-Byte-Werte. Die Zuweisung eines Arrays von Zeigern bedeutet in Wirklichkeit die Zuweisung von Speicherplatz für eine bestimmte Anzahl dieser Zeigerwerte. Ein Array von Referenzen ist vom Konzept her ähnlich, obwohl Referenzen selbst keine Objekte sind. Wir können Referenzen oder Teile von Referenzen nur durch Zuweisung manipulieren, und ihre Speicheranforderungen (oder deren Fehlen) sind nicht Teil der Java-Hochsprachen-Spezifikation.

Get Java lernen, 6. Auflage now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.