第六章《类的高级特性》第1节:static关键字的使用

时间:2022-12-30 19:04:35

static意为“静态”,在Java语言中,使用static关键字可以定义静态属性、静态方法和静态块。

6.1.1 静态属性

在第5章中,我们定义了一个Person类的子类Student,用它来表示学生。假如每一个在读学生每年都能得到1000元的助学津贴,并且程序员希望在Student类中以属性的形式把津贴的金额表示出来,我们就可以按如下方式定义Student类。

class Student extends Person{
int num;//学号
int allowance = 1000;//津贴金额
...
}

如果用以上Student类创建多个对象,那么把这些对象用图形表示出来就如图6-1所示。

第六章《类的高级特性》第1节:static关键字的使用

图6-1 多个Student对象

从表面上看,用图6-1所示的方式在内存中存储多个学生对象的信息似乎并没有什么不妥,但仔细观察就能发现一些问题:首先,1000元的助学津贴是针对所有学生的,也就是说每个学生的津贴金额都为1000元,这样的话内存中每一个Student对象的allowance属性值都是1000。图6-1中,1000这个数字在内存中被存储了很多次,造成了内存空间的浪费。其次,如果国家把所有学生的津贴金额调整为1500元,就需要把每个Student对象的allowance属性值全部都修改一遍,这会带来很大的工作量。如果能以图6-2所示的方式存储Student类对象,就能很好的解决以上两个问题。

第六章《类的高级特性》第1节:static关键字的使用

图6-2多个对象的属性共用内存单元

图6-2中,多个对象共用了一块内存单元来保存它们的allowance属性。这样的话,1000这个数字就只被存储了1次,极大的节省了内存空间。更重要的是:如果要调整助学津贴的金额,只需要修改一块内存空间中的数据即可,大大的减少了修改数据的工作量。那么,用一块共同的内存单元来保存多个对象的allowance属性,这种做法是否合理呢?前文说过:每个学生每年可以得到1000元的助学津贴,这表明1000元的津贴金额是为学生这个“群体”规定的,并不是针对某一个人规定的。既然1000元的津贴金额属于学生这个群体而不是个人,所以多个学生(Student)对象共用一块内存单元来保存allowance属性是完全合理的。

那么,如何才能让多个Student对象共用一块内存单元来保存它们的allowance属性呢?在Java语言中,如果在某个属性的前面加上static关键字,那么用来保存该属性的内存单元将被多个同类对象共用。被static关键字所修饰的属性被称为“静态属性”。下面的【例06_01】就能很好的证明静态属性的内存单元由多个对象共用。

【例06_01 静态属性被多个对象共享】

Student.java

public class Student  extends Person{
int num;//学号
static int allowance = 1000;//津贴金额

Student(){

}

Student(String name ,char sex ,int age,int num){
super(name,sex,age);//①初始化父类对象的属性
this.num = num;//②初始化子类对象自身的属性
}

int sum(int n) {
int r = 0;
if(n<1) {
r = n;
}else {
r = (1+n)*n/2;//用等差数列求和公式完成累加求和
}
System.out.println("子类的sum()方法求和结果:"+r);
return r;
}
}

Exam06_01.java

public class Exam06_01 {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
s1.allowance = 1200;//①
System.out.println("助学津贴的金额为:"+s2.allowance); //②
}
}

在【例06_01】中,Student类的allowance属性被static关键字所修饰,成为了一个静态属性,它的默认值为1000。在Exam06_01的main()方法中,s1和s2指向了两个完全独立的对象。语句①通过s1修改allowance属性,使其值变成了1200,语句②打印了s2的allowance属性值。【例06_01】的运行结果如图6-3所示。

第六章《类的高级特性》第1节:static关键字的使用

图6-3 【例06_01】运行结果

通过图6-3可以看出:s1和s2指向了两个独立Student对象,但通过s1却能把s2的allowance属性值修改为1200,这充分说明了这两个Student对象的allowance属性共用了同一块内存单元。此处再次提醒各位读者:本例仍然沿用了第4、5两章的Person类和Student类,但在本例中为Student类增加了一个静态属性allowance,想要正确运行本例须从《范例6-1》中下载Person及Student类的源代码。

在Java语言中,静态属性的意义可以分为两个角度去理解:首先从技术上,静态属性的内存单元由多个对象所共享。其次从逻辑上,静态属性是为某一类事物定义的属性,与具体的对象无关,因此通常情况下静态属性都会被设置一个公共的初始默认值。以Student类的allowance属性举例:它表示所有学生的津贴金额都是1000元,无论是张三还是李四,只要他是一个学生,他的助学津贴金额都是1000元。既然静态属性是为某一类事物所定义的属性,而Java语言中一个类就表示一类事物,那么静态属性就可以直接通过类名去访问,下面的【例06_02】就展示了如何使用类名去访问静态属性:

【例06_02 通过类名访问静态属性】

Exam06_02.java

public class Exam06_02 {
public static void main(String[] args) {
System.out.println("学生助学津贴的金额为:"+Student.allowance);
}
}


【例06_02】的运行结果如图6-4所示。

第六章《类的高级特性》第1节:static关键字的使用

图6-4 【例06_02】运行结果

很多读者可能会问:对象没有被创建时,虚拟机就不会给这个对象分配内存空间,没有内存空间怎么能保存allowance属性的值呢?根据图6-2可知,静态属性的内存单元并不属于某一个具体的对象,而是被该类的所有对象共用。这块被所有对象共用的内存空间并不是在对象创建时被分配的,而是在类加载的时候就已经分配了。那么,什么是“类加载”呢?

一个类被定义出来后,这个类的信息会被编译成字节码文件并保存在计算机的硬盘上。当需要创建这个类的对象时,虚拟机首先会把硬盘上的字节码文件读取到内存中,然后根据读取到的信息才能创建出这个类的对象。例如,要创建一个Student类的对象,虚拟机首先要把保存在字节码文件中的Student类信息全部读取到内存中,接下来再根据读到的信息分析这个类有几个属性,这些属性分别是什么类型。只有把这些信息都分析清楚之后,虚拟机才能计算出存储一个Student类对象需要多大内存空间,最后根据计算结果为这个对象分配相应的内存空间并初始化对象的各个属性。以上就是创建一个对象的基本过程,其中把字节码文件读入内存的操作就被称为“类的加载”。

不难看出,类的加载先于对象的创建。而静态属性的内存单元就是在类加载的过程中分配的,因此,在对象还没有被创建的时候,用于存储静态属性值的那块内存单元就已经存在了,所以我们可以在没有创建对象的情况下,直接通过类名称访问静态属性。Java语言允许程序员直接通过类名称去访问静态属性,也体现出静态属性是属于某一个类的,它与具体的对象无关。而在日常编程时,建议大家使用类名而不是引用的名称来访问静态属性,这样更能凸显静态属性的逻辑意义。

6.1.2 静态方法

static关键字除了可以修饰属性,还可以修饰方法,专业上把被static关键字修饰的方法称为静态方法。同静态属性一样,静态方法也可以直接通过类名完成调用。静态方法能够通过类名直接调用,也反映出它的两个特性:首先,从技术角度来说,静态方法可以在没有创建对象的情况下直接执行。其次,从逻辑角度来说:静态方法与具体的对象无关。

使一个方法成为静态方法的步骤很简单,只需要在方法的前面加上static关键字即可。但大多数初学者都不知道应该在什么情况下把一个方法设定为静态方法。其实,只要依据静态方法的两个特性就能很容易的判断出在什么情况下应该把一个方法设定为静态方法。

首先,一个方法如果在没有创建对象的情况下就能够被执行,那么这个方法就应该被设定为静态方法。最典型的就是大家熟知的main()方法。在一个Java程序尚未启动执行时,虚拟机还没有创建任何一个对象。如果想要启动程序,必须要有一个方法在对象根本不存在的情况下就能启动执行,静态方法就具有这样的特性,因此作为程序入口的main()方法就被设计成了静态方法。所以,如果希望一个方法在没有对象存在的情况下就能够被调用,就应该把它设定为静态方法。

其次,一个方法如果与具体的对象无关,这个方法也应该被设定为静态方法。所谓“与具体的对象无关”,是指无论用哪一个对象来执行这个方法,其运行结果都是相同的。例如之前定义的Person类,在Person类中有一个用来完成累加求和的sum()方法,程序员无论调用哪一个Person对象的sum()方法来计算1到100的累加求和,其结果都是5050,这样的方法就是“与具体的对象无关的方法”,因而它就应该被设定为静态方法。根据这一原则设计出来的还有Math类。Math类是Java基础类库中的一个类,它位于java.lang包。该类提供了求绝对值、求平方根、求各种三角函数等很多用于数学计算的方法。这些方法都被设定成了静态方法,之所以把它们设定为静态方法,就是因为无论通过哪个Math类对象来调用这些方法,方法的运算结果都不会有任何差别。

深刻理解了静态方法的这两个特性,我们就知道在什么情况下应该把一个方法设定为静态方法。但还需谨记:任何情况下都不能把构造方法设为静态方法。此外,静态方法的两个特性还导致了在静态方法中不能直接调用非静态属性。所谓“直接调用”,是指在调用一个属性或方法时,在属性和方法的前面没有加任何引用。例如,Person类的sayName()方法中有如下语句:

System.out.print("我叫"+name);

这条输出语句中调用了name属性,并且name属性前面没有加任何引用,这就属于“直接调用”。由于静态方法不能直接调用非静态属性,所以假如把sayName()设定为静态方法,sayName()方法中所调用的name属性在编辑器中将会变成红色以提示开发者静态方法中不能调用这个属性,如图6-5所示。

第六章《类的高级特性》第1节:static关键字的使用

图6-5 静态方法直接调用非静态属性

为什么在静态方法中不能直接调用非静态属性呢?为了解答这个问题,可以先回顾一下普通方法(也就是非静态方法)的执行过程。大家知道:普通方法不能通过类名直接调用,因此每次调用普通方法之前,必须先创建对象,例如想调用普通方法sayName(),须按如下方式编写代码:

Person p = new Person();
p.sayName();

可以看到,在执行sayName()方法前,就已经创建了p对象。p对象的创建,就意味着用于存储name属性的内存单元已经被分配。而sayName()方法中直接调用的那个name属性,正是p对象自身的name属性。因为用于存储name属性的内存单元已经被分配,p对象才能在执行sayName()方法时顺利的调用到name属性。

而静态方法则不同,静态方法可以在没有创建对象的情况下直接执行。但是,用于存储非静态属性的内存单元却必须等到对象被创建之后才会被分配。如果允许在静态方法中直接调用非静态属性,就会导致静态方法必须在对象已经被创建的情况下才能执行,这样的话,静态方法就和普通方法没有任何区别。基于这个原因,Java语言规定:在静态方法中不允许直接调用非静态属性。其实,静态方法中也不能直接调用普通方法(即非静态方法),这是因为普通方法可以直接调用非静态属性,如果允许在静态方法中直接调用普通方法,本质上等同于允许在静态方法中直接调用非静态属性。此外,静态方法中也不允许出现this和super关键字,因为this关键字表示本对象自身,而super关键字表示本对象当中所包含的那个父类对象。这两个关键字出现在静态方法中,也会导致静态方法必须在对象已经被创建的情况下才能执行。综上所述:对静态方法规定的各项语法规则,都是为了保证它在没有对象的情况下就能执行,也可以理解成是为了保证静态方法能够成为“与具体对象无关”的方法。

虽然我们不能在静态方法中“直接”调用非静态属性和方法,却可以在静态方法中创建一个对象,并通过对象去调用它的非静态属性和方法。下面的【例06_03】就很好的展示了这个特性:

【例06_03 在静态方法中创建对象并调用其非静态属性和方法】

Exam06_03.java

public class Exam06_03 {
int x;//非静态属性
void method() {//非静态方法
System.out.println("非静态方法");
}
public static void main(String[] args) {
Exam06_03 exam = new Exam06_03();
exam.x = 5;//①
System.out.println("x的属性值为:"+exam.x);//②
exam.method();//③
}
}

【例06_03】的Exam06_03类中,main()方法就是一个静态方法,但是在main()方法中却出现了非静态属性x以及非静态方法method()。仔细阅读代码不难发现:静态的main()方法中对x属性及method()方法都不是采用“直接调用”的方式,而是先创建了exam对象,之后通过exam对象完成的调用。因为先创建了exam对象,所以再调用它的非静态属性和方法都不会有任何问题。举出【例06_03】是希望告诉各位读者:在静态方法中不能“直接调用”非静态属性和方法,但如果先创建一个对象,然后调用该对象的非静态属性和方法是完全可以的。

static关键字可以用于修饰方法,那么,static关键字是否会对方法的重写产生影响呢?Java语言规定:如果父子两个类中都定义了名称、参数及返回值类型相同的方法,那么这两个方法前面是否添加static关键字必须做到“父子一致”。例如,在5.3小节的【例05_04】中,我们为Person类中定义了一个sum()方法,并且在它的子类Student中也定义了名称、参数及返回值类型相同的sum()方法。该案例中,父类的sum()方法前面并没有添加static关键字,那么子类Student的sum()方法前面也不能添加static关键字。相应的,如果在Person类sum()方法前添加上static关键字,那么子类Student的sum()方法前面也必须添加static关键字,这就是所谓“父子一致”。如果违反了“父子一致”原则,将会出现语法错误。但需要注意,在父子两个类的方法前如果都出现了static关键字,虚拟机将不再根据对象的类型来决定调用哪一个方法,出现这种现象的原因就是:静态方法与具体的对象无关。因此,如果父类中有一个静态方法,而子类中也定义了一个与之名称、参数及返回值类型都相同的方法,这两个方法之间所形成的关系就不能叫做“重写”或“覆盖”,而应该叫做“屏蔽”。读者把Person类和Student类的sum()方法前面都添加上static关键字,之后运行下面的【例06_04】就能验证这个结论。

【例06_04 子类屏蔽父类的静态方法】

Exam06_04.java

public class Exam06_04 {
public static void main(String[] args) {
Person p = new Student();
p.sum(100);
}
}

【例06_04】的运行结果如图6-6所示。

第六章《类的高级特性》第1节:static关键字的使用

图6-6 【例06_04】运行结果

从图6-6可以看出:虽然引用p指向的是子类(Student)对象,但仍然执行了父类(Person)的sum()方法。产生这样的运行结果就是因为:静态方法sum()与具体对象无关,p虽然指向的是子类Student类的对象,但p本身却是Person类的引用,通过引用p调用静态方法sum(),与直接通过类名Person调用它是一样的效果。其实,即使引用p指向的是一个空对象null,通过它也能调用到Person类的静态方法sum()。需要提醒各位读者:在父类的静态方法前面加上final关键字也能起到阻止屏蔽的作用。

6.1.3 静态块

static关键字除了可以修饰属性和方法之外,还可以修饰定义在类中的块。所谓“块”其实就是添加在代码中的一对大括号,而定义在类中的块就是指直接出现在类当中的一对大括号。被static关键字修饰的、并且是定义在类中的块被称为“静态块”。例如:

class Person{
...
static {
System.out.println("父类静态块");
}
...
}

这段代码中,在Person类中添加了一对用static关键字修饰的大括号。并且这对大括号不是定义在某个方法中,而是直接出现在类中,这就是一个典型的"静态块"。静态块中可以写上一些语句,例如以上代码中,静态块里写了一条输出语句。与静态方法一样,在静态块中不能直接调用非静态属性和普通方法,也不能出现this和super关键字。

静态块中的语句是在类加载时执行的。前文讲过:类的加载先于对象的创建,所以静态块中的语句先于构造方法执行。如果一个类之上还有父类,那么在加载本类之前会先加载父类,因此,如果父子两个类都有静态块,将会按照“先父后子”的顺序依次执行父子两个类的静态块代码。需要提醒各位读者:类加载的操作只会执行一次,通常虚拟机都是在第一次创建某个类对象时完成这个类的加载操作。由于类的加载也遵循“先父后子”的顺序,所以如果是第一次创建子类的对象,并且在创建子类对象之前没有创建过父类对象,那么虚拟机会按照“加载父类->加载子类->执行父类构造方法->执行子类构造方法”的顺序完成子类对象的创建。为了验证这个结论,各位读者可以按照以下方式给Person和Student两个类中分别添加静态块,并且在它们的其无参数构造方法中添加输出语句:

public class Person {
...
static {//为父类添加静态块,并在块中添加输出语句
System.out.println("父类静态块");
}
...

Person(){//为父类构造方法添加输出语句
System.out.println("父类构造方法");
}
...
}
public class Student extends Person{
...
static {//为子类添加静态块,并在块中添加输出语句
System.out.println("子类静态块");
}
...
Student(){//为子类构造方法添加输出语句
System.out.println("子类构造方法");
}
...
}

修改完Person和Student两个类之后,就可以运行下面的【例06_05】,当然,读者也可以直接从《范例6-5》中直接下载Person和Student类的源代码。

【例06_05 创建子类对象的完整过程】

Exam06_05.java

public class Exam06_05 {
public static void main(String[] args) {
Student s1 = new Student();
Student s2 = new Student();
}
}

【例06_05】的运行结果如图6-7所示。

第六章《类的高级特性》第1节:static关键字的使用

图6-7 【例06_05】运行结果

从图6-7可以看出:当创建s1对象时,首先分别执行了父子两个类的静态块代码,这说明类的加载遵循“先父后子”的顺序。在这之后,又执行了父子两个类的构造方法。静态块代码早于构造方法执行,也说明了类的加载先于对象的创建。当创建s2对象时,由于已经完成了类的加载,所以直接以“先父后子”的顺序执行两个类的构造方法。父子两个类的静态块代码都只被执行了一次,充分说明类加载的操作只会执行一次,第二次创建对象时无需再次加载这个类。

除此文字版教程外,小伙伴们还可以点击这里观看我在本站的视频课程学习Java。