Java类初始化顺序
从阿里的一道笔试题开始:
下面代码输出什么?
public class Base
{
private String baseName = "base";
public Base()
{
callName();
}
public void callName()
{
System. out. println(baseName);
}
static class Sub extends Base
{
private String baseName = "sub";
public void callName()
{
System. out. println (baseName) ;
}
}
public static void main(String[] args)
{
Base b = new Sub();
}
}
做对这道题的关键在于理解Java类的初始化顺序,本题输出的是Null,意想不到吧,因为在Main方法中new了一个子类,初始化子类前会先初始化父类,调用了父类的构造,构造中又调用了callName(),根据多态的特性,它会调用子类的callName(),然而此时子类的私有属性baseName 还未初始化,所以输出为null。
再来分析一段代码,程序比较简单,有静态属性,静态代码块,普通代码块,构造方法。程序启动时创建了一个Order对象。
public class Order {
private static String str = initStaticStr();
private String str2 = initStr();
public Order() {
System.out.println("父类构造方法执行");
}
static {
System.out.println("父类初始化静态代码块");
}
{
System.out.println("父类初始化普通代码块");
}
private String initStr() {
System.out.println("父类初始化普通成员变量");
return "initStr";
}
private static String initStaticStr() {
System.out.println("父类初始化静态成员变量");
return "initStaticStr";
}
public static void main(String[] args) {
new Order();
}
}
输出结果:
可见初始化顺序为: 静态成员变量 —> 静态代码块 —> 非静态成员变量 —> 非静态代码块 —> 构造方法
public class Order {
private static String str = initStaticStr();
private String str2 = initStr();
public Order() {
System.out.println("父类构造方法执行");
}
static {
System.out.println("父类初始化静态代码块");
}
{
System.out.println("父类初始化普通代码块");
}
private String initStr() {
System.out.println("父类初始化普通成员变量");
return "initStr";
}
private static String initStaticStr() {
System.out.println("父类初始化静态成员变量");
return "initStaticStr";
}
}
class Inner extends Order {
private static String str = initStaticStr();
private String str2 = initStr();
public Inner() {
System.out.println("子类构造方法执行");
}
static {
System.out.println("子类初始化静态代码块");
}
{
System.out.println("子类初始化普通代码块");
}
private String initStr() {
System.out.println("子类初始化普通成员变量");
return "initStr";
}
private static String initStaticStr() {
System.out.println("子类初始化静态成员变量");
return "initStaticStr";
}
public static void main(String[] args) {
new Inner();
}
}
输出结果:
初始化的顺序为:
父类
静态成员变量 —> 父类
静态代码块 —> 子类
静态成员变量 —> 子类
静态代码块 —> 父类
非静态成员变量 —> 父类
非静态代码块 —> 父类
构造方法 —> 子类
非静态成员变量 —> 子类
非静态代码块 —> 子类
构造方法。
参数传递问题
Java中方法参数的传递分为值传递
和引用传递
。
值传递
参数为八大基本数据类型使用的是值传递方式,它会将传递参数的值拷贝一份给执行的方法。
public class Demo {
public static void main(String[] args) {
int a = 1;
System.out.println("Main方法中a = " + a);
changeInt(a);
System.out.println("执行完changeInt后a = " + a);
}
private static void changeInt(int a) {
a++;
System.out.println("changeInt中a = " + a);
}
}
执行结果:
可以发现a并没有发生改变,这是因为调用changeInt()方法时将a的值复制了一份副本给changeInt()方法栈帧的局部变量表,在changeInt()中修改了局部变量表的内容后,方法执行完毕,栈帧出栈,并没有对Main方法中的a值发生更改。
引用传递
引用传递指的是,方法的形参类型为引用类型,引用传递传递的是引用对象的堆内存地址。
public class Demo {
public static void main(String[] args) {
Persion persion = new Persion("Tom");
System.out.println("Main方法中name = " + persion.getName());
changeName(persion);
System.out.println("执行changeName方法后name = " + persion.getName());
}
static class Persion{
String name;
//省略get/set/constructor
}
private static void changeName(Persion persion) {
persion.setName("change");
System.out.println("changeName方法中name = " + persion.getName());
}
}
执行结果:
这里发现引用类型中的属性被改变了,原因很简单,传入的是指向Person对象的内存地址,在changeName方法中将指定内存地址的对象属性修改了,所以发生了改变。
几种特殊的情况
- ①
public class Demo {
public static void main(String[] args) {
Persion persion = new Persion("Tom");
System.out.println("Main方法中name = " + persion.getName());
changeName(persion);
System.out.println("执行changeName方法后name = " + persion.getName());
}
static class Persion{
String name;
//省略get/set/constructor
}
private static void changeName(Persion p) {
p = new Persion("Tom");
p.setName("change");
System.out.println("changeName方法中name = " + p.getName());
}
}
执行结果为:
原理很简单,看图可以发现,changeName中只修改了P指针指向的对象,并修改了它的名称,对Main方法中的对象没有影响。
- ②
public class Demo {
public static void main(String[] args) {
Integer a = 1;
System.out.println("Main方法中a = " + a);
changeInt(a);
System.out.println("执行完changeInt后a = " + a);
}
private static void changeInt(Integer a) {
a = a + 1;
System.out.println("changeInt中a = " + a);
}
}
执行结果:
这次传递的是int的包装类Integer对象,但是执行完后a的值并没有发生改变,不说好的传递引用类型会改变吗??
这是因为Integer中的value值使用final修饰的,因此是不允许发送改变,指针a已经重新指向了另一个对象的地址,对原先的并没有改变
基本数据类型与包装类
不得不提自动装箱拆箱机制
public void demo() {
Integer a = 10; //A
int b = a; //B
}
代码中A处实际上就是Java的自动装箱,它会把基本数据类型自动封装成包装类,以Integer为例,它实际调用的是valueOf()方法
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
代码B处是Java的自动拆箱,将包装类转换成基本数据类型,还是以Integer为例,它实际调用的是intValue();
public int intValue() {
return value;
}
其他基本数据类型的实现也是类似,不再赘述
基本数据类型的常量池
八大基本数据类型中除了布尔类型和浮点类型,其他的Byte,Short,Long,Integer,Char都有自己维护的常量池,注意这里是包装类。在对其进行赋值时,如果值x的范围在 (127 >= x >= -128) 之间的都会放入常量池中缓存。
public class Demo {
public static void main(String[] args) {
Integer i01 = 59;
int i02 = 59;
Integer i03 = Integer.valueOf(59);
Integer i04 = new Integer(59);
System.out.println(i01 == i02);
System.out.println(i01 == i03);
System.out.println(i03 == i04);
System.out.println(i02 == i04);
}
}
运行结果:
逐一进行分析。
先对Integer的部分源码进行分析,有助于理解答案。
private final int value;
// 构造方法
public Integer(int value) {
this.value = value;
}
构造方法中,就是创建Integer对象,单纯的对value进行赋值。
// 自动拆箱调用的方法。 返回value值
public int intValue() {
return value;
}
//自动装箱调用的方法。
public static Integer valueOf(int i) {
// 判断i是否在 -128-127之间,在的话返回缓存中的对象。否则新建一个返回。
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
// Integer缓存的内部类,它在首次调用时会加载缓存,将cache数组中添加-127到128之间的Integer对象
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
//重点在这里,循环向缓存数组中创建Integer对象。
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
- i01 == i02 这个比较简单,执行时i01进行了拆箱,最后实际上是对比 59 == 59
- i01 == i03 i01执行的自动装箱和i03是相同的,并且他们都在缓存值的范围内,最后对比的是同一个对象。
- i03 == i04 i03中获得的是缓存当中的对象,i04创建了新的对象,对比内存地址必然为false
- i02 == i04 对比时i04会自动拆箱,最后变成 59 ==59