写点什么

Java 编程中忽略这些细节,Bug 肯定找上你

  • 2021 年 11 月 19 日
  • 本文字数:4283 字

    阅读完需:约 14 分钟

​​摘要:在 Java 语言的日常编程中,也存在着容易被忽略的细节,这些细节可能会导致程序出现各种 Bug。

 

本文分享自华为云社区《Java编程中容易忽略的细节总结丨【奔跑吧!JAVA】》,作者:jackwangcumt 。

 

Java 语言构建的各类应用程序,在人类的日常生活中占用非常重要的地位,各大 IT 厂商几乎都会使用它来构建自己的产品,为客户提供服务。作为一个企业级应用开发语言,稳定和高效的运行,至关重要。在 Java 语言的日常编程中,也存在着容易被忽略的细节,这些细节可能会导致程序出现各种 Bug,下面就对这些细节进行一些总结:

1、相等判断中的==和 equals

    

在很多场景中,我们都需要判断两个对象是否相等,一般来说,判定两个对象的是否相等,都是依据其值是否相等,如两个字符串 a 和 b 的值都为"java",则我们认为二者相等。在 Java 中,有两个操作可以判断是否相当,即==和 equals,但二者是有区别的,不可混用。下面给出示例:


String a = "java";String b = new String("java");System.out.println(a == b);//falseSystem.out.println(a.equals(b));//true
复制代码


​字符串 a 和 b 的字面值都为"java",用 a == b 判断则输出 false,即不相等,而 a.equals(b)则输出 true,即相等。这是为什么呢?在 Java 中,String 是一个不可变的类型,一般来说,如果两个 String 的值相等,默认情况下,会指定同一个内存地址,但这里字符串 String b 用 new String 方法强制生成一个新的 String 对象,因此,二者内存地址不一致。由于 == 需要判断对象的内存地址是否一致,因此返回 false,而 equals 默认(override 后可能不一定)是根据字面值来判断,即相等。


下面再给出一个示例:


//integer -128 to 127Integer i1 = 100;Integer i2 = 100;System.out.println(i1 == i2);//truei1 = 300;i2 = 300;System.out.println(i1 == i2);//falseSystem.out.println(i1.equals(i2));//true
复制代码


​这是由于 Java 中的 Integer 数值的范围为-128 到 127,因此在这范围内的对象的内存地址是一致的,而超过这个范围的数值对象的内存地址是不一致的,因此 300 这个数值在 == 比较下,返回 false,但在 equals 比较下返回 true。

2、switch 语句中丢失了 break

    

在很多场景中,我们需要根据输入参数的范围来分别进行处理,这里除了可以使用 if ... else ...语句外,还可以使用 switch 语句。在 switch 语句中,会罗列出多个分支条件,并进行分别处理,但如果稍有不注意,就可能丢失关键字 break 语句,从而出现预期外的值。下面给出示例:


//缺少break关键字 public static void switchBugs(int v ) {       switch (v) {            case 0:                System.out.println("0");                //break            case 1:                System.out.println("1");                break;            case 2:                System.out.println("2");                break;            default:                System.out.println("other");       }}
复制代码


​如果我们使用如下语句进行调用:


switchBugs(0);
复制代码


​则我们预期返回"0",但是却返回"0" "1"。这是由于 case 0 分支下缺少 break 关键字,则虽然程序匹配了此分支,但是却能穿透到下一个分支,即 case 1 分支,然后遇到 break 后返回值。

3、大量的垃圾回收,效率低下

    

字符串的拼接操作,是非常高频的操作,但是如果涉及的拼接量很大,则如果直接用 + 符号进行字符串拼接,则效率非常低下,程序运行的速度很慢。下面给出示例:


private static void stringWhile(){    //获取开始时间    long start = System.currentTimeMillis();    String strV = "";    for (int i = 0; i < 100000; i++) {        strV = strV + "$";    }    //strings are immutable. So, on each iteration a new string is created.    // To address this we should use a mutable StringBuilder:    System.out.println(strV.length());    long end = System.currentTimeMillis(); //获取结束时间    System.out.println("程序运行时间: "+(end-start)+"ms");    start = System.currentTimeMillis();    StringBuilder sb = new StringBuilder();    for (int i = 0; i < 100000; i++) {        sb.append("$");    }    System.out.println(strV.length());    end = System.currentTimeMillis();    System.out.println("程序运行时间: "+(end-start)+"ms");}
复制代码


​上述示例分别在循环体中用 + 和 StringBuilder 进行字符串拼接,并统计了运行的时间(毫秒),下面给出模拟电脑上的运行结果:


//+ 操作100000程序运行时间: 6078msStringBuilder操作100000程序运行时间: 2ms
复制代码


​由此可见,使用 StringBuilder 构建字符串速度相比于 + 拼接,效率上高出太多。究其原因,就是因为 Java 语言中的字符串类型是不可变的,因此 + 操作后会创建一个新的字符串,这样会涉及到大量的对象创建工作,也涉及到垃圾回收机制的介入,因此非常耗时。

4、循环时删除元素

    

有些情况下,我们需要从一个集合对象中删除掉特定的元素,如从一个编程语言列表中删除 java 语言,则就会涉及到此种场景,但是如果处理不当,则会抛出 ConcurrentModificationException 异常。下面给出示例:


private static void removeList() {    List<String> lists = new ArrayList<>();    lists.add("java");    lists.add("csharp");    lists.add("fsharp");    for (String item : lists) {        if (item.contains("java")) {            lists.remove(item);        }    }}
复制代码


​运行上述方法,会抛出错误,此时可以用如下方法进行解决,即用迭代器 iterator,具体如下所示:


private static void removeListOk() {    List<String> lists = new ArrayList<>();    lists.add("java");    lists.add("csharp");    lists.add("fsharp");    Iterator<String> hatIterator = lists.iterator();    while (hatIterator.hasNext()) {        String item = hatIterator.next();        if (item.contains("java")) {            hatIterator.remove();        }    }    System.out.println(lists);//[csharp, fsharp]}
复制代码


5、null 引用

    

在方法中,首先应该对参数的合法性进行验证,第一需要验证参数是否为 null,然后再判断参数是否是预期范围的值。如果不首先进行 null 判断,直接进行参数的比较或者方法的调用,则可能出现 null 引用的异常。下面给出示例:


private static void nullref(String words)  {    //NullPointerException    if (words.equals("java")){        System.out.println("java");    }else{        System.out.println("not java");    }}
复制代码


​如果此时我们用如下方法进行调用,则抛出异常:


nullref(null)
复制代码


​这是由于假设了 words 不为 null,则可以调用 String 对象的 equals 方法。下面可以稍微进行一些修改,如下所示:


private static void nullref2(String words)  {    if ("java".equals(words)){        System.out.println("java");    }else{        System.out.println("not java");    }}
复制代码


​则此时执行则可以正确运行:


nullref2(null)
复制代码


6、hashCode 对 equals 的影响

     

前面提到,equals 方法可以从字面值上来判断两个对象是否相等。一般来说,如果两个对象相等,则其 hash code 相等,但是如果 hash code 相等,则两个对象可能相等,也可能不相等。这是由于 Object 的 equals 方法和 hashCode 方法可以被 Override。下面给出示例:   


package com.jyd;import java.util.Objects;public class MySchool {    private String name;    MySchool(String name) {        this.name = name;    }    @Override    public boolean equals(Object o) {        if (this == o) {            return true;        }        if (o == null || getClass() != o.getClass()) {            return false;        }        MySchool _obj = (MySchool) o;        return Objects.equals(name, _obj.name);    }    @Override    public int hashCode() {        int code = this.name.hashCode();        System.out.println(code);        //return code; //true        //随机数        return (int) (Math.random() * 1000);//false
}}Set<MySchool> mysets = new HashSet<>();mysets.add(new MySchool("CUMT"));MySchool obj = new MySchool("CUMT");System.out.println(mysets.contains(obj));
复制代码


​执行上述代码,由于 hashCode 方法被 Override,每次返回随机的 hash Code 值,则意味着两个对象的 hash code 不一致,那么 equals 判断则返回 false,虽然二者的字面值都为"CUMT"。

7、内存泄漏

     

我们知道,计算机的内存是有限的,如果 Java 创建的对象一直不能进行释放,则新创建的对象会不断占用剩余的内存空间,最终导致内存空间不足,抛出内存溢出的异常。内存异常基本的单元测试不容易发现,往往都是上线运行一定时间后才发现的。下面给出示例:  


package com.jyd;
import java.util.Properties;//内存泄漏模拟public class MemoryLeakDemo { public final String key; public MemoryLeakDemo(String key) { this.key =key; } public static void main(String args[]) { try { Properties properties = System.getProperties(); for(;;) { properties.put(new MemoryLeakDemo("key"), "value"); } } catch(Exception e) { e.printStackTrace(); } }
/* @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; MemoryLeakDemo that = (MemoryLeakDemo) o; return Objects.equals(key, that.key); }
@Override public int hashCode() { return Objects.hash(key); } */
}
复制代码


​此示例中,有一个 for 无限循环,它会一直创建一个对象,并添加到 properties 容器中,如果 MemoryLeakDemo 类未给出自己的 equals 方法和 hashCode 方法,那么这个对象会被一直添加到 properties 容器中,最终内存泄漏。但是如果定义了自己的 equals 方法和 hashCode 方法(被注释的部分),那么新创建的 MemoryLeakDemo 实例,由于 key 值一致,则判定为已存在,则不会重复添加,此时则不会出现内存溢出。


点击关注,第一时间了解华为云新鲜技术~

发布于: 3 小时前阅读数: 9
用户头像

提供全面深入的云计算技术干货 2020.07.14 加入

华为云开发者社区,提供全面深入的云计算前景分析、丰富的技术干货、程序样例,分享华为云前沿资讯动态,方便开发者快速成长与发展,欢迎提问、互动,多方位了解云计算! 传送门:https://bbs.huaweicloud.com/

评论

发布
暂无评论
Java编程中忽略这些细节,Bug肯定找上你