前言
本篇博客主要包含以下几个内容
- Java中类加载的顺序
- Java中static关键字的用法
- 面试的一些常见问题
Java中的类加载
JVM要执行一段Java程序分为两步:
- 将类编译成.class字节码文件
- 进行类加载生成类
类加载的过程
想要理解static必须要了解一下类加载的过程,主要分为三步:
- 第一步:类文件加载
- 第二步:类链接
- 验证
- 准备
- 解析
- 第三步:类初始化
类加载的时机
类的加载一般通过JVM提供的类加载器ClassLoader完成,在以下几种情况下,JVM会把类加载进内存中进行使用:
- new 这个类的对象
- 访问这个类或接口的静态变量
- 调用这个类的静态方法
- 执行反射操作:Class.forName(“类名”)
- 加载这个类的子类
- JVM启动时表明的启动类(这说明了一个类并不一定要被使用的时候才会被加载)
类文件加载
JVM在编译时会生成类的.class字节码文件,这个文件是存储在磁盘上的。当JVM运行时需要加载某个类时,会将这个文件读入内存,并由类加载器为其生成一个独一无二的java.lang.Class对象。 这个对象保存了一个类的所有信息,用暴力反射甚至可以操作其私有变量。
类链接
这一步主要分为三小步
- 验证:主要验证这个类是否有正确的内部结构(比如.class文件格式是否符合规范等)(这里我有疑问,如果验证失败了,其创建的Class对象是否还有意义?)
- 准备:为类中的静态变量分配内存,并设置初始值(同时也会在常量池中创建这个常亮)
- 解析:将内存中二进制数据的符号引用生成直接引用(这个引用在堆里,详情可见HashMap 为什么会出现内存泄漏问题以及 Java 中引用类型的存储位置)
类初始化
这一步主要给静态变量赋正确的值。
也就是说,当我定义static a = 10
时,会先在第二步中进行初始化成0,然后在类初始化时被赋予正确的值。因为在程序运行时,每个类只会被加载一次,因此静态成员变量的初始化只会进行一次。
补充
- 加载子类前会先加载父类
- 类加载完成后,才会执行类实例化的操作
- 类初始化和类实例化时不同的概念
static关键字的用法
static关键字主要用于五个方面:
- 静态方法
- 静态成员变量
- 静态代码块
- 静态内部类
- 静态导入
静态变量和静态代码块
类加载的时候被赋值和调用,其顺序与代码顺序一致。
1 | public class TestStatic { |
静态方法
类的静态方法会在类加载时分配入口地址,并且早于类对象的构造函数调用,因此静态方法是无法访问类的非静态成员变量的,因此在静态方法中也无法使用this或者super
关键字。
静态内部类
具体可以查看我的博客:浅谈Java内部类
静态导入
这个是JDK5之后的特性,用于导入某个类的静态方法,导入后可以直接使用方法名来使用该方法。其用法如下:import static 包名.类名.方法名
,方法名可以改成通配符*
,这样就可以导入所有静态方法。(我试了一下,静态成员变量无法导入)
例子继续按照上方的代码,增加了一个静态方法,并把该类放置在memoforward包
下:
1 | package memoforward; |
随后我在另一个类中静态导入方法print()
1 | package memoforward; |
这个程序的输出是:10 和 gogogo...
,表示调用某个类静态方法后,这个类会先被加载。
补充
- 静态成员变量和静态方法都是被所有对象共有的,任意对象都可以操作同一份静态变量,使用静态方法。
- 子类对象可以使用父类对象的静态成员变量(且仍是同一份数据),但是如果子类重新声明了同名静态变量,则JVM会为子类额外分为一块内存空间,此时会由对象的类型来确定到底调用哪一个静态变量(因此静态变量不存在继承的关系,其调用完全根据其类型,静态方法同理),学术点讲:静态方法是静态绑定的。
- 如果子类和父类有同名的静态变量或者静态方法,则根据其引用类型进行调用。
测试代码:
- 父类Father
1 | package memoforward; |
- 子类Son
1 | package memoforward; |
- 测试:可见静态的变量和方法若有重名会根据其引用类型来确定,若没有重名,则子类调用父类
1 | package memoforward.test; |
面试常见问题
问题一
Q:static关键字会改变变量的访问权限吗?
A:不会,能影响访问权限的只有public、private和protected
关键字,如果一个静态变量被声明为private
,则在类外是无法使用的。问题二
Q:抽象方法可否是静态的?
A:不能,静态方法不能被重写,且抽象方法无法直接被类调用。问题三
Q:以下代码的输出是什么
1 | public class Test { |
A:输出如下1
2
3
4
5
6
71. test static
2. myclass static
2. person static
3. person Test
4. test constructors
5. person MyClass
6. myclass constructor
Explanation:
- 调用Test类的静态方法Main,首先会加载Test类,因此执行静态代码块,输出:
test static
- 使用new MyClass(),会先加载类MyClass,输出:
myclass static
- 执行子类实例化前,会先执行父类实例化,初始化操作的顺序:先给成员变量赋默认值,再执行构造函数,因此先执行父类
Test
中new Person("Test")
- 加载
Person
类,并执行构造函数,先后输出:person static
和person Test
- 父类
Test
中成员变量person
赋值完毕,执行构造函数,输出:test constructor
- 随后进行子类对象
MyClass
初始化,因为Person
类已经加载过,因此不会再加载了,所以先后输出:person MyClass
和myClass constructor
Attention:
这里有个小细节:子类所有实例化操作(包括成员变量赋值和构造器)都慢于父类实例化的操作,而实例化操作的顺序是:先进行成员变量赋值,再执行构造函数。