线索:我想采用实例代码驱动的方式来一步步地分析,这也符合我们探知新事物的过程。
一、基本数据类型的内存分配
代码1:
看到上面的输出结果,如果你还是有些不能理解的,那就耐心地接着看我的分析吧。
分析:
编号1:在java编译时期,当编译到“intp1=1000;”时会在栈中压入1000,其实后面的p2,i1,i2都是指向这个1000,这样可以提高java的性能,所以编号1、编号2、编号3的输出结果都是true.其实char,float,double等基本数据类型都是这样的。
编号2、编号3:同编号1
编号4:这是java中的自动装箱机制,将基本数据类型int自动转为类类型Integer,这是jdk1.5以上才有的功能,jdk1.5以下编译时会报错。自动装箱时java底层会调用Integer.valueOf(inti)方法自动装箱,下面我们来看看Integer.valueOf(inti)的源码吧:
注:分析源码我们知道IntegerCache.high其实就是127,在IntergerCache的静态块中定义的。
源码的意思是当i的值在-128—127之间时会返回IntegerCache.cache[]中的对象,其他的新建一个Integer对象。其实Integer类是这样实现的:考虑到-128—127之间的对象经常使用,就在Integer创建时将值在-128—127之间的对象先创建好,放在池中,以后要使用时,这些对象就不用重新创建了,目的在于提高性能。其实这种机制在Character中也用到了,Character是创建ASCII在0—127之间的对象。补充说明:Integer创建的对象引用在栈中,对象的内容在堆区,栈中的值是堆中对象的地址。Character、Long、Short等包装类都是这样的。所以编号4的输出结果是false,因为值大于127,java新创建了一个对象。
编号5:因为值在-128—127之间,所以两个引用指向的是堆区的同一个对象。
编号6:当使用new创建对象时,都会新创建一个对象,即在栈中创建一个引用,在堆中创建该对象,引用指向对象。
编号7:这种情况有些人可能会不太清楚,其实这是java的自动拆箱机制,当int和Integer发生操作时,Integer类型对象会自动拆箱成int值,这时比较的是两个int值,而我们前面分析了,int值都会指向常量池中的数据,所以,两者指向的是同一块空间。结果编号7输出true
编号8:同编号7,也是Integer的自动拆箱。
编号9:我想,分析了这么多,编号9不用我说,你也应该懂了,呵呵,这里就不赘述了哦~分析了这么多,终于第一块代码分析完了。
二、String类型的内存分配
大家都知道String类型是类类型,不过String类型是一个特殊的类类型,那它特殊在哪呢?代码2:
编号1:String类型是一个很特殊的类型,当我们使用Stringstr=”abc”;这种定义方法时,”abc”会放入常量池中,以后如果再有定义Stringstr2=”abc”时,其实str和str2指向的是常量池中同一个对象。而只有当使用new创建时才会每次都创建一个新的对象。(我觉得这是String类型和其他类类型的特殊之处)
编号2、编号3:编号1已经分析了。
编号4:执行到Stringc=b+"c";这一句时,java底层会先创建一个StringBuilder对象,封装b,接着再加上“c”,最后再创建一个String对象,将StringBuilder中的值赋给该String对象,用c来指向它。.其实此时的c指向的对象已经不是a指向的对象了。
编号5:当用final修饰后,s6就变为了常量,在常量池中创建“12”,当执行到Strings7=s6+"3";时,编译器直接就把s6当成了“12”,s7此时就已是“123”,它指向常量池中的“123”,所以s5和s7指向的是同一个对象,输出为true。
三、StringBuilder,StringBuffer,String的对比
(一)StringString类型的值是不可变的,听到这句话后可能你会有疑问,我们的String对象可以重新赋值呀,这里有两种情况,情况一:Stringstr=”abc”;,情况二:Stringstr=newString(“abc”);采用情况一重新赋值时,java会先看常量池中有没有“abc”,如果有则直接指向它,如果没有,在编译时就创建一个常量放入常量池中;对于情况二:str则重新指向一个先创建的对象,该新对象在堆中。下面提出问题:为什么String是不可变的呢?我们来看看String的源码:
我们看到String类型是用一个用final修饰的char数组来存储字符串的,所以String类型是不可变的,(其实Short,Character,Long等包装类型也是这样实现的),根据上面对String类型的分析,如果要改变String的值,就要重新创建一个对象,这无疑性能会很差。为了优化String,sun公司添加了StringBuffer,在jdk1.5之后又添加了StringBuilder。
(二)下面我们来分析一下StringBufferStringBuffer作为字符串缓冲类,当进行字符串拼接时,不会重新创建一个StringBuffer对象,而是直接在原有值后面添加,因为StringBuffer类继承了AbstractStringBuffer类,分析后者的源码后,我们发现存储字符串的char[]没有被final修饰。至于StringBuffer类是怎样扩充自己的长度的,我们可以参考它的append()方法,这里不再赘述。不过一定要提出的是:StringBuffer是线程安全的,它的方法体是被synchronized修饰了的。
(三)StringBuilder有是怎么样的呢?StringBuilder基本实现了StringBuffer的功能,最大的不同之处在于StringBuilder不是线程安全的。
(四)String、StringBuffer、StringBuilder的性能比较代码三:
实验结果为:
显然,StringBuilder的性能最好,String的性能最差,而且差很多;不过StringBuffer的线程安全性很好,性能也比较接近StringBuilder,所以我推荐的选择使用顺序为:StringBuffer>StringBuilder>String;
四、java传参
下面我们我看一段代码,不过有点长,请大家有点耐心哦~代码四:
输出结果为:
分析:这个例子我举得有点大,不过我觉得如果把我举得这个例子的参数传递完全搞懂了,你对java的参数传递过程就比较了解了。
不过在分析之前,我想给大家java传参的一个思想:java只有值传递,没有引用传递,也没有指针传递。对于基本数据类型,java是直接传值,其实就是将形参指向栈中的那个值;对于类类型(比如String,StringBuffer,自定义类类型等)是传引用(在栈中)的值,也就是堆中对应对象的地址。这个在我认为也是值传递。
下面我们开始分析test()方法1、首先定义了int类型变量,int类型变量传入change()方法是简单的值传递,这个大家都知道,所以就不说了;
2、下面是String类型的变量,大家可能会想,String类型是类类型啊,当调用change方法后test方法中也应该会发生变化呀,呵呵,其实这时你忘了String类型是不可变的,因为它存储数据的char[]是用final修饰过的。当change方法中改变了p2的值后,其实p2指向的已经是另一块内存空间了。
3、下面是StringBuffer类型,之前已说类类型传递变量的地址,所以bs和p3指向的是同一块内存空间,当p3重新赋值时,bs也会跟着变得。
4、下面是自定义的类类型,我不想再用文字述说了,就用一个图来表示吧,我相信你现在可以自己分析了。
五、java对象的克隆机制(以上概念的应用)
概念引入:
我相信大家都听过java中的“克隆”这个名词,在Object类中有一个本地化clone()方法就是用来克隆对象的,其实我们自己也可以用new来克隆对象,但这样的效率会比较低。概念名词:
浅度克隆:要克隆对象的属性如果是类类型变量,只在栈中创建一个该属性的新引用,指向源属性对象;如果是基本数据类型,我相信你懂得。
深度克隆:对于类类型的属性,在栈中和堆中都重新开辟空间,创建一个全新的属性对象。其实Object中的clone()方法就是一种浅度克隆,不过当我们重写该方法时一定要实现Cloneable接口,否则会报异常,代码验证如下:代码五:
这时的运行结果如下,很显然是浅克隆。
当我们把clone()方法中的注释语句“//o.sb=newStringBuffer();”启用后,这就是深度克隆了哦,运行结果如下:
上面实现深度克隆的方法是基于Object的clone()方法的,其实我们也可以采用序列化的方式来实现深度克隆的,这样就不用重写clone()方法了,我们给Point类添加一个deepClone方法,不过一定要让Point类实现Serializeble接口哦~,deepClone方法如下:
呵呵,通过这些实验,我想你对java的克隆机制还是比较了解了,具体的分析我也没有必要再说了。就到此为止吧