TU Wien:Einführung in die Programmierung 2 VU (Puntigam)/Zusammenfassung

Aus VoWi
Zur Navigation springen Zur Suche springen

Datenabstraktion[Bearbeiten]

Datenkapselung
Zusammenfassen von Methoden und Variablen zu Einheit
Data-Hiding
Verstecken vor Zugriffen von außen
class Foo {
    int x;               // Objektvariable
    static int y;        // Klassenvariable
    void foo(){};        // Objektmethode
    static void bar(){}; // Klassenmethode
}

Foo f = new Foo(); instanziert ein neues Objekt der Klasse Foo, die Objektvariable x wird auf 0 vorinitialisiert und der default constructor wird aufgerufen. Wir speichern eine Referenz auf unsere neue Instanz in der Variable f und können danach (sofern es das scope erlaubt) auf f.x und f.foo() zugreifen.

Objektvariablen existieren in jeder Instanz. Klassenvariablen hingegen nur einmal. Objektmethoden können nur von einem Objekt aufgerufen werden. Klassenmethoden können von überall aufgerufen werden (sofern es das scope erlaubt) und sind unabhängig von Instanzen (innerhalb von bar() kann man beispielsweise nicht auf x oder foo() zugreifen).

Innerhalb von Objektmethoden/konstruktoren referenziert das Schlüsselwort this das momentane Objekt selbst.

Queue, Stack, Deque, Map[Bearbeiten]

Queue
(FIFO): add, poll, peek, size
Stack
(LIFO): push, pop, peek, size
Double-ended queue
(FIFO, LIFO): addFirst, pollFirst, peekFirst, addLast, pollLast, peekLast, size
Assoziative Datenstruktur
put(k,v), get(k), remove(k), containsKey(k), containsValue(v), size()

Rekursive Datenstrukturen[Bearbeiten]

Eine Klasse bzw. Datenstruktur ist rekursiv, wenn ein Objekt eine Referenz auf ein anderes Objekt enthalten kann. Solche Methoden, Klassen und Datenstrukturen sind induktiv aufgebaut. Am Beginn steht eine leere Datenstruktur (Fundierung), zu der nach und nach Knoten hinzugefügt werden.

Singly linked list
A → B → C
Doubly linked list
A ↔ B ↔ C
Binary Tree
left ← node → right

Vererbung und Schnittstellen[Bearbeiten]

Klassen enthalten Implementierungen. Interfaces hingegen spezifizieren eine Schnittstelle. Wenn eine Klasse eine andere Klasse erweitert, erbt sie alles von der Superklasse. Wenn eine Klasse eine Schnittstelle implementiert, muss sie sämtliche im Interface spezifizierten Objektmethoden implementieren. In Java kann eine Klasse nur von einer anderen Klasse erben aber mehrere Schnittstellen implementieren.

deklarierter Typ
Vom Compiler ermittelt, bestimmt welche Methoden aufrufbar sind.
dynamischer Typ
Erst zur Laufzeit bekannt, bestimmt in welcher Klasse eine Methode ausgeführt wird. z.B. Unterklasse oder Implementierung des deklarierten Typs
statisches Binden
kein dynamisches Binden beim Methodenaufruf wenn
  • die Methode static, private, oder final ist
  • der deklarierte Typ keine Untertypen hat
  • der Compiler feststellen kann, wo die Methode auszuführen ist

java.lang.Object[Bearbeiten]

In Java ist jede Klasse ein Untertyp von Object und erbt daher deren Objektmethoden, u.a. folgende:

  • String toString()
  • boolean equals(Object obj){ return this == obj; }
  • int hashCode()
  • Class getClass() liefert dynamischen Typ
Typische Implementierung von equals
public boolean equals(Object obj) {
    if (this == obj) return true;// Optimierung, garantiert x.equals(x)
    if (obj == null || getClass() != obj.getClass()) {
        return false; // dyn. Typen verschieden, garantiert !x.equals(null)
    }
    NameOfThisClass that = (NameOfThisClass) obj; // access through that
    ...// type-specific comparison of this and that
}
int hashCode() Bedingungen
  • x.equals(y) → x.hashCode() == y.hashCode()
  • x unverändert → x.hashCode() == x.hashCode()
