几乎所有面向对象的高级程序设计语言都有继承机制,并且用这一非常简单而强大的机制实现代码重用,提高代码效率。在Java语言中,被继承的类成为超类(Superclass),与此对应的是其子类(Subclass);在c++中我们通常简称基类(Baseclass)和子类。但实质都是一样。
接下来,我从几个方面谈谈我对Java类继承的理解:
- 使用关键字extends
- 继承中成员访问控制
- 巧用super简化构造函数
- 超类引用能引用子类对象
- 继承中的重写机制--抽象类
- final实现封装特性
Part one : 使用关键字 extends
在c++中,继承有三种形式,分别是公有继承,私有继承,保护继承。这三种继承都会改变继承后子类对基类成员的访问可见性。除此之外,还有友元类与友元函数等。总体而言,c++的继承机制,说好听点,就是严谨细致;说不好听点,就显得十分烦琐,而且不太容易熟记(当然大神除外!)。所以Java继承中,继承方式只要extends一种,也就是只能使用关键字extends声明继承关系。需要注意一点:Java只允许单继承,但允许多层次继承和多宽度继承:一个子类只能有一个超类,一个超类可以有多个子类。
- public 表示公有继承;
- private 表示私有继承;
- protected 表示保护继承;
usingExtends1.java 使用 extends
class Superclass { //超类
String name; //超类一个表名字的String类型成员
public void showName() //成员函数
{
System.out.println("My name is "+ name);
}
}
class Subclass extends Superclass{ //子类继承超类的成员函数
public void sayHi() //子类新添加函数
{
System.out.println("Hello!");
}
public static void main(String args[]) //main函数
{
Subclass subcls = new Subclass(); //创建子类对象,并调用超类和子类的成员函数
subcls.sayHi();
subcls.showName();
}
}
// output:
Hello!
My name is null
解释:上面的例子是一个非常简单的关于Java继承的小程序,创建的子类Subclass成功继承了超类的成员变量和成员函数,并且实现了扩展,调用了自己新添加的函数sayHi(),与extends英文的意思不谋而合。以此小程序来展现Java的继承机制中extends的使用,但是问题来了,子类是如何访问超类中的成员变量name的,下面做了一些小的修改:
usingExtends2.java
class Superclass {
String name;
Superclass(String nam1){name = nam1;}; //定义了超类的构造函数
public void showName(String nam)
{
System.out.println("My name is "+ nam);
}
}
class Subclass extends Superclass{
Subclass(String nam) //子类的构造函数,super()的使用接下来详细介绍
{
super(nam);
}
public void sayHi()
{
System.out.println("Hello!");
}
public static void main(String args[])
{
Subclass subcls = new Subclass("David Lee");
subcls.sayHi();
subcls.showName(subcls.name); //子类对象成功引用了超类的类成员变量name!!!
}
}
正如我们所看到的,子类对象subcls调用了超类的showName(),并且成功的访问了超类的成员变量name。初学Java的时候,这点需要注意:Java的继承机制虽然在形式上简化了,但是继承的访问权限限制,以及成员变量可见性还是要谙熟于心滴。上面的usingExtends2.java程序中,正是因为超类中name变量未用private修饰。使超类成员变量对于同包的子类(派生类)完全可见,并不是像c++中的,当类中成员变量未用限制修饰符修饰的时候,默认为private。下面,我整理一个表格理解Java这种继承机制的访问限制。
Part two : 继承中成员访问限制
1.如果类成员没有显示的访问修饰符,那么其在所在包中可见,包外不可见。(本包public,外包private)
2.如果类成员被private修饰,那么在同一类中可见。
3.如果类成员被protected修饰,那么扩充了默认访问范围,不仅对包内可见,对其全部子类(包括其他包的子类)都是可以访问的。
4.如果类成员被public修饰,那么该成员就是一种赤裸裸的存在,可被其他任何代码一览无余!!!
注意:修饰类只能是默认或public;而修饰类成员可用默认、private、protected、public。
关于protected的访问限制,大部分Java书籍对此介绍的都十分笼统,只是简单说明,同一包中成员和子类可以访问protected成员。可能会让不少初学者感到疑惑。对此,需要明确知道两点:
-
基类的protected成员是包内可见的,并且对子类可见;
-
若子类与基类不在同一包中,那么在子类中,子类实例可以访问其从基类继承而来的protected方法,而不能访问基类实例的protected方法。
更详细的理解请参考 protected 关键字的真正内涵
Part three : 巧用super简化构造函数
3.1 fruit.java 未实例化创建子类对象。通过类名直接调用static函数。
package fruit;
public class fruit { //超类fruit类,包含一个default型成员变量name
String name;
protected String isName() //成员函数
{
System.out.println("This is a/an " + name);
return name;
}
}
class banana extends fruit{ //子类banana
static String color; //static 成员变量color
static private int genre = 1;
public static void isColor() //子类新扩展成员函数
{
System.out.println("My color is " + color + ".");
System.out.println("There is only "+ genre + " genre of banana.");
}
public static void main(String args[]) //main()函数中没有实例化对象
{
banana.isColor(); //直接使用类名调用子类 static 函数 isColor();
}
}
3.2 fruit.java 使用默认构造函数,利用super()实现带参数的构造函数,赋值构造函数!
package fruit;
public class fruit { //超类fruit类,包含一个default型成员变量name
String name;
fruit(){ name = "Fruit";} //默认构造函数
fruit(String nam){name=nam;} //带参数构造函数
fruit(fruit FU){ this.name = FU.name;} //复制构造函数
protected void isName() //成员函数
{
System.out.println("This is a/an " + name);
}
public void showInfo()
{
System.out.println("Info\nname: "+this.name);
}
}
class banana extends fruit{ //子类banana
banana(){ //子类默认构造函数,自动调用超类fruit的没有参数的构造函数。
color = "Yellow";
genre = 1;
}
banana(String nam, String color, int sum) //子类带参数的构造函数
{
super(nam);
this.color = color;
genre = sum;
}
banana(banana ba) //子类复制构造函数
super(ba);
this.color = ba.color;
this.genre = ba.genre;
}
String color; //扩展成员变量color
private int genre; //扩展成员变量genre
public void rename(String nam)
{
super.name = nam;
}
/*public void isColor() //子类新扩展成员函数
{
System.out.println("My color is " + color + ".");
System.out.println("There is/are only "+ genre + " genre of banana.");
}*/
public void showInfo() {
super.showInfo();
System.out.println("color: " + color);
System.out.println("Genre: " + genre);
System.out.println("That's it.");
}
public static void main(String args[])
{
banana bana = new banana();
banana bana_one = new banana("Argentina Banana","Light Yellow",2);
banana bana_two = new banana(bana_one);
bana.showInfo();
bana_one.showInfo();
}
}
运行结果:
需要注意的是:创建子类对象时,无论是默认的构造函数或带参数的构造函数,还是复制构造函数都是先调用超类的对应的构造函数,并且如果使用super()自动调用超类构造函数的话,必须是super()位于子类相应构造函数的第一行。super除了这一功能以外,还有一个功能就是在子类中rename()函数调用了super.name,以及子类showInfo()函数对超类的super.showInfo()进行了改写,这两个地方主要是用到了super.超类成员变量 or super.超类成员函数实现对超类可见成员的快速访问。
Part four: 超类引用能引用子类对象
对此我们将改进的 fruit.java 的 main()做了一些修改。
public static void main(String args[])
{
banana bana = new banana();
banana bana_one = new banana("Argentina Banana","Light Yellow",2);
banana bana_two = new banana(bana_one);
fruit fu; //创建一个超类对象
fu = bana_two; //将子类对象赋给超类对象
fu.showInfo(); //fu调用的是子类的showInfo()还是超类的showInfo()呢?
}
程序结果:
这一步通过超类引用子类对象的过程也称向上转型!!!
Part five: 继承中的重写机制--抽象类
如何理解抽象类:
1.抽象类需要使用关键字abstract声明
2.抽象类一定要被继承,所以不能使用private修饰成员。只能使用public&&protected
3.抽象类也可允许普通成员函数,只是比普通类多加入了抽象特性
4.抽象类不能实例化,必须先通过子类覆写抽象成员函数,向上转型
5.抽象类也能使用static 关键字
6.抽象类不能用final关键字修饰,因为抽象类必须要有子类,不热就毫无存在意义
7.抽象类中构造函数调用同普通类继承一样,先超类,后子类
下面创建一个fruit 抽象类:
package javaFileStreamPractice;
abstract class fruit { //超类fruit类,包含一个default型成员变量name
String name;
fruit(){ name = "Fruit";} //默认构造函数
fruit(String nam){name=nam;System.out.println("Fruit's constructor has been called.");} //带参数构造函数
fruit(fruit FU){ this.name = FU.name;} //复制构造函数
protected void isName() //成员函数
{
System.out.println("This is a/an " + name);
}
public abstract void rename(String nam);
public abstract void showInfo();
}
class banana extends fruit{ //子类banana
banana(){ //子类默认构造函数,自动调用超类fruit的没有参数的构造函数。
color = "Yellow";
genre = 1;
}
banana(String nam, String color, int sum) //子类带参数的构造函数
{
super(nam);
this.color = color;
genre = sum;
System.out.println("Banana's constructor has been called.");
}
banana(banana ba) { //子类复制构造函数
super(ba);
this.color = ba.color;
this.genre = ba.genre;
}
String color; //扩展成员变量color
private int genre; //扩展成员变量genre
public void rename(String nam) //override
{
super.name = nam;
}
public void showInfo() {
//super.showInfo();
System.out.println("Info\nThis is "+name);
System.out.println("color: " + color);
System.out.println("Genre: " + genre);
System.out.println("That's it.");
}
public static void main(String args[])
{
fruit fu = new banana("Argentina Banana","Light Yellow", 25); //创建子类对象,向上转型
fu.showInfo(); //调用重写的抽象函数showInfo()
fu.rename("Chinese Sweat Banana"); //调用重写的抽象函数rename()
fu.showInfo();
}
}
Part six: final实现封装特性
final是Java中保留的关键字,意为“最后的,最终的”,从字面上理解就是,被final修饰的变量是最终的,被final修饰的成员函数是最终的,被final修饰的类也是最终的。简单解释一下这三个最终如何帮助类实现封装特性。
1、final修饰类中变量
无论属性是基本类型还是引用类型,final所起的作用都是变量里面存放的“值”不能变。
这个值,对于基本类型来说,变量里面放的就是实实在在的值,如1,“abc”等。而引用类型变量里面放的是个地址,所以用final修饰引用类型变量指的是它里面的地址不能变,并不是说这个地址所指向的对象或数组的内容不可以变,这个一定要注意。
final修饰属性,声明变量时可以不赋值,而且一旦赋值就不能被修改了。对final属性可以在三个地方赋值:声明时、初始化块中、构造方法中。总之一定要赋值。
2、final修饰类中的方法
作用:可访问,但不可重写!!!实现私密方法的封装!!!
3、final修饰类
作用:类不可以被继承。实现类的封装特性!!!