Projekt: Bank-App
Baue eine vollständige Bank-Anwendung in Java mit Vererbung, Interfaces, Exception Handling, Collections und OOP-Design.
Projekt: Bank-App
In diesem Projekt baust du eine Bank-Anwendung mit mehreren Kontotypen, Ueberweisungen und Transaktionshistorie. Du wirst Vererbung, Interfaces, Collections und fortgeschrittene OOP-Konzepte in einem realen Szenario anwenden.
Was wir bauen
Unsere Bank-App unterstuetzt:
- Girokonto mit Ueberziehungsrahmen (Dispo)
- Sparkonto mit Zinsen
- Ueberweisungen zwischen Konten
- Transaktionshistorie fuer jedes Konto
- Kundenmanagement
- Interaktive Konsolensteuerung
Schritt 1: Transaktions-Record
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public record Transaktion(
String typ,
double betrag,
double kontostandNachher,
String beschreibung,
LocalDateTime zeitpunkt
) {
private static final DateTimeFormatter FORMAT =
DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm");
@Override
public String toString() {
var vorzeichen = betrag >= 0 ? "+" : "";
return "[%s] %s%s%.2f EUR | Stand: %.2f EUR | %s".formatted(
zeitpunkt.format(FORMAT), typ.length() < 12 ?
typ + " ".repeat(12 - typ.length()) : typ,
vorzeichen, betrag, kontostandNachher, beschreibung);
}
}
Schritt 2: Eigene Exceptions
public class BankException extends RuntimeException {
public BankException(String message) { super(message); }
}
public class KontoNichtGefundenException extends BankException {
public KontoNichtGefundenException(String kontoNr) {
super("Konto nicht gefunden: " + kontoNr);
}
}
public class KontostandException extends BankException {
private final double kontostand;
private final double betrag;
public KontostandException(double kontostand, double betrag) {
super("Nicht genug Guthaben: %.2f EUR vorhanden, %.2f EUR angefordert"
.formatted(kontostand, betrag));
this.kontostand = kontostand;
this.betrag = betrag;
}
public double getKontostand() { return kontostand; }
public double getBetrag() { return betrag; }
}
Schritt 3: Das Konto-Interface
public interface Konto {
String getKontoNr();
String getInhaber();
double getKontostand();
void einzahlen(double betrag);
void abheben(double betrag);
java.util.List<Transaktion> getTransaktionen();
String getKontoTyp();
}
Schritt 4: Abstrakte Basisklasse
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
public abstract class BasisKonto implements Konto {
private final String kontoNr;
private final String inhaber;
protected double kontostand;
private final List<Transaktion> transaktionen;
protected BasisKonto(String kontoNr, String inhaber, double startguthaben) {
if (kontoNr == null || kontoNr.isBlank()) {
throw new IllegalArgumentException("Kontonummer darf nicht leer sein!");
}
if (inhaber == null || inhaber.isBlank()) {
throw new IllegalArgumentException("Inhaber darf nicht leer sein!");
}
if (startguthaben < 0) {
throw new IllegalArgumentException("Startguthaben darf nicht negativ sein!");
}
this.kontoNr = kontoNr;
this.inhaber = inhaber;
this.kontostand = startguthaben;
this.transaktionen = new ArrayList<>();
if (startguthaben > 0) {
transaktionHinzufuegen("Eroeffnung", startguthaben, "Kontoeroeffnung");
}
}
@Override
public String getKontoNr() { return kontoNr; }
@Override
public String getInhaber() { return inhaber; }
@Override
public double getKontostand() { return kontostand; }
@Override
public void einzahlen(double betrag) {
if (betrag <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein!");
}
kontostand += betrag;
transaktionHinzufuegen("Einzahlung", betrag, "Bareinzahlung");
}
@Override
public List<Transaktion> getTransaktionen() {
return List.copyOf(transaktionen);
}
protected void transaktionHinzufuegen(String typ, double betrag, String beschreibung) {
transaktionen.add(new Transaktion(
typ, betrag, kontostand, beschreibung, LocalDateTime.now()));
}
@Override
public String toString() {
return "%s | %s | %s | Kontostand: %.2f EUR".formatted(
getKontoTyp(), kontoNr, inhaber, kontostand);
}
}
Schritt 5: Girokonto
public class Girokonto extends BasisKonto {
private final double dispoLimit;
public Girokonto(String kontoNr, String inhaber, double startguthaben, double dispoLimit) {
super(kontoNr, inhaber, startguthaben);
this.dispoLimit = dispoLimit;
}
public Girokonto(String kontoNr, String inhaber, double startguthaben) {
this(kontoNr, inhaber, startguthaben, 1000.0); // Standard-Dispo: 1000 EUR
}
@Override
public void abheben(double betrag) {
if (betrag <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein!");
}
if (kontostand - betrag < -dispoLimit) {
throw new KontostandException(kontostand, betrag);
}
kontostand -= betrag;
transaktionHinzufuegen("Abhebung", -betrag, "Barauszahlung");
}
public double getVerfuegbar() {
return kontostand + dispoLimit;
}
public double getDispoLimit() { return dispoLimit; }
@Override
public String getKontoTyp() { return "Girokonto"; }
}
Schritt 6: Sparkonto
public class Sparkonto extends BasisKonto {
private final double zinssatz; // in Prozent
public Sparkonto(String kontoNr, String inhaber, double startguthaben, double zinssatz) {
super(kontoNr, inhaber, startguthaben);
this.zinssatz = zinssatz;
}
public Sparkonto(String kontoNr, String inhaber, double startguthaben) {
this(kontoNr, inhaber, startguthaben, 2.5); // 2.5% Standard-Zinssatz
}
@Override
public void abheben(double betrag) {
if (betrag <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein!");
}
if (betrag > kontostand) {
throw new KontostandException(kontostand, betrag);
}
kontostand -= betrag;
transaktionHinzufuegen("Abhebung", -betrag, "Auszahlung vom Sparkonto");
}
public void zinsenBerechnen() {
var zinsen = kontostand * (zinssatz / 100.0);
if (zinsen > 0) {
kontostand += zinsen;
transaktionHinzufuegen("Zinsen", zinsen,
"Jahreszinsen (%.1f%%)".formatted(zinssatz));
}
}
public double getZinssatz() { return zinssatz; }
@Override
public String getKontoTyp() { return "Sparkonto"; }
}
Schritt 7: Die Bank-Klasse
import java.util.*;
public class Bank {
private final String name;
private final Map<String, Konto> konten;
private int kontoZaehler;
public Bank(String name) {
this.name = name;
this.konten = new HashMap<>();
this.kontoZaehler = 1000;
}
public String getName() { return name; }
private String naechsteKontoNr() {
kontoZaehler++;
return "DE%06d".formatted(kontoZaehler);
}
public Girokonto girokontoEroeffnen(String inhaber, double startguthaben) {
var kontoNr = naechsteKontoNr();
var konto = new Girokonto(kontoNr, inhaber, startguthaben);
konten.put(kontoNr, konto);
return konto;
}
public Sparkonto sparkontoEroeffnen(String inhaber, double startguthaben) {
var kontoNr = naechsteKontoNr();
var konto = new Sparkonto(kontoNr, inhaber, startguthaben);
konten.put(kontoNr, konto);
return konto;
}
public Konto kontoFinden(String kontoNr) {
var konto = konten.get(kontoNr);
if (konto == null) {
throw new KontoNichtGefundenException(kontoNr);
}
return konto;
}
public void ueberweisen(String vonKontoNr, String nachKontoNr, double betrag) {
if (betrag <= 0) {
throw new IllegalArgumentException("Betrag muss positiv sein!");
}
if (vonKontoNr.equals(nachKontoNr)) {
throw new IllegalArgumentException("Sender und Empfaenger duerfen nicht gleich sein!");
}
var vonKonto = kontoFinden(vonKontoNr);
var nachKonto = kontoFinden(nachKontoNr);
vonKonto.abheben(betrag);
nachKonto.einzahlen(betrag);
// Transaktionen mit Beschreibung aktualisieren
System.out.printf("Ueberweisung: %.2f EUR von %s nach %s%n",
betrag, vonKontoNr, nachKontoNr);
}
public List<Konto> alleKonten() {
return new ArrayList<>(konten.values());
}
public double gesamtEinlagen() {
return konten.values().stream()
.mapToDouble(Konto::getKontostand)
.sum();
}
public int kontoAnzahl() {
return konten.size();
}
}
Schritt 8: Die Konsolen-App
import java.util.Scanner;
public class BankApp {
private final Bank bank;
private final Scanner scanner;
public BankApp() {
this.bank = new Bank("Java-Bank AG");
this.scanner = new Scanner(System.in);
demoKontenErstellen();
}
private void demoKontenErstellen() {
bank.girokontoEroeffnen("Max Mustermann", 2500);
bank.girokontoEroeffnen("Anna Mueller", 5000);
bank.sparkontoEroeffnen("Max Mustermann", 10000);
}
public void starten() {
System.out.println("Willkommen bei der " + bank.getName() + "!");
System.out.println("Tippe 'hilfe' fuer eine Uebersicht der Befehle.\n");
while (true) {
System.out.print("Bank> ");
var eingabe = scanner.nextLine().strip();
if (eingabe.isEmpty()) continue;
try {
if (!verarbeiteEingabe(eingabe)) break;
} catch (BankException e) {
System.out.println("Fehler: " + e.getMessage());
} catch (IllegalArgumentException e) {
System.out.println("Ungueltige Eingabe: " + e.getMessage());
}
}
System.out.println("Auf Wiedersehen!");
scanner.close();
}
private boolean verarbeiteEingabe(String eingabe) {
return switch (eingabe.toLowerCase().split("\\s+")[0]) {
case "hilfe", "help" -> { zeigeHilfe(); yield true; }
case "konten" -> { zeigeKonten(); yield true; }
case "info" -> { zeigeKontoInfo(eingabe); yield true; }
case "einzahlen" -> { einzahlen(); yield true; }
case "abheben" -> { abheben(); yield true; }
case "ueberweisen" -> { ueberweisen(); yield true; }
case "neues" -> { neuesKonto(); yield true; }
case "verlauf" -> { zeigeVerlauf(eingabe); yield true; }
case "exit", "quit" -> false;
default -> { System.out.println("Unbekannter Befehl. Tippe 'hilfe'."); yield true; }
};
}
private void zeigeHilfe() {
System.out.println("""
=== Verfuegbare Befehle ===
konten Alle Konten anzeigen
info <kontoNr> Konto-Details anzeigen
einzahlen Geld einzahlen
abheben Geld abheben
ueberweisen Geld ueberweisen
neues konto Neues Konto eroeffnen
verlauf <kontoNr> Transaktionsverlauf
hilfe Diese Hilfe
exit Programm beenden
""");
}
private void zeigeKonten() {
System.out.println("\n=== Konten (%d) ===".formatted(bank.kontoAnzahl()));
for (var konto : bank.alleKonten()) {
System.out.println(" " + konto);
}
System.out.printf(" Gesamteinlagen: %.2f EUR%n", bank.gesamtEinlagen());
}
private void zeigeKontoInfo(String eingabe) {
var teile = eingabe.split("\\s+");
if (teile.length < 2) {
System.out.print("Kontonummer: ");
teile = new String[]{"info", scanner.nextLine().strip()};
}
var konto = bank.kontoFinden(teile[1]);
System.out.println("\n" + konto);
if (konto instanceof Girokonto giro) {
System.out.printf(" Dispo-Limit: %.2f EUR%n", giro.getDispoLimit());
System.out.printf(" Verfuegbar: %.2f EUR%n", giro.getVerfuegbar());
}
if (konto instanceof Sparkonto spar) {
System.out.printf(" Zinssatz: %.1f%%%n", spar.getZinssatz());
}
}
private void einzahlen() {
System.out.print("Kontonummer: ");
var kontoNr = scanner.nextLine().strip();
System.out.print("Betrag: ");
var betrag = Double.parseDouble(scanner.nextLine().strip());
bank.kontoFinden(kontoNr).einzahlen(betrag);
System.out.printf("%.2f EUR eingezahlt auf %s.%n", betrag, kontoNr);
}
private void abheben() {
System.out.print("Kontonummer: ");
var kontoNr = scanner.nextLine().strip();
System.out.print("Betrag: ");
var betrag = Double.parseDouble(scanner.nextLine().strip());
bank.kontoFinden(kontoNr).abheben(betrag);
System.out.printf("%.2f EUR abgehoben von %s.%n", betrag, kontoNr);
}
private void ueberweisen() {
System.out.print("Von Konto: ");
var von = scanner.nextLine().strip();
System.out.print("Nach Konto: ");
var nach = scanner.nextLine().strip();
System.out.print("Betrag: ");
var betrag = Double.parseDouble(scanner.nextLine().strip());
bank.ueberweisen(von, nach, betrag);
}
private void neuesKonto() {
System.out.print("Kontotyp (giro/spar): ");
var typ = scanner.nextLine().strip().toLowerCase();
System.out.print("Inhaber: ");
var inhaber = scanner.nextLine().strip();
System.out.print("Startguthaben: ");
var guthaben = Double.parseDouble(scanner.nextLine().strip());
var konto = switch (typ) {
case "giro" -> bank.girokontoEroeffnen(inhaber, guthaben);
case "spar" -> bank.sparkontoEroeffnen(inhaber, guthaben);
default -> throw new IllegalArgumentException("Unbekannter Kontotyp: " + typ);
};
System.out.println("Konto eroeffnet: " + konto.getKontoNr());
}
private void zeigeVerlauf(String eingabe) {
var teile = eingabe.split("\\s+");
if (teile.length < 2) {
System.out.print("Kontonummer: ");
teile = new String[]{"verlauf", scanner.nextLine().strip()};
}
var konto = bank.kontoFinden(teile[1]);
var transaktionen = konto.getTransaktionen();
System.out.println("\n=== Verlauf fuer %s ===".formatted(teile[1]));
if (transaktionen.isEmpty()) {
System.out.println(" Keine Transaktionen.");
} else {
for (var t : transaktionen) {
System.out.println(" " + t);
}
}
}
public static void main(String[] args) {
new BankApp().starten();
}
}
Angewendete Konzepte
| Konzept | Wo im Projekt |
|---|---|
| Interface | Konto definiert den Vertrag |
| Abstrakte Klasse | BasisKonto mit gemeinsamer Logik |
| Vererbung | Girokonto und Sparkonto erben von BasisKonto |
| Polymorphismus | Konto konto = bank.kontoFinden(nr) |
| Pattern Matching | instanceof Girokonto giro |
| Records | Transaktion fuer Transaktionsdaten |
| Collections | HashMap fuer Konten, ArrayList fuer Transaktionen |
| Eigene Exceptions | BankException, KontostandException |
| Kapselung | Private Felder, kontrollierte Methoden |
| Enums | Koennte fuer Kontotypen verwendet werden |
Uebungen
Uebung 1: Festgeldkonto
Erstelle ein Festgeldkonto mit einer Laufzeit. Abhebungen sind erst nach Ablauf der Laufzeit moeglich.
Uebung 2: Kontoauszug
Erstelle eine Methode kontoauszug(), die einen formatierten Auszug mit allen Transaktionen eines Monats erstellt.
Uebung 3: Zinsen berechnen
Implementiere eine Funktion, die fuer alle Sparkonten automatisch Jahres-Zinsen berechnet und gutschreibt.
Uebung 4: Such-Funktion
Fuege eine Suche nach Kundenname hinzu, die alle Konten eines Kunden anzeigt.
Was kommt als Naechstes?
Im naechsten und letzten Projekt baust du eine Todo-App mit Kategorien, Prioritaeten und Datei-Persistenz. Dort kommen Enums, Streams und Dateizugriff dazu.
Zusammenfassung
- Du hast eine vollstaendige Bank-Anwendung mit professioneller OOP-Architektur gebaut
- Interfaces und abstrakte Klassen bieten eine flexible, erweiterbare Struktur
- Vererbung ermoeglicht verschiedene Kontotypen mit geteilter Basislogik
- Eigene Exceptions machen Fehlermeldungen klar und aussagekraeftig
- Kapselung schuetzt den Kontostand vor ungueltigen Operationen
- Pattern Matching vereinfacht die Arbeit mit verschiedenen Kontotypen
Pro-Tipp: Dieses Projekt zeigt ein wichtiges Architekturmuster: Schichten-Trennung. Die Bank-Klasse enthaelt die Geschaeftslogik, die BankApp die Benutzeroberflaeche. Wenn du spaeter eine Web-API oder eine GUI brauchst, musst du nur die UI-Schicht austauschen — die Bank-Logik bleibt gleich. Das ist das Fundament professioneller Software-Architektur!