java的“值传递” + 异常抛出

方法调用(call by)(相当于c++中的“函数调用”),根据参数传递的情况分为值调用( call by reference ) 和引用调用( call by value ) 。

Java的对象参数传递仍然是值调用,这里强调的是“对象”的参数传递。

Java中除了primitive type以外一切都是对象。primitive type只有以下几种:QQ图片20151027211946

注意,就连我们熟知的数组或者String也都是对象。Java没有指针,所以对primitive type实现著名的swap函数就显得有些困难,一种可行的做法是先特地把它们包装成对象。

 

Java中对象变量名实际上代表的是对象在堆中的地址(专业术语叫做对象引用 )。在Java方法调用的时候,参数传递的是对象的引用。重要的是,形参和实参所占的内存地址并不一样,形参中的内容只是实参中存储的对象引用的一份拷贝。这正是“值传递”的表现形式。

通过传递对象引用,形参和实参指向了同一个地址。所以修改形参的成员变量,相当于修改了实参的成员变量

 

试着对一个对象实现swap。

交换失败。前面说到形参和实参都是对象引用,本质上是引用,那么上面那段代码只是把形参的两个引用交换了一下(相当于c++中,只是交换了指针,而不是交换指针所指向的内容),对实参的情况毫无影响。下面用图给出直观的描述:QQ图片20151027213817

由此可进一步理解,为什么说JAVA本质上仍然是值传递。

 

下面讲讲异常抛出。

若要实现以下功能:

QQ图片20151027214740

第一种方法,使用异常抛出:

第二种写法,不使用异常抛出:

显然第二种写法更简明清晰,也是我在学异常抛出之前做错误处理一贯的方式。这难道说明异常抛出反而是累赘吗?不是的,这实际上是一个失败的例子。

可以看到,在第一种写法中,既没有用到JAVA的“重新抛出异常”(就是在这个类的方法内部不处理异常,而是交给调用它的那个类的方法去处理,从里面一直往外抛, 简单说就是谁调用谁处理异常,我不管),而且也只有一层代码,事实上这种情况下很少强行使用异常抛出。

异常是有层级关系的错误处理机制,是专门抽象出来的一个用来处理错误的语法。子函数在出现异常的时候,可以自己处理掉,也可以抛给调用者处理,还可以自己处理一部分调用者处理一部分。处理异常的代码可以非常清晰地与逻辑主体隔绝开来。并且,Exception被捕获的话就不会终止程序,相比之下error则会使之宕掉。

可以设想这么一个场景,你在某个地方调用一个函数,这个函数的作用,是接受学生成绩并做相应的处理。但是,如题所说,输入的学生成绩可能是错误的。那么你在调用的地方怎么获取这个信息呢?两种办法,一种是用返回值表征状态,一种是用异常处理机制。此例中返回值可以有两个枚举值,0表示正常,1表示成绩不在0到100的闭区间内。

然而对于更一般的情况来说,返回值能表示的信息很有限,也就是,只能表示错误状态,却难以将更多的错误信息表达出来。比如如果接受的是一个数组,那么你可能还需要知道脱离0到100范围的数的下标是多少。出现别的错误时,你可能又想获取别的信息,这种情况下返回值就很难做到了。

所以后来我把代码改成了这个样子:

这样才不会背离“异常处理”的原则太远。

 

初初接触异常抛出,除了明白基本概念,我感觉自己还需要知道什么时候该用以及该怎么用,在网上找到了相关有营养的建议。

1.只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程。谨慎地使用异常,异常捕获的代价非常高昂,异常使用过多会严重影响程序的性能。

2.切忌使用空catch块。千万不要使用空的catch块,空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。如果你非常肯定捕获到的异常不会以任何方式对程序造成影响,最好用Log日志将该异常进行记录,以便日后方便更新和维护。

3.不要将提供给用户看的信息放在异常信息里。因为业务逻辑跟用户界面要解耦合,展示给用户的错误提示信息若写死在里面了,万一要改成英文版的,不是又得改下程序;万一你觉得这个promt不合适,想换句好听点的话,又得改下程序;万一你要支持多语言,你还得在代码里面敲进N多种语言,再用一个swtich去拿…

其中一种做法是拿一个 txt 文件或者 ini 文件之类的,把错误编码跟错误提示文本存起来。然后主程序再跑去文件里面根据错误编码找到具体的对应提示文本,这就是一种配置文件。如下图:

QQ图片20151028224530

4.避免多次在日志信息中记录同一个异常。只在异常最开始发生的地方进行日志信息记录。很多情况下异常都是层层向上跑出的,如果在每次向上抛出的时候,都Log到日志系统中,则会导致无从查找异常发生的根源。

5.异常处理尽量放在高层进行。尽量将异常统一抛给上层调用者,由上层调用者统一之时如何进行处理。如果在每个出现异常的地方都直接进行处理,会导致程序异常处理流程混乱,不利于后期维护和异常错误排查。由上层统一进行处理会使得整个程序的流程清晰易懂。

