1、String的split(String regex)方法參數注意點
使用這個方法時,當我們直接以“.”爲參數時,是會出錯的,如:
-
String str = "12.03";
-
String[] res = str.spilt(".");
此時,我們得到的res是爲空的(不是null),即str = [];
因爲String的split(String regex)根據給定的正則表達式的匹配來拆分此字符串,而"."是正則表達式中的關鍵字,沒有經過轉義split會把它當作一個正則表達式來處理的,需要寫成str.split("\\.")進行轉義處理。
2、關於hashCode方法
【參考文章:
】
我們可以先通過HashMap中hashCode的作用來體驗一下。
我們知道HashMap中是不允許插入重複元素的,如果是插入的同一個元素,會將前面的元素給覆蓋掉,那勢必在HashMap的put方法裏對key值進行了判斷,檢測其是否是同一個對象。其put源碼如下:
-
public V put(K key, V value) {
-
if (table == EMPTY_TABLE) {
-
inflateTable(threshold);
-
}
-
if (key == null)
-
return putForNullKey(value);
-
int hash = hash(key);
-
int i = indexFor(hash, table.length);
-
for (Entry<K,V> e = table[i]; e != null; e = e.next) {
-
Object k;
-
if (e.hash == hash && ((k = e.key) == key || key.equals(k))) {
-
V oldValue = e.value;
-
e.value = value;
-
e.recordAccess(this);
-
return oldValue;
-
}
-
}
-
modCount++;
-
addEntry(hash, key, value, i);
-
return null;
-
}
可以看到這裏的判斷語句 if (e.hash == hash && ((k = e.key) == key || key.equals(k))),裏面通過&&邏輯運算符相連,先判斷e.hash == hash,即判斷傳進來的key的hashCode值與table中的已有的hashCode值比較,如果不存在該key值,也就不會再去執行&&後面的equals判斷;當已經存在該key值時,再調用equals方法再次確定兩個key值對象是否相同。從這裏可以看出,hashCode方法的存在是爲了減少equals方法的調用次數,從而提高程序效率。
可以看到,判斷兩個對象是否相同,還是要取決於equals方法,而兩個對象的hashCode值是否相等是兩個對象是否相同的必要條件。所以有以下結論:
(1)如果兩個對象的hashCode值不等,根據必要條件理論,那麼這兩個對象一定不是同一個對象,即他們的equals方法一定要返回false;
(2)如果兩個對象的hashCode值相等,這兩個對象也不一定是同一個對象,即他們的equals方法返回值不確定;
反過來,
(1)如果equals方法返回true,即是同一個對象,它們的hashCode值一定相等;
(2)如果equals方法返回false,hashCode值也不一定不相等,即是不確定的;
(hashCode返回的值一般是對象的存儲地址或者與對象存儲地址相關聯的hash散列值)
然而,很多時候我們可能會重寫equals方法,來判斷這兩個對象是否相等,此時,爲了保證滿足上面的結論,即滿足hashCode值相等是equals返回true的必要條件,我們也需要重寫hashCode方法,以保證判斷兩個對象的邏輯一致(所謂的邏輯一致,是指equals和hashCode方法都是用來判斷對象是否相等)。如下例子:
-
public class Person {
-
private String name;
-
private int age;
-
public Person(String name,int age){
-
this.name = name;
-
this.age = age;
-
}
-
public String getName() {
-
return name;
-
}
-
public void setName(String name) {
-
this.name = name;
-
}
-
public int getAge() {
-
return age;
-
}
-
public void setAge(int age) {
-
this.age = age;
-
}
-
-
@Override
-
public boolean equals(Object obj) {
-
return this.name.equals(((Person)obj).name) && this.age== ((Person)obj).age;
-
}
-
}
在Person裏面重寫了equals方法,但是沒有重寫hashCode方法,如果就我們平時正常來使用的話也不會出什麼問題,如:
-
Person p1 = new Person("lly",18);
-
Person p2 = new Person("lly",18);
-
System.out.println(p1.equals(p2));
上面是按照了我們重寫的equals方法,返回了我們想要的值。但是當我們使用HashMap來保存Person對象的時候就會出問題了,如下:
-
<span style="white-space:pre"> </span>Person p1 = new Person("lly", 18);
-
System.out.println(p1.hashCode());
-
HashMap<Person, Integer> hashMap = new HashMap<Person, Integer>();
-
hashMap.put(p1, 1);
-
System.out.println(hashMap.get(new Person("lly", 18)));
這是因爲,我們沒有重寫Person的hashCode方法,使hashCode方法與我們equals方法的邏輯功能一致,此時的Person對象調用的hashCode方法還是父類的默認實現,即返回的是和對象內存地址相關的int值,這個時候,p1對象和new Person("lly",18);對象因爲內存地址不一致,所以其hashCode返回值也是不同的。故HashMap會認爲這是兩個不同的key,故返回null。
所以,我們想要正確的結果,只需要重寫hashCode方法,讓equals方法和hashCode方法始終在邏輯上保持一致性。
在《Java編程思想》一書中的P495頁有如下的一段話:
“設計hashCode()時最重要的因素就是:無論何時,對同一個對象調用hashCode()都應該產生同樣的值。如果在將一個對象用put()添加進HashMap時產生一個hashCdoe值,而用get()取出時卻產生了另一個hashCode值,那麼就無法獲取該對象了。所以如果你的hashCode方法依賴於對象中易變的數據,用戶就要當心了,因爲此數據發生變化時,hashCode()方法就會生成一個不同的散列碼”。
如下一個例子:
-
public class Person {
-
private String name;
-
private int age;
-
public Person(String name,int age){
-
this.name = name;
-
this.age = age;
-
}
-
public String getName() {
-
return name;
-
}
-
public void setName(String name) {
-
this.name = name;
-
}
-
public int getAge() {
-
return age;
-
}
-
public void setAge(int age) {
-
this.age = age;
-
}
-
@Override
-
public int hashCode() {
-
return name.hashCode()*37+age;
-
}
-
@Override
-
public boolean equals(Object obj) {
-
return this.name.equals(((Person)obj).name) && this.age== ((Person)obj).age;
-
}
-
}
此時我們繼續測試:
-
<span style="white-space:pre"> </span>Person p1 = new Person("lly", 18);
-
System.out.println(p1.hashCode());
-
HashMap<Person, Integer> hashMap = new HashMap<Person, Integer>();
-
hashMap.put(p1, 1);
-
p1.setAge(13);
-
System.out.println(hashMap.get(p1));
所以,在設計hashCode方法和equals方法的時候,如果對象中的數據易變,則最好在hashCode方法中不要依賴於該字段。
3、Override和Overload的區別
Override(重寫):
在子類中定義與父類具有完全相同的名稱和參數的方法,通過子類創建的實例對象調用這個方法時,將調用子類中的定義方法,這相當於把父類中定義的那個完全相同的方法給覆蓋了,是子類與父類之間多態性的一種體現。特點如下:
(1)子類方法的訪問權限只能比父類的更大,不能更小(可以相同);
(2)如果父類的方法是private類型,那麼,子類則不存在覆蓋的限制,相當於子類中增加了一個全新的方法;
(3)子類覆蓋的方法所拋出的異常必須和父類被覆蓋方法的所拋出的異常一致,或者是其子類;即子類的異常要少於父類被覆蓋方法的異常;
Overload(重載):
同一個類中可以有多個名稱相同的方法,但方法的參數個數和參數類型或者參數順序不同;
關於重載函數返回類型能否不一樣,需分情況:
(1)如果幾個Overloaded的方法的參數列表不一樣(個數或類型),它們的返回者類型當然也可以不一樣;
(2)兩個方法的參數列表完全一樣,則不能通過讓其返回類型的不同來實現重載。
(3)不同的參數順序也是可以實現重載的;如下:
-
public String getName(String str,int i){
-
return null;
-
}
-
public String getName(int i,String str){
-
return null;
-
}
我們可以用反證法來說明這個問題,因爲我們有時候調用一個方法時也可以不定義返回結果變量,即不要關心其返回結果,例如,我們調用map.remove(key)方法時,雖然remove方法有返回值,但是我們通常都不會定義接收返回結果的變量,這時候假設該類中有兩個名稱和參數列表完全相同的方法,僅僅是返回類型不同,java就無法確定編程者倒底是想調用哪個方法了,因爲它無法通過返回結果類型來判斷。
所以,Overloaded重載的方法是可以改變返回值的類型;只能通過不同的參數個數、不同的參數類型、不同的參數順序來實現重載。
4、ArrayList、Vector、LinkedList區別
ArrayList、Vector、LinkedList都實現了List接口,其關係圖如下:
三者都可以添加null元素對象,如下示例:
-
ArrayList<String> arrayList = new ArrayList<String>();
-
arrayList.add(null);
-
arrayList.add(null);
-
System.out.println(arrayList.size());
-
-
LinkedList<String> linkedList = new LinkedList<String>();
-
linkedList.add(null);
-
-
Vector<String> vectorList = new Vector<String>();
-
vectorList.add(null);
ArrayList和Vector相同點:
ArrayList和Vector兩者在功能上基本完全相同,其底層都是通過new出的Object[]數組實現。所以當我們能夠預估到數組大小的時候,我們可以指定數組初始化的大小,這樣可以減少後期動態擴充數組大小帶來的消耗。如下:
ArrayList<String> list= new ArrayList<String>(20);
Vector<String> list2 = new Vector<String>(15);
由於這兩者的數據結構爲數組,所以在獲取數據方面即get()的時候比較高效,而在add()插入或者remove()的時候,由於需要移動元素,效率相對不高。(其實對於我們平常使用來說,由於一般使用add(String
element)都是讓其加在數組末尾,所以並不需要移動元素,效率還是很好的,如果使用add(int index, String element)指定了插入位置,此時就需要移動元素了。)
ArrayList和Vector區別:
ArrayList的所有方法都不是同步的,而Vector的大部分方法都加了synchronized同步,所以,就線程安全來說,ArrayList不是線程安全的,而Vector是線程安全的,也因此Vector效率方面相較ArrayList就會更低,所以如果我們本身程序就是安全的,ArrayList是更好的選擇。
大多數的Java程序員使用ArrayList而不是Vector,因爲同步完全可以由程序員自己來控制。
LinkedList:
LinkedList其底層是通過雙向循環鏈表實現的,所以在大量增加或刪除元素時(即add和remove操作),由於不需要移動元素有更好的性能,但是在獲取數據(get操作)方面要差。
所以,在三者的使用選擇上,LinkedList適合於有大量的增加/刪除操作和較少隨機讀取操作,ArrayList適合於大規模隨機讀取數據,而較少插入和刪除元素情景下使用,Vector在要求線程安全的情況下使用。
5、String、StringBuffer、StringBuilder區別
String(since JDK1.0):
字符串常量,不可更改,因爲其內部定義的是一個final類型的數組來保存值的,如下:
private final char value[];
所以,當我們每次去“更改”String變量的值的時候(包括重新賦值或者使用String內部的一些方法),其實是重新新建了一個String對象(new String)來保存新的值,然後讓我們的變量指向新的對象。因此,當我們需要頻繁改變字符串的時候,使用String會帶來較大的開銷。
定義String的方法有兩種:
(1)String str = "abc";
(2)String str2 = new String("def");
第一種方式創建的String對象“abc”是存放在字符串常量池中,創建過程是,首先在字符串常量池中查找有沒有"abc"對象,如果有則將str直接指向它,如果沒有就在字符串常量池中創建出來“abc”,然後在將str指向它。當有另一個String變量被賦值爲abc時,直接將字符串常量池中的地址給它。如下:
-
<span style="white-space:pre"> </span>String a = "abc";
-
String b = "abc";
-
System.out.println(a == b);
也就是說通過第一種方式創建的字符串在字符串常量池中,是可共享的。同時,也是不可更改的,體現在:
-
<span style="white-space:pre"> </span>String a = "abc";
-
String b = "abc";
-
b = b + "def";
此時,字符串常量池中存在了兩個對象“abc”和“abcdef”。
第二種創建方式其實分爲兩步:
-
String s = "def";
-
String str2 = new String(s);
第一步就是上面的第一種情況;第二步在堆內存中new出一個String對象,將str2指向該堆內存地址,新new出的String對象內容,是在字符串常量池中找到的或創建出“def”對象,相當於此時存在兩份“def”對象拷貝,一份存在字符串常量池中,一份被堆內存的String對象私有化管理着。所以使用String str2 = new String("def");這種方式創建對象,實際上創建了兩個對象。
StringBuffer(since JDK1.0)和StringBuilder(since JDK1.5):
StringBuffer和StringBuilder在功能上基本完全相同,它們都繼承自AbstractStringBuilder,而AbstractStringBuilder是使用非final修飾的字符數組實現的,如:char[] value;
,所以,可以對StringBuffer和StringBuilder對象進行改變,每次改變還是再原來的對象上發生的,不會重新new出新的StringBuffer或StringBuilder對象來。所以,當我們需要頻繁修改字符串內容的時候,使用StringBuffer和StringBuilder是很好地選擇。
兩者的核心操作都是append和insert,append是直接在字符串的末尾追加,而insert(int index,String str)是在指定位置出插入字符串。 StringBuffer和StringBuilder的最主要區別就是線程安全方面,由於在StringBuffer內大部分方法都添加了synchronized同步,所以StringBuffer是線程安全的,而StringBuilder不是線程安全的。因此,當我們處於多線程的環境下時,我們需要使用StringBuffer,如果我們的程序是線程安全的使用StringBuilder在性能上就會更優一點。
三者的效率比較:
如上所述,
(1)當我們需要頻繁的對字符串進行更改的時候,使用 StringBuffer或StringBuilder是優先選擇,對於 StringBuffer和StringBuilder來說,只要程序是線程安全的,我們儘量使用StringBuilder來處理,要求線程安全的話只能使用StringBuffer。平常情況下使用字符串(不常更改字符串內容),String可以滿足需求。
(2)有一種情況下使用String和 StringBuffer或StringBuilder的效率是差不多的,如下:
-
<span style="white-space:pre"> </span>String a = "abc" + "def";
-
StringBuilder sb = new StringBuilder();
-
sb.append("abc").append("def");
對於第一條語句,Java在編譯的時候直接把a編譯成 a = "abcdef",但是當我們拼接的字符串是其他已定義的字符串對象時,就不會自動編譯了,如下:
-
<span style="white-space:pre"> </span>String a = "abc";
-
String b = "def";
-
String c = a + b;
根據String源碼中的解釋,這種情況下是使用concatenation操作符(+),內部是新創建StringBuffer或StringBuilder對象,利用其append方法進行字符串追加,然後利用toString方法返回String串。所以此時的效率也是不高的。
6、Map、Set、List、Queue、Stack的特點與用法
Java集合類用來保存對象集合,對於基本類型,必須要使用其包裝類型。Java集合框架分爲兩種:
(1)Collection
以單個對象的形式保存,其關係圖如下:
其中,Statck類爲Vector的子類。由於Collection類繼承Iterable類,所以,所有Collection的實現類都可以通過foreach的方式進行遍歷。
(2)Map
以<key,value>鍵值對的形式保存數據。Map常用類的結構關係如下:
List:
List集合裏面存放的元素有序、可重複。List集合的有序體現在它默認是按照我們的添加順序設置索引值(即我們可以通過get(索引值index)的方式獲取對象);可重複,是由於我們給每個元素設置了索引值,可以通過索引值找到相應的對象。
關於List集合下的具體實現類ArrayList、Vector、LinkedList可以參考上面的第四點總結。對於Statck,它是Vector的子類,模擬了棧後進先出的數據結構。
Queue:
接口,模擬了隊列先進先出的數據結構。
Set:
Set裏面的元素無序、不可重複。由於無序性,我們不能通過get方式獲取對象(因爲set沒有索引值)。如下:
-
<span style="white-space:pre"> </span>Set<String> set = new HashSet<String>();
-
set.add("ddd");
-
set.add("4444");
-
set.add("555");
-
set.add("777");
-
for(String s : set){
-
System.out.println(s);
-
}
打印結果如下:
ddd
777
4444
555
而對於不可重複性,Set的所有具體實現類其內部都是通過Map的實現類來保存對象的。如HashSet內部就是通過HashMap來保存數據的,如下源碼:
-
-
private static final Object PRESENT = new Object();
-
public HashSet() {
-
map = new HashMap<>();
-
}
-
public boolean add(E e) {
-
return map.put(e, PRESENT)==null;
-
}
可以看到,new出一個HashSet的時候,裏面new出了一個HashMap對象,在使用HashSet進行add添加數據的時候,HashSet將我們需要保存的數據作爲HashMap
的key值保存了起來,而key值是不允許重複的,相當於HashSet的元素也是不可重複的。
Map:
Map裏面的元素通過<key,value>鍵值對的形式保存,不允許重複(具體分析可參考第二點總結內容)。其實Map<key,value>有點像Java中的Model對象類,如下使用:
-
class Person{
-
private String name;
-
private int age;
-
-
}
-
List<Person> persons ;
-
-
List<Map<String,String>> persons ;
-
Map<String,String> map = new HashMap<String,String>();
-
map.put("name","lly");
-
map.put("age","18");
-
persons .add(map);