00-Java基础-数据类型、String、装箱拆箱与比较
1. 基本数据类型 (Primitive Data Types)
Java 中有 8 种基本数据类型,它们是直接存储在栈内存中的,不是对象
|
类型 |
占用字节 |
取值范围 |
默认值 |
示例 |
|
|
1 |
-128 到 127 |
|
|
|
|
2 |
-32768 到 32767 |
|
|
|
|
4 |
-231 到 231-1 (约 -20亿 到 20亿) |
|
|
|
|
8 |
-263 到 263-1 (非常大) |
|
|
|
|
4 |
单精度浮点数 |
|
|
|
|
8 |
双精度浮点数 |
|
|
|
|
2 |
Unicode 字符 (0 到 65535) |
|
|
|
|
1 (JVM实现) |
|
|
|
注意点:
- 整型默认
int,浮点型默认double:所以在声明long类型时,需要在数字后加L或l(推荐L);声明float类型时,需要在数字后加F或f。
char:可以存储单个字符,也可以存储 Unicode 字符(例如char c = '\u0041';表示 'A')。
boolean:理论上只需要 1 位,但在 JVM 中,通常用 1 字节(byte)或 4 字节(int)来存储。
2. 包装类 (Wrapper Classes) 与 自动装箱/拆箱 (Autoboxing/Unboxing)
2.1 为什么需要包装类?
基本数据类型不是对象,在很多场景下会受到限制,例如:
- Java 集合框架(
List,Set,Map等)只能存放对象,不能直接存放基本数据类型。
- 泛型参数不能是基本数据类型(例如
List<int>是错误的,必须是List<Integer>)。
- 需要为基本数据类型提供更多的操作方法(如
Integer.parseInt())。
为了解决这些问题,Java 为每种基本数据类型提供了对应的包装类 (Wrapper Class)。
|
基本数据类型 |
包装类 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
包装类是对象,存储在堆内存中。
2.2 自动装箱 (Autoboxing) 与 自动拆箱 (Unboxing)
这是 Java 5 引入的语法糖,极大地简化了基本数据类型和包装类之间的转换。
- 自动装箱 (Autoboxing):将基本数据类型自动转换为对应的包装类对象。
int i = 100; Integer integerObject = i; // 自动装箱:编译器将其转换为 Integer.valueOf(i);
- 自动拆箱 (Unboxing):将包装类对象自动转换为对应的基本数据类型。
Integer integerObject = 200; int i = integerObject; // 自动拆箱:编译器将其转换为 integerObject.intValue();
注意点:
- 性能开销:自动装箱/拆箱会在后台创建/销毁对象,这会带来一定的性能开销。在循环等对性能敏感的场景应尽量避免。
NullPointerException:如果包装类对象为null时进行自动拆箱,会抛出NullPointerException。
Integer x = null; int y = x; // 运行时抛出 NullPointerException
- 缓存机制:
Byte,Short,Integer,Long这几种包装类在一定范围内会进行缓存,以提高性能和节省内存。
Integer缓存范围:-128 到 127。
- 这意味着在这个范围内的
Integer对象,如果你用Integer a = 10; Integer b = 10;这样的方式创建,a和b很可能指向同一个对象。超出这个范围则会创建新对象。
Boolean会缓存TRUE和FALSE。
Character会缓存\u0000到\u007F(0到127) 范围的字符。
Float和Double没有缓存机制。
3. String 类
String 是 Java 中最常用、最重要的类之一,用于表示字符串。它有很多独特的特性。
3.1 String 的不可变性 (Immutability)
- 定义:
String对象一旦创建,它的内容就不能被改变。
- 表现:对
String对象的任何修改操作(如concat(),substring(),replace())都不会改变原String对象,而是会返回一个新的String对象。
String s = "Hello";
s.concat(" World"); // s 仍然是 "Hello"
System.out.println(s); // 输出 "Hello"
String s2 = s.concat(" World"); // s2 是 "Hello World"
System.out.println(s); // 输出 "Hello"
System.out.println(s2); // 输出 "Hello World"
- 优点:
- 线程安全:不可变对象天生线程安全,可以在多线程环境中共享而无需额外同步。
- String Pool (字符串常量池):可以实现字符串常量池,节省内存。
- 安全性:作为方法参数时,原始字符串内容不会被意外修改,这在密码存储、网络连接等方面很重要。
- 哈希值缓存:
String的hashCode()值在创建时计算并缓存,提升了HashMap,HashSet等集合的性能。
3.2 String 常量池 (String Pool)
- 定义:一个特殊的内存区域,用于存储字符串字面量(通过双引号
""定义的字符串)。
- 机制:当使用字面量创建
String对象时,JVM 会首先检查字符串常量池。
- 如果池中已存在相同内容的字符串,则直接返回池中对象的引用。
- 如果池中不存在,则在池中创建新的字符串对象,并返回其引用。
new String():使用new String("abc")总是会创建至少一个(如果池中没有,则两个)新对象在堆内存中。如果abc在常量池中不存在,则会先在常量池创建abc, 再在堆内存中创建new String("abc")对象,并引用常量池中的abc。
intern()方法:当你在一个 String 对象上调用 intern() 方法时(通常是在一个通过 new 创建的堆对象上),JVM 会检查常量池中是否有内容相同的字符串。
String s1 = "Hello"; // s1 指向常量池中的 "Hello"
String s2 = "Hello"; // s2 也指向常量池中的 "Hello",和 s1 是同一个对象
String s3 = new String("Hello"); // s3 指向堆中的新对象
System.out.println(s1 == s2); // true
System.out.println(s1 == s3); // false
System.out.println(s1.equals(s3)); // true (内容相等)
String s4 = s3.intern(); // s4 指向常量池中的 "Hello",即 s1 所指的对象
System.out.println(s1 == s4); // true
- 如果有,就返回池中那个字符串的引用。
- 如果没有,就把这个对象的引用(在 JDK 7+)或其内容复制到池中,并返回池中的引用。
3.3 字符串拼接:String、+ 运算符、StringBuilder 与 StringBuffer
Java 中用于字符串操作的主要有 String、StringBuilder 和 StringBuffer。理解它们在可变性、性能和线程安全性上的区别,以及 + 运算符的底层行为,对于高效开发至关重要。
核心区别总结
|
特性 |
|
|
|
|
|
|
底层实现 |
不可变 |
编译器优化为 |
每次循环创建新 |
可变 |
可变 |
|
内容可变性 |
不可变 |
每次生成新的 |
每次生成新的 |
可变 |
可变 |
|
线程安全 |
线程安全 (不可变性保证) |
编译期优化,无直接并发问题 |
每次循环创建局部对象,无直接并发问题 |
非线程安全 |
线程安全 |
|
性能 |
每次操作都创建新对象,性能低 |
高 (编译期优化) |
最差 (大量对象创建) |
最好 (无锁) |
较好 (有锁开销) |
详细解释与选择建议
String
- 核心:一个不可变的字符序列。任何对
String对象的修改(如拼接、截取)都会生成一个新的String对象,而不会改变原始对象。
- 优点:线程安全、可用于常量池、哈希值可缓存、安全性高。
- 缺点:频繁修改会导致创建大量临时对象,性能开销大。
- + 运算符 (String Concatenation Operator)
- 底层实现:
-
- 在 JDK 1.5 及之后,Java 编译器对字符串
+操作进行了优化:
- 在 JDK 1.5 及之后,Java 编译器对字符串
-
- 对于简单、非循环的
+拼接(例如String s = "a" + "b" + "c";),编译器会将其优化为使用一个StringBuilder来实现。
- 对于简单、非循环的
-
- 对于在循环中使用的
+拼接,编译器不会进行深度优化。这意味着在每次循环迭代中,都会创建一个新的StringBuilder对象,执行append(),然后调用toString()生成一个String对象。
- 对于在循环中使用的
- 性能:
-
- 在循环外使用时性能尚可(因为有编译器优化)。
-
- 但在循环内使用时性能最差。因为每次循环都会创建大量临时的
StringBuilder和String对象,造成严重的性能浪费和内存开销。
- 但在循环内使用时性能最差。因为每次循环都会创建大量临时的
- 线程安全:由于每次操作或每次循环都会创建新的
String或StringBuilder局部对象,+操作本身不直接涉及共享的可变状态,所以通常不从线程安全角度来讨论它。
StringBuilder
- 核心:一个可变的字符序列,底层通过
char[]数组实现。
- 性能:在单线程环境下,它是性能最好的字符串拼接工具。因为它不会像
String那样每次操作都创建新对象,且没有同步开销。
- 线程安全:非线程安全。它的所有方法都没有
synchronized修饰,在多线程环境下并发修改StringBuilder会导致数据不一致。
- 选择建议:在绝大多数单线程环境下的字符串拼接场景,应优先使用
StringBuilder。
StringBuffer
- 核心:与
StringBuilder类似,也是一个可变的字符序列,底层通过char[]数组实现。
- 性能:由于其所有公共方法(如
append,insert)都被synchronized关键字修饰,保证了线程安全,但这也带来了额外的锁竞争和释放开销,导致其性能略低于StringBuilder。
- 线程安全:线程安全。
- 选择建议:仅当在多线程环境下,且多个线程需要共享并修改同一个
StringBuffer实例时,才应该使用StringBuffer。在实际开发中,更常见的做法是每个线程使用自己的局部StringBuilder进行拼接,最后再将最终结果(一个不可变的String对象)返回或共享,这样可以避免锁竞争,性能通常更好。
总结性建议:
- 少量且简单拼接:直接使用
+运算符,编译器会优化。
- 单线程,大量拼接或循环拼接:用
StringBuilder。
- 多线程,有共享且修改需求:用
StringBuffer。
4. == 与 equals() 方法的比较
这是 Java 面试中的另一个经典考点,理解它们之间的区别至关重要。
4.1 == 运算符
- 作用对象:
- 基本数据类型:比较的是值是否相等。
int a = 10; int b = 10; int c = 20; System.out.println(a == b); // true System.out.println(a == c); // false
- 引用数据类型 (对象):比较的是两个引用的内存地址是否相等。如果相等,说明它们指向内存中的同一个对象实例。这个对象实例可能在堆的普通区域(例如通过
new创建),也可能在字符串常量池中(例如通过字符串字面量创建)。
// 示例1:指向字符串常量池中的同一个对象
String s1 = "Hello"; // s1 指向常量池中的 "Hello"
String s2 = "Hello"; // s2 也指向常量池中的 "Hello"
System.out.println(s1 == s2); // true
// 示例2:指向堆内存中的不同对象
String s3 = new String("Hello"); // s3 指向堆中的一个新对象
String s4 = new String("Hello"); // s4 指向堆中的另一个新对象
System.out.println(s3 == s4); // false
// 示例3:指向堆内存中的同一个对象
String s5 = s3; // s5 和 s3 指向同一个对象
System.out.println(s3 == s5); // true
4.2 equals() 方法
- 作用对象:只能用于引用数据类型 (对象)。
- 默认行为:
Object类中equals()方法的默认实现与==运算符行为一致,即比较两个对象的内存地址。
Object obj1 = new Object(); Object obj2 = new Object(); System.out.println(obj1.equals(obj2)); // false (默认行为,比较地址)
- 通常被重写:
equals()方法的真正意义在于子类可以根据自己的业务需求来重写它,从而定义对象内容的相等性。
String类:String类重写了equals()方法,用于比较字符串的内容是否相等。
String str1 = new String("Java");
String str2 = new String("Java");
System.out.println(str1.equals(str2)); // true (内容相等)
System.out.println(str1 == str2); // false (地址不同)
- 包装类:
Integer,Double等包装类也重写了equals()方法,用于比较它们所包装的基本数据类型的值是否相等。
Integer i1 = new Integer(100); Integer i2 = new Integer(100); System.out.println(i1.equals(i2)); // true (值相等) System.out.println(i1 == i2); // false (地址不同) Integer i3 = 10; // 自动装箱,触发缓存 Integer i4 = 10; // 自动装箱,触发缓存 System.out.println(i3.equals(i4)); // true (值相等) System.out.println(i3 == i4); // true (指向缓存的同一个对象)
- 重写
equals()的约定:如果你重写了equals()方法,通常也需要同时重写hashCode()方法,以遵守hashCode()和equals()的协定(详见HashMap章节)。
4.3 总结对比
|
特性 |
|
|
|
基本数据类型 |
比较值 |
不适用 (编译错误) |
|
引用数据类型 |
比较内存地址 |
默认比较内存地址,可重写为比较内容 |
|
适用范围 |
既可用于基本类型也可用于引用类型 |
只能用于引用类型 |
4.4 面试高频问题:String 比较
理解 String 的不可变性和字符串常量池后,以下代码的输出是面试中的经典考点:
String s1 = "hello"; // s1 指向常量池中的 "hello"
String s2 = "hello"; // s2 也指向常量池中的 "hello"
String s3 = new String("hello"); // s3 指向堆中的新对象
String s4 = "he" + "llo"; // 编译期优化,等同于 "hello",指向常量池
String s5 = new String("hello").intern(); // s5 指向常量池中的 "hello" (通过 intern())
String s6 = "he";
String s7 = "llo";
String s8 = s6 + s7; // 运行时拼接,s8 指向堆中的新对象
System.out.println(s1 == s2); // true (都指向常量池的同一个对象)
System.out.println(s1 == s3); // false (常量池对象 vs 堆对象)
System.out.println(s1 == s4); // true (编译期优化,等同于字面量)
System.out.println(s1 == s5); // true (s5 经 intern() 后指向常量池对象)
System.out.println(s1 == s8); // false (s8 是运行时拼接,在堆中创建)
System.out.println(s1.equals(s3)); // true (内容相等)
System.out.println(s3.equals(s8)); // true (内容相等)
5. Object 类及其常用方法
Object 类是所有 Java 类的根父类。这意味着在 Java 中创建的任何类,如果没有明确指定父类,都将默认直接或间接继承 Object 类。它提供了一些所有对象都具备的基础方法,这些方法对于 Java 对象的行为和交互至关重要。
以下是 Object 类中一些常见且重要的方法及其作用:
5.1 equals(Object obj)`
- 作用:用于比较两个对象是否相等。
- 默认行为:
Object类的equals方法默认实现与==运算符行为一致,即比较两个对象的内存地址(引用)。
- 重要性:在大多数实际应用中,我们需要比较对象的内容是否相等,而不是它们的内存地址。因此,通常需要在自定义类中重写 (Override)
equals()方法,以实现基于业务逻辑的相等性判断(例如,两个Person对象,如果他们的id相同,就认为是相等的)。
- 约定:如果重写
equals(),几乎总是需要同时重写hashCode()方法,以维护hashCode()与equals()的协定:如果两个对象equals,那么它们的hashCode必须相同。
5.2 hashCode()`
- 作用:返回对象的哈希码值(一个
int类型的整数)。
- 默认行为:
Object类的hashCode()方法通常返回对象的内存地址的某个映射值,或者由 JVM 内部生成的一个唯一标识符。
- 重要性:在基于哈希的集合类(如
HashMap,HashSet,HashTable)中,hashCode()方法用于快速定位对象。当一个对象被放入这些集合时,首先会调用hashCode()来确定它在内部数组中的位置。
- 约定:如上所述,如果重写
equals(),就必须重写hashCode(),确保equals为true的两个对象拥有相同的hashCode。反之不一定成立(不同的对象可以有相同的哈希码,这称为哈希冲突)。
5.3 toString()`
- 作用:返回对象的字符串表示。
- 默认行为:
Object类的toString()方法默认返回一个由类名 + "@" + 对象哈希码的无符号十六进制表示组成的字符串(例如:java.lang.Object@15db9742)。
- 重要性:在调试、日志记录或任何需要将对象以可读形式输出的场景中非常有用。通过重写
toString()方法,可以提供更有意义、更具描述性的对象信息(例如,对于Person对象,可以返回Person[name=张三, age=20])。
5.4 getClass()`
- 作用:返回运行时此
Object对象的Class对象。
- 默认行为:这是一个
final方法,不能被子类重写。
- 重要性:
Class对象是反射机制的入口。通过Class对象,可以获取类的构造器、方法、字段等信息,并动态地创建对象、调用方法。这在框架开发、序列化/反序列化等场景中非常关键。
5.5 notify(), notifyAll(), wait()`
- 作用:这三个方法用于实现线程间的通信和协调,是 Java 并发编程的基础。
- 默认行为:它们都与对象的监视器(monitor)锁相关联,并且只能在同步方法或同步代码块中调用,否则会抛出
IllegalMonitorStateException。
- 重要性:
wait():使当前线程等待,直到其他线程调用此对象的notify()或notifyAll()方法,或者经过一定时间。它会释放对象锁。
notify():唤醒在此对象监视器上等待的单个线程。
notifyAll():唤醒在此对象监视器上等待的所有线程。
5.6 clone()`
- 作用:创建并返回此对象的一个副本。
- 默认行为:
Object类的clone()方法执行的是浅拷贝 (shallow copy),即只复制对象本身及其基本数据类型的字段。如果对象包含对其他对象的引用,那么只复制这些引用,而不复制被引用的对象本身。
- 重要性:如果需要实现对象的深拷贝,通常需要重写
clone()方法,并在其中递归地克隆所有引用类型的字段,同时还要实现Cloneable接口。
- 注意:Java 推荐使用复制构造函数或工厂方法来实现对象复制,因为
clone()方法存在一些设计上的复杂性(如Cloneable接口是标记接口、受保护方法等)。
5.7 finalize()`
- 作用:当垃圾收集器确定不再有对该对象的引用时,由垃圾收集器调用此方法。
- 默认行为:
Object类的finalize()方法是空的,不执行任何操作。
- 重要性:这个方法用于在对象被垃圾回收前执行一些清理操作,例如关闭文件句柄、数据库连接等非 Java 资源。
- 注意:
finalize()方法在现代 Java 开发中已不推荐使用。它的执行时间不确定,可能永远不会被调用,且会带来性能开销和不确定性。推荐使用try-with-resources语句或CleanerAPI 来管理资源。

浙公网安备 33010602011771号