你知道麼?static關鍵字有5種用法

說到static,靜態變量和靜態方法大家隨口就來,因爲他們在實際開發中應用很廣泛,但他們真正在使用的時候會存在很多問題,而且它的使用不只那兩種:

      1.靜態變量。

      2.靜態方法。

      3.靜態代碼塊。

      4.靜態內部類。

      5.靜態導入。

接下來我們看一下這些用法。

1.靜態變量

      靜態變量屬於類,內存中只有一個實例,當類被加載,就會爲該靜態變量分配內存空間,跟 class 本身在一起存放在方法區中永遠不會被回收,除非 JVM 退出。(方法區還存哪些東西可以看看:Java虛擬機運行時數據區域)靜態變量的使用方式:【類名.變量名】和【對象.變量名】。

      【實例】實際開發中的日期格式化類SimpleDateFormat會經常用到,需要的時候會new一個對象出來直接使用,但我們知道頻繁的創建對象不好,所以在DateUtil中直接創建一個靜態的SimpleDateFormat全局變量,直接使用這個實例進行操作,因爲內存共享,所以節省了性能。但是它在高併發情況下是存在線程安全問題的。SimpleDateFormat線程安全問題代碼復現:

public class OuterStatic {
	
	 public static class InnerStaticSimpleDateFormat  implements Runnable {
	        @Override
	        public void run() {
	            while(true) {
	                try {
	                	Thread.sleep(3000);
	                    System.out.println(Thread.currentThread().getName()
                                  +":"+DateUtil.parse("2017-07-27 08:02:20"));
	                } catch (Exception e) {
	                    e.printStackTrace();
	                }
	            }
	        }    
	    }
	    public static void main(String[] args) {
	        for(int i = 0; i < 3; i++){
	        	 new Thread(new InnerStaticSimpleDateFormat(), "測試線程").start();
	        	
	        }
	            
	    }
}
class DateUtil {
    
    private static  volatile SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    
    public static  String formatFromDate(Date date)throws ParseException{
        return sdf.format(date);
    }
    public static Date parseToDate(String strDate) throws ParseException{

        return sdf.parse(strDate);
    }
}

      雖然有volatile使對象可見,但運行後有一定機率會報java.lang.NumberFormatException: multiple points或For input string: ""等錯誤,原因是多線程都去操作一個對象(本圖來自於:關於 SimpleDateFormat 的非線程安全問題及其解決方案):

                           你知道麼?static關鍵字有5種用法。

      解決辦法:1.使用私有的對象。2.加鎖。3.ThreadLocal。4.使用第三方的日期處理函數。5.Java8推出了線程安全、簡易、高可靠的時間包,裏面有LocalDateTime年月日十分秒;LocalDate日期;LocalTime時間三個類可供使用。

      下圖是使用私有對象和ThreadLocal解決高併發狀態的圖解。

                      你知道麼?static關鍵字有5種用法。

      本文給出使用私有的對象和加鎖兩種實現代碼,ThreadLocal方式讀者可以嘗試自己實現

public class DateUtil {

    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      
    public static String formatFromDate(Date date)throws ParseException{
      //方式一:讓內存不共享,到用的時候再創建私有對象,使用時註釋掉全局變量sdf
      //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
      //return sdf.format(date);
      //方式二:加鎖,使用時打開全局變量sdf的註釋
        synchronized(sdf){
            return sdf.format(date);
        }  
    }
   public static Date parseToDate(String strDate) throws ParseException{
       //方式一:使用時註釋掉全局變量sdf
       //SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       //return sdf.parse(strDate);
       //方式二:加鎖,使用時打開全局變量sdf的註釋
        synchronized(sdf){
            return sdf.parse(strDate);
        }
    } 
}

 

2.靜態方法

      靜態方法和非靜態方法一樣,都跟class 本身在一起存放在內存中,永遠不會被回收,除非 JVM 退出,他們使用的區別的一個方面是非static方法需要實例調用,static方法直接用類名調用。

      【實例一】單例模式,它提供了一種創建對象的最佳方式,保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

public class Singleton {
   private static volatile Singleton instance = null;  
    static {  //靜態代碼塊,後面講
       instance = new Singleton();  
    }  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }      
}

      靜態的方法不必實例化就能直接使用,用法方便,不用頻繁的爲對象開闢空間和對象被回收,節省系統資源。是不是相較之下覺得static用的比較爽呢?但是他也會帶來一些問題:

      【實例二】一般工具類中的方法都寫成static的,比如我們要實現一個訂單導出功能,代碼如下:

public class ExportExcelUtil{
    @Autowired
    private static OrderService orderService ;

