需求分析
有理数是整数(正整数、0、负整数)和分数的统称,是整数和分数的集合。由于有理数的子集分别是整数和分数,因此对于类的属性的设计,需要提供满足这两种数的表达方式。由于有理数本质上是数,因此有理数类需要实现数能做的事,也就是基本的四则运算。当然可以拓展其他基本运算,例如取绝对值和判断正负号。又由于有理数本质上是数,是数就有大小之分,因此实现有理数的比较非常重要。equals() 方法是每个类都可能需要频繁用到的方法,它是从 Object 类继承下来的方法,尤其是对于大小特征鲜明的数字,该方法也很有必要实现。因为在某些情景下,需要把有理数转换为其他数据类型支持更多操作,因此需要提供数制转换器。最后再加上必要的属性访问器和 toString() 方法,有理数类编写的需求思维导图如下:
设计一个类的一些建议如下:
保证数据私有,不要破坏封装性;
数据都要初始化,不应当以来 Java 的默认值;
不要在类中使用过多的基本类型,而是用其他类来替换;
并不是所有的属性都需要单独的属性访问器和属性更改器;
分解有过多职责的类,例如类可以明显地分为两个简单的类;
类名和方法要尽量体现它们的职责;
优先使用不可变类,即没有方法可以修改对象状态的类。
类的定义
有理数类应该是一个轮子类,它会被广泛应用于一些需要有理数承载数据的地方。将方法或类声明为 final 可以确保其不会在子类中改变语义,例如 String 类这种不可变类就要注意避免这种事发生,Rational 类同理。因为我们希望更方便地进行有理数的大小比较,并且希望兼容 Arrays 的 sort() 方法进行排序,因此此处用 implements 关键字启用 Comparable 接口。
public final class Rational implements Comparable
类的属性
根据需求,设计类的属性时需要考虑整数和分数。不过整数也可以认为是分数的子集,因为整数可以看作是分母为 1 的分数。因此这里只需要 2 个属性分别表示分子和分母,不需要更多的属性或者状态变量来进行管理。
为了保证数据私有,不要破坏封装性,此处将所有类的属性的访问修饰符都设置为 private。同时我选择将实例属性定义为 final 属性,这样的属性必须在构造对象是初始化,并且之后不能再修改。final 修饰符对于类型为基本类型或者不可变类属性很有用,例如 String 类就是个不可变类,而有理数类是类似于 String 类起到轮子功能的类。
private final int numerator;
private final int denominator;
我们考虑一个重要的问题,用户永远无法输入无限不循环小数,我们是否需要考虑接收用户的小数呢(有限位数的小数是有理数)?无论数学上还是理论上可行,此处我的个人建议是绝对不要这么做。做到这一点并不难,比如说把分子改为是诸如 double 类型的属性,然后分母统一设置为 1。用户没办法表示一个无限不循环小数,因为用户输入的小数始终会是有限个的,不过用户也可以用表达式注入参数,此时我们无法保证某个表达式的计算结果不会是无理数。
实践经验告诉我们,永远不要相信所有用户,不要天真地希望他们会按照开发者的心意来使用工具。如果我们这里开放了有理数的小数表示,则会留下别有用心或者胡来的用户注入无理数等奇怪的东西,从而使得我们的“有理数类”逻辑不清,定语形同虚设,这个类就是个失败的类。所以这里我们就把属性写死了,不允许任何浮点数被输入。
构造方法
由于用户输入的数字可能是整数或小数,因此我们需要重载一些类的构造方法。
Rational(int num) 方法
该方法对应的是最简单的情况,即用户输入的是个整数,整数一定是有理数了。
//输入整数的构造方法
public Rational(int num) {
this.numerator = num;
this.denominator = 1;
}
Rational(int numerator, int denominator) 方法
该方法对应的是分数形式的有理数,方法传入该有理数的分子和分母,分别将其赋值给对应属性。注意此时传入的分母不能为 0,否则这是个不合法的输入,应该主动抛出一个异常告知类的使用者。
//输入分子和分母的构造方法
public Rational(int numerator, int denominator) throws IllegalArgumentException{
if(denominator == 0) { //不合法有理数判断,分母不为 0
throw new IllegalArgumentException("分母是个不合法的参数");
}
int gcd = getGCD(numerator, denominator); //取分子和分母的最大公约数,用于化简
this.numerator = numerator / gcd;
this.denominator = denominator / gcd;
}
注意到我们使用了 throws 关键字进行异常规范声明,展示这个方法可能抛出的异常。我们选择抛出的异常是 IllegalArgumentException 异常,这个是 Exception 类分支下的不合法的参数异常类。
Rational(String str) 方法
这是最令人头疼而难办的方法,因为类通常会重载一个支持用字符串进行初始化的构造方法,以满足不同的需求(例如从文件流中读入数据创建对象)。但是参数是字符串意味着用户可以将任何内容以字符串的形式注入构造方法,上面的需求写到了,我们定义的 Rational 类仅支持整数形式和分数形式 2 种表示方法。也就是说我们需要判断字符串是否是我们所期望的输入形式,如果是其他恶意的输入,我们的方法应当主动地过滤掉并抛出异常。
//输入字符串的构造方法
public Rational(String str) throws IllegalArgumentException{
int idx = 0;
if(isInteger(str)){ //检测是否是整数型输入
this.numerator = Integer.valueOf(str);
this.denominator = 1;
}
else if(str.indexOf("/") != -1){ //检测是否是分数型输入
String numerator_str = str.substring(0, str.indexOf("/"));
String denominator_str = str.substring(str.indexOf("/") + 1, str.length());
if(isInteger(numerator_str) && isInteger(denominator_str)){ //判断除号两边是否是整数
int numerator = Integer.valueOf(numerator_str);
int denominator = Integer.valueOf(denominator_str);