文章目录
多态的概念多态的实现条件向上转型动态绑定静态绑定向下转型Object类
给个关注叭
个人主页
JavaSE专栏
前言:本篇文章主要整理了多态的概念、实现条件、多态性的体现、向上转型、向下转型、动态绑定和静态绑定以及Object类中的equals、toString、hashCode方法。
多态的概念
通俗来说,就是多种形态;具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。比如动物之间存在多态,不同动物持不同的食物,狗吃狗粮,猫吃猫粮,当不同的动物执行吃的动作时,就会产生不同的结果,即狗吃狗粮,猫吃猫粮。结合实际代码更容易理解,实际代码示例如下:
class Animal { public String name; public int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(name + "正在吃饭..."); }}class Dog extends Animal { public Dog (String name,int age) { super(name,age); } public void eat () { System.out.println(name + "正在吃狗粮..."); }}class Cat extends Animal { public Cat (String name,int age) { super(name,age); } public void eat () { System.out.println(name + "正在吃猫粮..."); }}public class Test { public static void main(String[] args) { Animal dog = new Dog("旺财",2); Animal cat = new Cat("小花",1); func(dog); func(cat); } public static void func(Animal a) { a.eat(); }}
运行结果:
旺财正在吃狗粮…
小花正在吃猫粮…
根据结果说明,当一个引用 引用了不同的对象 调用同一个方法,所表现的形式却不一样,这就是多态。
多态的实现条件
必须在继承体系下父类引用 引用了 子类的对象(向上转型)子类必须重写父类中的方法通过父类的引用 访问 子类重写的 这个方法【关于重写的解释,请移步 重写和重载的区别 】
要进一步理解多态性的体现,需要认识向上转型和动态绑定。如下
向上转型
向上转型就是父类引用 引用了 子类的对象。
向上转型的三种形式:
Animal animal = New Dog("旺财", 2);Animal animal = New Cat("小花", 1);
作为方法的参数进行传递,代码示例如下: public static void main(String[] args) { Animal dog = new Dog("旺财",2); Animal cat = new Cat("小花",1); func(dog); func(cat); } public static void func(Animal a) { a.eat(); }
作为返回值进行接收 public Animal func() { Dog dog = new Dog("旺财",2); return dog;}
向上转型的优点:
能够降低代码的 “圈复杂度”, 避免使用大量的 if - else什么叫 “圈复杂度” ?
圈复杂度是一种描述一段代码复杂程度的方式. 一段代码如果平铺直叙, 那么就比较简单容易理解. 而如果有很多的条件分支或者循环语句, 就认为理解起来更复杂.
因此我们可以简单粗暴的计算一段代码中条件语句和循环语句出现的个数, 这个个数就称为 “圈复杂度”.
如果一个方法的圈复杂度太高,就需要考虑重构了。
对于类的调用者来说, 只要创建一个新类的实例就可以了, 改动成本很低
向上转型的缺点:
不能通过父类的引用访问子类的成员,只能访问父类自己特有的成员
动态绑定
动态绑定也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
动态绑定的过程体现:
以上三个阶段过程 就是动态绑定(运行时绑定)
代码示例如下:
class Animal { public String name; public int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(name + "正在吃饭..."); }}class Dog extends Animal { public Dog (String name,int age) { super(name,age); } // 2.子类重写父类的eat方法 public void eat () { System.out.println(name + "正在吃狗粮..."); }}public class Test { public static void main(String[] args) { // 1.发生向上转型 Animal animal = new Dog("旺财",2); animal.eat();//编译时是父类的eat,运行时调用子类eat,体现动态绑定 }
运行结果:
旺财正在吃狗粮…
在发生向上转型的情况下,如果通过父类的引用去访问子类的方法,是会报错的。即发生向上转型,父类引用只能访问自己的成员,不能访问子类的成员。这也是向上转型的缺点。但是在发生向上转型 并且 子类重写了父类的方法 这两种前提下,通过父类引用 是可以访问 这个被子类重写的方法的。在最新生成的编译好的字节码文件里,此时编译的确实是父类的方法。但是在运行时被动态地 绑定到了子类这个被重写的方法上。所以动态绑定也叫做运行时绑定。
静态绑定
静态绑定也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用那个方法。典型代表方法重载。
编译时确定调用哪个方法 在运行时就会调用哪个方法,不会改变。这是静态绑定。
向下转型
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,但有时候可能需要调用子类特有的方法,此时:将父类引用再还原为子类对象即可,即向下转换。代码示例如下:
class Animal { public String name; public int age; public Animal(String name, int age) { this.name = name; this.age = age; } public void eat() { System.out.println(name + "正在吃饭..."); }}class Dog extends Animal { public Dog (String name,int age) { super(name,age); } public void eat () { System.out.println(name + "正在吃狗粮..."); } public void bark() { System.out.println(name + "正在汪汪叫..."); }class Bird extends Animal { public Bird (String name,int age) { super(name,age); } public void eat () { System.out.println(name + "正在吃鸟粮..."); } public void fly() { System.out.println(name + "正在飞..."); }}public class Test { public static void main(String[] args) { Animal animal1 = new Dog("旺财",2); func(animal1); Animal animal2 = new Bird("小花",1); func(animal2); //animal1.bark(); 编译错误,animal1不能访问子类特有的方法 //向下转型 Dog dog = (Dog)animal1; dog.bark();//编译成功并成功运行 } public static void func(Animal a) { a.eat(); }}
但是,向下转型 不安全,代码示例如下:
Animal animal1 = new Dog("旺财",2); func(animal1); Animal animal2 = new Bird("小花",1); func(animal2); //animal1.bark(); 编译错误,animal1不能访问子类特有的方法 //向下转型 Dog dog = (Dog)animal1; dog.bark();//编译成功并成功运行 Bird bird = (Bird)animal1; //bird.fly(); 编译不成功
此时,会报以下错误
animal1向下转型为Dog后,再调用子类Dog特有的bark方法,能够成功编译并运行。而animal1再次向下转型为Bird后,再次调用子类Bird中特有的fly方法时,虽然编译能通过但运行时会报错,这是因为向下转型时,animal1引用指向的正是转换之后的Dog类的对象,因此dog.bark()
能成功运行;而将animal1向下转型为Bird类时,因为此时animal1这个引用指向的是Dog类的对象,并不是发生转型后的Bird类的对象,所以运行时会报错。
为了避免这种不安全性,我们使用instanceof
关键字,代码示例如下:
Animal animal1 = new Dog("旺财",2); Animal animal2 = new Bird("小花",1); //向下转型 if(animal1 instanceof Dog) { Dog dog = (Dog)animal1; dog.bark();//编译成功并成功运行 }else { System.out.println("animal没有引用Dog实例"); } if(animal1 instanceof Bird) { Bird bird = (Bird)animal1; bird.fly(); }else { System.out.println("animal没有引用Bird实例"); }
Object类
Object类是所有类的父类,所以任何类都可以向Object类进行向上转型,即所有类的对象都可以使用Object的引用进行接收;Object类可以作为方法的参数接收任何类的对象,Object类是参数的最高统一类型。同时Object类中也存在一些已经定义好的方法,如下:
所以说,Java中的每一个类只要根据需求重写了Object中的方法就可以使用我们重写的方法,这个过程涉及了刚刚所整理的动态绑定的知识,也就是我们任何一个子类和Object类都构成了继承关系,任何的类就会作为子类重写父类Object类中的方法,进而形成了动态绑定(运行时绑定)。
【这里穿插一个我当时学习时的疑惑点】就是动态绑定的前提不一定会存在向上转型,也就是说不存在向上转型,只要在继承关系上,子类重写了父类的方法,那么通过父类引用访问这个重写的方法时,也一定会调用子类中这个被重写的方法。
【这里再提一下关于继承中访问成员的知识点】不论是通过子类引用访问成员变量还是成员方法,都会遵循以下准则:
equals方法
通过Object类的学习,我们在自定义的类中一般都要根据需要重写Object中的方法(当然是在用到这个方法前提下),比如说我们自定义了一个Person类,又实例化了两个对象,我们希望通过名字来进行比较他们是否同名同年龄,但是通过使用equals方法,只能比较这两个对象的引用是否相同(就是比较两个地址是否相同),显然没有达到需求。因为此时调用的是父类Object中的equals方法,而这个方法的底层就是使用 '='来比较两个对象的引用,即比较两个地址是否相同。代码示例如下:
class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; }}public class Test2 { public static void main(String[] args) { Person person1 = new Person("张三",18); Person person2 = new Person("张三",18); System.out.println(person1.equals(person2)); }}
运行结果:
false
因此我们可以根据需求在Person类中重写父类Object中的equals方法,代码示例如下:
class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; } public boolean equals(Object obj) { Person tmp = (Person)obj; return this.name.equals(tmp.name) && this.age == tmp.age; }}public class Test2 { public static void main(String[] args) { Person person1 = new Person("张三",18); Person person2 = new Person("张三",18); System.out.println(person1.equals(person2)); }}
运行结果:
true
对equals方法进行重写,也可以让编译器自己生成,右键—>construct—>equals() and hashCode(),然后一直next
自动生成的重写的equals方法如下:
@Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Person person = (Person) o; return age == person.age && Objects.equals(name, person.name); }
@Override 这个东西可以进行自动校验,检验你重写的方法名、参数列表、返回值是否相同,它会自动识别并提醒。
this == o
是判断这两个引用是否相同,如果是同一个引用,那么一定相同,返回true
0 == null || getClass() != o.getClass()
前者是判断另一个作为参数的引用是否为空,为空返回false;后者是判断进行比较的这两个引用的类型是否相同,类型不同返回false
前两个语句都不成立,则开始正式比较name和age是否都相同。
总结:比较对象中内容是否相同的时候,一定要重写equals方法。
toString方法
toString方法也和equals方法一样,如果需要使用toString方法打印一个person实例中的成员变量,来获取对象信息,如果不重写toString方法,调用时也还是会调用Object中的toString方法,打印出这个引用的地址,代码示例如下:
class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; }}public class Test2 { public static void main(String[] args) { Person person1 = new Person("张三",18); System.out.println(person1.toString()); }}
运行结果:
demo.Person@1b28cdfa
所以要达到需求,同样也需要在Person类中重写toString方法,代码示例如下:
class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; } public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }}public class Test2 { public static void main(String[] args) { Person person1 = new Person("张三",18); System.out.println(person1.toString()); }}
运行结果:
Person{name=‘张三’, age=18}
同样对toString方法进行重写也可以使用编译器自动生成,右键—>construct—>toString()—>选择变量—>OK
自动生成的重写的equals方法如下:
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; }
同样@Override也是校验作用
总结:获取对象中内容信息的时候,一定要重写toString方法。
hashCode方法
hashCode方法是来计算一个对象的具体位置,我们先假设它是一个内存地址。
我们假设名字相同、年龄相同的两个对象是储存在相同的内存地址,如果没有重写hashCode方法,那么在访问这个方法时就会调用Object中的这个方法,代码示例如下:
class Person { public String name; public int age; public Person(String name, int age) { this.name = name; this.age = age; }}public class Test2 { public static void main(String[] args) { Person person1 = new Person("张三",18); Person person2 = new Person("张三",18); System.out.println(person1.hashCode()); System.out.println(person2.hashCode()); }}
运行结果:
455659002
250421012
此时两个对象的hash值不一样
根据结果说明,Object类中的这个hashCode方法不满足我们的要求,所以我们可以重写hashCode方法,来满足只要对象中的名字和年龄相同就是相同的地址,代码示例如下:
class Person { public String name; public int age; @Override public int hashCode() { return Objects.hash(name, age); } public Person(String name, int age) { this.name = name; this.age = age; }}public class Test2 { public static void main(String[] args) { Person person1 = new Person("张三",18); Person person2 = new Person("张三",18); System.out.println(person1.hashCode()); System.out.println(person2.hashCode()); }
运行结果:
24022538
24022538
此时两个对象的hash值就一样了