Java 筆試:常見題目總結

1. 下面這條語句一共創建了多少個對象:String s="a"+"b"+"c"+"d";

答案:題目中的第一行代碼被編譯器在編譯時優化後,相當於直接定義了一個”abcd”的字符串,所以,上面的代碼應該只創建了一個String對象。
附加:

對於如下代碼:
    String s1 = "a";
    String s2 = s1 + "b";
    String s3 = "a" + "b";
    System.out.println(s2 == "ab");
    System.out.println(s3 == "ab");
  第一條語句打印的結果爲false,
  第二條語句打印的結果爲true,
  這說明javac編譯可以對字符串常量直接相加的表達式進行優化,
  不必要等到運行期去進行加法運算處理,
  而是在編譯時去掉其中的加號,直接將其編譯成一個這些常量相連的結果。

寫如下兩行代碼,
    String s = "a" + "b" + "c" + "d";
    System.out.println(s == "abcd");   
    最終打印的結果應該爲true

2. try {}裏有一個return語句,那麼緊跟在這個try後的finally {}裏的code會不會被執行,什麼時候被執行,在return前還是後?

public class Test {

    public static void main(String[] args) {
        System.out.println("return:" + new Test().test());
    }

    public int test() {
        int x = 1;
        try {
            return x;
        } finally {
            ++x;
            System.out.println("finally:" + x);
        }
    }
}

執行結果:
finally:2
return:1

那麼下面的程序代碼輸出的結果是多少?
    public class  smallT  
    {  
        public static void  main(String args[])  
        {  
            smallT t  = new  smallT();  
            int  b  =  t.get();  
            System.out.println(b);  
        }  

        public int  get()  
        {  
            try  
            {  
                return 1 ;  
            }  
            finally  
            {  
                return 2 ;  
            }  
        }  
    } 
執行結果是:
2

分析:try中的return語句調用的函數先於finally中調用的函數執行,
也就是說return語句先執行,finally語句後執行,所以,返回的結果是2。
Return並不是讓函數馬上返回,而是return語句執行後,
將把返回結果放置進函數棧中,此時函數並不是馬上返回,
它要執行finally語句後才真正開始返回。

下面用一個程序來幫助分析:

    public  class Test {  
        public static void main(String[] args) {  
            System.out.println(new Test().test());;  
        }  

        int test()  
        {  
            try  {  
                return func1();  
            }  
            finally  {  
                return func2();  
            }  
        }  

        int func1()  
        {  
            System.out.println("func1");  
            return 1;  
        }  
        int func2()  
        {  
            System.out.println("func2");  
            return 2;  
        }     
    }  

執行結果:
func1
func2
2

結論:finally中的代碼比returnbreak語句後執行

3. final, finally, finalize的區別

  • final 用於聲明屬性,方法和類,分別表示屬性不可變,方法不可覆蓋,類不可繼承。內部類要訪問局部變量,局部變量必須定義成final類型
  • finally是異常處理語句結構的一部分,表示總是執行。
  • finalize是Object類的一個方法,在垃圾收集器執行的時候會調用被回收對象的此方法,可以覆蓋此方法提供垃圾收集時的其他資源回收,例如關閉文件等。JVM不保證此方法總被調用

4. Java中的異常處理機制的簡單原理和應用

Java對異常進行了分類,不同類型的異常分別用不同的Java類表示,所有異常的根類爲java.lang.Throwable,Throwable下面又派生了兩個子類:ErrorException

  • Error 表示應用程序本身無法克服和恢復的一種嚴重問題,程序只有死的份了,例如,說內存溢出和線程死鎖等系統問題。
  • Exception表示程序還能夠克服和恢復的問題,其中又分爲系統異常和普通異常,系統異常是軟件本身缺陷所導致的問題,也就是軟件開發人員考慮不周所導致的問題,軟件使用者無法克服和恢復這種問題,但在這種問題下還可以讓軟件系統繼續運行或者讓軟件死掉,例如:
    數組越界(ArrayIndexOutOfBoundsException)
    空指針異常(NullPointerException)
    類轉換異常(ClassCastException)
    找不到類(ClassNotFoundException)
    普通異常是運行環境的變化或異常所導致的問題,是用戶能夠克服的問題,例如:網絡斷線硬盤空間不夠,發生這樣的異常後,程序不應該死掉。

java爲系統異常和普通異常提供了不同的解決方案,編譯器強制

  • 系統異常可以處理也可以不處理,所以,編譯器不強制用try..catch處理或用throws聲明,所以系統異常也稱爲unchecked異常。
  • 普通異常必須try..catch處理或用throws聲明繼續拋給上層調用方法處理,所以普通異常也稱爲checked異常。

提示答題者,就按照三個級別去思考:
虛擬機必須宕機的錯誤
程序可以死掉也可以不死掉的錯誤
程序不應該死掉的錯誤


5. sleep() 和 wait() 有什麼區別?

  • sleep是線程類(Thread)的方法,導致此線程暫停執行指定時間,給執行機會給其他線程,但是監控狀態依然保持,到時後會自動恢復。調用sleep不會釋放對象鎖。
  • wait是Object類的方法,對此對象調用wait方法導致本線程放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象發出notify方法(或notifyAll)後本線程才進入對象鎖定池準備獲得對象鎖進入運行狀態。)
    下面用一個例子來說明:
public class Test {

    public static void main(String[] args) {
        new Thread(new Thread1()).start();
        try {
            Thread.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Thread2()).start();
    }

    private static class Thread1 implements Runnable {
        @Override
        public void run() {
            // 由於這裏的Thread1和下面的Thread2內部run方法要用同一對象作爲監視器,
            // 我們這裏不能用this,因爲在Thread2裏面的this和這個Thread1的this不是同一個對象。
            // 我們用Test.class這個字節碼對象,當前虛擬機裏引用這個變量時,指向的都是同一個對象。
            synchronized (Test.class) {
                System.out.println("enter thread1...");
                System.out.println("thread1 is waiting");
                try {
                    // 釋放鎖有兩種方式:
                    // 第一種方式是程序自然離開監視器的範圍,也就是離開了synchronized關鍵字管轄的代碼範圍,
                    // 另一種方式就是在synchronized關鍵字管轄的代碼內部調用監視器對象的wait方法。
                    // 這裏,使用wait方法釋放鎖。
                    Test.class.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread1 is going on...");
                System.out.println("thread1 is being over!");
            }
        }
    }

    private static class Thread2 implements Runnable {
        @Override
        public void run() {
            synchronized (Test.class) {
                System.out.println("enter thread2...");
                System.out.println("thread2 notify other thread can release wait status..");
                // 由於notify方法並不釋放鎖,
                // 即使thread2調用下面的sleep方法休息了10毫秒,
                // 但thread1仍然不會執行,因爲thread2沒有釋放鎖,所以Thread1無法得不到鎖。
                Test.class.notify();
                System.out.println("thread2 is sleeping ten millisecond...");
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("thread2 is going on...");
                System.out.println("thread2 is being over!");
            }
        }
    }
}

執行結果:
enter thread1...
thread1 is waiting
enter thread2...
thread2 notify other thread can release wait status..
thread2 is sleeping ten millisecond...
thread2 is going on...
thread2 is being over!
thread1 is going on...
thread1 is being over!
  • sleep就是正在執行的線程主動讓出cpu,cpu去執行其他線程,在sleep指定的時間過後,cpu纔會回到這個線程上繼續往下執行,如果當前線程進入了同步鎖,sleep方法並不會釋放鎖,即使當前線程使用sleep方法讓出了cpu,但其他被同步鎖擋住了的線程也無法得到執行
  • wait是指在一個已經進入了同步鎖的線程內,讓自己暫時讓出同步鎖,以便其他正在等待此鎖的線程可以得到同步鎖並運行,只有其他線程調用了notify()方法(notify並不釋放鎖,只是告訴調用過wait方法的線程可以去參與獲得鎖的競爭了,但不是馬上得到鎖,因爲鎖還在別人手裏,別人還沒釋放。如果notify方法後面的代碼還有很多,需要這些代碼執行完後纔會釋放鎖,可以在notfiy方法後增加一個等待和一些代碼,看看效果),調用wait方法的線程就會解除wait狀態和程序可以再次得到鎖後繼續向下運行。wait()必須在synchronized內部調用

6. 當一個線程進入一個對象的一個synchronized方法後,其它線程是否可進入此對象的其它方法?

分幾種情況:

  1. 其他方法前是否加了synchronized關鍵字,如果沒加,則能。
  2. 如果這個方法內部調用了wait,則可以進入其他synchronized方法。
  3. 如果其他個方法都加了synchronized關鍵字,並且內部沒有調用wait,則不能。
  4. 如果其他方法是static,它用的同步鎖是當前類的字節碼,與非靜態的方法不能同步,因爲非靜態的方法用的是this。

7. 簡述synchronized和java.util.concurrent.locks.Lock的異同?

  • 主要相同點:Lock能完成synchronized所實現的所有功能
  • 主要不同點:Lock有比synchronized更精確的線程語義和更好的性能。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,並且必須在finally從句中釋放。Lock還有更強大的功能,例如,它的tryLock方法可以非阻塞方式去拿鎖。
    例子:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test {

    private int j;
    // ReentrantLock 可重入鎖,
    // 一個線程可以對已被加鎖的ReentrantLock鎖再次加鎖
    private Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        Test tt = new Test();
        new Thread(tt.new Adder()).start();
        new Thread(tt.new SubTractor()).start();
    }

    private class SubTractor implements Runnable {
        @Override
        public void run() {
            while (true) {
//                synchronized (Test.this) {
//                    System.out.println("j--=" + j--);
//                    //這裏拋異常了,鎖能釋放嗎?
//                }
                lock.lock();
                try {
                    System.out.println("j-- = " + j--);
                } finally {
                    lock.unlock();
                }
            }
        }
    }

    private class Adder implements Runnable {
        @Override
        public void run() {
            while (true) {
//                synchronized (Test.this) {
//                    System.out.println("j++=" + j++);
//                }
                lock.lock();
                try {
                    System.out.println("j++ = " + j++);
                } finally {
                    lock.unlock();
                }
            }
        }
    }
}

執行結果:
···
j++ = 42218
j++ = 42219
j++ = 42220
j-- = 42221
j-- = 42220
j-- = 42219
···

8. 設計4個線程,其中兩個線程每次對j增加1,另外兩個線程對j每次減少1。寫出程序

public class Test {
    private int j;

    public static void main(String args[]) {
        Test tt = new Test();
        Inc inc = tt.new Inc();
        Dec dec = tt.new Dec();
        for (int i = 0; i < 2; i++) {
            Thread t = new Thread(inc);
            t.start();
            t = new Thread(dec);
            t.start();
        }
    }

    private synchronized void inc() {
        j++;
        System.out.println(Thread.currentThread().getName() + "-inc:" + j);
    }

    private synchronized void dec() {
        j--;
        System.out.println(Thread.currentThread().getName() + "-dec:" + j);
    }

    class Inc implements Runnable {
        public void run() {
            for (int i = 0; i < 100; i++) {
                inc();
            }
        }
    }

    class Dec implements Runnable {
        public void run() {
            for (int i = 0; i < 100; i++) {
                dec();
            }
        }
    }
} 

執行結果:
···
Thread-0-inc:5
Thread-0-inc:6
Thread-2-inc:7
Thread-2-inc:8
···
Thread-2-inc:105
Thread-2-inc:106
Thread-3-dec:105
Thread-3-dec:104
···
Thread-3-dec:7
Thread-3-dec:6
Thread-1-dec:5
Thread-1-dec:4
···
Thread-1-dec:-20
Thread-1-dec:-21
Thread-0-inc:-20
Thread-0-inc:-19
···

9. 子線程循環10次,接着主線程循環5次,接着又回到子線程循環10次,接着再回到主線程又循環5次數。如此循環50次,請寫出程序

public class Test {
    public static void main(String[] args) {
        new Test().init();

    }

    public void init() {
        final Business business = new Business();
        new Thread(new Runnable() {
            public void run() {
                for (int i = 0; i < 50; i++) {
                    business.SubThread(i); // 子線程
                }
            }
        }).start();

        for (int i = 0; i < 50; i++) {
            business.MainThread(i); // 主線程
        }
    }

    private class Business {
        //這裏相當於定義了控制該誰執行的一個信號燈
        boolean isShouldSub = true;

        public synchronized void MainThread(int i) {
            if (isShouldSub)
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            for (int j = 0; j < 5; j++) {
                System.out.println(Thread.currentThread().getName() + ":i=" + i + ",j=" + j);
            }
            isShouldSub = true;
            this.notify();

        }

        public synchronized void SubThread(int i) {
            if (!isShouldSub) {
                try {
                    this.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            for (int j = 0; j < 10; j++) {
                System.out.println(Thread.currentThread().getName() + ":i=" + i + ",j=" + j);
            }
            isShouldSub = false;
            this.notify();
        }
    }
} 
執行結果:
···
Thread-0:i=48,j=7
Thread-0:i=48,j=8
Thread-0:i=48,j=9
main:i=48,j=0
main:i=48,j=1
main:i=48,j=2
main:i=48,j=3
main:i=48,j=4
Thread-0:i=49,j=0
Thread-0:i=49,j=1
Thread-0:i=49,j=2
Thread-0:i=49,j=3
Thread-0:i=49,j=4
Thread-0:i=49,j=5
Thread-0:i=49,j=6
Thread-0:i=49,j=7
Thread-0:i=49,j=8
Thread-0:i=49,j=9
main:i=49,j=0
main:i=49,j=1
main:i=49,j=2
main:i=49,j=3
main:i=49,j=4

10. ArrayList和Vector的區別

  • 同步性:
    Vector是線程安全的,也就是說是它的方法之間是線程同步的。而ArrayList是線程序不安全的,它的方法之間是線程不同步的。如果只有一個線程會訪問到集合,那最好是使用ArrayList,因爲它不考慮線程安全,效率會高些;如果有多個線程會訪問到集合,那最好是使用Vector,因爲不需要我們自己再去考慮和編寫線程安全的代碼。
  • 數據增長:
    ArrayList與Vector都有一個初始的容量大小,當存儲進它們裏面的元素的個數超過了容量時,就需要增加ArrayList與Vector的存儲空間,每次要增加存儲空間時,不是隻增加一個存儲單元,而是增加多個存儲單元,每次增加的存儲單元的個數在內存空間利用與程序效率之間要取得一定的平衡。Vector默認增長爲原來兩倍,而ArrayList的增長策略在文檔中沒有明確規定(從源代碼看到的是增長爲原來的1.5倍)。ArrayList與Vector都可以設置初始的空間大小,Vector還可以設置增長的空間大小,而ArrayList沒有提供設置增長空間的方法。

備註:
對於Vector&ArrayListHashtable&HashMap,要記住線程安全的問題,記住Vector與Hashtable是舊的,是java一誕生就提供了的,它們是線程安全的,ArrayList與HashMap是java2時才提供的,它們是線程不安全的。

補充:
這兩個類 都實現了List接口(List接口繼承了Collection接口),他們都是有序集合,即存儲在這兩個集合中的元素的位置都是有順序的,相當於一種動態的數組,我們以後可以按位置索引號取出某個元素,,並且其中的數據是允許重複的,這是HashSet之類的集合的最大不同處,HashSet之類的集合不可以按索引號去檢索其中的元素,也不允許有重複的元素


11. Collection 和 Collections的區別?

Collection 是集合類的上級接口,繼承與他的接口主要有Set 和List.
Collections 是針對集合類的一個幫助類,他提供一系列靜態方法實現對各種集合的搜索、排序、線程安全化等操作。


12. 字節流與字符流的區別?

java中主要有兩種流:
1. 字節流,繼承於InputStream OutputStream
2. 字符流,繼承於InputStreamReader OutputStreamWriter
字符流是字節流的包裝。
字符向字節轉換時,要注意編碼的問題,因爲字符串轉成字節數組,其實是轉成該字符的某種編碼的字節形式,讀取也是反之的道理。


13. 什麼是java序列化,如何實現java序列化?或者請解釋Serializable接口的作用

我們有時候將一個java對象變成字節流的形式傳出去或者從一個字節流中恢復成一個java對象
例如,要將java對象存儲到硬盤或者傳送給網絡上的其他計算機,這個過程我們可以自己寫代碼去把一個java對象變成某個格式的字節流再傳輸。
但是,jre本身就提供了這種支持,我們可以調用OutputStreamwriteObject方法來做,如果要讓java幫我們做,要被傳輸的對象必須實現serializable接口,這樣,javac編譯時就會進行特殊處理,編譯的類纔可以被writeObject方法操作,這就是所謂的序列化。需要被序列化的類必須實現Serializable接口,該接口是一個mini接口,其中沒有需要實現的方法,implements Serializable 只是爲了標註該對象是可被序列化的

例如,在web開發中,如果對象被保存在了Session中,tomcat在重啓時要把Session對象序列化到硬盤,這個對象就必須實現Serializable接口。如果對象要經過分佈式系統進行網絡傳輸或通過rmi等遠程調用,這就需要在網絡上傳輸對象,被傳輸的對象就必須實現Serializable接口。


14. java中會存在內存泄漏嗎,請簡單描述

內存泄露是指一個不再被程序使用的對象或變量一直被佔據在內存中。java中有垃圾回收機制,它可以保證一對象不再被引用的時候,即對象編程了孤兒的時候,對象將自動被垃圾回收器從內存中清除掉。

java中的內存泄露的情況長生命週期的對象持有短生命週期對象的引用就很可能發生內存泄露,儘管短生命週期對象已經不再需要,但是因爲長生命週期對象持有它的引用而導致不能被回收。
例如:

