一、引言
想必有相当一部分人听说过这么一个笑话:
Java程序员刘老六和柳老流在一块吐苦水,刘老六说,我这当了这么多年程序员,怎么就找不到一个女朋友呢,家里老催我找对象呢,这个柳老流就安慰他说,没关系啊,你可以 new 一个对象。
在Java中,万物皆对象。那么什么是对象呢?
有解释为:对象是类的实例化。以内存的角度来说,对象是堆中的一块内存空间。
那么Java中有两大类数据类型,一种是基本数据类型,一种是引用数据类型。基本数据类型不能创建对象啊,这不是与万物皆对象的思想不一致了吗?
二、包装类
基本数据类型经常被用于简单的计算和数据存储,然而有时我们需要将这些基本类型存储为对象来进行更复杂的操作,或者在泛型类(如:ArrayList)中使用数据。由此,Java万物皆对象的思想在基本数据类型上仍有体现。
1、什么是包装类
包装类是基本数据类型的对象表示。每个基本类型都有对应的包装类
基本数据类型 | 包装类 |
byte | Byte |
short | Short |
int | Integer |
long | Long |
double | Double |
float | Float |
char | Character |
boolean | Boolean |
包装类的主要作用是将基本数据类型转换为对象,或者将对象转换回基本数据类型。这对于需要对象而不能直接使用基本类型的情况非常有用。如:在ArrayList等集合类中,我们不能直接存储基本数据类型,只能存储对象,而包装类正好提供了这一功能。
Byte b1 = null; // √
byte b2 = null; // ×
包装类本质都是类,可以赋值为null,但是基本类型不可赋值为null。
2、包装类的特性
1. 不可变
一旦构造了包装器,就不允许更改包装在其中的值。
Integer x = 10;
x = 20; // 创建了一个新的Integer对象,原来的x对象不能改变
2. final修饰
包装类由final修饰,不可定义它们的子类。
3. 数字型包装类共同超类Number
Byte、Short、Integer、Long、Float、Double等六个包装类有共同的父类Number。
4. 提供常量
包装类提供了几个常量,这些常量通常是该类型的最大值、最小值等。可以通过这些常量来方便地获取数值范围。
System.out.println("最大值: " + Integer.MAX_VALUE); // 2147483647
System.out.println("最小值: " + Integer.MIN_VALUE); // -2147483648
System.out.println("True: " + Boolean.TRUE); // true
System.out.println("False: " + Boolean.FALSE); // false
5. 包装类和对应基本类型可以互相转换
每个包装类都提供了将其值转换为对应基本数据类型的方法。这些方法通常以intValue()、doubleValue()、booleanValue()等形式命名。
Integer integerObj = Integer.valueOf(100);
int num = integerObj.intValue(); // 转换为基本类型 int
System.out.println("转换后的值: " + num);
可以使用 valueOf()
方法将一个基本数据类型转换为对应的包装类对象。
int num = 10;
Integer integerObj = Integer.valueOf(num); // 将int转换为Integer对象
System.out.println("包装类对象: " + integerObj);
6. 静态方法: valueOf() 和 parseXXX()
valueOf() 方法用于将基本数据类型转换为包装类对象,或者将字符串转换为包装类对象。
Integer x = Integer.valueOf(42); // 从int转换为Integer
Integer y = Integer.valueOf("42"); // 从String转换为Integer
parseXXX() 方法用于将字符串转换为基本数据类型。
int x = Integer.parseInt("42"); // 从String转换为int
double y = Double.parseDouble("3.14"); // 从String转换为double
7. 实现了Comparable接口
所有的包装类都实现了Comparable接口。可以比较包装类对象的大小。
Integer a = 10;
Integer b = 20;
int comparisonResult = a.compareTo(b); // 比较a和b
System.out.println("比较结果: " + comparisonResult); // 输出负数,因为10小于20
8. 自动拆装箱
基本数据类型和包装类之间的转换可以自动进行,Java会在需要时自动将基本数据类型转换为对应的包装类对象(装箱),或者将包装类对象转换为基本数据类型(拆箱)。
ArrayList<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱,将int 10 转换为 Integer 对象
int num = list.get(0); // 自动拆箱,将 Integer 对象转换为 int
System.out.println("Number: " + num);
9. 重写了 hashCode() 和 equals()
包装类重写了 hashCode() 和 equals() ,以便进行对象比较。equals() 方法用于比较对象的值,hashCode()用于计算对象的哈希值。
Integer x = 10;
Integer y = 10;
System.out.println(x.equals(y)); // 输出 true,因为两个Integer对象的值相同
System.out.println(x.hashCode()); // 输出 10 的哈希值
包装类使用的是值的比较,而不是对象的内存地址比较。
10. 线程安全
包装类对象是不可变的,这意味着它们是线程安全的。在多线程环境中,多个线程共享包装类对象时,不会出现并发修改的问题。
三、128陷阱
1、什么是“128陷阱”
包装类采用了缓存机制来优化内存使用和性能。这一机制可能会导致一些看似矛盾的行为,通常被称为“128陷阱”。
对于数值型包装类,如果数值一样但是不在-128~127之间,则不是同一个对象。
对于Char、Boolean、Byte,需要 ≤ 127
这个范围的使用频率比较高,同一个变量指向同一个对象,会节省内存。
Integet a1 = 100;
Integet a2 = 100;
System.out.println(a1==a2); // true
Integet a3 = 200;
Integet a4 = 200;
System.out.println(a3==a4); // false
2、从源码理解“128陷阱”
以Integer为例,上源码
private 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() {}
}
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
private final int value;
public Integer(int value) {
this.value = value;
}
由Integer类源码可以发现,Integer类对-128到127范围内的整数进行缓存,缓存到一个长为256的数组中内,当创建值在该范围内的一个Integer对象,直接引用缓存中的数据,即引用了同一块内存地址,使用==操作符比较两个对象是返回true。
当创建一个值不在该范围内的Integer对象,Java不会使用缓存,而是创建新对象,这样即使两个Integer的值相同,引用也会不同,==操作符比较时会返回false。
四、自动拆装箱
自动拆装箱简化了基本数据类型和其包装类之间的转换。通过这两种机制,Java能够在不显式调用转换方法的情况下,自动进行这些转换操作。
1、自动装箱
自动装箱是指,Java会自动将基本数据类型转换为相应的包装类对象。
int a = 10;
// 自动装箱:将int类型的a装箱为Integer对象
Integer integerObj = a; // 相当于 Integer.valueOf(a);
System.out.println("integerObj: " + integerObj); // 输出 Integer对象
2、自动拆箱
自动拆箱是指,Java会自动将包装类对象转换为相应的基本数据类型。
Integer integerObj = 10; // 自动装箱
int a = integerObj; // 自动拆箱:将Integer对象转换为基本数据类型int
System.out.println("a: " + a); // 输出基本数据类型 int 值
3、自动装箱和自动拆箱的发生时机
1. 自动装箱
(1)将基本数据类型赋值给包装类对象
Integer integerObj = 100; // 自动装箱:int 100 被装箱成 Integer 对象
(2)将基本数据类型传递给需要对象的集合或方法
List<Integer> list = new ArrayList<>();
list.add(10); // 自动装箱:将 int 10 转换为 Integer 对象并添加到 List 中
2. 自动拆箱
(1)将包装类对象赋值给基本数据类型变量
Integer integerObj = 10;
int a = integerObj; // 自动拆箱:将 Integer 对象转换为 int 类型
(2)将包装类对象传递给要求基本数据类型的集合或方法
Integer integerObj = 20;
int result = integerObj + 10; // 自动拆箱:将 Integer 对象拆箱为 int 后进行加法运算
3. 混合使用
自动装箱与自动拆箱是同时发生的。“+”运算符会触发自动拆箱,然后将拆箱后的值与10相加,再将结果装箱成 Integer对象。
Integer a = 100;
Integer b = 200;
// 自动拆箱和装箱混合:a和b会被自动拆箱为int类型,运算结果会被装箱为Integer
Integer sum = a + b; // 自动拆箱:a和b被拆箱为int,再加法运算,结果会自动装箱为Integer
System.out.println("Sum: " + sum); // 输出 300
4. 例子
int a = 10;
int b = 10;
Integer a1 =10;
Integer b1 = 10;
Integer a2 = new Integer(10);
Integer b2 = new Integer(10);
System.out.println(a==b); // true ,不涉及拆装箱
System.out.println(a1==b1); // true ,不涉及拆装箱
System.out.println(a2==b2); // false ,不涉及拆装箱
System.out.println(a1==a); // true ,a1拆箱比较值
System.out.println(a1.equals(a)); // true ,a1拆箱比较值
System.out.println(a1==a2); // false,不涉及拆装箱
System.out.println(a==a2); // true ,a2拆箱比较值
解释System.out.println(a1.equals(a)); // true
a1 是 Integer 类型 的,a是 int 类型的,判断的时候 Integer 要转出为 int 来判断,进行了自动拆箱。此处的equals方法在Integer中重写了,底层还是使用==来判断