前言
- 本篇以Java初学者视角写下,难免有不足,或者术语不严谨之处。如有错误,欢迎评论区指正。
- 本篇说明多态相关的知识。
- 若本文无法解决您的问题,可以去最下方的参考文献出,找出想要的答案。
多态概念
多态(polymorphism):多种形态,具体是不同对象完成某个行为,它们的状态不同。
比如,同样是跑1000米,有些人气喘吁吁(我),还有一些大佬跑完气都不带喘的。
我认为多态是一种思想。而要理解多态得熟悉向上转型,和方法重写的知识。
有关笔者继承的博客:可以浅读了解一下
向上转型
字面意思:转型,类型转换;向上,即子类向超类转换。
向上转型属于自动转换类型可以不用强制类型转换运算符。
public class Main {
public static void main(String[] args) {
Dog dog =Dog.Init("旺财",2);//这里采用的是静态工厂方法,不熟悉的可以看下面的参考文献阅读一下。
Animal animal = dog;//这里Dog类转换成了父类Animal
}
}
不熟悉的可以先跳过,这里只是为了代码的完整性放这儿/
//Dog.java
class Dog extends Animal{
public static Dog Init(String name,int age){
Dog dog = new Dog(name,age);
return dog;
}
public Dog(String name,int age){
super(name,age);
}
public Dog(){
}
@Override
public void eat() {
System.out.println(this.name+" 正在吃狗粮.......");
}
}
//Animal.java
class Animal {
String name;
int age;
public Animal(String name,int age){
this.name=name;
this.age =age;
}
public Animal(){
}
public static void fun(Animal animal){
animal.eat();
}
public void eat(){
System.out.println(this.name + "正在吃法。");
}
}
在Java中,对象变量是多态的,它既可以引用自身的类型,还可以引用其子类的对象。
实现向上转型有三种方式
1.直接赋值
Animal animal = new Dog();//直接创建一个子类对象然后赋值给超类的对象变量
2.方法传参
//Main.java
public class Main {
public static void main(String[] args) {
Animal.fun1(new Dog());
}
}
//Animal.java类中有如下静态方法
public static void fun1(Animal animal){
}
这种通过调用超类的方法传子类的实参来达到向上转型的效果。
3.返回值
//Animal.java中有如下静态方法。
//函数内部实例化子类对象,然后由于返回值要与类型匹配,自动地发生向上转型。
public static Animal fun2(){
Dog dog = new Dog();
return dog;
}
动态绑定
这里说明动态绑定,但涉及到方法重写(先忽略这个问题)。
以下是三个类,两个子类一个超类,重点关注每个类中的eat方法.
//Dog.java
class Dog extends Animal{
public static Dog Init(String name,int age){
Dog dog = new Dog(name,age);
return dog;
}
public Dog(String name,int age){
super(name,age);
}
//关注eat方法
@Override
public void eat() {
System.out.println(this.name+" 正在吃狗粮.......");
}
}
//Animal.java
class Animal {
String name;
int age;
public Animal(String name,int age){
this.name=name;
this.age =age;
}
public static void fun(Animal animal){
animal.eat();
}
//关注eat方法
public void eat(){
System.out.println(this.name + "正在吃饭。");
}
}
//Bird.java
class Bird extends Animal{
public static Bird Init(String name,int age){
Bird bird = new Bird(name,age) ;
return bird;
}
public Bird(String name,int age){
super(name,age);
}
//关注eat方法
@Override
//eren jeger
public void eat() {
System.out.println(this.name+" 正在吃虫子.......");
}
}
🆗接下来来分析Main类了
public class Main {
public static void main(String[] args) {
Animal animal= Dog.Init("kunkun",1);
animal.eat();//这里的eat调用的是哪个类里面eat方法?
}
}
调用的是子类Dog,为什么呢?这里不是向上转型成Animal(超类)了吗?
嗯,这里就得介绍本标题的主角:动态绑定机制
- 动态绑定机制,简单来说,以父类为编译类型,创建子类对象的时候,绑定子类运行类型,当我们再调用方法的时候,能够有序的寻找方法,实现方法的有序调用。
- 解释:1.动态绑定机制在运行时调用方法绑定子类运行。
2.程序在编译时,确实调用的时父类的eat方法。当运行代码的时候,通过父类的引用,调用了父类和子类重写的方法,结果实际调用了子类的方法,此时称这种情况为动态绑定。
3.程序运行并且采用动态绑定调用方法时,JVM会调用父类所引用对象的实际类型的方法。
三种解释任选理解。下面来说明方法重写。
方法重写
在上面的例子中用到方法重写,我们这里详细说说。
//Dog类中(子类)
public void eat() {
System.out.println(this.name+" 正在吃狗粮.......");
}
//Bird类中(子类)
public void eat() {
System.out.println(this.name+" 正在吃虫子.......");
}
//Animal类中(超类)
public void eat(){
System.out.println(this.name + "正在吃饭。");
}
显而易见,Animal执行eat打印的是某某正在吃饭,而Dog打印的是某某吃狗粮。
超类的有些方法对子类并不适用,这里就是不够具体。每种动物的食物不同,这里就可以通过方法重写来针对特定的类写对应的eat方法来覆盖原先超类的方法。
没错,这就是方法重写(又称覆盖方法)。
若子类和超类的方法满足以下:
1.方法名相同
2.参数相同(参数类型,参数个数,参数序列相同)
3.返回类型相同
则构成方法重写。
方法重写的注意事项:
1.若方法前面有private static final 修饰,或者其本身是构造器(构造方法),编译器本身可以确定应该调用哪些方法。这些方法不能重写。
2.方法重写时,子类的访问权限一定大于等于超类的访问权限。若超类是public,则子类也必须是public类,不能protected等等。
3.子类和超类的覆盖方法返回类型可以不同,但必须构成父子关系。
`
//Animal类一部分
public Animal eat(){
System.out.println(this.name + "正在吃饭。");
return null;
}
//Dog类一部分
public Dog eat() {
System.out.println(this.name+" 正在吃狗粮.......");
return null;
}
}
重写与重载区别
重写与重载都是一种多态。
方法重载是类之间的多态。方法重写是子类与超类的多态。
方法重载:
方法之间同名,但参数列表必须修改。
返回类型,访问修饰符随意。方法重写:
1.方法名必须相同
2.参数相同(参数类型,参数个数,参数序列相同)
3.返回类型相同(构成父子关系可以不同)
4.访问修饰符必须满足子类权限大于等于超类。
下面是补充部分:
补充
向下转型
向下转型有风险。向下转型是超类强制转换成子类。这里必须借助强制转换运算符。
系统来说:将一个超类类型的引用变量强制转换成子类的过程叫做向下转型。
但不是所有对象都能向下转型,只要当这个对象原本就是子类向上转型过来时才能向下转型。
???啥意思呢?
举例
public class Main {
public static void main(String[] args) {
Animal animal= new Animal("kunkun",1);//直接new一个超类对象向下转型
Dog dog =(Dog)animal;
dog.eat();//结果如何呢?打印Dog的eat还是Animal的eat呢?
}
}
结果是都不打印,直接报错。
//本来是狗,还原成狗,可以。
public class Main {
public static void main(String[] args) {
Animal animal= Dog.Init("kunkun",2);
Dog dog =(Dog)animal;
dog.eat();
}
}
```java
//本来是狗,还原成鸟不合理。你家狗会飞???
public class Main {
public static void main(String[] args) {
Animal animal= Dog.Init("kunkun",2);
Bird bird =(Bird)animal;
bird.fly();
}
}
> 总结:
> 向下转型唯一正确的使用用法:
> 1.先向上转型,让父类引用子类对象。
> 2.向下转型类型要与最初子类类型相同。别从一个子类转换成另一个子类了。报错警告!
## instanceof关键字
**instanceof 是 Java 的保留关键字。它的作用是测试它左边的对象是否是它右边的类的实例,返回 boolean 的数据类型。**
[instanceof关键字详解](http://t.csdnimg.cn/IIZbD)
```java
public class Main {
public static void main(String[] args) {
Animal animal= Bird.Init("kunkun",2);
Bird bird =(Bird)animal;
if( bird instanceof Bird)
{
bird.fly();
}else{
System.out.println(false);
}
}
}
静态绑定
其实前面提过:若方法前面有private static final 修饰,构造器(构造方法),编译器本身可以确定在那个类调用这些方法。
像上述在这种程序执行前就已经绑定了方法在那个类中调用的方法称为静态绑定。
避免在构造方法中使用重写
//Test.java
class B {
public B() {
// do nothing
func();
}
public void func() {
System.out.println("B.func()");
}
}
class D extends B {
private int num = 1;
public void func() {
System.out.println("D.func() " + num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
看main方法,首先实例化D类对象,先进入D中构造方法,构造方法为默认构造方法,由于继承关系,子类构造还有里面还要调用父类构造方法。父类构造方法里的调用func()即子类和父类方法重写,由于动态绑定,会调用子类的func(),而此时子类的num还没初始化为1,默认为0.所以最终打印结果为 D.func() 0
总之,构造器中最后不要调用实例化的方法,调用静态绑定的方法比如final,private修饰的方法。因为,若实例方法被子类重写,那么就动态绑定了,会调用子类的方法。
参考文献
- Instance关键字详解
- 静态绑定与动态绑定
- 重载与重写—强烈建议看此篇
- 向上转型与向下转型
- 类加载顺序
愿此行,终抵群星。