mirror of
https://github.com/oldratlee/translations.git
synced 2026-04-27 03:52:14 +08:00
549 lines
16 KiB
Markdown
549 lines
16 KiB
Markdown
原文链接: [10 Things You Didn’t 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`已经有些年头了?还依稀记得这些吧:
|
||
那些年,它还叫做`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周休假前的最后一个提交里,我写了这样的代码,然后。。。
|
||

|
||
【***译注***:然后,亲爱的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】
|
||
|
||
找出上面用法的合适的使用场景,还是留给你作为一个练习吧。
|
||
|
||
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周休假前的最后一个提交里,我写了这样的代码,然后。。。
|
||

|
||
【***译注***:然后,亲爱的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】
|
||
|
||
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`中包含的诡异在程度上仅仅被它解决问题的能力超过。
|