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).
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.
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.
Tag | Beschreibung | Gilt für |
---|---|---|
|
Zugehöriger Klassenname |
Klasse, Methode oder Variable |
|
Inhalt des Quellcodes |
Klasse, Methode oder Variable |
|
Assoziierte URL |
Klasse, Methode oder Variable |
|
Name des Autors |
Klasse |
|
Version string |
Klasse |
|
Parametername und Beschreibung |
Methode |
|
Beschreibung des Rückgabewerts |
Methode |
|
Name und Beschreibung der Ausnahme |
Methode |
|
Erklärt einen Artikel für veraltet |
Klasse, Methode oder Variable |
|
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 String
s 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.
Typ | Definition | Ungefähre Reichweite oder Genauigkeit |
---|---|---|
|
Logischer Wert |
|
|
16-Bit, Unicode-Zeichen |
64K Zeichen |
|
8-Bit, vorzeichenbehaftete Ganzzahl |
-128 bis 127 |
|
16-Bit, vorzeichenbehaftete Ganzzahl |
-32.768 bis 32.767 |
|
32-Bit, vorzeichenbehaftete Ganzzahl |
-2,1e9 bis 2,1e9 |
|
64-Bit, vorzeichenbehaftete Ganzzahl |
-9,2e18 bis 9,2e18 |
|
32-Bit, IEEE 754, Fließkommazahl |
6-7 signifikante Nachkommastellen |
|
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
=
askUserForEmail
();
}
while
(
.
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
.
(
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
.
(
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
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 |
|
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 |
|
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 wieget()
undset()
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.
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 copy
OfRange()
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.
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.
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 int
s, double
s, char
s und String
s 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
-
Welches Textkodierungsformat wird von Java standardmäßig in kompilierten Klassen verwendet?
-
Welche Zeichen werden verwendet, um einen mehrzeiligen Kommentar einzuschließen? Können diese Kommentare verschachtelt werden?
-
Welche Schleifenkonstrukte unterstützt Java?
-
Was passiert in einer Kette von
if/else if
Tests, wenn mehrere Bedingungen wahr sind? -
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?
-
Welchen Wert hat der Ausdruck
18 - 7 * 2
? -
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:
-
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
undb
anzeigen kannst? Die ideale Ausgabe würde etwa so aussehen:% java Euclid The GCD of 2701 and 222 is 37
-
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
-
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 vonSystem.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.