Java知识点2

Java继承中方法的覆盖(重写)和重载

在类继承中,子类可以修改从父类继承来的方法,也就是说子类能创建一个与父类方法有不同功能的方法,但具有相同的名称、返回值类型、参数列表。如果在新类中定义一个方法,其名称、返回值类型和参数列表正好与父类中的相同,那么,新方法被称做覆盖旧(重写)方法。被覆盖的方法在子类中只能通过super调用。注意:覆盖不会删除父类中的方法,而是对子类的实例隐藏,暂时不使用。

public class Demo{
    public static void main(String[] args) {
        Dog myDog = new Dog("花花");
        myDog.say();  // 子类的实例调用子类中的方法
      
        Animal myAnmial = new Animal("贝贝");
        myAnmial.say();  // 父类的实例调用父类中的方法
    }
}
class Animal{
    String name;
    public Animal(String name){
        this.name = name;
    }
  
    public void say(){
        System.out.println("我是一只小动物,我的名字叫" + name + ",我会发出叫声");
    }
}
class Dog extends Animal{
    // 构造方法不能被继承,通过super()调用
    public Dog(String name){
        super(name);
    }

    // 覆盖say() 方法
    public void say(){
        System.out.println("我是一只小狗,我的名字叫" + name + ",我会发出汪汪的叫声");
    }
}

方法覆盖的原则:

  • 覆盖方法的返回类型、方法名称、参数列表必须与原方法的相同。
  • 覆盖方法不能比原方法访问性差(即访问权限不允许缩小)。
  • 覆盖方法不能比原方法抛出更多的异常。
  • 被覆盖的方法不能是final类型,因为final修饰的方法是无法覆盖的。
  • 被覆盖的方法不能为private,否则在其子类中只是新定义了一个方法,并没有对其进行覆盖。
  • 被覆盖的方法不能为static。如果父类中的方法为静态的,而子类中的方法不是静态的,但是两个方法除了这一点外其他都满足覆盖条件,那么会发生编译错误;反之亦然。即使父类和子类中的方法都是静态的,并且满足覆盖条件,但是仍然不会发生覆盖,因为静态方法是在编译的时候把静态方法和类的引用类型进行匹配。

方法的重载:
前面已经对Java方法重载进行了说明,这里再强调一下,Java父类和子类中的方法都会参与重载,例如,父类中有一个方法是 func(){ ... },子类中有一个方法是 func(int i){ ... },就构成了方法的重载。

覆盖和重载的不同:

  • 方法覆盖要求参数列表必须一致,而方法重载要求参数列表必须不一致。
  • 方法覆盖要求返回类型必须一致,方法重载对此没有要求。
  • 方法覆盖只能用于子类覆盖父类的方法,方法重载用于同一个类中的所有方法(包括从父类中继承而来的方法)。
  • 方法覆盖对方法的访问权限和抛出的异常有特殊的要求,而方法重载在这方面没有任何限制。
  • 父类的一个方法只能被子类覆盖一次,而一个方法可以在所有的类中可以被重载多次。

java多态和动态绑定

在Java中,父类的变量可以引用父类的实例,也可以引用子类的实例。

public class Demo {
    public static void main(String[] args){
        Animal obj = new Animal();
        obj.cry();

        obj = new Cat();
        obj.cry();

        obj = new Dog();
        obj.cry();
    }
}

class Animal{
    // 动物的叫声
    public void cry(){
        System.out.println("不知道怎么叫");
    }
   
}

class Cat extends Animal{
    // 猫的叫声
    public void cry(){
        System.out.println("喵喵~");
    }
}

class Dog extends Animal{
    // 狗的叫声
    public void cry(){
        System.out.println("汪汪~");
    }
}

上面的代码,定义了三个类,分别是 Animal、Cat 和 Dog,Cat 和 Dog 类都继承自 Animal 类。obj 变量的类型为 Animal,它既可以指向 Animal 类的实例,也可以指向 Cat 和 Dog 类的实例,这是正确的。也就是说,父类的变量可以引用父类的实例,也可以引用子类的实例。注意反过来是错误的,因为所有的猫都是动物,但不是所有的动物都是猫。(子类的变量不可以引用父类的实例)

