java基础 ---继承和多态
今天总结的内容分为三部分,即继承、多态、抽象类和抽象方法。
一、继承
1、什么是继承
之前我们已经学习过组合的概念,在一个类中,通过创建其他类的对象,并调用他的方法来解决某些问题,我们称之为组合。类与类之间还有另外一种父与子的关系,子类继承父类除private和final关键字的方法与属性,这个我们就称之为继承。子类拥有父类的一些公有的属性和方法,并且子类可能也会有自己的方法和属性,前者就象is-a,而后者就是islike-a.明白这点非常重要,比如当某个父类派生出许多子类,那么也许这些子类拥有自己的特有的方法和属性,但是他们都共享父类中的公有方法和属性.了解了什么是继承后下面将总结一下关于访问修饰符在继承中的作用.
(1)private(私有)
private修饰符表示此方法或者属性是私有的,拥有此修饰符的方法和属性不会被继承,因为他相对于任何外部的类都是不可见的.同样,具有final修饰符的方法和属性也不会被继承,因为他本身就表明是最终的.
(2)public(公共)
public修饰符是表示此方法或属性是公共的,那么就可以被任何外来的类访问,无论外部类是来自于本包还是外包,都可以继承.
(3)protected(受保护的)
protected表示此方法或属性是受保护的,什么是受保护的呢.这个关键字似乎用在继承这里才会体现其作用.因为在本类或本包都无限制的进行访问或者继承,如果外部类是在外包,那么只有继承关系的子类才拥有对他的访问权.因此在对于继承关系的类来说,protected修饰符相当于就是public(公共的).而非继承关系的类对这个关键字修饰的方法和属性是没有访问权限的.
(4)缺省的
缺省的意思是在方法或属性前面不加任何访问修饰符,那么就表明该方法或属性是拥有缺省的权限.仅当子类在其父类相同包里,才能继承父类的该方法.如果子类在父类的包外,那么缺省修饰符的方法和属性是不会被继承的.
2、为什么需要继承
当我们学习了继承之后,有时候也有这样的困惑!为什么需要继承了?组合和继承我们应该怎样选择呢?其实在很多java的教程中都提到(比如Think in java),继承并不是我们设计方式的最佳选择,因为有时候使用继承可能会让程序变得复杂,从而增大维护的成本。而在设计分析的时候,首先考虑的应该是组合,当某一问题使用组合无法解决问题时,我们才应该考虑继承。但是对于继承的概念,我们必须要清楚。
3、如何继承
使用继承的方法非常简单,首先创建一个需要继承的父类,通过他我们能派生出很多的子类,只需在声明类时加上extends关键字,就表明该类继承于某个父类。比如:
public class ChildClass extends SuperClass{}
上面这句代码表明ChildClass继承自他的父类SuperClass,此时的ChildClass就拥有了SuperClass的所有公共方法和属性。我们还需要了解方法的重载和重写。
(1)方法的重载
在同一个方法中,拥有不同的参数,相同的方法名,那么这两个方法就属于重载。理解重载首先要了解编译器是如何区分重载的方法。编译器主要根据两个地方来区分,第一、根据方法名。第二、根据参数列表。而参数则不在其列,只有当方法名和参数相同时才会被认为两个方法完全相同,当然如果在一个类里面,两个完全相同的方法是不允许的,因为这样编译器就不会知道该调用哪个方法了。而重载则是只有方法名相同,参数并不相同,这样编译器仍然可以区分他们,因此实现了重载。这里指参数不同有三种涵义,都可被认为是参数不同。第一、参数类型不同。第二、参数顺序不同。第三、参数个数不同。除此之外,如参数的引用名字不同,这个并不能用于区分不同的参数。
比如有有两个方法,都来自同一个类:
public void overload(int i){}
public void overload(long i){} //合法
public void overload(int i,float f){}
public void overload(float f,int i){} //合法
public void overload(int i,float f){}
public void overload(float f){} //合法
public void overload(int i){}
public void overload(int j){} //不合法
public void overload(int i){}
public int overload(int i){} //不合法
从上面基本上能够明白重载所需的条件。
(2)方法的重写
刚才说了什么是重载,那什么又是重写呢。这个就要从继承说起了,因为重写是发生在子类与父类上。比如父类有个方法,同样继承于此父类的子类也有父类的这个方法。如果在子类中没有显示的将该方法实现,那么调用的将是父类的方法。但是我们可以将该父类的方法在子类中重新显示的写出,并且改变该父类的方法中的行为。那么此时就出现了父类与子类具有相同的方法名,但是不同的行为特征。比如:
class SuperClass {
public void override(){
System.out.println("superclass" + i);
}
}
class ChildClass extends SuperClass{
public void override(){
System.out.println("childclass" + i);
}
}
如果我们创建一个ChildClass 的对象,调用override这个方法输出结果就会是childclass ,因为SuperClass的子类ChildClass 重写了原SuperClass的override()方法,他拥有了自己的行为,但是方法名以及参数是和原SuperClass中一样的。重写要和重载区别开来,重载需要不同的参数来区别相同方法名的方法。而重写必须方法名和参数列表都相同,同时返回类型也要相同,否则编译器就会将视为方法的重载而不是重写。注意:修饰符在重写这里起了很大的作用。当访问修饰符为private时,表明该方法不能被重写,同样当方法具有final关键字时也表明该方法不能被重写。
(3)构造器的调用顺序
在学习继承机制时,构造器的调用顺序也必须要了解。下来看看下面这段程序:
SupperClass() {
System.out.println("SupperClass");
}
}
class FirstChildClass extends SupperClass {
FirstChildClass() {
System.out.println("FirstChildClass");
}
}
class SecondChildClass extends FirstChildClass {
SecondChildClass() {
System.out.println("SecondChildClass");
}
}
public class InheritanceDemo extends FirstChildClass {
private SecondChildClass second = new SecondChildClass();
public InheritanceDemo() {
System.out.println("InheritanceDemo");
}
public static void main(String [] agrs) {
new InheritanceDemo();
}
}
下面是运行结果:
SupperClass
FirstChildClass
SupperClass
FirstChildClass
SecondChildClass
InheritanceDemo
由此我们可以总结出构造器的调用是通过下面三个顺序来的:
I、首先会调用基类的构造器,如果没有显示的调用,那么编译器会调用默认的基类构造器:super(),注意,如果需要显示的调用基类构造器,必须将其写在构造器里的第一行。编译器之所有要这么做,因为子类可能需要调用父类的方法或者属性,那么必须保证他的父类所有属性都会被初始化,否则将会带来严重的后果,所以调用基类的构造器也是必须的。
II、按声明的顺序调用成员初始化。我们可以看到当调用完本类的基类构造器SupperClass和FirstChildClass后,下面就是初始化second这个属性。
III、最后调用构造器的方法体,当完成了second这个属性的初始化操作后,最后就是调用构造器的方法体输出InheritanceDemo。
二、多态
1、什么是多态
多态是和继承密切相关的,正因为有继承,才会有多态出现。多态从字面上的意思应该是多种形态。更进一步,延伸到继承里来,那么多态就应该是具有相同的操作不同的行为的一种特征。
2、向上转型(upcasting)
在理解多态之前,我们需要明白什么是向上转型(upcasting)。其实这点在Think in java这本书中,作者进行了非常详细的描述,该书习惯以画几何图形为例,来阐述这一过程。这个例子还是非常形象的,我们以一段代码来描述这个过程。
void draw(){}
static void start(Shape s){
s.draw();
}
public static void main(String [] agrs){
start(new Circle());
start(new Square());
start(new Triangle());
}
}
class Circle extends Shape{
void draw(){System.out.println("draw Circle");}
}
class Square extends Shape{
void draw(){System.out.println("draw Square");}
}
class Triangle extends Shape{
void draw(){System.out.println("draw Triangle");}
}
首先我们创建了一个Shape这个基类,他里面有个空方法draw(),一个静态方法start(),其中接受Shape类型的参数。外面定义了画圆、画方、画三角形这三个类。他们都继承自Shape类,并且都重写了draw方法,实现了自己的特有的行为。因此我们看到在start()方法中,通过传入的参数类型不同,表现出来的行为也会不同。但是传入后的参数都统一转型为Shape这个基类型,这里就表现为向上转型。因为圆、方、三角形都是属于Shape类型。因此他们可以安全的向上转型为Shape,此时他们都是同一个类型Shape。这里又会出现一个问题,既然他们都转型为Shape了,那为什么当程序运行后,能够区分出到底是调用哪一个子类的方法呢。那么这里又出现个新的概念---动态绑定。
3、动态绑定
将一个方法调用同一个方法主体关联起来被称之为绑定,若在程序执行前绑定就被称为前期绑定。在面向过程的语言中默认都是前期绑定。有前期绑定也就会有后期绑定,后期绑定也被称为动态绑定,编译器始终都不会知道对象的类型,只有当方法在运行期间被调用时方法的调用机制就能找到正确的方法体,关于这个调用机制是如何运作的等学习反射之后再总结吧。
了解了这些后再回到上面的程序中来。将圆、方、三角形这三个不同的对象传入到start()方法中,并将他们自动向上转型为shape类,由于shape也同样拥有draw这个方法,因此到这里编译器不会认为有任何的错误。但是此时它并不知道传入的参数具体是哪个类型,更不知道应该调用哪个类的方法。但是当程序运行到这里时,通过动态绑定的机制,程序依然能够正确的判断出其类型,并调用相应的行为。这点就完全符合多态的特征,相同的操作,不同的行为!既然有了向上转型,那么就应该会有向下转型。
4、向下转型
在上面的程序中,3个画图的类仅有1个父类的方法,这个时候我们可以把他们和基类shape看作是is-a的关系,但是如果我们在3个子类中添加一些他们特有的方法,并且这个方法没有在shape中定义过,这个时候可以看作和shape是islike-a的关系。那么将他们向上转型后,他们特有的方法和属性就会丢失,如果需要找回这些属性和方法我们就需要进行强制的类型转换,在java中任何的类型转换错误都会抛出ClassCastException.因此向下转型总是不象向上转型那么安全,因为父类或父接口的方法或属性只可能比子类少或者一样,那么向上转型肯定是安全的。我们可以使用((Circle)s).draw()这样的方式来强制转换成圆这个类型。但是将这个代码加在上面的程序中,会抛出
ClassCastException,因为除了圆这个类的对象能正常转型外,其他两个类不属于这个类型,那么就会抛出异常,但是这个并不会在编译时报错,这点又可以看出动态绑定的特征。
5、构造器中多态方法的行为
在构造器中使用多态的方法总是不那么安全的,这点我们可以通过下面的程序来验证:
Shape(){
System.out.println("this is a shape");
draw();
}
void draw(){}
public static void main(String [] agrs){
Shape circle = new Circle();
}
}
class Circle extends Shape{
int i = 100;
Circle(){
draw();
}
void draw(){System.out.println("draw Circle" + i);}
}
首先我们创建了继承于Shape的子类Circle对象,并将其向上转型为Shape,然后在调用其基类构造器中调用了draw()方法,希望利用多态的特点,正确的输出我们想要的结果。但是结果确不是我们想像中的那么顺利,最后的输出结果是:
this is a shape
draw Circle0
draw Circle100
这的确很奇怪。在Shape构造器中调用子类的draw()方法,为什么这个时候的i值是0呢,但是当回到Circle自己构造器中调用draw()方法,i的值就是100了。这个还要从我们刚才讲的构造器的调用顺序说起。首先,调用的是基类的构造器,当调用draw()方法的时候,此时子类的构造器并没有运行,也就是说还没有将i的值初始化为100,而是0。但是这里又会有个问题,既然i的值没有被初始化,那么它的值为什么又会是0呢。这里又要讲述一下初始化的顺序,实际过程可以分下面四步:
I、在其他任何事物发生之前,将分配给对象的存储空间初始化为二进制的零。
II、如前所述的那样调用基类构造器。因此我们会发现,虽然在基类的构造器中调用了其子类的draw方法,而里面的i值还并没有被初始化,但由于步骤1的原因,i的值被置为0。
III、按照声明顺序调用成员的初始化方法。
IV、调用子类的构造器主体。
通过以上的例子大概能够明白多态的行为,以及如何使用多态。
三、抽象方法和抽象类
1、什么是抽象方法和抽象类
抽象方法:
拥有方法声明但没有方法体的方法被称之为抽象方法,抽象方法其实就是为了重写而存在的。因为它没有任何的方法体,需要子类来实现他。抽象方法前面必须要有abstract关键字。
抽象类:
同样,如果一个类中拥有1个或多个抽象方法,那么该类也必须是一个抽象类。抽象类不能被实例化。在类声明名前加上abstract关键字表明该类是一个抽象类。当然抽象类中也许还有一些具体实现了的方法,这点也是和接口的区别之一。
因此,抽象方法所在的类必须也是抽象类,但是抽象类不一定包含抽象方法,我们可以看窗口事件的适配器原代码就可以发现,每个适配器都是抽象方法,但是他们都没有任何的抽象方法。比如MouseAdapter。
抽象类就是被用来继承的,那么继承于抽象类的子类也要实现抽象类的所有抽象方法(当然也可以选择不实现),这个子类才不是一个抽象类。否则继承的子类也是一个抽象类。
2、abstract和final的区别
具有abstract关键字修饰类是不允许实例化,只能被用来继承。而final修饰的方法或者类是不允许继承,但是类可以被实例化。那么在同一个方法或者类声明中,不允许同时出现abstract和final。
3、关于事件适配器的类
这个事件适配器的类对于我们这些初学java学习的人来说确实有些奇怪。首先他们本身是一个抽象类,这些类又继承自相应的接口。我们知道如果一个类继承自某个接口,那么子类就必须实现父接口的所有方法。那么来看适配器的类,刚看见他们时会发现他们都是抽象类,我们第一反映就是肯定这些类有一些抽象方法。但是既然他们有抽象方法,那么在继承他们的时候就需要具体去实现这些方法。但实际情况确实我们可以选择性的实现其中某个方法。这好像不太符合抽象类的定义。但是当我们打开这些适配器类的原代码时,才恍然大悟,原来它里面实现了其监听器接口的所有方法,这些方法都是拥有方法体,但是没有任何具体实现的空方法。他们都仅仅是没有任何抽象方法的抽象类。
今天总结就到这里,明天继续总结关于接口、外部类、内部类、匿名内部类的知识。