Wie muss ich dieses Programm weiter anpassen?

1 Antwort

(Für Quellcode gibt es das Symbol </> in der Formatierungsleiste über dem Eingabefeld. Das macht die Sache wesentlich leichter lesbar.)

Vielleicht ist es einfacher, erst einmal alle Methodenkörper (bis auf die von GCD und LCM - die sind korrekt, zumindest für nichtnegative Argumente, wenn sie sich auch verbessern ließen) komplett zu entfernen und neu aufzubauen (Teile des Quellcodes kann man - vielleicht - als Ideensammlung verwenden). Es gibt hier ziemlich viele Baustellen, die mir auffallen. Dass eine Methode "cancel" aufgerufen wird, aber eine Methode "shorten" definiert ist, ist noch eins der kleineren Probleme.

Ich würde mir die Regeln der Bruchrechnung in eine Liste schreiben und diese Liste abarbeiten.

Wo ich zurückfragen würde, wäre beim Konstruktor - ist es wirklich gewollt, dass ein Nenner 0 stillschweigend durch 1 ersetzt wird? und beim Kehrwert - soll hier wirklich 0 unverändert zurückgegeben werden? (ohne ausdrückliche, schriftliche, per Unterschrift bestätigte Anweisung würde ich in beiden Fällen eine Ausnahme schmeißen, und auch dann damit rechnen, dass der Kunde sich später beschwert, und ihm dann ein paar Stunden Arbeit berechnen, wenn er es später doch anders haben will, auch wenn es mich nur ein paar Sekunden kostet, weil ich dieses Verhalten schon vorbereitet habe)

Konstruktor: wenn - wie allzu oft - die Argumente des Konstruktors ebenso heißen wie die Felder des zu erzeugenden Objekts, muss man this.<Feldname> verwenden (es sei denn, der verwendete Compiler kann diesen "Boilerplate"-Code von sich aus erzeugen). Dann würde ich mich fragen, ob nicht auch im Konstruktor schon gekürzt werden sollte. (Auch im Konstruktor wird ja ein Bruch erzeugt, und jeder erzeugte Bruch soll gekürzt werden.) - Andererseits hört sich die Aufgabenstellung so an, als sollten ungekürzt eingegebene Brüche auch ungekürzt übernommen werden, auch wenn das bescheuert wäre. Immerhin spricht dafür, dass "shorten" public ist. Auch dies würde ich mir schriftlich bestätigen lassen und das vernünftige Verhalten schon mal vorbereiten.

Da diese Methoden neue Objekte zurückgeben sollen, darf man hier unter keinen Umständen die Felder von "this" verändern (und natürlich auch nicht die des Methodenarguments); man muss also neue Variablen erzeugen. Am Ende der Methode dann so was wie

return new Fraction(newNumerator, newDenominator);

Eine weitere Überlegung, die man sich machen soll, ist, ob in der vorliegenden (unbrauchbaren) Form die Vorzeichen der Rückgabewerte richtig behandelt werden. Falls nein, muss man darauf auch noch achten.

Und wenn man dabei feststellt, dass man dies in jeder einzelnen verflixten Methode machen muss, kann man sich auch gleich fragen, ob man das nicht auch an "shorten" bzw. "cancel" delegiert und natürlich auch, ob es nicht ausreicht "shorten"/"cancel" von einer einzigen Stelle, nämlich dem Konstruktor, aus aufrufen zu lassen.

Eine weitere Rückfrage wäre, ob "subtract" mit aufgenommen werden soll.

C# kann implizite Umwandlungen aus und in eigene Typen definieren, ob Java dies ebenfalls kann, habe ich nicht gefunden. Falls ja, würde ich einen automatischen impliziten Cast von int und Integer in Fraction definieren. Falls nein, würde ich zurückfragen, ob auch Rechenoperationen mit Integern definiert werden sollen.

So weit ich weiß, ist es üblich, zu den Rechenmethoden, die sich auf "this" und ein Methodenargument beziehen, auch statische Methoden gleichen Namens zu definieren, die zwei Argumente akzeptieren. Das ist schnell gemacht, z. B.

public static Fraction add(Fraction a, Fraction b) {
    return a.add(b); // in this case, no problem because Fraction.add returns a new object
}

... aber das ist hier nicht gefordert und wäre vielleicht eine unerwünschte Erweiterung der Aufgabe.

Beim Testen gibt "toDouble" vermutlich 0.0 zurück, wenn es auf 2/3 angewandt wird. Wenn du sagen kannst, warum, weißt du auch, an welcher Stelle die typecasts, die in der Aufgabenstellung erwähnt werden, eingefügt werden müssen.

Noch was zu den Rückfragen: möglicherweise sollt ihr euch jetzt schon an bescheuerte Kundenwünsche gewöhnen. Dann würde m. E. aber unbedingt dazugehören, dass ihr ebenfalls lernt, den Kunden unterschreiben zu lassen, dass ihr ihn auf "ungewöhnliches" Verhalten hingewiesen habt und dass er genau dieses Verhalten ausdrücklich will.

(Übrigens würde ich "divide" zu

public Fraction divide(Fraction f) {
    return multiply(reciprocal(f));
}

vereinfachen. Aber in einem Fall wie diesem erst nach Test, ob das Fehlverhalten bei Division durch 0 korrekt reproduziert wird.)

