关于Java你不知道的10件事
转载自 关于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: 1java.lang.String x();0 ldc </String><String "abc"> [16]2 areturnLine 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: 1bridge synthetic java.lang.Object x();0 aload_0 [this]1 invokevirtual Child.x() : java.lang.String [19]4 areturnLine 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 缓存,然后使用自动装箱和拆箱。不要在家干这种事情!或者,我们应该换种方式进行此类操作。
7GOTO
这是我的最爱之一。Java也有GOTO!输入下试试……
int goto = 1;
将输出:
Test.java:44: error: <identifier> expectedint goto = 1;
^
这是因为goto是一个未使用的关键字, 仅仅是为了以防万一……
但这不是最令人兴奋的部分。令人兴奋的部分是你可以使用 break、continue 和标记块来实现 goto 功能:
向前跳:
label: {// do stuff if (check) break label;// do more stuff
}
在字节码中格式如下:
2 iload_1 [check]3 ifeq 6 // Jumping forward6 ..
向后跳:
label: do {// do stuff if (check) continue label;// do more stuff break label;} while(true);
在字节码中格式如下:
2 iload_1 [check]3 ifeq 96 goto 2 // Jumping backward9 ..
8Java 有类型别名
其它语言 (比如 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 Listinterface 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 实际上只是单纯的语法糖:
// Thisenum MyEnum {}// Is really just sugar for thisclass 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 compileTest<String> s = null;// CompilesTest<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你不知道的10件事相关推荐
- 关于维基百科你不知道的十件事:
关于维基百科你不知道的十件事 关于维基百科你不知道的十件事是专门让那些缺乏维基百科经验的人,如记者.新编辑者或新读者,能够对维基百科有一些较深入的认知.这些内容并不会带 ...
- 运维 服务器安装,IT服务器运维安装CentOS后,你要做的10件事
IT服务器运维安装CentOS后,你要做的10件事 IT服务器小知识CentOS是一款社区驱动的免费Linux发行版,也是一款功能很强大的可替代红帽企业级Linux(RHEL)的发行版.它源自红帽企业 ...
- JavaScript 需要清楚的10件事
文/谢传贵 在学习JavaScript的过程中,最需要搞清楚的10件事是什么?关于这个问题有人在 Quora上给出了的答案.其中提到了一些很有代表性的知识点(坑),但描述比较杂乱.下面我将在他的基础上 ...
- 保护嵌入式802.11 Wi-Fi设备时需要考虑的10件事
保护嵌入式802.11 Wi-Fi设备时需要考虑的10件事 10 things to consider when securing an embedded 802.11 Wi-Fi device 随着 ...
- 35岁前必做10件事 让你少奋斗8年挣足钱
35岁前必做10件事 让你少奋斗8年挣足钱(转)中财网 (2010-03-27 16:04:01) 转载 标签: 就业 求职 乘数 钱经 李彦宏 美国 杂谈 分类:转载 男人.女人都要过三十五岁这堵墙 ...
- [转载]出了国才明白的10件事~(MITBBS ZT)
出了国才明白的10件事~(MITBBS ZT) 2007年10月16日 星期二 05:18 在国内时,了解"外面的世界"并不难,然而,认识的误区只有在国外住久了,慢慢地体会才能逐渐 ...
- 大规模运行MongoDB应该知道的10件事
MongoDB的首席解决方案架构师Asya Kamsky 最近发表了一篇文章,概括了大规模运行MongoDB需要知道的10件事. MongoDB也需要DevOps.MongoDB是一个数据库.和任何其 ...
- linux mint 19界面美化,安装完 LinuxMint 19.3 后必做的10件事
安装完 LinuxMint 19.3 后必做的10件事 LinuxMint 发行版是一款基于Ubuntu的易用性好,特别适合入门者使用的一款Linux发行版,相比于Ubuntu,界面和操作更友好. 本 ...
- 作为一个新晋测试经理,在软件测试计划之前你必须知道的10件事
有人喜欢创造世界,他们做了开发者:有的人喜欢开发者,他们做了测试员.什么是软件测试?软件测试就是一场本该在用户面前发生的灾难提前在自己面前发生了,这会让他们生出一种救世主的感觉,拯救了用户,也就拯救者 ...
最新文章
- stream流对象的理解及使用
- u-boot.lds文件简介
- 安装elasticsearch-php,安装 |《Elasticsearch-PHP 中文文档 6.0》| PHP 技术论坛
- 虚拟内存的配置(页面文件大小)
- c#学习总结(一)---Mr.Zhang
- javascript的运算(小结)
- ionic-cordova 支付宝支付插件cordova-plugin-alipay-v2使用篇
- PTA — 表格输出 (5 分)
- 如何读群晖硬盘_群晖nas使用教程6:将USB设备识别为本地SATA口硬盘 - 群晖教程...
- BZOJ5336 DP套DP
- 监督学习算法的发展史和它们之间的关系:从文氏图到回归、决策树、支持向量机和人工神经网络
- 人月神话(二)——为什么巴比伦塔会失败
- 基于MT7688模块的开发笔记12——给MT7688开发板添加WiFi功能
- [SWPUCTF 2021 新生赛]第一波放题(nssctf刷题)
- Invalid namespace
- java实验报告系统分析怎么写_20155218 《Java程序设计》实验二(Java面向对象程序设计)实验报告...
- group_concat函数用法
- 制作圆形图片,你会以下几种?
- 光模块自动测试系统软件,一种用于测试光模块的多通道自动测试方法及系统
- pytorch基于cpu下的速度测试及cpu线程设置
热门文章
- [设计模式]代理模式
- LeetCode 872叶子相似的树-简单
- 2018 蓝桥杯省赛 A 组模拟赛(一)数列求值+推导
- python注入进程_向进程中注入Python代码
- iis php打开空白页,windows+IIS+php 访问显示空白页 php版本信息访问正常
- 自适应滤波器在matlab仿真的程序_电气信息类专业课程之matlab系统仿真 第五章 BPSK通信系统(3)...
- 使用refs获取节点_闲庭信步聊前端 - 原来你是这样的Refs
- Web Service简介
- char *与char []
- 记一次应用配置的数据库连接被打满问题