0%

浅谈Java类加载顺序和static关键字

前言

本篇博客主要包含以下几个内容

  1. Java中类加载的顺序
  2. Java中static关键字的用法
  3. 面试的一些常见问题

Java中的类加载

JVM要执行一段Java程序分为两步:

  1. 将类编译成.class字节码文件
  2. 进行类加载生成类

类加载的过程

想要理解static必须要了解一下类加载的过程,主要分为三步:

  1. 第一步:类文件加载
  2. 第二步:类链接
    1. 验证
    2. 准备
    3. 解析
  3. 第三步:类初始化

类加载的时机

类的加载一般通过JVM提供的类加载器ClassLoader完成,在以下几种情况下,JVM会把类加载进内存中进行使用:

  1. new 这个类的对象
  2. 访问这个类或接口的静态变量
  3. 调用这个类的静态方法
  4. 执行反射操作:Class.forName(“类名”)
  5. 加载这个类的子类
  6. JVM启动时表明的启动类(这说明了一个类并不一定要被使用的时候才会被加载)

类文件加载

JVM在编译时会生成类的.class字节码文件,这个文件是存储在磁盘上的。当JVM运行时需要加载某个类时,会将这个文件读入内存,并由类加载器为其生成一个独一无二的java.lang.Class对象。 这个对象保存了一个类的所有信息,用暴力反射甚至可以操作其私有变量。

类链接

这一步主要分为三小步

  1. 验证:主要验证这个类是否有正确的内部结构(比如.class文件格式是否符合规范等)(这里我有疑问,如果验证失败了,其创建的Class对象是否还有意义?)
  2. 准备:为类中的静态变量分配内存,并设置初始值(同时也会在常量池中创建这个常亮)
  3. 解析:将内存中二进制数据的符号引用生成直接引用(这个引用在堆里,详情可见HashMap 为什么会出现内存泄漏问题以及 Java 中引用类型的存储位置)

类初始化

这一步主要给静态变量赋正确的值。
也就是说,当我定义static a = 10时,会先在第二步中进行初始化成0,然后在类初始化时被赋予正确的值。因为在程序运行时,每个类只会被加载一次,因此静态成员变量的初始化只会进行一次。

补充

  1. 加载子类前会先加载父类
  2. 类加载完成后,才会执行类实例化的操作
  3. 类初始化和类实例化时不同的概念

static关键字的用法

static关键字主要用于五个方面:

  1. 静态方法
  2. 静态成员变量
  3. 静态代码块
  4. 静态内部类
  5. 静态导入

静态变量和静态代码块

类加载的时候被赋值和调用,其顺序与代码顺序一致。

1
2
3
4
5
6
7
8
9
10
11
public class TestStatic {
//改变两段定义的位置程序性会报错
private static int a;
static{
System.out.println(a); // a:0
}

public static void main(String[] args) {
TestStatic t = new TestStatic();
}
}

静态方法

类的静态方法会在类加载时分配入口地址,并且早于类对象的构造函数调用,因此静态方法是无法访问类的非静态成员变量的,因此在静态方法中也无法使用this或者super关键字。

静态内部类

具体可以查看我的博客:浅谈Java内部类

静态导入

这个是JDK5之后的特性,用于导入某个类的静态方法,导入后可以直接使用方法名来使用该方法。其用法如下:import static 包名.类名.方法名,方法名可以改成通配符*,这样就可以导入所有静态方法。(我试了一下,静态成员变量无法导入)
例子继续按照上方的代码,增加了一个静态方法,并把该类放置在memoforward包下:

1
2
3
4
5
6
7
8
9
10
11
package memoforward;

public class TestStatic {
private static int a = 10;
static{
System.out.println(a);
}
public static void print(){
System.out.println("gogogogo.....");
}
}

随后我在另一个类中静态导入方法print()

1
2
3
4
5
6
7
8
9
10
package memoforward;

