Java中的static关键字解析


  static关键字,以下是本文的目录大纲:
  一.static关键字的用途
      二.staitc成员的声明与初始化
      三.static成员与JVM
      四.static关键字的误区
      五.static关键字在并发中的使用
      六.static关键字与单例模式(Singleton)
      七.常见的static问题
一.static关键字的用途
    成员变量分为普通 变量 静态变量 。其中普通变量属于某一个具体的对象,必须在类的new实例化后才真正存在,不同的对象拥有不同的普通成员变量。而静态变量被该类所有的对象公有(相当于全局变量),不需要实例化就已经存在。
         方法也可分为普通 方法 静态方法 。其中,普通方法必须在类实例化之后通过对象来调用,而静态方法可以在类实例化之前就使用。与成员变量不同的是:static 方法,在内存中只有一份——无论该类有多少个实例,都共用同一个方法。

     在《Java编程思想》P86页有这样一段话:
   “static方法就是没有this的方法。在static方法内部不能调用非static方法,反过来是可以的,非static方法内部可以调用static方法。而且可以在没有创建任何对象的前提下,仅仅通过类本身来调用static方法。这实际上正是static方法的主要用途。”
  没有创建对象的情况下,调用static(方法/变量)。

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

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

1)static方法

  static方法一般称作静态方法,由于静态方法不依赖于任何对象就可以进行访问,因此对于静态方法来说,是没有this的,因为它不依附于任何对象,既然都没有对象,就谈不上this了。并且由于这个特性,在静态方法中不能访问类的非静态成员变量和非静态成员方法,因为非静态成员方法/变量都是必须依赖具体的对象才能够被调用。

  但是要注意的是,虽然在静态方法中不能访问非静态成员方法和非静态成员变量,但是在非静态成员方法中是可以访问静态成员方法/变量的。
     另外记住,即使没有显示地声明为static,类的构造器实际上也是静态方法

2)static变量

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

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

3)static代码块
  static块可以置于类中的任何地方,类中可以有多个static块。在类初次被加载的时候,会按照static块的顺序来执行每个static块,并且只会执行一次,用来初始化static成员变量。
  isBornBoomer是用来这个人是否是1946-1964年出生的,而每次isBornBoomer被调用的时候,都会生成startDate和birthDate两个对象,造成了空间浪费,如果改成这样效率会更好:
 
    class Person{
         private static Date startDate, endDate;
         static{
                  startDate = Date.valueOf("1946");
                  endDate = Date.valueOf("1964");
         }
     }

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


二.static成员的声明与初始化
  java初始化顺序:静态变量声明 > 静态变量初始化、静态初始化块 > (new对象的时候:)变量、初始化块 > 构造器
       
      以上static资源的初始化是在类加载的时候进行,后面变量、初始化块,构造器这三个是在new对象的时候裁进行。如果仅仅是类加载但是不去new对象,那么只会对static的资源进行初始化。
      那么当有父类的情况呢?

        java初始化顺序: 父类的静态代码 --->  子类的静态代码 -->   (new对象的时候: )   父类的非静态代码   --->   父类构造函数 --->  子类非静态代码  --->   子类构造函数



      类中的静态成员会随着类的加载而加载并初始化。那么类是什么时候加载呢?

          - 创建类的实例

          - 访问类的静态变量(  除常量【被final修辞的静态变量】原因:常量一种特殊的变量,因为编译器把他们当作值(value)而不是域(field)来对待。如果你的代码中用到了常变量(constant variable),编译器并不会生成字节码来从对象中载入域的值,而是直接把这个值插入到字节码中。这是一种很有用的优化,但是如果你需要改变final域的值那么每一块用到那个域的代码都需要重新编译。)

          - 访问类的静态方法

          - 反射如(Class.forName("my.xyz.Test"))

          - 当初始化一个类时,发现其父类还未初始化,则先出发父类的初始化

          - 虚拟机启动时,定义了main()方法的那个类先初始化

      所以,static的变量是jvm加载类的时候,就已经做了声明并初始化的了,与new对象无关。



问题1:static变量与static块

1.  Java类中可以定义一个static块,用于静态变量的初始化。如:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i;  
  3.     static {  
  4.         _i = 10;  
  5.     }  
  6. }  

当然最常用的初始化静态变量的操作是在声明变量时直接进行赋值操作。如:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i = 10;  
  3. }  

那么上述两例在本质上有什么区别吗?回答是没有区别。以上两例代码编译之后的字节码完全一致。

2.  由于静态变量是通过赋值操作进行初始化的,因此可以通过静态函数返回值的方式为其初始化。如:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i = init();  
  3.       
  4.     private static int init() {  
  5.         return 10;  
  6.     }  
  7. }  

其本质与下面的代码相同:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i;  
  3.     static {  
  4.         _i = init();  
  5.     }  
  6.       
  7.     private static int init() {  
  8.         return 10;  
  9.     }  
  10. }  
以上两例代码编译之后的字节码完全一致。

3.  类定义中可以存在多个static块吗?回答是可以。如:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i;  
  3.     static {  
  4.         _i = 10;  
  5.     }  
  6.       
  7.     public static void main(String[] args) {  
  8.     }  
  9.       
  10.     static {  
  11.         _i = 20;  
  12.     }  
  13. }  
[java]  view plain copy
  1. public class Test {  
  2.     public static int _i;  
  3.       
  4.     public static void main(String[] args) {  
  5.     }  
  6.       
  7.     static {  
  8.         _i = 10;  
  9.         _i = 20;  
  10.     }  
  11. }  

以上两例代码编译之后的字节码完全一致。
类定义中可以有多个static块,而且在编译时编译器会将多个static块按照代码的前后位置重新组合成一个static块。

问题2:如何看待static变量的声明和初始化

静态变量存放在常量池之中。如:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i = 10;  
  3. }  
静态变量被保存到常量池中的工作原理这里不深入讨论。在此需要注意的是:
  • 静态变量的声明与初始化是两个不同的操作;
  • 静态变量的声明在编译时已经明确了内存的位置。

如:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i = 10;  
  3. }  

上述代码的本质可以视为:

[java]  view plain copy
  1. public class Test {  
  2.     // 静态变量的声明  
  3.     public static int _i;  
  4.   
  5.     // 静态变量的初始化  
  6.     static {  
  7.         _i = 10;  
  8.     }  
  9. }  

由于静态变量的声明在编译时已经明确,所以静态变量的声明与初始化在编码顺序上可以颠倒。也就是说可以先编写初始化的代码,再编写声明代码。如:

[java]  view plain copy
  1. public class Test {  
  2.     // 静态变量的初始化  
  3.     static {  
  4.         _i = 10;  
  5.     }  
  6.       
  7.     // 静态变量的声明  
  8.     public static int _i;  
  9. }  




三.static成员与JVM


JAVA的JVM的内存可分为3个区:堆(heap)、栈(stack)和方法区(method)

堆区:
1.存储的全部是对象,每个对象都包含一个与之对应的class的信息。(class的目的是得到操作指令)
2.jvm只有一个堆区(heap)被所有线程共享(因此多线程下,单例对象中的成员变量和和普通类的static的变量是线程不安全的),堆中不存放基本类型和对象引用,只存放对象本身.
3.一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。

栈区:
1.每个线程包含一个栈区,栈中只保存基础数据类型的对象和自定义对象的引用(不是对象),对象都存放在堆区中
2.每个栈中的数据(原始类型和对象引用)都是私有的,其他栈不能访问。
3.栈分为3个部分:基本类型变量区、执行环境上下文、操作指令区(存放操作指令)。
4.由编译器自动分配释放 ,存放函数的参数值,局部变量的值等.

静态区/方法区:
1.方法区又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.方法区中包含的都是在整个程序中永远唯一的元素,如class,static变量。
3.全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。

所以:1. static的成员变量是保存在JVM的方法区中的。
         2. 基础数据类型、局部变量的成员放在栈区
         3. new的对象放在堆区中


四.static关键字的误区

1.static关键字会改变类中成员的访问权限吗?

  有些初学的朋友会将java中的static与C/C++中的static关键字的功能混淆了。在这里只需要记住一点:与C/C++中的static不同,Java中的static关键字不会影响到变量或者方法的作用域。在Java中能够影响到访问权限的只有private、public、protected(包括包访问权限)这几个关键字。看下面的例子就明白了:

  提示错误"Person.age 不可视",这说明static关键字并不会改变变量和方法的访问权限。

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

  虽然对于静态方法来说没有this,那么在非静态方法中能够通过this访问静态成员变量吗?先看下面的一个例子,这段代码输出的结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
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。在这里永远要记住一点:静态成员变量虽然独立于对象,但是不代表不可以通过对象去访问,所有的静态方法和静态变量都可以通过对象访问(只要访问权限足够)。

3.static能作用于局部变量么?

  在C/C++中static是可以作用域局部变量的,但是在Java中切记:static是不允许用来修饰局部变量。不要问为什么,这是Java语法的规定。


五.static关键字在并发中的使用
  
static在什么情况下线程不安全 :

1. 一个线程类里面有static成员变量,这个线程类被new多次去线程执行,那么这个static成员线程不安全。其他普通的成员变量肯定是线程安全的了,因为这个类的对象都是被new多次出来的,普通成员变量跟随这些对象都是不同的。而static变量只有一份,所以static成员变量线程不安全,普通变量是每个对象都有一份,线程安全。
2.  一个线程类是单例(Spring管理的Bean默认都是单例)的,这个线程类多次被线程执行。这个类的所有成员变量都是线程不安全的,无论是不是static的,都是线程不安全。因为这个类是单例的,对象只有一个,那么里面的成员变量都只有一份,一定是线程不安全的,static的成员变量也只有一份,所以也是线程不安全。
3. 静态工具类里面都是static静态方法。其他多线程类里面调这个静态工具类的静态方法,静态工具类里面的static成员变量都很危险(静态工具类里面不可能有普通的成员变量,因为静态工具类只会被类加载初始化static资源,不会被new成对象,仅仅是直接调用static静态工具方法)。因为静态工具类都是直接调用Static工具方法,又因为这个类的static成员变量都只有一份,所以线程不安全。



六.static关键字与单例模式(Singleton)
staitc修饰的成员,在jvm类加载的时候就已经初始化,并且是全局变量,内存中只有一份,与new对象无关。
单例模式(Singleton),某个成员变量,该类在内存中只有一个对象,所以该成员变量也只有一份

当多线程下,单例在多线程模式中,成员变量只有一份。普通对象在多线程中,每个线程都new一个新的对象,但是static成员变量却只有一份。因此两者能做到一样的效果。
Spring管理的bean默认是单例的,所以在多线程下,每个成员变量都是一份。
自己管理的项目,在多线程下,static成员变量只有一份。

相同点:
   如果想让某个类中的某个成员变量只有一份,可以把该成员变量做成一个Singleton单例模式,也可以把这个成员变量定义成static。(虽然以上亮点都能做到这个需求,但是原理不同,注意用途)  

不同点:
  除了以上相同点,就都是不同点。

七.常见的static问题

1.下面这段代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public  class  Test  extends  Base{
 
     static {
         System.out.println( "test static" );
     }
     
     public  Test(){
         System.out.println( "test constructor" );
     }
     
     public  static  void  main(String[] args) {
         new  Test();
     }
}
 
class  Base{
     
     static {
         System.out.println( "base static" );
     }
     
     public  Base(){
         System.out.println( "base constructor" );
     }
}
base static
test static
base constructor
test constructor

  至于为什么是这个结果,我们先不讨论,先来想一下这段代码具体的执行过程,在执行开始,先要寻找到main方法,因为main方法是程序的入口,但是在执行main方法之前,必须先加载Test类,而在加载Test类的时候发现Test类继承自Base类,因此会转去先加载Base类,在加载Base类的时候,发现有static块,便执行了static块。在Base类加载完成之后,便继续加载Test类,然后发现Test类中也有static块,便执行static块。在加载完所需的类之后,便开始执行main方法。在main方法中执行new Test()的时候会先调用父类的构造器,然后再调用自身的构造器。因此,便出现了上面的输出结果。

