java对象引用和值引用,特殊的String!

本文详细探讨了Java中对象引用的特性,特别是字符串处理的独特机制。揭示了对象引用与值引用的区别,解释了String对象在内存中的存储方式,以及如何利用StringPool优化字符串管理。

以前就知道JAVA对象分对象引用和值引用,并且还知道8种基础数据类型,即引用时是值引用的数据类型,比如int,short,long,byte,float,double,char,boolean,其它都是对象引用。可是其它的对象引用我一直都以为跟c里面是一样的指针传递,直到今天才发现原来JAVA里面还是别有一番天地的。

 

    1. 方法调用的时候,并不是类似c的指针传递,而是引用的复制

比如代码:

  1.     void func1(List s) {
  2.         s.add("dfdsa");
  3.     }
  4.     void test() {
  5.         List<String> list = new ArrayList<String>();
  6.         list.add("abc");
  7.         func1(list);
  8.         System.out.println(list.size()); // 此处结果为2 
  9.     }

以前一直以为在func1里面的s跟外面的list变量是同一个引用(暂且理解为指针好了)即在栈(stack)里面是同一个东东,这个结论无可厚非,可是看代码:

  1.     void func(String s) {
  2.     s += "tail";
  3.     }
  4.     void test() {
  5.         String a = "abc";
  6.         func(a);
  7.         System.out.println(a); // 此处结果为abc  
  8.     }

经过讨论才发现,原来在stack里面a和func里面的s是完全不同的两个引用,虽然它们指向同一个堆(heap)里面的对象,之所以跟上面的代码结果看起来不一样,只是因为String是一个非可变类(immutable),简单的说就是实例是不可被修改的。在func里面执行s += "tail";操作的时候,s这个引用已经变成指向heap里面另外一个值为"abctail"的对象了,老的s引用已经被废了,随时可以被gc回收了

 

    2. String对象在内存中的位置

既然String是一个immutable的类,那么对于同样值的String实例,我们是可以不必重复创建的,于是就有了JVM中的String Pool的概念。简单的说,String Pool里面放着heap里面String对象的引用。看代码:

  1. String s = "abc";

当程序执行该代码的时候,JVM会在String Pool里面通过equal("abc")方法查找有没有现成的String对象引用,如果没有,则在heap里面创建一个String对象并将该对象的引用保存到String Pool里面;如果有了,那么就直接返回该对象的引用。

 

再看一段非常类似的代码:

  1. String s = new String("abc");

当程序执行该代码的时候,JVM会像普通对象一样生成这个String对象,在heap里面保存,直接返回引用,并不会与String Pool交互,这样一来,String Pool的优势就没有被发挥了,怎么办呢?难道我们就不去使用new的方法创建String了吗?答案是JVM还提供了一个方法:String.intern();来让String Pool管理这种String对象。

intern方法的工作原理是这样的:首先在heap里面创建一个完全一样的String对象,并且将该对象的引用放入String Pool中,最后返回给调用方,看代码:

  1.         String s1=new String("abc"); 
  2.         String s2=s1.intern(); 
  3.         String s3="abc";
  4.         System.out.println(s1==s2); //false
  5.         System.out.println(s2==s3); //true
  6.         System.out.println(s1==s3); //false
  • s1引用的是heap里面的一个普通String对象,在String Pool中没有该对象的引用
  • s2是heap中另一个String对象的引用,并且该对象的引用已经存在在String Pool中了
  • s3在创建的时候JVM通过查找String Pool发现已经有一个同样的对象,所以直接返回给s3一个到该对象的引用

结论:我们在写JAVA代码的时候要尽量避免使用String s = new String("abc");这种方式,因为这样产生的对象并没有“注册”到String Pool中,无法被重复使用,如果已经存在这种对象了,我们可以通过使用s = s.intern();的方式重新创建对象并“注册”到String Pool中,方便后面的重复使用。

 

    3. 深入JVM内存的划分

由于JVM对与heap和stack内存的使用有其特殊的规则,深入了解JVM是如何使用内存的,非常有助于我们在写程序的时候搞清楚自己的对象到底在什么地方,从而可以帮助我们在多线程程序和性能要求较高的程序中优化自己代码,有兴趣的同学可以参考sun的官方文档(http://java.sun.com/docs/books/jvms/second_edition/html/Overview.doc.html#1732),下面仅就部分知识做简单描述。

    a. 每个线程都有自己独占的stack,里面存放的是当前线程执行的method及其局部变量

    b. heap中有部分是公共区域,存放的是类实例(class instance)和已分配内存的数组(array)

    c. heap中对于每个线程都有各自独立的内存区域,存放以下内容:

        运行时常量池(runtime constant pool),上面提到的String Pool就属于其中的一部分

        方法代码(method code),即线程要执行的方法代码

        静态变量和方法(static variables and method),我们定义的static类型的变量和方法都存放在这里

更详细的描述可以参考图片:

Java对象中的传递引用传递可以分三种情况讨论: - **基本数据类型**:属于传递。从内存角度来看,基本数据类型的存放在栈空间。当作为方法形参时,变量会被复制出一份,在该方法栈中对变量的改变不会影响其他方法栈的变量。例如以下代码: ```java public class Test { public static void main(String[] args) { int num1 = 10; int num2 = 20; temp(num1, num2); System.out.println("main() num1 = " + num1); System.out.println("main() num2 = " + num2); } public static void temp(int a, int b) { int temp = a; a = b; b = temp; System.out.println("temp() a = " + a); System.out.println("temp() b = " + b); } } ``` 输出结果为: ``` temp() a = 20 temp() b = 10 main() num1 = 10 main() num2 = 20 ``` 在`temp`方法中对`a``b`的交换操作,并没有影响到`main`方法中的`num1``num2`,这体现了基本数据类型传递的特点 [^2][^3]。 - **引用类型**:通常认为是引用传递,但本质上是传递。引用类型的是对象,存放在堆空间中。作为方法形参时,在栈空间中会复制一个变量副本,传递的是该对象的内存地址,方法中的变量相当于方法栈中指向堆内存的一个指针。直接对变量赋相当于改变指针的指向,对应内存地址里的并未改变,上层方法变量指向的对象不会改变;但如果对内存地址里的进行赋,就改变了堆内存对象本身的,上层方法变量指向的对象本身的也会不一样。例如有一个自定义类`Person`: ```java class Person { String name; public Person(String name) { this.name = name; } } public class Main { public static void changeName(Person p) { p.name = "New Name"; } public static void main(String[] args) { Person person = new Person("Old Name"); changeName(person); System.out.println(person.name); } } ``` 在`changeName`方法中,通过`p.name = "New Name"`改变了对象的属性,`main`方法中的`person`对象属性也随之改变,这体现了引用类型在修改对象内容时的特点 [^2]。 - **引用类型特例:String**:`String`是`final`类型,属于不可变类型。作为方法形参时,引用类型一样,通过指针变量副本指向内存地址。如果在方法内赋予新的,其实是在堆内存创建了新的对象,并将指针变量指向了新对象的内存地址。例如: ```java public class StringTest { public static void changeString(String s) { s = "New String"; } public static void main(String[] args) { String str = "Old String"; changeString(str); System.out.println(str); } } ``` 输出结果为`Old String`,在`changeString`方法中对`s`赋予新,并没有改变`main`方法中`str`指向的对象 [^2]。 关键结论指出,引用类型传递时,传递的是 “地址的副本”(传递),由于形参实参指向同一个对象,通过地址修改对象内容会影响外部,这是 “传递” 的特殊表现,而非引用传递 [^1]。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值