7

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

 2 years ago
source link: https://my.oschina.net/u/4526289/blog/5313217
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

摘要:在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);//false
System.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 127
Integer i1 = 100;
Integer i2 = 100;
System.out.println(i1 == i2);//true
i1 = 300;
i2 = 300;
System.out.println(i1 == i2);//false
System.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
程序运行时间: 6078ms
StringBuilder操作
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值一致,则判定为已存在,则不会重复添加,此时则不会出现内存溢出。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK