Case Study: The Rational Class

Paul Ngugi - Jun 17 - - Dev Community

This section shows how to design the Rational class for representing and processing rational numbers. A rational number has a numerator and a denominator in the form a/b, where a is the numerator and b the denominator. For example, 1/3, 3/4, and 10/4 are rational numbers.

A rational number cannot have a denominator of 0, but a numerator of 0 is fine. Every integer i is equivalent to a rational number i/1. Rational numbers are used in exact computations involving fractions—for example, 1/3 = 0.33333. . . . This number cannot be precisely represented in floating-point format using either the data type double or float. To obtain the exact result, we must use rational numbers.

Java provides data types for integers and floating-point numbers, but not for rational numbers. This section shows how to design a class to represent rational numbers.

Since rational numbers share many common features with integers and floating-point numbers, and Number is the root class for numeric wrapper classes, it is appropriate to define Rational as a subclass of Number. Since rational numbers are comparable, the Rational class should also implement the Comparable interface. Figure below illustrates the Rational class and its relationship to the Number class and the Comparable interface.

Image description

A rational number consists of a numerator and a denominator. There are many equivalent rational numbers—for example, 1/3 = 2/6 = 3/9 = 4/12. The numerator and the denominator of 1/3 have no common divisor except 1, so 1/3 is said to be in lowest terms.

To reduce a rational number to its lowest terms, you need to find the greatest common divisor (GCD) of the absolute values of its numerator and denominator, then divide both the numerator and denominator by this value. You can use the method for computing the GCD of two integers n and d, as suggested in here, GreatestCommonDivisor.java. The numerator and denominator in a Rational object are reduced to their lowest terms.
As usual, let us first write a test program to create two Rational objects and test its methods. The program below is a test program.

Image description

The main method creates two rational numbers, r1 and r2 (lines 7–8), and displays the results of r1 + r2, r1 - r2, r1 x r2, and r1 / r2 (lines 11–14). To perform r1 + r2, invoke r1.add(r2) to return a new Rational object. Similarly, invoke r1.subtract(r2) for r1 - r2, r1.multiply(r2) for r1 x r2, and r1.divide(r2) for r1 / r2.

The doubleValue() method displays the double value of r2 (line 15). The doubleValue() method is defined in java.lang.Number and overridden in Rational.

Note that when a string is concatenated with an object using the plus sign (+), the object’s string representation from the toString() method is used to concatenate with the string. So r1 + " + " + r2 + " = " + r1.add(r2) is equivalent to r1.toString() + " + " + r2.toString() + " = " + r1.add(r2).toString(). The Rational class is implemented in the program below.



package demo;

public class Rational extends Number implements Comparable<Rational> {
    // Data fields for numerator and denominator
    private long numerator = 0;
    private long denominator = 1;

    /** Construct a rational with default properties */
    public Rational() {
        this(0, 1);
    }

    /** Construct a rational with specified numerator and denominator */
    public Rational(long numerator, long denominator) {
        long gcd = gcd(numerator, denominator);
        this.numerator = ((denominator > 0) ? 1 : -1) * numerator / gcd;
        this.denominator = Math.abs(denominator) / gcd;
    }

    /** Find GCD of two numbers */
    private static long gcd(long n, long d) {
        long n1 = Math.abs(n);
        long n2 = Math.abs(d);
        int gcd = 1;

        for(int k = 1; k <= n1 && k <= n2; k++) {
            if(n1 % k == 0 && n2 % k == 0)
                gcd = k;
        }

        return gcd;
    }

    /** Return numerator */
    public long getNumerator() {
        return numerator;
    }

    /** Return denominator */
    public long getDenominator() {
        return denominator;
    }

    /** Add a rational number to this rational */
    public Rational add(Rational secondRational) {
        long n = numerator * secondRational.getDenominator() + denominator * secondRational.getNumerator();
        long d = denominator * secondRational.getDenominator();
        return new Rational(n, d);
    }

    /** Subtract a rational number to this rational */
    public Rational subtract(Rational secondRational) {
        long n = numerator * secondRational.getDenominator() - denominator * secondRational.getNumerator();
        long d = denominator * secondRational.getDenominator();
        return new Rational(n, d);
    }

    /** Multiply a rational number to this rational */
    public Rational multiply(Rational secondRational) {
        long n = numerator * secondRational.getNumerator();
        long d = denominator * secondRational.getDenominator();
        return new Rational(n, d);
    }

    /** Divide a rational number to this rational */
    public Rational divide(Rational secondRational) {
        long n = numerator * secondRational.getDenominator();
        long d = denominator * secondRational.numerator;
        return new Rational(n, d);
    }

    @Override
    public String toString() {
        if(denominator == 1)
            return numerator + "";
        else
            return numerator + "/" + denominator;
    }

    @Override // Override the equals method in the Object class
    public boolean equals(Object other) {
        if((this.subtract((Rational)(other))).getNumerator() == 0)
            return true;
        else
            return false;
    }

    @Override // Implement the abstract intValue method in Number
    public int intValue() {
        return (int)doubleValue();
    }

    @Override // Implement the abstract floatValue method in Number
    public float floatValue() {
        return (float)doubleValue();
    }

    @Override // Implement the abstract doubleValue method in Number
    public double doubleValue() {
        return numerator * 1.0 / denominator;
    }

    @Override // Implement the abstract longValue method in Number
    public long longValue() {
        return (long)doubleValue();
    }

    @Override // Implement the compareTo method in Comparable
    public int compareTo(Rational o) {
        if(this.subtract(o).getNumerator() > 0)
            return 1;
        else if(this.subtract(o).getNumerator() < 0)
            return -1;
        else
            return 0;
    }
}



Enter fullscreen mode Exit fullscreen mode

The rational number is encapsulated in a Rational object. Internally, a rational number is represented in its lowest terms (line 15), and the numerator determines its sign (line 16). The denominator is always positive (line 17).

The gcd method (lines 21–32 in the Rational class) is private; it is not intended for use by clients. The gcd method is only for internal use by the Rational class. The gcd method is also static, since it is not dependent on any particular Rational object.

The abs(x) method (lines 22–23 in the Rational class) is defined in the Math class and returns the absolute value of x.

Two Rational objects can interact with each other to perform add, subtract, multiply, and divide operations. These methods return a new Rational object (lines 45–70).

The methods toString and equals in the Object class are overridden in the Rational class (lines 72–86). The toString() method returns a string representation of a Rational object in the form numerator/denominator, or simply numerator if denominator is 1. The equals(Object other) method returns true if this rational number is equal to the other rational number.

The abstract methods intValue, longValue, floatValue, and doubleValue in the Number class are implemented in the Rational class (lines 88–106). These methods return the int, long, float, and double value for this rational number.

The compareTo(Rational other) method in the Comparable interface is implemented in the Rational class (lines 108–116) to compare this rational number to the other rational number.

The getter methods for the properties numerator and denominator are provided in the Rational class, but the setter methods are not provided, so, once a Rational object is created, its contents cannot be changed. The Rational class is immutable. The String class and the wrapper classes for primitive type values are also immutable.

The numerator and denominator are represented using two variables. It is possible to use an array of two integers to represent the numerator and denominator. The signatures of the public methods in the Rational class are not changed, although the internal representation of a rational number is changed. This is a good example to illustrate the idea that the data fields of a class should be kept private so as to encapsulate the implementation of the class from the use of the class.

The Rational class has serious limitations and can easily overflow. For example, the following code will display an incorrect result, because the denominator is too large.

public class Test {
public static void main(String[] args) {
Rational r1 = new Rational(1, 123456789);
Rational r2 = new Rational(1, 123456789);
Rational r3 = new Rational(1, 123456789);
System.out.println("r1 * r2 * r3 is " +
r1.multiply(r2.multiply(r3)));
}
}

r1 * r2 * r3 is -1/2204193661661244627

To fix it, you can implement the Rational class using the BigInteger for numerator and denominator.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .