Java中的语法糖

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

泛型和类型擦除

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

泛型思想早在C++语言的模板中就开始萌芽,在Java语言处于还没有出现泛型版本时,只能通过Object是所有类型的父类和类型强制转换两个特点来配合实现类型泛化。例如,在哈希表的存取中,JDK1.5之前使用HashMap的get方法,返回值就是一个Object对象,由于Java语言里面所有的类型都继承与java.lang.Object,所以Object转型成任何对象都是有可能的。但是也因为有无限种可能,就只有程序员和运行期的虚拟机才知道Object到底是什么对象。在编译期间,编译器无法检查到这个Object的强制类型转换是否成功,如果仅仅依赖程序员去保障这项操作的正确性的话,会有许多ClassCastException的风险转嫁到程序运行期间。

泛型在Java和C#中的使用方式看似相同,但实际上却有着根本性的分歧。C#里面泛型无论在程序源码中、编译后的IL中(Intermediate Language,中间语言,这时候泛型是一个占位符),或是运行期的CLR中,都是切实存在的, List<int> 与 List<String> 就是两个不同的类型,它们在系统运行期生成,有自己的虚方法表和类型数据,这种实现称为类型膨胀,基于这种方法实现的泛型称为真实泛型。

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

看一段简单的源码:

public class HH {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        list.add("1");
        list.add("2");
        list.add("3");
    }
}

将这段Java代码编译成Class文件,然后再用字节码反编译工具进行反编译后,会发现泛型都不见了,程序又变回了Java泛型出现之前的写法,泛型类型都变回了原生类型,如下所示:

public class HH {
    public HH() {
    }

    public static void main(String[] var0) {
        ArrayList var1 = new ArrayList();
        var1.add("1");
        var1.add("2");
        var1.add("3");
    }
}

当初JDK设计团队为什么选择类型擦除的方式来实现Java语言的泛型?这个不得而知,但是确实有不少人对Java语言提供的伪泛型颇有微词。在当时众多批评之中,有一些比较表面的,还有一些从性能上说泛型会由于强制转换操作和运行期缺少针对类型的优化等从而导致比C#的泛型要慢,则是完全偏离了方向,姑且不论Java泛型是不是真的会比C#泛型慢,选择从性能的角度评价用于提升语义准确性的泛型思想就不太恰当。但是在某些场景下,通过擦除法来实现泛型丧失了一些泛型思想应有的优雅:

public class HH {
    static void method(List<String> list) {

    }

    static void method(List<Integer> list) {

    }
}

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

但是在JDK1.6的环境下(JDK1.7和JDK1.8均是不可以的 ),下面的情况也是可以被编译的。

public class HH {
    static String method(List<String> list) {
        return "";
    }

    static int method(List<Integer> list) {
        return 1;
    }

    public static void main(String[] args) {
        String s = method(new ArrayList<String>());
        int i = method(new ArrayList<Integer>());
    }
}

重载当然不是根据返回值确定的,之所以这次能被编译和执行,是因为两个method方法加入了不同的返回值后才能共存在一个Class文件中。Class文件方法表(method_info)的数据结构中,方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重在选择,但是在Class文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法共存于一个Class文件中的。

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

从纯技术的角度讲,自动装箱、自动拆箱和循环遍历(Foreach遍历)这些语法糖,无论是实现上还是思想上都不能和泛型相比,两种的难度和深度有很大差距
。如下展示的是自动装箱、拆箱与遍历循环。

public class HH {
    public static void main(String[] args) {
        List<Integer> arrayList = Arrays.asList(1, 2, 5, 14, 5);
        int sum = 0;
        for (Integer integer : arrayList) {
            sum += integer;
        }
        System.out.println(sum);
    }
}

反编译后的结果如下所示,需要注意的是,不同的反编译器反编译后的结果是不同的。

