多态是继封装、继承之后,面向对象的第三大特性。
1、格式
父类类型 变量名 = 子类对象;
父类类型:指子类对象继承的父类类型,或者实现的父接口类型。
例如:
class Person{
private String name;
private int age;
Person(String name, int age){
this.name = name;
this.age = age;
}
public void speak(){
System.out.println(name + "说:我今年" + age);
}
}
class Man extends Person{
Man(String name, int age){
super(name,age);
}
}
class Woman extends Person{
Woman(String name, int age){
super(name,age);
}
}
class Test{
public static void main(String[] args){
Person[] arr = new Person[2];
arr[0] = new Man("张三",23);
arr[1] = new Woman("如花",18);
for(int i=0; i<arr.length; i++){
arr[i].speak();
}
System.out.println("------------------------");
show(new Man("张三",23));
show(new Woman("如花",18));
}
public static void show(Person p){
p.speak();
}
}
2、编译时类型与运行时类型不一致问题
代码如下:
定义父类:
public class Animal {
public void eat(){
System.out.println("吃~~~");
}
} ```
定义子类:
```java
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse(){
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
定义测试类:
public class Test {
public static void main(String[] args) {
Animal a1 = new Cat();
a1.eat();
Animal a2 = new Dog();
a2.eat();
}
}
3、多态的应用
1、多态应用在形参实参
父类类型作为方法形式参数,子类对象为实参。
代码如下:
public class Test01 {
public static void main(String[] args) {
showAnimalEat(new Dog());
showAnimalEat(new Cat());
}
public static void showAnimalEat (Animal a){
a.eat();
}
}
2、多态应用在数组
数组元素类型声明为父类类型,实际存储的是子类对象
public class Test02 {
public static void main(String[] args) {
Animal[] arr = new Animal[2];
arr[0] = new Cat();
arr[1] = new Dog();
for (int i = 0; i < arr.length; i++) {
arr[i].eat();
}
}
}
3、多态应用在返回值
方法的返回值类型声明为父类的类型,实际返回值是子类对象
public class Test03 {
public static void main(String[] args) {
Animal c = buy("猫咪");
System.out.println(c.getClass());
c.eat();
}
public static Animal buy(String name){
if("猫咪".equals(name)){
return new Cat();
}else if("小狗".equals(name)){
return new Dog();
}
return null;
}
}
4、向上转型与向下转型
首先,一个对象在new的时候创建是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型用于不会变。这个和基本数据类型的转换是不同的。
但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
class Animal {
void eat(){
System.out.println("~~~");
}
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
public void catchMouse() {
System.out.println("抓老鼠");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
public void watchHouse() {
System.out.println("看家");
}
}
class Test{
public static void main(String[] args){
Cat a = new Cat();
Animal b = a;
Object c = a;
System.out.println(a.getClass());
System.out.println(b.getClass());
System.out.println(c.getClass());
}
}
为什么要类型转换呢?
因为多态,就一定会有把子类对象赋值给父类变量的时候,这个时候,在编译期间,就会出现类型转换的现象。
但是,使用父类变量接收了子类对象之后,我们就不能调用子类拥有,而父类没有的方法了。这也是多态给我们带来的一点"小麻烦"。所以,想要调用子类特有的方法,必须做类型转换。
废话不多说上代码:
public class Test {
public static void main(String[] args) {
Animal a = new Cat();
a.eat();
Cat c = (Cat)a;
c.catchMouse();
Animal a2 = new Animal();
}
}
为了避免ClassCastException的发生,Java提供了 instanceof 关键字,给引用变量做类型的校验,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
变量名/对象 instanceof 数据类型
那么,哪些instanceof判断会返回true呢?
- 对象/变量的编译时类型 与 instanceof后面数据类型是直系亲属关系才可以比较
- 对象/变量的运行时类型<= instanceof后面数据类型,才为true
5、多态引用时关于成员变量与成员方法引用的原则
1、成员变量:只看编译时类型
如果直接访问成员变量,那么只看编译时类型
public class TestExtends {
public static void main(String[] args) {
Son s = new Son();
System.out.println(s.a);
System.out.println(((Father)s).a);
Father s2 = new Son();
System.out.println(s2.a);
System.out.println(((Son)s2).a);
}
}
class Father{
int a = 1;
}
class Son extends Father{
int a = 2;
}
2、非虚方法:只看编译时类型
在Java中的非虚方法有三种:
1、由invokestatic指令调用的static方法,这种方法在编译时确定在运行时不会改变。
javap -v .\Test.class
2、由invokespecial指令调用的方法,这些方法包括私有方法,实例构造方法和父类方法,这些方法也是在编译时已经确定,在运行时不会再改变的方法
3、由final关键字修饰的方法。虽然final方法是由invokevirtual指令进行调用的,但是final修饰的方法不能够进行在子类中进行覆盖,所以final修饰的方法是不能够在运行期进行动态改变的。在java语言规范中明确规定final方法就是非虚方法。
public class Test {
public static void main(String[] args) {
Father f = new Son();
f.test();
f.method();
}
}
class Father{
public static void test(){
System.out.println("Father.test");
}
public void method(){
System.out.println("Father.method");
fun();
other();
}
public void fun(){
System.out.println("Father.fun");
}
private void other(){
System.out.println("Father.other");
}
}
class Son extends Father{
public static void test(){
System.out.println("son");
}
public void fun(){
System.out.println("Son.fun");
}
private void other(){
System.out.println("Son.other");
}
}
3、虚方法:静态分派与动态绑定
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
当我们通过“对象.方法”的形式,调用一个虚方法,我们要如何确定它具体执行哪个方法呢?
(1)静态分派:先看这个对象的编译时类型,在这个对象的编译时类型中找到最匹配的方法
最匹配的是指,实参的编译时类型与形参的类型最匹配
(2)动态绑定:再看这个对象的运行时类型,如果这个对象的运行时类重写了刚刚找到的那个最匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个方法
(1)示例一:没有重载有重写
abstract class Animal {
public abstract void eat();
}
class Cat extends Animal {
public void eat() {
System.out.println("吃鱼");
}
}
class Dog extends Animal {
public void eat() {
System.out.println("吃骨头");
}
}
public class Test{
public static void main(String[] args){
Animal a = new Cat();
a.eat();
}
}
如上代码在编译期间先进行静态分派:此时a的编译时类型是Animal类,所以去Animal类中搜索eat()方法,如果Animal类或它的父类中没有这个方法,将会报错。
而在运行期间动态的在进行动态绑定:a的运行时类型是Cat类,而子类重写了eat()方法,所以执行的是Cat类的eat方法。如果没有重写,那么还是执行Animal类在的eat()方法
(2)示例二:有重载没有重写
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
public void method(Daughter f) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
Father f = new Father();
Father s = new Son();
Father d = new Daughter();
MyClass my = new MyClass();
my.method(f);
my.method(s);
my.method(d);
}
}
如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。
而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
此时不能分别执行method(Father f)、method(Son s)、method(Daughter d)
因为此时实参f,s,d编译时类型都是Father类型,因此method(Father f)是最合适的。
(3)示例三:有重载没有重写
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
MyClass my = new MyClass();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);
my.method(s);
my.method(d);
}
}
如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。
而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
此时实参f,s,d编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
(4)示例四:有重载没有重写
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class MySub extends MyClass{
public void method(Daughter d) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverload {
public static void main(String[] args) {
MyClass my = new MySub();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);
my.method(s);
my.method(d);
}
}
如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。
而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
-
my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法, -
f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配 -
而在MySub类中并没有重写method(Father f)方法,所以仍然执行MyClass类中的method(Father f)方法
(5)示例五:有重载有重写
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class MySub extends MyClass{
public void method(Father d) {
System.out.println("sub--");
}
public void method(Daughter d) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
public class TestOverloadOverride {
public static void main(String[] args) {
MyClass my = new MySub();
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
my.method(f);
my.method(s);
my.method(d);
}
}
如上代码在编译期间先进行静态分派:因为my是MyClass类型,那么在MyClass类型中寻找最匹配的method方法。
而在运行期间动态的在进行动态绑定:即确定执行的是MyClass类中的method(Father f)方法,因为my对象的运行时类型还是MyClass类型。
-
my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法, -
f,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配 -
而在MySub类中重写method(Father f)方法,所以执行MySub类中的method(Father f)方法
|