继承和多态是面向对象开发中非常重要的一组概念。它使整个程序架构具有一定的弹性。在程序中复用一些已经定义完善的类,不仅可以减少软件开发周期,也可以提高软件的可维护性和可扩展性。
在Java语言中,一个类继承另一个类需要使用关键字extends,关键字extends的使用方法如下:
class Child extends Parent { }
因为Java只支持单继承,即一个类只能有一个父类,所以类似下面的代码是错误的:
class Child extends Parent1,Parent2 { }
子类在继承父类之后,创建子类对象的同时也会调用父类的构造方法。
例题1:创建子类对象,观察构造方法执行顺序
父类Parent和子类Child都有各自有一个无参的构造方法,在main()方法中创建子类对象时,Java虚拟机会先执行父类的构造方法,任何再执行子类的构造方法。
第一步
第二步
第三步
子类继承父类之后可以调用父类创建好的属性和方法。
例题2:在电话类基础上衍生出手机类
Telephone电话类作为父类衍生出Moblie手机类,手机类可以直接使用电话类的按键属性和拨打电话行为。
第一步
第三步
子类Moblie类仅创建了一个显示屏属性,剩余的其他属性和方法都是从父类Telephone类中继承的。
Java虽然不允许同时继承两个父类,但不代表没有多继承的关系,可以通过类似“祖父>父>儿子>孙子”的方式实现多代继承。
例如,绝大多数动物有眼睛、鼻子和嘴,犬类继承动物类,所以犬类也有眼睛、鼻子和嘴,哈士奇是一个犬类的品种,犬类有的特性哈士奇类都有。但哈士奇的眼睛、鼻子和嘴并不是从犬类继承的,而是从动物类继承的,用Java代码编写则如下:
class Animal { //父类:动物类
Eye eye;
Mouth mouth;
Nose nose;
}
class Dog extends Animal { } //子类:犬类
class Husky extends Dog { } //孙子类:哈士奇类
这三个类的继承关系就是Husky类继承Dog类继承Animal类,虽然Husky类没有直接继承Animal类,但是Husky类可以调用Animal类提供的可被继承的成员变量和方法。
(1)在开始学习使用class关键字定义类时,就应用到了继承原理,因为在java中所有的类都直接或间接继承了Java.lang.Object类。
(2)Object类是比较特殊的类,它是所有类的父类,是Java类层中的最高层类。用户创建一个类时,除非已经指定要从其他类继承,否则它就是从Java.lang.Object类继承而来的。
(3)Java中的每一个类都源于java.lang.Object类,如String类、Integer类都是继承于Object类。除此之外,自定义的类也都属于Object类。由于所有类都是Object类的子类,所以在定义类时可省略extendsObject,如下代码所示
class Anything {
...
}
||等价于
class Anything extends Object {
...
}
在Object类中,主要包括clone()、finalize()、equals()、toString()等方法,其中常用的两个方法为equals()和toString()方法。由于所有的类都是Object类的子类,所以任何类都可以重写Object类中的方法去。
注意:Object类中的getClass()、notify()、notifyAll()、wait()等方法不能被重写,因为这些方法被定义为final类型。
下面详细讲述Object类中的几个重要方法。
1.getClass()方法
getClass()方法是Object类定义的方法,它会返回对象执行时Class实例,然后使用此实例调用getName()方法可以取得类的名称。语法如下:
getClass().gentname();
可以将getClass()方法与toString()方法联合使用。
2.toString()方法
toString()方法的功能是将一个对象返回为字符串形式,它会返回一个String实例。在实际的应用通常重写toString()方法,为对象提供一个特定的输出模式。当这个类转换为字符串或与字符串连接时,将自动调用重写的toString()方法。
例题:让学生自我介绍
创建Student类,重写toString()方法,使该类的对象可以定义输出自己的姓名和年龄。
3.equals()方法
在java语言中,有两种比较对象的方式,分别为“==”运算符与equals()方法。两者的区别在于:“==”比较的是两个对象引用内存地址是否相等,而equals()方法比较的是两个对象的实际内容。
例题:根据身份证号判断是否为同一个人
为people类创建身份证号和姓名两个属性,重写equals()方法,仅以身份证号作为区分条件。创建n个people对象,用equals()方法和“==”运算符来判断是否存在多个对象代表同一个人。
执行代码:
运行结果:
从这个结果可以看出,“tom”和“汤姆”虽然名字不同,但是两者的身份证号相同,所以equals()方法判断出了这两个对象实际上是同一个,而“==”运算符无法做出有效判断。如果两个对象的身份证号不同,或者两个对象类型不同,equals()方法就会认为两者不是同一个人。
向上转型的转换可以被理解为将子类类型的对象转换为父类类型的对象,即把子类类型的对象直接赋值给父类类型对象,进而实现按照父类描述子类的效果。
例题:tom是谁?
使用向上转型模拟如下场景:这里有一个人,他叫tom,他是一名教师。
class People { }
class Teacher extends People { }
public class Demo3 {
public static viod main(String [] args) {
People tom = new Teacher();
}
}
在上述代码中,“People tom = new Teacher();”运用了向上转型的语法,理解方式如下图:
综上所述,因为人类(People)是教师类(Teacher)的父类,所以通过向上转型,能够把教师类(Teacher)类的对象(new Teacher();)直接赋值给人类(People)类型的变量(tom)。也就是说,进行向上转型,父类类型的对象可以引用子类类型对象。而且向上转型是安全的,因为向上转型是将一个较具体的类的对象转换为一个抽象的类的对象。例如,可以说平行四边形是四边形,但不能说四边形是平行四边形。
向上转型简单概括:
* 向上转型:
* 将子类对象赋值给父类引用
* 自动类型转换
* Animal a = new Dog();
向下转型可以被理解为将父类类型的对象转换为子类类型的对象。但是,运用向下转型,如果把一个较抽象的类的对象转换为一个较具体的类的对象,这样的转型通常会出现错误。例如可以说一只鸽子是鸟,却不能说某只鸟是一只鸽子。因为鸽子是具体的,鸟是抽象的。一只鸟除了可能是鸽子,还有可能是老鹰,麻雀等。因此,向下转型是不安全的。
例题:谁是鸽子?
编写代码证明“可以说某只鸽子是一只鸟,却不能说某只鸟是一只鸽子”:鸟类是鸽子的父类,用Brid表示鸟类,用Pigeon表示鸽子类。
修改前的代码:
class Brid {}
class Pigeon extends Brid{}
public class Demo4{
public static void main(String[] args) {
Brid brid = new Pigeon();//某只鸽子是一只鸟
Pigeon pigeon = brid;//某只鸟是一只鸽子
}
}
修改后的代码:
class Brid {}
class Pigeon extends Brid{}
public class Demo4{
public static void main(String[] args) {
Brid brid = new Brid();//某只鸽子是一只鸟
Pigeon pigeon = (Pigeon) brid;//某只鸟是一只鸽子
}
}
本次代码在运行之前,Eclipse会报出如下图的编译错误,这是因为父类对象不能直接赋值给予类对象。
如果想要告诉编译器“某只鸟就是一只鸽子”,应该如何修正?答案就是强制类型转换。语法如下:
子类类型 子类对象 = (子类类型)父类对象;
因此,要想实现把对象鸟类对象转换为鸽子类对象,就必须强制类型转换,代码如下:
Pigeon pigeon = (Pigeon)brid; //通过强制类型转换,告诉编译器“某只鸟就是一只鸽子”
注意:
(1)两个没有继承关系的对象不可以进行向上转型或向下转型。
(2)父类对象可以强制转换为子类对象,但是有一个前提:这个父类对象要先引用这个子类对象。
如果把上述实例中的代码:
Brid brid = new Pigeon(); //某只鸽子是一只鸟
Pigeon pigeon = (Pigeon)brid; //通过强制类型转换,告诉编译器“某只鸟就是一只鸽子”
修改为如下代码:
Brid brid = new Brid(); //某只鸽子是一只鸟
Pigeon pigeon = (Pigeon)brid; //通过强制类型转换,告诉编译器“某只鸟就是一只鸽子”
虽然Eclipse没有提示编译错误,但是运行程序后,控制台将输出如下错误信息:
Exception in thread "main" java.lang.ClassCastException: class Brid cannot be cast to class pigeon
向下转型简单概括:
* 向下转型:
* 将父类对象赋值给子类引用
* 需要进行强制类型转换
* Dog a = (Dog) new animal();
当在程序中执行向下转型操作时,如果父类对象不是子类对象的实例,就会发生ClassCaseException异常,所以在执行向下转型之前需要养成一个良好的习惯,就是判断父类对象是否为子类对象的实例。这个判断通常使用instanceof关键字来完成。可以使用instanceof关键字判断是否一个类实现了某个接口,也可以用它来判断一个实例对象是否属于一个类。
instanceof的语法格式如下:
myobject instanceof ExampleClass
myobject:某类的对象引用。
ExampleClass:某个类。
使用instanceof关键字的表达式返回值为布尔值。如果返回值为true,说明myobject对象为ExampleClass的实例对象;如果返回值为false,说明myobject对象不是ExampleClass的实例对象。
误区警示:instanceof是java语言中的关键字,Java语言中的关键字都为小写。
下面来看一个向下转型与instanceof关键字结合的例子。
例题:分析几何图形之间的继承关系
创建Quadrangle四边形类、Square正方形类和Circular圆形类。其中,Square类继承Quadrangle类,在主方法中分别创建这些类的对象,然后使用instanceof关键字判断它们的类型并输出结果。
本实例在运行之前,Eclipse就会报出如下图所示的编译错误。
因为四边形类与圆形类没有继承关系,因此两者不能使用instanceof关键字进行比较,否则会发生“不兼容”错误。如果删除或注释掉这行代码,运行结果如下:
flase
ture
方法的重载就是在同一个类中允许存在一个以上的同名方法,只要这些方法的参数个数或类型不同即可。为了更好的解释重载,来看下面的实例。
例题1:编写不同形式的加法运算方法
创建OverLoadTest类,在该类中编写add()方法的多个重载形式,然后在主方法中分别输出这些方法的返回值。
在本实例中分别定义了五个方法,在这5个方法中,前两个方法的参数类型不同,并且方法的返回值类型也不同,所以这两个方法构成了重载关系;前两个方法与第三个方法相比,第3个方法的参数个数少于前两个方法,所以这3个方法也构成了重载关系;最后两个方法相比,发现除了参数的出现顺序不同,其他都相同,同样构成了重载关系。下图表明了所有可以构成了重载的条件。
误区警示:虽然在方法重载中可以使用两个方法的返回类型不同,但只有返回类型不同并不足以区分两个方法的重载,还需要通过参数的个数以及参数的类型来设置。
根据上图所示的构造方法重载的条件,可以总结出编译器是利用方法名、方法各参数类型和参数的个数、参数的顺序来确定类中的方法是否唯一。方法的重载使得方法以统一的名称被管理,使程序代码更有条理。
在谈到参数个数可以确定两个方法是具有重载关系时,会想到定义不定长参数方法。不定长方法的语法如下:
返回值 方法名 (参数数据类型···参数名称)
如果将上述代码放在上面的例题1,关键代码如例题2所示
例题2:使用不定长参数重载加法运算方法
创建OverLoadTest2类,在该类中编写add()方法的多种重载形式,并编写该方法的不定长参数形式。然后在主方法中调用这些重载方法,并输出返回值。
运行结果:
在上述实例中,在参数列表中使用“···”形式定义不定长参数,其实这个不定长参数a就是一个数组,编译器会将“int···a”这种形式看作是“int[] a”,所以在add()方法体做累加操作时使用到了for循环语句,在循环中是根据数组a的长度作为循环条件的,最后将累加结果返回。
final被译为“最后的”,“最终的”,final是java语言中的一个关键字,凡是被final关键字修饰过的内容都是不可改变的。
final关键字可用于变量声明,一旦该变量被设定,就不可以再改变该变量的值。通常,由final关键定义的变量为常量。例如,在类中定义PI值,可以使用如下语句:
final double PI = 3.14;
当在程序中使用到PI这个常量时,它的值就是3.14.如果在程序中再次对定义为final的常量赋值,编译器将不会接受。
final关键字定义的变量必须在声明时对其进行赋值操作。final除了可以修饰基本数据类型的常量,还可以修饰对象引用。由于数组也可以被看作一个对象来引用,所以final可以修饰数组。一旦一个对象引用被修饰符为final后,它就只能恒定指向一个对象,无法将其改变以指向另一个对象。一个既是static又是final的字段只占据一段不能改变的存储空间。
例题:定义不允许被修改的产量Π
创建FinalDate类,在该类中定义表示圆周率的常量PI,并尝试修改PI的值。
本实例在运行之前,Eclipse就会报出下图错误。常量PI不允许被修改。
将方法定义为final类型,可以防止子类修改父类的的定义与实现方式,同时定义为final的方法的执行效率要高于非final方法。在修饰权限中曾经提到过private修饰符,如果一个父类的某个方法被设置为private的方法再定义为final类型。例如下面的语句:
private final void test(){
```//省略一些程序代码
}
例题:使用final关键字为电视机上儿童锁
创建Dad爸爸类,给Dad类定义一个打开电视机的方法,该方法使用final关键字修饰。创建Dad类的子类Baby类,让Baby类尝试自己重写打开电视的方法。
本实例在运行之前,Eclipse就会报出如下图所示的错误。因为打开电视这个方法是由final修饰的,子类无法重写。所以Baby想要打开电视,就只能找爸爸来打开了。
final方法无法被重写
定义为final的类不能被继承。如果希望一个类不被任何类继承,并不允许其他人对这个类进行任何改动,可以将这个类设置为final类。final类的语法如下
final 类名{}
如果将某个类设置为final类,则该类中的所有方法都隐式设置为final方法,但是final类中的成员变量可以被定义为final或非final形式。
例如,已知jdk中的Java.lang包下的Math数学类和String字符串类都是由final关键字修饰的类,这两个类就无法做任何类的父类,如果这两个类出现在extends右侧就会发生编译错误,如下图。
利用多态可以使程序具有良好的扩展性,并可以将所有类对象进行通用的处理。
假如现在要编写一个绘制图形的方法draw(),如果传入正方形对象就绘制正方形,如果传入圆形对象就会绘制圆形,这种场景可以使用重载来实现,定义如下:
public void draw(Square s){ //绘制正方形的方法
}
public void draw(Circular c){ //绘制圆形的方法
}
但是这种写法有个问题:正方形和圆形都是图形,这场景细分的重载方式不仅增加了代码量,还降低了“易用度”。如果定义一个图形类,让它处理所有继承该类的对象,根据“向上转型”原则可以使每个继承图形类的对象作为draw()方法的参数,然后在draw()方法中做一些限定就可以根据不同图形类对象绘制相应的图形。这样处理能够很好地解决代码冗余问题,同时程序也易于维护。
例题:万能的绘图方法
创建Shape图形类,作为Square正方形类和Circular圆形类的父类。创建Demo6类,并在该类中创建一个绘图用的draw()方法,参数为Shape类型,任何Shape类的子类对象都可以作为方法的参数,并且方法会根据参数的类型绘制相应的图形。
从本实例的运行结果中可以看出,以不同类对象为参数调用draw()方法,可以处理不同的图形绘制问题。使用多态节省了开发和维护时间,因为程序员无须在所有的子类中定义执行相同功能的方法,避免了大量重复代码的编写,同时,只要实例化一个继承父类的子类对象,即可调用相应的方法,如果需求发生了变更,只需维护一个draw()方法即可。
在解决实际问题时,一般将父类定义为抽象类,需要使用这个父类进行基础与多态处理。继承树中越是在上方的类越抽象,如鸽子类继承鸟类、鸟类继承动物类等。在多态机制中,并不需要将父类初始化为对象,所以在java语言中设置抽象类不可以实例化为对象。
使用abstract关键字定义的类称为抽象类,而使用这个关键字定义的方法称为抽象方法。抽象方法没有方法体,这个方法本身没有任何意义,除非它被重写,而承载这个抽象方法的抽象类必须被继承。实际上抽象类除了被继承没有任何意义。定义抽象类的语法如下:
public abstract class Parent{
abstract void testAbstract(); //定义抽象方法
}
反过来讲,如果声明一个抽象方法,就必须将承载这个抽象方法的类定义为抽象类,不能在非抽象类中获取抽象方法。就是,只要有一个抽象方法,此类就被标记为抽象类。
抽象类被继承后需要实现其中所有抽象方法,也就是保证以相同的方法名称、参数列表和返回值类型创建出抽象方法,当然也可以是抽象方法。如下图说明了抽象类的继承关系。
从上图中可以看出,继承抽象类的所有子类需要将抽象类中的抽象方法进行覆盖。这样在多态机制中,就可以将父类修改为抽象类,将 draw0方法设置为抽象方法,然后每个子类都重写这个方法来处理。但这又会出现我们刚探讨多态时讨论的问题,程序中会有太多冗余的代码,同时这样的父类局限性很大,也许某个不需要 draw0方法的子类也不得不重写 draw0方法。如果将 draw0方法放置在另外一个类中,让那些需要 draw0方法的类继承该类,不需要 draw0方法的类继承图形类,又会产生新的问题:所有的子类都需要继承图形类,因为这些类是从图形类中导出的,同时某些类还需要 draw0方法,而 Java中规定类不能同时继承多个父类。为了应对这种问题,接口的概念便出现了。
接口是抽象类的延伸可以将它看作是纯粹的抽象类,接口中的所有方法都没有方法体。对于抽象类节中遗留的问题,可以将 draw0方法封装到一个接口中,使需要 draw0方法的类实现这个接口,同时也继承图形类,这就是接口存在的必要性。在下图中,描述了各个子类继承图形类后使用接口的关系。
接口使用 interface关键字进行定义,其语法如下:
public interface Paintable{
void draw(); //定义接口方法可省略public abstract关键字
}
public:接口可以像类一样被权限修饰符,但public关键字仅用于接口在与其同名的文件中被定义。
interface:定义接口关键字。
Paintable:接口名称。
一个类继承一个父类的同时再实现一个接口,可以写成如下形式:
public class Parallelogram extends Quadranle implements Paintable{
···
}
注意
(1)在接口中,方法必须被定义为 public 或 abstract 形式,其他修饰权限不被 Java 编译器认可。或者说,即使不将该方法声明为 public 形式,它也是 public 形式。
(2)在接口中定义的任何字段都自动是 static 和 final 的。
例题:将绘图方法设为接口方法
将图形对象的绘图方法剥离出来,作为 Paintable 可绘制接口中的抽象方法。创建四边形类作为平行四边形类和正方形类的父类,同时让这两个子类实现 Paintable 接口。创建圆形类实现 Paintable接口,但不继承四边形类。
从这个结果可以看出,“绘制”这个行为可不是四边形独有的,圆形也能绘制,所以 draw0方法被独立封装在了可绘制接口中。
正方形类与平行四边形类分别继承了四边形类并实现了可绘制接口,所以正方形类与平行四边形类既可以调用绘制方法,又可以调用四边形提供的 doAnything0方法。但是,圆形不属于四边形,且可以绘制,所以最后圆形对象只调用了 draw()方法。 Java 中不允许出现多重继承,但使用接口就可以实现多重继承。一个类可以同时实现多个接口.因此可以将所有需要继承的接口放置在 implements 关键字后并使用逗号隔开。实现多个接口的语法如下:
class 类名 implements 接口 1,接口 2,.,接口n
但这可能会在一个类中产生庞大的代码量,因为继承一个接口时需要实现接口中所有的方法。一个接口可以继承另一个接口,其语法如下:
interface intf1{}
interface intf2 extends intf1 {} //接口继承接口
/*继承 extends
* 父类 和普通类写法相同
* public class 父类类名{
* }
* 子类 需要继承父类
* public class 子类类名 extends 父类类名{
* }
* 所有类的构造方法第一行都有一个隐藏的"super();"
* 作用是在执行该构造方法之前调用其父类构造方法
*
* Object类
* 方法的重写
* 返回参数相同 方法名相同 传入参数相同 方法体不同
*
* Animal a = new Animal();
* 对象 = 引用类型对象
*
* 向上转型:
* 将子类对象赋值给父类引用
* 自动类型转换
* Animal a = new Dog();
*
* 向下转型:
* 将父类对象赋值给子类引用
* 需要进行强制类型转换
* Dog a = (Dog) new animal();
*
* 子类重写
*
* 重载
* 方法名相同 参数不同
*
* 关键词:final
* final修饰变量————不可以被修改(常量)
* final修饰方法————不可以被重写
* final修饰类——————不可以被继承
*
* instanceof
* 对象名 instanceof 类名
* 判断该对象是否属于该类或其子类
*
* 对象名 instanceof 接口名
* 判断该对象是否属于该接口的实现类
*
* 抽象方法
* 修饰符 abstract 返回参数 方法名(传入参数);
*
* 抽象类 有抽象方法的类一定是抽象类
* 修饰符 abstract class 类名{
* }
* 抽象类的使用场景
* 普通子类继承父类的构造方法需要子类重写父类的构造方法
*
* 接口 所有方法都是抽象方法
* 修饰符 interface 接口名{
* }
* 实现 implements
*
* 修饰符 class 类名 implements 接口1,接口2...{
* }
* 假设一个接口有三个抽象方法,但是只实现了两个,那么如何解决:
* 1.将第三个方法重写
* 2.把那个接口改为抽象类
* */