Logo weiterlesen.de
Objekte Tools Essentials Band 1: Objekte

Inhalt

ÜBERSICHT

Fahrplan und Rapid-Coding

Das erste Anliegen - Verständliche Texte

Konventionen und Verabredungen

DAS SOFTWARE-UNIVERSUM

1. Das Buch der unbrauchbaren Softwareteile

2. Ein Anfang – aber nicht mehr

EINE KLEINE JAVA-RUNDREISE – TEIL 1

1. Intro

2. 90 Prozent - Ein Plan und ein Stück Code - Was ein Entwickler wissen muss

3. Methoden

3.1. Die Signatur von Methoden

3.2. Werden Argumente und Returnwerte kopiert?

3.3. Returntyp und return Statement

3.4. Aufteilung von Code in Methoden

3.5. Variabel lange Argumentlisten

3.6. Argumentprüfung

4. Lokale Variable, Blöcke, Scope und final

5. Die Grundregel für zusammengesetzte Ausdrücke

6. Weitere elementare Regeln und Begriffe

7. Zuweisung

8. Kombinierte Zuweisungen

9. Inkrement und Dekrement

10. Stringverknüpfung (+)

11. Bedingung (if/else)

12. Verzweigung (switch)

13. Der new-Operator

14. Gleichheits- und Identitäts-Operator

15. Logische Operatoren, Vergleiche, Verknüpfung von Bedingungen

16. Der Conditional-Operator

17. Zusammenfassung der Operatoren

17.1. Auswertungsreihenfolge

17.2. Auflistung nach Priorität

17.3. Bit-Operatoren, Bit-Komplement-Operator und Bit-Shift-Operatoren

18. Iterationen

18.1. Übersicht

18.2. Bedingungen und Zähler

18.3. Ablaufschema

18.4. break und continue

18.5. Iteration und robuste Programmierung

18.6. for/in

19. Elementare Programmiertechnik anhand eines Beispiels

19.1. Die Aufgabe

19.2. Start ohne Skrupel

19.3. Übersichtlicher Code

19.4. Zerlegung in Methoden

19.5. Welche Anforderungen bestehen und wer überprüft sie?

19.6. Aufsammeln der Zahlen

19.7. Abspaltung der Benutzereingabe

20. Objekte*

20.1. Bausteine

20.2. Methodenaufrufe

20.3. Anatomie von Klassen und Objekten

20.4. Statische Elemente

20.5. Referenzen, Nirvana und this

21. Typen und Interfaces*

21.1. Ein leichtgewichtiges Tool

21.2. Ableitung und Vererbung

21.3. Das Verbergen und das Abgreifen von Information

21.4. Upcast und Downcast

21.5. Typen

21.6. Interfaces erweitern Interfaces

22. Allgemeine Vererbung*

23. Packages, Import und private Typen

23.1. Packages

23.2. Nicht-public Typen

23.3. Package-Sichtbarkeit

23.4. Import-Statements

23.5. Static Imports

24. Module*

25. Exceptions*

26. Elementare Typen und ihre Verpackung in Objekten – Wrappers

27. Zahlen und Zufallszahlen

27.1. Literale und Arithmetik

27.2. Große Zahlen

27.3. Der Zahlengenerator Random

28. Konstanten, Konstanz und Unveränderbarkeit

29. Enums*

30. Threads*

31. Datum und Uhrzeit

31.1. Zeitstempel und Zeitdauer

31.2. Zusammensetzen, Beschränken und Kopieren

31.3. Tageszeit - LocalTime

31.4. Datum – LocalDate

31.5. Zeitzonen – ZonedDateTime

31.6. Alt - Date und Calendar

31.7. Formatieren von Datums- und Zeitangaben

31.8. Parsen

32. Performancemessung

33. Strings*

34. Arrays

34.1. Basics

34.2. Initialisierung, elementare Elementtypen und Referenzelemente

34.3. Elementtyp, Vergleichbarkeit und das Tool Arrays

34.4. Mehrdimensionale Arrays

35. Collections*

36. Generische Klassen*

37. Generische Methoden*

38. Maps*

39. Essentials

40. Guidelines

RAPID-CODING EINSCHUB 1

1. Was ist Rapid-Coding

2. Der Zeitfaktor

3. Die Aufgabe

4. Zerlegung und Verteilung

5. Alt-Bekanntes

5.1. Iterationen

5.2. Argumentprüfungen

5.3. Exceptions werfen

5.4. Konstanten vereinbaren

5.5. Es gibt sehr viel mehr

6. Ein kleiner Schritt

7. Ein großer Schritt

OBJEKTE

1. Einige kurze Fakten zu Objekten, die das Verständnis erleichtern

1.1. Man kann Java auch ohne Objekte nutzen

1.2. Man kann Java mit Objekten und ohne Objektorientierung betreiben

1.3. Instanzen und Objekte sind dasselbe

1.4. Wir konstruieren keine Objekte, sondern Klassen

1.5. Auch mit Objekten kann man Mist bauen

1.6. Klassen sind Typen

1.7. ‚Abstrakt’ bedeutet ‚nicht implementiert’

2. Fange einfach irgendwo an

2.1. Straight-forward

2.2. Einsatz der üblichen Tools

2.3. Behandlung der Eingabefehler

2.4. Kommentare

2.5. Der gesamte Code

2.6. Das prozedurale Denken

2.7. Zäher Code

3. Die Klarheit der Objekte

3.1. Spielerischer Einstieg

3.2. Details der Komponenten

3.3. Resümee

3.4. Die Lottostatistik

4. Die Basics

4.1. Der Blick auf das Ganze

4.2. Instanzdaten und Kapselung

4.3. Wege, um die Kapselung zu brechen

4.4. Klassen-Elemente

4.5. Interface-Elemente

4.6. Konstruktoren

4.7. Object

4.8. Probleme mit equals() und hashCode()

5. Implementierung der Lotto-Applikation

5.1. LottoTicket

5.2. Win

5.3. LottoDrawing

5.4. RealLottery

5.5. Zustandsprogrammierung bei RealLottery

5.6. Objekte als robuste Maschinen

6. Statische Elemente

6.1. Syntax und Übersicht

6.2. Baustein ohne Instanzen

6.3. Singleton

6.4. Verwaltung von Instanzen

7. Vererbung

7.1. Cube

7.2. Terminologie

7.3. Konstruktoren

7.4. Schwierigkeiten, die genaues Hinsehen erfordern

7.5. Eine Art von Superclass-Chaining

7.6. Den Typ einer Instanz kann man nicht verbergen

7.7. Overriding

7.8. Der Downcast

7.9. Konsequenzen von später Bindung

7.10. Aufbau der Instanzen

7.11. NationalLottery

7.12. Vererbung von Implementierung bricht abgeleitete Klassen

7.13. Ist ein Math ein Cube?

8. Erzwingen und Verhindern von Overriding

8.1. Abstrakte Methoden und abstrakte Klassen

8.2. final Methoden und Klassen

9. Cloneable

10. Comparable

10.1. Vergleichbarkeit und natürliche Ordnung

10.3. Comparatoren sortieren Elemente mit und ohne natürliche Ordnung

10.4. Comparatoren in der Lotto-Applikation

10.5. Nur einfache Anforderungen an Listener

11. Essentials

12. Guidelines

RAPID-CODING EINSCHUB 2

1. Das Beispiel

2. Das einfache Schema der Klassen

3. Konzept, Typ und Erfassbarkeit

4. Trennung von Typ und Implementierung

MODULE

1. Arbeiten mit und ohne Module

2. Gespräch über Module

3. Benannte Module und Kapselung

4. Freigaben

5. Module und Objekte

6. Im Kreis

7. Essentials

ANONYME KLASSEN UND LAMBDAS

1. Innere Typen

1.1. Intro

1.2. Wechselseitiger Zugriff

1.3. Noch eine neue Syntax: new

1.4. Wie der Compiler den freien wechselseitigen Zugriff sieht

1.5. Übersicht zu inneren Typen

2. Syntax und Einsatz von anonymen Klassen

2.1. Der definierende Typ

2.2. Drei oder vier Schritte zu einer Formel

2.3. Für anonyme Klassen können keine Konstruktoren geschrieben werden

2.4. Code für eine spätere Ausführung

2.5. Klasse oder Methode?

2.6. Fallbeispiel mit einer Defaultmethode

2.7. Adapter als simple Hilfsmittel

3. Essentials zu inneren Typen

4. Lambdas

4.1. Intro

4.2. Von den anonymen Klassen zu den Lambdas

4.3. Die Syntax der Lambdas

4.4. Lambda-Interfaces

4.5. Methodenreferenzen

4.6. Der nächste Schritt: Kombination von Lambdas

5. Intro zu Streams*

5.1. Was ist ein Stream?

5.2. Eine eigenartige Konzeption

5.3. Was enthält ein Stream?

5.4. Als Beispiel - Die Arbeits- und Funktionsweise von reduce()

6. Essentials zu Lambdas

7. Guidelines zu Lambdas

EXCEPTIONS

1. Wirf eine Exception

2. Eine einfache Entscheidung

3. Stack-Traces, Methodenlabyrinthe und Ariadne-Fäden

4. Die methodenübergreifende Struktur von try-catch

5. Der passende Exception-Handler

6. finally

7. Checked- und Unchecked-Exceptions

8. Mehrfach-Handler (Multi-Catch)

9. Wiederauswurf – Rethrow

10. Smarter Compiler

11. Ressourcen-Behandlung

11.2. Aufwendige Ressourcen-Freigabe mit finally

11.3. Die Automatismen von try-with-resources

12. Suppressed Exceptions

13. Selbst verfasste Exceptiontypen

14. Exceptions und Threads

15. UncaughtExceptionHandler

16. Exceptions tatsächlich behandeln

16.1. Die Wiederholung

16.2. Unterscheide Produktivzeit und Entwicklungszeit

16.3. Exceptions zur Entwicklungszeit verlässlich anzeigen

16.4. Die Unterscheidung von Checked- und Unchecked-Exceptions ist unnötig

16.5. Etwas suchen

16.6. Auf Bauteile verzichten

16.7. Jedes Bauteil braucht ein Fehlerkonzept

16.8. Bei allen Methoden Exceptions deklarieren und spezifizieren

17. Essentials

18. Guidelines

TEIL 2 DER JAVA-RUNDREISE – TOOLS, OHNE DIE ES NICHT GEHT

1. Class und Reflection

2. System-Properties

3. Logging

3.1. Der Standard-Logging-Framework

3.2. Austauschbare Implementierungen und Tag-basiertes Logging

3.3. Guidelines zum Logging

4. Intro zu Threads

4.1. Ein Ariadne Faden

4.2. Dem Faden folgen

4.3. Parallele Fäden

4.4. Threads sind leichtgewichtige Prozesse

4.5. Objektorientierung und parallele Abläufe sind zwei verschiedene Welten

4.6. Ausführung in nur einem Thread

4.7. Ausführung in zwei Threads

4.8. Threads erzeugen und starten

4.9. Auf einen Thread warten - join()

4.10. Thread-Zustände

4.11. Blockierte Threads

4.12. Eine kurze Exkursion zum Wait-Set

4.13. Einen Thread beenden

4.14. Dämonen

4.15. Zeitscheiben und kooperatives Verhalten

4.16. Priorität und Id

4.17. Auflistung der Threads

4.18. Timer

4.20. Essentials zu Threads

5. Streams

5.1. Intro

5.2. Streams verwenden

5.3. Einzelne Zeichen und Arrays

5.4. Streams können blockieren

5.5. Eingangs-Streams

5.6. Übersicht der Byte-Streams

5.7. Übersicht der Character-Streams

5.8. Ausgangs-Streams

5.9. Typische Stream-Verwendungen

5.10. Daten-Streams

5.11. Essentials zu Streams

6. Files

6.1. Pfade

6.2. Die Klasse File

6.3. Absolute und relative Pfade

6.4. Path

6.5. Files lesen

6.6. Files schreiben

6.7. Ressource-Pfade

6.8. Ein kleiner Test zu den Ressource-Pfaden

6.9. Standardcode: Files lesen

6.10. Standardcode: Files schreiben

6.11. Directory Iterator und list()

6.12. Mehr über Files: copy(), move(), find() und walk()

6.13. Files und das Visitor-Pattern

6.14. Übersicht der Files-Methoden

6.15. Essentials zu Files

ANHANG

1. Die erste Klasse

2. Java und der JDK

2.1. Sourcecode, Bytecode und Virtuelle Maschine

2.2. Java-Versionen

2.3. Download des JDK

2.4. Installation des JDK

2.5. Die Path-Umgebungsvariable

2.6. Einige Tools

2.7. Entwicklung ohne und mit Entwicklungsumgebung

3. Modulepath und Classpath

3.1. Eine Liste von Orten, um nach referenzierten Typen zu suchen

3.2. Modulepath und Modulesourcepath

3.3. Disassembler

3.4. Vermeintliche und tatsächliche Classpath-Probleme

4. Compiler

4.1. Einfache Aufrufe

4.2. Module

5. Archive

5.1. Übersicht

5.2. Modulare Jar-Files

6. Interpreter

7. Eclipse-Intro

8. Das Richtige tun

INDEX

Abbildungen

Abbildung 1: Java-Arrays und Indizes

Abbildung 2: Einige Oberflächenelemente der Lotto-Applikation

Abbildung 3: Die Idee des Drawing-Bausteins

Abbildung 4: Leistung des Drawing-Bausteins

Abbildung 5: Bausteine und ihr Zusammenwirken

Abbildung 6: Der Typ Gambler

Abbildung 7: Der Typ Person

Abbildung 8: Ein ClassRoom für BirthDayPersons

Abbildung 9: Der Basistyp steht per Konvention oben, der abgeleitete Typ unten

Abbildung 10: Drei Example-Instanzen mit der Instanz ihrer Klasse

Abbildung 11: Die Standard-Log-Handler

Abbildung 12: Logger und Handler sind die Hauptakteure beim Logging

Abbildung 13: Zwei Welten: Character-Rohre und Byte-Rohre

Abbildung 14: Vier unabhängige Grundtypen: InputStream, OutputStream, Reader und Writer

Abbildung 15: Die vier Stream-Grundtypen und zugeordnete Interfaces

Abbildung 16: Filter - Streams werden als Rohre ineinander gesteckt

Abbildung 17: Reader und InputStream

Abbildung 18: Die Byte-Klassen des Stream-Frameworks

Abbildung 19: ByteArrayInputStreams werden zu einer Sequenz von Streams verknüpft

Abbildung 20: Die Character-Klassen des Stream-Frameworks

Abbildung 21: Writer und OutputStream

Abbildung 22: DataInputStream und DataOutputStream

Übersicht

Fahrplan und Rapid-Coding

Gute Konstruktionen basieren auf gutem Coding. Das Anliegen dieses Buches heißt Coding. Wir wollen gute Lesbarkeit, Stabilität, Einfachheit, Effizienz und Schnelligkeit. Wir wollen, dass Code solide gebaut ist und nicht bei kleinsten Änderungen im Umfeld wieder nachgearbeitet werden muss. Wir wollen, dass sein Aufbau einfach zu verstehen ist und seine Strukturen und Prinzipien leicht zu erfassen sind. Und wir wollen einen Ansatz, der uns systematisch und auf Dauer gestattet, Code schnell und sehr effizient zu erstellen. Das Stichwort dafür lautet: Rapid-Coding.

Rapid Coding umfasst eine Reihe von Techniken und Vorgehensweisen, die ein rasches und verlässliches Erstellen von hochwertigem Code ermöglichen. Neben der Grundschnelligkeit der Code-Erstellung sind Lesbarkeit und Verständlichkeit weitere Punkte, die uns interessieren. Die Verteilung von Code auf Methoden gehört zu den ersten und elementarsten Hilfsmitteln für verständliche Formulierungen. Man kann diese und andere robuste Techniken, die zu gutem Coding führen, anhand von simplen Beispielen darstellen. Und man könnte sich lange und mit Vergnügen bei den einfachen Beispielen und ihren Implikationen für Coding-Technik) aufhalten. Man würde viel dabei erfahren, was andern Orts - mit dem Fokus auf bloße Syntax - zu kurz kommt.

Es wartet jedoch mit der Objektorientierung ein Ansatz auf uns, dessen Leistungsfähigkeit die der prozeduralen Programmierung bei Weitem übersteigt. Der Schritt in die objektorientierte Welt ändert unsere Sichtweise auf Software und Coding – und die Änderung ist radikal. Es ergeben sich sowohl für das Denken über Probleme und Modelle als auch bei Konzeptionen und Strukturen ganz andere Möglichkeiten. Objektorientierung und Rapid-Coding sind zudem keine Gegensätze, sondern verschmelzen zu einer wunderbaren Einheit.

Entwickler werden Lambdas und Exceptions mit unterschiedlicher Zuneigung betrachten. Wir haben aber nur bei den Lambdas die Wahl, ob wir sie in unseren Programmen einsetzen. Hat man sich jedoch einmal an sie gewöhnt, wird man sie nicht mehr missen wollen. Bei den Exceptions haben wir diese Wahl nicht. Ohne Fehler-Behandlung läuft kein Java-Programm. Die vermeintlichen Alternativen haben allesamt große Handicaps. Aus diesen Gründen werden beide Techniken - Lambdas und Exceptions – sehr ausführlich behandelt.

Das vorliegende Buch setzt den Fokus auf elementare Programmierung, objektorientierte Programmierung und die Lambdas. Das erste braucht man, weil alles andere darauf aufbaut, das zweite, weil die Objekte Konstruktionen und Modelle eigentlich erst greifbar machen, und die Lambdas braucht man, weil sie viele Implementierungsaufgaben vereinfachen. Dieses Grundmaterial wird ergänzt durch die Module, die Java 9 etwas brisant machen, die Behandlung der Exceptions, um die man nicht herumkommt, und die Vorstellung einer Reihe von Tools, um die man gleichfalls nicht herumkommt. Rapid-Coding wird in diesem Buch vorbereitet. Es gibt zwei kleine Einschübe dazu, aber zunächst nicht mehr. Stattdessen kommen Klarheit der Formulierung, Standards, Präzision und selbst Einfachheit als unterschwellige Themen immer wieder zur Sprache. Diese Punkte sind zentral im Rapid-Coding-Ansatz und treiben ihn voran.

Die erste Java-Rundreise

-Ein Plan und ein Stück Code - Was ein Entwickler wissen muss

-Aufteilung von Code in Methoden

-Iteration und robuste Programmierung

-Elementare Programmiertechnik anhand eines Beispiels

-Objekte, Referenzen, Nirvana und this

-Erste Übersicht zu Typen, Interfaces und Vererbung

-Die bekannten Tools: Exceptions, Enums, Threads, Strings, Arrays, Collections, Maps und Generics

Die erste Rundreise macht Leserinnen und Leser mit den Java-Grundlagen vertraut. Sie lernen dabei alles an Syntax kennen, was Sie für das Schreiben von prozeduralem Code brauchen. Unterstützt wird dieses Kapitel vom Anhang, der weitere Informationen zum JDK und zum allgemeinen Arbeiten mit Java bietet. Die erste Rundreise enthält neben der grundlegenden Syntax einführendes Material zu vielen Themenpunkten, die häufig benötigt werden, so dass die Einführung relativ breit angelegt ist. Leserinnen und Leser werden dabei auch mit Aspekten und Themen bekannt gemacht, die erst in späteren Kapiteln tiefer ausgeführt werden. Derartige Themen sind mit einem Stern (*) gekennzeichnet.

Objekte

-Warum Objekte Klarheit bringen und wie sie dies tun

-Die Aufgaben einer Klasse

-Der Sprung in eine andere Art von Software – Das Denken in Objekten

-Der in sich abgeschlossene Baustein

-Objektorientierung übersetzt Konzepte in Objekte

-Diesmal vertieft: Typen, Interfaces und Vererbung

Das Kapitel zur Objektorientierung führt in das Denken mit Bausteinen ein und behandelt die zugehörige Java-Syntax. Mit vorhandenen Bausteinen zu arbeiten, ist manchmal trivial. Bausteine richtig zu erstellen, ist weniger trivial, aber auch nicht unbedingt schwierig. Mit Bausteinen zu konstruieren, die noch nicht vorhanden sind, ist zunächst etwas eigen, passt aber gut in das typische objektorientierte Vorgehen. Das Kapitel zeigt die Eigenheiten dieser Konstruktions- und Denkform. Es erklärt die Grundlagen und vermittelt vor allem die mentale Haltung, welche die Objektorientierung prägt.

Module

-Module sind das zentrale Thema in Java 9. Markus und Sonja, ein Freak und eine eigenwillige Entwicklerin, erschließen die Bedeutung der Module in einigen Gesprächen

-Wozu brauchen wir Module?

-Von der Abschließung der Module bis zu starker Kapselung und deep Reflection

-Alles, was Sie über Module wissen müssen, zusammengefasst in den Essentials dieses Kapitels

Das Thema der Module rumort in Java und berührt viele Detailthemen. Ihre Einführung hat fundamentalere Auswirkungen als seinerzeit die Einführung der Generics in Java 5 oder die der Lambdas in Java 8. Auch die JDK-Tools wie Compiler, Archiv-Tool und Interpreter sind stark betroffen. Sie sollten in Java 9 anders bedient werden als in Java 8. Viele Infos zu den Modulen findet man deshalb auch im Anhang, in dem der JDK und seine Tools beschrieben werden.

Lambdas

-Lambdas sind anonyme Klassen mit einer einfacheren Verwendung als diese

-Lambda-Interfaces

-Functionals

-Methodenreferenzen

-Eine kleine Einführung in eine neue Welt: Die Behandlung von Mengen durch Streams

Das Teilkapitel über anonyme Klassen behandelt zunächst ein Thema, für das sich typische Java-Entwickler nicht wirklich interessieren. Dennoch wird die Syntax ausführlich erklärt. Das Kapitel erarbeitet einige wichtige und merkbare Aussagen und kommt dann zum eigentlichen Grund für die ganze Mühe, nämlich zu den Lambdas. Dort wird der Stoff aber keineswegs leichter – ganz im Gegenteil. Die Abhandlung der Lambdas ist für Leserinnen und Leser sicherlich anstrengend. Diejenigen unter Ihnen, die zu Anfang des Buches noch Java-Neulinge sind und das Buch ohne Praxisbegleitung bearbeiten, stehen an dieser Stelle möglicherweise vor einer großen Herausforderung. Das vorliegende Buch bietet jedoch genügend Anregungen und Codebeispiele, mit denen man selbständig weiterarbeiten und sich damit beliebig viel praktische Erfahrung verschaffen kann.

Exceptions

-Wirf eine Exception

-Stack-Traces, Methodenlabyrinthe und Ariadne-Fäden

-Ein Code-Wächter

-Checked- und Unchecked-Exceptions

-Try-With-Resources und Suppressed-Exceptions

-Exceptions tatsächlich behandeln

