C++和Java中的“通用”类型有什么区别?

175

Java有泛型,而C++则提供了一个非常强大的编程模型,使用template

那么,C++和Java泛型之间有什么区别呢?


2
https://dev59.com/C3VD5IYBdhLWcg3wRpiX - n611x007
13个回答

170

它们之间有很大的区别。在C++中,您不必为通用类型指定类或接口。这就是为什么您可以创建真正的通用函数和类,但要注意类型更加松散。

template <typename T> T sum(T a, T b) { return a + b; }

上述方法是将两个相同类型的对象相加,可用于任何具有“+”运算符的类型T。

在Java中,如果您想调用传递的对象上的方法,则必须指定类型,类似于:

<T extends Something> T sum(T a, T b) { return a.add ( b ); }

在C++中,泛型函数/类只能在头文件中定义,因为编译器会为不同的类型(被调用时)生成不同的函数。因此编译速度较慢。而在Java中,编译不会有太大的惩罚,但Java使用一种称为“擦除”的技术,在运行时泛型类型会被擦除,所以在运行时Java实际上是调用...
Something sum(Something a, Something b) { return a.add ( b ); }

然而,Java的泛型有助于类型安全。


29
他说得非常正确,这只是一种精心设计的语法糖。 - alphazero
46
这并不是纯粹的语法糖。编译器使用此信息来检查类型。即使此信息在运行时不可用,我也不会简单地将编译器使用的东西称为“语法糖”。如果您这样称呼它,那么C语言只是汇编语言的语法糖,而汇编语言只是机器码的语法糖 :) - dtech
52
我认为“语法糖”很有用。 - poitroae
8
你漏掉了一个重要的区别点,就是可以用什么来实例化一个泛型。在C++中,可以使用“template <int N>”并获得任何数字实例化所产生的不同结果,它被用于编译时元编程,如stackoverflow.com/questions/189172/c-templates-turing-complete 的回答。 - stonemetal
2
@EJP:你说泛型不需要指定子类或超类是正确的。但在这个特定的例子中,当一个方法被调用在这个泛型类型的对象上时,这个泛型类型必须有一个声明了这个方法的超类(或接口)。 - klaar
显示剩余6条评论

139

Java泛型与C ++模板非常不同。

基本上,在C ++中,模板基本上是一个较旧的预处理器/宏集合(注意:因为有些人似乎无法理解类比,我并不是说模板处理是宏)。在Java中,它们基本上是一种语法糖,用于最小化对象的样板铸造。这里有一个相当不错的介绍C++模板与Java泛型

为了阐述这一点:当您使用C ++模板时,您基本上正在创建代码的另一个副本,就像使用#define宏一样。这使您能够执行诸如在模板定义中具有int参数以确定数组大小等操作。

Java不是这样工作的。在Java中,所有对象都从java.lang.Object扩展,因此,在泛型之前,您会编写以下代码:

public class PhoneNumbers {
    private Map phoneNumbers = new HashMap();
    
    public String getPhoneNumber(String name) {
      return (String) phoneNumbers.get(name);
    }
}

因为所有的Java集合类型都使用Object作为其基本类型,所以您可以将任何东西放入其中。 Java 5发布并添加了泛型,因此您可以执行以下操作:

public class PhoneNumbers {
    private Map<String, String> phoneNumbers = new HashMap<String, String>();
    
    public String getPhoneNumber(String name) {
        return phoneNumbers.get(name);
    }
}

这就是Java泛型的全部内容:用于对象转换的包装器。这是因为Java泛型没有被精炼,它们使用类型擦除。这个决定是因为Java泛型出现得太晚了,他们不想破坏向后兼容性(一个Map在任何需要调用Map的情况下都可以使用)。与之相比,.Net/C#中没有使用类型擦除,这导致了各种差异(例如,您可以使用原始类型和IEnumerable和IEnumerable彼此没有关系)。
使用泛型编译的类在Java 5+编译器上可用于JDK 1.4(假设它不使用任何其他需要Java 5+的特性或类)。
这就是为什么Java泛型被称为语法糖的原因。
但是,这个决定对泛型的实现有深远的影响,以至于(极好的)Java Generics FAQ已经涌现出来回答人们对Java泛型的许多问题。
C++模板具有许多Java泛型不具备的功能:
  • Use of primitive type arguments.

    For example:

    template<class T, int i>
    class Matrix {
        int T[i][i];
        ...
    }
    

    Java does not allow the use of primitive type arguments in generics.

  • Use of default type arguments, which is one feature I miss in Java but there are backwards compatibility reasons for this;

  • Java allows bounding of arguments.

    For example:

    public class ObservableList<T extends List> {
        ...
    }
    

