Benutzer-Werkzeuge

Webseiten-Werkzeuge


phwt:skriptjava



Skript zur Vorlesung Grundlagen der Informatik I (Java)

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)
  • class-Definition fehlt
  • public vor main() fehlt
  • Println muss println lauten
  • die Anführungszeichen um die Texte fehlen
  • fehlende Semikolons am Zeilenende
  • fehlende Klammern }

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:

  • short, int und long für Ganzzahlen
  • float und double für Gleitkommazahlen
  • char für einzelne Zeichen
  • bool für Wahrheitswerte

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
  • 1: test und test2 sind verwendbar
  • 2: test, test2 und test3 sind verwendbar
  • 3: test und test2 sind verwendbar
  • 4: test ist verwendbar

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
  • Beim Casten von Integer-Variablen zu Double gibt es keine Probleme, da der Wertebereich von Double größer ist als der von Integer.
  • Beim Casten von Double zu Integer wird der Dezimalwert nach dem Komma "abgeschnitten".

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;
  • Literale: 10
  • Variablen: x, y
  • Operatoren: = (Zuweisung), + (Addition)
  • Ausdruck: x + y + 10 (Rückgabewert ist die Summe von x, y und 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:

  • == (gleich)
  • < (kleiner)
  • > (größer)
  • (kleiner gleich)
  • >= (größer gleich)
  • != (ungleich)

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
  • die kopfgesteuerte Schleife
  • die fußgesteuerte Schleife

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:

  • Deklaration und Initialisierung der Laufvariablen, bzw. Zuwei-sung eines Startwertes falls Variable ausserhalb der Schleife deklariert wurde
  • Start der Schleife (nicht Ausführung des Codes im Anweisungsblock der Schleife)
  • Prüfung der Abbruchbedingung:
    • Bedingung wahr? Ausführung des Anweisungsblocks
    • Bedingung unwahr? Ende der Schleife, Fortsetzung des Programms
  • Manipulation der Laufvariablen im Aktionsteil
  • Sprung zur Prüfung der Abbruchbedingung ( 3. )

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:

  • private - Das Element ist nur innerhalb der aktuellen Klasse sichtbar.
  • public - Das Element ist für alle sichtbar.
  • protected - Das Element ist innerhalb der aktuellen Klasse und in abgeleiteten Klassen sichtbar.
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:

  • Getter: Methode, die nur die Variable zurückgibt: car.getColor()
  • Setter: Methode, die es ermöglicht, die Variable zu ändern: car.setColor("green");

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
  • Sequenzdiagramm
  • Verteilungsdiagramm
  • Aktivitätsdiagramme
  • Zustandsautomaten

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

  • DRY (Don't repeat yourself): Doppelter Code soll vermieden werden, da bei Anpassungen sonst mehrere Stellen aktualisiert werden müssen, was sehr fehleranfällig ist (z.B. wenn eine Stelle übersehen wird).
  • Es gibt nicht nur eine richtige Lösung, sondern immer mehrere Lösungsmöglichkeiten. Es gibt zwar Richtlinien, aber jeder Entwickler hat seinen eigenen Stil und somit gibt es verschiedene Herangehensweisen und Lösungen.
  • Aufgaben sollen nicht stumpf (z.B. anhand einer vorgegebenen Aufgabenstellung) abgearbeitet werden. Vielmehr muss stets untersucht werden, ob es eine einfachere Lösung gibt! (Beispiel Quadratzahlen → nicht mit den Quadraten anfangen und die Wurzeln ziehen, sondern die Zahlen quadrieren)
  • Single Responsibility Principle (SRP): Dieses Prinzip besagt, dass jede Klasse/Methode/Variable etc. nur eine fest definierte Aufgabe erfüllen soll. Dies soll u.a. helfen bei anfallenden Fehlern die Fehlerquelle schneller bestimmen zu können und macht Anpassungen einfacher umsetzbar.
  • Vorgehen beim Programmieren (divide and conquer)
    • aktuelle abstrakte Aufgabe in nächst kleinere/einfachere (konkretere) Teilschritte runterbrechen (= divide)
    • wenn Teilschritt implementierbar, "übersetzen" in Quelltext: Es ist in der Regel leichter, Lösungen (= conquer) für diese kleinen Aufgaben zu finden und diese später zusammenzufügen, als das Eingangsproblem auf Anhieb zu lösen.
    • Am Beispiel FizzBuzz kann man das Aufteilen der Aufgabe so angehen:
      1. 1. Zählen von 1 - 100
      2. Für alle Zahlen teilbar durch 5 und 3 setze FizzBuzz
      3. Für alle Zahlen teilbar durch 5 setze Buzz
      4. Für alle Zahlen teilbar durch 3 setze Fizz
      5. Redundanzen (z.B. 15 = FizzBuzz, Buzz und Fizz) entfernen
      6. Zwischen jedem dieser Schritte prüfen, ob das Teilergebnis ein Fortschritt ist.
  • YAGNI (You ain't gonna need it): Was nicht unbedingt gebraucht wird, wird auch nicht implementiert. Ansonsten handelt man sich ggfs. unnötige Komplexität ein, die spätere Anpassungen erschweren.

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}

  • Was ist eine Programmiersprache?
    • Eine Programmiersprache ist eine formale Sprache mit der ein Programm geschrieben werden kann. Dabei werden dem Computer Anweisungen übergeben, die verarbeitet werden. Der Code kann in Maschinensprache oder in Form von Quelltext geschrieben werden, der dann in Maschinensprache übersetzt wird. Die Bausteine einer Programmiersprache sind das Vokabular (z.B. System.out.println(), die Grammatik (z.B. Verschachtelung von Blöcken) und die Syntax (z.B. Semikolons am Zeilenende).
  • Was ist Assembler?
    • Ein Assembler ist ein Programm, dass maschinennahe Assemblersprache in Maschinensprache übersetzt. Die Assemblersprache wird von der CPU benutzt und mit der Assemblersprache kann direkt auf Ressourcen wie die CPU bzw. auf deren Speicher oder auf die Grafikkarte zugegrifen werden.
  • Was ist eine Hochsprache?
    • Eine Hochsprache ist eine Programmiersprache, die der menschlichen Sprache entfernt ähnelt. Die Hochsprache kann der Mensch verstehen und sie ist oft an den Denkgewohnheiten des Menschen angepasst und ist dabei zum größten Teil maschinenunabhängig.
  • Was ist ein Parser?
    • Ein Parser Überprüft die Syntax einer Eingabe auf Richtigkeit anhand syntaktischer Regeln.
  • Was ist ein Compiler und ein Interpreter?
    • Eine Compiler-Sprache wird vor der Ausführung in Maschinencode (bzw. Bytecode bei Java) übersetzt. Zur Übersetzung wird der Compiler benötigt.
    • Eine Interpreter-Sprache wird zur Laufzeit durch einen Interpreter Befehl für Befehl übersetzt und direkt ausgeführt.
  • Was ist ein JIT-Compiler?
    • Der Just-In-Time Compiler kompiliert ähnlich wie bei der Interpreter-Sprache zur Laufzeit. Jedoch werden die kompilierten Daten im Cache abgelegt, sodass diese nicht erneut übersetzt werden müssen wie beim Interpreter.
      * Was ist Bytecode?
    • In manchen Programmiersprachen wird nicht direkt in Maschinensprache übersetzt, sondern in einen Zwischencode, der Bytecode genannt wird. Dieser ist oftmals plattformunabhängig.
  • Was heißt "statisch typisiert" und "dynamisch typisiert"?
    • Bei der statischen Typisierung wird der Datentyp von Variablen bereits während der Kompilierung festgelegt. Bei der dynamischen Typisierung wird kein spezieller Datentyp definiert. Der Typ ergibt sich aus dem ihr zugewiesenen Wert und kann sich zur Laufzeit ändern.
  • Was heißt "stark typisiert"?
    • Bei starker Typisierung können Datentypen nicht verändert (siehe Cast) werden. Durch die starke Typisierung sollen Fehler zur Laufzeit verhindert werden. Hier ist es wichtig, dass die möglichen Eingabewerte so eingeschränkt werden, dass kein Fehler entstehen kann.
  • Was sind mögliche Vorteile von statischer Typisierung?
    • Fehler können bereits bei der Übersetzung gefunden werden und nicht erst zur Laufzeit. Die Eingabewerte müssen richtig sein. Die Performance wird gesteigert, da zur Laufzeit keine Überprüfung des Datentyps mehr erforderlich ist.
  • Was sind mögliche Vorteile von dynamischer Typisierung?
    • Ist einfacher für den Programmierer, da die einzelnen Methoden nicht für jeden Datentyp definiert werden müssen.
  • Was ist strukturierte und prozedurale Programmierung?
    • Bei der strukturierten Programmierung beschränkt man sich in unterster Ebene auf drei Kontrollstrukturen (siehe Algorithmus). Bei der prozeduralen Programmierung werden zusätzlich eigene Funktionen und Prozeduren definiert. Dabei geben Funktionen einen Rückgabewert zurück und Prozeduren geben keinen Wert zurück.
  • Was bedeutet "deterministisch"?
    • Deterministisch bedeutet, dass ein Computer ein Programm immer gleich ausführt und das grundsätzlich das gleiche Ergebnis zu erwarten ist.
  • Was ist ein Register?
    • Ein Speicherbereich der direkt mit der CPU verbunden ist und auf den die CPU zugreifen kann. Über diesen werden Berechnungen durchgeführt.
  • Was ist der Stack?
    • Ein Stapel der bei jedem Funktionsaufruf aufgebaut wird. Dabei wird nach dem LiFo-Prizip gearbeitet, sodass ein neuer Funktionsaufruf immer oben auf den Stapel gelegt wird. Nach der Abarbeitung, wird der nächste Aufruf des Stapel bearbeitet.
  • Was sind Stack und Heap?
    • Der Stack ist ein Stapel, der nach dem LiFo-Prinzip abgebaut wird. Der Heap ist der Großteil des Arbeitsspeichers, in dem die Daten zur Laufzeit verarbeitet werden.
  • Was ist ein Pointer/Zeiger?
    • Ein Pointer zeigt auf eine Adresse im Adressspeicher.

aus \cite{Krypczyk2011c}

  • Was ist eine Bibliothek?
    • Eine Sammlung an Funktionen, die innerhalb einer Programmiersprache verwendet werden können (z.B. Math in Java).
  • Was ist eine DLL?
    • DLL = Dynamic Link Library; Bezeichnet eine Bibliothek, die überlicherweise bei Microsoft Anwendung findet.
  • Welche Arten von Fehlern gibt es bei der Programmierung von Algorithmen?
    • Eingabefehler, Verfahrensfehler, Fortpflanzungsfehler, Rechenfehler
  • Wie geht man beim Entwickeln von Algorithmen vor?
    • Der Algorithmus muss in kleine Teilschritte zerlegt werden. Diese Teilschritte können dann nach und nach niedergeschrieben werden.
phwt/skriptjava.txt · Zuletzt geändert: 2015-01-04 19:12 (Externe Bearbeitung)