您的位置:首页 > 编程语言 > Java开发

【Java基础】分析java数据类型与自动装箱/自动拆箱机制原理

2020-01-15 10:54 351 查看

文章目录

一. 八大基本数据类型

  • 基本类型,或者叫做

    内置类型
    ,是Java中不同于类(Class)的特殊类型。它们是使用最频繁的类型。

  • Java是一种强类型语言,第一次

    申明变量
    必须声明数据类型,第一次
    变量赋值
    称为变量的初始化。

Java基本类型共有八种,基本类型可以分为三类:

分类 类型
字符类型 char(8位)
整数类型 byte(8位)、short(16位)、int(32位)、long(64位)
布尔类型 boolean(1位)
浮点数类型 float(32位)、double(64位)
  • boolean 只有两个值:true、false,可以使用
    1 bit
    来存储,但是具体大小没有明确规定。
  • JVM 会在
    编译时期
    将 boolean 类型的数据转换为
    int
    ,使用
    1 来表示 true,0 表示 false
  • JVM 支持 boolean 数组,但是是通过读写 byte 数组来实现的。
  • 实际上,Java中还存在另外一种
    基本类型void
    ,它也有对应的
    包装类java.lang.Void
    ,不过我们
    无法直接操作

二. 使用基本数据类型有什么好处

  1. 在java中, new一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象,创建对象本身来说是比较消耗资源的。

  2. 对于经常用到的类型,如int,double等,如果每次使用的时候都需要new一个Java对象的话,就会比较笨重。所以,和C++一样,Java提供了基本数据类型,这种数据的变量不需要使用new创建,他们不会在堆上创建,而是直接在栈内存中存储,因此会更加高效。

三. 整数类型的取值范围

Java中的整型主要包含byte、short、int和long这四种,表示的数字范围也是从小到大的,之所以表示范围不同主要和他们存储数据时所占的字节数有关。

1字节=8位(bit) java中的整型属于有符号数(可以带正负符号)

8bit可以表示的数字:

(以byte类型为例)

最小值:10000000
(-128)(-2^7)

最大值:01111111
(127)(2^7-1)

整型的这几个类型中

类型 字节占用 范围 默认值
byte
1个字节
范围为 -128
(-2^7)
127
(2^7-1)
在变量初始化的时候,byte类型的
默认值为0
short
2个字节
范围为 -32,768
(-2^15)
32,767
(2^15-1)
在变量初始化的时候,short类型的
默认值为0
,一般情况下,因为Java本身转型的原因,可以直接写为0
int
4个字节
范围为 -2,147,483,648
(-2^31)
2,147,483,647
(2^31-1)
在变量初始化的时候,int类型的
默认值为0
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

超出取值范围怎么办
每个整型类型都有一定取值范围,当超出取值范围,即溢出。如以下代码:

@Test
public void testOverFlow() {
int i = Integer.MAX_VALUE;
int j = Integer.MAX_VALUE;

int k = i + j;
System.out.println("i (" + i + ") + j (" + j + ") = k (" + k + ")");
//i (2147483647) + j (2147483647) = k (-2)
}

结果为: i (2147483647) + j (2147483647) = k (-2) 这就是发生了溢出,

溢出的时候并不会抛异常
也没有任何提示。
在程序中,使用同种类型的数据进行运算的时候,一定要注意
数据溢出
的问题。

四. 八大包装类型

  • 包装类(Wrapper Class)
    ,均位于
    java.lang
    包, 它将基本类型进行一层封装,并类中定义了许多方法操作该数据。如:
    转换字符串, 转换成基本类型
基本类型 包装类型 所属分类
byte Byte 整数类型
short Short 整数类型
int Integer 整数类型
long Long 整数类型
double Double 浮点数类型
float Float 浮点数类型
boolean Boolean 布尔类型
char Character 字符类型

为什么需要包装类型

  1. 因为Java是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如集合类List,Map中,我们无法将
    int 、double
    等类型保存进去的。因为集合的容器要求元素是
    Object类型
  2. 为了让基本类型也具有对象的特征,就出现了包装类型,它相当于将基本类型“包装起来”,使得它具有了
    对象的性质,并且为其添加了属性和方法,丰富了基本类型的操作

五. 自动拆箱与自动装箱

那么,有了基本数据类型和包装类,肯定有些时候要在他们之间进行转换。比如把基本数据类型的int转换成一个包装类型的Integer对象。把包装类型Integer对象转换成基本类型int

因此

  • 基本类型
    转换成
    包装类型
    的过程就是
    装箱
    ,英文对应于boxing
  • 包装类型
    转换成
    基本类型
    的过程就是
    拆箱
    ,英文对应于unboxing

Java 1.5以前
我们需要
手动进行转换
才行

/Java 1.5以前
Integer iObject = Integer.valueOf(3);// 装箱
int iPrimitive = iObject.intValue()// 拆箱

Java 1.5以后
提供了自动装箱的功能,直接使用
Integer integer = 10;
语法就可以完成装箱,所有的转换都是由
编译器
来完成,编译器会判断是否进行自动装箱动作。
自动装箱只适用于八大基本类型

  • 自动装箱: 就是将基本类型
    自动
    转换成对应的包装类型。
  • 自动拆箱:就是将包装类型
    自动
    转换成对应的基本类型。
//Java 1.5之后
Integer iObject = 3; //自动装箱 - primitive to wrapper conversion
int iPrimitive = iObject; //拆箱 - object to primitive conversion

六. 自动装箱/自动拆箱原理

public static void main(String[] args) {
Integer integerNum = 1; //自动装箱
int intNum = integerNum; //自动拆箱
}

反编译上面代码

public static void main(String[] args) {
Integer integerNum = Integer.valueOf(1);
int intNum = integerNum.intValue();
}

从上面反编译后的代码可以看出,

int
自动装箱
都是通过
Integer.valueOf()
方法来实现的,
Integer
自动拆箱
都是通过
integer.intValue
来实现的。

可以试着将八种类型都反编译一遍 ,你会发现以下规律:

自动装箱
都是通过
包装类的valueOf()方法
来实现的,
自动拆箱
都是通过
包装类对象
xxxValue()
来实现的。

七. 分析自动拆装箱使用场景

  • 场景一:将基本数据类型放入集合类
List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i ++){
li.add(i);
}

反编译结果

List<Integer> li = new ArrayList<>();
for (int i = 1; i < 50; i += 2){
li.add(Integer.valueOf(i));
}

结论: Java中的集合类只能保存对象类型,基本数据类型放入集合类中的时候,会进行自动装箱成包装类对象

  • 场景二:包装类型和基本类型的大小比较
Integer a = 1;
System.out.println(a == 1 ? "等于" : "不等于");
Boolean bool = false;
System.out.println(bool == true ? "真" : "假");

反编译结果

Integer a = Integer.valueOf(1);
System.out.println(a.intValue() == 1 ? "等于" : "不等于");
Boolean bool = Boolean.valueOf(false);
System.out.println(bool.booleanValue() == true ? "真" : "假");

结论: 包装类与基本类型比较时,是先将包装类进行拆箱成基本数据类型,然后进行比较的。

  • 场景三:包装类型的运算
Integer i = 10;
Integer j = 20;
System.out.println(i + j);

反编译结果

Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(20);
System.out.println(i.intValue() + j.intValue());

结论: 两个包装类型之间的运算,会被自动拆箱成基本类型进行。

  • 场景四:三目运算符的使用
Integer i = 0;
int j = 1;
boolean flag = true;
int k = flag ? i : j;

反编译结果

Integer i = Integer.valueOf(0);
int j = 1;
boolean flag = true;
int k = flag ? i.intValue() : j;

结论: 这是三目运算符的语法规范:当

