syntactic-sugar.md 30.7 KB
Newer Older
H
hollis.zhl 已提交
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29


语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。

本 Chat 从 Java 编译原理角度,深入字节码及 class 文件,抽丝剥茧,了解 Java 中的语法糖原理及用法,帮助大家在学会如何使用 Java 语法糖的同时,了解这些语法糖背后的原理,主要内容如下:

什么是语法糖 糖块一 —— switch 支持 String 与枚举 糖块二 —— 泛型与类型擦除 糖块三 —— 自动装箱与拆箱 ...... 糖块十一 —— try-with-resource 糖块十二 —— lambda 表达式 糖衣炮弹 —— 语法糖使用过程中需要注意的点 综合应用


### 语法糖

语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。简而言之,语法糖让程序更加简洁,有更高的可读性。

> 有意思的是,在编程领域,除了语法糖,还有语法盐和语法糖精的说法,篇幅有限这里不做扩展了。

我们所熟知的编程语言中几乎都有语法糖。作者认为,语法糖的多少是评判一个语言够不够牛逼的标准之一。很多人说Java是一个“低糖语言”,其实从Java 7开始Java语言层面上一直在添加各种糖,主要是在“Project Coin”项目下研发。尽管现在Java有人还是认为现在的Java是低糖,未来还会持续向着“高糖”的方向发展。

### 解语法糖

前面提到过,语法糖的存在主要是方便开发人员使用。但其实,Java虚拟机并不支持这些语法糖。这些语法糖在编译阶段就会被还原成简单的基础语法结构,这个过程就是解语法糖。

说到编译,大家肯定都知道,Java语言中,`javac`命令可以将后缀名为`.java`的源文件编译为后缀名为`.class`的可以运行于Java虚拟机的字节码。如果你去看`com.sun.tools.javac.main.JavaCompiler`的源码,你会发现在`compile()`中有一个步骤就是调用`desugar()`,这个方法就是负责解语法糖的实现的。

Java 中最常用的语法糖主要有泛型、变长参数、条件编译、自动拆装箱、内部类等。本文主要来分析下这些语法糖背后的原理。一步一步剥去糖衣,看看其本质。

### 糖块一、 switch 支持 String 与枚举

前面提到过,从Java 7 开始,Java语言中的语法糖在逐渐丰富,其中一个比较重要的就是Java 7中`switch`开始支持`String`

D
dearsn 已提交
30
在开始coding之前先科普下,Java中的`switch`自身原本就支持基本类型。比如`int``char`等。对于`int`类型,直接进行数值的比较。对于`char`类型则是比较其ascii码。所以,对于编译器来说,`switch`中其实只能使用整型,任何类型的比较都要转换成整型。比如`byte``short``char`(ackii码是整型)以及`int`
H
hollis.zhl 已提交
31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684

那么接下来看下`switch``String`得支持,有以下代码:

    public class switchDemoString {
        public static void main(String[] args) {
            String str = "world";
            switch (str) {
            case "hello":
                System.out.println("hello");
                break;
            case "world":
                System.out.println("world");
                break;
            default:
                break;
            }
        }
    }


[反编译][1]后内容如下:

    public class switchDemoString
    {
        public switchDemoString()
        {
        }
        public static void main(String args[])
        {
            String str = "world";
            String s;
            switch((s = str).hashCode())
            {
            default:
                break;
            case 99162322:
                if(s.equals("hello"))
                    System.out.println("hello");
                break;
            case 113318802:
                if(s.equals("world"))
                    System.out.println("world");
                break;
            }
        }
    }


看到这个代码,你知道原来**字符串的switch是通过`equals()`和`hashCode()`方法来实现的。**还好`hashCode()`方法返回的是`int`,而不是`long`

> 仔细看下可以发现,进行`switch`的实际是哈希值,然后通过使用`equals`方法比较进行安全检查,这个检查是必要的,因为哈希可能会发生碰撞。因此它的性能是不如使用枚举进行switch或者使用纯整数常量,但这也不是很差。

### 糖块二、 泛型

我们都知道,很多语言都是支持泛型的,但是很多人不知道的是,不同的编译器对于泛型的处理方式是不同的,通常情况下,一个编译器处理泛型有两种方式:`Code specialization``Code sharing`。C++和C#是使用`Code specialization`的处理机制,而Java使用的是`Code sharing`的机制。

> Code sharing方式为每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(`type erasue`)实现的。

也就是说,**对于Java虚拟机来说,他根本不认识`Map<String, String> map`这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。**

类型擦除的主要过程如下: 1.将所有的泛型参数用其最左边界(最顶级的父类型)类型替换。 2.移除所有的类型参数。

以下代码:

    Map<String, String> map = new HashMap<String, String>();
    map.put("name", "hollis");
    map.put("wechat", "Hollis");
    map.put("blog", "www.hollischuang.com");


解语法糖之后会变成:

    Map map = new HashMap();
    map.put("name", "hollis");
    map.put("wechat", "Hollis");
    map.put("blog", "www.hollischuang.com");


以下代码:

    public static <A extends Comparable<A>> A max(Collection<A> xs) {
        Iterator<A> xi = xs.iterator();
        A w = xi.next();
        while (xi.hasNext()) {
            A x = xi.next();
            if (w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }


类型擦除后会变成:

     public static Comparable max(Collection xs){
        Iterator xi = xs.iterator();
        Comparable w = (Comparable)xi.next();
        while(xi.hasNext())
        {
            Comparable x = (Comparable)xi.next();
            if(w.compareTo(x) < 0)
                w = x;
        }
        return w;
    }


**虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类并没有自己独有的`Class`类对象。比如并不存在`List<String>.class`或是`List<Integer>.class`,而只有`List.class`。**

### 糖块三、 自动装箱与拆箱

自动装箱就是Java自动将原始类型值转换成对应的对象,比如将int的变量转换成Integer对象,这个过程叫做装箱,反之将Integer对象转换成int类型值,这个过程叫做拆箱。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型byte, short, char, int, long, float, double 和 boolean 对应的封装类为Byte, Short, Character, Integer, Long, Float, Double, Boolean。

先来看个自动装箱的代码:

     public static void main(String[] args) {
        int i = 10;
        Integer n = i;
    }


反编译后代码如下:

    public static void main(String args[])
    {
        int i = 10;
        Integer n = Integer.valueOf(i);
    }


再来看个自动拆箱的代码:

    public static void main(String[] args) {

        Integer i = 10;
        int n = i;
    }


反编译后代码如下:

    public static void main(String args[])
    {
        Integer i = Integer.valueOf(10);
        int n = i.intValue();
    }


从反编译得到内容可以看出,在装箱的时候自动调用的是`Integer``valueOf(int)`方法。而在拆箱的时候自动调用的是`Integer``intValue`方法。

所以,**装箱过程是通过调用包装器的valueOf方法实现的,而拆箱过程是通过调用包装器的 xxxValue方法实现的。**

### 糖块四 、 方法变长参数

可变参数(`variable arguments`)是在Java 1.5中引入的一个特性。它允许一个方法把任意数量的值作为参数。

看下以下可变参数代码,其中print方法接收可变参数:

    public static void main(String[] args)
        {
            print("Holis", "公众号:Hollis", "博客:www.hollischuang.com", "QQ:907607222");
        }

    public static void print(String... strs)
    {
        for (int i = 0; i < strs.length; i++)
        {
            System.out.println(strs[i]);
        }
    }


反编译后代码:

     public static void main(String args[])
    {
        print(new String[] {
            "Holis", "\u516C\u4F17\u53F7:Hollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com", "QQ\uFF1A907607222"
        });
    }

    public static transient void print(String strs[])
    {
        for(int i = 0; i < strs.length; i++)
            System.out.println(strs[i]);

    }


从反编译后代码可以看出,可变参数在被使用的时候,他首先会创建一个数组,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中。

> PS:反编译后的print方法声明中有一个transient标识,是不是很奇怪?transient不是不可以修饰方法吗?transient不是和序列化有关么?transient在这里的作用是什么?因为这个与本文关系不大,这里不做深入分析了。相了解的同学可以关注我微信公众号或者博客。

### 糖块五 、 枚举

Java SE5提供了一种新的类型-Java的枚举类型,关键字`enum`可以将一组具名的值的有限集合创建为一种新的类型,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。

要想看源码,首先得有一个类吧,那么枚举类型到底是什么类呢?是`enum`吗?答案很明显不是,`enum`就和`class`一样,只是一个关键字,他并不是一个类,那么枚举是由什么类维护的呢,我们简单的写一个枚举:

    public enum t {
        SPRING,SUMMER;
    }


然后我们使用反编译,看看这段代码到底是怎么实现的,反编译后代码内容如下:

    public final class T extends Enum
    {
        private T(String s, int i)
        {
            super(s, i);
        }
        public static T[] values()
        {
            T at[];
            int i;
            T at1[];
            System.arraycopy(at = ENUM$VALUES, 0, at1 = new T[i = at.length], 0, i);
            return at1;
        }

        public static T valueOf(String s)
        {
            return (T)Enum.valueOf(demo/T, s);
        }

        public static final T SPRING;
        public static final T SUMMER;
        private static final T ENUM$VALUES[];
        static
        {
            SPRING = new T("SPRING", 0);
            SUMMER = new T("SUMMER", 1);
            ENUM$VALUES = (new T[] {
                SPRING, SUMMER
            });
        }
    }


通过反编译后代码我们可以看到,`public final class T extends Enum`,说明,该类是继承了`Enum`类的,同时`final`关键字告诉我们,这个类也是不能被继承的。**当我们使用`enmu`来定义一个枚举类型的时候,编译器会自动帮我们创建一个`final`类型的类继承`Enum`类,所以枚举类型不能被继承。**

### 糖块六 、 内部类

内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。

**内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,`outer.java`里面定义了一个内部类`inner`,一旦编译成功,就会生成两个完全不同的`.class`文件了,分别是`outer.class`和`outer$inner.class`。所以内部类的名字完全可以和它的外部类名字相同。**

    public class OutterClass {
        private String userName;

        public String getUserName() {
            return userName;
        }

        public void setUserName(String userName) {
            this.userName = userName;
        }

        public static void main(String[] args) {

        }

        class InnerClass{
            private String name;

            public String getName() {
                return name;
            }

            public void setName(String name) {
                this.name = name;
            }
        }
    }


以上代码编译后会生成两个class文件:`OutterClass$InnerClass.class``OutterClass.class` 。当我们尝试对`OutterClass.class`文件进行反编译的时候,命令行会打印以下内容:`Parsing OutterClass.class...Parsing inner class OutterClass$InnerClass.class... Generating OutterClass.jad` 。他会把两个文件全部进行反编译,然后一起生成一个`OutterClass.jad`文件。文件内容如下:

    public class OutterClass
    {
        class InnerClass
        {
            public String getName()
            {
                return name;
            }
            public void setName(String name)
            {
                this.name = name;
            }
            private String name;
            final OutterClass this$0;

            InnerClass()
            {
                this.this$0 = OutterClass.this;
                super();
            }
        }

        public OutterClass()
        {
        }
        public String getUserName()
        {
            return userName;
        }
        public void setUserName(String userName){
            this.userName = userName;
        }
        public static void main(String args1[])
        {
        }
        private String userName;
    }


### 糖块七 、条件编译

—般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,希望只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。

如在C或CPP中,可以通过预处理语句来实现条件编译。其实在Java中也可实现条件编译。我们先来看一段代码:

    public class ConditionalCompilation {
        public static void main(String[] args) {
            final boolean DEBUG = true;
            if(DEBUG) {
                System.out.println("Hello, DEBUG!");
            }

            final boolean ONLINE = false;

            if(ONLINE){
                System.out.println("Hello, ONLINE!");
            }
        }
    }


反编译后代码如下:

    public class ConditionalCompilation
    {

        public ConditionalCompilation()
        {
        }

        public static void main(String args[])
        {
            boolean DEBUG = true;
            System.out.println("Hello, DEBUG!");
            boolean ONLINE = false;
        }
    }


首先,我们发现,在反编译后的代码中没有`System.out.println("Hello, ONLINE!");`,这其实就是条件编译。当`if(ONLINE)`为false的时候,编译器就没有对其内的代码进行编译。

所以,**Java语法的条件编译,是通过判断条件为常量的if语句实现的。其原理也是Java语言的语法糖。根据if判断条件的真假,编译器直接把分支为false的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个Java类的结构或者类的属性上进行条件编译,这与C/C++的条件编译相比,确实更有局限性。在Java语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强。**

### 糖块八 、 断言

在Java中,`assert`关键字是从JAVA SE 1.4 引入的,为了避免和老版本的Java代码中使用了`assert`关键字导致错误,Java在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关`-enableassertions``-ea`来开启。

看一段包含断言的代码:

    public class AssertTest {
        public static void main(String args[]) {
            int a = 1;
            int b = 1;
            assert a == b;
            System.out.println("公众号:Hollis");
            assert a != b : "Hollis";
            System.out.println("博客:www.hollischuang.com");
        }
    }


反编译后代码如下:

    public class AssertTest {
       public AssertTest()
        {
        }
        public static void main(String args[])
    {
        int a = 1;
        int b = 1;
        if(!$assertionsDisabled && a != b)
            throw new AssertionError();
        System.out.println("\u516C\u4F17\u53F7\uFF1AHollis");
        if(!$assertionsDisabled && a == b)
        {
            throw new AssertionError("Hollis");
        } else
        {
            System.out.println("\u535A\u5BA2\uFF1Awww.hollischuang.com");
            return;
        }
    }

    static final boolean $assertionsDisabled = !com/hollis/suguar/AssertTest.desiredAssertionStatus();


    }


很明显,反编译之后的代码要比我们自己的代码复杂的多。所以,使用了assert这个语法糖我们节省了很多代码。**其实断言的底层实现就是if语言,如果断言结果为true,则什么都不做,程序继续执行,如果断言结果为false,则程序抛出AssertError来打断程序的执行。**`-enableassertions`会设置$assertionsDisabled字段的值。

### 糖块九 、 数值字面量

在java 7中,数值字面量,不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的就是方便阅读。

比如:

    public class Test {
        public static void main(String... args) {
            int i = 10_000;
            System.out.println(i);
        }
    }


反编译后:

    public class Test
    {
      public static void main(String[] args)
      {
        int i = 10000;
        System.out.println(i);
      }
    }


反编译后就是把`_`删除了。也就是说 **编译器并不认识在数字字面量中的`_`,需要在编译阶段把他去掉。**

### 糖块十 、 for-each

增强for循环(`for-each`)相信大家都不陌生,日常开发经常会用到的,他会比for循环要少写很多代码,那么这个语法糖背后是如何实现的呢?

    public static void main(String... args) {
        String[] strs = {"Hollis", "公众号:Hollis", "博客:www.hollischuang.com"};
        for (String s : strs) {
            System.out.println(s);
        }
        List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");
        for (String s : strList) {
            System.out.println(s);
        }
    }


反编译后代码如下:

    public static transient void main(String args[])
    {
        String strs[] = {
            "Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com"
        };
        String args1[] = strs;
        int i = args1.length;
        for(int j = 0; j < i; j++)
        {
            String s = args1[j];
            System.out.println(s);
        }

        List strList = ImmutableList.of("Hollis", "\u516C\u4F17\u53F7\uFF1AHollis", "\u535A\u5BA2\uFF1Awww.hollischuang.com");
        String s;
        for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))
            s = (String)iterator.next();

    }


代码很简单,**for-each的实现原理其实就是使用了普通的for循环和迭代器。**

### 糖块十一 、 try-with-resource

Java里,对于文件操作IO流、数据库连接等开销非常昂贵的资源,用完之后必须及时通过close方法将其关闭,否则资源会一直处于打开状态,可能会导致内存泄露等问题。

关闭资源的常用方式就是在`finally`块里是释放,即调用`close`方法。比如,我们经常会写这样的代码:

    public static void main(String[] args) {
        BufferedReader br = null;
        try {
            String line;
            br = new BufferedReader(new FileReader("d:\\hollischuang.xml"));
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            // handle exception
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (IOException ex) {
                // handle exception
            }
        }
    }


从Java 7开始,jdk提供了一种更好的方式关闭资源,使用`try-with-resources`语句,改写一下上面的代码,效果如下:

    public static void main(String... args) {
        try (BufferedReader br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"))) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            // handle exception
        }
    }


看,这简直是一大福音啊,虽然我之前一般使用`IOUtils`去关闭流,并不会使用在`finally`中写很多代码的方式,但是这种新的语法糖看上去好像优雅很多呢。看下他的背后:

    public static transient void main(String args[])
        {
            BufferedReader br;
            Throwable throwable;
            br = new BufferedReader(new FileReader("d:\\ hollischuang.xml"));
            throwable = null;
            String line;
            try
            {
                while((line = br.readLine()) != null)
                    System.out.println(line);
            }
            catch(Throwable throwable2)
            {
                throwable = throwable2;
                throw throwable2;
            }
            if(br != null)
                if(throwable != null)
                    try
                    {
                        br.close();
                    }
                    catch(Throwable throwable1)
                    {
                        throwable.addSuppressed(throwable1);
                    }
                else
                    br.close();
                break MISSING_BLOCK_LABEL_113;
                Exception exception;
                exception;
                if(br != null)
                    if(throwable != null)
                        try
                        {
                            br.close();
                        }
                        catch(Throwable throwable3)
                          {
                            throwable.addSuppressed(throwable3);
                        }
                    else
                        br.close();
            throw exception;
            IOException ioexception;
            ioexception;
        }
    }


**其实背后的原理也很简单,那些我们没有做的关闭资源的操作,编译器都帮我们做了。所以,再次印证了,语法糖的作用就是方便程序员的使用,但最终还是要转成编译器认识的语言。**

### 糖块十二、Lambda表达式

关于lambda表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。**Labmda表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个JVM底层提供的lambda相关api。**

先来看一个简单的lambda表达式。遍历一个list:

    public static void main(String... args) {
        List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");

        strList.forEach( s -> { System.out.println(s); } );
    }


为啥说他并不是内部类的语法糖呢,前面讲内部类我们说过,内部类在编译之后会有两个class文件,但是,包含lambda表达式的类编译后只有一个文件。

反编译后代码如下:

    public static /* varargs */ void main(String ... args) {
        ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
        strList.forEach((Consumer<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$0(java.lang.String ), (Ljava/lang/String;)V)());
    }

    private static /* synthetic */ void lambda$main$0(String s) {
        System.out.println(s);
    }


可以看到,在`forEach`方法中,其实是调用了`java.lang.invoke.LambdaMetafactory#metafactory`方法,该方法的第四个参数implMethod指定了方法实现。可以看到这里其实是调用了一个`lambda$main$0`方法进行了输出。

再来看一个稍微复杂一点的,先对List进行过滤,然后再输出:

    public static void main(String... args) {
        List<String> strList = ImmutableList.of("Hollis", "公众号:Hollis", "博客:www.hollischuang.com");

        List HollisList = strList.stream().filter(string -> string.contains("Hollis")).collect(Collectors.toList());

        HollisList.forEach( s -> { System.out.println(s); } );
    }


反编译后代码如下:

    public static /* varargs */ void main(String ... args) {
        ImmutableList strList = ImmutableList.of((Object)"Hollis", (Object)"\u516c\u4f17\u53f7\uff1aHollis", (Object)"\u535a\u5ba2\uff1awww.hollischuang.com");
        List<Object> HollisList = strList.stream().filter((Predicate<String>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)Z, lambda$main$0(java.lang.String ), (Ljava/lang/String;)Z)()).collect(Collectors.toList());
        HollisList.forEach((Consumer<Object>)LambdaMetafactory.metafactory(null, null, null, (Ljava/lang/Object;)V, lambda$main$1(java.lang.Object ), (Ljava/lang/Object;)V)());
    }

    private static /* synthetic */ void lambda$main$1(Object s) {
        System.out.println(s);
    }

    private static /* synthetic */ boolean lambda$main$0(String string) {
        return string.contains("Hollis");
    }


两个lambda表达式分别调用了`lambda$main$1``lambda$main$0`两个方法。

**所以,lambda表达式的实现其实是依赖了一些底层的api,在编译阶段,编译器会把lambda表达式进行解糖,转换成调用内部api的方式。**

### 可能遇到的坑

#### 泛型

**一、当泛型遇到重载** public class GenericTypes {

        public static void method(List<String> list) {
            System.out.println("invoke method(List<String> list)");
        }

        public static void method(List<Integer> list) {
            System.out.println("invoke method(List<Integer> list)");
        }
    }


B
baichangfu 已提交
685
上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是`List<String>`另一个是`List<Integer>` ,但是,这段代码是编译通不过的。因为我们前面讲过,参数`List<Integer>``List<String>`编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。
H
hollis.zhl 已提交
686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766

**二、当泛型遇到catch** 泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型`MyException<String>``MyException<Integer>`

**三、当泛型内包含静态变量**

    public class StaticTest{
        public static void main(String[] args){
            GT<Integer> gti = new GT<Integer>();
            gti.var=1;
            GT<String> gts = new GT<String>();
            gts.var=2;
            System.out.println(gti.var);
        }
    }
    class GT<T>{
        public static int var=0;
        public void nothing(T x){}
    }


以上代码输出结果为:2!由于经过类型擦除,所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的。

#### 自动装箱与拆箱

**对象相等比较**

public class BoxingTest {

    public static void main(String[] args) {
        Integer a = 1000;
        Integer b = 1000;
        Integer c = 100;
        Integer d = 100;
        System.out.println("a == b is " + (a == b));
        System.out.println(("c == d is " + (c == d)));
    }


输出结果:

    a == b is false
    c == d is true


在Java 5中,在Integer的操作上引入了一个新功能来节省内存和提高性能。整型对象通过使用相同的对象引用实现了缓存和重用。

> 适用于整数值区间-128 至 +127。
>
> 只适用于自动装箱。使用构造函数创建对象不适用。

#### 增强for循环

**ConcurrentModificationException**

    for (Student stu : students) {
        if (stu.getId() == 2)
            students.remove(stu);
    }


会抛出`ConcurrentModificationException`异常。

Iterator是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出`java.util.ConcurrentModificationException`异常。

所以 `Iterator` 在工作的时候是不允许被迭代的对象被改变的。但你可以使用 `Iterator` 本身的方法`remove()`来删除对象,`Iterator.remove()` 方法会在删除当前迭代对象的同时维护索引的一致性。

### 总结

前面介绍了12种Java中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成JVM认识的语法。当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。

有了这些语法糖,我们在日常开发的时候可以大大提升效率,但是同时也要避免过渡使用。使用之前最好了解下原理,避免掉坑。

参考资料: [Java的反编译][1] [Java中的Switch对整型、字符型、字符串型的具体实现细节][2] [深度分析Java的枚举类型—-枚举的线程安全性及序列化问题][3] [Java的枚举类型用法介绍][4] [Java中的增强for循环(for each)的实现原理与坑][5] [Java中泛型的理解][6] [Java中整型的缓存机制][7] [Java中的可变参数][8]

 [1]: http://www.hollischuang.com/archives/58
 [2]: http://www.hollischuang.com/archives/61
 [3]: http://www.hollischuang.com/archives/197
 [4]: http://www.hollischuang.com/archives/195
 [5]: http://www.hollischuang.com/archives/1776
 [6]: http://www.hollischuang.com/archives/230
 [7]: http://www.hollischuang.com/archives/1174
D
dearsn 已提交
767
 [8]: http://www.hollischuang.com/archives/1271