Wie muss ich dieses Programm weiter anpassen?
Die Rechnung mit endlichen (Gleit-)Kommazahlen kann zu Ungenauigkeiten führen, es kann daher wünschenswert sein, ganzzahlige Brüche zu verwenden.
In dieser Aufgabe soll eine Objektklasse zur Repräsentation von und Rechnung mit Brüchen erzeugt werden.
Die Köpfe der geforderten Methoden sind bereits vorgegeben und dürfen nicht verändert werden. Die Methoden sollen entsprechend der Kommentare vervollständigt werden.
Achten sie darauf diese Methoden, wann immer möglich, zu verwenden und so Duplikationen zu vermeiden.
Alle Methoden deren Rückgabetyp Fraction ist, sollen ein neues Objekt erzeugen und zurückgeben und die zur Berechnung verwendeten Objekte unverändert lassen!
Die Java-Standardbibliotheken Math und Integer dürfen (müssen aber nicht) verwendet werden. Um eine Gleitkommazahl bei der Rechnung mit Ganzzahlen zu erhalten muss ggf. Typecasting verwendet werden!
Es stehen außerdem die folgenden zwei Methoden schon zur Verfügung:
public static int GCD(int x, int y): Gibt den größten gemeinsamen Teiler der beiden Argumente (Greatest Common Divider) zurück.
public static int LCM(int x, int y): Gibt das kleinste gemeinsame Vielfache der beiden Argumente (Lowest Common Multiple) zurück.
public class Fraction {
private int numerator, denominator;
/**
* Gibt den größten gemeinsamen Teiler der beiden Argumente (Greatest Common Divider) zurück.
*/
public static int GCD(int x, int y) {
if (y == 0) return x;
return GCD(y, x % y);
}
/**
* Gibt das kleinste gemeinsame Vielfache der beiden Argumente (Lowest Common Multiple) zurück.
*/
public static int LCM(int x, int y) {
return (x * y) / GCD(x, y);
}
//Beginn der Aufgabe
/**
* Vollstaendig parametrisierter Konstruktor der Zaehler und Nenner
* uebergeben bekommt und die entsprechenden Attribute setzt.
* Negative Vorzeichen (Zahlen kleiner als Null) duerfen nur im Zaehler
* auftreten (nicht im "denominator"-Attribut).
* Die Uebergabe eines negativen Nenners ("denominator"-Argument) an den Konstruktor ist jedoch zulaessig.
* Der Konstruktor muss also den uebergebenen Nenner pruefen und sein Vorzeichen so behandeln,
* dass der resultierende Bruch (die Attribute) die genannte Restriktion erfüllt
* und der Wert des Bruchs (die Argumente) unverändert bleibt
* (ein negatives Vorzeichen im Nenner muss also methematisch korrekt beseitigt werden).
* Wird eine Null als Nenner uebergeben, so wird das entsprechende Attribut
* auf Eins gesetzt.
* Jeder erzeugte Bruch wird gekuerzt (dazu soll die entsprechende Mehode s.u. verwendet werden).
*/
public Fraction(int numerator, int denominator) {
if(pDenominator<0){
numerator = -pNumerator;
denominator = -pDenominator;
}
if(pDenominator == 0){
denominator = 1;
}
}
/**
* Gibt den Nenner zurueck.
*/
public int getDenominator() {
return denominator;
}
/**
* Gibt den Zaehler zurueck.
*/
public int getNumerator() {
return numerator;
}
/**
* Gibt den Bruch als Gleitkommazahl zurueck.
*/
public double toDouble() {
double fraction = numerator / denominator;
return fraction;
}
/**
* Gibt einen String im Format
* "Zaehler/Nenner" zurueck.
*/
public String toString() {
double fraction = toDouble();
return numerator + "/" + denominator + " = " + fraction;
}
/**
* Kuerzt (vereinfacht) den Bruch.
*/
public void shorten() {
int factorC = GCD(numerator, denominator);
numerator = numerator/factorC;
denominator = denominator/factorC;
}
/**
* Erweitert (macht gleichnamig), addiert dann den uebergebenen Bruch.
*/
public Fraction add(Fraction f) {
numerator = numerator * f.denominator + f.numerator * denominator;
denominator = denominator * f.denominator;
cancel();
}
/**
* Multipliziert mit dem uebergebenen Bruch.
*/
public Fraction multiply(Fraction f) {
numerator = numerator * f.numerator;
denominator = denominator * f.denominator;
cancel();
}
/**
* Bildet den Kehrwert, wenn der Zaehler ungleich Null ist.
* Sonst wird der Bruch unveraendert zurueckgegeben.
*/
public Fraction reciprocal() {
}
/**
* Dividiert durch den uebergebenen Bruch
* (unter Verwendung von Kehrwert und Multiplikation).
*/
public Fraction divide(Fraction f) {
numerator = numerator * f.denominator;
denominator = denominator * f.numerator;
cancel();
}
}
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.
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)
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?
Erwartet
2/1
Erhalten