2.这段代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public  class  Test {
     Person person =  new  Person( "Test" );
     static {
         System.out.println( "test static" );
     }
     
     public  Test() {
         System.out.println( "test constructor" );
     }
     
     public  static  void  main(String[] args) {
         new  MyClass();
     }
}
 
class  Person{
     static {
         System.out.println( "person static" );
     }
     public  Person(String str) {
         System.out.println( "person " +str);
     }
}
 
 
class  MyClass  extends  Test {
     Person person =  new  Person( "MyClass" );
     static {
         System.out.println( "myclass static" );
     }
     
     public  MyClass() {
         System.out.println( "myclass constructor" );
     }
}
复制代码
test static
myclass static
person static
person Test
test constructor
person MyClass
myclass constructor
复制代码

  类似地,我们还是来想一下这段代码的具体执行过程。首先加载Test类,因此会执行Test类中的static块。接着执行new MyClass(),而MyClass类还没有被加载,因此需要加载MyClass类。在加载MyClass类的时候,发现MyClass类继承自Test类,但是由于Test类已经被加载了,所以只需要加载MyClass类,那么就会执行MyClass类的中的static块。在加载完之后,就通过构造器来生成对象。而在生成对象的时候,必须先初始化父类的成员变量,因此会执行Test中的Person person = new Person(),而Person类还没有被加载过,因此会先加载Person类并执行Person类中的static块,接着执行父类的构造器,完成了父类的初始化,然后就来初始化自身了,因此会接着执行MyClass中的Person person = new Person(),最后执行MyClass的构造器。

3.这段代码的输出结果是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
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块的顺序执行的。

4. 下面 代码的结果是什么

[java]  view plain copy
  1. public class Test {  
  2.     static {  
  3.         _i = 20;  
  4.     }  
  5.     public static int _i = 10;  
  6.       
  7.     public static void main(String[] args) {  
  8.         System.out.println(_i);  
  9.     }  
  10. }  

其本质可以用下面的代码表示:

[java]  view plain copy
  1. public class Test {  
  2.     static {  
  3.         _i = 20;  
  4.     }  
  5.     public static int _i;  
  6.     static {  
  7.         _i = 10;  
  8.     }  
  9.       
  10.     public static void main(String[] args) {  
  11.         System.out.println(_i);  
  12.     }  
  13. }  

再简化一下,可以表示为:

[java]  view plain copy
  1. public class Test {  
  2.     public static int _i;  
  3.       
  4.     static {  
  5.         _i = 20;  
  6.         _i = 10;  
  7.     }  
  8.       
  9.     public static void main(String[] args) {  
  10.         System.out.println(_i);  
  11.     }  
  12. }  

答案:10

5. 当遇到分布式环境下的初始化问题

当使用Flink的时候,job会现在job manager进程上执行,然后将source/transform/sink operator解析成对应的execution graph,把这个graph分发到对应的task manager上去执行。因为job manager和task manager不是一个进程,更有可能不是一台服务器上的进程,因此spring初始化的代码在job manager进程中执行的,而具体的operator的时候,task manager进程中是没有经过spring初始化的,所以遇到的问题。

解决方法:
1. 临时方案:使用static块
将spring的初始化放在一个静态工具类的static块中:
classe SpringHelper{
     private static ApplicationContext context;    
     static {
                  init(new ClassPathXmlApplicationContext("classpath:applicationContext.xml"));
     }
     ........
}
这样写的话,每次有调用SpringHelper这个类的static方法的时候,如果这个类之前已经加载过,那么spring肯定已经初始化过。如果这个类没有被加载过,那么只要我们一调用这个static方法,JVM就会加载这个类,那么static块就会被JVM加载的时候最先执行,那么spring也就初始化了。

2. 当然是从flink的角度找到对应prepare()方法去初始化资源了。以上的临时方案只是对static块的一次深层次的理解和应用。







  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值