Java-再来学习下static关键字

一、前言

虽然平时经常使用static关键字,但对static了解的并不是特别清晰,恰好最近在复习基础,准备花点时间把这个关键字的用法给回顾下。

二、static关键字

1. 简介

static是Java中的关键字,一般用来修饰方法,变量,代码块。我们都知道,在Java中,一般的操作都要借助于对象来实现,而static是基于类的,一句话来说就是:在无需创建对象的情况下实现对方法或者变量的调用。

2. static变量

static变量被称为静态变量,或者说是共享变量,和普通的实例变量不同的是:该类的每个对象对于实例变量而言,都有自己的一份拷贝,各个对象间的拷贝互不影响;而对于静态变量来说,由于静态变量是基于类的,所以这个类的所有对象将共享同一份static变量,该静态变量当且仅当在类初次加载时初始化:

private static String name;

一个类即使没有一个对象,它的静态变量也存在,它属于类,而不属于任何该类的对象。在大多数面向对象的语言中,静态变量又称为类变量。

static变量我们一般使用的不是太多,但static常量我们会经常用到,比如我们常用到的 System.out

public final static PrintStream out = null;

另外,每个类对象都可以对公共的变量进行修改,所以,一般情况下,我们最好不要将变量设置为public,而如果是final类型的常量,一般是没什么问题的。

《Java核心技术》中提到,并不是没有办法修改final变量的值,比如 System.out中就有 setOut方法,这是因为 setOut 方法是一个本地方法,不是用Java语言实现的,本地方法可以绕过Java语言的存取控制机制,这是一种特殊的方法,我们自己一般不建议这么处理。

静态变量的初始化顺序是按照定义时的顺序进行加载的,有关初始化顺序,我们放到最后一并来了解。

3. static方法

同样,静态方法也是基于类的,不属于任何一个对象,静态方法的访问也不依赖于任何对象。可以认为静态方法是没有this参数的方法,因为在非静态方法中,this表示这个方法的隐式参数。静态方法有以下几点特性:

  • 静态方法不能访问非静态变量和非静态方法,因为非静态方法和变量都是依赖于具体的对象才能被调用,而它不能操作对象;
  • 静态方法可以访问自身类的静态变量;
  • 非静态方法是可以访问静态变量和静态方法的,因为静态变量和方法是对所有对象共享的;
  • 可以使用对象调用静态方法,但这种方式意义不大,因为通过对象调用静态方法得到的结果和该对象并没有什么关系,所以一般我们都是使用类名来进行调用;

我们来看一个例子:

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

虽然对象静态方法来说没有this,但在非静态方法中是可以访问静态变量的,而this代表当前对象,由于静态变量是被所有对象共享的,所以自然是可以通过this来进行调用的,所以这里结果是:33

另外,我们经常用到的main方法就是一个经典的静态方法。main方法不对任何对象进行操作,事实上,在启动程序时还没有任何一个对象,静态的main方法将执行并创建程序所需要的对象。另外,静态方法还有一种常见的用途就是用于设计模式中工厂方法的创建,这在学习设计模式的时候经常见到。

4. static代码块

所谓的静态代码块可以作为程序初始化的一种方式:

static {
        
}

静态代码块会在类初次被加载的时候加载,并且只会执行一次,所以可以用来做一些对象的初始化工作。

static代码块可以放在类的许多地方(除了方法内部),并且类中可以有好多个静态代码块,而在加载的时候,会按照他们在代码中的顺序进行加载;

5. 初始化顺序(无继承)

对于初始化顺序,我们这里不仅仅来看静态变量及静态代码块,顺序来看下构造方法,初始化块,非静态变量及继承情况下的顺序等。另外,下面的代码仅用于测试,用来证明各种初始化方式的顺序,实际编写时切勿这样写,否则会使程序非常难于维护。我们来看代码:

public class JavaTest {
    public JavaTest() {
        System.out.println("执行JavaTest构造方法1");
    }

    public JavaTest(String param) {
        System.out.println("执行JavaTest构造方法2");
    }

    static {
        System.out.println("JavaTest静态代码块1");
    }

    {
        System.out.println("JavaTest代码块1");
    }

    private static int max1 = getMax1();
    private int min1 = getMin1();

    public int getMin1() {
        System.out.println("初始化成员变量min1");
        return 0;
    }

