Einführung in die Informatik
Teil 1: Einführung
bro so was soll ich dazu überhaupt schreiben idk
Code ist eine Anreihung von Anweisungen dings
Teil 2: Objektbasierte Programmierung
Kapselung
Abstraktionsebene zwischen Dingen außerhalb und innerhalb von Klasse (z.B. Getter und Setter) erleichtert Wartung und Modifikationen am gekapselten Stuff.
Primitivity
| Primitive | Non-Primitive | |
|---|---|---|
| Beispiele | int, boolean |
String, Array, jede custom class |
| Wert | direkt die Daten selbst | eine referenz auf die Adresse mit den Daten |
| Als Methodenparameter | Methode erhält Kopie des Werts, kann den "äußeren" Wert nicht ändern | Methode erhält Kopie der Referenz (kann also die Daten dennoch ändern (⚠️ aber nicht bei reassignment! und nicht bei String, da dieser immutable ist). |
Modulo-Operator
Hier gibt es 1 wichtiges Detail, das relevant werden kann:
| in Mathe | in Java |
|---|---|
13 % 6 == 1 |
13 % 6 == 1 |
-13 % 6 == 5 (in Restklasse) |
-13 % 6 == -1 |
In Java hat das Ergebnis von a % b immer dasselbe Vorzeichen wie a (der Dividend). In der Mathematik ist der Rest hingegen meistens so definiert, dass er immer ≥0 ist. |
Vergleichsoperatoren
&und|prüfen immer rechts und links&&bricht nachfalsefür links schon ab||bricht nachtruefür links schon ab
-> Wenn links und rechts Methoden, die einen Zustand verändern, stehen macht das einen Unterschied!
Pre-/Post-Increment/Decrement
a++/a-- |
Wert von a gelesen und im Execution Stack gespeichert, dann wird a in Memory in-/dekrementiert. |
|---|---|
++a/--a |
a wird in-/dekrementiert, dann wird der Wert gelesen. |
Natürlich verwöhnt uns Big P hier wieder mit dem EIDI-Classic:
"Having to understand code that would instantly get you fired at a real job"
Da beim Assignment a=a++ der Wert von a zuerst im Execution Stack gespeichert wird, und die anschließende In-/Dekrementierung in Memory gespeichert wird, wird beim anschließenden Reassignment von a noch der alte Wert vor der Änderung aus dem Execution Stack verwendet. Die Anweisung a=a++ ändert also nicht den Wert von a. (Keine Sorge du musst nicht wissen was ein Execution Stack ist.)
Decorators
Access Modifier
| Modifier | Klasse | #Package | Unterklasse | Welt (Global) |
|---|---|---|---|---|
public |
✅ | ✅ | ✅ | ✅ |
protected |
✅ | ✅ | ✅ | ❌ |
| (kein Keyword) | ✅ | ✅ | ❌ | ❌ |
private |
✅ | ❌ | ❌ | ❌ |
Other Keywords
| decorator | function |
|---|---|
abstract |
- Methoden: Deklarierung ohne Implementierung - Klassen: nicht instanziierbar, also es können keine objekte von ihnen erstellt werden (see also: Interfaces) - Alle Klassen, die mindestens eine abstract Methode haben, müssen abstract sein |
final |
- Variablen - primitives: Konstanter Wert (kann nicht in runtime geändert werden) - non-primitives: nur der Wert, also die Referenz ist final, nicht die Daten auf die sie zeigt- Klassen: kann nicht extended werden |
static |
- Gehört nicht dem jeweiligen Objekt, sondern der Klasse an sich - Kann ohne bestimmte Referenz auf oder Existenz eines Objekts verwendet werden - Useful für Konstanten (mit static) oder id (static int count=0; int id=count++;[1]) |
default |
Nur in #Interfaces möglich, entfernt sozusagen das hier automatisch anwesende abstract, sodass wir eine Implementierung für die Methode angeben können. |
Package
- Zur Gruppierung verwandter Klassen
- Alle Klassen in einem File sind in einem Package
- Für Package mit mehreren Files: Ordner
myPackagemachen und die Files darin mitpackage myPackage;versehen
Call-by-Value (Referenzen)
Bei Non-Primitive Methodenargumenten übergibt Java Kopien der Referenz. Modifizierung des Objekts (z.B. arr[0]=1) ändert das Original-Objekt im Heap. Reassignment der Variable auf (neues) Objekt (z.B. arr = new int[1]) ändert nur die lokale Kopie der Referenz und hat außen keine Auswirkung.
Teil 3: Testen
Die Idee
Die Idee (bei Unit-Tests, eigentlich gibts auch noch andere wie z.B. Implementation Tests aber nicht Stoff) ist folgende:
Wir zerhacken unseren Code in die minimalen Bestandteile, idealerweise Funktionen ohne Side-Effects (nur Input und Output) und schreiben für jede Tests, bei denen wir Inputs providen und einen bestimmten Output dafür erwarten. So wissen wir direkt, wo genau der Fehler liegt. Wir schreiben sowohl Tests für das Base-Case (erwartbare Inputs), als auch für Edge Cases (weniger erwartbare Inputs). Eine gute Weise die Test-Cases zu strukturieren sind sogenannte Testklassen, bei denen man mit einer Reihe von Fallunterscheidungen alle möglichen Kombinationen und so alle Situationen abdeckt.
Meistens geht das mit dem Zerhacken nicht so schön, weil viele Methoden in der echten Welt inheränt Side-Effects haben (außer man ist auf Hascell-FP-Trip). Ich muss z.B. auf Arbeit oft Unit Tests schreiben für Methoden die auf ne DB zugreifen. Oder die Methode ist abgesehen von den Parametern noch von anderem Stuff abhängig, wie z.B. Datum. Hier braucht man dann Mocking, wodurch man versucht, möglichst nur die Methode im Test zu testen und alles andere zu imitieren und damit abzuschotten.
Syntax
@Test // Test annotation
public void testForBitFlip(){
int a = 6;
int b = 7;
int c = a+b;
assertEquals(c,13); // assert-Keyword (there's a bunch)
}
Dann vergleichst du nur deren Referenzen du Dulli. Mach einfach asssertEquals() du Dulli.
Teil 4: Kontrollstrukturen
broo free, außer maybe die hier:
Ternary Operator
a = b ? c : d
ist äquivalent zu
if (b)
{
a=c;
}
else
{
a=d;
}
Switch-Case
-Statement
switch (a)
{
case 1:
System.out.println("eins");
break;
case 2:
System.out.println("zwei");
break;
case 3: case 4:
System.out.println("drei oder vier");
break;
default:
System.out.println("out of bounds");
}
Ohne die break Statements, würde danach einfach das nächste Case getestet werden, quasi als wären es einfach lauter ifs (und default eine normale Anweisung). Das wäre hier z.B. ein Problem, wenn statt blablab() der Wert von a auf 2 gesetzt wird.
-Expression
Das ganze geht auch fancier, und zwar wenn es keine Anweisung, sondern ein Ausdruck sein soll:
System.out.println(
switch (a)
{
case 1 -> "eins";
case 2 -> "zwei";
case 3,4 -> "drei oder vier";
default -> "out of bounds";
}
)
Dann braucht man wegen dem -> auch keine breaks.
do-while
Der Böse Couseng von while, voll praktisch manchmal:
do
{
userInput = getUserInputOrSmth();
}
while (userInput.isInvalid());
Die Schleife wird halt immer mindestens 1x ausgeführt, bevor die Condition überhaupt gecheckt wird.
Enums
Nützlich, wenn es eine begrenze Anzahl an Belegungen geben soll für Variable.
enum Beziehungsstatus
{
LEDIG, LIIERT, VERHEIRATET, VERWITWET
}
Beziehungsstatus b = Beziehungsstatus.VERHEIRATET;
Beziehungsstatus[] alleOptionen = Beziehungsstatus.values()
Kontrollflussdiagramm
Beispiel:
flowchart TD
S([Start]) --> IN[/"Eingabe: x = read();"/]
IN --> A["Zuweisung/Anweisung: sum = 0; i = 0;"]
%% ---------- Schleife (while) ----------
A --> W{"while-Bedingung: i < n ?"}
W -- "yes" --> BODY["Schleifenrumpf: sum = sum + a[i];"]
BODY --> INC["Anweisung: i = i + 1;"]
INC --> W
W -- "no" --> AFTER_LOOP((Nach der Schleife))
%% ---------- switch-case (Mehrfachselektion) ----------
AFTER_LOOP --> SW{"switch(command)"}
SW -- "case 'n'" --> CNEW["createNewFile();"]
SW -- "case 'o'" --> COPEN["openFile();"]
SW -- "default" --> CDEF[/"Ausgabe: Unbekanntes Kommando"/]
Pfeile zu sich selbst
S -- "extends (≤1)" --> S
IN -- "extends (n)" --> INPolymorphie
Statischer vs. Dynamischer Typ
StaticType o = new DynamicType()
Der statische Typ bestimmt die sichtbaren Methodensignaturen für den Compiler; der dynamische Typ bestimmt die ausgeführte Implementierung der JVM.
Methodensignatur
Name (nicht Return-Type), Parametertypen und Reihenfolge (nicht Namen)
aka public static int method(String hallo, int hallo)
Generics & Bounds
Bei A<U extends C> ist die Konstruktion new A<C>() erlaubt, da für die Typprüfung und Generics jede Klasse als Subtyp ihrer selbst betrachtet wird.
Casting
oldStaticType object = new dynamicType();
newStaticType object = (newStaticType) object;
Erstellt ein neues Objekt mit dem alten dynamischen und dem in Klammern angegebenen neuen statischen Typ.
Wenn neuer statischer Typ in der Vererbungshierarchie unter dem dynamischen liegt, gibts ClassCastException.
Ein Upcast (von extender auf extendee) schränkt die Sichtbarkeit auf Methoden ein, die im statischen Zieltyp deklariert sind.
Implizit vs Explizit
Entlang der Vererbungsrichtung (in Pfeilrichtung) castet Java implizit aka automatisch (da keine Daten verloren gehen).
Andersrum muss man explizit casten.
Das gilt auch bei primitiven Typen.
!Polymorphie, p.1
Und welche scheiß Methode wird jetzt aufgerufen?
!Polymorphie, p.2
In other words: Such bei statischem Typ (oder wenn nichts gibt bei dessen Oberklassen) die Methode raus, die am besten passt. Dann nimmt die in der Vererbungshierarchie möglichst nah am dynamischen Typen liegende Implementierung dieser Methode. (Und zwar mit exakt der selben Signatur!)
Best Fitting Methode
Zunächst werden alle Methoden ausgesucht, deren #Methodensignatur auf den Call passt (auch wenn dafür implizite Parametercasts notwendig sind).
Für jeden supplied Parameter wird geschaut, welche dieser Methoden diesen mit den wenigsten impliziten Casts annehmen kann. Gibt es eine Methode, die für alle Parameter der beste Fit ist, wird sie gewählt, sonst ein Ambiguity Error geworfen.
#Teil 8: Rekursion
<rant>
du wirst das nie in deinem Job brauchen
Die wichtigsten Anwendungen für Rekursion finden sich bei Giga-Low-Level Zeug wie Linked Lists oder #Merge Sort. Also Zeug das schlaue Leute vor 30 Jahren schon für dich gemacht haben! Du wirst auf Arbeit nie ein Ticket assigined bekommen "bitte bau einen custom sortalg" dikkahhh
</rant>
Aufbau
- Base Case (Abbruchbedingung)
- Rekursiver Aufruf
- Logik
- Return
Backtracking
Die Idee is basically, dass du nicht immer alle rekursiven Branches betrachten musst. Wenn du z.B. nach etwas suchst, kannst du als return-value ne boolean nehmen, die der Methode Bescheid sagt, ob ihre rekursiven Aufrufe was gefunden haben. So kann bei Fund sofort das Programm beendet werden.
End-Rekursion
Ah btw, wenn möglich schreibt eure Rekursion so, dass sie der Compiler einfach als Iteration umformen kann 😭😭😭
Wenn der Rekursive Aufruf nach der Rechnungslogik der Funktion steht, muss beim Erreichen von ihm nicht die Return-Adresse modifiziert werden, da wir nicht mehr an diese Stelle returnen müssen (es gibt ja nach dem Rekursiven Aufruf nichts mehr zu tun). Deshalb können wir die einfach die ganze Zeit an der Stelle lassen, wo die Funktion das erste Mal aufgerufen wurde. beim Erreichen der Abbruchbedingung springen wir dann direkt ganz raus (Tail Call). So sparen wir uns ne Menge Platz im Stack weil sonst müssten wir ja pro Rekursivem Aufruf eine Rücksprungadresse und Werte für die Berechnung später reinschreiben. Stattdessen machen wir die Berechnung jetzt einfach vor dem Aufruf/im Parameter.
static int factorialRecursive(int n)
{
// 1 Base-Case
if (n <= 1)
return 1;
// 2 Rekursiver Aufruf
int preResult = factorial(n - 1); //Hierhin muss zurückgesprungen werden
// 3 Calculation
return n * preResult; // Erst jetzt beim Weg heraus wird auf preResult draufmultipliziert
}
vs.
static int factorialTailRecursive(int n, int preResult)
{
// 1 Base-Case
if (n <= 0)
return preResult;
// 2(!!) Calculation
preResult = preResult * n; // preResult wird schon beim Weg hinein draufmultipliziert
// 3(!!) Rekursiver Aufruf
return factTR(n-1, preResult);
}
but wait... das würde in Assembly dann etwa so aussehen
START_METHOD:
# 1 Base-Case
COMPARE n, 0
JUMP_IF_LESS_EQUAL FINISH
# 2 Calculation
MULTIPLY preResult, preResult, n
# 3 Rekursiver Aufruf
SUBTRACT n, n, 1
JUMP START_METHOD
FINISH:
RETURN preResult
sieht das nicht ein bisschen aus wie...
static int factorialLoop(int n, int preResult) {
// 1 Base-Case
while (n > 0) {
// 2 Calculation
preResult = n * preResult;
// 3 """Rekursiver Aufruf"""
n = n - 1;
// (jump passiert automatisch)
}
return preResult;
}
...
Divide and Conquer
Die Idee
Probleme solange zerhacken bis sie trivial sind (wünschte das ginge auch irl 🚬)
Beispiele
Binary Search
Die Idee des Algorithmus ist wie folgt (unter der Annahme, dass die Menge in aufsteigender Reihenfolge sortiert ist):
- Starte mit der gesamten Liste.
- Erstelle einen Zeiger für die linke Grenze.
- Erstelle einen Zeiger für die rechte Grenze.
- Solange die linke Grenze kleiner oder gleich der rechten Grenze ist, teile die Menge:
- Berechne den mittleren Index.
- Falls das mittlere Element größer als das Ziel ist:
- Wähle die linke Hälfte.
- Setze die rechte Grenze auf mittlerer Index - 1 (da wir das mittlere Element bereits geprüft haben).
- Sonst falls das mittlere Element kleiner als das Ziel ist:
- Wähle die rechte Hälfte.
- Setze die linke Grenze auf mittlerer Index + 1 (gleiche Logik).
- Sonst:
- Das mittlere Element ist das gesuchte Element; gib den Index/das Ziel zurück.
- Gib -1/null zurück (Ziel-Element konnte nicht gefunden werden).
Merge Sort
Teil 9: Fortgeschrittene Programmierkonstrukte
Iteration
Iterableimplementieren
->iterator()bereitstelleniterator()returnt ein Objekt einer Klasse, dieIteratorimplementiert
-> muss MethodenhasNext()&next()bereitstellen, sagen ob es next gibt bzw geben zurück- Um das dann zu nutzen, kann man entweder tatsächlich die
iterator()Methode aufrufen oder einfach diesen Syntax zum iterieren verwenden:is = new MyIteratable; for(MyIteratable i : is){ System.out.println(i); } - Implementierung eines solchen
Iterators für einen Baum nutzt am besten einen Stack, der die noch zu traversierenden Teile des Baums stored.
Lambda Expressions
| aus: | kann man machen: | Erklärung |
|---|---|---|
dtyp methode(dtyp param, ...){ stuff } |
(dtyp[2]param, ...) -> {stuff [..] return wert} |
kein methodenname nötig, aber dafür pfeil |
(...) { return wert } |
(...) -> wert |
nur 1 anweisung: direkt return |
dtyp methode(dtyp param){...} |
param -> {...} |
nur 1 parameter |
:: Methode einer Klasse/static Objekt auswählen |
Berühmte Interfaces (aus java.util.function)
| Interface | Funktion | Erklärung / Logik |
|---|---|---|
Supplier<T> |
T get() |
Liefert einen Wert vom Typ T. |
Consumer<T> |
void accept(T a) |
Nimmt einen Wert T an und macht "shit" damit, ohne etwas zurückzugeben. |
Function<T, R> |
R apply(T a) |
Nimmt T an und gibt ein Resultat R zurück. |
Predicate<T> |
boolean test(T a) |
Prüft eine Bedingung und gibt true oder false zurück. |
Comparator<T> |
int compare(T a, T b) |
Vergleicht zwei Objekte:a > b -> return > 0a == b -> return 0a < b -> return < 0 |
Streams
Erstellen
Stream.of(n,m,...)aus festen WertenStream.generate(insertSupplierName)aus Supplier (infinite amount of elements!)StreamSupport.stream(insertIterableName.spliterator(), false)aus IterablesArrays.stream(insertArrayName)aus ArrayinsertCollectionName.stream()aus Collections (Listen, Sets, ...)
Stream-Methoden
Stream kann nur eine terminale Operation bekommen, aber erst durch diese werden die intermediate Operationen überhaupt erst ausgeführt!
Intermediate
| Methode | Erklärung |
|---|---|
map() |
nimmt Funktion T -> T macht shit auf jedem Element des streams und gibt neuen zurück (Function). Datentyp ändern mit #Casting oder auch z.B. mapToInt() bei supported Typen (dann kriegt man auf Datentyp ausgerichteten Stream mit extra Methoden wie avg()). Braucht terminale Option zum funktionieren. |
flatMap() |
nimmt Funktion T -> Stream<T>, aber tut den Inhalt jedes Streams hintereinander in den neuen Stream so können pro Wert >1 neue Werte entstehen. |
filter() |
filtered nach Predicate. |
takeWhile() |
nimmt Werte weiter solange Predicate zutrifft, dann stopp. |
limit(n) |
nimmt die ersten n Elemente. |
distinct() |
self explanatory. |
peek() |
wie foreach() aber consumed nicht -> braucht terminale Option zum funktionieren. |
sorted() |
wenn Elemente nicht Comparable sind braucht es Comparator, es gibt standard aber man kann auch seinen eigenen machen. |
parallel() |
parallele Bearbeitung, Reihennfolge nicht vorhersehbar! |
Terminal
| Methode | Erklärung |
|---|---|
foreach() |
macht generell shit ohne return, aka nimmt einen Consumer |
collect() |
in Collection speichern (z.B. Collectors.toList()). |
toArray() |
kann Referenz für Konstruktor der Klasse nehmen. |
count() |
self explanatory. |
findAny() |
beliebiges Element finden (als Optional). |
max/min(comparator) |
findet das Maximum oder Minimum basierend auf dem Comparator. |
reduce(startWert,acc) |
reduziert auf einen Wert (using accumulator, erst Startwert mit Element 1, dann das Ergebnis mit Element 2, ...). |
Um foreach()-Style Operationen vorzunehmen (z.B. Attribute der enthaltenen Objekte modifizieren) ohne den Stream zu terminieren (weil man danach noch z.B. sortieren will), kann man stattdessen Operationen wie map() oder peek() nutzen. Diese werden aber logischerweise nur ausgeführt, wenn der Stream später noch terminiert wird.
Speichereffizienz
Streams sind lazy und verarbeiten Daten "on-the-fly", was sie speichereffizienter macht als persistente Collections im RAM.
I/O
Pathist ein Interface- du kannst Objekt von einer implementierenden Klasse erstellen mit
Path.of("x/y/z") - man kann auch von einem File Object den
Pathkriegen mittoPath() Path.resolve(String/Path other)ermöglicht das Verketten von PfadenPath.relativize(Path other)entfernt gleiche Elemente, wir wandeln den Pfad im Parameter um, sodass er relativ zum ersten Pfad ist (erst bei der Position vom ersten Pfad startet).
- du kannst Objekt von einer implementierenden Klasse erstellen mit
- zeigt ein
Pathauf eine Datei,- dann kann man mit
Files.readString()undFiles.writeString(String s)self explanatory shit machen - oder mit
Files.write(Iterable i)Iterables und so reinschreiben - oder mit
Files.readAllLines(Path p)Lines als Liste lesen - oder mit
Files.lines(Path p)als Stream - oder mit
Files.createFile(Path p)die Datei überhaupt erst erzeugen
- dann kann man mit
- zeigt ein Path auf ein Directory
- dann gibts
Files.walk(Path p), was einen gefüllten Stream ausgibt - oder mit
Files.createDirectory(Path dir)das Directory überhaupt erst erzeugenFiles.createDirectories(Path dir)erzeugt auch alle, die auf dem Weg benötigt werden, ersteres failed da
- dann gibts
Files.move(Path source, Path target)bewegt/renamed eine Datei/Folder
Oft muss bei diesen Operationen eine IOException gecatched werden
Buffers
Zwischenlager zwischen Produzent und Konsument im Allgemeinen. Bei #I/O nützlich da wir die Datei nicht Byte für Byte lesen müssen.
Output: 01001101 01100101 01110010 01101100 01101001 01101110
| Ohne Buffer | Mit Buffer |
|---|---|
0-> hm ok, interessant |
01001101-> M |
1-> aha crazy... |
01100101-> E |
0-> wow, wieder ne Null? |
01110010-> R |
0-> ohaa noch eine? |
01101100-> L |
1-> fuck jetzt wieder ne eins? |
01101001-> I |
1-> damnn noch eine? |
01101110 -> N |
Socket
Können Verbindung zwischen mehreren Rechnern herstellen. Dann ist ein Datentransfer via InputStream und OutputStream möglich.
Um eine Verbindung zu einem anderen Rechner aufzubauen, benötigt man dessen Hostname (IP-Adresse oder Domain) und den Port (0–65535). Beim Instanziieren mit new Socket(host, port) wird die Verbindung automatisch aufgebaut.
getInputStream(): Liefert den Stream, um Daten empfangen (hinein in das Programm).getOutputStream(): Liefert den Stream, um Daten zu senden (heraus aus dem Programm).
Reader & Writer
| Typ | Klasse | Funktion |
|---|---|---|
| Basic | InputStreamReader / OutputStreamWriter |
Konvertiert Bytes in char / String und umgekehrt. |
| Effizient | BufferedReader / BufferedWriter |
Nutzt #Buffers, um Datenblöcke statt einzelner Zeichen zu verarbeiten. Erlaubt readLine() oder .lines(). |
| Komfort | PrintWriter |
Kann beliebige Objekte via toString() direkt in den Stream schreiben (print(), println()). |
Server Sockets
Ein Server "spricht" nicht von sich aus, sondern "lauscht".
- Instanziierung:
ServerSocket server = new ServerSocket(port); - Warten:
Socket client = server.accept();- Diese Methode ist blockierend: Der Thread bleibt stehen, bis sich ein Client verbindet.
accept()gibt dann ein normalesSocket-Objekt für die Kommunikation mit diesem Client zurück.
.close() geschlossen werden, damit der Port wieder freigegeben wird und das Programm nicht beim Beenden blockiert.Error Handling
Netzwerkoperationen sind fehleranfällig. Fast alle Methoden werfen eine IOException.
UnknownHostException: IP/Domain nicht gefunden.ConnectException: Verbindung abgelehnt (z.B. falscher Port oder Server offline).- Beide sind Unterklassen der
IOException, eincatch-Block reicht also oft aus.
Exceptions/Errors
classDiagram
class Throwable {
<>
}
class Error {
+OutOfMemoryError
+StackOverflowError
}
class Exception {
<>
+IOException
+SQLException
}
class RuntimeException {
<>
+NullPointerException
+ArithmeticException
+IndexOutOfBoundsException
}
Throwable <|-- Error
Throwable <|-- Exception
Exception <|-- RuntimeException Checked bedeutet, dass man sie entweder catchen oder mit throws nach oben weitergeben muss.
Alle RuntimeException sind unchecked, alle anderen Exceptions checked. Dann gibt es auch noch Errors, die kann man nicht abfangen weil danach wallah Krise ist (z.B. StackOverflowError).
Bei try{} catch(Exception e){} finally{} ist die Funktion von finally, dass es fast immer ausgeführt wird, egal ob eine ungegangene Exception geworfen wurde oder ob im try/catch ein return steht.
Sinn: Aufräumarbeiten (Files schließen, Sockets beenden), damit die nicht offen bleiben, wenn es knallt.
Die Ausnahme: wenn System.exit(0) gerufen wird oder die JVM komplett abschmiert (Stromausfall type shit)
Teil 10: Nebenläufigkeit
Threads
Thread vs Runnable
Runnable
- etwas, das wir machen
- aka Liste von Anweisungen
- plainstes Interface, das keine Parameter nimmt und nichts zurückgibt
Thread
kann Runnable übergeben bekommen, um zu festlegen was er tun soll
Verwendung von Threads
start() benutzen, nie run()Wartende Methoden
wait(Object o)wartet darauf, dass auf dem Parameteronotify()aufgerufen wird.- Dafür nötig:
- Handling für
InterruptedException(try/catchoder mitthrowsnach oben geben) - Ownership des Objekts
oviasynchronized(o)
- Handling für
- Optional kann man ein Timeout passen, nachdem dann auch das Warten endet
- Dafür nötig:
join(Thread t)lässt den ausführenden Thread warten bis der Thread im Parameter fertig ist.sleep()wartet die angegebene Zeit
Unterbrechende Methoden
o.notify()weckt einen zufälligen Thread auf, der aufowarteto.notifyAll()weckt alle Threads auf, die aufowarteninterrupt()setzt bei einem Threadinterruptedauf true, wenn der Thread grade wartet, wird eineInterruptedExceptiongeworfen
Keywords
synchronized(Object o)markiert einen kritischen Bereich, in dem nur ein Zugriff auf einmal erlaubt ist- nimmt als Parameter die Ressource, mit der der Zugriff verbunden ist
- alle anderen Bereiche, die auf diese Ressource synchronisiert sind, sind dann geblockt
- Gibt es kein solches Objekt können wir einfach ein Dummy Objekt erstellen (sog. Lock) und das verwenden
- alternativ als Methoden-Decorator möglich, das ist äquivalent zum wrappen des gesamten Methodeninhalts in
synchronized(this)
- nimmt als Parameter die Ressource, mit der der Zugriff verbunden ist
volatileistsynchronizedlight (Race Conditions passieren noch, aber kein Caching sondern es wird direkt mit RAM geschrieben und gelesen, siehe Mesi Protokoll aus ERA)
Producer-Consumer-Problem
Ein klassisches Problem der Nebenläufigkeit:
2 Prozesse, die mit einem Pufferspeicher gekoppelt sind. Einer produziert etwas, einer konsumiert es, aber sie arbeiten nicht gleichschnell. Das kann zu Problemen führen, wenn der Puffer voll ist und der Producer weiter producen will oder der Buffer leer ist und der Consumer weiter consumen will.
Typische Problem-/Lösungsfolge
⬇️
Datenverfälschung durch zeitgleichen Zugriff
(z.B. Beide lesen erst ob ein Sitzplatz noch buchbar ist und reservieren ihn dann.)
⬇️
synchronized
Blockierung von Zugriff auf Ressource, wenn schon auf sie zugegriffen wird
(z.B. Bei Klick auf Sitzplatz wird nicht nur gecheckt ob er schon reserviert ist, sondern auch ob grade jemand schon im Begriff ihn zu buchen ist, dann ggf warten auf Freigabe)
⬇️
Mindestens zwei nebenläufige Prozesse befinden sich in zyklischer Wartesituation auf exklusiv und ununterbrechbar von einem anderen Prozess belegte, gemeinsame Ressourcen.

⬇️
Semaphor, Lock, etc.
Semamphore(n) mit n als Anzahl Permit
acquire()nimmt 1 Permit, wartet falls 0 istrelease()gibt 1 Permit zurück
Teil 11: GUIs
GUIs sind Graphical User Interfaces. Eine Anwendung besteht typischerweise aus einem Frame (Hauptfenster) und verschiedenen Swing-Komponenten (z.B. „ein Knopf“ und weitere einfache Bedienelemente), die über Ereignisse (z.B. Klicks) gesteuert werden. Für die Anordnung der Komponenten sind Layout-Konzepte zentral (behandelt als Layout I und Layout II), und als größere Anwendung wird die Kombination von GUIs mit Netzwerkprogrammierung thematisiert.
Teil 12: Compiler
Compiler-Phasen
1) Scanner (lexikalische Analyse)
Aufgabe: Quelltext wird in Tokens zerlegt. Identifiziert u.a. reservierte Wörter, Namen, Konstanten
Ignoriert: White Space und Kommentare Technik: Vergleich mit regulären Ausdrücken, daraus werden „Wörter“ (= Tokens).
2) Parser (Syntaxanalyse)
Aufgabe: Analysiert die Struktur des Programms.
Ergebnis: Erzeugt einen Syntaxbaum und prüft, ob die Syntax korrekt ist.
3) Semantische Analyse
Aufgabe: Prüft Kontextbedingungen, z.B. ob Variablen vor ihrer Verwendung deklariert wurden.
4) (Zwischen-)Codegenerierung
Aufgabe: Aus (annotiertem) Baum wird (Zwischen-)Code erzeugt.
5) Optimierung
Aufgabe: Der erzeugte Code wird abschließend optimiert.
Bytecode
Was steht „drin“? (Instruktionen)
| Befehl | Parameter | Bedeutung | ||||||
|---|---|---|---|---|---|---|---|---|
NEG |
int negieren (Vorzeichenwechsel) |
|||||||
ADD |
zwei int addieren |
|||||||
SUB |
zwei int subtrahieren |
|||||||
MUL |
zwei int multiplizieren |
|||||||
DIV |
zwei int dividieren |
|||||||
NOT |
boolean negieren |
|||||||
AND |
logisches UND | |||||||
OR |
logisches ODER | |||||||
LESS |
Vergleich < (liefert boolean) |
|||||||
LEQ |
Vergleich <= (liefert boolean) |
|||||||
EQ |
Vergleich == (liefert boolean) |
|||||||
NEQ |
Vergleich != (liefert boolean) |
|||||||
CONST i |
i (int) |
Konstante i auf den Stack legen |
||||||
TRUE |
true auf den Stack legen |
|||||||
FALSE |
false auf den Stack legen |
|||||||
LOAD i |
i (Index/Adresse) |
Wert aus Speicherzelle i laden (auf den Stack) |
||||||
STORE i |
i (Index/Adresse) |
obersten Stack-Wert in Speicherzelle i speichern |
||||||
JUMP i |
i (Ziel) |
unbedingter Sprung zu Instruktion/Label i |
||||||
FJUMP i |
i (Ziel) |
„False-Jump“: springt zu i, wenn Bedingung `false t |
||||||
READ |
Eingabe lesen (Wert auf den Stack) | |||||||
WRITE |
obersten Stack-Wert ausgeben | |||||||
ALLOC i |
i (Anzahl) |
Speicher für i Variablen/Speicherzellen reservieren |
||||||
HALT |
Programm beenden |
Übersetzung (High-Level → Bytecode)
Komplexere Konstrukte (z.B. Ausdrücke, if, while) werden in eine Sequenz solcher einfachen Instruktionen übersetzt; Sprünge sind dabei zentral für Kontrollstrukturen.
Bezug zu Funktionen
Funktionsaufrufe werden im Bytecode/VM-Kontext typischerweise über Stacks und eine passende Laufzeitorganisation (z.B. Aktivierungsdaten pro Aufruf) realisiert.
EBNF
Die Idee
EBNF ist eine formale Notation, um die Syntax einer Sprachepräzise zu beschreiben (welche Zeichenketten/Programme wohlgeformt sind).
Grundbausteine
- Terminale: „konkrete“ Symbole der Sprache (z.B.
"a","+","if","("). - Nichtterminale: Namen für syntaktische Kategorien (z.B.
Expr,Stmt,A). - Startsymbol: Das Nichtterminal, von dem aus die Beschreibung der ganzen Sprache beginnt.
Regeln (Produktionen)
- Schreibweise typischerweise:
Nichtterminal ::= Ausdruck
- Bedeutung: Das Nichtterminal auf der linken Seite darf/ soll durch den rechten Ausdruck ersetzt/abgeleitet werden.
Wichtige EBNF-Konstrukte (Operatoren)
- Konkatenation:
X Ybedeutet „X gefolgt von Y“. - Alternative:
X | Ybedeutet „entweder X oder Y“. - Option:
[ X ]bedeutet „X ist optional (0 oder 1 Mal)“. - Wiederholung (0 oder mehr):
{ X } - Wiederholung (1 oder mehr): oft als
X { X }(oder je nach Variante auch{ X }+). - Gruppierung:
( ... )zur Klammerung/Strukturierung.
Ableitung / Sprache einer Grammatik
- Durch wiederholtes Anwenden der Regeln wird aus dem Startsymbol eine Zeichenkette aus nur Terminalen erzeugt.
- Die Menge aller so erzeugbaren Terminalketten ist die Sprache der Grammatik.
Mini-Beispiel
- Sprache
: A ::= ( "a" A "b" ) | ε- Idee: Jedes Anwenden der Regel fügt außen ein
"a"und ein"b"hinzu;εbeendet.