【Java面試題系列】:Java基礎知識常見面試題匯總 第二篇
文中面試題從茫茫網海中精心篩選,如有錯誤,歡迎指正!
第一篇鏈接:【Java面試題系列】:Java基礎知識常見面試題匯總 第一篇
1.JDK,JRE,JVM三者之間的聯系和區別
你是否考慮過我們寫的xxx.java文件被誰編譯,又被誰執行,又為什麽能夠跨平臺運行?
1.1基本概念
JVM:Java Virtual Machine,Java虛擬機。
JVM並不能識別我們平時寫的xxx.java文件,只能識別xxx.class文件,它能夠將class文件中的字節碼指令進行識別並調用操作系統上的API完成指定的動作。所以,JVM是Java能夠跨平臺的核心。
JRE:Java Runtime Environment,Java運行時環境。
JRE主要包含2個部分,JVM的標準實現和Java的一些基本類庫。相比於JVM,多出來的是一部分Java類庫。
JDK:Java Development Kit,開發工具包。
JDK是整個Java開發的核心,它集成了JRE和一些好用的小工具,例如:javac.exe,java.exe,jar.exe等。
上一篇博客中也提到了,我們可以通過javac命令將xxx.java文件編譯為xxx.class文件。
1.2聯系和區別
了解完3者的基本概念,我們可以看出來3者的關系為一層層嵌套,即:JDK > JRE > JVM。
這裏,我們提出一個問題:為什麽我們安裝完JDK後會有兩個版本的JRE?
我電腦安裝的JDK是1.8版本,安裝完的目錄如下圖所示:
而jdk目錄下也有1個jre:
我電腦環境變量配置的是:
JAVA_HOME C:\Program Files\Java\jdk1.8.0_191
Path變量最後添加的是%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin。
也就是說,我電腦用的是jdk目錄下的jre,而不是和jdk同級目錄下的jre,也許大部分人都是這樣的,可能沒人註意,說實話,我之前還真沒在意,看了網上的文章才知道,看來真的是要多問為什麽。
這兩個不同版本的JRE其實沒什麽聯系,你可以修改下Path變量,指向任意1個都可以,只是很多人在安裝JDK的時候,並不清楚JDK和JRE的區別,所以都會安裝,比如說我,哈哈。
在jdk的目錄下,有一些可執行文件,比如說javac.exe,其實內部也是調用的java類,所以jdk目錄下的jre既提供了這些工具的運行時環境,也提供了我們編寫的Java程序的運行時環境。
所以,可以得出如下結論:
如果你是Java開發者,安裝JDK時可以選擇不安裝JRE
如果你的機器只是用來部署和運行Java程序,可以不安裝JDK,只安裝JRE即可
1.3Java 為什麽能跨平臺,實現一次編寫,多處運行?
Java引入了字節碼的概念,JVM只能識別字節碼,並將它們解釋到系統的API調用,針對不同的系統有不同的JVM實現,有Lunix版本的JVM實現,也有Windows版本的JVM實現,但是同一段代碼在編譯後的字節碼是一致的,而同一段字節碼,在不同的JVM實現上會映射到不同系統的API調用,從而實現代碼不修改即可跨平臺運行。
所以說Java能夠跨平臺的核心在於JVM,不是Java能夠跨平臺,而是它的JVM能夠跨平臺。
2.接口和抽象類的區別
2.1抽象方法
當父類的一些方法不確定時,可以用abstract關鍵字將其聲明為抽象方法,聲明語法如下:
public abstract double area();
抽象方法與普通方法的區別:
抽象方法需要用關鍵字abstract修飾
抽象方法沒有方法體,即只有聲明,而沒有具體的實現
抽象方法所在的類必須聲明為抽象類
抽象方法必須聲明為public或者protected,不能聲明為private
因為如果為private,則不能被子類繼承,子類便無法實現該方法,抽象方法也就失去了意義
2.2抽象類
如果一個類包含抽象方法,則這個類是抽象類,必須由關鍵字abstract修飾。
抽象類是為了繼承而存在的,如果你定義了一個抽象類,卻不去繼承它,那麽等於白白創建了這個抽象類,因為你不能用它來做任何事情,即沒由起到抽象類的意義。對於一個父類,如果它的某個方法在父類中沒有具體的實現,必須根據子類的實際需求來進行不同的實現,那麽就可以將這個方法聲明為abstract方法,此時這個類也就成為abstract類了。
抽象類與普通類的區別:
- 抽象類不能被實例化,即不能通過new來創建對象
- 抽象類需要用關鍵字abstract修飾
- 如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。如果子類沒有實現父類的抽象方法,則必須將子類也定義為abstract類。
- 抽象類除了可以擁有普通類的成員變量和成員方法,還可以擁有抽象方法
值得註意的是,抽象類不一定必須包含抽象方法,只是一般大家使用時,都包含了抽象方法
舉個具體的例子,比如我們有一個平面圖形類Shape,它有兩個抽象方法area()和perimeter(),分別用來獲取圖形的面積和周長,然後我們有矩形類Rectangle和圓形類Circle,來繼承抽象類Shape,各自實現area()方法和和perimeter()方法,因為矩形和圓形計算面積和周長的方法是不一樣的,下面看具體代碼:
package com.zwwhnly.springbootdemo;
public abstract class Shape {
public abstract double area();
public abstract double perimeter();
}
package com.zwwhnly.springbootdemo;
public class Rectangle extends Shape {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
@Override
public double area() {
return getLength() * getWidth();
}
@Override
public double perimeter() {
return (getLength() + getWidth()) * 2;
}
}
package com.zwwhnly.springbootdemo;
public class Circle extends Shape {
private double diameter;
public double getDiameter() {
return diameter;
}
public void setDiameter(double diameter) {
this.diameter = diameter;
}
@Override
public double area() {
return Math.PI * Math.pow(getDiameter() / 2, 2);
}
@Override
public double perimeter() {
return Math.PI * getDiameter();
}
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setLength(10);
rectangle.setWidth(5);
double rectangleArea = rectangle.area();
double rectanglePerimeter = rectangle.perimeter();
System.out.println("矩形的面積:" + rectangleArea + ",周長" + rectanglePerimeter);
Circle circle = new Circle();
circle.setDiameter(10);
double circleArea = circle.area();
double circlePerimeter = circle.perimeter();
System.out.println("圓形的面積:" + circleArea + ",周長" + circlePerimeter);
}
輸出結果:
矩形的面積:50.0,周長30.0
圓形的面積:78.53981633974483,周長31.41592653589793
2.2接口
接口,是對行為的抽象,聲明語法為:
package com.zwwhnly.springbootdemo;
public interface Alram {
void alarm();
}
可以看出,接口中的方法沒有具體的實現(會被隱式的指定為public abstract方法),具體的實現由實現接口的類來實現,類實現接口的語法為(這裏以ArrayList類為例):
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
......
}
可以看出,一個類可以實現多個接口。
如果一個非抽象類實現了某個接口,就必須實現該接口中的所有方法。
如果一個抽象類實現了某個接口,可以不實現該接口中的方法,但其子類必須實現。
2.3抽象類和接口的區別
語法層面上的區別:
- 一個類只能繼承一個抽象類,而一個類卻可以實現多個接口
- 接口中不能含有靜態代碼塊以及靜態方法,而抽象類可以有靜態代碼塊和靜態方法
- 抽象類可以提供成員方法的實現細節,而接口中的方法不可以
- 接口的方法默認是public,所有方法在接口中不能有實現,抽象類可以有非抽象的方法
- 抽象類中的成員變量可以是各種類型的,而接口中的成員變量只能是public static final類型的
- 接口不能用new實例化,但可以聲明,但是必須引用一個實現該接口的對象, 從設計層面來說,抽象是對類的抽象,是一種模板設計,接口是行為的抽象,是一種行為的規範。
設計層面上的區別:
抽象類是對整個類整體進行抽象,包括屬性、行為,但是接口卻是對類局部(行為)進行抽象。
繼承是一個 "是不是"的關系,而 接口實現則是 "有沒有"的關系。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而接口實現則是有沒有、具備不具備的關系,比如鳥是否能飛(或者是否具備飛行這個特點),能飛行則可以實現這個接口,不能飛行就不實現這個接口。
設計層面不同,抽象類作為很多子類的父類,它是一種模板式設計。而接口是一種行為規範,它是一種輻射式設計。
對於抽象類,如果需要添加新的方法,可以直接在抽象類中添加具體的實現,子類可以不進行變更;而對於接口則不行,如果接口進行了變更,則所有實現這個接口的類都必須進行相應的改動。
這裏引用下網上的門和警報的例子,門都有open()和close()兩個動作,此時我們可以通過抽象類或者接口定義:
public abstract class Door {
public abstract void open();
public abstract void close();
}
或者使用接口:
public interface Door {
void open();
void close();
}
現在我們需要門具有警報alarm功能,該如何設計呢?
你可能想到的2個思路為:
1)在抽象類中增加alarm()方法,這樣一來,所有繼承於這個抽象類的子類都具備了報警功能,但是有的門並不一定具備報警功能。
2)在接口中增加alarm()方法,這樣一來,用到報警功能的類就必須要實現接口中的open()和close()方法,也許這個類根本就不具備open()和close()這兩個功能,比如火災報警器。
從這裏可以看出,Door的open(),close()和alarm()屬於兩個不同範疇內的行為,open()和close()屬於門本身固有的行為特性,而alarm()屬於延伸的附加行為。
因此最好的設計方式是單獨將報警設計為一個接口Alarm,包含alarm()行為,Door設計為單獨的抽象類,包含open()和close()行為,再設計一個報警門繼承Door類並實現Alarm接口:
public abstract class Door {
public abstract void open();
public abstract void close();
}
public interface Alarm {
void alarm();
}
public class AlarmDoor extends Door implements Alarm {
@Override
public void alarm() {
}
@Override
public void open() {
}
@Override
public void close() {
}
}
3.重載與重寫的區別
3.1基本概念
重載(Overload):發生在1個類裏面,是讓類以統一的方式處理不同類型數據的一種手段,實質表現就是允許一個類中存在多個具有不同參數個數或者類型的同名函數/方法,是一個類中多態性的一種表現。
返回值類型可隨意,不能以返回類型作為重載函數的區分標準
重載規則如下:
- 必須具有不同的參數列表
- 可以有不同的返回類型
- 可以有不同的訪問修飾符
- 可以拋出不同的異常
重寫(Override):發生在父子類中,是父類與子類之間的多態性,實質是對父類的函數進行重新定義,如果在子類中定義某方法與父類有相同的方法名稱和參數則該方法被重寫,不過子類函數的訪問修飾符權限不能小於父類的;若子類中的方法與父類中的某一方法具有相同的方法名、返回類型和參數列表,則新方法將覆蓋原有的方法,如需調用父類中原有的方法可使用super關鍵字調用。
重寫規則如下:
- 參數列表必須完全與被重寫的方法相同,否則不能稱其為重寫而是重載
- 返回類型必須一直與被重寫的方法相同,否則不能稱其為重寫而是重載
- 訪問修飾符的限制一定要大於等於被重寫方法的訪問修飾符
- 重寫方法一定不能拋出新的檢查異常或者比被重寫方法申明更加寬泛的檢查型異常,譬如父類方法聲明了一個檢查異常 IOException,在重寫這個方法時就不能拋出 Exception,只能拋出 IOException 的子類異常,可以拋出非檢查異常
總之,重載與重寫是Java多態性的不同表現,重寫是父類與子類之間多態性的表現,在運行時起作用;而重載是一個類中多態性的表現,在編譯時起作用。
3.2示例
其實JDK的源碼中就有很多重載和重寫的例子,重載的話,我們看下Math類的abs()方法,就有以下幾種實現:
public static int abs(int a) {
return (a < 0) ? -a : a;
}
public static long abs(long a) {
return (a < 0) ? -a : a;
}
public static float abs(float a) {
return (a <= 0.0F) ? 0.0F - a : a;
}
public static double abs(double a) {
return (a <= 0.0D) ? 0.0D - a : a;
}
重寫的話,我們以String類的equals()方法為例,基類中equals()是這樣的:
public boolean equals(Object obj) {
return (this == obj);
}
而子類String的equals()重寫後是這樣的:
public boolean equals(Object anObject) {
if (this == anObject) {
return true;
}
if (anObject instanceof String) {
String anotherString = (String)anObject;
int n = value.length;
if (n == anotherString.value.length) {
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
while (n-- != 0) {
if (v1[i] != v2[i])
return false;
i++;
}
return true;
}
}
return false;
}
我們再來看一個特殊的例子:
package com.zwwhnly.springbootdemo;
public class Demo {
public boolean equals(Demo other) {
System.out.println("use Demo equals.");
return true;
}
public static void main(String[] args) {
Object o1 = new Demo();
Object o2 = new Demo();
Demo o3 = new Demo();
Demo o4 = new Demo();
if (o1.equals(o2)) {
System.out.println("o1 is equal with o2.");
}
if (o3.equals(o4)) {
System.out.println("o3 is equal with o4.");
}
}
}
輸出結果:
use Demo equals.
o3 is equal with o4.
是不是和你預期的輸出結果不一致呢,出現這個的原因是,該類的equals()方法並沒有真正重寫Object類的equals()方法,違反了參數規則,因此o1.equals(o2)時,調用的仍是Object類的equals()方法,即比較的是內存地址,因此返回false。而o3.equals(o4)比較時,因為o3,o4都是Demo類型,因此調用的是Demo類的equals()方法,返回true。
4.成員變量和局部變量的區別
4.1定義的位置不一樣
成員變量:在方法外部,可以被public,private,static,final等修飾符修飾
局部變量:在方法內部或者方法的聲明上(即在參數列表中),不能被public,private,static等修飾符修飾,但可以被final修飾
4.2作用範圍不一樣
成員變量:整個類全都可以通用
局部變量:只有方法當中才可以使用,出了方法就不能再用
4.3默認值不一樣
成員變量:如果沒有賦值,會有默認值(類型的默認值)
局部變量:沒有默認值,使用前必須賦值,否則編譯器會報錯
4.4內存的位置不一樣
成員變量:位於堆內存
局部變量:位於棧內存
4.5.生命周期不一樣
成員變量:隨著對象創建而誕生,隨著對象被垃圾回收而消失
局部變量:隨著方法的調用或者代碼塊的執行而存在,隨著方法的調用完畢或者代碼塊的執行完畢而消失
package com.zwwhnly.springbootdemo;
public class VariableDemo {
private String name = "成員變量";
public static void main(String[] args) {
new VariableDemo().show();
}
public void show() {
String name = "局部變量";
System.out.println(name);
System.out.println(this.name);
}
}
輸出結果:
局部變量
成員變量
5.字符型常量和字符串常量的區別
- 形式上: 字符常量是單引號引起的一個字符 字符串常量是雙引號引起的若幹個字符
- 含義上: 字符常量相當於一個整形值(ASCII值),可以參加表達式運算 字符串常量代表一個地址值(該字符串在內存中存放位置)
- 占內存大小:字符常量只占一個字節 字符串常量占若幹個字節
6.參考鏈接
弄懂 JRE、JDK、JVM 之間的區別與聯系
Java抽象類和抽象方法例子
深入理解Java的接口和抽象類
JAVA重寫和重載的區別
JAVA中局部變量 和 成員變量有哪些區別
成員變量與局部變量的區別
最最最常見的Java面試題總結——第二周
【Java面試題系列】:Java基礎知識常見面試題匯總 第二篇