6.在finally中释放资源。如果有使用文件读取、网络操作以及数据库操作等,记得在finally中释放资源。这样不仅会使得程序占用更少的资源,也会避免不必要的由于资源未释放而发生的异常情况。

 

reference:

Java的8种基本数据类型(primitive Type)

【解惑】Java方法参数是引用调用还是值调用?

JAVA异常重新抛出

Java异常处理和设计

以及,与@erueat的聊天

0 等你来赞

linux + github + notes + habit

大家可以看到我上一篇文章加了密码,事情是这样的,听我解释。这是我们课内的作业,贴上来的东西分分钟就是我自己的实验报告,并且deadline还远着。然后我是很相信我同学的“正确使用搜索引擎”的能力的,并且我之前装了一个插件可以帮助google搜索抓取我的页面使之更易被检索到,所以,这只是为了避免不必要的悲剧发生,等到deadline过去以后自然会解放权限。密码是,我的高中校名的拼音。如果不知道是众多校名称呼的哪一个,多试几次就好了,毕竟我只读过一个高中。

 

孟祥武孟爷爷老是说:“你们不能只用一个操作系统啊,除了windows,还应该多试试别的。如果只用一个操作系统,你就会觉得windows就是操作系统,操作系统就是windows——这是不对的。”

类似的,孟爷爷还有“不能只学一门语言啊”,“不能只用一种浏览器啊”的谆谆教导。

 

于是下定决心在自己电脑上装了个虚拟机,先装了ubuntu kylin 15.04,但是觉得不流畅,网上评价也很一般,于是转ubuntu kylin 14.04,使用至今感觉不错。之所以不用原版ubuntu,是因为麒麟版本对于中文更加友好,并且跟原版也没什么差别。网上有人在撕麒麟是不是国产的,其实明显它并不是国产,说是联合开发,实际上kylin只是ubuntu一个子分支,用起来不用心存芥蒂。

顺手贴个很著名的很洗脑的文章,《完全用Linux工作》。书架上还有一本《鸟哥私房菜》嗷嗷待操。

 

转型的过程总体还算顺利,装codeblocks写c++,然后g++和gcc系统是没有自带的,所以顺手装了好多东西。换语言的时候出了问题,出了一个“源引用”错误(提示信息: “ubuntu kylin It is impossible to install or remove any software”),需要打开某个属性栏然后勾选或者是删除某个选项才行,具体的记不清了(翻了一下历史记录没找到),似乎没什么遇到跟我一样的情况;再就是选择简体中文的时候,如下图

QQ图片20151026204344

一开始那个“汉语(中国)”的选项是在下面,而且是浅色的,常识告诉我它仍不能选。google了以后发现正确的打开方式是:点住他、拖动到语言栏的最上方。太魔性,别人不跟我说的话真是一辈子都找不出来毛病。

eclipse至今没有建起来,原因是我搜的那个办法有一步是在shell内使用vim修改一个源文件,当场我就萌比了。还有github理论上是可以直接用shell去push和pull的然而也还没学会(因为暂时还不知道怎么用shell往git直接提交,所以我是开了github的网页然后直接粘上去的)。shell命令也基本不懂,而这才是linux的精髓。以上,列入TODOLIST了。

linux部分大概就这些,使用了半个月,用户体验非常好。因为里面没有qq没有qq音乐也没有全家桶们没有弹窗们,没有任何可以打扰我的东西,做事效率高一点,并且是不是能骗取路人们的仰慕,无形装逼。

 

之前在windows上配好了github没用过,这次在linux上也配了一下(但是还不懂怎么push和pull),然后写作业的时候就真正用github做版本管理了。放几张效果图

QQ图片20151026205638

github的界面简洁明了

QQ图片20151026205529

上图为各个版本

QQ图片20151026205744

版本管理非常方便,点开以后可以看到当前版本与上一版本相比做了什么修改,红色行为原来的写法,其中深红色(此图没有)是具体的修改或删除的地方;绿色行为该版本的修改,深绿色标明了修改或添加的地方。没有改动的代码段自动折叠。最左有新旧两版本的行号对照。支持多人管理同一文档,每一个改动都会标明修改者id、修改内容的题目、修改内容的简要说明等关键信息。

实在很强大,难怪田队年初的时候决心在ACM校队内普及git的用法。今年北邮的校赛的出题验题工作就都是在git上完成的。

总之,github是一个强大的工具。

 

最后做一点个人的读书笔记。

关于如何写注释。

1.不要为那些从代码本身就能快速推断的事实写注释。eg:构造函数就不用再写 //constructor了

2.不要给不好的名字加注释,即注释不应用于粉饰不好的名字。一个好的名字比一个好的注释更重要,因为在任何用到这个函数的地方都能看见它。好代码 > 坏代码 + 好注释。