Die Exceptions sind Standard-Java-Stoff und sozusagen eine Pflichtveranstaltung. Das Thema ist nicht so kurzweilig wie die erste Rundreise, es ist nicht so interessant wie die Objektorientierung, nicht so aufregend wie Lambdas und nicht so neu wie die Module. Aber die Exceptions sind unumgänglich. Man muss da einfach durch. Und man muss da auch mit guter Aufmerksamkeit durch. Das Kapitel enthält einiges Material, wie man Code mit Exception-Handling schreibt. Da gibt es sehr viel mehr als nur Syntax. Man schreibt in der Praxis kaum einmal eine Seite Code, ohne mit Exceptions in Berührung zu kommen. Und sobald man es mit Exceptions tatsächlich zu tun hat, sollte man wissen, was es über sie zu wissen gibt und wie man sie handhabt.

Die zweite Java-Rundreise

-Kurze Intros zu Class, Reflection und System-Properties

-Sichtbarmachung von Abläufen mit Standard-Logging

-Das Notwendigste zu den Threads

-Ebenfalls unverzichtbare Tools: Die sehr eigene Welt der IO-Streams

-Files, Ressource-Pfade und das Standardvorgehen beim Lesen und Schreiben von Dateien

Die zweite Rundreise ist das, wonach es sich anhört: ein bequemer Ausflug. Mit Class und Reflection wird ein abseitiges Thema angeschnitten, weil man hin und wieder damit zu tun hat. Das Ganze bleibt aber ohne viel Tiefgang, weil Reflection (zum Glück) nicht zum essentiellen Konstruktionswissen zählt. Beim Thema Logging ist das anders, dies kann durchaus nützlich sein. Entsprechend gründlich wird es auch behandelt. Bei den Threads hingegen sind wir zufrieden, wenn wir hinterher ungefähr wissen, mit was man es da eigentlich zu tun hat. Demgemäß ist dieses Teilkapitel auch als ‚Intro’ betitelt. Anders ist es wiederum bei Streams und Files. Diese Themen werden gründlich und abschließend (wenn auch nicht erschöpfend) behandelt.

Anhang

-Falls alles andere versagt: Eine besondere Hilfestellung beim Schreiben der ersten Klasse

-Der JDK und seine Versionen – Eine Übersicht auf einer Seite

-Modulepath und Classpath

-Compiler, Interpreter und das Archiv-Tool

-Eine sehr kurze Intro zu Eclipse

Das erste Anliegen - Verständliche Texte

Betrachten Sie das folgende Stück Code. Auf seinen genauen Inhalt kommt es hier nicht so sehr an:

long number = … // Kommt von irgendwo her
boolean test = true;
if (number == 2)
test = true;
else if (number % 2 == 0)
test = false;
else
{
double squareRoot = Math.sqrt(number + 1);
long divider = 3;
for (; divider < squareRoot; divider += 2)
if (number % divider == 0)
test = false;
}

Dieses kleine Beispiel zeigt Java-Code. Um diesen zu schreiben, muss man einige Regeln beachten, mit denen wir uns befassen werden. Aber dieser Code ist kein besonders gut verständlicher Text. Man könnte sagen: Software sieht nun mal so aus. Doch das ist Nonsense. Wir können Software in völlig unlesbarer Form schreiben und wir können sie in gut verständlicher Form produzieren. Beides geht und das obige Beispiel ist keines von beiden. Wir sind damit nicht zufrieden. Denn wir wollen und brauchen grundsätzlich gut verständlichen Code. Wir wollen kleine und größere Systeme in Software erstellen. Und je umfangreicher die Systeme werden, desto größer ist die Notwendigkeit von verständlichen Texten. Wir brauchen Codeformulierungen, die super einfach lesbar und erstellbar sind. Etwa Code in dieser Art:

long number = … // Kommt irgendwoher. Wird getestet
boolean test = PrimeTester.isPrime(number); // Das ist im Prinzip verständlich

Ob die Bedeutung des Ausdruck: ‚PrimeTester.isPrime(number)’ tatsächlich jedermann zugänglich ist, ist hier nicht die richtige Fragestellung. Selbstverständlich setzen wir bei unseren Konstruktionen eine fachliche Voreingenommenheit voraus, die beispielsweise die Leistung von isPrime() einordnen kann. Aber darüber hinaus erheben wir beim Schreiben von Software grundsätzlich den Anspruch, dass wir mit lesbaren Texten operieren. Die Implementierung von isPrime() könnte dieses Aussehen haben:

public class PrimeTester
{
public static boolean isPrime(long number) throws NumberException
{
boolean result = false;
if (Eratosthenes.canHandle(number))
result = Eratosthenes.test(number);
else if (Atkin.canHandle(number))
result = Atkin.test(number);
else if (Fermat.canHandle(number))
result = Fermat.test(number);
else
throw new NumberException(“cannot handle: ” + number);
return result;
}
}

Die hypothetischen Komponenten Eratosthenes, Atkin und Fermat werden hier als gegeben vorausgesetzt. Sie sehen nun den Unterschied zum ersten Codebeispiel. Der Code von PrimeTester.isPrime() ist gut lesbar. Für jemand, der fachlich versiert ist, ist diese Implementierung wie ein offenes Buch. Es ist verständlicher Text, der in Java verfasst ist. Und der Leser muss noch nicht einmal Java können, um ihn zu verstehen. Uns ist klar, dass Code wie der im ersten Beispiel nicht immer vermieden werden kann. Aber prinzipiell kann jedes Stück Code entweder verständlich formuliert oder verständlich verpackt werden.

Die Forderung, mit verständlichen Texten zu arbeiten, ist in diesem Buch für lange Zeit eine unserer Leitlinien und eine Vision, die uns führt. Oftmals ist sie nur implizit vorhanden, sozusagen versteckt in unserem Hinterkopf, denn wir sind auf weiten Strecken – im Grunde große Teile des Buches hindurch – auch mit anderen Themen befasst. Wir müssen lernen, wie man elementaren Java-Code schreibt. Dann müssen wir sehen, wie wir zu umfassenderen Konstruktionen kommen, ohne die Sicherheit und Stabilität von elementarem Code aufzugeben. Und gelegentlich arbeiten wir auch daran, wie man dies auf verlässliche und schnelle Art macht (Rapid Coding).

Wir werden bei der Verfolgung unserer Vision Schritt für Schritt Techniken und Strategien entfalten, die uns die großen Konstruktionen erschließen. Aber dabei geht es sehr langsam voran. Denn das elementare Coding steht immer wieder im Zentrum unserer Bemühungen. Dieses ist die Basis und muss besonders gut beherrscht werden. Wir lernen Techniken, die uns Sicherheit geben, mit denen wir verlässlich konstruieren können, die uns schnell (sehr schnell) machen und die uns nachts gut schlafen lassen. Aber das elementare Coding wird dabei im Fokus bleiben und ein wichtiges Fundament bilden.

Konventionen und Verabredungen

Guidelines, Essentials und Merksätze

Zu den meisten Themen gibt es Essentials. Diese stellen den Inhalt des vorangehenden Kapitels in Kurzform dar. Die Essentials haben zwei Zielsetzungen. Zum einen ist das die Zusammenfassung, zum anderen die Kürze. Eine Zusammenfassung (in anderen Worten) soll zudem die Wahrscheinlichkeit für den Leser erhöhen, eine verständliche Erklärung zu den gerade aktuellen Punkten zu finden. Die Essentials sind dafür gedacht, zu einem gegebenen Stoff Zusammenhänge zu rekapitulieren oder auf knappem Raum nach bestimmten Informationen suchen zu können.

In den Text sind zuweilen kurze und im Layout hervorgehobene Statements eingestreut. Diese kann man als Merker, Merksätze oder Grundaussagen auffassen. Ihr Zweck ist es, die Aufmerksamkeit auf einen Zusammenhang zu richten, der das Erfassen einer Thematik erleichtert. Merker sind auf das erste Kennenlernen eines Themas ausgerichtet. Sie wollen einen Pflock einschlagen und eine feste Aussage auf einem noch schwankenden Boden machen. Sie wollen die Aufmerksamkeit auf einen Punkt fokussieren. Bei wiederholtem Lesen werden die Merker - anders als die Essentials - überflüssig.

Zu einigen Themen gibt es Guidelines. Diese stehen wie die Essentials am Ende eines Kapitels und enthalten Ratschläge und Empfehlungen.

Die *-Kapitel

Hauptsächlich in der ersten Rundreise befinden sich Kapitel, die mit einem Stern (*) gekennzeichnet sind. Sie besprechen Themen, die zu einem späteren Zeitpunkt ausführlich behandelt werden. Verständnisprobleme in diesen Kapiteln sollte man deshalb nicht allzu ernst nehmen, sondern eher als Anregung betrachten. Aber auch sonst ist Geduld eine schöne Tugend. Gerade auf der ersten Rundreise treffen Sie öfters auf Syntax, die erst später genauer erklärt wird. Zwei Quellen für schnelle Erklärungen, die man bei Schwierigkeiten als erstes konsultieren sollte, sind allgemein das Glossar und im Speziellen die Essentials des betreffenden Kapitels. Zu einigen der mit einem * gekennzeichneten Kapitel gibt es die ausführliche Behandlung nicht in diesem Buch, sondern im dem Folgeband ‚Tools’.

Das Glossar

Im Anhang dieses Buches sollte sich ein relativ umfangreiches Glossar befinden. Das war der Plan. Der ursprüngliche einleitende Text zum Glossar war dieser:

„Der Leser kann dort Begriffe nachschlagen oder sich zu Stichworten weitere Informationen besorgen. Das Glossar ist zusammen mit den Essentials der einzelnen Kapitel eine Alternative zu erklärendem Text. Leserinnen und Leser sollen mit dem Glossar über eine Möglichkeit verfügen, zu allen neu auftauchenden Begriffen eine kurze erste Erklärung zu erhalten. Wenn man beispielsweise in den einführenden Abschnitten auf die Begriffe ‚abstrakt’, ‚Referenzvariable’ oder ‚Nirvana’ stößt und damit nicht viel anfangen kann, so ist das ein Fall für das Glossar.“

Dass es das Glossar in diesem Buch nun doch nicht gibt, hat vor allem Platzgründe. Das Glossar wird stattdessen als eigene Publikation parallel zu diesem Buch erscheinen.

Konventionen

In diesem Buch werden Methodennamen durchgehend mit runden Klammern geschrieben. ‚main’ beispielsweise kann eine Variable sein oder vielleicht auch ein Threadname. Mit ‚main’ ist jedoch keine Klasse gemeint, denn Typnamen werden durchgehend groß geschrieben (wie etwa Example oder String). Und mit ‚main’ ist auch keine Methode gemeint, denn die würde als main() geschrieben werden.

Eine kleinere Schwierigkeit beim Schreiben über Software besteht in der Unterscheidung zwischen Standardtypen (die mit dem JDK mitgeliefert werden) und den selbst-verfassten Typen des Autors. Aus dem Kontext heraus ist die Unterscheidung zumeist klar. Standardtypen werden in der Regel bei der ersten Erwähnung als solche bezeichnet. Das zweite Hilfsmittel der Unterscheidung ist die Angabe der Packages. Standardtypen residieren grundsätzlich in den Packages java oder javax. Selbst-verfassten Typen residieren niemals in diesen Packages.

Es gibt einige wenige Grafiken. Kursive Schrift in Grafiken für Typ- oder Methodennamen hat gemäß UML grundsätzlich die Bedeutung von ‚abstrakt’, also von nicht-implementiert.

Codebeispiele

Ein Buch über Software lebt von Codebeispielen. Bei deren Darstellung im Buch wird darauf geachtet, dass die seitenverschlingende Wiederholung von ähnlichen Codepassagen vermieden wird. Vor allem auf die nochmalige Darstellung eines präsentierten Beispiels als Ganzes am Kapitelende wird verzichtet. Dennoch kommt es vor, dass einander ähnlich sehende Codestücke gezeigt werden, um im Vergleich irgendwelche Besonderheiten hervorzuheben.

Bei der Darstellung von Code in diesem Buch werden Schlüsselwörter durch Fettdruck hervorgehoben. Schlüsselwörter sind Elemente der Sprache, die für den Compiler eine besondere Bedeutung haben und die deshalb als Bezeichner für Variable, Typnamen oder Methodennamen nicht verwendet werden dürfen.

Obwohl der Autor die Meinung vertritt, dass Leserinnen und Leser dann am meisten von den Beispielen profitieren, wenn Sie deren Code selbst schreiben, sind die größeren Beispiele dieses Buches in einem .zip-File verfügbar. Der Sourcecode kann auf der Seite www.rinser.info heruntergeladen werden siehe dort den Link „Downloads“). Das .zip-File entpacken Sie durch Doppelklick. Das .jar-File, das Sie dann erhalten, müssen Sie mit dem Archiv-Tool des JDK entpacken.

Wer soll dieses Buch lesen?

Jeder Entwickler, der schon einmal in irgendeiner Sprache Code geschrieben hat, sollte dieses Buch lesen können. Leute mit Java- und C/C++-Kenntnissen werden sich dabei leichter tun als andere.

Gefundene Fehler

Meldungen von gefundenen Fehlern sind sehr willkommen, egal ob es sich um Rechtschreibfehler, inhaltliche Fehler oder um Fehler im dargestellten Code handelt. Hilfreich ist eine Mail an java@rinser.info mit Angabe des Kapitels, der Seite und der Abschnittsnummer (vom Beginn der Seite an gezählt) und einer kurzen Beschreibung des Fehlers. Die Mail sollte idealerweise den Betreff: ‚Objekte’ enthalten und eine Klassifizierung nach ‚Tippfehler’, ‚Inhaltlicher Fehler’ oder Codefehler’.

Das Software-Universum

Legen Sie sich ein Tagebuch zu. Nennen Sie es das „Buch der unbrauchbaren Softwareteile“ (BUST). Notieren Sie darin jede Komponente, die Sie erstellt haben und die nicht mehrfach verwendbar ist. Und notieren Sie jeweils sorgfältig den Grund, warum Sie schon wieder ein Stück Code für den Müll geschrieben haben

Nicht-dokumentierte Software ist nicht wiederverwendbar

Software, von der man den Code lesen muss, um zu wissen, was sie tut, ist nicht wirklich wiederverwendbar

Vor allem müssen wir aufhören, in Lösungen zu denken. Lösungen sind nahezu niemals wiederverwendbar

1.Das Buch der unbrauchbaren Softwareteile

2.Ein Anfang – aber nicht mehr

 

1. Das Buch der unbrauchbaren Softwareteile

Software bietet tausend Möglichkeiten. Es gibt mannigfache Wege, etwas zu konstruieren. Die Freiheit, was wir bauen und wie wir es bauen, ist unbegrenzt. Wir können raffinierte Algorithmen ersinnen, um gegebene Probleme zu lösen oder um bestehende Lösungen zu verbessern. Wir können jeden gewünschten Aspekt eines gegebenen Systems in Software übertragen. Und wir können Systeme in Software erfinden, die es in der realen, fassbaren Welt nicht gibt. Die Vielfalt an Programmen, Maschinen, Bauteilen, Konstruktions-Ideen, Techniken und Vorgehensweisen ist kaum zu benennen.

Ob wir mittelalterliche Handschriften analysieren, die Dynamik von physikalischen Abläufen berechnen oder Verkehrsströme simulieren, wie auch immer die zu erstellenden Systeme aussehen, jeder Softwareentwickler, der fachliches Wissen mit technischem Geschick kombiniert, kann sie bauen. Dies geschieht selbstverständlich mit unterschiedlichem Erfolg, in allen Qualitätsabstufungen und mit sehr verschiedenem zeitlichen Aufwand. Aber es gibt für die Konstruktion eines beliebigen Systems in Software keine prinzipiellen Voraussetzungen. Man braucht dazu keine besonderen Maschinen und keine teuren Werkzeuge. Ein Entwickler, der eine bestimmte Idee hat, kann unmittelbar damit beginnen, sie umzusetzen. Und er muss dazu keineswegs die bekannten Techniken und Ideengebäude verwenden. Software ist beliebig formbar und konstruierbar.

Im September 1990 veröffentlichte die amerikanische Kolumnistin Marilyn vos Savant das Ziegenproblem:

„Stellen Sie sich vor, Sie müssten als Teilnehmer an einer Gameshow eine von drei Türen auswählen. Hinter einer Tür befindet sich der Gewinn, ein Auto, hinter den beiden anderen befinden sich dagegen Ziegen. Sie wählen Tür Nr. 1 und der Showmaster, welcher weiß, was sich hinter den jeweiligen Türen befindet, öffnet eine andere Tür, z. B. Tür Nr. 3, hinter der eine Ziege erscheint. Nun fragt er Sie, ob Sie bei Tür Nr. 1 bleiben oder ob Sie stattdessen Tür Nr. 2 wählen wollen. Sollte man besser wechseln“ Frei nach de.wikipedia.org, Ziegenproblem)

Stellen Sie sich weiter vor, dass Sie sich mit Ihren Kolleginnen und Kollegen über die Beantwortung der Frage und die korrekte Lösung des Problems nicht einig werden. Eine Ihrer möglichen Optionen besteht darin, das Problem durch Software zu lösen. Man verfährt dabei nach dem Monte-Carlo-Verfahren. Man macht Tausend oder auch eine Million Versuche und verteilt Fahrzeug und Ziegen jedes Mal zufällig hinter den drei Türen. Dann zählt man die Treffer (für den Gewinn des Fahrzeugs), die man hat, wenn man von Tür 1 wechselt, und die Treffer, wenn man nicht wechselt. Teilt man die Zahl der Treffer ohne Türwechsel durch die Zahl der Versuche, so erhält man eine Quote von etwa 33 Prozent, wenn nur die Zahl der Versuche groß genug ist. Für die Trefferquote bei einem Wechsel der Tür liefert ein Durchlauf der Simulation diese Daten:

Doors: 3 trials: 1000000000 winRate: 0.66666967 Duration: 35.431s

Der springende Punkt ist hier nicht das Ergebnis des Tests. Der springende Punkt ist, dass wir im Handumdrehen zu einer Fragestellung ein kleines Programm schreiben können. Und so wie wir in relativ kurzer Zeit zu diesem Problem ein Stück Software erstellen, so können wir zu jedem Thema, das uns interessiert, Software schreiben, die etwas behandelt, berechnet, analysiert oder einfach nur grafisch darstellt.

Software ist ein eigenes Universum und mit Software werden eigene Welten geschaffen. Software ist ein Betätigungsfeld mit einem extremen Potential. Wir haben darin alle Freiheiten. Wir können nahezu alles in Software konstruieren, was uns einfällt, und wir können es auf tausend verschiedene Weisen tun. Diesen unglaublichen Möglichkeiten, die Software auf der einen Seite jedem einzelnen Entwickler bietet, steht auf der anderen Seite ein gewisser sanfter aber dennoch nachdrücklicher und dauerhafter Zwang gegenüber. Nämlich der Zwang, Software möglichst rational und effizient zu entwickeln. Dabei handelt es sich nicht wirklich um einen Gegensatz. Die potentielle Freiheit, beliebige Systeme bauen zu können, wird erst dann zu einer tatsächlichen Möglichkeit, wenn wir es schaffen, unsere Konstruktionen auch in begrenzter Zeit umzusetzen. Der Traum von den unbegrenzten Möglichkeiten von Software ist eng an die Bedingung geknüpft, dass wir für die Realisierung einer Konstruktion nicht beliebig lange brauchen. Um Spielräume und Bewegungsfreiheit zu haben, brauchen wir eine bestimmte Effizienz in der Umsetzung unserer Ideen und Konstrukte. Um im Software-Universum Freiräume und Gestaltungsmöglichkeiten zu haben, muss man Implementierungen mit hoher Produktivität erstellen können. Wenn man sich im Schneckentempo bewegt, ist Bewegungsfreiheit nicht wirklich gegeben.

Es liegt auf der Hand, dass uns erst die Geschwindigkeit der Umsetzung die Potentiale von Software eigentlich erschließt. Effizienz und Produktivität sind Schlüsselfaktoren in der Softwareentwicklung. Dabei ist es nicht so, dass wir bei der Erstellung von Code keine Zeit haben. Es geht nicht darum, permanent mit Höchstgeschwindigkeit unterwegs zu sein. Im Gegenteil: Gelassenheit ist in der Erstellung von Software ein guter Begleiter. Es geht um die Grundschnelligkeit, mit der man sich bewegt.

Und es gibt weitere Faktoren, die eine Rolle spielen. Wenn Sie beispielsweise zum Ziegenproblem nicht nur eine einfache Simulation wollen, sondern auch eine Benutzeroberfläche, um etwa Gesetzmäßigkeiten anschaulich zu machen, oder um zu zeigen, wie die Wahrscheinlichkeit sich mit der Zahl der Türen verändert, brauchen Sie dazu einen Oberflächenbaukasten. Dabei reden wir nicht von Swing oder JavaFX oder einer der etablierten Browser-basierten Oberflächentechniken. Wir reden von einem Baukasten, der Ihnen das Anlegen der gewünschten Oberfläche in wirklich kurzer Zeit erlaubt. Denn als halbwegs erfahrene Entwickler wissen wir, dass Sie keine Lust mehr auf eine anschauliche Darstellung des Ziegenproblems haben, wenn Sie diese auf der Basis von Swing oder JavaFX von Grund auf neu erstellen müssen. Denn die Erfahrung lehrt uns, dass wir damit Tage oder Wochen beschäftigt sind. Was wir brauchen, ist eine Art generelle Wiederverwendung. Wenn wir Software mit hoher Produktivität erstellen wollen, müssen wir als erstes damit aufhören, Software zu bauen, die nur einmal verwendet wird - beziehungsweise nur einmal verwendbar ist.

Wenn Ihnen das Thema Effizienz wichtig erscheint, dann kaufen Sie sich ein Notizbuch von nicht ganz kleinem Umfang und notieren Sie in diesem zukünftig jedes Stück Software, das Sie für den Müll produziert haben, das also nicht mehrfach verwendbar ist. Das ist ein ernst gemeinter Vorschlag. Vielleicht ist es hilfreich, wenn Sie sich systematisch damit befassen, wann und warum Sie Software auf eine einmalige Nutzung hin erstellen.

Legen Sie sich ein Tagebuch zu. Nennen Sie es das „Buch der unbrauchbaren Softwareteile“ (BUST). Notieren Sie darin jede Komponente, die Sie erstellt haben und die nicht mehrfach verwendbar ist. Und notieren Sie jeweils sorgfältig den Grund, warum Sie schon wieder ein Stück Code für den Müll geschrieben haben

Dieses Tagebuch wird Ihnen vermutlich zeigen, warum so viel Software nicht wiederverwendbar ist. Der allererste Grund ist fehlende Qualität. Damit ein Stück Software ein Bauteil wird, das man ohne Bauchschmerzen wieder einsetzen kann, muss es über eine gewisse Mindestqualität verfügen. Aber selbst wenn wir nun anfangen, unsere Softwareteile mit hoher Sorgfalt zu schreiben (was den Zeitaufwand möglicherweise erhöht, vielleicht aber auch nicht), sind wir von einer praktikablen Wiederverwendbarkeit noch weit entfernt. Damit wir ein Stück Code, das wir irgendwann einmal verfasst haben, wiederverwenden können, müssen wir es erst einmal wiederfinden. Dazu muss es a) geeignet abgelegt und b) geeignet beschrieben sein.

