目录
继承机制的使用可以复用一些定义好的类,减少重复代码的编写。多态机制的使用可以动态调整对象的调用,降低对象之间的依存关系。为了优化继承和多态,一些类除了继承父类外还可以使用接口形式。java中的类可以实现多个接口,接口被用来建立类与类之间的关联标准。
10.1类的继承
在前面介绍过,继承的基本思想是基于某个父类的扩展,制定出一个新的子类,子类可以继承父类原有的方法和属性,也可以增加父类不具备的属性和方法,或者直接重写父类的某些方法。
下面演示一下继承性。创建一个新类Test,同时创建另一个新类Test2继承Test类,其中包括重写的父类成员方法以及新增的成员方法。下图描述了两者之间的关系:
java中使用extends关键字来标识两个类之间的继承关系。
在子类中可以连同父类的构造方法来完成子类的初始化操作,既可以在子类的构造方法中使用super()语句调用父类的构造方法,也可以在子类中利用关键字super调用父类的成员方法,但是子类没有权限调用父类中被修饰为private的方法,只可以调用public或protected修饰的成员方法。
此外,继承并不仅仅是扩展父类的功能,还可以重写父类的成员方法。重写就是在子类中将父类的成员方法名称保留,重写成员方法的方法体或参数,再或者是更改方法的存储权限。
需要注意的是,在重写父类的方法时,修改方法的权限修饰符只能从小的范围到大的范围变化,例如父类的方法权限修饰符为protected,子类继承后的方法权限修饰符只能改为public,而不能修改为private.下面的重写关系就是错误的:
子类重写父类的方法还可以修改方法的返回值类型,但是这种重写方式要遵循一个原则,即重写后的返回值类型必须是父类同一方法返回值类型的子类。上例字,Test2正是Test类的子类。
在java中一切都是以对象的形式进行处理,在继承机制中,创建一个子类对象,将包含一个父类子对象,这个对象与父类创建的对象是一样的。两者的区别在于外部,父类的子对象来自子类对象的内部。当实例化子类对象时,父类对象也相应被实例化。换句话说,在实例化子类对象时,java编译器会在子类的构造方法中自动调用父类的无参构造方法。看下例;
从结果可以看出调用构造方法的顺序,先是顶层的父类,然后是上一级父类,最后才是子类的构造方法。也就是说在实例化子类对象时首先实例化父类的对象,然后再实例化子类对象,所以在子类构造方法访问父类构造方法之前,父类已完成实例化操作。
注意,在实例化子类对象时,父类的无参构造方法将被自动调用,但是有参构造方法不能被自动调用,只能依赖于super关键字显示的调用父类的有参构造方法。
如果使用finalize()方法对对象进行清理,需确保子类的finalize()方法的最后一个动作是调用父类的finalize()方法,以保证在回收垃圾对象时,对象的所有部分都能被正常终止。
10.2Object类
在java中,所有的类都直接或者间接继承了java.lang.Object类。这个类是比较特殊的类,是所有类的父类,是java类层中最高层类。由于所有的类都是Object的子类,所以在定义类时省略了extends Object关键字。如图:
由于所有的类都是Object类的子类,所以任何类都可以重写Object类的方法,除了那些被修饰为final的Object方法。下面详细介绍Object类的几个重要方法:
(1)getClass() 返回对象执行时的Class实例,然后使用此实例调用getName()方法可以取得类的名称。语法如下:
(2)toString() 将一个对象返回为字符串形式,它会返回一个String实例。在实际应用中通常重写toString()方法,为对象提供一个特定的输出模式。当这个类转换为字符串或字符串连接时,将会自动调用重写的toString()方法。
在本实例中重写toString方法,在子类的toString()方法中使用getClass()方法获取当前运行的类名,当用户打印对象时,将自动调用toString()方法。
(3)equals() 比较的是两个对象的实际内容
从本实例的结果可以看出,在自定义类中使用equals()方法进行比较时,将返回false,因为equals()方法默认实现时使用"=="运算符比较两个对象的地址,而不是比较对象的内容。所以要想真正做到比较两个对象的内容,需要在自定义类中重写equals()方法。
10.3对象类型的转换
对象类型的转换在java中经常遇到,主要包括向上转型和向下转型。
10.3.1向上转型
因为平行四边形是特殊的四边形,也就是说平行四边形是四边形的一种,那么就可以将平行四边形的对象看做是四边形的对象。
平行四边形继承了四边形,而四边形存在一个draw()方法,它的参数是四边形对象,而在平行四边形类中调用draw()时给予的参数类型却是平行四边形的对象,这里就是将子类对象赋值给父类类型的变量,这种技术叫做“向上转型”。
从上图可以看出,平行四边形继承了四边形,常规的继承图都是将父类放在顶部,然后子类逐渐往下,所以将子类对象看做是父类对象被称为“向上转型”。由于向上转型是从一个较具体的类到较抽象的类的转换,所以它总是安全的。如可以说平行四边形是四边形,但是不能说四边形是平行四边形。
10.3.2向下转型
可以说子类对象总是父类的一个实例,但是不能说父类对象时子类的实例。看下例:
可以看出,如果将父类对象直接赋给子类对象,编译器会报错,因为父类实例不一定是子类实例。从下图可以看出,越是具体的对象特性越多,越抽象的对象特性越少。在做向下转型操作时,将特性少的对象转换为特性多的对象时肯定会出问题,所以这时需要明确将父类对象强制转化为某个子类的对象,这种方式称为显示转换。
当在程序中使用向下转型时,必须使用显示类型转换,指明将父类转换为哪一种类型的子类对象。
10.4 使用instanceof操作符判断对象类型
当在程序中执行向下转型的操作时,如果父类对象并不是子类的实例,就会报错。所以在执行向下转型操作前,应该判断父类对象是否是子类的实例,这个判断通常使用instanceof操作符来完成,该操作符既可以判断某个对象是否是一个类的实例,也可以判断一个类是否实现了某个接口。语法格式如下:
操作符的返回值为boolean型,如果返回值为true,说明myobject对象为ExampleClass的实例对象;如果返回值为false,则不是。
请注意,instanceof为java的关键字,我们注意到java的关键字都是小写的,类名一般首字母大写。
10.5 方法的重载
前面学习过构造方法,因为构造方法的名称由类名决定,所以构造方法只有一个名称,但如果希望以不同的方式来实例化对象,就需要使用多个构造方法来完成。由于这些构造方法都需要根据类名来命名,为了让方法名相同而形参不同的构造方法同时存在,必须用到“方法重载”。
方法重载就是在同一个类中允许同时存在一个以上的同名方法,只要这些方法的参数个数或类型不同即可。
上例定义了5个同名方法,这5个方法构成了重载关系。下图表明了所有可以构成重载的条件:
方法的重载使得方法以统一的名称被管理,是程序代码有条理。在谈到参数个数可以确定两个方法是否有重载关系时,会想到定义不定长参数方法。
不定长参数方法的语法如下:
在参数列表中使用"..."形式定义不定长参数,其实这个不定长参数a就是一个数组,编译器会将(int...a)看做是(int[] a)。
10.6 多态
利用多态可以使程序具有良好的扩展性,并可以对所有类对象进行通用的处理。例如,定义一个四边形类,让它处理所有继承该类的对象,根据向上转型的原则可以使每个继承四边形类的子类对象作为draw()方法(原来的参数为四边形类)的参数,然后在该方法中做一些限定就可以根据不同的图形类对象绘制相应的图形,从而以更为通用的四边形类来取代具体的正方形类和平行四边形类,解决代码的冗余问题,同时也更加有利于维护。
从本例的结果看出,以不同类对象作为参数调用draw()方法可以处理不同的图形问题。使用多态节省了开发和维护时间,因为程序员无需在所有的子类中定义执行相同功能的方法,避免了大量重复代码的开发,同时只要实例化一个继承父类的子类对象即可调用相应的方法,这里只要维护父类中的方法即可。
10.7 抽象类与接口
10.7.1抽象类
在解决实际问题时,一般将父类定义为抽象类,需要使用这个父类进行继承和多态处理。在多态机制中,并不需要将父类实例化对象,我们需要的只是子类对象,所以在java中设置抽象类不可以实例化对象。抽象类的语法如下:
使用abatract关键字定义的类称为抽象类,而使用这个关键字定义的方法称为抽象方法。请注意,抽象方法没有方法体,这个方法本身没有任何意义,除非被重写,而承载这个抽象方法的抽象类必须被继承,实际上抽象类除了被继承之外没有任何意义。
反过来讲,如果声明一个抽象方法,就必须将承载这个抽象方法的类定义为抽象类,不可能在非抽象类中获取抽象方法。换句话说,只要类中有一个抽象方法,这个类就必须被定义为抽象类。
抽象类被继承后需要实现其中的所有抽象方法,也就是保证相同的方法名、参数列表和相同的返回值类型创建出非抽象方法,当然也可以是抽象方法。下图说明了抽象类的继承关系:
从上图中可以看出,继承抽象类的所有子类需要将抽象类中的所有抽象方法进行覆盖重写,这样在多态机制中,就可以将父类修改为抽象类,将draw()方法设置为抽象方法,然后每个子类都重写这个方法来处理。但是这样处理的话,程序中会有太多冗余代码,同时父类的局限性很大,也许某个不需要draw()方法的子类也不得不重写这个方法。如果将draw()方法放在另一个类中,让那些需要draw()方法的类继承该类,而不需要该方法的继承图形类,但是所有的子类都需要继承图形类,因为这些类都是从图形类中导出的,这样使得某些子类不得不继承多个父类。但是,java中规定一个子类只能有一个父类,也就是说类不能同时继承多个父类,面临这种问题,接口就出现了。
10.7.2接口
(1)接口简介
接口是抽象类的延伸,可以将它看做是纯粹的抽象类,接口中的所有方法都没有方法体。对于上小节遗留的问题,可以将draw()方法封装到一个接口中,使需要draw()方法的类实现这个接口,同时也继承图形类。下图描述了各个子类继承图形类后使用接口的关系:
接口使用interface关键字定义,其语法如下:
一个类实现一个接口可以使用implements关键字,代码如下:
在接口中定义的方法必须被定义为public或abstract形式,其他权限修饰符不被java编译器认可,即使不将该方法声明为public,它也是public。
此外,接口中定义的任何字段都自动是static和final的。
在本实例中,平行四边形类和正方形类分别实现了drawTest接口并继承了四边形类,所以需要覆盖接口中的方法。在调用draw()方法时,首先将两个子类向上转型为drawTest接口形式。这里请注意,java中将一个类向上转型为父类对象或实现接口,这都是没有问题的。
(2)接口与继承
java中不允许类与类之间的多重继承,但是使用接口就可以实现多重继承,因为一个类可以同时实现多个接口,但这可能在一个类中产生庞大的代码量,因为继承一个接口时需要实现接口中的所有方法。多重继承的语法如下: