2.04 Anwendungsbeispiel Bruchrechnung

Entwurf und erste Version

Die Klasse Auto, die wir uns bisher angesehen haben, war als eine anschauliche Einführung gedacht. Nun sehen wir uns ein etwas „seriöseres“ Beispiel an: Wir entwerfen eine Klasse Bruch, mit der wir Java die Bruchrechnung beibringen. Dabei üben wir alles, was wir bis jetzt kennengelernt haben und lernen auch noch ein paar neue Details zur objektorientierten Programmierung.


Wiedergabe stellt eine Verbindung zu YouTube her.

Wir starten mit einem ersten Entwurf:

Die Klasse Bruch hat die Attribute zaehler und nenner, einen Konstruktor, Methoden zum Erweitern und Kürzen und eine Methode ggT.
Ein erster Entwurf. Es werden noch viele Methoden folgen.

Einen Bruch können wir natürlich mit seinem Zähler und seinem Nenner darstellen. Beides sollen ganzzahlige Werte sein, so dass es naheliegt, den Datentyp int für diese zu verwenden. Der Konstruktor benötigt zum Erstellen eines Objektes Werte für diese beiden Attribute. In dieser ersten Version soll die Klasse nur wenige Methoden besitzen. Die Methode kuerze() soll den Bruch so weit wie möglich kürzen. Die Methode erweitere(int pFaktor) soll den Bruch mit der Zahl pFaktor erweitern.

Neben diesen beiden Methoden gibt es noch eine private Methode ggT(). Diese soll dazu dienen, den größten gemeinsamen Teiler von Zähler und Nenner zu bestimmen, damit der Bruch dann genau durch diesen gekürzt werden kann. Von außen muss die Methode nicht sichtbar sein, da sie nur zum Kürzen benutzt werden soll.


Hintergrundinformation

In der Methode ggT() werden wir den Euklidischen Algorithmus verwenden. Auf diesen gehen wir an dieser Stelle nicht näher ein. Dieser Algorithmus kann den ggT zweier Zahlen schnell bestimmen – viel schneller als es mit einer Primfaktorzerlegung möglich wäre.


Wiedergabe stellt eine Verbindung zu YouTube her.

Sehen wir uns eine Umsetzung dieses ersten Entwurfs an:

public class Bruch {

  // Attribute
  private int zaehler;
  private int nenner;

  // Kontruktor
  public Bruch(int pZaehler, int pNenner){
    if (pNenner == 0){
      System.out.println("Der Nenner darf nicht 0 sein.");
      System.out.println("Setze den Bruch auf 0.");
      zaehler = 0;
      nenner = 1;
    }else{
      zaehler = pZaehler;
      nenner = pNenner;
    }
  }

  // Methoden	zur Manipulation des Objektes
  public void kuerze(){
    int faktor = ggT();
    zaehler = zaehler / faktor;
    nenner = nenner / faktor;
    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  public void erweitere(int pFaktor){
    if (pFaktor != 0){
      zaehler = zaehler*pFaktor;
      nenner = nenner*pFaktor;
      if (nenner < 0){
        zaehler = -zaehler;
        nenner = -nenner;
      }
    }
  }

  // get-Methoden
  public int getZaehler() {
    return zaehler;
  }

  public int getNenner() {
    return nenner;
  }	

  // Ausgabe
  public String toString(){
    return zaehler +" / "+nenner;
  }

  // private Methode
  private int ggT(){

    int a = zaehler;
    int b = nenner;
    int h;

    while(b != 0){
      h = a%b;
      a = b;
      b = h;
    }

    return a;

  }

}

Bevor wir auf Einzelheiten der Implementierung eingehen, testen wir diese Klasse erst einmal:

public class Bruchtest {

  public static void main(String[] args) {
    
    Bruch meinBruch = new Bruch(12, 10);		
    System.out.println(meinBruch);
    
    meinBruch.kuerze();		
    System.out.println(meinBruch);		
    
    meinBruch.erweitere(3);
    System.out.println(meinBruch);
    
    
  }

}

Dieser kleine Test liefert die folgende Ausgabe:

12 / 10
6 / 5
18 / 15

Der erste Test war also erfolgreich. Der erzeugte Bruch wird tatsächlich so weit wie möglich gekürzt und anschließend mit 3 erweitert.

Sehen wir uns nun im Detail den Konstruktor der Klasse an:

// Kontruktor
public Bruch(int pZaehler, int pNenner){
  if (pNenner == 0){
    System.out.println("Der Nenner darf nicht 0 sein.");
    System.out.println("Setze den Bruch auf 0.");
    zaehler = 0;
    nenner = 1;
  }else{
    zaehler = pZaehler;
    nenner = pNenner;
  }
}

Bevor dieser den Attributen ihre Werte zuordnet prüft er, ob der Nenner 0 ist. Sollte dies der Fall sein, gibt einen Hinweis und der Bruch wird einfach auf den Standardwert 0 gesetzt, damit trotz der fehlerhaften Angabe beim Aufruf des Konstruktors ein Objekt erstellt werden kann.

Betrachten wir nun die beiden Methoden zum Kürzen und Erweitern:

// Methoden	zur Manipulation des Objektes
public void kuerze(){
  int faktor = ggT();
  zaehler = zaehler / faktor;
  nenner = nenner / faktor;
  if (nenner < 0){
    zaehler = -zaehler;
    nenner = -nenner;
  }
}

public void erweitere(int pFaktor){
  if (pFaktor != 0){
    zaehler = zaehler*pFaktor;
    nenner = nenner*pFaktor;
    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }
}

Das Erweitern ist ein wenig einfacher; wir Multiplizieren einfach Zähler und Nenner mit dem gegebenen Wert. Danach wird aber noch geprüft, ob der Nenner negativ ist. Ist dies der Fall, werden bei Zähler und Nenner das Vorzeichen umgedreht. Das soll dazu dienen, dass das Minus bei einem Bruch immer ganz vorne steht und nicht nach dem Bruchstrich. Außerdem kann dann auch so eine unschöne Ausgabe wie -5 / -2 verhindert werden.

Beim Kürzen wird kein Parameter benötigt. Stattdessen wird zunächst mit der privaten Methode ggT() der Faktor zum Kürzen bestimmt. Danach werden Zähler und Nenner durch diesen geteilt und es wird wieder geprüft, ob das Vorzeichen von beiden umgedreht werden sollte.


Hintergrundinfo

Die Methode kuerze() verwendet eine lokale Variable faktor. Diese wird nach Ausführung der Methode wieder gelöscht, so dass bei einem erneuten Aufruf der Methode der Name faktor wieder neu verwendet werden kann. Das sieht man daran, dass die Variable innerhalb der Methode deklariert wird. Das ist vergleichbar mit der Zählvariablen in einer Zählschleife. faktor ist damit kein Attribut der Klasse Bruch.
Siehe dazu auch den Abschnitt „Gültigkeitsbereich“ in dieser Lektion!


Hast Du aufmerksam mitgedacht, dann ist Dir vielleicht aufgefallen, dass auch der Konstruktor prüfen sollte, ob die Vorzeichen von Zähler und Nenner umgekehrt werden müssen. Also erweitern wir ihn:

// Kontruktor
public Bruch(int pZaehler, int pNenner){
  if (pNenner == 0){
    System.out.println("Der Nenner darf nicht 0 sein.");
    System.out.println("Setze den Bruch auf 0.");
    zaehler = 0;
    nenner = 1;
  }else{
    zaehler = pZaehler;
    nenner = pNenner;
  }

  if (nenner < 0){
    zaehler = -zaehler;
    nenner = -nenner;
  }
}

Ersetzen wir den alten Konstruktor durch diesen, können wir erneut testen, ob alles OK ist:

public class Bruchtest {

  public static void main(String[] args) {
    
    Bruch meinBruch = new Bruch(12, -10);		
    System.out.println(meinBruch);
    
    meinBruch.kuerze();		
    System.out.println(meinBruch);		
    
    meinBruch.erweitere(-2);
    System.out.println(meinBruch);
    
    
  }

}

Die Ausgabe sieht gut aus:

-12 / 10
-6 / 5
-12 / 10

Wie wir gewünscht hatten, ist das Minuszeichen immer ganz vorne.

Multiplikation und Division

Wir können nun einen oder auch mehrere Brüche erstellen und diese kürzen oder erweitern. Spannend wird aber es natürlich erst, wenn wir wirklich anfangen zu rechnen.


Wiedergabe stellt eine Verbindung zu YouTube her.

Wir sehen uns als erstes die Multiplikation an, da diese am einfachsten ist. Wir erreichen sie durch Ergänzen der folgenden Methode in der Klasse Bruch:

public void multipliziere(Bruch pBruch){
  zaehler = zaehler*pBruch.getZaehler();
  nenner = nenner*pBruch.getNenner();
  kuerze();
}

Eine Anwendung dieser Methode kann so aussehen:

public class Bruchtest {

  public static void main(String[] args) {
    
    Bruch meinBruch = new Bruch(5,2);		
    Bruch deinBruch = new Bruch(3,4);
    
    meinBruch.multipliziere(deinBruch);	
    
    System.out.println(meinBruch);
    System.out.println(deinBruch);
  }

}

Wir erhalten diese Ausgabe:

15 / 8
3 / 4

Das bedeutet, dass der Werte des Objektes meinBruch tatsächlich geändert wurde. Der Bruch wurde mit dem anderen multipliziert und hat daher einen neuen Wert. Der Bruch deinBruch hingegen ist unverändert geblieben. Nur der Bruch der den Befehl bekommt, die Methode auszuführen, kann seinen Wert verändern.

Bevor wir die Division umsetzen, fügen wir der Klasse Bruch noch eine weitere Methode hinzu:

public boolean istNull(){
  if (zaehler == 0){
    return true;
  }else{
    return false;
  }
}

Diese ist praktisch, da wir natürlich auch bei Brüchen nicht durch 0 teilen dürfen. Damit können wir die Divison so implementieren:

public void dividiere(Bruch pBruch){
  if (!pBruch.istNull()){
    nenner = nenner*pBruch.getZaehler();
    zaehler = zaehler*pBruch.getNenner();
  }else{
    System.out.println("Durch 0 darf man nicht teilen!");
  }
  kuerze();
}

Wir prüfen zuerst, ob wir teilen dürfen. Ist dies der Fall, tun wir dies, indem wir mit dem Kehrwert multiplizieren.

Halten wir als Zwischenergebnis noch einmal die bisherige Version der Klasse Bruch komplett fest:

public class Bruch {

  // Attribute
  private int zaehler;
  private int nenner;

  // Kontruktor
  public Bruch(int pZaehler, int pNenner){
    if (pNenner == 0){
      System.out.println("Der Nenner darf nicht 0 sein.");
      System.out.println("Setze den Bruch auf 0.");
      zaehler = 0;
      nenner = 1;
    }else{
      zaehler = pZaehler;
      nenner = pNenner;
    }

    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  // Methoden	zur Manipulation des Objektes
  public void kuerze(){
    int faktor = ggT();
    zaehler = zaehler / faktor;
    nenner = nenner / faktor;
    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  public void erweitere(int pFaktor){
    if (pFaktor != 0){
      zaehler = zaehler*pFaktor;
      nenner = nenner*pFaktor;
      if (nenner < 0){
        zaehler = -zaehler;
        nenner = -nenner;
      }
    }
  }
  
  public void multipliziere(Bruch pBruch){
    zaehler = zaehler*pBruch.getZaehler();
    nenner = nenner*pBruch.getNenner();
    kuerze();
  }

  public void dividiere(Bruch pBruch){
    if (!pBruch.istNull()){
      nenner = nenner*pBruch.getZaehler();
      zaehler = zaehler*pBruch.getNenner();
    }else{
      System.out.println("Durch 0 darf man nicht teilen!");
    }
    kuerze();
  }

  // get-Methoden
  public int getZaehler() {
    return zaehler;
  }

  public int getNenner() {
    return nenner;
  }	
  
  public boolean istNull(){
    if (zaehler == 0){
      return true;
    }else{
      return false;
    }
  }

  // Ausgabe
  public String toString(){
    return zaehler +" / "+nenner;
  }

  // private Methode
  private int ggT(){

    int a = zaehler;
    int b = nenner;
    int h;

    while(b != 0){
      h = a%b;
      a = b;
      b = h;
    }

    return a;

  }

}

Addition und Subtraktion

Bevor Du weiter liest, möchtest Du vielleicht selbst versuchen, die Addition und Subtraktion zu implementieren 🙂


Wiedergabe stellt eine Verbindung zu YouTube her.

Bei der Addition machen wir es uns zunächst leicht, indem wir darauf verzichten, den kleinsten gemeinsamen Nenner zu bestimmen. Stattdessen verwenden wir diese Rechenregel:

    \[\frac{a}{b} + \frac{x}{y} = \frac{a\cdot y}{b\cdot y} + \frac{b\cdot x}{b\cdot y} = \frac{a \cdot x + b \cdot y}{b\cdot y}\]

Wir erweitern also einfach so weit, dass der Nenner in jedem Fall gleich wird. Zum Beispiel:

    \[\frac{5}{2} + \frac{4}{3} = \frac{5\cdot 3}{2\cdot 3} + \frac{2\cdot 4}{2\cdot 3} = \frac{5 \cdot 3 + 2 \cdot 4}{2\cdot 3} = \frac{23}{6}\]

Jetzt müssen wir genau diese Rechnung in einer Methode umsetzen. Das gelingt uns so:

public void addiere(Bruch pBruch){
  zaehler = zaehler*pBruch.getNenner() + nenner*pBruch.getZaehler();
  nenner = nenner*pBruch.nenner;
  kuerze();
}

Der neue Nenner unseres Bruches wird zu dem, was oben in der Formel a \cdot x + b \cdot y ist. Der neue Nenner ist b \cdot y. Dabei müssen wir uns klar darüber sein, dass zaehler und nenner, die Attribute des Objektes sind, das die Methode ausführt. pBruch.getNenner() und pBruch.getZaehler() hingegen sind die entsprechenden Werte des Bruchs, das auf den ersten Bruch addiert werden soll. Nach der Addition wird wie üblich einmal gekürzt.

Wenn wir die Addition verstanden haben, dann fällt uns die Subtraktion nicht mehr schwer. Wir müssen einfach ein Vorzeichen umdrehen:

public void subtrahiere(Bruch pBruch){
  zaehler = zaehler*pBruch.getNenner() - nenner*pBruch.getZaehler();
  nenner = nenner*pBruch.nenner;
  kuerze();
}

Starten wir zum Beispiel diesen kleinen Test

public class Bruchtest {

  public static void main(String[] args) {
    
    Bruch meinBruch = new Bruch(1,2);		
    Bruch deinBruch = new Bruch(1,3);
    
    meinBruch.addiere(deinBruch);		
    System.out.println(meinBruch);
    
    meinBruch.subtrahiere(new Bruch(4,3));
    System.out.println(meinBruch);
  }

}

so erhalten wir die korrekte Ausgabe:

5 / 6
-1 / 2

Test auf Gleichheit mit equals

Wir wollen unserer Klasse Bruch eine etwas raffiniertere equals-Methode hinzufügen als in den Beispiel Auto. Das ist etwas für diejenigen, die sich schon recht sicher im Umgang mit dem bisher Gelernten fühlen. Beim ersten Durcharbeiten kannst Du diesen Abschnitt ggf. überspringen.


Wiedergabe stellt eine Verbindung zu YouTube her.

Bevor wir zum eigentlichen Test auf Gleichwertigkeit zweier Brüche kommen, wollen wir zunächst prüfen, ob

  • der Bruch, der zum Vergleich gegeben wird, nicht null ist, d.h., dass tatsächlich ein existierendes Objekt übergeben wird und
  • ob das gegebene Objekt wirklich von der Klasse Bruch ist.

Beim zweiten Punkt könntest Du einwenden, dass dies doch nicht nötig ist. Wenn wir in der equals-Methode als Parameter einen Bruch verlangen, wird es ja gar nicht zugelassen, dass dort etwas anderes landet. Unsere Methode wird hier aber allgemeiner ein beliebiges Objekt zum Vergleich entgegen nehmen:

public boolean equals(Object pVergleich){
  if (pVergleich == null){
    return false;
  }

  if (!(pVergleich instanceof Bruch)){
    return false;
  }

  if (pVergleich == this){
    return true;
  }

  Bruch hilfsobjekt = (Bruch) pVergleich;

  if (zaehler*hilfsobjekt.getNenner() == nenner*hilfsobjekt.getZaehler()){
    return true;
  }else{
    return false;
  }

}

Die este if-Abfrage prüft wie angekündigt, ob das Vergleichsobjekt tatsächlich schon erstellt wurde. Falls nicht, wird als Ergebnis sofort false zurück gegeben. Danach wird geprüft, ob es von der Klasse Bruch ist. Falls nicht, erhalten wir wieder false. Dann wird getestet, ob es einfach dasselbe Objekt ist, d.h., ob ein Bruch gerade mit sich selbst verglichen wird. Wenn ja, dann erhalten wir true.

Erst danach kommt der mathematische Vergleich. Dieser beruht auf folgender kleiner Rechnung:

    \[\frac{a}{b} = \frac{x}{y} \Leftrightarrow \frac{a\cdot y}{b \cdot y} = \frac{x \cdot b}{y \cdot b}\]

Wozu benötigen wir an dieser Stelle noch die Variable hilfsobjekt? Ein Problem ist, dass pVergleich als ein Objekt der Klasse Object deklariert ist. Das ist die oberste Oberklasse in Java überhaupt. Diese besitzt natürlich nicht unsere Methoden getZaehler() und getNenner(). Daher müssen wir das Objekt pVergleich erst manuell in Objekt der Klasse Bruch umwandeln – oder wie man auch sagt casten. Dass dies möglich ist, haben wir mit den if-Abfragen zuvor geprüft. Daher verlässt sich Java nun auf unsere Behauptung, dass pVergleich ein Bruch ist, was wir mit (Bruch) zum Ausdruck bringen.

Ist Dir das für den Moment zu kniffelig, kannst Du auch alternativ so vorgehen, dass Du als Parameter einen Bruch verlangst:

public boolean equals(Bruch pVergleich){
  if (pVergleich == null){
    return false;
  }

  if (!(pVergleich instanceof Bruch)){
    return false;
  }

  if (pVergleich == this){
    return true;
  }

  if (zaehler*pVergleich.getNenner() == nenner*pVergleich.getZaehler()){
    return true;
  }else{
    return false;
  }

}

Testen wir unsere Vergleichsmethode:

public class Bruchtest {

  public static void main(String[] args) {
    
    Bruch meinBruch = new Bruch(1,2);		
    Bruch deinBruch = new Bruch(1,3);
    Bruch keinBruch = null;
    
    
    System.out.println(meinBruch.equals(meinBruch));
    
    System.out.println(meinBruch.equals(new Bruch(2,4)));
    
    System.out.println(meinBruch.equals(deinBruch));
    
    System.out.println(meinBruch.equals(keinBruch));
    
  }

}

Wir erhalten wie gewünscht:

true
true
false
false

Halten wir zur Übersicht noch einmal die bisherige Klasse Bruch fest:

public class Bruch {

  // Attribute
  private int zaehler;
  private int nenner;

  // Kontruktor
  public Bruch(int pZaehler, int pNenner){
    if (pNenner == 0){
      System.out.println("Der Nenner darf nicht 0 sein.");
      System.out.println("Setze den Bruch auf 0.");
      zaehler = 0;
      nenner = 1;
    }else{
      zaehler = pZaehler;
      nenner = pNenner;
    }

    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  // Methoden	zur Manipulation des Objektes
  public void kuerze(){
    int faktor = ggT();
    zaehler = zaehler / faktor;
    nenner = nenner / faktor;
    if (nenner < 0){
      zaehler = -zaehler;
      nenner = -nenner;
    }
  }

  public void erweitere(int pFaktor){
    if (pFaktor != 0){
      zaehler = zaehler*pFaktor;
      nenner = nenner*pFaktor;
      if (nenner < 0){
        zaehler = -zaehler;
        nenner = -nenner;
      }
    }
  }
  
  public void multipliziere(Bruch pBruch){
    zaehler = zaehler*pBruch.getZaehler();
    nenner = nenner*pBruch.getNenner();
    kuerze();
  }

  public void dividiere(Bruch pBruch){
    if (!pBruch.istNull()){
      nenner = nenner*pBruch.getZaehler();
      zaehler = zaehler*pBruch.getNenner();
    }else{
      System.out.println("Durch 0 darf man nicht teilen!");
    }
    kuerze();
  }
  
  public void addiere(Bruch pBruch){
    zaehler = zaehler*pBruch.getNenner() + nenner*pBruch.getZaehler();
    nenner = nenner * pBruch.nenner;
    kuerze();
  }
  
  public void subtrahiere(Bruch pBruch){
    zaehler = zaehler*pBruch.getNenner() - nenner*pBruch.getZaehler();
    nenner = nenner * pBruch.nenner;
    kuerze();
  }

  // get-Methoden
  public int getZaehler() {
    return zaehler;
  }

  public int getNenner() {
    return nenner;
  }	
  
  public boolean istNull(){
    if (zaehler == 0){
      return true;
    }else{
      return false;
    }
  }

  // Ausgabe
  public String toString(){
    return zaehler +" / "+nenner;
  }

  // private Methode
  private int ggT(){

    int a = zaehler;
    int b = nenner;
    int h;

    while(b != 0){
      h = a%b;
      a = b;
      b = h;
    }

    return a;

  }
  
  // Vergleich
  public boolean equals(Object pVergleich){
    if (pVergleich == null){
      return false;
    }

    if (!(pVergleich instanceof Bruch)){
      return false;
    }

    if (pVergleich == this){
      return true;
    }

    Bruch hilfsobjekt = (Bruch) pVergleich;

    if (zaehler*hilfsobjekt.getNenner() == nenner*hilfsobjekt.getZaehler()){
      return true;
    }else{
      return false;
    }

  }

}

Einige weitere mögliche Methoden der Klasse Bruch

Wir haben nun eine im Grunde eine komplett einsetzbare Klasse Bruch erstellt. Dennoch können wir noch weitere Methoden ergänzen, um ihre Anwendung komfortabler zu gestalten. Vielleicht fallen Dir auch noch weitere Ergänzungsmöglichkeiten ein.


Wiedergabe stellt eine Verbindung zu YouTube her.

Zunächst könnten wir noch einen weiteren Konstruktor hinzufügen, der dazu dient, einen Bruch mit einem ganzzahligen Wert zu erstellen:

public Bruch(int pGanzzahligerWert){
  zaehler = pGanzzahligerWert;
  nenner = 1;
}

Dies ist möglich, da dieser Konstruktor im Gegensatz zum schon vorhandenen nur einen statt zwei int-Parametern verlangt.

Wir können auch Methoden zur Multiplikation mit und zur Division durch eine ganze Zahl ergänzen:

public void multipliziere(int pZahl){
  zaehler = zaehler*pZahl;
  kuerze();
}

public void dividiere(int pZahl){
  if (pZahl != 0){
    nenner = nenner*pZahl;
  }else{
    System.out.println("Durch 0 darf man nicht teilen!");
  }
  kuerze();
}

Diese Methoden verlangen als Parameter je einen int-Wert statt eines Bruches.

Schließlich ist vielleicht eine Möglichkeit zur Umwandlung in eine Dezimalzahl nützlich:

public double approx(){
  double hilfszaehler = zaehler;
  double hilfsnenner = nenner;

  return (hilfszaehler/hilfsnenner);
}

Diese Methode gibt den Wert des Bruches als double-Wert zurück. Der Bruch bleibt aber unverändert.

Wie oben bereits erwähnt, fallen Dir vielleicht noch weitere Ergänzungen ein.