【问题标题】:Can Java generics be parameterized with values instead of types?Java泛型可以用值而不是类型来参数化吗?
【发布时间】:2021-10-09 15:57:14
【问题描述】:

假设我想定义结构相似但参数不同的类型,可以是整数或其他参数。

是否可以在 Java 中定义由整数甚至任意对象参数化的类族?

考虑以下伪代码(无法编译):

/** 
 * String of a certain length n and a method to reduce to length n-1 
 */
public class StringN<int n> {
    private String str;
    public StringN( String str) {
        if(str.length() != n) {
            throw new IllegalArgumentException("string is not of required length!");
        }
        this.str = str;
    }

    public StringN<n-1> reduce() {
        return new StringN<n-1>(s.substring(0, s.length() - 1));
    }

    @Override
    public String toString() {
        return str;
    }
}

我想到的其他更自然的例子是数学中的张量积,所以如果要定义参数“n”,例如空间 R^n 作为 Java 类或在函数式编程中 Function&lt;&gt;-space 的“arity”。那么如何定义一个由n参数化的具有不同arity的类族呢?

如果这在 Java 中是不可能的,那么这个概念是否存在于其他功能更强大的语言中?它的正确名称是什么? (比如可能是“参数化类”?)

编辑:作为对 cme​​ts 的反应,最后一部分只是为了了解这样一个概念的一般名称,而不是绕道其他语言。

【问题讨论】:

  • 不,不可能。这个概念在 C++ 中称为模板参数。 --- 请阅读:Can I ask only one question per post?
  • @Turing85 感谢您的有用评论(正确的名称真的很有帮助!)。如果我问的是同一事物的存在和名称,您真的认为这是几个问题吗?我可以看到它可以被视为这样,但我认为可以争辩说这些信息属于一起。但由于我是新来的,我当然愿意适应。
  • @Sebastian 我认为这个问题很好,但它可能会被误读为要求提供具有此功能的其他语言列表的问题 - 这样的问题在这里会不受欢迎。
  • 嗯,我认为这可以通过使用注释以某种方式实现。
  • 这个概念的另一种方法是dependent typing;调查这将产生很多信息。 (我认为 C++ 非类型模板参数可以被认为是依赖类型的有限形式)

标签: java generics functional-programming dependent-type non-type-template-parameter


【解决方案1】:

唉,Java 要求类型参数是类型(实际上,它甚至要求它们是引用类型),并且由于所有整数都是同一类型,因此您无法让编译器区分泛型取决于整数的值。

通常的解决方法是为每个可能的(或需要的)值声明一个单独的类型。要共享结构,您可以使用抽象基类。如果基类需要任何具体类型,子类可以将它们作为类型参数传递:

abstract class StringN<S extends StringN<S,P>, P extends StringN<P,?>>
        implements Comparable<S> {
    
    final String value;
    
    protected StringN(String value, int n) {
        if (value.length() != n) {
            throw new IllegalArgumentException(value);
        }
        this.value = value;
    }
    
    @Override
    public int compareTo(S o) {
        return value.compareTo(o.value);
    }
    
    abstract P newP(String value);
    
    public P removeLast() {
        return newP(value.substring(0, value.length() - 1));
    }
}

class String0 extends StringN<String0, String0> {

    protected String0(String value) {
        super(value, 0);
    }

    @Override
    String0 newP(String value) {
        throw new UnsupportedOperationException();
    }
}

class String1 extends StringN<String1, String0> {

    protected String1(String value) {
        super(value, 1);
    }

    @Override
    String0 newP(String value) {
        return new String0(value);
    }
}

class String2 extends StringN<String2, String1> {
    protected String2(String value) {
        super(value, 2);
    }

    @Override
    String1 newP(String value) {
        return new String1(value);
    }
}

public class Test {
    public static void main(String[] args) {
        String2 s2 = new String2("hi");
        String1 s1 = s2.removeLast();
        s1.compareTo(s2); // compilation error: The method compareTo(String1) is not applicable for the arguments (String2)
    }   
}

如您所见,只要一组值是有限的并且预先知道,您甚至可以教编译器计数 :-)

但是,它变得相当笨拙且难以理解,这就是很少使用此类解决方法的原因。

【讨论】:

  • 不错的构造@meriton!问题只是,我当然想要无限多的课程! ;) 在我的原始帖子中,我还提到了函数的数量(其中数量或多或少是参数的数量),可以为每个特定的函数空间写一个名称,但不能为一般的函数空间写一个名称。考虑一个函数类型链:F0 implements Function&lt;Object, Object&gt; (arity 2), F1 implements Function&lt;Object, F0&gt; (arity 3), F2 implements Function&lt;Object, F1&gt; (arity 4) .... 它们中的每一个都可以很好地定义,只是不适用于泛型 n ...
  • @Sebastian 好吧,因为你需要一个代码生成器——这不能在 java 语言中完成。
【解决方案2】:

您的问题是一个有趣的问题,但我认为您认为满足您需求的解决方案必须是一个参数化的类,这太过分了。

参数化类是数据类型的组合,而不是

由于您不需要编译对您的代码强制执行任何额外的静态类型检查,我认为编程解决方案就足够了:

  1. 第一步:将伪参数“int n”移动到最终变量:
public class StringN {

    private final int n;

    private String str;

    public StringN( String str) {
        if(str.length() != n) {
            throw new IllegalArgumentException("string is not of required length!");
        }
        this.str = str;
    }

    public StringN reduce() {
        return new StringN(s.substring(0, s.length() - 1));
    }

    @Override
    public String toString() {
        return str;
    }
}
  1. 当然,这还没有编译。您必须在每个构造函数(声明和调用)上初始化 n 变量。

  2. 如果您对将参数 n 作为公共构造函数调用的一部分公开这一事实感到不舒服,则可以解决限制构造函数对包访问的问题,并将构造责任交给一个新的工厂 em> 类,它必须是创建 StringN 对象的唯一公共方式。

public StringNFactory
{
    private final int n;

    public StringNFactory(int n)
    {
        this.n=n;
    }

    public StringN create(String s)
    {
        return new StringN(this.n, s);
    }
}

【讨论】:

  • 这里,n 永远不会被分配,这不会编译。看来n 必须作为构造函数参数传递。
  • 感谢工厂,这当然是一个进步! (尽管我同意埃里克森的观点,即 StringN 的构造函数缺少 n)。但是,似乎我对 rzwitserloot 的回答的评论可能具有误导性。这并不是说我不期望额外的类型检查。我只是没想到会有一个非常复杂的。在 StringN 与 StringN 中,我希望编译器抱怨 1 !=5 。当然,子类型化可能会变得更复杂......所以我想我必须接受我的想法不存在,然后像你的工厂这样的东西可能是要走的路......
  • 是的,我的朋友们,第一段代码不会编译:我在第 2 点已经明确说明。我只是想逐步开发我的方法明确我在每一步都在做什么。
【解决方案3】:

顾名思义,“类型参数”是一种类型。不是“字符串的长度”。

具体来说:可以想象fixed length string这个类型的概念,也可以想象这个概念有一个参数,它的类型是int;可以有FixedString&lt;5&gt; myID = "HELLO"; 并且可以编译,但FixedString&lt;5&gt; myID = "GOODBYE"; 将是一个错误,希望是编译时错误。

Java 不支持这个概念。如果那是您正在寻找的东西,请将其破解;你当然可以用代码来做这件事,但这意味着所有的错误和检查都发生在运行时,编译时不会发生什么特别的事情。

相反,泛型赋予类型参数化自身的能力,但只能使用类型。如果您想传达“列表......但不仅仅是任何列表,诺诺,存储字符串的列表”的概念 - 您可以这样做,这就是泛型的用途。该概念仅适用于类型,但不适用于其他任何事物(例如长度)。

