Java static关键字详解
# static简介
static关键字作用于属性、方法、代码块、内部类之上,被static所修饰的变量和方法可以直接通过"类名.变量"和"类名.方法名"调用。需要注意的是,static修饰的成员变量和方法属于类,普通成员变量和方法属于对象,静态方法不能调用非静态成员
# static变量(静态变量)
静态变量和非静态变量的区别:
- 静态变量被所有对象共享,在内存中只有一个副本,在类初次加载的时候才会初始化。
- 非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。
// 日志
private static final Log logger = LogFactory.getLog(SpringApplication.class);
private static final Method CLASS_GET_MODULE;
private static final Method MODULE_GET_NAME;
2
3
4
5
6
提示
static成员变量初始化顺序依据编码的顺序进行初始化!并且static成员变量在类装载的时候就会被初始化。也就是说,只要类被装载,不管你是否使用了这个static变量,它都会被初始化。简而言之,它在创建对象的之前,就会被初始化,且只被初始化一次。
# static代码块(静态初始化块)
static代码块用于类的初始化操作,提升程序性能。在static代码块中不能直接访问非static成员。Spring中的LambdaSafe类加载模块和方法:
static {
CLASS_GET_MODULE = ReflectionUtils.findMethod(Class.class, "getModule");
MODULE_GET_NAME = (CLASS_GET_MODULE != null)
? ReflectionUtils.findMethod(CLASS_GET_MODULE.getReturnType(), "getName") : null;
}
2
3
4
5
提示
static代码块可以置于类中的任何地方,类中可以有多个静态初始化块。在类初次被加载时,会按照静态初始化块的顺序来执行每个块,并且只会执行一次。
# static方法(静态方法)
static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。Springboot中用于加载SpringApplication的辅助函数。
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified source using default settings.
* @param primarySource the primary source to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
return run(new Class<?>[] { primarySource }, args);
}
2
3
4
5
6
7
8
9
10
# 经典面试题
面试官:以下这段代码将会输出什么?
public class Test {
static {
System.out.println("-- Test Static --");
}
Test() {
System.out.println("-- Test Constructor --");
}
public static void main(String[] args) {
new Student();
}
}
class Person {
static {
System.out.println("-- Person Static --");
}
Person(String clazz) {
System.out.println("-- Person "+ clazz +" --");
}
}
class Student extends Test{
Person person = new Person("Student");
static {
System.out.println("-- Student Static --");
}
Student() {
System.out.println("-- Student Constructor --");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 执行流程分析
注意
本文建立在HotSpot虚拟机之上(Java 11),只提供大致的思路,类加载时机和过程等具体的细节请读者自行校验。static块的执行发生在“初始化”的阶段,而这个阶段是类加载的最后一个步骤。
step.1 执行java Test,JVM加载Test.class文件从磁盘加载(类加载)到内存并初始化Test类,执行Test类构造器<clinit>()方法。
提示
初始化阶段就是执行类构造器<clinit>()方法的过程,是类加载的最后一个步骤。<clinit>()并不是程序员在Java代码中直接编写的方法,它是Javac编译器的自动生成物。<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块) 中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
step.2 Test初始化完成,JVM执行main方法,发现含有new关键字实例化Student对象,根据《Java虚拟机规范》则必须要初始化Student类,JVM加载Student时发现Student继承自Test,而Test类已经加载完毕无需再次加载。
step.3 初始化Student类,执行Student类构造器<clinit>()方法,完成后将调用父类的构造器。
step.4 初始化Student类的非静态成员变量,发现含有new关键字实例化Person类,必须进行Person类的初始化。
step.5 初始化Person类,执行Person类构造器<clinit>()方法,完成后将调用本类的构造器。
step.6 Student类加载完成,调用本类的构造器。
至此,执行流程全部完成。则输出:
-- Test Static -- // 发生在step.1
-- Student Static -- // 发生在step.3
-- Test Constructor -- // 发生在step.3
-- Person Static -- // 发生在step.5
-- Person Student -- // 发生在step.5
-- Student Constructor -- // 发生在step.6
2
3
4
5
6
# DCL单例模式
public class Singleton {
private Singleton() {throw new RuntimeException("instance is null")}
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
public static void main(String[] args) {
Singleton.getInstance();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
参考资料 《Java编程思想 第四版》 《深入理解Java虚拟机:JVM高级特性与最佳实践(第3版)周志明》