您的位置:首页 > 编程语言 > Java开发

java第八章学习笔记:继承---多态的支柱

2018-03-14 03:18 381 查看
【因为每一章都篇幅比较长(10多页,所以有的地方会写错字,如发现请指正,本人不胜感激)但是真正过一遍这本书的内容才发现真的讲的很基础,浅显易懂】

本章介绍面向程序设计另外一个概念–继承。利用继承,可以基于已存在的类构造新类,继承已存在的类就是复用这些类的成员,在此基础上,还可以在新类中添加一些新的成员或修改继承了的成员,以满足新的需求。

8.1 继承的概述

java是纯面向对象程序设计语言,因此有必要了解一下面向对象中的继承的意思,本节首先介绍类之间的关系,然后介绍继承性;

8.1.1 类之间的关系

”USES-A“关系

”HAS-A“关系

”IS-A“关系

”USES-A“关系:

是最明显、最常见的关系,若类A的方法操纵了类 B(对象)的成员,则称之为类A”USES-A“(用到了)类B,eg:轿车的启动用到了汽油的储备量:

public class Test
{
public static void main(String[] args)
{
Benzine b=new Benzine();
SaloonCar sa=new SaloonCar();
sa.startUp(b);//将汽油对象作为参数传递给启动方法来检查汽油储备量
}
}

class Benzine//汽油类,检验汽油是否充足
{
private int cubage=100;//记载现有的汽油量【外界通过set,get访问器修改设置它的值,详情见第七章】
public boolean isEnough()
{
if(cubage>=80)
{
return true;
}else
{
return false;
}
}
}
class SaloonCar//汽车类,包含启动方法
{
public void startUp(Benzine b)//对象点方法名调用
{
if (b.isEnough())//判断汽油量是否充足
{
System.out.println("恭喜你,汽车储备量充足,汽车可以启动!");
}
}
}


运行结果:





通过以上代码更好的理解”USES-A“关系

2、”HAS-A“关系

是一种拥有关系,若类A中有类B类型的成员引用变量,则类A”HAS-A(拥有)类B eg,轿车拥有轮胎:

public class Test
{
public static void main(String[] args)
{
Car car=new Car();
System.out.println("轿车所使用的轮胎颜色为:"+car.getTyre().getColor());
System.out.println("轿车所使用的轮胎材质为:"+car.getTyre().getMaterial());
}
}

class Tyre//轮胎类
{
private String material="橡胶"
1ccfe
;//轮胎属性
private String color="黑色";
public String getMaterial()//访问器方法
{
return material;
}
public String getColor()
{
return color;
}
}
class Car//汽车类
{
private Tyre t=new Tyre();//汽车类中有轮胎类型的对象(成员引用变量)[拥有私有成员轮胎,并为轮胎设置了访问方法,这样轿车类就可以使用轮胎的任何可见成员]
public Tyre getTyre()//访问器方法
{
return t;
}
}


运行结果:



【轿车拥有私有成员轮胎,并为轮胎设置了访问方法,这样轿车类就可以使用轮胎的任何可见成员】可将”car.getTyre()“看做是Tyre类的一个对象

由于汽车类拥有了轮胎对象,所以汽车对象也就拥有了对象中一切可见信息

3、”IS-A“关系:

在面对对象中”IS-A“的概念是基于继承的,其旨在:表达一个类是另一个类子类的一种,即若类A是类B子类的一种,则可以说类A”IS-A“(是一种)类B,eg:”苹果“是“水果”子类的一种,则他们之间的关系为”苹果“IS-A(是一种)”水果“

提示:在实际开发中需要同时用到以上几种关系,需抓住现实世界中事物之间的实际关系来进行抽象,然后在java世界中建立模型,若搞错了关系的模型,有可能影响系统的开发或维护

8.1.2 面对对象中的继承性

下图显示了面对对象继承性的含义,注意汽车类定义了品牌、价格、最高时速等属性(成员变量)。以及刹车、启动等方法,当定义继承汽车类的子类卡车时,它自动继承了汽车类中的属性和方法。



汽车类的继承关系层次图

被继承的类叫:超类或父类

继承节省了定义新类中的大量工作,因为编程者可以方便的重用代码eg,当创建子类轿车时,品牌、价格,最高时速等属性会自动的被定义,调用刹车方法时会自动调用汽车类中定义的刹车方法,但一个子类不必非得使用继承下来的属性和方法,可以选择性覆盖已有的属性和方法或添加新的属性方法;

提示:在开发中继承不能乱用,其只是重用代码的一种方式,只有当需要向自己的新类中添加新的操作,并把已存在的类的缺省行为融进自己的新类中,才需要继承已有的类。

8.2 类的继承

在java中实际上所有的类都直接或间接的继承自java.lang.Object类,也可以说Object类是java的总根类,实际开发中,如不特殊指定,自己定义的类均直接继承自Object类;当需要某个类继承自指定类时,使用”extends“关键字,语法如下:

*class 子类名 extends 父类名

{子类类体}* eg:

class Vehicle{}//定义了一个交通工具类【Vehicle是Car的父类】
class Car extends Vehicle{} //【Car是Truck的父类】
class Truck extends Car{}//继承汽车类的卡车类【Truck也继承(派生)自Vehicle】


回到”IS-A“关系上,这样的陈述是正确的:”Car extends Vehicle“指”Car IS-A Vehicle“,并且也可以说”Truck extends Vehicle“因为Vehicle类在继承树中位于Trunk类的上面:



8.3 成员变量的继承与隐藏

当类B成功继承类A后,就涉及懂啊其成员变量的继承问题。本节包括成员变量的继承规则与成员变量的隐藏

提示:上述两个方面都是类继承以后要解决的问题,如果没有继承,则没有这些问题

8.3.1 成员变量的继承规则

成员变量能否被继承,完全取决于其对应的访问限制;在上一章中提到的访问限制修饰符对继承造成的影响,本小节将详细介绍:

公有成员

私有成员

默认(不写)成员

受保护的成员

1】 公有成员:

对于子类,如果其父类的成员声明为public类型,那么无论这两个类是否在同一个包中,子类均能继承其父类的该成员。eg:

package lxx;
public class Sample
{
public String str="该成员变量为public型,能够被子类成功继承!!";
}


package bupt;
import lxx;
public class Test
{
public static void main(String[] args)
{
Son s=new Son();//创建对象并访问方法与成员
System.out.println("子类外调用结果:"+s.str);
s.getShow();
}
}
class Son extends lxx.Sample
{
public void getShow()
{
System.out.println("\n子类内代码调用结果:"+this.str);
}
}




2. 私有成员

党成员变量声明为private时,任何子类都不能继承该成员,eg将上面的Sample类的Str变量修饰符修改为private结果:



private修饰的str只能在声明其的类中被访问

3. 默认(不写)成员:

包内相当于public,包外不能继承该成员变量。

4. 受保护的成员:

protected类型只有在谈论继承的时候,彩盒默认类型有区别,其他情况下和默认类型相同;该区别是:当成员变量被修饰为protected时若访问该变量的类位于包外,只有通过继承才能访问该变量。即protected的成员在包外只能通过继承访问

注意:
System.out.println("子类外调用结果:"+s.str);
这一行虽然用的是子类类型的引用调用,但这不是通过继承访问。

实际上,对子类来说,修饰为protected的成员被继承后,该成员对子类外的任何代码来说都是私有的,只能在子类内部对其进行调用

8.3.2 成员变量的隐藏

当子类具有与父类的成员变量名称相同的变量时,便构成了成员变量的隐藏,其含义指:在子类中直接调用该成员变量时,调用的是子类本身具有的,而不是父类继承的eg:

public class Sample
{
String a="父类成员变量";
}


public class Test
{
public static void main(String[] args)
{
Son ss=new Son();
ss.Show();
}
}
class Son extends Sample
{
String s="子类的成员变量";
public void Show()
{
System.out.println("这里调用的s是:"+s);
}
}


运行结果:



由于子类具有相同的名字,所以父类被隐藏;

在子类中使用super关键字,便可以访问该变量eg:将show方法修改为:

public void Show()
{
System.out.println("这里调用的s是:"+s);
System.out.println("使用super关键字获得父类成员变量s为:"+super.s);
}


运行结果为:



提示:关于如何在子类外面访问被隐藏的成员变量咋下面章节中介绍

8.4 对象引用的使用

本节包括对象引用能指向的对象类型、对象引用的强制类型转换、对象引用所能调用的成员、对象引用的赋值与比较

8.4.1 对象引用能指向的对象类型:

一个对象引用都能指向什么样的对象?

不管是什么类型的对象引用均能指向自身的对象实例eg:

Car c=new Car();//汽车类型的引用c指向汽车类型的对象
Truck t=new Truck();


另外,父类的引用变量还可以指向子类(直接子类或间接子类)的对象,eg:

class Vehicle{}
class Car extends Vehicle{}
class Truck extends Car{}


这三个类的引用变量可以这样进行赋值:

Vehicle v=new Car();
v=new Truck();
Car c=new Truck();


交通工具所具有的特征,卡车和汽车都有

上述三行代码都是父类指向子类,这是因为父类派生出子类,子类具有父类的特征和行为,可以将子类看做父类的对象。

汽车所具有的行为交通工具不一定具有,但卡车一定有,因此可以说汽车一定是交通工具,但交通工具不一定是汽车,所以交通工具类型的引用可以指向汽车或卡车对象,汽车类型的引用可以指向卡车对象,但汽车引用不能指向车辆对象。