第二,第三位操作数
分别为
基本类型
包装类型
时,其中的
包装类型
就会自动拆箱为基本类型进行操作。
例子中,
flag ? i : j;
片段中,第二段的
i
是一个包装类型的对象,而第三段的
j
是一个基本类型,所以会对包装类进行自动拆箱。如果这个时候
i 的值为null,那么久会发生NPE
。(自动拆箱导致空指针异常

  • 场景五:方法形参与返回值
//自动拆箱
public int getInt(Integer num) {
return num;
}
//自动装箱
public Integer getInteger(int num) {
return num;
}

反编译结果

public int getInt(Integer num) {
return num.intValue();
}

public Integer getInteger(int num) {
return Integer.valueOf(num);
}

结论:

八. 自动拆装箱与缓存

Java的编译器把

基本类型
自动 转换成
包装类对象
的过程叫做
自动装箱
,相当于使用
valueOf()
方法:

代码

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才对,但实际上却是一个true,一个false

那为什么返回的结果不一致呢?

  • 原因和Integer中的
    缓存机制
    有关。在Java1.5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用
    相同的对象引用
    实现了
    缓存和重用
    。 integer整数值取值范围 在-128 至 +127`直接从缓存中获取,不创建新的对象
  • 只适用于自动装箱。使用构造函数
    new Integer()
    创建对象不适用。

下面是

JDK 1.8
Integer
valueOf()
方法的实现:

/**
* 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
*/
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}

在创建对象之前首先 数值

i
取值范围是否在
IntegerCache
IntegerCache.high
之间, 如果在就 从
IntegerCache.cache
中寻找。如果找到就使用
缓存
里的Integer对象, 如果没找到就
new 一个新的
Integer对象

IntegerCache 类
IntegerCache 是

Integer类
中的一个
私有的静态内部类
,使用了
享元模式

/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage.  The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/

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) {
// If the property cannot be parsed into an int, ignore it.
}
}
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() {}
}

其中的

源码注释
详细的说明了缓存支持
-128到127
之间的自动装箱过程。

  1. 最大值127
    可以通过
    -XX:AutoBoxCacheMax=size
    修改。这个功能在Java1.5中引入的时候,范围是固定的
    -128 至 +127
    。后来在Java1.6中,可以通过
    java.lang.Integer.IntegerCache.high
    设置最大值。

  2. 缓存通过一个

    for循环
    实现。从低到高并创建尽可能多的整数并存储在一个整数数组中。

  3. 这个缓存会在Integer类

    第一次使用
    的时候被初始化出来。以后,就可以使用缓存中包含的实例对象,而不是创建一个新的实例(在自动装箱的情况下)。

实例

Integer a = 12;
Integer b = 12;
System.out.println(a == b);// true
Integer c = 1234;
Integer d = 1234;
System.out.println(c == d);// false

Integer e = new Integer(13);
Integer f = new Integer(13);
System.out.println(e == f); //false

Integer g = new Integer(1200);
Integer h = new Integer(1200);
System.out.println(g == h); //false
  1. 通过Java源代码可以看到
    Integer.valueOf()
    中有个内部类
    IntegerCache
    (类似于一个常量数组,也叫对象池),它维护了一个Integer数组cache,长度为(128+127+1)=256 。
  2. Integer类中还有一个
    Static Block(静态块)
    。从这个静态块可以看出,Integer已经默认创建了
    数值【-128-127】的Integer缓存数据
    。所以使用Integer a=10时,JVM会直接在该在对象池找到该值的引用。也就是说这种方式声明一个Integer对象时,JVM首先会在
    Integer对象的缓存池
    中查找有没有10的对象,如果有
    直接返回该对象的引用
    ;如果没有,则使用
    new一个对象
    ,并返回该对
    象的引用地址
  3. 因为Java中
    “==”
    比较的是两个对象的引用(即内存地址),a、b为同一个对象,所以结果为true,因为
    300超过了Integer缓存数据范围
    ,使用new方式创建:
    c = new Integer(1234); d=new Integer(40);
    虽然他们值相等,但是属于不同的对象,不会被放到对象池中,所以他们不是同一个引用,返回false。

用一句简短通俗的话话概括享元模式:

  • 如果有很多个小的对象,它们有很多属性相同,那可以把它们变成一个对象,那些不同的属性把它们变成方法的参数,称之为外部状态,相同的属性称之为内部状态,这种机制即为享元模式。

到底是什么原因选择这个-128到127范围呢?

  • 因为这个范围的数字是最被广泛使用的。 在程序中,第一次使用Integer的时候也需要一定的额外时间来初始化这个缓存。

在Boxing Conversion部分的Java语言规范(JLS)规定如下:

  • 如果一个变量p的值是:
    -128至127之间的整数
    或者
    true 和 false的布尔值
    或者
    ‘\u0000’至 ‘\u007f’之间的字符
    范围内的时,将p包装成a和b两个对象时,可以直接使用
    a==b
    判断a和b的值是否相等。否则就要使用equals来判断

其他整型对象有没有缓存机制
这种缓存行为不仅适用于Integer对象。

所有的整数类型的类都有类似的缓存机制

包装类 对应的缓存内部类
Byte ByteCache用于缓存Byte对象
Short ShortCache用于缓存Short对象
Long LongCache用于缓存Long对象
Character CharacterCache用于缓存Character对象
  • Byte, Short, Long
    固定
    范围:
    -128 到 127
    。对于
    Character
    , 范围是
    0 到 127
    , 并且他们的范围都不能改变

九.使用包装类弊端

  1. 包装对象的数值比较,不能简单的使用==,虽然-128到127之间的数字可以,但是这个范围之外还是需要使用equals比较。

    (包装类重写了equals方法)

  2. 有些场景由于自动拆箱,如果包装类对象为null,那么自动拆箱时就有可能抛出NPE(空指针异常)

  3. 容易生成无用对象,因为自动装箱会

    隐式地创建对象
    ,在一个循环体中,会创建无用的中间对象,这样会增加
    GC压力,拉低程序的性能。
    所以在写循环时一定要注意代码,避免引入不必要的自动装箱操作.

  • 点赞 2
  • 收藏
  • 分享
  • 文章举报
oollXianluo 发布了61 篇原创文章 · 获赞 108 · 访问量 5143 私信 关注
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