拾贰——面向对象程序的三大特性
一、面向对象的三大特点
面向对象有三大特点:封装性、继承性和多态性,它们是面向对象程序设计的灵魂所在。
1.封装的含义
封装( Encapsulation )是将描述某类事物的数据与处理这些数据的函数封装在一起,形成一个有机整体,称为类。类所具有的封装性可使程序模块具有良好的独立性与可维护性,这对大型程序的开发是特别重要的。类中的私有数据在类的外部不能直接使用,外部只能通过类的公有接口方法( 函数 )来处理类中的数据,从而使数据的安全性得到保证。封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而仅需要通过外部接口,特定的访问权限来使用类的成员。
一旦设计好类,就可以实例化该类的对象。我们在形成一个对象的同时也界定了对象与外界的内外界限。至于对象的属性、行为等实现的细节则被封装在对象的内部。外部的使用者和其他的对象只能经由原先规划好的接口和对象交互。
我们可用一个鸡蛋的三重构造来比拟一个对象:
属性( Attributes )类似蛋黄,它隐藏于中心,不能直接接触,它代表的对象的状态( State )。
行为( Behaviors )类似蛋白,它可以经由接口与外界交互而改变内部的属性值,并把这种改变通过接口呈现出来。
接口( Interface )类似蛋壳,它可以与外界直接接触。外部也只能通过公开的接口方法来改变对象内部的属性( 数据 )值,从而使类中数据的安全性得到保证。
2.继承的含义
对象( Object )是类( Class )的一个实例( Instance )。如果将对象比作房子,那么类就是房子的设计图纸。所以面向对象设计的重点是类的设计,而不是对象的设计。继承性是面向对象的第二大特征。继承( Inheritance )是面向对象程序设计中软件复用的关键技术,通过继承,可以进一步扩充新的特性,适应新的需求。这种可复用、可扩充技术在很大程度上降低了大型软件的开发难度,从而提高软件的开发效率。
当我们说某一个新类 A 继承某一既有类 B 时,表示这个新类 A 具有既有类 B 的所有成员,同时对既有类的成员作出修改,或是增加了新的成员。保持已有类的特性而构造新类的过程称为继承。在已有类的基础上新增自己的特性而产生新类的过程称为派生。我们把既有类称为基类( base class )、超类( super class )或者父类( parent class ),而派生出的新类,称为派生类( derived class )或子类( subclass )。
继承可以使得子类自动具有父类的各种属性和方法,而不需要再次编写相同的代码,从而达到类的复用目的。这样,子类 A 可以对父类 B 的定义加以扩充,从而制定出一个不同于父类的定义,让子类具备新的特性。故此,父类也不等于它的子类。
继承的目的在于实现代码重用,对已有的成熟的功能,子类从父类执行 “ 拿来主义 ”。而派生的目的则在于,当新的问题出现时,原有代码无法解决( 或不能完全解决 )时,需要对原有代码进行全部( 或部分 )改造。
3.多态的含义
多态( Polymorphisn ),从字面上理解,多态就是一种类型表现出多种状态。这也是人类思维方式的一种直接模拟,可以利用多态的特征,用统一的标识来完成这些功能。在 Java 中,多态性分为两类。
(1)方法多态性,体现在方法的重载与覆写上
方法的重载是指同一个方法名称,根据其传入的参数类型、个数和顺序的不同,所调用的方法体也不同,即同一个方法名称在一个类中有不同的功能实现。
方法的覆写是指父类之中的一个方法名称,在不同的子类有不同的功能实现,而后依据实例化子类的不同,同一个方法,可以完成不同的功能。
(2)对象多态性,体现在父、子对象之间的转型上
在这个层面上,多态性是允许将父对象设置成为一个与一个或更多的子对象相等的技术,通过赋值之后,父对象就可以根据当前赋值给的不同子对象,以子对象的特性加以运作。多态意味着相同的( 父类 )信息发送给不同的( 子 )对象,每个子对象表现出不同的形态。
多态中的一个核心概念就是,子类( 派生类 )对象可以视为父类( 基类 )对象。如鱼( Fish )类、鸟( Bird )类和马( Horse )类都继承与父类 Animal(动物)。
在 Java 编程里,我们可以用如下来描述:
Animal a;
Fish f = new Fish();
Bird b = new Bird();
House h = new Horse();
//统“一”的外部接口,表现出“多”种不同形态
a = f; a.move(); //鱼儿游
a = b; a.move(); //鸟儿飞
a = h; a.move(); //马儿跑
在上述代码中,分别实例化父类对象 a 以及子类对象 f、b 和 h。由于 Fish 类、Bird 类和 Horse 类均继承与父类 Animal,所以子类均继承了父类的 move() 方法。由于父类 Animal 的 move() 过于抽象,不能反映 Fish、Bird 和 Horse 等子类中 “ 个性化 ” 的 move() 方法。这样,我们在完成定义后,自然可以做到如下所示:
f.move(); //完成鱼类对象f的移动:鱼儿游
b.move(); //完成鱼类对象b的移动:鸟儿飞
h.move(); //完成鱼类对象h的移动:马儿跑
这并不是多态的表现,我们希望统一用父类对象 a 来接收子类对象 f、b、h,然后用统一的接口 “ a.move() ”,展现出不同的形态:当 “ a=f ” 时,“ a.move() ” 表现出的是子类 Fish 的 move() 方法——鱼儿游,而非父类的 move() 方法,以此类推。这样,就达到了 “ 一对多 ” 的效果——多态就在这里。
父、子对象之间的转型包括以下两种形式。
(1)向上转型( Upcast )( 自动转型 ):父类 父类对象 = 子类实例
将子类对象赋值给父类对象,这样将子类对象自动转换为父类对象。这种转换方式是安全的。例如,我们可以说鱼是动物,鸟是动物,马是动物。这种向上转型在多态中应用的很广泛。
(2)向下转型( Downcast )( 强制转型 ):子类 子类对象 = ( 子类 )父类对象
将父类对象赋值给子类对象。这种转换方式是非安全的。例如,我们说动物是鱼,动物是鸟,动物是马,这类描述是不全面的。因此,在特定背景下如果需要父类对象转换为子类对象,就必须使用强制类型转换。这种向下转型用的比较少。
二、封装的实现
下面具体讨论面向独享的第一大特性——封装性。
1.Java 访问权限修饰符
在Java 中有四种访问权限:公有( public )、私有( private )、保护( protected )、默认( default )。但访问权限修饰符只有三种,因为默认访问权限没有访问权限修饰符。默认访问权限是包访问权限,即在没有任何修饰符的情况下定义的类,属性和方法在一个包内都是客访问的。具体访问权限的规定如下表所示:
私有( private) | 默认( default ) | 保护( protected ) | 公有( public ) | |
类 | 只有内部类允许私有,只能在当前类中被访问 | 可以被当前包中的所有类访问 | 只有内部类可以设为保护权限,相同包中的类和其子类可以访问 | 可以被所有类访问 |
属性 | 只能被当前类访问 | 可以被相同包中的类访问 | 可以被相同包中的类和当前类的子类访问 | 可以被所有的类访问 |
方法 | 只能被当前类访问 | 可以被相同包中的类访问 | 可以被相同包中的类和当前类的子类访问 | 可以被所有的类访问 |
2.类的封装实例
用对象直接访问类中的属性,这在面向对象法则中是不允许的。所以为了避免程序中这种错误的发生,一般的在开发中往往要将类中的属性封装( private )。
也就是在声明属性前,多个修饰符 private( 私有的 )。但由于属性为私有数据类型,所以外界不能由对象直接访问这些私有属性。
程序设计人员一般在类的设计时,都会设计存或去这些属性的公共接口,这些接口的外在表现形式都是公有( public )方法。而在这些方法里,我们可以对存或取属性的操作,实施合理的检查,以达到保护属性数据的目的。通常,对属性值设置的方法被命名为 SetXxx(),其中 Xxx 为任意有意义的名称,这类方法可统称为 Setter 方法,而对取属性值的方法通常被命名为 GetYyy,其中 Yyy 为任意有意义的名称,这类方法可统称为 Getter 方法。
当然,用 private 也可以封装方法,封装的形式如下:
封装属性:private 属性类型属性名
封装方法:private 方法返回类型方法名称(参数)
提示:用 private 声明的属性或方法只能在其类的内部被调用,而不能在类的外部被调用。
举例:
//添加被private(私有)所封装的一个MakeSound()方法
//方法的封装使用
public class TestCat
{
public static void main(String[] args)
{
MyCat aCat = new MyCat();
aCat.SetWeight(-10); //设置MyCat的属性值
float temp = aCat.GetWeight(); //获取MyCat的属性值
System.out.println("The weight of a cat is:" + temp);
aCat.MakeSound();
}
}
class MyCat
{
private float weight; //通过private修饰符,封装MyCat的属性
public void SetWeight(float wt)
{
if(wt>0)
{
weight = wt;
}
else
{
System.out.println("weight 设置非法(应该>0).\n采用默认值10");
weight = 10.0f;
}
}
public float GetWeight()
{
return weight;
}
private void MakeSound()
{
System.out.println("weight:" + weight);
}
}
一旦方法的访问权限被声明为 private( 私有的 ),那么这个方法就只能在类的内部被访问了。若想让上例代码编译成功,则需改为如下:
public float GetWeight()
{
MakeSound(); //方法内添加的方法调用
return weight;
}
访问权限控制符是对类外而言的,而在同一类中,所有的类成员属性及方法都是相互可见的,也就是说,它们之间是可以相互访问的。
如果类中的某些数据在初始化后不想再被外界修改,则可以使用构造方法配合私有化的 Setter 函数来实现该数据的封装。
如下:
//使用构造函数实现数据的封装
class MyCat {
//创建私有化的属性weight,height
private float weight;
private float height;
//在构造函数中初始化私有变量
public MyCat(float height,float weight)
{
SetHeight(height); //调用私有方法设置height
SetWeight(weight); //抵用私有方法设置weight
}
//通过private修饰符,封装MyCat的SetWeight方法
private void SetWeight(float wt)
{
if(wt>0)
{
weight = wt;
}
else
{
System.out.println("weight设置非法(应该>0).\n采用默认值10");
weight = 10.0f;
}
}
//通过private修饰符,封装MyCat的SetHeight方法
private void SetHeight(float ht)
{
if(ht>0)
{
height = ht;
}
else
{
System.out.println("weight设置非法(应该>0).\n采用默认值20");
height = 20.0f;
}
}
//创建公有方法GetWeight()作为与外界的通信的接口
public float GetWeight()
{
return weight;
}
//创建公有方法GetHeight()作为与外界的通信的接口
public float GetHeight()
{
return height;
}
}
public class TestEncapsulation
{
public static void main(String[] args)
{
MyCat aCat = new MyCat(12,-5); //通过公有接口设置属性值
float ht = aCat.GetHeight(); //通过公有接口获取属性值height
float wt = aCat.GetWeight(); //通过公有接口获取属性值weight
System.out.println("height:" + ht);
System.out.println("weight:" + wt);
}
}
构造方法只能在实例化对象时自动调用一次,而 SetHeight() 方法和 SetWeight() 方法的访问权限为私有类型,外界不能调用,这样就实现了封装的目的。
通过构造函数进行初始化类中的私有属性能够达到一定的封装效果,但是也不能过度相信这种封装,有些情况下即使这样做,私有属性也有可能被外界修改。
提示:一般来说设计较好的程序的类中的属性都是需要封装的。此时要设置或取得属性值,则只能使用 Setter 和 Getter 方法,这是一个比较标准的做法。
3.封装问题的总结
在 Java 中,最基本的封装单元是类,类是基于面向对象思想编程语言的基础,程序员可以把具有相同业务性质的代码封装在一个类里,通过接口方法向外部代码提供服务,同时向外部代码屏蔽类里服务的具体实现方式。
数据封装的最重要的目的是在于要实现 “ 信息隐藏( Information Hidding )”。在类中的 “ 数据成员( 属性 )” 或者 “ 方法成员 ”,可以使用关键字 “ public ”、“ private ”、“ protected ” 来设置各成员的访问权限。
封装性是面向对象程序设计的原则之一。它规定对象应对外部环境隐藏它们的内部工作方式。良好的封装可以提高代码的模块化成都,它防止了对象之间不良的相互影响。使程序达到强内聚( 许多功能尽量在类的内部独立完成,不让外面干预 ),弱耦合( 提供给外部尽量少的方法调用 )的最终目标。
4.实现封装应该注意的问题
实现封装是对外部而言的,我们总是要有选择地提供一些类似于 setXxx() 或者 getYyy() 的公有接口,以 “ 可控 ” 的方式来设置或读取类内部的属性值。有些属性值在初始化以后就不再允许再进行修改,对这样的属性不设置 setXxx() 方法是明智的,或者这类设置 Setter 方法的访问权限设置为私有的,然后直接通过构造方法来调用这些方法,从而实现一次性的初始化。
举例:
//返回引用数据时应注意的问题
import java.util.ArrayList;
class TestReturn
{
//定义一个私有的ArrayList属性
private ArrayList<Integer> intArray = new ArrayList<Integer>();
TestReturn()
{
//通过构造函数对其进行初始化
intArray.add(1);
intArray.add(2);
intArray.add(3);
}
//设置该私有数据对应的get函数
ArrayList<Integer> getIntArray()
{
return intArray;
}
}
public class ReturnVariable
{
public static void main(String[] args)
{
TestReturn testReturn = new TestReturn();
//得到该私有数据,不是副本,而是引用
ArrayList<Integer> intArray = testReturn.getIntArray();
System.out.println(intArray.size());
intArray.add(4); //修改其值
ArrayList<Integer> intArray2 = testReturn.getIntArray();
//该类内部的私有变量已经被改变
System.out.println("在外部修改其私有变量以后其长度为:" + intArray2.size());
}
}
当通过 Getter 方法返回私有变量时,返回的是对该私有变量对象的引用,而不是其副本。引用的含义在于,通过它直接能找到所操作对象在内存中的原始位置,在该类的外部对其进行的修改会影响到内部。这类问题的解决办法是,如果返回值是对数据的引用则显式创建该数据的副本,然后返回该副本即可。
三、继承的实现
对于面向对象的程序而言,它的精华在于类的继承能以既有的类为基础,进而派生出新的类。通过这种方式,便能快速地开发出新的类,而不需编写相同的程序代码,这就是程序代码再利用的概念。
1.继承的基本概念
在 Java 中,通过集成可以简化类的定义,扩展类的功能。在 Java 中支持类的单继承和多层继承,但是不支持多继承,即一个雷智能继承一个类而不能继承多个类。
实现继承的格式如下:
class 子类名 extends 父类
extends 是 Java 中的关键字。Java 继承只能直接继承父类中的公有属性和公有方法,而隐含的( 不可见的 )继承了私有属性。
现在假设有一个 Person 类,里面有 name 与 age 两个属性,而另外一个 Student 类,需要有 name、age、school 等 3 个属性。由于 Person 中已存在有 name 和 age 两个属性,所以不希望在 Student 类中再次重新声明这两个属性,这时就需考虑是否可将 Person 类中的内容继续保留到 Student 类中,这就引出了继承的概念。
在这里希望 Student 类能够将 Person 类的内容继承下来后继续使用,这样就可以达到代码复用的目的。
Java 类的继承可用下面的语法来表示:
class 父类
{
//定义父类
}
class 子类 extends 父类
{
//用extends关键字实现类的继承
}
2.实现继承
我们使用继承完成相同的功能。
举例:
//类的继承示例
class Person
{
String name;
int age;
Person(String name,int age)
{
this.name = name;
this.age = age;
}
void speak()
{
System.out.println("我的名字叫:" + name + "我" + age + "岁");
}
}
class Student extends Person
{
String school;
Student(String name,int age,String school)
{
//对于来自父类的数据成员需要调用父类的构造方法
super(name,age);
this.school = school;
}
void study()
{
System.out.println("我在" + school + "读书");
}
}
public class InheritDemo
{
public static void main(String[] args)
{
//实例化一个Student对象
Student s = new Student("张三", 22, "北大");
s.speak();
s.study();
}
}
对于来自父类的数据成员需要调用父类的构造方法,例如,使用 super 关键字加上对应的参数,就是调用父类的构造方法。
提示:在 Java 中只允许单继承,而不允许多重继承,也就是说一个子类只能有一个父类。但在 Java 中允许多层继承。
3.继承的限制
以上实现了继承的基本要求,但是对于继承性而言实际上也存在着若干限制,下面进行详细说明:
(1)Java 之中不允许多重继承,但是却可以使用多层继承
所谓的多重继承指的一个类同时继承多个父类的行为和特征功能。所谓的多层继承,是指一个类 B 可以继承自某一个类 A,而另外一个类 C 又继承自 B,这样在继承层次上单项继承多个类,如下所示:
class A
{
}
class B extends A
{
}
class C extends B //正确:多层继承
{
}
类 B 继承了类 A,而类 C 又继承了类 B,也就是说类 B 是类 A 的子类,而类 C 则是类 A 的孙子类。此时,C 类就将具备 A 和 B 两个类的功能,但是一般情况下,在我们所编写的代码时,多重继承的层数之中不宜超过三层。
(2)从父类继承的私有成员,不能被子类直接使用
子类在继承父类的时候会将父类之中的全部成员( 包括属性及方法 )继承下来,但是对于所有的非私有( private )成员属于显式继承,而对于所有的私有成员采用隐式继承( 即对子类不可见 )。子类无法直接操作这些私有属性,必须通过设置 Setter 和 Getter 方法间接操作。
(3)子类在进行对象实例化时,从父类继承而来的数据成员需要先调用父类的构造方法来初始化,然后再用子类的构造方法来初始化本地的数据成员
子类继承了父类的所有数据成员,同时子类也可以添加自己的数据成员。但是,需要注意的是,调用构造方法实施数据成员初始化时,一定要 “ 各司其职 ”,即来自父类的数据成员,需要调用父类的构造方法来初始化,而来自子类的数据成员初始化,要在本地构造方法中完成。在调用次序上,子类的构造方法要遵循 “ 长辈优先 ” 的原则,先调用父类的构造方法( 生成父类对象 ),然后再调用子类的构造方法( 生成子类对象 )。也就是说,当实例化子类对象时,父类的对象会先 “ 诞生 ”——这符合我们现实生活中对象存在的伦理。
(4)被 final 修饰的类不能再被继承
Java 的继承性确实在某些时候可以提高程序的灵活性和代码的整洁度,但是有时我们定义了一个类却不想让其被继承,即所有的继承关系到此为止。为此,Java 提供了 final 关键字来实现这个功能。final 在 Java 之中称为终结器。通过在类的前面添加 final 关键字便可以阻止该类被继承。
四、深度认识类的继承
1.子类对象的实例化过程
既然子类可以直接继承父类中的方法与属性,那父类中的构造方法是如何处理的呢?子类对象在实例化时,子类对象实例化会默认先调用父类中的无参构造函数,然后再调用子类构造方法。
其中,如果用户显式用 super() 去调用父类的构造方法,那么它必须出现在这个子类构造方法中的第 1 行语句。
2.super 关键字的使用
从英文本意来说,它表示 “ 超级的 ”,从继承体系上,父类相对于子类是 “ 超级的 ”,故此,有时候我们也称父类为超类( super-class )。
super 关键字出现在子类中,而且目的是调用父类中的构造方法,由此可得出初步结论,super 主要的功能是完成子类调用父类中的内容,也就是调用父类中的属性或方法。
举例:
//super调用父类中的构造方法
class Person
{
String name;
int age;
public Person(String name,int age) //父类的构造方法
{
this.name = name;
this.age = age;
}
}
class Student extends Person
{
String school;
//子类的构造方法
public Student(String name,int age,String school)
{
super(name,age); //用super调用父类中的构造方法
this.school = school;
}
}
public class SuperDemo
{
public static void main(String[] args)
{
Student s = new Student("张三",20,"北大");
System.out.println("name:" + s.name + ",age:" + s.age + ",school:" + s.school);
}
}
调用 super() 必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有提供这种形式的构造方法,那么在编译的时候就会报错。
事实上,super 关键字不仅可用于调用父类中的构造方法,也可用于调用父类中的属性或方法,如下所示:
super.父类中的属性;
super.父类中的方法();
举例:
//改造代码,使用super调用父类中的构造方法
class Person
{
String name;
int age;
//父类的构造方法
public Person()
{
}
public String talk()
{
return "name:" + this.name + "age:" + this.age;
}
}
class Student extends Person
{
String school;
//子类的构造方法
public Student(String name,int age,String school)
{
//在这里用super调用父类中的属性
super.name = name;
super.age = age;
//调用父类中的talk()方法
System.out.print(super.talk());
//调用本类中的school属性
this.school = school;
}
}
public class SuperDemo
{
public static void main(String[] args)
{
Student s = new Student("张三",20,"北大");
System.out.println("school:" + s.school);
}
}
super 是相对于继承而言的。super 代表的是当前类的父类,而 this 这是代表当前类。如果父类的属性和方法访问权限不是 private( 私有的 ),那么这些属性和方法在子类中是可视的,换句话说,这些属性和方法也可视为当前类所有的,那么用 “ this. ” 来访问也是理所当然的。如果子类对 “ 父类 ” 很 “ 见外 ”,分的很清楚,那么就可用 “ super. ” 访问来自于父类的属性和方法。
3.限制子类的访问
在有些时候,父类并不希望子类可以访问自己的类中全部的属性或方法,所以需要将一些属性与方法隐藏起来,不让子类去使用。为此可在声明属性或方法时加上 “ private ” 关键字,表示私有访问权限,即除了声明该属性或方法的所在类,其他外部的类均无权访问。虽然父类的私有成员,外部( 包括子类 )无法访问,但是在父类内部,属性和方法彼此之间是不受访问权限约束的。针对这样的限制,我们可以用父类的方法( 例如构造方法 )来访问父类的私有数据成员。
五、覆写
“ 覆写( Override )” 的概念与 “ 重载( Overload )” 有相似之处。所谓 “ 重载 ”,即方法名称相同,方法的参数不同( 包括类型不同、顺序不同和个数不同 ),也就是它们的方法签名( 包括方法名 + 参数列表 )不同。重载以表面看起来一样的方式——方法名相同,却通过传递不同形式的参数,来完成不同类型的工作,这样 “ 一对多 ” 的方式实现 “ 静态多态 ”。
1.方法的覆写
当一个子类继承一个父类,如果子类中的方法与父类中的方法的名称、参数个数及类型且返回值类型等都完全一致时,就称子类中的这个方法覆写了父类中的方法。同理,如果子类中重复定义了父类中已有的属性,则称此子类中的属性覆写了父类中的属性。如下:
class Super //父类
{
返回值类型 方法名(参数列表)
{
}
}
class Sub extends Super //子类
{
返回值 方法名(参数列表) //与父类的方法同名,覆写父类中的方法
}
举例:
//子类覆写父类的实现
class Person
{
String name;
int age;
public String talk()
{
return "name:" + this.name + "age:" + age;
}
}
class Student extends Person
{
String school;
public Student(String name,int age,String school)
{
//分别为属性赋值
this.name = name; //super.name = name;
this.age = age; //super.age = age;
this.school = school;
}
//此处覆写Person中的talk()方法
public String talk()
{
return "school:" + this.school;
}
}
public class Override
{
public static void main(String[] args)
{
Student s = new Student("张三",20,"北大");
//此时调用的是子类中的talk()方法
System.out.println(s.talk());
}
}
如果需要调用父类中的方法,可以修改代码为如下所示:
//super调用父类的方法
class Person
{
String name;
int age;
public String talk()
{
return "name:" + this.name + "age:" + age;
}
}
class Student extends Person
{
String school;
public Student(String name,int age,String school)
{
//分别为属性赋值
this.name = name; //super.name = name;
this.age = age; //super.age = age;
this.school = school;
}
//此处覆写Person中的talk()方法
public String talk()
{
return super.talk() + "school:" + this.school;
}
}
public class Override
{
public static void main(String[] args)
{
Student s = new Student("张三",20,"北大");
//此时调用的是子类中的talk()方法
System.out.println(s.talk());
}
}
由于父类的 talk() 方法返回的是一个字符串,因此可以用连接符 “ + ”,连接来自子类的字符串,这样拼接的结果一起又作为子类的 talk() 方法的返回值。
在完成方法的覆写时,需注意以下几点:
(1)覆写的方法的返回值类型必须和被覆写的方法的返回值类型一致。
(2)被覆写的方法不能为 static。
如果父类中的方法为及静态的,而子类中的方法不是静态的,但是两个方法除了这一点外其它都满足覆写条件,仍然会发生编译错误。反之亦然。即使父类和子类中的方法都是静态的,并且满足覆写条件,但是仍然不会发生覆写,因为静态方法在编译时就和类的引用类型进行匹配。
(3)被覆写的方法不能拥有比父类更为严格的访问控制权限。
访问权限的大小通常依据下面的次序:私有( private )< 默认( default )< 公有( public )。如果父类的方法使用的是 public 定义,那么子类覆写时,权限只能是 public,如果父类的方法是 default 权限,则子类覆写,方法可以使用 default 或者是 public。也就是说,子类方法的权限一般要比父类大,至少相等。
2.属性的覆写
所谓的属性覆盖指的是子类定义了和父类之中名称相同的属性。如下:
class Book
{
String info = "Hello";
}
class ComputerBook extends Book
{
int info = 100; //属性名称与父类相同
public void print()
{
System.out.println(info);
System.out.println(super.info);
}
}
public class OverrideData
{
public static void main(String[] args)
{
ComputerBook cb = new ComputerBook(); //实例化子类对象
cb.print();
}
}
由于在父类 Book 中,info 的访问权限为默认类型( 即其前面没有任何修饰符 ),那么在子类 ComputerBook 中,从父类继承而来的字符串类型的 info,子类是可以感知到的,可以通过 “ super.父类成员 ” 的模式访问。
然而,上述代码并没有实现真正的覆写,从开发角度来说,为了满足类的封装性,类中的属性一般都需要使用 private 封装,一旦封装之后,子类压根就 “ 看不见 ” 父类的属性成员,子类定义的同名属性成员,其实就是一个 “ 全新的 ” 数据成员,所谓的覆写操作就完全没有意义了。
六、多态的实现
面向对象中第 3 个重要的特性——多态性。
1.多态的基本概念
重载的表现形式就是调用一系列具有相同名称的方法,这些方法可根据传入参数的不同而得到不同的处理结果,这其实就是多态性的一种体现,这属于静态多态,即同一种接口,不同的实现方式。这种多态是在代码编译阶段就确定下来的。还有一种多态形式,在程序运行阶段才能体现出来,这种方式称为动态联编,也称为晚期联编( late bingding )。
2.方法多态性
在 Java 中,方法的多态性体现在方法的重载。方法的多态即是通过传递不同的参数来令同一方法接口实现不同的功能。
举例:
public class FuncPoly
{
//定义了两个方法名完全相同的方法,该方法实现求和的功能
void sum(int i)
{
System.out.println("数字和为:" + i);
}
void sum(int i,int j)
{
System.out.println("数字和为:" + (i+j) );
}
public static void main(String[] args)
{
FuncPoly demo = new FuncPoly();
demo.sum(1); //计算一个数的和
demo.sum(2,3); //计算两个数的和
}
}
同一个方法( 方法名是相同的 )能够接受不同的参数,并完成多个不同类型的运算,因此体现了方法的多态性。
3.对象多态性
首先需要了解两个概念:向上转型和向下转型。
(1)向上转型:父类对象通过子类对象去实例化,实际上就是对象的向上转型。向上转型是不需要进行强制类型转换的,但是向上转型会丢失精度。
(2)向下转型:与向上转型对应的一个概念就是 “ 向下转型 ”,所谓向下转型,也就是说父类的对象可以转换为子类对象,但是需要注意的是,这时必须要进行强制的类型转换。
以上内容可以概括成下面的两句话:
(1)向上转型可以自动完成。
(2)向下转型必须进行强制类型转换。
值得注意的是,并非全部的父类对象都可以强制转换为子类对象,毕竟这种转换是不安全的。
举例:
//使用多态
class Animal
{
public void move()
{
System.out.println("动物移动");
}
}
class Fish extends Animal
{
//覆写了父类中的move方法
public void move()
{
System.out.println("鱼儿游");
}
}
class Bird extends Animal
{
//覆写了父类中的move方法
public void move()
{
System.out.println("鸟儿飞");
}
}
class Horse extends Animal
{
//覆写了父类中的move方法
public void move()
{
System.out.println("马儿跑");
}
}
public class ObjectPoly
{
public static void main(String[] args)
{
Animal a;
Fish f = new Fish();
Bird b = new Bird();
Horse h = new Horse();
a = f; a.move(); //调用Fish的move()方法,输出"鱼儿游"
a = b; a.move(); //调用Bird的move()方法,输出"鸟儿飞"
a = h; a.move(); //调用Horse的move()方法,输出"马儿跑"
}
}
父类对象依据被赋值的每个子类对象的类型,做出恰当的响应( 即与对象具体类别相适应的反应 ),这就是对象多态性的关键思想。同样的消息或接口在发送给不同的对象时,会产生多种形式的结果,这就是多态性的本质。利用对象多态性,我们可以设计和实现更具扩展性的软件系统。
提示:简单来说,继承是子类使用父类的方法,而多态则是父类使用子类的方法。但更为确切来说,多态是父类使用被子类覆盖的同名方法,如果子类的方法是全新的,父类不存在同名的方法,则父类也不能使用子类自己独有的 “ 个性化 ” 方法。
有一点需要注意的是,即使实施向上转型,父类对象所能看见的方法依然还是本类之中所定义的方法( 即被子类覆盖的方法 )。如果子类扩充了一些新方法的话,那么父类对象是无法找到的。
如下:
class A
{
public void print()
{
System.out.println("父类A:public void print(){}");
}
}
class B extends A
{
public void print() //方法覆写
{
System.out.println("子类B:public void print(){}");
}
public void getB() //此方法为子类扩充的功能
{
System.out.println("子类B:public void getB(){},B类扩充方法");
}
}
public class NewMethodTest
{
public static void main(String[] args)
{
A a = new B(); //实例化子类对象
a.print(); //调用子类B的print()
//a.getB(); //这个方法父类无法找到
}
}
如果除掉最后一行代码的注释符号 “ // ”,就会产生编译错误:“ 没有为类型 A 定义方法 getB() ”。尽管这个父类对象 a 的实例化依靠的是子类完成的,但是它能够看见的还是自己本类所定义的方法名称,如果方法被子类覆写了,则调用的方法体也是被子类所覆写过的方法。如果非要去调用 B 类的 getB() 方法,那么就需要进行向下转型,即将父类对象变为子类实例,向下转型是需要采用强制转换的方式完成的。
如下:
class A
{
public void print()
{
System.out.println("父类A:public void print(){}");
}
}
class B extends A
{
public void print() //方法覆写
{
System.out.println("子类B:public void print(){}");
}
public void getB() //此方法为子类扩充的功能
{
System.out.println("子类B:public void getB(){},B类扩充方法");
}
}
public class DownCastTest
{
public static void main(String[] args)
{
A a = new B(); //实例化子类对象
a.print(); //调用子类B的print()
B b = (B)a; //向下转型,强制完成
b.getB(); //子类对象b可找到getB()
}
}
我们可以用一句话来概括这类关系,“ 在形式上,类定义的对象只能看到自己所属类中的成员 "。虽然通过向上类型转换,子类对象可以给父类对象赋值,但父类对象也仅能看到在子类中被覆盖的成员( 这些方法也在父类定义过了 ),父类对象无法看到子类的新扩充方法。
4.隐藏
当子类覆写了父类的同名方法时,如果用子类实例化父类对象,会发生向上类型转换,这时调用该方法时,会自动调用子类的方法,这时实现多态的基础。但是在某些场景下,我们不希望父类的方法被子类方法覆写,即子类实例化后会调用父类的方法而不是自类的方法。这就需要用到另外一个概念——隐藏( Hide )。被关键词 static 修饰的静态方法是不能被覆盖的,Java 就是利用这一个特性达到隐藏的效果。
如下:
//隐藏子类的成员
class Father
{
public static void overWritting()
{
System.out.println("父类方法");
}
}
class Son extends Father
{
public static void overWritting() //方法覆写
{
System.out.println("子类方法");
}
}
public class HideSubClass
{
public static void main(String[] args)
{
Father dad = new Son();
dad.overWritting();
Father.overWritting();
Son.overWritting();
}
}
在调用 dad 的 overWritting() 方法时,没有被子类覆盖,这就是说父类 “ 隐藏 ” 了子类的同名方法。
而事实上,所有的静态方法都隶属于类,而非对象。所以,可以通过 “ 类名.静态方法名 ” 的方法来直接访问静态方法,在这样的情况下,“ 父类 ” 与 “ 子类 ” 之间的方法就不会存在谁隐藏谁的问题。在 Java 中,“ 隐藏 ” 概念的应用并不广泛。
七、本文注意事项
1.方法重载( Overload )和覆写( Override )的区别
重载是指在相同类内定义名称相同但参数个数或类型或顺序不同的方法,而覆写是在子类当中定义名称、参数个数与类型均与父类相同的方法,用于覆写父类中的方法。具体的区别如下表:
区别 | 重载 | 覆写 |
英文单词 | Overload | Override |
定义 | 方法名称相同、参数的类型及个数和顺序至少一个不同 | 方法名称、参数的类型及个数、返回值类型完全相同 |
范围 | 只发生在一个类之中 | 发生在类的继承关系中 |
权限 | 不受权限控制 | 被覆写的方法不能拥有比父类更严格的访问控制权限 |
在重载的关系之中,返回值类型可以不同,语法上没有错误,但是从实际的应用而言,建议,返回值类型相同。
2.this 和 super 的区别
区别 | this | super |
查找范围 | 先从本类找到属性或方法,本类找不到再查找父类 | 不查询本类的属性及方法,直接由子类调用父类的指定属性及方法 |
调用构造 | this 调用的是本类构造方法 | 由子类调用父类构造 |
特殊 | 表示当前对象 |
由于 this 和 super 都可以调用构造方法,所以 this() 和 super() 语法不能同时出现,两者是二选一的关系。
3.final 关键字的使用
final 在 Java 之中称为终结器,在 Java 之中 final 可以修饰三类情况:修饰类、修饰方法及修饰变量。
(1)使用 final 修饰的类不能有子类( 俗称太监类 )
如果父类的方法不希望被子类覆写,可在父类的方法前加上 final 关键字,这样该方法便不会有覆写的机会。
(2)使用 final 定义的方法不能被子类所覆写
在父类中,将方法设置 final 类型的操作,实际编程时用途并不广泛,但是在一些系统架构方面会出现比较多。
(3)使用 final 定义的变量就成为了常量
常量必须在其定义的时候就初始化( 即给予赋值 ),这样用 final 修饰的变量就变成了一个常量,其值一旦确定后,便无法在后续的代码中再做修改。一般来说,为了将常量和变量分开来,常量的命名规范要求全部字母采用大写方式表示。