Java泛型

一、 什么是泛型

泛型,即“参数化类型”,顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用时传入具体的类型(类型实参)

二、泛型类、泛型接口、泛型方法

  • 2-1 简单的泛型类
public class Test {

    public static void main(String[] args) {

    Box integerBox = new Box();
    Box stringBox = new Box();
 
    integerBox.add(new Integer(10));
    stringBox.add(new String("菜鸟教程"));
 
    System.out.printf("整型值为 :%d\n\n", integerBox.get());
    System.out.printf("字符串为 :%s\n", stringBox.get());
    }

}
 public class Box  {
        private T data;
          public Box() {
        }

        public Box(T data) {
          this.data = data;
      }

        public T getData() {
         return data;
      }

}

看完上面的例子应该有一个疑问:在泛型接口、泛型类和泛型方法的定义过程中,我们常见的如T、E、K、V等形式的参数常用于表示泛型形参,来接收来自外部使用时候传入的类型实参。相同的泛型类传入不同的类型实参,生成的实例的类型是不是一样的呢?这句翻译一下就是问,上面例子中的integerBox 和 stringBox 这两个实例对象的类型是不是一样呢?
↓↓↓↓↓我们来验证一下↓↓↓↓↓

public class Test {

    public static void main(String[] args) {

        Box name = new Box("dog");
        Box age = new Box(521);

        System.out.println("name class:" + name.getClass());      // com.jowney.Box
        System.out.println("age class:" + age.getClass());        // com.jowney.Box
        System.out.println(name.getClass() == age.getClass());    // true

    }

}

由此,我们发现,在使用泛型类时,虽然传入了不同的泛型实参(上例传入了String和Integer),但并没有真正意义上生成不同的类型,传入不同泛型实参的泛型类的实例的类型还是原来的最基本的类型(本实例中为Box),当然,在逻辑上我们可以理解成多个不同的泛型类型。

究其原因在于,Java中的泛型这一概念提出的目的导致其只是作用于代码编译阶段,在编译过程中,对于正确检验泛型结果后,会将泛型的相关信息擦出,也就是说,成功编译过后的class文件中是不包含任何泛型信息的。泛型信息不会进入到运行时阶段。

对此总结成一句话:泛型类型在逻辑上看以看成是多个不同的类型,实际上都是相同的类型。

↓↓↓↓↓问题来喽↓↓↓↓↓

public class Test {

    public static void main(String[] args) {

        Box name = new Box(66);
        Box age = new Box(521);

        getData(name);
        getData(age);   // 1

    }
    
    public static void getData(Box data){
        System.out.println("data :" + data.getData());
    }

}

如上例,Number是Integer的父类,这个时候注释1的代码会报错吗?会,错误提示是:

The method getData(Box) in the type Test is not applicable for the >arguments (Box)。

显然,通过提示信息,我们知道Box在逻辑上不能视为Box的父类。这也证实了上面的总结(编译过程中泛型类型在逻辑上可以看成是多个不同的类型,编译完成后实际上都是相同的类型)。
这怎么办,怎么让它编译通过
↓↓↓↓↓解决一下↓↓↓↓↓

public class Test {

    public static void main(String[] args) {

        Box name = new Box("dog");
        Box age = new Box(512);
        Box number = new Box(114);

        getData(name);
        getData(age);
        getData(number);
    }

    public static void getData(Box data) {
        System.out.println("data :" + data.getData());
    }

}

这就需要类型通配符来解决了。类型通配符一般是使用 ? 代替具体的类型实参。注意了,此处是类型实参,而不是类型形参!且Box在逻辑上是Box、Box...等所有Box<具体类型实参>的父类。

类型通配符上限通过形如Box形式定义,相对应的,类型通配符下限为Box形式,其含义与类型通配符上限正好相反,在此不作过多阐述了。

  • 2-2泛型方法

(1)
注意 上例中的public static void getData(Box data)不是泛型方法。
如果你定义了一个泛型(类、接口),那么Java规定,你不能在静态方法、静态初块等所有静态内容中使用泛型类的类型形参 。例如:

public class A {
    public static void func(T t) {
    //报错,编译不通过
    }
}

(2) 什么是泛型方法:定义泛型方法,其格式是:修饰符 <类型参数列表> 返回类型 方法名(形参列表) { 方法体 }。例如:public static int func(List list, Map map) { ... },其中T和S是泛型类型参数。

/**
 * 泛型方法的基本介绍
 * @param tClass 传入的泛型实参
 * @return T 返回值为T类型
 * 说明:
 *     1)public 与 返回值中间非常重要,可以理解为声明此方法为泛型方法。
 *     2)只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
 *     3)表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
 *     4)与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
 */
public  T genericMethod(Class tClass)throws InstantiationException ,
  IllegalAccessException{
        T instance = tClass.newInstance();
        return instance;
}

(3)泛型的作用域:

  • class A { ... }中T的作用域就是整个A;
  • public func(...) { ... }中T的作用域就是方法func;
  • 类型参数也存在作用域覆盖的问题,可以在一个泛型模板类/接口中继续定义泛型方法,例如:
class A {
    // A已经是一个泛型类,其类型参数是T
    public static  void func(T t) {
    // 再在其中定义一个泛型方法,该方法的类型参数也是T
    }
}
//当上述两个类型参数冲突时,在方法中,方法的T会覆盖类的T,即和普通变量的作用域一样,内部覆盖外部,外部的同名变量是不可见的。
//除非是一些特殊需求,一定要将局部类型参数和外部类型参数区分开来,避免发生不必要的错误,因此一般正确的定义方式是这样的:
class A {
    public static  void func(S s) {

    }
} 

(4)泛型方法类型参数范围限定
泛型方法的类型参数可以指定上限,类型上限必须在类型参数声明的地方定义上限,不能在方法参数中定义上限。规定了上限就只能在规定范围内指定类型实参,超出这个范围就会直接编译报错。

  • void func(List list){ ... },正确
  • void func(T t){ ... },正确
  • void func(List list){ ... } ,编译错误
  • void func(List dest, List src) { ... },编译正确

(5)泛型方法调用

  • 显式指定方法的类型参数,类型参数要写在尖括号中并放在方法名之前。例如:object. func(...),这样就显式指定了泛型方法的类型参数为String,那么所有出现类型参数T的地方都将替换成String类型。

  • 隐式地自动推断,不指明泛型参数,编译器根据传入的实参类型自动推断类型参数。例如: void func(T t){ ... }隐式调用object.func("name"),根据"name"的类型String推断出类型参数T的类型是String

  • 避免歧义,例如: void func(T t1, T t2){ ... }如果这样调用的话object.func("name", 15); 虽然编译不会报错,但是仍然会有很大隐患,T到底应该是String还是Integer存在歧义;

  • 有些歧义Java是会直接当成编译错误的,即所有和泛型参数有关的歧义,例如: void func(List l1, List l2){...}如果这样调用的话,object.func(new List(), new List()); 这里会有歧义,编译器无法知道T到底应该是String还是Integer,这种歧义会直接报错的,编译无法通过。即泛型方法中,如果类型参数刚好就是泛型参数的类型实参,那么这个类型实参不得有歧义,否则直接编译报错。

(6) 避免泛型方法与其他方法混淆
在java中,泛型类的定义非常简单,但是泛型方法就比较复杂了。
尤其是我们见到的大多数泛型类中的成员方法也都使用了泛型,有的甚至泛型类中也包含着泛型方法,这样在初学者中非常容易将泛型方法理解错了。

public class Test {
    public static void main(String[] args) {


    }
   //这个类是个泛型类
   public class Generic{     
        private T key;

        public Generic(T key) {
            this.key = key;
        }

        //我想说的其实是这个,虽然在方法中使用了泛型,但是这并不是一个泛型方法。
        //这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类已经声明过的泛型。
        //所以在这个方法中才可以继续使用 T 这个泛型。
        public T getKey(){
            return key;
        }

        /**
         * 这个方法显然是有问题的,在编译器会给我们提示这样的错误信息"cannot reslove symbol E"
         * 因为在类的声明中并未声明泛型E,所以在使用E做形参和返回值类型时,编译器会无法识别。  */
        public E setKey(E key){
             this.key = key
        }
      
    }

    /** 
     * 这才是一个真正的泛型方法。
     * 首先在public与返回值之间的必不可少,这表明这是一个泛型方法,并且声明了一个泛型T
     * 这个T可以出现在这个泛型方法的任意位置.
     * 泛型的数量也可以为任意多个 
     *    如:public  K showKeyName(Generic container){
     *        ...
     *        }
     */
    public  T showKeyName(Generic container){
        System.out.println("container key :" + container.getKey());
        //当然这个例子举的不太合适,只是为了说明泛型方法的特性。
        T test = container.getKey();
        return test;
    }

    //这也不是一个泛型方法,这就是一个普通的方法,只是使用了Generic这个泛型类做形参而已。
    public void showKeyValue1(Generic obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

    //这也不是一个泛型方法,这也是一个普通的方法,只不过使用了泛型通配符?
    //同时这也印证了泛型通配符章节所描述的,?是一种类型实参,可以看做为Number等所有类的父类
    public void showKeyValue2(Generic obj){
        Log.d("泛型测试","key value is " + obj.getKey());
    }

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


 

}
public class GenericFruit {
    class Fruit{
        @Override
        public String toString() {
            return "fruit";
        }
    }

    class Apple extends Fruit{
        @Override
        public String toString() {
            return "apple";
        }
    }

    class Person{
        @Override
        public String toString() {
            return "Person";
        }
    }

    class GenerateTest{
        public void show_1(T t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同。
        //由于泛型方法在声明的时候会声明泛型,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型。
        public  void show_3(E t){
            System.out.println(t.toString());
        }

        //在泛型类中声明了一个泛型方法,使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。
        public  void show_2(T t){
            System.out.println(t.toString());
        }
    }

    public static void main(String[] args) {
        Apple apple = new Apple();
        Person person = new Person();

        GenerateTest generateTest = new GenerateTest();
        //apple是Fruit的子类,所以这里可以
        generateTest.show_1(apple);
        //编译器会报错,因为泛型类型实参指定的是Fruit,而传入的实参类是Person
        //generateTest.show_1(person);

        //使用这两个方法都可以成功
        generateTest.show_2(apple);
        generateTest.show_2(person);

        //使用这两个方法也都可以成功
        generateTest.show_3(apple);
        generateTest.show_3(person);
    }
}

你可能感兴趣的:(Java泛型)