Java 语法糖

君子以自强不息。

泛型与类型擦除

泛型是 JDK 1.5 的一项新增特性,它的本质是参数化类型的应用。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法。

在 Java 没有出现泛型之前只能通过 Object 是所有类型的父类和类型强制转换两个特点的配合来实现类型泛化。

泛型技术在 C# 和 Java 之中的使用方式看似相同,但实现上却有着根本性的分歧:

C# 泛型

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

Java 泛型

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

泛型擦除示例

把 Java 代码编译成 class 文件,然后再通过字节码反编译工具进行反编译后, 将会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型。

泛型擦除前:

1
Map<String, String> map = new HashMap<>();

泛型擦除后:

1
Map map = new HashMap();

泛型与重载

下列代码是否可以编译通过?

1
2
3
4
5
6
7
8
9
public class martin{
    public static void test(List<String> list){
        // ...
    }

    public static void test(List<Integer> list){
        // ...  
    }  
}

答案是:不能。

因为参数 List 和 List 编译之后都被擦除了,变成了一样的原生类型 List,擦除动作导致这两种方法的特征签名变得一模一样。

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

自动装箱、拆箱与遍历循环编译之前:

1
2
3
4
5
6
7
public static void main(String[] args) {
    List<Integer> list = Arrays.asList(1,2,3);
    int sum = 0;
    for (int i : list){
        sum += i;
    }
}

自动装箱、拆箱与遍历循环编译之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
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;
    }
}

自动装箱、拆箱在编译之后被转化成了对应的包装和还原方法,如上面的 Integer.valueOf() 与 Integer.intValue() 方法。

遍历循环则把代码还原成了迭代器的实现,这也是为何遍历循环需要被遍历的类实现 Iterable 接口的原因。

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static void main(String[] args) {
    Integer a = 1;
    Integer b = 2;
    Integer c = 3;
    Integer d = 3;
    Integer e = 321;
    Integer f = 321;
    Long g = 3L;
    System.out.println(c == d); // 他们指向同一个对象,返回 True
    System.out.println(e == f); // 他们的值大于127,即使值相同,但是对应不同的内存地址,返回 false。
    System.out.println(c == (a + b)); // 自动拆箱后他们的值是相等的,返回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方法: 继承自Object类,在具体实现时可以覆盖父类中的实现。它的实现也是对对象的地址进行比较,此时它和”==”的作用相同。JDK 类中有一些类覆盖了 Object 类的 equals() 方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回 true,这些类有: java.io.file,java.util.Date,java.lang.string,包装类(Integer, Double 等)。

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

(注:Integer 使用一个内部静态类中的一个静态数组保存了 -128~127 范围内的数据,静态数组在类加载以后是存在方法区的,并不是什么常量池。在自动装箱的时候,首先判断要装箱的数字的范围,如果在 -128~127 的范围则直接返回缓存中已有的对象,否则 new 一个新的对象。)