资讯详情

Java 的这12个语法糖,有点甜

在日常开发中,我们经常使用它等等,我们只觉得很酷,因为这些特点可以帮助我们减少开发工作量;但我们没有仔细研究这些特征的本质,所以这篇文章,cxuan 揭示这些特征背后的真相。

语法糖

在谈话之前,我们需要先了解一下 语法糖 的概念:语法糖(Syntactic sugar),又称糖衣语法,是英国科学家发明的术语。一般来说,使用语法糖可以增加程序可读性,从而减少程序代码出错的机会,真的很香很甜。

语法糖是指在计算机语言中添加的某种语法,。因为 Java 代码需要运行 JVM 中,基本语法结构。这个过程是解语法糖。所以在 Java 真正支持语法糖的是 Java 编译器,真是换汤不换药,万变不离其宗,关灯都一样。

让我们来看看 Java 这些语法糖

泛型

泛型是一种语法糖。 JDK1.5 泛型机制已经引入,但泛型机制本身是通过的类型擦除 来实现,在 JVM 中没有泛型,只有普通类型和普通方法,泛型类的类型参数,在编译时都会被擦除。泛型并没有自己独特的 Class类型。如下代码所示

List<Integer> aList = new ArrayList(); List<String> bList = new ArrayList();  System.out.println(aList.getClass() == bList.getClass()); 

List<Ineger>List<String> 因为,。但是,如果是一个 Integer 放入类型数据 List<String> 或者一个 String 放置类型数据 List<Ineger> 不允许。

如下图所示

图片

无法将一个 Integer 放置类型数据 List<String> 而且不能把一个 String 类型的数据放在 List<Integer> 编译失败也是如此。

自动拆箱和自动装箱

自动拆卸和自动包装是一种语法糖,它指的是包装类型和基本数据类型之间的自动转换。简单地说,包装是自动将基本数据类型转换为包装器类型;拆箱是将包装类型自动转换为基本数据类型。

让我们先了解一下基本数据类型的包装类型

也就是说,上述基本数据类型和包装类型在转换过程中会自动包装/拆卸,如下面的代码

Integer integer = 66; // 自动拆箱  int i1 = integer;   // 自动装箱 

在上面的代码中 integer 对象使用基本数据类型赋值,而基本数据类型 i1 但它给了一个对象类型,通常不能这样做,但编译器允许我们这样做,这实际上是一种语法糖。这种语法糖使我们很容易计算数值。如果没有语法糖,您需要将对象转换为基本数据类型,基本数据类型也需要转换为包装类型,以使用其内置方法,这无疑增加了代码冗余。

那么如何实现自动拆箱和自动装箱呢?

其实这背后的原理是编译器做了优化。将基本类型赋值给包装类其实是调用了包装类的 valueOf() 该方法为基本类型创建了包装类再赋值。

int i1 = Integer.valueOf(1); 

包装赋值的基本类型是调用包装类 xxxValue() 方法在获得基本数据类型后赋值。

Integer i1 = new Integer(1).intValue();  

我们使用 javap -c 对上述自动装箱和自动拆箱进行反编译验证

可见,在 Code 2 处调用 invokestatic 当编译器自动为我们添加它时,它相当于 Integer.valueOf 该方法将基本数据类型转换为包装类型。

在 Code 7 处调用了 invokevirtual 当编译器为我们添加时,它相当于 Integer.intValue() 方法把 Integer 将值转换为基本数据类型。

枚举

我们经常在日常开发中使用它 enumpublic static final ... 这种语法。那么什么时候用呢? enum 或者是 public static final 这种常量呢?好像都可以。

但是在 Java 在字节码结构中,没有枚举类型。

让我们举个例子来看看

public enum School {     STUDENT,     TEACHER; } 

这是一个 School 枚举包括两个字段,一个是 STUDENT ,一个是 TEACHER,没有别的了。

下面我们用 javap 反编译这个 School.class 。反编译完成后的结果如下

