问题 Java Generics,支持“专业化”?与C ++模板的概念相似之处?


我知道如何使用 C ++ - 模板  - 请注意,不是专家。同 Java泛型 (和Scala,就此而言),我有我的困难。也许,因为我试图将我的C ++知识转换为Java世界。我读到其他地方,“它们没什么相似之处:Java Generics只是语法糖节省演员,C ++模板只是一个美化的预处理器”:-)

我很确定,两者都是一个简化的视图。所以,为了理解大的和微妙的差异,我试着开始 专业化

C ++ 我可以设计一个 模板 (作用的一类) 任何 类型 T 支持我所需的操作:

template<typename T>
T plus(T a, T b) { return a.add(b); }

这现在可能增加了 plus() 操作到任何可以的类型 add()[注1] [1]

因此,如果 T 支持 add(T) 我的模板工作。如果没有, 只要我不使用,编译器就不会抱怨 plus()。在Python中 我们称之为 “鸭子打字”:*如果它像鸭子一样,像鸭子一样嘎嘎叫, 它  一个鸭子。*(当然,使用type_traits这有点修改, 但只要我们没有概念,这就是C ++模板的工作方式,对吧?)

我想,那是怎么回事 Java中的泛型 工作,不是吗?通用类型I设备用作“模板”如何操作我试图放在那里的任何东西,对吧?据我所知,我可以(或必须?)放一些 限制 关于类型参数:如果我想使用 add 在我的模板中,我必须声明类型参数 implement Addable。正确?所以,没有“鸭子打字”(无论好坏)。

现在,在 C ++ 我可以选择 专攻 在没有的类型上 add()

template<>
T plus<MyX>(MyX a, MyX b) { return a + b; }

即便如此 所有其他 类型仍然可以使用“默认”实现,现在我添加了一个特殊的实现 MyX  - 没有运行时开销。

有没有 Java泛型 具有相同目的的机制?当然,在编程中,一切都是可行的,但我的意思是 概念,没有任何技巧和魔力?


2666
2018-04-02 11:13


起源



答案:


不,Java中的泛型不能以这种方式工作。

使用泛型,如果没有泛型,你就无法做任何事情 - 你只需要避免编写大量的强制转换,并且编译器确保一切都是类型安全的(只要你没有得到一些警告或抑制那些) 。

因此,对于每个类型变量,您只能调用其边界中定义的方法(没有鸭子类型)。

此外,没有代码生成(除了为了实现泛型类型而委托给具有其他参数类型的方法的一些适配器方法)。假设你有类似的东西

/**
 * interface for objects who allow adding some other objects
 */
interface Addable<T> {
   /** returns the sum of this object and another object. */
   T plus(T summand);
}

然后我们可以创建我们的 sum 有两个参数的方法:

public static <T extends Addable<T>> T sum(T first, T second) {
    return first.plus(second);
}

静态方法被编译为相同的字节码(注释中包含其他类型信息):

public static Addable sum(Addable first, Addable second) {
    return first.plus(second);
}

这就是所谓的 类型擦除

现在可以为可添加类型的每两对元素调用此方法,如下所示:

public class Integer implements Addable<Integer> {
    public Integer plus(Integer that) {
       return new Integer(this.value + that.value);
    }

     // private implementation details omitted
}

这里发生的是编译器创建一个额外的合成方法,如下所示:

public Object plus(Object that) {
    return this.plus((Integer)that);
}

这个方法只能由具有正确类型的通用代码调用,这保证了编译器,假设你没有在某处做一些不安全的转换 - 那么 (Integer) 在这里施放将捕获错误(并抛出ClassCastException)。

sum 方法吧 总是 打电话给 plus 第一个对象的方法,没有办法解决这个问题。没有为每个类型参数生成代码(这个  Java泛型和C ++模板之间的关键区别),因此我们不能简单地用专门的方法替换生成的方法之一。

当然,你可以创建第二个 sum 方法如 irreputable 建议(带有重载),但只有在使用时才会选择此选项 MyX 直接在源代码中输入,而不是在调用时 sum 来自其他一些通用代码的方法恰好用MyX进行参数化,如下所示:

public static <T extends Addable<T>> product (int times, T factor) {
    T result = factor;
    while(n > 1) {
        result = sum(result, factor);
    }
    return result;
}

现在 product(5, new MyX(...)) 会打电话给我们 sum(T,T) 方法(反过来调用 plus 方法),没有任何重载 sum(MyX, MyX) 方法。

(JDK 7增加了一个新的 dynamic 方法调度模式允许在运行时通过每个参数进行特化,但Java语言不使用它,仅用于其他基于JVM的语言。)


8
2018-04-02 13:00



类型擦除方法应该是 Addable sum(Addable first, Addable second) - irreputable
@irreputable:谢谢,你是对的。我换了这个。 - Paŭlo Ebermann
关于Generic如何工作的很好的解释 sum。现在,我可以将其转换为我的旧Java知识。但是什么是exaclty 擦除 这里? “类型擦除”这个名称是什么意思? - towi
当然!超载专业化。我多么愚蠢:-)那就是ot。谢谢。 - towi
@towi:“类型擦除”是指每个泛型类型(可以是参数化类型,通用数组类型或类型变量)转换为非泛型类型的过程,因为VM只能直接使用这些类型。请参阅本节 4.6类型擦除 在JLS中。 - Paŭlo Ebermann


答案:


不,Java中的泛型不能以这种方式工作。

使用泛型,如果没有泛型,你就无法做任何事情 - 你只需要避免编写大量的强制转换,并且编译器确保一切都是类型安全的(只要你没有得到一些警告或抑制那些) 。

因此,对于每个类型变量,您只能调用其边界中定义的方法(没有鸭子类型)。

此外,没有代码生成(除了为了实现泛型类型而委托给具有其他参数类型的方法的一些适配器方法)。假设你有类似的东西

/**
 * interface for objects who allow adding some other objects
 */
interface Addable<T> {
   /** returns the sum of this object and another object. */
   T plus(T summand);
}

然后我们可以创建我们的 sum 有两个参数的方法:

public static <T extends Addable<T>> T sum(T first, T second) {
    return first.plus(second);
}

静态方法被编译为相同的字节码(注释中包含其他类型信息):

public static Addable sum(Addable first, Addable second) {
    return first.plus(second);
}

这就是所谓的 类型擦除

现在可以为可添加类型的每两对元素调用此方法,如下所示:

public class Integer implements Addable<Integer> {
    public Integer plus(Integer that) {
       return new Integer(this.value + that.value);
    }

     // private implementation details omitted
}

这里发生的是编译器创建一个额外的合成方法,如下所示:

public Object plus(Object that) {
    return this.plus((Integer)that);
}

这个方法只能由具有正确类型的通用代码调用,这保证了编译器,假设你没有在某处做一些不安全的转换 - 那么 (Integer) 在这里施放将捕获错误(并抛出ClassCastException)。

sum 方法吧 总是 打电话给 plus 第一个对象的方法,没有办法解决这个问题。没有为每个类型参数生成代码(这个  Java泛型和C ++模板之间的关键区别),因此我们不能简单地用专门的方法替换生成的方法之一。

当然,你可以创建第二个 sum 方法如 irreputable 建议(带有重载),但只有在使用时才会选择此选项 MyX 直接在源代码中输入,而不是在调用时 sum 来自其他一些通用代码的方法恰好用MyX进行参数化,如下所示:

public static <T extends Addable<T>> product (int times, T factor) {
    T result = factor;
    while(n > 1) {
        result = sum(result, factor);
    }
    return result;
}

现在 product(5, new MyX(...)) 会打电话给我们 sum(T,T) 方法(反过来调用 plus 方法),没有任何重载 sum(MyX, MyX) 方法。

(JDK 7增加了一个新的 dynamic 方法调度模式允许在运行时通过每个参数进行特化,但Java语言不使用它,仅用于其他基于JVM的语言。)


8
2018-04-02 13:00



类型擦除方法应该是 Addable sum(Addable first, Addable second) - irreputable
@irreputable:谢谢,你是对的。我换了这个。 - Paŭlo Ebermann
关于Generic如何工作的很好的解释 sum。现在,我可以将其转换为我的旧Java知识。但是什么是exaclty 擦除 这里? “类型擦除”这个名称是什么意思? - towi
当然!超载专业化。我多么愚蠢:-)那就是ot。谢谢。 - towi
@towi:“类型擦除”是指每个泛型类型(可以是参数化类型,通用数组类型或类型变量)转换为非泛型类型的过程,因为VM只能直接使用这些类型。请参阅本节 4.6类型擦除 在JLS中。 - Paŭlo Ebermann


不 - 但你的特殊问题更多的是超载问题。

定义2没有问题 plus 像这样的方法

<T extends Addable> 
T   plus(T   a, T   b) { .. }

MyX plus(MyX a, MyX b) { .. }

即使这样也行 MyX 是一个 Addable; javac知道第二个 plus 比第1更具体 plus,所以当你打电话 plus 有两个 MyX args,第二名 plus 被选中。从某种意义上说,Java确实允许“专用”版本的方法:

f(T1, T2, .. Tn)

f(S1, S2, .. Sn)

如果每个都很有效 Si 是一个子类型 Ti

对于泛型类,我们可以做到

class C<T extends Number> { ... }

class C_Integer extends C<Integer>{ ... } 

来电者必须使用 C_Integer 代替 C<Integer> 选择“专业”版本。


关于鸭子打字:Java在静态打字中更严格 - 除非它是鸭子,它不是鸭子。


3
2018-04-02 12:37



这只是重载,如果是,它不起作用 plus 方法是从一些泛型方法调用的 T 并呼吁 MyX。 - Paŭlo Ebermann


HI,

java Generics它与C ++模板不同。

例:

Java代码:

 public <T> T sum(T a, T b) {
  T newValue = a.sum(b);
  return newValue;
 }

在java中,此代码不起作用,因为泛型基类是类java.lang.Object,因此您只能使用此类的方法。

你可以像这样构建这个methis:

public <T extends Number> T sum(T a, T b) {
  T newValue = a.sum(b);
  return newValue;
 } 

在这种情况下,泛型的基础是java.lang.Number类,所以你可以使用Integer,Double,Long ecc ..

方法“sum”取决于java.lang.Number的实现。

再见


2
2018-04-02 11:25