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修饰全局成员变量:必须在对象创建完成前完成赋值,仅支持三种合法赋值方式,否则编译报错:
    1. 声明变量时直接赋值(最常用)
    2. 在构造方法中完成赋值
    3. 在非静态代码块中完成赋值
代码示例:全局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对象。

不可变的底层原理

不可变性的核心来自于两点:

  1. final修饰数组:底层存储数组value被final修饰,固定了数组的内存引用地址,无法指向新的数组。
  2. 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回收
    }
}
不可变的核心好处
  1. 线程安全:多线程环境下,字符串对象的内容不会被修改,不存在并发修改的线程安全问题,无需加锁即可直接共享使用。
  2. 字符串常量池复用:正是因为不可变性,相同内容的字符串可以在字符串常量池中复用同一块内存对象,大幅节省内存空间。
  3. 哈希值缓存: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提供了两个可变的字符串操作类:StringBufferStringBuilder,三者的核心区别与选型如下:

对比维度 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()执行逻辑
  1. 先比较两个字符串的地址是否相同,如果相同,直接返回true(同一个对象,内容必然相同)。
  2. 如果地址不同,先判断类型是否为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 新手高频避坑点

  1. 字符串比较永远用equals(),不用==:只有判断两个引用是否指向同一个对象时,才用==,比较内容必须用equals()。
  2. 循环内字符串拼接禁止用String:循环内频繁+=拼接字符串,会创建大量临时对象,严重降低性能,必须用StringBuilder/StringBuffer。
  3. final修饰引用类型仅固定地址:不要误以为final修饰数组、对象后,内部内容也不能修改,仅地址不可变,内容可正常修改。
  4. String的所有修改方法都是返回新对象:replace()、substring()、trim()等方法,都不会修改原字符串,而是返回一个新的字符串对象,必须接收返回值才能生效。

6.2 核心知识点总结

  1. final关键字:核心是不可变,修饰变量固定值/地址,修饰方法禁止重写,修饰类禁止继承。
  2. String不可变性:底层是final修饰的私有数组,地址不可变、外部无法修改内容,所有修改操作都会创建新对象;不可变带来了线程安全、常量池复用的好处,也导致频繁拼接效率低。
  3. 字符串常量池:字面量赋值会优先复用常量池对象,new String()一定会在堆中创建新对象,同时可能在常量池创建对象。
  4. 可变字符串选型:单线程高频拼接用StringBuilder,多线程场景用StringBuffer,极少修改的固定字符串用String。
  5. ==与equals():基本类型比数值,引用类型比地址;String重写了equals(),用于比较字符串内容是否相等。
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