幹貨——詳解Java中的關鍵字
在平時編碼中,我們可能只註意了這些static,final,volatile等關鍵字的使用,忽略了他們的細節,更深層次的意義。
本文總結了Java中所有常見的關鍵字以及一些例子。
static 關鍵字
概述:
當static修飾類的屬性或者方法時,那麽就可以在沒有創建對象的情況下使用該屬性或方法。
靜態塊也是static的一個應用,用於初始化類時的一些操作。
靜態方法和靜態變量
劃重點
被static修飾後的屬性或者方法,使用時不需要new 一個類,用類.屬性名或方法名訪問.
比如java.lang.Math就存放了很多靜態資源,可以直接使用Math.random()來獲取隨機數.
一些需要註意的地方
非靜態方法是可以訪問靜態資源的,
靜態方法是不能引用非靜態資源的。
來看一個代碼實例:
1 public class TestStatic { 2 3 protected int i = 100; 4 5 public static void main(String args[]){ 6 System.out.println(i); 7 } 8 }
在以上代碼,編譯的時候會出錯,main方法是靜態方法,變量i是非靜態的。
解決辦法是,將變量i加上static修飾。
不經就要提出一個問題,
為什麽非靜態方法可以訪問靜態資源,而靜態方法不能訪問非靜態資源呢?
從類加載機制上講,靜態資源是類初始化的時候加載的,然後非靜態資源是new一個該類的對象的時候加載的。
這就帶來一個問題:
加載類時默認先加載靜態資源的,當new一個對象之後,才會加載其他資源,所以在new對象之前,靜態資源是不知道類有哪些非靜態資源的,
但是當對象new出來之後,該類的所有屬性和方法都知道。
還有需要註意的是:
1.靜態屬性和方法可以通過類.屬性名或方法名,而且,該類的對象也是訪問靜態屬性和變量的。
2.Java的語法規定,static不能修飾局部變量。沒有為什麽,這就是規定。
靜態塊
靜態塊和靜態變量、靜態方法是沒什麽區別的,也是在類加載的時候執行,而且只執行一次。
關於靜態塊有兩點需要註意:
1.靜態資源的加載順序嚴格按照靜態資源的定義順序加載的
2.靜態塊,對於定義在它之後的靜態變量,可以賦值但不能訪問。
static的題目
下面main()方法的輸出結果是什麽:
public class InstanceClass extends ParentClass{ public static String subStaticField = "子類靜態變量"; public String subField = "子類非靜態變量"; public static StaticClass staticClass = new StaticClass("子類"); static { System.out.println("子類 靜態塊初始化"); } { System.out.println("子類 [非]靜態塊初始化"); } public InstanceClass(){ System.out.println("子類構造器初始化"); } public static void main(String args[]) throws InterruptedException { new InstanceClass(); } } class ParentClass{ public static String parentStaticField = "父類靜態變量"; public String parentField = "父類[非]靜態變量"; public static StaticClass staticClass = new StaticClass("父類"); static { System.out.println("父類 靜態塊初始化"); } { System.out.println("父類 [非]靜態塊初始化"); } public ParentClass(){ System.out.println("父類 構造器初始化"); } } class StaticClass{ public StaticClass(String name){ System.out.println(name+" 靜態變量加載"); } }
輸出結果:
父類 靜態變量加載
父類 靜態塊初始化
子類 靜態變量加載
子類 靜態塊初始化
父類 [非]靜態塊初始化
父類 構造器初始化
子類 [非]靜態塊初始化
子類構造器初始化
View Code
下面是我總結類加載流程,可以對照著這個流程,可以再重新看一下上面的例子,會有新的理解。
1. 加載父類靜態
1.1 為靜態屬性分配存儲空間並賦初始值
1.2 執行靜態初始化塊和靜態初始化語句(從上至下)
2. 加載子類靜態
2.1 為靜態屬性分配存儲空間
2.2 執行靜態初始化塊和靜態初始化語句(從上至下)
3. 加載父類非靜態
3.1 為非靜態塊分配空間
3.2 執行非靜態塊
4. 加載子類非靜態
4.1 為非靜態塊分配空間
4.2 執行非靜態塊
5. 加載父類構造器
5.1 為實例屬性分配存數空間並賦初始值
5.2 執行實例初始化塊和實例初始化語句
5.3 執行構造器內容
6. 加載子類構造器
6.1 為實例屬性分配存數空間並賦初始值
6.2 執行實例初始化塊和實例初始化語句
6.3 執行構造器內容
對照著剛才的規則,再看一下這個例子:
1 public class TestStaticLoad { 2 Person person = new Person("TestStaticLoad"); 3 static{ 4 System.out.println("TestStaticLoad static"); 5 } 6 7 public TestStaticLoad() { 8 System.out.println("TestStaticLoad constructor"); 9 } 10 11 public static void main(String[] args) { 12 new God(); 13 } 14 15 } 16 17 class Person{ 18 static{ 19 System.out.println("person static"); 20 } 21 public Person(String str) { 22 System.out.println("person "+str); 23 } 24 } 25 26 27 class God extends TestStaticLoad { 28 Person person = new Person("God"); 29 static{ 30 System.out.println("God static"); 31 } 32 33 public God() { 34 System.out.println("God constructor"); 35 } 36 }
輸出結果:
1 TestStaticLoad static 2 God static 3 person static 4 person TestStaticLoad 5 TestStaticLoad constructor 6 person God 7 God constructorView Code
一步一步地解析:
- 在TestStaticLoad 的main方法中,執行了new God(),那就就會去加載God類,在這之前會先加載它的父類:TestStaticLoad
- 第一步:加載父類靜態,執行System.out.println("TestStaticLoad static"); 輸出:TestStaticLoad static,
- 第二步:加載子類靜態,執行System.out.println("God static");,輸出God static
- 第三步:加載父類非靜態,Person person = new Person("TestStaticLoad");,這裏實例化了Person 對象,那就會去加載Person類。
- 第四步:加載Person類,首先看有沒有父類,沒有。好,加載靜態塊,執行System.out.println("person static");輸出person static
- 第五步:Pernson類靜態塊加載完畢,加載構造器,new一個Person對象,輸出person TestStaticLoad。這時TestStaticLoad 類非靜態塊加載完畢
- 第六步:加載God 父類(TestStaticLoad )構造器,輸出TestStaticLoad constructor
- 第七步:God父類全部加載完畢,加載God的非靜態塊,Person person = new Person("God");這時又會去加載Person類,需要註意的是,static塊只加載一次,因為之前在父類已經加載過了,這時只加載構造器,輸出person God
- 最後一步:加載本類God 的構造器,輸出God constructor。
static關鍵字的總結:
- static關鍵字 可以再沒有創建對象的時候進行調用類的元素
- static 可以修飾類的方法 以及類的變量, 以及靜態代碼塊
- 被static修飾的成為靜態方法,靜態方法是沒有this的,靜態方法不能訪問同一個類中的非靜態方法和靜態變量,但是非靜態方法 可以可以訪問靜態變量
- 類的構造器 也是靜態的
- 靜態變量被所有的內存所有的對象共享,在內存中只有一個副本。非靜態變量是是在創建對象的時候初始化的,存在多個副本,每個副本不受影響。
- static 靜態代碼塊,static 代碼塊可以放在類中的任何地方,類加載的時候會按照static代碼塊的順序來加載代碼塊,並且只會執行一次。
- 枚舉類和靜態代碼塊 賦值靜態代碼塊的變量
- 非靜態方法能夠通過this訪問靜態變量
- 靜態成員變量雖然獨立於對象,但是不代表不可以通過對象去訪問,所有的靜態方法和靜態變量都可以通過對象訪問。
- static不可以修飾局部變量(java語法規定)
沒想到static能有這麽多需要註意的,可以說Java中的語法還是有很多可以深究的.
final 關鍵字
概述:
final關鍵字,在平時的過程中也是很常見的,在這裏進行一下深入的學習,加深對final關鍵字的理解。
使用註意點:
1.在java中final可以用來修飾類、方法、和變量(包括成員變量和局部變量)
2.final修飾類的時候,這個類將永遠不會被繼承,類中的成員方法也會被隱式的修飾為final(盡量不要用final修飾類)
3.如果不想方法被繼承,可以用final修飾,private也會隱式的將方法指定為final
4.final修飾變量的時候,如果是基本類型的變量,那麽他的值在初始化之後就不能更改
5.final在修飾對象的時候,在其初始化之後就不能指向其他對象
6.被static和final修飾的變量,將會占據一段不能改變的存儲空間,將會被看做編譯期常量
7.不可變的是變量的引用而非引用指向對象的內容。
幾個例子:
1.final變量和普通變量的區別
public class TestFinal { public static void main(String args[]){ String a = "test1"; final String b = "test"; String d = "test"; String c = b + 1; String e = d + 1; System.out.println((a == c)); System.out.println((a.equals(e))); } }
true trueView Code
因為final變量是基本類型以及String時,在編譯期的時候就把它當做常量來使用,不需要在運行時候使用。“==”是對比兩個對象基於內存引用,如果兩個對象的引用完全相同,則返回true,所以這裏b是用訪問常量的方式去訪問,d是鏈接的方式,所以a的內存引用和c的內存引用是相等的,所以結果為true,a和e兩個對象的值是相等的,所以結果為true
2.final在修飾對象的時候
1 public class TestFinal { 2 public static void main(String args[]){ 3 final TestFinal obj1 = new TestFinal(); 4 final TestFinal obj2 = new TestFinal(); 5 6 obj1 = obj2; 7 } 8 }
在編譯的時候,或報錯, 不能指向一個final對象。
volatile關鍵字
緩存一致性:
首先來看看線程的內存模型圖:
當執行代碼:
i = i + 1;
- 首先從主存中讀取i的值,
- 然後復制I到Cache中,
- CPU執行指令對i進行加1
- 將加1後的值寫入到Cache中
- 最後將Cache中i的值刷新到主存中
這個在單線程的環境中是沒有問題的,但是運行到多線程中就存在問題了。
問題出在主存中的變量,因為有可能其他線程讀的值,線程的Cache還沒有同步到主存中,每個線程中的Cahe中的值副本不一樣,可能會造成"臟讀"。
緩存一致性協議解決了這樣的問題,它規定每個線程中的Cache使用的共享變量副本是一樣的。
核心內容是當CPU寫數據時,如果發現操作的變量式共享變量,它將通知其他CPU該變量的緩存行為無效,
所以當其他CPU需要讀取這個變量的時候,發現自己的緩存行為無效,那麽就會從主存中重新獲取。
三個概念
Jvm定義了內存規範,試圖做到各個平臺對內存訪問的差異,但是依舊會發生緩存一致性的問題。
首先了解三個概念,原子性,可見性,有序性。
原子性:指某個操作,一個或者多個,要麽全部執行並且執行的過程中不會被任何因素打斷,要麽都不執行。
在JVM中,只有簡單的讀取、賦值(而且必須是將數字賦值給某個變量,變量之間的相互賦值不是原子操作)才是原子操作。看一個例子:
x = 70; //語句1
y = x; //語句2
y++; //語句3
y = x + 1; //語句4
上面四個語句中,只有語句1是原子性,其他都不是。
可見性:當多個線程訪問一個變量時,一個線程修改了這個變量的值,其他線程能夠看得到。
未加volatile變量修飾的變量,在被修改之後,什麽時候寫入到主存是不確定的,因此其他線程讀取該變量的值可能還是未被修改的值。
如果改變了被volatile關鍵字修飾了,那麽JVM將會標記它為共享變量,共享變量一經修改,就會立即同步到主存中,並且通知其他線程(CPU緩存)中值生效,請去主存中讀取該值。
有序性:程序的執行順序按照代碼的先後順序執行。但是JVM在執行語句的過程會對代碼進行重排序(重排序:CPU為了提高程序運行效率,可能會對輸入代碼進行優化,但是不保證程序的執行先後順序和代碼中的順序一致,但是會保證程序最終執行結果和代碼順序執行的結果是一致的)。
在多線程的環境下,原有的順序執行會發生錯誤。
在JVM中保證了一定的有序性,比如被volatile修飾後的變量,那麽該變量的寫操作先行發生於後面對這個變量的讀操作。
所以要想程序在多線程環境下正確運行,必須保證原子性,可見性,有序性。
volatile的作用
當一個變量(類的普通變量,靜態變量)被volatile修飾之後,那麽將具備兩個屬性:
1)保證了不同線程對這個變量進行操作時的可見性,即一個線程修改了某個變量的值,這新值對其他線程來說是立即可見的。
2)禁止進行指令重排序
下面來看看線程池中一些變量的定義:
private volatile ThreadFactory threadFactory;
private volatile RejectedExecutionHandler handler;
private volatile long keepAliveTime;
private volatile boolean allowCoreThreadTimeOut;
private volatile int corePoolSize;
private volatile int maximumPoolSize;
可以看到線程工廠threadFactory,拒絕策略handler,沒有任務時的活躍時間keepAliveTime,keepAliveTime的開關allowCoreThreadTimeOut,核心池大小corePoolSize,最大線程數maximumPoolSize
都是被volatile修飾中,因為在線程池中有若幹個線程,這些變量必需保持對線程可見性,不然會引起線程池運行不正確。
volatile不能保證原子性
i++;
它是非原子性的,當變量i被volatile修飾時,是否能保證原子性呢?
做個試驗:
public class TestAtomVolatile {
public volatile int i = 0;
public void increase() {
i++;
}
public static void main(String[] args) throws InterruptedException {
final TestAtomVolatile test = new TestAtomVolatile();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
System.out.println(test.i);
};
}.start();
}
}
}
以上代碼就是10個線程,分別對變量i進行自增操作,預期結果應該是10000,但是總會存在著小於10000的情況。輸出結果如下:
對於這種情況,可以使用鎖,synchronize,Lock,也可以使用原子變量。
原子變量的例子:
View Code
volatile的原理
下面這段話摘自《深入理解Java虛擬機》:
“”觀察加入volatile關鍵字和沒有加入volatile關鍵字時所生成的匯編代碼發現,加入volatile關鍵字時,會多出一個lock前綴指令”
lock前綴指令實際上相當於一個內存屏障(也成內存柵欄),內存屏障會提供3個功能:
1)它確保指令重排序時不會把其後面的指令排到內存屏障之前的位置,也不會把前面的指令排到內存屏障的後面;即在執行到內存屏障這句指令時,在它前面的操作已經全部完成;
2)它會強制將對緩存的修改操作立即寫入主存;
3)如果是寫操作,它會導致其他CPU中對應的緩存行無效。
assert關鍵字
assert斷言
在目前的java編碼中,是不推薦使用的,這裏只是稍微了解一下:
使用方式:
1、assert <boolean表達式> 如果<boolean表達式>為true,則程序繼續執行。 如果為false,則程序拋出AssertionError,並終止執行。 2、assert <boolean表達式> : <錯誤信息表達式> 如果<boolean表達式>為true,則程序繼續執行。 如果為false,則程序拋出java.lang.AssertionError,並輸入<錯誤信息表達式>。 如果要開啟斷言檢查,則需要用開關-enableassertions或-ea來開啟,java中IDE工具默認支持開啟-ea 下面是一個例子:public class LearnAssert { public static void main(String args[]){ assert true; System.out.println("斷言1成功執行"); System.out.println("-----------"); assert false:"error"; System.out.println("斷言2成功執行"); } }
assert是為了在調試程序時候使用的,默認不推薦使用,測試程序可以使用junit。
synchronized關鍵字
關於鎖關鍵字,有以下幾個總結:
- 無論synchronized關鍵字加在方法上還是對象上,如果它作用的對象是非靜態的,則它取得的鎖是對象;如果synchronized作用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的對象同一把鎖。
- 每個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以運行它所控制的那段代碼。
- 實現同步是要很大的系統開銷作為代價的,甚至可能造成死鎖,所以盡量避免無謂的同步控制。
下面介紹一個鎖的實例:
public class ManyThread { int count = 0; public synchronized void autoIncrement() { count++; } public static void main(String args[]) { ManyThread manyThread = new ManyThread(); Runnable runnable = new MyRunnable2(manyThread); new Thread(runnable, "a").start(); new Thread(runnable, "b").start(); new Thread(runnable, "c").start(); new Thread(runnable, "d").start(); } } class MyRunnable2 implements Runnable { private ManyThread manyThread; public MyRunnable2(ManyThread manyThread) { this.manyThread = manyThread; } @Override public void run() { for (int i = 0; i < 10000; i++) { manyThread.autoIncrement(); System.out.println(Thread.currentThread().getName() + " 執行中 " + "count:" + manyThread.count); } } }
用synchronized修飾後的autoIncrement()方法,會被加鎖,確保它每次執行的時候都能保證只有一個線程在運行。
transient關鍵字
Java中,一個類想要序列化,可以通過實現Serilizable接口的方式來實現,實現該接口之後,該類所有屬性和方法都會自動序列化。
但是如果屬性或方法被transient修飾,那麽將不會被序列化。
幹貨——詳解Java中的關鍵字