本文非原创,出自:http://freej.blog.51cto.com/235241/168676java
1、最开始的示例程序员
写代码最重要的就是实践,不通过反复试验而得出的说辞只能说是凭空遐想罢了。因此,在本文中首先以一个简单示例来抛出核心话题:小程序
public class StringAsParamOfMethodDemo {
public static void main(String[] args) {
StringAsParamOfMethodDemo StringAsParamOfMethodDemo = new StringAsParamOfMethodDemo();
StringAsParamOfMethodDemo.testA();
}
private void testA() {
String originalStr = "original";
System.out.println("Test A Begin:");
System.out.println("The outer String: " + originalStr);
simpleChangeString(originalStr);
System.out.println("The outer String after inner change: " + originalStr);
System.out.println("Test A End.");
System.out.println();
}
public void simpleChangeString(String original) {
original = original + " is changed!";
System.out.println("The changed inner String: " + original);
}
}
这段代码的逻辑是这样的:先赋值一个String类型的局部变量,而后把这个变量做为参数送进一个方法中,在这个方法中改变该变量的值。编译运行以后,发现输出结果是这样的: Test A Begin:The outer String: originalThe changed inner String: original is changed!The outer String after inner change: originalTest A End. 这个结果代表在方法内部对String类型的变量的从新赋值操做并无对这个变量的原型产生任何影响。好了,这个示例的逻辑和运行结果都展现清楚了,接下来咱们来对这个小程序进行分析。在这以前咱们先来回顾下Java中所谓的“传值”和“传引用”问题。数组
2、Java中的“传值”和“传引用”问题框架
许多初学Java的程序员都在这个问题上有所思索,那是由于这是所谓的“C语言的传值和传指针问题”在Java语言上同类表现。最后得出的结论是:在Java中,当基本类型做为参数传入方法时,不管该参数在方法内怎样被改变,外部的变量原型老是不变的,代码相似上面的示例: 函数
int number = 0;
changeNumber(number) {number++}; //改变送进的int变量
System.out.println(number); //这时number依然为0 这就叫作“值传递”,即方法操做的是参数变量(也就是原型变量的一个值的拷贝)改变的也只是原型变量的一个拷贝而已,而非变量自己。因此变量原型并不会随之改变。 但当方法传入的参数为非基本类型时(也就是说是一个对象类型的变量), 方法改变参数变量的同时变量原型也会随之改变,代码一样相似上面的示例: this
StringBuffer strBuf = new StringBuffer(“original”);
changeStringBuffer(strBuf) {strbuf.apend(“ is changed!”)} //改变送进的StringBuffer变量
System.out.println(strBuf); //这时strBuf的值就变为了original is changed! 这种特性就叫作“引用传递”,也叫作传址,即方法操做参数变量时是拷贝了变量的引用,然后经过引用找到变量(在这里是对象)的真正地址,并对其进行操做。当该方法结束后,方法内部的那个参数变量随之消失。可是要知道这个变量只是对象的一个引用而已,它只是指向了对象所在的真实地址,而非对象自己,因此它的消失并不会带来什么负面影响。回头来看原型变量,原型变量本质上也是那个对象的一个引用(和参数变量是同样同样的),当初对参数变量所指对象的改变就根本就是对原型变量所指对象的改变。因此原型变量所表明的对象就这样被改变了,并且这种改变被保存了下来。 了解了这个经典问题,不少细心的读者确定会马上提出新的疑问:“但是String类型在Java语言中属于非基本类型啊!它在方法中的改变为何没有被保存下来呢!”的确,这是个问题,并且这个新疑问几乎推翻了那个经典问题的所有结论。真是这样么?好,如今咱们就来继续分析。spa
3、关于String参数传递问题的曲解之一——直接赋值与对象赋值指针
String类型的变量做为参数时怎么会像基本类型变量那样以传值方式传递呢?关于这个问题,有些朋友给出过解释,但惋惜并不正确。一种解释就是,对String类型的变量赋值时并无new出对象,而是直接用字符串赋值,因此Java就把这个String类型的变量看成基本类型看待了。即,应该String str = new String(“original”);,而不是String str = “original”;。这是问题所在么?咱们来为先前的示例稍微改造下,运行以后看看结果就知道了。改造后的代码以下: orm
private void testB() {
String originalStr = new String("original");
System.out.println("Test B Begin:");
System.out.println("The outer String: " + originalStr);
changeNewString(originalStr);
System.out.println("The outer String after inner change: " + originalStr);
System.out.println("Test B End:");
System.out.println();
}
public void changeNewString(String original) {
original = new String(original + " is changed!");
System.out.println("The changed inner String: " + original);
}
咱们来看看此次运行结果是怎么样的: Test B Begin:The outer String: originalThe changed inner String: original is changed!The outer String after inner change: originalTest B End. 实践证实,这种说法是错的。实际上,字符串直接赋值和用new出的对象赋值的区别仅仅在于存储方式不一样。简单说明下:字符串直接赋值时,String类型的变量所引用的值是存储在类的常量池中的。由于”original”自己是个字符串常量,另外一方面String是个不可变类型,因此这个String类型的变量至关因而对一个常量的引用。这种状况下,变量的内存空间大小是在编译期就已经肯定的。而new对象的方式是将”original”存储到String对象的内存空间中,而这个存储动做是在运行期进行的。在这种状况下,Java并非把”original”这个字符串看成常量对待的,由于这时它是做为建立String对象的参数出现的。因此对String的赋值方式和其参数传值问题并无直接联系。总之,这种解释并非正解。
4、关于String参数传递问题的曲解之二——“=”变值与方法变值
又有些朋友认为,变值不一样步的问题是处在改变值的方式上。这种说法认为:“在Java 中,改变参数的值有两种状况,第一种,使用赋值号“=”直接进行赋值使其改变;第二种,对于某些对象的引用,经过必定途径对其成员数据进行改变,如经过对象的自己的方法。对于第一种状况,其改变不会影响到被传入该参数变量的方法之外的数据,或者直接说源数据。而第二种方法,则相反,会影响到源数据——由于引用指示的对象没有变,对其成员数据进行改变则实质上是改变的该对象。”这种方式听起来彷佛有些…,咱们仍是用老办法,编写demo,作个小试验,代码以下:
private void testC() {
String originalStr = new String("original");
System.out.println("Test C Begin:");
System.out.println("The outer String: " + originalStr);
changeStrWithMethod(originalStr);
System.out.println("The outer String after inner change: " + originalStr);
System.out.println("Test C End.");
System.out.println();
}
private static void changeStrWithMethod(String original) {
original = original.concat(" is changed!");
System.out.println("The changed inner String: " + original);
}
结果以下: Test C Begin:The outer String: originalThe changed inner String: original is changed!The outer String after inner change: originalTest C End. 怎么样,这证实了问题并非出在这,又一个解释在实践论据下夭折了。那究竟是什么缘由致使了这种情况呢?好了,不卖关子了,下面说下个人解释。
5、 String参数传递问题的症结所在
其实,要想真正理解一个类或者一个API/框架的最直接的方法就是看源码。下面咱们来看看new出String对象的那小段代码(String类中),也就是String类的构造函数:
public String(String original) {
int size = original.count;
char[] originalValue = original.value;
char[] v;
if (originalValue.length > size) {
// The array representing the String is bigger than the new
// String itself. Perhaps this constructor is being called
// in order to trim the baggage, so make a copy of the array.
int off = original.offset;
v = Arrays.copyOfRange(originalValue, off, off+size);
} else {
// The array representing the String is the same
// size as the String, so no point in making a copy.
v = originalValue;
}
this.offset = 0;
this.count = size;
this.value = v;
}
也许你注意到了里面的char[],这说明对String的存储实际上经过char[]来实现的。怎么样?其实就是一层窗户纸。不知道你们还记不记得在Java API中定义的那些基本类型的包装类。好比Integer是int包装类、Float是float的包装类等等。对这些包装类的值操做实际上都是经过对其对应的基本类型操做而实现的。是否是有所感悟了?对,String就至关因而char[]的包装类。包装类的特质之一就是在对其值进行操做时会体现出其对应的基本类型的性质。在参数传递时,包装类就是如此体现的。因此,对于String在这种状况下的展示结果的解释就天然而然得出了。一样的,Integer、Float等这些包装类和String在这种状况下的表现是相同的,具体的分析在这里就省略了,有兴趣的朋友能够本身作作试验。这也就是为何当对字符串的操做在经过不一样方法来实现的时候,推荐你们使用StringBuffer的真正缘由了。至于StringBuffer为何不会表现出String这种现象,你们再看看的StringBuffer的实现就会明白了,在此也再也不赘述了。
6、写在最后
由此String类型的参数传递问题的原理也就展示出来了。其实能够看出,只要分析方式正确,思考终究得出正确结论的。正确分析方法的基础有二:一、 多实践:手千万不要犯懒,实践必会出真知。二、 基于原理:搞清楚程序逻辑的最直接最简单的方式就是看源码,这毋庸置疑。只要基于这两个基础进行分析,在不少状况下会达到事半功倍的效果。这算是经验之谈吧,也算是分析程序的“捷径”方式之一。
示例五:
public class TestArray{
public static void main(String[] args){
Change chge = new Change();
String[] str1 = {new String("hello"),new String("world")};
String str2 = new String("hello");
chge.change(str1,str2);
System.out.println(str1[0]);
System.out.println(str2);
}
}
class Change {
public static void change(String[] s1,String s2){
s1[0] = new String("change");
s2="change";
}
}
解析:程序输出的结果将是change和hello,对于为何输出hello前面已经解释过了,如今解释一下String数组做为函数参数时的特色。在java中数组是被看作对象来处理的,以上面的str1[]为例,str1是指向数组对象的句柄,而str1的元素保存的是指向String对象的句柄,str1至关于C语言中指向指针的指针。上面的程序中,当咱们调用change()函数的时候,首先将str1的拷贝赋给s1,str1和s1指向相同的数组对象内存空间,当咱们经过s1改变数组元素的属性的时候,改变的是str1和s1共同指向的地址空间,因此函数中经过s1改变数组第一个元素处的句柄的时候会影响到str1,调用完change()函数str1指向的数组对象的第一个元素处的句柄发生了改变,指向了新的“change”字符串对象。
其实不只仅是String数组,对于任何类型的数组,当把数组做为函数参数时,在函数中对数组元素的改变都会反映到实参上面,以下面的示例:
示例六:
public class TestArray{
public static void main(String args[]){
int a[]=new int[]{1,2,3,4};
System.out.println(a[0]);
change(a);
System.out.println(a[0]);
}
public static void change(int a1[]){
a1[0]=0;
}
}
解析;和String数组相似,上面的程序输出的会是1和0。
总结:上面说了这么多,其实关键是理解String类型的特性,以及数组在java中构成的基本原理。String很是像基本数据类型,可是他不是,他就是一个对象,可是这个对象有他自己和普通对象不一样的特性。java中的数组,若是是对象数组的话其元素保存的是指向某特定类型对象的句柄,若是是基本类型的数组其元素保存的是该基本类型的值,而数组变量自己是指向数组内存的句柄,咱们对数组元素进行更改时改变的是指向某个对象的句柄或基本数据类型的值。