数据类型
Java数据类型可分为两大类:
基本类型:又叫内置数据类型,或值类型;
- 变量名指向具体数值;
- 变量声明后立刻分配内存空间;
- 使用时需要赋值,判等时可使用运算符
==
;
引用类型:除值类型之外,都是引用类型,如数组、字符串、对象引用;
- 变量名指向对象的内存地址;
- 变量声明后不会分配分配内存,只存储了一个内存地址;
- 使用时可以赋值为
null
,判等需使用equals()
方法;
基本类型
基本类型分类
Java语言提供了8种基本类型,大致分为4类:
类型名称 | 分类 | 比特位 | 默认值 | 取值范围 | 说明 |
---|---|---|---|---|---|
boolean | 布尔型 | 8bit | false | {false,true} | |
char | 字符型 | 16bit | \u0000 |
[0,2^16-1] | 存储Unicode 码,用单引号赋值 |
byte | 整形 | 8bit | 0 | [-2^7, 2^7] | -128到127 |
short | 整形 | 16bit | 0 | [-2^15, 2^15-1] | -32768到32767 |
int | 整形 | 32bit | 0 | [-2^31, 2^31-1] | 上限大约为21亿 |
long | 整形 | 64bit | 0L | [-2^63, 2^63-1] | 赋值需在数字后面加上l 或L |
float | 浮点型 | 32bit | +0.0F | (-2^-126, 2^127) | 赋值需在数字后加上f 或F |
double | 浮点型 | 64bit | +0.0D | (2^-1022, 2^1023 ) | 赋值需在数字后加上d 或D |
1字节=8比特位,或者说 1byte=8bit
整形及浮点型都是有符号数据
各种基础类型默认值显示不一样,但内存中实际都是0
单精度浮点型取值需去除非规格数
-(2-2^-23)*2^127
到2^-126
双精度浮点型取值需去除非规格数
-2(2-2^-52)* 2^1023
到2^-1022
Java中的数值类型不存在无符号的,取值范围也是固定的,不会随着硬件改变而变动;
实际上,Java中还存在另外一种基本类型Void
,对应包装类为java.lang.Void
,但无法对其进行操作;
基本类型的好处
我们都知道在Java语言中,new
一个对象是存储在堆里的,我们通过栈中的引用来使用这些对象,对象本身是比较消耗资源的。
对于经常用到的类型,如int
等,如果每次使用这种变量都要new
一个Java
对象的话,就会比较“笨重”。所以,与C++
一样,Java
提供了基本数据类型,这种类型的变量不需要使用new
来创建,所以它们不会在堆上创建,而是直接在栈内存中存储,因而更加高效。
什么是浮点型
计算机的数字存储与运算都是通过二进制进行的;
其表示形式如下:
类型 | 数符(m) | 阶码(E) | 尾数(M) | 指数范围 | |
---|---|---|---|---|---|
单精度 | 1位 | 8位 | 23位 | [-126,127] | |
双精度 | 1位 | 11位 | 52位 | [-1022,1023] | |
临时 | 1位 | 15位 | 64位 | [-16382,16383] |
其中:
- 数符:尾数的符号位;1为负,0为正;
- 阶码:表示数的幂,基数为2;用移码表示;
- 尾数:表示数的小数部分,基为2;用原码表示;
二进制转十进制
除二取余法(整数)
- 用2整除十进制数,得到商和余数;
- 循环往复,直到商小于2时为止;
- 将所有得到的余数按倒序排列即为对应的二进制数;
125转换为二进制,计算流程如下:
被除数 除数 商 余数
125 2 62 1
62 2 31 0
31 2 15 1
15 2 7 1
7 2 3 1
3 2 1 1
最后转换的二进制数为:111101;
乘二取整法(小数)
- 用2乘以十进制小数,得到乘积,取出整数部分;
- 循环往复,直到得到的乘积小数部分为零为止;
- 将所有取出的整数部分按正序排列作为转换后的二进制小数部分;
将0.1转换为二进制小数,计算流程如下:
被乘数 乘数 积 取整
0.1 2 0.2 0
0.2 2 0.4 0
0.4 2 0.8 0
0.8 2 1.6 1
0.6 2 1.2 1
0.2 2 0.4 0
……
转换后二进制小数为:0.000110……
0.1的二进制小数为无限循环小数,所以计算机无法精确表示0.1;
所以计算机科学中,使用浮点数来表示实数的近似值;
IEEE754标准
IEEE二进制浮点数算术标准(IEEE 754)是20世纪80年代以来最广泛使用的浮点数运算标准,为许多CPU与浮点运算器所采用。用以解决部分小数无法使用二进制精确表示的问题;
IEEE 754规定了四种表示浮点数值的方式:单精确度(32位)、双精确度(64位)、延伸单精确度(43比特以上,很少使用)与延伸双精确度(79比特以上,通常以80位实现);
其中最常用的就是32位单精度浮点数和64位双精度浮点数;
IEEE并没有解决小数无法精确表示的问题,只是提出了一种使用近似值表示小数的方式,并且引入了精度的概念;
计算机中保存的小数其实是十进制小数的近似值,并不是精确值;所以业务系统千万不要使用浮点数来表示金额等重要指标;建议使用
BigDecimal
或Long
来表示金额;
类型转换
Java中,数据类型转换有两种方式:
- 自动转换
- 强制转换
自动转换
一般情况下,定义了某种类型的变量,就不能随意转换。但Java允许用户对基本类型做有限度的类型转换;
以下情况会自动进行类型转换:
由小数据转换为大数据
显而易见的,“小”数据类型的数值表示范围小于“大”数据类型的数值表示范围,即精度小于“大”数据类型;
所以,如果“大”数据类型向“小”数据类型转换,可能会丢失精度,导致结果失真;反之,“小”数据类型向“大”数据类型转换,则不会存在数据丢失风险;这种类型转换也称为扩大转换;
数值运算
不同精度的数值进行运算后,结果为“大”数据类型;例如,整形与浮点型计算后结果为浮点型;
类型转换兼容
上文所说的“小”数据类型和“大”数据类型,指的是表示值的范围与精度大小;
基础类型的大小顺序为:
byte、char、short
<int
<long
<float
<double
强制转换
在不符合自动转换条件或根据用户的需要,可以对数据类型做强制转换;
通过小括号()
来进行类型强制转换,引用类型也可以使用强制类型转换;
数值溢出
常用基本类型计算时,都有可能超出表达范围的可能性,且数值溢出时不会抛出任何异常,所以这类问题非常容易被忽略。改进的方法为:
- 使用
Math
类的addExact
、subtractExact
等方法进行数值运算,这样在产生溢出时主动抛出异常; - 使用大数类
BigDecimal
和BigInteger
。BigDecimal
是处理浮点数专家,BigInteger
是处理整数专家;
数据类型判等
Java中,通常使用equals()
方法或者==
运算符进行判等操作;
==
- 操作符,用于对字面量进行判等,主要适用于基本类型;
equals()
:- 方法,可对对象内容进行判等,主要适用于引用类型;
- 自定义实现技巧:
- 考虑性能:先进行指针判等,如果相同则直接返回
true
- 控制判断:优先对乙方判空,空对象与自身比较,则直接返回
false
- 类型判断:如果类型不同,则直接返回
false
- 值判断:类型相同的情况下,逐一判断所有字符
- 考虑性能:先进行指针判等,如果相同则直接返回
引用类型判定注意事项:
hashCode()
和equals()
需要配对实现compareTo
和equals()
的实现逻辑需一致
Lombok
注解避坑:
Lombok
的@Data
注解会自动实现equals()
和hashCode()
方法,但存在继承关系时,注解自动生成的方法可能不符合开发者预期;
@EqualAndHashCode
默认实现没有使用父类属性,需要手动设置callSuper
开关为true
来覆盖默认行为;
开发技巧
包装类不能使用操作符
==
进行判等,必须使用equals()
进行判等;字符串正确的判定方式是使用
equals()
方法;但字符串内容存储在常量池中,也可以使用操作符==
进行判等;
引用类型
引用类型是指Java中除了基本类型之外的所有类型(比如通过class
定义的类型),它的值是指向内存空间的引用,也就是内存地址;
Java引用类型分类如下:
强引用
在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用;当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后一直不会被使用到,JVM也不会进行回收;
强引用是造成Java内存泄露的主要原因之一;
软引用
软引用需要通过SoftReference
类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存不足时它就会被回收;
软引用通常用在对内存敏感的程序中;
弱引用
弱引用需要用WeakReference
类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收触发,不管JVM的内存空间是否足够,总会回收该对象占用的内存;
虚引用
虚引用需要用PhantomReference
类来实现,它不能单独使用,必须和引用队列联合使用;
虚引用的主要作用是跟踪对象被垃圾回收的状态;
包装类型
Java
语言是一个面向对象的语言,但是Java
中的基本数据类型却不是面向对象的,这在实际使用中存在很多的不便,为了解决这个不足,在设计类时为每个基本数据类型设计了一个对应的类进行代表,这样8个和基本数据类型对应的类统称为包装类(Wrapper Class)。
包装类均位于java.lang
包中,包装类与基本类型的对应关系如下:
基本类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
char | Character |
在这8个类名中,除了Integer
和Character
类以后,其他6个类的类名与基本数据类型一致,只是类名的第一个字母大写;
为什么需要包装类
既然Java
为了提高效率,提供了8种基本数据类型,为什么还要提供包装类?
因为Java
是一种面向对象语言,很多地方都需要使用对象而不是基本数据类型。比如,集合类中,我们无法将int
、double
等基本类型放进去。因为集合的容器要求元素是Object
类型。
为了让基本类型也具有对象的特征,就出现了包装类型,相当于将基本类型“包装起来”,使得基本类型具备对象的属性,并且为其添加了属性和方法,丰富基本类型的操作。
拆箱与装箱
有了基本类型与包装类,就需要在两者之间进行转换。比如把一个基本类型的int
转换成一个包装类型Integer
对象。
包装类是对基本类型的包装,所以,把基本类型转换成包装类的过程就是boxing
,中文翻译为装箱;反之,把包装类转换为基本类型的过程就是unboxing
,中文译为拆箱。
自动拆箱与自动装箱
Java
在Java SE5
中为了减少开发人员的工作量,提供了自动拆装箱的能力。
自动装箱:即将基本类型自动转换为包装类;
自动拆箱:即将包装类自动转换为基本类型;
Integer i = 10; //自动装箱
int b = i; // 自动装箱
Integer i =10
实际对应的代码应该是Integer i = new Integer(10)
,得益于Java
自动装箱功能,让开发者可以省略了手动去new
一个对象。
自动装箱是通过包装类的
valueOf
方法来实现的,自动拆箱功能是通过包装类对象的xxxValue()
来实现的。
自动拆装箱应用场景
除了最简单基础的变量初始化与赋值外,以下场景均会进行自动拆装箱:
场景一:将基本类型放入集合类
List<Integer> list = new ArrayList<>();
for( int i=1; i<10; i++) {
list.add(Integer.valueOf(i))
}
场景二:包装类型与基本类型比较
Integer i1 = 1;
System.out.println( i1 == 1 ? "true" : "false");
Boolean bool = false;
System.out.println(bool ? "True" : "False");
反编译后代码如下:
Integer i1 = Integer.valueOf(1);
System.out.println(a.intValue() == 1 ? "true" : "false")
Boolean bool = Boolean.valueOf(false);
System.out.println(bool.booleanValue ? "True" : "False");
场景三:包装类运算
Integer i1 = 10;
Integer i2 = 5;
System.out.println( i1 + i2);
反编译代码如下:
Integer i = Integer.valueOf(10);
Integer j = Integer.valueOf(5);
System.out.println(i.intValue() + j.intValue());
场景四:三目运算符
boolean flag = true;
Integer i = 0;
int j =1;
int k = flag ? i : j;
以上代码反编译后如下:
boolean flag = true;
Integer i = Integer.valueOf(0);
int j = 1;
int k = flag ? i.intValue() : j;
System.out.println(k);
在三目运算表达式中,当第二、第三位操作数分别为基本类型与对象时,其中的对象会拆箱为基本类型。
场景五:函数参数与返回值
//自动拆箱
public int getNum1(Integer num) {
return num;
}
//自动装箱
public Integer getNum2(int num) {
return num;
}
自动拆装箱与缓存
Java SE
自动拆装箱提供了一个和缓存相关的功能;
public static void main(String... String) {
Integer int1 = 3;
Integer int2 = 3;
if (int1 == int2) {
System.out.println("int1 == int2");
}
else {
System.out.println("int1 != int2");
}
Integer int3 = 300;
Integer int4 = 300;
if (int3 == int4) {
System.out.println("int3 == int4");
}
else {
System.out.println("int3 != int4");
}
}
输出结果为:
int1 == int2
int3 != int4
原因与Integer
中的缓存机制有关,在Java SE5
中,在Integer
的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。
- 当需要进行自动装箱时,如果数字在 -128 至 127 之间时,会直接使用缓存中的对象,而不是重新创建一个对象。
- 只适用于自动装箱,使用构造函数创建对象不适用;
- Javadoc 详细的说明了缓存支持 -128 到 127 之间的自动装箱过程。最大值 127 可以通过
-XX:AutoBoxCacheMax=size
修改。- 在 Java 6 中,可以通过
java.lang.Integer.IntegerCache.high
设置最大值。
自动拆装箱带来的问题
自动拆箱装是一个很好的功能,大大节省开发人员的精力,但是也会引入一些问题:
- 包装对象的数值比较,不能简单的使用
==
,虽然-128到127之间的整数可以,但这个范围之外还是需要使用equals
比较。 - 有些场景会进行自动拆装箱,如果包装类对象为
null
,那么自动拆装箱时就可能抛出NPE。 for
循环内部进行大量拆装箱,会浪费大量资源;