关于Java的String类型,可能你会碰到这种情况,将String类型的变量传到一个函数,在这个函数中修改变量的值,但是,实参的值并没有发生改变。
Java中String的传值/传地址问题:
1 package com.cnblog.GDUTtiantian.String; 2 3 /** 4 * @author GDUTtiantian 5 */ 6 public class JavaString { 7 8 public static void change(String name){ 9 //修改name的值 10 name = "ChangedName"; 11 } 12 13 14 public static void main(String[] args) { 15 String name = "GDUTtiantian"; 16 17 change(name); 18 19 System.out.println(name); 20 21 } 22 23 }
运行结果:
1 GDUTtiantian
String类的底层实现是用一个字符数组去实现的,就像Integer类,底层也是对int进行封装[装箱和拆箱]。
看String类的修饰部分(源码):
1 public final class String 2 implements java.io.Serializable, Comparable<String>, CharSequence { 3 /** The value is used for character storage. */ 4 private final char value[];
注意,String类加了final关键字,所以不能被继承。
第4行是字符串底层的存储结构:字符数组。
String的内容不能被动态地修改,因为底层是字符数组实现的,数组的大小是在初始化时决定的;
如果可以修改,新的字符串长度比原来数组大,那么就会造成数组越界。
1 package com.cnblog.GDUTtiantian.String; 2 3 /** 4 * @author GDUTtiantian 5 * 6 * String, StringBuffer 在传参过程中的哈希值比较 7 */ 8 public class JavaString4 { 9 10 11 public static void change(String str) { 12 System.out.println("形参的哈希值:" + str.hashCode()); 13 14 str = "newString";//修改了形参的值 15 System.out.println("修改后:" + str.hashCode()); 16 } 17 18 public static void change(StringBuffer sb) { 19 System.out.println("形参的哈希值:" + sb.hashCode()); 20 sb.append("newStringBuffer");//修改了形参的值 21 System.out.println("修改后:" + sb.hashCode()); 22 } 23 24 25 public static void main(String[] args) { 26 String str = new String("GDUTtiantian"); 27 StringBuffer sb = new StringBuffer("tiantian"); 28 29 System.out.println("修改前:" + str.hashCode()); 30 change(str); 31 32 System.out.println("\n----------------------------\n"); 33 34 System.out.println("修改前:" + sb.hashCode()); 35 change(sb); 36 } 37 38 }
运行结果:
1 修改前:-501451264 2 形参的哈希值:-501451264 3 修改后:-595706415 4 5 ---------------------------- 6 7 修改前:1036782095 8 形参的哈希值:1036782095 9 修改后:1036782095
实参String变量传给形参,是传一个地址过去,并没有重新创建一个对象,StringBuffer变量也是这么做;
但是,在修改形参的值后,String变量的哈希值发生了改变,StringBuffer变量的哈希没有发生改变,即String变量指向了一个新建的对象。
看看JDK中String类的一段源码(String类的一个构造方法):
1 /** 2 * Allocates a new {@code String} that contains characters from a subarray 3 * of the <a href="Character.html#unicode">Unicode code point</a> array 4 * argument. The {@code offset} argument is the index of the first code 5 * point of the subarray and the {@code count} argument specifies the 6 * length of the subarray. The contents of the subarray are converted to 7 * {@code char}s; subsequent modification of the {@code int} array does not 8 * affect the newly created string. 9 * 10 * @param codePoints 11 * Array that is the source of Unicode code points 12 * 13 * @param offset 14 * The initial offset 15 * 16 * @param count 17 * The length 18 * 19 * @throws IllegalArgumentException 20 * If any invalid Unicode code point is found in {@code 21 * codePoints} 22 * 23 * @throws IndexOutOfBoundsException 24 * If the {@code offset} and {@code count} arguments index 25 * characters outside the bounds of the {@code codePoints} array 26 * 27 * @since 1.5 28 */ 29 public String(int[] codePoints, int offset, int count) { 30 if (offset < 0) { 31 throw new StringIndexOutOfBoundsException(offset); 32 } 33 if (count < 0) { 34 throw new StringIndexOutOfBoundsException(count); 35 } 36 // Note: offset or count might be near -1>>>1. 37 if (offset > codePoints.length - count) { 38 throw new StringIndexOutOfBoundsException(offset + count); 39 } 40 41 final int end = offset + count; 42 43 // Pass 1: Compute precise size of char[] 44 int n = count; 45 for (int i = offset; i < end; i++) { 46 int c = codePoints[i]; 47 if (Character.isBmpCodePoint(c)) 48 continue; 49 else if (Character.isValidCodePoint(c)) 50 n++; 51 else throw new IllegalArgumentException(Integer.toString(c)); 52 } 53 54 // Pass 2: Allocate and fill in char[] 55 final char[] v = new char[n]; 56 57 for (int i = offset, j = 0; i < end; i++, j++) { 58 int c = codePoints[i]; 59 if (Character.isBmpCodePoint(c)) 60 v[j] = (char)c; 61 else 62 Character.toSurrogates(c, v, j++); 63 } 64 65 this.value = v; 66 }
代码57行开始,就是对字符数组进行复制。
1 #include<stdio.h> 2 3 //形参中数组退化为指针了 4 //这里s是指向array数组的指针 5 void go(char * s){ 6 7 s = "JavaString";//指针指向另一个空间,"JavaString"字符串的首地址 8 printf("s:%s#\n", s); 9 } 10 11 //形参中数组退化为指针了 12 void change(char * s){ 13 14 s[0] = 'c'; 15 s[1] = 'h'; 16 s[2] = 'a'; 17 s[3] = 'n'; 18 s[4] = 'g'; 19 s[5] = 'e'; 20 s[6] = '\0'; 21 } 22 23 int main(){ 24 char array[100] = "GDUTtiantian"; 25 26 go(array); 27 printf("array:%s#\n", array); 28 29 change(array); 30 printf("array:%s#\n", array); 31 32 return 0; 33 }
第7行 : s = "JavaString";
这一行比较重要,s是指向main()函数中array数组的首地址的指针,然后在第7行,s指向另外一个字符串的首地址;
这里和String变量在形参中的改变有相似之处。
第14行: s[0] = 'c';
这里的s也是指向main()函数中array数组的首地址的指针,然后把array数组的第一个字符修改为'c'.
运行结果[在CodeBlock编译运行]:
1 s:JavaString# 2 array:GDUTtiantian# 3 array:change# 4 5 Process returned 0 (0x0) execution time : 0.140 s 6 Press any key to continue.
用数组实现
1 package com.cnblog.GDUTtiantian.String; 2 3 /** 4 * @author GDUTtiantian 5 */ 6 public class JavaString2 { 7 8 public static void change(String[] name){ 9 //修改name的值 10 name[0] = "ChangedName"; 11 } 12 13 14 public static void main(String[] args) { 15 String[] name = {"GDUTtiantian"}; 16 17 change(name); 18 19 System.out.println(name[0]); 20 21 } 22 23 }
运行结果:
1 ChangedName
将String设置为新建类型的一个成员变量
1 package com.cnblog.GDUTtiantian.String; 2 3 /** 4 * @author GDUTtiantian 5 */ 6 7 class NewString { 8 private String value; 9 10 public NewString(String str){ 11 value = str; 12 } 13 14 public String getValue() { 15 return value; 16 } 17 18 public void setValue(String value) { 19 this.value = value; 20 } 21 22 23 @Override 24 public String toString() { 25 return getValue(); 26 } 27 } 28 29 public class JavaString3 { 30 private static NewString newName = new NewString("ChangedName"); 31 32 public static void change(NewString name){ 33 //修改name的值 34 name.setValue(newName.getValue()); 35 } 36 37 38 public static void main(String[] args) { 39 NewString name = new NewString("GDUTtiantian"); 40 41 change(name); 42 43 System.out.println(name); 44 45 } 46 47 }
运行结果:
1 ChangedName
这两种方式中String变量的值都发生了改变,但是,其底层还是创建了一个新的String对象[忽略涉及字符串在缓冲池创建对象的部分],然后返回引用给当前句柄。
为什么通过这两种方式可以去改变String变量的值呢?
可以对比python的列表和元组,元组是不可变的,即元组的对象不可以删除,不可以增加;
如果该元组的一个元素为列表类型,那么,可以修改这个列表的内容,当然,列表对象还是当前这个列表对象。
欢迎讨论交流, 我的主页:http://www.cnblogs.com/GDUT/
我的邮箱:[email protected]