今天逛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)));
}
输出结果:
可以看出,在值为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)));
}
输出结果:
和
=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?
先看下源代码
可以看
sleep
是native
方法,意味着它是通过调用 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方法的注释
所以你需要再调用一次
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 要配合使用。
结束
没想到而且
划水
也会被背刺,真是心酸。还好学到了一些知识,心态平衡了许多。