java基礎面試題


參考:http://blog.csdn.net/jackfrued/article/details/44921941

說未經允許不轉載,我只好參考了。

1.面向對象的特征有哪些方面?

  • 抽象:抽象是將一類對象的共同特征總結出來構造類的過程,包括數據抽象和行為抽象兩方面。抽象只關注對象有哪些屬性和行為,並不關注這些行為的細節是什么。
  • 繼承:繼承是從已有類得到繼承信息創建新類的過程。提供繼承信息的類稱為父類(超累,基類);得到繼承信息的類被稱為子類(派生類)。繼承讓變換中的軟件系統有了一定的延續性。同時繼承也是封裝程序中可變因素的重要手段。
  • 封裝:通常認為封裝是把數據和操作數據的方法綁定起來,對數據的訪問只能通過已定義的接口。面向對象的本質就是將現實世界描繪成一系列的完全自治、封閉的對象。我們在類中編寫的方法就是對實現細節的一種封裝;我們編寫一個類就是對數據和數據操作的封裝。可以說,封裝就是隱藏一起可以隱藏的東西,只向外界提供最簡單的編程接口。
  • 多態性:多態性是允許不同子類型對象對同一消息作出不同的響應。簡單的說就是用同樣的對象引用調用同樣的方法但是做了不同的事情。多態性分為編譯時多態性和運行時多態性。如果將對象的方法是為對象向外界提供的服務,那么運行時的多態性可以解釋為:調用不同的子類對象替換父類對象。方法重載(overload)實現的是編譯時多態性(也成為前綁定),而方法重寫(override)實現的是運行時的多態性(也稱為后綁定)。運行時的多態是面向對象最精髓的東西,要實現多態需要做兩件事:1方法重寫(子類繼承父類並重寫父類中已有的或抽象的方法)2對象造型(用父類引用子類對象)

2.訪問修飾符權限

權限分為:當前類,同包,子類,其他包

public均可;protected其他包不可;default同包下的可以;private只有自己可以。

3.String是基本數據類型嗎

答:不是。java中8中基本類型:byte,short,int,long,float,double,char,boolean;除了基本類型(primitive type)和枚舉類型(enumeration type),剩下的都是引用類型(reference type)。

 4.float f=3.4

錯誤,默認是double的,需要強轉,或者f=3.4f;

5.int和integer

為了將基本數據類型當做對象操作,Integer為包裝類(wrapper class)。

Integer緩存為-128到127.所以,這個范圍內的Integer對象是同一個,==為true。其他為false。

6.&和&&

&鏈接的操作符都要計算。&&是短路運算,即當前面表達式有錯誤就停止計算。

7.解釋內存中的棧(stack)、堆(heap)、和靜態區(static area)的 用法

答:通常我們定義一個基本數據類型的變量,一個對象的引用,還有就是函數調用的現場保存都使用內存中的棧空間;而通過new關鍵字和構造器創建的對象放在堆空間;程序中的字面量(literal)如直接書寫的100、“hello"和常量都是放在靜態區中。棧空間操作起來最快但棧很小,通常大量的對象都是放在堆空間,理論上整個內存沒有被其他進程使用的空間甚至磁盤上的虛擬內存都可以當做堆空間來使用。

String str = new String("hello");

  上面,str放棧,用new出來的字符串對象放堆上,而“hello”這個字面量放在靜態區。

java6開始使用“逃逸分析”的技術,可以將一些局部對象放在棧上提升對象操作性能。

8.switch是否可以用在byte,long,String?

答:java5前只可以:byte、short、char、int。5后增加enum,7后增加String.

9.最有效率的方法計算2乘以8

答:2<<3(左移3相當於乘以2的3次方,右移3相當於除以2的3次方)

補充:我們為編寫的類重寫hashCode方法時,可能會看到如下所示的代碼,其實我們不太理解為什么要使用這樣的乘法運算來產生哈希碼(散列碼),而且為什么這個數是個素數,為什么通常選擇31這個數?前兩個問題的答案你可以自己百度一下,選擇31是因為可以用移位和減法運算來代替乘法,從而得到更好的性能。說到這里你可能已經想到了:31 * num 等價於(num << 5) - num,左移5位相當於乘以2的5次方再減去自身就相當於乘以31,現在的VM都能自動完成這個優化。

10.數組有沒有length()方法,String有沒有length()方法?

答:數組沒有length()方法,有length屬性。String有length()方法。js中字符串是length屬性。

 11.構造器constructor是否可以override?

答:構造器不能被繼承,因此不能被重寫,但可以被重載。

12.兩個對象值相同(x.equals(y)==true),但卻可以有不同的hash code,這句話對不對?

答:不對。equals的hashcode必須相同。

13.是否可以繼承String類

答:String類是final類,不可以被繼承。繼承String是個錯誤的行為,應該用關聯關系(Has-A)和依賴關系(Use A)而不是繼承關系(Is-A).

14.當一個對象被當作參數傳遞到一個方法后,此方法可改變這個對象的屬性,並可返回變化后的結果,那么這里到底是值傳遞還是引用傳遞? 
答:是值傳遞。Java語言的方法調用只支持參數的值傳遞。當一個對象實例作為一個參數被傳遞到方法中時,參數的值就是對該對象的引用。對象的屬性可以在被調用過程中被改變,但對對象引用的改變是不會影響到調用者的。

15.String==

@Test
    public void str_c(){
        String a = "hehe";
        String b = "he"+"he";
        String c = new String("hehe");
        String d = new String("hehe");

    System.out.println(a==b);//true
    System.out.println(a==c);//false
    System.out.println(a==a.intern());//true
    System.out.println(c==d);//false
    }

16.重載(Overload)和重寫(Override)的區別。重載的方法能否根據返回類型進行區分? 
答:方法的重載和重寫都是實現多態的方式,區別在於前者實現的是編譯時的多態性,而后者實現的是運行時的多態性。重載發生在一個類中,同名的方法如果有不同的參數列表(參數類型不同、參數個數不同或者二者都不同)則視為重載;重寫發生在子類與父類之間,重寫要求子類被重寫方法與父類被重寫方法有相同的返回類型,比父類被重寫方法更好訪問,不能比父類被重寫方法聲明更多的異常(里氏代換原則)。重載對返回類型沒有特殊的要求。

22、char 型變量中能不能存貯一個中文漢字,為什么? 
答:char類型可以存儲一個中文漢字,因為Java中使用的編碼是Unicode(不選擇任何特定的編碼,直接使用字符在字符集中的編號,這是統一的唯一方法),一個char類型占2個字節(16比特),所以放一個中文是沒問題的。

23.抽象類(abstract class)和接口(interface)有什么異同?

答:抽象類和接口都不能實例化,但可以定義抽象類和接口類型的引用。一個類如果繼承了某個抽象類或者實現了某個接口都需要對其中的抽象方法全部進行實現,否則該類仍然需要聲明為抽象類。接口比抽象類更加抽象,因為抽象類中可以定義構造器,可以有抽象方法和具體方法,而接口中不能定義構造器而且其中的方法全部都是抽象方法。抽象類中的成員可以是privae,默認,protected,public,而接口中的成員變量全部是public。抽象類中可以定義成員變量,而接口中定義的成員白嬢實際上都是常量。有抽象方法的類必須被聲明為抽象類,抽象類未必有抽象方法。

24、靜態嵌套類(Static Nested Class)和內部類(Inner Class)的不同? 
答:Static Nested Class是被聲明為靜態(static)的內部類,它可以不依賴於外部類實例被實例化。而通常的內部類需要在外部類實例化后才能實例化。

25、Java 中會存在內存泄漏嗎,請簡單描述。 
答:理論上Java因為有垃圾回收機制(GC)不會存在內存泄露問題(這也是Java被廣泛使用於服務器端編程的一個重要原因);然而在實際開發中,可能會存在無用但可達的對象,這些對象不能被GC回收,因此也會導致內存泄露的發生。例如Hibernate的Session(一級緩存)中的對象屬於持久態,垃圾回收器是不會回收這些對象的,然而這些對象中可能存在無用的垃圾對象,如果不及時關閉(close)或清空(flush)一級緩存就可能導致內存泄露。下面例子中的代碼也會導致內存泄露。

import java.util.Arrays;
import java.util.EmptyStackException;

public class MyStack<T> {
    private T[] elements;
    private int size = 0;

    private static final int INIT_CAPACITY = 16;

    public MyStack() {
        elements = (T[]) new Object[INIT_CAPACITY];
    }

    public void push(T elem) {
        ensureCapacity();
        elements[size++] = elem;
    }

    public T pop() {
        if(size == 0) 
            throw new EmptyStackException();
        return elements[--size];
    }

    private void ensureCapacity() {
        if(elements.length == size) {
            elements = Arrays.copyOf(elements, 2 * size + 1);
        }
    }
}

 上面的代碼實現了一個棧(先進后出(FILO))結構,乍看之下似乎沒有什么明顯的問題,它甚至可以通過你編寫的各種單元測試。然而其中的pop方法卻存在內存泄露的問題,當我們用pop方法彈出棧中的對象時,該對象不會被當作垃圾回收,即使使用棧的程序不再引用這些對象,因為棧內部維護着對這些對象的過期引用(obsolete reference)。在支持垃圾回收的語言中,內存泄露是很隱蔽的,這種內存泄露其實就是無意識的對象保持。如果一個對象引用被無意識的保留起來了,那么垃圾回收器不會處理這個對象,也不會處理該對象引用的其他對象,即使這樣的對象只有少數幾個,也可能會導致很多的對象被排除在垃圾回收之外,從而對性能造成重大影響,極端情況下會引發Disk Paging(物理內存與硬盤的虛擬內存交換數據),甚至造成OutOfMemoryError。

 26、抽象的(abstract)方法是否可同時是靜態的(static),是否可同時是本地方法(native),是否可同時被synchronized修飾? 
答:都不能。抽象方法需要子類重寫,而靜態的方法是無法被重寫的,因此二者是矛盾的。本地方法是由本地代碼(如C代碼)實現的方法,而抽象方法是沒有實現的,也是矛盾的。synchronized和方法的實現細節有關,抽象方法不涉及實現細節,因此也是相互矛盾的。

27、闡述靜態變量和實例變量的區別。 
答:靜態變量是被static修飾符修飾的變量,也稱為類變量,它屬於類,不屬於類的任何一個對象,一個類不管創建多少個對象,靜態變量在內存中有且僅有一個拷貝;實例變量必須依存於某一實例,需要先創建對象然后通過對象才能訪問到它。靜態變量可以實現讓多個對象共享內存。

補充:在Java開發中,上下文類和工具類中通常會有大量的靜態成員。

28.繼承中變量的覆蓋和輸出

class Base {
    String a = "父類a";
    String  b = "父類b";
    static void a() {
        System.out.println("父類的靜態A");
    }

    void b() {
        System.out.println("父類的B"+b);
    }

    void c(){
        System.out.println("父類的成員變量a:"+a);
    }

    void d(){
        System.out.println("父成員變量b:"+b);
    }
}

class Inherit extends Base {
    String a = "zi類a";
    String  b = "zi類b";
    static void a() {
        System.out.println("子類的靜態C");
    }

    void b() {
        System.out.println("子類b:"+b);
    }
    void c(){
        System.out.println("子類的成員變量a:"+a);
    }

    public static void main(String args[]) {
        Base b = new Base();
        Inherit inherit = new Inherit();
        Base c = new Inherit();
        System.out.println("父類===================");
        b.a();//父類的靜態A
        b.b();//父類的B
        System.out.println("子類===================");
        inherit.a();//子類掩蓋了父類的靜態方法,子類的靜態C
        inherit.b();//子類b:zi類b,打印自己的
        inherit.c();//子類的成員變量a:zi類a,打印自己的
        inherit.d();//父成員變量b:父類b,調用父類的d方法,並且d方法里的成員變量a也是b的
        System.out.println("父類指向子類============");
        c.a();//父類的靜態A
        c.b();//子類b:zi類b
        c.c();//子類的成員變量a:zi類a
        c.d();//父成員變量b:父類b
    }
}

子類覆蓋了父類的方法,並且覆蓋了父類的成員變量,並且在覆蓋的方法中調用了這個覆蓋的成員變量。這時候,調用這個覆蓋的方法會調用覆蓋的成員變量。如果子類只覆蓋了成員變量,沒有覆蓋方法,調用這個方法會調用父類的成員變量,盡管這個成員變量被覆蓋了。

28、是否可以從一個靜態(static)方法內部發出對非靜態(non-static)方法的調用?
答:不可以,靜態方法只能訪問靜態成員,因為非靜態方法的調用要先創建對象,在調用靜態方法時可能對象並沒有被初始化。

29、如何實現對象克隆? 
答:有兩種方式: 
  1). 實現Cloneable接口並重寫Object類中的clone()方法; 
  2). 實現Serializable接口,通過對象的序列化和反序列化實現克隆,可以實現真正的深度克隆,代碼如下。

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class MyUtil {

    private MyUtil() {
        throw new AssertionError();
    }

    public static <T> T clone(T obj) throws Exception {
        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bout);
        oos.writeObject(obj);

        ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
        ObjectInputStream ois = new ObjectInputStream(bin);
        return (T) ois.readObject();

        // 說明:調用ByteArrayInputStream或ByteArrayOutputStream對象的close方法沒有任何意義
        // 這兩個基於內存的流只要垃圾回收器清理對象就能夠釋放資源,這一點不同於對外部資源(如文件流)的釋放
    }
}
View Code

