面試準備:Java常見面試題彙總(二)

文章目錄

43.java 中的 Math.round(-1.5) 等於多少?

Math.round採用了四捨五入,四捨五入的原理是在參數上加0.5然後做向下取整。
Math.round(1.5)的返回值是2,Math.round(-1.5)的返回值是-1。

44.String str="i"與 String str=new String(“i”)一樣嗎?

不一樣,因爲內存的分配方式不一樣。String str="i"的方式,java虛擬機會將其分配到常量池中;而String str=new String(“i”)則會被分到堆內存中。

45.如何將字符串反轉?

1.利用 StringBuffer 或 StringBuilder 的 reverse 成員方法:
2.利用 String 的 toCharArray 方法先將字符串轉化爲 char 類型數組,然後將各個字符進行重新拼接:

46.String 類的常用方法都有那些?

toCharArray()
substring()
charAt()
length()
split()
trim()
toUpperCase()

47.抽象類必須要有抽象方法嗎?

抽象類可以沒有抽象方法,但是如果你的一個類已經聲明成了抽象類,即使這個類中沒有抽象方法,它也不能再實例化,即不能直接構造一個該類的對象。如果一個類中有了一個抽象方法,那麼這個類必須聲明爲抽象類,否則編譯通不過。

48.普通類和抽象類有哪些區別?

抽象類用abstract修飾,且不能被實例化。
抽象類不能是final的。
抽象方法不可以使用private、final或者static。
抽象類的子類必須實現抽象類中所有抽象方法,否則這個子類也是抽象類。

49.java 中 IO 流分爲幾種?

Java中的流分爲兩種,一種是字節流,另一種是字符流。
字符流和字節流是根據處理數據的不同來區分的。

  • 字節流:
    操作的時候不會用到緩存區(內存)
    1個字節-8位。
    字節流可用於任何類型的對象,包括二進制對象。
  • 字符流:
    操作的時候會用到緩存區。
    一個字符char-16位。
    而字符流只能處理字符或者字符串。

字節輸入流和輸出流:InputStream和OutputStream
字符輸入流和輸出流:Reader和Writer
在這裏插入圖片描述
BufferedInputStream和FileInputStream區別:

BufferedInputStream是套在某個其他的InputStream外,起着緩存的功能,用來改善裏面那個InputStream的性能(如果可能的話),它自己不能脫離裏面那個單獨存在。FileInputStream是讀取一個文件來作InputStream。所以你可以把BufferedInputStream套在FileInputStream外,來改善FileInputStream的性能。

FileInputStream是字節流,BufferedInputStream是字節緩衝流,使用BufferedInputStream讀資源比FileInputStream讀取資源的效率高(BufferedInputStream的read方法會讀取儘可能多的字節,執行read時先從緩衝區讀取,當緩衝區數據讀完時再把緩衝區填滿。),因此,當每次讀取的數據量很小時,FileInputStream每次都是從硬盤讀入,而BufferedInputStream大部分是從緩衝區讀入。讀取內存速度比讀取硬盤速度快得多,因此BufferedInputStream效率高,且FileInputStream對象的read方法會出現阻塞;BufferedInputStream的默認緩衝區大小是8192字節。當每次讀取數據量接近或遠超這個值時,兩者效率就沒有明顯差別了。BufferedOutputStream和FileOutputStream同理,差異更明顯一些。

50.BIO、NIO、AIO 有什麼區別?

參考:Java架構直通車——BIO、NIO、AIO

51.Files的常用方法都有哪些?

Files.exists() 檢測文件路徑是否存在
Files.createFile()創建文件
Files.createDirectory()創建文件夾
Files.delete() 刪除文件或者目錄
Files.copy() 複製文件
Files.move() 移動文件
Files.size()查看文件個數
Files.read() 讀取文件
Files.write()寫入文件

52.Collection 和 Collections 有什麼區別?

java.util.Collection 是一個集合接口。它提供了對集合對象進行基本操作的通用接口方法,比如add()、remove()、size()等等。

java.util.Collections 是一個包裝類。它包含有各種有關集合操作的靜態多態方法,比如sort()、reverse()、copy()、fill()。

53.說一下HashMap和HashSet的實現原理?

HashMap 的實現原理:
HashMap是基於Hash算法實現的,
我們通過put(key,value)存儲數據,通過get(key)來獲取數據

