为什么使用static关键字
通常来说,当定义一个类的时候,编译器并没有分配任何存储空间,直到使用new来创建对象,才为对象分配了存储空间。当使用new来创建对象时,其本质是在堆区为该对象开辟了一块存储区域,同一个类的不同对象实例具有相互独立的存储空间。
例如,创建一个描述员工的类如下所示。
public class Employee {
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static void main(String[] args) {
Employee e1 = new Employee("xxx",18);
Employee e2 = new Employee("yyy",19);
Employee e3 = new Employee("zzz",20);
}
}
其中,name和age属于非静态域,上例中的每一个对象(e1,e2,e3)都有name和age域的一份拷贝。所以操作一个对象的域时不会影响其他对象的域。如下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/5f8d94d62dcbae3f99ad3d196dd3902c.jpeg)
这种机制是面向对象语言的基本特性。但是在某些时候,只用非静态域却无法满足要求。主要有以下两种情形:
- 为某个域分配单一的存储空间,而不用和具体的对象关联
上例中,name域和age域在每一个对象内都有一份拷贝。而如果现在需要一个和对象无关的域,例如用一个域来表示员工的总数,那么就不能像定义name或者age一样来定义这个域。 - 希望某个方法不依赖于该类的任何一个对象
上例中,getName()方法和getAge()方法都是和对象关联的,只有创建了对象,这些方法才能够被调用。但是在某些情况下,我们需要一个不依赖于某个对象或者在没有创建任何对象的情况下都可以直接调用的方法。例如,上例中的main方法,编译器在调用main函数之前,是没有创建Employee对象的。
为了解决上述两个问题,Java中引入了static关键字,当一个类的域或者方法前面加上static关键字时,就表示该域或者方法不与任何该类具体的对象关联,可以脱离对象而独立存在。例如,可以在上例中引入employeeCount静态域存储员工数量,加入getEmployeeCount() 静态方法来获取员工数量。
public class Employee {
private static int employeeCount;
private String name;
private int age;
public Employee(String name, int age) {
this.name = name;
this.age = age;
employeeCount++;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
public static int getEmployeeCount() {
return employeeCount;
}
public static void main(String[] args) {
Employee e1 = new Employee("xxx",18);
Employee e2 = new Employee("yyy",19);
Employee e3 = new Employee("zzz",20);
System.out.println(Employee.getEmployeeCount());
}
}
静态域和非静态域的区别
- 存储位置和数量不同
静态域存储于静态存储区,而且无论创建多少个该类的对象,它只有唯一的存储单元。而非静态域存储于堆区,可能存在多个内存拷贝。如下图所示。
![](https://i-blog.csdnimg.cn/blog_migrate/c9e3ef597a022a2a21ce329997df8171.jpeg)
- 初始化方式及时间不同
下面先看一下对象的初始化过程。
在Java程序开始运行的时候,JVM类加载器并不会加载所有的类,类只有在第一次被使用的时候,才会动态加载到JVM中。所谓的第一次使用,其实包含了两种含义。
(1) 使用new来创建一个对象
(2) 访问该类的静态域或者静态方法
对象中各个域的初始化过程可以用以下流程图来表示。
![](https://i-blog.csdnimg.cn/blog_migrate/5a040a3ae5b3df9324a9aeba7d7922cc.jpeg)
从上图中,我们可以得到以下结论
(1) 在使用new创建对象或者访问类的静态数据域或方法时,JVM都会加载该类的.class文件。
(2) 加载类的.class文件和创建对象是两个独立的阶段。有时JVM加载类的.class文件可能是因为代码访问了类的静态域或者静态方法,并没有创建该类的对象。
(3) 静态域在JVM加载类的.class文件时进行初始化,由于每个类的.class文件只会被加载一次,所以无论创建多少个对象,静态域只会被初始化一次。而非静态域则是在创建对象时进行初始化,每创建一个对象,对应的非静态域都会被初始化。
(4) 基类的静态域会先于派生类的静态域初始化,基类的非静态域会先于派生类的非静态域初始化。
(5) 静态域的初始化永远先于非静态域初始化,即使派生类的静态域,也先于基类的非静态域初始化,因为前者位于阶段1,后者位于阶段2。
(6) 基类的构造函数执行要先于派生类的非静态域初始化。
了解了以上特点,来分析下下面这段代码的输出结果。
class Bowl {
static {
System.out.println("Load Bowl class");
}
public Bowl(int marker) {
System.out.println("Bowl("+marker+")");
}
void f1(int marker){
System.out.println("f1("+marker+")");
}
}
class Table {
static {
System.out.println("Load Table class");
}
static Bowl bowl1 = new Bowl(1);
public Table(){
System.out.println("Table()");
bowl2.f1(1);
}
void f2(int marker){
System.out.println("f2("+marker+")");
}
static Bowl bowl2 =new Bowl(2);
}
class Cupboard {
static {
System.out.println("Load Cupboard class");
}
Bowl bowl3 =new Bowl(3);
static Bowl bowl4 = new Bowl(4);
Cupboard(){
System.out.println("Cupboard()");
bowl4.f1(2);
}
void f3(int marker){
System.out.println("f3("+marker+")");
}
static Bowl bowl5 = new Bowl(5);
}
public class Test {
static {
System.out.println("Load Test class");
}
public static void main(String[] args) {
System.out.println("Creating new Cupboard() in main");
new Cupboard();
System.out.println("Creating new Cupboard() in main");
new Cupboard();
table.f2(1);
cupboard.f3(1);
}
static Table table = new Table();
static Cupboard cupboard = new Cupboard();
}
我们可以严格按照上图所示的步骤来分析。
首先从main函数开始,由于main函数是Test类的静态方法,因此访问main方法会使JVM加载Test类。Test类的静态域table 和cupboard 以及静态代码块按声明顺序进行初始化。这里会首先执行静态代码块,打印第一个输出 “Load Test class”。
然后调用new Table()来初始化table域,JVM将会加载Table类。Table类的静态域bowl1 和bowl2 以及静态代码块按声明顺序进行初始化。这里首先执行静态代码块,打印第二个输出Load Table class,然后初始化bowl1 ,调用new Bowl()来创建对象,JVM加载Bowl类。Bowl类的static域被执行,打印第三个输出 Load Bowl class。然后Bowl的构造函数开始执行,打印第四个输出Bow(1),接着初始化bowl2,由于在初始化bowl1时已经加载过了Bowl类,因此系统不会重复加载Bowl类,所以不会再次打印Load Bowl class,而是直接调用构造函数,打印第五个输出Bowl(2),初始化完bowl1 和bowl2 后,开始初始化Table的非静态域,由于Table没有非静态域,因此调用Table的构造函数,打印第六个输出Table()。并调用bowl2.f1打印第七个输出f(1)。至此,Test类的table 域初始化完毕。
再来看cupboard 域,调用new Cupboard()创建cupboard对象,JVM加载Cupboard类,静态代码块被执行,打印第八个输出 Load Cupboard class。接着静态域bowl4 和bowl5被初始化,调用new Bowl()来创建对象,由于在初始化化table时,已经加载过Bowl类了,因此JVM不会再加载Bowl类,因此Bowl的静态域不会被执行,而是直接调用构造函数,打印第九个输出Bowl(4)和第十个输出Bowl(5)。接着初始化Table的非静态域Bowl3,打印第十一个输出Bowl(3),然后指定Cupboard构造函数,打印第十二个输出Cupboard()和第十三个输出f1(2)。
通过上述分析可以看到,main函数一行代码都还没执行,已经打印了这么多输出。后续的执行结果也可以按照初始化的顺序去分析,最终的结果如下所示。
Load Test class
Load Table class
Load Bowl class
Bowl(1)
Bowl(2)
Table()
f1(1)
Load Cupboard class
Bowl(4)
Bowl(5)
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
Creating new Cupboard() in main
Bowl(3)
Cupboard()
f1(2)
f2(1)
f3(1)
静态方法和非静态方法区别
在面向对象中,为了能够让方法识别调用它的对象,编译器替我们做了一些幕后工作,它暗自把“所操作对象的引用”作为第一个参数传给方法。而在方法内部,我们可以通过this关键字来引用该对象。平时调用对象的非静态域和非静态方法本质也是通过this.field和this.method()的方式来进行的,只不过为了程序简洁,把this省略了。
按照静态方法的定义,静态方法不和特定的对象关联。因此在调用静态方法的时候,编译器并不会将对象的引用传入静态方法。相应的,在静态方法内部就不能通过访问非静态域和非静态方法。
但是反过来,由于静态域和静态方法是所有对象共享的,因此在每一个非静态方法内部,可以访问静态域和静态方法。
如何应用非静态域和非静态方法
虽然静态域和静态方法不与对象关联,但是他们仍然受到public,private,protect关键字的约束,因此如果要在类的外部访问静态域和静态方法,必须是public的。
对于访问静态域,可以通过两种方式。
ObjectName.StaticField或者ClassName.StaticField
同样的,对于访问静态方法,也可以通过两种方式
ObjectName.StaticMethod()或者ClassName.StaticMethod()