JAVA面向对象---多态

JAVA面向对象-----多态

在学习多态之前,我们需要认识这样一个概念:继承是实现多态的一个前提。多态能够帮助我们解决大量的分支语句的情况。

一.基础语法

1.向上转型
一个父类的引用指向了一个子类的对象(看起来好像是把子类的引用转成了父类的引用)。
示例代码1:

//Animal.java
public class Animal {
}

//Cat.java
public class Cat extends Animal {
}

//test.java
public class Test {
    public static void main(String[] args) {
       Cat cat = new Cat();//1
       Animal animal = null;//2
        // 向上转型
       animal = cat;//3
       
       //1,2,3行代码可以合并写成:
		Animal animal = new Cat();

上述代码中,animal = cat;这句代码就实现了向上转型,animal是父类(Animal)的一个引用,此时却指向了子类(Cat)的一个实例,这种写法就称作“向上转型”。

为什么叫 “向上转型”?
在面向对象程序设计中, 针对一些复杂的场景(很多类, 很复杂的继承关系), 程序员会用画 UML 图的方式来表示类之间的关系。此时父类通常画在子类的上方. 所以我们就称为 “向上转型” , 表示往父
类的方向转。

向上转型发生的时机:
1.直接赋值
2.方法传参
3.方法返回

直接赋值的方式如示例代码1中演示, 另外两种方式和直接赋值没有本质区别。
方法传参过程中发生向上转型如示例代码2所演示。方法传参的本质也就是在进行“赋值操作”。
示例代码2:

	func(new Cat());//方法传参
    
    public void func(Animal animal){
    
    }

方法返回的时候发生向上转型如实例代码3所演示。
示例代码3:

	Animal animal =func();//方法返回
	public static Animal func(){//1
		return new Cat();//2
		
		//1,2行代码等价于:
	public static Animal func(){
		Cat cat = new Cat();
		Animal animal = cat;//向上转型出现在此处的赋值操作上
		return animal;
}

在这段代码中,func返回的似乎是一个Animal类型的内容,实际上是把new Cat()的结果作为返回值,也相当于把Cat类型转变成Animal类型。我们需要注意的是,此处的父类引用(animal)虽然指向的是一个子类对象,但是它是无法访问子类(Cat)独有的属性和方法,由于访问属性这件事是在编译过程中确定的,所以编译的时候编译器就会检查当前的属性是否在该类中存在,否则编译不会通过。

二.动态绑定

在向上转型的过程中,如果父类中包含的方法在子类中有对应的同名同参数的方法,就会进行动态绑定。一般我们所谈到的“静态”和“动态”,分别指的是“编译期”,“运行时”,和“static”无关。这里的动态绑定也就是说在代码运行时决定我们调用哪个方法。

示例代码4:


```java
//Animal.java
public class Animal {
    public void eat(String food) {
        System.out.println("Animal 正在吃 " + food);
    }
}

//Test.java
public class Test {
    public static void main(String[] args) { 
        Animal animal = new Cat();
        animal.eat("鱼");
        }
}

在示例代码4中,eat()方法只在父类中存在,此时调用的eat()方法就是父类的这个eat()方法。该过程不涉及动态绑定。

示例代码5:

//Animal.java
public class Animal {
}

//Cat.java
public class Cat extends Animal {
    public void eat(String food) {
        System.out.println("Cat 正在吃 " + food);
    }
}

//Test.java
public class Test {
    public static void main(String[] args) { 
        Animal animal = new Cat();
        animal.eat("鱼");
        }
}

此时编译我们会发现,编译报错。因为此时eat()方法只在子类中存在,animal虽然作为父类引用指向了子类Cat的对象,但是它是无法访问子类Cat独有的属性和方法eat()。该过程不涉及动态绑定。

示例代码6:

//Animal.java
public class Animal {
    public void eat(String food) {
        System.out.println("Animal 正在吃 " + food);
    }
}

//Cat.java
public class Cat extends Animal {
    public void eat(String food) {
        System.out.println("Cat 正在吃 " + food);
    }
}

//Test.java
public class Test {
    public static void main(String[] args) { 
        Animal animal = new Cat();
        animal.eat("鱼");//输出结果为:Cat 正在吃鱼。
        }
}

在示例代码6中,eat()方法在父类和子类中都存在,并且参数也相同,此时调用eat()方法就涉及到了“动态绑定”。
在程序运行时,会看animal究竟指向的是一个父类实例还是一个子类实例,再决定调用父类版本的eat()还是调用子类版本的ea()方法。在示例代码中animal引用指向的是子类Cat的实例,所以最后的输出结果是:Cat正在吃鱼。

示例代码7:

//Animal.java
public class Animal {
    public void eat(String food) {
        System.out.println("Animal 正在吃 " + food);
    }
}

//Cat.java
public class Cat extends Animal {
    public void eat(String food,String arg) {
        System.out.println("Cat 正在吃 " + food);
    }
}

//Test.java
public class Test {
    public static void main(String[] args) { 
        Animal animal = new Cat();
        animal.eat("鱼");//输出结果为:Animal正在吃鱼。
        }
}

如实例代码7中所示,eat()方法在父类和子类中都存在,并且参数不相同,此时,如果调用eat()方法,不涉及“动态绑定”。看起来像是一种“重载”,但和重载存在差异,会根据调用方法时传入的参数类型和个数,判断在父类中是否存在匹配的方法,如果不存在就编译报错。该过程是在编译期执行。

注意:
方法限定符会影响到动态绑定。如果子类中的这个方法是private,这个时候外部根本看不到子类的这个方法,此时就不涉及到动态绑定了。

三.方法重写"override"

上述的动态绑定的规则,我们是站在编译器和JVM实现者的角度来看待的。实际上,这个操作过程在Java的语法层次上,有一个专门的术语—“方法重写(Override)”。即:子类实现父类的同名方法, 并且参数的类型和个数完全相同, 这种情况称为覆写/重写/覆盖

注意事项:

  1. 普通方法可以重写, static 修饰的静态方法不能重写。
  2. 重写中子类的方法的访问权限不能低于父类的方法访问权限。
  3. 重写的方法返回值类型不一定和父类的方法相同(但是建议最好写成相同, 特殊情况除外)。
    假设父类和子类方法的返回值完全互不相干,这种情况下是会编译报错的;如果父类和子类方法的返回值类型具有父子关系,这种情况下,就可以返回值类型不同。

示例代码8:

// Animal.java
public class Animal {
	public void eat(String food) {
		...
	}
}

// Bird.java
public class Bird extends Animal {
	@Override
	public void eat(String food) {
		...
	}
}

在示例代码8中,我们实现了一个eat()方法的重写,需要注意的是,如果我们将子类Bird的 eat()方法 由public改成 private,那么程序在编译时就会报错,因为Bird中的eat()方法只能在当前类中被访问,无法覆盖Animal中的eat()方法。另外, 针对重写的方法, 可以使用 @Override 注解来显式指定,告诉编译器,当前这个子类的方法是重写了父类的方法。

注意
JAVA中所有的类都是直接或者间接继承自Object类,该类可以看成是一个“祖宗类”。

四.重写的设计原则

对于已经投入使用的类,尽量不要进行修改。最好的方式是:重新定义一个新的类,来重复利用其中共性的内容,并且添加或者改动新的内容。(尤其是在使用别人开发的类的时候)
例如:
若干年前的手机,只能打电话,发短信,来电显示只能显示号码,而今天的手在来电显示的时候,不仅仅可以显示号码,还可以显示头像,地区等。在这个过程当中,我们不应该在原来的老的类上进行修改,因为原来的类,可能还在有用户使用,我们的正确做法是:新建一个新手机的类,对来电显示这个方法重写就好了,这样就达到了我们当今的需求了。

五.重写(Override)和重载(Overload)的区别

NO区别重载(Overload)覆写(Override)
1概念方法名称相同,参数的类型及个数不同方法名称,返回值类型,参数的类型及个数完全相同
2范围一个类继承关系
3限制没有权限要求被覆写的方法不能拥有比父类更严格的访问控制权限

六.理解多态

多态(polypeptide),是一种程序设计的思想方法,具体的语法体现就是“向上转型”, “动态绑定”,“ 方法重写”,学了上述内容后, 我们就可以使用 多态的形式来设计程序了。
多态,可以直观的理解为:一个引用,对应到多种形态(不同类型的实例)。我们在写代码时可以只关注父类的代码, 但能够同时兼容各种子类。

示例代码9:

// Shape.java
public class Shape {
    public void draw() {
       // 把当前的形状给打印出来
    }
}

//Circle.java
// 圆是一种形状, 符合 is-a 的语义, 于是就可以进行继承。
public class Circle extends Shape {
    @Override
    public void draw() {
        System.out.println("○");
    }
}

/Rectangle.java
// 矩形是一种形状, 符合 is-a 的语义, 于是就可以进行继承。
public class Rectangle extends Shape {
    @Override
    public void draw() {
        System.out.println("□");
    }
}

//Flower.java
public class Flower extends Shape {
    @Override
    public void draw() {
        System.out.println("❀");
    }
}

在示例代码9中,我们创建了一个Shape类,并且构建了一个draw()方法,然后相继创建了Circle类,Rectangle类,Flower类都继承了Shape类的draw()方法,并且重写了draw()方法。接下来,我们通过创建一个test类来对该代码实现多态。

示例代码10:

//test.java
public class test{
public static void main(String[] args) {

		Shape shape1 = new Rect();
        Shape shape2 = new Circle();
        Shape shape3 = new Flower();

		 draw(shape1);
       	 draw(shape2);
         draw(shape3);
}

  // 当新增形状的时候, drawShape方法本身无需做出任何修改。
    public static void drawShape(Shape shape) {
        shape.draw();
    }

此处代码体现出了“向上转型”,父类的引用指向了子类的实例。
在这里插入图片描述
此处代码体现了“动态绑定”,调用draw()方法时,具体指向哪个类型就要看运行时shape实际指向哪一个类型。
在这里插入图片描述
此处代码体现了“方法重写”,当然这里有三个类进行了draw()方法重写。
在这里插入图片描述
在Java体系中,具备了以上这三种语法,就可以使用多态机制了!三者缺一不可,当然,在Java体系中,我们也得必须搭配"继承"才能使用多态。

理解多态

上述代码中,我们创建了三个对象实例和一个drawShape()方法,我们这个时候需要知道,在我们开发代码的过程中,调用这个drawShape()方法的人和实现这几个类的人不一定是同一个人。也就说,调用drawShape()方法的人,都不需要知道此处的参数shape是那种类型就可以直接使用该方法。

使用多态的好处:

1.多态这种设计,本质上是“封装”得进一步。封装的目的是为了让使用者不需要知道类的使用细节,就能使用,但是使用者人需要知道这个类是什么类型。使用多态的话,此时类的使用者,不仅不需要知道类的实现细节,也不需要知道这个类具体是什么类型,只要知道这个类有一个draw()方法就可以~~这个时候,类的使用者知道的信息更少,使用成本就更低。

2.多态的使用方便了我们的扩展,如果我们此时需要新增一个形状,只需创建一个新的子类即可,并且让这个子类也去重写draw()这个方法。而类的调用者这里的代码很少需要做出修改。

3.多态的使用可以减少一些分支语句的使用。(示例代码11:未使用多态实现上述代码相同功能)

示例代码11:

public class test{
        Shape[] shapes = {
                new Rect(),
                new Circle(),
                new Flower(),
                new Triangle()
        };
        for (Shape shape : shapes) {
            shape.draw();
        }
        shapes[0]; // 类型就是 Shape 类型的引用, 但是实际指向的是 Rect 类型的实例
        for (Shape shape : shapes) {
            // 如果不使用多态(向上转型, 方法重写, 动态绑定)
            if (shape.type == "圆形") {
                // 执行打印圆形的代码
            } else if (shape.type == "方形") {
                // 执行打印方形的代码
            } else if (shape.type == "花型") {
                // 执行打印花型的代码
            } else if (shape.type == "三角形") {
                // 执行打印三角形的代码
            } else {
                // ...
            }
        }
    }

七.向下转型

在我们日常开发的过程中,我们大部分使用的场景都是向上转型,并且是结合“多态”来使用;向下转型用得不多,只有在一些特定的场景下才会使用。向下转型实质上就是把父类引用转成子类引用。

示例代码12:

public class test {
    //向上转型
    Animal animal1 = new Cat();//1
    //向下转型
    Cat cat = (Cat)animal1;//2
    
    Animal animal2 = new Bird();//3
    //此处的向下转型就是存在问题的,虽然编译可以通过,但是运行时会报异常。
    Cat cat2 = (Cat)animal2;//4

    Animal animal3 = new Animal();//5
    //此处的向下转型就是存在问题的,虽然编译可以通过,但是运行时会报异常。
    Cat cat3 = (Cat)animal2;//6
}
//Animal.java
public class Animal{
}
//Cat.java
public class Cat extends Animal{
}
//Bird.java
public class Bird extends Animal{
}

代码解释:

示例代码1中,//2代码把//1中父类的引用强行转变成子类的引用,从而实现了“向下转型”。我们需要注意的是,在向下转型中必须要确保animal1指向的实例是一个Cat类型的实例才可以进行转换,否则这样的转换可能会失效。在//3和//4代码中,由于animal2指向的实例是一个Bird实例,在//5和//6代码中,animal3指向的实例是一个Animal实例,所以这两个引用变量在进行向下转型时是不符合语法规则的,虽然在编译过程中不会报错,但是在运行时会发生“类型转换异常”报错。

虽然我们使用向下转型的机会不是很多,但是在一些特殊情况下,我们需要使用向下转型。

例如:
在我们学习JDBC编程(使用Java代码来操作数据库)时,我么会涉及到一个核心的类—DataSource。由于我们实际的数据库有很多种(Sql,Sever,MySQL,Oracl等),此时,我们的DataSource类就相当于一个父类,那些五花八门的数据库就相当于一个个子类,在使用的时候,我们需要根据不同的数据库的特性,来确定选择不同的数据库使用,就需要通过父类向下转型来调用这些数据库中的一些方法。

向下转型的应用场景:

有些方法只是在子类中存在,但是父类中不存在,此时使用多态的方式就无法执行到对应的子类的方法了,就必须把父类引用先转回成子类引用,然后在调用子类对应的方法。

在进行向下转型之前先做出判定,判定当前的父类的引用到底是不是指向该子类,如果不是就不进行向下转型。

示例代码13:

  Animal animal2 = new Bird();//3
    //此处的向下转型就是存在问题的,虽然编译可以通过,但是运行时会报异常。
    if(animal2 instanceof Cat){
    Cat cat2 = (Cat)animal2;//4
    }

我们可以使用instanceof关键字进行判断。如示例代码2所示,在进行//4执行之前,我们先判断一下animal2这个引用是不是Cat类型,如果是则返回True,执行下一句代码,如果不是则直接跳出。

八.在构造方法中调用重写的方法

一段有坑的代码. 我们创建两个类, A是父类, B是子类. B中重写 func() 方法. 并且在A的构造方法中调用func()。

示例代码14:

//通过这个代码来说明在构造方法中调用重写的方法带来的问题。
class A {
    public A() {
        this.func();
    }
    public void func() {
        System.out.println("A.func()");
    }
}
class B extends A {
    private int num = 1;
    @Override
    public void func() {
        System.out.println("B.func() " + num);
    }
}
class TestCSDN {
    public static void main(String[] args) {
        B b = new B();
    }
}

运行结果:在这里插入图片描述
代码解释:

1.A是B的父类,构造B的时候,就需要先构造A的实例。
2.构造A的实例,就会调用A的构造方法。
3.调用A的构造方法的时候就会调用到this.func()。此时的this.func()是指向子类的实例,触发了func()方法的动态绑定。
4.如果在B.func()中打印B的num属性值,会发现,结果是0。这就涉及到了对象被创建的时候的初始化顺序问题,我们在创建B的实例化的时候,会先触发父类A的构造方法,父类A的构造方法会去执行this.func()方法,在这个阶段,我们在子类B中重写的func()方法就被调用了,而此时子类B的属性num=1还没有被初始化,所以才打印出来的num=0。num=1需要等我们父类A完全构造完成后,开始执行子类B的构建时才能将num=1进行初始化。

上述代码是比较让人容易误会的,实际开发中不建议在构造方法中调用其他方法,尤其是可能被重写的方法,在实现构造方法的时候,最好是只进行简单的赋值操作,让我们的实例能够尽快进入“工作”状态。

九.代码执行顺序

1.实例代码块和静态代码块的执行顺序

示例代码15:

class Person {
	public String name;
	public int age;
public Person(String name, int age) {
	this.name = name;
	this.age = age;
System.out.println("构造方法执行");
}
{
System.out.println("实例代码块执行");
}
static {
	System.out.println("静态代码块执行");
	}
}
public class TestDemo {
	public static void main(String[] args) {
		Person person1 = new Person("bit",10);
		System.out.println("============================");
		Person person2 = new Person("gaobo",20);
	}
}

运行结果:在这里插入图片描述
代码解释:

静态代码只执行一次,并且静态代码块是在类加载的时候就进行执行,然后再执行实例代码块,最后执行构造方法代码块。

2.继承关系上的执行顺序

示例代码16:

class Person {
	public String name;
	public int age;
	public Person(String name, int age) {
		this.name = name;
		this.age = age;
		System.out.println("Person:构造方法执行");
	}
	{
		System.out.println("Person:实例代码块执行");
	}
	static {
		System.out.println("Person:静态代码块执行");
	}
}

class Student extends Person{
	public Student(String name,int age) {
		super(name,age);
		System.out.println("Student:构造方法执行");
	}
	{
		System.out.println("Student:实例代码块执行");
	}
	static {
		System.out.println("Student:静态代码块执行");
	}
}

public class TestDemo4 {
	public static void main(String[] args) {
		Student student1 = new Student("张三",19);
		System.out.println("===========================");
		Student student2 = new Student("gaobo",20);
	}
     //这里的意思是“第二次实例化对象”
	public static void main1(String[] args) {
		Person person1 = new Person("bit",10);
		System.out.println("============================");
		Person person2 = new Person("gaobo",20);
	}
}

运行结果:在这里插入图片描述
根据运行结果,可以得到以下结论:

1、父类静态代码块优先于子类静态代码块执行,且是最早执行。
2、父类实例代码块和父类构造方法紧接着执行。
3、子类的实例代码块和子类构造方法紧接着再执行。
4、第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值