Java——多态(Polymorphism)

news2024/9/9 1:37:33

一、多态

1、什么是多态

多态(Polymorphism)是面向对象编程的三大核心特性之一(另外两个是封装和继承)。多态性允许一个接口或基类的不同实现或子类以统一的方式处理。

二、方法多态

方法的多态性主要通过方法重载(Overload)和方法重写(Override)来实现。

1、方法重载实现方法多态

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Cal cal = new Cal();
		System.out.println(cal.sum(1, 2));
		System.out.println(cal.sum(1.1, 2.2));
	}
}

class Cal {
	public int sum(int n1, int n2) {
		return n1 + n2;
	}

	public double sum(double n1, double n2) {
		return n1 + n2;
	}
}

运行结果:

这个例子中我们使用了方法重载来实现 sum 这个方法的多态,对方法 sum 传入不同的参数,可以看调用不同的 sum 函数,这就是多态的一种体现。

2、方法重写实现方法多态

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Animal animal = new Animal();
		animal.makeSound();

		Dog dog = new Dog();
		dog.makeSound();
	}
}

class Animal {
	public void makeSound() {
		System.out.println("Animal make sound...");
	}
}

class Dog extends Animal {
	public void makeSound() {
		System.out.println("woof...");
	}
}

运行结果:

这一个例子就是通过方法重写实现多态,当我们对不同对象使用 makeSound 方法时,调用的 makeSound 方法也是不同的,这里方法重写也能体现多态。

三、对象多态

1、编译时类型和运行时类型

对象的多态主要体现在是编译时类型运行时类型。编译时类型是声明对象引用变量时使用的类型,也就是引用变量的类型,运行时类型是实际创建的对象的类型,也就是实际指向的对象的类型。

例如下面的一段代码:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Animal animal = new Dog();
	}
}

class Animal {
	
}

class Dog extends Animal {
	
}

这段代码中 Dog 为 Animal 类的子类,然后我们声明的一个 Animal 类的对象引用变量 animal ,然后创建一个 Dog 类的对象,将这个 Dog 类的对象的引用赋值给这个 Animal 类的对象引用变量 animal 。

这里的 animal 的编译时类型就是 Animal ,运行时类型就是 Dog 。

从这个例子中,我们可以总结出一些结论:

  • 编译时类型在声明对象引用变量时就确定了,不能改变;而运行类型是可以改变的。
  • 编译时类型看声明时的 “=” 左边,运行时类型看 “=” 右边。

2、向上转型(Upcasting)

上面的例子中我们可以看到我们使用一个 Animal 类的对象引用变量存储了一个 Dog 类的对象的引用,也就是使用一个父类引用变量指向了一个子类对象,实际上这个就是向上转型的本质。

所以说,向上转型(Upcasting)是指将子类对象的引用转换为父类类型的引用。在Java中,这是一个隐式的类型转换,因为子类对象本身就具备父类的所有特性和行为。因此,我们可以直接将子类对象赋值给父类引用,而不需要显式的类型转换。

向上转型的语法:

Parent parentRefe = new Child();

在这个例子中,Child 是 Parent 的子类。通过向上转型,Child 对象被赋值给 Parent 类型的引用 parent

向上转型的特点:

  1. 编译类型(或者说引用变量的类型)为父类:向上转型后的编译类型是父类,因此只能访问父类中定义的方法和属性,不能访问子类中的特有的方法和属性。这是因为在编译阶段能调用哪些成员和属性是由编译类型决定的,对于向上转型的编译类型一般是父类类型,父类中没有子类特有的属性和方法,所以父类引用变量无法访问子类特有的属性和方法,如果使用这个向上转型的引用变量访问了子类的特有方法,则在编译阶段就会报错。
  2. 运行类型(或者说指向的对象的类型)为子类:虽然编译类型是父类,但运行类型是子类。这意味着,如果子类重写了父类的方法,调用的将是子类的方法。这是因为子类重写父类的方法,父类中也有同签名的方法,所以使用这个向上转型的引用变量在编译阶段不会报错。在运行阶段,调用的方法是哪个方法,实际上是由运行时类型决定的,向上转型的运行时类型一般是子类类型,所以这时调用方法时,是从子类中依次向上找的(先从子类中找是否有这个方法,如果没有,在向父类找,一次向上找,直到 Object 类,也就是之前我们提到的方法调用的规则),所以子类中如果有重写父类的方法,会调用子类的方法。

