Java 基础(四)String相关
Java final关键字与String类核心知识点随堂笔记
前言
本篇笔记承接Java面向对象、多线程系列内容,完整复盘课堂讲解的final关键字核心用法、String类的底层本质与不可变特性、字符串常量池原理、==与equals()的核心区别,以及String常用方法、String/StringBuffer/StringBuilder的选型对比,修正课堂代码的语法问题,补充新手易混淆的底层原理与面试高频考点,适配Java入门复盘与巩固需求。
一、final关键字核心用法
final关键字的核心含义是不可改变、最终的,可以用来修饰变量、方法、类,不同场景下有明确的语法规则与使用限制。
1.1 final修饰变量:常量
被final修饰的变量,一旦完成赋值,其值就不可被二次修改,也被称为常量。根据修饰的变量类型(基本数据类型/引用数据类型)、位置(局部变量/全局成员变量),有不同的规则。
核心规则1:基本数据类型 vs 引用数据类型
- final修饰基本数据类型:固定的是变量的数据值本身,赋值后绝对不允许修改值,二次赋值会直接编译报错。
- final修饰引用数据类型:固定的是变量的内存引用地址,地址不能改变,但引用指向的对象内部的内容可以修改。
课堂代码示例(规范修正版)
import java.util.Arrays;
public class FinalDemo {
public static void main(String[] args) {
// 1. final修饰基本数据类型
final int a = 10;
// a = 20; 编译报错:final变量赋值后无法修改
// 普通变量可随意修改
int age = 18;
age = 20;
// 2. final修饰引用数据类型(数组)
final int[] arr = {1,2,3,4,5};
// 数组内部的元素可以修改
arr[0] = 100;
arr[1] = 200;
System.out.println(Arrays.toString(arr)); // 输出:[100, 200, 3, 4, 5]
// arr = new int[]{6,7,8}; 编译报错:final固定了数组的引用地址,不能指向新的数组对象
}
}
核心规则2:局部变量 vs 全局成员变量
- final修饰局部变量:使用前必须完成直接赋值,赋值后不可修改,无其他强制要求。
- final修饰全局成员变量:必须在对象创建完成前完成赋值,仅支持三种合法赋值方式,否则编译报错:
- 声明变量时直接赋值(最常用)
- 在构造方法中完成赋值
- 在非静态代码块中完成赋值
代码示例:全局final变量的合法赋值
public class Person {
// 方式1:声明时直接赋值
private final String NAME = "张三";
// 方式2:预留构造方法赋值
private final int AGE;
// 方式3:代码块赋值
private final String ADDRESS;
{
// 非静态代码块中赋值
ADDRESS = "北京市";
}
// 构造方法中赋值
public Person(int age) {
this.AGE = age;
}
public Person() {
this.AGE = 18; // 重载构造方法也必须完成赋值
}
}
1.2 final修饰方法
被final修饰的方法,可以被子类继承调用,但不能被子类重写,核心用途是保护核心业务逻辑不被修改,保证父类方法的行为在所有子类中保持一致。
代码示例
public class Base {
// final修饰的方法,子类无法重写
public final void coreMethod() {
System.out.println("核心业务逻辑,不允许子类修改");
}
// 普通方法,子类可重写
public void normalMethod() {
System.out.println("普通方法");
}
}
class Child extends Base {
// 编译报错:final方法无法被重写
// @Override
// public void coreMethod() {
// System.out.println("尝试修改核心逻辑");
// }
// 普通方法可正常重写
@Override
public void normalMethod() {
System.out.println("子类重写的普通方法");
}
}
1.3 final修饰类
被final修饰的类,无法被任何类继承,断绝了继承关系,类中的所有方法默认都无法被重写。Java中最典型的就是String类,就是被final修饰的,没有子类。
代码示例
// final修饰的类,无法被继承
public final class FinalClass {
public void test() {
System.out.println("final类的方法");
}
}
// 编译报错:无法继承final类
// class SubClass extends FinalClass {
// }
1.4 final关键字核心总结
| 修饰目标 | 核心作用 | 核心限制 |
|---|---|---|
| 基本数据类型变量 | 定义常量 | 赋值后值不可修改 |
| 引用数据类型变量 | 固定引用地址 | 地址不可变,对象内部内容可修改 |
| 方法 | 保护方法逻辑 | 可继承调用,不可被子类重写 |
| 类 | 禁止类被继承 | 无法有子类,类中所有方法默认不可重写 |
二、String类的核心本质与不可变特性
2.1 String的底层结构
String的底层存储结构,在JDK版本中有明确的迭代:
- JDK1.8及之前:String的本质是一个
private final char[] value字符数组,字符串的每个字符都存在这个char数组中。 - JDK1.9及之后:String的底层优化为
private final byte[] value字节数组,核心原因是char类型占2字节,而大部分字符串是Latin1字符,仅需1字节存储,用byte数组可以大幅节省内存空间。
2.2 String的核心特性1:不可变性
String是Java中最典型的不可变对象,一旦一个String对象被创建,其内容永远无法被修改,所有看似修改字符串的操作,本质都是创建了一个全新的String对象。
不可变的底层原理
不可变性的核心来自于两点:
- final修饰数组:底层存储数组
value被final修饰,固定了数组的内存引用地址,无法指向新的数组。 - private修饰数组:底层数组被private私有化,String类外部无法直接访问和修改数组内容,且String类没有对外提供任何修改数组元素的public方法。
不可变带来的问题:频繁拼接效率极低
课堂示例中,字符串的+=拼接,每次都会创建新对象,频繁操作会大量消耗内存,降低程序性能。
public class StringDemo {
public static void main(String[] args) {
String a = "abc";
a += 'd'; // 创建新对象"abcd",a指向新地址
a += 'e'; // 再次创建新对象"abcde",a再次指向新地址
System.out.println(a); // 输出abcde
// 整个过程创建了3个字符串对象,原有的"abc"、"abcd"都会成为垃圾,等待GC回收
}
}
不可变的核心好处
- 线程安全:多线程环境下,字符串对象的内容不会被修改,不存在并发修改的线程安全问题,无需加锁即可直接共享使用。
- 字符串常量池复用:正是因为不可变性,相同内容的字符串可以在字符串常量池中复用同一块内存对象,大幅节省内存空间。
- 哈希值缓存:String的hashCode值只会在第一次调用时计算并缓存,后续直接复用,作为HashMap的key时性能更高。
2.3 String的核心特性2:字符串常量池
字符串常量池是JVM中专门用于存储字符串常量的内存区域,核心目的是复用相同内容的字符串,避免重复创建对象,节省内存。
两种字符串创建方式的核心区别
| 创建方式 | 内存分配规则 | 创建对象数量 |
|---|---|---|
String s = "abc"; 直接字面量赋值 |
优先检查常量池是否存在"abc":存在则直接复用常量池对象;不存在则在常量池创建对象,再返回引用 | 最多1个(常量池中) |
String s = new String("abc"); 构造方法创建 |
1. 先检查常量池是否存在"abc",不存在则先在常量池创建;2. 无论常量池是否存在,都会在堆中创建一个全新的String对象,最终返回堆中对象的引用 | 最多2个(常量池1个+堆中1个) |
代码示例与执行原理
public class StringPoolDemo {
public static void main(String[] args) {
// 1. 字面量赋值:常量池创建"abc",c指向常量池地址
String c = "abc";
// 2. 字面量赋值:常量池已存在"abc",直接复用,b和c指向同一个地址
String b = "abc";
// 3. 构造方法创建:堆中创建新对象,a指向堆中地址,复用常量池已有的"abc"
String a = new String("abc");
System.out.println(b == c); // 输出true:同一个常量池地址
System.out.println(a == c); // 输出false:a是堆中地址,c是常量池地址
}
}
编译优化规则
// 常量直接拼接:编译器会直接优化为 String a = "abcdef";
String a = "abc" + "def";
// 变量拼接:无法编译优化,会创建StringBuilder完成拼接,最终生成新对象
String b = "abc";
b += "def";
2.4 String的核心特性3:不可被继承
String类本身被final修饰,因此无法被任何类继承,没有子类,保证了所有Java程序中String类的行为是统一的,不会被子类修改不可变特性。
三、String、StringBuffer、StringBuilder 核心对比
针对String不可变导致的拼接效率低的问题,Java提供了两个可变的字符串操作类:StringBuffer和StringBuilder,三者的核心区别与选型如下:
| 对比维度 | String | StringBuffer | StringBuilder |
|---|---|---|---|
| 可变性 | 不可变,每次修改都会创建新对象 | 可变,底层是可扩容的数组,修改无需创建新对象 | 可变,底层是可扩容的数组,修改无需创建新对象 |
| 线程安全 | 安全(不可变对象,无并发修改风险) | 安全(所有方法都加了synchronized互斥锁) |
不安全(无锁,无同步控制) |
| 执行效率 | 最低(频繁创建新对象,产生内存垃圾) | 中等(加锁、释放锁有性能消耗) | 最高(无锁开销,单线程场景下性能最优) |
| 适用场景 | 字符串内容极少修改、固定不变的场景,如常量定义、少量拼接 | 多线程环境下,频繁进行字符串拼接、修改的场景,如日志拼接、多线程参数拼接 | 单线程环境下,频繁进行字符串拼接、修改的场景,如循环内拼接、SQL语句拼接 |
课堂代码示例:StringBuffer的可变操作
public class StringBufferDemo {
public static void main(String[] args) {
StringBuffer b = new StringBuffer("abc");
// 拼接:直接修改底层数组,不创建新对象
b.append("d");
b.append("e");
// 替换:直接修改数组内容
b.replace(0,2,"AA");
System.out.println(b); // 输出:AAcde
}
}
补充:StringBuilder的API和StringBuffer完全一致,仅区别在于有无synchronized锁,单线程场景下优先使用StringBuilder。
四、== 和 equals() 的核心区别
这是Java入门的核心考点,也是字符串比较最容易踩坑的知识点,二者的核心区别如下:
4.1 == 比较运算符
- 作用于基本数据类型:比较的是变量存储的数据值本身是否相等。
- 作用于引用数据类型:比较的是变量存储的对象内存引用地址是否相等,也就是判断两个引用是否指向堆中的同一个对象。
4.2 equals() 方法
equals()是Object类中定义的方法,所有引用数据类型都能调用,基本数据类型无法使用equals()。
- Object类中的默认实现:和
==完全一致,比较两个对象的内存地址是否相同。 - 重写后的实现:大部分类(如String、Integer等)都会重写equals()方法,改为比较对象的实际内容是否相等,而非地址。
String类的equals()执行逻辑
- 先比较两个字符串的地址是否相同,如果相同,直接返回true(同一个对象,内容必然相同)。
- 如果地址不同,先判断类型是否为String,再逐字符比较两个字符串的内容,内容完全一致则返回true,否则返回false。
代码示例:字符串比较避坑
public class EqualsDemo {
public static void main(String[] args) {
String s1 = "hello";
String s2 = "hello";
String s3 = new String("hello");
String s4 = new String("hello");
// == 比较地址
System.out.println(s1 == s2); // true:同一个常量池地址
System.out.println(s1 == s3); // false:s1是常量池地址,s3是堆中地址
System.out.println(s3 == s4); // false:两个不同的堆中对象,地址不同
// equals() 比较内容
System.out.println(s1.equals(s2)); // true:内容相同
System.out.println(s1.equals(s3)); // true:内容相同
System.out.println(s3.equals(s4)); // true:内容相同
// 新手避坑:字符串比较内容,永远优先使用equals(),而非==
}
}
五、String类的常用方法
String类提供了丰富的字符串操作方法,是日常开发和算法题的核心基础,课堂中讲解的常用方法整理如下,修正了语法错误并补充使用说明:
import java.util.Arrays;
public class StringMethodDemo {
public static void main(String[] args) {
String str = "heLlo worLd";
// 1. charAt(int index):获取字符串指定索引位置的字符,索引从0开始
char firstChar = str.charAt(0);
System.out.println(firstChar); // 输出:h
// 2. length():获取字符串的长度(字符个数)
int length = str.length();
System.out.println(length); // 输出:11
// 3. split(String regex):按照指定规则切割字符串,返回字符串数组
// 按空格切割
String[] splitBySpace = str.split(" ");
System.out.println(Arrays.toString(splitBySpace)); // 输出:[heLlo, worLd]
// 按空字符切割,拆分每个字符
String[] splitByEmpty = str.split("");
System.out.println(Arrays.toString(splitByEmpty)); // 输出:[h, e, L, l, o, , w, o, r, L, d]
// 4. equals(Object anObject):比较两个字符串的内容是否相等,区分大小写
String str1 = "heLlo worLd";
String str2 = "hello world";
boolean isEquals = str1.equals(str2);
System.out.println(isEquals); // 输出:false
// 补充:equalsIgnoreCase() 忽略大小写比较内容
boolean isEqualsIgnoreCase = str1.equalsIgnoreCase(str2);
System.out.println(isEqualsIgnoreCase); // 输出:true
// 5. replace(CharSequence target, CharSequence replacement):字符串替换
// 将所有的"L"替换为"l"
String replaceStr = str.replace("L", "l");
System.out.println(replaceStr); // 输出:hello world
// 6. substring(int beginIndex, int endIndex):截取字符串,左闭右开区间[begin, end)
// 截取索引1到6的字符(包含1,不包含7)
String substring = str.substring(1, 7);
System.out.println(substring); // 输出:eLlo w
// 7. getBytes():将字符串转换为字节数组,常用于网络IO、文件IO
byte[] bytes = str.getBytes();
System.out.println(Arrays.toString(bytes)); // 输出字符串对应的ASCII字节数组
// 8. reverse():字符串翻转,需借助StringBuilder/StringBuffer
StringBuilder stringBuilder = new StringBuilder(str);
stringBuilder.reverse();
System.out.println(stringBuilder); // 输出:dLrow olLeh
}
}
六、新手避坑指南与核心总结
6.1 新手高频避坑点
- 字符串比较永远用equals(),不用==:只有判断两个引用是否指向同一个对象时,才用==,比较内容必须用equals()。
- 循环内字符串拼接禁止用String:循环内频繁+=拼接字符串,会创建大量临时对象,严重降低性能,必须用StringBuilder/StringBuffer。
- final修饰引用类型仅固定地址:不要误以为final修饰数组、对象后,内部内容也不能修改,仅地址不可变,内容可正常修改。
- String的所有修改方法都是返回新对象:replace()、substring()、trim()等方法,都不会修改原字符串,而是返回一个新的字符串对象,必须接收返回值才能生效。
6.2 核心知识点总结
- final关键字:核心是不可变,修饰变量固定值/地址,修饰方法禁止重写,修饰类禁止继承。
- String不可变性:底层是final修饰的私有数组,地址不可变、外部无法修改内容,所有修改操作都会创建新对象;不可变带来了线程安全、常量池复用的好处,也导致频繁拼接效率低。
- 字符串常量池:字面量赋值会优先复用常量池对象,new String()一定会在堆中创建新对象,同时可能在常量池创建对象。
- 可变字符串选型:单线程高频拼接用StringBuilder,多线程场景用StringBuffer,极少修改的固定字符串用String。
- ==与equals():基本类型比数值,引用类型比地址;String重写了equals(),用于比较字符串内容是否相等。
更多推荐



所有评论(0)