需要强调的是,使用不同参数的模板调用确实是不同的类型。它们甚至没有共享静态成员。在Java中不是这样。

除了泛型的差异外,为了完整起见,这里有一个C++和Java的基本比较(以及另一个比较)。

我还可以建议一下《Java编程思想》。作为C++程序员,很多概念像对象这样都是很自然的,但是也存在一些微妙的差异,因此即使您只是浏览一些部分,拥有一本入门文本也是值得的。

当学习Java时,您将学到的很多都是库(包括标准库 - JDK中的内容 - 和非标准库,其中包括常用的东西如Spring)。Java语法比C++语法更冗长,而且没有许多C++特性(例如运算符重载、多继承、析构函数机制等),但这并不严格意味着它是C++的子集。


1
它们在概念上并不等同。最好的例子是奇妙的递归模板模式。第二好的例子是面向策略的设计。第三个例子是C++允许在尖括号中传递整数(myArray<5>)。 - Max Lybbert
1
不,它们在概念上并不等价。这个概念有一些重叠,但不是很多。两者都允许您创建List<T>,但仅限于此。C++模板可以做得更多。 - jalf
5
需要注意的是,类型擦除问题不仅仅涉及到针对 Map map = new HashMap<String, String> 的向后兼容性。由于字节码的相似性,这意味着您可以在旧的 JVM 上部署新代码,并且它将能够运行。 - Yuval Adam
4
模板代码与复制粘贴非常不同。如果你从宏展开的角度来思考,迟早会遇到像这样微妙的错误:http://womble.decadentplace.org.uk/c++/template-faq.html#type-syntax-error。 - Nemanja Trifunovic
1
Java泛型不是语法糖,因为它们扩展了Java类型系统。虽然JVM的限制阻止了它们提供其他语言从参数多态性中获得的效率优势,但这只是一个实现问题。 - dfeuer
显示剩余8条评论

99

C++有模板,Java有泛型,看起来有点像C++的模板,但它们非常不同。

正如其名称所示,模板通过提供可以用于填充模板参数以生成类型安全代码的(等待它……)模板来工作。

据我所知,泛型是相反的:编译器使用类型参数来验证使用它们的代码是否类型安全,但生成的代码根本没有类型。

把C++的模板想象成一个非常好的宏系统,而将Java的泛型视为自动生成类型转换的工具。

 


5
这是一个相当好的、简明扼要的解释。我想做的一个微调是,Java泛型是一种用于自动生成类型转换的工具,保证是安全的(在一些条件下)。从某种意义上说,它们与C++中的const有关。在C++中,除非将const强制转换掉,否则对象不会通过const指针被修改。同样,在Java中由泛型类型创建的隐式转换保证是“安全”的,除非在代码中手动转换类型参数。 - Laurence Gonsalves
将C++模板视为一个非常好的宏系统,严重低估了C++模板的威力。 - Nitesh
巨大?我不能同意。模板比真正好的宏更有用的情况是少之又少。 - barneypitt

17

C++模板有而Java泛型没有的另一个特性是专门化。这允许您针对特定类型拥有不同的实现。因此,您可以例如为int拥有高度优化的版本,同时仍为其余类型拥有通用版本。或者对于指针和非指针类型可以有不同的版本。如果您想在传递指针时操作取消引用的对象,则这非常方便。


1
+1 模板特化对于编译时元编程非常重要 - 这种差异本身使得 Java 泛型的效力大打折扣。 - Faisal Vali

14

Java中的泛型类似于C++中的模板,语法故意相似但语义不同。在语义上,Java泛型是通过擦除来定义的,而C++模板则是通过展开来定义的。

这个主题有一个很好的解释,可以在Maurice Naftalin和Philip Wadler所著的Java Generics and Collections一书中找到。我强烈推荐这本书。

请在此处阅读全文解释。

alt text
(来源: oreilly.com)


5
基本上,据我所知,C++模板为每种类型创建代码副本,而Java泛型使用完全相同的代码。
是的,你可以说C++模板等同于Java通用概念(虽然更正确的说法是Java泛型等同于C++概念)。
如果您熟悉C ++的模板机制,则可能认为泛型类似,但相似之处是肤浅的。泛型不会为每个特化生成新类,也不允许“模板元编程”。
来自:Java Generics

4

Java(以及C#)的泛型似乎是一个简单的运行时类型替换机制。
C++模板是编译时构造,它给你一种修改语言来适应你需求的方式。它们实际上是一种在编译期间由编译器执行的纯函数式语言。


3
下面的答案来自于书籍《Cracking The Coding Interview》第13章的解决方案,我认为非常好。
Java泛型的实现基于"type erasure"的思想:在将源代码转换为Java虚拟机(JVM)字节码时,该技术会消除参数化类型。例如,假设您有以下Java代码:
Vector<String> vector = new Vector<String>();
vector.add(new String("hello"));
String str = vector.get(0);

编译时,该代码会被重写为:

Vector vector = new Vector();
vector.add(new String("hello"));
String str = (String) vector.get(0);

Java泛型的使用并没有真正改变我们的能力;它只是让事情变得更加美观。因此,有时将Java泛型称为“语法糖”。

这与C ++完全不同。在C++中,模板本质上是一个被吹嘘的宏集,编译器为每种类型创建模板代码的新副本。这一点的证据在于MyClass的实例不会与MyClass共享静态变量。但是,两个MyClass的实例将共享静态变量。

/*** MyClass.h ***/
 template<class T> class MyClass {
 public:
 static int val;
 MyClass(int v) { val v;}
 };
 /*** MyClass.cpp ***/
 template<typename T>
 int MyClass<T>::bar;

 template class MyClass<Foo>;
 template class MyClass<Bar>;

 /*** main.cpp ***/
 MyClass<Foo> * fool
 MyClass<Foo> * foo2
 MyClass<Bar> * barl
 MyClass<Bar> * bar2

 new MyClass<Foo>(10);
 new MyClass<Foo>(15);
 new MyClass<Bar>(20);
 new MyClass<Bar>(35);
 int fl fool->val; // will equal 15
 int f2 foo2->val; // will equal 15
 int bl barl->val; // will equal 35
 int b2 bar2->val; // will equal 35

在Java中,静态变量是在MyClass的所有实例之间共享的,无论不同类型参数如何。

Java泛型和C++模板有许多其他的区别,包括:

  • C++模板可以使用原始类型,例如int。而Java不能,必须使用Integer。
  • 在Java中,您可以限制模板的类型参数为某种类型。例如,您可以使用泛型来实现一个CardDeck,并指定类型参数必须扩展自CardGame。
  • 在C++中,类型参数可以被实例化,而Java不支持此功能。
  • 在Java中,类型参数(即MyClass中的Foo)不能用于静态方法和变量,因为这些会在MyClass和MyClass之间共享。在C++中,这些类是不同的,所以类型参数可以用于静态方法和变量。
  • 在Java中,MyClass的所有实例,无论它们的类型参数如何,都是相同的类型。类型参数在运行时被擦除。在C++中,具有不同类型参数的实例是不同的类型。

3
C++模板的另一个优点是特化。
template <typename T> T sum(T a, T b) { return a + b; }
template <typename T> T sum(T* a, T* b) { return (*a) + (*b); }
Special sum(const Special& a, const Special& b) { return a.plus(b); }

现在,如果您使用指针调用sum函数,将调用第二个方法;如果您使用非指针对象调用sum函数,则将调用第一个方法;如果您使用Special对象调用sum函数,则将调用第三个方法。我认为这在Java中是不可能的。


2
可能是因为Java没有指针..!! 你能用更好的例子来解释一下吗? - Bhavuk Mathur
@BhavukMathur我觉得Keithb是指你可以使用模板重载方法。有点像“通用”重载。指针只是一种示例类型。 - Shmuel Kamensky

2
我会用一句话来概括:模板创建新类型,泛型限制现有类型。

3
你的解释非常简洁明了!对于那些已经了解该主题的人来说,这完全有意义。但对于尚未理解该主题的人来说,它并没有太大帮助。(这是任何在SO上提问的人的情况,你懂吗?) - Jakub

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