1 前言
static意思是静态的、全局的,在java中一旦被static修饰,说明被修饰的东西在一定范围内是共享的,谁都可以访问,这时候需要注意并发读写时的线程安全问题。被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问,这使得我们可以很方便的在没有创建对象的情况下来进行方法/变量的调用/访问。
2 修饰的对象
static 可以用来修饰变量、方法、内部类和代码块。
2.1 静态变量
使用 static 修饰的变量是属于类的,而不是属于对象的,静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。
注意,static 关键字并不能修改变量的访问权限,也就是说 private 变量即使使用 static 关键字修饰,依然只能由本类及本类的对象访问,而用 static 关键字修饰的 public 变量可以被任何类直接访问,而无需初始化类,直接使用 类名.static变量 这种形式访问即可。
在使用多个线程对静态变量进行读写时,需要注意它的线程安全问题,比如对于public static List<String> list = new ArrayList();
这样的共享变量在并发环境下进行读写时就会产生线程安全问题,这时我们可以使用线程安全的 CopyOnWriteArrayList 或者在读写时手动进行加锁来保证多线程读写下的线程安全。
静态变量在字节码层面是使用访问标志位来进行标识的,如下图所示:
也就是说,在类加载时,JVM会通过class文件中的访问标志位来判断某个变量是否为静态变量。
2.2 静态方法
和静态变量一样,静态方法也可以不依赖于任何对象就直接进行访问,因此对于静态方法来说,是没有 this 的,因为它不依附于任何对象,而 this 本身就是个对象。由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用/访问的,而反过来,非静态成员方法是可以调用/访问静态成员方法/变量的。
static 方法内部的变量在执行时是没有线程安全问题的。方法执行时,数据运行在栈里面,栈里面的数据每个线程都是隔离开的,所以不会有线程安全的问题。
static 方法在字节码层面是使用一个ACC_STATIC的标志位来进行标识的,如下图所示:
2.3 静态内部类
普通类是不允许声明为静态的,只有内部类才可以,使用 static 修饰的内部类可以直接由外部类来创建和访问,而没有用 static 修饰的内部类则必须要先实例化一个外部类的对象,再通过外部类的对象来创建内部类的实例。
2.4 静态代码块
static 关键字还可以修饰代码块用于在类启动之前,初始化一些值。static 块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。在静态代码块中只能调用同样被 static 修饰的变量,并且 static 的变量需要写在静态块的前面,不然编译会报错。
和 static 方法一样,静态代码块在字节码层面使用一个ACC_STATIC的标志位来进行标识。
3 初始化时机
我们通过一个演示程序来测试一下被 static 修饰的类变量、代码块和方法的初始化时机:
publicclassParent { privatestaticList<String>parentList=newArrayList(){{ System.out.println("父类静态变量初始化"); }}; static { System.out.println("父类静态代码块初始化"); } publicParent() { System.out.println("父类构造器初始化"); } publicstaticvoidtestStatic() { System.out.println("父类静态方法被调用"); } }
publicclassChildextendsParent { static { System.out.println("子类静态代码块初始化"); } privatestaticList<String>childList=newArrayList(){{ System.out.println("子类静态变量初始化"); }}; publicChild() { System.out.println("子类构造器初始化"); } publicstaticvoidmain(String[] args) { System.out.println(" main 方法执行"); newChild(); } }
运行程序,发现打印的结果是:
父类静态变量初始化 父类静态代码块初始化 子类静态代码块初始化 子类静态变量初始化 main 方法执行 父类构造器初始化 子类构造器初始化
因此,我们可以得出以下结论:
- 父类的静态变量和静态代码块比子类优先初始化;
- 静态变量和静态块比类构造器优先初始化;
- 静态变量和静态代码块按照定义的顺序依次进行初始化;
- 被 static 修饰的方法,在类初始化的时候并不会执行,只有当自己被调用时,才会被执行。
4 面试题
如何证明 static 静态变量和类无关?
- 不需要初始化类就可直接使用静态变量;
- 在类中写个 main 方法运行,即便不写初始化类的代码,静态变量都会自动初始化;
- 静态变量只会初始化一次,初始化完成之后,不管再 new 多少个类出来,静态变量都不会再初始化了。