Java 開發當中一些經典問題,學會了升級你的能力,聯繫收藏

九種基本類型及封裝類

基本類型booleanbytecharshortintlongdoublevoid二進制位數18(一字節)16(2字節)16(2字節)32(4字節)64(8字節)64(8字節)--封裝器類BooleanByteCharacterShortIntegerLongDoubleVoid

switch語句後的控制表達式只能是short、char、int、long整數類型和枚舉類型,不能是float,double和boolean類型。String類型是java7開始支持。

位運算符

  • 左移(<
  • 右移(>>):int是32位,最高位是符號位,0代表正數,1代表負數,負數以補碼的形式存儲在計算機中。右移規則:最高位是

什麼(0或者1),右移的時候左邊就補什麼。即正數右移用0補位左邊,負數右移用1補位左邊。

無符號右移(>>>):不管是負數還是正數,右移總是左邊補0。

與運算(&)

或運算(|)

非運算(~)

異或運算(^):位相同為0,相異為1

-5右移3位後結果為-1,-1的二進制為:

1111 1111 1111 1111 1111 1111 1111 1111 // (用1進行補位)

-5無符號右移3位後的結果 536870911 換算成二進制:

0001 1111 1111 1111 1111 1111 1111 1111 // (用0進行補位)

應用:不用臨時變量交換兩個數

void swap(int argc, char *argv[])

{

a = a ^ b;

b = b ^ a;

a = a ^ b;

}

for循環,ForEach,迭代器效率

直接for循環效率最高,其次是迭代器和 ForEach操作。

其實 ForEach 編譯成字節碼之後,使用的是迭代器實現的。

synchronized和volatile

volatile僅能使用在變量級別;synchronized則可以使用在變量、方法、和類級別的。

volatile保證了變量的可見性,synchronized保證了原子性和可見性。

volatile

原理:首先我們要先意識到有這樣的現象,編譯器為了加快程序運行的速度,對一些變量的寫操作會先在寄存器或者是CPU緩存上進行,最後才寫入內存。而在這個過程,變量的新值對其他線程是不可見的,而volatile的作用就是使它修飾的變量的讀寫操作都必須在內存中進行。volatile告訴JVM, 它所修飾的變量不保留拷貝,直接訪問主內存中的。

volatile與synchronized

volatile本質是在告訴JVM當前變量在寄存器中的值是不確定的,需要從主存中讀取,synchronized則是鎖定當前變量,只有當前線程可以訪問該變量,其他線程被阻塞住。

volatile僅能使用在變量級別,synchronized則可以使用在變量,方法.

volatile僅能實現變量的修改可見性,但不具備原子特性,而synchronized則可以保證變量的修改可見性和原子性。

volatile不會造成線程的阻塞,而synchronized可能會造成線程的阻塞。

volatile標記的變量不會被編譯器優化,而synchronized標記的變量可以被編譯器優化。

volatile不能保證原子性原因:線程A修改了變量還沒結束時,另外的線程B可以看到已修改的值,而且可以修改這個變量,而不用等待A釋放鎖,因為Volatile 變量沒上鎖。

注意

聲明為volatile的簡單變量如果當前值由該變量以前的值相關,那麼volatile關鍵字不起作用。

也就是說如下的表達式都不是原子操作:

n = n + 1 ;

n ++ ;

只有當變量的值和自身上一個值無關時對該變量的操作才是原子級別的,如n = m + 1。

Java內存模型的抽象(volatile)

在java中,所有實例域、靜態域和數組元素存儲在堆內存中,堆內存在線程之間共享(本文使用“共享變量”這個術語代指實例域,靜態域和數組元素)。局部變量,方法定義參數和異常處理器參數不會在線程之間共享,在棧內存中,不需要同步處理,因為棧內存是線程獨享的,它們不會有內存可見性問題,也不受內存模型的影響。

Java線程之間的通信由Java內存模型(本文簡稱為JMM)控制,JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本(寄存器或CPU緩存)本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其他的硬件和編譯器優化。Java內存模型的抽象示意圖如下:

java內存模型

從上圖來看,線程A與線程B之間如要通信的話,必須要經歷下面2個步驟:

首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。

然後,線程B到主內存中去讀取線程A之前已更新過的共享變量。

下面通過示意圖來說明這兩個步驟:

如上圖所示,本地內存A和B有主內存中共享變量x的副本。假設初始時,這三個內存中的x值都為0。線程A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地內存A中。當線程A和線程B需要通信時,線程A首先會把自己本地內存中修改後的x值刷新到主內存中,此時主內存中的x值變為了1。隨後,線程B到主內存中去讀取線程A更新後的x值,此時線程B的本地內存的x值也變為了。

從整體來看,這兩個步驟實質上是線程A在向線程B發送消息,而且這個通信過程必須要經過主內存。JMM通過控制主內存與每個線程的本地內存之間的交互,來為java程序員提供內存可見性保證。

equals與==的區別

==常用於比較原生類型,而equals()方法用於檢查對象的相等性。另一個不同的點是:如果==和equals()用於比較對象,當兩個引用地址相同,== 返回true。而equals()可以返回true或者false主要取決於重寫實現。最常見的一個例子,字符串的比較,不同情況 == 和equals()返回不同的結果。

看Object源碼:

public boolean equals(Object obj) {

return (this == obj);

}

==表示的是比較兩個對象實例的內存地址是否相同。如果不重寫equal(),就和==等效,

相等(相同)的對象必須具有相等的哈希碼(或者散列碼)。

如果兩個對象的hashCode相同,它們並不一定相同。

術語來講的區別:

==是判斷兩個變量或實例是不是指向同一個內存空間。

equals是判斷兩個變量或實例所指向的內存空間的值是不是相同。

==指引用是否相同

equals()指的是值是否相同

hashCode作用

以java.lang.Object來理解JVM每new一個Object,它都會將這個Object丟到一個Hash哈希表中去,這樣的話,下次做Object的比較或者取這個對象的時候,它會根據對象的hashcode再從Hash表中取這個對象。這樣做的目的是提高取對象的效率。

具體過程是這樣:

new Object(),JVM根據這個對象的Hashcode值放入到對應的Hash表對應的Key上,如果不同的對象卻產生了相同的hash值,也就是發生了Hash key相同導致衝突的情況,那麼就在這個Hash key的地方產生一個鏈表,將所有產生相同hashcode的對象放到這個單鏈表上串在一起。

比較兩個對象的時候,首先根據他們的hashcode去hash表中找他的對象,當兩個對象的hashcode相同,那麼就是說他們這兩個對象放在Hash表中的同一個key上,那麼他們一定在這個key上的鏈表上。那麼此時就只能根據Object的equal方法來比較這個對象是否equal。當兩個對象的hashcode不同的話,肯定他們不能equal。

java.lang.Object中對hashCode的約定:

在一個應用程序執行期間,如果一個對象的equals方法做比較所用到的信息沒有被修改的話,則對該對象調用hashCode方法多次,它必須始終如一地返回同一個整數。

如果兩個對象根據equals(Object o)方法是相等的,則調用這兩個對象中任一對象的hashCode方法必須產生相同的整數結果。

如果兩個對象根據equals(Object o)方法是不相等的,則調用這兩個對象中任一個對象的hashCode方法,不要求產生不同的整數結果。但如果能不同,則可能提高散列表的性能。

Object的公用方法

clone 保護方法,只有實現了Cloneable接口才可以調用,否則拋異常

getClass final方法,獲得運行時類型

toString

equals

hashCode

wait 就是使當前線程等待該對象的鎖,當前線程必須是該對象的擁有者,也就是具有該對象的鎖。wait方法一直等待,直到獲得鎖或者被中斷。wait設定一個超時間隔,如果在規定時間內沒有獲得鎖就返回。

調用該方法後當前線程進入睡眠狀態,直到以下事件發生。

其他線程調用了該對象的notify方法。

其他線程調用了該對象的notifyAll方法。

其他線程調用了interrupt中斷該線程。

時間間隔到了。

此時該線程就可以被調度了,如果是被中斷的話就拋出一個InterruptedException異常。

notify

notifyAll

Java四種引用 --- 這裡指的是“引用“,不是對象

強引用

平常我們使用對象的方式Object object = new Object();如果一個對象具有強引用,它就不會被垃圾回收器回收。即使當前內存空間不足,JVM也不會回收它,而是拋出 OutOfMemoryError 錯誤,使程序異常終止。例如下面的代碼:

public class Main {

public static void main(String[] args) {

new Main().fun1();

}

public void fun1() {

Object object = new Object();

Object[] objArr = new Object[1000];

}

}

當運行至Object[] objArr = new Object[1000];這句時,如果內存不足,JVM會拋出OOM錯誤也不會回收object指向的對象。不過要注意的是,當fun1運行完之後,object和objArr都已經不存在了,所以它們指向的對象都會被JVM回收。

但如果想中斷強引用和某個對象之間的關聯,可以顯式地將引用賦值為null,這樣一來的話,JVM在合適的時間就會回收該對象。

軟引用

軟引用通過SoftReference創建,在使用軟引用時,如果內存的空間足夠,軟引用就能繼續被使用,而不會被垃圾回收器回收,只有在內存不足時,軟引用才會被垃圾回收器回收。

軟引用的這種特性使得它很適合用來解決 OOM 問題,實現緩存機制,例如:圖片緩存、網頁緩存等等……

軟引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果軟引用所引用的對象被JVM回收,這個軟引用就會被加入到與之關聯的引用隊列中。

弱引用

事實上軟引用和弱引用非常類似,兩者的區別在於:只具有弱引用的對象擁有的生命週期更短暫。因為當 JVM 進行垃圾回收,一旦發現弱引用對象,無論當前內存空間是否充足,都會將弱引用回收。不過由於垃圾回收器是一個優先級較低的線程,所以並不一定能迅速發現弱引用對象。

弱引用可以和一個引用隊列(ReferenceQueue)聯合使用,如果弱引用所引用的對象被JVM回收,這個弱引用就會被加入到與之關聯的引用隊列中。

虛引用

虛引用”顧名思義,就是形同虛設,與其他幾種引用都不同,虛引用並不會影響對象的生命週期。如果一個對象僅持有虛引用,那麼它相當於沒有引用,在任何時候都可能被垃圾回收器回收。

強引用:不管什麼時候都不會被回收。

軟引用:當內存不足的時候,JVM垃圾回收會回收。

弱引用:不管內存足不足,只要發生JVM垃圾回收就會回收。

虛引用:隨時都可能會被回收。

小結

引用和引用隊列提供了一種通知機制,允許我們知道對象已經被銷燬或者即將被銷燬。GC要回收一個對象的時候,如果發現該對象有軟、弱、虛引用的時候,會將這些引用加入到註冊的引用隊列中。軟引用和弱引用差別不大,JVM都是先把SoftReference和WeakReference中的referent字段值設置成null,之後加入到引用隊列;而虛引用則不同,如果某個堆中的對象,只有虛引用,那麼JVM會將PhantomReference加入到引用隊列中,JVM不會自動將referent字段值設置成null。

實際應用:利用軟引用和弱引用緩存解決OOM問題。

如:Bitmap的緩存

設計思路是:用一個HashMap來保存圖片的路徑和相應圖片對象(Bitmap)的軟引用之間的映射關係,在內存不足時,JVM會自動回收這些緩存圖片對象所佔用的空間,從而有效地避免了OOM的問題。在Android開發中對於大量圖片下載會經常用到。

wait()、notify()和sleep()

wait()和notify()

wait()和notify()是直接隸屬於Object類,在JAVA中的Object類型中,都是帶有一個內存鎖的,在有線程獲取該內存鎖後,其它線程無法訪問該內存,從而實現JAVA中簡單的同步、互斥操作。明白這個原理,就能理解為什麼synchronized(this)與synchronized(static XXX)的區別了,synchronized就是針對內存區塊申請內存鎖,this關鍵字代表類的一個對象,所以其內存鎖是針對相同對象的互斥操作,而static成員屬於類專有,其內存空間為該類所有成員共有,這就導致synchronized()對static成員加鎖,相當於對類加鎖,也就是在該類的所有成員間實現互斥,在同一時間只有一個線程可訪問該類的實例。wait只能由持有對像鎖的線程來調用。

Obj.wait()與Obj.notify()必須要與synchronized(Obj)一起使用,從功能上來說wait就是說線程在獲取對象鎖後,主動釋放對象鎖,同時本線程休眠。直到有其它線程調用對象的notify()喚醒該線程,才能繼續獲取對象鎖,並繼續執行。相應的notify()就是對對象鎖的喚醒操作。但有一點需要注意的是notify()調用後,並不是馬上就釋放對象鎖的,而是在相應的synchronized(){}語句塊執行結束,自動釋放鎖後,JVM會在wait()對象鎖的線程中隨機選取一線程,賦予其對象鎖,喚醒線程,繼續執行。這樣就提供了在線程間同步、喚醒的操作。Thread.sleep()與Object.wait()二者都可以暫停當前線程,釋放CPU控制權,主要的區別在於Object.wait()在釋放CPU同時,釋放了對象鎖的控制。

wait():促使當前線程等待直到另外一個線程調用這個對象的notify()方法喚醒。和synchronized塊使用的時候,synchronized獲取對象的鎖以後,可以通過wait()方法釋放,同時阻塞當前線程,停止執行(這也是和sleep的區別)。

public static void firstMethod(){

synchronized (a){

System.out.println(Thread.currentThread().getName() + " firstMethod--死鎖");

try {

// Thread.sleep(10);

a.wait();

} catch (InterruptedException e) {

e.printStackTrace();

}

synchronized (b){

System.out.println(Thread.currentThread().getName() + " firstMethod--解鎖");

}

}

}

public static void seconedMethod(){

synchronized (b){

System.out.println(Thread.currentThread().getName() + " seconedMethod--死鎖");

synchronized (a){

System.out.println(Thread.currentThread().getName() + " seconedMethod--解鎖");

a.notify();

}

}

}

如果用兩個線程分別執行這兩個方法

public static void main(String[] args) {

Runnable runnable1 = new Runnable() {

@Override

public void run() {

firstMethod();

}

};

Runnable runnable2 = new Runnable() {

@Override

public void run() {

seconedMethod();

}

};

Thread thread1 = new Thread(runnable1);

Thread thread2 = new Thread(runnable2);

thread1.start();

thread2.start();

}

如果是用sleep方法替換掉wait方法,就是一個死鎖,線程thread1先執行拿到a對象的鎖,然後阻塞10ms(並沒有釋放鎖),thread2然後拿到對象b的鎖,這時候seconedMethod需要a對象的鎖,但是firstMethod並沒有釋放,然後10ms過後,firstMethod需要b的鎖,然後b的鎖也沒有在seconedMethod方法中釋放,兩個線程相互等待對方釋放鎖,就形成了死鎖。

運行結果:

Thread-0 firstMethod--死鎖

Thread-1 seconedMethod--死鎖

如果不使用sleep而是使用wait方法,就不會發生死鎖。因為wait釋放了firstMethod中的a對象的鎖,當seconedMethod需要a對象鎖的時候就可以用了。

運行結果:

Thread-0 firstMethod--死鎖

Thread-1 seconedMethod--死鎖

Thread-1 seconedMethod--解鎖

Thread-0 firstMethod--解鎖

notify():喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程(隨機)。直到當前的線程放棄此對象上的鎖,才能繼續執行被喚醒的線程。

sleep()

通過Thread.sleep()使當前線程暫停執行一段時間,讓其他線程有機會繼續執行,但它並不釋放對象鎖。也就是說如果有synchronized同步塊,其他線程仍然不能訪問共享數據。注意該方法要捕捉異常。

public class ThreadLock {

Object lock = new Object();

int num = 0;

public static void main(String[] args) {

ThreadLock test = new ThreadLock();

Runnable runnable = new Runnable() {

@Override

public void run() {

test.method2();

}

};

Thread thread1 = new Thread(runnable);

thread1.start();

test.method1();

}

public void method1(){

synchronized (lock){

try {

Thread.sleep(1000);

// lock.wait(1000);

num += 100;

} catch (InterruptedException e) {

e.printStackTrace();

}

}

}

public void method2(){

synchronized (lock){

num += 9;

System.out.println(num);

}

}

}

因為在main線程調用方法,因此先執行主線程的method1,對象鎖被主線程拿走了,那麼子線程執行method2的時候就需要等待1秒後把鎖還回來。

1秒後輸出結果:

109

如果替換成lock.wait(1000);

lock.wait(1000)會讓當前線程(main線程)睡眠1秒,同時釋放synchronized的對象鎖,因此小於1秒輸出9

synchronized和lock

幾個概念

共享變量(shared variable):多個線程都能訪問的變量。

變量可見性(variable visibility):一個線程更新了共享變量,對其他線程立刻可見。

互斥(mutual exclusion ):幾個線程中的任何一個不能與其他一個或多個同時操作一個變量。

臨界區(critical section):訪問共享資源的一段代碼塊。

synchronized

保證共享變量的可見性:變量緩存與編譯器指令優化會導致變量修改的不可見性。

保證共享變量的互斥性:同一時刻只能有一個線程對共享變量的修改(注意修改一次,是先讀再寫,是兩個操作)。

特點:

當程序運行到非靜態的synchronized同步方法上時,自動獲得與正在執行代碼類的當前實例(this實例)有關的鎖。

如果兩個線程要執行一個類中的synchronized方法,並且兩個線程使用相同的實例來調用方法,那麼一次只能有一個線程能夠執行方法,另一個需要等待,直到鎖被釋放。也就是說:如果一個線程在對象上獲得一個鎖,就沒有任何其他線程可以進入(該對象的)類中的任何一個同步方法。

線程可以獲得多個鎖。比如,在一個對象的同步方法裡面調用另外一個對象的同步方法,則獲取了兩個對象的同步鎖。

線程同步方法是通過鎖來實現,每個對象都有且僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其他訪問該對象的線程就無法再訪問該對象的其他同步方法。

對於同步,要時刻清醒在哪個對象上同步,這是關鍵。

死鎖是線程間相互等待鎖造成的。

lock

lock提供瞭如下的方法:

void lock(),獲取一個鎖,如果鎖當前被其他線程獲得,當前的線程將被休眠。

boolean tryLock(),嘗試獲取一個鎖,如果當前鎖被其他線程持有,則返回false,不會使當前線程休眠。

boolean tryLock(long timeout,TimeUnit unit),如果獲取了鎖定立即返回true,如果別的線程正持有鎖,會等待參數給定的時間,在等待的過程中,如果獲取了鎖定,就返回true,如果等待超時,返回false。

void lockInterruptibly(),如果獲取了鎖定立即返回,如果沒有獲取鎖定,當前線程處於休眠狀態,直到或者鎖定,或者當前線程被別的線程中斷。

synchronized和lock區別

synchronized是在JVM層面上實現的,如果代碼執行時出現異常,JVM會自動釋放monitor鎖。而lock代碼是用戶寫的,需要用戶來保證最終釋放掉鎖。

lock提供了一個重要的方法newConditon(),ConditionObject有await()、signal()、signalAll(),類似於Ojbect類的wait()、notify()、notifyAll()。這些方法都是用來實現線程間通信。lock將synchronized的互斥性和線程間通信分離開來,一個lock可以有多個condition。另外lock的signal可以實現公平的通知,而notify是隨機從鎖等待隊列中喚醒某個進程。

性能上來說,在多線程競爭比較激烈地情況下,lock比synchronize高效得多。

public class ThreadLock {

public static void main(String[] args) {

Test test = new Test();

Lock lock = new ReentrantLock();

Runnable runnable = new Runnable() {

@Override

public void run() {

lock.lock();

for (int i = 0; i < 50 ; i++) {

test.setX(1);

System.out.println(Thread.currentThread().getName() + " : " +test.getX());

}

lock.unlock();

}

};

Thread thread1 = new Thread(runnable);

Thread thread2 = new Thread(runnable);

thread1.start();

thread2.start();

}

static class Test{

private int x = 100;

public int getX(){

return x;

}

public void setX(int y){

x = x - y;

}

}

}

ReentrantLock與synchronized的比較

ReentrantLocak(可重入鎖)

簡單來說,它有一個與鎖相關的獲取計數器,如果擁有鎖的某個線程再次得到鎖,那麼獲取計數器就加1,然後鎖需要被釋放兩次才能獲得真正釋放。

ReentrantLock提供了synchronized類似的功能和內存語義。

不同

ReentrantLock功能性方面更全面,比如時間鎖等候,可中斷鎖等候,鎖投票等,因此更有擴展性。在多個條件變量和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了Condition,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition實例,所以更有擴展性。

ReentrantLock的性能比synchronized會好點。

ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續處理,取得不成功,可以等下次運行的時候處理,所以不容易產生死鎖,而synchronized則一旦進入鎖請求要麼成功,要麼一直阻塞,所以更容易產生死鎖。

缺點

lock 必須在 finally 塊中釋放。否則,如果受保護的代碼將拋出異常,鎖就有可能永遠得不到釋放!這一點區別看起來可能沒什麼,但是實際上,它極為重要。忘記在 finally 塊中釋放鎖,可能會在程序中留下一個定時炸彈,當有一天炸彈爆炸時,您要花費很大力氣才能找到源頭在哪。而使用同步,JVM 將確保鎖會獲得自動釋放。

當 JVM 用 synchronized 管理鎖定請求和釋放時,JVM 在生成線程轉儲時能夠包括鎖定信息。這些對調試非常有價值,因為它們能標識死鎖或者其他異常行為的來源。 Lock 類只是普通的類,JVM 不知道具體哪個線程擁有 Lock 對象。

ArrayList,LinkedList和Vector

ArrayList和Vector都是基於數組實現的,所以查詢效率高,插入效率低。

LinkedList基於雙向鏈表實現的,所以插入效率高,查詢效率低。

Vector使用了synchronized方法,所以線程安全,性能比ArrayList低。

LinkedList實現了List接口,還提供了額外的get,remove,insert方法在LinkedList的首部或尾部,這些操作使LinkedList可被用作棧(Stack),隊列(Queue)或雙向隊列(deque)。

ArrayList和LinkedList允許null元素,重複元素。

HashMap和HashTable

都實現了Map接口

HashMap允許key為null,value為null而HashTable不允許,如果新加入的key和之前重複了,會覆蓋之前的value。

HashTable線程安全,而HashMap不是線程安全。因此單線程下HashTable比HashMap慢。

HashMap不能保證隨著時間推移Map中的元素次序是不變的。

Hashtable中hash數組默認大小是11,增加的方式是 old*2+1。HashMap中hash數組的默認大小是16,而且一定是2的指數。

ConcurrentHashMap

鎖分段技術

HashTable容器在競爭激烈的併發環境下表現出效率低下的原因是所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裡不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。

HashSet

實現了Set接口,HashSet< T >本質就是一個HashMap,把HashMap的key作為HashSet的值,HashMap的value是一個固定的Object對象,因為HashMap的key是不允許重複的,所以HashSet裡的元素也是不能重複的,也可以看出HashSet的查詢效率很高。

String,StringBuilder和StringBuffer

CharSequence接口:一個字符序列。String,StringBuilder 和 StringBuffer都實現了它。

String類:是常量,不可變.

StringBuilder類:只可以在單線程的情況下進行修改(線程不安全),字符串拼接用,除了StringBuffer可用場景外。

StringBuffer類:可以在多線程的情況下進行改變(線程安全),比如:在http請求中拼接。

StringBuilder比StringBuffer效率高,應該儘量使用StringBuilder。

Excption與Error包結構

結構圖:

結構圖

Throwable

Throwable是 Java 語言中所有錯誤或異常的超類。

Throwable包含兩個子類: Error 和 Exception 。它們通常用於指示發生了異常情況。

Throwable包含了其線程創建時線程執行堆棧的快照,它提供了printStackTrace()等接口用於獲取堆棧跟蹤數據等信息。

Exception

Exception及其子類是 Throwable 的一種形式,它指出了合理的應用程序想要捕獲的條件。

RuntimeException

RuntimeException是那些可能在 Java 虛擬機正常運行期間拋出的異常的超類。

編譯器不會檢查RuntimeException異常。 例如,除數為零時,拋出ArithmeticException異常。RuntimeException是ArithmeticException的超類。當代碼發生除數為零的情況時,倘若既"沒有通過throws聲明拋出ArithmeticException異常",也"沒有通過try...catch...處理該異常",也能通過編譯。這就是我們所說的"編譯器不會檢查RuntimeException異常"!

如果代碼會產生RuntimeException異常,則需要通過修改代碼進行避免。 例如,若會發生除數為零的情況,則需要通過代碼避免該情況的發生!

Error

和Exception一樣, Error也是Throwable的子類。 它用於指示合理的應用程序不應該試圖捕獲的嚴重問題,大多數這樣的錯誤都是異常條件。

和RuntimeException一樣, 編譯器也不會檢查Error。

Interface與abstract類的區別

參數抽象類接口

默認的方法實現它可以有默認的方法實現接口完全是抽象的。它根本不存在方法的實現

實現子類使用extends關鍵字來繼承抽象類。如果子類不是抽象類的話,它需要提供抽象類中所有聲明的方法的實現。子類使用關鍵字implements來實現接口。它需要提供接口中所有聲明的方法的實現

構造器抽象類可以有構造器接口不能有構造器

與正常Java類的區別除了你不能實例化抽象類之外,它和普通Java類沒有任何區別接口是完全不同的類型

訪問修飾符抽象方法可以有public、protected和default這些修飾符接口方法默認修飾符是public。你不可以使用其它修飾符。

main方法抽象方法可以有main方法並且我們可以運行它接口沒有main方法,因此我們不能運行它。

多繼承抽象類可以繼承一個類和實現多個接口接口只可以繼承一個或多個其它接口

速度它比接口速度要快接口是稍微有點慢的,因為它需要時間去尋找在類中實現的方法。

添加新方法如果你往抽象類中添加新的方法,你可以給它提供默認的實現。因此你不需要改變你現在的代碼。如果你往接口中添加方法,那麼你必須改變實現該接口的類。

靜態內部類和非靜態內部類

相同點

內部類都可以用public,protected,private修飾。

方法中都可以調用外部類的靜態成員變量。

不同點

靜態內部類可以聲明靜態和非靜態成員變量,非靜態內部類只能聲明非靜態成員變量。

靜態內部類可以聲明靜態和非靜態方法,非靜態內部類只能聲明非靜態方法。

靜態內部類不能調用外部類的非靜態成員變量(靜態方法和非靜態方法都一樣),非靜態內部類都可以調用。

泛型擦除

一篇好博客

泛型在JDK5以後才有的,擦除是為了兼容之前沒有的使用泛型的類庫和代碼。如:ArrayList< String >和ArrayList< Integer > 在編譯器編譯的時候都變成了ArrayList。

List<integer> list = new ArrayList<integer>();/<integer>/<integer>

Map<integer> map = new HashMap<integer>();/<integer>/<integer>

System.out.println(Arrays.toString(list.getClass().getTypeParameters()));

System.out.println(Arrays.toString(map.getClass().getTypeParameters()));

/* Output

[E]

[K, V]

*/

我們期待的是得到泛型參數的類型,但是實際上我們只得到了一堆佔位符。

public class Main {

public T[] makeArray() {

// error: Type parameter 'T' cannot be instantiated directly

return new T[5];

}

}

我們無法在泛型內部創建一個T類型的數組,原因也和之前一樣,T僅僅是個佔位符,並沒有真實的類型信息,實際上,除了new表達式之外,instanceof操作和轉型(會收到警告)在泛型內部都是無法使用的,而造成這個的原因就是之前講過的編譯器對類型信息進行了擦除。

public class Main

{

private T t;

public void set(T t) {

this.t = t;

}

public T get() {

return t;

}

public static void main(String[] args) {

Main<string> m = new Main<string>();/<string>/<string>

m.set("findingsea");

String s = m.get();

System.out.println(s);

}

}

/* Output

findingsea

*/

雖然有類型擦除的存在,使得編譯器在泛型內部其實完全無法知道有關T的任何信息,但是編譯器可以保證重要的一點:內部一致性,也是我們放進去的是什麼類型的對象,取出來還是相同類型的對象,這一點讓Java的泛型起碼還是有用武之地的。

代碼片段展現就是編譯器確保了我們放在T上的類型的確是T(即便它並不知道有關T的任何類型信息)。這種確保其實做了兩步工作:

set()處的類型檢驗

get()處的類型轉換

這兩步工作也成為邊界動作。

public class Main {

public List fillList(T t, int size) {

List list = new ArrayList();

for (int i = 0; i < size; i++) {

list.add(t);

}

return list;

}

public static void main(String[] args) {

Main<string> m = new Main<string>();/<string>/<string>

List<string> list = m.fillList("findingsea", 5);/<string>

System.out.println(list.toString());

}

}

/* Output

[findingsea, findingsea, findingsea, findingsea, findingsea]

*/

代碼片段同樣展示的是泛型的內部一致性。

擦除的補償

如上看到的,但凡是涉及到確切類型信息的操作,在泛型內部都是無法共工作的。那是否有辦法繞過這個問題來編程,答案就是顯示地傳遞類型標籤。

public class Main {

public T create(Class type) {

try {

return type.newInstance();

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

public static void main(String[] args) {

Main<string> m = new Main<string>();/<string>/<string>

String s = m.create(String.class);

}

}

代碼片段展示了一種用類型標籤生成新對象的方法,但是這個辦法很脆弱,因為這種辦法要求對應的類型必須有默認構造函數,遇到Integer類型的時候就失敗了,而且這個錯誤還不能在編譯器捕獲。

public class Main {

public T[] create(Class type) {

return (T[]) Array.newInstance(type, 10);

}

public static void main(String[] args) {

Main<string> m = new Main<string>();/<string>/<string>

String[] strings = m.create(String.class);

}

}


分享到:


相關文章: