Java难绷知识03——包装器类及其自动装箱和拆箱
本篇文章和之前的倾向稍微有些不同,这篇文章我不仅要讨论一些容易头疼的细节,而且我打算尝试讨论一下如何理解Java中的包装类以及自动拆箱和自动装箱
自动装箱(Autoboxing)和自动拆箱(Unboxing)是在基本数据类型和它们对应的包装类之间“转换”的一个包装过程,其中
装箱:基本数据类型包装成对应的包装类
拆箱:包装类拆包装成基本数据类型
自动拆装箱下,上述转换在代码中是隐式的,由编译器自动完成。
为什么Java要引入包装类,来包装起来数据类型
其实很简单,原因就是因为:Java的面向对象语言,一切面向对象
为了让基本类型也具有对象的特征,Java引入了包装器类,使得它具有了对象的性质
统一数据类型处理
基本数据类型不是对象,无法使用对象的特性,包装类将基本数据类型包装成对象,使其能够融入面向对象的编程体系
在集合框架中,如ArrayList、HashMap等,它们只能存储对象类型。如果要将基本数据类型存储到这些集合中,就需要使用对应的包装类。
使其支持多态
包装类使得基本数据类型也能参与多态的实现。通过向上转型,不同的包装类对象可以被统一处理。
例如,所有的包装类都继承自Number类(Boolean除外),可以在需要Number类型的地方使用Integer、Double等包装类对象,来满足Number的特别支持,也就是满足多态
使其支持泛型和反射机制
支持泛型
在泛型代码中,类型参数必须是引用类型,不能是基本数据类型。这与泛型的实现原理有关,在编译后,泛型类型信息会被擦除,替换为其限定的类型,所以基本数据类型无法直接参与这种类型擦除机制。引入了包装器类
用ArrayList存储整数时使用泛型作为例子
import java.util.ArrayList;
import java.util.List;
public class GenericWithWrapper {
public static void main(String[] args) {
// 使用Integer包装类在泛型中存储整数
List<Integer> intList = new ArrayList<>();
intList.add(10);
int num = intList.get(0); // 自动拆箱
}
}
有兴趣可以试试String丢进去会咋样瞬间爆炸
支持反射
反射允许程序在运行时获取和操作类的信息
那你基本数据类型就没法支持了,众所周知反射在java里面有多有用,所以引入了包装器类
包装类为基本数据类型提供了对应的类对象,使得可以通过反射操作基本数据类型
包装类在反射机制中为基本数据类型提供对象层面的操作能力
通过Class.forName("java.lang.Integer")获取Integer包装类的Class对象,然后利用反射机制调用其构造函数创建Integer对象,同样,也可以通过反射调用包装类的方法。
public class ReflectionInWrapper {
public static void main(String[] args) {
try {
// 获取Integer类的Class对象
Class<?> wrapperClass = Class.forName("java.lang.Integer");
// 通过反射调用构造函数创建对象
Object instance = wrapperClass.getConstructor(int.class).newInstance(10);
System.out.println(instance);
} catch (Exception e) {
e.printStackTrace();
}
}
}
基本数据类型及其细节
为什么还要重新讲一下8 种基本数据类型,很简单,因为他们是Java语言的基础,并且在自动装箱(autoboxing)和自动拆箱(unboxing)机制中扮演着关键角色(毕竟进行的是基本数据类型和引用数据类型的“转换”)
不厌其烦的八种基本数据类型
Java是一种强类型语言,第一次变量赋值称为变量的初始化
8 种基本数据类型可以分类为如下三类:
字符类型 char
布尔类型 boolean
数值类型 byte、short、int、long、float、double
基本数据类型 | 所占字节数(大小) | 备注 |
---|---|---|
byte | 1字节 | 表示范围 -128 到 127 |
short | 2字节 | 表示范围 -32,768 到 32,767 |
int | 4字节 | 范围是-2,147,483,648 (-2^31)到2,147,483,647 (2^31-1) |
long | 8字节 | 范围为-9,223,372,036,854,775,808 (-2^63)到9,223,372,036, 854,775,807 (2^63-1) |
float | 4字节 | 大约 7 位有效数字 |
double | 8字节 | 大约 15 - 17 位有效数字 |
char | 2字节 | 采用 Unicode 编码 |
boolean | 通常占用 1 位 |
类型转换问题(向上和向下取型)
为什么上面我还要列个表格,就是要注意,在进行自动拆装箱和类型转换时,要注意数据的范围和精度问题,可能会隐藏一些类型转换错误。
在 Java 的基本数据类型中,类型转换分为自动类型转换(向上转型)和强制类型转换(向下转型)。
自动类型转换(向上转型):当把一个取值范围小的类型赋值给取值范围大的类型时,会自动进行转换。
强制类型转换(向下转型):当把一个取值范围大的类型赋值给取值范围小的类型时,需要进行强制类型转换,这可能会导致数据丢失。
当基本数据类型自动装箱为包装器类时,也遵循自动类型转换的规则,转换的是包装器类所继承的类,例如,byte 装箱为 Byte,Byte 可以自动向上转型为 Number(因为 Byte 继承自 Number)。
当从包装器类自动拆箱为基本数据类型时,如果要进行向下转型,同样需要强制类型转换。
跨类型的包装器转换:对于数值类型的包装器类,有时需要进行跨类型的转换。例如,将 Integer 转换为 Double。这需要先拆箱再装箱。
Integer intValue = 10;
// 先拆箱为int,再装箱为Double
Double doubleValueFromInt = new Double(intValue);
boolean 类型及其包装类 Boolean 与其他基本数据类型和包装类之间不存在类型转换关系。boolean 类型只有 true 和 false 两个值,不能转换为数值类型或其他类型。
char 类型及其包装类 Character 可以与数值类型进行一些转换。
char 本质上是一个无符号的 16 位整数,所以 char 可以自动转换为 int 类型。
Character charValue = 'A';
int intValueFromChar = charValue; // 自动装箱后,Character可自动转换为int
有关溢出
在基本数据类型下,进行同类型数值运算的时候溢出并不会抛异常,也没有任何提示,需要注意
包装器类下溢出的情况代码
public class WrapperOverflowExample {
public static void main(String[] args) {
Integer maxInt = Integer.MAX_VALUE;
// 尝试增加1
Integer result = maxInt + 1;
System.out.println("运算结果: " + result);
}
}
以上例而言,Integer.MAX_VALUE 是 int 类型能表示的最大值。当对 maxInt 加 1 时,会发生溢出,结果变为 Integer.MIN_VALUE,这和直接使用 int 基本数据类型进行运算溢出的情况一致。
所以处理极大数的时候,我们偏向使用 BigInteger 和 BigDecimal 类
Java中的数值类型不存在无符号的,它们的取值范围是固定的
伏笔
实际上,Java中还存在另一种基本类型void,它也有对应的包装类java.lang.Void,不过他很特殊,我们无法直接对它们进行操作,这个在下面我会特意说
基本数据类型及其包装类
八种基本数据类型都分别都有对应的包装类,如下表
基本数据类型 | 包装类 | 缓存值范围 |
---|---|---|
boolean | java.lang.Boolean | true和false |
byte | java.lang.Byte | -128~127 |
char | java.lang.Character | 0 ~ 127 |
float | java.lang.Float | 没有缓存 |
int | java.lang.Integer | -128~127 |
long | java.lang.Long | -128~127 |
short | java.lang.Short | -128~127 |
double | java.lang.Double | 没有缓存 |
有关记忆:在这八个类名中,除了Integer和Character类以后,其它六个类的类名和基本数据类型一致,只是类名的第一个字母大写即可。
包装类的方法与常量
首先,使用和声明包装器类需要实例化,因为包装器类对象,需要进行实例化,才能对变量数据进行处理。
包装类提供了丰富的方法和常量方便对基本数据类型进行操作
方法
构造方法(在 Java 9 及之后不推荐使用)
Integer(int value)
Integer i = new Integer(1000);
因为我们更多使用静态工厂方法:(也就是valueOf进行装箱)
例如
valueOf(byte b):返回一个表示指定 byte 值的 Byte 实例。例如:Byte byteObj = Byte.valueOf((byte)5);
valueOf(String s):返回表示字符串指定值的相应包装类实例,其值由字符串参数解析得到
例如:Integer intFromString = Integer.valueOf("123");,但字符串必须是合法的数值表示,否则会抛出 NumberFormatException。
解析方法:
parseXxx()
该方法用于将字符串解析为对应的基本数据类型。字符串必须是合法的数值表示形式,否则会抛出 NumberFormatException
int num = Integer.parseInt("123");
double d = Double.parseDouble("3.14");
特殊的一点,在parseBoolean(String s),将字符串参数解析为 boolean 值时候
如果输入的字符串不是 "true"(不区分大小写),该方法将返回 false。
这种设计使得 Boolean.parseBoolean 方法在处理非标准布尔字符串输入时,有一个明确且一致的返回值,不会抛出异常,而是统一返回 false。
转换方法:
XxxValue()
该方法以Xxx类型返回输入的Byte、Short、Integer、Long、Float、Double 的值
例如:
shortValue()以 short 类型返回此 Short、Integer、Long、Float、Double 的值。Long l = 20L; short s = l.shortValue();
其中在Character中,还有一些字符判断方法和字符转换方法,看一下就会用,也没啥特殊之处需要注意,就不在这里说了。
常量
MIN_VALUE 和 MAX_VALUE
每个数值型包装类和Character都有这两个常量,分别表示该类型能够表示的最大值和最小值。
例如,Integer.MAX_VALUE 表示 int 类型能表示的最大整数值,Double.MIN_VALUE 表示 double 类型能表示的最小正非零值(接近零)。
在Character中,Character.MIN_VALUE 表示 char 类型能表示的最小 Unicode 代码点('\u0000'),Character.MAX_VALUE 表示 char 类型能表示的最大 Unicode 代码点('\uffff')。
在Character中,Character.MIN_VALUE和Character.MAX_VALUE分别表示所缓存的最大值
TRUE 和 FALSE
两个常量分别表示布尔值 true 和 false。它们是 Boolean 类的静态成员,用于获取对应的 Boolean 对象。
在使用 Boolean 对象时,推荐使用这两个常量,而不是通过 new Boolean(true) 或 new Boolean(false) 创建对象,因为后者会创建新的对象实例,可能会引起问题
基本数据类型和包装类需要注意的问题
缓存机制:部分包装类(如 Integer、Byte、Short、Long、Character)在一定范围内会缓存对象。就拿Integer来说,Integer缓存了 -128 到 127 之间的整数。这意味着在这个范围内,相同值的对象是共享的。
Integer a = 100;
Integer b = 100;
System.out.println(a == b);
// 输出 true,因为 a 和 b 引用的是缓存中的同一个对象
Integer c = 200;
Integer d = 200;
System.out.println(c == d);
// 输出 false,因为 200 超出缓存范围,c 和 d 是不同的对象
自动拆装箱
如何理解自动拆装箱
从用途上理解其实就是下述这样,自动装箱就是将基本数据类型自动转换为封装类型,自动拆箱是将封装类型自动转换为基本数据类型。
但是其实编译器的自动执行情况如下:
自动装箱,相当于Java编译器替我们执行了 Integer.valueOf(XXX);
自动拆箱,相当于Java编译器替我们执行了Integer.intValue(XXX)
自动拆装箱需要注意的问题和细节
空指针异常:
当对一个 null 值的包装类对象进行自动拆箱时,会抛出 NullPointerException;
因为自动拆箱实际是调用包装类对象的 xxxValue 方法,null 对象无法调用该方法。
装箱拆箱有开销:
自动装箱和拆箱过程涉及对象的创建与销毁,相较于直接操作基本数据类型,会带来额外的性能开销。在性能敏感的场景(如频繁的循环操作)中,应尽量减少自动拆装箱的使用。
可以用如下代码了解自动拆装箱的性能开销
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000000; i++) {
Integer wrapper = i;
int primitive = wrapper;
}
long endTime = System.currentTimeMillis();
System.out.println("Time taken: " + (endTime - startTime) + " ms");
方法重载与自动拆装箱:
在方法重载的情况下,自动拆装箱可能导致选择错误的方法。
例如,当有一个方法接受 int 参数,另一个方法接受 Integer 参数时,传递一个 Integer 对象可能会调用接受 Integer 参数的方法,而不是自动拆箱后调用接受 int 参数的方法。
public class AutoBoxingOverload {
public static void print(Object obj) {
System.out.println("Object method: " + obj);
}
public static void print(int num) {
System.out.println("int method: " + num);
}
public static void main(String[] args) {
Integer i = 10;
print(i);
// 调用 print(Object obj) 方法,可能与预期不符
}
}
有关void和Void
void 是 Java 中的一种特殊数据类型,它表示 “无类型” 或 “空类型”
Java不能声明 void 类型的变量,void 不能作为数组元素类型。
Void包装类:
Void 是 void 对应的包装类,它是一个不可实例化的类(其构造函数是私有的)。Void 类主要用于与 Java 反射机制和泛型等特性交互。
特殊之处如下:
Void 类没有公共的构造函数,所以无法创建 Void 类的实例。
因为 void 本身表示无值,创建 Void 实例没有实际意义。
唯一常量 TYPE:Void 类包含一个公共的静态成员 TYPE
它是一个 Class
在反射中获取一个返回 void 的方法的返回类型
import java.lang.reflect.Method;
public class VoidExample {
public void voidMethod() {}
public static void main(String[] args) throws NoSuchMethodException {
Method method = VoidExample.class.getMethod("voidMethod");
if (method.getReturnType() == Void.TYPE) {
System.out.println("The method returns void.");
}
}
}
不要混淆 Void 与 void:虽然 Void 是 void 的包装类,但它们的使用场景和语义有很大区别。void 用于声明方法返回类型或在特定语义中表示无值,而 Void 主要用于在需要对象表示 void 类型的场景
上一篇: Java难绷知识02——抽象类中只能有或者必须有抽象方法吗以及有关抽象类的细节探讨
下一篇:Java难绷知识04——异常处理中的 finally 块
文章个人编辑较为匆忙,或许存在各种缺陷之处,需要大家积极反馈来帮助这篇文章和我的技术知识的更进一步,感谢每一位读者
QQ:1746928194,是喜欢画画的coder,欢迎来玩!