父类引用指向子类对象多态

要理解多态性,首先要知道什么是“向上转型”。          在面向对象语言中,接口的多种不同的实现方式即为多态

我定义了一个子类Cat,它继承了Animal类,那么后者就是前者是父类。我可以通过   Cat c = new Cat(); 实例化一个Cat的对象,这个不难理解。
但当我这样定义时:   Animal a = new Cat();表示定义了一个Animal类型的引用,指向新建的Cat类型的对象。由于Cat是继承自它的父类Animal,所以Animal类型的引用是可以指向Cat类型的对象的。


那么这样做有什么意义呢?因为子类是对父类的一个改进和扩充,所以一般子类在功能上较父类更强大,属性较父类更独特,   定义一个父类类型的引用指向一个子类的对象既可以使用子类强大的功能,又可以抽取父类的共性。

所以,父类类型的引用可以调用父类中定义的所有属性和方法,而对于子类中定义而父类中没有的方法,它是无可奈何的;


  同时,父类中的一个方法只有在父类中定义而在子类中没有重写的情况下,才可以被父类类型的引用调用;   对于父类中定义的方法,如果子类中重写了该方法,那么父类类型的引用将会调用子类中的这个方法,这就是动态连接。也可以叫做动态绑定。

动态绑定是指”在执行期间(而非编译期间)“判断所引用对象的实际类型,根据实际的类型调用其相应的方法。
看下面这段程序:

class Father {

public void func1()

{

func2();

}

// 这是父类中的func2()方法,因为下面的子类中重写了该方法 ,所以在父类类型的引用中调用时,这个方法将不再有效,取而代之的是将调用子类中重写的func2()方法

public void func2() {

System.out.println("AAA");

}

}

class Child extends Father { // func1(int i)是对func1()方法的一个重载

由于在父类中没有定义这个方法,所以它不能被父类类型的引用调用

所以在下面的main方法中child.func1(68)是不对的

public void func1(int i) {

System.out.println("BBB");

} // func2()重写了父类Father中的func2()方法   如果父类类型的引用中调用了func2()方法,那么必然是子类中重写的这个方法

public void func2() {

System.out.println("CCC");

}

}

public class PolymorphismTest {

public static void main(String[] args) {

Father child = new Child();

child.func1();// 打印结果将会是什么? } }

上面的程序是个很典型的多态的例子。子类Child继承了父类Father,并重载了父类的func1()方法,重写了父类的func2()方法。重载后的 func1(int i)和func1()不再是同一个方法,由于父类中没有func1(int i),那么,父类类型的引用child就不能调用func1(int  i)方法。而子类重写了func2()方法,那么父类类型的引用child在调用该方法时将会调用子类中重写的func2()

}

}

那么该程序将会打印出什么样的结果呢?       很显然,应该是“CCC”。

对于多态,可以总结它为:

一、使用父类类型的引用指向子类的对象;

二、该引用只能调用父类中定义的方法和变量;

三、如果子类中重写了父类中的一个方法,那么在调用这个方法的时候,将会调用子类中的这个方法;(动态连接、动态调用)

四、变量不能被重写(覆盖),”重写“的概念只针对方法,如果在子类中”重写“了父类中的变量,那么在编译时会报错。

多态的3个必要条件:

1.继承   2.重写   3.父类引用指向子类对象。

首先检查父类中是否有该方法,如果没有,则编译错误;如果有,则检查子类是否覆盖了该方法。如果子类覆盖了该方法,就调用子类的方法,否则调用父类方法。

多态的一个好处是:当子类比较多时,也不需要定义多个变量,可以只定义一个父类类型的变量来引用不同子类的实例。请再看下面的一个例子:

public class Demo {
    public static void main(String[] args){
        // 借助多态,主人可以给很多动物喂食
        Master ma = new Master();
        ma.feed(new Animal(), new Food());
        ma.feed(new Cat(), new Fish());
        ma.feed(new Dog(), new Bone());
    }
}

// Animal类及其子类
class Animal{
    public void eat(Food f){
        System.out.println("我是一个小动物,正在吃" + f.getFood());
    }
}

