面试|如何理解Java中的多态

1 多态的含义及作用

在面向对象(OOP)的程序设计语言中,多态与封装、继承合称为OOP的三大特性
封装,说简单点就是合并属性和行为创建一种新的数据类型,而继承就是建立数据类型之间的某种关系(is-a),而多态就是这种关系在实际场景的运用
简单点说,多态就是把做什么和怎么做分开了;其中,做什么是指调用的哪个方法,我是去吃饭(方法a)还是去睡觉(方法b),怎么做是指实现方案,如果我选择吃饭,那么我是吃米饭还是吃面条,”分开了“则是指两件事不在同一时间确定
说的学术点,多态就是父类的引用指向子类的对象。这样做的好处就是可以消除类型之间的耦合关系。可以看下面的实例:

public class Test1 {
	public static void main(String[] args) {	
		method(new Circle());
	}
	
	static void method(Shape shape) {
		shape.draw();
	}
}

class Shape {
	public void draw() {};
	public void erase() {}
}

class Circle extends Shape {
	public void draw() {
		System.out.println("draw circle");
	}
	public void erase() {
		System.out.println("erase circle");
	}
}

class Square extends Shape {
	public void draw() {
		System.out.println("draw square");
	}
	public void erase() {
		System.out.println("erase square");
	}
}

class Triangle extends Shape {
	public void draw() {
		System.out.println("draw triangle");
	}
	public void erase() {
		System.out.println("erase triangle");
	}
}

这里通过类Circle,类Square和类Triangle继承类Shape,使得可以在Test1提供统一的访问入口,即方法method();如果没有继承和多态,不仅代码冗余而且类(Circle/Square/Triangle)与类(Test1)之间有耦合

那么为什么可以通过继承实现这样的功能呢?这就要讨论多态的原理了。

2 多态的分类及原理

上面的实例中,Shape类是父类,其他三个类是子类;method()方法接收的参数是父类Shape的对象,但调用的时候却传递子类Circle的对象,这就是奇怪之处,也是多态的体现
这里我们提出一个疑问,method(Shape shape)接受一个Shape引用,那么在这种情况下,编译器怎么样才能知道这个Shape引用指向的是Circle对象,而不是Square对象或Triangle对象呢
答案是:编译器也不知道。既然编译器不知道,那么肯定就是在代码运行时确定的,具体表现就是确定方法调用以及调用方法对象之间的关系,那么这个关系的确定就称为绑定。由于绑定动作发生在运行时,所以多态也称运行时绑定或者后期绑定,相比于静态的编译期,多态也称为动态绑定

动态绑定很抽象,我们不禁要问:JVM是如何绑定的呢?

关于这个问题,TIJ里面是这么说的,如果一种语言想实现后期绑定,就必须具有某种机制,以便在运行时能判断对象的类型,从而调用恰当的方法。也就是说,编译器不知道对象的类型,但是方法调用机制能够找到正确的方法体,并加以调用。后期绑定机制随编程语言的不同而有所不同,但是只要想一下就会得知,不管怎么样都必须在对象中安置某种”类型信息“。
所以,动态绑定的过程可以理解为下面几个过程:

  • 1 当虚拟机创建Circle类对象的时候,会创建一个类的方法列表,同时包含父类的方法列表
  • 2 子类型circle对象的引用向上转换成shape引用,确定引用与对象之间的关系
  • 3.虚拟机会找到参数引用实际指向的地址,也就是shape引用实际指向circle对象,并查询对象的方法列表,如果在circle对象中找到这个方法,就直接调用,否则查询父类shape方法并调用

注意步骤2中发生了类型转换。我们知道基本数据类型可以发生类型转换,如byte,short,char和int之间;那么引用类型之间也可以,但需要满足一定的条件(有继承关系)。那么这里发生的转换是从circle对象转到shape对象,即从子类转换父类,是向上转换

3 多态无处不在

Java中除了static方法和final方法(private方法属于final方法)是前期绑定,其他所有方法都是后期绑定。

①final关键字修饰的方法不能被重写,即防止被其他人覆盖该方法,同时也能避免动态绑定。
public class Test1 {
	public static void main(String[] args) {	
		method(new Circle());
	}
	
	static void method(Shape shape) {
		shape.change();
	}
}

class Shape {
	public void draw() {};
	public void erase() {}
	public final void change() {
		System.out.println("shape change");
	}
}

class Circle extends Shape {
	public void draw() {
		System.out.println("draw circle");
	}
	public void erase() {
		System.out.println("erase circle");
	}
}

由于final方法不能被重写,所以直接调用父类的change()方法。

②private关键字修饰的方法对子类不可见,也就不能被子类继承,那么也就无法通过子类对象调用private级别的方法;唯一调用的途径就是在类内通过其对象本身。所以说,private级别的方法和定义这个方法的类绑定在一起了。
③static关键字修饰的方法属于类而非对象,所以不能被重写,但static方法可以被子类继承(不能表现出多态性,因为父类的静态方法会被子类覆盖)。
public class Test1 {
	public static void main(String[] args) {	
		new Circle().change();    // ① shape change
		new Square().change();  // ② square change
		
		method(new Circle());     // ③ shape change
		method(new Square());   // ④ shape change
	}
	
	static void method(Shape shape) {
		shape.change();
	}
}

class Shape {
	public static void change() {
		System.out.println("shape change");
	}
}

class Circle extends Shape {}

class Square extends Shape {
	public static void change() {
		System.out.println("square change");
	}
}

从上面的运行结果可以得出:

  • 类Circle没有重写change()方法,但①表明Circle类继承了Shape类的change()方法,所以static方法可以被子类继承。
  • 类Square”重写“了父类change()方法,结果②表明square对象调用的是自身对象的change()方法;注意这里的”重写“加了引号。
  • 结果③和结果④表明,当子类向上转换成父类时,不论子类中有没有定义该静态方法,引用都会调用父类的静态方法。如果子类square真的重写父类change()方法,那么结果④就不对了,所以这里的重写只是表面的重写,而不是真正意义上的重写,即static方法并不能被子类重写。(可以尝试在子类的change()方法加上@Override注解)
  • 结果④进一步表明,子类的change()方法被父类shape隐藏了。可以简单理解为,调用该引用类型内的static方法。

4 多态的缺陷

①类的属性没有多态性

除了特别注意private级别的方法不能被继承以及静态方法可以继承但不能表现多态性外,还要注意类的属性也不能表现多态性,但是可以被继承

public class Test1 {
	public static void main(String[] args) {	
		Shape shape = new Circle();
		System.out.println("shape.radius: " + shape.radius + " , shape.getRadius: " + shape.getRadius());
		System.out.println("shape.area: " + shape.area + " , shape.getArea: " + shape.getArea());
		
		Circle circle = new Circle();
		System.out.println("circle.radius: " + circle.radius + " , circle.getRadius: " + circle.getRadius() + " , circle.getSuperRadius: " + circle.getSuperRadium());
		System.out.println("circle.area: " + circle.area + " , circle.getArea: " + circle.getArea());
	}
}

class Shape {
	public int radius = 0;
	public double area = 0.0;
	public int getRadius() {
		return radius;
	}
	public double getArea() {
		return area;
	}
}

class Circle extends Shape {
	public int radius = 1;
	public int getRadius() {
		return radius;
	}
	public double getArea() {
		return area;
	}
	public int getSuperRadium() {
		return super.radius;
	}
}

运行结果:

shape.radius: 0 , shape.getRadius: 1
shape.area: 0.0 , shape.getArea: 0.0
circle.radius: 1 , circle.getRadius: 1 , circle.getSuperRadius: 0
circle.area: 0.0 , circle.getArea: 0.0

根据结果可以知道,属性area被子类继承,属性radius被覆盖,所以属性域可以被子类继承,但不具备多态性。对于属性area,子类和父类共同指向同一块内存空间,而对于属性radius,子类和父类独自拥有存储字符串的内存空间,所以radius属性根据引用类型来决定。

②构造函数没有多态性

构造函数实际上是static方法,所以构造函数不具备多态性。但我们知道子类的每次实例化,父类都会默认执行不带参数的构造方法;这样做的目的是为了让父类自己初始化自个的属性(一般为private)从而构建完整的父类对象共子类使用。
但如果父类构造函数中调用被子类重写的实例方法,会出现什么结果呢?看下面的实例:

public class Test1 {
	public static void main(String[] args) {	
		new Circle(5);
	}
}

class Shape {
	public Shape() {
		System.out.println("shape() before draw()");
		draw();
		System.out.println("shape() after draw()");
	}
	public void draw() {
		System.out.println("shape draw()");
	}
}

class Circle extends Shape {
	private int radius = 1;
	
	public Circle(int radius) {
		this.radius = radius;
		System.out.println("circle.circle(). radius = " + radius);
	}
	public void draw() {
		System.out.println("Circle.draw(). radius = " + radius);
	}
}

运行结果:

shape() before draw()
Circle.draw(). radius = 0
shape() after draw()
circle.circle(). radius = 5

new子类Circle对象时,会调用父类Shape的不带参数的构造方法,但是父类构造方法内调用了一个被子类Circle重写的方法draw(),根据结果可知,会调用子类的draw()方法;但是radius属性值却不是默认的1,而是0,这表明Circle对象还没有来得及构建,其radius属性为JVM给的默认值0,如果是引用类型的话则为null。所以在构造函数内,尽量避免使用实例方法,除了final和private方法

毕!

你可能感兴趣的:(Java基础知识,Java基础面试题)