Zum Inhalt springen
Java Anfänger 45 min

Projekt: Taschenrechner

Baue einen vollständigen Konsolen-Taschenrechner in Java mit OOP, Enums, Exception Handling und einer REPL-Schleife.

Aktualisiert:

Projekt: Taschenrechner

Zeit, alles in die Praxis umzusetzen! In diesem Projekt baust du einen vollstaendigen Konsolen-Taschenrechner in Java. Du wirst Klassen, Enums, Methoden, Exception Handling und Switch Expressions kombinieren — alles, was du bisher gelernt hast.

Was wir bauen

Unser Taschenrechner kann:

  • Grundrechenarten (+, -, *, /)
  • Potenzierung und Wurzelziehen
  • Speicher-Funktionen (speichern, laden, loeschen)
  • Verlauf der letzten Berechnungen
  • Fehlerbehandlung (Division durch 0, ungueltige Eingaben)
  • Interaktive REPL-Schleife (Read-Eval-Print Loop)

Schritt 1: Die Operation als Enum

Wir beginnen mit einem Enum fuer alle unterstuetzten Operationen:

public enum Operation {
    ADDITION("+", "Addition"),
    SUBTRAKTION("-", "Subtraktion"),
    MULTIPLIKATION("*", "Multiplikation"),
    DIVISION("/", "Division"),
    POTENZ("^", "Potenz"),
    WURZEL("sqrt", "Quadratwurzel"),
    MODULO("%", "Modulo");

    private final String symbol;
    private final String name;

    Operation(String symbol, String name) {
        this.symbol = symbol;
        this.name = name;
    }

    public String getSymbol() { return symbol; }
    public String getName() { return name; }

    public static Operation vonSymbol(String symbol) {
        for (var op : values()) {
            if (op.symbol.equals(symbol)) {
                return op;
            }
        }
        return null;
    }
}

Schritt 2: Eigene Exception

Eine Exception fuer Berechnungsfehler:

public class BerechnungException extends RuntimeException {
    private final String ausdruck;

    public BerechnungException(String nachricht, String ausdruck) {
        super(nachricht);
        this.ausdruck = ausdruck;
    }

    public String getAusdruck() { return ausdruck; }
}

Schritt 3: Record fuer Ergebnisse

Ein Record, um Berechnungen im Verlauf zu speichern:

import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

public record Berechnung(
    double operand1,
    Operation operation,
    double operand2,
    double ergebnis,
    LocalTime zeitpunkt
) {
    private static final DateTimeFormatter FORMAT =
        DateTimeFormatter.ofPattern("HH:mm:ss");

    @Override
    public String toString() {
        if (operation == Operation.WURZEL) {
            return "[%s] sqrt(%.4g) = %.4g".formatted(
                zeitpunkt.format(FORMAT), operand1, ergebnis);
        }
        return "[%s] %.4g %s %.4g = %.4g".formatted(
            zeitpunkt.format(FORMAT),
            operand1, operation.getSymbol(), operand2, ergebnis);
    }
}

Schritt 4: Die Rechner-Klasse

Hier steckt die Hauptlogik:

import java.time.LocalTime;
import java.util.ArrayList;
import java.util.List;

public class Rechner {
    private double speicher;
    private final List<Berechnung> verlauf;

    public Rechner() {
        this.speicher = 0;
        this.verlauf = new ArrayList<>();
    }

    public double berechne(double a, Operation op, double b) {
        var ergebnis = switch (op) {
            case ADDITION -> a + b;
            case SUBTRAKTION -> a - b;
            case MULTIPLIKATION -> a * b;
            case DIVISION -> {
                if (b == 0) {
                    throw new BerechnungException(
                        "Division durch 0 ist nicht erlaubt!",
                        "%.4g / 0".formatted(a));
                }
                yield a / b;
            }
            case POTENZ -> Math.pow(a, b);
            case WURZEL -> {
                if (a < 0) {
                    throw new BerechnungException(
                        "Wurzel aus negativer Zahl nicht moeglich!",
                        "sqrt(%.4g)".formatted(a));
                }
                yield Math.sqrt(a);
            }
            case MODULO -> {
                if (b == 0) {
                    throw new BerechnungException(
                        "Modulo durch 0 ist nicht erlaubt!",
                        "%.4g %% 0".formatted(a));
                }
                yield a % b;
            }
        };

        var berechnung = new Berechnung(a, op, b, ergebnis, LocalTime.now());
        verlauf.add(berechnung);

        return ergebnis;
    }

    // Speicher-Funktionen
    public void speichern(double wert) {
        this.speicher = wert;
    }

    public double speicherLaden() {
        return speicher;
    }

    public void speicherLoeschen() {
        this.speicher = 0;
    }

    // Verlauf
    public List<Berechnung> getVerlauf() {
        return List.copyOf(verlauf);
    }

    public void verlaufLoeschen() {
        verlauf.clear();
    }

    public int getVerlaufGroesse() {
        return verlauf.size();
    }
}

Schritt 5: Die Benutzeroberflaeche

Die interaktive Konsolen-Anwendung:

import java.util.Scanner;

public class TaschenrechnerApp {

    private final Rechner rechner;
    private final Scanner scanner;
    private double letztesErgebnis;

    public TaschenrechnerApp() {
        this.rechner = new Rechner();
        this.scanner = new Scanner(System.in);
        this.letztesErgebnis = 0;
    }

    public void starten() {
        zeigeBanner();

        while (true) {
            System.out.print("\nRechner> ");
            var eingabe = scanner.nextLine().strip();

            if (eingabe.isEmpty()) continue;

            if (verarbeiteBefehl(eingabe)) continue;
            if (eingabe.equalsIgnoreCase("exit") || eingabe.equalsIgnoreCase("quit")) {
                System.out.println("Auf Wiedersehen!");
                break;
            }

            verarbeiteBerechnung(eingabe);
        }

        scanner.close();
    }

    private void zeigeBanner() {
        System.out.println("""
            ╔══════════════════════════════════════╗
            ║     Java Taschenrechner v1.0         ║
            ║                                      ║
            ║  Operatoren: + - * / ^ % sqrt        ║
            ║  Befehle: hilfe, verlauf, exit        ║
            ║  'ans' = letztes Ergebnis             ║
            ╚══════════════════════════════════════╝
            """);
    }

    private boolean verarbeiteBefehl(String eingabe) {
        return switch (eingabe.toLowerCase()) {
            case "hilfe", "help", "?" -> {
                zeigeHilfe();
                yield true;
            }
            case "verlauf", "history" -> {
                zeigeVerlauf();
                yield true;
            }
            case "ms" -> {
                rechner.speichern(letztesErgebnis);
                System.out.printf("Gespeichert: %.4g%n", letztesErgebnis);
                yield true;
            }
            case "mr" -> {
                var wert = rechner.speicherLaden();
                System.out.printf("Speicher: %.4g%n", wert);
                yield true;
            }
            case "mc" -> {
                rechner.speicherLoeschen();
                System.out.println("Speicher geloescht.");
                yield true;
            }
            case "clear", "cls" -> {
                rechner.verlaufLoeschen();
                System.out.println("Verlauf geloescht.");
                yield true;
            }
            default -> false;
        };
    }

    private void verarbeiteBerechnung(String eingabe) {
        try {
            // Wurzel-Sonderfall: sqrt 25
            if (eingabe.toLowerCase().startsWith("sqrt ")) {
                var zahlStr = eingabe.substring(5).strip();
                var zahl = parseZahl(zahlStr);
                var ergebnis = rechner.berechne(zahl, Operation.WURZEL, 0);
                letztesErgebnis = ergebnis;
                System.out.printf("  = %.6g%n", ergebnis);
                return;
            }

            // Standard: "10 + 5"
            var teile = eingabe.split("\\s+");
            if (teile.length != 3) {
                System.out.println("Format: <zahl> <operator> <zahl>");
                System.out.println("Beispiel: 10 + 5");
                return;
            }

            var a = parseZahl(teile[0]);
            var op = Operation.vonSymbol(teile[1]);
            var b = parseZahl(teile[2]);

            if (op == null) {
                System.out.println("Unbekannter Operator: " + teile[1]);
                System.out.println("Verfuegbar: + - * / ^ %");
                return;
            }

            var ergebnis = rechner.berechne(a, op, b);
            letztesErgebnis = ergebnis;
            System.out.printf("  = %.6g%n", ergebnis);

        } catch (BerechnungException e) {
            System.out.println("Fehler: " + e.getMessage());
        } catch (NumberFormatException e) {
            System.out.println("Ungueltige Zahl! " + e.getMessage());
        }
    }

    private double parseZahl(String text) {
        if (text.equalsIgnoreCase("ans")) {
            return letztesErgebnis;
        }
        if (text.equalsIgnoreCase("mr")) {
            return rechner.speicherLaden();
        }
        if (text.equalsIgnoreCase("pi")) {
            return Math.PI;
        }
        if (text.equalsIgnoreCase("e")) {
            return Math.E;
        }
        return Double.parseDouble(text);
    }

    private void zeigeHilfe() {
        System.out.println("""

            === Hilfe ===
            Berechnung:  <zahl> <op> <zahl>    z.B. 10 + 5
            Wurzel:      sqrt <zahl>            z.B. sqrt 25
            Konstanten:  pi, e
            Letztes Ergebnis: ans

            Operatoren:
              +   Addition        -   Subtraktion
              *   Multiplikation  /   Division
              ^   Potenz          %   Modulo
              sqrt Quadratwurzel

            Speicher:
              ms   Ergebnis speichern
              mr   Speicher laden
              mc   Speicher loeschen

            Sonstiges:
              verlauf  Berechnungsverlauf anzeigen
              clear    Verlauf loeschen
              hilfe    Diese Hilfe anzeigen
              exit     Programm beenden
            """);
    }

    private void zeigeVerlauf() {
        var verlauf = rechner.getVerlauf();
        if (verlauf.isEmpty()) {
            System.out.println("Noch keine Berechnungen.");
            return;
        }
        System.out.println("\n=== Verlauf (%d Berechnungen) ===".formatted(verlauf.size()));
        for (var berechnung : verlauf) {
            System.out.println("  " + berechnung);
        }
    }

    // Hauptprogramm
    public static void main(String[] args) {
        var app = new TaschenrechnerApp();
        app.starten();
    }
}

Schritt 6: Testen

Erstelle die Dateien und teste den Rechner:

Rechner> 10 + 5
  = 15.0000

Rechner> ans * 3
  = 45.0000

Rechner> sqrt 144
  = 12.0000

Rechner> 2 ^ 10
  = 1024.00

Rechner> 10 / 0
Fehler: Division durch 0 ist nicht erlaubt!

Rechner> pi * 2
  = 6.28319

Rechner> ms
Gespeichert: 6.28319

Rechner> verlauf
=== Verlauf (4 Berechnungen) ===
  [14:30:15] 10 + 5 = 15
  [14:30:18] 15 * 3 = 45
  [14:30:22] sqrt(144) = 12
  [14:30:25] 2 ^ 10 = 1024

Rechner> exit
Auf Wiedersehen!

Was du gelernt hast

In diesem Projekt hast du angewendet:

KonzeptWo im Projekt
EnumsOperation mit Feldern und Methoden
RecordsBerechnung fuer den Verlauf
KlassenRechner, TaschenrechnerApp
Exception HandlingBerechnungException, try-catch
Switch ExpressionIn berechne() und verarbeiteBefehl()
KapselungPrivate Felder, kontrollierter Zugriff
CollectionsArrayList fuer den Verlauf
MethodenKlare Aufteilung der Logik
String-VerarbeitungParsing der Eingabe

Erweiterungs-Ideen

  • Klammerausdruecke: (2 + 3) * 4
  • Trigonometrie: sin, cos, tan
  • Logarithmus: log, ln
  • Variablen: x = 42, dann x * 2
  • Verlauf speichern: In Datei schreiben und beim Start laden

Uebungen

Uebung 1: Erweiterte Operationen

Fuege die Operationen abs (Absolutwert) und neg (Vorzeichenwechsel) hinzu.

Uebung 2: Formatierung

Fuege eine Option hinzu, die Dezimalstellen-Anzahl einzustellen (format 2 fuer 2 Nachkommastellen).

Uebung 3: Verlauf speichern

Speichere den Verlauf in einer Datei (verlauf.txt) und lade ihn beim Start.

Uebung 4: Unit Tests

Schreibe Tests fuer die Rechner-Klasse: Teste alle Operationen, Randfaelle (Division durch 0, negative Wurzel) und den Speicher.

Was kommt als Naechstes?

Im naechsten Projekt baust du eine Bank-App — mit mehreren Konten, Ueberweisungen und einer Transaktionshistorie. Dort wendest du Vererbung, Interfaces und eine komplexere OOP-Struktur an.

Zusammenfassung

  • Du hast einen vollstaendigen Taschenrechner mit OOP-Prinzipien gebaut
  • Enums eignen sich perfekt fuer eine feste Menge von Operationen
  • Records sind ideal fuer unveraenderliche Daten (Verlauf)
  • Exception Handling macht die Anwendung robust gegen Fehleingaben
  • Switch Expressions machen die Logik kompakt und lesbar
  • Trennung der Verantwortlichkeiten: Rechner-Logik und UI sind getrennt

Pro-Tipp: Die wichtigste Lektion aus diesem Projekt: Trenne Logik von Darstellung. Die Rechner-Klasse weiss nichts von der Konsole — sie berechnet nur. Die TaschenrechnerApp kuemmert sich um Ein-/Ausgabe. So koenntest du spaeter eine grafische Oberflaeche (GUI) bauen, ohne die Rechner-Logik aendern zu muessen!

Zurück zum Java Kurs