基本类型
基本类型
既然Java一切皆对象,那又为何要保留int等基本类型?
上一节课,我们讲到,Java中的类型可以分为两类:基本类型和引用类型,并且,重点讲解了引用类型。本节,我们重点讲一下基本类型。作为面向对象编程语言,在Java语言中,有一个比较流行的说法,那就是“一切皆对象”,这也是Java语言的设计理念之一。但基本类型的存在似乎与此相矛盾,因此也有人说,Java是非纯的面向对象编程语言。既然已经有了Integer、Long等类,为什么Java语言又保留了int、long等基本类型呢?本节,我们就详细讲解一下Java中的基本类型以及对应的包装类。
一、八种基本类型
Java中的基本类型有8种,并且可以分为三类,具体如下所示。
- 整型类型:byte(字节型)、short(短整型)、int(整型)、long(长整型)
- 浮点类型:float(单精度)、double(双精度)
- 字符类型:char
- 布尔类型:boolean
Java不支持无符号类型。除此之外,不管32位JVM,还是64位JVM,这8种基本类型的长度(占用的字节个数)都是固定的,如下所示。对比C/C++,在32位编译器中,long类型的长度是4字节,而在64位编译器中,long类型的长度是8字节。
| 基本类型 | 字节大小 | 数值范围 | 默认值 |
|---|---|---|---|
| byte | 1 | -128~127 | 0 |
| short | 2 | -32768~32767 | 0 |
| int | 4 | -231 - 231-1 | 0 |
| long | 8 | -263~263-1 | 0L |
| float | 4 | -3.4e38~3.4e38 | 0.0f |
| double | 8 | -1.7e308~1.7e308 | 0.0 |
| char | 2 | '\u0000'~'\uFFFF' | '\u0000' |
| boolean | 1 | true or false | false |
关于上图给出的每种类型的长度和数据范围,我们有几点需要解释一下。
1)对整型类型的说明
在byte、short、int、long这四个整型类型的数据范围中,负数比正数多一个。这是为什么呢?负数在计算机中是如何用二进制来表示的呢?关于这一点,在位运算中提到。
2)对浮点类型的说明
同样是4字节长度,为什么float表示的数据范围比int大?同理,为什么double表示的数据范围比long的大?计算机是如何用二进制表示浮点数的?关于这一点,我们在浮点数中提到。
3)对boolean类型的说明
boolean类型只有true和false两个值,理论上只需要1个二进制位就可以表示。我们知道,数据存储的位置是通过内存地址来标识的,内存地址一般以字节为单位,一个字节一个地址。单个二进制位很难存储和访问,考虑到字节对齐(在类和对象中会详细说明),在JVM具体实现boolean类型时,大都采用1个字节来存储,用0表示false,用1表示true。尽管在存储空间上有些浪费,但操作起来更加简单。当然,如果在项目中需要大量使用boolean类型的数据,那么我们也有更加节省内存的存储方式,关于这一点,我们在[容器](这一部分讲解。
4)对char类型的说明
char类型的长度是2字节,而在C/C++中char类型的数据长度是1个字节,为什么会有这种差别呢?关于这一点,将在字符中讲解。
在上图中,每种类型的数据在没有赋值和初始化之前,都会有默认值(所有二进制位都是0)。注意,long、float类型的默认值的后面都带着标志符。这是因为在Java中,整数(整型字面常量)默认是int类型的,如果要表示long类型的字面常量(Literals),需要在整数的后面添加l或L标志符。浮点数(浮点类型字面常量)默认是double类型的,如要要表示float类型的字面常量,需要在浮点数后面添加f或F标志符。
假设我们需要计算100年有多少秒,如下计算方法将会出错。
long s = 100 * 12 * 30 * 86400;
System.out.println(s); //打印-1184567296
尽管等号后面的式子的结果并没有超过long型数据的最大值,但已经超过int型数据的最大值,在没有明确指明类型的情况下,整数默认是int类型的,所以,等号右边式子的计算结果放入int类型中时会溢出,溢出之后的值再赋值给变量s,所以,s中存储的就不是正确的值了。正确的写法应该是下面这样子。
long s = 100L * 12 * 30 * 86400;
System.out.println(s); //打印3110400000
二、基本类型转换
基本类型转换有两种:自动类型转换和强制类型转换 。自动类型转化也叫做隐式类型转换 ,强制类型转换也叫做显式类型转化 。一般来讲,从数据范围小的类型向数据范围大的类型转换,可以触发自动类型转换;从数据范围大的类型向数据范围小的类型转换,需要强制类型转换。 不过,boolean类型数据不支持与任何类型的自动或强制类型转换。
1) 自动类型转换
自动类型转换规则如下所示。
- byte自动转换为:short, int, long, float, double;
- short自动转换为:int, long, float, double;
- char自动转换为:int, long, float, double;
- int自动转换为:long, float, double;
- long自动转换为:float、double;
- float自动转换为:double;
注意,这里是将数据范围作为是否自动转换的参考,而非类型长度(所占字节个数)。这也是为什么short和char都占用2个字节,却不能互相转换,而long占用8个字节,却能转换为长度为4字节的float的原因。
char类型数据转换为int、long、float、double类型,得到的是char类型数据的UTF-16编码(在第7节中讲解)。而char类型数据转换为short类型时,因为char类型数据的范围是0~65535,而short类型数据的范围是-32768~32767。char类型中的UTF-16编码大于32767的数据,将会转换为short类型中的负数,转换跨度有点大,所以,Java不允许char类型数据自动转换为short类型。同理,也不允许short类型数据转换为char类型。
尽管long类型是8字节的,float类型是4字节的,但float类型可以表示的数据范围却比long类型大,这是因为浮点数的表示方法比较特殊,采用的是科学计数法 ,关于这一点,我们今天暂时不讲,会在浮点数中详细讲解。将long类型的数据转换为float类型,会损失一些精度,相当于做了类似四舍五入的精度舍弃,对于本身就无法表示精确值的float类型来说,这种转换是符合常理的。所以,Java允许long类型数据自动转换为float类型。
long l = 500000000000000l;
float f = l;
System.out.println(f); //输出4.99999993E14
2) 强制类型转换
除了以上罗列的允许自动类型转换的规则之外,其他类型之间的转换都需要显示地指明,也就是需要强制类型转换。强制类型转换有可能会导致数据的截断(将高位字节舍弃)或精度的丢失,需要程序员自己保证转换的正确性,符合自己的应用场景。
在真实的项目开发中,精度丢失是可以接受的,如下面代码中从float转为int,可以实现取整操作。但大部分截断是不被允许的,如下面代码中从很大的long值转换为int,因为这种截断得到的值没有意义。 只有程序员保证数据落在另一个类型可表示的范围内时,这种转换才是有意义的,如下面代码中从比较小的long值转换为int。
public class Demo4_1 {
public static void main(String[] args) {
long l1 = 500000000000000l;
func((int)l1); //输出1382236160
long l2 = 3245;
func((int)l2); //输出3245
float f = 19234.54343f;
func((int)f); //输出19234
}
public static void func(int i) {
System.out.println(i);
}
}
我们刚刚讲了基本类型之间互相转换,实际上,引用类型也可以互相转换。不过,这种转换仅限于有继承关系的类之间。 转换有两种类型:向上转换(Upcasting)和向下转换(Downcasting)。向上转换的意思是将对象的类型转换为父类或接口类型。向上转换总是被允许的,所以是自动类型转换 。向下转换的意思是将对象的类型转换为子类类型。向下转换需要显式指明,所以是强制类型转换 。
需要特别注意的是,对于向下转换,因为转换为子类之后,有可能会调用子类存在而父类不存在的属性和方法,所以,程序员需要保证转换的对象本身就是子类类型的,只不过暂时转换为了父类类型了,现在只是再转换回去而已。这点不好理解,我们举个例子解释一下。
public class ParentC {
public int a;
}
public class ChildC extends ParentC {
public int b;
}
public class OtherC {
public int c;
}
public class Demo4_1 {
public static void main(String[] args) {
ChildC child = new ChildC();
f(child);
}
//传递给obj的对象,本身就是ChildC类型的
public static void f(ParentC obj) {
OtherC oc = (OtherC)obj; //报错
ChildC cc = (ChildC)obj; //OK
System.out.println(cc.b);
}
}
三、自动装箱拆箱
Java一切皆对象,所以,对于基本类型,Java还提供了对应的包装类(Wrapper Class)。如下所示。其中,Number是整型和浮点型包装类的父类。
| 基本类型 | 对应包装类 |
|---|---|
| byte | Byte |
| short | Short |
| int | Integer |
| long | Long |
| float | Float |
| double | Double |
| char | Character |
| boolean | Boolean |
基本类型和包装类之间可以转换,这种转换可以显式执行,也可以隐式执行。Java提供了一些方法来实现这种显示的转换。以上八种基本类型和包装类在使用上类似,所以,我们只拿Integer类举例讲解,示例代码如下所示。
// 基本类型转换为包装类
int i = 5;
Integer iobj1 = new Integer(i);
Integer iobj2 = Integer.valueOf(i);
// 包装类转换为基本类型
i = iobj1.intValue();
除了以上显式地调用方法来转换之外,Java还支持基本类型和包装类之间的隐式转换,专业的叫法为自动装箱(autoboxing)和自动拆箱(unboxing)。 自动装箱是指自动将基本类型转换为包装类。自动拆箱是指自动将包装类转换为基本类型。示例代码如下所示。
Integer iobj = 12; //自动装箱
int i = iobj; //自动拆箱
字面常量12是int基本类型的,当赋值给包装类Integer对象时,触发自动装箱操作,创建一个Integer类型的对象,并赋值给变量iobj。实际上,自动装箱只是一个语法糖,其底层相当于执行了Integer类的valueOf()方法,如下所示。
Integer iobj = 12; 底层实现为:Integer iobj = Integer.valueOf(12);
反过来,当把包装类对象iobj赋值给基本类型变量i时,触发自动拆箱操作,将iobj中的数据取出,再赋值给变量i。其底层相当于执行了Integer类的intValue()方法,如下所示。
int i = iobj; 底层实现为:int i = iobj.intValue();
了解了自动装箱和自动拆箱原理之后,我们来看下,什么时候会触发自动装箱和自动拆箱。我总结了以下几种情况。
- 将基本类型数据赋值给包装类变量(包括参数传递)时,触发自动装箱。
- 将包装类对象赋值给基本类型变量(包括参数传递)时,触发自动拆箱。
- 当包装类对象参与算术运算时,触发自动拆箱操作。
- 当包装类对象参与关系运算(<、>)时,触发自动拆箱操作。
- 当包装类对象参与关系运算(==),并且另一方是基本类型数据时,触发拆箱操作。
对于以上几种触发自动装箱和自动拆箱的情况,我们举例说明一下,如下所示。
//第一种情况:赋值
int i1 = 4;
Integer iobj1 = 5; //自动装箱
iobj1 = i1; //自动装箱
List<Integer> list = new ArrayList<>();
list.add(i1); //自动装箱
//第二种情况:赋值
Integer iobj2 = new Integer(6);
int i2 = iobj2; //自动拆箱
//第三种情况:算术运算
Integer iobj3 = iobj1 + iobj2; //自动拆箱
System.out.println(iobj3); //输出10
//第四种情况:大于小于关系运算
boolean bl = (iobj1 < iobj2); //自动拆箱
System.out.println(bl); //输出true
bl = (iobj1 < 2); //自动拆箱
System.out.println(bl); //输出false
//第五种情况:==关系运算
Integer iobj4 = new Integer(1345);
bl = (iobj4 == 1345); //自动拆箱
System.out.println(bl); //输出true
自动装箱和自动拆箱给我们的开发带来了便利,但同时,不恰当的使用它们,也会导致性能问题。如下代码所示。
public class Demo4_3 {
public static void main(String[] args) {
Integer count = 0;
for (int i = 0; i < 10000; ++i) {
count += 1;
}
}
}
在上述代码中,count += 1等价于count = count + 1。因为Integer等包装类是不可变类(至于为什么设计成不可变类,在字符串中讲解),执行这条语句会先触发自动拆箱,执行加法操作,然后再触发自动装箱,生成新的Integer类对象,最后赋值给count变量。也就是说,执行上述代码,要执行10000次自动装箱和拆箱,并且生成10000个Integer对象,相对于如下代码实现方式,上述实现方式内存消耗大,执行速度慢。
public class Demo4_3 {
public static void main(String[] args) {
int count = 0;
for (int i = 0; i < 10000; ++i) {
count += 1;
}
}
}
四、常量池技术
我们先来看下面这段代码,你觉得它的打印结果是什么。这也是一道常考的面试题。
Integer a = 12;
Integer b = 12;
Integer c = new Integer(12);
System.out.println("a==12: " + (a==12)); //自动拆箱,输出true
System.out.println("a==b: " + (a==b)); //常量池技术,输出true
System.out.println("a==c: " + (a==c)); //引用不同对象,输出false
对于第一条打印语句,a==12触发自动拆箱,所以打印true。对于第三条打印语句,a和c引用不同的对象,所以打印false。对于第二条打印语句,a和b引用不同的对象,理应打印false,但运行程序,打印结果却是true。这是为什么呢?
这是因为Integer等包装类使用了常量池技术。IntegerCache类中会缓存值为-128到127之间的Integer对象。当我们通过自动装箱,也就是调用valueOf()来创建Integer对象时,如果要创建的Integer对象的值在-128和127之间,会从IntegerCache中直接返回,否则才会真正调用new方法创建。 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);
}
IntegerCache类的代码如下所示。它是享元模式的典型引用。
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
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) {
try {
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);
} catch( NumberFormatException nfe) {
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
为什么IntegerCache只缓存-128到127之间的整型值呢?
在IntegerCache的代码实现中,当这个类被加载的时候,缓存的Integer对象会被集中一次性创建好。毕竟整型值太多了,我们不可能在IntegerCache类中预先创建好所有的整型值对象,这样既占用太多内存,也使得加载IntegerCache类的时间过长,更没有必要。所以,JVM只选择缓存对大部分应用来说常用的整型值,也就是一个字节范围内的整型值(-128~127)。
实际上,JVM也提供了方法,让我们可以自定义缓存的最大值,如下所示。如果你通过分析应用程序的JVM内存占用情况,发现-128到255之间的数据占用的内存比较多,你就可以用如下方式,将缓存的最大值从127调整到255。不过,JDK并没有提供设置最小值的方法。
//方法一:
-Djava.lang.Integer.IntegerCache.high=255
//方法二:
-XX:AutoBoxCacheMax=255
综上所述,在平时的开发中,对于下面这样三种创建整型对象的方式,我们优先使用后两种。这是因为,第一种创建方式并不会使用到IntegerCache,而后面两种创建方法可以利用IntegerCache缓存,返回共享的对象,以达到节省内存的目的。 举一个极端一点的例子,假设应用程序需要创建1万个-128到127之间的Integer对象。使用第一种创建方式,我们需要分配1万个Integer对象的内存空间;使用后两种创建方式,我们最多只需要分配256个Integer对象的内存空间。
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);
除了Integer类型之外,其他部分包装类也使用了常量池技术。其中,Long、Short、Byte利用常量池技术来缓存值在-128到127之间对象。Character利用常量池技术缓存值在0到127之间的对象(因为Character的值没有负数)。Float、Double表示浮点数,无法利用常量池技术。Boolean只有两个值,不需要使用常量池技术。
五、基本类型VS包装类
了解了基本类型和包装类,我们来对比一下两者的优缺点。
包装类是引用类型,对象的引用和对象本身是分开存储的,而对于基本类型数据,变量对应的内存块直接存储数据本身。所以,基本类型数据在读写效率方面,都要比包装类高效。除此之外,在64位JVM上在开启引用压缩的情况下,一个Integer对象占用16个字节的内存空间(关于这一点,在类和对象中讲解),而一个int类型数据只占用4字节的内存空间,前者对空间的占用是后者的4倍。
也就是说,不管是读写效率,还是存储效率,基本类型都比包装类高效。这就是Java保留基本类型的原因。尽管Java最初的设计理念是一切皆对象,这样可以统一对变量的处理逻辑,但为了性能做了妥协,毕竟基本类型数据在开发中使用太频繁了。
不过,Java真的想要做到一切皆对象,也是有可能的。Java可以只提供包装类,不提供基本类型给开发者使用。编译器在底层将包装类转换为基本类型处理。这样相当于提供了基本类型的语法糖给开发者使用。实际上,像Groovy, Scala等语言也正是这么做的。而Java之所以没有这么做,我猜测,很可能是历史的原因,毕竟Java发明于上个世纪90年代,当时没有考虑那么全面,而之后大家已经习惯了使用基本类型,如果再将其废弃,那么影响过于大。
不过,包装类也有优势,它提供了更加丰富的方法,可以更加方便地实现复杂功能。
基本类型和包装类各自有各自的优点,那么,在平时的开发中,什么时候使用基本类型?什么时候使用包装类呢?
在项目开发中,首选基本类型,毕竟基本类型在性能方面更好。当然,也有一些特殊情况,比较适合使用包装类。比如映射数据库的Entity、映射接口请求的DTO,在数据库或请求中的字段值为null时,我们需要将其映射为Entity或DTO中的null值。还有,我们在初始化变量时,需要将其设置为没有业务意义的值,如果某个变量的默认值0是有业务意义的值,这个时候,我们需要找一个其他值(例如-1)来初始化变量。这种情况下,我们就推荐使用包装类,因为包装类变量的默认值是null,是没有业务意义的。
六、思考题
以下代码打印结果是什么?
public class Demo4_4 {
public static void main(String[] args) {
int i = 100;
int j = 100;
compare(i, j);
}
public static void compare(Integer obj1, Integer obj2) {
Integer obj3 = obj1+1;
Integer obj4 = obj2+1;
System.out.println("" + (obj3==obj4));
}
}