赞
踩
目录
2.避免带有变长参数的方法的重载、别让null威胁到变长方法
25.集合相等只需要关心元素数据(集合类的equals方法)
38.反射访问属性或方法时将Accessible设置为true
三元操作符是if-else的简化写法,它被使用的比较频繁。在Java中三元操作符中非常需要注意的问题就是类型转换,隐式的拆装箱问题。对于类型转换,规则如下:
先看一下下面的例子:
- int i = 80;
- String str1 = String.valueOf(i > 90 ? 100 : 70);
- String str2 = String.valueOf(i > 90 ? 100.0 : 70);
- System.out.println(str1.equals(str2)); //输出false
输出false的主要原因是:对于str2, 由于三元操作符必须要返回一个数据,而且类型要确定,不可能条件为真时返回double类型,条件为假时返回int类型,编译器是不允许如此的,所以它就会进行类型转换了,int型转换为double类型的70.0。(这里体现的是第4条规则)
对于第二条和第三条规则:
- char c = 'c';
- int index = 10;
- System.out.println(i > 90 ? index : c); //第二条。输出为99。返回值必须由一个固定类型接收,所以char类型的c转换为int类型
- System.out.println(i > 90 ? 10 : c); //第三条。输出为c。此处10就是S,T就是char类型。因为10在char的范围内(char类型范围为-128-127),所以最终类型为char类型。
对于第一条比较好理解,只不过需要注意的是如果操作数1是Integer类型,操作数2是Double类型,这两种类型可以转换吗?当然直接转换是不可以的,但是Java有拆装箱操作,所以可以利用此特性进行转换,且会向范围大的转换,即最终转换为Double类型,一个示例如下:
- int i = 80;
- Integer a = 2;
- Double d = 3.0;
- Double res = i < 90 ? a : d;
在编译前,我们需要用Double类型来接收这个三元操作符的返回值,因为a和d一个是Integer类型的,一个是Double类型的,返回值只能是一个固定类型(我们在Double和Integer中选一个),Double类型的拆箱类型是double,Integer的拆箱类型是int,int类型会向范围比它大的double自动转换,所以该三元操作符的返回值是double类型,那么我们利用它的装箱类Double接收自然是可以的,JVM会帮我们装箱。如果非要用Integer接收也是可以的,只不过需要显式地强转。
最后一句的字节码如下:
- 18 if_icmple 29 (+11) //?前的表达式结果为假时跳转到29行
- 21 aload_1 //将第二个局部变量入栈(即变量a)
- 22 invokevirtual #24 <java/lang/Integer.intValue> //执行拆箱操作(即Integer类型的2转化为int类型)
- 25 i2d //将栈顶int类型的数据转换为double类型(即2转换为2.0)
- 26 goto 33 (+7) //跳转指令:跳转到33这个指令地址
- 29 aload_2 //将第三个引用类型本地变量(d)推送至栈顶
- 30 invokevirtual #30 <java/lang/Double.doubleValue>
- 33 invokestatic #18 <java/lang/Double.valueOf> //将栈顶元素装箱(即2.0装箱为Double类型)
- 36 astore_3 //将栈顶元素存入第4个局部变量中(即res)
- 37 return
这里的过程是这样的,先判断i<90的真假,此处为真,所以就需要执行下面的字节码:它会先将a(Integer)拆箱为int类型,再将int类型转换为double类型的(2.0),然后将这个double类型的2.0装箱为Double类型,最后将其赋值给res变量。在执行字节码的时候,如果i>90为假,那么JVM执行字节码时就不会执行Integer->Double类型的转化。只不过编译器不会去判断,所以需要用范围更大的Double接收。
Java 5引入了变长参数,目的是为了让调用者“随心所欲”地传递实参数量,当然也需要遵守一定的规则,比如:
使用这个特性的时候也需要注意,比如我们定义三个方法如下:
- public static void method(String str, int num) {
- System.out.println("这是没有变长参数的方法");
- }
- public static void method(String str, int... nums) {
- System.out.println("这是带有变长参数nums的方法");
- }
- public static void method(String str, Object... objs) {}
当调用不当时可能产生的问题如下:
- public static void main(String[] args) {
- /**
- * 这里调用的是没有变长参数的方法。因为int是一个原生数据类型,而数组本身是一个对象。
- * 编译器想要“偷懒”,于是它会从最简单的开始“猜想”,只要符合编译条件的即可通过,于是就出现了此问题。
- */
- method("1",2);
- /**
- * 下面两个方法编译出错,如果没有method(String str, Object... objs)这个方法那是能通过编译的。
- * 但是由于存在method(String str, int... nums)和method(String str, Object... objs)两个第一个参数类型相同,第二个都是变长参数
- * 所以下面的调用就会让编译器不能理解到底是要调用哪个方法,所以编译不通过
- */
- //method("1");
- //method("1", null);
- }
所以我们调用带有变长参数的方法时,如果不想传递变长参数时,不建议直接传递null,最好是传递一个空的对应类型的数组,这样就不会让编译器懵逼了(当然将参数们都装进数组中然后去传递数组这是极好的)。
首先,重写必须满足的条件:
我们来看一个示例:
父类:
- public class Father {
- public void method(String... strs) {
- System.out.println("这是父类的方法");
- };
- }
子类:
- public class Sub extends Father {
- @Override
- public void method(String[] strs) {
- System.out.println("这是子类的方法");
- }
- }
调用时可能产生的问题:
- public static void main(String[] args) {
- //向上转型
- Father father = new Sub();
- //这里形参列表是由父类确定的,Java编译器会先将"1",编译成{“1”},然后由子类调用
- father.method("1");
-
- /*
- * 这里由于没有做向上转型,所以形参列表是由子类Sub确定,而由于子类中明确要求要是String[]类型
- * 而Java编译器由不会帮我们编译成{"1"},所以下面的调用编译不通过
- */
- Sub sub = new Sub();
- //sub.method("1"); //此处编译不通过
- }
所以重写方法的时候一定要注意参数列表必须与被重写方法相同,这里的相同不仅仅是类型、数量,还包括显示形式。
我们一定听到过一句话, i++是先赋值后加1,++i是先加1后赋值。那么我们通过一个例子来看下,Java中的i++到底是怎样一个流程。
- public static void main(String[] args) {
- int count = 0;
- for(int index = 0; index < 10; index++) {
- count = count++;
- }
- System.out.println("count的值为:" + count); //输出0
- }
注意最后是输出0。因为它底层其实是这样做的:
一个类实现Serializable接口的目的是为了可持久化,比如网络传输或本地存储,为系统的分布和异构部署提供先决支持条件。若没有序列化,现在我们熟悉的远程调用、对象数据库都不可能存在。我们先来看一个简单的序列化类,如下:
- public class Person implements Serializable{
- private String name;
- //省略getter和setter方法
- }
这个类实现了Serializable接口,但是并没有显式声明UID。这么做会有什么问题?假如这是一个分布式部署的应用,消息生产者的Person类增加了一个age属性,而消费者没有增加,那么反序列化时就会抛出InvalidClassException异常,异常出现的原因是序列化和反序列化所对应的类版本发生了变化,JVM不能把数据流转化为实例对象。而JVM就是通过SerialVersionUID来判断一个类的版本的。SerialVersionUID它有两种声明方式,一种是显式声明,一种是隐式声明(隐式声明则是我不声明,你编译器在编译的时候帮我生成。生成的依据是通过包名、类名、继承关系、非私有的方法和属性,以及参数、返回值等诸多因子计算得出的,极度复杂,基本上计算出来的这个值是唯一的。)所以才会抛出这个异常。
有时候我们需要一点特例场景,例如:我们的类改变不大,JVM是否可以把我以前的对象反序列化过来?就是依靠显式声明serialVersionUID,向JVM撒谎说“我的类版本没有变更”,如此,我们编写的类就实现了向上兼容。以上面例子来说就是这样:
- public class Person implements Serializable{
- //显式声明
- private static final long serialVersionUID = -6648364975261402195L;
- private String name;
- }
这时面对生产者和消费者类版本不一致的情况,依然是可以反序列化出对象的,只不过消费者无法读取到新增的业务属性(age)而已。
注意:显式声明SerialVersionUID可以避免对象不一致,但尽量不要以这种方式向JVM“撒谎”。
Java中我们用final修饰 不变量,为final赋值的方式有:
如下为第一种方式:
- public class Student implements Serializable{
- private static final long serialVersionUID = -1825641010498986620L;
- public final String name = "李四";
- }
此处做一个测试:
注意这里反序列化得出的对象的name属性值更新了,这是反序列化的一个规则:如果final是一个直接量,则在反序列化时就会重新计算其值。
如下为第二种方式:
- public class Student implements Serializable{
- private static final long serialVersionUID = -1825641010498986620L;
- public final String name;
-
- public Student() {
- name = "李四";
- }
- }
此处做跟示例一一样的测试,结果并没有输出“张三”,而是之前的“李四”。
这里触及了反序列化的另一个规则:反序列化时构造函数不会执行。反序列化的执行过程是这样的:JVM从数据流中获取一个Object对象,然后根据数据流中的类文件描述信息(在序列化时,保存到磁盘的对象文件中包含了类描述信息,注意是类描述信息,不是类)查看,发现是final变量,需要重新计算,于是引用Person类中的name值,而此时JVM又发现name竟然没有赋值,不能引用,于是它很“聪明”地不再初始化,保持原值状态。所以依然是“李四”。
其实序列化保存到磁盘上(或网络传输)的对象文件包括两部分:
总结:反序列化时final变量在一下情况下不会被重新赋值:
假如现在有一个算法来计算股票走势,该算法会根据市场环境逐渐调整,不断变化如果把这个算法写到某个类中(或者几个类中),就需要经常发布重启等操作。使用脚本语言可以很好的简化这一过程。Java 6开始正式支持脚本语言。但是因为脚本语言比较多,Java的开发者也很难确定该支持哪种语言,于是JCP(Java Community Process)很聪明地提出了JSR223规范,只要符合该规范的语言都可以在Java平台上运行(它对JavaScript是默认支持的)。
假设下面是利用JavaScript实现的该算法,如下:
- function formula(num1, num2) {
- //factor是从上下文中来的
- return num1 + num2 * factor;
- };
下面是利用Java调用执行该函数:
- public static void main(String[] args) throws Exception {
- //获得一个JavaScript的执行引擎
- ScriptEngine engine = new ScriptEngineManager().getEngineByName("javascript");
- //建立上下文变量
- Bindings bind = engine.createBindings();
- bind.put("factor", 1);
- //绑定上下文,作用域是当前引擎范围
- engine.setBindings(bind, ScriptContext.ENGINE_SCOPE);
- Scanner scanner = new Scanner(System.in);
- while(scanner.hasNextInt()) {
- int num1 = scanner.nextInt();
- int num2 = scanner.nextInt();
- //执行JS代码
- engine.eval(new FileReader("./src/model.js"));
- if(engine instanceof Invocable) {
- Invocable in = (Invocable)engine;
- //执行JS中的函数
- Double result = (Double) in.invokeFunction("formula", num1, num2);
- System.out.println("计算结果为:" + result.intValue());
- }
- }
-
- scanner.close();
- }
在JVM没有重新启动的情况下,两次输出结果对比:
instanceof是一个简单的二元操作符,它是用来判断一个对象是否是一个类实例的。我们还是通过几个例子来看看instanceof呈现的各种结果:
- public static void main(String[] args) {
- boolean b1 = "name" instanceof Object; //String对象是否是Object类的实例,此处返回true
- boolean b2 = new String() instanceof String;//String对象是否是否是String的实例,此处返回true
- /**
- * Object对象是否是String类的实例,此处返回false,Object是父类,其对象当然不是String类的实例
- * 此处可以编译通过,只要instanceof左右两个操作数有继承或实现关系,就可以编译通过。
- */
- boolean b3 = new Object() instanceof String;
- /**
- * 拆箱类是否是装箱类型的实例
- * 编译不通过,因为‘A’是一个char类型,即基本类型,instanceof只能用于对象的判断,不能用于基本类型的判断
- */
- // boolean b4 = 'A' instanceof Character;
- /**
- * 空对象是否是String类的实例
- * 返回false。instanceof规则:若左操作数是null,结果直接返回false
- */
- boolean b5 = null instanceof String;
- boolean b6 = (String)null instanceof String;//类型转换后的空对象是否是String类的实例,此处返回false
- // boolean b7 = new Date() instanceof String;//编译不通过。因为Date类和String类没有继承或实现关系
- /**
- * 在泛型类中判断String对象是否是String类的实例
- * 此处返回false。泛型对象在编译时被擦除为原始类型Object类, Object instanceof Date ==> 返回false
- */
- boolean b8 = new GenericClass<String>().isDateInstance("name");
- }
-
- static class GenericClass<T>{
- public boolean isDateInstance(T t) {
- return t instanceof Date;
- }
- }
假如现在有一个产品畅销的场景:它规定每个会员最多可以预定2000个产品,目的是防止囤货积压肆意加价,预先输入预定的数量,符合条件则成功。后台的处理逻辑模拟如下:
- //一个会员拥有产品最多的数量
- public static final int LIMIT = 2000;
-
- public static void main(String[] args) {
- //会员当前拥有的产品数量
- int cur = 1000;
- //准备订购的数量(假设这是用户输入)
- int order = Integer.MAX_VALUE;
- if(order > 0 && order + cur <= LIMIT) {
- System.out.println("你已经成功预订了" + order + "个产品");
- //……
- }else {
- System.out.println("超过限额,预定失败");
- }
- }
这段程序输出如下:
出现这个问题的根本原因是忽略了int类型的边界,Integer.MAX_VALUE是整型4字节表示的最大正数数值,在此基础上再加1000,那其结果值就是负数,肯定是小于LIMIT(2000)的,然后就错误地被预订了,当然解决该问题只需要修改if判断逻辑即可(order > 0 && order <= LIMIT - cur)这样一来,order的上下边界就被确定。虽然这是一个小例子但是细心真的很重要。
四舍五入是一种近似精确的计算方法,但也是有误差的,我们以舍入运用最频繁的银行利息计算为例来阐述该问题。四舍五入,小于5的数字被舍去,大于等于5的数字进位后舍去,由于所有位上的数字都是自然计算出来的,按照概率计算可知,被舍入的数字均匀分布在0到9之间,下面以10笔存款利息计算作为模型,以银行家的身份来思考这个算法:
因为舍弃和进位的数字是在0到9之间均匀分布的,所以对于银行家来说,每10笔存款的利息因采用四舍五入而获得的盈利是:0.000 + 0.001 + 0.002 + 0.003 + 0.004 - 0.005 - 0.004 - 0.003 - 0.002 - 0.001 = -0.005 也就是说每10笔利息计算中就损失0.005元,即每笔利息计算中损失0.0005元。如果存款的人很多的话那么损失是巨大的。
这个算法误差是由美国银行家发现的,并且对此提出了一个修正算法,叫做银行家舍入(Banker's Round)的近似算法,其规则如下:
以上规则汇总成一句话:四舍六入五考虑,五后非零就进一,五后为零看奇偶,五前为偶应舍去,五前为奇要进一。要在Java 5以上的版本中使用银行家的舍入法则非常简单,直接使用RoundingMode类提供的Round模式即可,示例代码如下:
- public static void main(String[] args) {
- //存款
- BigDecimal b = new BigDecimal(888888);
- //月利率 * 3 计算季利率
- BigDecimal r = new BigDecimal(0.001875 * 3);
- //计算利息
- BigDecimal i = b.multiply(r).setScale(2, RoundingMode.HALF_EVEN);
- System.out.println("季利息是:" + i); //输出:季利息是:4999.99
- }
在上面的例子中,我们使用了BigDecimal类,并且采用setScale方法设置了精度,同时传递了一个RoundingMode.HALF_EVEN参数表示使用银行家舍入法则进行近似计算,BigDecimal和RoundingMode是一个绝配,想要采用什么舍入模式使用RoundingMode设置即可。目前Java支持以下七种舍入方式:
在普通的项目中舍入模式不会有太多影响,可以直接使用Math.round方法,但在大量与货币数字交互的项目中,一定要选择好近似的计算模式,尽量减少因算法不同而造成的损失。
我们知道Java引入包装类型(Wrapper Types)是为了解决基本类型的实例化问题,以便让一个基本类型也能参与到面向对象的编程世界中。而在Java 5中泛型更是对基本类型说了“不”,如想把一个整型放到List中,就必须使用Integer包装类型。我们来看一段代码:
- public static void main(String[] args) {
- List<Integer> numberList = new ArrayList<Integer>();
- numberList.add(1);
- numberList.add(null);
-
- //计算集合中所有数据的和
- int res = 0;
- for(int i : numberList) {
- res += i;
- }
- System.out.println(res);
- }
这段代码最终会以空指针异常告终,因为我们向集合中添加了一个null值,而for循环时,又是以基本类型int来接收Integer变量的,所以这里隐含了一个自动拆箱的过程,Integer对象会调用它的intValue方法,而其真实值又是null的,所以才出现异常,我们只需要修改一个for循环即可,如下:
- for(Integer i : numberList) {
- res += (i == null) ? 0 : i;
- }
对于包装类型,我们还需要注意它的大小比较(包装类是对象,如果对象直接使用“==”、“<”、“>”,那么其比较的其实的地址),对于Integer类型,它有一个整型池的概念,Integer对象中存在一个Integer元素的数组,他会将(-128到127中的Integer对象缓存起来)通过valueOf方法会优先从该数组中获取Integer对象。装箱动作也是通过valueOf方法实现的。在以后使用的过程中,可以考虑使用valueOf方法,优先使用整型池。
随机数在太多的地方使用了,比如加密、混淆数据等,我们使用随机数是期望获得一个唯一的、不可仿造的数字,以避免产生相同的业务数据造成混乱。在Java项目中通常是通过Math.random方法和Random类来获得随机数的,我们来看一段生成随机数的代码:
- public static void printRandom() {
- Random r = new Random();
- for(int i = 1; i < 4; i++) {
- System.out.println("第" + i + "次:" + r.nextInt());
- }
- }
这样生成随机数每次执行都会输出不同的一组随机数。如果对上述代码稍作修改,那么情况就会发生变化,如下:
Random r = new Random(1000); //只修改这里即可
计算机不同输出的随机数也不同,但是有一点是相同的:在同一台机器上,甭管运行多少次,所打印的随机数都是相同的,也就是说第一次运行,会打印出这三个随机数,第二次运行还是打印出这三个随机数,只要是在同一台硬件机器上,就永远都会打印出相同的随机数。这是因为产生随机数的种子被固定了,在Java中,随机数的产生取决于种子,随机数和种子之间的关系遵从以下两个规则:
Random类的默认种子(无参构造)是System.nanoTime()的返回值(JDK 1.5版本以前默认种子是System. currentTimeMillis()的返回值),注意这个值是距离某一个固定时间点的纳秒数,不同的操作系统和硬件有不同的固定时间点,也就是说不同的操作系统其纳秒值是不同的,而同一个操作系统纳秒值也会不同,随机数自然也就不同了。(顺便说下,System.nanoTime不能用于计算日期,那是因为“固定”的时间点是不确定的,纳秒值甚至可能是负值,这点与System. currentTimeMillis不同。)new Random(1000)显式地设置了随机种子为1000,运行多次,虽然实例不同,但都会获得相同的三个随机数。所以,除非必要,否则不要设置随机种子。
我们知道一个实例对象有两个类型:表面类型(Apparent Type)和实际类型(Actual Type),表面类型是声明时的类型,实际类型是对象产生时的类型。对于非静态方法,它是根据对象的实际类型来执行的,而对于静态方法来说就比较特殊了,首先静态方法不依赖实例对象,它是通过类名访问的;其次,可以通过对象访问静态方法(这并不是一个好习惯),如果是通过对象调用静态方法,JVM则会通过对象的表面类型查找到静态方法的入口,继而执行之。
在Java中静态方法可以被继承,但是不能被重写。在子类中构建与父类相同的方法名、输入参数、输出参数、访问权限(权限可以扩大),并且父类、子类都是静态方法,此种行为叫做隐藏(Hide),它与重写有两点不同:
其实重写是多态特性的体现,JVM通过判断对象实例的实际类型来调用相应的实例方法,所以重写是跟着对象实例走的,而静态方法是专属于某个类的,我们访问静态方法的时候一般也是通过类名来访问的,所以对于静态方法无法体现多态性。
在构造函数中初始化其他类可能会出现的问题,其中之一:
- public static void main(String[] args) {
- Son s = new Son();
- s.doSomething();
- }
-
- //父类
- static class Father {
- public Father() {
- new Other();
- }
- }
- //子类
- static class Son extends Father{
- public void doSomething() {
- System.out.println("doSomething");
- }
- }
- //相关类
- static class Other{
- public Other() {
- new Son();
- }
- }
子类继承父类,初始化子类之前JVM会先初始化父类,在子类默认的构造方法中隐藏了super()这条语句,而父类构造方法中又去实例化Other对象,Other构造方法中又实例化Son对象,这就造成了三个构造方法的死递归,方法出不了栈,造成栈溢出。如果真的遇到这种情况,我觉得可以将Son对象定义为一个成员,使用懒加载的思想,在使用的时候再初始化。
在Java中一共有四种类型的代码块:
Java中每个类至少会有一个构造方法,对于构造方法和构造代码块:编译器会把构造代码块插入到每个构造函数的最前端,也就是说:构造代码块会在每个构造函数内首先执行(需要注意的是:构造代码块不是在构造函数之前运行的,它依托于构造函数的执行)。需要注意的是:如果构造方法A中利用this关键字调用了构造方法B的话,利用构造方法A实例化对象时,会先执行构造代码块中的内容,完毕之后执行构造方法B(这个构造方法B中不插入构造代码块的内容),接着执行构造方法A的剩余内容。构造代码块是为了提取构造函数的共同量,减少各个构造函数的代码而产生的,而且要确保每个构造函数只执行一次构造代码块。
下面是一个测试示例:
- public class Test {
- {
- System.out.println("构造代码块");
- }
- public Test(String str) {
- this();
- System.out.println("带参构造方法");
- }
-
- public Test() {
- System.out.println("无参构造方法");
- }
-
- public static void main(String[] args) {
- new Test("");
- /**
- * 输出如下:
- 构造代码块
- 无参构造方法
- 带参构造方法
- */
- }
- }
Java中的嵌套类分为两种:静态内部类(也叫静态嵌套类)和内部类。只有在是静态内部类的情况下才能把static修饰符放在类前,其他任何时候static都是不能修饰类的。静态内部类有两个优点:加强了类的封装性和提高了代码的可读性。这和我们一般定义的类有什么区别呢?又有什么吸引人的地方呢?如下所示:
静态内部类与普通内部类区别如下:
匿名类虽然没有名字,但也是可以有构造方法的,它的构造方法就是构造代码块。匿名类的构造方法比较特殊特殊,一般类(也就是具有显式名字的类)的所有构造方法默认都是调用父类的无参构造的,而匿名类因为没有名字,只能由构造代码块代替,也就无所谓的有参和无参构造方法了,它在初始化时直接调用了父类的同参数构造,然后再调用了自己的构造代码块。
Java项目中使用的工具类非常多,比如JDK自己的工具类java.lang.Math、java.util. Collections等都是我们经常用到的。工具类的方法和属性都是静态的,不需要生成实例即可访问,而且JDK也做了很好的处理,由于不希望被初始化,于是就设置构造方法为private访问权限,表示除了类本身外,谁都不能产生一个实例。由于Java反射机制的存在,修改构造方法的访问权限易如反掌,为了避免反射的操作,我们可以在修改构造方法为private的同时再抛出异常。如下:
- public class MyUtils {
- private MyUtils() {
- throw new RuntimeException("不要实例化我");
- }
- }
我们知道所有类都继承自Object,Object提供了一个对象拷贝的默认方法,即clone方法,它是用protected修饰的只允许在同包和子类内部调用。我们如果想要让外部调用能够拷贝自己的对象实例,可以实现JDK提供的Cloneable接口(一个标记接口),并重写Object类的clone方法。如果不实现这个接口直接重写clone方法,则会抛出CloneNotSupportedException异常。Object类提供的clone方法是有缺陷的,它提供的是一种浅拷贝方式,也就是说它并不会把对象的所有属性全部拷贝一份,而是有选择性的拷贝,它的拷贝规则如下:
可以编写一个工具类来(CloneUtils)实现深拷贝,此工具类要求被拷贝的对象必须实现Serializable接口,否则是没办法拷贝的。用此方法进行对象拷贝时需要注意两点:
- public class CloneUtils {
- private CloneUtils() {}
-
- //被克隆的对象需要实现Serializable接口
- @SuppressWarnings("unchecked")
- public static <T extends Serializable> T clone(T obj) {
- T result = null;
- ObjectOutputStream oos = null;
- ObjectInputStream ois = null;
- try {
- ByteArrayOutputStream bos = new ByteArrayOutputStream();
- oos = new ObjectOutputStream(bos);
- oos.writeObject(obj);
- //分配内存空间
- ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
- ois = new ObjectInputStream(bis);
- result = (T) ois.readObject();
- } catch (Exception e) {
- e.printStackTrace();
- }finally {
- try {
- oos.close();
- ois.close();
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
-
- return result;
- }
- }
当然,采用序列化方式拷贝时还有一个更简单的办法,即使用Apache下的commons工具包中的SerializationUtils类,直接使用更加简洁方便。
我们在写一个JavaBean时,经常会覆写equals方法,其目的是根据业务规则判断两个对象是否相等,比如我们写一个Person类,然后根据姓名判断两个实例对象是否相同,这在DAO(Data Access Objects)层是经常用到的。下面为Person类的equals方法:
- @Override
- public boolean equals(Object obj) {
- //此处建议用getClass()方法判断,而不要使用instanceOf判断
- if(obj != null && obj.getClass() == this.getClass()) {
- Person p = (Person)obj;
- //此处需要注意null的情况
- if(p.getName() == null || name == null) {
- return false;
- }
- //考虑到从Web上传递过来的对象有可能输入了前后空格,所以用trim方法剪切一下
- return name.equalsIgnoreCase(p.getName().trim());
- }
- return false;
- }
针对上面这种写法(注意有一个trim()),我们进行一个识别不出自己的示例:
- public static void main(String[] args) {
- Person p1 = new Person("张三");
- Person p2 = new Person("张三 ");
- List<Person> personList = new ArrayList<Person>();
- personList.add(p1);
- personList.add(p2);
- System.out.println("集合中是否包含张三:" + personList.contains(p1)); //true
- System.out.println("集合中是否包含张三:" + personList.contains(p2)); //false
- }
这里将“张三”和“张三 ”一起添加进了集合,然后利用ArrayList的contains方法判断是否包含,第二个输出竟然是false。我们可以先从contains方法看起,该方法内部就是调用了一个indexOf(Object o)方法,如果返回-1就说明不包含。indexOf方法如下:
- public int indexOf(Object o) {
- if (o == null) {
- for (int i = 0; i < size; i++)
- if (elementData[i]==null)
- return i;
- } else {
- for (int i = 0; i < size; i++)
- if (o.equals(elementData[i]))
- return i;
- }
- return -1;
- }
看了源码就比较清楚了,这里参数对象o逐个与集合中的元素做相等比较,如果相等则返回下标值。我们对Person的equals的处理是先获取到姓名,然后去掉姓名后的空格,在进行比较。所以:
- 集合中的元素0:"张三"; 1:"张三 "
- 所以第二个比较逻辑相当于:
- "张三 " -->"张三" 与集合中第一个元素比较
- "张三 " -->"张三" 与集合中第二个元素比较,在equals内,去除了集合中"张三 "后的空格--->"张三"
我们都知道HashMap的底层处理机制是以数组的方式保存Map节点的,这其中的关键是这个数组下标的处理机制:依据传入元素hashCode方法的返回值决定其数组的下标,如果该数组位置上已经有了节点元素,且与传入的键值相等则不处理,若键相等值不相等则覆盖;如果数组位置没有节点元素,则插入。同理,检查键是否存在也是根据哈希码确定位置,然后遍历查找键值的。(不去重写hashCode方法,那么可能两个equals判断为相等的"张三"对象其哈希码不同。这是关键所在)
对于hashCode方法:它是一个对象的哈希码,是由Object类的本地方法生成的,确保每个对象有一个哈希码(这也是哈希算法的基本要求:任意输入k,通过一定算法f(k),将其转换为非可逆的输出,对于两个输入k1和k2,要求若k1=k2,则必须f(k1)=f(k2),但也允许k1≠k2,f(k1)=f(k2)的情况存在)。
在Java里我们可以调用System.gc主动进行垃圾回收(当然JVM也不是看到就直接回收,这个操作相当于给JVM提建议),这样比较危险,是因为垃圾回收时要停止所有的响应(Stop the world),才能检查内存中是否有可回收的对象,这对一个应用系统来说风险极大,如果是一个Web应用,所有的请求都会暂停,等待垃圾回收器执行完毕,若此时堆内存(Heap)中的对象少的话则还可以接受,一旦对象较多(现在的Web项目是越做越大,框架、工具也越来越多,加载到内存中的对象当然也就更多了),那这个过程就非常耗时了,可能0.01秒,也可能是1秒,甚至是20秒,这就会严重影响到业务的正常运行。
在Java中我们可以通过foreach的方式(迭代器的变形用法)进行集合遍历,也可以利用下标索引的方式遍历。
如果定义三个不同的集合类(ArrayList,Vector,LinkedList),都存入相同的元素(比如都存入“A”),然后利用他们的equals方法比较这三个集合是否相等,则都会返回true。这是因为这三个集合类都直接或间接的继承了AbstractList这个抽象类,该类中存在equals方法(这三个集合类都是用的这个equals方法)的实现如下:
- public boolean equals(Object o) {
- if (o == this)
- return true;
- if (!(o instanceof List))
- return false;
-
- //获取两个集合对象的迭代器
- ListIterator<E> e1 = listIterator();
- ListIterator<?> e2 = ((List<?>) o).listIterator();
- while (e1.hasNext() && e2.hasNext()) {
- E o1 = e1.next();
- Object o2 = e2.next();
- //若相同位置处有且仅有一个元素为null则返回false
- //若两个元素都不为null的情况下,利用元素的equals方法判断不相等则返回false
- if (!(o1==null ? o2==null : o1.equals(o2)))
- return false;
- }
- //如果两个集合类中的元素数量不相等则为false
- return !(e1.hasNext() || e2.hasNext());
- }
在Java中,要想给数据排序,有两种实现方式,一种是实现Comparable接口,一种是实现Comparator接口。这两个接口是有区别的。
实现了Comparable接口的类表明自身是可比较的,有了比较才能进行排序;而Comparator接口(比较器)是一个工具类接口,它与原有类的逻辑没有关系,只是实现两个对象的比较逻辑,从这方面来说,一个类可以有很多的比较器,只要有业务需求就可以产生比较器,有比较器就可以产生N多种排序,而Comparable接口的排序只能说是实现类的默认排序算法,一个类稳定、成熟后其compareTo方法基本不会改变,也就是说一个类只能有一个固定的、由compareTo方法提供的默认排序算法。如果一个类既实现Comparable接口也实现Comparator接口,会优先按照Comparator的规则排序。
先定义一个员工类,实现Comparable接口,其内部的compareTo方法是针对员工的实际年龄排序:
- public class Employee implements Comparable<Employee>{
- private int age;
- private int workAge;
-
- public Employee(int age, int workAge) {
- this.age = age;
- this.workAge = workAge;
- }
-
- @Override
- public int compareTo(Employee o) {
- int res = 0;
- if(this.age < o.age) {
- res = -1;
- }else {
- res = 1;
- }
- return res;
- }
- //或者 return this.age - o.age;
- }
测试如下:
- public static void main(String[] args) {
- Employee e1 = new Employee(21, 1);
- Employee e2 = new Employee(27, 1);
- Employee e3 = new Employee(28, 6);
- Employee e4 = new Employee(24, 4);
- Employee e5 = new Employee(26, 2);
- Employee[] emps = {e1,e2,e3,e4,e5};
- Arrays.sort(emps);
- System.out.println(Arrays.toString(emps));
- //输出按照年龄排序为:21、24、26、27、28
- }
从输出可以看出这是升序排列后的结果,但是compareTo方法只不过是返回一个int类型的数值,这跟升序降序有什么关系?
我之前也听到过一句话:对于compareTo方法返回值:
这个其实只是一种“规则”,或者说大家的约定,当然具体怎么实现还是要看自己的compareTo逻辑,只不过升序降序不是靠这三个数这么单纯的。而是看具体的排序方法怎么实现,或者说怎么利用compareTo方法,JDK中有很多排序方法,或者有序的数据结构都是可以传递Comparable接口的。我们就针对Arrays.sort方法看(对于升降序来说其他的也是一样的),Arrays的sort方法有一处调用了ComparableTimSort类的countRunAndMakeAscending方法(这里就不贴源码了,可以自己看),从该方法中可以看出:
方法内部将数组中的 后一个元素 与 前一个元素比较(此处指相对位置),有两种情况:
依照这种形式可以看出:后一个元素为方法的调用者(当前对象)。前一个元素为参数对象。那么:
方法再贴一下就不用往前翻了:
- public int compareTo(Employee o) {
- return this.age - o.age;
- }
由于我们的方法是这样调用的:当前对象.compareTo(参数对象) 后面元素为方法调用者,而且我们比较的是各自的age成员。所以,当前对象的age即为后面的值,参数对象的age即为前面的值。所以基于此处的写法(升序写法),this.age会出现如下两种情况(这也是为什么会是升序排列的原因):
当然降序排列的话就是return o.age - this.age。
为了方便记忆,我这样总结了一下(对JDK可以用Comparable接口的所有地方,比如Arrays.sort或者优先队列这些,下面两句话具有普适性):我们可以将当前对象看成前一个对象,将参数对象看成后一个对象
此时该类内部已经实现了基于年龄来排序了,但是假如现在我们需要利用工龄来排序,再来修改这个类的源码肯定是不合适的,所以这时就可以利用Comparator接口了。可以这样做:
- Comparator<Employee> workAgeComparator = new Comparator<Employee>() {
- //这里o1为前一个对象,o2为后一个对象,那个口诀也是适用的。
- @Override
- public int compare(Employee o1, Employee o2) {
- return o1.workAge - o2.workAge;
- }
- };
特别的:在重写了compareTo方法后,我们还需要注意与equals方法同步,特别是使用一些检索类的API时,比如集合类的indexOf方法(内部会遍历集合用equals方法做相等比较)和Collections的binarySearch方法(使用二分查找的方式用compareTo方法来判断目标值与参数值在集合中的位置是否一致,注意:使用二分查找时集合必须是排好序的)。
我们就通过一个例子来看一下求两个集合的交集、并集、差集(由所有属于A但不属于B的元素组成的集合,叫做A与B的差集)。假如现在有两个集合arrayList1{A,B,C,D}和arrayList2{A,A,B,E,E}
- //求交集,两个集合的交集存储进arrayList1中
- arrayList1.retainAll(arrayList2); //arrayList1中的元素为:A B
- //求并集,此时arrayList1中就是两个集合的并集元素了,这里是包含重复元素的
- arrayList1.addAll(arrayList2); //arrayList1中的元素为:A B C D A A B E E
- //求差集,即从arrayList1中删除出现在arrayList2中的元素
- arrayList1.removeAll(arrayList2); //arrayList1中的元素为:C D
这里说的是集合工具类Collections的shuffle方法,我们就其源码看一下它是如何打乱集合元素的。如下:
- public static void shuffle(List<?> list, Random rnd) {
- int size = list.size();
- //为了性能而做的if-else判断
- if (size < SHUFFLE_THRESHOLD || list instanceof RandomAccess) {
- //从后往前遍历集合,取得[0,i)的随机数,将当前元素与这个随机位置处的元素交换位置
- for (int i=size; i>1; i--)
- swap(list, i-1, rnd.nextInt(i));
- } else {
- //这里先将集合数据转成数组,然后打乱数组元素,然后利用迭代器将这个打乱的数组的数据更新到集合中的节点上
- Object arr[] = list.toArray();
- for (int i=size; i>1; i--)
- swap(arr, i-1, rnd.nextInt(i));
- ListIterator it = list.listIterator();
- for (int i=0; i<arr.length; i++) {
- it.next();
- it.set(arr[i]);
- }
- }
- }
集合容器家族非常庞大。大致可以分为几类:
定义常量的方式多种,比如:类常量,接口常量,枚举常量。单说枚举常量,枚举的优点主要表现在以下几个方面。
虽然枚举常量在很多方面比接口常量和类常量好用,但是有一点它是比不上接口常量和类常量的,那就是继承,枚举类型是不能有继承的,但是,一般常量在项目构建时就定义完毕了,很少会出现必须通过扩展才能实现业务逻辑的场景。
在定义枚举的时候有一个建议:定义枚举时,枚举项数量不要超过64,否则建议拆分。
推荐在枚举定义中为每个枚举项定义描述,特别是在大规模的项目开发中,大量的常量项定义使用枚举项描述比在接口常量或类常量中增加注释的方式友好得多,简洁得多。可以使用构造函数来协助完成,如下:
- public enum Season {
- Spring("春"), Summer("夏"), Autumn("秋"), Winter("冬");
-
- private String desc;
-
- //枚举类的构造方法使用private,如果可以构造枚举值,那枚举类的存在价值就没了
- private Season(String desc){
- this.desc = desc;
- }
-
- public String getDesc() {
- return desc;
- }
- }
使用枚举定义常量时,会伴有大量的switch语句判断,目的是为每个枚举项解释其行为,例如这样一个方法:传入null会导致空指针异常,方法如下:
- public static void doSports(Season season) {
- switch (season) {
- case Spring: System.out.println("春天放风筝"); break;
- case Summer: System.out.println("夏天游泳"); break;
- case Autumn: System.out.println("秋天捉知了"); break;
- case Winter: System.out.println("冬天滑雪"); break;
- default: System.out.println("输入错误"); break; //其实这里也可以抛出一个异常,视情况而定
- }
- }
至于为什么会出现空指针异常,这跟Java中switch后可以跟的类型有关系:目前Java中的switch只能判断byte,short,char,int,String(JDK1.7新特性)类型,这是Java编译器的限制。为什么枚举类型也可以跟在switch后面呢?这也是异常出现的原因所在。因为编译时,编译器判断出switch语句后的参数是枚举类型,然后就会获取枚举的排序值(执行枚举项的ordinal()方法,这个方法返回枚举值在枚举类中的声明顺序,从0开始)进而匹配,所以会出现空指针异常。
我们知道注解的写法和接口很类似,都采用了关键字interface,而且都不能有实现代码,常量定义默认都是public static final类型的等,它们的主要不同点是:注解要在interface前加上@字符,而且不能继承,不能实现,这经常会给我们的开发带来一些障碍。我们来分析一个ACL(Access Control List,访问控制列表)设计案例,看看如何避免这些障碍,ACL有三个重要元素:
首先鉴权人接口如下:
- public interface Identifier {
- //无权访问时的礼貌语
- String REFUSE_WORD = "您无权访问";
- //鉴权
- public boolean identify();
- }
用枚举类实现该鉴权接口:
- public enum CommonIdentifier implements Identifier {
- //权限级别
- Reader("读者"), Author("作者"), Admin("管理员");
-
- private String desc;
-
- private CommonIdentifier(String desc) {
- this.desc = desc;
- }
-
- @Override
- public boolean identify() {
- return false;
- }
- }
枚举结合注解实现权限级别控制
- @Retention(RUNTIME)
- @Target({ TYPE, METHOD })
- public @interface Access {
- //默认是管理员级别
- CommonIdentifier level() default CommonIdentifier.Admin;
- }
资源类:
- @Access(level = CommonIdentifier.Author)
- public class Foo {}
ACL的模拟实现:
- public static void main(String[] args) {
- Foo f = new Foo();
- Access access = f.getClass().getAnnotation(Access.class);
- //鉴权失败
- if(access == null || !access.level().identify()) {
- System.out.println(Identifier.REFUSE_WORD);
- }
- }
注意上面模拟实现类中access.level().REFUSE_WORD其中access是一个注解,注解是不能继承的,而上述例子中通过结合枚举类,从而利用注解对象调用了鉴权方法,能调用该方法当然就可以通过该方法获取更多的信息。
Java泛型支持通配符,可以单独使用一个“?”表示任意类,也可以使用extends关键字表示某一个类(接口)的子类型,还可以使用super关键字表示某一个类(接口)的父类型,但问题是什么时候该用extends,什么时候该用super呢?
比如下面的例子:
- public static <E> void read(List<? extends E> list) {
- for(E e : list) {
- //业务逻辑处理,比如这里我可以将list中的元素加入到另外的List<E>集合中,或者作为Map<E,V>的键等
- }
- //比如ArrayList的一个构造方法 public ArrayList(Collection<? extends E> c)
- //它就是读取集合c中的元素,然后加到Object[]中去
- }
- public static void write(List<? super Number> list) {
- list.add(123);
- list.add(3.14);
- }
对于read方法,如果参数改成List<? super E> 那将无法操作,因为我们不知道list到底存放的是什么元素,只能推断出是E类型的父类(当然,也可以是E类型),但问题是E类型的父类是什么呢?无法再推断,只有运行时才知道,那么编码期就完全无法操作了。所以这里for(E e : list)编译都不会通过(因为没办法接收list中的变量,按什么类型接收?编译器不知道啊,这里写的是按照E类型接收,不合适。因为?这个类型是E类型的父类,那么编译器怎么能将?类型自动转换为E类型呢?(自动向下转型,你手动转都可能出现ClassCastException异常,你不能把一个父类实例对象赋值给一个子类类型变量)),所以拒绝执行此操作。当然,你可以把它当作是Object类来处理,需要时再转换成E类型—这完全违背了泛型的初衷。
对于write方法,不管它是Integer类型的123,还是Double类型的3.14,都可以加入到list列表中,因为它们都是Number类型,这就保证了泛型类的可靠性。如果此处是List<? extends Number>则不可以,因为这样只是确定了泛型的上界(Number),具体是什么类型就无法确定,是Integer类型?是Double?还是Byte?这些都符合extends关键字的定义,由于无法确定实际的泛型类型,所以编译器拒绝了此类操作。(编译器:我都不知道list中具体存什么类型你就往里面加,你都没告诉我到底是Integer类型吗你就胡加八加的)。
其实,在Java中协变和逆变我们已经用了很久了,就像下面一样:
- Base base = new Sub(); //Sub类继承Base类,base变量发生协变,声明为Base,实际却是Sub类型。
- Number[] n = new Integer[10]; //数组支持协变
泛型不支持协变,下面这行代码编译不通过。原因就是Java为了保证运行期的安全性,必须保证泛型参数类型是固定的,所以它不允许一个泛型参数可以同时包含两种类型,即使是父子类关系也不行。
List<Number> list = new ArrayList<Integer>();
泛型不支持协变,但可以使用通配符模拟协变,如下:
List<? extends Number> arrayList = new ArrayList<Integer>();
Java虽然可以允许逆变存在,但在对类型赋值上是不允许逆变的,你不能把一个父类实例对象赋值给一个子类类型变量,泛型自然也不允许此种情况发生了,但是它可以使用super关键字来模拟实现,如下:
- /*
- * 这里所有Integer父类型(自身、父类或接口)作为泛型参数,这里看着就像是把一个Number类型的ArrayList赋值给了Integer类型的List,
- * 其外观类似于使用一个宽类型覆盖一个窄类型,它模拟了逆变的实现。
- */
- List<? super Integer> list = new ArrayList<Number>();
泛型既不支持协变也不支持逆变,带有泛型参数的子类型定义与我们经常使用的类类型也不相同,其基本的类型关系如下图所示。
推而广之,Dao<T>应该比Dao<?>、Dao<Object>更先采用,Desc<Person>则比Desc<?>、Desc<Object>更优先采用。
在Java的泛型中,可以使用“&”符号关联多个上界并实现多个边界限定,而且只有上界才有此限定,下界没有多重限定的情况。具体写法就像下面的例子一样。
- public class Me implements Staff, Passenger{
- //指定了泛型类型T必须是Staff和Passenger的共有子类型,
- //此时变量t就具有了这两个接口(类)中限定的方法和属性
- public static <T extends Staff & Passenger> void discount(T t) {
- //这两个方法分别定义在Saff和Passenger中
- if(t.getSalary() < 2500 && t.isStanding()) {
- System.out.println("……");
- }
- }
- //省略getSalary()和isStanding()的实现
- }
-
Java的Class类提供了很多的getDeclared×××方法和get×××方法,例如getDeclaredMethod和getMethod等成对出现,这两者的区别如下:
其他的getDeclaredConstructors和getConstructors、getDeclaredFields和getFields等与此相似。
Java之所以如此处理,是因为反射本意只是正常代码逻辑的一种补充,而不是让正常代码逻辑产生翻天覆地的变动,所以public的属性和方法最容易获取,私有属性和方法也可以获取,但要限定本类。如果需要列出所有继承自父类的方法,该如何实现呢?简单,先获得父类,然后使用getDeclaredMethods,之后持续递归即可。
我们知道,动态修改一个类或方法或执行方法时都会受Java安全体系的制约,而安全的处理是非常消耗资源的(性能非常低),因此对于运行期要执行的方法或要修改的属性就提供了Accessible可选项:由开发者决定是否要逃避安全体系的检查(这个值默认是false的)。Accessible属性只是用来判断是否需要进行安全检查的,如果不需要则直接执行,这就可以大幅度地提升系统性能(当然了,由于逃避了安全检查,也可以运行private方法、访问private私有属性了)。经过测试,在大量的反射情况下,设置Accessible为true可以提升性能20倍以上。我们在设置Field或执行Constructor时,务必要设置Accessible为true,这并不仅仅是因为操作习惯的问题,还是在为我们系统的性能考虑。
我们还是依靠一个例子来看。这个例子是将普通小动物包装一下,获得某些能力。
动物接口(被包装的动物接口):
- public interface IAnimal {
- public void doStuff();
- }
能力接口(装饰器接口):
- public interface IFeature {
- //加载能力
- public void load();
- }
包装动作类:
- public class DecorateAnimal implements IAnimal {
- private IAnimal animal; //被包装的动物
- private Class<? extends IFeature> klass; //使用哪个装饰器
-
- public DecorateAnimal(IAnimal animal, Class<? extends IFeature> klass) {
- this.animal = animal;
- this.klass = klass;
- }
-
- @Override
- public void doStuff() {
- //装饰器类的代理。此处实质上是修改load()方法
- InvocationHandler handler = new InvocationHandler() {
- //具体的包装行为
- @Override
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- Object obj = null;
- //判断load方法的访问权限
- if(Modifier.isPublic(method.getModifiers())) {
- //反射执行load()方法
- obj = method.invoke(klass.newInstance(), args);
- }
- //调用被包装类的doStuff方法
- animal.doStuff();
- return obj;
- }
- };
-
- ClassLoader classLoader = getClass().getClassLoader();
- //动态代理,装饰器类的代理对象
- IFeature proxy = (IFeature)Proxy.newProxyInstance(classLoader, klass.getInterfaces(), handler);
- //执行其代理对象的load()方法,也就是上面的invoke方法
- proxy.load();
- }
- }
到此,核心东西已经构建完毕,现在定义两种能力:
- public class FlyFeature implements IFeature {
- @Override
- public void load() {
- System.out.println("增加飞行能力");
- }
- }
-
- public class DigFeature implements IFeature {
- @Override
- public void load() {
- System.out.println("增加钻地能力");
- }
- }
我们来写一个小示例:创建一个“猫”的类并装饰上两种飞行和钻地能力。
- public static void main(String[] args) {
- IAnimal cat = new IAnimal() {
- @Override
- public void doStuff() {
- System.out.println("我天生就会捉老鼠");
- }
- };
- cat = new DecorateAnimal(cat, FlyFeature.class);
- cat = new DecorateAnimal(cat, DigFeature.class);
- cat.doStuff();
- }
就cat本身而言,本来只有捉老鼠的能力,但是我们用包装动作类在捉老鼠能力之前为其添加了飞行能力,又在飞行和捉老鼠两种能力之前为其添加了钻地能力。而这两种能力其实都是用相同的一段代码“动态代理”为其添加的,如果现在又需要第三种能力或者以后又需要第四种能力,那么我们只需要实现IFeature接口来实现load方法定义好能力就行,然后直接使用包装动作类(根本不需要改动),而且装饰类和被装饰类都是互相独立的,二者通过包装动作类组合到一起,实现了对装饰类和被装饰类的完全解耦,提高了系统的扩展性。
Java中的异常一次只能抛出一个,如果在第一个逻辑片段中抛出异常,则第二个逻辑片段就不再执行了,也就无法抛出第二个异常了,现在的问题是:如何才能一次抛出两个(或多个)异常呢?你可能会问,这种情况可能出现吗?怎么会要求一个方法抛出多个异常呢?绝对可能出现,例如Web界面注册时,展现层依次把User对象传递到逻辑层,Register方法需要对各个Field进行校验并注册,例如用户名不能重复,密码必须符合密码策略等,不要出现用户第一次提交时系统提示“用户名重复”,在用户修改用户名再次提交后,系统又提示“密码长度少于6位”的情况,这种操作模式下的用户体验非常糟糕,最好的解决办法就是封装异常,建立异常容器,一次性地对User对象进行校验,然后返回所有的异常。下面是一段示例代码,MyException类中建立集合用于存储各种异常,在业务中使用时,将所有异常存进MyException对象中,最后抛出异常对象就行,然后可以利用该异常对象获取所有异常,一次性提示。
- public class MyException extends Exception{
- private static final long serialVersionUID = -8823063849980176780L;
- private List<Throwable> cacuses = new ArrayList<Throwable>();
-
- public MyException(List<? extends Throwable> cacuses) {
- this.cacuses.addAll(cacuses);
- }
-
- public List<Throwable> getExceptions(){
- return this.cacuses;
- }
-
- public static void doStuff() throws MyException{
- List<Throwable> list = new ArrayList<Throwable>();
- try { //第一个逻辑片段
- }catch(Exception e) {
- list.add(e);
- }
-
- try { //第一个逻辑片段
- }catch(Exception e) {
- list.add(e);
- }
-
- if(list.size() > 0) {
- throw new MyException(list);
- }
- }
- }
异常仅仅封装还是不够的,还需要传递异常。
比如,我们的J2EE项目一般都有三层结构:持久层、逻辑层、展现层,持久层负责与数据库交互,逻辑层负责业务逻辑的实现,展现层负责UI数据的处理。
有这样一个模块:用户第一次访问的时候,需要持久层从user.xml中读取信息,如果该文件不存在则提示用户创建之。那问题来了:如果我们直接把持久层的异常FileNotFoundException抛弃掉,逻辑层根本无从得知发生了何事,也就不能为展现层提供一个友好的处理结果了,最终倒霉的就是展现层:没有办法提供异常信息,只能告诉用户说“出错了,我也不知道出什么错了”—毫无友好性可言。正确的做法是先封装,然后传递,过程如下:
受检异常不能全都转化为非受检异常,它有自己存在的合理性,但受检异常确实有不足的地方:
受检异常使接口声明脆弱。以下面的User接口为例:
- public interface User {
- //修改用户名密码,抛出安全异常
- public void changePassword() throws MySecurityException;
- }
随着系统的开发,User接口有了多个实现者,比如普通的用户UserImpl、模拟用户MockUserImpl、非实体用户NonUserImpl(如自动执行机、逻辑处理器等)等,此时如果发现changePassword方法可能还需要抛出RejectChangeException(拒绝修改异常,如自动执行机正在处理任务时不能修改其密码),那就需要修改User接口了:changePassword方法增加抛出RejectChangeException异常,这会导致所有的User调用者都要追加对RejectChangeException异常问题的处理。
为了改善上述情况,我们可以将受检异常转化为非受检异常,我们在声明接口时不再声明异常,而是在具体实现时根据不同的情况产生不同的非受检异常,这样持久层和逻辑层抛出的异常将会由展现层自行决定如何展示,不再受异常的规则约束了,大大简化开发工作,提高了代码的可读性。在开发中当受检异常威胁到了系统的安全性、稳定性、可靠性、正确性时,则必须处理,不能转化为非受检异常,其他情况则可以转换为非受检异常。
对于构造函数,错误只能抛出,这是程序人员无能为力的事情;非受检异常不要抛出,抛出了“对己对人”都是有害的;受检异常尽量不抛出,能用曲线的方式实现就用曲线方式实现,总之一句话:在构造函数中尽可能不出现异常。
提取书中几种提升Java性能的基本方法:
我们知道运行一段程序需要三种资源:CPU、内存、I/O,提升CPU的处理速度可以加快代码的执行速度,直接表现就是返回时间缩短了效率提高了;内存是Java程序必须考虑的问题,在32位的机器上,一个JVM最多只能使用2GB的内存,而且程序占用的内存越大,寻址效率也就越低,这也是影响效率的一个因素。I/O是程序展示和存储数据的主要通道,如果它很缓慢就会影响正常的显示效果。所以我们在编码时需要从这三个方面入手接口(当然了,任何程序优化都是从这三方面入手的)。
我们先来看一些不好的注释习惯:
- //自增
- num++;
- /*
- * 2010-09-16 张三 创建该类,实现XX算法
- * 2010-10-28 李四 修正算法中的XXXX缺陷
- * 2010-11-30 李四 重构了XXX方法
- * 2011-02-06 王五 删除了XXXX无用方法
- * 2011-04-08 马六 优化了XXX的性能
- */
好的注释首先要求正确,注释与代码意图吻合;其次要求清晰,格式统一,文字准确;最后要求简洁,说明该说明的,惜字如金,拒绝不必要的注释,如下类型的注释就是好的注释:
什么是职责?职责是一个接口(或类)要承担的业务含义,或是接口(或类)表现出的意图,例如一个User类可以包含写入用户信息到数据库、删除用户、修改用户密码等职责,而一个密码工具类则可以包含解密职责和加密职责。单一职责有以下三个优点:
职责单一在设计和编码中如何应用呢?下面以电话通信为例子来说明如何实施单一职责原则:
- //通信协议
- interface Connection {
- // 拨通电话
- public void dial();
- // 通话完毕,挂电话
- public void hangup();
- }
-
- //数据传输
- interface Transfer {
- // 通话
- public void chat();
- }
注意:接口职责一定要单一,实现类职责尽量单一。
噢,他明白了,河水既没有牛伯伯说的那么浅,也没有小松鼠说的那么深,只有自己亲自试过才知道。——寓言故事《小马过河》
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。