【Java编程思想读书笔记】Java数据成员初始化顺序

Java数据成员的初始化顺序

112页有感

一、初始化对象的方式

在平时的编程中,对于数据成员的初始化是非常重要的,所以,Java尽量保证,在所有的对象使用之前都能得到正确的初始化,否则一个被随意赋值为一个垃圾值时非常不安全的。对于基本类型而言,所有的基本类型在使用之前都会得到一个初始值,具体如下

boolean false
char null
byte 0
short 0
int 0
long 0
float 0.0
double 0.0

可以看到,除了char类型被赋予null之外,其他的基本类型都被赋予了一个值0(如果把false也看成0的话)。但是并不是在所有的情况下,都能被正常的初始化的,若被定义成相对于一个方法的“局部”变
量,那么程序在编译时期将会不被通过,到不了初始化的阶段就会报一个错变量未被初始化的错误,举一个最简单的例子,for循环

void demo(){
    for(int i;i<10;i++){
        //something
    }
}
void test(){
    int i;
    i++;
}

在以上两种情况下,编译时期就会无法通过,因此综上所述,在大多数情况下,只要是通过了编译期,一个类的基本类型都会被正确的赋予一个初始值。而对于引用类型来说,在一个类的内部定义一个对象句柄时,如果不将其初始化成新对象,那个句柄就会获得一个空值,如果在没有初始化之前就要使用这个新的对象,将会在运行时期抛出异常。

总结:在使用对象之前,务必要对对象进行初始化操作,基本类型初始化默认值为0,boolean为false,字符串或者对象引用初始化为null。而初始化对象的方式主要有构造器初始化,变量定义的初始化,静态变量初始化,静态代码块初始化,代码块初始化

二、初始化顺序

2.1.默认初始化:

在一个类里,初始化的顺序是由变量在类内的定义顺序决定的。即使变量定义大量遍布于方法定义的中间,
那些变量仍会在调用任何方法之前得到初始化——甚至在构建器调用之前。例如:

 class initData {
    public static void main(String[] args) {
        Card card = new Card();
        card.func();
    }
}
class Tag{
    Tag(String flag){
        System.out.println("tag构造函数"+flag);
    }
}
class Card{
    Tag t1 = new Tag("tag1");
    Card(){
        System.out.println("Card构造函数");
         t3 = new Tag("tag300");
    }
    Tag t2 = new Tag("tag2");
    void func(){
        System.out.println("成员方法");
    }
    Tag t3 = new Tag("tag3");
}


输出:

tag构造函数tag1
tag构造函数tag2
tag构造函数tag3
Card构造函数
tag构造函数tag300
成员方法

可以看到,虽然这些变量定义分布在Card类的各个位置,甚至在Card类构造方法之前,但是还是优先执行的是变量定义,由此可见,变量定义的初始化顺序优先级比构造函数高,并且跟在代码的存在的顺序无关。在这里例子中,t3会被初始化两次,一次在构建器调用前,一次在调用期间(第一个对象会被丢弃,所以它后来
可被当作垃圾收掉)。从表面看,这样做似乎效率低下,但它能保证正确的初始化。

2.2.静态数据的初始化

如果对静态存储空间特性了解的,可以知道,静态存储是独占一块存储空间的,与具体的对象无关

静态存储区域也是位于RAM中,程序运行期间,静态存储的数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的

而被Static修饰的类,与具体的对象无关

以上为我上一篇《Java数据存储位置》博客中关于静态存储位置的介绍,详细的可以右转去那篇文章,言归正传,由于静态存储空间的独立性,并且只与类有关,因此,静态数据的初始化顺序,是优先于上面的变量或者对象说的初始化,当然也优先于构造器的初始化,如果是同为被static修饰的,不管是变量,还是代码块,代码中定义的顺序决定了他们的初始化顺序。代码就是最好的例子,看代码

public class StaticInitData {


    public static void main(String[] args) {
        System.out.println("t1引用前");
        t1.func("01");
        System.out.println("c1引用前");
        c1.func("01");
    }
    static  Table t1= new Table();
    static Cupboard c1 = new Cupboard();
}
class Bowl{
    Bowl(String flag){
        System.out.println("bowl构造方法"+flag);
    }

    void func(String flag){
        System.out.println("bowl成员方法"+flag);
    }
}

class Table{
    static Bowl b1 = new Bowl("bowl01");
    String flag = "---变量定义和代码块顺序的测试---";
    Table(){
        System.out.println("Table构造方法");
       b2.func("table构造方法调用");
    }
    static {
        System.out.println("Table静态代码块执行");
    }

