JAVA面試題大全2

5一、類ExampleA 繼承Exception,類ExampleB 繼承ExampleA。

有以下代碼片段:java

try{
    throw new ExampleB("b")
}catch(ExampleA e){
    System.out.println("ExampleA");
}catch(Exception e){
    System.out.println("Exception");
}

請問執行此段代碼的輸出是什麼?mysql

答:輸出:ExampleA。(根據里氏代換原則[能使用父類型的地方必定能使用子類型],抓取ExampleA類型異常的catch塊可以抓住try塊中拋出的ExampleB類型的異常)程序員

class Annoyance extends Exception {}
class Sneeze extends Annoyance {}

class Human {

	public static void main(String[] args) 
		throws Exception {
		try {
			try {
				throw new Sneeze();
			} 
			catch ( Annoyance a ) {
				System.out.println("Caught Annoyance");
				throw a;
			}
		} 
		catch ( Sneeze s ) {
			System.out.println("Caught Sneeze");
			return ;
		}
		finally {
			System.out.println("Hello World!");
		}
	}
}

上面這段代碼輸出是什麼?應該是Caught Annoyance, Caught Sneeze, Hello World!面試

5二、List、Set、Map 是否繼承自Collection 接口?

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

5三、說出ArrayList、Vector、LinkedList 的存儲性能和特性?

答:ArrayList 和Vector都是使用數組方式存儲數據,此數組元素數大於實際存儲的數據以便增長和插入元素,它們都容許直接按序號索引元素,可是插入元素要涉及數組元素移動等內存操做,因此索引數據快而插入數據慢,Vector因爲使用了synchronized 方法(線程安全),一般性能上較ArrayList 差,而LinkedList 使用雙向鏈表實現存儲(將內存中零散的內存單元經過附加的引用關聯起來,造成一個能夠按序號索引的線性結構,這種鏈式存儲方式與數組的連續存儲方式相比,其實對內存的利用率更高),按序號索引數據須要進行前向或後向遍歷,可是插入數據時只須要記錄本項的先後項便可,因此插入速度較快。Vector屬於遺留容器(早期的JDK中使用的容器,除此以外Hashtable、Dictionary、BitSet、Stack、Properties都是遺留容器),如今已經不推薦使用,可是因爲ArrayList和LinkedListed都是非線程安全的,若是須要多個線程操做同一個容器,那麼能夠經過工具類Collections中的synchronizedList方法將其轉換成線程安全的容器後再使用(這實際上是裝潢模式最好的例子,將已有對象傳入另外一個類的構造器中建立新的對象來增長新功能)。sql

補充:遺留容器中的Properties類和Stack類在設計上有嚴重的問題,Properties是一個鍵和值都是字符串的特殊的鍵值對映射,在設計上應該是關聯一個Hashtable並將其兩個泛型參數設置爲String類型,可是Java API中的Properties直接繼承了Hashtable,這很明顯是對繼承的濫用。這裏複用代碼的方式應該是HAS-A關係而不是IS-A關係,另外一方面容器都屬於工具類,繼承工具類自己就是一個錯誤的作法,使用工具類最好的方式是HAS-A關係(關聯)或USE-A關係(依賴)。同理,Stack類繼承Vector也是不正確的。數據庫

 

5四、Collection 和Collections 的區別?

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

 

5五、List、Map、Set 三個接口,存取元素時,各有什麼特色?

答:List以特定索引來存取元素,可有重複元素。Set不能存放重複元素(用對象的equals()方法來區分元素是否重複)。Map保存鍵值對(key-value pair)映射,映射關係能夠是一對一或多對一。Set和Map容器都有基於哈希存儲和排序樹的兩種實現版本,基於哈希存儲的版本理論存取時間複雜度爲O(1),而基於排序樹版本的實如今插入或刪除元素時會按照元素或元素的鍵(key)構成排序樹從而達到排序和去重的效果。設計模式

 

5六、TreeMap和TreeSet在排序時如何比較元素?Collections工具類中的sort()方法如何比較元素?

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

public class Student implements Comparable<Student> {
	private String name;		// 姓名
	private int age;			// 年齡

	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}

	@Override
	public int compareTo(Student o) {
		return this.age - o.age; // 比較年齡(年齡的升序)
	}

}
import java.util.Set;
import java.util.TreeSet;

class Test01 {

	public static void main(String[] args) {
		Set<Student> set = new TreeSet<>();		// Java 7的鑽石語法(構造器後面的尖括號中不須要寫類型)
		set.add(new Student("Hao LUO", 33));
		set.add(new Student("XJ WANG", 32));
		set.add(new Student("Bruce LEE", 60));
		set.add(new Student("Bob YANG", 22));
		
		for(Student stu : set) {
			System.out.println(stu);
		}
//		輸出結果: 
//		Student [name=Bob YANG, age=22]
//		Student [name=XJ WANG, age=32]
//		Student [name=Hao LUO, age=33]
//		Student [name=Bruce LEE, age=60]
	}
}
public class Student {
	private String name; 	// 姓名
	private int age; 		// 年齡

	public Student(String name, int age) {
		this.name = name;
		this.age = age;
	}

	/**
	 * 獲取學生姓名
	 */
	public String getName() {
		return name;
	}

	/**
	 * 獲取學生年齡
	 */
	public int getAge() {
		return age;
	}

	@Override
	public String toString() {
		return "Student [name=" + name + ", age=" + age + "]";
	}

}
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;

class Test02 {

	public static void main(String[] args) {
		List<Student> list = new ArrayList<>();		// Java 7的鑽石語法(構造器後面的尖括號中不須要寫類型)
		list.add(new Student("Hao LUO", 33));
		list.add(new Student("XJ WANG", 32));
		list.add(new Student("Bruce LEE", 60));
		list.add(new Student("Bob YANG", 22));
		
		// 經過sort方法的第二個參數傳入一個Comparator接口對象
		// 至關因而傳入一個比較對象大小的算法到sort方法中
		// 因爲Java中沒有函數指針、仿函數、委託這樣的概念
		// 所以要將一個算法傳入一個方法中惟一的選擇就是經過接口回調
		Collections.sort(list, new Comparator<Student> () {

			@Override
			public int compare(Student o1, Student o2) {
				return o1.getName().compareTo(o2.getName());	// 比較學生姓名
			}
		});
		
		for(Student stu : list) {
			System.out.println(stu);
		}
//		輸出結果: 
//		Student [name=Bob YANG, age=22]
//		Student [name=Bruce LEE, age=60]
//		Student [name=Hao LUO, age=33]
//		Student [name=XJ WANG, age=32]
	}
}

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

答:sleep()方法是線程類(Thread)的靜態方法,致使此線程暫停執行指定時間,將執行機會給其餘線程,可是監控狀態依然保持,到時後會自動恢復(線程回到就緒(ready)狀態),由於調用sleep不會釋放對象鎖。wait()是Object 類的方法,對此對象調用wait()方法致使本線程放棄對象鎖(線程暫停執行),進入等待此對象的等待鎖定池,只有針對此對象發出notify方法(或notifyAll)後本線程才進入對象鎖定池準備得到對象鎖進入就緒狀態。

補充:這裏彷佛漏掉了一個做爲先決條件的問題,就是什麼是進程,什麼是線程?爲何須要多線程編程?答案以下所示:

進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,是操做系統進行資源分配和調度的一個獨立單位;線程是進程的一個實體,是CPU調度和分派的基本單位,是比進程更小的能獨立運行的基本單位。線程的劃分尺度小於進程,這使得多線程程序的併發性高;進程在執行時一般擁有獨立的內存單元,而線程之間能夠共享內存。使用多線程的編程一般可以帶來更好的性能和用戶體驗,可是多線程的程序對於其餘程序是不友好的,由於它佔用了更多的CPU資源。

 

5八、sleep()和yield()有什麼區別?

答:

① sleep()方法給其餘線程運行機會時不考慮線程的優先級,所以會給低優先級的線程以運行的機會;yield()方法只會給相同優先級或更高優先級的線程以運行的機會

② 線程執行sleep()方法後轉入阻塞(blocked)狀態,而執行yield()方法後轉入就緒(ready)狀態;

③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常;

④ sleep()方法比yield()方法(跟操做系統相關)具備更好的可移植性。

 

5九、當一個線程進入一個對象的synchronized方法A以後,其它線程是否可進入此對象的synchronized方法?

答:不能。其它線程只能訪問該對象的非同步方法,同步方法則不能進入。

 

60、請說出與線程同步相關的方法。

