【问题标题】:Why am I unable to add the contents of a ArrayList into another ArrayList via the addAll method when the ArrayLists are both generically typed?当 ArrayList 都是通用类型时,为什么我无法通过 addAll 方法将 ArrayList 的内容添加到另一个 ArrayList 中?
【发布时间】:2018-10-11 23:01:34
【问题描述】:

我正在尝试将文件读取到ArrayList,该文件具有通过通配符“?”声明的未知类型。在使用ObjectInputStream.readObject() 获得反序列化的Object 之后,我将Object 转换为ArrayList<?>。然后我尝试通过ArrayList.addAll(Collection) 方法将我的ArrayList<?> 的元素添加到另一个ArrayList<?> 中。但是,我尝试调用 list.addAll(buffer) 失败并出现以下异常消息:

没有找到适合addAll(ArrayList<CAP#1>)的方法

为什么我无法通过addAll() 方法将ArrayList<?> 的元素添加到ArrayList<?> 的另一个实例中?


下面是产生这个异常的方法:

public void readFile(ArrayList<?> list, String fileName) throws Exception
{
    FileInputStream fis = new FileInputStream(fileName);
    try (ObjectInputStream ois = new ObjectInputStream(fis))
    {
        ArrayList<?> buffer = (ArrayList<?>) ois.readObject();
        list.addAll(buffer);
        System.out.println("Added  to the customer list.");
    }
}

【问题讨论】:

标签: java generics collections types type-erasure


【解决方案1】:
  1. 如果type 相关: 为了能够将List 添加到另一个,第一个元素必须与主List 的子类型相同,您需要:

    • 在签名中添加&lt;T&gt; 以告知使用了泛型
    • 在ArrayList&lt;T&gt; 中使用它作为主要的
    • ArrayList&lt;? extends T&gt; 获取子列表

    public <T> void readFile(ArrayList<T> list, String fileName) throws Exception {
        FileInputStream fis = new FileInputStream(fileName);
        try (ObjectInputStream ois = new ObjectInputStream(fis)) {
            ArrayList<? extends T> buffer = (ArrayList<? extends T>) ois.readObject();
            list.addAll(buffer);
            System.out.println("Added  to the customer list.");
        }
    }
    

  1. 如果类型是随机的,则改为

    ArrayList<Object> list
    

【讨论】:

  • 这不安全。您可以使用ArrayList&lt;String&gt; 或ArrayList&lt;Integer&gt; 和相同的文件名调用readFile:至少其中一个会失败(当然,除非ois.readObject() 返回一个空列表)。
  • 应编辑此答案以包括类型安全性,方法是使用 ObjectInputFilter 和 ObjectInputStream.setObjectInputFilter​(ObjectInputFilter)。然后只需要求调用者为您提供单个Class 或Class[] 实例,并使用它来填充过滤器的类名Strings。如果过滤器在从文件中读取序列化对象时返回Status.REJECTED,使用这些方法将导致ObjectInputStream 抛出InvalidClassException。
  • 实际上,对于这个特定的用例,我会用比ObjectInputFilter 更好的方法自己提供一个替代答案(我们只是试图反序列化包含一个Object 的文件,该文件只能有效地转换为一种类型,我们可以在没有ObjectInputFilter 的情况下简单地检查。
【解决方案2】:

泛型或:我如何开始担心和讨厌类型擦除

当你通过编译的源代码给它一系列bytecodes/literals 时,你的 JVM 试图确定你的意思。 “通常”使用具体的类类型(如String)或具体的原始类型(如int)很好地声明所有内容。例如,6 的值带有一些元数据,告诉 JVM 它是 int 文字而不是其他类型。一系列字节本身没有意义;您需要一种方法来告诉 JVM 二进制数实际编码的内容。相同的二进制值可以代表无限数量的想法或状态,因此包含类型元数据以帮助理解这一切非常重要。如果没有类型,JVM 将无法区分一个值为'a' 的char 和一个值为0x61 的字节。当您提供泛型类型时,例如List 接口的实现所使用的类型(例如您使用的ArrayList),您是在告诉编译器“这不是特定的具体类型,它可以是任何东西”(尽管使用extends 你可以稍微限制一下)。现在,通常这可能不是问题,但由于我不会涉及的向后兼容性问题,动态类型在称为类型擦除的过程中被删除(查看this answer 以获得非常详细的推理解释) .解决您的问题的简短而甜蜜的答案是简单地考虑泛型类型的额外复杂性,并积极检查和防止不允许的强制转换。因此,类型安全主要可以通过手动检查泛型类型可分配性来实现,而不是像使用具体类型时那样依赖自动类型安全。

示例方法

这是一个相对类型安全的代码版本(在最坏的情况下,您将收到运行时错误消息)检查和泛型类型边界(ObjectInputStream 和 ObjectOutputStream 使用来自 @987654338 的方法@ 接口来实现它们对流的Object 实例的读写)。

public static <OUTPUT extends Serializable, INPUT extends Serializable>
    OUTPUT getObjectFromFile(Class<INPUT> type, CharSequence fileName)
    throws IOException, ClassNotFoundException {

    final String filenameString = Objects.requireNonNull(fileName).toString();
    final Class<INPUT> inputType = Objects.requireNonNull(type);

    try (ObjectInput ois = new ObjectInputStream(new FileInputStream(filenameString))) {
        final Object rawObject = ois.readObject();

        @SuppressWarnings("unchecked")
        final OUTPUT output = (OUTPUT) (rawObject == null ? null : inputType.cast(rawObject));

        return output;
    }

}

public static <DESIRED extends Serializable, STORED extends Serializable>
    boolean addArrayListFromFile(Collection<DESIRED> out, Class<STORED> type, CharSequence fileName)
    throws IOException, ClassNotFoundException {

    @SuppressWarnings("unchecked")
    final ArrayList<DESIRED> inList = getObjectFromFile(type, fileName);

    return inList != null && out.addAll(inList);
}

更多详情

这些方法从文件中读取序列化的对象,并通过方法参数指定类型安全和文件位置的细节。顶层方法的返回值实际上是一个boolean表示成功或失败(true是成功),但是实际输出的数据是写入到被调用方提供的Collection实例中。与可以使用熟悉的a = (a) b; 语法安全地显式指定强制转换的非泛型方法不同,具有泛型类型(如DESIRED、STORED、INPUT 和OUTPUT)的泛型方法不能完全确定他们将处理什么类型。他们通过称为类型擦除的过程丢失了有关类型的这些信息。

通过类型擦除,开发人员和 JVM 运行时会丢失有关泛型类型的具体类型的元数据。结果,运行时和开发人员受制于可分配给所需输出类型的输入类型。只有编译器甚至可能具有强制转换的可证明性,并且它不能像验证具体类型那样在一般意义上验证泛型类型。通常情况下,直到运行时才可能进行类型安全检查。运行时的 List&lt;?&gt; 可能实际上已被编译和编程为 List&lt;String&gt; ,但我们无法在运行时确定两个列表之间的差异,因为编译器无法维护或确定我们需要做的信息所以。

除了类型擦除导致类型元数据丢失之外,编译器还可能对程序的运行时类型图了解不完整。诸如链接引入我们当前编译器无法识别的新类型之类的可能性意味着我们永远无法避免使用完全算法方法对泛型类型进行不安全强制转换。事实上,对于泛型类型,没有办法以可概括的方式满足这种类型安全问题,因为它相当于一个布尔可满足性问题。但是,我们可以在其他地方获得有关方法调用者想要和提供的类型的具体信息……开发人员!开发人员必须为泛型类型指定明确的意图以提供类型安全。通过让方法调用者(最终是开发人员)负责通过将Class&lt;T&gt; 实例传递给代码/方法来告诉运行时期望什么,我们可以尝试以避免ClassCastException 的方式检查泛型类型.

如果我们有一个List&lt;T&gt;,它就变成了一个List,所以我们不能再识别为与任何其他List 不同的类型。但是,Class&lt;T&gt; 并非如此,我们可以利用它在编译器周围传递类型元数据并进入运行时(因为 Class 对象将类型信息存储在对象图中,而不是专门使用 Java 的类型系统)。然而,最终让程序员负责为每个方法调用提供正确的Class&lt;T&gt;。通过使运行时检查类型的有效性,编译器在编译期间检测类型安全错误的能力主要牺牲了使用泛型类型的表达式。编译时不会再收到“int 不能转换为String”之类的警告,而只会收到应用程序异常或必须在运行时适当处理的转换安全验证失败。

如果你想玩弄一些东西,看看当你使用这个方法时会发生什么,我在 GitHub 上创建了a gist,其中包含一个带有 main 方法的简短的小单文件类,你可以作为一个小测试程序运行。

【讨论】:

    【解决方案3】:

    如果您不知道类型,但想安全地添加到列表中,则必须是ArrayList&lt;Object&gt;。

    这是唯一可以安全添加任何内容的类型。 (我想它在技术上可能是ArrayList&lt;? super Object&gt;;但除了它本身之外,Object 没有任何下限)。

    否则,您可以使用ArrayList&lt;Integer&gt; 调用readFile,然后尝试添加您从ObjectInputStream 读取的Strings。稍后,当您尝试从列表中获取 Integer 时,这将失败,但该元素实际上是 String。

    您不能安全地使用ArrayList&lt;T&gt;,原因完全相同:具有此签名的方法:

    public <T> void readFile(ArrayList<T> list, String fileName) throws Exception
    

    可以这样调用:

    ArrayList<String> strings = new ArrayList<>();
    readFile(strings, "filename");
    
    ArrayList<Integer> integers = new ArrayList<>();
    readFile(integers, "filename");  // <-- Note the same filename
    

    除非filename 包含一个空列表,否则这些列表中至少有一个类型不正确;因此,该方法不是类型安全的。但它是不安全的类型:此代码可以正常执行,但代码(可能很远)会在尝试从列表中检索对象并发现它的类型错误时失败并返回 ClassCastException。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-01-11
      • 1970-01-01
      • 2013-05-22
      • 2018-08-19
      • 1970-01-01
      • 2013-05-19
      相关资源
      最近更新 更多