变量的初始化,继承后方法的执行顺序,多态的作用,三大特征的优点,对象的向上转型和向下转型,虚方法和非虚方法(十一)

278 阅读9分钟

成员变量初始化

1.成员变量分为两种

(1)静态变量,又称为类变量。它是用static修饰的。

(2)实例变量,就是非静态成员变量,它是没有static修饰的。

这两种成员变量的初始化位置和时机都是不同的。

2.静态变量的初始化

(1)什么时候进行的静态变量的初始化?

在类加载之后,对类进行初始化时发生的。

当第一次使用某个类时,如果发现这个类没有加载到内存,那么会先将它加载到内存。

(2)初始化的执行特点

​ 一个类的初始化只会发生一次。只执行一次。

​ 如果子类初始化时,发现父类没有初始化,会先初始化父类,再初始化子类;如果父类初始化过了,就不会再初始化父类了。

(3)哪些代码负责静态变量的初始化

​ ①静态变量声明时的显式赋值

​ ②静态代码块中的语句

class Demo{
    static int a = 1;//静态变量显式赋值
    static int b;

    static{
        //静态代码块
        System.out.println("a = " + a);//1
        System.out.println("b = " + b);//0
//        System.out.println("c = " + c);//这样语法检查不通过,编译器本身也是程序,它的逻辑当中会认为这样是错误的
        System.out.println("c = " + Demo.c);//可以通过"类名."进行访问
        a = 2;
        b = 2;
    }

    static int c = 1;

    static{
        System.out.println("c = " + c);
    }
}

(4)底层的原理:一个类在初始化时,本质上是执行<clinit>()方法

​ cl:class 类

​ init:initialize 初始化

​ clinit()方法不是程序员手动声明,它是由编译器根据程序员写的上面的A,B两部分代码“按顺序”组装而成的。 ​ 也就是说上面的Demo类在编译后会变成:

class Demo{
    //静态变量的声明
    static int a;
    static int b;
    static int c;

    void clinit(){
        a = 1;
        System.out.println("a = " + a);//1
        System.out.println("b = " + b);//0
        System.out.println("c = " + Demo.c);//可以通过"类名."进行访问,0
        a = 2;
        b = 2;
        c = 1;
        System.out.println("c = " + c);
    }
}

实例变量初始化

1.什么时候进行的实例变量初始化?

在new对象时

2.初始化的执行特点。

(1)每次new对象都要执行

(2)子类实例初始化时也会执行父类实例初始化相关的代码

子类的构造器中一定会调用父类的构造器,其中本质上就是调用父类中实例变量初始化相关的代码。

3.哪些代码负责实例变量的初始化

(1)实例变量声明时的显式赋值表达式

(2)非静态代码块

(3)构造器

​ ①:super(),super(实参列表),this(),this(实参列表),如果构造器没有写上面的代码,默认时super()

​ ②:构造器中剩下的代码。

(4)底层的原理

每一次new对象时,实际上是执行对应的()方法

<init>方法不是由程序员手动声明的,而是根据上面的(1)(2)(3)部分代码组装而成的:

​ Ⅰ.组装上面的(3)①

​ Ⅱ.组装上面的(1)(2),他俩按照编写的顺序组装

​ Ⅲ.组装上面的(3)②

注意:我们如果给某个类编写了多个构造器,那么将会有多个<init>方法,即有几个构造器,就有几个<init>方法。它们的形参列表不同。组装时,上面(3)①和(3)②根据不同的构造器,放到对应的<init>方法中,上面(1)和(2)每一个放一份。

class Data{
    int a = 1;//实例变量声明时的显式赋值表达式 a=1

    {
        //非静态代码块,又称为构造块,实际开发中很少使用
        System.out.println("Data非静态代码块1");
        System.out.println("a = " + a);
       // System.out.println("b = " + b);//编译报错,编译器检查语法时,是遵循“前向引用”
        System.out.println("b = " + this.b);
        a = 2;
    }

    Data(){
        a = 3;
        b = 3;
        System.out.println("无参构造");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }

    int b = 1;
    {
        //非静态代码块
        System.out.println("非静态代码2");
        System.out.println("a = " + a);
        System.out.println("b = " + b);
    }
}
class SubData extends Data{
    {
        //非静态代码块,又称为构造块,实际开发中很少使用
        System.out.println("SubData非静态代码块");
    }
}

例如:上面的Data类组装后是这样的

 class Data{
    int a;
    int b;
	void /<init>(){
    super();//省略了,也是存在的,它是访问默认父类的实例初始化代码,默认父类是Object类

    a= 1;
    //非静态代码块,又称为构造块,实际开发中很少使用
    System.out.println("Data非静态代码块1");
    System.out.println("a = " + a);
   // System.out.println("b = " + b);//编译报错,编译器检查语法时,是遵循“前向引用”
    System.out.println("b = " + this.b);
    a = 2;

    b = 1;

     //非静态代码块
    System.out.println("非静态代码2");
    System.out.println("a = " + a);
    System.out.println("b = " + b);

    a = 3;
    b = 3;
    System.out.println("无参构造");
    System.out.println("a = " + a);
    System.out.println("b = " + b);
	}	
}

执行顺序

当我们第一次使用某个类时,就是在new对象,那么会先完成类的初始化,然后在进行实例初始化。 (1)父类的初始化(只会执行一次)

(2)子类的初始化(只会执行一次)

(3)父类对应的实例初始化代码

(4)子类对应的实例初始化代码

先后顺序:

父类静态代码块

子类静态代码块

父类构造代码块

父类构造方法

子类构造代码块

子类构造方法

多态

1.什么是多态?

字面上来说,多种形态。

在Java中,多态是指,某个对象(准确来说是变量)表现为编译时

2.如何才能体现出多态?

多态引用:

父类的类型 变量名 = 子类的对象;

​ 此时这个变量,就会出现编译时类型与运行时类型不一致的情况。

3.表现结果?

编译时,该变量按照父类类型进行编译,只能调用父类中声明的方法等成员。

运行时,按照子类类型进行运行,方法被子类重写过的话,是执行子类重写的代码。

4.多态有哪些应用?

多态的应用在于:

(1)多态的数组

​ 声明数组时,元素的类型指定为父类类型,实际存储的对象是子类对象。

Person[] array = new Person[5];
    array[0] = new Woman(); //左边的array[0]的类型是Person类型,右边是实际存储的对象,Woman对象

(2)多态的参数

​ 声明方法时,形参的类型声明为父类类型,实际调用方法时,传入的实参是子类的对象。

 声明:public void method(Person person){...}    (Person person)形参
    调用:method(new Woman());                      (new Woman())实参

(3)多态的返回值

方法的返回值类型声明为父类的类型,实际返回值时子类的对象。

 声明:public Person method(){
            ...
            return new Woman();
        }

(4)总结:多态可以使代码更灵活,功能更强大。

面向对象三个基本特征(封装、继承、多态)的好处

1.封装:安全、隐藏细节、简便

2.继承:代码复用性提高、代码扩展能力提高

3.多态:使得代码更灵活,使用更方便。

向上转型和向下转型

1.什么是向上与向下转型?

他们都是属于类型的转换。

向上转型:把一个对象/变量类型自动提升为父类的类型。

向下转型:把一个对象/变量类型强制类型转换为子类的类型。

注意:向上转型与向下转型都只是针对编译时类型来说的,运行时类型不会改变。

Java的基本数据类型的转换:

(1)自动类型转换‘

byte->short->int->long->float->double

​ char->

(2)强制类型转换:

double->float->long->int->short->byte

​ char->

2.向上转型

(1)什么情况下会发生向上转型?

​ 当把子类对象/子类类型变量赋值给父类的变量时,自动就会升级为父类的类型,就是多态引用时就发生了向上转型。

(2)向上转型后有什么问题?

通过父类的变量

3.向下转型

(1)什么情况下会发生向下转型?

当如果某个子类的对象想要调用子类“扩展”的成员时,就必须对刚刚向上转型的变量进行向下转型

(2)向下转型后有什么问题?

可能发生ClassCastException:类型转换异常

4、如何避免向下转型报错?

可以使用instanceof关键字进行判断后再转型

5、哪些情况下instanceof关键字判断是返回true的呢?

变量/对象 instanceof 类型

变量/对象的运行时类型 <= 类型就返回true。

方法的分类(虚方法与非虚方法)

之前学习方法,说分为:静态方法和非静态方法。

现在说方法,从底层分为:

1.非虚方法:编译期间就能确定调用哪个的方法

​ A:静态方法

​ B:私有方法等

​ C:final方法

/*
结论:对于非虚方法来说,只看编译时类型。
 */
public class TestMethod {
    public static void main(String[] args) {
        Base base = new Sub();
        base.method();//base编译时类型是Base,运行时类型是Sub类型
                     //这么写容易误解,以为调用的是子类重写的method方法
                    //为了避免这些误会,建议静态方法使用“类名."进行调用

        Sub.method();//执行子类的method
        Base.method();//执行父类的method

        Base.test();//执行父类的test
        Sub.test();//执行父类的test  静态方法可以继承到子类,但是不能重写
    }
}
class Base{
    public static void method(){
        System.out.println("父类静态方法");
    }
    public static void test(){
        System.out.println("父类test静态方法");
    }
}
class Sub extends Base{

    /*
    method()方法不是重写父类的方法,它是相当于子类自己的静态方法,和父类没什么关系
     */
//    @Override  //建议重写的方法上面都加上@Override,编译器会帮你校验重写方法是否满足它的要求,不满足要求就会报错
    public static void method(){
        System.out.println("子类静态方法");
    }
}

2.虚方法:需要在运行期间才能确定最终执行的是哪个方法

简单来说,虚方法就是指可以被重写的方法,除了虚方法之外的都是非虚方法。

虚方法: xx.方法

(1)静态分派

​ 编译时看xx.的编译时类型,在编译时类型中去匹配合适的方法。

​ 匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度

​ A:找最匹配 实参的编译时类型 = 方法形参的类型

​ B:找兼容 实参的编译时类型 < 方法形参的类型

(2)动态绑定

​ 运行时看xx.的运行时类型,在运行时类型中看是否重写了刚刚找到的匹配方法,

​ 如果有重写,就执行重写的代码,否则就执行刚刚在编译时类型中找到的匹配方法的代码

需要格外注意的是:静态分派->动态绑定,是有先后顺序的,即使子类中有参数列表匹配度更高的方法,系统也是不会直接去调用这个方法的,而是先从父类中找参数类型兼容的方法,然后看这个方法在子类中有没有被重写。如果被重写了,那就执行这个被重写的方法;如果没有被重写,那么它依旧执行父类中的这个方法。

/*

 */
public class TestVirtual {
    public static void main(String[] args) {
        Fu f = new Zi();
        f.method();//(1)静态分派,f的编译时类型是Fu,去Fu类中找method()
                    //(2)动态绑定,f的运行时类型是Zi,去Zi中看是否有对method()进行重写,如果有,就执行重写的
        System.out.println("---------------");
        BaBa b = new ErZi();
        b.method();//(1)静态分派,b的编译时类型是BaBa,去BaBa类中找method()
                //(2)动态绑定,b的运行时类型是ErZi,去ErZi中看是否有对method()进行重写,如果有,就执行重写的,如果没有重写,就执行BaBa类中的
    }
}

class Fu{
    public void method(){
        System.out.println("父类的method方法");
    }
}
class Zi extends Fu{
    public void method(){
        System.out.println("子类的method方法");
    }
}

class BaBa{
    public void method(){
        System.out.println("爸爸类的method方法");
    }
}
class ErZi extends BaBa{
    //不是重写
    public void method(int a){
        System.out.println("儿子类的method方法");
    }
}