这篇文章讲的是方法的参数传递机制与String类的理解
我觉得这两个知识点我说的还是比较清楚的
方法的参数传递机制
先来看一段代码,main方法中有五种类型的数据:int类型,String类型,Integer类型,int[]数组类型,Mydata引用类型。
change方法中,传入了这五种类型的数据,并做了修改,那么经过change方法后,输出这五个变量的值会是多少呢?
package com.test;
import java.util.Arrays;
public class Test {
public static void main(String[] args) {
int i = 1;
String str = "hello";
Integer num = 200;
int[] arr = {1,2,3,4,5};
MyData my = new MyData();
change(i, str, num, arr, my);
System.out.println(i);
System.out.println(str);
System.out.println(num);
System.out.println(Arrays.toString(arr));
System.out.println(my.a);
}
public static void change(int j, String s, Integer n, int[] a, MyData m){
j += 1;
s += "world";
n += 1;
a[0] += 1;
m.a += 1;
}
}
class MyData{
int a = 10;
}
这里主要用到的知识点就是
- 方法的参数传递机制
- String、包装类等对象的不可变性
程序运行的结果为:
1
hello
200
[2, 2, 3, 4, 5]
11
方法的参数传递机制
1、形参是基本数据类型,实参传递给形参的是“数据值”,形参的修改和实参无关。
2、形参是引用数据类型,实参传递给形参的是“地址值”,如果通过形参修改对象的属性值,那么就相当于实参对象在修改。
注意:
如果在方法中,形参指向了“新对象”,那么就和实参无关了。
那什么时候,形参会在指向“新对象"呢?引用数据类型有特殊的类型,比如String类型、包装类等,这些类型是不可变的,即修改都会产生新对象,所以形参的修改和实参无关
分析
那根据这道题,来分析一下具体的流程,体会一下方法的参数传递机制
第一步
在main方法中创建了这5个变量并赋值后,在JVM虚拟机中的分布情况
int i = 1,直接在虚拟机栈中存在
String str = “hello”,这种字面量赋值的方式,str保存的是字符串常量池中hello的引用地址
Integer num = 200,200会在堆内存中开辟空间,num保存的是200的引用地址
int[] arr = {1, 2, 3, 4, 5},数组会在堆内存中开辟空间,arr保存的是数组的引用地址
MyData my = new MyData(),新建一个对象,会在堆内存为这个对象开辟空间,my保存的是这个对象的引用地址
画张图说明一下,图中数据的内存地址是假定的
字符串常量池实际是在堆内存中的
JDK6的时候,字符串常量池是在永久代中的,JDK7开始,就转移至了堆内存中了
第二步
调用change方法,change方法入虚拟机栈,change方法中的对形参的操作,修改了形参的值形参j,直接在虚拟机栈中存在并赋值
形参s,保存的是字符串常量池中helloworld的引用地址,这里体现了字符串的不可变性,s += “world”,world在字符串常量池中并不存在,所以会在字符串常量池中创建一个world字符串常量,s += "world"后,s是helloworld,依然会在字符串常量池中创建一个helloworld字符串常量
形参n,保存的是堆内存中int类型的数据201的引用地址,Integer类型的数据也是不可变的,n传入的值为200,n += 1后,n变为了201,此时会在堆内存中,为这个Integer 201开辟空间,n保存的是201的引用地址
形参a,保存的是堆内存中数组类型的数据{2,2,3,4,5}的引用地址
形参m,保存的是堆内存中MyData类型的对象的引用地址
第三步
在change方法执行完后,change方法出栈
此时在main方法中,int i还是等于1;
String str还是指向hello;
Integer num还是指向200;
int[] arr还是指向原数组的内存地址,但是此时数组中第一个元素发生了变化;
MyData my还是指向int a的内存地址,但是a的值发生了变化
这就是方法的传递机制,包含了字符串和包装类的不可变性的知识点
String类的理解
String字符串,有两种声明方式
- String str = “hello”; //字面量的定义方式,“hello”保存在字符串常量池中
- String str = new String(“hello”);// “hello”保存在字符串常量池中,new String在堆内存中分配空间
看String类的源码:
String是声明为final的,不可被继承
String实现了Serializable接口,是支持序列化的;实现了Comparable接口,是可以比较大小的
还有个很重要的一点
在JDK8及以前,内部定义的是final char[] value来存储字符串数据;JDK9开始改为byte[]
StringBuffer和StringBuilder以及HostSpot虚拟机中与String相关的部分,都进行了同步修改和更新
在http://openjdk.java.net/jeps/254中提供了相关说明
String类的几个容易困惑的问题
- test方法中,hello字符串存在于字符串常量池中,字符串常量池中的字符串是唯一的
每次new String()会在堆内存中分配空间并创建对象,s1和s2保存的是不同String对象的引用地址,所以System.out.println(s1 == s2)会输出false
public void test(){
String s1 = new String("hello");
String s2 = new String("hello");
System.out.println(s1 == s2);
}
- test1方法中,hello字符串存在于字符串常量池中,而字符串常量池中的字符串是唯一的,s1和s2保存的都是字符串常量池中hello字符串的引用地址,所以System.out.println(s1 == s2)会输出true
public void test1(){
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2);
}
- test2方法中,字符串常量的值在编译的时候就确定了,hello和world都是字符串常量,所以s2在编译的时候就已经确定了,s2保存的是字符串常量池中helloworld字符串的引用地址,所以System.out.println(s1 == s2)会输出true
public void test2(){
String s1 = "helloworld";
String s2 = "hello" + "world";
System.out.println(s1 == s2);
}
- test3方法中,s4由s2和s3这两个字符串类型的变量相加得到,注意是变量的相加,所以不能在编译期确定下来,所以s4会在堆内存中创建一个String对象,s4保存这个String对象的引用地址,而s1保存的是字符串常量池中helloworld字符串的引用地址,所以System.out.println(s1 == s2)会输出false
public void test3(){
String s1 = "helloworld";
String s2 = "hello";
String s3 = "world";
String s4 = s2 + s3;
System.out.println(s1 == s4);
}
- test4方法中,s2和s3这两个字符串类型的变量被final关键字修饰,程序中使用s2和s3遍历的地方,编译器会直接用该变量的值进行替代。所以s4的值在编译期就确定为helloworld,s4保存的是字符串常量池中helloworld字符串的引用地址,而s1保存的也是字符串常量池中helloworld字符串的引用地址,所以System.out.println(s1 == s2)会输出true
public void test4(){
String s1 = "helloworld";
final String s2 = "hello";
final String s3 = "world";
String s4 = s2 + s3;
System.out.println(s1 == s4);
}