作为 Java 书呆子,比起实用技能,我们会对介绍 Java 和 JVM 的概念细节更感兴趣。

你是从很早开始就一直使用 Java 吗?那你还记得它的过去吗?那时,Java 还叫 Oak,OO 还是一个热门话题,C++ 的 folk 者认为 Java 是不可能火起来,Java 开发的小应用程序 Applets 还受到关注

我敢打赌,下面我要介绍的这些事,有一半你都不知道。下面让我们来深入探索 Java 的神秘之处。

1. 没有检查异常这种事情

没错!JVM 不会知道这些事情,只有 Java 语句知道。

如今大家都认为检查异常是个错误。正如 Bruce 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 就能办到。

这篇文章可以看到更详细的相关内容,或者在 Stack Overflow 上看。

2. 你可以定义仅在返回值有差异的重载函数

这样的代码无法编译,对不?

class Test { Object x() { return "abc"; } String x() { return "123"; }}

对。 Java 语言不允许两个方法在同一个类中“等效重载”,而忽略其诸如throws自居或返回类型等的潜在的差异。

查看 Class.getMethod(String, Class…) 的 Javadoc。 其中说明如下:

请注意,类中可能有多个匹配方法,因为 Java 语言禁止在一个类声明具有相同签名但返回类型不同的多个方法,但 Java 虚拟机并不是如此。虚拟机中增加的灵活性可以用于实现各种语言特征。例如,可以用桥接方法实现协变返回; 桥接方法和被重写的方法将具有相同的签名但拥有不同的返回类型。

哇哦,有道理。实际上下面的代码暗藏着很多事情:

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: 1java.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: 1bridge 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 来允许这个特性所带来的痛苦会更小(副作用是允许协变凌驾于一切之上) 很聪明,不是吗?

你看过语言内部的细节吗?不妨看看,在这里会发现更多很有意思的东西。

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 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 [] = {{}};}

类型注解。看起来很神秘,其实并不难理解。

或者换句话说:

当我做最近一次提交的时候是在我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.01

由此可见,三目条件运算符会在有需要的情况下,对操作数进行类型提升。注意,是只在有需要时才进行;否则,代码可能会抛出 NullPointerException 空引用异常:

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);

5. 你还没搞懂复合赋值运算符

很奇怪吗?来看看下面这两行代码:

i += j;i = i + j;

直观看来它们等价,是吗?但可其实它们并不等价!JLS 解释如下:

E1 op= E2 形式的复合赋值表达式等价于 E1 = (T)((E1) op (E2)),这里 T 是 E1 的类型,E1 只计算一次。

非常好,我想引用 Peter Lawrey 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'

现在看到它的作用了吗?我会在应用程序中对字符串进行乘法计算。因为,你懂的…

6. 随机整数

现在有一个更难的谜题。不要去看答案,看看你能不能自己找到答案。如果运行下面的程序:

for (int i = 0; i < 10; i++) { System.out.println((Integer) i);}

… “有时候”,我会得到下面的输出:

922214548236183391933384

这怎么可能??

. spoiler… 继续解答…

好了,答案在这里 (https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/),这必须通过反射重写 JDK 的 Integer 缓存,然后使用自动装箱和拆箱。不要在家干这种事情!或者,我们应该换种方式进行此类操作。


7. GOTO

这是我的最爱之一。Java也有GOTO!输入下试试……

int goto = 1;

将输出:

Test.java:44: error:  expectedint goto = 1;^

这是因为goto是一个未使用的关键字, 仅仅是为了以防万一……

但这不是最令人兴奋的部分。令人兴奋的部分是你可以使用 break、continue 和标记块来实现 goto 功能:

向前跳:

label: { // do stuff if (check) break label; // do more stuff}

在字节码中格式如下:

2 iload_1 [check]3 ifeq 6 // Jumping forward6 ..

向后跳:

label: do { // do stuff if (check) continue label; // do more stuff break label;} while(true);

在字节码中格式如下:

2 iload_1 [check]3 ifeq 96 goto 2 // Jumping backward9 ..

8. Java 有类型别名

其它语言 (比如 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() +