Und auch bei "toString()" würde ich zurückfragen, ob es wirklich gewollt ist, dass nicht nur der Bruch, sondern auch der Dezimalwert ausgegeben werden soll - noch dazu mit "=" verbunden, und dabei darauf hinweisen, dass es auf diese Weise ziemlich verwirrend aussehen wird, wenn Brüche in zusammengesetzten Ausdrücken ausgegeben werden.

Letztlich ist dies aber nicht schlimmer, als ich es aus meinem Beruf (Softwareentwickler) von Kundenanfragen her gewohnt bin. Seufz.

Woher ich das weiß:Berufserfahrung

Miraaa1319 
Fragesteller
 14.01.2024, 15:30
public class Fraction {

    private int numerator, denominator;

    public static int GCD(int x, int y) {
        if (y == 0) return x;
        return GCD(y, x % y);
    }

    public static int LCM(int x, int y) {
        return (x * y) / GCD(x, y);
    }

    public Fraction(int numerator, int denominator) {
        if (denominator == 0)
            throw new IllegalArgumentException("Division by zero");
        int factor = GCD(Math.abs(numerator), Math.abs(denominator));
        this.numerator = numerator / factor;
        this.denominator = denominator / factor;
        if (this.denominator < 0) {
            this.numerator = -this.numerator;
            this.denominator = -this.denominator;
        }
    }

    public int getDenominator() {
        return denominator;
    }

    public int getNumerator() {
        return numerator;
    }

    public double toDouble() {
        return (double) numerator / denominator;
    }

    public String toString() {
        return numerator + "/" + denominator;
    }

    public void shorten() {
        int factor = GCD(Math.abs(numerator), Math.abs(denominator));
        this.numerator = numerator / factor;
        this.denominator = denominator / factor;
        if (this.denominator < 0) {
            this.numerator = -this.numerator;
            this.denominator = -this.denominator;
        }
    }

    public Fraction add(Fraction f) {
        int commonDenominator = LCM(this.denominator, f.denominator);
        return new Fraction(this.numerator * (commonDenominator / this.denominator)
                + f.numerator * (commonDenominator / f.denominator), commonDenominator);
    }

    public Fraction multiply(Fraction f) {
        return new Fraction(this.numerator * f.numerator, this.denominator * f.denominator);
    }

    public Fraction reciprocal() {
        return numerator == 0 ? this : new Fraction(denominator, numerator);
    }

    public Fraction divide(Fraction f) {
        return this.multiply(f.reciprocal());
    }

    public static void main(String[] args) {
        // Getter Test
        Fraction twothird = new Fraction(2, 3);
        System.out.println("Fraction twothird: " + twothird.getNumerator() + "/" + twothird.getDenominator());
        // Erwartet: 2/3

        try {
            Fraction f = new Fraction(2, 0);
            System.out.println("Fraction f: " + f.getNumerator() + "/" + f.getDenominator());
        } catch (IllegalArgumentException e) {
            System.out.println("Exception caught: " + e.getMessage());
            // Erwartet: Exception mit "Division by zero"
        }
    }
}

Ich habe jetzt das, allerdings kommt dass da noch ein Fehler ist, ich weiss aber nicht wie ich den beheben soll bzw. kann, würde echt dankbar sein wenn jemand eine Ahnung hat

Was wird getestet?

Fraction f = new Fraction(2,0);
System.out.println(f.getNumerator()+"/"+f.getDenominator());

Erwartet

2/1

Erhalten

Exception in thread "main" java.lang.IllegalArgumentException: Division by zero
\tat Fraction.<init>(Fraction.java:16)
\tat __Tester__.main(__Tester__.java:4)
0
PWolff  15.01.2024, 14:30
@Miraaa1319

Dein Konstruktor hat zwar die sinnvolle Funktionalität, erfüllt aber - wie ich schon bemerkte - nicht die (m. E. reichlich unsinnigen und kontraproduktiven) Vorgaben.

Hiernach soll tatsächlich ein Nenner 0 stillschweigend (!) durch 1 ersetzt werden.

Das ist offensichtlich der Grund, warum der Unit Test fehlschlägt.

Außderdem kann man die Spezifikation auch so verstehen, dass im Konstruktor (und nur dort) nicht gekürzt werden soll.

------

Ebenfalls wie in meiner Antwort erwähnt, würde ich mir außerhalb solcher reiner Lehrspezifikationen derartige Spezifikationen einzeln schriftlich bestätigen lassen, einschließlich der Kenntnisnahme der Warnung vor späterem möglicherweise schwerwiegendem und möglicherweise schwer auffindbarem und behebbarem Fehlverhalten. Der Kunde ist dann zwar oft genug kein Bisschen weniger sauer, aber wenigstens ist es weniger wahrscheinlich, dass man sich vor Gericht wiedersieht.

Im konkreten Fall WIRD es bei einer tatsächlichen Bibliothek irgendwann vorkommen, dass ein Nenner 0 übergeben wird, und bei einem geworfenen Fehler die Anwendung wenigstens abstürzt, während sie ansonsten mit einem völlig falschen Wert weiterrechnet. Wegen vergleichbarer Fehler sind schon öfters mit einem Schlag zig Millionen vernichtet worden.

-----

Noch was zum Überlegen: Könntest du erklären, wieso

return (double) numerator / denominator;

funktioniert? (Tipp: Präzedenz der Operatoren)

0