所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成 和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃 嘛)
注意:以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外, 编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了几乎等价的 java 源码方式,并不是编译器还会转换出中间的 java 源码,切记。
public class Candy1 {
}
public class org.example.classLoading.Candy1
...
Constant pool:
#1 = Methodref #3.#13 // java/lang/Object."":()V
...
{
// 编译器优化:这个无参构造是编译器帮助我们加上的
public org.example.classLoading.Candy1();
...
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 //super();
4: return
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lorg/example/classLoading/Candy1;
}
代码片段1 :显然当前版本太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是 包装类型)
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
代码片段2: 这段代码在 JDK 5 之前是无法编译通过的,JDK 5 开始自动优化
public class Candy2 {
public static void main(String[] args) {
Integer x = 1; //编译器进行优化
int y = x; //编译器进行优化
}
}
泛型是在 JDK 5 开始加入的特性,主要是在编译期间做类型检查。
java 在编译泛型代码后会执行 泛型擦除 的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理:取值时,字节码需做一个强制类型转换
public class Candy3 {
public static void main(String[] args) {
List<Integer> list=new ArrayList<>();
list.add(10);
Integer x=list.get(0);//取值时,字节码需做一个强制类型转换
}
}
泛型是为了在编译期间的类型检查,泛型擦除是为了便于字节码处理
擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable(局部变量类型表) 仍然保留了方法参数泛型的信息
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: new #2
3: dup
4: invokespecial #3
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // 1.Integer.valueOf(10)
14: invokeinterface #5, 2 // 2.注意为Object:List.add(Object)
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // 3.list.get(Object);
27: checkcast #7 // 4.注意强制类型转换:(Integer)list.get(0);
30: astore_2
31: return
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
0 32 0 args [Ljava/lang/String;
8 24 1 list Ljava/util/List;
31 1 2 x Ljava/lang/Integer;
LocalVariableTypeTable:
Start Length Slot Name Signature
8 24 1 list Ljava/util/List<Ljava/lang/Integer;>;
反射泛型
注意:局部遍历的泛型信息无法通过反射获取。只有类结构上的泛型可以通过反射获取
public class Candy3 {
public static void main(String[] args) throws NoSuchMethodException {
Method test = Candy3.class.getMethod("test", List.class, Map.class);
Type[] types = test.getGenericParameterTypes();
for (Type type : types) {
if (type instanceof ParameterizedType){
ParameterizedType parameterizedType= (ParameterizedType) type;
System.out.println("原始类型 - "+parameterizedType.getRawType());
Type[] arguments = parameterizedType.getActualTypeArguments();
for (int i=0;i<arguments.length;i++) {
System.out.printf("泛型参数[%d] - %s\n",i,arguments[i]);
}
}
}
}
public Set<Integer> test(List<String> list,Map<Integer,Object> map){
return new HashSet<>();
}
}
原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object
可变参数也是 JDK 5 开始加入的新特性:
public class Candy4 {
public static void main(String[] args) {
foo("hello", "world");
}
public static void foo(String... args) {
String[] array = args; //可以直接赋值
System.out.println(array);
}
}
可变参数 String… args 其实是一个 String[] args ,从代码中的赋值语句中就可以看出来。
同样 java 编译器会在编译期间将上述代码变换为:
注意:如果调用了 foo() 则等价代码为 foo(new String[]{}) ,创建了一个空的数组,而不会传递 null 进去
public class Candy4 {
public static void main(String[] args) {
foo(new String[]{"hello", "world"}); //编译器进行优化
}
public static void foo(String[] args) {
String[] array = args; // 可以直接赋值
System.out.println(array);
}
}
仍是 JDK 5 开始引入的语法糖。
注意: foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用。其中 Iterable 用来获取集合的迭代器( Iterator )
数组的循环
public class Candy5_1 {
public static void main(String[] args) {
int[] array = {1, 2, 3, 4, 5};
for (int e : array) {
System.out.println(e);
}
}
}
编译器编译后的字节码:
public class Candy5_1 {
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5}; //编译器进行优化
for(int i = 0; i < array.length; ++i) { //编译器进行优化
int e = array[i];
System.out.println(e);
}
}
}
集合的循环
public class Candy5_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1,2,3,4,5);
for (Integer i : list) {
System.out.println(i);
}
}
}
编译器编译后的字节码:迭代器的调用
public class Candy5_2 {
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator iter = list.iterator(); //编译器进行优化
while(iter.hasNext()) { //编译器进行优化
Integer e = (Integer)iter.next();
System.out.println(e);
}
}
}
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
注意:配合 String 和枚举使用时,不能 case “null”。因为 null 不能调用hashCode()
public class Candy6_1 {
public static void choose(String str) {
switch (str) {
case "hello": {
System.out.println("h");
break;
}
case "world": {
System.out.println("w");
break;
}
}
}
}
编译器编译后的字节码:将字符串转换为 byte 类型,再进行比较
public class Candy6_1 {
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 99162322: //"hello".(hashCode):编译器进行优化
if (str.equals("hello")) {
x = 0;
}
break;
case 113318802: //"world".(hashCode):编译器进行优化
if (str.equals("world")) {
x = 1;
}
}
switch(x) {
case 0:
System.out.println("h");
break;
case 1:
System.out.println("w");
}
}
}
switch 枚举的例子,原始代码:
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男"); break;
case FEMALE:
System.out.println("女"); break;
}
}
}
enum Sex {
MALE, FEMALE
}
编译器编译后的字节码:
public class Candy7 {
//编译器进行优化:定义一个合成类,仅 jvm 使用
static class $MAP {
static int[] map = new int[2]; //大小即为枚举元素个数
static {
map[Sex.MALE.ordinal()] = 1; //存储case对应的数字
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
switch (sex.ordinal()) { //编译器进行优化
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
}
}
JDK 7 新增了枚举类,以前面的性别枚举为例:
enum Sex {
MALE, FEMALE
}
编译器编译后的字节码:
final class Sex extends Enum<Sex>{ //编译器进行优化
static{
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values(){
return (Sex[])$VALUES.clone();
}
public static Sex valueOf(String name){
return (Sex)Enum.valueOf(Sex.class,name);
}
}
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources
:
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、 Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如:
public class Candy9 {
public static void main(String[] args) {
try(资源变量 = 创建资源对象){
} catch( ) {
}
}
}
编译器编译后的字节码:
public class Candy9 {
public static void main(String[] args) {
try { //编译器进行优化
InputStream is = new FileInputStream("d:\\1.txt");
Throwable t = null;
try {
System.out.println(is);
} catch (Throwable e1) {
t = e1;
throw e1;
} finally {
if (is != null) {
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
t.addSuppressed(e2); //注意:输出外层套内存的异常
}
} else {
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
我们都知道,方法重写时对返回值分两种情况:
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
public Integer m() { //返回值类型与父类不一致,那怎么说是重写呢?
return 2;
}
}
对于子类,编译器编译后的字节码:
class B extends A {
public Integer m() {
return 2;
}
// 合成方法,仅JVM可见:真正的重写方法
public synthetic bridge Number m() {
return m();
}
}
验证
public class Candy10 {
public static void main(String[] args) {
Method[] declaredMethods = B.class.getDeclaredMethods();
for (Method declaredMethod : declaredMethods) {
System.out.println(declaredMethod); //输出两个m()
}
}
}
#两个m()
public java.lang.Integer org.example.classLoading.candy.B.m()
public java.lang.Number org.example.classLoading.candy.B.m()
源代码:
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
编译器编译后的字节码:
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
引用 局部变量 的匿名内部类,源代码:
注意
为什么匿名内部类引用局部变量时,局部变量必须是 final 的?
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + x);
}
};
}
}
编译器编译后的字节码:
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
// 额外生成的类
final class Candy11$1 implements Runnable {
int val$x;
Candy11$1(int x) {
this.val$x = x;
}
public void run() {
System.out.println("ok:" + this.val$x);
}
}