Java:final 关键字 + String 不可变性 + 字符串拼接
一、final 关键字
前置知识(必看)
Java 数据类型分类,直接决定 final 修饰后的特性:
- 基本数据类型:byte/short/int/long/float/double/char/boolean,直接存储数据值(栈内存);
- 引用数据类型:类、接口、数组、字符串等,存储对象的内存地址(栈存地址,堆存对象)。
1. final 修饰变量(常量)—— 最常用、陷阱最多
核心规则(背诵)
- 基本数据类型:赋值后值不可修改(值本身固定);
- 引用数据类型:引用地址不可修改(不能指向新对象),但 对象内部内容可修改(堆中对象属性可改)。
赋值规则(必须遵守,否则编译报错)
| 变量类型 | 赋值要求 | 示例 |
|---|---|---|
| 成员变量(全局) | 三选一:声明时赋值 / 构造方法赋值 / 静态代码块(static final)/ 实例代码块赋值 | private final int NUM = 10;(声明赋值) |
| 局部变量(方法内) | 可先声明,使用前必须赋值,赋值后不可改 | final String name; name = "Java"; |
| 静态常量(static final) | 必须在声明时 / 静态代码块赋值(全局唯一,类加载时初始化) | public static final String URL = "www.xxx.com"; |
海量经典例题(覆盖所有场景)
例题 1:final 修饰基本类型变量(成员 + 局部)
public class FinalVarDemo1 {
// 1. 成员变量:声明时赋值(推荐)
private final int AGE = 20;
// 2. 成员变量:实例代码块赋值
private final String NAME;
{
NAME = "张三"; // 代码块赋值,合法
}
// 3. 静态常量:静态代码块赋值
public static final String SCHOOL;
static {
SCHOOL = "北京大学";
}
public void testLocal() {
// 局部变量:先声明,使用前赋值
final double SCORE;
SCORE = 98.5;
// SCORE = 99.0; ❌ 编译报错,赋值后不可改
// 基本类型常量,值不可变
// AGE = 25; ❌ 报错
System.out.println(AGE); // 20
}
public static void main(String[] args) {
FinalVarDemo1 demo = new FinalVarDemo1();
demo.testLocal();
System.out.println(SCHOOL); // 北京大学
}
}
例题 2:final 修饰引用类型(数组 + 对象)
class Student {
private String name;
private int age;
// getter/setter
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
public class FinalVarDemo2 {
// 1. final 修饰数组(引用类型)
private final int[] ARR = {1, 2, 3, 4};
// 2. final 修饰对象(引用类型)
private final Student STU = new Student();
public void testRef() {
// 错误1:修改引用地址(数组)
// ARR = new int[5]; ❌ 编译报错,地址不可改
// 正确:修改数组内部元素
ARR[0] = 100;
System.out.println(ARR[0]); // 100(正常运行)
// 错误2:修改引用地址(对象)
// STU = new Student(); ❌ 报错
// 正确:修改对象内部属性
STU.setName("李四");
STU.setAge(22);
System.out.println(STU.getName() + ":" + STU.getAge()); // 李四:22
}
public static void main(String[] args) {
new FinalVarDemo2().testRef();
}
}
例题 3:final 易错陷阱(局部变量 vs 成员变量)
public class FinalErrorDemo {
// 错误1:成员变量未赋值(未满足三选一)
// private final int NUM; ❌ 编译报错
// 错误2:局部变量未赋值就使用
public void test() {
final String str;
// System.out.println(str); ❌ 报错,未赋值
str = "test"; // 赋值后可使用
}
// 错误3:静态常量用实例代码块赋值
// public static final String TEST;
// { TEST = "test"; } ❌ 报错,静态常量只能用静态代码块/声明时赋值
}
2. final 修饰方法 —— 保护核心逻辑
核心规则(背诵)
- 被
final修饰的方法,可以被继承、可以被调用,但 不能被子类重写; - 作用:保护类的核心业务逻辑,防止子类篡改(比如工具类的核心方法);
- 注意:
final方法不能和abstract方法共存(abstract 要求重写,final 禁止重写)。
经典例题(含易错点)
例题 1:final 方法不能被重写
class Parent {
// final 修饰核心方法
public final void coreMethod() {
System.out.println("父类核心逻辑,不能被篡改");
}
// 普通方法,可被重写
public void commonMethod() {
System.out.println("父类普通方法");
}
}
class Child extends Parent {
// 错误:重写final方法
// @Override
// public void coreMethod() {} ❌ 编译报错
// 正确:重写普通方法
@Override
public void commonMethod() {
System.out.println("子类重写普通方法");
}
}
public class FinalMethodDemo1 {
public static void main(String[] args) {
Child child = new Child();
child.coreMethod(); // 继承父类final方法,正常调用
child.commonMethod(); // 调用重写后的普通方法
}
}
例题 2:final 方法可以被重载
class MathUtil {
// final 方法
public final int add(int a, int b) {
return a + b;
}
// 正确:final方法可以被重载(同一类中,同名不同参)
public final int add(int a, int b, int c) {
return a + b + c;
}
}
3. final 修饰类 —— 杜绝继承
核心规则(背诵)
- 被
final修饰的类,不能被继承,没有子类; - 类中的所有方法,会自动变为 final 方法(无需手动添加),但成员变量不会自动变为 final;
- 典型案例:Java 中的
String、Integer、StringBuffer、Math等核心类,都是 final 类(防止被篡改)。
经典例题
例题 1:final 类不能被继承
// final 类
final class FinalClass {
public void show() {
System.out.println("final类的方法");
}
}
// 错误:继承final类
// class SubClass extends FinalClass {} ❌ 编译报错
例题 2:final 类的方法自动为 final
final class FinalDemoClass {
// 无需手动加final,方法自动为final
public void test() {
System.out.println("自动final方法");
}
}
// 无法继承,自然无法重写方法
// class Sub extends FinalDemoClass {
// @Override
// public void test() {} ❌ 报错
// }
4. final 关键字总结(必背)
- 修饰变量:基本类型值不可改,引用类型地址不可改、内容可改;
- 修饰方法:可继承、可调用,不可重写,可重载;
- 修饰类:不可继承,内部方法自动为 final;
- 核心作用:保证数据安全、保护核心逻辑、提升代码可读性。
二、String 类(Java 最核心、最常用类,面试重中之重)
1. String 类底层数据结构演进(面试高频)
String 底层存储的是字符 / 字节数组,JDK 版本不同,结构不同,核心是为了节省内存、提升性能。
| JDK 版本 | 底层存储结构 | 核心说明 | 优势 |
|---|---|---|---|
| JDK 1.8 及以前 | private final char value[] |
用 char 数组存储,每个 char 占 2 字节 | 兼容所有 Unicode 字符,无编码问题 |
| JDK 1.9 及以后 | private final byte value[] |
用 byte 数组存储,每个 byte 占 1 字节 | 针对 ASCII 字符(占 1 字节)节省一半内存,提升字符串操作效率 |
关键补充
- 无论 JDK 版本如何,
value数组都被private + final修饰,这是 String 不可变性的核心; - JDK 9 优化的核心原因:大部分字符串都是 ASCII 字符(如英文、数字),用 byte 数组存储更高效。
2. String 不可变性(面试核心考点)
什么是不可变性?
String 对象创建后,其内容(底层数组的元素)不能被修改,看似修改(如拼接、替换),本质是创建了新的 String 对象。
不可变性的双重保障(底层原理,必背)
- 类被
final修饰:禁止继承,防止子类重写方法破坏不可变性; - 底层数组
value被private + final修饰:private:外部无法直接访问数组,不能通过 “对象。属性” 修改元素;final:数组的内存地址不可变,不能指向新的数组。
不可变性的核心优势(面试必答)
- 线程安全:多线程环境下,无需加锁,可直接使用(字符串内容不会被篡改);
- 字符串常量池复用:相同内容的字符串,在常量池中只存储一份,极大节省内存;
- 哈希值稳定:字符串内容不变,哈希值(hashCode)也不变,适合作为 HashMap 的键;
- 安全性:避免字符串被恶意篡改(如密码、路径等核心字符串)。
例题:验证 String 不可变性
public class StringImmutableDemo {
public static void main(String[] args) {
String str = "hello";
// 看似修改,实则创建新对象
String newStr = str.replace('l', 'x');
System.out.println(str); // hello(原对象内容未变)
System.out.println(newStr); // hexxo(新对象)
// 拼接也是创建新对象
String str2 = str + "java";
System.out.println(str); // hello(原对象仍未变)
System.out.println(str2); // hellojava(新对象)
}
}
3. 字符串创建机制(面试必考题,含陷阱)
String 有两种创建方式,底层逻辑完全不同,直接决定内存占用和 == 比较结果。
方式 1:直接赋值(推荐)
语法:String s = "abc";
底层逻辑
- 先去 字符串常量池(方法区中,专门存储字符串常量)中查找是否有 "abc";
- 若有,直接让变量 s 指向常量池中的 "abc";
- 若没有,在常量池中创建 "abc",再让 s 指向它;
- 最终:只创建 1 个对象(常量池中的对象)。
方式 2:new 关键字创建
语法:String s = new String("abc");
底层逻辑
- 先在 堆内存 中创建一个 String 对象(存储 "abc");
- 再去字符串常量池中查找是否有 "abc";
- 若有,堆对象的 value 数组指向常量池中的数组;
- 若没有,先在常量池中创建 "abc",再让堆对象指向它;
- 最终:最多创建 2 个对象(堆中 1 个 + 常量池中 1 个),最少创建 1 个对象(堆中 1 个,常量池已有)。
海量对比例题(覆盖所有陷阱)
例题 1:直接赋值 vs new 创建(== 比较)
public class StringCreateDemo1 {
public static void main(String[] args) {
// 直接赋值:s1、s2 都指向常量池中的 "abc"
String s1 = "abc";
String s2 = "abc";
// new 创建:s3 指向堆对象,s4 指向另一个堆对象
String s3 = new String("abc");
String s4 = new String("abc");
// == 比较地址
System.out.println(s1 == s2); // true(地址相同,都指向常量池)
System.out.println(s1 == s3); // false(s1指向常量池,s3指向堆)
System.out.println(s3 == s4); // false(两个不同的堆对象,地址不同)
// equals 比较内容(String重写了equals)
System.out.println(s1.equals(s3)); // true(内容相同)
System.out.println(s3.equals(s4)); // true(内容相同)
}
}
例题 2:字符串拼接与创建机制
public class StringCreateDemo2 {
public static void main(String[] args) {
// 1. 纯字面量拼接:编译期优化,直接生成 "abcdef",只在常量池创建1个对象
String s1 = "abc" + "def";
String s2 = "abcdef";
System.out.println(s1 == s2); // true
// 2. 变量 + 字面量拼接:创建新的堆对象,不优化
String s3 = "abc";
String s4 = s3 + "def";
System.out.println(s4 == s2); // false(s4指向堆,s2指向常量池)
// 3. final变量拼接:编译期优化(final变量值固定,视为字面量)
final String s5 = "abc";
String s6 = s5 + "def";
System.out.println(s6 == s2); // true
}
}
例题 3:intern () 方法(手动入池)
public class StringInternDemo {
public static void main(String[] args) {
// new 创建:堆中有对象,常量池中有 "abc"
String s1 = new String("abc");
// intern():将堆对象的内容入池(若常量池已有,直接返回常量池地址)
String s2 = s1.intern();
String s3 = "abc";
System.out.println(s1 == s3); // false(s1堆,s3常量池)
System.out.println(s2 == s3); // true(s2指向常量池)
}
}
4. 字符串拼接(开发高频,效率对比)
由于 String 不可变性,每次拼接都会创建新对象,大量拼接时效率极低,因此需要使用可变字符串类。
1. String 拼接的效率问题(例题验证)
public class StringConcatDemo1 {
public static void main(String[] args) {
// 大量拼接:效率极低,创建大量临时对象
String str = "";
long start = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
str += i; // 每次拼接都创建新对象
}
long end = System.currentTimeMillis();
System.out.println("String拼接耗时:" + (end - start) + "ms"); // 耗时较长
}
}
2. 可变字符串类对比(核心,必背)
Java 提供两个可变字符串类,用于大量拼接,解决 String 效率问题,重点区分线程安全。
| 类名 | 线程安全 | 底层结构 | 效率 | 核心方法 | 使用场景 |
|---|---|---|---|---|---|
| StringBuilder | 非线程安全 | char[](JDK8)/ byte[](JDK9+) | 最高 | append()、insert()、delete()、toString() | 单线程环境(开发首选,如普通业务逻辑、单线程拼接) |
| StringBuffer | 线程安全 | 同 StringBuilder | 较低(加了 synchronized 锁) | 与 StringBuilder 一致 | 多线程环境(如多线程日志拼接、并发字符串操作) |
3. 可变字符串实战例题(覆盖常用方法)
例题 1:StringBuilder 基础使用(单线程首选)
public class StringBuilderDemo {
public static void main(String[] args) {
// 1. 创建对象(可指定初始容量,避免扩容,提升效率)
StringBuilder sb = new StringBuilder(100); // 初始容量100
// 2. 拼接(append(),支持所有数据类型)
sb.append("Hello");
sb.append(" ");
sb.append("Java");
sb.append(2026);
sb.append(true);
// 3. 插入(insert(下标, 内容))
sb.insert(5, ","); // 在第5个下标插入逗号
// 4. 删除(delete(开始下标, 结束下标),左闭右开)
sb.delete(6, 7); // 删除第6个下标(空格)
// 5. 反转
sb.reverse();
// 6. 转为String(不可变)
String result = sb.toString();
System.out.println(result); // aJ,olleH6202eurt
}
}
例题 2:效率对比(String vs StringBuilder vs StringBuffer)
public class ConcatEfficiencyDemo {
public static void main(String[] args) {
// 1. String 拼接
String str = "";
long start1 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
str += i;
}
long end1 = System.currentTimeMillis();
// 2. StringBuilder 拼接
StringBuilder sb = new StringBuilder();
long start2 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
sb.append(i);
}
long end2 = System.currentTimeMillis();
// 3. StringBuffer 拼接
StringBuffer sb2 = new StringBuffer();
long start3 = System.currentTimeMillis();
for (int i = 0; i < 10000; i++) {
sb2.append(i);
}
long end3 = System.currentTimeMillis();
System.out.println("String拼接耗时:" + (end1 - start1) + "ms");
System.out.println("StringBuilder拼接耗时:" + (end2 - start2) + "ms");
System.out.println("StringBuffer拼接耗时:" + (end3 - start3) + "ms");
// 输出结果:String > StringBuffer > StringBuilder(效率)
}
}
5. == 与 equals 的区别
String 中 == 和 equals 的区别,是面试高频题,结合创建机制理解,避免踩坑。
核心区别(必背表格)
| 比较方式 | 基本数据类型 | 引用数据类型(如 String) |
|---|---|---|
== |
比较 数值 是否相等 | 比较 内存地址 是否相等(是否指向同一个对象) |
equals() |
不适用(基本类型没有方法) | 1. Object 类:默认比较地址;2. String 类:重写后,比较 字符串内容 是否相等 |
海量对比例题(覆盖所有场景)
例题 1:基本场景(直接赋值 vs new)
public class EqualsDemo1 {
public static void main(String[] args) {
String s1 = "abc";
String s2 = "abc";
String s3 = new String("abc");
System.out.println(s1 == s2); // true(地址相同)
System.out.println(s1 == s3); // false(地址不同)
System.out.println(s1.equals(s2)); // true(内容相同)
System.out.println(s1.equals(s3)); // true(内容相同)
}
}
例题 2:拼接后的 == 对比
public class EqualsDemo2 {
public static void main(String[] args) {
String s1 = "a" + "b";
String s2 = "ab";
String s3 = "a";
String s4 = s3 + "b";
System.out.println(s1 == s2); // true(编译期优化,地址相同)
System.out.println(s4 == s2); // false(s4是堆对象,s2是常量池对象)
System.out.println(s4.equals(s2)); // true(内容相同)
}
}
例题 3:null 与空字符串对比
public class EqualsDemo3 {
public static void main(String[] args) {
String s1 = null;
String s2 = "";
String s3 = " ";
// 注意:null调用equals会报空指针异常
// System.out.println(s1.equals(s2)); ❌ 报错
// 正确写法:先判空
System.out.println(s1 == null); // true
System.out.println(s2.isEmpty()); // true(空字符串,长度为0)
System.out.println(s3.isEmpty()); // false(有空格,长度为1)
System.out.println(s2.equals(s3)); // false(内容不同)
}
}
6. String 常用 API
String 提供了大量常用方法,覆盖字符串查询、处理、转换等场景,每个方法配例题,直接套用。
一、基础查询方法
public class StringApiDemo1 {
public static void main(String[] args) {
String str = "HelloJava2026";
// 1. 获取字符串长度:length()
int len = str.length();
System.out.println("长度:" + len); // 11
// 2. 获取指定下标字符:charAt(下标)(下标从0开始)
char c1 = str.charAt(0);
char c2 = str.charAt(5);
System.out.println(c1); // H
System.out.println(c2); // J
// 3. 获取字符的下标:indexOf(字符/字符串)(从左找,找不到返回-1)
int index1 = str.indexOf('J');
int index2 = str.indexOf("Java");
int index3 = str.indexOf('x');
System.out.println(index1); // 5
System.out.println(index2); // 5
System.out.println(index3); // -1
// 4. 从指定下标开始找:indexOf(字符, 起始下标)
int index4 = str.indexOf('a', 6);
System.out.println(index4); // 7
// 5. 从右找下标:lastIndexOf(字符/字符串)
int index5 = str.lastIndexOf('a');
System.out.println(index5); // 7
}
}
二、字符串处理方法(截取、替换、分割)
public class StringApiDemo2 {
public static void main(String[] args) {
String str = "Hello,Java,2026";
// 1. 截取字符串:substring(起始下标) / substring(起始下标, 结束下标)(左闭右开)
String sub1 = str.substring(5); // 从下标5开始,截取到末尾
String sub2 = str.substring(0, 5); // 截取0~4下标
System.out.println(sub1); // ,Java,2026
System.out.println(sub2); // Hello
// 2. 替换字符/字符串:replace(旧, 新)
String replace1 = str.replace(',', '-'); // 替换字符
String replace2 = str.replace("Java", "Python"); // 替换字符串
System.out.println(replace1); // Hello-Java-2026
System.out.println(replace2); // Hello,Python,2026
// 3. 分割字符串:split(分隔符)(返回String数组)
String[] arr = str.split(","); // 用逗号分割
for (String s : arr) {
System.out.print(s + " "); // Hello Java 2026
}
System.out.println();
// 4. 去空格:trim()(去掉前后空格,中间空格保留)
String str2 = " Hello Java ";
String trimStr = str2.trim();
System.out.println(trimStr); // Hello Java
}
}
三、判断方法
public class StringApiDemo3 {
public static void main(String[] args) {
String str1 = "Hello.java";
String str2 = "HELLO.JAVA";
String str3 = "";
String str4 = " ";
// 1. 判断是否以指定内容开头:startsWith(前缀)
boolean b1 = str1.startsWith("He");
System.out.println(b1); // true
// 2. 判断是否以指定内容结尾:endsWith(后缀)
boolean b2 = str1.endsWith(".java");
System.out.println(b2); // true
// 3. 判断是否包含指定子串:contains(子串)
boolean b3 = str1.contains("Java");
System.out.println(b3); // true
// 4. 判断是否为空字符串:isEmpty()(长度为0才返回true)
boolean b4 = str3.isEmpty();
boolean b5 = str4.isEmpty();
System.out.println(b4); // true
System.out.println(b5); // false(空格字符串,长度不为0)
// 5. 忽略大小写比较内容:equalsIgnoreCase(字符串)
boolean b6 = str1.equalsIgnoreCase(str2);
System.out.println(b6); // true
}
}
四、转换方法(与基本类型互转、大小写转换)
public class StringApiDemo4 {
public static void main(String[] args) {
// 1. 大小写转换
String str = "HelloJava";
String lower = str.toLowerCase(); // 转小写
String upper = str.toUpperCase(); // 转大写
System.out.println(lower); // hellojava
System.out.println(upper); // HELLOJAVA
// 2. 字符串 → 基本类型(重点)
String numStr = "123";
int num1 = Integer.parseInt(numStr);
double num2 = Double.parseDouble(numStr);
boolean flag = Boolean.parseBoolean("true");
System.out.println(num1 + 1); // 124
System.out.println(num2 + 0.5); // 123.5
System.out.println(flag); // true
// 3. 基本类型 → 字符串(重点)
int a = 456;
double b = 3.14;
String s1 = String.valueOf(a);
String s2 = String.valueOf(b);
System.out.println(s1 + "abc"); // 456abc
System.out.println(s2 + "def"); // 3.14def
// 4. 字符串 → 字符数组
char[] charArr = str.toCharArray();
for (char c : charArr) {
System.out.print(c + " "); // H e l l o J a v a
}
}
}
7. String 高频易错陷阱
陷阱 1:String 不可变性,修改本质是创建新对象
String str = "hello";
str += "world";
// 错误认知:str的内容被修改为"helloworld"
// 正确认知:创建了新对象"helloworld",str指向新地址,原"hello"仍在常量池
陷阱 2:new String ("abc") 一定创建 2 个对象?
// 错误:若常量池中已有"abc",则只创建1个堆对象
String s1 = "abc";
String s2 = new String("abc"); // 只创建1个堆对象(常量池已有"abc")
陷阱 3:null 调用 equals 报空指针
String s = null;
// s.equals("abc"); ❌ 报错
// 正确写法:先判空
if (s != null && s.equals("abc")) {
// 逻辑
}
陷阱 4:trim () 去掉所有空格?
String str = " he llo ";
String trimStr = str.trim();
// 错误:trim()只去掉前后空格,中间空格保留
// 结果:"he llo"
陷阱 5:String 是基本类型?
// 错误:String是引用类型,不是基本类型(8种基本类型中没有String)
String s = "abc"; // 引用类型,指向常量池对象
8. 总结
final 关键字总结
- 修饰变量:基本类型值不可改,引用类型地址不可改、内容可改;成员变量三选一赋值,局部变量使用前赋值。
- 修饰方法:可继承、可调用、不可重写,可重载。
- 修饰类:不可继承,内部方法自动为 final。
String 类总结
- 底层:JDK8-char [],JDK9+byte [],都是 private final,保证不可变性。
- 不可变性:内容不可改,修改本质是创建新对象,优势是线程安全、常量池复用。
- 创建方式:直接赋值(常量池,1 个对象),new 创建(堆 + 常量池,最多 2 个对象)。
- 拼接:少量拼接用 +,大量拼接用 StringBuilder(单线程)/StringBuffer(多线程)。
- 比较:== 比地址,equals 比内容;null 不能调用 equals。
- 常用 API:记住截取、替换、分割、转换方法,结合例题熟练使用。
更多推荐

所有评论(0)