    public static void exportExcel(String id){
       //查詢要導出的訂單的數據
       Order order =orderService.getById(id);//這裏的orderService對象會是null
      //...省略導出代碼...
    }
} 

      爲什麼orderService會是null?原因不是Spring沒注入,而是static方法給它"清空"了。解決方案一:@PostConstruct,它修飾的方法會在服務器加載Servlet時執行一次,代碼如下:

@Component //這個註解必須加
public class ExportExcelUtil{
    @Autowired
    OrderService orderService ;

    private static ExportExcelUtil  exportExcelUtil; 

    //註解@PostConstruct 這個其實就是類似聲明瞭,當你加載一個類的構造函數之後執行的代碼塊,
    //也就是在加載了構造函數之後,就將service複製給一個靜態的service。
     @PostConstruct  
     public void init() {       
        exportExcelUtil= this; 
        exportExcelUtil.orderService = this.orderService ; 
     }  

    public static void exportExcel(String id){
       //是不是很像經典main方法的調用模式呢?
       Order order =exportExcelUtil.orderService .getById(id);
       //...省略導出代碼...
    }
}

     

      每個工具類都要去加上@PostConstruct註解,代碼重複性高。那我們可不可以直接從Spring容器中獲取Bean實例?

解決方案二:ApplicationContextAware。通過它Spring容器會自動把上下文環境對象注入到ApplicationContextAware接口的實現類中setApplicationContext方法裏。

換句話說,我們在ApplicationContextAware的實現類中,就可以通過這個上下文環境對象得到Spring容器中的Bean。

首先,在web項目中的web.xml中配置加載Spring容器的Listener:

<!-- 初始化Spring容器,讓Spring容器隨Web應用的啓動而自動啓動 -->  
    <listener>  
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>  
    </listener>  

然後,實現ApplicationContextAware接口:

public class SpringContextBean implements ApplicationContextAware{
	private static ApplicationContext context = null;

	public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
	{
		context = applicationContext;
	}

	public static <T> T getBean(String name)
	{
		return (T)context.getBean(name);
	}

	public static <T> T getBean(Class<T> beanClass){
		return context.getBean(beanClass);
	}
}

最後,在Spring配置文件中註冊該工具類:

<bean id="springContextBean" class="com.test.SpringContextBean"></bean>

原來的導出工具類代碼可以簡化到如下:

public class ExportExcelUtil{
    public static void exportExcel(String id){
      OrderService orderService = SpringContextBean.getBean(OrderService.class);
      Order order =orderService .getById(id);
       //...省略導出代碼...
    }
}

3.靜態代碼塊

      我們其實在工作中一直用到的代碼塊,所謂代碼塊是指使用“{}”括起來的一段代碼。其中靜態代碼塊只執行一次,構造代碼塊在每次創建對象是都會執行。根據位置不同,代碼塊可以分爲四種:普通代碼塊、構造塊、靜態代碼塊、同步代碼塊。ref:Java中普通代碼塊,構造代碼塊,靜態代碼塊區別及代碼示例

      【實例】因爲JVM只爲靜態分配一次內存,在加載類的過程中完成靜態變量的內存分配。所以實際工作中我們可以使用靜態代碼塊初始化一些不變的屬性:

//final表示此map集合是不可變得
public  static  final  Map<String,String> spuKeysMap = new HashMap<String,String>();
static{
   spuKeysMap.put("spuName","男裝");
   spuKeysMap.put("spuCode","男裝編碼");
   spuKeysMap.put("spuBrand","品牌");
   spuKeysMap.put("owner","所有者");
}

       但是靜態代碼塊和靜態變量初始化有什麼關係?在上文的單例模式中,我們使用了靜態代碼塊來創建對象,爲何那那樣寫?我在網上看到了這樣一段代碼:

    static {  
        _i = 10;  
    }  
    public static int _i = 20;  
      
    public static void main(String[] args) {  
        System.out.println(_i);  
    }  

      上面的結果是10還是20?如果存在多個代碼塊呢?

	static {  
	     _i = 10;  
	}  
	public static int _i =30;
	static {  
	    _i = 20;  
	}   
	public static void main(String[] args) {  
	    ystem.out.println(_i);
	}  
 

     測試過後你會發現兩個答案結果都是20。

     因爲其實public static int _i = 10;  和如下代碼:

    public static int _i;  
    static {  
        _i = 10;  
    }  

      是沒有區別的,他們在編譯後的字節碼完全一致(讀者可以使用javap -c命令查看字節碼文件),所以兩個例子的結果就是最後一次賦值的數值。

4.靜態內部類

      在定義內部類的時候,可以在其前面加上一個權限修飾符static,此時這個內部類就變爲了靜態內部類。

     【實例一】前文中寫靜態方法時的實例一,我們用了static塊初始化單例對象,這樣做有一個弊端,在調用單例其他方法時也會初始化對象,現在我們只希望在調用getInstance方法時初始化單例對象,要怎麼改進呢?因爲餓汗式寫法性能不太好,所以最終單例模式優化到如下:

public class Singleton {
    //使用靜態內部類初始化對象
    private static class  SingletonHolder{
      private static volatile Singleton instance = new Singleton();   
    }
    private Singleton (){}  
    public static Singleton getInstance() {  
        return SingletonHolder.instance;  
    }    
    public static  void otherMothed(){
      System.out.println("調用單例的其他方法時不會創建對象.")
    }  
    public static  void  main(String [] args){
        //Singleton.otherMothed();
         Singleton.getInstance();
    }
}

     【實例二】博主在內部類的實際開發中應用不多,但有時候還真不能沒有它,比如LinkedList使用瞭如下靜態內部類:

             

      其實在數據結構中我們把next和prev稱爲前後節點的指針,HashMap內部也使用了靜態內部類Entry的數組存放數據。爲了加深理解,讀者可以親自運行以下的代碼來體會一下靜態內部類。

      private static String name = "北京";  //靜態變量
	    public static void main(String[] args) { 
	    	new StaticInternal().outMethod();
	    }   
	    public static void outStaticMethod(String tempName) {       
	        System.out.println("外部類的靜態方法 name:"+tempName);  
	    } 
	    public void outMethod() {             // 外部類訪問靜態內部類的靜態成員:內部類.靜態成員  
	    	System.out.println("外部類的非靜態方法調用");
	        StaticInternal.InnerStaticClass inner = new   StaticInternal.InnerStaticClass();// 實例化靜態內部類對象  
	    	inner.setInnerName("呼呼");// 訪問靜態內部類的非靜態方法  
	    	InnerStaticClass.innerStaticMethod(); // 訪問靜態內部類的靜態方法  
	    	System.out.println("外部類訪問靜態內部類的非靜態方法 name:"+inner.getInnerName());
	    }  
	    static class InnerStaticClass {            
	    	String  InnerName="西安";  
	        static void innerStaticMethod() {    // 靜態內部類的靜態方法  
	            System.out.println("靜態內部類訪問外部類的靜態變量: name = " + name);  
	            outStaticMethod(new InnerStaticClass().InnerName);     // 訪問外部類的靜態方法  
	        }  
	        // 靜態內部類的非靜態方法  
	        public void setInnerName(String name) {  
	            System.out.println("靜態內部類的非靜態方法");  
	            this.InnerName = name;  
	        }  
	        public String getInnerName() {  
	        	System.out.println("靜態內部類的非靜態get方法 name="+name); 
	            return this.InnerName;  
	        } 
	    }  

實際中的應用可以看看:SpringMvc 靜態內部類 封裝請求數據,在這裏我們來總結一下靜態內部類:

      1.加強代碼可讀性。如:StaticInternal.InnerStaticClass inner = new   StaticInternal.InnerStaticClass();

      2.多個外部類的對象可以共享同一個靜態內部類的對象。

      3.靜態內部類無需依賴於外部類,它可以獨立於外部對象而存在。因爲靜態類和方法只屬於類本身,並不屬於該類的對象,更不屬於其他外部類的對象。

5.靜態導入

      靜態導入是JKD1.5後新加的功能,一般不怎麼常用,瞭解即可。有時候面試答出來這個會讓別的覺得你熱愛技術。

     【實例】 回想一下,我們以前是不是這樣寫獲取隨機數:

  public static void main(String[] args) {
	double random = Math.random();
	System.out.println(Math.PI);
	System.out.println(Math.round(random));
  } 

      Math出現的次數太多了,可以簡化嗎?現在我們可以直接使用靜態導入來寫,如下

import static java.lang.Math.*;

public class StaticInternal {
	
  public static void main(String[] args) {
	double random = random();
	System.out.println(PI);
	System.out.println(round(random));
  } 
}

      是不是方便了許多?但彆着急偷懶,因爲使用它過多會導致代碼可讀性差:

import static java.lang.Math.*;
import static java.lang.Integer.*;
public class StaticInternal {
	
  public static void main(String[] args) {
	double random = random();
	System.out.println(PI);
	System.out.println(round(random));
	System.out.println(bitCount(11));
  } 
}

      或許你知道PI是Math類的方法,那bitCount是哪個類的方法呢?所以儘量避免使用static導入,實在要導入的話,去掉*號通配符,直接寫成:java.lang.Integer.bitCount。

 

參考鏈接:https://my.oschina.net/liughDevelop/blog/1490005

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章