注意:基於序列化和反序列化實現的克隆不僅僅是深度克隆,更重要的是通過泛型限定,可以檢查出要克隆的對象是否支持序列化,這項檢查是編譯器完成的,不是在運行時拋出異常,這種是方案明顯優於使用Object類的clone方法克隆對象。讓問題在編譯的時候暴露出來總是優於把問題留到運行時。

30、GC是什么?為什么要有GC? 
答:GC是垃圾收集的意思,內存處理是編程人員容易出現問題的地方,忘記或者錯誤的內存回收會導致程序或系統的不穩定甚至崩潰,Java提供的GC功能可以自動監測對象是否超過作用域從而達到自動回收內存的目的,Java語言沒有提供釋放已分配內存的顯示操作方法。Java程序員不用擔心內存管理,因為垃圾收集器會自動進行管理。要請求垃圾收集,可以調用下面的方法之一:System.gc() 或Runtime.getRuntime().gc() ,但JVM可以屏蔽掉顯示的垃圾回收調用。 
垃圾回收可以有效的防止內存泄露,有效的使用可以使用的內存。垃圾回收器通常是作為一個單獨的低優先級的線程運行,不可預知的情況下對內存堆中已經死亡的或者長時間沒有使用的對象進行清除和回收,程序員不能實時的調用垃圾回收器對某個對象或所有對象進行垃圾回收。在Java誕生初期,垃圾回收是Java最大的亮點之一,因為服務器端的編程需要有效的防止內存泄露問題,然而時過境遷,如今Java的垃圾回收機制已經成為被詬病的東西。移動智能終端用戶通常覺得iOS的系統比Android系統有更好的用戶體驗,其中一個深層次的原因就在於Android系統中垃圾回收的不可預知性。

補充:垃圾回收機制有很多種,包括:分代復制垃圾回收、標記垃圾回收、增量垃圾回收等方式。標准的Java進程既有棧又有堆。棧保存了原始型局部變量,堆保存了要創建的對象。Java平台對堆內存回收和再利用的基本算法被稱為標記和清除,但是Java對其進行了改進,采用“分代式垃圾收集”。這種方法會跟Java對象的生命周期將堆內存划分為不同的區域,在垃圾收集過程中,可能會將對象移動到不同區域: 
- 伊甸園(Eden):這是對象最初誕生的區域,並且對大多數對象來說,這里是它們唯一存在過的區域。 
- 幸存者樂園(Survivor):從伊甸園幸存下來的對象會被挪到這里。 
- 終身頤養園(Tenured):這是足夠老的幸存對象的歸宿。年輕代收集(Minor-GC)過程是不會觸及這個地方的。當年輕代收集不能把對象放進終身頤養園時,就會觸發一次完全收集(Major-GC),這里可能還會牽扯到壓縮,以便為大對象騰出足夠的空間。

 

與垃圾回收相關的JVM參數:

  • -Xms / -Xmx — 堆的初始大小 / 堆的最大大小
  • -Xmn — 堆中年輕代的大小
  • -XX:-DisableExplicitGC — 讓System.gc()不產生任何作用
  • -XX:+PrintGCDetails — 打印GC的細節
  • -XX:+PrintGCDateStamps — 打印GC操作的時間戳
  • -XX:NewSize / XX:MaxNewSize — 設置新生代大小/新生代最大大小
  • -XX:NewRatio — 可以設置老生代和新生代的比例
  • -XX:PrintTenuringDistribution — 設置每次新生代GC后輸出幸存者樂園中對象年齡的分布
  • -XX:InitialTenuringThreshold / -XX:MaxTenuringThreshold:設置老年代閥值的初始值和最大值
  • -XX:TargetSurvivorRatio:設置幸存區的目標使用率

31、String s = new String("xyz");創建了幾個字符串對象? 
答:兩個對象,一個是靜態區的"xyz",一個是用new創建在堆上的對象。

32、接口是否可繼承(extends)接口?抽象類是否可實現(implements)接口?抽象類是否可繼承具體類(concrete class)? 
答:接口可以繼承接口,而且支持多重繼承。抽象類可以實現(implements)接口,抽象類可繼承具體類也可以繼承抽象類。

35、內部類可以引用它的包含類(外部類)的成員嗎?有沒有什么限制? 
答:一個內部類對象可以訪問創建它的外部類對象的成員,包括私有成員。

36、Java 中的final關鍵字有哪些用法? 
答:(1)修飾類:表示該類不能被繼承;(2)修飾方法:表示方法不能被重寫;(3)修飾變量:表示變量只能一次賦值以后值不能被修改(常量)。

46、try{}里有一個return語句,那么緊跟在這個try后的finally{}里的代碼會不會被執行,什么時候被執行,在return前還是后? 
答:會執行,在方法返回調用者前執行。

注意:在finally中改變返回值的做法是不好的,因為如果存在finally代碼塊,try中的return語句不會立馬返回調用者,而是記錄下返回值待finally代碼塊執行完畢之后再向調用者返回其值,然后如果在finally中修改了返回值,就會返回修改后的值。顯然,在finally中返回或者修改返回值會對程序造成很大的困擾

 49、列出一些你常見的運行時異常? 
答: 
- ArithmeticException(算術異常) 
- ClassCastException (類轉換異常) 
- IllegalArgumentException (非法參數異常) 
- IndexOutOfBoundsException (下標越界異常) 
- NullPointerException (空指針異常) 
- SecurityException (安全異常)

50、闡述final、finally、finalize的區別。 
答: 
- final:修飾符(關鍵字)有三種用法:如果一個類被聲明為final,意味着它不能再派生出新的子類,即不能被繼承,因此它和abstract是反義詞。將變量聲明為final,可以保證它們在使用中不被改變,被聲明為final的變量必須在聲明時給定初值,而在以后的引用中只能讀取不可修改。被聲明為final的方法也同樣只能使用,不能在子類中被重寫。 
- finally:通常放在try…catch…的后面構造總是執行代碼塊,這就意味着程序無論正常執行還是發生異常,這里的代碼只要JVM不關閉都能執行,可以將釋放外部資源的代碼寫在finally塊中。 
- finalize:Object類中定義的方法,Java中允許使用finalize()方法在垃圾收集器將對象從內存中清除出去之前做必要的清理工作。這個方法是由垃圾收集器在銷毀對象時調用的,通過重寫finalize()方法可以整理系統資源或者執行其他清理工作。

52、List、Set、Map是否繼承自Collection接口? 
答:List、Set 是,Map 不是。Map是鍵值對映射容器,與List和Set有明顯的區別,而Set存儲的零散的元素且不允許有重復元素(數學中的集合也是如此),List是線性結構的容器,適用於按數值索引訪問元素的情形。

53、闡述ArrayList、Vector、LinkedList的存儲性能和特性。 
答:ArrayList 和Vector都是使用數組方式存儲數據,此數組元素數大於實際存儲的數據以便增加和插入元素,它們都允許直接按序號索引元素,但是插入元素要涉及數組元素移動等內存操作,所以索引數據快而插入數據慢,Vector中的方法由於添加了synchronized修飾,因此Vector是線程安全的容器,但性能上較ArrayList差,因此已經是Java中的遺留容器。LinkedList使用雙向鏈表實現存儲(將內存中零散的內存單元通過附加的引用關聯起來,形成一個可以按序號索引的線性結構,這種鏈式存儲方式與數組的連續存儲方式相比,內存的利用率更高),按序號索引數據需要進行前向或后向遍歷,但是插入數據時只需要記錄本項的前后項即可,所以插入速度較快。Vector屬於遺留容器(Java早期的版本中提供的容器,除此之外,Hashtable、Dictionary、BitSet、Stack、Properties都是遺留容器),已經不推薦使用,但是由於ArrayList和LinkedListed都是非線程安全的,如果遇到多個線程操作同一個容器的場景,則可以通過工具類Collections中的synchronizedList方法將其轉換成線程安全的容器后再使用(這是對裝潢模式的應用,將已有對象傳入另一個類的構造器中創建新的對象來增強實現)。

補充:遺留容器中的Properties類和Stack類在設計上有嚴重的問題,Properties是一個鍵和值都是字符串的特殊的鍵值對映射,在設計上應該是關聯一個Hashtable並將其兩個泛型參數設置為String類型,但是Java API中的Properties直接繼承了Hashtable,這很明顯是對繼承的濫用。這里復用代碼的方式應該是Has-A關系而不是Is-A關系,另一方面容器都屬於工具類,繼承工具類本身就是一個錯誤的做法,使用工具類最好的方式是Has-A關系(關聯)或Use-A關系(依賴)。同理,Stack類繼承Vector也是不正確的。Sun公司的工程師們也會犯這種低級錯誤,讓人唏噓不已。

54、Collection和Collections的區別? 
答:Collection是一個接口,它是Set、List等容器的父接口;Collections是個一個工具類,提供了一系列的靜態方法來輔助容器操作,這些方法包括對容器的搜索、排序、線程安全化等等。

55、List、Map、Set三個接口存取元素時,各有什么特點? 
答:List以特定索引來存取元素,可以有重復元素。Set不能存放重復元素(用對象的equals()方法來區分元素是否重復)。Map保存鍵值對(key-value pair)映射,映射關系可以是一對一或多對一。Set和Map容器都有基於哈希存儲和排序樹的兩種實現版本,基於哈希存儲的版本理論存取時間復雜度為O(1),而基於排序樹版本的實現在插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。

56、TreeMap和TreeSet在排序時如何比較元素?Collections工具類中的sort()方法如何比較元素? 
答:TreeSet要求存放的對象所屬的類必須實現Comparable接口,該接口提供了比較元素的compareTo()方法,當插入元素時會回調該方法比較元素的大小。TreeMap要求存放的鍵值對映射的鍵必須實現Comparable接口從而根據鍵對元素進行排序。Collections工具類的sort方法有兩種重載的形式,第一種要求傳入的待排序容器中存放的對象比較實現Comparable接口以實現元素的比較;第二種不強制性的要求容器中的元素必須可比較,但是要求傳入第二個參數,參數是Comparator接口的子類型(需要重寫compare方法實現元素的比較),相當於一個臨時定義的排序規則,其實就是通過接口注入比較元素大小的算法,也是對回調模式的應用(Java中對函數式編程的支持)。 

57、Thread類的sleep()方法和對象的wait()方法都可以讓線程暫停執行,它們有什么區別? 
答:sleep()方法(休眠)是線程類(Thread)的靜態方法,調用此方法會讓當前線程暫停執行指定的時間,將執行機會(CPU)讓給其他線程,但是對象的鎖依然保持,因此休眠時間結束后會自動恢復(線程回到就緒狀態,請參考第66題中的線程狀態轉換圖)。wait()是Object類的方法,調用對象的wait()方法導致當前線程放棄對象的鎖(線程暫停執行),進入對象的等待池(wait pool),只有調用對象的notify()方法(或notifyAll()方法)時才能喚醒等待池中的線程進入等鎖池(lock pool),如果線程重新獲得對象的鎖就可以進入就緒狀態。

58、線程的sleep()方法和yield()方法有什么區別? 
答: 
① sleep()方法給其他線程運行機會時不考慮線程的優先級,因此會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會; 
② 線程執行sleep()方法后轉入阻塞(blocked)狀態,而執行yield()方法后轉入就緒(ready)狀態; 
③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常; 
④ sleep()方法比yield()方法(跟操作系統CPU調度相關)具有更好的可移植性。

60、請說出與線程同步以及線程調度相關的方法。 
答: 
- wait():使一個線程處於等待(阻塞)狀態,並且釋放所持有的對象的鎖; 
- sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要處理InterruptedException異常; 
- notify():喚醒一個處於等待狀態的線程,當然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且與優先級無關; 
- notityAll():喚醒所有處於等待狀態的線程,該方法並不是將對象的鎖給所有線程,而是讓它們競爭,只有獲得鎖的線程才能進入就緒狀態;

提示:關於Java多線程和並發編程的問題,建議大家看我的另一篇文章《關於Java並發編程的總結和思考》

補充:Java 5通過Lock接口提供了顯式的鎖機制(explicit lock),增強了靈活性以及對線程的協調。Lock接口中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了newCondition()方法來產生用於線程之間通信的Condition對象;此外,Java 5還提供了信號量機制(semaphore),信號量可以用來限制對某個共享資源進行訪問的線程的數量。在對資源進行訪問之前,線程必須得到信號量的許可(調用Semaphore對象的acquire()方法);在完成對資源的訪問后,線程必須向信號量歸還許可(調用Semaphore對象的release()方法)。

61、編寫多線程程序有幾種實現方式? 
答:Java 5以前實現多線程有兩種實現方法:一種是繼承Thread類;另一種是實現Runnable接口。兩種方式都要通過重寫run()方法來定義線程的行為,推薦使用后者,因為Java中的繼承是單繼承,一個類有一個父類,如果繼承了Thread類就無法再繼承其他類了,顯然使用Runnable接口更為靈活。

補充:Java 5以后創建線程還有第三種方式:實現Callable接口,該接口中的call方法可以在線程執行結束時產生一個返回值

64、啟動一個線程是調用run()還是start()方法? 
答:啟動一個線程是調用start()方法,使線程所代表的虛擬處理機處於可運行狀態,這意味着它可以由JVM 調度並執行,這並不意味着線程就會立即運行。run()方法是線程啟動后要進行回調(callback)的方法。

