类的继承是java面向对象体系的一个重要方面(封装、继承、多态),对于java类的继承,需要注意如下细节。
1.构造函数。
如果一个类没有任何构造函数,系统会默认分配一个无参的构造函数给它,这个构造函数什么都不做。但是一旦类中有定义其他有参数的构造函数,且没有显式的的定义无参构造函数,那么系统不会为该类提供一个默认的无参构造函数。
而类的继承,子类继承了 父类的所有可继承的成员和方法,那什么是不可继承的呢?答案就是构造函数。构造函数也是一个函数,但是子类却不可以继承。虽然子类无法继承父类构造函数,但是他却必须调用父类的构造函数。通过super(参数列表)来调用父类的构造函数。当子类没有显式调用父类构造函数时,并且没有调用其他构造函数,程序默认为其调用父类无参构造函数。
注意以下几种情况
a.父类和子类都没有显式的定义任何构造函数
class Father{ private String userName; private int age; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } } class son extends Father{ }
如上代码所示,父类与子类都没有显式定义一个构造函数。但是系统默认为二者均提供一个无参构造函数。又因为子类必须调用父类的构造函数,而当程序没有显式的调用父类构造函数的时候,系统默认为其调用父类无参构造函数。所以上述代码效果其实与下面代码是一样的。
class Father{ private String userName; private int age; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //显式无参构造函数 public Father(){ } } class son extends Father{ public son(){ super(); } }
b.父类包含有参构造函数,但无显式定义无参构造函数,子类也并未显式调用父类构造函数,此时将出现错误。
class Father{ private String userName; private int age; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //显式无参构造函数 public Father(String userName,int age){ this.userName=userName; this.age=age; } } class son extends Father{ }
ide此时会在son的定义处给出提示,Implicit super constructor Father() is undefined for default constructor. Must define an explicit constructor。翻译一下就是父类没有显式的定义一个无参构造函数,而我子类也没有显式调用父类构造函数,所以程序默认的去调用一个不存在的父类的无参构造函数。当然出错了。
只要显式的调用父类的一个构造函数,既可以解除错误。为son加入下面构造函数。
public son(String userName,int age){ super(userName,age); }
或者更简单点,给父类显式添加一个无参构造函数,这样子类即便不显式调用父类构造函数,也可以避免上述错误。事实上,一般情况下,当我们定义了有参构造函数,最好是显式定义一个无参构造函数,以避免此类错误的出现。
2.子类实例化的过程
子类的实例化过程是较为复杂的一个过程,必须弄清楚,因为很多看起来很含糊的问题都是因为没有弄清楚这个问题而导致的。
例如son是father的子类,father是grandfather的子类,则son的实例化将经过如下过程。
首先,我们整理出一个流程。简称大循环.
第一步:分配成员变量的存储空间,并且按java的规则进行默认初始化(如int型的值为0,引用型为null)。此时其实该子类(son)已经在内存中占有了一席之地。
第二步:给构造函数的参数赋值,比如说我们执行的是son s=new ("tom",18),则将“tom”和18分别赋值给构造函数的参数userName和age,注意这一步还没有执行到构造函数里面的具体代码。
第三步:调用父类构造函数。
第四步:执行son 类中成员变量的显式初始化,就是我们在定义成员变量的时候,有时候会显式初始化。比如private String userName="xdx"。这就是显式的初始化。
第五步:执行子类构造函数的代码。
上述的流程,是没有考虑父类的情况,但是通过第1点关于构造函数的讨论,我们知道,在执行子类构造函数的时候,必须先显式或隐式的调用父类构造函数。我们将这个过程在细做分解,则形成一个小循环。将这个小循环插入(替代)上面大循环的第三步。
第(1)步:father类执行类似大循环中第二步,即给father类构造函数中的形式参数赋值。
第(2)步:调用father类的父类的构造函数
第(3)步:执行father类中的成员变量的显示初始化。
第(4)步:执行father类的构造函数
由于父类又可能有父类,所以在小循环中的第二步起始处继续插入一个小循环,一直到追溯到object类为止,因为object类就是所有类的父类。
以上便是子类实例化的详细过程。有几个地方需要注意。
a.整个过程中在内存中只实例化出了一个对象,便是子类这个对象,父类并没有被实例化。
b.注意执行成员变量显式初始化这一步,它针对的是当前类,并且是显式的。也就是说当前处于子类的循环中,执行的是子类的成员变量显式初始化,如果是处于父类的循环中,执行的是父类的成员变量的显式初始化。虽然子类继承了父类的成员变量,但这个初始化过程并不会重复。
举个例子。
class Father{ private String userName="tom"; private int age; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public Father(String userName,int age){ this.userName=userName; this.age=age; } } class son extends Father{ private String university="xmu"; public String getUniversity() { return university; } public void setUniversity(String university) { this.university = university; } public son(String userName,int age,String university){ super(userName,age); this.university=university; } }
上述father类有两个成员变量,userName和age,son继承了他们并且自己有一个新的成员university。
执行如下代码。
public class ExtendDemo { public static void main(String[] args) { son s=new son("xdx",18,"Peking University"); System.out.println(s.getUserName()); System.out.println(s.getUniversity()); }
上述代码的son类实例化过程为(假设father类就是最顶级的类了,不考虑object类):
(1).分配存储空间,并默认初始化,即userName=null;age=0;university=null;(此时成员变量userName=null,age=0;university=null)
(2).给构造函数的参数赋值,userName(参)="xdx",age(参)=18,university(参)="Peking University"。标注参是为了与成员变量混淆。(此时成员变量userName=null,age=0;university=null)
(3)调用父类构造函数public Father(String userName,int age),这一步分为:a.先给构造函数的参数赋值,userName="xdx",age=18,(此时成员变量userName=null,age=0;university=null);b.显式执行成员变量初始化,即userName="tom"。(此时成员变量userName=“tom”age=0;university=null)c.然后执行构造函数的代码:即this.userName=userName; this.age=age;这两句代码。(此时成员变量userName="xdx",age=18;university=null)
(4)给son类的成员进行显式初始化,即 private String university="xmu";(此时成员变量userName="xdx",age=18;university="xmu").
(5)执行son类的构造函数,即 this.university=university;这句,(此时成员变量userName="xdx",age=18;university="Peking University")
执行结果是:
xdx
Peking University
我们再改变程序,验证一下结果。
public class ExtendDemo { public static void main(String[] args) { son s=new son("xdx",18,"Peking University"); System.out.println(s.getUserName()); System.out.println(s.getAge()); System.out.println(s.getUniversity()); } } class Father{ private String userName="tom"; private int age=10; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } //显式无参构造函数 public Father(){ } public Father(String userName,int age){ this.userName=userName; this.age=age; } } class son extends Father{ private String university="xmu"; public String getUniversity() { return university; } public void setUniversity(String university) { this.university = university; } public son(String userName,int age,String university){ this.university=university; } }
执行结果
tom
10
Peking University
具体的原因就不分析了,只要按照上述的步骤来分析,很容易就能得到正确的结果。
3.方法的重写,同名成员变量的隐藏。
子类继承父类的方法,方法名、参数、返回值都必须是一样的,需要注意的是,如果你重写了方法(就是你显式地再定义了一个跟父类一样的方法),在该重写的方法中还要调用父类方法中的代码逻辑,必须要用super关键字去调用,程序并不会隐式地去调用。
方法的重写比较好理解,说简单点就是覆盖了,关键是成员变量,有这样一种情况,假如我在子类中定义了一个跟父类同样名称的成员变量。这是什么情况呢?
注意,在java中是没有所谓的覆盖父类成员变量的说法的。一旦在子类中定义了一个与父类同名的成员变量,父类的成员变量将隐藏。所谓的隐藏就是对子类不可见,子类也不会操纵父类的这个成员变量了。子类只能操纵(访问或者改变)自己这个成员变量,如若他要访问或者父类的成员变量,则必须使用super关键字。
看如下例子。
public class ExtendDemo { public static void main(String[] args) { son s=new son(); System.out.println(s.userName); System.out.println(s.getUserName()); } } class Father{ private String userName="fatherName"; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } } class son extends Father{ public String userName="sonName"; }
执行结果是:
sonName
fatherName
由上述执行结果我们可以了解到,s.userName获取到的是子类的userName,而getUserName因为是继承自父类的方法,其得到的是父类的userName。父类的userName并没有消失(被覆盖),它只是隐藏了。
这样理解还是有点不清晰,可以换种方法说,当子类没有定义与父类同名的成员变量时,子类是拥有这个成员变量的,当子类定义了一个与父类同名的成员变量,先前这个变量被隐藏了。你不能直接再使用son.userName这种方式去获取它,必须通过父类的方法(super)去获取它。
public class ExtendDemo { public static void main(String[] args) { son s=new son(); System.out.println(s.userName); System.out.println(s.getUserName()); System.out.println(s.getFatherName()); } } class Father{ private String userName="fatherName"; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } } class son extends Father{ public String userName="sonName"; public String getFatherName(){ return super.getUserName(); } }
执行结果:
sonName
fatherName
fatherName
在getFatherName中,我们使用了super.getUserName。其实他跟s.getUserName()的效果是一模一样的,都是通过父类来获取userName这个被隐藏了属性。
那假如我重写了getUserName这个方法呢?看如下代码。
public class ExtendDemo { public static void main(String[] args) { son s=new son(); System.out.println(s.userName); System.out.println(s.getUserName()); System.out.println(s.getFatherName()); } } class Father{ private String userName="fatherName"; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } } class son extends Father{ public String userName="sonName"; public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getFatherName(){ return super.getUserName(); } }
执行结果是:
sonName
sonName
fatherName
由于我现在重写了getUserName方法,此时它所操纵的userName自然是子类的userName了。
所以一般情况下,当我们再父类中已经定义了一个成员变量,在子类中不会去定义一个相同名字的成员变量的。因为当你想刻意的访问子类这个成员变量,但是却没有重写父类的get方法时,是访问不到这个成员变量的。而是会访问到父类的那个成员变量。而有时候你还浑然不觉。