你不知道java的10件事
原文:10 Things You Didn't Know About Java
虽然我尝试了翻译这篇文章,但是有好多东西还是没搞明白,估计你看我翻译的也不会明白.今天(2014-12-22)在ImportNew上面看到了原文的另外一篇翻译文章,是的,翻译的非常好,希望对你有帮助.
这是链接http://www.importnew.com/13859.html
如果你看过那篇翻译,就不要看我的了,哈哈,我自己都不好意思说这是翻译了.
你从一开始就在使用java吗?你是否还记得java被称作为”Oak”的时期?那时,面向对象仍然是一个热门的话题,使用C++的人们都认为java没有任何机会, Applets 风头正劲.
我敢打赌你肯定不知道以下一半的事情.现在,让我们开始一些java内部运作的大惊喜.
1.并没有所谓的检查异常
没错,java虚拟机(JVM)不知道异常,只有java语言自己知道.
如今,每个人都同意检查异常是一个错误.正如Bruce Eckel 在Prague的 GeeCON闭幕词上所说,在java之后没有其他语言会约定使用检查异常,甚至 Java 8 新的流API都不再包含这些(在lambdas表达式中使用IO 或者JDBC时,是有点痛苦).
(译者注:java8 引入了lambads表达式,使用它能够使设计代码更简洁)
使用下面的代码可以证明JVM并不知道这些:
<span style="font-size:18px;"></pre><pre name="code" class="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;
}
}</span>
上面的代码不仅能通过编译,而且抛出了异常SQLException ,你甚至不需要使用Lombok’s 的注解@SneakyThrows .
(译者注:如果你在方法中没有声明throws子句,当程序出现异常时Lomok 注解@SneakyThrows 会偷偷的抛出检查异常)
点这里获取关于这篇文章的更多细节, 或 Stack Overflow.
2.重载仅返回类型不同的方法
这不能通过编译,不是吗?
<span style="font-size:18px;">class Test {
Object x() { return "abc"; }
String x() { return "123"; }
}
</span>
是的.Java语言不允许在同一个类中存在"等价覆盖" 的两个方法.不管它们有不同的 throws 子句或是不同的返回类型.
在 Javadoc Class.getMethod(String,Class...).上面有如下说明:
注意:它在一个类中可能会匹配到多个方法,虽然java语言禁止在一个类中声明多个签名相同而仅返回类型不同的方法,但是java虚拟机不会如此.在java虚拟机中,这种增强 的灵活性被用于实现多样的语言特性.例如,协变返回值类型能通过桥接方法实现;桥接方法和被覆盖的方法将有相同的签名,不同的返回类型.
这很有意义.事实上,下面的语句所发生的几乎就是这样.
<span style="font-size:18px;">abstract class Parent<T> {
abstract T x();
}
class Child extends Parent<String> {
@Override
String x() { return "abc"; }
}</span>
查看生成的字节码:
<span style="font-size:18px;"><span style="font-size:18px;">// 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]</span>
</span>
这个合成的桥接方法实际上是由编译器生成的,因为Parent.x()的返回类型签名在某些调用位置可能会被期望成 object.
加入的泛型没有这样的桥接方法就不可能以一种二进制的方式兼容.
因此,改变JVM去支持这种特性只需很少的代价(同样也允许协变性压倒一切负效应)很聪明,不是吗?
你分析过语言的细节和内幕吗?点这里发现更多有趣的细节.
3.这些都是二维数组
<span style="font-size:18px;">class Test {
int[][] a() { return new int[0][]; }
int[] b() [] { return new int[0][]; }
int c() [][] { return new int[0][]; }
}</span>
这是真的.你可能无法立刻理解上面方法的返回类型,但它们都是一样的.类似于下面的方法:
<span style="font-size:18px;">class Test {
int[][] a = {{}};
int[] b[] = {{}};
int c[][] = {{}};
}</span>
你肯定认为这疯了.想象一下,为上面的方法使用 JSR-308 / Java 8 的类型注解. 句法的数量将会激增.
<span style="font-size:18px;">@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 [] = {{}};
}
</span>
4.你不明白条件表达式
你认为你在使用条件表达式时明白一切吗?让我告诉你吧,你不明白.
<span style="font-size:18px;">Object o1 = true ? new Integer(1) : new Double(2.0);</span>
真的一样吗?
<span style="font-size:18px;">Object o2;
if (true)
o2 = new Integer(1);
else
o2 = new Double(2.0);
</span>
不一样.我们来验证一下:
<span style="font-size:18px;">System.out.println(o1);
System.out.println(o2);</span>
程序的输出结果为:
1.0
1
<span style="font-size:18px;">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);</span>
点这里获取更多细节.
5.你也不明白复合赋值运算符
<span style="font-size:18px;">i += j;
i = i + j;</span>
乍一看它们应该是等价的,但事实上不是.见 JSL(java 语言规范):
复合赋值表达式 E1 op= E2 与 E1 = (T)((E1) op (E2)) 是等价的,T的类型与E1相同,此外 E1仅计算一次
这真是太美了,我想引用Peter Lawrey's 关于堆栈溢出问题的回答:
<span style="font-size:18px;">A good example of this casting is using *= or /=
byte b = 10;
b *= 5.7;
System.out.println(b); // prints 57
or
byte b = 100;
b /= 2.5;
System.out.println(b); // prints 40
or
char ch = '0';
ch *= 1.1;
System.out.println(ch); // prints '4'
or
char ch = 'A';
ch *= 1.5;
System.out.println(ch); // prints 'a'</span>
6.随机整数
这是一个难题.不要参看解答,你能独立的解决问题吗?
运行下面的代码:
<span style="font-size:18px;">for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}</span>
我有时候会得到如下输出:
92
221
45
48
236
183
39
193
33
84
这怎么可能?
答案在这 .通过反射覆盖 JDK's 的 Integercache ,然后使用自动装箱和自动拆箱机制.
import java.lang.reflect.Field;
import java.util.Random;
public class Entropy {
public static void main(String[] args)
throws Exception {
// Extract the IntegerCache through reflection
Class<?> clazz = Class.forName(
"java.lang.Integer$IntegerCache");
Field field = clazz.getDeclaredField("cache");
field.setAccessible(true);
Integer[] cache = (Integer[]) field.get(clazz);
// Rewrite the Integer cache
for (int i = 0; i < cache.length; i++) {
cache[i] = new Integer(
new Random().nextInt(cache.length));
}
// Prove randomness
for (int i = 0; i < 10; i++) {
System.out.println((Integer) i);
}
}
}
运行上面的代码,你就可以得到类似的结果了.
7.GOTO
在java中编写下列语句:
<span style="font-size:18px;">int goto = 1;</span>
程序会编译失败,错误信息为:
<span style="font-size:18px;">Test.java:44: error: <identifier> expected
int goto = 1;
^</span>
因为goto是一个未使用的关键字.
虽然无法在源码中直接使用 goto 但是我们可以通过 break ,continue 和 标记块实现.
字节码的 goto ;
向前跳转 :
<span style="font-size:18px;">label: {
// do stuff
if (check) break label;
// do more stuff
}</span>
它的字节码为:
<span style="font-size:18px;">2 iload_1 [check]
3 ifeq 6 // Jumping forward
6 ..
</span>
向后跳转:
<span style="font-size:18px;">label: do {
// do stuff
if (check) continue label;
// do more stuff
break label;
} while(true);</span>
它的字节码为:
<span style="font-size:18px;">2 iload_1 [check]
3 ifeq 9
6 goto 2 // Jumping backward
9 ..</span>
看,是不是出现了 goto
8.java 的类型别名
在其他语言中可以很简单的使用类型别名 ,例如Ceylon
(译者注:Ceylon是一种新兴的计算机编程语言,号称"java杀手",它不是Java,而是一种受Java影响的新语言.)<span style="font-size:18px;">interface People => Set<Person> ;</span>
以这种方式构造的 People 可以被 Set<Person> 替换 :
<span style="font-size:18px;">People? p1 = null;
Set<Person>? p2 = p1;
People? p3 = p2;</span>
在 java中 ,我们无法在全局范围上定义类型别名. 由于存在 class 域 或 方法 域,
亦可以定义. 考虑两个我们很不喜欢的命名 Integer 和 Long ,为它们取个简短的名称 I 和 L :
<span style="font-size:18px;">class Test<I extends Integer> {
<L extends Long> void x(I i, L l) {
System.out.println(
i.intValue() + ", " +
l.longValue()
);
}
}
</span>
上面的程序中 , 在 TestClass 域内 定义 Integer 别名为 I ,在 x() 方法域中定义Long
别名为 L . 我们可以这样使用上面的方法 :
<span style="font-size:18px;">new Test().x(1, 2L);</span>
显然这种技术不值得重视. 在这个例子中 ,Integer 和 Long 都是 final 类型,也就意味着类型 I 和 L 是 有效的别名(那样的话,程序与类型兼容性也就无缘了).如果我们使 用的不是 final 类型,那么就应该使用泛型.
看够了这些无聊的把戏了吧! 来点厉害的.
9. 一些不可判定的关系类型
让我们来点咖啡 ,集中你的注意力,这可是很时髦的东西.
考虑下面两个类型:
<span style="font-size:18px;">// A helper type. You could also just use List
interface Type<T> {}
class C implements Type<Type<? super C>> {}
class D<P> implements Type<Type<? super D<D<P>>>> {}
</span>
现在,你知道 C 和 D 的类型吗 ?
它们包含了递归,java.lang.Enum 也是递归的.这两种方式有些相似,但略有不同.
<span style="font-size:18px;">public abstract class Enum<E extends Enum<E>> { ... }
</span>
由上面的规范可知,Enum 实际上是由一种糖衣语法实现的.
(译者注:糖衣语法:指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用)
<span style="font-size:18px;">// This
enum MyEnum {}
// Is really just sugar for this
class MyEnum extends Enum<MyEnum> { ... }</span>
考虑到这一点,让我们回到先前定义的两个类型.下列代码能编译成功吗?
<span style="font-size:18px;">class Test {
Type<? super C> c = new C();
Type<? super D<Byte>> d = new D<Byte>();
}
</span>
很难回答,Ross Tate 有个答案,不可判定:
<span style="font-size:18px;">Is C a subtype of Type<? super C>?
Step 0) C <?: Type<? super C>
Step 1) Type<Type<? super C>> <?: Type (inheritance)
Step 2) C (checking wildcard ? super C)
Step . . . (cycle forever)
And then:
Is D a subtype of Type<? super D<Byte>>?
Step 0) D<Byte> <?: Type<? super C<Byte>>
Step 1) Type<Type<? super D<D<Byte>>>> <?: Type<? super D<Byte>>
Step 2) D<Byte> <?: Type<? super D<D<Byte>>>
Step 3) List<List<? super C<C>>> <?: List<? super C<C>>
Step 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>
Step . . . (expand forever)</span>
尝试在你的Eclipse中编译上面的代码,崩溃了吧!
看这句话:
有些类型关系在java中是不可判定的.
如果你有兴趣了解这个奇怪的java特性更多细节,就读一读 Ross Tate的论文 "Taming Wildcards in Java's Type System"(与 Alan Leung 和 Sorin Lerner 合著),或者自己思考关联子类型多态性与泛型多态性.
10. 交集类型
java 有一个很独特的特性称作交集类型( type intersections). 你可以声明一个泛型 ,它由两个类型的交集构成.例如:
<span style="font-size:18px;">class Test<T extends Serializable & Cloneable> {
}</span>
要使用绑定的泛型参数 T 去实例化 Test 类, 这个参数就必须同时实现 Serializable 和 Cloneable.例如 String 不是 ,而 Date 是:
<span style="font-size:18px;">// Doesn't compile
Test<String> s = null;
// Compiles
Test<Date> d = null;</span>
为了让你有一个专门的交集类型,这种特性在java8中得到了重用.如何用它呢?几乎没用.但是,当你在 lambda表达式中强行应用这样的类型时,就只有此种方法可行.
假设你在方法中使用了这种疯狂的类型约束:
<span style="font-size:18px;"><T extends Runnable & Serializable> void execute(T t) {}</span>
你需要一个实现了 Runnable 和Serializable 的对象.为了让你可以在某些地方执行它,或是发送它,
Lambads 可以被序列化:
即使这是真的,那也不会自动的实现序列化标记接口.所以你必须自己动手.如果一个lambda 表达式的目标类型和所需参数是可序列化的,那么这个表达式就能序列化.
现在你有一个可序列化的,但是它不能被执行
<span style="font-size:18px;">execute((Serializable) (() -> {}));</span>
所以你必须自己加上:
<span style="font-size:18px;">execute((Runnable & Serializable) (() -> {}));</span>