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

2021年11月21日 阅读数:9
这篇文章主要向大家介绍Java编程中忽略这些细节,Bug确定找上你,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。
摘要:在Java语言的平常编程中,也存在着容易被忽略的细节,这些细节可能会致使程序出现各类Bug。

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

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

1 相等判断中的==和equals

在不少场景中,咱们都须要判断两个对象是否相等,通常来讲,断定两个对象的是否相等,都是依据其值是否相等,如两个字符串a和b的值都为"java",则咱们认为两者相等。在Java中,有两个操做能够判断是否至关,即==和equals,但两者是有区别的,不可混用。下面给出示例:app

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后可能不必定)是根据字面值来判断,即相等。dom

下面再给出一个示例:编程语言

//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。ide

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后返回值。ui

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

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

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值一致,则断定为已存在,则不会重复添加,此时则不会出现内存溢出。

 

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