【问题标题】:Generic methods returning dynamic object types返回动态对象类型的通用方法
【发布时间】:2012-09-24 17:21:15
【问题描述】:

可能是一个以前被问过的问题,但像往常一样,当你提到泛型这个词时,你会得到一千个解释类型擦除的答案。我很久以前就经历过那个阶段,现在对泛型及其使用有了很多了解,但这种情况稍微微妙一些。

我有一个容器,它表示电子表格中的一个数据单元格,它实际上以两种格式存储数据:作为用于显示的字符串,但也以另一种格式,取决于数据(存储为对象)。该单元还包含一个在类型之间转换的转换器,并且还对类型进行有效性检查(例如,一个 IntegerTransformer 检查字符串是否为有效整数,如果它返回一个整数来存储,反之亦然)。

单元格本身没有输入,因为我希望能够更改格式(例如,将辅助格式更改为浮点而不是整数,或更改为原始字符串),而不必使用新类型重建单元格对象。之前的尝试确实使用了泛型类型,但是一旦定义了就无法更改类型,编码变得非常庞大,需要大量反射。

问题是:如何以键入的方式从 Cell 中获取数据?我进行了实验,发现即使没有定义约束,也可以使用方法来使用泛型类型

public class Cell {
    private String stringVal;
    private Object valVal;
    private Transformer<?> trans;
    private Class<?> valClass;

    public String getStringVal(){
        return stringVal;
    }

    public boolean setStringVal(){
        //this not only set the value, but checks it with the transformer that it meets constraints and updates valVal too
    }

    public <T> T getValVal(){
        return (T) valVal;
        //This works, but I don't understand why
    }
}

让我失望的是:那是?它不能投射任何东西,没有类型 T 的输入限制它匹配任何东西,实际上它并没有在任何地方说任何东西。有一个 Object 的返回类型只会在任何地方给转换带来复杂性。

在我的测试中,我设置了一个 Double 值,它存储了 Double(作为一个对象),当我执行 Double testdou = testCell.getValVal();它立即起作用,甚至没有未经检查的演员表警告。但是,当我执行 String teststr = testCell.getValVal() 时,我得到了 ClassCastException。真的不足为奇。

我对此有两种看法:

一:使用未定义的 Cast to 似乎只不过是一种将强制转换放在方法内部而不是在方法返回后放在外部的方法。从用户的角度来看,它非常简洁,但是该方法的用户必须担心使用正确的调用:所做的只是隐藏复杂的警告和检查,直到运行时,但似乎有效。

第二种观点是:我不喜欢这样的代码:它不干净,不是我通常以编写为荣的那种代码质量。代码应该是正确的,而不仅仅是工作。错误应该被捕获和处理,并且可以预见,即使唯一的预期用户是我自己,接口也应该是万无一失的,我总是更喜欢一种灵活的通用和可重用的技术,而不是笨拙的技术。问题是:有什么正常的方法可以做到这一点?这是实现无类型、所有接受 ArrayList 的一种偷偷摸摸的方式,它返回你想要的任何东西而不进行强制转换?或者我在这里缺少什么。有些东西告诉我我不应该相信这段代码!

也许比我想象的更像一个哲学问题,但我想这就是我要问的。

编辑:进一步测试。

我尝试了以下两个有趣的sn-ps:

public <T> T getTypedElem() {
    T output = (T) this.typedElem;
    System.out.println(output.getClass());
    return output;
}

public <T> T getTypedElem() {
    T output = null;
    try {
        output = (T) this.typedElem;
        System.out.println(output.getClass());
    } catch (ClassCastException e) {
        System.out.println("class cast caught");
        return null;
    }
    return output;
}

当将一个 double 分配给 typedElem 并尝试将其放入 String 时,我得到一个异常,不是在强制转换为 ,而是在返回时,第二个 sn-p 不保护。 getClass 的输出是 java.lang.Double,表明它是从 typedElem 动态推断的,但编译器级别的类型检查只是被强制排除在外。

作为辩论的注释:还有一个用于获取 valClass 的函数,这意味着可以在运行时进行可分配性检查。

Edit2:结果

在考虑了选项后,我采用了两种解决方案:一种是轻量级解决方案,但将函数注释为@depreciated,其次是您将要尝试将其转换为的类传递给它的解决方案。这种方式是根据情况选择的。

标签: java generics methods casting


【解决方案1】:

您可以尝试输入令牌:

public <T> T getValue(Class<T> cls) {
    if (valVal == null) return null;
    else {
        if (cls.isInstance(valVal)) return cls.cast(valVal);
        return null;
    }
}

请注意,这不会进行任何转换(即,如果valValFloatInteger 的实例,则不能使用此方法提取Double)。

顺便说一句,你应该得到一个关于你定义 getValVal 的编译器警告。这是因为在运行时无法检查强制转换(Java 泛型通过“擦除”工作,这本质上意味着泛型类型参数在编译后被遗忘),所以生成的代码更像:

public Object getValVal() {
    return valVal;
}

【讨论】:

  • 我知道擦​​除是如何工作的,是的,尽管生成的代码比这要复杂一些。我已经思考过这样的方法,但是isInstance不是一个很好的检查,我更喜欢Class.isAssignableFrom(cls)。它不像我想要的那样优雅,但它是我可以获得断言的方法之一。我总是可以同时拥有这两种选择,并附上免责声明。对于具有可预测数据类型的简单情况,使用 simple 选项会更容易,而对于真正动态的情况,使用更强的选项会更容易。不要求上课可能会更容易,而只是一个例子
  • 我越想越觉得这似乎是最安全的解决方案。我确信我可以找到一种方法来处理额外的复杂性。更有说服力的一点是,这是许多其他收藏库使用的方法,这表明没有更清洁或更好的方法。如果您希望能够强制执行您需要从某个地方获取它的类型。
  • 根据我的经验,即使 JVM 没有在源显式转换时进行检查,但当您尝试将通用方法的结果分配给变量时,它仍然会进行检查预期的类型(实际上我发现在大多数情况下这比在 getValue() 方法中抛出异常要好一些,因为在这种情况下,这通常是方法调用时的代码问题,而不是比 getValue() 方法本身中的任何内容都重要,因此将堆栈跟踪中的最顶层项目作为调用该方法的行对我来说更有意义)。
  • (好吧,当然,假设类型参数本身不是泛型类型。当您使用泛型类型进行强制转换时,事情会变得更加棘手。我还认为分配对象可能存在问题当实际对象是变量类型的子类的实例时,将变量转换为变量,但是当我仔细检查时,将变量转换为超类或实现的接口似乎完全没问题,因为getClass()instanceof 仍然使用值的实际类。)
【解决方案2】:

正如您所发现的,使用 Java 的类型系统可以表达的内容是有限的,即使使用泛型也是如此。有时,您希望使用类型声明来断言某些值的类型之间存在关系,但您不能(或者也许您可以,但代价是过度复杂和冗长冗长的代码)。我认为这篇文章中的示例代码(问题和答案)很好地说明了这一点。

在这种情况下,如果您将对象/字符串表示形式存储在“转换器”中,Java 编译器可以进行更多类型检查。 (也许您必须重新考虑它是什么:也许它不仅仅是一个“转换器”。)在您的基础 Transformer 类上放置一个通用绑定,并将相同的绑定设置为“对象”的类型。

就获取单元格的值 out 而言,编译器类型检查无法帮助您,因为该值可以是不同的类型(而且您在编译时不知道time 什么类型的对象将存储在给定的单元格中)。

我相信你也可以做类似的事情:

public <T> void setObject(Transformer<T> transformer, T object) {}

如果设置转换器和对象的唯一方法是通过该方法,编译器对参数的类型检查将防止不兼容的转换器/对象对进入单元格。

如果我理解你在做什么,你使用的Transformer 的类型完全取决于单元格所持有的对象类型,对吗?如果是这样,我不会将转换器/对象设置在一起,而是只为对象提供一个设置器,并进行哈希查找以找到合适的转换器(使用对象类型作为键)。每次设置值或将其转换为字符串时都可以进行哈希查找。无论哪种方式都可以。

这样自然就不可能传入错误类型的Transformer

【讨论】:

  • 我知道有限制,我已经接近边缘了。这就是为什么我试图了解我在边缘的哪一边。如果我不能积极断言这是一个安全的演员表,我认为我不会太沮丧,我知道比期望这是标准用法更好,泛型通常是一个很好的工具,过去非常有用为了我。 Transformer 是一个接口,当然不像示例那么简单。 DateTransformer 概念是需要特定日期格式的实现,您可以有 PercentageTransformer、MonthTransformer 或自定义类型。
  • 如果它是一个接口,你仍然可以参数化接口,并给它使用接口本身的通用绑定的getter/setter,对吗?即interface Transformer&lt;T&gt; { T get(); void set(T object); }
  • Transformer(以及示例中遗漏的一些其他函数)是我正在重建的库的一部分(因为以前的类型化版本显然太麻烦了)。特别是我的转换器接口是双重类型的,我的单元格将其锁定为 。我有很多我正在构建的功能作为 DualTyped 数据的概念,它以两种形式存在,Transformers 形成交换和检查。然而,Cell 并没有那么受限。据我所知,没有办法制作一个动态但共享的通用类型变量,如果我错了,请纠正我?
  • 抱歉,评论占用了超过 1 个框。我认为我们正在并行评论:P。转换器已键入,但那是另一个文件。问题是类型参数仅适用于转换器内部,更具体地说,它在您实例化它的那一刻就已修复。但是一个单元格可能会删除它的值和变压器,并用不同的变压器替换并重新计算。这就是为什么我试图消除单元格和电子表格的复杂性,使其仅适用于数据。
  • 我还认为将值放入转换器中并没有什么意义——这只是一种从编译器中获得更严格类型检查的方法。我刚刚为您添加了另一个想法...
【解决方案3】:

我认为您是一个静态类型的人,但请试一试:您是否考虑过使用像 groovy 这样的动态语言来处理这部分内容?

根据您的描述,在我看来,类型更多的是妨碍而不是帮助任何事情。

在 groovy 中,您可以让 Cell.valVal 成为动态类型并轻松进行转换:

class Cell {
  String val
  def valVal
}

def cell = new Cell(val:"10.0")
cell.valVal = cell.val as BigDecimal
BigDecimal valVal = cell.valVal
assert valVal.class == BigDecimal
assert valVal == 10.0

cell.val = "20"
cell.valVal = cell.val as Integer
Integer valVal2 = cell.valVal
assert valVal2.class == Integer
assert valVal2 == 20

as 是最常见的转换所需的一切。你也可以添加你的。

如果需要转换其他代码块,请注意 java 的语法是有效的 groovy 语法,do { ... } while() 块除外

【讨论】:

  • 尽管我很喜欢这个答案,但说“在没有这个问题的地方使用不同的语言”是欺骗性的。..
  • 不超出标准 Java。四个原因是兼容性和可移植性、库在其他机器上的可用性、与他人的协作,以及我不喜欢使用比自己制作更复杂的大型库/语言扩展。
  • @DavidCowden -- 这不是作弊。 Groovy、Scala、Clojure 和 JRuby 都在 JVM 上运行,并且可以与 OP 的 Java 代码库进行互操作。不仅如此,整个软件行业需要得到这样的信息:我们现在拥有比 Java/C#/C++ 更好的语言。 Java 在 1995 年还不错,但我们现在可以做得更好,而且人们需要继续听到这一点。
  • @K.Barad -- WillP 在这里向您展示的语言与 Java 兼容,并且可以移植到 Java 所在的任何地方(因为它在 JVM 上运行)。它可以调用 Java 代码,因此库的可用性不是问题。至于与他人的协作,很多人已经在使用这些更新的语言,而且还会有更多人使用。如果您遵循 WillP 的建议并学习一种更新的、与 Java 兼容的语言,您不仅会解决泛型问题 - 您可能会发现它彻底改变了您的整个编程方式。
  • @AlexD Sure Clojure 和 Groovy 等都运行在 JVM 上,允许您在不同的范例下一起编程。伟大的。但这并不意味着您回答了 K.Barad 的问题..
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-06-10
  • 1970-01-01
  • 2012-05-04
  • 1970-01-01
  • 2017-01-21
  • 1970-01-01
相关资源
最近更新 更多