Nicht-dokumentierte Software ist nicht wiederverwendbar

An dieser Stelle eröffnet sich nun ein noch größeres Problemfeld. Denn welche Softwareteile haben eine vernünftige Dokumentation, vor allem eine solche, die wir glauben, selbst wenn es unsere eigene ist? Haben Sie tatsächlich schon öfters Softwareteile gesehen, die nicht zu offiziell vertriebenen Bibliotheken gehören und für die eine brauchbare Beschreibung existiert? In der Regel sind Dokumentationen zu Softwareteilen unbrauchbar, wenn diese nicht aus anerkannten Bibliotheken stammen. Denn selbst wenn Beschreibungen existieren, gibt es zwei Seiten: Das, was in der Beschreibung steht, und das, was tatsächlich realisiert ist. Um Software, die nicht aus anerkannten Bibliotheken stammt, wiederverwenden zu können, muss man sich oftmals zuerst mit dem Sourcecode befassen, oder die Software erproben – was ebenfalls mühsam ist. Das heißt, wir müssen Code lesen, um ihn verwenden zu können, weil Beschreibungen entweder nicht existieren, oder nicht präzis genug sind, oder mit der Software, die sie beschreiben, nicht wirklich viel zu tun haben. Auch das ist ein Qualitätsproblem und wir alle wissen, dass es sich dabei um ein real existierendes und häufiges Qualitätsproblem handelt.

Software, von der man den Code lesen muss, um zu wissen, was sie tut, ist nicht wirklich wiederverwendbar

Wenn Sie sich nun aus purem Spaß an der Sache entschließen, trotz des hohen Aufwands eine Benutzeroberfläche für das Ziegenproblem zu schreiben, werden Sie anschließend einige Einträge in Ihr Buch der unbrauchbaren Softwareteile stellen. Denn die Oberfläche, die Sie da schreiben, ist höchstwahrscheinlich nicht wiederverwendbar. Sie haben sich nun zwar bemüht und haben eine Dokumentation verfasst. Und das war wirklich Aufwand. Wenn Sie es einmal konsequent durchgezogen haben, wissen Sie, von was wir hier reden. Zudem mussten Sie die Dokumentation bei jeder Änderung der Software mit anpassen. Doch am Ende folgt die Erkenntnis, dass auch das für den Müll ist. Denn Sie können diese Oberfläche nicht wiederverwenden. Sie ist auf das Ziegenproblem zugeschnitten und ist für andere Zwecke nicht nutzbar.

Treten Sie Ihr Buch der unbrauchbaren Softwareteile deshalb nicht gleich ebenfalls in den Eimer. Es ist nützlich und wird Ihnen noch gute Dienste erweisen. Um Software vernünftig zu erstellen, brauchen wir nicht nur eine funktionierende Wiederverwendung, sondern auch eine andere Haltung zu der Art, wie wir Software konstruieren. Ihr Problem beim Erstellen einer Benutzeroberfläche für das Ziegenproblem ist, dass Sie Lösungen verfassen und in Lösungen denken. Um Software ‚vernünftig’ zu erstellen und um dieser Idee von einem Software-Universum näher zu kommen, in dem wir Bauteile, Maschinen, Applikationen und Konstrukte unseres Erfindungsreichtums mit einer gewissen Leichtigkeit und Grundschnelligkeit umsetzen, brauchen wir vor allem eine entsprechende mentale Haltung.

Vor allem müssen wir aufhören, in Lösungen zu denken. Lösungen sind nahezu niemals wiederverwendbar

Der rote Faden in dem umfassenden Plan, in dem das vorliegende Buch ein Anfang ist, ist die Idee von einem allgemeinen Konstruktionsansatz, mit dem wir erdachte Systeme und Konstruktionen in akzeptabler Zeit verfassen können, ohne das Buch der unbrauchbaren Softwareteile erheblich zu erweitern. Einige der Themen dieses Buches werden dabei eine Rolle spielen und sich letztlich als einfach erweisen, obwohl sie nicht von Anfang an so erscheinen. Auf andere Punkte, wie etwa auf das überragend wichtige Konstruktionsthema von Einfachheit selbst, trifft dies jedoch nicht zu.

2. Ein Anfang – aber nicht mehr

Das Buch ist mit dem Ziel geschrieben, zu zeigen, wie man sich dem Software-Universum annähern kann. Es ist jedoch keine Eintrittskarte – der Titel lautet nicht: Ingenieur in 21 Tagen. Es wird ganz im Gegenteil die Auffassung vertreten, dass das Erlernen und der Erwerb von Konstruktionstechniken sehr lange dauert. Aber es gibt diese Konstruktionstechniken und es gibt diese Vorgehensweisen, die es erlauben, Software mit relativ hoher Produktivität und hoher Qualität zu schreiben. Diese Techniken muss man sich erschließen. Man kann sie erlernen. Das vorliegende Buch ist dazu ein Anfang – aber auch nur ein Anfang. Die einzelnen Themen werden uns in der konkreten Ausführung voranbringen, weil im Konkreten immer die Möglichkeit von Zugang und Verstehen liegt. Aber unsere Themenauswahl ist in erster Linie durch naheliegende Coding-Aspekte wie elementare Syntax, Exceptions oder Streams bestimmt, die in Java zum notwendigen Handwerk gehören. Es wird sich in dem ein oder anderen Beispiel die Möglichkeit ergeben, mehr zu sehen. Und je weiter wir fortschreiten, desto klarer wird die Bedeutung von Konstruktion und ihren speziellen Prinzipien hervortreten.

Es wird im Übrigen kein Unterschied zwischen dem ‚Schreiben’ und dem ‚Konstruieren’ von Software gemacht. Das Wort Konstruieren betont einen Gegenpol zu einer verbreiteten Art der Erstellung, die ich als ‚Bastelei’ bezeichne. Wenn wir an Lösungen ‚herumbasteln’, ist die Qualität oft schlecht, die Produktivität lausig und der Zeitdruck hoch. Wenn wir Software erstellen, sollten wir nicht basteln. Wir können experimentieren, wenn wir etwas erproben wollen oder uns etwas erschließen müssen. Aber die legitime Tätigkeit des Experimentierens sollte vom Konstruieren von Software und ihren Bausteinen unterschieden werden.

Stellt man die wesentlichen Faktoren, die gerade genannt wurden, zusammen, dann geht es um Effizienz, Qualität, Einfachheit und Wiederverwendbarkeit. Diese Faktoren sind bestimmend, sie interessieren und es ist offensichtlich, dass sie zum Teil auch voneinander abhängen. Und es kommen weitere Faktoren hinzu, die im großen Plan nicht ganz so stark im Vordergrund stehen, aber dennoch Wirkung haben, wie Lesbarkeit, Robustheit, Stabilität und Kürze. Wegen dieser Faktoren und Punkte wurde das vorliegende Buch geschrieben. Aber dieses Buch handelt in erster Linie und bewusst vom Coding und nicht von Qualität oder Einfachheit. Die genannten Themen werden nebenbei aufgegriffen – zwanglos, nicht häufig, und auch nur dann, wenn das Coding-Material dazu gute Gelegenheit bietet. Das ist nicht sehr oft der Fall. Wir befassen uns mit elementarem Code, mit anspruchsvollerem Code, mit größeren Konstruktionen, mit Konstruktionsprinzipien und Vorgehensweisen. Und hin und wieder bemerkt man, dass man gerade mit einer Schlüsselsituation zu tun hat und dass der betreffende Code (oder die Konstruktionsidee) mehr beinhaltet als nur eine Illustration der aktuellen Syntax.

Es ist nicht so – wenn wir kurz den Punkt Effizienz herausgreifen – dass man zuerst lernt, was Effizienz im Coding bedeutet, und im nächsten Schritt dann Coding selbst erlernt. Es geht selbstverständlich umgekehrt. Man lernt, wie man mit gesundem Menschenverstand an Programmierung herangeht. Und auf diesem Weg kann man ab und zu auf die ein oder andere Einsicht hinweisen, die nicht ganz offensichtlich ist. Das vorliegende Buch ist ein Buch über Coding. An einigen Stellen wird man eventuell bemerken, dass es mit anderen Hintergedanken geschrieben wurde.

Eine kleine Java-Rundreise – Teil 1

Gut geschriebener Code besteht aus einer Vielzahl kleiner und kleinster Methoden

Entwickler müssen sich hin und wieder klar machen, welchen Spielraum sie durch die Gestaltung der Methoden und Methodenköpfe haben, in die sie den Code aufteilen. Die Methodenköpfe bestimmen das Vokabular, mit dem Code formuliert wird

Jeder Teilausdruck und jeder Methodenaufruf wird im Programmablauf durch seinen Ergebniswert ersetzt

Die Wirkung einer Operation und der Ergebniswert einer Operation sind nicht notwendigerweise dasselbe

‚Handgeschriebene’ Iterationen, also solche Iterationen, in denen der Entwickler die Schleifenbedingung und die Inkrementierung selbst verfasst, sind eine vermeidbare Fehlerquelle

Code erhält Struktur und Erklärung durch Aufteilung in Methoden. Methoden sind sozusagen die kleinen Bausteine, aus denen wir Code aufbauen, beziehungsweise in die wir Code zerlegen

Methodenaufrufe für Objekte haben immer dieses Standardaussehen: object.perform(). Dies liest man in dieser Weise: Schicke die Nachricht perform() an object. Das Objekt entscheidet dann, was es mit der Nachricht anfängt

Alle Objekte, die keinen elementaren Typ haben, leben im Nirvana

Traue niemals einer Aussage zur Performance. Messe grundsätzlich selbst

1.Intro

2.90 Prozent - Ein Plan und ein Stück Code - Was ein Entwickler wissen muss

3.Methoden

4.Lokale Variable, Blöcke, Scope und final

5.Die Grundregel für zusammengesetzte Ausdrücke

6.Weitere elementare Regeln und Begriffe

7.Zuweisung

8.Kombinierte Zuweisungen

9.Inkrement und Dekrement

10.Stringverknüpfung (+)

11.Bedingung (if/else)

12.Verzweigung (switch)

13.Der new-Operator

14.Gleichheits- und Identitäts-Operator

15.Logische Operatoren, Vergleiche, Verknüpfung von Bedingungen

16.Der Conditional-Operator

17.Zusammenfassung der Operatoren

18.Iterationen

19.Elementare Programmiertechnik anhand eines Beispiels

20.Objekte *

21.Typen und Interfaces *

22.Allgemeine Vererbung *

23.Packages, Import und private Typen

24.Module *

25.Exceptions *

26.Elementare Typen und ihre Verpackung in Objekten - Wrappers

27.Zahlen und Zufallszahlen

28.Konstanten, Konstanz und Unveränderbarkeit

29.Enums *

30.Threads *

31.Datum und Uhrzeit

32.Performancemessung

33.Strings *

34.Arrays

35.Collections *

36.Generische Klassen *

37.Generische Methoden *

38.Maps *

39.Essentials

40.Guidelines

1.Intro

Dieses Kapitel zeigt Statements, Operatoren und Ausdrücke, die in einer durchschnittlichen Anwendungsprogrammierung häufig vorkommen. So begegnen uns etwa kombinierte Zuweisungen, Inkrement (c++) und Dekrement (c--) oder auch Stringverknüpfungen auf Schritt und Tritt. Dieses Kapitel zeigt und erklärt all die Java-Elemente, die für das Schreiben von einfachem prozeduralen Code typisch sind. Einige Entwickler sehen die genannten Operatoren skeptisch, weil es sich um verkürzende Schreibweisen handelt. Das vorliegende Buch möchte dagegen zeigen, dass kompakte Syntax und lesbarer Code gut zusammenpassen.

Umständlich geschriebener Code erscheint vielleicht Anfängern leichter fassbar, aber er ist für Übersichtlichkeit, Lesbarkeit und klare Strukturen schädlich. Lesbarkeit von Code wird nicht mit Ausführlichkeit und einem hohen Anteil von Leerzeilen erreicht, sondern durch die beständige Nutzung von immer gleichen Redewendungen. Diese werden sozusagen als elementarer Wortschatz in diesem Kapitel behandelt. Wenn sich ein Java-Neuling mit den häufigsten Redewendungen vertraut macht, hat er vermutlich einen einfacheren Einstieg, als wenn er sich durch alle Syntaxelemente mit gleichmäßiger Anstrengung hindurcharbeitet.

Der erste Teil der Java-Rundreise ist nur wenig mehr als ein Umherschweifen in hügeligem Gelände, um die Grundausrüstung kennenzulernen. Der zweite Teil erkundet dann ein größeres Gebiet und ist entsprechend anstrengender. Dazwischen erschließen wir uns angenehme Features wie die Lambdas, schwierige Tools wie die unvermeidbaren Exceptions, und schließlich auch die Philosophie, die Seele im Code, nämlich die Objektorientierung.

2.90 Prozent - Ein Plan und ein Stück Code - Was ein Entwickler wissen muss

Wenn ein Entwickler einen Plan hat, kann er beginnen, Code zu schreiben. Die Pläne des Entwicklers im Kleinen betreffen dabei immer diese beiden Aufgaben:

imageDas Organisieren von Code in Methoden

imageDas Schreiben des Codes einer einzelnen Methode.

Die Rede ist von Funktionen. Funktionen heißen in Java Methoden. Beide Bezeichnungen werden weitgehend synonym verwendet. Das Denken eines Entwicklers dreht sich um Methoden. Jeglicher Code in Java läuft in Methoden ab. Und jede Methode ist Teil einer Klasse. Um Code zu schreiben, muss ein Entwickler eine Klasse bereitstellen und in dieser oder einer anderen Klasse eine main()-Funktion. Denn um ein Programm in Java auszuführen, braucht man eine main()-Funktion. Mit deren Aufruf wird das Programm gestartet. Ein Kopfdruck an der richtigen Stelle in der Entwicklungsumgebung und die Eingabe des Namens ‚Experiment’ in einen Dialog der Entwicklungsumgebung erzeugen eine Klasse mit der gewünschten main()-Methode (siehe den ersten Abschnitt im Anhang, wenn Sie hier Unterstützung brauchen):

public class Experiment
{
// Dieser Bezeichner ist beliebig
public static void main(String[] args)
{
}
// main() jedoch muss es immer geben
}  

Ein weiterer Knopfdruck bringt die Klasse zur Ausführung. Das lässt uns natürlich kalt, denn sie tut ja nichts und folglich sehen wir auch nichts. Sollten Sie diese Klasse nicht mit einer Entwicklungsumgebung erzeugen, sondern vollständig selbst erstellen, dann achten Sie darauf, dass die Klasse in einem File mit dem Namen der Klasse und der Namenserweiterung .java steht. Die Klasse Experiment muss in dem Java-File Experiment.java gespeichert sein. Jede öffentliche Klasse muss in einem eigenen File ihres Namens stehen. Sie sollten zudem darauf achten, dass die Kopfzeile der main()-Funktion genau das gezeigte Aussehen hat. Wenn Sie vom Ablauf der Klasse etwas sehen wollen, dann stellen Sie Ausgabe-Statements wie das folgende in die main()-Funktion:

System.out.println(“Hello World”);

Den Button, dessen Betätigung dieses erste Programm zum Ablauf bringt, finden Sie ebenfalls in Ihrer Entwicklungsumgebung. In Eclipse ist dies: Run → Run As → Java Application. Wenn Sie nicht mit einer Entwicklungsumgebung arbeiten, müssen Sie das File Experiment.java zuerst in der Konsole übersetzen (javac Experiment.java). Dann können Sie den Code vom Interpreter ausführen lassen (java Experiment). Informationen über Compiler und Interpreter finden Sie im Anhang. Um sicherzustellen, dass Sie in der Lage sind, eine Klasse zu erstellen, gibt es im Anhang zudem ein kurzes Kapitel, das sich den möglichen Schwierigkeiten bei der ersten Klasse widmet. Denn ohne eigene Experimente, mit denen Sie die hier vorgestellten Beispiele und Codefragmente erproben, macht das Lesen dieses Buches wenig Sinn.

Ein Entwickler, der sich eine bevorstehende Codierung zurechtlegt, denkt entweder darüber nach, wie er den Code in Methoden aufteilt, oder wie er den Code einer einzelnen Methode schreibt. Im ersten Fall hat er etwas in dieser Art vor Augen:

public class Experiment
{
 
public static int getMyInput() {… } // Die geschweiften Klammern müssen
public static int calculate(int number) {… } // noch mit Code gefüllt
public static void writeItOut(int number) {… } // werden
public static void main(String[] args)
{
// Programmausführung beginnt hier
int number = getMyInput(); // Führe der Reihe nach diese drei
int result = calculate(number); // Methoden aus
writeItOut(result);  
} // Mit dem Ende von main() ist auch
} // Programmausführung beendet

Wir sehen hier ganz gut, wie der Entwickler sich die Sache denkt. Sein Plan nimmt Gestalt an und man kann den Plan lesen. Man muss sich das so vorstellen, dass wir hier zwar nur eine Klasse zeigen, dass in der Regel aber viele Klassen beteiligt sind, die allesamt sprechende Namen haben, und dass es in jeder dieser vielen Klassen viele Methoden mit ebenfalls sprechenden Namen gibt. Der Witz bei der Organisation von Code in Methoden liegt darin, dass der Code so aufgeteilt wird, dass die zu lösende Aufgabe in jeder einzelnen Methode möglichst übersichtlich und der Plan als Ganzes möglichst gut lesbar und zu verstehen ist.

Der ‚Plan als Ganzes’ ist dabei eine schöne Idee. Entwickler verfassen Pläne, wenn sie Software schreiben. Man sieht jedoch bald, dass es nicht ganz einfach ist, einen Plan zu machen und ihn aufzuschreiben. Die Schwierigkeiten beginnen bereits mit der Frage, wie ein aufgeschriebener Plan denn aussehen oder wann man ihn verfassen soll. Einen funktionierenden Plan zu erstellen, ist bedeutend schwieriger als die anschließende Umsetzung in Code. Planung und Codierung haben aber andererseits eine Menge miteinander zu tun. Sie verhalten sich etwa so wie Denken und Sprache. Das eine geht nicht ohne das andere. Niemand kann ohne fundierte Codekenntnisse einen vernünftigen Plan machen und umgekehrt ist eine Codierung ohne Plan genau das, wonach es manchmal aussieht – eben planlos.

In diesem Buch geht es sehr viel um Denken und Sprache und um Plan und Code. Man könnte auch sagen, es geht darum, wie man Software konstruiert. Für Konstruktionen braucht man mehr als die Syntax einiger Java-Statements. Wir brauchen dazu die Fähigkeit der Konzeption, das ist Planung, und wir brauchen die Fähigkeit der Umsetzung, das ist Coding. Und zudem brauchen wir eine gewisse Grundschnelligkeit. Es geht nicht nur darum, irgendwann einmal nach langer Zeit und vielen Anläufen alles in schöne Ordnung gebracht zu haben. Es geht auch um die Fähigkeit, mit einer hohen Produktivität zu konstruieren, also robuste Konstruktionen in kurzer Zeit zu erstellen. Planung und Coding und ihre Verbindung müssen so aufgesetzt sein, dass Konstruktionen rational und damit schnell, sehr schnell, durchgeführt werden können.

In diesem Hauptkapitel geht es in erster Linie um Coding. Rationalität beginnt jedoch - in kleinen Ansätzen - bereits hier. Wir müssen nicht jedes Syntaxelement in gleicher Breite beschreiben. Wir konzentrieren uns bei den Coding-Themen auf diejenige Auswahl von Syntax und Redewendungen, die geschätzte 90 Prozent des ‚durchschnittlichen’ Codes ausmachen. Wir betrachten dazu Tools wie Strings, Collections und Enums und eben elementare Syntax. Die 90-%-Regel gilt für elementare Syntax und Tools gleichermaßen. Mit einer Handvoll Tools erledigt man 90 Prozent der anfallenden Aufgaben in einer ‚durchschnittlichen’ Entwicklung. Mit der Beherrschung der wichtigsten Sprachmittel und einer guten Kombination aus Planung und Coding beginnt das minimale Konstruktionswissen, über das ein Entwickler verfügen sollte.

Sieht man nochmals auf die obige Klasse Experiment mit ihren Methoden, so verschwimmt die Grenze zwischen Planung und Code. Ist die Aufteilung in Methoden ein Mittel der Planung oder ein Mittel der Codierung? Tatsache ist, dass der Methodenaufruf eines der wichtigsten Elemente ist, um den Programmablauf zu kontrollieren. Und die Organisation von Code in Methoden ist ein elementares und zugleich wichtiges Engineering-Mittel. Wir behandeln Methoden und ihre Aufrufe deshalb als erstes. Der Java-Entwickler hat daneben aber auch alle anderen gängigen Sprachmittel zur Verfügung, die man in modernen prozeduralen und objektorientierten Sprachen kennt. Unter anderem sind das diese:

Methodenaufrufe. Um Leistungen an andere Methoden zu delegieren:

public static int perform()
{
int fromUser = getMyInput();
int result = calculate(fromUser);
writeItOut(result);
}

Bedingungen. Um Prüfungen durchzuführen:

public static int perform()
{
int fromUser = getMyInput();  
if (fromUser < 0) // Bedingung: Ist diese Zahl kleiner 0
writeItOut(“Info für Anwender – deine Eingabe: ” + fromUser);
}

Logische Operatoren. Um Bedingungen zu verknüpfen:

public static int perform()
{
int fromUser = getMyInput();  
if (fromUser == 0 && getResults() == null)
{
// Logisches Und
writeItOut(“Info für Anwender – null Eingabe”);
try {Thread.sleep(10_000);} catch(Throwable ignore) {}
System.exit();
}
}

Verzweigungen. Um mehrwertige Bedingungen zu behandeln:

public static int perform()
{
int fromUser = getMyInput();
switch(fromUser)
{
case -1:
writeItOut(“Info für Anwender”);
break;
case 0:
writeItOut(“Andere Kommentierung der Eingabe”);
break;
case 1:
writeItOut(“Ergebnis … ”);
break;
}
}

Iterationen. Um Elemente einer Menge zu durchlaufen:

public static int calculate(int number)
{
int[] array = getArray(number);
int index = 0;
while(index < array.length) // Iteriere über das Array
writeItOut(array[index++]);
}

