代码示例
首先来个例子说明下本文研究的内容:
public class StringTest {
private String aa = "1111";
private StringBuilder bb = new StringBuilder("bbbb");
private char ca = 'y';
private Character cr = 'z';
private char[] ar = {
'a', 'b', 'c'};
{
System.out.println("这是一个考研");
}
static {
System.out.println("这是一个考验");
}
public static void main(String[] args) {
try {
StringTest test = new StringTest();
test.change(test.bb, test.aa, test.cr, test.ca, test.ar, test);
System.out.print(test.bb);
System.out.print(test.aa);
System.out.print(test.ca);
System.out.print(test.cr);
System.out.println(test.ar);
Field field = StringTest.class.getDeclaredField("aa");
field.setAccessible(true);
field.set(test, "0000");
System.out.print(test.bb);
System.out.print(test.aa);//0 修改String值成功
System.out.print(test.ca);
System.out.print(test.cr);
System.out.println(test.ar);
} catch (Exception e) {
e.printStackTrace();
}
}
public void change(StringBuilder bb, String aa, Character cr, char ca, char[] ar, StringTest st) {
bb.append(" is Change");
aa = "1111 AND ";
cr = new Character('m');
ca = 'n';
ar[0] = 'g';
st.aa = "3333 AND";
}
}
输出结果:
这个结果表明在方法内部对String类型的变量的重新赋值操作并没有对这个String变量的原型产生任何影响。好了,这个示例的逻辑和运行结果都展示清楚了,接下来我们来对这个小程序进行分析。在这之前我们先来回顾下Java中所谓的***“传值”和“传引用”问题***。
Java中的“传值”和“传引用”问题
许多初学Java的程序员都在这个问题上有所思索,那是因为这是所谓的“C语言的传值和传指针问题”在Java语言上同类表现。
最后得出的结论是:
***在Java中,当基本类型作为参数传入方法时,无论该参数在方法内怎样被改变,外部的变量原型总是不变的,因为方法内部有外部变量的一份拷贝,对这个拷贝的更改不会改变外部变量的值。***代码类似上面的示例:
int number = 0;
changeNumber(number) {
number++}; //改变送进的int变量
System.out.println(number); //这时number依然为0
这就叫做***“值传递”***,即方法操作的是参数变量(也就是原型变量的一个值的拷贝)改变的也只是原型变量的一个拷贝而已,而非变量本身。所以变量原型并不会随之改变。
但当方法传入的参数为非基本类型时(也就是说是一个对象类型的变量), 方法里面改变参数变量的同时变量原型也会随之改变,代码同样类似上面的示例:
StringBuffer strBuf = new StringBuffer(“original”);
changeStringBuffer(strBuf) {
strbuf.apend(“ is changed!”)} //改变送进的StringBuffer变量
System.out.println(strBuf); //这时strBuf的值就变为了original is changed!
这种特性就叫做***“引用传递”,也叫做传址***,即方法操作参数变量时是拷贝了变量的引用,注意下传递给方法的参数为变量的引用,其实也就是指针,而后通过这个引用找到变量(在这里是对象)的真正地址,并对其进行操作。当 该方法结束后,方法内部的那个参数变量随之消失。但是要知道这个变量只是对象的一个引用而已,它只是指向了对象所在的真实地址,而非对象本身,所以它的消 失并不会带来什么负面影响。回头来看原型变量,原型变量本质上也是那个对象的一个引用(和参数变量是一样一样的),当初对参数变量所指对象的改变就根本就 是对原型变量所指对象的改变。所以原型变量所代表的对象就这样被改变了,而且这种改变被保存了下来。
了解了这个经典问题,很多细心的读者肯定会立刻提出新的疑问:“可是String类型在Java语言中属于非基本类型啊!它在方法中的改变为什么没有被保 存下来呢!”的确,这是个问题,而且这个新疑问几乎推翻了那个经典问题的全部结论。真是这样么?好,现在我们就来继续分析。
关于String参数传递问题的曲解之一——直接赋值与对象赋值
String类型的变量作为参数时怎么会像基本类型变量那样以传值方式传递呢?关于这个问题,有些朋友给出过解释。
一种解释就是,对String类型的变量赋值时并没有new出对象,而是直接用字符串赋值,所以Java就把这个String类型的变量当作基本类型看待 了。即,应该String str = new String(“original”);,而不是String str = “original”;。这种因为给String类型变量赋值方式不同是造成问题所在么?我们来为先前的示例稍微改造下,运行之后看看结果就知道了。改造后的代码如下:
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: original
The changed inner String: original is changed!
The outer String after inner change: original
Test B End.
实践证明,这种说法是错的。
实际上,字符串直接赋值和用new出的对象赋值的区别仅仅在于存储方式不同。
简单说明下:
字符串直接赋值时,String类型的变量所引用的值是存储在类的常量池中的。因为”original”本身是个字符串常量,另一方面String是个不可 变类型,所以这个String类型的变量相当于是滴对一个常量的引用。这种情况下,变量的内存空间大小是在编译期就已经确定的。
而new对象的方式是将”original”存储到String对象的内存堆空间中,而这个存储动作是在运行期进行的。在这种情况下,Java并不是把”original”这个字符串当作常量对待的,因为这时它是作为创建String对象的参数出现的。
所以对String的赋值方式和其参数传值问题并没有直接联系。总之,这种解释并不是正解。
四、 关于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: original
The changed inner String: original is changed!
The outer String after inner change: original
Test C End.
怎么样,这证明了问题并不是出在这,
那到底是什么原因导致了这种状况呢?
好了,下面说下我的解释。
这个问题真正原因是因为String类的存储是通过final修饰的char[]数组来存放结果的。不可更改。所以每次当外部一个String类型的引用传递到方法内部时候,只是把外部String类型变量的引用传递给了方法参数变量。对的。外部String变量和方法参数变量都是实际char[]数组的引用而已。所以当我们在方法内部改变这个参数的引用时候,因为char[]数组不可改变,所以每次新建变量都是新建一个新的String实例。很显然外部String类型变量没有指向新的String实例。所以也就不会获取到新的更改。
下面程序例程假定tString指向A内存空间,A内存空间存放了”hello”这个字符串,然后调用modst函数将tString引用赋值给了text引用,注意是引用。确实是传址,我们知道String是不可变的,任何进行更改的操作都会产生新的String实例。所以在方法里面text指向了B空间,B空间存放了”sdf” 字符串,但是这个时候tString还是指向A空间,并没有指向B空间。
从String源码可见,我们可以知道以下信息:
a)String是最终类,因为是final修饰的class,不可被继承,也无重写一说。
b) 实际存储字符串的是一个数组,并且是final修饰的,分配空间之后内存地址不变。
c) 所有成员变量都是private final修饰的,并且没有提供对应的XXXSetter方法,不允许外部修改这些字段,并且只能对它们赋值一次。
d) 涉及value数组的操作(上面只提供了部分源码)都使用了拷贝数组元素的方法,保证了不能在内部修改字符数组
所以说String在初始化之后是不可变的。
如何修改已经初始化的String字符串的值
即使是不可变类,通过反射仍然可以改变其属性的值。Java 反射是颠覆一切Java常规理论.
IllegalArgumentException - 如果指定对象不是声明底层字段(或者其子类或实现者)的类或接口的实例,或者解包转换失败。因为JVM在编译时期, 就把final类型的String进行了优化, 在编译时期就会把String处理成常量。,所以无法直接修改String aa ="1111"值,而是通过为声明底层字段(或者其子类或实现者)的类或接口的实例来修改String aa =“1111”。
如何才能自定义一个不可变类呢
总结一下,如何才能自定义一个不可变类呢?
1) 类使用final修饰符修饰
2)类的所有字段使用private final修饰
3)不提供XXXSetter方法,getXXX方法返回拷贝的对象,不返回对象本身。
4)构造器初始化成员变量时,使用深拷贝。
深拷贝是什么?
‘深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。 简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
如何实现深拷贝?
实现对象拷贝的类,必须实现Cloneable接口,并覆写clone().
注:如果没有实现Cloneable接口,将出现 CloneNotSupportedException运行时异常。
示例:
/*
* 实现深拷贝
* */
class Teacher implements Cloneable {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Object clone() throws CloneNotSupportedException
{
return super.clone();
}
}
class Student_One implements Cloneable{
private String name;
private Teacher teacher;//添加教师的引用
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();//调用obeject的clone默认为浅拷贝
}
}
class Student_Two implements Cloneable{
private String name;
private Teacher teacher;//添加教师的引用
public Teacher getTeacher() {
return teacher;
}
public void setTeacher(Teacher teacher) {
this.teacher = teacher;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public Object clone() throws CloneNotSupportedException {
//return super.clone();//调用obeject的clone默认为浅拷贝
Student_Two student = (Student_Two) super.clone();
student.setTeacher((Teacher) student.getTeacher().clone());//T复制一份eacher对象并重新set进来
return student;
}
}
public class StringDemo1 {
public static void main(String[] args) throws CloneNotSupportedException {
//新建一个老师对象
Teacher teacher = new Teacher();
teacher.setAge(25);
teacher.setName("李华");
Student_One student_one = new Student_One();
student_one.setName("同学甲");
System.err.println();
student_one.setTeacher(teacher);
//拷贝一个Student_One对象(浅拷贝)
Student_One student_one1 = (Student_One)student_one.clone();
System.err.println(student_one1.getTeacher().getName());//李华 原拷贝对象
//修改老师的名字,会把拷贝的对象的老师名称也一同修改了,因为它们指向的是同一块地址,也就是同一个对象
teacher.setName("黄珊");
System.err.println(student_one1.getTeacher().getName());//黄珊
//重新设置老师名为为李华
teacher.setName("李华");
Student_Two student_two = new Student_Two();
student_two.setTeacher(teacher);
student_two.setName("同学乙");
//拷贝一个Student_Two对象
Student_Two student_two1 = (Student_Two) student_two.clone();
System.err.println(student_two1.getTeacher().getName());//李华 原拷贝对象
//修改老师的名字,打印发现并没有影响原拷贝对象的值,所以为深拷贝,是不同的两个对象
teacher.setName("黄珊");
System.err.println(student_two1.getTeacher().getName());//李华
}
}