Java 中的变量类型、拆箱装箱及相互间的转换
一、Java 中变量类型
1.1 以数据类型划分
1.1.1 基本数据类型
- 整数型变量
变量名 | 说明 |
---|---|
byte | 1字节,包装类为Byte |
short | 2字节,包装类为Short |
int | 4字节,包装类为Integer |
long | 8字节,赋值常量后面加L,包装类为Long |
注意:1字节是8byte,以int为例,范围是[-2^31, 2^31-1],因为存在负数,故指数位要-1,正整数部分存在0,故要-1。0开头表示八进制,0x开头表示十六进制。
- 浮点型
表示形式:十进制数形式(3.14)、科学记数法形式(314E-2)。
特点:- 不精确,不能用于比较;(除非使用java.math包中的BigDecimal类)
- Java默认double为浮点数默认类型。
变量名 | 说明 |
---|---|
float | 4字节,赋值时加后缀F,包装类为Float |
double | 8字节,包装类为Double |
浮点数的题外话
- 浮点数的内存存储方式
IEEE规定浮点数num = (-1)^S * M * 2^E,下面以32位的float类型为例:(double类型为1+11+52,精度为15-16位小数)
|-|--------|-----------------------|
S(1bit) E(8bit) M(23bit)
首先需要说明一下,任何数的科学计数法都可以表示为1.xxx*2^n(计算机中均以二进制存储),由于尾数部分第一位均以1开头,故在M中省略,即23位的尾数部分可以表示24bit精度,也就是小数点后6-7位,绝对保证的精度为6位。
S:符号位,0表示正数,1表示负数
E:指数位,可正可负,故第一位为符号位,实际范围为-128~127。采用偏移位存储,元数据为127(0111 1111表示0)
M:尾数位,实际数值为1.M,后面不足23为加0,注意小数转化为二进制的方法
举例:6.2的存储结构
整数部分:6的二进制为110
小数部分:0.2的二进制为0011 0011 0011 0011 0011…
这里的计算方法可以为: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是一个无限循环小数。
规格化:110.00110011… = 1.1000110011 * 2^2
填充:S = 0
E = 2 + 127 = 129 = 10000001
M = 10001100
故6.2的float存储结果为:|0|1000 0001|100 0110 0110 0110 0110 0110|
- BigDecimal的使用
构造 BigDecimal 对象的方法:
BigDecimal bigDecimal = new BigDecimal(long a); //不常用,不精确会变大
BigDecimal bigDecimal = new BigDecimal(String s);//常用
bigDecimal.valueOf(long a); //常用,是long转BigDecimal的方法
建议在使用 BigDecimal 的构造函数时最好采用基于整数或 String 的构造函数。
此外,BigDecimal 类型不能用使用一般的运算符号(±*/),需要使用对象的相应运算方法(如add())。
- Double中的两个特殊值
上面说到了浮点数的存储是不精确的,在 Double 类中就存在这样的两个数据:Double.NaN 和 Double.POSITIVE_INFINITY,Float 类同理。
3.1 i == i + 1
无穷大加1还是无穷大,在 Java 中如果你计算1/0
结果会抛出 ArithmeticException,但是计算1.0/0
结果会得到 Infinity,这是标准类库提供的常量。浮点数在计算时不会抛出异常。
3.2 i != i
IEEE 754 浮点算术保留了一个特殊的值用来表示一个不是数字的数量:NaN(Not a Number),用于表示没有良好的数字定义的浮点计算,如0.0/0
。任何浮点操作,只有它的一个或多个操作数为 NaN,其结果必然是 NaN,显然 NaNcy 与任何数比较结果均返回 false。但是如果使用 Double.compare() 函数来比较两个 NaN,则结果返回0(相等)。
这里有个神奇的现象,对于 0.0 与 -0.0,compare() 函数认为正0要比负0大。所以对于0的比较还是用 == 比较好。
- 字符型
特点:使用Unicode字符集(’\uxxx’)。
变量名 | 说明 |
---|---|
char | 2字节,包装类为Charac |
- 逻辑型
变量名 | 说明 |
---|---|
boolean | 1字节,包装类为Boolean |
1.1.2 引用数据类
- 类
- 接口
- 数组
1.2 以声明的位置为依据划分
1.2.1 成员变量
类中定义的变量,但是在方法、构造方法和语句块之外
- 实例变量:不以static修饰
- 类变量:以static修饰
1.2.2 局部变量
方法、构造方法和语句块中定义的变量
- 形参:方法签名中定义
- 方法局部变量:方法体内定义
- 代码块局部变量:代码块中定义
二、拆箱与装箱机制
Java 中一切皆对象,为了方便编程引入了基本数据类型,但是每个类型都引入了对应的包装类型,Java 5 开始引入了自动装箱/拆箱机制
,使得二者可以互相转换。但是注意 Ingeter 初值为 null,而 int 初值为 0。
注意:所有的包装类都是final类,即不可变类。虽然在代码A处看起来是改变了counter的值,但实际上是创建了另一个对象,并将方法内的counter参数的引用指向了这个新创建的对象,由于是不同的引用,所以不会对方法外的引用有任何的影响。
装箱:
//二者等价
Integer a = 123;
Integer a = Integer.valueOf(123);
拆箱:
Integer a = new Integer(123);
int b = a;
//等价于 b = a.intValue();
深入理解:
Integer a = new Integer(123);
Integer b = new Integer(123);
Integer c = 123;
Integer d = 123;
Integer e = 128;
Integer f = 128;
//逻辑表达式(a == b)为false,因为栈中的对象a、b指向不同的堆中对象
//逻辑表达式(a == c)为true,因为自动拆箱的原因,实际比较的是两个int型数值
//逻辑表达式(c == d)为true,因为自动装箱时IntegerCache类在初始化时,生成了一个-128-127的Integer类型的常量池,如果值在此范围内则不会再生成新的对象
//逻辑表达式(e == f)为falsed,理由同上
一个 String 的例子
这样就不难理解 String 不是基本数据类型,而是一个对象,但是需要注意的是 String 可以直接赋值创建,而 StringBuilder 必须通过 new 来创建。
为了更进一步了解 Java 中的 String 请先看以下代码:
final String c1 = "a";
String c2 = "a";
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
String s4 = new String("abc");
String s5 = c1 + "bc";
String s6 = c2 + "bc";
在JVM里,考虑到垃圾回收(Garbage Collection)的方便,将heap(堆)划分为三部分:young generation(新生代)、tenured generation (old generation)(旧生代)、permanent generation(永生代)。
字符串为了解决字符串重复问题,生命周期长,存于pergmen中。
- 逻辑表达式
s1 == s2
为 true
因为String s1 = "abc"
可能创建一个或不创建对象,如果 “abc” 这个字符串在 Java String 池中不存在,则会在 JVM 的字符串池中创建一个 String 对象 “abc”,然后将 s1 指向这个内存地址,以后在创建值为 “abc” 的字符串对象,始终只有一个内存地址被分配,其余的都是 String 的拷贝。
所以这里比较的是两个变量名实际指向的 String 对象地址。
Java 中成为“字符串驻留”
:所有的字符串常量都会在编译之后自动地驻留。
-
逻辑表达式
s3 == s4
为 false
因为String s3 = new String("abc")
创建一个或两个对象,由于 new 关键字的存在,会在堆中创建一个 String 类型的 s3 对象,它的值为 “abc”(创建与否同上)。
所以这里比较的是堆中两个 String 对象的地址。如果想要比较值,应使用s3.equals(s4)
,内部逐项进行比较。 -
逻辑表达式
s1 == s5
为 true,逻辑表达式s1 == s6
为 false
因为将一个字符串连接表达式赋给字符串变量时,如果这个字符串连接表达式的值可以在编译时就确定下来,那么 JVM 会在编译时确定字符串变量的值,并让它指向字符串池中对应的字符串。这里 final 关键字在编译时对 c1 进行了宏替换
,在编译时可以确定 s5 的值。
建议:在使用字符串、基本数据类型包装实例时,进行使用直接复制,而不是通过 new、包装类实例化,这样可以保证更好的性能。
三、相互间的转换
/* int与String的互转
*/
int i = 123;
String s1 = String.valueOf(i); //方法1
String s2 = Integer.toString(i);//方法2
String s = "123";
int i1 = Integer.parseInt(s);//方法1,返回的是int类型,常用(性能和Integer常量池范围限制问题)
int i2 = Integer.valueOf(s); //方法2,返回的是Integer类型,调用了parseInt的方法,如果要变为int型,还要进行一次装箱操作intValue()
/* StringBuilder与String的互转
*/
String s = "abc";
StringBuilder sb1 = new StringBuilder(s);//方法1
StringBuilder sb2 = new StringBuilder(); //方法2
sb2.append(s);
String s1 = sb.toString();//方法1
String s2 = "" + sb; //方法2
/* char[]与String的互转
*/
String str = "abc";
char[] ca = str.toCharArry();
char[] ca = {'a','b','c'};
String s1 = String.valueOf(ca); //方法1,这里的valueOf函数可以有三个参数,截取字符数组范围
String s2 = Arrays.toString(ca);//方法2,但是输出格式为[, , ,]
//char[]转StringBuilder只能利用循环硬转