Java中的语法糖

语法糖(Syntactic sugar),也译为糖衣语法,是由英国计算机科学家彼得·约翰·兰达(Peter J. Landin)发明的一个术语,指计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。通常来说使用语法糖能够增加程序的可读性,从而减少程序代码出错的机会。

Java语法糖的种类

java7(含)之前的语法糖主要包括以下语法:

  • 泛型
  • 自动装箱和自动拆箱
  • 遍历循环
  • 变长参数
  • 内部类
  • 枚举变量
  • 断言语句
  • 枚举和字符串的switch支持
  • try语句的定义和关闭资源
    为了了解这些语法糖背后的事情,我们来分别举例子,通过反编译每个例子生成的字节码文件,看看语法糖的真正实现。

泛型

泛型是Java5之后才添加的一种新特性,它的本质是参数化类型的应用,也就是类、接口、方法所操作的类型被指定一个参数,分别被称为泛型类、泛型接口、泛型方法。

但是Java的泛型和C++/C#的泛型本质上是不一样的,C++/C#的泛型是一个占位符,在运行期都是存在的,比如List<Integer>List<String>是不同的类型,在运行期都有自己的虚方法表和类型数据,这种实现才是真正的泛型,但是在Java里,泛型就是一个语法糖,只是在源码内存在,在编译后的字节码文件中,就已经替换为原来的原生类型(Object)了,也就是说在运行期中,List<Integer>List<String>在运行期就变成了List<Object>List<Object>,就是相同的类型,这种泛型就是伪泛型了,这种也是泛型擦除。
因为Java中的所有类的父类都是Object,而Object可以强制转换为所有类,基于Java的这个特性,泛型才能实现。
我们举个例子,反编译一下以下代码:

    public static void main(String[] args) {
        Map<String, String> map = new HashMap<>();
        map.put("hi", "你好");
        map.put("how are you?", "最近好吗");
        System.out.println(map.get("hi"));
        System.out.println(map.get("how are you?"));
    }

反编译之后的结果:

   public static void main(String[] var0) {
      HashMap var1 = new HashMap();
      var1.put("hi", "你好");
      var1.put("how are you?", "最近好吗");
      System.out.println((String)var1.get("hi"));
      System.out.println((String)var1.get("how are you?"));
   }

通过泛型擦除的方式有些缺点,导致一些人对Java语言提供的伪泛型颇有微词,比如运行效率,因为强制转换会有一定的性能损失,暂且不说运行消息影响是否很大,但是有一点对我们开发者来说是很难接受的,就是就遇到重载的问题。
比如下面的代码:

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

对我们开发人员来说,希望这是个重载,毕竟很可能有这种需求,但是这段代码是编译不通过的,所以很尴尬。
还有更尴尬的事情,我们知道方法的重载不能根据返回值来确定的,但是下面的代码在JDK 1.6的环境确实能编译通过,并且能运行,得到我们要的效果,1.7之后已经优化了,无法编译通过。

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

    public static void main(String[] args) {
        GenericTypes types = new GenericTypes();
        types.method(new ArrayList<String>());
        types.method(new ArrayList<Integer>());
    }
}

虽然重载要求方法具备不同的特征签名,而返回值不在这个签名里,所以返回值不会参与重载的选择,但是对在Class文件格式中,只要描述符不是完全一致,就可以共存。

自动装箱和自动拆箱

编译之前的源码:

    public static void main(String[] args) {
        Integer i = 10;
        i = i + 5;
        System.out.println(i);
    }

经过反编译之后的代码:

   public static void main(String[] var0) {
      Integer var1 = Integer.valueOf(10);
      var1 = Integer.valueOf(var1.intValue() + 5);
      System.out.println(var1);
   }

可以看出,包装类型只要涉及到计算的操作,都要经过拆箱Integer.intValue()和装箱操作Integer.valueOf(),所以,如果数字经常变化时,最好使用非包装类型,减少拆箱和装箱操作,提高效率。
另外要说下自动装箱和拆箱是有一定的坑的,使用的时候要注意,举个例子:

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

