一,前言
网上经常看到编程语言之争,大伙儿皈依到不同门派,各自怀抱信仰,时不时还发生点儿“冲突”。
这其中,C++和Java的优劣,十多年前就常吵的火热。然而时代在进步,技术在发展,满街早已是“云大智”了,现在还讨论C++ vs Java这个话题,似乎早已经过时了。
家鸽资质鲁钝学的慢,热门的知识没掌握,只能拿这个老话题炒炒冷饭。
参加工作的前两年,主要写Java;近些年由于工作需要,主要写C/C++了。也算是敝帚自珍吧,常觉得之前学会儿点儿东西不容易,舍不得丢掉。总会自觉不自觉的拿现在用的相互对比,也就粗略的总结了些东西,又怕记性不好以后忘了,就写个文字形式的记下来。
这个系列不敢说两种语言孰优孰劣,只妄图达成一个具体的目标:认真看过这些文字的人,以前写主写C/C++的,也能够写Java了;以前主做Java的,也可以尝试做C/C++了。这当然仅限于语言本身这个层面,框架与应用领域等范畴不在此列。与此同时,要是家鸽能够略微传达出一些观念,那就更好了。
二,形而上形而上谓之道。人在道中而不知道,如同鱼在水中却不知水一样。
本节以下为家鸽自己看法的总结,纯属一家之言,有不同意见欢迎拍砖。语言层面的“道”,是它的设计理念与发展思路。先看一下C++。它就是一个实用主义的大杂烩,有点儿吸星大法的感觉。
1979,Bjarne等人试图去分析UNIX的内核的时候,诞生了这门语言。它初期的目标,首先是在兼容C的前提下,引入“面向对象”思想。1984年,“C with class”才改名为“C++”。当时面向对象思想刚出来没多久,炒的火热,也确实能解决很多问题。“兼容C”这个大前提不光是兼容,显然是当时出于复用与实用性的考虑。导致它近乎完全吸纳了C的风格,可以面向过程,又能面向对象(这个对象显然不纯,不如称之为基于对象),于是语言特性和写法不可避免的增加了不少,相对而言显得有些复杂。面向对象是C++编程思想的一个重要的部分,但不是它最核心的思想,从来也不是。STL也于1979年创立,于1993年成为C++标准的一部分。泛型思维是STL的基础。当前的STL就是个以高度复用为目标的,以泛型思维为基础的,高层次的,系统化的,类别分明的C++库。早期的C++并不支持template,而是用宏来构建复杂的结构。但很快就引入了template,又有了新的风格与写法。如果去分析不同版本的STL源码,会发现其中没多少面向对象,都是在泛型思维指导下,使用template实现的具有高复用性低时间复杂度的精巧代码。早期的Java没有泛型,后来也有了(虽然二者机制与地位并不等同)。可以看到语言之间常常会相互学习各自的语言特性。性能的考量以及直接而灵活的资源操控与系统调用,是这门语言设计的又一目标。它本来就为分析与实现操作系统而诞生的语言,操作系统内核里不会讲什么面向对象,主要是各种资源操控与管理的策略及算法。在从始至终的发展过程中,所有新特性的引入,都不会违背这个性能与资源操控这个前提。由于这个原因,这门语言灵活而强大。有这么一个说法,C++的灵活与强大不在于它能实现多高层的抽象,而恰恰在于它可以不抽象,以及可以自如的控制自己抽象的程度,要是觉得C/C++语言本身这一层抽象也碍事,甚至可以在里面嵌入汇编。在C++的世界里,既可以面向实际问题的结构,建立抽象模型,又可以看到解决问题时基于的计算机的结构,实际项目研发过程中一个重要的设计,就是建立二者之间的映射关系,这个过程需要不菲的成本。在更高层面抽象的语言诞生之后(比如Java),依项目类型和需求而定,这种映射关系的充分建立也许就不是必要的了。也是由于这些原因,使得C++不容易掌握,更难以精通,不易维护,开发效率低下,容易引入严重的内存泄漏和宕机等问题。于是实际工作中也就有了各种版本的开发规范。11版本及之后的C++吸取了现代语言的新思想,譬如有人说lamda和std::function std::bind让它能够进行函数式编程,能支持高阶函数和链式调用。也许在正统函数式编程语言的支持者看来,这个“函数式编程”未必纯正。但对新思想的吸纳,给C++带来很多新的语言特性。活用种种新特性与新风格写的代码,看起来似乎成了一种新的编程语言。C++委员会一方面想让这门语言长盛不衰,给开发者提供更加便捷,安全的语言特性;另一方面必须保持它在已有领域的优势,能够相对灵活自由的操作底层资源;当然必须保持对历史代码的兼容,以前的特性不能丢。这中间免不了权衡取舍综合折中,就成了我们现在看到的C++。这种权衡与引入,形成了当前一个重要的趋势:在提高(至少保持)对资源灵活控制能力的前提下,提高抽象程度,提升编程的方便与程序安全性。但在语言快速发展的过程中,对旧特性老式风格的兼容,legacy代码的维护,大部分一知半解的程序员,以及各种编程范式与新老风格在实际研发中的无原则的混用,都如同重重迷雾一般,掩盖了这种趋势,C++项目看起来反而愈加复杂,甚至有些让人望而生畏。总体上,C++是面向过程、基于对象、泛型思维和新思想并重,还在不断发展的一门编程语言。Java的设计者看到了(当时)C++的各种问题,总结实际项目的研发需求,形成了一套高度抽象与精炼的语言特性体系集合,从研发的源头就规避掉这些问题(与此同时也限制了自身在底层的能力)。这个精炼集合写出来的代码未必精炼了,有不少人评价它的语言风格有些啰嗦,但相对而言这种问题就无伤大雅了。Java的语言特性相当精炼,容易掌握,以至于很大一部分Java程序员相对的不怎么讨论语言特性本身,能更多的面向实际问题,将主要精力放在各种框架,API,设计模式中。你当然可以说,C++项目那些问题是由于编程人员水平不够,而不是语言本身的问题。但在Java这一套新的语言体系中,更多的人经过短期学习,更容易高效的写出问题很少的代码,何乐而不为呢?
高度抽象,是Java的设计原则。Java本身就是抽象而完备的;并且基于它可以进行更高层面的抽象。这使得代码量少,维护性好,研发工作高效,安全性容易得到保证。纯粹的面向对象是Java语言核心的思想与原则,在Java的王国里“一切都是对象”,连基本类型都有对应的包装类。它的复用也是更多基于面向对象来实现的。更完备的抽象,完全的面向对象,意味着更完全的封装,程序员不再需要知道细节就可以完成任务,也可以把无关他人的细节更好的隐藏起来。当然在一些情况下,掌控底层成为一种必要,它就无能为力了。在Java的设计思想中,开发效率的重要性高于程序运行效率本身,这在计算资源不是主要约束的实现需求中,显然有很大优势。
质量与安全性也是重要的考虑,除了规避掉容易引入问题的语言特性,Java风格中的异常处理机制非常完善。很多时候同一段逻辑,Java的写法也相对单一,不够自如灵活,但安全性与质量更高。在自由与安全中,Java选择了安全。
Java的语言特性相对精炼,但它也有C++不具备的语言特性。这些语言特性或者更加面向对象,比如接口。或者使它更能适用于Web,并发(C++也引入了)等领域当中。本节的描述有些抽象,不易理解,至少不容易有切身体会。为了说明问题,后文中家鸽会用自己写的一些代码示例,C++代码用黑背景,Java用浅绿。这些示例仅为突出演示某些语言特性的“玩具例子”,尽可能的简短易懂,但并不适用于实际工程。三、 形而下
形而上谓之道,形而下谓之器。舍器而近道者,几稀!器,可以简单的理解为工具。编程语言本来就是工具,工具的区别在于使用方法。上图中,C++,C,Pascal都类似瑞士军刀,是用来做细活的工具。C语言的刀上有个USB,说明是可以做硬件操作的。C++刀是什么都有,说明C++是一种特性(过分)繁多的语言。Python是把电锯,面对大型的物体的修整,威力很大,人挡杀人,佛招杀佛,比C++/C/Java什么的得心应手得多得多,但是相对并不适合一些精细的调优工作。Java/C#是一把单刃工具刀,相对来说,其语法和使用相并不复杂。
不太关注技术细节的朋友,这一节后面的文字可以略过,直接看下一节。
这一节从语言具体的用法出发,来分析一下C++与Java二者的区别。可以粗分为三类:
Java不支持的C++特性
C++不支持的Java特性
C++和Java都有但是却不相同的特性
限于篇幅,本文只阐述前两种情况,第三种情况后续系列中再行阐述。
Java不支持的C++的特性Java不支持指针;
Java不支持预处理,也不再支持预处理指令(宏等等都没有了);
Java不支持typedef;
Java不支持goto语句;
Java不支持结构体与联合体;
Java不支持操作符重载(比如Java中的<>不再重载I/O操作);
Java不支持全局变量或全局函数;
Java不支持默认参数。
Java不支持析构函数。(Java增加了finalize()函数);
Java不支持delete操作符;
Java中,参数传递的方式只能是传值(“传值”这个说法有争议,且看后文详细解释。C++中可以传值、传指针或传引用);
Java不支持多重继承,即不允许一个子类继承多个父类。
............
下面挑几条典型的详细分析一下。
指针
最显然的区别是,C++支持指针,而Java不支持。
下面这个例子,将平面一个点,移动到与它原点对称的位置。
上面例子的写法都有些笨拙。比如可以用STL的copy()函数也许会清爽一些。这里只为比较"指针"这一点。
可以看到,C++有了指针,灵活的多也自由的多,在细节操作方面有更多掌控力;但代码也复杂了。比如*p++这种写法,不熟悉的话,总会纠结优先级结合方式之类。指针操作不当也有相当的风险,加之可读性下降,一般说来开发效率是有所降低的。
Java在设计之初,就把安全性与开发效率作为考虑的重要因素。在语法上去除指针,从根本上就避免了一些可能的质量问题。而且语法相对简单,容易上手,不用太过小心翼翼规避问题,开发效率相对也会高些。
对象的生命周期
标号为(1)的C++构造函数调用中,我们可以把u本身当作一个User对象。实际是在栈空间中构造的对象。这种构造方法Java中并不支持(基本类型除外)。标号为(2)的C++构造函数调用中,使用一个指针p指向堆空间中构造的对象。类似Java中引用q的作用。
也许在C++程序员看来,这没什么区别,只是悄悄的把指针藏起来了。换成了“引用”这一语法糖,底层还是用指针实现的。事实上,Java的引用与C++的引用有重要的不同(具体下一段中分析),“引用的底层也是指针”这一说法,也仅仅是对内部实现的一种假设模型,对理解某些问题更加方便,但把它理解为“别名”更为恰当。至于析构函数与finalize()函数,它们在机制上有着本质的区别。下面这个例子中,假设资源有限,该类型对象只能创建有限个,需要计数控制。C++栈空间的对象会及时析构并释放空间(堆空间的对象则可在主动delete时释放),Java的finalize()只能在系统进行自动垃圾回收时调用。程序运行时报错的原因是每次f()返回时垃圾回收并不出现。因此,没有调用finalize()方法,count的值也没有减少。在5次调用方法后,count到达其最大值。C++有析构函数,可以自主决定堆中对象的析构时机。Java没有这个机制。析构的时机不能自主掌控,代码优化空间相对有限。因而内存占用率等指标无法与精心优化过的C++代码相比较,性能也会多少受影响。视具体软硬件运行情况,垃圾回收时可能会出现若干毫秒的延迟,不能适用于一些资源有限或高度实时的应用场景。但Java写法简单,容易掌握。而且不容易出错。C++中忘记析构导致内存泄露,重复析构引发崩溃等问题,Java中极少遇到。让C++程序员饱受折磨的内存问题,在JAVA的GC机制下不再是什么问题了。质量与安全性有了保证。参数传递
家鸽喜欢的所谓“Java一切皆传值”的说法,对于基本数据类型,指的便是变量值的拷贝,而对于对象(或String、Integer等包装基本类型),指的是对象地址的拷贝,可以理解为所传递的值是对象的地址。
C++的参数传递有传值,传引用,传指针的用法,还可以传多重指针,搭配上const和参数的析构问题,相对复杂很多;C++程序员始终面临着什么情况下什么样的用法比较好这个问题,大神可以写出简明易用的代码,令人叹为观止,而大部分程序员工作多年也搞不清楚,导致项目代码混乱不堪,惨不忍睹。这些在Java程序员眼中都不是问题,没有引用指针等方式反而简单,用JavaBean之类的包装类传参轻松愉快不出问题,中规中矩,虽然有时候包装类太多也会显得不够简洁。
其他
C++不支持的Java特性
包
上面这个程序中定义了一个名为Module的名字空间,为了在main函数中使用名字空间中所定义的方法,使用using指令。
Java有现成的“包”,而C++程序的模块组织结构,更多需要自己设计与实现。不同项目的结构不一定相同。这在带来自由度的同时,也变的不及Java方便。接口
C++中的抽象类,是通过纯虚函数实现的,读起来相对不那么明确。而且有不为人知的用法,比如与大部分人的理解不同,虽然抽象类不可实例化,但纯虚函数其实是可以有对应的实现的,并且还有方法调用纯虚函数的实现(不要惊讶,家鸽这里没写错),而且这种写法也是有适用场合的。限于篇幅,这里不做深入描述。后续会专写一篇探究虚表的文章。这里只是举例,看一下C++语法的隐晦与诡秘。
Java中,抽象类和抽象方法必须用abstract关键字修饰,语法标志清晰明确。规定很明白也很容易理解:一旦类中包含了abstract方法,那类该类必须声明为abstract类;
抽象类不一定有抽象方法,有抽象方法的类一定是抽象类或者是接口;
抽象类不能实例化,只能被继承;
抽象类的子类,要么是抽象类,要么重写抽象类中的所有抽象方法。
几句话说明白,是不是很清楚,而且也藏不了什么特殊用法。二者的不同风格在这个例子中也有所体现。
另一个相关概念是继承。C++支持多重继承,因而有了菱形继承的问题和虚继承等等概念。这个用法很灵活,也有适用场合。比如需要适配的场景,可以用适配器模式解决问题。它分为两种,类适配器和对象适配器,类适配器用多重继承的方式比较合适。但多重继承引来的问题会很多,一般的规范里都不推荐用。这里也不写示例代码了。Java干脆不支持多重继承,但可以实现多个接口,可以通过实现多个接口模拟多重继承,达到类似的效果。这一节举例并在一些具体用法上进行比较,但总体上来说,二者的相似之处还是多于不同之处的;理解并熟悉那些不同之处,可以让你根据实际工作的需要,在二者之间自如切换,而不会在不知不觉中写出问题代码。限于篇幅,二者其他一些重要区别会在本系列后续展开分析。四、多与少
少即是多
前几日的国庆阅兵,我们看到了陆军,海军,空军,火箭军,也看到了雷军。中国互联网史的元老人物雷军,在改革开放民营企业家的彩车方阵中,站在花车的C位出席了庆典。
“专注,极致,口碑,快”是雷军总结的产品经验。雷军认为的“专注"主要是一条,“少即是多”。雷军曾看到,传统的软件研发项目过于复杂,干着干着就干偏了方向,所以一定要把事情简化到两、三个月就能做完,而且,这两、三个月做完可能就解决了某一些用户的最核心的需求,然后这件事情就可以干了。
专注,是很重要的一件事情,是每天需要不断地提醒自己的事情。“少就是多”,通常情况下,时间少,资源少,人也很不够,只能尽可能少做事情,找到关键点,以少胜多。用手机举例,在诺基亚和MOTO的时代,一个手机厂商一年要出60款,甚至100款手机,手机的型号都是很复杂的,没有人记得住哪个产品的名字。
但从苹果开始,产品型号简单的要死,每个产品型号都特别的简单,但是大家都记住了他的产品型号。“集中所有的精力做一款产品”的成功概率,肯定比“分散精力做100款产品”要高,所以,要集中所有的资源,认认真真做好一两款产品,这就是“专注”。
于是雷军认为"砍掉90%,只做10%,认认真真把那10%做好就足够好"。通常的产品经理,是这个也想做,那个也想做,最后看起来花里胡哨,实际什么都没做好。优秀的产品经理,不是把需求往多了搞,而是不断的砍“需求”,专注于最核心的事情。早期的建筑大师路德维希·密斯·凡德罗就提出:“Less is more。”这也是他坚持的哲学,他的设计作品中各个细部精简到不可精简的绝对境界,不少作品结构几乎完全暴露,但是它们高贵、雅致,已使结构本身升华为建筑艺术。
老外说的的"architecture"来源于建筑领域。很多优秀的产品,都是简洁的代表。一款好的软件,必然是在满足需求的大前提下,拥有高适应能力和低复杂度的产品。如何控制复杂度就是软件工程的一个核心问题,不论业务架构、功能架构还是技术架构。所谓”复杂“不是“深”,而是“多”。通常的软件常常带着太过繁多的功能项目,没有分类整理,有着太过冗杂的交互。设计人员似乎为了彰显功能的强大,恨不能让屏幕上满是按钮,找个东西很难,仔细分析才发现未必有什么特别厉害的功能。技术实现中需要一次性考虑太多的对象,对象和对象之前的关系也繁多混杂,两两间都可能存在着关联关系,如同一个满是跳线的电路板,维护修改越来越困难,质量也越来越难以保证,这样显然做出不好软件。反观Google的大神们,做的页面只有一个logo和一个搜索框,却可以在十几毫秒内中从全世界包罗万象的海量web信息中获取到相应主题的内容,并完成相关度计算排序等工作;其中的大数据存储、处理与检索技术高难艰深,架构设计却精巧清晰。是“少即是多”的典型代表。
通过上面“形而下”一节的分析,C++和Java的区别,语言层面上归结起来,C++是“多”,而Java是“少”。
C++的多,是有历史原因的。从他诞生那一刻起,它必须兼容C,又要面向对象(或者说基于对象)。随后的发展要求他支持泛型编程。为了保有市场占有率,确保已有领域的优势,它必须足够贴近底层;为了保持活力,又必须吸收现代语言的新特性;与之同时还要保证对历史代码的兼容......搞到后来,语言特性越来越多,理论上什么都可以干;而且一段逻辑,可以用多种写法或不同风格来实现。
什么都可以干,注定了它的强大灵活,作为技术人员,掌握了这种语言,如同手握利器,知识类比迁移起来也容易。但什么都可以干常常意味着什么都容易干不好。就项目而言,针对自己的需求与所属的领域,很多时候常常有更好的选择。
语言特性就越来越多,初学者的门槛也越来越高,很少有人敢说自己“精通”C++。这严重影响了它的使用与推广。
Java就是那个“少”。他的设计者吸取了C++语言在项目中遇到的问题,砍掉了容易引入出问题的语言特性,变的“少”起来;在语言层面,一些事情的做法也变的相对单一化了。让开发、维护与质量保证变得相对简单,研发效率大幅提升。虽然这样做也会损失一些性能,使之不再适合做一些领域的事情。但在它擅长的领域(网络,Web, 企业级应用等),它成为经久不衰的语言,并为后来者(如C#)所仿效。如果把语言当成一种产品,从受欢迎程度和使用率上来说,Java显然比C++要成功。不少C++程序员并没有多喜欢C++,而是项目类型和需求决定,必须得使用它。
通常3年的Java程序员,多在讨论各种框架,API,设计模式乃至架构思想,可以引入使用成熟的包搭建大数据存储与处理环境,进行分布式与高并发编程。而通常3年的C++程序员,对开发规范还没有充分理解,经常碰到不明白的语法特性,在各种奇怪的问题中被考验着细致与耐心。这就是“少即是多”。
对个人发展而言,艺不压身,在能够透彻而全面理解的前提下,“多”会有“多”的优势,但不能拿这个标准来要求所有人。对团队而言,新同学需要从“少”开始学起。从这个角度说,“C++规范”起的作用就是:在充分考虑项目需求与具体情况的前提下,通过限制容易出问题的特性和写法,让风格与特性由多变少,从而达到代码复杂度控制与质量保证的效果。在学习C++11及更新的标准时也可以看到,当代C++会不断的从其他语言中汲取思想和新特性。我们如果有个参照对比和宏观视角,就会更容易学习与理解。而对于全新的项目,最好是完全摒弃老旧的写法与特性,新同学更应该从新标准学起,使用一个少而美的子集。To Be Continued......