这样做的意义是实现基于继承的多态,有了多态可以大大提高程序的灵活性,所谓基于继承的多态指:拿着父类的引用,可以根据需要将引用指向不同的子类对象,类调用不同的子类实现。这样父类引用的选择性将变得很灵活本章最后介绍基于继承的多态。

8.4.2 对象引用的强制类型转换

eg:

class Car {
String aMember="我是汽车类的成员变量";
}
class Truck extends Car{
String aMember="我是卡车类的成员变量,汽车类也有";
}


public class Test
{
public static void main(String[] args)
{
Car c=new Truck();//父类引用指向子类对象
System.out.println("访问的成员变量为:"+c.aMember);
}
}


运行结果:



从图中可以看出,虽然对象是子类Truck类型的,但打印的是父类Car的成员变量,这是因为在运行时,对于成员变量的访问系统认的是引用类型,引用是什么类型,系统访问谁的成员,若要访问子类Truck对象的成员,则需要将指向对象的父类引用转换为子类类型,则将上面的代码修改为:

System.out.println("访问的成员变量为:"+((Truck)c).aMember+"!!");


其中在c引用前加上一对圆括号,在圆括号中放类名的语法称为强制类型转换,表示把Car型引用转化为Truck型。

强制类型转换转的只是引用类型,真正指向的对象是不会发生变化的,可以将引用看作看待对象的角度层次,这就像可以将红富士看作苹果,也可以看作水果一样,看待的角度,层次变了,但是苹果还是苹果。

运行结果:





此次打印的是子类的成员,因为引用的类型已经转换成子类Truck型了,需注意,对编译而言,只要*被转换的引用类型与转换后的目标类型是派生或被派生关系则可以编译通过***eg:

public class TestSon
{
public static void main(String[] args)
{
Fruit f=new Apple();
Pear p=(Pear)f;
}
}

class Fruit{}
class Apple extends Fruit{}
class Pear extends Fruit{}//梨


编译上面的代码,编译通过,但是【Pear p=(Pear)f】这一行是有问题的,怎么可以将苹果看成梨呢?这是因为在编译时,系统并不知道引用指向的具体对象是什么,其只能根据引用类型来判断转化有没有希望

像本例中将水果(Fruit)转成梨(Pear),而梨派生自水果,这是有希望的,故编译通过,如果将水果转换为没有任何关系的其他引用类型,则编译报错。运行代码eg:



“java.lang.ClassCastException”异常,这是因为Fruit型引用f实际上指向的是Apple型对象,其不能看作梨,同时,

在运行时才真正能确定是否能进行引用强制类型转换。【引用是什么类型,系统访问谁的成员,若想访问子类的就得强制将引用转为子类的】

8.4.3 对象引用所能调用的成员

public class TestSon
{
public static void main(String[] args)
{
Fruit f=new Apple();
System.out.println("调用父类子类共有的成员变量name="+f.name);
System.out.println("子类特有的成员变量aname="+f.aname);//error:”找不到符号“
}
}

class Fruit
{
String name="水果";
}
class Apple extends Fruit
{
String name="苹果";
String aname="苹果a";
}


编译直接不通过

name是Apple与Fruit类共有的成员,aname是Apple类特有成员。

虽然f引用指向的是apple对象,apple对象有aname属性,但这点在编译时系统并不知道,系统只能根据引用类型来判断,如果引用类型中没有,则编译报错。

因此,对象引用所能调用的成员规则是:引用只能调用其所在的类具有的成员,不能调用子类特有的成员,这点对于成员变量和方法是一样的。

8.4.4 对象引用的赋值与比较

一般引用的相互赋值

引用的比较

一般引用的相互赋值:

开发中,常需要进行引用的相互赋值,下面介绍赋值规则:eg:



” 不兼容的类型“因为将Fruit型的看作苹果不一定正确,水果不一定是苹果( a=f)【前者指向后者,将后者看作前者】

提示:引用赋值如果不一定正确,则编译报错要与前面的强制类型转换运行时异常分开

从上例可以看出,引用赋值时可以直接将子类引用赋值给父类引用,(将子类看作父类【将苹果看作水果】),若需要将父类引用赋值给子类引用则必须强制类型转换。

引用的比较

引用等于/不等于比较的含义:比较两个引用是否指向同一个对象,从表面上看,任何两个引用应该都可以比较,若指向同一个对象则返回true,否则返回false,其实不然,比如将大象类型的引用与苹果类型的引用进行比较是没有任何实际意义的。

java是聪明的语言,因此java对进等于/不等于比较的引用类型有要求:

相同类型的引用可以进行比较

不同类型的引用要进行比较,则其中一个类型必须派生自另一个类型,否则编译报错【比较的两个引用要么出处相同要么是继承关系】

提示:类型不同,又没有派生关系的引用其实不可能指向同一个对象,因此比较没有实际意义,这样的规则是合理的。eg:



上面的“a==f”可以比较是因为两个引用间有派生关系

而“a==p”编译报错是因为两个引用类型不同也没有派生关系

