《深入理解Java虚拟机》-- 语法糖

865 阅读5分钟

语法糖简述 

定义:

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

Java语法糖的味道:

几乎各种语言或多或少都提供过一些语法糖来方便程序员的代码开发,这些语法糖虽然不会提供实质性的功能改进,但是它们或能提高效率,或能提升语法的严谨性,或能减少编码出错的机会。不过也有一种观点认为语法糖并不一定都是有益的,大量添加和使用“含糖”的语法,容易让程序员产生依赖,无法看清语法糖的糖衣背后,程序代码的真实面目。

解语法糖:

Java在现代编程语言之中属于“低糖语言”(相对于C#及许多其他JVM语言来说),尤其是JDK 1.5之前的版本。Java中最常用的语法糖主要是前面提到过的泛型(泛型并不一定都是语法糖实现,如C#的泛型就是直接由CLR支持的)、变长参数、自动装箱/拆箱,遍历循环(Foreach循环)等,虚拟机运行时不支持这些语法,它们在编译阶段还原回简单的基础语法结构,这个过程称为解语法糖。


泛型与类型擦除

泛型是JDK 1.5的一项新增特性,它的本质是参数化类型(Parametersized Type)的应用,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

最早期的泛型应用:

泛型思想早在C++语言的模板(Template)中就开始生根发芽,在Java语言处于还没有 出现泛型的版本时,只能通过Object是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。


缺点:

JDK 1.5之前使用HashMap的get()方法,返回值 就是一个Object对象,由于Java语言里面所有的类型都继承于java.lang.Object,所以Object转 型成任何对象都是有可能的。但是也因为有无限的可能性,就只有程序员和运行期的虚拟机才知道这个Object到底是个什么类型的对象。在编译期间,编译器无法检查这个Object的强制转型是否成功,如果仅仅依赖程序员去保障这项操作的正确性,许多ClassCastException的风险就会转嫁到程序运行期之中。


泛型技术:

C#里面 泛型无论在程序源码中、编译后的IL中,或是运行期的CLR中,都是切实存在的,List<int>与List<String>就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

Java语言中的泛型则不一样,它只在程序源码中存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Raw Type,也称为裸类型)了,并且在相应的地方插入了强制转型代码,因此,对于运行期的Java语言来说,ArrayList<int>与ArrayList<String>就是同一个类,所以泛型技术实际上是Java语言的一颗语法糖,Java语言中的泛型实现方法称为类型擦除,基于这种方法实现的泛型称为伪泛型


伪泛型的缺点:

当泛型遇见重载

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

这段代码是不能被编译的,因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样。


自动装箱、拆箱与遍历循环

// 编译前
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1, 2, 3, 4);
    // 如果在JDK1.7中,还有另外的一刻语法糖
    // 能让上面这句代码进一步简写成List<Integer> list = [1,2,3,4];
    int sum = 0;
    for (int i : list) {
        sum += i;
    }
    System.out.println(sum);
}

代码清单中一共包含了泛型、自动装箱、自动拆箱、遍历循环与变长参数5种语法糖。

// 编译后
public static void main(String[] args) {
    List list = Arrays.asList(
        new Integer[]{Integer.valueOf(1), Integer.valueOf(2), Integer.valueOf(3), Integer.valueOf(4)});    
    int sum = 0;
    for (Iterator localIterator = list.iterator(); localIterator.hasNext(); ) {
        int i = ((Integer) localIterator.next()).intValue();
        sum += i;
    }
    System.out.println(sum);
}

代码清单则展示了它们在编译后的变化。

泛型就不必说了,自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,如本例中的Integer.valueOf()与Integer.intValue()方法,而遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因。最后再看看变长参数,它在调用的时候变成了一个数组类型的参数,在变长参数出现之前,程序员就是使用数组来完成类似功能的。


自动装箱的陷阱:

public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    int d = 3;
    Integer e = 127;
    Integer f = 127;
    Integer e1 = 321;
    Integer f1 =321;
    Long g = 3L;
    System.out.println(c.equals(d));         // true
    System.out.println(c == d);              // true
    System.out.println(c == (a + b));        // true
    System.out.println(e == f);              // true
    System.out.println(e1 == f1);            // false
    System.out.println(e1.equals(f1));       // true
    System.out.println(c.equals(a + b));     // true
    System.out.println(g == (a + b));        // true
    System.out.println(g.equals(a + b));     // false
}

包装类的“==”运算在不遇到算术运算的情况下不会自动拆箱,以及它们equals()方法不处理数据转型的关系。

JVM会自动维护八种基本类型的常量池,int常量池中初始化-128~127的范围,所以当为Integer i=127时,在自动装箱过程中是取自常量池中的数值,而当Integer i=128时,128不在常量池范围内,所以在自动装箱过程中需new 128,所以地址不一样。对于Integer来说,你用==比较的是对象引用地址,而不是Integer的值。Integer你要把当当成一个对象来看待