美文网首页
Java泛型的简单使用

Java泛型的简单使用

作者: 钢镚koala | 来源:发表于2018-05-24 10:23 被阅读0次

    一、泛型的定义

    泛型这个术语的意思是"适用于许多许多的类型"。它 是JDK 1.5的一项新特性,它的本质是 参数化类型 (Parameterized Type)的应用,也就是说 所操作的数据类型被指定为一个参数,在用到的时候再指定具体的类型。

    1. 什么是参数化类型

    参数化类型就是一个编译器可以自动定制作用于特定类型上的类 。举例:

    List list = new ArrayList();
    

    这是原生类型(未引入参数化类型时)的写法,list集合中可以存储不同类型的元素,如此便有了安全隐患,编译器不能保证你取值时的转型(拆箱)一定正确。
    jdk1.5引入了参数化类型(泛型)之后,写法变为

    List<类型(例如String)> list = new ArrayList();
    

    这样的话,list中只能存储String类型的元素,编译器在编译时便会验证list中的元素是否全为String类型,否则编译错误。如此一来便不存在安全隐患,读取数据时也不需要自己进行拆箱,编译器会判断其元素类型为String。

    简单的说就是, 原本集合中用来处理的通用类型为Object,而使用了参数化类型后,编译器会自动的将Object参数的类型修改为你传递给它的参数化类型,例如此例定义一个只接收和取出String的list容器。

    二、 泛型的表达

    一对尖括号,中间包含类型信息。常见的如T、E、K、V等形式的参数常用于表示泛型。如常用的集合

    ArrayList<Food> foods = new ArrayList<Food>(); //此处的泛型类型是Food
    ArrayList<T>  list = new ArrayList<T>(); //此处的泛型类型是T,只是一个泛型标识,可以随意更改
    

    三、 泛型的使用

    泛型的参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。下面看看具体是如何定义的。

    1. 泛型类

    泛型作用于类上,叫泛型类。泛型类的定义规则:

    public class Test<T>{//此处在实例化泛型类时,必须指定T的具体类
          //key这个成员变量的类型为T,   T的类型由外部指定
          private T key;
          public Test(T key) {
              //泛型构造方法形参key的类型也为T,T的类型由外部指定
                this.key = key;
          }
          public T getKey(){
                return key;
          }
      }
    

    传入的实参类型需与泛型的类型参数类型相同,即为String.

    Test<String> testStr = new Test("Call me");
    

    传入的实参类型需与泛型的类型参数类型相同,即为Integer.

    Test<Integer> testInteger = new Test(123);
    
    System.out.println(testInteger.getKey())
    
    System.out.println(testStr.getKey());
    

    输出结果:

    05-22 14:46:50.128 28166-28166/? I/System.out: Call me
    
    05-22 14:46:50.129 28166-28166/? I/System.out: 123
    

    我们在定义泛型类的时候,其实不是必要传入泛型类型实参。这里是为了更好的 体现泛型对类的约束 ,所以在成员变量中或构造方法中传入泛型类实参,从而在泛型类Test中,我们只能使用与泛型类参数一致的变量类型或方法类型。

    1. 泛型接口

    泛型也可用于接口。与泛型类的使用和定义基本一致。看下面的例子:

    public interface Kitchen<T> {//泛型定义接口
           T cook();
    }
    
    class Cooker implemments Kitchen<String> {
    
            String[] menu =new String[]{"Noodles", "Rice", "Dumplings"};
    
            @Override
            public String cook() {
                    int r = Random().nextInt(3);
                    return menu[r];
            }
    }
    

    1.假设情景:客人随便来4份主食,通知后厨,厨师在厨房开始做饭。

    Cooker cooker = new Cooker();
    int i = 0;
    while( i < 3){
        Log.i("test","zhushi :"+ cooker.cook());
        i ++;
    }
    

    结果:

    05-22 18:16:48.676 22270-22270/com.example.papayawp.test I/test: zhushi :Rice
    05-22 18:16:48.676 22270-22270/com.example.papayawp.test I/test: zhushi :Noodles
    05-22 18:16:48.676 22270-22270/com.example.papayawp.test I/test: zhushi :Noodles
    05-22 18:16:48.677 22270-22270/com.example.papayawp.test I/test:  zhushi :Noodles
    
    1. 1 泛型方法
      到目前为止,我们看到的泛型,都是应用于整个类上。但同样可以在类中包含参数化方法,而这个方法所在的类可以是泛型类,也可以不是泛型类。也就是说,是否拥有泛型方法,与其所在的类是否是泛型没有关系。定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型<T>,然后才可以用泛型<T>作为方法的返回值。就像下面这样:
    public class TestMethods{
          public <T> void  readBook( T t){//定义泛型方法
                   Log.i( "test" ,  t.getClass().getName() );
           }
    
         public <T>  T  writeBook( T t){//标准定义泛型方法
              Log.i( "test" ,  t.getClass().getName() );
          }
    
           public static void main(String[] args){
                 TestMethods test = new TestMethods();
                 test.readBook(" ");
                 test.readBook( 1 );
                 test.writeBook('c');
                 test.writeBook(test);
            }
    }
    

    输出结果:

    java.lang.String
    java.lang.Integer
    java.lang.Character
    TestMethods
    

    TestMethods并不是参数化的,尽管这个类和其内部的方法可以被同时参数化,但是在这个例子中,只有方法readBook()、writeBook() 拥有类型参数。这是由该方法的返回类型前面的类型参数列表指明的。注意,当使用泛型类时,必须在创建对象的时候指定类型参数的值,而使用泛型方法的时候,通常不必指明参数类型,因为编译器会为我们找出具体的类型 。

    为什么要使用泛型方法呢?因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活。如我们可以像调用普通方法一样调用readBook(),而且就好像是readBook()被无限次地重载过。

    下面介绍几种情况(引用其他人,例子很好),类似泛型方法却不是:

    public class Test<T>{ // 这个是上面已经介绍过的泛型类

    private T key;
    
    public Test(T key) {
    
         this.key = key;
    
    }
    

    问题1. 虽然在方法中使用了泛型,但是这并不是一个泛型方法。这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。所以在这个方法中才可以继续使用 T 这个泛型。

    public T getKey(){
    
           return key;
    
    }
    

    问题2. 这也不是一个泛型方法,这就是一个普通的方法,只是使用了Test这个泛型类做形参而已。

    public void look(Test test){
    
        Log.d("test","key is "+ test.getKey());
    
    }
    

    问题3. 这个方法是有问题的,编译器会为我们提示错误信息:"UnKnown class E", 虽然我们声明了,也表明了这是一个可以处理泛型的类型的泛型方法。但是只声明了泛型类型T,并未声明泛型类型E,因此编译器并不知道该如何处理E这个类型。

    public T watch(Test<E> test){
    
        ...
    
    }
    

    问题4. 这个方法也是有问题的,编译器会为我们提示错误信息:"UnKnown class T"。对于编译器来说T这个类型并未项目中声明过,因此编译也不知道该如何编译这个类。所以这也不是一个正确的泛型方法声明。

        public void see(T t){
    
        }
    

    另外,对于一个static的方法而言,无法访问泛型类的类型参数,所以,如果static 方法需要使用泛型能力,就必须使其成为泛型方法。

    public  class  StaticTest<T> {
    
             ....
    
        /**
    
        * 如果在类中定义使用泛型的静态方法,需要添加额外的泛型声明(将这个方法定义成泛型方法)
    
       * 即使静态方法要使用泛型类中已经声明过的泛型也不可以。
    
       * 如:public static void show( T t ){..},此时编译器会提示错误信息:
    
       *  "StaticTest cannot be refrenced from static context"
    
       */
    
        public static <T> void show( T t){//正确定义,必须在方法上泛型声明
    
        }
    
    }
    

    3.2 泛型方法与可变参数
    再看一个泛型方法和可变参数的例子:

    public void printMsg( T... args){
           for(T t : args){
                 Log.d("test", "t is "+ t);
            }
    }
    printMsg("111",222,"aaaa",55.55);
    

    泛型方法使得该方法能够独立与类而产生变化。以下是一个基本的指导原则:无论何时,只要你能做到,就尽量使用泛型方法。也就是说,如果使用泛型方法可以取代将整个类泛型化,那么就只是用泛型方法,因为它可以使事情更清楚明白。

    四、泛型通配符

    public class Person{}
    public class Student extends Person{}
    

    我们知道Student是Person的一个子类。那么在使用Test作为形参的方法中,能否使用Test的实例传入呢?在逻辑上类似于Button和View是否可以看成具有父子关系的泛型类型呢?
    为了弄清楚这个问题,我们使用Test这个泛型类继续看下面的例子:

                public void readBook(Test<Person> test){
                         Log.d( "test" , "value is "+ test.getKey());
                 }
    
                 Test tStudent =new Test(new Student());
                 Test tPerson =new Test(new Student());
                 readBook(tPerson);
                // readBook这个方法编译器会为我们报错:inferred type is Test<Student>         but Test<Person> was Expected
    

    通过提示信息我们可以看到Test<Student>不能被看作为Test<Person>的子类。由此可以看出: 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的 。

    回到上面的例子,如何解决上面的问题?总不能为了定义一个新的方法来处理Test<Student>类型的类,这显然与java中的多台理念相违背。因此我们需要一个在逻辑上可以表示 同时 是Test<Student>和Test<Person>父类的引用类型。由此类型通配符应运而生。
    我们可以将上面的方法改一下:

            public void readBook( Test<?> test ){
    
                        Log.d( "test", "value is " +  test.getKey() );
    
            }
    

    再看一个例子:

            public class ClassA<T> {
    
                    public T t;
    
                    public ClassA(T t) {
    
                            this.t = t;
    
                    }
    
                    public void read(ClassA<?> classA) {
    
                            System.out.println("test"+classA.t.getClass().getName());
    
                    }
    
            }
    
        调用过程:(Number是抽象类,Integer是它的实现子类)
    
            ClassA<Integer> aa =new ClassA(123);
            ClassA<Number> vv =new ClassA(333);//如果上面read方法中泛型参数类型是Integer,则vv.read(vv)会传入参数与方法参数类型不匹配错误
            ClassA<Number> cc =new ClassA(555.0f);// Float也是Number的实现子类,在通配符的作用下,可以正常编译
    
            aa.read(aa);
            cc.read(cc);//输出结果是test java.lang.Integer  test  java.lang.Float
            ClassA<String> ff =new ClassA("hello ,CiCi");
            ff.read(ff) ; //输出结果是 test java.lang.String
    

    类型通配符一般是使用 <?>代替具体的类型实参,注意,此处<?>是类型实参。通俗的讲就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

    另外,如果想要只能接收Number类型的数据,不要String类型的数据。那么我们可以增加边界限定,

            public class ClassA<T>{
                    public T t;
                    public ClassA(T t) {
                            this.t = t;
                    }
    
                    public void read(ClassA< ? extends Number> classA) {//让?继承Number类,此时方法只能接收Number类型的数据
                            System.out.println("test"+classA.t.getClass().getName());
                    }
    
            }
    

    测试:

     ClassA<Integer> a =new ClassA(123);
     a.read(a);
     ClassA<Float> b =new ClassA(999.0f);
     b.read(b);
     ClassA<Double> c =new ClassA(199.99);
     c.read(c);
     ClassA<String> d =new ClassA("hello world");
     d.read(d);   //这一行代码编译器会提示错误,因为String类型并不是Number类型 的子类
    

    五、总结

    泛型的使用使我们的代码更加具有通用性,不会导致定义了一种类型之后其他的类型都无法使用该代码。通过泛型可以定义类型安全的数据结构(类型安全),而无须使用实际的数据类型(可扩展)。这能够显著提高性能并得到更高质量的代码(高性能),因为我们可以重用数据处理算法,而无须复制类型特定的代码(可重用)。

    相关文章

      网友评论

          本文标题:Java泛型的简单使用

          本文链接:https://www.haomeiwen.com/subject/kgtdjftx.html