您的位置:首页 > 编程语言 > Java开发

Java中的static关键字详解

2015-08-12 19:17 375 查看
有些很基础的东西, 你以为你懂了, 但其实并不, 只是你用得比较少. 本来以为已经已经掌握了static关键字了, 实际也只是一知半解写个博客做备忘吧. 另外, 越来越觉得只读<<Thinking in Java>> 是不够的, 单是static关键字, 就可以牵扯到 类加载时机, 代码块的执行顺序, JVM编译时的处理, 如果有兴趣还可以看JDBC和hibernate中静态代码块的应用. 很多东西<<Thinking in java>>只是给了个引子,
然后想要深入理解, 还得自己再去搜其他的资料.

"一种情形是只想用一个存储区域来保存一个特定的数据——无论要创建多少个对象,甚至根本不创建对象。另一种情形是我们需要一个特殊的方法,它没有与这个类的任何对象关联。也就是说,即使没有创建对象,也需要一个能调用的方法。为满足这两方面的要求,可使用static(静态)关键字。" ----<< Thinking in Java >>

Static的作用:

static关键字可以用来修饰类内的 成员变量, 方法, 代码块 (静态代码块), 和 类(静态类这里不做介绍). 我们可以在不创建对象的前提下, 通过使用类名来引用被 static 修饰的的成员变量 或 方法 . 注意, static关键字并不会影响类成员的访问权限(不会影响 private, public, protect, 默认的friendly 的作用).

static 修饰类内的代码块时 (静态代码块), 可用于将执行一些我们希望类被加载时候就被执行的操作.由于static 修饰的代码块不管类被加载几次只会执行一次, 所以对于我们优化程序很有帮助.( 例如:将消耗大, 却可以共享/重复使用的成员设为static变量, 在static代码块内进行其初始化操作, 可以避免每次声明一个新的对象都需要初始化这些大消耗的变量. 当然,我们可以直接在static变量声明的时候就赋予它们一个初值, 但是如果我们需要额外进行一些操作, 我们还是需要static代码块的帮助
).

使用静态方法我们需要注意:

1. 不可以在静态方法 / 静态代码块中调用非静态成员变量/方法, 而非静态方法却可以调用静态方法/ 成员 (其实应该理解为在静态方法内不可直接调用非静态成员/方法,
因为如果我们使用对象句柄, 是可以调用的, 对静态方法而言我们需要将一个对象句柄作为参数传入一个static方法, 对静态代码块而言,我们需要定义一个对象类型的静态成员变量)

比如:

/**
* @author csdn libertine1993
*/
class Obj{
public void func(String str){
System.out.println("func called "+str);
};
}

public class MyTest{
private static Obj staObj = new Obj(); //定义了一个对象的静态句柄, 这样就可以在静态代码块借助句柄访问非静态成员/方法
static{
staObj.func("staObj");
}

public static void staMethod(Obj ob){ //将对象句柄作为参数传入静态方法, 可以借助句柄在静态方法中访问非静态成员/方法
ob.func("ob");
}
public static void main(String[] str){
staMethod(new Obj());
}
}


2.不可以在静态方法中使用this关键字

注:关于这两点, 其实也好理解. 非静态成员/ 方法 和 this关键字都是依赖于具体对象的调用的, 而静态方法的调用是不需要依赖于对象的. 当我们通过类名来调用静态方法, 假设静态方法里面出现了非静态成员, 这些非静态成员是属于谁的我们将无从所知.

3.静态成员变量的初始化在只会在类首次加载时候进行,而且[b]初始化只会进行一次(static代码块也只会被执行一次, 这不是巧合), 之后不管new类对象都不会再初始化[/b](实际上一个类在一个虚拟机周期, 只会被加载一次).但是, 对静态成员的赋值是可以多次进行的.

/*
* @author csdn libertine1993
* 测试静态成员的初始化
*/
public class MyTest{
public static int staMember = showUse(126);

static{
staMember = showUse(2006);
}

public static int showUse(int i){
System.out.println(i);
return i;
}

public static void main(String[] agrs){
new MyTest();
new MyTest();
new MyTest();
new MyTest();

System.out.println("staMember is " + staMember);
}
}

