美文网首页
Buggy Java Code:Java程序员最容易犯的10个错

Buggy Java Code:Java程序员最容易犯的10个错

作者: 叩丁狼教育 | 来源:发表于2018-10-17 16:02 被阅读138次

    翻译:叩丁狼教育吴嘉俊

    Java语言最开始是为了交互电视机而开发的,随着时间的推移,他已经广泛应用各种软件开发领域。基于面向对象的设计,屏蔽了诸如C,C++等语言的一些复杂性,提供了垃圾回收机制,平台无关的虚拟机技术,Java创造了一种前所未有的开发方式。另一方面,得益于Java提出的“一次编码,到处运行”的口号,让Java更加出名。但是Java中的异常也是处处发生,下面我就列出了我认为的Java开发最容易出现的10个错误。

    [注:这篇文章还是比较新的文章。老外的技术文章,开篇都喜欢写一堆历史渊源或者背景铺垫的话。为了保证文章的完整性,我原样翻译了]

    #1、重复造轮子

    一个明显的错误就是Java程序员习惯性的忽略已经存在的大量的库。在你决定造一个轮子之间,我建议你试着先搜一下是否有已经存在库。例如日志方面,有logback,新log4j,网络方面,有Netty或者Akka。有一些库,已经逐步变成了标准,比如Java8中加入的Joda-Time。

    下面讲述的是我上一个项目中的个人经历。有一部分用于HTML转义的代码是一个开发自己完成的。这个代码正常工作了多年,但是又一次遇到了一个用户输入,代码陷入了死循环。这个用户发现应用没有反应,又重新输入了一遍,服务器因为这个死循环挂了。如果这个开发使用已有的HTML转义工具,比如Google Guava项目提供的HtmlEscaper,这个严重的问题可能就不会出现。并且现在市面上流行的大部分的开源库,背后都有团队和社区在支持,类似这样的错误,都能够及时的被修复。

    #2、在Switch-Case中错误的使用break

    这是一个很尴尬的问题,但是仍然在实际开发中经常出现。瀑布特性在switch语句中有时会非常有用,但是必要的break关键字的缺失,有时会带来灾难性的后果。比如在下面的代码中,如果在case 0中忘记放一个break关键字,代码会继续向下执行,就会在Zero之后再输出一个One:

    public static void switchCasePrimer() {
            int caseIndex = 0;
            switch (caseIndex) {
                case 0:
                    System.out.println("Zero");
                case 1:
                    System.out.println("One");
                    break;
                case 2:
                    System.out.println("Two");
                    break;
                default:
                    System.out.println("Default");
            }
    }
    
    

    最好的解决办法是使用多态,并把不同的处理代码放到子类中。当然,类似这样的错误,也可以通过类似FindBugs或者PMD这样的工具检查出来。

    #3、忘记释放资源

    一旦打开一个文件,或者建立一个网络连接,一个非常重要的习惯是记得关闭资源。并且一定记得,如果在使用类似这样的资源过程中出现了错误,在异常处理中,也需要做对应的关闭操作。可能有人会说,FileInputStream对象在GC的时候,Java终结器(finalizer)会自动调用其close()方法,但是我们知道,我们无法预知GC在什么时候开始,所以我们无法预知在执行GC之前,会有多少资源无法及时关闭。为了避免这种情况,Java7推出的try-with-resources语法,是值得每个开发使用的。

    private static void printFileJava7() throws IOException {
        try(FileInputStream input = new FileInputStream("file.txt")) {
            int data = input.read();
            while(data != -1){
                System.out.print((char) data);
                data = input.read();
            }
        }
    }
    
    

    try-with-resources语法适用于所有实现了AutoClosable接口的类。它能保证每一个资源及时的关闭。

    #4、内存泄露

    Java使用自动内存管理,所以大部分时间,我们都不会去关心内存的分配和释放,但是,这并不意味着Java开发人员需要忽略内存。在Java应用中,内存的问题也经常出现。我们知道,对象如果没有被引用了,这个对象就会被释放,但是并不意味着,就不会出现内存泄露的问题。在Java中,造成内存泄露的原因有很多,但最容易出现的情况就是对象引用无法释放,因为GC在回收堆内存的时候,如果一个对象仍然被其他对象引用,这个对象空间是不会被回收的,举个例子,如果在类中,有一个静态字段引用到一个集合,假如我们没有手动的在使用完成这个集合之后,将他设置为null,那么这个集合及这个集合中的对象,是永远不会被回收的,因为类静态字段是不会被GC的。

    比如还有一种造成内存泄露的原因,就是一组对象互相引用对方,就是我们经常说的循环引用,因为循环引用,所以GC不能确定这些互相引用的对象是否还有继续存活的必要。还有一种情况,就是使用JNI时的非堆内存泄露。

    一个典型的内存泄露例子:

    final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
    final Deque numbers = new LinkedBlockingDeque<>();
    final BigDecimal divisor = new BigDecimal(51);
    
    scheduledExecutorService.scheduleAtFixedRate(() -> {
        BigDecimal number = numbers.peekLast();
           if (number != null && number.remainder(divisor).byteValue() == 0) {
             System.out.println("Number: " + number);
            System.out.println("Deque size: " + numbers.size());
        }
    }, 10, 10, TimeUnit.MILLISECONDS);
    
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            numbers.add(new BigDecimal(System.currentTimeMillis()));
        }, 10, 10, TimeUnit.MILLISECONDS);
    
    try {
        scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    
    

    在上面的例子中,我们创建了两个定时任务。第一个定时任务,从deque中获取了最后的一个数字”numbers”并判断,如果这个数字能被51整除,则打印该数字和deque的大小。第二个定时任务,不断的向deque中添加数据。两个任务都间隔10ms执行。如果这个代码执行,你会发现,deque的大小会持续的增加,直到deque中的数据占满整个堆空间。为了阻止这种情况的发生,我们可以使用pollLast方法来代替peekLast方法,因为pollLast方法会在拿到最后一个元素之后,把这个元素从deque中移除。

    #5、过度产生垃圾数据

    过度产生垃圾数据的意思,是程序运行中大量产生短声明周期的对象。这回导致GC频繁的执行,从内存中回收空间,GC的执行是需要完成堆扫描的,这对系统的性能影响是非常大的。下面是一个小例子:

    String oneMillionHello = "";
    for (int i = 0; i < 1000000; i++) {
        oneMillionHello = oneMillionHello + "Hello!";
    }
    System.out.println(oneMillionHello.substring(0, 6));
    
    

    在Java中,字符串是不可变的,所以每一次循环都会创建一个新的字符串对象。为了改进这种代码,我们可以使用StringBuilder来代替:

    StringBuilder oneMillionHelloSB = new StringBuilder();
        for (int i = 0; i < 1000000; i++) {
            oneMillionHelloSB.append("Hello!");
        }
    System.out.println(oneMillionHelloSB.toString().substring(0, 6));
    
    

    第二个版本的代码,在执行的时候会提高不少的性能。

    原文:https://www.voxxed.com/2017/02/buggy-java-code-mistakes-part-i/

    想获取更多技术视频,请前往叩丁狼官网:http://www.wolfcode.cn/openClassWeb_listDetail.html

    相关文章

      网友评论

          本文标题:Buggy Java Code:Java程序员最容易犯的10个错

          本文链接:https://www.haomeiwen.com/subject/abjrzftx.html