读者可以先猜测一下答案,然后再上机进行验证一些下。
结果是不是有些出乎意料。
我们反编译一下这段代码,看看最终拆箱装箱之后的结果:

   public static void main(String[] var0) {
      Integer var1 = Integer.valueOf(1);//1
      Integer var2 = Integer.valueOf(2);//2
      Integer var3 = Integer.valueOf(3);//3
      Integer var4 = Integer.valueOf(3);//4
      Integer var5 = Integer.valueOf(321);//5
      Integer var6 = Integer.valueOf(321);//6
      Long var7 = Long.valueOf(3L);
      System.out.println(var3 == var4);//a
      System.out.println(var5 == var6);//b
      System.out.println(var3.intValue() == var1.intValue() + var2.intValue());//c
      System.out.println(var3.equals(Integer.valueOf(var1.intValue() + var2.intValue())));//d
      System.out.println(var7.longValue() == (long)(var1.intValue() + var2.intValue()));//e
      System.out.println(var7.equals(Integer.valueOf(var1.intValue() + var2.intValue())));//f
   }

我们先把结果贴出来,一个一个的分析:

true
false
true
true
true
false

结果b为false很容易理解,==是对引用的比较,var5 == var6不一样很正常的,为什么a会是true呢?答案就是在装箱的方法valueOf()中,我们看下源码:

    public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }

原来和String一样,Integer也有一个cache,如果装箱结果命中了这个cache,直接从缓存池中拿一个对象,而默认情况下,这个缓存池的大小是-128到127,当然这个缓存池的大小可以进行调整,具体请看源码,这里就不具体介绍。
通过c的结果我们可以知道在数字的==前后如果有数字的计算,会进行拆箱操作,使用原始值进行比较,所以c为true。
d就不用说了,类型一样,值一样,肯定为true。
e和c类似,Long和Integer无法进行==,编译器就会做拆箱操作,进行数学层面的比较。
f是不同类型的equals的比较,肯定为true,这个也是equals的设计原则,想了解深的可以看下我的另外一篇文章
好了,装箱和拆箱操作就先讲到这,原则是尽量避免使用自动装箱和拆箱。

条件编译

我们知道C/C++的条件编译是通过#ifdef来实现的,Java的编译器在编译阶段就会处理条件编译,举个例子:

    public static void main(String[] args) {
        if(true){
            System.out.println("true");
        }else {
            System.out.println("false");
        }
    }

反编译之后的代码:

   public static void main(String[] var0) {
      System.out.println("true");
   }

由于条件编译的实现方式使用了if语句,所以它必须遵循最基本的Java语法,只能写在方法体内部,隐藏它只能实现语句的基本块的选择性编译,没办法根据条件调整整个Java类的结构,不管怎样,条件编译能提高运行效率就行。

变长参数

变长参数就是参数个数不确定,但是变长参数有条件的:
1. 变长的那一部分参数一定要具有相同的类型。
2. 变长参数必须位于方法参数列表的最后面。
变长参数同样是Java中的语法糖,其内部实现是Java数组。

    static void printStrs(String... strs){
        for (String str:strs){
            System.out.println(str);
        }
    }
    public static void main(String[] args) {
        printStrs("我", "是", "中", "国", "人");
    }

class文件反编译只有的代码:

   static void printStrs(String ... var0) {
      String[] var1 = var0;
      int var2 = var0.length;

      for(int var3 = 0; var3 < var2; ++var3) {
         String var4 = var1[var3];
         System.out.println(var4);
      }

   }

   public static void main(String[] var0) {
      printStrs(new String[]{"我", "是", "中", "国", "人"});
   }

变长参数让我们少写了一些代码,很方便。

增强的遍历循环

我们上一个例子就使用了普通数组的增强遍历循环,这让我们开发人员可以少些很多代码。
上面的例子,可以看出普通数组的遍历循环最终是普通的for循环。
那么Collection框架下的list或者set是怎么样个语法糖呢?我们举个例子:

        List<String> list = Arrays.asList("我", "是", "中", "国", "人");
        for(String str:list){
            System.out.println(str);
        }

class文件反编译之后的结果:

   public static void main(String[] var0) {
      List var1 = Arrays.asList(new String[]{"我", "是", "中", "国", "人"});
      Iterator var2 = var1.iterator();

      while(var2.hasNext()) {
         String var3 = (String)var2.next();
         System.out.println(var3);
      }

   }

可以看出Collection框架下的遍历循环最终还是使用了遍历器进行遍历。
值得提一下的是,如果我们使用了这个语法糖的遍历循环,就不能在循环体内删除元素,会报异常,这里就不做试验了。

内部类

内部类就是定义在一个类内部的类。

有些时候我们希望一个类只在另一个类中有用,不想让这类在其他地方被使用,这个时候就要用到内部类了,内部类之所以是语法糖,是因为其只是一个编译时的概念,一旦编译完成,编译器就会为内部类生成一个单独的class文件,名为outer$innter.class,比如Mybatis生成器生成的Example文件就使用了静态内部类。

public class Out {
    class Inner{
    }
}

我们看下编译之后的class文件会发现生成了两个class文件,一个是Out.class,一个是Out$Inner.class,内容如下:

public class Out {
}
class Out$Inner {

   // $FF: synthetic field
   final Out this$0;


   Out$Inner(Out var1) {
      this.this$0 = var1;
   }
}

通过注释可以看出是编译器生成的成员,感兴趣的可以看下我的关于反射的系列文章
内部类有四种:成员内部类、局部内部类、匿名内部类、静态内部类,每一种都有各自的用法,这里就不介绍了。

枚举类型

枚举类型可以看做是一组类型一样的常量,当一个变量有几种可能的取值时,可以将它定义为枚举类型。
枚举类型在编译之后会生成一个类,最终还是变为类。

public enum Vehicle{
    CAR, BUS;
}

这个语法糖通过我们上面提到的网址已经做不到反编译了,使用jad 来进行反编译:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3)+
// Source File Name:   Vehicle.java


public final class Vehicle extends Enum
{

    public static Vehicle[] values()
    {
        return (Vehicle[])$VALUES.clone();
    }

    public static Vehicle valueOf(String s)
    {
        return (Vehicle)Enum.valueOf(Vehicle, s);
    }

    private Vehicle(String s, int i)
    {
        super(s, i);
    }

    public static final Vehicle CAR;
    public static final Vehicle BUS;
    private static final Vehicle $VALUES[];

    static+
    {
        CAR = new Vehicle("CAR", 0);
        BUS = new Vehicle("BUS", 1);
        $VALUES = (new Vehicle[] {
            CAR, BUS
        });
    }
}

可以看出在Java的字节码结构中,其实并没有枚举类型,枚举类型只是一个语法糖,在编译完成后被编译成一个普通的类。这个类继承了java.lang.Enum,并被final关键字修饰,是不可被继承的类。

断言

声明一个简单的断言:

    public static void main(String[] args) {
        assert false : "执行错误";
    }

运行时加上参数-ea,执行结果:

Exception in thread "main" java.lang.AssertionError: 执行错误
        at Main.main(Main.java:6)

那么断言是怎么实现的呢?通过jad看下反编译后的结果:

public class Main
{

    public Main()
    {
    }

    public static void main(String args[])
    {
        if(!$assertionsDisabled)
            throw new AssertionError("\u6267\u884C\u9519\u8BEF");
        else
            return;
    }

    static final boolean $assertionsDisabled = !Main.desiredAssertionStatus();

}

最终还是语法糖,有异常时抛出AssertionError。

枚举的switch支持

我们知道switch只支持int型以及编译器能自己转换为int的类型,在老版本中,枚举变量和下面要介绍的字符串是不支持switch语句的,但是1.5以上的却支持,它是怎么做到的呢?我们探究以下,借用上面的枚举类型的java代码,我们实现一个switch语句。

public class Main {
    public static void main(String[] args) {
        Vehicle vehicle = Vehicle.CAR;
        switch (vehicle){
            case BUS:
                System.out.println("BUS");
                break;
            case CAR:
                System.out.println("CAR");
                break;
            default:
                System.out.println("default");

        }
    }
}

class反编译之后的结果:

public class Main
{

    public Main()
    {
    }

    public static void main(String args[])
    {
        Vehicle vehicle = Vehicle.CAR;
        static class _cls1
        {

            static final int $SwitchMap$Vehicle[];

            static+
            {
                $SwitchMap$Vehicle = new int[Vehicle.values().length];
                try
                {
                    $SwitchMap$Vehicle[Vehicle.BUS.ordinal()] = 1;
                }
                catch(NoSuchFieldError nosuchfielderror) { }
                try
                {
                    $SwitchMap$Vehicle[Vehicle.CAR.ordinal()] = 2;
                }
                catch(NoSuchFieldError nosuchfielderror1) { }
            }
        }

        switch(_cls1..SwitchMap.Vehicle[vehicle.ordinal()])
        {
        case 1: // '\001'
            System.out.println("BUS");
            break;

        case 2: // '\002'
            System.out.println("CAR");
            break;

        default:
            System.out.println("default");
            break;
        }
    }
}