向上转型补充:

在向上转型后,如果子类中有与父类相同的属性,如果使用向上转型后的引用变量访问这个属性,访问的实际上是编译类型(或者说引用变量的类型)对应的那个属性,也就是父类对应的属性。

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Animal animal = new Dog();// 向上转型
		System.out.println(animal.num);
	}
}

class Animal {
	public int num = 100;
}

class Dog extends Animal {
	public int num = 200;
}

运行结果:

所以说属性是不存在重写这一说法的,这里与子类中重写父类的方法然后调用这个方法的情况不一样。这个问题我们将在下面的动态绑定和静态绑定里讲解。

3、向下转型(Downcasting)

上面我们提到向上转型后不能调用子类的特有属性和方法,那如果我们需要访问子类中特有的方法和属性呢,就必须使用向下转型(Downcasting)。

向下转型的语法:

ChildClass childRefe = (ChildClass)parentRefe;

向下转型的细节:

向下转型是将父类引用转换为子类类型的引用。向下转型需要显式转换(也就是强制类型转换),并且需要确保实际对象类型是目标子类类型,否则会抛出 ClassCastException 异常。

也就是说必须保证这里的 parentRefe 引用变量指向的是 ChildClass 类的对象(这里就表明了前面进行了向上转型,所以一般情况下必须先向上转型然后才能向下转型),不然就会抛出异常。而且这里强制转换的只是这个引用变量的类型,没有转换对象的类型,对象一直都是子类类型。在向下转型之后就可以完成我们上面提到的目的了——访问子类特有的属性和方法。

实际上,这里的向下转型使引用变量的类型变成了子类类型,或者说把编译类型变成了子类类型,所以就可以调用子类的特有方法了。这时的运行类型(或者说是指向的对象的类型)依旧是子类类型,这个子类类型与引用变量的子类类型是一致的。

例子:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Animal animal = new Dog();// 向上转型
		animal.cry();
		// 这里的animal引用变量的编译类型是父类Animal类,
		// 所以不能调用子类Dog类中特有的shakeTail方法,
		// 但是可以调用父类Animal中也有的cry方法,
		// 因为这里的运行类型是子类Dog类,也就是这个animal引用变量指向的是Dog类的对象
		// 这里子类中重写了这个cry方法,所以实际调用的是子类的cry方法

		Dog dog = (Dog)animal;// 向下转型
		dog.shakeTail();
		// 这里向下转型后就可以调用子类的特有方法了,
		// 因为引用变量(或者说编译类型)的类型是子类类型
	}
}

class Animal {
	public void cry() {
		System.out.println("动物叫~");
	}
}

class Dog extends Animal {
	public void cry() {
		System.out.println("汪汪叫~");
	}
	public void shakeTail() {
		System.out.println("摇尾巴~");
	}
}

运行结果:

4、动态绑定和静态绑定

1)静态绑定(Static Binding)

静态绑定,也称为早期绑定(Early Binding),是在编译时决定的绑定。这意味着在编译阶段,编译器已经确定了方法调用的目标对象和要调用的方法。这种绑定通常适用于静态方法、私有方法、final修饰的方法和所有的字段(属性)访问,因为它们在编译时是确定的,不涉及对象的动态类型。

这里我们就可以对上面所说的属性不存在重写这种说法进行解释了,属性的绑定在编译时进行,称为静态绑定(Static Binding)。这意味着编译器根据引用类型确定访问哪个类的属性,而不管实际对象的类型是什么。

 示例:

class Parent {
    int value = 10;
}

class Child extends Parent {
    int value = 20;
}

public class TestFields {
    public static void main(String[] args) {
        Parent parent = new Child();
        System.out.println(parent.value); // 输出 10
    }
}

在这个示例中,parent.value 访问的是 Parent 类的 value 属性,而不是 Child 类的 value 属性。因为在编译时,parent 的类型是 Parent,所以编译器选择了 Parent 类的 value 属性。

2)动态绑定(Dynamic Binding)

动态绑定,也称为晚期绑定(Late Binding),是在运行时决定的绑定。这意味着在运行阶段,根据实际对象的类型来决定调用的方法。这种绑定方法适用于非静态、非私有、非final方法。动态绑定是实现多态性(Polymorphism)的关键机制。

方法调用在运行时确定,称为动态绑定(Dynamic Binding)。这意味着方法调用根据实际对象的类型来确定执行哪个类的方法。

class Parent {
    void display() {
        System.out.println("Parent display");
    }
}

class Child extends Parent {
    @Override
    void display() {
        System.out.println("Child display");
    }
}

public class TestMethods {
    public static void main(String[] args) {
        Parent parent = new Child();
        parent.display(); // 输出 "Child display"
    }
}

在这个示例中,调用 parent.display() 时,执行的是 Child 类的 display 方法。因为在运行时,parent 实际上指向的是 Child 类型的对象,所以调用的是 Child 类的实现。

3)动态绑定机制

当调用方法(非静态、非私有、非final方法)时,该方法会与实际对象的类型绑定。也就是说在调用某个方法时,具体调用的是哪个方法是由现在调用这个方法的实际对象的类型决定的。对于属性,是没有动态绑定的,在哪里声明的就使用哪里的,也就是说属性的值是由声明它的类决定,而不是由实际对象的类型决定。

例子:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Base b = new Sub();// 向上转型
		System.out.println(b.foo());// 输出30
		// 这里的b指向的实际对象的类型是Sub类,
		// 所以调用foo方法会在Sub类中找,
		// 但是Sub类中没有foo方法,所以就向父类Base类中找,
		// 父类中有foo方法,调用父类中的foo方法,父类的foo方法中调用了getNum方法,
		// 我们发现子类中对getNum方法进行了重写,这时实际对象的类型是子类Sub类,
		// 所以getNum这个方法实际上是与这时的实际对象类型即子类Sub类绑定的,这里就体现了动态绑定,
		// 所以调用的是子类Sub类的getNum方法,getNum访问的是属性num,对于属性是没有动态绑定的,
		// 也就是说,属性的值取决于声明该变量的类,而不是实际对象的类型,也就是说哪里声明就使用哪里的,
		// 所以会使用子类中的num属性,所以getNum方法返回的值为子类的num值,即20,
		// 所以这里调用的foo函数返回的值为30

		System.out.println(b.bar());// 输出20
		// 这里b指向的实际对象的类型为Sub类,调用bar方法会先从Sub类中寻找,
		// 然后在Sub类中没找到,再向上面的父类Base类中寻找,然后再父类Base类中找到,
		// 然后调用父类中的bar方法,bar方法访问了num属性,上面我们提到了,
		// 属性是没有动态绑定的,在哪里声明就使用哪里的,所以直接使用父类中的num属性,
		// 所以bar方法会返回20
	}
}

class Base {
	public int num = 10;

	public int foo() {
		return getNum() + 10;
	}

	public int getNum() {
		return num;
	}

	public int bar() {
		return num + 10;
	}
}

class Sub extends Base {
	public int num = 20;

	public int getNum() {
		return num;
	}
}

运行结果:

5、补充

在之前讲解的运算符知识中,我们提到 instanceof 这个运算符,到这里我们可以说这个运算符判断的其实是运行类型(或者说实际对象的类型)是否为指定类型的同类或者子类。

我们可以通过一个例子来验证:

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Animal animal = new Dog();// 向上转型
		System.out.println(animal instanceof Animal);
		System.out.println(animal instanceof Dog);
	}
}

class Animal {

}

class Dog extends Animal {

}

这里的运行结果为:

这里显然判断的是实际对象的类型(运行类型),因为这里的实际对象的类型是 Dog 类,Dog 类是 Animal 的子类,所以第一个为 true,Dog 类是 Dog 类,所以第二个为 true。

我们假设判断的是引用变量的类型(编译类型),引用类型为 Animal 类,Animal 类是 Animal 类,第一个为 true,但是第二个 Animal 类是 Dog 类的父类,而不是其子类,所以第二个为 false,与实际运行结果不同,假设不成立,所以判断的不是引用变量的类型。

所以说这个运算符 instanceof 判断的是实际对象的类型是否为指定类型的同类或子类。

6、实例

1)多态数组

将不同类型的对象(PersonStudentTeacher)存储在同一个数组中,可以利用动态绑定(也就是多态)来调用这些对象的相应方法。

package com.pack1;

public class Test {
	public static void main(String[] args) {
		Person[] arr = new Person[3];
		arr[0] = new Person("Alice", 18);
		arr[1] = new Student("Bob", 19, 79.5);
		arr[2] = new Teacher("Kim", 28, 10000.0);
		// 向上转型
		
		for (int i = 0; i < 3; i++) {
			arr[i].say();// 调用say方法时,会根据实际对象的类型来判断调用的方法是哪一个,
			// 如果实际对象是父类,则调用父类的say方法,
			// 如果实际对象是子类,则会调用子类重写父类的say方法
		}

		for (int i = 0; i < 3; i++) { // 向下转型
			// 这里使用instanceof判断实际对象的类,如果实际对象是特定子类,
			// 则向下转型,然后就可以调用子类特有的方法了
			if(arr[i] instanceof Student) {
				Student student = (Student)arr[i];
				student.study();
			} else if (arr[i] instanceof Teacher) {
				Teacher teacher = (Teacher)arr[i];
				teacher.teach();
			}
		}
	}
}

class Person {
	private String name;
	private int age;

	public Person(String name, int age) {
		this.name = name;
		this.age = age;
	}

	public String getName() {
		return name;
	}

	public int getAge() {
		return age;
	}

	public void setAge(int age) {
		this.age = age;
	}

	public void setName(String name) {
		this.name = name;
	}

	public void say() {
		System.out.println(name + "\t" + age + "\t");
	}
}

class Student extends Person {
	private double score;
	public Student(String name, int age, double score) {
		super(name, age);
		this.score = score;
	}

	@Override
	public void say() {
		System.out.print("student ");
		System.out.print(getName() + "\t" + getAge() + "\t");
		System.out.println("score " + score);
	}

	public void study() {
		System.out.print("student ");
		System.out.print(getName() + "\t" + getAge() + "\t");
		System.out.println("study~");
	}
}

class Teacher extends Person {
	private double salary;
	public Teacher(String name, int age, double salary) {
		super(name, age);
		this.salary = salary;
	}

	@Override
	public void say() {
		System.out.print("teacher ");
		System.out.print(getName() + "\t" + getAge() + "\t");
		System.out.println("salary " + salary);
	}

	public void teach() {
		System.out.print("teacher ");
		System.out.print(getName() + "\t" + getAge() + "\t");
		System.out.println("teach~");
	}
}

2)多态参数

package com.pack1;

public class Test {
	public void testSound(Animal animal) {
		animal.makeSound();
	}

	public static void main(String[] args) {
		Test test = new Test();

		Animal dog = new Dog();// 向上转型
		Animal cat = new Cat();// 向上转型

		test.testSound(dog);
		test.testSound(cat);
	}
}

class Animal {
	public void makeSound() {
		System.out.println("Animal sound~");
	}
}

class Dog extends Animal {
	@Override
	public void makeSound() {
		System.out.println("Woof~");
	}
}

class Cat extends Animal {
	@Override
	public void makeSound() {
		System.out.println("Meow~");
	}
}

运行结果:

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1959113.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Git原理与用法系统总结

目录 Reference前言版本控制系统Git的诞生配置Git配置用户名和邮件配置颜色配置.gitignore文件 Git的基础用法初始化仓库克隆现有的仓库添加暂存文件提交变动到仓库比较变动查看日志Git回退Git重置暂存区 Git版本管理重新提交取消暂存撤销对文件的修改 Git分支Git分支的优势Git…

2024年中小企业为何更需要找百度竞价托管代运营公司

企业间的竞争日益激烈&#xff0c;网络营销已成为企业获取市场份额、提升品牌知名度的关键途径。而在众多网络营销手段中&#xff0c;百度竞价推广因其高效、精准的特点&#xff0c;成为众多企业的首选。然而&#xff0c;随着市场竞争的加剧和百度竞价规则的不断调整&#xff0…

值得细读的8个视觉大模型生成式预训练方法

大语言模型的进展催生出了ChatGPT这样的应用&#xff0c;让大家对“第四次工业革命”和“AGI”的来临有了一些期待&#xff0c;也作为部分原因共同造就了美股2023年的繁荣。LLM和视觉的结合也越来越多&#xff1a;比如把LLM作为一种通用的接口&#xff0c;把视觉特征序列作为文…

年化27.9%,最大回撤-13.6%的可转债因子策略,结合机器学习特征筛选(附python代码)

原创文章第603篇&#xff0c;专注“AI量化投资、世界运行的规律、个人成长与财富自由"。 我们重新更新了可转债的全量数据&#xff0c;包含全量已经退市的转债。 ——这是与股票市场不一样的地方&#xff0c;股票退市相对少&#xff0c;而转债本身就有退出周期。 因此&…

喝奶 (全脂 抵脂肪 脱脂 )

鲜牛奶就是全脂的. 婴儿配方奶粉, 脂肪含量就高 全脂牛奶通常口感更浓郁&#xff0c;适合许多人的口味偏好, 全脂牛奶含有较高的脂肪含量&#xff0c;这有助于提供能量和饱腹感,从而减少总体热量的摄入, 有研究指出&#xff0c;喝全脂牛奶的儿童超重或肥胖的风险可能比喝低脂…

80端口被system占用 ,system进程是4!!!亲测-----解决

最近需要使用nginx&#xff0c;发现80端口北占用 正常情况下&#xff0c;查看那个进程占用&#xff0c;然后找到对应的程序&#xff0c;关闭对应的就可了。 使用 netstat 命令&#xff1a; 打开命令提示符&#xff08;以管理员身份&#xff09;。输入命令 netstat -ano | fi…

昇思25天学习打卡营第18天|ResNet50 迁移学习实战:从数据准备到模型构建

目录 环境配置 加载数据集 数据集可视化 构建Resnet50网络 固定特征进行训练 训练和评估 可视化模型预测 环境配置 MindSpore 库的版本管理和数据集的下载操作。首先&#xff0c;它卸载了已安装的 MindSpore 版本&#xff0c;并重新安装指定版本&#xff08;2.3.0rc1&…

帆软BI 模仿一个可视化护理软件大屏 (三百六十行 行行fine BI)

帆软BI 模仿一个可视化护理软件大屏 &#xff08;三百六十行 行行fine BI&#xff09; 文章目录 帆软BI 模仿一个可视化护理软件大屏 &#xff08;三百六十行 行行fine BI&#xff09;前言一、怎么做&#xff1f;二、导入数据三、编辑数据制作联合饼图四、编辑数据风险管理五、…

微短剧出海CPS分销推广影视平台系统搭建思维逻辑介绍

随着国内短剧市场的蓬勃发展&#xff0c;其独特的魅力与影响力已跨越国界&#xff0c;成为海外观众的新宠。这一趋势不仅推动了短剧内容的全球化传播&#xff0c;也为海外市场的CPS&#xff08;按销售分润&#xff09;分销模式提供了广阔舞台。连接内容创作者、平台运营者、系统…

数据结构与算法-关于堆的基本排序介绍

&#x1f49d;&#x1f49d;&#x1f49d;首先&#xff0c;欢迎各位来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里不仅可以有所收获&#xff0c;同时也能感受到一份轻松欢乐的氛围&#xff0c;祝你生活愉快&#xff01; 文章目录 引言一、堆排序…

职场中的六条建议

