第十一章 java重要知识点(Java语法糖)

  • Post author:
  • Post category:java




语法糖



定义

语法糖也称为糖衣语法,指的是在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但更方便程序员使用。总结:语法糖让程序更加简洁,更高的可读性。



常见的语法糖

语法糖的存在主要是方便开发人员使用。但是JVM不支持这些语法糖。在编译阶段会被还原成简单的基础语法结构,这个过程是解语法糖。

javac命令可以把后缀名为.java的源文件编译为后缀名为.class的可在JVM运行的字节码。

举例:

com.sun.tools.javac.main.JavaCompiler的源码,你会发现在compile()中有一个步骤就是调用desugar(),这个方法就是负责解语法糖的实现的。



switch支持String和枚举

Java7中switch开始支持String。switch自身原本就支持基本类型。对于编译器来说,switch中只能使用整型,任何类型的比较都要转换成整型。而String:

public class switchDemoString {
    public static void main(String[] args) {
        String str = "world";
        switch (str) {
        case "hello":
            System.out.println("hello");
            break;
        case "world":
            System.out.println("world");
            break;
        default:
            break;
        }
    }
}

反编译后:

public class switchDemoString
{
    public switchDemoString()
    {
    }
    public static void main(String args[])
    {
        String str = "world";
        String s;
        switch((s = str).hashCode())
        {
        default:
            break;
        case 99162322:
            if(s.equals("hello"))
                System.out.println("hello");
            break;
        case 113318802:
            if(s.equals("world"))
                System.out.println("world");
            break;
        }
    }
}

可以看出字符串的switch是通过equals()和hashCode()来实现的。hashCode()返回的是int,而不是long

进行switch的实际是哈希值,然后

通过equals方法比较进行安全检查,这个检查是必要的,因为哈希可能发生碰撞



泛型

不同编译器对泛型的处理方式不同,通常情况下,一个编译器处理泛型有两种方式:Code specialization和Code sharing。

C++和C#是使用Code specialization的处理机制

Java是Code sharing的机制

Code sharing方式:每个泛型类型创建唯一的字节码表示,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类型实例映射到唯一的字节码表示是通过类型擦除实现的。


对于JVM来说,不认识泛型的语法。需要在编译阶段通过类型擦除的方式进行解语法糖



类型擦除

  1. 将所有的泛型参数用其最左边界(最顶级的父类型)类型替换
  2. 移除所有的类型参数
Map<String, String> map = new HashMap<String, String>();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");

解语法糖:

Map map = new HashMap();
map.put("name", "hollis");
map.put("wechat", "Hollis");
map.put("blog", "www.hollischuang.com");

结论:

虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,泛型类没有自己独有的Class类对象。



自动装箱和拆箱

自动装箱:Java自动将原始类型值转换成对应的对象

自动拆箱:例如将Integer对象转换成int类型值

这里的装箱和拆箱是自动进行的非人为转换,所以称为自动装箱和拆箱。

原始类型 byte, short, char, int, long, float, double 和 boolean

对应的封装类为 Byte, Short, Character, Integer, Long, Float, Double, Boolean。


所以,装箱过程是通过调用包装器的 valueOf 方法实现的,而拆箱过程是通过调用包装器的 xxxValue 方法实现的。



可变长参数

是在Java1.5引入的一个特性。允许一个方法把任意数量的值作为参数。

public static void main(String[] args)
    {
        print("1", "12", "123", "1234");
    }

public static void print(String... strs)
{
    for (int i = 0; i < strs.length; i++)
    {
        System.out.println(strs[i]);
    }
}

反编译后:

 public static void main(String args[])
{
    print(new String[] {
        "1", "12", "123", "1234"
    });
}

public static transient void print(String strs[])
{
    for(int i = 0; i < strs.length; i++)
        System.out.println(strs[i]);
}

结论:

可变参数在被使用时,首先创建一个数组,数组的长度就是调用该方法时,传递的实参的个数,然后再把参数值全部放到这个数组当中,把这个数组作为参数传递到被调用的方法中。



枚举

Java SE5提供了一种新的类型,Java枚举类型,关键字enum可以将一组具名的值的有限集合创建为一种新的类型,这些具名的值可以作为常规的程序组件使用。

举例:

public enum t {
    SPRING,SUMMER;
}

反编译:

public final class T extends Enum
{
    private T(String s, int i)
    {
        super(s, i);
    }
    public static T[] values()
    {
        T at[];
        int i;
        T at1[];
        System.arraycopy(at = ENUM$VALUES, 0, at1 = 
        new T[i = at.length], 0, i);
        return at1;
    }

    public static T valueOf(String s)
    {
        return (T)Enum.valueOf(demo/T, s);
    }

    public static final T SPRING;
    public static final T SUMMER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER
        });
    }
}