Wenn equals überschrieben wird, sollte auch hashCode überschrieben werden.
Kann einfach mit return Objects.hash(Object... objects); implementiert werden.

In IntelliJ können equals() und hashCode() mit Rechtsklick → Generate generiert werden.

Iterable und Iterator[Bearbeiten]

Iterable und Iterator sind zwei Interfaces, welche ein einheitliches Iterieren über Objekte, unabhängig von der unterliegenden Datenstruktur ermöglichen. Iterieren verändert die unterliegenden Daten nicht.

  • Iterierbare Klassen implementieren Iterable<E> und müssen daher die Iterator<E> iterator() Methode implementieren, welche einen neuen Iterator zurückgibt.
  • Iterator-Klassen implementieren Iterator<E>, was folgende Methoden vorschreibt:
    • boolean hasNext() gibt an ob der Iterator ein weiteres Element hat
    • E next() gibt das nächste Element zurück oder wirft eine NoSuchElementException, falls es kein weiteres Element gibt

Die in <> übergeben Klasse ist ein sogenanntes Generic und bestimmt den Rückgabetyp von next().

public class MyLinkedList implements Iterable<MyLinkedList.Node> {
    // ...
    @Override
    public Iterator<Node> iterator() {
        return new MyIterator();
    }

	class MyIterator implements Iterator<Node> {
            Node current = head;

            @Override
            public boolean hasNext() {
                return current != null;
            }

            @Override
            public Node next() {
                if (current == null) throw new NoSuchElementException();
                Node returned = current;
                current = current.next;
                return returned;
            }
        };
	}
    // ...
}

Über MyLinkedList myList = new MyLinkedList(); könnte man wie folgt iterieren:

for (Iterator<Node> i = myList.iterator(); i.hasNext();) {
    Node n = i.next();
    // verwende n
}

Da dies allerdings mühselig und fehleranfällig ist, bietet Java die For-Each-Schleife, welche das Gleiche macht:

for (Node n : myList){
    // verwende n
}

Weil man die Iterator-Klassen generell nur in iterator() benötigt, bietet es sich an sie als anonyme Klassen zu definieren:

public class MyLinkedList implements Iterable<MyLinkedList.Node> {
    // ...
    @Override
    public Iterator<Node> iterator() {
        return new Iterator() {
            Node current = head;

            @Override
            public boolean hasNext() {
                return current != null;
            }

            @Override
            public Node next() {
                if (current == null) throw new NoSuchElementException();
                Node returned = current;
                current = current.next;
                return returned;
            }
        };
    }
    // ...
}

Exceptions[Bearbeiten]

throw new IllegalArgumentException();
try {
    FileReader reader = new FileReader("test.txt");
} catch (FileNotFoundException e) {
    e.printStackTrace();
}

Design by contract[Bearbeiten]

  • Vorbedingung: muss vor Methodenausführung erfüllt sein
  • Nachbedingung: muss nach Methodenausführung erfüllt sein
  • Invariante: Bedingung in Bezug auf Objektzustand
  • Schleifeninvariante: beschreibt, was sich in den Iterationen nicht ändert

Testen[Bearbeiten]

  • Black-Box-Tests: unabhängig von Implementierung
  • White-Box-Tests: vom Implementierung abgeleitet
  • Grey-Box-Tests: Testfälle vor Implementierung als Spezifikation festgelegt

Weitere Tests:

  • Code-Review
  • Sicherheitstest (Pentest)
  • Belastungstest (Stresstest, Crashtest)
  • Benutzeroberflächentest

Hilfsmittel zum Finden von Fehlerursachen:

  • Code-Review
  • Debugger
  • Log-Dateien, Traces
  • Einbeziehen von Entwicklern, Experten, Nutzern

Mehrstufiges Testen:

  1. Modul- bzw. Komponententest
  2. Schnittstellentest
  3. Integrationstest
  4. Systemtest (auch Abnahmetest)

Qualität und Optimierung[Bearbeiten]

  • brauchbar
  • zuverlässig und sicher
  • effizient
  • wartbar

Maßnahmen:

  • Qualitätsbewusstsein
  • Anforderungsanalyse
  • ständige Kontrolle
  • Besprechungen
  • Analyse-Werkzeuge
  • formale Beweise
  • aufeinander Verlassen
  • Qualitätsmanagement