主要內(nèi)容
- 3.泛型
- 4.斗地主案例
3.泛型
3.1 泛型概述
在前面學習集合時,我們都知道集合中是可以存放任意對象的,只要把對象存儲集合后,那么這時他們都會被提升成Object類型。當我們在取出每一個對象,并且進行相應的操作,這時必須采用類型轉(zhuǎn)換。
大家觀察下面代碼:
public class Person {
public static void main(String[] args) {
// 1.創(chuàng)建集合對象
Collection coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add(1);
//可以添加,因為集合沒有聲明泛型,但是在之后向下轉(zhuǎn)型時,回拋出ClassCastException,不能把Integer類型轉(zhuǎn)換為String類型
for (Object o : coll) {
System.out.println(o); //zhangsan lisi
//使用Object類型接收
Object st = o;
// 向下轉(zhuǎn)型為String對象
String str = (String) st;
//調(diào)用String類的方法
System.out.println(str.length()); //8 4
}
}
}
程序在運行時發(fā)生了問題java.lang.ClassCastException。
-
為什么會發(fā)生類型轉(zhuǎn)換異常呢?
我們來分析下:由于集合中什么類型的對象都可以存儲。導致取出時強轉(zhuǎn)引發(fā)運行時 `ClassCastException。
2.怎么來解決這個問題呢?
Collection雖然可以存儲各種對象,但實際上通常Collection只存儲同一類型對象。例如都是存儲字符串對象。因此在JDK5之后,新增了泛型(Generic)語法,讓你在設(shè)計API時可以指定類或方法支持泛型,這樣我們使用API的時候也變得更為簡潔,并得到了編譯時期的語法檢查。
- 泛型:可以在類或方法中預支地使用未知的類型。
tips:一般在創(chuàng)建對象時,將未知的類型確定具體的類型。當沒有指定泛型時,默認類型為Object類型。
圖例:

3.2 使用泛型的好處
上一節(jié)只是講解了泛型的引入,那么泛型帶來了哪些好處呢?
利端:
- 將運行時期的ClassCastException,轉(zhuǎn)移到了編譯時期變成了編譯失敗。
- 避免了類型強轉(zhuǎn)的麻煩。
弊端:
- 泛型是什么類型,只能存儲什么類型的數(shù)據(jù)。
實例:
public class Person {
public static void main(String[] args) {
// 1.創(chuàng)建集合對象,使用泛型-->String
Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
// coll.add(1); 直接編譯報錯
for (Object o : coll) {
// o為Object類型,需要向下轉(zhuǎn)型為String對象
String str = (String) o;
//調(diào)用String類的方法
System.out.println(str+":"+str.length()); //8 4
}
Iterator<String> it = coll.iterator();
while (it.hasNext()) {
// 直接使用String類型變量接收
String str = it.next();
System.out.println(str+":"+str.length());
}
}
}
coll.add(1)在編寫帶碼時出現(xiàn)錯誤,因為Collection集設(shè)置了泛型為String類型,只能存儲String類型的數(shù)據(jù)。
- 在使用增強for進行遍歷時,需要向下轉(zhuǎn)型為
String,遍歷變量o為Object類型。 - 使用迭代器對象的
hasNext()和next()遍歷元素時,next()的返回值就是該集合對象的所設(shè)置的泛型,直接String接收即可,無需類型轉(zhuǎn)換。
3.3 含有泛型的類
泛型 :用來靈活地將數(shù)據(jù)類型應用到不同的類、方法、接口當中。將數(shù)據(jù)類型作為參數(shù)進行傳遞。
意思就是在在創(chuàng)建類對象時,可以設(shè)置其創(chuàng)建對象的屬性類型。
- 泛型是一個未知的數(shù)據(jù)類型,當我們不確定什么什么數(shù)據(jù)類型的時候,可以使用泛型。
- 泛型可以接收任意的數(shù)據(jù)類型,可以使用
Integer,String,Student... - 創(chuàng)建類的對象的時候確定泛型的數(shù)據(jù)類型。
定義格式
修飾符 class 類名<泛型類型>{
// ...
}
實例
public class Test<MVP> {
private MVP name;
public MVP getName() {
return name;
}
public void setName(MVP name) {
this.name = name;
}
main:
public static void main(String[] args) {
// 不適用泛型,那么該類的對象的泛默認為Object,但JDK會根據(jù)參數(shù)類型自動識別
Test test1 = new Test();
test1.setName("lisi");
System.out.println(test1.getName()); //lisi
System.out.println(test1.getName().getClass().getName()); //java.lang.String
Object name = test1.getName();
// 使用Integer作為泛型,該對象參數(shù)只能傳遞Integer類型的數(shù)據(jù),其他數(shù)據(jù)編譯錯誤
Test<Integer> test2 = new Test<>();
test2.setName(123);
System.out.println(test2.getName()); //123
System.out.println(test2.getName().getClass().getName()); //java.lang.Integer
Integer name1 = test2.getName();
// 使用String作為該對象的泛型,只能傳遞并返回String類型的數(shù)據(jù),其他數(shù)據(jù)編譯錯誤
Test<String> test3 = new Test<>();
test3.setName("234");
System.out.println(test3.getName()); //234
System.out.println(test3.getName().getClass().getName()); //java.lang.String
String name2 = test3.getName();
}
}
分析:
- 定義一個泛型為MVP的類,因為我們不確定該類在創(chuàng)建對象時的數(shù)據(jù)類型是什么?
- 創(chuàng)建類對象時,可以不使用泛型,默認為
<Object>,最好寫上。其在創(chuàng)建對象時將泛型傳遞到類中,相當于將該類改造為:
public class Test<Object> {
private Object name;
public Object getName() {
return name;
}
public void setName(Object name) {
this.name = name;
}
該對象在在可以傳遞任何數(shù)據(jù)類型(Object類為所有類的父類),并且數(shù)據(jù)類型都為Object類型的數(shù)據(jù),但是JDK會自動根據(jù)傳入?yún)?shù)類型進行判斷。
- 泛型設(shè)置為
Integer/String,將該類改造后的結(jié)果不在贅述,替換MVP的位置即可。該類對象只能傳遞Integer/String類型的參數(shù),并且返回同樣類型的參數(shù),其他類型傳遞時會編譯錯誤。
3.4 含有泛型的方法
定義格式
修飾符 static/() <表示泛型的變量> 返回值類型 方法名(參數(shù)類型 參數(shù)){
// ...
}
-
表示泛型的變量可以為任意的標識符(只要符合Java標識符規(guī)則),作用就是:當我們不確定該方法傳進來的參數(shù)類型時,可以將該方法變?yōu)榉盒头椒ǎ趨?shù)列表中的參數(shù)類型設(shè)置為標識泛型的變量,這樣在調(diào)用時數(shù)據(jù)類型不在固定。
當然
參數(shù)類型可以設(shè)置為已有的數(shù)據(jù)類型,例如:int/String,這樣的在調(diào)用方法時只能傳遞相應的數(shù)據(jù)類型,那么該含有泛型的方法與普通的方法一樣了,沒有必要定義泛型方法。
實例
public class Test2 {
// 定義一個泛型的靜態(tài)方法
public <S>void Method(S m) {
System.out.println(m);
System.out.println(m.getClass().getName());
}
public static void main(String[] args) {
new Test2().Method(13);
//13
//java.lang.Integer
}
}
可以看到輸出結(jié)果為13,以及對應被包含的包裝類。
3.5 含有泛型的接口
當我們不確定接口的實現(xiàn)類在重寫接口中的方法時,可以設(shè)置接口的泛型
定義格式:
修飾符 interface接口名<代表泛型的變量> {
// ...
}
定義和使用
例如定義MyInterface接口,泛型為I:
public interface MyInterface<I> {
void Method(I i);
}
其有兩種方式可以指定其泛型:
1. 定義實現(xiàn)類時,確定泛型類型
public class GenericInterfaceImpl1 implements MyInterface<String>{
@Override
public void method(String s) {
System.out.println(s);
}
public static void main(String[] args) {
GenericInterfaceImpl1 gi = new GenericInterfaceImpl1();
gi.method("123");
}
}
GenericInterfaceImpl1類指定了接口的泛型I為String。
2.創(chuàng)建實現(xiàn)類對象時,指定接口泛型(常用)
public class Person<I> implements MyInterface<I> {
@Override
public void Method(I s) {
System.out.println(s);
}
public static void main(String[] args) {
Person<Integer> mi = new Person<>();
mi.Method(123);
}
}
這樣的方式相當于把接口的泛型繼承下來,實現(xiàn)類自己也會不知道自己的泛型具體是什么。
最后在Main中創(chuàng)建實現(xiàn)類對象時,指定泛型為Integer,那么這個類中的Method(Integer s)方法的參數(shù)類型也會變?yōu)?code>Integer。
注意上述兩種寫法的不同:
實現(xiàn)類確定泛型,在接口中直接傳遞泛型的
"實參",在創(chuàng)建對象時,與普通類創(chuàng)建對象一樣的。
類似于Scanner類:
創(chuàng)建方式:
Scanner sc = new Scanner();實現(xiàn)類對象確定泛型(常用),類中泛型寫法與接口泛型一致,在創(chuàng)建對象時可以使用普通類的創(chuàng)建對象的方式(不推薦),盡量指定泛型類的泛型。
例如ArrayList類:
創(chuàng)建對象的方式:
ArrayList<String> arr = new ArrayList<>();在
實現(xiàn)類中實現(xiàn)接口時如果只是普通的實現(xiàn),實現(xiàn)類和接口都沒有泛型的聲明,那么接口中定義的泛型默認為Object類型。
3.6 泛型通配符
當使用泛型類或者接口時,傳遞的數(shù)據(jù)中,泛型類型不確定,可以通過通配符<?>表示。但是一旦使用泛型的通配符后,只能使用Object類中的共性方法(因為類型不確定,所以在使用后得到的都是Object類型,如果相導轉(zhuǎn)會原來的類型,只能向下轉(zhuǎn)型),集合中元素自身方法無法使用。
1. 通配符基本使用
泛型的通配符:不知道使用什么類型來接收的時候,此時可以使用?。
?表示未知通配符。
public static void main(String[] args) {
Collection<Intger> list1 = new ArrayList<Integer>();
getElement(list1);
Collection<String> list2 = new ArrayList<String>();
getElement(list2);
// ArrayList<?> list03 = new ArrayList<?>();
}
public static void getElement(Collection<?> coll){
}
//?代表可以接收任意類型
<?>只能作為方法的參數(shù)使用,表示該方法接收的參數(shù)為不確定的類型,不能作為創(chuàng)建對象使用,直接編譯報錯。
tips:泛型不存在繼承關(guān)系
Collection<Object> list = new ArrayList<String>();這種是錯誤的。
2. 通配符高級使用----受限泛型
之前設(shè)置泛型的時候,實際上是可以任意設(shè)置的,只要是類就可以設(shè)置。但是在JAVA的泛型中可以指定一個泛型的上限和下限。
泛型的上限:
-
格式:
類型名稱 <? extends 類 > 對象名稱 -
意義:
只能接收該類型及其子類
泛型的下限:
-
格式:
類型名稱 <? super 類 > 對象名稱 -
意義:
只能接收該類型及其父類型
也就是設(shè)置了泛型通配符<?>的范圍,使之在某一個范圍內(nèi)有效。
比如:現(xiàn)已知Object類,String 類,Number類,Integer類,其中Number是Integer的父類,Object類是所有類的父類。
示例:
public class Demo06Generic {
public static void main(String[] args) {
Collection<Integer> list1 = new ArrayList<Integer>();
Collection<String> list2 = new ArrayList<String>();
Collection<Number> list3 = new ArrayList<Number>();
Collection<Object> list4 = new ArrayList<Object>();
getElement1(list1);
//getElement1(list2);//報錯
getElement1(list3);
//getElement1(list4);//報錯
//getElement2(list1);//報錯
//getElement2(list2);//報錯
getElement2(list3);
getElement2(list4);
/*
類與類之間的繼承關(guān)系
Integer extends Number extends Object
String extends Object
*/
}
// 泛型的上限:此時的泛型?,必須是Number類型或者Number類型的子類
public static void getElement1(Collection<? extends Number> coll){}
// 泛型的下限:此時的泛型?,必須是Number類型或者Number類型的父類
public static void getElement2(Collection<? super Number> coll){}
}
4.斗地主案例
4.1 案例介紹
按照斗地主的規(guī)則,完成洗牌發(fā)牌的動作。
具體規(guī)則:
使用54張牌打亂順序,三個玩家參與游戲,三人交替摸牌,每人17張牌,最后三張留作底牌。
4.2 案例分析
-
準備牌:
牌可以設(shè)計為一個
ArrayList<String>,每個字符串為一張牌。
每張牌由花色數(shù)字兩部分組成,我們可以使用花色集合與數(shù)字集合嵌套迭代完成每張牌的組裝。牌由Collections類的shuffle方法進行隨機排序。 -
發(fā)牌
將每個人以及底牌設(shè)計為
ArrayList<String>,將最后3張牌直接存放于底牌,剩余牌通過對3取模依次發(fā)牌。 -
看牌
直接打印每個集合。
4.3 代碼實現(xiàn)
1.生成牌
準備54張牌存到集合中,其中有:
- 特殊牌:
大王,小王 - 普通牌:定義
數(shù)組/集合存儲花色,定義數(shù)組/集合存儲牌的大小序號,嵌套循環(huán)組裝牌。
/*
1.準備牌
*/
//定義一個存儲54張牌的ArrayList集合,泛型使用String
ArrayList<String> poker = new ArrayList<>();
//定義兩個數(shù)組,一個數(shù)組存儲牌的花色,一個數(shù)組存儲牌的序號
String[] colors = {"?","?","?","?"};
String[] numbers = {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
//先把大王和小王存儲到poker集合中
poker.add("大王");
poker.add("小王");
//循環(huán)嵌套遍歷兩個數(shù)組,組裝52張牌
for(String number : numbers){
for (String color : colors) {
//System.out.println(color+number);
//把組裝好的牌存儲到poker集合中
poker.add(color+number);
}
}
//System.out.println(poker);
其生成的牌時按照花色和序號順序排列的。
2.洗牌
使用集合的工具類Collections中的方法
static void shuffle(List<?> list) 使用默認隨機源對指定列表進行置換。
Collections.shuffle(poker);
//System.out.println(poker);
這時牌的順序是打亂的。
3.發(fā)牌
發(fā)牌的思路:
因為牌的順序是打亂的,所以直接按順序發(fā)牌即可,跟我們平時真實打牌時是一樣的,也就是將牌按照順序分為三份。
- 首先床架三個數(shù)組對象作為玩家村存儲牌的容器,在創(chuàng)建一個容器數(shù)組存儲三張底牌。
- 我們使用對牌數(shù)組索引
%3的方式,這樣每次產(chǎn)生的值都是0,1,2, - 由于牌的總數(shù)是
54,索引值到53,當索引大于50時,將底牌存入創(chuàng)建好的數(shù)組中。
/*
3.發(fā)牌
*/
//定義4個集合,存儲玩家的牌和底牌
ArrayList<String> player01 = new ArrayList<>();
ArrayList<String> player02 = new ArrayList<>();
ArrayList<String> player03 = new ArrayList<>();
ArrayList<String> diPai = new ArrayList<>();
/*
遍歷poker集合,獲取每一張牌
使用poker集合的索引%3給3個玩家輪流發(fā)牌
剩余3張牌給底牌
注意:
先判斷底牌(i>=51),否則牌就發(fā)沒了
*/
for (int i = 0; i < poker.size() ; i++) {
//獲取每一張牌
String p = poker.get(i);
//輪流發(fā)牌
if(i>=51){
//給底牌發(fā)牌
diPai.add(p);
}else if(i%3==0){
//給玩家1發(fā)牌
player01.add(p);
}else if(i%3==1){
//給玩家2發(fā)牌
player02.add(p);
}else if(i%3==2){
//給玩家3發(fā)牌
player03.add(p);
}
}
4.看牌
由于ArrayList重寫了toString方法,所以直接打印三個玩家的數(shù)組以及存儲底牌的數(shù)組即可。
//4.看牌
System.out.println("劉德華:"+player01);
System.out.println("周潤發(fā):"+player02);
System.out.println("周星馳:"+player03);
System.out.println("底牌:"+diPai);
運行結(jié)果:

4.4 完整代碼
import java.util.ArrayList;
import java.util.Collections;
/*
斗地主綜合案例:
1.準備牌
2.洗牌
3.發(fā)牌
4.看牌
*/
public class DouDiZhu {
public static void main(String[] args) {
/*
1.準備牌
*/
//定義一個存儲54張牌的ArrayList集合,泛型使用String
ArrayList<String> poker = new ArrayList<>();
//定義兩個數(shù)組,一個數(shù)組存儲牌的花色,一個數(shù)組存儲牌的序號
String[] colors = {"?","?","?","?"};
String[] numbers = {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
//先把大王和小王存儲到poker集合中
poker.add("大王");
poker.add("小王");
//循環(huán)嵌套遍歷兩個數(shù)組,組裝52張牌
for(String number : numbers){
for (String color : colors) {
//System.out.println(color+number);
//把組裝好的牌存儲到poker集合中
poker.add(color+number);
}
}
//System.out.println(poker);
/*
2.洗牌
使用集合的工具類Collections中的方法
static void shuffle(List<?> list) 使用默認隨機源對指定列表進行置換。
*/
Collections.shuffle(poker);
//System.out.println(poker);
/*
3.發(fā)牌
*/
//定義4個集合,存儲玩家的牌和底牌
ArrayList<String> player01 = new ArrayList<>();
ArrayList<String> player02 = new ArrayList<>();
ArrayList<String> player03 = new ArrayList<>();
ArrayList<String> diPai = new ArrayList<>();
/*
遍歷poker集合,獲取每一張牌
使用poker集合的索引%3給3個玩家輪流發(fā)牌
剩余3張牌給底牌
注意:
先判斷底牌(i>=51),否則牌就發(fā)沒了
*/
for (int i = 0; i < poker.size() ; i++) {
//獲取每一張牌
String p = poker.get(i);
//輪流發(fā)牌
if(i>=51){
//給底牌發(fā)牌
diPai.add(p);
}else if(i%3==0){
//給玩家1發(fā)牌
player01.add(p);
}else if(i%3==1){
//給玩家2發(fā)牌
player02.add(p);
}else if(i%3==2){
//給玩家3發(fā)牌
player03.add(p);
}
}
//4.看牌
System.out.println("劉德華:"+player01);
System.out.println("周潤發(fā):"+player02);
System.out.println("周星馳:"+player03);
System.out.println("底牌:"+diPai);
}
}

