一、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 修饰方法 —— 保护核心逻辑

核心规则(背诵)

  1. final 修饰的方法,可以被继承、可以被调用,但 不能被子类重写
  2. 作用:保护类的核心业务逻辑,防止子类篡改(比如工具类的核心方法);
  3. 注意: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 修饰类 —— 杜绝继承

核心规则(背诵)

  1. final 修饰的类,不能被继承,没有子类;
  2. 类中的所有方法,会自动变为 final 方法(无需手动添加),但成员变量不会自动变为 final;
  3. 典型案例:Java 中的 StringIntegerStringBufferMath 等核心类,都是 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 关键字总结(必背)

  1. 修饰变量:基本类型值不可改,引用类型地址不可改、内容可改;
  2. 修饰方法:可继承、可调用,不可重写,可重载;
  3. 修饰类:不可继承,内部方法自动为 final;
  4. 核心作用:保证数据安全、保护核心逻辑、提升代码可读性。

二、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 对象。

不可变性的双重保障(底层原理,必背)

  1. 类被 final 修饰:禁止继承,防止子类重写方法破坏不可变性;
  2. 底层数组 valueprivate + final 修饰:
    • private:外部无法直接访问数组,不能通过 “对象。属性” 修改元素;
    • final:数组的内存地址不可变,不能指向新的数组。

不可变性的核心优势(面试必答)

  1. 线程安全:多线程环境下,无需加锁,可直接使用(字符串内容不会被篡改);
  2. 字符串常量池复用:相同内容的字符串,在常量池中只存储一份,极大节省内存;
  3. 哈希值稳定:字符串内容不变,哈希值(hashCode)也不变,适合作为 HashMap 的键;
  4. 安全性:避免字符串被恶意篡改(如密码、路径等核心字符串)。

例题:验证 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";

底层逻辑

  1. 先去 字符串常量池(方法区中,专门存储字符串常量)中查找是否有 "abc";
  2. 若有,直接让变量 s 指向常量池中的 "abc";
  3. 若没有,在常量池中创建 "abc",再让 s 指向它;
  4. 最终:只创建 1 个对象(常量池中的对象)。

方式 2:new 关键字创建

语法:String s = new String("abc");

底层逻辑

  1. 先在 堆内存 中创建一个 String 对象(存储 "abc");
  2. 再去字符串常量池中查找是否有 "abc";
    • 若有,堆对象的 value 数组指向常量池中的数组;
    • 若没有,先在常量池中创建 "abc",再让堆对象指向它;
  3. 最终:最多创建 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 关键字总结

  1. 修饰变量:基本类型值不可改,引用类型地址不可改、内容可改;成员变量三选一赋值,局部变量使用前赋值。
  2. 修饰方法:可继承、可调用、不可重写,可重载。
  3. 修饰类:不可继承,内部方法自动为 final。

String 类总结

  1. 底层:JDK8-char [],JDK9+byte [],都是 private final,保证不可变性。
  2. 不可变性:内容不可改,修改本质是创建新对象,优势是线程安全、常量池复用。
  3. 创建方式:直接赋值(常量池,1 个对象),new 创建(堆 + 常量池,最多 2 个对象)。
  4. 拼接:少量拼接用 +,大量拼接用 StringBuilder(单线程)/StringBuffer(多线程)。
  5. 比较:== 比地址,equals 比内容;null 不能调用 equals。
  6. 常用 API:记住截取、替换、分割、转换方法,结合例题熟练使用。

 

更多推荐