将上面代码中“Fruit f=a;”修改为指向水果对象编译不报错:

public class TestSon
{
public static void main(String[] args)
{
Apple a=new Apple();
Fruit f=new Fruit();//将子类引用赋值给父类引用f
Pear pear=new Pear();
if(a==f)
{
System.out.println("苹果引用==水果引用,他们都指向了同一个对象");
}
}
}
class Fruit
{
}
class Apple extends Fruit
{
}
class Pear extends Fruit{}//梨


运行结果:



8.5 方法的继承与重写

8.5.1 方法的继承规则

从本质上将,方法也是一种成员,因此其继承规则与成员变量的继承规则完全一样,其是否能被继承完全取决于访问限制eg:说明public的方法被继承的情况。

package lxx;
public class Sample
{
public void show()
{
System.out.println("该方法是public类型的,可以被继承!");
}
}


package bupt;
import lxx;
public class Test
{
public static void main(String[] args)
{
SampleSon son=new SampleSon();
System.out.println("子类外调用结果为:");
son.show();
son.getShow();
}
}

class SampleSon extends lxx.Sample
{
public void getShow()
{
System.out.println("子类内调用的结果为:");
this.show();
}

}


运行结果:



从上例可以看出,方法的继承规则与成员变量完全相同,其他类型的访问限制方法可以自行测试

8.5.2 方法重写的基本知识

子类的方法中,如果与继承过来的方法具有相同的签名,便构成了方法的重写(别名:覆盖),重写的主要优点是能够定义子类特有行为eg:

public class Test
{
public static void main(String[] args)
{
Car c=new Car();
System.out.println("重写时实际调用的方法为:");
c.startUp();
}
}
class Vehicle{
public void startUp()
{
System.out.println("一般交通工具的启动方法");
}
}
class Car extends Vehicle{
public void startUp()
{
System.out.println("轿车的启动方法");
}
}


运行结果为:



将子类中重写的startUp()方法注释掉,再次运行调用的就是父类的方法

当子类重写了方法就调用子类方法,否则调用父类方法。

同样,父类引用指向子类对象,当 父类引用调用被重写的方法时,java将如何处理呢?eg 将上面的main()方法修改为:

public static void main(String[] args)
{
Vehicle v=new Car();
System.out.println("父类引用指向子类对象时,重写时实际调用的方法为:");
v.startUp();
}


运行结果:



从语法上,用父类的引用只能调用父类中的方法,若只是子类中有的方法,不能通过父类引用调用

运行结果与上面直接子类引用指向子类对象的结果一模一样,这说明:当父类的引用指向子类对象时,若访问被重写的方法,则将访问,被重新定义的子类中的方法。

提示:特别注意,方法调用按对象类型,无论使用什么类型的引用,其调用的都是具体对象所在类中定义的方法,这与成员变量不同,成员变量按引用的类型调用前面已经介绍过,同时这也是实现多态的方式

若想构成方法的重写,子类中方法名与参数列表必须完全相同,一旦构成重写,必须遵循如下规则:

返回类型若为基本数据类型,则返回类型必须完全相同;若为对象引用类型,必须与被重写的方法返回类型相同,或派生自被重写方法的返回类型(出处相同或继承于被重写的)

访问级别的限制一定不能比被重写的窄,可以比被重写的方法宽

不能重写表示为final的方法(下一节中详细介绍)

提示:重写时基于继承的,如果不能继承一个方法,则不能构成重写,不必遵循重写规则。下面详细介绍重写规则:

8.5.3 构成重写的条件

java中方法名与其参数列表共同构成了方法的唯一标识,而方法若想要构成重写,首先方法的唯一标识必须与被重写方法的完全相同eg:

public class Test
{
public static void main(String[] args)
{
Vehicle v=new Car();
System.out.println("父类引用指向子类对象时,重写时实际调用的方法为:");
v.startUp();
}
}
class Vehicle{
public void startUp()
{
System.out.println("一般交通工具的启动方法");
}
}
class Car extends Vehicle{
public void startUp(int t)
{
System.out.println("轿车的启动方法");
}
}




上面的代码不满足重写条件,并没有重写,Car中的startUp(int i)只是Car类中自己单独定义的方法,与集成重写没有任何关系;

提示:只是方法名称相同,参数列表不同的不是重写,而是继承以后继承的方法与新写的方法构成的重载。

8.5.4 返回类型的规则

1、基本数据类型:

被重写方法的返回数据类型若是基本数据类型,则重写的方法返回的类型必须与之完全相同,否则会编译报错。

public class Test
{
public static void main(String[] args)
{
Vehicle v=new Car();
System.out.println("轿车的价格为:"+v.getMoney());
}
}
class Vehicle{
public int getMoney()
{
return 20000;
}
}
class Car extends Vehicle{
public String  getMoey()//Error
{
return "20W";
}
}




子类Car中有与父类Vehicle中名称相同,参数序列一样的getMoney()方法,但返回值类型不同,且为基本数据类型,违反了重写规则

2、对象引用类型:

被重写方法的返回类型若为引用类型时,则重写方法的返回类型要么出处相同要么继承自被重写方法的返回类型

eg:

public class Test
{
public static void main(String[] args)
{
Vehicle vehicle=new Car();
System.out.println("调用的结果为:");
vehicle.getMe();
}
}

class Father{}
class Son extends Father{}
class Vehicle
{
public Father getMe()
{
Father father=new Father();
System.out.println("方法没有重写成功");//若没有重写成功则返回父类的方法
return father;
}
}
class  Car extends Vehicle
{
public Son getMe()
{
Son son=new Son();
System.out.println("方法成功重写");
return son;
}
}




类Car中有与父类Vehicle名称与参数序列相同的方法getMe(),构成重写,但返回值不同

Vehicle类中的getMe()方法返回的是Father类,而Car类的getMe()返回的是继承自Father的Son类;

在调用方法时使用的是父类的引用v

public class Test
{
public static void main(String[] args)
{
Vehicle vehicle=new Vehicle();
System.out.println("调用的结果为:");
vehicle.getMe();
}
}

class Father{}
class Son extends Father{}
class Vehicle
{
public Father getMe()
{
Father father=new Father();
System.out.println("方法没有重写成功");//若没有重写成功则返回父类的方法
return father;
}
}
class  Car extends Vehicle
{
public Son getMe()
{
Son son=new Son();
System.out.println("方法成功重写");
return son;
}
}


运行结果为:



getMe()方法被重写,执行的是重写后的代码;

8.5.5 访问级别的要求

在重写方法时注意,重写的方法不能比被重写的方法更窄的访问限制,可以更宽,eg:不能将一个public的方法重写成protected的,eg:

public class Test
{
public static void main(String[] args)
{
Car c=new Car();
System.out.println("父类引用指向子类对象时,重写时实际调用的方法为:");
c.startUp();
}
}
class Vehicle{
public void startUp()
{
System.out.println("一般交通工具的启动方法");
}
}
class Car extends Vehicle{
protected void startUp()//Error:Cannot reduce the visibility of the inherited method from Vehicle
{
System.out.println("轿车的启动方法");
}
}


被重写的方法与重写方法限制不同,重写更窄故报错。

8.5.6 重写基于继承

需注意的是,方法的重写是基于继承的,若方法没有继承,则不可能发生重写eg:

public class Test
{
public static void main(String[] args)
{
Car c=new Car();
System.out.println("父类引用指向子类对象时,重写时实际调用的方法为:");
c.startUp();
}
}
class Vehicle{
private void startUp()
{
System.out.println("一般车辆的启动方法");
}
}
class Car extends Vehicle{
public int startUp()
{
System.out.println("轿车的启动方法");
return 0;
}
}




程序执行了,其实这一点也不奇怪,由于父类中的“startUp()”方法是private的,根本不能被继承,所以在本例中没有重写,当然也不用遵循重写的规则了。其实子类中的public int startUp()与父类的 private void startUp()没有一点关系,只是两个独立的方法。

开发中一定要记住继承是重写的基础,没有继承,没有重写

8.5.7 静态方法没有重写

前面的重写都是针对非静态方法的,java中的静态方法可以被继承但不能被重写,本小节从以下两方面介绍:

1、非静态方法试图重写静态方法

2、静态方法的隐藏

1、非静态方法试图重写静态方法:

eg:



2、静态方法的隐藏

当子类中重写的方法也是静态的时候,其实质是:将父类的静态方法隐藏,而并非是将其重写,eg:

public class Test
{
public static void main(String[] args)
{
Truck t=new Truck();
Car c=t;
System.out.println("t.brake()调用的方法为:");
t.brake();
System.out.println("t.startUp()调用的而方法为:");
t.startUp();
System.out.println("c.brake()调用的方法为:");
c.brake();
System.out.println("c.startUp()调用的而方法为:");
c.startUp();
}
}
class Car
{
public static void brake()
{
System.out.println("汽车正在刹车");
}
public static void startUp()
{
System.out.println("正在汽车上启动");
}
}
class Truck extends Car
{
public static void brake()//Error:This instance method cannot override the static method from Car
{
System.out.println("正在卡车上刹车");
}
}




在调用子类父类都有的“break()”方法时用父类引用调用的是父类方法,子类引用调用的是子类方法与对象的类型无关,(本例中对象的类型一直都是Truck)这与非静态方法不同

当用子类引用调用子类本身没有,父类有且可继承的startUp()方法时,调用的是从父类继承的方法;

可以总结出,静态方法没有重写可以继承(就算方法名相同但是也是自己的引用调自己)

提示:区分好隐藏和重写,隐藏是根据引用类型来调用,重写是根据对象类型来调用

