3.编译期处理
所谓的语法糖,其实就是指java编译器把*.java源码编译为*.class字节码的过程中,自动生成和转换的一些代码,主要为了减轻程序员的负担,算是java编译器给我们的一个额外福利(给糖吃嘛)
注意,以下代码的分析,借助了javap工具,IDEA的反编译功能,IDEA插件jclasslib等工具。另外,编译器转换的结果直接就是class字节码,只是为了便于阅读,给出了几乎等价的java源码方式,并不是编译器还会转换中间的java源码,切记。
3.1默认构造器
public class Candy1{
}
//编译成class后的代码
public class Candy1 {
//这个无参构造是编译器帮助我们加上的
public Candy1() {
//super();//即调用父类Object的无参构造方法
//java/lang/Object."<init>":()v
}
}
3.2 自动拆装箱
- 这个特性是JDK 5开始加入的,代码片段1:
public class Candy2{
public static void main(String[] args){
Integer x = 1;
int y = x;
}
}
- 这段代码在JDK 5 之前是无法编译通过的,必须改写为,代码片段2:
public class Candy2{
public static void main(String[] args){
Integer x = Integer.valueOf(1);
int y = x.intValue();//拆箱
}
}
- 显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因为这些转换的事情在JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为代码片段2
3.3泛型集合取值
- 泛型也是JDK 5 开始加入的特性,但java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就会丢掉,实际的类型都当作Object类型处理(有一些不会被擦除):
import java.util.ArrayList;
import java.util.List;
public class Candy3{
public static void main(String[] args){
List<Integer> list = new ArrayList<>();
list.add(10); // 实际调用的是List.add(Object e);
Integer x = list.get(0); // 实际调用的是 Object obj = List.get(int index);
}
}
- 所以在取值时,编译器真正生成的字节码中,还要额外做一个类型转换的操作
// 需要将 Object 转为 Integer
Integer x = list.get(0);
- 如果前面的 x 变量类型修改为 int 基本类型,那么最终生成字节码为:
// 需要将 Object 转为 Integer,并执行拆箱操作
int = ((Integer)list.get(0)).inValue();
- JDK 5 后就不用我们程序员
上面代码的字节码:
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 // class java/util/ArrayList
3: dup
4: invokespecial #3 // Method java/util/ArrayList."<init>":()V
7: astore_1
8: aload_1
9: bipush 10
11: invokestatic #4 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
22: invokeinterface #6, 2 // InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
27: checkcast #7 // class java/lang/Integer
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;
LocalVariableTypeTaVable:
...
- 擦除的是字节码上的泛型信息,可以看到LocalVariableTable仍然保留了方法参数的泛型信息
- 使用反射,仍然能够获得这些信息
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class Candy4{
public static void main(String[] args) throws NoSuchMethodException {
Method test = Candy4.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[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for (int i = 0; i < actualTypeArguments.length; i++) {
System.out.printf("泛型参数[%d] - %s\n", i, actualTypeArguments[i]);
}
}
}
}
public static Set<Integer> test(List<String> list, Map<Integer, String> 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.String
3.4可变参数
- 可变参数也是JDK 5 开始加入的新特性
例如:
public class Candy5{
public static void foo(String... args){
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("Hello", "World");
}
}
- 可变参数String… args 其实是一个String[] args,从代码中的赋值语句就可以看出来。
- 同样java编译器会在编译期间将上述代码变换为:
public class Candy5{
public static void foo(String[] args){
String[] array = args; // 直接赋值
System.out.println(array);
}
public static void main(String[] args) {
foo("Hello", "World");
}
}
- 两中方法的字节码(可以发现是一样的)
public static void foo(java.lang.String...);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: astore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: aload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: return
LineNumberTable:
...
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
2 8 1 array [Ljava/lang/String;
=====================================
public static void foo(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: aload_0
1: astore_1
2: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
5: aload_1
6: invokevirtual #3 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
9: return
LineNumberTable:
...
LocalVariableTable:
Start Length Slot Name Signature
0 10 0 args [Ljava/lang/String;
2 8 1 array [Ljava/lang/String;
===================注意====================
如果调用了foo()则等价代码为foo(new String[]),创建了一个空的数组,而不会传递null进去
3.5foreach循环
- 依然是JDK 5 开始引入的语法糖,数组循环:
public class Candy6{
public static void main(String[] args){
int[] array = {1, 2, 3, 4, 5}; // 这种初始化写法也是语法糖
for (int i : array) {
System.out.println(i);
}
}
}
- 会被编译器转换为
public class Candy6 {
public Candy6() {
}
public static void main(String[] args) {
int[] array = new int[]{1, 2, 3, 4, 5};
int[] var2 = array;
int var3 = array.length;
for(int var4 = 0; var4 < var3; ++var4) {
int i = var2[var4];
System.out.println(i);
}
}
}
- 集合:
import java.util.Arrays;
import java.util.List;
public class Candy7{
public static void main(String[] args){
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
for (int i : list) {
System.out.println(i);
}
}
}
- 编译为 迭代器调用
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
public class Candy7 {
public Candy7() {
}
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
Iterator var2 = list.iterator();
while(var2.hasNext()) {
int i = (Integer)var2.next();
System.out.println(i);
}
}
}
注意
foreach循环写法,能够配合数组以及所有实现了Iterator接口的集合类一起使用,其中Iterator用来获取集合的迭代器(Iterator)
3.6 switch 字符串
- 从JDK 7 开始,switch可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
public class Candy8{
public static void choose(String str){
switch (str){
case "Hello":{
System.out.println("H");
break;
}
case "World":{
System.out.println("W");
break;
}
}
}
}
注意
switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚
- 编译为:
public class Candy8 {
public Candy8() {
}
public static void choose(String str) {
byte var2 = -1;
switch(str.hashCode()) {
case 69609650: // Hello 的 hashCode
if (str.equals("Hello")) {
var2 = 0;
}
break;
case 83766130: // World 的 hashCode
if (str.equals("World")) {
var2 = 1;
}
}
switch(var2) {
case 0:
System.out.println("H");
break;
case 1:
System.out.println("W");
}
}
}
===================================================
可以看出,执行了两遍switch,第一遍是根据字符串的 hashCode 和 equals 将字符串转换为相应的byte类型,第二遍才是利用byte执行进行比较
为什么第一遍时必须比较 hashCode ,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较,而 equals 是为了解决 hashCode 冲突,例如 BM 和 C. ,这两个字符串的 hashCode 值都是 2123,例如:
===================================================
public class Candy9{
public static void choose(String str){
switch (str){
case "C.":{
System.out.println("C");
break;
}
case "BM":{
System.out.println("BW");
break;
}
}
}
}
=========================编译为=============================
public class Candy9 {
public Candy9() {
}
public static void choose(String str) {
byte var2 = -1;
switch(str.hashCode()) {
case 2123: // hashCode 冲突
if (str.equals("BM")) {
var2 = 1;
} else if (str.equals("C.")) {
var2 = 0;
}
default:
switch(var2) {
case 0:
System.out.println("C");
break;
case 1:
System.out.println("BW");
}
}
}
}
3.7 switch 枚举
- switch枚举的例子,原始代码:
enum Sex {
MALE,
FEMALE;
}
public class Candy10 {
public static void sexChoose(Sex sex) {
switch (sex) {
case MALE:
System.out.println("MALE");
break;
case FEMALE:
System.out.println("FEMALE");
break;
}
}
}
=============编译后========================
public class Candy10 {
public Candy10() {
}
public static void sexChoose(Sex sex) {
switch(sex) {
case MALE:
System.out.println("MALE");
break;
case FEMALE:
System.out.println("FEMALE");
}
}
}
- 这里有另一个编译的版本你,但是我编译后没出现,仅供参考
3.8 枚举
- JDK 7 新增了枚举类,以前面的性别枚举为例:
enum Sex {
MALE,
FEMALE;
}
==========编译后==============
enum Sex {
MALE,
FEMALE;
private Sex() {
}
}
- 另一个版本,仅供参考
3.9 try-with-resources
- JDK 7 开始新增了对需要关闭资源处理的特殊语法 try-with-resources:
try(资源变量 = 创建资源对象){
}catch(){
}
- 其中资源对象需要实现 AutoCloseable 接口,例如 InputStream、OutputStream、Connection、Statnment、ResultSet等接口都实现了,使用try-with-resources可以不用写finally语句块,编译器会帮助生成关闭资源代码,例如:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class Candy11 {
public static void main(String[] args) throws FileNotFoundException {
try(InputStream is = new FileInputStream("./1.txt")){
System.out.println(is);
}catch (IOException e){
e.printStackTrace();
}
}
}
=================编译后=================================
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
public class Candy11 {
public Candy11() {
}
public static void main(String[] args) throws FileNotFoundException {
try {
InputStream is = new FileInputStream("./1.txt");
Throwable var2 = null;
try {
System.out.println(is);
} catch (Throwable var12) {
// var2 是我们代码出现的异常
var2 = var12;
throw var12;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果代码有异常
if (var2 != null) {
try {
is.close(); // 关闭
} catch (Throwable var11) {
// 如果 close 出现了异常,作为被压制异常添加
var2.addSuppressed(var11);
}
} else {
// 如果我们代码没有异常,close 出现了异常就是最后的 catch 块中的 e
is.close(); // 关闭
}
}
}
} catch (IOException var14) {
var14.printStackTrace();
}
}
}
- 为什么要设计一个 addSuppressed(Exception e);(添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources生成的finally中如果抛出了异常):
public class Candy12 {
public static void main(String[] args) {
try (MyResource myResource = new MyResource()){
int i = 1/0;
}catch (Exception e){
e.printStackTrace();
}
}
}
class MyResource implements AutoCloseable{
@Override
public void close() throws Exception {
throw new Exception("close 异常");
}
}
===============运行输出=====================
java.lang.ArithmeticException: / by zero
at Candy12.main(Candy12.java:5)
Suppressed: java.lang.Exception: close 异常
at MyResource.close(Candy12.java:16)
at Candy12.main(Candy12.java:6)
============================================
如以上代码所示,两个异常信息都不会丢失
3.10 方法重写的桥接方法
- 我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 字类返回值可以是父类返回值的字类(比较绕口,见下面例子)
class A {
public Number m(){
return 1;
}
}
class B extends A {
// 子类 m 方法的返回值 Integer 是 Number 的子类
@Override
public Integer m() {
return 2;
}
}
==============编译后=======================
3.11 匿名内部类
- 代码片段
public class Candy13 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
===============编译后====================
public class Candy13 {
public Candy13() {
}
public static void main(String[] args) {
Runnable var10000 = new Runnable() {
public void run() {
System.out.println("ok");
}
};
}
}
- 另一个版本
- 引用局部变量的匿名内部类,代码片段
public class Candy14 {
public static void test(final int x){
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok" + x);
}
};
}
}
============编译后======================
public class Candy14 {
public Candy14() {
}
public static void test(final int x) {
Runnable var10000 = new Runnable() {
public void run() {
System.out.println("ok" + x);
}
};
}
}
- 另一个版本