【Java】Java关键字-static 关键字

Posted by 西维蜀黍 on 2019-04-01, Last Modified on 2021-09-21

static关键字的用途

在《Java编程思想》P86页有这样一段话:

“static方法就是没有this的方法。在static方法内部不能调用非静态方法,反过来是可以的。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”

这段话虽然只是说明了static方法的特殊之处,但是可以看出static关键字的基本作用,简而言之,一句话来描述就是:

方便在没有创建对象的情况下来进行调用(方法/变量)。

很显然,被static关键字修饰的方法或者变量不需要依赖于对象来进行访问,只要类被加载了,就可以通过类名去进行访问。

static可以用来修饰类的成员方法、类的成员变量,另外可以编写static代码块来优化程序性能。

静态方法(static方法)

static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。

并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。


而对于非静态成员方法,它访问静态成员方法/变量显然是毫无限制的。

因此,如果说想在不创建对象的情况下调用某个方法,就可以将这个方法设置为static。我们最常见的static方法就是main方法,至于为什么main方法必须是static的,现在就很清楚了。因为程序在执行main方法的时候没有创建任何对象,因此只有通过类名来访问。

静态变量(static变量)

static变量也称作静态变量,静态变量和非静态变量的区别是:静态变量被所有的对象所共享,在内存中只有一个副本,它当且仅当在类初次加载时会被初始化。而非静态变量是对象所拥有的,在创建对象的时候被初始化,存在多个副本,各个对象拥有的副本互不影响。

static成员变量的初始化顺序按照定义的顺序进行初始化。

static代码块

static关键字还有一个比较关键的作用,就是用来形成静态代码块以优化程序性能。static块可以置于类中的任何地方,类中可以有多个static块。

在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次。

例子

为什么说static块可以用来优化程序性能,是因为它的特性:只会在类被加载的时候被执行一次。下面看个例子:

class Person{
    private Date birthDate;
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        Date startDate = Date.valueOf("1946");
        Date endDate = Date.valueOf("1964");
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:

class Person{
    private Date birthDate;
    private static Date startDate,endDate;
    static{
        startDate = Date.valueOf("1946");
        endDate = Date.valueOf("1964");
    }
     
    public Person(Date birthDate) {
        this.birthDate = birthDate;
    }
     
    boolean isBornBoomer() {
        return birthDate.compareTo(startDate)>=0 && birthDate.compareTo(endDate) < 0;
    }
}

因此,很多时候会将一些只需要进行一次的初始化操作都放在static代码块中进行。

static关键字的误区

能通过this访问静态成员变量吗?

虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗?

先看下面的一个例子,这段代码输出的结果是什么?

public class Main {  
    static int value = 33;
 
    public static void main(String[] args) throws Exception{
        new Main().printValue();
    }
 
    private void printValue(){
        int value = 3;
        System.out.println(this.value);
    }
}

结果

33

分析  

这里面主要考察对this和static的理解。

this代表什么?this代表当前对象,那么通过new Main()来调用printValue的话,当前对象就是通过new Main()生成的对象。

而static变量是被对象所享有的,因此在printValue中的this.value的值毫无疑问是33。在printValue方法内部的value是局部变量,根本不可能与this关联。

所以输出结果是33。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

常见的笔试面试题

下面列举一些面试笔试中经常遇到的关于static关键字的题目,仅供参考。

问题 1 - 这段代码的输出结果是什么?

public class Test {
     
    static{
        System.out.println("test static 1");
    }
    public static void main(String[] args) {
         
    }
     
    static{
        System.out.println("test static 2");
    }
}

结果

test static 1
test static 2

分析  

虽然在main方法中没有任何语句,但是还是会输出,原因上面已经讲述过了。另外,static块可以出现类中的任何地方(只要不是方法内部,记住,任何方法内部都不行),并且执行是按照static块的顺序执行的。

问题 2 - 下面这段代码的输出结果是什么?

public class Child extends Parent {
    static {
        System.out.println("Child static");
    }

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

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

class Parent {
    static {
        System.out.println("Parent static");
    }

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

执行结果

Parent static
Child static
Parent constructor
Child constructor

分析

至于为什么是这个结果,我们先不讨论,先来想一下这段代码具体的执行过程。

在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。

在加载完所需的类之后,便开始执行main方法。

在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。

问题 3 - 这 2 段代码的输出结果是什么?

public class Test {
    public static void main(String[] args) {
        new B();
    }
    static {
        System.out.println("Test static");
    }  
}

class B {
    static {
        System.out.println("B static");
    }