通过反编译之后的代码我们就能知道,原来枚举类型的语法糖生成的代码是通过一个数组实现,会把枚举变量的原始值作为下标,存的值是1、2、3…从1到枚举个数的整数值,最终还是使用的int型做switch操作。

字符串的switch支持

    public static void main(String[] args) {
        String str = "bbb";
        switch (str){
            case "aaa":
                System.out.println("a");
                break;
            case "bbb":
                System.out.println("b");
                break;
            case "ccc":
                System.out.println("b");
                break;
            default:
                System.out.println("default");

        }
    }

jad反编译之后的结果:

public class Main
{

    public Main()
    {
    }

    public static void main(String args[])
    {
        String s = "b";
        String s1 = s;
        byte byte0 = -1;
        switch(s1.hashCode())
        {
        case 97: // 'a'
            if(s1.equals("a"))
                byte0 = 0;
            break;

        case 98: // 'b'
            if(s1.equals("b"))
                byte0 = 1;
            break;

        case 99: // 'c'
            if(s1.equals("c"))
                byte0 = 2;
            break;
        }
        switch(byte0)
        {
        case 0: // '\0'
            System.out.println("a");
            break;

        case 1: // '\001'
            System.out.println("b");
            break;

        case 2: // '\002'
            System.out.println("b");
            break;

        default:
            System.out.println("default");
            break;
        }
    }
}

很容易就看明白了字符串(String)的switch的实现逻辑,先通过字符串的hashCode值定位到要case的字符串,再通过equal()确认这个字符串,确认后给一个int型(编译器能自己转换为int的类型)中间变量赋值,这个中间变量的值是从0到case的个数n-1。最终在通过switch这个int型(编译器能自己转换为int的类型)中间变量做switch,本例的中间变量是byte型。

try语句的定义和关闭资源

这种方式我们用的比较少,也介绍一下吧。

public class Main {
    public static void main(String[] args) throws Exception {
        String s = "CYF";
        try (//创建对象输出流
             ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("b.bin"));
             //创建对象输入流
             ObjectInputStream ois = new ObjectInputStream(new FileInputStream("b.bin"));
        )
        {
            //序列化java对象
            oos.writeObject(s);
            oos.flush();
        }
    }
}

反编译之后的结果:

import java.io.*;

public class Main
{

    public Main()
    {
    }

    public static void main(String args[])
        throws Exception
    {
        String s;
        ObjectOutputStream objectoutputstream;
        Throwable throwable;
        s = "CYF";
        objectoutputstream = new ObjectOutputStream(new FileOutputStream("b.bin"));
        throwable = null;
        ObjectInputStream objectinputstream;
        Throwable throwable3;
        objectinputstream = new ObjectInputStream(new FileInputStream("b.bin"));
        throwable3 = null;
        try
        {
            objectoutputstream.writeObject(s);
            objectoutputstream.flush();
        }
        catch(Throwable throwable5)
        {
            throwable3 = throwable5;
            throw throwable5;
        }
        if(objectinputstream != null)
            if(throwable3 != null)
                try
                {
                    objectinputstream.close();
                }
                catch(Throwable throwable4)
                {
                    throwable3.addSuppressed(throwable4);
                }
            else
                objectinputstream.close();
        break MISSING_BLOCK_LABEL_139;
        Exception exception;
        exception;
        if(objectinputstream != null)
            if(throwable3 != null)
                try
                {
                    objectinputstream.close();
                }
                catch(Throwable throwable6)
                {
                    throwable3.addSuppressed(throwable6);
                }
            else
                objectinputstream.close();
        throw exception;
        if(objectoutputstream != null)
            if(throwable != null)
                try
                {
                    objectoutputstream.close();
                }
                catch(Throwable throwable1)
                {
                    throwable.addSuppressed(throwable1);
                }
            else
                objectoutputstream.close();
        break MISSING_BLOCK_LABEL_215;
        Throwable throwable2;
        throwable2;
        throwable = throwable2;
        throw throwable2;
        Exception exception1;
        exception1;
        if(objectoutputstream != null)
            if(throwable != null)
                try
                {
                    objectoutputstream.close();
                }
                catch(Throwable throwable7)
                {
                    throwable.addSuppressed(throwable7);
                }
            else
                objectoutputstream.close();
        throw exception1;
    }
}

反编译之后的代码很长,长的都不想阅读了,简单的来说把try和关闭资源的代码分类,try代码会生成try/catch语句,和一个异常中奖变量,用这个中间变量保存异常信息,在最后关闭资源的代码执行后再抛出这个异常中奖变量保存的异常。

总而言之,语法糖是编译器实现的一些“小把戏”,这些“小把戏”能提升我们的工作效率,但是我也应该要明白这些“小把戏”背后的原理,这样我们才能更好的利用他们,而不是被他们迷惑。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Java语法是指Java语言为了更方便开发者使用而提供的一些特性,这些特性本质上并不会改变Java语言的运行机制,而是通过编译器或运行时库等方式将这些语法转换为Java语言本身能够识别的代码。这些语法的目的是为了简化代码的写法,让代码更易于理解和维护。 一些典型的Java语法包括: - 自动装箱与拆箱:可以在Java代码直接使用基本数据类型,而无需手动创建对应的包装类对象。 - for-each循环:可以直接遍历数组或集合的所有元素,而无需使用下标或迭代器。 - 可变参数列表:可以将一组参数封装为数组传递给方法,而无需手动创建数组。 - Lambda表达式:可以创建简单的匿名函数,而无需定义单独的函数对象。 这些语法都是在编译期间转换为Java语言本身的特性,因此不会对程序的性能造成影响。 ### 回答2: Java语法是指在Java编程语言的一些语法上的改进和简化,它使得代码更加易读、简洁和易于理解。语法不是新增加的语言功能,而是对现有功能的语法上的改良。 一个常见的Java语法是自动装箱和拆箱。在Java 1.5之前,基本类型(如int、float等)和它们对应的包装类(如Integer、Float等)之间不能直接进行赋值或比较操作,需要通过手动装箱和拆箱的方式。但通过语法的改进,现在可以直接在基本类型和对应的包装类之间进行自动转换,使得代码更加简洁和优雅。 另一个例子是增强的for循环。在Java 1.5之前,遍历数组或集合需要使用传统的for循环,并且需要手动获取和指定迭代器。而通过语法的改进,现在可以使用更加简洁的增强的for循环,将原始的方法调用、初始化和变量声明过程都隐藏在背后,使得代码更加易读和简洁。 还有一些其他的语法,如可变参数、枚举类型、Lambda表达式等,它们都是通过简化和优化语法上的表示方式,提高代码的可读性和可维护性。 需要注意的是,尽管语法使得代码更加简洁,但底层执行的逻辑并没有改变。编译器会将语法转换为等价的原始代码,然后再进行编译和执行。所以在阅读和理解代码时,还是需要了解底层的语言特性和实现细节。 ### 回答3: Java语法是一种语法的简化形式,它能够使得代码更加易读易写,并且不会增加程序的运行效率。 在Java语言,有些常见的操作会使用较为繁琐的语法去实现,为了简化这些操作的写法,Java引入了语法语法并不是一种新的特性或者语法规则,而是一种编译器提供的功能,可以将一些常见的代码模式转化为更简洁的语法结构。 常见的Java语法包括自动拆装箱、泛型、枚举类型、增强的for循环以及可变参数等。通过使用这些语法,可以使得代码更加简洁易读,并且减少了一些常见错误的发生。 比如,自动拆装箱允许我们在基本类型和包装类型之间进行自动的转换,不需要手动进行转换操作。使用泛型可以在编译时进行类型检查,避免了类型转换的错误。枚举类型提供了更好的可读性和类型安全性。增强的for循环可以简化对数组和集合的迭代操作。可变参数允许我们以更方便的方式传递不定数量的参数。 尽管语法提供了更加简洁的写法,但是在编译过程,这些语法都会被转化为等价的标准Java代码,所以对于程序的运行效率没有实质的影响。 总的来说,Java语法使得代码更加易读易写,并且减少了一些常见错误的发生,提高了程序的可维护性和开发效率。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值