class Cat extends Animal{
    public void eat(Food f){
        System.out.println("我是一只小猫咪,正在吃" + f.getFood());
    }
}

class Dog extends Animal{
    public void eat(Food f){
        System.out.println("我是一只狗狗,正在吃" + f.getFood());
    }
}

// Food及其子类
class Food{
    public String getFood(){
        return "事物";
    }
}

class Fish extends Food{
    public String getFood(){
        return "鱼";
    }
}

class Bone extends Food{
    public String getFood(){
        return "骨头";
    }
}

// Master类
class Master{
    public void feed(Animal an, Food f){
        an.eat(f);
    }
}

运行结果:
我是一个小动物,正在吃事物
我是一只小猫咪,正在吃鱼
我是一只狗狗,正在吃骨头

Master 类的 feed 方法有两个参数,分别是 Animal 类型和 Food 类型,因为是父类,所以可以将子类的实例传递给它,这样 Master 类就不需要多个方法来给不同的动物喂食。

最直观的多态概念:Java多态是针对方法而言的,指子类对父类的方法进行多用性扩展。
1. 情况一:在执行多态方法的时候,JVM (Java虚拟机 )先会去找父类有没有与子类同名的方法,如果有直接调用子类的同名方法并执行它。
2. 情况二:当然,如果没有,则编译器报错。

在执行new子类对象的时候,多态执行步骤是这样的:
1. 先new父类的对象
2. 再new子类的对象
3. 然后,去查询父类中的方法。
4. 再去,到子类中匹配,最后执行。

————————————————Java instanceof 运算符——————————————————————————

instanceof 的作用是测试它左边的对象是否是它右边的类的实例(或者是否是右边类的子类的实例),

—————————————————Java多态对象的类型转换——————————————————————————

这里所说的对象类型转换,是指存在继承关系的对象,不是任意类型的对象。当对不存在继承关系的对象进行强制类型转换时,java 运行时将抛出 java.lang.ClassCastException 异常。

在继承链中,我们将子类向父类转换称为“向上转型”,将父类向子类转换称为“向下转型”。

很多时候,我们会将变量定义为父类的类型,却引用子类的对象,这个过程就是向上转型。程序运行时通过动态绑定来实现对子类方法的调用,也就是多态性。

然而有些时候为了完成某些父类没有的功能,我们需要将向上转型后的子类对象再转成子类,调用子类的方法,这就是向下转型。

注意:不能直接将父类的对象强制转换为子类类型,只能将向上转型后的子类对象再次转换为子类类型。也就是说,子类对象必须向上转型后,才能再向下转型。请看下面的代码:

public class Demo {
    public static void main(String args[]) {
        SuperClass superObj = new SuperClass();
        SonClass sonObj = new SonClass();

        // 下面的代码运行时会抛出异常,不能将父类对象直接转换为子类类型
        // SonClass sonObj2 = (SonClass)superObj;

        // 先向上转型,再向下转型
        superObj = sonObj;
        SonClass sonObj1 = (SonClass)superObj;
    }
}

class SuperClass{ }
class SonClass extends SuperClass{ } 

将第7行的注释去掉,运行时会抛出异常,但是编译可以通过。
因为向下转型存在风险,所以在接收到父类的一个引用时,请务必使用 instanceof 运算符来判断该对象是否是你所要的子类,请看下面的代码

public class Demo {
    public static void main(String args[]) {
        SuperClass superObj = new SuperClass();
        SonClass sonObj = new SonClass();

        // superObj 不是 SonClass 类的实例
        if(superObj instanceof SonClass){
            SonClass sonObj1 = (SonClass)superObj;
        }else{
            System.out.println("①不能转换");
        }

        superObj = sonObj;
        // superObj 是 SonClass 类的实例
        if(superObj instanceof SonClass){
            SonClass sonObj2 = (SonClass)superObj;
        }else{
            System.out.println("②不能转换");
        }
    }
}

class SuperClass{ }
class SonClass extends SuperClass{ }

运行结果:①不能转换

