coding……
但行好事 莫问前程

Java编程拾遗『static详解』

static关键字在开发中是比较常见的,但是很多人对其都没有一个明确的认识,只有一种比较模糊的概念,甚至不知道该怎么用。本篇文章将总结一下,Java中static关键词的用法。首先来看一下static关键字的作用:

  • 为特定的数据类型或对象分配单一的存储空间,而与创建对象的个数无关
  • 实现某个方法或属性与类而不是对象关联在一起,也就是将方法或属性是属于类的,可以通过类名直接访问,而不用借助于对象。

在Java中,static主要有以下四种使用情况:

  • 静态变量
  • 静态方法
  • 静态代码块
  • 静态内部类

1. 静态变量

静态变量就是类中被static修饰的成员变量,我们都知道Java中没有全局的概念,但是可以通过static关键字达到全局的效果。与普通成员变量相比,静态变量属于类,在内存中只有一个复制,所有的实例对象的该成员都指向同一个内存地址。只要类被加载,这个静态变量就会被分配存储空间,之后就可以被使用了,而不用等创建了对象再访问。下面看一个简单的例子:

public class StaticVariables {
    public static Integer staticInt = 0;
    public Integer nonStaticInt = 0;

    public static void main(String[] args) {
        StaticVariables var = new StaticVariables();
        System.out.println("var.staticInt: " + var.staticInt);
        System.out.println("StaticVariables.staticInt: " + StaticVariables.staticInt);
        System.out.println("var.nonStaticInt: " + var.nonStaticInt);
        System.out.println("静态变量与非静态变量加1");
        var.staticInt++;
        var.nonStaticInt++;
        StaticVariables var1 = new StaticVariables();
        System.out.println("var1.staticInt: " + var1.staticInt);
        System.out.println("StaticVariables.staticInt: " + StaticVariables.staticInt);
        System.out.println("var1.nonStaticInt: " + var1.nonStaticInt);
    }
}

运行结果:

var.staticInt: 0
StaticVariables.staticInt: 0
var.nonStaticInt: 0
静态变量与非静态变量加1
var1.staticInt: 1
StaticVariables.staticInt: 1
var1.nonStaticInt: 0

可以发现,任意一个对象对于静态变量的改变,会影响到其它对象,验证了静态变量为类所持有,所有对象共享这个静态变量。而某个对象对于实例变量的改变,并不会影响到其它对象,所以实例变量是被特定对象持有的。

2. 静态方法

静态方法是指类中被static修饰的成员方法,在对象和类那篇文章中已详细讲述,本文直接搬过来了,不再多讲述了。

类方法也叫静态方法,是指通过static修饰的成员方法,用于描述类行为,在调用方式上可以直接使用类名调用,而不用通过实例调用。在同一个类中,实例方法内部可以直接使用静态方法,但是静态方法中则无法直接调用这个实例方法,相应的静态方法也只能访问静态成员。关于静态方法,相信很多人都有一种熟悉又陌生的感觉,下面简单讲一下关于静态方法和非静态方法的一些误解:

  1. 大家都以为“静态方法常驻内存,实例方法不是,所以静态方法效率高但占内存
    事实上,他们都是一样的,在加载时机和占用内存上,静态方法和实例方法是一样的,在被使用时加载到栈中,也就是栈帧,在方法调用结束,从栈中弹出,调用的速度基本上没有差别。
  2. 大家都以为“静态方法在堆上分配内存,实例方法在堆栈上
    事实上所有的方法都不可能在堆或者堆栈上分配内存,方法作为代码是被加载到特殊的代码内存区域,这个内存区域是不可写的。方法占不占用更多内存,和它是不是static没什么关系。  
    因为字段是用来存储每个实例对象的信息的,所以字段会占有内存,并且因为每个实例对象的状态都不一致(至少不能认为它们是一致的),所以每个实例对象的所有字段都会在内存中有一分拷贝,也因为这样你才能用它们来区分你现在操作的是哪个对象。但方法不一样,不论有多少个实例对象,它的方法的代码都是一样的,所以只要有一份代码就够了。因此无论是static还是non-static的方法,都只存在一份代码,也就是只占用一份内存空间。  
    同样的代码,为什么运行起来表现却不一样?这就依赖于方法所用的数据了。主要有两种数据来源,一种就是通过方法的参数传进来,另一种就是使用class的成员变量的值……
  3. 大家都以为“实例方法需要先创建实例才可以调用,比较麻烦,静态方法不用,比较简单
    事实上如果一个方法与他所在类的实例对象无关,那么它就应该是静态的,而不应该把它写成实例方法。所以所有的实例方法都与实例有关,既然与实例有关,那么创建实例就是必然的步骤,没有麻烦简单一说。当然你完全可以把所有的实例方法都写成静态的,将实例作为参数传入即可,一般情况下可能不会出什么问题。从面向对象的角度上来说,在抉择使用实例化方法或静态方法时,应该根据是否该方法和实例化对象具有逻辑上的相关性,如果是就应该使用实例化对象 反之使用静态方法。这只是从面向对象角度上来说的。如果从线程安全、性能、兼容性上来看 也是选用实例化方法为宜。

那么为什么还要区分静态方法和实例化方法 ?

如果我们继续深入研究的话,就要脱离技术谈理论了。早期的结构化编程,几乎所有的方法都是“静态方法”,引入实例化方法概念是面向对象概念出现以后的事情了,区分静态方法和实例化方法不能单单从性能上去理解,创建c++,java,c#这样面向对象语言的大师引入实例化方法一定不是要解决什么性能、内存的问题,而是为了让开发更加模式化、面向对象化。这样说的话,静态方法和实例化方式的区分是为了解决模式的问题。

这里举个静态方法比较常用的例子—单例,如下利用静态方法通过双重检查锁实现一个单例:

public class Singleton {
        private volatile static Singleton singleton;

        private Singleton() {
        }

        public static Singleton getSingleton() {
            if (singleton == null) {
                synchronized (Singleton.class) {
                    if (singleton == null) {
                        singleton = new Singleton();
                    }
                }
            }
            return singleton;
        }
    }

注意单例类中的静态成员变量修饰符为private,一般用public修饰的静态变量和方法本质上是全局的,若在static变量前用private修饰,则表示这个变量只能在类内使用,不能在其它类中通过类名访问。比如上述单例中的静态变量就是在本类中的静态方法中使用的。另外先讲一下,这种单例并不保证一定只实例化一个对象,这个后续会讲解。

3. 静态代码块

所谓代码块就是用大括号({})将多行代码封装在一起,形成的独立数据体。静态代码块就是用static修饰的用{}括起来的代码段,它的主要目的就是对静态属性进行初始化。静态代码块独立于成员变量和成员函数的代码块。它不再任何一个方法体内,而直接存在与类中。JVM在加载类是会执行静态代码块,如果类中存在多个静态代码块,JVM会按照顺序执行静态代码块。静态代码块常用来初始化静态变量,并且只会被执行一次。

public class StaticBlock {
    public static Integer staticVariable;

    static {
        StaticBlock.staticVariable = 10;
        System.out.println("static block is called");
        System.out.println("StaticBlock.staticVariable: " + staticVariable);
    }

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

运行结果:

static block is called
StaticBlock.staticVariable: 10

可以看到static代码块被执行了。除了静态代码块,类中还可以存在非静态代码块,用于初始化对类的属性进行初始化。当实例化对象时,对象所在的类所有的成员变量首先要进行初始化,经过一系列初始化过程,最后才会执行构造函数创建对象。用来初始化的可以是直接赋值、静态代码块、非静态代码块、构造函数,下面讲一下,对象创建时的初始化过程。首先将一下结论,如下:

public class Base {
    public static Integer baseStaticVar = 10;
    public Integer baseNonStaticVar = 10;

    static {
        System.out.println("========父类静态代码块开始========");
        System.out.println("before base static block, baseStaticVar = " + baseStaticVar);
        baseStaticVar = 9;
        System.out.println("base static block change baseStaticVar, baseStaticVar = " + baseStaticVar);
        System.out.println("========父类静态代码块结束========");
    }

    {
        System.out.println("========父类非静态代码块开始========");
        System.out.println("before base block, baseNonStaticVar = " + baseNonStaticVar);
        baseNonStaticVar = 9;
        System.out.println("base block change baseNonStaticVar, baseNonStaticVar = " + baseNonStaticVar);
        System.out.println("========父类非静态代码块结束========");
    }

    public Base() {
        System.out.println("========父类构造函数开始========");
        System.out.println("base constructor called");
        System.out.println("========父类构造函数结束========");
    }
}

public class SubClass extends Base {
    public static Integer subClassStaticVar = 10;
    public Integer subClassNonStaticVar = 10;

    static {
        System.out.println("========子类静态代码块开始========");
        System.out.println("before SubClass static block, subClassStaticVar = " + subClassStaticVar);
        subClassStaticVar = 9;
        System.out.println("base SubClass block change subClassStaticVar, subClassStaticVar = " + subClassStaticVar);
        System.out.println("========子类静态代码块结束========");
    }

    {
        System.out.println("========子类非静态代码块开始========");
        System.out.println("before SubClass block, subClassNonStaticVar = " + subClassNonStaticVar);
        subClassNonStaticVar = 9;
        System.out.println("SubClass block change subClassNonStaticVar, subClassNonStaticVar = " + subClassNonStaticVar);
        System.out.println("========子类非静态代码块结束========");
    }

    public SubClass() {
        System.out.println("========子类构造函数开始========");
        System.out.println("base constructor called");
        System.out.println("========子类构造函数结束========");
    }

    public static void main(String[] args) {
        SubClass subClass = new SubClass();
    }
}

运行结果:

========父类静态代码块开始========
before base static block, baseStaticVar = 10
base static block change baseStaticVar, baseStaticVar = 9
========父类静态代码块结束========
========子类静态代码块开始========
before SubClass static block, subClassStaticVar = 10
base SubClass block change subClassStaticVar, subClassStaticVar = 9
========子类静态代码块结束========
========父类非静态代码块开始========
before base block, baseNonStaticVar = 10
base block change baseNonStaticVar, baseNonStaticVar = 9
========父类非静态代码块结束========
========父类构造函数开始========
base constructor called
========父类构造函数结束========
========子类非静态代码块开始========
before SubClass block, subClassNonStaticVar = 10
SubClass block change subClassNonStaticVar, subClassNonStaticVar = 9
========子类非静态代码块结束========
========子类构造函数开始========
base constructor called
========子类构造函数结束========

可以发现在进静态代码块或者非静态代码块之前,成员变量都已经是有值的了(10),说明静态变量和非静态变量的赋值在代码块前执行,上述运行结果可以说明初始化顺序是按照上图进行的。

4. 静态内部类

静态内部类在上篇文章内部类中已经详细讲述了,这里不再过多描述了。静态内部类就是static修饰的内部类,跟静态成员变量和静态方法一样,它可以不依赖于外部类示例而被实例化。静态内部类使用场景很多,当外部类需要使用内部类,而内部类无需外部类资源,并且内部类可以单独创建的时候会考虑采用静态内部类的设计。

public class Outer {
    private static int shared = 100;

    public static class StaticInner {
        public void innerMethod() {
            System.out.println("inner " + shared);
        }
    }
    
    public void useStaticInnerClass() {
        StaticInner si = new StaticInner();
        si.innerMethod();
    }

    public static void main(String[] args) {
        Outer.StaticInner staticInner = new Outer.StaticInner();
        staticInner.innerMethod();
    }
}

外部类为Outer,静态内部类为StaticInner,静态内部类除了位置放在别的类内部,其它的跟普通的类几乎没什么区别,也可以定义静态变量、实例变量、静态方法、实例方法。另外,静态内部类可以访问外部类的静态变量、静态方法和构造函数,而不能访问外部类的实例变量和实例方法。在外部类中可以直接访问静态内部类,如useStaticInnerClass中所示,否则要使用main方法所示的初始化方式(前提是静态内部类外部可访问,非private修饰)。更多细节,建议去上篇文章内部类去了解。

赞(0) 打赏
Zhuoli's Blog » Java编程拾遗『static详解』
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址