语言基础
基本类型与包装类型
核心区别
| 对比维度 | 基本类型 | 包装类型 |
|---|---|---|
| 存储方式 | 栈内存直接存储值 | 堆内存存储对象引用 |
| 默认值 | int->0, boolean->false | 统一为null |
| 泛型支持 | 不支持(如List<int>非法) | 支持(如List<Integer>合法) |
| 内存占用 | 固定大小(int=4字节) | 额外对象头(通常16字节) |
| 性能开销 | 无额外开销 | 对象创建/GC开销 |
| 方法调用 | 值传递 | 引用传递(实际是值传递引用副本) |
自动装箱/拆箱
编译器自动完成基本类型与包装类型的转换,装箱调用valueOf(),拆箱调用xxxValue()。**性能隐患:**循环内频繁装箱(未命中缓存)可能会导致大量对象创建
缓存机制
Java对部分包装类实现了对象缓存机制,这是Java语言设计中的一种内存优化手段,主要应用于频繁使用的数值范围。该机制通过复用已创建的对象来减少内存分配和垃圾回收压力。底层实现:使用静态缓存数组(首次调用时初始化)
| 基本类型 | 位数 | 字节 | 包装类 | 缓存范围 | 缓存是否可配置 | 实现方式 |
|---|---|---|---|---|---|---|
| byte | 8 | 1 | Byte | -128~127 | 不可变 | 静态final数组 |
| short | 16 | 2 | Short | -128~127 | 不可变 | 静态final数组 |
| int | 32 | 4 | Integer | -128~127* | 可配置 | IntegerCache |
| long | 64 | 8 | Long | -128~127 | 不可变 | 静态final数组 |
| float | 32 | 4 | Float | 无缓存 | - | - |
| double | 64 | 8 | Double | 无缓存 | - | - |
| char | 16 | 2 | Character | 0~127 | 不可变 | 静态final数组 |
| boolean | 1 | 1(实际) | Boolean | TRUE/FALSE | 不可变 | 静态final实例 |
对于
Integer,可以通过 JVM 参数-XX:AutoBoxCacheMax=<size>修改缓存上限,但不能修改下限 -128。实际使用时,并不建议设置过大的值,避免浪费内存,甚至是 OOM。
Integer a=127和Integer b=127用==比较结果?
(答:true,命中缓存)
包装类型作为方法参数传递是值传递还是引用传递?
(答:值传递,但传递的是引用的副本)
为什么Float/Double不实现缓存?
浮点数范围过大(±3.4E38和±1.7E308),缓存所有可能值不现实
浮点数比较通常应该使用误差范围而非精确相等
浮点运算规范(IEEE 754)的特殊性导致缓存意义不大
浮点运算本身比整数运算开销大,缓存带来的收益相对较小
缓存机制会导致内存泄漏吗?
不会,因为:
缓存对象通过静态引用持有,生命周期与类相同
缓存范围有限且可控(默认256个对象)
包装类本身是不可变对象,不存在状态变化问题
类卸载时缓存会自然释放(虽然包装类很少被卸载)
==与equals区别
- 对于基本数据类型来说,
==比较的是值。 - 对于引用数据类型来说,
==比较的是对象的内存地址。
equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于基类Object类中,因此所有的类都有equals()方法。
equals() 方法存在两种使用情况:
- 没有重写
equals()方法:使用的默认是Object类equals()方法,等价于通过“==”比较这两个对象。 - 类重写了
equals()方法:一般我们都重写equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即认为这两个对象相等)。
使用场景选择
用基本类型 1.循环体内;2.大规模数据计算 ;3.方法局部变量
用包装类型 1.集合类存储;2.需要表示NULL语义;3.泛型场景
1. 为什么必须同时重写 equals() 和 hashCode()?
基于 Java 对象契约:当 a.equals(b) 返回 true 时,必须保证 a.hashCode() == b.hashCode()。
2. hashCode() 在哈希集合(如 HashMap)中的作用?
- 先通过
hashCode()快速定位桶位置,大幅减少equals()的调用次数,提升性能 - 若哈希值不同,直接存入;若相同,再用
equals()判断是否真正相等
3. 只重写 equals() 不重写 hashCode() 的后果?
两个“相等”对象可能拥有不同的哈希值,导致同时存入 HashMap 的不同位置,违反契约,造成数据重复和检索失败。
4. 默认的 hashCode() 实现?
Object 的默认实现通常与内存地址相关,不同对象的哈希值一般不同。
5. 哈希冲突处理?
哈希值相同但对象不等时发生冲突,HashMap 采用链地址法(链表+红黑树)解决。
核心总结:
重写 equals() 必须重写 hashCode(),且必须保证:相等对象必有相同哈希值(反之不一定成立)。
x.equals(y) == true ==没有重写equals()方法==> 两个对象的内存地址一定相同 ==> 两个对象的HashCode一定相同
x.equals(y) == true ==只重写equals()、没有重写HashCode()方法==> 两个对象的内存地址不一定相同 ==> 两个对象的HashCode不一定相同 ==>这个类无法和所有集合类一起工作核心原因:哈希集合的可靠性
Java集合框架中的哈希表实现(如HashMap、HashSet、Hashtable等)依赖这两个方法协同工作:
先比较哈希码:集合首先调用hashCode()确定对象存储的"桶"位置
再比较相等性:当哈希冲突时,调用equals()确认是否真正相等
三条关键契约规则
一致性:如果两个对象equals()比较相等,它们的hashCode()必须返回相同值,反过来不成立:哈希码相同的对象不一定equals()
不变性:在对象未被修改时,多次调用hashCode()应返回相同值
哈希分布:不相等的对象应尽量产生不同的哈希码(但不是强制要求)
始终记住:相等的对象必须具有相等的哈希码,这是Java集合框架正常工作的基础契约。
String、StringBuffer、StringBuilder
| 特性 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变 | 可变 | 可变 |
| 线程安全 | 线程安全(因为不可变) | 线程安全(同步锁) | 非线程安全(未加锁) |
| 性能 | 最低(频繁创建新对象) | 中等(有同步开销) | 最高(无同步开销) |
| 使用场景 | 少量字符串操作 | 多线程环境下的字符串操作 | 单线程环境下的字符串操作 |
| 继承关系 | 直接继承Object | 继承AbstractStringBuilder | 继承AbstractStringBuilder |
| 方法同步 | 不适用 | 是(synchronized方法) | 否 |
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作,如 expandCapacity、append、insert、indexOf 等公共方法。
可变性
String 是不可变的。1.String类声明为final,杜绝了继承修改行为;2.内部保存字符数组声明为private final,禁止外部修改;3.未对外提供修改方法,所有修改方法(concat/substring等)其实都返回新对象。
StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串,不过没有使用 final 和 private 关键字修饰,最关键的是这个 AbstractStringBuilder 类还提供了很多修改字符串的方法比如 append 方法。
性能
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
在 Java 9 之后,String、StringBuilder 与 StringBuffer 的实现由 char 数组改用 byte 数组存储字符串
新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,byte 占一个字节(8 位),char 占用 2 个字节(16),byte 相较 char 节省一半的内存空间。
JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,byte 和 char 所占用的空间是一样的。
String#intern
String.intern() 是一个 native (本地) 方法,用来处理字符串常量池中的字符串对象引用。
intern()方法的主要作用是确保字符串引用在常量池中的唯一性。- 当调用
intern()时,如果常量池中已经存在相同内容的字符串,则返回常量池中已有对象的引用;否则,将该字符串添加到常量池并返回其引用。
字符串常量池
JVM为了提高性能和减少内存开销,在实例化字符串常量的时候,为字符串开辟一个字符串常量池,类似于缓存区。
字符串常量池底层是 HotSpot的C++实现的,它是一个StringTable(类似java中的 HashTable),保存的本质上是字符串对象的引用。
字符串常量池位置
Jdk1.6及之前:方法区被实现为永久代,运行时常量池在永久代,运行时常量池包含字符串常量池;
Jdk1.7:已经逐步开始“去永久代”,字符串常量池和静态变量被移到了堆内存中;
Jdk1.8及之后:永久代被移除,取而代之的是元空间。而字符串常量池里依然在堆内存中;
总结
从JDK1.7开始,字符串常量池被移动了堆内存中;
字面量赋值,会先去字符串常量池中判断是否存在相同值的字符串对象,存在则返回常量池中的引用,不存在则在常量池中创建一个新对象,并返回引用;
使用new关键字创建字符串对象,会先去字符串常量池中判断是否存在相同值的字符串对象,存在则去堆中创建一个新对象并返回;不存在也在字符串常量池和堆内存中各创建一个字符串对象,并返回堆内存中的对象引用;
常量之间的连接,Java编译器在编译期间会做优化;
连接时含有变量的,会被编译成(new StringBuilder().append(a).append(b).append(c).toString();
当调用intern(时,首先判断常量池中是否存在相同值的字符串常量,如果存在则返回该字符串的引用;如果不存在,则在常量池中增加一个相同值的字符串常量,并返回它在堆内存中的引用;
对于final修饰的变量,它在编译时被解析为常量值的一个本地拷贝存储到自己的常量池中或嵌入到它的字节码流中;
Java中的部分关键字在初始化的类中可能被提前使用而放入常量池。
String s = new String("hello"); //1.字符串常量池中放入了一个“hello” 2.在堆内存中创建了一个String对象存储了“hello”,使用s指向了这个对象引用 String s1 = s.intern(); //s1指向的是字符串常量池中“hello”的引用地址 String s2 = "hello"; //s2指向的是字符串常量池中“hello”的引用地址 System.out.println( s == s2); //false System.out.println( s1 == s2); //true
Object 类方法清单
| 方法签名 | 作用简述 | 重要考点 |
|---|---|---|
public final native Class<?> getClass() | 返回此对象的运行时类。 | 反射的起点。 |
public native int hashCode() | 返回对象的哈希码值。 | 1. 与 equals() 联动: 2. 重写 equals() 必须重写 hashCode() 3. 哈希集合(如HashMap)依赖此方法。 |
public boolean equals(Object obj) | 判断两个对象是否“相等”。 | 默认比较地址(==)。通常需要重写以实现逻辑相等。 |
protected native Object clone() throws CloneNotSupportedException | 创建并返回此对象的一个副本。 | 1. 需实现 Cloneable 接口(标记接口),否则抛异常。 2. 浅拷贝 (默认)。 |
public String toString() | 返回对象的字符串表示。 | 默认输出:类名@哈希码十六进制。通常会被重写。 |
public final native void notify() | 唤醒在此对象监视器上等待的单个线程。 | 多线程协作(wait/notify机制),必须在synchronized代码块内使用。 |
public final native void notifyAll() | 唤醒在此对象监视器上等待的所有线程。 | 同上。 |
public final native void wait(long timeout) throws InterruptedException | 让当前线程等待,直到被通知或超过指定的时间。 | 同上。会释放锁。 |
public final void wait(long timeout, int nanos) | 更精确定时的等待。 | 同上。 |
public final void wait() throws InterruptedException | 让当前线程无限期等待,直到被通知。 | 同上。 |
protected void finalize() throws Throwable | 当垃圾回收器确定不存在对该对象的更多引用时,由对象上的垃圾回收器调用。 | JDK9已弃用。不靠谱,不要手动调用,不要依赖它做资源清理。 |
getClass():反射基础。hashCode()与equals():- 规则1:两个对象
equals()为true,则它们的hashCode()必须相同。 - 规则2:两个对象
hashCode()相同,它们equals()不一定为true(哈希冲突)。 - 重写一个必须重写另一个,否则会破坏基于哈希的集合(如
HashMap,HashSet)的正常工作。
- 规则1:两个对象
clone():- 需要实现
Cloneable接口。 - 是浅拷贝(复制字段引用,不复制引用的对象本身)。如需深拷贝,需在方法内重写逻辑。
- 需要实现
toString():打印/日志调试常用,建议为每个实体类重写。wait(),notify(),notifyAll():- 必须在同步代码块(synchronized)内调用,否则会抛
IllegalMonitorStateException。 wait()会释放当前持有的锁。
- 必须在同步代码块(synchronized)内调用,否则会抛
finalize():已过时。面试时知道它作用且不推荐使用即可。
Java动态代理
- JDK 动态代理
- 基于接口:要求目标类必须实现至少一个接口。
- 实现方式:通过
java.lang.reflect.Proxy类生成代理对象,结合InvocationHandler接口实现增强逻辑。
- CGLib 动态代理
- 基于继承:通过继承目标类来创建子类代理,无需实现接口。
- 实现方式:借助ASM字节码操作框架,在运行时动态生成目标类的子类,并重写方法来实现增强。
| 特性 | JDK 动态代理 | CGLib 动态代理 |
|---|---|---|
| 机制原理 | 通过实现接口,创建接口的代理实例 | 通过继承目标类,生成子类字节码 |
| 目标类要求 | 必须实现接口 | 类不能是 final(无法继承) |
| 方法限制 | 只能代理接口中定义的方法 | 可以代理目标类中非 final、非 static 的方法 |
| 性能 | 生成代理对象较快,但方法调用稍慢 | 生成代理对象较慢,但方法调用执行更快 |
| 依赖 | Java 标准库,无额外依赖 | 需要引入 CGLib 第三方库 |
| 别名 | 接口代理 | 子类代理 |
- 有接口用JDK:Spring AOP默认策略,标准、简单。
- 无接口用CGLib:代理普通类,性能更高,但需引入第三方库。
Spring框架的选择:
- 如果目标对象实现了接口,默认使用 JDK 动态代理。
- 如果目标对象没有实现接口,则使用 CGLib。
- 可以通过配置强制Spring全部使用CGLib。
