类的编译期与运行期

非静态内部类

非静态内部类到底可以有静态属性吗?

static成员变量,或者static final常量

非静态内部类不可以有static修饰的成员变量,以及不可以有static final修饰的除基本数据类型和字符串直接量声明的String以外的属性。

import java.util.*;
public class OuterClass {
    static int k = printI();

    static int printI () {
        System.out.println("Inner class is loading before " +
            "creating OuterClass instance!");
            return InnerClass.i;
    }

    OuterClass () {
        System.out.println("static k is " + k);
        System.out.println("OuterClass constructor");
    }

    class InnerClass {
        private static final int i = 1;
        static String s = new String("abc");
        static Date date = new Date();
    }
}

Console输出结果:

非静态内部类属性不可以有的静态字段.png

上述两种情况都是在内部类加载过程中的初始化过程中必须完成初始化。由于是非静态内部类,所以访问该内部类一定要持有一个外部类的引用。同样的道理,初始化该内部类上述哪一种成员变量,都需要读取、设置它们,而读取和设置它们必须让内部类持有一个外部类引用。而此时处于外部类和内部类加载阶段,没有任何实例化,也就没有外部类引用。这样编译时无法通过。

参考

浅谈非静态内部类不能有静态成员

为什么java非静态内部类可以有static final的数据成员?

static final基本类型和String

类加载过程有三个,加载、连接、初始化。
非静态内部类,可以有static final修饰的基本数据类型和通过字符串声明的String实例。这是因为它们属于编译期常量。所以在编译期进行初始化,将它们存放在JVM运行时内存结构中的方法区的运行时常量池。这里对应的就是类加载过程中的第一阶段加载

内部类的加载和外部类实例化先后

外部类&内部类

public class OuterClass {
    static int k = printI();

    static int printI () {
        System.out.println("Inner class is loading before " +
            "creating OuterClass instance!");
            return InnerClass.i;
    }

    OuterClass () {
        System.out.println("static k is " + k);
        System.out.println("OuterClass constructor");
    }

    class InnerClass {
        private static final int i = 1;
    }
}

代码中在外部类编写了静态变量k的声明,用于定位外部类初始化时机,而在内部类中含有一个基本类型的静态常量i声明,用于定位内部类的加载时机,最后在外部类的构造函数中打印k的值,从而比较内部类加载和外部类实例化时机的先后。

主方法类

public class TestInnerClassLoader {
    public static void main(String[] args) {
        OuterClass o = new OuterClass();
    }
}

Console输出如下:

内部类的加载在外部类实例化之前.png

可以看到内部类的加载在外部类实例化之前,更进一步说明非静态内部类不可以有静态变量,和static final修饰的除基本数据类型以及字符串直接量声明的String属性。

这样好像并不能确定内部类的初始化阶段在外部类实例化之前。

外部类不可拥有static final修饰的non static InnerClass成员

public class TestClassLoader{
    static final B a = new B();
    
    class B {

    }
}

Console输出:

非静态内部类,不可以用static final修饰作为外部类的属性.png

这样是编译不通过的。这是因为static修饰的成员,必须在类加载中的初始化阶段进行初始化。但是要初始化该常量必须使用内部类构造函数在堆内存中实例化,而内部类属于非静态内部类,必须持有一个外部类引用才可以使用它的构造函数。而此时处于类加载的初始化阶段,没有外部类实例作为内部类持有的引用。

编译期&运行期

编译期在类加载过程中的加载阶段。而运行期主要指类加载过程中的初始化阶段。

编译期常量

加载阶段(编译期)完成以下操作:

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构转换化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
public class TestClassLoader{
    public static void main(String[] args) {
        //System.out.println(Data.i); //1
        //System.out.println(Data.integer); //initialization!  2
        //System.out.println(Data.booleanObject); //initialization!  true
        //System.out.println(Data.b); //true
        //System.out.println(Data.str); //abc
        //System.out.println(Data.s); //initialization!  abc
        //System.out.println(Data.a); //initialization!  A@7852e922 堆内存中的地址
        System.out.println(Data.e); //initialization!   A
    }
}

class Data {
    static {
        System.out.println("initialization!");
    }
    public static final int i = 1;
    public static final Integer integer = 2;
    public static final Boolean booleanObject = true;
    public static final boolean b = true;
    public static final String str = "abc";
    public static final String s = new String("abc");
    public static final A a = new A();
    public static final Enum e = E.A;
}

enum E {
    A,B,C,D,E,F,G;
}

class A {

}

触发类加载过程的初始化阶段,可以通过读取或设置静态字段(被final修饰,已经在编译期吧结果放入常量池的静态字段除外)触发类的初始化阶段。上述代码就是通过读取类的静态字段来判断到底哪些是编译期常量(执行静态代码块一定是在初始化阶段)。

主方法中每一行代码的注释表示Console输出结果。

可以看到只有通过static final修饰的基本类型数据或者用字符串字面量直接声明的String的类属性,属于编译期常量。

编译期可以将编译期常量代入到任何用到它的计算式中,也就是说可以在编译期执行计算式。同时需要注意的是编译期常量必须要在声明时进行初始化。

参考

java编译期常量

运行期初始化

初始化阶段属于类加载过程的最后一个阶段。主要用于显示的初始化类变量(static修饰的变量),执行静态代码块。

测试类

public class TestClassInit {
    public static void main(String[] args) {
        //System.out.println(ClassInit.a); //编译不通过,提示可能尚未初始化变量a
        ClassInit object1 = new ClassInit();
        ClassInit object2 = new ClassInit();
    }
}

准备阶段

从类的加载到类的初始化过程中,还有一个过程称为“连接”。在连接过程中,有一个准备阶段,用于初始化类变量(static修饰的变量)系统默认值(例如整型默认值是0...)。当然也有特殊情况,public static final value = 123;该变量会根据常量池中的值初始化为123。

初始化过程

初始化阶段对类变量显示的赋值,并且执行静态代码块。

import java.util.*;
public class ClassInit {
    static final int a;
    static final Date date;
    //static int i;
    static int i = 1;
    static {
        a = 7;
        date = new Date();
        System.out.println("i = " + i);
        System.out.println("static block is executed!");
    }

    int j;
    final int z;

    {
        //a = 7;
        //date = new Date();
        System.out.println("j = " + j);
        //System.out.println("z = " + z); //final z not default init.
        System.out.println("non static block is executed!");
    }



    public ClassInit() {
        //System.out.println("j = " + j);
        i = 5;
        //a = 7;
        //date = new Date();
        z = 6; //if final z not init ,can init here.
        System.out.println("ClassInit Object Constructor");
    }

}    

这样是可以编译通过的。运行测试类后可以看到,Console输出如下:

运行期变量初始化.png

可以看到当static block执行时,类变量已经被赋值为1,而不是准备阶段的0(当然也可以在实例化过程中显示赋值,或者在类方法中修改)。而且在测试类中创建了两个ClassInit对象,但是只执行了一次static block。这是因为类加载的过程只有一次,所以static block只会执行一次。

这里看到还有ClassInit还有两个属性adate。被static final修饰的类变量,不会在准备阶段赋予JVM默认的值,而必须进行显示的赋值。但是又不能在ClassInit实例化过程中赋值(会提示无法为最终变量xxx分配值),所以只能够在初始化阶段显示的赋值(声明中赋值,或者static block中赋值)

实例化

import java.util.*;
public class ClassInit {
    static final int a;
    static final Date date;
    //static int i;
    static int i = 1;
    static {
        System.out.println("static block is executed!");
        a = 7; //static final 常量只有显示的初始化,没有准备阶段的初始化,而且不能在实例化过程中初始化
        date = new Date();//同上
        System.out.println("i = " + i);
    }

    int j;
    final int z;

    {
        //a = 7;//提示无法为最终变量a分配值
        //date = new Date();//提示无法为最终变量date分配值
        //System.out.println("j = " + j); //输出为0,实例化过程中的默认初始化
        //System.out.println("z = " + z); //final z not default init.final修饰的实例常量没有默认初始化阶段。
        System.out.println("non static block is executed!");
    }



    public ClassInit() {
        System.out.println("j = " + j); //j = 0
        j = 5;
        //a = 7;//提示无法为最终变量a分配值
        //date = new Date();//提示无法为最终变量date分配值
        z = 6; //if final z not init ,can init here.如果没有在声明中初始化,那么必须在构造器执行结束之后必须被初始化
        System.out.println("ClassInit Object Constructor");
    }
}

