1. 程式人生 > >【面試】面向物件相關-這一篇全瞭解

【面試】面向物件相關-這一篇全瞭解

1、什麼是面向物件,什麼是面向過程。面向物件的三大基本特徵和五大基本原則是什麼?

解:

什麼是面向過程?

把問題分解成一個一個步驟,每個步驟用函式實現,依次呼叫即可。就是說,在進行面向過程程式設計的時候,不需要考慮那麼多,上來先定義一個函式,然後使用各種諸如if-else、for-each等方式進行程式碼執行。最典型的用法就是實現一個簡單的演算法,比如實現氣泡排序。(自己在家做飯)

什麼是面向物件?

將問題分解成一個一個步驟,對每個步驟進行相應的抽象,形成物件,通過不同物件之間的呼叫,組合解決問題。就是說,在進行面向物件進行程式設計的時候,要把屬性、行為等封裝成物件,然後基於這些物件及物件的能力進行業務邏輯的實現。比如想要造一輛車,上來要先把車的各種屬性定義出來,然後抽象成一個Car類。(點外賣)

面向物件的三大基本特徵和五大基本原則?

三大基本特徵:封裝、繼承(上次外賣點過的再來一單)、多型(可以對上次點的在購物車中在增刪修改)。

五大基本原則:單一職責原則(Single-Responsibility Principle)、開放封閉原則(Open-Closed principle)、Liskov替換原則(Liskov-Substituion Principle)、依賴倒置原則(Dependency-Inversion Principle)和 介面隔離原則(Interface-Segregation Principle)。關於五大基本原則的具體內容參見我的博文:連結:牢記面向物件五個基本原則-HollisChuang's Blog

後話:

這些東西雖然都是概念性的,但是很多程式設計師根本不知道,更別提如何更好的面向物件程式設計了,我就看過有些同學寫的程式碼,從頭到尾一個函式,各種if-else,完全不考慮複用、擴充套件等。這種程式碼非常不利於閱讀和維護。希望球友們不僅記住上面的概念,還要通匯貫通,切實的運用到平時的工作中。

2、Java和C++同為面嚮物件語言,Java和C++主要區別有哪些?雙方個有哪些優缺點?

解:

C++ 被設計成主要用在系統性應用程式設計上的語言,對C語言進行了擴充套件。對於C語言這個為執行效率設計的過程式程式設計語言, C++ 特別加上了以下這些特性的支援:靜態型別的面向物件程式設計的支援、異常處理、RAII以及泛型。另外它還加上了一個包含泛型容器和演算法的C++庫函式。

Java 最開始是被設計用來支援網路計算。它依賴一個虛擬機器來保證安全和可移植性。Java包含一個可擴充套件的庫用以提供一個完整的的下層平臺的抽象。Java是一種靜態面嚮物件語言,它使用的語法類似C++,但與之不相容。為了使更多的人到使用更易用的語言,它進行了全新的設計。

其實,最主要的區別是他們分別代表了兩種型別的語言:

C++是編譯型語言(首先將原始碼編譯生成機器語言,再由機器執行機器碼),執行速度快、效率高;依賴編譯器、跨平臺性差些。

Java是解釋型語言(原始碼不是直接翻譯成機器語言,而是先翻譯成中間程式碼,再由直譯器對中間程式碼進行解釋執行。),執行速度慢、效率低;依賴直譯器、跨平臺性好。

PS:也有人說Java是半編譯、半解釋型語言。Java 編譯器(javac)先將java源程式編譯成Java位元組碼(.class),JVM負責解釋執行位元組碼檔案。

二者更多的主要區別如下:

C++是平臺相關的,Java是平臺無關的。

C++對所有的數字型別有標準的範圍限制,但位元組長度是跟具體實現相關的,不同作業系統可能。Java在所有平臺上對所有的基本型別都有標準的範圍限制和位元組長度。

C++除了一些比較少見的情況之外和C語言相容 。 Java沒有對任何之前的語言向前相容。但在語法上受 C/C++ 的影響很大

C++允許直接呼叫本地的系統庫 。 Java要通過JNI呼叫, 或者 JNAC++允許過程式程式設計和麵向物件程式設計 。Java必須使用面向物件的程式設計方式C++支援指標,引用,傳值呼叫 。Java只有值傳遞。

C++需要顯式的記憶體管理,但有第三方的框架可以提供垃圾蒐集的支援。支援解構函式。

Java 是自動垃圾收集的。沒有解構函式的概念。C++支援多重繼承,包括虛擬繼承 。

Java只允許單繼承,需要多繼承的情況要使用介面。

3、什麼是平臺無關性,Java是如何做到平臺無關的?

解:

跨平臺指的是一種語言在計算機上的執行不受平臺的約束,一次編譯,到處執行。 平臺無關有兩種:原始碼級和目的碼級。

我們常說的跨平臺,或者平臺無關,指的就是目的碼,或者說是軟體交付件跨平臺。

C和C++具有一定程度的原始碼級平臺無關,表明用C或C++寫的應用程式不用修改只需重新編譯就可以在不同平臺上執行。但是,關鍵是要重新編譯。可是,一般軟體交付都是給你個成品,對於C或者C++開發出的軟體,只能執行在某個平臺的。沒有原始碼,怎麼編譯。

Java編譯出來的是位元組碼,去到哪個平臺都能用,只要有那個平臺的JDK就可以執行,所以,Java程式的最大優勢就是平臺無關。對於Java,交付的就是一堆jar包或者war包,只要系統上有個Java虛擬機器,就可以直接執行,這不就是跨平臺了麼。

我們使用的C、C++還有Java等高階語言,都需要最終被編譯成機器語言,才能被計算機執行。

C語言和C++語言的編譯過程是把原始碼編譯生成機器語言。這樣機器可以直接執行。但是不同系統對同一段“機器語言”的處理結果可能是不一樣的,原因可能有很多,比如CPU的指令集不同的。C語言不能實現跨平臺執行,就是因為它編譯出來的檔案的格式,只適用於某種cpu,其他cpu無法識別。

那麼Java是如何實現跨平臺的呢?

我們編寫的Java原始碼,編譯後會生成一種 .class 檔案,稱為位元組碼檔案。這種位元組碼檔案需要經過JVM虛擬機器,然後翻譯成機器語言,才能被機器執行。

那麼,由於我們可以在不同的作業系統上安裝不同的JVM,而JVM封裝了所有對於.class檔案的處理,即JVM幫我們把位元組碼翻譯成機器語言的過程中就已經充分考慮到對應平臺的特性了。比如,一個.class檔案,在不同機器上最終生成的機器語言可能是不同的,但是這種不同不需要我們關心,JVM會保證他可以正常執行,完整的表達正確的程式語義。

4、什麼是值傳遞,什麼是引用傳遞。為什麼說Java中只有值傳遞

5、 什麼是編譯,什麼是反編譯。Java的編譯和反編譯方法。

解:

在介紹編譯和反編譯之前,我們先來簡單介紹下程式語言(Programming Language)。程式語言(Programming Language)分為低階語言(Low-level Language)和高階語言(High-level Language)。

機器語言(Machine Language)和組合語言(Assembly Language)屬於低階語言,直接用計算機指令編寫程式。

而C、C++、Java、Python等屬於高階語言,用語句(Statement)編寫程式,語句是計算機指令的抽象表示。

上面提到語言有兩種,一種低階語言,一種高階語言。可以這樣簡單的理解:低階語言是計算機認識的語言、高階語言是程式設計師認識的語言。

那麼如何從高階語言轉換成低階語言呢?這個過程其實就是編譯。將便於人編寫、閱讀、維護的高階計算機語言所寫作的原始碼程式,翻譯為計算機能解讀、執行的低階機器語言的程式的過程就是編譯。負責這一過程的處理的工具叫做編譯器。

反編譯的過程與編譯剛好相反,就是將已編譯好的程式語言還原到未編譯的狀態,也就是找出程式語言的原始碼。就是將機器看得懂的語言轉換成程式設計師可以看得懂的語言。Java語言中的反編譯一般指將class檔案轉換成java檔案。

Java語言中負責編譯的編譯器是一個命令:javac

javac是收錄於JDK中的Java語言編譯器。該工具可以將字尾名為.java的原始檔編譯為字尾名為.class的可以運行於Java虛擬機器的位元組碼。

當我們寫完一個HelloWorld.java檔案後,我們可以使用javac HelloWorld.java命令來生成HelloWorld.class檔案,這個class型別的檔案是JVM可以識別的檔案。通常我們認為這個過程叫做Java語言的編譯。其實,class檔案仍然不是機器能夠識別的語言,因為機器只能識別機器語言,還需要JVM再將這種class檔案型別位元組碼轉換成機器可以識別的機器語言。

Java中反編譯工具有多個,如javap ,jad,CRF等。

6、語法糖

7、Java 8中的lambda表示式是不是語法糖,具體如何實現的。

解:

關於lambda表示式,網上有人說他並不是語法糖,也有人說他是匿名內部類的語法糖。其實我想糾正下這個說法。Labmda表示式不是匿名內部類的語法糖,但是他也是一個語法糖。實現方式其實是依賴了幾個JVM底層提供的lambda相關api。

先來看一個簡單的lambda表示式。遍歷一個list:

public static void main(String... args) {

     List<String> strList = ImmutableList.of("Hollis", "公眾號:Hollis", "部落格:www.hollischuang.com");

     strList.forEach( s -> { System.out.println(s); } );

}

為啥說他並不是內部類的語法糖呢,#直面Java#的第006期介紹過的,內部類在編譯之後會有兩個class檔案,但是,包含lambda表示式的類編譯後只有一個檔案。

反編譯後代碼如下:

public static /* varargs */ void main(String ... args) {

        ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");

 strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());

}

private static /* synthetic */ void lambda$main$0(String s) {

       System.out.println(s);

}

可以看到,在forEach方法中,其實是呼叫了java.lang.invoke.LambdaMetafactory#metafactory方法,該方法的第四個引數implMethod指定了方法實現。可以看到這裡其實是呼叫了一個lambda$main$0方法進行了輸出。

再來看一個稍微複雜一點的,先對List進行過濾,然後再輸出:

public static void main(String... args) {

List<String> strList = ImmutableList.of("Hollis", "公眾號:Hollis", "部落格:www.hollischuang.com");

List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList());

HollisList.forEach( s -> { System.out.println(s); } ); }

反編譯後代碼如下:

public static /* varargs */ void main(String ... args) {

ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");

List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());

HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)()); }

private static /* synthetic */ void lambda$main$1(Object s) {

System.out.println(s);

}

private static /* synthetic */ boolean lambda$main$0(String string) {

return string.contains("Hollis");

}

兩個lambda表示式分別呼叫了`lambda$main$1`和`lambda$main$0`兩個方法。 所以,lambda表示式的實現其實是依賴了一些底層的api,在編譯階段,編譯器會把lambda表示式進行解糖,轉換成呼叫內部api的方式。

8、什麼是多型,多型有什麼好處,多型的必要條件是什麼、Java中多型的實現方式

解:

多型的概念比較簡單,就是同一操作作用於不同的物件,可以有不同的解釋,產生不同的執行結果。

如果按照這個概念來定義的話,那麼多型應該是一種執行期的狀態。

為了實現執行期的多型,或者說是動態繫結,需要滿足三個條件。

即有類繼承或者介面實現、子類要重寫父類的方法、父類的引用指向子類的物件。

簡單來一段程式碼解釋下:

public class Parent{

public void call(){

sout("im Parent");

}}

public class Son extends Parent{// 1.有類繼承或者介面實現

public void call(){// 2.子類要重寫父類的方法sout("im Son");

}}

public class Daughter extends Parent{// 1.有類繼承或者介面實現

public void call(){// 2.子類要重寫父類的方法sout("im Daughter");

}}

public class Test{

public static void main(String[] args){

Parent p = new Son(); //3.父類的引用指向子類的物件

Parent p1 = new Daughter(); //3.父類的引用指向子類的物件

}}

這樣,就實現了多型,同樣是Parent類的例項,p.call 呼叫的是Son類的實現、p1.call呼叫的是Daughter的實現。

有人說,你自己定義的時候不就已經知道p是son,p1是Daughter了麼。但是,有些時候你用到的物件並不都是自己宣告的啊 。

比如Spring 中的IOC出來的物件,你在使用的時候就不知道他是誰,或者說你可以不用關心他是誰。根據具體情況而定。

另外,還有一種說法,包括維基百科也說明,動態還分為動態多型和靜態多型。

上面提到的那種動態繫結認為是動態多型,因為只有在執行期才能知道真正呼叫的是哪個類的方法。

還有一種靜態多型,一般認為Java中的函式過載是一種靜態多型,因為他需要在編譯期決定具體呼叫哪個方法、

關於這個動態靜態的說法,我更偏向於過載和多型其實是無關的。

但是也要看情況,普通場合,我會認為只有方法的重寫算是多型,畢竟這是我的觀點。但是如果在面試的時候,我“可能”會認為過載也算是多型,畢竟面試官也有他的觀點。我會和麵試官說:我認為,多型應該是一種執行期特性,Java中的重寫是多型的體現。不過也有人提出過載是一種靜態多型的想法,這個問題在StackOverflow等網站上有很多人討論,但是並沒有什麼定論。我更加傾向於過載不是多型。

這樣溝通,既能體現出你瞭解的多,又能表現出你有自己的思維,不是那種別人說什麼就是什麼的。

9、什麼是方法重寫,什麼是方法過載,成員變數可以被重寫嗎?

解:

過載(Overloading)和重寫(Overriding)是Java中兩個比較重要的概念。但是對於新手來說也比較容易混淆。

過載

簡單說,就是函式或者方法有同樣的名稱,但是引數列表不相同的情形,這樣的同名不同引數的函式或者方法之間,互相稱之為過載函式或者方法。

