首先,我们知道,Java中的数据类型分为两种,基本数据类型和引用数据类型。而基本数据类型,为什么不直接使用他们的包装类呢,例如Integer、Long等等呢?下面是Thinking inJava 中的解释:
有一系列类需特别对待;可将它们想象成“基本”、“主要”或者“主”(Primitive)类型,进行程序设计时要频繁用到它们。之所以要特别对待,是由于用new创建对象(特别是小的、简单的变量)并不是非常有效,因为new将对象置于“堆”里。对于这些类型,Java采纳了与C和C++相同的方法。也就是说,不是用new创建变量,而是创建一个并非句柄的“自动”变量。这个变量容纳了具体的值,并置于堆栈中,能够更高效地存取。
Java决定了每种主要类型的大小。就象在大多数语言里那样,这些大小并不随着机器结构的变化而变化。这种大小的不可更改正是Java程序具有很强移植能力的原因之一。
可以看到,Sun是为了系统的优化,才采用基本的数据类型。
那么,基本数据类型和引用数据类型到底有什么区别呢 ?
下面依然是Thinking in Java中的话:
程序运行时,我们最好对数据保存到什么地方做到心中有数。特别要注意的是内存的分配。有六个地方都可以保存数据:
(1) 寄存器。这是最快的保存区域,因为它位于和其他所有保存方式不同的地方:处理器内部。然而,寄存器的数量
十分有限,所以寄存器是根据需要由编译器分配。我们对此没有直接的控制权,也不可能在自己的程序里找到寄存器
存在的任何踪迹。
(2) 堆栈。驻留于常规RAM(随机访问存储器)区域,但可通过它的“堆栈指针”获得处理的直接支持。堆栈指针若向
下移,会创建新的内存;若向上移,则会释放那些内存。这是一种特别快、特别有效的数据保存方式,仅次于寄存器
。创建程序时,Java编译器必须准确地知道堆栈内保存的所有数据的“长度”以及“存在时间”。这是由于它必须生
成相应的代码,以便向上和向下移动指针。这一限制无疑影响了程序的灵活性,所以尽管有些Java数据要保存在堆栈
里——特别是对象句柄,但Java对象并不放到其中。
(3) 堆。一种常规用途的内存池(也在RAM区域),其中保存了Java对象。和堆栈不同,“内存堆”或“堆”(Heap)
最吸引人的地方在于编译器不必知道要从堆里分配多少存储空间,也不必知道存储的数据要在堆里停留多长的时间。
因此,用堆保存数据时会得到更大的灵活性。要求创建一个对象时,只需用new命令编制相关的代码即可。执行这些
代码时,会在堆里自动进行数据的保存。当然,为达到这种灵活性,必然会付出一定的代价:在堆里分配存储空间时
会花掉更长的时间!
(4) 静态存储。这儿的“静态”(Static)是指“位于固定位置”(尽管也在RAM里)。程序运行期间,静态存储的
数据将随时等候调用。可用static关键字指出一个对象的特定元素是静态的。但Java对象本身永远都不会置入静态存
储空间。
(5) 常数存储。常数值通常直接置于程序代码内部。这样做是安全的,因为它们永远都不会改变。有的常数需要严格
地保护,所以可考虑将它们置入只读存储器(ROM)。
从上面我们就可以看到,基本数据类型的变量,是存放在堆栈中的,我更喜欢直接称之为栈(stack)。而引用数据类型的引用,
也就是Thinking in Java所说的句柄,也是直接放在栈里面,而引用数据类型引用的对象,则是放在堆(heap)中的。
明白了基本数据类型和引用数据类型在内存中的区别,那么理解下面的问题就和简单了。
一、String对象的创建问题
这本是一个简单的问题,无关轻重,大家用天天使用字符串也用的很happy,你们真的了解字符串吗?
大家知道,String的创建有两种形式,一种是直接放在双引号("")中,一种是直接用new的方式创建,那么这两种方式有什么区别呢?
1、String a="abc";
2、String b=new String("abcdef")
上面的两行代码,分别创建了几个对象呢?
还是先说说JVM对字符串是怎么处理的吧。字符串是一个引用数据类型,根据我们上面对java中基本数据类型和引用数据类型的
分析,String对象肯定是存放在堆中的。关键在于,String对象是一个final的常量,JVM为了管理String和高效性,在堆中,会用一个
特殊的字符串池来保存字符串直接量。
所谓的字符串直接量,就是指字符串由字符串、数值常量直接构成,没有变量,也没有方法。
例如
String a="abc";
String b="abc"+10;
String c="abc"+"def";
这样的字符串,JVM在编译的时候就能完全确定这个字符串,就可以在字符串池中创建该字符串,如果字符串池中已经有这样的
String,就让该变量直接指向在字符串池中的该字符串。
而另外一些不是字符串直接量的字符串
例如
String a="abc"+"abc".lenght();
String b=a+"asc";
JVM无法在编译的时候就确定该字符串,就只能在程序运行期间,在堆中非字符串池中的地方开辟内存,存放这样的字符串。
好了,明白JVM对字符串的处理,在来谈谈String对象创建的问题:(这两行代码是无关的)
1、String a="abc";
2、String b=new String("abcdef");
上面的两个创建字符串的方法到底分别创建了几个对象?
第1行,创建了一个对象,它是一个字符串直接量,直接被创建存放在字符串池中。
第2行,创建了2个对象,"abcdef"本身就是一个字符串直接量,被创建后存放在字符串池中,然后,它又作为一个参数,
传递给了构造器new String(),这样又在堆中非字符串池的地方开辟了块内存,创建了一个字符串对象。
那么,就在问去几个问题,下面的几段代码分别创建了几个对象
1、String a="ab";
2、String b="ab";
3、String d="abcd";
4、String c=a+"cd";
5、String e=new String("abcd")
6、String f="aaa"+"bbb";
答案:
第1行,1个、因为字符串池还是空的,不存在字符串"ab",所以在字符串池创建了一个对象"ab"。
第2行,0个、因为字符串池中"ab"已经存在,所以没有创建,直接将b指向"ab";
第3行,1个、因为也是在字符串池中创建了一个"abcd"对象
第4行,2个、先在字符串池中创建"cd",因为采用了变量,所以要在堆中非字符串池中在创建一个对象a+"cd";
第5行,1个、因为字符串池中已经存在"abcd",直接将他作为一个参数传给构造器new一个新的对象。
第6行,1个,因为在编译的时候就能确定f的值,所以只创建一个"aaabbb"到字符串池中;(很多人误解为3个)
在看下面的
final String a="aaa";
final String b="bbb";
String c=a+b;
字符串c对被创建在哪里,字符串池,还是堆中的其他地方,答案是字符串池中,因为a、b是常量,不是变量。
二、String和StringBuffer的区别
在Java中,String和StringBuffer,StringBuilder都能表示一个字符串。
String的定义是:表示一个字符串常量。那么,看下面的一段代码:
String str="abc";
str="def";
常量,基本数据类型的常量是不可变的,可是我们可以看到,这里所谓的字符串常量str是可变的。这是为什么呢?
引用数据类型的常量,指的是引用的对象不可变,而引用,也即句柄是可变的。也就是多,当一个String类型的字符串
改变时,实际上是它指向的内存单元发生了改变,原来的内存单元的内容并没有发生改变。
StringBuffer的定义是:表示一个字符串变量。StringBuffer strbuf="abc";这样的初始化是错误的,要想初始化一个
StringBuffer类型的字符串必须使用new来初始化,如1、StringBuffer strbuf=new StringBuffer("abc");
2、strbuf=new StringBuffer("def");
3、strbuf.append("def");
由此可见,StringBuffer字符串变量,指的是不仅引用可变,引用的对象也是可变的。
下面的两个例子可以很好的说明这个问题。
1、
String str1="abc";
String str2=str1;
StringBuffer str3=new StringBuffer("abc");
StringBuffer str4=str3;
str1=str1+str2;
str3=str3.append(str4);
System.out.println(str1);
System.out.println(str2);
System.out.println(str3);
System.out.println(str4);
输出的结果:
abcabc
abc
abcabc
abcabc
2、
public class Test5
{
public static void main(String[] args)
{
String str1="java";
StringBuffer str2=new StringBuffer("java");
test(str1);
test(str2);
System.out.println(str1);
System.out.println(str2);
}
public static void test(String str)
{
str=str.replace("j", "i");
}
public static void test(StringBuffer str)
{
str.append("c");
}
}
程序的输出结果为
java
javac
(String对象的处理确实与其他的对象有所区别,在深度克隆中也有体现,对它的处理更像是对一个基本数据类型的处理。
这主要就是因为它是一个常量的原因。)
三、==和equals的问题
==的含义,如果是比较基本数据类型,那么就是比较数据类型字面值的大小。如果是比较引用数据类型,就是比较它们
在内存地址上是否是相同的。
而equals方法,是Object的方法之一,所有的java类都有这个方法,区别只是自己有没有重写的问题。如果没有重写,那么
也是直接比较内存地址是否相同。重写了,那就要看它们是怎么重写的。
对象内容的比较,正是equals方法做的事。
看一下Object对象的equals方法是如何实现的:
boolean equals(Object o){
return this==o;
}
Object对象默认使用了==操作符。所以如果你自创的类没有覆盖equals方法,那你的类使用equals和使用==会得到同样的结果。同样也可以看出,Object的equals方法没有达到equals方法应该达到的目标:比较两个对象内容是否相等。因为答案应该由类的创建者决定,所以Object把这个任务留给了类的创建者
所以当你是用equals方法判断对象的内容是否相等,请不要想当然。因为可能你认为相等,而这个类的作者不这样认为,而类的equals方法的实现是由他掌握的。如果你需要使用equals方法,或者使用任何基于散列码的集合(HashSet,HashMap,HashTable),请察看一下java doc以确认这个类的equals逻辑是如何实现的。
看下面的例子
String str1 = "abc";
String str2 = "abc";
String str3=new String("abc");
String str4=new String("abc");
StringBuffer str5=new StringBuffer("abc");
StringBuffer str6=new StringBuffer("abc");
System.out.println("1:"+(str1==str2));
System.out.println("2:"+(str1.equals(str2)));
System.out.println("3:"+(str2==str3));
System.out.println("4:"+(str2.equals(str3)));
System.out.println("5:"+(str3==str4));
System.out.println("6:"+(str3.equals(str4)));
//System.out.println("7:"+(str4==str5));
System.out.println("8:"+(str4.equals(str5)));
System.out.println("9:"+(str5==str6));
System.out.println("10:"+(str5.equals(str6)));
输出结果是
1:true
2:true
3:false
4:true
5:false
6:true
8:false
9:false
10:false
解释:
String对象的初始化有两种方式,前面已经解释了。
可见,str1和str2表示的"abc"都是存放在字符串池中,而在字符串池中,这两个"abc"其实是一个内存中的数据,
所以str1==str2是true。str1.equals(str2)是true。
str3和str4采用的new方式,那么它们对用的字符串"abc"都是在堆中非字符串池中,分别存放在堆中不同的地方,
所以str2==str3是false。str3==str4是false。
而String和Strinbuffer除了都是直接继承Object之外,并没有其他的直接联系,两者完全是不相干的类。
所以才有str4==str5是false、str4.equals(str5)是false。
而StringBuffer也根本没有重写从父类继承的equals方法,所以
str5==str6是false。str5.equals(str6)是false。
四、数组的问题
数组也是一个引用数据类型,Java中的数组是静态的,也就是说,一旦数组初始化完成指定了它的长度,就不能在去改变它的长度。
可是,数组也是和String一样的,引用的对象不可变,但是引用是可变的
int[] a=new int[5];
a代表的是引用,new int[5]才代表引用的对象。也就是说new int[5]这个真正代表数组的对象在内存里面是不能改变所占内存的大小,
但是,却可以去改变数组变量a的引用,指向别的数组对象。
在来说下数组在Java中的内存分配问题。
数组的引用是放在栈中的,而引用的对象是放在堆中的,不管这个数组中的元素是基本数据类型,还是引用数据类型。
只要记住一点:
在Java中,所有局部变量都是放在栈里面保存的,不管是基本数据类型的变量,还是引用数据类型的变量,都是存储在各自的方法
栈区;但引用类型变量所引用的对象,都是放在堆内存中的。
值得一说的是,如果是一个引用类型的数组,例如
String[] str=new String[6];
引用str存放在栈里面,而str指向的堆内存中也是引用,它说指向的引用才真正的指向堆内存中的字符串。
转自:http://blog.csdn.net/lonelyroamer/article/details/7899129
,