FAQs – Java

FAQs – Java

Wie schreibe ich Kommentare?

In Java gibt es drei Arten von Kommentaren:

  • Zeilenkommentar: //
    Alles was in der selben Zeile hinter dem Kommentarzeichen steht, wird vom Compiler nicht übersetzt.
  • Blockkommentar: /* */
    Der Text der zwischen den Kommentarzeichen steht, wird vom Compiler ignoriert. Blockkommentare können nicht ineinander geschachtelt werden.
  • Javadoc-Kommentar: /** */
    Dieser Blockkommentar dient zum Dokumentieren von Klassen und Methoden. Diese Kommentare können zum automatisierten Generieren von Dokumentationen (Javadoc, Doxygen) verwendet werden.

Code sollte grundsätzlich so geschrieben sein, dass möglichst wenig Kommentare notwendig sind. Das kann durch passende Strukturierung, durch passende Namen für Pakete, Klassen, Methoden und Variablen erreicht werden.
Kommentare können Teil der Dokumentation sein (Javadoc). Sie sollen das Lesen des Codes für andere erleichtern. Bei Änderungen im Code sollte man nicht vergessen, die Kommentare an die Veränderungen anzupassen.

Beispiel für Javadoc-Kommentare in der Klasse String aus der Java-Klassenbibliothek:

/**
 * Returns the length of this string.
 * The length is equal to the number of <a href="Character.html#unicode">Unicode
 * code units</a> in the string.
 *
 * @return  the length of the sequence of characters represented by this
 *          object.
 */
public int length() {
    return value.length;
}

/**
 * Returns {@code true} if, and only if, {@link #length()} is {@code 0}.
 *
 * @return {@code true} if {@link #length()} is {@code 0}, otherwise
 * {@code false}
 *
 * @since 1.6
 */
public boolean isEmpty() {
    return value.length == 0;
}

Was ist der Unterschied zwischen “static” und “final”? Wann werden statische Methoden verwendet?

Was ist der Unterschied zwischen static und final?

Static und final sind Schlüsselwörter, die recht unterschiedliche Sachverhalte beschreiben.

Mit dem Schlüsselwort static wird festgelegt, dass eine Variable oder Methode zu einer Klasse gehört und nicht zu einem Objekt. Das heißt, alle Instanzen einer Klasse greifen auf ein und dieselbe statische Variable zu.

Beispiel:
Mit der statischen Variable count kann die Anzahl der erzeugten Person-Objekte gezählt werden.

public class Person {

    private static int count;
    private String name;

    public Person(String name) {
        this.name = name;
        count++;
    }

    public String getName() { return name; }

    public static int getCount() { return count; }

    public static void main(String[] args) {
        Person p1 = new Person("Peter");
        Person p2 = new Person("Werner");
        System.out.println(getCount()); // Ausgabe: 2
    }
}

Das Schlüsselwort final kann sich auf Variablen, Methoden und Klassen beziehen.

  • Deklariert man eine Variable als final, muss der Variable genau einmal bei der Deklaration oder im Konstruktor ein Wert zugewiesen werden. Ein schreibender Zugriff auf die Variable ist nach der ersten Zuweisung nicht mehr möglich.
  • Deklariert man eine Methode als final, kann diese Methode nicht mehr mittels Vererbung überschrieben werden.
  • Deklariert man eine Klasse als final, kann von dieser Klasse keine weitere Klasse (extends) abgeleitet werden.

Eine Kombination von static und final ist sowohl bei Variablen als auch bei Methoden möglich. Die Kombination static final wird oft zur Deklaration globaler Konstanten verwendet:

public static final double PI = 3.141592653589793;

public static final Color BACKGROUND_COLOR = Color.LIGHT_GRAY;
Wann werden statische Methoden verwendet?

Statische Methoden können verwendet werden, ohne ein Objekt der Klasse, in der die Methode implementiert ist, erzeugen zu müssen. Sie werden oft für mathematische Funktionen und Utility-Klassen verwendet (Math.sin(), Arrays.sort(), System.out.println(), …).

Was ist eine Methode? Wie rufe ich sie auf? Kann eine Methode eine weitere aufrufen?

Mit einer Methode kann man Funktionalitäten zusammenfassen. Eine Methode kann Übergabewerte (Parameter) haben, mit denen in der Methode gearbeitet wird. Sie kann einen Rückgabewert haben, der ein Ergebnis an den Aufrufer der Methode übergibt. In Java ist bei der Definition der Methode für die Parameter und den Rückgabewert ein Typ anzugeben.

Eine Methode wird mit dem Methodennamen und den aktuellen Parametern aufgerufen: y = pythagoras(3, 4);

Methoden können weitere Methoden aufrufen.

Beispiel:

static double pythagoras(double x1, double x2) {
    return Math.sqrt(quadrat(x1) + quadrat(x2));
}

static double quadrat(double x) {
    return x * x;
}

static long fact(int n) {
    return (n <= 1) ? 1 : n * fact(n - 1);
}

Die Methode pythagoras() berechnet die Länge der Hypotenuse \(c=\sqrt{a^2+b^2}\) in einem rechtwinkligen Dreieck. Zur Berechnung der Quadrate verwendet die Methode pythagoras() die Methode quadrat() und zur Berechnung der Quadratwurzel die Methode Math.sqrt().

Die Methode fact() zur Berechnung der Fakultät \(n!\) ruft sich selber auf (Rekursion).

Was ist eine Exception? Was ist eine NullPointerException?

Exceptions werden ausgelöst, wenn zur Laufzeit des Programms ein Fehler auftritt.

Exceptions werden verwendet, um zu verhindern, dass Laufzeitfehler zum “Absturz” des Programms führen. Zur Fehlerbehandlung können Anweisungen, bei denen ein Laufzeitfehler auftreten kann, in einen try-catch-Block eingeschlossen werden.

try {
    // Bereich, in dem ein Fehler auftreten kann
} catch (Exception e) {
    // Fehlerbehandlung
}

Beispiele für Exceptions:

  • java.lang.NullPointerException: Tritt auf, wenn auf Methoden oder Felder eines Objekts, das nicht existiert, zugegriffen wird.
  • java.lang.ArrayIndexOutOfBoundsException: Tritt bei fehlerhafter Indizierung von Arrays auf.
  • java.lang.StackOverflowError: Dieser Fehler tritt beispielsweise auf, wenn bei einer Rekursion das Abbruchkriterium nicht erreicht wird.
  • java.lang.ArithmeticException: Tritt beispielsweise bei einer Division durch 0 auf.
  • java.lang.NumberFormatException: Auslöser ist meist der Versuch, einen String in eine Zahl zu konvertieren.

Was sind die Unterschiede zwischen “public” und “private”?

Die Schlüsselwörter public und private sind Zugriffsmodifikatoren. Mit Hilfe dieser Modifikatoren wird die Sichtbarkeit einer Methode, einer Klassenvariable oder einer Instanzvariable festgelegt.

Werden Methoden und Variablen als private deklariert, sind sie nur innerhalb der Klasse sichtbar.
Werden Methoden und Variablen als public deklariert, sind keine Einschränkungen der Sichtbarkeit gegeben.

Wann ist es sinnvoll eine Methode zu erstellen? Machen Methoden in kürzeren Programmen (60-80 Zeilen) Sinn?

Das Erstellen einer Methode ist – unabhängig von der Länge des Programms – immer dann sinnvoll, wenn verschiedene Anweisungen zu einer Funktionalität zusammengebaut werden können.

Methoden kapseln also Funktionalitäten. Sie sind wiederverwendbar und machen den Code übersichtlicher und besser wartbar, indem sie Code-Verdopplungen vermeiden. Außerdem sind Methoden mit aussagekräftigen Namen Teil der Dokumentation.

Mit Hilfe von Methoden werden komplexe Aufgaben in Teilaufgaben zerlegt. Durch diesen Vorgang kann die Komplexität der Gesamtaufgabe reduziert werden. Dieses Zerlegen in Teilaufgaben muss jeder Entwickler und jede Entwicklerin lernen. Deshalb ist eine Gliederung in Teilaufgaben auch bei kleineren Programmen durchzuführen.

Methoden können unabhängig vom restlichen Programm getestet werden und erleichtern das Erstellen von automatisierten Tests (JUnit).

Wie überlädt man Methoden? Wann ist es sinnvoll eine Methode zu überladen?

Überladene Methoden sind Methoden mit gleichem Namen aber unterschiedlicher Parameterliste. Sie besitzen also unterschiedliche Signaturen. Sie werden oft verwendet, um dieselbe Funktionalität mit unterschiedlichen Datentypen zu realisieren. Mehr dazu findest du in unserer FAQ hier.

Überladene Methoden können zur Beschreibung von Methoden mit unterschiedlichem Differenzierungsgrad verwendet werden.
Beispiel:

public void drawPixel(int x, int y) {
    // ...
}

public void drawPixel(int x, int y, Color c) {
    // ...
}

Die erste Methode zeichnet einen Punkt in der aktuellen Vordergrundfarbe.
Die zweite Methode besitzt einen zusätzlichen Parameter c, der angibt, in welcher Farbe der Punkt dargestellt wird.

Was ist ein Array? Wofür brauche ich Arrays?

Ein Array ist eine Datenstruktur zum Speichern gleichartig strukturierter Daten. Auf diese Daten kann über einen Index zugegriffen werden. In Java sind Arrays typisiert. Das heißt, der Typ der Elemente wird bei der Deklaration festgelegt.

Anwendung:

Arrays können verwendet werden, um gleichartig strukturierte Elemente (Konten, Schüler) zusammenzufassen. Beispielsweise kann ein Array alle Schüler einer Klasse oder alle Konten einer Bank enthalten. Über einen Index kann auf die einzelnen Array-Elemente zugegriffen werden. Dieser Index kann die Kontonummer oder eine Schülerkennzahl sein. Auf den Elementen eines Arrays können Operationen wie Suchen, Sortieren oder Auflisten ausgeführt werden.

Arrays können eine, zwei oder mehrere Dimensionen haben. Eindimensionale Arrays bilden beispielsweise Zeitreihen – wie einen Temperaturverlauf an einem Tag – ab. Zweidimensionale Arrays kann man zum Repräsentieren eines Spielfeldes oder zum Speichern von Pixeldaten eines Bildes verwenden, … Mehr dazu findet man in unserer FAQ hier und hier.

Warum fangen Arrays bei 0 an?

Das Indizieren von Array-Elementen beginnend beim Index 0 bildet die interne Darstellung des Arrays im Speichers ab. Intern wird ein Array durch die Adresse a, an der der Speicherbereich für die Array-Daten beginnt, repräsentiert.

|<-- k -->|<-- k -->|<-- k -->|
+---------+---------+---------+------------
|   a[0]  |   a[1]  |   a[2]  |
+---------+---------+---------+------------
|         |         |         |
a       a+k*1     a+k*2

Für das Speichern eines Elements des Arrays werden k Bytes benötigt. Addiert man zur Adresse a, den Wert 0*k, so erhält man die Adresse des 1-ten Array-Elements. Addiert man zur Adresse a, den Wert 1*k, so erhält man die Adresse des 2-ten Array-Elements. …
Allgemein: Addiert man zur Adresse a, den Wert n*k, so erhält man die Adresse des n+1-ten Array-Elements.
Mit n*k berechnet man also die Position des Array-Elements mit dem Index n im Speicher relativ zu a.

In C kann mit Adressen (pointer) gerechnet werden. Dabei wird der oben beschriebene Faktor k für die Größe eines Array-Elements implizit eingesetzt.
Beispiel:

int a[10];
int b = *a;
int c = *(a + 2);
  1. a ist ein Zeiger (Adresse) auf den Beginn des Arrays.
  2. *a oder *(a + 0) ist der Inhalt des ersten Array-Elements – Kurzschreibweise: a[0].
  3. *(a + 2) ist der Inhalt des Array-Elements mit Index 2 – Kurzschreibweise: a[2].

Warum kann der Compiler nicht selbst den passenden Datentyp (“int”, “float”, …) wählen?

Grundsätzlich unterscheidet man statisch und dynamisch typisierte Sprachen. Bei dynamisch typisierten Sprachen – wie z. B. PHP oder JavaScript – ergibt sich der Datentyp einer Variable zur Laufzeit aus dem Kontext. Der Typ einer Variable kann sich während der Laufzeit des Programms ändern. Im Beispiel bekommt die Variable name initial durch die Zuweisung eines Strings den Typ str. Durch die Zuweisung eines Integers in Zeile 3 bekommt name den Typ int. Der Aufruf einer String-Methode in Zeile 5 führt zu einem Laufzeitfehler.

name = 'Klaus'
print(type(name))   # -> str
name = 123
print(type(name))   # -> int
name = name.upper() # AttributeError: 'int' object has no attribute 'upper'

Bei statisch typisierten Sprachen – wie z. B. Java oder C – wird der Datentyp einer Variable bei der Deklaration durch den Programmierer festgelegt.

Bei statisch typisierten Sprachen kann der Compiler die Typkompatibilität beim Übersetzen prüfen. Dadurch kann man Fehler, die bei dynamisch typisierten Sprachen erst zu Laufzeit auftreten, schon zur Compilezeit erkennen (Zeile 2). Sicherheitskritische Anwendungen sollten daher in einer typisierten Sprache geschrieben werden.

Java:

String name = "Klaus";
name = 123; // Type mismatch: cannot convert from int to String
name.toUpperCase();

Da die Typangabe Teil der Dokumentation ist, erhöht die statische Typisierung die Lesbarkeit des Codes.

Der Vorteil dynamisch typisierter Sprachen ist, dass der Code oft kürzer und kompakter wird.

Was bedeutet compilieren und wie compiliert man etwas in Java?

Beim Compilieren wird aus dem Sourcecode maschinenlesbarer Code erzeugt. In Abhängigkeit vom verwendeten Compiler und der Programmiersprache besteht dieser Code entweder aus direkt vom Prozessor (CPU) ausführbaren Instruktionen oder aus einem Zwischencode (Bytecode), der von einer virtuellen Maschine ausgeführt werden kann.

Mit dem Java-Compiler wird aus dem Sourcecode Bytecode erzeugt.

  • Zum Compilieren der Datei MyProg.java wird auf der Console die Anweisung javac MyProg.java eingegeben. Mit diesem Befehl erzeugt man die Datei MyProg.class, die den Bytecode enthält.
  • Mit dem Befehl java myProg kann das Programm ausgeführt werden.

Was ist der Unterschied zwischen einer statischen Methode und einer nicht-statischen Methode?

Statische Methoden können verwendet werden, ohne ein Objekt der Klasse, in der die Methode implementiert ist, erzeugen zu müssen. Statische Methoden können nur auf statische Felder (Klassenvariablen) zugreifen. Statische Methoden werden oft für mathematische Funktionen und Utility-Klassen verwendet (Math.sin(), Arrays.sort(), System.out.println(), …).

Nicht-statische Methoden können nur im Kontext eines Objekts verwendet werden. Sie können nicht nur auf die statischen Felder der Klasse (Klassenvariablen), sondern auch auf die Zustände des Objekts (Instanzvariablen) zugreifen.

Was ist der Unterschied zwischen einer lokalen und einer globalen Variable?

Deklaration:

Globale Variablen werden außerhalb von Methoden deklariert. Die Deklaration einer globalen Variable kann vor oder nach ihrer Verwendung im Code stehen.

Lokale Variablen werden innerhalb einer Methode deklariert. Die Deklaration einer lokalen Variable muss vor ihrer Verwendung im Code stattfinden.

Sichtbarkeit:

Ein wesentlicher Unterschied zwischen globalen und lokalen Variablen ist ihre Sichtbarkeit. Siehe Scope in unserer FAQ.

Lebensdauer:

Eine lokale Variable existiert nur in dem Block, in dem sie deklariert wird. Eine globale Variable hat die gleiche Lebensdauer wie das zugehörige Objekt.

Überschattung:

Deklariert man in einem Block ein lokale Variable, die namensgleich mit einer globalen Variable ist, wird in dem Block in dem die Variable deklariert wurde, die globale Variable von der lokalen Variable überdeckt. In einem solchen Kontext kann mit this auf die verschattete Variable zugegriffen werden.

public class Test {
    int n = 1;

    void foo() {
        int n = 3;
        System.out.println("lokal: " + n);
        System.out.println("global: " + this.n);
    }

    public static void main(String[] args) {
        Test o = new Test();
        System.out.println(o.n);
        o.foo();
    }
}

In Zeile 2 wird die globale Variable n deklariert und initialisiert.
In Zeile 5 wird die lokale Variable n deklariert und initialisiert. Die globale Variable n wird durch die lokale Variable überschattet.
In Zeile 6 wird der Wert der lokalen Variable ausgegeben.
In Zeile 7 wird mit this auf die globale Variable n zugegriffen.
In Zeile 12 wird der Wert der globalen Variable ausgegeben.
In Zeile 13 wird die Methode foo() aufgerufen und der Wert der lokalen und globalen Variable ausgegeben.

Wie wähle ich den richtigen Namen für eine Variable? Kann ich den Namen für eine Variable 2 Mal verwenden?

Namen für Variablen sind häufig Substantive (name, age, …) oder Adjektive (selected, enabled, …). Die Bedeutung einer Variable soll aus dem Namen ersichtlich sein. Für die Namensgebung gibt es oft Richtlinien.

In einer Klasse können Variablen mit gleichen Namen global und lokal deklariert werden. Die dadurch entstehende Verschattung führt oft zu schlecht lesbarem Code, und sollte daher vermieden werden. Mehr zum Thema Verschattung findest du in unserer FAQ.

Ausnahme: Konstruktoren und Setter:

public class MyClass {

    private String name;
    private int age;

    public MyClass(String name) {
        this.name = name;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Was ist der Unterschied zwischen Initialisieren und Deklarieren?

Deklarieren

Beim Deklarieren wird festgelegt, dass es eine Variable mit einem bestimmten Namen gibt. Bei typsicheren Sprachen wie z.B. Java wird bei der Deklaration auch der Typ der Variable festgelegt.

Initialisieren

Beim Initialisieren bekommt eine Variable erstmals einen Wert zugewiesen.

In Java werden Klassen- und Instanzvariablen bei der Deklaration mit einem default-Wert (0, false, null) initialisiert.

Warum gibt es die “main”-Methode? Kann Sie auch anders aussehen? Kann ich mehrere deklarieren?

Die main-Methode ist der Einstiegspunkt in das Programm. Die Ausführung eines Programms beginnt mit der ersten Anweisung, die in der main-Methode steht.

Die Signatur der main-Methode ist festgelegt: public static void main(String[] x) oder public static void main(String... x)

In jeder Klasse kann höchstens eine main-Methode deklariert werden. Dies ist oft sinnvoll, um die Klasse testen zu können.
Welche main-Methode ausgeführt wird, ist durch den Befehl an der Kommandozeile (java <Klassenname>) oder in den Einstellungen der Entwicklungsumgebung (Eclipse: Run > Run as > Java Application) festzulegen.

Wie wird ein Scope in Java definiert?

Mit einem Scope bezeichnet man die Sichtbarkeit von Klassen, Variablen und Methoden. Mit der Sichtbarkeit wird der Bereich beschrieben, in dem auf eine Klasse, Variable oder Methode zugegriffen werden kann.

Klassen / Methoden / Globale Variablen

Bei Klassen, Methoden und globalen Variablen wird der Scope durch einen Modifier bestimmt:

  • public: Keine Einschränkung der Sichtbarkeit
  • protected: Die Sichtbarkeit ist innerhalb der Vererbungshierarchie gegeben.
  • private: Die Sichtbarkeit ist auf die Klasse eingeschränkt.
  • default: Wenn kein Modifier angegeben ist, spricht man von einer default-Sichtbarkeit. Die Sichtbarkeit auf das Paket beschränkt.
Lokale Variablen

Lokale Variablen sind Variablen, die innerhalb einer Methode deklariert sind. Sie sind innerhalb des Blockes, in dem sie deklariert wurden, sichtbar. Ein Block besteht aus Anweisungen, die in geschwungene Klammern eingeschlossen sind. Hat eine lokale Variable den gleichen Namen wie eine globale Variable, verschattet die lokale Variable die globale Variable. Das heißt, innerhalb des Blocks wird die lokale Variable verwendet. Nach Beenden des Blocks wird mit der globalen Variable weitergearbeitet. Mehr dazu findest du im Artikel “Was ist der Unterschied zwischen einer lokalen und einer globalen Variable?” in unserer FAQ.

Beispiel:

public class ScopeDemo {
    int a = 99;

    void foo() {
        for (int a = 0; a < 3; a++) {
            System.out.print(a + " ");
        }
        System.out.println(a);
    }

    void setA(int a) {
        this.a = a;
    }
}

In Zeile 2 wird die globale Variable a deklariert und initialisiert.
In Zeile 5 wird die lokale Variable a deklariert und initialisiert .
Im Schleifenkörper (Zeile 6) wird die lokale Variable a verwendet.
In Zeile 8 wird wieder auf die globale Variable a zugegriffen.
Ein Aufruf der Methode foo() liefert die Ausgabe: 0 1 2 99

In Zeile 11 wird die lokale Variable a als Methodenparameter deklariert.
In Zeile 12 wird die Verschattung mit dem Schlüsselwort this aufgehoben.

Was sind Laufzeiten? Wie kann ich Laufzeiten bestimmen?

Mit Laufzeit bezeichnet man die Zeit, die ein Programm oder eine bestimmte Sequenz in ein einem Programm zur Ausführung benötigt.

In Java kann man zur Messung der Laufzeit System.nanoTime() verwenden. Im folgenden Beispiel wird die Laufzeit der Methode test() gemessen. Diese Methode berechnet die Summe der ersten n aufeinanderfolgenden natürlichen Zahlen (kleiner Gauß).

Java:

static long test(int n) {
    long sum = 0;
    for (int i = 0; i <= n; i++) {
        sum += i;
    }
    return sum;
}

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        long t1 = System.nanoTime();
        test(1000000);
        long t2 = System.nanoTime();
        long dt = t2 - t1;
        System.out.println(dt + " ns");
    }
}

Die Ausgabe des Programms zeigt die Zeitmessung in Nanosekunden für n=1 000 000 mit zehn aufeinanderfolgenden Versuchen.

6968700 ns
2738800 ns
424800 ns
427600 ns
474600 ns
426300 ns
412500 ns
412200 ns
354900 ns
352700 ns

Da die virtuelle Maschine während der Laufzeit Optimierungen vornimmt, nimmt bei mehreren Durchläufen die Ausführungszeit ab.

Zeitmessungen werden oft verwendet, um die Effizienz von Algorithmen zu vergleichen. Man muss dabei beachten, dass auf einem Multitasking-Betriebssystem die Ressourcen wie Speicher oder CPU von verschiedenen Prozessen genutzt werden. Die Laufzeit eines Programms kann in Abhängigkeit der Auslastung der Ressourcen variieren.

Was ist ein Laufzeitfehler?

In der Programmierung kann man verschiedene Arten von Fehlern unterscheiden. Es gibt logische Fehler, Fehler, die beim Compilieren auftreten oder eben auch Laufzeitfehler. Laufzeitfehler können vom Compiler nicht erkannt werden und treten erst bei der Ausführung des Programms auf.

Beispiele für häufige Laufzeitfehler:

  • java.lang.NullPointerException: Tritt auf, wenn auf Methoden oder Felder eines Objekts, das nicht existiert, zugegriffen wird.
  • java.lang.ArrayIndexOutOfBoundsException: Tritt bei fehlerhafter Indizierung von Arrays auf.
  • java.lang.StackOverflowError: Dieser Fehler tritt beispielsweise auf, wenn bei einer Rekursion das Abbruchkriterium nicht erreicht wird.
  • java.lang.ArithmeticException: Tritt beispielsweise bei einer Division durch 0 auf.
  • java.lang.NumberFormatException: Auslöser ist meist der Versuch, einen String in eine Zahl zu konvertieren.

Was ist objektorientierte Programmierung? Ist Java 100% objektorientiert?

Eine wesentliche Idee der objektorientierten Programmierung ist, dass Programmabläufe mit Hilfe von Objekten modelliert werden. Objekte bestehen aus Daten und den zur Verarbeitung der Daten notwendigen Methoden. Über Vererbung können Objekte durch neuen Eigenschaften ergänzt werden.

Java verfügt über elementare Datentypen wie int, double, boolean, …. Ist also nicht 100% objektorientiert.

Wann ist ein Interface sinnvoll? Wozu braucht man Interfaces?

Interfaces werden verwendet, um Software zu schreiben, die unabhängig von einer konkreten Implementierung ist. Im Interface wird lediglich eine Schnittstelle vereinbart. Diese Schnittstelle beschreibt, wie eine Klasse zu verwenden ist.

In Java ist eine Mehrfachvererbung nicht möglich. Mit Hilfe von Interfaces kann trotzdem gewährleistet werden, dass Klassen die nicht in einer gemeinsamen Vererbungshierachie sind, bestimmte Eigenschaften und Methoden besitzen.

Beispiel aus der Java-Klassenbibliothek:
Abgesehen von Object sind die Klassen String und Integer in keiner gemeinsamen Vererbungshierachie. Trotzdem implementieren beide Klassen das Comparable-Interface – müssen also eine konkrete Implementierung der Methode compareTo() anbieten. Objekte der Klasse String können also genauso wie Objekte der Kasse Integer miteinander verglichen und damit auch sortiert werden.

Was ist der Unterschied zwischen einer abstrakten Klasse und einem Interface?

Interface

Ein Interface definiert Methoden. Diese Methoden müssen von allen abgeleiteten Klassen, die nicht abstrakt sind, implementiert werden. Neben den Signaturen für die zu implementierenden Methoden kann ein Interface Konstanten und Enumerationen haben.

Interfaces werden verwendet, um Software zu schreiben, die unabhängig von einer konkreten Implementierung ist. Festgelegt wird im Interface lediglich eine Schnittstelle. Diese Schnittstelle beschreibt, wie eine Klasse zu verwenden ist.

Beispiel aus der Java-Klassenbibliothek:
Alle nicht abstrakten Klassen, die das Comparable-Interface implementieren, müssen eine konkrete Implementierung der Methode compareTo() anbieten.

Abstrakte Klasse

Eine abstrakte Klasse kann im Unterschied zum Interface neben abstrakten Methoden auch konkrete Implementierungen von Methoden haben. Außerdem kann sie auch Instanz- und Klassenvariablen besitzen. Eine abstrakte Methode legt – genauso wie eine Interface-Methode – eine Signatur für die zu implementierende Methode fest.

Bei der Verwendung von abstrakten Klassen ist zu beachten, dass eine Klasse von höchstens einer abstrakten Klasse abgeleitet werden kann. Im Gegensatz dazu kann eine Klasse beliebig viele Interfaces implementieren.

Können mehrere Methoden in einer Klasse den gleichen Namen haben? Wenn ja, wie wird festgelegt, welche dieser Methoden aufgerufen wird?

In Java können mehrere Methoden in einer Klasse den gleichen Namen haben, wenn sie sich durch ihre Signatur unterscheiden lassen. Das heißt Methoden mit gleichem Namen müssen anhand der Typen und Reihenfolge der Übergabeparameter unterscheidbar sein. Methoden mit gleichem Namen nennt man auch überladene Methoden.

Im folgenden Beispiel wird die Methode max() für verschiedene Datentypen implementiert. Die Methode max() liefert das Maximum der beiden Argumente. Beim Methodenaufruf wird die zu den Typen der Übergabeparameter passende Methode ausgewählt. Wird die Methode beispielsweise mit zwei int-Werten als Argument aufgerufen, wird die erste Methode verwendet.

static int max(int a, int b) {
    return a > b ? a : b;
}

static double max(double a, double b) {
    return a > b ? a : b;
}

static byte max(byte a, byte b) {
    return a > b ? a : b;
}

Wie funktioniert eine “for-each” Schleife in Java?

In Java wird die verkürzte for-Schleife oft als for-each-Schleife bezeichnet. Die verkürzte for-Schleife wird zum Iterieren über Arrays und Datenstrukturen, die das Iterable-Interface implementieren (List, Set, …), verwendet.

Seit Java Version 8 gibt es im Iterable-Interface die Methode forEach(), die in Verbindung mit Lambda-Ausdrücken verwendet wird.

Beispiel:

In Zeile 2 wird mit der verkürzten for-Schleife über ein Array iteriert. Die Variable n nimmt nacheinander alle Werte des Arrays a an.
In Zeile 11 werden alle Elemente einer Menge unter Zuhilfenahme der verkürzten for-Schleife aufgelistet.
In Zeile 15 werden unter Verwendung eines Lambda-Ausdrucks alle Elemente der Menge set mit der forEach() Methode ausgegeben.

int[] a = { 1, 4, 9, 16, 25 };
for (int n : a) {
    System.out.println(n);
}

SortedSet<String> set = new TreeSet<>();
set.add("Berta");
set.add("Caesar");
set.add("Anton");

for (String s : set) {
    System.out.println(s);
}

set.forEach(s -> {
    System.out.println(s);
});

Was sind verschachtelte Schleifen? Wann nutze ich verschachtelte Schleifen?

Eine verschachtelte Schleife ist eine Schleife, die in einer Schleife ausgeführt wird. Bei jedem Durchlauf der äußeren Schleife wird die innere Schleife ausgeführt. Das heißt, wenn die äußere Schleife n-mal ausgeführt wird und die innere Schleife als solche m-mal, dann wird der innere Schleifenkörper insgesamt n*m-mal durchlaufen.

Im folgenden Code wird diese Berechnung durchgeführt, indem bei jedem Durchlauf der inneren Schleife der Zähler count um 1 erhöht wird.

final int N = 200, M = 1000;
int count = 0;
for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        count++;
    }
}
System.out.println(count); // Ausgabe: 200000

Anwendungsmöglichkeiten von verschachtelten Schleifen:

  • Iterieren über mehrdimensionale Datenstrukturen wie Arrays, Bilddaten, …
  • Sortieralgorithmen, z.B. Bubble-Sort, Selection-Sort, …
  • Rechnen mit Matrizen

Im Folgenden wird gezeigt, wie man mit einer doppelten for-Schleife eine Addition von zwei Matrizen durchführt:

static double[][] add(double[][] x, double[][] y) {
    double[][] result = new double[x.length][x[0].length];
    for (int row = 0; row < result.length; row++) {
        for (int col = 0; col < result[0].length; col++) {
            result[row][col] = x[row][col] + y[row][col];
        }
    }
    return result;
}

public static void main(String[] args) {
    double[][] m1 = { { 1, 2 },
                      { 3, 4 }, 
                      { 5, 6 } };
    double[][] m2 = { { 2, 4 }, 
                      { 4, 6 }, 
                      { 5, 1 } };

    double[][] m3 = add(m1,m2);
    System.out.println(Arrays.deepToString(m3));
}

Wie funktioniert die Array-Zuweisung und das Kopieren von Arrays?

Zuweisung:

Arrays sind Referenzdatentypen. Variablen eines Referenzdatentyps beinhalten einen Verweis auf den Speicherort der Daten.

Wird ein Array a einer Variable b zugewiesen, wird das Array auf das a zeigt nicht kopiert, sondern eine zusätzliche Referenz b auf das Array gesetzt. Die Referenzen a und b zeigen also auf dasselbe Array. Wird über b ein Wert im Array verändert, greift auch a auf diesen veränderten Wert zu.

Beispiel:

int a[] = new int[10];
int b[] = a; // b ist Referenz auf das Array a, a und b zeigen auf dasselbe Array
b[2] = 123;  // Das von a und b referenzierte Array bekommt am Index 2 einen Wert zugewiesen
System.out.println(a[2]); // 123 wird ausgegeben
Kopieren:

In Java gibt es unterschiedliche Möglichkeiten ein Array zu kopieren. Einige davon sind in der folgenden Code-Sequenz ausgeführt.

  1. In den Zeilen 4 – 6 wird mit einer for-Schleife jedes Array-Element einzeln von a1 nach a3 kopiert.
  2. in Zeile 8 wird das Array mit der Methode arraycopy() der Klasse System kopiert. Die Parameter sind im Artikel “Wie erweitert man ein Array?” in unserer FAQ beschrieben.
  3. In Zeile 10 wird java.util.Arrays.copyOf() verwendet. Der erste Parameter der Methode ist das zu kopierende Array, der zweite Parameter ist die Größe des neuen Arrays.
  4. In Zeile 12 wird das Array mit der Methode clone() der Klasse Object kopiert.
int[] a = { 1, 4, 9, 16, 25 };
int[] b = new int[5];

for (int i = 0; i < a.length; i++) {
    b[i] = a[i];
}

System.arraycopy(a, 0, b, 0, a.length);

b = Arrays.copyOf(a, a.length);

b = a.clone();

Was ist der Unterschied zwischen Listen und Arrays in Java?

Im Unterschied zur Liste ist das Array keine dynamische Datenstruktur. Das Vergrößern eines Arrays ist nur durch Anlegen eines neuen Arrays und Kopieren des Inhalts des alten Arrays in das neue Array möglich.

Arbeitet man mit Daten fester Größe sind Arrays in der Regel performanter als Listen. Das gilt insbesondere beim Arbeiten mit primitiven Datentypen, da das Array im Unterschied zur Liste primitive Datentypen speichern kann. Will man in einer Liste mit primitiven Datentypen arbeiten, findet implizit immer eine aufwendige boxing oder unboxing Operation statt.

Listen sind vielseitig verwendbar, da oft benötigte Methoden wie contains(), addAll(), indexOf(), remove(), sort(), … bereits fertig implementiert und getestet sind.

Was versteht man unter einem dynamischen Array? Was versteht man unter einer dynamischen Datenstruktur?

Dynamische Arrays sind Arrays, die ihre Größe zur Laufzeit an den Bedarf anpassen können.

In Java gibt es keine dynamischen Arrays. Die Länge eines Arrays wird bei seiner Erzeugung festgelegt. Es besteht allerdings die Möglichkeit das Verhalten eines dynamischen Arrays nachzubilden, indem man bei Bedarf ein neues größeres Array anlegt und die Daten des alten Arrays in das neue Array kopiert. Mehr dazu findest du im Artikel “Wie erweitert man ein Array?” in unserer FAQ.

Oft ist es geschickter, die Klasse java.util.ArrayList zu verwenden, wenn man das Verhalten eines dynamischen Arrays benötigt.

Im Unterschied zu nicht dynamischen Datenstrukturen wie beispielsweise Arrays in Java, können dynamische Datenstrukturen mit ihrem Bedarf wachsen. Beispiele für dynamische Datenstrukturen in Java sind List, Set, Map, Stack oder der StringBuilder.

Ein Stack als dynamische Datenstruktur kann in Java als vekettete Liste wie folgt implementiert werden:

public class Stack<T> {

    private class Node<T1> {
        T1 data;
        Node<T1> next;
    }

    private Node<T> head = null;

    public void push(T elem) {
        Node<T> temp = head;
        head = new Node<T>();
        head.data = elem;
        head.next = temp;
    }

    public T pop() {
        T temp = head.data;
        head = head.next;
        return temp;
    }

    public boolean isEmpty() {
        return head == null;
    }

    public static void main(String[] args) {
        Stack<String> st = new Stack<>();
        st.push("Anton");
        st.push("Berta");
        st.push("Caesar");

        while (!st.isEmpty()) {
            System.out.println(st.pop()); // Ausgabe: Caesar, Berta, Anton
        }
    }
}

Zur Implementierung der push-Methode wird die Referenz temp auf den zuletzt eingefügten Knoten gesetzt (Zeile 11). Somit ist sicher gestellt, dass ein neuer Knoten als head eingefügt werden kann (Zeile 12). Die Referenz des neuen Knoten (head.next) wird auf sein Nachfolgeelement temp gesetzt (Zeile 14). Dieser Ablauf ist in der Abbildung Stack push dargestellt.

Stack push

Zur Implementierung der pop-Methode wird der zuletzt eingefügte Wert auf die Variable temp gesichert (Zeile 18). Damit sind die Voraussetzungen geschaffen, dass die Referenz head auf den in Abbildung Stack pop als C bezeichneten Knoten gesetzt werden kann (Zeile 19). Der Wert von temp wird zurückgegeben (Zeile 20).

Stack pop

Was genau ist die Funktion eines Konstruktors? Was genau machen “Getter” und “Setter”?

Der Konstruktor ist eine Methode, die beim Erzeugen eines Objekts (new, …) aufgerufen wird. Er dient oft zum Initialisieren der Instanzvariablen.

Getter und Setter sind Methoden, die verwendet werden, um Eigenschaften des Objekts abzufragen oder zu setzen. Oft werden sie für den Zugriff auf private Instanzvariablen verwendet. Bei einem gekapselte Zugriff über Methoden besteht die Möglichkeit die Gültigkeit der übergebenen Parameter zu überprüfen,

Beispiel:
Die Klasse Item dient zur Verwaltung von Gegenständen / Artikeln. Jeder Artikel hat einen Namen und eine Anzahl. Der Name muss eine Mindestlänge besitzen. Die Anzahl eines Artikels darf nicht negativ sein.

class Item {
    private String name;
    private int count;

    public Item(String name) {
        this(name, 0);
    }

    public Item(String name, int count) {
        setName(name);
        setCount(count);
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        if (name == null || name.length() < 3) {
            throw new IllegalArgumentException("Name muss mindestens 3 Zeichen haben.");
        }
        this.name = name;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        if (count < 0) {
            throw new IllegalArgumentException("Anzahl darf nicht negativ sein.");
        }
        this.count = count;
    }
}

Es gibt zwei Konstruktoren. Der Konstruktor in Zeile 5 wird verwendet, um einen Artikel mit der Stückzahl 0 zu erzeugen. Mit this(name, 0) in Zeile 6 wird der Konstruktor in Zeile 9 aufgerufen. Dieser setzt die Attribute nicht direkt, sondern delegiert das Setzen der Instanzvariablen an die Setter in Zeile 18 und 29.
In Zeile 19 und 30 wird überprüft, ob die zu setzenden Attribute den oben beschriebenen Regeln entsprechen.

Wieso sollten immer die kleinstmöglichen Datentypen ausgewählt werden?

Datentypen sollten so klein wie möglich und so groß wie notwendig gewählt werden.

Je kleiner der Datentyp ist, desto geringer ist der Ressourcenverbrauch. Durch Wahl eines geeigneten Datentyps kann Datenspeicher effizient genutzt oder bei der Datenübertragung Datenvolumen klein gehalten und damit die Übertragungszeit optimiert werden.

Allerdings kann ein zu klein gewählter Datentyp zu folgenschweren Fehlern führen, wie beispielsweise das Jahr-2000-Problem eindrucksvoll illustriert.

Wann benutze ich den Datentyp “boolean”? Kann ich damit rechnen?

Ein boolean wird zum Repräsentieren eines Wahrheitswertes verwendet. Er kann die zwei Werte true oder false annehmen.

Mit diesem Datentyp kann man in Java nicht numerisch rechnen. Wahrheitswerte werden mit logischen Operatoren verknüpft und mit den Regeln der boolschen Algebra berechnet.

Im folgenden Beispiel wird der Datentyp boolean verwendet, um zu speichern, ob bestimmte Eigenschaften zutreffen oder nicht zutreffen.

class Fahrzeug {
    String name;
    double preis;

    boolean klimaanlage;
    boolean allrad;
    boolean automatikGetriebe;
}
public static void main(String[] args) {
    List<Fahrzeug> list = new ArrayList<>();
    // ...
    for (Fahrzeug f : list) {
        if (f.klimaanlage && f.allrad && f.preis < 1000) {
            System.out.println("Willhaben ...");
        }
    }
}

Was ist der Unterschied zwischen “int” und “Integer” im Java?

Im Unterschied zum Datentyp Integer ist int ein primitiver Datentyp. Objekte der Klasse Integer speichern einen int. Darüber hinaus bietet die Klasse Integer verschiedene Methoden wie parseInt(), toHexString(), reverse(), valueOf(), … zum Arbeiten mit Ganzzahlen an.

Datenstrukturen wie List, Set, Map, … arbeiten mit Elementen, die von Object abgeleitet sind. In solchen Datenstrukturen kann man keine primitiven Datentypen verwenden, sondern muss zum Speichern von numerischen Werten auf Objekte der Klassen Integer, Double, … zurückgreifen.

In Java erfolgt eine Umwandlung zwischen int und Integer implizit (boxing, unboxing).

List<Integer> li = new ArrayList<>();
li.add(new Integer(123)); 
li.add(456);            // boxing
int j = li.get(1);      // unboxing

Wo werden globale Variablen deklariert? Wie wird darauf zugegriffen?

Globale Variablen (Instanzvariablen und Klassenvariablen) werden außerhalb von Methoden im Body einer Klasse deklariert.

Auf globale Instanzvariablen kann in jeder nicht-statischen Methode zugegriffen werden. Da Instanzvariablen zu einem Objekt gehören, benötigt man in einer statischen Methode eine Referenz auf ein Objekt der Klasse, damit auf die globalen Instanzvariablen des Objekts zugegriffen werden kann.

Auf globale Klassenvariablen (static) kann in jeder statischen und nicht-statischen Methode der Klasse zugegriffen werden.

Was ist der Unterschied zwischen Collection und List?

Eine java.util.Collection ist ein Interface für eine Sammlung von Daten. Das Collection-Interface bietet keine Möglichkeit, indiziert auf die Elemente der Collection zuzugreifen. Das Collection-Interface implementiert das Iterable-Interface und ermöglicht damit den Zugriff auf alle Elemente der Collection.

Datenstrukturen die das Collection-Interface implementieren sind: List, Set, Queue, …

Im folgenden Beispiel wird gezeigt, wie man mit einem Iterator über eine Collection iteriert:

Collection<Integer> c = new HashSet<>();
c.add(123);
c.add(456);

Iterator<?> it = c.iterator();
while (it.hasNext()) {
    System.out.println(it.next());
}

Eine einfachere Möglichkeit über eine Collection zu iterieren ist die verkürzte for-Schleife. Auch hier wird intern mit einem Iterator gearbeitet.

for (Integer n : c) {
    System.out.println(n);
}

Auch die List implementiert das Collection-Interface und bietet darüber hinaus die Möglichkeit, indiziert (wie bei einem Array) auf die Daten zuzugreifen:

List<Integer> li = new ArrayList<>();
li.add(123);
li.add(456);
for (int i = 0; i < li.size(); i++) {
    System.out.println(li.get(i));
}

Muss ich allen Variablen gleich bei der Deklaration einen Wert zuweisen?

Nein, es muss nur gewährleistet sein, dass die Variable beim ersten lesenden Zugriff belegt ist. Dabei ist zu berücksichtigen, dass Klassenvariablen und Instanzvariablen im Unterschied zu lokalen Variablen eine Default-Initialisierung haben.

Lokale Variablen müssen vor ihrem ersten lesenden Zugriff einen Wert zugewiesen bekommen.

Wie verbindet man eine Datenbank mit einem Programm?

Um ein Java-Programm mit einer Datenbank zu verbinden muss in einem ersten Schritt ein passender Connector zum Projekt hinzugefügt werden. Das heißt, für die Verbindung mit einer konkreten Datenbank (mysql, Oracle, …) müssen bestimmte herstellerspezifische Dateien in das Projekt eingebunden werden.

Die Datei zum Verbinden mit einer mysql-Datenbank (mysql-Connector) findet man hier: Download mysql-Connector.
Die Datei mysql-connector-java-8.0.24.jar aus dem zip-Archiv wird in das Projekt kopiert und in den Build-Path aufgenommen.

Im Folgenden wird die Verbindung mit einer mysql-Datenbank beschrieben.

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class DB_Test {

    public static void main(String args[]) {
        String url = "jdbc:mysql://localhost:3306/test";
        String username = "root";
        String password = "";

        try {
            Class.forName("com.mysql.cj.jdbc.Driver");
            Connection con = DriverManager.getConnection(url, username, password);
            PreparedStatement stmt = con.prepareStatement("select * from person");
            ResultSet rs = stmt.executeQuery();

            while (rs.next()) {
                System.out.printf("| %10d | %20s |\n", rs.getInt("id"), rs.getString("name"));
            }
            con.close();
        } catch (SQLException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

In Zeile 10 wird die Adresse und der Name der Datenbank (test) angegeben.
In Zeile 15 wird mit Hilfe von Reflection der Datenbanktreiber geladen.
In Zeile 16 wird die Verbindung zur Datenbank aufgebaut.
In Zeile 17 und 18 wird auf die Verbindung zugegriffen, um eine Abfrage durchzuführen. Im Anschluss daran wird zur Ausgabe der Ergebnisse über das von der Datenbank erhaltene ResultSet iteriert.
In Zeile 23 wird die in Zeile 16 geöffnete Datenbankverbindung geschlossen.

Ist es in einer for-Schleife möglich, das Abbruchkriterium der Schleife dynamisch anzupassen?

Das Abbruchkriterium in einer for-Schleife kann auf unterschiedliche Weise dynamisch angepasst werden.

Eine Möglichkeit ist die Verwendung einer boolean-Variable als Abbruchkriterium. Im folgenden Beispiel ist der boolean doIt initial true. Beim Eintreten eines bestimmten Ereignisses (i == 5) wird er in der Schleife auf false gesetzt und bewirkt einen Abbruch der Schleife.

boolean doIt = true;
for (int i = 0; doIt; i++) {
    if (i == 5) {
        doIt = false;
    }
    System.out.println(i);
}
// Ausgabe: 0 1 2 3 4 5

Eine weitere Möglichkeit die Schleife bei einem bestimmten Ereignis zu beenden, ist die Verwendung der break-Anweisung. Die break-Anweisung bewirkt, dass die Schleife sofort verlassen wird. Im folgenden Beispiel wird die Schleife beendet, wenn in einem Text zwei Mal das gleiche Zeichen in Folge auftritt.

char ca[] = "Hello".toCharArray();
char lastChar = 0;
for (int i = 0; i < ca.length; i++) {
    char aktChar = ca[i];
    if (lastChar == aktChar) {
        System.out.println("Zwei gleiche Zeichen in Folge: " + aktChar);
        break;
    }
    lastChar = aktChar;
}
// Ausgabe: Zwei gleiche Zeichen in Folge: l

Im letzten Beispiel wird ein Histogramm gezeichnet. Das Abbruchkriterium der inneren Schleife wird durch die Daten in einem Array bestimmt.

int a[] = { 2, 3, 7, 14, 8, 4, 3 };

for (int i = 0; i < a.length; i++) {
    for (int j = 0; j < a[i]; j++) {
        System.out.print('*');
    }
    System.out.println();
}
// Ausgabe:
// **
// ***
// *******
// **************
// ********
// ****
// ***

Gibt es eine Schleife, die beim ersten Durchlauf nicht die Bedingung prüft?

Beim ersten Durchlauf einer do-while-Schleife wird die Bedingung nicht überprüft.

Beispiel:

public static void main(String[] args) {
    int i = -1;
    do {
        System.out.println(i++);
    } while (i > 0);
}

// Ausgabe: -1

Die do-while-Schleife wird mindestens einmal durchlaufen, da die Schleifenbedingung erst am Ende jedes Schleifendurchlaufs überprüft wird.

Was ist eine Schleife (loop) und welche Arten gibt es?

Eine Schleife besteht aus einer Schleifenbedingung und einem Schleifenkörper. Die Schleife ist eine Wiederholstruktur. Die Instruktionen des Schleifenkörpers werden solange wiederholt ausgeführt, bis die Schleifenbedingung nicht mehr erfüllt ist. In Java unterscheidet man die while-Schleife, die do-while-Schleife, die for-Schleife und die foreach-Schleife.

  • while-Schleife: Bei dieser Schleife wird die Schleifenbedingung am Beginn – also vor der Ausführung des Schleifenkörpers – ausgewertet.
  • do-while-Schleife: Bei dieser Schleife wird die Schleifenbedingung am Ende – also nach der Ausführung des Schleifenkörpers – ausgewertet.
    Das hat zur Konsequenz, dass der Schleifenkörper mindestens ein Mal durchlaufen wird.
    Anwendung findet diese Schleife beispielsweise bei der Evaluierung von Benutzereingaben auf der Konsole, da man die Eingabe vor der Auswertung lesen muss.
    Im folgenden Beispiel wird von der Konsole gelesen, bis der leere String eingegeben wird.
public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    String s;
    do {
        s = sc.nextLine();
        System.out.println(s.toUpperCase());
    } while (!s.isEmpty());
    System.out.println(s);
    sc.close();
}
  • for-Schleife: Genauso wie bei der while-Schleife wird bei der for-Schleife die Schleifenbedingung vor der Ausführung des Schleifenkörpers ausgewertet. Diese Schleife findet Anwendung, wenn die Anzahl der Schleifendurchläufe bekannt ist. Die for-Anweisung besteht aus:
    • Initialisierungsteil – Der Initialisierungsteil wird oft zum Initialisieren einer Zählvariable verwendet
    • Schleifenbedingung
    • Inkrementierungsteil – Der Inkrementierungsteil wird am Ende jeder Iteration ausgeführt. An dieser Stelle können verschiedene Anweisungen stehen. In der Regel wird dieser Teil aber zum Inkrementieren der Zählvariable verwendet.
  • foreach-Schleife: Die foreach-Schleife wird auch als verkürzte for-Schleife bezeichnet. Sie ermöglicht ein einfaches Iterieren über Arrays und Objekte, die das Iterable-Interface implementieren (List, Set, …).

Mehr dazu findet man hier.

Was ist ein Stack?

Ein Stack (Stapelspeicher, Kellerspeicher) ist eine dynamische Datenstruktur. Elemente können nur oben auf dem Stapel abgelegt oder von oben entnommen werden. Das heißt, das letzte Element, das abgelegt wurde, wird als erstes entnommen. Daher bezeichnet man diese Datenstruktur auch als LIFO (Last-In-First-Out) Struktur.

Verwendet wird diese Datenstruktur von vielen Compilern, um Argumente, lokale Variablen, den Rückgabewert und die Rücksprungadresse bei einem Methodenaufruf abzulegen.

Die wichtigsten Zugriffsmethoden eines Stacks sind:

  • push(x) – legt das Element x auf den Stack
  • pop() – entnimmt das oberste Element und gibt es zurück
  • peek() – gibt das Element zurück ohne den Stack zu verändern

Im folgenden Beispiel sieht man die Umsetzung eines generischen Stacks. Der Stack wurde mit Hilfe eines Arrays implementiert.

import java.util.Arrays;

public class Stack<T> {
    private Object[] data;
    private int sp;

    public Stack() {
        data = new Object[10];
        sp = 0;
    }

    public void push(T elem) {
        if (sp == data.length) {
            data = Arrays.copyOf(data, 2 * data.length);
        }
        data[sp++] = elem;
    }

    @SuppressWarnings("unchecked")
    public T pop() {
        return (T) data[--sp];
    }

    public int size() {
        return sp;
    }
}

Eine interessante Anwendung des Stacks ist die umgekehrte polnische Notation, mit der Taschenrechner realisiert wurden. Diese Notation erlaubt die Formulierung beliebig komplizierter mathematischer Ausdrücke ohne Klammern.

Im Folgenden wird die Anwendung eines Stacks anhand eines Beispiels der umgekehrten polnischen Notation erläutert.

2 + (3 * 4)
===========

|     |     |     |     |     |     |     |     |     |
|     |     |     |     |  4  |     |     |     |     |
|     |     |  3  |     |  3  |     | 12  |     |     |
|  2  |     |  2  |     |  2  |     |  2  |     | 14  |
 -----       -----       -----       -----       -----
push(2)     push(3)     push(4)        *           +

Mit Hilfe unserer Stack-Implementierung kann diese Berechnung folgendermaßen ausgeführt werden:

    Stack<Integer> st = new Stack<>();
    st.push(2);
    st.push(3);
    st.push(4);
    System.out.println(st);       // Ausgabe: 2, 3, 4,
    st.push(st.pop() * st.pop());
    System.out.println(st);       // Ausgabe: 2, 12,
    st.push(st.pop() + st.pop());
    System.out.println(st);       // Ausgabe: 14,

Muss ich Arrays immer über eine Schleife ausgeben lassen? Gibt es eine effizientere Methode?

Eine einfache Möglichkeit für die Ausgabe eines Arrays bietet die Methode toString() der Klasse java.util.Arrays.

Beispiel:

int a[] = { 2, 4, 1, 3 };
System.out.println(Arrays.toString(a)); // Ausgabe: [2, 4, 1, 3]

Wie viele Elemente kann ein Array maximal haben?

Der Array-Index ist vom Typ int. Dadurch ist die Arraygröße auf Integer.MAX_VALUE (= 231 – 1 = 2147483647) begrenzt. Zusätzliche Einschränkungen ergeben sich durch die virtuelle Maschine und die Größe des Heap-Space.

Wie erweitert man ein Array?

In Java haben Arrays eine feste Größe. Um ein Array zu erweitern, muss zuerst ein zweites, größeres Array angelegt werden, in das die Elemente des kleineren Arrays kopiert werden. Im Anschluss daran wird die Referenz auf das größere Array gesetzt.

Im Folgenden werden drei Implementierungen zur Erweiterung eines Arrays ausgeführt.

1. Implementierung mit einer for-Schleife:
int a[] = { 2, 4, 1, 3 };

int temp[] = new int[2 * a.length];
for (int i = 0; i < a.length; i++) {
    temp[i] = a[i];
}
a = temp;

System.out.println(Arrays.toString(a)); // Ausgabe: [2, 4, 1, 3, 0, 0, 0, 0]
2. Verwendung der Methode arraycopy():

Bei dieser Implementierung wird zum Kopieren der Array-Elemente die Methode arraycopy() der Klasse System verwendet.

public static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length)

Diese Methode hat folgende 5 Parameter:

  1. src – Referenz auf das zu kopierende Array
  2. srcPos – Index des ersten Elements von src, das kopiert werden soll
  3. dest – Referenz auf das Ziel-Array
  4. destPos – Index des ersten Elements des Arrays, in das geschrieben werden soll
  5. length – Anzahl der Elemente, die kopiert werden sollen

Beispiel:

int a[] = { 2, 4, 1, 3 };

int temp[] = new int[2 * a.length];
System.arraycopy(a, 0, temp, 0, a.length);
a = temp;

System.out.println(Arrays.toString(a)); 
3. Verwendung der Methode copyOf():

Bei der Implementierung mit der Methode copyOf() der Klasse java.util.Arrays erfolgt das Erzeugen des vergrößerten Arrays und das Kopieren der Daten mit nur einem Methodenaufruf. Intern verwendet diese Methode System.arraycopy().

int a[] = { 2, 4, 1, 3 };
a = Arrays.copyOf(a, 2 * a.length);
System.out.println(Arrays.toString(a));

Bei größeren Datenmengen kann mit der Methode arraycopy() oder copyOf() eine Steigerung der Performanz gegenüber der Verwendung einer for-Schleife erzielt werden. Die Methode arraycopy() ist eine sogenannte native-Methode. Sie wird nicht von der virtuellen Maschine ausgeführt, sondern arbeitet mit Maschinencode für die jeweilige Rechnerarchitektur.

Wie kann ich int in double umwandeln? Wie wird aus einem int ein String?

Wie kann ich int in double umwandeln?

Wird einer double Variable ein int zugewiesen, erfolgt ein impliziter Cast nach double.

int i = 7;
double x = i;
System.out.println(x); // Ausgabe: 7.0

Ist bei mathematischen Operationen mindestens ein Operand vom Typ double, erfolgt die Berechnung mit dem Typ double:

int n = 4;
System.out.println(1 / n);
System.out.println(1.0 / n);
double x = 1 / n;
System.out.println(1 / 2 / 2.0);
System.out.println(1 / 2.0 / 2);

In Zeile 2 sind beide Operanden vom Typ int. Die Division wird ganzzahlig ausgeführt. Das Ergebnis ist 0.
In Zeile 3 ist einer der Operanden (1.0) vom Typ double. Die Division wird mit double ausgeführt. Das Ergebnis ist 0.25.

Achtung: Der Datentyp auf der linken Seite hat keinen Einfluss auf die Datentypen bei der Berechnung des Ausdrucks auf der rechten Seite. In Zeile 4 bekommt x den Wert 0.0 zugewiesen.
Weiters ist darauf zu achten, dass die Ausdrücke von links nach rechts ausgewertet werden. In Zeile 5 wird zuerst die ganzzahlige Division 1 / 2 berechnet. Das Ergebnis dieser Division ist 0. Damit ergibt die Division durch 2.0 den Wert 0.0.
Anders verhält es sich in Zeile 6. Hier erfolgt die erste Division (1 / 2.0) mit double-Werten. Damit werden alle nachfolgenden Operationen mit double-Werten durchgeführt. Das Ergebnis ist 0.25.

Ein int kann freilich auch mit einem expliziten Cast in einen double umgewandelt werden:

int n = 4;
System.out.println(1 / (double)n);
Wie wird aus einem int ein String?

Das geht mit der Methode valueOf() der Klasse String oder der Methode toString() der Klasse Integer:

int n = 4;
String s1 = String.valueOf(n);
String s2 = Integer.toString(n);

Eine einfache (wenngleich nicht besonders elegante) Lösung ist die Verkettung des int mit einem String:

int n = 4;    
String s = n + "";

Wann soll ich mir Gedanken über die Wahl von Datentypen machen?

Jedes Mal, wenn du Variablen oder Rückgabewerte definierst und verwendest musst du dir Gedanken über Datentypen machen.

Jeder Datentyp beschreibt bestimmte Verwendungszwecke. Soll beispielsweise mit einer Zählvariable durch eine Datenstruktur iteriert werden, oder soll ein Vergleich auf Identität mit ganzen Zahlen erfolgen, nimmt man int. Die Verwendung von Fließpunktzahlen als Zählvariable kann beim Vergleich auf Identität fehlschlagen, da nicht jede Zahl exakt als Fließpunktzahl darstellbar ist. Aufgrund der internen Zahlendarstellung haben Fließpunktzahlen eine begrenzte Genauigkeit.

double x = 0.0;

while (x != 1) {
    x += 0.1;
    System.out.println(x);
}

In diesem Beispiel wird die Schleife – entgegen der Erwartung – nicht nach dem 10-ten Durchlauf abgebrochen.

Betrachtet man die Ausgabe unten, sieht man, dass verschiedene Werte nur näherungsweise dargestellt werden.

0.1
0.2
0.30000000000000004
0.4
0.5
0.6
0.7
0.7999999999999999
0.8999999999999999
0.9999999999999999
1.0999999999999999
1.2
...

Bei mathematischen Berechnungen ist oft ein großer Wertebereich wichtig. Hier nimmt man meist double als Datentyp.

Bei Datensammlungen ist es wichtig, die zur Aufgabe passende Datenstruktur (Array, List, Set, Map, Queue, Stack, …) zu finden. Jede dieser Datenstrukturen bietet spezifische Möglichkeiten und Einschränkungen. Die richtige Wahl hat Einfluss auf die Komplexität des Sourcecodes und die Performance – ist also entscheidend für die Lesbarkeit und Wiederverwendbarkeit des Codes sowie die Ausführungsgeschwindigkeit des Programms.

Was ist der Unterschied zwischen “continue” and “break” and “pass”?

Mit der break-Anweisung wird eine Schleife abgebrochen. Der Schleifenkörper wird verlassen und der Programmlauf mit der Instruktion unterhalb der Schleife fortgesetzt.

Mit der continue-Anweisung wird eine Schleifeniteration abgebrochen und mit der nächsten weiter gemacht.

Mehr dazu findest du hier.

In Python gibt es die pass-Anweisung. Diese Anweisung bewirkt nichts. Sie wird als Platzhalter an Stellen verwendet, an denen aus syntaktischen Gründen eine Anweisung erwartet wird.

Wie wandelt man einen String in ein char-Array um um?

Zur Umwandlung eines Strings in ein char-Array gibt es in der Klasse String die Methode toCharArray().

String s = "Hello World!";
char[] ca = s.toCharArray();
System.out.println(Arrays.toString(ca));

// Ausgabe: [H, e, l, l, o,  , W, o, r, l, d, !]

Alternativ kann man mit der Methode charAt() auf die einzelnen Zeichen im String zugreifen und die Zeichen in ein Array ablegen:

String s = "Hello World!";

char[] ca = new char[s.length()];
for (int i = 0; i < ca.length; i++) {
    ca[i] = s.charAt(i);
}
System.out.println(Arrays.toString(ca));

// Ausgabe: [H, e, l, l, o,  , W, o, r, l, d, !]

Anmerkung:
Um aus einem char-Array wieder einen String zu erzeugen, kann man wie folgt vorgehen:

char ca[] = "Hello".toCharArray();
String s = String.valueOf(ca);

Warum hat Java keinen “GOTO”-Befehl?

Der goto-Befehl führt oft zu unübersichtlichem und schwer lesbarem Code. Verwendet man viele goto-Anweisungen kann der Kontrollfluss nur schwer nachvollzogen werden.

Welcher Unterschied ist zwischen dem Befehl “x++” und “++x” genau?

Ein Unterschied besteht nur dann, wenn der Ausdruck in der rechten Seite einer Zuweisung vorkommt oder als Argument für einen Methodenaufruf oder für einen Vergleich verwendet wird.

int a, b;

a = 1;
b = a++ * 3;

a = 1;
b = ++a * 3;

In Zeile 4 wird zuerst der Wert von a gelesen, dann a inkrementiert, dann der gelesene Wert mit 3 multipliziert. b wird der Wert 3 zugewiesen, a hat den Wert 2. Die Operation a++ bezeichnet man auch als Post-Inkrement.
In Zeile 7 wird a inkrementiert, dann wird der inkrementierte Wert mit 3 multipliziert. b wird der Wert 6 zugewiesen, a hat den Wert 2. Die Operation ++a bezeichnet man auch als Pre-Inkrement.

Wann verwende ich “if” und wann “switch”?

Grundsätzlich gilt: Jedes switch-case Statement kann auf ein if-else Statement abgebildet werden, aber nicht jedes if-else Statement kann durch ein switch-case Statement ausgedrückt werden.

Das switch-case Statement wird verwendet, um eine Variable mit mehreren Konstanten zu vergleichen. Das Ergebnis dieses Vergleichs dient zur Auswahl genau einer Option. Die Variable wird dem switch() als Argument übergeben. Die Konstanten sind den einzelnen Fällen (case) zugeordnet.

Das if-else Statement wird verwendet, wenn die Verzweigung nur wenige Optionen umfasst oder wenn beliebige boolsche Ausdrücke als Bedingung verwendet werden. Soll in einem Programm beispielsweise geprüft werden, ob ein Wert in einem bestimmten Bereich liegt, implementiert man einen if-else Konstrukt:

static String temperaturAuswertung(int temperature) {
    if (temperature > 30) {
        return "Heiss";
    } else if (temperature > 15) {
        return "Mittel";
    } else if (temperature > -20) {
        return "Kalt";
    } else {
        return "Saukalt";
    }
}

Switch-case-Statements sind oft übersichtlicher als eine Kaskade von if-else Anweisungen. Die Lesbarkeit wird verbessert.

Verwendet man switch-case-Statements mit Enumerationen, kann automatisch überprüft werden, ob alle in der Enumeration gegebenen Möglichkeiten im switch-case-Statement abgedeckt sind. Diese Zusicherung hilft Fehler zu vermeiden:

enum Color {
    rot, grün, blau
}

void doIt(Color c) {
    switch (c) {
        case rot:
            // ...
            break;
        case grün:
            // ...
            break;
        case blau:
            // ...
            break;
    }
}

Was sind Instanzvariablen? Was sind Klassenvariablen?

Instanzvariablen:

Instanzvariablen gehören zu einer Instanz einer Klasse – also einem Objekt. Jedes Objekt verfügt über seine eigenen Instanzvariablen. Auf eine Instanzvariable v kann mit this.v zugegriffen werden. Falls keine Verwechslungsmöglichkeit mit einer lokalen Variable gleichen Namens besteht, kann man this auch weglassen.

Klassenvariablen:

Klassenvariablen gehören zu einer Klasse. Unabhängig von der Anzahl der von dieser Klasse erzeugten Objekte existieren sie genau ein Mal. Klassenvariablen werden auch als statische Variablen bezeichnet (Schlüsselwort: static).

Beispiel:

class Person {
    private static int count; // Klassenvariable
    private String name;      // Instanzvariable

    Person(String name) {
        this.name = name;
        count++;
    }

    String getName() {
        return name;
    }

    int getCount() {
        return count;
    }
}

class Test {
    public static void main(String[] args) {
        Person p1 = new Person("Anna");
        Person p2 = new Person("Berta");
        Person p3 = new Person("Caesar");

        System.out.println(p3.getCount());  // Ausgabe: 3
    }
}

Jedes Mal wenn eine Instanz angelegt wird, wird die statische Variable count im Konstruktor inkrementiert. Am Ende des Programmlaufs hat count den Wert 3.

Welche Arten von Variablen gibt es in der Programmierung in Java?

Die Einteilung der Variablen kann nach verschiedenen Kategorien erfolgen:

  • Zugriffsmodifikatoren: Mit den Zugriffsmodifikatoren wird festgelegt, wo eine Klassen- oder Instanzvariable sichtbar ist.
    • private – innerhalb der Klasse sichtbar
    • protected – innerhalb der Vererbungshierarchie sichtbar
    • public – keine Einschränkung der Sichtbarkeit
    • Ist kein Modifikator angegeben, ist die Variable im Paket sichtbar.
      Mehr dazu findest du im Artikel “Wie wird ein Scope in Java definiert?” in unserer FAQ.
  • Global / Local: Neben den Zugriffsmodifikatoren ist die Sichtbarkeit einer Variable dadurch bestimmt, ob sie innerhalb oder außerhalb einer Methode deklariert wird.
    • Global – Eine globale Variable wird außerhalb von Methoden deklariert und als Instanz- oder Klassenvariable bezeichnet. Die Lebensdauer einer Instanzvariable entspricht der Lebensdauer des Objekt, in dem die Variable deklariert wurde.
    • Local – Eine lokale Variable wird innerhalb eines Blocks deklariert und ist nur innerhalb dieses Blocks sichtbar. Die Lebensdauer einer lokalen Variable endet mit der Ausführung des Blocks, in dem sie deklariert wurde.
      Mehr dazu findest du im Artikel “Was ist der Unterschied zwischen einer lokalen und einer globalen Variable?” in unserer FAQ.
  • Zugehörigkeit
  • Typ

Anmerkungen:

  • Konstanten werden oft als final-Variablen bezeichnet. Eine mit dem Modifikator final deklarierte Variable muss initialisiert werden und kann danach ihren Wert nicht mehr ändern.
  • Mit dem Schlüsselwort transient deklarierte Variablen werden bei der Serialisierung von Objekten nicht gespeichert.

Wie kann ich mit “Out.print(String.format())” linksbündige bzw. rechtsbündige Ausgaben erzeugen?

Im folgenden Beispiel werden für die Ausgabe eines Strings 15 Positionen (%15s) und für die Ausgabe eines int 10 Positionen (%10d) reserviert. Ist der String kürzer als 15 Zeichen, wird links mit Leerzeichen aufgefüllt.
Wird der Längenangabe ein Minus-Zeichen vorangestellt (%-15s), wird rechts mit Leerzeichen aufgefüllt.

String s1 = "Hello World", s2 = "foo";
int n1 = 123, n2 = 12345678;
        
Out.print(String.format("|%15s|%10d|\n",   s1, n1)); // Ausgabe: |    Hello World|       123| 
Out.print(String.format("|%15s|%10d|\n",   s2, n2)); // Ausgabe: |            foo|  12345678|

Out.print(String.format("|%-15s|%-10d|\n", s1, n1)); // Ausgabe: |Hello World    |123       | 
Out.print(String.format("|%-15s|%-10d|\n", s2, n2)); // Ausgabe: |foo            |12345678  |

Wofür braucht man einen float genau?

Genauso wie double verwendet man float zur Darstellung von Dezimalzahlen. Der Datentyp float benötigt weniger Speicher, hat dafür aber eine geringere Genauigkeit und einen kleineren Wertebereich als double.

DatentypSignifikante DezimalstellenWertebereichSpeicherbedarf
float7 – 8-3,4*1038 … 3,4*103832 Bit / 4 Byte
double15 – 16-1,7*10308 … 1,7*1030864 Bit / 8 Byte
Datentypen für Fließpunktzahlen

Benötigt man bei einer großen Menge zu verarbeitender Daten die Genauigkeit von double nicht, kann bei Verwendung von float der Speicherbedarf halbiert werden.

Anwendungsbeispiele für den sinnvollen Einsatz des Datentyps float findet man in der Bildverarbeitung.

Wie kann man herausfinden, ob es in der Java-Bibliothek fertige Klassen für ein konkretes Problem gibt?

Je spezifischer das Problem ist, desto mehr wird man auf Suchmaschinen und entsprechende Foren angewiesen sein. Um sich einen grundlegenden Überblick über die wichtigsten Klassen der Java-Klassenbibliotheken zu verschaffen, kann es freilich sehr zielführend sein, ein Buch zu Rate zu ziehen:

Für was steht das String[] args bei der main-Methode?

Mit Hilfe dieses String-Arrays können der main-Methode beim Start des Programms Parameter übergeben werden. Oft hat dieses Array den Namen args.

Beispiel:

public class Pythagoras {
    public static void main(String[] args) {
        if (args.length != 2) {
            System.out.println("Geben Sie zwei Zahlen als Argument an.");
            return;
        }

        double a = Double.parseDouble(args[0]);
        double b = Double.parseDouble(args[1]);
        System.out.println(Math.sqrt(a * a + b * b));
    }
}

Auf der Kommandozeile werden die Argumente wie folgt angegeben:

> javac Pythagoras.java
> java Pythagoras 3 4
  5.0
>

In der Entwicklungsumgebung Eclipse werden die Argumente wie folgt angegeben: Run > Run Configuration > Arguments

Warum werden .java-files nicht direkt ausgeführt? Wie führt man Java-files aus?

Warum werden .java-files nicht direkt ausgeführt?

Man kann Java-Dateien nicht direkt ausführen, da in Java mit dem Compiler in einem ersten Schritt ein auf der virtuellen Maschine lauffähiger Bytecode (.class-Dateien) erzeugt werden muss. Erst dieser Bytecode kann dann von der virtuellen Maschine verarbeitet werden.

Im Unterschied dazu werden Skriptsprachen – wie beispielsweise Python – mit einem Interpreter ausgeführt. Ist für eine Skriptsprache ein passender Interpreter installiert, kann die Datei direkt ausgeführt werden.

Wie führt man Java-files aus?

Ein Java-Programm kann auf der Kommandozeile folgendermaßen übersetzt und ausgeführt werden:

> cd <Verzeichnis mit sourcecode>
> javac MyProg.java
> java MyProg

Was sind formale Parameter? Was sind aktuelle Parameter? Wie viele Parameter sind zu viele?

Formale Parameter werden beim Deklarieren einer Methode angegeben. In Java haben sie einen Namen und einen Typ.

Aktuelle Parameter sind die Werte, die beim Aufruf der Methode übergeben werden.

import java.time.LocalDate;

public class Persons {

    public void addPerson(String name, LocalDate birthday, int gender) {
        // ...
    }

    public static void main(String[] args) {
        Persons p = new Persons();
        p.addPerson("Franz", LocalDate.of(2000, 1, 1), 2);
    }
}

In Zeile 5 werden für die Methode addPerson() die formalen Parameter String name, LocalDate birthday, int gender festgelegt.
In Zeile 11 wird die Methode mit den aktuellen Parametern aufgerufen.

Wie viele Parameter sind zu viele?

Ein Richtwert für eine sinnvolle Obergrenze von Methoden-Parametern ergibt sich aus der Millerschen Zahl.
Die Millersche Zahl gibt die Anzahl der Informationseinheiten an, die der Mensch im Kurzzeitgedächtnis halten kann. Der Wert dieser Zahl beträgt 7 ± 2.

Da sich also der Mensch in der Regel nicht mehr als 7 Informationseinheiten kurzzeitig merken kann, sind mehr als 7 Parameter für eine Methode problematisch.

Natürlich hängt das auch von der jeweiligen Methode ab. Manchmal ist die Reihenfolge der Parameter durch eine Konvention gegeben oder aus dem Methodennamen ersichtlich:

void setRGBcolor(int red, int green, int blue) { ... }
void setTime(int year, int month, int day, int hour, int minute, int second, int milliSecond) { ... }

Wie liest man in Java eine Textdatei ein? Wie schreibt man in eine Textdatei?

Zum Lesen und Schreiben von Textdateien kann man auf das Paket java.io der Java-Klassenbibliothek zurückgreifen. Alternativ dazu kann man die Klassen In und Out aus “Sprechen Sie Java?” verwenden.

Beispiel für die Verwendung des Pakets java.io der Java-Klassenbibliothek:

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;

public class FileIODemo {
    
    public static void main(String[] args) throws IOException {
        File f = new File("MyFile.txt");
        BufferedWriter bw = new BufferedWriter(new FileWriter(f));
        for (int i = 0; i < 10; i++) {
            bw.write("Hello World!\n");
        }
        bw.close();
        
        BufferedReader br = new BufferedReader(new FileReader(f));
        while(br.ready()) {
            System.out.println(br.readLine());
        }
        br.close();
    }
}

Falls die Datei MyFile.txt noch nicht vorhanden ist, wird sie in Zeile 12 angelegt und geöffnet.
Falls die Datei MyFile.txt bereits vorhanden ist, wird sie zum Überschreiben geöffnet.
In Zeile 14 wird mit write() zeilenweise in die Datei geschrieben.
Zum Beenden des Schreibvorgangs wird in Zeile 16 die Datei geschlossen.

In Zeile 18 wird die Datei MyFile.txt zum Lesen geöffnet.
In Zeile 19 wird die Schleifenbedingung formuliert. Die Schleife wird ausgeführt, bis alle Zeichen gelesen sind.
Mit der Methode readLine() wird die Datei zeilenweise gelesen.
In Zeile 22 wird die Datei geschlossen.

Beispiel für die Verwendung der Klassen In und Out:

    Out.open("MyFile.txt");
    for (int i = 0; i < 10; i++) {
        Out.println("Hello World!");
    }
    Out.close();

    In.open("MyFile.txt");
    String s = In.readFile();
    In.close();

    System.out.println(s);

Falls die Datei MyFile.txt noch nicht vorhanden ist, wird sie in Zeile 1 mit Out.open() angelegt und geöffnet.
Falls die Datei MyFile.txt bereits vorhanden ist, wird sie mit Out.open() zum Überschreiben geöffnet.
In Zeile 3 wird mit Out.println() zeilenweise in die Datei geschrieben.
Zum Beenden des Schreibvorgangs wird in Zeile 5 die Datei geschlossen.

In Zeile 7 wird die Datei MyFile.txt zum Lesen geöffnet.
Mit der Methode In.readFile() wird die gesamte Datei in den String s eingelesen.
In Zeile 9 wird die Datei geschlossen.

Was versteht man unter dem Begriff Vererbung?

Objekte in der realen Welt haben gemeinsame Eigenschaften und Eigenschaften, die nur für spezielle Objekte gelten.

Im Foliensatz Grundlagen der Programmierung, Kapitel 11 ab Seite 15 werden als Beispiel für Objekte der realen Welt Verkaufs-Artikel in einem Geschäft herangezogen. Alle Artikel des Geschäfts haben einen Preis und eine Artikelnummer. Eine Produktgruppe, die das Geschäft anbietet, sind Bücher. Im Unterschied zu anderen Produkten haben Bücher Eigenschaften wie Seitenanzahl und Autor.

Mittels Vererbung können die allgemeinen Eigenschaften wie Preis oder Artikelnummer in der Basisklasse (Produkt) modelliert werden. Die speziellen Eigenschaften für Bücher wie Autor oder Seitenanzahl werden in einer Unterklasse Buch, die von der Basisklasse Produkt abgeleitet wird, hinzugefügt.

Was versteht man unter Datenkapselung?

Bei der Datenkapselung wird ein direkter Zugriff auf die Daten einer Datenstruktur unterbunden. Der Zugriff ist nur über definierte Schnittstellen möglich. Dadurch können Fehler beim Datenzugriff vermieden werden.

Beispiel:
In einer Klasse Konto gibt es eine private Instanzvariable kontostand. Ein direkter Zugriff auf private Variablen aus einer anderen Klasse ist nicht möglich. Könnte auf den Kontostand direkt zugegriffen werden, wären beliebige Manipulationen am Kontostand möglich. Erfolgt der Zugriff ausschließlich über Schnittstellenmethoden, kann überprüft werden, ob beispielsweise beim Abbuchen der Überziehungsrahmen überschritten wird.

Beispiel in Java:

public class Konto {
    private double kontostand;
    private double kredit = 2000.0;

    public boolean einzahlen(double betrag) {
        if (betrag > 0.0) {
            kontostand += betrag;
            return true;
        } else {
            return false;
        }
    }

    public boolean abbuchen(double betrag) {
        if (betrag > 0.0 && kontostand - betrag >= -kredit) {
            kontostand -= betrag;
            return true;
        } else {
            return false;
        }
    }

    public double abfragen() {
        return kontostand;
    }
}

Neben der Fehlervermeidung können Datenkapselungen auch verwendet werden, um Implementierungsdetails zu verbergen und zu abstrahieren.
Im obigen Beispiel könnte die Methode einzahlen() so modifiziert werden, dass der Kontostand in einer Datei oder in einer Datenbank gespeichert wird. Veränderungen solcher Details würden keine Änderung der Signatur der Schnittstellenmethode bewirken.

Wieso ist die Main-Methode statisch?

Statische Methoden einer Klasse können aufgerufen werden, ohne dass ein Objekt der Klasse erzeugt werden muss. Die main-Methode ist per Definition der Einstiegspunkt ins Programm. Das heißt: Die virtuelle Maschine beginnt an dieser Stelle mit der Ausführung des Programms.

Wäre die main-Methode nicht statisch, müsste zuerst ein Objekt der Klasse, in der sich die main-Methode befindet, erzeugt werden. Beim Erzeugen des Objekts würde erst ein Konstruktor und dann die main-Methode aufgerufen werden. Damit wäre nicht die main-Methode der Einstiegspunkt ins Programm, sondern der Konstruktor.

Daraus folgt, dass die main-Methode statisch sein muss, um den Einstiegspunkt in das Programm beschreiben zu können.

Was ist der Unterschied zwischen implizitem Casting und explizitem Casting?

Beim expliziten Casting muss im Code ein Ausdruck für die Typumwandlung angegeben werden: (int), (float), …

Beim impliziten Casting ist das nicht der Fall. Die Typumwandlung wird vom Compiler ausgeführt, ohne dass im Code ein Ausdruck für die Typumwandlung angegeben wird.

Bei primitiven Datentypen erfolgt ein impliziter Cast, wenn einem größeren Datentyp ein kleinerer zugewiesen wird.
Es gilt: byte < short < int < long < float < double.

Arithmetische Operationen werden mindestens mit dem Typ int durchgeführt. Berechnungen mit byte oder short haben also den Typ int als Ergebnis.

Beispiel:

    int i = 123;
    long l = i;        // implizit
    int i2 = (int) l;  // explizit

    short s1 = 2, s2 = 3, s3;
    s3 = (short) (s1 + s2); // expliziter Cast notwendig, weil (s1 + s2) vom Typ int ist

Wie unterscheiden sich Klassen von Arrays?

Arrays sind Objekte. Arrays werden also mit new erzeugt und alle Methoden der Klasse Object können auf ein Array angewandt werden.

Arrays unterscheiden sich von anderen Objekten dadurch, dass sie eine beliebige Anzahl von Instanzvariablen haben können. Diese Variablen, die alle vom gleichen Typ sind, haben keine Namen, sondern werden über einen Index angesprochen. Zusätzlich besitzt das Array eine Konstante length, über die die Anzahl der Variablen abgefragt werden kann.

Ab wann ist es sinnvoll, Methoden in einer eigenen Klasse zusammenzufassen?

Es gibt Metriken, die eine Ober- und Untergrenze für die Anzahl der Methoden in einer Klasse angeben. Es ist allerdings fraglich, ob diese Metriken immer sinnvoll angewendet werden können. Auf alle Fälle ist es wichtig, dass Methoden, die inhaltlich zusammengehören oder gemeinsame Zustände haben, in einer Klasse zusammengefasst sind.

Wann sind static-Klassen (Klassen von denen kein Objekt erzeugt wird) sinnvoll?

Der Einsatz von static-Klassen ist sinnvoll für Sammlungen von Methoden, die keinen Zustand haben. Die Methoden bekommen Daten übergeben, verarbeiten diese und geben das Resultat zurück. Es besteht eine Ähnlichkeit zu Funktionen in der Mathematik.

In der Klassenbibliothek von Java ist die Klasse java.lang.Math ein Musterbeispiel für eine statische Klasse. Diese Klasse implementiert verschiedene mathematische Funktionen (sin(), cos(), log(), …), die keine gemeinsamen Zustände haben.

Die im folgenden Beispiel vorgestellte Klasse ArrayUtils zeigt die Umsetzung einer statischen Klasse mit Hilfsmethoden für Arrays. Auch diese Methoden haben keine gemeinsamen Zustände. Die statischen Methoden der Klasse ArrayUtils bekommen ein Array übergeben, verarbeiten die Werte des Arrays und geben ein Ergebnis zurück.

public class ArrayUtils {

    private ArrayUtils() {
    }

    public static double sum(double[] a) {
        double result = 0.0;
        for (double ai : a) {
            result += ai;
        }
        return result;
    }

    public static double average(double[] a) {
        return sum(a) / a.length;
    }
}

// Verwendung:
double[] a = {1.2, 3.4, 5.6};
double avg = ArrayUtils.average(a);

Um zu erzwingen, dass eine Klasse nur in einem statischen Kontext verwendet wird, kann ein privater Konstruktor implementiert (Zeile 3) werden. Damit wird verhindert, dass Instanzen der Klasse mittels new erzeugt werden können.

Darf eine Klasse mehrere Interfaces implementieren?

Ja. Eine Klasse kann mehrere Interfaces implementieren.

Interfaces werden verwendet, um Software zu schreiben, die unabhängig von einer konkreten Implementierung ist. Im Interface werden lediglich Schnittstellen vereinbart, die dann in der konkreten Implementierung umgesetzt werden müssen.

Das Implementieren mehrerer Interfaces kann sinnvoll sein, wenn eine Klasse A verschiedene Verhalten besitzen soll. Sollen die Objekte einer Klasse sortierbar und serialisierbar sein, dann wird diese Klasse die Interfaces Comparable und Serializable implementieren. Bei der Verwendung der Klasse A hat man die Zusicherung, dass alle Methoden der beiden Interfaces implementiert sind.

Darf eine Klasse von mehreren Klassen erben?

Nein. Im Unterschied zu anderen Sprachen wie C++ ist die Mehrfachvererbung in Java nicht möglich.

In Java kann eine Klasse nur von einer Klasse mit extends abgeleitet werden.

Um die Implementierung weiterer Schnittstellen sicher zu stellen, kann eine Klasse mehrere Interfaces implementieren. Mehr dazu findest du im Artikel “Darf eine Klasse mehrere Interfaces implementieren?” in unserer FAQ.

Was sind Enumerationen in Java? Was ist der Unterschied zu einer Klasse?

Enumerationen sind spezielle Klassen. Sie dienen zum Speichern von konstanten Objekten. Die konstanten Objekte sind von java.lang.Enum abgeleitet und besitzen unter anderem folgende Methoden:

  • public final int ordinal() – gibt die Ordinalzahl (eine laufende Nummer, beginnend bei 0) des Enum-Objekts zurück
  • public final String name() – gibt den Namen des Enum-Objekts zurück
  • public final String toString() – gibt – genauso wie die Methode name() – den Namen des Enum-Objekts zurück
  • public final int compareTo(E other) – vergleicht das Enum-Objekt mit einem anderen Enum-Objekt vom Typ E anhand der Ordinalzahl.

Zusätzlich werden vom Compiler für eine Enumeration vom Typ E folgende Methoden generiert:

  • public static final E[] values() – gibt ein Array mit allen Enum-Objekten zurück
  • public static E valueOf(String s) – gibt das Enum-Objekt mit dem Namen s zurück

In einer Enumeration können eigene Instanzvariablen und Methoden definiert werden.

Eine Enumeration ist ein Aufzählungstyp. Das heißt, ihre Elemente sind geordnet, können also mit der Methode compareTo() verglichen werden. Das Vergleichskriterium ist die Ordinalzahl des Enum-Objekts. Die Ordinalzahl eines Enum-Objekts entspricht – wenn nicht anders definiert – der Position, an der der Name des Objekts in der enum angeführt ist. Im untenstehenden Beispiel hat ROT die Ordinalzahl 0, GRÜN die Ordinalzahl 1 und BLAU die Ordinalzahl 2.

Einfaches Beispiel für eine Enumeration:

enum Farbe {
    ROT, GRÜN, BLAU
}

// Verwendung:
        
Farbe f = Farbe.GRÜN;
System.out.println(f.ordinal());  // Ausgabe: 1
System.out.println(f.name());     // Ausgabe: GRÜN

Komplexeres Beispiel mit einer Enumeration:

public class EnumTest {
    enum Wochentag {
        MONTAG("Mo"), DIENSTAG("Di"), MITTWOCH("Mi"), DONNERSTAG("Do"), FREITAG("Fr"), SAMSTAG("Sa"), SONNTAG("So");

        private final String shortName;

        Wochentag(String shortName) {
            this.shortName = shortName;
        }
        
        String getShortName() {
            return shortName;
        }

        boolean isWerktag() {
            return Wochentag.SONNTAG.compareTo(this) != 0;
        }
    }

    public static void main(String[] args) {
        System.out.println(Wochentag.MITTWOCH.isWerktag());
        System.out.println(Wochentag.SONNTAG.isWerktag());
        for (Wochentag w : Wochentag.values()) {
            System.out.printf("%d %-5b %-10s %s\n", w.ordinal(), w.isWerktag(), w.name(), w.getShortName());
        }
    }
}

// Ausgabe:
//
// 0 true  MONTAG     Mo
// 1 true  DIENSTAG   Di
// 2 true  MITTWOCH   Mi
// 3 true  DONNERSTAG Do
// 4 true  FREITAG    Fr
// 5 true  SAMSTAG    Sa
// 6 false SONNTAG    So

Die Enum-Objekte von Wochentag haben ein zusätzliches Attribut shortName, das die Kurzbezeichnung des Wochentags speichert. Beim Initialisieren der Enumeration wird für jedes Objekt der Aufzählung der Konstruktor in Zeile 7 mit dem Kurznamen als Argument aufgerufen.

Die Methode isWerktag() vergleicht das Objekt, in dem diese Methode aufgerufen wird, mit dem Objekt SONNTAG. Liefert der Vergleich einen Wert ungleich 0, handelt es sich um einen Werktag. Ansonsten handelt es sich um einen Sonntag.

Wie verwende ich die Methode ‘String.toLowerCase()’?

Die Methode toLowerCase() aus der Klasse String wandelt alle Großbuchstaben in Kleinbuchstaben um und gibt das Resultat als neuen String zurück.

Wichtig: Der Aufruf s.toLowerCase() verändert den String s nicht. Für die Weiterverarbeitung ist es notwendig, den zurückgegebenen String zuzuweisen:

String s = "hELlO wOrLd!";
String s2 = s.toLowerCase();
System.out.println(s);  // Ausgabe: hELlO wOrLd!
System.out.println(s2); // Ausgabe: hello world!

Könnte ich statt einer Methode den Code einfach jedes Mal hinschreiben, um es für mich verständlicher zu machen?

Das kann man machen. Dadurch wird der Code aber unübersichtlich, lang, schlecht lesbar und schlecht wartbar.

Wenn man den Code sinnvoll mit Methoden strukturiert, werden Code-Verdoppelungen vermieden. Änderungen oder Fehlerbehebungen sind nur an einer Stelle durchzuführen.

Wie muss eine als Parameter übergebene Variable definiert sein, dass Veränderungen an diesem Parameter innerhalb der Methode, nach dem Methodenaufruf erhalten bleiben?

Bei primitiven Datentypen wird der Wert einer Variable a einer Methode als Kopie übergeben. Innerhalb der Methode wird mit dieser Kopie gearbeitet. Der Wert der Kopie von a kann in der Methode verändert werden, ohne dass sich der Wert von a ändert. Möchte man auf den veränderten Wert der Kopie von a außerhalb der Methode zugreifen, muss dieser Wert mit return zurückgegeben werden.

Bei Objekten wie beispielsweise Arrays wird einer Methode eine Kopie der Referenz auf das Objekt übergeben. Es gibt also nur ein Objekt, auf das mehrere Referenzen verweisen. Werden innerhalb der aufgerufenen Methode Veränderungen im Objekt vorgenommen, manifestieren sich diese Veränderungen auch außerhalb.

static void f1(int x) {
    x++;
}

static int f2(int x) {
    x++;
    return x;
}

static void f3(int a[]) {
    a[0]++;
}

public static void main(String[] args) {
    int x = 123;
    f1(x);
    System.out.println(x);    // Ausgabe: 123
    x = f2(x);
    System.out.println(x);    // Ausgabe: 124
    int a[] = { 123 };
    f3(a);
    System.out.println(a[0]); // Ausgabe: 124
}

In der Methode f1() wird auf der Kopie der Variable x gearbeitet. Der Wert der in Zeile 15 deklarierten Variable ändert sich nicht.

In der Methode f2() wird der veränderte Wert zurückgegeben und kann dadurch auch außerhalb der Methode verwendet werden.

In der Methode f3() wird die Referenz auf das Array a übergeben. Änderungen am Array sind auch außerhalb der Methode sichtbar.

Wie kann man eine Methode schreiben, die eine Fehlermeldung zurückgeben kann?

In Java gibt es dafür das Konzept der Exceptions (Ausnahmen).
Wenn in einer Methode ein Fehler auftritt, kann ein Fehler-Objekt erzeugt und mit throw zum Aufrufer ‚geworfen‘ werden.

Im Code-Beispiel gibt der Benutzer sein Alter auf der Konsole ein. In der Methode setAlter() wird überprüft, ob der übergebene Wert positiv ist. Ist das nicht der Fall, wird in Zeile 7 ein Fehler-Objekt erzeugt und mit throw dem Aufrufer p.setAlter() übermittelt. Die Zuweisung in Zeile 9 wird im Fehlerfall nicht mehr ausgeführt. Das Programm springt in Zeile 25. Der Fehler wird behandelt, indem eine Fehlermeldung ausgegeben wird.

public class Person {

    private int alter;

    public void setAlter(int alter) throws Exception {
        if (alter < 0) {
            throw new Exception("Alter darf nicht negativ sein: " + alter);
        }
        this.alter = alter;
    }

    public int getAlter() {
        return alter;
    }

    public static void main(String[] args) {
        Person p = new Person();
        while (true) {
            try {
                System.out.print("Alter eingeben > ");
                String s = In.readLine();
                int a = Integer.parseInt(s);
                p.setAlter(a);  // hier kann Fehler auftreten
                break;
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
        System.out.println("Du bist " + p.getAlter() + " Jahre alt.");
    }
}

Weitere Information in Mössenböck, Kap. 19.

Wie deklariere und verwende ich eine Methode mit Varargs? Wofür verwendet man das?

Die Deklaration einer Methode mit variabler Argumentliste (Varargs) erfolgt, indem man hinter die Beschreibung des Datentyps der variablen Argumentliste drei Punkte setzt. Es kann nur ein formaler Parameter einer Methode eine variable Argumentliste sein. Dieser Parameter muss an der letzten Stelle der formalen Parameter deklariert sein.

Der Aufruf der Methode erfolgt, indem man eine beliebige Anzahl von Argumenten des entsprechenden Datentyps übergibt.

In der Methode kann auf die Elemente der variablen Argumentliste wie bei einem Array mit .length und über den Index zugegriffen werden.

Im Codebeispiel wird in Zeile 10 eine variable Anzahl von int-Werten übergeben. Aus diesen Werten wird in der Methode sum() die Summe berechnet.

static int sum(int... x) {
    int sum = 0;
    for (int i = 0; i < x.length; i++) {
        sum += x[i];
    }
    return sum;
}

public static void main(String[] args) {
    System.out.println(sum(1, 2, 3));

    int a[] = { 9, 8, 7, 6 };
    System.out.println(sum(a));
}

Hinweis: Anstelle von n int-Werten kann auch ein int-Array übergeben werden (Zeile 13).

Anwendung:

Soll bei einer Methode die Anzahl der Parameter variabel gehalten werden, kann mit varargs gearbeitet werden.

Beispiele für solche Methoden: min(), max(), average(), drawPolygon(), …

Kann man für Methoden-Parameter einen Standardwert angeben (z.B. public int func(int a=0))?

Das geht in Java mit der von dir beschriebenen Syntax nicht. Man kann allerdings die Methode func() überschreiben, um dieses Verhalten nachzubilden.

Im Codebeispiel wird beim Aufruf der parameterlosen Methode func() die Methode func(0) aufgerufen. func() gibt also den Wert 0 zurück.

static int func(int a) {
    return a * a;
}

static int func() {
    return func(0);
}

public static void main(String[] args) {
    func(5);  // gibt 25 zurück
    func();   // gibt 0 zurück
}

Kann man konkrete Methoden im Interface deklarieren?

In einem Interface können Methoden deklariert aber nicht implementiert werden.

Die in einem Interface deklarierten Methoden müssen von allen abgeleiteten Klassen, die nicht abstrakt sind, implementiert werden. Neben den Signaturen für die zu implementierenden Methoden kann ein Interface Konstanten und Enumerationen haben.

Mehr dazu findest du in den Artikeln “Wann ist ein Interface sinnvoll? Wozu braucht man Interfaces?” und “Was ist der Unterschied zwischen einer abstrakten Klasse und einem Interface?” in unserer FAQ.

Dürfen Klassen innerhalb einer Methode implementiert werden?

Ja, das geht.

Verwendung findet das beispielsweise bei der Implementierung von abstrakten Klassen oder Interfaces in Methoden.

Im folgenden Beispiel wird in der Methode foo() die abstrakte Klasse Thread implementiert.

static void foo() {
    class A extends Thread {
        @Override
        public void run() {
            //
        }
    }
    Thread o = new A();
    o.start();
}

public static void main(String[] args) {
    foo();
}

Rein syntaktisch ist es auch möglich, eigene Klassen innerhalb einer Methode zu implementieren. Der Einsatz solcher Klassen ist freilich nur eingeschränkt möglich, weil auf die Methoden und Attribute außerhalb der Methode, in der die Klasse implementiert ist, nicht zugegriffen werden kann. Eine mögliche Anwendung wäre die Umsetzung einer mathematischen Funktion, die nur innerhalb einer Methode sichtbar ist.

Im nachfolgenden Codebeispiel wird die sinc-Funktion in der Klasse MyInnerClass der Methode printTable() implementiert.

static void printTable() {
    class MyInnerClass {
        double sinc(double x) {
            if (x != 0) {
                return Math.sin(x) / x;
            } else {
                return 1;
            }
        }
    }

    MyInnerClass o = new MyInnerClass();
    for (double x = 0; x < 2.0; x += 0.1) {
        double y = o.sinc(x);
        System.out.println(x + " " + y);
    }
}

public static void main(String[] args) {
    printTable();
}

Kann ich den Wert eines Feldes (Array) auch außerhalb der main Methode ändern, wenn das Feld (Array) in der main Methode erstellt wurde?

Ja, das geht, wenn du das Array als Parameter übergibst. Das funktioniert, weil Array ein Referenzdatentyp ist.

Im Codebeispiel wird in der main-Methode ein Array a angelegt. Eine Referenz auf das Array wird der Methode foo() übergeben. Die Arrayelemente von a werden in der Methode foo() um Eins inkrementiert. Anhand der Testausgabe in der main-Methode sieht man, dass sich der Inhalt des Arrays geändert hat.

public static void main(String[] args) {
    int a[] = { 1, 2, 3, 4 };
    foo(a);
    System.out.println(Arrays.toString(a)); // Ausgabe: [2, 3, 4, 5]
}

static void foo(int a[]) {
    for (int i = 0; i < a.length; i++) {
        a[i]++;
    }
}

Was ist eine generische Methode? (also eine Methode, in der lokal ein generischer Datentyp existiert)

Eine generische Methode ist eine Methode, bei der mindestens ein Parameter generisch ist. Generisch bedeutet in diesem Zusammenhang, dass sich der Typ des Parameters aus dem übergebenen Typ beim Methodenaufruf ergibt.

Zur Syntax: Die Platzhalter für die verwendeten Typen sind in im Kopf der Methode vor der Deklaration des Rückgabeparameters anzugeben.

Im Codebeispiel wird eine Methode implementiert, die die Elemente eines Arrays in eine Liste kopiert. Die Liste und das Array sind mit dem generischen Typ T gestempelt. Das heißt: Wird beim Methodenaufruf eine Liste vom Typ Integer übergeben (List<Integer>) nimmt T den Typ Integer an. Analog List<Double>, List<Person>, … .

public class GenericTest {

	static <T> void copyArrayToList(List<T> list, T[] a) {
		for (T elem : a) {
			list.add(elem);
		}
	}
	
	public static void main(String[] args) {
		Integer a[] = { 1, 2, 3, 4 };
		List<Integer> list = new ArrayList<>();
		
		copyArrayToList(list, a);
		
		System.out.println(list);
	}
}

Wie wähle ich den richtigen Namen für eine Methode?

Eine Methode soll eine bestimmte Funktionalität kapseln. Ist der Name der Methode gut gewählt, lässt er Rückschlüsse auf diese Funktionalität zu. Beispiele: getLength(), setLength(), drawLine(), insert(), delete(), …

In Java ist es üblich, dass Methodennamen mit einem Kleinbuchstaben beginnen. Zusammengesetzte Namen werden im sogenannten Camel-Case geschrieben.

Warum sind Strings in Java nicht veränderbar?

Für die Unveränderlichkeit von Strings sprechen verschiedene Faktoren wie Performanz, Sicherheit und Caching.

Eine Begründung dieser Design-Entscheidung findest du hier: Why String is Immutable in Java?

Wie formatiere ich einen String immer auf die gleiche Länge, unabhängig von der Anzahl der chars?

Bei der Formatierung von Strings auf einheitliche Längen ist zu unterscheiden, ob der jeweilige String zu lang oder zu kurz ist. Im ersten Fall muss der String abgeschnitten werden. Im zweiten Fall ist er mit Leerzeichen aufzufüllen. Daraus ergibt sich entweder eine linksbündige oder eine rechtsbündige Darstellung.

Eine einfache Möglichkeit einen String auf eine konstante Länge zu bringen, bietet die Methode format() der Klasse String. Diese Methode hat als erstes Argument einen Formatstring, die weiteren Argumente sind durch die Leerstellen im Formatstring gegeben.

String s1 = "Hello World!";
String s2 = "Das ist ein langer Text";
String s3 = String.format("|%20.20s|\n", s1);
String s4 = String.format("|%20.20s|\n", s2);
String s5 = String.format("|%-20.20s|\n", s1);
String s6 = String.format("|%-20.20s|\n", s2);
System.out.println(s3 + s4 + s5 + s6);

Ausgabe:

|        Hello World!|
|Das ist ein langer T|
|Hello World!        |
|Das ist ein langer T|

Bedeutung der Zeichen im Formatstring %20.20s:

  • % – Kennzeichnet den Beginn einer Leerstelle
  • 20 – Beschreibt die Mindestlänge des Strings. Ist der als Argument übergebene String, der diese Leerstelle sättigt, kürzer als die angegebene Mindestlänge, wird links mit Leerzeichen ausgefüllt. Der String wird also rechtsbündig ausgegeben.
  • .20 – Beschreibt die maximale Länge des Strings. Ist der als Argument übergebene String, der diese Leerstelle sättigt, länger als die angegebene maximale Länge, wird er abgeschnitten.
  • s – Legt fest, dass die Leerstelle mit einem String zu sättigen ist.

In Zeile 5 steht vor der Angabe der Mindestlänge ein Minuszeichen. Dadurch wird ausgedrückt, dass der als Argument übergebene String s1 linksbündig ausgegeben wird.

Mehr zur Arbeit mit Formatstrings findest du im Artikel “Wie gebe ich eine bestimmte Formatierung aus?” in unserer FAQ.

Was ist der Unterschied zwischen einem ‘char’ und einem String?

Ein char repräsentiert ein einzelnes Zeichen im Unicode. Der Typ char ist ein primitiver Datentyp. Er ist zuweisungskompatibel zu den ganzzahligen Datentypen. Mit char kann also auch gerechnet werden, z.B. bei Verschlüsselungen.

Beispiel: Zum Unicode jedes Zeichens wird 1 addiert (Aus ‘a’ wird ‘b’, aus ‘b’ wird ‘c’, …).

char ca[] = "Hello world!".toCharArray();
for(char c : ca) {
    System.out.print((char)(c + 1));
}
// Ausgabe: Ifmmp!xpsme"

Die Objekte der Klasse String dienen zum Speichern einer Folge von Zeichen und bieten Methoden zum Arbeiten mit Strings an. Der Typ String speichert eine Folge von chars in einem char-Array. Dieses Array wird von verschiedenen Methoden wie charAt(), length(), equals(), toLowerCase(), … verwendet. Strings in Java sind unveränderlich, das heißt bei Stringmanipulationen wie der String-Konkatenation oder substring(), trim(), toUpperCase(), … werden neue String-Objekte erzeugt.

Was ist ein StringBuilder? Wie kann man einen String mit einem StringBuilder bearbeiten?

Die Klasse StringBuilder implementiert Methoden zur Stringmanipulation. Im Gegensatz zu Objekten der Klasse String sind StringBuilder-Objekte veränderlich. Aus diesem Grund wird der StringBuilder oft für eine effiziente Konkatenation von Strings eingesetzt. Dazu wird die Methode append() verwendet. Weitere Methoden des StringBuilders sind insert(), delete(), … . Mit der Methode toString() kann aus dem StringBuilder-Objekt wieder ein String erzeugt werden.

String s = "Hello";
StringBuilder sb = new StringBuilder(s);   // Hello
sb.append("World");                        // HelloWorld
sb.insert(5, ' ');                         // Hello World
sb.delete(0, 6);                           // World
s = sb.toString();

Kann ich in einer Schleife neue Variablen einführen?

Man kann in einer Schleife neue Variablen einführen.

Da auf diese Variablen nur innerhalb der Schleife zugegriffen werden kann, ist das immer dann sinnvoll, wenn die Variablen außerhalb der Schleife nicht benötigt werden.

Beispiel:

static void reverse(int a[]) {
    int left = 0, right = a.length - 1;
    while (left < right) {
        int temp = a[left];
        a[left] = a[right];
        a[right] = temp;
        left++;
        right--;
    }
}

Die Methode reverse() ordnet die Elemente eines Arrays in umgekehrter Reihenfolge an. Zum paarweisen Vertauschen der Elemente benötigt man eine Hilfsvariable (temp). Diese Variable wird nur innerhalb der Schleife benötigt und wird daher innerhalb der Schleife – also so lokal wie möglich – deklariert.

Wie kann eine Schleife mehrfach ausgeführt werden, nachdem das Abbruchkriterium erfüllt war?

Die Schleife, die mehrfach ausgeführt werden soll, wird im Schleifenrumpf einer zweiten Schleife implementiert.

Die äußere Schleife wird ausgeführt bis das Abbruchkriterium der äußeren Schleife (i >= 3) erfüllt ist. Die innere Schleife wird bei jeder Schleifeniteration der äußeren Schleife ausgeführt, bis das Abbruchkriterium der inneren Schleife (j >= 2) erfüllt ist.

for (int i = 0; i < 3; i++) {
    System.out.println("\ni = " + i);
    for (int j = 0; j < 2; j++) {
        System.out.print("j = " + j + ", ");
    }
}

// Ausgabe:
// i = 0
// j = 0, j = 1,
// i = 1
// j = 0, j = 1,
// i = 2
// j = 0, j = 1,

Wann fängt man bei einer Schleife mit 0 an und wann mit 1?

Das hängt in der Regel von der Aufgabenstellung ab.

Wenn der Schleifenzähler dazu dient, ein Array zu indizieren, wird man in der Regel bei 0 anfangen. Mehr dazu im Artikel “Warum fangen Arrays bei 0 an?” in unserer FAQ.

Wenn in einer Schleife eine Aufgabe n-mal auszuführen ist, dann ist es unter Umständen lesbarer, wenn der Schleifenzähler bei 1 beginnt.

Was ist eine Durchlaufschleife?

In Grundlagen der Programmierung – Kapitel 4 (Seite 7) wird die do-while Schleife als Durchlaufschleife bezeichnet, weil sie mindestens einmal durchlaufen werden muss.

Wie iteriert man über die einzelnen Zeichen eines Strings?

Im Folgenden werden zwei Möglichkeiten vorgestellt, wie man über die einzelnen Zeichen eines Strings iterieren kann:

String s = "Hello world!";

// 1
for (int i = 0; i < s.length(); i++) {
    char c = s.charAt(i);
    System.out.println(c);
}

// 2
char[] ca = s.toCharArray();
for (char c : ca) {
    System.out.println(c);
}

In Variante 1 wird mit der Methode charAt(i) des String-Objekts auf das Zeichen an der Position i zugegriffen.

In Variante 2 wird mit der Methode toCharArray() des String-Objekts der String in ein char-Array übertragen. Danach wird mit einer for-each-Schleife über die einzelnen Zeichen iteriert.

Wie bricht man eine Schleife ab?

Eine Schleife kann mit break abgebrochen werden. Der Schleifenkörper wird verlassen und der Programmlauf mit der Instruktion unterhalb der Schleife fortgesetzt:

for (int i = 0; i < 10; i++) {
    if (i == 5) {
        break;
    }
    System.out.print(i + " ");
}
System.out.println("fertig");

// Ausgabe:
// 0 1 2 3 4 fertig

Befindet sich die break-Anweisung bei doppelt verschachtelten Schleifen in der inneren Schleife, wird mit der break-Anweisung die innere Schleife beendet und mit der Ausführung der nächsten Iteration der äußeren Schleife weiter gemacht:

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 10; j++) {
        System.out.printf("i=%d,j=%d;  ", i, j);
        if (j == 2) {
            break;
        }
    }
}
// Ausgabe:
// i=0,j=0;  i=0,j=1;  i=0,j=2;  i=1,j=0;  i=1,j=1;  i=1,j=2; 

Wird return in einer Schleife aufgerufen, wird nicht nur die Schleife beendet, sondern die gesamte Methode:

static String prettyPrint(String s) {
    while (true) {
        if (s.length() >= 40) {
            return s;
        }
        s = "<" + s + ">";
    }
}

public static void main(String[] args) {
    System.out.println(prettyPrint("Hallo"));
    System.out.println(prettyPrint("Hallo Welt!"));
}

// Ausgabe:
// <<<<<<<<<<<<<<<<<<Hallo>>>>>>>>>>>>>>>>>>
// <<<<<<<<<<<<<<<Hallo Welt!>>>>>>>>>>>>>>>

Mit einer continue-Anweisung wird eine Schleifeniteration abgebrochen und mit der nächsten weiter gemacht:

for (int i = 0; i < 10; i++) {
    if (i % 3 == 0) {
        continue;
    }
    System.out.print(i + " ");
}

// Ausgabe:
// 1 2 4 5 7 8

Wird ein finally-Block in Java immer ausgeführt?

Ja, ein finally-Block wird in Java immer ausgeführt.

Der finally-Block kann in Java in Verbindung mit einem try-Block auftreten. Unabhängig davon, wie der Kontrollfluss in den try-catch-Blöcken ist, wird die Anweisung im finally-Block immer ausgeführt.

Beispiel:

public class FinallyTest {

    static void foo(int n) {
        System.out.printf("foo(%d)\n", n);
        try {
            if (n == 1) {
                return;
            }
            n = 1 / n;
        } catch (Exception e) {
            System.out.println("Im catch-Block.");
        } finally {
            System.out.println("Im finally-Block.");
        }
    }

    public static void main(String[] args) {
       foo(1);
       System.out.println("----");
       foo(0);
    }
}

// Ausgabe:
// --------
//
// foo(1)
// Im finally-Block.
// ----
// foo(0)
// Im catch-Block.
// Im finally-Block.

Wird die Methode foo() mit dem Argument 1 aufgerufen, wird trotz des return in Zeile 7 der finally-Block ausgeführt.

Wird die Methode foo() mit dem Argument 0 aufgerufen, wird in Zeile 9 eine ArithmeticException geworfen. Daraufhin werden die Anweisungen im catch-Block ausgeführt. Im Anschluss daran wird der finally-Block abgearbeitet.

Welche Schleifen gibt es? Wofür werden sie verwendet?

Grundsätzlich unterscheidet man zwischen der while-Schleife, der do-while-Schleife, der for-Schleife und der for-each-Schleife. Die Wahl der Schleife ist abhängig von der Aufgabenstellung.

Für die Auswahl der Schleife könnte man folgende Heuristiken verwenden:

  • Wenn die Anzahl der Schleifendurchläufe bekannt ist, sollte man die for-Schleife als konventionelle und gut lesbare Variante in Erwägung ziehen. Oft wird die for-Schleife zum Iterieren über indizierte Datenstrukturen (Arrays) verwendet.
  • Ist die Anzahl der Schleifendurchläufe nicht bekannt, nimmt man die while-Schleife. Ein Beispiel dafür ist das zeilenweise Lesen einer Datei.
  • Will man sicher gehen, dass die Schleife mindestens einmal durchlaufen wird, nimmt man die do-while-Schleife. Ein Beispiel dafür ist die Abfrage von Benutzereingaben mit Fehlerbehandlung oder iterativen Algorithmen, die solange wiederholt werden, bis das Abbruchkriterium erreicht ist.
  • Die for-each-Schleife ist eine elegante Möglichkeit zum Iterieren über Arrays und Collections (Set, List, Map, …)

Mehr dazu: Grundlagen der Programmierung; Kapitel 4.

Was bedeutet inkrementieren?

Inkrementieren bezeichnet das schrittweise Erhöhen des Wertes einer Variablen. Das Inkrement beschreibt, um wieviel sich der Wert in jedem Schritt erhöht.

Analog beschreibt das Dekrementieren die schrittweise Verminderung des Wertes einer Variablen und das Dekrement, um wieviel sich der Wert vermindert.

Beim Inkrementieren unterscheidet man zwischen dem Pre-Inkrement (++x) und dem Post-Inkrement (x++).
Mehr dazu findest du im Artikel “Welcher Unterschied ist zwischen dem Befehl „x++“ und „++x“ genau?” in unserer FAQ.

Beispiel: In jeder Iteration wird der Wert der Variable i um 10 inkrementiert.
Der Ausdruck i += 10 ist eine Kurzschreibweise für i = i + 10.

for (int i = 0; i <= 100; i += 10) {
    System.out.print(i + " ");
}
// Ausgabe: 0 10 20 30 40 50 60 70 80 90 100

Wie kann man einen String-Array ausgeben?

In Java gibt es für die Ausgabe von Arrays mehrere Möglichkeiten. Drei davon werden im Folgenden beschrieben:

String[] a = { "aaa", "bbb", "ccc", "ddd" };

// 1
for (int i = 0; i < a.length; i++) {
    System.out.println(a[i]);
}

// 2
for (String s : a) {
    System.out.println(s);
}

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

In 1 wird mit das Array mit einer for-Schleife indiziert durchlaufen.
In 2 kommt die verkürzte for-Schleife (for-each-Schleife) zum Einsatz.
In 3 wird auf die Bibliotheksfunktion java.util.Arrays.toString() zurückgegriffen.

Wieso muss man Arrays klonen und kann sie nicht einfach wie andere Daten einer zweiten Variable zuweisen, wenn man zwei Arrays mit dem gleichen Inhalt möchte?

In Java wird in der Variable für das Array eine Referenz in Form einer Speicheradresse der Array-Daten abgelegt. Weist man das Array einer zweiten Variable zu, enthalten beide Variablen dieselbe Speicheradresse und referenzieren daher dieselben Array-Daten.

Mehr Information findest du in den Artikeln Wie funktioniert die Array-Zuweisung und das Kopieren von Arrays? und Was ist der Unterschied zwischen einem Referenzdatentyp und einem Wertdatentyp? in unserer FAQ.

Kann ein Array auch mehr als 3 Dimensionen haben?

Ein Array kann mehr als 3 Dimensionen haben.

Beispiel:
In einem Raum sind Temperatursensoren in einem dreidimensionalen rechtwinkligen Gitter angebracht.
Ein Punkt dieser Gitterstruktur wird durch drei Indizes x, y, z beschrieben. Eine weitere Dimension erhält man, wenn man für verschiedene Zeitpunkte ein Array von dreidimensionalen Arrays anlegt. Für jede der drei Raumkoordinaten ergibt sich die Möglichkeit, Werte zu verschiedenen Zeitpunkten zu speichern.

In Java kann man diese Aufgabenstellung wie folgt formulieren:

final int N = 10;
int[][][][] a = new int[60 * 24][N][N][N];
for (int time = 0; time < a.length; time++) {
    int data[][][] = a[time];
    for (int x = 0; x < N; x++) {
        for (int y = 0; y < N; y++) {
            for (int z = 0; z < N; z++) {
                data[x][y][z] = getSensorData(x, y, z);
            }
        }
    }
}

Im Array a ist die erste Dimension die Zeit. Im Minutenabstand können im Array für einen Tag Werte für die durch x, y, z definierten Raumpunkte abgelegt werden. Die Methode getSensorData(x, y, z) liefert den Messwert an der Position (x, y, z).

Wie erstellt man ein Array aus einem Baum?

Es gibt verschiedene Möglichkeiten, um aus einem Baum ein Array zu erzeugen. Zum Traversieren des Baums verwendet man die Tiefensuche oder die Breitensuche.

  • Tiefensuche:
    Die Tiefensuche bei Binärbäumen ist rekursiv definiert. Dabei wird ausgehend vom aktuellen Knoten der Baum in einen linken und rechten Teilbaum zerlegt. Je nachdem, ob der aktuelle Knoten vor, nach oder zwischen den Teilbäumen abgearbeitet wird, unterscheidet man zwischen pre-order, post-order oder in-order.
    • Pre-order: Zuerst wird der aktuelle Knoten im Array abgelegt. Dann wird der linke und zuletzt der rechte Teilbaum gemäß der pre-order Abfolge rekursiv abgearbeitet.
    • Post-order: Zuerst wird der linke, dann der rechte Teilbaum gemäß der post-order Abfolge rekursiv durchlaufen. Dann wird der aktuelle Knoten im Array abgelegt.
    • In-order: Zuerst erfolgt der rekursive Abstieg gemäß der in-order Abfolge im linken Teilbaum. Dann wird der aktuelle Knoten im Array abgelegt und zuletzt erfolgt der rekursive Abstieg gemäß der in-order Abfolge im rechten Teilbaum.
  • Breitensuche (level-order):
    Bei diesem Verfahren wird der Baum beginnen mit dem Wurzelknoten Ebene für Ebene durchlaufen.

Beispiel:
Beim Traversieren der Bäume gemäß der angegebenen Ordnung ergibt sich die Zeichenkette “HelloWorld”.

Pre-Order:           
              H
             / \
            /   \
           /     \
          e       r
         / \     / \
        /   \   /   \
       l     W l     d
      / \   /
     l   o o
  
         
Post-Order:        
              d
             / \
            /   \
           /     \
          W       l
         / \     / \
        /   \   /   \
       l     o o     r
      / \   /
     H   e l
  
     
In-Order: 
              o
             / \
            /   \
           /     \
          l       l
         / \     / \
        /   \   /   \
       e     W r     d
      / \   /
     H   l o

Mehr dazu: Wikipedia Binärbaum

Was ist der Heapspeicher?

Bei der Ausführung eines Programms werden verschiedene Speicherbereiche reserviert. Einer davon ist der Heapspeicher. Im Heapspeicher werden dynamisch zugewiesene Daten abgelegt. Dazu gehören in Java alle zur Laufzeit erzeugten Objekte.

Werden Ressourcen nicht mehr benötigt (nicht mehr referenziert), können sie durch den Garbage-Collector aus dem Heapspeicher gelöscht werden. Dadurch kann dieser Speicherbereich durch das Programm wieder verwendet werden.

Wie bestimme ich in Java die Größe eines Arrays?

int[] a = new int[7]; // explizit
int[] b = {1, 2, 3};  // implizit

Explizit kann man die Größe eines Arrays festlegen, indem man sie bei der Initialisierung mit new angibt (Zeile 1).
Implizit wird die Größe des Arrays über die Anzahl der bei der Initialisierung angegebenen Aufzählungselemente festgelegt (Zeile 2).

Über das Attribut length kann die Größe abgefragt werden.

Was ist der erste Index eines zweidimensionalen Arrays A[i][j]? Zeile oder Spalte?

Ob der erste Index eines zweidimensionalen Arrays die Zeile oder Spalte beschreibt, wird durch die Entwicklerin oder den Entwickler festgelegt.

In der Mathematik ist es freilich Konvention, für die Indizierung einer Matrix den ersten Index für die Zeilen zu verwenden. Diese Konvention gilt auch in der Informatik.

Betrachtet man die Initialisierung eines zweidimensionalen Arrays durch Aufzählung, erkennt man, dass es zweckmäßig ist den ersten Index für die Zeilen zu verwenden.

Die Deklaration und Initialisierung des Arrays a:

int a[][] = { { 1, 2 },
              { 3, 4 }, 
              { 5, 6 } };

ist äquivalent zu:

int a[][] = new int [3][2];
a[0][0] = 1;  a[0][1] = 2;
a[1][0] = 3;  a[1][1] = 4;
a[2][0] = 5;  a[2][1] = 6;

Was ist effizienter beim Iterieren durch ein Array. Bei jedem Schleifendurchlauf das Attribut ‘length’ zu verwenden, oder die Länge des Arrays in einer Variable zwischenzuspeichern?

Diese Frage lässt sich nicht eindeutig beantworten. Verschiedene Compiler bzw. virtuelle Maschinen haben unterschiedliche Strategien zur Optimierung des Programms.
D.h.: Beide Varianten können gleich schnell ablaufen. Im Zweifelsfall ist die Variante mit der zusätzlichen Variable schneller.

Man sollte allerdings berücksichtigen, dass es konventionell ist, keine zusätzliche Variable zu verwenden. Das heißt: Der Code ist ohne zusätzliche Variable besser lesbar und wartbar.

Gibt es bei Arrays eine einfache Möglichkeit, alle Elemente mit einem Anfangswert zu initialisieren?

Es gibt die Möglichkeit alle Elemente eines Arrays mit einem Anfangswert zu belegen, indem man die Werte bei der Initialisierung aufzählend angibt. Im folgenden Beispiel wird ein Array der Länge 4 mit Strings belegt.

String s[] = {"Java", "C++", "Fortran", "Python"};

Außerdem bietet die Klasse Arrays aus dem Paket java.util eine einfache Möglichkeit alle Elemente eines Arrays mit dem gleichen Wert zu belegen:

int a[] = new int[1000];
Arrays.fill(a, 123);  // Alle Arrayelemente haben den Wert 123

Muss ich alle Arrays am Anfang des Programms festlegen?

Nein, das musst du nicht und das ist auch nicht immer ratsam.

Wird beispielsweise ein Array nur lokal in einer Methode benötigt, dann soll und muss man das Array nicht am Anfang des Programms als Instanzvariable deklarieren. In diesem Fall wird das Array innerhalb der Methode deklariert.

Möchte man eine Methode schreiben, die ein neues Array zurückgibt, so muss das Array innerhalb der Methode deklariert werden.

Beispiel: Die Methode add() berechnet die Summe von zwei Vektoren und gibt das Ergebnis in einem neuen Array zurück.

public static double[] add(double v1[], double v2[]) {
    if (v1.length != v2.length) {
        throw new IllegalArgumentException();
    }
    double result[] = new double[v1.length];
    for (int i = 0; i < v1.length; i++) {
        result[i] = v1[i] + v2[i];
    }
    return result;
}

Wofür werden zweidimensionale Arrays in der Praxis verwendet?

Zweidimensionale Arrays können verwendet werden, um Daten zu verwalten, die Wertetabellen, Spielfelder (Tic Tac Toe, Schach, …), Kalender, Matrizen, Bilddaten, … beschreiben.

Wie funktionieren mehrdimensionale Arrays in Java?

Bei einem zweidimensionalen Array sind die Arrayelemente der ersten Dimension eindimensionale Arrays.
Bei einem dreidimensionalen Array sind die Arrayelemente der ersten Dimension zweidimensionale Arrays. Die Arrayelemente der zweiten Dimension sind eindimensionale Arrays …

Zweidimensionales Array

Das Array a in der Abbildung ist zweidimensional. a[0] ist eine Referenz auf ein eindimensionales Array mit dem Inhalt [ 4, 5, 6 ].

Im folgenden Code wird gezeigt, wie Arrays deklariert und initialisiert werden.

public class ArrayDemo {

    static void printArray(int a[][]) {
        for (int i = 0; i < a.length; i++) {
            int row[] = a[i];
            for (int j = 0; j < row.length; j++) {
                System.out.printf("%5d ", row[j]);
            }
            System.out.println();
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int a[][] = new int[3][4];
        printArray(a);
        
        int b[][] = new int[3][];
        b[0] = new int[2];
        b[1] = new int[3];
        b[2] = new int[4];
        printArray(b);
        
        int c[][] = { { 1, 2 }, { 3, 4 }, { 5, 6 } };
        printArray(c);
    }
}

// Ausgabe:
//    0     0     0     0 
//    0     0     0     0 
//    0     0     0     0 
//    
//    0     0 
//    0     0     0 
//    0     0     0     0 
//    
//    1     2 
//    3     4 
//    5     6 

In Zeile 15 wird ein int-Array mit 3 Zeilen und 4 Spalten angelegt.
In Zeile 18 wird das int-Array b mit 3 Zeilen angelegt. Die Arrays der zweiten Dimension sind noch nicht definiert.
In Zeile 19 – 21 werden die einzelnen Zeilen von b mit Arrays unterschiedlicher Länge belegt. Damit soll gezeigt werden, dass ein mehrdimensionales Array nicht rechteckig sein muss.
In Zeile 24 wird ein int-Array durch Aufzählen der Elemente deklariert und initialisiert.

Mehr dazu findest du im Artikel “Kann ein Array auch mehr als 3 Dimensionen haben?” in unserer FAQ.

Wie füge ich einen Knoten in einen Binärbaum ein?

Beim Einfügen eines Knotens in einen Binärbaum ist darauf zu achten, dass die Ordnung der im Baum gespeicherten Werte erhalten bleibt.

Das Einfügen erfolgt mit einem rekursiven Algorithmus wie folgt:

  1. Nimm den Wurzelknoten als aktuellen Knoten.
  2. Ist der einzufügende Wert kleiner/größer als der Wert des aktuellen Knotens, gehe weiter in den linken/rechten Unterbaum. Wenn der Unterbaum leer ist, dann füge den neuen Knoten hier ein.
    Wenn der Knoten nicht eingefügt wurde (weil der Unterbaum nicht leer ist), dann nimm den Wurzelknoten des rechten/linken Unterbaums als aktuellen Knoten. Weiter mit 2 (Rekursion).

Wie entferne ich einen Knoten aus einem Binärbaum?

Beim Entfernen eines Knotens aus einem Binärbaum ist darauf zu achten, dass die Ordnung der im Baum gespeicherten Werte erhalten bleibt.

Es ist zu unterscheiden, ob ein Blatt, ein innerer Knoten mit nur einem Kind-Knoten oder ein innerer Knoten mit zwei Kind-Knoten gelöscht wird.
Mehr dazu findest du hier und hier.

Wie vergleiche ich Arrays?

Da Arrays wie jedes Objekt die Methode equals() besitzen, ist es naheliegend, diese Methode für einen Vergleich zu verwenden.
Wichtig: Der Vergleich mit equals() liefert nur dann true zurück, wenn beide Referenzen auf dasselbe Array zeigen. Ein Vergleich des Inhalts findet nicht statt.

Mehr dazu findest du im Artikel “Was ist der Unterschied zwischen einem Referenzdatentyp und einem Wertdatentyp?” in unserer FAQ.

int a1[] = { 1, 2, 3 };
int a2[] = { 1, 2, 3 };
System.out.println(a1.equals(a2)); // Ausgabe: false

Im Folgenden werden zwei Methoden vorgestellt, die zwei Arrays auf Gleichheit des Inhalts überprüfen.

1. Überprüfung von zwei int-Arrays auf Gleichheit in einer selbst geschriebenen Methode:

static boolean arrayEquals(int a1[], int a2[]) {
    if (a1.length != a2.length) {
        return false;
    }
    for (int i = 0; i < a1.length; i++) {
        if (a1[i] != a2[i]) {
            return false;
        }
    }
    return true;
}

In der Methode arrayEquals() wird zuerst die Länge der beiden Arrays a1 und a2 verglichen. Haben beide Arrays dieselbe Länge, werden die Elemente von a1 und a2 paarweise verglichen.

2. Eine weitere Möglichkeit zwei Arrays auf Gleichheit der Inhalte zu prüfen, bietet die Methode java.util.Arrays.equals().

import java.util.Arrays;
...

int a1[] = { 1, 2, 3 };
int a2[] = { 1, 2, 3 };
System.out.println(Arrays.equals(a1, a2)); // Ausgabe: true

Wie werden Buchstaben (also Datentyp char) abgespeichert?

Zeichen werden in codierter Form als Zahl gespeichert. In Java ist der Datentyp char 2 Byte breit. Die Codierung der Zeichen erfolgt im Unicode.

Für die ersten 128 (0 … 127) Zeichen ist das Codierungsschema von Unicode und ASCII identisch.

Mit dem folgenden Code-Beispiel können die Zeichencodes für die wichtigsten Zeichen auf der Konsole ausgegeben werden:

for (int i = 32; i < 128; i++) {
    char c = (char) i;
    System.out.println(i + "\t" + c);
}

Mehr dazu findest du im Artikel “ASCII Zeichencode / Unicode?” in unserer FAQ.

Wofür brauche ich Konstantendeklarationen?

Konstanten werden verwendet, um in einem Programm unveränderliche Werte verfügbar zu machen.

Beispiele aus der Klassenbibliothek:
Color.BLUE, Math.PI

Eigenes Beispiel:

public class Person {
    final String name;
    String adresse;
    final Date geburtdatum;
    final String svNummer;

    Person(String name, Date geburtdatum, String svNummer) {
        this.name = name;
        this.geburtdatum = geburtdatum;
        this.svNummer = svNummer;
    }
}

Die Werte für Name, Geburtsdatum und SV-Nummer können und müssen genau einmal (im Konstruktor oder bei der Deklaration) zugewiesen werden. Weitere schreibende Zugriffe auf diese Konstanten werden vom Compiler als Fehler gemeldet.

Konstanten bieten die Zusicherung, dass mit unveränderlichen Werten gearbeitet wird. Diese Zusicherung verbessert die Verständlichkeit des Codes, erleichtert das Testen und verringert die Ausführungszeiten.

Warum heißt es String.length() aber bei einem Array nur .length ohne die Klammern?

Beim Entwurf der Sprache Java wurde die Design-Entscheidung getroffen, dass bei einem Array die Länge in der Konstante length gespeichert wird. Der Zugriff auf diese Konstante erfolgt mit .length.

Im Gegensatz dazu erfolgt beim String-Objekt die Abfrage über die Methode length().

Welche Benutzereingabe auf der Konsole ergibt “true”, welche “false”?

Ob eine Benutzereingabe auf der Konsole true oder false ergibt, hängt von der Verarbeitung der Eingabe ab.

  • Die in der Lehrveranstaltung verwendete Klasse In besitzt die Methode readBoolean(). Diese Methode liefert den Wert true, wenn auf der Konsole die Zeichenfolge „true“ eingegeben wird. Jede andere eingegebene Zeichenfolge liefert den Wert false.
    Mit der Methode done() kann überprüft werden, ob die Eingabe gültig war. Gültige Eingaben sind die Zeichenketten „true“ und „false“.
boolean b = In.readBoolean();
if (In.done()) {
    System.out.println(b);
} else {
    System.out.println("Ungültige Eingabe");
}
  • Die Klasse Scanner bietet mit der Methode nextBoolean() die Möglichkeit, einen Wahrheitswert von der Konsole einzulesen. Diese Methode ignoriert die Groß- / Kleinschreibung. Leerzeichen oder Tabulatoren am Anfang oder am Ende der Eingabe werden entfernt. Wenn die Zeichenfolge “true” auf die Konsole eingegeben wird, wird true zurückgegeben. Wenn die Zeichenfolge “false” auf die Konsole eingegeben wird, wird false zurückgegeben. Jede andere Zeichenfolge löst eine InputMismatchException aus.
Scanner sc = new Scanner(System.in);
boolean b = sc.nextBoolean();
System.out.println(b);
  • Die Methode parseBoolean() der Java-Klasse Boolean ermöglicht es, einen String in einen boolean umzuwandeln. Auch hier spielt hier die Groß- / Kleinschreibung keine Rolle. Wie in der oben beschriebenen Methode liefert die Zeichenfolge “true” true. Jede andere Zeichenfolge liefert den Wert false zurück. (Auch der leere String oder null …)
Scanner sc = new Scanner(System.in);
String s = sc.nextLine();
boolean b = Boolean.parseBoolean(s);
System.out.println(b);

Was bedeutet “null” z. B. Bei Stringvariablen? Können primitive Datentypen wie int oder double den Wert “null” haben?

Primitive Datentypen wie int oder double können nicht null sein, nur Objekte.

Strings sind in Java keine primitiven Datentypen, sondern Objekte. Objektvariablen können Objekte referenzieren. Objektvariablen, die kein Objekt referenzieren, haben den Wert null.

Benötigt man null-Werte bei int oder double kann man auf die Wrapper-Klassen Integer oder Double zurückgreifen.

Wie liest man einzeln Bits aus einer int Variable aus?

Um einzelne Bits aus einer int-Variable auszulesen, verwendet man den Shift-Operator sowie die bitweise &-Verknüpfung.

Die Beispielsimplementierung setzt die Methode isBitSet(n, pos) um. Diese Methode gibt true zurück, wenn bei der Zahl n das Bit an der Position pos gesetzt ist.

public static boolean isBitSet(int n, int pos) {
    return ((n >> pos) & 1) == 1;
}

Zuerst wird der Ausdruck (n >> pos) ausgewertet. Der Operator >> (Shift-Operator) verschiebt die Bits von n um pos Positionen nach rechts.
Das Bit an der gesuchten Position pos wird an die Position mit dem Index 0 geschoben.

Sieht man bei einer Zahl vom Wert des Bits am Index 0 ab, ist das Ergebnis einer bitweise &-Verknüpfung von n mit 1 (binär: 0 ... 001) eine Folge von Nullen.

Hat das Bit der um pos Positionen verschobenen Zahl n am Index 0 den Wert 1, liefert eine bitweise &-Verknüpfung mit 1 den Wert 1.
Hat das Bit der um pos Positionen verschobenen Zahl n am Index 0 den Wert 0, liefert eine bitweise &-Verknüpfung mit 1 den Wert 0.

Beispiel:

    0010 1101     (45)
    0000 0101     (45 >> 3)

    0000 0101
&   0000 0001     (1)
-------------
    0000 0001

Warum sind Integer 4 Byte und nicht 2?

Integer in Java sind 4 Byte (32 Bit) breit. Damit kann man Ganzzahlen im Bereich von -231 bis 231-1 darstellen.

Die Breite eines Integers in verschiedenen Programmiersprachen ist abhängig von der Rechnerarchitektur (Busbreite, CPU). Beispielsweise ist bei vielen C-Compilern für Mikro-Controller die Breite eines Integers 2 Byte. In der Entstehungszeit von Java waren 32-Bit Architekturen verbreitet. Üblicherweise wurden also die Breite von Integern mit 32 Bit festgelegt.

Wieso muss man bei einer switch-Anweisung in jedem case-Teil ein break; einfügen? Kann das nicht automatisch danach abbrechen?

Es gibt Probleme, bei deren Lösung man für mehrere Fälle die gleichen Anweisungen ausführen will. Mit einer switch-Anweisung, in der nicht jede case-Anweisung mit einem break abschließt, können solche Probleme elegant gelöst werden.

Beispiel:

static int tage(int monat) {
    int result;
    switch(monat) {
        case 1: case 3: case 5: case 7: case 8: case 10: case 12:
            result = 31;
            break;
        case 4: case 6: case 9: case 11:
            result = 30;
            break;
        case 2:
            result = 28; // TODO: Schaltjahr
            break;
        default:
            throw new IllegalArgumentException();
    }
    return result;
}

Die Methode tage() gibt für jeden Monat die Anzahl der Tage zurück. Die Monate 1, 3, 5, … sind gleich zu behandeln und werden daher mit einem gemeinsamen break abgeschlossen.

Wie wird aus einem Java-Code ein fertiges Programm?

Die Erstellung eines Programms nennt man Build-Prozess. Dieser umfasst mehrere Schritte:

In einem ersten Schritt wird der Source-Code kompiliert. Das heißt, der Java-Compiler übersetzt die .java-Dateien in .class-Dateien (Byte-Code).

Die .class-Dateien werden von der virtuellen Maschine gelesen. Der Interpreter der virtuellen Maschine übersetzt jede Anweisung des Byte-Codes in Instruktionen, die die CPU ausführen kann. Um Optimierungen vornehmen zu können, wird während der Ausführung der Instruktionen das Laufzeitverhalten der einzelnen Sequenzen evaluiert. Diese Evaluierung ist eine notwendige Voraussetzung, dass der Just In Time Compiler für besonders zeitkritische Sequenzen des Programms optimierten Maschinencode erzeugen kann.

Möchte man das Programm weitergeben, ist es empfehlenswert, die .class-Files und sonstige Ressourcen (Bilder, Audio, …) in einem .jar-File (JavaArchive) zu bündeln.

Weiters besteht die Möglichkeit, aus dem Programm eine installierbare Datei zu erzeugen. Unter Windows kann so eine Datei mit dem Programm NSIS erstellt werden.

Von der Java-Datei zum ausführbaren Programm

Was ist der Zweck eines “Default” in einem “Switch”?

Ein Switch-Statement besteht aus mehreren case-Marken (Label). Jeder case-Marke ist ein konstanter Wert zugeordnet. Bei Abarbeitung eines switch-Statements wird zuerst der Ausdruck, der dem Schlüsselwort switch folgt, ausgewertet und mit den Werten der case-Marken verglichen. Bei Gleichheit werden die Anweisungen hinter dem entsprechenden Label ausgeführt.

Wenn der Vergleich des Wertes, der dem switch-Statement folgt, mit allen Werten der Labels fehlschlagen, werden die Anweisungen, die dem default-Label folgen ausgeführt.

Ist kein default-Label angegeben, wird die Ausführung hinter dem switch-statement fortgeführt. Im default-Label wird oft eine Fehlerbehandlung implementiert.

Beispiel:

int wochentag = 42;
switch (wochentag) {
    case 0: 
        System.out.println("Montag");
        break;
    ...
    case 6:
        System.out.println("Sonntag");
        break;
    default:
        System.out.println("Ungültiger Wochentag");
}

Mehr dazu in Mössenböck, Sprechen Sie Java, Kapitel 3.3.

Wie kann ich eine Zahl in einen String umwandeln?

Es gibt verschiedene Möglichkeiten, eine Zahl in einen String umzuwandeln. Beispielsweise kann die Methode toString() der Klasse Integer verwendet werden (Zeile 3).

Alternativ können verschiedene Methoden der Klasse String zum Einsatz kommen. In Zeile 4 wird die Methode valueOf() verwendet. In Zeile 5 wird mit der Methode format() ein formatierter String erzeugt.

Eine einfache, wenngleich nicht besonders elegante Art aus einer Zahl einen String zu machen, wird in Zeile 6 vorgestellt. Bei der Konkatenation einer Zahl und einem String mit dem +-Operator ist das Ergebnis ein String.

int n = 123;

String s1 = Integer.toString(n);
String s2 = String.valueOf(n);        // s2 -> "123"
String s3 = String.format("%05d", n); // s3 -> "00123"
String s4 = n + "";

Wie mache ich aus einem String eine Integer-Variable?

Ein String wird mit der Methode parseInt() der Klasse Integer in den primitiven Datentyp int umgewandelt. Lässt sich der String nicht umwandeln wird eine NumberFormatException ausgelöst.

Beispiel:

String s = "23";
int i = Integer.parseInt(s);

Wie werden try-catch-Blöcke zur Fehlerbehandlung verwendet?

Anweisungen, die fehlschlagen können, werden im try-Block notiert. Löst eine Anweisung im try-Block eine Ausnahme (Exception) aus, wird keine weitere Anweisung mehr in diesem Block ausgeführt. Stattdessen wird ein Exception-Objekt erzeugt und der zum Fehler passende catch-Block verarbeitet.

Wenn kein Fehler auftritt, werden die catch-Blöcke übersprungen.

Beispiel:

try {
    Scanner sc = new Scanner(new File("myFile"));
    String s = sc.nextLine();
    sc.close();
} catch (FileNotFoundException e) {
    // hier die Fehlerbehandlung
}

Im Code-Beispiel wird mithilfe der Klasse Scanner aus einer Datei gelesen. Ist die zu lesende Datei nicht vorhanden, wird in Zeile 2 eine FileNotFoundException ausgelöst. Die Anweisungen in Zeile 3 und 4 werden nicht mehr ausgeführt. Stattdessen wird zum catch-Block in Zeile 5 gesprungen. Die Anweisungen zur Fehlerbehandlung ab Zeile 6 werden abgearbeitet.

Mehr dazu in: Mössenböck, Sprechen Sie Java, Kapitel 19.

Wie funktioniert der Modulo-Operator?

In Java beschreibt der %-Operator den Rest bei der Ganzzahldivision. Für positive Zahlen verhält sich der %-Operator genauso wie der aus der Mathematik bekannte mod-Operator.
Beispiel: 7 mod 5 = 2, 7 % 5 = 2

Unterschiede zwischen den beiden Operatoren ergeben sich bei negativen Zahlen. Der mod-Operator berechnet eine Restklasse.
Die möglichen Restklassen von n mod m sind 0 … m-1.
Beispiel: -7 mod 5 = 3

Der %-Operator in Java berechnet den Rest bei der Ganzzahldivision.
Beispiel: -7 % 5 = -2

Die mod-Operation kann in Java wie folgt implementiert werden:

static int mod(int n, int m) {
    int r = n % m;
    if (r < 0) {
        r += m;
    }
    return r;
}

Wann wird eine Variable deklariert?

Eine Variable muss deklariert werden, bevor man sie verwenden kann.
Grundsätzlich sollte die Deklaration von Variablen so lokal wie möglich erfolgen. D. h. Variablen, die nur in einem Anweisungsblock benötigt werden, sollten in diesem Block deklariert werden. Diese Vorgehensweise erhöht die Übersichtlichkeit und Lesbarkeit des Codes.

Instanz- und Klassenvariablen sind sparsam zu verwenden. Sie werden meist am Anfang der Datei deklariert. In vielen Firmen gibt es Richtlinien, die diesbezüglich Festlegungen treffen.

Was ist eine long-Variable?

Eine Variable vom Typ long dient genauso wie eine Variable vom Typ int zum Speichern von ganzen Zahlen.
Der Typ int ist 32 Bit lang und hat einen Wertebereich von -231 bis 231-1.
Der Typ long ist 64 Bit lang und hat einen Wertebereich von -263 bis 263-1.

Zur Typkompatibilität: Einer long-Variable kann ein int-Wert zugewiesen werden. Da eine long-Variable einen größeren Wertebereich wie eine int-Variable hat, kann die Zuweisung einer long-Variable an eine int-Variable nur mit explizitem Cast erfolgen.

public static void main(String[] args) {
    long a = 4294967298L;
    System.out.printf("a = %64s\n", toBinaryString(a));
    int b = (int) a;
    System.out.printf("b = %64s\n", toBinaryString(b));
}

// Ausgabe:
// a = 0000000000000000000000000000000100000000000000000000000000000010 (4294967298)
// b =                                 00000000000000000000000000000010 (         2)

// Hilfsmethoden zur formatierten Ausgabe

static String toBinaryString(int n) {
    StringBuilder sb = new StringBuilder(Long.toBinaryString(n));
    while (sb.length() < 32) {
        sb.insert(0, '0');
    }
    return sb.toString();
}

static String toBinaryString(long n) {
    StringBuilder sb = new StringBuilder(Long.toBinaryString(n));
    while (sb.length() < 64) {
        sb.insert(0, '0');
    }
    return sb.toString();
}

Wird einer int-Variable ein long-Wert zugewiesen, ist auf den Wertebereich zu achten. In Zeile 2 wird die long-Variable a mit dem Wert 232 + 2 belegt. Die Ausgabe in Zeile 9 zeigt die Zahl a im Binärformat.
In Zeile 4 wird a der int-Variable b zugewiesen. Die Ausgabe in Zeile 10 zeigt die Zahl b im Binärformat. Man sieht, dass beim Cast nur die 32 niedrig wertigen Bits (Bit 0 … 31) übernommen werden. Die Variable b hat nach dem Cast den Wert 2.

Welche Variablen werden in Java automatisch initialisiert?

Lokale Variablen werden in Java nicht automatisch initialisiert.

Instanz- und Klassenvariablen werden in Java automatisch initialisiert.

In der folgenden Tabelle sieht man die Werte, die den unterschiedlichen Typen von Variablen bei der automatischen Initialisierung zugewiesen werden.

DatentypWert
Objectnull
booleanfalse
char’\0’ *
long, int, short, byte0
double, float0.0
Default-Werte von Instanz- und Klassenvariablen

* Zeichen mit dem Code 0. Dieses Zeichen hat keine graphische Repräsentierung, ist also nicht darstellbar.

Warum gibt man Variablen eine Datentyp?

Java ist eine typsichere Sprache. Daher bekommen Variablen einen Datentyp.

Die Idee von Java und anderen typsicheren Sprachen ist, dass Variablen so deklariert werden, dass nur Werte bzw. Objekte bestimmter Typen zugewiesen werden können. In diesem Zusammenhang spielen Zuweisungskompatibilitäten und Vererbung eine Rolle.

Typisierte Sprachen haben den Vorteil, dass man viele Fehler, die man bei nicht typisierten Sprachen erst zur Laufzeit erkennen kann, schon beim Kompilieren sieht.

Was sind die Grenzen von Java? Wann muss man sich Sorgen um die Effizienz machen?

Wie andere Programmiersprachen auch, hat Java bestimmte Anwendungsbereiche.

Da Java eine plattformunabhängige Sprache ist, ist Java für die hardwarenahe Programmierung wenig geeignet.

Java ist effizient genug für die meisten Probleme, allerdings kann man bei der Umsetzung von aufwändigen Grafiken an Grenzen stoßen (Computerspiele).

Kann man die Klasse Out verwenden, um Ausgaben sowohl auf die Konsole als auch in eine Datei zu schreiben?

Nein, die gleichzeitige Ausgabe auf die Konsole und in eine Datei geht mit Klasse Out nicht. Wir möchten das anhand der folgenden Code-Sequenz erklären.

Out.println("hallo 1");
Out.open("myFile.txt");
Out.println("hallo 2");
Out.close();
Out.println("hallo 3");
Out.open("myFile.txt");
Out.println("hallo 4");
Out.close();

Initial zeigt der PrintStream auf System.out. Schreibt also auf die Konsole. Öffnet man mit Out.open() einen weiteren PrintStream s2 wird der aktuelle PrintStream s1 auf einem Stack abgelegt und s2 wird für die Ausgabe verwendet. Schließt man s2, wird s1 wieder zum aktuellen PrintStream.

Zeile 1: Die Ausgabe “hallo 1” erfolgt über den PrintStream System.out auf die Konsole.
Zeile 2: Ab hier wird als PrintStream ein FileOutputStream verwendet.
Zeile 3: In die Datei myFile.txt wird “hallo 2” geschrieben.
Zeile 4: Der FileOutputStream wird geschlossen. Der PrintStream zeigt wieder auf System.out.
Zeile 5: Auf der Konsole wird “hallo 3” ausgegeben.
Zeile 6: Ab hier wird wieder ein FileOutputStream als PrintStream verwendet.
Zeile 7: In die Datei myFile.txt wird “hallo 4” geschrieben. Dieser Text wird allerdings nicht an den bereits vorhandenen Inhalt der Datei angehängt, sondern überschreibt diesen.


Wie man in Java die gleichzeitige Mehrfachausgabe auf die Konsole und in eine Datei umsetzen kann, wird in der folgenden Code-Sequenz gezeigt.

PrintStream ps = new PrintStream(new FileOutputStream("myFile.txt"));

String s = "Hello World!";
ps.println(s);
System.out.println(s);
...
ps.close();

Was macht this.variable?

Mit this.variable greift man auf die Instanzvariable mit dem Namen variable zu.
Falls es im lokalen Scope eine zweite Variable mit dem Namen variable gibt, würde im lokalen Scope ohne die Angabe von this. die lokale Variable variable verwendet. Falls es bei lokalen Variablen und Instanzvariablen keine Namensgleichheiten gibt, kann man beim Zugriff auf Instanzvariablen this. weglassen.

Im folgenden Beispiel wird in Zeile 6 die Instanzvariable name belegt. Weil die Instanzvariable und der Parameter den gleichen Bezeichner haben, muß mit this. unterschieden werden, was gemeint ist.
In Zeile 10 haben Instanzvariable und Parameter unterschiedliche Bezeichner. Hier ist es egal, ob man this.name oder name schreibt.

public class Person {

    private String name;

    public void setName1(String name) {
        this.name = name;
    }

    public void setName2(String n) {
        name = n;
    }

    public String getName() {
        return name;
    }

}

Wie kann ich feststellen, ob eine Zeichenvariable einen Großbuchstaben, einen Kleinbuchstaben oder eine Ziffer enthält?

Dafür kann man die Klasse Character verwenden:

char d = '1';
char c = 'a';
System.out.println(Character.isLowerCase(c)); // -> true
System.out.println(Character.isUpperCase(c)); // -> false
System.out.println(Character.isDigit(d));     // -> true

Alternativ kann man diese Überprüfungen auch selber umsetzen. Ob ein char eine Ziffer repräsentiert, kann man mit folgender Methode überprüfen:

static boolean isDigit(char c) {
    return '0' <= c && c <= '9';
}

In diesem Beispiel wird geprüft, ob der Zeichencode der Variable c in einem gewissen Intervall liegt. Die untere Grenze des Intervalls entspricht der Darstellung des Zeichens '0' mit dem Wert 48 im Unicode. Analog – obere Grenze.

Man kann die Methode also auch wie folgt schreiben:

static boolean isDigit(char c) {
    return 48 <= c && c <= 57;
}

Wie vergleiche ich int und wie vergleiche ich Strings korrekt?

Primitive Datentypen (int, double, …) vergleicht man mit dem Operator ==.

Den Inhalt von Objekten, wie z.B. Strings vergleicht man mit der Methode equals().

String s = "Hello World";
String s1 = "Hello World";
System.out.println(s.equals(s1)); // ergibt true

Vergleicht man Objekte mit ==, wird ein Vergleich der Referenzen durchgeführt. Das heißt, es wird überprüft, ob beide Referenzen auf dasselbe Objekt zeigen.

Beim Vergleich von Strings mit dem Operator == kann es zu überraschenden Ergebnissen kommen.

Beispiel:

String s1 = "Hello World";
String s2 = "Hello World";
String s3 = "Hello ";
s3 += "World";
System.out.println(s1 == s2); // ergibt true
System.out.println(s1 == s3); // ergibt false
System.out.println(s1.equals(s3)); // ergibt true

String-Literale werden in Java in einem String-Pool verwaltet. Bei der Zuweisung eines String-Literals an eine Variable prüft die virtuelle Maschine, ob das Literal bereits im Pool vorhanden ist. Ist das der Fall, wird eine Referenz auf das bereits vorhandene Literal im Pool gesetzt. Ansonsten wird ein neues String-Literal im Pool abgelegt und referenziert.

Zeile 1: Der String “Hello World” wird im Pool abgelegt und von s1 referenziert.
Zeile 2: s2 referenziert den im Pool abgelegten String.
Zeile 3: Der String “Hello ” wird im Pool abgelegt und von s3 referenziert.
Zeile 4: Bei der Konkatenation von Strings wird ein neues String-Objekt erzeugt und von s3 referenziert.

Zeile 5: Der Vergleich ergibt true, weil beide Referenzen auf dasselbe Objekt im Pool zeigen.
Zeile 6: Der Vergleich ergibt false. Da bei der Konkatenation in Zeile 4 ein neues Objekt erzeugt wurde, referenzieren s1 und s3 verschiedene Objekte.
Zeile 7: Der Vergleich des Inhalts von s1 und s3 ergibt true;

Warum funktioniert der Stringvergleich mit == manchmal nicht?

Den Inhalt von Objekten, wie z.B. Strings vergleicht man mit der Methode equals().

String s = "Hello World";
String s1 = "Hello World";
System.out.println(s.equals(s1)); // ergibt true

Vergleicht man Objekte mit ==, wird ein Vergleich der Referenzen durchgeführt. Das heißt, es wird überprüft, ob beide Referenzen auf dasselbe Objekt zeigen.

Beim Vergleich von Strings mit dem Operator == kann es zu überraschenden Ergebnissen kommen.

Ein Beispiel dazu findest du im Artikel “Wie vergleiche ich int und wie vergleiche ich Strings korrekt?” in unserer FAQ.

Warum muss man immer mühsam Getter und Setter händisch anlegen?

Getter und Setter werden zur Datenkapselung und zur Definition einheitlicher Schnittstellen verwendet.

Bei der Datenkapselung wird ein direkter Zugriff auf die Daten einer Datenstruktur unterbunden. Dadurch können Fehler beim Datenzugriff vermieden werden. Ein Beispiel dazu findest du im Artikel “Was versteht man unter Datenkapselung?” in unserer FAQ.

Weiters werden Getter und Setter verwendet, um für verschiedene Implementierungen ein gemeinsames Interface anbieten zu können.

interface Tier {
    String getName();
}

class Huhn implements Tier {
    public String getName() {
        return "Huhn";
    }
}

class Katze implements Tier {
    private String name;

    public String getName() {
        return name;
    }
}

Beim Schreiben der Getter und Setter muss man sich überlegen, welche Zugriffsmethoden eine Klasse benötigt. Oft sieht man, dass unreflektiert Getter und Setter für alle Instanzvariablen einer Klasse angelegt werden. Eine solche Vorgehensweise ist selten sinnvoll.

Viele Entwicklungsumgebungen (IDEs) können Getter und Setter automatisch generieren. Will man also Getter und Setter nicht händisch einfügen, kann man auf die Möglichkeit zur automatischen Generierung von Gettern und Settern zurückgreifen.

Wie rundet man eine Gleitkommazahl auf eine ganze Zahl / auf eine bestimmte Anzahl von Kommastellen?

Soll das Ergebnis der Rundung eine Zeichenkette sein, kann man String.format() benützen:

String s = String.format("%.3f", 1.2345678);  // s = 1.235

Benötigt man den gerundeten Wert nur für die Konsolenausgabe, kann die Methode printf() verwendet werden:

System.out.printf("%.3f\n", 1.2345678);  // gibt 1.235 aus

Der String “%.3f” ist ein sogenannter Format-String. Mit dem Zeichen ‘%’ wird die Beschreibung einer Leerstelle eingeleitet. Mit dem Zeichen ‘f’ wird der Datentyp der Leerstelle spezifiziert. Im konkreten Anwendungsfall ist die Leerstelle also mit einem double-Wert zu sättigen. Mit ‘.3’ wird die Formatierung mit 3 gerundeten Nachkommastellen festgelegt.

Benötigt man die gerundete Gleitkommazahl für weitere Berechnungen, kann man folgendermaßen vorgehen:

double x = 1.2345678;
double a = Math.round(x * 1000.0) / 1000.0; // a = 1.235

Beachte: Math.round() liefert für double-Argumente ein Ergebnis vom Typ long zurück. Mit dem Typ long kann man nicht den gesamten Wertebereich des Typs double abbilden.
Long: -9223372036854775808 bis 9223372036854775807 (-263 bis 263-1)
Double: -1,7E+308 bis +1,7E+308
Ist das Argument von round() größer als die größte long-Zahl, ist das Ergebnis die größte long-Zahl.

Wie erzeugt man in Java eine Zufallszahl?

Zur Erzeugung von Pseudo-Zufallszahlen gibt es in Java verschiedene Vorgehensweisen.

Die Klasse Math bietet die statische Methode random() zur Erzeugung einer double-Zufallszahl im halboffenen Intervall \([0.0; 1.0[\) an.

Oft benötigt man ganzzahlige Zufallszahlen in einem bestimmten Bereich. Um Zufallszahlen aus der Menge \(\{a, a+1, …, b-1\}\) zu erzeugen, kann man folgenden Ausdruck verwenden:

int z = a + (int) (Math.random() * (b - a));

Mehr Möglichkeiten zum Erzeugen von Zufallszahlen bietet die Klasse java.util.Random.

Random r = new Random();
int z1 = r.nextInt(10); 
double z2 = r.nextDouble();
boolean z3 = r.nextBoolean();
double z4 = r.nextGaussian();

In Zeile 1 wird ein neues Random-Objekt erzeugt.
In Zeile 2 liefert der Aufruf der Methode nextInt() mit dem Argument 10 eine Zufallszahl aus der Menge \(\{0, 1, …, 9\}\).
In Zeile 3 liefert die Methode nextDouble() eine double-Zufallszahl im halboffenen Intervall \([0.0; 1.0[\).
In Zeile 4 wird mit der Methode nextBoolean() ein zufälliger Wahrheitswert erzeugt.
In Zeile 5 wird eine normalverteilte Zufallszahl generiert.

Was ist der Unterschied zwischen int[] b; und b = new int[5];?

Mit der Anweisung int[] b; deklariert man ein Array. Das heißt, es wird eine Referenz für ein int-Array festgelegt. Da es noch kein Array gibt, das dieser Referenz zugewiesen wurde, ist der Wert dieser Referenz null.

Damit auf Elemente des Arrays zugegriffen werden kann, ist bezüglich der Anzahl der Elemente eine Vereinbarung zu treffen. Dazu wird mit new ein Array mit festgelegter Größe erzeugt. Die Elemente des Arrays haben default-Werte des Datentyps. Mehr dazu findest du in der Tabelle “Default-Werte von Instanz- und Klassenvariablen” in unserer FAQ.

int b[];
b = new int[5];

Mit der Anweisung b = new int[5]; wird das Array b initialisiert und Speicher für 5 Array-Elemente reserviert.


Anmerkung: Ein Array kann auch durch Aufzählen der Werte initialisiert werden. Diese Art der Initialisierung muss unmittelbar bei der Deklaration erfolgen:

int b[] = { 2, 4, 1, 5, 3 };

Wie kann man auf Bibliotheken zugreifen?

Beim Zugriff auf Bibliotheken muss man unterscheiden, ob es sich um die mitgelieferte Java-Klassenbibliothek oder um externe Bibliotheken handelt. Die Java-Klassenbibliothek stellt Klassen mit oft benötigten Funktionalitäten zur Verfügung.

Relevante Pakete der Klassenbibliothek sind:

  • java.util
    Neben wesentlichen Datenstrukturen wie List, Map, Set enthält dieses Paket Klassen wie Random, Locale, Formatter, Scanner.
  • java.lang
    Wichtige Klassen diese Pakets sind Math, String, Thread, StringBuilder, Double, Integer, …
  • java.io
    In diesem Paket findet man Klassen zum Dateizugriff, wie File, FileReader, FileWriter, BufferedInputStream, IOException, …
  • javax.swing
    Die von diesem Paket zur Verfügung gestellten Funktionalitäten werden zum Erstellen von graphischen Benutzeroberflächen genutzt. Oft verwendete Klassen dieses Pakets sind JFrame, JPanel, JButton, JTextField, JComboBox, …
  • java.net
    In diesem Paket finden sich Klassen zur Netzwerkkommunikation, wie InetAddress, Socket, ServerSocket, URLConnection, …
  • java.sql
    Mithilfe der in diesem Paket enthaltenen Funktionalitäten können Interaktionen mit sql-Datenbanken realisiert werden.

Für den Zugriff auf Klassen der Java-Klassenbibliothek muss im Programm der Pfad zur entsprechenden Klasse bekannt sein. Der Pfad kann entweder über eine import-Anweisung oder durch Angabe des vollständigen Pfads beim Klassennamen erfolgen.

Eine Ausnahme bildet das Paket java.lang. Der Pfad zu diesem Paket ist auch ohne explizite Angabe bekannt.

import java.net.URL;  // importiert die Klaas URL
import javax.swing.*; // importiert alle Klassen im Paket javax.swing

java.util.List<Integer> li = new java.util.ArrayList<>(); // Angabe des vollständigen Pfads

Externe Bibliotheken werden meist in Form von .jar-Dateien in Projekte eingebunden, indem man die jar-Dateien ins Projektverzeichnis kopiert und dem Build-path hinzufügt. Im Anschluss daran kann auf die externe Bibliothek genauso wie auf die Java-Klassenbibliothek zugegriffen werden.

In Eclipse funktioniert das wie folgt:
Kontextmenü auf die Bibliotheks-Datei > Build path > Add to Build path.

Wie kann ich herausfinden, welche Methoden ich für eine Klasse/Objekt verwenden kann?

Einen umfangreichen Einblick in die Methoden der Klassen der Java-Klassenbibliothek gibt die offizielle Dokumentation von Java.

Viele Entwicklungsumgebungen bieten die Möglichkeit, die Methoden einer Klasse aufzulisten und Dokumentationen der Methoden anzuzeigen. Im folgenden Screenshot sieht man, wie das in der Entwicklungsumgebung Eclipse ausschaut.

Autocomplete – Eclipse

Im konkreten Beispiel werden die Methoden der Klasse Random über ein Autocomplete aufgelistet, sobald man r. eingegeben hat. Mit der Tastenkombination Strg + Leertaste kann das Autocomplete manuell getriggert werden. Über Benutzerinteraktion kann die Dokumentation zu den einzelnen Methoden abgerufen werden.

Analog können die Methoden einer Klasse über den Package-Explorer ausgewählt und angezeigt werden.

Um den Umgang mit den Klassen der Java-Bibliothek besser kennenzulernen, gibt es viele Tutorials auf der Webseite von Oracle.

Welchen Sinn hat es eine gut ausgestattete Entwicklungsumgebung zu verwenden?

Für Anfänger kann es durchaus sinnvoll sein, mit einfachen Editoren zu arbeiten. Dadurch wird die Syntax gelernt und geübt.

Die Verwendung einer gut ausgestatteten Entwicklungsumgebung (IDE) – wie beispielsweise Eclipse – erleichtert vor allem bei größeren Projekten die Software-Entwicklung. Wichtige Features solcher Entwicklungsumgebungen sind:

  • Fehler und Warnungen, die der Compiler erkennen kann, werden bereits beim Schreiben des Codes gekennzeichnet.
  • Autocomplete – Beim Autocomplete werden beim Schreiben von Variablen- oder Methodennamen Vorschläge gemacht, die durch Drücken der Eingabetaste in den Code übernommen werden können. Das ist insbesondere bei längeren Namen eine große Erleichterung.
  • Syntaxhervorhebung – Unterschiedliche syntaktische Elemente wie Schlüsselwörter, Variablen, Konstanten, Literale, Kommentare werden typographisch und farbig hervorgehoben.
  • Autoformat – Mit dem Autoformat kann die Formatierung des Codes automatisch durchgeführt werden.
  • Refaktorisierung – Die Refaktorisierung wird oft zum Umbenennen von Namen oder zum Extrahieren von Code-Sequenzen in eine eigene Methode verwendet.
  • Verknüpfung mit der Dokumentation – Die Javadoc-Kommentare werden als Tooltip oder in einem separaten Fenster angezeigt. Mehr dazu findest du im Artikel “Wie schreibe ich Kommentare?” in unserer FAQ.
  • Debugger – Mit dem Debugger kann ein Programm an einer bestimmten Stelle (Breakpoint) angehalten werden, um die Werte der Variablen an dieser Stelle untersuchen zu können. Es kann beispielsweise mit Triggern gearbeitet werden, das Programm kann in Einzelschritten ausgeführt werden, …
  • Zusammenarbeit im Team beim Verwenden einer Versionsverwaltung

Wie genau funktioniert System.out.println()?

In Java gibt es die Klasse System. Diese bietet die Ausgabekanäle System.out und System.err sowie den Eingabekanal System.in an.

Das Objekt System.out ist vom Typ PrintStream. Der PrintStream System.out gibt die ihm übergebenen Daten typischerweise auf der Konsole aus. Um Daten in den PrintStream zu schreiben, stehen verschiedene Methoden zur Verfügung: print(), println(), printf(), append(), write().

Die Methode println() vom Objekt System.out nimmt Daten entgegen, schreibt sie in den PrintStream und hängt einen Zeilenumbruch an.
Die Methode printf() vom Objekt System.out nimmt Daten entgegen, formatiert diese und schreibt sie in den PrintStream.

Wie installiert man Eclipse?

Auf der Homepage der Eclipse Foundation findest du eine detaillierte Schritt- für Schritt Anleitung.

Wieso muss man String in Java großschreiben, aber z.B. int nicht?

In Java ist es Konvention, dass Klassennamen mit einem Großbuchstaben beginnen und primitive Datentypen mit einem Kleinbuchstaben. String ist ein Klassenname und int bezeichnet einen primitiven Datentyp.

Was bringt es, yield statt return in einer Funktion zu benutzen?

Die Methode yield() wird üblicherweise in der run() Methode eines Threads verwendet, um den Thread zu unterbrechen und die Kontrolle an andere Threads abzugeben. Der unterbrochene Thread wird nach einer gewissen Zeit wieder fortgesetzt. Das heißt, er bekommt die Kontrolle zurück.

Schreibt man anstelle von yield() die Anweisung return, wird die Methode run() und somit der Thread beendet.

Wie kann ich bestimmen, dass eine Eingabe nur numerisch sein darf?

In Java kann eine Eingabe überprüft werden, indem man versucht, den eingegebenen String mit Double.parseDouble() oder Integer.parseInt() in eine Zahl umzuwandeln. Wenn der String keinen numerischen Wert beschreibt, wird eine IllegalArgumentException ausgelöst. In diesem Fall kann eine Fehlermeldung angezeigt werden.

Im folgenden Beispiel wird der Eingabedialog wiederholt, falls die Eingabe nicht numerisch war.

Scanner sc = new Scanner(System.in);
double d;
while (true) {
    try {
        System.out.print("Gib Zahl > ");
        String s = sc.next();
        d = Double.parseDouble(s);
        break;
    } catch (NumberFormatException e) {
        System.out.println("FEHLER: Die Eingabe muss eine Zahl sein.");
    }
}
System.out.println("Du hast den numerischen Wert " + d + " eingegeben.");
sc.close();

Ausgabe:

Gib Zahl > hallo
FEHLER: Die Eingabe muss eine Zahl sein.
Gib Zahl > 1,2
FEHLER: Die Eingabe muss eine Zahl sein.
Gib Zahl > 3.14
Du hast den numerischen Wert 3.14 eingegeben.

Was wäre ein Beispiel für eine teure Operation bzw. eine billige Operation in einem Programm?

Bei der Bewertung der Kosten einer Operation können Faktoren wie Rechenzeit, Speicherbelegung oder der Verbrauch an menschlichen Ressourcen durch den Entwicklungsaufwand eine Rolle spielen.

Bsp 1: Eine billige Operation bezüglich der Rechenzeit ist die Änderung des Werts eines Array-Elements bei gegebenem Index. Verhältnismäßig teuer ist es, ein Array-Element am Anfang eines langen Arrays einzufügen, da alle anderen Elemente des Arrays um eine Position nach hinten verschoben werden müssen, bevor eingefügt werden kann.

Bsp 2: Einen langen String mit dem + Operator aufzubauen ist in Java bezüglich Rechenzeit und Speicherbelegung eine teure Operation. Bei jeder Konkatenation von zwei Strings wird intern ein StringBuilder-Objekt erzeugt, der String zusammengesetzt und abschließend die toString() Methode des Stringbuilders aufgerufen. Erzeugt man explizit einen Stringbuilder und führt die String-Konkatenationen mit diesem Stringbuilder durch, ist dies wesentlich billiger.

Beispiel mit Zeitmessung:

static long f1(int n) {
    long t1 = System.nanoTime();
    String s = "";
    for (int i = 0; i < n; i++) {
        s += "a";
    }
    long t2 = System.nanoTime();
    return (t2 - t1) / 1000;
}

static long f2(int n) {
    long t1 = System.nanoTime();
    StringBuilder s = new StringBuilder();
    for (int i = 0; i < n; i++) {
        s.append("a");
    }
    long t2 = System.nanoTime();
    return (t2 - t1) / 1000;
}

public static void main(String[] args) {
    for (int n = 1000; n <= 128000; n *= 2) {
        System.out.printf("n=%6d | %7dµs | %7dµs\n", n, f1(n), f2(n));
    }
}

Sowohl in der Methode f1(n) als auch in der Methode f2(n) wird ein String der Länge n durch wiederholtes Anhängen eines einzelnen Zeichens aufgebaut.

In der Methode f1() wird der +-Operator für den Aufbau des Strings verwendet.
In der Methode f2() wird der String mithilfe eines explizit erzeugten Stringbuilders aufgebaut.

Zeitmessungen von f1() und f2()

Die Auswertung der Zeitmessung von f1() zeigt, dass der Aufwand bei der Konkatenation mit dem +-Operator quadratische Ordnung hat. Das heißt, bei einer Verdoppelung der Anzahl der Konkatenationen n steigt der Zeitaufwand auf das Vierfache.

Im Vergleich dazu ist der Aufwand bei der Konkatenation mit dem Stringbuilder (f2()) linear. Das heißt, bei einer Verdoppelung der Anzahl der Konkatenationen n verdoppelt sich der Zeitaufwand.

Bezüglich der Rechenzeit verbraucht f1() also wesentlich mehr Ressourcen als f2(). Bewertet man den Faktor Zeit, ist also f1() wesentlich teurer als f2().

Was ist der Unterschied zwischen einem Referenzdatentyp und einem Wertdatentyp?

Ein Wertdatentyp (primitiver Datentyp) kann einen numerischen Wert oder einen Wahrheitswert speichern. Für diesen Datentyp gibt es arithmetische oder logische Operatoren. Beispiele für Wertdatentypen in Java sind int, double, boolean, char, …

Ein Referenzdatentyp kann die Speicheradresse des referenzierten Objekts speichern. Für diesen Datentyp gibt es keine arithmetischen Operatoren. Beispiele für Referenzdatentypen in Java sind Object, String, List, Arrays, …

Zuweisungsoperationen:

Ein Wertdatentyp enthält einen Wert w. Wird dieser Wert einer zweiten Variable zugewiesen, enthält diese Variable eine Kopie von w.

Bei einem Referenzdatentypen wird einer Variable eine Kopie der Referenz zugewiesen. Die referenzierten Daten werden dabei nicht dupliziert.

Javabeispiel:

int a[] = new int[10]; // a ist eine Referenz auf ein Array
int b[] = a; // b ist Referenz auf das Array a, 
             // a und b zeigen auf das selbe Array
b[2] = 123;  // Das von a und b referenzierte Array 
             // bekommt am Index 2 einen Wert zugewiesen
System.out.println(a[2]); // 123 wird ausgegeben

Wie gebe ich eine bestimmte Formatierung aus?

In Java kann mit System.out.printf(formatstring, argumente) eine formatierte Ausgabe erzeugt werden.

Der Formatstring enthält den auszugebenden konstanten Text, sowie Platzhalter (Leerstellen) für die Argumente. Die angeführten Argumente sättigen die Leerstellen.
Es gibt verschiedene Platzhalter für unterschiedliche Datentypen, unter anderem %s für Strings, %d für Ganzzahlen, %f für Fließpunktzahlen, %x für eine hexadezimale Ausgabe.

Beispiel:

double x = 1.0456;
String s = "kg";
System.out.printf("Menge: %06.2f %s", x, s); // gibt aus: Menge: 001,05 kg

Bedeutung der einzelnen Zeichen im Formatstring %06.2f:

  • % – Kennzeichnet den Beginn einer Leerstelle
  • 0 – Beschreibt, dass bei der Ausgabe führende Nuller geschrieben werden.
  • 6 – Gibt an, dass die Ausgabe mindestens 6 Zeichen lang ist. Zu diesen 6 Zeichen gehören Vorkommastellen (mit optionalem negativem Vorzeichen), Nachkommastellen, der Dezimaltrenner und gegebenenfalls der Exponent.
  • .2 – Beschreibt die Anzahl der Nachkommastellen
  • f – Legt fest, dass die Leerstelle mit einer Fließpunktzahl zu sättigen ist.

Mehr Informationen und Beispiele zur formatierten Ausgabe findet man im Buch “Java ist auch eine Insel” von Christian Ullenboom im Kapitel 4.10 Ausgaben formatieren.

Wie werden in Java irrationale Zahlen behandelt?

Irrationale Zahlen werden genauso wie rationale Zahlen in Java mit einer endlichen Anzahl von Stellen als Fließpunktzahlen dargestellt.

Beispiel:
In der Klasse Math werden die irrationalen Zahlen \(e\) und \(\Pi\) folgendermaßen dargestellt:

    /**
     * The {@code double} value that is closer than any other to
     * <i>e</i>, the base of the natural logarithms.
     */
    public static final double E = 2.7182818284590452354;

    /**
     * The {@code double} value that is closer than any other to
     * <i>pi</i>, the ratio of the circumference of a circle to its
     * diameter.
     */
    public static final double PI = 3.14159265358979323846;

Was ist der Unterschied zwischen “extends” und “implements”?

Extends verwendet man für die Vererbung von Klassen. Leitet man eine Unterklasse B mit extends von der Oberklasse A ab, übernimmt die Klasse B alle nicht privaten Eigenschaften und Methoden von A. In der Unterklasse können zusätzliche Eigenschaften und Methoden implementiert werden.
In Java kann mit extends nur von einer Klasse geerbt werden.

Extends kann auch verwendet werden, um Interfaces zu erweitern.

Beispiel:

interface A {
    void m1();
}

interface B extends A {
    void m2();
}

class C implements B {

    @Override
    public void m1() {
        // TODO Auto-generated method stub
    }

    @Override
    public void m2() {
        // TODO Auto-generated method stub
    }
}

Das Interface B erweitert Interface A um die Methode m2(). Klasse C implementiert das Interface B, muss also die Methoden m1() und m2() implementieren.

Implements verwendet man für die Implementierung von Interfaces. Alle im Interface definierten Methoden müssen von der abgeleiteten Klasse implementiert werden, außer die abgeleitete Klasse ist mit dem Schlüsselwort abstract versehen. In diesem Fall müssen die Methoden nicht implementiert werden.
In Java kann mit implements von mehreren Interfaces geerbt werden.

Was bedeutet “GUI”?

Ein GUI (Graphical User Interface) stellt den Benutzern und Benutzerinnen grafische Ein- und Ausgabe-Elemente wie Textfelder, Buttons, Check-Boxen, Combo-Boxen oder Dialogfenster zur Interaktion mit einer Anwendung zur Verfügung.

In Java gibt es für die Programmierung graphischer Oberflächen unter anderem die Bibliotheken Swing und JavaFX.

Beispiele fur Klassen der Bibliothek javax.swing sind:

  • JFrame – Ist ein Container der Elemente wie z.B. Panels, Menü, … enthält.
  • JPanel – Container, der GUI-Elemente wie Textfelder oder Buttons enthält.
  • JButton
  • JTextField
  • JCheckBox

Wo sind Attribute, Methoden oder Klassen sichtbar, sollte man weder “public”, “private” noch “protected” davor schreiben?

Sind keine Modifikatoren (public, private, protected) angegeben, ist die Sichtbarkeit der Attribute, Methoden und Klassen package private. Das heißt, sie sind nur innerhalb des Pakets sichtbar.

Die folgende Tabelle zeigt eine Übersicht über die Modifikatoren und ihre Auswirkung auf die Sichtbarkeit von Attributen, Methoden und Klassen.

ModifikatorKlassePaketUnterklasseProgramm
publicJaJaJaJa
protectedJaJaJaNein
kein ModifikatorJaJaNeinNein
privateJaNeinNeinNein
Sichtbarkeit von Attributen, Methoden und Klassen

Es soll eine Methode implementiert werden, die den Wert x zurückgibt. Was bewirkt die return-Anweisung oder die print-Anweisung in diesem Zusammenhang?

Soll eine Methode implementiert werden, die einen Wert x zurückgibt, ist die return-Anweisung zu verwenden. Mit der return-Anweisung wird eine Methode beendet. Folgt der return-Anweisung ein Ausdruck, wird mit dem Beenden der Methode der Wert des Ausdrucks zurückgegeben.

Soll eine Methode implementiert werden, die den Wert x auf die Konsole schreibt, verwendet man die print-Anweisung.

int m() {
    int x = 123;
    System.out.println(x); // Gibt den Wert von x auf der Konsole aus
    return x;              // Methode wird beendet und der Wert von x zurückgegeben
}

Warum verwende ich Threads? Wie startet man einen Thread? Was ist ein Deadlock bei mehreren Threads?

Warum verwende ich Threads?

Threads werden verwendet, um mehrere Aufgaben oder Tasks nebeneinander auszuführen. Benötigt eine Aufgabe (z.B. Dateidownload) viel Zeit, ist es sinnvoll, diese Aufgabe in einem eigenen Thread auszuführen. Damit ist es möglich, dass das Programm während des Downloads andere Tasks ausführt. Solche Tasks könnten die Abarbeitung einer Benutzereingabe oder die Aktualisierungen der Fortschrittsanzeige sein.

Wie startet man einen Thread in Java?
Thread t = new Thread() {
    @Override
    public void run() {
        // Code der im Thread ausgeführt wird
    }
};
t.start();

In Zeile 1 wird ein neues Thread-Objekt t erzeugt.
Ab Zeile 3 wird die Methode run() der Klasse Thread implementiert. Diese Methode enthält den Code, der nebenläufig ausgeführt werden soll.
In Zeile 7 wird der Thread t gestartet.

Achtung: Startet man die run-Methode mit t.run(), wird die Methode nicht nebenläufig im Thread ausgeführt, sondern als normaler Methodenaufruf abgearbeitet. Das heißt, die weitere Ausführung des Programms wird blockiert, bis die Methode run() beendet ist.

Was ist ein Deadlock?

Deadlocks entstehen, wenn zwei Threads wechselseitig auf eine Ressource warten, die der jeweils andere Thread für sich beansprucht.

Beispiel: Thread t1 und Thread t2 wollen auf die Dateien a und b schreibend zugreifen.
Für den schreibenden Zugriff benötigen Threads einen exklusiven Zugriff auf die Datei, damit nicht abwechselnd unterschiedliche Threads in eine Datei schreiben können.
Thread t1 öffnet die Datei a. Thread t2 öffnet die Datei b. Im Anschluss daran möchte t1 die Datei b öffnen und t2 die Datei a. Thread t1 kann die Datei b nicht öffnen, weil sie bereits von t2 geöffnet ist. Beide Threads warten an dieser Stelle also darauf, dass der jeweils andere Thread die Datei schließt.

Beispiel aus dem Alltag: 4 Fahrzeuge stehen an einer Kreuzung. Gemäß der Rechtsregel wartet jedes Fahrzeug darauf, dass das von rechts kommende Fahrzeug die Kreuzung passiert.

Was ist der Unterschied zwischen “JDK”, “JRE” und “JVM”? Wie spielen diese zusammen?

Das JDK (Java Development Kit) ist eine Sammlung von Programmierwerkzeugen und Programmbibliotheken zur Entwicklung von Software. Das JDK enthält einen Compiler, der den von dir geschriebenen Code in Bytecode für die virtuelle Maschine übersetzt. Die Programmbibliothek (Java-Klassenbibliothek) enthält Klassen für mathematische Funktionen, Datei-Zugriff, Datenstrukturen, graphische Benutzeroberflächen und vieles mehr. Die Klassen der Bibliothek kannst du in dein Programm einbinden (import) und verwenden.

Das JRE (Java Runtime Environment) enthält unter anderem die Programmbibliothek und die virtuelle Maschine (JVM).

Die JVM (Java Virtual Machine) wandelt den vom Compiler erzeugten Bytecode in Maschineninstruktionen für das jeweilige Zielsystem um und führt den Maschinencode aus.

Mehr dazu findest du im Artikel “Wie wird aus einem Java-Code ein fertiges Programm?” in unserer FAQ.

Wie erzeugt man Ausgaben zum Testen eines Programms.

Für einfache Übungen oder zum Testen von Programmen werden häufig Kontrollausgaben auf die Konsole geschrieben. In Java wird dafür folgender Befehl verwendet: System.out.println(...);

Die println() Methode kann mit verschiedenen Datentypen verwendet werden:

int n = 42;
double x = 3.14159;
String s = "Hallo";
System.out.println(n);
System.out.println(x);
System.out.println(s);

// Ausgabe:
// 42
// 3.14159
// Hallo

Schreibt man eine Klasse, kann es hilfreich sein, die von Object abgeleitete Methode toString() zu implementieren. Diese Methode gibt einen String zurück, der bei zweckmäßiger Verwendung Informationen zum Objekt enthält. Diese Informationen können zum Testen mit der println() Methode auf die Konsole ausgegeben werden.

class Point2D {
    public final float x, y;

    public Point2D(float x, float y) {
        this.x = x;
        this.y = y;
    }

    @Override
    public String toString() {
        return "[x=" + x + ", y=" + y + "]";
    }
}

// Testausgabe:
Point2D p = new Point2D(3, 4);
System.out.println(p);  // [x=3.0, y=4.0]

Übergibt man der Methode toString() das Objekt p, so wird intern p.toString() aufgerufen und die Informationen zum Objekt ausgegeben.


Anmerkung: Man kann ein Programm auch testen, ohne Kontrollausgaben im Code zu schreiben. Verwendet man eine geeignete Entwicklungsumgebung, kann man auf einen Debugger zurückgreifen. Mit dem Debugger kann ein Programm an bestimmten Stellen (Breakpoint) angehalten oder zeilenweise ausgeführt werden. Immer wenn das Programm anhält, werden die Inhalte der Variablen angezeigt.