继承过程中重载VS重写---(重写规则)

本文深入探讨Java中的多态性,特别是方法重写与重载的区别,并通过实例讲解了成员变量继承、this与super的用法,以及静态方法的重写与构造方法中的方法调用等关键知识点。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

PS : java中实现多态的机制是什么? 
     

        方法的重写Overriding和重载Overloading是Java多态性的不同表现。


           重写Overriding是父类与子类之间多态性的一种表现,发生在运行时

                       (也就是动态绑定),根据对象类型来决定调用的具体方法。

           重载Overloading是一个类中多态性的一种表现,发生在编译时。

            重载方法在编译时起作用(例如,静态绑定),重写方法在运行时起作用(例如,动态绑定)。静态绑定意味着JVM在编译时决定调用的类或方法。而动态绑定时,JVM是在运行时决定调用的类或方法。动态绑定设计是多态的基础。


一 子类---父类 中成员变量的继承及this--super

                 访问变量看声明,访问方法看实际对象类型(new出来的类型)


子类从父类继承分成两种

1.方法的继承:

     方法的继承,父类直接把自己的方法转移到子类中去,当然前提是父类的方法修饰符的访问范围是子类可以访问的范围, 但是如果子类已经重写了父类的方法,这时候这个方法就不能父类从转移到子类中

2.成员变量的继承 :

         成员变量就会有点奇妙了,它不会从父类转移到子类,而是保留在父类中,这就会出现,子类和父类可能同时拥有两个相同名字的变量。

下面用一段代码来说明这种相对比较复杂的关系:

package com.text;   
public class Test { 
    public static void main(String[] args) { 
        Father a = new Father(); 
        Chilren b = new Chilren(); 
        Father c = new Chilren(); 
        a.getAge();   40
        System.out.println(a.age);  40 
        b.getAge();   18
        System.out.println(b.age);  18 
        c.getAge();    18
        System.out.println(c.age);  40 
    } 

  
class Father { 
    int age = 40; 
  
    public void getAge() { 
        System.out.println(age); 
    } 

  
class Chilren extends Father { 
    int age = 18; 
  
    public void getAge() { 
        System.out.println(age); 
    } 

输出 
40
40
18
18
18
40 

前四个结果可以理解,但是最后两个结果有人可能会问为什么结果会不一样,这段代码就体现了,成员变量继承和方法继承的区别。
可以得出结论: 
  
 Father c = new Chilren();

   
     在以上父类引用指向子类对象情况下,访问变量看的是引用类型,所以c.age是父类的成员变量,而c.getAge()访问到的是子类Chilren的方法,所以在这个方法中用到的age变量是Chilren的变量。


   
  反正一句话,访问变量看声明,访问方法看实际对象类型(new出

来的类型)。静态方法在继承过程中或者说是重写过程中,只能被隐

藏,不能被覆盖,具体调用子类or父类的,取决于对象声明的类型。


3 this  ---VS---super

接下来对代码做部分修改

   public static void main(String[] args) { 
  
        Chilren b = new Chilren(); 
        Father c = b 
        System.out.println(b.age);  18
        System.out.println(c.age);   40
    } 
   
  
输出 
18
40 
     b 和c 两个引用都是指向内存中同一个对象,但是打印出来的结果却是不同,这就说明了,内存中保存了两个 age的值,一个是18 一个是40 。
     这里就会产生一些疑问,在内存中他们是怎么存储的?这时候会想到有个super关键字,通过super. 可以访问到父类的变量和方法,这里有人会说:“super.代表的就是一个父类对象,因为他指向父类”  之前我也是这么想,但是看过一些书后知道其实不是这样
     其实super.不是“东西”,说道super.自然会想到this.,有人把他们归为同类,其实他们大大不同

     this:是一个真真实实对象,代表的就是当前对象,可以用 return this;  去返回一个对象。


     super:不能一个对象,不是指向父类对象的意思,super只是修饰了他后边的内容,告诉JVM,后面这部分内容不是当前对象所属类的内容而已,若用return super,JVM是不允许的,是一种错误的语法。


public static void main(String[] args) { 

        Chilren b = new Chilren(); 
        Father c = b 
        System.out.println(b.age); 
        System.out.println(c.age); 
    } 
   
输出   
40
   
回归到上面这段代码,这里并不是说内存中有两个对象 b 和 c ,内存中其实就只有一个 b对象 ,只是c 不仅有自己的实例 变量,同时也存在父类所定义的全部实例变量。
        所以可以得出结论:
                     在实例化一个子类的同时,系统会给子类所有实例变量分配内存,也会给他的父类的实例变量分配内存。即使父子类中存在重名的实例变量,也会两个都分配内存的,这个时候子类只是隐藏了父类的这个变量,但还是会给它分配内存,然后可以用super来访问属于父类的变量。

二  静态方法的重写/覆盖 --(静态方法的隐藏--父类成员变量的隐藏

看看一个程序,动态绑定与静态绑定。

  1. class Dog{
  2. publicstaticvoidbark(){
  3. System.out.print("woof");
  4. }
  5. }
  6. class Basenji extend sDog{
  7. publicstaticvoidbark(){}
  8. }
  9. publicclassBark{
  10. publicstaticvoidmain(Stringargs[]){
  11. Dog   woofer=new Dog();
  12. Dog    nipper=new Basenji();  // 若nipper声明为 Basenji ,则nipper.bark();输出为空
  13. woofer.bark(); // woof
  14. nipper.bark();// 也是woof ,
  15. }
  16. }

啊,你可能一开始就认为这应该是动态绑定吧,多态性!!那么应该只打印一个woof吧!可并非如此,可怜的小狗Basenji也叫唤了(据书中所说,此种狗在非洲,而且从来不叫唤,无法想象这世上有不叫唤的狗,呵呵)问题就在于bark是一个静态方法,而静态方法的调用不存在任何动态绑定机制对于静态方法,只会根据调用者的编译期类型进行调用,woofer,nipper都被声明为Dog,他们的编译期类型相同。你可能要问,子类 Basenji中的bark()方法不是覆写了父类中的bark方法,那么是怎么回事?答案是它隐藏了父类中的bark方法,静态方法是不能被覆写的,他们只能被隐藏要想使这只可怜的小狗回归不叫唤的“正常”状态,去掉方法之前的static标签即可。

注意隐藏的含义。

三 java构造方法与成员初始化分析---正确重写,调用的是子类中重写的函数,作用于初始化之前。

package ibm;

public class TestConstructor{

public static void main(String[] args) {

Drived drived=new Drived();

System.out.println(drived.memeber);

}

}

 

class Base {

public Base(){

System.out.println("base constructor");

preprocess();

}

public void preprocess(){

System.out.println("base invoked");

}

}

 

class Drived extends Base{

public String memeber="set the string";

public Drived(){

System.out.println("drived constructor");

System.out.println(memeber);

memeber="set in constructor";

}

@Override

public void preprocess(){

System.out.println("drived invoked ");

memeber="set in proprocess ";

}

}

 

请问以上程序的打印输出什么?

base constructor

drived invoked 

drived constructor

set the string

set in constructor

 

 

程序的执行顺序是这样的;

1 进入Drived 构造函数。

2 Drived 成员变量的内存被分配。

3 Base 构造函数被隐含调用。

4 Base 构造函数调用preprocess(),这里调用的是Drived的preprocess()(多态性)-----正确重写。

5 Drived 的preprocess设置memeber值为 “set in proprocess”。

6 Drived 的成员变量初始化被调用。

7 执行Drived 构造函数体。

 

Java中的声明和初始化不能看成一体的。在C++的世界中,C++并不支持成员变量在声明的时候进行初始化,其需要你在构造函数中显式的初始化其成员变量的值,看起来很土,但其实C++用心良苦。在面

 

向对象的世界中,因为程序以对象的形式出现,导致了我们对程序执行的顺序雾里看花。所以,在面向对象的世界中,程序执行的顺序相当的重要 。

 

下面是对上面各个步骤的逐条解释。

进入构造函数。

为成员变量分配内存。

除非你显式地调用super(),否则Java 会在子类的构造函数最前面偷偷地插入super() 。

调用父类构造函数。

调用preProcess,因为被子类override,所以调用的是子类的。

于是,初始化发生在了preProcess()之后。这是因为,Java需要保证父类的初始化早于子类的成员初始化,否则,在子类中使用父类的成员变量就会出现问题。

正式执行子类的构造函数(当然这是一个空函数,居然我们没有声明)。

你可以查看《Java语言的规格说明书》中的 相关章节 来了解更多的Java创建对象时的细节。

 

C++的程序员应该都知道,在C++的世界中在“构造函数中调用虚函数”是不行的,Effective C++ 条款9:Never call virtual functions during construction or destruction,Scott Meyers已经解

 

释得很详细了。在语言设计的时候,“在构造函数中调用虚函数”是个两难的问题。

1 如果调用的是父类的函数的话,这个有点违反虚函数的定义。

2 如果调用的是子类的函数的话,这可能产生问题的:因为在构造子类对象的时候,首先调用父类的构造函数,而这时候如果去调用子类的函数,由于子类还没有构造完成,子类的成员尚未初始化,这

 

么做显然是不安全的。

C++选择了第一种,而Java选择了第二种。

C++类的设计相对比较简陋,通过虚函数表来实现,缺少类的元信息。

而Java类的则显得比较完整,有super指针来导航到父类。

 四  重载(overloading)与重写(overriding) ---正确重写的规则

这篇文章介绍的常见面试题是关于重载(overloading)方法和重写(overriding)方法的。

Q.下面代码片段的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class MethodOverrideVsOverload {

 // 是重载,not 重写,重写要符合重写的规则
 
(此处函数的参数变了,故是重载,not 重写)

  public boolean equals( MethodOverrideVsOverload other ) {

       System.out.println( "MethodOverrideVsOverload equals method reached" );
      return true ;
   }
 
  public static void main(String[] args) {
   Object o1 = new MethodOverrideVsOverload();  
//因为是重载,故调用方法是在编译时确定,即取决于所声明的对象类型,
// 此时,声明为object 的类型,故调用的是object的equals方法,
// 比较的是俩个对象的地址是否一样,必然不一样。
   Object o2 = new MethodOverrideVsOverload();
 
// 重载,发生与运行时,即取决与所声明的类型,

// 声明为   MethodOverrideVsOverload ,调用子类的重载方法

   MethodOverrideVsOverload o3 = new MethodOverrideVsOverload();
   MethodOverrideVsOverload o4 = new MethodOverrideVsOverload();
 
   if (o1.equals(o2)){
    System.out.println( "objects o1 and o2 are equal" );
   }
 
   if (o3.equals(o4)){
    System.out.println( "objects o3 and o4 are equal" );
   }
  }
}

A.输出结果是:

MethodOverrideVsOverload equals method reached

objects o3 and o4 are equal

这个问题考察了哪些概念呢?

  • Java语言中,一个类只能从一个类中继承出来(也就是,单继承结构),如果没有显式的标明所继承自的类,那么自动继承自Object对象。
  • 大多数的非final对象类方法都会被子类重写(overridden):

public boolean equals(Object obj); // make note of this method

public int hashCode();

public String toString();

  • 重载方法在编译时起作用(例如,静态绑定),重写方法在运行时起作用(例如,动态绑定)。静态绑定意味着JVM在编译时决定调用的类或方法。而动态绑定时,JVM是在运行时决定调用的类或方法。动态绑定设计是多态的基础。更多了解编译时和运行时.

  • 子类中重写父类的对应方法必须遵循下面的规则:

参数 不可变(译者注:包括参数类型和个数)。
返回类型 不可变,除了协变返回类型或其子类型(covariant (subtype) returns)。
异常 子类中可以抛出更少的异常,但绝对不能抛出父类中没有定义的已检查异常。
访问权限 比父类中对应方法更宽松。
调用 运行时(也就是动态绑定),根据对象类型来决定调用的具体方法。


      现在,再回头看上面的代码,MethodOverrideVsOverload  类中的”equals(MethodOverrideVsOverload other)”方法并没有重写Object类中的”public boolean equals(Object obj)” 方法。这是因为其违背了参数规则,其中一个是MethodOverrideVsOverload 类型,而另一个是Object类型。

     因此,这两个方法是重载关系发生在编译时),而不是重写关系


因此,当调用o1.equals(o2)时,实际上调用了object类中的public boolean equals(Object obj)方法。这是因为在编译时,o1和o2都是Object类型,而Object类的equals( … )方法是比较内存地址(例如,Object@235f56和Object@653af32)的,因此会返回false。

当调用o3.equals(o4)时,实际上调用了MethodOverrideVsOverload 类中的equals( MethodOverrideVsOverload other )方法。这是因为在编译时,o3和o4都是MethodOverrideVsOverload类型的,因此得到上述结果。

接下来还可以怎么提问呢?

Q.那怎么解决上面的那个问题呢?

A.在Java5中,新增了注解,其中包括很好用的编译时注解(compile time annotations)@override,来保证方法正确的重写了父类方法。如果在上面的代码中添加了注解,那么JVM会抛出一个编译错误。

因此,解决的方法就是给MethodOverrideVsOverload  类的boolean equals( MethodOverrideVsOverload other )方法添加@override注解。这样的话编译时就会有错误抛出来提示开发者某个方法没有正确的重写父类方法。之后,还需要修改方法的参数,将其从MethodOverrideVsOverload变成Object,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class MethodOverrideVsOverload {
 
  @Override // 重载
  public boolean equals( Object other ) {
      System.out.println( "MethodOverrideVsOverload equals method reached" );
      return true ;
  }
 
  public static void main(String[] args) {

   Object o1 = new MethodOverrideVsOverload(); //during compile time o1 is of type Object
                                               //during runtime o1 is of type MethodOverrideVsOverload
   Object o2 = new MethodOverrideVsOverload(); //during compile time o2 is of type Object
                                               //during runtime o2 is of type MethodOverrideVsOverload
 
   MethodOverrideVsOverload o3 = new MethodOverrideVsOverload(); //o3 is of type MethodOverrideVsOverload
                                                                 // during both compile time and runtime
   MethodOverrideVsOverload o4 = new MethodOverrideVsOverload(); //o4 is of type MethodOverrideVsOverload
                                                                 // during both compile time and runtime
 
   if (o1.equals(o2)){  
// 重写,发生在运行时,运行时动态绑定,调用子类重写的equals方法
    System.out.println( "objects o1 and o2 are equal" );
   }
 
   if (o3.equals(o4)){
// 重写,发生在运行时,运行时动态绑定,调用子类重写的equals方法
    System.out.println( "objects o3 and o4 are equal" );
   }
 
  }
 
}

输出为:

MethodOverrideVsOverload equals method reached

objects o1 and o2 are equal

MethodOverrideVsOverload equals method reached

objects o3 and o4 are equal

上面的代码中,运行时equals方法正确的重写了Object中的相应方法。这是一个比较容易混淆的问题,面试的时候需要很详尽的解释相关的概念。




参考 : http://blog.youkuaiyun.com/haoel/article/details/4319793 


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值