java基础学习:JAVA中static的做用详解

1.综述

static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也能够造成静态static代码块,可是Java语言中没有全局变量的概念。java

被static修饰的成员变量和成员方法独立于该类的任何对象。也就是说,它不依赖类特定的实例,被类的全部实例共享。bash

只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们。所以,static对象能够在它的任何对象建立以前访问,无需引用任何对象。函数

用public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明它类的对象市,不生成static变量的副本,而是类的全部实例共享同一个static变量。学习

static变量前能够有private修饰,表示这个变量能够在类的静态代码块中,或者类的其余静态成员方法中使用(固然也能够在非静态成员方法中使用--废话),可是不能在其余类中经过类名来直接引用,这一点很重要。实际上你须要搞明白,private是访问权限限定,static表示不要实例化就可使用,这样就容易理解多了。static前面加上其它访问权限关键字的效果也以此类推。测试

static修饰的成员变量和成员方法习惯上称为静态变量和静态方法,能够直接经过类名来访问,访问语法为:ui

类名.静态方法名(参数列表...)
类名.静态变量名复制代码

用static修饰的代码块表示静态代码块,当Java虚拟机(JVM)加载类时,就会执行该代码块。this

2 static变量

按照是否静态的对类成员变量进行分类可分两种:一种是被static修饰的变量,叫静态变量或类变量;另外一种是没有被static修饰的变量,叫实例变量。spa

二者的区别是:调试

  • 对于静态变量在内存中只有一个拷贝(节省内存),JVM只为静态分配一次内存,在加载类的过程当中完成静态变量的内存分配,可用类名直接访问(方便),固然也能够经过对象来访问(可是这是不推荐的)。
  • 对于实例变量,每建立一个实例,就会为实例变量分配一次内存,实例变量能够在内存中有多个拷贝,互不影响(灵活)。

因此通常在须要实现如下两个功能时使用静态变量: - 在对象之间共享值时 - 方便访问变量时日志

3 静态方法

静态方法能够直接经过类名调用,任何的实例也均可以调用, 所以静态方法中不能用this和super关键字,不能直接访问所属类的实例变量和实例方法(就是不带static的成员变量和成员成员方法),只能访问所属类的静态成员变量和成员方法。 由于实例成员与特定的对象关联!!

由于static方法独立于任何实例,所以static方法必须被实现,而不能是抽象的abstract。

例如为了方便方法的调用,Java API中的Math类中全部的方法都是静态的,而通常类内部的static方法也是方便其它类对该方法的调用。

静态方法是类内部的一类特殊方法,只有在须要时才将对应的方法声明成静态的,一个类内部的方法通常都是非静态的。

4 static代码块

static代码块也叫静态代码块,是在类中独立于类成员的static语句块,能够有多个,位置能够随便放,它不在任何的方法体内,JVM加载类时会执行这些静态的代码块,若是static代码块有多个,JVM将按照它们在类中出现的前后顺序依次执行它们,每一个代码块只会被执行一次。例如:

public class Test5 {
private static int a;
private int b;
static{
Test5.a=3;
System.out.println(a);
Test5 t=new Test5();
t.f();
t.b=1000;
System.out.println(t.b);
}
static{
Test5.a=4;
System.out.println(a);
}
public static void main(String[] args) {
// TODO 自动生成方法存根
}
static{
Test5.a=5;
System.out.println(a);
}
public void f(){
System.out.println("hhahhahah");
}
}复制代码

运行结果:

3
hhahhahah
1000
4
5复制代码

利用静态代码块能够对一些static变量进行赋值,最后再看一眼这些例子,都一个static的main方法,这样JVM在运行main方法的时候能够直接调用而不用建立实例。

5.static和final一块用表示什么

static final用来修饰成员变量和成员方法,可简单理解为“全局常量”!

对于变量,表示一旦给值就不可修改,而且经过类名能够访问。对于方法,表示不可覆盖,而且能够经过类名直接访问。

有时你但愿定义一个类成员,使它的使用彻底独立于该类的任何对象。一般状况下,类成员必须经过它的类的对象访问,可是能够建立这样一个成员,它可以被它本身使用,而没必要引用特定的实例。在成员的声明前面加上关键字static(静态的)就能建立这样的成员。若是一个成员被声明为static,它就可以在它的类的任何对象建立以前被访问,而没必要引用任何对象。你能够将方法和变量都声明为static。static 成员的最多见的例子是main( ) 。由于在程序开始执行时必须调用main() ,因此它被声明为static。

声明为static的变量实质上就是全局变量。当声明一个对象时,并不产生static变量的拷贝,而是该类全部的实例变量共用同一个static变量。声明为static的方法有如下几条限制:

  • 它们仅能调用其余的static 方法。
  • 它们只能访问static数据。
  • 它们不能以任何方式引用this 或super。

若是你须要经过计算来初始化你的static变量,你能够声明一个static块,Static 块仅在该类被加载时执行一次。下面的例子显示的类有一个static方法,一些static变量,以及一个static 初始化块:

// Demonstrate static variables,methods,and blocks.
class UseStatic {
static int a = 3;
static int b;
static void meth(int x) {
System.out.println("x = " + x);
System.out.println("a = " + a);
System.out.println("b = " + b);
}
static {
System.out.println("Static block initialized.");
b = a * 4;
}
public static void main(String args[]) {
meth(42);
}
}复制代码

一旦UseStatic 类被装载,全部的static语句被运行。首先,a被设置为3,接着static 块执行(打印一条消息),最后,b被初始化为a*4 或12。而后调用main(),main() 调用meth() ,把值42传递给x。3个println ( ) 语句引用两个static变量a和b,以及局部变量x 。

注意:在一个static 方法中引用任何实例变量都是非法的。下面是该程序的输出:

Static block initialized.
x = 42
a = 3
b = 12复制代码

在定义它们的类的外面,static 方法和变量能独立于任何对象而被使用。这样,你只要在类的名字后面加点号运算符便可。例如,若是你但愿从类外面调用一个static方法,你可使用下面通用的格式:

classname.method( )复制代码

这里,classname 是类的名字,在该类中定义static方法。能够看到,这种格式与经过对象引用变量调用非static方法的格式相似。一个static变量能够以一样的格式来访问——类名加点号运算符。这就是Java 如何实现全局功能和全局变量的一个控制版本。

下面是一个例子。在main() 中,static方法callme() 和static 变量b在它们的类以外被访问。

class StaticDemo {
static int a = 42;
static int b = 99;
static void callme() {
System.out.println("a = " + a);
}
}
class StaticByName {
public static void main(String args[]) {
StaticDemo.callme();
System.out.println("b = " + StaticDemo.b);
}
}复制代码

下面是该程序的输出:

a = 42
b = 99复制代码

static成员是不能被其所在class建立的实例访问的。

若是不加static修饰的成员是对象成员,也就是归每一个对象全部的。

加static修饰的成员是类成员,就是能够由一个类直接调用,为全部对象共有的.

6.与非静态代码区别

静态代码块,在虚拟机加载类的时候就会加载执行,并且只执行一次;非静态代码块,在建立对象的时候(即new一个对象的时候)执行,每次建立对象都会执行一次。例如:

例:
//普通类
public class PuTong {
    public PuTong(){
        System.out.print("默认构造方法!-->");
    }
    //非静态代码块
    {
        System.out.print("非静态代码块!-->");
    }
    //静态代码块
    static{
        System.out.print("静态代码块!-->");
    }
    public static void test(){
        {
            System.out.println("普通方法中的代码块!");
        }

    }
}复制代码

7.static语句块详解

static{}(即static块),会在类被加载的时候执行且仅会被执行一次,通常用来初始化静态变量和调用静态方法。

public class Test
{
    public static int X = 100;

    public final static int Y = 200;

    public Test()
    {
        System.out.println("Test构造函数执行");
    }
    static
    {
        System.out.println("static语句块执行");
    }

    public static void display()
    {
        System.out.println("静态方法被执行");
    }

    public void display_1()
    {
        System.out.println("实例方法被执行");
    }

}
public class StaticBlockTest
{
    public static void main(String args[])
    {
        try
        {
            Class.forName("Test");
            Class.forName("Test");
        }
        catch (ClassNotFoundException e)
        {
            e.printStackTrace();
        }
    }
}复制代码

结果:你会发现虽然执行了两条Class.forName("Test")语句,可是,只输出了一条"静态方法被执行"语句;其实第二条Class.forName()语句已经无效了,由于在虚拟机的生命周期中一个类只被加载一次;又由于static{}是伴随类加载执行的,因此,无论你new多少次对象实例,static{}都只执行一次。

7.1 static{}语句块执行的时机

static{}语句块执行的时机,即类被加载准确含义:

(1)用Class.forName()显示加载的时候;

(2)实例化一个类的时候,如将main()函数的内容改成:Test t=new Test();//这种形式其实和1相比,原理是相同的,都是显示的加载这个类,读者能够验证Test t=new Test();和Test t=(Test)Class.forName().newInstance();这两条语句效果相同。

(3)调用类的静态方法的时候,如将main()函数的内容改成:Test.display();

(4)调用类的静态变量的时候,如将main()函数的内容改成:System.out.println(Test.X);

整体来讲就这四种状况,可是咱们特别须要注意一下两点:

(1)调用类的静态常量的时候,是不会加载类的,即不会执行static{}语句块,读者能够本身验证一下(将main()函数的内容改成System.out.println(Test.Y);),你会发现程序只输出了一个200;(这是java虚拟机的规定,当访问类的静态常量时,若是编译器能够计算出常量的值,则不会加载类,不然会加载类)

(2)用Class.forName()形式的时候,咱们也能够本身设定要不要加载类,如

将Class.forName("Test")
改成
Class.forName("Test",false,StaticBlockTest.class.getClassLoader())复制代码

你会发现程序什么都没有输出,即Test没有被加载,static{}没有被执行。

7.2 static{}语句块的执行次序

(1)当一个类中有多个static{}的时候,按照static{}的定义顺序,从前日后执行;

(2)先执行完static{}语句块的内容,才会执行调用语句;

public class TestStatic
{
    static
    {
        System.out.println(1);
    }
    static
    {
        System.out.println(2);
    }
    static
    {
        System.out.println(3);
    }

    public static void main(String args[])
    {
        System.out.println(5);
    }

    static
    {
        System.out.println(4);
    }
}复制代码

结果:

程序会输出1,2,3,4,5复制代码

(3)若是静态变量在定义的时候就赋给了初值(如 static int X=100),那么赋值操做也是在类加载的时候完成的,而且当一个类中既有static{}又有static变量的时候,一样遵循“先定义先执行”的原则;

class Test
{
    public static int X = 300;
    static
    {
        System.out.println(X);
        X = 200;
        System.out.println(X);
    }
}

public class StaticBlockTest
{
    public static void main(String args[])
    {
        System.out.println(Test.X);
    }
}复制代码

结果:程序会依次输出300,200,200,先执行完X=300,再执行static{}语句块。

(4)访问静态常量,若是编译器能够计算出常量的值,则不会加载类。即若是A类的静态常量值是经过B类的静态常量赋值,则不加载,不然须要加载A类。

public class TestA
{
    public static final int a = TestB.a;

    public static final int b = TestB.b;
   public static final int c = 90;
  static
    {
        System.out.println("TestA static语句块执行");
    }
}

public class TestB
{
    public static int a = 90;

    public static final int b = 90;

    static
    {
        System.out.println("TestB static语句块执行");
    }
}

public class StaticTest
{
    public static void main(String args[])
    {
        System.out.println(TestA.a);
    }
}复制代码

System.out.println(TestA.a);的结果:

TestB static语句块执行
TestA static语句块执行
90复制代码

System.out.println(TestA.b)和System.out.println(TestA.c)的结果:

1复制代码

7.3类加载特性 :

1)在虚拟机的生命周期中一个类只被加载一次。

2)类加载的原则:延迟加载,能少加载就少加载,由于虚拟机的空间是有限的。

3)类加载的时机: - 第一次建立对象要加载类. - 调用静态方法时要加载类,访问静态属性时会加载类。 - 加载子类时一定会先加载父类。 - 建立对象引用不加载类. - 子类调用父类的静态方法时

(1)当子类没有覆盖父类的静态方法时,只加载父类,不加载子类
(2)当子类有覆盖父类的静态方法时,既加载父类,又加载子类复制代码
  • 访问静态常量,若是编译器能够计算出常量的值,则不会加载类,例如:public static final int a =123;不然会加载类,例如:public static final int a = math.PI

8.Java的初始化块、静态初始化块、构造函数的执行顺序

8.1 执行顺序

首先定义A, B, C三个类用做测试,其中B继承了A,C又继承了B,并分别给它们加上静态初始化块、非静态初始化块和构造函数,里面都是一句简单的输出。主类Main里面也如法炮制。 测试代码

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

    {
        System.out.println("Instance init A.");
    }

    A() {
        System.out.println("Constructor A.");
    }
}

class B extends A {
    static {
        System.out.println("Static init B.");
    }

    {
        System.out.println("Instance init B.");
    }

    B() {
        System.out.println("Constructor B.");
    }
}

class C extends B {

    static {
        System.out.println("Static init C.");
    }

    {
        System.out.println("Instance init C.");
    }

    C() {
        System.out.println("Constructor C.");
    }
}

public class Main {

    static {
        System.out.println("Static init Main.");
    }

    {
        System.out.println("Instance init Main.");
    }

    public Main() {
        System.out.println("Constructor Main.");
    }

    public static void main(String[] args) {
        C c = new C();
        //B b = new B();
    }
}复制代码

固然这里不使用内部类,由于内部类不能使用静态的定义;而用静态内部类就失去了通常性。那么能够看到,当程序进入了main函数,并建立了一个类C的对象以后,输出是这样子的:

Static init Main.
Static init A.
Static init B.
Static init C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.
Instance init C.
Constructor C.复制代码

观察上面的输出,能够观察到两个有趣的现象:

1)Main类是确定没有被实例化过的,可是因为执行main入口函数用到了Main类,因而static初始化块也被执行了;

2)全部的静态初始化块都优先执行,其次才是非静态的初始化块和构造函数,它们的执行顺序是:

  • 父类的静态初始化块
  • 子类的静态初始化块
  • 父类的初始化块
  • 父类的构造函数
  • 子类的初始化块
  • 子类的构造函数

那么若是有多个实例化对象,又会不会发生变化呢?因而在第一个C类的对象后面,再实例化一个B类的对象,再观察输出:

Static init Main.
Static init A.
Static init B.
Static init C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.
Instance init C.
Constructor C.
Instance init A.
Constructor A.
Instance init B.
Constructor B.复制代码

能够发现这输出跟前面的基本长得同样对吧?只是在后面多了4行,那是新的B类对象实例化时产生的信息,一样也是父类A的初始化块和构造函数先执行,再轮到子类B的初始化块和构造函数执行;同时还发现,静态初始化块的输出只出现了一次,也就是说每一个类的静态初始化块都只在第一次实例化该类对象时执行一次。

不管如何,初始化块和构造函数总在一块儿执行是件有趣的事情,让咱们反编译一下看看吧!

查看生成目录发现已经生成了4个.class文件,分别是A.class, B.class, C.class, Main.class,先看看Main.class的结构(这里从新注释了new B):

1 javap -c Main

Compiled from "Main.java"
public class Main {
  public Main();
    Code:
       0: aload_0
       1: invokespecial #1 // Method java/lang/Object."<init>":()V
       4: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3 // String Instance init Main.
       9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #5 // String Constructor Main.
      17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: return

  public static void main(java.lang.String[]);
    Code:
       0: new           #6 // class C
       3: dup
       4: invokespecial #7 // Method C."<init>":()V
       7: astore_1
       8: return

  static {};
    Code:
       0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #8 // String Static init Main.
       5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

Main.class的反编译结果复制代码

能够看到整个Main类被分红三个部分,static {}部分很显然,就是咱们的static初始化块,在里面调用了println并输出了String“Static init Main.”;而main入口函数也很清晰,首先新实例化了一个类C的对象,而后调用了类C的构造函数,最后返回;而上面public Main();的部分就颇有意思了,这是类Main的构造函数,但咱们看到里面调用了两次println,分别输出了String“Instance init Main.”和String“Constructor Main.”。难道初始化块和构造函数被合并到一块儿了?

咱们再看看C类的反编译结果吧:

1 javap -c C

Compiled from "Main.java"
class C extends B {
  C();
    Code:
       0: aload_0
       1: invokespecial #1 // Method B."<init>":()V
       4: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3 // String Instance init C.
       9: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
      15: ldc           #5 // String Constructor C.
      17: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      20: return

  static {};
    Code:
       0: getstatic     #2 // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #6 // String Static init C.
       5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return
}

C.class的反编译结果复制代码

静态初始化块仍然单独分出一部分,输出了咱们的调试语句。而另外一部分,仍然仍是类C的构造函数C();,能够看到它先调用了父类B的构造函数,接着输出了咱们初始化块中的语句,而后才输出咱们写在构造函数中的语句,最后返回。屡次试验也都是如此。因而咱们可以推断:初始化块的代码是被加入到子类构造函数的前面,父类初始化的后面了。

8.2 可能的用途:

8.2.1 静态初始化块

1)用于初始化静态成员变量:

好比给类C增长一个静态成员变量sub,咱们在static块里面给它赋值为5:

1 class C extends B {
 2 
 3     static public int a;
 4 
 5     static {
 6         a = 5;
 7         System.out.println("Static init C.");
 8     }
 9 
10 ......
11 
12 }复制代码

main函数里输出这个静态变量C.sub:

1 public static void main(String[] args) {
2     System.out.println("Value of C.sub: " + C.sub);
3 }复制代码

则输出结果:

Static init Main.
Static init A.
Static init B.
Static init C.
Value of C.sub: 5复制代码

符合类被第一次加载时执行静态初始化块的结论,且C.sub被正确赋值为5并输出了出来。

可是乍一看彷佛没有什么用,由于静态成员变量在定义时就能够顺便赋值了。所以在赋值方面有点鸡肋。

8.2.2 执行初始化代码

好比能够记录第一次访问类的日志,或方便单例模式的初始化等。对于单例模式,能够先用static块初始化一些可能还被其余类访问的基础参数,等到真正须要加载大量资源的时候(getInstance)再构造单体,在构造函数中加载资源。

8.3 非静态初始化块

基本跟构造函数一个功能,但比构造函数先执行。最多见的用法应该仍是代码复用,即多个重载构造函数都有若干段相同的代码,那么能够把这些重复的代码拉出来放到初始化块中,但仍然要注意它的执行顺序,对顺序有严格要求的初始化代码就不适合使用了。

总结:

  • 静态初始化块的优先级最高,也就是最早执行,而且仅在类第一次被加载时执行;
  • 非静态初始化块和构造函数后执行,而且在每次生成对象时执行一次;
  • 非静态初始化块的代码会在类构造函数以前执行。所以若要使用,应当养成把初始化块写在构造函数以前的习惯,便于调试;
  • 静态初始化块既能够用于初始化静态成员变量,也能够执行初始化代码;
  • 非静态初始化块能够针对多个重载构造函数进行代码复用。

欢迎你们扫下方二维码加java学习技术交流群,一块儿夯实基础,提高自我价值

相关文章
相关标签/搜索