public class HH
{
    public static void main(String[] paramArrayOfString)
    {
        List localList = Arrays.asList(new Integer[] { 
                Integer.valueOf(1), 
                Integer.valueOf(2), 
                Integer.valueOf(5), 
                Integer.valueOf(14), 
                Integer.valueOf(5) });
        int var2 = 0;

        Integer var4;
        for(Iterator var3 = var1.iterator(); var3.hasNext(); var2 += var4) {
            var4 = (Integer)var3.next();
        }

        System.out.println(var2);
    }
}

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

关于自动装箱

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);
    System.out.println(e == f);
    System.out.println(c == (a + b));
    System.out.println(c.equals(a + b));
    System.out.println(g == (a + b));
    System.out.println(g.equals(a + b));
}
打印结果:
true
false
true
true
true
false

鉴于包装类的“==”运算在遇不到算数运算的情况下不会自动拆箱,以及它们的equals方法不处理数据转型的关系,建议在实际编码中尽量避免这样使用自动装箱和拆箱。但是打印结果的前两条还是有些匪夷所思,看一看编译后的代码,能发现一些问题:

public static void main(String[] paramArrayOfString)
{
    Integer localInteger1 = Integer.valueOf(1);
    Integer localInteger2 = Integer.valueOf(2);
    Integer localInteger3 = Integer.valueOf(3);
    Integer localInteger4 = Integer.valueOf(3);
    Integer localInteger5 = Integer.valueOf(321);
    Integer localInteger6 = Integer.valueOf(321);
    Long localLong = Long.valueOf(3L);
    int i = 3;
    System.out.println(localInteger3 == localInteger4);
    System.out.println(localInteger5 == localInteger6);
    System.out.println(localInteger3.intValue() == localInteger1.intValue() + localInteger2.intValue());
    System.out.println(localInteger3.equals(Integer.valueOf(localInteger1.intValue() + localInteger2.intValue())));
    System.out.println(localLong.longValue() == localInteger1.intValue() + localInteger2.intValue());
    System.out.println(localLong.equals(Integer.valueOf(localInteger1.intValue() + localInteger2.intValue())));
    System.out.println(i == 3);
}

Integer在初始化的时候,其实是调用了Integer.valueOf()方法,如下:

    /**
     * Returns an {@code Integer} instance representing the specified
     * {@code int} value.  If a new {@code Integer} instance is not
     * required, this method should generally be used in preference to
     * the constructor {@link #Integer(int)}, as this method is likely
     * to yield significantly better space and time performance by
     * caching frequently requested values.
     *
     * This method will always cache values in the range -128 to 127,
     * inclusive, and may cache other values outside of this range.
     *
     * @param  i an {@code int} value.
     * @return an {@code Integer} instance representing {@code i}.
     * @since  1.5
     */
    public static Integer valueOf(int i) {
        assert IntegerCache.high >= 127;
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

注释中明确指出,如果数值在-128~127中间的话,将使用缓存值,其他数值会创建新的Integer对象,因此c和d的比较其实就是Integer缓存的比较,而非对象和对象之间的比较,故返回的是true。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 原文谈谈Java中的语法糖 语法糖(Syntactic Sugar),也称糖衣语法,指在计算机语言中添加的某种语法...
    小小少年Boy阅读 310评论 1 0
  • 他的硬盘坏了 存了多年的照片一张也找不到了 换了块新硬盘 一切都是新的 曾经的照片就那么不见了 他懊悔恼火 却不知...
    龙幺阅读 475评论 0 4
  • 少年得志,是每个人都渴盼的,毕竟人生在世也就几十个寒暑,而少年岁月又十分短暂,故而,一个人能少年得志,那是十分难得...
    锡安之光阅读 200评论 0 0
  • 许久未提笔 写什么呢? 那么多美好的日子 都已远去 下雪的时候 常常在结满霜的玻璃窗上 写你的名字 常常在夏日的河...
    素衣西子阅读 275评论 14 11