/* static int staMember = showUse(126);
static{
staMember = showUse(2006);
}
等价于
static int staMember;
static{
staMember =  = showUse(126);
staMember = showUse(2006);
}
*/


输出:



解析:
要理解这么输出的原因, 首先要知道静态变量的声明和初始化是两个不同的过程. 变量的声明在编译的时候就明确了. 初始化可以视为是在static块内进行(看代码下方的注释, 这也就是static块在类内可以写在变量声明之前的原因), 因此首次加载类时, 我们在static块内进行的等价操作是一次将126, 2006赋值给staMember(初始化), 因此依次输出126, 2006. 然而 正如前面说的, 静态成员变量的初始化只会在类首次加载时初始化,
而且只初始化一次(static 块只在首次加载执行一次), 不管后面怎么new对象, 都不会再次进行初始化.因此不管后面怎么new,不会再有与初始化相关的 输出, staMember的最终值为2006.

静态代码块与相对于其他代码的执行顺序:

先看代码结构示例:
/**
*  @author csdn libertine1993
*  代码结构
*/
public class CodeStructure{
public int a;     //非静态代码块, 如有操作, 需要类似静态代码块加大括号

static {
//静态代码块, 使用static关键字修饰
}

CodeStructure(){} //构造方法
}


接着我们需要了解类的加载时机: (引自 csdn newjerryj )

类加载特性 :
*在虚拟机的生命周期中一个类只被加载一次。
*类加载的原则:延迟加载,能少加载就少加载,因为虚拟机的空间是有限的。
*类加载的时机:
1)第一次创建对象要加载类.
2)调用静态方法时要加载类,访问静态属性时会加载类。
3)加载子类时必定会先加载父类。
4)创建对象引用不加载类.
5) 子类调用父类的静态方法时
(1)当子类没有覆盖父类的静态方法时,只加载父类,不加载子类
(2)当子类有覆盖父类的静态方法时,既加载父类,又加载子类
6)访问静态常量,如果编译器可以计算出常量的值,则不会加载类,例如:public static final int a =123;否则会加载类,例如:public static final int a = math.P         I。


然后让我们明确类代码的执行顺序, 次数, 执行时机:
静态代码块:
1.最先执行的块
2.只在类首次加载时执行(即只会被执行一次)
3.如果有基类的话, 基类和子类的都要执行, 基类先于子类执行

非静态代码块:
1.执行次序次于静态代码块, 先于构造方法
2.与构造方法一样,
新建一个对象执行一次
3.如果有基类的话, 基类和子类的都要执行, 基类先于子类执行

构造方法:
1.执行次序最后
2.与非静态代码块一样, 新建一个对象执行一次
3.如果有基类的话, 基类先于子类执行, 基类和子类的都要执行

注意:
在执行完所有需要加载的父类基类的 static块之前是不会执行任何类的非静态代码和构造方法的, 另外每次new对象都会执行一次 非静态代码块+构造方法, 父类的非静态代码块/构造方法 全部执行完毕之后, 才会轮到子类的静态代码块和构造方法.

读者可以执行下面代码, 观察输出
/**
*  执行次序与次数测试, 每个块被调用的时候都会有一条输出记录
*  @author csdn libertine1993
*/
import java.util.*;

class  Father{
Father(){
System.out.println("Father constructor called");
}

{
System.out.println("Father not static initialization block called");
}

static{
System.out.println("Father static initialization block called");
}

}

public class Son extends Father{
Son(){
System.out.println("Son constructor called");
System.out.println();
}

{
System.out.println("Son not static initialization block called");
}

static{
System.out.println("Son static initialization block called");
System.out.println();
}

public static void main(String[] args){
new Son();
new Son();
new Son();
}

}
输出:



解析:
正如我们前面所提及的, 调用静态方法时会加载类(程序入口main就是一个静态方法), 我们调用了类Son的静态方法main, 所以我们需要加载类Son, 但是类Son继承Father类, 因此会先加载Father类, 这样就会先执行Father 的static block, 然后是Son 的static block. 此时我们并没有new对象, 因此没有执行非静态代码块和构造方法. 接着我们new了3个Son, 由于前面已经加载过了Son和Father类, static块只会在首次加载执行,
因此不会再执行static块. 但是创建3个对象, 会导致非静态代码和构造方法都会分别被执行三次(新建一个对象执行一次), 同样的, 如果涉及继承, 父类块会优于子类执行, 因此有了下面的三块输出.

最后再结合面试题, 愉快的将前面提到的知识点串联起来(静态成员变量的初始化, 代码执行顺序, 类加载时机).
题1:求下面代码的输出:

/* 代码引自 cnblogs 海子 */
public class Test {
Person person = new Person("Test");
static{
System.out.println("test static");
}

public Test() {
System.out.println("test constructor");
}

public static void main(String[] args) {
new MyClass();
}
}

class Person{
static{
System.out.println("person static");
}
public Person(String str) {
System.out.println("person "+str);
}
}

class MyClass extends Test {
Person person = new Person("MyClass");
static{
System.out.println("myclass static");
}

public MyClass() {
System.out.println("myclass constructor");
}
}


输出:



解析:
首先,我们找到程序入口是Test类的main方法, 因此会最先加载test类, 执行Test类static块 输出test static. 此时, 由于没有new Test对象, 不会执行非静态代码和构造方法.

接着开始执行main方法, new 了一个MyClass 对象. 那么我们需要加载MyClass对象了, 虽然MyClass继承自Test类, 但是Test类已经加载过一次了, 不用再次加载. 那么我们就直接加载MyClass类, 执行MyClass的static块输出 myclass static.

接着开始执行非静态代码块和构造方法, 由于MyClass继承自Test, 所以我们需要先执行Test的非静态代码块和构造方法, Test的非静态代码块new了一个Person, 因此我们需要加载Person类, 首次加载执行Person的static块, 输出person static.

现在在执行父类的非静态代码块的new Person操作, 执行Person的静态代码块, 输出person static. 然后开始执行Person的静态代码块和构造方法, 由于这个new的person是在Test类内创建的, 所以输出person Test.

执行完父类的非静态代码块, 执行父类的构造函数, 输出test constructor.

接着执行子类的非静态代码块, 子类非静态代码块也有一个 new person 的操作, 但是person的static块已经执行过了, 所以我们直接执行person的非静态代码块(person这里没有非静态代码块)和构造方法,输出person myclass.

执行完子类的非静态代码块, 执行子类的构造方法, 输出myclass constructor.

题2:求输出

/* 代码引自csdn论坛 */
public class TestStaticCode {
private static TestStaticCode tsc = new TestStaticCode();
static{
System.out.println("4");
}
private InstanceVariable iv = new InstanceVariable();

private TestStaticCode(){
System.out.println("3");
}

public static void main(String[] args){
}
}

class InstanceVariable {
static{
System.out.println("1");
}
public InstanceVariable(){
System.out.println("2");
}
}


输出:



解析:

如果没吃透前面的知识(静态变量的初始化), 很容易就错认为最先输出的是4. 实际上, 前面讲静态变量的初始化时候, static TestStaticCode tsc = new TestStaticCode() 等价于

static TestStaticCode tsc; static{ tsc = new TestStaticCode() }, 因此, 实际上tsc = new TestStaticCode() 可以视为是在静态代码块内进行的第一条语句. 在static块内, 最先被执行的是new TestStaticCode() 的操作. 而TestStaticCode类已经被加载了, 那么就开始执行TestStaticCode类的非静态代码块和构造方法, 而TestStaticCode的非静态代码块内又有new
InstanceVariable(), 因此先加载InstanceVariable, 执行其static块输出1, 再执行其构造方法, 输出2,.

执行完TestStaticCode的非静态代码块就可以执行其构造方法, 输出3.

到此, 等价的static块的第一个语句 tsc = new TestStaticCode()执行完毕, 开始执行static块的最后一个语句, 输出4.

参考资料:
http://blog.sina.com.cn/s/blog_86e34ca801014lex.html http://blog.csdn.net/newjerryj/article/details/8650268 http://bbs.csdn.net/topics/380211495
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: