Zum Inhalt springen
Java Anfänger 60 min

Projekt: Bank-App

Baue eine vollständige Bank-Anwendung in Java mit Vererbung, Interfaces, Exception Handling, Collections und OOP-Design.

Aktualisiert:

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

KonzeptWo im Projekt
InterfaceKonto definiert den Vertrag
Abstrakte KlasseBasisKonto mit gemeinsamer Logik
VererbungGirokonto und Sparkonto erben von BasisKonto
PolymorphismusKonto konto = bank.kontoFinden(nr)
Pattern Matchinginstanceof Girokonto giro
RecordsTransaktion fuer Transaktionsdaten
CollectionsHashMap fuer Konten, ArrayList fuer Transaktionen
Eigene ExceptionsBankException, KontostandException
KapselungPrivate Felder, kontrollierte Methoden
EnumsKoennte 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!

Zurück zum Java Kurs