Java是静态的强类型语言,在编译时所有的变量和表达式的类型就已经确定了。由于是强类型的,所有的变量和表达式也只能有一种类型,并且每种类型的都是严格定义的。这样就限制了表达式的操作类型。当然有的数据类型是可以兼容的,在赋值等操作时,兼容的数据类型可以相互操作,但是需要进行类型兼容检查。
在Java中有两种数据类型,一种是基本数据类型,一种是引用数据类型。
基本数据类型只有包含值,并且它是内存中存的值,只要找到基本数据类型的内存位置,就可以获得值。
引用类型引用的是对象,Java中的对象都是引用类型,对象散乱的存储在堆的不同位置;我们需要通过引用查找对象的内存位置;如果是对象数组,还需要查找引用的下一个内容空间。并且对象的占用空间更大。它除了本身的值之后,还有存储对象头,对齐填充等。
所以相对来说基本数据类型要比引用类型效率更高。
基本数据类型
在java中有8种基本数据类型。包括 byte、short、int、long、float、double、char、boolean。
其中表示数值的类型有 byte、short、int、long、float、double;其中float 和 double 表示的浮点型,也就是我们常说的小数,其它的则表示整型。
其中表示字符的有char ,有的地方也将char 作为数值类型看待,因为它本质上也是一个int。
其中的boolean比较特殊,它只有两个值,true 和 false 。一般用于判断。
整形
整形包含 byte、short、int、long,分别用 8、16、32、64 位表示,即使用 1、2、4、8 个字节表示。它们都是2的补码的形式表示。默认值都是0。
在java虚拟机规范中把 char 类型也包含到整形中,char是一个无符号的整形,它表示的是一个 Unicode 基本多语言平面中的码点,使用的是 UTF-16 编码,默认值为 null ,其码点值为 \u0000
。
byte
byte 由1个字节,8位表示。在java中属于最小的整数类型。它的取值范围是 [-128 - 127]
,即 [-2^7 - 2^7-1]
。
short
short 由2个字节,16位表示。它的取值范围是 [-32768 - 32767]
,即 [-2^15 - 2^15-1]
。在开发中 short 极少用到,它与 int 或者 long 进行运算时,一般都会向上提升为 int 或者 long 型。
int
int 由4个字节,32位表示。它的取值范围是[-2147483648 - 2147483647]
,即 [-2^31 - 2^31 - 1]
。
long
long 由8个字节,64位表示。它的取值范围是 [-9223372036854775808 - 9223372036854775807]
,即[-2^63 - 2^63-1]
。它的默认值为0L , 一般表示 long 型是需要在后面加 L
或者 l
后缀 。
char
char 由 2 个字节,16位表示。是一个无符号的整型,表示的是一个 Unicode 基本多语言平面中的码点,使用的是 UTF-16 编码,默认值为 null ,其码点值为 \u0000
。取值范围 \u0000 - \uFFFF
。\u
是转义字符,表示把后面的16进制转换为相应的字符。
char 可以进行基本的加减乘除四则运算,在运算前会把字符转换成对应的 int 值,然后再进行运算。
浮点型
浮点型可以简单理解成小数。分为 float 和 double。float 占4个字节32位,double占8个字节64位。
Java 中的浮点型遵循了 IEEE 754 标准,根据 IEEE 754 标准,任意一个二进制浮点数V可以表示成下面的形式:
![](https://cdn.nlark.com/yuque/__latex/a424988e5aa40f2b9674415ccae9f1e4.svg#alt=img)
表示符号位,当s = 0,V为正数;当s = 1,V 为负数。
M 表示有效数字,大于等于1,小于2。
表示指数位。其中叫做指数,或者 阶码。
M 的取值范围一般为 (二进制浮点数一般的都能写成 1.xxxx 的形式)
根据 IEEE754 标准,浮点数存储分为三个部分: 符号(sign),指数(exponet),有效数字(fraction),分别对应上面的 ,E , M。
根据 IEEE754 标准,对于32位的浮点数,最高位是符号位 s,接着8位是指数E,剩下的 23 位为有效数字M。
对于64位的浮点数,最高的1位是符号位 S ,接着的11位是指数E,剩下的52位为有效数字M。
具体请参见:浮点值的二进制表示
float
float 由4个字节,32位表示。对应的是单精度浮点数。最小值:1.4E-45,最大值:3.4028235E38,默认值 0.0F。
double
double 由8个字节,64位表示。对应的是双精度浮点数。最小值:4.9E-324,最大值:1.7976931348623157E308,默认值 0.0D。
对于浮点类型的数据,如果不显示的添加 F/f 或者 D/d 后缀,则默认其为 double 类型。
对于float和double 在计算时都会有精度问题。如果要求精度,最后使用 BigDecimal 。
boolean
boolean 只能取到两个值,true或者false。虽然 JVM 定义了boolean 类型,但是对其的支持有限。在 JVM 中没有专门的指令用于的boolean的操作。boolean 会被编译 int 类型。在 JVM 中是boolean 是以 int 值类型操作的。
JVM 使用 1 表示true ,对应的指令是 iconst_1;使用 0 表示 false ,对应的指令是 iconst_0。
类型转换
在 Java 中有些类型的值是可以互相转换的。不过 boolean 的值是不可以同其它类型的值相互转换的;其它7中基本类型都可以相互转换。
类型转换可能会导致溢出或者牺牲一部分精度。
类型转换分为自动类型转换和强制类型转换。
自动类型转换
当将取值范围低的类型赋值给取值范围高的类型时,会自动完成类型转换。比如将 byte 类型的值赋值给 int 类型,byte 会自动转换成 int 类型。
byte->short->int->long
取值范围从低到高,按照箭头将低取值范围的类型赋值高取值类型,就会触发自动类型转换。
float->double
取值范围从低到高,按照箭头将低取值范围的类型赋值高取值类型,就会触发自动类型转换。
以上的自动类型转换,一般不会损失精度,也不会溢出。
强制类型转换
当将取值范围高的类型赋值给取值范围低的类型时,必须使用强制类型转换,这种情况下,可能发生溢出或者精度损失。
比如,我们将 int 赋值给 byte ,超出byte取值范围的高位值将会被舍弃,只保留其范围内的数据。
int i = 300;
byte b = (byte) i;
System.out.println(b); // 44
上面代码中我们将 long 型的 300 强制转换成 byte 类型,结果为 44 。300 超过了byte的取值范围 [-128 - 127]
,将会舍弃高位。
我们将300转换为2进制,int 占有4个字节 32 位,300 的原码 反码 补码 不变 :
0000 0000 0000 0000 0000 0001 0010 1100
byte 占有一个字节,8位,将高位舍弃,只保留地位:
0010 1100
其首位为0,表明其为正数,不需要反码和原码的转换。将其转换为10进制为 44 。
包装类
Java 是面向对象的语言,但是其提供的8种数据类型是一个例外。 Java的基本数据类型,仅仅能表示一个单纯的值,不是一个复杂的对象,只是为了性能和效率方面的问题。对应的Java提供了一批包装类,来一一对应其这些基本数据类型。包装类不仅提供了字段来保存各自类型的数据,还提供了一些其它的基础操作。
另外,Java的基本数据类型不能与Java泛型配合使用。Java 设计了自动拆箱装箱机制,使基本数据类型与包装类直接可以进行自动转换。
Integer
这是一个非常有代表性的类。它可以基本代表除Character之外的是所有整形包装类的实现规则。只要明白了这个类的实现细节,其它的包装类就会触类旁通。
Integer 是对基本数据类型 int 的包装。一个Integer对象只能包含一个int的值。
Integer 提供了一些方法,使 int 可以同 String或者其它数据类型进行转换。也提供了一些有用的方法用来处理 int 值。还提供了一些简单数学运算。
自动拆箱、装箱
Java 5 提供了自动装箱和拆箱的新特性。Java 可以根据上下文,对基础类型和和包装类型之间自动转换。这些转换是隐式的,它本质上是一个语法糖,是由编译器帮我们完成的类型转换。
自动装箱就是自动将基本数据类型转换成对应的包装类型。
自动拆箱就是自动将包装类型转换成对应的基本数据类型。
Integer i = 123;
上面的代码中,123
属于基本数据类型,而变量 i
是 Integer
类的对象 ,我们把基本数据类型直接赋值给 Integer 对象,这就属于装箱操作,把基本数据类型装箱撑包装类。
Integer i = new Integer(3);
int i2= i;
上面的代码把一个Integer对象赋值一个基本数据类型,这就属于拆箱。
从上面的代码看,自动装箱就是一种语法糖。它的实现方式什么呢?我们反编译看一看。
先看 Integer i = 123;
,反编译后的代码如下:
public static void main(java.lang.String[]);
Code:
0: bipush 123
2: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
5: astore_1
6: return
}
在 #2
处我们看到它执行了 Integer.valueOf(int)
方法。 也就是说装箱的操作使用的 Integer.valueOf(int)
。我们看它的源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
很简单,判断基本类型的值是否在缓存里面,如果存在返回,如果不存在,直接new一个新的Integer对象。
然后看拆箱代码,将上面的拆箱相关的代码反编译后:
public static void main(java.lang.String[]);
Code:
0: new #2 // class java/lang/Integer
3: dup
4: iconst_3
5: invokespecial #3 // Method java/lang/Integer."<init>":(I)V
8: astore_1
9: aload_1
10: invokevirtual #4 // Method java/lang/Integer.intValue:()I
13: istore_2
14: return
在#4
处,我们看到执行了 Integer.intValue()
。也就是说当我们把包装类型赋值给基本类型时,包装类调用了 intValue()
。我们看 intValue()
的源码:
private final int value;
public int intValue() {
return value;
}
直接返回了包装的基本类型的值。
设计自动拆箱和装箱的最大原因是因为在 Java 5 引入的泛型不能配合基本数据类型使用。从另一方面想,一般涉及到的泛型操作,很多时候都会设计到拆箱或者装箱。
一般在什么时候会涉及到拆箱或者装箱呢?
- 赋值操作,比如上面提到的两个例子,拆箱和装箱都有可能遇到;
- 加减乘除,比较操作;只要涉及到包装对象的,运算的时候都会有拆箱操作,这样效率更高;
- 包装类调用 equals() ,这个方法必须由包装类调用,当 Integer对象与 int 比较时,就会涉及到装箱操作;
- 集合类添加基本数据时;这个涉及的了泛型,当通过包装类声明的集合类,往集合中添加基础类型数据时,就会涉及到装箱操作。
自动装箱、拆箱在 double/Double
,float/Float
,boolean/Boolean
等基本类型与相应对象之间的自动转换。
缓存
我们再来重新看一下 valueOf(int)
的源码:
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
这是一个静态工厂方法,里面会先检查传入的 int 值是否已经在缓存中了,如果不存在则 new 一个 Integer 。
这里的 IntegerCache
类的作用就是用来缓存一部分 int 数据。默认 IntegerCache
缓存 -128 ~ 127
之间的整数。其中最大值 127 可以通过 -XX:AutoBoxCacheMax=<size>
进行配置。
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
// 最大值可以通过 `-XX:AutoBoxCacheMax=<size>` 配置
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 数组里保存了 -128 ~ 127
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
当我们的代码涉及到自动装箱操作的时候就要注意了,比如下面的代码:
Integer i = 123; // 自动装箱,调用 valueOf(int)
Integer i2 = 123;
System.out.println(i == i2); // true
上面的代码输出 true ,就是因为在装箱的时候执行了 valueOf(int)
方法。只要 int 的值在缓存范围以内就会返回同一个 Integer 对象。
这种缓存机制不止 Integer 才有,其他的一些包装类也同样有。
- Boolean 缓存了 true 和 false 对应的 Boolean 对象那个的实例,即
new Boolean(true/flase)
- Short 同样缓存了 -128 到 127 ;
- Byte 把它范围内的所有数值都缓存了;
- Character 缓存了
\u0000 到 \u007F
。
其它方法
除了上面说的功能,Integer 还提供了很多其它的操作:
toHexString(int i)
转换成 16 进制toOctalString(int i)
转换成 8 进制toBinaryString(int i)
转换成 2 进制parseInt(String s, int radix)
按照指定进制,将字符串转换为数字parseInt(String s)
将字符串转换为10进制的数字decode(String nm)
将十进制、十六进制和八进制数的字符串解析成 IntegergetInteger(String nm)
根据 nm 获取 property 的值,即System.getProperty(String)
上面提供的仅仅是一些常用的,还有其它一些不在赘述。
Boolean
Boolean 是对基本数据类型boolean的包装类。里面除了提供了 boolean 类型的属性外,还提供了一些逻辑运算相关操作。
Boolean 的取值只有两个,它把它们都缓存起来了。
Boolean 缓存了 true 和 false 对应的 Boolean 对象那个的实例,即 new Boolean(true/flase)
Boolean 类还有一些其它静态方法,来补充它的功能。
parseBoolean(String)
将String解析成 boolean 类型,这里忽略大小写的,只要字符串是true
则返回true,其它情况下都是 false。
booleanValue()
: 将当前 Boolean 对象的 boolean 值。
valueOf(String)
: 类似于 parseBoolean(String)
valueOf(boolean)
: 返回一个指定了 boolean 基本类型的值的 Boolean 对象
toString()
:将当前 Boolean 对象代表的 boolean 基本类型的值转换成 String
Character
character 是对基本数据类型 char 的包装类。char 一般表示一个字符,不能转换成其它的数据类型,也不能查看它的码点等等。所以 Java 提供了char 的包装类 Character。
具体请查看 char 和 Character
Character 缓存了 [0 - 127]
即 [\u0000 - \u007F]
之间的数值。
Byte 和 Short
Byte 和 Short 很多实现都是用的 Integer 和 int ,然后强转成 byte / short。
它们都缓存了 [-128 - 127]
之间的值。
Float 和 Double
Java中的单/双精度浮点数符合 IEEE 754 标准。具体的参见 浮点值的二进制表示
Float 和 Double 中是没有缓存的。因为它们的取值太多,缓存没有意义。
它们提供了基本相同的功能:
最大、最小值
Float/Double.MAX_VALUE
和Float/Double.MIN_VALUE
将字符串转换成浮点数
parseFloat(String)
和parseDouble(String)
将浮点数转换16进制的字符串 toHexString(float/double)
还有其他的一些操作就不一一赘述了。
总结
每种基本数据类型对应了一种包装数据类型。
自动装箱和拆箱的出现主要是为了解决泛型不能与基本数据类型配合使用的问题。
整形相关的包装类,都缓存了一部分常用数据,以提升性能。
Boolean 也缓存了 true 和 false 。
浮点类型的 Float 和 Double 都遵循 IEEE 754 标准。