关于程序员:讲讲Java的泛型

10次阅读

共计 5160 个字符,预计需要花费 13 分钟才能阅读完成。

在 Java 没有泛型之前,只能应用 Object 变量并在须要的中央应用强制类型转换,但这样会使得代码没有安全性和可读性。因而,Java 5 开始反对泛型。应用泛型编写的代码能够被很多不同类型的对象所重用。

定义泛型

泛型的实质是指参数化类型。所谓参数化类型,是指用来申明数据的类型自身,也是能够扭转的,它由理论参数来决定。

泛型:把类型明确的工作推延到创建对象或调用办法的时候才去明确的非凡的类型。

泛型类

上面定义一个简略的泛型。

public class Holder<T> {
    private T a;
    public GenericHolder() {}
    public void set(T a) {this.a = a;}
    public T get() { return a;}

    public static void main(String[] args) {Holder<Integer> v = new Holder<Integer>();
        v.set(1024); // 此处有类型校验
        Integer a = v.get();  // 无需类型转换}
}

下面创立 Holder 实例时,要明确持有对象的类型,而后,Holder 实例就只能存储该类型或其子类的值。当调用 get() 取值获取的是该类型的值。

类定义中的类型变量是指办法的返回类型以及域和局部变量的类型。

Holder<Integer> v = new Holder<Integer>() 是 Java 5 中的写法,Java 7 中写法为 Holder<Integer> v = new Holder<>(),称为“钻石语法”。

编写泛型时,须要定义泛型类型 <T>;静态方法不能引用泛型类型 <T>,必须定义其它类型(例如 <K>)来实现动态泛型办法;泛型能够同时定义多种类型,例如:Map<K,V>

留神:类型变量应用大写模式,且比拟短,这是很常见的。在 Java 库中,应用变量 E 示意汇合的元素类型,K 和 V 别离示意表的关键字与值的类型。T(须要时还能够应用邻近的字母 U 和 S)示意“任意类型”。

泛型办法

上面定义一个带有类型参数的泛型办法。泛型处在办法的修饰符及其返回值之间。

public static <E> Set<E> union(Set<E> s1, Set<E> s2) {Set<E> result = new HashSet<>(s1);
    result.addAll(s2);
    return result;
}

下面的静态方法独立于类而扭转办法,这样形式要比将类泛型化更清晰易懂。

对于泛型类,必须在实例化该类时指定类型参数。应用泛型办法时,通常不须要指定参数类型,因为编译器会找出这些类型。这称为 类型参数推断

如果要在参数中应用可变参数时,会创立一个数组用来寄存;这个数组是一个实现细节,是可见的。因而,当可变参数有泛型或者参数化类型时,编译正告信息就会产生凌乱。

在之前,应用 SuppressWarnings("uncheck") 注解来打消正告。从 Java 7 开始,减少了 @SafeVarargs 注解,用来承诺申明的办法是类型平安的,并打消正告。

 @SafeVarargs
public static <T> List<T> makeList(T... args) {List<T> result = new ArrayList<>();
    for (T item : args) {result.add(item);
    }
    return result;
}

类型限定

当对一个数组进行排序时,能够实现一个排序办法:

public class Sort implements Serializable {public void sort(T[] source) {for (int i = 1; i < source.length; i++) {for (int j = 0; j < source.length - 1; j++) {if (source[j+1].compareTo(source[j]) < 0) {T t = source[j];
                    source[j] = source[j+1];
                    source[j+1] = t;
                }
            }
        }
    }
}

但这里的 source 是应用泛型 T[] 示意的类型数组,当没有限定类型而执行类型擦除后,泛型数组 T[] 会应用 Object[] 援用,而 Object 类也没有实现 Comparable 接口,调用不了 compareTo 办法。因而,类或办法须要对类型变量施加束缚,将 T 限度为实现了 Comparable 接口的类。如下所示:

public class Sort<T extends Comparable<T> & Serializable> implements Serializable {public void sort(T[] source) {for (int i = 1; i < source.length; i++) {for (int j = 0; j < source.length - 1; j++) {if (source[j+1].compareTo(source[j]) < 0) {T t = source[j];
                    source[j] = source[j+1];
                    source[j+1] = t;
                }
            }
        }
    }
}

<T extends BoundingType> 是一种类型限定的形式。

其中,BoundingType 为父类,而 T 示意为 BoundingType 类型的子类型。它的界线为 [T, BoundingType]TBoundingType 都在边界之内。一个类型变量能够有多个限定,且限定之间用 & 分隔。

<T extends Comparable & Serializable>

通配符类型

泛型类型用起来较为不便,但参数化类型是不变的,当在 Holder<T> 类中减少一个比拟办法。

public boolean toEquals(Holder<T> obj) {return this.get() == obj.get();}

当这种形式创立的对象进行比拟时只能比拟雷同的类型。

Holder<Integer> a = new Holder<>();
a.set(100);
Holder<Integer> b = new Holder<>();
b.set(200);
a.toEquals(b);  // 正确
Holder<Double> c = new Holder<>();
c.set(123.32);
a.toEquals(c);  // 谬误 

Java 提供了一种非凡的参数化类型,称作 有限度的通配符类型 ,它能够解决相似的状况。如下所示:

public boolean toEquals(Holder<? extends Number> obj) {return this.get() == obj.get();}

该办法的参数化类型为 Number 的子类。

a.toEquals(c);  // 正确 

因而,下面的办法就会正确无误地编译。

还能够指定一个超类型限定通配符,如下所示:

? super T

这种形式示意的是 T 或 T 的一个父类型。因而下面的办法换一种写法:

public boolean toEquals(Holder<? super Integer> obj) {return this.get() == obj.get();}

该办法的参数化类型为 Integer 的父类。即为 IntegerNumber 都能够,当与 Double 值比拟时会报错。

Holder<Integer> a = new Holder<>();
a.set(100);
Holder<Number> b = new Holder<>();
b.set(2003.02);
a.toEquals(b);  // 正确
Holder<Double> c = new Holder<>();
c.set(123.23);
a.toEquals(c);  // 报错 

还能够应用有限定的通配符,如 Holder<?>,它与 Holder<? extends Object> 是一样的。

因而,要想取得最大限度的灵活性,要在参数类型上应用通配符类型。

上面的助记符便于让你记住要应用哪种通配符类型:

PECS 示意 producer-extends,consumer-super:如果参数化类型示意一个生产者 T,就应用 <? extends T>;如果它示意一个消费者 T,就应用 <? super T>

泛型擦除

虚拟机没有泛型类型对象,所有对象都属于一般类。因而,定义的泛型最初都会擦除,并替换为限定类型,有限定类型的变量用 Object 替换。

public class Holder<T> {
    private T a;
    public Holder() {}
    public void set(T a) {this.a = a;}
    public T get() { return a;}
}

T 是一个有限定的变量,所以间接用 Object 替换。

如果给定限定,原始类型用第一个限定的类型变量来替换。

public class Holder<T extends Comparable & Serializable> implements Serializable {
    private T a;
    public Holder() {}
    public void set(T a) {this.a = a;}
    public T get() { return a;}
}

在执行类型擦除时,原始类型 Holder 如下所示:

public class Holder implements Serializable {
    private Comparable a;
    public Holder() {}
    public void set(Comparable a) {this.a = a;}
    public Comparable get() { return a;}
}

这也阐明,Java 提供的如 List 等汇合类,不论应用时设置的是 List<String> 还是 List<Integer>,在运行时都是雷同的类型,都会擦除成原生类型 List

模糊性谬误

泛型的引入,减少了模糊性谬误的可能。如泛型类 Holder<U, S> 中的两个泛型申明,在擦除后变成雷同的类型而导致的抵触,就会产生模糊性谬误。

public class Holder<U, S> {
    U var1;
    S var2;

    // 重载谬误
    public void set(U u) {var1 = u;}
    // 重载谬误
    public void set(S s) {var2 = s;}
}

当申明两个雷同的类型时,会导致 set() 办法的两个版本完全相同。而且,对 set() 办法的类型擦除会使两个版本都变为 set(Object o),也会导致谬误。

限度

根本类型

不能用类型参数代替根本类型。如 Holder<T> 中的泛型 T 只能是 Object 类及其子类,不能是 Java 中的根本类型。

类型查问

不能应用 instanceof 查问一个对象是否属于某个泛型类型。

if (a instanceof Holder<String>)  // Error

而且,getClass() 办法返回的也是原始类型。

Holder<String> s = new Holder<>();
s.getClass();  // class xx.xx.Holder
Holder<Integer> i = new Holder<>();
i.getClass();  // class xx.xx.Holder

类型变量

不能应用泛型来实例化变量。如应用 new T(...)new T[...]T.class 都是谬误的。泛型只是一个占位符。

泛型类的动态上下文中类型变量有效

动态成员不能应用在类中申明的类型参数,然而能够申明动态的泛型办法

public class Holder<T> {
    // 谬误
    private static T t;
    // 谬误
    public static T getT() {return t;}
    // 正确
    public static <K> void test(K k) {}}

泛型数组

不能实例化参数化类型的数组。如下所示:

 Holder<String>[] holders = new Holder<>[10];

下面的形式是不容许的,然而申明类型为 Holder<String>[] 的变量是非法的。

泛型类型的实例

泛型类型的实例既不能被抛出也不能捕捉。实际上,甚至泛型类扩大 Throwable 都是不非法的。

// Error:Generic class may not extend 'java.lang.Throwable'
public CustomException<E> extends Exception {}

catch 子句中不能应用类型变量。例如,如下办法将不能编译:

public static <T extends Throwable> void doWork(Class<T> t) {
    try {// do work} catch (T e) {// Error--can't catch type variable}
}

不过,在异样标准中应用类型变量是容许的。以下办法是非法的:

public static <T extends Throwable> void doWork(T t) {
    try {// do work} catch (Throwable e) {t.initCause(e);
        throw t;
    }
}

欢送关注公众号「海人为记」,期待与你共同进步!

正文完
 0