    static A a = new A();

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

class A {
    static {
        System.out.println("A static");
    }

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

执行结果

Test static
B static
A static
A constructor
B constructor

分析

类似的,还是先找到程序的执行入口(main函数),并加载Test类。在加载的初始化阶段,执行Test类的static块,即输出 Test static

此后,准备实例化B对象。这时,先加载B类,即输出B static

此后,实例化类B的静态成员(即执行static A a = new A();)。这时,仍然需要先加载类A,因此输出A static。在加载完成后,执行类A的构造函数,以实例化一个类A对象,这时输出A constructor

在类B被加载后,类B的构造函数被调用,以实例化一个B对象,这时输出B constructor

问题 4 - 这段代码的输出结果是什么?

public class Parent {
    Person person = new Person("Parent");
    static{
        System.out.println("Parent static");
    }
     
    public Parent() {
        System.out.println("Parent constructor");
    }
     
    public static void main(String[] args) {
        new Child();
    }
}
 
class Person{
    static{
        System.out.println("Person static");
    }
    public Person(String str) {
        System.out.println("Person constructor - "+str);
    }
}
 
 
class Child extends Parent {
    Person person = new Person("Child");
    static{
        System.out.println("Child static");
    }
     
    public Child() {
        System.out.println("Child constructor");
    }
}

结果

Parent static
Child static
Person static
Person constructor - Parent
Parent constructor
Person constructor - Child
Child constructor

分析

类似地,我们还是来想一下这段代码的具体执行过程。

首先加载 Parent 类(因为 main 函数位于 Parent 类中),因此会执行 Parent 类中的 static 块。接着执行 new Child(),而 Child 类还没有被加载,因此需要加载 Child 类。

在加载 Child 类的时候,发现 Child 类继承自 Parent 类,但是由于 Parent 类已经被加载了,所以只需要加载 Child 类,那么就会执行 Child 类的中的 static 块。

在加载完 Child 类之后,就开始生成一个 Child 类的对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行 Parent 类中的 Person person = new Person(),而由于 Person 类还没有被加载过,因此需要先加载 Person 类(这个过程中,会执行 Person 类中的 static 块),接着执行父类的构造器,完成了父类的初始化,然后就来初始化子类对象了,因此会接着执行 Child 中的Person person = new Person("Child"),最后执行 Child 的构造器。

总结

当实例化一个类对象时,需要经历以下过程:

  • 如果该类有父类,则先加载父类(加载过程中,会按书写的先后顺序执行 static 块和初始化 static 变量);
  • 父类加载完成后,加载这个类(类似地,在加载过程中,会按书写的先后顺序执行 static 块和初始化 static 变量);
  • 这个类加载完成后,开始实例化一个父类对象,在这个过程中:
    • 先处理父类对象中的成员变量;
    • 再执行父类对象的构造函数。
  • 当这个类的父类对象实例化完成后:
    • 开始处理子类对象的成员变量;
    • 再执行子类对象的构造函数。

问题 5 - 这段代码的输出结果是什么?

public class Test {
    static A a1 = new A();

    static {
        System.out.println("Test static");
    }

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

public class A {
    public A() {
        System.out.println("A constructor");
    }
    static {
        System.out.println("A static");
    }
}

结果

A static
A constructor
Test static

分析

JVM首先找到程序的入口 main(String[] args) 方法,因此加载类Test。

为static变量 a1 赋值时,发现类A还没有被加载,因此加载类A,此时输出A static。加载完成后,实例化一个类A对象,此时输出A constructor。

为static变量 a1 赋值完成后,执行static块,此时输出Test static。

这说明:对static变量与static块的执行,是按照其书写的先后顺序进行对应执行的。

Reference