一个类实例化过程主要执行实例域的默认初始化(赋予系统默认的值)和显示初始化,执行非静态代码块,最后执行构造函数在堆内存中生成一个实例对象。执行测试类,Console输出:

实例化过程.png

从输出日志可以看出,实例化过程首先执行了类实例域的初始化(包括默认初始化和声明中的显示初始化),非静态代码块,最后执行构造函数。而且在测试类中实例化了两个ClassInit对象,执行了两次非静态代码块。

代码中还有一个final修饰的实例域,在实例化过程第一步的实例域初始化并不会执行默认的初始化。如果没有在声明中显示的赋值,那么必须在构造器执行结束之后,该实例域已经被赋值,否则编译不通过。

子父类分层加载

父类

public class X {
    static String xVar = "X Value";
    Y b = new Y();
    static {
        System.out.println("X static block executed!");
        System.out.println(xVar);
        xVar = "static value";
    }
    X() {
        System.out.println("X construction executed!");
        System.out.println(xVar);
        xVar = "x value changed!";
    }

    //X(int i) {
    //    System.out.println("X parameters construction executed!");
    //}
}

子类

public class Z extends X{
    Y y;
    static {
        System.out.println("Z static block executed!");
    }

    {
        System.out.println("Z non static block executed!");
        y = new Y();
        y.show();
    }

    Z() {
        //super(1);
        System.out.println("Z construction executed!");
    }

    public static void main(String[] args) {
        System.out.println(new Z().xVar);
    }
}

Y类

public class Y {
    String yVar = "Y value";
    Y() {
        System.out.println("Y construction executed!");
        System.out.println(yVar);
        yVar = "y value changed!";
    }

    void show() {
        System.out.println(yVar);
    }
}

作用:方便判断父类X和子类Z实例化先后。

Console输出:

子父类的分层初始化.png

分析:继承关系,Z继承自X,主方法在Z.java中,所以运行(命令行输入 java z)时,会触发Z类加载的初始化。

  1. 由于Z是X的子类,所以先进性X类加载,对应Console输出语句:X static block executed! X value
  2. 进行Z类加载,对应Console输出语句:Z static block executed!
  3. 进行X类实例化,对应Console输出语句:Y construction executed! Y value X construction executed! static value。如果先执行Z的实例化,在Console先输出Z non static block executed!
  4. 进行Z的实例化,对应Console输出Z non static block executed! Y construction executed! Y value y value changed! Z construction executed!
  5. 最后打印Z的成员变量xVar,对应Console输出x value changed!

参考

理解Java中的类初始化

Java类成员初始化陷阱

public class Base{
    Base() {
        preProcess();
    }
    void preProcess(){

    }
}

public class Derived extends Base{
    public String whenAmISet = "set when instantiation";

    @Override
    void preProcess() {
        whenAmISet = "set in preProcess method";
    }
}


public class Test {
    public static void main(String[] args) {
        Derived d = new Derived();
        System.out.println(d.whenAmISet);
    }
}

代码分析:Base作为Derived的基类。然后在Derived.java中覆写preProcess()方法。最后执行Test.java主方法。Console输出:set when instantiation

这个结果好像不对吧?!为什么不是set in preProcess method?正常的执行顺序不是Base父类实例化,调用子类Derived.preProcess()方法修改whenAmISet字段,然后打印出set in preProcess method

其实不然,上面已经分析过子父类的分层初始化,父类Base先实例化,然后调用子类Derived.preProcess()方法修改字段whenAmISet值,然后进行子类Derived实例化,显示初始化字段whenAmISet为set when instantiation,最后打印输出set when instantiation

参考

JAVA构造时成员初始化的陷阱

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,684评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,143评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,214评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,788评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,796评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,665评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,027评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,679评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,346评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,664评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,766评论 1 331
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,412评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,015评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,974评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,073评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,501评论 2 343

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,572评论 18 399
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,596评论 18 139
  • 搜索的时候看了好几篇文,自己就想记录一遍,加深一下记忆,以下是原文的地址,受益匪浅。blog.csdn.net/n...
  • Advanced Language Features 知识点:一. static修饰符 static修饰符可以用来...
    风景凉阅读 434评论 0 0
  • 今天遇到了高能量的王咏老师。在喜悦曼陀罗中,在我穿越内在的恐惧后,能量不断提升。全身都在振动发麻。在之前抽彩虹卡的...
    艳敏_c9e0阅读 257评论 0 0