3.不要写废话。如果一定要解释一个函数,请尝试给出它的逻辑细节。

4.记录你的思想,克服“作者心理阻值”。例如,假如你正在写一个函数,然后心想:“哦,天啊,如果一旦这东西在列表中有重复的话会变得很难处理的”,不如把它写下来:

It does help. 并且最后review的时候你可以尝试改一下这句话,把具体问题解释得更清楚一点。

5.在代码中加入注释记录你对代码有价值的见解。例如,

6.为代码中的瑕疵写注释。

尝试使用标记。书里给出一种流行的写法。“TODO”:我还没有处理的事情。“FIXME”:已知的无法运行的代码。“HACK”:对于一个问题不得不采用的比较粗糙的解决方案。“XXX”:危险!这里有重要的问题。

7.当定义常量时,通常背后有一个关于它是什么或者为什么它是这个值的“故事”。

这可以让读代码的人有了调整这个值的指南。经验告诉我们,很多常量可以通过加注释得到改进,而这只不过是匆匆记下你在决定这个常量值时的想法而已。

8.可能需要一些高级别的注释,比如文件级别的:

——以上摘自DB & TF 的<The Art of Readable Code>

 

45个习惯。

7.懂得丢弃。敏捷的根本之一就是拥抱变化。在学习一门新技术的时候,多问问自己,是否把太多旧的态度和方法用在了新技术上。例如,学习一门新的编程语言时,应该使用推荐的集成开发环境,而不是你过去开发时用的工具插件。转换的时候,完全不要使用过去的语言开发工具。只有更少被旧习惯牵绊,才更容易养成新习惯。对旧习惯不是完全摒弃,如果环境合适,可以举一反三地灵活应用。

9.把我开发节奏。以固定、有规律的长度运行迭代。

14.提早集成,频繁集成。在产品的开发过程中,集成是一个主要的风险区域。让你的子系统不停地增长,不去做系统集成,就等于一步一步把自己置于越来越大的风险中。而若在早期就进行集成,你会更清晰地看到子系统之间的交互和影响,就可以估算它们之间的通信和共享的数据信息。如果落实得好,集成就不再会是一个繁重的任务。它只是编写代码周期中的一部分,集成时产生的问题都会是小问题并且容易解决

——以上摘自VS & AH 的 <Practices of an Agile Developer>

 

0 等你来赞

linux系统下的fork函数,wait函数和exit函数

fork()函数通过系统调用创建一个新进程(子进程),系统为它分配资源,并把其父进程的当前的情况复制到子进程中,从整体上看相当于有两份数据(所以简单改动一个进程内的变量的数据完全不会影响到另一个进程)。

fork()函数调用一次,返回两次。看下面一段代码

这里fork()函数只出现了一次,即只被调用了一次,但此时它已经创建了另一个进程,所以共两个进程。可以理解为,父进程调用了fork()函数,创建子进程以后,fork()返回了一个值给父进程,同时还返回了一个值给新创建的子进程。

fork()函数有三种返回值:

1)在父进程中,fork()返回新创建子进程的进程ID;

2)在子进程中,fork返回0;

3)如果出现错误,fork返回一个负值;

上面提到fork()函数只复制所谓“当前的情况”,可以理解为“此时已经执行过的语句所呈现的情况”,所以时间上在fork()以后才执行的语句会在两个进程中执行。若有这样的语句:

则执行完后系统将会有4个进程。具体的,一开始只有进程1,执行完第一个fork()以后进程1创建了进程2,fork()分别给他们返回了一个值。之后,对于进程1和进程2,都是从得到一个返回值以后继续运行的,也就是分别调用了一次fork()。这样进程1就创建了进程3,进程2创建了进程4(此处数字不表示先后创建顺序,见下一段)。面试题中可能会有这样的考点,给出的例子是最简单最基本的情形。

 

创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

如果要控制fork()以后,等子进程执行完并返回了,再执行父进程,可以调用wait()函数。讲wait()函数之前,先简单讲一个概念:僵尸进程

在UNIX系统下,若一个子进程结束了,但它的父进程没有等待(调用wait() /waitpid())它,那它将会变成一个僵尸进程。僵尸进程的危害在于:若不及时释放,它会一直占用着它的进程号,而系统所能使用的进程号是有限的,所以大量的僵尸进程可能导致系统无法创建新进程。

讲回wait()函数。进程一旦调用了wait(),就立即阻塞自己,并由wait自动分析是否有某个当前进程的子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

 

exit()函数可用于终结一个进程。exit的返回值是返回给操作系统的,但如果是多进程,则是返回给父进程的。

 

写了一个简单的小程序:创建子进程,在子进程中计算斐波那契数列并把父进程挂起。

 

 

文中提到的相关概念只做了很粗浅的解释,深入学习的话请移步下面链接。

reference:

fork()函数

僵尸进程

wait()函数

exit和return的区别

 

 

0 等你来赞