这节课以 BlueJ 的一个电子表为例:
新建一个ClockDisplay 的对象后, 点击它, 里面有一个实例变量:
String displayString.
现在显示的是 “00:00”, 也代表在现实中电子表会显示的字符串
在 Java 中, 我们要想跟 object 互动 就需要 使用 method:
里面只有三个 methods, 是个很简单的 interface.
getTime() 和 setTime(int hour, int minute) 就是我上文讲到的 accessor 和 mutator.
timeTick() 以某种方式被物理计时器调用. 在现实的时钟里会产生有规律的时间信号. 在这个例子当中, 我们每 call 一次这个方法, 电子表的显示就会增加一分钟.
现在我们来研究在 Java 中怎样用代码来实现这些.
Modularisation & Abstraction
如果说这是我们要做的东西:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5bdZtdqJ-1635579379516)(C:UsersLeeAppDataRoamingTyporatypora-user-imagesimage-20211029213958124.png)]
我们现在想一下怎么样来实现这样一个东西.
modularisation (模块化) 和 abstraction (抽象化) 的思想告诉我们要先把大个事物细分成小的部分.
就这个例子, 我们可以把这个四维数显示的电子表显示器看成 两个 两位数 的显示, 然后一个一个的去实现它们, 这样就容易多了.
时间的单位都是从"0"开始计时, 到某一个临界值就会归零.
public class NumberDispaly
{
private int limit;
private int value;
// Constructor and methods omitted.
}
按照这个想法, 我们只需要先把分钟做出来, 再照着模子把时钟做出来就行了
现在, 我们管整个四位数显示 “00:00” 叫 ClockDisplay, 管两个分别代表时, 分的两位显示叫 NumberDisplay.
在 NumberDisplay 中:
minute, 从"0" 走到 “59”, hour, 从"0" 走到 “23”
最低值永远是零, 但是最高值是不固定的, 所以在最高值这里不得不有一个 variable
在上面的代码中, 我们用 limit 来存储这个变量, 而 value 用来存储现在的时间. 假如说现在 ClockDisplay 显示的是 “22:03”, 在时针里, value 存储的就是 “22”, “03” 则被存储到分针中.
Class & Object
在深入 Constructor 和 method 之前, 我们继续探讨一下这个表的结构.
两个两位显示分别代表 hour 和 minute, 换句话说, hour 和 minute 都是 NumberDisplay 的 对象,
public class ClockDisplay
{
private NumberDisplay hours;
private NumberDisplay minutes;
// Constructor and methods omitted.构造器和方法略
}
有了两个 NumberDisplay, 就可以合成一个 ClockDisplay, 也就是我们最终要做的电子表.
在 ClockDisplay类 里存储两个变量分别代表 minute 和 hour 就是上面的代码.
在这里我们要注意到一个细节, “NumberDisplay” 在 field declaration (字段声明) 中出现了.
这里就体现了Java的强大之处: 在 Java 中 类 名是可以当作 数据类型 来使用的, 也就是说我们可以在 Java 中通过新建类的方式来创造数据类型.
或许, 数据类型 和 类 本质上是同一种东西吧.
现在我们有了两个类型为 NumberDisplay 的变量, 那就意味着这两个变量可以存储 NumberDisplay 的 对象. 所以, ClockDispaly类 的 对象 会在 实例变量 “hours” 和 “minutes” 中分别存储一个 NumberDisplay 的 对象.
这样说可能听不懂, 看图更直观一些:
这里有一个非常重要的细节需要提一下, 当一个 Java 对象存储另一个 Java对象时 (比如说这种情况), 它 并不是直接存储这个对象, 而是存储了这个对象的 reference (引用). 当我说 ClockDisplay 存储了一个 NumberDisplay 时. 如果我在适当的细节层次上发言,我实际上应该说的是,ClockDisplay 持有 NumberDisplay 的 “引用”. 再直白一点, 上图的 NumberDisplay 11 并不会被直接存储到白格子里.
如果你们当中有人学过 C++, 这种被另一个对象存储的对象引用在 C++ 中叫 “pointers”, 直译过来就是 “指针”. 它相较 Java 有一点儿不一样, 在 Java 中, 我们不叫它 “pointers” 而叫 “references” 是有原因的, Java 中的 reference 能做的事情是有限的. 而在 C++ 中, 一个对象是可以直接存储在另一个对象里的. 如果没有学过 C++ 请忽略这里.
在 Java 中, 对象可以且只能存储其他对象的引用. 原因我会在以后的博文里讲.
Overloading of Constructors
回归正题, 现在我们来看里面的代码.
先看 ClockDisplay 的构造器:
public class ClockDisplay
{
private NumberDisplay hours;
private NumberDisplay minutes;
private String displayString; // simulates the actual display 用来模拟真实的显示
public ClockDisplay()
{
hours = new NumberDisplay(24);
minutes = new NumberDisplay(60);
updateDisplay();
}
public ClockDisplay(int hour, int minute)
{
hours = new NumberDisplay(24);
minutes = new NumberDisplay(60);
setTime(hour, minute);
}
// methods omitted. 方法略
}
这里有一个有关构造器的重载的很好的例子.
不过我们这节课主要看 NumberDisplay, 呵呵.
public class NumberDisplay
{
private int limit;
private int value;
public NumberDisplay(int rollOverLimit)
{
limit = rollOverLimit;
value = 0;
}
public int getValue()
{
return value;
}
public String getDisplayValue()
{
if(value < 10) {
return "0" + value;
}
else {
return "" + value;
}
}
public void setValue(int replacementValue)
{
if((replacementValue >= 0) && (replacementValue < limit)) {
value = replacementValue;
}
}
public void increment()
{
value = value + 1;
if (value == limit) {
value = 0;
}
}
}
我们回到 BlueJ 外面来看一下:
getValue() 和 setValue(int replacementValue) 分别可以获取数据和更改数据 (一个用来看时间, 一个用来调时间).
void increment() 是用让时间往前走和管归零的.
Zero-padding
那么问题来了, getDisplayValue() 是用来干嘛的? 我们都有 getValue() 了, 我们为什么需要一个getDisplayValue() 呢?
答案是: “补零”.
现实中的电子表的时, 分, 秒,显示的数值永远都是两位数, 哪怕是个个位数也会用一个 “0” 补成两位数
如果我现在用 setValue() 把 minute 调成 “7”:
然后我再直接拿 getValue() 来看
是个整数. 如果我用这个来作为我的电子表的显示, 那它一定将是个稀有的
电子表, 因为只有这个电子表的个位没有补零, 如果现在是凌晨三点零七, 那么它就会显示 “3:7”, 但是我们想要的是 “03:07”.
在系统看来, 整数类型下的 “7” 和 “07” 的值是一样的, 它是不可能给你输出一个 “07” 的, 这也是 getValue() 和 getDisplayValue() 的区别:
如果数值小于 10 那么 getDisplayValue() 就会给它补一个零:
public String getDisplayValue()
{
if(value < 10) {
return "0" + value;
}
else {
return "" + value;
}
}
注意, 这里在 else {} 里加了一个空的字符串, 这里又回到了我之前写的 String Concatenation 的内容, 如果我删掉了这个空字符串, 这里将会报错:
value 本身是个 int, 可是这个方法返回的数据类型应该是 String, 这个肯定是不行的.
在 Java 中有很多转换数据类型的手段, 如果想要转换成 String 的话, 最简单的方法应该就是加个空字符串了.
modulo operator “%”
另一个我想给你们看的 method 是这个:
public void increment()
{
value = value + 1;
if (value == limit) {
value = 0;
}
}
将显示值增加1,如果达到限制,则滚动到零. 这个方法写的没有问题, 注释中提到的功能全部都具备, 但是有更简洁的写法:
在 Java 里, 有一种东西叫做 “modulo operator” (%)
以整数类型 int 举个例子, 18 ÷ 4 = 4 余 2, 那么 在 Java 中, 17 / 4 == 4; 17 % 4 == 2, “%” 这个符号就代表取除式的余数.
这个符号和 increment(), 有什么联系呢?
我们假设 m, n 是两个正整数, 花些时间来想一下 m % n 的取值范围是多少?你可能会有点儿懵, 不要想得太复杂.
我们知道余数最低不能低于零, 最高也得比被除数低一点.
一旦你明白了这一点, 你就可以用更精简的代码来实现这个方法:
public void increment()
{
value = (value + 1) % limit;
}
两种写法都没有问题. 不管你们选用哪种写法, 第一原则永远是 干净, 易读, 易理解. 其中, 易理解并没有一个明确的标准, 它是一个主观层面上的准则. 就比如说上面的代码, 如果说别人因为自己水平的原因看不懂这个行代码就说它不易理解那就太刻薄了. 如果是在初学阶段, if-statement 的写法确实更容易理解也比较可取, 但是对于普通的 Java 程序员来说, 第二种写法并不比第一种难理解, 而且第二种写法更聪明. 但是总想着让自己的代码更 “聪明” 的这种行为并不可取, 因为我们写代码不光是给自己写, 也要给自己的老师, 同学, 以及以后的同事看. 就好比我写的这些博客, 我承认我写这些是为了养成良好的写博客的习惯, 顺便巩固自己所学, 同时我也希望自己的博客能够被别人看到, 希望能够帮助其他学习计算机的晚辈, 希望能够得到前辈的指点.



