Java 泛型可以用值而不是类型来参数化吗?

Can Java generics be parameterized with values instead of types?

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

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

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

/** 
 * 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;
    }
}

我想到的其他更自然的例子是数学中的张量积,所以如果想定义例如space R^n 作为 Java class 或在函数式编程中 'arity' 的 Function<>-space。那么如何定义一个由 n 参数化的具有不同 arity 的 classes 族?

如果这在 Java 中是不可能的,这个概念是否存在于其他功能性更强的语言中?它的正确名称是什么? (比如 'parameterized class'?)

编辑:作为对评论的反应,最后一部分只是知道这样一个概念的通用名称,而不是绕道其他语言。

顾名思义,“类型参数”是一个类型。不是 'a length of a string'.

具体来说:可以想象fixed length string类型的概念,可以想象这个概念有一个参数,其类型是int;一个人可能有 FixedString<5> myID = "HELLO"; 并且会编译,但是 FixedString<5> myID = "GOODBYE"; 会是一个错误,希望是编译时错误。

Java 不支持这个概念。如果那是你要找的东西,那就把它拼凑起来;您当然可以使用代码使其工作,但这意味着所有错误和检查都发生在运行时,编译时不会发生任何特殊情况。

相反,泛型赋予类型参数化自身的能力,但仅具有类型。如果你想传达 'A List... but not just any list, nono, a list that stores Strings' 的概念——你可以做到,这就是泛型的用途。该概念仅适用于类型而不适用于其他任何东西(例如长度)。

此外,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 'knows' 如何应用类型界限。它不知道如何执行大小限制,所以你不能这样做。

你的问题很有趣,但我认为你过于假设满足你需求的解决方案必然是参数化的 class。

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

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

  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 作为 public 构造函数调用的一部分公开这一事实感到不舒服,可以通过限制构造函数访问包来解决这个问题,并且将构造责任交给新的 Factory class,这必须是创建 StringN 对象的唯一 public 方法。

public StringNFactory
{
    private final int n;

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

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

唉,Java要求类型参数是类型(实际上,它甚至要求它们是引用类型),而且由于所有整数都是相同类型, 你不能让编译器根据整数的值来区分泛型。

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

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)
    }   
}

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

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

我自己在回答这个问题,因为有用的信息分布在几个 comments/answers 上。我将其设为社区维基答案,这样我就不会因他人的建议而声名狼藉。

我正在寻找的功能显然是所谓的 dependent-typing 的特例(感谢@DylanSp)。 C++ 的模板参数(参数不是类型)也是此类功能的一个示例(感谢@Turing85)。不幸的是,所有答案都同意 Java 中不存在此功能,也不存在 Java 泛型的语法中(@rzwitserloot 和其他人指出 Java 规范仅允许菱形 <>),也不是任何其他语法。

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

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

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