多态
相同类型的不同对象,调用同一个方法,最终执行结果是不同的。
1.java中的对象,它的实际类型一旦确定不会改变。
例如:
new Student(); 这个对象的实际类型就是Student,因为是使用Student类创建出来的。但是这个对象也属性Object类型,同时这个对象也属于Person类型,因为Person、Object都是这个对象的父类型。
new Person();
注意,java中的对象,可以【属于】很多种类型(因为有父类型,还有接口),但是这个对象只有一个实际类型,就是这个对象在运行的时候到底使用的是哪一个类创建出来的,并且这个实际类型一旦确定就不会再改变了。
注意,之前再学习变量的时候,也是类似的这种情况
一个变量一旦被声明出来,那么这个变量的类型就不会再改变了,虽然可以做类型的转换的,但这只是得到了一个转换之后的结果,转换结束之后,这变量还是原来的类型。
例如:
Person p;
p = new Student();
Student stu = (Student)p;
这里的变量p,声明为Person类型的,那么任何时候访问到这个变量p都是Person类型的,不会改变。
虽然下面做了类型强制转换,但是我们只是把转换后的结果赋值给了新的变量stu,这个操作结束后,变量p还是原来的Person类型。
2.指向同一个对象的引用,可以是不同的类型
一个对象的实际类型虽然是确定的只有一个,但是这个对象所属的类型却有很多种(一般是它的的父类型或者实现的接口),所以我们可以使用不同的类型去指向这个一个对象。
例如:
Student s1 = new Student();
Person s2 = new Student();
Object s3 = new Student();
因为Person、Object都是Student的父类型
3.一个父类型的引用,可以指向它的任何一个子类对象。
这句话反过来描述就是:
一个对象,可以被它的任何一个【直接父类或者间接父类或者实现的接口类型】 的引用所指向
例如:
Object o = new 任意类型();
Person p;
p = new Student();
p = new Teacher();
p = new Person();
4.多态中的方法调用(需要结合重写)
例如:
public class Person{
public void run(){}
}
public class Student extends Person{
}
//掉用到的run方法,是从父类Person中继承过来的run方法
main:
Person p = new Student();
p.run();
例如:
public class Person{
public void run(){}
}
public class Student extends Person{
public void run(){
//子类中重写这个run方法
}
}
//掉用到的run方法,是子类中重写后的run方法
main:
Person p = new Student();
p.run();
5.子类中独有方法的调用
就是子类继承了父类,然后在子类中又定义扩展一些属于自己的方法。
例如:
public class Person{
public void run(){}
}
public class Student extends Person{
public void say(){
}
}
main:
Person p;
p = new Student();
//编译通过
p.run();
//编译报错
//因为变量p的类型是Person,并且这个变量p的类型是不会改变的,编译器检查这个Person发现没有say方法,所以报错。
p.say();
注意,一个变量,假设名字为x,当这个变量去调用一个方法的时候,例如x.test(),编译器能否让这句代码编译通过,主要是看这个变量x声明时候的类型是什么,以及这个类型中是否定义了要调用的test方法,如果有test方法则编译通过,否则编译报错。至于这个变量x是指向哪一个具体的对象,编译器是不关心的。因为在运行期间,这个变量x是可以随意指向它的任何一个子类对象的,在编译的期间根本无法确定将来到底会指向哪一个对象。
总结:编译器只关心引用声明时的所属类型,而不关心这个引用指向哪一个具体的对象。
6.子类引用和父类引用指向对象的区别
Student s = new Student();
Person p = new Student();
子类类型的变量s,能调用的方法是Student中有的方法,包含Student继承过来和Student中自己的方法。
父类类型的变量p,能调用的方法是Person中的有的方法,包含Person继承过来和Person中自己的方法。
注意,很明显可以看出,变量s能调用的方法是比变量p能调用的方法要多的。
父类类型的变量p,不仅可以指向Student对象,还可以指向Teacher对象,假设Teacher、Student都是Person的子类型
子类类型的变量s,只能指向Student类型的对象,以及Student子类类型的对象。
注意,很明显可以看出,变量p能够指向的对象范围是要变量s要大的。
例如,Object o = new Student();
Object类型的变量o,能指向的对象访问是最大的,能够指向java中的任意的对象,但是变量o能调用到的方式也是最少的,只能调用方法只有Object类中声明的方法,因为变量o声明的类型就是Object
总结:到这为止,我们只是解决了编译器是否会运行我们的这代码o.test()编译通过的问题,后面的问题:这个test方法将来调用的到底是哪一个? 这个问题是和变量o将来指向的对象是谁,以及这个对象中是否重写过test方法,这个俩个方面有关系。
注意,java中的方法调用,是运行时动态和对象绑定的,不到最后运行的时候,是无法知道将来哪一个对象中的test方法被调用。
7.使用多态的相关示例
一个java文件中,可以写多个类,但只能有一个类是public的
Shape.java:
public class Shape{
public void draw(){
System.out.println("正在进行图形的绘制...");
}
public void draw(String title){
System.out.println("正在进行图形的绘制... 标题为:"+title);
}
}
class Circle extends Shape{
public void draw(){
System.out.println("绘制一个圆形");
}
public void draw(String title){
System.out.println("绘制一个圆形... 标题为:"+title);
}
}
class Rectangle extends Shape{
public void draw(){
System.out.println("绘制一个正方形");
}
public void draw(String title){
System.out.println("绘制一个正方形... 标题为:"+title);
}
}
class ShapeTest{
public static void main(String[] args){
ShapeTest t = new ShapeTest();
Shape shape;
int a = (int)(Math.random()*10); //[0,9]
if(a%3==0){
shape = new Shape();
}
else if(a%3==1){
shape = new Circle();
}
else{
shape = new Rectangle();
}
t.draw(shape);
}
//这里shape.draw()方法的调用
//将来到底会调用到哪一个对象中的draw方法
//主要是看在运行期间,这个引用shape最终指向的对象是谁,以及这个对象中是否重写过draw方法
public void draw(Shape shape){
shape.draw();
}
public void draw(Shape shape,String title){
shape.draw(title);
}
}
Game.java:
public class Game{
public void start(Ball ball){
ball.run();
}
}
class Ball{
public void run(){
System.out.println("启动游戏...");
}
}
class BasketBall extends Ball{
public void run(){
System.out.println("启动运行篮球游戏...");
}
}
class FootBall extends Ball{
public void run(){
System.out.println("启动运行足球游戏...");
}
}
class PingPangBall extends Ball{
public void run(){
System.out.println("启动运行乒乓球游戏...");
}
}
class GameTest{
public static void main(String[] args){
Game g = new Game();
Ball ball;
ball = new BasketBall();
ball = new FootBall();
ball = new PingPangBall();
g.start(ball);
}
}
instanceof关键字
instanceof本质是判断的是一个引用所指向的对象实际类型和另一个类型是否有子父类型的关系(将来还可以用来判断类和实现接口的关系)
例如:
public class Person{
public void run(){}
}
public class Student extends Person{}
public class Teacher extends Person{}
main:
Object o = new Student();
System.out.println(o instanceof Student);//true
System.out.println(o instanceof Person);//true
System.out.println(o instanceof Object);//true
System.out.println(o instanceof Teacher);//false
System.out.println(o instanceof String);//false
Person o = new Student();
System.out.println(o instanceof Student);//true
System.out.println(o instanceof Person);//true
System.out.println(o instanceof Object);//true
System.out.println(o instanceof Teacher);//false
//System.out.println(o instanceof String);//编译报错
Student o = new Student();
System.out.println(o instanceof Student);//true
System.out.println(o instanceof Person);//true
System.out.println(o instanceof Object);//true
//System.out.println(o instanceof Teacher);//编译报错
//System.out.println(o instanceof String);//编译报错
注意,System.out.println(x instanceof Y); 编译的问题
这个代码能不能编译通过,主要是看变量x的声明的类型,和要判断的Y类型之间,有没有子父类关系,如果有就编译通过,如果没有则编译报错。(这个是针对x的声明类型和Y类型都是【类】的前提下)
注意,System.out.println(x instanceof Y); 运行的问题
编译通过后,运行的结果是true还是false,主要是看引用类型变量x所指向对象的实际类型,是不是要比较的Y类型或者Y类型的子类类型,如果是的话,就返回true,否则返回false
例如:获取一个引用所指向对象的实际类型
public void draw(Shape shape){
shape.draw();
//这句代码就可以输出将来在运行期间
//引用shape所指向对象的【实际类型】的名字
System.out.println(shape.getClass().getName());
}
main:
ShapeTest t = new ShapeTest();
Shape shape;
int a = (int)(Math.random()*10); //[0,9]
if(a%3==0){
shape = new Shape();
}
else if(a%3==1){
shape = new Circle();
}
else{
shape = new Rectangle();
}
t.draw(shape);
总结,很多时候,编译器在编译期间,关注的是这个引用类型变量声明的类型是什么,因为一个变量的类型一旦声明就不会再改变了,但是这个变量所指向的对象,是可以随意改变的,只要满足子父类关系即可。
而JVM在运行的时候,关注的时候这个引用类型变量所指向的对象是谁,因为运动时调用方法,是需要知道这个方法到底是哪一个对象中的方法,这就要JVM非常明确这个引用类型变量所指向的对象到底是谁