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 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 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 {
    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 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 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. 创建类对象时,可以不使用泛型,默认为,最好写上。其在创建对象时将泛型传递到类中,相当于将该类改造为:
    public class Test {
        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 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 {
        void Method(I i);
    }
    

    其有两种方式可以指定其泛型:

    1. 定义实现类时,确定泛型类型

    public class GenericInterfaceImpl1 implements MyInterface{
        @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 implements MyInterface {
        @Override
        public void Method(I s) {
            System.out.println(s);
        }
    
        public static void main(String[] args) {
            Person mi = new Person<>();
            mi.Method(123);
        }
    }
    

    这样的方式相当于把接口的泛型继承下来,实现类自己也会不知道自己的泛型具体是什么。
    最后在Main中创建实现类对象时,指定泛型为Integer,那么这个类中的Method(Integer s)方法的参数类型也会变为Integer

    注意上述两种写法的不同:

    • 实现类确定泛型,在接口中直接传递泛型的"实参",在创建对象时,与普通类创建对象一样的。
      类似于Scanner类:


      创建方式:
      Scanner sc = new Scanner();

    • 实现类对象确定泛型(常用),类中泛型写法与接口泛型一致,在创建对象时可以使用普通类的创建对象的方式(不推荐),尽量指定泛型类的泛型。
      例如ArrayList类:


      创建对象的方式:
      ArrayList arr = new ArrayList<>();

    • 实现类中实现接口时如果只是普通的实现,实现类接口都没有泛型的声明,那么接口中定义的泛型默认为Object类型。

    3.6 泛型通配符

    当使用泛型类或者接口时,传递的数据中,泛型类型不确定,可以通过通配符表示。但是一旦使用泛型的通配符后,只能使用Object类中的共性方法(因为类型不确定,所以在使用后得到的都是Object类型,如果相导转会原来的类型,只能向下转型),集合中元素自身方法无法使用。

    1. 通配符基本使用

    泛型的通配符:不知道使用什么类型来接收的时候,此时可以使用?。
    ?表示未知通配符。

    public static void main(String[] args) {
        Collection list1 = new ArrayList();
        getElement(list1);
        Collection list2 = new ArrayList();
        getElement(list2);
    //  ArrayList list03 = new ArrayList();
    }
    public static void getElement(Collection coll){
      }
    //?代表可以接收任意类型
    

    只能作为方法的参数使用,表示该方法接收的参数为不确定的类型,不能作为创建对象使用,直接编译报错。

    tips:泛型不存在继承关系 Collection list = new ArrayList();这种是错误的。

    2. 通配符高级使用----受限泛型

    之前设置泛型的时候,实际上是可以任意设置的,只要是类就可以设置。但是在JAVA的泛型中可以指定一个泛型的上限下限

    泛型的上限

    • 格式类型名称 对象名称
    • 意义只能接收该类型及其子类

    泛型的下限

    • 格式类型名称 对象名称
    • 意义只能接收该类型及其父类型

    也就是设置了泛型通配符的范围,使之在某一个范围内有效。

    比如:现已知Object类,String 类,Number类,Integer类,其中NumberInteger的父类,Object类是所有类的父类。

    示例:

    public class Demo06Generic {
        public static void main(String[] args) {
            Collection list1 = new ArrayList();
            Collection list2 = new ArrayList();
            Collection list3 = new ArrayList();
            Collection list4 = new ArrayList();
    
            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 coll){}
        // 泛型的下限:此时的泛型?,必须是Number类型或者Number类型的父类
        public static void getElement2(Collection coll){}
    }
    
    

    4.斗地主案例

    4.1 案例介绍

    按照斗地主的规则,完成洗牌发牌的动作。

    具体规则:

    使用54张牌打乱顺序,三个玩家参与游戏,三人交替摸牌,每人17张牌,最后三张留作底牌。

    4.2 案例分析

    • 准备牌:

      牌可以设计为一个ArrayList,每个字符串为一张牌。
      每张牌由花色数字两部分组成,我们可以使用花色集合数字集合嵌套迭代完成每张牌的组装。牌由Collections类的shuffle方法进行随机排序。

    • 发牌

      将每个人以及底牌设计为ArrayList,将最后3张牌直接存放于底牌,剩余牌通过对3取模依次发牌。

    • 看牌

      直接打印每个集合。

    4.3 代码实现

    1.生成牌

    准备54张牌存到集合中,其中有:

    • 特殊牌:大王,小王
    • 普通牌:定义数组/集合存储花色,定义数组/集合存储牌的大小序号,嵌套循环组装牌。
    /*
            1.准备牌
            */
            //定义一个存储54张牌的ArrayList集合,泛型使用String
            ArrayList 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 player01 = new ArrayList<>();
            ArrayList player02 = new ArrayList<>();
            ArrayList player03 = new ArrayList<>();
            ArrayList 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 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 player01 = new ArrayList<>();
            ArrayList player02 = new ArrayList<>();
            ArrayList player03 = new ArrayList<>();
            ArrayList 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);
        }
    }
    

    你可能感兴趣的:(2020-09-04--Java--day02【泛型,斗地主案例】)