Files
translations/10-things-you-didnt-know-about-java/README.md
2014-11-21 11:06:27 +08:00

549 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
原文链接: [10 Things You Didnt Know About Java](http://blog.jooq.org/2014/11/03/10-things-you-didnt-know-about-java/)
译文发在[ImportNew](http://www.importnew.com/) [http://blog.jobbole.com/76550/](http://www.importnew.com/13859.html)2014-12-21
关于`Java`你可能不知道的10件事
===============================================
![java-mystery](java-mystery.jpg)
呃,你是不是写`Java`已经有些年头了?还依稀记得这些吧:
那些年,它还叫做`Oak`;那些年,`OO`还是个热门话题;那些年,`C++`同学们觉得`Java`是没有出路的;那些年,`Applet`还风头正劲……
但我打赌下面的这些事中至少有一半你还不知道。这周我们来聊聊这些会让你有些惊讶的`Java`内部事儿吧。
1. 其实没有受检异常(`checked exception`
---------------------------------------
是的!`JVM`才不知道这类事情,只有`Java`语言才会知道。
今天,大家都赞同,受检异常是个设计失误,一个`Java`语言中的设计失误。正如 *Bruce Eckel* [在布拉格的`GeeCON`会议上演示的总结](http://www.geecon.cz/speakers/?id=2)中说的,
`Java`之后的其它语言都不会再有受检异常,甚至`Java` 8的新式流`API``Streams API`)都不再拥抱受检异常
[以`lambda`的方式使用`IO`和`JDBC`,这个`API`用起来还是有些痛苦的](http://blog.jooq.org/2014/05/23/java-8-friday-better-exceptions/)。)
想证明`JVM`不感知受检异常?试试下面的这段代码:
```java
public class Test {
// 方法没有声明throws
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`](http://projectlombok.org/features/SneakyThrows.html)。
更多细节,可以在看看[这篇文章](http://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/),或`Stack Overflow`上的[这个问题](http://stackoverflow.com/q/12580598/521799)。
2. 可以有只是返回类型不同的重载方法
---------------------------------------
下面的代码不能编译,是吧?
```java
class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
```
是的!`Java`语言不允许一个类里有2个方法是『***重载一致***』的而不会关心这2个方法的`throws`子句或返回类型实际是不同的。
但是等一下!来看看[`Class.getMethod(String, Class...)`](http://docs.oracle.com/javase/8/docs/api/java/lang/Class.html#getMethod-java.lang.String-java.lang.Class...-)方法的`Javadoc`
> 注意,可能在一个类中会有多个匹配的方法,因为尽管`Java`语言禁止在一个类中多个方法签名相同只是返回类型不同,但是`JVM`并不禁止。
这让`JVM`可以更灵活地去实现各种语言特性。比如,可以用桥方法来实现方法的协变返回类型;桥方法和被重载的方法可以有相同的方法签名,但返回类型不同。
嗯,这个说的通。实际上,当写了下面的代码时,就发生了这样的情况:
```java
abstract class Parent<T> {
abstract T x();
}
class Child extends Parent<String> {
@Override
String x() { return "abc"; }
}
```
查看一下`Child`类所生成的字节码:
```java
// Method descriptor #15 ()Ljava/lang/String;
// Stack: 1, Locals: 1
java.lang.String x();
0 ldc <String "abc"> [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. 所有这些写法都是二维数组!
---------------------------------------
```java
class Test {
int[][] a() { return new int[0][]; }
int[] b() [] { return new int[0][]; }
int c() [][] { return new int[0][]; }
}
```
是的,这是真的。尽管你的人肉解析器不能马上理解上面这些方法的返回类型,但都是一样的!下面的代码也类似:
```java
class Test {
int[][] a = {{}};
int[] b[] = {{}};
int c[][] = {{}};
}
```
是不是觉得这个很2B想象一下在上面的代码中使用[`JSR-308`/`Java` 8的类型注解](https://jcp.org/en/jsr/detail?id=308)。
语法糖的数目要爆炸了吧!
```java
@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周休假前的最后一个提交里我写了这样的代码然后。。。
![for-you-my-dear-coworkers](for-you-my-dear-coworkers.jpg)
【***译注***:然后,亲爱的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】
找出上面用法的合适的使用场景,还是留给你作为一个练习吧。
4. 你没有掌握条件表达式
---------------------------------------
你认为自己知道什么时候该使用条件表达式面对现实吧你还不知道。大部分人会下面的2个代码段是等价的
```java
Object o1 = true ? new Integer(1) : new Double(2.0);
```
等同于:
```java
Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
```
让你失望了。来做个简单的测试吧:
```java
System.out.println(o1);
System.out.println(o2);
```
打印结果是:
```java
1.0
1
```
哦!如果『需要』,条件运算符会做数值类型的类型提升,这个『需要』有非常非常非常强的引号。因为,你觉得下面的程序会抛出`NullPointerException`吗?
```java
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);
```
关于这一条的更多的信息可以在[这里](http://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/)找到。
5. 你没有掌握复合赋值运算符
---------------------------------------
是不是觉得不服来看看下面的2行代码
```java
i += j;
i = i + j;
```
直觉上认为2行代码是等价的对吧但结果即不是`JLS``Java`语言规范)指出:
> 复合赋值运算符表达式 `E1 op= E2` 等价于 `E1 = (T)((E1) op (E2))`
> 其中`T`是`E1`的类型,但`E1`只会被求值一次。
这个做法太漂亮了,请允许我引用[*Peter Lawrey*](https://twitter.com/PeterLawrey)在`Stack Overflow`上的[回答](http://stackoverflow.com/a/8710747/521799)
使用`*=``/=`作为例子可以方便说明其中的转型问题:
```java
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. 随机`Integer`
---------------------------------------
这条其实是一个迷题,先不要看解答。看看你能不能自己找出解法。运行下面的代码:
```java
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
```
…… 然后要得到类似下面的输出(每次输出是随机结果):
```java
92
221
45
48
236
183
39
193
33
84
```
这怎么可能?!
.
.
.
.
.
.
. 我要剧透了…… 解答走起……
.
.
.
.
.
.
好吧,解答在这里(<http://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/>)
和用反射覆盖`JDK``Integer`缓存,然后使用自动打包解包(`auto-boxing`/`auto-unboxing`)有关。
同学们请勿模仿!或换句话说,想想会有这样的状况,再说一次:
> 在我4周休假前的最后一个提交里我写了这样的代码然后。。。
![for-you-my-dear-coworkers](for-you-my-dear-coworkers.jpg)
【***译注***:然后,亲爱的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】
7. GOTO
---------------------------------------
这条是我的最爱。`Java`是有GOTO的打上这行代码
```java
int goto = 1;
```
结果是:
```java
Test.java:44: error: <identifier> expected
int goto = 1;
^
```
这是因为`goto`是个[还未使用的关键字](http://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html),保留了为以后可以用……
但这不是我要说的让你兴奋的内容。让你兴奋的是,你是可以用`break``continue`和有标签的代码块来实现`goto`的:
向前跳:
```java
label: {
// do stuff
if (check) break label;
// do more stuff
}
```
对应的字节码是:
```java
2 iload_1 [check]
3 ifeq 6 // 向前跳
6 ..
```
向后跳:
```java
label: do {
// do stuff
if (check) continue label;
// do more stuff
break label;
} while(true);
```
对应的字节码是:
```java
2 iload_1 [check]
3 ifeq 9
6 goto 2 // 向后跳
9 ..
```
8. `Java`是有类型别名的
---------------------------------------
在别的语言中(比如,[`Ceylon`](http://blog.jooq.org/2013/12/03/top-10-ceylon-language-features-i-wish-we-had-in-java/)
可以方便地定义类型别名:
```java
interface People => Set<Person>;
```
这样定义的`People`可以和`Set<Person>`互换地使用:
```java
People? p1 = null;
Set<Person>? p2 = p1;
People? p3 = p2;
```
`Java`中不能在顶级(`top level`)定义类型别名。但可以在类级别、或方法级别定义。
如果对`Integer``Long`这样名字不满意,想更短的名字:`I``L`。很简单:
```java
class Test<I extends Integer> {
<L extends Long> void x(I i, L l) {
System.out.println(
i.intValue() + ", " +
l.longValue()
);
}
}
```
上面的代码中,在`Test`类级别中`I``Integer`的『别名』,在`x`方法级别,`L``Long`的『别名』。可以这样来调用这个方法:
```java
new Test().x(1, 2L);
```
当然这个用法不严谨。在例子中,`Integer``Long`都是`final`类型,结果`I``L` *效果上*是个别名
(大部分情况下是。赋值兼容性只是单向的)。如果用非`final`类型(比如,`Object`),还是要使用原来的泛型参数类型。
玩够了这些恶心的小把戏。现在要上干货了!
9. 有些类型的关系是不确定的
---------------------------------------
这条会很稀奇古怪你先来杯咖啡再集中精神来看。看看下面的2个类型
```java
// 一个辅助类。也可以直接使用List
interface Type<T> {}
class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}
```
类型`C``D`是啥意思呢?
这2个类型声明中包含了递归和[`java.lang.Enum`](http://docs.oracle.com/javase/8/docs/api/java/lang/Enum.html)的声明类似
(但有微妙的不同):
```java
public abstract class Enum<E extends Enum<E>> { ... }
```
有了上面的类型声明,一个实际的`enum`实现只是语法糖:
```java
// 这样的声明
enum MyEnum {}
// 实际只是下面写法的语法糖:
class MyEnum extends Enum<MyEnum> { ... }
```
记住上面的这点后回到我们的2个类型声明上。下面的代码可以编译通过吗
```java
class Test {
Type<? super C> c = new C();
Type<? super D<Byte>> d = new D<Byte>();
}
```
很难的问题,[`Ross Tate `](http://www.cs.cornell.edu/~ross/)回答过这个问题。答案实际上是不确定的:
***`C``Type<? super C>`的子类吗?***
```java
步骤 0) C <?: Type<? super C>
步骤 1) Type<Type<? super C>> <?: Type 继承
步骤 2) C 检查通配符 ? super C
步骤 . . . 进入死循环
```
然后:
***`D``Type<? super D<Byte>>`的子类吗?***
```java
步骤 0) D<Byte> <?: Type<? super C<Byte>>
步骤 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>>
步骤 2) D<Byte> <?: Type<? super D<D<Byte>>>
步骤 3) List<List<? super C<C>>> <?: List<? super C<C>>
步骤 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>
步骤 . . . 进入永远的展开中
```
试着在你的`Eclipse`中编译上面的代码会Crash别担心我已经提交了一个Bug。
我们继续深挖下去……
> 在`Java`中有些类型的关系是不确定的!
如果你有兴趣知道更多古怪`Java`行为的细节,可以读一下*Ross Tate*的论文[『驯服`Java`类型系统的通配符』](http://www.cs.cornell.edu/~ross/publications/tamewild/tamewild-tate-pldi11.pdf)
(论文和*Alan Leung*和*Sorin Lerner*合著),或者也可以看看我们在[子类型多态和泛型多态的关联](http://blog.jooq.org/2013/06/28/the-dangers-of-correlating-subtype-polymorphism-with-generic-polymorphism/)方面的思索。
10. 类型交集(`Type intersections`
---------------------------------------
`Java`有个很古怪的特性叫类型交集。你可以声明一个泛型类型这个类型是2个类型的交集。比如
```java
class Test<T extends Serializable & Cloneable> {
}
```
绑定到类`Test`的实例上的泛型类型参数`T`必须同时实现`Serializable``Cloneable`。比如,`String`不能做绑定,但`Date`可以:
```java
// 编译不通过!
Test<String> s = null;
// 编译通过
Test<Date> d = null;
```
`Java` 8保留了这个特性你可以转型成临时的类型交集。这有什么用
几乎没有一点用,但如果你想强转一个`lambda`表达式成这样的一个类型,就没有其它的方法了。
假定你在方法上有了这个蛋疼的类型限制:
```java
<T extends Runnable & Serializable> void execute(T t) {}
```
你想一个`Runnable`同时也是个`Serializable`,这样你可能在另外的地方执行它并通过网络发送它。`lambda`和序列化都有点古怪。
`lambda`是可以序列化的:
> 如果`lambda`表达式的目标类型和它捕获的参数(`captured arguments`)是可以序列化的,则这个`lambda`表达式是可序列化的。
但即使满足这个条件,`lambda`表达式并没有自动实现`Serializable`这个标记接口(`marker interface`)。
为了强制成为这个类型,就必须使用转型。但如果只转型成`Serializable` ...
```java
execute((Serializable) (() -> {}));
```
... 则这个`lambda`表达式不再是一个`Runnable`
呃……
So……
同时转型成2个类型
```java
execute((Runnable & Serializable) (() -> {}));
```
结论
---------------------------------------
一般我只对`SQL`会说这样的话,但是时候用下面的话来结束这篇文章了:
> `Java`中包含的诡异在程度上仅仅被它解决问题的能力超过。