Java范型那些事(二)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/unicorn97/article/details/81837480

接上篇博文 《Java范型那些事(一)》

参考oracle官网对于范型的介绍 : https://docs.oracle.com/javase/tutorial/extra/generics/legacy.html

目录

6. 范型与遗留代码(未使用范型的代码)互操作

6.1 在泛型代码中使用遗留代码

6.2 类型擦除和转换

6.3 在遗留代码中使用范型代码

7. cast 和 instance of

7.1 范型数组

8. 使用类字面常量(class literals)作为运行时类型标记


扫描二维码关注公众号,回复: 2985294 查看本文章

6. 范型与遗留代码(未使用范型的代码)互操作

到目前为止,我们所有的例子都假设了一个理想化的世界,每个人都在使用最新版本的Java编程语言,它支持泛型。

唉,实际情况并非如此。 在早期版本的语言中已经编写了数百万行代码,并且它们不会在一夜之间全部转换。

稍后,在“将未使用范型的代码(下文简称遗留代码,或者旧代码)转换为使用泛型”部分中,我们将解决将旧代码转换为使用泛型的问题。 在本节中,我们将关注一个更简单的问题:遗留代码和泛型代码如何互操作? 这个问题有两个部分:在泛型代码中使用遗留代码和在遗留代码中使用泛型代码。

6.1 在泛型代码中使用遗留代码

你如何使用遗留代码,同时仍然在自己的代码中享受泛型的好处?

 举个例子:例如,假设您要使用com.Example.widgets包。 Example.com的人们推出了一个库存控制系统,其亮点如下所示

package com.Example.widgets;

public interface Part {...}

public class Inventory {
    /**
     *将新装配添加到库存数据库。
     *新装配名为name,由一个parts集合组成。
     *集合parts的所有元素都必须支持Part接口
     **/ 
    public static void addAssembly(String name, Collection parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // 返回一个Parts集合
    Collection getParts();
}

现在当你调用addAssembly()方法时,你肯定会传正确的参数,即一个Part对象的集合。而范型天生就是为了这种情况而生的。

package com.mycompany.inventory;

import com.Example.widgets.*;

public class Blade implements Part {
    ...
}

public class Guillotine implements Part {
}

public class Main {
    public static void main(String[] args) {
        Collection<Part> c = new ArrayList<Part>();
        c.add(new Guillotine()) ;
        c.add(new Blade());
        Inventory.addAssembly("thingee", c);
        Collection<Part> k = Inventory.getAssembly("thingee").getParts();
    }
}

大多数人的第一直觉是Collection真正意味着Collection <Object>。但是,正如我们之前看到的那样,在需要Collection <Object>的地方传递Collection <Part>是不安全的。更准确地说,类型Collection表示一些未知类型的集合,就像Collection <?>一样。

但等等,这也不是正确的!考虑调用getParts(),它返回一个Collection。然后将其分配给k,这是Collection <Part>。如果调用的结果是Collection <?>,则赋值将是错误。

实际上,分配是合法的,但它会生成未经检查的警告。而这个警告是必须要有的,因为事实是编译器无法保证其正确性。我们无法检查getAssembly()中的遗留代码,以确保返回的集合确实是Parts的集合。代码中使用的类型是Collection,可以合法地将所有类型的对象插入到这样的集合中。

那么,这不应该是一个错误吗?从理论上讲,是的;但实际上,如果范型代码要调用遗留代码,则必须允许这样做。由程序员来决定,在这种情况下,赋值是安全的,因为getAssembly()的契约表示它返回了一个Parts集合,即使类型签名没有显示这一点。

因此原始类型非常类似于通配符类型,但它们并不严格地进行类型检查。这是一个深思熟虑的设计决策,允许泛型与预先存在的遗留代码进行互操作。

从范型代码调用遗留代码本质上是危险的;一旦将范型代码与非泛型遗留代码混合,泛型类型系统通常提供的所有安全保证都是无效的。但是,你仍然比没有使用泛型更好。至少你知道你的代码是一致的。

目前还有更多非范型代码,然后是范型代码,并且不可避免地会出现需要混合的情况。

如果您发现必须混合使用旧代码和范型代码,请密切注意未经检查的警告。仔细考虑如何证明产生警告的代码的安全性。
如上面代码中编译器抛出的警告:

但是如果将Assembly接口中的getParts()方法的返回类型改为Collection<Part>,即将遗留的代码改成范型的形式,则可以消除该警告。

6.2 类型擦除和转换

先看以下一段代码,最后一行是 return ys.iterator().next();

在未经检查的调用add方法试图在原始类型的List中添加元素时,编译器报出警告;如果我们忽略警告并尝试执行此代码,它将在我们尝试使用错误类型时报错:

Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String

在运行时,此代码的行为类似于:

public String loophole(Integer x) {
    List ys = new LinkedList;
    List xs = ys;
    xs.add(x); 
    return(String) ys.iterator().next(); // run time error
}

发生上述的原因是,泛型是由Java编译器实现的一种称为擦除的前后转换。您(几乎)可以将其视为源到源的转换,其中loophole()的范型本将转换为非泛型版本。

因此,即使存在未经检查的警告,Java虚拟机的类型安全性和完整性也不会存在风险。

基本上,擦除消除(或擦除)所有泛型类型信息。抛出尖括号之间的所有类型信息,例如,像List <String>这样的参数化类型被转换为List。类型变量的所有剩余使用都由类型变量的上限(通常为Object)替换。并且,每当结果代码不是类型正确时,就会插入适当类型的强制转换,就像在loophole()的最后一行一样。

擦除的全部细节超出了本教程的范围,但我们刚才给出的简单描述与事实差不多。了解一下这一点很好,特别是如果你想做更复杂的事情,比如将现有的API转换为使用泛型(参见后续章节:使用泛型转换遗留代码),或者只是想了解为什么事情就是这样。

6.3 在遗留代码中使用范型代码

现在我们考虑一下相反的情况,即API端将其遗留代码改成使用范型,但是部分客户端还没有改成使用范型,修改后的API端代码:

package com.Example.widgets;

public interface Part { 
    ...
}

public class Inventory {
     /**
     *将新装配添加到库存数据库。
     *新装配名为name,由一个parts集合组成。
     *集合parts的所有元素都必须支持Part接口
     **/ 
    public static void addAssembly(String name, Collection<Part> parts) {...}
    public static Assembly getAssembly(String name) {...}
}

public interface Assembly {
    // 返回一个 Parts 集合
    Collection<Part> getParts();
}

客户端代码如下:

客户端代码是在引入泛型之前编写的,但它使用包com.Example.widgets和集合库,两者都使用泛型类型。 客户端代码中泛型类型声明的所有用法都是原始类型。

第1行生成未经检查的警告,因为原始Collection正在传递到期望Part类型集合的位置,并且编译器无法确保原始集合确实是Part集合。

作为替代方法,您可以使用source 1.4标志编译客户端代码,确保不会生成警告。 但是,在这种情况下,您将无法使用JDK 5.0中引入的任何新语言功能。

⚠️注意:所以当API(或者称为被调用端)改成范型形式时,客户端(或者称为调用端)也应该相应的改成范型形式,以避免警告的产生。

7. cast 和 instance of

泛型类在其所有实例之间共享这一事实的另一个含义是,询问实例它是否是泛型类型的特定调用的实例通常是没有意义的:

 下面的代码会报出未经检查的警告,因为在运行时,因为这不是运行时系统要检查的东西

运行时不存在类型变量。 这意味着它们在时间和空间上都不会产生性能开销,这很好。 不幸的是,这也意味着你无法在类型转换时可靠地使用它们。即下面的代码也会报未经检查的警告:

7.1 范型数组

oracle官网文档有如下一段描述:

The component type of an array object may not be a type variable or a parameterized type, unless it is an (unbounded) wildcard type.You can declare array types whose element type is a type variable or a parameterized type, but not array objects.

即:在java中是”不能创建一个确切的泛型类型的数组”的。

也就是说下面的这个例子是不可以的:

List<String>[] ls = new ArrayList<String>[10];  

而使用通配符创建泛型数组是可以的,如下面这个例子:

List<?>[] ls = new ArrayList<?>[10];  

这样也是可以的,但会有未经检查的警告:

类似地,尝试创建元素类型为类型变量的数组对象会导致编译时错误,因为由于类型变量在运行时不存在,因此无法确定实际的数组类型:

解决这些限制的方法是使用范型类作为运行时类型标记,如下一节中所述

8. 使用类字面常量(class literals)作为运行时类型标记

JDK 1.5 加入了范型,java.lang.Class是范型的。下面这是一个有趣的例子,它将范型用于集合类以外的东西。

既然Class有一个类型参数T,你可能会问,T代表什么?它代表Class对象所代表的类型。

例如,String.class的类型是Class <String>,Serializable.class的类型是Class <Serializable>。这可用于改善反射代码的类型安全性。

特别是,由于Class中的newInstance()方法现在返回一个T,因此在反射创建对象时可以获得更精确的类型

例如,假设您需要编写一个执行数据库查询的工具类方法,以SQL字符串形式给出,并返回数据库中与该查询匹配的对象集合。

一种方法是显式传入工厂对象,编写如下代码:

interface Factory<T> { T make();} 

public <T> Collection<T> select(Factory<T> factory, String statement) { 
    Collection<T> result = new ArrayList<T>(); 

    /* Run sql query using jdbc */  
    for (/* Iterate over jdbc results. */) { 
        T item = factory.make();
        /* Use reflection and set all of item's 
         * fields from sql results. 
         */ 
        result.add(item); 
    } 
    return result; 
}

然后可以通过匿名内部类的形式,这样来调用:

select(new Factory<EmpInfo>(){ 
    public EmpInfo make() {
        return new EmpInfo();
    }}, "selection strin

或者通过创建一个继承自Factory接口的实现类:

class EmpInfoFactory implements Factory<EmpInfo> {
    ...
    public EmpInfo make() { 
        return new EmpInfo();
    }
}


select(getMyEmpInfoFactory(), "selection string");

上述解决方案的缺点是它需要:

  • 在调用站点使用详细的匿名内部类,
  • 或为每个使用的类型声明一个接口实现类,并在调用时传递创建的工厂对象,这有点不自然。

而将类用作工厂对象是很自然的,然后可以通过反射使用它。今天(不使用泛型)代码可能写成:

Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) { 
    Collection result = new ArrayList();
    /* Run sql query using jdbc. */
    for (/* Iterate over jdbc results. */ ) { 
        Object item = c.newInstance(); 
        /* Use reflection and set all of item's
         * fields from sql results. 
         */  
        result.add(item); 
    } 
    return result; 
}

但是,这不会给我们提供我们想要的精确类型的集合。 既然Class是范型的,我们可以转而这样来写该段代码:

Collection<EmpInfo> emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) { 
    Collection<T> result = new ArrayList<T>();
    /* Run sql query using jdbc. */
    for (/* Iterate over jdbc results. */ ) { 
        T item = c.newInstance(); 
        /* Use reflection and set all of item's
         * fields from sql results. 
         */  
        result.add(item);
    } 
    return result; 
} 

上面的代码以类型安全的方式为我们提供了精确的集合类型。

这种使用类字面常量(class literals)作为运行时类型标记的技术是一个非常有用的技巧。 例如,这是一种在新API中广泛用于操作注释的习惯用法。

猜你喜欢

转载自blog.csdn.net/unicorn97/article/details/81837480