还记得去年公司的一次知识地图认证,我报了一个“Java基础”。下面是我答题过程:dfsfasd
考官:Java有哪些数据类型?
我:有八大基本类型,分别是:byte、int、long、float、double、char、boolean…还有一种是啥来着?
考官:这种类型很少用,平时开发基本上看不到,你再想想。
我(想了一会儿):我确实想不起来了
考官:short,一种整型数据类型。
作为一个使用java近十年的老兵没有回答出这个问题,我感到非常汗颜,也因此将这个作为了我重学Java路线中第一课。
接下来正式开始学习java的基本类型和他们的包装类型,里面会有一些工作过程中遇到的和学习到的一些内容,还有本次学习到的一些新内容。
基本类型
基本类型作为java中类的基础,几乎所有的类追根溯源后都是基本类型的组合,这个C语言中的结构体有些相似——通过基本类型+数据结构组合成为结构体以达到某些特殊功能。与其他某些开发语言不同的是,java的基本类型位数是固定的,这也是java能跨平台运行的一大保障。
Java语言提供了八种基本类型。六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型:
基本类型 | 分类 | 默认值 | 位数 | 取值范围 | 包装类型 |
---|---|---|---|---|---|
byte | 整型 | 0 | 8 | -2^7^ ~ 2^7^-1 | Byte |
short | 整型 | 0 | 16 | -2^15^ ~ 2^15^-1 | Short |
int | 整型 | 0 | 32 | -2^31^ ~ 2^31^-1 | Integer |
long | 整型 | 0L | 64 | -2^63^ ~ 2^63^-1 | Long |
float | 浮点类型 | 0.0f | 32 | 32位单精度 IEEE 754标准 | Float |
double | 浮点类型 | 0.0d | 64 | 64位双精度 IEEE 754标准 | Double |
char | 字符类型 | 空格符 | 16 | \u0000 ~ \uffff | Character |
boolean | 布尔类型 | false | 1 | ture 和 false | Boolean |
char类型
java中char类型长度为16位,采用的是Unicode编码。Unicode编码包含了ASCII的所有码值,对应的整型数值与ASCII一致(0-127)。
其实在English,Spanish,German, French中只需要ASCII码即可,但是考虑到其他语言,比如中文、日语、汉语、俄语等ASCII码无法表示的语言,做出了一些妥协。
赋值方法
char类型在java中有三种初始化方法:
// 字符:汉字、符号、数字、转义字符等
char c = '你';
// 整型数值赋值:十进制、八进制、十六进制均可。
char c = 23;
// unicode码值赋值
char c = '\uffff';
单引号在java语法中唯一合法的地方就是char类型复制。
运算
由于char使用的Unicode码,可以使用整型方式赋值,因此在java中char可以进行‘+、-、* 、/ 、%、>、<、^’等运算,例如:
char a = 'a';
char a = a + 'a';
int i = a + 'a';
char a = a + 1;
int i = a + 1;
......
总之,char + char 和 char + int 均为提升为int运算,赋值给char类型后,输出后就是对应的字符了。
类型提升问题
其实在char+char, char+int的过程中,并没有发生实际上的类型提升:
package com.xxxx.t3;
public class BaseType {
public static void main(String[] args) {
int i=0;
float f = 1;
byte b = 1;
char c = 0;
System.out.println(c >> b);
System.out.println(c + b);
System.out.println(i+f);
System.out.println(i + b);
}
}
编译后使用javap -c -l BaseType
输出汇编指令集:
public class com.xxxx.t3.BaseType {
public com.xxxx.t3.BaseType();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: fconst_1
3: fstore_2
4: iconst_1
5: istore_3
// 从这儿可以看出,Java中生命char的时候就是以整型数据存储的
6: iconst_0
7: istore 4
9: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// char类型的位运算,直接取了两个整型数值进行运算
12: iload 4
14: iload_3
// 位运算
15: ishr
16: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
19: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// char + int 的指令,其实就是整数相加
22: iload 4
24: iload_3
25: iadd
26: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
29: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// int + float的指令, 执行了i2f执行,将整型转为浮点类型,然后相加
32: iload_1
33: i2f
34: fload_2
35: fadd
36: invokevirtual #4 // Method java/io/PrintStream.println:(F)V
39: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
// int + byte的指令, 也是两个整型数值相加
42: iload_1
43: iload_3
44: iadd
45: invokevirtual #3 // Method java/io/PrintStream.println:(I)V
48: return
LineNumberTable:
line 5: 0
line 6: 2
line 7: 4
line 8: 6
line 9: 9
line 10: 19
line 11: 29
line 12: 39
line 13: 48
}
更多类型提升问题,后面专门做一个课来研究这个。
相等性判断
相同基本类型判断相等性的时候是直接使用==
符号进行判断,例如:
int i1 = 1;
int i2 = 2;
System.out.println(i1 == i2);
不相同的基本类型也是可以进行相等性判断,例如:
byte b = 1;
int i = 1;
System.out.println(b == i); // true
不同基本类型判断相等性是,遵循一定的原则:
- 数值型可以相互进行相等性判断,判断过程中会进行类型提升,提升顺序为:byte -> short -> int -> long -> float -> double。即短类型转长类型,整型转浮点型。
- 字符类型可以与数值型进行相等性判断,字符型类型在java中可以使用int类型表示,所以不需要在代码中进行强制类型转换。判断过程中取字符类型变量对应的整型数值与之进行判断。
- 布尔型不可与其他类型进行相等性判断。
整型数值范围计算
我们拿byte这个8位的数据类型来举例,取值范围是怎么算出来的呢?
首先byte只有8位,那么二进制补码取值就在:11111111 ~ 01111111 之间了。
- 第一位是符号位,1表示负数,0表示这个数。
- 0开始的数值总共有2^7^个,去除00000000,还有2^7^-1个,所以最大值为2^7^-1。
- 1开始的数值也有2^7^个,全部为负数,所以最小是为-2^7^。
由此可以算出byte的取值范围:-2^7^ ~ 2^7^-1。
其他整形数据类型取值范围计算方式类似。
取值范围估算
- int 32位:可以估算为:2^10^ 2^10^ 2^10^ 2 ≈ 1000 1000 * 1000 = 1000000000 (十亿级)
- long 64位:十亿 * 十亿 ,姑且称为“百亿亿级”数据
浮点型取值范围计算
浮点数取值范围计算咱们以floatl类型举例,double的计算方法几乎一致。
变量存储
基本类型
包装类
包装类(Wrapper Class): Java是一个面向对象的编程语言,但是Java中的八种基本数据类型却是不面向对象的,为了使用方便和解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样八种基本数据类型对应的类统称为包装类(Wrapper Class),包装类均位于java.lang包。
对于包装类说,用途主要包含两种:
- 作为 和基本数据类型对应的类 类型存在,方便涉及到对象的操作。
- 包含每种基本数据类型的相关属性如最大值、最小值等,以及相关的操作方法。
自动装箱和拆箱
先看一下下面的代码, 还是以int为例子:
public class BaseType {
public static void main(String[] args) {
Integer i = 1;
int b = i;
}
}
编译后执行javap -c -l BaseType
的结果:
Compiled from "BaseType.java"
public class BaseType {
public BaseType();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 1: 0
public static void main(java.lang.String[]);
Code:
0: iconst_1
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
4: astore_1
5: aload_1
6: invokevirtual #3 // Method java/lang/Integer.intValue:()I
9: istore_2
10: return
LineNumberTable:
line 3: 0
line 4: 5
line 5: 10
}
从上面的执行结果可以得出一下结论:
Integer
的自动装箱是使用Integer.valueOf(int i)
实现的。Integer
的自动拆箱是使用Integer.intValue()
实现的。
那么还有一个问题,这个自动装箱拆箱是在什么时候进行的呢?
反编译了一下BaseType.class
(不要用idea的反编译工具,idea这个太智能了):
public class BaseType
{
public static void main(String[] paramArrayOfString)
{
Integer localInteger = Integer.valueOf(1);
int i = localInteger.intValue();
}
}
可以看出自动装箱拆箱在编译阶段就完成了,也就是说这就是一个“语法糖”。
其他基本类型的自动装箱和拆箱原理基本都是如此。
热值缓存
java包装类中针对一些热点数据进行了缓存,主要目的是用于自动装箱的时候直接获取缓存对象,不创建新对象。这个地方是享元模式
的一种使用场景。浮点类型数据没有较为特别的热点数据,所以没有进行缓存。
所以,通过自动装箱或者valueOf
方法产生的包装对象,在其基本类型值相同的时候,可以使用==
判等。但是绝不建议这么做,毕竟我们无法保证两个对象都是自动装箱或者valueOf
,也许是new
出来的也不一定。
各种包装类型热值缓存情况如下:
包装类型 | 缓存数据范围 |
---|---|
Byte | 所有byte范围 |
Short | -128 ~ 127 |
Integer | -128 ~ 127(可配置) |
Long | -128 ~ 127 |
Character | ASCII码值 |
Boolean | 直接声明TRUE和FALSE两个变量 |
Float | 无缓存 |
Double | 无缓存 |
需要特别注意的是Integer的缓存范围可以通过参数配置,有两种方式:
- 添加JVM配置
-XX:AutoBoxCacheMax=<size>
配置项 - 启动参数设置
java.lang.Integer.IntegerCache.high
属性
但是,如果手动设置的值如果小于127,则不会生效。
包装类型初始化
由于热值缓存的存在,所以更建议非浮点类型包装类型使用valueOf
的方式进行初始化。
针对浮点类型Float
和Double
初始化使用valueOf
和new
是等效的,从某方面讲new
可能更加高效一些。
不可变对象
所有包装类中只有一个final
的基本类型成员变量,这意味着,包装类实例一旦初始化,其成员变量就变得不可更改了。
这里面有两个点:
- 成员变量全是基本类型
final
不可修改
不可变对象保证了所有包装类实例的线程安全。
可以参考《Effective Java》中对不可变对象的定义和描述。
包装类实例运算
包装类实例本身是一个“对象”,对象本身是不支持+、-、* 、/....
等运算符的(除String
支持+
符号以外)。所以,包装类实例运算实际上就是:拆箱后使用基本类型运算。
那么,这个地方就有一个咱们经常犯的错误:在为对实例判空,就直接进行运算。比如:
Integer count = computeValue(); // maybe null
if(count < 1) {
// do something!
}
如果computeValue
方法直接返回null
,在执行count < 1
的时候就会NPE了。
至于原因就是拆箱的时候,实际上就是在调用实例自身的xxValue()
方法,实例为空自然会NPE。