结论:

使用enum来定义一个枚举类型,编译器会自动创建一个final类型的类继承Enum类,所以枚举类型不能被继承。



内部类

又称嵌套类,解释为:外部类的一个普通成员。

这个是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同



条件编译

如果出于对程序代码优化的考虑,只对其中一部分内容进行编译,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃。

C和C++通过预处理来实现条件编译。

Java实现条件编译如下:

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

        final boolean ONLINE = false;

        if(ONLINE){
            System.out.println("Hello, ONLINE!");
        }
    }
}

反编译:

public class ConditionalCompilation
{

    public ConditionalCompilation()
    {
    }

    public static void main(String args[])
    {
        boolean DEBUG = true;
        System.out.println("Hello, DEBUG!");
        boolean ONLINE = false;
    }
}
//当if(ONLINE)false 的时候,编译器就没有对其内的代码进行编译。

结论:

Java语法的条件编译,是**通过判断条件为常量的if语句实现的,根据if判断条件的真假,编译器直接把分支为false的代码块消除。**通过该方式实现的条件编译,必须在方法体内实现,而无法在Java类的结构,或者类的属性上进行条件编译,与C/C++的条件编译相比更有局限性



断言

assert关键字是从 JAVA SE 1.4 引入的,为了避免和老版本的 Java 代码中使用了assert关键字导致错误,Java 在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关-enableassertions或-ea来开启


断言的底层实现的是if语言,如果断言结果为true,则什么都不做,程序继续执行,如果为false,则程序抛出AssertError来打断程序的执行。


-enableassertions会设置$assertionsDisabled 字段的值

关闭断言:-da



数值字面量

不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,目的是为了方便阅读。


编译器不认识在数字字面量中的_,在编译阶段会把它去掉



for-each

这个叫做

增强for循环




实现原理是使用了普通的for循环和迭代器


使用:

for (String s : strs) —-String[] strs

for (String s : strList) —–List< String > strList

反编译:

for(int j = 0; j < i; j++)

for(Iterator iterator = strList.iterator(); iterator.hasNext(); System.out.println(s))



try-with-resource

Java对文件操作IO流,数据库连接等开销是非常昂贵的资源,用完后需要通过close方法将其关闭,否则资源会一致处于打开状态,会出现内存泄漏的问题。


一般关闭资源是在finally块中释放,在finally中调用close方法

如下:

public static void main(String[] args) {
    BufferedReader br = null;
    try {
        String line;
        br = new BufferedReader(new 
        FileReader("d:\\hollischuang.xml"));
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        // handle exception
    } finally {
        try {
            if (br != null) {
                br.close();
            }
        } catch (IOException ex) {
            // handle exception
        }
    }
}

从 Java 7 开始,jdk 提供了一种更好的方式关闭资源,使用try-with-resources语句,改写一下上面的代码,效果如下:

public static void main(String... args) {
    try (BufferedReader br = new BufferedReader(new 
    FileReader("d:\\ hollischuang.xml"))) {
        String line;
        while ((line = br.readLine()) != null) {
            System.out.println(line);
        }
    } catch (IOException e) {
        // handle exception
    }
}



Lambda表达式

该表达式不是匿名内部类的语法糖,但也是一个语法糖。

实现方式:依赖几个JVM底层提供的lambda相关api

用lambda遍历一个 list:

public static void main(String... args) {
    List<String> strList = ImmutableList.of("1", "2", "3");

    strList.forEach( s -> { System.out.println(s); } );
}

反编译:

public static /* varargs */ void main(String ... args) {
    ImmutableList strList = ImmutableList.of("1", "2", "3");
    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);
}


解释不是内部类的语法糖:


内部类会在编译后产生两个class文件,但是包含lambda表达式的类编译后只有一个文件

在上述forEach方法中,调用了java.lang.invoke.LambdaMetafactory#metafactory方法,该方法的第五个参数 implMethod 指定了方法实现。可以看到这里其实是调用了一个lambda$main$0方法进行了输出。


在编译阶段,编译器会把 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)");
    }
}

这段代码重载了method方法,但是编译不通过。因为参数泛型会在编译后被擦除,变成一样的原生类型List,擦除动作导致这两个方法的特征签名变得一模一样。



泛型遇到catch

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



泛型内包含了静态变量

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.只适用于自动装箱,使用构造函数创建对象不适用。其他情况需要用equals()



增强for循环

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


会抛出ConcurrentModificationException异常。


Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,

当原来的对象数量发生变化时,这个索引表的内容不会同步改变

,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照

fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常

。所以

Iterator 在工作的时候是不允许被迭代的对象被改变的

。但你可以使用 Iterator 本身的方法remove()来删除对象,

Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性



版权声明:本文为Re_view原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。