关于 Java 你不知道的十件事

作为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 语言的诡异性只是被它解决问题的能力掩盖了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容