Java核心基础机制深度解析:数据类型、参数传递与对象拷贝
基本数据类型与包装类机制
自动装箱与拆箱原理
在Java中,基本数据类型不具备面向对象的特征,因此提供了对应的包装器类型(Wrapper Classes)。将基本类型转换为包装器类型的过程称为装箱,反之称为拆箱。自Java 5起,编译器会自动处理这些转换。
public class BoxingDemo {
public static void main(String[] args) {
// 自动装箱:编译器底层调用 Integer.valueOf(42)
Integer wrappedNumber = 42;
// 自动拆箱:编译器底层调用 wrappedNumber.intValue()
int primitiveNumber = wrappedNumber;
}
}
装箱的主要优势在于将基本数据封装为对象,从而能够利用对象提供的丰富API,并使其能够参与泛型集合(如 List<Integer>)的操作。
包装类缓存池与对象比较
为了优化内存使用,Java为部分包装类实现了缓存机制。以 Integer 为例,默认缓存了 [-128, 127] 范围内的对象。
public class CacheComparison {
public static void main(String[] args) {
Integer valA = 120;
Integer valB = 120;
Integer valC = 200;
Integer valD = 200;
// 在缓存范围内,指向同一个对象
System.out.println(valA == valB); // true
// 超出缓存范围,在堆中创建新对象,地址不同
System.out.println(valC == valD); // false
// Short, Byte, Character, Long 也有类似的缓存机制
Short s1 = 127;
Short s2 = 127;
System.out.println(s1 == s2); // true
// Double 和 Float 没有实现缓存池,每次装箱都会创建新对象
Double d1 = 100.0;
Double d2 = 100.0;
System.out.println(d1 == d2); // false
}
}
通过 new Integer(100) 显式创建对象时,不会触发缓存机制,每次都会在堆内存中分配新空间。而直接赋值或调用 valueOf 则会优先从缓存池中获取,执行效率更高且节省内存。
对象相等性判断机制
== 与 equals 的本质区别
== 运算符用于比较两个变量的内存地址。对于基本数据类型,比较的是具体的数值;对于引用类型,比较的是对象在堆内存中的地址引用。
equals 是 Object 类的方法,默认实现等同于 ==。但在实际开发中,诸如 String、Integer 等类都重写了该方法,使其用于比较对象的实际内容(状态)是否相等。
String str1 = "hello";
String str2 = "hello";
String str3 = new String("hello");
System.out.println(str1 == str2); // true,指向字符串常量池中的同一对象
System.out.println(str1 == str3); // false,str3在堆中开辟了新空间
System.out.println(str1.equals(str3)); // true,内容相同
hashCode 与 equals 的契约
hashCode 用于获取对象的哈希值,主要用于哈希表(如 HashMap、HashSet)中的快速定位。两者必须遵循以下契约:
- 如果两个对象通过
equals比较相等,那么它们的hashCode必须相同。 - 如果两个对象的
hashCode相同,它们通过equals比较不一定相等(哈希冲突)。 - 重写
equals方法时,必须同时重写hashCode方法,否则会导致对象在哈希集合中无法被正确检索。
Java泛型与类型擦除
Java泛型在编译期提供严格的类型安全检查,避免显式的强制类型转换。然而,Java的泛型是伪泛型,在编译后的字节码中,泛型类型参数会被擦除(Type Erasure),替换为其上界(如未指定则默认为 Object)。
常见的通配符包括:
<? extends T>:上界通配符,表示只能读取(Producer),不能写入。<? super T>:下界通配符,表示只能写入(Consumer),读取时只能当作Object处理(PECS原则)。
参数传递机制:为什么Java只有值传递
在方法调用时,Java始终采用值传递(Pass-by-Value)。这意味着传递给方法参数的是实际参数的副本。对于基本类型,传递的是数值的副本;对于引用类型,传递的是对象内存地址的副本。
基本类型的值传递
public class PrimitivePassing {
public static void modifyValue(int num) {
num = 99; // 修改的是副本,不影响原变量
}
public static void main(String[] args) {
int original = 10;
modifyValue(original);
System.out.println(original); // 输出 10
}
}
引用类型的值传递
传递引用类型的地址副本时,可以通过该副本修改堆内存中对象的内部状态,但无法改变原引用变量所指向的对象。
class User {
String name;
User(String name) { this.name = name; }
}
public class ReferencePassing {
// 修改对象内部状态
public static void modifyObject(User user) {
user.name = "Alice";
}
// 尝试交换两个引用
public static void swapReferences(User u1, User u2) {
User temp = u1;
u1 = u2;
u2 = temp; // 仅交换了副本的指向,不影响原引用
}
public static void main(String[] args) {
User userA = new User("Bob");
modifyObject(userA);
System.out.println(userA.name); // 输出 Alice
User x = new User("X");
User y = new User("Y");
swapReferences(x, y);
System.out.println(x.name + " " + y.name); // 输出 X Y,未发生交换
}
}
对象克隆:深拷贝与浅拷贝
在复用复杂对象时,直接赋值引用会导致多个变量指向同一内存地址。通过克隆可以创建对象的副本。实现克隆通常需要实现 Cloneable 接口并重写 clone 方法。
浅拷贝(Shallow Copy)
浅拷贝会创建一个新的对象,但对于对象内部的引用类型属性,仅仅拷贝其引用地址。新旧对象的引用属性指向同一个堆内存对象。
深拷贝(Deep Copy)
深拷贝不仅创建新对象,还会递归地拷贝所有引用类型属性,确保新旧对象及其关联的整个对象图在内存中完全独立,互不干扰。
class Department implements Cloneable {
String deptName;
Department(String name) { this.deptName = name; }
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
class Employee implements Cloneable {
String empName;
Department dept;
Employee(String name, Department dept) {
this.empName = name;
this.dept = dept;
}
@Override
protected Object clone() throws CloneNotSupportedException {
Employee cloned = (Employee) super.clone();
// 递归调用引用对象的clone方法实现深拷贝
cloned.dept = (Department) this.dept.clone();
return cloned;
}
}
public class CopyDemo {
public static void main(String[] args) throws Exception {
Department dept = new Department("R&D");
Employee emp1 = new Employee("John", dept);
Employee emp2 = (Employee) emp1.clone();
emp2.dept.deptName = "HR";
System.out.println(emp1.dept.deptName); // 输出 R&D,深拷贝保证了独立性
}
}
多态基础:方法重载与重写
方法重载(Overloading)和重写(Overriding)是实现多态的重要手段,两者在作用域和规则上有显著差异。
| 特性 | 方法重载 (Overload) | 方法重写 (Override) |
|---|---|---|
| 发生范围 | 同一个类中 | 父子类之间 |
| 方法名 | 必须相同 | 必须相同 |
| 参数列表 | 必须不同(类型、个数或顺序) | 必须相同 |
| 返回类型 | 无限制 | 子类返回值类型必须小于或等于父类(协变返回) |
| 访问修饰符 | 无限制 | 子类方法的访问权限必须大于或等于父类 |
| 异常声明 | 无限制 | 子类抛出的非运行时异常范围必须小于或等于父类 |
| 绑定阶段 | 编译期(静态绑定) | 运行期(动态绑定) |
在重写父类方法时,需遵循"两同两小一大"原则:方法名和参数列表相同;子类返回值类型和抛出异常范围小于等于父类;子类访问权限大于等于父类。若父类方法返回基本数据类型或 void,则子类不可更改返回类型。