【腾讯面试题】Java集合:List、Set以及Map
Java集合:List、Set以及Map
- 概述
- Collection接口
- List:有序,可重復
- ArraysList
- Vector
- LinkedList
- Set:無序,唯一
- HashSet
- LinkedHashSet
- Tree Set
- Map接口
- TreeSet,HashSet,LinkedHashSet的區別
- 1.介紹
- 2.相同點
- 3.不同點
- 4.代碼比較
- TreeSet的兩種排序方式比較
- 1.排序的引入(以基本數據類型的排序為例)
- 2.如果是引用數據類型呢,比如自定義對象,又該如何排序呢?
- (1).自然排序
- (2).比較器排序
- (三) 性能測試
- Java常見集合的默認大小及擴容機制
- List:元素有序的、可重復的
- ArrayList、Vector默認初始容量為10
- Vector:線程安全,速度慢,擴容10-->20
- ArrayList:線程不安全,速度快,擴容10-->16
- Set:元素無序、不可重復
- HashSet:線程不安全,存取速度快,擴容16-->32
- Map:雙列集合
- HashMap:默認初始容量為16,一次擴容為32
- HashMap數組長度為什么是2的次冪?
- 為什么HashMap的數組初始化是2的次方時效率最高?
- 參考資料
概述
List , Set, Map都是接口,前兩個繼承至Collection接口,Map為獨立接口
List下有ArrayList,Vector,LinkedList
Set下有HashSet,LinkedHashSet,TreeSet
Map下有Hashtable,LinkedHashMap,HashMap,TreeMap
Collection接口下還有個Queue接口,有PriorityQueue類
Queue接口與List、Set同一級別,都是繼承了Collection接口。看圖你會發現,LinkedList既可以實現Queue接口,也可以實現List接口。只不過呢,LinkedList實現了Queue接口。Queue接口窄化了對LinkedList的方法的訪問權限(即在方法中的參數類型如果是Queue時,就完全只能訪問Queue接口所定義的方法了,而不能直接訪問 LinkedList的非Queue的方法),以使得只有恰當的方法才可以使用。
SortedSet是個接口,它里面的(只有TreeSet這一個實現可用)中的元素一定是有序的。
Collection接口
List:有序,可重復
ArraysList
優點: 底層數據結構是數組,查詢快,增刪慢。
缺點: 線程不安全,效率高
Vector
優點: 底層數據結構是數組,查詢快,增刪慢。
缺點: 線程安全,效率低
LinkedList
優點: 底層數據結構是鏈表,查詢慢,增刪快。
缺點: 線程不安全,效率高
Set:無序,唯一
HashSet
底層數據結構是哈希表。(無序,唯一)
如何來保證元素唯一性?
依賴兩個方法:hashCode()和equals()
LinkedHashSet
底層數據結構是鏈表和哈希表。(FIFO插入有序,唯一)
1.由鏈表保證元素有序
2.由哈希表保證元素唯一
Tree Set
底層數據結構是紅黑樹。(有序,唯一)
1.如何保證元素排序的呢? 自然排序和比較器排序
2.如何保證元素唯一性的呢? 根據比較的返回值是否是0來決定
Map接口
Map接口有三個比較重要的實現類,分別是HashMap、TreeMap和HashTable。
- HashMap和HashTable是無序的,TreeMap是有序的
- Hashtable的方法是同步的,HashMap的方法不是同步的。這是兩者最主要的區別。
這就意味著:
- Hashtable是線程安全的,HashMap不是線程安全的。
- Hashtable效率較低,HashMap效率較高。
- 如果對同步性或與遺留代碼的兼容性沒有任何要求,建議使用HashMap。
- 查看Hashtable的源代碼就可以發現,除構造函數外,Hashtable的所有 public 方法聲明中都有 synchronized關鍵字,而HashMap的源碼中則沒有。
- Hashtable不允許null值,HashMap允許null值(key和value都允許)
- 父類不同:Hashtable的父類是Dictionary,HashMap的父類是AbstractMap
TreeSet,HashSet,LinkedHashSet的區別
1.介紹
TreeSet, LinkedHashSet and HashSet 在java中都是實現Set的數據結構
- TreeSet的主要功能用于排序
- LinkedHashSet的主要功能用于保證FIFO即有序的集合(先進先出)
- HashSet只是通用的存儲數據的集合
2.相同點
- Duplicates elements: 因為三者都實現Set interface,所以三者都不包含相同元素
- Thread safety: 三者都不是線程安全的,如果要使用線程安全可以Collections.synchronizedSet()
3.不同點
- Performance and Speed: HashSet插入數據最快,其次LinkHashSet,最慢的是TreeSet,因為內部實現排序
- Ordering: HashSet不保證有序,LinkHashSet保證FIFO即按插入順序排序,TreeSet安裝內部實現排序,也可以自定義排序規則
- null:HashSet和LinkHashSet允許存在null數據,但是TreeSet中插入null數據時會報NullPointerException
4.代碼比較
public static void main(String args[]) {HashSet<String> hashSet = new HashSet<>();LinkedHashSet<String> linkedHashSet = new LinkedHashSet<>();TreeSet<String> treeSet = new TreeSet<>();for (String data : Arrays.asList("B", "E", "D", "C", "A")) {hashSet.add(data);linkedHashSet.add(data);treeSet.add(data);}//不保證有序System.out.println("Ordering in HashSet :" + hashSet);//FIFO保證安裝插入順序排序System.out.println("Order of element in LinkedHashSet :" + linkedHashSet);//內部實現排序System.out.println("Order of objects in TreeSet :" + treeSet);} 運行結果: Ordering in HashSet :[A, B, C, D, E] (無順序) Order of element in LinkedHashSet :[B, E, D, C, A] (FIFO插入有序) Order of objects in TreeSet :[A, B, C, D, E] (排序)TreeSet的兩種排序方式比較
1.排序的引入(以基本數據類型的排序為例)
由于TreeSet可以實現對元素按照某種規則進行排序,例如下面的例子
public class MyClass {public static void main(String[] args) {// 創建集合對象// 自然順序進行排序TreeSet<Integer> ts = new TreeSet<Integer>();// 創建元素并添加// 20,18,23,22,17,24,19,18,24ts.add(20);ts.add(18);ts.add(23);ts.add(22);ts.add(17);ts.add(24);ts.add(19);ts.add(18);ts.add(24);// 遍歷for (Integer i : ts) {System.out.println(i);}} } 運行結果: 17 18 19 20 22 23 242.如果是引用數據類型呢,比如自定義對象,又該如何排序呢?
public class MyClass {public static void main(String[] args) {TreeSet<Student> ts=new TreeSet<Student>();//創建元素對象Student s1=new Student("zhangsan",20);Student s2=new Student("lis",22);Student s3=new Student("wangwu",24);Student s4=new Student("chenliu",26);Student s5=new Student("zhangsan",22);Student s6=new Student("qianqi",24);//將元素對象添加到集合對象中ts.add(s1);ts.add(s2);ts.add(s3);ts.add(s4);ts.add(s5);ts.add(s6);//遍歷for(Student s:ts){System.out.println(s.getName()+"-----------"+s.getAge());}} } public class Student {private String name;private int age;public Student() {super();// TODO Auto-generated constructor stub}public Student(String name, int age) {super();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;} }原因分析:
由于不知道該安照那一中排序方式排序,所以會報錯。
解決方法:
1.自然排序
2.比較器排序
(1).自然排序
自然排序要進行一下操作:
1.Student類中實現 Comparable接口
2.重寫Comparable接口中的Compareto方法
運行結果:
lis-----------22 qianqi-----------24 wangwu-----------24 chenliu-----------26 zhangsan-----------20 zhangsan-----------22(2).比較器排序
比較器排序步驟:
1.單獨創建一個比較類,這里以MyComparator為例,并且要讓其繼承Comparator接口
2.重寫Comparator接口中的Compare方法
3.在主類中使用下面的 構造方法
TreeSet(Comparator<? superE> comparator) //構造一個新的空 TreeSet,它根據指定比較器進行排序。 public class MyClass {public static void main(String[] args) {//創建集合對象//TreeSet(Comparator<? super E> comparator) 構造一個新的空 TreeSet,它根據指定比較器進行排序。TreeSet<Student> ts=new TreeSet<Student>(new MyComparator());//創建元素對象Student s1=new Student("zhangsan",20);Student s2=new Student("lis",22);Student s3=new Student("wangwu",24);Student s4=new Student("chenliu",26);Student s5=new Student("zhangsan",22);Student s6=new Student("qianqi",24);//將元素對象添加到集合對象中ts.add(s1);ts.add(s2);ts.add(s3);ts.add(s4);ts.add(s5);ts.add(s6);//遍歷for(Student s:ts){System.out.println(s.getName()+"-----------"+s.getAge());}} } public class Student {private String name;private int age;public Student() {super();// TODO Auto-generated constructor stub}public Student(String name, int age) {super();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;}} public class MyComparator implements Comparator<Student> {@Overridepublic int compare(Student s1,Student s2) {// 姓名長度int num = s1.getName().length() - s2.getName().length();// 姓名內容int num2 = num == 0 ? s1.getName().compareTo(s2.getName()) : num;// 年齡int num3 = num2 == 0 ? s1.getAge() - s2.getAge() : num2;return num3;}}運行結果:
lis-----------22 qianqi-----------24 wangwu-----------24 chenliu-----------26 zhangsan-----------20 zhangsan-----------22(三) 性能測試
class Dog implements Comparable<Dog> {int size;public Dog(int s) {size = s;}public String toString() {return size + "";}@Overridepublic int compareTo(Dog o) {//數值大小比較return size - o.size;} } public class MyClass {public static void main(String[] args) {Random r = new Random();HashSet<Dog> hashSet = new HashSet<Dog>();TreeSet<Dog> treeSet = new TreeSet<Dog>();LinkedHashSet<Dog> linkedSet = new LinkedHashSet<Dog>();// start timelong startTime = System.nanoTime();for (int i = 0; i < 1000; i++) {int x = r.nextInt(1000 - 10) + 10;hashSet.add(new Dog(x));}// end timelong endTime = System.nanoTime();long duration = endTime - startTime;System.out.println("HashSet: " + duration);// start timestartTime = System.nanoTime();for (int i = 0; i < 1000; i++) {int x = r.nextInt(1000 - 10) + 10;treeSet.add(new Dog(x));}// end timeendTime = System.nanoTime();duration = endTime - startTime;System.out.println("TreeSet: " + duration);// start timestartTime = System.nanoTime();for (int i = 0; i < 1000; i++) {int x = r.nextInt(1000 - 10) + 10;linkedSet.add(new Dog(x));}// end timeendTime = System.nanoTime();duration = endTime - startTime;System.out.println("LinkedHashSet: " + duration);}}Java常見集合的默認大小及擴容機制
在面試后臺開發的過程中,集合是面試的熱話題,不僅要知道各集合的區別用法,還要知道集合的擴容機制,今天我們就來談下ArrayList 和 HashMap的默認大小以及擴容機制。
在 Java 8 中,查看源碼可以知道:ArrayList 的默認大小是 10 個元素,HashMap 的默認大小是16個元素(必須是2的冪,為什么呢???下文有解釋)。這就是 Java 8 中 ArrayList 和 HashMap 類 的代碼片段:
// from ArrayList.java JDK 1.7 private static final int DEFAULT_CAPACITY = 10;//from HashMap.java JDK 7 static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16這里要討論這些常用的默認初始容量和擴容的原因是:
當底層實現涉及到擴容時,容器需要重新分配一段更大的連續內存(如果是離散分配,則不需要重新分配,離散分配都是插入新元素時,動態分配內存),要將容器原來的數據全部復制到新的內存上,這無疑使效率大大降低。加載因子的系數小于等于1,意指即當元素個數超過容量長度*加載因子的系數時,進行擴容。另外,擴容也是有默認的倍數的,不同的容器擴容情況不同。
List:元素有序的、可重復的
ArrayList、Vector默認初始容量為10
Vector:線程安全,速度慢,擴容10–>20
底層數據結構是數組結構
加載因子為1:即當 元素個數 超過 容量長度 時,進行擴容
擴容增量:原容量的 1倍
如 Vector的容量為10,一次擴容后是容量為20
ArrayList:線程不安全,速度快,擴容10–>16
底層數據結構是數組結構
擴容增量:原容量的 0.5倍+1
如 ArrayList的容量為10,一次擴容后是容量為16
Set:元素無序、不可重復
HashSet:線程不安全,存取速度快,擴容16–>32
底層實現是一個HashMap(保存數據),實現Set接口
默認初始容量為16(為何是16,見下方對HashMap的描述)
加載因子為0.75:即當 元素個數 超過 容量長度的0.75倍 時,進行擴容
擴容增量:原容量的 1 倍
如 HashSet的容量為16,一次擴容后是容量為32
Map:雙列集合
HashMap:默認初始容量為16,一次擴容為32
(為何是16:16是2^4,可以提高查詢效率,另外,32=16<<1)
加載因子為0.75:即當 元素個數 超過 容量長度的0.75倍 時,進行擴容
擴容增量:原容量的 1 倍
如 HashSet的容量為16,一次擴容后是容量為32
HashMap數組長度為什么是2的次冪?
hashMap的數組長度一定保持2的次冪,比如16的二進制表示為 10000,那么length-1就是15,二進制為01111,同理擴容后的數組長度為32,二進制表示為100000,length-1為31,二進制表示為011111。
這樣會保證低位全為1,而擴容后只有一位差異,也就是多出了最左位的1,這樣在通過 h&(length-1)的時候,只要h對應的最左邊的那一個差異位為0,就能保證得到的新的數組索引和老數組索引一致(大大減少了之前已經散列良好的老數組的數據位置重新調換),還有,數組長度保持2的次冪,length-1的低位都為1,會使得獲得的數組索引index更加均勻。
static int indexFor(int h, int length) { return h & (length-1); }首先計算得到key的hashcode值,然后跟數組的長度-1做一次“與”運算(&)??瓷先ズ芎唵?#xff0c;其實比較有玄機。比如數組的長度是2的4次方,那么hashcode就會和2的4次方-1做“與”運算。
為什么HashMap的數組初始化是2的次方時效率最高?
很多人都有這個疑問,為什么hashmap的數組初始化大小都是2的次方大小時,hashmap的效率最高,我以2的4次方舉例,來解釋一下為什么數組大小為2的冪時hashmap訪問的性能最高。
看下圖,左邊兩組是數組長度為16(2的4次方),右邊兩組是數組長度為15。兩組的hashcode均為8和9,但是很明顯,當它們和1110“與”的時候,產生了相同的結果,也就是說它們會定位到數組中的同一個位置上去,這就產生了碰撞,8和9會被放到同一個鏈表上,那么查詢的時候就需要遍歷這個鏈表,得到8或者9,這樣就降低了查詢的效率。
同時,我們也可以發現,當數組長度為15的時候,hashcode的值會與14(1110)進行“與”,那么最后一位永遠是0,而0001,0011,0101,1001,1011,0111,1101這幾個位置永遠都不能存放元素了,空間浪費相當大,更糟的是這種情況中,數組可以使用的位置比數組長度小了很多,這意味著進一步增加了碰撞的幾率,減慢了查詢的效率!
所以說,當數組長度為2的n次冪的時候,不同的key計算得到的index相同的幾率較小,那么數據在數組上分布就比較均勻,也就是說碰撞的幾率小,相對的,查詢的時候就不用遍歷某個位置上的鏈表,這樣查詢效率也就較高了。
說到這里,我們再回頭看一下hashmap中默認的數組大小是多少,查看源代碼可以得知是16,為什么是16,而不是15,也不是20呢,看到上面的解釋之后我們就清楚了吧,顯然是因為16是2的整數次冪的原因,
在小數據量的情況下16比15和20更能減少key之間的碰撞,而加快查詢的效率。
參考資料
總結
以上是生活随笔為你收集整理的【腾讯面试题】Java集合:List、Set以及Map的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【深度学习】通过python画出loss
- 下一篇: Python数据分析·读取CSV文件转为