目录
1. 重写
面试问题:重写和重载的区别(详):
静态绑定和动态绑定:
2. 向上转型和向下转型
2.1 向上转型
发生向上转型的场景/使用场景:
2.2 向下转型
3. 多态
3.1 什么是多态
3.2 多态的实现条件(缺一不可)
3.3 多态的优缺点
优点:
缺点:
3.4 避免在构造方法中调用重写的方法
子类对父类的某个方法进行重新实现,一定发生在继承层次上。
重写:也称为覆盖。重写是子类对父类非静态、非private修饰,非final修饰,非构造方法等的方法的实现过程进行重新编写。子类能够根据需要实现子类的方法或是父类的方法。
重写的注意事项:
1. 子类在重写父类的方法时,一般必须与父类方法一样: 返回值类型 方法名 (参数列表) 要完全一致
2. 被重写的方法返回值类型可以不同,但是必须是具有父子关系的
3. 修饰符可以不一样,但是有要求:子类的访问权限要大于等于父类的访问权限且修饰符不可以是private,private修饰的方法是不可以重写的。
访问权限由大到小:【public > protected > default > private 】
4. final 修饰的方法,我们叫做密封方法,不能被重写
5. 如果被static修饰,也不能被重写
6. 构造方法也不能重写,只能重载
7. 重写的方法, 可以使用 @Override 注解,用来检查错误
1. 重写必须满足返回值类型,方法名,参数列表都完全一样。返回值类型也可以不一样,但必须要构成父子关系。而重载的返回值类型不做要求,方法名必须一样,参数列表必须不一样,个数,数据类型,不同数据类型的顺序,有一个不同就算不一样。
2. 重写一定发生在继承层次上,重载可以发生在一个类里的多个方法之间,也可以发生在继承当中
重写的特点:(!!!)
只有成员方法才有重写,因为成员方法遵循就近原则,所以重写也遵循就近原则,new的对象是谁就先访问谁,new的是子类的对象,就会调用此子类中的此方法。new的是父类的对象,就会调用父类中的此方法。如:是new Dog() 就调用Dog中的此重写方法;是new Cat()就调用Cat中的此重写方法;是new Animials 就调用Animals中的此被重写的方法。
下面看一个重写的例子:
class Animals{
public String name;
public int age;
public void eat(){
System.out.println("Animals::eat()");
System.out.println(name+"正在吃饭");
}
public Animals(String name,int age){
this.name = name;
this.age = age;
}
}
class Cat extends Animals{
public void fun(){
super.eat();
}
public void eat(){
System.out.println("Cat::eat()");
System.out.println(name+"正在吃猫粮");
}
public Cat(String name,int age){
super(name,age);
}
}
class Dog extends Animals{
public boolean silly;
public void fun(){
super.eat();
}
public void eat(){
System.out.println("Dog::eat()");
System.out.println(name+"正在吃狗粮");
}
public Dog(String name,int age,boolean silly){
super(name,age);
this.silly = silly;
}
}
public class Test {
public static void main(String[] args) {
Dog dog = new Dog("hello",2,false);
dog.eat();
dog.fun();
System.out.println("------------------------------");
Cat cat = new Cat("mimi",3);
cat.eat();
cat.fun();
}
}
静态绑定:也称为前期绑定(早绑定),编译的时候,根据你传入的参数,能够确定你调用哪个方法,这就叫做静态绑定。
动态绑定:也称为后期绑定(晚绑定),即在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
个人理解,有重写的就是有动态绑定过程而没有重写的就是静态绑定
由于动态绑定的存在,才有了运行时多态的存在。
编译时,不能确定方法的行为【不知道调用对象是谁】,等到程序运行时,才能够确定具体调用哪个类的方法,【看等号右侧,即new的是哪个对象。是new Dog() 就调用Dog中的此方法;是new Cat()就调用Cat中的此方法;是new Animials 就调用Animals中的此方法】。这些只有程序运行后才能知道,所以为动态绑定。
javap -c 类名:看反汇编
对象从子类类型转为父类类型,自动类型转换,一定发生在继承层次上。
把子类对象给父类的引用 实际就是创建一个子类对象,将其当成父类对象来使用。
那么父类的这个引用【animals】就指向了子类的对象【new Dog("小灰",20,false)】
Animals animals = new Dog("小灰",20,false);
Animals是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
猫和狗都是动物,所以将子类对象【猫或狗】给父类【动物】引用是合理的,此对象的类型就变成了Animals,所以只能访问父类中特有的成员变量和成员方法,不能访问子类中特有的成员属性和成员方法
此时,父类的这个引用可以访问:
1. 父类中特有的成员变量和成员方法(若此方法为被重写的方法,不会访问)
2. 子类中的重写方法
1. 直接赋值
2. 方法传参
3. 方法返回
具体见下面代码:
class Animals{
public String name;
public int age;
public void eat(){
System.out.println("Animals::eat()");
System.out.println(name+"正在吃饭");
}
public void func(){
System.out.println("Animals::func()");
}
public Animals(String name,int age){
this.name = name;
this.age = age;
}
}
class Cat extends Animals{
public void eat(){
System.out.println("Cat::eat()");
System.out.println(name+"正在吃猫粮");
}
public void catchMouse(){
System.out.println(name+"正在抓老鼠");
}
public Cat(String name,int age){
super(name,age);
}
}
class Dog extends Animals{
public boolean silly;
public void eat(){
System.out.println("Dog::eat()");
System.out.println(name+"正在吃狗粮");
}
public void barks(){
System.out.println(name+"正在汪汪叫");
}
public Dog(String name,int age,boolean silly){
super(name,age);
this.silly = silly;
}
@Override
public String toString() {
return name+" "+age+" "+silly;
}
}
public class Test {
/**
* 2.
* 传子类的引用,用父类类型的引用接收
*
* 此时,站在function的角度上,只有一个引用animals,
* 一个引用animals调用同一个方法eat(),
* 因为这个引用animals指向的对象不一样,(或者可以说,因为传的参数不一样)
* 所以调用这个方法function时,所表现的行为不一样,
* 这种思想就叫做多态,多态是一种思想:
* 不同对象去完成同一行为,会产生不同的状态
*/
public static void function(Animals animals1){
animals1.eat();
}
/**
* 3. 方法返回 - 返回子类对象,会转换为父类类型
* 被父类引用接收
*/
public static Animals function2(){
System.out.println("方法返回");
return new Cat("haha",7);
}
public static void main(String[] args) {
/**
* 1. 直接赋值
*/
Animals animals = new Dog("大黄",4,true);
animals.func();
animals.eat();
System.out.println("---------------------------");
/**
* 2. 方法传参 - 传参一般传的引用
* 接收参数的方法一般设置为静态方法,
* 因为静态方法不需要new一个此类的对象就可以直接通过类名访问,
* 且在同一个类中,类名还可以省略,就特别的方便,
* 所以平常我们使用的方法都是带static的静态方法
*/
Dog dog = new Dog("小黑",6,false);
function(dog);
Cat cat = new Cat("mimmi",5);
function(cat);
System.out.println("---------------------------");
/**
* 3.
* 用父类引用接收
*/
Animals animals2 = function2();
}
}
对象从父类类型转为子类类型,需要强制类型转换
将一个子类对象经过向上转型之后当成父类方法使用,再无法调用子类的方法,
但有时候可能需要调用子类特有的 方法,此时:将父类引用再还原为子类对象即可,即向下转换。
向下转型极度不安全,不建议使用
if (animals instanceof Cat) {}
instancof:
左边是对象,右边是类,返回类型是boolean类型。它的具体作用是测试左边的对象是否是右边类或者该类的子类创建的实例对象,是,则返回true,否则返回false。
多态是一种思想,不同对象去完成同一行为,会产生不同的状态
一个引用,调用同一个方法,因为引用的子类对象不一样, 所以调用这个方法后,表现出来的行为就不一样,这种思想就叫做多态。
1. 【继承】必须在继承体系下
2. 【重写】子类必须要对父类中方法进行重写
3. 【向上转型】通过父类的引用调用重写的方法
1. 能够降低代码的 "圈复杂度", 避免使用大量的 if - else
2. 可扩展能力更强
通过一个例子来体会:【画图形】
/**
* 题目:
* 1. 按顺序打印出:矩形,圆,三角形
* 2. 按顺序打印出:矩形,圆,矩形,圆,三角形
*/
class Shape{
public void draw(){
System.out.println("画图形");
}
}
class rectangle extends Shape{
@Override
public void draw() {
System.out.println("画矩形");
}
}
class Circle extends Shape{
@Override
public void draw() {
System.out.println("画圆");
}
}
class Triangle extends Shape{
@Override
public void draw() {
System.out.println("画三角形");
}
}
public class Test {
/**
* 第一题:
*
* 一个引用,调用同一个方法,因为引用的子类对象不一样,
* 所以调用这个方法后,表现出来的行为就不一样
*/
public static void drawFuc(Shape shape){
shape.draw();
}
/**
* 第二题:
*/
public static void drawShapes(Shape shape){
shape.draw();
}
public static void main(String[] args) {
/**
* 第一题做法:按顺序打印出:矩形,圆,三角形
*
* 传参
* 1. 直接传父类类型的引用,,由父类类型接收
* 2. 传子类类型的引用 传过去会发生自动类型转换,由父类类型接收
* 3. 直接传匿名对象,传过去会发生自动类型转换,由父类类型接收
*/
Shape shape = new rectangle();
drawFuc(shape);
Circle circle = new Circle();
drawFuc(circle);
drawFuc(new Triangle());
System.out.println("----------------------");
/**
* 第二题做法:按顺序打印出:矩形,圆,矩形,圆,三角形
* 传不同的对象,会产生不同的行为
*/
drawShapes(new rectangle());
drawShapes(new Circle());
drawShapes(new rectangle());
drawShapes(new Circle());
drawShapes(new Triangle());
//改进后:把对象存在数组中,遍历数组
System.out.println("---------改进后----------------");
Shape[] shapes = {new rectangle(),new Circle(),new rectangle(),new Circle(),new Triangle()};//这串代码就发生了向上转型
for (Shape s:shapes) {
// drawShapes(s);//调用了方法drawShapes进行传参
s.draw(); // 遍历数组赋值,s中已经是不同的子类对象了,
// 所以不传参也可以,直接调用draw方法,不同的对象会产生不同的行为
}
}
}
代码的运行效率降低
1. 属性没有多态性
属性没有多态性的本质是:重写不会发生在属性中,只会发生在方法中,没有重写的特点,就没有多态性。
当父类和子类都有同名属性的时候,通过父类引用,只能引用父类自己的成员属性
2. 构造方法没有多态性
一段有坑的代码:
我们创建两个类, B 是父类, D 是子类。D 中重写 func 方法,并且在 B 的构造方法中调用 func
构造 D 对象的同时, 调用D的构造方法之前会【通过super();】先调用B 的构造方法
B 的构造方法中调用了被重写的方法 func , 此时会触发动态绑定,因为new的对象是,new D();所以会调用到 D 中的 func
此时 D 对象自身还没有构造, 没有调用D的构造方法,所以 num 处在未赋值的状态, 值为 0
所以,在构造方法内,不要调用实例方法,避免发生重写;final和private方法除外,【它们不会出现重写情况】
如果这个方法被子类重写, 就会触发动态绑定, 但是此时子类对象还没构造完成,可能会出现一些隐藏的但是又极难发现的问题。