今日內容
- Collection集合
- 迭代器
- 泛型
- 數據結構
教學目標
- 能夠說出集合與數組的區別
- 能夠使用Collection集合的常用功能
- 能夠使用迭代器對集合進行取元素
- 能夠使用增強for循環遍歷集合和數組
- 能夠理解泛型上下限
- 能夠闡述泛型通配符的作用
- 能夠說出常見的數據結構
- 能夠說出數組結構特點
- 能夠說出棧結構特點
- 能夠說出隊列結構特點
- 能夠說出單向鏈表結構特點
第一章 Collection集合
1.1 集合概述
在前面我們已經學習過並使用過集合ArrayList ,那麼集合到底是什麼呢?
- 集合:集合是java中提供的一種容器,可以用來存儲多個數據。
集合和數組既然都是容器,它們有什麼區別呢?
- 數組的長度是固定的。集合的長度是可變的。
- 數組中存儲的是同一類型的元素,可以存儲任意類型數據。集合存儲的都是引用數據類型。如果想存儲基本類型數據需要存儲對應的包裝類型。
存儲整數:int[] 存儲字符串:String[]
存儲整數:ArrayList<Integer> 存儲字符串:ArrayList<String>
1.2 集合常用類的繼承體系
Collection:單列集合類的根接口,用於存儲一系列符合某種規則的元素,它有兩個重要的子接口,分別是java.util.List
和java.util.Set
。其中,List
的特點是元素存取有序、元素可重複 ; Set
的特點是元素存取無序,元素不可重複。List
接口的主要實現類有java.util.ArrayList
和java.util.LinkedList
,Set
接口的主要實現類有java.util.HashSet
和java.util.LinkedHashSet
。
從上面的描述可以看出JDK中提供了豐富的集合類庫,爲了便於初學者進行系統地學習,接下來通過一張圖來描述集合常用類的繼承體系
注意:這張圖只是我們常用的集合有這些,不是說就只有這些集合。
集合本身是一個工具,它存放在java.util包中。在Collection
接口定義着單列集合框架中最最共性的內容。
1.3 Collection 常用功能
Collection是所有單列集合的父接口,因此在Collection中定義了單列集合(List和Set)通用的一些方法,這些方法可用於操作所有的單列集合。方法如下:
public boolean add(E e)
: 把給定的對象添加到當前集合中 。public void clear()
:清空集合中所有的元素。public boolean remove(E e)
: 把給定的對象在當前集合中刪除。public boolean contains(Object obj)
: 判斷當前集合中是否包含給定的對象。public boolean isEmpty()
: 判斷當前集合是否爲空。public int size()
: 返回集合中元素的個數。public Object[] toArray()
: 把集合中的元素,存儲到數組中
tips: 有關Collection中的方法可不止上面這些,其他方法可以自行查看API學習。
public class Demo02ListMethod {
public static void main(String[] args) {
//多態寫法
Collection<String> c = new ArrayList<>();
//boolean add(E e)
//添加方法(添加成功會返回true,添加失敗會返回false.但是ArrayList只會成功不會失敗所以返回永遠是true,不接受這個返回值)
c.add("石原里美");
c.add("新垣結衣");
c.add("石原里美");
//void clear()
//清空集合中的元素
//c.clear(); //調用後集合被清空
//boolean remove(Object e)
//刪除集合中的某個元素(返回的是true或false代表刪除成功或失敗,只會刪除第一個匹配的元素)
c.remove("柳巖"); //沒有柳巖,刪除沒作用
c.remove("石原里美"); //集合中有兩個“石原里美”,會刪除第一個
//boolean contains(Object obj)
//判斷集合是否包含某個元素
boolean b = c.contains("石原里美");
System.out.println(b); //集合中包含有“石原里美”這個元素所以結果是true
//boolean isEmpty()
//判斷集合是否爲空
boolean b2 = c.isEmpty();
System.out.println(b2); //集合裏面有元素,不爲空 結果是false
//int size()
//獲取集合的長度
int size = c.size();
System.out.println(size); //2
//Object[] toArray()
//把集合轉成Object[]類型
Object[] arr = c.toArray();
//打印數組
System.out.println(Arrays.toString(arr));
//如果想要轉成具體的類型,用到了一個高級寫法,我現在可以給演示一下,不講原理
String[] strs = c.toArray(new String[c.size()]);
System.out.println(Arrays.toString(strs));
}
}
第二章 Iterator迭代器
2.1 Iterator接口
在程序開發中,經常需要遍歷集合中的所有元素。針對這種需求,JDK專門提供了一個接口java.util.Iterator
。
想要遍歷Collection集合,那麼就要獲取該集合迭代器完成迭代操作,下面介紹一下獲取迭代器的方法:
方法 | 說明 |
---|---|
public Iterator iterator() | 獲取集合對應的迭代器,用來遍歷集合中的元素的。 |
下面介紹一下迭代的概念:
-
迭代:即Collection集合元素的通用獲取方式。在取元素之前先要判斷集合中有沒有元素,如果有,就把這個元素取出來,繼續再判斷,如果還有就再取出來。一直把集合中的所有元素全部取出。這種取出方式專業術語稱爲迭代。
注意:調用next()方法獲取數據之前一定先調用hasNext()方法判斷有沒有有數據,否則會報異常。
Iterator接口的常用方法如下:
方法 | 說明 |
---|---|
E next() | 獲取集合中的元素 |
boolean hasNext() | 判斷集合中有沒有下一個元素,如果仍有元素可以迭代,則返回 true。 |
void remove() | 刪除當前元素 |
接下來我們通過案例學習如何使用Iterator迭代集合中元素:
代碼如下:
public class Demo02 {
public static void main(String[] args) {
// 使用多態方式 創建對象
Collection<String> coll = new ArrayList<String>();
// 添加元素到集合
coll.add("aaaa");
coll.add("bbbb");
coll.add("cccc");
//根據當前集合對象生成迭代器對象
Iterator<String> it = coll.iterator();
//獲取數據
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());
}
}
運行結果:
aaaa
bbbb
cccc
Exception in thread "main" java.util.NoSuchElementException
at java.base/java.util.ArrayList$Itr.next(ArrayList.java:896)
at com.itheima.sh.demo_03.Demo02.main(Demo02.java:22)
分析異常的原因:
說明:當迭代器對象指向集合時,可以獲取集合中的元素,如果迭代器的光標移動集合的外邊時,此時迭代器對象不再指向集合中的任何元素,會報NoSuchElementException沒有這個元素異常。
解決方案:在使用next()函數獲取集合中元素前,使用hasNext()判斷集合中是否還有元素。
上述代碼一條語句重複執行多次,我們可以考慮使用循環來控制執行的次數,
循環條件是 迭代器對象.hasNext() 爲false時表示集合中沒有元素可以獲取了,循環條件迭代器對象.hasNext() 爲true的時候說明還可以獲取元素。
代碼演示如下:
package cn.itcast.sh.iterator;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
/*
* 演示迭代器的使用
*/
public class IteratorDemo {
public static void main(String[] args) {
//創建集合對象
Collection coll=new ArrayList();
//向集合中添加數據
coll.add("aaaa");
coll.add("bbbb");
coll.add("cccc");
//根據當前集合獲取迭代器對象
Iterator it = coll.iterator();
//取出數據
/*System.out.println(it.next());//it.next()表示獲取迭代器對象指向集合中的數據
System.out.println(it.next());
System.out.println(it.next());
System.out.println(it.next());*/
//使用while循環遍歷集合
while(it.hasNext())//it.hasNext()表示循環條件,如果爲true,說明集合中還有元素可以獲取,否則沒有元素
{
//獲取元素並輸出
System.out.println(it.next());
}
}
}
tips:
1.使用while循環迭代集合的快捷鍵:itit
2.在進行集合元素獲取時,如果集合中已經沒有元素了,還繼續使用迭代器的next方法,將會拋出java.util.NoSuchElementException沒有集合元素異常。
2.2 迭代器的實現原理
我們在之前案例已經完成了Iterator遍歷集合的整個過程。當遍歷集合時,首先通過調用集合的iterator()方法獲得迭代器對象,然後使用hashNext()方法判斷集合中是否存在下一個元素,如果存在,則調用next()方法將元素取出,否則說明已到達了集合末尾,停止遍歷元素。
Iterator迭代器對象在遍歷集合時,內部採用指針的方式來跟蹤集合中的元素。在調用Iterator的next方法之前,迭代器的索引位於第一個元素之前,不指向任何元素,當第一次調用迭代器的next方法後,迭代器的索引會向後移動一位,指向第一個元素並將該元素返回,當再次調用next方法時,迭代器的索引會指向第二個元素並將該元素返回,依此類推,直到hasNext方法返回false,表示到達了集合的末尾,終止對元素的遍歷。
1)ctrl + N 搜索 ArrayList這個類
2)在ArrayList裏面按 alt + 7 左邊點擊Itr
3)Itr是ArrayList裏面的一個內部類。(這些東西不要求研究)
- 源碼
public class ArrayList<E>{
//Itr屬於ArrayList集合的成員內部類,實現了迭代器Iterator接口
private class Itr implements Iterator<E> {
//定義遊標,默認值是0
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
/*
1.第一個 modCount 集合結構變動次數,如:一開始add調用了4次,那麼這個變量就是4
2.第二個 expectedModCount表示期望的更改次數 在調用iterator()方法時,初始化值等於modCount ,初始化時也是4,這裏 int expectedModCount = modCount;
*/
int expectedModCount = modCount;
//判斷集合是否有數據的方法,如果有數據返回true,沒有返回false
//size表示集合長度,假設添加3個數據,那麼size的值就是3
public boolean hasNext() {
/*
判斷遊標變量cursor是否等於集合長度size,假設向集合中存儲三個數據那麼size等於3
1.第一次cursor的默認值是0 size是3 --》cursor != size --》0!=3-->返回true
*/
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
//主要作用是判斷it迭代器數據是否和list集合一致 我們下面會講解
checkForComodification();
//每次調用一次next方法都會將遊標cursor的值賦值給變量i
int i = cursor;
//判斷i是否大於等於集合長度size,如果大於等於則報NoSuchElementException沒有這個元素異常
if (i >= size)
throw new NoSuchElementException();
//獲取ArrayList集合的數組
Object[] elementData = ArrayList.this.elementData;
//判斷i的值是否大於等於數組長度,如果爲true則報併發修改異常
if (i >= elementData.length)
throw new ConcurrentModificationException();
/*
將i的值加1賦值給遊標cursor.就是將遊標向下移動一個索引,可以理解指針向下移動一次
*/
cursor = i + 1;
/*
elementData就是集合的底層數組,獲取索引i位置的元素返回給調用者
*/
return (E) elementData[lastRet = i];
}
}
}
- 講解圖
2.3迭代器的問題:併發修改異常
-
異常:
ConcurrentModificationException(併發修改異常)
-
產生原因:
- 在迭代器遍歷的同時如果使用集合對象修改集合長度(增或刪)就會出現併發修改異常。
-
代碼演示:
package com.itheima.sh.demo_04; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; public class Test01 { public static void main(String[] args) { Collection<String> list = new ArrayList<>(); list.add("aaa"); list.add("ddd"); list.add("bbb"); list.add("ccc"); Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); if("ddd".equals(s)){ //使用集合調用方法刪除 list.remove(s); } } System.out.println(list); } }
-
源碼
public class ArrayList<E>{
private class Itr implements Iterator<E> {
int cursor; // index of next element to return
int lastRet = -1; // index of last element returned; -1 if no such
/*
1.第一個 modCount 集合結構變動次數,如:一開始add調用了4次,那麼這個變量就是4
2.第二個 expectedModCount表示期望的更改次數 在調用iterator()方法時,初始化值等於 modCount ,初始化時也是4,這裏 int expectedModCount = modCount;
*/
int expectedModCount = modCount;
public boolean hasNext() {
return cursor != size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[lastRet = i];
}
}
}
1.爲什麼會報併發修改異常?
1)在Itr內部類中的成員位置有這樣一行代碼,
private class Itr implements Iterator<E> {
int expectedModCount = modCount;
}
由於是成員變量,所以在執行list.iterator()創建It對象時就已經有值了。
上述兩個變量含義:
第一個 modCount 集合結構變動次數,如:一開始add調用了4次,那麼這個變量就是4,
第二個 expectedModCount 在調用iterator()方法時,初始化值等於modCount ,初始化時也是4,這裏 int expectedModCount = modCount;
2)it.next(): 看源代碼可以發現每次在next()調用後,都會先調用checkForComodification()這個方法;
public E next() {
// checkForComodification(): 主要作用是判斷it迭代器數據是否和list集合一致,
checkForComodification();
}
3)在checkForComodification()方法體中有如下代碼:
final void checkForComodification() {
// 這個方法判斷當 modCount != expectedModCount 時,
//拋出異常ConcurrentModificationException:
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
4)
//如果我們調用的是集合list的remove方法,那麼modCount 就會+1 而expectedModCount 不變,這就會造成 modCount != expectedModCount;
public boolean remove(Object o) {
if (o == null) {
-------------
} else {
-------------刪除方法
fastRemove(index);
----------------
}
return false;
}
fastRemove(index);方法:
private void fastRemove(int index) {
//這裏加1了
modCount++;
。。。。。。。。。。。。。
}
-
解決辦法:
- 使用集合增加或刪除都會出現併發修改異常。
- 在Iterator迭代器中,有刪除方法可以避免異常。但是他沒有增加的方法。
public class Test01 { public static void main(String[] args) { Collection<String> list = new ArrayList<>(); list.add("aaa"); list.add("ddd"); list.add("bbb"); list.add("ccc"); Iterator<String> it = list.iterator(); while(it.hasNext()){ String s = it.next(); if("ddd".equals(s)){ //使用集合調用方法刪除 // list.remove(s); //使用迭代器的刪除方法 it.remove(); } } System.out.println(list); } }
-
爲什麼使用迭代器Iterator中的刪除方法就不會報併發修改異常了
5)如果我們調用迭代器的remove方法,expectedModCount 會重新賦值, 迭代器中的remove方法: public void remove() { 。。。。。。。。。 expectedModCount = modCount; 。。。。。。。。。。。。。。。。。 }
-
在迭代器中有特殊的子類可以做元素的增加,這個類是ArrayList裏面的內部類,叫ListItr.
因爲以後也不用,所以現在不講了,但是簡單擴展一下告訴你有這個東西,以後萬一有興趣大家自己學一下。
2.4 增強for
增強for循環(也稱for each循環)是JDK1.5以後出來的一個高級for循環,專門用來遍歷數組和集合的。它的內部原理其實是個Iterator迭代器,所以在遍歷的過程中,不能對集合中的元素進行增刪操作。
格式:
for(元素的數據類型 變量 : Collection集合or數組){
//寫操作代碼
}
它用於遍歷Collection和數組。通常只進行遍歷元素,不要在遍歷的過程中對集合元素進行增刪操作。
代碼演示
public class NBForDemo1 {
public static void main(String[] args) {
int[] arr = {3,5,6,87};
//使用增強for遍歷數組
for(int a : arr){//a代表數組中的每個元素
System.out.println(a);
}
Collection<String> coll = new ArrayList<String>();
coll.add("小河神");
coll.add("老河神");
coll.add("神婆");
for(String s :coll){
System.out.println(s);
}
}
}
tips:
1.快捷鍵:
1)數組/集合.for
2)iter2.增強for循環必須有被遍歷的目標,目標只能是Collection或者是數組;
3.增強for(迭代器)僅僅作爲遍歷操作出現,不能對集合進行增刪元素操作,否則拋出ConcurrentModificationException併發修改異常
4.如果需要使用索引,也不能使用增強for
小結:Collection是所有單列集合的根接口,如果要對單列集合進行遍歷,通用的遍歷方式是迭代器遍歷或增強for遍歷。
第三章 泛型
3.1 泛型引入
集合是一個容器,可以保存對象。集合中是可以保存任意類型的對象。
List list = new ArrayList();
list.add(“abc”); 保存的是字符串對象
list.add(123); 保存的是Integer對象
list.add(new Person()); 保存的是自定義Person對象
這些對象一旦保存到集合中之後,都會被提升成Object類型。當我們取出這些數據的時候,取出來的時候一定也是以Object類型給我們,所以取出的數據發生多態了。發生多態了,當我們要使用保存的對象的特有方法或者屬性時,需要向下轉型。而向下轉型有風險,我們還得使用 instanceof關鍵字進行判斷,如果是想要的數據類型才能夠轉換,不是不能強制類型轉換,使用起來相對來說比較麻煩。
舉例:
現在要使用String類的特有方法,需要把取出的obj向下轉成String類型。
String s = (String)obj;
代碼如下:
需求:查看集合中字符串數據的長度。
分析和步驟:
1)創建一個ArrayList的集合對象list;
2)使用list集合調用add()函數向集合中添加幾個字符串數據和整數數據;
3)迭代集合分別取出集合中的數據,並查看集合中的字符串數據的長度;
代碼如下:
package cn.itcast.sh.generic.demo;
import java.util.ArrayList;
import java.util.Iterator;
/*
* 泛型引入
*/
public class GenericDemo1 {
public static void main(String[] args) {
// 創建集合對象
ArrayList list = new ArrayList();
// 向集合中添加數據
list.add("aaa");
list.add("bbb");
list.add("ccc");
list.add(true);
list.add(123);
//迭代集合
for (Iterator it = list.iterator(); it.hasNext();) {
Object obj = it.next();
/*
* 需求:想使用String類型的特有的函數查看字符串的長度
* Object obj = it.next();上述代碼發生多態了,想使用String類中特有的函數必須得強制類型轉換
* 可是由於集合可以存儲任意類型的對象,而這裏只是適合String類型的強制類型轉換,其他數據類型會報classCastException類轉換
* 異常,如果爲了不報異常只能在轉換前需要判斷,這樣做比較麻煩
* 由於這裏的需求只是遍歷集合後對於取出來的數據是String類型,查看他的長度,其他數據類型不管
* 我們能否想辦法不讓運行時報錯呢,在存儲的時候就告訴我,只要是String類型的可以存儲,其他數據類型不讓存儲,這樣做起來效率會高一些
*/
String s=(String)obj;
System.out.println(s+"長度是"+s.length());
}
}
}
上述的情況會發生ClassCastException異常。發生這個異常的原因是由於集合中保存了多種不同類型的對象,而在取出的時候沒有進行類型的判斷,直接使用了強轉。
換句話也就是說我們存儲的時候,任何類型都讓我們存儲。
然後我們取的時候,卻報錯,拋異常。非常不靠譜。你應該在我存的時候就告訴我:鎖哥,我只能存字符串,其他引用數據類型不能存儲,那麼這樣我在取出數據的時候就不會犯錯了。
假設我們在使用集合的時候,如果不讓給集合中保存類型不同的對象,那麼在取出的時候即使有向下轉型,也不會發生異常。
我們回顧下以前學習的數組容器:
在前面學習數組的時候,我們知道數組這類容器在定義好之後,類型就已經確定,如果保存的數據類型不一致,編譯直接報錯。
代碼舉例如下所示:
數組是個容器,集合也是容器,數組可以在編譯的時候就能檢測數保存的數據類型有問題,如果我們在定義集合的時候,也讓集合中的數據類型進行限定,然後在編譯的時候,如果類型不匹配就不讓編譯通過, 那麼運行的時候也就不會發生ClassCastException。
要做到在向集合中存儲數據的時候限定集合中的數據類型,也就是說編譯的時候會檢測錯誤。java中從JDK1.5後提供了一個新的技術,可以解決這個問題:泛型技術。
3.2 泛型概述
泛型的格式:
<具體的數據類型>
使用格式:
ArrayList<限定集合中的數據類型> list = new ArrayList<限定集合中的數據類型>();
說明:給集合加泛型,就是讓集合中只能保存具體的某一種數據類型。
使用泛型改進以上程序中存在的問題:
說明:由於創建ArrayList集合的時候就已經限定集合中只能保存String類型的數據,所以編譯的時候保存其他的數據類型就會報錯,這樣就達到了我們上述保存數據的目的了。
3.3 使用泛型的好處
上一節只是講解了泛型的引入,那麼泛型帶來了哪些好處呢?
- 將運行時期的ClassCastException,轉移到了編譯時期變成了編譯失敗。
- 避免了類型強轉的麻煩。
通過我們如下代碼體驗一下:
public class GenericDemo2 {
public static void main(String[] args) {
Collection<String> list = new ArrayList<String>();
list.add("abc");
list.add("itcast");
// list.add(5);//當集合明確類型後,存放類型不一致就會編譯報錯
// 集合已經明確具體存放的元素類型,那麼在使用迭代器的時候,迭代器也同樣會知道具體遍歷元素類型
Iterator<String> it = list.iterator();
while(it.hasNext()){
String str = it.next();
//當使用Iterator<String>控制元素類型後,就不需要強轉了。獲取到的元素直接就是String類型
System.out.println(str.length());
}
}
}
tips:泛型是數據類型的一部分,我們將類名與泛型合併一起看做數據類型。
3.4 泛型的注意事項
1)泛型只支持引用數據類型(類類型或接口類型等),泛型不支持基本數據類型:
ArrayList<int> list = new ArrayList<int>();//錯誤的
2)泛型不支持數據類型以繼承的形式存在,要求前後泛型的數據類型必須一致:
ArrayList<Object> list = new ArrayList<String>();//錯誤的
3)在jdk1.7之後,泛型也可以支持如下寫法:
ArrayList<String> list = new ArrayList<>();//正確的
注意:現在的開發中,泛型已經成爲編寫代碼的規範。
3.5 自定義泛型
在集合中,不管是接口還是類,它們在定義的時候類或接口名的後面都使用<標識符>,當我們在使用的時候,可以指定其中的類型。如果當前的類或接口中並沒有<標識符>,我們在使用的時候也不能去指定類型。
舉例:比如我們之前所學習的集合ArrayList類:
new ArrayList
說明:在ArrayList類上有一個泛型的參數:E
設計API的時候,設計者並不知道我們要用ArrayList存儲哪種數據類型,所以他定義了一個泛型。
然後當我們使用ArrayList的時候,我們知道要存儲的類型,比如要存儲String類型:
ArrayList<String> list = new ArrayList<String>();
當我們new對象的時候,就把泛型E替換成了String,於是JVM就知道我們要存儲的其實是String。
泛型:如果我們不確定數據的類型,可以用一個標識符來代替。
如果我們在使用的時候已經確定要使用的數據類型了,我們在創建對象的時候可以指定使用的數據類型。
泛型自定義格式:
<標識符>
這裏的標識符可以是任意的字母、數字、下劃線和 $ 。但是這裏一般規範使用單個大寫字母。
注意:自定義泛型也屬於標識符,滿足標識符的命名規則。1)數字不能開始;2)關鍵字不能作爲標識符;
根據以上分析我們可以思考一個問題:既然我們學習過的集合類可以定義泛型,那麼我們自己在描述類或接口的時候,是否也可以在自己的類或接口上定義泛型,然後別人使用的時候也可以指定這個類型呢?
答案當然是可以的。
自定義泛型類(掌握)
泛型類:
在定義類的時候,在類名的後面書寫泛型。
格式:
class 類名<泛型參數>
{
}
泛型參數其實就是標識符。
分析和步驟:
1)創建測試類GenericDemo1 ,在這個類中定義一個main函數;
2)定義一個泛型類Demo;
3)在這個類中定義一個成員變量name,類型是泛型ABC類型;
4)在定義一個非靜態成員函數show,接收參數給name屬性賦值,局部變量name的類型是ABC類型;
5)在main函數中創建Demo類的對象,並指定泛型分別是String 和Integer類型;
package cn.itcast.sh.generic.demo1;
/*
* 自定義泛型類演示
* 在類上定義的泛型,當外界創建這個對象的時候,由創建者指定泛型的類型
* 在類上定義的泛型,類中的成員變量和函數都可以使用
*/
class Demo<ABC>
{
ABC name;
public void show(ABC name)
{
this.name=name;
}
}
public class GenericDemo1 {
public static void main(String[] args) {
//創建Demo類的對象
/*
* 注意如果這裏創建Demo類的對象時沒有指定泛型類的類型時,這裏ABC默認是Object類型
* 如下代碼所示創建Demo類的對象的時候,指定的泛型類類型是String類型,
* 那麼Demo類中的泛型類ABC就是String類
* 同理,如果指定的數據類型是Integer,那麼ABC就是Integer類
*/
// Demo<String> d=new Demo<String>();
Demo<Integer> d=new Demo<Integer>();
// d.show("哈哈");
d.show(123);
System.out.println(d.name);
}
}
說明:
1)在類上定義的泛型,當外界在創建這個類的對象的時候,需要創建者自己來明確當前泛型的具體類型;
2)在類上定義的泛型,在類中的方法上和成員變量是可以使用的;
3)上述代碼中如果創建Demo類的對象時沒有指定泛型類的類型時,那麼ABC默認是Object類型;
4)上述代碼中如果創建Demo類的對象時指定的泛型類類型是String類型,那麼ABC默認是String類型;
注意:對於自定義泛型類只有在創建這個類的對象的時候纔可以指定泛型的類型。
在方法上使用泛型(掌握)
我們不僅可以在自己定義的類或接口上使用泛型,還可以在自定義的函數上使用泛型。
雖然可以在類上定義泛型,但是有時類中的方法需要接收的數據類型和類上外界指定的類型不一致。也就是說對於某個函數而言參數的數據類型和所屬類的泛型類型不一致了,這時我們可以在這個類中的這個方法上單獨給這個方法設定泛型。
在函數上使用泛型的格式:
函數修飾符 <泛型名> 函數返回值類型 方法名( 泛型名 變量名 )
{
函數體;
}
說明:函數返回值類型前面的<泛型名>相當於定義了方法參數列表中泛型的類型。
代碼演示如下圖所示:
說明:
1)類上的泛型是在創建這個類的對象的時候明確具體是什麼類型;
2)方法上的泛型是在真正調用這個方法時,傳遞的是什麼類型,泛型就會自動變成什麼類型;
舉例:上述代碼中當調用者傳遞的是2,那麼這個Q就代表Integer類型。
如果調用者傳遞的是new Student(“班長”,19),那麼這個Q類型就是Student類型。
3)上述method函數中表示定義的泛型,而參數列表中的Q q是在使用泛型類型,而這裏的Q類型具體由調用者來指定;
**泛型接口和泛型傳遞(**掌握)
通過查閱API得知,類支持泛型,那麼接口也可以支持泛型,比如集合中的接口.
那麼既然API中的接口支持泛型,自己定義的接口同樣也可以使用泛型。
泛型接口的格式:
修飾符 interface 接口名<泛型>{}
問題:泛型類的泛型,在創建類的對象時確定。
那麼接口又無法創建對象,什麼時候確定泛型的類型呢?有兩種方式可以實現。
方式1:類實現接口的時候,直接明確泛型類型。
方式2:類實現接口的時候,還不確定數據類型,這時可以在實現類上隨便先定義個泛型,當這個類被創建對象的時候,
就明確了類上的泛型,於是接口上的泛型也明確了。
我們管方式2這種方式叫做泛型的傳遞。
代碼實現如下:
舉例,API中集合的泛型使用情況解釋如下所示:
比如:
interface Collection<E>{
}
interface List<E> extends Collection<E>{
}
class ArrayList<E> implements List<E>{
}
ArrayList<String> list = new ArrayList<String>();
結論:通過以上操作,上述集合接口中的泛型類型是String類型。
3.6泛型通配符
-
格式:
1)<?> :可以表示任何類型 2)<? extends XXX> :表示可以接受XXX和XXX的子類類型(泛型的上限限定) 舉例:<? extends Person> :?代表的是一種類型,當前這個類型可以是Person本身,也可以是Person的子類。 3)<? super XXX> :表示可以接受XXX和XXX的父類類型(泛型的下限限定) 舉例: <? super Student> :?代表當前的類型可以是Student類型,也可以是Student的父類類型。
-
示例代碼:
public class Demo03 { public static void main(String[] args) { //創建對象 ArrayList<String> list1 = new ArrayList<>(); ArrayList<Integer> list2 = new ArrayList<>(); //調用方法 method(list1); method(list2); } //定義方法 //<?>可以接受任何類型 public static void method(ArrayList<?> l){ } }
public class Demo04 { public static void main(String[] args) { //創建集合 ArrayList<Person> list1 = new ArrayList<>(); //創建集合 ArrayList<Student> list2 = new ArrayList<>(); //調用方法 method(list1); method(list2); } //實際接受的是Person或者是Person的子類(泛型的上限) public static void method(ArrayList<? extends Person> list){ } }
public class Demo05 { public static void main(String[] args) { //創建集合 ArrayList<Person> list1 = new ArrayList<>(); //創建集合 ArrayList<Student> list2 = new ArrayList<>(); //調用方法 method(list1); method(list2); } //實際接受的是Student以及Student的父類類型(泛型的下限) public static void method(ArrayList<? super Student> list){ } }
3.6泛型在開發中的使用
類上、方法上、接口上定義泛型,我們今天只是學習寫法。在開發中也不需要自己定義泛型。剛入行我們做的只是使用泛型,而不是定義泛型。
第四章 數據結構
4.1 數據結構介紹
數據結構 : 數據用什麼樣的方式組合在一起。就是數據的存儲方式。
4.2 常見數據結構
數據存儲的常用結構有:棧、隊列、數組、鏈表和紅黑樹。我們分別來了解一下:
4.2.1棧和隊列
棧
- 棧:stack,又稱堆棧,它是運算受限的線性表,其限制是僅允許一端進行插入和刪除操作,不允許在其他任何位置進行添加、查找、刪除等操作。
簡單的說:採用該結構的集合,對元素的存取有如下的特點
- 先進後出(即,存進去的元素,要在後它後面的元素依次取出後,才能取出該元素)。例如,子彈壓進彈夾,先壓進去的子彈在下面,後壓進去的子彈在上面,當開槍時,先彈出上面的子彈,然後才能彈出下面的子彈。
- 棧的入口、出口的都是棧的頂端位置。
- 需求:演示向棧中存儲數據ABC,然後再取出數據的過程。
這裏兩個名詞需要注意:
- 壓棧:就是存元素。即,把元素存儲到棧的頂端位置,棧中已有元素依次向棧底方向移動一個位置。
- 彈棧:就是取元素。即,把棧的頂端位置元素取出,棧中已有元素依次向棧頂方向移動一個位置。
隊列
-
隊列:queue,簡稱隊,它同堆棧一樣,也是一種運算受限的線性表,其限制是僅允許在一端進行插入,而在另一端進行取出並刪除。
簡單的說,採用該結構的集合,對元素的存取有如下的特點:
- 先進先出(即,存進去的元素,要在後它前面的元素依次取出後,才能取出該元素)。例如,小火車過山洞,車頭先進去,車尾後進去;車頭先出來,車尾後出來。排隊買票等。
- 隊列的入口、出口各佔一側。例如,下圖中的左側爲入口,右側爲出口。
- 需求:演示向隊列中存儲數據ABC,然後再取出數據的過程。
4.2.2數組和鏈表
數組
是有序的元素序列,數組是在內存中開闢一段連續的空間,並在此空間存放元素.就像是一排出租屋,有100個房間,從001到100每個房間都有固定編號,通過編號就可以快速找到租房子的人。
簡單的說,採用該結構的集合,對元素的存取有如下的特點:
- 特點:查找快,增刪慢
-
查找元素快:通過索引,可以快速訪問指定位置的元素
-
增刪元素慢
-
指定索引位置增加元素:需要創建一個新數組,將指定新元素存儲在指定索引位置,再把原數組元素根據索引,複製到新數組對應索引的位置。如下圖
-
**指定索引位置刪除元素:**需要創建一個新數組,把原數組元素根據索引,複製到新數組對應索引的位置,原數組中指定索引位置元素不復制到新數組中。如下圖
鏈表
- 鏈表:linked list,由一系列結點node(鏈表中每一個元素稱爲結點)組成,結點可以在運行時動態生成。每個結點包括多個部分:一部分是存儲數據元素的數據域,另一部分是存儲前後一個結點地址的指針域。我們常說的鏈表結構有單向鏈表與雙向鏈表,那麼這裏給大家介紹的是單向鏈表。後面講雙向鏈表。
簡單的說,採用該結構的集合,對元素的存取有如下的特點:
-
多個結點之間,通過地址進行連接。例如,多個人手拉手,每個人使用自己的右手拉住下個人的左手,依次類推,這樣多個人就連在一起了。
-
查找元素慢:想查找某個元素,需要通過連接的節點,依次向後查找指定元素。
- 增刪元素快:
說明:
查找慢:因爲每個元素在內存中位置不同,所以查找慢。
增刪快:增刪時只需要改變前後兩個元素的指針指向,對其他元素沒有任何影響。
- 增刪元素快:
4.2.3 樹基本結構介紹
樹結構
計算機中的樹結構就是生活中倒立的樹。
樹具有的特點:
- 每一個節點有零個或者多個子節點
- 沒有父節點的節點稱之爲根節點,一個樹最多有一個根節點。類似於生活中大樹的樹根。
- 每一個非根節點有且只有一個父節點
名詞 | 含義 |
---|---|
節點 | 指樹中的一個元素(數據) |
節點的度 | 節點擁有的子樹(兒子節點)的個數,二叉樹的度不大於2,例如:下面二叉樹A節點的度是2,E節點的度是1,F節點的度是0 |
葉子節點 | 度爲0的節點,也稱之爲終端結點,就是沒有兒子的節點。 |
高度 | 葉子結點的高度爲1,葉子結點的父節點高度爲2,以此類推,根節點的高度最高。例如下面二叉樹ACF的高度是3,ACEJ的高度是4,ABDHI的高度是5. |
層 | 根節點在第一層,以此類推 |
父節點 | 若一個節點含有子節點,則這個節點稱之爲其子節點的父節點 |
子節點 | 子節點是父節點的下一層節點 |
兄弟節點 | 擁有共同父節點的節點互稱爲兄弟節點 |
二叉樹
如果樹中的每個節點的子節點的個數不超過2,那麼該樹就是一個二叉樹。
二叉查找樹
上面都是關於樹結構的一些概念,那麼下面的二叉查找樹就是和java有關係的了,那麼接下來我們就開始學習下什麼是二叉查找樹。
二叉查找樹的特點:
1. 【左子樹】上所有的節點的值均【小於】他的【根節點】的值
2. 【右子樹】上所有的節點值均【大於】他的【根節點】的值
3. 每一個子節點最多有兩個子樹
4. 二叉查找樹中沒有相同的元素
說明:
1.左子樹:根節點左邊的部分稱爲左子樹.
2.右子樹: 根節點右邊的部分稱爲右子樹.
案例演示(20,18,23,22,17,24,19)數據的存儲過程;
遍歷二叉樹有幾種遍歷方式:
1)前序(根)遍歷:根-----左子樹-----右子樹
2)中序(根)遍歷:左子樹-----根-----右子樹
3)後序(根)遍歷:左子樹-----右子樹-----根
4)按層遍歷:從上往下,從左向右
遍歷獲取元素的時候可以按照"左中右"(中序(根)遍歷)的順序進行遍歷來實現數據的從小到大排序;
注意:二叉查找樹存在的問題:會出現"瘸子"的現象,影響查詢效率
(17,24,19,18,20,23)數據的存儲過程;
平衡二叉樹
概述
爲了避免出現"瘸子"的現象,減少樹的高度,提高我們的搜素效率,又存在一種樹的結構:“平衡二叉樹”
規則:它的左右兩個子樹的高度差的絕對值不超過1,並且左右兩個子樹都是一棵平衡二叉樹
如下圖所示:
1.如上圖所示,左圖是一棵平衡二叉樹.
舉例:
1)根節點10,左右兩子樹的高度差的絕對值是1。10的左子樹有3個子節點(743),10的右子樹有2個子節點.所 以高度差是1.
2)15的左子樹沒有節點,右子樹有一個子節點(17),兩個子樹的高度差的絕對值是1.
2.而右圖不是一個平衡二叉樹,雖然根節點10左右兩子樹高度差是0(左右子樹都是3個子節點),但是右子樹15的左右子樹高度差爲2,不符合定義。15的左子樹的子節點爲0,右子樹的子節點爲2.差的絕對值是2。所以右圖不是一棵平衡二叉樹。
說明:爲什麼需要平衡二叉樹?如下圖:
左圖是一個平衡二叉樹,如果查到左子樹的葉子節點需要執行3次。而右圖不是一個平衡二叉樹,那麼查到最下面的葉子節點需要執行5次,相對來說平衡二叉樹查找效率更高。
旋轉
在構建一棵平衡二叉樹的過程中,當有新的節點要插入時,檢查是否因插入後而破壞了樹的平衡,如果是,則需要做旋轉去改變樹的結構,變爲平衡的二叉樹。
各種情況如何旋轉:
左左:只需要做一次右旋就變成了平衡二叉樹。
右右:只需要做一次左旋就變成了平衡二叉樹。
左右:先做一次分支的左旋,再做一次樹的右旋,才能變成平衡二叉樹。
右左:先做一次分支的右旋,再做一次數的左旋,才能變成平衡二叉樹。
課上只講解“左左”的情況,其餘情況都作爲擴展去學習,這裏只是讓你知道怎麼旋轉即可。
左左
左左即爲在原來平衡的二叉樹上,在節點的左子樹的左子樹下,有新節點插入,導致節點的左右子樹的高度差爲2,如下即爲"18"節點的左子樹"16",的左子樹"13",插入了節點"10"導致失衡。
需求:二叉樹已經存在的數據:18 16 20 13 17
後添加的數據是:10
說明:
1.左左:只需要做一次右旋就變成了平衡二叉樹。
2.右旋:將節點的左支往右拉,左子節點變成了父節點,並把晉升之後多餘的右子節點出讓給降級節點的左子節點
擴展(自己去學習)
如果搞不懂可以問老師
左旋:
左旋就是將節點的右支往左拉,右子節點變成父節點,並把晉升之後多餘的左子節點出讓給降級節點的右子節點;
右旋:
將節點的左支往右拉,左子節點變成了父節點,並把晉升之後多餘的右子節點出讓給降級節點的左子節點
舉個例子,像上圖是否平衡二叉樹的圖裏面,左圖在沒插入前"19"節點前,該樹還是平衡二叉樹,但是在插入"19"後,導致了"15"的左右子樹失去了"平衡",
所以此時可以將"15"節點進行左旋,讓"15"自身把節點出讓給"17"作爲"17"的左樹,使得"17"節點左右子樹平衡,而"15"節點沒有子樹,左右也平衡了。如下圖,
由於在構建平衡二叉樹的時候,當有新節點插入時,都會判斷插入後時候平衡,這說明了插入新節點前,都是平衡的,也即高度差絕對值不會超過1。當新節點插入後,
有可能會有導致樹不平衡,這時候就需要進行調整,而可能出現的情況就有4種,分別稱作左左,左右,右左,右右。
左左
左左即爲在原來平衡的二叉樹上,在節點的左子樹的左子樹下,有新節點插入,導致節點的左右子樹的高度差爲2,如下即爲"10"節點的左子樹"7",的左子樹"4",插入了節點"5"或"3"導致失衡。
左左調整其實比較簡單,只需要對節點進行右旋即可,如下圖,對節點"10"進行右旋,
左右
左右即爲在原來平衡的二叉樹上,在節點的左子樹的右子樹下,有新節點插入,導致節點的左右子樹的高度差爲2,如上即爲"11"節點的左子樹"7",的右子樹"9",
插入了節點"10"或"8"導致失衡。
左右的調整就不能像左左一樣,進行一次旋轉就完成調整。我們不妨先試着讓左右像左左一樣對"11"節點進行右旋,結果圖如下,右圖的二叉樹依然不平衡,而右圖就是接下來要
講的右左,即左右跟右左互爲鏡像,左左跟右右也互爲鏡像。
左右這種情況,進行一次旋轉是不能滿足我們的條件的,正確的調整方式是,將左右進行第一次旋轉,將左右先調整成左左,然後再對左左進行調整,從而使得二叉樹平衡。
即先對上圖的節點"7"進行左旋,使得二叉樹變成了左左,之後再對"11"節點進行右旋,此時二叉樹就調整完成,如下圖:
右左
右左即爲在原來平衡的二叉樹上,在節點的右子樹的左子樹下,有新節點插入,導致節點的左右子樹的高度差爲2,如上即爲"11"節點的右子樹"15",的左子樹"13",
插入了節點"12"或"14"導致失衡。
前面也說了,右左跟左右其實互爲鏡像,所以調整過程就反過來,先對節點"15"進行右旋,使得二叉樹變成右右,之後再對"11"節點進行左旋,此時二叉樹就調整完成,如下圖:
右右
右右即爲在原來平衡的二叉樹上,在節點的右子樹的右子樹下,有新節點插入,導致節點的左右子樹的高度差爲2,如下即爲"11"節點的右子樹"13",的左子樹"15",插入了節點
"14"或"19"導致失衡。
右右只需對節點進行一次左旋即可調整平衡,如下圖,對"11"節點進行左旋。
紅黑樹
概述
紅黑樹是一種自平衡的二叉查找樹,是計算機科學中用到的一種數據結構,它是在1972年由Rudolf Bayer發明的,當時被稱之爲平衡二叉B樹,後來,在1978年被
Leoj.Guibas和Robert Sedgewick修改爲如今的"紅黑樹"。它是一種特殊的二叉查找樹,紅黑樹的每一個節點上都有存儲位表示節點的顏色,可以是紅或者黑;
紅黑樹不是高度平衡的,它的平衡是通過"紅黑樹的特性"進行實現的;
紅黑樹的特性:
1. 每一個節點或是紅色的,或者是黑色的。
2. 根節點必須是黑色
3. 每個葉節點(Nil)是黑色的;(如果一個節點沒有子節點,則該節點相應的指針屬性值爲Nil,這些 Nil視爲葉節點)
4. 如果某一個節點是紅色,那麼它的子節點必須是黑色(不能出現兩個紅色節點相連的情況)
5. 對每一個節點,從該節點到其所有後代葉節點的路徑上,均包含相同數目的黑色節點
如下圖所示就是一個紅黑樹
在進行元素插入的時候,和之前一樣; 每一次插入完畢以後,使用黑色規則進行校驗,如果不滿足紅黑規則,就需要通過變色,左旋和右旋來調整樹,使其滿足紅黑規則;