    public static int getMax1() {
        System.out.println("初始化静态成员变量max1");
        return 0;
    }

    static {
        System.out.println("JavaTest静态代码块2");
    }

    {
        System.out.println("JavaTest代码块2");
    }

    private static int max2 = getMax2();
    private int min2 = getMin2();

    public int getMin2() {
        System.out.println("初始化成员变量min2");
        return 0;
    }

    public static int getMax2() {
        System.out.println("初始化静态成员变量max2");
        return 0;
    }

    public static void main(String[] args) {
        System.out.println("==============================");
        new JavaTest();
        System.out.println("==============================");
        new JavaTest("param");
    }
}

代码是基于1.8的,来看下结果:

JavaTest静态代码块1
初始化静态成员变量max1
JavaTest静态代码块2
初始化静态成员变量max2
==============================
JavaTest代码块1
初始化成员变量min1
JavaTest代码块2
初始化成员变量min2
执行JavaTest构造方法1
==============================
JavaTest代码块1
初始化成员变量min1
JavaTest代码块2
初始化成员变量min2
执行JavaTest构造方法2

通过这段代码我们可以看到:

  1. 首先,在类加载时,先为类的静态变量分配内存空间,并初始化默认值;
  2. 执行静态成员变量的初始化操作:而静态成员的初始化有两种方式:在声明时直接初始化与静态代码块。两种初始化方式会按照在类中出现的顺序(声明的顺序)来执行;
  3. 并且上面两步只会在类加载时执行一次;
  4. 接下来,如果创建了类的对象,便在堆中为类的实例分配内存空间,并初始化默认值;
  5. 执行实例变量的初始化操作,同样有两种方式:声明时直接初始化与初始化块。这两种方式也是按照在类中出现的顺序来执行;
  6. 最后执行类的构造方法;

另外,虽然类的成员变量可以在声明时为其变量直接初始化,但声明与初始化并不是同时执行的。对于静态成员变量,会先为类中声明的静态成员变量分配空间,每个变量存在默认值后,才会执行变量声明处的初始化,而这种初始化方式是静态初始化块按照在类中出现的先后顺序来执行的。对于实例变量,与静态变量是类似的。

既然类的变量空间分配是先于初始化执行的,那么就存在这样一种状态,变量创建之后但变量未初始化。如果在此期间使用此变量,就可能得不到我们想要的结果。通过一个例子来看下:

public class JavaTest {
    {
        print();
    }

    private int max = 9;
    private String maxValue;

    public String getMax() {
        return maxValue;
    }

    public void print() {
        maxValue = "max: " + max;
    }

    public static void main(String[] args) {
        JavaTest test2 = new JavaTest();
        System.out.println(test2.getMax());
    }
}

我们打印一下结果,可以看到值为:max: 0。正常情况下,应该打印出 9 的,但却打印出了0。问题就在于对方法 print 的调用:

因为该方法是在初始化块中调用的,而初始化块与实例变量在声明处的初始化是同等级的,会按照类中出现的顺序执行;程序中初始化块出现在max变量的前面,所以会在max变量初始化前执行,在调用print方法时,max变量虽然已经声明,但却尚未执行初始化,其默认值为0,所以打印的结果就是0;

6. 初始化顺序(继承)

接下来,我们来看下继承情况下的初始化顺序。继承的情况我们也都熟悉,当初始化的时候,首先会初始化父类,这是一个递归的过程。同样,我们看一个小例子,代码可能多一点:

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

class ThisClass extends SuperClass {
    public ThisClass() {
        System.out.println("执行ThisClass构造方法");
    }

    static {
        System.out.println("ThisClass静态代码块");
    }

    {
        System.out.println("ThisClass代码块");
    }

    private static int max = getMax();
    private int min = getMin();

    public static int getMin() {
        System.out.println("ThisClass初始化成员变量min");
        return 0;
    }

    public static int getMax() {
        System.out.println("ThisClass初始化静态成员变量max");
        return 0;
    }

}

class SuperClass {
    public SuperClass() {
        System.out.println("执行SuperClass构造方法");
    }

    static {
        System.out.println("SuperClass静态代码块");
    }

    {
        System.out.println("SuperClass代码块");
    }

    private static int max = getMax();
    private int min = getMin();

    public static int getMin() {
        System.out.println("SuperClass初始化成员变量min");
        return 0;
    }

    public static int getMax() {
        System.out.println("SuperClass初始化静态成员变量max");
        return 0;
    }
}

我们直接来看一下打印结果:

==========================
SuperClass静态代码块
SuperClass初始化静态成员变量max
ThisClass静态代码块
ThisClass初始化静态成员变量max
SuperClass代码块
SuperClass初始化成员变量min
执行SuperClass构造方法
ThisClass代码块
ThisClass初始化成员变量min
执行ThisClass构造方法
==========================
SuperClass代码块
SuperClass初始化成员变量min
执行SuperClass构造方法
ThisClass代码块
ThisClass初始化成员变量min
执行ThisClass构造方法

我们可以看到:

  1. 继承情况下,JVM会首先加载父类,这是一个递归的过程,直到Object类为止。在类加载中,首先为类中的静态成员变量分配内存空间,并初始化默认值,然后按照在类中的顺序执行静态代码块与静态成员变量的初始化,这个过程是从父类到子类,并且只执行一次;
  2. 如果创建了类的对象,在初始化子类之前,会首先对父类的实例变量初始化默认值,然后按照在类中的顺序进行初始化,最后调用父类的构造器。如果没有创建任何对象,本环节就不会执行。

注意:我们知道,静态成员变量的初始化会在类首次加载时执行,并且只会执行一次,那么类在什么情况下会被JVM载入执行呢?

public class JavaTest {
    public static void main(String[] args) {
//        Super superTest1;
//        Super superTest2 = new Super();
//        int height = Super.height;
//        Super.getHeight();
/*
        try {
            Class.forName("Super");
        } catch (Exception e) {
            System.out.println("异常");
        }
*/
    }
}

class Super {
    static {
        System.out.println("静态代码块");
    }
    public static int height = 30;

    public static int getHeight() {
        return height;
    }
}
  • 首先,完全注释掉后,运行程序,很显然,什么都没有输出;
  • 然后我们将第3行注释取消,运行,还是什么都没有输出;
  • 然后我们随意取消第4,5,6行任意一行,就可以打印出 "静态代码块";

由此说明,如果只是引入了类的引用,JVM是不会载入类的。JVM只是在需要某个类时才载入该类,这种需要可能是使用了该类的静态成员变量,或是调用了该类的静态方法,或是生成了该类的实例,但这并非是加载一个类的全部可能,如当加载子类时,那么父类自然也就被加载了。

同理,如果我们保留上面的注释,而取消第8行到13行的注释,那程序还是会打印出 "静态代码块"。因为JVM在加载类的时候会生成一个Class类的对象,而Class类的forName方法就是取得该类的Class对象,所以JVM会载入该类。

关于例子,就到这里了,另外等有时间,试着从JVM的角度和字节码的执行过程来研究一下Java变量的初始化顺序这个问题。

三、总结

1. 使用场景
  • 静态变量一般作为共享变量使用,有的时候为了减少对象的创建,也会使用静态变量,还有就是单例模式中会经常用到;
  • 静态方法一般用于在不创建对象的情况下调用。这种使用方式就比较多了,首先Java的Arrays,Collections这些工具类及各种Utils类中的方法,工厂模式方法等等;
  • 静态代码块一般用来对静态变量进行初始化,如单例模式,枚举类的定义等;

其实还有静态内部类,后续会说到内部类,这里就不多说了;

2. 加载

静态变量及代码块只会在类进行加载的时候加载一次,另外对Java中变量的加载可以用一句话概括:

Java变量的加载顺序,是从父类到子类,静态到非静态的过程。

3. 《细说Java》

最近在柜子里翻书,翻到了《细说Java》这本书,上面全是Java的一些基础细节问题,我都忘了是什么时候买的这本书了。变量初始化的这些例子还是我以前基于这本书整理的,整理之后不知道把这些例子给放到哪个平台上了。现在把他们统一再给梳理下,另外,这本书挺有意思,都是基础知识,准备最近再拿出来看一看。

本文还参考了:
《Java核心技术》
海子 - Java中的static关键字解析
R大-实例构造器是不是静态方法?- iteye.com

你可能感兴趣的:(Java-再来学习下static关键字)