For symbolic representation of mathematical expressions, I am trying to build a hierarchy of number system classes.
In addition to Integer
and Real
, I also need classes like Rational
and Complex
. I want all of these classes to inter-operate seamlessly with each other.
e.g. Adding a Complex
number to an Integer
would give a Complex
number etc.
I made all of them to implement the Number
interface. (NOT java.lang.Number
)
For being able to add numbers of different types, I tried making hierarchy like following.
Integer
extends Rational
extends Real
extends Complex
- This makes an
Integer
to unnecessarily store imaginary part etc. This overhead is undesired.
- Also allowing access to imaginary part of an
Integer
seems improper.
Can anyone suggest a better design where overhead is avoided and interoperation is still possible?
I'd rather create an interface that has something like getRealPart() and getImaginaryPart(). Then your integer can simply return 0 for getImaginaryPart(). That since you want Integer to "be" a Complex, but you don't want Integer to contain the internal implementation of Complex.
public interface Numberr {
public Numberr plus(Numberr n);
public Numberr minus(Numberr n);
public Numberr multiply(Numberr n);
public Numberr sqrt();
...
public Class<? extends Numberr> getType();
}
/////////////////////////////////////////////
public class Integerr implements Numberr {
protected BigInteger value;
@Override
public Numberr plus(Numberr n) {
if (n instanceof Integerr) {
return value.add(n.value);
} else {
// in case of more broad argument type, use method of that class
return n.plus(this);
}
}
....
}
///////////////////////////////////////////////
public class Rational implements Numberr {
protected BigInteger numerator;
protected BigInteger denominator;
@Override
public Numberr plus(Numberr n) {
if (n instance of Integerr) {
return new Rational(numerator.multiply(n.value), denominator);
} else if (n instanceof Rational) {
return new Rational(numerator.multiply(n.denominator).add(n.numerator.multiply(denominator)), denominator.multiply(n.denominator));
} else {
return n.plus(this);
}
}
....
}
I don't see a problem here. Real number is a complex number, integer is a real number. Complex number can be expressed as a + bi
and an integer is a complex number, such that a
is an integer and b = 0
. So every integer has b
and it is equal to 0.
You may however consider using composition (and interfaces) over inheritance:
interface Complex {
Real a();
Real b();
}
interface Real extends Complex {
@Override
default Real b() {
return new Integer(0);
}
}
class Integer implements Real {
public Integer(int value) {
// ...
}
@Override
public Real a() {
return this;
}
// ...
}
The disadvantage of this approach is that Integer
class can override b()
method, so maybe inheritance would be better, because you can use final
keyword on the method:
abstract class Complex {
abstract Real a();
abstract Real b();
}
abstract class Real extends Complex {
@Override
public final Real b() {
return new Integer(0);
}
}
class Integer extends Real {
public Integer(int value) {
// ...
}
@Override
public Real a() {
return this;
}
// ...
}
I have tried to model it myself and I came up with this terrible code below. I am not happy about it, because of the following problems:
Interface
- InterfaceImpl
antipattern
IntegerNumber
has methods such as realPart()
or numerator()
and denominator()
- some numbers (complex and rational) use other numbers, while others (real and integer) use Java primitives
Code:
public class Test {
public static void main(String[] args) {
ComplexNumber complexOne = new ComplexNumber(new RealNumber(1.25), new RealNumber(3));
ComplexNumber complexTwo = new ComplexNumber(new RealNumber(7), new RealNumber(18.875));
System.out.println("adding two complex numbers:");
System.out.println(complexOne.add(complexTwo));
RealNumber realOne = new RealNumber(15.125);
RealNumber realTwo = new RealNumber(7.375);
System.out.println("adding two real numbers:");
System.out.println(realOne.add(realTwo));
System.out.println(realTwo.add(realOne));
System.out.println("adding complex and real number:");
System.out.println(complexOne.add(realOne));
System.out.println(realOne.add(complexOne));
RationalNumber rationalOne = new RationalNumber(new IntegerNumber(1), new IntegerNumber(2));
RationalNumber rationalTwo = new RationalNumber(new IntegerNumber(1), new IntegerNumber(3));
System.out.println("adding two rational numbers:");
System.out.println(rationalOne.add(rationalTwo));
IntegerNumber integerOne = new IntegerNumber(6);
IntegerNumber integerTwo = new IntegerNumber(7);
System.out.println("adding two integers:");
System.out.println(integerOne.add(integerTwo));
System.out.println("adding real number and integer:");
System.out.println(integerOne.add(realOne));
System.out.println(realOne.add(integerOne));
System.out.println("adding complex number and integer:");
System.out.println(integerOne.add(complexOne));
System.out.println(complexOne.add(integerOne));
}
}
// interfaces
interface Complex {
Real realPart();
Real imaginaryPart();
default Complex add(Complex other) {
return new ComplexNumber(
this.realPart().add(other.realPart()),
this.imaginaryPart().add(other.imaginaryPart())
);
}
}
interface Real extends Complex {
double asDouble();
@Override
default Real imaginaryPart() {
return new IntegerNumber(0);
}
default Real add(Real other) {
return new RealNumber(this.asDouble() + other.asDouble());
}
}
interface Rational extends Real {
Integer numerator();
Integer denominator();
@Override
default Real realPart() {
return new RealNumber(1.0d * numerator().asInt() / denominator().asInt());
}
@Override
default double asDouble() {
return realPart().asDouble();
}
default Rational add(Rational other) {
return new RationalNumber(
this.numerator().multiply(other.denominator()).add(this.denominator().multiply(other.numerator())),
this.denominator().multiply(other.denominator())
);
}
}
interface Integer extends Rational {
int asInt();
@Override
default Integer numerator() {
return new IntegerNumber(asInt());
}
@Override
default Integer denominator() {
return new IntegerNumber(1);
}
default Integer add(Integer other) {
return new IntegerNumber(this.asInt() + other.asInt());
}
default Integer multiply(Integer other) {
return new IntegerNumber(this.asInt() * other.asInt());
}
}
// implementations
class ComplexNumber implements Complex {
private final Real realPart;
private final Real imaginaryPart;
public ComplexNumber(Real realPart, Real imaginaryPart) {
this.realPart = realPart;
this.imaginaryPart = imaginaryPart;
}
@Override
public Real realPart() {
return realPart;
}
@Override
public Real imaginaryPart() {
return imaginaryPart;
}
@Override
public String toString() {
return String.format("%s + %si", realPart, imaginaryPart);
}
}
class RealNumber implements Real {
private final double value;
public RealNumber(double value) {
this.value = value;
}
@Override
public Real realPart() {
return this;
}
@Override
public double asDouble() {
return value;
}
@Override
public String toString() {
return "" + value;
}
}
class RationalNumber implements Rational {
private final Integer numerator;
private final Integer denominator;
public RationalNumber(Integer numerator, Integer denominator) {
this.numerator = numerator;
this.denominator = denominator;
}
@Override
public Integer numerator() {
return numerator;
}
@Override
public Integer denominator() {
return denominator;
}
@Override
public String toString() {
return String.format("%s/%s", numerator, denominator);
}
}
class IntegerNumber implements Integer {
private final int value;
public IntegerNumber(int value) {
this.value = value;
}
@Override
public int asInt() {
return value;
}
@Override
public String toString() {
return "" + value;
}
}
I am wondering whether interfaces should be abstract classes with implemented methods being final. In the end, I think it may be better to just go with simple inheritance and ignore the fact that every integer will have a field for imaginary part.
I hope this will give you some ideas.