java方法重载/重写

在JAVA中,多态主要体现为方法的重载和重写。

方法重载:同一个类中,方法名相同,参数列表不同(在编译器那层基本不会考虑返回参数)的两个或多个方法就视为这个几个方法是重载的。
Java的“重载机制”为开发者提供了便利的同时也规定了一系列的规范和限制。

方法重写:对应于父类和子类,方法名和方法描述符相同(参数列表和返回类型组成),就会产生重写,即子类的方法会覆盖掉父类的方法(
对了,静态方法的话效果也差不多哦?,父类的静态方法会被子类的静态方法隐藏掉,也和覆盖差不多)

在java中,是不允许两个参数列表相同且方法名相同的方法出现的,即使他们的返回类型不同,这是在编译期就已经会检测的。(之所以强调
这个是因为,事实上如果你绕开编译器,你生成了两个方法,方法名相同,方法参数列表相同,但是方法返回不同,你通过字节码去调用,
同样是可以做到重载的,因为在字节码的层次对于方法的签名是包含了对返回类型的区分的)。

一、什么是方法重载:重载的概念
在搞明白“重载”之前要先弄清楚另一个概念“方法签名”
方法签名 = 方法名 + 参数列表 (方法签名不包括返回值)
我们用方法签名区分不同的方法,而所谓“重载”,指的就是“两个方法间的方法签名中方法名相同而参数列表不同的情况”,比如:

void sum(int a, int b){
    System.out.println("a + b = "+(a+b));
}
void sum(double a, double b){
    System.out.println("a + b = "+(a+b));
}

调用时如果传入的参数是sum(1, 2),就调用上面的方法;如果传入的参数是sum(1.1, 2.2),就调用下面的方法;如果传入的参数是sum(1, 3.14),还是调用下面的方法,因为要有一个信念就是编译器总是有能力找出“最适合的对应调用方法”,这里“最适合”指的是“最匹配”、“精确度最高”,值得注意的是第三个调用的第一个参数在编译时先要被转换为1.0,然后才能调用下面的方法(但是如果有方法三sum(int a, double b)那么就调用方法三)

方法重载,对于像Java、C、C++这样的静态语言来说其作用就是“最大限度的增强代码复用性,减少程序员的体力劳动”(为什么我不说Python这样的动态类型语言?因为人家的类型是不确定的呀!好吧扯远了……)

任何方法都可以“重载”的,包括“构造函数(构造方法)”

继承来的方法也可以重载吗:当然可以
对照着我们的定义再看一遍,“方法名相同而参数列表不同……”,再把继承即概念抠出来,“……子类会继承父类的的所有方法(父类对象作为子类对象的内核)”——这也就意味着,我们的子类方法可以重载继承得来的方法

这里最容易混淆的两个概念就是“重载继承来的方法”和“覆写继承来的方法”

public class Father{
    ...
    public void func(int a, int b){
        System.out.println("a + b = "+(a+b));
    }
    ...
}
public class Son extends Father{
    ...
    public void func(double a, double b){ //这叫“重载”
        System.out.println("a + b = "+(a+b));
    } //“重载”并不关心方法体或返回值是否相同
    publcic void func(int a, int b){ //这叫“覆写”
        System.out.println("a - b "+(a-b));
    } //“覆写”只能改变方法的主体部分,而返回值和方法签名必须和父类一致
    ...

窃以为这一个例子就够了,保证以后不会出错就行

当重载遇到范型
背景知识:“范型”是JDK1.5引入的一个很棒的特性
在有范型之前是这样的:

ArrayList al = new ArrayList();
al.add("Hello");
al.add(1.5);
String element1 = (String)al.get(0); /*得到的是Object类型
int element2=(Integer)al.get(1);     必须强制类型转换*/

自从有了范型之后:

ArrayList<String> al = new ArrayList<>();
al.add("Hello");
al.add("1.5"); //1.5的话编译器报错!
String element1 = al.get(0);
String element2 = al.get(1);

范型的引入,实际上是在编译期增加了一次额外的类型检查以防止体制外的类型的介入,保证了类型安全

关于范型只要注意一下几点就可以无敌了:
1. 范型分为范型类和范型方法,确定范型种类的是所谓的“类型参数”
2. 范型的“类型参数”中只可以传递指向实际对象的变量,不允许填入字面量
3. 范型的工作原理是“类型擦除”后再“类型填充”
4. 范型的存活只限于“编译期”
5. Java不支持“范型数组”
其中第三、四点,具体来说:“Java语言的泛型基本上完全是在编译器中实现的,由编译器执行类型检测和类型推断,然后生成普通的非泛型的字节码;JVM是完全意识不到范型的存在的,这称为类型擦除;编译器使用泛型类型信息保证类型安全,然后在生成字节码之前将其清除”

好啦!背景知识介绍完了,下面进入正题——当重载遇到范型,会碰撞出怎样的火花?话不多少,直接上代码:

public static<N extends Number> double sum(N a, N b){
    double sum = a.doubleValue() + b.doubleValue();
    return sum;
}

范型原来还可以这么玩——只用一个方法就表示若干个本来需要重载的方法

然,正如古训说的:“世间万物都有两面性”——范型也不是个例外:

public void func(List<String> ls){
    ...
}
public void func(List<Integer> ls){
    ...
}
这会让编译器报错:
Method test(List<String>) has the same erasure test(List<E>) as another method in type TR

这时候除非改变方法的返回值:

public String func(List<String> ls){
    ...
}
public int func(List<Integer> ls){
    ...
}

这样就可以通过编译了(但是这不可是一般意义上的“重载”啊)

为什么“范型参数”不可以作为重载的判据?为什么返回值在有范型的方法中却可以像“方法签名”一样用来区分两个不同的方法?这涉及到简单的编译原理——
Java函数的方法签名包括方法名称、参数类型以及参数顺序;但在字节码中,特征签名还包括了方法的返回值以及受查异常表,这就是为什么在class文件中,其他都相同仅仅返回值不同的两个方法能共存的原因(不懂请回头看那个小结范型的第4条机下面的注解)

结论:1. 使用范型可以代替多个方法的重载;2. 重载同样不关心范型的类型参数,因为在编译期被类型擦出根本看不出来;3. 有范型参与的方法可以用返回值来区分,类似于方法签名
(请将这三点作为补充,加到之前关于范型小结的那部分)

 

二、方法重写

以例题为例:(单选题) 下面代码的输出是什么?

public class Base {
    
    private String baseName= "base";
    public Base(){
        callName();
    }
    
    public void callName(){
        System.out.println(baseName);
    }
    
    static class Sub extends Base{
        private String baseName = "sub";
        public void callName(){
            System.out.println(baseName);
        }
    }
    
    public static void main(String[] args){
        Base b = new Sub();
    }
}


A.null

B.sub

C.base

答案:A

为了能更好的分析代码运行过程, 做原有代码做一些变动如下:

public class Base {
    private String baseName= "base";
    
    public Base(){
        System.out.println("Constructor Base : " + baseName);
        
        System.out.println("before Base callName() -----" );
        
        callName();
        
        System.out.println("after Base callName() -----" );
    }
    
    public void callName(){
        System.out.println("& " + baseName);
    }
    
    static class Sub extends Base{
        private String baseName = "sub";
        
        public Sub(){
            System.out.println("Constructor Sub : " + baseName);
        }
        
        @Override
        public void callName(){
            System.out.println("# " + baseName);
        }
    }
    
    public static void main(String[] args){
        new Sub();
    }
}

此时 main 方法内 new Sub();
输出结果:

Constructor Base : base
before Base callName() -----
# null
after Base callName() -----
Constructor Sub : sub


再将main方法做如下变动:

    public static void main(String[] args){
        
        Base b = new Sub();
        
        b.callName();
    }


输出结果:

Constructor Base : base
before Base callName() -----
# null
after Base callName() -----
Constructor Sub : sub
# sub 


综上所述, 此时运行的是子类Sub的callName() 方法, 

new Sub();在创造派生类的过程中首先创建基类对象,然后才能创建派生类。
创建基类即默认调用Base()方法,在方法中调用callName()方法,由于派生类中存在此方法,则被调用的callName()方法是派生类中的方法,此时派生类还未构造,所以变量baseName的值为null


先成员变量再构造方法,先父类再子类
多态表现:有同名方法执行子类的

执行 Base b = new Sub();时,由于多态 b编译时表现为Base类特性,运行时表现为Sub类特性,
Base b = new Sub();不管是哪种状态都会调用Base构造器执行 callName()方法;
执行方法时,由于多态表现为子类特性,所以会先在子类是否有 callName();
而此时子类尚未初始化(执行完父类构造器后才会开始执行子类),如果有 就 执行(此时, 因为还没有调用子类构造函数, 所以子类的 baseName 输出为 null),没有再去父类寻找。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值