Java面試題沖刺第五天–基礎篇2

面試題1:說一下抽象類和接口有哪些區別?

正經回答:

抽象類和接口的主要區別:

從設計層面來說,抽象類是對類的抽象,是一種模板設計;接口是行為的抽象,是一種行為的規范。

  • 一個類可以有多個接口 隻能有繼承一個父類
  • 抽象類可以有構造方法,接口中不能有構造方法。
  • 抽象類中可以有普通成員變量,接口中沒有普通成員
  • 變量接口裡邊全部方法都必須是abstract的;抽象類的可以有實現瞭的方法
  • 抽象類中的抽象方法的訪問類型可以是public,protected;但接口中的抽象方法隻能是public類型的,並且默認即為public abstract類型
  • 抽象類中可以包含靜態方法,接口中不能包含靜態方法
  • 抽象類和接口中都可以包含靜態成員變量,抽象類中的靜態成員變量的訪問類型可以任意;但接口中定義的變量隻能是public static final類型,並且默認即為public static final類型。

Java8中接口中引入默認方法和靜態方法,以此來減少抽象類和接口之間的差異

接口和抽象類各有優缺點,在接口和抽象類的選擇上,必須遵守這樣一個原則:

行為模型應該總是通過接口而不是抽象類定義,所以通常是優先選用接口,盡量少用抽象類。

選擇抽象類的時候通常是如下情況:需要定義子類的行為,又要為子類提供通用的功能。

深入追問:

追問1:說一說你對抽象類的理解吧,他到底是幹啥用的

我們常說面向對象的核心思想是:先抽象,後具體。抽象類是含有抽象方法的類,不能被實例化,抽象類常用作當做模板類使用。

接口更多的是在系統架構設計方法發揮作用,主要用於定義模塊之間的通信契約。

而抽象類在代碼實現方面發揮作用,可以實現代碼的重用,例如,模板方法設計模式是抽象類的一個典型應用,假設某個項目的所有Servlet類都要用相同的方式進行權限判斷、記錄訪問日志和處理異常,那麼就可以定義一個抽象的基類,讓所有的Servlet都繼承這個抽象基類,在抽象基類的service方法中完成權限判斷、記錄訪問日志和處理異常的代碼,在各個子類中隻是完成各自的業務邏輯代碼。父類方法中間的某段代碼不確定,再留給子類幹,就用模板方法設計模式。

追問2:用抽象類實現一個接口,和普通類實現接口會有什麼不同麼?

一般來說我們使用普通類來實現接口,這個普通類就必須實現接口中所有的方法,這樣的結果就是普通類中就需要實現多餘的方法,造成代碼冗餘。但是如果我們使用的是抽象類來實現接口,那麼就可以隻實現接口中的部分方法,並且當其他類繼承這個抽象類時,仍然可以實現接口中有但抽象類並未實現的方法。

如以下代碼,抽象類隻是實現瞭接口A中的方法a,方法b,但是當類C繼承抽象類B時,可以直接實現接口A中的c方法,有一點需要註意的是,類C中的方法a,方法b都是調用的父類B的方法a,方法b,不是直接實現接口的方法a和b。

/**
 *接口
 */
 interface A{
 public void aaa();
 public void bbb();
 public void ccc();
 }
 /**
 *抽象類
 */
 abstract class B implements A{
 public void aaa(){}
 public void bbb(){}
 }
 /**
 * 實現類
 */
 public class C extends B{
 public void aaa(){}
 public void bbb(){}
 public void ccc(){}
 }

追問3:抽象類能使用 final 修飾嗎?

不能,定義抽象類就是讓其他類繼承的,如果定義為 final 該類就不能被繼承,這樣彼此就會產生矛盾,所以 final 不能修飾抽象類。

在這裡插入圖片描述

面試題2:final 在 Java 中有什麼作用?

正經回答:

用於修飾類、方法和屬性;

1、修飾類

當用final修飾類的時,表明該類不能被其他類所繼承。需要註意的是:final類中所有的成員方法都會隱式的定義為final方法。

2、修飾方法

使用final方法的原因主要是把方法鎖定,以防止繼承類對其進行更改或重寫。

若父類中final方法的訪問權限為private,將導致子類中不能直接繼承該方法,因此,此時可以在子類中定義相同方法名的函數,此時不會與重寫final的矛盾,而是在子類中重新地定義瞭新方法。

class A{
    private final void getName(){
        System.out.println("getName - A");
    }
}
public class B extends A{
    public void getName(){
        System.out.println("getName - B");
    }
    public void main(String[]args){
        this.getName(); // 日志輸出:getName - B
    }
}

3、修飾變量

當final修飾一個基本數據類型時,表示該基本數據類型的值一旦在初始化後便不能發生變化;如果final修飾一個引用類型時,則在對其初始化之後便不能再讓其指向其他對象瞭,但該引用所指向的對象的內容是可以發生變化的。本質上是一回事,因為引用的值是一個地址,final要求值,即地址的值不發生變化。

final修飾一個成員變量(屬性),必須要顯示初始化。這裡有兩種初始化方式,一種是在變量聲明的時候初始化;第二種方法是在聲明變量的時候不賦初值,但是要在這個變量所在的類的所有的構造函數中對這個變量賦初值。

當函數的參數類型聲明為final時,說明該參數是隻讀型的。即你可以讀取使用該參數,但是無法改變該參數的值。

深入追問:

追問1:能分別說一下final、finally、finalize的區別麼?

  • final可以修飾類、變量、方法,修飾類表示該類不能被繼承、修飾方法表示該方法不能被重寫、修飾變量表示該變量是一個常量不能被重新賦值。
  • finally一般作用在try-catch代碼塊中,在處理異常的時候,通常我們將一定要執行的代碼方法finally代碼塊中,表示不管是否出現異常,該代碼塊都會執行,一般用來存放一些關閉資源的代碼。當然,還有多種情況走不瞭finally~
  • finalize是一個方法,屬於Object類的一個方法,而Object類是所有類的父類,該方法一般由垃圾回收器來調用,當我們調用System.gc() 方法的時候,由垃圾回收器調用finalize(),回收垃圾,一個對象是否可回收的最後判斷。

面試題3:你對Java序列化瞭解麼?

正經回答:

序列化過程:

是指把一個Java對象變成二進制內容,實質上就是一個byte[]數組。

因為序列化後可以把byte[]保存到文件中,或者把byte[]通過網絡傳輸到遠程(IO),這樣,就相當於把Java對象存儲到文件或者通過網絡傳輸出去瞭。

反序列化過程:

把一個二進制內容(也就是byte[]數組)變回Java對象。有瞭反序列化,保存到文件中的byte[]數組又可以“變回”Java對象,或者從網絡上讀取byte[]並把它“變回”Java對象。

以下是一些使用序列化的示例:

以面向對象的方式將數據存儲到磁盤上的文件,例如,Redis存儲Student對象的列表。

將程序的狀態保存在磁盤上,例如,保存遊戲狀態。

通過網絡以表單對象形式發送數據,例如,在聊天應用程序中以對象形式發送消息。

一個Java對象要能序列化,必須實現一個特殊的java.io.Serializable接口,它的定義如下:

public interface Serializable {
}

Serializable接口沒有定義任何方法,它是一個空接口。我們把這樣的空接口稱為“標記接口”(Marker Interface),實現瞭標記接口的類僅僅是給自身貼瞭個“標記”,並沒有增加任何方法。

深入追問:

追問1:Java序列化是如何工作的?

當且僅當對象的類實現java.io.Serializable接口時,該對象才有資格進行序列化。可序列化 是一個標記接口(不包含任何方法),該接口告訴Java虛擬機(JVM)該類的對象已準備好寫入持久性存儲或通過網絡進行讀取。

默認情況下,JVM負責編寫和讀取可序列化對象的過程。序列化/反序列化功能通過對象流類的以下兩種方法公開:

ObjectOutputStream。writeObject(Object):將可序列化的對象寫入輸出流。如果要序列化的某些對象未實現Serializable接口,則此方法將引發NotSerializableException

ObjectInputStream。readObject():從輸入流讀取,構造並返回一個對象。如果找不到序列化對象的類,則此方法將引發ClassNotFoundException。

如果序列化使用的類有問題,則這兩種方法都將引發InvalidClassException,如果發生I / O錯誤,則將引發IOException。無論NotSerializableExceptionInvalidClassException是子類IOException異常。

讓我們來看一個簡單的例子。以下代碼將String對象序列化為名為“ data.ser”的文件。字符串對象是可序列化的,因為String類實現瞭Serializable 接口:

String filePath = "data.ser";
String message = "Java Serialization is Cool";
try (
    FileOutputStream fos = new FileOutputStream(filePath);
    ObjectOutputStream outputStream = new ObjectOutputStream(fos);
) {
    outputStream.writeObject(message);
} catch (IOException ex) {
    System.err.println(ex);
}

以下代碼反序列化文件“ data.ser”中的String對象:

String filePath = "data.ser";
try (
    FileInputStream fis = new FileInputStream(filePath);
    ObjectInputStream inputStream = new ObjectInputStream(fis);
) {
    String message = (String) inputStream.readObject();
    System.out.println("Message: " + message);
} catch (ClassNotFoundException ex) {
    System.err.println("Class not found: " + ex);
} catch (IOException ex) {
    System.err.println("IO error: " + ex);
}

請註意,readObject()返回一個Object類型的對象,因此您需要將其強制轉換為可序列化的類,在這種情況下為String類。

讓我們看一個涉及使用自定義類的更復雜的示例。

給定以下學生班:

import java.io.*;
import java.util.*;
/**
 * Student.java
 * @author chenhh
 */
public class Student extends Person implements Serializable {
    public static final long serialVersionUID = 1234L;
    private long studentId;
    private String name;
    private transient int age;
    public Student(long studentId, String name, int age) {
        super();
        this.studentId = studentId;
        this.name = name;
        this.age = age;
        System.out.println("Constructor");
    }
    public String toString() {
        return String.format("%d - %s - %d", studentId, name, age);
    }
}

如上面代碼,你會發現兩點:

long serialVersionUID類型的常量。

成員變量age被標記為transient。 下面兩個問題讓我們搞明白它們。

追問2:什麼是serialVersionUID常數

serialVersionUID是一個常數,用於唯一標識可序列化類的版本。從輸入流構造對象時,JVM在反序列化過程中檢查此常數。如果正在讀取的對象的serialVersionUID與類中指定的序列號不同,則JVM拋出InvalidClassException。這是為瞭確保正在構造的對象與具有相同serialVersionUID的類兼容。

請註意,serialVersionUID是可選的。這意味著如果您不顯式聲明Java編譯器,它將生成一個。

那麼,為什麼要顯式聲明serialVersionUID呢?

原因是:自動生成的serialVersionUID是基於類的元素(成員變量,方法,構造函數等)計算的。如果這些元素之一發生更改,serialVersionUID也將更改。想象一下這種情況:

  • 您編寫瞭一個程序,將Student類的某些對象存儲到文件中。Student類沒有顯式聲明的serialVersionUID。
  • 有時,您更新瞭Student類(例如,添加瞭一個新的私有方法),現在自動生成的serialVersionUID也被更改瞭。
  • 您的程序無法反序列化先前編寫的Student對象,因為那裡的serialVersionUID不同。JVM拋出InvalidClassException。

這就是為什麼建議為可序列化類顯式添加serialVersionUID的原因。

追問3、那你知道什麼是瞬時變量麼?

在上面的Student類中,您看到成員變量age被標記為transient,對嗎?JVM 在序列化過程中跳過瞬態變量。這意味著在序列化對象時不會存儲age變量的值。

因此,如果成員變量不需要序列化,則可以將其標記為瞬態。

以下代碼將Student對象序列化為名為“ students.ser”的文件:

String filePath = "students.ser";
Student student = new Student(123, "John", 22);
try (
    FileOutputStream fos = new FileOutputStream(filePath);
    ObjectOutputStream outputStream = new ObjectOutputStream(fos);
) {
    outputStream.writeObject(student);
} catch (IOException ex) {
    System.err.println(ex);
}

請註意,在序列化對象之前,變量age的值為22。

下面的代碼從文件中反序列化Student對象:

String filePath = "students.ser";
try (
    FileInputStream fis = new FileInputStream(filePath);
    ObjectInputStream inputStream = new ObjectInputStream(fis);
) {
    Student student = (Student) inputStream.readObject();
    System.out.println(student);
} catch (ClassNotFoundException ex) {
    System.err.println("Class not found: " + ex);
} catch (IOException ex) {
    System.err.println("IO error: " + ex);
}

此代碼將輸出以下輸出:

1個 123 – John – 0

總結

本篇文章就到這裡瞭,希望能給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: