非静态内部类
非静态内部类到底可以有静态属性吗?
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输出结果:
上述两种情况都是在内部类加载过程中的初始化过程中必须完成初始化。由于是非静态内部类,所以访问该内部类一定要持有一个外部类的引用。同样的道理,初始化该内部类上述哪一种成员变量,都需要读取、设置它们,而读取和设置它们必须让内部类持有一个外部类引用。而此时处于外部类和内部类加载阶段,没有任何实例化,也就没有外部类引用。这样编译时无法通过。
参考
为什么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输出如下:
可以看到内部类的加载在外部类实例化之前,更进一步说明非静态内部类不可以有静态变量,和static final修饰的除基本数据类型以及字符串直接量声明的String属性。
这样好像并不能确定内部类的初始化阶段在外部类实例化之前。
外部类不可拥有static final修饰的non static InnerClass成员
public class TestClassLoader{
static final B a = new B();
class B {
}
}
Console输出:
这样是编译不通过的。这是因为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的类属性,属于编译期常量。
编译期可以将编译期常量代入到任何用到它的计算式中,也就是说可以在编译期执行计算式。同时需要注意的是编译期常量必须要在声明时进行初始化。
参考
运行期初始化
初始化阶段属于类加载过程的最后一个阶段。主要用于显示的初始化类变量(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输出如下:
可以看到当static block执行时,类变量已经被赋值为1,而不是准备阶段的0(当然也可以在实例化过程中显示赋值,或者在类方法中修改)。而且在测试类中创建了两个ClassInit对象,但是只执行了一次static block。这是因为类加载的过程只有一次,所以static block只会执行一次。
这里看到还有ClassInit还有两个属性a
和date
。被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输出:
从输出日志可以看出,实例化过程首先执行了类实例域的初始化(包括默认初始化和声明中的显示初始化),非静态代码块,最后执行构造函数。而且在测试类中实例化了两个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输出:
分析:继承关系,Z继承自X,主方法在Z.java中,所以运行(命令行输入 java z
)时,会触发Z类加载的初始化。
- 由于Z是X的子类,所以先进性X类加载,对应Console输出语句:
X static block executed! X value
。 - 进行Z类加载,对应Console输出语句:
Z static block executed!
。 - 进行X类实例化,对应Console输出语句:
Y construction executed! Y value X construction executed! static value
。如果先执行Z的实例化,在Console先输出Z non static block executed!
。 - 进行Z的实例化,对应Console输出
Z non static block executed! Y construction executed! Y value y value changed! Z construction executed!
。 - 最后打印Z的成员变量xVar,对应Console输出
x value changed!
。
参考
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
。
参考