作为Java 控,我们总是对不太可能直接使用,但能使我们更了解 Java 和 Java 虚拟机(Java Virtual Machine,JVM) 的晦涩细节感兴趣。这也是我将 Lukas Eder 在 jooq.org 上写的这篇文章发布出来的原因。
你在Java发布的时候就开始使用了吗?还记得那时它叫“Oak”,面向对象也 (Object Oriented, OO )还是个热门话题,C++ 程序员们觉得 Java 完全没机会成功,Applet的出现也是一件新鲜大事?
我打赌下文中至少一半的内容你都不知道。让我们来看看这些令人惊喜的 Java 细节吧。
1. 受检异常(checked exception)这件事是不存在的
是这样的,JVM 完全不知道这件事,都是Java语言做的[只有Java语言这么干]。
现在,异常检查被公认为是个错误,正如 Brue Eckel 在布拉格的 GeeCON 大会上的闭幕词中所说, Java 后的其他语言都不再使用异常检查了,就连 Java 8 都不愿在新的 Stream 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. doThrow0(e);
}
@SuppressWarnings("unchecked")
static void doThrow0(Exception e) throws E {
throw (E) e;
}
}
这个不仅会编译,还会抛出 SQLException ,你甚至不需要 Lombok 的 @SneakyThrows 标签。
更多详情请参考这篇文章,
https://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/
或者 Stack Overflow 上的这篇文章。
http://stackoverflow.com/q/12580598/521799
2. 可以使用不同的返回值类型来重载方法
以下代码是编译不过的,对吧?
class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
是的,Java 不允许在一个类中通过不同的返回值类型和异常语句来重载方法。
不过稍等,Java 文档中关于 Class.getMethod(String, Class…) 这样写道:
请注意,在一个类中会有多个匹配的方法,因为虽然 Java 语法规则禁止一个类中存在多个方法函数签名相同仅仅返回类型不同,但 JVM 允许。这样提高了 JVM 的灵活性以实现各种语言特性。例如,可以用桥接方法(bridge method)来实现方法的协变返回类型,桥接方法和被重载的方法可以有相同的函数签名和不同的返回值类型。
喔,这是合理的。事实上,以下代码就是这样执行的,
abstract class Parent {
abstract T x();
}
class Child extends Parent {
@Override
String x() { return "abc";}
}
Child 类编译后的字节码是这样的:
// Method descriptor #15 ()Ljava/lang/String;
// Stack: 1, Locals: 1
java.lang.String x();
0 ldc [16]
2 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();
0 aload_0 [this]
1 invokevirtual Child.x() : java.lang.String [19]
4 areturn
Line numbers:
[pc: 0, line: 1]
看,T 在字节码中就是 Object,这个很好理解。
合成桥接方法是编译器自动生成的,因为 Parent.x() 签名的返回值类型被认为是 Object。如果没有这样的桥接方法是无法在兼容二进制的前提下支持泛型的。因此,修改 JVM 是实现这个特性最简单的方法了(同时实现了协变式覆盖)。很聪明吧。
你明白语言的内部特性了吗?这里有更多细节。
http://stackoverflow.com/q/442026/521799
3. 这些都是二维数组
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 Crazyy {}
class Test {
@Crazyy int[][] a1 = {{}};
int @Crazyy [][] a2 = {{}};
int[] @Crazyy [] a3 = {{}};
@Crazyy int[] b1[] = {{}};
int @Crazyy [] b2[] = {{}};
int[] b3 @Crazyy [] = {{}};
@Crazyy int c1[][] = {{}};
int c2 @Crazyy [][] = {{}};
int c3[] @Crazyy [] = {{}};
}
类型注解,它的诡异性只是被他强大的功能掩盖了。
换句话说:
当我在4周假期之前的最后一次代码提交中这么做的话
为以上所有内容找到相应的实际用例的任务就交给你啦。
4. 你不懂条件表达式
你以为你已经很了解条件表达式了吗?我告诉你,不是的。大多数人会认为以下的两个代码片段是等效的:
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);
程序的输出是:
1.0
1
是的,在确有必要的情况下,条件表达式会升级数字类型。你希望这个程序抛出一个空指针异常吗?
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);
更多细节请看这里。
https://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/
5. 你也不懂复合赋值运算符
很诡异吗?让我们来看以下两段代码:
i += j;
i = i + j;
直觉上,他们是等价的吧?事实上不是,Java 语言规范(Java Language Standard,JLS)中这样写道:
符合赋值表达式 E1 op= E2 与 E1 = (T)((E1) op (E2)) 是等价的,这里 T 是 E1 的类型,期望 E1 只被求值一次。
很美吧,我想引用 Peter Lawrey 在Stack Overflow 上回复,
http://stackoverflow.com/a/8710747/521799
这种类型转换很好的一个例子是使用 *= or /=
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'
这个很有用吧?我会将它们应用到我的程序里。原因你懂的。
6. 随机数
这更像是一道题,先别看结果。看你自己能否找到答案。当我运行以下程序时,
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
有时,我会得到以下输出:
92
221
45
48
236
183
39
193
33
84
这是怎么回事?
答案已经在前面剧透了……
答案在这里,需要通过反射来重载 JDK 中的 Integer 缓存,然后使用自动装箱(auto-boxing)和自动拆箱(auto-unboxing)。千万不要这么做,我们假设如果再做一次。
https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/
我在4周假期之前的最后一次代码提交中这么做了。
7. GOTO
这是我喜欢的一个。Java 有 GOTO 语句!输入以下:
int goto = 1;
结果将会是:
Test.java:44: error: expected
int goto = 1;
这是因为 goto 是一个保留的关键字,以防万一……
但这不是最激动人心的部分。最给力的是你可以通过 break、continue 以及标签代码块来实现 goto。
向前跳转
label: {
// do stuff
if (check) break label;
// do more stuff
}
字节码:
2 iload_1 [check]
3 ifeq 6 // Jumping forward
6 ..
向后跳转
label: do {
// do stuff
if (check) continue label;
// do more stuff
break label;
} while(true);
字节码:
2 iload_1 [check]
3 ifeq 9
6 goto 2 // Jumping backward
9 ..
8. Java 支持类型别名(type aliases)
在其它语言中(例如:Ceylon),定义类型别名是很容易的。
interface People => Set;
People 类型通过这个方法就可以与 Set 互换使用了:
People? p1 = null;
Set? p2 = p1;
People? p3 = p2;
在 Java 中,顶层代码里是不能定义类型别名的,但是我们可以在类和方法的作用域内这么做。假设我们不喜欢 Integer,[、]Long 这些名字,想要短一点的如 I 和 L,这是小菜一碟:
class Test {
void x(I i, L l) {
System.out.println(
i.intValue() + ", " +
l.longValue()
);
}
}
以上代码中,Integer 在 Test 类中用别名 I 替换, Long 在 x() 方法中用别名 L 替换。我们可以这样调用以上方法:
new Test().x(1, 2L);
这个技术别太当真。在上边的例子里,Integer 和 Long 都是 final 类型, 也就是说 I 和 L 效果上是类型别名(大多数情况下,赋值兼容是单向的)。如果我们用非 final 的类型(例如 Object),就需要使用原来的泛型了。
以上是一些雕虫小技,下面才是真正有用的!
9. 一些类型之间的关系是不确定的!
这个会很有趣的,所以来一杯咖啡然后集中注意力。假设以下两种类型:
// A helper type. You could also just use List
interface Type {}
class C implements Type> {}
class D
implements Type>>> {}
类型 C 和 D 到底是什么意思呢?
他们包含了递归,很像 java.lang.Enum ,但又稍有不同。考虑以下代码:
public abstract class Enum> { ... }
以上定义中, enum 的实现是一个纯粹的语法糖。
// This
enum MyEnum {}
// Is really just sugar for this
class MyEnum extends Enum { ... }
记住这个,让我们再回到刚才那两个类型。下边的代码可以通过编译吗?
class Test {
Type< ? super C> c = new C();
Type< ? super D> d = new D();
}
这是个很难的问题,Ross Tate 已经回答了。答案是不确定的:
C 是 的子类型吗?
Step 0) C
Step 1) Type>
Step 2) C (checking wildcard ? super C)
Step . . . (cycle forever)
然后
D 是 > 的子类型吗?
Step 0) D >
Step 1) Type>>> >
Step 2) D >>
Step 3) Type>> >
Step 4) D> >>
Step . . . (expand forever)
尝试在 Eclipse 中编译以上代码,Eclipse 会挂掉的!(不要担心,我已经提过 bug 了)
理解下这个…
Java 中的一些类型的关系是不确定的!
如果你想了解更多关于 Java 的这个特性,请阅读 Ross Tate 与 Alan Leung 和 Sorin Lerner 共同编著的论文 “Taming Wildcards in Java’s Type System”或者我们自己总结的correlating subtype polymorphism with generic polymorphism。
《 Taming Wildcards in Java’s Type System 》
http://www.cs.cornell.edu/~ross/publications/tamewild/tamewild-tate-pldi11.pdf
《 correlating subtype polymorphism with generic polymorphism 》
https://blog.jooq.org/2013/06/28/the-dangers-of-correlating-subtype-polymorphism-with-generic-polymorphism/
10. 类型交集(Type intersections)
Java 有个特性叫做类型交集。你可以声明一个泛型,这个泛型是两个类型的交集,例如:
class Test {
}
绑定到 Test 类的实例的泛型类型参数 T 需要同时实现 Serializable 和 Cloneable。例如,String 是不能绑定的,但 Date 可以:
// Doesn't compile
Test s = null;
// Compiles
Test d = null;
Java 8 中保留了这个功能,你可以将类型转换为临时的类型交集。这有用吗?几乎没用,但如果你想要将lambda表达式强制转换为这个类型,除此就别无他法了。我们假设你的方法有这个疯狂的类型限制:
void execute(T t) {}
你想要同时支持 Runnable 和 Serializable,是为了以防万一要在网络的另一处执行它。Lambda 和序列化都有些古怪:
Lambda 表达式可以被序列化:
如果一个 lambda 表达式的返回值和输入参数可以被序列化,则这个表达式是可以被序列化的。
但即使这是真的,它也不会自动继承 Serializable 接口。你需要转换才能成为那个类型。但如果你只是转换为 Serializable…
execute((Serializable) (() -> {}));
lambda 就不支持 Runnable 了。
所以,
把它转换为两个类型:
execute((Runnable & Serializable) (() -> {}));
结论
我经常只这么说 SQL,但现在要用下边的话来总结这篇文章了:
Java 语言的诡异性只是被它解决问题的能力掩盖了。