在职场的征途中&#xff0c;我们每个人都是独一无二的行者&#xff0c;面对挑战与机遇并存的每一天。我们在职场中工作要弄清楚工作的本质&#xff0c;一定要牢记几点&#xff1a; 工作的本质与态度 我们工作的目的就是为了挣钱&#xff0c;我们不是来义务劳动也不是来参加快乐…

【Redis 初阶】Redis 常见数据类型(预备知识、String、哈希、List)

Redis 提供了 5 种数据结构&#xff0c;理解每种数据结构的特点对于 Redis 开发运维非常重要&#xff0c;同时掌握每种数据结构的常见命令&#xff0c;会在使用 Redis 的时候做到游刃有余。 一、预备知识 官方文档&#xff1a;Commands | Docs (redis.io) 1、最核心的两个命令…

npm提示 certificate has expired 证书已过期 已解决

在用npm新建项目时&#xff0c;突然发现报错提示 : certificate has expired 证书已过期 了解一下&#xff0c;在网络通信中&#xff0c;HTTPS 是一种通过 SSL/TLS 加密的安全 HTTP 通信协议。证书在 HTTPS 中扮演着至关重要的角色&#xff0c;用于验证服务器身份并加密数据传输…

python——joblib进行缓存记忆化-对计算结果缓存

问题场景 在前端多选框需要选取多个数据进行后端计算。 传入后端是多个数据包的对应路径。 这些数据包需要按一定顺序运行&#xff0c;通过一个Bag(path).get_start_time() 可以获得一个float时间值进行排序&#xff0c;但由于数据包的特性&#xff0c;这一操作很占用性能和时…

动手学深度学习V2每日笔记(模型初始化和激活函数)

本文主要参考沐神的视频教程 https://www.bilibili.com/video/BV1u64y1i75ap2&vd_sourcec7bfc6ce0ea0cbe43aa288ba2713e56d 文档教程 https://zh-v2.d2l.ai/ 本文的主要内容对沐神提供的代码中个人不太理解的内容进行笔记记录&#xff0c;内容不会特别严谨仅供参考。 1. 模…

Linux安装与配置

下载VMware 首先我们需要下载一个叫VMware的软件&#xff1a; 进入官方下载&#xff0c;地址&#xff1a;https://www.vmware.com/cn/products/workstation-pro/workstation-pro-evaluation.html选择与自己电脑版本适配的VMware版本【 输入许可证密钥 MC60H-DWHD5-H80U9-6V85…

硬盘分区读不出来的解决之道:从自救到专业恢复

在日常的计算机使用过程中&#xff0c;硬盘分区读不出来的问题常常令人头疼不已。这一问题不仅阻碍了用户对数据的正常访问&#xff0c;还可能预示着数据安全的潜在威胁。硬盘分区读不出来&#xff0c;通常是由于分区表损坏、文件系统错误、物理扇区损坏、驱动程序冲突或硬件连…

流量卡对比?看看哪个运营商的流量卡更适合你?

你用过流量卡吗&#xff1f;在这个一机双卡的时代&#xff0c;大部分的朋友都是人手两张电话卡&#xff0c;甚至更多&#xff0c;其中还包括一张大流量卡&#xff0c;那么&#xff0c;你会选择流量卡吗&#xff1f; ​ 为了冲量和拉新用户&#xff0c;三大运营商都在线上推出一…

优化极限学习机,实现回归预测,三种算法对比,MATLAB代码免费获取

本期将原始蜣螂算法、减法优化器算法、鲸鱼优化算法进行应用。对极限学习机进行优化实现股票回归预测&#xff0c;三种算法相互对比。 股票预测案例 股票数据特征有&#xff1a;开盘价&#xff0c;盘中最高价&#xff0c;盘中最低价&#xff0c;收盘价等。预测值为股票价格。股…

大白话讲清楚GPT嵌入(Embedding)的基本原理

嵌入&#xff08;Embedding&#xff09;是机器学习中的一个基本概念&#xff0c;尤其是在自然语言处理 (NLP) 领域&#xff0c;但它们也广泛应用于其他领域。通常&#xff0c;嵌入是一种将离散的分类数据转换为连续向量的方法&#xff0c;通常在高维空间中&#xff0c;将复杂、…