Java SE - 继承与多态
目录
- 1.继承
- 1.1 继承的语法形式
- 1.2 super关键字
- 1.3 父类的构造方法
- 2.this 和 super的联系与区别
- 3.继承中代码块的执行顺序
- 4.final关键字
- 5.组合
- 6.多态
- 6.1 多态发生的条件
- 6.1.1 向上转型:使用父类接收子类创建的对象。
- 6.1.2 重写父类中的方法
- 6.1.3 使用父类的引用调用子类重写的方法
- 6.2.多态的优缺点
- 6.2.1 优点
- 6.2.2 缺点
- 6.3 向下转型
1.继承
继承的作用是将大量具有相同性质的代码进行抽离,通过继承的方式提供给子类复用,在Java中的继承是单继承,即一个子类只能有一个父类,但是一个父类可以有多个子类。
在没有继承前实现一个Frog(蛙)类和Duck类观察各自的特性。
package Demo1;public class Frog {public String name;public int age;public void eat(){System.out.println(this.name + "正在吃昆虫......");}public void sleep(){System.out.println(this.name + "这一只小青蛙正在休息中.......");}public void swim(){System.out.println(this.name + "在蛙泳....");}public void jump(){System.out.println(this.name + "正在跳....");}
}
public class Duck {public String name;public int age;public void eat(){System.out.println(this.name + "正在吃昆虫......");}public void sleep(){System.out.println(this.name + "这一只小青蛙正在休息中.......");}public void swim(){System.out.println(this.name + "在蛙泳....");}public void walk(){System.out.println(this.name + "正在路上走....");}
}
为了提高代码的执行效率,可以将以上大量重复的过程进行共性的抽取,Frog和Duck都是属于动物,将其的共性定义在一个Animal类中。
public class Animal {public String name;public int age;public void eat(){System.out.println(name + "正在吃饭");}public void sleep(){System.out.println(name + "正在休息");}public void swim(){System.out.println(name + "在游泳");}
}
通过继承父类Animal就可以使子类中减少大量重复出现的代码。
1.1 继承的语法形式
在子类中继承父类需要使用extends关键字,格式为:访问修饰符+class+子类名+extends+父类名
public class Duck extends Animalpublic class Frog extends Animal
继承后以上子类的程序可以修改为如下部分:
public class Duck extends Animal{public void walk(){System.out.println(this.name + "正在路上走....");}
}
public class Frog extends Animal {public void jump(){System.out.println(this.name + "正在跳....");}
}
此时在Frog类中使用main函数实例化一个Frog类型的对象,调用父类和子类中的方法。
class Animal {public String name;public int age;public void eat(){System.out.println(name + "正在吃饭");}public void sleep(){System.out.println(name + "正在休息");}public void swim(){System.out.println(name + "在游泳");}
}
class Frog extends Animal {public void jump(){System.out.println(this.name + "正在跳....");}public static void main(String[] args) {Frog frog =new Frog();frog.eat();frog.jump();}
}
输出结果如下:名字是null,表示空应用,这是应为还未对父类中的成员初始化。
1.2 super关键字
在子类中已经继承父类的成员变量,使用实例化的对象的引用进行访问父类成员。
class Animal {public String name;public int age;public void eat(){System.out.println(name + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
class Frog extends Animal {public void jump(){System.out.println(this.name + "正在跳....");}public static void main(String[] args) {Frog frog =new Frog();frog.name = "哇哇";//通过引用访问到父类成员frog.age = 22;frog.eat();frog.jump();}
}
输出结果如下:可以通过子类引用访问到父类的成员变量。
如果在子类中存在与父类相同的成员变量,调用这一个相同的成员变量输出的结果是什么?
class Animal {public String name;public int age;public void eat(){System.out.println(name + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
class Frog extends Animal {public String name;public void jump(){System.out.println(this.name + "正在跳....");}public static void main(String[] args) {Frog frog =new Frog();frog.name = "哇哇";frog.eat();frog.jump();}
}
输出如下:通过引用调用父类和子类同名的成员变量时,优先访问的是子类的成员变量(就近原则),所以在调用父类方法时,父类成员还未初始化,输出的name为null,而子类已经通过引用初始化成员变量,输出的name为哇哇。
需要访问父类的成员变量时,可以使用super关键字,格式是super + 点 + 父类变量名。
public static void main(String[] args) {Frog frog =new Frog();super.name = "哇哇";frog.eat();}
}
在main方法中使用发现会报错,因为main方法是一个静态方法,静态方法中只能初始化静态变量,而name是一个非静态变量,因此系统会报错,无法正常初始化。
class Frog extends Animal {//编译报错super.name = "哇哇";public void jump(){System.out.println(super.name + "正在跳....");}
如果将super关键字单独作为一条语句,会出现编译报错,不符合语法规则,super关键字的使用需要在子类的方法中使用,出现在其它类中就会编译报错,正确的使用应该将super关键字调用父类成员变量的语句包含在子类方法中。
class Animal {public String name;public int age;public void eat(){System.out.println(name + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
class Frog extends Animal {public void jump(){//子类方法中使用:super + 点 + 父类变量名super.name = "哇哇";System.out.println(super.name + "正在跳....");}public static void main(String[] args) {Frog frog =new Frog();frog.jump();}
}
1.3 父类的构造方法
在子类中使用父类的成员变量,如果父类中已经完成构造方法,在子类中就不需要帮助父类初始化,使用IDEA快速生成构造方法,鼠标点击右键找到Generate,点击找到Constructor方法,选择参数完成构造。
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 + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
但是此时编译器会报错,There is no no-arg constructor available in ‘Demo1.Animal’,不允许在父类中直接使用构造方法;既然不能在父类中直接使用构造方法,那么父类成员的初始化,应该在子类中完成,可以使用super关键字,调用父类的构造方法完成初始化。
package Demo1;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 + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
class Frog extends Animal {//构造方法public Frog(String name, int age) {//super替代的是Animal这个构造方法//等价于Animal(name,age)super(name, age);}
}
如果子类没有构造方法,默认是提供一个不带参数的构造方法,父类的构造方法也是默认是不带参数的,在子类完成构造前,必须先帮助父类构造,调用父类的构造方法super(参数)必须在子类构造方法的首行。
class Animal {public String name;public int age;//不带参数的构造方法,系统默认提供public Animal() {}//带两个参数的构造方法public Animal(String name, int age) {this.name = name;this.age = age;}public void eat(){System.out.println(name + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
class Frog extends Animal {//不带参数的构造方法public Frog() {//super替代的是Animal这个构造方法//等价于Animal()super();}//带两个参数的构造方法public Frog(String name, int age) {//super替代的是Animal这个构造方法//等价于Animal(name,age)super(name, age);}
完成了父类成员的构造方法,在实例化对象的过程就可以同时初始化父类成员变量。
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 + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}public void swim(){System.out.println(name + "在游泳.....");}
}
class Frog extends Animal {//构造方法public Frog(String name, int age) {//super替代的是Animal这个构造方法//等价于Animal(name,age)super(name, age);}public void jump(){System.out.println(this.name + "正在跳....");}public static void main(String[] args) {//创建的同时初始化Frog frog =new Frog("哈哈",2);frog.jump();//创建的同时初始化Frog frog1 = new Frog("歪歪",4);frog1.swim();}
}
2.this 和 super的联系与区别
联系:1.this 和 super都是关键字;2.只能在非静态成员方法中使用,访问字段(成员变量)和方法;
3.在构造方法中调用两者执行的方法,必须放在方法的首句,并且两者不能同时存在。
区别:1.this表示当前对象的引用,该引用指向当前实例化的对象,而super关键字是当前子类实例化对象从父类继承部分的引用,调用的是父类的成员和方法。2.子类的构造方法中一定存在super(参数)方法,帮助父类成员完成初始化,如果子类的构造方法不带参数,可以默认不写,系统会自动提供一个不带参数的构造方法,包含super(),而this()方法不一定必须存在。
class Base{//成员变量int a;int b;//构造方法public Base(int a, int b) {this.a = a;this.b = b;}
}class Device extends Base{//不带参数的构造方法public Device(){this(10,20);//首行}//带两个参数的构造方法public Device(int a,int b) {super(a,b);//首行}//普通方法void print(){System.out.println("a = " + this.a + " b = " + b);}}public class Main{public static void main(String[] args) {//使用不带参数的构造方法Device device1 = new Device();device1.print();//使用带两个参数的构造方法Device device2 = new Device(20,10);device2.print();}
}
3.继承中代码块的执行顺序
在继承关系中,通过实例化对象会执行子类的构造方法,子类的构造方法中第一条语句是帮助父类完成构造方法,因此在构造方法中会先执行父类的构造方法后执行子类的构造方法;代码块中包含静态代码块,存储于方法区,在执行中最先被加载,整个程序中只被加载一次;实例化代码块的执行比构造方法先执行,因此代码块的执行顺序是最先执行静态代码块,然后执行实例化代码块,最后执行构造方法。
以下程序执行的顺序是什么?
class Base{public Base() {System.out.println("Base()::构造方法被执行了........1");}{System.out.println("Base()::实例代码块被执行了........2");}static{System.out.println("Base()::静态代码块被执行了........3");}
}class Device extends Base{public Device() {System.out.println("Device()::构造方法被执行了.........4");}{System.out.println("Device()::实例化代码块被执行了........5");}static{System.out.println("Device()::静态代码块被执行了.........6");}}public class Main{public static void main(String[] args) {//实例化对象Device device = new Device();}
}
先执行父类的静态代码块,后执行子类的静态代码块,然后执行父类的实例化代码块和构造方法,最后执行子类的实例化代码块和构造方法。
4.final关键字
final本意有最终的,不可改变的意思,final关键字有三个作用,修饰变量,方法,和类,被final修饰的变量是不可被修改的,类似于修饰后变为常量值;被final修饰的方法不可被重写;被final修饰的类不可被继承,防止该类成为基类(父类)。
//不可继承
final class Test {//不可修改final int a = 20;a = 10;//errfinal void print(){System.out.println(this.a);}//可以重载void print(int a){System.out.println(a);}
}//final修饰的不可继承,编译报错
public class Main extends Test{@Override//不可重写,编译报错void print(){System.out.println("hhhhh");}
}
5.组合
组合与继承类似,都可实现将重复的部分进行归类后复用,组合是将实例化对象的引用归类,而不是通过继承,例如实现一个教室类,教室中包含学生和老师,老师和学生是属于人这一个类。
//人类
class Person{String name;int age;public Person(String name, int age) {this.name = name;this.age = age;}//....
}
//老师类
class Teacher extends Person{public Teacher(String name, int age) {super(name, age);}//....
}
//学生类
class Student extends Person{public Student(String name, int age) {super(name, age);}//....
}
//教室类
class Classroom{//组合Teacher和Student,创建引用private Teacher teacher;private Student student;//构造方法public Classroom(Teacher teacher, Student student) {this.teacher = teacher;this.student = student;}//....
}
public class Main{public static void main(String[] args) {//实例化对象Classroom classroom = new Classroom(new Teacher("王老师",47),new Student("小李",12));}}
在Java只能支持单继承,但是使用组合间接的可以在类中访问到其它类,减少大量重复的程序实现,可以提高程序的运行效率,因此在能使用组合的情况下尽量使用组合,可以参考以下文章理解:
【为什么说要慎用继承,优先使用组合】
6.多态
多态简单理解就是多种形态,当同一个行为或者属性展示的对象不一样时,呈现的状态就不一样,例如:Animal作为父类,存在子类Dog和Cat,父类存在一个eat() 的成员方法,当对象是Dog时就是吃狗粮,当对象是Cat时就是吃猫粮,对象不同,呈现的状态就不一样,此时就发生了多态。
6.1 多态发生的条件
6.1.1 向上转型:使用父类接收子类创建的对象。
创建一个Aniaml类,继承一个Frog类和一个Duck类,使用父类接收子类创建的对象。
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 Duck extends Animal{public Duck(String name, int age) {super(name, age);}
}class Frog extends Animal {//构造方法public Frog(String name, int age) {//super替代的是Animal这个构造方法//等价于Animal(name,age)super(name, age);}public static void main(String[] args) {//向上转型:使用父类的引用接收子类创建的对象Animal animal1 = new Duck("丫丫",3);Animal animal = new Frog("瓦瓦",3);}
}
此时有了父类的引用,接收不同子类的对象,调用父类的eat方法。
public static void main(String[] args) {//向上转型:使用父类的引用接收子类创建的对象Animal animal1 = new Duck("丫丫",3);Animal animal = new Frog("瓦瓦",3);//eat方法animal.eat();animal1.eat();
}
运行时的结果动作是一样的,并没有发生多态,这是因为通过父类引用调用的是父类的eat()方法,如果想实现多态,需要在子类中重写父类的方法。
6.1.2 重写父类中的方法
重写方法的规则是,方法名相同,参数列表相同(参数个数,参数顺序,参数类型都一样),返回值相同,返回类型相同(如果是继承关系,返回类型是父类或子类而不同的,也默认是符合重写规则的,这种类型称为协变类型)。
Frog重写的eat()方法
@Overridepublic void eat() {System.out.println(this.name + "在吃昆虫......");}
Duck重写的eat()方法
@Overridepublic void eat() {System.out.println(this.name + "在吃小鱼儿......");}
6.1.3 使用父类的引用调用子类重写的方法
在子类完成重写后就可以使用父类的引用取调用重写的方法,此时父类调用的是子类的重写方法,而不是父类本身的方法,此时就发生了动态绑定。
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 + "正在吃饭....");}public void sleep(){System.out.println(name + "正在休息....");}
}
class Duck extends Animal{public Duck(String name, int age) {super(name, age);}@Overridepublic void eat() {System.out.println(this.name + "在吃小鱼儿......");}
}class Frog extends Animal {//构造方法public Frog(String name, int age) {//super替代的是Animal这个构造方法//等价于Animal(name,age)super(name, age);}@Overridepublic void eat() {System.out.println(this.name + "在吃昆虫......");}public static void main(String[] args) {//向上转型:使用父类的引用接收子类创建的对象Animal animal1 = new Duck("丫丫",3);Animal animal = new Frog("瓦瓦",3);//调用重写的eat()方法animal.eat();animal1.eat();}
}
动态绑定的原因是从程序运行时通过父类引用调用重写的方法时,引用存储的是子类重写方法的地址,调用的是子类的方法。
与动态绑定相关有静态绑定,在程序运行前通过方法的参数列表就确定调用的是哪一个函数,程序运行时直接使用该函数时,常见的静态绑定有方法重载。
6.2.多态的优缺点
6.2.1 优点
多态的使用可以减低圈复杂度,圈复杂度是指程序中使用嵌套的循环或分支的次数,一般圈复杂度不建议超过10,如下:
//形状类
class Shape {void draw(){System.out.println("画画");}
}
//正方形类
class Square extends Shape{@Overridevoid draw(){System.out.println("画出一个正方形......");}
}
//三角形类
class Triangle extends Shape{@Overridevoid draw() {System.out.println("画出一个三角形......");}
}
//圆类
class Cycle extends Shape{@Overridevoid draw() {System.out.println("画出一个圆.......");}public static void main(String[] args) {//创建对象,向上转型Shape shape1 = new Square();Shape shape2 = new Triangle();Shape shape3 = new Cycle();String[] shape = {"cycle","triangle","square","triangle","cycle"};for (int i = 0; i < shape.length; i++) {//创建一个临时变量接收String shapeT = shape[i];//判断,圈复杂度if (shapeT.equals("cycle")) {shape3.draw();//多态} else if (shapeT.equals("triangle")) {shape2.draw();//多态} else if (shapeT.equals("square")) {shape1.draw();//多态}} }
}
当程序每多一层圈复杂度,程序的逻辑就越复杂,太多的if _ else if语句的判断,程序效率会减低,代码也不够间接,使用多态修改后如下:
public static void main(String[] args) {//创建对象,向上转型Shape shape1 = new Square();Shape shape2 = new Triangle();Shape shape3 = new Cycle();Shape[] shape = {shape3,shape2,shape1,shape2,shape3};//多态for (Shape tem : shape) {tem.draw();}
}
减低圈复杂度后也可达到同样的运行效果,代码也比较简洁。
6.2.2 缺点
在构造方法中使用重写的方法,可能出现预期外的效果,例如:以下程序的运行结果是什么?
class A{//构造方法public A() {func();}void func(){System.out.println("A::func()......");}
}
class B extends A{int num = 1;public B() {}@Overridevoid func(){System.out.println("B::func()执行了......" + num);}
}
public class Main {public static void main(String[] args) {A a = new B();}
}
实例化一个子类的对象,会先完成父类的构造方法,父类中调用了func() 方法,此时父类和子类都有func方法,此时还未通过引用去调用重写的方法,一般默认使用父类的func方法,完成构造方法后会进行变量的初始化,因此num应该是1。实际上输出的结果如下:
执行的是子类的重写方法,因为在完成构造方法是,调用的方法是重写的,此时会发生动态绑定,实际上调用的是子类重写的方法,此时num因为构造方法还未执行完,并未完成初始化,输出的是默认值0;因此,在构造方法中使用重写的方法可能会造成一些难以察觉的错误,所以构造方法中应该避免使用重写方法。
6.3 向下转型
向下转型是用子类的引用接收父类的引用,但是向下转型是不安全,有时编译器并不会直接报错,但是在编译时报错。例如:创建一个动物类,包含子类Chicken和Pig。
class Animal{//成员变量public String name;public int age;//构造方法public Animal(String name, int age) {this.name = name;this.age = age;}//重写toString方法@Overridepublic String toString() {return "Animal{" +"name='" + name + '\'' +", age=" + age +'}';}
}
//子类Chicken
class Chicken extends Animal{//构造方法public Chicken(String name, int age) {super(name, age);}
}
//子类Pig
class Pig extends Animal{//构造方法public Pig(String name, int age) {super(name, age);}public static void main(String[] args) {//向上转型Animal animal1 = new Chicken("吉吉",3);Animal animal2 = new Pig("朱朱", 2);//向下转型Chicken chicken1 = (Chicken) animal2;//编译不报错Chicken chicken2 = (Chicken) animal1;//animal1指向的是Chicken的对象,不报错//输出System.out.println(chicken1);System.out.println(chicken2);}
}
以上程序编译器中并不会直接报错,但是在程序运行时会报错,这个错误是ClassCastException(类型转换异常),原因是class声明的Pid类不能转换为class声明的Chicken类,这种错误比较隐蔽,所以向下转型是不安全的,如果需要向下转型,可以使用instanceof判断。
public static void main(String[] args) {//向上转型Animal animal1 = new Chicken("吉吉",3);Animal animal2 = new Pig("朱朱", 2);//向下转型:使用关键字instanceof//判断引用指向的类型是否为指定的子类//如果是ture就执行,如果是false就不执行if(animal2 instanceof Chicken){Chicken chicken1 = (Chicken) animal2;//编译不报错System.out.println(chicken1);}if(animal1 instanceof Chicken) {Chicken chicken2 = (Chicken) animal1;//animal1指向的是Chicken的对象,不报错System.out.println(chicken2);}}
使用instanceof关键字判断给向下转换提供多一个安全保障,表达式为假时,就可以不执行转换操作,防止类型转换异常,更多instanceof的解释可以参考以下网站:instanceof介绍