关于 Java 你不知道的 10 件事

作为 Java 书呆子,比起实用技能,我们会对介绍 Java 和 JVM 的概念细节更感兴趣。因此我想推荐 Lukas Eder 在 jooq.org 发表的原创作品给大家。

你是从很早开始就一直使用 Java 吗?那你还记得它的过去吗?那时,Java 还叫 Oak,OO 还是一个热门话题,C++ 的 folk 者认为 Java 是不可能火起来,Java 开发的小应用程序 Applets 还受到关注。

我敢打赌,下面我要介绍的这些事,有一半你都不知道。下面让我们来深入探索 Java 的神秘之处。

1. 没有检查异常这种事情

没错!JVM 不会知道这些事情,只有 Java 语句知道。

如今大家都认为检查异常是个错误。正如 Bruce Eckel 在布拉格 GeeCON 闭幕时所说,Java 之后再没别的语言检查异常,甚至 Java 8 在新的 Stream API 中也不再干这个事情(如果你的 Lambda 使用 IO 和 JDBC,这其实还是有点痛苦)。

如何证实 JVM 并不清楚检查异常一事?试试下面的代码:

public class Test {

// No throws clause here

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 语言不允许两个方法在同一个类中“等效重载”,而忽略其诸如throws自居或返回类型等的潜在的差异。

查看 Class.getMethod(String, Class…) 的 Javadoc。 其中说明如下:

请注意,类中可能有多个匹配方法,因为 Java 语言禁止在一个类声明具有相同签名但返回类型不同的多个方法,但 Java 虚拟机并不是如此。虚拟机中增加的灵活性可以用于实现各种语言特征。例如,可以用桥接方法实现协变参返回; 桥接方法和被重写的方法将具有相同的签名但拥有不同的返回类型。

哇哦,有道理。实际上下面的代码暗藏着很多事情:

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><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[][] = {{}};

}

你认为这很疯狂?想象在上面使用 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. 条件表达式的特殊情况

可能大多数人会认为:

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. 你还没搞懂复合赋值运算符

很奇怪吗?来看看下面这两行代码:

i += j;

i = i + j;

直观看来它们等价,是吗?但可其实它们并不等价!JLS 解释如下:

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. 随机整数

现在有一个更难的谜题。不要去看答案,看看你能不能自己找到答案。如果运行下面的程序:

for (int i = 0; i < 10; i++) {

System.out.println((Integer) i);

}

… “有时候”,我会得到下面的输出:

92

221

45

48

236

183

39

193

33

84

这怎么可能??

. spoiler… 继续解答…

好了,答案在这里 (https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/),这必须通过反射重写 JDK 的 Integer 缓存,然后使用自动装箱和拆箱。不要在家干这种事情!或者,我们应该换种方式进行此类操作。

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          // Jumping forward

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          // Jumping backward

9  ..

8. Java 有类型别名

其它语言 (比如 Ceylon) 中,我们很容易为类型定义别名:

interface People => Set<Person>;

这里产生了 People 类型,使用它就跟使用 Set<Person> 一样:

People?      p1 = null;

Set</Person><Person>? p2 = p1;

People?      p3 = p2;

Java 中我们不能在顶层作用域定义类型别名,但是我们可以在类或方法作用域中干这个事情。假如我们不喜欢 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 类作用域内 Integer 被赋予 I 这样的 “别名”,类似地,Long 在 x() 方法中被赋予 L 这样的 “别名”。之后我们可以这样调用方法:

new Test().x(1, 2L);

这种技术当然不太会受重视。这种情况下,Integer 和 Long 都是 final 类型,也就是说,I 和 L 是事实上的别名(基本上赋值兼容性只需要考虑一种可能性)。如果我们使用非 final 类型 (比如 Object),那就是一般的泛型。

这些把戏已经玩够了。现在来看看真正了不起的东西!

9. 某些类型的关系并不确定!

好了,这会很引人注目,先来杯咖啡提提神。思考一下下面两个类型:

// 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>>>> {}

现在告诉我,类型 C 和 D 到底是什么?

它们存在递归,是一种类似 java.lang.Enum (但有略微不同)的递归方式。看看:

public abstract class Enum<E extends Enum<E>> { ... }

在上面的描述中,enum 实际上只是单纯的语法糖:

// This

enum MyEnum {}

// Is really just sugar for this

class MyEnum extends Enum<MyEnum> { ... }

认识到这一点之后我们回过头来看看前面提到的两个类型,下面的代码会编译成什么样?

class Test {

Type< ? super C> c = new C();

Type< ? super D<Byte>> d = new D<Byte>();

}

非常难回答的问题,不过 Ross Tate 已经回答了。这个问题的答案是不可判定的:

C 是 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)

然后:

D 是 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) Type<Type<? super C<C>>> <?: Type<? super C<C>>

Step 4) D<D<Byte>> <?: Type<? super D<D<Byte>>>

Step . . . (expand forever)

在 Eclipse 中试着编译一下,它会崩溃! (不用担心,我提交了 BUG 报告)

让这个事情沉下去…

Java 中某些类型的关系是不明确的!

如果你对 Java 这个用法感到奇怪之余也感兴趣,就去看看 Ross Tate 写的 “在 Java 的类型系统中使用通配符” (与 Alan Leung 和 Sorin Lerner 合著),我们也在讨论泛型多态中的相关子类多态性。

10. 类型交集

Java 有一个非常奇怪的特性叫类型交集。你可以申明某个(泛型)类型,而它实际上是两个类型的交集,比如:

class Test<T extends Serializable & Cloneable> {

}

绑定到 Test 类型实例的泛型类型参数 T 必须实现 Serializable 和 Cloneable。比如,String 就不符合要求,但 Dete 满足:

// Doesn‘t compile

Test<String> s = null;

// Compiles

Test<Date> d = null;

这个特性已经在 Java 8 中使用。这很有用吗?几乎没用,但是如果你希望某个 Lambda 表达式是这种类型,还真没别的办法。假设你的方法有这种疯狂的类型约束:

<T extends Runnable & Serializable> void execute(T t) {}

你想通过执行它得到一个可以序列化 (Serializable) 的 Runnable 对象。Lambda 和序列化也有点奇怪。

Lambda 可以序列经:

如果 Lambda 的目标类型和参数类型都可以序列化,那么你可以序列化这个 Lambda

但是即使是这样,他们都不能自动实现 Serializable 标记接口。你必须强制转换类型。但是当你只扔给 Serializable 时…

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

… 那么 lambda 将不再是 Runnable 的。

因此要把它转换为两种类型:

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

结论

一句话总结这篇文章就是:

Java 恰好是一种看起来神秘的语言,其实不然。

来源:码农网

时间: 2024-10-05 05:59:10

关于 Java 你不知道的 10 件事的相关文章

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

关于Java你可能不知道的10件事 分享到: 24 本文由 ImportNew - Jerry Lee 翻译自 Jooq.欢迎加入翻译小组.转载请参见文章末尾的要求. 呃,你是不是写Java已经有些年头了?还依稀记得这些吧: 那些年,它还叫做Oak:那些年,OO还是个热门话题:那些年,C++同学们觉得Java是没有出路的:那些年,Applet还风头正劲-- 但我打赌下面的这些事中至少有一半你还不知道.这周我们来聊聊这些会让你有些惊讶的Java内部的那些事儿吧. 1. 其实没有受检异常(check

Ubutnu安装后要做的10件事

Ubutnu安装后要做的10件事 安装过很多次的Ubuntu,每次都要进行一系列的系统开发环境的配置工作,如安装jdk,配置java环境变量,安装mysql,wine QQ,安装最新的eclipse,下载一些应用软件等,但是很多博文都很零散,每次都得一个一个的找,感觉特别的麻烦,今天在这里整理一下.注意,干货来了!!! 一.首先更新系统 $ sudo apt-get update && sudo apt-get upgrade 二.安装flash player 1.首先下载flash pl

关于左撇子你不知道的6件事

1.Left-handers are more likely to develop schizophrenia. A whopping 20 percent of schizophrenics are said to be left-handers, which is a strong proportion considering they only make up 10 percent of the world's population. What's more, being left han

「译」forEach循环中你不知道的3件事

前言 本文925字,阅读大约需要7分钟. 总括: forEach循环中你不知道的3件事. 原文地址:3 things you didn't know about the forEach loop in JS 公众号:「前端进阶学习」,回复「666」,获取一揽子前端技术书籍 自弃者扶不起,自强者击不倒. 正文 你觉得你真的学会用forEach了么? 这是我之前对forEach循环的理解:就是一个普通语义化之后的for循环,可以被break,continue,return. 这篇文章将向你展示for

高效人士睡觉前做的10件事

本文翻译自网站lifehack,原文链接http://www.lifehack.org/articles/productivity/10-things-productive-people-before-bed.html,作者为LUIGI POTENZA,译者为foruok 大部分高效能人士在日常生活中都有一些能帮助他们成功的习惯.他们明白,事业成功的关键在于自己的心理和身体健康,而这又取决于他们的睡前习惯. 这10件事,帮助那些成功人士成为社会上最能赚钱的那20%. 1.他们回顾自己的一天 史蒂

iOS 9:你需要知道的10件事

所有的iPhone用户都在翘首等待最新的iOS发布,iOS 9于昨日正式发布!在这里我们一起来看看有关于iOS 9你必须了解的10件事. 1) Notes 苹果公司的Note应用程序将变得更加先进,其将能够在你的日常生活中作出更大的贡献.新增类别标题.地图.图片,同时URL支持也加入其中.新版本的Note应用程序将为用户提供使用图像注释和创建手写提醒的功能. 2) 地图 用户们可以预计到苹果的地图将会彻底转变.最初,当苹果刚开始推出地图时出现了很多问题,它无法在其推出的初始阶段获得准确的数据.随

使用Office 365前,企业必须要知道的10件事

目前的市场上充斥着很多关于微软Office 365的炒作,相信厂商.客户或者企业的都有自己不同的考虑.Office 365是微软云版本的Office,用户可以通过互联网创建一个帐户,付款.下载应用安装,然后使用,这一过程中不需要使用光盘.如果企业用户认为使用Office 365对于公司来说是正确的一步的话,那么管理者就有必要先了解以下由桌面解决方案顾问Sales Harkins所提出的10件事. 1.什么是云? 云是一个行业术语,指异地文件托管服务.当使用Office 365文件的时候,用户使用

摄影新手最想知道的10件事(转)

摄影新手最想知道的10件事 原文地址 http://www.fsbus.com/danfanrumen/25114.html 作为摄影新手 面对摄影难免有许多疑问 焦距.光圈.景深.快门如何把控? 怎么拍才不跑焦? 如何掌握构图.用光? 一天中最佳拍摄时间是什么时候? 无需上网逐个搜索这些问题 在本篇文章中小编为你整理了 摄影新手最想知道的10件事 简单易懂 赶紧马住 焦距镜头的焦距是什么呢?简单来说,就是一个镜头能拍多远或者多宽.数字越小,焦距越短,视角也就越广,反之,数字越大,焦距越长,视角

当开发者产生一个伟大的想法之后应该做的10件事

当你正和家人享受一个悠闲的午后,一个不错的想法突然出现在你的脑海里.不管它是一个 App 还是服务,或是一个新的概念.只要你把这个想法付诸实践,它就可能会成为下一个 uber,甚至会改变世界. 那接下来你应该怎么做呢?这里有一个指南,会告诉你在决定把自己这个想法实现之后应该做的事情. 1. 起一个名字 在你决定开始之后,要做的第一件事就是为你的产品起一个名字,这个名字是有多重要大家心里都很清楚,如果你并不擅长起名字,你可以通过一些工具来扩展你的思路,如 visual thesaurus, Wer