深入讲解Java中的多态和抽象类

 更新时间:2023年08月07日 10:48:35   作者:两个猫崽子和你  
这篇文章主要介绍了深入讲解Java中的多态和抽象类,有时候,设计一个数组或方法的参数,返回值类型时,无法确定具体的类型,只能确定是某个系列的类型,这时就引入了多态,需要的朋友可以参考下

1、多态

  • 同一行为,通过不同事物,可以体现不同的形态。Java是强类型静态语言。
    • 强类型:每个变量在声明之前必须声明它确切的类型
    • 静态:赋值和运算时都是严格按照声明时的数据类型来处理的
  • 有时候,设计一个数组或方法的参数,返回值类型时,无法确定具体的类型,只能确定是某个系列的类型,这时就引入了多态

1.1 多态的实现方式

父类的引用,创建子类对象。必须有继承,父类定义方法,子类重写方法。

  • 方法的形参用父类类型,传入的实参用子类类型
  • 使用父类类型声明对象,创建的是子类对象
class Pet{
    public String name="pet";
    public void speak(){
    }
}
class Dog extends Pet{
    public String name="dog";
    @Override
    public void speak(){
        System.out.println("汪汪汪");
    }
    public void work(){
    }
}
class Cat extends Pet{
    public String name="cat";
    @Override
    public void speak(){
        System.out.println("喵喵喵");
    }
}
class Master{
    public void speak(Pet pet){//形参是父类类型对象
        pet.speak();
    }
}

1.2 多态状态下需要注意的问题

Pet pet = new Dog();//使用父类类型声明对象,创建的是子类对象
//等号左边是编译时类型,等号右边是运行时类型
pet.play();//编译和运行都正常
//pet.work();//报错
/*
	报错原因
	1.调用时按照编译时方法调用,运行方法时按照运行时类型运行
    2.能调用什么类型看等号左边有定义什么方法,子类独有的方法不能被调用
    3.调用了重写方法时,调用的是父类的方法,执行时入栈的是子类重写过的方法
*/
Dog dog = (Dog)pet;
dog.work();//正确,先进行强转,然后调用子类独有的方法
//访问属性时,默认访问的是引用类型的属性
System.out.println(pet.name);//pet
System.out.println(pet1.name);//pet
@Test
public void test(){
    Master master = new Master();
    Pet pet = new Cat();//使用父类类型声明对象,创建的是子类对象
    //等号左边是编译时类型,等号右边是运行时类型
    //调用时按照编译时方法调用,运行方法时按照运行时类型运行
    //能调用什么类型看等号左边有定义什么方法,子类独有的方法不能被调用
    //调用了重写方法时,调用的是父类的方法,执行时入栈的是子类重写过的方法
    Pet pet1 = new Dog();
    master.speak(new Cat());//实参是子类类型对象
    master.speak(new Dog());
    //访问属性时,默认访问的是引用类型的属性
    System.out.println(pet.name);//pet
    System.out.println(pet1.name);//pet
}

1.3 多态状态下的转型

@Test
public void test(){
    //实际在堆中创建的对象类型是运行时类型,也就是Dog类对象
    Pet pet = new Dog();//将子类对象的引用类型从子类类型自动提升成父类类型,称为向上转型
    Cat cat = (Cat)pet;//编译不报错,运行时报错ClassCastException
    Dog dog = (Dog)pet;//强制类型转换,将父类引用类型转成子类引用类型,向下转型
}
  • 向上转型:自动转换,将子类对象的引用类型从子类类型自动提升成父类类型
  • 向下转型:强制转换,将父类引用类型转成子类引用类型
    • 向下转型要注意的问题:需要使用instanceof关键字判断引用对象是否为某一类的对象
@Test
public void test(){
    //实际在堆中创建的对象类型是运行时类型,也就是Dog类对象
    Pet pet = new Dog();//将子类对象的引用类型从子类类型自动提升成父类类型,称为向上转型
    Cat cat = (Cat)pet;//编译不报错,运行时报错ClassCastException
    Dog dog = (Dog)pet;//强制类型转换,将父类引用类型转成子类引用类型,向下转型
    System.out.println(pet instanceof Dog);//true
    System.out.println(pet instanceof Cat);//false
    System.out.println(pet instanceof Pet);//true
}

1.4 多态的应用

  • 多态参数:方法的形参用父类类型,传入的实参用子类类型
  • 多态数组:同一父类的不同子类对象可以组成一个数组
