Java内存分配,基本数据类型及自动拆装箱
一 Java内存分配
一般Java在内存分配时会涉及到以下区域:
- 寄存器:我们在程序中无法控制
- 栈:存放 基本数据类型的数据和 对象的引用 ,但对象本身不存放在栈中,而是存放在堆中
- 堆:存放new产生的数据
- 静态域:存放在对象中用static定义的静态成员
- 常量池: 存放常量
- 非RAM存储:硬盘永久存储空间
堆
堆内存用来存放由new创建的对象和数组。在堆中分配的内存,有Java虚拟机的垃圾回收器管理。
在堆中产生了一个数组或对象后,还可以 在栈中定义一个特殊的变量,让栈中这个变量的取值等于数组或对象在堆内存中的首地址,栈中的这个变量就成了数组或对象的引用变量。
引用变量是普通的变量,定义时在栈中分配,引用变量在程序运行到其作用域之外后被释放。而数组和对象本身在堆中分配,即使程序 运行到使用 new 产生数组或者对象的语句所在的代码块之外,数组和对象本身占据的内存不会被释放,数组和对象在没有引用变量指向它的时候,才变为垃圾,不能在被使用,但仍 然占据内存空间不放,在随后的一个不确定的时间被垃圾回收器收走(释放掉)。
栈
函数中定义的一些基本类型的数据变量和对象的引用变量都在函数的栈内存中分配。
栈的优势是存取速度比堆要快,仅次于直接位于CPU 的寄存器,而且数据可以共享。但是存在栈中的数据大小与生存周期必须是确定的。
当在一段代码块定义一个变量时,Java就在栈中 为这个变量分配内存空间,当该变量退出该作用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。
二 基本数据类型
-
基本数据类型(原始数据类型): byte short int long float double char boolean
基本类型的变量持有原始值。这种类型是通过诸如 int a=7; 的形式来定义的,称为自动变量。这里自动变量是字面值。不是类的实例,即不是类的引用,这里并没有类的存在。a 是指向一个 int 类型的引用,指向 7 这个字面值。由于其大小确定生存期可知(这些定义在某个程序块中,程序块退出后,字段值就消失),因此存在栈中.
由于栈的数据可以共享,因此int a=3; int b=3;
这段代码,编译器首先处理int a =3;
,先会在栈中创建一个变量为 a 的引用,然后查找有没有字面值为 3的地址,没有找到,就开辟一个存放 3 这个字面值的地址,然后将a 指向 3 的地址。接下来处理int b =3;
在创建完 b 这个引用变量后,由于栈中已经有 3 这个字面值,便将 b 指向 3 的地址。【定义变量,给变量赋值】实际上,Java中还存在另外一种基本类型
void
,它也有对应的包装类java.lang.Void
,不过我们无法直接对它们进行操作。
三 包装类数据
1. 为什么需要包装类
Java中的基本类型不是面向对象的,它们只是纯粹的数据,除了数值本身的信息之外,基本类型数据不带有其他信息或者可操作方法。这在实际使用中存在很多不足,比如,在集合类中,我们是无法将int 、double等类型放进去的。因为集合的容器要求元素是Object类型。为了解决这个不足,对每个基本类型都对应了一个引用的类型,称为装箱基本类型。
为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作。
2. 拆箱与装箱
把基本数据类型转换成包装类的过程就是打包装,英文对应于boxing,中文翻译为装箱。
反之,把包装类转换成基本数据类型的过程就是拆包装,英文对应于unboxing,中文翻译为拆箱。
在Java SE5之前,可以通过以下代码进行装箱:
Integer i = new Integer(6);
3. 自动拆箱与自动装箱
自动装箱: 将基本数据类型自动转换成对应的包装类。
自动拆箱:将包装类自动转换成对应的基本数据类型。
Integer i =6; //自动装箱
int j= i; //自动拆箱
4. 自动装箱与自动拆箱的实现原理
自动拆装箱的代码:
public static void main(String[]args){
Integer integer=1; //装箱
int i=integer; //拆箱
}
反编译以上代码后可以得到以下代码:
public static void main(String[]args){
Integer integer=Integer.valueOf(1);
int i=integer.intValue();
}
从反编译后的代码可以看出,int的自动装箱都是通过Integer.valueOf()
方法来实现的,Integer的自动拆箱都是通过integer.intValue
来实现的。如果读者感兴趣,可以试着将八种类型都反编译一遍 ,你会发现以下规律:
自动装箱都是通过包装类的
valueOf()
方法来实现的。自动拆箱都是通过包装类对象的xxxValue()
来实现的。
5. 整型的取值范围
Java中的整型主要包含byte
、short
、int
和long
这四种,表示的数字范围也是从小到大的,之所以表示范围不同主要和他们存储数据时所占的字节数有关。
整型的这几个类型中,
- byte:byte用1个字节来存储,范围为-128(-27)到127(27-1),在变量初始化的时候,byte类型的默认值为0。
- short:short用2个字节存储,范围为-32,768 (-2^15)到32,767 (2^15-1),在变量初始化的时候,short类型的默认值为0,一般情况下,因为Java本身转型的原因,可以直接写为0。
- int:int用4个字节存储,范围为-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1),在变量初始化的时候,int类型的默认值为0。
- long:long用8个字节存储,范围为-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1),在变量初始化的时候,long类型的默认值为0L或0l,也可直接写为0。
6. 超出范围怎么办
上面说过了,整型中,每个类型都有一定的表示范围,但是,在程序中有些计算会导致超出表示范围,即溢出。如以下代码:
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;
int k = i + j;
System.out.println(k); //输出: -2
这就是发生了溢出,溢出的时候并不会抛异常,也没有任何提示。所以,在程序中,使用同类型的数据进行运算的时候,一定要注意数据溢出的问题。
7. 缓存
Java SE的自动拆装箱提供了一个和缓存有关的功能,看如下代码:
public static void main(String... strings) {
Integer integer1 = 3;
Integer integer2 = 3;
if (integer1 == integer2)
System.out.println("integer1 == integer2");
else
System.out.println("integer1 != integer2");
Integer integer3 = 300;
Integer integer4 = 300;
if (integer3 == integer4)
System.out.println("integer3 == integer4");
else
System.out.println("integer3 != integer4");
}
我们普遍认为上面的两个判断的结果都是false。虽然比较的值是相等的,但是由于比较的是对象,而对象的引用不一样,所以会认为两个if判断都是false的。在Java中,==比较的是对象引用,而equals比较的是值。所以,在这个例子中,不同的对象有不同的引用,所以在进行比较的时候都将返回false。奇怪的是,这里两个类似的if条件判断返回不同的布尔值。
上面这段代码真正的输出结果:
integer1 == integer2
integer3 != integer4
原因就和Integer中的缓存机制有关。在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。
适用于整数值区间-128 至 +127。
只适用于自动装箱。使用构造函数创建对象不适用。
缓存其实是由valueOf
方法实现的,该方法有可能通过缓存经常请求的值而显著提高空间和时间性能 。
查看Integer的valueOf
方法:
public static Integer valueOf(int i) {
assert IntegerCache.high >= 127;
//static final int low = -128;
//当-128=<i<=127的时候,就直接在缓存中取出 i de Integer 类型对象
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
//否则就在堆内存中创建
return new Integer(i);
}
看出对于范围 [-128,127] 的整数,valueOf
方法做了特殊处理。采用IntegerCache.cache[i + (-IntegerCache.low)];
这个方法。
查看 IntegerCache
类的实现为:
private static class IntegerCache {
static final int low = -128; //最小值是固定的
static final int high;
static final Integer cache[];//cache 缓存是一个存放Integer类型的数组
static { //初始化,最大值可以配置
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
}
high = h;
cache = new Integer[(high - low) + 1]; //初始化数组
int j = low;
//缓存区间数据
for(int k = 0; k < cache.length; k++)
//将-128~127包装成256个对象存入缓存
cache[k] = new Integer(j++);
}
private IntegerCache() {}
IntegerCache
初始化后内存中就有Integer
缓冲区cache[]
了,-128~127
区间的int
值有其对应的的包装对象。这就是 valueOf
方法真正的优化方法。
public class ZhuangXaing {
public static void main(String[] args) {
Integer i= new Integer(12);
Integer j=12;
Integer k=Integer.valueOf(12);
Integer l= new Integer(232);
Integer m=232;
Integer n=232;
Double q = 232.0;
System.out.println("use ==.......");
System.out.println(i==12);
System.out.println(i==j);
System.out.println(j==k);
System.out.println(l==232);
System.out.println(l==m);
System.out.println(m==n);
System.out.println("use equals.....");
System.out.println(m.equals(n));
System.out.println(m.equals(q));
}
}
输出结果:
use ==.......
true
false
true
true
false
false
use equals.....
true
false
Integer i= new Integer(12);
是指明了在堆内存中创建对象;
Integer j=12;
是自动装箱,调用valueOf 方法,返回return IntegerCache.cache[12 + 128]
, 得到的是Integer 缓冲池中的对象。Integer k=Integer.valueOf(12);
与Integer j=12;
本质上相同,指向缓冲池中同一对象。包装对象与数值比较,自动拆箱。
而对于大于127 的数值,执行的都是return new Integer(i)
都在堆内存中,但是地址不同。
对于equals 方法比较的是数值大小:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
可以看出比较的 obj
如果是Integer
的实例,则比较拆箱后数值的是否相等。否则返回false。
下面这段代码输出结果是什么:
public class Main {
public static void main(String[] args) {
Double i1 = 100.0;
Double i2 = 100.0;
Double i3 = 200.0;
Double i4 = 200.0;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
//false
//false
因为Double
类的valueOf
方法会采用与Integer
类的valueOf
方法不同的实现。很简单:在某个范围内的整型数值的个数是有限的,而浮点数却不是。
其他的包装器:
Boolean: (全部缓存)
Byte: (全部缓存)
Character ( <=127 缓存)
Short (-128~127 缓存)
Long (-128~127 缓存)
Float (没有缓存)
Doulbe (没有缓存)
下面这段代码输出结果是什么:
public class Main {
public static void main(String[] args) {
Boolean i1 = false;
Boolean i2 = false;
Boolean i3 = true;
Boolean i4 = true;
System.out.println(i1==i2);
System.out.println(i3==i4);
}
}
先看Boolean
类的源码 ,valueOf
方法的实现:
public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}
而其中的 TRUE
和FALSE
又是什么呢?在Boolean
中定义了2个静态成员属性:
public static final Boolean TRUE = new Boolean(true);
/**
* The <code>Boolean</code> object corresponding to the primitive
* value <code>false</code>.
*/
public static final Boolean FALSE = new Boolean(false);
由此可知上面代码输出都为true 。
8. 自动拆箱导致空指针异常
Map<String,Boolean> map = new HashMap<String, Boolean>();
Boolean b = (map!=null ? map.get("test") : false);
执行该代码,会报NullPointerException。
Exception in thread "main" java.lang.NullPointerException
既然报了空指针,那么一定是有些地方调用了一个null的对象的某些方法。在这短短的两行代码中,看上去只有一处方法调用map.get("test")
,但是我们也都是知道,map已经事先初始化过了,不会是Null,那么到底是哪里有空指针呢。反编译上面代码:
HashMap hashmap = new HashMap();
Boolean boolean1 = Boolean.valueOf(hashmap == null ? false : ((Boolean)hashmap.get("test")).booleanValue());
可以看出((Boolean)hashmap.get("test")).booleanValue()
的执行过程中报了空指针:
hashmap.get(“test”)->null;
(Boolean)null->null;
null.booleanValue()->报错
在三目运算符的语法规范中,当第二,第三位操作数分别为基本类型和对象时,其中的对象就会拆箱为基本类型进行操作。
由于使用了三目运算符,并且第二、第三位操作数分别是基本类型和对象。所以对对象进行拆箱操作,由于该对象为null,所以在拆箱过程中调用null.booleanValue()的时候就报了NullPointerException. 。
正确写法
Map<String,Boolean> map = new HashMap<String, Boolean>();
Boolean b = (map!=null ? map.get("test") : Boolean.FALSE);
保证三目运算符的第二第三位操作数都为对象类型即可。