Einige vorkommende Methoden wie getResults() oder getArray() sind hier nicht wesentlich und werden deshalb nicht weiter erläutert. In der Praxis müssen natürlich alle Methoden, die aufgerufen werden, an anderer Stelle vereinbart sein. Im weiteren Verlauf des Kapitels greifen wir die gezeigte Basissyntax auf, erläutern Regeln, auf die es ankommt, und zeigen an Standard-Situationen, die man häufig antrifft, was man tun und was man vermeiden sollte. Wir gehen dabei davon aus, dass die Leserinnen und Leser mit der grundsätzlichen Wirkungsweise einer Programmiersprache bereits halbwegs vertraut sind. Ein Java-Neuling, der von einer anderen Sprache her kommt, wie etwa Pascal, C oder C++, sollte mit dem gewählten Niveau gut zurecht kommen.

3.Methoden

3.1.Die Signatur von Methoden

Die Grundlage von elementarem Java sind Methoden. Methoden sind ein erster Ansatz von Engineering. Sie dienen dazu, Code zu organisieren und die zu erbringende Leistung in benennbare Einheiten aufzuteilen. Eine Methode besteht aus Methodenkopf und Methodenkörper:

public static int printLongArray(long[] array, String separator) // Methodenkopf
{ // Ab hier Methodenkörper
// Die Klasse, zu der diese Methode
} // gehört, ist nicht dargestellt

Der Methodenkopf enthält alle formalen Deklarationen zu einer Methode und legt Ein- und Ausgang fest. Er bestimmt die Sichtbarkeit (private oder public), den Returntyp und die Argumente. Die Sichtbarkeit von printLongArray() ist public, was bedeutet, dass die Methode auch außerhalb ihrer Klasse gesehen und zugegriffen werden kann. printLongArray() hat zwei Argumente, nämlich array und separator mit den Standardtypen long[] und String. Die Argumente sind der Eingang einer Methode. Sie legen fest, was an eine Methode bei ihrem Aufruf übergeben wird. Das Gegenstück dazu ist der Returnwert. Dieser ist das Ergebnis, das eine Methode nach ihrer Ausführung an den Aufrufer zurückliefert. Der Methodenkopf von printLongArray() legt fest, dass der Returnwert den elementaren Typ int hat. Die geordnete Liste der Argumente mit ihren Typen zusammen mit Methodenname, Returntyp und Sichtbarkeit nennt man auch die Signatur einer Methode.

Eine jede Methode ist Teil einer Klasse. Auch printLongArray() muss in irgendeiner Klasse vereinbart sein. Nehmen wir an, dass es sich dabei um die Klasse Experiment handelt, die wir bereits kennen. Wie die anderen Experiment-Methoden hat auch printLongArray() den Qualifier static. Java-Methoden werden nicht grundsätzlich als static vereinbart. Bis wir die Instanzmethoden kennenlernen, die nicht statisch sind (siehe dazu im Kapitel Objekte), sollten Sie Ihre Experimente jedoch mit statischen Methoden ausführen. Die Reihenfolge der Schlüsselworte public und static spielt übrigens keine Rolle. Beide Qualifier müssen aber vor dem Returntyp stehen. Wenn eine (statische) Methode von beliebigen anderen Klassen aus aufgerufen werden soll, so muss sie dazu public sein und der Methodenaufruf muss dabei durch den Klassennamen qualifiziert werden:

Experiment.printLongArray(…); // Aufruf über Klassengrenzen hinweg

Um eine statische Methode der eigenen Klasse aufzurufen, genügt es, den Namen der Methode zu nennen. Der Aufruf einer Methode muss in jedem Fall ihrer Signatur entsprechen. Dies bedeutet, dass alle Argumente mit dem richtigen Typ gegeben und in der richtigen Reihenfolge präsentiert werden müssen. Der Returnwert kann entgegen genommen werden oder auch nicht. Im ersteren Fall muss auch für ihn eine Variable mit dem richtigen Typ bereitgestellt werden. Hier ist eine kleine Reihe von Beispielstatements, welche die Aufrufregeln illustrieren:

public class Experiment
{
public static void main(String[] args)
{
long[] longArray = new long[100]; // Zu Arrays gibt es einen eigenen
Arrays.fill(longArray, 131313); // ausführlichen Abschnitt
printLongArray(longArray, " : "); // Klappt
printLongArray(null, null); // Okay für Compiler. Laufzeitfehler‼
int[] intArray = new int[100];  
printLongArray(intArray, " : "); // Error. int[] passt nicht auf long[]
printLongArray(longArray); // Error. Nur ein Argument statt zwei
printLongArray(longArray, 13); // Error. Zweiter Arg-Typ passt nicht
String result = printLongArray(longArray, " : "); // Error. Returntyp passt nicht
}  
public static int printLongArray(long[] array, String separator) { … }
}

Derartige im Kommentar markierte Fehler sind immer harmlos. Denn es handelt sich um Fehler, die der Compiler bemerkt. Der Compiler lehnt dann die Übersetzung des betreffenden Codes ab. Ein Fehler, den der Compiler bemerkt, ist deshalb harmlos, weil er vom Entwickler unmittelbar erkannt wird und behoben werden kann. Ein nicht-harmloser Fehler ist ein Fehler, der erst zur Laufzeit auftritt, alle Test übersteht, nur sporadisch auftritt und das erste Mal in Erscheinung tritt, wenn die Applikation gerade an 1000 Anwender verteilt worden ist.

Das Beispiel zeigt, dass beim Aufruf einer Methode die übergebenen Argumente in ihren Typen auf die deklarierten Argumente passen müssen. Ebenso muss der Returntyp zum Typ der aufnehmenden Variable passen. Dies ist in Java elementar. Der Compiler kann geringfügige Anpassungen automatisch vornehmen, etwa wenn durch den Typ eines Arguments ein long gefordert und beim Aufruf irgendein anderer ganzzahliger Typ wie short oder byte übergeben wird. Denn diese Typen lassen sich ohne Informationsverlust in ein long umwandeln (siehe unter Cast). Wenn beim Methodenaufruf die Erwartung an die Typen der Argumente und ihre Anzahl nicht erfüllt wird, lehnt der Compiler ab und übersetzt nicht. Da der Name der Methode nicht das einzige entscheidende Kriterium bei ihrem Aufruf ist, können verschiedene Methoden mit gleichem Namen vereinbart werden, wenn sie sich in Zahl, Reihenfolge oder Typ der Argumente unterscheiden. Zwei Methoden gleichen Namens in einer Klasse, die sich nur im Returntyp unterscheiden, sind dagegen nicht erlaubt.

Verschiedene Methoden können den gleichen Namen tragen, wenn sich ihre Argumente durch Anzahl, Typ oder Reihenfolge unterscheiden

Wenn mehrere Methoden mit gleichem Namen innerhalb einer Klasse vereinbart werden, so nennt man dies Overloading. Overloading (Überladen) ist etwas sehr Triviales. Gäbe es für Instanzmethoden nicht einen wichtigen ähnlichen Mechanismus, nämlich Overriding, so würde der Term ‚Overloading’ hier gar nicht erwähnt werden. Das folgende Beispiel zeigt einige mögliche Überladungen in der Klasse Experiment:

public class Experiment
{
public static void main(String[] args) {… }
public static int printLongArray(long[] array, String separator) {… }
public static int printLongArray(long[] array, String separator, boolean useSingleLine) {… }
public static void printLongArray(long[] array, String separator, short itemsPerLine) {… }
public static int printLongArray(int[] array, String separator) { … }
}

Wir sehen einfache Überladungen, die dadurch entstehen, dass die Zahl der Argumente variiert wird oder dass ein Argument einen anderen Typ erhält. Natürlich kann man Überladungen auch dadurch erreichen, dass man die Reihenfolge der Argumente vertauscht. Allerdings sollte für den Anwender der Methoden ohne Weiteres ersichtlich sein, was diese tun und worin ihr Unterschied liegt. Im obigen Beispiel gibt es da keinerlei Schwierigkeiten. Im folgenden Beispiel ist dagegen von außen nicht zu erkennen, was die beiden printLongArray()-Methoden unterscheidet:

public class Experiment
{
// Problematische Überladung
public static void main(String[] args) {… }
public static int printLongArray(int[] array, String separator) { … }
public static int printLongArray(String separator, int[] array) { … }
}

Die Möglichkeit, mehrere Methoden in einer Klasse mit gleichem Namen zu verfassen, ist nicht nur theoretisch gegeben, sondern wird in der Praxis ausgiebig genutzt. Es ist sehr hilfreich, nicht beständig neue Methodennamen erfinden zu müssen, wenn man mehrere Methoden mit einer ähnlichen Leistung anbieten möchte. Welche Namen würde man für die Menge der printLongArray()-Methoden im obigen Beispiel wählen, wenn Overloading nicht zulässig wäre. Einige von uns würden es sicher mit printLongArray1(), printLongArray2() und so fort versuchen. Bei der Anwendung dieser Methoden würde man dann den unangenehmen Effekt entdecken, dass man bei jedem Methodenaufruf erst mühsam herausfinden muss, welche Zahl, beziehungsweise welcher Name zu welcher Argumentkombination gehört. Von daher ist Overloading zwar kein großes Feature, aber sehr praktisch.

3.2.Werden Argumente und Returnwerte kopiert?

In einer der obigen Versionen von printLongArray() sind drei Argumente gegeben: ein Array, ein String und ein boolean. Was passiert mit diesen Argumenten beim Methodenaufruf? Werden die Argumente kopiert? Oder sieht die gerufene Methode das Original? Ist es für den Aufrufer relevant, wenn die gerufene Methode ein Argument ändert? Die Antwort auf diese Fragen liegt in dem Unterschied zwischen elementaren Typen wie boolean und wirklichen Objekten (Instanzen) wie String und Array. Wir beginnen diese wichtige Erläuterung mit einer Nonsense-Implementierung von printLongArray():

public static void main(String[] ignore)
{
// Dieses Array interessiert hier nicht
long[] longArray = new long[100]; // Erzeuge Array mit 100 Elementen
Arrays.fill(longArray, 131313); // Jedes Element hat nun diesen Wert
boolean singleLine = true;
String delimiter = " : ";
printLongArray(longArray, delimiter, singleLine); // Auf diesen Aufruf kommt es hier an
int test = array[0]; // 23. Dieser Wert wurde verändert
boolean flag = singleLine; // true. Dieser Wert ist unverändert
}
public static int printLongArray(long[] array, String separator, boolean useSingleLine)
{
array[0] = 23; // Änderung, die in main() sichtbar ist
array = new long[1]; // Änderung der Array-Referenz
useSingleLine = false; // Änderung eines elementaren Typs
separator.replace(“ ”, “.”); // ! Änderung eines Strings. Vorsicht !
}

Möglicherweise sind Sie ein ungeduldiger Mensch und möchten sofort wissen, was denn in Java ein boolean ist oder ein long oder was elementare Typen sind. Wenn Sie öfters von dieser Ungeduld getrieben werden, so gibt es für Sie zwei einfache Optionen. Diese heißen Glossar und Essentials. Beide sind Orte der Erklärung und Teil dieses Buches und befinden sich somit in Ihrem unmittelbaren Zugriff. Die genannten Fragen haben mit dem Typsystem von Java zu tun, das in einem späteren Teil dieses Buches behandelt wird. Eine kurze Übersicht ist hier hilfreich.

Es gibt in Java acht elementare Typen, die auch als primitive Typen bezeichnet werden. Diese umfassen die vier ganzzahligen Typen byte, short, int und long, die zwei Gleitkommatypen: float und double, den Typ char, um Einzelzeichen aufzunehmen, und schließlich den mit allen anderen inkompatiblen Typ boolean. Während sieben elementare Typen zumindest mit Zwang (Cast) ineinander umgewandelt werden können, hat boolean eine Sonderstellung. Eine boolean Variable kann nur die beiden Werte true und false (beides sind Schlüsselwörter) annehmen und kann in keinen anderen Typ umgewandelt werden.

Alle Größen in Java, die keinen elementaren Typ haben, sind Instanzen von Klassen. Während die elementaren Typen ein fester Teil der Sprache sind, sind Klassen im Prinzip selbst-vereinbarte Typen, die nach Belieben erzeugt und dem System Java hinzugefügt werden können. Obwohl man Klassen auch Benutzer-definierte Typen nennt, werden nicht alle Klassen vom Entwickler selbst bereitgestellt. Eine große Menge von Klassen wird unter der Bezeichnung ‚Standardklassen’ mit Java mitgeliefert und ist Bestandteil des JDK.

Variable haben in Java also entweder einen elementaren Typ oder sie verweisen auf Instanzen von Klassen. Im letzteren Fall werden sie auch als Referenzen bezeichnet. Eine Referenz ist ein Zeiger, ein Pointer. Typen, die nicht elementar sind, sind Referenztypen. Und damit sind wir schon wieder mitten im Thema dieses Kapitels. Denn ob eine Variable bei der Übergabe an eine Methode kopiert wird oder nicht, hängt davon ab, ob sie ein Referenztyp oder ein elementarer Typ ist.

Wir beginnen mit dem dritten Argument in obigem Beispiel. Dieses hat in main() die Bezeichnung singleLine und in printLongArray() die Bezeichnung useSingleLine. Da es sich um einen elementaren Typ handelt, wird diese Variable bei der Übergabe an eine Methode (oder bei der Rückgabe aus einer Methode) kopiert. Eine Methode erhält bei ihrem Aufruf für einen elementaren Wert quasi ein Stück Papier ausgehändigt, auf das der elementare Wert übertragen wurde. Sie kann mit diesem Wert anstellen, was sie möchte, ihn verändern oder ihn löschen, die rufende Methode bekommt davon nichts mit, denn es gibt den betreffenden Wert ja zweimal: einmal in der rufenden und einmal in der gerufenen Methode. Eine Veränderung von useSingleLine in print-LongArray() ist in der rufenden Methode main() folglich nicht zu sehen.

Anders ist dies bei der Übergabe von Referenzen beim Methodenaufruf. Eine Referenz ist ein Zeiger auf eine Instanz, der vier Bytes im Speicher belegt und damit ähnlich groß ist wie ein elementarer Wert, der ebenfalls nur einige Bytes benötigt. Und ebenso wie ein elementarer Wert wird auch eine Referenz bei der Übergabe an eine Methode kopiert. Aber die Instanz, auf welche die Referenz zeigt, wird nicht kopiert. Wenn eine Referenz an eine Methode übergeben wird, können wir uns diese als eine Drachenschnur vorstellen. Da die Referenz kopiert wird, gibt es zwei Drachenschnüre. Eine hat die gerufene und die andere hat die rufende Methode in der Hand. Und beide zeigen auf die gleiche Instanz. Wenn die gerufene Methode auf das Objekt zugreift, das am Ende der Drachenschnur hängt, und dieses verändert, so ist die Veränderung für die gerufene Methode sichtbar. Das erste Statement in printLongArray() macht genau dieses, es ändert das Array-Objekt, und zwar das Element mit dem Index 0. Diese Änderung ist anschließend in main() zu sehen.

Arrays sind in Java echte Objekte, auch wenn es sich um Arrays mit elementarem Elementtyp handelt. Variable, die Arrays enthalten, sind also Referenzvariable. Das zweite Statement in printLongArray() erzeugt ein neues long-Array und stellt dessen Referenz in die Variable array. Die Methode lässt also die übergebene Drachenschnur los und nimmt stattdessen eine andere Drachenschnur in die Hand, an deren Ende ein anderes Objekt hängt. Ab diesem Punkt ist das übergebene Array für printLongArray() nicht mehr zugreifbar. Die betreffende Referenz wurde ersetzt. Und die Bedeutung für die rufende Methode ist offensichtlich. Wenn eine Methode das übergebene Objekt loslässt, so ist dies für die rufende Methode nicht relevant. Sie bekommt davon nichts mit.

Betrachten wir nun das zweite Argument, den String separator, der in main() delimiter heißt. Die Variable separator ist ebenfalls eine Referenz. Das String-Objekt wird deshalb wie alle echten Objekt im Original übergeben und die Änderungen, die printLongArray() an separator vornimmt, müssten folglich auch in main() zu sehen sein. Doch ein String ist ein ganz besonderes Objekt. Er kann nicht wirklich verändert werden. Ein String ist unveränderbar. Wäre separator ein anderer Typ, dann würde der replace()-Aufruf das Objekt verändern (wenn es diese Methode dann gäbe) und die Änderung wäre in printLongArray() und in main() zu sehen. Bei einem String jedoch erzeugt eine Manipulation ein neues Objekt. Dieses neue Objekt enthält die betreffende Änderung und wird als Ergebnis des Methodenaufrufs zurückgegeben. Da printLongArray() dieses Ergebnis nicht auffängt, ist das gezeigte Statement überflüssig, denn es hat auf diese Weise weder in print-LongArray() noch in main() eine Wirkung. Richtig schreibt man Stringmanipulationen deshalb so:

separator = separator.replace(“ ”, “.”); // Dieses Statement hat Wirkung

Nun wird die von String.replace() gelieferte Drachenschnur aufgenommen und das Statement macht Sinn. Mehr zu diesem grundlegenden Thema der Referenzen gibt es weiter unten im Teilkapitel über Objekte. Hier halten wir nach dieser ausführlichen Behandlung einer sehr wichtigen Eigenschaft von Java fest, dass im Standardfall bei der Übergabe von Werten zwischen Methoden keine Kopien erzeugt, sondern Originale ausgetauscht werden. Die unbedeutende Ausnahme dabei betrifft die wenigen elementaren Typen, die es in Java gibt.

3.3.Returntyp und return-Statement

Eine Methode, die kein Ergebnis zurückliefert, hat den ‚Returntyp’ void. void ist ein ganz eigentümliches Ding. void ist nicht wirklich ein Typ, es handelt sich weder um einen elementaren Typ noch um einen Benutzer-definierten Typ. Aber void ist ein Schlüsselwort. Und dieses bedeutet so viel wie ‚leer’ und besagt, dass eine Methode nichts zurückgibt. Normale Methoden, die keinen Ergebniswert liefern, müssen als void deklariert werden. Im Falle von printLongArray() würde dies so aussehen:

public static void printLongArray(long[] array, String separator) {… }

Mit einer einzigen Ausnahme müssen alle benannten Methoden in Java den Typ ihres Returnwerts vereinbaren oder als void deklarieren werden. Die eine Ausnahme sind die Konstruktoren, denen wir bei anderer Gelegenheit wiederbegegnen. Eine Methode mit einem Returntyp ungleich void muss mindestens ein return-Statement enthalten. Hier sehen wir die Methode printLongArray() (jetzt wieder mit einem int-Ergebniswert) mit zwei return-Statements:

public static int printLongArray(long[] array, String separator)
{
if (array == null) // Siehe Bedingung
return 0; // Ein bedingtes return-Statement
for (long element : array) // Siehe Iterationen
System.out.print(element + separator); // Siehe Stringverknüpfung
return 1; // Noch ein return-Statement
}

Ein return-Statement beendet die Ausführung einer Methode und gibt den Ergebniswert an den Aufrufer zurück. Jedes return-Statement muss dabei dem vereinbarten Returntyp der Methode entsprechen, es muss also einen Wert des Typs zurückgeben, der im Methodenkopf deklariert ist.

Eine void-Methode, also eine Methode mit dem Returntyp void, kann ebenfalls return-Statements enthalten, muss aber nicht. Bei void-Methoden ist das return-Statement leer:

public static void printLongArray(long[] array, String separator)
{
// void-Methode mit return-Statements
if (array == null)
return; // Leeres return-Statement
for (long element : array)
System.out.print(element + separator);
return; // Der Compiler wird dies kritisieren }

Ein leeres return-Statement am Ende einer Methode ist natürlich überflüssig, da die Methode hier sowieso zurückkehrt. Der Compiler wird deshalb einen entsprechenden Kommentar dazu abgeben (eine Warnung). Eine void-Methode kommt auch ohne return-Statements aus:

public static void printLongArray(long[] array, String separator)
{
// Methode ohne return-Statement
for (long element : array)
System.out.print(element + separator);
}

3.4Aufteilung von Code in Methoden

Der springende Punkt an der Aufteilung von Code in Methoden besteht darin, den Code so zu organisieren, dass eine bestimmte Leistung nicht mehrmals geschrieben wird. In einem Programm, das mit long-Arrays hantiert und diese des Öfteren auf Konsole ausgibt, ist printLongArray() sicher hilfreich und vermeidet, dass die gleichen Ausgabe-Statements an mehreren Stellen im Code auftauchen. In den folgenden Zeilen sehen wir ein printLongArray(), das die Array-Elemente auf Zeilen verteilt, die ausgegebenen Zeilen zählt und diese Zahl als Ergebnis zurückgibt:

public static int printLongArray(long[] array, String separator, int itemsPerLine)
{
int lineCounter = 0;
int itemCounter = 0;
for (long element : array)
{
// Siehe Iterationen
System.out.print(element + separator);
itemCounter++; // Siehe Post-Inkrement
if (itemCounter == itemsPerLine) // Siehe Gleichheits-Operator
{
System.out.println(); // Schreibe einen Zeilenwechsel
lineCounter++; // Siehe Post-Inkrement
itemCounter = 0; // Setze den Zähler zurück
}
}
System.out.println();
return lineCounter;
}

Wenn – unabhängig vom genauen Inhalt - dieser Code nicht in einer Methode isoliert ist, dann steht er in leicht variierenden Fassungen an verschiedenen Stellen im Programm. Man spricht dann von redundantem Code. Bei einer Fehlerkorrektur oder anderen Änderungen arbeitet man an mehreren Stellen und hat entsprechend mehrfachen Aufwand.

Code kann bequem in Methoden aufgeteilt werden. Eine der Richtlinien dabei heißt: strikte Vermeidung von Code-Redundanz

Die Verteilung von Code auf viele Methoden ist für einen Entwickler nicht schwierig, sondern erleichtert ihm das Leben. Es ist bequem, eine bestimmte Codemenge in einer größeren Zahl von Methoden zu strukturieren, weil Code damit flexibler und leichter zu handhaben wird und weil er mit der Einordnung unter Methodennamen auch leichter zu erfassen ist. Ein Stück Code in eine Methode zu stecken, ist ein ganz einfacher Weg, dieses Stück Code zu benennen und es damit der Modellierung, dem Nachdenken oder einer Diskussion zugänglich zu machen.

Dass man eine bestimmte Leistung nicht mehrfach implementieren möchte, ist naheliegend. Die Verpackung von Leistung in entsprechend benannte Methoden hat aber noch andere Vorteile. Beispielsweise kann eine Methode durch Argumente konfiguriert werden, was bei einem Codestück, das in eine umfangreiche Statementfolge eingebettet ist, nicht möglich ist.

Gut geschriebener Code besteht aus einer Vielzahl kleiner und kleinster Methoden

Ein weiterer gewichtiger Punkt, der für die Aufteilung von Code in viele Methoden spricht, ist der Umfang der einzelnen Methoden. Je größer die Gesamtzahl aller Methoden ist, desto kürzer sind die einzelnen Methoden. Kurze Methoden sind uns sehr viel lieber als lange Methoden. Ein Programm mit wenigen aber sehr umfangreichen Methoden ist schwierig in Pflege, Wartung und Weiterentwicklung, weil man große Probleme hat, das Programm zu verstehen. Ein Programm mit vielen kleinen Methoden erhält allein schon durch die Methodenköpfe eine Dokumentierung, die eventuell gut gelesen und verstanden werden kann.

Entwickler müssen sich hin und wieder klar machen, welchen Spielraum sie durch die Gestaltung der Methoden und Methodenköpfe haben, in die sie den Code aufteilen. Die Methodenköpfe bestimmen das Vokabular, mit dem Code formuliert wird

Bei einem Programm, das durch viele kleine Methoden strukturiert ist, weiß man eventuell sehr schnell, in welcher Methode man Änderungen durchführen muss, um einen gewünschten Effekt zu erreichen. Bei sehr umfangreichen Methoden ist es dagegen äußerst schwierig, alle Auswirkungen einer Änderung zu erfassen. Man muss selbst bei kleinsten Korrekturen die Methode in ihrer ganzen Länge lesen und verstehen. Der Aufwand kann hierbei so groß sein, dass man irgendwann dazu übergeht, laufenden Code gar nicht mehr anzurühren. Dann ist man bei einer sehr teuren aber nicht seltenen Codeform angelangt.

Die Aufteilung in viele kleine Methoden ist auch für das Verfahren der Protokollierung vorteilhaft. Oftmals möchte man während einer Entwicklung, dass man von dem ablaufenden Code mehr sieht als nur die Ausgabe eines Resultats. Ein einfaches Verfahren protokolliert das Betreten und Verlassen von ausgewählten Methoden. Wenn das ganze Programm nur aus wenigen Methoden besteht, sieht man bei diesem Ansatz naturgemäß nur wenig. Wenn es dagegen viele kleine Methoden gibt, erhält man eine reichhaltige Wiedergabe der internen Abläufe. Hier sehen wir eine einfache Form der Protokollierung:

public static int printLongArray(long[] array, String separator)
{
// Methode mit Protokollierung
System.out.println("Start of printLongArray "); // Das ist so nicht ideal
System.out.println("End of printLongArray ");
return lineCounter;
}

Diese simple Protokollierungstechnik hat allerdings einen großen Hacken, der sich mit etwas Praxis deutlich und schnell bemerkbar macht. Eine Protokollierung dieser Art lässt sich nämlich nicht mehr abstellen. Es ist jedoch ganz wesentlich, dass man Codeteile nach einiger Zeit der Erprobung gezielt aus der Protokollierung wieder heraus nehmen kann. Hier gibt es wesentlich raffiniertere Ansätze, denen wir noch begegnen werden.

3.5.Variabel lange Argumentlisten

„Einer Versuchung sollte man nachgeben.
Wer weiß, wann sie wiederkommt.“

Oscar Wilde

Eine besondere Art der Vereinbarung von Argumenten findet man bei Methoden mit variabel langen Argumentlisten (Varargs). Im Aufruf sehen diese so aus:

print();
print(1, 2);
print(2, 3, 5, 7, 11, 13);

Die gezeigte print()-Methode gibt es nur einmal. Um eine Methode mit variabel vielen Argumenten zu vereinbaren, wird eine besondere Syntax verwendet, nämlich drei aufeinander folgende Punkte. Hier sehen wir eine Vereinbarung einer variabel langen Liste von int-Argumenten:

public static void print(int … array)
{
// Variabel viele int-Argumente
int count = array.length; // int… wird in int[] übersetzt
int firstArg;
if (count > 0)
firstArg = array[0];
}

Die Methode print() kann mit beliebig vielen int-Argumenten aufgerufen werden, auch ohne Argumente. Der Compiler präsentiert innerhalb der Methode eine variabel lange Argumentliste als ein Array (siehe zu Arrays weiter hinten), wobei der Argumenttyp zum Elementtyp des Arrays wird. Der Aufrufer kann an print() aber auch gleich ein Array übergeben:

int[] arguments = new int[0] print(arguments); // Gültiger Aufruf von Vararg-Meth.

Die Gleichstellung dieser beiden Methoden aus Sicht des Compilers:

public static void print(int … array) {… }
public static void print(int[] array) {… } // Error. Compiler sieht das als gleich

führt dazu, dass es nicht erlaubt ist, in einer Klasse zwei Methoden zu vereinbaren, deren einziger Unterschied in einer variabel langen Argumentliste und einem Array mit dem selben Elementtyp besteht. Man beachte im Übrigen, dass es trotz dieser Gleichstellung durch den Compiler nicht möglich ist, die print()-Methode aus dem letzten Statement mit den eingangs gezeigten print()-Beispielen aufzurufen. Eine Methode mit einem Array-Argument kann nur mit einem Array aufgerufen werden. Eine Methode, an die nahezu beliebige Argument-Kombinationen übergeben werden können, ist die folgende:

public static void test(Object … array)
{
System.out.println(array.length);
}

Auch die eingangs gezeigten print()-Aufrufe würden von dieser Methode akzeptiert werden, wenn man die Aufrufe in test() umbenennen würde. Denn der Compiler wandelt die int-Argumente in Object-Instanzen um. Diese Umwandlung ist jedoch kein Cast, sondern Boxing. Dabei wird für jedes int ein echtes Objekt erzeugt, in das der elementaren Wert hineingestellt wird. Genauer wird dies als Auto-Boxing bezeichnet, weil der Compiler das Boxing automatisch anwendet, wenn es benötigt wird. Interessant ist, dass der Compiler diese folgende test()-Methode neben der obigen zulässt (Overloading):

public static void test(int … array)
{
System.out.println(array.length);
}

Erprobt man nun diese beiden test()-Methoden mit dem folgenden Aufruf:

test(new int[0]);

so wählt der Compiler test(int …) und wir sehen die Ausgabe 0 auf der Konsole. Das haben wir so erwartet. Kommentieren wir nun aber diese Methode aus, so wählt der Compiler natürlich die verbleibende Methode test(Object …) und wir sehen die Ausgabe 1 auf der Konsole. Der Compiler nimmt das Argument int[0] jetzt als Object und sieht somit ein Element des Arrays Object[], was entsprechend protokolliert wird.

Nun sind wir mit der Syntax-Erprobung der Varargs in Bereiche geraten, an die wir uns eigentlich nicht begeben wollten. Deshalb eine deutliche Warnung: Auch wenn der Compiler die beiden test()-Methoden nebeneinander akzeptiert, schreiben Sie niemals Code in dieser Art! Wenn Sie darüber grübeln müssen, welche Methode der Compiler mit einem Aufruf: test(new int[0]); verbindet, dann ist dies bereits schlecht. Wir wollen immer klaren und einfachen Code und keine Syntax-Gymnastik. Wenn wir hier die Syntax zu variablen Argument-Listen etwas ausgelotet haben, so nur deshalb, weil wir hin und wieder spontan dem Reiz des Unfugs nachgeben.

Methoden mit variabel langen Argumentlisten sind in Praxis auf Grund der Flexibilität, die sie bieten, nicht ganz selten (aber auch nicht häufig) und sind einfach zu verwenden. Variabel lange Argumentlisten können mit anderen Argumenten gemeinsam auftreten, aber es gibt in einer Methode maximal nur eine variabel lange Argumentliste und diese steht immer am Ende der Reihe der Argumente:

public static void print(String text, int … array) {… } // Erlaubt
public static void print(int … array, String text) {… } // Error

3.6.Argumentprüfung

Ein letzter Punkt zu Methoden im Allgemeinen betrifft die Argumentprüfung. Public Methoden sollten ihre Argumente prüfen, bevor sie mit ihrer eigentlichen Arbeit beginnen. Hier sehen wir, wie eine einfache Prüfung in printLongArray() aussehen könnte:

public static void printLongArray(long[] array, String separator)
{
if (array == null)
throw new NullPointerException("null is not allowed for the array argument");
for (long element : array) // Ohne Prüfung fliegt die Exception hier
System.out.print(element + separator);
}

Fehlerhafte Argumente werden mit Exceptions quittiert (mehr zu Exceptions in einem der folgenden Abschnitte). Die null-Überprüfung von separator kann hier unterbleiben, wenn es in Ordnung ist, dass eine für separator übergebene null in der Ausgabe als null-String erscheint. Wenn dagegen array als null übergeben und keine Prüfung durchgeführt wird, fliegt die Exception im for-Statement. Es sieht so aus, als würde es keinen Unterschied machen, wo die Exception fliegt. Tatsächlich ist der Unterschied jedoch groß. Wenn eine Exception im for-Statement auftritt, so ist dies die Schuld von printLongArray(). Fliegt die Exception in der Argumentprüfung, so ist dies die Schuld des Aufrufers. Argumentprüfungen helfen also bei Fehlerlokalisierung und Ursachenforschung. Argumentprüfung umfasst meistens mehr als nur die hier gezeigte Prüfung auf null. Public Methoden haben in der Regel eine Spezifikation, welchen Eingang sie erwarten, und sie testen ihre Argumente entsprechend diesen Vorgaben.

Wir kennen bisher nur public Methoden. Wichtig im Zusammenhang mit der Argumentprüfung ist, dass diese auf public Methoden beschränkt ist. Bei privaten Methoden, die ja nur innerhalb ihrer Klasse aufgerufen werden können, ist eine Argumentprüfung nicht notwendig beziehungsweise sinnvoll, da bei diesen Aufrufen nur die eine Klasse beteiligt ist und eine Fehlerzuordnung sich daher erübrigt.

4.Lokale Variable, Blöcke, Scope und final

Das ist gültiger Java-Code:

public void emptyBlock()
{
{
}
 
}

Ein Code-Block wird mit geschweiften Klammern eröffnet und wieder geschlossen. Er fasst in der Regel eine Folge von Statements zu einer Einheit zusammen. Und er kann überall dort vereinbart werden, wo auch ein anderes Statement geschrieben werden darf.

Blöcke sind ein elementares Mittel, um Einheiten und Strukturen zu bilden. Selbst eine Methode kann als benannter (und aufrufbarer) Block aufgefasst werden. Die folgenden Beispielzeilen, die wir schon kennen, zeigen neben dem Methodenblock zwei weitere Blöcke. Der erste wird von einem for-Statement kontrolliert, der zweite von einem if-Statement:

public static int printLongArray(long[] array, String separator)
{
int lineCounter = 0; // Variablenvereinbarung lineCounter
int counter = 0; // Variablenvereinbarung counter
for (long element : array) // Variablenvereinbarung element
{
System.out.print(element + separator);
if (++counter == 20)
{
// Pre-Inkrement, Magic-Number
System.out.println();
lineCounter++; // Post-Inkrement
counter = 0;
}
}
System.out.println();
return lineCounter;
}

Der äußere Block wird für alle Elemente des long-Arrays durchlaufen. Der innere Block wird nur dann durchlaufen, wenn die Variable counter die gegebene Bedingung erfüllt und den Wert 20 annimmt. Die Bildung von Einheiten aus aufeinander folgenden Statements, um sie gemeinsam in eine Kontrollstruktur zu stellen, ist der Hauptzweck von Blöcken. Blöcke sind ein unkompliziertes Hilfsmittel. Die einzige Komplikation betrifft den Gültigkeitsbereich von Variablen. Zudem gilt die eher triviale Regel, dass Blöcke sich nicht nur teilweise überschneiden dürfen. Blöcke können beliebig ineinander gestellt werden. Aber jeder Block muss grundsätzlich in dem umgebenden Block wieder geschlossen werden, in dem er begonnen wird.

Variable können überall in einem Block beziehungsweise in einer Methode vereinbart werden. Es gibt keinen Grund, alle Variablen, die in einer Methode benötigt werden, gleich in den ersten Zeilen zu deklarieren. Im Gegenteil, Variable sollten erst dort vereinbart werden, wo sie gebraucht werden. Variable haben einen Gültigkeitsbereich, der auch Scope genannt wird. Dieser reicht von der Zeile, in der eine Variable deklariert wird, bis zum Ende ihres Blocks (also bis zum Ende des Blocks, in dem sie vereinbart wird).

Woran erkennt man die Vereinbarung einer Variablen? Ein Variablen-Statement beginnt immer mit einer Typangabe, also mit Angaben wie long oder String, dann folgt der Name der Variablen und ein Semikolon. Zwischen Namen und Semikolon kann sich die Zuweisung eines Wertes befinden. Wenn eine Variable ohne initialen Wert vereinbart wird, hat sie bis zur ersten Zuweisung den 0-Wert ihres Typs. Bei boolean ist dies false, bei den anderen elementaren Typen 0, beziehungsweise 0.0, und bei den Referenztypen null. Häufig wird Variablen in ihrer Vereinbarung ein 0-Wert dennoch explizit zugewiesen. Dies geschieht zuallererst der Klarheit zuliebe, dann um bei späteren Bearbeitern des Codes die Unsicherheit zu vermeiden, ob die Initialisierung übersehen wurde, und schließlich um dem Compiler zuvorzukommen, der manchmal eine scheinbar vergessene Initialisierung anmahnt.

In printLongArray() haben wir fünf Variable gegeben, genau genommen sind es drei lokale Variable und zwei Argumente. Aber wir unterscheiden (hier) nicht zwischen lokalen Variablen und Argumenten, denn Argumente haben den gleichen Status wie lokale Variable (sie existieren im ablaufenden Programm nur in der Zeitspanne, in der die betreffende Methode durchlaufen wird). Lokale Variable sind Variable, die innerhalb von Methoden vereinbart werden. Die fünf lokalen Variablen im Beispiel oben sind also: array, separator, lineCounter, counter und element. Der Scope von vier dieser Variablen erstreckt sich dabei bis zum Ende von printLongArray(). Einzig element ist nur innerhalb des Blocks der for-Schleife gültig. Wir können element hinter der for-Schleife nicht mehr zugreifen. Würde man auch die Variable lineCounter innerhalb der for-Schleife vereinbaren, könnte man sie im return-Statement nicht verwenden:

for (long element : array)
{
int lineCounter = 0; // Demonstration eines kleinen Scopes
} // Ende des Scopes von lineCounter
return lineCounter; // Error. lineCounter ist nicht sichtbar

Eine lokale Variable kann außer in ihrem eigenen Block auch in allen anderen Blöcken verwendet werden, die sich in ihrem eigenen Block und hinter ihrer Deklaration befinden. Für die Statements nach ihrem Block ist eine lokale Variable allerdings nicht mehr vereinbart. Dies bedeutet, dass ihr Name dort wieder verwendet werden darf. Innerhalb des Scopes einer lokalen Variablen darf eine zweite lokale Variable mit dem gleichen Namen dagegen nicht vereinbart werden. Das folgende Beispiel zeigt in Bezug auf Scope und Variablenbenennung gültigen Code:

for(int i = 0; i < 2;) {;} // Der Scope von erstem i endet hier
for(int i = 0; i < 2;) {;} // Okay? Die Namensgleichheit schon

Spaßigerweise meldet der Compiler für die zweite for-Schleife einen Fehler, wenn man in der ersten for-Schleife die Bedingung (i < 2) weglässt (Unreachable Code). So, wie die beiden Schleifen jetzt geschrieben sind, ist die zweite Schleife immer noch nicht erreichbar, aber der Compiler ist zufrieden. Dies erinnert nebenbei daran, dass die Fähigkeiten von Compilern differieren und sich zudem im Laufe der Zeit wandeln. Die Syntaxregeln der Sprache sind jedoch auf allen Systemen die gleichen.

Es ist möglich, Variable durch das Schlüsselwort final zu qualifizieren. Einer final Variablen wird wie anderen Variablen ein initialer Wert zugewiesen. Danach kann ihr aber kein weiterer Wert mehr zugewiesen werden. Im folgenden deklarieren wir einige der Variablen in printLongArray() als final und sehen uns an, was das bedeutet:

public static int printLongArray(final long[] array, final String separator)
{
final int lineCounter;
int counter = 0;
lineCounter = 0; // Okay. Wird erst hier initialisiert
for (final long element : array) // Okay
{
System.out.print(element + separator);
lineCounter++; … // Error. lineCounter nicht änderbar
}
array[0] = 12345; // Okay
return lineCounter;
}

Die final-Setzung von array, separator und element hat keine Auswirkungen auf den bisherigen Code, da diesen Variablen kein neuer Wert zugewiesen wird. Diese Variablen werden im Code nicht verändert. Die final-Deklaration kann man hier deshalb mit der Motivation vornehmen, genau dies auszudrücken und um zugleich diese Variablen vor einer versehentlichen Neu-Zuweisung zu schützen.

Unmittelbar vor dem return-Statement haben wir das erste Element von array verändert. Der Compiler erlaubt dies, obwohl array final ist. final-Objekte können verändert werden. final bedeutet nicht in erster Linie Konstanz, sondern dass an final-Variable nach ihrer Initialisierung nicht erneut zugewiesen werden kann. Bei Variablen mit elementarem Typ erreicht man eine Wertveränderung jedoch nur durch Zuweisung. Bei diesen Variablen ist final deshalb gleichbedeutend mit konstant.

Die final-Deklaration von lineCounter im letzten Beispiel verhindert, dass printLongArray() weiterhin korrekt funktioniert, denn der Zähler kann nun nicht mehr verändert werden. Das Beispiel zeigt zudem, dass der Compiler schlau genug ist, die Initialisierung, die erst zwei Zeilen nach der Deklaration erfolgt, zu erlauben, während er eine weitere Zuweisung ablehnt. Betrachten wir nun eine weitere Variable in printLongArray(), die wir ebenfalls als final vereinbaren:

public static int printLongArray(long[] array, String separator)
{
final int ITEMS_PER_LINE = 20; // Okay. Aber nicht wirklich gut
if (counter == ITEMS_PER_LINE) {… } …
}

Da final Variable mit elementarem Typ in keiner Weise mehr verändert werden können, können wir sie auch als Konstanten auffassen. Nach einer verbreiteten Konvention, die man tunlichst beachten sollte, sind Konstanten, wie hier zu sehen, an ihrer vollständigen Großschreibung zu erkennen. Warum aber sollte man die Konstante ITEMS_PER_LINE überhaupt verwenden? Hierzu gibt es zwei gute Gründe. Der erste ist, dass wir keine Magic-Numbers wollen. Ein Code wie dieser:

if (counter == 20) {… } // Magic-Number. Vermeiden!

kontrolliert den zugeordneten Block mit einer zufällig oder magisch erscheinenden Zahl, für die wir an dieser Stelle nicht erkennen können, warum genau diese Größe gewählt wurde. Hier ist die Zahl 20 nicht so entscheidend. Übertragen Sie das Ganze jedoch einmal in Bereiche aus Ihrem Arbeitsumfeld. Irgendwo in den Tiefen des Codes taucht plötzlich die Zahl 141 auf. Warum genau 141, warum nicht 140 oder 130? Mit ‚magic’ soll ausgedrückt werden, dass ein bestimmter Wert (Zahlenwert) zwar begründet sein kann, dass wir aber den Zusammenhang nicht sehen können. Der zweite Grund ist, dass die Konstante bereits durch ihren Namen den Zweck des if-Statements beschreibt, der mit einer bloßen Zahl so nicht zu erkennen ist. Für uns ist im Beispiel bereits klar, was an dieser Stelle kontrolliert wird und welchen Zweck die betreffende Zahl hat. In Codeteilen, die Ihnen neu sind, muss das keineswegs so sein. Die Verwendung einer Konstante statt einer bloßen Zahl trägt dazu bei, dass Code besser verstanden werden kann.

Die Ersetzung von Magic-Numbers durch Konstanten weist in die Richtung eines Coding-Stils, der in diesem Buch sehr nachdrücklich propagiert wird. Wir wollen nach Möglichkeit flüssig lesbaren Code, der ohne weitere Zutaten für sich alleine verständlich ist. Dazu gehören sorgfältig gewählte Namen und Bezeichner und Formulierungen wie diese:

if (counter == ITEMS_PER_LINE) {… }

Was wir nicht wollen, ist ein Code, der wie dieser aussieht:

if(ctr == 20)
ftp();

Nun gehen wir noch einmal einige Zeilen zurück: Was stört uns oben an der Deklaration von ITEMS_PER_LINE? Was hier nicht in Ordnung ist, ist der Ort, an der die Konstante vereinbart wird. Wir möchten im Allgemeinen die Parameter, die irgendeinen Aspekt der Applikation bestimmen, nicht irgendwo im Code verstreut, sondern an zentraler Stelle versammelt haben. Für die Konstante ITEMS_PER_LINE wäre es deshalb besser, wenn sie nicht innerhalb einer Methode vereinbart wäre, sondern an einer mehr hervorgehobenen Stelle. Darauf werden wir wieder zurückkommen, wenn wir mehr über die Möglichkeiten von Klassen und Interfaces wissen.

Eine gute Alternative für printLongArray() wäre es, die Einstellung der auszugebenden Elemente pro Zeile über ein Argument festzulegen:

public static int printLongArray(final long[] array, final String separator, final int itemsPerLine) { …}

Diese Alternative bewahrt uns allerdings nicht vor der Aufgabe, (an anderer Stelle) darüber nachzudenken, wo man am besten Konstanten vereinbart. Denn in der Praxis ist man oft in der Situation, dass mehrere überladene Versionen einer Methode nebeneinander bestehen. Die eine wird über Argumente konfiguriert, die anderen arbeiten mit Defaultwerten. Und Defaultwerte wiederum sind Konstanten, die irgendwo vereinbart sein müssen:

public static int printLongArray(final long[] array, final String separator, final int itemsPerLine) { …}
public static int printLongArray(final long[] array, final String sep) { …} // Eventuell mit Defaultwert
public static int printLongArray(final long[] array) { …} // Eventuell mit Defaultwerten

Dass im Normalfall Konstanten nicht innerhalb von Methoden deklariert werden sollen, hat auch mit einer grundsätzlichen Haltung gegenüber Methoden zu tun. Wenn Methoden wie printLongArray() erst einmal erstellt sind, wollen wir uns nicht mehr mit ihrer Implementierung befassen. Wir sehen ihrer Deklaration an, was sie leisten, und wir verwenden sie entsprechend. Aber wir kramen nicht mehr in ihren Implementierungen herum und drehen dort an Knöpfen und Konstanten. Eine Methode wird zusammen mit ihren Argumenten so verständlich formuliert, dass sie problemlos verwendet werden kann und dass es keinen weiteren Bedarf mehr gibt, sich mit ihrem Innenleben auseinander zu setzen.

Und schließlich noch ein letzter Punkt zu Codeblöcken. Bei Blöcken, die nur ein einziges Statement umfassen wie dieser:

for (long element : array)
{
System.out.print(element);
}

können die geschweiften Klammern auch weggelassen werden:

for (long element : array)
System.out.print(element);

Dieses Weglassen der geschweiften Klammern nutzen wir häufig, um Code kompakt zu schreiben.

5.Die Grundregel für zusammengesetzte Ausdrücke

Der folgende Codeabschnitt zeigt die Grundregel zum Aufbau von Ausdrücken aus Teilausdrücken:

int i = 0;
boolean test = (i = getNumber()) > 0; // Kann in die unten stehenden Teile aufgelöst werden

Zunächst wird der Teilausdruck in der Klammer ausgewertet. Das ist: i = getNumber(). Als erstes wird dabei die Funktion getNumber() ausgeführt. Sie muss ein Ergebnis liefern, das von void verschieden ist, sonst könnte sie nicht in einer Zuweisung stehen. Im Programmablauf ersetzt nun das Ergebnis der Funktion den Aufruf der Funktion. Wir bezeichnen dieses Ergebnis hier (in der Logik der Erklärung) als ergebnis1 und die anderen Zwischenergebnisse als ergebnis2 und ergebnis3. Im Programmcode findet man diese Zwischenergebnisse natürlich nicht:

getNumber() // PseudocodeWird ersetzt durchergebnis1
i = ergebnis1 // PseudocodeWird ersetzt durchergebnis2
ergebnis2 > 0 // PseudocodeWird ersetzt durchergebnis3
test = ergebnis3 // Pseudocode

Die Abarbeitung des obigen Ausdrucks im Programmablauf erfolgt in diesen vier Teilausdrücken und in dieser Reihenfolge. Jeder der vier Teilausdrücke hat ein Ergebnis. Das Ergebnis einer Zuweisung ist der Wert, der zugewiesen wird. Im Einzelnen passiert folgendes:

-Es wird die Funktion getNumber() aufgerufen

-Das Ergebnis von getNumber() wird an i zugewiesen

-Das Ergebnis der Zuweisung wird mit 0 verglichen.

-Das Ergebnis des Vergleichs wird an test zugewiesen.

Der gesamte Ausdruck besteht aus mehreren ineinander gestellten Teilausdrücken. Nach den Regeln von Operatorvorrang, Assoziativität und Klammersetzung wird der Teilausdruck bestimmt, der zuerst ausgeführt wird. Dann wird der Teilausdruck durch sein Ergebnis ersetzt, dann der nächste Teilausdruck abarbeitet und durch sein Ergebnis ersetzt und so fort.

Unabhängig davon wie komplex oder wie einfach Ausdrücke aufgebaut sind, hat man es in Java häufig mit Teilausdrücken in umfassenderen Ausdrücken zu tun. Für deren Auflösung gilt die folgende elementare Regel:

Jeder Teilausdruck und jeder Methodenaufruf wird im Programmablauf durch seinen Ergebniswert ersetzt

Diese Regel kennt keine Ausnahmen. Der Entwickler kann in jeder Situation auf die Wirksamkeit dieser Regel bauen.

Was sagt der Operatorvorrang zu obigem Ausdruck? Der Ausdruck enthält zwei Zuweisungen und einen Vergleichsoperator. Zuweisungen haben einen sehr niedrigen Operatorvorrang (nur der Lambda-Operator ist noch niedriger). Sie werden folglich als letzte ausgeführt. Vergleiche haben einen relativ hohen Operatorvorrang. Ausdrücke in Klammern erhalten Vorrang vor allen anderen Operationen. Also wird in obigem Ausdruck zuerst die geklammerte Zuweisung ausgeführt, dann der Vergleich und dann die zweite Zuweisung. Die Auflistung der Operatoren und die Regeln zum Vorrang findet man in einem der folgenden Abschnitte. Wenn Sie eine Abhandlung in drei Minuten bevorzugen, dann merken Sie sich einfach dies:

-Unäre Operatoren sind solche mit nur einem Operanden. In index++ ist ++ der Operator und index der eine Operand. Binäre Operatoren (das sind die meisten) haben zwei Operanden.

-Die unären Operatoren stehen ganz oben im Operatorvorrang, sie haben sehr hohe Priorität. Noch höher stehen nur Elementzugriff mit Indexklammern [] und Funktionsaufruf f().

-Der Conditional-Operator steht sehr weit unten. Noch weiter unten stehen nur noch der Zuweisungsoperator und seine verschiedenen Kombinationen und der Lambda-Operator.

-Ebenfalls sehr weit unten, aber über dem Conditional-Operator, stehen die logischen Operatoren.

-Die arithmetischen Punkt-Operatoren (* /) haben wie beim gewöhnlichen Rechnen eine höhere Priorität als die arithmetischen Strich-Operatoren (+ -).

Außer dem Conditional- und dem Lambda-Operator steht hier vermutlich nichts, das Sie noch nicht kennen. Und sehr viel mehr muss man sich zum Operatorvorrang auch nicht merken. Wenn Unsicherheit über einen Operator besteht, schlägt man natürlich nach.

Was ist Operator-Assoziativität? Das ist ein großes Wort für eine einfache Regel. Diese klärt, welcher Operator Vorrang hat, wenn mehrere gleiche Operatoren nebeneinander stehen. So ist beispielsweise der Zuweisungsoperator rechts-assoziativ. Bei einer Aufeinanderfolge mehrerer Zuweisungen wird diejenige zuerst ausgewertet, die ganz rechts steht:

int k, l, m = 10;
k = l = m = 0; // Auswertung von rechts nach links: rechts-assoziativ
k = (l = (m = 0)); // Die Klammerung verdeutlicht die Auswertungsrichtung

Hier wird zuerst die Zuweisung der 0 an m ausgeführt. Dann wird das Ergebnis (0) an l zugewiesen und schließlich wird die ganz links stehende Zuweisung ausgeführt.

Zur Assoziativität merkt man sich, dass im Wesentlichen Zuweisungen (wie wir gerade gesehen haben) und unäre Operatoren rechts-assoziativ sind und nahezu der gesamte Rest links-assoziativ. Das ist zwar nicht ganz richtig, aber fast, und die Abweichungen spielen keine wirkliche Rolle.

Wie kompakt soll man Ausdrücke schreiben? Das Kriterium ist der durchschnittlich erfahrene Entwickler. Wenn dieser bei einem Ausdruck ins Stolpern kommt und ihn nicht flüssig lesen kann, dann ist der Ausdruck nicht in Ordnung. Der Java-Anfänger spielt bei dieser Orientierung definitiv keine Rolle. Der obige Ausdruck mit getNumber() ist nach dem gerade gegebenen Kriterium völlig flüssig lesbar. Ein durchschnittlich erfahrener Entwickler sieht mühelos und auf den ersten Blick, was in diesem Ausdruck passiert.

6.Weitere elementare Regeln und Begriffe

Es gibt in Java einige grundlegende Syntax-Prinzipien. Deren Kenntnis erleichtert das Erlernen der Sprache. Betrachten Sie die folgenden Punkte als Orientierung und als eine Vorab-Auflistung von elementaren Aussagen zu Java. Manche davon sind einfach. Andere nutzen hier vielleicht nur jenen, die schon mehr über Java wissen.

imageJava ist Case-sensitive: Groß- und Kleinschreibung muss unterschieden werden. nextpart und next-Part sind zwei verschiedene Bezeichner.

imageKlassen: Jeglicher Javacode befindet sich innerhalb von Klassen. Nichts, was für eine Klasse wesentlich ist, wird außerhalb dieser Klasse vereinbart.

imageFunktionen: In den Klassen befinden sich Funktionen - kenntlich an runden Argumentklammern. Einzig Lambdas und die seltenen Initializer sind namenlose Codeblöcke/Funktionen, die nicht notwendig Argumentklammern haben.

imageMethoden: Die beiden Begriffe: ‚Funktion’ und ‚Methode’ sind in Java synonym. Sie bezeichnen dasselbe. Anders als in C++ gibt es zwischen ihnen keinen Unterschied. Diese Begriffsverwendung ist pure Konvention. Niemand hat sich hier den Kopf zerbrochen.

imageBedeutungsgleiche Bezeichnungen: Darüber hinaus können auch die folgenden Begriffe: Operation, Funktion, Methode, Nachricht weitgehend als bedeutungsgleich angesehen werden. Unter "Nachricht versenden" versteht man den Aufruf einer Methode für ein Objekt.

imageWirkung und Ergebnis: Eine Funktion tut etwas – das ist ihre Wirkung – und sie hat (meistens) ein Ergebnis. Wirkung und Ergebnis einer Funktion sind zwei verschiedene Dinge. Die Wirkung von i++ und ++i ist beispielsweise gleich, der Ergebniswert ist jedoch verschieden.

imageOperatoren sind Funktionen: In Java gibt es wie in C++ zahlreiche Operatoren (wie etwa + oder ++). Man sollte Operatoren und Funktionen als äquivalent ansehen. Beide haben Wirkung und Ergebniswert und in beiden Fällen wird der Aufruf durch das Ergebnis ersetzt. Operatoren kann man als Funktionen ansehen, die über eine Kurzschreibweise aufgerufen werden.

imageVariable: Java kennt verschiedene Arten von Variablen:

Lokale Variable: Diese werden innerhalb von Methoden vereinbart. Ihr Scope ist die umgebende Methode, beziehungsweise der umgebende Block.

Argumente: Diese haben den Status von lokalen Variablen. Ihr Scope ist ihre Methode - also die Methode, deren Dateneingang sie beschreiben.

Instanzvariable: Diese werden innerhalb von Klassen als Teil der Instanzen dieser Klasse vereinbart. Ihr Scope ist ihre Klasse. Sie können nur mit Bezug zu Instanzen zugegriffen werden.

Klassenvariable: Auch diese werden innerhalb von Klassen vereinbart und ihr Scope ist ebenfalls ihre Klasse. Sie können mit und ohne Bezug zu Instanzen zugegriffen werden.

imageLiterale: Die Werte von elementaren Typen wie etwa 0x3.14f werden aus einer eingeschränkten Zeichenmenge nach festgelegten Regeln gebildet. Diese Wertkonstanten nennt man Literale. Neben den Wertkonstanten der elementaren Typen gibt es noch die String-Literale, die Zeichenketten. Diese sind an den doppelten Hochkommatas kenntlich.

imageAusdrücke (Expressions): Mit Ausdrücken werden Werte berechnet. Ein Ausdruck ist eine Kombination von Variablen, Literalen, Operatoren und Methodenaufrufen mit dem Ziel, einen Wert zu erhalten.

imageStatements: Statements sind die nächst-kleineren Codestücke, aus denen Methoden aufgebaut werden. Ein Statement ist ein Stück Code, das in der Programmausführung eine logische Einheit bildet. Java definiert etwas mehr als ein Dutzend Statements, darunter das leere Statement, das nur aus einem Semikolon (;) besteht. Vielen Statements, wie etwa for-, if-, while-, switch-, break-, continue-, return- oder dem throw-Statement, wird man in diesem Kapitel erneut begegnen. Einige Statements, wie das block-Statement (zur Definition von Blöcken), das Variablen-Statement (zur Vereinbarung von Variablen) oder das Expression-Statement (das einfache Hinschreiben eines Ausdrucks mit einer eventuellen Zuweisung) werden hier dagegen nicht weiter hervorgehoben.

imageNachrichtenempfänger: Jeder Funktionsaufruf ist eine Nachricht, die an einen Nachrichtenempfänger gerichtet ist. Das ist in der Regel das Objekt, für welches die Funktion aufgerufen wird (das ist der Standardfall). Bei statischen Methoden ist es jedoch die Klasse. Es ist wichtig, dass man sich über den jeweiligen Nachrichtenempfänger bei einem Funktionsaufruf im Klaren ist. Manchmal sieht man ihn im Code nicht direkt, aber er ist immer vorhanden.

imageKommentare: Ein Kommentar ist ein in den Sourcecode geschriebener Text, den der Compiler ignoriert. Es gibt Zeilenkommentare und Blockkommentare. Zeilenkommentare blenden den Rest der Zeile für den Compiler aus. Sie werden mit einem Doppel-Slash (//) eingeleitet. Blockkommentare können über mehrere Zeilen geschrieben werden. Sie blenden für den Compiler alles aus, was sich zwischen /* (Kommentaranfang) und */ (Kommentarende) befindet. Über den Nutzen von Kommentaren gibt es verschiedene Ansichten. Man begegnet gelegentlich der Auffassung, dass Entwickler, die nicht in der Lage sind, Code verständlich zu schreiben, ihn mit Kommentaren erklären können.

imageReferenzen: Eine besondere Rolle spielen in Java die Referenzen. Java-Code ist immer mit vielen Objekten befasst, die irgendwo außerhalb und unabhängig von Methoden leben. Methoden können Objekte aber über Referenzen – das sind Verweise - zugreifen und manipulieren. Eine besondere Referenz ist this. this zeigt auf den Nachrichtenempfänger.

imageElementare Typen und Benutzer-definierte Typen: Es gibt in Java zwei Arten von Typen. a) Elementare Typen (boolean, byte, char, short, int, long, float, double), die auch primitive Typen genannt werden. Diese sind fester Teil der Sprache. b) Zusammengesetzte Typen, die durch Klassen vereinbart werden. Diese werden auch Referenztypen genannt oder Benutzer-definierte Typen, weil sie der Sprache von außen (durch Klassen) zugefügt werden. Referenzen gibt es nur für Instanzen von Referenztypen (= Objekte).

imageObjekte und Instanzen: Das ist nahezu das selbe. Es gibt nur ganz wenige Gelegenheiten, bei denen es einen Unterschied zwischen diesen beiden Bezeichnungen gibt.

imageStandard-Library: Java wird an Entwickler in Form des JDK (Java-Development-Kit) ausgeliefert. Dessen wesentlicher Inhalt außer Compiler und Interpreter ist die Standard-Library. Dies ist eine Sammlung von gut 4000 Referenztypen, die der Entwickler für seine Arbeit neben eigenen Typen zur Verfügung hat.

imageKontrollstrukturen: Es gibt in Java nur wenige Ablauf-Kontrollstrukturen. Diese erlernt man rasch: Verzweigung (switch), Bedingung (if), Schleifen (for, while, for/in), Exception-Handling (try-catch) und natürlich der Funktionsaufruf selbst.

imageAutomatische Umwandlungen: Bei jedem Ausdruck erwartet der Compiler ein formales Ergebnis, also einen Typ, den der Ausdruck liefern muss, weil das Umfeld des Ausdruck diesen Typ vorgibt. Auf der anderen Seite sieht der Compiler, was der Ausdruck liefert. Wenn beide Teile nicht exakt zusammenpassen, kann der Compiler Anpassungen vornehmen. So kann er etwa ein short in ein int umwandeln. Falls die Teile nicht zusammenpassen und der Compiler keine Anpassung vornehmen kann, meldet er einen Fehler und compiliert das File nicht.

imageCasts: In einigen Fällen kann der Entwickler den Compiler bei Anpassungen unterstützen. Wenn beispielsweise ein int in ein short umgewandelt werden soll, kann der Compiler dies nicht automatisch tun, da ein int-Wert nicht in jedem Fall in ein short passt und Datenverlust auftreten kann. Der Entwickler kann in diesem Fall einen Cast vornehmen. Dazu stellt er eine Typangabe in runden Klammern vor den Ausdruck und sagt damit dem Compiler, dass die Umwandlung hier passt:

int count = 13;
short number = (short) count; // Cast von int nach short
List books = (List) getBooks(); // Cast nach List von was auch immer

imageWrapper und Auto-Boxing: Für jeden der acht elementaren Typen gibt es einen entsprechenden Referenztyp, etwa Integer für int. Diese nennt man ‚Wrapper’, weil die Instanzen dieser Typen einen elementaren Wert einschließen. Wenn der Compiler einen elementaren Typ antrifft und einen Referenztyp braucht, nimmt er eine automatische Umwandlung vor, indem er für den elementaren Wert eine Wrapper-Instanz erzeugt. Dies nennt man Auto-Boxing. Auch die Umwandlung in die andere Richtung geschieht automatisch. Diese nennt man Auto-Unboxing.

imageKonventionen: Packagenamen werden vollständig klein geschrieben. Der einzelne Packagename wird zudem ohne Punktzeichen und Trennzeichen gebildet (my-package oder myPackage beispielsweise gibt es nicht). Konstanten werden vollständig groß geschrieben (MAX_NUMBER). Trennzeichen ist der Unterstrich (Underscore). Klassennamen und die Namen anderer Typen beginnen immer mit einem Großbuchstaben. Trennung von Wortteilen erfolgt durch Großbuchstaben (MyVeryImportantType). Variablennamen und Methodennamen beginnen mit einem Kleinbuchstaben. Trennung von Wortteilen erfolgt durch Großbuchstaben. Umlaute in Bezeichnern sind nicht erlaubt.

Nach diesem Sprung im Wissen um Syntax und Regeln ist hier ein guter Platz, um eine Hilfestellung für das eigene Coding zu platzieren. Das folgende Beispiel ist als Vorlage gedacht, um Leserinnen und Lesern im weiteren Verlauf der Themen einen Coderahmen für deren Erprobungen zu geben:

import javax.swing.JOptionPane; // Eine Standardklasse, die wir einsetzen
public class Experiment
{
private static String USER_DIALOG = "Your input, please";
public static void main(String[] args)
{
System.out.println("Start of main");
proveInput();
//proveEnum();
System.out.println("End of main");
}
private static String getInput() // Ob public oder private spielt keine Rolle
{
String userInput = JOptionPane.showInputDialog(USER_DIALOG);
return userInput;
}
public static void proveInput()
{
System.out.println("Start of proveInput");
String input = getInput(); // Zeigt einen Dialog. Eingabe + Return
System.out.println("Input: " + input); // Ohne Eingabe ist input gleich null
System.out.println("End of proveInput");
}
}

Die Idee dabei ist, dass jede Erprobung irgendeines Features in eine eigene Methode gestellt wird, deren Name mit ‚prove’ beginnt. Im Beispiel wird dazu nur die Methode proveInput() gezeigt. Die Klasse Experiment sollte jedoch nach Beendigung der ersten Rundreise einige Dutzend solcher Methoden enthalten. In main() wird nur die gerade aktuelle Methode aufgerufen, die anderen Methodenaufrufe werden auskommentiert. Wenn man zu einem Thema verschiedene Versuche erstellt, so sollte man mehrere Erprobungsmethoden verfassen, etwa proveString1() bis proveString5(). Wenn man den Erprobungscode auf diese Weise gut aufteilt und organisiert, hat man zu späteren Zeiten eine gute Chance, zu bestimmten Fragen den passenden Code wiederzufinden.

Sowohl main() als auch die eigentlichen Erprobungsmethoden sind mit Protokollierungsstatements (System.out.println()) versehen. Diese Art der Protokollierung ist nicht ideal. Aber für kleine Erprobungen ist dies ausreichend und gibt Ihnen auf der Konsole eine kleine Kontrolle, ob das Programm überhaupt gestartet wurde und was gerade abläuft. System.out ist - wie Sie bereits wissen - ein Objekt, mit dem man auf die Konsole schreiben kann. Dieses Objekt hat den Namen out und wird von der Standardklasse System als public Element angelegt. Einiges an System.out ist erklärungsbedürftig und Sie sollten dies vorerst einfach so stehen lassen.

Das Beispiel zeigt auch eine Import-Anweisung. Hier wird die Standardklasse JOptionPane aus dem Package javax.swing importiert, weil damit die Benutzereingabe ausgeführt wird. Grundsätzlich müssen alle benötigten Referenztypen auf diese Weise explizit importiert werden, die sich nicht in dem Package java.lang befinden oder im Package der aktuellen Klasse. Dies gilt auch für Ihre eigenen Klassen, die Sie in Experiment verwenden und für die Sie Packages angelegt haben. Alle Import-Anweisungen stehen vor der betreffenden Klasse.

7.Zuweisung

Viele Statements sind Zuweisungen. Es wird dabei ein Wert in einer Variablen gespeichert. An einer anderen Stelle im Programmablauf kann auf diesen Wert dann wieder zugegriffen werden. Zuweisungen werden entweder für elementare Typen oder für Referenztypen ausgeführt:

int i = 13; // Initialisierung von i; Auch eine Form von Zuweisung
i = 14; // Speichere in i einen neuen Wert
PrintStream out = System.out; // Zuweisung an Referenz. Beide R. zeigen auf dasselbe Objekt
String hello = new String("Hello"); // hello zeigt auf String-Objekt
String string = hello; // string und hello zeigen auf das selbe Objekt

Wenn elementare Werte zugewiesen werden, werden diese grundsätzlich kopiert. Eine Variable mit elementarem Typ, an die zugewiesen wird, enthält nach der Zuweisung eine Kopie des Wertes der gelesenen Variablen. Beide Variable können anschließend unabhängig voneinander geändert werden.

Für Referenztypen bedeutet eine Zuweisung, dass ein weiterer Verweis (Referenz) auf ein gegebenes Objekt angelegt wird. Das Objekt wird dabei nicht kopiert. In diesem Statement:

PrintStream out = System.out; // Zwei Referenzen zeigen auf ein Objekt

geht es um ein Objekt, das den Typ PrintStream hat und das im Nirvana lebt (siehe unter Objektorientierung). Auf dieses Objekt gibt es die Referenz System.out. Das obige Statement legt eine weitere Referenz an, nennt diese out und weist ihr den Wert System.out zu. Jetzt existieren mindestens zwei Referenzen auf dieses Objekt. Das Objekt selbst wird in der Zuweisung nicht kopiert. Eine Änderung des Objekts, die über System.out oder über out veranlasst wird, wird auch über die jeweils andere Referenz gesehen.

Bei Zuweisungen von echten Objekten werden Referenzen kopiert, jedoch nicht die Objekte, auf welche diese Referenzen zeigen

In Java sieht man keine Notwendigkeit, Objekte zu kopieren. Man braucht im Normalfall keine Duplikate von Objekten. Referenzen auf ein gegebenes Objekt können in beliebiger Anzahl erstellt und einander zugewiesen werden. Das Objekt interessiert sich dafür nicht und bekommt davon auch nichts mit. Wenn dagegen ein Objekt tatsächlich kopiert wird, dann interessiert dies das Objekt sehr wohl. Es muss der Kopie zustimmen. Denn es muss die Kopie selbst durchführen. Auch das ist ein Aspekt von Objektorientierung. Objekte erledigen Angelegenheiten, die sie angehen, zumeist selbst.

Hin und wieder sieht man mehrere Zuweisungen, die zu einem Ausdruck verkettet sind:

a = b = c = 13; // (a = (b = (c = 13)));

Zuweisungen werden anders als die meisten Operatoren in Java von rechts nach links ausgewertet. Dies gehört neben dem ‚Nicht-Kopieren’ zu dem Wenigen, was man sich zu Zuweisungen wirklich merken muss. Das Ergebnis einer einzelnen Zuweisung ist im Übrigen der zugewiesene Wert.

8.Kombinierte Zuweisungen

In Java sind wie in C++ kombinierte Zuweisungen möglich und üblich. Anstatt eine Variable über einen Ausdruck zu verändern, bei dem zuerst der Wert der Variablen abgefragt und ihr dann in veränderter Größe wieder zugewiesen wird, kann man dafür auch eine kürzere Schreibweise verwenden:

i = i + 14; // Normale Wertveränderung einer vorher vereinbarten int
i += 14; // Kombinierte Zuweisung
array[f(n)] = array[f(n)] + 14; // Arrayzugriff, bei dem eine Funktion den Index liefert
array[f(n)] += 14; // Kombinierte Zuweisung

(Es versteht sich hier wie auch bei allen anderen Codebeispielen in diesem Buch von selbst, dass alle verwendeten Größen – also i, f(), n und array - irgendwo erklärt sein müssen. Wir schenken uns die penible Deklaration der beteiligten Größen hin und wieder, wenn das Beispiel auch so verständlich ist.)

Kombinierte Zuweisungen haben neben der kürzeren Schreibweise auch die Wirkung, dass die betroffene Größe nur einmal zugegriffen wird statt zweimal. Dies wird besonders in dem Array-Beispiel deutlich, bei dem der Index durch eine Funktion berechnet wird. Diese wird bei einer normalen Zuweisung zweimal aufgerufen, bei kombinierter Zuweisung dagegen nur einmal. Man beachte die besondere Syntax bei einer kombinierten Zuweisung: Der kombinierte Operator wird vor das Zuweisungszeichen (=) gestellt.

Kombinierte Zuweisungen gibt es für die arithmetischen Operatoren (+, -, *, /, %), für die Bit-Operatoren (&, |, ^) und die Bit-Shift-Operatoren (<<, >>, >>>):

int i = 0;
i += 2; // Anstatt i = i + 2 Ebenso i -= 2
i *= 31; // Anstatt i = i * 31 Ebenso i /= 31
i <<= 8; // Anstatt i = i << 8 Schiebe Bits 8 Stellen nach links
i >>= 2; // Anstatt i = i >> 2 Schiebe Bits 2 Stellen nach rechts
i >>>= 3; // Anstatt i = i >>> 3 Schiebe Bits ohne Vorzeichen
i &= 64; // Anstatt i = i & 64 Setze das 7-te Bit. Analog für | und ^

Kombinierte Zuweisungsoperatoren arbeiten mit einer Ausnahme nur auf elementaren Typen. Diese eine Ausnahme ist der Plus-Operator (+). Der arbeitet auch für Strings. Deshalb gibt es auch für Strings eine kombinierte Zuweisung:

String hello = “Hello ”;
hello += “World”;

Der Ergebniswert einer kombinierten Zuweisung ist der neue Wert der Variablen, an die zugewiesen wurde.

9.Inkrement und Dekrement

Den Ausdruck i += 1 kann man noch weiter abkürzen. Man kann ihn als Inkrementation schreiben. Dabei wird ein doppeltes Pluszeichen an die Variable gestellt, also i++ oder ++i. Analog dazu gibt es die Dekrementation: i-- oder --i. Beide, Inkrementation und Dekrementation sind abkürzende Schreibweisen, die einen Zahlenwert um 1 erhöhen oder erniedrigen. Aufgrund der Kürze sind die betreffenden Ausdrücke gut lesbar und in der Praxis sehr verbreitet. Der Ergebniswert von Inkrement- und Dekrement-Operator ist der Wert der Variablen. Beide Operatoren können nur auf Integer- und Floatingpoint-Variable angewandt werden:

int i = 12;
byte b = 10;
char c = 'A';
double f = 1.0;
i++; // i = 13 Inkrementation
b++; // b = 11 Inkrementation
c++; // c = 'B' Unüblich!
i--; // i = 12 Dekrementation
f++; // f = 2.0 Syntaktisch möglich, aber unüblich

Die hier gezeigten Operationen bezeichnet man als Postfix-Operationen. Das "Post" bezieht sich auf die Stellung von ++ oder – hinter der Variablen. Analog dazu gibt es Prefix-Operationen. Bei ihnen steht ++ oder – vor der Variablen. Beide Operatoren: Prefix und Postfix sind unäre Operatoren. Sie haben nur einen Operanden. Die Wirkung von beiden ist, dass ihr Operand um 1 erhöht oder erniedrigt wird. Insoweit gibt es zwischen Prefix und Postfix keinen Unterschied. Ein Operator hat aber auch einen Ergebniswert, und der ist bei Prefix- und Postfix-Operatoren verschieden. Der Ergebniswert eines Prefix-Operators ist der Wert des Operanden nach der Operation. Der Ergebniswert eines Postfix-Operators ist der Wert des Operanden vor der Operation. Dies wird nachfolgend vorgeführt:

// Zunächst so einfach wie möglich
int i = 0, k = 0;
k = i++; // ++ erhöht i auf 1, aber der Resultatwert ist 0. k = 0
i = 0;
k = ++i; // ++ erhöht i auf 1 und das Resultat ist diesmal auch 1
// Zur Verdeutlichung
int index = 0;
int[] array = new int[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; // Initialisierung eines Arrays mit Werten in Klammern
int value = array[index]; // Zugriff auf array[0]. value = 0. index = 0
value = array[index++]; // Zugriff auf array[0]. value = 0. index = 1
index = 0;
int value = array[++index]; // Zugriff auf array[1]. value = 1. index = 1

Die Schwierigkeit im ersten Verständnis der Inkrement- und Dekrement-Operatoren rührt daher, dass viele Leute intuitiv glauben, der Resultatwert von so einfachen Operatoren wäre die Variable, auf die sie angewandt werden. Das ist in diesem Fall in Java (anders als in C++) nicht so. Der Resultatwert von i++ ist nicht i selbst, sondern ein Wert, den die Variable nach der Operation nicht mehr hat. Wir lernen von Prefix und Postfix jedenfalls etwas über den Ergebniswert von Operationen:

Die Wirkung einer Operation und der Ergebniswert einer Operation sind nicht notwendigerweise dasselbe

10.Stringverknüpfung (+)

Der +-Operator ist zunächst für elementare Typen außer boolean definiert und hat dort die naheliegende Bedeutung der Addition. Darüber hinaus ist der +-Operator aber auch für Strings definiert. Dies heißt, dass es den +-Operator in Java zweimal gibt, einmal für elementare Typen und einmal für Strings. Die Zeichenketten, die den Inhalt der Strings bilden, kann man diesen einfach zuweisen. Die Zeichenketten werden in dieser expliziten Form als String-Literale bezeichnet und sind an den umschließenden Hochkommatas zu erkennen. Hier sehen wir die Initialisierung eines Strings durch ein String-Literal:

String help = “Help me if you can”;

Der +-Operator für Strings wird automatisch ausgewählt, wenn einer der beiden Operanden ein String ist. Der +-Operator verknüpft dann seine Operanden zu einem neuen String (Concatenation). Das Ergebnis des Operators ist deshalb ebenfalls ein String. Einen eventuellen Operanden, der noch nicht den Typ String hat, wandelt er vorher kurzerhand in einen String um. Der Operator ist dabei nicht wählerisch. Es gibt in der Javawelt keinen Typ, den er dabei nicht akzeptieren würde:

int value = 13;
System.out.println("int value: " + value); // Umwandlung value in String, Verkettung, Ausgabe
String out = value; // Error: int kann nicht an String zugewiesen werden
String out = "" + value; // Okay: Umwandlung, Verkettung, Zuweisung
String result = "Collect some values. "; // Erzeuge einen String
result = result + Instant.now() + value + "."; // Hänge Zeitstempel, value und Trennzeichen (.) an
result += Instant.now() + value + "."; // Dasselbe mit kombinierter Zuweisung

Die Verkettung mit einem Leerstring (in der vierten Codezeile) ist ein netter Trick, um ein beliebiges Objekt in einen String umzuwandeln. Auch bei der Umwandlung in einen String ist es wieder so, dass ein Objekt diese Operation selbst bestimmt und kontrolliert. Um dies zu verstehen, betrachten wir den Zeitstempel (Instant) in den letzten beiden Codezeilen. Instant.now() erzeugt ein Instant-Objekt, das den momentanen Zeitpunkt darstellt - nennen wir es now. Folglich haben wir es damit zu tun:

result = result + now + value + "."; // Stringverkettung mit dem Instant-Objekt now

Der Compiler erwartet bei der betreffenden Plus-Operation einen String, erhält jedoch ein Instant. Folglich ruft er now.toString() auf, um now in den gewünschten String umzuwandeln. Das ist eine Methode der Klasse Instant. Instant bestimmt also, in welche Strings ihre Objekte konvertiert werden. Für den Entwickler ist dies ein Hinweis, dass letztlich er bei jeder Klasse, die er schreibt, mit der Methode toString() festlegt, wie die String-Repräsentation der Instanzen dieser Klasse aussieht.

Alle Objekte und alle elementaren Werte können auf einfache Weise in Strings umgewandelt werden

11.Bedingung (if/else)

Ein grundlegendes Mittel der Programmierung ist die Bedingung. Diese wird in Java wie in vielen anderen Programmiersprachen mit if formuliert. Das if-Statement umfasst einen bool'schen Ausdruck - das ist die Bedingung - und ein kontrolliertes Statement. Wenn der Ausdruck den bool'schen Wert true ergibt, wird das kontrollierte Statement ausgeführt, andernfalls wird es übersprungen:

String input = getInput();
if (input == null) // Bool'scher Ausdruck – Audruck mit true oder false
System.exit(); // Wird nur dann ausgeführt, wenn input null ist

Das kontrollierte Statement kann auch ein Codeblock sein (Blöcke und Statements sind ja gegeneinander austauschbar). Obiges Beispiel kann deshalb auch so geschrieben werden:

String input = getInput();
if (input == null)
{
System.out.println("Cannot run without your input");
System.exit();
}

Für die Bedingung des if-Statements (für den Ausdruck in den runden Klammern) muss ein bool'sches Ergebnis (true oder false) geliefert werden. Es ist auch der Ergebnistyp Boolean zulässig (ein Wrapper), der vom Compiler automatisch nach boolean umgewandelt wird. Das if-Statement hat einen optionalen else-Zweig, der ausgeführt wird, wenn die Bedingung zu false ausgewertet wird:

String input = getInput();
if (input == null)
System.exit();
else
System.out.println("Do something else");

if-Statements werden des Öfteren verschachtelt. Dabei ergibt sich das Problem, dass nicht für jeden Leser des Codes klar ist, wie die else-Zweige zugeordnet werden müssen. Dabei hilft auch die Einrückung zur Klärung nicht weiter, denn der Compiler ignoriert Einrückungen. Das Problem bei verschachtelten if-Statements ist, dass sie fehleranfällig sind. Das größere Problem ist, dass sie schwer zu lesen und zu pflegen sind. Wie ist der folgende Code gemeint?

if (input != null) // Problematisch
if (input.length() == 0)
System.exit();
else // Zu welchem if gehört dieses else?
System.out.println("Do something else");

Oder ist er eher so gedacht?

if (input != null) // Problematisch
if (input.length() == 0)
System.exit();
else // Wie oben: Zu welchem if gehört dieses else?
System.out.println("Do something else");

Ein Code-Review würde keine der beiden Schreibweise durchgehen lassen. Bei mehrfachen if-Statements muss man entweder durch Klammerung klar strukturieren:

if (input != null)
{
// Durch Klammerung Klarheit schaffen
if (input.length() == 0)
System.exit();
else
System.out.println("Do something else");
}

oder man verwendet else-if-Ketten. Bei diesen erhält der else-Zweig ein weiteres if-Statement. Damit kann man nun eine Kette von Bedingungen schreiben:

String input = getInput();
if (input == null)
{… }
else if (input.equals("help"))
{… }
else if (input.equals("quit"))
{… }
else // Gehe hierher, wenn keine Bedingung zutrifft
{… }

Im Programmablauf werden die Bedingungen einer else-if-Kette der Reihe nach geprüft, bis eine Bedingung true ergibt. Dann wird der zugehörige Block durchlaufen. Anschließend wird der Programmablauf hinter der Kette fortgesetzt. Wenn keine Bedingung zutrifft, wird in den else-Zweig am Ende der Kette verzweigt. Dieser ist optional.

Anstelle von verschachtelten if-Statements sollte man else-if-Ketten verwenden

Der Vergleich der else-if-Kette mit dem switch-Statement (siehe nächstes Teilkapitel) ist lehrreich:

imageelse-if-Ketten können beliebig lang werden. Aber auch das switch-Statement kann beliebig viele Marken umfassen. In beiden Fällen ist übermäßige Größe ein Problem, weil die Übersichtlichkeit verloren geht.

imageIn switch wird die zutreffende case-Marke angesprungen. In else-if-Ketten werden alle Bedingungen von Anfang an durchlaufen und ausgewertet, bis eine der Bedingungen true ergibt.

imageswitch arbeitet nur für Enums, Strings und ganzzahlige Werte (+ Wrapper). In else-if-Ketten sind dagegen beliebige Bedingungen erlaubt.

imageDie Marken im switch-Statement sind Konstanten, deren Werte zur Compilezeit feststehen müssen. Die Bedingungen in else-if-Ketten werden erst zur Laufzeit bestimmt.

12.Verzweigung (switch)

Mit dem switch-Statement werden Verzweigungen programmiert, die von einem aktuellen Wert gesteuert durchlaufen werden. Man muss sich das so vorstellen, dass man einen Ausdruck gegeben hat, häufig auch nur eine Variable, die switch-Variable, und dass man bestimmten Werten dieses Ausdrucks bestimmte Codeteile zuordnet. Diese möchte man ausführen, wenn Ausdruck oder Variable die betreffenden Werte annehmen. Das switch-Statement bietet die Auflistung dieser Codeteile.

Am Anfang steht die Prüfung der switch-Variablen mit dem Schlüsselwort switch. Hier ist ein Ausdruck gefordert, der einen elementaren ganzzahligen Typ jedoch nicht long (nur byte, char, short und int sind zulässig) oder einen entsprechenden Wrapper-Typ, einen String oder eine Enum liefert. Hinter der Prüfung wird der gesamte Rest des switch-Statements in geschweifte Klammern gesetzt. Das folgende Beispiel, das einige Schwächen hat, zeigt die Syntax:

int input = getInput(); // input ist hier die switch-Variable
switch(input) // Ausdruck mit String, Enum, byte, char, short, int …
{
case 1: // Springe hierher, wenn switch-Variable Wert 1 hat
// Bearbeitung von Fall 1; ein oder mehrere Statements
break; // Bearbeitung fertig. Springe zu schliessender Klammer
case 2:
function2(); // Gute Praxis: Erledige den Fall in eigener Funktion
System.out.println("Move to case 3"); // Schlechte Praxis: break fehlt. Deshalb weiter zu Fall
3 case 3:
function3();
break;
case 'A': // Springe hierher, wenn die Variable den Wert 'A' hat
return 'A'; // Hier braucht man natürlich kein break
case 'B':
// Bearbeitung von Fall B; ein oder mehrere Statements
break;
default: // Gehe hierher, wenn der Variablenwert nicht vorkommt
throw new IllegalStateException("No code for case: " + input);// Eine Standard-Exception
}

Innerhalb der umfassenden geschweiften Klammern folgen die einzelnen Codeteile aufeinander, die jeweils mit dem Schlüsselwort case eingeleitet werden. Die Reihenfolge der Codeteile spielt im Allgemeinen keine Rolle. Auf case folgt der Wert, dem dieser Codeteil zugeordnet ist, und dann ein Doppelpunkt. Die Bearbeitung eines Falles wird üblicherweise mit einem break-Statement abgeschlossen. Mit break wird das switch-Statement verlassen und der Programmablauf hinter switch fortgesetzt. Wenn ein Fall nicht mit break, return oder einer Exception verlassen wird, wird der Programmablauf mit dem unmittelbar folgenden Fall fortgesetzt. Dies geschieht oben in case 2.

Von einem Fall in einen anderen zu rutschen, ist zwar syntaktisch erlaubt, ist aber von Stil und Übersichtlichkeit her schwierig. Wenn wir switch-Statements lesen, in denen breaks fehlen, sind wir irritiert. Unsere erste Überlegung geht dahin, ob die breaks nicht einfach vergessen wurden. Software wird wesentlich öfter gelesen als geschrieben und irritierte Leser sind Leser, die unnötig Zeit brauchen. Dies heißt in der Konsequenz, dass man es sich dreimal überlegen sollte, einen Fall in einem switch-Statement ohne abschließendes break zu schreiben.

Ein switch-Statement mit den Case-Marken: 1, 2, 3 und ‚A’ und ‚B’ ist ebenfalls verwirrend. Hier gibt es jedoch nichts zu überlegen. Ein switch-Statement so schreiben, ist zwar nicht formell verboten, aber nahezu verboten.

Der default-Case ist optional. In den default-Case wird verzweigt, wenn der Wert der switch-Variablen von keinem anderen Fall behandelt wird. Gibt es keinen default-Case und wird der aktuelle Wert von keinem Fall behandelt, wird der Programmablauf hinter dem switch-Statement fortgesetzt. Zumeist handelt es sich dabei jedoch um einen Fehler. Ein bestimmter Fall wurde einfach vergessen. Mit dem default-Case hat man die Möglichkeit, diese Situation anzuzeigen. default ist im übrigen selbst ein Schlüsselwort und der default-Case wird nicht durch das Schlüsselwort case eingeleitet.

Den default-Case mit einer Exception zu melden, um damit einen vergessenen Fall anzumahnen, ist zur Entwicklungszeit eine gute Möglichkeit. Einen switch-Case und die umgebende Funktion mit einem return zu verlassen, etwa weil man den gesuchten Wert gefunden hat, ist ein übliches Verfahren und ein Programmierstil, den man öfters antrifft.

Das switch-Statement hat einige kleinere Schönheitsfehler:

imageMit switch bearbeitet man eine kleine abgezählte Menge von Fällen. Eine solche ist jedoch bei ganzzahligen Werten oder Strings, mit denen die Fälle markiert werden, nicht gegeben.

imageDie Case-Marken müssen Compiler-Konstanten sein. Hinter dem Schlüsselwort case kann zwar ein Ausdruck stehen, aber dieser muss konstant sein in dem Sinn, dass bereits der Compiler seinen Wert bestimmen kann.

imageWenn nach der Erstellung des Programms weitere Fälle dazukommen (was üblicherweise auch der Fall ist), müssen die switch-Statements nachprogrammiert werden. Und das ist noch der gute Teil dieses Problems. Oft wird die Anpassung einfach vergessen.

imageswitch-Statements können zu überlangen Funktionen führen. Funktionen sollten aufgrund allgemeiner Prinzipien jedoch eher kurz sein.

Seit der Einführung der Enums in Java entfallen der erste und der dritte der genannten Punkte. Mit dem vierten kann man umgehen (siehe die Standard-Implementierung weiter unten). Wenn der zweite Punkt ins Gewicht fällt, muss man statt switch eine else-if-Kette verwenden.

Als Verzweigungs-Typ im switch-Statement sollte man grundsätzlich Enums verwenden

Beim Einsatz von Enums in Switches sollte man den default-Case weglassen. Denn dann informiert der Compiler, wenn man Enum-Werte im switch-Statement vergessen hat. Auf diese Compilerunterstützung sollte man nicht verzichten

Das switch-Statement ist ein Klassiker. Es fehlt in keiner prozeduralen Programmiersprache und seine Syntax wird seit vielen Jahrzehnten in jeder Sprach-Ausbildung ausgiebig behandelt. Das Bild einer Problemlösung durch Sortierung nach Fällen ist wohl sehr eingängig und sitzt hartnäckig in den Köpfen. In der Java-Praxis ist die Rolle von switch jedoch eher klein. Es kommt natürlich vor, dass man Verzweigungen braucht, aber das ist nicht so oft der Fall. Die wichtigen Mittel zur Kontrolle des Programmflusses in Java sind Bedingung, Iteration und Funktionsaufruf. switch gehört nicht dazu.

Für einen Entwickler, der eine gegebene Aufgabe mit einem switch-Statement lösen möchte, gibt es dennoch keinen Grund, switch nicht zu verwenden. Es soll deshalb zum Abschluss dieses Themenblocks eine Standard-Implementierung des switch-Statements gezeigt werden. Dazu braucht man eine Reihe von Konstanten, welche die einzelnen Fälle festlegen, und die wir mit einer Enum vereinbaren. Der folgende Code zeigt, wie einfach dies geht:

public enum GameState {STARTED, PENDING, READY, FINISHED} // In dem File GameState.java
// In einem anderen File
GameState gameState = getGameState(); // Erhalte Wert irgendwoher
switch(gameState)
{
case STARTED:
handleStarted();
break;
case PENDING:
handlePending();
break;
case READY:
handleReady();
break;
case FINISHED:
handleFinished();
break;
} // Mit Absicht kein default ‼

Die Enum können wir als Einzeiler in einem eigenen File vereinbaren. Dieses muss dann den Namen Game-State.java tragen. Das ist die bevorzugte Variante. Wir können sie aber auch innerhalb einer bereits bestehenden Klasse vereinbaren:

public class Experiment
{
public static enum GameState {STARTED, PENDING, READY, FINISHED}
}

Sobald die Enum existiert, schreiben wir ein leeres switch-Statement:

GameState gameState = getGameState(); switch(gameState) { }

Jetzt beklagt der Compiler, dass in switch nicht alle möglichen Fälle behandelt werden und bietet an, diese zu ergänzen. Wir nehmen das Angebot an und der Compiler erzeugt den Inhalt des obigen switch-Statements ohne die Funktionsaufrufe. Diese fügen wir ein und sind fertig. Die Vorteile bei dieser Codestruktur liegen in der Übersichtlichkeit, in der Systematik im Vorgehen und in der Fehlersicherheit. Wenn irgendwann die Zahl der Konstanten in GameState erweitert wird, wird der Compiler in diesem switch-Statement (und in allen anderen, die mit GameState arbeiten) mit einer Warnung anzeigen, dass hier noch etwas getan werden muss. Voraussetzung dafür ist, dass wir keinen default-Case verwenden.

Auf einzelne Enum-Konstanten muss man übrigens außerhalb von switch-Statements in dieser Weise zugreifen:

GameState gameState = GameState.READY;

Innerhalb eines switch-Statements werden Enum-Konstanten dagegen niemals durch den Typnamen qualifiziert.

13.Der new-Operator

Objektorientierung hat mit Objekten zu tun. Um mit diesen zu arbeiten, muss man sie zuerst erzeugen. Dies geht mit dem new-Operator:

Object object = new Object(); // Einfaches Objekt; nur eingeschränkt nützlich
Integer integer = new Integer(13); // Eine Zahl in Objektform - ein Wrapper
JFrame frame = new JFrame("Frame Title"); // Teil einer grafischen Oberfläche
String string = new String("Some words"); // Ein String. Meist verwendeter Typ in Java
String words = "Some words"; // Compiler fügt new-Operator für uns ein
String[] array = new String[10]; // Ein Array von Strings. Zunächst noch leer
Integer number = Integer.getInteger("51627537"); // Parsen und interne Verwendung von new

In Java werden alle Objekte mit dem new-Operator erzeugt. Die Regel ist strikt formuliert, aber gemäß den Beispielen scheint es Ausnahmen zu geben. Doch der Schein trügt. Bei den String-Literalen beispielsweise fügt der Compiler für uns den new-Operator ein. Einige allgemein verwendete Objekte stellt das System für uns bereit - wie etwa den Stream System.out, der beim Start systemintern mit new erzeugt wird. Ein weiteres Beispiel ist die Klasse Integer, die uns auf Anfrage ihre Objekte scheinbar auch ohne die Verwendung von new liefert. Aber auch bei Integer (und anderen Wrappern) wird new intern verwendet und letztlich ist der new-Operator immer involviert.

Es ist im übrigen eine gute Entwicklerhaltung, solche Aussagen nicht immer bedingungslos zu glauben, sondern sie hin und wieder auch zu überprüfen. Gehen Sie dazu in die Entwicklungsumgebung und tippen Sie den Namen der Klasse Integer oder System in irgendeinen Editor. Selektieren Sie den Namen und wählen Sie (in Eclipse) den Befehl Navigate → Open Declaration. Die Entwicklungsumgebung öffnet das File und Sie sehen nun die Implementierung des betreffenden Typs. Sehen Sie sich bei Integer etwa die Methode getInteger() an und verfolgen Sie, wie Integer letztlich zu einer neuen Zahl kommt.

Alle Objekte in Java werden mit new erzeugt. In einigen wenigen Fällen erlaubt der Compiler eine verkürzende Schreibweise (etwa bei String) und fügt ein new für den Entwickler ein

Der new-Operator hat zwei Operanden: die Klasse des zu erzeugenden Objekts und eine Liste von Argumenten. Der Operator ruft in der gegebenen Klasse einen passenden Konstruktor auf und übergibt an diesen die Argumentliste. Der Ergebniswert des new-Operators ist das erzeugte Objekt.

Elementare Werte (boolean, byte, char, short, int, long, float, double) haben mit new nichts zu tun. Diese werden erzeugt, indem man den Typ, den Variablennamen und einen Initialwert (ein Literal des betreffenden Typs) hinschreibt. Zu allen acht elementaren Typen gibt es jedoch entsprechende Referenz-Typen (die Wrapper: Boolean, Byte …), die man verwendet, wenn man einen elementaren Wert in Objektform braucht. Und für diese Objekte gilt natürlich wieder die obige Aussage.

14.Gleichheits- und Identitäts-Operator

Für alle elementaren Typen gibt es die Vergleiche auf Gleichheit (==) und Ungleichheit (!=). Für den Neuling sorgt die Verwechslung des Gleichheits-Operators (==) mit der Zuweisung (=) gelegentlich für Überraschungen. Glücklicherweise compiliert der Compiler dies nicht immer. Der folgende Code zeigt zwei offensichtliche Fehler:

int i = 13; // Erstes Beispiel
// Irgendwelche Veränderungen an der Variablen i
if (i = 14) // i == 14 ist beabsichtigt. Compiler bemerkt den Fehler durch
Typprüfung
boolean problem = false; // Zweites Beispiel
// Irgendwelche Veränderungen an der Variablen problem
if (problem = true) // Diesmal hilft keine Typprüfung. Das Unglück nimmt seinen Lauf
System.exit(0);

Der Ergebniswert einer Zuweisung ist der zugewiesene Wert. Der logische Fehler im ersten Beispiel wird deshalb zu einem syntaktischen Fehler. Damit kann der Compiler diesen Fehler entdecken. Denn in einer Bedingung (if) wird ein bool'scher Wert erwartet und nicht der int-Wert der Zuweisung. Der logische Fehler im zweiten Beispiel ergibt keinen syntaktischen Fehler und wird folglich übersetzt.

Der Operator == prüft die Werte von elementaren Typen auf Gleichheit. Deshalb heißt er Gleichheits-Operator. Einen Operator == gibt es auch für Referenztypen. Dieser Operator heißt Identitäts-Operator. Der Identitäts-Operator prüft nicht die Gleichheit von zwei Referenzobjekten, sondern er prüft, ob zwei Referenzvariable auf dasselbe Objekt zeigen. In entsprechender Weise wirkt der Operator != ebenfalls auf elementare Werte und auf Objekte. Er testet auf Ungleichheit beziehungsweise Nicht-Identität.

15.Logische Operatoren, Vergleiche, Verknüpfung von Bedingungen

Die logischen Operatoren haben bool'sche Operanden und ein bool'sches Ergebnis. Es gibt sie in zwei Formen. Die geläufigen logischen Operatoren sind Und (&&) und Oder (||). Daneben gibt es einen zweiten Satz von logischen Operatoren, nämlich Und (&), Oder (|) und Exklusives Oder (^), deren Gebrauch recht selten ist. Logische Operatoren verknüpfen zwei Ausdrücke mit je einem bool'schem Ergebnis zu einem umfassenden bool'schen Ausdruck:

if (isSunShining() && isTemperatureHigh())
moveToBeach();
// Zwei bool'sche Werte werden zu einem verknüpft

Die Operatoren logisches Und (&&) und Logisches Oder (||) werden auch als konditionales Und beziehungsweise konditionales Oder bezeichnet, da sie den zweiten Operanden nur bewerten, wenn die Bewertung des ersten Operators noch nicht zu einem eindeutigen Ergebnis geführt hat. Wenn bei && der erste Operand false ist, steht das Gesamtergebnis als false bereits fest. Wenn bei || der erste Operand true ist, steht das Gesamtergebnis ebenfalls schon als true fest. Das folgende Beispiel zeigt eine typische Anwendung des &&-Operators. Das Ziel der Prüfung ist, zu sehen, ob das Array Elemente hat oder ob die Länge des Arrays 0 ist. Es ist jedoch nicht gesichert, ob getArray() wirklich ein Array liefert, oder nur einen null-Wert. Deshalb wird in der Prüfung zuerst getestet, ob das Array überhaupt vorhanden ist:

int[] array = getArray(); // Kommt von irgendwoher
if (array != null && array.length > 0) // Üblicher Test
if (array != null & array.length > 0) // NullPointerException falls array null. Selten
verwendet

Wenn das Array null ist, wird im ersten if-Statement der zweite Operand nicht mehr bewertet. Und dies ist vom Programmierer so kalkuliert. Denn der Zugriff auf ein Element einer null-Referenz würde zu einer Null-PointerException führen. Im zweiten if-Statement ist die Prüfung des Arrays auf null überflüssig. Denn die NullPointerException lässt sich auf diese Weise nicht verhindern, wenn das Array tatsächlich null ist. Man kann deshalb das zweite if-Statement hier auch als Programmierfehler ansehen.

Die Operatoren bool'sches Und (&), bool'sches Oder (|) und exklusives bool'sches Oder (^) bewerten in jedem Fall beide Operanden. Ansonsten haben & und | dieselbe Wirkung wie && und ||. Das exklusive bool'sche Oder hat das Resultat true, wenn seine beiden Operanden verschieden sind, andernfalls ist das Resultat false. Merken muss man sich dies allerdings nicht. Man würde es sowieso wieder vergessen.

Ganz nebenbei zeigt das obige Beispiel auch einen Vergleichs-Operator, nämlich mit der Bedeutung größer als. An Vergleichs-Operatoren gibt es in Java neben der Prüfung auf Gleichheit beziehungsweise Ungleichheit diese vier Stück: > (größer), >= (größer gleich), < (kleiner) und <= (kleiner gleich). Ihre beiden Operanden haben elementare Werte und das Ergebnis eines Vergleichs ist in jedem Fall ein bool’scher Wert, also true oder false.

Betrachten wir als kleine Fingerübung nochmals das obige Array und prüfen, ob sein erstes Element negativ ist:

int[] array = getArray();
if (array != null && array.length > 0 && array[0] < 0)
System.out.println(“Negative value: ” + array[0]);

Um sicher zu sein, dass dies so funktioniert, wie der Programmierer sich das offensichtlich erhofft, muss man wie schon für die obigen Tests wissen, dass logische Operatoren eine sehr niedrige Priorität besitzen und dass sie deshalb erst nach den Vergleichen ausgewertet werden. Man könnte trotzdem in Erwägung ziehen, die Teilausdrücke zwecks besserer Lesbarkeit zu klammern. Ich rate jedoch davon ab. Der Ausdruck wird dadurch nur noch länger. Zudem wird erwartet, dass ein Entwickler ein if-Statement in dieser Art ohne Schwierigkeiten lesen kann. Uns schließlich wollen wir nicht beliebige Klammersetzungen, wo einfache Vorrangregel ausreichen.

Im letzten if-Statement stehen zwei logische Operatoren nebeneinander. Welcher wird zuerst ausgewertet? Logische Operatoren sind links-assoziativ. Die Operatoren in obigem Ausdruck und ihre Operanden werden also von links her kommend abgearbeitet. Zuerst erfolgt die Prüfung auf null, dann die Prüfung der Array-Länge und dann erst der Zugriff auf das Element des Arrays. Jede Teilprüfung kann das if-Statement beenden.

Bemerkenswert an diesem Test ist, dass er die Fallstricke von Arrays aufzeigt. Es ist oft tatsächlich sinnvoll, vor dem Zugriff auf ein Array zu prüfen, ob dieses die vermutete Anzahl von Elementen auch hat. Zudem ist bei vielen Methoden nicht klar, ob sie die gewünschte Größe in jedem Fall liefern oder ob das Ergebnis auch null sein kann. Bei einem potentiellen null muss auch diese Möglichkeit vor dem Zugriff geprüft werden. Aber dies gilt nicht nur für Arrays, sondern für alle Referenzobjekte.

16.Der Conditional-Operator

Der Operator ? : ist der einzige ternäre Operator in Java (ein Operator mit drei Operanden). Er ist nützlich, um Entscheidungen in knapper Notation zu formulieren. Seine Syntax lautet:

Bedingung ? Ausdruck1 : Ausdruck2;

Sein Resultat ist entweder das Ergebnis von Ausdruck1 oder das Ergebnis von Ausdruck2, je nach der Auswertung der Bedingung. Wenn die Bedingung zu true ausgewertet wird, wird der Ausdruck zwischen Fragezeichen und Doppelpunkt (Colon) bewertet, andernfalls der Ausdruck zwischen Colon und Semicolon. Beide Ausdrücke müssen denselben Ergebnistyp haben und das ist dann auch der Ergebnistyp des Operators:

String input = getInputFromUser(); // Erhalte von irgendwoher einen String
String result = input != null ? input.trim() : "no input"; // Das Fragezeichen begrenzt die Bedingung
input = input != null ? input.trim() : "no input"; // Schöner, aber für Anfänger noch verwirrender

Die beiden letzten Zuweisungs-Statements enthalten im rechten Teil der Zuweisung den Conditional-Operator. Die Bedingung ist der Ausdruck: input != null. Die Bedingung muss immer ein Ausdruck mit einem bool'schen Ergebnis sein. In diesem Fall prüft die Bedingung, ob die Referenzvariable input auf ein Objekt zeigt und deshalb eine Elementfunktion aufgerufen werden kann. Wenn dies der Fall, wird input.trim() durchgeführt, was die Leerzeichen (Whitespaces) am Anfang und Ende des Strings entfernt. Wenn input jedoch null ist, wird im dritten Operand ein Ersatzstring ("no input") bereit gestellt.

Der Conditional-Operator ist nützlich und anfangs schwierig zu lesen. Mit etwas Übung verschwindet die Schwierigkeit. Was bleibt, ist ein nettes Tool für kompakte Formulierungen

Die beiden nächsten Beispiele sind einfacher zu lesen. Im ersten Beispiel wird abgefragt, ob eine Zahl kleiner 0 ist. Das Ergebnis wird als ja/nein-Aussage gespeichert. Im zweiten Fall wird der größere von zwei Werten gesucht:

int number = getNumber(); // Irgendwoher erhalten wir number
boolean numberTooLow = number < 10 ? true : false; // Kurze Schreibweise mit Conditional Operator
int index = getIndex(); // Irgendwoher erhalten wir index
int max = number > index ? number : index; // Eine weitere typische Anwendung
int max = 0; // Jetzt das Ganze ohne Conditional Operator
if (number > index) // Diese fünf Zeilen sind oben ein Einzeiler
max = number;
else
max = index;

Der Conditional-Operator wird in der Praxis wie einige andere nützliche Techniken wenig angewandt, weil er auf den ersten Blick nicht ganz einfach erscheint. Man sollte sich jedoch davon nicht abhalten lassen. Wie immer hilft etwas Übung. Man gewöhnt sich rasch an seine Notation. Der Conditional-Operator führt bei richtiger Verwendung zu kompakterem und leichter lesbarem Code. In der Software-Herstellung sind uns alle

Mittel willkommen, die zu übersichtlichem und stabilem Code führen. Der Conditional-Operator gehört zu diesen Hilfsmitteln. Man sollte ihn von Anfang an verwenden.

17.Zusammenfassung der Operatoren

Wir kennen nun schon einige Operatoren und so ist dies ein guter Platz, sich alle Java-Operatoren in einer Übersicht anzusehen. Operatoren sind Kürzel, die oft nur aus ein oder zwei Zeichen bestehen, mit denen eine in der Sprache definierte Operation aufgerufen wird. Betrachten wir den Index-Operator. Er wird mit eckigen Klammern geschrieben ( [] ):

int[] array = new int[] {1, 2, 3, 4, 5};
int value = array[4];

Für den Index-Operator ist verabredet, dass er zwei Operanden hat. Die Operanden eines Operators sind die Größen, mit denen er arbeitet und aus denen er sein Ergebnis erzeugt. Der erste Operand des Operators in array[4] ist array, der zweite Operand ist 4. Die Operation, die der Index-Operator durchführt, ist der Zugriff auf ein Element eines Arrays. Der Ergebniswert des Index-Operators ist der Inhalt der betreffenden Speicherzelle. Für jeden Operator in Java sind die folgenden Größen verabredet:

imageSeine alphanumerische Notation (wie +, <<, [] oder instanceof)

imageDie durchzuführende Operation

imageZahl und Typ der Operanden

imageDer Ergebnistyp.

Da der Index-Operator zwei Operanden hat, wird er als binärer Operator bezeichnet. Die meisten Operatoren in Java sind unär (nur ein Operand) oder binär. Es gibt einen einzigen Operator mit drei Operanden, das ist der Conditional-Operator (?:). Auch der Operator für Methodenaufrufe () ist binär. Der erste Operand ist der Methodenname, der zweite Operand ist die Argumentliste. Es wird also nicht jedes Argument als eigener Operand gezählt.

Betrachten wir kurz den Elementzugriffs-Operator, der auch Member-Selektions-Operator genannt wird. Er wird durch den Punkt (.) dargestellt:

System.out.println(); // Zweimalige Anwendung der Member-Selektion

Dieser Operator ist ebenfalls binär. Sein erster Operand ist ein entweder eine Klasse oder eine Referenz. Sein zweiter Operand ist ein Element des ersten Operanden. Wenn das Element ein Objekt ist, dann ist die Aufgabe dieses Operators, dieses Element auszuwählen. Der Ergebniswert ist in diesem Fall das ausgewählte Objekt. Wenn das Element eine Funktion ist, dann besteht die Aufgabe darin, diese Funktion auszuführen, und der Ergebniswert des Operators ist der Ergebniswert der Funktion.

Das Beispiel illustriert die genannten Fälle. Der erste Member-Selektions-Operator wird auf die Klasse System angewandt. Das ist der erste Operand. Der zweite Operand benennt das Element out der Klasse System. Dieses ist dann auch das Ergebnis der ersten Operation. Der zweite Member-Selektions-Operator arbeitet auf dem Objekt out und selektiert dessen Element println(). Da dieses Element eine Funktion ist, führt der Operator diese aus. Der Returntyp dieser Funktion ist void, und void ist deshalb das Ergebnis des gesamten Ausdrucks.

Wenn zwei Operatoren wie in System.out.println() nebeneinander stehen, dann besteht die prinzipielle Frage, welcher der beiden Operatoren zuerst ausgeführt wird (wir sehen weiter unter, warum sich diese Frage nicht immer stellt). Wenn es sich um zwei verschiedene Operatoren handelt, wird diese Frage möglicherweise durch den Operator-Vorrang (Priorität) geklärt. Der Operator mit der höheren Priorität wird zuerst ausgeführt. Die beiden gerade diskutierten Operatoren Index und Member-Selektion haben höchste Priorität. Sie sind in derselben Prioritätsgruppe. Es gibt in Java keine Operatoren mit höherer Priorität. Wenn die beiden Operatoren gleich sind, oder wenn sie gleiche Priorität haben, dann entscheidet die sogenannte Assoziativität, welcher Operator zuerst ausgeführt wird. Bei der Assoziativität von Operatoren wird zwischen links und rechts unterschieden. Ein Ausdruck mit mehreren gleichwertigen Operatoren wird von links kommend oder von rechts kommend ausgeführt. Die Assoziativität der Gruppe, zu welcher der Index-Operator und der Member-Selektions-Operator gehören, ist links-assoziativ. Aufeinander folgende Operatoren dieser Gruppe werden also in Schreibrichtung ausgewertet. Deshalb wird der Member-Selektions-Operator zwischen System und out zuerst ausgeführt, und dann erst der zwischen out und println().

Das folgende Beispiel zeigt, dass Operator-Vorrang und Assoziativität für die Reihenfolge der Abarbeitung nicht immer entscheidend sind:

int i = 2;
int value = array[3 * i];

Die Priorität des Multiplikations-Operators ist niedriger als die des Index-Operators. Dennoch wird der Multi-plikations-Operator aus offensichtlichen Gründen zuerst ausgeführt.

17.1.Auswertungsreihenfolge

In einem Ausdruck wird die Reihenfolge der Ausführung von Operatoren unter anderem durch Operatorvorrang (Priorität) und bei gleichem Vorrang durch Operator-Assoziativität bestimmt. Der Default-Vorrang und die Default-Assoziativität können durch runde Klammern überschrieben werden. Bevor jedoch irgendein Operator ausgeführt wird, werden alle Operanden eines Ausdrucks bewertet. Ausnahmen sind die Operatoren Logisches Und (&&), Logisches Oder (||) und der ternäre Conditional-Operator (?:), bei denen nicht immer alle Operanden bewertet werden. Ansonsten werden alle Operanden eines Ausdrucks bewertet und zwar geschieht diese Bewertung unabhängig von der Reihenfolge der Ausführung der Operatoren immer strikt von links nach rechts.

Operanden können nun ihrerseits wieder Operatoren enthalten. Damit ein solcher Operand bewertet werden kann, müssen die enthaltenen Operatoren ausgeführt werden. Dies bedeutet, dass die Reihenfolge der Ausführung von Operatoren in einem Ausdruck Top-Down bestimmt wird, indem man vom umfassenden Ausdruck zu den Teilausdrücken geht.

Um dies zu illustrieren, sehen wir uns den nachfolgenden Nonsense-Code an. Im ersten Statement wird ein int-Array mit 14 Elementen erzeugt. Dann wird eine Variable namens index deklariert und mit ihrer Hilfe wird aus den Array-Elementen ein komplizierter Ausdruck gebildet, wie man ihn in der Praxis hoffentlich nie antrifft:

int[] array = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13}; // Erzeuge ein Array mit 14 Elementen
int index = 1; // Nonsense-Code. Zeigt die Reihenfolge
array[index += 3] = index = array[index++] + array[index > 2 ? 1 : index % 14] * array[++index];

Wir sind nicht ernsthaft am dem Wert interessiert, der rechts von der Zuweisung herauskommt, noch daran, welchem Element des Arrays der Wert zugewiesen wird. Aber manchmal stellt sich auch bei harmloseren Ausdrücken die Frage, welcher Operator zuerst ausgeführt wird. Das obige Statement analysieren wir mit Hilfe folgender Überlegungen:

imageWir haben es mit einer Zuweisung zu tun. Zuweisungen sind rechts-assoziativ. Das heißt, eine Folge von Zuweisungen: a = b = c = d wird immer von rechts nach links abgearbeitet: a = (b = (c = d)). Letzteres ist einer der wenigen Punkte, die man sich merken sollte. Es wird in einer Kette von Zuweisungen also die rechte vor der linken Zuweisung durchgeführt.

imageIn dem Ausdruck: index = a[] + a[] * a[], hat die Multiplikation eine höhere Priorität als die Addition, und die Addition hat wiederum Vorrang vor der Zuweisung. Die Multiplikation wird folglich vor der Addition ausgeführt, und diese vor der Zuweisung.

imageBevor Addition, Multiplikation oder Zuweisung dieses Ausdruck durchgeführt werden, müssen die Operanden der einzelnen Operatoren bewertet werden. Einer der Operanden des Multiplikations-Operators ist a[index > 2 ? 1 : index % 14].

imageUm diesen Operanden zu bewerten, muss der eingeschlossene Ausdruck index > 2 ? 1 : index % 14 ausgeführt werden. Dafür sind die Regeln erneut anzuwenden. Dieser kompliziert erscheinende Ausdruck besteht aus dem ternären Operator a ? b : c.

Diese Überlegungen wird man sich nicht im Detail merken. Aber man sollte mitnehmen, dass die Aufschlüsselung von Ausdrücken nicht allein durch den Vorrang von Operatoren bestimmt wird. In der Entwicklungs-Praxis spielt diese Art von Gymnastik im Übrigen keine Rolle. Man schreibt keine Ausdrücke, die ein größeres Nachdenken erfordern. Wir befassen uns damit, um Prinzipien zu erklären, aber nicht, um zu komplexen Ausdrücken zu ermutigen.

Wenn man dennoch einmal aus irgendeinem Grund die Ausführungs- oder Bewertungsreihenfolge experimentell bestimmen möchte, kann man sich mit folgendem Trick behelfen. Man macht die Ausführungsreihenfolge durch protokollierende Funktionen sichtbar. Für unser aktuelles Beispiel sieht das so aus:

int result = a[f1()] = a[f2()] + a[f3() ? f4() : f5() % f6()] * a[f7()];
private static int f1()
{
// Die anderen Funktionen analog
System.out.println("f1");
return 1; // Wir brauchen irgendeinen Returnw.
}

Einzig f3() weicht etwas ab und braucht ein bool’sches Ergebnis. Die Funktionen protokollieren hier übrigens in der Reihenfolge ihrer Nummerierung, also so, wie sie von links nach rechts dastehen (wobei entweder f4() oder f5() und f6() ausgeführt werden).

17.2.Auflistung nach Priorität

Die Tabelle unten listet die Java-Operatoren in der Reihenfolge ihres Vorrangs auf. Die Tabelle zeigt in der linken Spalte zu jedem Operator die Assoziativität. Außer den Zuweisungsoperatoren sind für rechtsassoziativ nur die unären Operatoren und der Conditional-Operator als eine Regel zu nennen, die man sich eventuell merkt. Bei new und den Lambdas dürften Fragen der Assoziativität in der Praxis kaum jemals eine Rolle spielen. In der mittleren Spalte findet man die Operatoren. In der rechten Spalte stehen ihre Bezeichnungen in der selben Reihenfolge. Der Eintrag "2 Postfix" steht für zwei Postfix-Operatoren, nämlich für Postinkrement und Postdekrement. Analoges gilt für "2 Prefix".