Java 数据类型解析

Catalogue
  1. 1 数据类型分类
    1. 1.1 值类型
    2. 1.2 值类型和引用类型的区别
  2. 2 数据转换
    1. 2.1 自动转换
    2. 2.2 强制类型转换
    3. 2.3 赋值及表达式中的类型转换
  3. 3 装箱和拆箱
    1. 3.1 包装类、装箱、拆箱
    2. 3.2 自动装箱、自动拆箱
    3. 3.3 装箱、拆箱的应用和注意点
  4. 参考资料

Java 数据类型的问题不是 Java 开发中的难点,而且问题一般都能在编译的时候就显现出来,可是知道它的原理对于写出高质量的代码的很有帮助的。本文将详细介绍 Java 数据类型相关的知识,必须要提的是,本文的大部分知识来源于网上的优秀文章(文末有给链接),笔者只是在理解它们的基础上重新排版并增加了一些补充的内容。

1 数据类型分类

Java 的数据类型有两类:
1、值类型(又叫内置数据类型,基本数据类型)
2、引用类型(除值类型以外,都是引用类型,包括 String、数组)

1.1 值类型

Java 语言提供了 8 种值类型,大致分为 4 类:

  • 整型:byte 、short 、int 、long
  • 浮点型:float 、 double
  • 字符型:char
  • 布尔型:boolean

1.2 值类型和引用类型的区别

从概念方面来说

  • 值类型:变量名指向具体的数值。
  • 引用类型:变量名指向存数据对象的内存地址。

从内存方面来说

  • 值类型:变量在声明之后,Java 就会立刻分配给他内存空间。
  • 引用类型:它以特殊的方式(类似 C 指针)指向对象实体(具体的值),这类变量声明时不会分配内存,只是存储了一个内存地址。

从使用方面来说

  • 基本类型:使用时需要赋具体值,判断时使用 == 号。
  • 引用类型:使用时可以赋 null,判断时使用 equals 方法。

扩展阅读
Java 基本数据类型和引用类型
这篇文章对于基本数据类型和引用类型的内存存储讲述比较生动。
Android 内存泄漏总结
这篇文章对于 Java 的内存存储机制讲得比较透彻。

2 数据转换

将一种类型的值赋值给另一种类型是很常见的。在 Java 中,boolean 类型与其他 7 种类型的数据都不能进行转换,这一点很明确。但对于其他 7 种数据类型,它们之间都可以进行转换,只是可能会存在精度损失或其他一些变化。Java 中,值类型转换有两种方式:
1、自动转换(隐式):无需任何操作。
2、强制转换(显式):需使用转换操作符(type)。
将6种数据类型按下面顺序排列一下:
double > float > long > int > short > byte
如果从小转换到大,那么可以自动转换,而从大到小,则必须使用强制转换。需要注意的是,对于 char 类型,除 char 转 double、float、long、int 可以自动转换外,对于 char 的剩余情况的转换必须要强制转换。

扩展阅读:
Java char数据类型
全面理解 char 基本类型数据的使用方法。
原码反码补码
特别地,对于[1000 0000]和[00000000]的原码、补码是其本身(以8位为例)。
byte类型的最小值为什么是-128而非-127
关于数据类型表示的数据范围问题
位操作
了解位运算的机制。需要注意,位运算都是针对补码的运算,运算结果也是补码。

2.1 自动转换

自动转换因为较大的类型(如int)要保存较小的类型(如byte),内存总是足够的,不需要强制转换。如果将字面值保存到byte、short、char、long的时候,也会自动进行类型转换,注意区别,此时从int(没有带 L 的整型字面值为 int)到byte/short/char也是自动完成的,虽然它们都比 int 小,但是有个限制是字面值必须要在目标类型的取值范围内,超过了目标的取值范围,则还是需要强转,例如:byte b=1 是没有问题的,而byte b=200 就必须写成 byte b=(byte)200 了,因为 byte 的取值范围是 [-128,127] 。

2.2 强制类型转换

如果要把大的转成小的,就必须强制转换。强制转换采用转换操作符()。严格地说,将byte转为char不属于强制转换,因为从byte到char的过程其实是byte–>int–>char,所以自动转换和强制转换都有。强制转换除了可能的精度损失外,还可能使模发生变化。强制转换格式如下:

(target-type) value;

如果整数的值超出了byte所能表示的范围,结果将对byte类型的范围取余数。例如a=256超出了byte的[-128,127]的范围,所以将257除以byte的范围(256)取余数得到b=1;需要注意的是,当a=200时,此时除了256取余数应该为-56,而不是200。
将浮点类型赋给整数类型的时候,会发生截尾。也就是把小数的部分去掉,只留下整数部分。此时如果整数超出目标类型范围,一样将对目标类型的范围取余数。

2.3 赋值及表达式中的类型转换

1、字面值赋值
在使用字面值对整数赋值的过程中,可以将 int 字面值赋值给byte short char int,只要不超出范围。这个过程中的类型转换时自动完成的,但是如果你试图将long 字面值赋给byte,即使没有超出范围,也必须进行强制类型转换。例如 byte b = 10L;是错的,要进行强制转换。
2、表达式中的自动类型提升
除了赋值以外,表达式计算过程中也可能发生一些类型转换。在表达式中,类型提升规则如下:

  • 所有byte/short/char都被提升为int。
  • 如果有一个操作数为long,整个表达式提升为long。float和double情况也一样。

3 装箱和拆箱

3.1 包装类、装箱、拆箱

Java 中为每一种基本数据类型提供了相应的包装类,如下:

Byte <-> byte
Short <-> short
Integer <-> int
Long <-> long
Float <-> float
Double <-> double
Character <-> char
Boolean <-> boolean

引入包装类的目的就是:提供一种机制,使得值类型可以与引用类型互相转换。
基本数据类型与包装类的转换被称为装箱和拆箱。

  • 装箱是将值类型转换为引用类型,例如:int 转 Integer。装箱过程是通过调用包装类的 valueOf 方法实现的。
  • 拆箱是将引用类型转换为值类型,例如:Integer 转 int。拆箱过程是通过调用包装类的 xxxValue 方法实现的。(xxx 代表对应的基本数据类型)。

3.2 自动装箱、自动拆箱

值类型的自动装箱、拆箱自 JDK 5 开始提供的功能。自动装箱与拆箱的机制可以让我们在 Java 的变量赋值或者是方法调用等情况下使用原始类型或者对象类型更加简单直接。
因为自动装箱会隐式地创建对象,如果在一个循环体中,会创建无用的中间对象,这样会增加 GC 压力,拉低程序的性能。所以在写循环时一定要注意代码,避免引入不必要的自动装箱操作。
JDK 5 之前的形式:

1
Integer i1 = new Integer(10); // 非自动装箱

JDK 5 之后:

1
Integer i2 = 10; // 自动装箱

Java 对于自动装箱和拆箱的设计,依赖于一种叫做享元模式的设计模式(笔者也没有具体去了解过,有兴趣的朋友可以去研究下)。

扩展阅读:
深入剖析 Java 中的装箱和拆箱
结合示例,一步步阐述装箱和拆箱原理。

3.3 装箱、拆箱的应用和注意点

装箱、拆箱应用场景:
1、一种最普通的场景是:调用一个含类型为 Object 参数的方法,该 Object 可支持任意类型(因为 Object 是所有类的父类),以便通用。当你需要将一个值类型(如 int)传入时,需要使用 Integer 装箱。
2、一个非泛型的容器,同样是为了保证通用,而将元素类型定义为 Object。于是,要将值类型数据加入容器时,需要装箱。
3、当 == 运算符的两个操作,一个操作数是包装类,另一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。
示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Integer i1 = 10; // 自动装箱
Integer i2 = new Integer(10); // 非自动装箱
Integer i3 = Integer.valueOf(10); // 非自动装箱
int i4 = new Integer(10); // 自动拆箱
int i5 = i2.intValue(); // 非自动拆箱
System.out.println("i1 = [" + i1 + "]");
System.out.println("i2 = [" + i2 + "]");
System.out.println("i3 = [" + i3 + "]");
System.out.println("i4 = [" + i4 + "]");
System.out.println("i5 = [" + i5 + "]");
System.out.println("i1 == i2 is [" + (i1 == i2) + "]");
System.out.println("i1 == i4 is [" + (i1 == i4) + "]"); // 自动拆箱
// Output:
// i1 = [10]
// i2 = [10]
// i3 = [10]
// i4 = [10]
// i5 = [10]
// i1 == i2 is [false]
// i1 == i4 is [true]

示例说明:
上面的例子,虽然简单,但却隐藏了自动装箱、拆箱和非自动装箱、拆箱的应用。从例子中可以看到,明明所有变量都初始化为数值 10 了,但为何会出现 i1 == i2 is [false] 而 i1 == i4 is [true] ?
原因在于:
i1、i2 都是包装类,使用 == 时,Java 将它们当做两个对象,而非两个 int 值来比较,所以两个对象自然是不相等的。正确的比较操作应该使用 equals 方法。
i1 是包装类,i4 是基础数据类型,使用 == 时,Java 会将两个 i1 这个包装类对象自动拆箱为一个 int 值,再代入到 == 运算表达式中计算;最终,相当于两个 int 进行比较,由于值相同,所以结果相等。

装箱、拆箱应用注意点:
1、装箱操作会创建对象,频繁的装箱操作会造成不必要的内存消耗,影响性能。所以应该尽量避免装箱。
2、基础数据类型的比较操作使用 ==,包装类的比较操作使用 equals 方法。


参考资料

深入理解 Java 基本数据类型
Java 基本数据类型 - 四类八种