65、什么是線程池(thread pool)? 
答:在面向對象編程中,創建和銷毀對象是很費時間的,因為創建一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每一個對象,以便能夠在對象銷毀后進行垃圾回收。所以提高服務程序效率的一個手段就是盡可能減少創建和銷毀對象的次數,特別是一些很耗資源的對象創建和銷毀,這就是”池化資源”技術產生的原因。線程池顧名思義就是事先創建若干個可執行的線程放入一個池(容器)中,需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷毀線程而是放回池中,從而減少創建和銷毀線程對象的開銷。 
Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。要配置一個線程池是比較復雜的,尤其是對於線程池的原理不是很清楚的情況下,因此在工具類Executors面提供了一些靜態工廠方法,生成一些常用的線程池,如下所示: 
- newSingleThreadExecutor:創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因為異常結束,那么會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。 
- newFixedThreadPool:創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因為執行異常而結束,那么線程池會補充一個新線程。 
- newCachedThreadPool:創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那么就會回收部分空閑(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。 
- newScheduledThreadPool:創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求。 
- newSingleThreadExecutor:創建一個單線程的線程池。此線程池支持定時以及周期性執行任務的需求。

第60題的例子中演示了通過Executors工具類創建線程池並使用線程池執行線程的代碼。如果希望在服務器上使用線程池,強烈建議使用newFixedThreadPool方法來創建線程池,這樣能獲得更好的性能。

68、Java中如何實現序列化,有什么意義? 
答:序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流化。可以對流化后的對象進行讀寫操作,也可將流化后的對象傳輸於網絡之間。序列化是為了解決對象流讀寫操作時可能引發的問題(如果不進行序列化可能會存在數據亂序的問題)。 
要實現序列化,需要讓一個類實現Serializable接口,該接口是一個標識性接口,標注該類對象是可被序列化的,然后使用一個輸出流來構造一個對象輸出流並通過writeObject(Object)方法就可以將實現對象寫出(即保存其狀態);如果需要反序列化則可以用一個輸入流建立對象輸入流,然后通過readObject方法從流中讀取對象。序列化除了能夠實現對象的持久化之外,還能夠用於對象的深度克隆(可以參考第29題)。

69、Java中有幾種類型的流? 
答:字節流和字符流。字節流繼承於InputStream、OutputStream,字符流繼承於Reader、Writer。在java.io 包中還有許多其他的流,主要是為了提高性能和使用方便。關於Java的I/O需要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不同於C#的是它只有一個維度一個方向。

面試題 - 編程實現文件拷貝。(這個題目在筆試的時候經常出現,下面的代碼給出了兩種實現方案)

public static void fileCopy(String source,String targer) throws IOException {
        try(InputStream in = new FileInputStream(source)) {
            try(OutputStream out = new FileOutputStream(targer)) {
                byte[] buffer = new byte[3096];
                int byteToRead;
                while((byteToRead = in.read(buffer))!=-1){
                    out.write(buffer,0,byteToRead);
                }
            }
        }
    }
    public static void fileCopyNIO(String source,String target) throws IOException {
        try(FileInputStream in = new FileInputStream(source)) {
            try(FileOutputStream out = new FileOutputStream(target)) {
                FileChannel inChannel  = in.getChannel();
                FileChannel outChannel = out.getChannel();
                ByteBuffer buffer = ByteBuffer.allocate(4096);
                while (inChannel.read(buffer)!=-1){
                    buffer.flip();
                    outChannel.write(buffer);
                    buffer.clear();
                }
            }
        }
    }
View Code

注意:上面用到Java 7的TWR,使用TWR后可以不用在finally中釋放外部資源 ,從而讓代碼更加優雅。

70、寫一個方法,輸入一個文件名和一個字符串,統計這個字符串在這個文件中出現的次數。 

 /**
     *統計給定文件中字符word的個數
     */
    public static int countWordInFile(String filename,String word) throws IOException {
        int count = 0;
        try(FileReader fr = new FileReader(filename)){
            try(BufferedReader br = new BufferedReader(fr)){
                String line = null;
                while ((line = br.readLine())!=null){
                    int index = -1;
                    while (line.length()>=word.length() && (index=line.indexOf(word))>=0){
                        count++;
                        line = line.substring(index+word.length());
                    }
                }
            }
        }
        return count;
    }
View Code

71、如何用Java代碼列出一個目錄下所有的文件? 

  /**
     * 列出當前文件夾下的文件
     */
    public void fileList(String source){
        File file = new File(source);
        for (File temp : file.listFiles()) {
            if (temp.isFile()){
                System.out.println(temp.getName());
            }
        }
    }

如果需要對文件夾繼續展開,代碼如下所示:

/**
     * 列出文件夾下的所有文件,深入
     */
    private static void _walkDirectory(File f,int level) throws IOException {
        if (f.isDirectory()){
            writeTofile(f, level);
            for (File temp : f.listFiles()) {
                _walkDirectory(temp,level+1);
            }
        }else {
            writeTofile(f, level);
        }
    }

    private static void writeTofile(File f, int level) throws IOException {
        try(BufferedWriter bw = new BufferedWriter(new FileWriter(new File("F:\\listFile.txt"),true))){
            for (int i = 0; i < level - 1; i++) {
                System.out.print("----");
                bw.write("----");
            }
            System.out.println("|"+f.getName());
            bw.write("|"+f.getName());
            bw.newLine();
            bw.flush();
        }
    }

    public static void showDirectory(File f) throws IOException {
        _walkDirectory(f,0);
    }
    @Test
    public void testShow(){
        try {
            showDirectory(new File("D:\\MyApp"));
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
View Code

在java7中可以使用NIO.2的api來做:

/**
     * 列出文件夾下的所有文件,深入
     */
    @Test
    public void listFiles() throws IOException {
        Path path = Paths.get("D:\\MyApp");
        Files.walkFileTree(path,new SimpleFileVisitor<Path>(){
            @Override
            public FileVisitResult visitFile(Path file, BasicFileAttributes attrs){
                System.out.println(file.getFileName().toString());
                return FileVisitResult.CONTINUE;
            }
        });
    }
View Code

 72、用Java的套接字編程實現一個多線程的回顯(echo)服務器。 
答:

/**
 * socket多線程回顯
 * Created by mrf on 2016/3/17.
 */
public class EchoServer {
    private static final int ECHO_SERVER_PORT = 6789;

    public static void main(String[] args) {
        try(ServerSocket server = new ServerSocket(ECHO_SERVER_PORT)) {
            System.out.println("=================================");
            System.out.println("============服務啟動 =============");
            while (true){
                Socket client = server.accept();
                new Thread(new ClientHandler(client)).start();
            }

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    private static class ClientHandler implements Runnable{
        private Socket client;
        public ClientHandler(Socket client){
            this.client = client;
        }

        @Override
        public void run() {
            try(BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
                PrintWriter pw = new PrintWriter(client.getOutputStream())
            ) {
                String msg = br.readLine();
                System.out.println("收到"+client.getInetAddress()+"發送的:"+msg);
                pw.println(msg);
                pw.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }finally {
                try {
                    client.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

/**
 * 測試
 */
class EchoClient{
    public static void main(String[] args) throws IOException {
        Socket client = new Socket("localhost",6789);
        Scanner sc = new Scanner(System.in);
            System.out.println("請輸入內容:");
            String msg = sc.nextLine();
            sc.close();
            PrintWriter pw = new PrintWriter(client.getOutputStream());
            pw.println(msg);
            pw.flush();
            BufferedReader br = new BufferedReader(new InputStreamReader(client.getInputStream()));
            System.out.println(br.readLine());
    }
}
View Code

73、XML文檔定義有幾種形式?它們之間有何本質區別?解析XML文檔有哪幾種方式? 

答:xml文檔定義分為DTD和Schema兩種形式,二者都是對xml語法的約束,其本質區別在於Schema本身也是一個xml文件,可以被xml解析器解析,而且可以為xml承載的數據定義類型,約束能力較之DTD更強大。對xml的解析主要有

dom(文檔對象模型,Document Object Model)、SAX(Simple API for xml)和StAx(java6中引入的新的解析xml的方式,Streaming API for xml),其中dom處理大型文件時其性能下降的非常厲害,這個問題是由DOM樹結構占用的內存較多造成的,而且dom解析方式必須在解析文件之前把整個文件裝入內存,適合對xml的隨機訪問(典型的空間換時間);sax是事件驅動的xml解析方法,它順序讀取xml文件,不需要一次全部裝載整個文件。檔遇到像文件開頭,文檔結束,或者標簽開頭與標簽結束時,它會觸發一個事件,用戶通過事件回調代碼來處理xml文件,適合對xml的順序訪問;顧名思義,StAx把重點放在流上,實際上StAX與其他解析方式的本質區別就在於應用程序能夠把xml做為一個事件流來處理。將xml做為一組事件來處理的想法並不新穎(sax就是這樣做的),但不同之處在於StAx允許應用程序代碼把這些事件逐個拉出來,而不用提供在解析器方便時從解析器中接收事件的處理程序。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM