主要内容
- 3.泛型
- 4.斗地主案例
3.泛型
3.1 泛型概述
在前面学习集合时,我们都知道集合中是可以存放任意对象的,只要把对象存储集合后,那么这时他们都会被提升成Object类型。当我们在取出每一个对象,并且进行相应的操作,这时必须采用类型转换。
大家观察下面代码:
public class Person {
public static void main(String[] args) {
// 1.创建集合对象
Collection coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
coll.add(1);
//可以添加,因为集合没有声明泛型,但是在之后向下转型时,回抛出ClassCastException,不能把Integer类型转换为String类型
for (Object o : coll) {
System.out.println(o); //zhangsan lisi
//使用Object类型接收
Object st = o;
// 向下转型为String对象
String str = (String) st;
//调用String类的方法
System.out.println(str.length()); //8 4
}
}
}
程序在运行时发生了问题java.lang.ClassCastException。
-
为什么会发生类型转换异常呢?
我们来分析下:由于集合中什么类型的对象都可以存储。导致取出时强转引发运行时 `ClassCastException。
2.怎么来解决这个问题呢?
Collection
虽然可以存储各种对象,但实际上通常Collection只存储同一类型对象。例如都是存储字符串对象。因此在JDK5之后,新增了泛型(Generic)语法,让你在设计API时可以指定类或方法支持泛型,这样我们使用API的时候也变得更为简洁,并得到了编译时期的语法检查。
- 泛型:可以在类或方法中预支地使用未知的类型。
tips:一般在创建对象时,将未知的类型确定具体的类型。当没有指定泛型时,默认类型为Object类型。
图例:
3.2 使用泛型的好处
上一节只是讲解了泛型的引入,那么泛型带来了哪些好处呢?
利端:
- 将运行时期的ClassCastException,转移到了编译时期变成了编译失败。
- 避免了类型强转的麻烦。
弊端:
- 泛型是什么类型,只能存储什么类型的数据。
实例:
public class Person {
public static void main(String[] args) {
// 1.创建集合对象,使用泛型-->String
Collection<String> coll = new ArrayList<>();
coll.add("zhangsan");
coll.add("lisi");
// coll.add(1); 直接编译报错
for (Object o : coll) {
// o为Object类型,需要向下转型为String对象
String str = (String) 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)
在编写带码时出现错误,因为Collection集设置了泛型为String
类型,只能存储String
类型的数据。
- 在使用增强for进行遍历时,需要向下转型为
String
,遍历变量o
为Object
类型。 - 使用迭代器对象的
hasNext()
和next()
遍历元素时,next()
的返回值就是该集合对象的所设置的泛型,直接String
接收即可,无需类型转换。
3.3 含有泛型的类
泛型 :用来灵活地将数据类型应用到不同的类、方法、接口当中。将数据类型作为参数进行传递。
意思就是在在创建类对象时,可以设置其创建对象的属性类型。
- 泛型是一个未知的数据类型,当我们不确定什么什么数据类型的时候,可以使用泛型。
- 泛型可以接收任意的数据类型,可以使用
Integer,String,Student...
- 创建类的对象的时候确定泛型的数据类型。
定义格式
修饰符 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会根据参数类型自动识别
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作为泛型,该对象参数只能传递Integer类型的数据,其他数据编译错误
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类型的数据,其他数据编译错误
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的类,因为我们不确定该类在创建对象时的数据类型是什么?
- 创建类对象时,可以不使用泛型,默认为
<Object>
,最好写上。其在创建对象时将泛型传递到类中,相当于将该类改造为:
public class Test<Object> {
private Object name;
public Object getName() {
return name;
}
public void setName(Object name) {
this.name = name;
}
该对象在在可以传递任何数据类型(Object
类为所有类的父类),并且数据类型都为Object
类型的数据,但是JDK会自动根据传入参数类型进行判断。
- 泛型设置为
Integer/String
,将该类改造后的结果不在赘述,替换MVP
的位置即可。该类对象只能传递Integer/String
类型的参数,并且返回同样类型的参数,其他类型传递时会编译错误。
3.4 含有泛型的方法
定义格式
修饰符 static/() <表示泛型的变量> 返回值类型 方法名(参数类型 参数){
// ...
}
-
表示泛型的变量
可以为任意的标识符(只要符合Java标识符规则),作用就是:当我们不确定该方法传进来的参数类型时,可以将该方法变为泛型方法,在参数列表中的参数类型
设置为标识泛型的变量
,这样在调用时数据类型不在固定。
当然
参数类型
可以设置为已有的数据类型,例如:int/String
,这样的在调用方法时只能传递相应的数据类型,那么该含有泛型的方法与普通的方法一样了,没有必要定义泛型方法。
实例
public class Test2 {
// 定义一个泛型的静态方法
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
}
}
可以看到输出结果为13,以及对应被包含的包装类。
3.5 含有泛型的接口
当我们不确定接口的实现类在重写接口中的方法时,可以设置接口的泛型
定义格式:
修饰符 interface接口名<代表泛型的变量> {
// ...
}
定义和使用
例如定义MyInterface接口,泛型为I:
public interface MyInterface<I> {
void Method(I i);
}
其有两种方式可以指定其泛型:
1. 定义实现类时,确定泛型类型
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.创建实现类对象时,指定接口泛型(常用)
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);
}
}
这样的方式相当于把接口的泛型继承下来,实现类自己也会不知道自己的泛型具体是什么。
最后在Main中创建实现类对象时,指定泛型为Integer
,那么这个类中的Method(Integer s)
方法的参数类型也会变为Integer
。
注意上述两种写法的不同:
实现类确定泛型,在接口中直接传递泛型的
"实参"
,在创建对象时,与普通类创建对象一样的。
类似于Scanner
类:
创建方式:
Scanner sc = new Scanner();
实现类对象确定泛型(常用),类中泛型写法与接口泛型一致,在创建对象时可以使用普通类的创建对象的方式(不推荐),尽量指定泛型类的泛型。
例如ArrayList
类:
创建对象的方式:
ArrayList<String> arr = new ArrayList<>();
在
实现类
中实现接口时如果只是普通的实现,实现类
和接口
都没有泛型的声明,那么接口中定义的泛型默认为Object
类型。
3.6 泛型通配符
当使用泛型类
或者接口
时,传递的数据中,泛型类型不确定,可以通过通配符<?>
表示。但是一旦使用泛型的通配符后,只能使用Object
类中的共性方法
(因为类型不确定,所以在使用后得到的都是Object
类型,如果相导转会原来的类型,只能向下转型
),集合中元素自身方法无法使用。
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){
}
//?代表可以接收任意类型
<?>
只能作为方法的参数
使用,表示该方法接收的参数为不确定的类型,不能作为创建对象
使用,直接编译报错。
tips:泛型不存在继承关系
Collection<Object> list = new ArrayList<String>()
;这种是错误的。
2. 通配符高级使用----受限泛型
之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限和下限。
泛型的上限:
-
格式:
类型名称 <? extends 类 > 对象名称
-
意义:
只能接收该类型及其子类
泛型的下限:
-
格式:
类型名称 <? super 类 > 对象名称
-
意义:
只能接收该类型及其父类型
也就是设置了泛型通配符<?>
的范围,使之在某一个范围内有效。
比如:现已知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);
/*
类与类之间的继承关系
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 案例介绍
按照斗地主的规则,完成洗牌发牌的动作。
具体规则:
使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。
4.2 案例分析
-
准备牌:
牌可以设计为一个
ArrayList<String>
,每个字符串为一张牌。
每张牌由花色数字
两部分组成,我们可以使用花色集合
与数字集合
嵌套迭代完成每张牌的组装。牌由Collections
类的shuffle
方法进行随机排序。 -
发牌
将每个人以及底牌设计为
ArrayList<String>
,将最后3
张牌直接存放于底牌,剩余牌通过对3
取模依次发牌。 -
看牌
直接打印每个集合。
4.3 代码实现
1.生成牌
准备54张牌存到集合中,其中有:
- 特殊牌:
大王,小王
- 普通牌:定义
数组/集合
存储花色,定义数组/集合
存储牌的大小序号,嵌套循环组装牌。
/*
1.准备牌
*/
//定义一个存储54张牌的ArrayList集合,泛型使用String
ArrayList<String> poker = new ArrayList<>();
//定义两个数组,一个数组存储牌的花色,一个数组存储牌的序号
String[] colors = {"♠","♥","♣","♦"};
String[] numbers = {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
//先把大王和小王存储到poker集合中
poker.add("大王");
poker.add("小王");
//循环嵌套遍历两个数组,组装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.发牌
发牌的思路:
因为牌的顺序是打乱的,所以直接按顺序发牌即可,跟我们平时真实打牌时是一样的,也就是将牌按照顺序分为三份。
- 首先床架三个数组对象作为玩家村存储牌的容器,在创建一个容器数组存储三张底牌。
- 我们使用对牌数组索引
%3
的方式,这样每次产生的值都是0,1,2
, - 由于牌的总数是
54
,索引值到53
,当索引大于50
时,将底牌存入创建好的数组中。
/*
3.发牌
*/
//定义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个玩家轮流发牌
剩余3张牌给底牌
注意:
先判断底牌(i>=51),否则牌就发没了
*/
for (int i = 0; i < poker.size() ; i++) {
//获取每一张牌
String p = poker.get(i);
//轮流发牌
if(i>=51){
//给底牌发牌
diPai.add(p);
}else if(i%3==0){
//给玩家1发牌
player01.add(p);
}else if(i%3==1){
//给玩家2发牌
player02.add(p);
}else if(i%3==2){
//给玩家3发牌
player03.add(p);
}
}
4.看牌
由于ArrayList
重写了toString
方法,所以直接打印三个玩家的数组以及存储底牌的数组即可。
//4.看牌
System.out.println("刘德华:"+player01);
System.out.println("周润发:"+player02);
System.out.println("周星驰:"+player03);
System.out.println("底牌:"+diPai);
运行结果:
4.4 完整代码
import java.util.ArrayList;
import java.util.Collections;
/*
斗地主综合案例:
1.准备牌
2.洗牌
3.发牌
4.看牌
*/
public class DouDiZhu {
public static void main(String[] args) {
/*
1.准备牌
*/
//定义一个存储54张牌的ArrayList集合,泛型使用String
ArrayList<String> poker = new ArrayList<>();
//定义两个数组,一个数组存储牌的花色,一个数组存储牌的序号
String[] colors = {"♠","♥","♣","♦"};
String[] numbers = {"2","A","K","Q","J","10","9","8","7","6","5","4","3"};
//先把大王和小王存储到poker集合中
poker.add("大王");
poker.add("小王");
//循环嵌套遍历两个数组,组装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.发牌
*/
//定义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个玩家轮流发牌
剩余3张牌给底牌
注意:
先判断底牌(i>=51),否则牌就发没了
*/
for (int i = 0; i < poker.size() ; i++) {
//获取每一张牌
String p = poker.get(i);
//轮流发牌
if(i>=51){
//给底牌发牌
diPai.add(p);
}else if(i%3==0){
//给玩家1发牌
player01.add(p);
}else if(i%3==1){
//给玩家2发牌
player02.add(p);
}else if(i%3==2){
//给玩家3发牌
player03.add(p);
}
}
//4.看牌
System.out.println("刘德华:"+player01);
System.out.println("周润发:"+player02);
System.out.println("周星驰:"+player03);
System.out.println("底牌:"+diPai);
}
}