static 是咱们平常生活中常常用到的关键字,也是 Java 中很是重要的一个关键字,static 能够修饰变量、方法、作静态代码块、静态导包等,下面咱们就来具体聊一聊这个关键字,咱们先从基础开始,从基本用法入手,而后分析其原理、优化等。html
static
关键字表示的概念是 全局的、静态的
,用它修饰的变量被称为静态变量
。java
public class TestStatic { static int i = 10; // 定义了一个静态变量 i }
静态变量也被称为类变量,静态变量是属于这个类全部的。什么意思呢?这其实就是说,static 关键字只能定义在类的 {}
中,而不能定义在任何方法中。编程
就算把方法中的 static 关键字去掉也是同样的。网络
static 属于类全部,由类来直接调用 static 修饰的变量,它不须要手动实例化类进行调用oracle
public class TestStatic { static int i = 10; public static void main(String[] args) { System.out.println(TestStatic.i); } }
这里你须要理解几个变量的概念jvm
外
的变量被称为实例变量,实例变量的副本数量和实例的数量同样。内
的变量被称为局部变量;中
的变量被称为参数。static 能够修饰方法,被 static 修饰的方法被称为静态方法
,其实就是在一个方法定义中加上 static
关键字进行修饰,例以下面这样工具
static void sayHello(){}
《Java 编程思想》在 P86 页有一句经典的描述优化
static 方法就是没有 this 的方法,在 static 内部不能调用非静态方法,反过来是能够的。并且能够在没有建立任何对象的前提下,仅仅经过类自己来调用 static 方法,这其实是 static 方法的主要用途。this
其中有一句很是重要的话就是 static 方法就是没有 this 的方法,也就是说,能够在不用建立对象的前提下就可以访问 static 方法,如何作到呢?看下面一段代码spa
在上面的例子中,因为 staticMethod
是静态方法,因此可以使用 类名.变量名进行调用。
所以,若是说想在不建立对象的状况下调用某个方法,就能够将这个方法设置为 static。日常咱们见的最多的 static 方法就是 main方 法,至于为何 main 方法必须是 static 的,如今应该很清楚了。由于程序在执行 main 方法的时候没有建立任何对象,所以只有经过类名来访问。
static 修饰方法的注意事项
类名.变量名
便可访问;static 关键字能够用来修饰代码块,代码块分为两种,一种是使用 {}
代码块;一种是 static {}
静态代码块。static 修饰的代码块被称为静态代码块。静态代码块能够置于类中的任何地方,类中能够有多个 static 块,在类初次被加载的时候,会按照 static 代码块的顺序来执行,每一个 static 修饰的代码块只能执行一次。咱们会面会说一下代码块的加载顺序。下面是静态代码块的例子
static 代码块能够用来优化程序执行顺序,是由于它的特性:只会在类加载的时候执行一次。
内部类的使用场景比较少,可是内部类还有具备一些比较有用的。在了解静态内部类前,咱们先看一下内部类的分类
静态内部类
就是用 static 修饰的内部类,静态内部类能够包含静态成员,也能够包含非静态成员,可是在非静态内部类中不能够声明静态成员。
静态内部类有许多做用,因为非静态内部类的实例建立须要有外部类对象的引用,因此非静态内部类对象的建立必须依托于外部类的实例;而静态内部类的实例建立只需依托外部类;
而且因为非静态内部类对象持有了外部类对象的引用,所以非静态内部类能够访问外部类的非静态成员;而静态内部类只能访问外部类的静态成员;
public class ClassDemo { private int a = 10; private static int b = 20; static class StaticClass{ public static int c = 30; public int d = 40; public static void print(){ //下面代码会报错,静态内部类不能访问外部类实例成员 //System.out.println(a); //静态内部类只能够访问外部类类成员 System.out.println("b = "+b); } public void print01(){ //静态内部内所处的类中的方法,调用静态内部类的实例方法,属于外部类中调用静态内部类的实例方法 StaticClass sc = new StaticClass(); sc.print(); } } }
不知道你注意到这种现象没有,好比你使用了 java.util
内的工具类时,你须要导入 java.util 包,才能使用其内部的工具类,以下
可是还有一种导包方式是使用静态导包
,静态导入就是使用 import static
用来导入某个类或者某个包中的静态方法或者静态变量。
import static java.lang.Integer.*; public class StaticTest { public static void main(String[] args) { System.out.println(MAX_VALUE); System.out.println(toHexString(111)); } }
咱们在了解了 static 关键字的用法以后,来看一下 static 深刻的用法,也就是由浅入深,慢慢来,前戏要够~
static 所修饰的属性和方法都属于类的,不会属于任何对象;它们的调用方式都是 类名.属性名/方法名
,而实例变量和局部变量都是属于具体的对象实例。
首先,先来认识一下 JVM 的不一样存储区域。
虚拟机栈
: Java 虚拟机栈是线程私有的数据区,Java 虚拟机栈的生命周期与线程相同,虚拟机栈也是局部变量的存储位置。方法在执行过程当中,会在虚拟机栈种建立一个 栈帧(stack frame)
。本地方法栈
: 本地方法栈也是线程私有的数据区,本地方法栈存储的区域主要是 Java 中使用 native
关键字修饰的方法所存储的区域程序计数器
:程序计数器也是线程私有的数据区,这部分区域用于存储线程的指令地址,用于判断线程的分支、循环、跳转、异常、线程切换和恢复等功能,这些都经过程序计数器来完成。方法区
:方法区是各个线程共享的内存区域,它用于存储虚拟机加载的 类信息、常量、静态变量、即时编译器编译后的代码等数据,也就是说,static 修饰的变量存储在方法区中 堆
: 堆是线程共享的数据区,堆是 JVM 中最大的一块存储区域,全部的对象实例,包括实例变量都在堆上进行相应的分配。static 变量的生命周期与类的生命周期相同,随类的加载而建立,随类的销毁而销毁;普通成员变量和其所属的生命周期相同。
咱们知道,序列化的目的就是为了 把 Java 对象转换为字节序列。对象转换为有序字节流,以便其可以在网络上传输或者保存在本地文件中。
声明为 static 和 transient 类型的变量不能被序列化,由于 static 修饰的变量保存在方法区中,只有堆内存才会被序列化。而 transient
关键字的做用就是防止对象进行序列化操做。
咱们前面提到了类加载顺序这么一个概念,static 修饰的变量和静态代码块在使用前已经被初始化好了,类的初始化顺序依次是
加载父类的静态字段 -> 父类的静态代码块 -> 子类静态字段 -> 子类静态代码块 -> 父类成员变量(非静态字段)
-> 父类非静态代码块 -> 父类构造器 -> 子类成员变量 -> 子类非静态代码块 -> 子类构造器
咱们在开发过程当中,常常会使用 static
关键字做为日志打印,下面这行代码你应该常常看到
private static final Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
然而把 static 和 final 去掉均可以打印日志
private final Logger LOGGER = LogFactory.getLoggger(StaticTest.class); private Logger LOGGER = LogFactory.getLoggger(StaticTest.class);
可是这种打印日志的方式存在问题
对于每一个 StaticTest 的实例化对象都会拥有一个 LOGGER,若是建立了1000个 StaticTest 对象,则会多出1000个Logger 对象,形成资源的浪费,所以一般会将 Logger 对象声明为 static 变量,这样一来,可以减小对内存资源的占用。
因为单例模式指的就是对于不一样的类来讲,它的副本只有一个,所以 static 能够和单例模式彻底匹配。
下面是一个经典的双重校验锁实现单例模式的场景
public class Singleton { private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
来对上面代码作一个简单的描述
使用 static
保证 singleton 变量是静态的,使用 volatile
保证 singleton 变量的可见性,使用私有构造器确保 Singleton 不能被 new 实例化。
使用 Singleton.getInstance()
获取 singleton 对象,首先会进行判断,若是 singleton 为空,会锁住 Singletion 类对象,这里有一些小伙伴们可能不知道为何须要两次判断,这里来解释下
若是线程 t1 执行到 singleton == null 后,判断对象为 null,此时线程把执行权交给了 t2,t2 判断对象为 null,锁住 Singleton 类对象,进行下面的判断和实例化过程。若是不进行第二次判断的话,那么 t1 在进行第一次判空后,也会进行实例化过程,此时仍然会建立多个对象。
这个问题我相信大部分小伙伴都没有考虑过,在 Java 编程思想中有这么一句话 类的构造器虽然没有用 static 修饰,可是其实是 static 方法,可是并无给出实际的解释,可是这个问题能够从下面几个方面来回答
类.方法名
不须要新建立对象就可以访问,因此从这个角度来看,构造器也不是静态的public class StaticTest { public StaticTest(){} public static void test(){ } public static void main(String[] args) { StaticTest.test(); StaticTest staticTest = new StaticTest(); } }
咱们使用 javap -c 生成 StaticTest 的字节码看一下
public class test.StaticTest { public test.StaticTest(); Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return public static void test(); Code: 0: return public static void main(java.lang.String[]); Code: 0: invokestatic #2 // Method test:()V 3: new #3 // class test/StaticTest 6: dup 7: invokespecial #4 // Method "<init>":()V 10: astore_1 11: return }
咱们发现,在调用 static 方法时是使用的 invokestatic
指令,new 对象调用的是 invokespecial
指令,并且在 JVM 规范中 https://docs.oracle.com/javas... 说到
从这个角度来说,invokestatic
指令是专门用来执行 static 方法的指令;invokespecial
是专门用来执行实例方法的指令;从这个角度来说,构造器也不是静态的。