think in java interview-高级开发人员面试宝典(四)
2014-01-15 13:33
591 查看
java实现shallow clone(浅克隆)与深克隆(deep clone)
克隆就是复制一个对象的复本.但一个对象中可能有基本数据类型,如:int,long,float 等,也同时含有非基本数据类型如(数组,集合等)被克隆得到的对象基本类型的值修改了,原对象的值不会改变.这种适合shadow clone(浅克隆).但如果你要改变一个非基本类型的值时,原对象的值却改变了,.比如一个数组,内存中只copy他的地址,而这个地址指向的值并没有 copy,当clone时,两个地址指向了一个值,这样一旦这个值改变了,原来的值当然也变了,因为他们共用一个值.,这就必须得用深克隆(deep clone)
以下举个例子,说明以上情况.
被克隆类:ShadowClone.java
[java]view plaincopy<span style="font-size:12px;">publicclass ShadowClone implements Cloneable
{
// 基本类型
privateint a;
// 非基本类型
private String b;
// 非基本类型
privateint[] c;
// 重写Object.clone()方法,并把protected改为public
@Override
public Object clone()
{
ShadowClone sc = null;
try
{
sc = (ShadowClone) super.clone();
} catch (CloneNotSupportedException e)
{
e.printStackTrace();
}
return sc;
}
publicint getA()
{
return a;
}
publicvoid setA(int a)
{
this.a = a;
}
public String getB()
{
return b;
}
publicvoid setB(String b)
{
this.b = b;
}
publicint[] getC()
{
return c;
}
publicvoid setC(int[] c)
{
this.c = c;
}
}</span>
测试类Test.java
[java]view plaincopy<span style="font-size:12px;">publicclass Test
{
publicstaticvoid main(String[] args) throws CloneNotSupportedException
{
ShadowClone c1 = new ShadowClone();
//对c1赋值
c1.setA(100) ;
c1.setB("clone1") ;
c1.setC(newint[]{1000}) ;
System.out.println("克隆前: c1.a="+c1.getA() );
System.out.println("克隆前: c1.b="+c1.getB() );
System.out.println("克隆前: c1.c[0]="+c1.getC()[0]);
System.out.println("-----------") ;
//克隆出对象c2,并对c2的属性A,B,C进行修改
ShadowClone c2 = (ShadowClone) c1.clone();
//对c2进行修改
c2.setA(50) ;
c2.setB("clone2");
int []a = c2.getC() ;
a[0]=500 ;
c2.setC(a);
System.out.println("克隆后: c1.a="+c1.getA() );
System.out.println("克隆后: c1.b="+c1.getB() );
System.out.println("克隆后: c1.c[0]="+c1.getC()[0]);
System.out.println("---------------") ;
System.out.println("克隆后: c2.a=" + c2.getA());
System.out.println("克隆后: c2.b=" + c2.getB());
System.out.println("克隆后: c2.c[0]=" + c2.getC()[0]);
}
}</span>
结果:
克隆前: c1.a=100
克隆前: c1.b=clone1
克隆前: c1.c[0]=1000
-----------
克隆后: c1.a=100
克隆后: c1.b=clone1
克隆后: c1.c[0]=500
---------------
克隆后: c2.a=50
克隆后: c2.b=clone2
克隆后: c2.c[0]=500
问题出现了,我指修改了克隆后的对象c2.c的值,但c1.c的值也改变了,与c2的值相等.
以下针对浅克隆得出结论:基本类型是可以被克隆的,但引用类型只是copy地址,并没有copy这个地址指向的对象的值,这使得两个地址指向同一值,修改其中一个,当然另一个也就变了.
由此可见,浅克隆只适合克隆基本类型,对于引用类型就不能实现克隆了.
那如何实现克隆引用对象呢,以下提供一种方法. 用序列化与反序列化实现深克隆(deep copy)
被克隆对象.DeepClone.java
[java]view plaincopy<span style="font-size:12px;">import java.io.Serializable;
//要实现深克隆必须实现Serializable接口
publicclass DeepClone implements Serializable
{
privateint a;
private String b;
privateint[] c;
publicint getA()
{
return a;
}
publicvoid setA(int a)
{
this.a = a;
}
public String getB()
{
return b;
}
publicvoid setB(String b)
{
this.b = b;
}
publicint[] getC()
{
return c;
}
publicvoid setC(int[] c)
{
this.c = c;
}
}</span>
测试类Test.java
[java]view plaincopy<span style="font-size:12px;">import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
publicclass Test
{
publicstaticvoid main(String[] args) throws CloneNotSupportedException
{
Test t = new Test();
DeepClone dc1 = new DeepClone();
// 对dc1赋值
dc1.setA(100);
dc1.setB("clone1");
dc1.setC(newint[] { 1000 });
System.out.println("克隆前: dc1.a=" + dc1.getA());
System.out.println("克隆前: dc1.b=" + dc1.getB());
System.out.println("克隆前: dc1.c[0]=" + dc1.getC()[0]);
System.out.println("-----------");
DeepClone dc2 = (DeepClone) t.deepClone(dc1);
// 对c2进行修改
dc2.setA(50);
dc2.setB("clone2");
int[] a = dc2.getC();
a[0] = 500;
dc2.setC(a);
System.out.println("克隆前: dc1.a=" + dc1.getA());
System.out.println("克隆前: dc1.b=" + dc1.getB());
System.out.println("克隆前: dc1.c[0]=" + dc1.getC()[0]);
System.out.println("-----------");
System.out.println("克隆后: dc2.a=" + dc2.getA());
System.out.println("克隆后: dc2.b=" + dc2.getB());
System.out.println("克隆后: dc2.c[0]=" + dc2.getC()[0]);
}
// 用序列化与反序列化实现深克隆
public Object deepClone(Object src)
{
Object o = null;
try
{
if (src != null)
{
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(src);
oos.close();
ByteArrayInputStream bais = new ByteArrayInputStream(baos
.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
o = ois.readObject();
ois.close();
}
} catch (IOException e)
{
e.printStackTrace();
} catch (ClassNotFoundException e)
{
e.printStackTrace();
}
return o;
}
}</span>
结果:
克隆前: dc1.a=100
克隆前: dc1.b=clone1
克隆前: dc1.c[0]=1000
-----------
克隆前: dc1.a=100
克隆前: dc1.b=clone1
克隆前: dc1.c[0]=1000
-----------
克隆后: dc2.a=50
克隆后: dc2.b=clone2
克隆后: dc2.c[0]=500
深克隆后:修改dc1或者dc2,无论是基本类型还是引用类型,他们的值都不会随着一方改变另一方也改变.
总结:当克隆的对象只有基本类型,不含引用类型时,可以用浅克隆实现. 当克隆的对象含有引用类型时,必须使用深克隆实现.
谈谈final, finally, finalize的区别
final修饰符(关键字)如果一个类被声明为final,意味着它不能再派生出新的子类,不能作为父类被继承。因此一个类不能既被声明为 abstract的,又被声明为final的。将变量或方法声明为final,可以保证它们在使用中不被改变。被声明为final的变量必须在声明时给定初值,而在以后的引用中只能读取,不可修改。被声明为final的方法也同样只能使用,不能重载finally?再异常处理时提供 finally 块来执行任何清除操作。如果抛出一个异常,那么相匹配的 catch 子句就会执行,然后控制就会进入 finally 块(如果有的话)。
finalize方法名
Java 技术允许使用 finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。这个方法是由垃圾收集器在确定这个对象没有被引用时对这个对象调用的。它是在 Object 类中定义的,因此所有的类都继承了它。子类覆盖 finalize() 方法以整理系统资源或者执行其他清理工作。finalize()方法是在垃圾收集器删除对象之前对这个对象调用的。
Java对象的强、软、弱和虚引用
强引用
[java]view plaincopy<span style="font-size:12px;">Object o=new Object();Object o1=o; </span>
上面代码中第一句是在heap堆中创建新的Object对象通过o引用这个对象,第二句是通过o建立o1到new Object()这个heap堆中的对象的引用,这两个引用都是强引用.只要存在对heap中对象的引用,gc就不会收集该对象.如果通过如下代码:[java]view plaincopy<span style="font-size:12px;">o=null;
o1=null;</span>
如果显式地设置o和o1为null,或超出范围,则gc认为该对象不存在引用,这时就可以收集它了。可以收集并不等于就一会被收集,什么时候收集这要取 决于gc的算法,这要就带来很多不确定性。例如你就想指定一个对象,希望下次gc运行时把它收集了,那就没办法了,有了其他的三种引用就可以做到了。其他 三种引用在不妨碍gc收集的情况下,可以做简单的交互。
heap中对象有强可及对象、软可及对象、弱可及对象、虚可及对象和不可到达对象。应用的强弱顺序是强、软、弱、和虚。对于对象是属于哪种可及的对象,由他的最强的引用决定。如下: [java]view plaincopy<span style="font-size:12px;">String abc=new String("abc"); //1
SoftReference<String> abcSoftRef=new SoftReference<String>(abc); //2
WeakReference<String> abcWeakRef = new WeakReference<String>(abc); //3
abc=null; //4
abcSoftRef.clear();//5</span>
第一行在heap对中创建内容为“abc”的对象,并建立abc到该对象的强引用,该对象是强可及的。
第二行和第三行分别建立对heap中对象的软引用和弱引用,此时heap中的对象仍是强可及的。
第四行之后heap中对象不再是强可及的,变成软可及的。同样第五行执行之后变成弱可及的。
SoftReference(软引用)
软引用是主要用于内存敏感的高速缓存。在jvm报告内存不足之前会清除所有的软引用,这样以来gc就有可能收集软可及的对象,可能解决内存吃紧问题,避 免内存溢出。什么时候会被收集取决于gc的算法和gc运行时可用内存的大小。当gc决定要收集软引用是执行以下过程,以上面的abc SoftRef为例:1、首先将abcSoftRef的referent设置为null,不再引用heap中的new String("abc")对象。
2、将heap中的new String("abc")对象设置为可结束的(finalizable)。
3、当heap中的new String("abc")对象的finalize()方法被运行而且该对象占用的内存被释放, abcSoftRef被添加到它的ReferenceQueue中。
注:对ReferenceQueue软引用和弱引用可以有可无,但是虚引用必须有,参见:
[java]view plaincopy<span style="font-size:12px;">Reference(T paramT, ReferenceQueue<? super T>paramReferenceQueue) </span>
被 Soft Reference 指到的对象,即使没有任何 Direct Reference,也不会被清除。
一直要到 JVM 内存不足且 没有 Direct Reference 时才会清除,SoftReference 是用来设计 object-cache 之用的。
如此一来 SoftReference 不但可以把对象 cache 起来,也不会造成内存不足的错误 (OutOfMemoryError)。我觉得 Soft Reference 也适合拿来实作 pooling 的技巧。
[java]view plaincopy<span style="font-size:12px;">A obj = new A();
SoftRefenrence sr = new SoftReference(obj);
//引用时
if(sr!=null){
obj = sr.get();
}else{
obj = new A();
sr = new SoftReference(obj);
}</span>
弱引用
当gc碰到弱可及对象,并释放abcWeakRef的引用,收集该对象。但是gc可能需要对此运用才能找到该弱可及对象。通过如下代码可以了明了的看出它的作用:[java]view plaincopy<span style="font-size:12px;">String abc=new String("abc");
WeakReference<String> abcWeakRef = new WeakReference<String>(abc);
abc=null;
System.out.println("before gc: "+abcWeakRef.get());
System.gc();
System.out.println("after gc: "+abcWeakRef.get()); </span>
运行结果:
before gc: abc
after gc: null
gc收集弱可及对象的执行过程和软可及一样,只是gc不会根据内存情况来决定是不是收集该对象。
如果你希望能随时取得某对象的信息,但又不想影响此对象的垃圾收集,那么你应该用 Weak Reference 来记住此对象,而不是用一般的 reference。[java]view plaincopy<span style="font-size:12px;">A obj = new A();
WeakReference wr = new WeakReference(obj);
obj = null;
//等待一段时间,obj对象就会被垃圾回收
...
if (wr.get()==null) {
System.out.println("obj 已经被清除了 ");
} else {
System.out.println("obj 尚未被清除,其信息是 "+obj.toString());
}
...</span>
在此例中,透过 get() 可以取得此 Reference 的所指到的对象,如果返回值为 null 的话,代表此对象已经被清除。
这类的技巧,在设计 Optimizer 或 Debugger 这类的程序时常会用到,因为这类程序需要取得某对象的信息,但是不可以 影响此对象的垃圾收集。
PhantomRefrence(虚引用)
虚顾名思义就是没有的意思,建立虚引用之后通过get方法返回结果始终为null,通过源代码你会发现,虚引用通向会把引用的对象写进referent,只是get方法返回结果为null。先看一下和gc交互的过程在说一下他的作用。1 不把referent设置为null,直接把heap中的new String("abc")对象设置为可结束的(finalizable).
2 与软引用和弱引用不同,先把PhantomRefrence对象添加到它的ReferenceQueue中,然后在释放虚可及的对象。
你会发现在收集heap中的new String("abc")对象之前,你就可以做一些其他的事情。通过以下代码可以了解他的作用。
[java]view plaincopy<span style="font-size:12px;">import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.reflect.Field;
publicclass Test {
publicstaticboolean isRun = true;
publicstaticvoid main(String[] args) throws Exception {
String abc = new String("abc");
System.out.println(abc.getClass() + "@" + abc.hashCode());
final ReferenceQueue referenceQueue = new ReferenceQueue<String>();
new Thread() {
publicvoid run() {
while (isRun) {
Object o = referenceQueue.poll();
if (o != null) {
try {
Field rereferent = Reference.class
.getDeclaredField("referent");
rereferent.setAccessible(true);
Object result = rereferent.get(o);
System.out.println("gc will collect:"
+ result.getClass() + "@"
+ result.hashCode());
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}.start();
PhantomReference<String> abcWeakRef = new PhantomReference<String>(abc,
referenceQueue);
abc = null;
Thread.currentThread().sleep(3000);
System.gc();
Thread.currentThread().sleep(3000);
isRun = false;
}
}</span>
结果为:
class java.lang.String@96354
gc will collect:class java.lang.String@96354
为什么需要使用软引用
首先,我们看一个雇员信息查询系统的实例。我们将使用一个Java语言实现的雇员信息查询系统查询存储在磁盘文件或者数据库中的雇员人事档案信息。作为一个用户,我们完全有可能需要回头去查看几分钟甚至几秒钟前查看过的雇员档案信息(同样,我们在浏览WEB页面的时候也经常会使用“后退”按钮)。这时我们通常会有两种程序实现方式:一种是把过去查看过的雇员信息保存在内存中,每一个存储了雇员档案信息的Java对象的生命周期贯穿整个应用程序始终;另一种是当用户开始查看其他雇员的档案信息的时候,把存储了当前所查看的雇员档案信息的Java对象结束引用,使得垃圾收集线程可以回收其所占用的内存空间,当用户再次需要浏览该雇员的档案信息的时候,重新构建该雇员的信息。很显然,第一种实现方法将造成大量的内存浪费,而第二种实现的缺陷在于即使垃圾收集线程还没有进行垃圾收集,包含雇员档案信息的对象仍然完好地保存在内存中,应用程序也要重新构建一个对象。我们知道,访问磁盘文件、访问网络资源、查询数据库等操作都是影响应用程序执行性能的重要因素,如果能重新获取那些尚未被回收的Java对象的引用,必将减少不必要的访问,大大提高程序的运行速度。在J2EE的bean设计时我们对有些bean需要实现Serializable,为什么?
实现了Serializable接口的对象,可将它们转换成一系列字节,并可在以后完全恢复回原来的样子。这一过程亦可通过网络进行。这意味着序列化机制能自动补偿操作系统间的差异。换句话说,可以先在Windows机器上创建一个对象,对其序列化,然后通过网络发给一台Unix机器,然后在那里准确无误地重新“装配”。不必关心数据在不同机器上如何表示,也不必关心字节的顺序或者其他任何细节。相关文章推荐
- [置顶] think in java interview-高级开发人员面试宝典代码示例
- think in java interview-高级开发人员面试宝典(七)
- think in java interview-高级开发人员面试宝典(三)
- [置顶] think in java interview-高级开发人员面试宝典(一)
- think in java interview-高级开发人员面试宝典(二)
- [置顶] think in java interview-高级开发人员面试宝典(七)
- think in java interview-高级开发人员面试宝典(八)
- think in java interview-高级开发人员面试宝典(二)
- think in java interview-高级开发人员面试宝典(二)
- think in java interview-高级开发人员面试宝典(三)
- think in java interview-高级开发人员面试宝典(四)
- think in java interview-高级开发人员面试宝典(九)
- think in java interview-高级开发人员面试宝典(九)
- think in java interview-高级开发人员面试宝典(十)
- think in java interview-高级开发人员面试宝典
- think in java interview-高级开发人员面试宝典代码示例
- think in java interview-高级开发人员面试宝典(七)
- think in java interview-高级开发人员面试宝典(八)
- think in java interview-高级开发人员面试宝典(二)
- think in java interview-高级开发人员面试宝典(九)