关于 Java 编码中的一些细节问题

今天逛B乎遇到这么一个提问《能讲些关于java的笑话吗?》。原本是抱着摸鱼找乐子的目的打开的,但是看到热门回答的中有几个问题是我不清楚的,有被刺到,所以整理了问题和答案,用文字记录一下。

Integer 类型的数据的比较

问题:当你⽤Integer类型的时候,要⾮常⼩⼼,因为100等于100、但是200不等于200,当然,如果 你会⼀点⼩花招,也可以让100不等于100、让200等于200。

写个程序测试下:

    @Test
    public void test() {
        Integer a100 = 100;
        Integer b100 = 100;
        Integer c100Obj = new Integer(100);
        Integer d100Obj = new Integer(100);

        System.out.println("(a100==b100) = " + (a100 == b100));
        System.out.println("(a100.equals(b100)) = " + (a100.equals(b100)));
        System.out.println("(a100==c100Obj) = " + (a100 == c100Obj));
        System.out.println("(a100.equals(c100Obj)) = " + (a100.equals(c100Obj)));
        System.out.println("(c100Obj==d100Obj) = " + (c100Obj == d100Obj));
        System.out.println("(c100Obj.equals(d100Obj)) = " + (c100Obj.equals(d100Obj)));
    }

输出结果:

image.png

可以看出,在值为100时:

  • 两个直接赋值的Integer对象(a100、b100),使用 ==equals 都能达到预期效果:ture
  • 使用new创建的Integer对象(c100Obj、d100Obj),因为 ==比较的是内存地址,所以结果是 false
  • 不管是哪种初始化方式,使用equals 总能获取预期的结果:true

接下来看下 =200的情况:

    @Test
    public void test() {
        Integer a200 = 200;
        Integer b200 = 200;
        Integer c200Obj = new Integer(200);
        Integer d200Obj = new Integer(200);

        System.out.println("(a200==b200) = " + (a200 == b200));
        System.out.println("(a200.equals(b200)) = " + (a200.equals(b200)));
        System.out.println("(a200==c200Obj) = " + (a200 == c200Obj));
        System.out.println("(a200.equals(c200Obj)) = " + (a200.equals(c200Obj)));
        System.out.println("(c200Obj==d200Obj) = " + (c200Obj == d200Obj));
        System.out.println("(c200Obj.equals(d200Obj)) = " + (c200Obj.equals(d200Obj)));
    }

输出结果:

image.png

=100的输出结果相比,唯一的区别是 (a200==b200) = false,这是因为在 Integer a200 = 200 赋值时,调用了 java.lang.Integer#valueOf(int) 方法,这个方法的逻辑是,如果数值在-128~127之间,就从IntegerCache.cache中获取已缓存的 Integer 对象,所以=200时,其实比较的是对象的内存地址。

java.lang.Integer#valueOf(int) 代码:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

结论:包装类型(例如Integer)比较时,一律用equals方法比较。

equals 的逻辑是:先比较对象,对象一致再比较基础类型的值。

    public boolean equals(Object obj) {
        if (obj instanceof Integer) {
            return value == ((Integer)obj).intValue();
        }
        return false;
    }

浮点数(Double)的比较

问题:当你⽤double的时候,也要⾮常⼩⼼,因为你觉得相等的2个数字在Java⾥可能是不相等的,你

认为相差100万的两个数字却是相等的。

代码示例

    public void test3() {
        System.out.println(0.1 + 0.2 == 0.3);//输出false

        Double a = 0.1;
        Double b = 0.2;
        System.out.println((a + b) == 0.3);//输出false

        Double aObj = new Double(0.1);
        Double bObj = new Double(0.2);
        System.out.println((aObj + bObj) == 0.3);//输出false

        Double cObj = new Double(0.3);
        System.out.println((aObj + bObj) == cObj);//输出false

        BigDecimal decimalA = new BigDecimal(0.1);
        BigDecimal decimalB = new BigDecimal(0.2);
        BigDecimal decimalC = new BigDecimal(0.3);
        System.out.println(decimalA.add(decimalB).equals(decimalC)); //输出false
    }

可以看出无论怎么比较,结果都是false。这是因为浮点数是存在误差的,也就是说0.1在计算机中存储时不是精确的0.1,而有可能是0.1000000001,或者其他数,而0.2或0.3也是如此,所以0.1+0.2和0.3在计算机中是不相等的。

结论:因为浮点数存在这个特性,所以我们在编程中间要尽量避免用浮点数进行比较。如果真的要比较,那就控制精度,比如取到小数点后两位,然后进行比较。

System.out.print 对性能的影响

问题:Java号称是⾼并发、⾼性能的,但实际上如果你⽤了Java的标准输出(System.out.print()),

那我保证你⾼并发不起来。

System.out.print 的源码:

    public void print(String s) {
        if (s == null) {
            s = "null";
        }
        write(s);
    }

    private void write(String s) {
        try {
            synchronized (this) {
                ensureOpen();
                textOut.write(s);
                textOut.flushBuffer();
                charOut.flushBuffer();
                if (autoFlush && (s.indexOf('\n') >= 0))
                    out.flush();
            }
        }
        catch (InterruptedIOException x) {
            Thread.currentThread().interrupt();
        }
        catch (IOException x) {
            trouble = true;
        }
    }

可以看出,在 write 方法中使用了同步锁 synchronized,从而影响性能。

结论:并发场景下优先使用日志框架打印日志。

Thread.sleep 问题

问题:你可以sleep(1),表示你想暂停执行1ms,但Java到底是暂停1ms还是10ms、20ms?

先看下源代码

image.png

可以看 sleepnative 方法,意味着它是通过调用 JVM 本地的 C 代码实现的。作者在代码注释里也描述了这种情况的原因:subject to the precision and accuracy of system timers and schedulers(受制于系统定时器和调度器的精度和准确性), 这也就是 sleep 不精确的原因。

结论:了解原因就好,我至今还没遇到过要控制时间精度的问题。

printStackTrace 对性能的影响

这个问题和 System.out.print 的原因是一样的,都是内部用了 synchronized 导致的性能问题。

代码:

    try {
        throw new RuntimeException("Test");
    } catch (RuntimeException e) {
        e.printStackTrace();
    }
        
    public void printStackTrace() {
        printStackTrace(System.err);
    }

    public void printStackTrace(PrintStream s) {
        printStackTrace(new WrappedPrintStream(s));
    }

    private void printStackTrace(PrintStreamOrWriter s) {
        // Guard against malicious overrides of Throwable.equals by
        // using a Set with identity equality semantics.
        Set<Throwable> dejaVu =
            Collections.newSetFromMap(new IdentityHashMap<Throwable, Boolean>());
        dejaVu.add(this);

        synchronized (s.lock()) {
            // Print our stack trace
            s.println(this);
            StackTraceElement[] trace = getOurStackTrace();
            for (StackTraceElement traceElement : trace)
                s.println("\tat " + traceElement);

            // Print suppressed exceptions, if any
            for (Throwable se : getSuppressed())
                se.printEnclosedStackTrace(s, trace, SUPPRESSED_CAPTION, "\t", dejaVu);

            // Print cause, if any
            Throwable ourCause = getCause();
            if (ourCause != null)
                ourCause.printEnclosedStackTrace(s, trace, CAUSE_CAPTION, "", dejaVu);
        }
    }

finalize 方法

java.lang.Object#finalize 方法用于在GC时被调用起来执行某些操作。引用一段别人的解释:

当对象变成(GC Roots)不可达时,GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。执行finalize方法完毕后,GC会再次判断该对象是否可达,若不可达,则进行回收,否则,对象“复活”。

Java 的 GC 时机本身就不好判断,finalize还是在GC的时候生效的,会影响GC回收策略的方法,很难想象什么场景会用到它。

代码示例:

public class FinalizationDemo {
    public static void main(String[] args) {
        Cake c1 = new Cake(1);
        Cake c2 = new Cake(2);
        Cake c3 = new Cake(3);

        c2 = c3 = null;
        System.gc(); //调用Java垃圾收集器

        ThreadUtil.sleep(3000, TimeUnit.MILLISECONDS);
        c1 = null;
        System.gc(); //调用Java垃圾收集器

        ThreadUtil.sleep(3000, TimeUnit.MILLISECONDS);
    }
}

class Cake extends Object {
    private int id;
    public Cake(int id) {
        this.id = id;
        System.out.println("Cake Object " + id + "is created");
    }

    @Override
    protected void finalize() throws java.lang.Throwable {
        super.finalize();  //finalize的调用方法
        System.out.println("Cake Object " + id + "is disposed");
    }
}

Futurn 的 isDone、get

如果你只用 java.util.concurrent.Future#isDone 方法来判断线程是否结束,那很可能线程出了异常,你也察觉不到(不管有没有try-catch)。因为 isDone 方法在线程正常结束、异常结束、线程被取消是都会返回true

isDone方法的注释

image.png

所以你需要再调用一次java.util.concurrent.Future#get()来获取异常。代码示例:

    @Test
    public void test() {
        try {
            MyThreadA threadA = new MyThreadA();

            ExecutorService executorService = Executors.newCachedThreadPool();
            Future<?> future = executorService.submit(threadA);
            while (!future.isDone()) {
                ThreadUtil.sleep(1000);
            }

            System.out.println("===isDone over===");

            Object o = future.get();
            System.out.println("print future.get : " + o);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public class MyThreadA extends Thread{
        @Override
        public void run() {
            throw new RuntimeException("Test");
        }
    }

不得不说,这项设计很反直觉,而且直接绕过了 try-catch 语句,增加多线程的使用的风险。

结论:使用 Future 时,isDone 和 get 要配合使用。

结束

没想到而且划水也会被背刺,真是心酸。还好学到了一些知识,心态平衡了许多。

猜你喜欢

转载自juejin.im/post/7124938265182306335