@Test
public void test(){
    Pet[] pets = new Pet[4];
    pets[0] = new Cat();
    pets[1] = new Dog();
    pets[2] = new Dog();
    pets[3] = new Cat();
    for(int i=0;i<pets.length;i++){
        pets[i].play();
    }
}

1.5 虚方法和非虚方法

只有虚方法才能实现多态,使用的比较多的虚方法

1.5.1 非虚方法

方法在编译期,就确认了具体的调用版本,在运行时不可变,这种方法就称为非虚方法

  • 静态方法:与类型直接关联
  • 私有方法:在外部不可访问
  • final修饰的方法:不能被继承
  • 实例构造器(构造方法),通过super调用的父类方法

 1.5.2 虚方法

  • 静态分派:使用父类的引用调用方法
  • 动态绑定:根据具体的运行时类型决定运行哪个方法
    • 方法的参数在编译期间确定,根据编译器时类型,找最匹配的
    • 方法的所有者如果没有重写,就按照编译时类型处理;如果有重写,就按照运行时类型处理

1.5.3重写和重载中的方法调用

  • 重写示例1
   /*
   	1、编译期间进行静态分派:即确定是调用Animal类中的public void eat()方法,如果Animal类或它的父类中没有这个方法,将会报错。
   	2、运行期间进行动态绑定:即确定执行的是Cat类中的public void eat()方法,因为子类重写了eat()方法,如果没有重写,那么还是执行Animal类在的eat()方法
   */
   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();
       }
   }
  • 重载示例1
   class Father{
   }
   class Son extends Father{
   }
   class Daughter extends Father{
   }
   class MyClassOne{
   	public void method(Father f) {
   		System.out.println("father");
   	}
   	public void method(Son s) {
   		System.out.println("son");
   	}
       public void method(Daughter d) {
   		System.out.println("daughter");
   	}
   }
   class MyClassTwo{
   	public void method(Father f) {
   		System.out.println("father");
   	}
   	public void method(Son s) {
   		System.out.println("son");
   	}
   }
   public class TestOverload {
       /*
       	1、编译期间进行静态分派:即确定是调用MyClassOne类中的method(Father f)方法。
       	2、运行期间进行动态绑定:确定执行的是MyClassOne类中method(Father f)方法
       	## 因为此时f,s,d编译时类型都是Father类型,因此method(Father f)是最合适的
       */
       @Test
   	public void test() {
   		MyClassOne my = new MyClassOne();
   		Father f = new Father();
   		Father s = new Son();
   		Father d = new Daughter();
   		my.method(f);//father
   		my.method(s);//father
   		my.method(d);//father
   	}
       /*
       	1、编译期间进行静态分派:即确定是分别调用MyClassTwo类中的method(Father f),method(Son s),method(Father f)方法。
       	2、运行期间进行动态绑定:即确定执行的是MyClass类中的method(Father f),method(Son s),method(Father f)方法
       	## 因为此时f,s,d编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配
       */
       @Test
   	public void test() {
   		MyClassTwo my = new MyClassTwo();
   		Father f = new Father();
   		Son s = new Son();
   		Daughter d = new Daughter();
   		my.method(f);//father
   		my.method(s);//Son
   		my.method(d);//father
   	}
   }

重载与重写示例1

    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{
    }
    /*
    	1、编译期间进行静态分派:即确定是分别调用MyClass类中的method(Father f),method(Son s),method(Father f)方法。
    	2、运行期间进行动态绑定:即确定执行的是MyClass类中的method(Father f),method(Son s),method(Father f)方法。
    	## my变量在编译时类型是MyClass类型,那么在MyClass类中,只有method(Father f),method(Son s)方法。,s,d变量编译时类型分别是Father、Son、Daughter,而Daughter只能与Father参数类型匹配。而在MySub类中并没有重写method(Father f)方法,所以仍然执行MyClass类中的method(Father f)方法
    */
    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);//father
    		my.method(s);//son
    		my.method(d);//father
    	}
   }

2、抽象类

2.1 定义及特点

使用abstract关键字修饰类,定义的就是抽象方法

  • abstract关键字可以修饰类和方法
  • abstract关键字修饰的方法特点
    • 只能在抽象类中
    • 没有方法体,不能执行
  • 抽象类的特点
    • 抽象类中可以有抽象方法,也可以有普通方法
    • 抽象类不允许创建对象

抽象父类的子类一定要重写抽象父类的所有抽象方法

2.2 抽象类需要注意的问题

