1. 基本数据类型
Java基本数据类型(也称原生数据类型,primitive type)一共有8种。
1.1 特性
1.1.1 高性能
原生数据类型的声明方式一般如下:
int a = 3;
这里的a是一个指向int类型的引用,指向3这个字面值。这些字面值的数据,由于大小可知,生存期可知(这些字面值定义在某个程序块里面,程序块退出后,字段值就消失了),出于追求速度的原因,就存在于栈中,存在栈中的数据拥有较高的存取速率,所以原生数据类型比引用类型性能更高一些。
1.1.2 可共享
另外,栈有一个很重要的特殊性,就是存在栈中的数据可以共享。比如: 我们同时定义:
int a=3; int b=3;
编译器先处理int a = 3;首先它会在栈中创建一个变量为a的引用,然后查找有没有字面值为3的地址,没找到,就开辟一个存放3这个字面值的地址,然后将a指向3的地址。接着处理int b = 3;在创建完b这个引用变量后,由于在栈中已经有3这个字面值,便将b直接指向3的地址。这样,就出现了a与b同时均指向3的情况。
定义完a与b的值后,再令a = 4;那么,b不会等于4,还是等于3。在编译器内部,遇到时,它就会重新搜索栈中是否有4的字面值,如果没有,重新开辟地址存放4的值;如果已经有了,则直接将a指向这个地址。因此a值的改变不会影响到b的值。
1.2 整型
Java提供了4种整型类型,与其他语言不同的是,Java中整型的取值范围与宿主机器无关,具有跨平台性,无论什么平台,int都是4个字节,long都是8个字节。
类型 | 存储需求 | 取值范围 |
---|---|---|
int | 4字节 | -2147483648〜2147483647 |
short | 2字节 | -32768〜32768 |
long | 8字节 | -9223372036854775808〜�9223372036854775807 |
byte | 1字节 | -128〜127 |
1.3 浮点型
Java提供了2种浮点类型
类型 | 存储需求 | 有效整数位 |
---|---|---|
float | 4字节 | 6~7位(比int能表示的范围小) |
double | 8字节 | 15位(比int能表示的范围大,比long小) |
默认浮点类型采用的是double,在浮点数后加f表示float类型。
注意,当需要得到精确的计算结果时,不要使用浮点型。主要是因为浮点数采用二进制系统表示,而二进制中无法精确地表示分数1/10。应使用BigDecimal或者转换为整型进行计算。
1.4 字符型
char类型用来表示单个字符,事实上,有些字符需要2个char才能表示。char表示的是一个码元。
与字符串不同,char类型的字面量是用单引号括起来的。
'A' //这是一个char
"A" // 这是一个String对象
1.5 布尔类型
Java用boolean来定义布尔类型,只有两个取值:true和false.
1.6 基本数据类型之间的转换
当使用两个不同类型的数值进行计算时,先要将两个操作数转换成同一类型,规则如下:
- 如果两个操作数中有一个是double类型,另一个操作数就会转换为double类型
- 否则,如果其中一个是float类型,另一个操作数将会转换成float类型
- 否则,如果其中一个操作数是long类型,另一个操作数就会转换为long类型
- 否则,两个操作数都将是int类型(char, byte, short与int运算,都要先转换为int)。
可以简单地理解为,转换的优先级是: double > float > long > int�
而这种转换实际上会导致数据失真。
比如一个较大的int数值与float数值进行运算时,按规则会将int转换为float,但我们知道float能表示的有效整数位仅为6到7位,如果int数值大于7位,就会失真。
如下图所示:实线表示数值转换不会失真的情况,虚线表示数值转换可能会失真的情况。
当然,也可以直接进行强制类型转换,比如
double a = 100.50
int b = (int)a; // b == 100
强制类型转换,可能会导致数据失真。
2. 包装类型
2.1 为什么需要包装类
大多数情况下,我们使用Java基本数值类型进行数值运算。那为什么还需要包装类型呢?
一般来说,以下三种情况,必须使用包装类型
- 作为泛型的参数类型时,Java规定泛型的参数类型必须是引用类型
Collection<int> numbers;//不合法,编译失败
Collection<Integer> numbers; //合法
- 触发反射方法时。被触发的反射方法中的参数必须定义为包装类型,因为Java反射的时候会把基础数据类型获取的数据类型都变成包装类,当你需要调用的那个方法却不是包装类而是基础数据类型,就会报找不到方法的异常,NoSuchMethodException。
- 想要使用一些包装类的特性时,比如得到类中的常量值,如Integer.MAX_VALUE,或者比如调用包装类的一些方法进行便利的计算时,比如调用Integer的valueOf方法将一个字符串转换为一个整型数值。
2.2 自动拆装箱
自动装箱是Java编译器自发地将原生类型转换为包装类型。比如,将一个int类型转换为Integer。 自动拆箱则是反向。那么什么情况下会触发这种自动拆装箱呢?
2.2.1 触发自动拆装箱的情况
- 以下情况,会触发自动装箱
(1)将一个基本数值类型传递给一个接收参数为相应包装类型的方法时。
比如下面的consume方法,它接收的是Integer类型的参数
public void consume(Integer value){}
当将一个int值传给这个方法,编译器并不会报错,因为编译器自动做了装箱操作。
int param = 100;
consume(param);
编译器会将上述代码编译成类似以下形式:
int param = 100;
consume(Integer.valueOf(param));//自动装箱
(2)将一个基本数值类型直接赋值给相应包装类型时。
Integer number = 100;
- 以下情况,会触发自动拆箱
(1)将一个包装类型传递给一个接收参数为相应基本数值类型的方法时
public void sum(int value){} //方法定义为接收int类型
sum(new Integer(100);//调用时传入的是对应的Integer类型
(2)对包装类型执行算术运算时
Integer number = new Integer(100);
number++;
(3)直接将包装类型赋值给一个基本数值类型时
Integer number = new Integer(100);
int num = number;
之所以不会报错,是因为编译器自动做了相应的拆装箱操作。而装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。(xxx代表对应的基本数据类型)。
2.2.2 整型包装类的缓存机制
先来看下面的程序片断
public static void main(String[] args) {
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
Integer c = 300;
Integer d = 300;
System.out.println(c == d);
}
输出结果是:
true
false
为什么是这个结果呢?
我们知道,将int数值直接赋值给Integer类型,会触发自动装箱,也就是实际运行时,Integer a = 100会转换为Integer a = Integer.valueOf(100);来执行。那么,让我们直接来查看一下Integer类的valueOf方法的实现源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
可以看到,当包装的int的值在IntegerCache的low和high区间内时,会直接从IntegerCache这个缓存中读取一个Integer对象,而不是直接创建一个新的Integer对象。只有不在这个缓存区间内,才会直接new一个Integer对象。这个缓冲区间是多少呢?
private static class IntegerCache {
static final int low = -128;
//...省略部分代码
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
可以看出,这个缓存区间是 -128 到127。所以上例中c和d的值为300,超出了这个区间,自动装箱时是直接new出来的对象,==运算符比较两个不同的对象,自然返回false。
2.2.3 浮点型包装类没有缓存机制
除了Integer类,Boolean、Byte、Short、Long和Character也都有各自的缓存区间,实现缓存机制。
注意,Double和Float类没有缓存机制,我们可以看一下Double类和Float类的valueOf方法源码
//double原生类型自动装箱成Double
public static Double valueOf(double d) {
return new Double(d);
}
//float原生类型自动装箱成Float
public static Float valueOf(float f) {
return new Float(f);
}
之所以使用缓存机制是因为缓存的对象都是经常使用到的(如字符、-128至127之间的数字),防止每次自动装箱都创建一次对象的实例。
而double、float是浮点型的,没有特别的热的数据(比如在某个范围内的整型数值的个数是有限的,而浮点数却是不是有限的),缓存效果没有其它几种类型使用效率高。
3. 原生数据类型与包装类型的区别及注意点
3.1 原生数据类型与包装类型的区别
原生数据类型与包装类型主要有三个区别。
(1)原生数据类型是值类型,只是用来表示值的,它是存在方法区中的,相同值的原生数据类型共享同一个内存空间。而包装类型是引用类型,一个包装类型除了表示值,还可以表示一个内存空间。换句话说,两个值相同的包装类型对象,也许是存在不同的内存空间的。
(2)包装类型可能存在null的情况。一个包装类型如果只定义,未初始化或赋值,则默认就是null的。
(3)原生数据类型比包装类型在时间和空间上拥有更高的性能。
3.2 尽量使用原生数据类型,避免使用包装类型
来看以下程序片断:
public static void main(String[] args) {
Long sum = 0L;
for(long i = 0;i <Integer.MAX_VALUE;i++) {
sum += i ;
}
System.out.println(sum);
}
这个程序执行起来会非常慢,只因为它写错了一个小小的地方:Long sum = 0L;
为什么呢?sum定义为一个Long类型的包装对象,那么在接下来的for循环中,当执行sum +=i;因为i是long类型的,所以会先将sum进行自动拆箱,以便于与i进行算术运算,然后将结果赋予sum时,又得进行自动装箱,当超出Long类型的缓存区间时,就会不断地在堆内创建新的Long对象。所以这个程序非常慢,仅仅只是因为将sum定义为了Long,如果将sum定义为long时,问题就解决了。
所以我们应该尽量使用原生数据类型,在万不得已的情况下,不要使用包装类型。
3.3 包装类对象未初始化导致NullPointerException问题
我们来看下面的例子:
public class WrapperMess{
static Integer i ;
public static void main(String[] args){
if(i==0){
System.out.println(" i is 0");
}
}
}
程序会不会输出"i is 0"呢?答案是不会,而且还会抛出NullPointerException。
当程序执行到 if(i==0)时,因为要将Integer对象i与int值0进行比较,会进行自动拆箱。而Integer i还未初始化,它现在的值是null,当对一个null对象进行拆箱操作,即调用Integer的intValue方法时,就会抛出NullPointerException了。
3.4 使用==与equals方法的注意点
当我们想要判断两个包装类型对象的值是否相等时,不要使用==,而应该使用equals。
因为==判断的是对象的地址,我们知道两个包装类型对象即使值相等,也可能是存在堆中不同的空间的。
而包装类都重写了Object类的equals方法,直接比较所包装的值。比如Integer的equals方法源码:
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
参考资料: