您的位置:首页 > 其它

由String引出的若干相关问题

2015-04-19 13:44 183 查看
一、参数传递

  Java中的变量类型分为基本数据类型和引用数据类型。引用数据类型存放的是对象实例的地址,一个对象可以有多个引用,即这些引用存放的都是该对象的地址。(基本类型变量和引用类型变量存放在栈(stack)中,对象存放在堆(heap)中)

  在进行参数传递时,不管参数是基本类型还是引用类型,形参(parameter)都是实参(argument)的一个拷贝。具体来说,当传递基本类型时,形参和实参是两个地址不同的变量,虽然变量中存放的值相同,所以改变形参的值,不会影响到实参。传递引用类型时,形参和实参是两个不同的引用,但是由于引用中存放的都是同一个对象的地址,所以可以通过形参去改变实参指向的对象。

  值得注意的是,对于String等不可变(immutable)类型,虽然传递的确实是引用的拷贝,但是因为这种类型没有提供自身修改的函数,每次操作都是新生成一个对象,所以不可能通过传进来的引用去操作原来的对象。

二、不可变(immutable)类型

  不可变的对象指的是一旦创建之后,它的状态就不能改变,包括对象中引用的其他对象的状态也不能改变。String,Integer(包括其他基本数据类型的包装类等都是不可变类。不可变对象对于缓存是非常好的选择,因为你不需要担心它的值会被更改。不可变类的另外一个好处是它自身是线程安全的,你不需要考虑多线程环境下的线程安全问题。

  看两行代码:

  String str=new String("123");

  str=new String("456");//ok

  需要弄清楚的是,String的不可变体现在new String("123")一旦创建,该对象的状态就不会再改变,对String的所有操作,例如subString()等都是重新生成一个对象。而str只是一个引用,因为没有用final关键字限制,所以它仍然可以引用其他的String对象。再看以下两行代码就很清楚了。

  final String str=new String("123");

  str=new String("456");//error

  我们可以通过以下方式创建自己的不可变类:

  1.将类声明为final,保证该类不能被继承,因此类的结构就不能被改变;

  2.把属性定义为private final,保证属性的值只能赋值一次;

  3.对属性不提供setter方法;

  4.通过构造器初始化所有成员,进行深拷贝(deep copy);

  5.在getter方法中,不要直接返回对象本身(的引用),而是克隆对象,并返回对象的拷贝。

举例如下:

finalclass Person{

private final String name;

private final intage;

private final HashMap<String, String> phoneMap;

public String getName() {

returnname;

}

publicint getAge() {

returnage;

}

public HashMap<String, String> getPhoneMap() {

//return phoneMap;

return (HashMap<String, String>)phoneMap.clone();

}

/**

* 实现浅拷贝的构造函数

* @param name

* @param age

* @param phoneMap

*/

// public Person(String name,int age,HashMap<String, String> phoneMap){

// this.name=name;

// this.age=age;

// this.phoneMap=phoneMap;

// }

/**

* 实现深拷贝的构造函数

* @param name

* @param age

* @param phoneMap

*/

public Person(String name,int age,HashMap<String, String> phoneMap){

this.name=name;

this.age=age;

HashMap<String, String> newPhoneMap=new HashMap<>();

Iterator<String> iterator=phoneMap.keySet().iterator();

while (iterator.hasNext()) {

String key = (String) iterator.next();

newPhoneMap.put(key, phoneMap.get(key));

}

this.phoneMap=newPhoneMap;

}

}

publicstaticvoid main(String[] args){

HashMap<String, String> h1=new HashMap<>();

h1.put("Home","1234567");

h1.put("Company", "2345678");

String name="Mr.CHOW";

int age=30;

//新建person对象,以下验证person是否为不可变对象

Person person=new Person(name, age, h1);

System.out.println(name==person.getName());

System.out.println(h1==person.getPhoneMap());

//改变局部变量的值,测试能否影响person对象的状态

name="MR.Law";

age=35;

h1.put("Office", "3456789");

System.out.println("person's name after local variable changed: "+person.getName());

System.out.println("person's age after local variable changed: "+person.getAge());

System.out.println("person's phoneMap after local variable changed: "+person.getPhoneMap());

//通过getter函数获取person中的phoneMap,改变其值,测试能否影响person中phoneMap所引用的对象

HashMap<String, String> h2=person.getPhoneMap();

h2.put("cellphone", "88888");

System.out.println("person's phoneMap after variable changed: "+person.getPhoneMap());

}

  Person类必须限制为final class,Person的所有成员变量必须都用private final限制,且不能提供直接操作成员变量的setter方法。用局部变量h1去构造Person时,如果不进行深拷贝,则person对象中的phoneMap引用仍指向h1,所以通过改变h1就会改变person的状态。对于getPhoneMap(),如果返回时不克隆对象而直接返回对象的引用,就可以通过引用去改变phoneMap对象,从而就改变了person的状态。

三、Java中 final关键字总结

  1.final标记的类不能被继承,这样可以防止类的结构被更改;

  2.final标记的方法不能被重写;

  3.final标记的变量即为常量,只能赋值一次。

  注意,如果用final来标记引用,只有引用自身为常量,只能被赋值一次,但引用指向的对象仍可以更改(不包括引用不可变对象),例如:

  final HashMap<String,String> hm=new HashMap<>();

  hm=new HashMap<>();//error

  hm.put("1","apple");//ok

四、浅克隆(shallow clone)与深克隆(deep clone) (----也叫浅拷贝和深拷贝)

  在Java中,可以使用clone( )复制一个对象,调用clone方法时,分配的内存大小与结构和源对象(即调用clone方法的对象)相同,然后再使用源对象中的各个域,填充新对象中对应的域。由于对象中可能存在对其他对象的引用,所以使得clone有浅克隆和深克隆之分。

  浅克隆:只复制基本类型变量和引用类型变量的值到新对象,并不复制源对象中的引用所指向的对象,所以新对象中的引用和源对象中的引用指向相同的对象

  深克隆:复制基本类型变量的值到新对象,并且复制源对象中的引用所指向的对象,用新对象中的引用来指向该复制的对象,所以新对象中的引用和源对象中的引用指向不同的对象

  Object的clone( )方法默认是浅克隆,如果想要深克隆一个对象,这个对象必须要实现Cloneable接口,实现clone方法,并且在clone方法内部,把该对象中引用的其他对象也要clone一份,这就要求这个被引用的对象必须也要实现Cloneable接口并且实现clone方法。以下用代码来说明:

class Body implements Cloneable{

public Head head;

public Body() {}

public Body(Head head) {this.head = head;}

@Override

protected Object clone() throws CloneNotSupportedException {

Body newBody = (Body) super.clone();

newBody.head = (Head) head.clone();

return newBody;

}

}

class Head implements Cloneable{

public Head() {}

@Override

protected Object clone() throws CloneNotSupportedException {

returnsuper.clone();

}

}

publicclass FinalClassTest {

publicstaticvoid main(String[] args) throws CloneNotSupportedException {

Body body = new Body(new Head());

Body body1 = (Body) body.clone();

System.out.println("body == body1 : " + (body == body1) );

System.out.println("body.head == body1.head : " +

(body.head == body1.head));

}

}

打印结果为:body == body1 : false

body.head == body1.head : false

  在派生类中覆盖Object的clone()方法时,一定要先调用super.clone(),因为在运行时刻,Object中的clone()识别出你要复制的是哪一个对象,然后为此对象分配空间,进行对象的复制。以上代码中,在Body类重载的clone()里面,首先调用super.clone()建立一个当前对象的拷贝newBody,再调用Head类的clone(),建立一个当前Body类对象中正引用的Head对象的拷贝,最后用newBody中的Head引用指向该拷贝,由此完成了对象的深克隆。

  我们还可以利用串行化来实现深克隆。把对象写到流里的过程是串行化(Serialization)过程,又叫对象序列化,而把对象从流中读出来的(Deserialization)过程叫反序列化。应当指出的是,写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面,因此在Java语言里深克隆一个对象,常常可以先使对象实现Serializable接口,然后把对象(实际上只是对象的一个拷贝)写到一个流里,再从流里读出来便可以重建对象。

五、和String 相关的JVM知识

  Java程序运行时,编译器首先会将源代码编译成字节码文件(class文件)。class文件是8位字节的二进制流 ,由一些紧凑的有意义的项 组成。在class文件中有一个非常重要的项——常量池,常量池中除了包含源代码中所定义的各种基本类型(如int、long等)和对象型(如String及数组)的常量值外,还包含一些以文本形式出现的符号引用,比如:

  类和接口的全限定名;

  字段的名称和描述符;

  方法和名称和描述符。

  源代码中的所有String常量(如"hello world"),都会在编译成class文件时,形成标志号为8(CONSTANT_String)的常量表。当JVM使用类加载器加载 class文件时,会为对应的常量池建立一个内存数据结构,并存放在方法区中。同时JVM会自动为CONSTANT_String常量表中的字符串常量值在堆中创建新的String对象,intern字符串对象,又叫拘留字符串对象。然后把CONSTANT_String常量表的入口地址转变成这个堆中String对象的直接地址(常量池解析)。

  所以源代码中所有相同字面值的字符串常量只可能建立唯一一个拘留字符串对象。在Java程序中,可以调用String的intern()方法,如果常量池中已经有了当前String的值,那么返回这个常量指向拘留对象的地址。如果没有,则将String值加入常量池中,并创建一个新的拘留字符串对象。

  看以下代码:

  //代码1

  String a=new String("hello world");

  String b=new String("hello world");

  System.out.println(a==b); //打印出false

  //代码2

  String c="hello world";

  String d="hello world";

  System.out.println(c==d); //打印出true

  代码1中,a,b中存储的是JVM在堆中new出来的两个String对象的内存地址,虽然两个对象中存储的字符序列相同,但两个对象的内存地址不同,所以"=="不成立。而代码2中,c,d存放的都是常量池中"hello world"在堆中对应的唯一拘留字符串对象的地址,所以"=="成立。

六、String的"+"操作与StringBuilder

  不可变性会带来一定的效率问题,为String 重载的"+"操作符就是一个例子。重载的意思是,一个操作符在应用于特定的类时,被赋予了特殊的意义(用于String的"+"与"+="是Java中仅有的两个重载过的操作符,而Java不允许程序员重载任何操作符)。

  String mango="mango";

  String s="abc"+mango+"def"+43;

  System.out.println(s);

  在以上对String的"+"操作中,由于其中含有引用类型,所以编译器会自动引入更高效的StringBuilder类完成拼接操作。首先编译器创建一个StringBuilder对象,接着为每个字符串调用一次StringBuilder的append()方法,总共四次,最后调用toString()生成结果对象。

  再看三组代码:

  //代码1

  String a="hello";

  String b="world";

  String c=a+b;

  String d="helloworld";

  System.out.println(c==d);//打印结果为:false

  //代码2

  String a="hello"+"world";

  String b="helloworld";

  System.out.println(a==b);//打印结果为:true

  //代码3

  final String a="hello";

  final String b="world"

  String c=a+b;

  String d="helloworld"

  System.out.println(c==d);//打印结果为:true

  代码1中,a,b存储的是堆中两个拘留字符串对象的地址,执行a+b时,因为引用的值在编译期是无法确定的,所以不能直接优化成"helloworld"。编译器会创建StringBuilder对象,先后调用append()完成字符串的合并,最后调用toString()生成一个新的对象,而d对应的是"helloworld"对应的拘留字符串对象的地址,所以c与d引用的对象的地址不一样。

  代码2中,"hello"+"world"在编译期就会合并成常量"helloworld",所以a与b引用的都是"helloworld"对应的拘留字符串对象,所以两者引用的对象的地址一样。

  代码3中,因为a和b有final修饰,a+b会被直接解析成"hello"+"world",因此情况和代码2中的相同。

  对于有引用参加的字符串"+"操作,虽然编译器会自动使用StringBuilder来帮我们优化性能,但是如果操作在循环中进行,则每次循环中都要依次创建和回收一个StringBuilder对象和一个String对象(调用toString生成),而JVM运行程序的耗费时间主要是在创建和回收对象上,所以我们最好自己创建一个StringBuilder对象,这样每次就只需要调用append()即可。

  最后,谈下String,StringBuffer,StringBuilder三者的区别。String是不可变类型,其余两者是可变类型。StringBuffer是线程安全的,StringBuilder是1.5引入的,前身就是StringBuffer。StringBuilder不是线程安全的,但是效率比StringBuffer稍高。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: