继承和多态
继承
继承用来对类进行共性抽取,实现代码复用。
继承的语法
在JAVA中如果要表示类之间的继承关系,要借助extends关键字:
修饰符 class 子类 extends 父类{
......
}
对于狗、猫,他们都属于动物,那么就可以把动物这个类当做父类,狗和猫就属于子类。
java
//Animal.java
public class Animal {
String name;
int age;
public void eat(){
System.out.println(name+"正在吃饭");
}
public void sleep(){
System.out.println(name+"正在睡觉");
}
}
java
//Cat.java
public class Cat extends Animal{
void mew(){
System.out.println(name+"喵喵喵");
}
}
java
//Dog.java
public class Dog extends Animal{
void bark(){
System.out.println(name+"汪汪汪");
}
}
java
//TestExtend
public class TestExtend {
public static void main(String[] args){
Dog dog = new Dog();
//dog类中没有定义任何成员变量,name和age属性肯定是从父类Animal中继承下来的
System.out.println(dog.name);
System.out.println(dog.age);
//eat()和sleep()方法也是从Animal中继承下来的
dog.eat();
dog.sleep();
dog.bark();
}
}
注意:
1.子类会将父类中的成员变量或者成员方法继承到子类中
2.子类继承父类后,必须要添加自己特有的成员,体现出与基类的不同,否则就没有必要继承了
父类成员访问
子类访问父类成员变量
成员变量访问遵循就近原则,自己有,则优先访问自己的,如果没有,向父类中寻找。
1.子类和父类不存在同名成员变量
java
//Base.java
public class Base {
int a;
int b;
}
java
//Derived.java
public class Derived extends Base{
int c;
public void method(){
a=10;//访问从父类中继承下来的a
b=20;//访问从父类中继承下来的b
c=30;//访问子类自己的c
}
}
2.子类和父类成员变量同名
java
//Base.java
public class Base {
int a;
int b;
double c;
}
java
//Derived.java
public class Derived extends Base{
int c;
public void method(){
a=10;//访问从父类中继承下来的a
b=20;//访问从父类中继承下来的b
c=8.999;//访问子类自己的c
}
}
此时会发生报错,因为子类和父类中都有成员c,那么优先访问子类中的c,子类中的c是int类,那么不能赋值为double类。
子类访问父类成员方法
java
//Base.java
public class Base {
public void methodA(){
System.out.println("Base中的methodA()");
}
public void methodB(){
System.out.println("Base中的methodB()");
}
}
java
//Derived.java
public class Derived extends Base{
public void methodA(int a){
System.out.println("Derived中的method(int)方法");
}
public void methodB(){
System.out.println("Derived中的methodB()方法");
}
public void methodC(){
methodA();
methodA(33);
methodB();
}
}
java
//TestDerived.java
public class TestDerived {
public static void main(String[] args){
Derived d = new Derived();
d.methodC();
}
}
通过子类对象访问父类与子类同名方法时,如果子类和父类同名方法的参数列表不同(重载),根据调用方法传递的参数选择合适的方法访问。
super关键字
如果子类和父类中有相同的成员变量和成员方法(参数列表也一样),如果要访问父类中的成员,则用super关键字即可。
我们仅将上述案例中methodC代码略作修改:
调用TestDerived:
子类和父类中methodB方法同名且参数一致,用super.methodB()则可以访问到父类中的methodB方法。
注意:super只能在非静态方法中使用
子类构造方法
子类对象构造时,需要先调用父类构造方法,然后执行子类的构造方法。
java
//Base.java
public class Base {
public Base(){
System.out.println("Base()");
}
}
java
//Derived.java
public class Derived extends Base{
public Derived(){
System.out.println("Derived()");
}
}
java
//TestDerived.java
public class TestDerived {
public static void main(String[] args){
Derived d = new Derived();
}
}
注意:
1.若父类显式定义无参或者默认的构造方法,在子类构造方法第一行默认有隐含的super()调用,即调用基类构造方法
2.如果父类构造方法是带有参数的,则此时用户需要为子类显式定义构造方法,并在子类构造方法中选择合适的父类构造方法调用,否则编译失败。
3.在子类构造方法中,super(......)调用父类构造时,必须是子类构造函数中第一条语句。
4.super(......)只能在子类构造方法中出现一次,并且不能和this同时出现
super和this
两者相同点:
1.都是Java中的关键字
2.只能在类的静态方法中使用,用来访问非静态成员方法和字段
3.在构造方法中调用时,必须是构造方法中的第一条语句,并且不能同时存在
不同点:
1.this是当前对象的引用,当前对象即调用实例方法的对象,super相当于是子类对象中从父类继承下来部分成员的引用
2.在非静态成员方法中,this用来访问本类的方法和属性,super用来访问父类继承下来的方法和属性
3.在构造方法中,this(......)用于调用本类构造方法,super(......)用于调用父类构造方法,两种调用不能同时在构造方法中出现
4.构造方法中一定会存在super(......)的调用,用户没有写,编译器也会增加,但是this(......)用户不写则没有
再谈初始化
在没有继承关系时,实例代码块和静态代码块的执行顺序是怎样的呢?
java
class Person{
public String name;
public int age;
public Person(String name,int age){
this.name=name;
this.age=age;
System.out.println("构造方法执行");
}
{
System.out.println("实例代码块执行");
}
static{
System.out.println("静态方法执行");
}
}
public class TestDemo {
public static void main(String[] args){
Person person1 = new Person("张三",15);
System.out.println("==========");
Person person2 = new Person("李四",20);
}
}
1.静态代码块先执行,并且只执行一次,在类加载阶段进行
2.当有对象创建时,才会执行实例代码块,实例代码块执行完成后,最后构造方法执行
当有继承关系的执行顺序
java
class Person{
public String name;
public int age;
public Person(String name,int age){
this.name=name;
this.age=age;
System.out.println("Person构造方法执行");
}
{
System.out.println("Person实例代码块执行");
}
static{
System.out.println("Person静态方法执行");
}
}
class Student extends Person{
public Student(String name, int age) {
super(name, age);
System.out.println("Student构造方法执行");
}
{
System.out.println("Student实例代码块执行");
}
static{
System.out.println("Student静态代码块执行");
}
}
public class TestDemo {
public static void main(String[] args){
Student student1 = new Student("张三",15);
System.out.println("==========");
Student student2 = new Student("李四",20);
}
}
我们通过打印顺序得出以下结论:
1.父类静态代码块优先于子类静态代码块执行,且是最早执行
2.父类实例代码块和父类构造方法紧接着执行
3.子类的实例代码块和子类构造方法紧接着再执行
4.第二次实例化子类对象时,父类和子类的静态代码块都将不会再执行
protected关键字
Java中的访问限定符主要限定:类或者类中成员能否在类外或者其他包中被访问。
我们接下来在具体代码中测试一下:
java
package extend01;
public class B {
private int a;
protected int b;
public int c;
int d;
}
java
package extend01;
public class D extends B{
public void method(){
super.a=1;//报错,父类private成员在同一包中的子类中不可见
super.b=20;//父类中的protected成员在相同包的子类中可见
super.c=30;//父类中的public成员在相同包中的子类中可见
super.d=40;//父类中的default成员在相同包中的子类中可见
}
}
java
package extend02;
import extend01.B;
public class C extends B{
public void method(){
super.a=10;//报错,父类中的private在不同包中的子类中不可见
super.b=20;//父类中的protected成员在不同包中的子类中可见
super.c=30;//父类中的public成员在不同包中的子类中可见
super.d=40//报错,父类中的default成员在不同包中的子类中不可见
}
}
java
package extend02;
import extend01.B;
public class E{
public void method(){
B b = new B();
b.a=10;
b.b=20;
b.c=30;//只有public成员才能在不同包中的非子类中被访问
b.d=40;
}
}
注意:
1.父类private成员变量虽然在子类中不能直接访问,但是也继承到子类中了
2.尽可能做到"封装",能用private尽量不用public
继承方式
Java中支持的继承方式有:
1.单继承,父类仅有一个子类,子类仅有这一个父类
2.多层继承,A继承B,C继承A
3.不同类继承同一个类,A和B都继承C
Java中不支持一个类继承多个类。
final关键字
1.修饰变量或字段,表示常量,不能被修改
final int a=10;
2.修饰类,则此类不可被继承
final public class Animal{
......}
3.修饰方法,则不可被重写
组合
继承表示对象之间是is-a的关系,比如:狗是动物,猫是动物
组合表示对象之间是has-a的关系,比如:汽车有引擎、轮胎、散热器......
例如,已有Tire、Engine类,则Car类可以加入如下成员:
Tire tire;
Engine engine;
多态
多态即,去完成某个行为,当不同对象去完成时会产生不同状态
在Java中实现多态,必须要满足如下几个条件,缺一不可:
1.必须在继承体系下
2.子类必须要对父类中方法 进行重写
3.通过父类的引用调用重写的方法
多态的体现:在代码运行时,当传递不同类对象时,会调用对应类中方法。
例如:
Animal.java
java
public class Animal {
String name;
int age;
public Animal(String name,int age){
this.name=name;
this.age=age;
}
public void eat(){
System.out.println(name+"吃饭");
}
}
Cat.java
java
public class Cat extends Animal{
public Cat(String name, int age) {
super(name, age);
}
public void eat(){
System.out.println(name+"吃鱼");
}
}
Dog.java
java
public class Dog extends Animal{
public Dog(String name, int age) {
super(name, age);
}
public void eat(){
System.out.println(name+"吃骨头");
}
}
Test.java
java
public class TestAnimal {
public static void eat(Animal a){
a.eat();
}
public static void main(String[] args) {
Cat cat=new Cat("咪咪",5);
Dog dog=new Dog("旺旺",6);
eat(cat);
eat(dog);
}
}
TestAnimal的运行结果为:
上述代码中,分割线以上的代码是类的实现者 编写的,分割线下方的代码是类的调用者编写的
当类的调用者在编写eat这个方法的时候,参数类型为Animal(父类),此时在该方法内部并不知道,也不关注当前的a引用指向的是哪个类型(哪个子类)的实例。此时a这个引用调用eat方法可能会有多种不同的表现,这种行为就称为多态。
重写(override)
也称为覆盖。重写是子类对父类非静态、非private修饰、非final修饰,非构造方法 等的实现过程进行重新编写,返回值和形参都不能改变 。即外壳不变,核心重写
【方法重写的规则:】
1.子类在重写父类的方法时,一般必须与父类方法原型一致:返回值类型 方法名 参数列表要完全一致
2.被重写的方法返回值类型可以不同,但是必须是具有父子关系的
3.访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类方法被public修饰,则子类中重写该方法就不能声明为protected
4.父类被static、private、final(密封方法)修饰的方法、构造方法都不能被重写
5.重写的方法,可以使用@Override注解来显式指定,有了这个注解能帮我们进行一些合法性校验,比如不小心将方法名字拼错了(eat写成了aet),那么此时编译器就会发现父类中没有aet方法,就会编译报错,提示无法构成重写。
【重写和重载的区别:】
静态绑定 :也称为前期绑定(早绑定),即在编译时,根据用户所传递实参类型就确定了具体调用哪个方法。典型代表函数重载。
动态绑定:也叫后期绑定(晚绑定),在编译时,不能确定方法的行为,需要等到程序运行时,才能够确定具体调用哪个类的方法。
向上转型和向下转型
向上转型
向上转型实际就是创建一个子类对象,将其当成父类对象来使用
语法格式 :父类类型 对象名 = new 子类类型()
例如:
Animal animal = new Cat("喵喵",15);
animal是父类类型,但可以引用一个子类对象,因为是从小范围向大范围的转换。
【使用场景】
1.直接赋值
2.方法传参
3.方法返回
以下是具体使用案例:
java
public class TestAnimal {
//方法传参:形参为父类型引用,可以接收任意子类的对象
public static void eat(Animal a){
a.eat();
}
//作返回值:返回任意子类对象
public static Animal buyAnimal(String var){
if("狗".equals(var)){
return new Dog("狗狗",1);
}else if("猫".equals(var)){
return new Cat("猫猫",3);
}else{
return null;
}
}
public static void main(String[] args) {
Animal cat= new Cat("mimi",4);//直接赋值:子类对象赋值给父类对象
Dog dog = new Dog("gougou",5);
eat(cat);
eat(dog);
Animal animal=buyAnimal("狗");
animal.eat();
animal=buyAnimal("猫");
animal.eat();
}
}
向上转型的优点:让代码实现更简单灵活
向上转型的缺点:不能调用到子类特有的方法
java
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",5);
Dog dog= new Dog("旺财",6);
//向上转型
Animal animal = cat;
animal.eat();
animal=dog;
animal.eat();
//animal.bark();
//编译失败,编译时编译器将animal当成Animal对象处理
//而Animal类中没有bark方法,因此编译失败
//cat=(Cat)animal;
//cat.mew();
//ClassCastExceptionclass: ani.Dog cannot be cast to class ani.Cat
dog=(Dog)animal;
dog.bark();
//animal本来指向的就是狗,因此将animal还原为狗也是安全的
}
}
向下转型用的比较少,而且不安全,万一转换失败,运行时就会抛异常,Java中为了提高向下转型的安全性,引入了instanceof,如果该表达式为true,则可以安全转换。
java
public class TestAnimal {
public static void main(String[] args) {
Cat cat = new Cat("元宝",5);
Dog dog= new Dog("旺财",6);
//向上转型
Animal animal = cat;
animal.eat();
// animal=dog;
// animal.eat();
if(animal instanceof Cat){
cat=(Cat)animal;
cat.mew();
}
if(animal instanceof Dog){
dog=(Dog)animal;
dog.bark();
}
}
}
多态的优缺点
使用多态的好处:
- 1.降低代码的"圈复杂度",避免使用大量的"if else"语句
例如,现在要打印多个形状,如果不基于多态,实现代码如下:
java
class Shape{
public void draw(){
System.out.println("画图");
}
}
class Rect extends Shape{
@Override
public void draw(){
System.out.println("♦");
}
}
class Cycle extends Shape{
@Override
public void draw(){
System.out.println("●");
}
}
class Flower extends Shape{
@Override
public void draw(){
System.out.println("❀");
}
}
public class DrawShape {
public static void main(String[] args) {
Rect rect =new Rect();
Cycle cycle = new Cycle();
Flower flower=new Flower();
String[] shapes ={"cycle","rect","cycle","rect","flower"};
for (String shape:shapes) {
if(shape.equals("cycle")){
cycle.draw();
}else if(shape.equals("rect")){
rect.draw();
}else if(shape.equals("flower")){
flower.draw();
}
}
}
}
如果使用多态,则不必写这么多if-else语句:
java
public class DrawShape {
public static void main(String[] args) {
Shape[] shapes ={new Cycle(),new Rect(),new Cycle(),new Rect(),new Flower()};
for (Shape shape:shapes) {
shape.draw();
}
}
}
- 2.可扩展能力更强
如果要增加一种新的形状,使用多态的方式。改动代码的成本也更低
java
class Triangle extends Shape{
@Override
public void draw(){
System.out.println("△");
}
}
对于类的调用者来说(DrawShape方法),只需要创建一个新的类就可以了,而不用多态的话,就要把DrawShape中的if-else进行修改,改动成本高。
避免在构造方法中调用重写的方法
下面这段代码略有小坑:
java
class B{
public B(){
func();
}
public void func(){
System.out.println("B.func()");
}
}
class D extends B{
private int num=1;
@Override
public void func(){
System.out.println("D.func()"+num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
- 构造对象D的同时,会调用B的方法。
- B的构造方法中调用了func方法,此时会除法动态绑定,会调用到D中的func。
- 此时D对象自身还没有构造,此时num处在未初始化的状态,值为0。
- 所以在构造函数内,尽量避免使用实例方法,除了final和private方法。
java
class B{
public B(){
func();
}
private void func(){
System.out.println("B.func()");
}
}
class D extends B{
private int num=1;
private void func(){
System.out.println("D.func()"+num);
}
}
public class Test {
public static void main(String[] args) {
D d = new D();
}
}
但注意,此时B和D类中的func方法就不是重写的关系了,而是重载关系。