public class TestOne {
    public static void main(String[] args) {
        //Person person = new Person();//报错,不允许创建对象
        //Person p = new Woman();//报错,不允许创建对象
        Person student = new Student();//正确
        Woman student1 = new Student();//正确
        Student student3 = new Student();//正确
        //创建的对象可以调用抽象类的普通方法
        //当继承体系中抽象类存在相同方法签名的重名方法时,采用就近原则
        //不管创建的是哪个子类的对象,都是从现在当前类寻找方法进行调用,然后寻找本类的子父类,然后寻找本类的父类的父类
        student.methodOne();
        student1.methodOne();
        student3.methodOne();
        //访问成员变量,声明的引用类型是什么类型,就访问此类型中的成员变量
        System.out.println(student.country);
        System.out.println(student1.country);
        System.out.println(student3.country);
    }
}
abstract class Person{
    //抽象类可以有自己的属性
    public String country="person";
    //可以有抽象方法
    abstract void sayHello();
    //可以有普通方法
    public void methodOne(){
        System.out.println("in abstract class Person methodOne");
    }
}
abstract class Woman extends Person{
    public String country="woman";
    abstract void sing();
    public void methodOne(){
        System.out.println("in abstract class Woman methodOne");
    }
}
class Student extends Woman{
    public String country="student";
    @Override
    void sayHello() {
    }
    @Override
    void sing() {
    }
    public void methodOne(){
        System.out.println("in abstract class Student methodOne");
    }
}

3、Object根父类

java.lang.Object是类层次结构的根类,即所有类的父类。

每个类都使用 Object 作为超类。

  • Object类型的变量与除Object以外的任意引用数据类型的对象都多态引用
  • 所有对象(包括数组)都实现这个类的方法。
  • 如果一个类没有特别指定父类,那么默认则继承自Object类

3.1 Object类中的API

API(Application Programming Interface),应用程序编程接口。

Java API是一本程序员的字典 ,是JDK中提供给我们使用的类的说明文档。

所以我们可以通过查询API的方式,来学习Java提供的类,并得知如何使用它们。

在API文档中是无法得知这些类具体是如何实现的,如果要查看具体实现代码,那么我们需要查看src源码

根据JDK源代码及Object类的API文档,Object类当中包含的方法有11个。今天我们主要学习其中的5个:

3.1.1 toString()

public String toString()

①默认情况下,toString()返回的是“对象的运行时类型 @ 对象的hashCode值的十六进制形式"

②通常是建议重写,如果在eclipse中,可以用Alt +Shift + S–>Generate toString()

③如果我们直接System.out.println(对象),默认会自动调用这个对象的toString()

因为Java的引用数据类型的变量中存储的实际上时对象的内存地址,但是Java对程序员隐藏内存地址信息,所以不能直接将内存地址显示出来,所以当你打印对象时,JVM帮你调用了对象的toString()。

例如自定义的Person类:

public class Person {  
    private String name;
    private int age;
    @Override
    public String toString() {
        return "Person{" + "name='" + name + '\'' + ", age=" + age + '}';
    }
    // 省略构造器与Getter Setter
}

3.1.2 getClass()

public final Class<?> getClass():获取对象的运行时类型

因为Java有多态现象,所以一个引用数据类型的变量的编译时类型与运行时类型可能不一致,因此如果需要查看这个变量实际指向的对象的类型,需要用getClass()方法

public static void main(String[] args) {
		Object obj = new String();
		System.out.println(obj.getClass());//运行时类型
	}

3.1.3 finalize()

protected void finalize():用于最终清理内存的方法

