2020-09-04--Java--day02【泛型,斗地主案例】

主要内容

  • 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

  1. 为什么会发生类型转换异常呢?
    我们来分析下:由于集合中什么类型的对象都可以存储。导致取出时强转引发运行时 `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,遍历变量oObject类型。
  • 使用迭代器对象的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();
    }
}

分析:

  1. 定义一个泛型为MVP的类,因为我们不确定该类在创建对象时的数据类型是什么?
  2. 创建类对象时,可以不使用泛型,默认为<Object>,最好写上。其在创建对象时将泛型传递到类中,相当于将该类改造为:
public class Test<Object> {
    private Object name;

    public Object getName() {
        return name;
    }
    public void setName(Object name) {
        this.name = name;
    }

该对象在在可以传递任何数据类型(Object类为所有类的父类),并且数据类型都为Object类型的数据,但是JDK会自动根据传入参数类型进行判断。

  1. 泛型设置为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类指定了接口的泛型IString

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类,其中NumberInteger的父类,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.发牌

发牌的思路:
因为牌的顺序是打乱的,所以直接按顺序发牌即可,跟我们平时真实打牌时是一样的,也就是将牌按照顺序分为三份。

  1. 首先床架三个数组对象作为玩家村存储牌的容器,在创建一个容器数组存储三张底牌。
  2. 我们使用对牌数组索引%3的方式,这样每次产生的值都是0,1,2,
  3. 由于牌的总数是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);
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 207,113评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,644评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,340评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,449评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,445评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,166评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,442评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,105评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,601评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,066评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,161评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,792评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,351评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,352评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,584评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,618评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,916评论 2 344