Previously on OOP:
In the last article, we have made the last example on object creation. Car class is stored in cars package, nevertheless, its objects were created in default package. To do this, we can either use fully qualified name or import cars package to this file.
创建实例的下一个实践内容就是继承,inheritance。本课程中最为典型的继承的例子是Employee, Manager, and CEO。另外一个经典的例子是Object class,它是所有类的父类。本黄鸭会在本文中重点讨论第二个例子。
我们先新建一个package,名字是arrays,然后复制一份default package中的Person class进入arrays package。当然,编译器会自动在Person类的开头加上一行package declaration的语句。这种做法是为了迎合本黄鸭昨天立的一个flag:既然强烈建立不要在default package中编写代码,所以从这篇文章开始,我们不会再在default package中编写代码。
现在,我们可以在arrays package中的Person类中增加如下代码:
Object class中一共有两个函数,其中一个是toString(),返回值是这个Object类的名称,concatenated with这个Object类的实例的hash code。本黄鸭在科普部分和理论部分都曾经重点讨论过hash函数,它具有uniformity and irreversible两个性质,所以人们以为hash code是random的,但是只是因为hash函数过于复杂,没有人能求得反函数。
所有Object类的子类,即所有类,都可以修改(overriding)toString()函数。如果method里面没有这个函数,则说明没有修改,继续沿用Object类的toString()函数。
在Person类中,只是打印出Person + hash code没有什么特别的意义,用户希望知道的是这个Person是谁,身份证号是多少,所以我们有充分的理由来overriding toString() method。
首先,我们看“@Override”这行代码。以“@”开头的所有代码都被称作为annotation,中文可以理解为“标注”。在“@”的后面是标注的内容,即说明这个method是被overridden的,不是凭空创造出来的。
那么有或者没有annotation这行代码有什么区别呢?Overridden一个函数的前提条件是:
(1)两个函数分别在ancestor class和precedent class中
(2)两个函数拥有完全相同的method signature。所谓method signature又由两个部分组成,一个是函数名,另外一个是ordered list of passing argument types。
只要上面这些条件有一个不满足,那么就不是overridden,而是声明了一个新的函数。为了保证上述条件都被满足,编译器为开发者提供了一个好办法,annotation。
在加上annotation的时候,如果Person类的toString()函数不满足overridden的条件,那么在编译的时候就会报出错误并给出修改意见。相对的,去掉annotation的话,编译器将不会有任何提示,如果Person类的toString()满足overridden的条件,那么会按照overridden来处理;如果Person类的toString()不满足overridden的条件,那么会按照声明了一个新的函数来处理。总之,两种其实都没有特别严重的问题,开发者可以自行选择。
本黄鸭的例子是有annotation这行代码的,那么toString()的函数头必须和Object class中的完全一致,不能擅自把返回值的类型从String改成Person。而Person类中的toString()的函数体可以自由发挥,比如上段代码中的返回一个由三个attributes构成的字符串。
最后一点,要把返回值的类型从String改成Person应该怎么办呢?这个toString()函数就会不满足overridden的条件,被视为一个新声明的函数。Annotation这行代码肯定是要去掉的,而且强烈建议把这个函数换一个名字,比如print()。
Object类中的另外一个函数是equal(),在Person类中也可以被overridden:
在函数头的前面有annotation,所以函数头不能随便发挥,必须和Object class中的一样,所以接收到的参数是Object类型的。又因为Object类是Person类的父类,所以想要把Object类cast到Person类属于downcast,有潜在风险,这个Object类不是Person,而是Duck。就好比Manager一定是Employee,但是Employee不一定全是Manager。总之,Object类不能直接与Person类比较,必须要先做类型转换。
Downcast的结果有三种:
(1)接收到的参数是一个空的reference,所以肯定不相等
(2)接收到的参数是Person类型,可以继续比较
(3)接收到的参数不是Person类型,所以肯定不相等
判断downcast成功与否,我们可以使用“instanceof”关键字,如上段代码所示。第一种情况可以和第三种情况合并,因为空指针也会被instanceof关键字认定为不能downcast。
有很多同学看到“this.SSN.equals(((Person)o).SSN);”,以为这是非常复杂的递归调用,觉得自己没有希望。其实不然,还是可以抢救一下的。里面只有一个函数,就是equals(),那么这个equals()是不是Person类的equals()函数呢?要看dotted notation前面的object reference的类型,即SSN的类型,即String类型。因此,这个equals()不是Person类的equals(),而是String类的equals()函数。换一句话说,不是Person类的equals()在调用自己,是Person类的equals()在调用别人,所以不是recursive调用。
最前面的“this.”可以写也可以不写,因为Person类的equals()函数的参数和Person类的attributes不重名,所以明确地知道SSN就是Person类attribute。
equals()函数的括号里面是参数,即“((Person)o).SSN”是参数。参数的本质是SSN,只是不是当前Person类实例的SSN,而是Object o的SSN,因为dotted notation的前面是reference。Object o前面还有一个括号“(Person)”,这是一种explicit的casting。
这样分析下来,“this.SSN.equals(((Person)o).SSN);”就非常清楚了,如果还有不清楚的,请移驾理论部分,重头开始学习。就像只有看懂别人的文章,自己才会写一样,读懂代码是一个绝对不能缺的技能。
欢迎使用本黄鸭编写的小程序~
微信公众号二维码:
领取专属 10元无门槛券
私享最新 技术干货