Projekt: Taschenrechner
Baue einen vollständigen Konsolen-Taschenrechner in Java mit OOP, Enums, Exception Handling und einer REPL-Schleife.
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:
| Konzept | Wo im Projekt |
|---|---|
| Enums | Operation mit Feldern und Methoden |
| Records | Berechnung fuer den Verlauf |
| Klassen | Rechner, TaschenrechnerApp |
| Exception Handling | BerechnungException, try-catch |
| Switch Expression | In berechne() und verarbeiteBefehl() |
| Kapselung | Private Felder, kontrollierter Zugriff |
| Collections | ArrayList fuer den Verlauf |
| Methoden | Klare Aufteilung der Logik |
| String-Verarbeitung | Parsing der Eingabe |
Erweiterungs-Ideen
- Klammerausdruecke:
(2 + 3) * 4 - Trigonometrie: sin, cos, tan
- Logarithmus: log, ln
- Variablen:
x = 42, dannx * 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!