public class TestFinalize {
	public static void main(String[] args) {
		for (int i = 0; i < 10; i++) {
			MyData my = new MyData();
		}
		System.gc();//通知垃圾回收器来回收垃圾
		try {
			Thread.sleep(2000);//等待2秒再结束main,为了看效果
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
class MyData{
	@Override
	protected void finalize() throws Throwable {
		System.out.println("轻轻的我走了...");
	}
}

面试题:对finalize()的理解?

  • 当对象被GC确定为要被回收的垃圾,在回收之前由GC帮你调用这个方法,不是由程序员手动调用。
  • 这个方法与C语言的析构函数不同,C语言的析构函数被调用,那么对象一定被销毁,内存被回收,而finalize方法的调用不一定会销毁当前对象,因为可能在finalize()中出现了让当前对象“复活”的代码
  • 每一个对象的finalize方法只会被调用一次。
  • 子类可以选择重写,一般用于彻底释放一些资源对象,而且这些资源对象往往时通过C/C++等代码申请的资源内存

3.1.4 hashCode()

public int hashCode():返回每个对象的hash值。

hashCode 的常规协定:

①如果两个对象的hash值是不同的,那么这两个对象一定不相等;

②如果两个对象的hash值是相同的,那么这两个对象不一定相等。

主要用于后面当对象存储到哈希表等容器中时,为了提高存储和查询性能用的。

public static void main(String[] args) {
		System.out.println("Aa".hashCode());//2112
		System.out.println("BB".hashCode());//2112
	}

3.1.5 equals()

public boolean equals(Object obj):用于判断当前对象this与指定对象obj是否“相等”

①默认情况下,equals方法的实现等价于与“==”,比较的是对象的地址值

②我们可以选择重写,重写有些要求:

1.如果重写equals,那么一定要一起重写hashCode()方法,因为规定:

​ a:如果两个对象调用equals返回true,那么要求这两个对象的hashCode值一定是相等的;

​ b:如果两个对象的hashCode值不同的,那么要求这个两个对象调用equals方法一定是false;

​ c:如果两个对象的hashCode值相同的,那么这个两个对象调用equals可能是true,也可能是false

2.如果重写equals,那么一定要遵循如下几个原则:

​ a:自反性:x.equals(x)返回true

​ b:传递性:x.equals(y)为true, y.equals(z)为true,然后x.equals(z)也应该为true

​ c:一致性:只要参与equals比较的属性值没有修改,那么无论何时调用结果应该一致

​ d:对称性:x.equals(y)与y.equals(x)结果应该一样

​ e:非空对象与null的equals一定是false

到此这篇关于深入讲解Java中的多态和抽象类的文章就介绍到这了,更多相关Java多态和抽象类内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • SpringBoot通过整合Dubbo解决@Reference注解问题

    SpringBoot通过整合Dubbo解决@Reference注解问题

    这篇文章主要介绍了SpringBoot通过整合Dubbo解决@Reference注解问题,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-03-03
  • maven 插件 assembly 打tar.gz包的详细过程

    maven 插件 assembly 打tar.gz包的详细过程

    这篇文章主要介绍了maven插件assembly打tar.gz包的详细过程,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2023-06-06
  • Spring中的Filter过滤器详解

    Spring中的Filter过滤器详解

    这篇文章主要介绍了Spring中的Filter过滤器详解,Filter 程序是一个实现了特殊接口的 Java 类,与 Servlet 类似,也是由 Servlet 容器进行调用和执行的,需要的朋友可以参考下
    2023-08-08
  • Java 线程的优先级(setPriority)案例详解

    Java 线程的优先级(setPriority)案例详解

    这篇文章主要介绍了Java 线程的优先级(setPriority)案例详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08
  • Java用for循环Map详细解析

    Java用for循环Map详细解析

    本篇文章主要介绍了Java用for循环Map,需要的朋友可以过来参考下,希望对大家有所帮助
    2013-12-12
  • 一篇文章带你搞定JAVA注解

    一篇文章带你搞定JAVA注解

    这篇文章主要介绍了详解Java注解的实现与使用方法的相关资料,希望通过本文大家能够理解掌握Java注解的知识,需要的朋友可以参考下
    2021-07-07
  • Java面试题冲刺第十四天--PRC框架

    Java面试题冲刺第十四天--PRC框架

    这篇文章主要为大家分享了最有价值的三道关于PRC框架的面试题,涵盖内容全面,包括数据结构和算法相关的题目、经典面试编程题等,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • Spring关于@Scheduled限制的问题

    Spring关于@Scheduled限制的问题

    这篇文章主要介绍了Spring关于@Scheduled限制的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-10-10
  • SpringBoot整合RabbitMQ处理死信队列和延迟队列

    SpringBoot整合RabbitMQ处理死信队列和延迟队列

    这篇文章将通过示例为大家详细介绍SpringBoot整合RabbitMQ时如何处理死信队列和延迟队列,文中的示例代码讲解详细,需要的可以参考一下
    2022-05-05
  • Java使用Iterator迭代器遍历集合数据的方法小结

    Java使用Iterator迭代器遍历集合数据的方法小结

    这篇文章主要介绍了Java使用Iterator迭代器遍历集合数据的方法,结合实例形式分析了java迭代器进行集合数据遍历的常见操作技巧,需要的朋友可以参考下
    2019-11-11

最新评论