Inhaltsverzeichnis



Skript zur Vorlesung Grundlagen der Informatik I (Java)

URL dieses Dokuments: http://wiki.macke.it/doku.php/fhwt:skriptjava

Einführung in Java

Java ist eine Compilersprache, was bedeutet, dass der Quelltext, den ein Programmierer erstellt, von einem speziellen Übersetzungsprogramm - dem Compiler - in ausführbaren Maschinencode übersetzt werden muss. Die Besonderheit bei Java ist dabei, dass der erzeugte Maschinencode nicht plattformspezifisch ist, sondern auf der Java Virtual Machine (JVM) ausführbar ist, die es für so ziemlich jedes Betriebssystem gibt. Daher sind Javaprogramme plattformunabhängig (eine Programmiersprache → viele Plattformen). Der vom Compiler erzeugte Maschinencode heißt Bytecode. Im Vergleich dazu verfolgt Microsofts .NET Framework den Ansatz, mit mehreren Programmiersprachen (C#, Visual Basic usw.) eine Plattform (Windows) zu bedienen (wobei es z.B. mit Mono auch schon eine Implementierung von .NET für Linux gibt).

Der normale Ablauf beim Programmieren ist:

  1. Quelltext im Editor eingeben
  2. Quelltext mit dem Compiler in Bytecode übersetzen
  3. Bytecode auf der Java-VM ausführen

Die Java-VM ist Bestandteil des Java Runtime Environment (JRE), dass auf einem Betriebssystem installiert sein muss, damit man Java-Programme ausführen kann. Im Gegensatz dazu enthält das sog. Java Development Kit (JDK) zusätzliche Programmierwerkzeuge (insb. den Compiler), um selbst Programme zu erstellen.

Ein einfaches Beispielprogramm in Java sieht z.B. so aus:

public class HelloWorld 
{
    public static void main(String[] args)
    {
        System.out.println("Hello World!");
    }
}

Dieses Programm muss in einer Datei HelloWorld.java gespeichert und dann mit javac HelloWorld.java kompiliert werden. Dazu muss javac.exe im PATH liegen. Danach kann das Programm mit java HelloWorld aufgerufen werden. Wichtig: Die Datei muss exakt so heißen wie die Klasse, die in ihr definiert wird. Groß- und Kleinschreibung wird unterschieden (Java ist case-sensitive).

Des weiteren ist es sehr wichtig, die Syntax der Programmiersprache exakt einzuhalten. Schon der kleinste Tippfehler kann dazu führen, dass das Programm nicht mehr kompiliert werden kann. Hier ist ein Beispielprogramm, das einige Syntaxfehler enthält:

static void main(String arg[]) {
System.out.Println(Die Zahl ist 127 )
System.out.Println(Das doppelte davon ist  2*127)
System.out.Println(ENDE)

Für Menschen ist es schwierig, an all diese Dinge zu denken. Daher gibt es Programmierwerkzeuge, die einem diese Arbeit abnehmen. Das bekannteste Beispiel im Java-Umfeld ist Eclipse, ein Integrated Development Environment (IDE). Insbesondere bei der Teamarbeit mit Hilfe einer zentralen Quelltextverwaltung wie z.B. Subversion (SVN) ist es sinnvoll, dass alle Teammitglieder die gleichen Einstellungen bzgl. der Quelltextformatierung verwenden, damit es bei Änderungen keine Unterschiede aufgrund von Formatierungsoptionen gibt.

Elementare Datentypen

Für bestimmte wichtige Daten sind in Java sog. elementare Datentypen (auch primitive Datentypen genannt) definiert. Die wichtigsten sind:

Als Dezimaltrennzeichen wird nicht ein Komma, sondern ein Punkt verwendet. Bei langen Zahlen vom Datentyp Long ist es sinnvoll, ein "L" als Suffix zu verwenden.

Den Wertebereich der numerischen Datentypen kann man mittels der Konstanten MINVALUE und MAXVALUE herausfinden.

Beispiel:

System.out.println("short kann Werte von " + Short.MIN_VALUE + " bis " + Short.MAX_VALUE + " aufnehmen.");
System.out.println("int kann Werte von " + Integer.MIN_VALUE + " bis " + Integer.MAX_VALUE + " aufnehmen.");
System.out.println("long kann Werte von " + Long.MIN_VALUE + " bis " + Long.MAX_VALUE + " aufnehmen.");
System.out.println("float kann Werte von " + Float.MIN_VALUE + " bis " + Float.MAX_VALUE + " aufnehmen.");
System.out.println("double kann Werte von " + Double.MIN_VALUE + " bis " + Double.MAX_VALUE + " aufnehmen.");

Mit dem char '\n' kann ein Zeilenumbruch verursacht werden, mit dem char '\t' ein Tab-Stop.

Datentyp Byte Wertebereich
Byte 1 -128 bis 127
Short 2 -32.768 bis 32.767
Integer 4 -2.147.483.648 bis 2.147.483.647
Long 8 -18.446.744.073.709.551.616 bis 18.446.744.073.709.551.615

Um Zahlen aus anderen Zahlsystemen sichtbar zu machen, können die Präfixe "0b" für binäre, "0" für oktale und "0x" für hexadezimale Zahlen verwendet werden. Intern werden alle Zahlen jedoch dezimal gespeichert.

Arten von Klammern

{} curly braces () parenthesis <> angle brackets [] brackets

Camel Case

Der Camel Case ist eine Konvention zur Benennung von Variablen, Methoden und Methodenparametern. Für sie ist kennzeichnend, dass das erste Zeichen eines Bezeichners grundsätzlich klein geschrieben wird. Innerhalb des Bezeichners werden Groß- und Kleinschreibung gemischt verwendet, wobei das erste Zeichen jedes den Bezeichner konstituierenden Wortes groß geschrieben wird.

Beispiele: userName deleteUser createBankAccount logoutAndShutdown

Variablen

Die wichtigsten Elemente der meisten Programmiersprachen sind die sog. Variablen. Variablen sind benannte Speicherbereiche, die Daten aufnehmen können. Sie werden deklariert (Datentyp und Name werden festlegen) und initialisiert (Wert wird zugewiesen). Der Inhalt einer Variablen kann dann über ihren Namen ausgelesen werden.

Beispiel:

int zahl = 1;
System.out.println("zahl hat den Wert: " + zahl);

In Java müssen Variablen initialisiert werden, bevor sie wie im Beispiel verwendet werden können. Dieses Beispiel würde daher einen Fehler erzeugen:

int zahl;
System.out.println(zahl);

Die Bezeichner (Namen) der Variablen dürfen beliebig lang sein und Buchstaben, Zahlen und bestimmte Sonderzeichen (z.B. -, _, $) enthalten. Sie dürfen jedoch nicht mit einer Zahl beginnen. Obwohl es möglich ist, rate ich von der Verwendung von Umlauten und anderen Sonderzeichen wie ß in Java-Bezeichnern dringend ab. Zum besseren Verständnis sollte man sich auf eine Sprache einigen. Wir arbeiten grundsätzlich mit Englisch. Laut Konvention werden Variablen-Namen in Java kleingeschrieben und dann in der sog. CamelCase-Notation fortgesetzt: int diesIstEineGrosseZahl; Bestimmte Zählvariablen haben meist nur einen einzelnen Buchstaben als Namen. Ansonsten sollten unbedingt sprechende (=lange) Namen verwendet werden, da dies die Programme leichter lesbar und verständlicher macht.

Da Java eine statisch typisierte Programmiersprache ist, muss allen Variablen ein Datentyp zugewiesen werden. Dieser Datentyp ist im Nachhinein nicht mehr änderbar. Folgendes Beispiel würde daher zu einem Fehler führen:

int zahl = 1;
zahl = "text";

Gültigkeitsbereich von Variablen (Scope)

Variablen haben einen sogenannten Gültigkeitsbereich, den Scope. Sie sind nur innerhalb des Blocks verfügbar, in dem sie definiert wurden, und in allen Blöcken innerhalb dieses Blocks. Ein Block ist ein Codebereich, der mit geschweiften Klammern umschlossen ist. Man kann in seinem Code beliebige Blöcke definieren und einige Sprachkonstrukte (wie z.B. class, if und for) benötigen auch explizit einen Block.

Beispiel:

int test = 1;
{
    int test2 = 2;
    // 1
    {
        int test3 = 3;
        // 2
    }
    // 3
}
// 4

Konstanten

Es gibt Variablen, die sich während des Programmablaufs verändern können. Im Gegensatz dazu stehen die Konstanten, deren zugewiesener Wert, nicht im Programmablauf verändert werden kann. Dies bewirkt, dass zentrale Werte nicht überschrieben werden können, und die Konstante in anderen Programmteilen widerverwendet werden kann.

Laut Konvention werden Bezeichner von Konstanten in Java in Großbuchstaben und mit Unterstrichen getrennt geschrieben. Damit eine verändere Variable zu einer Konstante wird, ist der Modifizierer final notwendig. Ein Schreibzugriff auf eine Konstante, führt zu einer Fehlermeldung.

Beispiel:

 public static final int MWST_SATZ = 19;

Casts

Mit Hilfe von Casts kann man Datentypen in andere Datentypen umwandeln.

int number1 = 123;
double decimal1 = number1; // impliziter Cast ohne Wertverlust 
decimal1 = 123.45;
int number2 = (int) decimal1; // expliziter Cast mit Wertverlust

Des Weiteren ist das Casten von Character zu Integer und umgekehrt möglich.

int asciiPositionOfA = 'A';
System.out.println(asciiPositionOfA);
char characterAtPosition65 = (char) 65;
System.out.println(characterAtPosition65);

Um Werte in Strings zu konvertieren, besitzt jeder Datentype die Methode toString(). Dazu muss man jedoch die Klassen der Datentypen verwenden. Die elementaren Datentypen wie int, double und boolean besitzen keine Klasse. Es gibt jedoch für jeden elementaren Datentyp auch eine enstprechende Klasse (Integer, String, Double, Boolean usw.).

Integer int1 = 20;
String numberStr = int1.toString();
System.out.println(numberStr + 3); // -> 203
 
String numberToParse = "20.50";
double parsedNumber = Double.parseDouble(numberToParse);
System.out.println(parsedNumber + 3); // -> 23.5

Arrays

Ein Array ist eine Liste mit mehreren Elementen eines bestimmten Datentyps. Der Datentyp muss angegeben werden sowie auch die Länge, die später nicht mehr geändert werden kann. Eine Array muss wie folgt angelegt werden, wichtig ist dabei das Schlüsselwort new:

Datentyp[] Bezeichner = new Datentyp[Länge des Arrays];

Der Vorteil von Arrays ist, dass mehrere Werte einfach verarbeitet werden können (z.B. mit einer for-Schleife), wobei Redundanzen im Code (z.B. var1, var2, var3 etc.) vermieden werden.

Ausdrücke und Anweisungen

Die sog. Ausdrücke verknüpfen Literale (wie Zahlen oder Zeichenketten), Variablen oder Konstanten mit Operatoren (z.B. +, * usw.) und liefern dabei meist einen Wert zurück.

Beispiel:

int sum = x + y + 10;

Das gesamte Konstrukt aus dem Beispiel nennt man Anweisung. Alle Anweisungen in Java müssen mit einem Semikolon abgeschlossen werden.

Grundlegende Arithmetik

Die grundlegenden Operationen der Mathematik können in Java sehr einfach verwendet werden:

int result = 1 + 5 * 3 - 4 / 2;
double result2 = (1 + 5) * (3 - 4) / 2

Man muss jedoch auf den korrekten Datentyp achten, da ansonsten ggfs. Wertverluste auftreten (insb. bei der Division).

double result = 1 / 2; // -> 0
result = 1.0 / 2; // -> 0.5

Eine wichtige Operation ist die Division mit Rest, der Modulo:

int remainder = 10 % 4; // -> 2

Weitere wichtige mathematische Werte und Funktionen - wie das Wurzelziehen, die Potenzrechnung oder die Zahl Pi - sind im Package Math definiert:

System.out.println(Math.sqrt(9)); // -> 3
System.out.println(Math.PI); // -> 3.14...
System.out.println(Math.pow(2, 10)); // -> 1024

Zusätzlich sind eigene Operatoren für das Inkrementieren und Dekrementieren von Ganzzahlen verfügbar:

int x = 5;
System.out.println(x++); // Post-Increment -> x wird erst nach dem Ausgeben erhöht
System.out.println(++x); // Pre-Increment -> x wird vor dem Ausgeben erhöht
System.out.println(x--); // Post-Decrement -> x wird erst nach dem Ausgeben verringert
System.out.println(--x); // Pre-Decrement -> x wird vor dem Ausgeben verringert

Vergleiche und grundlegende Boolesche Algebra

Es gibt verschiedene Vergleichsoperatoren für logische Vergleiche:

Das = kann nicht zum Vergleichen verwendet werden, da es sich hierbei um den Zuweisungsoperator handelt.

System.out.println(3 < 5); // -> true
System.out.println(3 == 5); // -> false
System.out.println(4 > 8); // -> false

Das Ergebnis eines Vergleichs kann auch in einer Variablen gespeichert werden:

boolean comparison = 3 > 2;
System.out.println(comparison); // -> true

Boolesche Werte können mit AND, OR, XOR und NOT kombiniert werden:

System.out.println("true AND false: " + (true && false)); // -> false
System.out.println("true OR false: " + (true || false)); // -> true
System.out.println("true XOR false: " + (true ^ false)); // -> true
System.out.println("NOT false: " + (!false)); // -> true

Algorithmen

Ein Algorithmus ist eine Handlungsvorschrift (ein "Rezept") zum Lösen eines Problems. Er umfasst eine endliche Anzahl an Handlungsschritten. Alle Algorithmen können aus drei grundlegenden "Bausteinen" zusammengesetzt werden: Sequenz, Verzweigung und Wiederholung.

Sequenz

Ein Folge von mehreren Anweisungen wird als Sequenz bezeichnet.

int ergebnis = 3 + 5;
ergebnis = ergebnis * 8;
ergebnis++;
System.out.println(ergebnis); // -> 65

Verzweigung

Die Verzweigung ist ein grundlegendes Strukturelement für Fallunterscheidungen innerhalb eines Algorithmus. Sie bewirkt die Entscheidung für eine von zwei Möglichkeiten.

if (boolescher Ausdruck)
{
    Anweisung, die ausgeführt wird, wenn der obige Ausdruck ''true'' ist
}
else 
{
    Anweisung, die ausgeführt wird, wenn der obige Ausdruck ''false'' ist
}

Wiederholung

Es gibt drei grundlegende Arten von Wiederholungen (Schleifen).

Die zählergesteuerte Schleife kann verwendet werden, wenn bekannt ist, wie viele Wiederholungen durchgeführt werden sollen.

Aufbau:

for (Startwert; Abbruchbedingung; Aktion) {}

Abfolge einer for-Schleife:

Beispiel:

for (int i=0; i <= 100; i++)
{
    System.out.println(i);
}

Die kopfgesteuerte Schleife sollte verwendet werden, wenn vor dem ersten Durchlauf schon die Bedingung geprüft werden muss.

Aufbau:

while (Abbruchbedingung) {}

Die fußgesteuerte Schleife sollte verwendet werden, wenn mindestens eine Wiederholung durchgeführt werden und die Bedingung erst nach dem Durchlauf geprüft werden soll.

Aufbau:

do { } while (Abbruchbedingung)

Eine Endlosschleife entsteht dann, wenn die Abbruchbedingung nie erfüllt wird und die Schleife daher immer wieder durchlaufen wird.

Anwendung von continue in Schleifen

"Continue" bewirkt, dass die aktuell laufende Wiederholung einer Schleife abgebrochen wird. Es wird zum Ende des Anweisungsblocks gesprungen. Alle nachfolgenden Anweisungen kommen in diesem Schleifendurchlauf nicht mehr zur Ausführung. Die Schleife kann ggf. dennoch weiter durchlaufen werden. Beispiel:

  int i = 5;
		while (i > 0)
		{
			i--;
 
			if (i == 2)
			{
				continue;  // -> die 2 wird bei dem Schleifendurchlauf nicht ausgegeben.
			}
 
			System.out.println(i);
		}

break in Schleifen

Die Anwendung von break veranlasst, im Gegensatz zu continue nicht nur das Abbrechen der aktuellen Schleifenwiederholung, sondern den Komplettabbruch der Schleife.

Methoden (Funktionen)

In Java werden Funktionen Methoden genannt. Sie kapseln Code und können bei Bedarf von verschiedenen Stellen aus aufgerufen werden (Wiederverwendung). Methodenbezeichner können wie die von Variablen frei gewählt werden. Es gelten die gleichen Namenskonvention wie bei Variablen.

Aufbau: Rückgabetyp Name(Parameter) { Implementierung }

Parameter und innerhalb der Methode definierte Variablen sind nur innerhalb dieser Methode gültig (siehe Scope). Die Kommunikation erfolgt über einen (!) Rückgabewert (Schlüsselwort return). Beachte: return verlässt die Methode, d.h. weiterer Code in der Methode ist unerreichbar (der Compiler merkt das auch). Der Rückgabetyp ist nicht immer zwingend ein Wert. Er kann auch leer sein, z.B. bei System.out.println(): Rückgabetyp ist void = kein Rückgabewert. Name + Parameter = Signatur der Methode (z.B. ist System.out.println() mehrfach definiert mit unterschiedlichen Parametern. Welche konkrete Methode ausgeführt wird, entscheiden die Parameter (true, "fdfhdfh", 1 etc.)

Rekursion

Als Rekursion bezeichnet man das Aufrufen einer Methode aus derselben Methode heraus. Beispiele: Fakultätenberechnung (fak(n) = n * fak(n - 1);), Suchen durch ein Dateisystem. Achtung: Rekursion ist sehr teuer, da viel Platz auf dem Stack benutzt wird. Es besteht die Gefahr eines StackOverflows, bei dem der Speicherplatz des Stacks nicht mehr ausreicht. Das Gegenteil ist die Iteration. Man kann alle rekursiven Berechnungen auch iterativ lösen.

Exceptions

Bei der Programmausführung können Fehler auftreten, z.B. wenn durch 0 geteilt wird. Um mit diesen Fehlern umgehen zu können, gibt es die sogenannten Exceptions. In Java werden Exceptions "geworfen", wenn ein Fehler auftritt. Sie können dann im Code "aufgefangen" werden, um sie zu behandeln.

Anweisungen, von denen der Entwickler weiß oder vermutet, dass sie einen Fehler erzeugen könnten, fasst er in try-catch-Blöcke ein. Das Programm durchläuft zunächst den try-Block und springt in den catch-Block, sobald eine Exception auftritt. Wenn keine Exception auftritt, wird der catch-Block nicht durchlaufen. Soll in beiden Fällen, also unabhängig vom Auftreten eines Fehlers, ein weiterer Codeteil durchlaufen werden, wird dieser in den finally-Block hinter dem catch-Block geschrieben.

Es ist dringend davon abzuraten, dass Exceptions durch einen leeren catch-Block "geschluckt" werden. Vielmehr sollte im catch-Block der Fehler behandelt werden oder zumindest eine sinnvolle Fehlermeldung an den Benutzer ausgegeben werden.

Alle Methoden in Java, die im Laufe ihrer Ausführung eine Exception auslösen könnten, müssen dies explizit kenntlich machen. Dies geschieht bei der Definition der Methode durch die Verwendung des Schlüsselwortes throws. Dadurch wird sichergestellt, dass alle Aufrufer dieser Methode immer ein try/catch nutzen müssen, wenn sie die Methode aufrufen wollen.

Der Entwickler selbst kann manuell eine Exception erzeugen, indem er im Code mittels throw eine Exception wirft.

Beispiel:

double divide(int a, int b) throws Exception
{
    if (b == 0)
        throw new Exception("b darf nicht 0 sein.");
    return (double)a / b;
}
void test()
{
    double result;
    try
    {
        double = divide(1, 0);
    }
    catch (Exception e)
    {
        System.out.println("Fehler bei der Berechnung: " + e.getMessage());
        result = 0;
    }
}

Objektorientierung

Was sind Objekte?

Ein Objekt ist ein "Ding" der realen Welt, das in der Software abgebildet werden soll. Ein Objekt kann alles sein: ein Auto, ein Vertrag, eine Tür, etc. Ein Objekt hat Funktionen und Eigenschaften. Eine Funktion eines Autos ist z.B. 'Gas geben' und eine Eigenschaft ist 'Farbe'. Die Funktionen heißen in der OO Methoden und die Eigenschaften nennt man Attribute.

Klasse vs. Objekt

Eine Klasse definiert die abstrakten Charakteristika eines Objekts, z.B. dass alle Hunde vier Beine haben und bellen können. Sie dient als Bauplan/Schablone für Objekte. Ein Objekt ist dann eine konkrete Instanz dieser Klasse, z.B. der Schäferhund mit Namen Rex. In Java werden Klassen geschrieben. Eine Klasse ist gleichzusetzen mit einem Datentyp. Es könnte also eine Klasse Car geben, wobei Car dann der Datentyp wäre. Die Attribute sind Variablen, die direkt unterhalb der Klasse definiert werden (also nicht in einer Methode). Diese Variablen können in allen Methoden der Klasse verwendet werden (siehe Scope). Methoden werden ebenfalls auf Ebene der Klasse definiert und können damit intern verwendet werden, aber auch von außen aufgerufen werden (wie z.B. s.charAt(1);, wobei s dann der Name des Objektes ist).

Beispiel:

class Auto
{
    String farbe;
 
    void gibGas()
    {
        System.out.println("Brumm brumm");
    }
}

Ein Objekt wird erstellt, indem der Konstruktor der Klasse aufgerufen wird. Konstruktoren sind besondere Methoden, deren Name mit dem der Klasse übereinstimmen muss und die keinen Rückgabewert haben. Ist kein expliziter Konstruktor angegeben, erzeugt Java automatisch einen Default-Konstruktor ohne Parameter. Wenn man selbst einen Konstruktor erstellt, wird der Default-Konstruktor nicht erzeugt und es kann nur noch mit dem angegebenen Konstruktor gearbeitet werden. Man kann beliebig oft den Konstruktor einer Klasse aufrufen und dabei wird jedes Mal ein völlig neues Objekt erstellt, mit dem dann gearbeitet wird.

Beispiel:

class Auto
{
    String farbe;
 
    Auto(String farbe)
    {
        this.farbe = farbe;
    }
}

Kapselung

Bei der Objektorientierung soll möglichst alles so implementiert werden, dass einzelne Programmteile gekapselt sind. Dies hat drei Gründe:

  1. Übersichtlichkeit: Der Code bleibt lesbar und ist schneller nachzuvollziehen.
  2. Wiederverwendbarkeit: Die einzelnen Teile können relativ einfach in anderen Programmen wiederverwendet werden.
  3. Sicherheit: Die Code-Teile kommen sich nicht in die Quere und können nicht versehentlich auf bereits vergebene Variablen o.ä. zugreifen.

Sichtbarkeit

Die Sichtbarkeit beschreibt, wer Elemente (Klassen, Variablen und Methoden) nutzen kann. Es gibt drei Sichtbarkeitmodifikatoren:

Selbst Subklassen Welt
private x
protected x x
public x x x

Bei der Implementierung von Klassen ist darauf zu achten, dass Attribute grundsätzlich als private zu kennzeichnen sind! Grund: Andernfalls könnte man Objekten von außen ungültige Werte setzen (z.B. Alter = -1). Um private-Variablen auch außerhalb der Klasse benutzen zu können, werden Getter und Setter verwendet:

Vererbung

Die Vererbung ist eine zentrale Funktion der Objektorientierung und bedeutet, dass Klassen von einer anderen Klasse Methoden und Attribute erben können. Diese Methoden und Attribute werden dadurch wiederverwendbar. Die erbende Klasse heißt Subklasse oder abgeleitete Klasse, die vererbende Klasse heißt Basisklasse.

Das Schlüsselwort extends gibt die Basisklasse zu einer Subklasse an. Hier erbt die Klasse Circle von der Klasse Shape. Mit dem Schlüsselwort super hat man die Möglichkeit den Konstruktor der Basisklasse aufzurufen.

public class Circle(int originX, int originY, int radius) extends Shape
{
    super(originX, originY);
    this.radius = radius;
}

Mit der Annotation @Override werden Methoden gekennzeichnet, die gleichnamige Methoden ihrer Basisklasse überschreiben.

public class A 
{
    public void eineMethode() {}
}
 
public class B extends A 
{
    @Override
    public void eineMethode() {}
}

Durch das Schlüsselwort final kann eine Methode nicht in einer Subklasse überschrieben werden. Das Schlüsselwort abstract definiert eine Methode oder eine ganze Klasse als abstrakt. Abstrakte Klassen können nicht instantiiert werden. Eine abstrakte Methode definiert lediglich ihre Signatur, und eine Subklasse muss diese Methode dann implementieren. Die Klasse ist dann nur für den Kopf der Methode zuständig, während die Implementierung an anderer Stelle erfolgt. Durch abstrakte Methoden wird ausgedrückt, dass die Basisklasse keine Ahnung von der Implementierung hat und dass sich die Unterklassen darum kümmern müssen.

public abstract class MyVehicle
{
    // die Methode getType wird für die Subklassen vorgegeben
    abstract protected String getType();
}
 
public class MyCar extends MyVehicle
{
    // die Subklasse überschreibt die abstrakte Methode und implementiert etwas Konkretes
    @Override
    protected String getType()
    {
        return "car";
    }
}

UML

Die UML (Unified Modeling Language) ist eine grafische Beschreibungssprache, die insb. in der objektorientierten Softwareentwicklung zur Modellierung und Dokumentation eingesetzt wird.

Es gibt in Version 2.0 13 verschiedene Diagrammtypen. Die wichtigsten sind:

Klassendiagramm

Ein Klassendiagramm ist ein Strukturdiagramm zur Modellierung von Klassen, Schnittstellen und deren Beziehungen. Ein Klassendiagramm zeigt also, in welcher Beziehung Klassen zueinander stehen (z.B. Basisklasse und Subklassen). Zudem gibt das Diagramm die Attribute und Methoden der Klassen an.

Beispieldiagramm: http://upload.wikimedia.org/wikipedia/commons/0/03/Klassendiagramm-1.png

Automatische Tests

In Java gibt es die Möglichkeit, den geschriebenen Code durch einen Testcode testen zu lassen. Dazu wird meist das Framework 'JUnit' verwendet. JUnit ist ein Framework für Unit-Tests und stellt die nötigen Funktionen zum Testen von Code bereit (insb. die sogenannten Assertions). Ein JUnit-Test sieht wie folgt aus:

@Before // wird vor JEDEM Test ausgeführt und sorgt dafür, dass das Testobjekt in seinen Initialzustand versetzt wird
public void setup()
{
    sut = new ObjectUnderTest();
}
@Test // alle Methoden, die als Test markiert sind, führt JUnit automatisch aus
public void test
{
   assertThat(sut.calculate(), is(1));
}

Die zentrale Aufgabe beim Schreiben von Tests ist, sich zu überlegen, welche Fälle getestet werden sollen/müssen. Beispiel: int add(int z1, int z2) → zu testen sind mindestens alle Kombinationen aus [positiven Zahlen, negativen Zahlen, Null, Integer.MAX, Integer.MIN, MAX+1, MIN-1, +1, -1] Das ergibt mindestens 81 Kombinationen, damit die Methode erschöpfend getestet ist. Das ist in der Praxis so nicht durchführbar. Die Menge an Testfällen muss also der Situation angepasst werden. So wird ein Raketeningenieur in einem überlebenswichtigen System sicherlich mehr Tests für die add-Methode implementieren als Informatikstudenten in einer Übungsaufgabe. Eine interessante Vorgehensweise beim Testen ist das Test Driven Development: Man schreibt zuerst einen Test und erst danach den Code, der den Test erfüllt. Dabei kann man sich von Eclipse den zu testenden Code generieren lassen, um sich Schreibarbeit zu sparen. Wichtig: Jeder neu geschriebene Test muss am Anfang fehlschlagen, um sicherzugehen, dass der Test nicht immer positiv ist!

Wie finde ich Testfälle?

  1. Anfangen mit dem einfachsten Fall, zB. leerer String, 0, etc.
  2. Danach mit dem nächst schwierigeren Testfall weitermachen, z.B. String bestehend aus einem Buchstaben usw.
  3. Grenzfälle sollten unbedingt getestet werden (z.B. -1, +1, 0, MIN, MAX, MIN - 1, MAX + 1, NULL usw.)

Programmierprinzipien und -tipps

Refactoring

In der Programmierung ist es wichtig, in Hinsicht auf zukünftige und noch nicht vorhersehbare Änderungen am Programm, den Code so variabel wie möglich zu erstellen, sodass er sich in Zukunft ohne großen Aufwand abändern lässt. In diesem Zusammenhang führt man häufig das sog. Refactoring durch, eine semantikinvariante Modifikation des Quelltexts. Refactoring ist somit eine manuelle oder automatisierte Strukturverbesserung von Programm-Quelltexten unter Beibehaltung des beobachtbaren Programmverhaltens. Dabei sollen die Lesbarkeit, Verständlichkeit, Wartbarkeit und Erweiterbarkeit verbessert werden, mit dem Ziel, den jeweiligen Aufwand für Fehleranalyse und funktionale Erweiterungen deutlich zu senken. Eine zentrale Voraussetzung für ein "sicheres" Refactoring ohne das Verhalten des Codes zu ändern sind automatische Tests.

Extract Method

Eclipse bietet die Möglichkeit, durch Markieren eines Codeabschnittes diesen automatisch in eine separate Methode auszulagern. Dies funktioniert durch Markieren des gewünschten Abschnittes mit der rechten Maustaste und Auswahl von Refactor > Extract Method. Es öffnet sich ein Fenster, in dem ein Name für die neue Methode eingegeben werden kann. Die Variablen, die in der Methode Verwendung finden, erkennt Eclipse selbstständig. Durch bestätigen des Vorgangs implementiert Eclipse im Code selbstständig eine neue Methode.

Debugging

Eclipse stellt uns im System einen Debugger zur Verfügung. Dies ist ein Tool, mit dem der Code Schritt für Schritt ausgeführt werden kann, um ihn nach Fehlern zu durchsuchen. Um in Eclipse den Debugger nutzen zu können, muss zunächst ein Breakpoint im Code gesetzt werden. Ein Breakpoint ist ein Haltepunkt im Code, bei dessen Erreichen im Programmablauf das Programm angehalten wird, sodass die nächsten Schritte einzeln nachvollzogen werden können. Ein Breakpoint wird in Eclipse gesetzt, indem mit der rechten Maustaste am Seitenrand "Toggle Breakpoint" ausgewählt wird. Um zu sehen was passiert, muss die Perspektive auf "Debug" geändert werden. In dieser Perspektive werden zusätzliche Views bereitgestellt, die z.B. die aktuell verwendeten Variablen und ihren jeweiligen Inhalt anzeigen.

Mögliche Klausurfragen

aus \cite{Lau2011b}

aus \cite{Krypczyk2011c}