當傳入key時,HashMap會根據Key.hashCode()計算出Hash值,根據Hash值將value保存在bucket裏 ,。

當計算出相同的Hash值時,我們稱之爲Hash衝突,HashMap 的做法是用鏈表和紅黑樹存儲相同Hash值的value,
當hash衝突的個數比較少時,使用鏈表存儲,
否則使用紅黑樹。

HashSet 的實現原理:
HashSet是基於HashMap實現的,HashSet 底層使用HashMap來保存所有元素,
因此HashSet 的實現比較簡單,相關HashSet 的操作,基本上都是直接調用底層HashMap的相關方法來完成,HashSet不允許有重複的值(通過Hashcode()和equals()方法來去重),並且元素是無序的。

54.如何實現數組和 List 之間的轉換?

		String[] arr = new String[5];
        arr[0]="a";
        arr[1]="b";
        arr[2]="c";
        arr[3]="d";
        arr[4]="e";
        List<String> list = new ArrayList<>();
        List<String> list1 = Arrays.asList(arr);


        //將list集合轉換爲數組Array
        List<Integer> ls = new ArrayList<>();
        ls.add(1);
        ls.add(2);
        ls.add(3);
        ls.add(4);
        ls.add(5);
        Object[] objects = ls.toArray();

55.在 Queue 中 poll()和 remove()有什麼區別?

poll()和remove()都將移除並且返回對頭,但是在poll()在隊列爲空時返回null,而remove()會拋出NoSuchElementException異常。

56.ArrayList 和 Vector 的區別是什麼?

ArrayList 和Vector底層是採用數組方式存儲數據

Vector:
線程同步(因爲效率較低,現在已經不太建議使用)
當Vector中的元素超過它的初始大小時,Vector會將它的容量翻倍,
ArrayList:
線程不同步,但性能很好
當ArrayList中的元素超過它的初始大小時,ArrayList只增加50%的大小

57.哪些集合類是線程安全的?

常見的併發集合:
ConcurrentHashMap:線程安全的HashMap的實現
CopyOnWriteArrayList:線程安全且在讀操作時無鎖的ArrayList
CopyOnWriteArraySet:基於CopyOnWriteArrayList,不添加重複元素
ArrayBlockingQueue:基於數組、先進先出、線程安全,可實現指定時間的阻塞讀寫,並且容量可以限制
LinkedBlockingQueue:基於鏈表實現,讀寫各用一把鎖,在高併發讀寫操作都多的情況下,性能優於ArrayBlockingQueue
ConcurrentLinkedQueue:基於鏈接節點的無界線程安全隊列。

另外還有
Vector:就比Arraylist多了個同步化機制(線程安全)。
Hashtable:就比Hashmap多了個線程安全。
Stack:繼承自Vector

58.迭代器 Iterator 是什麼?

迭代器(Iterator) 迭代器是一種設計模式,它是一個對象,它可以遍歷並選擇序列中的對象。參考:Java設計模式——迭代器模式
以ArrayList爲例,它將Iterator作爲ArrayList的一個私有內部類,內部類可以訪問外部ArrayList的數組,然後通過一個遊標cursor來遍歷整個數組的。

迭代器可以使用remove()刪除對象(因爲使用的是遊標),而直接使用foreach是不行的。而如果使用for循環for(int i=0;i<list.size();i++),如果在中途刪除了這個對象,會導致list.size()改變。
而在使用迭代器remove的時候,會調用ArrayList的remove方法,並且正確修改遊標cursor的位置。在此期間不允許list進行list.add()或者list.remove()操作,因爲這回改變expectedModCountmodCount這兩個值,導致迭代器拋出ModificationException的錯誤。

59.怎麼確保一個集合不能被修改?

採用Collections包下的unmodifiableMap方法,通過這個方法返回的map,是不可以修改的。他會報 java.lang.UnsupportedOperationException錯。

同理:Collections包也提供了對list和set集合的方法。
Collections.unmodifiableList(List)
Collections.unmodifiableSet(Set)

unmodifiableMap會對Map進行包裝,返回一個不可修改的Map。

60.併發和並行有什麼區別?

併發是指一個處理器同時處理多個任務。並行是指多個處理器或者是多核的處理器同時處理多個不同的任務。併發是邏輯上的同時發生(simultaneous),而並行是物理上的同時發生。

61.線程和進程之間的區別?

1、進程是一段正在執行的程序,是資源分配的基本單元,而線程是CPU調度的基本單元。
2、進程間相互獨立,進程之間不能共享資源。一個進程至少有一個線程,同一進程的各線程共享整個進程的資源(寄存器、堆棧、上下文)。
3、線程的創建和切換開銷比進程小,換句話說線程是輕量級的進程。

62.Java線程之間怎麼通信?

兩種通信機制:共享內存機制和消息通信機制。

  1. 共享內存機制

使用同步方法:
syncrhoized加鎖的線程的Object類的wait()/notify()/notifyAll()
ReentrantLock類加鎖的線程的Condition類的await()/signal()/signalAll()
CountDownLatch
CyclicBarrier

或者使用:輪詢+volatile關鍵字

public class TestSync {
    // 定義一個共享變量來實現通信,它需要是volatile修飾,否則線程不能及時感知
    static volatile boolean notice = false;

    public static void main(String[] args) {
        List<String>  list = new ArrayList<>();
        // 實現線程A
        Thread threadA = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                list.add("abc");
                System.out.println("線程A向list中添加一個元素,此時list中的元素個數爲:" + list.size());
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (list.size() == 5)
                    notice = true;
            }
        });
        // 實現線程B
        Thread threadB = new Thread(() -> {
            while (true) {
                if (notice) {
                    System.out.println("線程B收到通知,開始執行自己的業務...");
                    break;
                }
            }
        });
        // 需要先啓動線程B
        threadB.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 再啓動線程A
        threadA.start();
    }
}
  1. 消息通信機制

使用java.io.PipedInputStream 和 java.io.PipedOutputStream進行通信。
一個線程發送數據到輸出管道,另一個線程從輸入管道讀數據。

package pipeInputOutput;

import java.io.IOException;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;

public class Run {

    public static void main(String[] args) {

        try {
            WriteData writeData = new WriteData();
            ReadData readData = new ReadData();

            PipedInputStream inputStream = new PipedInputStream();
            PipedOutputStream outputStream = new PipedOutputStream();

            //將兩個Stream之間產生通信鏈接,這樣才能將數據進行輸入輸出,下面兩種方式都可以,其一即可
            //inputStream.connect(outputStream);
            outputStream.connect(inputStream);

            //開啓讀線程
            ThreadRead threadRead = new ThreadRead(readData, inputStream);
            threadRead.start();

            Thread.sleep(2000);

            //開啓寫線程
            ThreadWrite threadWrite = new ThreadWrite(writeData, outputStream);
            threadWrite.start();

        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }

}

63.守護線程是什麼?

JAVA分爲守護線程daemon thread和用戶線程user thread。
當 JVM 中不存在任何一個正在運行的非守護線程(用戶線程)時,則 JVM 進程即會退出。
換句話說,守護線程擁有自動結束自己生命週期的特性,而非守護線程不具備這個特點。

通常來說,守護線程經常被用來執行一些後臺任務,但是呢,你又希望在程序退出時,或者說 JVM 退出時,線程能夠自動關閉,此時,守護線程是你的首選。

JVM 中的垃圾回收線程就是典型的守護線程,如果說不具備該特性,會發生什麼呢?
當 JVM 要退出時,由於垃圾回收線程還在運行着,導致程序無法退出。

64.創建線程有哪幾種方式?

  1. 繼承Thread類,並重寫run()方法。
class Runner extends Thread{
    @Override
    public void run() {
        super.run();
        System.out.println("ok");
    }

    public static void main(String[] args) {
        new Runner().start();
    }
}
  1. 繼承Runnable接口,並重寫run()方法
class Runner implements Runnable{
    @Override
    public void run() {
        System.out.println("ok");
    }
    public static void main(String[] args) {
        new Thread(new Runner()).start();
    }
}

實際上Thread也是繼承了run方法的。

  1. 通過Callable和Future創建線程。
class Runner implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("運行ok");
        return "success";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Runner runner=new Runner();
        FutureTask<String> futureTask=new FutureTask<>(runner);
        new Thread(futureTask).start();
        System.out.println("執行後返回值:"+futureTask.get());
    }
}
  1. 通過線程池
class Runner{
    public static void main(String[] args) {
        ExecutorService pool=Executors.newFixedThreadPool(2);
        for (int i=0;i<5;i++){
            pool.submit(()->{//可以使用Callable或者Runnable
                System.out.println("ok");
            });
        }
    }
}

65.說一下 runnable 和 callable 有什麼區別?

1、兩者最大的不同點是:實現Callable接口的任務線程能返回執行結果;而實現Runnable接口的任務線程不能返回結果;
2、Callable接口的call()方法允許拋出異常;而Runnable接口的run()方法的異常只能在內部消化,不能繼續上拋;

Callable接口支持返回執行結果,此時需要調用FutureTask.get()方法實現,此方法會阻塞主線程直到獲取‘將來’結果;當不調用此方法時,主線程不會阻塞!

66.線程有哪些狀態?

線程狀態分別是:新建、就緒、運行、阻塞、死亡。
在這裏插入圖片描述

67.sleep() 和 wait() 有什麼區別?

  1. 同步鎖的對待不同:
    sleep()後,程序並不會不釋放同步鎖。
    wait()後,程序會釋放同步鎖。

  2. 用法的不同:
    sleep()可以用時間指定來使他自動醒過來。如果時間不到你只能調用interreput()來強行打斷。
    wait()可以用notify()直接喚起。

  3. 屬於不同的類:
    sleep()的類是Thread。
    wait()的類是Object。

68.notify()和 notifyAll()有什麼區別?

notifyAll調用後,會將全部線程由等待池移到鎖池,然後參與鎖的競爭,競爭成功則繼續執行,如果不成功則留在鎖池等待鎖被釋放後再次參與競爭。而notify只會隨機喚醒一個線程。

鎖池:假設線程A已經擁有了某個對象(注意:不是類)的鎖,而其它的線程想要調用這個對象的某個synchronized方法(或者synchronized塊),由於這些線程在進入對象的synchronized方法之前必須先獲得該對象的鎖的擁有權,但是該對象的鎖目前正被線程A擁有,所以這些線程就進入了該對象的鎖池中。
等待池:假設一個線程A調用了某個對象的wait()方法,線程A就會釋放該對象的鎖後,進入到了該對象的等待池中

69.線程的 run()和 start()有什麼區別?

調用start方法方可啓動線程,而run方法只是thread的一個普通方法調用,還是在主線程裏執行。

70.創建線程池有哪幾種方式?

Executors目前提供了5種不同的線程池創建配置:

1、newCachedThreadPool(),它是用來處理大量短時間工作任務的線程池,具有幾個鮮明特點:它會試圖緩存線程並重用,當無緩存線程可用時,就會創建新的工作線程;如果線程閒置時間超過60秒,則被終止並移除緩存;長時間閒置時,這種線程池,不會消耗什麼資源。其內部使用SynchronousQueue作爲工作隊列。

2、newFixedThreadPool(int nThreads),重用指定數目(nThreads)的線程,其背後使用的是無界的工作隊列,任何時候最多有nThreads個工作線程是活動的。這意味着,如果任務數量超過了活動線程數目,將在工作隊列中等待空閒線程出現;如果工作線程退出,將會有新的工作線程被創建,以補足指定數目nThreads

3、newSingleThreadExecutor(),它的特點在於工作線程數目限制爲1,操作一個無界的工作隊列,所以它保證了所有的任務都是被順序執行,最多會有一個任務處於活動狀態,並且不予許使用者改動線程池實例,因此可以避免改變線程數目

4、newSingleThreadScheduledExecutor()和newScheduledThreadPool(int corePoolSize),創建的是個ScheduledExecutorService,可以進行定時或週期性的工作調度,區別在於單一工作線程還是多個工作線程。

5、newWorkStealingPool(int parallelism),這是一個經常被人忽略的線程池,Java 8 才加入這個創建方法,其內部會構建ForkJoinPool,利用Work-Stealing算法,並行地處理任務,不保證處理順序。

6、使用new ThreadPoolExecutor()自定義。

71.線程池都有哪些狀態?

  1. Running: 接受新task, 處理等待的task;
  2. ShutDown: 不接受新task,但處理等待的task;
  3. Stop: 不接受新task, 不處理等待的task, 嘗試打斷正在執行的task;
  4. Tidying:但所有task都被終止, worCount == 0的時候(workCount是指有效的線程數);
  5. Terminated: 執行完terminated()方法;

  • Running -> ShutDown:
    執行shutdown();

  • Running or ShutDown --> Stop:
    執行shutdownNow();

  • Stop --> Tidying
    當pool爲空時

  • ShutDown --> Tidying
    當queue 和 pool都爲空時

  • Tidying --> Terminated
    當terminated()方法結束時;

72.線程池中 submit()和 execute()方法有什麼區別?

public interface ExecutorService extends Executor {...}

可以看出ExecutorService接口繼承自Executor,而這個Executor接口需要實現execute()方法。

public interface Executor {
    void execute(Runnable command);
}

execute()方法的入參爲一個Runnable,返回值爲void。

而我們再來看submmit()方法是怎麼來的:

public interface ExecutorService extends Executor {
  ...
  <T> Future<T> submit(Callable<T> task);

  <T> Future<T> submit(Runnable task, T result);

  Future<?> submit(Runnable task);
  ...
}

它是ExecutorService的實現類實現的,入參可以爲Callable<T>,也可以爲Runnable,而且方法有返回值Future<T>

總結,從上面的源碼以及講解可以總結execute()和submit()方法的區別:

  1. 接收的參數不一樣;
  2. submit()有返回值Future,而execute()沒有;
  3. submit()可以進行Exception處理
    可以通過對Future.get()進行拋出異常的捕獲,然後對其進行處理。

73.Java是如何保證多線程運行安全的?

1.原子性:提供互斥訪問,同一時刻只能有一個線程對數據進行操作,(atomic,synchronized);

2.可見性:一個線程對主內存的修改可以及時地被其他線程看到,(synchronized,volatile);

3.有序性:一個線程觀察其他線程中的指令執行順序,由於指令重排序,該觀察結果一般雜亂無序,(happens-before原則)。

導致線程存在安全問題的原因:

  1. CPU緩存機制導致可見性問題
  2. 線程切換導致原子性問題
  3. 編譯優化導致的有序性問題

74.多線程鎖的升級原理是什麼?

鎖的級別從低到高:
偏向鎖 -> 輕量級鎖 -> 重量級鎖

鎖分級別原因:
沒有優化以前,sychronized是重量級鎖(悲觀鎖), JVM 對 sychronized 關鍵字進行了優化,把鎖分爲 無鎖、偏向鎖、輕量級鎖、重量級鎖 狀態。鎖可以升級不可以降級,但是偏向鎖狀態可以被重置爲無鎖狀態。
(注意:無鎖不能算在鎖升級裏面)。

  • 無鎖:
    沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功,其他修改失敗的線程會不斷重試直到修改成功。
    無鎖主要採用CAS(還用到了內存屏障(有序性)、可見性和原子性)。

  • 偏向鎖:
    偏向鎖,指的就是偏向加鎖線程(原因在爲什麼要引入偏向鎖中),該線程是不會主動釋放偏向鎖的,這樣可以降低加鎖和解鎖帶來的性能開銷,只有當其他線程嘗試競爭偏向鎖纔會被釋放或者升級
    偏向鎖的釋放或者升級,需要判斷鎖的對象頭和棧幀:比較當前線程的threadID和Java對象頭中的threadID是否一致。如果不一致,再判斷記錄裏的線程是否存活,如果不存活,則將對象頭設置成無鎖狀態,並撤銷偏向鎖;如果線程處於存活狀態,升級爲輕量級鎖。

當一個線程訪問同步代碼塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,以後該線程再進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word裏是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。

爲什麼要引入偏向鎖?
因爲經過HotSpot的作者大量的研究發現,大多數時候是不存在鎖競爭的,常常是一個線程多次獲得同一個鎖,因此如果每次都要競爭鎖會增大很多沒有必要付出的代價,爲了降低獲取鎖的代價,才引入的偏向鎖。

  • 輕量級鎖:
    輕量級鎖考慮的是競爭鎖對象的線程不多,而且線程持有鎖的時間也不長的情景。因爲阻塞線程需要CPU從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失了,因此這個時候就乾脆不阻塞這個線程,讓它自旋這等待鎖釋放。
    當前只有一個等待線程,則該線程將通過自旋進行等待。但是當自旋超過一定的次數時,輕量級鎖便會升級爲重量級鎖;當一個線程已持有鎖,另一個線程在自旋,而此時又有第三個線程來訪時,輕量級鎖也會升級爲重量級鎖。

  • 重量級鎖:
    指當有一個線程獲取鎖之後,其餘所有等待獲取該鎖的線程都會處於阻塞狀態,防止CPU空轉。

其他知識點參考:Java架構直通車——爲什麼線程切換會導致用戶態與內核態的切換?

75.什麼是死鎖?怎麼防止死鎖?

死鎖是指多個線程(或者進程)在執行過程中,由於競爭資源造成的一種阻塞。

死鎖發生的原因:
1、資源互斥,即當資源被一個線程使用(佔有)時,別的線程不能使用
2、不可搶佔,資源請求者不能強制從資源佔有者手中奪取資源,資源只能由資源佔有者主動釋放。
3、請求和保持,即當資源請求者在請求其他的資源的同時保持對原有資源的佔有。
4、循環等待,即存在一個等待隊列:P1佔有P2的資源,P2佔有P3的資源,P3佔有P1的資源。這樣就形成了一個等待環路。

防止死鎖:

  1. 有序資源分配法
    對它所必須使用的而且屬於同一類的所有資源,必須一次申請完;在申請不同類資源時,必須按各類設備的編號依次申請。
  2. 銀行家算法
    在避免死鎖方法中允許進程動態地申請資源,但系統在進行資源分配之前,應先計算此次分配資源的安全性,若分配不會導致系統進入不安全狀態,則分配,否則等待。

76.什麼是 java 序列化?什麼情況下需要序列化?

序列化:將 Java 對象轉換成字節流的過程。
反序列化:將字節流轉換成 Java 對象的過程。

當 Java 對象需要在網絡上傳輸 或者 持久化存儲到文件中時,就需要對 Java 對象進行序列化處理。

  • 網絡傳輸:我們將系統拆分成多個服務之後,服務之間傳輸對象,不管是何種類型的數據,都必須要轉成二進制流來傳輸,接受方收到後再轉爲數據對象。
  • 數據持久化:比如一個電商平臺,有數萬個用戶併發訪問的時候會產生數萬個session 對象,這個時候內存的壓力是很大的。我們可以把session對象序列化到硬盤中,需要時在反序列化,減少內存壓力。

序列化的實現:類實現 Serializable 接口,這個接口沒有需要實現的方法。實現 Serializable 接口是爲了告訴 jvm 這個類的對象可以被序列化。然後使用一個輸出流並通過writeObect(Obejct)方法就可以將實現對象寫出。

比如:

		OutputStream op = new FileOutputStream("io"+File.separator+"a.txt");
        ObjectOutputStream ops = new ObjectOutputStream(op);
        ops.writeObject(new Person("vae",1));
         
        ops.close();

注意事項:
聲明爲 static 和 transient 的成員變量,不能被序列化。static 成員變量是描述類級別的屬性,transient 表示臨時數據

77.爲什麼要使用克隆?

開發過程中,有時會遇到把現有的一個對象的所有成員屬性拷貝給另一個對象的需求。

淺拷貝介紹:
它會創建一個新對象,其成員變量是基本類型,拷貝的就是基本類型的值;如果屬性是引用類型,拷貝的就是內存地址 。

深拷貝介紹:
(1) 對於基本數據類型的成員對象,因爲基礎數據類型是值傳遞的,所以是直接將屬性值賦值給新的對象。基礎類型的拷貝,其中一個對象修改該值,不會影響另外一個(和淺拷貝一樣)。
(2) 對於引用類型,比如數組或者類對象,深拷貝會新建一個對象空間,然後拷貝里面的內容,所以它們指向了不同的內存空間。改變其中一個,不會對另外一個也產生影響。
(3) 對於有多層對象的,每個對象都需要實現 Cloneable 並重寫 clone() 方法,進而實現了對象的串行層層拷貝。
(4) 深拷貝相比於淺拷貝速度較慢並且花銷較大。

78.Java實現深拷貝的兩種方式?

通過序列化和反序列化深拷貝參考:
原型模式

通過clone接口:

public class Demo implements Cloneable {
 
    private String name;
 
    private String value;
 
    private DemoInternal demoInternal;
 
    /*省略getter和setter方法*/
 
    @Override
    public Demo clone() {
        Demo demo = null;
        try {
            demo = (Demo) super.clone(); //淺複製
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        demo.demoInternal = demoInternal.clone(); //深度複製
        return demo;
    }
}
public class DemoInternal implements Cloneable {
 
    private String internalName;
 
    private String internalValue;
 
    /*省略getter和setter方法*/
 
    @Override
    public DemoInternal clone() {
        DemoInternal demoInternal = null;
        try {
            demoInternal = (DemoInternal) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return demoInternal;
    }

如果包含了數組,可以new一個新的數組來重新開闢空間。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章