黑马程序员-Java继承和多态

——Java培训、Android培训、iOS培训、.Net培训、期待与您交流! ——-
目录:
1. 继承
2. 多态
3. 抽象类
4. 接口


1. 继承

继承是面向对象的重要基本特性之一,它通过现有的类创建新的类,新的类继承了父类的成员和方法,而自身又可以增加自己的成员和方法。通过继承的得到的类被称为子类,被继承的类被称为父类(基类、超类)。
继承有效的实现了代码的复用,子类只需要添加新的代码即可。Java语言不支持多继承,只能有一个父类。

继承通过关键字extends 来声明一个类继承另一个类。
例如:

class Child extends Father{
}

class Father{
}

注:一个类没有声明任何父类,那么它会默认Object类为其父类。
Object类是Java类体系中的根类,每个类都是Object的子类。也就是说每个类都拥有Object类的方法。

(1)性质

  • 继承的定义
    子类成员中有一部分是子类自己声明定义的,另一部分是从它的父类继承来的。子类继承父类的成员变量作为自己的一个成员变量,就好像它们是在子类中直接声明一样,可以被子类中的自己声明的任何实例方法操作。

  • 子类和父类在同一包中的继承性
    如果子类和父类在同一包中,那么子类会继承父类中不是private修饰的成员,继承后的成员访问权限保持不变。

  • 子类和父类不在同一包中的继承性
    如果子类和父类不在同一包中,那么子类会继承父类中protected、public修饰的成员,继承后的成员访问权限保持不变。子类不能继承父类的默认访问权限的成员,也就是不能继承父类中未加权限修饰符声明的成员。

(2)子类对象的构造

子类继承父类后,子类构造方法中会隐含一个super()语句,这样在子类创建时,父类的初始化就得到了保证。

class Child extends Father{
    Child(){
        //此处隐含super()语句,而父类只有带参构造super(int i),并没有声明无参构造,所以会报错
    }
    public static void main(String[] args){
        Child child=new Child();
    }
}

class Father{
    Father(int i){
        System.out.println(i);
    }
}

以上例子是错误的,运行结果为:
这里写图片描述
由此反证出子类构造器隐含这样一条语句:
super();

用子类的构造方法创建一个子类对象时,子类的构造方法总是先调用父类的某个构造方法。
我们通过下面的代码来观察子类对象的创建过程中,子类和父类成员的初始化过程。

class Father{
    String name="Most_want";//实例成员
    static String country="中国";//静态成员
    //构造初始化块
    {
        System.out.println(name);
        System.out.println("我是父类构造初始化块");
    }
    //类初始化块
    static{
        System.out.println(country);
        System.out.println("我是父类类初始化块");
    }
    //无参构造方法
    Father(){
        System.out.println("我是父类无参构造方法");
    }
}

class Child extends Father{
    String game="Game";
    static int age=23;
    //构造初始化块
    {
        System.out.println(game);
        System.out.println("我是子类构造初始化块");
    }
    //类初始化块
    static{
        System.out.println(age);
        System.out.println("我是子类类初始化块");
    }
    //无参构造方法
    Child(){
        System.out.println("我是子类无参构造方法");
    }

    public static void main(String[] args){
        Child child=new Child();
    }

}

初始化结果

由此可以看出来对于有继承的类初始化顺序为:
父类静态成员—>父类类初始化块—>子类静态成员—>子类类初始化块—>父类实例成员—>父类构造方法—>子类实例成员—>子类构造方法。

(3)方法的重写

子类中定义一个方法,这个方法和父类中的方法名,返回类型,参数完全一样。这就是方法的重写。子类通过方法重写会把从父类继承的方法隐藏(覆盖)。子类调用该方法则是调用重写后的方法。
同样的父类的成员变量也可以被隐藏,当在子类中定义的成员变量只要和父类中的成员变量同名,子类就隐藏了父类的成员变量。

class Child extends Father{
    int weight =80;

    public void show(){   //重写的父类方法
        System.out.println("Child weight:"+weight);
    }

    public static void main(String[] args){
        Child child=new Child();
        child.show();
    }

}

class Father{
    Double weight=140.5 ;

    public void show(){
        System.out.println("Father weight:"+weight);
    }
}

/*输出结果为Child weight:80
可见父类的被重写成员被隐藏了*/

如果想调用父类被隐藏的成员,这时候就需要用到关键字super。
例如上例通过使用super之后:

class Child extends Father{
    int weight =80;

    public void show(){   //重写的父类方法
        System.out.println("Child weight:"+weight);
        super.show();//用super调用父类方法
    }

    public static void main(String[] args){
        Child child=new Child();
        child.show();
    }

}

class Father{
    Double weight=140.5 ;
    public void show(){
        System.out.println("Father weight:"+weight);
}

输出结果为:
Child weight:80
Father weight:140.5//通过super调用的方法结果

通过此例子还可以发现,父类被隐藏的成员变量可以通过父类的方法调用出来。

this和super区别

this关键字有两个用途:一是引用隐式参数,二是调用该类的其他构造方法。
super关键字的用途:一是调用超类构造方法,二是调用超类方法。

区别:
this是一个对象的引用。而super只是一个表示调用超类方法的特殊关键字,不能把super赋值给另一个变量。

(4)final关键字
final修饰的类不能被继承,被final修饰的方法不能被重写。

(5)继承原则
继承中不能因为要使用某个类的方法或成员就使用extends声明继承关系。继承中通常使用“is-a”原则来判断一个类是否是另一个类的子类。

2. 多态

(1)对象的向上转型

在下例中,我们创建了一个Person类作为超类,Chinese类,American类,分别继承Person类。
在Test中,分别创建了Chinese类的chinese实例,Person类的p实例,因为Chinese继承了Person类,所以p实际上接受的是一个Chinese类对象。而这时候其实就发生了对象的向上转型,即用父类引用接收子类对象。
有了向上转型,也就有了向下转型。向下转型格式如下例子中演示:

public class Test{
    public static void main(String[] args){
        Chinese chinese=new Chinese();//创建一个中国人
        Person p=new Chinese();//向上转型
        Chinese c=(Chinese)p;//向下转型
    }
}

class Person {
    public void eat(){}
}

class Chinese extends Person{
    public void eat(){
        System.out.println("中国人在吃北京烤鸭");
    }
}

class American extends Person{
    public void eat(){
        System.out.println("美国人在吃汉堡");
    }
}

向上转型有如下特点:

  • 向上转型后该对象不能操作子类中本身声明的成员变量。
  • 向上转型后该对象调用重写的方法是调用子类重写后的方法。
  • 向上转型后该对象可以调用子类继承的成员变量和隐藏的成员变量。

(2)多态

继续用上面的例子来说明
通过向上转型的特点我们得知向上转型后该对象调用重写的方法是调用子类重写后的方法。
所以在下例中输出的结果是:
中国人在吃北京烤鸭
美国人在吃汉堡

public class (){
    public static void main(String[] args){
        Person p1=new Chinese();//创建一个Chinese对象并向上转型
        Person p2=new American();//创建一个American对象并向上转型

        p1.eat();//中国人在吃北京烤鸭
        p2.eat();//美国人在吃汉堡
    }
}

根据多态原则,一个类中超类出现的地方可以用子类替换,这就是置换法则,即“is-a”原则的另一种表述。

(3) 动态绑定
上例中,多态的实现其实依赖于动态绑定来调用子类重写的方法。下面是多态的实现过程:

1)编译器获取可能被调用的候选方法。
编译器查看对象的声明类型和方法名。编译器会列举该对象中以及超类(访问属性为public)中可用的并且与当前调用的方法名相同的方法。
2)重载解析
查看当前调用方法提供的参数类型。如果在候选方法中存在一个与提供的参数类型完全匹配的方法,就选择这个方法。
3)如果是private方法,static方法,final方法或者构造器,那么编译器可以准确知道调用了哪个方法,这种调用方式成为静态绑定。
4)程序运行并采用动态绑定调用方法时,虚拟机将调用最合适的方法,比如在多态中,将调用子类重写的方法。
5)虚拟机对方法搜索进行了优化,它为每个类预先创建了一个方法表,其中列出了所有方法的签名和实际调用的方法。在真正调用方法的时候,直接查找这个表即可。

(3)多态示例
先联想下经典的俄罗斯方块,不管是一字形,还是正方形,还是L形,它们都属于方块。
所以我们可以创建以下具有继承关系的类。
这时候就利用了多态:可以看下结果。

public class Test{
    public static void main(String[] args){
        FK y=new YFK();
        FK l=new LFK();
        FK z=new ZFK();
        //下面的方法调用就是多态
        y.change();//一字形方块在变形
        l.change();//L形方块在变形
        z.change();//正方形方块在变形
    }
}
//这是方块类超类
class FK{
    public void change(){}
}
//一字形方块类
class YFK extends FK{   
    public void change(){
        System.out.println("一字形方块在变形");
    }
}
//L形方块类
class LFK extends FK{   
    public void change(){
        System.out.println("L形方块在变形");
    }
}
//正方形方块类
class ZFK extends FK{   
    public void change(){
        System.out.println("正方形方块在变形");
    }
}

结果:
多态结果

3. 抽象类

(1)抽象类的声明
在类的继承层次中,位于顶端的类更具有通用性或者抽象性。这时候可以把该类设计为抽象类。抽象类是对多个类的抽象,用关键字abstract声明抽象类。

抽象类中可以有抽象方法,也可以有非抽象方法。
抽象方法是只有方法声明不含方法体的方法,用关键字abstract修饰。
例如:

abstract class Person{
    //抽象方法,只包含方法声明部分
    abstract show();
}

抽象类中可以有具体方法和成员变量。
抽象类不能被实例化。即不能通过new关键字创建抽象类的对象。
如果一个非抽象类的父类是一个抽象类,那么它必须重写所以父类的抽象方法,同时加上方法体。这样通过多态就可以调用子类重写的方法。

注意:抽象类不能用abstract和final同时修饰一个方法。因为用abstract修饰后的方法为抽象方法,只有通过重写才能被实现,所以不能用final修饰。

(2)抽象类的使用
扩展抽象类,由于抽象类不能被实例化,所以只能通过扩展它,创建它的子类对象来使用。
扩展抽象类有两种方法:一是在子类中部分定义抽象方法,此时子类仍然是一个抽象类;二是在子类中全部定义抽象方法,这时可以创建子类对象来使用。

4. 接口

Java不支持多继承性。单继承使得Java简单,易于管理和安全。为了克服单继承的局限性,Java提供了接口,一个类可以实现多个接口。

(1)接口的声明
用关键字interface来声明一个接口。例如:

interface Area{
    double getArea();
}

接口中只能包含抽象方法和常量。
注意:接口中方法默认是public abstract来修饰,在接口中声明方法时可以省略它们。
但是实现类在重写方法时候一定要用public来修饰,否则会以降低方法权限而异常。

(2)接口的使用
一个类通过关键字implements来声明自己实现了一个或多个接口。多个接口之间用逗号隔开。
例如:

interface Area{
    double getArea();
}

interface Longs{
    double get Longs();
}
//实现多个接口
class Circle implements Area,Longs{
    //定义接口中的方法
    public double getArea(){
        return 100.2;
    }
    //定义接口中的方法
    public double Longs(){
        return 100.5;
    }
}

(3)接口回调

其实是多态的一种体现。
接口回调是指:可以把使用某一接口的子类对象向上转型为接口,通过接口来调用子类对象所实现的方法,不同的子类实现即有不同的实现方法。

例如:
存在Person接口:

interface Person{
    void show();
}

通过匿名内部类创建两个子类:

public class Test{
    public static void main(String[] args){
        //匿名内部类使用接口变量student实例化
        Person student=new Person(){

            @Override
            public void show() {//定义接口的抽象方法
                System.out.println("我是一名学生");
            }

        };
        //接口回调  ,调用子类定义的方法
        student.show();//打印我是一名学生

        //匿名内部类使用接口变量teacher实例化
        Person teacher=new Person(){

            @Override
            public void show() {//定义接口的抽象方法
                System.out.println("我是一名教师");
            }

        };
        //接口回调  ,不同的子类可以有不同的实现
        teacher.show();//打印我是一名教师
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值