#架构师之性能优化篇--使用包装类的parseXXX()代替valuesOf()

valuesOf和parseXXX都是包装类的静态方法,都可以将字符串类型的数字转化为整型数据,但略有不同,我们来看下面这一段代码:

String numStr1 = "127";
String numStr2 = "127";
System.out.println(Integer.valueOf(numStr1) == Integer.valueOf(numStr2));
System.out.println(Integer.parseInt(numStr1) == Integer.parseInt(numStr2));
String numStr3 = "128";
String numStr4 = "128";
System.out.println(Integer.valueOf(numStr3) == Integer.valueOf(numStr4));
System.out.println(Integer.parseInt(numStr3) == Integer.parseInt(numStr4));
/**output:
true
true
false
true
*/

如果我们使用int变量来进行接收,那么获取的结果又会有所不同:

int numStr31 = Integer.valueOf(numStr3);
int numStr41 = Integer.valueOf(numStr4);
System.out.println(numStr31 == numStr41);
/**output:
true
*/

1.1 IntegerCache的cache缓存器与静态代码
这是为什么呢?我们来分析一下底层的源代码:

public static int parseInt(String s) throws NumberFormatException {
return parseInt(s,10);
}

注意到,parseInt(String s)和parseInt(s,10)方法的返回值均为int类型,两个字面值相等的int类型值比较结果必为true;而valuesOf(String s)方法的源代码如下所示:

public static Integer valueOf(String s) throws NumberFormatException {
return Integer.valueOf(parseInt(s, 10));
}
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

从上面的源代码可以看出,parseInt(String s)和valuesOf(String s)都调用了parseInt(s,10)方法,但parseInt(String s)是直接返回parseInt(s,10)的值,而valuesOf(String s)是将parseInt(s,10)作为入参,调用了Integer.valueOf(int i)作为其返回值的,因此从效率上而言,parseInt(String s)> valuesOf(String s);
从安全性而言,valueOf(String s)是借助valueOf(int i)的返回值作为返回值的,从上面的代码来看,当-128<=i<=127时,那么返回,利用的缓存中的引用,也就是在这个区间的都是用的同一个数组中的引用,所以两个字面值相等的数据其实是用同一个缓存引用在比较,所以必然相等!(这也是数组作为缓存(内存或硬盘中)的一个应用)而超过这个区间则直接创建出一个对象,而==比较的是引用,所以此时两个字面值相等的Integer对象的引用不同,所以比较的结果为false!我们来看一下Integer类中valueOf(int i)方法的缓存器源码:

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) {
                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);
            }
            high = h;
	     //为什么要加1呢?例如-2到3之间共有6个数(包括0),而3-(-2)=5
            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);
        }

        private IntegerCache() {}
}

我们现在来分析分析这个缓存器是如何设计的,注意到,这里缓存器使用了static静态块设计而成,何为static静态块呢?静态块会在类被加载的时候执行且仅会被执行一次,一般用来初始化静态变量和调用静态方法。因为类加载在虚拟机的生命周期中一个类只被加载一次。而静态块正是利用了类加载这一特性。那么类在什么时候会被加载呢?有下面几种情况:
(1) 创建对象时
当创建对象时,就会加载类,静态块在首次创建对象时会被执行。
(2) 调用Class对象的newInstance()方法
(3) 执行类加载器方法:Class.forName(“com.yzh.maven.main.ThisStatic”);
(4) 调用静态方法或调用静态变量时
上面四种情况都会导致静态块被执行一次。

package com.yzh.maven.main;

public class ThisStatic {
	private static int i;
	{
		System.out.println("我是实例块!"+(++i));
	}
	static{
		System.out.println("我是静态块!"+(++i));
	}
	public ThisStatic(){
		System.out.println("我是构造器!"+(++i));
	}
	public int getI(){
		return i;
	}
	public static int getI2(){
		return i;
	}
}
package com.yzh.maven.main;

public class ThisStaticTest {
	public static void main(String[] args) {
		//1.new ThisStatic();
		//new ThisStatic();
		//2.类加载的方式
		Class<ThisStatic> c = ThisStatic.class;
		try {
			c.newInstance();
		} catch (InstantiationException e) {
			e.printStackTrace();
		} catch (IllegalAccessException e) {
			e.printStackTrace();
		}
		try {
			//3.类加载方式
			Class.forName("com.yzh.maven.main.ThisStatic");
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		
		ThisStatic.getI2();
		
	}
}

除了静态块之外,还有实例块,先来看看实例代码块的概念:实例块会在创建对象的时候执行,每创建一次对象就会执行一次。例如:

new ThisStatic();
new ThisStatic();
/**output:
我是静态块!1
我是实例块!2
我是构造器!3
我是实例块!4
我是构造器!5
*/

从上面的结果我们可以看出,实例代码块的执行快于构造器的执行,慢于静态代码块的执行。
此外,还有普通代码块,普通代码的执行取决于该方法是否被调用,如果该方法被调用,那么它的执行顺序在当前构造器执行之后立即执行:

package com.yzh.maven.main;

public class ThisStatic {
	public static int i;
	{
		System.out.println("我是实例块!"+(++i));
	}
	static{
		System.out.println("我是静态块!"+(++i));
	}
	public ThisStatic(){
		System.out.println("我是构造器!"+(++i));
	}
	public int getI(){
		{
			System.out.println("我是普通代码块");
		}
		return i;
	}
	public static int getI2(){
		return i;
	}
}
package com.yzh.maven.main;
public class ThisStaticTest {
	public static void main(String[] args) {		
		ThisStatic thisStatic1 = new ThisStatic();
		thisStatic1.getI();
		ThisStatic thisStatic2 =  new ThisStatic();
		
	}
}
/**output:
我是静态块!1
我是实例块!2
我是构造器!3
我是普通代码块
我是实例块!4
我是构造器!5
*/

1.2 IntegerCache的cache缓存器的原理与实现
由此我们可以看出,Integer的valueOf(int i)执行IntegerCache.cache时,在底层的静态代码块只执行了一次,所以底层的缓存数组实际上只创建了一次,因此该数组的引用值是固定不变的。所以底层的静态代码块是该引用缓存器的关键,我们来模拟一下这个缓存器的实现:

package com.yzh.maven.main;

public class DataCache {
	public static MyInteger[] cacheArr = {};
	private static int low = -128; 
	private static int high = 127;
	static{
		int cacheLen = high - low +1;
		int temp = low;
		cacheArr = new MyInteger[cacheLen];
		for(int i = 0;i<cacheLen;i++){
			cacheArr[i] = new MyInteger(temp);
			temp++;
		}
	}
}
测试:
package com.yzh.maven.main;

public class MyInteger {
	private int low = -128;
	private int high = 127;
	private int value;
	public MyInteger valueOf(int i){
		if(i >= this.low && i<= this.high){
			//底层静态代码只会被执行一次
			return DataCache.cacheArr[i];
		}else{
			return new MyInteger(i);
		}
	}
	
	public MyInteger(){}
	
	public MyInteger(int value){
		this.value = value;
	}

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
	
	public static void main(String[] args) {
		MyInteger myInteger = new MyInteger();
		MyInteger a = myInteger.valueOf(127);
		MyInteger b = myInteger.valueOf(127);
		//当大于127时相当于每次都是创建了一次对象,因此引用都不相同,所以==比较的结果为false
		MyInteger c = myInteger.valueOf(128);
		MyInteger d = myInteger.valueOf(128);
		System.out.println(a == b);
		System.out.println(c == d);
	}
}
/**
true
false
*/

从上面的结果我们可以看出,当-128<=i<=127时,则直接返回静态cacheArr缓存器的引用,由于静态块只会在类夹载(调用了该静态缓存器)后执行有且仅有一次,因此在这个区间的数字,其实都是返回该缓存器中的MyInteger对象的引用,所以==比较的结果为false,而超过这个区间的数字则会直接新建一个对象:new MyInteger(i),这会导致即使是同样字面的数字也会导致其返回不同的引用,因为MyInteger myInteger = new MyInteger(128)与MyInteger myInteger2 = new MyInteger(128);实在不同时刻创建的,所以并不是同一个引用,也就是在栈区开辟空间中不是同一个地址,因此比较的结果为false。我们可以从内存模型来表示下面这两句代码:

MyInteger myInteger = new MyInteger(128);
MyInteger myInteger2 = new MyInteger(128);

在这里插入图片描述

从上面的内存分配图可以看出,myInteger和myInteger2分别指向的对象的引用是不同的,其引用值分别为00x和00y,但对象对应的值是相等的,因此使用==比较的结果是不相等的,而使用equals比较的结果是相等的;另外,当新建一个对象时,不管这个对象对应的值是否相等,都会在堆中重新开辟一个内存空间来存放对象。因此,两个字面值相等的对象,其实在堆中分配了两个不同的内存空间,也就不存在多个不同的引用指向同一个对象。例如,下面是一道经典的面试题:
int[] arr = new int[]{1,4,5,56};
int[] arr2 = new int[]{3,7,6,89};
arr = arr2;
arr[1] = 9;
System.out.println(arr2[1]);
如果执行了上面的代码会输出什么?arr2[1]=7还是arr2[1]=9?
分析:其实关键在于arr = arr2这一句,可是还是存在一个疑问,那就是明明我是将arr2赋值给了arr,那么arr2为什么会被影响了呢?我们其实可以从内存分配的角度来解释:
在下图中,左边是存在于栈中数组的引用,而右边是存在于堆中数组的对象(包括对象值)
在这里插入图片描述
arr变量的引用00x指向堆内存中的{1,4,5,56}对应的对象,而arr2变量的引用00y指向堆内存中的{3,7,6,89}对应的对象,当执行arr = arr2后,此时表示将arr2的引用(地址)赋给arr,因此arr的地址与arr2地址变成一致,所以同时指向arr2对应的对象,也就是这两个变量共用一个对象,任何一个引用引起对象的改变都会造成对象引起变化,而arr和arr2的地址一致,所以任何一个取出来的对象对应的值都是一致的。该过程可以用下图来表示:

在这里插入图片描述
因此,不存在栈中同一个引用会指向不同的堆空间。我们再来看一道经典的栈内存分配的面试题:
下面的执行结果为(3);
int a1 = 6;
int b1 = 3;
a1 = b1;
a1 = 4;
System.out.println(b1);
为什么结果还是3呢?通过上面的理解来看,结果不应该是4吗?事实上,基本数据类型和引用都保存在栈内存中,上面的过程也可以使用内存分配来解释:

在这里插入图片描述
当执行a1 = b1时,表示将b1的地址赋值给a1,此时a1对应的地址为00y,因此指向同一个数字3,当执行a1=4时,此时a1首先会在栈中寻找有没有4对应的地址,如果有则直接指向这个地址,如果没有,则直接开辟一个新的内存地址。例如,int a = 5;int b = 5;则c1和d1对应的内存地址是同一个,我们可以通过System.identityHashCode()来获取该数据的内存地址的id:
int c1 = 5;
int d1 = 5;
System.out.println(System.identityHashCode(c1));
System.out.println(System.identityHashCode(d1));
/**output:
1326857436
1326857436
*/
当执行a1 = b1后,则内存分配图如下所示。
在这里插入图片描述

但是,当a1 = 4;时,此时a1会首先在栈内存中寻找4对应的地址,如果没有,那么会开辟一个新的内存地址,显然这里新开辟了一个内存地址,因此b1的内存地址还是00y,而a1的内存地址变成了00Z,所以b1还是3。我们再来将代码改变一点点:

int a2 = 6;
System.out.println(System.identityHashCode(a2));
int b2 = 7;
System.out.println(System.identityHashCode(b2));
b2 = a2;
System.out.println(System.identityHashCode(a2));
System.out.println(System.identityHashCode(b2));
a2 = 8;
System.out.println(System.identityHashCode(a2));
System.out.println(System.identityHashCode(b2));
/**output:
1547637284
1326857436
1547637284
1547637284
1022736404
1547637284
*/

通过上面的代码我们发现,只有在b2 = a2时,b2的地址发生了改变,后面a2改变了值时,b2地址并没有变化,而a2则新开辟了一个地址,指向新的数字8。但是为什么在上面引用类型中,改变arr的元素也会引起arr2的元素变化呢?原来arr1[1]=9引起的是对象值的变化,而不是引用值的变化,所以在执行arr1[1]=9后并没有引起arr1=arr2中的arr1的引用变化,此时arr1还是等于arr2,我们来打印一下它们的地址:

System.out.println("========================================================");
int[] arr = new int[]{1,4,5,56};
System.out.println(System.identityHashCode(arr));
int[] arr2 = new int[]{3,7,6,89};
System.out.println(System.identityHashCode(arr2));
arr = arr2;
System.out.println(System.identityHashCode(arr));
System.out.println(System.identityHashCode(arr2));
arr[1] = 9;
System.out.println(arr2[1]);
//arr = new int[]{23,56,78,90};
System.out.println(arr2[1]);
System.out.println(arr[1]);
System.out.println(System.identityHashCode(arr));
System.out.println(System.identityHashCode(arr2));
System.out.println("========================================================");
/**output:
========================================================
1547637284
817899724
817899724
817899724
9
9
9
817899724
817899724
========================================================
*/

从上面的结果正好可以验证我们的猜想。但是,如果我需要在arr = arr2后改变arr的引用呢?事实上,因为每次创建对象都会在堆中开辟一个新的内存空间,每次返回在栈中的引用也会改变。因此,我们要想改变arr的引用,就必须让它指向新的对象,例如:

System.out.println("========================================================");
int[] arr = new int[]{1,4,5,56};
System.out.println(System.identityHashCode(arr));
int[] arr2 = new int[]{3,7,6,89};
System.out.println(System.identityHashCode(arr2));
arr = arr2;
System.out.println(System.identityHashCode(arr));
System.out.println(System.identityHashCode(arr2));
arr = new int[]{31,71,61,891};
arr[1] = 9;
System.out.println(arr2[1]);
System.out.println(arr[1]);
System.out.println(System.identityHashCode(arr));
System.out.println(System.identityHashCode(arr2));
System.out.println("========================================================");
/**`output:`
========================================================
817899724
397836821
397836821
397836821
7
7
9
1326857436
397836821
========================================================
*/

说明,在上面的代码中,arr通过arr = new int[]{31,71,61,891};重新指向了新的对象,因此它对应的引用也发生了变化,而arr2的引用没有改变,所以通过arr[index]改变对象的值时,并不会引起arr2的对象值的变化。该过程可以用下图来表示:

在这里插入图片描述


下面的str2的结果是(bbb)

String str1 = "aaa";
String str2 = "bbb";
System.out.println(System.identityHashCode(str1));
System.out.println(System.identityHashCode(str2));
str1 = str2;
str1 = "ccc";
System.out.println(System.identityHashCode(str1));
System.out.println(System.identityHashCode(str2));
System.out.println(str2);
分析:String直接创建对象时,会在一个名为常量区的地方进行内存分配。在进行内存分配时,会先在该常量区进行搜寻,是否有现有的”值”,如果有,则直接指向这个常量,此时分配的地址与其他指向该值的地址保持一致,因此使用==比较为true,例如:
String cStr = "ddd";
String dStr = "ddd";
System.out.println(cStr == dStr);
System.out.println(System.identityHashCode(cStr));
System.out.println(System.identityHashCode(dStr));

如果找不到现有的值,则重新分配一个内存空间的地址指向常量区的数据。在上面的案例中,通过str1 = str2;此时,str2将它的地址赋值给str1,str1 = “ccc”时,由于在常量区找不到现有的数据,所以会重新开辟一个空间并指向新的值,而str2的地址并没有变化(只要不进行赋值,其地址就不会改变),所以其结果还是bbb。

1.3按值传递
需要清楚一点,不管是引用类型还是基本数据类型,都是按值传递,因为它们归根结底是对数据类型的拷贝。
例1
下面打印的结果为(34,56)

public static void set(int b,int a){
	b = 23;
	a = b;
}

public static void main(String[] args) {
	int a = 34;
	int b = 56;
	set(a,b);
	System.out.println("a:"+a+" b:"+b);
}

分析:本题考查对Java按值传递的理解。需要注意的是,不存在按引用传递的说法(这种说法对Java按值传递理解还不够深刻),不管是基本类型还是引用类型,都是按值传递。我们先来分析一下下面这段代码的执行结果:

int a1 = 34;
int b1 = 56;
int c = a1;
int d = b1;
c = 23;
d = c;
System.out.println("a1:"+a1+" b1:"+b1);
/**output:
a1:34 b1:56
*/

那么在上题中,通过调用set(a,b),a,b传入的其实是地址,在set(a,b)方法的参数相当于上面代码中的int c = a1;int d = b1;因此方法形参其实相当于定义变量来接收变量或引用的地址,而当执行b = 23和a = b时,其实是方法形参改变了地址(相当于新开辟了一个栈空间,当然如果该数字已存在,那么就直接指向现有的数据),而不是实参改变了地址。因此,打印的值仍然是34,56。我们可以使用System.identityHashCode()方法来查看这个传值过程:

package com.yzh.maven.main;
public class Test2 {
	public static void main(String[] args) {
		int a = 23;
		int b = 56;
		System.out.println(System.identityHashCode(a));
		System.out.println(System.identityHashCode(b));
		set(a,b);
		System.out.println(System.identityHashCode(a));
		System.out.println(System.identityHashCode(b));
		System.out.println("a:"+a+" b:"+b);
		
	}
	public static void set(int a,int b){
		System.out.println("b:"+System.identityHashCode(b));
		b = 23;
		System.out.println("b:"+System.identityHashCode(b));
		a = b;
		System.out.println("a:"+System.identityHashCode(a));
		System.out.println("b:"+System.identityHashCode(b));
	}
}
/**output:
671631440
935563448
b:935563448
b:671631440
a:671631440
b:671631440
671631440
935563448
a:23 b:56
*/

例2
下面执行的结果是(“aaa”,”bbb”);

package com.yzh.maven.main;
public class Test2 {
	public static void set(String b,String a){
		a = "ccc";
		b = a;
	}
	
	public static void main(String[] args) {
		String str1 = "aaa";
		String str2 = "bbb";
		set(str1,str2);
		System.out.println("str1:"+str1+" str2:"+str2);
	}
}

说明:这个与例1是一样的道理,唯一的区别就是String是在常量区开辟的内存空间。

例3
下面的执行结果是(“aaa”,”bbbccc”)

package com.yzh.maven.main;
public class Test2 {
	public static void set2(StringBuffer b,StringBuffer a){
		a.append("ccc");
		b = a;
	}
	public static void main(String[] args) {
		StringBuffer str3 = new StringBuffer("aaa");
		StringBuffer str4 = new StringBuffer("bbb");
		set2(str3,str4);
		System.out.println("str3:"+str3+" str4:"+str4);
	}
}

说明:我们看到,该set2方法的形参与上面两个题目不一样,它的参数类型是引用类型,我们可以这样来理解这一题,首先set2的形参接收来自str3和str4的地址,该形参的作用相当于在方法内部重新定义了两个变量a,b,由于a,b接收了来自str3和str4的地址,因此a和str4、b和str3分别用油相同的地址,即分别指向相同的对象,当a.append(“ccc”)时,实际上返回的地址还是a的地址,也就是str4的地址,为什么呢?因为append返回的是this:

public synchronized StringBuffer append(String str) {
super.append(str);
return this;
}
所以,a.append(“ccc”)的引用还是this保持不变,但是通过底层方法System.arraycopy()对该引用对应的对象在堆中值进行了修改而已。这可以用下面的代码来进行解释,改变数组对象的某个元素值不会引起栈地址的变化:
int[] arr1 = new int[]{1,2,45};
int[] arr2 = new int[]{2,4,47};
System.out.println(System.identityHashCode(arr1));
System.out.println(System.identityHashCode(arr2));
arr1 = arr2;
//改变arr1第二个元素不会引起arr1的地址变化
arr1[1] = 3;
System.out.println(System.identityHashCode(arr1));
System.out.println(System.identityHashCode(arr2));
/**output:
1820973978
1320194849
1320194849
1320194849
*/

而在set2方法中,执行完a.append(“ccc”);后,执行b = a,其含义是将a的地址赋值给b,这个局部变量b的地址变化并不会引起str3的地址变化,因为它们处于不同的独立栈空间,这个过程模拟如下:

package com.yzh.maven.main;

public class Test2 {
	public static void set2(StringBuffer b,StringBuffer a){
		a.append("ccc");
		b = a;
		System.out.println("a:"+System.identityHashCode(a));
		System.out.println("b:"+System.identityHashCode(b));
	}
	
	public static void main(String[] args) {
		StringBuffer str3 = new StringBuffer("aaa");
		StringBuffer str4 = new StringBuffer("bbb");
		System.out.println(System.identityHashCode(str3));
		System.out.println(System.identityHashCode(str4));
		System.out.println("str3:"+str3+" str4:"+str4);
		set2(str3,str4);
		System.out.println(System.identityHashCode(str3));
		System.out.println(System.identityHashCode(str4));
		System.out.println("str3:"+str3+" str4:"+str4);
	}
}
/**output:
1022736404
671631440
str3:aaa str4:bbb
a:671631440
b:671631440
1022736404
671631440
str3:aaa str4:bbbccc
*/

练习1

package com.yzh.maven.main;
public class Test2 {
	int a;
	int b;
	public void set(int a,int b){
		this.b = b+1;
		this.a = a+1;
	}
	
	public static void main(String[] args) {
		Test2 t = new Test2();
		int a = 34;
		int b = 56;
		t.set(a,b);
		System.out.println("a:"+a+" b:"+b);
		System.out.println("a:"+t.a+" b:"+t.b);
	}
}
this.a和this.b、t.a和t.b均属于成员属性,而System.out.println("a:"+a+" b:"+b)则是利用了局部变量覆盖成员属性值,因此打印结果如下所示:
a:34 b:56
a:35 b:57

练习2

package com.yzh.maven.main;
public class Test3 {
	private int i;
	private int j;
	public void set2(Test3 t){
		Test3 t2 = new Test3(); 
		t2.i = 12;
		t2.j = 34;
		//相当于Test3 t = t2并不会引起main方法中t地址的改变
		t = t2;
	}
	public static void main(String[] args) {
		Test3 t = new Test3();
		t.i = 35;
		t.j = 56;
		t.set2(t);
		System.out.println("i:"+t.i+" i:"+t.j);
	}
}
/**output:
i:35 i:56
*/
package com.yzh.maven.main;

public class Test3 {
	private int i;
	private int j;
	public void set(Test3 t){
		t.i = 12;
		t.j = 34;
	}
	public static void main(String[] args) {
		Test3 t = new Test3();
		t.i = 35;
		t.j = 56;
		t.set(t);
		System.out.println("i:"+t.i+" i:"+t.j);
	}
}
/**output:
i:12 i:34
*/

练习3

package com.yzh.maven.main;
public class Test4 {
	public static void main(String[] args) {
		int[] arr = new int[]{1,4,6,7};
		int[] arr2 = new int[]{2,5,8,9};
		set(arr,arr2);
		System.out.println(arr[1]);//34
		System.out.println(arr2[1]);//5
	}
	
	public static void set(int[] a,int[] b){
		//通过a的元素改变引起arr的元素改变,因为a与arr是指向同一个对象
		a[1] = 34;
		b = a; 
	}
}
/**output:
34
5
*/

说明:在set方法中,b = a;表示将a的地址赋值给int[] b,但这并不会引起arr2地址的变化,因此arr2[1]还是5,但a[1] = 34;表示通过a的元素改变引起arr的元素改变,因为a与arr是指向同一个对象,所以arr[1]为34。

练习4
下面打印的结果是(“aaa”)

public static void set(String s){
	s += "ccc";
}

public static void main(String[] args) {
	String s = "aaa";
	set(s);
	System.out.println(s);
}

说明:在set()方法中,我们可以理解s += “ccc”;为定义的变量,同时将String s = “aaa”;的s的引用赋给了该s变量,但是s += “ccc”;后,相当于新开辟了内存空间,并不会影响String s = “aaa”;的s的引用,所以打印出来还是aaa。

练习5

public static void set(Map<String,String> map){
map = new HashMap<String,String>();
map.put("1002", "bbb");
}
public static void main(String[] args) {	
	Map<String,String> map = new HashMap<String,String>();
	map.put("1001", "aaa");
	set(map);
	System.out.println(map);
}

说明:在set方法中,我们可以理解map = new HashMap<String,String>();中的map是
方法内部定义的变量,用于接收外部实参的地址,但是这里的map又通过新建对象重新指向了另一个对象,这并不会影响实参的地址(相当于指向了另一个对象,并没有对实参对象的值做什么事),所以输出的结果为{1001=aaa}。

练习6

public static void set2(Map<String,String> map){
	map.put("1002", "bbb");
}
public static void main(String[] args) {
	
	Map<String,String> map = new HashMap<String,String>();
	map.put("1001", "aaa");
	set(map);
	System.out.println(map);
	
	set2(map);
	System.out.println(map);
	
}

说明:和例5不同的是,这里的实参的map通过set方法将地址传给map.put(“1002”, “bbb”);的map,因此这两个map都指向同一个对象,因此这两个map任意一个map都可以对相关对象进行操作,而且任何一个对象修改的结果对两个map都是同步修改的。set2方法中map通过put方法对对象进行了修改,因此main方法中map指向的对象也发生了变化,所以结果为{1001=aaa, 1002=bbb}。

例7

public static void main(String[] args) {
Map<String,String> map = new HashMap<String,String>();
map.put("1001", "aaa");
set(map);
System.out.println(map);

set2(map);
System.out.println(map);

set3(map);
System.out.println(map);
}
public static void set3(Map<String,String> map){
map.put("1003", "ccc");
map = new HashMap<String,String>();
}

说明:在例6的基础上,set3方法中map在修改完对象的值后,又指向了另一个对象,但改变的值已经发生了改变并不会擦除,而set3方法的map指向另一个对象后并不会影响main方法中的map,所以结果为{1003=ccc, 1001=aaa, 1002=bbb}。

例8

package com.yzh.maven.main;

public class A {
	public int modCount;
	public String string;
	public void set(){
		modCount++;
	}
	public void set(String str){
		string += str; 
	}
}
package com.yzh.maven.main;
public class TestA {
	public static void main(String[] args) {
		A a1 = new A();
		A a2 = new A();
		a2 = a1;
		a2.modCount = 4;
		System.out.println(a1.modCount);
		a2.set();
		System.out.println(a1.modCount);
		a1.set();
		System.out.println(a1.modCount);
		a2.set("aaa");
		a1.set("bbb");
		System.out.println(a1.string);
	}
}
/**output:
4
5
6
nullaaabbb
*/

为什么由a2地址操作的set()方法,能够对a1的成员属性值进行修改呢?这涉及到内存开辟方法面的知识,我们知道,局部变量(方法内部的变量、引用)存储在栈区,而对象则存储在堆中。事实上,成员变量保存在堆中,但由于创建出来的对象都会开辟不同的堆内存空间,所以这些对象本身会因为内存空间不同而不同,而成员变量实际上是保存在堆中,所以每一个对象都包含有各自的成员变量,这意味着这些对象的成员变量并不是共享的,所以当某一个实例操作这个成员变量时并不会改变其他实例的成员变量值。那么为什么每个实例都能调用方法呢?方法和成员变量不一样,在对象的堆内存空间中只包含属于各自的成员变量,并不包括成员方法,但是他们共享该类的方法,并不是每创建一个对象就把成员方法复制一次。也就意味着方法是保存在一个名为“方法区”中的,而不是保存在堆空间中的,它时每一个对象所共享的,但是由于方法操作的就是成员变量,而成员变量又各不相同,所以结果并不影响其他对象的成员变量值。在上面的例子中,我们看到,a1和a2都是指向同一个对象,而成员变量保存在堆内存中,因此a1和a2都能对该对象的成员变量进行操作,而且结果是a1和a2叠加的效果。但是,当a1和a2是独立的两个对象的引用时,a1和a2调用set方法只能修改自身对象的值,而无法影响对方对象的值,因为此时a1和a2拥有两份独立的堆空间,同时也拥有独立的成员变量,例如:

package com.yzh.maven.main;
public class TestA {
	public static void main(String[] args) {
		A a1 = new A();
		A a2 = new A();
		//a2 = a1;
		a2.modCount = 4;
		System.out.println(a1.modCount);
		
		a2.set();
		System.out.println(a1.modCount);
		
		a1.set();
		System.out.println(a1.modCount);
		a2.set("aaa");
		a1.set("bbb");
		System.out.println(a1.string);
	}
}
/**output:
0
0
1
nullbbb
*/

当然,这也是get和set方法的原理,实际上,让外部类直接操作成员属性是一件非常危险的事情,因此,为了安全起见,我们需要将成员变量改成私有化,当我们将上面例子中的成员属性修改为private私有化时,要想被外补类进行操作,就必须提供一个public方法,set和get方法应运而生。现在回到最初的话题,那么为什么要使用pasrseXXX代替valueOf呢?因为valueOf(int i)底层相比parseXXX而言多了一些创建对象的语句,至于valueOf(int i)的缓存数组只是缓存了-128到127这个区间呢?因为一次性缓存太多的数据会影响性能!

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值