总结:对象的类型转换在程序运行时检查,向上转型会自动进行,向下转型的对象必须是当前引用类型的子类。

小弟想补充一句:
A、无论向上还是向下转换,都必须是基于继承关系之上的两个类或者多个类,让他们的类型是一致的,这样才能够相互进行转换。
B、否则的话,对不起、您什么就都不能转。
C、例子:一个人转能够转换成狗吗?(这显然错误,单身狗除外!哈哈!)
class Dog
{

}
class Person
{

}
* 没有继承关系,无法相互转换。

注:向上转型本质就是:把子类的实例的指针指向父类变量,这时父类变量存储着子类实例的指针。向下转型的本质就是:把存储着子类的实例的指针的父类变量强制转换成该子类类型的变量。所以一定要先向上转型,再向下转型。





动态绑定为了理解多态的本质,下面讲一下Java调用方法的详细流程。

1) 编译器查看对象的声明类型和方法名。

假设调用 obj.func(param),obj 为 Cat 类的对象。需要注意的是,有可能存在多个名字为func但参数签名不一样的方法。例如,可能存在方法 func(int) 和 func(String)。编译器将会一一列举所有 Cat 类中名为func的方法和其父类 Animal 中访问属性为 public 且名为func的方法。

这样,编译器就获得了所有可能被调用的候选方法列表。

2) 接下来,编泽器将检查调用方法时提供的参数签名。

如果在所有名为func的方法中存在一个与提供的参数签名完全匹配的方法,那么就选择这个方法。这个过程被称为重载解析(overloading resolution)。例如,如果调用 func("hello"),编译器会选择 func(String),而不是 func(int)。由于自动类型转换的存在,例如 int 可以转换为 double,如果没有找到与调用方法参数签名相同的方法,就进行类型转换后再继续查找,如果最终没有匹配的类型或者有多个方法与之匹配,那么编译错误。

这样,编译器就获得了需要调用的方法名字和参数签名。

3) 如果方法的修饰符是private、static、final(static和final将在后续讲解),或者是构造方法,那么编译器将可以准确地知道应该调用哪个方法,我们将这种调用方式 称为静态绑定(static binding)

与此对应的是,调用的方法依赖于对象的实际类型, 并在运行时实现动态绑。例如调用 func("hello"),编泽器将采用动态绑定的方式生成一条调用 func(String) 的指令。

4)当程序运行,并且釆用动态绑定调用方法时,JVM一定会调用与 obj 所引用对象的实际类型最合适的那个类的方法。我们已经假设 obj 的实际类型是 Cat,它是 Animal 的子类,如果 Cat 中定义了 func(String),就调用它,否则将在 Animal 类及其父类中寻找。

每次调用方法都要进行搜索,时间开销相当大,因此,JVM预先为每个类创建了一个方法表(method lable),其中列出了所有方法的名称、参数签名和所属的类。这样一来,在真正调用方法的时候,虚拟机仅查找这个表就行了。在上面的例子中,JVM 搜索 Cat 类的方法表,以便寻找与调用 func("hello") 相匹配的方法。这个方法既有可能是 Cat.func(String),也有可能是 Animal.func(String)。注意,如果调用super.func("hello"),编译器将对父类的方法表迸行搜索。

假设 Animal 类包含cry()、getName()、getAge() 三个方法,那么它的方法表如下:
cry() -> Animal.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()

实际上,Animal 也有默认的父类 Object(后续会讲解),会继承 Object 的方法,所以上面列举的方法并不完整。

假设 Cat 类覆盖了 Animal 类中的 cry() 方法,并且新增了一个方法 climbTree(),那么它的参数列表为:
cry() -> Cat.cry()
getName() -> Animal.getName()
getAge() -> Animal.getAge()
climbTree() -> Cat.climbTree()

在运行的时候,调用 obj.cry() 方法的过程如下:

  • JVM 首先访问 obj 的实际类型的方法表,可能是 Animal 类的方法表,也可能是 Cat 类及其子类的方法表。
  • JVM 在方法表中搜索与 cry() 匹配的方法,找到后,就知道它属于哪个类了。
  • JVM 调用该方法。

你可能感兴趣的:(Java知识点2)