前情提要
上一篇中,通过一道常见的面试题(即:String、StringBuilder、StringBuffer的区别),引申到Java中基本类型和包装类的相关内容。在这一篇中,我们将解决上一篇中引申出来的问题——基本类型和包装类型到底有什么区别?
首先,要弄明白这两者的区别,我们就必须要知道基本数据类型和包装类到底是啥?各自都有些什么特性?
请注意,除非特别注明,否则本文篇中所涉及得到内容都是基于jdk1.8来说的。
先来了解一下基本数据类型吧。
关于基本数据类型
基本数据类型到底是个啥?
关于基本数据类型的定义,我翻了一些资料,至今没有在任何地方找到一个准确的描述。
这里我就谈一下我个人的见解吧,如果有朋友觉得下面的内容有不妥的地方欢迎在评论区留下您的高见,大家共同进步!
在一个变量定义后,该变量指向的只能是具体的数值而非内存地址。这样的变量就属于基本数据类型。
这一块的内容后续会再来补充,由于不是主线,暂时搁置影响也不大。
Java中基本数据类型有哪些
在Java中,基本数据类型有8种,分别为:
布尔类型:boolean
字符类型:char
整数类型:byte、short、int、long
浮点类型:float、double
各类型的详细信息如下表:
类型描述
名称
位数
字节数
默认值
布尔类型
boolean
-
-
false
字符类型
char
16
2
'u0000'
整数类型
byte
8
1
0
整数类型
short
16
2
0
整数类型
int
32
4
0
整数类型
long
64
8
0L
浮点类型
float
32
4
0f
浮点类型
double
64
8
0d
对于boolean而言,依赖于jvm厂商的具体实现。逻辑上理解是占用1位,但是实际中会考虑的因素较多。在此也不展开描述,如果有人问你究竟boolean占多少内存空间,你只需要回答:理论上1位(注意不是一个字节)就可满足需求,实际还要看jvm的实现。
基本数据类型为什么不用new运算符?
我们都知道new运算符可用于实例化一个对象,也就是给对象实例分配一块足够大的内存空间并返回指向该内存的引用。注意,这里所指的对象实际上是引用类型。引用对象开辟的内存空间一般是在堆中。
相对于引用类型来说,值类型一般存放在栈上(作为成员变量的时候才会放在堆中)。因为虚拟机(根据虚拟机不同,boolean可能占用空间大小不同)对每一种基本类型的空间占用大小都是明确知晓的,所以不再需要new去开辟空间。
实际中,Java的数据类型分为两种:值类型和引用类型,我们习惯于把所有引用类型都统称为对象。所以,基本数据类型不在我们理解的对象的定义范围内。
关于包装类
包装类的定义
其实包装类的意义从名字就能看出一些端倪。啥叫包装,通俗了说就是把一个物体打包然后装起来。举个例子来说,比如今天我在网上买了一颗苹果,等我收到货的时候还是只有那一颗苹果吗?并不是,我拿到的是一个贴了有运单号的箱子,里面用了塑胶袋把苹果给包起来了。同理,包装类也是一样,每一个包装类里面都包了一种基本数据类型。作用也和这个例子类似,是为了让运输更方便,让苹果更安全,让我们操作更简单。
包装类的种类
八种基本类型都有自己对应的包装类,分别为:
布尔类型:Boolean
字符类型:Character
整数类型:Byte、Short、Integer、Long
浮点类型:Float、Double
装箱和拆箱、包装类型的缓存机制
下面我们以Integer为例,了解一下什么是装箱和拆箱,还有所谓的包装类的缓存机制到底是什么?首先,关于装箱和拆箱的概念如下:
装箱——将基本类型用各自对应的包装(引用)类型包装起来:即基本类型->包装类型;
拆箱——将包装类型转换为基本数据类型:即包装类型->基本类型;
例如下面的代码将会发生装箱和拆箱的过程。
public static void main(String[] args){
// 装箱
Integer packageObject = 100;
// 拆箱
int baseObject = packageObject;
}
编译上面的代码(javac命令),查看对应的.class文件的内容(javap命令)。
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: bipush 100
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: aload_1
7: invokevirtual #3 // Method java/lang/Integer.intValue:()I
10: istore_2
11: return
LineNumberTable:
line 9: 0
line 11: 6
line 12: 11
可以看到装箱的时候用的是Integer的valueOf方法;而拆箱的时候用的是intValue方法。在Integer中找到这两个方法。
valueOf方法的API说明如下:
/**
* Returns an {@code Integer} instance representing the specified
* {@code int} value. If a new {@code Integer} instance is not
* required, this method should generally be used in preference to
* the constructor {@link #Integer(int)}, as this method is likely
* to yield significantly better space and time performance by
* caching frequently requested values.
*
* This method will always cache values in the range -128 to 127,
* inclusive, and may cache other values outside of this range.
*
* @param i an {@code int} value.
* @return an {@code Integer} instance representing {@code i}.
* @since 1.5
*/
因为篇幅原因,就不一步一步的点进去看了,有兴趣的朋友自行去翻源码吧。上面的内容已经足够说明问题了,上面的描述大概说了这些东西:
该返回的是一个基于int(这个是入参,就是代码中定义的100)值的Integer的实例对象;
首先会判断入参的范围在不在[-128,127]之间。
如果不在这之间,则会调用构造方法返回一个新的Integer实例;
如果在这个范围内,则会从缓存中取一个Integer对象返回;
Integer所谓的缓存其实是在Integer类的内部定义了一个IntegerCache的class,IntegerCache里面持有一个静态并且final修饰的缓存数组,在一开始这个数组里面就已经存入了[-128,127]之间的整型值,当你用自动装箱的方式初始化一个Integer对象并且你的整型值在这个范围内的话,会自动从这个数组中找到对应的Integer对象返回给你,而不是重新创建一个Integer对象。
下面再来看一下拆箱中遇到的intValue方法。
intValue方法的API说明如下:
/**
* Returns the value of this {@code Integer} as an
* {@code int}.
*/
intValue方法的描述很简单,直接返回一个int类型的值。这个int值其实就是在Integer内部包装的基本数据类型(int)。
到此,(Integer类的)装箱、拆箱以及缓存机制差不多咱们就已经揭开那层面纱了。事实上,八大包装类型中除了浮点类型的包装类(Double和Float)并没有实现缓存技术外,其他的包装类都实现了。
Byte,Short,Integer,Long这四个包装类都提供了数值[-128,127]之间的相应类型的缓存;
Character提供了数值在[0,127]之间的缓存;
Boolean提供了取值在{True,False}之间的缓存;
为什么浮点型不提供?因为浮点型的取值范围太广,不可能实现缓存。
基本数据类型和包装数据类型常见的面试题
(一)为什么List = new List();类似这样的代码会报错?
答:因为基本数据类型不支持泛型。
List支持泛型,但是泛型必须是对象。也就是说List支持所有继承自Object类的类型参数,但基本数据类型并没有继承自Object,所以基本数据类型并不是对象。
(二)包装类缓存的常见题
// 题目
Integer i1 = 55;
Integer i2 = 55;
Integer i3 = new Integer(55);
Integer i4 = new Integer(55);
Integer i5 = new Integer(56);
Integer i6 = 1;
System.out.println("i1 = i2 ? " + (i1 == i2));
System.out.println("i3 = i4 ? " + (i3 == i4));
System.out.println("i5 = i4 + i6 ? " + (i5 == i4 + i6));
System.out.println("56 = i4 + i6 ? " + (56 == i4 + i6));
Double d1 = 1.0d;
Double d2 = 1.0d;
System.out.println("d1 = d2 ? " + (d1 == d2));
// 答案
i1 = i2 ? true
i3 = i4 ? false
i5 = i4 + i6 ? true
56 = i4 + i6 ? true
d1 = d2 ? false
// 解析
// 从缓存数组中取一个值为55的Integer实例
Integer i1 = 55;
// 从缓存数组中取一个值为55的Integer实例
Integer i2 = 55;
// 创建一个值为55的Integer实例
Integer i3 = new Integer(55);
// 创建一个值为55的Integer实例
Integer i4 = new Integer(55);
// 创建一个值为56的Integer实例
Integer i5 = new Integer(56);
// 从缓存数组中取一个值为1的Integer实例
Integer i6 = 1;
// 只要值相等,从缓存数组中取出来的一定是同一个实例
System.out.println("i1 = i2 ? " + (i1 == i2));
// 虽然值相同,但是属于两个不同的实例(因为遇到了两个new)
System.out.println("i3 = i4 ? " + (i3 == i4));
// Integer类不提供+的实现,所以i4和i6先拆箱为基本数据类型,因为i5要和基本类型比较,i5也只能拆箱
System.out.println("i5 = i4 + i6 ? " + (i5 == i4 + i6));
// 同上一个
System.out.println("56 = i4 + i6 ? " + (56 == i4 + i6));
Double d1 = 1.0d;
Double d2 = 1.0d;
// Double不提供缓存机制,每次都是new的新对象
System.out.println("d1 = d2 ? " + (d1 == d2));
(三)Integer类的缓存区间为什么是[-128,127],为什么不把范围定义的再广一些?
事实上,针对于Integer类,我们可以通过改参数的方式来设置这个区间的上下限。那是不是意味着我的区间越大越好呢?
并不是,看源码的时候就会知道,Integer类的缓存区间的每一个整型值都会被提前创建并加载到内存中去。换句话说,这个区间越大,你的内存就占用的越多。这是一个典型的时间和空间的抉择问题。缓存的意义就是拿空间换时间,但是你拿了太多的空间,可能换来的回报远远不是那点时间能补偿得了的。
(四)在Java中存在i+1
存在,所有的基本类型都有位数的限制,比如int是32位,那么int能表示的整型范围为[-2147483648,2147483647],即:[负2的31次幂,正2的31次幂-1]。当超过这个范围时,将发生溢出,溢出后该值将变为负数。
int x = 2147483647;
int y = x + 1;
System.out.println(y);// 输出:-2147483648
System.out.println("y < x ? " + (y < x));// 输出:y < x ? true
关于为什么溢出后会变为负数,大家可自己演练一下。简单提一下,32位的有符号数,第一位是符号位(为0表示正数,为1表示负数)。所以32位能表示的最大的正数是[0111 1111 1111 1111 1111 1111 1111 1111],写成16进制就是0x7FFFFFFF。再加1,低位向高位进位,相加的结果变成[1000 0000 0000 0000 0000 0000 0000 0000],表示成16进制则为0x80000000。这个结果一看就是负数,因为最高位的符号位已经变成1了。至于为什么是-2147483648,就需要去算一下这个二进制串的补码了。
扩展区域
扩展区域主体
这是一个没有实现的扩展。