重寫

重寫指的是在Java的子類與父類中有兩個名稱、引數列表都相同的方法的情況。由於他們具有相同的方法簽名,所以子類中的新方法將覆蓋父類中原有的方法。

關於過載和重寫,你應該知道以下幾點:

1、過載是一個編譯期概念、重寫是一個執行期間概念。

2、過載遵循所謂“編譯期繫結”,即在編譯時根據引數變數的型別判斷應該呼叫哪個方法。

3、重寫遵循所謂“執行期繫結”,即在執行的時候,根據引用變數所指向的實際物件的型別來呼叫方法

4、因為在編譯期已經確定呼叫哪個方法,所以過載並不是多型。而重寫是多型。過載只是一種語言特性,是一種語法規則,與多型無關,與面向物件也無關。(注:嚴格來說,過載是編譯時多型,即靜態多型。但是,Java中提到的多型,在不特別說明的情況下都指動態多型)

正是因為Java在繼承中有方法的重寫,所以,這也體現了Java的動態多型性。

重寫,指的是方法。並沒有提到成員變數。成員變數不會被重寫,這裡就有另外一個詞:隱藏。

在一個類中,子類中的成員變數如果和父類中的成員變數同名,那麼即使他們型別不一樣,只要名字一樣。父類中的成員變數都會被隱藏。在子類中,父類的成員變數不能被簡單的用引用來訪問。而是,必須從父類的引用獲得父類被隱藏的成員變數,一般來說,我們不推薦隱藏成員變數,因為這樣會使程式碼變得難以閱讀。其實,簡單來說,就是子類不會去重寫覆蓋父類的成員變數,所以成員變數的訪問不能像方法一樣使用多型去訪問。

10、介面和抽象類的區別,如何選擇?

解:

介面和抽象類,最明顯的區別就是介面只是定義了一些方法而已,在不考慮Java8中default方法情況下,介面中是沒有實現的程式碼的。

抽象類中的抽象方法可以有public、protected和default這些修飾符,而介面中預設修飾符是public。不可以使用其它修飾符。

關於如何選擇,我們一般會把介面暴露給外部,然後在業務程式碼中實現介面。如果多個實現類中有相同可複用的程式碼,則在介面和實現類中間加一層抽象類,將公用部分程式碼抽出到抽象類中。可以參考下模板方法模式,這是一個很好的理解介面、抽象類和實現類之間關係的設計模式。

11、Java能不能多繼承,可不可以多實現?

解:

Java中不能多繼承,可以多實現。 即定義一個類的時候,可以同時實現多個介面,但是不能同時繼承多個類。

12、什麼是建構函式,什麼是預設建構函式?

解:

建構函式,是一種特殊的方法。 主要用來在建立物件時初始化物件, 即為物件成員變數賦初始值,總與new運算子一起使用在建立物件的語句中。 特別的一個類可以有多個建構函式,可根據其引數個數的不同或引數型別的不同來區分它們即建構函式的過載。

建構函式跟一般的例項方法十分相似;但是與其它方法不同,構造器沒有返回型別,不會被繼承,且可以有範圍修飾符。構造器的函式名稱必須和它所屬的類的名稱相同。 它承擔著初始化物件資料成員的任務。

如果在編寫一個可例項化的類時沒有專門編寫建構函式,多數程式語言會自動生成預設構造器(預設建構函式)。預設建構函式一般會把成員變數的值初始化為預設值,如int -> 0,Integet -> null。

13、構造方法能不能被過載,構造方法能不能被重寫?

解:

構造方法可以被過載,實現物件資料不同的初始化; 構造方法不能被重寫,子類不能繼承父類的構造方法,只能在子類的構造方法中呼叫父類的構造方法(自動呼叫父類預設構造方法,或呼叫父類指定的構造方法),保證父類物件也進行初始化(子類繼承父類物件資料得到初始化)

14、對於成員變數和方法的作用域,public,protected,private以及不寫之間的區別。

解:

-public :表明該成員變數或者方法是對所有類或者物件都是可見的,所有類或者物件都可以直接訪問

- private:表明該成員變數或者方法是私有的,只有當前類對其具有訪問許可權,除此之外其他類或者物件都沒有訪問許可權.子類也沒有訪問許可權.

- protected:表明成員變數或者方法對類自身,與同在一個包中的其他類可見,其他包下的類不可訪問,除非是他的子類

- default:表明該成員變數或者方法只有自己和其位於同一個包的內可見,其他包內的類不能訪問,即便是它的子類

15、介面能否繼承介面? 抽象類能否實現介面? 抽象類能否繼承具體類?

解:

介面可以繼承

抽象類可以實現介面

抽象類可以繼承具體類