只是把我个人觉得一些比较有存在感的Java和C++的观念与异同点作一些讨论。
类相关的语法糖:
Java与C++有着同一个前辈——C语言,因此二者在语法上有着惊人的相似性,不论知晓哪一个,试图阅读另一个的代码都不会有什么大问题(尤其是C++11以后的C++代码,借鉴了大量的Java关键字),二者在一些细节上还是存在一些差异,下列列举的语法差异主要围绕class的细节以及一些相关关键字上。我个人觉得试图去硬记这些差异是不明智的,然而一些差异背后的意图仍然是值得思考与研究的。
1.Java任意class型别(8个原生类型之外的类型,Java一个特色在于把他们全部首字母大写了)的类型均为引用,但是最好把它理解成指针(适当的忽略语法细节)。
原因在于Java允许空引用,这对应的应该是指针,此外,引用必须在new后方可视为拥有对象,同样对应的是指针。另一个区别是没有delete,但这不属于类的问题。
2.每个物件(函数或者数据成员)单独赋予访问符——这属于细节差异,个人觉得Java的这个规则有些啰嗦了。对于没有章法的人,很可能会因为这个特性而在Java中肆意无序排列访问符,C++反而能从侧面约束他们,可能用Java的人有别的看法,这个问题仁者见仁。
3.final与const:我觉得这个差别不太好归类,毕竟C++的const到处都在用。总而言之,Java的final承担了对数据的const,对类(或者其中的函数)的不可继承语义。C++的const在修饰函数或变量时与final作用一致。针对不可继承语义,在C++11后引入了对类的final关键字。
4.构造函数:Java喜欢叫它构造器,这个东西在作用上二者几乎没有差异,一个比较特殊的地方在于Java的构造器可以调用同类中的同名构造器(被重载的那个),一开始我觉得这算一个创举,后来想想其实这个问题于C++也好解决,无非就是公共函数。但总的来说Java的这个设定还是很亲民的。
此外,Java拥有所谓初始化块的设定,在写法上提供了一定的灵活性,但其实和写进构造器效果是一致的。初始化块可以是静态初始化块,意义等同于C++类中的的静态变量定义。
5.默认初始化:就是直接在类中给成员赋初值,Java没有初始化列表,所以这个功能理所当然。C++同样在C++11后支持了这个功能,但所有教科书都会告诉你——等价于初始化列表,无需多做讨论。
6.拷贝:首先请参考第一条,因为Java的类变量实际上是引用,所以拷贝构造函数的地位很尴尬。Java提供了函数clone来完成深拷贝,值得注意的一点在于对返回值做赋值时如果忽略了这一点,会直接把内部成员(的引用)暴露出去,除非目的如此,否则往往会扔一个clone出去。对于学习Java的C++编码者来说,这个问题相当的具有迷惑性。
7.析构:Java有垃圾回收(GC),所以没有析构一说,C++则相反。这一条没什么需要解释的。关于要不要有GC,这同样属于仁者见仁的问题。
8.值传递:同样请参考第一条,Java的教材会反复强调并不存在引用传递,即使是类引用也会触发值传递。这对于C++来谈倒是很好理解(感谢指针),对于Java和C++均可以这样理解:原生类型执行变量的值传递,指针以及引用类型执行指针(C++会把引用退化为指针,对于Java还是引用)的值传递。
9.static关键字:一句话,C++存在面向过程的需求,所以static在C++中具有的额外作用均是与函数有关的——局部变量单次初始化,变量的单个文件内限制(对应extern),在类中其功能于Java是一致的。
10.一个关键区别——Java的class可以有一个main函数,而且一般被写成public static void main,意义不需要多解释,实际上,每个类都可以有一个main函数,而只有被直接使用的那个类会触发main。理论上C++可以这样写,但没有什么意义。
11.关于import:这个看完之后我惊讶地发现我也陷入了误区。C++如果试图使用一个库必须include,而java的import只是提供语法便捷(和using类似的功用),也就是说Java的编译器一开始就知道所有库(或者包)的情况,理论上可以提供列出全名而免去所有的import。
继承与多态:
面向对象的目的引入了继承体系以及与之密切相关的多态机制。Java和C++在实现上有着各自的考量。对于多态而言,本质为数据导向的函数调用,C++的具体实现是vtable与vptr。Java在这一点上异曲同工。就《深度探索C++对象模型》中的讨论来看,C++在多重继承与虚继承上花费了大量心思,而Java——直接舍弃了多重继承与虚继承。同时引入了接口用于实现类似多重继承的形式。
1.继承的语法差别:微不足道的区别,但是Java是没有多继承一说的,也就自然没有虚拟继承。这个问题我没有什么发言权,但是从我看到的东西来看,C++的多重继承收到的诟病多于褒奖。一个典型的例子便是stream相关的菱形体系,然而即使是《C++ Primer》作者本人也认为这个设计是存在弊端的。同时Java通过接口的设定侧面支持了多重继承(扩展)的概念。
2.多态的行为模式:简单的说,C++默认非多态,而Java默认多态。造成这个区别的原因依然可以参阅类语法部分的第1条——相当于Java每一次对类的方法调用都通过引用进行,引发多态也是理所当然。从我个人的感觉来看,试图向Java用户解释C++的多态规则远要比向C++用户解释Java多态规则要麻烦(当然这主要还是因为C++的多态太麻烦了)。
由于其默认多态,自然会引发的问题便是它如何采取非多态策略,解决办法是通过super来调用基类对应函数(python也是一样的规则)。此外,通过final关键字可以禁止对函数的子类覆盖,也是一个非多态的策略。
3.类型转换:首先两门语言在功能需求上是一致的,行为上各有千秋。C++的标准手段是dynamic_cast,这个函数兼具了判别和转型两个功能,用起来更加流畅。而Java则是提供了关键字instanceof来做一个类型的继承判别,因此Java一般会多写1-2行代码。这里我个人觉得这个关键字虽然少了一些方便,但多了一些泛用性。类型检查可能会处于别的目的,将其功能予以独立应该也是经过考量的结果。
Java的全部对象默认继承于Object,这个基类规范了一些必要的通用方法,这在抽象体系上提供了一些赏心悦目的保障。而C++并没有这样的设定,所有的指针可以转为void是C的解决之道,然而void几乎什么都做不了,实际上将工作都推给了开发者。
4.抽象类:主要是语法差异。Java提供关键字abstract来辨识抽象类和方法,C++则简单地用=0来标识纯虚函数,其拥有者为抽象类。
5.protected关键字:Java的protected在C++的基础上允许同一文件中的其他类对基类进行访问,这带来了一些问题。总的来说,Java的访问限制没有C++那么分明。
6.反射/RTTI:有的人会觉得Java有反射而C++没有。Java反射的实质是运行时类型识别(RTTI),而一门支持多态的语言理所当然的会支持这个特性。相对而言反射在写法上更好理解一些(RTTI本身就是一个很难简洁优雅的功能,说反射好理解也只是相对C++而言)。反射提供了getClass/Fields/Methods等一些类分析类的方法,能获取较为完整的类信息。C++RTTI的主要方法是tydeid(跨平台有差异),auto/decltype(C++11以后),原理均是依赖于类中隐式创建的typeinfo条目。
泛型解决方案:
泛型编程可谓是工程实践中的一大利器。C++实现了模板,并提供了STL的诸多组件用于支持泛型编程的种种要求。而Java拥有泛型体系以及对应的相关解决方案。两种语言在写法上存在着惊人的一致性,有趣之处在于二者在本质上存在着质的差异。由于Java编译——解释的机制使得其在泛型实现能够采用不同于模板的另一套方案。至于STL,我认为这即使抛开C++单独讨论也可认为是一大创举,至少对于今天的C++初学者而言,所写的代码中不含STL几乎是不可能的事情,更别说那些复杂的大型项目了。
1.Java没有模板实例化过程:所有区别均源于此。一个直观的例子:求和函数的整型与浮点数泛型实现:C++的模板会在使用之后为两种类型分别生成两份函数代码,换言之,所有类型在编译期被唯一确定,同时模板的实例化将引发代码膨胀,但保证了效率。Java的方式被视为擦除——并不提供实例化,而是将类型替换为Object(在擦除前检查类型,并按需求插入类型转换的代码),由于并不会生成多份代码(利用类型转换来过渡),自然不会出现代码膨胀。
2.Java引入了桥方法:我认为桥方法并不算一个闪光点,只是为了解决Java泛型漏洞的必然方案。当混合使用继承以及泛型时,擦除与多态会产生冲突,出现两个可调用的函数,同时诱使多态行为去调用那个错误的函数,为了回归正轨,桥方法应运而生。它在类型为Object的函数中去调用那个正确类型的函数,从而讲结果引回正确之路。这个名字确实也恰如其分。而C++由于通过实例化来构建代码,因此并没有桥方法的出场机会。
3.泛型与反射/模板与RTTI:这个问题似乎应该避免被讨论。我之所以写在这里,是觉得这两个属性恰好构成了某种奇特的互逆——一个试图消除类型,而另一个试图找回类型。然而不得不承认,这个过程太容易犯错了(书上也是怎么写的),于Java而言,泛型完成了类型擦除,反射可以认出这应当是一个泛型(但不能认出尖括号里实际写入的类型了),一个比较好的理解方式为:擦除这个行为本身被编译器记录下来了,因此反射能够找回一定的信息(虽然不一定有意义)。C++的RTTI在这里要简单得多,因为所有类型编译期唯一确定(可以认为,编译后的代码找不到模板的痕迹,虽然会出现由于实例化而导致的代码膨胀),decltype找到的必然是那个正确的类型。
4.STL与容器:STL可谓是C++的一大创举,包含了容器,算法,适配器等诸多工具,同时STL与模板密不可分。Java没有被称为STL(或类似称呼)的物件,java.util算是对应的库包。这一类工具的设计目的可谓相当明了,同时不论Java或C++的书籍都会鼓励你尽量使用他们来解决问题——而不是试图自己发明这些东西。一个好消息是,不论是C++还是Java,在这方面的研发都没有停滞,C++11开始引入了一系列新特性(functinal,lambda, variadic template)来强化STL,而Java也没有忘记其在多线程上的初心,扩展了一系列用于解决不同线程场景的同质异构容器。