方法调用(call by)(相当于c++中的“函数调用”),根据参数传递的情况分为值调用( call by reference ) 和引用调用( call by value ) 。
Java的对象参数传递仍然是值调用,这里强调的是“对象”的参数传递。
Java中除了primitive type以外一切都是对象。primitive type只有以下几种:
注意,就连我们熟知的数组或者String也都是对象。Java没有指针,所以对primitive type实现著名的swap函数就显得有些困难,一种可行的做法是先特地把它们包装成对象。
Java中对象变量名实际上代表的是对象在堆中的地址(专业术语叫做对象引用 )。在Java方法调用的时候,参数传递的是对象的引用。重要的是,形参和实参所占的内存地址并不一样,形参中的内容只是实参中存储的对象引用的一份拷贝。这正是“值传递”的表现形式。
通过传递对象引用,形参和实参指向了同一个地址。所以修改形参的成员变量,相当于修改了实参的成员变量。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Employee { public String name=null; public Employee(String n){ this.name=n; } public static void Change(Employee e1,Employee e2){ e2.name = "王五"; System.out.println(e1.name+" "+e2.name); //打印结果:张三 王五 } //主函数 public static void main(String[] args) { Employee worker=new Employee("张三"); Employee manager=new Employee("李四"); Change(worker,manager); System.out.println(worker.name+" "+manager.name); //打印结果仍然是: 张三 王五 } } |
试着对一个对象实现swap。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
public class Employee { public String name=null; public Employee(String n){ this.name=n; } //将两个Employee对象交换 public static void swap(Employee e1,Employee e2){ Employee temp=e1; e1=e2; e2=temp; System.out.println(e1.name+" "+e2.name); //打印结果:李四 张三 e2.name = "王五"; System.out.println(e1.name+" "+e2.name); //打印结果:李四 王五 } //主函数 public static void main(String[] args) { Employee worker=new Employee("张三"); Employee manager=new Employee("李四"); swap(worker,manager); System.out.println(worker.name+" "+manager.name); //打印结果仍然是: 张三 李四 } } |
交换失败。前面说到形参和实参都是对象引用,本质上是引用,那么上面那段代码只是把形参的两个引用交换了一下(相当于c++中,只是交换了指针,而不是交换指针所指向的内容),对实参的情况毫无影响。下面用图给出直观的描述:
由此可进一步理解,为什么说JAVA本质上仍然是值传递。
下面讲讲异常抛出。
若要实现以下功能:
第一种方法,使用异常抛出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class student{ ... public static void main(){ while(markForMaths < 0 || markForMaths > 100 || markForEnglish < 0 || markForEnglish > 100 || markForScience < 0 || markForScience > 100){ System.out.println("请输入学生三门课成绩(数学,英语,科学):"); String tmp1 = in.next(); //输入三个数字,以逗号分隔 String []tmp2 = tmp1.split(","); try{ markForMaths = Integer.parseInt(tmp2[0]); markForEnglish = Integer.parseInt(tmp2[1]); markForScience = Integer.parseInt(tmp2[2]); if(markForMaths < 0 || markForMaths > 100 || markForEnglish < 0 || markForEnglish > 100 || markForScience < 0 || markForScience > 100) throw new ScoreException(); } catch(ScoreException e){ System.out.println(e); } catch(IOException e){ System.out.println(e); } } } ... } |
1 2 3 4 5 |
public class ScoreException extends IOException { public ScoreException(){ super("成绩数据有误,请重新输入"); } } |
第二种写法,不使用异常抛出:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
public class student{ ... public static void main(){ while(true){ System.out.println("请输入学生三门课成绩(数学,英语,科学):"); String tmp1 = in.next(); //输入三个数字,以逗号分隔 String []tmp2 = tmp1.split(","); markForMaths = Integer.parseInt(tmp2[0]); markForEnglish = Integer.parseInt(tmp2[1]); markForScience = Integer.parseInt(tmp2[2]); if(markForMaths < 0 || markForMaths > 100 || markForEnglish < 0 || markForEnglish > 100 || markForScience < 0 || markForScience > 100) break; else System.out.println("成绩数据有误,请重新输入"); } } ... } |
显然第二种写法更简明清晰,也是我在学异常抛出之前做错误处理一贯的方式。这难道说明异常抛出反而是累赘吗?不是的,这实际上是一个失败的例子。
可以看到,在第一种写法中,既没有用到JAVA的“重新抛出异常”(就是在这个类的方法内部不处理异常,而是交给调用它的那个类的方法去处理,从里面一直往外抛, 简单说就是谁调用谁处理异常,我不管),而且也只有一层代码,事实上这种情况下很少强行使用异常抛出。
异常是有层级关系的错误处理机制,是专门抽象出来的一个用来处理错误的语法。子函数在出现异常的时候,可以自己处理掉,也可以抛给调用者处理,还可以自己处理一部分调用者处理一部分。处理异常的代码可以非常清晰地与逻辑主体隔绝开来。并且,Exception被捕获的话就不会终止程序,相比之下error则会使之宕掉。
可以设想这么一个场景,你在某个地方调用一个函数,这个函数的作用,是接受学生成绩并做相应的处理。但是,如题所说,输入的学生成绩可能是错误的。那么你在调用的地方怎么获取这个信息呢?两种办法,一种是用返回值表征状态,一种是用异常处理机制。此例中返回值可以有两个枚举值,0表示正常,1表示成绩不在0到100的闭区间内。
然而对于更一般的情况来说,返回值能表示的信息很有限,也就是,只能表示错误状态,却难以将更多的错误信息表达出来。比如如果接受的是一个数组,那么你可能还需要知道脱离0到100范围的数的下标是多少。出现别的错误时,你可能又想获取别的信息,这种情况下返回值就很难做到了。
所以后来我把代码改成了这个样子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
public class student{ ... public static void main(){ int score[] = new int[3]; illegalFlag = true; while(illegalFlag){ illegalFlag = false; System.out.println("请输入学生三门课成绩(数学,英语,科学):"); try{ getScore(score); } catch(_2013212130_zz_2_ScoreException e){ System.out.println(e); } catch(Exception e){ System.out.println("输入格式不符合要求,请重新输入"); } } ... } |
1 2 3 4 5 6 7 8 9 10 11 |
public static void getScore(int score[]) throws Exception{ Scanner in = new Scanner(new BufferedInputStream(System.in)); String tmp1 = in.next(); String []tmp2 = tmp1.split(","); int markForMaths = score[0] = Integer.parseInt(tmp2[0]); int markForEnglish = score[1] = Integer.parseInt(tmp2[1]); int markForScience = score[2] = Integer.parseInt(tmp2[2]); if(markForMaths < 0 || markForMaths > 100 || markForEnglish < 0 || markForEnglish > 100 || markForScience < 0 || markForScience > 100) throw new _2013212130_zz_2_ScoreException(); } |
1 2 3 4 5 |
public class ScoreException extends IOException { public ScoreException(){ super("成绩数据有误,请重新输入"); } } |
这样才不会背离“异常处理”的原则太远。
初初接触异常抛出,除了明白基本概念,我感觉自己还需要知道什么时候该用以及该怎么用,在网上找到了相关有营养的建议。
1.只在必要使用异常的地方才使用异常,不要用异常去控制程序的流程。谨慎地使用异常,异常捕获的代价非常高昂,异常使用过多会严重影响程序的性能。
2.切忌使用空catch块。千万不要使用空的catch块,空的catch块意味着你在程序中隐藏了错误和异常,并且很可能导致程序出现不可控的执行结果。如果你非常肯定捕获到的异常不会以任何方式对程序造成影响,最好用Log日志将该异常进行记录,以便日后方便更新和维护。
3.不要将提供给用户看的信息放在异常信息里。因为业务逻辑跟用户界面要解耦合,展示给用户的错误提示信息若写死在里面了,万一要改成英文版的,不是又得改下程序;万一你觉得这个promt不合适,想换句好听点的话,又得改下程序;万一你要支持多语言,你还得在代码里面敲进N多种语言,再用一个swtich去拿…
其中一种做法是拿一个 txt 文件或者 ini 文件之类的,把错误编码跟错误提示文本存起来。然后主程序再跑去文件里面根据错误编码找到具体的对应提示文本,这就是一种配置文件。如下图:
4.避免多次在日志信息中记录同一个异常。只在异常最开始发生的地方进行日志信息记录。很多情况下异常都是层层向上跑出的,如果在每次向上抛出的时候,都Log到日志系统中,则会导致无从查找异常发生的根源。
5.异常处理尽量放在高层进行。尽量将异常统一抛给上层调用者,由上层调用者统一之时如何进行处理。如果在每个出现异常的地方都直接进行处理,会导致程序异常处理流程混乱,不利于后期维护和异常错误排查。由上层统一进行处理会使得整个程序的流程清晰易懂。
6.在finally中释放资源。如果有使用文件读取、网络操作以及数据库操作等,记得在finally中释放资源。这样不仅会使得程序占用更少的资源,也会避免不必要的由于资源未释放而发生的异常情况。
reference:
以及,与@erueat的聊天
0 等你来赞