从图中可以看出,枚举实际上是继承的 java.lang.Enum 类的 class 。里面的属性 STUDENT 和 TEACHER 本质也就是 public static final 修改字段。这也是编译器的优化,毕竟 STUDENT 要比 public static final School STUDENT 美观简洁多了。

此外,编译器还将为我们生成两种方法,values() 方法和 valueOf 这两种方法都是通过使用编译器为我们添加的方法 values() 方法可以获得一切 Enum 通过属性值 valueOf 该方法用于获取单个属性值。

注意,Enum 的 values() 方法不属于 JDK API 部分,在 Java 没有源码 values() 方法的相关注释。

用法如下

public enum School {      STUDENT("Student"),     TEACHER("Teacher");      private String name;      School(String name){         this.name = name;     }      public String getName() {         return name;     }      public static void main(String[] args) {          System.out.println(School.STUDENT.getName());          School[] values = School.values();         for(School school : values){             System.out.println("name = "  school.getName());         }      } } 

内部类

内部类是 Java 一个小众 我之所以说利基,并不是说内部类别没用,而是说我们在日常开发中很少使用,而是翻看 JDK 发现许多源代码都有内部结构。例如,常见的 ArrayList 有一个源码 Itr 内部类继承 Iterator 类;再比如 HashMap 中间构造了一个 Node 继承于 Map.Entry 来表示 HashMap 每个节点。

Java 之所以在语言中引入内部类,是因为有时候一个类只想在一个类中有用,不想在其他地方使用,也就是隐藏内部细节。

其实内部类也是一个法糖,因为其只是一个编译时的概念,一旦编译完成,编译器就会为内部类生成一个单独的class 文件,名为 outer$innter.class。

下面我们就根据一个示例来验证一下。

public class OuterClass {

    private String label;

    class InnerClass {

        public String linkOuter(){
            return label = "inner";
        }

    }
    public static void main(String[] args) {

        OuterClass outerClass = new OuterClass();
        InnerClass innerClass = outerClass.new InnerClass();
        System.out.println(innerClass.linkOuter());

    }
}

上面这段编译后就会生成两个 class 文件,一个是 OuterClass.class ,一个是 OuterClass$InnerClass.class ,这就表明,外部类可以链接到内部类,内部类可以修改外部类的属性等。

我们来看一下内部类编译后的结果

如上图所示,内部类经过编译后的 linkOuter() 方法会生成一个指向外部类的 this 引用,这个引用就是连接外部类和内部类的引用。

变长参数

变长参数也是一个比较小众的用法,所谓变长参数,就是方法可以接受长度不定确定的参数。一般我们开发不会使用到变长参数,而且变长参数也不推荐使用,它会使我们的程序变的难以处理。但是我们有必要了解一下变长参数的特性。

其基本用法如下

public class VariableArgs {

    public static void printMessage(String... args){
        for(String str : args){
            System.out.println("str = " + str);
        }
    }

    public static void main(String[] args) {
        VariableArgs.printMessage("l","am","cxuan");
    }
}

变长参数也是一种语法糖,那么它是如何实现的呢?我们可以猜测一下其内部应该是由数组构成,否则无法接受多个值,那么我们反编译看一下是不是由数组实现的。

可以看到,printMessage() 的参数就是使用了一个数组来接收,所以千万别被变长参数忽悠了!

变长参数特性是在 JDK 1.5 中引入的,使用变长参数有两个条件,一是变长的那一部分参数具有相同的类型,二是变长参数必须位于方法参数列表的最后面。

增强 for 循环

为什么有了普通的 for 循环后,还要有增强 for 循环呢?想一下,普通 for 循环你不是需要知道遍历次数?每次还需要知道数组的索引是多少,这种写法明显有些繁琐。增强 for 循环与普通 for 循环相比,功能更强并且代码更加简洁,你无需知道遍历的次数和数组的索引即可进行遍历。

增强 for 循环的对象要么是一个数组,要么实现了 Iterable 接口。这个语法糖主要用来对数组或者集合进行遍历,其在循环过程中不能改变集合的大小。

public static void main(String[] args) {
    String[] params = new String[]{"hello","world"};
    //增强for循环对象为数组
    for(String str : params){
        System.out.println(str);
    }

    List<String> lists = Arrays.asList("hello","world");
    //增强for循环对象实现Iterable接口
    for(String str : lists){
        System.out.println(str);
    }
}

经过编译后的 class 文件如下

public static void main(String[] args) {
   String[] params = new String[]{"hello", "world"};
   String[] lists = params;
   int var3 = params.length;
   //数组形式的增强for退化为普通for
   for(int str = 0; str < var3; ++str) {
       String str1 = lists[str];
       System.out.println(str1);
   }

   List var6 = Arrays.asList(new String[]{"hello", "world"});
   Iterator var7 = var6.iterator();
   //实现Iterable接口的增强for使用iterator接口进行遍历
   while(var7.hasNext()) {
       String var8 = (String)var7.next();
       System.out.println(var8);
   }

}

如上代码所示,如果对数组进行增强 for 循环的话,其内部还是对数组进行遍历,只不过语法糖把你忽悠了,让你以一种更简洁的方式编写代码。

而对继承于 Iterator 迭代器进行增强 for 循环遍历的话,相当于是调用了 Iterator 的 hasNext()next() 方法。

Switch 支持字符串和枚举

switch 关键字原生只能支持整数类型。如果 switch 后面是 String 类型的话,编译器会将其转换成 String 的hashCode 的值,所以其实 switch 语法比较的是 String 的 hashCode 。

如下代码所示

public class SwitchCaseTest {

    public static void main(String[] args) {

        String str = "cxuan";
        switch (str){
            case "cuan":
                System.out.println("cuan");
                break;
            case "xuan":
                System.out.println("xuan");
                break;
            case "cxuan":
                System.out.println("cxuan");
                break;
            default:
                break;
        }
    }
}

我们反编译一下,看看我们的猜想是否正确

根据字节码可以看到,进行 switch 的实际是 hashcode 进行判断,然后通过使用 equals 方法进行比较,因为字符串有可能会产生哈希冲突的现象。

条件编译

这个又是让小伙伴们摸不着头脑了,什么是条件编译呢?其实,如果你用过 C 或者 C++ 你就知道可以通过预处理语句来实现条件编译。

那么什么是条件编译呢?

一般情况下,源程序中所有的行都参加编译。但有时希望对其中一部分内容只在满足一定条件下才进行编译,即对一部分内容指定编译条件,这就是 条件编译(conditional compile)

#define DEBUG  
#IFDEF DEBUUG  
  /* 
   code block 1 
   */   
#ELSE  
  /* 
   code block 2 
  */  
#ENDIF  

但是在 Java 中没有预处理和宏定义这些内容,那么我们想实现条件编译,应该怎样做呢?

使用 final + if 的组合就可以实现条件编译了。如下代码所示

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

这段代码会发生什么?我们反编译看一下

我们可以看到,我们明明是使用了 if …else 语句,但是编译器却只为我们编译了 DEBUG = true 的条件,

所以,Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的,编译器不会为我们编译分支为 false 的代码。

断言

你在 Java 中使用过断言作为日常的判断条件吗?

断言:也就是所谓的 assert 关键字,是 jdk 1.4 后加入的新功能。它主要使用在代码开发和测试时期,用于对某些关键数据的判断,如果这个关键数据不是你程序所预期的数据,程序就提出警告或退出。当软件正式发布后,可以取消断言部分的代码。它也是一个语法糖吗?现在我不告诉你,我们先来看一下 assert 如何使用。

//这个成员变量的值可以变,但最终必须还是回到原值5  
static int i = 5;  
public static void main(String[] args) {  
  assert i == 5;  
  System.out.println("如果断言正常,我就被打印");  
} 

如果要开启断言检查,则需要用开关 -enableassertions 或 -ea 来开启。其实断言的底层实现就是 if 判断,如果断言结果为 true,则什么都不做,程序继续执行,如果断言结果为 false,则程序抛出 AssertError 来打断程序的执行。

assert 断言就是通过对布尔标志位进行了一个 if 判断。

try-with-resources

JDK 1.7 开始,java引入了 try-with-resources 声明,将 try-catch-finally 简化为 try-catch,这其实是一种语法糖,在编译时会进行转化为 try-catch-finally 语句。新的声明包含三部分:try-with-resources 声明、try 块、catch 块。它要求在 try-with-resources 声明中定义的变量实现了 AutoCloseable 接口,这样在系统可以自动调用它们的 close 方法,从而替代了 finally 中关闭资源的功能。

如下代码所示

public class TryWithResourcesTest {

    public static void main(String[] args) {
        try(InputStream inputStream = new FileInputStream(new File("xxx"))) {
            inputStream.read();
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

我们可以看一下 try-with-resources 反编译之后的代码

可以看到,生成的 try-with-resources 经过编译后还是使用的 try…catch…finally 语句,只不过这部分工作由编译器替我们做了,这样能让我们的代码更加简洁,从而消除样板代码。

字符串相加

这个想必大家应该都知道,字符串的拼接有两种,如果能够在编译时期确定拼接的结果,那么使用 + 号连接的字符串会被编译器直接优化为相加的结果,如果编译期不能确定拼接的结果,底层会直接使用 StringBuilderappend 进行拼接,如下图所示。

public class StringAppendTest {

    public static void main(String[] args) {
        String s1 = "I am " + "cxuan";
        String s2 = "I am " + new String("cxuan");
        String s3 = "I am ";
        String s4 = "cxuan";
        String s5 = s3 + s4;

    }
}

上面这段代码就包含了两种字符串拼接的结果,我们反编译看一下

首先来看一下 s1 ,s1 因为 = 号右边是两个常量,所以两个字符串拼接会被直接优化成为 I am cxuan。而 s2 由于在堆空间中分配了一个 cxuan 对象,所以 + 号两边进行字符串拼接会直接转换为 StringBuilder ,调用其 append 方法进行拼接,最后再调用 toString() 方法转换成字符串。

而由于 s5 进行拼接的两个对象在编译期不能判定其拼接结果,所以会直接使用 StringBuilder 进行拼接。

关于lambda表达式,有人可能会有质疑,因为网上有人说他并不是语法糖。其实我想纠正下这个说法。

Labmda表达式不是匿名内部类的语法糖,但是他也是一个语法糖。实现方式其实是依赖了几个JVM底层提供的lambda相关api。

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

public static void main(String... args) {
    List<String> strList = ImmutableList.of("a", "b", "博客:c");

    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("a", "b", "博客:c");

    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 和 l a m b d a 1和lambda 1和lambdamain$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)");  
    }  
}  

上面这段代码,有两个重载的函数,因为他们的参数类型不同,一个是List另一个是List,但是,这段代码是编译通不过的。因为我们前面讲过,参数List和List编译之后都被擦除了,变成了一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。

**泛型—— **

泛型的类型参数不能用在Java异常处理的catch语句中。因为异常处理是由JVM在运行时刻来进行的。由于类型信息被擦除,JVM是无法区分两个异常类型MyException和MyException的

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 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循环

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

会抛出ConcurrentModificationException异常。

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

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

前面介绍了12种Java中常用的语法糖。所谓语法糖就是提供给开发人员便于开发的一种语法而已。

但是这种语法只有开发人员认识。要想被执行,需要进行解糖,即转成JVM认识的语法。

当我们把语法糖解糖之后,你就会发现其实我们日常使用的这些方便的语法,其实都是一些其他更简单的语法构成的。

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

标签: 贴片二极管丝印f17006m重载连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台