android面試(1)-Java基礎
最近需要面臨找工作的壓力,所以在寒假的時候惡補了一下關於android方面的知識,這是一個系列的部落格,希望自己可以堅持更新下去。
今天找了一些Java基礎的面試題,我在裡面挑選了一些我還有些模糊的題,在此處記錄一下:
1.面向物件的特徵:
(1)抽象:將一類物件的共同特徵總結出來構造類的過程。
(2)繼承:從已有類得到繼承資訊,建立新類的過程。
(3)封裝:把資料和操作資料的方法繫結起來,對資料的訪問只能通過已定義的介面。
(4)多型:允許不同子型別的物件對同一訊息做出不同的響應。
2.float f=3.4是否正確?
這是一個比較經典的問題了,答案相信大家都已經知道了,不正確,當然的解釋就是3.4是雙精度數,也就是double型,將double轉換為float需要強轉,也就是float f=(float)3.4;當時我看到解釋後還是很懵逼,我就想知道,為啥3.4就是double型呢?後來我才明白,java對於一個小數的初始預設型別就是double型,所以一切都明白了;
3.A. short s1=1;s1=s1+1;與B. short s1=1;s1+=1;的區別
這一題只要弄清楚兩點,第一就是,1是int型;第二就是,s1+=1相當於s1=(short)(s1+1);這樣就知道,A式是錯誤的,s1+1是int型的,賦值給s1需要強轉;B式相應就是正確的了;
4.Java中的記憶體管理機制:
Java中把記憶體大致分為三個部分:
(1)方法區:也叫靜態儲存區,在這裡儲存的是直接書寫的常量,靜態資料,全域性變數,在這裡的資料在整個程式執行時一直都會存在。
(2)棧區:方法體內的區域性變數,基本資料型別的變數,物件的引用等;
(3)堆區:也叫動態記憶體分配,new 和 構造器建立的物件儲存在這裡;
舉個例子: String str=new String(“hello”);
str這個引用儲存在棧上,new出來的字串物件儲存在堆上,hello這個字串儲存在方法區內;
5.一些容易被忽略的小點:
(1)Math.round(11.5)=12; Math.round(-11.5)=-11; 向大值取整;
(2)如果兩個物件 x,y滿足x.equals(y)=true;那麼他們的雜湊碼也應該相同;
(3)String是final類,無法被繼承;
(4)Java中方法呼叫僅支援值傳遞;
6.String ,StringBuilder和StringBuffer的區別:
(1)String為只讀字串,String引用的字串內容是不能改變的,StringBuffer和StringBuilder的值可以直接修改;
(2)StringBulider與StringBuffer的方法完全一樣,但是StringBulider的所有方法沒有被Synchronized修飾,所以是單執行緒,是執行緒不安全的,但是其效率最高;
(3)String賦予新值會重新開闢記憶體地址,而StringBuffer和StringBuilder則採用append和insert等方法來改變字串的值,是在原有物件的記憶體上進行操作,所以大量字串拼接時採用StringBuffer或StringBuilder,少量可採用String的“+”;
(4)這裡還有一個比較偏的點:請說出下面程式的輸出:
1 2 3 4 5 6 7 8 9 10 11 | class StringEqualTest { public static void main(String[] args) { String s1 = "Programming"; String s2 = new String("Programming"); String s3 = "Program" + "ming"; System.out.println(s1 == s2); System.out.println(s1 == s3); System.out.println(s1 == s1.intern()); } } |
正確答案是:false, true ,true;
第一個很好理解,s1和s2的引用不一樣,當然是false
第二個我也很納悶為什麼會是true,原來啊,String的+被Java進行了特殊處理,在運算過程中會使用StringBuilder或者StringBuffer代替求和,而這兩個在進行字串的運算時,不會改變其記憶體地址,記憶體中已經存在了Programming這個字串,那麼就不會重新開闢記憶體地址,所以是true;
第三個要知道intern方法的使用,String物件的intern方法會得到字串物件在常量池中對應的版本的引用(如果常量池中有一個字串與String物件的equals結果是true),如果常量池中沒有對應的字串,則該字串將被新增到常量池中,然後返回常量池中字串的引用。
7.過載(OverLoad)和重寫(Override)的區別:
這個大部分人應該都清楚,這裡我總結一下:
(1)過載實現的是編譯時的多型,重寫實現的執行時的多型,所謂的編譯時的多型就是在編譯時就能確定執行的是哪個多型方法,否則就是執行時的多型。
(2)過載發生在一個類中,同名方法具有不同的引數列表(引數型別不同,引數個數不同),重寫發生在子類與父類中,子類被重寫的方法與父類被重寫的方法具有相同的返回型別,比父類要更好訪問,不能比父類有更多的異常
這裡有一個經典的面試題,為什麼函式過載不能根據返回型別區分?
答案:因為函式呼叫時不會指定型別資訊,編譯器不知道你到底要呼叫那個方法。
8.描述一下JVM載入class檔案的原理機制:
這個問題設計的東西比較多,在回答這個問題前,我們要知道一些知識點:
(1)Java的類載入器:
A.BootStrop:根載入器,負責載入JVM基礎核心類庫;
B.Extension:擴充套件類載入器,從java.ext.dirs系統屬性所指定的目錄中載入類庫,它的父載入器是Bootstrap;
C.System:又叫應用類載入器,其父類是Extension。它是應用最廣泛的類載入器。它從環境變數classpath或者系統屬性java.class.path所指定的目錄中記載類,是使用者自定義載入器的預設父載入器。
(2)由於Java的跨平臺性,經過編譯的Java源程式並不是一個可執行程式,而是一個或多個類檔案。當Java程式需要使用某個類時,JVM會確保這個類已經被載入、連線(驗證、準備和解析)和初始化。
A.載入:把類的.class檔案中的資料讀入到記憶體中,通常是建立一個位元組陣列讀入.class檔案,然後產生與所載入類對應的Class物件。
B.連線:經過載入後的Class物件還不可用,進入連線階段,此階段又分為三個步驟:
a.驗證:檢查二進位制位元組碼是否正確合格;
b.準備:給類的類變數分配記憶體並初始化;
c.解析:將常量池的符號引用轉換為直接引用;
C.初始化:最後一步對類進行初始化,遵循“先父後子”,“先靜態後普通”的原則,依次執行初始化語句。
9.如何實現物件的克隆?
(1)實現cloneable介面並重寫clone()方法;(淺克隆,只能複製值型別的成員變數,引用型別的無法複製)
class Student implements Cloneable{
private int number;
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
@Override
public Object clone() {
Student stu = null;
try{
stu = (Student)super.clone();
}catch(CloneNotSupportedException e) {
e.printStackTrace();
}
return stu;
}
}
public class Test {
public static void main(String args[]) {
Student stu1 = new Student();
stu1.setNumber(12345);
Student stu2 = (Student)stu1.clone();
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
stu2.setNumber(54321);
System.out.println("學生1:" + stu1.getNumber());
System.out.println("學生2:" + stu2.getNumber());
}
}
(2)實現Serializable介面;(深克隆,真正的實現物件克隆)
public class Outer implements Serializable{
private static final long serialVersionUID = 369285298572941L; //最好是顯式宣告ID
public Inner inner;
//Discription:[深度複製方法,需要物件及物件所有的物件屬性都實現序列化]
public Outer myclone() {
Outer outer = null;
try { // 將該物件序列化成流,因為寫在流裡的是物件的一個拷貝,而原物件仍然存在於JVM裡面。所以利用這個特性可以實現物件的深拷貝
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(this);
// 將流序列化成物件
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bais);
outer = (Outer) ois.readObject();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return outer;
}
}
public class Inner implements Serializable{
private static final long serialVersionUID = 872390113109L; //最好是顯式宣告ID
public String name = "";
public Inner(String name) {
this.name = name;
}
@Override
public String toString() {
return "Inner的name值為:" + name;
}
}
10.將GB2312編碼的字串轉換為ISO-8859-1編碼的字串:
String s1=“你好”;
String s2=new String(s1,getBytes("GB2312","ISO-8859-1"));
11.ArrayList,Vector,LinkedList的儲存效能和特性:
(1)ArrayList和Vector底層都是使用陣列作為儲存資料的方式,具有索引速度快,增刪速度慢的特點,且由於Vector內部使用了Synchronized關鍵字,所以效能會較差;
(2)LinkedList底層是雙向連結串列,具有查詢索引速度慢,插入刪除速度快的特點,且為執行緒不安全的;
12.TreeMap與TreeSet中比較元素的方法:
TreeSet要求物件類必須實現Comparable介面,提供了CompareTo()方法;
TreeMap要求存放的鍵值對中的鍵必須實現Comparable介面;
13.Sleep()與yield()的區別:
(1)sleep()給其他執行緒執行機會不會考慮執行緒的優先順序,yield()只會給相同優先順序或者更高的優先順序的執行緒以執行機會;
(2)sleep()後轉入阻塞狀態,yield()後轉入就緒狀態;
(3)sleep()丟擲InterruptedException異常,而yield()不會;
(4)sleep()比yield()具有更好地移植性;
14.執行緒池的有關知識:
(1)核心類:ThreadPoolExecutor類;
(2)構造方法:public ThreadPoolExecutor(int corePoolsize,int maxnumpoolsize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedException handler);
(3)7大引數:
A.corePoolsize:核心執行緒數;
B.maxnumpoolsize:最大執行緒數;
C.keepAliveTime:保持活躍的時間,執行緒無任務時多長時間被終止;
D.TimeUnit:時間單位;
E.workQueue:阻塞佇列,一般使用LinkedBlockingQueue和SynchronizeQueue
F.threadFactory:執行緒工廠,建立執行緒;
G.handler:表示拒絕處理任務是的策略;
(4)關鍵方法:
A.execute():提交任務時執行;
B.submit():可返回任務執行結果的提交任務的方法;
C.shutdown()/shutdownNow():關閉執行緒池;
(5) 四大執行緒池:
A.newCachedThreadPool:可快取執行緒池,可回收空閒執行緒;
B.newFixedThreadPool:定長執行緒池,控制最大併發數;
C.newScheduleThreadPool:定長定時週期性執行緒池;
D.newSingleThreadPool:單執行緒執行緒池;
例子一個:
public static class ThreadPool { private int corePoolSize;//核心執行緒數 private int maximumPoolSize;//最大執行緒數 private long keepAliveTime;//休息時間 private ThreadPool(int corePoolSize,int maximumPoolSize,long keepAliveTime) { this.corePoolSize=corePoolSize; this.maximumPoolSize=maximumPoolSize; this.keepAliveTime=keepAliveTime; } public void execute(Runnable r) { if (executor==null){ executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); } executor.execute(r); } public void cancle(Runnable r){ if (executor!=null){ //從執行緒佇列中移除物件 executor.getQueue().remove(r); } } }
15.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),這裡可能還會牽扯到壓縮,以便為大物件騰出足夠的空間。
(1)Lock可完成Synchronize的所有功能
(2)Lock有比Synchronized更精確的執行緒語義和更好地效能,不強制一定要獲得鎖,lock必須手動釋放鎖;
(3)Synchronized是Java內建的關鍵字,而Lock是java的一個介面類;
(4)Synchronized在發生異常時會自動釋放鎖,而lock必須手動釋放鎖;
(5)Lock可以讓等待鎖的執行緒相應中斷,而Synchronized無法辦到;
(6)Lock可以知道是否獲取到了鎖,而Synchronized無法辦到;
(7)Lock可以提高多執行緒進行讀操作的效率,而Synchronized不可以;
17.程式設計實現檔案拷貝:
(1)public static void fileCopy(String source,String target) throws IOException{
try(InputStream in=new FileInputStream(source)){
try(OutputStream out=new FileOutputStream(target)){
byte[] bs=new byte[4096];
int count=0;
while((count=in.read(bs))!=-1){
out.write(bs, 0, count);
}
}
}
}
Java NIO實現:
(2)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();
}
}
}
}
18.統計字串在檔案中出現的次數:
public static int countWordInFile(String fileName,String word) throws IOException {
int count=0;
File file=new File(fileName);
try(FileReader reader=new FileReader(file)){
try(BufferedReader bufferedReader=new BufferedReader(reader)){
String line=null;
while((line=bufferedReader.readLine())!=null){
int index=-1;
while((line.length()>word.length())&&(index=line.indexOf(word))>0){
count++;
line=line.substring(index+word.length());
}
}
}
}
return count;
}
19.列出目錄所有檔案:
public static void showDirectory(String pathname) {
File file=new File(pathname);
for(File temp:file.listFiles()){
if (temp.isFile()) {
System.out.println(temp.getName());
}
}
}
20.關於網路程式設計:用Java的套接字程式設計實現一個多執行緒的回顯(echo)伺服器。
伺服器端:
中心思想:在主執行緒中開啟一個死迴圈,死迴圈中開啟一個待命的執行緒,用於接收客戶端傳送過來的訊息;
public class SocketServer {
public static void main(String[] args) {
try(ServerSocket serverSocket=new ServerSocket(6789)){
System.out.println("伺服器已開啟...");
while(true){
Socket socket=serverSocket.accept();
new Thread(new ClientHandler(socket)).start();
}
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}
}
private static class ClientHandler implements Runnable{
private Socket client;
public ClientHandler(Socket client) {
this.client=client;
}
@Override
public void run() {
// TODO Auto-generated method stub
try(BufferedReader bufferedReader=new BufferedReader(new InputStreamReader(client.getInputStream()))){
PrintWriter pWriter=new PrintWriter(client.getOutputStream());
String msg=bufferedReader.readLine();
System.out.println(msg+"由"+client.getInetAddress()+"發出");
pWriter.println(msg);
pWriter.flush();
}catch (Exception e) {
// TODO: handle exception
e.printStackTrace();
}finally {
try {
client.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
客戶端:
中心思想:通過socket.getOutputStream()得到寫出字元流,將訊息傳送出去;
public class ScoketClient {
public static void main(String[] args) throws IOException{
Socket socket=new Socket("localhost", 6789);
Scanner scanner=new Scanner(System.in);
System.out.println("請輸入傳送的資訊");
String msg=scanner.nextLine();
scanner.close();
PrintWriter pWriter=new PrintWriter(socket.getOutputStream());
pWriter.println(msg);
pWriter.flush();
BufferedReader br=new BufferedReader(new InputStreamReader(socket.getInputStream()));
System.out.println("收到:"+br.readLine());
socket.close();
}
}
21.正則表示式:
\b:匹配單詞的開頭和結尾,單詞分界處;
\w:匹配所有字母或數字或下劃線或漢字;
\d:匹配所有數字;
\s:匹配空白符;
^:表示單詞的開始;$:表示單詞結束;
.:除了換行符以外的任意字元;
*:前面的內容可以連續重複使用任意次以使整個表示式得到匹配;
{2}:連續重複匹配2次;
?:匹配0次或1次;
+:匹配1次或更多;
(?=exp):匹配exp前面的位置;
(?<=exp):匹配exp後面的位置;
一個栗子:
如果要從字串中擷取第一個英文左括號之前的字串,例如:北京市(朝陽區)(西城區)(海淀區),擷取結果為:北京市,那麼正則表示式怎麼寫?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import java.util.regex.Matcher; import java.util.regex.Pattern; class RegExpTest { public static void main(String[] args) { String str = "北京市(朝陽區)(西城區)(海淀區)"; Pattern p = Pattern.compile(".*?(?=\\()"); Matcher m = p.matcher(str); if(m.find()) { System.out.println(m.group()); } } } |
中心思想:
可以通過類物件的getDeclaredField()方法欄位(Field)物件,然後再通過欄位物件的setAccessible(true)將其設定為可以訪問,接下來就可以通過get/set方法來獲取/設定欄位的值了。
public class ReflectionUtils {private ReflectionUtils() {
throw new AssertionError();
}
public static Object getValue(Object target,String fieldName) {
Class<?> clazz=target.getClass();
String[] fs=fieldName.split("\\.");
try{
for(int i=0;i<fs.length-1;i++){
Field field=clazz.getDeclaredField(fs[i]);
field.setAccessible(true);
target=field.get(target);
clazz=target.getClass();
}
Field f =clazz.getDeclaredField(fs[fs.length-1]);
f.setAccessible(true);
return f.get(target);
}catch (Exception e) {
throw new RuntimeException();
}
}
public static void setValue(Object target,String fieldName,Object value) {
Class<?> clazz=target.getClass();
String[] fs=fieldName.split("\\.");
try {
for(int i=0;i<fs.length-1;i++){
Field field=clazz.getDeclaredField(fs[i]);
Object val=field.get(target);
field.setAccessible(true);
if (val==null) {
Constructor<?> constructor=field.getType().getDeclaredConstructor();
constructor.setAccessible(true);
val=constructor.newInstance();
field.set(target, val);
}
target=val;
clazz=field.getClass();
}
Field field=clazz.getDeclaredField(fs[fs.length-1]);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
// TODO: handle exception
}
}
public static void main(String[] args) throws Exception{
String aString="hello";
Method method=aString.getClass().getMethod("split",String.class);
System.out.println(method.invoke(aString,"e"));
}
}
23.氣泡排序:(逼格灰常高)
它重複地走訪過要排序的數列,一次比較兩個元素,如果他們的順序錯誤就把他們交換過來。走訪數列的工作是重複地進行直到沒有再需要交換,也就是說該數列已經排序完成。
(1)先將演算法封裝到具有共同介面的獨立的類中使得它們可以相互替換
public interface Sort {
/**
* 排序器介面(策略模式: 將演算法封裝到具有共同介面的獨立的類中使得它們可以相互替換)
*/
/**
* 排序
* @param list 待排序的陣列
*/
public <T extends Comparable<T>> void sort(T[] list);
/**
* 排序
* @param list 待排序的陣列
* @param comp 比較兩個物件的比較器
*/
public <T> void sort(T[] list, Comparator<T> comp);
}
(2)寫工具類實現上述介面:
public class BubbleSorter implements Sort{
@Override
public <T extends Comparable<T>> void sort(T[] list) {
// TODO Auto-generated method stub
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (list[j].compareTo(list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
@Override
public <T> void sort(T[] list, Comparator<T> comp) {
// TODO Auto-generated method stub
boolean swapped = true;
for (int i = 1, len = list.length; i < len && swapped; ++i) {
swapped = false;
for (int j = 0; j < len - i; ++j) {
if (comp.compare(list[j], list[j + 1]) > 0) {
T temp = list[j];
list[j] = list[j + 1];
list[j + 1] = temp;
swapped = true;
}
}
}
}
}
24.折半查詢:
折半查詢,也稱二分查詢、二分搜尋,是一種在有序陣列中查詢某一特定元素的搜尋演算法。搜素過程從陣列的中間元素開始,如果中間元素正好是要查詢的元素,則搜素過程結束;如果某一特定元素大於或者小於中間元素,則在陣列大於或小於中間元素的那一半中查詢,而且跟開始一樣從中間元素開始比較。如果在某一步驟陣列已經為空,則表示找不到指定的元素。這種搜尋演算法每一次比較都使搜尋範圍縮小一半,其時間複雜度是O(logN)。
/*** 使用迴圈實現的二分查詢
* @param x
* @param key
* @param comp
* @return
*/
private static <T> int binarySearch(T[] x,T key,Comparator<T> comp) {
int low=0;
int high=x.length-1;
while(low<=high){
int mid=(high-low)>>>1;
int temp=comp.compare(x[mid], key);
if (temp>0) {
high=mid-1;
}else if(temp<0){
low=mid+1;
}else {
return mid;
}
}
return -1;
}
/**
* 使用遞迴實現的二分查詢
* @param x
* @param low
* @param high
* @param key
* @return
*/
private static <T extends Comparable<T>> int binarySearch(T[] x,int low,int high,T key){
if(low<=high){
int mid=low+((high-low)>>1);
if(key.compareTo(x[mid])==0){
return mid;
}else if(key.compareTo(x[mid])<0){
return binarySearch(x, low, mid-1, key);
}else {
return binarySearch(x, mid+1, high, key);
}
}
return -1;
}