8.5.8 通过重写扩展父类方法的功能

在实际开发张,很多时候是需要扩展父类的功能,而不是完全抛弃,这样的需求使用关键字super很容易就能实现eg:

public class Test
{
public static void main(String[] args)
{
Car c=new Car();
System.out.println("c.startUp()调用的而方法为:");
c.startUp();
}
}
class Vehicle
{
public void startUp()
{
System.out.println("一般车辆的启动方法");
}
}
class Car extends Vehicle
{
public void startUp()
{
super.startUp();//用super预定义对象引用调用了父类的startUp()方法
System.out.println("轿车的启动方法");
}
}




一旦构成了重写,在子类外面通过子类对象不可能再访问到被重写的父类方法,实际开发中,如果需要父类方法的功能,可以使用本例提供的方法

8.5.9 替代性原则

前面介绍继承覆盖等时候讲了很多规则,其实这些规则的产生都是基于面向对象中的一个非常重要的原理:——替代性原理

替代性原理很简单:就是在任何情况下,子类应该可以替代其父类,正是有了替代性原理,才能够将父类引用指向子类对象,因为可以将子类对象当做父类对象使用。其实方法的重写也一样,就是让新的方法替代老的方法,如果不能替代,将违反重写的目的,其实返回值,参数列表,访问限制规则都是依据替代性原理制定的。

重写方法返回的数据必须可以替代原来返回的数据。原始类型,只有完全相同才能取代,对象引用则必须是原来引用类型的子类或同类,否则新的类型将无法胜任原来类型的任务

参数列表一旦发生变化,也就意味着这个方法的调用方式发生了变化,这么一来更不可能去替代老方法了。

访问限制如果比老方法还严格,那么原先可以访问该方法的代码现在可能访问不到了,这样也就无法替代老方法的功能;

总之,方法重写的规则,最终都可以归纳为替代性原理。

8.6 方法的重载

指在同一个类里面,有两个或两个以上方法名相同,参数序列不同的方法,eg:三角形类可以定义多个名称为area的计算面积的方法,有的接收底和高做参数,有的三条等边做参数等,这样做的好处是,使开发人员不必为同一操作绞尽脑汁取新名字,同时也可以让使用者更容易记住名字。

8.6.1 方法重载的规则

同一个类中名称相同的方法构成重载必须满足:

重载的参数列表各不相同;

返回值类型,访问权限没有特别要求,可以相同可以不同;

eg:下面几个方法其相互间构成重载

public double changeSize(int size,String name,float pattem){//方法体略}
public void changeSize(int size,String name){}
public int changeSize(int size,float pattem){}
void changeSize(float pattem,String name){}


java中用名称和参数序列作为方法的唯一标识,因此,重载的方法对于人而言是名称相同,对于计算机来说是两个完全不同的方法,没有什么关系

8.6.2 重载方法的匹配

当调用方法时,被调用方法所在的类或对象可能存在多个具有相同名称的方法,参数序列决定了调用哪个方法。

基本类型的参数

引用类型的参数

1、基本类型的参数:

public class TestSon
{
public static void main(String[] args)
{
Addr a=new Addr();
System.out.println("2+5="+a.add(2,5));
System.out.println("5+30.8="+a.add(5,30.8));
}
}

class Addr
{
public int add(int i,int j)
{
System.out.println("两个int类型的参数的方法被调用");
return i+j;
}
public double add(double i,double j)
{
System.out.println("两个double类型的参数的方法被调用");
return i+j;
}

}


第二次直接将int类型的5提升为double类型

运行结果:



注意:只能进行类型提升,不能下降;

2、引用类型的参数:

对于参数是对象引用类型的方法来说,一样是由参数类型决定调用哪个方法。与基本类型相同,如果给的参数没有完全匹配的,其会尽可能寻找最兼容该参数的方法eg:

public class TestSon
{
public static void main(String[] args)
{
UseCar userCar=new UseCar();
Vehicle v=new Vehicle();
Car c=new Car();
Truck t=new Truck();
System.out.println("使用Vehicle作为参数调用:");
userCar.show(v);
System.out.println("使用Car作为参数调用:");
userCar.show(c);
System.out.println("使用Truck作为参数调用:");
userCar.show(t);
}
}

class Vehicle{} class Car extends Vehicle{} class Truck extends Car{}
class UseCar{
public void show(Vehicle v)
{
System.out.println("调用的是具有Vehicle参数的方法。");
}
public void show(Car c)
{
System.out.println("调用的是具有Car参数的方法");
}
}


运行结果:



若没有完全匹配的方法,则寻找重载方法的参数类型中最能兼容传递的参数,比如上面的Car是Truck的直接父类。

但是如果使用Vehicle型引用指向Car对象,将匹配的方法是Vehicle型的参数,eg:将上面的主方法修改如下:

public static void main(String[] args)
{
UseCar userCar=new UseCar();
Vehicle v=new Vehicle();
Car c=new Car();
Truck t=new Truck();
System.out.println("使用Vehicle作为参数调用:");
userCar.show(v);
System.out.println("使用Car作为参数调用:");
userCar.show(c);
System.out.println("使用Truck作为参数调用:");
userCar.show(t);
System.out.println("使用Truck作为参数并将Vehicle引用指向Car对象");
Vehicle vehicle=new Car();
userCar.show(vehicle);
}


运行结果:



从图中执行结果可以看出,引用类型决定调用哪个重载方法,而不是对象类型也有一些特殊情况,同时有多个可以匹配的重载方法,但编译器又不能确定哪个更匹配eg:

public class TestSon
{
public static void main(String[] args)
{
UseMethod useMethod=new UseMethod();
System.out.println("用null类型的参数调用:");
useMethod.show(null);//Error:show引用不明确
}
}
class A{}
class B{}
class UseMethod
{
public void show(A a)
{
System.out.println("调用的是具有A类型的参数的方法。");
}
public void show(B b)
{
System.out.println("调用的是具有B类型参数的方法!!");
}
}




由于null可以看成是任意类型,A和B之间没有派生关系,不能说哪个更匹配,想正确编译运行有两种方法。

让A、B间有派生关系(A继承B或B继承A),则系统会匹配子类为入口参数的方法,因为子类范围更窄,看作更匹配

给“null”加上强制类型转换,指出null的具体类型,eg:”u.show((A)null);”或u.show((B)null)

对于引用型和基本数据类型联合组成的参数序列,其规则为前面讲的两种组合起来,匹配时各自遵守各自规则。

8.6.3 重写与重载的区别

准确的讲,重载不是面对对象专有的,其只是高级语言为了使用方便而提供的特性,而重写是完全属于面对对象的,他们的具体区别如下表:



8.7 final与继承

关键字final不但可以修饰变量,而且对类及方法的继承也有很大影响

8.7.1 最终的类

当关键字final用来修饰类时,其含义是该类不能再分子类,任何其他类都不能继承用final修饰的类,即使该类的访问限制为public类型的,也不能被继承,否则将编译报错。

那么什么时候使用final修饰类呢?只有当需要确保类中的所有方法都不要被重写改进时,才应该建立最终(final)类,final关键字将为这些方法提供安全,没有任何人能重写final类中的方法,因为不能继承。

提示:其实java核心库中的很多都是最终类,eg:String 类就是其中一个,因为String的实现有很多特殊机制,若开发人员可以自由继承String类,那么就不能保证String类的正常使用了,下面demo说明final不能被继承:



提示:在实际开发中,只有在特定情况下,才能使用最终类,不要滥用,因为最终类失去了面向对象的一个主要优点—可扩展性。

8.7.2 最终的方法

当用final关键字修饰方法后,该方法中在子类中将无法重写,只能继承(类不是final的但是包含final的方法,final方法能在子类中被调用)eg:

public class Test
{
public static void main(String[] args)
{
Son s=new Son();
s.show();
}
}
class Father
{
public final void show() {
System.out.println("我是final方法,可以被继承,但是不能被重写");
}
}
class Son extends Father
{
}


运行结果为:



final的方法show被成功继承,但是若试图将final方法在子类中重写如下,将Son类修改为:



提示:要恰当使用final方法只有在子类重写某个方法会带来问题时,才能将此方法设为final的方法,一般情况下不必使用;

8.8. abstract与继承

在修饰类与方法的角度,关键字abstract与final的含义是完全相反的,本节从抽象的类与抽象的方法两方面介绍

8.8.1 抽象的类

当人们认识世界时,也会把现实世界很多类具有相同特征的事物归为一个抽象类,例如水果,其是很多具体植物果实的总称(抽象类),当需要拿出一个水果实例时,拿出来的不是苹果,就是香蕉等具体种类的实例,是拿不出只是水果的实例的,在需要一个抽象类实例时,只能用某个具体类的实例来代替。

java是用来抽象和描述现实世界的,因此也提供抽象类,并且其永远不能实例化,其唯一用途用于继承扩展,这与人们人事现实世界的方式是相同的 eg:

abstract class Car
{
private double price;//成员变量
private String brand;
private int speed;
}


当需要声明一个类为抽象类时,应该在类声明前加上abstract关键字

编译上面代码没问题,但是想创建Car的对象,就会报错eg”



Car c;没有问题,可以声明引用;

抽象类不能实例化也就是不能创建对象;

提示:不能将一个类同时标为abstract和final的,这两个类关键字相互冲突

8.8.2 抽象的方法

抽象方法的声明

抽象方法的继承与实现

1、 抽象方法的声明:

抽象方法只有方法声明,没有方法体,使用abstract关键字来声明抽象的方法,**因为抽象方法没有方法体,用分号表示声明的结束**eg:
abstract void startUp();


抽象类与抽象方法之间的关系是:抽象的方法只能存在于抽象类中eg:



抽象类中可以有非抽象的方法,抽象类中的非抽象方法往往是抽象类所有未来子类都具有的,且不会因为子类的不同而具体实现不同的而方法,eg本例中的获取价格的getPrice方法

抽象类中的抽象方法没有方法体,也就是没有方法的实现,是因为这些方法会因为子类的不同而具体实现不同,例如本例中的汽车启动方法StartUp();

提示:抽象方法不能是private的,因为抽象方法需要在子类中重写类实现。

2、 抽象方法的继承与实现

当某个类继承自抽象类时,如果她本身不是抽象类,则必须实现所继承抽象类中的抽象方法,也就是抽象类的第一个非抽象子类必须实现其父类所有的抽象方法,其中也包括父类继承的抽象方法

eg:具有启动(StartUp)方法的抽象车辆类Car,其每种具体子类都必须实现其自己的、专属于某种类型的车辆的具体启动(StartUp)方法eg:

public class Test
{
public static void main(String[] args)
{
Audi_A6 a6=new Audi_A6();
a6.startUp();
a6.turbo();
Audi_A8 a8=new Audi_A8();
a8.startUp();
a8.turbo();
}
}
abstract class Car
{
public abstract void startUp();
}
abstract class Audi extends Car
{
public abstract void turbo();
}
class Audi_A6 extends Audi
{
public void startUp()
{
System.out.println("调用了奥迪A6的启动功能");
}

public void turbo() {
System.out.println("实现了奥迪A6的加速功能");
}
}
class Audi_A8 extends Audi
{
public void startUp()
{
System.out.println("调用了奥迪A8的启动功能..");
}

public void turbo() {
System.out.println("实现了奥迪A8的加速功能");
}
}


上述demo中的Car和Audi分别代表了轿车和奥迪汽车,故不能是具体的类,因为轿车和奥迪汽车还能分很多子类;

Audi_A6、Audi_A8代表奥迪A6和奥迪A8的具体车型,故为非抽象类。

运行结果:



方法永远不可能标识为abstract、final的,abstract必须要重写,而final阻止重写,当然private和abstract也是不能同时修饰方法的,private阻止继承,也就阻止了重写的实现,eg:下面声明的抽象方法均是非法的:

public abstract final void startUp();
private abstract void startUp();


抽象方法也不能用synchronized关键字修饰。

8.9 基于继承的多态

多态性在实际中的含义就是不同的对象有相同的一般轮廓或形态,但具体执行的过程却大相近庭,eg:开车时“遇到红灯时刹车”这与驾驶员驾驶什么具体型号的车无关,所有的车都具有相同轮廓或形态的刹车。

在java中,基于继承的多态就是指对象功能的调用者用超类的引用来进行方法的调用,这样可以很大提高灵活性,因为用超类的引用就能调用各种不同的子类实现,就像汽车驾驶员可以开各种不同的车一样,下面的demo说明了这个问题:

public class TestSon
{
public static void main(String[] args)
{
Car c=new Truck();
System.out.println("调用的方法为:");
c.brake();
c=new Mini();
System.out.println("调用的方法为:");
c.brake();
}
}
abstract class Car
{
public abstract void brake();
}
class Truck extends Car
{
public void brake() {
System.out.println("正在卡车上刹车");
}
}
class Mini extends Car
{
public void brake()
{
System.out.println("正在面包车上刹车");
}
}




在main方法中,驾驶者站在汽车的角度(定义Car型引用c),可以去开卡车(引用c指向Truck对象,同样也可以去开面包车)(引用c指向Mini对象)

从图中可以看出,虽然是在汽车(Car)的角度调用刹车方法,但实际调用的是所指向的具体对象的方法,这就实现了多态,未来它有其他类型汽车的子类也一样可以调用,程序有很好的灵活性

下面列出了java中基于继承多态的实质含义和作用。

实际上,抽象类中的抽象方法只是起到了契约的作用。例如,继承自汽车的非抽象类必须实现具体的刹车方法。父类中的抽象方法个数是具体子类需要实现方法个数的最低限度,不能比其少,但是可以比父类中的抽象方法多,比如一些子类特有的方法

因为具体子类遵守了契约,所以对于调用者而言,只要使用父类的引用就可以使用所有具体子类实现的各种不同功能,父类中的抽象方法,调用者不必了解子类方法中的实现细节。

但是,如果站在具体子类的角度上(使用特定具体子类类型的引用),则就没有这么强的灵活性了,因为特定类型的子类引用只能指向这个类的对象,要想使用其他类型的对象就不方便了,这就是多态性带来的好处,基于继承的多态充分说明,抽象层比具体层耦合的选择性,灵活性更好

提示:后面章节将介绍接口技术,基于接口的多态比基于继承的多态灵活性选择性更大。

第八章完~

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: