Java编程中十大最糟糕的事情

Java已经成为最受欢迎和语言与平台之一,但是在实际编程中有不少坑,了解了这些坑,你还敢说精通Java,如果它们是面试题,你能得几分?当然你也可以把这些坑看成是在坑黑Java:

1. Weak Soft和Phantom等软引用是邪恶的,它们会导致不可预期奇怪的Bug,使用这些引用导致你的程序运行时的行为就会依赖GC垃圾回收机制,因为 GC是自行根据情况动态运行的,这就会让你的程序变得无法预期。

2.每个对象是互斥的mutex,如果你有一个指向已经锁住对象的引用,如下:


List<Integer> list = getList();
synchronized(list){
..
}

这种使用同步的方式是不对的,会引起死锁。
所以,尽量不要使用普通对象作为锁,而是使用JDK7以后专门提供的Lock系列专门锁:

Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}

3.每个对象都是Condition的,也就是包含运行背景线程的条件,比如上一条的锁,Object这个对象内还有wait notify和notifyAll等方法,可以使用JDK7以后提供的Condition,通过这个Condition对象提供的await sigmal等方法来判断当前线程的情况:


class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();

final Object[] items = new Object[100];
int putptr, takeptr, count;

public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await();
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal();
} finally {
lock.unlock();
}
}

public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await();
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}

4.每个对象都有hashCode 和equals 方法,程序员为了在集合中保存对象就必须实现这些方法,接口应该事先已经加入了这些实现:


interface Hashable{
int hashCode();
}
interface Equatable<T>{
boolean equals(T other);
}

5.检测到的Exception,它们在lambda和方法引用等函数编程中会很坏(函数风格是只有一个输入和输出,而抛出Exception属于有第二个输出,这些会导致函数链式chain或流式执行的中断,可以使用Option包装正确与出错的结果),它们的丑陋之处可见这里

6.数组是Covariance(逆变类型),这是类型系统必然会碰到的类型变异variance问题,比如下面代码:


String strings[] = {"Broken","Type", "system"};
Object objects[] = strings;
objects[0] = 69;
// 能够编译,但是运行时会抛出ArrayStoreException

这里数组是covariant逆变类型富有幻想的程序员总是将String[]看成是Object[]子对象。比如如果Cat是Animal的子对象,那么Cat的集合肯定也是Animal集合的子对象吗?这时Cat集合就是一种covariant逆变类型,这种类型变异会误导程序员。

上面代码中String[]属于Covariant类型,虽然String是Object子类型,但是String集合String[]不一定是Object集合Object[]的子类型,而是Covariant类型。

对于Covariant类型只有你将东东从其中取出来时就不会出错,上面代码出错是试图Covariant类型写入,现在改为读取就很安全:


String strings[] = {"Broken","Type", "System"};
Object objects[] = strings;
Object obj = objects[0] ;
//safe

Contravariant反逆变类型则是在将东东放入时是安全的,什么是Contravariant反逆变类型?也就是Animal集合是Cat集合的反逆变类型。

7.Byte总是带符号Signed的,Java语言有带符号的字节类型,但没有不带符号的类型,这不同于整数型,当将带符号字节转换成int整数类型时,每个字节都需要屏蔽,见:Java带符号字节类型是一个错误

8.方法重载的解决规则是狡猾的:


void fn(float x);
void fn(double x);
// 使用一个整数型调用
fn(1);

那么语言会在fn(float)和fn(double)两者之间下选择哪个执行fn(1)呢?选择fn(float),因为它更规范specific,因为float是32位signed floating指针类型,而double是一个64位的,这样我们可以把float压挤到double中,所以选择float。:)

那么对于引用类型方法重载怎么解决?


class Foo{
}
class Bar extends Foo{
}
void fn(Foo foo);
void fn(Bar bar);
// call fn
fn(null);

调用fn方法是一个空参数,有两个候选方法,每个Bar是一个Foo但不是每个Foo是Bar,这样选择Bar因为它更规范specific,

如果我们有第三个候选者,比如是String类型:


class Foo{
}
class Bar extends Foo{
}
void fn(Foo foo);
void fn(Bar bar);
void fn(String s);
// call fn, 歧义调用ambiguous call
fn(null);

这是一个歧义调用,因为String和Bar之间没有任何关系。

这种情况在函数编程的lambda和方法引用中会变得很痛苦:


ExecutorService executorService = Executors.newSingleThreadExecutor();
executorService.submit(()->{
throw new RuntimeException();
});

现在你猜它会选择Callable环视Runnable呢?
答案是Callable,因为其他Runnable返回的是void,你能将其分配给一个Future对象:

ExecutorService executorService = Executors.newSingleThreadExecutor();
Future<Object> future = executorService.submit(()->{
throw new RuntimeException();
});

如果你想掌握主导这种规律,你应该在每天睡觉前阅读Oracle的规范

9.extends和super关键词对于描述逆变covariant 和反逆变 contravariant类型容易混淆,你需要了解PECS (short for Producer extends and Consumer super) ,如果你从一个泛型集合中拉取其中条目元素,那么这个集合是一个生产者Producer,你就应该使用extends,而如果把元素放入集合,这个集合是一个消费者Consumer,你应该使用super,如果你对于一个集合既取又放,那么你就不应该是一extends或super。

10. Cloneable却没有clone方法,需要每个实现编程去完成。


The 10 Worst Things In Java | Code Lexems