重拾Java基础知识:泛型

前言

多态是一种面向对象思想的泛化机制。你可以将方法的参数类型设为基类,这样的方法就可以接受任何派生类作为参数,包括暂时还不存在的类。这样的方法更通用,应用范围更广。拘泥于单一的继承体系太过局限,因为只有继承体系中的对象才能适用基类作为参数的方法中。即便是接口也还是有诸多限制。一旦指定了接口,它就要求你的代码必须使用特定的接口。而我们希望编写更通用的代码,能够适用“非特定的类型”,而不是一个具体的接口或类。

这就是泛型的概念,是 Java 5 的重大变化之一。泛型实现了参数化类型,这样你编写的组件(通常是集合)可以适用于多种类型。“泛型”这个术语的含义是“适用于很多类型”。编程语言中泛型出现的初衷是通过解耦类或方法与所使用的类型之间的约束,使得类或方法具备最宽泛的表达力。随后你会发现 Java 中泛型的实现并没有那么“泛”,你可能会质疑“泛型”这个词是否合适用来描述这一功能。

如果你从未接触过参数化类型机制,你会发现泛型对 Java 语言确实是个很有益的补充。在你实例化一个类型参数时,编译器会负责转型并确保类型的正确性。这是一大进步。

然而,如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。使用别人创建好的泛型相对容易,但是创建自己的泛型时,就会遇到很多意料之外的麻烦。这并不是说 Java 泛型毫无用处。在很多情况下,它可以使代码更直接更优雅。

简单泛型

促成泛型出现的最主要的动机之一是为了创建集合类,集合用于存放要使用到的对象。数组也是如此,不过集合比数组更加灵活,功能更丰富。几乎所有程序在运行过程中都会涉及到一组对象,因此集合是可复用性最高的类库之一。一个集合中存储多种不同类型的对象的情况很少见,通常而言,我们只会用集合存储同一种类型的对象。泛型的主要目的之一就是用来约定集合要存储什么类型的对象,并且通过编译器确保规约得以满足。

Java 5 之前,我们可以让这个类直接持有 Object 类型的对象:

public class Hero {
    private Object name;

    public Hero(Object name) {
        this.name = name;
    }

    public Object getName() {
        return name;
    }

    public void setName(Object name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Hero hero = new Hero(new Integer(1));
        Hero hero1 = new Hero(new String());
    }
}

name字段可以持有任何类型的对象,因此,与其使用 Object ,我们更希望先指定一个类型占位符,稍后再决定具体使用什么类型。要达到这个目的,需要使用类型参数,用尖括号括住,放在类名后面。然后在使用这个类时,再用实际的类型替换此类型参数。在下面的例子中,T 就是类型参数:

public class Hero<T> {
    private T name;

    public T getName() {
        return name;
    }

    public void setName(T name) {
        this.name = name;
    }

    public static void main(String[] args) {
        Hero<Integer> hero = new Hero();
        hero.setName(new Integer(1));
        Integer name = hero.getName();
    }
}

创建 Hero对象时,必须指明要持有的对象的类型,将其置于尖括号内,就像 main() 中那样使用。然后,你就只能在 Hero中存储该类型(或其子类,因为多态与泛型不冲突)的对象了。当你调用 get() 取值时,直接就是正确的类型。

这就是 Java 泛型的核心概念:你只需告诉编译器要使用什么类型,剩下的细节交给它来处理。

一个元组类库

有时一个方法需要能返回多个对象。而 return 语句只能返回单个对象,解决方法就是创建一个对象,用它打包想要返回的多个对象。当然,可以在每次需要的时候,专门创建一个类来完成这样的工作。但是有了泛型,我们就可以一劳永逸。同时,还获得了编译时的类型安全。

这个概念称为元组,它是将一组对象直接打包存储于单一对象中。可以从该对象读取其中的元素,但不允许向其中存储新对象(这个概念也称为 数据传输对象 或 信使 )。通常,元组可以具有任意长度,元组中的对象可以是不同类型的。我们可以利用继承机制实现长度更长的元组。

public class Hero<A,B> {
    public final A a1;
    public final B b1;
    public Hero(A a, B b) { a1 = a; b1 = b; }
    public String rep() { return a1 + ", " + b1; }
}
public class Hero2<A,B,C> extends Hero {
    public final C c1;

    public Hero2(A a, B b, C c1) {
        super(a, b);
        this.c1 = c1;
    }

    @Override
    public String rep() {
        return super.rep()+","+c1;
    }

    public static void main(String[] args) {
        Hero2<String, Integer, Boolean> hero2 = new Hero2<>("hello", 1, true);
        System.out.println(hero2.rep());
        /** Output:
         *  hello, 1,true
         */
    }
}

泛型接口

泛型也可以应用于接口。例如 生成器,这是一种专门负责创建对象的类。实际上,这是 工厂方法 设计模式的一种应用。不过,当使用生成器创建新的对象时,它不需要任何参数,而工厂方法一般需要参数。生成器无需额外的信息就知道如何创建新对象。

public interface Generator<T> {
    T getData();
}
public class GeneratorImpl implements Generator<String> {
    @Override
    public String getData() {
        return "hello";
    }

    public static void main(String[] args) {
//        Generator<Integer> generator = new GeneratorImpl(); 类型错误
        Generator<String> generator = new GeneratorImpl();
        String data = generator.getData();
        System.out.println(data);
        /** Output: 
         *  hello
         */
    }
}

T可以传入无数个实参,形成无数种类型的Generator接口。在实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型。

泛型方法

类本身可能是泛型的,也可能不是,不过这与它的方法是否是泛型的并没有什么关系。泛型方法独立于类而改变方法。作为准则,请“尽可能”使用泛型方法。通常将单个方法泛型化要比将整个类泛型化更清晰易懂。

public class Generator<T> {
    public T getData(T t){
        System.out.println(t.getClass().getName());
        return t;
    }

    public static void main(String[] args) {
        Generator generator = new Generator();
        generator.getData("a");
        generator.getData(1);
        generator.getData(1.2);
        generator.getData(true);
        /** Output: 
         *  java.lang.String
         *  java.lang.Integer
         *  java.lang.Double
         *  java.lang.Boolean
         */
    }
}

变长参数和泛型方法

public class Generator<T> {
    public List<T> getData(T...t){
        return Arrays.asList(t);
    }

    public static void main(String[] args) {
        Generator generator = new Generator();
        List list = generator.getData("a", 1, 1.2, true);
        System.out.println(list);
        /** Output:
         *  [a, 1, 1.2, true]
         */
    }
}

静态方法与泛型

如果方法是 static 的,则无法访问该类的泛型类型参数,因此,如果使用了泛型类型参数,则它必须是泛型方法。

public class Generator<T> {
    /**
     * 'com.study.test.Generator.this' cannot be referenced from a static context
     * @param args
     */
//    public static void getData(T t){
//    }

    public static <T> void getData(T t){
        
    }

}

泛型擦除

当你开始更深入地钻研泛型时,会发现有大量的东西初看起来是没有意义的。考虑下面的情况:

public class Generator {
    public static void main(String[] args) {
        ArrayList<String> strArray = new ArrayList<>();
        ArrayList<Integer> intArray = new ArrayList<>();
        System.out.println(strArray.getClass() == intArray.getClass());
        /** Output:
         *  true
         */
    }
}

我们定义了两个不同类型的ArrayList,通过getClass()方法获取它们的类信息并比较,发现结果为true。因为,在编译期间,所有的泛型信息都会被擦除,List<Integer>和List<String>类型,在编译后都会变成List类型(原始类型),普通的类型变量在未指定边界的情况下会被擦除为 ObjectJava中的泛型基本上都是在编译器这个层次来实现的,这也是Java的泛型被称为“伪泛型”的原因。

擦除的核心动机是你可以在泛化的客户端上使用非泛型的类库,反之亦然。这经常被称为“迁移兼容性”。在理想情况下,所有事物将在指定的某天被泛化。因此 Java 泛型不仅必须支持向后兼容性——现有的代码和类文件仍然合法,继续保持之前的含义——而且还必须支持迁移兼容性,使得类库能按照它们自己的步调变为泛型,当某个类库变为泛型时,不会破坏依赖于它的代码和应用。擦除使得这种向泛型的迁移成为可能,允许非泛型的代码和泛型代码共存。

通配符

类型通配符一般是使用 ? 代替具体的类型实参,注意了,此处 ? 是类型实参,而不是类型形参 。可以解决当具体类型不确定的时候,这个通配符就是 ? ;当操作类型时,不需要使用类型的具体功能时,只使用Object类中的功能。那么可以用 ? 通配符来表未知类型。

public class Generator<T> {
    public static void test(List<?> list){
        for (Object o:list){
            System.out.print(o);
        }
    }

    public static void main(String[] args) {
        test(Arrays.asList("a","b","c"));
        test(Arrays.asList(1,2,3));
        /** Output:
         *  abc123
         */
    }
}

泛型中通配符

我们在定义泛型类,泛型方法,泛型接口的时候经常会碰见很多不同的通配符,比如 T,E,K,V 等等,这些通配符又都是什么意思呢?

字母含义
?表示不确定的java类型
T(type)表示具体的Java类型,指类的的泛型类型
K(key)表示Java中的 主键
V(value)java中主键对应的值
E(element)代表Element元素,指集合里面元素的类型

其实这个几个并没有什么区别,它们都是JAVA泛型通配符,之所有会经常见到这些通配符是因为代码的约定俗成,增加代码的可阅读性,也就是说换成A-Z 之间的字母也都是可以的。

边界

边界允许我们对泛型使用的参数类型施加约束。尽管这可以强制执行有关应用了泛型类型的规则,但潜在的更重要的效果是我们可以在绑定的类型中调用方法。

上边界

由于擦除会删除类型信息,因此唯一可用于无限制泛型参数的方法是那些 Object 可用的方法。但是,如果将该参数限制为某类型的子集,则可以调用该子集中的方法。为了应用约束,Java 泛型使用了 extends 关键字。

public class A {}
public class B extends A{}
public class C extends A {}

public class Generator<T> {
    public static void test(List<? extends A> list){
    }

    public static void main(String[] args) {
        List<B> list = new ArrayList<>();
        test(list);
        List<C> list2 = new ArrayList<>();
        test(list2);
//        List<D> list3 = new ArrayList<>();
//        test(list3); 编译错误
    }
}

泛型类也可以这样操作

public class A {}
public class B extends A{}
public class C extends A {}

public class Generator<T extends A> {

    public static void main(String[] args) {
        Generator<B> generator = new Generator<>();
        //编译报错:非继承关系
//        Generator<D> generator1 = new Generator<D>();
    }
}

下边界

还可以走另外一条路,即使用超类型通配符。这里,可以声明通配符是由某个特定类的任何基类来界定的,方法是指定 <?super Class> ,或者甚至使用类型参数: <?super T>(尽管你不能对泛型参数给出一个超类型边界;即不能声明 )。这使得你可以安全地传递一个类型对象到泛型类型中。

public class A {}
public class B extends A{}
public class C extends A {}

public class Generator<T> {
    public static void test(List<? super B> list){
    }

    public static void main(String[] args) {
        List<A> list = new ArrayList<>();
        list.add(new A());
        test(list);
    }
}

无界通配符

无界通配符 看起来意味着“任何事物”,因此使用无界通配符好像等价于使用原生类型。事实上,编译器初看起来是支持这种判断的:

public class Generator<T> {
    public static void test(List list){
    }

    public static void main(String[] args) {
        List<A> list = new ArrayList<>();
        test(list);
        List<String> list1 = new ArrayList<>();
        test(list1);
    }
}

有很多情况都和你在这里看到的情况类似,即编译器很少关心使用的是否原生类型。当你在处理多个泛型参数时,有时允许一个参数可以是任何类型,同时为其他参数确定某种特定类型的这种能力会显得很重要。

潜在类型机制

潜在类型机制是一种代码组织和复用机制。有了它,编写出的代码相对于没有它编写出的代码,能够更容易地复用。代码组织和复用是所有计算机编程的基本手段:编写一次,多次使用,并在一个位置保存代码。因为我并未被要求去命名我的代码要操作于其上的确切接口,所以,有了潜在类型机制,我就可以编写更少的代码,并更容易地将其应用于多个地方。

Java中的直接潜在类型

因为泛型是在后期才添加到 Java 中,因此没有任何机会可以去实现任何类型的潜在类型机制,因此 Java 没有对这种特性的支持。所以,初看起来,Java 的泛型机制比支持潜在类型机制的语言更“缺乏泛化性”。(使用擦除来实现 Java 泛型的实现有时称为第二类泛型类型)。

对缺乏潜在类型机制的补偿

尽管 Java 不直接支持潜在类型机制,但是这并不意味着泛型代码不能在不同的类型层次结构之间应用。也就是说,我们仍旧可以创建真正的泛型代码,但是这需要付出一些额外的努力。

反射
public interface A {
    void test();
    void test2();
}
public class B implements A{
    @Override
    public void test() {
        System.out.println("B test()");
    }

    @Override
    public void test2() {
        System.out.println("B test2()");
    }
}
public class C implements A {
    @Override
    public void test() {
        System.out.println("C test()");
    }

    @Override
    public void test2() {
        System.out.println("C test2()");
    }
}
public class Generator<T> {

    public void perform(T t) throws Exception {
        t.getClass().getMethod("test",null).invoke(t);
        t.getClass().getMethod("test2",null).invoke(t);
    }

    public static void main(String[] args) throws Exception {
        Generator generator = new Generator<>();
        generator.perform(new B());
        generator.perform(new C());
        /** Output:
         *  B test()
         *  B test2()
         *  C test()
         *  C test2()
         */
    }
}

通过反射,能够动态地确定所需要的方法是否可用并调用它们。泛型信息在编译之后被擦除了,只保留了原始类型,类型变量(T)被替换为Object,在运行时,我们可以行其中插入任意类型的对象。Java中的泛型基本上都是在编译器这个层次来实现的“伪泛型”。并不推荐以这种方式操作泛型类型,因为这违背了泛型的初衷(减少强制类型转换以及确保类型安全)。

本章小结

使用泛型类型机制的最吸引人的地方,就是在使用集合类的地方,这些类包括诸如各种 List 、各种 Set 、各种 Map 等。 Java 5 之前,当你将一个对象放置到集合中时,这个对象就会被向上转型为 Object ,因此你会丢失类型信息。当你想要将这个对象从集合中取回,用它去执行某些操作时,必须将其向下转型回正确的类型。但是,泛型出现之前的 Java 并不会让你误用放入到集合中的对象。

还要注意到,因为泛型是后来添加到 Java 中,而不是从一开始就设计到这种语言中的,所以某些容器无法达到它们应该具备的健壮性。例如,观察一下 Map ,在特定的方法 containsKey(Object key)get(Object key) 中就包含这类情况。如果这些类是使用在它们之前就存在的泛型设计的,那么这些方法将会使用参数化类型而不是 Object ,因此也就可以提供这些泛型假设会提供的编译期检查。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值