這個項目是從20年底就立好的 flag,通過幾年的學習,回過頭再去看不少知識點又有新的理解。因此趁着找實習的準備,結合之前的學習儲備,建立一個主要針對應屆生和初學者的 Java 開源知識項目,專一 Java 後端面試題 + 解析 + 重點知識詳解 + 精選文章的開源項目,但願它能伴隨你我一直進步!java
說明:此項目我確實有很用心在作,內容所有是我參考了諸多博主(已註明出處),資料,N本書籍,以及結合本身理解,從新繪圖,從新組織語言等等所制。我的之力綿薄,或有不足之處,在所不免,但更新/完善會一直進行。你們的每個 Star 都是對個人鼓勵 !但願你們能喜歡。c++
注:全部涉及圖片未使用網絡圖牀,文章等均開源提供給你們。git
項目名: Java-Ideal-Interviewgithub
Github 地址: Java-Ideal-Interview - Github 面試
Gitee 地址:Java-Ideal-Interview - Gitee(碼雲) 正則表達式
持續更新中,在線閱讀將會在後期提供,若認爲 Gitee 或 Github 閱讀不便,可克隆到本地配合 Typora 等編輯器溫馨閱讀算法
若 Github 克隆速度過慢,可選擇使用國內 Gitee 倉庫編程
說明:本章主要涉及到了:Object類、Scanner類、String類、StringBuffer和StringBuilder、Arrays工具類、基本類型包裝類、正則表達式、System類、Math、Random類、BigInteger和BigDecimal類、Date、DateFormat和Calendar類後端
補充:因爲 Object 以及 String 類屬於高頻內容,因此總結題目以及小點知識以前,會對其作一個基本的概括複習。數組
在講解這些常見類以前,咱們不得不簡單的提一下什麼是API,先貼一組百度百科的解釋:
API(Application Programming Interface,應用程序編程接口)是一些預先定義的函數,目的是提供應用程序與開發人員基於某軟件或硬件得以訪問一組例程的能力,而又無需訪問源碼,或理解內部工做機制的細節。
簡單的說:就是 Java 中有好多現成的類庫,其中封裝了許多函數,只提供函數名和參數,但隱藏了函數的具體實現,這些可見的部分做爲與外界聯繫的橋樑,也就是咱們所稱的 API ,不過因爲Java是開源的,因此這些隱藏的實現咱們也是能夠看到的。
這就對應了前面學習中的一句話,子類構造方法默認訪問父類的構造是無參構造
咱們須要瞭解的方法又有哪些呢?
A:hashCode() B:getClass() C: finalize() D:clone() E:wait() F:notify() G:notifyAll()
咱們須要掌握的方法又有哪些呢?
A:toString() B:equals()
方法總結:
// 1. 返回此Object的運行時類,是一個 native方法,同時由於使用了final關鍵字修飾,故不容許子類重寫。 public final native Class<?> getClass() // 2. 用於返回對象的哈希碼,是一個native方法,例如主要涉及在 HashMap 中。 public native int hashCode() // 3. 比較兩個對象是否相同,默認比較的是地址值是否相同。而比較地址值是沒有意義的,因此,通常子類也會重寫該方法。 public boolean equals(Object obj) // 4. 實現對象的克隆,包括成員變量的數據複製,分爲深淺克隆兩種。是一個native方法。 protected native Object clone() throws CloneNotSupportedException // 5. 返回類的名字@該實例16進制的哈希碼字符串。所以建議Object 全部的子類都重寫此方法。 public String toString() // 6. 喚醒一個在此對象監視器上等待的線程(監視器理解爲鎖)。如有多個線程在等待只會任意喚醒一個。是一個 native方法,且不能重寫。 public final native void notify() // 7. 同 notify(),區別是會喚醒在此對象監視器上等待的全部線程。 public final native void notifyAll() // 8. 意爲暫停線程的執行.是一個native方法。注意:釋放了鎖,而sleep方法不釋放鎖。timeout是等待時間。 public final native void wait(long timeout) throws InterruptedException // 9. 多了一個nanos參數,表明額外時間(以毫微秒爲單位,範圍是 0-999999)。 因此時間最後要計算總和。 public final void wait(long timeout, int nanos) throws InterruptedException // 10同前兩個 wait() 只不過該方法一直等待 public final void wait() throws InterruptedException // 11. 在對象將被垃圾回收器清除前調用,但不肯定時間 protected void finalize() throws Throwable { }
String 是一個很經常使用的類,簡單概括一下常見的方法
構造方法
// 1. 空構造 public String() // 2. 把字節數組轉換成字符串 public String(byte[] bytes) // 3. 把字節數組的一部分轉換成字符串 public String(byte[] bytes,int offset,int length) // 4. 把字符數組轉換成字符串 public String(char[] value) // 5. 把字符數組的一部分轉換成字符串 public String(char[] value,int offset,int count) // 6. 把字符串常量值轉換成字符串 public String(String original) // 7. 下面的這一個雖然不是構造方法,可是結果也是一個字符串對象 String s = "hello";
簡單總結:String類的構造方法能夠將 字節、字符數組、字符串常量(所有或者部分)轉換爲字符串類型
判斷方法
// 1. 比較字符串的內容是否相同,區分大小寫 boolean equals(Object obj) // 2. 比較字符串的內容是否相同,不區分大小寫 boolean equalsIgnoreCase(String str) // 3. 判斷大字符串中是否包含小字符串 boolean contains(String str) // 4. 判斷某個字符串是否以某個指定的字符串開頭 boolean startsWith(String str) // 5. 判斷某個字符串是否以某個指定的字符串結尾 boolean endsWith(String str) // 6. 判斷字符串是否爲空 boolean isEmpty() 注意: String s = 「」; // 字符串內容爲空 String s = null; // 字符串對象爲空
獲取方法
// 1. 獲取字符串的長度 int length() // 2. 獲取指定索引的字符 char charAt(int index) // 3. 返回指定字符在此字符串中第一次出現的索引 int indexOf(int ch) // 爲何這裏是int而不是char? // 緣由是:‘a’和‘97’其實都能表明‘a’ int方便 // 4. 返回指定字符串在此字符串中第一次出現的索引 int indexOf(String str) // 5. 返回指定字符在此字符串中從指定位置後第一次出現的索引 int indexOf(int ch,int fromIndex) // 6. 返回指定字符串在此字符串中從指定位置後第一次出現的索引 int indexOf(String str, int fromIndex) // 7. 從指定位置開始截取字符串,默認到末尾 String substring(int start) // 8. 從指定位置開始指定位置結束截取字符串 String substring(int start, int end)
轉換方法
// 1. 把字符串轉換爲字節數組 byte[] getBytes() // 2. 把字符串轉換成字符數組 char[] toCharArray() // 3. 把字符數組轉換成字符串 static String valueOf(char[] chs) // 3. 把int類型的數據轉換成字符串 static String valueOf(int i) // 注意:String類的valueOf方法能夠把任何類型的數據轉換成字符串! // 4. 把字符串轉換成小寫 String toLowerCase() // 5. 把字符串轉換成大寫 String toUpperCase() // 7. 把字符串拼接 String concat(String str)
其餘方法
// 1. 替換功能 String replace(char old,char new) String replace(String old,String new) // 2. 去除字符串兩端空格 String trim() // 3. 按字典比較功能 int compareTo(String str) int compareToIgnoreCase(String str)
==
:若是比較的對象是基本數據類型,則比較的是數值是否相等;若是比較的是引用數據類型,則比較的是對象
的地址值是否相等。
equals()
:equals 方法不能用於基本數據類型的變量,若是沒有對 equals 方法進行重寫,則比較的是引用類型的變量所指向的對象的地址。通常會選擇重寫此方法,來比較兩個對象的內容是否相等,相等則返回 true。
例如一個 Student 類,new 兩個對象出來,單純的想比較內容是否相同如何作呢。
public class Student { private String name; public int age; // get set ... }
經過 equals() 比較兩個對象是否相同,默認狀況下,比較的是地址值是否相同。而比較地址值是沒有意義的,因此,通常子類也會重寫該方法。在諸多子類,如String、Integer、Date 等均重寫了 equals() 方法
改進思路:咱們能夠將比較地址值轉變爲比較成員變量
//重寫v1.0 public boolean equals(Object o) { Student s = (Student) o; if (this.name.equals(s.name) && this.age == s.age) { return true; } else { return false; } }
//重寫v2.0 (可做爲最終版) public boolean equals(Object o) { if (this.name == o) { return true; } //測試它左邊的對象是不是它右邊的類的實例,返回 boolean 的數據類型。 if (!(o instanceof Student)) { return false; } Student s = (Student) o; return this.name.equals(s.name) && this.age == s.age; }
// IDEA自動生成版 @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Student student = (Student) o; return age == student.age && Objects.equals(name, student.name); }
hashCode()
方法是 Object 類中的一個本地方法(用 c 語言或 c++ 實現的),會返回該對象的哈希碼,也稱爲散列碼;其本質是返回一個 int 整數。哈希碼的做用是肯定該對象在哈希表中的索引位置。能夠經過散列碼,在散列表中根據「鍵」快速的檢索出對應的「值」。從而快速找到須要的對象,而後進行判斷是否是同一個對象。
public native int hashCode();
equals()
方法是Object 類中的一個方法,若是沒有對 equals 方法進行重寫,則比較的是引用類型的變量所指向的對象的地址。通常會選擇重寫此方法,來比較兩個對象的內容是否相等,相等則返回 true。
總結:單考慮目的二者是差很少的,都是用來對比兩個對象是否相等一致。
重寫 equals() 裏面的內容通常比較全面周詳,可是效率就比較低,例如:若是集合中如今已經有2000個元素,那麼第2001個元素加入集合時,它就要調用 2000次 equals方法。
而使用 hashCode() ,其使用的哈希算法也稱爲散列算法,是將數據依特定算法直接指定到一個地址上,因此 hashCode() 這種造成 hash 碼的方式比較是比較高效的。
hashCode() 方法不是一個 100% 可靠的方法,個別狀況下,不一樣的對象生成的 hashcode 也可能會相同。
若是大量內容都是用 equals() 去比對,效率顯然是比較低的,因此每次比對以前都去使用 hashCode() 去對比,若是返回的 hashCode 不一樣,表明兩個對象確定不相同,就能夠直接返回結果了。若是 hashCode 相同,又爲了保證其絕對可靠,因此使用 equals() 再次進行比對,一樣是相同,就保證了這兩個對象絕對相同。
若是重寫了 equals() 而未重寫 hashcode() 方法,可能就會出現兩個字面數據相同的對象(例以下面 stu1 和 stu2) equals 相同(由於 equals 都是根據對象的特徵進行重寫的),但 hashcode 不相同的狀況。
public class Student { private String name; public int age; // get set ... // 重寫 equals() 不重寫 hashcode() } -------------------------------------------- Student stu1 = new Student("BWH_Steven",22); Student stu2 = new Student("BWH_Steven",22); -------------------------------------------- stu1.equals(stu2); // true stu1.hashCode(); // 和 stu2.hashCode(); 結果不一致 stu2.hashCode();
若是把對象保存到 HashTable、HashMap、HashSet 等中(不容許重複),這種狀況下,去查找的時候,因爲都是先使用 hashCode() 去對比,若是返回的 hashCode 不一樣,則會認爲對象不一樣。能夠存儲,從內容上看,明顯就重複了。
因此通常的地方不須要重寫 hashcode() ,只有當類須要放在 HashTable、HashMap、HashSet 等hash 結構的集合時纔會去重寫。
補充:阿里巴巴 Java 開發手冊關於 hashCode 和 equals 的處理遵循規則:
- 只要重寫 equals,就必須重寫 hashCode。
- 由於 Set 存儲的是不重複的對象,依據 hashCode 和 equals 進行判斷,因此 Set 存儲的對象必須重寫這兩個方法。
- 若是自定義對象作爲 Map 的鍵,那麼必須重寫 hashCode 和 equals。
- String 重寫了 hashCode 和 equals 方法,因此咱們能夠很是愉快地使用 String 對象做爲 key 來使用。
淺拷貝(淺克隆):基本數據類型爲值傳遞,對象類型爲引用傳遞(二者同生共死)
深拷貝(深克隆):對於對象或者數值,全部元素或者屬性均徹底複製,與原對象脫離(真正意義上的複製, 二者獨立無關)
舉例:
public class Book { private String name; // 姓名 private int price; // 價格 private Partner partner; // 合做夥伴 // 省略構造函數、get set、toString 等 }
public class Partner{ private String name; // 省略構造函數、get set、toString 等 }
淺拷貝用到拷貝,首先就對 Book 類進行處理
public class Book implements Cloneable{ private String name; // 姓名 private int price; // 價格 private Partner partner; // 合做夥伴 @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } // 省略構造函數、get set、toString 等 }
再來測試一下
public class Test { public static void main(String[] args) throws CloneNotSupportedException { // 初始化一個合做夥伴類型 Partner partner = new Partner("張三"); // 帶參賦值 Book bookA = new Book("理想二旬不止", 66, partner); // B 克隆 A Book bookB = (Book) bookA.clone(); System.out.println("A: " + bookA.toString()); System.out.println("A: " + bookA.hashCode()); System.out.println("B: " + bookB.toString()); System.out.println("B: " + bookB.hashCode()); } }
執行結果
A: Book{name='理想二旬不止', price=66, partner=Partner{name=張三}}
A: 460141958
B: Book{name='理想二旬不止', price=66, partner=Partner{name=張三}}
B: 1163157884
結果很是明顯,書籍信息是一致的,可是內存地址是不同的,也就是說確實克隆成功了,打印其 hashCode 發現二者並不相同,說明不止指向同一個,也是知足咱們要求的
到這裏並無結束,你會發現仍是有問題,當你刊印的過程當中修改一些值的內容的時候,你看看效果
public class Test { public static void main(String[] args) throws CloneNotSupportedException { // 初始化一個合做夥伴類型 Partner partner = new Partner("張三"); // 帶參賦值 Book bookA = new Book("理想二旬不止", 66, partner); // B 克隆 A Book bookB = (Book) bookA.clone(); // 修改數據 bookB.getPartner().setName("李四"); bookB.setPrice(44); System.out.println("A: " + bookA.toString()); System.out.println("A: " + bookA.hashCode()); System.out.println("B: " + bookB.toString()); System.out.println("B: " + bookB.hashCode()); } }
執行結果
A: Book{name='理想二旬不止', price=66, partner=Partner{name=李四}}
A: 460141958
B: Book{name='理想二旬不止', price=66, partner=Partner{name=李四}}
B: 1163157884
???這不對啊,B 明明是克隆 A 的,爲何我在克隆後,修改了 B 中兩個值,可是 A 也變化了啊
這就是典型的淺克隆,在 Book 類,當字段是引用類型,例如 Partner 這個合做夥伴類,就是咱們自定義的類,這種狀況不復制引用的對象,所以,原始對象和複製後的這個Partner對象是引用同一個對象的。而做爲基本類型的的值就沒事。
如何解決上面的問題呢,咱們須要重寫主類的 clone 的內容(改成深拷貝),同時在引用類型中也實現淺拷貝
A:被引用類型實現淺克隆
public class Partner implements Cloneable { private String name; @Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } // 省略構造函數、get set、toString 等 }
B:修改引用類 cloen 方法
public class Book implements Cloneable{ private String name; // 姓名 private int price; // 價格 private Partner partner; // 合做夥伴 @Override protected Object clone() throws CloneNotSupportedException { Object clone = super.clone(); Book book = (Book) clone; book.partner =(Partner) this.partner.clone(); return clone; } // 省略構造函數、get set、toString 等 }
C:測試一下
public class Test { public static void main(String[] args) throws CloneNotSupportedException { // 初始化一個合做夥伴類型 Partner partner = new Partner("張三"); // 帶參賦值 Book bookA = new Book("理想二旬不止", 66, partner); // B 克隆 A Book bookB = (Book) bookA.clone(); // 修改數據 partner.setName("李四"); System.out.println("A: " + bookA.toString()); System.out.println("A: " + bookA.hashCode()); System.out.println("B: " + bookB.toString()); System.out.println("B: " + bookB.hashCode()); } }
執行效果
A: Book{name='理想二旬不止', price=66, partner=Partner{name=李四}}
A: 460141958
B: Book{name='理想二旬不止', price=66, partner=Partner{name=張三}}
B: 1163157884
能夠看到,B 克隆 A 後,修改 A 中 合做夥伴 的值,沒有受到影響,這也就是咱們一般意義上想要實現的效果了。
主要目的仍是爲了簡化輸出
在類中重寫toString()後,輸出類對象就變得有了意義(輸出s 和 s.toString()是同樣的 ,不寫也會默認調用),變成了咱們實實在在的信息 ,例如 Student{name='admin', age=20},而不是上面的 cn.ideal.pojo.Student@1b6d3586
若是咱們想要屢次輸出 類中的成員信息,就須要屢次書寫 ge t方法(每用一次就得寫)
toString() 方法,返回該對象的字符串表示。
Object
類的toString
方法返回一個字符串,該字符串由類名(對象是該類的一個實例)at 標記符@
和此對象哈希碼的無符號十六進制表示組成。換句話說,該方法返回一個字符串,它的值等於:代碼:
getClass().getName()+ '@' + Integer.toHexString(hashCode())
一般咱們但願,
toString
方法會返回一個「以文本方式表示」 此對象的字符串。結果應是一個簡明但易於讀懂的信息表達式。所以建議全部子類都重寫此方法。
答案:不會
/* * 字符串特色:一旦被賦值,就不能改變 */ public class StringDemo { public static void main(String[] args) { String s = "Hello"; s += "World"; System.out.println("s:" + s); } } //運行結果: s:HelloWorld
解釋:不能改變是指字符串對象自己不能改變,而不是指對象的引用不能改變,上述過程當中,字符串自己的內容是沒有任何變化的,而是分別建立了三塊內存空間,(Hello) (World) (HelloWorld) Hello + World 拼接成 HelloWorld 這時,s 不指向原來那個 「Hello」 對象了,而指向了另外一個String對象,內容爲 「HelloWorld 」 ,原來那個對象還存在內存中,只是 s 這個引用變量再也不指向它了。
總結:開發中,儘可能少使用 + 進行字符串的拼接,尤爲是循環內,咱們更加推薦使用StringBuild、StringBuffer。
經過 new 構造函數建立字符串對象。String s = new String("Hello"); 系統會先建立一個匿名對象 "Hello" 存入堆內存,然後 new 關鍵字會在堆內存中又開闢一塊新的空間,而後把"Hello"存進去,而且把地址返回給棧內存中的 s, 剛纔的匿名對象 "Hello" 就變成了一個垃圾對象,由於它沒有被任何棧中的變量指向,會被GC自動回收。
直接賦值。如String str = "Hello"; 首先會去字符串常量池中找有沒有一個"Hello"對象,若是沒有,則新建一個,而且入池,因此此種賦值有一個好處,下次若是還有 String 對象也用直接賦值方式定義爲「Hello」, 則不須要開闢新的堆空間,而仍然指向這個池中的"Hello"。
//二者的區別 String s = new String("Hello"); String s = "Hello";
總結:前者new一個對象,「hello」隱式建立一個對象,後者只有「Hello」建立一個對象,在開發中,儘可能使用 String s = "Hello" 的方式,效率比另外一種高。
前面咱們用字符串作拼接,比較耗時而且也耗內存(每次都會構造一個新的string對象),而這種拼接操做又是比較常見的,爲了解決這個問題,Java就提供了兩個字符串緩衝區類。StringBuffer和StringBuilder供咱們使用。
簡單比較:
String:長度大小不可變
StringBuffer:長度可變、線程安全、速度較慢
StringBuilder:長度可變、線程不安全、速度最快
解釋:
在執行速度方面的比較:StringBuilder > StringBuffer
StringBuffer與StringBuilder,他們是字符串變量,是可改變的對象,每當咱們用它們對字符串作操做時,其實是在一個對象上操做的,不像String同樣建立一些對象進行操做,因此速度就快了。
StringBuilder:線程非安全的
StringBuffer:線程是安全的(synchronized關鍵字進行修飾)
當咱們在字符串緩衝區被多個線程使用時,JVM 不能保證 StringBuilder 的操做是安全的,雖然他的速度最快,可是能夠保證 StringBuffer 是能夠正確操做的。固然大多數狀況下就是咱們是在單線程下進行的操做,因此大多數狀況下是建議用StringBuilder而不用StringBuffer的,就是速度的緣由。
對於三者使用的總結:
首先java並不支持運算符重載(String類中的 「+」 和 「+=」 是 Java 中僅有的兩個重載過的運算符),因此咱們能夠經過 「+」 符號 將多個字符串進行拼接
將圖中代碼(使用了 「+」 符號)利用 javap -c filename
反編譯
咱們能夠看到代碼被編譯器自動優化成使用StringBuilder方式拼接,運行效率獲得了保證
下面一個案例 數組拼接成指定格式的字符串 代碼中使用了循環語句
// 在循環中經過String拼接字符串 public class StringBuilderDemo { public static void main(String[] args) { String[] arr = {"Hello", "World", "!!!"}; String s1 = arrayToString(arr); System.out.println(s1); } public static String arrayToString(String[] arr) { String s = ""; s += "["; for (int x = 0; x < arr.length; x++) { if (x == arr.length - 1) { s += arr[x]; } else { s += arr[x]; s += ", "; } } s += "]"; return s; } } //運行結果 [Hello, World, !!!]
使用String方式進行拼接,咱們反編譯能夠看到,StringBuilder被建立在循環的內部,這意味着每循環一次就會建立一次StringBuilder對象,這但是一個糟糕的事情。
// 在循環中使用StringBuilder拼接字符串 public class StringBuilderDemo2 { public static void main(String[] args) { String[] arr = {"Hello", "World", "!!!"}; String s1 = arrayToString(arr); System.out.println(s1); } public static String arrayToString(String[] arr) { StringBuilder s = new StringBuilder(); s.append("["); for (int x = 0; x < arr.length; x++) { if (x == arr.length - 1) { s.append(arr[x]); } else { s.append(arr[x]); s.append(", "); } } s.append("]"); return s.toString(); } } //運行結果 [Hello, World, !!!]
使用StringBuilder方式進行拼接,自行去看一下彙編代碼中,不只循環部分的代碼更爲簡潔,並且它只生成了一個StringBuilder對象。顯式的建立StringBuilder對象還容許你預先爲其指定大小。能夠避免屢次從新分配緩衝。
總結:
若是字符串操做比較簡單,就可使用 「+」 運算符操做,編譯器會爲你合理的構造出最終的字符串結果
若是使用循環語句 最好本身手動建立一個StringBuilder對象,用它來構最終結果