我和JAVA以及eclipse

March 30, 2018

我虽然算不上是老程序员,但我使用JAVA 也有一段的时间了,这段期间,有不少的话想写下来。

第一次接触

第一次使用JAVA来写程序,是在大一那会儿的校科协授课上。他们教我使用JAVA的基本方法,那个时候,别说面向对象了,就连面向过程的代码我基本都没有接触过。所以,我只觉得,哇~,JAVA似乎好厉害的样子。像甚么class,什么extends,完全搞不懂。

第二次接触JAVA,是1年半之后的事情了。那个时候自己也有了C语言和C++的基础。再次接触JAVA,感觉JAVA只不过是一个表层封装的C++。从C语言转JAVA,我的感觉就像是当年我从DOTA转到了LOL。

比较JAVA和C或C++的基本程序入口:

C和C++

int main(){...}

JAVA

public class __CLASS_NAME__{
   public static void main(String[] args){...}
}

JAVA为什么要给入口函数之外加上一层封装,代码写多了就隐隐感觉出其中的道理。

再看看基本文本终端输出函数:

printf(...);
System.out.print(...);

感觉JAVA似乎是比C语言难不少,就我而言,我感觉JAVA并不是一种适合初学者学习的编程语言。如果你是一名初学者,我强烈的建议你不要去学习JAVA,C++这样子的高级语言,C语言的指针也许很难,但是,如果你真的掌握了,你就会发现JAVA,C++里面的那些高级特性,不过是对指针的封装。C语言如果真的掌握得好,对于学习JAVA是肯定没有坏处的。

因为:

JAVA的简便之处,不在于输出HELLOWORLD,而是他的内存自动释放和class的写法。

面向对象,面向物件即OOP。他的出现,应该是从大型项目中产生的,因为使用class,程序的扩展会非常的方便。

用C++写面向对象时,最怕自己忘记做的事情就是new了之后忘记了delete,虽然这个错误在大部分情况下都不会发生。但是当在写动态内存相关的程序的时候,这是经常犯的错误,而且是逻辑错误。这种错误我想不管多么厉害的语法检测工具都不可能说100%帮你查出来。造成的结果就是内存不断的被使用,最后内存枯竭,系统蓝屏。这样的错误要查起来,基本就是大海捞针。

自从我真有一次让我的系统蓝屏了之后,我就很少再写过动态内存的函数了。宁可开个无比巨大的array,也不想冒这样的险。

而JAVA就不一样,你完全可以不管内存的释放。据说JAVA虚拟机会自动帮你完成你需要做的这项工作。可以让程序员集中精力在程序的核心部分,而不是浪费时间去找内存错误这样的bug的原因。

然,JAVA的优点似乎也就到此为止了。

以前我写JAVA,因为基本不会写什么大项目,就写个HelloWorld之类的小程序,也用不着什么IDE,用用javac就足够了。后来,因为写的项目越来越大,渐渐也感觉到javac有点力不从心,就接触了eclipse。开始觉得还挺好用的,至少比javac好用。似乎也不比visual studio差。

但我刚把话说完,我就知道我错了。就现在的我,我感觉,自己估计是再也不会去使用eclipse,也再也不会去用JAVA写程序了。因为,说实话,JAVA和eclipse的体验让我非常失望。

注释中的错误

我第一次见到可以在注释中报错的编程语言。恩,你没看错,是在comment中报错,是报错,真的是报错。我当时完全不知道该用什么样子的词语去形容我的心情,居然在注释里给我报错!

注释,自古就是程序员逃避编译检测的最好的手段,而JAVA居然会去检查注释里面的东西,而且,还会给我报错?!如果只是警告,我也许还能接受。但它非得显示出自己无比严格的语法检测能力,在我的注释中横行霸道。

我特意去网上看了一下,遇到这种情况的人,还不只我一个。但是,回答的人却告诉我,这是JAVA的标准。请遵守他。

算了,我已经没有精力去纠结这些破事了。不要为了JAVA的一个注释,而累得半死。

那个给我报错的语句如下:

/* copy: Fundamental-2D-Game-Programming-With-Java-master\CH11\src\javagames\util\utility.java */

报错的地方是\u,JAVA的答复是,\u后面必须接4个数字,因为会把他们解析成一个unicode的编码字元。

其实在C和C++定的char型中也可以通过\u+四个数字进行设置,比如

wchar_t ch = "\u1234";

但是,就算你后面没有跟四个数字,编译器也只会给你个警告,而不像JAVA,他写在注释里都给我报错。

之后,第二个问题又来了。

if语句中的整数

我们都知道,JAVA是不允许你在应该写逻辑表达式的地方写赋值表达式的。C和C++则会去判断赋值表达式所设置的值是不是0。

这样的语句在JAVA里会直接报错:

if(1){...}

这有好处,也有坏处。仁者见仁,智者见智。我还是想多说几句:

在C语言中,我经常会使用这种写法来写逻辑表达式。就算偶尔写错了,查代码花的时间也不会超过10分钟。相比查内存泄漏那样可能要花上十几个小时,这种错误的代价其实很小。而且在某些时候,把赋值和判断写在一起,代码的简洁度和风格会更好看。

JAVA强制要求程序员必须写逻辑表达式,这样的规定我觉得并非总是必要。我从来没有因为在逻辑表达式里写了赋值而感到困惑过。这样的要求,对于代码的可读性,我认为帮助有限。当然,它可能有利于新手程序员养成良好的习惯。但当他们以后去读一些底层代码,比如UNIX内核时,可能会发现风格差异带来的冲击。

所以,这个要求作为一种标准是有好处的,我也尽量要求自己不要在逻辑表达式里写赋值。但如果把它作为强制要求,就显得有些过度了。

还有第三个问题。

省略return的错误

我在用Visual Studio写C++代码的时候,都会习惯性地在开头添加一句代码:

#pragma warning(disable:4716)

这句代码的作用是,让所有关于“应该有返回值的函数却没有写return”的错误静默。

你可能会问,为什么不给那些函数写上return?因为它们的返回值根本没有被用到。我又何必浪费时间去加上完全没有意义的return 0这样的语句呢?

再问,既然返回值没有被用上,为什么不直接把它们设定成void型?因为现在没有用上,不代表以后不会用上。你知道重构代码的痛苦吗?既然如此,在一开始就把接口设计好不是更省事吗?其实printf函数也有返回值,你用过它的返回值吗?大多数人没有吧?但谁知道哪天会不会用到呢。所以,在一开始就设定成有返回值,总是没有错的。

而对于C语言,就算你声明了函数的返回类型为void,但还是写了个return 0语句,编译器也不会抱怨。

这是因为在通常情况下,C和C++会将函数的返回值保存在EAX寄存器中,而之后的函数如果想要获取到之前调用的返回值,只需要查看EAX寄存器中的值就可以了。所以,不管你函数有没有写return,又或者函数声明为void,这些都无大碍。这些其实都是标准约定,只是用来参考的罢了。如果你足够厉害,自己写个编译器,把返回值保存在MMX寄存器中,也没人会说你错。

那么,对于应该有返回值的地方却没有return语句的情况,JAVA是怎么处理的呢?

报错,报错,报错

而且,JAVA的错误提示比Visual Studio明显得多。VS顶多是在出错的地方显示一条很短的红色下划线,然后在右边的滚动条上标记一个红点。有时候我还得仔细找才能发现。而Eclipse却是一条长长的下划线,明显得不能再明显,生怕你看不到。这让我这种有强迫症的人非常难受。不过我也没有再去抱怨,因为接下来,还有第四个问题。

两个return的错误

我在debug代码的时候会为了简便,而强制一个函数返回我们想要的值。但是,我们又不想破坏整个辛辛苦苦写出来的函数,所以,我一般都会在函数的开头写一个return __HOPE_VALUE__。代码侦错中叫做打桩。我想,只有新手才会在编译成release版的时候忘记删除这样子的语句。

然而,JAVA却会很机智地告诉我:你写在return __HOPE_VALUE__后面的语句是不可能被执行的,你要把后面的语句删掉,不然我就报错。

我只是为了方便debug而写的一个return,结果整个写程序的心情都被这种严格的编译器提示打断了。难道我会不知道return后面的代码不会被执行吗?这种提示让我觉得自己被当成了小学生。

我感觉自己回到了用vb6.0写代码的时候。每一次我写if忘记了写then时,编译器都会弹出一个警告对话框。那个时候,我还是一个编程新手,所以会觉得,哇~~这个IDE好贴心啊。

但是,时隔多年,我已经从新手变成了“老司机”。如果我还在学习vb并且还在用vb6.0写代码的话,我可能会默默地把右手从键盘上拿开,凭着直觉和眼睛的余光伸向放在键盘右边的鼠标,准确无误地把食指放在鼠标的左键上,眼睛看着屏幕上的箭头,右手缓慢地移动,把箭头移到警告对话框OK按钮所在的位置,按下食指,然后松开。之后,WIN+X+P -> P R O -> Enter -> wait -> click “Name” -> find vb6.0 -> double click -> click “yes”。这套操作在Windows 8.1中就是卸载vb6.0。

因为,这一连串的动作做完所花费的时间,我想,足够我的双手敲击键盘至少10下。我们程序员的命,就是在这不知不觉中被抹杀了。还好现在vs2013内的vb编辑器,已经可以帮你自动在if后面把所缺的then补上了,而且不会再弹出吓人的警告对话框。

回到JAVA的问题,JAVA会告诉你,你return后面的语句不会被执行,并且不允许你的代码编译。如果我是一个刚刚接触编程的孩子,我可能也会像以前用vb6.0的时候那样,觉得这个IDE很贴心。但现在的我,只会觉得它过于啰嗦。也许这是为了帮助新手养成习惯,但对于已经熟悉代码逻辑的人来说,这种强制要求反而显得多余。

我想起了一句话:每一个辅助,都要有一个家长的心,去包容ADC的错误。确实,我写的语句不符合标准,但是,所有的标准,都只不过是一个参考罢了。不管是ASCII标准,ISO标准,它们都只是参考。真正如何做,是取决于自己。

烦人的自动完成

你以为JAVA的问题只有这么一点?如果真的是,那么,也许我会怀着更大的耐心去接受它。毕竟,它的内存释放和class的写法,确实比C++方便。可惜的是,它的“搭档”eclipse,却让我最终选择了放弃。

一个顺手的IDE总能让人心情愉快,我想,所有的程序员都会有这样的感触。我喜欢把IDE设置成自己习惯的样子,这样写起代码来才会觉得熟悉和自然。

所有的IDE,不管是eclipse还是VS,我拿到手的第一件事,就是把代码自动完成关掉。vc6.0除外,因为它根本没有这个功能。

大一用vc6.0的时候,我感觉非常辛苦。如果vc6.0能够提供自动完成功能,那个时候的自己也许会觉得轻松一些。可惜它没有。就这样,在vc6.0的陪伴下,我度过了一年,接受了一年的“磨炼”。

后来接触了VS,有一次我写完一条语句,在最后按下;时,VS很机智地把所有应该有空格的地方都补上了。我的感觉是:哇~这样代码看起来真漂亮。不过这种新鲜感没有持续多久,不到一个月我就把VS的自动完成关掉了。虽然它能让代码更整齐,但如果代码出了问题需要修改,我肯定不会再去输入;让它帮我格式化。而每一次Ctrl+A -> Ctrl+K Ctrl+F 都会浪费不少时间。更重要的是,代码里混杂两种不同的风格让我很不适应。即便我原本的风格不算完美,也比风格混乱要好。再加上我渐渐习惯用tab来对齐,所以我关闭了VS中所有的自动完成。

从那以后,我接触过的每一款IDE,都会选择关闭它的自动完成。不管是[](){}<>的尾括号补全,还是代码的自动格式化。作为程序员,敲一个符号不过是瞬间的事。我们每天写上百行代码,还在乎一个括号的工作量吗?而自动格式化往往会破坏原本的代码风格。

所以,拿到JAVA的时候,我也费了不少功夫,关闭了它几乎所有的自动完成。但有一点,我始终关不掉:当你输入/*并按下回车后,它会自动在下一行开头加入一个*

/*
 *
 *

我承认,这样的注释风格很漂亮。

但我想问,如果没有IDE的支持,这样的代码维护量有多大?按表格来对齐的代码固然好看,但并不是每个程序员都喜欢,原因就是维护成本太高。程序员不仅要写代码,还要花时间用tabspace去对齐,这非常浪费精力。而IDE凭什么强制我们接受这样的习惯?虽然现在它帮我完成了,我暂时没什么负担,但以后呢?我还要一直依赖它吗?我找遍了所有设置,都没有找到关闭这个自动完成的选项。我还特意去Google了一下,发现遇到同样问题的人不少,但也没有解决办法。最后,我只能忍了。大不了再手动把它加上的*删掉就是了。

不过,让我头疼的,还不止这一点。

不支持虚空行的IDE

基本上所有的IDE都会允许“虚拟尾空行”,英文叫 scroll past end,也就是允许你把最后一行的代码滚到屏幕上方,让自己在合适的位置继续写代码。讲道理,这项功能应该是所有IDE的标配,甚至vc6.0都有支持。但是,eclipse,却没有?!我一开始还以为是默认关闭,找了半天设置,结果发现根本没有!我又去Google了一下,发现这并不是我一个人的错觉,很多人都有同样的困惑。这难道是个BUG?

IDE的小问题,往往会让程序员非常失望。就像我曾经因为vs2013的一个右括号对齐问题而差点想换掉IDE。而eclipse,你真的让我无语。提供了代码折叠功能,却不允许折叠到class级别;也不能记住上一次我折叠的位置,每次打开都要重新折叠一遍。你就不能学学VS吗?还强制在下面放了一个足足能占两行代码高度的状态栏,低调一点不好吗?像VS、Sublime那样,只显示几行几列的位置就够了。还有你的双列并排显示,占据了我宝贵的代码显示空间,却没有什么实际作用。

而我也是,居然花了半天时间来写一篇抱怨IDE的文章。

怎么说呢,算是“恨铁不成钢”吧。JAVA本身我还是挺喜欢的,但它一次次让我失望。最后,当我用eclipse写完了我的JAVA大作业后,我的想法是:也许,之后,我再也不会用JAVA了。

而对于JAVA,我还有一点疑惑。也不知道是我太年轻,还是JAVA本身确实有一些让人难以接受的地方。

真的有必要放在第一行吗?

JAVA和C++一样,有一种机制叫做继承,但是,JAVA却有一个严格的要求:Constructor call must be the first statement in a constructor。意思很简单,就是,你如果想用你的子来初始化你的父,那么,你必须要把初始化父的代码写在除了注释以外的第一行。

举个例子,

class father{
    father(int a){};
}
class child extends father{
    child(){
        int a;
        super(1);//构造父
    };
}

这样的代码是不可以通过的。因为,构造父的代码不是子的构造函数中的第一条语句。你必须要把a的申请放到super(1)后面。

class child extends father{
    child(){
        super(1);
        int a;
    };
}

但是,这样的代码是可以编译过的:

class child extends father{
    child(){
        super(1+2);
    };
}

甚至可以加入一个add函数让他编译通过﹕

int add(int a,int b){
    return a+b;
}
class child extends father{
    child(){
        super(add(1,2));
    };
}

而这样的代码却不行:

class child extends father{
    child(){
        int a = 1+2;
        super(a);
    };
}

我真的不明白为什么,是因为在构造父之前,申请了内存会导致内存对齐的问题?我想不明白。

有前辈曾说过,不要认为你比编程语言的创造者或写头文件的人聪明,那怕只有一点点。

所以,我感觉,JAVA设置了这样的强制性规定,一定是有它的原因的。但是,“Constructor call must be the first statement in a constructor"这样的理由,我还是觉得难以接受。

在论坛上我看过其他程序员的发言,他们说,在编译器生成的字节码文件里面是没有这样子的规定的。难道?这又是JAVA的特殊要求?