import static memoforward.TestStatic.print;
//import static memoforward.TestStatic.*;

public class TestStaticImport {
public static void main(String[] args) {
print();
}
}

这个程序的输出是:10 和 gogogo...,表示调用某个类静态方法后,这个类会先被加载。

补充

  • 静态成员变量和静态方法都是被所有对象共有的,任意对象都可以操作同一份静态变量,使用静态方法。
  • 子类对象可以使用父类对象的静态成员变量(且仍是同一份数据),但是如果子类重新声明了同名静态变量,则JVM会为子类额外分为一块内存空间,此时会由对象的类型来确定到底调用哪一个静态变量(因此静态变量不存在继承的关系,其调用完全根据其类型,静态方法同理),学术点讲:静态方法是静态绑定的。
  • 如果子类和父类有同名的静态变量或者静态方法,则根据其引用类型进行调用。

测试代码:

  • 父类Father
1
2
3
4
5
6
7
8
9
package memoforward;

public class Father {
public static int a = 10;
public static int b = 17;
public static void staticMethod(){
System.out.println("Father method...");
}
}
  • 子类Son
1
2
3
4
5
6
7
8
package memoforward;

public class Son extends Father{
public static int a = 5;
public static void staticMethod(){
System.out.println("Son method...");
}
}
  • 测试:可见静态的变量和方法若有重名会根据其引用类型来确定,若没有重名,则子类调用父类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package memoforward.test;

import memoforward.Father;
import memoforward.Son;

public class TestStaticExtends {
public static void main(String[] args) {
Father father = new Father();
Son son = new Son();
Father fs = new Son();
System.out.println(father.a); // 10
System.out.println(son.a); // 5
System.out.println(fs.a); // 10
System.out.println(son.b); // 17
father.staticMethod(); // Father staticMehtod...
son.staticMethod(); // Son staticMethod...
fs.staticMethod(); // Father staticMehtod...
}
}

面试常见问题

  • 问题一
    Q:static关键字会改变变量的访问权限吗?
    A:不会,能影响访问权限的只有public、private和protected关键字,如果一个静态变量被声明为private,则在类外是无法使用的。

  • 问题二
    Q:抽象方法可否是静态的?
    A:不能,静态方法不能被重写,且抽象方法无法直接被类调用。

  • 问题三
    Q:以下代码的输出是什么

1
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
public class Test {
Person person = new Person("Test");
static{
System.out.println("test static");
}

public Test() {
System.out.println("test constructor");
}

public static void main(String[] args) {
new MyClass();
}
}

class Person{
static{
System.out.println("person static");
}
public Person(String str) {
System.out.println("person "+str);
}
}

class MyClass extends Test {
Person person = new Person("MyClass");
static{
System.out.println("myclass static");
}

public MyClass() {
System.out.println("myclass constructor");
}
}

A:输出如下

1
2
3
4
5
6
7
1. test static
2. myclass static
2. person static
3. person Test
4. test constructors
5. person MyClass
6. myclass constructor

Explanation:

  1. 调用Test类的静态方法Main,首先会加载Test类,因此执行静态代码块,输出:test static
  2. 使用new MyClass(),会先加载类MyClass,输出:myclass static
  3. 执行子类实例化前,会先执行父类实例化,初始化操作的顺序:先给成员变量赋默认值,再执行构造函数,因此先执行父类Testnew Person("Test")
  4. 加载Person类,并执行构造函数,先后输出:person staticperson Test
  5. 父类Test中成员变量person赋值完毕,执行构造函数,输出:test constructor
  6. 随后进行子类对象MyClass初始化,因为Person类已经加载过,因此不会再加载了,所以先后输出:person MyClassmyClass constructor

Attention:
这里有个小细节:子类所有实例化操作(包括成员变量赋值和构造器)都慢于父类实例化的操作,而实例化操作的顺序是:先进行成员变量赋值,再执行构造函数。