首先我们来搞清楚java中的静态属性和静态方法。
有关静态常见的问题
这里我问几个问题,供大家思考一下,看看自己是否可以秒答。
- 1.静态属性可以被继承吗?
- 2.静态方法可以访问非静态属性吗?
- 3.非静态方法可以访问静态属性吗?
- 4.静态方法中可以使用this/super关键字吗?
- 5.为什么静态方法不能调用非静态方法或者变量呢?
- 6.静态属性、非静态属性、静态代码块、非静态代码块、构造函数在初始化时的执行顺序是怎样的?
先说答案:
- 1.可以,继承跟是否静态无关,只跟作用域有关(private public protected)
- 2.不可以,同4
- 3.可以,因为静态变量是存储在静态内存单元内,可以直接用类进行调用,也可以用实例化的对象对其引用。所以非静态方法是可以引用静态变量的
- 4.不可以,this指代当前对象的引用,super指代父类对象的引用,静态方法属于类,不属于某一个对象,当静态方法加载的时候,对象还不一定存在。如果静态方法中有this和super那么当静态方法被加载到内存中,其中的this和super也被加载到内存中,但是对象还没创建this和super还没被初始化,所以加载时会报错。
- 5.静态方法是属于类的,不需要实例化,不是属于对象的,而非静态变量是属于对象的,需要先实例化。在一个类的静态方法中访问非静态成员,之所以出错是因为,在类的非静态成员不存在的时候,类的静态属性已经存在了,访问一个内存中不存在的东西当然会报错。
- 6.不考虑继承关系的话,静态属性和代码块(按照代码出现的顺序)>非静态属性和非静态代码块>构造函数 马上来验证一下
public class StaticDemo {
public String b = "非静态属性";
public static String a = "静态属性";
static {
System.out.println(a);
System.out.println("静态代码块");
}
{
System.out.println(b);
System.out.println("非静态代码块");
}
public StaticDemo() {
System.out.println("构造函数");
}
public static void main(String[] args) {
StaticDemo demo = new StaticDemo();
}
}
打印结果:
静态属性
静态代码块
非静态属性
非静态代码块
构造函数
在有继承关系的情况下
父类的静态属性和静态代码块>子类的静态属性和静态代码块>父类的非静态属性>父类的构造函数>子类的非静态函数>子类的构造函数
再来验证一下
class ParentClass{
public static String pA = "父类-静态属性";
public String pB = "父类-非静态属性";
static {
System.out.println(pA);
System.out.println("父类-静态代码块");
}
{
System.out.println(pB);
System.out.println("父类-非静态代码块");
}
public ParentClass() {
System.out.println("父类-构造函数");
}
}
public class StaticDemo extends ParentClass{
public String b = "非静态属性";
public static String a = "静态属性";
static {
System.out.println(a);
System.out.println("静态代码块");
}
{
System.out.println(b);
System.out.println("非静态代码块");
}
public StaticDemo() {
System.out.println("构造函数");
}
public static void main(String[] args) {
StaticDemo demo = new StaticDemo();
}
}
执行结果:
父类-静态属性
父类-静态代码块
静态属性
静态代码块
父类-非静态属性
父类-非静态代码块
父类-构造函数
非静态属性
非静态代码块
构造函数
这几个问题我们知道答案了,但是为什么会产生这样的结果呢?
类是如何加载的
我们看一下类的加载过程:
graph TD
加载 --> 验证--> 准备 --> 解析--> 初始化
流程图中的过程就是一个类加载的生命周期,每个阶段会做相应的工作
- 加载:主要做了三件事:
- 获取二进制字节流,也就是通过全限定名找到这个类在哪里,是jar还是class文件
- 将静态存储结构转化为方法区运行时数据结构
- 在java堆中生成一个class类对象(相当于一个句柄,方法区的访问入口),去访问方法区
- 验证:主要就是验证类的正确性,包括魔数、版本号、常量池验证、元数据验证、字节码验证、符号引用验证等等
- 准备:为类变量分配内存并设置类变量的初始化。这些内存都在方法区分配。注意此时就会为我们的类变量也就是静态变量分配内存,但是普通成员变量还没。 还有一点需要注意的是对final修饰的类变量,在准备阶段就赋值了
static int n = 2;//初始化的值是0 而不是2 因为此时还没执行任何java方法
static final int n = 2;//初始化的值是2,ConstantValue对应到常量池,在准备阶段n被赋值成2
- 解析:对符号引用进行解析。(直接引用:指向目标的指针或者偏移量)把符号引用变为直接引用
- 初始化: 在编译生成class文件时,会自动产生两个方法,一个是类的初始化方法< clinit>, 另一个是实例的初始化方法< init>
- < init >方法 实例的初始化阶段
- < clinit >方法 静态变量、静态块的初始化(如果没有静态块,静态变量,则没有这个方法)
Class A{
//clinit方法中初始化
static int i = 2;
//clinit
static {
System.out.print("");
}
int n;
}
静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接去访问;非静态成员(变量和方法)属于类的对象,所以只有该对象初始化之后才存在,然后通过类的对象去访问。
也就是说如果我们在静态方法中调用非静态成员变量会超前,可能会调用了一个还未初始化的变量。因此编译器会报错。