一、static关键字的使用场景
我们这里所说的静态,无非就是两种,一种是静态变量,一种是静态函数,我们分这两种情况进行说明static关键字的使用场合。
一、静态变量
我们在7.8节中对static关键字特点和7.9节中成员变量与静态变量区别进行学习的时候就已经很明确的指出了静态变量是共享数据,而对象中封装的特有数据。
因此我们可以这样解释:当分析对象中所具备成员变量的值是相同的,且不需要对象做修改,这时这个成员变量就可以被static关键字修饰为静态。
从另一个角度也就是说,只要数据在对象中都是不同的,也就是对象的特有数据,必须存在在对象中,则就是非静态的,不能被static关键字修饰为静态。
因此用static关键字定义静态变量的使用场合:如果是相同数据,且对象不需要做修改,只需要使用,且不需要存储在对象中,则使用static关键字定义成静态。
二、静态函数
我们在7.10static关键字使用细节时说到了静态方法中不能调用非静态成员,且不能使用this关键字,所以对于函数是否使用static关键字修饰,只需要参考一点,就是该函数功能是否有访问到对象中的特有数据。
为了帮助我们理解,我们可以通俗的说,从源码看,该功能是否需要访问非静态成员变量,如果需要,该功能就是非静态的,如果不需要,我们就可以将该功能用static关键字定义成静态函数。
当然,我们也可以定义成非静态,但是非静态需要对象去调用,而当创建了对象却没有访问特有数据的方法,该对象创建是没有意义的。因为我们之前说过对象是用于封装特有数据的,当我们没有访问特有数据,那么我们为什么要创建对象呢?创建的对象只是浪费了堆内存中的空间,再什么也没有做,所以说此时创建对象是没有意义的。
因此用static关键字定义静态函数的使用场合:如果一个函数不需要访问非静态成员变量,就可以用static关键字定义成静态。
**
**这一节我们就简单的说这些吧。
二、静态的内存加载
我们还是先看一个例子,希望我们通过对这个例子的分析让我们初学者们对static所修饰的静态在内存中的具体体现有一个深刻的理解。
class Person
{
String name;//姓名,这是一个成员变量
int age;
static String country = "美国";//国籍,这是一个静态变量
Person(String name,int age)//构造函数
{
this.name = name;
this.age = age;
}
public void printInfo()//非静态函数
{
System.out.println(name+":"+age);
}
public static void printCoun()//静态函数,打印静态变量country
{
System.out.println(Person.country);
}
}
class StaticTest
{
public static void main(String[] args)
{
Person.printCoun();
Person p = new Person("科比",37);
p.printInfo();
}
}
我们先来看运行结果,然后再一步步分析:
例子很简单,结果也很明显,我们之前在7.9中谈成员变量与静态变量的区别时提到了一个区别就是成员变量是存储在堆内存中的对象中,而静态变量则存储在方法区中的静态区中。
这里,我们就引入了内存的一个新区域,那就是方法区,对于方法,当程序运行时,都会被存储在这个区域。
那么我们就对上面的代码运行过程和内存变化进行分析,当然在我们分析之前,我们必须明确一个常识,那就是当我们执行类时,类就会进入内存。
**
**
那么对于上面的代码,分析过程就会很清晰了:
1.当我们运行程序时,StaticTest类进入内存,虚拟机会在方法区的非静态区中分配空间存储StaticTest(){}默认构造函数,同时在方法区的静态中分配空间存储static main(){……}主函数,当然包括主函数的所有代码的字节码。
2.静态区的main函数进栈内存,main方法中有一个对象变量p。
3.执行Person.printCoun(){}方法,Person类进入内存,方法区的非表态区分配空间存放构造函数Person(name,age){……}和非静态函数void printInfo(){……},在方法区的静态区中分配空间存储静态变量country="美国"和静态方法printCoun(){……}。
4.静态区的printCoun()方法进栈内存,并从静态区找到静态变量country并打印,控制台输出:“美国”。
5.printCoun()方法执行结束,跳出方法,printCoun()方法出栈内存。
6.执行Person p = new Person("科比",37),此时堆内存中创建空间存储对象,这里假设地址为0x0056,则所属this=0x0056,并有成员变量name和age。
7.非静态区的构造函数Person(name,age)进栈内存,对对象进行初始化,为堆内存中的对象进行初始化,name=科比,age=37。
8.初始化完成,把地址0x0056赋值给对象p,p=0x0056。
9.构造函数出栈内存,释放参数name和age。
10.执行p.printInfo()语句,非表态区的printInfo()方法进栈内存,this=0x0056。
11.打印this所指向的成员变量this.name和this.age,控制台输出:科比:37。
12.printInfo()方法执行结束,跳出方法,方法出栈内存。
13.main()函数执行结束,跳出,函数出栈内存。
14.程序运行结束。
上面我们对例子中的代码进行了逐步分析,基本上明晰了static关键字所修饰的静态在程序运行时在内存中的具体变化,希望在以后的实际开发过程中有所帮助。
最后我们再说一个小知识点:存储在方法区中的变量和方法都会对象所共享,所以方法区又称为共享区。
**
**
三、静态代码块
这一节我们看一个比较特殊的概念,那就是静态代码块。
前面我们也提到过代码块,就是一段独立的代码空间,那么什么是静态代码块呢?说白了,就是用static关键字修饰的代码块。
我们来看一个例子:
class StaticBlock
{
static
{
System.out.println("静态代码块被执行");
}
void myPrint()
{
System.out.println("myPrint方法执行");
}
}
class StaticBlockTest
{
public static void main(String[] args)
{
new StaticBlock().myPrint();
}
}
我们来看一看运行结果:
从结果我们看到了我们调用的myPrint函数被调用执行了,并且在此函数被调用之前,静态代码块就已经被执行。
这就是我们要说的特别之处,静态代码块是特殊的代码块,它被static关键字修饰,并且拥有静态的所有特征,最主要的是它有一个比较自然独特的特点:我们之前说,静态随着类的加载而加载,而静态代码块随着着类的加载而执行,只要类被加载,那么该静态代码块就会被执行,并且只执行一次。
我们看下面的测试:
class StaticBlockTest
{
public static void main(String[] args)
{
new StaticBlock().myPrint();
new StaticBlock().myPrint();
}
}
结果:
我们看到,静态代码块只执行了一次,而我们的myPrint方法被调用了两次执行了两次。所以当类加载时,静态方法就已经加载并且执行一次。
通过上面的例子让我们明确了静态代码块的实际作用:用于给类进行初始化。
这就相当于我们之前学习的构造函数,构造函数是用于给对象进行初始化,而静态代码块是用来给类进行初始化。
这里我们也许会有疑问,既然构造函数能够进行初始化,那么我们为什么还要用静态代码块来初始化呢,其实不是所有的类都能创建对象,因为有些类有可能不需要被创建对象的,比如我们在一个类中定义的全部是静态成员,那么创建对象就没有意义。
我们再看一个静态代码块的用法,我们看下面的代码:
class StaticBlock
{
static int num;
static
{
num = 10;
num = num * 3;
}
void myPrint()
{
System.out.println("num = "+num);
}
}
class StaticBlockTest
{
public static void main(String[] args)
{
new StaticBlock().myPrint();
}
}
我们来看运行结果:
从结果我们直接可以看到,我们在静态代码块中对静态变量进行了多次运算和赋值,所以当我们需要对静态变量进行多次运算时我们可以运用静态代码块。
不过这个在开发中用的并不多,在一些底层的框架开发中会专门用到。
这一节我们就简单的学到这里。
四、构造代码块
这一节我们再看一个特殊的代码块,那就是构造代码块。
这里我们简单的通过例子来说明一下:
class Person
{
private String name;
{
System.out.println("Person类的第一个代码块被执行");
}
Person()
{
System.out.println("无参数构造函数被执行");
this.name = "小宝宝";
}
Person(String name)
{
System.out.println("有name参数构造函数被执行");
this.name = name;
}
public void speak()
{
System.out.println("名字:"+name);
}
}
class ConBlockTest
{
public static void main(String[] args)
{
Person p1 = new Person();
p1.speak();
Person p2 = new Person("小科比");
p2.speak();
}
}
我们在这个例子中看到了Person类中有一个代码块,它没有被static关键字修饰,这就是我们这一节所说的构造代码块,为什么这么说呢,我们看运行结果:
我们很显然就看到了在我们创建两个对象时,该代码块都被执行了,而构造函数只是当创建对应对象时被调用。
所以构造代码块的作用就是:给所有对象进行相同部分的初始化。
而我们的构造方法是对对应的对象进行有针对性的独特的初始化。
那么构造代码块的构造函数哪个先执行呢?我们看代码:
class Person
{
private String name;
{//第一个构造代码块
System.out.println("Person类的第1个代码块被执行");
}
Person()
{
System.out.println("无参数构造函数被执行");
this.name = "小宝宝";
}
Person(String name)
{
System.out.println("有name参数构造函数被执行");
this.name = name;
}
public void speak()
{
System.out.println("名字:"+name);
}
{//第二个构造代码块
System.out.println("Person类的第2个代码块被执行");
}
}
我们看结果:
我们看到两个不同位置的构造代码块都在构造函数被执行之前就已经执行了,所以说构造代码块优先于构造函数执行。
**
**
所以,当我们需要把所有对象都有相同的初始化时,我们可以使用构造代码块来实现,比如上面的例子中,人一出生都会哭,那么我们就可以用构造代码块来初始哭这个功能:
class Person
{
private String name;
{
cry();
}
Person()
{
this.name = "小宝宝";
}
Person(String name)
{
this.name = name;
}
public void cry()
{
System.out.println("哇哇");
}
public void speak()
{
System.out.println("名字:"+name);
}
}
这样我们就把所有对象哭的功能封装到了一个构造代码块中,在创新对象是会优先执行,很好的实现了我们想要的功能。