Java static 与 final关键字实例详解
作者:J_liaty
本文详细介绍了Java中的static和final关键字,包括它们的本质、内存分配、线程安全问题以及在类加载过程中的内存变化,通过举例和解释,感兴趣的朋友跟随小编一起看看吧
深入理解 Java 中 static 和 final 关键字的本质,掌握类加载过程中的内存变化,以及在多线程环境下的线程安全问题。
核心概念速览
static:属于类而非实例
本质:声明为 static 的成员(变量、方法、代码块、内部类)属于类本身,而非任何对象实例。
内存分配:在类加载时,static 成员分配在方法区(JDK 8+ 为元空间+堆的静态存储区),只有一份副本,所有实例共享。
final:不可变
本质:声明为 final 的内容一旦初始化就不能改变。
适用范围:
- 类:不能被继承(如
String) - 方法:不能被子类重写
- 变量:引用不可变(基本类型值不变,对象引用地址不变)
static final:全局常量
本质:既是类级别的,又是不可变的。通常用于定义编译期常量。
内存优化:编译时常量会被编译器内联到使用处,不会触发类加载。
static 详解
四种使用形式
1. 静态变量(类变量)
public class Counter {
// 静态变量 - 所有实例共享
private static int count = 0;
public Counter() {
count++; // 每创建一个实例,count 加 1
}
public static int getCount() {
return count;
}
}2. 静态常量
public class MathConstants {
// 公开静态常量
public static final double PI = 3.141592653589793;
public static final double E = 2.718281828459045;
// 私有静态常量
private static final int MAX_PRECISION = 100;
}3. 静态代码块
public class DatabaseConfig {
private static Properties config;
// 静态代码块 - 类加载时执行一次
static {
config = new Properties();
try (InputStream input = DatabaseConfig.class.getResourceAsStream("/config.properties")) {
config.load(input);
} catch (IOException e) {
throw new RuntimeException("加载配置文件失败", e);
}
}
public static String get(String key) {
return config.getProperty(key);
}
}4. 静态方法
public class StringUtils {
// 工具类方法,无需创建对象
public static boolean isEmpty(String str) {
return str == null || str.length() == 0;
}
public static String capitalize(String str) {
if (isEmpty(str)) {
return str;
}
return str.substring(0, 1).toUpperCase() + str.substring(1);
}
}5. 静态内部类
public class Outer {
private static int outerStatic = 10;
private int outerInstance = 20;
// 静态内部类
public static class StaticInner {
private int innerVar = 30;
public void show() {
// 可以访问外部类的静态成员
System.out.println("outerStatic = " + outerStatic);
// 不能访问外部类的实例成员
// System.out.println(outerInstance); // 编译错误
}
}
public static void main(String[] args) {
Outer.StaticInner inner = new Outer.StaticInner();
inner.show();
}
}static 的内存特点
public class StaticMemoryDemo {
static int a = 10; // 存储在方法区
int b = 20; // 存储在堆中
public static void main(String[] args) {
StaticMemoryDemo obj1 = new StaticMemoryDemo();
StaticMemoryDemo obj2 = new StaticMemoryDemo();
// 修改静态变量
StaticMemoryDemo.a = 100;
obj1.b = 200;
obj2.b = 300;
System.out.println(obj1.a + ", " + obj1.b); // 100, 200
System.out.println(obj2.a + ", " + obj2.b); // 100, 300
// 修改实例变量不影响静态变量
obj1.a = 1000; // 等价于 StaticMemoryDemo.a = 1000
System.out.println(StaticMemoryDemo.a); // 1000
}
}final 详解
三种使用形式
1. final 类
// final 类不能被继承
public final class String {
// String 类的实现
}
// 编译错误
// public class MyString extends String {}2. final 方法
public class Parent {
// final 方法不能被子类重写
public final void essentialMethod() {
System.out.println("重要方法,不允许修改行为");
}
public void normalMethod() {
System.out.println("普通方法,可以重写");
}
}
public class Child extends Parent {
@Override
public void normalMethod() {
System.out.println("重写普通方法");
}
// 编译错误
// @Override
// public void essentialMethod() {}
}3. final 变量
public class FinalVariableDemo {
// final 实例变量 - 必须初始化
private final int x; // 方式1:构造器初始化
private final int y = 20; // 方式2:声明时初始化
// final 静态变量
private static final int STATIC_FINAL = 30;
// final 实例变量 - 方式3:实例代码块初始化
private final int z;
{
z = 40;
}
public FinalVariableDemo(int x) {
this.x = x;
}
// final 参数 - 参数不可修改
public void process(final int data, final String str) {
// data = 10; // 编译错误
// str = "new"; // 编译错误
System.out.println(data + ", " + str);
}
// final 局部变量
public void localFinal() {
final int local = 100;
// local = 200; // 编译错误
}
}final 的内存安全特性
public class FinalObjectDemo {
public static void main(String[] args) {
// final 对象 - 引用不可变,但内容可变
final List<String> list = new ArrayList<>();
list.add("A"); // ✅ 合法:对象内容可变
list.add("B");
list.remove(0);
// list = new ArrayList<>(); // ❌ 非法:引用不可重新赋值
// final 数组同理
final int[] arr = {1, 2, 3};
arr[0] = 100; // ✅ 合法:数组元素可变
// arr = new int[5]; // ❌ 非法:引用不可变
}
}static final 组合使用
常量定义的最佳实践
/**
* 全局常量定义
*/
public final class GlobalConstants {
// 私有构造器,防止实例化
private GlobalConstants() {}
// 编译期常量 - 编译器会内联
public static final int MAX_CONNECTIONS = 100;
public static final long TIMEOUT_MS = 5000L;
// 运行时常量 - 需要类加载
public static final String APP_VERSION = "1.0.0";
public static final String JAVA_VERSION = System.getProperty("java.version");
// 不可变集合
public static final List<String> SUPPORTED_FORMATS =
Collections.unmodifiableList(Arrays.asList("JSON", "XML", "YAML"));
// 不可变 Map
public static final Map<String, Integer> ERROR_CODES;
static {
Map<String, Integer> codes = new HashMap<>();
codes.put("NOT_FOUND", 404);
codes.put("SERVER_ERROR", 500);
ERROR_CODES = Collections.unmodifiableMap(codes);
}
}常量在内存中的优化
public class ConstantOptimization {
// 编译期常量 - 会被内联
public static final int MAX_SIZE = 100;
// 运行时常量 - 不会内联
public static final String CONFIG_PATH = System.getProperty("config.path");
public static void main(String[] args) {
// 使用编译期常量时,编译器会直接替换为 100
// 不会触发 ConstantOptimization 类的加载
int size = ConstantOptimization.MAX_SIZE;
// 使用运行时常量时,会触发类加载
String path = ConstantOptimization.CONFIG_PATH;
}
}类加载过程中的内存变化
类加载的生命周期
加载 → 链接(验证→准备→解析) → 初始化 → 使用 → 卸载
详细过程解析
阶段 1:加载(Loading)
JVM 读取类的字节码文件(.class),在内存中生成一个代表这个类的 java.lang.Class 对象。
此时内存中:
- 方法区开始准备存储类的运行时数据结构
static final的编译期常量(如MAX_SIZE = 100)不会触发类加载,因为已被编译器内联
阶段 2:链接(Linking)
2.1 验证(Verification)
确保加载的类信息符合 JVM 规范,没有安全风险。
2.2 准备(Preparation)- 关键阶段
为类的静态变量分配内存,并设置默认初始值。
public class PreparationDemo {
// 准备阶段后的状态:
static int a = 10; // a = 0(零值)
static boolean b = true; // b = false(零值)
static char c = 'A'; // c = '\u0000'(零值)
static long d = 100L; // d = 0L(零值)
// 引用类型:null
static String e = "hello"; // e = null(零值)
// static final 编译期常量:直接设置为最终值
static final int f = 20; // f = 20(最终值)
// static final 运行时常量:先设为零值
static final Random rand = new Random();
static final int g = rand.nextInt(100); // g = 0(零值)
}内存分配:
static变量在方法区分配内存,设置零值(0, null, false)static final的编译期常量(如f = 20)直接设置为最终值static final的运行时常量(如g)先设为零值,初始化阶段才赋真值
2.3 解析(Resolution)
将常量池中的符号引用替换为直接引用。
阶段 3:初始化(Initialization)- 真正赋值
执行 <clinit> 方法(类构造器),按代码顺序初始化静态变量和执行静态代码块。
public class InitializationDemo {
static int a = 10; // 第1步:a = 10
static {
System.out.println("静态代码块1: a = " + a); // 第2步:输出 a = 10
a = 20; // 第3步:a = 20
}
static int b = 30; // 第4步:b = 30
static {
System.out.println("静态代码块2: a = " + a + ", b = " + b);
// 第5步:输出 a = 20, b = 30
}
public static void main(String[] args) {
System.out.println("最终值: a = " + a + ", b = " + b);
// 第6步:输出 最终值: a = 20, b = 30
}
}输出结果:
静态代码块1: a = 10
静态代码块2: a = 20, b = 30
最终值: a = 20, b = 30
阶段 4:使用(Using)
public class UsingDemo {
public static void main(String[] args) {
// 主动引用 - 会触发类初始化
int count = Counter.getCount(); // 触发 Counter 类加载
// 被动引用 - 不会触发类初始化
int[] arr = new int[10]; // 不会触发数组类型的父类初始化
// 访问编译期常量 - 不触发类初始化
int max = Constants.MAX_SIZE; // 不触发 Constants 类加载
// 访问运行时常量 - 触发类初始化
String ver = Constants.VERSION; // 触发 Constants 类加载
}
}线程安全问题
static 变量的线程安全问题
问题示例
public class UnsafeCounter {
// 非线程安全的计数器
private static int count = 0;
public static void increment() {
count++; // 非原子操作,存在竞态条件
}
public static int getCount() {
return count;
}
}count++ 的实际操作:
- 读取 count 的值
- 将 count 加 1
- 将新值写回 count
在多线程环境下,这三个步骤可能被交叉执行,导致数据不一致。
解决方案 1:使用 synchronized
public class SafeCounterWithSync {
private static int count = 0;
// 使用类锁保护静态变量
public synchronized static void increment() {
count++;
}
public synchronized static int getCount() {
return count;
}
}解决方案 2:使用 AtomicInteger
import java.util.concurrent.atomic.AtomicInteger;
public class SafeCounterWithAtomic {
private static final AtomicInteger count = new AtomicInteger(0);
public static void increment() {
count.incrementAndGet(); // 原子操作
}
public static int getCount() {
return count.get();
}
}解决方案 3:使用 LongAdder(高并发场景)
import java.util.concurrent.atomic.LongAdder;
public class HighPerformanceCounter {
private static final LongAdder count = new LongAdder();
public static void increment() {
count.increment();
}
public static long getCount() {
return count.sum();
}
}static 代码块的线程安全
问题:静态代码块只执行一次,但初始化可能涉及复杂操作
public class UnsafeSingleton {
private static UnsafeSingleton instance;
static {
// 复杂的初始化逻辑
try {
Thread.sleep(100); // 模拟耗时操作
instance = new UnsafeSingleton();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private UnsafeSingleton() {}
public static UnsafeSingleton getInstance() {
return instance;
}
}解决方案:使用静态内部类(延迟加载 + 线程安全)
public class ThreadSafeSingleton {
private ThreadSafeSingleton() {}
// 静态内部类 - 延迟加载
private static class Holder {
private static final ThreadSafeSingleton INSTANCE = new ThreadSafeSingleton();
}
public static ThreadSafeSingleton getInstance() {
return Holder.INSTANCE;
}
}final 的线程安全特性
final 字段的初始化安全性
public class FinalExample {
private final int x;
private int y;
public FinalExample() {
x = 1; // final 字段在构造器完成前对其他线程可见
y = 2; // 普通字段可能在对象引用逸出后仍未完成初始化
}
public static void main(String[] args) {
// 线程 A 创建对象
FinalExample obj = new FinalExample();
// 线程 B 直接读取对象
// obj.x 保证可见(1),obj.y 可能是 0
}
}JMM 保证:只要对象引用对其他线程可见,那么 final 字段也一定初始化完成。
final 不可变对象的线程安全
public final class ImmutablePerson {
private final String name;
private final int age;
private final List<String> hobbies;
public ImmutablePerson(String name, int age, List<String> hobbies) {
this.name = name;
this.age = age;
// 防御性拷贝
this.hobbies = Collections.unmodifiableList(new ArrayList<>(hobbies));
}
// 只提供 getter,不提供 setter
public String getName() {
return name;
}
public int getAge() {
return age;
}
public List<String> getHobbies() {
return hobbies;
}
}static final 常量的线程安全
public class ThreadSafeConstants {
// 编译期常量 - 线程安全(只读)
public static final int MAX_SIZE = 100;
// 运行时常量 - 线程安全(final 保证不可变)
public static final String APP_NAME = "MyApp";
// 不可变集合 - 线程安全
public static final List<String> SAFE_LIST =
Collections.unmodifiableList(Arrays.asList("A", "B", "C"));
// 即使在多线程环境下访问,也不需要同步
public static void main(String[] args) {
new Thread(() -> {
System.out.println(MAX_SIZE); // 安全
}).start();
new Thread(() -> {
System.out.println(APP_NAME); // 安全
}).start();
}
}常见并发陷阱
陷阱 1:误以为 final 保证对象内容不可变
public class FinalNotImmutable {
public static void main(String[] args) {
final Map<String, Integer> map = new HashMap<>();
map.put("A", 1); // ✅ 合法
// 多线程环境下修改 final 对象的内容 - 不安全!
new Thread(() -> map.put("B", 2)).start();
new Thread(() -> map.put("C", 3)).start();
}
}解决方案:使用不可变集合
public class ImmutableMapExample {
private static final Map<String, Integer> SAFE_MAP;
static {
Map<String, Integer> temp = new HashMap<>();
temp.put("A", 1);
temp.put("B", 2);
SAFE_MAP = Collections.unmodifiableMap(temp); // 不可变
}
public static int getValue(String key) {
return SAFE_MAP.get(key); // 线程安全
}
}陷阱 2:static 初始化的可见性问题
public class StaticVisibility {
private static boolean initialized = false;
private static Data data;
static {
data = new Data(); // 初始化数据
initialized = true; // 标记已初始化
}
public static Data getData() {
if (!initialized) { // 可能读到旧的 initialized 值
throw new IllegalStateException("Not initialized");
}
return data; // 可能 data 未完成初始化
}
}解决方案:使用 volatile 或确保类初始化的原子性
public class StaticVisibilityFixed {
// volatile 保证 happens-before 关系
private static volatile boolean initialized = false;
private static Data data;
static {
data = new Data();
initialized = true; // volatile 写保证之前的操作对其他线程可见
}
public static Data getData() {
if (!initialized) {
throw new IllegalStateException("Not initialized");
}
return data;
}
}最佳实践
static 使用最佳实践
/**
* 工具类最佳实践
*/
public final class CollectionUtils {
// 私有构造器,防止实例化
private CollectionUtils() {
throw new AssertionError("工具类不允许实例化");
}
// 静态工具方法
public static <T> boolean isEmpty(Collection<T> collection) {
return collection == null || collection.isEmpty();
}
public static <T> boolean isNotEmpty(Collection<T> collection) {
return !isEmpty(collection);
}
public static <T> List<T> emptyIfNull(List<T> list) {
return list == null ? Collections.emptyList() : list;
}
}final 使用最佳实践
/**
* 不可变对象最佳实践
*/
public final class Money {
private final long amount;
private final String currency;
public Money(long amount, String currency) {
this.amount = amount;
this.currency = Objects.requireNonNull(currency, "currency cannot be null");
}
// 返回新对象,而不是修改原对象
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("Currency mismatch");
}
return new Money(this.amount + other.amount, this.currency);
}
public long getAmount() {
return amount;
}
public String getCurrency() {
return currency;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Money)) return false;
Money money = (Money) o;
return amount == money.amount && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount, currency);
}
}单例模式最佳实践
/**
* 枚举单例 - 最佳实践
*/
public enum EnumSingleton {
INSTANCE;
private final DataService dataService;
EnumSingleton() {
this.dataService = new DataService();
}
public DataService getDataService() {
return dataService;
}
}性能对比
关键字组合对比
| 关键字组合 | 内存分配 | 访问速度 | 线程安全 | 适用场景 |
|---|---|---|---|---|
static | 方法区,全局唯一 | 快(直接访问) | 需额外同步 | 工具方法、共享数据 |
final | 视上下文而定 | 快(编译器优化) | 不可变保证安全 | 不可变数据、方法锁定 |
static final | 方法区,编译期常量可能内联 | 最快 | 天然安全(只读) | 全局常量、配置项 |
性能测试示例
public class PerformanceTest {
private static int staticCount = 0;
private int instanceCount = 0;
private static final int STATIC_FINAL_CONSTANT = 100;
public static void testStatic() {
staticCount++; // 直接访问方法区
}
public void testInstance() {
instanceCount++; // 需要通过对象引用
}
public void testConstant() {
int value = STATIC_FINAL_CONSTANT; // 可能被内联为 int value = 100
}
}总结
核心要点
static决定归属:- 属于类,所有实例共享
- 在类加载时初始化,存储在方法区
- 访问速度快,但多线程下需注意线程安全
final决定可变性:- 一旦初始化不可改变
- 提供初始化安全保证
- 适用于不可变对象设计
static final决定本质:- 全局常量,编译期可能内联
- 天然线程安全(只读)
- 性能最优
- 类加载时序:
- 准备阶段:静态变量设为零值,常量设为最终值
- 初始化阶段:按代码顺序执行静态变量赋值和静态代码块
- 线程安全:
static变量:需使用synchronized、Atomic或LongAdderstatic final常量:天然线程安全final对象:引用不可变,但内容可能需额外保护- 静态内部类单例:延迟加载 + 线程安全
记忆口诀
static 归类不归你,final 不变永相依 static final 全局常,编译内联性能强 类加载时分两步,准备初始化要清楚 线程安全需注意,原子操作不可欺
参考资料
- Java Language Specification - Classes
- Java Virtual Machine Specification - Class Loading
- Java Concurrency in Practice
- Effective Java (3rd Edition) - Chapter 4
到此这篇关于Java static 与 final关键字详解的文章就介绍到这了,更多相关Java static 与 final内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
