泛型机制

一:为什么会引入泛型

泛型的本质是为了参数化类型,在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。

二:泛型的意义
1、代码复用:适用于多种数据类型执行相同的代码

private static int add(int a, int b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static float add(float a, float b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

private static double add(double a, double b) {
    System.out.println(a + "+" + b + "=" + (a + b));
    return a + b;
}

如果没有泛型,要实现不同类型的加法,每种类型都需要重载一个add方法;

引入泛型,我们可以复用为一个方法:

private static <T extends Number> double add(T a, T b) {
    System.out.println(a + "+" + b + "=" + (a.doubleValue() + b.doubleValue()));
    return a.doubleValue() + b.doubleValue();
}

2、不需要强制类型转换。泛型中的类型在使用时指定(类型安全,编译器会检查类型)

List list = new ArrayList();
list.add("xstring");
list.add(1000);
list.add(new Person());

我们在使用上述list中,list中的元素都是Object类型(无法约束其中的类型),所以在取出集合元素时需要人为的强制类型转化到具体的目标类型,且很容易出现java.lang.ClassCastException异常。

引入泛型,它将提供类型的约束,提供编译前的检查:

// list中只能放String, 不能放其它类型的元素
 List<String> list = new ArrayList<String>();

三:泛型使用
1、泛型类

class Point<T>{         // 此处可以随便写标识符号,T是type的简称  
    private T var ;     // var的类型由T指定,即:由外部指定  
    public T getVar(){  // 返回值的类型由外部决定  
        return var ;  
    }  
    public void setVar(T var){  // 设置的类型也由外部决定  
        this.var = var ;  
    }  
}

2、泛型接口

interface Info<T>{        // 在接口上定义泛型  
    public T getVar() ; // 定义抽象方法,抽象方法的返回值就是泛型类型  
}  
class InfoImpl<T> implements Info<T>{   // 定义泛型接口的子类  
    private T var ;             // 定义属性  
    public InfoImpl(T var){     // 通过构造方法设置属性内容  
        this.setVar(var) ;    
    }  
    public void setVar(T var){  
        this.var = var ;  
    }  
    public T getVar(){  
        return this.var ;  
    }  
}

3、泛型方法

java 泛型 实体类 java泛型实现原理_泛型

定义泛型方法语法格式如上图

解释一下定义泛型方法时,必须在返回值前边加一个,来声明这是一个泛型方法,持有一个泛型T,然后才可以用泛型T作为方法的返回值。 Class的作用就是指明泛型的具体类型,而Class类型的变量c,可以用来创建泛型类的对象。

java 泛型 实体类 java泛型实现原理_java_02

调用泛型方法语法格式如上图

为什么要用变量c来创建对象呢?既然是泛型方法,就代表着我们不知道具体的类型是什么,也不知道构造方法如何,因此没有办法去new一个对象,但可以利用变量c的newInstance方法去创建对象,也就是利用反射创建对象 。

在本例中,forName()方法中传入的是User类的完整路径,因此返回的是Class类型的对象,因此调用泛型方法时,变量c的类型就是Class,因此泛型方法中的泛型T就被指明为User,因此变量obj的类型为User。
泛型方法要求的参数是Class类型,而Class.forName()方法的返回值也是Class,因此可以用Class.forName()作为参数。其中,forName()方法中的参数是何种类型,返回的Class就是何种类型。

4、为什么要使用泛型方法呢,或者泛型方法相对于泛型类的优势是什么?
因为泛型类要在实例化的时候就指明类型,如果想换一种类型,不得不重新new一次,可能不够灵活;而泛型方法可以在调用的时候指明类型,更加灵活方便。

四:泛型的上下限
1、为什么需要泛型上下限
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。例如:<? extends A>表示该类型参数可以是A(上边界)或者A的子类类型。编译时擦除到类型A,即用A类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待

2、泛型上下限的使用
在使用泛型的时候,我们可以为传入的泛型类型实参进行上下边界的限制,如:类型实参只准传入某种类型的父类或某种类型的子类。

//上限
class Info<T extends Number>{    // 此处泛型只能是数字类型
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class demo1{
    public static void main(String args[]){
        Info<Integer> i1 = new Info<Integer>() ;        // 声明Integer的泛型对象
    }
}

//下限
class Info<T>{
    private T var ;        // 定义泛型变量
    public void setVar(T var){
        this.var = var ;
    }
    public T getVar(){
        return this.var ;
    }
    public String toString(){    // 直接打印
        return this.var.toString() ;
    }
}
public class GenericsDemo21{
    public static void main(String args[]){
        Info<String> i1 = new Info<String>() ;        // 声明String的泛型对象
        Info<Object> i2 = new Info<Object>() ;        // 声明Object的泛型对象
        i1.setVar("hello") ;
        i2.setVar(new Object()) ;
        fun(i1) ;
        fun(i2) ;
    }
    
    //下限
    public static void fun(Info<? super String> temp){    // 只能接收String或Object类型的泛型,String类的父类只有Object类
        System.out.print(temp + ", ") ;
    }
  }
}

<?> 无限制通配符
<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类
<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

为了获得最大限度的灵活性,要在表示 生产者或者消费者 的输入参数上使用通配符,使用的规则就是:生产者有上限、消费者有下限

  1. 如果参数化类型表示一个 T 的生产者,使用 < ? extends T>;
  2. 如果它表示一个 T 的消费者,就使用 < ? super T>;
  3. 如果既是生产又是消费,那使用通配符就没什么意义了,因为你需要的是精确的参数类型。

五:如何理解Java中的泛型是伪泛型?泛型中类型擦除
Java泛型是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除有助于泛型的理解和使用。

1、泛型的类型擦除原则

  • 消除类型参数声明,即删除<>及其包围的部分。
  • 替换所有的类型参数为原生态类型,即根据类型参数的上下界推断,如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则替换为类型参数的上界或者下界,比如形如和<? extends Number>的类型参数被替换为Number,<? super Number>被替换为Object。
  • 为了保证类型安全,必要时插入强制类型转换代码
  • 通过“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”

2、如何进行擦除

擦除类无限制类型擦除

java 泛型 实体类 java泛型实现原理_开发语言_03


擦除类有限制类型擦除

java 泛型 实体类 java泛型实现原理_java 泛型 实体类_04


擦除方法定义中的类型参数

java 泛型 实体类 java泛型实现原理_java 泛型 实体类_05

3、如何理解泛型的编译期检查?
类型检查就是编译时完成的,Java编译器是通过先检查代码中泛型的类型,然后在进行类型擦除,再进行编译。

public static  void main(String[] args) {  

    ArrayList<String> list = new ArrayList<String>();  
    list.add("123");  
    list.add(123);//编译错误  
}

在上面的程序中,使用add方法添加一个整型,在IDE中,直接会报错,说明这就是在编译之前的检查,因为如果是在编译之后检查,类型擦除后,原始类型为Object,是应该允许任意引用类型添加的。可实际上却不是这样的,这恰恰说明了关于泛型变量的使用,是会在编译之前检查的。

public class Test {  

    public static void main(String[] args) {  

        ArrayList<String> list1 = new ArrayList();  
        list1.add("1"); //编译通过  
        list1.add(1); //编译错误  
        String str1 = list1.get(0); //返回类型就是String  

        ArrayList list2 = new ArrayList<String>();  
        list2.add("1"); //编译通过  
        list2.add(1); //编译通过  
        Object object = list2.get(0); //返回类型就是Object  

        new ArrayList<String>().add("11"); //编译通过  
        new ArrayList<String>().add(22); //编译错误  

        String str2 = new ArrayList<String>().get(0); //返回类型就是String  
    }  
}

通过上面的例子,我们可以明白,类型检查就是针对引用的,谁是一个引用,用这个引用调用泛型方法,就会对这个引用调用的方法进行类型检测,而无关它真正引用的对象。

4、泛型中参数话类型为什么不考虑继承关系?

ArrayList<String> list1 = new ArrayList<Object>(); //编译错误  
ArrayList<Object> list2 = new ArrayList<String>(); //编译错误

泛型出现的原因,就是为了解决类型转换的问题。我们使用了泛型考虑继承关系,到头来,还是要自己强转,违背了泛型设计的初衷。所以java不允许这么干。

5、如何理解泛型的多态?
有这样一个泛型类:

class Pair<T> {  

    private T value;  

    public T getValue() {  
        return value;  
    }  

    public void setValue(T value) {  
        this.value = value;  
    }  
}

一个子类继承它

class DateInter extends Pair<Date> {  

    @Override  
    public void setValue(Date value) {  
        super.setValue(value);  
    }  

    @Override  
    public Date getValue() {  
        return super.getValue();  
    }  
}

类型擦除会造成多态的冲突,而JVM解决方法就是桥接方法。
也就是说,子类中真正覆盖父类两个方法的就是这两个我们看不到的JVM自动生成的桥方法。而打在我们自己定义的setvalue和getValue方法上面的@Oveerride只不过是假象。而桥方法的内部实现,就只是去调用我们自己重写的那两个方法。 所以,虚拟机巧妙的使用了桥方法,来解决了类型擦除和多态的冲突。

6、如何理解基本类型不能作为泛型类型?
因为当类型擦除后,ArrayList的原始类型变为Object,但是Object类型不能存储基本类型比如int值,只能引用Integer的值。 另外需要注意,我们能够使用list.add(1)是因为Java基础类型的自动装箱拆箱操作。

7、为什么ArrayList或其他集合都不能存储基本类型呢?
结论:Java集合中实际存放的只是对象的引用,每个集合元素都是一个引用变量,实际内容都放在堆内存或者方法区里面

原理:ArrayList底层是数组,其内存地址是连续的,为了方便集合通过索引或者下标直接查询,必须知道起始内存地址(address),然后其寻址公式为:result = address + 4 * n(4代表数据类型大小,int占4个字节),这样就可以通过下标访问,运行时所有的引用大小都是一样的(取决于操作系统位数、JVM版本、GC机制等,但是和类型本身没有关系),而基本数据类型的大小不一样,比如int占4个byte,long占8个等,所以只能存放引用变量。基本类型可以通过包装类使用,即自动装箱拆箱操作。

8、如何理解泛型类型不能实例化?

因为在 Java 编译期没法确定泛型参数化类型,也就找不到对应的类字节码文件,所以自然就不行了,此外由于T 被擦除为 Object,如果可以 new T() 则就变成了 new Object(),失去了本意。

如果我们确实需要实例化一个泛型,应该如何做呢?可以通过反射实现:

static <T> T newTclass (Class < T > clazz) throws InstantiationException, IllegalAccessException {
    T obj = clazz.newInstance();
    return obj;
}

9、泛型数组:能不能采用具体的泛型类型进行初始化?

List<String>[] lsa = new List<String>[10]; // Not really allowed.
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
oa[1] = li; // Unsound, but passes run time store check
String s = lsa[1].get(0); // Run-time error ClassCastException.

JVM 泛型的擦除机制,所以上面代码可以给 oa[1] 赋值为 ArrayList 也不会出现异常,但是在取出数据的时候却要做一次类型转换,所以就会出现 ClassCastException,如果可以进行泛型数组的声明则上面说的这种情况在编译期不会出现任何警告和错误,只有在运行时才会出错,但是泛型的出现就是为了消灭 ClassCastException,所以如果 Java 支持泛型数组初始化操作就是自相矛盾。

解决方式:

  • 我们在使用到泛型数组的场景下应该尽量使用列表集合替换
  • 也可以通过反射来初始化泛型数组算是优雅实现 java.lang.reflect.Array.newInstance(Class
    componentType, int length) 方法来创建一个具有指定类型和维度的数组

10、如何获取泛型的参数类型?
既然类型被擦除了,那么如何获取泛型的参数类型呢?可以通过反射(java.lang.reflect.Type)获取泛型。
java.lang.reflect.Type是Java中所有类型的公共高级接口, 代表了Java中的所有类型. Type体系中类型的包括:数组类型(GenericArrayType)、参数化类型(ParameterizedType)、类型变量(TypeVariable)、通配符类型(WildcardType)、原始类型(Class)、基本类型(Class), 以上这些类型都实现Type接口。

11、如何理解异常中使用泛型?
不能抛出也不能捕获泛型类的对象。事实上,泛型类扩展Throwable都不合法。
为什么不能扩展Throwable,因为异常都是在运行时捕获和抛出的,而在编译的时候,泛型信息全都会被擦除掉,那么,假设上面的编译可行,那么,在看下面的定义:

try{

} catch(Problem<Integer> e1) {

} catch(Problem<Number> e2) {

}

类型信息被擦除后,那么两个地方的catch都变为原始类型Object,那么也就是说,这两个地方的catch变的一模一样,就相当于下面的这样

try{

} catch(Problem<Object> e1) {

} catch(Problem<Object> e2) {

}

这个当然就是不行的。

12、如何理解泛型类中的静态方法和静态变量?
泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

public class Test2<T> {    
    public static T one;   //编译错误    
    public static  T show(T one){ //编译错误    
        return null;    
    }    
}

因为泛型类中的泛型参数的实例化是在定义对象的时候指定的,而静态变量和静态方法不需要使用对象来调用。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。