Java学习——泛型的介绍和基本使用

介绍

一般的类和方法,只能使用具体的类型,要么是基本类型,要么是自定义类型。如果要编写可以应用于多种类型的代码,就需要借助于多态和泛型实现。
多态勉强算是一种泛化机制,比如你可以将方法的参数类型设置为基类或者接口,那么该方法就可以接受从这个类中导出的任何类作为参数。这种方式使我们相当常见也是相当方便的。但是这种方式必须要求你的代码必须实现特定的接口或者某些基类,这样对程序的约束性太大了,不仅如此,如果我们为了追求较强的泛化性而选择Object这样的底层类型,那么在存入类型后必然会发生类型丢失,我们很容器在代码中对其进行错误的向下转型。因此我们如何实现一个更通用的代码使得代码能够应用于某种不具体的类型。因此再JavaSE5中引入了泛型的概念。

事实上,于C++相比,Java的泛型事实上一种伪泛型。它只存在于编译器。具体原理我们放在另外一章进行描述



泛型的使用


简单泛型

泛型的主要目的之一就是用来指定容器要持有什么类型的对象,而且由编译器来保证类型的正确性。

要使用泛型,需要使用类型参数,用尖括号<>括住,放在类名后面。然后在使用这个类的时候再用具体的类型替换此类型参数。我们可以这么认为,泛型中的类型参数相当于一个占位符。然后咋使用的时候我们需要更换占位符为实际类型。在下面的例子中,T就是类型参数。

class SimpleGeneric<T> {}

在下面例子中,由于我们指定了类型参数为String类型,因此我们从这个类中只能存入String类型,取出的也是String类型。

public class SimpleGeneric<T> {
    
    
    private T t;

    public T getT() {
    
    
        return t;
    }

    public void setT(T t) {
    
    
        this.t = t;
    }

    public static void main(String[] args) {
    
    
        SimpleGeneric<String> gs = new SimpleGeneric<>();
        gs.setT("hello");
        System.out.println(gs.getT());
    }
}

事实上,虽然看起来我们的SimpleGeneric<String>好像是一个新类,该被定义为一个持有的String类型的SimpleGeneric类,事实上,这只是一种表面,SimpleGeneric<String>与SimpleGeneric<Integer>依旧是同一个类,它们所传入的类型参数只是起到一个编译检查的作用,也就是说,我们传入了String是告诉编译器SimpleGeneric<String>只接受String类型的对象,如果是其他不符合的类型,编译无法通过。这就是java实现泛型的原理,我们叫做擦除,也就是说在编译时,所有正常的没有边界的泛型类型也就是我们只写了T的地方会被替换为Object,关于擦除的原理我们另外写一章


泛型接口

泛型也可以应用于接口

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

class Coffee{
    
    }

class CoffeeGenerator implements  Generator<Coffee>{
    
    

    @Override
    public Coffee next() {
    
    
        return new Coffee();
    }
}

泛型方法

到目前为止,我们看到的所有的泛型都是应用于整个类上的。我们可以看到泛型类中包含了泛型方法,但是拥有泛型方法的类不一定是泛型类。如果可以使用泛型方法取来泛型类的可能性,那就应该只用泛型方法。
对于一个static方法,无法访问泛型类的类型参数,所有如果static方法需要使用泛型参数,就需要成为泛型方法

定义一个泛型方法,只需要将泛型的类型参数列表放置在返回值前。

public class MethodGeneric {
    
    

    public <T> void f(T t){
    
    
        System.out.println(t.getClass().getSimpleName());
    }

    public static void main(String[] args) {
    
    
        MethodGeneric methodGeneric = new MethodGeneric();
        methodGeneric.f("111");
        methodGeneric.f(111);
    }
}

类型推断

通过上面的例子可以发现,泛型方法不需要通过指定类型参数来使用,在使用泛型方法时存在一个类型推断,但是注意,泛型方法的类型推断只在赋值时起作用,其他时候,如果没有显式指定类型参数的类型,那么一律按照Object来使用

在下面例子中1,2,3都是非赋值状态下的泛型方法调用,1,2由于没有指定类型参数,所以两个参数都被转化为了Object类型,所以编译通过,但在3中,我们显式指定了我们类型参数为Integer类型,但是我们传入的确是String类型,编译器认为类型不匹配,所以编译失败。

在4和5的例子中我们采用了赋值,因此存在类型推断,所以在5中当我们希望泛型方法赋值给String类型时,我们的编译器会将参数类型限制为String类型

public class MethodGeneric {
    
    

    public <T> void f(T t){
    
    
        System.out.println(t.getClass().getSimpleName());
    }

    public <T> T f2(T t){
    
    
        System.out.println(t.getClass().getSimpleName());
        return (T)t;
    }

/*    public <T> T f3(String object){
        System.out.println(object.getClass().getSimpleName());
        return (T)object;
    }*/

    public static void main(String[] args) {
    
    
        MethodGeneric methodGeneric = new MethodGeneric();
        methodGeneric.f("111");//1
        methodGeneric.f(111);//2
        methodGeneric.<Integer>f("111");//3编译错误

        String s1 = methodGeneric.f2("111");//4
        String s2 = methodGeneric.f2(111);//5编译错误
    }
}

注意:假设我们在方法参数中声明一个具体的类型(也就是说编译器无法通过类型推断来限制传入的类型,因为是Obejct所以什么类都可以安全的向上转型),同时我们的返回值确是T类型。那么即时我们显式传入了T的类型参数,编译器也没有办法正确的判断返回值与接收变量是否符合。

看下面的例子,我们在方法参数中声明一个Object类型,但在使用时传入了一个String,这是被认可的,因为String继承了Obejct,但是我们同时声明T是一个Integer类型,也就是说,该方法会将我们传入的String类型传化为Integer类型,但是这从逻辑上来说是错的,为什么编译正常。
因为编译器发现我们的方法形参为一个Object类型,也就是说我们传入的String类型进入方法后变成了一个Object类型,然后在返回时将其转化为一个Integer类型,这是合理的,java运行这样的向下转型。所以说下面的代码编译正常,但会有一个运行时的类型转换异常

public class MethodGeneric {
    
    

    public <T> T f(Object object){
    
    
        System.out.println(object.getClass().getSimpleName());
        return (T)object;
    }

    public static void main(String[] args) {
    
    
        MethodGeneric methodGeneric = new MethodGeneric();
        Integer s = methodGeneric.<Integer>f("111");
    }
}

猜你喜欢

转载自blog.csdn.net/qq_33905217/article/details/109237471