【Java面試題系列】:Java基礎知識常見面試題彙總 第二篇

周偉偉的部落格發表於2019-05-13

文中面試題從茫茫網海中精心篩選,如有錯誤,歡迎指正!

第一篇連結:【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版本,安裝完的目錄如下圖所示:

【Java面試題系列】:Java基礎知識常見面試題彙總 第二篇

而jdk目錄下也有1個jre:

【Java面試題系列】:Java基礎知識常見面試題彙總 第二篇

我電腦環境變數配置的是:

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();

抽象方法與普通方法的區別:

  1. 抽象方法需要用關鍵字abstract修飾

  2. 抽象方法沒有方法體,即只有宣告,而沒有具體的實現

  3. 抽象方法所在的類必須宣告為抽象類

  4. 抽象方法必須宣告為public或者protected,不能宣告為private

    因為如果為private,則不能被子類繼承,子類便無法實現該方法,抽象方法也就失去了意義

2.2抽象類

如果一個類包含抽象方法,則這個類是抽象類,必須由關鍵字abstract修飾。

抽象類是為了繼承而存在的,如果你定義了一個抽象類,卻不去繼承它,那麼等於白白建立了這個抽象類,因為你不能用它來做任何事情,即沒由起到抽象類的意義。對於一個父類,如果它的某個方法在父類中沒有具體的實現,必須根據子類的實際需求來進行不同的實現,那麼就可以將這個方法宣告為abstract方法,此時這個類也就成為abstract類了。

抽象類與普通類的區別:

  1. 抽象類不能被例項化,即不能通過new來建立物件
  2. 抽象類需要用關鍵字abstract修飾
  3. 如果一個類繼承於一個抽象類,則子類必須實現父類的抽象方法。如果子類沒有實現父類的抽象方法,則必須將子類也定義為abstract類。
  4. 抽象類除了可以擁有普通類的成員變數和成員方法,還可以擁有抽象方法

值得注意的是,抽象類不一定必須包含抽象方法,只是一般大家使用時,都包含了抽象方法

舉個具體的例子,比如我們有一個平面圖形類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抽象類和介面的區別

語法層面上的區別:

  1. 一個類只能繼承一個抽象類,而一個類卻可以實現多個介面
  2. 介面中不能含有靜態程式碼塊以及靜態方法,而抽象類可以有靜態程式碼塊和靜態方法
  3. 抽象類可以提供成員方法的實現細節,而介面中的方法不可以
  4. 介面的方法預設是public,所有方法在介面中不能有實現,抽象類可以有非抽象的方法
  5. 抽象類中的成員變數可以是各種型別的,而介面中的成員變數只能是public static final型別的
  6. 介面不能用new例項化,但可以宣告,但是必須引用一個實現該介面的物件, 從設計層面來說,抽象是對類的抽象,是一種模板設計,介面是行為的抽象,是一種行為的規範。

設計層面上的區別:

  1. 抽象類是對整個類整體進行抽象,包括屬性、行為,但是介面卻是對類區域性(行為)進行抽象。

    繼承是一個 "是不是"的關係,而 介面實現則是 "有沒有"的關係。如果一個類繼承了某個抽象類,則子類必定是抽象類的種類,而介面實現則是有沒有、具備不具備的關係,比如鳥是否能飛(或者是否具備飛行這個特點),能飛行則可以實現這個介面,不能飛行就不實現這個介面。

  2. 設計層面不同,抽象類作為很多子類的父類,它是一種模板式設計。而介面是一種行為規範,它是一種輻射式設計。

    對於抽象類,如果需要新增新的方法,可以直接在抽象類中新增具體的實現,子類可以不進行變更;而對於介面則不行,如果介面進行了變更,則所有實現這個介面的類都必須進行相應的改動。

這裡引用下網上的門和警報的例子,門都有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個類裡面,是讓類以統一的方式處理不同型別資料的一種手段,實質表現就是允許一個類中存在多個具有不同引數個數或者型別同名函式/方法,是一個類中多型性的一種表現。

返回值型別可隨意,不能以返回型別作為過載函式的區分標準

過載規則如下:

  1. 必須具有不同的引數列表
  2. 可以有不同的返回型別
  3. 可以有不同的訪問修飾符
  4. 可以丟擲不同的異常

重寫(Override):發生在父子類中,是父類與子類之間的多型性,實質是對父類的函式進行重新定義,如果在子類中定義某方法與父類有相同的方法名稱和引數則該方法被重寫,不過子類函式的訪問修飾符許可權不能小於父類的;若子類中的方法與父類中的某一方法具有相同的方法名、返回型別和引數列表,則新方法將覆蓋原有的方法,如需呼叫父類中原有的方法可使用super關鍵字呼叫。

重寫規則如下:

  1. 引數列表必須完全與被重寫的方法相同,否則不能稱其為重寫而是過載
  2. 返回型別必須一直與被重寫的方法相同,否則不能稱其為重寫而是過載
  3. 訪問修飾符的限制一定要大於等於被重寫方法的訪問修飾符
  4. 重寫方法一定不能丟擲新的檢查異常或者比被重寫方法申明更加寬泛的檢查型異常,譬如父類方法宣告瞭一個檢查異常 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.字元型常量和字串常量的區別

  1. 形式上: 字元常量是單引號引起的一個字元 字串常量是雙引號引起的若干個字元
  2. 含義上: 字元常量相當於一個整形值(ASCII值),可以參加表示式運算 字串常量代表一個地址值(該字串在記憶體中存放位置)
  3. 佔記憶體大小:字元常量只佔一個位元組 字串常量佔若干個位元組

6.參考連結

弄懂 JRE、JDK、JVM 之間的區別與聯絡

Java抽象類和抽象方法例子

深入理解Java的介面和抽象類

JAVA重寫和過載的區別

JAVA中區域性變數 和 成員變數有哪些區別

成員變數與區域性變數的區別

最最最常見的Java面試題總結——第二週

相關文章