专栏名称: ImportNew
伯乐在线旗下账号,专注Java技术分享,包括Java基础技术、进阶技能、架构设计和Java技术领域动态等。
目录
相关文章推荐
芋道源码  ·  关于DeepSeek的最新认知 ·  11 小时前  
芋道源码  ·  凌晨四点,线上CPU告警,绩效没了! ·  11 小时前  
Java编程精选  ·  字节员工无助:字节36岁技术2-2,被通知n ... ·  3 天前  
芋道源码  ·  老板爱瞎改权限怎么办:注解+AOP ... ·  3 天前  
芋道源码  ·  腾讯开源:零代码、全功能、强安全 ORM 库 ·  3 天前  
51好读  ›  专栏  ›  ImportNew

关于 Java 你不知道的十件事

ImportNew  · 公众号  · Java  · 2017-07-25 12:04

正文

。(点击 上方公众号 ,可快速关注)


编译:ImportNew - 心灵是一棵开花的树,

如有好文章投稿,请点击 → 这里了解详情


作为Java 控,我们总是对不太可能直接使用,但能使我们更了解 Java 和 Java 虚拟机(Java Virtual Machine,JVM) 的晦涩细节感兴趣。这也是我将 Lukas Eder 在 jooq.org 上写的这篇文章发布出来的原因。


你在Java发布的时候就开始使用了吗?还记得那时它叫“Oak”,面向对象也 (Object Oriented, OO )还是个热门话题,C++ 程序员们觉得 Java 完全没机会成功,Applet的出现也是一件新鲜大事?


我打赌下文中至少一半的内容你都不知道。让我们来看看这些令人惊喜的 Java 细节吧。


1. 受检异常(checked exception)这件事是不存在的


是这样的,JVM 完全不知道这件事,都是Java语言做的[只有Java语言这么干]。


现在,异常检查被公认为是个错误,正如 Brue 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. doThrow0(e);

}

@SuppressWarnings("unchecked")

static void doThrow0(Exception e) throws E {

throw (E) e;

}

}


这个不仅会编译,还会抛出 SQLException ,你甚至不需要 Lombok 的 @SneakyThrows 标签。


更多详情请参考 这篇文章


https://blog.jooq.org/2012/09/14/throw-checked-exceptions-like-runtime-exceptions-in-java/


或者 Stack Overflow 上的 这篇文章


http://stackoverflow.com/q/12580598/521799


2. 可以使用不同的返回值类型来重载方法


以下代码是编译不过的,对吧?


class Test {

Object x() { return "abc"; }

String x() { return "123"; }

}


是的,Java 不允许在一个类中通过不同的返回值类型和异常语句来重载方法。


不过稍等,Java 文档中关于 Class.getMethod(String, Class…) 这样写道:


请注意,在一个类中会有多个匹配的方法,因为虽然 Java 语法规则禁止一个类中存在多个方法函数签名相同仅仅返回类型不同,但 JVM 允许。这样提高了 JVM 的灵活性以实现各种语言特性。例如,可以用桥接方法(bridge method)来实现方法的协变返回类型,桥接方法和被重载的方法可以有相同的函数签名和不同的返回值类型。


喔,这是合理的。事实上,以下代码就是这样执行的,


abstract class Parent {

abstract T x();

}

class Child extends Parent {

@Override

String x() { return "abc";}

}


Child 类编译后的字节码是这样的:


// Method descriptor #15 ()Ljava/lang/String;

// Stack: 1, Locals: 1

java.lang.String x();

0  ldc [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 是实现这个特性最简单的方法了(同时实现了协变式覆盖)。很聪明吧。


你明白语言的内部特性了吗? 这里 有更多细节。


http://stackoverflow.com/q/442026/521799


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

class Test {

@Crazyy int[][]  a1 = {{}};

int @Crazyy [][] a2 = {{}};

int[] @Crazyy [] a3 = {{}};

@Crazyy int[] b1[]  = {{}};

int @Crazyy [] b2[] = {{}};

int[] b3 @Crazyy [] = {{}};

@Crazyy int c1[][]  = {{}};

int c2 @Crazyy [][] = {{}};

int c3[] @Crazyy [] = {{}};

}


类型注解,它的诡异性只是被他强大的功能掩盖了。


换句话说:


当我在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


是的,在确有必要的情况下,条件表达式会升级数字类型。你希望这个程序抛出一个空指针异常吗?


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


更多细节请看 这里


https://blog.jooq.org/2013/10/08/java-auto-unboxing-gotcha-beware/


5. 你也不懂复合赋值运算符


很诡异吗?让我们来看以下两段代码:


i += j;

i = i + j;


直觉上,他们是等价的吧?事实上不是,Java 语言规范(Java Language Standard,JLS)中这样写道:


符合赋值表达式 E1 op= E2 与 E1 = (T)((E1) op (E2)) 是等价的,这里 T 是 E1 的类型,期望 E1 只被求值一次。


很美吧,我想引用 Peter Lawrey 在 Stack Overflow 上回复


http://stackoverflow.com/a/8710747/521799


这种类型转换很好的一个例子是使用 *= or /=


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


这是怎么回事?


答案已经在前面剧透了……


答案在 这里 ,需要通过反射来重载 JDK 中的 Integer 缓存,然后使用自动装箱(auto-boxing)和自动拆箱(auto-unboxing)。千万不要这么做,我们假设如果再做一次。


https://blog.jooq.org/2013/10/17/add-some-entropy-to-your-jvm/


我在4周假期之前的最后一次代码提交中这么做了。



7. GOTO


这是我喜欢的一个。Java 有 GOTO 语句!输入以下:


int goto = 1;


结果将会是:


Test.java:44: error: 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 支持类型别名(type aliases)


在其它语言中(例如:Ceylon),定义类型别名是很容易的。


interface People => Set ;


People 类型通过这个方法就可以与 Set 互换使用了:


People?      p1 = null;

Set ? p2 = p1;

People?      p3 = p2;


在 Java 中,顶层代码里是不能定义类型别名的,但是我们可以在类和方法的作用域内这么做。假设我们不喜欢 Integer,[、]Long 这些名字,想要短一点的如 I 和 L,这是小菜一碟:


class Test {

void x(I i, L l) {

System.out.println(

i.intValue() + ", " +

l.longValue()

);

}

}


以上代码中,Integer 在 Test 类中用别名 I 替换, Long 在 x() 方法中用别名 L 替换。我们可以这样调用以上方法:


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


这个技术别太当真。在上边的例子里,Integer 和 Long 都是 final 类型, 也就是说 I 和 L 效果上是类型别名(大多数情况下,赋值兼容是单向的)。如果我们用非 final 的类型(例如 Object),就需要使用原来的泛型了。







请到「今天看啥」查看全文