答:

  1. wait():使一個線程處於等待(阻塞)狀態,而且釋放所持有的對象的鎖;
  2. sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要捕捉InterruptedException 異常;
  3. notify():喚醒一個處於等待狀態的線程,固然在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是由JVM肯定喚醒哪一個線程,並且與優先級無關;
  4. notityAll():喚醒全部處入等待狀態的線程,注意並非給全部喚醒線程一個對象的鎖,而是讓它們競爭;
  5. JDK 1.5經過Lock接口提供了顯式(explicit)的鎖機制,加強了靈活性以及對線程的協調。Lock接口中定義了加鎖(lock())和解鎖(unlock())的方法,同時還提供了newCondition()方法來產生用於線程之間通訊的Condition對象;
  6. JDK 1.5還提供了信號量(semaphore)機制,信號量能夠用來限制對某個共享資源進行訪問的線程的數量。在對資源進行訪問以前,線程必須獲得信號量的許可(調用Semaphore對象的acquire()方法);在完成對資源的訪問後,線程必須向信號量歸還許可(調用Semaphore對象的release()方法)。

下面的例子演示了100個線程同時向一個銀行帳戶中存入1元錢,在沒有使用同步機制和使用同步機制狀況下的執行狀況。

public class Account {
	private double balance;		// 帳戶餘額
	
	/**
	 * 存款
	 * @param money 存入金額
	 */
	public void deposit(double money) {
		double newBalance = balance + money;
		try {
			Thread.sleep(10);	// 模擬此業務須要一段處理時間
		}
		catch(InterruptedException ex) {
			ex.printStackTrace();
		}
		balance = newBalance;
	}
	
	/**
	 * 得到帳戶餘額
	 */
	public double getBalance() {
		return balance;
	}
}
public class AddMoneyThread implements Runnable {
	private Account account;	// 存入帳戶
	private double money;		// 存入金額

	public AddMoneyThread(Account account, double money) {
		this.account = account;
		this.money = money;
	}

	@Override
	public void run() {
		account.deposit(money);
	}

}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Test01 {

	public static void main(String[] args) {
		Account account = new Account();
		ExecutorService service = Executors.newFixedThreadPool(100);
		
		for(int i = 1; i <= 100; i++) {
			service.execute(new AddMoneyThread(account, 1));
		}
		
		service.shutdown();
		
		while(!service.isTerminated()) {}
		
		System.out.println("帳戶餘額: " + account.getBalance());
	}
}

在沒有同步的狀況下,執行結果一般是顯示帳戶餘額在10元如下,出現這種情況的緣由是,當一個線程A試圖存入1元的時候,另一個線程B也可以進入存款的方法中,線程B讀取到的帳戶餘額仍然是線程A存入1元錢以前的帳戶餘額,所以也是在原來的餘額0上面作了加1元的操做,同理線程C也會作相似的事情,因此最後100個線程執行結束時,原本指望帳戶餘額爲100元,但實際獲得的一般在10元如下。解決這個問題的辦法就是同步,當一個線程對銀行帳戶存錢時,須要將此帳戶鎖定,待其操做完成後才容許其餘的線程進行操做,代碼有以下幾種調整方案:

1. 在銀行帳戶的存款(deposit)方法上同步(synchronized)關鍵字

public class Account {
	private double balance;		// 帳戶餘額
	
	/**
	 * 存款
	 * @param money 存入金額
	 */
	public synchronized void deposit(double money) {
		double newBalance = balance + money;
		try {
			Thread.sleep(10);	// 模擬此業務須要一段處理時間
		}
		catch(InterruptedException ex) {
			ex.printStackTrace();
		}
		balance = newBalance;
	}
	
	/**
	 * 得到帳戶餘額
	 */
	public double getBalance() {
		return balance;
	}
}

2. 在線程調用存款方法時對銀行帳戶進行同步

public class AddMoneyThread implements Runnable {
	private Account account;	// 存入帳戶
	private double money;		// 存入金額

	public AddMoneyThread(Account account, double money) {
		this.account = account;
		this.money = money;
	}

	@Override
	public void run() {
		synchronized (account) {
			account.deposit(money);	
		}
	}

}

3.經過JDK 1.5顯示的鎖機制,爲每一個銀行帳戶建立一個鎖對象,在存款操做進行加鎖和解鎖的操

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Account {
	private Lock accountLock = new ReentrantLock();
	private double balance; // 帳戶餘額

	public void deposit(double money) {
		accountLock.lock();
		try {
			double newBalance = balance + money;
			try {
				Thread.sleep(10); // 模擬此業務須要一段處理時間
			}
			catch (InterruptedException ex) {
				ex.printStackTrace();
			}
			balance = newBalance;
		}
		finally {
			accountLock.unlock();
		}
	}

	/**
	 * 得到帳戶餘額
	 */
	public double getBalance() {
		return balance;
	}
}

6一、編寫多線程程序有幾種實現方式?

答:Java 5之前實現多線程有兩種實現方法:一種是繼承Thread類;另外一種是實現Runnable接口。兩種方式都要經過重寫run()方法來定義線程的行爲,推薦使用後者,由於Java中的繼承是單繼承,一個類有一個父類,若是繼承了Thread類就沒法再繼承其餘類了,顯然使用Runnable接口更爲靈活。

補充:Java 5之後建立線程還有第三種方式:實現Callable接口,該接口中的call方法能夠在線程執行結束時產生一個返回值,代碼以下所示

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;


class MyTask implements Callable<Integer> {
	private int upperBounds;
	
	public MyTask(int upperBounds) {
		this.upperBounds = upperBounds;
	}
	
	@Override
	public Integer call() throws Exception {
		int sum = 0; 
		for(int i = 1; i <= upperBounds; i++) {
			sum += i;
		}
		return sum;
	}
	
}

public class Test {

	public static void main(String[] args) throws Exception {
		List<Future<Integer>> list = new ArrayList<>();//callable的返回值要用future來接收
		ExecutorService service = Executors.newFixedThreadPool(10);
		for(int i = 0; i < 10; i++) {
            //採用service註冊的方式將callable實現類註冊到future裏面,靜候結果便可。
			list.add(service.submit(new MyTask((int) (Math.random() * 100))));
		}
		
		int sum = 0;
		for(Future<Integer> future : list) {
			while(!future.isDone()) ;
			sum += future.get();
		}
		
		System.out.println(sum);
	}
}

6二、synchronized關鍵字的用法?

答:synchronized關鍵字能夠將對象或者方法標記爲同步,以實現對對象和方法的互斥訪問,能夠用synchronized(對象) { … }定義同步代碼塊,或者在聲明方法時將synchronized做爲方法的修飾符。在第60題的例子中已經展現了synchronized關鍵字的用法。

 

6三、舉例說明同步和異步。

答:若是系統中存在臨界資源(資源數量少於競爭資源的線程數量的資源),例如正在寫的數據之後可能被另外一個線程讀到,或者正在讀的數據可能已經被另外一個線程寫過了,那麼這些數據就必須進行同步存取數據庫操做中的悲觀鎖就是最好的例子)。當應用程序在對象上調用了一個須要花費很長時間來執行的方法,而且不但願讓程序等待方法的返回時,就應該使用異步編程,在不少狀況下采用異步途徑每每更有效率。事實上,所謂的同步就是指阻塞式操做,而異步就是非阻塞式操做。

 

6四、啓動一個線程是用run()仍是start()方法?

答:啓動一個線程是調用start()方法,使線程所表明的虛擬處理機處於可運行狀態,這意味着它能夠由JVM 調度並執行,這並不意味着線程就會當即運行。run()方法是線程啓動後要進行回調(callback)的方法。

 

6五、什麼是線程池(thread pool)?

答:在面向對象編程中,建立和銷燬對象是很費時間的,由於建立一個對象要獲取內存資源或者其它更多資源。在Java中更是如此,虛擬機將試圖跟蹤每個對象,以便可以在對象銷燬後進行垃圾回收。因此提升服務程序效率的一個手段就是儘量減小建立和銷燬對象的次數,特別是一些很耗資源的對象建立和銷燬,這就是"池化資源"技術產生的緣由。線程池顧名思義就是事先建立若干個可執行的線程放入一個池(容器)中,須要的時候從池中獲取線程不用自行建立,使用完畢不須要銷燬線程而是放回池中,從而減小建立和銷燬線程對象的開銷。

