【Generic】详解 Java 中的泛型(下)

泛型有三种使用方式,分别为:泛型类、泛型接口、泛型方法

1. 泛型类

泛型类型用于类的定义中,被称为泛型类。
通过泛型可以完成对一组类的操作对外开放相同的接口。最典型的就是各种容器类,如:List、Set、Map。

定义一个泛型类:

class 类名 <泛型标识> {
	private 泛型标识 var;
}

泛型标识:可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型。在实例化泛型类时,必须指定 T 的具体类型

public class GenericType<T> {
	
    private String name;
	// 可使用泛型修饰成员变量
    private T key;

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

    public T getKey() {
        return key;
    }

    public static void main(String[] args) {
        GenericType<Integer> type = new GenericType<>(666);

        Integer key = type.getKey();
        System.out.println(key);
    }
}

使用泛型时,注意两点:

  1. 泛型的类型参数只能是引用类型
  2. 传入的实参类型需与泛型的类型参数类型相同

定义了泛型类,就一定要传入泛型实参吗?

其实并非如此:在使用泛型的时候,如果传入泛型实参,则会根据传入的泛型实参的类型做相应的限制,此时,泛型才会起到本应起到的限制作用;如果不传入泛型类型实参的话,在泛型类中使用泛型的方法或成员变量定义的类型可以为任何的类型。

如下代码:

public static void main(String[] args) {
    // 整型
    GenericType type = new GenericType(666);
    // 字符串
    GenericType type2 = new GenericType("zzc");
}

2. 泛型接口

泛型接口与泛型类的定义及使用基本相同

定义一个泛型接口:

public interface Generator<T> {
    T next();
}

当实现泛型接口的类,未传入泛型实参时:

public class GeneratorTest<T> implements Generator<T> {

    @Override
    public T next() {
        return null;
    }
}

未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中

当实现泛型接口的类,传入泛型实参时:

public class GeneratorTest implements Generator<String> {

    @Override
    public String next() {
        return null;
    }
}

在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型

3. 泛型方法

什么是泛型方法?

位于 public方法的返回值 之间的 <T> 非常重要,只有声明了 <T> 的方法才是泛型方法;在泛型类中使用了泛型的方法并不是泛型方法。如下代码:

public class GenericTestThree {
	
	// 泛型方法
    public <T> T genericMethod(Class<T> clazz) throws Exception{
        T instance = clazz.newInstance();
        return instance;
    }

    public Object getInstance() throws Exception{
        GenericTestThree genericTestThree = genericMethod(GenericTestThree.class);
        return genericTestThree;
    }
}

genericMethod() 方法是泛型方法

<T> 表明该方法将使用泛型类型 T,此时,才可以在方法中使用泛型类型 T


泛型方法总结:

public class GenericType<T> {

    private T key;
	
	/**
	 * 这只是类中一个普通的成员方法,只不过他的返回值是在声明泛型类时已经声明过的泛型
	 * 所以在这个方法中才可以继续使用 T 这个泛型
	 */
    public T getKey() {
        return key;
    }
	
	/**
	 * 这个方法显然是有问题的。 因为在类的声明中并未声明泛型 E,
	 * 所以在使用 E 做形参和返回值类型时,编译器会无法识别
	 *
    public E setKey(E key){
         this.key = keu
    }
    */
    
    /**
     * 泛型方法。声明了两个泛型变量 E、K
     * 它们可以出现在这个泛型方法的任意位置
     */
    public <E, K> K print(GenericType<E> genericType) {
        return null;
    }
	
	/**
	 * 泛型方法。使用泛型E,这种泛型E可以为任意类型。可以类型与T相同,也可以不同
	 * 由于泛型方法在声明的时候会声明泛型<E>,因此即使在泛型类中并未声明泛型,编译器也能够正确识别泛型方法中识别的泛型
	 */
	public <E> void print_2(E t){
        return;
    }
	
	/**
	 * 泛型方法。使用泛型T,注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型
	 */
	public <T> void print_3(T t){
        System.out.println(t.toString());
    }
}

泛型方法与可变参数:

在泛型方法中使用可变参数:

public <T> void printMsg( T... args){
    for(T t : args){
        // ...
    }
}

4. 泛型与静态方法/属性

静态方法有一种情况需要注意一下,那就是在类中的静态方法使用泛型:静态方法无法访问类上定义的泛型;如果静态方法操作的引用数据类型不确定的时候,必须要将泛型定义在方法上。

即:如果静态方法要使用泛型的话,必须将静态方法也定义成泛型方法 。

代码如下:

public class GenericType<T> {
	
	// 编译报错
    //private static T key;
	
	/**
	 * 静态方法。编译报错
	public static void show_static(T t) {
        return;
    }
    */
	
	// 静态的泛型方法
    public static <T> void show_static(T t) {
        return;
    }
}

泛型类中的静态方法和静态变量不可以使用泛型类所声明的泛型类型参数

在 Java 中,泛型只是一个占位符,必须在传递类型后才能使用就泛型而言,类实例化时才能正真的的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数静态的方法就已经加载完成了。对象都没有创建,如何确定这个泛型参数是何种类型,所以当然是错误的。

5. 泛型通配符

5.1 ? 无界通配符

我们知道 IngeterNumber的一个子类,同时之前也讲过 GenericType<Ingeter>GenericType<Number>实际上是相同的一种基本类型。那么问题来了,在使用 GenGenericTyperic<Number> 作为形参的方法中,能否使用GenericType<Ingeter> 的实例传入呢?即:GenericType<Number>GenericType<Ingeter> 是否可以看成具有父子关系的泛型类型呢?

我们使用如下代码进行验证下吧:

public class GenericTest {

    public void test(GenericType<Number> genericType) {
        return;
    }

    public static void main(String[] args) {
        GenericTest genericTest = new GenericTest();

        GenericType<Integer> integer = new GenericType<>();
        GenericType<Number> number = new GenericType<>();
		// 编译报错
        //genericTest.test(integer);
        genericTest.test(number);
    }
}

调用 test(integer); 方法时,发现会编译报错。由此可以看出:同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。

那么如何解决上面的问题?总不能为了定义一个新的方法来处理 Generic<Integer> 类型的类,这显然与Java 中的多态理念相违背。因此我们需要一个在逻辑上可以表示同时是 Generic<Integer>Generic<Number>父类的引用类型。由此类型通配符应运而生。

我们使用 表示类型通配符。? 表示的意思是:不确定的 Java 类型。默认是允许 Object 及其下的任何 Java 类了。也就是任意类。

使用 ? 进行改造,代码如下:

publicvoid test(GenericType<?> genericType) {
    return;
}

只是修改了下 test() 方法,发现就好使了。

所以,对于不确定或者不关心实际要操作的类型,可以使用无限制通配符(尖括号里一个问号,即 ),表示可以持有任何类型。所以,这就是为什么要使用通配符 ? 而不是泛型 T 了。

5.2 上界通配符 < ? extends E>

上界:用 extends 关键字声明,表示参数化的类型可能是所指定的类型,或者是此类型的子类

public class GenericTest {
    
    public void test(List<? extends Number> list) {
        return;
    }

    public static void main(String[] args) {
        GenericTest genericTest = new GenericTest();
        
        List<Number> numbers = new ArrayList<>();
        List<Integer> integers = new ArrayList<>();
        List<String> strings = new ArrayList<>();

        genericTest.test(numbers);
        genericTest.test(integers);
        // 编译报错
        //genericTest.test(strings);
    }
}

5.4 下界通配符 < ? super E>

下界: 用 super 进行声明,表示参数化的类型可能是所指定的类型,或者是此类型的父类型,直至 Object

上界通配符主要用于读数据,下界通配符主要用于写数据。

public class GenericTest {

    public static void main(String[] args) {
        List<? extends SecondSon> numbers = new ArrayList<>();
        List<? super SecondSon> superNumbers = new ArrayList<>();

        numbers.get(0);
        // 编译报错
        //numbers.add(new SecondSon());

        superNumbers.add(new SecondSon());
        superNumbers.add(new String());
    }
}

class Father {}

class SecondSon extends Father {}

class ThirdSon extends SecondSon{}

我们知道 List 泛型类中存着 Number 的某一个子类的类型,但是到底是什么类型,我们就不得而知了。这时候,我们是可以读取 List 当中的内容的,因为再怎么不知道它到底是什么类型的数据,也知道他至少是一个 Number 的对象嘛。但是写呢,写就不能那么随意了。不知道 List 中到底存的是什么类型的数据,如果还支持他的写入,那显然是不安全的。

这么说来,难道,用了泛型通配符,我们就从此不能写入了吗?人家 Java 的设计者也是考虑到了这一点,所以就有了 ? super T 这种用法。这种用法也不是完全没有限制的。他支持对泛型类写入 T 或者是 T的子类,不支持写入 T 的父类。

简单说来就是,如果希望支持读,就用 ? extends T,希望支持写,就用 ? super T

5.5 ?和 T 的区别

T 是一个 确定的 类型,通常用于泛型类和泛型方法的定义;?是一个 不确定 的类型,通常用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。

区别一:通过 T 来 确保 泛型参数的一致性

// 通过 T 来 确保 泛型参数的一致性
public <T extends Number> void
test(List<T> dest, List<T> src)

//通配符是 不确定的,所以这个方法不能保证两个 List 具有相同的元素类型
public void
test(List<? extends Number> dest, List<? extends Number> src)

区别二:类型参数可以多重限定而通配符不行

public class Test {
	public static <T extends A & B> void test(T t) {}
}

interface A {}
interface B {}

使用 & 符号设定多重边界(Multi Bounds),指定泛型类型 T 必须是 A 和 B 的共有子类型,此时变量 t 就具有了所有限定的方法和属性。对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定。

区别三:通配符可以使用超类限定而类型参数不行
类型参数 T 只具有 一种 类型限定方式:

T extends A

但是通配符 ? 可以进行 两种限定:

? extends A
? super A

5.6 Class<T>Class<?> 的区别

看看如下代码:

public class GenericTest {

    public static void main(String[] args) throws Exception{
        Class aClass = Class.forName("com.tiandy.zzc.generic.GenericTest");
        GenericTest genericTest = (GenericTest)aClass.newInstance();
    }
}

对于上述代码,在运行期,如果反射的类型不是 GenericTest 类,那么一定会报 java.lang.ClassCastException 错误

对于这种情况,则可以使用下面的代码来代替,使得在在编译期就能直接 检查到类型的问题:

public class GenericTest {

	// 使用泛型进行约束
    public static <T> T createInstance(Class<T> clazz) throws Exception{
        return clazz.newInstance();
    }

    public static void main(String[] args) throws Exception{
        GenericTest instance = GenericTest.createInstance(GenericTest.class);
    }
}

Class<T>在实例化的时候,T 要替换成具体类。Class<?>它是个通配泛型,? 可以代表任何类型,所以主要用于声明时的限制情况

public class Test {
	// 可以
	public Class<?> clazz;
	// 不可以,因为 T 需要指定类型
	public Class<T> clazzT;
}

那如果也想 public Class clazzT; 这样的话,就必须让当前的类也指定 T :

public class Test<T> {
	// 可以
	public Class<?> clazz;
	// 可以
	public Class<T> clazzT;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值