00-Java基础-数据类型、String、装箱拆箱与比较

1. 基本数据类型 (Primitive Data Types)

Java 中有 8 种基本数据类型,它们是直接存储在栈内存中的,不是对象

类型

占用字节

取值范围

默认值

示例

byte

1

-128 到 127

0

byte b = 10;

short

2

-32768 到 32767

0

short s = 100;

int

4

-231 到 231-1 (约 -20亿 到 20亿)

0

int i = 100000;

long

8

-263 到 263-1 (非常大)

0L

long l = 10000000000L;

float

4

单精度浮点数

0.0f

float f = 3.14f;

double

8

双精度浮点数

0.0d

double d = 3.14159;

char

2

Unicode 字符 (0 到 65535)

\u0000

char c = 'A';

boolean

1 (JVM实现)

truefalse

false

boolean bool = true;

注意点:

  • 整型默认 int,浮点型默认 double:所以在声明 long 类型时,需要在数字后加 Ll (推荐 L);声明 float 类型时,需要在数字后加 Ff
  • 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)

基本数据类型

包装类

byte

Byte

short

Short

int

Integer

long

Long

float

Float

double

Double

char

Character

boolean

Boolean

包装类是对象,存储在堆内存中。

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; 这样的方式创建,ab 很可能指向同一个对象。超出这个范围则会创建新对象。
    • Boolean 会缓存 TRUEFALSE
    • Character 会缓存 \u0000\u007F (0到127) 范围的字符。
    • FloatDouble 没有缓存机制。

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 (字符串常量池):可以实现字符串常量池,节省内存。
    • 安全性:作为方法参数时,原始字符串内容不会被意外修改,这在密码存储、网络连接等方面很重要。
    • 哈希值缓存StringhashCode() 值在创建时计算并缓存,提升了 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+ 运算符、StringBuilderStringBuffer

Java 中用于字符串操作的主要有 StringStringBuilderStringBuffer。理解它们在可变性、性能和线程安全性上的区别,以及 + 运算符的底层行为,对于高效开发至关重要。

核心区别总结

特性

String (每次操作)

+ 运算符 (循环外)

+ 运算符 (循环内)

StringBuilder

StringBuffer

底层实现

不可变 char[]

编译器优化为 StringBuilder

每次循环创建新 StringBulder

可变 char[]

可变 char[]

内容可变性

不可变

每次生成新的 String

每次生成新的 String

可变

可变

线程安全

线程安全 (不可变性保证)

编译期优化,无直接并发问题

每次循环创建局部对象,无直接并发问题

非线程安全

线程安全

性能

每次操作都创建新对象,性能低

高 (编译期优化)

最差 (大量对象创建)

最好 (无锁)

较好 (有锁开销)

详细解释与选择建议

  1. String
    • 核心:一个不可变的字符序列。任何对 String 对象的修改(如拼接、截取)都会生成一个新的 String 对象,而不会改变原始对象。
    • 优点:线程安全、可用于常量池、哈希值可缓存、安全性高。
    • 缺点:频繁修改会导致创建大量临时对象,性能开销大。
  1.  + 运算符 (String Concatenation Operator)
    • 底层实现
      •   在 JDK 1.5 及之后,Java 编译器对字符串 + 操作进行了优化:
      •   对于简单、非循环的 + 拼接(例如 String s = "a" + "b" + "c";),编译器会将其优化为使用一个 StringBuilder 来实现。
      •   对于在循环中使用的 + 拼接,编译器不会进行深度优化。这意味着在每次循环迭代中,都会创建一个新的 StringBuilder 对象,执行 append(),然后调用 toString() 生成一个 String 对象。
    • 性能
      •   在循环外使用时性能尚可(因为有编译器优化)。
      •   但在循环内使用时性能最差。因为每次循环都会创建大量临时的 StringBuilderString 对象,造成严重的性能浪费和内存开销。
    • 线程安全:由于每次操作或每次循环都会创建新的 StringStringBuilder 局部对象,+ 操作本身不直接涉及共享的可变状态,所以通常不从线程安全角度来讨论它。
  1. StringBuilder
    • 核心:一个可变的字符序列,底层通过 char[] 数组实现。
    • 性能:在单线程环境下,它是性能最好的字符串拼接工具。因为它不会像 String 那样每次操作都创建新对象,且没有同步开销。
    • 线程安全非线程安全。它的所有方法都没有 synchronized 修饰,在多线程环境下并发修改 StringBuilder 会导致数据不一致。
    • 选择建议:在绝大多数单线程环境下的字符串拼接场景,应优先使用 StringBuilder
  1. 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() 方法的真正意义在于子类可以根据自己的业务需求来重写它,从而定义对象内容的相等性
    • StringString 类重写了 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 总结对比

 

 

特性

== 运算符

equals() 方法

基本数据类型

比较

不适用 (编译错误)

引用数据类型

比较内存地址

默认比较内存地址,可重写为比较内容

适用范围

既可用于基本类型也可用于引用类型

只能用于引用类型

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(),确保 equalstrue 的两个对象拥有相同的 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 语句或 Cleaner API 来管理资源。
posted @ 2025-12-24 10:19  我是刘瘦瘦  阅读(3)  评论(0)    收藏  举报