Java 5+中的Executor接口定義一個執行線程的工具。它的子類型即線程池接口是ExecutorService。要配置一個線程池是比較複雜的,尤爲是對於線程池的原理不是很清楚的狀況下,所以在工具類Executors面提供了一些靜態工廠方法,生成一些經常使用的線程池,以下所示:

  • newSingleThreadExecutor:建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。
  • newFixedThreadPool:建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。
  • newCachedThreadPool:建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池不會對線程池大小作限制,線程池大小徹底依賴於操做系統(或者說JVM)可以建立的最大線程大小。
  • newScheduledThreadPool:建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。
  • newSingleThreadExecutor:建立一個單線程的線程池。此線程池支持定時以及週期性執行任務的需求。

第60題的例子中有經過Executors工具類建立線程池並使用線程池執行線程的代碼。若是但願在服務器上使用線程池,強烈建議使用newFixedThreadPool方法來建立線程池,這樣能得到更好的性能

6六、線程的基本狀態以及狀態之間的關係?

除去起始(new)狀態和結束(finished)狀態,線程有三種狀態,分別是:就緒(ready)、運行(running)和阻塞(blocked)。其中就緒狀態表明線程具有了運行的全部條件,只等待CPU調度(萬事俱備,只欠東風);處於運行狀態的線程可能由於CPU調度(時間片用完了)的緣由回到就緒狀態,也有可能由於調用了線程的yield方法回到就緒狀態,此時線程不會釋放它佔有的資源的鎖,坐等CPU以繼續執行;運行狀態的線程可能由於I/O中斷、線程休眠、調用了對象的wait方法而進入阻塞狀態(有的地方也稱之爲等待狀態);而進入阻塞狀態的線程會由於休眠結束、調用了對象的notify方法或notifyAll方法或其餘線程執行結束而進入就緒狀態。注意:調用wait方法會讓線程進入等待池中等待被喚醒,notify方法或notifyAll方法會讓等待鎖中的線程從等待池進入等鎖池,在沒有獲得對象的鎖以前,線程仍然沒法得到CPU的調度和執行。

 

6七、簡述synchronized 和java.util.concurrent.locks.Lock的異同?

答:Lock是Java 5之後引入的新的API,和關鍵字synchronized相比主要相同點:Lock 能完成synchronized所實現的全部功能;主要不一樣點:Lock 有比synchronized 更精確的線程語義和更好的性能。synchronized 會自動釋放鎖,而Lock 必定要求程序員手工釋放,而且必須在finally 塊中釋放(這是釋放外部資源的最好的地方)。

 

6八、Java中如何實現序列化,有什麼意義? 

答:序列化就是一種用來處理對象流的機制,所謂對象流也就是將對象的內容進行流化。能夠對流化後的對象進行讀寫操做,也可將流化後的對象傳輸於網絡之間。序列化是爲了解決對象流讀寫操做時可能引起的問題(若是不進行序列化可能會存在數據亂序的問題)。

要實現序列化,須要讓一個類實現Serializable接口,該接口是一個標識性接口,標註該類對象是可被序列化的,而後使用一個輸出流來構造一個對象輸出流並經過writeObject(Object obj)方法就能夠將實現對象寫出(即保存其狀態);若是須要反序列化則能夠用一個輸入流創建對象輸入流,而後經過readObject方法從流中讀取對象。序列化除了可以實現對象的持久化以外,還可以用於對象的深度克隆(參見Java面試題集1-29題)

 

6九、Java 中有幾種類型的流?

答:字節流,字符流。字節流繼承於InputStream、OutputStream,字符流繼承於Reader、Writer。在java.io 包中還有許多其餘的流,主要是爲了提升性能和使用方便。

補充:關於Java的IO須要注意的有兩點:一是兩種對稱性(輸入和輸出的對稱性,字節和字符的對稱性);二是兩種設計模式(適配器模式和裝潢模式)。另外Java中的流不一樣於C#的是它只有一個維度一個方向。

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class MyUtil {

	private MyUtil() {
		throw new AssertionError();
	}
	
	public static void fileCopy(String source, String target) throws IOException {
		try (InputStream in = new FileInputStream(source)) {
			try (OutputStream out = new FileOutputStream(target)) {
				byte[] buffer = new byte[4096];
				int bytesToRead;
				while((bytesToRead = in.read(buffer)) != -1) {
					out.write(buffer, 0, bytesToRead);
				}
			}
		}
	}
	
	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();
				}
			}
		}
	}
}
相關文章
相關標籤/搜索