此外,javac 将负责应用参数。因此,您不能通过制作一些虚假的层次结构来将其组合在一起,例如:

public interface ListSize {}
public interface ListIsSizeOne implements ListSize {}
public interface ListIsSizeTwo implements ListSize {}
public interface ListIsSizeThree implements ListSize {}

然后有一个FixedSizeList&lt;T extends ListSize&gt;,以便有人可以声明:FixedSizeList&lt;ListIsSizeTwo&gt; list = List.of(a, b);

不能工作的原因是:你不能告诉javac该做什么,它不是一个可插拔的系统。 Java“知道”如何应用类型界限。它不知道如何强制执行大小限制,所以你不能这样做。

【讨论】:

  • 感谢您的回答,但从您的推理来看,我认为您在说明一些我无论如何都不会从编译器获得的东西。如果您查看我的伪代码,我不希望编译器对我的字符串进行长度检查。我在构造函数中自己检查大小。我只希望编译器在进行类型检查时检查 n 是否匹配。您的“人造层次结构”示例对我来说完全足够了,只是我不想定义无限多的接口/类,而是使用一些参数'n'在一个定义中全部定义。但这似乎是不可能的。
  • @Sebastian 声明泛型类型的目的是提供类型安全。类型安全意味着编译器拒绝类型错误。例如,StringN&lt;5&gt; s5 = new StringN&lt;&gt;("HELLO"); StringN&lt;1&gt; s1 = s5; 应该因为赋值中的不兼容类型而无法编译。如果您只需要运行时验证,则不需要类型参数;一个普通的构造函数参数就可以了。
  • 明白了。是的,我的例子的重点是s1 = s5 应该在编译时失败,给定它们声明的类型。期望对字符串进行长度检查会太多。但是,不幸的是,即使是类型检查也不支持。
  • @rzwitserloot 很好,我知道这在 Java 中是不可能的。在我最初的问题中,我不一定期望此功能必须具有与 java 泛型相同的语法(它由 erickson 稍微编辑),因此可能存在我还不知道的语法,但显然没有吨。我唯一不同意你的一点是你为什么不可能。即使在 Java 泛型的语法中,也可以在 Java 中实现此功能而无需太多开销并具有向后兼容性。
  • @Sebastian 如果添加这样的东西我会喜欢的:) - 唉。
【解决方案4】:

我自己回答这个问题,因为有用的信息分布在几个 cmets/answers 中。我将此作为社区 wiki 答案,以免因他人的建议而赢得声誉。

我正在寻找的功能显然是所谓的dependent-typing 的一个特例(感谢@DylanSp)。 C++ 的模板参数(参数不是类型)也是这种特性的一个例子(感谢@Turing85)。所有答案都同意这个特性不幸在 Java 中不存在,在 Java 泛型的语法中也不存在(@rzwitserloot 和其他人指出 Java 规范只允许在菱形 &lt;&gt; 中使用引用类型),也不在任何其他语法中。

当然可以在 Java 中为每个特定的 n 手动定义类型。因此,对于我在问题中的示例,可以定义类 String1、String2、String3,...,但只能定义有限多个。为了使每个特定类型的定义尽可能简单,可以使用一种方法,其中包含所有这些类共享的抽象基类,请参阅@meriton 的好建议。

不是我想的那样,但在有限的许多情况下,代码生成器(@Hulk 提到)也应该是一种选择。如果我理解正确,这也是@MC Emperor 在提到注释时的想法。

但是,如果一个人真的想坚持无限多个类(这就是我想要的),唯一的出路似乎是让计数器 n 成为单个类的成员,并认为它们是不同的类型。在编译器级别,不会有任何类型检查,因此必须自己实现类型安全。 @Little Santi 对工厂提出的建议将是一种为这种方法带来更多结构的方法。

【讨论】:

    猜你喜欢
    • 2022-06-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-03-29
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-06-21
    相关资源
    最近更新 更多