Java爬坑系列(一)

本文探讨了Java类的初始化顺序,通过实例分析了静态成员、静态代码块、非静态成员和构造方法的初始化顺序。接着讨论了Java中的值传递和引用传递,解释了为什么基本类型参数不会改变,而引用类型参数会。文章还提到了一些特殊情况,如对象引用的改变和final修饰的影响。最后,讲解了基本数据类型与包装类的关系,包括自动装箱拆箱机制以及常量池的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值