关于Java你可能不知道的10件事
source link: http://www.androidchina.net/1268.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
呃,你是不是写Java
已经有些年头了?还依稀记得这些吧: 那些年,它还叫做Oak
;那些年,OO
还是个热门话题;那些年,C++
同学们觉得Java
是没有出路的;那些年,Applet
还风头正劲……
但我打赌下面的这些事中至少有一半你还不知道。这周我们来聊聊这些会让你有些惊讶的Java
内部的那些事儿吧。
1. 其实没有受检异常(checked exception
)
是的!JVM才不知道这类事情,只有Java语言才会知道。
今天,大家都赞同受检异常是个设计失误,一个Java
语言中的设计失误。正如 Bruce Eckel 在布拉格的GeeCON
会议上演示的总结中说的, Java之后的其它语言都没有再涉及受检异常了,甚至Java
8的新式流API
(Streams API
)都不再拥抱受检异常 (以lambda
的方式使用IO
和JDBC
,这个API
用起来还是有些痛苦的。)
想证明JVM
不理会受检异常?试试下面的这段代码:
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
。
更多细节,可以再看看这篇文章,或Stack Overflow
上的这个问题。
2. 可以有只是返回类型不同的重载方法
下面的代码不能编译,是吧?
class
Test {
Object x() {
return
"abc"
; }
String x() {
return
"123"
; }
}
是的!Java
语言不允许一个类里有2个方法是『重载一致』的,而不会关心这2个方法的throws
子句或返回类型实际是不同的。
但是等一下!来看看Class.getMethod(String, Class...)
方法的Javadoc
:
注意,可能在一个类中会有多个匹配的方法,因为尽管
Java
语言禁止在一个类中多个方法签名相同只是返回类型不同,但是JVM
并不禁止。 这让JVM
可以更灵活地去实现各种语言特性。比如,可以用桥方法(bridge method)来实现方法的协变返回类型;桥方法和被重载的方法可以有相同的方法签名,但返回类型不同。
嗯,这个说的通。实际上,当写了下面的代码时,就发生了这样的情况:
abstract
class
Parent<T> {
abstract
T x();
}
class
Child
extends
Parent<String> {
@Override
String x() {
return
"abc"
; }
}
查看一下Child
类所生成的字节码:
// 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
允许这个特性,可以愉快解决这个问题(实际上可以允许协变重载的方法包含有副作用的逻辑)。 聪明不?呵呵~
你是不是想要扎入语言规范和内核看看?可以在这里找到更多有意思的细节。
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[][] = {{}};
}
是不是觉得这个很2B?想象一下在上面的代码中使用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. 你没有掌握条件表达式
呃,你认为自己知道什么时候该使用条件表达式?面对现实吧,你还不知道。大部分人会下面的2个代码段是等价的:
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
哦!如果『需要』,条件运算符会做数值类型的类型提升,这个『需要』有非常非常非常强的引号。因为,你觉得下面的程序会抛出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. 你没有掌握复合赋值运算符
是不是觉得不服?来看看下面的2行代码:
i += j;
i = i + j;
直觉上认为,2行代码是等价的,对吧?但结果即不是!JLS
(Java
语言规范)指出:
复合赋值运算符表达式
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. 随机Integer
这条其实是一个迷题,先不要看解答。看看你能不能自己找出解法。运行下面的代码:
for
(
int
i =
0
; i <
10
; i++) {
System.out.println((Integer) i);
}
…… 然后要得到类似下面的输出(每次输出是随机结果):
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的!打上这行代码:
int
goto
=
1
;
Test.java:
44
: error: <identifier> 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
// 向前跳
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
// 向后跳
9
..
8. Java
是有类型别名的
在别的语言中(比如,Ceylon
), 可以方便地定义类型别名:
interface
People => Set<Person>;
这样定义的People
可以和Set<Person>
互换地使用:
People? p1 =
null
;
Set<Person>? p2 = p1;
People? p3 = p2;
在Java
中不能在顶级(top level
)定义类型别名。但可以在类级别、或方法级别定义。 如果对Integer
、Long
这样名字不满意,想更短的名字:I
和L
。很简单:
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
的『别名』。可以这样来调用这个方法:
new
Test().x(
1
, 2L);
当然这个用法不严谨。在例子中,Integer
、Long
都是final
类型,结果I
和L
效果上是个别名 (大部分情况下是。赋值兼容性只是单向的)。如果用非final
类型(比如,Object
),还是要使用原来的泛型参数类型。
玩够了这些恶心的小把戏。现在要上干货了!
9. 有些类型的关系是不确定的
好,这条会很稀奇古怪,你先来杯咖啡,再集中精神来看。看看下面的2个类型:
// 一个辅助类。也可以直接使用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
的声明类似 (但有微妙的不同):
public
abstract
class
Enum<E
extends
Enum<E>> { ... }
有了上面的类型声明,一个实际的enum
实现只是语法糖:
// 这样的声明
enum
MyEnum {}
// 实际只是下面写法的语法糖:
class
MyEnum
extends
Enum<MyEnum> { ... }
记住上面的这点后,回到我们的2个类型声明上。下面的代码可以编译通过吗?
class
Test {
Type<?
super
C> c =
new
C();
Type<?
super
D<Byte>> d =
new
D<Byte>();
}
很难的问题,Ross Tate
回答过这个问题。答案实际上是不确定的:
C是Type<? super C>的子类吗?
步骤 0) C <?: Type<? super C>
步骤 1) Type<Type<? super C>> <?: Type (继承)
步骤 2) C (检查通配符 ? super C)
步骤 . . . (进入死循环)
D是Type<? super D<Byte>>的子类吗?
步骤 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
类型系统的通配符』 (由Ross Tate、Alan Leung和Sorin Lerner合著),或者也可以看看我们在子类型多态和泛型多态的关联方面的思索。
10. 类型交集(Type intersections
)
Java
有个很古怪的特性叫类型交集。你可以声明一个(泛型)类型,这个类型是2个类型的交集。比如:
class
Test<T
extends
Serializable & Cloneable> {
}
绑定到类Test
的实例上的泛型类型参数T
必须同时实现Serializable
和Cloneable
。比如,String
不能做绑定,但Date
可以:
// 编译不通过!
Test<String> s =
null
;
// 编译通过
Test<Date> d =
null
;
Java
8保留了这个特性,你可以转型成临时的类型交集。这有什么用? 几乎没有一点用,但如果你想强转一个lambda
表达式成这样的一个类型,就没有其它的方法了。 假定你在方法上有了这个蛋疼的类型限制:
<T
extends
Runnable & Serializable>
void
execute(T t) {}
你想一个Runnable
同时也是个Serializable
,这样你可能在另外的地方执行它并通过网络发送它。lambda
和序列化都有点古怪。
lambda
是可以序列化的:
如果
lambda
表达式的目标类型和它捕获的参数(captured arguments
)是可以序列化的,则这个lambda
表达式是可序列化的。
但即使满足这个条件,lambda
表达式并没有自动实现Serializable
这个标记接口(marker interface
)。 为了强制成为这个类型,就必须使用转型。但如果只转型成Serializable
…
execute((Serializable) (() -> {}));
… 则这个lambda
表达式不再是一个Runnable
。
同时转型成2个类型:
execute((Runnable & Serializable) (() -> {}));
一般我只对SQL
会说这样的话,但是时候用下面的话来结束这篇文章了:
Java
中包含的诡异在程度上仅仅被它解决问题的能力超过。
转载请注明:Android开发中文站 » 关于Java你可能不知道的10件事
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK