Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Ich versuche in Netbeans ein funktionierendes GWT-Projekt anzulegen. Was mit GWT4NB als Java-Webapplication eigentlich auch sehr einfach aussieht.
Allerdings bekomme ich beim Ausführen immer folgende Fehlermeldung:

Show

[EVIL]GWT Compiling client-side code. Fehler: Hauptklasse com.google.gwt.dev.GWTCompiler konnte nicht gefunden oder geladen werden C:_Dev\Netbeans\GWT\HelloGwt bproject\build-gwt.xml:326: The following error occurred while executing this line: C:_Dev\Netbeans\GWT\HelloGwt

bproject\build-gwt.xml:356: Java returned: 1[/EVIL]

Das wäre diese Stelle.

[XML]

[/XML]

Gehe ich recht in der Annahme das hier die Compiler-Anweisungen der GWT-Versionen 1.5-2.5 angegeben werden?(Nutze 2.7!)
Kennt sich jemand mit dem Problem aus? Bin für jede Hilfe dankbar.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Nachdem wir die grundlegenden Konzepte von Java besprochen haben, wollen wir ganz dem Zitat von Dennis M. Ritchie folgen, der sagt:

»Eine neue Programmiersprache lernt man nur, wenn man in ihr Programme schreibt.«

In diesem Abschnitt nutzen wir den Java-Compiler und Interpreter von der Kommandozeile. Wer gleich eine ordentliche Entwicklungsumgebung wünscht, der kann problemlos diesen Teil überspringen und bei den IDEs fortfahren. Die Beispiele im Buch basieren auf Windows.

Der Quellcode eines Java-Programms lässt sich so allein nicht ausführen. Ein spezielles Programm, der Compiler (auch Übersetzer genannt), transformiert das geschriebene Programm in eine andere Repräsentation. Im Fall von Java erzeugt der Compiler die DNA jedes Programms, den Bytecode.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.8.1    Ein Quadratzahlen-Programm 

Das erste Java-Programm soll einen Algorithmus ausführen, der die Quadrate (engl. squares) der Zahlen von 1 bis 4 ausgibt. Wir setzen den Quellcode (engl. source code) beispielhaft in das Verzeichnis C:\firstlove. Der Name der Quellcodedatei ist Squares.java. Da der Quellcode reiner Text ist, kann er mit jedem Texteditor angelegt und editiert werden.

Hinweis für Windows-Nutzer: Unter Windows findet sich der Editor Notepad unter StartProgrammeZubehörEditor. Beim Abspeichern mit Notepad mithilfe von DateiSpeichern unter… muss bei Dateiname Squares.java stehen und beim Dateityp Alle Dateien ausgewählt sein, damit der Editor nicht automatisch die Dateiendung .txt vergibt.

Listing 1.1    C:\firstlove\Squares.java

/** * Erstes Java-Beispielprogramm. * @version 1.02 26 Dez 2013 * @author Christian Ullenboom

*/

public class Squares {

static int quadrat( int n ) {

return n * n;

}

static void ausgabe( int n ) {

for ( int i = 1; i <= n; i = i + 1 ) {

String s = "Quadrat(" + i + ") = " + quadrat( i );

System.out.println( s );

} }

public static void main( String[] args ) {

ausgabe( 4 );

}

}

Die ganze Programmlogik sitzt in einer Klasse Squares, die drei Methoden enthält. Alle Methoden in einer objektorientierten Programmiersprache wie Java müssen in Klassen platziert werden. Die erste Methode, quadrat(int), bekommt als Übergabeparameter eine ganze Zahl und berechnet daraus die Quadratzahl, die sie anschließend zurückgibt. Eine weitere Methode ausgabe(int) übernimmt die Ausgabe der Quadratzahlen bis zu einer vorgegebenen Grenze. Die ausgabe-Methode bedient sich dabei der Methode quadrat(int). Zum Schluss muss es noch ein besonderes Unterprogramm main(String[]) geben, das für den Java-Interpreter den Einstiegspunkt bietet. Die Methode main(String[]) ruft dann die Methode ausgabe(int) auf.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.8.2    Der Compilerlauf 

Wir wechseln zur Eingabeaufforderung (Konsole) und in das Verzeichnis mit dem Quellcode. Liegt die Quellcodedatei vor, übersetzt der Compiler sie in Bytecode. Für unsere Java-Klasse in der Datei Squares.java heißt das:

C:\firstlove>javac Squares.java

Der Compiler legt nun – vorausgesetzt, das Programm war fehlerfrei – die Datei Squares.class an. Diese enthält den Bytecode.

Bei Aufruf von javac muss die zu übersetzende Datei komplett mit Dateiendung angeben werden. Doch auch Wildcards sind möglich: So übersetzt javac *.java alle Java-Klassen im aktuellen Verzeichnis.

Die Beachtung der Groß- und Kleinschreibung ist zwar unter Windows nicht wichtig, doch sollte sie eingehalten werden.

Compilerfehler

Findet der Compiler in einer Zeile einen syntaktischen Fehler, so meldet er diesen unter der Angabe der Datei und der Zeilennummer. Nehmen wir noch einmal unser Quadratzahlen-Programm, und bauen wir in der quadrat(int)-Methode einen Fehler in Zeile 9 ein (das Semikolon fällt der Löschtaste zum Opfer). Der Compilerdurchlauf meldet:

Squares.java:9: ';' expected. return n * n ^

1 error

Wenn der Compiler aufgrund eines syntaktischen Fehlers eine Übersetzung in Java-Bytecode nicht durchführen kann, sprechen wir von einem Compilerfehler (engl. compile-time error) oder Übersetzungsfehler. Auch wenn der Begriff »Compilerfehler« so klingt, als ob der Compiler selbst einen Fehler hat, ist doch unser Programm fehlerhaft.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.8.3    Die Laufzeitumgebung 

Der vom Compiler erzeugte Bytecode ist kein üblicher Maschinencode für einen speziellen Prozessor, da Java als plattformunabhängige Programmiersprache entworfen wurde, die sich also nicht an einen physikalischen Prozessor klammert – Prozessoren wie Intel-, AMD- oder PowerPC-CPUs können mit diesem Bytecode nichts anfangen. Hier hilft eine Laufzeitumgebung weiter. Diese liest die Bytecode-Datei Anweisung für Anweisung aus und führt sie auf dem konkreten Mikroprozessor aus.

Der Interpreter java bringt das Programm zur Ausführung:

C:\firstlove>java SquaresQuadrat(1) = 1Quadrat(2) = 4Quadrat(3) = 9

Quadrat(4) = 16

Als Argument bekommt die Laufzeitumgebung java den Namen der Klasse, die eine main(…)-Methode enthält und somit als ausführbar gilt. Die Angabe ist nicht mit der Endung .class zu versehen, da hier kein Dateiname, sondern ein Klassenname gefordert ist.

Ab Java 11 lassen sich sogenannte Single-File Source-Code Programs auch ohne eine Übersetzung durch die Laufzeitumgebung ausführen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.8.4    Häufige Compiler- und Interpreter-Probleme 

Arbeiten wir auf der Kommandozeilenebene (Shell) ohne integrierte Entwicklungsumgebung, können verschiedene Probleme auftreten. Ist der Pfad zum Compiler nicht richtig gesetzt, gibt der Kommandozeileninterpreter eine Fehlermeldung der Form

$ javac Squares.javaDer Befehl ist entweder falsch geschrieben oder konnte nicht gefunden werden.

Bitte überprüfen Sie die Schreibweise und die Umgebungsvariable 'PATH'.

aus. Unter Unix lautet die Meldung gewohnt kurz:

Die Lösung ist hier also, javac in den Suchpfad aufzunehmen, wie wir es vorher schon beschrieben haben.

War der Compilerdurchlauf erfolgreich, können wir den Interpreter mit dem Programm java aufrufen. Verschreiben wir uns bei dem Namen der Klasse oder fügen wir unserem Klassennamen das Suffix .class hinzu, so meckert der Interpreter. Beim Versuch, die nicht existente Klasse Q zum Leben zu erwecken, schreibt der Interpreter auf den Fehlerkanal:

$ java QFehler: Hauptklasse Q konnte nicht gefunden oder geladen werden.

Ursache: java.lang.ClassNotFoundException: Q

Ist der Name der Klassendatei korrekt, hat aber die Hauptmethode keine Signatur public static void main(String[]), so kann der Java-Interpreter keine Methode finden, bei der er mit der Ausführung beginnen soll. Verschreiben wir uns bei der main(…)-Methode in Squares, folgt die Fehlermeldung:

$ java SquaresFehler: Hauptmethode in Klasse Squares nicht gefunden. Definieren Sie die Hauptmethode als: public static void main(String[] args):

oder eine JavaFX-Anwendung muss javafx.application.Application erweitern

Der Java-Compiler und die Java-Laufzeitumgebung haben einen Schalter -help, der weitere Informationen zeigt.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 2

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Als Laufzeitumgebung ist das JRE geeignet, und mit dem JDK können auf der Kommandozeile Java-Programme übersetzt und ausgeführt werden – angenehm ist das allerdings nicht. Daher haben unterschiedliche Hersteller in den letzten Jahren einigen Aufwand betrieben, um die Java-Entwicklung zu vereinfachen. Moderne Entwicklungsumgebungen bieten gegenüber einfachen Texteditoren den Vorteil, dass sie besonders Spracheinsteigern helfen, sich mit der Syntax anzufreunden. Eclipse beispielsweise unterkringelt ähnlich wie moderne Textverarbeitungssysteme fehlerhafte Stellen. Zusätzlich bieten die IDEs die notwendigen Hilfen beim Entwickeln, wie etwa farbige Hervorhebung, automatische Codevervollständigung und Zugriff auf Versionsverwaltungen oder auch Wizards, die mit ein paar Eintragungen Quellcode etwa für grafische Oberflächen oder Webservice-Zugriffe generieren.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.9.1    Eclipse IDE 

Um die alte WebSphere-Reihe und die Umgebung Visual Age for Java abzulösen, entwickelte IBM Eclipse (http://www.eclipse.org). Im November 2001 veröffentlichte IBM die IDE als Open-Source-Software, und 2003/2004 gründete IBM mit der Eclipse Foundation ein Konsortium, das die Weiterentwicklung bestimmt. Diesem Konsortium gehören unter anderem die Mitglieder BEA, Borland, Computer Associates, Intel, HP, SAP und Sybase an. Eclipse steht heute unter der Common Public License und ist als quelloffene Software für jeden kostenlos zugänglich. Alle halbe Jahre gibt es ein Update.

Eclipse macht es möglich, Tools als sogenannte Plugins zu integrieren. Viele Anbieter haben ihre Produkte schon für Eclipse angepasst, und die Entwicklung läuft weltweit in einem raschen Tempo.

Da NetBeans ebenfalls frei ist und um andere Fremdkomponenten bereichert werden kann, zog sich IBM damals den Groll von Sun zu. Sun warf IBM vor, die Entwicklergemeinde zu spalten und noch eine unnötige Entwicklungsumgebung auf den Markt zu werfen, wo doch NetBeans schon so toll sei. Nun ja, die Entwickler haben entschieden: Statistiken sehen Eclipse deutlich vorne, auf Platz zwei steht IntelliJ, NetBeans ist weit abgeschlagen.

Eclipse ist ein Java-Produkt mit einer nativen grafischen Oberfläche, das flüssig seine Arbeit verrichtet – genügend Speicher vorausgesetzt (> 512 MiB). Die Arbeitszeiten sind auch deswegen so schnell, weil Eclipse mit einem sogenannten inkrementellen Compiler arbeitet. Speichert der Anwender eine Java-Quellcodedatei, übersetzt der Compiler diese Datei automatisch. Dieses Feature nennt sich Autobuild.

Treibende Kraft hinter Eclipse war Erich Gamma, der 2011 zu Microsoft ging und dort Teamleiter für Microsoft Visual Studio Code (kurz VSC) wurde. VSC ist ein neuer Editor und eine schlanke Entwicklungsumgebung. Für die Webentwicklung hat VSC schnell Freunde gefunden, und auch Java-Entwicklung ist möglich, wobei die Refactoring-Möglichkeiten noch hinter Eclipse oder IntelliJ zurückstehen. Das kann sich jedoch schnell ändern.

Eclipse-IDE-Pakete und Installation

Die Entwicklungsumgebung Eclipse ist größtenteils in Java programmiert, und die aktuelle Eclipse IDE 2020-03 benötigt zur Ausführung mindestens eine Java-Version 8. Da Teile wie die grafische Oberfläche in C implementiert sind, ist Eclipse nicht 100 % pures Java, und beim Download ist auf das passende System zu achten.

Für die Installation unter Windows, Linux und macOS gibt es zwei Möglichkeiten:

  • mit einem nativen Installer, der Komponenten aus dem Netz lädt

  • über ein Eclipse-Paket: ZIP-Archiv auspacken und starten

Eclipse gliedert sich in unterschiedliche Pakete. Die wichtigsten sind:

  • Eclipse IDE for Java Developers: Dies ist die kleinste Version zum Entwickeln von Java SE-Anwendungen.

  • Eclipse IDE for Enterprise Java Developers (ehemals Eclipse IDE for Java EE Developers): Diese Version enthält diverse Erweiterungen für die Entwicklung von Webanwendungen und Java-Enterprise-Applikationen. Dazu zählen unter anderem Data Tools Platform, Eclipse Git Team Provider, Enterprise Tools, JavaScript Development Tools, Maven Integration for Eclipse und Eclipse XML Editors and Tools.

  • Eclipse IDE for C/C++ Developers: Dies ist Eclipse als Entwicklungsumgebung für C(++)-Programmierer. Es ist das kleinste Paket, da es ausschließlich für die Programmiersprache C(++) und nicht für Java konzipiert ist.

Die Pakete sind unter https://www.eclipse.org/downloads/packages/ zum Download verlinkt.

Auf der Download-Seite sind neben der aktuellen Version auch die letzten Releases zu finden. Die Hauptversionen heißen Maintenance Packages. Neben ihnen gibt es Stable Builds und für Mutige die Integration Builds und Nightly Builds, die einen Blick auf kommende Versionen erlauben. Standardmäßig sind Beschriftungen der Entwicklungsumgebung in englischer Sprache, doch gibt es mit den Eclipse Language Packs Übersetzungen etwa für Deutsch, Spanisch, Italienisch, Japanisch, Chinesisch und weitere Sprachen. Für die Unterprojekte (WST, JST) gibt es individuelle Updates. Die aktuellen Releases und Builds finden sich unter http://download.eclipse.org/eclipse/downloads.

Eclipse IDE entpacken und starten

Es ist nicht unbedingt erforderlich, Eclipse im typischen Sinne mit einem Installer zu installieren. Die folgenden Schritte beschreiben die Benutzung unter Windows. Nach dem Download und Auspacken des ZIP-Archivs eclipse-jee-2020-03-R-incubation-win32-x86_64.zip gibt es einen Ordner eclipse mit der ausführbaren Datei eclipse.exe. Das Eclipse-Verzeichnis lässt sich frei wählen.

Nach dem Start von eclipse.exe folgen ein Willkommensbildschirm und ein Dialog, in dem der Workspace ausgewählt werden muss (siehe Abbildung 1.3). Mit einer Eclipse-Instanz ist immer ein Workspace verbunden: Das ist ein Verzeichnis, in dem Eclipse-Konfigurationsdaten, Dateien zur Änderungsverfolgung und standardmäßig Quellcode-Dateien sowie Binärdateien gespeichert sind. Der Workspace kann später gewechselt werden, jedoch ist nur ein Workspace zur gleichen Zeit aktiv; er muss zu Beginn der Eclipse-Sitzung festgelegt werden. Wir belassen es bei dem Home-Verzeichnis des Benutzers und setzen einen Haken bei Use this as the default and do not ask again, um beim nächsten Start nicht noch einmal gefragt zu werden.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 1.3    Den Workspace in Eclipse auswählen

Nun folgt das Hauptfenster von Eclipse. Es zeigt zunächst ein Hilfsangebot inklusive Tutorials für Einsteiger sowie Erklärungen, was in der Version neu ist, für Fortgeschrittene. Ein Klick auf das × rechts vom abgerundeten Reiter Welcome schließt diese Ansicht.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 1.4    Die Eclipse IDE nach dem Start

Java 14-Unterstützung installieren

Die Eclipse IDE 2020-03 (Build 4.15) unterstützt durch den eigenen Compiler unterschiedliche Java-Versionen bis Java 13, Java 14 allerdings nur mit einem Extra-Plugin. Das Plugin ist im Build 4.16 integriert, was bei Drucklegung des Buches (Juni 2020) verfügbar sein sollte.

Gehen wir auf Help • Eclipse Marketplace … Dort geben wir hinter Find den Suchbegriff »Java 14« ein. Es folgen Suchergebnisse mit Java 14 Support for Eclipse 2020-03 (4.15). Ein Klick auf Install startet den Download und die Installation. Wir starten neu; danach ist Java 14 in den Menüeinträgen vorhanden.

Java 14 einstellen, »Installed JREs«

Startet Eclipse, wertet es die Umgebungsvariable PATH aus, in der die Java-Laufzeitumgebung eingetragen ist. Das ist auch die Java-Version, die Eclipse als Java SE-Implementierung annimmt, und Eclipse extrahiert auch die Java-Dokumentation aus den Quellen. Die Java-Version, mit der Eclipse läuft, muss aber nicht identisch mit der Version sein, mit der wir entwickeln. Daher wollen wir prüfen, dass zur Entwicklung Java 14 eingetragen ist.

Die Einstellung über das JRE/JDK und die Java-Version kann global oder lokal für jedes Projekt gesetzt werden. Wir bevorzugen eine globale Einstellung. Wählen wir im Menü Window • Preferences, was einen umfassenden Konfigurationsdialog öffnet. Gehen wir dann im Baum links auf Java und weiter zu Installed JREs. Durch eine ältere Java-Installation kann etwas anderes als Java 14 eingetragen sein.

Klicken wir auf Search…, und wählen wir im Dialog das Verzeichnis aus, in dem das OpenJDK entpackt liegt, etwa C:\Program Files\Java. Nach Beenden des Dialogs dauert es etwas, während Eclipse uns alle Java-Installation in diesem Ordner heraussucht.

Die Liste kann je nach Installation länger oder kürzer sein, doch sollte sie den Eintrag jdk-14 enthalten. Wir setzen ein Häkchen beim gewünschten JDK, in unserem Fall bei JDK-14, das dann fett hervorgehoben wird und (default) wird. Nach dem Klick auf Apply übernimmt Eclipse die Einstellungen, aber der Konfigurationsdialog bleibt offen. Wir sind mit den Einstellungen nämlich noch nicht fertig.

Java 14 einstellen, »Execution Environments«

Eclipse hat eine Abstraktionsschicht für Laufzeitumgebungen, sodass Projekte ausdrücken können: »irgendeine Java 14-Installation«, ohne sich an OpenJDK, Oracle JDK usw. binden zu müssen. Die Abstraktion erreicht Eclipse über Execution Environments, das im Konfigurationsdialog direkt unter Installed JREs liegt.

Nun ist die Ausführungsumgebung auf Java 14 gestellt. Wir können Java 14-Projekte mit dieser Umgebung anlegen, die durch diese Abbildung vom eingestellten OpenJDK ausgeführt werden. Sollte auf einem anderen Rechner das Projekt importiert werden und liegt die JDK-Installation in einem komplett anderen Pfad, so ist nur in der lokalen Eclipse-Installation das Verzeichnis zum JDK zu setzen, nicht aber in den Java-Projekten.

Java-Compiler auf Version 14 stellen

Eclipse nutzt einen eigenen Compiler und nicht den Compiler aus dem JDK. Wir müssen daher sicherstellen, dass der Compiler die Syntax von Java 14 unterstützt. Im Konfigurationsdialog (Window > Preferences) suchen wir Java • Compiler. Rechts im Feld stellen wir sicher, dass bei Compiler compliance level auch die Version 14 eingestellt ist.

Wir haben also zwei Einstellungen vorgenommen: Wir haben zuerst die passende Laufzeitumgebung konfiguriert und dann den Compiler auf die gewünschte Version gesetzt. Jetzt kann der Konfigurationsdialog mit Apply and Close geschlossen werden.

Das erste Projekt anlegen

Nach dem Start von Eclipse muss ein Projekt angelegt (oder ein existierendes eingebunden) werden – ohne dieses lässt sich kein Java-Programm ausführen. Im Menü ist dazu FileNewOther auszuwählen. Dann öffnet sich ein Dialog mit allen möglichen Projekten, die Eclipse anlegen kann.

Wählen wir Java Project. Der Klick auf Next blendet einen neuen Dialog für weitere Einstellungen ein. Unter Project name geben wir einen Namen für unser Projekt ein, zum Beispiel »Insel«. Mit dem Projekt ist ein Pfad verbunden, in dem die Quellcodes, Ressourcen und Klassendateien gespeichert sind. Standardmäßig speichert Eclipse die Projekte im Workspace ab. Wir könnten zwar einen anderen Ordner wählen, belassen es hier aber bei einem Unterverzeichnis im Workspace. Im Bereich JRE sollte die erste der drei Optionen gewählt sein: Use an Execution Environment JRE steht auf JavaSE-14.Die Schaltfläche Finish schließt das Anlegen ab. Da es sich um ein Java-Projekt handelt, möchte Eclipse je nach vorheriger Perspektive unter Umständen in eine Java-Ansicht gehen – den folgenden Dialog sollten wir mit Yes bestätigen.

Jetzt arbeiten wir mit einem Teil von Eclipse, der sich Workbench nennt. Welche Ansichten Eclipse platziert, bestimmt die Perspektive (engl. perspective). Zu einer Perspektive gehören Ansichten (engl. views) und Editoren. Im Menüpunkt WindowOpen Perspective lässt sich diese Perspektive ändern, doch um in Java zu entwickeln, ist die Java-Perspektive im Allgemeinen die beste. Das ist die, die Eclipse auch automatisch gewählt hat, nachdem wir das Java-Projekt angelegt haben.

Jede Ansicht lässt sich per Drag & Drop beliebig umsetzen. Die Ansicht Outline oder Task List auf der rechten Seite lässt sich auf diese Weise einfach an eine andere Stelle schieben – unter dem Package Explorer ist sie meistens gut aufgehoben.

Verzeichnisstruktur für Java-Projekte *

Ein Java-Projekt braucht eine ordentliche Ordnerstruktur, und hier gibt es zur Organisation der Dateien unterschiedliche Ansätze. Die einfachste Form ist, Quellen, Klassendateien und Ressourcen in ein Verzeichnis zu setzen. Doch diese Mischung ist in der Praxis nicht vorteilhaft.

Im Allgemeinen finden sich zwei wichtige Hauptverzeichnisse: src für die Quellen und bin für die erzeugten Klassendateien. Diese Einteilung nutzt Eclipse standardmäßig, wenn ein neues Java-Projekt angelegt wird. Ein eigener Ordner lib ist sinnvoll für Java-Bibliotheken.

Maven Standard Directory Layout

Noch weiter in der Aufteilung geht ein Projekt nach dem Maven-Standard. Es definiert ein Standard Directory Layout mit den folgenden wichtigen Ordnern (siehe Tabelle 1.4).

Ordner

Inhalt

src/main/java

Quellen

src/main/resources

Ressourcen

src/main/config

Konfigurationen

src/main/scripts

Skripte

src/test/java

Testfälle

src/test/resources

Ressourcen für Testfälle

src/site

Website

target

Ergebnis eines Builds

LICENSE.txt

Lizenzform des Projekts (etwa Apache, BSD, GPL)

NOTICE.txt

Hinweise oder Abhängigkeiten zu anderen Projekten

README.txt

Lies-mich-Datei

Tabelle 1.4    Standard Directory Layout

Einen lib-Ordner gibt es nicht, den verwalten Maven-Projekte an anderer Stelle, und zwar in einem lokalen Repository. Wenn das Projekt gebaut wird und zum Beispiel zu einem Webprojekt wird, werden die JAR-Dateien automatisch mit in den target-Ordner gesetzt.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Die Eclipse IDE for Enterprise Java Developers enthält Maven und kann direkt Maven-Projekte anlegen und folglich auch die Verzeichnisstruktur. Dazu wählen wir File • New • Other … • Maven/Maven Project. Für ein erstes Miniprojekt reicht es, den Schalter bei Create a simple project (skip archetype selection) zu setzen. Dann sind zwingend eine Group Id (globale Projektkennung wie ein Paketname) und Artifact Id (Name des JARs) anzugeben – die Versionsnummer ist im Dialog vorbelegt. Dann rappelt es in der Kiste, und Maven beginnt mit dem Download einiger Dateien. Am Ende erhalten wir vier src-Ordner, einen target-Ordner und eine Datei pom.xml für die Maven-Beschreibung. Vorhandene Java-Projekte lassen sich auch in Maven-Projekte konvertieren (dabei ändert sich aber nicht die Verzeichnisstruktur). Dazu wählen wir im Package Explorer das Projekt aus und im Kontextmenü Configure • Convert to Maven project. Wir kommen in Abschnitt 15.10, »Maven: Build-Management und Abhängigkeiten auflösen«, detaillierter auf Maven zurück.

Eine Klasse hinzufügen

Dem Projekt können nun Dateien wie Klassen, Java-Archive, Grafiken oder andere Inhalte hinzugefügt werden. Auch lassen sich in das Verzeichnis nachträglich Dateien einfügen, die Eclipse dann direkt anzeigt. Doch beginnen wir mit dem Hinzufügen einer Klasse aus Eclipse. Dazu aktivieren wir über FileNewClass ein neues Fenster. Das Fenster öffnet sich ebenfalls nach der Aktivierung der Schaltfläche mit dem grünen C in der Symbolleiste oder im Kontextmenü unter src.

Notwendig ist der Name der Klasse; hier Squared. Wir wollen auch einen Schalter für public static void main(String[] args) setzen, damit wir gleich eine Einstiegsmethode haben, in der sich unser erster Quellcode platzieren lässt. Nach dem Finish fügt Eclipse diese Klasse unserem Projektbaum hinzu, erstellt also eine Java-Datei im Dateisystem, und öffnet sie gleichzeitig im Editor. In die main(…)-Methode schreiben wir zum Testen:

int n = 2;

System.out.println( "Quadrat: " + n * n );

Eclipse besitzt keine Schaltfläche zum Übersetzen. Zum einen lässt Eclipse automatisch einen Compiler im Hintergrund laufen (sonst könnten wir die Fehlermeldungen zur Tippzeit nicht sehen), und zum anderen nimmt Eclipse das Speichern zum Anlass, einen Übersetzungsvorgang zu starten.

Übersetzen und ausführen

Damit Eclipse eine bestimmte Klasse mit einer main(…)-Methode ausführt, können wir mehrere Wege gehen. Wird zum ersten Mal Programmcode einer Klasse ausgeführt, können wir rechts neben dem grünen Kreis mit dem Play-Symbol auf den Pfeil klicken und im Popup-Menü Run As und anschließend Java Application auswählen. Ein anderer Weg: (Alt)+ (ª)+(X), dann (J).

Anschließend startet die JVM die Applikation. Assoziiert Eclipse einmal mit einem Start eine Klasse, reicht in Zukunft ein Aufruf mit (Strg)+(F11). Unten in der Ansicht mit der Aufschrift Console ist die Ausgabe zu sehen.

Start eines Programms ohne Speicheraufforderung

In der Standardeinstellung fragt Eclipse vor der Übersetzung und Ausführung mit einem Dialog nach, ob noch nicht gesicherte Dateien gespeichert werden sollen. Dort kann das Häkchen gesetzt werden, das die Quellen immer speichert.

In der Regel soll die Entwicklungsumgebung selbst die veränderten Dateien vor dem Übersetzen speichern. Es gibt noch einen anderen Weg, dies einzustellen. Dazu muss eine Einstellung in der Konfiguration vorgenommen werden: Unter WindowPreferences öffnen wir wieder das Konfigurationsfenster und wählen den Zweig Run/Debug und dort den Unterzweig Launching. Im Rahmen rechts – betitelt mit Save required dirty editors before launching – aktivieren wir dann Always.

Projekt einfügen, Workspace für die Programme wechseln

Alle Beispielprogramme im Buch gibt es im Netz unter http://tutego.de/javabuch. Die Beispielprogramme sind in einem kompletten Eclipse-Workspace zusammengefasst, lassen sich aber natürlich in jeder anderen IDE nutzen. Da Eclipse nur einen Workspace gleichzeitig geöffnet halten kann, müssen wir mit FileSwitch Workspace… • Other den Workspace neu setzen. Eclipse beendet sich dann und startet anschließend mit dem neuen Workspace neu.

Plugins für Eclipse

Zusätzliche Anwendungen, die in Eclipse integriert werden können, werden Plugins genannt. Durch Plugins kann die IDE erweitert werden, und so kann ein Entwickler auch in Bereiche wie Webentwicklung mit PHP oder Mainframe-Anwendungen mit COBOL vordringen, die nichts mit Java zu tun haben.

Ein Plugin besteht aus einer Sammlung von Dateien in einem Verzeichnis oder Java-Archiv. Für die Installation gibt es mehrere Möglichkeiten: Eine davon besteht darin, den Update-Manager zu bemühen, der automatisch im Internet das Plugin lädt; die andere besteht darin, ein Archiv zu laden, das in das plugin-Verzeichnis von Eclipse entpackt wird. Beim nächsten Start erkennt Eclipse automatisch das Plugin und integriert es. (Bisher war immer ein Neustart von Eclipse bei hinzugenommenen Plugins nötig.)

Durch die riesige Anzahl an Plugins ist nicht immer offensichtlich, welches Plugin gut ist. Unter http://tutego.de/java/eclipse/plugin/eclipse-plugins.html ist eine Auswahl zu finden.

Auch ist nicht offensichtlich, welches Plugin mit welchem anderen Plugin gut zusammenspielt; aufeinander abgestimmte Sammlungen sind da Gold wert. Mit dem Paket Eclipse IDE for Enterprise Java Developers sorgt die Eclipse Foundation für die Java Enterprise-Entwicklung schon vor: Es enthält alles, um Webapplikationen oder Webservices zu entwickeln. Die JBoss Tools (http://tools.jboss.org/) bauen auch wieder darauf auf und haben JBoss-Produkte im Fokus, etwa Hibernate, aber auch jBPM oder JMX. Die Spring Tool Suite (https://spring.io/tools) ihrerseits erweitert Eclipse um Lösungen zum Spring Framework bzw. Spring Boot.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.9.2    IntelliJ IDEA 

Dass Unternehmen mit Entwicklungsumgebungen noch Geld verdienen können, zeigt JetBrains, ein aus Tschechien stammendes Softwarehaus. Die Java-Entwicklungsumgebung IntelliJ IDEA (https://www.jetbrains.com/idea) gibt es in zwei Editionen:

  • Die freie, quelloffene Grundversion, die alles abdeckt, was zur Java SE-Entwicklung nötig ist, nennt sich Community Edition. Der Quellcode ist auf GitHub für jeden einsehbar.

  • Die kommerzielle Ultimate Edition richtet sich an Java Enterprise-Entwickler. Sie kostet 499 EUR im ersten Jahr, 399 EUR im zweiten Jahr und 299 EUR in den darauffolgenden Jahren.

Die Basisversion enthält auch schon einen GUI-Builder, Unterstützung für Test-Frameworks und Versionsverwaltungssysteme und ist vom Funktionsumfang mit Eclipse IDE for Java Developers vergleichbar. Die freie Community-Version ist im Bereich Maven oder Frameworks wie Java EE oder Spring sehr eingeschränkt, hier ist die Ultimate Edition unverzichtbar – vieles davon bekommen Nutzer der freien Eclipse IDE for Enterprise Java Developers bereits umsonst. JetBrains ist Entwickler der Programmiersprache Kotlin, weshalb natürlich die Unterstützung in IntelliJ optimal ist. Studierende und Lehrer können die Ultimate-Version beantragen und bekommen sie dann kostenlos. Die Unterschiede zeigt http://www.jetbrains.com/idea/features/editions_comparison_matrix.html auf.

IntelliJ Community installieren

Unter http://www.jetbrains.com/idea/download/ können Sie die freie Community Edition herunterladen, genauso wie die Ultimate-Version, die Sie 30 Tage lang testen können. Nach dem Download führen wir das Installationsprogramm aus. Zuerst erscheint ein Startfenster. Weiter führt zum nächsten Dialog, in dem wir ein Verzeichnis für die Installation auswählen. Außerdem können wir einen Startmenüordner für die IntelliJ IDEA-Verknüpfung erstellen und auswählen, ob eine Desktop-Verknüpfung erstellt werden soll und ob Java-Dateien mit dem IntelliJ-Editor verknüpft werden sollen. Nach dem Klick auf Weiter dauert es etwas, bis die Installation abgeschlossen ist.

Erstes Projekt anlegen

Ist IntelliJ gestartet, zeigt sich ein Dialog mit der Auswahlmöglichkeit Create New Project. Auf der linken Seite sind mehrere Optionen angezeigt. Wir wählen Java und klicken auf Next. Im nächsten Fenster setzen wir kein Häkchen und klicken ebenfalls auf Next. Dann ist das Projekt zu benennen, und Finish legt das Projekt an. Die Arbeitsoberfläche erscheint. Links im Bereich ist das Projekt zu sehen. Wird es ausgefaltet, sind die drei Ordner idea, out und src zu sehen.

Eine Klasse hinzufügen

Wird das Kontextmenü auf dem src-Ordner aktiviert, öffnen New und dann Java Class einen kleinen Dialog, in dem wir den Klassennamen eintragen können. Nennen wir die Klasse Squared und gehen wir weiter.

Im Editor wollen wir das Programm vervollständigen:

public class Squared {

public static void main(String[] args) {

int n = 2;

System.out.println( "Quadrat: " + n * n );

}

}

Übersetzen und ausführen

IntelliJ hat keine Speichern-Schaltfläche und arbeitet im Hintergrund automatisch. Um das Programm auszuführen, können wir in der Zeile mit der main-Methode auf das grüne Dreieck klicken und das Programm starten.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

1.9.3    NetBeans 

NetBeans (http://netbeans.org) bietet komfortable Möglichkeiten zur Java SE- und Java Enterprise-Entwicklung mit Editoren und Wizards für die Erstellung grafischer Oberflächen und Webanwendungen. Die Hauptentwicklung leistete damals Sun Microsystems, doch als Oracle Sun übernahm, hatten Entwicklungsumgebungen keine große Priorität im Datenbankhaus. Es gab zwar Weiterentwicklungen, doch schleppte sich Oracle eher von Version zu Version. Oracle hat letztendlich die Codebasis an die Apache Foundation übergeben und sich damit von der IDE getrennt. Das neue Zuhause ist https://netbeans.apache.org/. Der Marktanteil liegt bei rund 10 %.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 3

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Sun gab ein kleines Büchlein mit dem Titel »Hello World(s) – From Code to Culture: A 10 Year Celebration of Java Technology« heraus (ISBN 0131888676), das Informationen zur Entstehung von Java bietet. Weitere Online-Informationen zur Entwicklermannschaft und zum *7-Projekt liefern http://tutego.de/go/star7 sowie http://tutego.de/go/javasaga. Die virtuelle Maschine selbst gibt es für Geschichtsliebhaber in allen Versionen unter http://tutego.de/go/javaarchive.

Java wird von vielen Unternehmen geliebt; Top-Unternehmen wie LinkedIn, Netflix, eBay, Pinterest, Groupon, Spotify, Pandora, Square, Trivago und TripAdvisor vertrauen auf Java. Es ist schon eine Ausnahme, wenn ein Unternehmen nicht auf Java setzt.

Eclipse und IntelliJ sind in der Regel die Standardwerkzeuge der Softwareentwickler. Entwickler, die Eclipse einsetzen, können in der Hilfe unter http://tutego.de/go/eclipsehelp viel Interessantes erfahren und sollten diverse Plugins als Ergänzung evaluieren. Die IDE mit ihren Tastenkürzeln zu beherrschen macht einen guten Softwareentwickler aus.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 4

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

»Wenn ich eine Oper hundertmal dirigiert habe, dann ist es Zeit, sie wieder zu lernen.«

– Arturo Toscanini (1867–1957)

Ein Programm in Java wird nicht umgangssprachlich beschrieben, sondern ein Regelwerk und eine Grammatik definieren die Syntax und die Semantik. In den nächsten Abschnitten werden wir kleinere Beispiele für Java-Programme kennenlernen, und dann ist der Weg frei für größere Programme.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1    Elemente der Programmiersprache Java 

Wir wollen im Folgenden über das Regelwerk, die Grammatik und die Syntax der Programmiersprache Java sprechen und uns unter anderem über die Unicode-Kodierung, Tokens sowie Bezeichner Gedanken machen. Bei der Benennung einer Methode zum Beispiel dürfen wir aus einer großen Anzahl Zeichen wählen; der Zeichenvorrat nennt sich Lexikalik.

Die Syntax eines Java-Programms definiert die Tokens und bildet so das Vokabular. Richtig geschriebene Programme müssen aber dennoch nicht korrekt sein. Unter dem Begriff Semantik fassen wir daher die Bedeutung eines syntaktisch korrekten Programms zusammen. Die Semantik bestimmt, was das Programm macht. Die Abstraktionsreihenfolge ist: Lexikalik, Syntax und Semantik. Der Compiler durchläuft diese Schritte, bevor er den Bytecode erzeugen kann.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.1    Token 

Ein Token ist eine lexikalische Einheit, die dem Compiler die Bausteine des Programms liefert. Der Compiler erkennt an der Grammatik einer Sprache, welche Folgen von Zeichen ein Token bilden. Für Bezeichner heißt dies beispielsweise: »Nimm die nächsten Zeichen, solange auf einen Buchstaben nur Buchstaben oder Ziffern folgen.« Eine Zahl wie 1982 bildet zum Beispiel ein Token durch folgende Regel: »Lies so lange Ziffern, bis keine Ziffer mehr folgt.« Bei Kommentaren bilden die Kombinationen /* und */ ein Token.

Das ist in C(++) unglücklich, denn so wird ein Ausdruck *s/*t nicht wie erwartet geparst. Erst ein Leerzeichen zwischen dem Geteiltzeichen und dem Stern »hilft« dem Parser, die gewünschte Division zu erkennen.

Whitespace

Der Compiler muss die Tokens voneinander unterscheiden können. Daher fügen wir Trennzeichen ein; zu diesen zählt Weißraum (engl. whitespace) wie Leerzeichen, Tabulatoren, Zeilenvorschub- und Seitenvorschubzeichen. Außer als Trennzeichen haben diese Zeichen keine Bedeutung. Daher können sie in beliebiger Anzahl zwischen die Tokens gesetzt werden – beliebig viele Leerzeichen sind zwischen Tokens gültig. Und da wir damit nicht geizen müssen, können sie einen Programmabschnitt enorm verdeutlichen. Programme sind besser lesbar, wenn sie luftig formatiert sind.

Separatoren

Neben den Trennern gibt es noch zwölf aus ASCII-Zeichen geformte Tokens, die als Separatoren definiert werden:

( ) { } [ ] ; , . ... @ ::

Folgendes ist jedoch alles andere als gut zu lesen, obwohl der Compiler es akzeptiert:

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.2    Textkodierung durch Unicode-Zeichen 

Java kodiert Texte durch Unicode-Zeichen. Jedem Zeichen ist ein eindeutiger Zahlenwert (engl. code point) zugewiesen, sodass zum Beispiel das große A an Position 65 liegt. Der Unicode-Zeichensatz beinhaltet die ISO-US-ASCII-Zeichen von 0 bis 127 (hexadezimal 0x00 bis 0x7f, also 7 Bit) und die erweiterte Kodierung nach ISO 8859-1 (Latin-1), die Zeichen von 128 bis 255 hinzunimmt. Mehr Details zu Unicode liefert Kapitel 4, »Arrays und ihre Anwendungen«.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.3    Bezeichner 

Für Variablen (und damit Konstanten), Methoden, Klassen und Schnittstellen werden Bezeichner vergeben – auch Identifizierer (von engl. identifier) genannt –, die die entsprechenden Bausteine anschließend im Programm identifizieren. Unter Variablen sind dann Daten verfügbar. Methoden sind die Unterprogramme in objektorientierten Programmiersprachen, und Klassen sind die Bausteine objektorientierter Programme.

Ein Bezeichner ist eine Folge von Zeichen, die fast beliebig lang sein kann (die Länge ist nur theoretisch festgelegt). Die Zeichen sind Elemente aus dem Unicode-Zeichensatz, und jedes Zeichen ist für die Identifikation wichtig. Das heißt, ein Bezeichner, der 100 Zeichen lang ist, muss auch immer mit allen 100 Zeichen korrekt angegeben werden. Manche C- und FORTRAN-Compiler sind in dieser Hinsicht etwas großzügiger und bewerten nur die ersten Stellen.

Im folgenden Java-Programm sind die Bezeichner fett gesetzt:

class Application {

public static void main( String[] args ) {

System.out.println( "Hallo Welt" );

}

}

Dass String fett ist, hat seinen Grund, denn String ist eine Klasse und kein eingebauter Datentyp wie int. Zwar wird die Klasse String in Java bevorzugt behandelt – und ein Plus kann Strings zusammenhängen –, aber es ist immer noch ein Klassentyp.

Aufbau der Bezeichner

Jeder Java-Bezeichner ist eine Folge aus Java-Buchstaben und Java-Ziffern, wobei der Bezeichner mit einem Java-Buchstaben beginnen muss. Ein Java-Buchstabe umfasst nicht nur unsere lateinischen Buchstaben aus dem Bereich »A« bis »Z« (auch »a« bis »z«), sondern auch viele weitere Zeichen aus dem Unicode-Alphabet, etwa den Unterstrich, Währungszeichen – wie die Zeichen für Dollar ($), Euro (€), Yen (¥) – oder griechische oder arabische Buchstaben. Auch wenn damit viele wilde Zeichen als Bezeichnerbuchstaben grundsätzlich möglich sind, sollte doch die Programmierung mit englischen Bezeichnernamen erfolgen. Es ist noch einmal zu betonen, dass Java streng zwischen Groß- und Kleinschreibung unterscheidet.

Tabelle 2.1 listet einige gültige Bezeichner auf.

Gültige Bezeichner

Grund

Mami

Mami besteht nur aus Alphazeichen und ist daher korrekt.

__RAPHAEL_IST_LIEB__

Unterstriche sind erlaubt.

Bóolêáñ

Ist korrekt, auch wenn es Akzente enthält.

α

Das griechische Alpha ist ein gültiger Java-Buchstabe.

REZE$$SION

Das Dollar-Zeichen ist ein gültiger Java-Buchstabe.

¥€$

tatsächlich auch gültige Java-Buchstaben

Tabelle 2.1    Beispiele für gültige Bezeichner in Java

Ungültige Bezeichner dagegen sind:

Ungültige Bezeichner

Grund

2und2macht4

Das erste Symbol muss ein Java-Buchstabe sein und keine Ziffer.

hose gewaschen

Leerzeichen sind in Bezeichnern nicht erlaubt.

faster!

Das Ausrufezeichen ist, wie viele Sonderzeichen, ungültig.

null

class

Der Name ist schon von Java belegt. Null – Groß-/Kleinschreibung ist relevant – oder cláss wären möglich.

_

Ein einzelner Unterstrich gilt ab Java 9 als reserviertes Schlüsselwort.

Tabelle 2.2    Beispiele für ungültige Bezeichner in Java

In Java-Programmen bilden sich Bezeichnernamen oft aus zusammengesetzten Wörtern einer Beschreibung. Dies bedeutet, dass in einem Satz wie »open file read only« die Leerzeichen entfernt werden und die nach dem ersten Wort folgenden Wörter mit Großbuchstaben beginnen. Damit wird aus dem Beispielsatz anschließend »openFileReadOnly«. Sprachwissenschaftler nennen einen Großbuchstaben inmitten von Wörtern Binnenmajuskel. Programmierer und IT-affine Personen hingegen sprechen gern von der CamelCase-Schreibweise, wegen der zweihöckrigen Kamele. Schwierig wird die gemischte Groß-/Kleinschreibung bei großgeschriebenen Abkürzungen, wie HTTP, URL; hier sind die Java-Bibliotheken nicht einheitlich, Klassennamen wie HttpConnection oder HTTPConnection sind akzeptabel.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.4    Literale 

Ein Literal ist ein konstanter Ausdruck. Es gibt verschiedene Typen von Literalen:

  • die Wahrheitswerte true und false

  • integrale Literale für Zahlen, etwa 122

  • Fließkommaliterale, etwa 12.567 oder 9.999E-2

  • Zeichenliterale, etwa 'X' oder '\n'

  • String-Literale für Zeichenketten, wie "Paolo Pinkas"

  • null steht für einen besonderen Referenztyp.

Im folgenden Java-Programm sind die drei Literale fett gesetzt:

class Application {

public static void main( String[] args ) {

System.out.println( "Hallo Welt" );

System.out.println( 1 + 2.65 );

}

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.5    (Reservierte) Schlüsselwörter 

Bestimmte Wörter sind als Bezeichner nicht zulässig, da sie als Schlüsselwörter vom Compiler besonders behandelt werden. Schlüsselwörter bestimmen die »Sprache« eines Compilers, und es können vom Programmierer keine eigenen Schlüsselwörter hinzugefügt werden.

Schlüsselwörter sind im Folgenden fett gesetzt:

class Application {

public static void main( String[] args ) {

System.out.println( "Hallo Welt" );

}

}

Schlüsselwörter und Literale in Java

Die Zeichenfolgen in Tabelle 2.3 sind Schlüsselwörter (bzw. Literale im Fall von true, false und null) und sind in Java daher nicht als Bezeichnernamen möglich:

abstract

continue

for

new

switch

assert

default

goto†

package

synchronized

boolean

do

if

private

this

break

double

implements

protected

throw

byte

else

import

public

throws

case

enum

instanceof

return

transient

catch

extends

int

short

try

char

final

interface

static

void

class

finally

long

strictfp

volatile

const†

float

native

super

while

Tabelle 2.3    (Reservierte) Schlüsselwörter in Java

Obwohl die mit † gekennzeichneten Wörter zurzeit nicht von Java benutzt werden, können doch keine Variablen dieses Namens deklariert werden. Diese Schlüsselwörter nennen wir reservierte Schlüsselwörter, weil sie für eine zukünftige Nutzung reserviert sind. Allerdings ist nicht abzusehen, dass goto jemals verwendet werden wird.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.6    Zusammenfassung der lexikalischen Analyse 

Übersetzt der Compiler Java-Programme, so beginnt er mit der lexikalischen Untersuchung des Quellcodes. Wir haben die zentralen Elemente schon kennengelernt – sie sollen hier noch einmal zusammengefasst werden. Nehmen wir dazu das folgende einfache Programm:

class Application {

public static void main( String[] args ) {

String text = "Hallo Welt";

System.out.println( text );

System.out.println( 1 + 2.65 );

}

}

Der Compiler überliest alle Kommentare, und die Trennzeichen bringen den Compiler von Token zu Token. Folgende Tokens lassen sich im Programm ausmachen:

Token-Typ

Beispiel

Erklärung

Bezeichner

Application, main, String, args, text, System, out, println

Namen für Klasse, Variable, Methode …

Schlüsselwort

class, public, static, void

reservierte Wörter

Literal

"Hallo Welt", 1, 2.65

konstante Werte, wie Strings, Zahlen …

Operator

=, +

Operator für Zuweisungen, Berechnungen …

Separator

(, ), {, }, ;

Symbole, die neben dem Trennzeichen die Tokens trennen

Tabelle 2.4    Tokens des Beispielprogramms

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.1.7    Kommentare 

Programmieren heißt nicht nur, einen korrekten Algorithmus in einer Sprache auszudrücken, sondern auch, unsere Gedanken verständlich zu formulieren. Dies geschieht beispielsweise durch eine sinnvolle Namensgebung für Programmelemente wie Klassen, Methoden und Variablen. Ein selbsterklärender Klassenname hilft den Entwicklern erheblich. Doch die Lösungsidee und der Algorithmus werden auch durch die schönsten Variablennamen nicht zwingend klarer. Damit Außenstehende (und nach Monaten wir selbst) unsere Lösungsidee schnell nachvollziehen und später das Programm erweitern oder abändern können, werden Kommentare in den Quelltext geschrieben. Sie dienen nur den Lesern der Programme, haben aber auf die Abarbeitung keine Auswirkungen.

Unterschiedliche Kommentartypen

In Java gibt es zum Formulieren von Kommentaren drei Möglichkeiten:

  • Zeilenkommentare: Sie beginnen mit zwei Schrägstrichen // und kommentieren den Rest einer Zeile aus. Der Kommentar gilt von diesen Zeichen an bis zum Ende der Zeile, also bis zum Zeilenumbruchzeichen.

  • Blockkommentare: Sie kommentieren in /* */ Abschnitte aus. Der Text im Blockkommentar darf selbst kein */ enthalten, denn Blockkommentare dürfen nicht verschachtelt sein.

  • Javadoc-Kommentare: Das sind besondere Blockkommentare, die Javadoc-Kommentare mit /** */ enthalten. Ein Javadoc-Kommentar beschreibt etwa die Methode oder die Parameter, aus denen sich später die API-Dokumentation generieren lässt.

Schauen wir uns ein Beispiel an, in dem alle drei Kommentartypen vorkommen:

/*

* Der Quellcode ist public domain.

*/

// Magic. Do not touch.

/**

* @author Christian Ullenboom

*/

class DoYouHaveAnyCommentsToMake { // TODO: Umbenennen

// When I wrote this, only God and I understood what I was doing

// Now, God only knows

public static void main( String[] args /* Kommandozeilenargument */ ) {

System.out.println( "Ich habe /*richtig*/ viel //Hunger//" );

}

}

Für den Compiler ist ein Kommentar ein Token, weshalb 1/*2*/3 nicht das Token 13 gibt, sondern die beiden Tokens 1 und 3. Doch vereinfacht gesprochen sieht für den Compiler eine Datei mit Kommentaren genauso aus wie ohne, also wie class DoYouHaveAnyCommentsToMake { public static void main( String[] args ) { System.out.println( "Ich habe /*richtig*/ viel //Hunger//" );} }. Die Ausgabe zeigt, dass es innerhalb von String-Literalen keine Kommentare geben kann; die Symbole /*, */ und // gehören zum String.

Im Bytecode steht exakt das Gleiche – alle Kommentare werden vom Compiler verworfen, kein Kommentar kommt in den Bytecode.

Kommentare mit Stil

Alle Kommentare und Bemerkungen sollten in Englisch verfasst werden, um Projektmitgliedern aus anderen Ländern das Lesen zu erleichtern.

Die Javadoc-Kommentare dokumentieren im Allgemeinen das »Was« und die Blockkommentare das »Wie«.

Für allgemeine Kommentare sollten wir die Zeichen // benutzen. Sie haben zwei Vorteile:

  • Bei Editoren, die Kommentare nicht farbig hervorheben, oder bei einer einfachen Quellcodeausgabe auf der Kommandozeile lässt sich ersehen, dass eine Zeile, die mit // beginnt, ein Kommentar ist. Den Überblick über einen Quelltext zu behalten, der für mehrere Seiten mit den Kommentarzeichen /* und */ unterbrochen wird, ist schwierig. Zeilenkommentare machen deutlich, wo Kommentare beginnen und wo sie enden.

  • Der Einsatz der Zeilenkommentare eignet sich besser dazu, während der Entwicklungs- und Debug-Phase Codeblöcke auszukommentieren. Benutzen wir zur Programmdokumentation die Blockkommentare, so sind wir eingeschränkt, denn Kommentare dieser Form können wir nicht verschachteln. Zeilenkommentare können einfacher geschachtelt werden.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Die Tastenkombination (Strg)+(7) – oder (Strg)+(/), was das Kommentarzeichen / noch deutlicher macht – kommentiert eine Zeile aus. Eclipse setzt dann vor die Zeile die Kommentarzeichen //. Sind mehrere Zeilen selektiert, kommentiert die Tastenkombination alle markierten Zeilen mit Zeilenkommentaren aus. In einer kommentierten Zeile nimmt ein erneutes (Strg)+(7) die Kommentare einer Zeile wieder zurück.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  (Strg)+(/) kommentiert eine Zeile bzw. einen Block in IntelliJ ein und aus. Achtung, der (/) muss über den Ziffernblock der Tastatur ausgewählt werden!

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 5

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Programme sind Ablauffolgen, die im Kern aus Anweisungen bestehen. Sie werden zu größeren Bausteinen zusammengesetzt, den Methoden, die wiederum Klassen bilden. Klassen selbst werden in Paketen gesammelt, und eine Sammlung von Paketen wird als Java-Archiv ausgeliefert.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.1    Was sind Anweisungen? 

Java zählt zu den imperativen Programmiersprachen, in denen der Programmierer die Abarbeitungsschritte seiner Algorithmen durch Anweisungen (engl. statements) vorgibt. Anweisungen können unter anderem sein:

  • Ausdrucksanweisungen, etwa für Zuweisungen oder Methodenaufrufe

  • Fallunterscheidungen, zum Beispiel mit if

  • Schleifen für Wiederholungen, etwa mit for oder do-while

Die imperative Befehlsform ist für Programmiersprachen gar nicht selbstverständlich, und es gibt andere Programmierparadigmen. Eine entgegengesetzte Philosophie verfolgen deklarative Programmiersprachen, bei denen die Logik im Vordergrund steht und kein Programmablauf formuliert wird. Bekannte Vertreter der deklarativen Programmierung sind SQL, reguläre Ausdrücke und allgemein funktionale sowie logische Programmiersprachen. Ein Vertreter der letzten Gattung ist die Sprache Prolog, bei der das System zu einer Problembeschreibung selbstständig eine Lösung findet. Die Herausforderung besteht darin, die Aufgabe so präzise zu beschreiben, dass das System eine Lösung finden kann. Bei der Datenbanksprache SQL müssen wir beschreiben, wie unser Ergebnis aussehen soll. Dann kann das Datenbankmanagement-System anfangen zu arbeiten, doch die internen Abläufe kontrollieren wir nicht.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.2    Klassendeklaration 

Programme setzen sich aus Anweisungen zusammen. In Java können jedoch nicht einfach Anweisungen in eine Datei geschrieben und dem Compiler übergeben werden. Sie müssen zunächst in einen Rahmen gepackt werden. Dieser Rahmen heißt Compilationseinheit (engl. compilation unit) und deklariert eine Klasse mit ihren Methoden und Variablen.

Die nächsten Programmcodezeilen werden am Anfang etwas befremdlich wirken (wir erklären die Elemente später genauer). Die folgende Datei erhält den (frei wählbaren) Namen Application.java:

Listing 2.1    src/main/java/Application.java

public class Application {

public static void main( String[] args ) {

// Hier ist der Anfang unserer Programme

// Jetzt ist hier Platz für unsere eigenen Anweisungen

// Hier enden unsere Programme

}

}

Hinter den beiden Schrägstrichen // befindet sich ein Zeilenkommentar. Er gilt bis zum Ende der Zeile und dient dazu, Erläuterungen zu den Quellcodezeilen hinzuzufügen, die den Code verständlicher machen.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Eclipse zeigt Schlüsselwörter, Literale und Kommentare farbig an. Diese Farbgebung lässt sich unter WindowPreferences ändern.

Java ist eine objektorientierte Programmiersprache, die Programmanweisungen außerhalb von Klassen nicht erlaubt. Aus diesem Grund deklariert die Datei Application.java mit dem Schlüsselwort class eine Klasse Application, um später eine Methode mit der Programmlogik anzugeben. Der Klassenname ist ein Bezeichner und darf grundsätzlich beliebig sein, doch besteht die Einschränkung, dass in einer mit public deklarierten Klasse der Klassenname so lauten muss wie der Dateiname.

Alle Schlüsselwörter in Java beginnen mit Kleinbuchstaben, und Klassennamen beginnen üblicherweise mit Großbuchstaben. Methoden (wie main) sind kleingeschrieben, anders als in C#, wo sie großgeschrieben werden.

In den geschweiften Klammern der Klasse folgen Deklarationen von Methoden, also Unterprogrammen, die eine Klasse anbietet. Eine Methode ist eine Sammlung von Anweisungen unter einem Namen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.3    Die Reise beginnt am main(String[]) 

Die Methode public static void main(String[] args) ist für die Laufzeitumgebung etwas ganz Besonderes, denn beim Aufruf des Java-Interpreters mit einem Klassennamen wird unsere Methode als Erstes ausgeführt. Demnach werden genau die Anweisungen ausgeführt, die innerhalb der geschweiften Klammern stehen. Halten wir uns fälschlicherweise nicht an die Syntax für den Startpunkt, so kann der Interpreter die Ausführung nicht beginnen, und wir haben einen semantischen Fehler produziert, obwohl die Methode selbst korrekt gebildet ist. Innerhalb von main(String[]) befindet sich ein Parameter mit dem Namen args. Der Name ist willkürlich gewählt, wir werden allerdings immer args verwenden. Die Schlüsselwörter public und static müssen wir einhalten.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Dass Fehler unterkringelt werden, hat sich als Visualisierung durchgesetzt. Eclipse gibt im Falle eines Fehlers sehr viele Hinweise. Ein Fehler im Quellcode wird von Eclipse mit einer gekringelten roten Linie angezeigt. Als weiterer Indikator wird (unter Umständen erst beim Speichern) ein kleines Kreuz auf einem Kreis an der Fehlerzeile angezeigt. Gleichzeitig findet sich im Schieberegler ein kleiner roter Block. Im Package Explorer findet sich ebenfalls ein Hinweis auf Fehler. Den nächsten Fehler springt man mit der Tastenkombination (Strg)+(.) (Punkt) an.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  (F2) springt bei IntelliJ zum Fehler, (ª)+(F2) geht rückwärts zum vorangehenden Fehler.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.4    Der erste Methodenaufruf: println(…) 

In Java gibt es eine große Klassenbibliothek, die es Entwicklern erlaubt, Dateien anzulegen, Fenster zu öffnen, auf Datenbanken zuzugreifen, Webservices aufzurufen und vieles mehr. Am untersten Ende der Klassenbibliothek stehen Methoden, die eine gewünschte Operation ausführen.

Eine einfache Methode ist println(…). Sie gibt Meldungen auf dem Bildschirm (der Konsole) aus. Innerhalb der Klammern von println(…) können wir Argumente angeben.

Die println(…)-Methode erlaubt zum Beispiel Zeichenketten (ein anderes Wort ist Strings) als Argumente, die dann auf der Konsole erscheinen. Ein String ist eine Folge von Buchstaben, Ziffern oder Sonderzeichen in doppelten Anführungszeichen. Implementieren wir damit eine vollständige Java-Klasse mit einem Methodenaufruf, die über println(…) etwas auf dem Bildschirm ausgibt:

Listing 2.2    src/main/java/Application2.java

class Application2 {

public static void main( String[] args ) {

// Start des Programms

System.out.println( "Hallo Javanesen" );

// Ende des Programms

}

}

Anders als viele Skriptsprachen hat Java keine eingebauten Methoden, die einfach so »da« sind. Jede Methode »gehört« immer zu einem Typ. In unserem Fall gehört println(…) zu out. Auch out gehört jemandem, und zwar der Klasse System. Erst durch die vollständige Schreibweise ist dem Java-Compiler und der Laufzeitumgebung klar, wer die Ausgabe übernimmt.

Ein String kann Escape-Sequenzen enthalten, um besondere Zeichen darzustellen:

System.out.println( "'Hallo' \"Welt\"" );

Die Escape-Sequenz \" setzt in einem String ein doppeltes Anführungszeichen. Einfache Anführungszeichen müssen in einem String nicht ausmaskiert werden. Die Anweisung führt zur Bildschirmausgabe 'Hallo' "Welt".

Ein Zeilenumbruch wird mit \n eingefügt:

System.out.println( "Hallo\nWelt" );

Die Ausgabe ist:

Der Begriff Methode ist die korrekte Bezeichnung für ein Unterprogramm in Java – die Java Language Specification (JLS) verwendet den Begriff Funktion nicht.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.5    Atomare Anweisungen und Anweisungssequenzen 

Methodenaufrufe wie

  • System.out.println(),’

  • die leere Anweisung, die nur aus einem Semikolon besteht,

  • oder auch Variablendeklarationen (die später vorgestellt werden)

nennen sich atomare (auch elementare) Anweisungen. Diese unteilbaren Anweisungen werden zu Anweisungssequenzen zusammengesetzt, die Programme bilden.

Eine Anweisungssequenz:

System.out.println( "Wer morgens total zerknittert aufsteht, " );

System.out.println( "hat am Tag die besten Entfaltungsmöglichkeiten." );

;

System.out.println();

;

Leere Anweisungen (also die Zeilen mit dem Semikolon) gibt es im Allgemeinen nur bei Endloswiederholungen.

Die Laufzeitumgebung von Java führt jede einzelne Anweisung der Sequenz in der angegebenen Reihenfolge hintereinander aus. Anweisungen und Anweisungssequenzen dürfen nicht irgendwo stehen, sondern nur an bestimmen Stellen, etwa innerhalb eines Methodenkörpers.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.6    Mehr zu print(…), println(…) und printf(…) für Bildschirmausgaben 

Die meisten Methoden verraten durch ihren Namen, was sie leisten, und für eigene Programme ist es sinnvoll, aussagekräftige Namen zu verwenden. Wenn die Java-Entwickler die Ausgabemethode statt println() einfach glubschi() genannt hätten, bliebe uns der Sinn der Methode verborgen. println() zeigt jedoch durch den Wortstamm »print« an, dass etwas »gedruckt« wird, also auf den Bildschirm geschrieben wird. Die Endung ln (kurz für line) bedeutet, dass noch ein Zeilenvorschubzeichen ausgegeben wird. Umgangssprachlich heißt das: Eine neue Ausgabe beginnt in der nächsten Zeile. Neben println(…) existiert die Bibliotheksmethode print(…), die keinen Zeilenvorschub anhängt.

Die printXXX(…)-Methoden können in Klammern unterschiedliche Argumente bekommen. Ein Argument ist ein Wert, den wir der Methode beim Aufruf mitgeben. Auch wenn wir einer Methode keine Argumente übergeben, muss beim Aufruf hinter dem Methodennamen ein Klammernpaar folgen. Dies ist konsequent, da wir so wissen, dass es sich um einen Methodenaufruf handelt und um nichts anderes. Andernfalls führt es zu Verwechslungen mit Variablen.

Überladene Methoden

Java erlaubt Methoden, die gleich heißen, denen aber unterschiedliche Dinge übergeben werden können; diese Methoden nennen wir überladen. Die printXXX(…)-Methoden sind zum Beispiel überladen und akzeptieren neben dem Argumenttyp String auch Typen wie einzelne Zeichen, Wahrheitswerte oder Zahlen – oder auch gar nichts:

Listing 2.3    src/main/java/OverloadedPrintln.java

public class OverloadedPrintln {

public static void main( String[] args ) {

System.out.println( "Verhaften Sie die üblichen Verdächtigen!" );

System.out.println( true );

System.out.println( -273 );

System.out.println(); // Gibt eine Leerzeile aus

System.out.println( 1.6180339887498948 );

}

}

Die Ausgabe ist:

Verhaften Sie die üblichen Verdächtigen!true–273

1.618033988749895

In der letzten Zeile ist gut zu sehen, dass es Probleme mit der Genauigkeit gibt – dieses Phänomen werden wir uns noch genauer in Kapitel 22 anschauen.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Ist in Eclipse eine andere Ansicht aktiviert, etwa weil wir auf das Konsolenfenster geklickt haben, bringt uns die Taste (F12) wieder in den Editor zurück.

Variable Argumentlisten

Java unterstützt variable Argumentlisten, was bedeutet, dass es möglich ist, bestimmten Methoden beliebig viele Argumente (oder auch kein Argument) zu übergeben. Die Methode printf(…) erlaubt zum Beispiel variable Argumentlisten, um gemäß einer Formatierungsanweisung – einem String, der immer als erstes Argument übergeben werden muss – die nachfolgenden Methodenargumente aufzubereiten und auszugeben:

Listing 2.4    src/main/java/VarArgs.java

public class VarArgs {

public static void main( String[] args ) {

System.out.printf( "Was sagst du?%n" );

System.out.printf( "%d Kanäle und überall nur %s.%n", 220, "Katzen" );

}

}

Die Ausgabe der Anweisung ist:

Was sagst du?

220 Kanäle und überall nur Katzen.

Die Formatierungsanweisung %n setzt einen Zeilenumbruch, %d ist ein Platzhalter für eine ganze Zahl (%f wäre der Platzhalter für eine Fließkommazahl) und %s ist ein Platzhalter für eine Zeichenkette oder etwas, was in einen String konvertiert werden soll. Weitere Platzhalter werden in Abschnitt 5.5, »Die Klasse String und ihre Methoden«, vorgestellt.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.7    Die API-Dokumentation 

Die wichtigste Informationsquelle für Programmierer ist die offizielle API-Dokumentation von Oracle. Zu der Methode println() können wir bei der Klasse PrintStream zum Beispiel erfahren, dass diese eine Ganzzahl, eine Fließkommazahl, einen Wahrheitswert, ein Zeichen oder aber eine Zeichenkette akzeptiert. Die Dokumentation ist weder Teil des JRE noch des JDK – dafür ist die Hilfe zu groß. Stattdessen kann man die Dokumentation online unter http://tutego.de/go/javaapi lesen oder sie von der Oracle-Seite http://www.oracle.com/technetwork/java/javase/downloads herunterladen und als Sammlung von HTML-Dokumenten auspacken (siehe Abbildung 2.1).

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.1    Online-Dokumentation bei Oracle

Aus Entwicklungsumgebungen ist die API-Dokumentation auch zugänglich, sodass eine Suche auf der Webseite nicht nötig ist.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Eclipse zeigt mithilfe der Tasten (ª)+(F2) in einem eingebetteten Browser-Fenster die API-Dokumentation an, wobei die Javadoc von den Oracle-Seiten kommt. Mithilfe der (F2)-Taste bekommen wir ein kleines gelbes Vorschaufenster, das ebenfalls die API-Dokumentation zeigt (siehe Abbildung 2.2).

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.2    API-Dokumentation in Eclipse

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  IntelliJ zeigt mithilfe der Tasten (Strg)+(Q) die API-Dokumentation in einem kleinen Fenster an.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.8    Ausdrücke 

Ein Ausdruck (engl. expression) ergibt bei der Auswertung ein Ergebnis. Im Beispiel OverloadedPrintln.java (Listing 2.3) steht in der main(…)-Methode:

System.out.println( "Verhaften Sie die üblichen Verdächtigen!" );

System.out.println( true );

System.out.println( -273 );

System.out.println( 1.6180339887498948 );

Die Argumente für println(…), wie der String, der Wahrheitswert oder die Zahlen, sind Ausdrücke. In dem Beispiel kommt der Ausdruck von einem Literal, aber mit Operatoren lassen sich auch komplexere Ausdrücke wie (1 + 2) * 1.19 bilden:

System.out.println( (1 + 2) * 1.19 );

Der Wert eines Ausdrucks wird auch Resultat genannt. Ausdrücke haben immer einen Wert, während das für Anweisungen (wie eine Schleife) nicht gilt. Daher kann ein Ausdruck an allen Stellen stehen, an denen ein Wert benötigt wird, etwa als Argument von println(…). Dieser Wert ist entweder ein numerischer Wert (von arithmetischen Ausdrücken), ein Wahrheitswert (boolean) oder eine Referenz (etwa von einer Objekterzeugung).

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.9    Ausdrucksanweisung 

In einem Programm reiht sich Anweisung an Anweisung. Auch bestimmte Ausdrücke und Methodenaufrufe lassen sich als Anweisungen einsetzen, wenn sie mit einem Semikolon abgeschlossen sind; wir sprechen dann von einer Ausdrucksanweisung (engl. expression statement). Jeder Methodenaufruf mit Semikolon bildet zum Beispiel eine Ausdrucksanweisung. Dabei ist es egal, ob die Methode selbst eine Rückgabe liefert oder nicht.

System.out.println(); // println() besitzt keine Rückgabe (void)

Math.random(); // random() liefert eine Fließkommazahl

Die Methode Math.random() liefert als Ergebnis einen Zufallswert zwischen 0 (inklusiv, kann also 0 werden) und 1 (exklusiv, erreicht 1 also nie wirklich). Da mit dem Ergebnis des Ausdrucks nichts gemacht wird, wird der Rückgabewert verworfen. Im Fall der Zufallsmethode ist das nicht sinnvoll, denn sie macht außer der Berechnung nichts anderes.

Neben Methodenaufrufen mit abschließendem Semikolon gibt es andere Formen von Ausdrucksanweisungen, wie etwa Zuweisungen. Doch allen ist das Semikolon gemeinsam.

Nicht jeder Ausdruck kann eine Ausdrucksanweisung sein. 1+2 ist etwa ein Ausdruck, aber 1+2; (der Ausdruck wurde mit einem Semikolon abgeschlossen) ist keine gültige Anweisung. In JavaScript ist so etwas erlaubt, in Java nicht.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.10    Erster Einblick in die Objektorientierung 

In einer objektorientierten Programmiersprache sind alle Methoden an bestimmte Objekte gebunden (daher der Begriff objektorientiert). Betrachten wir zum Beispiel das Objekt Radio: Ein Radio spielt Musik ab, wenn der Einschalter betätigt wird und ein Sender und die Lautstärke eingestellt sind. Ein Radio bietet also bestimmte Dienste (Operationen) an, wie Musik an/aus, lauter/leiser. Zusätzlich hat ein Objekt einen Zustand, zum Beispiel die Lautstärke oder das Baujahr.

Wichtig ist in objektorientierten Sprachen, dass die Operationen und Zustände immer (und da gibt es keine Ausnahmen) an Objekte bzw. Klassen gebunden sind (mehr zu dieser Unterscheidung folgt in Kapitel 3, »Klassen und Objekte«, und in Kapitel 6, »Eigene Klassen schreiben«). Der Aufruf einer Methode auf einem Objekt richtet die Anfrage genau an dieses bestimmte Objekt. Steht in einem Java-Programm nur die Anweisung lauter, so weiß der Compiler nicht, wen er fragen soll, wenn es etwa drei Radio-Objekte gibt. Was ist, wenn es auch einen Fernseher mit der gleichen Operation gibt? Aus diesem Grund verbinden wir das Objekt, das etwas kann, mit der Operation. Ein Punkt trennt das Objekt von der Operation oder dem Zustand. So gehört println(…) zu einem Objekt out, das die Bildschirmausgabe übernimmt. Dieses Objekt out wiederum gehört zu der Klasse System.

System.out und System.err

Das Laufzeitsystem bietet uns zwei Ausgabekanäle: einen für normale Ausgaben und einen, in den wir Fehler leiten können. Der Vorteil ist, dass über diese Unterteilung die Fehler von der herkömmlichen Ausgabe getrennt werden können. Standardausgaben wandern in System.out, und Fehlerausgaben werden in System.err weitergeleitet. out und err sind vom gleichen Typ, sodass die printXXX(…)-Methoden bei beiden gleich sind:

Listing 2.5    src/main/java/SystemOutErr.java, main()

System.out.println( "Das ist eine normale Ausgabe" );

System.err.println( "Das ist eine Fehlerausgabe" );

Die Objektorientierung wird hierbei noch einmal besonders deutlich. Das out- und das err-Objekt sind zwei Objekte, die das Gleiche können, nämlich mit printXXX(…) etwas ausgeben. Doch ist es nicht möglich, ohne explizite Objektangabe die Methode println() in den Raum zu rufen und von der Laufzeitumgebung zu erwarten, dass diese weiß, ob die Anfrage an System.out oder an System.err geht.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.3    Eclipse stellt normale Ausgaben schwarz und Fehlerausgaben rot dar. Damit ist leicht zu erkennen, welche Ausgabe in welchen Kanal geschickt wurde. Das »System.err«-Objekt sollte nicht verwendet werden, damit es auf dem Bildschirm bunt wird!

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.11    Modifizierer 

Die Deklaration einer Klasse oder Methode kann einen oder mehrere Modifizierer (engl. modifier) enthalten, die zum Beispiel die Nutzung einschränken oder parallelen Zugriff synchronisieren.

Im folgenden Programm kommen zwei verschiedene Modifizierer an drei Stellen vor; sie sind fett gesetzt:

public class Application {

public static void main( String[] args ) {

System.out.println( "Hallo Welt" );

}

}

public ist ein Sichtbarkeitsmodifizierer. Er bestimmt, ob die Klasse bzw. die Methode für Programmcode anderer Klassen sichtbar ist oder nicht.

static erlaubt, dass die Methode aufgerufen werden kann, ohne ein Objekt der Klasse bilden zu müssen. Anders gesagt: ohne static muss ein Objekt gebildet und die Methode über das konkrete Objekt aufgerufen werden. Wir arbeiten in den ersten beiden Kapiteln nur mit statischen Methoden und werden ab Kapitel 3, »Klassen und Objekte«, nichtstatische Methoden einführen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.2.12    Gruppieren von Anweisungen mit Blöcken 

Ein Block fasst eine Gruppe von Anweisungen zusammen, die hintereinander ausgeführt werden. Ein Block ist somit eine Anweisung, die in geschweiften Klammern { } eine Folge von Anweisungen zu einer neuen Anweisung zusammenfasst. Anders gesagt: Die Folge von Anweisungen wird dadurch, dass sie in geschweifte Klammern gesetzt und damit zu einem Block zusammengefasst wird, zu einer neuen Anweisung:

{

Anweisung1;

Anweisung2;

...

}

Ein Block kann überall dort verwendet werden, wo auch eine einzelne Anweisung stehen kann. Der neue Block hat jedoch eine Besonderheit in Bezug auf Variablen, da er einen lokalen Bereich für die darin befindlichen Anweisungen inklusive der Variablen bildet.

Die Zeilen, die in geschweiften Klammern stehen, werden in der Regel mit Leerraum eingerückt. Üblicherweise sind es zwei (wie in diesem Buch) oder vier Leerzeichen. Viele Autoren setzen die geschweiften Klammern in eine eigene Zeile. Es gibt eine Fülle von weiteren Ratschlägen zum Code-Style, die das einfache Lesen und Verstehen für den Menschen in den Mittelpunkt rücken: Zeilen sollen nicht zu lang sein (80 bis 100 Zeichen sind eine gute Größe), tiefe Schachtelungen sind zu vermeiden, Sinneinheiten werden mit Leerzeilen geschaffen. Der »Google Java Style Guide« unter http://google.github.io/styleguide/javaguide.html gibt gute Vorschläge.

Leerer Block

Ein Block {} ohne Anweisung nennt sich leerer Block. Er verhält sich wie eine leere Anweisung, also wie ein Semikolon. In einigen Fällen ist der leere Block mit dem Semikolon wirklich austauschbar, in anderen Fällen erzwingt die Java-Sprache einen Block, der, falls es keine Anweisungen gibt, leer ist, anstatt hier auch ein Semikolon zu erlauben.

Geschachtelte Blöcke

Blöcke können beliebig geschachtelt werden. So ergeben sich innere Blöcke und äußere Blöcke:

{ // Beginn äußerer Block

{ // Beginn innerer Block

} // Ende innerer Block

} // Ende äußerer Block

Mit leeren Blöcken ist Folgendes in der statischen Methode main(…) in Ordnung:

public static void main( String[] args ) {

{ System.out.println( "Hallo Computer" ); {{}}{{}{}}}

}

Blöcke spielen eine wichtige Rolle beim Zusammenfassen von Anweisungen, die in Abhängigkeit von Bedingungen einmal oder mehrmals ausgeführt werden. In Abschnitt 2.5, »Bedingte Anweisungen oder Fallunterscheidungen«, und in Abschnitt 2.6, »Immer das Gleiche mit den Schleifen«, kommen wir darauf noch einmal praktisch zurück.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 6

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Java nutzt, wie es für imperative Programmiersprachen typisch ist, Variablen zum Ablegen von Daten. Eine Variable ist ein reservierter Speicherbereich und belegt – abhängig vom Inhalt – eine feste Anzahl von Bytes. Alle Variablen (und auch Ausdrücke) haben einen Typ, der zur Übersetzungszeit bekannt ist. Der Typ wird auch Datentyp genannt, da eine Variable einen Datenwert, auch Datum genannt, enthält. Beispiele für einfache Datentypen sind: Ganzzahlen, Fließkommazahlen, Wahrheitswerte und Zeichen. Der Typ bestimmt auch die zulässigen Operationen, denn Wahrheitswerte lassen sich nicht addieren, Ganzzahlen schon. Dagegen lassen sich Fließkommazahlen addieren, aber nicht XOR-verknüpfen. Da jede Variable einen vom Programmierer vorgegebenen festen Datentyp hat, der zur Übersetzungszeit bekannt ist und sich später nicht mehr ändern lässt, und da Java stark darauf achtet, welche Operationen erlaubt sind, und auch von jedem Ausdruck spätestens zur Laufzeit den Typ kennt, ist Java eine statisch typisierte und streng (stark) typisierte Programmiersprache.

In Java muss der Datentyp einer Variablen zur Übersetzungszeit bekannt sein. Das nennt sich dann statisch typisiert. Das Gegenteil ist eine dynamische Typisierung, wie sie etwa JavaScript verwendet. Hier kann sich der Typ einer Variablen zur Laufzeit ändern, je nachdem, was die Variable enthält.

Primitiv- oder Verweistyp

Die Datentypen in Java zerfallen in zwei Kategorien:

  • Primitive Typen: Die primitiven (einfachen) Typen sind fest in der Sprache Java eingebaute Datentypen für Zahlen, Unicode-Zeichen und Wahrheitswerte.

  • Referenztypen: Mit diesem Datentyp lassen sich Objektverweise etwa auf Zeichenketten, Datenstrukturen oder Zwergpinscher verwalten.

Für diese Teilung entschied sich Sun seinerzeit aus einem einfachen Grund: Java wurde als Programmiersprache entworfen, die kleine, schwache Geräte unterstützen sollte, und auf denen musste die Java-Software, die am Anfang noch interpretiert wurde, so schnell wie möglich laufen. Unterscheidet der Compiler zwischen primitiven Typen und Referenztypen, so kann er relativ leicht Bytecode erzeugen, der ebenfalls zwischen den beiden Typen unterscheidet. Damit kann die Laufzeitumgebung auch den Programmcode viel schneller ausführen, und das mit einem relativ einfachen Compiler. Das war in der Anfangszeit ein wichtiges Kriterium, ist heute aber nicht mehr wichtig, weil die Laufzeitumgebung diverse Optimierungen vornimmt.

In Smalltalk ist alles ein Objekt, auch die eingebauten Sprachdatentypen. Für Zahlen gibt es einen Basistyp Number sowie Integer, Float, Fraction als Untertypen. Immer noch gibt es arithmetische Operatoren (+, -, *, /, //, \\, um sie alle aufzuzählen), aber das sind nur Methoden der Klasse Number. Für Java-Entwickler sind Methodennamen wie + oder - ungewöhnlich, doch in Smalltalk sind sie das nicht. Syntaktisch unterscheidet sich ein 1 + 2 in Java und Smalltalk nicht, nur in Smalltalk ist die Addition ein Nachrichtenaufruf an das Integer-Objekt 1 und an die Methode + mit dem Argument 2, das wiederum ein Integer-Objekt ist – die Objekte baut der Compiler selbstständig aus den Literalen auf. Eine Klasse Integer für Ganzzahlen besitzt weitere Methoden wie asCharacter und floor. Es ist wichtig zu verstehen, dass dies nur das semantische Modell auf der Sprachseite ist; das hat nichts damit zu tun, wie später die Laufzeitumgebung diese speziellen Nachrichtenaufrufe optimiert. Durch die Einteilung von Java in primitive Datentypen und Referenztypen haben die Sprachschöpfer einen Bruch des objektorientierten Prinzips in Kauf genommen, um die interpretierte Laufzeit Anfang der 1990er zu optimieren – eine Optimierung, die aus heutiger Sicht unnötig war.

In .NET ist es eher wie in Java: Der Compiler kennt die eingebauten Datentypen und lässt ihnen eine Sonderbehandlung zukommen; es sind keine Methodenaufrufe. Auch im Bytecode (Common Intermediate Language, kurz CIL in .NET genannt) finden sich Anweisungen wie Addition und Subtraktion wieder. Doch es gibt noch einen Unterschied zu Java: Der Compiler bildet Datentypen der .NET-Sprachen auf .NET-Klassen ab, und diese Klassen haben Methoden. In C# ist der eingebaute Datentyp float mit dem Datentyp Single (aus dem .NET-Paket System) identisch, und es ist egal, ob Entwickler float f oder Single f schreiben. Doch Single (respektive float) hat im Vergleich zu Smalltalk keine mathematischen Operationen, aber dennoch ein paar wenige Methoden wie ToString(). In .NET verhalten sich folglich die eingebauten Datentypen wie Objekte: Sie haben Methoden, haben aber die gleiche Wertsemantik (zum Beispiel bei Methodenaufrufen) wie in Java und sehen auch im Bytecode ähnlich aus, was ihnen die gleiche gute Performance verleiht.

Wir werden uns im Folgenden erst mit primitiven Datentypen beschäftigen. Referenzen werden nur dann eingesetzt, wenn Objekte ins Spiel kommen. Diese nehmen wir uns in Kapitel 3, »Klassen und Objekte«, vor.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.1    Primitive Datentypen im Überblick 

In Java gibt es zwei Arten eingebauter primitiver Datentypen:

  • arithmetische Typen (ganze Zahlen – auch integrale Typen genannt –, Fließkommazahlen, Unicode-Zeichen)

  • Wahrheitswerte für die Zustände wahr und falsch

Tabelle 2.5 vermittelt dazu einen Überblick. Anschließend betrachten wir jeden Datentyp präziser.

Typ

Belegung (Wertebereich)

boolean

true oder false

char

16-Bit-Unicode-Zeichen (0x0000 … 0xFFFF)

byte

–27 bis 27 – 1 (–128 … 127)

short

–215 bis 215 – 1 (–32.768 … 32.767)

int

–231 bis 231 – 1 (–2.147.483.648 … 2.147.483.647)

long

–263 bis 263 – 1

(–9.223.372.036.854.775.808 … 9.223.372.036.854.775.807)

float

1,40239846E–45f … 3,40282347E+38f

double

4,94065645841246544E–324 … 1,79769131486231570E+308

Tabelle 2.5    Java-Datentypen und ihre Wertebereiche

Bei den Ganzzahlen fällt auf, dass es eine positive Zahl »weniger« gibt als negative. Das liegt an der Kodierung im Zweierkomplement.

Für float und double ist das Vorzeichen nicht angegeben, da die kleinsten und größten darstellbaren Zahlen sowohl positiv als auch negativ sein können. Mit anderen Worten: Die Wertebereiche unterscheiden sich nicht – anders als etwa bei int – in Abhängigkeit vom Vorzeichen.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Genau genommen sieht die Sprachgrammatik von Java keine negativen Zahlenliterale vor. Bei einer Zahl wie -1.2 oder -1 ist das Minus der unäre Operator und gehört nicht zur Zahl. Im Bytecode selbst sind die negativen Zahlen wieder abgebildet.

Tabelle 2.6 zeigt eine etwas andere Darstellung.

Typ

Größe

Format

Ganzzahlen

byte

8 Bit

Zweierkomplement

short

16 Bit

Zweierkomplement

int

32 Bit

Zweierkomplement

long

64 Bit

Zweierkomplement

Fließkommazahlen

float

32 Bit

IEEE 754

double

64 Bit

IEEE 754

Weitere Datentypen

boolean

1 Bit

true, false

char

16 Bit

16-Bit-Unicode

Tabelle 2.6    Java-Datentypen und ihre Größen und Formate

Strings werden bevorzugt behandelt, sind aber lediglich Verweise auf Objekte und kein primitiver Datentyp.

Zwei wesentliche Punkte zeichnen die primitiven Datentypen aus:

  • Alle Datentypen haben eine festgesetzte Länge, die sich unter keinen Umständen ändert. Der Nachteil, dass sich bei einigen Hochsprachen die Länge eines Datentyps ändern kann, besteht in Java nicht. In den Sprachen C(++) bleibt dies immer unsicher, und die Umstellung auf 64-Bit-Maschinen bringt viele Probleme mit sich. Der Datentyp char ist 16 Bit lang.

  • Die numerischen Datentypen byte, short, int und long sind vorzeichenbehaftet, Fließkommazahlen sowieso. Dies ist leider nicht immer praktisch, aber wir müssen stets daran denken. Probleme gibt es, wenn wir einem Byte zum Beispiel den Wert 240 zuweisen wollen, denn 240 liegt außerhalb des Wertebereichs, der von –128 bis 127 reicht. Ein char ist im Prinzip ein vorzeichenloser Ganzzahltyp.

Wenn wir die numerischen Datentypen (lassen wir hier char außen vor) nach ihrer Größe sortieren wollten, könnten wir zwei Linien für Ganzzahlen und Fließkommazahlen aufbauen:

byte < short < int < long

float < double

Die Klassen Byte, Integer, Long, Short, Character, Double und Float deklarieren die Konstanten MAX_VALUE und MIN_VALUE, die den größten und kleinsten zulässigen Wert des jeweiligen Wertebereichs bzw. die Grenzen der Wertebereiche der jeweiligen Datentypen angeben:

System.out.println( Byte.MIN_VALUE ); // -128

System.out.println( Byte.MAX_VALUE ); // 127

System.out.println( Character.MIN_VALUE ); // '\u0000'

System.out.println( Character.MAX_VALUE ); // '\uFFFF'

System.out.println( Double.MIN_VALUE ); // 4.9E-324

System.out.println( Double.MAX_VALUE ); // 1.7976931348623157E308

Es gibt für jeden primitiven Datentyp eine eigene Klasse mit Hilfsmethoden rund um diesen Datentyp. Mehr zu diesen besonderen Klassen folgt in Kapitel 10, »Besondere Typen der Java SE«.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.2    Variablendeklarationen 

Mit Variablen lassen sich Daten speichern, die vom Programm gelesen und geschrieben werden können. Um Variablen zu nutzen, müssen sie deklariert (definiert) werden. Die Schreibweise einer Variablendeklaration ist immer die gleiche: Hinter dem Typnamen folgt der Name der Variablen. Die Deklaration ist eine Anweisung und wird daher mit einem Semikolon abgeschlossen. In Java kennt der Compiler von jeder Variablen und jedem Ausdruck genau den Typ.

Deklarieren wir ein paar (lokale) Variablen in der main(…)-Methode:

Listing 2.6    src/main/java/FirstVariable.java

public class FirstVariable {

public static void main( String[] args ) {

String name; // Name

int age; // Alter

double income; // Einkommen

char gender; // Geschlecht ('f' oder 'm')

boolean isPresident; // Ist Präsident (true oder false)

boolean isVegetarian; // Ist die Person Vegetarier?

}

}

Links steht entweder ein primitiver Typ (wie int) oder ein Referenztyp (wie String). Viel schwieriger ist eine Deklaration nicht – kryptische Angaben wie in C gibt es in Java nicht. Ein Variablenname (der dann Bezeichner ist) kann alle Buchstaben und Ziffern des Unicode-Zeichensatzes beinhalten, mit der Ausnahme, dass am Anfang des Bezeichners keine Ziffer stehen darf. Auch darf der Bezeichnername mit keinem reservierten Schlüsselwort identisch sein.

Mehrere Variablen kompakt deklarieren

Im vorangehenden Listing sind zwei Variablen vom gleichen Typ: isPresident und isVegetarian.

boolean isPresident;

boolean isVegetarian;

Immer dann, wenn der Variablentyp der gleiche ist, lässt sich die Deklaration verkürzen – Variablen werden mit Komma getrennt:

boolean isPresident, isVegetarian;

Variablendeklaration mit Wertinitialisierung

Gleich bei der Deklaration lassen sich Variablen mit einem Anfangswert initialisieren. Hinter einem Gleichheitszeichen steht der Wert, der oft ein Literal ist. Ein Beispielprogramm:

Listing 2.7    src/main/java/Obama.java

public class Obama {

public static void main( String[] args ) {

String name = "Barack Hussein Obama II";

int age = 48;

double income = 400000;

char gender = 'm';

boolean isPresident = false;

}

}

Wir haben gesehen, dass bei der Deklaration mehrerer Variablen gleichen Typs ein Komma die Bezeichner trennt. Das überträgt sich auch auf die Initialisierung. Ein Beispiel:

boolean sendSms = true,

bungaBungaParty = true;

String person1 = "Silvio",

person2 = "Ruby the Heart Stealer";

double x, y,

bodyHeight = 165 /* cm */;

Die Zeilen deklarieren mehrere Variablen auf einen Schlag. x und y am Schluss bleiben uninitialisiert.

Zinsen berechnen als Beispiel zur Variablendeklaration, -initialisierung und -ausgabe

Zusammen mit der Konsolenausgabe können wir schon einen einfachen Zinsrechner programmieren. Er soll uns ausgeben, wie hoch die Zinsen für ein gegebenes Kapital bei einem gegebenen Zinssatz (engl. interest rate) nach einem Jahr sind.

Listing 2.8    src/main/java/InterestRates.java

public class InterestRates {

public static void main( String[] args ) {

double capital = 20000 /* Euro */;

double interestRate = 3.6 /* Prozent */;

double totalInterestRate = capital * interestRate / 100; // Jahr 1

System.out.print( "Zinsen: " );

System.out.println( totalInterestRate ); // 720.0

}

}

Strings können mit einem Plus aneinandergehängt werden. Ist ein Segment kein String, so wird es in einen String konvertiert und dann angehängt.

System.out.println( "Zinsen: " + totalInterestRate ); // Zinsen: 720.0

Mehr Beispiele dazu folgen in Abschnitt 2.4.11, »Überladenes Plus für Strings«.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.3    Automatisches Feststellen der Typen mit var 

Java 10 hat die Erweiterung gebracht, dass der Variablentyp bei gewissen Deklarationen entfallen kann und wir einfach stattdessen var nutzen können:

Listing 2.9    src/main/java/VarObama.java, Ausschnitt

var name = "Barack Hussein Obama II";

var age = 48;

var income = 400000;

var gender = 'm';

var isPresident = false;

Wir sehen, dass im Gegensatz zu unserem vorherigen Beispiel nicht mehr die Variablentypen wie String oder int bei der Variablendeklaration explizit im Code stehen, sondern nur noch var. Das heißt allerdings nicht, dass der Compiler die Typen offenlässt! Der Compiler braucht zwingend die rechte Seite neben dem Gleichheitszeichen, um den Typ feststellen zu können. Das nennt sich Local-Variable Type Inference. Daher gibt es in unserem Programm auch eine Unstimmigkeit, nämlich bei var income = 400000, die gut ein Problem mit var aufzeigt: Die Variable ist kein double mehr wie vorher, sondern 400000 ist ein Ganzzahl-Literal, weshalb der Java-Compiler der Variablen income den Typ int gibt.

Die Nutzung von var soll Entwicklern helfen, Code kürzer zu schreiben, insbesondere wenn der Variablenname schon eindeutig auf den Typ hinweist. Finden wir eine Variable text vor, ist der Typ String naheliegend, genauso wie age ein int ist oder ein Präfix wie is oder has auf eine boolean-Variable hinweist. Aber wenn var auf die Kosten der Verständlichkeit geht, darf die Abkürzung nicht eingesetzt werden. Auch der Java-Compiler gibt Schranken vor:

  • var ist nur dann möglich, wenn eine Initialisierung einen Typ vorgibt. Eine Deklaration der Art var age; ohne Initialisierung ist nicht möglich und führt zu einem Compilerfehler.

  • var kann nur bei lokalen Variablen eingesetzt werden, wo der Bereich überschaubar ist. Es gibt aber noch viele weitere Stellen, wo in Java Variablen deklariert werden – dort ist var nicht möglich.

Java ist mit var relativ spät dran. Andere statisch typisierte Sprachen bieten die Möglichkeit schon länger, etwa C++ mit auto oder C# auch mit var. Auch JavaScript nutzt var, allerdings in einem völlig anderen Kontext: In JavaScript sind Variablen erst zur Laufzeit typisiert, und alle Operationen werden erst zur Ausführungszeit geprüft, während Java die Typsicherheit mit var nicht aufgibt.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.4    Finale Variablen und der Modifizierer final 

Variablen können mit dem Modifizierer final deklariert werden, sodass genau eine Zuweisung möglich ist. Dieses zusätzliche Schlüsselwort verbietet folglich eine weitere Zuweisung an diese Variable, sodass sie nicht mehr verändert werden kann. Ein üblicher Anwendungsfall sind Konstanten:

int width = 40, height = 12;

final int area = width * height;

final int perimeter;

final var random = Math.random() * 100;

perimeter = width * 2 + height * 2;

area = 200; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

perimeter = 100; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

Im Fall einer versuchten zweiten Zuweisung meldet der Compiler von Eclipse: »The final local variable … cannot be assigned. It must be blank and not using a compound assignment.«; IntelliJ meldet über den Java-Compiler »cannot assign a value to final variable …«.

Java erlaubt bei finalen Werten eine aufgeschobene Initialisierung. Das heißt, dass nicht zwingend zum Zeitpunkt der Variablendeklaration ein Wert zugewiesen werden muss. Das sehen wir im Beispiel an der Variablen perimeter.

Werden Variablen deklariert und initialisiert, können final und var zusammen eingesetzt werden. Einige Programmiersprachen bieten hier ein eigenes Schlüsselwort, z. B. val, Java jedoch nicht.

Auch Objektvariablen und Klassenvariablen können final sein. Allerdings müssen die Variablen dann entweder bei der Deklaration belegt werden oder in einer aufgeschobenen Initialisierung im Konstruktor. Wir werden uns dies in Kapitel 6, »Eigene Klassen schreiben«, noch einmal genauer ansehen. Werden finale Variablen vererbt, so können Unterklassen diesen Wert auch nicht mehr überschreiben (das wäre ein Problem, aber vielleicht auch ein Vorteil für manche Konstanten).

Das Schlüsselwort final hat noch zusätzliche Bedeutungen im Zusammenhang mit Vererbung. Das werden wir uns ebenfalls später anschauen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.5    Konsoleneingaben 

Bisher haben wir Methoden zur Ausgabe kennengelernt und random(). Die println(…)-Methoden »hängen« am System.out- bzw. System.err-Objekt, und random() »hängt« am Math-Objekt.

Der Gegenpol zu printXXX(…) ist eine Konsoleneingabe. Hier gibt es unterschiedliche Varianten. Die einfachste ist die mit der Klasse java.util.Scanner. In Abschnitt 5.10.2, »Yes we can, yes we scan – die Klasse Scanner«, wird die Klasse noch viel genauer untersucht. Es reicht aber an dieser Stelle zu wissen, wie Strings, Ganzzahlen und Fließkommazahlen eingelesen werden.

Eingabe lesen vom Typ

Anweisung

String

String s = new java.util.Scanner(System.in).nextLine();

int

int i = new java.util.Scanner(System.in).nextInt();

double

double d = new java.util.Scanner(System.in).nextDouble();

Tabelle 2.7    Einlesen einer Zeichenkette bzw. Ganz- und Fließkommazahl von der Konsole

Verbinden wir die drei Möglichkeiten zu einem Beispiel. Zunächst soll der Name eingelesen werden, dann das Alter und anschließend eine Fließkommazahl:

Listing 2.10    src/main/java/SmallConversation.java

public class SmallConversation {

public static void main( String[] args ) {

System.out.println( "Moin! Wie heißt denn du?" );

String name = new java.util.Scanner( System.in ).nextLine();

System.out.printf( "Hallo %s. Wie alt bist du?%n", name );

int age = new java.util.Scanner( System.in ).nextInt();

System.out.printf( "Aha, %s Jahre, das ist ja die Hälfte von %s.%n",

age, age * 2 );

System.out.println( "Sag mal, was ist deine Lieblingsfließkommazahl?" );

double value = new java.util.Scanner( System.in ).nextDouble();

System.out.printf( "%s? Aha, meine ist %s.%n",

value, Math.random() * 100000 );

}

}

Eine Konversation sieht somit etwa so aus:

Moin! Wie heißt denn du?

Christian

Hallo Christian. Wie alt bist du?

37

Aha, 37 Jahre, das ist ja die Hälfte von 74.Sag mal, was ist deine Lieblingsfließkommazahl?

9,7

9.7? Aha, meine ist 60769.81705995359.

Die Eingabe der Fließkommazahl muss mit Komma erfolgen, wenn die JVM auf einem deutschsprachigen Betriebssystem läuft. Die Ausgabe über printf(…) kann ebenfalls lokalisierte Fließkommazahlen schreiben, dann muss jedoch statt des Platzhalters %s die Kennung %f oder %g verwendet werden. Das wollen wir in einem zweiten Beispiel nutzen.

Zinsberechnung mit der Benutzereingabe

Die Zinsberechnung, die vorher feste Werte im Programm hatte, soll eine Benutzereingabe bekommen. Des Weiteren erwarten wir die Dauer in Monaten statt in Jahren. Zinseszinsen berücksichtigt das Programm nicht.

Listing 2.11    src/main/java/MyInterestRates.java

public class MyInterestRates {

public static void main( String[] args ) {

System.out.println( "Kapital?" );

double capital = new java.util.Scanner( System.in ).nextDouble();

System.out.println( "Zinssatz?" );

double interestRate = new java.util.Scanner( System.in ).nextDouble();

System.out.println( "Anlagedauer in Monaten?" );

int month = new java.util.Scanner( System.in ).nextInt();

double totalInterestRate = capital * interestRate * month / (12*100);

System.out.printf( "Zinsen: %g%n", totalInterestRate );

}

}

Die vorher fest verdrahteten Werte sind nun alle dynamisch:

Kapital?

20000

Zinssatz?

3,6

Anlagedauer in Monaten?

24

Zinsen: 1440,00

Um den Zinseszins berücksichtigen zu können, muss eine Potenz mit in die Formel gebracht werden. Die nötige Methode dazu ist Math.pow(a, b), was a hoch b berechnet. Finanzmathematikern ist das als Übung überlassen.

Soll die Eingabe nicht von der Konsole kommen, sondern von einem eigenen Dialog, hilft eine Klasse aus dem Swing-Paket:

String input = javax.swing.JOptionPane.showInputDialog( "Eingabe" );

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.6    Fließkommazahlen mit den Datentypen float und double 

Für Fließkommazahlen (auch Gleitkommazahlen genannt) einfacher und erhöhter Genauigkeit bietet Java die Datentypen float und double. Die Datentypen sind im IEEE-754-Standard beschrieben und haben eine Länge von 4 Byte für float und 8 Byte für double. Fließkommaliterale können einen Vorkommateil und einen Nachkommateil besitzen, die durch einen Dezimalpunkt (kein Komma) getrennt sind. Ein Fließkommaliteral muss keine Vor- oder Nachkommastellen besitzen, sodass auch Folgendes gültig ist:

double d = 10.0 + 20. + .11;

Nur den Punkt allein zu nutzen ist natürlich Unsinn, wobei .0 schon erlaubt ist.

Der Datentyp float ist mit 4 Byte, also 32 Bit, ein schlechter Scherz. Der Datentyp double geht mit 64 Bit ja gerade noch. Die IA32-, x86-64- und Itanium-Prozessoren unterstützen mit 80 Bit einen »double extended«-Modus und damit bessere Präzision.

Der Compiler meldet keinen Fehler, wenn eine Fließkommazahl nicht präzise dargestellt werden kann. Es ist kein Fehler, Folgendes zu schreiben:

double pi = 3.141592653589793238462643383279502884197169399375105820974944592;

Der Datentyp float *

Standardmäßig sind die Fließkommaliterale vom Typ double. Ein nachgestelltes f (oder F) zeigt dem Compiler an, dass es sich um ein float handelt.

Gültige Zuweisungen für Fließkommazahlen vom Typ double und float sehen so aus:

double pi = 3.1415, delta = .001;

float ratio = 4.33F;

Auch für den Datentyp double lässt sich ein d (oder D) nachstellen, was allerdings nicht nötig ist, wenn Literale für Kommazahlen im Quellcode stehen; Zahlen wie 3.1415 sind automatisch vom Typ double. Während jedoch bei 1 + 2 + 4.0 erst 1 und 2 als int addiert werden, dann das Ereignis in double konvertiert wird und anschließend 4.0 addiert wird, würde 1D + 2 + 4.0 gleich mit der Fließkommazahl 1 beginnen. So ist auch 1D gleich 1. bzw. 1.0.

Was ist das Ergebnis der Ausgabe?

System.out.println( 20000000000F == 20000000000F+1 );

System.out.println( 20000000000D == 20000000000D+1 );

Tipp: Was sind die Wertebereiche von float und double?

Noch genauere Auflösung bei Fließkommazahlen *

Einen höher auflösenden bzw. präziseren Datentyp für Fließkommazahlen als double gibt es nicht. Die Standardbibliothek bietet für diese Aufgabe in java.math die Klasse BigDecimal an, die in Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, näher beschrieben ist. Das ist sinnvoll für Daten, die eine sehr gute Genauigkeit aufweisen sollen, wie zum Beispiel Währungen.

In C# gibt es den Datentyp decimal, der mit 128 Bit (also 16 Byte) auch genügend Präzision bietet, um eine Zahl wie 0,000000000000000000000000001 auszudrücken.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.7    Ganzzahlige Datentypen 

Java stellt fünf ganzzahlige Datentypen zur Verfügung: byte, short, char, int und long. Die feste Länge von jeweils 1, 2, 2, 4 und 8 Byte ist eine wesentliche Eigenschaft von Java. Ganzzahlige Typen sind in Java immer vorzeichenbehaftet (mit der Ausnahme von char); einen Modifizierer unsigned wie in C(++) gibt es nicht. Negative Zahlen werden durch Voranstellen eines Minuszeichens gebildet. Ein Pluszeichen für positive Zeichen ist möglich. int und long sind die bevorzugten Typen. byte kommt selten vor und short nur in wirklich sehr seltenen Fällen, etwa bei Arrays mit Bilddaten.

Ganzzahlen sind standardmäßig vom Typ int

Betrachten wir folgende Zeile, so ist auf den ersten Blick kein Fehler zu erkennen:

System.out.println( 123456789012345 ); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Dennoch übersetzt der Compiler die Zeile nicht, da er ein Ganzzahlliteral ohne explizite Größenangabe als 32 Bit langes int annimmt. Die obige Zeile führt daher zu einem Compilerfehler, da unsere Zahl nicht im gültigen int-Wertebereich von –2.147.483.648 … +2.147.483.647 liegt, sondern weit außerhalb: 2147483647 < 123456789012345. Java reserviert also nicht so viele Bits wie benötigt und wählt nicht automatisch den passenden Wertebereich.

Mit long zum mehrfachen Milliardär

Der Java-Compiler betrachtet jedes Ganzzahl-Literal automatisch als int. Sollte der Wertebereich von etwa plus/minus 2 Milliarden nicht reichen, greifen Entwickler zum nächsthöheren Datentyp, zum long. Dass eine Zahl long ist, muss ausdrücklich angegeben werden. Dazu wird an das Ende von Ganzzahlliteralen vom Typ long ein l oder L gesetzt. Um die Zahl 123456789012345 gültig ausgeben zu lassen, ist Folgendes zu schreiben:

System.out.println( 123456789012345L );

Das kleine »l« hat sehr viel Ähnlichkeit mit der Ziffer Eins. Daher sollte bei Längenangaben immer ein großes »L« eingefügt werden.

Was gibt die folgende Anweisung aus?

System.out.println( 123456789 + 5432l );

Der Datentyp byte

Ein byte ist ein Datentyp mit einem Wertebereich von –128 bis +127. Eine Initialisierung wie

byte b = 200; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

ist also nicht erlaubt, da 200 > 127 ist. Somit fallen alle Zahlen von 128 bis 255 (hexadezimal 0x80 – 0xFF) raus. In der Datenverarbeitung ist das Java-byte, weil es ein Vorzeichen trägt, nur mittelprächtig brauchbar, da insbesondere in der Dateiverarbeitung Wertebereiche von 0 bis 255 erwünscht sind.

Java erlaubt zwar keine vorzeichenlosen Ganzzahlen, aber mit einer expliziten Typumwandlung lassen sich doch Zahlen wie 200 in einem byte speichern:

Der Java-Compiler nimmt dazu einfach die Bitbelegung von 200 (0b00000000_00000000_00000000_11001000), schneidet bei der Typumwandlung die oberen drei Byte ab und interpretiert das oberste dann gesetzte Bit als Vorzeichen-Bit. Bei der Ausgabe fällt das auf:

byte b = (byte) 200;

System.out.println( b ); // -56

Mehr zur Typumwandlung folgt in Abschnitt 2.4.10, »Die Typumwandlung (das Casting)«.

Der Datentyp short *

Der Datentyp short ist selten anzutreffen. Mit seinen 2 Byte kann er einen Wertebereich von –32.768 bis +32.767 darstellen. Das Vorzeichen »kostet« wie bei den anderen Ganzzahlen 1 Bit, sodass nicht 16 Bit, sondern nur 15 Bit für Zahlen zu Verfügung stehen. Allerdings gilt wie beim byte, dass auch ein short ohne Vorzeichen auf zwei Arten initialisiert werden kann:

short s = (short) 33000;

System.out.println( s ); // -32536

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.8    Wahrheitswerte 

Der Datentyp boolean beschreibt einen Wahrheitswert, der entweder true oder false ist. Die Zeichenketten true und false sind reservierte Wörter und bilden neben konstanten Strings und primitiven Datentypen Literale. Kein anderer Wert ist für Wahrheitswerte möglich, insbesondere werden numerische Werte nicht als Wahrheitswerte interpretiert.

Der boolesche Typ wird beispielsweise bei Bedingungen, Verzweigungen oder Schleifen benötigt. In der Regel ergibt sich ein Wahrheitswert aus Vergleichen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.9    Unterstriche in Zahlen 

Um eine Anzahl von Millisekunden in Tage zu konvertieren, muss einfach eine Division vorgenommen werden. Um Millisekunden in Sekunden umzurechnen, brauchen wir eine Division durch 1.000, von Sekunden auf Minuten eine Division durch 60, von Minuten auf Stunden eine Division durch 60, und die Stunden auf Tage bringt die letzte Division durch 24. Schreiben wir das auf:

long millis = 10 * 24 * 60 * 60 * 1000L;

long days = millis / 86400000L;

System.out.println( days ); // 10

Eine Sache fällt bei der Zahl 86.400.000 auf: Besonders gut lesbar ist sie nicht. Die eine Lösung ist, es erst gar nicht zu so einer Zahl kommen zu lassen und sie wie in der ersten Zeile durch eine Reihe von Multiplikationen aufzubauen – mehr Laufzeit kostet das nicht, da dieser konstante Ausdruck zur Übersetzungszeit feststeht.

Die zweite Variante macht durch Unterstriche Zahlen besser lesbar, denn der Unterstrich gliedert die Zahl in Blöcke. Anstatt ein numerisches Literal als 86.400.000 zu schreiben, ist auch Folgendes erlaubt:

long millis = 10 * 86_400_000L;

long days = millis / 86_400_000L;

System.out.println( days ); // 10

Die Unterstriche machen die 1.000er-Blöcke gut sichtbar.

Hilfreich ist die Schreibweise auch bei Literalen in Binär- und Hexadezimaldarstellung, da die Unterstriche hier ebenfalls Blöcke absetzen können:

int i = 0b01101001_01001101_11100101_01011110;

long l = 0x7fff_ffff_ffff_ffffL;

Mit 0b beginnt ein Literal in Binärschreibweise, mit 0x in Hexadezimalschreibweise (weitere Details folgen in Kapitel 21, »Bits und Bytes, Mathematisches und Geld«).

Der Unterstrich darf in jedem Literal stehen, zwei aufeinanderfolgende Unterstriche sind aber nicht erlaubt, und er darf nicht am Anfang stehen.

Die Unterstriche in Literalen sind nur eine Hilfe wie Leerzeichen zur Einrückung. Im Bytecode ist davon nichts mehr zu lesen. In der Klassendatei sehen 0b01101001_01001101_11100101_01011110 und 0b01101001010011011110010101011110 identisch aus, insbesondere weil sie sowieso als Ganzzahl 1766712670 abgelegt sind.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.10    Alphanumerische Zeichen 

Der alphanumerische Datentyp char (von engl. character, Zeichen) ist 2 Byte groß und nimmt ein Unicode-Zeichen auf. Ein char ist nicht vorzeichenbehaftet. Die Literale für Zeichen werden in einfache Hochkommata gesetzt. Spracheinsteiger verwechseln häufig die einfachen Hochkommata mit den Anführungszeichen der Zeichenketten (Strings). Die einfache Merkregel lautet: ein Zeichen – ein Hochkomma; mehrere Zeichen – zwei Hochkommata (Gänsefüßchen).

Korrekte Hochkommata für Zeichen und Zeichenketten:

char c = 'a';

String s = "Heut' schon gebeckert?";

Da der Compiler ein char automatisch in ein int konvertieren kann, ist auch int c = 'a'; gültig.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.11    Gute Namen, schlechte Namen 

Für die optimale Lesbarkeit und Verständlichkeit eines Programmcodes sollten Entwickler beim Schreiben einige Punkte berücksichtigen:

  • Ein konsistentes Namensschema ist wichtig. Heißt ein Zähler no, nr, cnr oder counter? Auch sollten wir korrekt schreiben und auf Rechtschreibfehler achten, denn leicht wird aus necessaryConnection sonst nesesarryConnection. Variablen ähnlicher Schreibweise, etwa counter und counters, sind zu vermeiden.

  • Abstrakte Bezeichner sind ebenfalls zu vermeiden. Die Deklaration int TEN = 10; ist absurd. Eine unsinnige Idee ist auch die folgende: boolean FALSE = true, TRUE = false;. Im Programmcode würde dann mit FALSE und TRUE gearbeitet. Einer der obersten Plätze bei einem Wettbewerb für die verpfuschtesten Java-Programme wäre uns gewiss.

  • Unicode-Sequenzen können zwar in Bezeichnern aufgenommen werden, doch sollten sie vermieden werden. In double übelkübel, \u00FCbelk\u00FCbel; sind beide Bezeichnernamen gleich, und der Compiler meldet einen Fehler.

  • 0 und O und 1 und l sind leicht zu verwechseln. Die Kombination »rn« ist schwer zu lesen und je nach Zeichensatz leicht mit »m« zu verwechseln. Gültig – aber böse – ist auch: int ínt, ìnt, înt; boolean bôõleañ;

In China gibt es 90 Millionen Familien mit dem Nachnamen Li. Das wäre so, als ob wir jede Variable temp1, temp2 … nennen würden.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Ist ein Bezeichnername unglücklich gewählt (pneumonoultramicroscopicsilicovolcanoconiosis ist schon etwas lang), so lässt er sich problemlos konsistent umbenennen. Dazu wählen wir im Menü RefactorRename – oder auch kurz (Alt)+(ª)+(R); der Cursor muss auf dem Bezeichner stehen. Eine optionale Vorschau (engl. preview) zeigt an, welche Änderungen die Umbenennung nach sich ziehen wird. Neben Rename gibt es auch noch eine andere Möglichkeit: Dazu lässt sich auf der Variablen mit (Strg)+(1) ein Popup-Fenster mit Local Rename öffnen. Der Bezeichner wird selektiert und lässt sich ändern. Gleichzeitig ändern sich alle Bezüge auf die Variable mit.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.3.12    Keine automatische Initialisierung von lokalen Variablen 

Die Laufzeitumgebung – bzw. der Compiler – initialisiert lokale Variablen nicht automatisch mit einem Nullwert bzw. Wahrheitsvarianten nicht mit false. Vor dem Lesen müssen lokale Variablen von Hand initialisiert werden, andernfalls gibt der Compiler eine Fehlermeldung aus.

Im folgenden Beispiel seien die beiden lokalen Variablen age und adult nicht automatisch initialisiert, und so kommt es bei der versuchten Ausgabe von age zu einem Compilerfehler. Der Grund ist, dass ein Lesezugriff nötig ist, aber vorher noch kein Schreibzugriff stattfand.

int age;

boolean adult;

System.out.println( age ); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Local variable age may not

// have been initialized.

age = 18;

if ( age >= 18 ) // Fallunterscheidung: wenn-dann

adult = true;

System.out.println( adult ); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Local variable adult may not

// have been initialized.

Weil Zuweisungen in bedingten Anweisungen vielleicht nicht ausgeführt werden, meldet der Compiler auch bei System.out.println(adult) einen Fehler, da er analysiert, dass es einen Programmfluss ohne die Zuweisung gibt. Da adult nur nach der if-Abfrage auf den Wert true gesetzt wird, wäre nur unter der Bedingung, dass age größer gleich 18 ist, ein Schreibzugriff auf adult erfolgt und ein folgender Lesezugriff möglich. Doch da der Compiler annimmt, dass es andere Fälle geben kann, wäre ein Zugriff auf eine nicht initialisierte Variable ein Fehler.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Eclipse zeigt einen Hinweis und einen Verbesserungsvorschlag an, wenn eine lokale Variable nicht initialisiert ist.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 7

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Beginnen wir mit mathematischen Ausdrücken, um dann die Schreibweise in Java zu ermitteln. Eine mathematische Formel, etwa der Ausdruck -27 * 9, besteht aus Operanden (engl. operands) und Operatoren (engl. operators). Ein Operand ist zum Beispiel eine Variable, ein Literal oder die Rückgabe eines Methodenaufrufs. Im Fall einer Variablen wird der Wert aus der Variablen ausgelesen und mit ihm die Berechnung durchgeführt.

Die Arten von Operatoren

Die Operatoren verknüpfen die Operanden. Je nach Anzahl der Operanden unterscheiden wir folgende Arten von Operatoren:

  • Ist ein Operator auf genau einem Operanden definiert, so nennt er sich unärer Operator (oder einstelliger Operator). Das Minus (negatives Vorzeichen) vor einem Operanden ist ein unärer Operator, da er für genau den folgenden Operanden gilt.

  • Die üblichen Operatoren Plus, Minus, Mal sowie Geteilt sind binäre (zweistellige) Operatoren.

  • Es gibt auch einen Fragezeichen-Operator für bedingte Ausdrücke, der dreistellig ist.

Operatoren erlauben die Verbindung einzelner Ausdrücke zu neuen Ausdrücken. Einige Operatoren sind aus der Schule bekannt, wie Addition, Vergleich, Zuweisung und weitere. C(++)-Programmierer werden viele Freunde wiedererkennen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.1    Zuweisungsoperator 

In Java dient das Gleichheitszeichen = der Zuweisung (engl. assignment). Der Zuweisungsoperator ist ein binärer Operator, bei dem auf der linken Seite die zu belegende Variable steht und auf der rechten Seite ein Ausdruck.

Ein Ausdruck mit Zuweisungen:

Die Multiplikation berechnet das Produkt von 12 und 2 und speichert das Ergebnis in j ab. Von allen primitiven Variablen, die in dem Ausdruck vorkommen, wird also der Wert ausgelesen und in den Ausdruck eingesetzt. Dies nennt sich auch Wertoperation, da der Wert der Variablen betrachtet wird und nicht ihr Speicherort oder gar ihr Variablenname.

Erst nach dem Auswerten des Ausdrucks kopiert der Zuweisungsoperator das Ergebnis in die Variable. Gibt es Laufzeitfehler, etwa durch eine Division durch null, gibt es keinen Schreibzugriff auf die Variable.

Das einfache Gleichheitszeichen = dient in Java nur der Zuweisung. Das ist in fast allen Programmiersprachen so. Selten verwenden Programmiersprachen für die Zuweisung ein anderes Symbol, etwa Pascal := oder F# und R <-. Um Zuweisungen von Vergleichen trennen zu können, definiert Java hier, der C(++)-Tradition folgend, einen binären Vergleichsoperator == mit zwei Gleichheitszeichen. Der Vergleichsoperator liefert immer den Ergebnistyp boolean:

int baba = 1;

System.out.println( baba == 1 ); // "true": Ausdruck mit Vergleich

System.out.println( baba = 2 ); // "2": Ausdruck mit Zuweisung

Zuweisungen sind auch Ausdrücke

Zwar finden sich Zuweisungen oft als Ausdrucksanweisung wieder, doch können sie an jeder Stelle stehen, an der ein Ausdruck erlaubt ist, etwa in einem Methodenaufruf wie printXXX(…):

int a = 1; // Deklaration mit Initialisierung

a = 2; // Anweisung mit Zuweisung

System.out.println( a = 3 ); // Ausdruck mit Zuweisung. Liefert 3.

Mehrere Zuweisungen in einem Schritt

Zuweisungen der Form a = b = c = 0; sind erlaubt und gleichbedeutend mit den drei Anweisungen c = 0; b = c; a = b;. Die explizite Klammerung a = (b = (c = 0)) macht noch einmal deutlich, dass sich Zuweisungen verschachteln lassen und Zuweisungen wie c = 0 Ausdrücke sind, die einen Wert liefern. Doch auch dann, wenn wir meinen, dass

eine coole Vereinfachung im Vergleich zu

ist, sollten wir mit einer Zuweisung pro Zeile auskommen.

Die Reihenfolge der Auswertung zeigt anschaulich folgendes Beispiel:

int b = 10;

System.out.println( (b = 20) * b ); // 400

System.out.println( b ); // 20

Im Produktivcode sollte so etwas dennoch nicht stehen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.2    Arithmetische Operatoren 

Ein arithmetischer Operator verknüpft die Operanden mit den Operatoren Addition (+), Subtraktion (–), Multiplikation (*) und Division (/). Zusätzlich gibt es den Restwert-Operator (%), der den bei der Division verbleibenden Rest betrachtet. Alle Operatoren sind für ganzzahlige Werte sowie für Fließkommazahlen definiert. Die arithmetischen Operatoren sind binär, und auf der linken und rechten Seite sind die Typen numerisch. Der Ergebnistyp ist ebenfalls numerisch.

Numerische Umwandlung

Bei Ausdrücken mit unterschiedlichen numerischen Datentypen, etwa int und double, bringt der Compiler vor der Anwendung der Operation alle Operanden auf den umfassenderen Typ. Vor der Auswertung von 1 + 2.0 wird somit die Ganzzahl 1 in ein double konvertiert und dann die Addition vorgenommen – das Ergebnis ist auch vom Typ double. Das nennt sich numerische Umwandlung (engl. numeric promotion). Bei byte und short gilt die Sonderregelung, dass sie vorher in int konvertiert werden. (Auch im Java-Bytecode gibt es keine arithmetischen Operationen auf byte, short und char.) Anschließend wird die Operation ausgeführt, und der Ergebnistyp entspricht dem umfassenderen Typ.

Der Divisionsoperator

Der binäre Operator / bildet den Quotienten aus Dividend und Divisor. Auf der linken Seite steht der Dividend und auf der rechten der Divisor. Die Division ist für Ganzzahlen und für Fließkommazahlen definiert. Bei der Ganzzahldivision wird zu null hin gerundet, und das Ergebnis ist keine Fließkommazahl, sodass 1/3 das Ergebnis 0 ergibt und nicht 0,333… Den Datentyp des Ergebnisses bestimmen die Operanden und nicht der Operator. Soll das Ergebnis vom Typ double sein, muss mindestens ein Operand ebenfalls double sein.

System.out.println( 1.0 / 3 ); // 0.3333333333333333

System.out.println( 1 / 3.0 ); // 0.3333333333333333

System.out.println( 1 / 3 ); // 0

Strafe bei Division durch null

Schon die Schulmathematik lehrte uns, dass die Division durch null nicht erlaubt ist. Führen wir in Java eine Ganzzahldivision mit dem Divisor 0 durch, so bestraft uns Java mit einer ArithmeticException. Wird die ArithmeticException nicht behandelt, führt das zum Ende des ausführenden Threads, und wenn die ungeprüfte Ausnahme im main-Thread stattfindet und nicht abgefangen wird, führt dies zum Ende des Programms. Bei Fließkommazahlen liefert eine Division durch 0 keine Ausnahme, sondern +/– unendlich und bei 0.0/0.0 den Sonderwert NaN (mehr dazu folgt in Kapitel 21, »Bits und Bytes, Mathematisches und Geld«). NaN steht für Not a Number (auch manchmal »Unzahl« genannt) und wird vom Prozessor erzeugt, falls er eine mathematische Operation wie die Division durch null nicht durchführen kann.

Auf dem Lenkraketenkreuzer USS Yorktown gab ein Mannschaftsmitglied aus Versehen die Zahl Null ein. Das führte zu einer Division durch null, und der Fehler pflanzte sich so weit fort, dass die Software abstürzte und das Antriebssystem stoppte. Das Schiff trieb mehrere Stunden antriebslos im Wasser.

Der Restwert-Operator % *

Eine Ganzzahldivision muss nicht unbedingt glatt aufgehen, wie im Fall von 9/2. In diesem Fall gibt es den Rest 1. Diesen Rest liefert der Restwert-Operator (engl. remainder operator), oft auch Modulo genannt. Die Operanden können auch negativ sein.

System.out.println( 9 % 2 ); // 1

Die Division und der Restwert richten sich in Java nach einer einfachen Formel: (int)(a/b) × b + (a%b) = a.

Die Gleichung ist erfüllt, wenn wir etwa a = 10 und b = 3 wählen. Es gilt: (int)(10/3) = 3 und 10 % 3 ergibt 1. Dann ergeben 3 * 3 + 1 = 10.

Aus dieser Gleichung folgt, dass beim Restwert das Ergebnis nur dann negativ ist, wenn der Dividend negativ ist; das Ergebnis ist nur dann positiv, wenn der Dividend positiv ist. Es ist leicht einzusehen, dass das Ergebnis der Restwert-Operation immer echt kleiner ist als der Wert des Divisors. Wir haben den gleichen Fall wie bei der Ganzzahldivision, dass ein Divisor mit dem Wert 0 eine ArithmeticException auslöst und bei Fließkommazahlen zum Ergebnis NaN führt.

Listing 2.12    src/main/java/RemainderAndDivDemo.java, main()

System.out.println( "+5% +3 = " + (+5% +3) ); // 2

System.out.println( "+5 / +3 = " + (+5 / +3) ); // 1

System.out.println( "+5% -3 = " + (+5% -3) ); // 2

System.out.println( "+5 / -3 = " + (+5 / -3) ); // -1

System.out.println( "-5% +3 = " + (-5% +3) ); // -2

System.out.println( "-5 / +3 = " + (-5 / +3) ); // -1

System.out.println( "-5% -3 = " + (-5% -3) ); // -2

System.out.println( "-5 / -3 = " + (-5 / -3) ); // 1

Gewöhnungsbedürftig ist die Tatsache, dass der erste Operand (Dividend) das Vorzeichen des Restes definiert und niemals der zweite (Divisor). In Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, werden wir eine Methode floorMod(…) kennenlernen, die etwas anders arbeitet.

Um mit value % 2 == 1 zu testen, ob value eine ungerade Zahl ist, muss value positiv sein, denn -3 % 2 wertet Java zu –1 aus. Der Test auf ungerade Zahlen wird erst wieder korrekt mit value % 2 != 0.

Restwert für Fließkommazahlen

Der Restwert-Operator ist auch auf Fließkommazahlen anwendbar, und die Operanden können wiederum negativ sein.

Teste, ob eine double-Zahl doch eine Ganzzahl ist: (d % 1) == 0.

Wem das zu verrückt ist, der nutzt alternativ d == Math.rint(d).

Restwert für Fließkommazahlen und Math.IEEEremainder( ) *

Über die oben genannte Formel können wir auch bei Fließkommazahlen das Ergebnis einer Restwert-Operation leicht berechnen. Dabei muss beachtet werden, dass sich der Operator nicht so wie unter IEEE 754 verhält. Denn diese Norm schreibt vor, dass die Restwert-Operation den Rest von einer rundenden Division berechnet und nicht von einer abschneidenden. So wäre das Verhalten nicht analog zum Restwert bei Ganzzahlen. Java definiert den Restwert jedoch bei Fließkommazahlen genauso wie den Restwert bei Ganzzahlen. Wünschen wir ein Restwert-Verhalten, wie IEEE 754 es vorschreibt, so können wir die statische Bibliotheksmethode Math.IEEEremainder(…) verwenden.

Auch bei der Restwert-Operation bei Fließkommazahlen werden wir niemals eine Exception erwarten. Eventuelle Fehler werden, wie im IEEE-Standard beschrieben, mit NaN angegeben. Ein Überlauf oder Unterlauf kann zwar vorkommen, aber nicht geprüft werden.

Rundungsfehler *

Prinzipiell sollten Anweisungen wie 1.1 - 0.1 immer 1.0 ergeben, jedoch treten interne Rundungsfehler bei der Darstellung auf und lassen das Ergebnis von Berechnung zu Berechnung immer ungenauer werden. Ein besonders ungünstiger Fehler trat 1994 beim Pentium-Prozessor im Divisionsalgorithmus Radix-4 SRT auf, ohne dass der Programmierer der Schuldige war:

double x, y, z;

x = 4195835.0;

y = 3145727.0;

z = x - (x/y) * y;

System.out.println( z );

Ein fehlerhafter Prozessor liefert hier 256, obwohl laut Rechenregel das Ergebnis 0 sein muss. Laut Intel sollte für einen normalen Benutzer (Spieler, Softwareentwickler, Surfer?) der Fehler nur alle 27.000 Jahre auftauchen. Glück für die meisten. Eine Studie von IBM errechnete eine Fehlerhäufigkeit von einmal in 24 Tagen. Alles in allem nahm Intel die CPUs zurück, verlor über 400 Millionen US-Dollar und zog spät den Kopf gerade noch aus der Schlinge.

Die meisten Rundungsfehler resultieren aber daher, dass endliche Dezimalbrüche im Rechner als Näherungswerte für periodische Binärbrüche repräsentiert werden müssen. 0.1 entspricht einer periodischen Mantisse im IEEE-Format.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.3    Unäres Minus und Plus 

Die binären Operatoren sitzen zwischen zwei Operanden, während sich ein unärer Operator genau einen Operanden vornimmt. Das unäre Minus (Operator zur Vorzeichenumkehr) etwa dreht das Vorzeichen des Operanden um. So wird aus einem positiven Wert ein negativer und aus einem negativen Wert ein positiver.

Drehe das Vorzeichen einer Zahl um: a = -a;

Eine Alternative ist: a = –1 * a;

Das unäre Plus ist eigentlich unnötig; die Entwickler haben es jedoch aus Symmetriegründen mit eingeführt.

Minus und Plus sitzen direkt vor dem Operanden, und der Compiler weiß selbstständig, ob dies unär oder binär ist. Der Compiler erkennt auch folgende Konstruktion:

Dies ergibt den Wert –5. Es ist leichter, den Compiler zu verstehen, wenn wir die Operatorrangfolge einbeziehen und gedanklich Klammen setzen: -(-(-2)) + (-(+3)). Einen Ausdruck wie ‐--2+-+3 erkennt der Compiler dagegen nicht an, da die zusammenhängenden Minuszeichen als Dekrement interpretiert werden und nicht als unärer Operator. Das Trennzeichen, Leerzeichen in diesem Fall, ist also bedeutend.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.4    Präfix- oder Postfix-Inkrement und -Dekrement 

Das Herauf- und Heruntersetzen von Variablen ist eine sehr häufige Operation, wofür die Entwickler in der Vorgängersprache C auch einen Operator spendiert hatten. Die praktischen Operatoren ++ und -- kürzen die Programmzeilen zum Inkrement und Dekrement ab:

i++; // Abkürzung für i = i + 1

j--; // j = j - 1

Eine lokale Variable muss allerdings vorher initialisiert sein, da ein Lesezugriff vor einem Schreibzugriff stattfindet. Der ++/––-Operator erfüllt somit zwei Aufgaben: Neben der Wertrückgabe gibt es eine Veränderung der Variablen.

Das Post-Inkrement finden wir auch im Namen der Programmiersprache C++. Es soll ausdrücken, dass es »C-mit-eins-drauf« ist, also ein verbessertes C. Mit dem Wissen über den Postfix-Operator ist klar, dass diese Erhöhung aber erst nach der Nutzung auftritt – also ist C++ auch nur C, und der Vorteil kommt später. Einer der Entwickler von Java, Bill Joy, beschrieb einmal Java als C++-- . Er meinte damit C++ ohne die schwer zu pflegenden Eigenschaften.

Vorher oder nachher?

Die beiden Operatoren liefern einen Ausdruck und geben daher einen Wert zurück. Es macht jedoch einen feinen Unterschied, wo dieser Operator platziert wird. Es gibt ihn nämlich in zwei Varianten: Er kann vor der Variablen stehen, wie in ++i (Präfix-Schreibweise), oder dahinter, wie bei i++ (Postfix-Schreibweise). Der Präfix-Operator verändert die Variable vor der Auswertung des Ausdrucks, und der Postfix-Operator ändert sie nach der Auswertung des Ausdrucks. Mit anderen Worten: Nutzen wir einen Präfix-Operator, so wird die Variable erst herauf- bzw. heruntergesetzt und dann der Wert geliefert.

Präfix/Postfix in einer Ausgabeanweisung:

Präfix-Inkrement und -Dekrement

Postfix-Inkrement und -Dekrement

int i = 10, j = 20;

System.out.println( ++i ); // 11

System.out.println( --j ); // 19

System.out.println( i ); // 11

System.out.println( j ); // 19

int i = 10, j = 20;

System.out.println( i++ ); // 10

System.out.println( j-- ); // 20

System.out.println( i ); // 11

System.out.println( j ); // 19

Mit der Möglichkeit, Variablen zu erhöhen und zu vermindern, ergeben sich vier Varianten:

Präfix

Postfix

Inkrement

Prä-Inkrement, ++i

Post-Inkrement, i++

Dekrement

Prä-Dekrement, --i

Post-Dekrement, i--

Tabelle 2.8    Präfix- und Postfix-Inkrement und -Dekrement

In Java sind Inkrement (++) und Dekrement (--) für alle numerischen Datentypen erlaubt, also auch für Fließkommazahlen:

double d = 12;

System.out.println( --d ); // 11.0

double e = 12.456;

System.out.println( --e ); // 11.456

Einige Kuriositäten *

Wir wollen uns abschließend noch mit einer Besonderheit des Post-Inkrements und Prä-Inkrements beschäftigen, die nicht nachahmenswert ist:

int i = 1;

i = ++i;

System.out.println( i ); // 2

int j = 1;

j = j++;

System.out.println( j ); // 1

Der erste Fall überrascht nicht, denn i = ++i erhöht den Wert 1 um 1, und anschließend wird 2 der Variablen i zugewiesen. Bei j ist es raffinierter: Der Wert von j ist 1, und dieser Wert wird intern vermerkt. Anschließend erhöht j++ die Variable um 1. Doch die Zuweisung setzt j auf den gemerkten Wert, der 1 war. Also ist j = 1.

Je mehr Freiheiten ein Compiler hat, desto ungenierter kann er optimieren. Besonders Schreibzugriffe interessieren einen Compiler, denn kann er diese einsparen, läuft das Programm später ein bisschen schneller. Damit das Resultat eines Compilers jedoch beherrschbar bleibt, definiert der C(++)-Standard Sequenzpunkte (engl. sequence points), an denen alle Schreibzugriffe klar zugewiesen wurden (dass der Compiler später Optimierungen vornimmt, ist eine andere Geschichte; die Sequenzpunkte gehören zum semantischen Modell, Optimierungen verändern das nicht). Das Semikolon als Abschluss von Anweisungen bildet zum Beispiel einen Sequenzpunkt. In einem Ausdruck wie i = i + 1; j = i; muss der Schreibzugriff auf i aufgelöst sein, bevor ein Lesezugriff für die Zuweisung zu j erfolgt. Problematisch ist, dass es gar nicht so viele Sequenzpunkte gibt und es passieren kann, dass zwischen zwei Sequenzpunkten zwei mehrdeutige Schreibzugriffe auf die gleiche Variable stattfinden. Da das jedoch in C(++) undefiniert ist, kann sich der Compiler so verhalten, wie er will – er muss sich ja nur an den Sequenzpunkten so verhalten, wie gefordert. Problemfälle sind: (i=j) + i oder, weil ein Inkrement/Dekrement ein Lese-/Schreibzugriff ist, auch i = i++, was ja nichts anderes als i = (i = i + 1) ist. Bedauerlicherweise bildet die Zuweisung keinen Sequenzpunkt. Bei Zuweisungen der Art i = ++i + --i kann alles Mögliche später in i stehen, je nachdem, was der Compiler zu welchem Zeitpunkt ausführt. In Java sind diese Dinge von der Spezifikation klar geregelt; in C(++) ist nur geregelt, dass das Verhalten zwischen den Sequenzpunkten klar sein muss. Doch moderne Compiler erkennen konkurrierende Schreibzugriffe zwischen zwei Sequenzpunkten und mahnen sie (bei entsprechender Warnstufe) an.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.5    Zuweisung mit Operation (Verbundoperator) 

In Java lassen sich Zuweisungen mit numerischen Operatoren kombinieren. Für einen binären Operator (symbolisch # genannt) im Ausdruck a = a # (b) kürzt der Verbundoperator (engl. compound assignment operator) den Ausdruck zu a #= b ab.

Dazu einige Beispiele:

Ausführliche Schreibweise

Schreibweise mit Verbundoperator

a = a + 2

a += 2

a = a - 10

a -= 10

a = a * –1

a *= –1

a = a / 10

a /= 10

Tabelle 2.9    Ausgeschriebene Variante und kurze Schreibweise mit dem Verbundoperator

Während das Präfix-/Postfix-Inkrement/-Dekrement nur um eins vergrößert/vermindert, erlauben die Verbundoperationen in einer relativ kompakten Schreibweise auch größere Inkremente/Dekremente, wie eben a+=2 oder a-=10.

Eine Zuweisung ist auch immer ein Ausdruck:

int a = 0;

System.out.println( a ); // 0

System.out.println( a = 2 ); // 2

System.out.println( a += 1 ); // 3

System.out.println( a ); // 3

Besondere Obacht sollten wir auf die automatische Klammerung geben. Bei einem Ausdruck wie a *= 3 + 5 gilt a = a * (3 + 5) und nicht selbstverständlich die Punkt-vor-Strich-Regelung a = a * 3 + 5.

Kaum bekannt ist der »Sleepy-Operator« -=-. Beispiel: i -=- i. Was passiert hier?

Einmalige Auswertung bei Array-Zugriffen *

Falls die linke Seite beim Verbundoperator ein Array-Zugriff ist (siehe Abschnitt 4.1, »Einfache Feldarbeit«), wird die Indexberechnung nur einmal vorgenommen. Dies ist wichtig beim Einsatz des Präfix-/Postfix-Operators oder von Methodenaufrufen, die Nebenwirkungen besitzen, also etwa Zustände wie einen Zähler verändern.

Wir profitieren bei Array-Zugriffen vom Verbundoperator, da erstens die Schreibweise kurz ist und zweitens die Auswertung des Index nur einmal stattfindet:

int[] array1 = { 10, 90, 0 };

int i = 0;

array1[++i] = array1[++i] + 10;

System.out.println( Arrays.toString( array1 ) ); // [10, 10, 0]

int[] array2 = { 0, 90, 0 };

int j = 0;

array2[++j] += 10;

System.out.println( Arrays.toString( array2 ) ); // [0, 100, 0]

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.6    Die relationalen Operatoren und die Gleichheitsoperatoren 

Relationale Operatoren sind Vergleichsoperatoren, die Ausdrücke miteinander vergleichen und einen Wahrheitswert vom Typ boolean ergeben. Die von Java für numerische Vergleiche zur Verfügung gestellten Operatoren sind:

  • größer (>)

  • kleiner (<)

  • größer/gleich (≥)

  • kleiner/gleich (≤)

Außerdem gibt es einen Spezialoperator instanceof zum Testen von Typbeziehungen.

Zudem kommen zwei Vergleichsoperatoren hinzu, die Java als Gleichheitsoperatoren bezeichnet:

  • Test auf Gleichheit (==)

  • Test auf Ungleichheit (!=)

Dass Java hier einen Unterschied zwischen Gleichheitsoperatoren und Vergleichsoperatoren macht, liegt an einem etwas anderen Vorrang, der uns aber nicht weiter beschäftigen soll.

Ebenso wie arithmetische Operatoren passen die relationalen Operatoren ihre Operanden an einen gemeinsamen Typ an. Handelt es sich bei den Typen um Referenztypen, so sind nur die Vergleichsoperatoren == und != erlaubt – eine Ausnahme ist der String, bei dem auch + erlaubt ist.

Kaum Verwechslungsprobleme durch == und =

Die Verwendung des relationalen Operators == und der Zuweisung = führt bei Einsteigern oft zu Problemen, da die Mathematik für Vergleiche und Zuweisungen immer nur ein Gleichheitszeichen kennt. Glücklicherweise ist das Problem in Java nicht so drastisch wie beispielsweise in C(++), da die Typen der Operatoren unterschiedlich sind. Der Vergleichsoperator ergibt immer nur den Rückgabewert boolean. Zuweisungen von numerischen Typen ergeben jedoch wieder einen numerischen Typ. Es kann also kein Problem wie das folgende geben:

int a = 10, b = 11;

boolean result1 = ( a = b ); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

boolean result2 = ( a == b );

Die Wahrheitsvariable hasSign soll dann true sein, wenn das Zeichen sign gleich dem Minus ist:

boolean hasSign = (sign == '-');

Die Auswertungsreihenfolge ist folgende: Erst wird das Ergebnis des Vergleichs berechnet, und dieser Wahrheitswert wird anschließend in hasSign kopiert.

Bei einem Vergleich mit == können beide Operanden vertauscht werden – wenn die beiden Seiten keine beeinflussenden Seiteneffekte produzieren, also etwa Zustände ändern. Am Ergebnis ändert sich nichts, denn der Vergleichsoperator ist kommutativ. So sind

if ( worldExpoShanghaiCostInUSD == 58000000000L )

und

if ( 58000000000L == worldExpoShanghaiCostInUSD )

semantisch gleich. Bei einem Gleichheitsvergleich zwischen Variable und Literal werden viele Entwickler mit einer Vergangenheit in der Programmiersprache C die Konstanten links und die Variable rechts setzen. Der Grund für diesen sogenannten Yoda-Stil ist die Vermeidung von Fehlern. Fehlt in C ein Gleichheitszeichen, so ist if(worldExpoShanghaiCostInUSD = 58000000000L) als Zuweisung compilierbar (wenn auch mittlerweile mit einer Warnung), if(58000000000L = worldExpoShanghaiCostInUSD) aber nicht. Die erste fehlerhafte Version initialisiert eine Variable und springt immer in die if-Anweisung, da in C jeder Ausdruck (hier von der Zuweisung, die ja ein Ausdruck ist) ungleich 0 als wahr interpretiert wird. Das ist ein logischer Fehler, den die zweite Schreibweise verhindert, denn sie führt zu einem Compilerfehler. In Java ist dieser Fehlertyp nicht zu finden – es sei denn, der Variablentyp ist boolean, was sehr selten vorkommt –, und daher sollte diese Yoda-Schreibweise vermieden werden.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.7    Logische Operatoren: Nicht, Und, Oder, XOR 

Die Abarbeitung von Programmcode ist oft an Bedingungen geknüpft. Diese Bedingungen sind oftmals komplex zusammengesetzt, wobei drei Operatoren am häufigsten vorkommen:

  • Nicht (Negation): Dreht die Aussage um; aus wahr wird falsch, und aus falsch wird wahr.

  • Und (Konjunktion): Beide Aussagen müssen wahr sein, damit die Gesamtaussage wahr wird.

  • Oder (Disjunktion): Eine der beiden Aussagen muss wahr sein, damit die Gesamtaussage wahr wird.

Mehr als diese drei logischen Operatoren sind auch nicht nötig, um alle möglichen logischen Verknüpfungen zu realisieren. In der Mathematik wird das boolesche Algebra genannt.

Mit logischen Operatoren werden Wahrheitswerte nach definierten Mustern verknüpft. Logische Operatoren operieren nur auf boolean-Typen, andere Typen führen zu Compilerfehlern. Java bietet die Operatoren Nicht (!), Und (&&), Oder (||) und XOR (^) an. XOR ist eine Operation, die nur dann wahr liefert, wenn genau einer der beiden Operanden true ist. Sind beide Operanden gleich (also entweder true oder false), so ist das Ergebnis false. XOR heißt auch exklusives oder ausschließendes Oder. Im Deutschen trifft die Formulierung »entweder … oder« diesen Sachverhalt gut: Entweder ist es das eine oder das andere, aber nicht beides zusammen. Beispiel: »Willst du entweder ins Kino oder DVD schauen?« a ^ b ist eine Abkürzung für (a && !b) || (!a && b).

boolean a

boolean b

! a

a && b

a || b

a ^ b

true

true

false

true

true

false

true

false

false

false

true

true

false

true

true

false

true

true

false

false

true

false

false

false

Tabelle 2.10    Verknüpfungen der logischen Operatoren Nicht, Und, Oder und XOR

Die logischen Operatoren arbeiten immer auf dem Typ boolean. In Abschnitt 21.1.1, »Die Bit-Operatoren Komplement, Und, Oder und XOR«, werden wir sehen, dass sich die gleichen Verknüpfungen auf jedem Bit einer Ganzzahl durchführen lassen.

Verknüpfungen dieser Art sind in der Aussagenlogik bzw. booleschen Algebra sehr wichtig. Die für uns gängigen Begriffe Und, Oder, XOR sind dort auch unter anderen Namen bekannt. Die Und-Verknüpfung nennt sich Konjunktion, die Oder-Verknüpfung Disjunktion, und das exklusive Oder heißt Kontravalenz. Die drei binären Operatoren Und, Oder, XOR decken bestimmte Verknüpfungen ab, jedoch nicht alle, die prinzipiell möglich sind. In der Aussagenlogik gibt es außerdem die Implikation (Wenn-dann-Verknüpfung) und die Äquivalenz.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.8    Kurzschluss-Operatoren 

Ein logischer Ausdruck muss nur dann weiter ausgewertet werden, wenn sich das Endergebnis noch ändern kann. Steht das Ergebnis schon vor der Auswertung aller Teile unumstößlich fest, kürzt der Compiler den Programmfluss ab. Die beiden Operatoren && (Und) bzw. || (Oder) bieten sich zur Optimierung der Ausdrücke an:

  • Und: Ist einer der beiden Ausdrücke falsch, so kann der Ausdruck schon nicht mehr wahr werden. Das Ergebnis ist falsch.

  • Oder: Ist mindestens einer der Ausdrücke schon wahr, so ist auch der gesamte Ausdruck wahr.

Nehmen wir zum Beispiel true || Math.random() > 0.5; hier wird es nicht zum Aufruf der Methode kommen, denn die beiden Operatoren && und || sind Kurzschluss-Operatoren (engl. short-circuit operators). Das Kennzeichen der Kurzschluss-Operatoren ist also eine Abkürzung, wenn das Ergebnis des Ausdrucks feststeht; die restlichen Ausdrücke werden nicht ausgewertet.

Nicht-Kurzschluss-Operatoren *

In einigen Fällen ist es erwünscht, dass die Laufzeitumgebung alle Teilausdrücke auswertet. Das kann der Fall sein, wenn Methoden Nebenwirkungen haben sollen, etwa Zustände ändern. Daher bietet Java zusätzlich die nicht über einen Kurzschluss arbeitenden Operatoren | und & an, die eine Auswertung aller Teilausdrücke erzwingen. Das Ergebnis der Auswertung ist das Gleiche wie vorher.

Die Arbeitsweise dokumentiert das folgende Programm, bei dem gut abzulesen ist, dass eine Variable nur dann erhöht wird, wenn der Nicht-Kurzschluss-Operator auswertet:

Listing 2.13    src/main/java/CircuitNotCircuitOperator.java, main()

int a = 0, b = 0, c = 0, d = 0;

System.out.println( true || a++ == 0 ); // true, a wird nicht erhöht

System.out.println( a ); // 0

System.out.println( true | b++ == 0 ); // true, b wird erhöht

System.out.println( b ); // 1

System.out.println( false && c++ == 0 ); // false, c wird nicht erhöht

System.out.println( c ); // 0

System.out.println( false & d++ == 0 ); // false, d wird erhöht

System.out.println( d ); // 1

Für XOR kann es keinen Kurzschluss-Operator geben, da immer beide Operanden ausgewertet werden müssen, bevor das Ergebnis feststeht.

Unter gewissen Voraussetzungen kann der Verzicht auf den Kurzschlussoperator performanter sein, weil der Compiler Verzweigungen und Sprünge einsparen kann.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.9    Der Rang der Operatoren in der Auswertungsreihenfolge 

Aus der Schule ist der Spruch »Punktrechnung geht vor Strichrechnung« bekannt, sodass sich der Ausdruck 1 + 2 × 3 zu 7 und nicht zu 9 auswertet.

Auch wenn bei Ausdrücken wie a() + b() * c() zuerst das Produkt gebildet wird, schreibt doch die Auswertungsreihenfolge von binären Operatoren vor, dass der linke Operand zuerst ausgewertet werden muss, was bedeutet, dass Java zuerst die Methode a() aufruft.

Neben Plus und Mal gibt es eine Vielzahl von Operatoren, die alle ihre eigenen Vorrangregeln besitzen. Der Multiplikationsoperator besitzt zum Beispiel eine höhere Priorität und damit eine andere Auswertungsreihenfolge als der Plus-Operator.

Die Rangordnung der Operatoren (engl. operator precedence) ist in Tabelle 2.11 aufgeführt, wobei auf den Lambda-Pfeil -> verzichtet wird. Der arithmetische Typ steht für Ganz- und Fließkommazahlen, der integrale Typ für char und Ganzzahlen und der Eintrag »primitiv« für jegliche primitiven Datentypen (also auch boolean):

Operator

Rang

Typ

Beschreibung

++, --

1

arithmetisch

Inkrement und Dekrement

+, -

1

arithmetisch

unäres Plus und Minus

~

1

integral

bitweises Komplement

!

1

boolean

logisches Komplement

(Typ)

1

jeder

Cast

*, /, %

2

arithmetisch

Multiplikation, Division, Rest

+, -

3

arithmetisch

binärer Operator für Addition und Subtraktion

+

3

String

String-Konkatenation

<<

4

integral

Verschiebung nach links

>>

4

integral

Rechtsverschiebung mit Vorzeichenerweiterung

>>>

4

integral

Rechtsverschiebung ohne Vorzeichenerweiterung

<, <=, >, >=

5

arithmetisch

numerische Vergleiche

instanceof

5

Objekt

Typvergleich

==, !=

6

primitiv

Gleich-/Ungleichheit von Werten

==, !=

6

Objekt

Gleich-/Ungleichheit von Referenzen

&

7

integral

bitweises Und

&

7

boolean

logisches Und

^

8

integral

bitweises XOR

^

8

boolean

logisches XOR

|

9

integral

bitweises Oder

|

9

boolean

logisches Oder

&&

10

boolean

logisches konditionales Und, Kurzschluss

||

11

boolean

logisches konditionales Oder, Kurzschluss

?:

12

jeder

Bedingungsoperator

=

13

jeder

Zuweisung

*=, /=, %=, +=,

=, <<=, >>=, >>>=,

&=, ^=, |=

14

arithmetisch

Zuweisung mit Operation

+=

14

String

Zuweisung mit String-Konkatenation

Tabelle 2.11    Operatoren mit Rangordnung in Java (Operatorrangfolge)

Die Rechenregel für »Mal vor Plus« kann sich jeder noch leicht merken. Auch ist leicht zu merken, dass die typischen arithmetischen Operatoren wie Plus und Mal eine höhere Priorität als Vergleichsoperationen haben. Komplizierter ist die Auswertung bei den zahlreichen Operatoren, die seltener im Programm vorkommen.

Wie ist die Auswertung bei dem nächsten Ausdruck?

boolean A = false,

B = false,

C = true;

System.out.println( A && B || C );

Das Ergebnis könnte je nach Rangordnung true oder false sein. Doch die Tabelle lehrt uns, dass im Beispiel das Und stärker als das Oder bindet, also der Ausdruck als (A && B) || C und nicht als A && (B || C) gelesen wird und somit zu true ausgewertet wird.

Vermutlich gibt es Programmierer, die dies wissen oder eine Tabelle mit Rangordnungen am Monitor kleben haben. Aber beim Durchlesen von fremdem Code ist es nicht schön, immer wieder die Tabelle konsultieren zu müssen, die verrät, ob nun das binäre XOR oder das binäre Und stärker bindet.

Alle Ausdrücke, die über die einfache Regel »Punktrechnung geht vor Strichrechnung« hinausgehen, sollten geklammert werden. Da die unären Operatoren ebenfalls sehr stark binden, kann eine Klammerung in ihrem Fall wegfallen.

Links- und Rechtsassoziativität *

Bei den Operatoren + und * gilt die mathematische Kommutativität und Assoziativität. Das heißt, die Operanden können prinzipiell umgestellt werden, und das Ergebnis sollte davon nicht beeinträchtigt sein. Bei der Division unterscheiden wir zusätzlich Links- und Rechtsassoziativität. Deutlich wird das am Beispiel A / B / C. Den Ausdruck wertet Java von links nach rechts aus, und zwar als (A / B) / C; daher ist der Divisionsoperator linksassoziativ. Hier sind Klammern angemessen. Denn würde der Compiler den Ausdruck zu A / (B / C) auswerten, käme dies einem A * C / B gleich. In Java sind die meisten Operatoren linksassoziativ, aber es gibt Ausnahmen, wie Zuweisungen der Art A = B = C, die der Compiler zu A = (B = C) auswertet.

Die mathematische Assoziativität ist natürlich gefährdet, wenn durch Überläufe oder Nichtdarstellbarkeit Rechenfehler mit im Spiel sind:

float a = -16777217F;

float b = 16777216F;

float c = 1F;

System.out.println( a + b + c ); // 1.0

System.out.println( a + (b + c) ); // 0.0

Mathematisch ergibt –16.777.217 + 16.777.216 den Wert –1, und –1 plus +1 ist 0. Im zweiten Fall liefert –16.777.217 + (16.777.216 + 1) = –16.777.217 + 16.777.217 = 0. Doch Java wertet a + b durch die Beschränkung von float zu 0 aus, sodass bei 0 plus c die Ausgabe 1 statt 0 erscheint.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.10    Die Typumwandlung (das Casting) 

Java eine statisch typisierte Sprache, aber sie ist nicht so stark typisiert, dass es hinderlich ist. So übersetzt der Compiler die folgenden Zeilen problemlos:

int anInt = 1;

long long1 = 1;

long long2 = anInt;

Streng genommen könnte ein Compiler bei einer sehr starken Typisierung die letzten beiden Zeilen ablehnen, denn das Literal 1 ist vom Typ int und kein 1L, also long, und in long2 = anInt ist die Variable anInt vom Typ int statt vom gewünschten Datentyp long.

Arten der Typumwandlung

In der Praxis kommt es also vor, dass Datentypen konvertiert werden müssen. Dies nennt sich Typumwandlung (engl. typecast, kurz cast). Java unterscheidet zwei Arten der Typumwandlung:

  • Automatische (implizite) Typumwandlung: Daten eines kleineren Datentyps werden automatisch (implizit) dem größeren angepasst. Der Compiler nimmt diese Anpassung selbstständig vor. Daher funktioniert unser erstes Beispiel mit etwa long2 = anInt.

  • Explizite Typumwandlung: Ein größerer Typ kann einem kleineren Typ mit möglichem Verlust von Informationen zugewiesen werden.

Typumwandlungen gibt es bei primitiven Datentypen und bei Referenztypen. Während die folgenden Absätze die Anpassungen bei einfachen Datentypen beschreiben, kümmert sich Kapitel 6, »Eigene Klassen schreiben«, um die Typkompatibilität bei Referenzen.

Automatische Anpassung der Größe

Werte der Datentypen byte und short werden bei Rechenoperationen automatisch in den Datentyp int umgewandelt. Ist ein Operand vom Datentyp long, dann werden alle Operanden auf long erweitert. Wird aber short oder byte als Ergebnis verlangt, dann ist dieses durch einen expliziten Typecast anzugeben, und nur die niederwertigen Bits des Ergebniswerts werden übergeben. Folgende Typumwandlungen führt Java automatisch aus:

Vom Typ

In den Typ

byte

short, int, long, float, double

short

int, long, float, double

char

int, long, float, double

int

long, float, double

long

float, double

float

double

Tabelle 2.12    Implizite Typumwandlungen

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.4    Grafische Darstellung der automatischen Typumwandlung

Die Anpassung wird im Englischen widening conversion genannt, weil sie den Wertebereich automatisch erweitert. Der Typ boolean taucht nicht auf; er lässt sich in keinen anderen primitiven Typ konvertieren.

Dass ein long auf ein double gebracht werden kann – das Gleiche gilt für int auf float –, ist wohl im Nachhinein als Fehler in der Sprache Java zu betrachten, denn es gehen Informationen verloren. Ein double kann die 64 Bit für Ganzzahlen nicht so »effizient« nutzen wie ein long. Ein Beispiel:

System.out.println( Long.MAX_VALUE ); // 9223372036854775807

double d = Long.MAX_VALUE;

System.out.printf( "%.0f%n", d ); // 9223372036854776000

System.out.println( (long) d ); // 9223372036854775807

Die implizite Typumwandlung sollte aber verlustfrei sein.

Obwohl von der Datentypgröße her ein char (16 Bit) zwischen byte (8 Bit) und int (32 Bit) liegt, taucht der Typ nirgends in der rechten Spalte von Tabelle 2.12 auf, da char kein Vorzeichen speichern kann, während die anderen Datentypen byte, short, int, long, float, double alle ein Vorzeichen besitzen. Daher kann so etwas wie das Folgende nicht funktionieren:

byte b = 'b';

char c = b; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Type mismatch: cannot convert from byte to char

Explizite Typumwandlung

Die explizite Anpassung engt einen Typ ein, sodass diese Operation im Englischen narrowing conversion genannt wird. Der gewünschte Typ für eine Typumwandlung wird vor den umzuwandelnden Datentyp in Klammern gesetzt. Uns muss bewusst sein, dass bei jeder expliziten Typumwandlung Informationen verloren gehen können.

Umwandlung einer Fließkommazahl in eine Ganzzahl, der gesamte Nachkommaanteil verschwindet:

int n = (int) 3.1415; // n = 3

Eine Typumwandlung hat eine sehr hohe Priorität. Daher muss der Ausdruck gegebenenfalls geklammert werden.

Die Zuweisung an n verfehlt das Ziel:

int n = (int) 1.0315 + 2.1;

int m = (int)(1.0315 + 2.1); // das ist korrekt

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.5    Passt der Typ eines Ausdrucks nicht, lässt er sich mit den Tasten (Strg)+(1) korrigieren.

Typumwandlung von Fließkommazahlen in Ganzzahlen

Bei der expliziten Typumwandlung von double und float in einen Ganzzahltyp kann es selbstverständlich zum Verlust von Genauigkeit kommen sowie zur Einschränkung des Wertebereichs. Bei der Konvertierung von Fließkommazahlen verwendet Java eine Rundung gegen null, schneidet also schlicht den Nachkommaanteil ab.

Explizite Typumwandlung einer Fließkommazahl in ein int:

System.out.println( (int) +12.34 ); // 12

System.out.println( (int) +67.89 ); // 67

System.out.println( (int) -12.34 ); // -12

System.out.println( (int) -67.89 ); // -67

int r = (int)(Math.random() * 5 ); // 0 <= r <= 4

Automatische Typumwandlung bei Berechnungen mit byte und short in int *

Eine Operation vom Typ int mit int liefert den Ergebnistyp int, und long mit long liefert ein long.

Listing 2.14    src/main/java/AutoConvert.java, main()

int int1 = 1, int2 = 2;

int int3 = int1 + int2;

long long1 = 1, long2 = 2;

long long3 = long1 + long2;

Diese Zeilen übersetzt der Compiler wie erwartet. Und so erscheint es intuitiv, dass das Gleiche auch für die Datentypen short und byte gilt. Während

short short1 = 1, short2 = 2;

byte byte1 = 1, byte2 = 2;

noch funktioniert, führt

short short3 = short1 + short2; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Type mismatch: cannot convert from int to short

byte byte3 = byte1 + byte2; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Type mismatch: cannot convert from int to byte

zu einem Compilerfehler.

Es ist nicht möglich, ohne explizite Typumwandlung zwei short- oder byte-Zahlen zu addieren. Richtig ist:

short short3 = (short)( short1 + short2 );

byte byte3 = (byte)( byte1 + byte2 );

Der Grund liegt beim Java-Compiler. Wenn Ganzzahl-Ausdrücke vom Typ kleiner int mit einem Operator verbunden werden, passt der Compiler eigenmächtig den Typ auf int an. Die Addition der beiden Zahlen im Beispiel arbeitet also nicht mit short- oder byte-Werten, sondern mit int-Werten; intern im Bytecode ist es ebenso realisiert. So führen also alle Ganzzahloperationen mit short und byte automatisch zum Ergebnistyp int. Und das führt bei der Zuweisung aus dem Beispiel zu einem Problem, denn steht auf der rechten Seite der Typ int und auf der linken Seite der kleinere Typ byte oder short, muss der Compiler einen Fehler melden. Mit der ausdrücklichen Typumwandlung erzwingen wir diese Konvertierung.

Dass der Compiler diese Anpassung vornimmt, müssen wir einfach akzeptieren. int und int bleibt int, long und long bleibt long. Wenn ein int mit einem long tanzt, wird der Ergebnistyp long. Arbeitet der Operator auf einem short oder byte, ist das Ergebnis automatisch int.

Kleine Typen wie short und byte führen oft zu Problemen. Wenn sie nicht absichtlich in großen Arrays verwendet werden und Speicherplatz nicht ein absolutes Kriterium ist, erweist sich int als die beste Wahl – auch weil Java nicht durch besonders intuitive Typkonvertierungen glänzt, wie das Beispiel mit dem unären Minus und Plus zeigt:

byte b = 0;

b = -b; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Cannot convert from int to byte

b = +b; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Cannot convert from int to byte

Der Compiler meldet einen Fehler, denn der Ausdruck auf der rechten Seite wird durch den unären Operator in ein int umgewandelt, was immer für die Typen byte, short und char gilt.

Keine Typumwandlung zwischen einfachen Typen und Referenztypen

Allgemeine Umwandlungen zwischen einfachen Typen und Referenztypen gibt es nicht. Falsch sind zum Beispiel:

Listing 2.15    src/main/java/TypecastPrimRef.java, Ausschnitt main()

String s = (String) 1; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Cannot cast from int to String

int i = (int) "1"; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Cannot cast from String to int

Einiges sieht dagegen nach Typumwandlung aus, ist aber in Wirklichkeit eine Technik, die sich Autoboxing nennt (Abschnitt 10.5, »Wrapper-Klassen und Autoboxing«, geht näher darauf ein):

Listing 2.16    src/main/java/TypecastPrimRef.java, Ausschnitt main()

Long lông = (Long) 2L; // Alternativ: Long lông = 2L;

System.out.println( (Boolean) true );

((Integer)2).toString();

Methoden zur Typumwandlung

Bei der explizierten Typumwandlung von Ganzzahlen schneidet Java die höherwertigen Bytes ab. Bei der Konvertierung von long (8 Byte) in ein int (4 Byte) fallen die oberen 4 Byte heraus. Sind diese vier oberen Byte 0x00 – wir betrachten nur die positiven Ganzzahlen –, dann gibt es keinen Verlust an Informationen. Wollen wir von long in int umwandeln, wobei aber ein Verlust von Informationen gemeldet werden soll, so kann die statische Math-Methode int toIntExact(long value) verwendet werden. Sie löst eine Ausnahme aus, wenn die Umwandlung einen Datenverlust bedeutet.

Bei der Umwandlung von Fließkommazahlen in Ganzzahlen kommt es immer zum Abschneiden der Nachkommastellen. Die Klasse Math hat Methoden, die auch runden können. Dazu zählen Math.round(float) und long round(double); Kapitel 21, »Bits und Bytes, Mathematisches und Geld«, gibt detaillierte Auskunft über die Mathe-Klasse.

Typumwandlung beim Verbundoperator *

Beim Verbundoperator wird noch etwas mehr gemacht, als E1 #= E2 zu E1 = (E1) # (E2) aufzulösen, wobei # symbolisch für einen binären Operator steht. Interessanterweise kommt auch noch der Typ von E1 ins Spiel, denn der Ausdruck E1 # E2 wird vor der Zuweisung auf den Datentyp von E1 gebracht, sodass es genau heißen muss: E1 #= E2 wird zu E1 = (Typ von E1)((E1) # (E2)).

Der Verbundoperator soll eine Ganzzahl zu einer Fließkommazahl addieren:

Die Anwendung des Verbundoperators ist in Ordnung, denn der Übersetzer nimmt eine implizite Typumwandlung vor, sodass die Bedeutung bei i = (int)(i + 30.2) liegt. So viel dazu, dass Java eine intuitive und einfache Programmiersprache sein soll.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.11    Überladenes Plus für Strings 

Obwohl sich in Java die Operatoren fast alle auf primitive Datentypen beziehen, gibt es doch eine weitere Verwendung des Plus-Operators. Diese wurde in Java eingeführt, da ein Aneinanderhängen von Zeichenketten oft benötigt wird. Objekte vom Typ String können durch den Plus-Operator mit anderen Strings und Datentypen verbunden werden. Falls zusammenhängende Teile nicht alle den Datentyp String annehmen, werden sie automatisch in einen String umgewandelt. Der Ergebnistyp ist immer String.

Setze fünf Teile zu einem String zusammen:

String s = '"' + "Extrem Sandmännchen" + '"' + " frei ab " + 18;

// char String char String int

System.out.println( s ); // "Extrem Sandmännchen" frei ab 18

Die String-Konkatenation erfolgt strikt von links nach rechts und ist natürlich nicht kommutativ wie die numerische Addition. Besteht der Ausdruck aus mehreren Teilen, so muss die Auswertungsreihenfolge beachtet werden, andernfalls kommt es zu seltsamen Zusammensetzungen. So ergibt "Aufruf von " + 1 + 0 + 0 + " Ökonomen" tatsächlich »Aufruf von 100 Ökonomen« und nicht »Aufruf von 1 Ökonomen«, da der Compiler die Konvertierung in Strings dann startet, wenn er einen Ausdruck als String-Objekt erkannt hat.

Schauen wir uns die Auswertungsreihenfolge des Plus an einem Beispiel an:

Listing 2.17    src/main/java/PlusString.java, main()

System.out.println( 1 + 2 ); // 3

System.out.println( "1" + 2 + 3 ); // 123

System.out.println( 1 + 2 + "3" ); // 33

System.out.println( 1 + 2 + "3" + 4 + 5 ); // 3345

System.out.println( 1 + 2 + "3" + (4 + 5) ); // 339

System.out.println( 1 + 2 + "3" + (4 + 5) + 6 ); // 3396

Der Plus-Operator für Zeichenketten geht streng von links nach rechts vor und bereitet mit eingebetteten arithmetischen Ausdrücken mitunter Probleme. Eine Klammerung hilft, wie im Folgenden zu sehen ist:

"Ist 1 größer als 2? " + (1 > 2 ? "ja" : "nein");

Wäre der Ausdruck um den Bedingungsoperator nicht geklammert, dann würde der Plus-Operator an die Zeichenkette die 1 anhängen, und es käme der >-Operator. Der erwartet aber kompatible Datentypen, die in unserem Fall – links stünde die Zeichenkette und rechts die Ganzzahl 2 – nicht gegeben sind.

char-Zeichen in der Konkatenation

Nur eine Zeichenkette in doppelten Anführungszeichen ist ein String, und der Plus-Operator entfaltet seine besondere Wirkung. Ein einzelnes Zeichen in einfachen Hochkommata konvertiert Java nach den Regeln der Typumwandlung bei Berechnungen in ein int, und Additionen sind Ganzzahl-Additionen.

System.out.println( '0' + 2 ); // 50, denn der ASCII-Wert von '0' ist 48

System.out.println( 'A' + 'a' ); // 162, denn 'A'=65, 'a'=97

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.4.12    Operator vermisst * 

Einige Programmiersprachen haben einen Potenz-Operator (etwa **), den es in Java nicht gibt. Da es in Java keine Pointer-Operationen gibt, existieren die unter C(++) bekannten Operatorzeichen zur Referenzierung (&) und Dereferenzierung (*) nicht. Ebenso ist ein sizeof unnötig, da das Laufzeitsystem und der Compiler immer die Größe von Klassen kennen bzw. die primitiven Datentypen immer eine feste Länge haben.

Skriptsprachen wie Perl oder Python bieten nicht nur einfache Datentypen, sondern definieren zum Beispiel Listen oder Assoziativspeicher. Damit sind automatisch Operatoren assoziiert, etwa um die Datenstrukturen nach Werten zu fragen oder Elemente einzufügen. Zudem erlauben viele Skriptsprachen das Prüfen von Zeichenketten gegen reguläre Ausdrücke, etwa Perl mit den Operatoren =~ und !~.

Beim Testen von Referenzen auf Identität gibt es in Java den Operator ==. Einige Programmiersprachen bieten zusätzlich einen Operator ===, sodass mit dem einen Operator ein Test auf Gleichheit und mit dem anderen ein Test auf Identität möglich ist. Wir werden uns in Kapitel 3, »Klassen und Objekte«, mit der Identität und dem ==-Operator näher beschäftigen.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 8

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Kontrollstrukturen dienen in einer Programmiersprache dazu, Programmteile unter bestimmten Bedingungen auszuführen. Java bietet zum Ausführen verschiedener Programmteile eine if- und if-else-Anweisung sowie die switch-Anweisung. Neben der Verzweigung dienen Schleifen dazu, Programmteile mehrmals auszuführen. Bedeutend im Wort »Kontrollstrukturen« ist der Teil »Struktur«, denn die Struktur zeigt sich schon durch das bloße Hinsehen. Als es noch keine Schleifen und »hochwertigen« Kontrollstrukturen gab, sondern nur ein Wenn/Dann und einen Sprung, war die Logik des Programms nicht offensichtlich; das Resultat nannte sich Spaghetti-Code. Obwohl ein allgemeiner Sprung in Java mit goto nicht möglich ist, besitzt die Sprache dennoch eine spezielle Sprungvariante. In Schleifen erlauben continue und break definierte Sprungziele.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.5.1    Verzweigung mit der if-Anweisung 

Die if-Anweisung besteht aus dem Schlüsselwort if, dem zwingend ein Ausdruck mit dem Typ boolean in Klammern folgt. Es folgt eine Anweisung, die oft eine Blockanweisung ist.

Ein Beispiel: Mit der if-Anweisung wollen wir testen, ob der Anwender eine Zufallszahl richtig geraten hat:

Listing 2.18    src/main/java/WhatsYourNumber.java

public class WhatsYourNumber {

public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);

System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();

if ( number == guess ) {

System.out.println( "Super getippt!" );

}

System.out.println( "Starte das Programm noch einmal und rate erneut!" );

}

}

Die Abarbeitung der Ausgabeanweisungen hängt vom Ausdruck im if ab.

  • Ist das Ergebnis des Ausdrucks wahr (number == guess wird zu true ausgewertet), wird die folgende Anweisung, also die Konsolenausgabe mit "Super getippt!", ausgeführt.

  • Ist das Ergebnis des Ausdrucks falsch (number == guess wird zu false ausgewertet), so wird die Anweisung übersprungen, und es wird mit der ersten Anweisung nach der if-Anweisung fortgefahren.

In Java muss der Testausdruck für die Bedingung der if-Anweisung immer vom Typ boolean sein – für Schleifenbedingungen gilt das Gleiche. Andere Programmiersprachen wie JavaScript, C(++) oder PHP bewerten einen numerischen Ausdruck als wahr, wenn das Ergebnis des Ausdrucks ungleich 0 ist. So ist in ihnen auch if (10) gültig, was in Java einem if (true) entspräche. In PHP oder JavaScript sind auch der leere String oder ein leeres Array »falsy«.

if-Abfragen und Blöcke

Hinter dem if und der Bedingung erwartet der Compiler eine Anweisung. Ein {}-Block ist eine besondere Anweisung, und die nutzt das Beispiel. Ist nur eine Anweisung in Abhängigkeit von der Bedingung auszuführen, kann die Anweisung direkt ohne Block gesetzt werden. Folgende Varianten sind also identisch:

Explizite Klammerung der Anweisung

Ohne Klammerung der Anweisung

if ( number == guess ) {

System.out.println( "Super!" );

}

if ( number == guess )

System.out.println( "Super!" );

Ein Programmfehler entsteht, wenn der {}-Block fehlt, aber mehrere Anweisungen in Abhängigkeit von der Bedingung ausgewertet werden sollen. Dazu ein Beispiel: Eine if-Anweisung soll testen, ob die geratene Zahl gleich der Zufallszahl war, und dann, wenn das der Fall war, die Variable number mit einer neuen Zufallszahl belegen und eine Ausgabe liefern. Zunächst betrachten wir die semantisch falsche Variante:

if ( number == guess )

number = (int)(Math.random()*5 + 1); System.out.println( "Super getippt!" );

Die Implementierung ist semantisch falsch, da unabhängig vom Test immer die Ausgabe erscheint. Der Compiler ordnet nur die nächstfolgende Anweisung der Fallunterscheidung zu, auch wenn die Optik etwas anderes suggeriert. Dies ist eine große Gefahr für Programmierer, die optisch Zusammenhänge schaffen wollen, die in Wirklichkeit nicht existieren. Der Compiler interpretiert die Anweisungen in folgendem Zusammenhang, wobei die Einrückung die tatsächliche Ausführung widerspiegelt:

if ( number == guess )

number = (int) (Math.random() * 5 + 1);

System.out.println( "Super getippt!" );

Für unsere gewünschte Logik, beide Anweisungen zusammen in Abhängigkeit von der Bedingung auszuführen, heißt es, sie in einen Block zu setzen:

if ( number == guess ) {

number = (int) (Math.random() * 5 + 1);

System.out.println( "Super getippt!" );

}

Grundsätzlich Anweisungen in Blöcke zu setzen – auch wenn nur eine Anweisung im Block steht – ist also nicht verkehrt.

Einrückungen ändern nicht die Semantik des Programms! Einschübe können das Verständnis nur empfindlich stören. Damit das Programm korrekt wird, müssen wir einen Block verwenden und die Anweisungen zusammensetzen. Entwickler sollten Einrückungen konsistent zur Verdeutlichung von Abhängigkeiten nutzen. Es sollte zudem immer nur eine Anweisung in einer Zeile stehen.

Zusammengesetzte Bedingungen

Die bisherigen Abfragen waren sehr einfach, doch kommen in der Praxis viel komplexere Bedingungen vor. Oft im Einsatz sind die logischen Operatoren && (Und), || (Oder), ! (Nicht).

Wenn wir etwa testen wollen, ob

  • eine geratene Zahl number entweder gleich der Zufallszahl guess ist oder

  • eine gewisse Anzahl von Versuchen schon überschritten ist (trials größer 10),

dann schreiben wir die zusammengesetzte Bedingung so:

if ( number == guess || trials > 10 )

Sind die logisch verknüpften Ausdrücke komplexer, so sollten zur Unterstützung der Lesbarkeit die einzelnen Bedingungen in Klammern gesetzt werden, da nicht jeder sofort die Tabelle mit den Vorrangregeln für die Operatoren im Kopf hat.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.5.2    Die Alternative mit einer if-else-Anweisung wählen 

Ist die Bedingung erfüllt, wird der if-Zweig durchlaufen. Doch wie lässt es sich programmieren, dass im umgekehrten Fall – die Bedingung ist nicht wahr – auch Programmcode ausgeführt wird?

Eine Lösung ist, eine zweite if-Anweisung mit negierter Bedingung zu schreiben:

if ( number == guess )

System.out.println( "Super getippt!" );

if ( number != guess )

System.out.printf( "Tja, stimmt nicht, habe mir %s gedacht!", number );

Allerdings gibt es eine bessere Lösung: Neben der einseitigen Alternative existiert die zweiseitige Alternative. Das optionale Schlüsselwort else mit angehängter Anweisung veranlasst die Ausführung einer Alternative, wenn der if-Test falsch ist.

Rät der Benutzer aus unserem kleinen Spiel die Zahl nicht, wollen wir ihm die Zufallszahl präsentieren:

Listing 2.19    src/main/java/GuessTheNumber.java

public class GuessTheNumber {

public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);

System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();

if ( number == guess )

System.out.println( "Super getippt!" );

else

System.out.printf( "Tja, stimmt nicht, habe mir %s gedacht!", number );

}

}

Falls der Ausdruck number == guess wahr ist, wird die erste Anweisung ausgeführt, andernfalls die zweite Anweisung. Somit ist sichergestellt, dass in jedem Fall eine Anweisung ausgeführt wird.

Das Dangling-else-Problem

Bei Verzweigungen mit else gibt es ein bekanntes Problem, das Dangling-else-Problem genannt wird. Zu welcher Anweisung gehört das folgende else?

if ( Ausdruck1 )

if ( Ausdruck2 )

Anweisung1;

else

Anweisung2;

Die Einrückung suggeriert, dass das else die Alternative zur ersten if-Anweisung ist. Dies ist aber nicht richtig. Die Semantik von Java (und auch fast aller anderen Programmiersprachen) ist so definiert, dass das else zum innersten if gehört. Daher lässt sich nur der Programmiertipp geben, die if-Anweisungen zu klammern:

if ( Ausdruck1 ) {

if ( Ausdruck2 ) {

Anweisung1;

}}

else {

Anweisung2;

}

So kann eine Verwechslung gar nicht erst aufkommen. Wenn das else immer zum innersten if gehört und das nicht erwünscht ist, können wir, wie gerade gezeigt, mit geschweiften Klammern arbeiten oder auch eine leere Anweisung im else-Zweig hinzufügen:

if ( x >= 0 )

if ( x != 0 )

System.out.println( "x echt größer null" );

else

; // x ist gleich null

else

System.out.println( "x echt kleiner null" );

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.6    Die Tastenkombination (Strg) + Leertaste hinter dem if bietet an, eine if-Anweisung mit Block anzulegen.

Das böse Semikolon

An dieser Stelle ist ein Hinweis angebracht: Ein Programmieranfänger schreibt gerne hinter die schließende Klammer der if-Anweisung ein Semikolon. Das führt zu einer ganz anderen Ausführungsreihenfolge. Ein Beispiel:

int age = 29;

if ( age < 0 ) ; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
logischer Fehler

System.out.println( "Aha, noch im Mutterleib" );

if ( age > 150 ) ; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
logischer Fehler

System.out.println( "Aha, ein neuer Moses" );

Das Semikolon führt dazu, dass die leere Anweisung in Abhängigkeit von der Bedingung ausgeführt wird und unabhängig vom Inhalt der Variablen age immer die Ausgabe »Aha, noch im Mutterleib« und »Aha, ein neuer Moses« erzeugt. Das ist sicherlich nicht beabsichtigt. Das Beispiel soll ein warnender Hinweis sein, in jeder Zeile nur eine Anweisung zu schreiben – und die leere Anweisung durch das Semikolon ist eine Anweisung.

Folgen hinter einer if-Anweisung zwei Anweisungen, die durch keine Blockanweisung zusammengefasst sind, dann wird die eine folgende else-Anweisung als Fehler bemängelt, da der zugehörige if-Zweig fehlt. Der Grund ist, dass der if-Zweig nach der ersten Anweisung ohne else zu Ende ist:

int age = 29;

if ( age < 0 )

;

System.out.println( "Aha, noch im Mutterleib" );

else if ( age > 150 ) ; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compiler-Fehlermeldung: 'else' without 'if'

System.out.println( "Aha, ein neuer Moses" );

Mehrfachverzweigung bzw. geschachtelte Alternativen

if-Anweisungen zur Programmführung kommen sehr häufig in Programmen vor, und noch häufiger werden sie genutzt, um eine Variable auf einen bestimmten Wert zu prüfen. Dazu werden if- und if-else-Anweisungen gerne verschachtelt (kaskadiert). Wenn eine Variable einem Wert entspricht, dann wird eine Anweisung ausgeführt, sonst wird die Variable mit einem anderen Wert getestet usw.

Kaskadierte if-Anweisungen sollen uns helfen, die Variable days passend zu dem Monat (vorbelegte Variable month) und der Information, ob das Jahr ein Schaltjahr ist (vorbelegte boolean-Variable isLeapYear), zu belegen:

int month = 2; boolean isLeapYear = false; int days;

if ( month == 4 )

days = 30;

else if ( month == 6 )

days = 30;

else if ( month == 9 )

days = 30;

else if ( month == 11 )

days = 30;

else if ( month == 2 )

if ( isLeapYear ) // Sonderbehandlung im Fall eines Schaltjahrs

days = 29;

else

days = 28;

else

days = 31;

In dem kleinen Programm ist semantisch eingerückt, eigentlich würden die Anweisungen bei jedem else immer weiter nach rechts wandern.

Die eingerückten Verzweigungen nennen sich auch angehäufte if-Anweisungen oder if-Kaskade, da jede else-Anweisung ihrerseits weitere if-Anweisungen enthält, bis alle Abfragen gestellt sind.

Angewendet auf unser Zahlenratespiel, wollen wir dem Benutzer einen Tipp geben, ob seine eingegebene Zahl kleiner oder größer als die zu ratende Zahl war:

Listing 2.20    src/main/java/GuessTheNumber2.java

public class GuessTheNumber2 {

public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);

System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();

if ( number == guess )

System.out.println( "Super getippt!" );

else if ( number > guess )

System.out.println( "Nee, meine Zahl ist größer als deine!" );

else // number < guess

System.out.println( "Nee, meine Zahl ist kleiner als deine!" );

}

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.5.3    Der Bedingungsoperator 

In Java gibt es einen einzigen Operator, der drei Operanden benutzt: den Bedingungsoperator, auch Konditionaloperator genannt. Er erlaubt es, den Wert eines Ausdrucks von einer Bedingung abhängig zu machen, ohne dass dazu eine if-Anweisung verwendet werden muss. Die Operanden sind durch ? und : voneinander getrennt. Die allgemeine Syntax ist:

Bedingung ? Ausdruck, wenn Bedingung wahr : Ausdruck, wenn Bedingung falsch

Die Bestimmung des Maximums ist eine schöne Anwendung des Bedingungsoperators:

Der Wert der Variablen max wird in Abhängigkeit von der Bedingung a > b gesetzt. Der Ausdruck entspricht folgender if-Anweisung:

if ( a > b ) max = a; else max = b;

Drei Ausdrücke kommen im Bedingungsoperator vor, was ihn zu einem ternären/trinären Operator – vom lateinischen ternarius (»aus drei bestehend«) – macht. Der erste Ausdruck (in unserem Fall der Vergleich a > b) muss vom Typ boolean sein. Ist die Bedingung erfüllt, dann erhält die Variable den Wert des zweiten Ausdrucks, andernfalls wird der Variablen max der Wert des dritten Ausdrucks zugewiesen.

Der Bedingungsoperator kann eingesetzt werden, wenn der zweite und dritte Operand ein numerischer Typ, boolescher Typ oder Referenztyp ist. Der Aufruf von Methoden, die void zurückgeben, ist nicht gestattet. Es ist also keine kompakte Syntax, einfach zwei beliebige Methoden abhängig von einer Bedingung ohne if aufzurufen, denn es gibt beim Bedingungsoperator immer ein Ergebnis. Mit diesem können wir alles Mögliche machen, es etwa direkt ausgeben.

Gib das Maximum von a und b direkt aus:

System.out.println( ( a > b ) ? a : b );

Das wäre mit if-else nur mit temporären Variablen möglich oder eben mit zwei println(…)-Anweisungen.

Beispiele

  • Das Maximum oder Minimum zweier Zahlen liefern die Ausdrücke a > b ? a : b bzw. a < b ? a : b.

  • Den Absolutwert einer Zahl liefert x >= 0 ? x : -x.

  • Ein Ausdruck soll eine Zahl n, die zwischen 0 und 15 liegt, in eine Hexadezimalzahl konvertieren: (char)((n < 10) ? ('0' + n) : ('a' – 10 + n )).

Geschachtelte Anwendung des Bedingungsoperators*

Die Anwendung des Bedingungsoperators führt schnell zu schlecht lesbaren Programmen. Er sollte daher vorsichtig eingesetzt werden. In C(++) führt die unbeabsichtigte Mehrfachauswertung in Makros zu schwer auffindbaren Fehlern. Gut, dass uns das in Java nicht passieren kann! Durch ausreichende Klammerung muss sichergestellt werden, dass die Ausdrücke auch in der beabsichtigten Reihenfolge ausgewertet werden. Im Gegensatz zu den meisten Operatoren ist der Bedingungsoperator rechtsassoziativ (die Zuweisung ist ebenfalls rechtsassoziativ).

Der Ausdruck

ist demnach gleichbedeutend mit:

b1 ? a1 : ( b2 ? a2 : a3 )

Wollen wir einen Ausdruck schreiben, der für eine Zahl n abhängig vom Vorzeichen –1, 0 oder 1 liefert, lösen wir das Problem mit einem geschachtelten Bedingungsoperator:

int sign = (n < 0) ? -1 : (n > 0) ? 1 : 0;

Ein Umbruch hinter dem Doppelpunkt bietet sich an und macht die geschachtelten Bedingungssausdrücke deutlicher:

int sign = (n < 0) ? -1 :

(n > 0) ? 1 :

0;

Der Bedingungsoperator ist kein L-Value *

Der Bedingungsoperator liefert als Ergebnis einen Ausdruck zurück, der auf der rechten Seite einer Zuweisung verwendet werden kann. Da er rechts vorkommt, nennt er sich auch R‐Value. Er lässt sich nicht derart auf der linken Seite einer Zuweisung einsetzen (L-Value), dass er eine Variable auswählt, der ein Wert zugewiesen wird.

Die folgende Anwendung des Bedingungsoperators ist in Java nicht möglich:

boolean up = false, down = false;

int direction = 12;

((direction >= 0) ? up : down) = true; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

Der Bedingungsoperator kann jedoch eine Referenz auswählen, und dann ist ein Methodenaufruf gültig.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.5.4    Die switch-Anweisung bietet die Alternative 

Eine Kurzform für speziell gebaute, angehäufte if-Anweisungen bietet switch. Im switch-Block gibt es eine Reihe von unterschiedlichen Sprungzielen, die mit case markiert sind. Die switch-Anweisung erlaubt die Auswahl von:

  • Ganzzahlen

  • Wrapper-Typen (Mehr dazu folgt in Kapitel 10, »Besondere Typen der Java SE«.)

  • Aufzählungen (enum)

  • Strings

Im Bytecode gibt es nur eine switch-Variante für Ganzzahlen. Bei Strings, Aufzählungen und Wrapper-Objekten wendet der Compiler Tricks an, um sie auf Ganzzahl-switch-Konstruktionen zu reduzieren.

switch bei Ganzzahlen (und somit auch chars)

Ein einfacher Taschenrechner für vier binäre Operatoren ist mit switch schnell implementiert (und wir nutzen die Methode charAt(0), mit der wir von der String-Eingabe auf das erste Zeichen zugreifen, um ein char zu bekommen):

Listing 2.21    src/main/java/Calculator.java

public class Calculator {

public static void main( String[] args ) {

double x = new java.util.Scanner( System.in ).nextDouble();

char operator = new java.util.Scanner( System.in ).nextLine().charAt( 0 );

double y = new java.util.Scanner( System.in ).nextDouble();

switch ( operator ) {

case '+':

System.out.println( x + y );

break;

case '-':

System.out.println( x - y );

break;

case '*':

System.out.println( x * y );

break;

case '/':

System.out.println( x / y );

break;

}

}

}

Die Laufzeitumgebung sucht eine bei case genannte Sprungmarke (auch Sprungziel genannt) – eine Konstante –, die mit dem in switch angegebenen Ausdruck übereinstimmt. Gibt es einen Treffer, so werden alle auf das case folgenden Anweisungen ausgeführt, bis ein (optionales) break die Abarbeitung beendet. (Ohne break geht die Ausführung im nächsten case-Block automatisch weiter; mehr zu diesem »It’s not a bug, it’s a feature!« folgt im übernächsten Abschnitt »switch hat ohne Unterbrechung Durchfall«.) Stimmt keine Konstante eines case-Blocks mit dem switch-Ausdruck überein, werden erst einmal keine Anweisungen im switch-Block ausgeführt. Die case-Konstanten müssen unterschiedlich sein, andernfalls gibt es einen Compilerfehler.

Die switch-Anweisung hat einige Einschränkungen:

  • Die JVM kann switch nur auf Ausdrücken vom Datentyp int ausführen. Elemente der Datentypen byte, char und short sind somit erlaubt, da der Compiler den Typ automatisch an int anpasst. Ebenso sind die Aufzählungen und die Wrapper-Objekte Character, Byte, Short, Integer möglich, da Java automatisch die Werte entnimmt – mehr dazu folgt in Abschnitt 10.7, »Die Spezial-Oberklasse Enum«. Die Datentypen boolean, long, float und double können nicht benutzt werden. Zwar sind auch Aufzählungen und Strings als switch-Ausdruckstypen möglich, doch intern werden sie auf Ganzzahlen abgebildet. Allgemeine Objekte sind sonst nicht erlaubt.

  • Die hinter case aufgeführten Werte müssen konstant sein. Dynamische Ausdrücke, etwa Rückgaben aus Methodenaufrufen, sind nicht möglich.

  • Es sind keine Bereichsangaben möglich. Das wäre etwa bei Altersangaben nützlich, um zum Beispiel die Bereiche 0–18, 19–60, 61–99 zu definieren. Als Lösung bleiben nur angehäufte if-Anweisungen.

Die Angabe bei case muss konstant sein, aber kann durchaus aus einer Konstanten (finalen Variablen) kommen:

final char PLUS = '+';

switch ( operator ) {

case PLUS:

}

Alles andere mit default abdecken

Soll ein Programmteil in genau dem Fall abgearbeitet werden, in dem es keine Übereinstimmung mit irgendeiner case-Konstanten gibt, so lässt sich die besondere Sprungmarke default einsetzen. Soll zum Beispiel im Fall eines unbekannten Operators das Programm eine Fehlermeldung ausgeben, so schreiben wir:

switch ( operator ) {

case '+':

System.out.println( x + y );

break;

case '-':

System.out.println( x - y );

break;

case '*':

System.out.println( x * y );

break;

case '/':

System.out.println( x / y );

break;

default:

System.err.println( "Unbekannter Operator " + operator );

}

Der Nutzen von default besteht darin, falsch eingegebene Operatoren zu erkennen, denn die Anweisungen hinter default werden immer dann ausgeführt, wenn keine case-Konstante gleich dem switch-Ausdruck war. default kann auch zwischen den case-Blöcken auftauchen, doch das ist wenig übersichtlich und nicht für allgemeine Anwendungen zu empfehlen. Somit würde der default-Programmteil auch dann abgearbeitet, wenn ein dem default vorangehender case-Teil kein break hat. Nur ein default ist erlaubt.

Der Compiler kann natürlich nicht wissen, ob alle von uns eingesetzten Werte mit einem case-Block abgedeckt sind. Ein default kann helfen, Fehler schneller ausfindig zu machen, wenn bei switch eine Zahl ankommt, für die das Programm keine Operationen hinterlegt. Falls wir kein default einsetzen, bleibt ein unbehandelter Wert sonst ohne Folgen.

Bei Aufzählungen sieht das schon wieder anders aus, denn hier ist die Menge abzählbar. Eine Codeanalyse kann herausfinden, ob es genauso viele case-Blöcke wie Konstanten gibt. Eclipse kann das zum Beispiel melden.

switch hat ohne Unterbrechung Durchfall

Bisher haben wir in die letzte Zeile eine break-Anweisung gesetzt. Ohne ein break würden nach einer Übereinstimmung alle nachfolgenden Anweisungen ausgeführt. Sie laufen somit in einen neuen Abschnitt hinein, bis ein break oder das Ende von switch erreicht ist. Da dies vergleichbar mit einem Spielzeug ist, bei dem Kugeln von oben nach unten durchfallen, nennt sich dieses auch Fall-through. Ein häufiger Programmierfehler ist, das break zu vergessen, und daher sollte ein beabsichtigter Fall-through immer als Kommentar angegeben werden.

Über dieses Durchfallen ist es möglich, bei unterschiedlichen Werten immer die gleiche Anweisung ausführen zu lassen. Das nutzt auch das nächste Beispiel, das einen kleinen Parser für einfache Datumswerte realisiert. Der Parser soll mit drei unterschiedlichen Datumsangaben umgehen können; hier folgt je ein Beispiel:

  • "18 12": Jahr in Kurzform, Monat

  • "2018 12": Jahr, Monat

  • "12": Nur Monat; das aktuelle Jahr soll implizit gelten.

Listing 2.22    src/main/java/SimpleYearMonthParser.java

public class SimpleYearMonthParser {

@SuppressWarnings( "resource" )

public static void main( String[] args ) {

String date = "17 12";

int month = 0, year = 0;

java.util.Scanner scanner = new java.util.Scanner( date );

switch ( date.length() ) {

case 5: // YY MM

year = 2000;

// Fall-through

case 7: // YYYY MM

year += scanner.nextInt();

// Fall-through

case 2: // MM

month = scanner.nextInt();

if ( year == 0 )

year = java.time.Year.now().getValue();

break;

default :

System.err.println( "Falsches Format" );

}

System.out.println( "Monat=" + month + ", Jahr=" + year );

}

}

In dem Beispiel bestimmt eine case-Anweisung über die Länge, wie der Aufbau ist. Ist die Länge 5, so ist die Angabe des Jahres verkürzt, und wir initialisieren das Jahr mit 2000, um im folgenden Schritt mithilfe vom Scanner das Jahr einzulesen. Zu diesem Schritt wären wir auch direkt gekommen, wenn die Länge der Eingabe 7 gewesen wäre, also das Jahr vierstellig gewesen wäre. Damit ist der Jahresanteil geklärt, es bleibt, die Monate zu parsen. Kommen wir direkt über einen String der Länge 2, ist vorher kein Jahr gesetzt, wir bekommen über java.time.Year.now().getValue() das aktuelle Jahr, andernfalls überschreiben wir die Variable nicht.

Was sollte der Leser von diesem Beispiel mitnehmen? Eigentlich nur Kopfschütteln für eine schwer zu verstehende Lösung. Das Durchfallen ist eigentlich nur zur Zusammenfassung mehrerer case-Blöcke sinnvoll zu verwenden.

Obwohl ein fehlendes break zu lästigen Programmierfehlern führt, haben die Entwickler von Java dieses Verhalten vom syntaktischen Vorgänger C übernommen. Eine interessante andere Lösung wäre gewesen, das Verhalten genau umzudrehen und das Durchfallen explizit einzufordern, zum Beispiel mit einem Schlüsselwort. Dazu gibt es eine interessante Entwicklung: Java übernimmt diese Eigenschaft von C(++) und C(++) wiederum erbt den Durchfall von der Programmiersprache B. Einer der »Erfinder« von B ist Ken Thompson, der heute bei Google arbeitet und an der neuen Programmiersprache Go beteiligt ist. In Go müssen Entwickler ausdrücklich die fallthrough-Anweisung verwenden, wenn ein case-Block zum nächsten weiterleiten soll. Das Gleiche gilt für die neue Programmiersprache Swift; auch hier gibt es die Anweisung fallthrough. Selbst in C++ gibt es seit dem Standard C++17 das Standardattribut [[fallthrough]], das den Compiler anweist, bei einem Durchfallen keine Warnung anzuzeigen.

Stack-Case-Labels

Stehen mehrere case-Blöcke untereinander, um damit Bereiche abzubilden, nennt sich das auch Stack-Case-Labels. Nehmen wir an, eine Variable hour steht für eine Stunde am Tag, und wir wollen herausfinden, ob Mittagsruhe, Nachtruhe oder Arbeitszeit ist:

Listing 2.23    src/main/java/RestOrWork.java, Ausschnitt

int hour = 12;

switch ( hour ) {

// Nachtruhe von 22 Uhr bis 6 Uhr

case 22:

case 23:

case 24: case 0:

case 1:

case 2:

case 3:

case 4:

case 5:

System.out.println( "Nachtruhe" );

break;

// Mittagsruhe von 13 bis 15 Uhr

case 13:

case 14:

System.out.println( "Mittagsruhe" );

break;

default :

System.out.println( "Arbeiten" );

break;

}

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.7    Die Tastenkombination (Strg) + Leertaste nach dem switch bietet an, ein Grundgerüst für eine switch-Fallunterscheidung anzulegen.

switch auf Strings

Neben der Möglichkeit, ein switch bei Ganzzahlen zu verwenden, ist eine switch-Anweisung auf String-Objekten möglich:

Listing 2.24    src/main/java/SweetsLover.java, main()

String input = javax.swing.JOptionPane.showInputDialog( "Eingabe" );

switch ( input.toLowerCase() ) {

case "kekse":

System.out.println( "Ich mag Keeeekse" );

break;

case "kuchen":

System.out.println( "Ich mag Kuchen" );

break;

case "schokolade": // Fällt durch

case "lakritze":

System.out.println( "Hm. Lecker" );

break;

default:

System.out.printf( "Kann man %s essen?%n", input );

}

Obwohl direkte Zeichenkettenvergleiche möglich sind, fallen Überprüfungen auf reguläre Ausdrücke leider heraus, die insbesondere Skriptsprachen wie Ruby oder Perl anbieten.

Wie auch beim switch mit Ganzzahlen können die Zeichenketten beim String-case-Zweig aus finalen (also nicht änderbaren) Variablen stammen. Ist etwa String KEKSE = "kekse"; vordefiniert, ist case KEKSE erlaubt.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.5.5    Switch-Ausdrücke 

Java hat seit Version 1.0 eine switch-Anweisung zum Kontrollfluss. Im Wesentlichen basiert die Syntax auf der Programmiersprache C, die aus den 1970er-Jahre stammt. In Java 12 wurde eine neue Syntax probeweise eingeführt, in Java 13 wurde sie verändert und in Java 14 endgültig integriert.

Insgesamt kann switch in vier Formen auftauchen:

Anweisung/Ausdruck

Ab Java-Version

Syntax

Durchfall

vollständige Abdeckung

Anweisung

1.0

:

Ja

Nein

Anweisung

14

->

Nein

Nein

Ausdruck

14

:

Ja

Ja

Ausdruck

14

->

Nein

Ja

Tabelle 2.13    Vier Typen von »switch«

Den ersten Typ haben wir schon ausgiebig betrachtet, schauen wir uns die weiteren Varianten an.

Vereinfachte Switch-Anweisung, kein Durchfall, keine vollständige Abdeckung

Bei der vereinfachten switch-Anweisung steht hinter dem Label bzw. default kein Doppelpunkt, sondern ein ->. Dieser hat nichts mit Lambda-Ausdrücken zu tun, auch wenn die Symbole gleich sind. Hinter dem Pfeil steht entweder ein Ausdruck, ein Block in geschweiften Klammern oder eine throw-Anweisung, die eine Ausnahme auslöst. Implizit beendet ein break jeden Zweig, es gibt also kein Durchfallen mehr.

String operator = "+";

switch ( operator ) {

case "+" -> System.out.println( "Plus" );

case "-" -> { String minus = "Minus"; System.out.println( minus ); }

}

Dadurch, dass bei mehreren Anweisungen immer Blöcke gesetzt werden müssen, tritt eine lokale Variable auch nicht aus dem Bereich aus.

Ein default kann gesetzt werden, muss aber nicht. Das switch muss nicht jede Möglichkeit abdecken, was bei Zahlen und Strings eh nicht funktioniert.

String operator = "+";

switch ( operator ) {

case "+" -> System.out.println( "Plus" );

case "-" -> System.out.println( "Minus" );

default -> System.out.println( "Unbekannter Operator" );

}

Bei vereinfachten switch-Anweisungen sind mehrere Labels möglich, die die gleiche Behandlung haben. Kommas trennen die Labels.

String operator = "+";

switch ( operator ) {

case "+" -> System.out.println( "Plus" );

case "*", "×" -> System.out.println( "Mal" );

}

Switch-Ausdrücke, kein Durchfall, vollständige Abdeckung

Traditionell finden sich die Fallunterscheidungen mit switch als Anweisung, und Anweisungen geben nichts zurück. In Java 14 ist es möglich, switch als Ausdruck mit Ergebnis zu nutzen.

String operator = "+";

String writtenOperator = (switch ( operator ) {

case "+" -> "Plus";

case "-" -> "Minus";

default -> "Unbekannter Operator";

} ).toUpperCase();

System.out.println( writtenOperator );

Ausdrücke müssen immer Ergebnisse liefern, und folglich muss switch immer einen Pfad auf einen Wert nehmen. Der übliche Fall ist default wie gezeigt, es gibt allerdings Sonderfälle, wie bei Aufzählungen, wo der Compiler prüfen kann, dass alle Möglichkeiten abgedeckt sind.

DayOfWeek today = LocalDate.now().getDayOfWeek();

System.out.println( switch ( today ) {

case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> "Arbeit";

case SATURDAY, SUNDAY -> "Party";

} );

Falls rechts neben dem Pfeil kein einfacher Ausdruck steht, sondern ein Block, muss auch dieser Block ein Ergebnis zurückgeben. Dafür wird das neue Schlüsselwort yield eingesetzt, hinter dem ein Ausdruck kommt.

String operator = "+";

System.out.println( switch ( operator ) {

case "+" -> "Plus";

case "-" -> { String minus = "Minus"; yield minus; }

default -> throw new IllegalArgumentException( "Unknown operator" );

} );

Ein Block muss ein yield besitzen oder eine ungeprüfte Ausnahme auslösen.

Switch-Expression mit :-Syntax, mit Durchfall, vollständige Abdeckung

Auch die Doppelpunkt-Syntax lässt sich als Ausdruck einsetzen. Mit ihr ist auch ein Durchfall wieder möglich; ein yield ist zwingend, oder eine ungeprüfte Ausnahme muss ausgelöst werden. Die Syntax birgt mit dem Durchfallen eine Fehlerquelle, sodass diese Variante vielleicht die schlechteste ist.

String operator = "+";

System.out.println( switch ( operator ) {

case "+" : yield "Plus";

case "*" : System.out.println( "Sternchen" );

case "×" : yield "Mal";

default : throw new IllegalArgumentException( "Unknown operator" );

} );

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 9

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Schleifen dienen dazu, bestimmte Anweisungen immer wieder abzuarbeiten. Zu einer Schleife gehören die Schleifenbedingung und der Rumpf. Die Schleifenbedingung, ein boolescher Ausdruck, entscheidet darüber, unter welcher Bedingung die Wiederholung ausgeführt wird. Abhängig von der Schleifenbedingung kann der Rumpf mehrmals ausgeführt werden. Dazu wird bei jedem Schleifendurchgang die Schleifenbedingung geprüft. Das Ergebnis entscheidet, ob der Rumpf ein weiteres Mal durchlaufen (true) oder die Schleife beendet wird (false). Java bietet vier Typen von Schleifen:

Schleifentyp

Syntax

while-Schleife

while ( Bedingung ) Anweisung

do-while-Schleife

do Anweisung while ( Bedingung );

einfache for-Schleife

for ( Initialisierung; Bedingung; Fortschaltung )

  Anweisung

erweiterte for-Schleife (auch for-each-Loop genannt)

for ( Variablentyp variable : Sammlung )

  Anweisung

Tabelle 2.14    Die vier Schleifentypen in Java

Die ersten drei Schleifentypen erklären die folgenden Abschnitte, während die erweiterte for-Schleife nur bei Sammlungen nötig ist und daher später bei Arrays (siehe Kapitel 4, »Arrays und ihre Anwendungen«) und dynamischen Datenstrukturen (siehe Kapitel 17, »Einführung in Datenstrukturen und Algorithmen«) Erwähnung findet.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.6.1    Die while-Schleife 

Die while-Schleife ist eine abweisende Schleife, die vor jedem Schleifeneintritt die Schleifenbedingung prüft. Ist die Bedingung wahr, führt sie den Rumpf aus, andernfalls beendet sie die Schleife. Wie bei if muss auch bei den while-Schleifen der Typ der Bedingung boolean sein.

Vor jedem Schleifendurchgang wird der Ausdruck neu ausgewertet, und ist das Ergebnis true, so wird der Rumpf ausgeführt. Die Schleife ist beendet, wenn das Ergebnis false ist. Ist die Bedingung schon vor dem ersten Eintritt in den Rumpf nicht wahr, so wird der Rumpf erst gar nicht durchlaufen.

Zähle von 100 bis 40 in Zehnerschritten herunter:

Listing 2.25    src/main/java/WhileLoop.java, main()

int cnt = 100;

while ( cnt >= 40 ) {

System.out.printf( "Ich erblickte das Licht der Welt " +

"in Form einer %d-Watt-Glühbirne.%n", cnt );

cnt -= 10;

}

Wird innerhalb des Schleifenkopfs schon alles Interessante erledigt, so muss trotzdem eine Anweisung folgen. Dies ist der passende Einsatz für die leere Anweisung ; oder den leeren Block {}.

while ( Files.notExists( Paths.get( "dump.bin" ) ) )

;

Existiert die Datei nicht, liefert notExists(…) die Rückgabe true, die Schleife läuft weiter, und es folgt sofort ein neuer Existenztest. Existiert die Datei, ist die Rückgabe false, und dies läutet das Ende der Schleife ein. Ein Tipp an dieser Stelle: Anstatt direkt zum nächsten Dateitest überzugehen, sollte eine kurze Verzögerung eingebaut werden.

Endlosschleifen

Ist die Bedingung einer while-Schleife immer wahr, dann handelt es sich um eine Endlosschleife. Die Konsequenz ist, dass die Schleife endlos wiederholt wird:

Listing 2.26    src/main/java/WhileTrue.java

public class WhileTrue {

public static void main( String[] args ) {

while ( true ) {

// immer wieder und immer wieder

}

}

}

Endlosschleifen bedeuten normalerweise das Aus für jedes Programm. Doch es gibt Hilfe! Aus dieser Endlosschleife können wir mittels break entkommen; das schauen wir uns in Abschnitt 2.6.5, »Schleifenabbruch mit break und zurück zum Test mit continue«, genauer an. Genau genommen beenden aber auch nicht abgefangene Exceptions oder auch System.exit(int) die Programme.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.8    Die Ansicht »Console« mit der roten Schaltfläche zum Beenden von Programmen

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  In Eclipse lassen sich Programme von außen beenden. Dazu bietet die Ansicht Console eine rote Schaltfläche in Form eines Quadrats, die nach der Aktivierung jedes laufende Programm beendet.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.6.2    Die do-while-Schleife 

Dieser Schleifentyp ist eine annehmende Schleife, da do-while die Schleifenbedingung erst nach jedem Schleifendurchgang prüft. Bevor es zum ersten Test kommt, ist der Rumpf schon einmal durchlaufen worden. Der Schleifentyp hilft uns bei unserem Zahlenratespiel perfekt, denn es gibt ja mindestens einen Durchlauf mit einer Eingabe, und nur dann, wenn der Benutzer eine falsche Zahl eingibt, soll der Rumpf wiederholt werden.

Listing 2.27    src/main/java/TheFinalGuess.java

public class TheFinalGuess {

public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);

int guess;

do {

System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

guess = new java.util.Scanner( System.in ).nextInt();

if ( number == guess )

System.out.println( "Super getippt!" );

else if ( number > guess )

System.out.println( "Nee, meine Zahl ist größer als deine!" );

else // number < guess

System.out.println( "Nee, meine Zahl ist kleiner als deine!" );

}

while ( number != guess );

}

}

Es ist wichtig, auf das Semikolon hinter der while-Anweisung zu achten. Liefert die Bedingung ein true, so wird der Rumpf erneut ausgeführt. Andernfalls wird die Schleife beendet, und das Programm wird mit der nächsten Anweisung nach der Schleife fortgesetzt. Interessant ist das Detail, dass wir die Variable guess nun außerhalb des do-while-Blocks deklarieren müssen, da eine im Schleifenblock deklarierte Variable für den Wiederholungstest in while nicht sichtbar ist. Auch weiß der Compiler, dass der do-while-Block mindestens einmal durchlaufen wird und guess auf jeden Fall initialisiert wird; der Zugriff auf nicht initialisierte Variablen ist verboten und wird vom Compiler als Fehler angesehen.

Äquivalenz einer while- und einer do-while-Schleife *

Die do-while-Schleife wird seltener gebraucht als die while-Schleife. Dennoch lassen sich beide ineinander überführen. Zunächst der erste Fall: Wir ersetzen eine while-Schleife durch eine do-while-Schleife:

while ( Ausdruck )

Anweisung

Führen wir uns noch einmal vor Augen, was hier passiert: In Abhängigkeit vom Ausdruck wird der Rumpf ausgeführt. Da zunächst ein Test kommt, wäre die do-while-Schleife schon eine Blockausführung weiter. So fragen wir in einem ersten Schritt mit einer if-Anweisung ab, ob die Bedingung wahr ist oder nicht. Wenn ja, dann lassen wir den Programmcode in einer do-while-Schleife abarbeiten.

Die äquivalente do-while-Schleife sieht wie folgt aus:

if ( Ausdruck )

do

Anweisung

while ( Ausdruck ) ;

Nun der zweite Fall: Wir ersetzen die do-while-Schleife durch eine while-Schleife:

do

Anweisung

while ( Ausdruck ) ;

Da zunächst die Anweisungen ausgeführt werden und anschließend der Test, schreiben wir für die while-Variante die Ausdrücke einfach vor den Test. So ist sichergestellt, dass diese zumindest einmal abgearbeitet werden:

Anweisung

while ( Ausdruck )

Anweisung

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.6.3    Die for-Schleife 

Die for-Schleife ist eine spezielle Variante einer while-Schleife und wird typischerweise zum Zählen benutzt. Genauso wie while-Schleifen sind for-Schleifen abweisend: Der Rumpf wird erst dann ausgeführt, wenn die Bedingung wahr ist.

Gib die Zahlen von 1 bis 10 auf dem Bildschirm aus:

Listing 2.28    src/main/java/ForLoop.java, main()

for ( int i = 1; i <= 10; i++ ) // i ist Schleifenzähler

System.out.println( i );

Eine genauere Betrachtung der Schleife zeigt die unterschiedlichen Segmente:

  • Initialisierung der Schleife: Der erste Teil der for-Schleife ist ein Ausdruck wie i = 1, der vor der Durchführung der Schleife genau einmal ausgeführt wird. Der Ausdruck initialisiert die Variable i, das Ergebnis wird dann aber verworfen. Tritt in der Auswertung ein Fehler auf, so wird die Abarbeitung unterbrochen, und die Schleife kann nicht vollständig ausgeführt werden. Der erste Teil kann lokale Variablen deklarieren und initialisieren. Diese Zählvariable ist dann außerhalb des Blocks nicht mehr gültig. Es darf keine lokale Variable mit dem gleichen Namen geben.

  • Schleifentest/Schleifenbedingung: Der mittlere Teil, wie i <= 10, wird vor dem Durchlaufen des Schleifenrumpfs – also vor jedem Schleifeneintritt – getestet. Ergibt der Ausdruck false, wird die Schleife nicht bzw. kein weiteres Mal durchlaufen und beendet. Das Ergebnis muss, wie bei einer while-Schleife, vom Typ boolean sein. Ist kein Test angegeben, so ist das Ergebnis automatisch true.

  • Schleifen-Inkrement durch einen Fortschaltausdruck: Der letzte Teil, wie i++, wird immer am Ende jedes Schleifendurchlaufs, aber noch vor dem nächsten Schleifeneintritt ausgeführt. Das Ergebnis wird nicht weiter verwendet. Ergibt die Bedingung des Tests true, dann befindet sich beim nächsten Betreten des Rumpfs der veränderte Wert im Rumpf.

Betrachten wir das Beispiel, so ist die Auswertungsreihenfolge folgender Art:

  1. Initialisiere i mit 1.

  2. Teste, ob i <= 10 gilt.

  3. Ergibt sich true, dann führe den Block aus, sonst ist es das Ende der Schleife.

  4. Erhöhe i um 1.

  5. Gehe zu Schritt 2.

Schleifenzähler

Wird die for-Schleife zum Durchlaufen einer Variablen genutzt, so heißt der Schleifenzähler entweder Zählvariable oder Laufvariable.

Wichtig sind die Initialisierung und die korrekte Abfrage am Ende. Schnell läuft die Schleife einmal zu oft durch und führt so zu falschen Ergebnissen. Die Fehler bei der Abfrage werden auch Off-by-one-Errors genannt, wenn zum Beispiel statt <= der Operator < steht. Dann nämlich läuft die Schleife nur bis 9. Ein anderer Name für den Schleifenfehler lautet Fencepost-Error (»Zaunpfahl-Fehler«). Es geht um die Frage, wie viele Pfähle für einen 100 m langen Zaun nötig sind, sodass alle Pfähle einen Abstand von 10 m haben: 9, 10 oder 11?

Wann for- und wann while-Schleife?

Da sich die while- und die for-Schleife sehr ähnlich sind, ist die Frage berechtigt, wann die eine und wann die andere zu nutzen ist. Leider verführt die kompakte for-Schleife sehr schnell zu einer Überladung. Manche Programmierer packen gerne alles in den Schleifenkopf hinein, und der Rumpf besteht nur aus einer leeren Anweisung. Dies ist ein schlechter Stil und sollte vermieden werden.

for-Schleifen sollten immer dann benutzt werden, wenn eine Variable um eine konstante Größe erhöht wird. Tritt in der Schleife keine Schleifenvariable auf, die inkrementiert oder dekrementiert wird, sollte eine while-Schleife genutzt werden. Eine do-while-Schleife sollte dann eingesetzt werden, wenn die Abbruchbedingung erst am Ende eines Schleifendurchlaufs ausgewertet werden kann. Auch sollte die for-Schleife dort eingesetzt werden, wo sich alle drei Ausdrücke im Schleifenkopf auf dieselbe Variable beziehen. Vermieden werden sollten unzusammenhängende Ausdrücke im Schleifenkopf. Der schreibende Zugriff auf die Schleifenvariable im Rumpf ist eine schlechte Idee, wenn sie auch gleichzeitig im Kopf modifiziert wird – das ist schwer zu durchschauen und kann leicht zu Endlosschleifen führen.

Die for-Schleife ist nicht auf einen bestimmten Typ festgelegt, auch wenn for-Schleifen für das Hochzählen den impliziten Typ int suggerieren. Der Initialisierungsteil kann alles Mögliche vorbelegen, ob int, double oder eine Referenzvariable. Die Bedingung kann alles Erdenkliche testen, nur das Ergebnis muss hier ein boolean sein.

Eine for-Schleife muss keine Zählschleife sein

Die for-Schleife zeigt kompakt im Kopf alle wesentlichen Informationen, ist aber nicht auf das Hochzählen von Werten beschränkt. Sie ist vielmehr dann eine gute Option, wenn es eine Variable gibt, deren Zustand in jeder Iteration verändert wird, und wenn der Abbruch irgendwie abhängig von der Variablen ist.

Eine Endlosschleife mit for

Da alle drei Ausdrücke im Kopf der Schleife optional sind, können sie weggelassen werden, und es ergibt sich eine Endlosschleife. Diese Schreibweise ist somit mit while(true) semantisch äquivalent:

Die trennenden Semikola dürfen nicht verschwinden. Falls in der for-Schleife keine Schleifenbedingung angegeben ist, ist der Ausdruck immer wahr. Es folgt keine Initialisierung und keine Auswertung des Fortschaltausdrucks.

Geschachtelte Schleifen

Schleifen, und das gilt insbesondere für for-Schleifen, können verschachtelt werden. Syntaktisch ist das auch logisch, da sich innerhalb des Schleifenrumpfs beliebige Anweisungen aufhalten dürfen. Um fünf Zeilen von Sternchen auszugeben, wobei in jeder Zeile immer ein Stern mehr erscheinen soll, schreiben wir:

Listing 2.29    src/main/java/Superstar.java, main()

for ( int i = 1; i <= 5; i++ ) {

for ( int j = 1; j <= i; j++ )

System.out.print( '*' );

System.out.println();

}

Als besonderes Element ist die Abhängigkeit des Schleifenzählers j von i zu werten. Hier folgt die Ausgabe:

Die übergeordnete Schleife nennt sich äußere Schleife, die untergeordnete innere Schleife. In unserem Beispiel zählt die äußere Schleife mit i die Zeilen, und die innere Schleife gibt die Sternchen in eine Zeile aus, ist also für die Spalte verantwortlich.

Da Schleifen beliebig tief verschachtelt werden können, muss besonderes Augenmerk auf die Laufzeit gelegt werden. Die inneren Schleifen werden mit ihren Durchläufen immer so oft ausgeführt, wie die äußere Schleife durchlaufen wird.

for-Schleifen und mit Komma Ausdrucksanweisungen hintereinandersetzen *

Im ersten und letzten Teil einer for-Schleife lässt sich ein Komma einsetzen, um mehrere Ausdrucksanweisungen hintereinanderzusetzen. Damit lassen sich entweder mehrere Variablen gleichen Typs deklarieren – wie wir es schon kennen – oder mehrere Ausdrücke nebeneinanderschreiben, aber keine beliebigen Anweisungen oder sogar andere Schleifen.

Mit den Variablen i und j können wir auf diese Weise eine kleine Multiplikationstabelle aufbauen:

for ( int i = 1, j = 9; i <= j; i++, j-- )

System.out.printf( "%d * %d = %d%n", i, j, i*j );

Dann ist die Ausgabe:

1 * 9 = 92 * 8 = 163 * 7 = 214 * 6 = 24

5 * 5 = 25

Ein weiteres Beispiel mit komplexerer Bedingung wäre das folgende, das vor dem Schleifendurchlauf den Startwert für die Variablen x und y initialisiert, dann x und y heraufsetzt und die Schleife so lange ausführt, bis x und y beide 10 sind:

int x, y;

for ( x = initX(), y = initY(), x++, y++;

x < 10 || y < 10;

x += xinc(), y += yinc() )

{

// …

}

Komplizierte for-Schleifen werden dadurch lesbarer, dass die drei for-Teile in getrennten Zeilen stehen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.6.4    Schleifenbedingungen und Vergleiche mit == * 

Eine Schleifenabbruchbedingung kann ganz unterschiedlich aussehen. Beim Zählen ist es häufig der Vergleich auf einen Endwert. Oft steht an dieser Stelle ein absoluter Vergleich mit ==, der aus zwei Gründen problematisch werden kann.

Das Programm zählt bis 10, oder?

int input = new java.util.Scanner( System.in ).nextInt();

for ( int i = input; i != 11; i++ )

System.out.println( i );

Ist der Wert der Variablen i kleiner als 11, so haben wir beim Zählen kein Problem, denn dann ist anschließend spätestens bei 11 Schluss und die Schleife bricht ab. Kommt der Wert aber aus einer unbekannten Quelle und ist er echt größer als 11, so ist die Bedingung ebenso wahr und der Schleifenrumpf wird ziemlich lange durchlaufen – genau genommen so weit, bis wir durch einen Überlauf wieder bei 0 beginnen und dann auch bei 11 und dem Abbruch landen. Die Absicht war sicherlich eine andere. Die Schleife sollte nur so lange zählen, wie i kleiner 11 ist, und nicht einfach nur ungleich 11. Daher passt Folgendes besser:

int input = new java.util.Scanner( System.in ).nextInt();

for ( int i = input; i < 11; i++ ) // Zählt immer nur bis 10 oder gar nicht

System.out.println( i );

Jetzt rennt der Interpreter bei Zahlen größer 11 nicht endlos weiter, sondern stoppt die Schleife sofort ohne Durchlauf.

Rechenungenauigkeiten sind nicht des Programmierers Freund

Das zweite Problem ergibt sich bei Fließkommazahlen. Es ist sehr problematisch, echte Vergleiche zu fordern:

double d = 0.0;

while ( d != 1.0 ) { // Achtung! Problematischer Vergleich!

d += 0.1;

System.out.println( d );

}

Lassen wir das Programmsegment laufen, so sehen wir, dass die Schleife hurtig über das Ziel hinausschießt:

0.10.20.300000000000000040.4

0.5

0.60.70.79999999999999990.89999999999999990.99999999999999991.09999999999999991.2

1.3

Und das so lange, bis das Auge müde wird …

Bei Fließkommawerten bietet es sich daher immer an, mit den relationalen Operatoren <, >, <= oder >= zu arbeiten.

Eine zweite Möglichkeit neben dem echten Kleiner/Größer-Vergleich ist, eine erlaubte Abweichung (Delta) zu definieren. Mathematiker bezeichnen die Abweichung von zwei Werten mit dem griechischen Kleinbuchstaben Epsilon. Wenn wir einen Vergleich von zwei Fließkommazahlen anstreben und bei einem Gleichheitsvergleich eine Toleranz mitbetrachten wollen, so schreiben wir einfach:

if ( Math.abs(x - y) <= epsilon )

Epsilon ist die erlaubte Abweichung. Math.abs(x) berechnet von einer Zahl x den Absolutwert.

Wie Bereichsangaben schreiben? *

Für Bereichsangaben der Form a >= 23 && a <= 42 empfiehlt es sich, den unteren Wert in den Vergleich einzubeziehen, den Wert für die obere Grenze jedoch nicht (inklusive untere Grenzen und exklusive obere Grenzen). Für unser Beispiel, in dem a im Intervall bleiben soll, ist Folgendes besser: a >= 23 && a < 43. Das gilt für Fließkommazahlen wie für Ganzzahlen. Die Begründung dafür ist einleuchtend:

  • Die Größe des Intervalls ist die Differenz aus den Grenzen.

  • Ist das Intervall leer, so sind die Intervallgrenzen gleich.

  • Die linkere untere Grenze ist nie größer als die rechtere obere Grenze.

Die Standardbibliothek verwendet diese Konvention auch durchgängig, etwa im Fall von substring(…) bei String-Objekten oder subList(…) bei Listen oder bei der Angabe von Array-Indexwerten.

Die Vorschläge können für normale Schleifen mit Vergleichen übernommen werden. So ist eine Schleife mit zehn Durchgängen besser in der Form

for ( i = 0; i < 10; i++ ) // Besser

formuliert als in der semantisch äquivalenten Form:

for ( i = 0; i <= 9; i++ ) // Nicht so gut

for ( i = 1; i <= 10; i++ ) // Auch nicht so gut

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.6.5    Schleifenabbruch mit break und zurück zum Test mit continue 

Eine break-Anweisung innerhalb einer for-, while- oder do-while-Schleife beendet den Schleifendurchlauf, und die Abarbeitung wird bei der ersten Anweisung nach der Schleife fortgeführt.

Dass eine Endlosschleife mit break beendet werden kann, ist nützlich, wenn eine Bedingung eintritt, die das Ende der Schleife bestimmt. Das lässt sich prima auf unser Zahlenratespiel übertragen:

Listing 2.30    src/main/java/GuessWhat.java

public class GuessWhat {

public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);

while ( true ) {

System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();

if ( number == guess ) {

System.out.println( "Super getippt!" );

break; // Ende der Schleife

}

else if ( number > guess )

System.out.println( "Nee, meine Zahl ist größer als deine!" );

else if ( number < guess )

System.out.println( "Nee, meine Zahl ist kleiner als deine!" );

}

}

}

Die Fallunterscheidung stellt fest, ob der Benutzer noch einmal in einem weiteren Schleifendurchlauf neu raten muss oder ob der Tipp richtig war; dann beendet die break-Anweisung den Spuk.

Da ein kleines break schnell im Programmtext verschwindet, seine Bedeutung aber groß ist, sollte ein kleiner Hinweis auf diese Anweisung gesetzt werden.

Flaggen oder break

break lässt sich gut verwenden, um aus einer Schleife vorzeitig auszubrechen, ohne Flags zu benutzen. Dazu ein Beispiel dafür, was vermieden werden sollte:

boolean endFlag = false;

do {

if ( Bedingung ) {

endFlag = true;

}

} while ( AndereBedingung && ! endFlag );

Stattdessen schreiben wir:

do {

if ( Bedingung ) {

break;

}

} while ( AndereBedingung );

Die alternative Lösung stellt natürlich einen Unterschied dar, wenn nach dem if noch Anweisungen in der Schleife stehen.

Neudurchlauf mit continue

Innerhalb einer for-, while- oder do-while-Schleife lässt sich eine continue-Anweisung einsetzen, die nicht wie break die Schleife beendet, sondern zum Schleifenkopf zurückgeht. Nach dem Auswerten des Fortschaltausdrucks wird im nächsten Schritt erneut geprüft, ob die Schleife weiter durchlaufen werden soll. Ein häufiges Einsatzfeld sind Schleifen, die im Rumpf immer wieder Werte so lange holen und testen, bis diese für die Weiterverarbeitung geeignet sind.

Dazu ein Beispiel, wieder mit dem Ratespiel. Dem Benutzer wird bisher mitgeteilt, dass er nur Zahlen zwischen 1 und 5 (inklusive) eingeben soll, aber wenn er –1234567 eingibt, ist das auch egal. Das wollen wir ändern, indem wir einen Test vorschalten, der zurück zur Eingabe führt, wenn der Wertbereich falsch ist. continue hilft uns dabei, zurück zum Anfang des Blocks zu kommen, und der beginnt mit einer neuen Eingabeaufforderung.

Listing 2.31    src/main/java/GuessRight.java

public class GuessRight {

public static void main( String[] args ) {

int number = (int) (Math.random() * 5 + 1);

while ( true ) {

System.out.println( "Welche Zahl denke ich mir zwischen 1 und 5?" );

int guess = new java.util.Scanner( System.in ).nextInt();

if ( guess < 1 || guess > 5 ) {

System.out.println( "Nur Zahlen zwischen 1 und 5!" );

continue;

}

if ( number == guess ) {

System.out.println( "Super getippt!" );

break; // Ende der Schleife

}

else if ( number > guess )

System.out.println( "Nee, meine Zahl ist größer als deine!" );

else if ( number < guess )

System.out.println( "Nee, meine Zahl ist kleiner als deine!" );

} }

}

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Manche Programmstücke sind aber ohne continue lesbarer. Ein continue am Ende einer if-Abfrage kann durch einen else-Teil bedeutend klarer gefasst werden. Zunächst das schlechte Beispiel:

while ( Bedingung ) { // Durch continue verzuckert

if ( AndereBedingung ) {

// Code, Code, Code

continue;

}

// Weiterer schöner Code

}

Viel deutlicher ist:

while ( Bedingung ) {

if ( AndereBedingung ) {

// Code, Code, Code

}

else {

// Weiterer schöner Code

}

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.6.6    break und continue mit Marken * 

Obwohl das Schlüsselwort goto in der Liste der reservierten Wörter auftaucht, erlaubt Java keine beliebigen Sprünge, und goto ist ohne Funktionalität. Allerdings lassen sich in Java Anweisungen – oder ein Block, der eine besondere Anweisung ist – markieren. Ein Grund für die Einführung von Markierungen ist der, dass break bzw. continue mehrdeutig ist:

  • Wenn es zwei ineinander verschachtelte Schleifen gibt, würde ein break in der inneren Schleife nur die innere abbrechen. Was ist jedoch, wenn die äußere Schleife beendet werden soll? Das Gleiche gilt für continue, wenn die äußere Schleife fortgesetzt werden soll und nicht die innere.

  • Nicht nur Schleifen nutzen das Schlüsselwort break, sondern auch die switch-Anweisung. Was ist, wenn eine Schleife eine switch-Anweisung enthält, jedoch nicht der lokale case-Zweig mit break beendet werden soll, sondern die ganze Schleife mit break abgebrochen werden soll?

Die Sprachdesigner von Java haben sich dazu entschlossen, Markierungen einzuführen, sodass break und continue die markierte Anweisung entweder verlassen oder wieder durchlaufen können. Falsch eingesetzt, können sie natürlich zu Spaghetti-Code wie aus der Welt der unstrukturierten Programmiersprachen führen. Doch als verantwortungsvolle Java-Programmierer werden wir das Feature natürlich nicht missbrauchen.

break mit einer Marke für Schleifen

Betrachten wir ein erstes Beispiel mit einer Marke (engl. label), in dem break nicht nur aus der inneren Teufelsschleife ausbricht, sondern aus der äußeren gleich mit. Marken werden definiert, indem ein Bezeichner mit Doppelpunkt abgeschlossen und vor eine Anweisung gesetzt wird – die Anweisung wird damit markiert wie eine Schleife:

Listing 2.32    src/main/java/BreakAndContinueWithLabels.java, main()

heaven:

while ( true ) {

hell:

while ( true )

break /* continue */ heaven;

// System.out.println( "hell" );

}

System.out.println( "heaven" );

Ein break ohne Marke in der inneren while-Schleife beendet nur die innere Wiederholung, und ein continue würde zur Fortführung dieser inneren while-Schleife führen. Unser Beispiel zeigt die Anwendung einer Marke hinter den Schlüsselwörtern break und continue.

Das Beispiel benutzt die Marke hell nicht, und die Zeile mit der Ausgabe »hell« ist bewusst auskommentiert, denn sie ist nicht erreichbar und würde andernfalls zu einem Compilerfehler führen. Dass die Anweisung nicht erreichbar ist, ist klar, denn mit einem break heaven kommt das Programm nie zur nächsten Anweisung hinter der inneren Schleife, und somit ist eine Konsolenausgabe nicht erreichbar.

Setzen wir statt break heaven ein break hell in die innere Schleife, ändert sich dies:

heaven:

while ( true ) {

hell:

while ( true )

break /* continue */ hell;

System.out.println( "hell" );

}

// System.out.println( "heaven" );

In diesem Szenario ist die Ausgabe »heaven« nicht erreichbar und muss auskommentiert werden. Das break hell in der inneren Schleife wirkt wie ein einfaches break ohne Marke, und das ablaufende Programm führt laufend zu Bildschirmausgaben von »hell«.

Marken können vor allen Anweisungen (und Blöcke sind damit eingeschlossen) definiert werden; in unserem ersten Fall haben wir die Marke vor die while(true)-Schleife gesetzt. Interessanterweise kann ein break mit einer Marke nicht nur eine Schleife und case verlassen, sondern auch einen ganz einfachen Block:

label:{ …

break label;

}

Somit entspricht das break label einem goto zum Ende des Blocks.

Das break kann nicht durch continue ausgetauscht werden, da continue in jedem Fall eine Schleife braucht. Und ein normales break ohne Marke wäre im Übrigen nicht gültig und könnte nicht den Block verlassen.

Warum übersetzt der Compiler Folgendes ohne Murren?

Listing 2.33    src/main/java/WithoutComplain.java

class WithoutComplain {

static void main( String[] args ) {

http://www.tutego.de/

System.out.print( "Da gibt's Java-Tipps und -Tricks." );

}

}

Mit dem break und einer Marke aus dem switch aussteigen

Da dem break mehrere Funktionen in der Sprache Java zukommen, kommt es zu einer Mehrdeutigkeit, wenn im case-Block einer switch-Anweisung ein break eingesetzt wird.

Im folgenden Beispiel läuft eine Schleife einen String ab. Den Zugriff auf ein Zeichen im String realisiert die String-Objektmethode charAt(int); die Länge eines Strings liefert length(). Als Zeichen im String sollen C, G, A, T erlaubt sein. Für eine Statistik über die Anzahl der einzelnen Buchstaben zählt eine switch-Anweisung beim Treffer jeweils die richtige Variable c, g, a, t um 1 hoch. Falls ein falsches Zeichen im String vorkommt, wird die Schleife beendet. Und genau hier bekommt die Markierung ihren Auftritt:

Listing 2.34    src/main/java/SwitchBreak.java

public class SwitchBreak {

public static void main( String[] args ) {

String dnaBases = "CGCAGTTCTTCGGXAC";

int a = 0, g = 0, c = 0, t = 0;

loop:

for ( int i = 0; i < dnaBases.length(); i++ ) {

switch ( dnaBases.charAt( i ) ) {

case 'A': case 'a':

a++;

break;

case 'G': case 'g':

g++;

break;

case 'C': case 'c':

c++;

break;

case 'T': case 't':

t++;

break;

default:

System.err.println( "Unbekannte Nukleinbasen " + dnaBases.charAt( i ) );

break loop;

} }

System.out.printf( "Anzahl: A=%d, G=%d, C=%d, T=%d%n", a, g, c, t );

}

}

Wenn Folgendes in der main(…)-Methode stünde, würde es der Compiler übersetzen? Was wäre die Ausgabe? Achte genau auf die Leerzeichen!

int val = 2;

switch ( val ) {

case 1:

System.out.println( 1 );

case2:

System.out.println( 2 );

default:

System.out.println( 3 );

}

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 10

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

In objektorientierten Programmen interagieren zur Laufzeit Objekte miteinander und senden sich gegenseitig Nachrichten als Aufforderung, etwas zu machen. Diese Aufforderungen resultieren in einem Methodenaufruf, in dem Anweisungen stehen, die dann ausgeführt werden. Das Angebot eines Objekts, also das, was es »kann«, wird in Java durch Methoden ausgedrückt.

Wir haben schon mehrere Methoden kennengelernt und am häufigsten println(…) in unseren Beispielen eingesetzt. Sie ist eine Methode vom out-Objekt. Ein anderes Programmstück schickt nun eine Nachricht an das out-Objekt, die println(…)-Methode auszuführen. Im Folgenden werden wir den aktiven Teil des Nachrichtenversendens nicht mehr so genau betrachten, sondern wir sagen nur noch, dass eine Methode aufgerufen wird.

Für die Deklaration von Methoden gibt es drei Gründe:

  • Wiederkehrende Programmteile sollen nicht immer wieder programmiert, sondern an einer Stelle angeboten werden. Änderungen an der Funktionalität lassen sich dann leichter durchführen, wenn der Code lokal zusammengefasst ist.

  • Komplexe Programme werden in kleine Teilprogramme zerlegt, damit die Komplexität des Programms heruntergebrochen wird. Damit ist der Kontrollfluss leichter zu erkennen.

  • Die Operationen einer Klasse, also das Angebot eines Objekts, sind ein Grund für Methodendeklarationen in einer objektorientierten Programmiersprache. Daneben gibt es aber noch weitere Gründe, die für Methoden sprechen. Sie werden im Folgenden erläutert.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.1    Bestandteile einer Methode 

Eine Methode setzt sich aus mehreren Bestandteilen zusammen. Dazu gehören der Methodenkopf (kurz Kopf) und der Methodenrumpf (kurz Rumpf). Der Kopf besteht aus einem Rückgabetyp (auch Ergebnistyp genannt), dem Methodennamen und einer optionalen Parameterliste.

Nehmen wir die bekannte statische main(String[])-Methode:

public static void main( String[] args ) {

System.out.println( "Wie siehst du denn aus? Biste gerannt?" );

}

Sie hat folgende Bestandteile:

  • Die statische Methode liefert keine Rückgabe, daher ist der »Rückgabetyp« void. (An dieser Stelle sollte bemerkt werden, dass void in Java kein Typ ist.) void heißt auf Deutsch übersetzt »frei«, »die Leere« oder »Hohlraum«.

  • Der Methodenname ist main.

  • Die Parameterliste ist String[] args.

  • Der Rumpf besteht nur aus der Bildschirmausgabe.

Methodennamen beginnen wie Variablennamen mit Kleinbuchstaben und werden in der gemischten Groß-/Kleinschreibung verfasst. Bezeichner dürfen nicht wie Schlüsselwörter heißen.

Die Signatur einer Methode

Der Methodenname und die Parameterliste bestimmen die Signatur einer Methode; der Rückgabetyp und Ausnahmen gehören nicht dazu. Die Parameterliste ist durch die Anzahl, die Reihenfolge und die Typen der Parameter beschrieben. Pro Klasse darf es nur eine Methode mit derselben Signatur geben, sonst meldet der Compiler einen Fehler. Da die Methoden void main(String[] args) und String main(String[] arguments) die gleiche Signatur (main, String[]) besitzen – die Namen der Parameter spielen keine Rolle –, können sie nicht zusammen in einer Klasse deklariert werden (später werden wir sehen, dass Unterklassen durchaus gewisse Sonderfälle zulassen).

Insbesondere Skriptsprachen (wie JavaScript, Python oder Ruby) erlauben Methodendeklarationen ohne Parametertyp, sodass die Methoden mit unterschiedlichen Argumenttypen aufgerufen werden können:

printSum( a, b ) print a + b

Aufgrund des nicht bestimmten Parametertyps lässt sich die Methode mit Ganzzahlen, Fließkommazahlen oder Strings aufrufen. Es ist die Aufgabe der Laufzeitumgebung, diesen dynamischen Typ zu erkennen und die Addition auf dem konkreten Typ auszuführen. In Java ist das nicht möglich; der Typ muss dem Compiler bekannt sein. Programmiersprachen, die erst zur Laufzeit das Vorhandensein von Methoden oder Operatoren auf dem Typ prüfen, nutzen das sogenannte Duck-Typing. Der Begriff stammt aus einem Gedicht von James Whitcomb Riley, in dem es heißt: »When I see a bird that walks like a duck and swims like a duck and quacks like a duck, I call that bird a duck.« Auf unseren Fall übertragen: Wenn die Parameter a und b die Operation »addieren« unterstützen, dann sind die Werte eben addierbar.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.2    Signatur-Beschreibung in der Java-API 

In der Java-Dokumentation sind alle Methoden mit ihren Rückgaben und Parametern inklusive möglicher Ausnahmen genau definiert. Betrachten wir die Dokumentation der statischen Methode max(int, int) der Klasse Math:

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.9    Die Online-API-Dokumentation für »Math.max()«

Die Hilfe (siehe Abbildung 2.9) gibt Informationen über die komplette Signatur der Methode. Der Rückgabetyp ist ein int, die statische Methode heißt max, und sie erwartet genau zwei int-Zahlen. Verschwiegen haben wir die Schlüsselwörter public und static, die sogenannten Modifizierer. public gibt die Sichtbarkeit an und sagt, wer diese Methode nutzen kann.

  • Im Fall von public bedeutet es, dass jeder diese Methode verwenden kann. Das Gegenteil ist private: In dem Fall kann nur das Objekt selbst diese Methode nutzen. Das ist sinnvoll, wenn Methoden benutzt werden, um die Komplexität zu verkleinern und Teilprobleme zu lösen. Private Methoden werden in der Regel nicht in der Hilfe angezeigt, da sie ein Implementierungsdetail sind.

  • Das Schlüsselwort static zeigt an, dass sich die Methode mit dem Klassennamen nutzen lässt, also kein Exemplar eines Objekts nötig ist.

Weitere Modifizierer und Ausnahmen *

Es gibt Methoden, die noch andere Modifizierer und eine erweiterte Signatur besitzen. Ein weiteres Beispiel aus der API zeigt Abbildung 2.10. Die Sichtbarkeit dieser Methode ist protected. Das bedeutet: Nur abgeleitete Klassen und Klassen im gleichen Verzeichnis (Paket) können diese Methode nutzen. Ein zusätzlicher Modifizierer ist final, der in einer Vererbung der Unterklasse nicht erlaubt, die Methode zu überschreiben und ihr neuen Programmcode zu geben. Zum Schluss folgt hinter dem Schlüsselwort throws eine Ausnahme. Diese sagt etwas darüber aus, welche Fehler die Methode verursachen kann und worum sich der Programmierer kümmern muss. Im Zusammenhang mit der Vererbung werden wir noch über protected und final sprechen. Dem Ausnahmezustand widmen wir Kapitel 8, »Ausnahmen müssen sein«. Die Dokumentation zeigt mit »Since: 1.1« an, dass es die Methode seit Java 1.1 gibt. Die Information kann auch an der Klasse festgemacht sein.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.10    Ausschnitt aus der API-Dokumentation für die Klasse java.net.ServerSocket

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.3    Aufruf einer Methode 

Da eine Methode immer einer Klasse oder einem Objekt zugeordnet ist, muss der Eigentümer beim Aufruf angegeben werden. Im Fall von System.out.println() ist println() eine Methode vom out-Objekt. Wenn wir das Maximum zweier Fließkommazahlen mit dem Aufruf Math.max(a, b) bilden, dann ist max(…) eine (statische) Methode der Klasse Math. Für den Aufrufer ist damit immer ersichtlich, wer diese Methode anbietet, also auch, wer diese Nachricht entgegennimmt. Was der Aufrufer nicht sieht, ist die Arbeitsweise der Methode. Der Methodenaufruf verzweigt in den Programmcode, aber der Aufrufer weiß nicht, was dort geschieht. Er betrachtet nur das Ergebnis.

Die aufgerufene Methode wird mit ihrem Namen genannt. Die Parameterliste wird durch ein Klammerpaar umschlossen. Diese Klammern müssen auch dann gesetzt werden, wenn die Methode keine Parameter enthält. Die Anzahl und Typen der übergebenen Werte müssen zur Deklaration der Methode passen – bei Math.max(…) sind exakt zwei Übergaben nötig, und stimmt die Anzahl oder stimmen die Typen nicht, gibt es einen Compilerfehler.

Will was, will nix, gibt nix, gibt was

Eine Methode wie System.out.println() gibt nichts als Ergebnis einer »Berechnung« zurück. Anders die statischen Methoden Math.max(…) und Math.random(), sie liefern ein Ergebnis. Damit ergeben sich vier unterschiedliche Typen von Methoden:

Methode

Ohne Rückgabewert

Mit Rückgabewert

Ohne Parameter

System.out.println()

Math.random()

Mit Parameter

System.out.println(4)

Math.max(12, 33)

Tabelle 2.15    Methoden mit Rückgabewerten

Die folgenden Abschnitte gehen diese vier Fälle der Reihe nach durch:

  • Methode ohne Parameter und ohne Rückgabe

  • Methode mit Parameter und ohne Rückgabe

  • Methode ohne Parameter und mit Rückgabe

  • Methode mit Parameter und mit Rückgabe

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.4    Methoden ohne Parameter deklarieren 

Die einfachste Methode besitzt keinen Rückgabewert und keine Parameter. Der Programmcode steht in geschweiften Klammern hinter dem Kopf und bildet damit den Körper der Methode. Gibt die Methode nichts zurück, dann wird void vor den Methodennamen geschrieben. Falls die Methode etwas zurückgibt, wird der Typ der Rückgabe anstelle von void geschrieben.

Schreiben wir eine statische Methode ohne Rückgabe und Parameter, die etwas auf dem Bildschirm ausgibt:

Listing 2.35    src/main/java/FriendlyGreeter.java

class FriendlyGreeter {

static void greet() {

System.out.println( "Guten Morgen. Oh, und falls wir uns nicht mehr" +

" sehen, guten Tag, guten Abend und gute Nacht!" );

}

public static void main( String[] args ) {

greet();

}

}

Eigene Methoden können natürlich wie Bibliotheksmethoden heißen, da sie zu unterschiedlichen Klassen gehören. Statt greet() hätten wir also auch den Namen println() vergeben dürfen.

Die Vergabe eines Methodennamens ist gar nicht so einfach. Nehmen wir zum Beispiel an, wir wollen eine Methode schreiben, die eine Datei kopiert. Spontan kommen uns zwei Wörter in den Sinn, die zu einem Methodennamen verbunden werden wollen: »file« und »copy«. Doch in welcher Kombination? Soll es copyFile(…) oder fileCopy(…) heißen? Wenn dieser Konflikt entsteht, sollte das Verb die Aktion anführen, unsere Wahl also auf copyFile(…) fallen. Methodennamen sollten immer mit dem Tätigkeitswort beginnen, gefolgt vom Was, dem Objekt.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Eine gedrückte (Strg)-Taste und ein Mausklick auf einen Bezeichner lässt Eclipse zur Deklaration springen. Ein Druck auf (F3) hat den gleichen Effekt. Steht der Cursor in unserem Beispiel auf dem Methodenaufruf greet() und wird (F3) gedrückt, dann springt Eclipse zur Definition in Zeile 3 und hebt den Methodennamen hervor.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.5    Statische Methoden (Klassenmethoden) 

Bisher haben wir nur mit statischen Methoden (auch Klassenmethoden genannt) gearbeitet. Das Besondere daran ist, dass die statischen Methoden nicht an einem Objekt hängen und daher immer ohne explizit erzeugtes Objekt aufgerufen werden können. Das heißt, statische Methoden gehören zu Klassen an sich und sind nicht mit speziellen Objekten verbunden. Am Aufruf unserer statischen Methode greet() lässt sich ablesen, dass hier kein Objekt gefordert ist, mit dem die Methode verbunden ist. Das ist möglich, denn die Methode ist als static deklariert und innerhalb der Klasse lassen sich alle Methoden einfach mit ihrem Namen nutzen.

Statische Methoden müssen explizit mit dem Schlüsselwort static kenntlich gemacht werden. Fehlt der Modifizierer static, so deklarieren wir damit eine Objektmethode, die wir nur aufrufen können, wenn wir vorher ein Objekt angelegt haben. Das heben wir uns aber bis Kapitel 3, »Klassen und Objekte«, auf. Die Fehlermeldung sollte uns aber keine Angst machen. Lassen wir von der greet()-Deklaration das static weg und ruft die statische main(…)-Methode wie jetzt ohne Aufbau eines Objekts die dann nicht mehr statische Methode greet() auf, so gibt es den Compilerfehler »Cannot make a static reference to the non-static method greet() from the type FriendlyGreeter«.

Ist die statische Methode in der gleichen Klasse wie der Aufrufer deklariert – in unserem Fall main(…) und greet() in FriendlyGreeter –, so ist der Aufruf allein mit dem Namen der Methode eindeutig. Befinden sich jedoch Methodendeklaration und Methodenaufruf in unterschiedlichen Klassen, so muss der Aufrufer den Namen der Klasse nennen; wir haben so etwas schon einmal bei Aufrufen wie Math.random(…) gesehen.

class FriendlyGreeter {

static void greet() {

System.out.println( "Moin!" );

}

}

class FriendlyGreeterCaller {

public static void main( String[] args ) {

FriendlyGreeter.greet();

}

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.6    Parameter, Argument und Wertübergabe 

Einer Methode können Werte übergeben werden, die sie dann in ihre Arbeitsweise einbeziehen kann. Der Methode println(2001) ist zum Beispiel ein Wert übergeben worden. Sie wird damit zur parametrisierten Methode.

Werfen wir einen Blick auf die Methodendeklaration printMax(double, double), die den größeren der beiden übergebenen Werte auf dem Bildschirm ausgibt:

static void printMax( double a, double b ) {

if ( a > b )

System.out.println( a );

else

System.out.println( b );

}

Um die an Methoden übergebenen Werte anzusprechen, gibt es formale Parameter. Von unserer statischen Methode printMax(double a, double b) sind a und b die formalen Parameter der Parameterliste. Jeder Parameter wird durch ein Komma getrennt aufgelistet, wobei für jeden Parameter der Typ angegeben sein muss; eine Kurzform wie bei der sonst üblichen Variablendeklaration wie double a, b ist nicht möglich. Jede Parametervariable einer Methodendeklaration muss natürlich einen anderen Namen tragen, sonst gibt es keine Einschränkungen.

Argumente (aktuelle Parameter)

Der Aufrufer der Methode muss für jeden Parameter ein Argument angeben. Die im Methodenkopf deklarierten Parameter sind letztendlich lokale Variablen im Rumpf der Methode. Beim Aufruf initialisiert die Laufzeitumgebung die lokalen Variablen mit den an die Methode übergebenen Argumenten. Rufen wir unsere parametrisierte Methode etwa mit printMax(10, 20) auf, so sind die Literale 10 und 20 Argumente (aktuelle Parameter der Methode). Beim Aufruf der Methode setzt die Laufzeitumgebung die Argumente in die lokalen Variablen, kopiert also den Wert 10 in die Parametervariable a und 20 in die Parametervariable b. Innerhalb des Methodenkörpers gibt es so Zugriff auf die von außen übergebenen Werte.

Das Ende des Methodenblocks bedeutet automatisch das Ende für die Parametervariablen. Der Aufrufer weiß auch nicht, wie die internen Parametervariablen heißen. Eine Typumwandlung von int in double nimmt der Compiler in unserem Fall automatisch vor. Die Argumente müssen vom Typ her natürlich passen, und es gelten die für die Typumwandlung bekannten Regeln.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.11    Die Begriffe »Parameter« und »Argument«

Wertübergabe per Call by Value

Wenn eine Methode aufgerufen wird, dann gibt es in Java ein bestimmtes Verfahren, in dem jedes Argument einer Parametervariablen übergeben wird. Diese Technik heißt Parameterübergabe-Mechanismus. Viele Programmiersprachen verfügen über eine ganze Reihe von verwirrenden Möglichkeiten. Java kennt nur einen einfachen Mechanismus der Wertübergabe (engl. call by value, selten auch copy by value genannt). Ein Beispiel zum Methodenaufruf macht das deutlich:

int i = 2;

printMax( 10, i ); // 10 geht in a und 2 geht in b

Unsere aufgerufene Methode printMax(double a, double b) bekommt zunächst 10 in die Variable a kopiert und dann den Inhalt der Variablen i (in unserem Beispiel 2) in b. Auf keinen Fall gibt der Aufrufer Informationen über den Speicherbereich von i an die Methode mit. In dem Moment, in dem die Methode aufgerufen wird, erfragt die Laufzeitumgebung die Belegung von i und initialisiert damit die Parametervariable b. Ändert printMax(…) intern seine Variable b, so ändert dies nur die lokale Variable b (überschreibt also 2), aber die Änderung in der Methode ist für das außenstehende i ohne Konsequenz; i bleibt weiterhin bei 2. Durch diese Aufrufart kommt auch die Bezeichnung Copy by Value zustande. Lediglich der Wert wird übergeben (kopiert) und kein Verweis auf die Variable.

Auswertung der Argumentliste von links nach rechts *

Bei einem Methodenaufruf werden erst alle Argumente ausgewertet und anschließend der Methode übergeben. Dies bedeutet im Besonderen, dass Untermethoden ausgewertet und Zuweisungen gemacht werden können. Fehler führen dann zu einem Abbruch des Methodenaufrufs. Bis zum Fehler werden alle Ausdrücke ausgewertet.

Was ist die Ausgabe folgender Zeilen?

int i = 1;

System.out.println( Math.max( i++, --i ) );

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.7    Methoden vorzeitig mit return beenden 

Läuft eine Methode bis zum Ende durch, dann ist die Methode damit beendet, und es geht zurück zum Aufrufer. In Abhängigkeit von einer Bedingung kann eine Methode jedoch vor dem Ende des Ablaufs mit einer return-Anweisung beendet werden. Das ist nützlich bei Methoden, die abhängig von Parametern vorzeitig aussteigen wollen. Wir können uns vorstellen, dass vor dem Ende der Methode automatisch ein verstecktes return steht. Ein unnötiges return am Ende des Methodenrumpfs sollte nicht geschrieben werden.

Eine statische Methode printSqrt(double) soll die Wurzel einer Zahl auf dem Bildschirm ausgeben. Bei Zahlen kleiner null erscheint eine Meldung, und die Methode wird verlassen. Andernfalls wird die Wurzelberechnung durchgeführt:

Listing 2.36    src/main/java/PrintSqrt.java, printSqrt()

static void printSqrt( double d ) {

if ( d < 0 ) {

System.out.println( "Keine Wurzel aus negativen Zahlen!" );

return;

}

System.out.println( Math.sqrt( d ) );

}

Die Realisierung wäre natürlich auch mit einer else-Anweisung möglich gewesen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.8    Nicht erreichbarer Quellcode bei Methoden * 

Folgt direkt hinter einer return-Anweisung Quellcode, so ist dieser nicht erreichbar – im Sinne von nicht ausführbar. return beendet immer die Methode und kehrt zum Aufrufer zurück. Folgt nach dem return noch Quelltext, meldet der Compiler einen Fehler:

public static void main( String[] args ) {

int i = 1;

return;

i = 2; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Unreachable code!

}

Reduzieren wir eine Anweisung bis auf das Nötigste, das Semikolon, so führt dies bisweilen zu amüsanten Ergebnissen:

public static void main( String[] args ) {

;return;; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

}

Das Beispiel enthält zwei Null-Anweisungen: eine vor dem return und eine dahinter. Doch das zweite Semikolon hinter dem return ist unzulässig, da es eine nicht erreichbare Anweisung darstellt.

In manchen Fällen ist ein return in der Mitte einer Methode gewollt. Soll etwa eine Methode in der Testphase nicht komplett durchlaufen, sondern in der Mitte beendet werden, so können wir uns mit einer Anweisung wie if ( true ) return; retten.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.9    Methoden mit Rückgaben 

Damit Methoden Rückgabewerte an den Aufrufer liefern können, müssen zwei Dinge gelten:

  • Eine Methodendeklaration bekommt einen Rückgabetyp ungleich void.

  • Eine return-Anweisung gibt einen Wert zurück.

Eine statische Methode liefert Zufallszahlen von 0 bis echt kleiner 100 zurück:

static double largerRandom() {

return Math.random() * 100;

}

Fehlt der Ausdruck und ist nur ein einfaches return vorhanden, meldet der Compiler einen Programmfehler, dass ein Ausdruck nötig ist oder die Methode nichts zurückgeben darf.

Obwohl einige Programmierer den Ausdruck gerne klammern, ist das nicht nötig. Klammern sollen lediglich komplexe Ausdrücke besser lesbar machen. Geklammerte Ausdrücke erinnern sonst nur an einen Methodenaufruf, und diese Verwechslungsmöglichkeit sollte bei Rückgabewerten nicht bestehen.

Obwohl zwar mehrere Parameter deklariert werden können, kann doch nur höchstens ein Wert an den Aufrufer zurückgegeben werden. In der Programmiersprache Python lassen sich auch mehrere Werte über ein sogenanntes Tupel zurückgeben. In Java lässt sich das über eine zurückgegebene Sammlung nachbilden.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Eclipse erkennt, ob ein Rückgabetyp fehlt, und schlägt einen passenden Typ vor, wenn man (Strg)+(1) drückt.

Rückgaben abhängig von Eingaben

Statische Methoden wie Math.max(…) liefern in Abhängigkeit von den Argumenten ein Ergebnis zurück. Für den Aufrufer ist die Implementierung egal; er abstrahiert und nutzt lediglich die Methode statt eines Ausdrucks.

Eine statische Methode bildet den Mittelwert und gibt diesen zurück:

static double avg( double x, double y ) {

return (x + y) / 2;

}

Rückgaben nutzen *

Der Rückgabewert muss an der Aufrufstelle nicht zwingend benutzt werden. Berechnet unsere Methode den Durchschnitt zweier Zahlen, ist es wohl eher ein Programmierfehler, den Rückgabewert nicht zu verwenden. Häufig passiert das bei den String-Methoden, die String-Objekte nicht modifizieren, sondern neue String-Objekte zurückgeben.

Bei einer String-Methode die Rückgabe zu ignorieren, ist ein semantischer Programmfehler, aber kein syntaktischer Compilerfehler.

String s = "Ja";

System.out.println( s.concat( "va" ) ); // Java

s.concat( "va" ); // Rückgabe ignoriert, Fehler!

System.out.println( s ); // Ja

Mehrere Ausstiegspunkte mit return

Für Methoden mit Rückgabewert gilt ebenso wie für void-Methoden, dass es mehr als ein return geben kann. Aber wird irgendein return »getroffen«, ist Schluss mit der Methode, und es geht zurück zum Aufrufer.

Schauen wir uns dazu eine Methode an, die das Vorzeichen einer Zahl ermittelt und +1, 0 oder –1 zurückgibt, wenn die Zahl entweder positiv, null oder negativ ist:

static int sign( int value ) {

if ( value < 0 )

return -1;

else if ( value > 0 )

return +1;

else

return 0;

}

Bei genauer Betrachtung fällt auf, dass auf keinen Fall ein anderer Test durchgeführt werden kann, wenn ein return die Methode beendet. Der Programmcode lässt sich umschreiben, denn in if-Anweisungen mit weiteren else-if-Alternativen und Rücksprung ist die Semantik die gleiche, wenn das else-if durch ein einfaches if ersetzt wird. Eine äquivalente sign(int)-Methode wäre:

static int sign( int value ) {

if ( value < 0 )

return -1;

if ( value > 0 )

return +1;

return 0;

}

Kompakte Rümpfe bei boolean-Rückgaben

Ist die Rückgabe ein Wahrheitswert, so kann oftmals die Implementierung gekürzt werden, wenn ein Ausdruck vorher ausgewertet wird, der den Rückgabewert direkt bestimmt. Gewisse Konstruktionen lassen sich kompakter schreiben:

Konstrukt mit Fallunterscheidung

Verkürzung

if ( Bedingung )

return true;

else

return false;

if ( Bedingung )

return false;

else

return true;

return Bedingung ? true : false;

return Bedingung ? false : true;

Tabelle 2.16    Codeoptimierungen

Schummeln ohne return geht nicht

Jeder denkbare Programmfluss einer Methode mit Rückgabe muss mit einem return value; beendet werden. Der Compiler verfügt über ein scharfes Auge und merkt, wenn es einen Programmpfad gibt, der nicht zu einem return-Abschluss führt.

Die statische Methode isLastBitSet(int) soll 0 zurückgeben, wenn das letzte Bit einer Ganzzahl nicht gesetzt ist, und 1, wenn es gesetzt ist. Den Bit-Test erledigt der Und-Operator:

static int isLastBitSet( int i ) { //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

switch ( i & 1 ) {

case 0: return 0;

case 1: return 1;

}

}

Obwohl ein Bit nur gesetzt oder nicht gesetzt sein kann – dazwischen gibt es nichts –, lässt sich die Methode nicht übersetzen. Der Fehler ist: »This method must return a result of type int.«

Bei den Dingen, die für den Benutzer meistens offensichtlich sind, muss der Compiler passen, da er die Bedeutung nicht durchschaut. Ähnliches würde für eine Wochenmethode gelten, die mit einem Ganzzahl-Argument (0 bis 6) einen Wochentag als String zurückgibt. Wenn wir die Fälle 0 = Montag bis 6 = Sonntag beachten, dann kann in unseren Augen ein Wochentag nicht 99 sein. Der Compiler kennt aber die Methode nicht und weiß nicht, dass der Wertebereich beschränkt ist. Das Problem ließe sich mit einem default leicht beheben.

Die statische Methode posOrNeg(double) soll eine Zeichenkette mit der Information liefern, ob die übergebene Fließkommazahl positiv oder negativ ist:

static String posOrNeg( double d ) { //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

if ( d >= 0 )

return "pos";

if ( d < 0 )

return "neg";

}

Überraschenderweise ist dieser Programmcode ebenfalls fehlerhaft. Denn obwohl er offensichtlich für positive oder negative Zahlen den passenden String zurückgibt, gibt es einen Fall, den diese Methode nicht abdeckt. Wieder gilt, dass der Compiler nicht erkennen kann, dass der zweite Ausdruck eine Negation des ersten sein soll. Es gibt aber noch einen zweiten Grund, der damit zu tun hat, dass es in Java spezielle Werte gibt, die keine Zahlen sind, denn die Zahl d kann auch ein NaN (Not a Number) als Quadratwurzel aus einer negativen Zahl sein. Diesen speziellen Wert überprüft posOrNeg(double) nicht. Als Lösung für den einfachen Fall ohne NaN reicht es, aus dem zweiten if und der Abfrage einfach ein else zu machen oder die Anweisung auch gleich wegzulassen bzw. mit dem Bedingungsoperator im Methodenrumpf kompakt zu schreiben: return d >= 0 ? "pos" : "neg";.

Methoden, die einen Fehlerwert wie 1 zurückliefern, sind häufig so implementiert, dass am Ende immer automatisch der Fehlerwert zurückgeliefert und dann in der Mitte die Methode bei passendem Ende verlassen wird.

Fallunterscheidungen mit Ausschlussprinzip *

Eine Methode between(a, x, b) soll testen, ob ein Wert x zwischen a (untere Schranke) und b (obere Schranke) liegt. Bei Methoden dieser Art ist es immer sehr wichtig, darauf zu achten und es zu dokumentieren, ob der Test auf echt kleiner (<) oder kleiner gleich (<=) durchgeführt werden soll. Wir wollen hier auch die Gleichheit betrachten.

Für die Implementierung gibt es zwei Lösungen, wobei die meisten Programmierer zur ersten Lösung neigen. Die erste Lösungsidee zeigt sich in einer mathematischen Gleichung. Wir möchten gerne a <= x <= b schreiben, doch ist dies in Java nicht erlaubt. So müssen wir einen Und-Vergleich anstellen, der etwa so lautet: Ist a <= x && x <= b, dann liefere true zurück. In Form einer kompletten Methode sieht das so aus:

static boolean between( int a, int x, int b ) {

return a <= x && x <= b;

}

Die zweite Implementierung zeigt, dass sich das Problem auch ohne Und-Vergleich durch das Ausschlussprinzip lösen lässt:

static boolean between( int a, int x, int b ) {

if ( x < a )

return false;

if ( x <= b )

return true;

return false;

}

Mit verschachtelten Anfragen sieht das dann so aus:

static boolean between( int a, int x, int b ) {

if ( a <= x )

if ( x <= b )

return true;

return false;

}

Ob Programmierer die Variante mit dem Und-Operator oder dem verschachtelten if nutzen, ist oft Geschmacksache, aber die am besten lesbare Lösung sollte gewinnen, und das dürfte in dem Fall die mit dem Und sein.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.10    Methoden überladen 

Eine Methode ist gekennzeichnet durch Rückgabewert, Name, Parameter und unter Umständen durch Ausnahmefehler, die sie auslösen kann. Java erlaubt es, den Namen der Methode beizubehalten, aber andere Parameter einzusetzen. Eine Methode nennt man überladen, wenn sie unter dem gleichen Namen mehrfach auftaucht und unterschiedliche Parameterlisten hat. Das ist auf zwei Arten möglich:

  • Eine Methode heißt gleich, unterscheidet sich aber in der Anzahl der Parameter.

  • Eine Methode heißt gleich, hat aber für den Compiler unterscheidbare Parametertypen.

Anwendungen für den ersten Fall gibt es viele. Der Name einer Methode soll ihre Aufgabe beschreiben, aber nicht die Typen der Parameter, mit denen sie arbeitet, extra erwähnen. Das ist bei anderen Sprachen üblich, doch nicht in Java. Sehen wir uns als Beispiel die in der Mathe-Klasse Math angebotene statische Methode max(…) an. Sie ist mit den Parametertypen int, long, float und double deklariert – das ist viel schöner als etwa separate Methoden maxInt(…) und maxDouble(…).

Die statische Methode avg(…) könnten wir für zwei und drei Parameter deklarieren:

static double avg( int x, int y ) {

return (x + y) / 2.;

}

static double avg( double x, double y ) {

return (x + y) / 2;

}

static double avg( double x, double y, double z ) {

return (x + y + z) / 3;

}

static double avg( int x, int y, int z ) {

return (x + y + z) / 3.;

}

Es ist nicht möglich, zwei Methoden mit dem gleichen Methodennamen und gleicher Parameterliste, aber unterschiedlichen Rückgabetypen in einer Klasse zu deklarieren:

static double largerRandom() { return Math.random() * 100; }

static int largerRandom() { return (int)(Math.random() * 100); }

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Der Compiler könnte gar nicht auswählen, wenn es etwa heißen würde: System.out.println(largerRandom());.

print(…) und println(…) sind überladen *

Die bekannten print(…) und println(…) sind überladene Methoden, die etwa wie folgt deklariert sind:

class PrintStream { …

void print( int arg ) { … }

void print( String arg ) { … }

void print( double arg ) { … }

}

Wird nun die Methode print(…) mit irgendeinem Objekttyp aufgerufen, dann wird die am besten passende Methode herausgesucht. Das funktioniert selbst bei beliebigen Objekten, wie Abschnitt 7.5.2, »Implementierung von System.out.println(Object)«, zeigt.

Negative Beispiele und schlaue Leute *

Oft verfolgt auch die Java-Bibliothek die Strategie mit gleichen Namen und unterschiedlichen Typen. Es gibt allerdings einige Ausnahmen. In der Grafikbibliothek finden sich die folgenden drei Methoden:

  • drawString( String str, int x, int y )

  • drawChars( char[] data, int offset, int length, int x, int y )

  • drawBytes( byte[] data, int offset, int length, int x, int y )

Das ist äußerst hässlich und schlechter Stil.

Ein anderes Beispiel findet sich in der Klasse DataOutputStream. Hier heißen die Methoden etwa writeInt(…), writeChar(…) usw. Obwohl wir dies auf den ersten Blick verteufeln würden, ist diese Namensgebung sinnvoll. Ein Objekt vom Typ DataOutputStream dient zum Schreiben von primitiven Werten in einen Datenstrom. Gäbe es in DataOutputStream die überladenen Methoden write(byte), write(short), write(int), write(long) und write(char) und würden wir sie mit write(21) füttern, dann hätten wir das Problem, dass die Typumwandlung von Java die Daten automatisch anpasst und der Datenstrom mehr Daten beinhalten würde, als wir wünschen. Denn write(21) ruft nicht etwa write(short) auf und schreibt zwei Bytes, sondern write(int) und schreibt somit vier Bytes. Um also die Übersicht über die geschriebenen Bytes zu behalten, ist eine ausdrückliche Kennzeichnung der Datentypen in manchen Fällen gar nicht so dumm.

Überladene Methoden sind in anderen Programmiersprachen nichts Selbstverständliches. Zum Beispiel erlauben C# und C++ überladene Methoden; JavaScript, PHP und C tun dies jedoch nicht. In Sprachen ohne überladene Methoden wird der Methode bzw. Funktion ein Array mit Argumenten übergeben. So ist die Typisierung der einzelnen Elemente ein Problem – genauso wie die Beschränkung auf eine bestimmte Anzahl von Parametern.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.11    Gültigkeitsbereich 

Variablen können in jedem Block und in jeder Klasse deklariert werden. Jede Variable hat einen Gültigkeitsbereich (engl. scope), auch Geltungsbereich genannt. Nur in ihrem Gültigkeitsbereich kann das Java-Programm auf die Variable zugreifen, außerhalb des Gültigkeitsbereichs nicht. Genauer gesagt: Im Block und in den tiefer geschachtelten Blöcken ist die Variable gültig. Der lesende Zugriff ist nur dann erlaubt, wenn die Variable auch initialisiert wurde.

Der Gültigkeitsbereich bestimmt direkt die Lebensdauer der Variablen. Eine Variable ist nur in dem Block »lebendig«, in dem sie deklariert wurde. In diesem Block ist die Variable lokal.

Dazu ein Beispiel mit zwei statischen Methoden:

Listing 2.37    src/main/java/Scope.java

public class Scope {

public static void main( String[] args ) {

int foo = 0;

{

int bar = 0; // bar gilt nur in diesem Block

System.out.println( bar );

System.out.println( foo );

}

System.out.println( foo );

System.out.println( bar ); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Fehler: bar cannot be resolved

}

static void qux() {

int foo, baz; // foo hat nichts mit foo aus main() zu tun

{

int baz; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Fehler: Duplicate local variable baz

} }

}

Zu jeder Zeit können Blöcke aufgebaut werden. Außerhalb des Blocks sind deklarierte Variablen nicht gültig. Nach Abschluss des inneren Blocks, der bar deklariert, ist ein Zugriff auf bar nicht mehr möglich; auf foo ist der Zugriff innerhalb der statischen Methode main(…) weiterhin erlaubt. Dieses foo ist aber ein anderes foo als in der statischen Methode qux(). Eine Variable im Block ist so lange gültig, bis der Block durch eine schließende geschweifte Klammer beendet ist.

Innerhalb eines Blocks können Variablennamen nicht genauso gewählt werden wie Namen lokaler Variablen eines äußeren Blocks oder wie die Namen für die Parameter einer Methode. Das zeigt die zweite statische Methode am Beispiel der Deklaration baz. Obwohl andere Programmiersprachen (C++ sei genannt) diese Möglichkeit erlauben – und auch eine Syntax anbieten, um auf eine überschriebene lokale Variable eines höheren Blocks zuzugreifen –, haben sich die Java-Sprachentwickler dagegen entschieden. Gleiche Namen in den inneren und äußeren Blöcken sind nicht erlaubt. Das ist auch gut so, denn es minimiert Fehlerquellen. Die in Methoden deklarierten Parameter sind ebenfalls lokale Variablen und gehören zum Methodenblock.

Der Gültigkeitsbereich ist noch etwas komplexer, denn es kommen ja noch zum Beispiel geerbte Eigenschaften aus den Oberklassen mit hinzu.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.12    Vorgegebener Wert für nicht aufgeführte Argumente * 

Überladene Methoden lassen sich gut verwenden, wenn vorinitialisierte Werte bei nicht vorhandenen Argumenten genutzt werden sollen. Ist also ein Parameter nicht belegt, soll ein Standardwert eingesetzt werden. Um das zu erreichen, überladen wir einfach die Methode und rufen die andere Methode mit dem Standardwert passend auf (die Sprachen C# und C++ definieren in der Sprachgrammatik eine Möglichkeit für optionale Argumente, die wir in Java nicht haben).

Zwei überladene statische Methoden, tax(double cost, double taxRate) und tax(double cost), sollen die Steuer berechnen. Wir möchten, dass der Steuersatz automatisch 19 % ist, wenn die statische Methode tax(double cost) aufgerufen wird und der Steuersatz nicht explizit gegeben ist; im anderen Fall können wir taxRate beliebig wählen.

static double tax( double cost, double taxRate ) {

return cost * taxRate / 100.0;

}

static double tax( double cost ) {

return tax( cost, 19.0 ); // statt cost * 19.0 / 100;

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.13    Rekursive Methoden * 

Wir wollen den Einstieg in die Rekursion mit einem kurzen Beispiel beginnen. Auf dem Weg durch den Wald begegnet uns eine Fee (engl. fairy). Sie sagt zu uns: »Du hast drei Wünsche frei.« Tolle Situation. Um das ganze Unglück aus der Welt zu räumen, entscheiden wir uns nicht für eine egozentrische Wunscherfüllung, sondern für die sozialistische: »Ich möchte Frieden für alle, Gesundheit und Wohlstand für jeden.« Und schwups, so war es geschehen, und alle lebten glücklich, bis …

Einige Leser werden vielleicht die Hand vor den Kopf schlagen und sagen: »Quatsch! Selbstgießende Blumen, das letzte Ü-Ei in der Sammlung und einen Lebenspartner, der die Trägheit des Morgens duldet.« Glücklicherweise können wir das Dilemma mit der Rekursion lösen. Die Idee ist einfach – und in unseren Träumen schon erprobt –, sie besteht nämlich darin, den letzten Wunsch als »Noch mal drei Wünsche frei« zu formulieren.

Eine kleine Wunsch-Methode:

static void fairy() {

wish();

wish();

fairy();

}

Durch den dauernden Aufruf der fairy()-Methode haben wir unendlich viele Wünsche frei. Rekursion ist also das Aufrufen der eigenen Methode, in der wir uns befinden. Dies kann auch über einen Umweg funktionieren. Das nennt sich dann nicht mehr direkte Rekursion, sondern indirekte Rekursion. Sie ist ein alltägliches Phänomen, das wir auch von der Rückkopplung Mikrofon/Lautsprecher oder vom Blick mit einem Spiegel in den Spiegel kennen.

Unendliche Rekursionen

Wir müssen nun die Fantasieprogramme (deren Laufzeit und Speicherbedarf auch sehr schwer zu berechnen sind) gegen Java-Methoden austauschen.

Eine Endlosrekursion:

Listing 2.38    src/main/java/EndlessRecursion.java, down()

static void down( int n ) {

System.out.print( n + ", " );

down( n - 1 );

}

Rufen wir down(10) auf, dann wird die Zahl 10 auf dem Bildschirm ausgegeben und anschließend down(9) aufgerufen. Führen wir das Beispiel fort, so ergibt sich eine endlose Ausgabe, die so beginnt und die irgendwann mit einem StackOverflowError abbricht:

10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0, –1, –2, …

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 2.12    Steht der Cursor auf einem Methodenaufruf, so markiert Eclipse automatisch alle gleichen anderen Aufrufe oder die Deklaration der Methode.

Abbruch der Rekursion

An dieser Stelle erkennen wir, dass Rekursion prinzipiell etwas Unendliches ist. Für Programme ist dies aber ungünstig. Wir müssen daher ähnlich wie bei Schleifen eine Abbruchbedingung formulieren und dann keinen Rekursionsaufruf mehr starten. Die Abbruchbedingung sieht so aus, dass eine Fallunterscheidung das Argument prüft und mit return die Abarbeitung beendet:

Listing 2.39    src/main/java/Recursion.java, down1()

static void down1( int n ) {

if ( n <= 0 ) // Rekursionsende

return;

System.out.print( n + ", " );

down1( n - 1 );

}

Die statische down1(int)-Methode ruft jetzt nur noch so lange down1(n – 1) auf, wie das n größer null ist. Das ist die Abbruchbedingung einer Rekursion.

Unterschiedliche Rekursionsformen

Ein Kennzeichen der bisherigen Programme war, dass nach dem Aufruf der Rekursion keine Anweisung stand, sondern die Methode mit dem Aufruf beendet wurde. Diese Rekursionsform nennt sich Endrekursion. Diese Form ist verhältnismäßig einfach zu verstehen. Schwieriger sind Rekursionen, bei denen hinter dem Methodenaufruf Anweisungen stehen. Betrachten wir folgende Methoden, von denen die erste bekannt und die zweite neu ist:

Listing 2.40    src/main/java/Recursion.java, down1() und down2()

static void down1( int n ) {

if ( n <= 0 ) // Rekursionsende

return;

System.out.print( n + ", " );

down1( n - 1 );

}

static void down2( int n ) {

if ( n <= 0 ) // Rekursionsende

return;

down2( n - 1 );

System.out.print( n + ", " );

}

Der Unterschied besteht darin, dass down1(int) zuerst die Zahl n ausgibt und anschließend rekursiv down1(int) aufruft. Die Methode down2(int) steigt jedoch erst immer tiefer ab, und die Rekursion muss beendet sein, bis es zum ersten print(…) kommt. Daher gibt die statische Methode down2(int) im Gegensatz zu down1(int) die Zahlen in aufsteigender Reihenfolge aus:

1, 2, 3, 4, 5, 6, 7, 8, 9, 10,

Dies ist einleuchtend, wenn wir die Ablaufreihenfolge betrachten. Beim Aufruf down2(10) ist der Vergleich von n mit null falsch, also wird ohne Ausgabe wieder down2(9) aufgerufen. Ohne Ausgabe deshalb, da print(…) ja erst nach dem Methodenaufruf steht. Es geht rekursiv tiefer, bis n gleich null ist. Dann endet die letzte Methode mit return, und die Ausgabe wird nach dem down2(int), dem Aufrufer, fortgeführt. Dort ist print(…) die nächste Anweisung. Da wir nun noch tief verschachtelt stecken, gibt print(n) die Zahl 1 aus. Dann ist die Methode down2(int) wieder beendet (ein unsichtbares, nicht direkt geschriebenes return), und sie springt zum Aufrufer zurück. Das ist wieder die Methode down2(int), aber mit der Belegung n = 2. Das geht so weiter, bis das Programm zurück zum Aufrufer kommt, der down(10) aufgerufen hat, zum Beispiel der statischen main(…)-Methode. Der Trick bei der Sache besteht nun darin, dass jede Methode ihre eigene lokale Variable besitzt.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
  Die Tastenkombination (Strg)+(Alt)+(H) zeigt die Aufrufhierarchie an. So ist zu sehen, wer eine Methode aufruft. In den Aufrufen von down2(int) tauchen also wiederum wegen des rekursiven Aufrufs down2(int) sowie main(…) auf.

Rekursion und der Stack sowie die Gefahr eines StackOverflowErrors *

Am Beispiel haben wir gesehen, dass der Aufruf von down2(10) zum Aufruf von down2(9) führt. Und down2(10) kann erst dann beendet werden, wenn down2(9) komplett abgearbeitet wurde. down2(10) ist sozusagen so lange »offen«, bis der Schwanz von untergeordneten Aufrufen beendet ist. Nun muss sich die Laufzeitumgebung natürlich bei einem Methodenaufruf merken, wo es nach dem Methodenaufruf weitergeht. Dazu nutzt sie den Stack. Beim Aufruf von down2(9) etwa wird der Stack mit der Rücksprungadresse gefüllt, die zum Kontext von down2(10) zurückführt. In jedem Kontext gibt es auch wieder die alten lokalen Variablen.

Gibt es bei Rekursionen keine Abbruchbedingung, so kommen immer mehr Rücksprungadressen auf den Stapel, bis der Stapel keinen Platz mehr hat. Dann erzeugt die JVM zur Laufzeit ein java.lang.StackOverflowError, und das Programm (der Thread) bricht ab. In der Regel deutet der StackOverflowError auf einen Programmierfehler hin, es gibt aber Programme, die einen wirklich großen Stack benötigen und für die die gewöhnliche Stack-Größe einfach zu klein ist.

Stack-Größen ändern *

Die Oracle-JVM nutzt für unterschiedliche Betriebssysteme unterschiedliche Stack-Größen. Die Stack-Größe lässt sich über einen JVM-Schalter ändern, der -Xss:n heißt (oder in etwas längerer Schreibweise -XX:ThreadStackSize=n). Um die Stack-Größe auf 2 MiB (2.048 KiB) zu setzen, schreiben wir:

$ java -Xss:2048 MyApplication

Die Stack-Größe gilt damit für alle Threads in der JVM, was natürlich bei großen Stacks und vielen Threads zu einem Speicherproblem führen kann. Umgekehrt lässt sich auch Speicher einsparen, wenn das System sehr viele Threads nutzt und die Stack-Größe verringert wird.

Keine Optimierung von Endrekursion *

Eine Endrekursion (engl. tail call recursion) ist eine Besonderheit bei Methoden, da sie mit einem rekursiven Aufruf enden. Zum Einstieg: Ist die bekannte rekursive Implementierung der Fakultät endrekursiv?

int factorial( int n ) {

if ( n <= 0 ) return 1;

return n * factorial( n - 1 );

}

Zwar sieht es optisch so aus, als ob factorial(int) mit einem Methodenaufruf an factorial(…) endet, doch findet hier keine Endrekursion statt, da nach dem Methodenaufruf noch eine Multiplikation folgt. Wenn man den Code etwas anders schreibt, ist das besser zu erkennen:

int factorial( int n ) {

if ( n <= 0 ) return 1;

int fac = factorial( n - 1 );

return n * fac;

}

Die Berechnung der Fakultät lässt sich aber auch so umschreiben, dass tatsächlich eine Endrekursion stattfindet, und zwar durch Einführung eines Containers für Zwischenergebnisse, genannt Akkumulator:

int factorial( int n ) {

return factorialTailrec( n, 1 );

}

private int factorialTailrec( int n, int accumulator ) {

if ( n <= 0 ) return accumulator;

return factorialTailrec( n - 1, n * accumulator );

}

Die umgeschriebene Version büßt gegenüber der ursprünglichen Version an Schönheit ein. Doch endrekursive Aufrufe sind attraktiv, da eine schlaue Übersetzungseinheit sie so optimieren kann, dass der rekursive Methodenaufruf durch einen Sprung ersetzt wird. In der Java-Sprache haben wir keine direkten Sprünge, doch im Bytecode schon, sodass die Basis der Optimierung im Grunde so aussehen kann:

private int factorialTailrec( int n, int accumulator ) {start:

if ( n <= 0 ) return accumulator;

accumulator *= n; n--;

goto start;

}

Die Rekursion ist durch eine ordinäre Schleife ersetzt, was Stack einspart und eine sehr gute Performance ergibt.

In funktionalen Programmen ergeben sich eher Situationen, in denen Endrekursion vorkommt, sodass es attraktiv ist, diese zu optimieren. Die Standard-JVM kann das bisher nicht, weil Java traditionell keine funktionale Programmiersprache ist und Endrekursion eher selten vorkommt. Zwar wird die Optimierung von Endrekursion (engl. tail call optimization, kurz TCO) immer wieder diskutiert und auch schon in Prototypen ausprobiert, aber nie von der Oracle-JVM implementiert. Für Entwickler heißt das, rekursive Aufrufe nicht in endrekursive Varianten umzuschreiben, da sie sowieso nicht optimiert werden und nur unleserlicher würden. Bei großen Datenvolumen, sprich Stack-Tiefe, ist es besser, auf eine nichtrekursive iterative Version umzustellen. Im Fall von factorialTailrec(...) kann dies so aussehen:

private int factorialTailrec( int n, int accumulator ) {

while ( n > 0 ) {

accumulator *= n; n--; }

return accumulator;

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

2.7.14    Die Türme von Hanoi * 

Die Legende der Türme von Hanoi soll erstmalig von Ed Lucas in einem Artikel in der französischen Zeitschrift »Cosmo« im Jahre 1890 veröffentlicht worden sein. Der Legende nach standen vor langer Zeit im Tempel von Hanoi drei Säulen. Die erste war aus Kupfer, die zweite aus Silber und die dritte aus Gold. Auf der Kupfersäule waren einhundert Scheiben aufgestapelt. Die Scheiben hatten in der Mitte ein Loch und waren aus Porphyr. Die Scheibe mit dem größten Umfang lag unten, und alle kleiner werdenden Scheiben lagen obenauf. Ein alter Mönch stellte sich die Aufgabe, den Turm der Scheiben von der Kupfersäule zur Goldsäule zu bewegen. In einem Schritt sollte aber nur eine Scheibe bewegt werden, und zudem war die Bedingung, dass eine größere Scheibe niemals auf eine kleinere bewegt werden durfte. Der Mönch erkannte schnell, dass er die Silbersäule nutzen musste; er setzte sich an einen Tisch, machte einen Plan, überlegte und kam zu einer Entscheidung. Er konnte sein Problem in drei Schritten lösen. Am nächsten Tag schlug der Mönch die Lösung an die Tempeltür:

  • Falls der Turm aus mehr als einer Scheibe besteht, bitte deinen ältesten Schüler, einen Turm von (N – 1) Scheiben von der ersten zur dritten Säule unter Verwendung der zweiten Säule umzusetzen.

  • Trage selbst die erste Scheibe von einer zur anderen Säule.

  • Falls der Turm aus mehr als einer Scheibe besteht, bitte deinen ältesten Schüler, einen Turm aus (N – 1) Scheiben von der dritten zu der anderen Säule unter Verwendung der ersten Säule zu transportieren.

Und so rief der alte Mönch seinen ältesten Schüler zu sich und trug ihm auf, den Turm aus 99 Scheiben von der Kupfersäule zur Goldsäule unter Verwendung der Silbersäule umzuschichten und ihm den Vollzug zu melden. Nach der Legende würde das Ende der Welt nahe sein, bis der Mönch seine Arbeit beendet hätte. Nun, so weit die Geschichte. Wollen wir den Algorithmus zur Umschichtung der Porphyrscheiben in Java programmieren, so ist eine rekursive Lösung recht einfach. Werfen wir einen Blick auf das folgende Programm, das die Umschichtungen über die drei Pflöcke (engl. pegs) vornimmt:

Listing 2.41    src/main/java/TowerOfHanoi.java

class TowerOfHanoi {

static void move( int n, String fromPeg, String toPeg, String usingPeg ) {

if ( n > 1 ) {

move( n - 1, fromPeg, usingPeg, toPeg );

System.out.printf( "Bewege Scheibe %d von der %s zur %s.%n", n, fromPeg, toPeg );

move( n - 1, usingPeg, toPeg, fromPeg );

}

else

System.out.printf( "Bewege Scheibe %d von der %s zur %s.%n", n, fromPeg, toPeg );

}

public static void main( String[] args ) {

move( 4, "Kupfersäule", "Goldsäule", "Silbersäule" );

}

}

Starten wir das Programm mit vier Scheiben, so bekommen wir folgende Ausgabe:

Bewege Scheibe 1 von der Kupfersäule zur Silbersäule.Bewege Scheibe 2 von der Kupfersäule zur Goldsäule.Bewege Scheibe 1 von der Silbersäule zur Goldsäule.Bewege Scheibe 3 von der Kupfersäule zur Silbersäule.Bewege Scheibe 1 von der Goldsäule zur Kupfersäule.Bewege Scheibe 2 von der Goldsäule zur Silbersäule.Bewege Scheibe 1 von der Kupfersäule zur Silbersäule.Bewege Scheibe 4 von der Kupfersäule zur Goldsäule.Bewege Scheibe 1 von der Silbersäule zur Goldsäule.Bewege Scheibe 2 von der Silbersäule zur Kupfersäule.Bewege Scheibe 1 von der Goldsäule zur Kupfersäule.Bewege Scheibe 3 von der Silbersäule zur Goldsäule.Bewege Scheibe 1 von der Kupfersäule zur Silbersäule.Bewege Scheibe 2 von der Kupfersäule zur Goldsäule.

Bewege Scheibe 1 von der Silbersäule zur Goldsäule.

Schon bei vier Scheiben haben wir 15 Bewegungen. Selbst wenn unser Prozessor mit vielen Millionen Operationen pro Sekunde arbeitet, benötigt ein Computer für die Abarbeitung von 100 Scheiben Tausende geologischer Erdzeitalter. An diesem Beispiel wird eines deutlich: Viele Dinge sind im Prinzip berechenbar, nur praktisch ist so ein Algorithmus nicht.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 11

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Die allumfassende Superquelle ist die Java Language Specification, die online unter http://docs.oracle.com/javase/specs zu finden ist. Zweifelsfälle löst die Spezifikation auf, obwohl die Informationen zum Teil etwas verstreut sind.

Der niederländische Maler Maurits Cornelis Escher (1898–1972) machte die Rekursion auch in Bildern berühmt. Seiten mit Bildern und seine Vita finden sich zum Beispiel unter http://de.wikipedia.org/wiki/M._C._Escher.

Zu Beginn eines Projekts sollten Entwickler Kodierungsstandards (engl. code conventions) festlegen. Eine erste Informationsquelle ist http://tutego.de/go/codeconv. Amüsant ist dazu auch http://tutego.de/go/unmain zu lesen.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 12

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

»Nichts auf der Welt ist so gerecht verteilt wie der Verstand. Denn jedermann ist davon überzeugt, dass er genug davon habe.«

– René Descartes (1596–1650)

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.1    Objektorientierte Programmierung (OOP) 

In einem Buch über Java-Programmierung müssen mehrere Teile vereinigt werden:

  • zunächst die grundsätzliche Programmierung nach dem imperativen Prinzip (Variablen, Operatoren Fallunterscheidung, Schleifen, einfache statische Methoden) in einer neuen Grammatik für Java,

  • dann die Objektorientierung (Objekte, Klassen, Vererbung, Schnittstellen), erweiterte Möglichkeiten der Java-Sprache (Ausnahmen, Generics, Lambda-Ausdrücke) und zum Schluss

  • die Bibliotheken (String-Verarbeitung, Ein-/Ausgabe …).

Dieses Kapitel stellt das Paradigma der Objektorientierung in den Mittelpunkt und zeigt die Syntax, wie etwa in Java Klassen realisiert werden und Attribute und Methoden eingesetzt werden.

Java ist natürlich nicht die erste objektorientierte Sprache (OO-Sprache), auch C++ war nicht die erste. Klassischerweise gelten Smalltalk und insbesondere Simula-67 aus dem Jahr 1967 als Stammväter aller OO-Sprachen. Die eingeführten Konzepte sind bis heute aktuell, darunter die vier allgemein anerkannten Prinzipien der OOP: Abstraktion, Kapselung, Vererbung und Polymorphie.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.1.1    Warum überhaupt OOP? 

Da Menschen die Welt in Objekten wahrnehmen, wird auch die Analyse von Systemen häufig schon objektorientiert modelliert. Doch mit prozeduralen Systemen, die lediglich Unterprogramme als Ausdrucksmittel haben, wird die Abbildung des objektorientierten Designs in eine Programmiersprache schwer, und es entsteht ein Bruch. Im Laufe der Zeit entwickeln sich Dokumentation und Implementierung auseinander; die Software ist dann schwer zu warten und zu erweitern. Besser ist es, objektorientiert zu denken und dann eine objektorientierte Programmiersprache zur Abbildung zu haben.

Bad code can be written in any language.

Identität, Zustand, Verhalten

Die in der Software abgebildeten Objekte haben drei wichtige Eigenschaften:

  • Jedes Objekt hat eine Identität.

  • Jedes Objekt hat einen Zustand.

  • Jedes Objekt zeigt ein Verhalten.

Diese drei Eigenschaften haben wichtige Konsequenzen: zum einen, dass die Identität des Objekts während seines Lebens bis zu seinem Tod dieselbe bleibt und sich nicht ändern kann. Zum anderen werden die Daten und der Programmcode zur Manipulation dieser Daten als zusammengehörig behandelt. In prozeduralen Systemen finden sich oft Szenarien wie das folgende: Es gibt einen großen Speicherbereich, auf den alle Unterprogramme irgendwie zugreifen können. Bei den Objekten ist das anders, da sie logisch ihre eigenen Daten verwalten und die Manipulation überwachen.

In der objektorientierten Softwareentwicklung geht es also darum, in Objekten zu modellieren und dann zu programmieren. Das Design nimmt dabei eine zentrale Stellung ein; große Systeme werden zerlegt und immer feiner beschrieben. Hier passt sehr gut die Aussage des französischen Schriftstellers François Duc de La Rochefoucauld (1613–1680):

»Wer sich zu viel mit dem Kleinen abgibt, wird unfähig für Großes.«

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.1.2    Denk ich an Java, denk ich an Wiederverwendbarkeit 

Bei jedem neuen Projekt fällt auf, dass in früheren Projekten schon ähnliche Probleme gelöst werden mussten. Natürlich sollen bereits gelöste Probleme nicht neu implementiert, sondern sich wiederholende Teile bestmöglich in unterschiedlichen Kontexten wiederverwendet werden; das Ziel ist die bestmögliche Wiederverwendung von Komponenten.

Wiederverwendbarkeit von Programmteilen gibt es nicht erst seit den objektorientierten Programmiersprachen, objektorientierte Programmiersprachen erleichtern aber die Programmierung wiederverwendbarer Softwarekomponenten. So sind auch die vielen Tausend Klassen der Bibliothek ein Beispiel dafür, dass sich Entwickler nicht ständig um die Umsetzung etwa von Datenstrukturen oder um die Pufferung von Datenströmen kümmern müssen.

Auch wenn Java eine objektorientierte Programmiersprache ist, ist das kein Garant für tolles Design und optimale Wiederverwendbarkeit. Eine objektorientierte Programmiersprache erleichtert objektorientiertes Programmieren, aber auch in einer einfachen Programmiersprache wie C lässt sich objektorientiert programmieren. In Java sind auch Programme möglich, die aus nur einer Klasse bestehen und dort 5.000 Zeilen Programmcode mit statischen Methoden unterbringen. Bjarne Stroustrup (der Schöpfer von C++, von seinen Freunden auch Stumpy genannt) sagte treffend über den Vergleich von C und C++:

»C makes it easy to shoot yourself in the foot, C++ makes it harder, but when you do, it blows away your whole leg.«

Im Sinne unserer didaktischen Vorgehensweise wird dieses Kapitel zunächst einige Klassen der Standardbibliothek verwenden. Wir beginnen mit der Klasse Point, die zweidimensionale Punkte repräsentiert. In einem zweiten Schritt werden wir eigene Klassen programmieren. Anschließend kümmern wir uns um das Konzept der Abstraktion in Java, nämlich darum, wie Gruppen zusammenhängender Klassen gestaltet werden.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 13

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Klassen sind ein wichtigste Merkmal vieler objektorientierter Programmiersprachen. Eine Klasse definiert einen neuen Typ, beschreibt die Eigenschaften der Objekte und gibt somit den Bauplan an.

Jedes Objekt ist ein Exemplar (auch Instanz oder Ausprägung genannt) einer Klasse.

Eine Klasse deklariert im Wesentlichen zwei Dinge:

  • Attribute (was das Objekt hat)

  • Operationen (was das Objekt kann)

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

Die Operationen einer Klasse setzt die Programmiersprache Java durch Methoden um. Die Attribute eines Objekts definieren die Zustände, und sie werden durch Variablen implementiert (die auch Felder genannt werden).

Im Begriff »objektorientierte Programmierung« taucht zwar der Begriff »Objekt« auf, aber nicht der Begriff »Klasse«, den wir auch schon oft verwendet haben. Warum heißt es also nicht stattdessen »klassenbasierte Programmierung«? Der Grund ist, dass Klassendeklarationen für objektorientierte Programme nicht zwingend nötig sind. Ein anderer Ansatz ist die prototypbasierte objektorientierte Programmierung. Hier ist JavaScript der bekannteste Vertreter; dabei gibt es nur Objekte, und die sind mit einer Art Basistyp, dem Prototyp, verkettet.

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

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.2.1    Klassenarbeit mit Point 

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

Begriff

Erklärung

Klassenname

Ich bin ein Punkt.

Attribute

Ich habe eine x- und y-Koordinate.

Operationen

Ich kann mich verschieben und meine Position festlegen.

Tabelle 3.1    OOP-Begriffe und was sie bedeuten

Zu unserem Punkt können wir in der API-Dokumentation (http://docs.oracle.com/en/java/javase/14/docs/api/java.desktop/java/awt/Point.html) von Oracle nachlesen, dass er die Attribute x und y definiert, unter anderem eine Methode setLocation(…) besitzt und einen Konstruktor anbietet, der zwei Ganzzahlen annimmt.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 14

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

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

Im ersten Abschnitt eines UML-Diagramms lassen sich die Attribute ablesen, im zweiten die Operationen. Das + vor den Eigenschaften (siehe Abbildung 3.1) zeigt an, dass sie öffentlich sind und jeder sie nutzen kann. Die Typangabe ist gegenüber Java umgekehrt: Zuerst kommt der Name der Variablen, dann der Typ bzw. bei Methoden der Typ des Rückgabewerts.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.1    Die Klasse »java.awt.Point« in der UML-Darstellung

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.3.1    Hintergrund und Geschichte der UML * 

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

Vor der UML waren andere Darstellungsvarianten wie OMT oder Booch verbreitet. Diese waren eng mit einer Methode verbunden, die einen Entwicklungsprozess und ein Vorgehensmodell beschrieb. Methoden versuchen, eine Vorgehensweise beim Entwurf von Systemen zu beschreiben, etwa »erst Vererbung einsetzen und dann die Attribute finden« oder »erst die Attribute finden und dann mit Vererbung verfeinern«. Bekannte OO-Methoden sind etwa Shlaer/Mellor, Coad/Yourdon, Booch, OMT und OOSE/Objectory.

Aus dem Wunsch heraus, OO-Methoden zusammenzufassen, entstand die UML – anfangs stand die Abkürzung noch für Unified Method. Die Urversion 0.8 wurde im Jahre 1995 veröffentlicht. Die Initiatoren waren Jim Rumbaugh und Grady Booch. Später kam Ivar Jacobson dazu, und die drei »Amigos« erweiterten die UML, die in der Version 1.0 bei der Object Management Group (OMG) als Standardisierungsvorschlag eingereicht wurde. Die Amigos nannten die UML nun Unified Modeling Language, was deutlich macht, dass die UML keine Methode ist, sondern lediglich eine Modellierungssprache. Die Spezifikation erweitert sich ständig mit dem Aufkommen neuer Softwaretechniken, und so bildet die UML 2.0 Konzepte wie Model-Driven Architecture (MDA) und Geschäftsprozessmodellierung (engl. Business Process Modeling, kurz BPM) ab und unterstützt Echtzeitmodellierung durch spezielle Diagrammtypen. Eine aktuelle Version des Standards lässt sich unter http://tutego.de/go/uml einsehen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.3.2    Wichtige Diagrammtypen der UML * 

Die UML definiert diverse Diagrammtypen, die unterschiedliche Sichten auf die Software beschreiben können. Für die einzelnen Phasen im Softwareentwurf sind jeweils andere Diagramme wichtig. Wir wollen kurz vier Diagramme und ihr Einsatzgebiet besprechen.

Anwendungsfalldiagramm

Ein Anwendungsfalldiagramm (Use-Cases-Diagramm) entsteht meist während der Anforderungsphase und beschreibt die Geschäftsprozesse, indem es die Interaktion von Personen – oder von bereits existierenden Programmen – mit dem System darstellt. Die handelnden Personen oder aktiven Systeme werden Aktoren genannt und sind im Diagramm als kleine (geschlechtslose) Männchen angedeutet. Anwendungsfälle (Use Cases) beschreiben dann eine Interaktion mit dem System.

Klassendiagramm

Für die statische Ansicht eines Programmentwurfs ist das Klassendiagramm einer der wichtigsten Diagrammtypen. Ein Klassendiagramm stellt zum einen die Elemente der Klasse dar, also die Attribute und Operationen, und zum anderen die Beziehungen der Klassen untereinander. Klassendiagramme werden in diesem Buch häufiger eingesetzt, um insbesondere die Assoziation und Vererbung zu anderen Klassen zu zeigen. Klassen werden in einem solchen Diagramm als Rechteck dargestellt, und die Beziehungen zwischen den Klassen werden durch Linien angedeutet.

Objektdiagramm

Ein Klassendiagramm und ein Objektdiagramm sind sich auf den ersten Blick sehr ähnlich. Der wesentliche Unterschied besteht darin, dass ein Objektdiagramm die Belegung der Attribute, also den Objektzustand, visualisiert. Dazu werden sogenannte Ausprägungsspezifikationen verwendet. Mit eingeschlossen sind die Beziehungen, die das Objekt zur Laufzeit mit anderen Objekten hält. Beschreibt zum Beispiel ein Klassendiagramm eine Person, so ist nur ein Rechteck im Diagramm. Hat diese Person zur Laufzeit Freunde (gibt es also Assoziationen zu anderen Personen-Objekten), so können sehr viele Personen in einem Objektdiagramm verbunden sein, während ein Klassendiagramm diese Ausprägung nicht darstellen kann.

Sequenzdiagramm

Das Sequenzdiagramm stellt das dynamische Verhalten von Objekten dar. So zeigt es an, in welcher Reihenfolge Operationen aufgerufen und wann neue Objekte erzeugt werden. Die einzelnen Objekte bekommen eine vertikale Lebenslinie, und horizontale Linien zwischen den Lebenslinien der Objekte beschreiben die Operationen oder Objekterzeugungen. Das Diagramm liest sich somit von oben nach unten.

Da das Klassendiagramm und das Objektdiagramm eher die Struktur einer Software beschreiben, heißen die Modelle auch Strukturdiagramme (neben Paketdiagrammen, Komponentendiagrammen, Kompositionsstrukturdiagrammen und Verteilungsdiagrammen). Ein Anwendungsfalldiagramm und ein Sequenzdiagramm zeigen eher das dynamische Verhalten und werden Verhaltensdiagramme genannt. Weitere Verhaltensdiagramme sind das Zustandsdiagramm, das Aktivitätsdiagramm, das Interaktionsübersichtsdiagramm, das Kommunikationsdiagramm und das Zeitverlaufsdiagramm. In der UML ist es aber wichtig, die zentralen Aussagen des Systems in einem Diagramm festzuhalten, sodass sich problemlos Diagrammtypen mischen lassen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.3.3    UML-Werkzeuge * 

In der Softwareentwicklung gibt es nicht nur den Java-Compiler und die Laufzeitumgebung, sondern viele weitere Tools. Eine Kategorie von Produkten bilden Modellierungswerkzeuge, die bei der Abbildung einer Realwelt auf die Softwarewelt helfen. Insbesondere geht es um Software, die alle Phasen im Entwicklungsprozess abbildet.

Mit UML-Werkzeugen können Software-Architekten und Entwickler das Design einer Software besser im Blick halten. Mit ihm lassen sich die UML-Diagramme zeichnen und verändern. Im nächsten Schritt kann ein gutes UML-Tool aus diesen Zeichnungen Java-Code erzeugen. Noch weiter als eine einfache Codeerzeugung gehen Werkzeuge, die aus Java-Code umgekehrt UML-Diagramme generieren. Diese Reverse-Engineering-Tools haben jedoch eine schwere Aufgabe, da Java-Quellcode semantisch so reichhaltig ist, dass entweder das UML-Diagramm »zu voll« ist, völlig unzureichend formatiert ist oder Dinge nicht kompakt abgebildet werden. Die Königsdisziplin der UML-Tools bildet das Roundtrip-Engineering. Im Optimalfall sind dann das UML-Diagramm und der Quellcode synchron, und jede Änderung der einen Seite spiegelt sich sofort in einer Änderung auf der anderen Seite wider.

Hier eine Auswahl von Produkten:

  • Enterprise Architect (http://www.sparxsystems.de) ist ein Produkt von Sparx Systems. Es unterstützt UML 2.5 und bietet umfangreiche Modellierungsmöglichkeiten. Für die Business & Software Engineering Edition Standard License sind 599 USD fällig. Eine 30-tägige Testversion ist frei. Das Tool ist an sich eine eigenständige Software, die Integration in Eclipse (und MS Visual Studio) ist möglich.

  • MyEclipse (http://www.genuitec.com/products/myeclipse) von Genuitec besteht aus einer großen Sammlung von Eclipse-Plugins, unter anderem mit einem UML-Werkzeug. Einblick in das kommerzielle Werkzeug bekommen Sie hier: http://www.genuitec.com/products/myeclipse/learning-center/uml/myeclipse-uml2-development-overview.

  • ObjectAid UML Explorer for Eclipse (http://www.objectaid.com) ist ein kleines und kompaktes Werkzeug, das Klassen aus Eclipse einfach visualisiert. Es entwickelt sich langsam zu einem größeren kommerziellen Produkt. Die UML-Diagramme aus dem Buch sind in der Mehrzahl mit ObjectAid generiert.

  • Together (http://www.microfocus.com/de-de/products/requirements-management/together) ist ein alter Hase unter den UML-Tools – mittlerweile ist der Hersteller Borland bei Micro Focus gelandet. Es gibt eine 30-tägige Demoversion. Die letzte Version 12.9 unterstützt Java 8 und basiert auf Eclipse 4.6.1, ist also schon angestaubt.

  • Rational Rose (http://www-03.ibm.com/software/products/de/enterprise) ist das professionelle UML-Werkzeug von IBM. Es zeichnet sich durch seinen Preis aus, aber auch durch die Integration einer ganzen Reihe weiterer Werkzeuge, etwa für Anforderungsdokumente und Tests.

  • UMLet (http://www.umlet.com) ist ein UML-Zeichenwerkzeug und geht auf ein Projekt der Vienna University of Technology zurück. Es kann alleinstehend eingesetzt oder in Eclipse eingebettet werden. Auf GitHub liegt der offene Quellcode unter http://github.com/umlet/umlet. Die neue Version http://www.umletino.com funktioniert auch im Browser.

  • Der quelloffene UML Designer (http://obeonetwork.github.io/UML-Designer) greift auf viele Eclipse-Projekte zurück.

  • UML Lab von yatta (http://www.uml-lab.com/de/uml-lab) ist ein Eclipse-basiertes Werkzeug mit Round-Trip-Engineering. Es kostet ab 199 EUR, und es gibt eine Demo-Version zum Testen.

Viele Werkzeuge kamen und gingen. Unter ihnen waren:

  • ArgoUML (http://argouml.tigris.org), ein freies UML-Werkzeug mit UML-1.4-Notation auf der Basis von NetBeans. Es ist eigenständig und nicht in Eclipse integriert – Ende 2011 stoppte die Entwicklung; die letzte Version ist 0.34.

  • TOPCASED/PolarSys (http://www.topcased.org) war ein umfangreicher UML-Editor für Eclipse. Nachfolger ist Papyrus.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 15

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Eine Klasse beschreibt also, wie ein Objekt aussehen soll. In einer Mengen- bzw. Elementbeziehung ausgedrückt, entsprechen Objekte den Elementen und Klassen den Mengen, in denen die Objekte als Elemente enthalten sind. Diese Objekte haben Eigenschaften, die sich nutzen lassen. Wenn ein Punkt Koordinaten repräsentiert, wird es Möglichkeiten geben, diese Zustände zu erfragen und zu ändern.

Im Folgenden wollen wir untersuchen, wie sich von der Klasse Point zur Laufzeit Exemplare erzeugen lassen und wie der Zugriff auf die Eigenschaften der Point-Objekte aussieht.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.4.1    Ein Exemplar einer Klasse mit dem Schlüsselwort new anlegen 

Objekte müssen in Java immer ausdrücklich erzeugt werden. Dazu definiert die Sprache das Schlüsselwort new.

Anlegen eines Punkt-Objekts:

Im Grunde ist new so etwas wie ein unärer Operator. Hinter dem Schlüsselwort new folgt der Name der Klasse, von der ein Exemplar erzeugt werden soll. Der Klassenname ist hier voll qualifiziert angegeben, da sich Point in einem Paket java.awt befindet. (Ein Paket ist eine Gruppe zusammengehöriger Klassen; wir werden in Abschnitt 3.6.3, »Volle Qualifizierung und import-Deklaration«, sehen, dass Entwickler diese Schreibweise auch abkürzen können.) Hinter dem Klassennamen folgt ein Paar runder Klammern für den Konstruktoraufruf. Dieser ist eine Art Methodenaufruf, über den sich Werte für die Initialisierung des frischen Objekts übergeben lassen.

Konnte die Speicherverwaltung von Java für das anzulegende Objekt freien Speicher reservieren und konnte der Konstruktor gültig durchlaufen werden, gibt der new-Ausdruck anschließend eine Referenz auf das frische Objekt an das Programm zurück. Merken wir uns diese Referenz nicht, kann die automatische Speicherbereinigung das Objekt wieder freigeben.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.4.2    Deklarieren von Referenzvariablen 

Das Ergebnis eines new ist eine Referenz auf das neue Objekt. Die Referenz wird in der Regel in einer Referenzvariablen zwischengespeichert, um fortlaufende Eigenschaften vom Objekt nutzen zu können.

Deklariere die Variable p vom Typ java.awt.Point. Die Variable p nimmt anschließend die Referenz von dem neuen Objekt auf, das mit new angelegt wurde.

java.awt.Point p;

p = new java.awt.Point();

Die Deklaration und die Initialisierung einer Referenzvariablen lassen sich kombinieren (auch eine lokale Referenzvariable ist wie eine lokale Variable primitiven Typs zu Beginn uninitialisiert):

java.awt.Point p = new java.awt.Point();

Die Typen müssen natürlich kompatibel sein, und ein Punkt-Objekt geht nicht als String durch. Der Versuch, ein Punkt-Objekt einer int- oder String-Variablen zuzuweisen, ergibt somit einen Compilerfehler:

int p = new java.awt.Point(); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Type mismatch: cannot convert from

// Point to int

String s = new java.awt.Point(); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Type mismatch: cannot convert from

// Point to String

Damit speichert eine Variable entweder einen einfachen Wert (Variable vom Typ int, boolean, double …) oder einen Verweis auf ein Objekt. Der Verweis ist letztendlich intern ein Pointer auf einen Speicherbereich, doch der ist für Java-Entwickler so nicht sichtbar.

Referenztypen gibt es in vier Ausführungen: Klassentypen, Schnittstellentypen (auch Interface-Typen genannt), Array-Typen (auch Feldtypen genannt) und Typvariablen (eine Spezialität von generischen Typen). In unserem Fall haben wir ein Beispiel für einen Klassentyp.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.2    Die Tastenkombination (Strg)+(1) ermöglicht es, entweder eine neue lokale Variable oder eine Objektvariable für den Ausdruck anzulegen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.4.3    Jetzt mach mal ’nen Punkt: Zugriff auf Objektattribute und -methoden 

Die in einer Klasse deklarierten Variablen heißen Objektvariablen bzw. Exemplar-, Instanz- oder Ausprägungsvariablen. Jedes erzeugte Objekt hat seinen eigenen Satz von Objektvariablen: Sie bilden den Zustand des Objekts.

Der Punkt-Operator . erlaubt auf Objekten den Zugriff auf die Zustände oder den Aufruf von Methoden. Der Punkt steht zwischen einem Ausdruck, der eine Referenz liefert, und der Objekteigenschaft. Welche Eigenschaften eine Klasse genau bietet, zeigt die API-Dokumentation – wenn ein Objekt eine Eigenschaft nicht hat, wird der Compiler eine Nutzung verbieten.

Die Variable p referenziert ein java.awt.Point-Objekt. Die Objektvariablen x und y sollen initialisiert werden:

java.awt.Point p = new java.awt.Point();

p.x = 1;

p.y = 2 + p.x;

Ein Methodenaufruf gestaltet sich genauso einfach wie ein Attributzugriff. Hinter dem Ausdruck mit der Referenz folgt nach dem Punkt der Methodenname.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.3    Die Tastenkombination (Strg) + Leertaste zeigt an, welche Eigenschaften eine Referenz ermöglicht. Eine Auswahl mit der (¢)-Taste wählt die Eigenschaft aus und setzt insbesondere bei Methoden den Cursor zwischen das Klammerpaar.

Tür und Spieler auf dem Spielbrett

Punkt-Objekte erscheinen auf den ersten Blick als mathematische Konstrukte, doch sie sind allgemein nutzbar. Alles, was eine Position im zweidimensionalen Raum hat, lässt sich gut durch ein Punkt-Objekt repräsentieren. Der Punkt speichert für uns ja x und y, und hätten wir keine Punkt-Objekte, so müssten wir x und y immer extra speichern.

Nehmen wir an, wir wollen einen Spieler und eine Tür auf ein Spielbrett setzen. Natürlich haben die beiden Objekte Positionen. Ohne Objekte würde eine Speicherung der Koordinaten vielleicht so aussehen:

int playerX;

int playerY;

int doorX;

int doorY;

Die Modellierung ist nicht optimal, da wir mit der Klasse Point eine viel bessere Abstraktion haben, die zudem hübsche Methoden anbietet.

Ohne Abstraktion nur die nackten Daten

Kapselung der Zustände in ein Objekt

int playerX;

int playerY;

java.awt.Point player;

int doorX;

int doorY;

java.awt.Point door;

Tabelle 3.2    Objekte kapseln Zustände.

Das folgende Beispiel erzeugt zwei Punkte, die die x/y-Koordinate eines Spielers und einer Tür auf einem Spielbrett repräsentieren. Nachdem die Punkte erzeugt wurden, werden die Koordinaten gesetzt, und es wird außerdem getestet, wie weit der Spieler und die Tür voneinander entfernt sind:

Listing 3.1    PlayerAndDoorAsPoints.java

class PlayerAndDoorAsPoints {

public static void main( String[] args ) {

java.awt.Point player = new java.awt.Point();

player.x = player.y = 10;

java.awt.Point door = new java.awt.Point();

door.setLocation( 10, 100 );

System.out.println( player.distance( door ) ); // 90.0

}

}

Im ersten Fall belegen wir die Variablen x, y des Spiels explizit. Im zweiten Fall setzen wir nicht direkt die Objektzustände über die Variablen, sondern verändern die Zustände über die Methode setLocation(…). Die beiden Objekte besitzen eigene Koordinaten und kommen sich nicht in die Quere.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.4    Die Abhängigkeit zwischen einer Klasse und dem »java.awt.Point« zeigt das UML-Diagramm mit einer gestrichelten Linie an. Attribute und Operationen von »Point« sind nicht dargestellt.

toString()

Die Methode toString() liefert als Ergebnis ein String-Objekt, das den Zustand des Punktes preisgibt. Sie ist insofern besonders, als es immer auf jedem Objekt eine toString()-Methode gibt – nicht in jedem Fall ist die Ausgabe allerdings sinnvoll.

Listing 3.2    PointToStringDemo.java

class PointToStringDemo {

public static void main( String[] args ) {

java.awt.Point player = new java.awt.Point();

java.awt.Point door = new java.awt.Point();

door.setLocation( 10, 100 );

System.out.println( player.toString() ); // java.awt.Point[x=0,y=0]

System.out.println( door ); // java.awt.Point[x=10,y=100]

}

}

Anstatt für die Ausgabe explizit println(obj.toString()) aufzurufen, funktioniert auch ein println(obj). Das liegt daran, dass die Signatur println(Object) jedes beliebige Objekt als Argument akzeptiert und auf diesem Objekt automatisch die toString()-Methode aufruft.

Nach dem Punkt geht’s weiter

Die Methode toString() liefert, wie wir gesehen haben, als Ergebnis ein String-Objekt:

java.awt.Point p = new java.awt.Point();

String s = p.toString();

System.out.println( s ); // java.awt.Point[x=0,y=0]

Das String-Objekt besitzt selbst wieder Methoden. Eine davon ist length(), die die Länge der Zeichenkette liefert:

System.out.println( s.length() ); // 23

Das Erfragen des String-Objekts und seiner Länge können wir zu einer Anweisung verbinden; wir sprechen von kaskadierten Aufrufen.

java.awt.Point p = new java.awt.Point();

System.out.println( p.toString().length() ); // 23

Objekterzeugung ohne Variablenzuweisung

Bei der Nutzung von Objekteigenschaften muss der Typ links vom Punkt immer eine Referenz sein. Ob die Referenz nun aus einer Variablen kommt oder on-the-fly erzeugt wird, ist egal. Damit folgt, dass

java.awt.Point p = new java.awt.Point();

System.out.println( p.toString().length() ); // 23

genau das Gleiche bewirkt wie:

System.out.println( new java.awt.Point().toString().length() ); // 23

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.5    Jede Schachtelung ergibt einen neuen Typ.

Im Prinzip funktioniert auch Folgendes:

new java.awt.Point().x = 1;

Dies ist hier allerdings unsinnig, da zwar das Objekt erzeugt und ein Attribut gesetzt wird, anschließend das Objekt aber für die automatische Speicherbereinigung wieder Freiwild ist.

Finde über ein File-Objekt heraus, wie groß eine Datei ist:

long size = new java.io.File( "file.txt" ).length();

Die Rückgabe der File-Methode length() ist die Länge der Datei in Bytes.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.4.4    Der Zusammenhang von new, Heap und Garbage-Collector 

Bekommt das Laufzeitsystem die Anfrage, ein Objekt mit new zu erzeugen, so reserviert es so viel Speicher, dass alle Objekteigenschaften und Verwaltungsinformationen dort Platz finden. Ein Point-Objekt speichert die Koordinaten in zwei int-Werten, also sind mindestens 2 mal 4 Byte nötig. Den Speicherplatz nimmt die Laufzeitumgebung vom Heap. Der Heap wächst von einer Startgröße bis hin zu einer erlaubten Maximalgröße, damit ein Java-Programm nicht beliebig viel Speicher vom Betriebssystem abgreifen kann, was die Maschine möglicherweise in den Ruin treibt. In der HotSpot JVM ist der Heap zum Start 1/64 des Hauptspeichers groß und wächst dann bis zur maximalen Größe von ¼ des Hautspeichers.

Es gibt in Java nur wenige Sonderfälle, in denen neue Objekte nicht über new angelegt werden. So erzeugt die auf nativem Code basierende Methode newInstance() vom Constructor-Objekt ein neues Objekt. Auch clone() kann ein neues Objekt als Kopie eines anderen Objekts erzeugen. Bei der String-Konkatenation mit + ist für uns zwar kein new zu sehen, doch der Compiler wird Anweisungen bauen, um das neue String-Objekt anzulegen.

Ist das System nicht in der Lage, genügend Speicher für ein neues Objekt bereitzustellen, versucht die automatische Speicherbereinigung in einer letzten Rettungsaktion, alles Ungebrauchte wegzuräumen. Ist dann immer noch nicht ausreichend Speicher frei, generiert die Laufzeitumgebung einen OutOfMemoryError und beendet das gesamte Programm.

Heap und Stack

Die JVM-Spezifikation sieht für Daten fünf verschiedene Speicherbereiche (engl. runtime data areas) vor. Neben dem Heap-Speicher wollen wir uns den Stack-Speicher (Stapelspeicher) kurz anschauen. Den nutzt die Java-Laufzeitumgebung zum Beispiel für lokale Variablen. Auch verwendet Java den Stack beim Methodenaufruf mit Parametern. Die Argumente kommen vor dem Methodenaufruf auf den Stapel, und die aufgerufene Methode kann über den Stack auf die Werte lesend oder schreibend zugreifen. Bei endlosen rekursiven Methodenaufrufen ist irgendwann die maximale Stack-Größe erreicht, und es kommt zu einer Exception vom Typ java.lang.StackOverflowError. Da mit jedem Thread ein JVM-Stack assoziiert ist, bedeutet das das Ende des Threads, wobei andere Threads unbeeindruckt weiterlaufen.

Automatische Speicherbereinigung/Garbage-Collector (GC) – es ist dann mal weg

Nehmen wir folgendes Szenario an:

java.awt.Point binariumLocation;

binariumLocation = new java.awt.Point( 50, 9 );

binariumLocation = new java.awt.Point( 51, 7 );

Wir deklarieren eine Point-Variable, bauen ein Exemplar auf und belegten die Variable. Dann bauen wir ein neues Point-Objekt auf und überschreiben die Variable. Doch was ist mit dem ersten Punkt?

Wird das Objekt nicht mehr vom Programm referenziert, so bemerkt dies die automatische Speicherbereinigung alias der Garbage-Collector (GC) und gibt den reservierten Speicher wieder frei. Die automatische Speicherbereinigung testet dazu regelmäßig, ob die Objekte auf dem Heap noch benötigt werden. Werden sie nicht benötigt, löscht der Objektjäger sie. Es weht also immer ein Hauch von Friedhof über dem Heap, und nachdem die letzte Referenz vom Objekt genommen wird, ist es auch schon tot. Es gibt verschiedene GC-Algorithmen, und jeder Hersteller einer JVM hat eigene Verfahren.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.4.5    Überblick über Point-Methoden 

Ein paar Methoden der Klasse Point kamen schon vor, und die API-Dokumentation zählt selbstverständlich alle Methoden auf. Die interessanteren sind:

  • double getX()

  • double getY()

    Liefert die x- bzw. y-Koordinate.

  • void setLocation(double x, double y)

    Setzt gleichzeitig die x- und die y-Koordinate.

  • boolean equals(Object obj)

    Prüft, ob ein anderer Punkt die gleichen Koordinaten besitzt. Dann ist die Rückgabe true, sonst false. Wird etwas anderes als ein Point übergeben, so wird der Compiler das nicht bemäkeln, nur wird das Ergebnis dann immer false sein.

Ein paar Worte über Vererbung und die API-Dokumentation *

Eine Klasse besitzt nicht nur eigene Eigenschaften, sondern erbt auch immer welche von ihren Eltern. Im Fall von Point ist die Oberklasse Point2D – so sagt es die API-Dokumentation. Selbst Point2D erbt von Object, einer magischen Klasse, die alle Java-Klassen als Oberklasse haben. Der Vererbung widmen wir später ein sehr ausführliches Kapitel 7, »Objektorientierte Beziehungsfragen«, aber es ist jetzt schon wichtig zu verstehen, dass die Oberklasse Attribute und Methoden an Unterklassen weitergibt. Sie sind in der API-Dokumentation einer Klasse nur kurz im Block »Methods inherited from…« aufgeführt und gehen schnell unter. Für Entwickler ist es unabdingbar, nicht nur bei den Methoden der Klasse selbst zu schauen, sondern auch bei den geerbten Methoden. Bei Point sind es also nicht nur die Methoden dort selbst, sondern auch die Methoden aus Point2D und Object.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.6    Vererbungshierarchie bei Point2D

Nehmen wir uns einige Methoden der Oberklasse vor. Die Klassendeklaration von Point enthält ein extends Point2D, was explizit klarmacht, dass es eine Oberklasse gibt:

class java.awt.Point

extends Point2D

  • static double distance(double x1, double y1, double x2, double y2)

    Berechnet den Abstand zwischen den gegebenen Punkten nach der euklidischen Distanz.

  • double distance(double x, double y)

    Berechnet den Abstand des aktuellen Punktes zu angegebenen Koordinaten.

  • double distance(Point2D pt)

    Berechnet den Abstand des aktuellen Punktes zu den Koordinaten des übergebenen Punktes.

Sind zwei Punkte gleich?

Ob zwei Punkte gleich sind, sagt uns die equals(…)-Methode. Die Anwendung ist einfach. Stellen wir uns vor, wir wollen Koordinaten für einen Spieler, eine Tür und eine Schlange verwalten und dann testen, ob der Spieler »auf« der Tür steht und die Schlange auf der Position des Spielers:

Listing 3.3    PointEqualsDemo.java

class PointEqualsDemo {

public static void main( String[] args ) {

java.awt.Point player = new java.awt.Point();

player.x = player.y = 10;

java.awt.Point door = new java.awt.Point();

door.setLocation( 10, 10 );

System.out.println( player.equals( door ) ); // true

System.out.println( door.equals( player ) ); // true

java.awt.Point snake = new java.awt.Point();

snake.setLocation( 20, 22 );

System.out.println( snake.equals( door ) ); // false

}

}

Da Spieler und Tür die gleichen Koordinaten besitzen, liefert equals(…) die Rückgabe true. Dabei ist es egal, ob wir den Spieler mit der Tür oder die Tür mit dem Spieler vergleichen – das Ergebnis bei equals(…) sollte immer symmetrisch sein.

Eine andere Testmöglichkeit ergibt sich durch distance(…), denn ist der Abstand der Punkte null, so liegen die Punkte natürlich aufeinander und haben keinen Abstand.

Listing 3.4    Distances.java

class Distances {

public static void main( String[] args ) {

java.awt.Point player = new java.awt.Point();

player.setLocation( 10, 10 );

java.awt.Point door = new java.awt.Point();

door.setLocation( 10, 10 );

java.awt.Point snake = new java.awt.Point();

snake.setLocation( 20, 10 );

System.out.println( player.distance( door ) ); // 0.0

System.out.println( player.distance( snake ) ); // 10.0

System.out.println( player.distance( snake.x, snake.y ) ); // 10.0

}

}

Spieler, Tür und Schlange sind wieder als Point-Objekte repräsentiert und mit Positionen vorbelegt. Beim player rufen wir die Methode distance(…) auf und übergeben den Verweis auf die Tür und die Schlange. Ob wir den Abstand vom Spieler zur Tür berechnen lassen oder den Abstand von der Tür zum Spieler, kommt wie bei equals(…) auf dasselbe heraus.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.4.6    Konstruktoren nutzen 

Werden Objekte mit new angelegt, so wird ein Konstruktor aufgerufen. Ein Konstruktor hat die Aufgabe, ein Objekt in einen Startzustand zu versetzen, zum Beispiel die Objektvariablen zu initialisieren. Ein Konstruktor ist dazu ein guter Weg, denn er wird immer als Erstes aufgerufen, noch bevor eine andere Methode aufgerufen wird. Die Initialisierung im Konstruktor stellt sicher, dass das neue Objekt einen sinnvollen Anfangszustand aufweist.

Aus der API-Dokumentation von Point sind drei Konstruktoren abzulesen:

class java.awt.Point

extends Point2D

  • Point()

    Legt einen Punkt mit den Koordinaten (0, 0) an.

  • Point(int x, int y)

    Legt einen neuen Punkt an und initialisiert ihn mit den Werten aus x und y.

  • Point(Point p)

    Legt einen neuen Punkt an und initialisiert ihn mit den gleichen Koordinaten, die der übergebene Punkt hat. Wir nennen so einen Konstruktor auch Copy-Konstruktor.

Ein Konstruktor ohne Argumente ist der parameterlose Konstruktor, selten auch No-Arg-Konstruktor genannt. Jede Klasse kann höchstens einen parameterlosen Konstruktor besitzen, es kann aber auch sein, dass eine Klasse keinen parameterlosen Konstruktor deklariert, sondern nur Konstruktoren mit Parametern, also parametrisierte Konstruktoren.

Die drei folgenden Varianten legen ein Point-Objekt mit denselben Koordinaten (1, 2) an; java.awt.Point ist mit Point abgekürzt:

Point r = new Point( q );

Als Erstes steht der parameterlose Konstruktor, im zweiten und dritten Fall handelt es sich um parametrisierte Konstruktoren.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 16

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Ein Klassiker aus dem Genre der Computerspiele ist Snake. Auf dem Bildschirm gibt es den Spieler, eine Schlange, Gold und eine Tür. Die Tür und das Gold sind fest, den Spieler können wir bewegen, und die Schlange bewegt sich selbstständig auf den Spieler zu. Wir müssen versuchen, die Spielfigur zum Gold zu bewegen und dann zur Tür. Wenn die Schlange uns vorher erwischt, haben wir Pech gehabt, und das Spiel ist verloren.

Vielleicht hört sich das auf den ersten Blick komplex an, aber wir haben alle Bausteine zusammen, um dieses Spiel zu programmieren:

  • Spieler, Schlange, Gold und Tür sind Point-Objekte, die mit Koordinaten vorkonfiguriert sind.

  • Eine Schleife läuft alle Koordinaten ab. Ist ein Spieler, die Tür, die Schlange oder Gold »getroffen«, gibt es eine symbolische Darstellung der Figuren.

  • Wir testen drei Bedingungen für den Spielstatus: 1. Hat der Spieler das Gold eingesammelt und steht auf der Tür? (Das Spiel ist zu Ende.) 2. Beißt die Schlange den Spieler? (Das Spiel ist verloren.) 3. Sammelt der Spieler Gold ein?

  • Mit dem Scanner können wir auf Tastendrücke reagieren und den Spieler auf dem Spielbrett bewegen.

  • Die Schlange muss sich in Richtung des Spielers bewegen. Während der Spieler sich nur entweder horizontal oder vertikal bewegen kann, erlauben wir der Schlange, sich diagonal zu bewegen.

Im Quellcode sieht das so aus:

Listing 3.5    ZZZZZnake.java

public class ZZZZZnake {

public static void main( String[] args ) {

java.awt.Point playerPosition = new java.awt.Point( 10, 9 );

java.awt.Point snakePosition = new java.awt.Point( 30, 2 );

java.awt.Point goldPosition = new java.awt.Point( 6, 6 );

java.awt.Point doorPosition = new java.awt.Point( 0, 5 );

boolean rich = false;

while ( true ) {

// Raster mit Figuren zeichnen

for ( int y = 0; y < 10; y++ ) {

for ( int x = 0; x < 40; x++ ) {

java.awt.Point p = new java.awt.Point( x, y );

if ( playerPosition.equals( p ) )

System.out.print( '&' );

else if ( snakePosition.equals( p ) )

System.out.print( 'S' );

else if ( goldPosition.equals( p ) )

System.out.print( '$' );

else if ( doorPosition.equals( p ) )

System.out.print( '#' );

else System.out.print( '.' );

}

System.out.println();

}

// Status feststellen

if ( rich && playerPosition.equals( doorPosition ) ) {

System.out.println( "Gewonnen!" );

return;

}

if ( playerPosition.equals( snakePosition ) ) {

System.out.println( "ZZZZZZZ. Die Schlange hat dich!" );

return;

}

if ( playerPosition.equals( goldPosition ) ) {

rich = true;

goldPosition.setLocation( -1, -1 );

}

// Konsoleneingabe und Spielerposition verändern

switch ( new java.util.Scanner( System.in ).next() ) {

// Spielfeld ist im Bereich 0/0 .. 39/9

case "h" : playerPosition.y = Math.max( 0, playerPosition.y - 1 ); break;

case "t" : playerPosition.y = Math.min( 9, playerPosition.y + 1 ); break;

case "l" : playerPosition.x = Math.max( 0, playerPosition.x - 1 ); break;

case "r" : playerPosition.x = Math.min( 39, playerPosition.x + 1 ); break;

}

// Schlange bewegt sich in Richtung Spieler

if ( playerPosition.x < snakePosition.x )

snakePosition.x--;

else if ( playerPosition.x > snakePosition.x )

snakePosition.x++;

if ( playerPosition.y < snakePosition.y )

snakePosition.y--;

else if ( playerPosition.y > snakePosition.y )

snakePosition.y++;

} // end while

}

}

Die Point-Eigenschaften, die wir nutzen, sind:

  • die Objektzustände x, y: Der Spieler und die Schlange werden bewegt, und die Koordinaten müssen neu gesetzt werden.

  • die Methode setLocation(…): Ist das Gold aufgesammelt, setzen wir die Koordinaten so, dass die Koordinate vom Gold nicht mehr auf unserem Raster liegt.

  • die Methode equals(…): Testet, ob ein Punkt auf einem anderen Punkt steht.

Erweiterung

Wer Lust hat, an der Aufgabe noch ein wenig weiterzuprogrammieren, der kann Folgendes tun:

  • Spieler, Schlange, Gold und Tür sollen auf Zufallskoordinaten gesetzt werden.

  • Statt nur eines Stücks Gold soll es zwei Stücke geben.

  • Statt einer Schlange soll es zwei Schlangen geben.

  • Mit zwei Schlangen und zwei Stücken Gold kann es etwas eng für den Spieler werden. Er soll daher am Anfang 5 Züge machen können, ohne dass die Schlangen sich bewegen.

  • Für Vorarbeiter: Das Programm, das sich bisher nur in der main-Methode befindet, soll in verschiedene Methoden aufgespalten werden.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 17

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Die Klassenbibliothek von Java ist mit Tausenden Typen sehr umfangreich und deckt alles ab, was Entwickler von plattformunabhängigen Programmen als Basis benötigen. Dazu gehören Datenstrukturen, Klassen zur Datums-/Zeitberechnung, Dateiverarbeitung usw. Die meisten Typen sind in Java selbst implementiert (und der Quellcode ist in der Regel aus der Entwicklungsumgebung direkt verfügbar), aber einige Teile sind nativ implementiert, etwa wenn es darum geht, aus einer Datei zu lesen.

Wenn wir eigene Klassen programmieren, ergänzen sie sozusagen die Standardbibliothek; im Endeffekt wächst damit die Anzahl der möglichen Typen, die ein Programm nutzen kann.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.1    Java-Pakete 

Ein Paket ist eine Gruppe thematisch zusammengehöriger Typen. Pakete lassen sich in Hierarchien ordnen, sodass ein Paket wieder ein anderes Paket enthalten kann; das ist genauso wie bei der Verzeichnisstruktur des Dateisystems. Beispiele für Pakete sind:

  • java.awt

  • java.util

  • com.google

  • org.apache.commons.math3.fraction

  • com.tutego.insel

Die Klassen der Java-Standardbibliothek befinden sich in Paketen, die mit java und javax beginnen. Google nutzt die Wurzel com.google; die Apache Foundation veröffentlicht Java-Code unter org.apache. So können wir von außen ablesen, von welchen Typen die eigene Klasse abhängig ist.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.2    Pakete der Standardbibliothek 

Die logische Gruppierung und Hierarchie lässt sich sehr gut an der Java-Bibliothek beobachten. Die Java-Standardbibliothek beginnt mit der Wurzel java, einige Typen liegen in javax. Unter diesem Paket liegen weitere Pakete, etwa awt, math und util. In java.math liegen zum Beispiel die Klassen BigInteger und BigDecimal, denn die Arbeit mit beliebig großen Ganz- und Fließkommazahlen gehört eben zum Mathematischen. Ein Punkt und ein Polygon, repräsentiert durch die Klassen Point und Polygon, gehören in das Paket für grafische Oberflächen, und das ist das Paket java.awt.

Wenn jemand eigene Klassen in Pakete mit dem Präfix java setzen würde, etwa java.tutego, würde ein Programmautor damit Verwirrung stiften, da nicht mehr nachvollziehbar ist, ob das Paket Bestandteil jeder Distribution ist. Daher ist dieses Präfix für eigene Pakete verboten.

Klassen, die in einem Paket liegen, das mit javax beginnt, können Teil der Java SE sein wie javax.swing, müssen aber nicht zwingend zur Java SE gehören; dazu folgt mehr in Abschnitt 15.1.2, »Übersicht über die Pakete der Standardbibliothek«.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.3    Volle Qualifizierung und import-Deklaration 

Um die Klasse Point, die im Paket java.awt liegt, außerhalb des Pakets java.awt zu nutzen – und das ist für uns Nutzer immer der Fall –, muss sie dem Compiler mit der gesamten Paketangabe bekannt gemacht werden. Hierzu reicht der Klassenname allein nicht aus, denn es kann ja sein, dass der Klassenname mehrdeutig ist und eine Klassendeklaration in unterschiedlichen Paketen existiert.

Typen sind erst durch die Angabe ihres Pakets eindeutig identifiziert. Ein Punkt trennt Pakete, also schreiben wir java.awt und java.util – nicht einfach nur awt oder util. Mit einer weltweit unzähligen Anzahl von Paketen und Klassen wäre sonst eine Eindeutigkeit gar nicht machbar. Es kann in verschiedenen Paketen durchaus ein Typ mit gleichem Namen vorkommen, etwa java.util.List und java.awt.List oder java.util.Date und java.sql.Date. Und org.apache.commons.lang3.tuple hat einen Typ Pair, javafx.util auch. Daher bilden Paket und Typ eine Einheit und das eine ist oder das andere nicht eindeutig.

Um dem Compiler die präzise Zuordnung einer Klasse zu einem Paket zu ermöglichen, gibt es zwei Möglichkeiten: Zum einen lassen sich die Typen voll qualifizieren, wie wir das bisher getan haben. Eine alternative und praktischere Möglichkeit besteht darin, den Compiler mit einer import-Deklaration auf die Typen im Paket aufmerksam zu machen:

Listing 3.6    AwtWithoutImport.java

class AwtWithoutImport {

public static void main(String[] args){

java.awt.Point p =

new java.awt.Point();

java.awt.Polygon t =

new java.awt.Polygon();

t.addPoint( 10, 10 );

t.addPoint( 10, 20 );

t.addPoint( 20, 10 );

System.out.println( p );

System.out.println( t.contains(15, 15) );

}

}

Listing 3.7    AwtWithImport.java

import java.awt.Point;

import java.awt.Polygon;

class AwtWithImport {

public static void main(String[] args){

Point p = new Point();

Polygon t = new Polygon();

t.addPoint( 10, 10 );

t.addPoint( 10, 20 );

t.addPoint( 20, 10 );

System.out.println( p );

System.out.println( t.contains(15, 15) );

}

}

Tabelle 3.3    Typzugriff über volle Qualifikation und mit »import«-Deklaration

Während der Quellcode auf der linken Seite die volle Qualifizierung verwendet und jeder Verweis auf einen Typ mehr Schreibarbeit kostet, ist im rechten Fall beim import nur der Klassenname genannt und die Paketangabe in ein import »ausgelagert«. Alle Typen, die bei import genannt werden, merkt sich der Compiler für diese Datei in einer Datenstruktur. Kommt der Compiler zur Zeile mit Point p = new Point();, findet er den Typ Point in seiner Datenstruktur und kann den Typ dem Paket java.awt zuordnen. Damit ist wieder die unabkömmliche Qualifizierung gegeben.

Die Typen aus java.lang sind automatisch importiert, sodass z. B. ein import java.lang. String; nicht nötig ist.

Import-Deklarationen sind der übliche Weg, um Schreibarbeit zu sparen. Eine Ausnahme bilden gleichlautende Typen in unterschiedlichen Paketen. So gibt es in den Paketen java.awt und java.util den Typ List. Ein einfaches import java.awt.* und java.util.* hilft da nicht, weil der Compiler nicht weiß, ob die GUI-Komponente oder die Datenstruktur gemeint ist. Eine volle Qualifizierung löst das Problem.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.4    Mit import p1.p2.* alle Typen eines Pakets erreichen 

Greift eine Java-Klasse auf mehrere andere Typen des gleichen Pakets zurück, kann die Anzahl der import-Deklarationen groß werden. In unserem Beispiel nutzen wir mit Point und Polygon nur zwei Klassen aus java.awt, aber es lässt sich schnell ausmalen, was passiert, wenn aus dem Paket für grafische Oberflächen zusätzlich Fenster, Beschriftungen, Schaltflächen, Schieberegler usw. eingebunden werden. In diesem Fall darf ein * als letztes Glied in einer import-Deklaration stehen:

import java.awt.*;

import java.math.*;

Mit dieser Syntax kennt der Compiler alle Typen im Paket java.awt und java.math, sodass die Klassen Point und Polygon genau bekannt sind, wie auch die Klasse BigInteger.

Das * ist nur auf der letzten Hierarchieebene erlaubt und gilt immer für alle Typen in diesem Paket. Syntaktisch falsch sind:

import *; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Syntax error on token "*", Identifier expected

import java.awt.Po*; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Syntax error on token "*", delete this token

Eine Anweisung wie import java.*; ist zwar syntaktisch korrekt, aber dennoch ohne Wirkung, denn direkt im Paket java gibt es keine Typdeklarationen, sondern nur Unterpakete.

Die import-Deklaration bezieht sich nur auf ein Verzeichnis (in der Annahme, dass die Pakete auf das Dateisystem abgebildet werden) und schließt die Unterverzeichnisse nicht ein.

Das * verkürzt zwar die Anzahl der individuellen import-Deklarationen, es ist aber gut, zwei Dinge im Kopf zu behalten:

  • Falls zwei unterschiedliche Pakete einen gleichlautenden Typ beherbergen, etwa Date in java.util und java.sql, so kommt es bei der Verwendung des Typs zu einem Übersetzungsfehler. Hier muss voll qualifiziert werden.

  • Die Anzahl der import-Deklarationen sagt etwas über den Grad der Komplexität aus. Je mehr import-Deklarationen es gibt, desto größer werden die Abhängigkeiten zu anderen Klassen, was im Allgemeinen ein Alarmzeichen ist. Zwar zeigen grafische Tools die Abhängigkeiten genau an, doch ein import * kann diese erst einmal verstecken.

Entwicklungsumgebungen setzen die import-Deklarationen in der Regel automatisch und falten die Blöcke üblicherweise ein. Daher sollte der * nur sparsam eingesetzt werden, denn er »verschmutzt« den Namensraum durch viele Typen und erhöht die Gefahr von Kollisionen.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.5    Hierarchische Strukturen über Pakete und die Spiegelung im Dateisystem 

Die zu einem Paket gehörenden Klassen befinden sich normalerweise im gleichen Verzeichnis. Der Name des Pakets ist gleich dem Namen des Verzeichnisses (und natürlich umgekehrt). Statt des Verzeichnistrenners (etwa »/« oder »\«) steht ein Punkt.

Nehmen wir folgende Verzeichnisstruktur mit einer Hilfsklasse an:

com/tutego/insel/printer/DatePrinter.class

Hier ist der Paketname com.tutego.insel.printer und somit der Verzeichnisname com/tutego/insel/printer. Umlaute und Sonderzeichen sollten vermieden werden, da sie auf dem Dateisystem immer wieder für Ärger sorgen. Aber Bezeichner sollten ja sowieso immer auf Englisch sein.

Der Aufbau von Paketnamen

Prinzipiell kann ein Paketname beliebig sein, doch Hierarchien bestehen in der Regel aus umgedrehten Domänennamen. Aus der Domäne zur Webseite http://tutego.com wird also com.tutego. Diese Namensgebung gewährleistet, dass Klassen auch weltweit eindeutig bleiben. Ein Paketname wird in aller Regel komplett kleingeschrieben.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.6    Die package-Deklaration 

Um die Klasse DatePrinter in ein Paket com.tutego.insel.printer zu setzen, müssen zwei Dinge gelten:

  • Sie muss sich physikalisch in einem Verzeichnis befinden, also in com/tutego/insel/ printer.

  • Der Quellcode enthält zuoberst eine package-Deklaration.

Die package-Deklaration muss ganz am Anfang stehen, sonst gibt es einen Übersetzungsfehler (selbstverständlich lassen sich Kommentare vor die package-Deklaration setzen):

Listing 3.8    src/main/java/com/tutego/printer/DatePrinter.java

package com.tutego.insel.printer;

import java.time.LocalDate;

import java.time.format.*;

public class DatePrinter {

public static void printCurrentDate() {

DateTimeFormatter fmt = DateTimeFormatter.ofLocalizedDate( FormatStyle.MEDIUM );

System.out.println( LocalDate.now().format( fmt ) );

}

}

Hinter die package-Deklaration kommen wie gewohnt import-Deklaration(en) und die Typdeklaration(en).

Um die Klasse zu nutzen, bieten sich wie bekannt zwei Möglichkeiten: einmal über die volle Qualifizierung und einmal über die import-Deklaration. Die erste Variante sieht so aus:

Listing 3.9    src/main/java/DatePrinterUser1.java

public class DatePrinterUser1 {

public static void main( String[] args ) {

com.tutego.insel.printer.DatePrinter.printCurrentDate(); // 20.09.2020

}

}

Und hier ist die Variante mit der import-Deklaration:

Listing 3.10    src/main/java/DatePrinterUser2.java

import com.tutego.insel.printer.DatePrinter;

public class DatePrinterUser2 {

public static void main( String[] args ) {

DatePrinter.printCurrentDate(); // 20.09.2020

}

}

Eine Entwicklungsumgebung nimmt uns viel Arbeit ab, daher bemerken wir die Dateioperationen – wie das Anlegen von Verzeichnissen – in der Regel nicht. Auch das Verschieben von Typen in andere Pakete und die damit verbundenen Änderungen im Dateisystem und die Anpassungen an den import- und package-Deklarationen übernimmt eine moderne IDE für uns.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.7    Unbenanntes Paket (default package) 

Eine Klasse ohne Paketangabe befindet sich im unbenannten Paket (engl. unnamed package) bzw. Default-Paket. Es ist eine gute Idee, eigene Klassen immer in Paketen zu organisieren. Das erlaubt auch feinere Sichtbarkeiten, und Konflikte mit anderen Unternehmen und Autoren werden vermieden. Es wäre ein großes Problem, wenn a) jedes Unternehmen unübersichtlich alle Klassen in das unbenannte Paket setzen und dann b) versuchen würde, die Bibliotheken auszutauschen: Konflikte wären vorprogrammiert.

Eine im Paket befindliche Klasse kann jede andere sichtbare Klasse aus anderen Paketen importieren, aber keine Klassen aus dem unbenannten Paket. Nehmen wir Sugar im unbenannten Paket und Chocolate im Paket com.tutego an:

Sugar.class

com/tutego/insel/Chocolate.class

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 3.7    Das Verzeichnis »default package« steht in Eclipse für das unbenannte Paket.

Die Klasse Chocolate kann Sugar nicht nutzen, da Klassen aus dem unbenannten Paket nicht für Unterpakete sichtbar sind. Nur andere Klassen im unbenannten Paket können Klassen im unbenannten Paket nutzen.

Stände nun Sugar in einem Paket – das auch ein Oberpaket sein kann! –, so wäre das wiederum möglich, und Chocolate könnte Sugar importieren:

com/Sugar.class

com/tutego/insel/Chocolate.class

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.8    Compilationseinheit (Compilation Unit) 

Eine .java-Datei ist eine Compilationseinheit (Compilation Unit), die aus drei (optionalen) Segmenten besteht – in dieser Reihenfolge:

  1. package-Deklaration

  2. import-Deklaration(en)

  3. Typdeklaration(en)

So besteht eine Compilationseinheit aus höchstens einer Paketdeklaration (nicht nötig, wenn der Typ im Default-Paket stehen soll), beliebig vielen import-Deklarationen und beliebig vielen Typdeklarationen. Der Compiler übersetzt jeden Typ einer Compilationseinheit in eine eigene .class-Datei. Ein Paket ist letztendlich eine Sammlung aus Compilationseinheiten. In der Regel ist die Compilationseinheit eine Quellcodedatei; die Codezeilen könnten grundsätzlich auch aus einer Datenbank kommen oder zur Laufzeit generiert werden.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.6.9    Statischer Import * 

Die import-Deklaration informiert den Compiler über die Pakete, sodass ein Typ nicht mehr voll qualifiziert werden muss, wenn er im import-Teil explizit aufgeführt wird oder wenn das Paket des Typs über * genannt ist.

Falls eine Klasse statische Methoden oder Konstanten vorschreibt, werden ihre Eigenschaften immer über den Typnamen angesprochen. Java bietet mit dem statischen Import die Möglichkeit, die statischen Methoden oder Variablen ohne vorangestellten Typnamen sofort zu nutzen. Während also das normale import dem Compiler Typen benennt, macht ein statisches import dem Compiler Klasseneigenschaften bekannt, geht also eine Ebene tiefer.

Binde für die Bildschirmausgabe die statische Variable out aus System statisch ein:

import static java.lang.System.out;

Bei der sonst üblichen Ausgabe über System.out.printXXX(…) kann nach dem statischen Import der Klassenname entfallen, und es bleibt beim out.printXXX(…).

Binden wir in einem Beispiel mehrere statische Eigenschaften mit einem statischen import ein:

Listing 3.11    src/main/java/com/tutego/insel/oop/StaticImport.java

package com.tutego.insel.oop;

import static java.lang.System.out;

import static javax.swing.JOptionPane.showInputDialog;

import static java.lang.Integer.parseInt;

import static java.lang.Math.max;

import static java.lang.Math.min;

class StaticImport {

public static void main( String[] args ) {

int i = parseInt( showInputDialog( "Erste Zahl" ) );

int j = parseInt( showInputDialog( "Zweite Zahl" ) );

out.printf( "%d ist größer oder gleich %d.%n",

max(i, j), min(i, j) );

}

}

Mehrere Typen statisch importieren

Der statische Import

import static java.lang.Math.max;

import static java.lang.Math.min;

bindet die statische max(…)/min(…)-Methode ein. Besteht Bedarf an weiteren statischen Methoden, gibt es neben der individuellen Aufzählung eine Wildcard-Variante:

import static java.lang.Math.*;

Auch wenn Java diese Möglichkeit bietet, sollte der Einsatz maßvoll erfolgen. Die Möglichkeit der statischen Importe ist nützlich, wenn Klassen Konstanten nutzen wollen, allerdings besteht auch die Gefahr, dass durch den fehlenden Typnamen nicht mehr sichtbar ist, woher die Eigenschaft eigentlich kommt und welche Abhängigkeit sich damit aufbaut. Auch gibt es Probleme mit gleichlautenden Methoden: Eine Methode aus der eigenen Klasse überdeckt statisch importierte Methoden. Wenn also später in der eigenen Klasse – oder Oberklasse – eine Methode aufgenommen wird, die die gleiche Signatur hat wie eine statisch importierte Methode, wird das zu keinem Compilerfehler führen, sondern die Semantik wird sich ändern, weil jetzt die neue eigene Methode verwendet wird und nicht mehr die statisch importierte.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 18

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

In Java gibt es mit null eine sehr spezielle Referenz, die Auslöser vieler Probleme ist. Doch ohne sie geht es nicht, und warum das so ist, wird der folgende Abschnitt zeigen. Anschließend wollen wir sehen, wie Objektvergleiche funktionieren und was der Unterschied zwischen Identität und Gleichwertigkeit ist.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.7.1    null-Referenz und die Frage der Philosophie 

In Java gibt es drei spezielle Referenzen: null, this und super. (Wir verschieben die Beschreibung von this und super auf Kapitel 6, »Eigene Klassen schreiben«.) Das spezielle Literal null lässt sich zur Initialisierung von Referenzvariablen verwenden. Die null-Referenz ist typenlos, kann also jeder Referenzvariablen zugewiesen und jeder Methode übergeben werden, die ein Objekt erwartet.

Deklaration und Initialisierung zweier Objektvariablen mit null:

Point p = null;

String s = null;

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

Die Konsolenausgabe über die letzte Zeile liefert kurz »null«. Wir haben hier die String-Repräsentation vom null-Typ vor uns.

Da null typenlos ist und es nur ein null gibt, kann null zu jedem Typ typangepasst werden, und so ergibt zum Beispiel ((String) null == null && (Point) null == null das Ergebnis true. Das Literal null ist ausschließlich für Referenzen vorgesehen und kann in keinen primitiven Typ wie die Ganzzahl 0 umgewandelt werden.

Mit null lässt sich eine ganze Menge machen. Der Haupteinsatzzweck sieht vor, damit uninitialisierte Referenzvariablen zu kennzeichnen, also auszudrücken, dass eine Referenzvariable auf kein Objekt verweist. In Listen oder Bäumen kennzeichnet null zum Beispiel das Fehlen eines gültigen Nachfolgers oder bei einem grafischen Dialog, dass der Benutzer den Dialog abgebrochen hat; null ist dann ein gültiger Indikator und kein Fehlerfall.

Bei einer mit null initialisierten lokalen Variablen funktioniert die Abkürzung mit var nicht; es gibt einen Compilerfehler:

var text = null; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Cannot infer type for local variable initialized to 'null'

Auf null geht nix, nur die NullPointerException

Da sich hinter null kein Objekt verbirgt, ist es auch nicht möglich, eine Methode aufzurufen oder von null ein Attribut zu erfragen. Der Compiler kennt zwar den Typ jedes Ausdrucks, aber erst die Laufzeitumgebung (JVM) weiß, was referenziert wird. Bei dem Versuch, über die null-Referenz auf eine Eigenschaft eines Objekts zuzugreifen, löst eine JVM eine NullPointerException aus:

Listing 3.12    src/main/java/com/tutego/insel/oop/NullPointer.java

package com.tutego.insel.oop; // 1

public class NullPointer { // 2

public static void main( String[] args ) { // 3

java.awt.Point p = null; // 4

String s = null; // 5

p.setLocation( 1, 2 ); // 6

s.length(); // 7

} // 8

} // 9

Wir beobachten eine NullPointerException zur Laufzeit, denn das Programm bricht bei p.setLocation(…) mit folgender Ausgabe ab:

Exception in thread "main" java.lang.NullPointerException

at com.tutego.insel.oop.NullPointer.main(NullPointer.java:6)

Die Laufzeitumgebung teilt uns in der Fehlermeldung mit, dass sich der Fehler, die NullPointerException, in Zeile 6 befindet. Um den Fehler zu korrigieren, müssen wir entweder die Variablen initialisieren, das heißt, ein Objekt zuweisen wie in

p = new java.awt.Point();

s = "";

oder vor dem Zugriff auf die Eigenschaften einen Test durchführen, ob Objektvariablen auf etwas zeigen oder null sind, und in Abhängigkeit vom Ausgang des Tests den Zugriff auf die Eigenschaft zulassen oder nicht.

Ist Java eine rein objektorientierte Programmiersprache? Nein, da Java einen Unterschied zwischen primitiven Typen und Referenztypen macht. Nehmen wir für einen Moment an, dass es primitive Typen nicht gäbe. Wäre Java dann eine rein objektorientierte Programmiersprache, bei der jede Referenz ein pures Objekt referenziert? Die Antwort ist immer noch nein, da es mit null etwas gibt, womit Referenzvariablen initialisiert werden können, was aber kein Objekt repräsentiert und keine Methoden besitzt. Und das kann bei der Dereferenzierung eine NullPointerException geben.

Andere Programmiersprachen haben andere Lösungsansätze, und null-Referenzierungen sind nicht möglich. In der Sprache Ruby zum Beispiel ist immer alles ein Objekt. Wo Java mit null ein »nicht belegt« ausdrückt, macht Ruby das mit nil. Der feine Unterschied ist, dass nil ein Exemplar der Klasse NilClass ist, genau genommen ein Singleton, das es im System nur einmal gibt. nil hat auch ein paar öffentliche Methoden wie to_s (wie Javas toString()), das dann einen leeren String liefert. Mit nil gibt es keine NullPointerException mehr, aber natürlich immer noch einen Fehler, wenn auf diesem Objekt vom Typ NilClass eine Methode aufgerufen wird, die es nicht gibt. In Objective-C, der (bisherigen) Standardsprache für iOS-Programme, gibt es das Null-Objekt nil. Üblicherweise passiert nichts, wenn eine Nachricht an das nil-Objekt gesendet wird; die Nachricht wird einfach ignoriert.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.7.2    Alles auf null? Referenzen testen 

Mit dem Vergleichsoperator == oder dem Test auf Ungleichheit mit != lässt sich leicht herausfinden, ob eine Referenzvariable wirklich ein Objekt referenziert oder nicht:

if ( object == null )

// Variable referenziert nichts, ist aber gültig mit null initialisiert

else

// Variable referenziert ein Objekt

null-Test und Kurzschluss-Operatoren

Wir wollen an dieser Stelle noch einmal auf die üblichen logischen Kurzschluss-Operatoren und den logischen, nicht kurzschließenden Operator zu sprechen kommen. Erstere werten Operanden nur so lange von links nach rechts aus, bis das Ergebnis der Operation feststeht. Auf den ersten Blick scheint es nicht viel auszumachen, ob alle Teilausdrücke ausgewertet werden oder nicht. In einigen Ausdrücken ist dies aber wichtig, wie das folgende Beispiel für die Variable s vom Typ String zeigt:

Listing 3.13    src/main/java/NullCheck.java, main

public static void main( String[] args ) {

String s = javax.swing.JOptionPane.showInputDialog( "Eingabe" );

if ( s != null && ! s.isEmpty() )

System.out.println( "Eingabe: " + s );

else

System.out.println( "Abbruch oder keine Eingabe" );

}

Die Rückgabe von showInputDialog(…) ist null, wenn der Benutzer den Dialog abbricht. Das soll unser Programm berücksichtigen. Daher testet die if-Bedingung, ob s überhaupt auf ein Objekt verweist, und prüft gleichzeitig, ob die Länge größer 0 ist. Dann folgt eine Ausgabe.

Diese Schreibweise tritt häufig auf, und der Und-Operator zur Verknüpfung muss ein Kurzschluss-Operator sein, da es in diesem Fall ausdrücklich darauf ankommt, dass die Länge nur dann bestimmt wird, wenn die Variable s überhaupt auf ein String-Objekt verweist und nicht null ist. Andernfalls bekämen wir bei s.isEmpty() eine NullPointerException, wenn jeder Teilausdruck ausgewertet würde und s gleich null wäre.

Da null viel zu oft vorkommt, null-Referenzierungen aber vermieden werden müssen, gibt es viel Code der Art o != null ? o : non_null_o. Diverse Programmiersprachen wie JavaScript, Kotlin, Objective-C, PHP oder Swift bieten für dieses Konstrukt eine Abkürzung über den sogenannten null coalescing operator (coalescing heißt auf Deutsch »verschmelzend«). Er wird mal als ?? oder als ?: geschrieben, für unser Beispiel so: o ?? non_null_o. Besonders hübsch ist das bei sequenziellen Tests der Art o ?? p ?? q ?? r, wo es dann sinngemäß heißt: »Liefere die erste Referenz ungleich null.« Java bietet keinen solchen Operator.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.7.3    Zuweisungen bei Referenzen 

Eine Referenz erlaubt den Zugriff auf das referenzierte Objekt, und eine Referenzvariable speichert eine Referenz. Es kann durchaus mehrere Referenzvariablen geben, die die gleiche Referenz speichern. Das wäre so, als ob ein Objekt unter verschiedenen Namen angesprochen wird – so wie eine Person von den Mitarbeitern als »Chefin« angesprochen wird, aber von ihrem Mann als »Schnuckiputzi«. Dies nennt sich auch Alias.

Ein Punkt-Objekt wollen wir unter einem alternativen Variablennamen ansprechen:

Point p = new Point();

Point q = p;

Ein Punkt-Objekt wird erzeugt und mit der Variablen p referenziert. Die zweite Zeile speichert nun dieselbe Referenz in der Variablen q. Danach verweisen p und q auf dasselbe Objekt. Zum besseren Verständnis: Wichtig ist, wie oft es new gibt, denn das sagt aus, wie viele Objekte die JVM bildet. Und bei den zwei Zeilen gibt es nur ein new, also auch nur einen Punkt.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Verweisen zwei Objektvariablen auf dasselbe Objekt, hat das natürlich zur Konsequenz, dass über zwei Wege Objektzustände ausgelesen und modifiziert werden können. Heißt die gleiche Person in der Firma »Chefin« und zu Hause »Schnuckiputzi«, wird der Mann sich freuen, wenn die Frau in der Firma keinen Stress hat.

Wir können das Beispiel auch gut bei Punkt-Objekten nachverfolgen. Zeigen p und q auf dasselbe Punkt-Objekt, können Änderungen über p auch über die Variable q beobachtet werden:

Listing 3.14    ItsTheSame.java, main

public static void main( String[] args ) {

Point p = new Point();

Point q = p;

p.x = 10;

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

q.y = 5;

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

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.7.4    Methoden mit Referenztypen als Parametern 

Dass sich dasselbe Objekt unter zwei Namen (über zwei verschiedene Variablen) ansprechen lässt, können wir gut bei Methoden beobachten. Eine Methode, die über den Parameter eine Objektreferenz erhält, kann auf das übergebene Objekt zugreifen. Das bedeutet, die Methode kann dieses Objekt mit den angebotenen Methoden ändern oder auf die Attribute zugreifen.

Im folgenden Beispiel deklarieren wir zwei Methoden. Die erste Methode, initializeToken(Point), soll einen Punkt mit Zufallskoordinaten initialisieren. Übergeben werden ihr dann zwei Point-Objekte: einmal für einen Spieler und einmal für eine Schlange. Die zweite Methode, printScreen(Point, Point), gibt das Spielfeld auf dem Bildschirm aus und gibt dann, wenn die Koordinate einen Spieler trifft, ein & aus und bei der Schlange ein S. Falls Spieler und Schlange zufälligerweise zusammentreffen, »gewinnt« die Schlange.

Listing 3.15    src/main/java/com/tutego/insel/oop/DrawPlayerAndSnake.java

package com.tutego.insel.oop;

import java.awt.Point;

public class DrawPlayerAndSnake {

static void initializeToken( Point p ) {

int randomX = (int)(Math.random() * 40); // 0 <= x < 40

int randomY = (int)(Math.random() * 10); // 0 <= y < 10

p.setLocation( randomX, randomY );

}

static void printScreen( Point playerPosition,

Point snakePosition ) {

for ( int y = 0; y < 10; y++ ) {

for ( int x = 0; x < 40; x++ ) {

if ( snakePosition.distanceSq( x, y ) == 0 )

System.out.print( 'S' );

else if ( playerPosition.distanceSq( x, y ) == 0 )

System.out.print( '&' );

else System.out.print( '.' );

}

System.out.println();

} }

public static void main( String[] args ) {

Point playerPosition = new Point();

Point snakePosition = new Point();

System.out.println( playerPosition );

System.out.println( snakePosition );

initializeToken( playerPosition );

initializeToken( snakePosition );

System.out.println( playerPosition );

System.out.println( snakePosition );

printScreen( playerPosition, snakePosition );

}

}

Die Ausgabe kann so aussehen:

java.awt.Point[x=0,y=0]java.awt.Point[x=0,y=0]java.awt.Point[x=38,y=1]java.awt.Point[x=19,y=8]..............................................................................&....................................................................................................................................................................................................................................................................S....................

........................................

In dem Moment, in dem main(…) die statische Methode initializeToken(Point) aufruft, gibt es sozusagen zwei Namen für das Point-Objekt: playerPosition und p. Allerdings ist das nur innerhalb der virtuellen Maschine so, denn initializeToken(Point) kennt das Objekt nur unter p, aber kennt die Variable playerPosition nicht. Bei main(…) ist es umgekehrt: Nur der Variablenname playerPosition ist in main(…) bekannt, er hat aber vom Namen p keine Ahnung. Die Point-Methode distanceSq(int, int) liefert den quadrierten Abstand vom aktuellen Punkt zu den übergebenen Koordinaten.

Der Name einer Parametervariablen darf durchaus mit dem Namen der Argumentvariablen übereinstimmen, was die Semantik nicht verändert. Die Namensräume sind völlig getrennt, und Missverständnisse gibt es nicht, da beide – die aufrufende Methode und die aufgerufene Methode – komplett getrennte lokale Variablen haben.

Wertübergabe und Referenzübergabe per Call by Value

Primitive Variablen werden immer per Wert kopiert (Call by Value). Das Gleiche gilt für Referenzen, die ja als eine Art Zeiger zu verstehen sind, und das sind im Prinzip nur Ganzzahlen. Daher hat auch die folgende statische Methode keine Nebenwirkungen:

Listing 3.16    JavaIsAlwaysCallByValue.java

package com.tutego.insel.oop;

import java.awt.Point;

public class JavaIsAlwaysCallByValue {

static void clear( Point p ) {

System.out.println( p ); // java.awt.Point[x=10,y=20]

p = new Point();

System.out.println( p ); // java.awt.Point[x=0,y=0]

}

public static void main( String[] args ) {

Point p = new Point( 10, 20 );

clear( p );

System.out.println( p ); // java.awt.Point[x=10,y=20]

}

}

Nach der Zuweisung p = new Point() in der clear(Point)-Methode referenziert die Parametervariable p ein anderes Punkt-Objekt, und der an die Methode übergebene Verweis geht damit verloren. Diese Änderung wird nach außen hin natürlich nicht sichtbar, denn die Parametervariable p von clear(…) ist ja nur ein temporärer alternativer Name für das p aus main; eine Neuzuweisung an das clear-p ändert nicht den Verweis vom main-p. Das bedeutet, dass der Aufrufer von clear(…) – und das ist main(…) – kein neues Objekt unter sich hat. Wer den Punkt mit null initialisieren möchte, muss auf die Zustände des übergebenen Objekts direkt zugreifen, etwa so:

static void clear( Point p ) {

p.x = p.y = 0;

}

In C++ gibt es eine weitere Argumentübergabe, die sich Call by Reference nennt. Würde eine Methode wie clear(…) mit Referenzsemantik deklariert, würde die Variable p ein Synonym darstellen, also einen anderen Namen für eine Variable – in unserem Fall q. Damit würde die Zuweisung im Rumpf den Zeiger auf ein neues Objekt legen. Die swap(…)-Funktion ist ein gutes Beispiel für die Nützlichkeit von Call by Reference:

void swap( int& a, int& b ) { int tmp = a; a = b; b = tmp; }

Zeiger und Referenzen sind in C++ etwas anderes, was Spracheinsteiger leicht irritiert. Denn in C++ und auch in C hätte eine vergleichbare swap(…)-Funktion auch mit Zeigern implementiert werden können:

void swap( int *a, int *b ) { int tmp = *a; *a = *b; *b = tmp; }

Die Implementierung gibt in C(++) einen Verweis auf das Argument.

Final deklarierte Referenzparameter und das fehlende const

Wir haben gesehen, dass finale Variablen dem Programmierer vorgeben, dass er Variablen nicht wieder beschreiben darf. Final können lokale Variablen, Parametervariablen, Objektvariablen oder Klassenvariablen sein. In jedem Fall sind neue Zuweisungen tabu. Dabei ist es egal, ob die Parametervariable vom primitiven Typ oder vom Referenztyp ist. Bei einer Methodendeklaration der folgenden Art wäre also eine Zuweisung an p und auch an value verboten:

public void clear( final Point p, final int value )

Ist die Parametervariable nicht final und ein Referenztyp, so würden wir mit einer Zuweisung den Verweis auf das ursprüngliche Objekt verlieren, und das wäre wenig sinnvoll, wie wir im vorangehenden Beispiel gesehen haben. final deklarierte Parametervariablen machen im Programmcode deutlich, dass eine Änderung der Referenzvariablen unsinnig ist, und der Compiler verbietet eine Zuweisung. Im Fall unserer clear(…)-Methode wäre die Initialisierung direkt als Compilerfehler aufgefallen:

static void clear( final Point p ) {

p = new Point(); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
The final local variable p cannot be assigned.

}

Halten wir fest: Ist ein Parameter mit final deklariert, sind keine Zuweisungen möglich. final verbietet aber keine Änderungen an Objekten – und so könnte final im Sinne der Übersetzung als »endgültig« verstanden werden. Mit der Referenz des Objekts können wir sehr wohl den Zustand verändern, so wie wir es auch im letzten Beispielprogramm taten.

final erfüllt demnach nicht die Aufgabe, schreibende Objektzugriffe zu verhindern. Eine Methode mit übergebenen Referenzen kann also Objekte verändern, wenn es etwa setXXX(…)-Methoden oder Variablen gibt, auf die zugegriffen werden kann. Die Dokumentation muss also immer ausdrücklich beschreiben, wann die Methode den Zustand eines Objekts modifiziert.

In C++ gibt es für Parameter den Zusatz const, an dem der Compiler erkennen kann, dass Objektzustände nicht verändert werden sollen. Ein Programm nennt sich const-korrekt, wenn es niemals ein konstantes Objekt verändert. Dieses const ist in C++ eine Erweiterung des Objekttyps, die es in Java nicht gibt. Zwar haben die Java-Entwickler das Schlüsselwort const reserviert, doch genutzt wird es bisher nicht.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.7.5    Identität von Objekten 

Die Vergleichsoperatoren == und != sind für alle Datentypen so definiert, dass sie die vollständige Übereinstimmung zweier Werte testen. Bei primitiven Datentypen ist das einfach einzusehen und bei Referenztypen im Prinzip genauso (zur Erinnerung: Referenzen lassen sich als Pointer verstehen, was Ganzzahlen sind). Der Operator == testet bei Referenzen, ob sie übereinstimmen, also auf dasselbe Objekt verweisen. Der Operator != testet das Gegenteil, also ob sie nicht übereinstimmen, die Referenzen somit ungleich sind. Demnach sagt der Test etwas über die Identität der referenzierten Objekte aus, aber nichts darüber, ob zwei verschiedene Objekte möglicherweise den gleichen Inhalt haben. Der Inhalt der Objekte spielt bei == und != keine Rolle.

Zwei Objekte mit drei unterschiedlichen Punktvariablen p, q, r und die Bedeutung von ==:

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

Point r = new Point( 10, 10 );

System.out.println( p == q ); // true, da p und q dasselbe Objekt referenzieren

System.out.println( p == r ); // false, da p und r zwei verschiedene Punkt-

// Objekte referenzieren, die zufällig dieselben

// Koordinaten haben

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

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

3.7.6    Gleichwertigkeit und die Methode equals(…) 

Die allgemeingültige Lösung besteht darin, die Klasse festlegen zu lassen, wann Objekte gleich(wertig) sind. Dazu kann jede Klasse eine Methode equals(…) implementieren, und mit ihrer Hilfe kann sich jedes Exemplar dieser Klasse mit beliebigen anderen Objekten vergleichen. Die Klassen entscheiden immer nach Anwendungsfall, welche Attribute sie für einen Gleichheitstest heranziehen, und equals(…) liefert true, wenn die gewünschten Zustände (Objektvariablen) übereinstimmen.

Zwei nichtidentische, inhaltlich gleiche Punkt-Objekte, werden mit == und equals(…) verglichen:

Point p = new Point( 10, 10 );

Point q = new Point( 10, 10 );

System.out.println( p == q ); // false

System.out.println( p.equals(q) ); // true, da symmetrisch auch q.equals(p)

Nur equals(…) testet in diesem Fall die inhaltliche Gleichwertigkeit.

Bei den unterschiedlichen Bedeutungen müssen wir demnach die Begriffe Identität und Gleichwertigkeit (auch Gleichheit) von Objekten sorgfältig unterscheiden. Daher zeigt Tabelle 3.4 noch einmal eine Zusammenfassung:

Getestet mit

Implementierung

Identität der Referenzen

== bzw. !=

Nichts zu tun

Gleichwertigkeit der Zustände

equals(…) bzw. ! equals(…)

Abhängig von der Klasse

Tabelle 3.4    Identität und Gleichwertigkeit von Objekten

equals(…)-Implementierung von Point *

Die Klasse Point deklariert equals(…), wie die API-Dokumentation zeigt. Werfen wir einen Blick auf die Implementierung, um eine Vorstellung von der Arbeitsweise zu bekommen:

Listing 3.17    java/awt/Point.java, Ausschnitt

public class Point ... {

public int x;

public int y;

...

public boolean equals( Object obj ) {

... Point pt = (Point) obj;

return (x == pt.x) && (y == pt.y); // (*)

... }

}

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

Es gibt immer ein equals(…) – die Oberklasse Object und ihr equals(…) *

Glücklicherweise müssen wir als Programmierer nicht lange darüber nachdenken, ob eine Klasse eine equals(…)-Methode anbieten soll oder nicht. Jede Klasse besitzt sie, da die universelle Oberklasse Object sie vererbt. Wir greifen hier auf Kapitel 7, »Objektorientierte Beziehungsfragen«, vor; der Abschnitt kann aber übersprungen werden. Wenn eine Klasse also keine eigene equals(…)-Methode angibt, dann erbt sie eine Implementierung aus der Klasse Object. Diese Klasse sieht wie folgt aus:

Listing 3.18    java/lang/Object.java, Ausschnitt

public class Object {

public boolean equals( Object obj ) {

return ( this == obj );

} ...

}

Wir erkennen, dass hier die Gleichwertigkeit auf die Identität der Referenzen abgebildet wird. Ein inhaltlicher Vergleich findet nicht statt. Das ist das Einzige, was die vorgegebene Implementierung machen kann, denn sind die Referenzen identisch, sind die Objekte logischerweise auch gleich. Nur über Zustände »weiß« die Basisklasse Object nichts.

Es gibt Programmiersprachen, die für den Identitätsvergleich und Gleichwertigkeitstest eigene Operatoren anbieten. Was bei Java == und equals(…) sind, sind bei Python is und ==, bei Swift === und ==.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 19

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

In diesem Kapitel wurde das Thema Objektorientierung recht schnell eingeführt, was nicht bedeuten soll, dass OOP einfach ist. Der Weg zu gutem Design ist steinig und führt nur über viele Java-Projekte. Hilfreich sind das Lesen von fremden Programmen und die Beschäftigung mit Entwurfsmustern. Rund um UML ist ebenfalls eine Reihe von Produkten entstanden. Das Angebot beginnt bei einfachen Zeichenwerkzeugen, geht über UML-Tools mit Roundtrip-Fähigkeit und reicht bis zu kompletten CASE-Tools mit MDA-Fähigkeit.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 20

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

»Die aus der Reihe fallen, bilden den Anfang einer neuen Reihe.«

– Erhard Horst Bellermann (*1937)

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1    Einfache Feldarbeit 

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

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

  • elementare Datentypen wie int, byte, long usw.

  • Referenztypen

  • Referenztypen anderer Arrays, um mehrdimensionale Arrays zu realisieren

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.1    Grundbestandteile 

Für das Arbeiten mit Arrays müssen wir drei neue Dinge kennenlernen:

  1. das Deklarieren von Array-Variablen

  2. das Initialisieren von Array-Variablen sowie die Platzbeschaffung

  3. den Zugriff auf Arrays, den lesenden Zugriff ebenso wie den schreibenden

  1. Deklariere eine Variable randoms, die ein Array referenziert:

  2. Initialisiere die Variable mit einem Array-Objekt der Größe 10:

    randoms = new double[ 10 ];

  3. Belege das erste Element mit einer Zufallszahl und das zweite Element mit dem Doppelten des ersten Elements:

    randoms[ 0 ] = Math.random();

    randoms[ 1 ] = randoms[ 0 ] * 2;

Wir sehen, dass eckige Klammern an verschiedenen Stellen zum Einsatz kommen: einmal zur Deklaration des Typs, dann zum Aufbau des Arrays, dann zum Schreiben in Arrays und zum Lesen aus Arrays.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 4.1    Begriffe eines Arrays

Diese drei Punkte schauen wir uns nun detaillierter an.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.2    Deklaration von Array-Variablen 

Die Deklaration einer Array-Variablen ähnelt einer gewöhnlichen Deklaration, nur dass nach dem Datentyp die Zeichen [ und ] gesetzt werden.

Deklariere zwei Array-Variablen:

int[] primes;

double[] incomm;

Eine Variable wie primes hat jetzt den Typ »ist Array« und »speichert int-Elemente«, also eigentlich zwei Typen.

Die eckigen Klammern lassen sich bei der Deklaration einer Array-Variablen auch hinter den Namen setzen, doch ganz ohne Unterschied ist die Deklaration nicht. Das zeigt sich spätestens dann, wenn mehr als eine Variable deklariert wird:

int []primes,

matrix[], threeDimMatrix[][];

Das entspricht dieser Deklaration:

int primes[], matrix[][], threeDimMatrix[][][];

Damit Irrtümer dieser Art ausgeschlossen werden, sollten Sie in jeder Zeile nur eine Deklaration eines Typs schreiben. Nach reiner Java-Lehre gehören die Klammern jedenfalls hinter den Typbezeichner, so hat es der Java-Schöpfer James Gosling gewollt.

Arrays mit nichtprimitiven Elementen

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

Deklariere zwei Array-Variablen:

String[] names;

Point[] locations;

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.3    Array-Objekte mit new erzeugen 

Das Anlegen der Array-Referenzvariablen allein erzeugt noch kein Array mit einer bestimmten Länge. In Java ist das Anlegen des Arrays genauso dynamisch wie die Objekterzeugung. Ein Array muss mit dem Schlüsselwort new erzeugt werden, da Arrays Objekte sind. Die Länge des Arrays wird in eckigen Klammern angegeben. Hier kann ein beliebiger Integer-Wert stehen, auch eine Variable. Selbst 0 ist möglich. Später kann die Größe nicht mehr verändert werden.

Erzeuge ein Array für zehn Elemente:

int[] values;

values = new int[ 10 ];

Die Array-Deklaration ist auch zusammen mit der Initialisierung möglich:

double[] values = new double[ 10 ];

Die JVM initialisiert die Arrays standardmäßig: bei primitiven Werten mit 0, 0.0 oder false und bei Verweisen mit null.

Arrays sind ganz normale Objekte

Dass Arrays Objekte sind, zeigen einige Indizien:

  • Eine spezielle Form der new-Schreibweise erzeugt ein Exemplar der Array-Klasse; new erinnert uns immer daran, dass ein Objekt zur Laufzeit aufgebaut wird.

  • Ein Array-Objekt kennt das Attribut length, und auf dem Array-Objekt sind Methoden – wie clone() und alles, was java.lang.Object hat – definiert.

  • Die Operatoren == und != haben ihre Objektbedeutung: Sie vergleichen lediglich, ob zwei Variablen auf das identische Array-Objekt verweisen, aber auf keinen Fall die Inhalte der Arrays (das kann aber Arrays.equals(…)).

Der Zugriff auf die Array-Elemente über die eckigen Klammern [] lässt sich als versteckter Aufruf über geheime Methoden wie array.get(index) verstehen. Der []-Operator wird bei anderen Objekten nicht angeboten.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.4    Arrays mit { Inhalt } 

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

Wertebelegung eines Arrays bei der Initialisierung:

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

String[] strings = {

"Haus", "Maus",

"dog".toUpperCase(), // DOG

new java.awt.Point().toString(),

};

In diesem Fall wird ein Array mit passender Größe angelegt, und die Elemente, die in der Aufzählung genannt sind, werden in das Array kopiert. Innerhalb der Aufzählung kann abschließend ein Komma stehen, wie die Aufzählung bei strings demonstriert.

Auch leere Arrays ohne Inhalt sind erlaubt. Die Arrays sind initialisiert, haben aber keine Elemente und ihre length ist 0. Zwei Beispiele:

oder

int primes[] = new int[0]

Es ist möglich, dass vor der schließenden geschweiften Klammer noch ein Komma folgt, sodass es etwa int[] primes = { 2, 3, }; heißt. Das vereinfacht das Hinzufügen, ein leeres Element produziert es nicht. Selbst Folgendes ist in Java möglich: int[] primes = { , };.

Die Deklaration einer Array-Variablen mit Initialisierung funktioniert mit var nicht:

var primes = { 2, 3 }; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Array initializer needs an explicit target-type

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.5    Die Länge eines Arrays über das Attribut length auslesen 

Die Anzahl der Elemente, die ein Array aufnehmen kann, wird Größe oder Länge genannt und ist für jedes Array-Objekt in der frei zugänglichen Objektvariablen length gespeichert. length ist eine public-final-int-Variable, deren Wert entweder positiv oder null ist. Die Größe lässt sich später nicht mehr ändern.

Ein Array und die Ausgabe der Länge:

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

System.out.println( primes.length ); // 5

Array-Längen sind final

Das Attribut length eines Arrays ist nicht nur öffentlich (public) und vom Typ int, sondern natürlich auch final. Schreibzugriffe sind nicht gestattet, denn eine dynamische Vergrößerung eines Arrays ist nicht möglich; ein Schreibzugriff führt zu einem Übersetzungsfehler.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.6    Zugriff auf die Elemente über den Index 

Der Zugriff auf die Elemente eines Arrays erfolgt mithilfe der eckigen Klammern [], die hinter die Referenz an das Array-Objekt gesetzt werden. In Java beginnt ein Array beim Index 0 (und nicht bei einer frei wählbaren Untergrenze wie in Pascal). Da die Elemente eines Arrays ab 0 nummeriert werden, ist der letzte gültige Index um 1 kleiner als die Länge des Arrays. Das heißt: Bei einem Array a der Länge n ist der gültige Bereich a[0] bis a[n – 1].

Da der Zugriff auf die Variablen über einen Index erfolgt, werden diese Variablen auch indexierte Variablen genannt.

Greife auf das erste und letzte Zeichen aus dem Array zu:

char[] name = { 'C', 'h', 'r', 'i', 's' };

char first = name[ 0 ]; // C

char last = name[ name.length - 1 ]; // s

Laufe das Array der ersten Primzahlen komplett ab:

int[] primes = { 2, 3, 5, 7, 11 };

for ( int i = 0; i < primes.length; i++ ) // Index: 0 <= i < 5 = primes.length

System.out.println( primes[ i ] );

Anstatt ein Array einfach nur so abzulaufen und die Werte auszugeben, soll unser nächstes Programm den Mittelwert einer Zahlenfolge berechnen und ausgeben:

Listing 4.1    src/main/java/com/tutego/insel/array/PrintTheAverage.java

public class PrintTheAverage {

public static void main( String[] args ) {

double[] numbers = { 1.9, 7.8, 2.4, 9.3 };

double sum = 0;

for ( int i = 0; i < numbers.length; i++ )

sum += numbers[ i ];

double avg = sum / numbers.length;

System.out.println( avg ); // 5.35

}

}

Das Array muss mindestens ein Element besitzen, sonst gibt es bei der Division durch 0 eine Ausnahme.

Über den Typ des Index *

Innerhalb der eckigen Klammern steht ein positiver Ganzzahl-Ausdruck vom Typ int, der sich zur Laufzeit berechnen lassen muss. long-Werte, boolean, Gleitkommazahlen oder Referenzen sind nicht möglich; durch int verbleiben aber mehr als zwei Milliarden Elemente. Bei Gleitkommazahlen bliebe die Frage nach der Zugriffstechnik. Hier müssten wir den Wert auf ein Intervall herunterrechnen.

Der Index eines Arrays muss vom Typ int sein, das schließt Anpassungen von byte, short und char ein. Günstig ist ein Index vom Typ char, zum Beispiel als Laufvariable, wenn Arrays von Zeichen generiert werden:

char[] alphabet = new char[ 'z' - 'a' + 1 ]; // 'a' entspricht 97 und 'z' 122

for ( char c = 'a'; c <= 'z'; c++ )

alphabet[ c - 'a' ] = c; // alphabet[0]='a', alphabet[1]='b', usw.

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

Strings sind keine Arrays *

Ein Array von char-Zeichen hat einen ganz anderen Typ als ein String-Objekt. Während bei Arrays eckige Klammern erlaubt sind, bietet die String-Klasse keinen Zugriff auf Zeichen über []. Die Klasse String bietet jedoch einen Konstruktor an, sodass aus einem Array mit Zeichen ein String-Objekt erzeugt werden kann. Alle Zeichen des Arrays werden kopiert, sodass anschließend Array und String keine Verbindung mehr besitzen. Dies bedeutet: Wenn sich das Array ändert, ändert sich der String nicht automatisch mit. Das kann er auch nicht, da Strings unveränderlich sind.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.7    Typische Array-Fehler 

Beim Zugriff auf ein Array-Element können Fehler auftreten. Zunächst einmal kann das Array-Objekt fehlen, sodass die Referenzierung fehlschlägt.

Der Compiler bemerkt den folgenden Fehler nicht, und die Strafe ist eine NullPointerException zur Laufzeit:

int[] array = null;

array[ 1 ] = 1; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
NullPointerException

Weitere Fehler können im Index begründet sein. Ist der Index negativ oder zu groß, dann gibt es eine IndexOutOfBoundsException. Jeder Zugriff auf das Array wird zur Laufzeit getestet, auch wenn der Compiler durchaus einige Fehler finden könnte.

Bei folgenden Zugriffen könnte der Compiler theoretisch Alarm schlagen, was aber zumindest der Standard-Compiler nicht tut. Der Grund ist, dass der Zugriff auf die Elemente auch mit einem ungültigen Index syntaktisch völlig in Ordnung ist.

int[] array = new int[ 100 ];

array[ -10 ] = 1; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Fehler zur Laufzeit, nicht zur Compilezeit

array[ 100 ] = 1; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Fehler zur Laufzeit, nicht zur Compilezeit

Wird die IndexOutOfBoundsException nicht abgefangen, bricht das Laufzeitsystem das Programm mit einer Fehlermeldung ab. Dass die Array-Grenzen überprüft werden, ist Teil von Javas Sicherheitskonzept und lässt sich nicht abstellen. Es ist aber heute kein großes Performance-Problem mehr, da die Laufzeitumgebung nicht jeden Index prüfen muss, um sicherzustellen, dass ein Block mit Array-Zugriff korrekt ist.

Spielerei: Der Index und das Inkrement *

Wir haben beim Inkrement schon ein Phänomen wie i = i++ betrachtet. Ebenso ist die Anweisung bei einem Array-Zugriff zu behandeln:

Bei der Position array[i] wird i gesichert und anschließend die Zuweisung vorgenommen. Wenn wir eine Schleife um das Array herum konstruieren, erweitern wir dies zu einer Initialisierung:

int[] array = new int[ 4 ];

int i = 0;

while ( i < array.length )

array[ i ] = i++;

Die Initialisierung ergibt 0, 1, 2 und 3. Von der Anwendung ist wegen mangelnder Übersichtlichkeit abzuraten.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.8    Arrays an Methoden übergeben 

Verweise auf Arrays lassen sich bei Methoden genauso übergeben wie Verweise auf ganz normale Objekte. In der Deklaration heißt es dann zum Beispiel foo(int[] val) statt foo(String val).

Wir haben bereits den Mittelwert einer Zahlenreihe ermittelt. Die Logik dafür ist perfekt in eine Methode ausgelagert:

Listing 4.2    src/main/java/com/tutego/insel/array/Avg1.java

public class Avg1 {

static double avg( double[] array ) {

double sum = 0;

for ( int i = 0; i < array.length; i++ )

sum += array[ i ];

return sum / array.length;

}

public static void main( String[] args ) {

double[] numbers = { 2, 3, 4 };

System.out.println( avg( numbers ) ); // 3.0

}

}

null-Referenzen prüfen

Referenzen bringen immer das Problem mit sich, dass sie null sein können. Syntaktisch gültig ist ein Aufruf von avg(null). Daher sollte eine Implementierung auf null testen und ein falsches Argument melden, etwa so:

if ( array == null || array.length == 0 )

throw new IllegalArgumentException( "Array null oder leer" );

Zu den Details siehe Kapitel 8, »Ausnahmen müssen sein«.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.9    Mehrere Rückgabewerte * 

Wenn wir in Java Methoden schreiben, dann haben sie über return höchstens einen Rückgabewert. Wollen wir aber mehr als einen Wert zurückgeben, müssen wir eine andere Lösung suchen. Zwei Ideen lassen sich verwirklichen:

  • Behälter wie Arrays oder andere Sammlungen fassen Werte zusammen und liefern sie als Rückgabe.

  • Spezielle Behälter werden übergeben, in denen die Methode Rückgabewerte platziert; eine return-Anweisung ist nicht mehr nötig.

Betrachten wir eine statische Methode, die für zwei Zahlen die Summe und das Produkt als Array liefert:

Listing 4.3    src/main/java/com/tutego/insel/array/MultipleReturnValues.java, Ausschnitt

static int[] productAndSum( int a, int b ) {

return new int[]{ a * b, a + b };

}

public static void main( String[] args ) {

System.out.println( productAndSum(9, 3)[ 1 ] );

}

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.1.10    Vorinitialisierte Arrays 

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

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

Java erlaubt es nicht, Array-Inhalte nach der Variablendeklaration zu initialisieren oder das Array auch ohne Variable als Argument zu nutzen:

primes = { 2, 5, 7, 11, 13 }; //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

avg( { 1.23, 4.94, 9.33, 3.91, 6.34 } ); //

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden
Compilerfehler

Ein Versuch wie dieser schlägt mit der Compilermeldung »Array constants can only be used in initializers« fehl.

Zur Lösung gibt es zwei Ansätze. Der erste ist die Einführung einer neuen Variablen, hier tmpprimes:

int[] primes;

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

primes = tmpprimes;

Als zweiten Ansatz gibt es eine Variante der new-Schreibweise, die durch ein Paar eckiger Klammern erweitert wird. Es folgen in geschweiften Klammern die Initialwerte des Arrays. Die Größe des Arrays entspricht genau der Anzahl der Werte. Für die oberen Beispiele ergibt sich folgende Schreibweise:

int[] primes;

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

Diese Notation ist auch bei Methodenaufrufen sehr praktisch, wenn Arrays übergeben werden:

avg( new double[]{ 1.23, 4.94, 9.33, 3.91, 6.34 } );

Da hier ein initialisiertes Array mit Werten gleich an die Methode übergeben wird und keine zusätzliche Variable benutzt wird, heißt diese Art der Arrays anonyme Arrays. Eigentlich gibt es auch sonst anonyme Arrays, wie new int[2000].length zeigt, doch wird in diesem Fall das Array nicht mit eigenen Werten initialisiert.

Die Wahrheit über die Array-Initialisierung *

So schön die kompakte Initialisierung der Array-Elemente ist, so laufzeit- und speicherintensiv ist sie auch. Da Java eine dynamische Sprache ist, passt das Konzept der Array-Initialisierung nicht ganz in das Bild. Daher wird die Initialisierung auch erst zur Laufzeit durchgeführt.

Unser Primzahl-Array

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

wird vom Java-Compiler umgeformt und analog zu Folgendem behandelt:

int[] primes = new int[ 6 ];

primes[ 0 ] = 2;

primes[ 1 ] = 3;

primes[ 2 ] = 5;

primes[ 3 ] = 7;

primes[ 4 ] = 11;

primes[ 5 ] = 13;

Erst nach kurzem Überlegen wird das Ausmaß der Umsetzung sichtbar: Zunächst ist es der Speicherbedarf für die Methoden. Ist das Array primes in einer Methode deklariert und mit Werten initialisiert, kostet die Zuweisung Laufzeit, da wir viele Zugriffe haben, die auch alle schön durch die Indexüberprüfung gesichert sind. Da zudem der Bytecode für eine einzelne Methode wegen diverser Beschränkungen in der JVM nur beschränkt lang sein darf, kann dieser Platz für richtig große Arrays schnell erschöpft sein. Daher ist davon abzuraten, etwa Bilder oder große Tabellen im Programmcode zu speichern. Unter C war es populär, ein Programm einzusetzen, das eine Datei in eine Folge von Array-Deklarationen verwandelte. Ist dies in Java wirklich nötig, sollten wir Folgendes in Betracht ziehen:

  • Wir verwenden ein statisches Array (eine Klassenvariable), sodass das Array nur einmal während des Programmlaufs initialisiert werden muss.

  • Liegen die Werte im Byte-Bereich, können wir sie in einen String konvertieren und später den String in ein Array umwandeln. Das ist eine sehr clevere Methode, Binärdaten einfach unterzubringen.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 21

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

for-Schleifen laufen oft Arrays oder Datenstrukturen ab. Bei der Berechnung des Mittelwerts konnten wir das ablesen:

double sum = 0;

for ( int i = 0; i < array.length; i++ )

sum += array[ i ];

double arg = sum / array.length;

Die Schleifenvariable i hat lediglich als Index ihre Berechtigung; nur damit lässt sich das Element an einer bestimmten Stelle im Array ansprechen.

Weil das komplette Durchlaufen von Arrays häufig ist, gibt es eine Abkürzung für solche Iterationen:

for ( Typ Bezeichner : Array )

...

Die erweiterte Form der for-Schleife löst sich vom Index und erfragt jedes Element des Arrays. Das können Sie sich als Durchlauf einer Menge vorstellen, denn der Doppelpunkt liest sich als »in«. Rechts vom Doppelpunkt steht immer ein Array oder, wie wir später sehen werden, etwas vom Typ Iterable, wie eine Datenstruktur. Links wird eine neue lokale Variable deklariert, die später beim Ablauf jedes Element der Sammlung annehmen wird.

Die Berechnung des Durchschnitts lässt sich nun umschreiben. Die statische Methode avg(…) soll mit dem erweiterten for über die Schleife laufen, anstatt den Index selbst hochzuzählen. Eine Ausnahme zeigt an, ob der Array-Verweis null ist oder das Array keine Elemente enthält:

Listing 4.4    src/main/java/com/tutego/insel/array/Avg2.java, avg()

static double avg( double[] array ) {

if ( array == null || array.length == 0 )

throw new IllegalArgumentException( "Array null oder leer" );

double sum = 0;

for ( double n : array )

sum += n;

return sum / array.length;

}

Zu lesen ist die for-Zeile demzufolge als »Für jedes Element n vom Typ double in array tue …«. Eine Variable für den Schleifenindex ist nicht mehr nötig.

Anonyme Arrays in der erweiterten for-Schleife nutzen

Rechts vom Doppelpunkt lässt sich auf die Schnelle ein Array aufbauen, über welches das erweiterte for dann laufen kann:

for ( int prime : new int[]{ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31 } )

System.out.println( prime );

Das ist praktisch, um über eine feste Menge von Werten zu laufen. Das funktioniert auch für Objekte, etwa Strings:

for ( String name : new String[]{ "Cherry", "Gracel", "Fe" } )

System.out.println( name );

Rechts vom Doppelpunkt kann ein Array oder ein Objekt vom Typ Iterable stehen:

for ( String name : Arrays.asList( "Cherry ", "Gracel", "Fe" ) )

System.out.println( name );

Arrays.asList(…) erzeugt kein Array als Rückgabe, sondern baut aus der variablen Argumentliste eine Sammlung auf, die von einem speziellen Typ Iterable ist; alles, was Iterable ist, kann die erweiterte for-Schleife ablaufen. Wir kommen später noch einmal darauf zu sprechen. Unabhängig vom erweiterten for hat die Nutzung von Arrays.asList(…) noch einen anderen Vorteil, etwa bei Ist-Element-von-Anfragen, etwa so:

if ( Arrays.asList( 1, 2, 6, 7, 8, 10 ).contains( number ) )

Mehr zu der Methode folgt in Abschnitt 4.6, »Die Klasse Arrays zum Vergleichen, Füllen, Suchen und Sortieren nutzen«.

Umsetzung und Einschränkung

Intern setzt der Compiler diese erweiterte for-Schleife ganz klassisch um, sodass der Bytecode unter beiden Varianten gleich ist. Nachteile dieser Variante sind jedoch:

  • Das erweiterte for läuft standardmäßig immer das ganze Array ab. Ein Anfang- und ein Ende-Index können nicht ausdrücklich gesetzt werden.

  • Die Ordnung ist immer »von vorn nach hinten«.

  • Die Schrittlänge ist immer 1.

  • Der Index ist nicht sichtbar.

  • Die Schleife liefert ein Element, kann aber nicht in das Array schreiben.

Abbrechen lässt sich die Schleife mit einem break. Bestehen andere Anforderungen, kann weiterhin nur eine klassische for-Schleife helfen.

Beispiel: Arrays mit Strings durchsuchen

In unserem ersten Beispiel soll ein nichtprimitives Array Strings referenzieren und später schauen, ob eine Benutzereingabe im Array ist. String-Vergleiche lassen sich mit equals(…) realisieren:

Listing 4.5    src/main/java/com/tutego/insel/array/UserInputInStringArray.java, main()

String[] validInputs = { "Banane", "Apfel", "Kirsche" };

boolean found = false;

while ( ! found ) {

String input = new Scanner( System.in ).nextLine();

for ( String s : validInputs )

if ( s.equals( input ) ) {

found = true;

break;

}}

System.out.println( "Gültiges Früchtchen eingegeben" );

Zur Initialisierung des Arrays nutzt das Programm eine kompakte Variante, die drei Dinge vereint: den Aufbau eines Array-Objekts (mit Platz für drei Referenzen), die Initialisierung des Array-Objekts mit den drei Objektreferenzen und schlussendlich die Initialisierung der Variablen validInputs mit dem neuen Array – alles in einer Anweisung.

Für die Suche kommt das erweiterte for zum Einsatz, das in einer Schleife eingebettet ist, die genau dann endet, wenn das Flag found gleich true wird. Wenn wir nie einen korrekten String eingeben, wird die äußere Schleife auch nie enden. Wenn wir einen Eintrag finden, kann das flag gesetzt werden und break die Array-Schleife früher verlassen.

Zufällige Spielerpositionen erzeugen

Im zweiten Beispiel sollen fünf zufällig initialisierte Punkte in einem Array abgelegt werden. Die Punkte sollen Spieler repräsentieren.

Zunächst benötigen wir ein Array:

Point[] players = new Point[ 5 ];

Die Deklaration schafft Platz für fünf Verweise auf Punkt-Objekte, aber kein einziges Point-Objekt ist angelegt. Standardmäßig werden die Array-Elemente mit der null-Referenz initialisiert, sodass System.out.println(players[0]) die Ausgabe »null« auf den Bildschirm bringen würde. Bei null wollen wir es nicht belassen, daher müssen die einzelnen Array-Plätze etwa mit players[0] = new Point() initialisiert werden.

Zufallszahlen erzeugt die mathematische Methode Math.random(). Da die statische Methode jedoch Fließkommazahlen zwischen 0 (inklusiv) und 1 (exklusiv) liefert, werden die Zahlen zunächst durch Multiplikation frisiert und dann abgeschnitten.

Im letzten Schritt geben wir ein Raster auf dem Bildschirm aus, in dem zwei ineinander verschachtelte Schleifen alle x/y-Koordinaten des gewählten Bereichs ablaufen und dann ein & setzen, wenn der Punkt einen Spieler trifft.

Das Programm als Ganzes:

Listing 4.6    src/main/java/com/tutego/insel/array/FivePlayers.java, main()

Point[] players = new Point[ 5 ];

for ( int i = 0; i < players.length; i++ )

players[ i ] = new Point( (int)(Math.random() * 40),

(int)(Math.random() * 10) );

for ( int y = 0; y < 10; y++ ) {

for ( int x = 0; x < 40; x++ )

if ( Arrays.asList( players ).contains( new Point(x,y) ) )

System.out.print( "&" );

else

System.out.print( "." );

System.out.println();

}

Der Ausdruck Arrays.asList(players).contains(new Point(x, y)) testet, ob irgendein Punkt im Array players gleich dem Punkt mit den x/y-Koordinaten ist.

Die Ausgabe erzeugt zum Beispiel Folgendes:

.......................................................&........................&.....................................................................................................................................................&....................................................&....................&.......................................................

........................................

Während die erweiterte for-Schleife gut das Array ablaufen kann, funktioniert das zur Initialisierung nicht, denn das erweiterte for ist nur zum Lesen gut. Elementinitialisierungen funktionieren bei Arrays nur mit players[i]=…, und dazu ist eben eine klassische for-Schleife mit dem Index nötig.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 22

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Bei vielen Methoden ist klar, wie viele Argumente exakt übergeben werden müssen; einer Methode Math.random() darf nichts übergeben werden, und bei Math.sin(double) ist genau ein Argument gültig.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.3.1    System.out.printf(…) nimmt eine beliebige Anzahl von Argumenten an 

Es gibt auch Methoden, bei denen die Anzahl gültiger Argumente prinzipiell unbeschränkt ist. Ein Beispiel ist printf(String, …), das verpflichtend einen String als Erstes erwartet, dann aber frei darin ist, was sonst übergeben wird. Gültig sind zum Beispiel:

Aufruf

Variable Argumentliste

System.out.printf("%n")

Ist leer.

System.out.printf("%s", "Eins")

Besteht aus nur einem Element: "Eins".

System.out.printf("%s,%s,%s", "1", "2", "3")

Besteht aus drei Elementen: "1", "2", "3".

Tabelle 4.1    Gültige Aufrufe von »printf(…)«

Um die Anzahl der Parameter beliebig zu gestalten, sieht Java Methoden mit variabler Argumentanzahl vor, Varargs genannt – in anderen Programmiersprachen heißen sie variadische Funktion. Die Methode printf(formatierungsstring, arg1, args2, arg3, …) ist so eine Varargs-Methode.

class java.io.PrintStream extends FilterOutputStream

implements Appendable, Closeable

  • PrintStream printf(String format, Object… args)

    Nimmt eine beliebige Liste von Argumenten an und formatiert sie nach dem gegebenen Formatierungs-String format. Der Formatierungs-String bestimmt, wie viele Argumente nötig sind. Der Compiler wertet den String aber nicht aus, kann also die Korrektheit – dass die Anzahl stimmt – auch zur Compilezeit nicht prüfen.

Eine Methode mit variabler Argumentanzahl nutzt die Ellipse (...) zur Verdeutlichung, dass eine beliebige Anzahl Argumente angegeben werden darf. Dazu zählt auch die Angabe keines Elements. Der Typ fällt dabei aber nicht unter den Tisch; er wird ebenfalls angegeben.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.3.2    Durchschnitt finden von variablen Argumenten 

Wir haben vorher eine Methode avg(double[] array) geschrieben, die den arithmetischen Mittelwert von Werten berechnet. Den Parametertyp können wir nun ändern in avg(double array), sodass die Methode einfach mit variablen Argumenten gefüllt werden kann.

Ausprogrammiert sieht das wie folgt aus; es gibt nur eine kleine Änderung von [] in …, sonst ändert sich an der Implementierung nichts:

Listing 4.7    src/main/java/com/tutego/insel/array/AvgVarArgs.java, Ausschnitt

public class AvgVarArgs {

static double avg( double... array ) { /* Implementierung wie vorher */ }

public static void main( String[] args ) {

System.out.println( avg(1, 2, 9, 3) ); // 3.75

}

}

Werden variable Argumentlisten in der Signatur definiert, so dürfen sie nur den letzten Parameter bilden; andernfalls könnte der Compiler bei den Parametern nicht unbedingt zuordnen, was nun ein Vararg und was schon der nächste gefüllte Parameter ist. Das bedingt automatisch, dass es nur maximal ein Vararg in der Parameterliste geben kann.

Der Zusammenhang zwischen Vararg und Array

Eine Methode mit Vararg ist im Kern eine Methode mit einem Array als Parametertyp. Im Bytecode steht nicht wirklich avg(double… array), sondern avg(double[] array) mit der Zusatzinfo, dass array ein Vararg ist, damit der Compiler beliebig viele Argumente und nicht ausschließlich ein double[]-Array als Argument erlaubt.

Der Nutzer kann eine Varargs-Methode aufrufen, ohne ein Array für die Argumente explizit zu definieren. Er bekommt auch gar nicht mit, dass der Compiler im Hintergrund ein Array mit vier Elementen angelegt hat. So generiert der Compiler aus

System.out.println( avg(1, 2, 9, 3) );

Folgendes:

System.out.println( avg( new double[] { 1, 2, 9, 3 } ) );

An der Schreibweise lässt sich gut ablesen, dass wir ein Array auch von Hand übergeben können:

double[] values = { 1, 2, 9, 3 };

System.out.println( avg( values ) );

Da Varargs als Arrays umgesetzt werden, sind überladene Varianten wie avg(int… array) und avg(int[] array), also einmal mit einem Vararg und einmal mit einem Array, nicht möglich. Besser ist es hier, immer eine Variante mit Varargs zu nehmen, da sie mächtiger ist. Einige Autoren schreiben auch die Einstiegsmethode main(String[] args) mit variablen Argumenten, also main(String… args). Das ist gültig, denn im Bytecode steht ja ein Array.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.3.3    Varargs-Designtipps * 

  • Hat eine Methode nur einen Array-Parameter und steht er noch am Ende, so kann er relativ einfach durch ein Vararg ersetzt werden. Das gibt dem Aufrufer die komfortable Möglichkeit, eine kompaktere Syntax zu nutzen. Unsere main(String[] args)-Methode kann auch als main(String... args) deklariert werden, sodass der main(…)-Methode bei Tests einfach variable Argumente übergeben werden können.

  • Muss eine Mindestanzahl von Argumenten garantiert werden – bei max(…) sollten das mindestens zwei sein –, ist es besser, eine Deklaration wie folgt zu nutzen: max(int first, int second, int... remaining).

  • Aus Performance-Gründen ist es empfehlenswert, Methoden mit häufigen Parameterlistengrößen als feste Methoden anzubieten, etwa max(double, double), max(double, double, double) und dann max(double...). Der Compiler wählt automatisch immer die passende Methode aus, für zwei oder drei Parameter sind keine temporären Array-Objekte nötig, und die automatische Speicherbereinigung muss nichts wegräumen.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 23

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Java realisiert mehrdimensionale Arrays durch Arrays von Arrays. Sie können etwa für die Darstellung von mathematischen Matrizen oder Rasterbildern Verwendung finden. Dieser Abschnitt lehrt, wie Objekte für mehrdimensionale Arrays initialisiert, aufgebaut und genutzt werden.

Mehrdimensionale Array-Objekte mit new aufbauen

Die folgende Zeile deklariert ein zweidimensionales Array mit Platz für insgesamt 32 Zellen, die in vier Zeilen und acht Spalten angeordnet sind:

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

Obwohl mehrdimensionale Arrays im Prinzip Arrays mit Arrays als Elementen sind, lassen sie sich leicht deklarieren.

Zwei alternative Deklarationen (die Position der eckigen Klammern ist verschoben) sind:

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

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

Es empfiehlt sich, alle eckigen Klammern hinter den Typ zu setzen.

Anlegen und Initialisieren in einem Schritt

Ebenso wie bei eindimensionalen Arrays lassen sich mehrdimensionale Arrays gleich beim Anlegen initialisieren:

int[][] A3x2 = { {1, 2}, {2, 3}, {3, 4} };

int[][] B = { {1, 2}, {2, 3, 4}, {5} };

Der zweite Fall lässt erkennen, dass das Array nicht unbedingt rechteckig sein muss. Dazu gleich mehr.

Zugriff auf Elemente

Einzelne Elemente spricht der Ausdruck A[i][j] an. Der Zugriff erfolgt mit so vielen Klammerpaaren, wie die Dimension des Arrays angibt.

Der Aufbau von zweidimensionalen Arrays (und der Zugriff auf sie) ist mit einer Matrix bzw. Tabelle vergleichbar. Dann lässt sich der Eintrag im Array a[x][y] in folgender Tabelle ablesen:

a[0][0] a[0][1] a[0][2] a[0][3] a[0][4] a[0][5] ...

a[1][0] a[1][1] a[1][2] a[1][3] a[1][4] a[1][5]

a[2][0] a[2][1] a[2][2] a[2][3] a[2][4] a[2][5]

...

length bei mehrdimensionalen Arrays

Nehmen wir eine Buchstabendefinition wie die folgende:

char[][] letter = { { ' ', '#', ' ' },

{ '#', ' ', '#' },

{ '#', ' ', '#' },

{ '#', ' ', '#' },

{ ' ', '#', ' ' } };

Dann können wir length auf zwei verschiedene Weisen anwenden:

  • letter.length ergibt 5, denn es gibt fünf Zeilen.

  • letter[0].length ergibt 3 – genauso wie letter[1].length usw. –, weil jedes Unter-Array die Größe 3 hat.

Zweidimensionale Arrays mit ineinander verschachtelten Schleifen ablaufen

Um den Buchstaben unseres Beispiels auf dem Bildschirm auszugeben, nutzen wir zwei ineinander verschachtelte Schleifen:

for ( int line = 0; line < letter.length; line++ ) {

for ( int column = 0; column < letter[line].length; column++ )

System.out.print( letter[line][column] );

System.out.println();

}

Fassen wir das Wissen zu einem Programm zusammen, das vom Benutzer eine Zahl erfragt und diese Zahl in Binärdarstellung ausgibt. Wir drehen die Buchstaben um 90 Grad im Uhrzeigersinn, damit wir uns nicht damit beschäftigen müssen, die Buchstaben horizontal nebeneinanderzulegen.

Listing 4.8    src/main/java/com/tutego/insel/array/BinaryBanner.java

package com.tutego.insel.array;

import java.util.Scanner;

public class BinaryBanner {

static void printLetter( char[][] letter ) {

for ( int column = 0; column < letter[0].length; column++ ) {

for ( int line = letter.length - 1; line >= 0; line-- )

System.out.print( letter[line][column] );

System.out.println();

}

System.out.println();

}

static void printZero() {

char[][] zero = { { ' ', '#', ' ' },

{ '#', ' ', '#' },

{ '#', ' ', '#' },

{ '#', ' ', '#' },

{ ' ', '#', ' ' } };

printLetter( zero );

}

static void printOne() {

char[][] one = { { ' ', '#' },

{ '#', '#' },

{ ' ', '#' },

{ ' ', '#' },

{ ' ', '#' } };

printLetter( one );

}

public static void main( String[] args ) {

int input = new Scanner( System.in ).nextInt();

String bin = Integer.toBinaryString( input );

System.out.printf( "Banner für %s (binär %s):%n", input, bin );

for ( int i = 0; i < bin.length(); i++ )

switch ( bin.charAt( i ) ) {

case '0': printZero(); break;

case '1': printOne(); break;

} }

}

Die Methode printLetter(char[][]) bekommt als Argument das zweidimensionale Array und läuft anders ab als im ersten Fall, um die Rotation zu realisieren. Mit der Eingabe »2« gibt es folgende Ausgabe:

Banner für 2 (binär 10): ###### #### #

###

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.4.1    Nichtrechteckige Arrays * 

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

Ein dreieckiges Array mit Zeilen der Länge 1, 2 und 3:

int[][] a = new int[ 3 ][];

for ( int i = 0; i < 3; i++ )

a[ i ] = new int[ i + 1 ];

Initialisierung der Unter-Arrays

Wenn wir ein mehrdimensionales Array deklarieren, erzeugen versteckte Schleifen automatisch die inneren Arrays. Bei

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

erzeugt die Laufzeitumgebung die passenden Unter-Arrays automatisch. Dies ist bei

int[][] a = new int[ 3 ][];

nicht so. Hier müssen wir selbst die Unter-Arrays initialisieren, bevor wir auf die Elemente zugreifen:

for ( int i = 0; i < a.length; i++ )

a[ i ] = new int[ 4 ];

PS: int[][] m = new int[][4]; funktioniert natürlich nicht!

Es gibt verschiedene Möglichkeiten, ein mehrdimensionales Array zu initialisieren:

int[][] A3x2 = { {1,2}, {2,3}, {3,4} };

oder

int[][] A3x2 = new int[][]{ {1,2}, {2,3}, {3,4} };

oder

int[][] A3x2 = new int[][]{ new int[]{1,2}, new int[]{2,3}, new int[]{3,4} };

Das pascalsche Dreieck

Das folgende Beispiel zeigt eine weitere Anwendung von nichtrechteckigen Arrays, in der das pascalsche Dreieck nachgebildet wird. Das Dreieck ist so aufgebaut, dass die Elemente unter einer Zahl genau die Summe der beiden direkt darüberstehenden Zahlen bilden. Die Ränder sind mit Einsen belegt.

Listing 4.9    Das pascalsche Dreieck

1

1 1

1 2 1

1 3 3 1

1 4 6 4 1

1 5 10 10 5 1

1 6 15 20 15 6 1

In der Implementierung wird zu jeder Ebene dynamisch ein Array mit der passenden Länge angefordert. Die Ausgabe tätigt printf(…) mit einigen Tricks mit dem Formatspezifizierer, da wir auf diese Weise ein führendes Leerzeichen bekommen:

Listing 4.10    src/main/java/com/tutego/insel/array/PascalsTriangle.java, main()

class PascalsTriangle {

public static void main( String[] args ) {

int[][] triangle = new int[7][];

for ( int row = 0; row < triangle.length; row++ ) {

System.out.print( new String( new char[(14 - row * 2)] ).replace( '\0', ' ' ) );

triangle[row] = new int[row + 1];

for ( int col = 0; col <= row; col++ ) {

if ( (col == 0) || (col == row) )

triangle[row][col] = 1;

else

triangle[row][col] = triangle[row - 1][col - 1] +

triangle[row - 1][col];

System.out.printf( "%3d ", triangle[row][col] );

}

System.out.println();

} }

}

Die Anweisung new String( new char[(14 - row * 2)] ).replace( '\0', ' ' ) produziert Einrückungen und greift eine fortgeschrittene API auf. (14 - row * 2) ist die Größe des standardmäßig mit 0 initialisierten Arrays, das dann an den Konstruktor der Klasse String übergeben wird, der wiederum ein String-Objekt aus dem char-Array aufbaut. Die replace(…)-Methode auf dem frischen String-Objekt führt wieder zu einem neuen String-Objekt, in dem alle '\0' -Werte durch Leerzeichen ersetzt sind.

Andere Anwendungen

Mit zweidimensionalen Arrays ist die Verwaltung von symmetrischen Matrizen einfach, da eine solche Matrix symmetrisch zur Diagonalen gleiche Elemente enthält. Daher kann entweder die obere oder die untere Dreiecksmatrix entfallen. Besonders nützlich ist der Einsatz dieser effizienten Speicherform für Adjazenzmatrizen bei ungerichteten Graphen.

Eine ungewöhnliche Syntax in Java erlaubt es, bei Array-Rückgaben das Paar eckiger Klammern auch hinter den Methodenkopf zu stellen, also statt

static int[] productAndSum( int a, int b )

alternativ Folgendes zu schreiben:

static int productAndSum( int a, int b )[]

Das wird nicht empfohlen. Selbst so etwas wie int[] transposeMatrix(int[][] m)[] ist möglich, wird aber sinnvollerweise als int[][] transposeMatrix(int[][] matrix) geschrieben.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 24

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Wollen wir eine Kopie eines Arrays mit gleicher Größe und gleichem Elementtyp schaffen, so nutzen wir dazu die Objektmethode clone(). Sie klont – in unserem Fall kopiert – die Elemente des Array-Objekts in ein neues.

Listing 4.11    src/main/java/com/tutego/insel/array/CloneDemo.java, Ausschnitt, Teil 1

int[] sourceArray = new int[ 6 ];

sourceArray[ 0 ] = 4711;

int[] targetArray = sourceArray.clone();

System.out.println( targetArray.length ); // 6

System.out.println( targetArray[ 0 ] ); // 4711

Im Fall von geklonten Objekt-Arrays ist es wichtig, zu verstehen, dass die Kopie flach ist. Die Verweise aus dem ersten Array kopiert clone() in das neue Array, es klont aber die referenzierten Objekte selbst nicht. Bei mehrdimensionalen Arrays wird also nur die erste Dimension kopiert, Unter-Arrays werden somit gemeinsam genutzt:

Listing 4.12    src/main/java/com/tutego/insel/array/CloneDemo.java, Ausschnitt, Teil 2

Point[] pointArray1 = { new Point(1, 2), new Point(2, 3) };

Point[] pointArray2 = pointArray1.clone();

System.out.println( pointArray1[ 0 ] == pointArray2[ 0 ] ); // true

Die letzte Zeile zeigt anschaulich, dass die beiden Arrays dasselbe Point-Objekt referenzieren; die Kopie ist flach, aber nicht tief.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.5.2    Warum »können« Arrays so wenig? 

Arrays haben ein Attribut length und eine Methode clone() sowie die von Object geerbten Methoden. Das ist nicht viel. Für Arrays gibt es keine Klassendeklaration, also auch keine .class-Datei, die Methoden deklariert. Es gibt auch keinen Java-Code wie bei anderen Klassen wie String, Arrays oder System, die mit Javadoc dokumentiert sind und folglich in der API-Dokumentation auftauchen. Da es unendlich viele Array-Typen gibt – hinter jedem beliebigen Typ kann [] gesetzt werden –, würde das auch unendlich viele Klassendeklarationen nach sich ziehen.

Das, was Arrays können, ist in der Java-Sprachdefinition (JLS) festgeschrieben. Bei jeder neuen Methode oder Änderung müsste die JLS angepasst werden, was unpraktisch ist. Natürlich wären Methoden wie sort(…), indexOf(…) auf Array-Objekten praktisch, aber zu viel Eingebautes in der Sprache ist nicht gut und auch die Dokumentation solcher Methoden will Oracle aus der JLS heraushalten, und so sind die Methoden in eine Extraklasse Arrays gewandert.

Die Länge ist immer die Kapazität des Arrays. Es gibt keine Information darüber, wie viele Array-Elemente von uns in der Nutzung sind. Wenn das Array zum Beispiel 10 Elemente groß ist, aber nur die ersten 2 Positionen von uns verwendet werden, so müssen wir uns das selbst merken.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.5.3    Array-Inhalte kopieren 

Eine weitere nützliche statische Methode ist System.arraycopy(…). Sie kann auf zwei Arten arbeiten:

  • auf zwei schon existierenden Arrays: Ein Teil eines Arrays wird in ein anderes Array kopiert. arraycopy(…) eignet sich dazu, sich vergrößernde Arrays zu implementieren, indem zunächst ein neues größeres Array angelegt wird und anschließend die alten Array-Inhalte in das neue Array kopiert werden.

  • auf dem gleichen Array: So lässt sich die Methode dazu verwenden, Elemente eines Arrays um bestimmte Positionen zu verschieben. Die Bereiche können sich durchaus überlappen.

Um zu zeigen, dass arraycopy(…) auch innerhalb des eigenen Arrays kopiert, sollen alle Elemente bis auf eines im Array f nach links und nach rechts bewegt werden:

System.arraycopy( f, 1, f, 0, f.length - 1 ); // links

System.arraycopy( f, 0, f, 1, f.length - 1 ); // rechts

Hier bleibt jedoch ein Element doppelt!

final class java.lang.System

  • static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

    Kopiert length Einträge des Arrays src ab der Position srcPos in ein Array dest ab der Stelle destPos. Der Typ des Arrays ist egal, es muss nur in beiden Fällen der gleiche Typ sein. Die Methode arbeitet für große Arrays schneller als eine eigene Kopierschleife.

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Abbildung 4.2    Kopieren der Elemente von einem Array in ein anderes

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 25

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

Die Klasse java.util.Arrays deklariert nützliche statische Methoden für den Umgang mit Arrays. So bietet sie Möglichkeiten zum Vergleichen, Sortieren und Füllen von Arrays sowie zur binären Suche.

String-Repräsentation eines Arrays

Nehmen wir an, wir haben es mit einem Array von Hundenamen zu tun, das wir auf dem Bildschirm ausgeben wollen:

Listing 4.13    DogArrayToString, main()

String[] dogs = {

"Flocky Fluke", "Frizzi Faro", "Fanny Favorit", "Frosty Filius",

"Face Flash", "Fame Friscco"

};

Soll der Array-Inhalt zum Testen auf den Bildschirm gebracht werden, so kommt eine Ausgabe mit System.out.println(dogs) nicht infrage, denn toString() ist auf dem Objekttyp Array nicht sinnvoll definiert:

System.out.println( dogs ); // [Ljava.lang.String;@10b62c9

Die statische Methode Arrays.toString(array) liefert für unterschiedliche Arrays die gewünschte String-Repräsentation des Arrays:

System.out.println( Arrays.toString(dogs) ); // [Flocky Fluke, ...]

Das spart nicht unbedingt eine for-Schleife, die durch das Array läuft und auf jedem Element printXXX(…) aufruft, denn die Ausgabe ist immer von einem bestimmten Format, das mit [ beginnt, jedes Element mit Komma und einem Leerzeichen trennt und mit ] abschließt.

Die Klasse Arrays deklariert die toString()-Methode für unterschiedliche Array-Typen:

  • static String toString(XXX[] a)

    Liefert eine String-Repräsentation des Arrays. Der Typ XXX steht stellvertretend für boolean, byte, char, short, int, long, float, double.

  • static String toString(Object[] a)

    Liefert eine String-Repräsentation des Arrays. Im Fall des Objekttyps ruft die Methode auf jedem Objekt im Array toString() auf.

  • static String deepToString(Object[] a)

    Ruft auch auf jedem Unter-Array Arrays.toString(…) auf und nicht nur toString() wie bei jedem anderen Objekt.

Sortieren

Diverse statische Arrays.sort(…)/Arrays.parallelSort(…)-Methoden ermöglichen das Sortieren von Elementen im Array. Bei primitiven Elementen (kein boolean) gibt es keine Probleme, da sie eine natürliche Ordnung haben.

Sortiere zwei Arrays:

int[] numbers = { -1, 3, -10, 9, 3 };

String[] names = { "Xanten", "Alpen", "Wesel" };

Arrays.sort( numbers );

Arrays.sort( names );

System.out.println( Arrays.toString( numbers ) ); // [-10, -1, 3, 3, 9]

System.out.println( Arrays.toString( names ) ); // [Alpen, Wesel, Xanten]

Besteht das Array aus Objektreferenzen, müssen die Objekte vergleichbar sein. Das gelingt entweder mit einem Extra-Comparator, oder die Klassen implementieren die Schnittstelle Comparable, wie zum Beispiel Strings. Kapitel 10, »Besondere Typen der Java SE«, beschreibt diese Möglichkeiten im Detail.

  • static void sort(XXX[] a )

  • static void sort(XXX[] a, int fromIndex, int toIndex)

    Sortiert die gesamte Liste vom Typ XXX (wobei XXX für byte, char, short, int, long, float oder double steht) oder einen ausgewählten Teil. Bei angegebenen Grenzen ist fromIndex wieder inklusiv und toIndex exklusiv. Sind die Grenzen fehlerhaft, löst die Methode eine IllegalArgumentException (im Fall fromIndex > toIndex) oder eine ArrayIndexOutOfBoundsException (fromIndex < 0 oder toIndex > a.length) aus.

  • static void sort(Object[] a)

  • static void sort(Object[] a, int fromIndex, int toIndex)

    Sortiert ein Array von Objekten. Die Elemente müssen Comparable implementieren. Bei der Methode gibt es keinen generischen Typparameter, der das zur Übersetzungszeit erzwingt!

  • static <T> void sort(T[] a, Comparator<? super T> c)

  • static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c)

    Sortiert ein Array von Objekten mit gegebenem Comparator.

Paralleles Sortieren

Spezielle Sortiermethoden sind für sehr große Arrays gedacht. Bei den parallelSort(…)-Methoden verwendet die Bibliothek mehrere Threads, um Teile parallel zu sortieren, was die Geschwindigkeit erhöhen kann. Eine Garantie ist das aber nicht, denn ein Performance-Vorteil ergibt sich wirklich nur bei großen Arrays.

  • static void parallelSort(byte[] a)

  • static void parallelSort(byte[] a, int fromIndex, int toIndex)

  • static void parallelSort(char[] a)

  • static void parallelSort(char[] a, int fromIndex, int toIndex)

  • static void parallelSort(short[] a)

  • static void parallelSort(short[] a, int fromIndex, int toIndex)

  • static void parallelSort(int[] a)

  • static void parallelSort(int[] a, int fromIndex, int toIndex)

  • static void parallelSort(long[] a)

  • static void parallelSort(long[] a, int fromIndex, int toIndex)

  • static void parallelSort(float[] a)

  • static void parallelSort(float[] a, int fromIndex, int toIndex)

  • static void parallelSort(double[] a)

  • static void parallelSort(double[] a, int fromIndex, int toIndex)

  • static <T extends Comparable<? super T>> void parallelSort(T[] a)

  • static <T extends Comparable<? super T>> void parallelSort(T[] a, int fromIndex,

    int toIndex)

  • static <T> void parallelSort(T[] a, Comparator<? super T> c)

  • static <T> void parallelSort(T[] a, int fromIndex, int toIndex,

    Comparator<? super T> c)

Der verwendete Algorithmus ist einfach zu verstehen: Zunächst wird das Array in Teil-Arrays partitioniert, diese werden parallel sortiert und dann zu einem größeren sortierten Array zusammengelegt. Das Verfahren nennt sich im Englischen auch parallel sort-merge.

Arrays von Primitiven mit Arrays.equals(…) und Arrays.deepEquals(…) vergleichen *

Die statischen Methoden Arrays.equals(…) vergleichen, ob zwei Arrays die gleichen Inhalte besitzen; dazu ist die überladene Methode für alle wichtigen Typen definiert. Wenn zwei Arrays tatsächlich die gleichen Inhalte besitzen, ist die Rückgabe der Methode true, sonst false. Natürlich müssen beide Arrays schon die gleiche Anzahl von Elementen besitzen, sonst ist der Test sofort vorbei und das Ergebnis false.

Vergleiche drei Arrays:

int[] array1 = { 1, 2, 3, 4 };

int[] array2 = { 1, 2, 3, 4 };

int[] array3 = { 9, 9, 2, 3, 9 };

System.out.println( Arrays.equals( array1, array2 ) ); // true

System.out.println( Arrays.equals( array2, 1, 3, array3, 2, 4 ) ); // true

Ein Vergleich von Teil-Arrays ist erst in Java 9 hinzugekommen.

Bei unterreferenzierten Arrays (Arrays zeigen auf Arrays) betrachtet Arrays.equals(…) das innere Array als einen Objektverweis und vergleicht es auch mit equals(…) – was jedoch bedeutet, dass nicht identische, aber mit gleichen Elementen referenzierte innere Arrays als ungleich betrachtet werden. Die statische Methode deepEquals(…) bezieht auch unterreferenzierte Arrays in den Vergleich mit ein.

Der Unterschied zwischen equals(…) und deepEquals(…):

int[][] a1 = { { 0, 1 }, { 1, 0 } };

int[][] a2 = { { 0, 1 }, { 1, 0 } };

System.out.println( Arrays.equals( a1, a2 ) ); // false

System.out.println( Arrays.deepEquals( a1, a2 ) ); // true

System.out.println( a1[0] ); // zum Beispiel [I@10b62c9

System.out.println( a2[0] ); // zum Beispiel [I@82ba41

Dass die Methoden unterschiedlich arbeiten, zeigen die beiden letzten Konsolenausgaben: Die von a1 und a2 unterreferenzierten Arrays enthalten die gleichen Elemente, sind aber zwei unterschiedliche Objekte, also nicht identisch.

deepEquals(…) vergleicht auch eindimensionale Arrays:

Object[] b1 = { "1", "2", "3" };

Object[] b2 = { "1", "2", "3" };

System.out.println( Arrays.deepEquals( b1, b2 ) ); // true

  • static boolean equals(XXX[] a, XXX[] a2)

    Vergleicht zwei Arrays gleichen Typs und liefert true, wenn die Arrays gleich groß und die Elemente paarweise gleich sind. XXX steht stellvertretend für boolean, byte, char, int, short, long, double oder float.

  • static boolean equals(XXX[] a, int aFromIndex, int aToIndex, XXX[] b,  int bFromIndex, int bToIndex)

    Vergleicht zwei Arrays, bleibt jedoch in den gewählten Ausschnitten. In Java 9 kamen diverse neue überladene Methoden hinzu.

Objekt-Arrays mit Arrays.equals(…) und Arrays.deepEquals(…) vergleichen *

Die Arrays.equals(…)-Methode kann auch Arrays mit beliebigen Objekten vergleichen, doch nutzt sie dann nicht die Identitätsprüfung per ==, sondern die Gleichwertigkeit per equals(…). Eine seit Java 9 hinzugekommene Methode fragt einen Comparator.

Enthalten zwei String-Arrays die gleichen Wörter, wobei Groß-/Kleinschreibung keine Rolle spielt?

String[] words1 = { "Zufriedenheit", "übertrifft" , "Reichtum" };

String[] words2 = { "REICHTUM", "übertrifft" , "ZuFRIEDEnheit" };

Arrays.sort( words1, String.CASE_INSENSITIVE_ORDER );

Arrays.sort( words2, String.CASE_INSENSITIVE_ORDER );

System.out.println( Arrays.equals( words1, words2,

String.CASE_INSENSITIVE_ORDER ) );

  • static boolean equals(Object[] a, Object[] b)

    Vergleicht zwei Arrays mit Objektverweisen. Ein Objekt-Array darf null enthalten; dann gilt für die Gleichwertigkeit, dass für alle Elemente e1 aus a und e2 aus b an gleicher Stelle gilt: e1==null ? e2==null : e1.equals(e2).

  • static boolean deepEquals(Object[] a, Object[] b)

    Liefert true, wenn die beiden Arrays ebenso wie alle Unter-Arrays – rekursiv im Fall von Unter-Objekt-Arrays – gleich sind.

  • static <T> boolean equals(T[] a, T[] b, Comparator<? super T> cmp)

    Vergleicht zwei Arrays mit einem Comparator, und cmp.compare(a[i], b[i]) muss für alle Pärchen 0 sein, damit beide Elemente als gleich gelten. Die Angabe <? super T> können wir erst einmal ignorieren.

  • static <T> boolean equals(T[] a, int aFromIndex, int aToIndex, T[] b, int bFromIndex, int bToIndex, Comparator<? super T> cmp)

    Vergleicht Ausschnitte von Arrays mit einem Comparator.

Unterschiede suchen mit mismatch (…) *

Weiterhin gibt es die folgenden Methoden:

  • int mismatch(XXX[] a, XXX[] b)

  • int mismatch(XXX[] a, int aFromIndex, int aToIndex, XXX[] b, int bFromIndex, int bToIndex)

Sie geben den Index auf das erste Element zurück, das ungleich ist. Sind beide Arrays gleich, ist die Rückgabe –1.

Für Objekt-Array gibt es weiterhin:

  • int mismatch(Object[] a, Object[] b)

  • int mismatch(Object[] a, int aFromIndex, int aToIndex, Object[] b, int bFromIndex, int bToIndex)

  • <T> int mismatch(T[] a, T[] b, Comparator<? super T> cmp)

  • <T> int mismatch(T[] a, int aFromIndex, int aToIndex, T[] b, int bFromIndex, int bToIndex, Comparator<? super T> cmp)

Die erste und zweite Methode nutzt direkt equals(…), die dritte und vierte verwendet einen externen Comparator.

Füllen von Arrays *

Arrays.fill(…) füllt ein Array mit einem festen Wert. Der Start-End-Bereich lässt sich optional angeben.

Fülle ein char-Array mit Sternchen:

char[] chars = new char[ 4 ];

Arrays.fill( chars, '*' );

System.out.println( Arrays.toString( chars ) ); // [*, *, *, *]

  • static void fill(XXX[] a, XXX val)

  • static void fill(XXX[] a, int fromIndex, int toIndex, XXX val)

    Setzt das Element val in das Array. Mögliche Typen für XXX sind boolean, char, byte, short, int, long, double, float oder mit Object beliebige Objekte. Beim Bereich ist fromIndex inklusiv und toIndex exklusiv.

Neben der Möglichkeit, ein Array mit festen Werten zu füllen, gibt es außerdem die Methoden setAll(…)/parallelSetAll(…). Die Methoden durchlaufen ein gegebenes Array und rufen eine bestimmte Methode für jeden Index auf, die zur Initialisierung verwendet wird.

Fülle ein double-Array mit Zufallszahlen:

double[] randoms = new double[ 10 ];

Arrays.setAll( randoms, v -> Math.random() );

System.out.println( Arrays.toString( randoms ) );

Das Beispiel nutzt Lambda-Ausdrücke, um die Funktion zu beschreiben. Wir kommen in Kapitel 12, »Lambda-Ausdrücke und funktionale Programmierung«, auf diese Syntax noch einmal zurück.

  • static void setAll(int[] array, IntUnaryOperator generator)

  • static void setAll(double[] array, IntToDoubleFunction generator)

  • static void setAll(long[] array, IntToLongFunction generator)

  • static <T> void setAll(T[] array, IntFunction<? extends T> generator)

  • static void parallelSetAll(double[] array, IntToDoubleFunction generator)

  • static void parallelSetAll(int[] array, IntUnaryOperator generator)

  • static void parallelSetAll(long[] array, IntToLongFunction generator)

  • static <T> void parallelSetAll(T[] array, IntFunction<? extends T> generator)

    Läuft ein gegebenes Array komplett ab und übergibt dabei dem generator Schritt für Schritt den Index. Der Generator bildet den Index auf einen Wert ab, der wiederum zur Array-Initialisierung genutzt wird.

Array-Abschnitte kopieren *

Die Klasse Arrays bietet eine Reihe von copyOf(…)- und copyOfRange(…)-Methoden, die gegenüber clone() den Vorteil haben, dass sie auch Bereichsangaben erlauben und das neue Array größer machen können; im letzten Fall füllen die Methoden das Array je nach Typ mit null, false oder 0.

String[] snow = { "Neuschnee", "Altschnee", "Harsch", "Firn" };

String[] snow1 = Arrays.copyOf( snow, 2 ); // [Neuschnee, Altschnee]

String[] snow2 = Arrays.copyOf( snow, 5 ); // [Neuschnee, Altschnee, Harsch,

// Firn, null]

String[] snow3 = Arrays.copyOfRange( snow, 2, 4 ); // [Harsch, Firn]

String[] snow4 = Arrays.copyOfRange( snow, 2, 5 ); // [Harsch, Firn, null]

  • static boolean[] copyOf(boolean[] original, int newLength)

  • static byte[] copyOf(byte[] original, int newLength)

  • static char[] copyOf(char[] original, int newLength)

  • static double[] copyOf(double[] original, int newLength)

  • static float[] copyOf(float[] original, int newLength)

  • static int[] copyOf(int[] original, int newLength)

  • static long[] copyOf(long[] original, int newLength)

  • static short[] copyOf(short[] original, int newLength)

  • static <T> T[] copyOf(T[] original, int newLength)

  • static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType)

  • static boolean[] copyOfRange(boolean[] original, int from, int to)

  • static byte[] copyOfRange(byte[] original, int from, int to)

  • static char[] copyOfRange(char[] original, int from, int to)

  • static double[] copyOfRange(double[] original, int from, int to)

  • static float[] copyOfRange(float[] original, int from, int to)

  • static int[] copyOfRange(int[] original, int from, int to)

  • static long[] copyOfRange(long[] original, int from, int to)

  • static short[] copyOfRange(short[] original, int from, int to)

  • static <T> T[] copyOfRange(T[] original, int from, int to)

  • static <T,U> T[] copyOfRange(U[] original, int from, int to,

    Class<? extends T[]> newType)

    Erzeugt ein neues Array mit der gewünschten Größe bzw. dem angegebenen Bereich aus einem existierenden Array. Wie üblich ist der Index from inklusiv und to exklusiv.

Hänge zwei Arrays aneinander. Das ist ein gutes Beispiel für copyOf(…), wenn das Ziel-Array größer ist:

public static <T> T[] concat( T[] first, T[] second ) {

T[] result = Arrays.copyOf( first, first.length + second.length );

System.arraycopy( second, 0, result, first.length, second.length );

return result;

}

Hinweis: Das Beispiel nutzt Generics (siehe Kapitel 11, »Generics<T>«), um den Typ flexibel zu halten. Zum einfacheren Verständnis können wir uns T als Typ Object vorstellen und das, was in spitzen Klassen steht, löschen.

Halbierungssuche *

Ist das Array sortiert, lässt sich mit Arrays.binarySearch(…) eine binäre Suche (Halbierungssuche) durchführen. Ist das Array nicht sortiert, ist das Ergebnis unvorhersehbar. Findet binarySearch(…) das Element, ist der Rückgabewert der Index der Fundstelle, andernfalls ist die Rückgabe negativ.

Suche ein Element im sortierten Array:

int[] numbers = { 1, 10, 100, 1000 };

System.out.println( Arrays.binarySearch( numbers, 100 ) ); // 2

Ist das Array nicht aufsteigend sortiert, ist ein falsches Ergebnis die Folge.

binarySearch(…) liefert in dem Fall, dass das Element nicht im Array ist, eine kodierte Position zurück, an der das Element eingefügt werden könnte. Damit der Index nicht mit einer normalen Position einer Fundstelle kollidiert, die immer ≥ 0 ist, ist die Rückgabe negativ und als -Einfügeposition - 1 kodiert.

Das Element 101 ist nicht im Array:

int[] numbers = { 1, 10, 100, 1000 };

System.out.println( Arrays.binarySearch( numbers, 101 ) ); // -4

Die Rückgabe ist –4, denn –4 = –3 – 1, was eine mögliche Einfügeposition von 3 ergibt. Das ist korrekt, denn 101 käme wie folgt ins Array: 1 (Position 0), 10 (Position 1), 100 (Position 2), 101 (Position 3).

Da das Array bei Arrays.binarySearch(…) zwingend sortiert sein muss, kann ein vorangehendes Arrays.sort(…) dies vorbereiten:

int[] numbers = { 10, 100, 1000, 1 };

Arrays.sort( numbers );

System.out.println( Arrays.toString( numbers ) ); // [1, 10, 100, 1000]

System.out.println( Arrays.binarySearch( numbers, 100 ) ); // 2

Die Sortierung ist nur einmal nötig und sollte nicht unnötigerweise wiederholt werden.

  • static int binarySearch(XXX[] a, XXX key)

    Sucht mit der Halbierungssuche nach einem Schlüssel. XXX steht stellvertretend für byte, char, int, long, float oder double.

  • static int binarySearch(Object[] a, Object key)

    Sucht mit der Halbierungssuche nach key. Die Objekte müssen die Schnittstelle Comparable implementieren. Das bedeutet im Allgemeinen, dass die Elemente vom gleichen Typ sein müssen – also nicht Strings und Hüpfburg-Objekte gemischt.

  • static <T> int binarySearch(T[] a, T key, Comparator<? super T> c)

    Sucht mit der Halbierungssuche ein Element im Objekt-Array. Die Vergleiche übernimmt ein spezielles Vergleichsobjekt c.

  • static <T> int binarySearch(T[] a, int fromIndex, int toIndex,

    T key, Comparator<? super T> c)

    Schränkt die Binärsuche auf Bereiche ein.

Die API-Dokumentation von binarySearch(…) ist durch Verwendung der Generics (mehr darüber folgt in Kapitel 11, »Generics<T>«) etwas schwieriger. Wir werden in Kapitel 17, »Einführung in Datenstrukturen und Algorithmen«, auch noch einmal auf die statische Methode binarySearch(…) für beliebige Listen zurückkommen und insbesondere die Bedeutung der Schnittstellen Comparator und Comparable in Kapitel 10, »Besondere Typen der Java SE«, genau klären.

Lexikografische Array-Vergleiche mit compare(…) und compareUnsigned(…)

Diverse in Java 9 eingeführte int compareXXX(XXX[] a, XXX[] b)-Methoden gehen die Arrays ab und testen alle Paare auf ihre Ordnung. Es gibt die von Comparator bekannte Rückgabe: Ist jedes a[i] == b[i], ist die Rückgabe 0. Ist in der Abfragefolge ein a[i] kleiner als b[i], dann ist die Rückgabe negativ; ist ein a[i] größer als b[i], ist die Rückgabe positiv. Die Methode ist überladen mit einer Variante, die einen Bereich im Array auswählt: compare(XXX[] a, int aFromIndex, int aToIndex, XXX[] b, int bFromIndex, int bToIndex). Für byte, short, int und long gibt es weiterhin eine Vergleichsmethode ohne Vorzeichen über den gesamten Wertebereich:

  • int compareUnsigned(XXX[] a, XXX[] b)

  • int compareUnsigned(XXX[] a, int aFromIndex, int aToIndex, XXX[] b, int bFromIndex, int bToIndex)

Für Objekte gibt es eigene Methoden:

  • static <T extends Comparable<? super T>> int compare(T[] a, T[] b)

    Vergleicht zwei Objekt-Arrays, wobei der Comparator die Ordnung der Objektpaare feststellt.

  • static <T extends Comparable<? super T>> int compare(T[] a, int aFromIndex, int aToIndex, T[] b, int bFromIndex, int bToIndex)

    Vergleicht Ausschnitte.

  • static <T> int compare(T[] a, T[] b, Comparator<? super T> cmp)

  • static <T> int compare(T[] a, int aFromIndex, int aToIndex,T[] b, int bFromIndex, int bToIndex, Comparator<? super T> cmp)

    Vergleicht mithilfe eines externen Comparator-Objekts.

Arrays zu Listen mit Arrays.asList(…) – praktisch für die Suche und zum Vergleichen *

Ist das Array unsortiert, funktioniert binarySearch(…) nicht. Die Klasse Arrays hat für diesen Fall keine Methode im Angebot – eine eigene Schleife muss her. Es gibt aber noch eine Möglichkeit: Die statische Methode Arrays.asList(…) dekoriert das Array als Liste vom Typ java.util.List, die dann praktische Methoden wie contains(…), equals(…) oder subList(…) anbietet. Mit den Methoden sind Dinge auf Arrays möglich, für die Arrays bisher keine Methoden definierte.

Teste, ob auf der Kommandozeile der Schalter -? gesetzt ist. Die auf der Kommandozeile übergebenen Argumente übergibt die Laufzeitumgebung als String-Array an die main(String[] args)-Methode:

if ( Arrays.asList( args ).contains( "-?" ) )

...

Teste, ob Teile zweier Arrays gleich sind:

// Index 0 1 2

String[] a = { "Asus", "Elitegroup", "MSI" };

String[] b = { "Elitegroup", "MSI", "Shuttle" };

System.out.println( Arrays.asList( a ).subList( 1, 3 ).

equals( Arrays.asList( b ).subList( 0, 2 ) ) ); // true

Im Fall von subList(…) ist der Start-Index inklusiv und der End-Index exklusiv (das ist die Standardnotation von Bereichen in Java, etwa auch bei substring(…) oder fill(…)). Somit werden im obigen Beispiel die Einträge 1 bis 2 aus a mit den Einträgen 0 bis 1 aus b verglichen.

  • static <T> List<T> asList(T... a)

    Liefert eine Liste vom Typ T bei einem Array vom Typ T.

Die statische Methode asList(…) nimmt über das Vararg entweder ein Array von Objekten (kein primitives Array!) an oder aufgezählte Elemente.

Im Fall der aufgezählten Elemente ist auch kein oder genau ein Element erlaubt:

System.out.println( Arrays.asList() ); // []

System.out.println( Arrays.asList("Chris") ); // [Chris]

Ein an Arrays.asList(…) übergebenes primitives Array liefert keine Liste von primitiven Elementen (es gibt keine List, die mit primitiven Werten gefüllt ist):

int[] nums = { 1, 2 };

System.out.println( Arrays.asList(nums).toString() ); // [[I@82ba41]

System.out.println( Arrays.toString(nums) ); // [1, 2]

Der Grund ist einfach: Arrays.asList(…) erkennt nums nicht als Array von Objekten, sondern als genau ein Element einer Aufzählung. So setzt die statische Methode das Array mit Primitiven als ein Element in die Liste, und die Objektmethode toString() eines java.util.List-Objekts ruft lediglich auf dem Array-Objekt toString() auf, was die kryptische Ausgabe zeigt.

Parallele Berechnung von Präfixen *

Stehen mehrere Prozessoren oder Kerne zur Verfügung, können bestimmte Berechnungen bei Arrays parallelisiert werden. Ein Algorithmus nennt sich parallele Präfix-Berechnung und basiert auf der Idee, dass eine assoziative Funktion – nennen wir sie f – auf eine bestimmte Art und Weise auf Elemente eines Arrays – nennen wir es a – angewendet wird, nämlich so:

  • a[0]

  • f(a[0], a[1])

  • f(a[0], f(a[1], a[2]))

  • f(a[0], f(a[1], f(a[2], a[3])))

  • f(a[0], f(a[1], … f(a[n-2], a[n-1])…))

In der Aufzählung sieht das etwas verwirrend aus, daher soll ein praktisches Beispiel das Verständnis erleichtern. Das Array sei [1, 3, 0, 4] und die binäre Funktion die Addition.

Index

Funktion

Ergebnis

0

a[0]

1

1

a[0] + a[1]

1 + 3 = 4

2

a[0] + (a[1] + a[2])

1 + (3 + 0) = 4

3

a[0] + (a[1] + (a[2] + a[3]))

1 + (3 + (0 + 4)) = 8

Tabelle 4.2    Präfix-Berechnung des Arrays [1, 3, 0, 4] mit Additionsfunktion

Auf den ersten Blick wirkt das wenig spannend, doch kann der Algorithmus parallelisiert werden und somit im besten Fall in logarithmischer Zeit (mit n Prozessoren) gelöst werden. Voraussetzung dafür ist allerdings eine assoziative Funktion, wie Summe und Maximum. Ohne ins Detail zu gehen, könnten wir uns vorstellen, dass ein Prozessor/Kern 0 + 4 berechnet, ein anderer zeitgleich 1 + 3 und dass dann das Ergebnis zusammengezählt wird.

Das Beispiel unserer Präfix-Berechnung mithilfe einer Methode aus Arrays:

int[] array = {1, 3, 0, 4};

Arrays.parallelPrefix( array, (a, b) -> a + b );

System.out.println( Arrays.toString( array ) ); // [1, 4, 4, 8]

Die Implementierung nutzt die fortgeschrittene Syntax der Lambda-Ausdrücke. Statt (a + b) ‐> a + b verkürzt es Integer::sum sogar noch.

Ein weiteres Beispiel: Finde das Maximum in einer Menge von Fließkommazahlen:

double[] array = {4.8, 12.4, -0.7, 3.8 };

Arrays.parallelPrefix( array, Double::max );

System.out.println( array[array.length -1 ] ); // 12.4

Das Beispiel nutzt schon die Methode, die Arrays für die parallele Präfix-Berechnung bietet:

  • static void parallelPrefix(int[] array, IntBinaryOperator op)

  • static void parallelPrefix(int[] array, int fromIndex, int toIndex,

    IntBinaryOperator op)

  • static void parallelPrefix(long[] array, LongBinaryOperator op)

  • static void parallelPrefix(long[] array, int fromIndex, int toIndex,

    LongBinaryOperator op)

  • static void parallelPrefix(double[] array, DoubleBinaryOperator op)

  • static void parallelPrefix(double[] array, int fromIndex, int toIndex,

    DoubleBinaryOperator op)

  • static <T> void parallelPrefix(T[] array, BinaryOperator<T> op)

  • static <T> void parallelPrefix(T[] array, int fromIndex, int toIndex,

    BinaryOperator<T> op)

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.6.1    Eine lange Schlange 

Das neu erworbene Wissen über Arrays wollen wir für unser Schlangenspiel nutzen, um die Schlange länger zu machen. Bisher hatte die Schlange keine Länge, sondern nur eine Position auf dem Spielbrett. Das wollen wir ändern: Ein Programm im Array soll sich immer die letzten Positionen merken. Folgende Änderungen sind dazu nötig:

  • Anstatt die Position in einem Point-Objekt zu speichern, liegen die letzten fünf Positionen in einem Point[] snakePositions.

  • Trifft der Spieler einen dieser fünf Schlangenpunkte, ist das Spiel verloren. Ist bei der Bildschirmausgabe eine Koordinate gleich einem der Schlangenpunkte, zeichnen wir ein »S«. Der Test, ob eine der Schlangenkoordinaten einen Punkt p trifft, wird mit Arrays.asList(snakePositions).contains(p) durchgeführt.

Ein weiterer Punkt ist, dass die Schlange sich bewegt, aber das Array mit den fünf Punkten immer nur die letzten fünf Bewegungen speichert – die alten Positionen werden verworfen. Das Programm realisiert das mit einem Ringspeicher – zusätzlich zu den Positionen verwalten wir einen Index, der auf den Kopf zeigt. Jede Bewegung der Schlange setzt den Index eine Position weiter, bis am Ende des Arrays der Index wieder bei 0 steht.

Eine symbolische Darstellung mit möglichen Punkten verdeutlicht dies:

snakeIdx = 0

snakePositions = { [2,2], null, null, null, null }

snakeIdx = 1

snakePositions = { [2,2], [2,3], null, null, null }

...

snakeIdx = 4

snakePositions = { [2,2], [2,3], [3,3], [3,4], [4,4] }

snakeIdx = 0

snakePositions = { [5,5], [2,3], [3,3], [3,4], [4,4] }

Alte Positionen werden überschrieben und durch neue ersetzt.

Hier ist das ganze Spiel mit den Änderungen (die fett hervorgehoben sind):

Listing 4.14    src/main/java/com/tutego/insel/array/LongerZZZZZnake.java

package com.tutego.insel.array;

import java.awt.Point;

import java.util.Arrays;

public class LongerZZZZZnake {

public static void main( String[] args ) {

Point playerPosition = new Point( 10, 9 );

Point goldPosition = new Point( 6, 6 );

Point doorPosition = new Point( 0, 5 );

Point[] snakePositions = new Point[5];

int snakeIdx = 0;

snakePositions[ snakeIdx ] = new Point( 30, 2 );

boolean rich = false;

while ( true ) {

if ( rich && playerPosition.equals( doorPosition ) ) {

System.out.println( "Gewonnen!" );

break;

}

if ( Arrays.asList( snakePositions ).contains( playerPosition ) ) {

System.out.println( "ZZZZZZZ. Die Schlange hat dich!" );

break;

}

if ( playerPosition.equals( goldPosition ) ) {

rich = true;

goldPosition.setLocation( -1, -1 );

}

// Raster mit Figuren zeichnen

for ( int y = 0; y < 10; y++ ) {

for ( int x = 0; x < 40; x++ ) {

Point p = new Point( x, y );

if ( playerPosition.equals( p ) )

System.out.print( '&' );

else if ( Arrays.asList( snakePositions ).contains( p ) )

System.out.print( 'S' );

else if ( goldPosition.equals( p ) )

System.out.print( '$' );

else if ( doorPosition.equals( p ) )

System.out.print( '#' );

else

System.out.print( '.' );

}

System.out.println();

}

// Konsoleneingabe und Spielerposition verändern

switch ( new java.util.Scanner( System.in ).next() ) {

case "h" : playerPosition.y = Math.max( 0, playerPosition.y - 1 ); break;

case "t" : playerPosition.y = Math.min( 9, playerPosition.y + 1 ); break;

case "l" : playerPosition.x = Math.max( 0, playerPosition.x - 1 ); break;

case "r" : playerPosition.x = Math.min( 39, playerPosition.x + 1 ); break;

}

// Schlange bewegt sich in Richtung Spieler

Point snakeHead = new Point( snakePositions[snakeIdx].x,

snakePositions[snakeIdx].y );

if ( playerPosition.x < snakeHead.x )

snakeHead.x--;

else if ( playerPosition.x > snakeHead.x )

snakeHead.x++;

if ( playerPosition.y < snakeHead.y )

snakeHead.y--;

else if ( playerPosition.y > snakeHead.y )

snakeHead.y++;

snakeIdx = (snakeIdx + 1) % snakePositions.length;

snakePositions[snakeIdx] = snakeHead;

} // end while

}

}

Wer wieder etwas vorarbeiten möchte, kann Folgendes tun:

  • Ersetze das Array durch eine dynamische Datenstruktur vom Typ ArrayList<Point>.

  • Nach jedem zweiten Schritt vom Benutzer soll die Länge der Schlage um eins wachsen.

  • Wenn der Spieler ein Goldstück einsammelt, soll die Länge der Schlange um eins schrumpfen.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an


Page 26

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

In Java-Klassen gibt es eine besondere statische Methode main(…), die das Laufzeitsystem in der angegebenen Hauptklasse (oder Startklasse) des Programms aufruft.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.7.1    Korrekte Deklaration der Startmethode 

Damit die JVM ein Java-Programm starten kann, muss es eine besondere Methode main(…) geben. Da die Groß-/Kleinschreibung in Java relevant ist, muss diese Methode main lauten – nicht Main oder MAIN. Die Sichtbarkeit ist auf public gesetzt, und die Methode muss statisch sein, da die JVM die Methode auch ohne ein Exemplar der Klasse aufrufen möchte. Als Parameter wird ein Array von String-Objekten angenommen. Darin sind die auf der Kommandozeile übergebenen Parameter abgelegt.

Zwei Varianten gibt es zur Deklaration:

  • public static void main( String[] args )

  • public static void main( String... args )

Die zweite Schreibweise nutzt variable Argumentlisten, ist aber nur eine etwas andere Schreibweise für die Annahme eines Arrays.

Falsche Deklarationen

Nur eine Methode mit dem Kopf public static void main(String[] args) wird als Startmethode akzeptiert. Ein Methodenkopf wie public static void Main(String[] args) ist syntaktisch gültig, aber eben keiner, den die JVM zum Start ansteuern würde. Findet die JVM die Startmethode nicht, gibt sie eine Fehlermeldung aus:

Fehler: Hauptmethode in Klasse ABC nicht gefunden. Definieren Sie die Hauptmethode als:

public static void main(String[] args)

Im Gegensatz zu C(++) steht im ersten Element des Argument-Arrays mit dem Index 0 nicht der Programmname, also der Name der Hauptklasse, sondern bereits der erste Programmparameter der Kommandozeile.

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.7.2    Kommandozeilenargumente verarbeiten 

Eine besondere Variable für die Anzahl der übergebenen Argumente der Kommandozeile ist nicht erforderlich, weil das String-Array-Objekt uns diese Information über length mitteilt. Um etwa alle übergebenen Argumente über die erweiterte for-Schleife auszugeben, schreiben wir:

Listing 4.15    src/main/java/com/tutego/insel/array/LovesGoldenHamster.java, main()

public static void main( String[] args ) {

if ( args.length == 0 )

System.out.println( "Was!! Keiner liebt kleine Hamster?" );

else {

System.out.print( "Liebt kleine Hamster: " );

for ( String s : args )

System.out.printf( "%s ", s );

System.out.println();

}

}

Das Programm lässt sich auf der Kommandozeile wie folgt aufrufen:

$ java com.tutego.insel.array.LovesGoldenHamster Raphael Perly Mirjam Paul

 

Netbeans Hauptklasse konnte nicht gefunden oder geladen werden

4.7.3    Der Rückgabetyp von main(…) und System.exit(int) * 

Der Rückgabetyp void der Startmethode main(…) ist sicherlich diskussionswürdig, da diejenigen, die die Sprache entworfen haben, auch hätten fordern können, dass ein Programm immer einen Statuscode an das aufrufende Programm zurückgibt. Für diese Lösung haben sie sich aber nicht entschieden, da Java-Programme in der Regel nur minimal mit dem umgebenden Betriebssystem interagieren sollen und echte Plattformunabhängigkeit gefordert ist, etwa bei Java in Java-Programmen auf Smartphones.

Für die Fälle, in denen ein Statuscode zurückgeliefert werden soll, steht die statische Methode System.exit(status) zur Verfügung; sie beendet eine Applikation. Das an exit(int) übergebene Argument nennt sich Statuswert (engl. exit status) und wird an die Kommandozeile zurückgegeben. Der Wert ist für Skriptprogramme wichtig, da sie über diesen Rückgabewert auf das Gelingen oder Misslingen des Java-Programms reagieren können. Ein Wert von 0 zeigt per Definition das Gelingen an, ein Wert ungleich 0 einen Fehler. Der Wertebereich sollte sich zwischen 0 und 255 bewegen. Auf der Unix-Kommandozeile ist der Rückgabewert eines Programms unter $? verfügbar und in der cmd.exe von Windows unter %ERRORLEVEL%, einer Art dynamischer Umgebungsvariablen.

Dazu ein Beispiel; ein Java-Programm liefert den Statuswert 42:

Listing 4.16    src/main/java/com/tutego/insel/array/SystemExitDemo.java

package com.tutego.insel.array;

public class SystemExitDemo {

public static void main( String[] args ) {

System.exit( 42 );

}

}

Das folgende Shell-Programm gibt den Statuswert zunächst aus und zeigt zudem, welche Fallunterscheidung die Shell für Statuswerte bietet:

Listing 4.17    showreturn.bat

@echo offcd target\classesjava com.tutego.insel.array.SystemExitDemoecho %ERRORLEVEL%if errorlevel 10 ( echo Exit-Code ist über 10, genau %ERRORLEVEL%

)

Die JVM startet das Java-Programm und beendet es mit System.exit(int), was zu einer Belegung der Variablen %ERRORLEVEL% mit 42 führt. Das Skript gibt zunächst die Belegung der Variablen aus. Die Windows-Shell besitzt mit if errorlevel Wert eine spezielle Variante für Fallunterscheidungen mit Exit-Codes, die genau dann greift, wenn der aktuelle Exit-Code größer oder gleich dem angegebenen Wert ist. Das heißt in unserem Beispiel: Es gibt eine Ausgabe, wenn der Exit-Code größer 10 ist, und mit 42 ist er das. Daher folgt die Ausgabe des kleinen Skripts:

>showreturn.bat42

Error-Level ist über 10, genau 42

Es ist wichtig, zu bedenken, dass %ERRORLEVEL% natürlich überschrieben wird, wenn Befehle folgen. So gibt Folgendes nur 0 aus, da dir erfolgreich abgeschlossen werden kann und dir nach der Durchführung den Exit-Code auf 0 setzt:

java SystemExitDemodir

echo %ERRORLEVEL%

Liegen zwischen dem Aufruf der JVM und der Auswertung der Variablen Aufrufe, die den Exit-Code verändern, dann ist es sinnvoll, den Inhalt von %ERRORLEVEL% zwischenzuspeichern, etwa so:

Listing 4.18    showreturn2.bat

@echo offcd target\classesjava SystemExitDemoSET EXITCODE=%ERRORLEVEL%dir > NUL:echo %ERRORLEVEL%

echo %EXITCODE%

Die Ausgabe ist dann:

final class java.lang.System

  • static void exit(int status)

    Beendet die aktuelle JVM und gibt das Argument der Methode als Statuswert zurück. Ein Wert ungleich 0 zeigt einen Fehler an. Also ist der Rückgabewert beim normalen fehlerfreien Verlassen 0. Eine SecurityException wird ausgelöst, falls der aktuelle SecurityManager dem aufrufenden Code nicht erlaubt, die JVM zu beenden. Das gilt insbesondere bei Programmen in einem Container, wie einem Webserver.

Wie hat Ihnen das Openbook gefallen? Wir freuen uns immer über Ihre Rückmeldung. Schreiben Sie uns gerne Ihr Feedback als E-Mail an