  1. 緩存系統,我們加載了一個對象放在緩存中(例如放在一個全局map對象中),然後一直不再使用它,這個對象一直被緩存引用,但卻不再被使用。檢查java中的內存泄露,一定要讓程序將各種分支情況都完整執行到程序結束,然後看某個對象是否被使用過,如果沒有,則才能判定這個對象屬於內存泄露。
  2. 如果一個外部類的實例對象的方法返回了一個內部類的實例對象,這個內部類對象被長期引用了,即使那個外部類實例對象不再被使用,但由於內部類持久外部類的實例對象,這個外部類對象將不會被垃圾回收,這也會造成內存泄露。

15. volatile關鍵字是否能保證線程安全?()

答案:不能
解析:volatile關鍵字用在多線程同步中,可保證讀取的可見性,JVM只是保證從主內存加載到線程工作內存的值是最新的讀取值,而非cache中。但多個線程對volatile的寫操作,無法保證線程安全。
例如:假如線程1,線程2 在進行read,load 操作中,發現主內存中count的值都是5,那麼都會加載這個最新的值,在線程1對count進行修改之後,會write到主內存中,主內存中的count變量就會變爲6;線程2由於已經進行read,load操作,在進行運算之後,也會更新主內存count的變量值爲6;導致兩個線程及時用volatile關鍵字修改之後,還是會存在併發的情況。。


16. Java接口的修飾符可以爲()

A private B protected C final D abstract
答案:CD
解析:接口很重要,爲了說明情況,這裏稍微囉嗦點:

  1. 接口用於描述系統對外提供的所有服務,因此接口中的成員常量和方法都必須是公開(public)類型的,確保外部使用者能訪問它們;
  2. 接口僅僅描述系統能做什麼,但不指明如何去做,所以接口中的方法都是抽象(abstract)方法
  3. 接口不涉及和任何具體實例相關的細節,因此接口沒有構造方法,不能被實例化,沒有實例變量,只有靜態(static)變量
  4. 接口的中的變量是所有實現類共有的,既然共有,肯定是不變的東西,因爲變化的東西也不能夠算共有。所以變量是不可變(final)類型,也就是常量了。

    通俗的講,你認爲是要變化的東西,就放在你自己的實現中,不能放在接口中去,接口只是對一類事物的屬性和行爲更高層次的抽象。對修改關閉,對擴展(不同的實現 implements)開放,接口是對開閉原則的一種體現。

所以:

  • 接口的方法默認是public abstract;
  • 接口中不可以定義變量即只能定義常量(加上final修飾就會變成常量)。所以接口的屬性默認是public static final 常量,且必須賦初值。

注意:final和abstract不能同時出現。


17. 不通過構造函數也能創建對象嗎()

A 是 B 否
答案:A
解析:Java創建對象的幾種方式(重要):

  1. 用new語句創建對象,這是最常見的創建對象的方法。
  2. 運用反射手段,調用java.lang.Class或者java.lang.reflect.Constructor類的newInstance()實例方法。
  3. 調用對象的clone()方法。
  4. 運用反序列化手段,調用java.io.ObjectInputStream對象的 readObject()方法。
    (1)和(2)都會明確的顯式的調用構造函數 ;
    (3)是在內存上對已有對象的影印,所以不會調用構造函數 ;
    (4)是從文件中還原類的對象,也不會調用構造函數。
發佈了62 篇原創文章 · 獲贊 54 · 訪問量 55萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章