    {
        System.out.println("Table普通代码块执行"+flag);
    }
     static  Bowl b2 = new Bowl("bowl02");
    void func(String flag){
        System.out.println("Table成员方法"+flag);
    }
}
class Cupboard{

    Bowl b3 = new Bowl("bowl03");
    static  Bowl b4  = new Bowl("bowl04");
    Cupboard(){
        System.out.println("cupboard构造方法");
        b4.func("cupboard构造方法调用");
    }
    void func(String flag){
        System.out.println("cupboard成员方法"+flag);
    }
    static  Bowl b5  = new Bowl("bowl05");
}


输出:

bowl构造方法bowl01
Table静态代码块执行
bowl构造方法bowl02
Table普通代码块执行---变量定义和代码块顺序的测试---
Table构造方法
bowl成员方法table构造方法调用
bowl构造方法bowl04
bowl构造方法bowl05
bowl构造方法bowl03
cupboard构造方法
bowl成员方法cupboard构造方法调用
t1引用前
Table成员方法01
c1引用前
cupboard成员方法01

可以看到,被所有被static修饰的对象是首先被初始化的,优先级比该类中的构造方法还高,然后再是默认的对象定义,然后再是构造函数。另外static只有在必要的时候才会初始化,只有在创建了第一个 Table 对象之后(或者发生了第一次static 访问),它们才会创建。在那以后,static Table对象不会重新初始化。因为占的都是同一块存储空间。详情可见《Java数据存储位置》博客中关于静态存储位置的介绍。

并且对于代码块而言,静态代码块快执行顺序高于普通代码块,从输出这一句:

“Table普通代码块执行---变量定义和代码块顺序的测试---”

可以看出来普通变量定义的顺序高于普通代码块,这些在执行结果中都可以看得出来,

三、执行顺序总结

至此,总结一下Java数据的初始化顺序,拿一个类的创建过程来举例 假设有一个Dog类,如果有父类将会先初始化父类,(原因:没有父亲就没有儿子,用于父类的构建器肯定在一个子类的构建器中调用,而且逐渐向上链接,使每个父类使用的构建器都能得到调用。之所以要这样做,是由于构建器负有一项特殊任务:检查对象是否得到了正确的构建。一个子类只能访问它自己的成员,不能访问父类的private成员(这些成员通常都具有private 属性)。只有父类的构建器在初始化自己的元素时才知道正确的方法以及拥有适当的权限。所以,必须令所有构建器都得到调用,否则整个对象的构建就可能不正确。那正是编译器为什么要强迫对子类的每个部分进行构建器调用的原因。在子类的构建器主体中,若我们没有明确指定对一个父类构建器的调用,它就会“默默”地调用
默认构建器。如果不存在默认构建器,编译器就会报告一个错误(若某个类没有构建器,编译器会自动组织
一个默认构建器))

  1. 当一个Dog类首次被创建,或者这个类的静态方法或者静态字段被首次访问的时候,Java解释器将会在类路径找到Dog.class
  2. 在找到class文件之后(会创建一个Class对象,注意是Class对象,不是Dog对象,这里的知识和反射有关)。所有的static模块都会被运行,并且只被加载一次,即在class对象首次被载入的时候
  3. 创建一个new Dog(),会在堆内存为Dog分配一个足够的内存
  4. 将Dog 中的所有基本类型设为它们的默认值(零用于数字,以及 boolean 和
    char 的等价设定)。
  5. 将类中所有引用对象的初始化都会被执行(例如这个对象中有new Cat(),String s = "xxx"这种操作,那就会在这一步执行)
  6. 执行所有的普通代码块的初始化
  7. 执行构造器。这实际可能要求进行相当多的操作,特别是在涉及继承的时候,将会先执行父类

所以数据初始化的顺序,遵循先父后子,先静态后普通,先变量后普通代码块,最后构造 的思路,可以得出顺序

父类静态变量= 父类静态代码块 -》子类静态变量 =子类静态代码块-》父类非静态变量-》父类非静态代码块-》父类构造方法-》子类非静态变量-》子类非静态代码块-》子类构造方法)
    
    这里为什么是等于呢,原因上面其实提到了,在同一个类里面的被static修饰的,初始化顺序取决于在代码中的定义顺序,所以在这里认为他们优先级是一致的


完。

posted @ 2020-02-06 16:59  穿黑风衣的牛奶  阅读(391)  评论(0编辑  收藏  举报