Java泛型能否使用值而不是类型进行参数化?

16

假设我想定义一些在结构上相似但参数可能是整数或其他值的类型。

在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;
    }
}

我脑海中想到的其他更自然的例子是数学中的张量积,如果想将空间R^n定义为Java类或在函数编程中的Function<>空间的'arity',那么应该放置参数'n'。那么如何定义具有不同参数n的一组类?

如果Java中无法实现此功能,是否存在于其他更为函数式的语言中,并且该概念的正确名称是什么?(例如,'参数化类'?)

编辑:针对评论所做的最后一部分只是想了解这种概念的通用名称,而不是转向其他语言。


12
不可能。在C++中,这个概念被称为模板参数。---请阅读:我能只在一个帖子中问一个问题吗? - Turing85
1
@Turing85 谢谢你的有用评论(正确的名称真的很有帮助!)。如果我询问同一事物的存在和名称,您是否认为它是几个问题?我可以看到它可能被视为这样,但我认为人们可以主张这些信息应该放在一起。但因为我在这里还很新,我当然愿意适应。 - Sebastian
2
@Sebastian 我认为这个问题没问题,但是可能会被误读为一个要求列出其他具有此功能的语言的问题 - 这样的问题在这里会受到反对。 - Hulk
1
我认为可以通过使用注释来实现这个目标。 - MC Emperor
2
这个概念的另一种方法是依赖类型; 研究它将会得到很多信息。(我认为C++非类型模板参数可以被视为依赖类型的有限形式) - DylanSp
@DylannSp 太好了,谢谢你提供的链接,这会更好!虽然比我要求的多一些,但绝对能满足我的需求! - Sebastian
4个回答

5

你的问题很有趣,但我认为你过于假设需要解决你问题的是参数化类。

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

由于您不需要编译器对代码进行任何额外的静态类型检查,所以我认为可以使用编程方案来解决:

  1. 第一步:将伪参数"int n"移动到一个final变量中:
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 作为公共构造函数调用的一部分暴露感到不舒服,那么可以通过将构造函数限制为包访问并将构造责任交给一个新的 工厂 类来解决该问题。这个类必须是创建 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);
    }
}

2
这里,n从未被赋值,这样无法编译。似乎需要将n作为构造函数参数传递。 - erickson
感谢这个工厂,它肯定是一个改进!(虽然我同意 Erickson 的观点,即 StringN 的构造函数缺少 n)。然而,似乎我的评论对 rzwitserloot 的回答可能有误导性。我并不是不希望进行额外的类型检查。我只是不希望它变得非常复杂。在 StringN<1> 与 StringN<5> 中,我希望编译器会抱怨 1 != 5。当然,子类型化可能会变得更加复杂...所以我想我必须接受我心中的想法不存在,然后像你的工厂一样去做... - Sebastian
2
是的,朋友们,第一段代码无法编译:我在第二点清楚地说明了。我只是想逐步发展我的方法,以便在每个步骤上清楚地表明我正在做什么。 - Little Santi

5
很遗憾,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!问题只是我想要无限多个类! ;) 在我的原帖中,我还提到了函数的arity(arity大致上是指参数的数量),在这里可以为每个具体的函数空间编写一个名称,但是不能为通用函数空间编写一个名称。考虑一个函数类型链:F0 implements Function<Object,Object>(arity 2),F1 implements Function<Object,F0>(arity 3),F2 implements Function<Object,F1>(arity 4) ....它们都可以被很好地定义,只是对于通用的n不行... - Sebastian
1
@Sebastian 好的,为此您需要一个代码生成器 - 这不能在 Java 语言内完成。 - Hulk

4
正如其名称所示,“类型参数”是一种“类型”,而不是“字符串长度”。
具体来说,可以想象类型为“固定长度字符串”的概念,并且可以想象此概念有一个参数,其类型为“int”; 可以编写代码 “FixedString <5> myID =“ HELLO”; ”并且它将编译通过,但是“FixedString <5> myID =“ GOODBYE”; ”会产生错误,希望是编译时错误。
Java根本不支持这个概念。如果您正在寻找这样的功能,那么可以使用代码进行解决;当然,这意味着所有错误和检查发生在运行时,编译时没有任何特殊操作。
相反,泛型是为了使类型能够自我参数化,但仅限于“类型”。如果要传达“A List ...但不仅仅是任何列表,而是存储字符串的列表”的概念-可以这样做,这就是泛型的作用。该概念仅适用于类型,而不适用于任何其他内容(例如长度)。
此外,javac将负责应用该参数。因此,无法通过创建某些伪层次结构来进行解决:
public interface ListSize {}
public interface ListIsSizeOne implements ListSize {}
public interface ListIsSizeTwo implements ListSize {}
public interface ListIsSizeThree implements ListSize {}

然后有一个FixedSizeList<T extends ListSize>,这样某人可以声明:FixedSizeList<ListIsSizeTwo> list = List.of(a, b);

不能这样做的原因是:你无法告诉javac该如何做,它不是可插拔系统。Java“知道”如何应用类型边界。它不会知道如何强制大小限制,因此你不能这样做。


1
谢谢你的回答,但从你的推理中,我认为你在谈论一些我不指望编译器做的事情。如果你看一下我的伪代码,我并不指望编译器检查我的字符串长度。我在构造函数中自己检查大小。我只希望编译器在进行类型检查时检查n是否匹配。你提到的“虚假层次结构”对我来说完全足够了,只是我不想定义无数个接口/类,而是在一个定义中使用某个参数'n'。但似乎这是不可能的。 - Sebastian
实际上,子类型化也很简单。与普通泛型类似,StringN <n> 中的任何一个都不是另一个的子类型,但可以使用(在泛型通配符的精神下)谓词,如StringN <k-> k <= n> 来包含所有长度为n的字符串,并且这些将包含StringN <n> 作为子类型。 - Sebastian
1
明白了。是的,我举的例子的重点在于s1 = s5应该在编译时失败,因为它们声明的类型不同。期望对字符串进行长度检查可能过于苛刻。但不幸的是,事实仍然是即使类型检查也不受支持。 - erickson
1
@rzwitserloot 没问题,我已经明白在Java中不可能实现。在我的原始问题中,我并没有期望这个特性必须具有与Java泛型相同的语法(它被erickson稍微编辑了一下),所以可能存在一种我不知道的语法,但显然是不存在的。我唯一不同意你的观点的地方是你为什么认为这是不可能的。即使在java泛型的语法内部,也完全可以实现这个特性,而且没有太多的开销和向后兼容性。 - Sebastian
1
@Sebastian,如果能添加这样的功能,我会很高兴 :) - 唉。 - rzwitserloot
显示剩余3条评论

0

我自己回答这个问题,因为有用的信息分散在几个评论/答案中。我将其作为社区维基答案,以便我不会因他人的建议而获得声望。

我正在寻找的功能显然是所谓dependent-typing的特例(感谢@DylanSp)。此外,C++的模板参数(参数不是类型)也是这种功能的示例(感谢@Turing85)。所有答案都认为,不幸的是,这个功能在Java中不存在,无论是在Java泛型的语法中(@rzwitserloot和其他人指出Java规范只允许钻石<>中的引用类型),还是在任何其他语法中。

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

不是我想的那样,但有限数量的情况下也可以使用代码生成器(由@Hulk提到)作为一种选择。如果我理解正确,这也是@MC Emperor在提到注释时所考虑的。

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接