数据类型与包装类
数据类型
Java 作为强类型语言,能够承载多种数据类型。这些属性是构建代码的基本单位,决定了变量能够存储何种类型的数据。Java 的数据类型分为两类——基本数据类型与引用数据类型:
基本数据类型。
基本数据类型是 Java 中最基础的数据存储单位,它们直接存储数据值,而不是引用。Java 提供了 8 种基本数据类型,分为四类:
- 整数类型
- byte
- 大小:1 字节(8 位)
- 范围:-128 到 127
- 用途:节省内存时使用,适合存储小范围的整数。
- short
- 大小:2 字节(16 位)
- 范围:-32,768 到 32,767
- 用途:比
byte
范围更大,但仍较为节省内存。
- int
- 大小:4 字节(32 位)
- 范围:-2^31 到 2^31-1
- 用途:最常用的整数类型,适合大多数场景。
- long
- 大小:8 字节(64 位)
- 范围:-2^63 到 2^63-1
- 用途:需要存储更大范围的整数时使用,需在数值后加
L
或l
标记。
- byte
- 浮点类型
- float
- 大小:4 字节(32 位)
- 范围:约 ±3.40282347E+38F
- 用途:节省内存时使用,适合存储小范围的浮点数,需在数值后加
F
或f
标记。
- double
- 大小:8 字节(64 位)
- 范围:约 ±1.79769313486231570E+308
- 用途:默认的浮点类型,适合大多数需要小数的场景。
- float
- 字符类型
- char
- 大小:2 字节(16 位)
- 范围:0 到 65,535(Unicode 字符)
- 用途:存储单个字符,用单引号
''
包裹。
- char
- 布尔类型
- boolean
- 大小:未明确定义(通常为 1 位)
- 范围:
true
或false
- 用途:表示逻辑真伪,用于条件判断。
- boolean
引用数据类型。
引用数据类型不直接存储数据值,而是存储数据的引用(即内存地址)。这些类型适合存储复杂的数据结构,如对象、数组、接口等。
包装类
包装类是什么
在 Java 中,每个基本数据类型都有一个对应的包装类。包装类是一个类,内部包含一个实例变量,用于存储对应的基本数据类型的值。此外,包装类还提供了一些静态方法、静态变量和实例方法,以便更方便地操作数据。
以下是基本数据类型及其对应的包装类:
基本类型 | 包装类 |
---|---|
byte | Byte |
short | Short |
int | Integer |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
char | Character |
为什么需要包装类
对象操作的需求
Java 中有很多代码(如容器类ArrayList
、HashMap
等)只能操作对象,而不能操作基本数据类型。为了能够使用这些代码,需要将基本数据类型转换为对应的包装类对象。提供更多功能
包装类提供了许多有用的方法,例如将字符串转换为数值、数值比较、进制转换等。这些方法可以方便地对数据进行操作。支持泛型
Java 的泛型(如List<T>
、Map<K, V>
)只能使用引用类型,不能使用基本数据类型。包装类使得基本数据类型可以用于泛型。
装箱与拆箱
为了在基本数据类型和包装类之间灵活转换,Java 引入了**装箱(Boxing)和拆箱(Unboxing)**的概念。
装箱:将基本类型转换为包装类的过程。
拆箱:将包装类型转换为基本类型的过程。
Java 5 以后引入了自动装箱和拆箱技术,可以直接将基本类型赋值给引用类型,反之亦可。
各个包装类都可以与其对应的基本类型相互转换,每种包装类都有一个静态方法 valueOf()
,接受基本类型,返回引用类型,也都有一个实例方法 xxxValue()
,返回对应的基本类型。
Boolean bObj = Boolean.valueOf(true); // 装箱
boolean b = bObj.booleanValue(); // 拆箱
Integer iObj = Integer.valueOf(10); // 装箱
int i = iObj.intValue(); // 拆箱
Double dObj = Double.valueOf(10.0); // 装箱
double d = dObj.doubleValue(); // 拆箱
Character cObj = Character.valueOf('a'); // 装箱
char c = cObj.charValue(); // 拆箱
Byte byObj = Byte.valueOf((byte) 10); // 装箱
byte by = byObj.byteValue(); // 拆箱
Float fObj = Float.valueOf(10.0f); // 装箱
float f = fObj.floatValue(); // 拆箱
Short sObj = Short.valueOf((short) 10); // 装箱
short s = sObj.shortValue(); // 拆箱
Long lObj = Long.valueOf(10L); // 装箱
long l = lObj.longValue(); // 拆箱
装箱和拆箱是如何实现的
Java 的自动装箱和拆箱是通过编译器在编译时插入相应的字节码指令来实现的。以下是一个简单的 Java 代码示例及其对应的字节码:
public class Main {
public static void main(String[] args) {
Integer i = 10; // 自动装箱
int n = i; // 自动拆箱
}
}
以Java 8为例,main方法对应的字节码如下:
0 bipush 10 // bipush 是 push byte 的缩写,用于将一个字节(-128 到 127)的常量值压入操作数栈中。
2 invokestatic #2 <java/lang/Integer.valueOf> // invokestatic 用于调用静态方法。
5 astore_1 // astore_1 是 store reference into local variable 1 的缩写, 用于将栈顶的 Integer 对象存储到局部变量表(Local Variable Table)的第 1 个位置(索引为 1)
6 aload_1 // aload_1 是 load reference from local variable 1 的缩写,从局部变量表的第 1 个位置加载 Integer 对象到操作数栈。
7 invokevirtual #3 <java/lang/Integer.intValue>
10 istore_2 // istore_2 是 store int into local variable 2 的缩写,将栈顶的 int 值存储到局部变量表的第 2 个位置(索引为 2)
11 return
可以看到,java的自动装箱与拆箱本质上正是使用了静态方法valueOf()
与实例方法xxxValue()
来实现的。
注意事项
- 空指针异常(NullPointerException)
自动拆箱时,如果包装类对象为null
,会抛出NullPointerException
。
Integer num = null;
int value = num; // 抛出 NullPointerException
性能开销
装箱和拆箱会引入额外的性能开销,尤其是在大量操作时(大量的创建包装类与调用方法)。应尽量避免在性能敏感的代码中频繁使用。缓存机制 (jvm篇会详解) Java 对部分包装类(如
Integer
、Long
)的常用值(-128 到 127)进行了缓存,以提高性能。
Integer a = 100;
Integer b = 100;
System.out.println(a == b); // true(缓存范围内的值)
Integer c = 200;
Integer d = 200;
System.out.println(c == d); // false(缓存范围外的值)
包装类的共同特点
各个包装类的共同点包括都重写了Object类中的方法,都实现了Comparable接口,都继承了Number抽象类,都是不可变的等。
所有的包装类都重写了 Object
类中的以下方法:
boolean equals(Object obj)
Object
类的默认实现是比较对象的地址(即引用是否相同),equals
应该反映的是对象间的逻辑相等关系,所以所有包装类都重写了该实现,实际比较用的是其包装的基本类型值Java 8 中
Integer
类的equals
方法实现:javapublic boolean equals(Object obj) { if (obj instanceof Integer) { return value == ((Integer)obj).intValue(); } return false; }
Float
类的equals
方法实现:javapublic boolean equals(Object obj) { return (obj instanceof Float) && (floatToIntBits(((Float)obj).value) == floatToIntBits(value)); }
作用:比较两个
Float
对象的二进制表示是否完全相同。 原因:由于浮点数计算存在精度问题,数学上相等的两个浮点数在计算机中的二进制表示可能不同。因此,Float
类的equals
方法通过floatToIntBits()
将float
的二进制表示转换为int
,再进行比较。Float f1 = 0.01f; Float f2 = 0.1f * 0.1f; System.out.println(f1.equals(f2)); // false System.out.println(Float.floatToIntBits(f1)); // 1008981770 System.out.println(Float.floatToIntBits(f2)); // 1008981771
int hashCode()
hashCode
方法返回一个对象的哈希值。哈希值是一个int
类型的数,由对象中一般不变的属性映射得来,用于快速对对象进行区分、分组等。哈希值的特点包括:- 一个对象的哈希值不能改变。
- 相同对象的哈希值必须一样。
- 不同对象的哈希值一般应不同,但这不是必需的,可以有对象不同但哈希值相同的情况。
hashCode
和equals
的关系- 规则 1:如果两个对象的
equals
方法返回true
,则它们的hashCode
必须相同。 - 规则 2:如果两个对象的
equals
方法返回false
,则它们的hashCode
可以相同,也可以不同,但应尽量不同。 - 默认实现:
Object
类的hashCode
默认实现是将对象的内存地址转换为整数。 - 重写要求:如果子类重写了
equals
方法,则必须同时重写hashCode
方法。这是因为 Java API 中的许多类(尤其是容器类)依赖于这一行为。
包装类都重写了
hashCode
方法,根据其包装的基本类型值计算哈希值。对于Byte
、Short
、Integer
和Character
,哈希值就是其内部值。以Byte类型为例,其代码为:
@Override public int hashCode() { return Byte.hashCode(value); } public static int hashCode(byte value) { return (int)value; }
对于Boolean类型,其hashCode代码为:
@Override public int hashCode() { return Boolean.hashCode(value); } /** * Returns a hash code for a {@code boolean} value; compatible with * {@code Boolean.hashCode()}. * * @param value the value to hash * @return a hash code value for a {@code boolean} value. * @since 1.8 */ public static int hashCode(boolean value) { return value ? 1231 : 1237; }
根据基类类型返回了两个不同的质数(即只能被1和自己整除的数),质数用于哈希时不易冲突。(质数在计算机中的作用可以看这篇质数)
对于Long类型,hashCode代码为:
@Override public int hashCode() { return Long.hashCode(value); } /** * Returns a hash code for a {@code long} value; compatible with * {@code Long.hashCode()}. * * @param value the value to hash * @return a hash code value for a {@code long} value. * @since 1.8 */ public static int hashCode(long value) { return (int)(value ^ (value >>> 32)); }
Long类型的哈希值是高32位和低32位进行异或操作后的结果。 通过将高 32 位和低 32 位进行异或操作,可以将
long
值的所有 64 位信息压缩到 32 位的哈希值中,使得哈希值的分布更加均匀,减少哈希冲突的概率。对于Float类型,hashCode代码为:
@Override public int hashCode() { return Float.hashCode(value); } /** * Returns a hash code for a {@code float} value; compatible with * {@code Float.hashCode()}. * * @param value the value to hash * @return a hash code value for a {@code float} value. * @since 1.8 */ public static int hashCode(float value) { return floatToIntBits(value); }
Float是将float的二进制表示看作int作为哈希值。
对于Double类型,hashCode代码为:
@Override public int hashCode() { return Double.hashCode(value); } /** * Returns a hash code for a {@code double} value; compatible with * {@code Double.hashCode()}. * * @param value the value to hash * @return a hash code value for a {@code double} value. * @since 1.8 */ public static int hashCode(double value) { long bits = doubleToLongBits(value); return (int)(bits ^ (bits >>> 32)); }
Double类型首相将double的二进制表示看作long,然后再按long计算hashCode.
String toString()
返回对象的字符串表示形式。Integer a = 10; System.out.println(a.toString()); // "10"
实现了
Comparable
接口所有的包装类都实现了
Comparable
接口,这意味着它们可以进行比较和排序。Comparable
接口定义了一个方法:- int compareTo(T o)
用于比较当前对象与另一个对象的大小。
接口只有一个方法
CompareTo
,当前对象与参数对象进行比较,在小于、等于、大于参数时,应分别返回-1、0、1.各个包装类的实现基本都是根据其基本类型值进行比较,对于Boolean,false小于true。对于Float和Double,存在和equals方法一样的问题,0.010.010.01和0.1∗0.10.1*0.10.1∗0.1相比的结果并不为0.
通过实现
Comparable
接口,包装类可以直接用于排序(如Collections.sort()
)和比较操作。- int compareTo(T o)
继承了
Number
抽象类除了
Boolean
和Character
,其他包装类(如Integer
、Double
、Float
等)都继承了Number
抽象类。Number
类定义了一些用于将数值转换为基本数据类型的方法:javapublic abstract class Number implements java.io.Serializable { public abstract int intValue(); public abstract long longValue(); public abstract float floatValue(); public abstract double doubleValue(); public byte byteValue() { return (byte)intValue(); } public short shortValue() { return (short)intValue(); } /** use serialVersionUID from JDK 1.0.2 for interoperability */ private static final long serialVersionUID = -8742448824652078965L; }
通过继承
Number
类,包装类可以方便地将数值转换为不同的基本数据类型。
不可变性(Immutable)
所有的包装类都是不可变的(Immutable),这意味着一旦创建了一个包装类对象,它的值就不能被修改。如果需要修改值,必须创建一个新的对象。
- 所有包装类都声明为了
final
,不能被继承 - 内部基本类型值是私有的,且声明为了
final
。 - 没有定义
setter
方法
Integer a = 10; a = a + 5; // 创建了一个新的 Integer 对象,值为 15
不可变性的优点:
- 线程安全:不可变对象可以在多线程环境中安全地共享,无需额外的同步机制。
- 简化设计:不可变对象的行为更可预测,减少了出错的可能性。
- 缓存支持:由于不可变性,Java 可以对包装类的常用值(如
Integer
的 -128 到 127)进行缓存,提高性能。
- 所有包装类都声明为了
静态工厂方法
valueOf()
所有包装类都提供了静态方法valueOf()
,用于将基本类型或字符串转换为包装类对象。javaInteger a = Integer.valueOf(10); Integer b = Integer.valueOf("10");
常量池缓存
部分包装类(如Integer
、Long
)对常用值(-128 到 127)进行了缓存,以减少对象的创建开销。javaInteger a = 100; Integer b = 100; System.out.println(a == b); // true(缓存范围内的值)
在Java 8中
Integer
的valueOf()
方法的具体实现如下:java// IntegerCache.low = -128 // IntegerCache.high 可通过JVM参数 -XX:AutoBoxCacheMax=<size> 设置,必须大于等于127,默认为127 public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }
其中
IntegerCache
是一个私有静态内部类,其实现如下:javaprivate static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property 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[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} }
IntegerCache
表示Integer
缓存,其中的cache
变量是一个静态Integer
数组,在静态初始化块中被初始化,默认情况下,保存了-128~127
共256个整数对应的Integer
对象。在
valueOf
代码中,如果数值位于被缓存的范围,则直接从IntegerCache
中获取已预先创建的Integer
对象,只有不在缓存范围时,才通过new
创建对象。通过共享常用对象,可以节省内存空间,由于
Integer
是不可变的,所以缓存的对象可以安全的被共享。Boolean、Byte、Short、Long、Character
都有类似的实现。这种共享常用对象的思路,叫享元模式,英文叫Flyweight,即共享的轻量级元素。
类型转换方法
包装类提供了将字符串转换为基本类型的方法(如Integer.parseInt()
、Double.parseDouble()
)。javaint i = Integer.parseInt("10"); double d = Double.parseDouble("10.5");
自动拆装箱的触发
示例:
public 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);
System.out.println(e==f);
System.out.println(c==(a+b));
System.out.println(c.equals(a+b));
System.out.println(g==(a+b));
System.out.println(g.equals(a+b));
System.out.println(g.equals(a+h));
}
}
output:
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
大家一定对System.out.println(c==(a+b))
这段代码的结果有很大疑惑,System.out.println(c==(a+b))
对应的字节码如下:
91 aload_3 // 将c压入操作数栈
92 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
95 aload_1 // 将a压入操作数栈
96 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
99 aload_2 // 将b压入操作数栈
100 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
103 iadd // 计算a+b的值并将结果压入操作数栈
104 if_icmpne 111 (+7) // 比较 c 和 a+b 的值
107 iconst_1
108 goto 112 (+4)
111 iconst_0
112 invokevirtual #9 <java/io/PrintStream.println>
115 getstatic #8 <java/lang/System.out>
当==
运算符的两个操作数都是包装器类型的引用时,则比较的是引用地址,而如果其中有一个操作数是表达式(即包含算术运算符)则比较的是数值(即会触发自动拆箱过程),从对应的字节码也可以看出调用了intValue()
方法,触发了自动拆箱,比较它们的数值是否相等,因此c==(a+b)
的结果为true
。
System.out.println(c.equals(a+b))
对应的字节码如下:
118 aload_3 // 将c压入操作数栈
119 aload_1 // 将a压入操作数栈
120 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
123 aload_2 // 将b压入操作数栈
124 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
127 iadd // 计算a+b的值并将结果压入操作数栈
128 invokestatic #2 <java/lang/Integer.valueOf> // 将a+b的值的自动装箱
131 invokevirtual #11 <java/lang/Integer.equals> // 调用equals方法
134 invokevirtual #9 <java/io/PrintStream.println>
137 getstatic #8 <java/lang/System.out>
从字节码可以看出,c.equals(a+b)
首先触发自动拆箱,又触发了自动装箱,再调用equals
方法,而所有的包装类都重写了Object类中的equals
方法,equals用于判断当前对象和参数传入的对象是否相同,Object类的默认实现是比较地址,它和比较运算符(==
)的结果是一样的。
equals
应该反映的是对象间的逻辑相等关系,所以包装类都重写了该实现,实际比较用的是其包装的基本类型值,对于Integer
类,其equals
方法代码如下(Java 8):
public boolean equals(Object obj) {
if (obj instanceof Integer) {
return value == ((Integer)obj).intValue();
}
return false;
}
因此,c.equals(a+b)
的结果为true
.
System.out.println(g==(a+b))
对应的字节码如下:
140 aload 7 // 将g压入操作数栈
142 invokevirtual #12 <java/lang/Long.longValue> // 自动拆箱
145 aload_1 // 将a压入操作数栈
146 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
149 aload_2 // 将b压入操作数栈
150 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
153 iadd // 计算a+b的值并将结果压入操作数栈
154 i2l // 将 a+b 的结果从 int 转换为 long
155 lcmp // 比较 g 和 a+b 的值
156 ifne 163 (+7)
159 iconst_1
160 goto 164 (+4)
163 iconst_0
164 invokevirtual #9 <java/io/PrintStream.println>
167 getstatic #8 <java/lang/System.out>
g==(a+b)
的比较过程和c==(a+b)
类似,多了一个类型转换,都是比较数值,因此结果为true
.
System.out.println(g.equals(a+b))
对应的字节码如下:
170 aload 7 // 将g压入操作数栈
172 aload_1 // 将a压入操作数栈
173 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
176 aload_2 // 将b压入操作数栈
177 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
180 iadd // 计算a+b的值并将结果压入操作数栈
181 invokestatic #2 <java/lang/Integer.valueOf> // 自动装箱
184 invokevirtual #13 <java/lang/Long.equals> // 调用equals比较
187 invokevirtual #9 <java/io/PrintStream.println>
190 getstatic #8 <java/lang/System.out>
对于Long类,其equals
方法代码如下(Java 8):
public boolean equals(Object obj) {
if (obj instanceof Long) {
return value == ((Long)obj).longValue();
}
return false;
}
从字节码可以看出,g.equals(a+b)
也进行了自动拆箱、装箱过程,但是a+b
装箱后为Integer
类型,而g
是Long
类型,因此调用equasl
会输出false
.
System.out.println(g.equals(a+h))
对应的字节码如下:
193 aload 7 // 将g压入操作数栈
195 aload_1 // 将a压入操作数栈
196 invokevirtual #10 <java/lang/Integer.intValue> // 自动拆箱
199 i2l // 从a的数值从 int 转换成 long
200 aload 8 // 将h压入操作数栈
202 invokevirtual #12 <java/lang/Long.longValue> // 自动拆箱
205 ladd // 计算a+h的值并将结果压入操作数栈
206 invokestatic #5 <java/lang/Long.valueOf> // 自动装箱
209 invokevirtual #13 <java/lang/Long.equals> // 调用equals比较
212 invokevirtual #9 <java/io/PrintStream.println>
g.equals(a+h)
相比于g.equals(a+b)
多了一步类型转换,a+h
装箱后的类型为Long
,因此结果为true
.