FAQs – Allgemeine Konzepte der Programmierung

FAQs – Allgemeine Konzepte der Programmierung

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).

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 iterativ / rekursiv?

Der Begriff iterativ wird bei Algorithmen verwendet, die zum Finden einer Lösung unter Verwendung von bereits berechneten Zwischenergebnissen wiederholt die gleiche Prozedur ausführen. Beispiele für solche Algorithmen: Newtonverfahren, Primzahlsuche, …

Die Idee eines rekursiven Verfahrens wurde in dieser FAQ in folgendem Beitrag erläutert.

Im folgenden Sourcecode ist ein Vergleich zwischen rekursiver und iterativer Implementierung der Fakultätsfunktion \(n! = 1*2* … *n\) dargestellt.

static double factIterativ(int n) {
    double result = 1.0;
    for (int i = 1; i <= n; i++) {
        result *= i;
    }
    return result;
}

static double factRekursiv(int n) {
    if (n > 1) {
        return n * factRekursiv(n - 1);
    } else {
        return 1;
    }
}

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.

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.

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,

Was sind Pointer? Worauf zeigen Pointer? Was ist wenn ich den Wert ändere?

Wird in einem Programm auf einen Wert zugegriffen, erfolgt das implizit über eine Speicher-Adresse. Der Zugriff auf eine Speicheradresse kann mit einem Pointer (Zeiger) erfolgen. Man sagt auch, ein Pointer zeigt auf eine Speicheradresse.

In C haben Pointer einen Typ. Die Deklaration: int *p; kann man lesen als: *p ist vom Typ int, p ist ein Zeiger auf den int-Wert *p. Mit *p kann also auf den int-Wert, auf den der Zeiger p zeigt, zugegriffen werden.

C:

int a, *pa;
a = 123;
pa = &a;
*pa = 456;
printf("%d %d %p\n", a, *pa, pa); // Ausgabe: 456 456 0028fee0

In Zeile 1 wird die int-Variable a sowie ein Zeiger pa auf eine int-Variable deklariert.
In Zeile 3 wird mit dem Adressoperator (&) die Adresse von a bestimmt und dem Zeiger pa zugewiesen. Man sagt, der Zeiger pa zeigt auf a.
In Zeile 4 wird mit dem Inhaltsoperator / Dereferenzierungsoperator (*) der Wert an der Speicherstelle, auf die der Zeiger pa zeigt, verändert. a hat jetzt den Wert 456.
In Zeile 5 wird a, der Inhalt auf den pa zeigt, sowie die Speicheradresse auf die pa zeigt, ausgegeben.

Zeiger auf Arrays:

int *p;
int ar[] = { 1, 4, 9, 16 };
p = ar;
printf("%d %p\n", *p, p); // 1 0028fee0
p++;
printf("%d %p\n", *p, p); // 4 0028fee4

In Zeile 1 wird ein Pointer vom Typ int angelegt.
In Zeile 2 wird das Array ar[] deklariert und initialisiert.
In Zeile 3 wird der Pointer p auf das Array ar[] gesetzt. D.h. p enthält die Speicheradresse von ar[0]. Als Synonym für den Code-Sequenz p = ar; könnte man also theoretisch auch die Code-Sequenz p = &ar[0]; verwenden.
In Zeile 4 wird der Wert von ar[0] und die Speicheradresse von ar[0] ausgegeben.
In Zeile 5 wird der Zeiger p inkrementiert. Er zeigt jetzt also auf ar[1].
In Zeile 6 wird der Wert von ar[1] und die Speicheradresse von ar[1] ausgegeben.

Vergleicht man die zwei ausgegebenen Speicheradressen, sieht man, dass die Differenz 4 beträgt. Dieser Wert entspricht der Byte-Breite eines int-Wertes im Speicher.

Beispiel Pointerarithmetik:

Mittels Pointer und Pointer-Arithmetik lassen sich verschiedene Algorithmen sehr kompakt formulieren. Im folgenden Code-Sample wird eine Methode zur Bestimmung der Länge eines Strings gezeigt.

int strlen(char *s) {
    int len = 0;
    while (*s++) {
        len++;
    }
    return len;
}

Der Methode strlen() wird ein Zeiger auf eine Zeichenkette übergeben.

In Zeile 3 wird mit dem Inhaltsoperator auf das aktuelle Zeichen zugegriffen und als Schleifenbedingung verwendet. Im Anschluss daran wird der Zeiger inkrementiert. Wenn der Zeiger auf das Terminierungssymbol der Zeichenkette zeigt, wird die Schleife beendet. In jedem Iterationsschritt wird der Zähler für die Länge der Zeichenkette um 1 erhöht und nach Beenden der Schleife als Rückgabewert verwendet.

Anmerkung:

Zeichenketten werden in C durch char-Arrays die null-terminiert sind dargestellt. Das letzte Zeichen einer null-terminierten Zeichenkette hat den ASCII-Code 0 (‘\0’). In C gibt es keinen Datentyp für Wahrheitswerte. Der Wert 0 wird in einem boolschen Ausdruck auf false evaluiert. Alle anderen numerischen Werte werden auf true evaluiert.

Beispiel: Inhaltsoperator / Adressoperator:

Der Inhaltsoperator verhält sich invers zum Adressoperator.

*(&a) ≡ a

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) { ... }

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.

Wie muss man sich einen rekursiven Code vorstellen?

Bei einer Rekursion wird eine Frage mit einer Frage beantwortet.

Will man beispielsweise die Faktorielle von 3 berechnen (3!), kann man das tun, indem man die Faktorielle von 2 berechnet und das Resultat mit 3 multipliziert.
Stellt sich freilich die Frage, was die Faktorielle von 2 ist. Diese Frage wird nach dem gleichen Schema beantwortet. Man stellt sich die Frage, was die Faktorielle von 1 ist und multipliziert das Ergebnis mit 2.
Die Frage nach der Faktoriellen von 1 wird nicht durch einen weitere Frage beantwortet, sondern ist per Festlegung 1. Somit ist die Beantwortung der Frage nach der Faktoriellen von 2 einfach zu beantworten: 1 · 2 = 2
Die Beantwortung der Frage nach der Faktoriellen von 3 erfolgt dann analog: 2 · 3 = 6

 ⭣ ⭡6
fact(3) = 3 * fact(2)
               ⭣ ⭡2
              fact(2) = 2 * fact(1)
                             ⭣ ⭡1
                            fact(1) = 1

Eine Rekursion benötigt immer ein Abbruchkriterium. Im oben ausgeführte Beispiel ist das der Aufruf der Faktoriellen mit dem Argument 1.

Code-Beispiel:

public static int fact(int n) {
    if (n > 1) {
        return n * fact(n - 1); // Rekursion
    } else {
        return 1; // Abbruchkriterium
    }
}

Welche Bedeutung haben Zeiger und Referenzen bei Arrays?

In verschiedenen Programmiersprachen werden Pointer- oder Zeigerkonzepte umgesetzt. In C beispielsweise beschreibt ein Pointer auf ein Array eine Adresse im Speicher, an der der Wert des Arrayelements mit dem Index 0 abgespeichert ist.

Pointer-Arithmetik:
Inkrementiert man beispielsweise in C den Pointer auf ein Array um eins wird auf den Wert des nachfolgenden Arrayelements gezeigt. Details dazu findet man in unserer FAQ hier.

In Java wird nicht mit Pointern sondern mit Referenzen gearbeitet. Auch eine Referenz bezieht sich auf eine Speicheradresse. Allerdings gibt es für Referenzen keine arithmetischen Operationen.

Was sind Shifts?

Mit Hilfe der Shift-Operation m << n werden die Bits der Zahl m um n Positionen nach links verschoben. Analog werden mit dem Operator >> die Bits nach rechts verschoben.

Beispiel: 5 << 2
Binärdarstellung der Zahl 5: 0000 0101
0000 0101 um zwei Position nach links geschoben, ist in der Binärdarstellung 0001 0100. Im Zehnersystem also 20.

Pro Shift um eine Stelle nach links, erfolgt eine Verdoppelung der Zahl. Pro Shift um eine Stelle nach rechts, erfolgt eine Halbierung der Zahl.

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

Was ist Reflection und wofür verwendet man das?

Mit Reflection kann zur Laufzeit auf Informationen über Klassen und Objekte zugegriffen werden. Beispielsweise kann ermittelt werden, welche Methoden und Variablen es in einer Klasse gibt. Außerdem ist es möglich, mit Reflection Objekte von Klassen zu erzeugen, die zur Übersetzungszeit noch nicht bekannt sind.

Somit ist Reflection ein geeignetes Mittel für die Umsetzung von Plugin-Konzepten oder grafischen GUI-Editoren.

Im folgenden Beispiel wird gezeigt, wie mittels Reflection aus einem Objekt der Klasse A die Namen und Werte aller Felder ausgelesen werden können.

class A {
    int n = 123;
    String s = "hello";
}
import java.lang.reflect.Field;

public class Reflection_Demo {
    public static void main(String[] args) throws IllegalArgumentException, IllegalAccessException {
        A a = new A();
        Field f[] = a.getClass().getDeclaredFields();
        for (Field fi : f) {
            String name = fi.getName();
            Object value = fi.get(a);
            System.out.println(name + " = " + value);
        }
    }
}

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.

Wann ist es sinnvoll, Methoden in eine eigene Klasse zu schreiben?

Es ist sinnvoll, statische Methoden, die inhaltlich zusammen gehören, in einer eigenen Klasse zu sammeln. Ein Beispiel für eine solche Klasse in Java ist die Klasse java.lang.Math. Diese enthält mathematische Funktionen wie sin, cos, log, exp, …

In der objektorientierten Programmierung versucht man die Komplexität eines Problems zu reduzieren, indem man durch Klassen mit den dazugehörenden Methoden Funktionalitäten kapselt. Diese Kapselungen sind oft auch eine abstrahierte Abbildung der Realität. Eine Klasse Buch kann Methoden zu Abfrage des Buchtitels, eine Methode zur Abfrage des Autors oder der Autorin und eine Methode zum Setzen der Inhaltsangabe enthalten. Die Klasse Bücherei kann eine Methode enthalten, mit der man ein Buch zum Bestand hinzufügt. Eine weitere Methode dieser Klasse könnte die Entlehnung eines Buches abbilden …