Java不为人知的10个真相
你是不是一开始就用Java来编程的呢?还记得当年它还被称为"Oak",OO还是热门的话题,C++的用户觉得Java没有前景,applets还只是个小玩意,菊花也还是一种花的时候吗?
我敢打赌下面至少有一半是你不清楚的。这周我们来看一下跟Java的内部实现相关的一些神奇的事情。
- 其实根本没有受检查异常这回事
没错!JVM压根儿就不知道有这个东西,它只存在于Java语言里。
如今大家都承认受检查异常就是个错误。正如Bruce Eckel最近在布拉格的的GeeCON会议上所说的,除了Java外没有别的语言会使用受检查异常这种东西,即使是Java 8的新的Streams API中也不再使用这一异常了(不然当你的lambda表达式中用到IO或者JDBC的话就得痛苦死了)。
如何能证实JVM确实不知道这个异常?看下下面这段代码:
public class Test {
// No throws clause here
public static void main(String[] args) {
doThrow(new SQLException());
}
static void doThrow(Exception e) {
Test.<RuntimeException> doThrow0(e);
}
@SuppressWarnings("unchecked")
static <E extends Exception>
void doThrow0(Exception e) throws E {
throw (E) e;
}
}
这不仅能通过编译,而且也的确会抛出SQLException异常,并且完全不需要用到Lombok的@SneakyThrows注解。
更进一步的分析可以看下这篇文章,或者Stack Overflow上的这个问题。
- 不同的返回类型也可以进行方法重载
这个应该是编译不了的吧?
class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
是的。Java语言并不允许同一个类中出现两个重写等价("override-equivalent")的方法,不管它们的throws子句和返回类型是不是不同的。
不过等等。看下Java文档中的Class.getMethod(String, Class...)是怎么说的。里面写道:
尽管Java语言不允许一个类中的多个相同签名的方法返回不同的类型,但是JVM并不禁止,所以一个类中可能会存在多个相同签名的方法。这添加了虚拟机的灵活性,可以用来实现许多语言特性。比如说,可以通过bridge方法来实现协变返回(covariant return,即虚方法可以返回子类而不一定得是基类),bridge方法和被重写的方法拥有相同的签名,但却返回不同的类型。 哇,这倒有点意思。事实上,下面这段代码就会触发这种情况:
abstract class Parent<T> {
abstract T x();
}
class Child extends Parent<String> {
@Override
String x() { return "abc"; }
}
看一下Child类所生成的字节码:
// Method descriptor #15 ()Ljava/lang/String;
// Stack: 1, Locals: 1
java.lang.String x();
ldc <String "abc"> [16]
areturn
Line numbers:
[pc: 0, line: 7]
Local variable table:
[pc: 0, pc: 3] local: this index: 0 type: Child
// Method descriptor #18 ()Ljava/lang/Object;
// Stack: 1, Locals: 1
bridge synthetic java.lang.Object x();
aload_0 [this]
invokevirtual Child.x() : java.lang.String [19]
areturn
Line numbers:
[pc: 0, line: 1
在字节码里T其实就是Object而已。这理解起来就容易多了。
synthetic bridge方法是由编译器生成的,因为在特定的调用点Parent.x()签名的返回类型应当是Object类型。如果使用了泛型却没有这个bridge方法的话,代码的二进制形式就无法兼容了。因此,修改JVM以支持这个特性貌似更容易一些(这顺便还实现了协变返回),看起来还挺不错 的吧?
你有深入了解过Java语言的规范和内部实现吗?这里有许多很有意思的东西。
- 它们都是二维数组!
class Test {
int[][] a() { return new int[0][]; }
int[] b() [] { return new int[0][]; }
int c() [][] { return new int[0][]; }
}
是的,没错。也许你无法马上说出上述方法的返回类型是什么,但它们的确都是一样的!同样的还有下面这段代码:
class Test {
int[][] a = ;
int[] b[] = ;
int c[][] = ;
}
你是不是觉得这有点疯狂?相像一下如果再用上JSR-308/Java 8中的类型注解吧。各种写法数不胜数。
@Target(ElementType.TYPE_USE)
@interface Crazy {}
class Test {
@Crazy int[][] a1 = ;
int @Crazy [][] a2 = ;
int[] @Crazy [] a3 = ;
@Crazy int[] b1[] = ;
int @Crazy [] b2[] = ;
int[] b3 @Crazy [] = ;
@Crazy int c1[][] = ;
int c2 @Crazy [][] = ;
int c3[] @Crazy [] = ;
}
类型注解。神秘之极,强大之极。 或者这么说:
亲爱的同事,提交完这段代码下月我就要休假了:
这些写法到底有什么用,这个就留给你自己慢慢探索吧。
- 你根本就不懂条件表达式
那么,你以为条件表达式你就很了解了吗?我告诉你吧,你压根就不懂。很多人都认为下面两段代码是一样的:
Object o1 = true ? new Integer(1) : new Double(2.0);
它和下面这个是一样的吧?
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
不是的。我们来测试下。
System.out.println(o1);
System.out.println(o2);
这段代码会输出:
.0
没错,条件操作符在"必要"的时候会进行数值类型的提升,这个“必要”得加上一个重重的引号。你能想到下面这段程序居然会抛出一个空指针异常吗?
Integer i = new Integer(1);
if (i.equals(1))
i = null;
Double d = new Double(2.0);
Object o = true ? i : d; // NullPointerException!
System.out.println(o);
想了解更多请参考这里。
- 你也不懂复合赋值操作符
很奇怪吧?我们再来看下这两段代码:
i += j;
i = i + j;
显然他们都是一样的嘛。不过,其实不然。Java语言规范中是这么说的:
E1 op= E2形式的复合赋值表达式等价于E = (T)((E1) op (E2)),这里的T的类型是E1,E1仅会进行一次求值。
这太奇妙了。我想引用一下Peter Lawre在Stack Overflow上面的一个回答:
这种类型的类型转换通过*=或者/=可以很容易地说明:
byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57
或者
byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40
或者
char ch = '0';
ch *= 1.1;
System.out.println(ch); // prints '4'
又或者:
char ch = 'A';
ch *= 1.5;
System.out.println(ch); // prints 'a'
那么这么做到底有什么用?你可以在你的程序里试一下char类型的类型转换和乘法操作。因为你懂的,这够装逼。。
- 随机整数
这更像是一个脑筋急转弯。先不要着急看答案。看你能不能自己搞定。当运行如下这段程序时:
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
“偶尔”它会输出这样的结果:
这特么可能么?
. . . . . . . . .
要剧透了,答案就要来了。。
. . . . . . . .
好吧,答案在这里,你得通过反射重写掉JDK的Integer里面的缓存,然后再使用自动装箱和拆箱的功能。不要真的这么做!换句话说,我们再强调一次:
亲爱的同事,提交完这段代码,我下月就休假了哦(你看不懂可别怪我):
- GOTO
这是我最津津乐道的。Java其实也有GOTO!敲下代码看看
int goto = 1;
它的输出是
Test.java:44: error: <identifier> expected
int goto = 1;
这是因为goto是一个预留关键字,万一以后有用呢。。。
不过这还不是重点。有意思的是你可以通过break,continue以及标签块来实现goto的功能:
跳转到前面:
label: {
// do stuff
if (check) break label;
// do more stuff
}
它的字节码是:
iload_1 [check]
ifeq 6 // Jumping forward
..
跳转到后面:
label: do {
// do stuff
if (check) continue label;
// do more stuff
break label;
} while(true);
它的字节码是:
iload_1 [check]
ifeq 9
goto 2 // Jumping backward
..
- Java也有类型别名
在其它语言中(比如说Ceylon),定义类型别名非常简单:
interface People => Set<Person>;
这样所定义出来的People类型可以和Set交替使用:
People? p1 = null;
Set<Person>? p2 = p1;
People? p3 = p2;
Java中无法在最外层实现类型别名。不过我们可以在类或者方法的作用域内来实现这点。假设我们现在觉得Integer, Long的名字看着不爽了,希望能更简短些:I以及L。这很简单:
class Test<I extends Integer> {
<L extends Long> void x(I i, L l) {
System.out.println(
i.intValue() + ", " +
l.longValue()
);
}
}
上面这段程序中,在Test类的作用域内,Integer的别名是I,而在x方法内,Long的是L。我们可以这样来调用上面的方法:
new Test().x(1, 2L);
这当然不是什么正式的别名的方式。在本例中,Integer和Long都是final类型,也就是说类型I和L就是真正意义上的别名(算是吧。因为它只有一种赋值相容(assignment-compatibility)的方式)。如果你使用的是非final类型时(比如说Object),那你用的其实就是泛型而已了。
这种雕虫小技就先说到这吧。现在我们来讲点有意义的!
- 某些类型关系是无法确定的
好吧,这个就有点费脑了,先喝杯咖啡集中下精神吧。假设有下面两种类型:
// A helper type. You could also just use List
interface Type<T> {}
class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}
那么,C类型和D类型到底是什么?
这看起来是有点像递归了,java.lang.Enum看起来也是类似的递归(尽管略有不同)。看下它的定义:
public abstract class Enum<E extends Enum<E>> { ... }
知道了上面这个规范之后,我们就明白了,枚举的实现其实不过是个语法糖罢了:
// This
enum MyEnum {}
// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }
记住这点,我们再回来看一下这两个类型。下面这段代码能通过编译吗?
class Test {
Type<? super C> c = new C();
Type<? super D<Byte>> d = new D<Byte>();
}
这个问题有点难,不过Ross Tate曾经回答过它。这个问题其实是无解的:
C是Type<? super C>的子类吗?
Step 0) C Step 1) Type> 还有
D是Type<? super D>的子类型吗?
Step 0) D > Step 1) Type>>> > Step 2) D >> Step 3) List>> > Step 4) D> >> Step . . . (expand forever)
试试在你的Eclispe里编译一下,这会让它崩溃的!(别担心,这个BUG我已经提交了)
要想彻底搞清楚这点。。。
Java中的某些类型是不确定的 如果你对Java的这个罕见的奇怪行为感兴趣,可以读下Ross Tate的这篇论文“如何驾驭Java类型系统中的通配符”(与Alan Leung及Sorin Lerner共同发表),或者看下我们的几点愚见“论子类型多态与泛型多态的关联关系”。
- 类型交集
Java有一个非常古怪的特性叫做类型交集(虽然乍看上去有点像并集)。你可以声明一个泛型,它其实是两个类型的交集。比如说:
class Test<T extends Serializable & Cloneable> {
}
绑定到Test类中的这个泛型参数T必须同时实现Serializable接口以及Cloneable接口。比方说,String就没有同时实现这两个接口,但Date是:
// Doesn't compile
Test<String> s = null;
// Compiles
Test<Date> d = null;
Java 8中也用到了这一特性,你可以将某个类型转换成两个类型的交集。这有什么用?好像是没啥用,但是如果你要把一个lambda表达式强转成这样一个类型的话,除此之外就别无它法了。假设你的方法里面有这么一个看似疯狂的约束条件:
<T extends Runnable & Serializable> void execute(T t) {}
你希望Runnable对象同时还实现了Serializable接口,这样你既可以去执行它,也可以将它发送出去。Lambda表达式和序列化?这看上去有点基情。
如果Lambda表达式的目标类型和参数都是可序列化的,那么它也是可序列化的。
尽管这是可以实现的,但它并没有直接实现Serializable这个接口。为了能适配成这个类型,你必须得进行类型转换。但是如果你只转成Serializable了的话:
execute((Serializable) (() -> {}));
那么这个Lambda表达式就不是一个Runnable类型了。
那么,
就只能转成两个类型了:
execute((Runnable & Serializable) (() -> {}));