目录
为什么需要静态成员变量
当我们定义一个类时,需要在类体中声明成员变量用来表现该类的一些属性。我们每创建一个对象,就会在对象中创建这些成员变量并赋值。但是,对于一些不属于某一对象特有的,而是全体对象共有的属性,例如:一个学校的全体学生类,那么他们的学校名称是相同的,假如依然在每个对象中都创建存储学校名的变量,重复的给一个属性赋相同的值,这一方面很不方面,也会造成内存资源的浪费,总体来说是不合适的。
要解决(1)重复赋值(2)浪费内存空间 这两个问题,从根本上就不能把这个全体共有的属性定义为成员变量,而是考虑定义一种“全新的,属于类的”变量。Java当中使用static关键字修饰成员变量,表示“静态成员变量“。同样的,static关键字也可以用来修饰成员方法,表示“静态成员方法”。
静态成员的定义语法和特点
静态成员变量
静态成员变量是一种被全体对象共享,属于类的变量,存储在一片能够被全体对象所共享的内存区域,而不像成员变量一样存储在对象当中。静态成员变量在Java语法中是被static修饰的成员变量,具体语法如下:
[访问权限修饰符] static 数据类型 变量名;
静态成员变量的特点:
- 和成员变量一样,同样具有默认初始化,具有默认值,而且默认初始化的步骤同样永远最先进行,静态成员变量各种数据类型的默认值与成员变量一样。静态成员变量同样可以显式赋值,并且显式赋值也会覆盖默认值。
- 静态成员变量不属于对象,而属于类,要使用“类名.”访问它们,在编写代码中不要使用“对象名.”访问静态成员变量(尽管这样的代码也可以访问到,但是不要这样写)。
- 静态成员变量无需依赖对象,即使没有对象,直接使用“类名.”也可以访问它们。
静态成员方法
同样的,静态成员方法也是一种被全体对象共享,属于类的方法。静态成员方法在Java中是被static修饰的成员方法,具体语法如下:
[访问权限修饰符] static 返回值类型 方法名(形参列表){
// 方法体
}
静态成员方法的特点:
- 同样使用“类名.”访问静态成员方法,当访问同类中的static方法时,可以省略类名直接调用访问。
- 同样不依赖于对象,不需要创建对象,没有对象也可以调用静态成员方法。
再次强调:静态成员变量/方法都是属于类的,不需要创建对象,直接使用“类名.”就可以访问它们。二者合称为静态成员。
静态成员的原理和类加载时机
了解了静态成员怎么写、怎么用,还想要了解为什么静态成员能够本文开头提出的两个需求,接下来结合类加载的时机了解一下静态成员的原理。
类加载是在使用类之前,必须进行的一个步骤,让我们的JVM了解类的基本信息,进入main方法、或是创建新对象都会触发类加载,只有在类加载完成之后,才能进入main方法中执行代码、或是创建新的对象。因此类加载一定在创建对象之前,而静态成员的准备工作就是在类加载的过程中完成的,随着类加载完毕,静态成员就存在,并且能够使用了。而且,由于类加载在程序执行过程中只有一次,所以静态成员变量在整个类的全局只有一份,被该类的全体对象共享。
- 静态成员变量在类加载过程中完成了默认初始化,具有默认值,然后进行一些其他赋值,类加载结束就可以访问了,静态成员变量的赋值也是先进行初始化,然后才考虑其他复制手段。
- 静态成员方法的二进制指令集合在类加载过程也准备完毕,可以调用。
如果直接访问类的静态成员,可以不进行类加载吗?答案当然是否定的,实际上访问类的静态成员也是一种触发类加载的时机(类加载的时机就是必须类加载某个类的场景),只要访问类的静态成员,会先完成类加载,然后再去做访问操作。启动mian方法、new一个对象、访问类的静态成员都是类加载的时机。
一些其他值得注意的细节:
- 类加载过程中,JVM会把在类加载过程中会执行的代码封装成一个方法执行(就是在debug模式下看到的clinit方法栈桢),也遵循栈的“先进后出”原则,这也是类加载连环触发的根据。
- 在类加载时期,静态成员变量的赋值有默认初始化和显式赋值,默认初始化永远是第一步,然后才会进行其他赋值方式。
- 静态成员方法是在类加载过程中将二进制指令集合准备好,等待调用,而不是在类加载过程中主动调用的方法,要想调用静态成员方法依然需要主动调用,遵循方法的不调用不执行原则。
与非static的比较
static成员与非static成员正是由static成员的特点决定的。
以普通的成员变量为例,从四个角度对静态成员变量与成员变量做出比较:
所属不同
静态成员变量属于类,也称为类变量。
普通的成员变量属于对象,也称为对象变量或实例变量。
在内存中的位置不同
静态成员变量存储于堆上(Java7及之前的版本存储于方法区),被所有对象共享。
成员变量存储于堆上,每个对象独享自己的成员变量,成员变量直接存储于对象当中。
在内存中出现的时间不同
静态成员变量对着类的加载而创建,一定比成员变量出现的早。
成员变量随着对象的创建而存在,一定比静态成员变量出现的晚。
调用方式不同
静态成员变量可以直接通过类名调用(语法上也可以通过对象调用,但我们不这样做)。
成员变量只能通过对象名调用,必须先创建对象。
那么静态成员方法与成员方法又有什么区别呢?
这种区别主要体现在能否互相调用上,而根本还是由于他们存在的时机不同。
成员方法可以访问静态成员,这是由于成员方法如果被调用,对象一定存在,类加载一定进行了,那么静态成员肯定已经存在。
静态成员方法中可以访问非静态成员,这是由于只要类加载就可以调用静态成员方法了,但是不一定创建了对象,自然无法调用属于对象的非静态成员。
没有对象就无法访问非静态成员(静态成员方法中没有this),先存在的不能访问后存在的,后存在的可以访问先存在的。
关于成员变量和静态成员变量的赋值问题
成员变量的赋值不能依赖于静态成员方法等类加载过程中的结构,或者说不能在静态成员方法中进行成员变量的赋值,因为静态成员方法的执行只需要在类加载之后就能执行了,不一定创建对象,而成员变量的赋值必须有对象。
静态成员变量的赋值可以依赖成员方法或构造器等创建对象的过程,或者说可以在成员方法中进行静态成员变量的赋值,因为创建对象时,静态成员变量一定存在。
使用场景
了解了静态成员变量和静态成员方法的原理和特点,那么在实际中它们有什么用呢,我们在什么情况下需要使用static关键字呢。
静态成员变量
- 属于全体对象所共享而不是独属于某个对象的成员变量,所以当存在需要所有对象共享的变量时,应该使用static修饰的静态成员变量。
- 在整个类全局独一份的(因为类加载只有一次),所以当需要某个变量在类的全局只有一份时,应该使用static修饰的静态成员变量。
静态成员方法
- 静态成员方法无需创建对象就可以调用,作用当希望一个方法能够更方便快捷地被调用时,可以把它声明为static修饰的静态成员方法。
- 当一个类中所有方法都是静态成员方法时,类中所有方法都可以使用“类名.”去调用,这就是Java中的“工具类”,常见的例如:数组工具类Arrays、数学工具类Math、集合工具类Collections等。
一些需要注意的细节
- 可以使用静态成员方法给静态成员变量赋值,不过需要注意的是,静态成员方法是在类加载过程中做好被调用的准备,而不是在类加载过程中自动被调用,静态成员方法和其他方法一样不调用不执行,要想依赖静态成员方法完成赋值或其他操作,必须主动调用该方法。
- 在一个类中,静态方法无法直接访问非静态的方法和属性,因为非静态的方法和属性需要依赖于对象,而在静态方法调用的时机,完全有可能没有对象,也不能使用this、super关键字。当在静态方法中访问普通成员时,会报错。
Non-static field/method xxx cannot be referenced from a static context
- 普通成员方法则可以访问静态成员,因为对象既然已经存在了,类加载就一定已经进行了。
- static修饰的静态成员变量只能存在于类的成员位置,而不能在局部位置,不存在所谓的“静态局部变量”,局部变量已经被限制了作用域,不能用static修饰。
- 静态成员通常不建议使用创建对象时期执行的结构来赋值,例如构造器,这种方式依赖于创建对象,不过也不绝对禁止这种方式,以需求为主。
类加载的连环触发
当我们在类加载某一个类的过程中,可能会触发另一个类的类加载,例如在加载第一个类的时候,给静态成员变量赋值创建另一个类的对象而触发第二个类的类加载,此时则会暂停第一个类的类加载,去进行第二个类的类加载,当第二个类类加载完成成功创建对象赋值给第一个类的静态成员变量后,继续进行第一个类的类加载。
第二个类中同理可以触发第三个类的类加载,从而形成类加载的连环触发。
最早开始的类加载将会最后结束,最早结束的类加载一定是最晚开始的,这完全符合方法栈帧“先进后出”的逻辑。实际上,类加载的过程正是将类加载过程中所需要执行的指令封装成一个方法进入栈中。