深入理解Java原生类型与包装器类型
摘要:
本文对 Java 原生类型与包装器类型进行深度剖析,主要涉及以下四个方面:原生类型与包装器类型基础、字面值概念和种类、 基本类型的自动转型与强制转型和自动装箱与拆箱机制。
要点:
原生类型与包装器类型
字面值概念和种类
基本类型、自动转型与强制转型
自动装箱与拆箱机制(Autoboxing and Unboxing)
一. 原生类型与包装器类型
在Java中有八种基本数据类型:
特别需要注意以下几点:
未带有字符后缀标识的整数默认为 int 类型,未带有字符后缀标识的浮点数默认为 double 类型;
只有 byte,char,short,int 四种基本类型及其包装类(需要Java5.0/1.5以上版本支持)可以用于 switch 语句(只有 enum 类型 和 int类型可以应用到 swith),其它类型编译会报错;
char(两个字节)可以用单引号表示单个字符(可以是汉字),如:‘良’,’A’;
整数可以由二进制(以0B/0b开头)、八进制(以0开头的整数)、十进制、十六进制(以0x或0X开头的整数)表示;
二. 字面值
在Java源代码中,字面值用于表示固定的值(fixed value)。数值型的字面值是最常见的,字符串字面值也很常见可以算是一种,当然也可以把特殊的null当做字面值。字面值大体上可以分为 整型字面值(int型字面值 、 long型字面值 和 字符字面值)、整型字面值、浮点型字面值(double型字面值 和 float型字面值)、字符串字面值、特殊字面值 四种。
(1) 整型字面值
从形式上看是整数的值归类为整型字面值。例如: 10, 100000L, ‘B’‘B’、0XFF 这些都可以称为整型字面值。在使用时,需要注意以下几点:
整型字面值可以用 十进制、十六进制(以0X/0x开头)、八进制(以0开头) 和 二进制(以0B/0b开头) 来表示。当然,基数不能超出进制的范围,比如09是不合法的,八进制的基数只能到 7。
一般情况下,字面值创建的是int类型,但是int字面值可以赋值给 byte、short、char、long 和 int。当然,前提是字面值在目标范围以内(Java会自动完成转换);如果试图将超出范围的字面值赋给某一类型(比如把128赋给byte类型),编译通不过,需要强制类型转换。
byte b0 = 128; // Type mismatch: cannot convert from int to bytebyte b1 = 12; // OKbyte b2 = (byte)300; // OK 需要强制转换,但表示的数字是截取后的数字(300二进制为100101100,截取后为00101100,即44)char c0 = 5; // OKchar c1 = char(-3); // 编译通过,但打印值为 ?(char 为无符号的)System.out.println(c1); // ?
如果想创建一个 int 类型无法表示的 long 类型,则需要在字面值最后面加上L或者l(通常建议使用容易区分的L)。所以,整型字面值包括int字面值和long字面值两种。
Java中,字符字面值用单引号括起来(也属于整型字面值),如‘@’,‘1’。所有的UTF-16字符集都包含在字符字面值中。不能直接输入的字符,可以使用转义字符,如‘\n’为换行字符。也可以使用八进制或者十六进制表示字符,八进制使用反斜杠加3位数字表示,例如’\141’表示字母a。十六进制使用\u加上4为十六进制的数表示,如’\u0061’表示字符a。也就是说,通过使用转义字符,可以表示键盘上的有的或者没有的所有字符。常见的转义字符序列有:
\ddd(八进制) 、 \uxxxx(十六进制Unicode字符)、\’(单引号)、\”(双引号)、\ (反斜杠)\r(回车符) \n(换行符) \f(换页符) \t(制表符) \b(回格符)
(2) 浮点字面值
浮点字面值简单的理解可以理解为小数,分为float字面值和double字面值。如果在小数后面加上F或者f,则表示这是个float字面值,如11.8F。如果小数后面不加F/f(如10.4),或者小数后面加上D/d,则表示这是个double字面值。
double d1 = 10; // OK,自动类型转换double d2 = 11.4; // OKdouble d3 = 1.23E3; // OK double d4 = 10D; // OK double d5 = 0.4D; // OK float f1 = 10; // OK,自动类型转换float f2 = 11.1F; // OKfloat f3 = 11.1; // Type mismatch: cannot convert from double to float
(3) 字符及字符串字面值
Java中字符字面值用单引号括起来,如‘@’,‘1’。所有的UTF-16字符集都包含在字符字面值中。不能直接输入的字符,可以使用转义字符,如‘\n’为换行字符。也可以使用八进制或者十六进制表示字符,八进制使用反斜杠加3位数字表示,例如’\141’表示字母a。十六进制使用\u加上4为十六进制的数表示,如’\u0061’表示字符a。也就是说,通过使用转义字符,可以表示键盘上的有的或者没有的所有字符。常见的转义字符序列有:
\ddd(八进制) 、 \uxxxx(十六进制Unicode字符)、\’(单引号)、\”(双引号)、\ (反斜杠)\r(回车符) \n(换行符) \f(换页符) \t(制表符) \b(回格符)
字符串字面值则使用双引号,字符串字面值中同样可以包含字符字面值中的转义字符序列。
(4) 特殊字面值
null 是一种特殊的类型(type),可以将它赋给任何引用类型变量,表示这个变量不引用任何东西。如果一个引用类型变量为null,表示这个变量不可用。
还有一种特殊的 class literal,用 type name 加上 .class 表示,例如 String.class。 首先,String是类Class(java.lang.Class)的一个实例(对象),而”This is a string”是类String的一个对象。然后,class literal用于表示类Class的一个对象,比如String.class用于表示类Class的对象String。简单地说,类字面值(class literal)就是诸如String.class 、Integer.class这样的字面值,它所表示的就是类String、类Integer。如果输出Integer.class,你会得到class java.lang.Integer。List.class的输出为interface java.util.List。总之,class literal 用于表示类型本身。
特别需要注意:
在 在数值型字面值 中可以 中可以使用下划线(JDK7开始,可以在数值型字面值(包括整型字面值和浮点字面值)插入一个或者多个下划线。但是下划线只能用于分隔数字下划线只能用于分隔数字,不能分隔字符与字符,也不能分隔字符与数字);
int x = 123_456_789; // 在编译的时候,下划线会自动去掉。float f = 1.22_3344; //可以连续使用下划线int = _123; // Errorlong = 1_L; // Error
三. 自动转型与强制转型
1、 自动转型
自动转型总原则:byte,short,char(同级)-> int -> long -> float -> double (由低精度到高精度)
(1) 由低精度到高精度的自动转换
具体可分为以下两种情形:
从位数低的类型向位数高的类型转换
byte b = 1; char c = 1; short s = 1; int i = 1; c = b; // Error,同级 c = s; // Error,同级 s = c; // Error,同级 i = c; // OK
从整型向浮点型的转换———-
从整型向浮点型的转换
int i = 1; long t = 1; float f = 1; double d = 1; f = i; // Ok f = t; // Ok d = f; // Ok
(2) 运算符对基本类型的影响
具体可分为以下两种情形:
1) 当使用 +、-、*、/、%、==、>、< 等 等运算符对基本类型进行运算时,遵循如下规则:
两个操作数中,先考虑是否有一个是double类型的。如果有,另一个操作数和结果 将会被转换成double类型。再依次考虑float,long。除此之外,两个操作数(包括byte、short、int、char)都将会被转换成int类型。
byte b1 = 10 ; //OK,会检查发现10没有超过byte类型的最大值byte b2 = 12; //OK,会检查发现12没有超过byte类型的最大值byte b = b1 + b2; //Error,byte类型在计算时会自动提升为int类型,此时就会报错,因为b1+b2实际上是int类型,但是左侧的变量为byte类型。short s1=1; //OK,会检查发现1没有超过short类型的最大值s1=s1+1; //Error,因为s1+1 结果int,但左侧变量为 short,报错s1++; //OK,不会报错,与s1=s1+1不同!!!,会检查发现2没有超过short类型的最大值s1=1+1; //OK,1+1 是个编译时可以确定的常量,'+'运算在编译时就被执行了,而不是在程序执行的时候,这个语句的效果等同于s1=2
2) 当使用 +=、-=、*=、/=、%= 、 i++、 ++i 运算符对基本类型进行运算时,遵循如下规则:
运算符右边的数值将首先被强制转换成与运算符左边数值相同的类型,然后再执行运算,且运算结果与运算符左边数值类型相同。自增(减)运算也类似。
short s1=1; // OK,会检查发现1没有超过short类型的最大值short s2; s1+=1; // OK,正确,1首先被强制转换为short型,然后再参与运算,并且结果也是short类型s2 = ++s1; // OK,正确,s2的值为2
2、强制转型
强制转换的格式是在需要转型的数据前加上 “( )”, 然后在括号内加入需要转化的数据类型。主要发生于以下两种情形:
由高精度向低精度转换
一种类型到另一种类型转换,则必须使用强制类型转化(同级之间:byte,short,char)
byte b = 3; int i = 3; long t = 3; float f = 3; char c = 3; short s = 3; i = (int) f; // OK,由高精度向低精度转换 t = (long) f; // OK,由高精度向低精度转换 b = (byte) i; // OK,由高精度向低精度转换 i = b; // OK,由低精度向高精度转换,自动转型 System.out.println(c==s); // OK,true,c 和 s 自动转型为int,然后比较 b = (byte) s; // OK,一种类型到另一种类型转换 c = (char) b; // OK,一种类型到另一种类型转换 c = (char) s; // OK,一种类型到另一种类型转换
特别需要注意的是,强制转换常常会导致二进制位的截取,甚至会导致意想不到的结果:
int i = 128; byte b = (byte)i; System.out.println(b); // -128(即-0)
四. 自动装箱与拆箱(Autoboxing and Unboxing)
1、什么是装箱?什么是拆箱?
Java为每种基本数据类型都提供了对应的包装器类型。在 Java SE5 之前,如果要 创建一个数值为10的Integer对象,必须这样进行:
Integer i = new Integer(10);
而从 Java SE5 之后就提供了自动装箱的特性,如果要 创建一个数值为10的Integer对象,只需要这样就可以了:
Integer i = 10;
这个过程中会自动根据数值创建对应的 Integer对象,这就是装箱。
那什么是拆箱呢?顾名思义,跟装箱对应,就是自动将包装器类型转换为基本数据类型:
Integer i = 10; //装箱int n = i; //拆箱
简单一点说,装箱就是自动将基本数据类型转换为包装器类型;拆箱就是自动将包装器类型转换为基本数据类型。
2、装箱和拆箱是如何实现的
上一小节了解装箱的基本概念之后,这一小节来了解一下装箱和拆箱是如何实现的。我们就以Interger类为例,下面看一段代码:
public class Main { public static void main(String[] args) { Integer i = 10; int n = i; } }
反编译class文件之后得到如下内容:
从反编译得到的字节码内容可以看出,在装箱的时候自动调用的是Integer的valueOf(int)方法。而在拆箱的时候自动调用的是Integer的intValue方法。
对于其他的包装器类,比如Double、Character,也同样适用。
因此,可以用一句话总结装箱和拆箱的实现过程:
装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的(xxx代表对应的基本数据类型)。
3、valueOf、xxxValue 方法在JDK中的实现
(1) 在 Byte,Character,Short,Integer,Long 中的实现(以 Integer 为例)
public static Integer valueOf(int i) : 类方法
public static Integer valueOf(int i) { if(i >= -128 && i <= IntegerCache.high) return IntegerCache.cache[i + 128]; else return new Integer(i); } private static class IntegerCache { static final int high; static final Integer cache[]; static { // 静态代码块 final int low = -128; // high value may be configured by property int h = 127; if (integerCacheHighPropValue != null) { // Use Long.decode here to avoid invoking methods that // require Integer's autoboxing cache to be initialized int i = Long.decode(integerCacheHighPropValue).intValue(); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - -low); } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) // 初始化 cache[k] = new Integer(j++); } private IntegerCache() {} }
在装箱时,valueOf方法会被自动调用:如果整型字面值在[-128,127]之间,便返回 IntegerCache.cache(在类加载时就自动创建) 中已经存在的对象的引用;否则,创建一个新的Integer对象并返回。
public int intValue():实例方法
public int intValue() { return value; }
从这段代码可以看出,在拆箱时,Integer对象会自动调用其 intValue 方法,返回该对象对应的 int 值。
下面代码可以很好说明这一点:
public class Main { public static void main(String[] args) { Integer i1 = 100; Integer i2 = 100; Integer i3 = 200; Integer i4 = 200; System.out.println(i1==i2); // true System.out.println(i3==i4); // false } }
(2) 在 Float, Double 中的实现(以 Double 为例)
public static Double valueOf(double d) : 类方法
public static Double valueOf(double d) { return new Double(d); }
在装箱时,valueOf方法会被自动调用,从而创建相应的Double对象并返回。
public int intValue():实例方法
public double doubleValue() { return value; }
从这段代码可以看出,在拆箱时,Double对象会自动调用其 doubleValue 方法,返回该对象对应的 double 值。
下面代码可以很好说明这一点:
public class Main { public static void main(String[] args) { Double i1 = 100.0; Double i2 = 100.0; Double i3 = 200.0; Double i4 = 200.0; System.out.println(i1==i2); // false System.out.println(i3==i4); // false } }
为什么Double类的valueOf方法会采用与Integer类的valueOf方法不同的实现呢?原因很简单,在某个范围内的整型数值的个数是有限的,而浮点数却不是。
(3) 在 Boolean 中的实现
public static Boolean valueOf(boolean b) : 类方法
public static final Boolean TRUE = new Boolean(true); public static final Boolean FALSE = new Boolean(false); public static Boolean valueOf(boolean b) { return (b ? TRUE : FALSE); }
在装箱时,valueOf方法会被自动调用,从而创建相应的Boolean对象并返回。
public boolean booleanValue():实例方法
public boolean booleanValue() { return value; } }
从这段代码可以看出,在拆箱时,Boolean对象会自动调用其 booleanValue 方法,返回该对象对应的 boolean 值。
下面代码可以很好说明这一点:
public class Main { public static void main(String[] args) { Boolean i1 = false; Boolean i2 = false; Boolean i3 = true; Boolean i4 = true; System.out.println(i1==i2); // true System.out.println(i3==i4); // true } }
总之,
Integer、Short、Byte、Character、Long 这几个类的valueOf方法的实现是类似的,有限可列举,共享[-128,127];
Double、Float的valueOf方法的实现是类似的,无限不可列举,不共享;
Boolean的valueOf方法的实现不同于以上的整型和浮点型,只有两个值,有限可列举,共享;
4、Integer i = new Integer(xxx) 和 Integer i =xxx;的区别
第一种方式不会触发自动装箱的过程,而第二种方式会触发;
在执行效率和资源占用上的区别。第二种方式的执行效率和资源占用在一般性情况下([-128,127])要优于第一种情况(注意这并不是绝对的)。
5、“==”运算符
当使用“==”运算符在基本类型和其包装类对象之间比较时,涉及到自动装箱、拆箱机制,遵循如下规则:
1). 只要两个操作数中有一个是基本类型或表达式(即包含算术运算符),就是比较它们的数值是否相等。
2). 否则,就是判断这两个对象的内存地址是否相等,即是否是同一个对象。
// 代码片段1public class Main { public static void main(String[] args) { Integer i01=59; int i02=59; Integer i03=Integer.valueOf(59); Integer i04=new Integer(59); System.out.println(i01==i02); // true,拆箱 System.out.println(i01==i03); // true,同一对象 System.out.println(i03==i04); // false,不同对象 System.out.println(i02==i04); // true,拆箱 } }
// 代码片段2public class Main { public static void main(String[] args) { Integer a = 1; Integer b = 2; Integer c = 3; Integer d = 3; Integer e = 321; Integer f = 321; Long g = 3L; Long h = 2L; System.out.println(c==d); // true System.out.println(e==f); // false System.out.println(c==(a+b)); // true System.out.println(c.equals(a+b)); // true System.out.println(g==(a+b)); // true System.out.println(g.equals(a+b)); // false System.out.println(g.equals(a+h)); // true } }
第一个和第二个输出结果没有什么疑问;
第三个打印语句由于 a+b 包含了算术运算,因此会触发自动拆箱过程(会调用intValue方法),因此它们比较的是数值是否相等;
对于c.equals(a+b)会先触发自动拆箱过程,再触发自动装箱过程,也就是说a+b,会先各自调用intValue方法,得到了加法运算后的数值之后,便调用Integer.valueOf方法,再进行equals比较;
对于g==(a+b),会分别触发 Integer 和 Long 的自动拆箱过程,然后 int 自动转为 long,进行比较;
对于g.equals(a+b),最终会归结于 Long对象与Integer对象的比较,由于二者不为同一类型,直接返回 false ;
对于g.equals(a+h),最终会归结于 Long对象与Long对象的比较,由于 -128 <= 3 <= 127, 二者为同一对象,直接返回 true 。
6、小结
使用“==”运算符在基本类型和其包装类对象之间比较时,只要两个操作数中有一个是 基本类型 或 表达式(即包含算术运算符),就是比较它们的数值是否相等。否则,就是判断这两个对象的内存地址是否相等,即是否是同一个对象。
-
如果一个 方法中参数类型为原生数据类型 ,所传入的参数类型为其封装类对象,则会自动对其进行 拆箱 ;相应地,如果一个方法中 参数类型为封装类型对象 ,所传入的参数类型为其原始数据类型,则会自动对其进行 装箱 ,例如上述的equals方法。
至于什么时候装箱,什么时候拆箱主要取决于:在当前场景下,你需要的是引用类型还是原生类型。若需要引用类型,但传进来的值是原生类型,则自动装箱(例如,使用equals方法时传进来原生类型的值);若需要的是原生类型,但传进来的值是引用类型,则自动拆箱(例如,使用运算符进行运算时,操作数是包装类型)。
以上就是深入理解Java原生类型与包装器类型的详细内容,更多请关注其它相关文章!