关于Java你可能不知道的10件事

原文链接: 10 Things You Didn’t Know About Java,2014-11-03
译文发在ImportNewhttp://www.importnew.com/13859.html,2014-12-21

呃,你是不是写Java已经有些年头了?还依稀记得这些吧: 那些年,它还叫做Oak;那些年,OO还是个热门话题;那些年,C++同学们觉得Java是没有出路的;那些年,Applet还风头正劲……

但我打赌下面的这些事中至少有一半你还不知道。这周我们来聊聊这些会让你有些惊讶的Java内部的那些事儿吧。

1. 其实没有受检异常(checked exception

是的!JVM才不知道这事儿,只有Java语言才会知道。

今天,大家都赞同受检异常是个设计失误,一个Java语言中的设计失误。正如 Bruce Eckel 在布拉格的GeeCON会议上演示的总结中说的, Java之后的其它语言都没有再涉及受检异常了,甚至Java 8的新式流APIStreams API)都不再拥抱受检异常 (lambda的方式使用IOJDBC,这个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 [] = {{}};
}

类型注解。这个设计带来的诡异程度仅次于它带来的解决问题的能力。

或换句话说:

我要去休一个月的假了,在最后一个提交里撸上了这样的代码,然后。。。
for-you-my-dear-coworkers
译注:然后,亲爱的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】

请找出上面用法的合适的使用场景,这个还是留给你作为一个练习吧。

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);

打印结果是:

哦!如果『需要』,条件运算符会做数值类型的类型提升,这个『需要』有非常非常非常强的引号。因为…… 你觉得下面的程序会抛出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行代码:

直觉上认为,2行代码是等价的,对吧?但结果不是!JLSJava语言规范)指出:

复合赋值运算符表达式 E1 op= E2 等价于 E1 = (T)((E1) op (E2)) 其中TE1的类型,但E1只会被求值一次。

这个做法太漂亮了,请允许我引用Peter LawreyStack 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/), 和用反射覆盖JDKInteger缓存,然后使用自动打包解包(auto-boxing/auto-unboxing)有关。 同学们请勿模仿!或换句话说,想想会有这样的状况,再说一次:

我要去休一个月的假了,在最后一个提交里撸上了这样的代码,然后。。。
for-you-my-dear-coworkers
译注:然后,亲爱的同事你,就有得火救啦,哼,哼哼,哦哈哈哈哈~】

7. GOTO

这条是我的最爱。Java是有GOTO的!打上这行代码:

结果是:

Test.java:44: error: <identifier> expected
    int goto = 1;
        ^

这是因为goto是个还未使用的关键字,保留了为以后可以用……

但这不是我要说的让你兴奋的内容。让你兴奋的是,你是可以用breakcontinue和有标签的代码块来实现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)定义类型别名。但可以在类级别、或方法级别定义。 如果对IntegerLong这样名字不满意,想更短的名字:IL。很简单:

class Test<I extends Integer> {
    <L extends Long> void x(I i, L l) {
        System.out.println(
            i.intValue() + ", " + l.longValue()
        );
    }
}

上面的代码中,在Test类级别中IInteger的『别名』,在x方法级别,LLong的『别名』。可以这样来调用这个方法:

当然这个用法不严谨。在例子中,IntegerLong都是final类型,结果IL _效果上_是个别名 (大部分情况下是。赋值兼容性只是单向的)。如果用非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>>>> {}

类型CD是啥意思呢?

这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回答过。答案实际上是不确定的:

CType<? super C>的子类吗?

步骤 0) C <?: Type<? super C>
步骤 1) Type<Type<? super C>> <?: Type (继承)
步骤 2) C (检查通配符 ? super C)
步骤 . . . (进入死循环)

然后:

DType<? 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必须同时实现SerializableCloneable。比如,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

呃……

So……

同时转型成2个类型:

execute((Runnable & Serializable) (() -> {}));

结论

一般我只对SQL会说这样的话,但是时候用下面的话来结束这篇文章了:

Java中包含的诡异之多仅次于它解决问题的能力之大。


Page 2

You can’t perform that action at this time.

You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.

首页 - Wiki
Copyright © 2011-2024 iteam. Current version is 2.125.1. UTC+08:00, 2024-05-17 12:58
浙ICP备14020137号-1 $访客地图$