泛型

目录

泛型三种形式

泛型继承规则

通配符类型

无限定通配符

泛型实现原理:类型擦除

特殊的擦除:桥方法

泛型的限制

总之


泛型的意义:使得程序具有更好的可读性和安全性。

泛型三种形式

泛型类:public class Axxx<T> {} ,public interface Iterable<T>

泛型方法:public static <T> getMiddle(T a) {}

类型变量:

public <T extends Comparable & Serializable> void method(T[] strs) {
    T a = strs[0];
    T b = strs[1];
    a.compareTo(b);
}

泛型继承规则

上图是正确的类型继承方式。但是下面的情况是不允许的。

public interface A<T> {
    void a(T a);
}
public class B<T> implements A<T> {
    @Override
    public void a(T a) {
        System.out.println(this);
    }
}
public class Pair<T> {
    void  print(Pair<T> pair) {
        System.out.println(pair);
    }
}
public class Test {

    public static void main(String[] args) {
        Pair<A> pairA = new Pair<>();
        Pair<B> pairB = new Pair<>();
        pairA.print(pairB); // 这里编译会出错。
    }
}

应对这种情况 Java的设计者发明了一种巧妙的 ( 仍然是安全的 ) “ 解决方案 ” : 通配符类型 。

通配符类型

通配符:<? extends A>, <? super A>

所以上面的上面的代码可以换成:

public class Pair<T> {
    void  print(Pair<? extends T> pair) {
        System.out.println(pair);
    }
}
public class Test {
    public static void main(String[] args) {
        Pair<A> pairA = new Pair<>();
        Pair<B> pairB = new Pair<>();
        pairA.print(pairB); // 这里编译会通过。
    }
}
通配符形式一

 

通配符二

无限定通配符

       还可以使用无限定的通配符 , 例如, Pair < ? > 。 初看起来 , 这好像与原始的 Pair 类型一样 。实际上 ,有很大的不同。 类型Pair < ? > 有以下方法 :

? getFirst ( )
void set First (?)

       getFirst 的返回值只能赋给一个 Object 。 setFirst 方法不能被调用, 甚至不能用Object 调用 。 Pair < ? > 和 Pair 本质的不同在于 : 可以用任意 Object 对象调用原始 Pair 类的 setObject方法。

 可以调用 setFirst ( null ) 。为什么要使用这样脆弱的类型 ? 它对于许多简单的操作非常有用。 例如 , 下面这个方法将用来测试一个 pair 是否包含一个 null 引用, 它不需要实际的类型 。

public static boolean hasNulls (Pair<?> p) {
    return p.getFirst() = null || p.getSecond() = null ;
}

通过将 hasNulls 转换成泛型方法, 可以避免使用通配符类型 :

public static < T > boolean hasNulls ( Pair < T > p )

但是, 带有通配符的版本可读性更强 。

当然如果是下面的情况还是要选择泛型:

public static void swap (Pair <?> p) {
    ? t = p.getFirst() ; // Error, 编译错误。
    p.setFirst(p.getSecond());
    p.setSecond(t); 
}

应该改成:

public static <T> void swapHelper (Pair <T> p){
    T t = p.getFirst();
    p.setFirst(p.getSecond());
    p.setSecond(t) ;
}

 

泛型实现原理:类型擦除

其实Java的泛型是伪泛型。因为Java泛型的实现方式是类型檫除。

无论和是你定义一个泛型,都会自动给你提供一个相应的原始类型(raw type)。但是这个raw type我们并不能见到,jvm也见不到。但是可以通过下面的代码证明:

Assert.assertTrue(new ArrayList<String>().getClass() == new ArrayList<Integer>().getClass());

通过字节码来看:

    public static void main(String[] args) {
        new Test().method(new Integer[]{1,2});
    }

    public <T extends Comparable & Serializable> void method(T[] strs) {
        T a = strs[0];
        T b = strs[1];
        System.out.println(a.compareTo(b));
    }
  public <T extends java.lang.Comparable & java.io.Serializable> void method(T[]
);
    descriptor: ([Ljava/lang/Comparable;)V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=5, args_size=2
         0: aload_1
         1: iconst_0
         2: aaload
         3: astore_2
         4: aload_1
         5: iconst_1
         6: aaload
         7: astore_3
         8: aload_3
         9: checkcast     #7                  // class java/io/Serializable
        12: astore        4
        14: getstatic     #8                  // Field java/lang/System.out:Ljav
a/io/PrintStream;
        17: aload_2
        18: aload         4
        20: invokeinterface #9,  2            // InterfaceMethod java/lang/Compa
rable.compareTo:(Ljava/lang/Object;)I
        25: invokevirtual #10                 // Method java/io/PrintStream.prin
tln:(I)V
        28: return
      LineNumberTable:
        line 21: 0
        line 22: 4
        line 23: 8
        line 24: 14
        line 25: 28
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      29     0  this   Lcom/leetcode/tal/template/Test;
            0      29     1  strs   [Ljava/lang/Comparable;
            4      25     2     a   Ljava/lang/Comparable;
            8      21     3     b   Ljava/lang/Comparable;
           14      15     4     s   Ljava/io/Serializable;
      LocalVariableTypeTable:
        Start  Length  Slot  Name   Signature
            0      29     1  strs   [TT;
            4      25     2     a   TT;
            8      21     3     b   TT;
    Signature: #36                          // <T::Ljava/lang/Comparable;:Ljava/
io/Serializable;>([TT;)V
}

这种状况下原始类型就会变成Comparable。

通过上面两段可以得出下面的结论。 

https://blog.csdn.net/shinecjj/article/details/52075499

照片来自:https://blog.csdn.net/shinecjj/article/details/52075499 

特殊的擦除:桥方法

举个例子:

class Datelnterval extends Pair <LocalDate> {
    public void setSecond() {
        if ( second.compareTo(getFirst()) >= 0 )
        super.setSecond(second);
    }
}

它的raw type是:

class Datelnterval extends Pair // after erasure
{
    public void setSecond ( LocalDate second ) { . . . }
}

但是下面这种情况:

Datelnterval interval = new Datelnterval ( . . . ) ;
Pair <LocalDate> pair = interval; / / OK assignment to superclass,raw type也是它
pair.setSecond(aDate) ;

编译器在类中生成一个桥方法 ( bridge method ) :
public void setSecond ( Object second ) { setSecond ( ( Date ) second ) ; 

但是桥方法可能会变得十分奇怪。 假设方法也覆盖了 getSecond 方法 :

class Datelnterval extends Pair <LocalDate>{
    public Local Date getSecond() { return (Date)super.getSecond().clone(); 
}

在 Datelnterval 类中, 有两个 getSecond 方法 :

  • Local Date getSecond ( ) / / defined in Datelnterval
  • Object getSecond 0 // overrides the method defined in Pair to call the first method

不能这样编写 Java 代码( 在这里, 具有相同参数类型的两个方法是不合法的 ) 。它们都没有参数。 但是 , 在虚拟机中 , 用参数类型和返回类型确定一个方法 。 因此 ,编译器可能产生两个仅返回类型不同的方法字节码, 虚拟机能够正确地处理这一情况 。

泛型的限制

  • 不能用基本类型实例化类型参数。如Class<int>, Class<double>是不允许的,只能Class<Integer>, Class<Double>
  • 运行时类型查询只适用于原始类型。如if ( a instanceof Class<String> ) 是不允许的。
  • 不能创建参数化类型的数组。如Pair<String>[] table = new Pair<String> [10]; 也是不允许的。

这有什么问题呢 ? 擦除之后 , table 的类型是 Pair [ ]。 可以把它转换为 Object[]:
Object [] objarray = table ;
数组会记住它的元素类型 , 如果试图存储其他类型的元素 , 就会抛出一个 ArrayStoreException 异常 :
obj array [ 0 ] = " Hello " ; // Error component type is Pair

可以声明通配类型的数组 , 然后进行类型转换 :
Pair <String>[] table = (Pair<String>[]) new Pair<?>[10] 

结果将是不安全的。如果在table[0] 中存储一个Pair<Employee>, 然后对 table [0].getFirst ( ) 调用一个 String 方法 , 会得到一个 ClassCastException 异常。

 如果需要收集参数化类型对象 , 只有一种安全而有效的方法 : 使用 ArrayList : ArrayList <Pair<String>> 

  • Varargs 警告

考虑下面这个简单的方法, 它的参数个数是可变的 :

public static <T> void addAll(Collections coll , T . . . ts ){
    for (t : ts) coll.add(t);
}

应该记得, 实际上参数ts 是一个数组 , 包含提供的所有实参 。现在考虑以下调用 :

Col 1 ection <Pair <String>> table = . . . ;
Pair<String> pairl = . . . ;
Pair<String> pair2 = . .
addAll(table , pairl , pair2);

为了调用这个方法, Java 虚拟机必须建立一个Pair<String>数组 ,这就违反了前面的规则。 不过 ,对于这种情况 , 规则有所放松 , 你只会得到一个警告 , 而不是错误 。
可以采用两种方法来抑制这个警告。 一种方法是为包含addAll 调用的方法增加注解 @SuppressWamings ( " unchecked " )。或者在 Java SE 7 中,还 可 以 用 @ SafeVarargs 直 接 标 注addAll 方法 :

@SafeVarargs
public static < T > void addAll ( Collection < T > coll , T ... ts )

现在就可以提供泛型类型来调用这个方法了。 对于只需要读取参数数组元素的所有方法, 都可以使用这个注解 , 这仅限于最常见的用例 。

但是这个时候要异常小心,否则会在别处得到一个异常 。

  • 不能实例化类型变置。如public Pair ( ) { first = new T () ; second = new T ( ) ; } 是不允许的。

 在 Java SE 8 之后 ,最好的解决办法是让调用者提供一个构造器表达式。 例如 :
Pair < String > p = Pair.makePair(String : : new) ;
makePair 方法接收一个 Supplier < T >, 这是一个函数式接口 , 表示一个无参数而且返回类型为 T 的函数 :

public static <T> Pair <T> makePair(Supplier <T> constr){
    return new Pair <>(constr.get());
}

比较传统的解决方法是通过反射调用 Class.newlnstance 方法来构造泛型对象。

public static <T> Pair <T> makePair (Class <T> cl) {
    try { return new Pair<>(cl.newInstance()); }
    catch ( Exception ex ) { return null; }
}
  • 不能构造泛型数组
  • 泛型类的静态上下文中类型变量无效
  • 不能抛出或捕获泛型类的实例
  • 可以消除对受查异常的检查

通过使用泛型类 、 擦除和 @ SuppressWamings 注解 , 就能消除 Java 类型系统的部分基本限制。

总之

需要记住有关
Java 泛型转换的事实 :
• 虚拟机中没有泛型 , 只有普通的类和方法 。
• 所有的类型参数都用它们的限定类型替换 。
• 桥方法被合成来保持多态 。
• 为保持类型安全性 , 必要时插人强制类型转换 。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值