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

作者:我叫劉半仙
來源:https://my.oschina.net/liughDevelop/blog/1490005

說到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:

 

org.springframework.web.context.ContextLoaderListener

然後,實現ApplicationContextAware接口:

public class SpringContextBean implements ApplicationContextAware{
private static ApplicationContext context = null;
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException
{
context = applicationContext;
}
public static T getBean(String name)
{
return (T)context.getBean(name);
}
public static T getBean(Class beanClass){
return context.getBean(beanClass);
}
}

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


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

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 spuKeysMap = new HashMap();
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使用瞭如下靜態內部類:

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

其實在數據結構中我們把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。

博主是個普通的程序猿,水平有限,文章難免有錯誤,歡迎犧牲自己寶貴時間的讀者,就本文內容直抒己見,我的目的僅僅是希望對讀者有所啟發。


分享到:


相關文章: