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 是 数据类型 的组合,而不是 值 。
由于您不需要编译对您的代码强制执行任何额外的静态类型检查,我认为编程解决方案就足够了:
- 第一步:将伪参数“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;
}
}
当然,这个还没有编译通过。您必须在每个构造函数(声明和调用)上初始化 n
变量。
如果您对将参数 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 对工厂提出的建议将是一种将更多结构引入这种方法的方法。
假设我想定义结构相似但参数不同的类型,该参数可以是整数,也可以是其他类型。
是否可以在 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 是 数据类型 的组合,而不是 值 。
由于您不需要编译对您的代码强制执行任何额外的静态类型检查,我认为编程解决方案就足够了:
- 第一步:将伪参数“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;
}
}
当然,这个还没有编译通过。您必须在每个构造函数(声明和调用)上初始化
n
变量。如果您对将参数
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 对工厂提出的建议将是一种将更多结构引入这种方法的方法。