文章目录
Java语法糖
所谓的 语法糖 ,其实就是指 java 编译器把 *.java 源码编译为 *.class 字节码的过程中,自动生成
和转换的一些代码,主要是为了减轻程序员的负担,算是 java 编译器给我们的一个额外福利(给糖吃嘛)
注意,以下代码的分析,借助了 javap 工具,idea 的反编译功能,idea 插件 jclasslib 等工具。另外,
编译器转换的结果直接就是 class 字节码,只是为了便于阅读,给出了 几乎等价 的 java 源码方式,并
不是编译器还会转换出中间的 java 源码,切记。
默认构造器
public class Candy1 {
}
public class Candy1 {
// 这个无参构造是编译器帮助我们加上的
public Candy1() {
super(); // 即调用父类 Object 的无参构造方法,即调用 java/lang/Object."<init>":()V
}
}
- 当我们写的类没有写构造器,那么我们前端编译器在转换成字节码的时候,会自动生成一个无参构造方法,且会默认调用父类的无参构造方法
自动拆装箱
这个特性是在JDK1.5添加的
public class Candy2 {
public static void main(String[] args) {
Integer x = 1;
int y = x;
}
}
在JDK1.5之前上面这种代码是无法通过编译的
public class Candy2 {
public static void main(String[] args) {
Integer x = Integer.valueOf(1);
int y = x.intValue();
}
}
显然之前版本的代码太麻烦了,需要在基本类型和包装类型之间来回转换(尤其是集合类中操作的都是包装类型),因此这些转换的事情在 JDK 5 以后都由编译器在编译阶段完成。即 代码片段1 都会在编译阶段被转换为 代码片段2
泛型
当没有引入泛型的时候
public class Candy3 {
public static void main(String[] args) {
Object [] array=new Object[10];
array[0]=10; //编译器不会有问题
ArrayList arrayList=new ArrayList();
arrayList.add(Integer.valueOf(10)); //编译器和运行时都不会有问题
arrayList.add("hello");
}
}
我们知道在我们的Java中,有一个二进制向后兼容,也就是比如一个在JDK1.2中编译出来的Class文件,必须保证在JDK12乃至之后的版本也能正常运行
为了能正常运行,所以有两条路走
- 需要泛型化的类型(主要是容器类型),以前有的就保持不变,然后平行地加一套泛型化版本的新类型
- 直接把已有类型泛型化,即让所有的需要泛型化的已有类型都原地泛型化,不添加任何平行于已有类型的泛型版
我们的Java采用的是第二种
我们继续以ArrayList为例来介绍Java泛型的类型擦除具体是如何实现的。由于Java选择了第二条路,直接把已有的类型泛型化。要让所有需要泛型化的已有类型,譬如ArrayList,原地泛型化后变成了ArrayList, 而且保证以前直接用ArrayList的代码在泛型新版本里必须还能继续用这同一个容器,这就必须让所有泛型化的实例类型,譬如ArrayList、ArrayLists 这些全部自动成为ArayList的子类型才能可以,否则类型转换就是不安全的。由此就引出了“裸类型” (Raw Type)的概念,裸类型应被视为所有该类型泛型化实例的共同父类型(Super Type), 只有这样,像代码清单10-4中的赋值才是被系统允许的从子类到父类的安全转型。
- 在这我们就应该想起来一个类型就是Object,我们的集合存储的是我们的对象的引用,对于引用类型存储空间都是一样的,而且我们的泛型是不支持基本数据类型的
- 也是因为这种实现,我们的泛型不支持基本数据类型,所以出现大量的拆箱和装箱操作,导致运行速度变慢
- 也因为类型擦除,导致我们运行期间无法获得类型的信息(反射除外)
ArrayList<Integer> ilist = new ArayList<Integers);
ArylListkstring slist.二new Aryisistrin>Lni
ArrayList list; //裸类型
list = ilist;
list = slist;
- 泛型的本质是参数化类型或者参数化多态的应用,也就是可以将操作的数据类型指定为方法签名中的一种特殊参数,这种参数类型能够用在类,接口和方法的创建中
- 泛型也是在JDK5开始加入的特性,但是java在编译泛型代码后会执行泛型擦除的动作,即泛型信息在编译为字节码之后就丢失了,实际的类型都当做了 Object 类型来处理
泛型擦除
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 = (Integer)list.get(0);
如果前面的 x 变量类型修改为 int 基本类型那么最终生成的字节码是:
// 需要将 Object 转为 Integer, 并执行拆箱操作
int x = ((Integer)list.get(0)).intValue();
验证一下
Classfile /E:/JAVA代码/JVMDemo/out/production/chapter01/com/atguigu/java/Candy3.class
Last modified 2022-12-1; size 791 bytes
MD5 checksum 2c1fe36d6e75a1c8176e8d87343351e4
Compiled from "Candy3.java"
public class com.atguigu.java.Candy3
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #9.#29 // java/lang/Object."<init>":()V
#2 = Class #30 // java/util/ArrayList
#3 = Methodref #2.#29 // java/util/ArrayList."<init>":()V
#4 = Methodref #7.#31 // java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
#5 = InterfaceMethodref #32.#33 // java/util/List.add:(Ljava/lang/Object;)Z
#6 = InterfaceMethodref #32.#34 // java/util/List.get:(I)Ljava/lang/Object;
#7 = Class #35 // java/lang/Integer
#8 = Class #36 // com/atguigu/java/Candy3
#9 = Class #37 // java/lang/Object
#10 = Utf8 <init>
#11 = Utf8 ()V
#12 = Utf8 Code
#13 = Utf8 LineNumberTable
#14 = Utf8 LocalVariableTable
#15 = Utf8 this
#16 = Utf8 Lcom/atguigu/java/Candy3;
#17 = Utf8 main
#18 = Utf8 ([Ljava/lang/String;)V
#19 = Utf8 args
#20 = Utf8 [Ljava/lang/String;
#21 = Utf8 list
#22 = Utf8 Ljava/util/List;
#23 = Utf8 x
#24 = Utf8 Ljava/lang/Integer;
#25 = Utf8 LocalVariableTypeTable
#26 = Utf8 Ljava/util/List<Ljava/lang/Integer;>;
#27 = Utf8 SourceFile
#28 = Utf8 Candy3.java
#29 = NameAndType #10:#11 // "<init>":()V
#30 = Utf8 java/util/ArrayList
#31 = NameAndType #38:#39 // valueOf:(I)Ljava/lang/Integer;
#32 = Class #40 // java/util/List
#33 = NameAndType #41:#42 // add:(Ljava/lang/Object;)Z
#34 = NameAndType #43:#44 // get:(I)Ljava/lang/Object;
#35 = Utf8 java/lang/Integer
#36 = Utf8 com/atguigu/java/Candy3
#37 = Utf8 java/lang/Object
#38 = Utf8 valueOf
#39 = Utf8 (I)Ljava/lang/Integer;
#40 = Utf8 java/util/List
#41 = Utf8 add
#42 = Utf8 (Ljava/lang/Object;)Z
#43 = Utf8 get
#44 = Utf8 (I)Ljava/lang/Object;
{
public com.atguigu.java.Candy3();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 6: 0
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/atguigu/java/Candy3;
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;
//注意点 调用的方法对应的参数是Object
14: invokeinterface #5, 2 // InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
19: pop
20: aload_1
21: iconst_0
//注意点 调用的方法对应返回的参数是Object
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:
line 8: 0
line 9: 8
line 10: 20
line 11: 31
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;>;
}
SourceFile: "Candy3.java"
- 擦除的是字节码上的泛型信息,可以看到 LocalVariableTypeTable 仍然保留了方法参数泛型的信息
- 我们的字节码调用相应的方法都是Object的参数
泛型导致无法重载
正是由于类型擦除的隐蔽存在,直接导致了众多的泛型灵异问题,如当泛型遇见重载
public class GenericTypes {
public static void method(List<String> list) {
System.out.println("List<String> list");
}
public static void method(List<Integer> list) {
System.out.println("List<Integer> list");
}
}
这段代码将无法进行编译,因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型List<E>,擦除动作导致这两种方法的特征签名变得一模一样。初步看来,无法重载的原因已经找到了,但真的就是如此吗?只能说,泛型擦除成相同的原生类型只是无法重载的其中一部分原因,请再接着如下代码
public class GenericTypes {
public static String method(List<String> list) {
System.out.println("invoke method List<String> list");
return "";
}
public static int method(List<Integer> list) {
System.out.println("invoke method List<Integer> list");
return 1;
}
public static void main(String[] args) {
method(new ArrayList<String>());
method(new ArrayList<Integer>());
}
}
执行结果
invoke method List<String> list
invoke method List<Integer> list
方法重载要求方法具备不同的特征签名,返回值并不包含在方法的特征签名之中,所以返回值不参与重载选择,但是在 Class 文件格式之中,只要描述符不是完全一致的两个方法就可以共存。也就是说,两个方法如果有相同的名称和特征签名,但返回值不同,那它们也是可以合法地共存于一个 Class 文件中的。
由于 List<String>和 List<Integer>擦除后是同一个类型,我们只能添加两个并不需要实际使用到的返回值才能完成重载。
擦除法所谓的擦除,仅仅是对方法的 Code 属性中的字节码进行擦除,实际上元数据中还是保留了泛型信息,这也是我们能通过反射手段取得参数化类型的根本依据。
foreach 循环
仍是 JDK 5 开始引入的语法糖,数组的循环
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 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 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);
}
}
}
注意
foreach 循环写法,能够配合数组,以及所有实现了 Iterable 接口的集合类一起使用,其中
Iterable 用来获取集合的迭代器( Iterator )
switch字符串
从 JDK 7 开始,switch 可以作用于字符串和枚举类,这个功能其实也是语法糖,例如:
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;
}
}
}
}
注意
switch 配合 String 和枚举使用时,变量不能为null,原因分析完语法糖转换后的代码应当自然清楚
public class Candy6_1 {
public Candy6_1() {
}
public static void choose(String str) {
byte x = -1;
switch(str.hashCode()) {
case 99162322: // hello 的 hashCode
if (str.equals("hello")) {
byte 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,第一遍是根据字符串的 hashCode 和 equals 将字符串的转换为相应byte 类型,第二遍才是利用 byte 执行进行比较。
- 为什么第一遍时必须既比较 hashCode,又利用 equals 比较呢?hashCode 是为了提高效率,减少可能的比较;而 equals 是为了防止 hashCode 冲突,例如 BM 和 C. 这两个字符串的hashCode值都是2123 ,如果有如下代码:
switch 枚举
enum Sex {
MALE, FEMALE
}
public class Candy7 {
public static void foo(Sex sex) {
switch (sex) {
case MALE:
System.out.println("男"); break;
case FEMALE:
System.out.println("女"); break;
}
}
}
编译后的代码
public class Candy7 {
/**
* 定义一个合成类(仅 jvm 使用,对我们不可见)
* 用来映射枚举的 ordinal 与数组元素的关系
* 枚举的 ordinal 表示枚举对象的序号,从 0 开始
* 即 MALE 的 ordinal()=0,FEMALE 的 ordinal()=1
*/
static class $MAP {
// 数组大小即为枚举元素个数,里面存储case用来对比的数字
static int[] map = new int[2];
static {
map[Sex.MALE.ordinal()] = 1;
map[Sex.FEMALE.ordinal()] = 2;
}
}
public static void foo(Sex sex) {
int x = $MAP.map[sex.ordinal()];
switch (x) {
case 1:
System.out.println("男");
break;
case 2:
System.out.println("女");
break;
}
枚举类的编译处理
enum Sex {
MALE, FEMALE
}
编译后的代码
JDK 7 新增了枚举类,以前面的性别枚举为例
public final class Sex extends Enum<Sex> {
public static final Sex MALE;
public static final Sex FEMALE;
private static final Sex[] $VALUES;
static {
MALE = new Sex("MALE", 0);
FEMALE = new Sex("FEMALE", 1);
$VALUES = new Sex[]{MALE, FEMALE};
}
/**
* Sole constructor. Programmers cannot invoke this constructor.
* It is for use by code emitted by the compiler in response to
* enum type declarations.
* *
@param name - The name of this enum constant, which is the identifier
* used to declare it.
* @param ordinal - The ordinal of this enumeration constant (its position
* in the enum declaration, where the initial constant is
assigned
*/
private Sex(String name, int ordinal) {
super(name, ordinal);
}
public static Sex[] values() {
return $VALUES.clone();
}
public static Sex valueOf(String name) {
return Enum.valueOf(Sex.class, name);
}
}
- 枚举本质也是一个class,跟普通类的最大区别,就是它的实例对象是有限的,比如我们的性别类就两个实例
- 构造方法是私有的,防止被使用者来创建实例对象
try-with-resources
JDK 7 开始新增了对需要关闭的资源处理的特殊语法 try-with-resources`
try(资源变量 = 创建资源对象){
}
catch( ) {
}
其中资源对象需要实现 AutoCloseable 接口,例如 InputStream 、 OutputStream 、Connection 、 Statement 、 ResultSet 等接口都实现了 AutoCloseable ,使用 try-withresources 可以不用写 finally 语句块,编译器会帮助生成关闭资源代码,例如
public class Candy9 {
public static void main(String[] args) {
try(InputStream is = new FileInputStream("d:\\1.txt")) {
System.out.println(is);
} catch (IOException e) {
e.printStackTrace();
}
}
}
会被转换成
public class Candy9 {
public 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 是我们代码出现的异常
t = e1;
throw e1;
} finally {
// 判断了资源不为空
if (is != null) {
// 如果我们代码有异常
if (t != null) {
try {
is.close();
} catch (Throwable e2) {
// 如果 close 出现异常,作为被压制异常添加
t.addSuppressed(e2);
}
} else {
// 如果我们代码没有异常,close 出现的异常就是最后 catch 块中的 e
is.close();
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
为什么要设计一个 addSuppressed(Throwable e) (添加被压制异常)的方法呢?是为了防止异常信息的丢失(想想 try-with-resources 生成的 fianlly 中如果抛出了异常)
方法重写时的桥接方法
我们都知道,方法重写时对返回值分两种情况:
- 父子类的返回值完全一致
- 子类返回值可以是父类返回值的子类(比较绕口,见下面的例子)
class A {
public Number m() {
return 1;
}
}
class B extends A {
@Override
// 子类 m 方法的返回值是 Integer 是父类 m 方法返回值 Number 的子类
public Integer m() {
return 2;
}
}
对于子类,java 编译器会做如下处理:
class B extends A {
public Integer m() {
return 2;
}
// 此方法才是真正重写了父类 public Number m() 方法
public synthetic bridge Number m() {
// 调用 public Integer m()
return m();//向上转型是可以通过的
}
}
其中桥接方法比较特殊,仅对 java 虚拟机可见,并且与原来的 public Integer m() 没有命名冲突
匿名内部类
这里是内部类的语法糖,只是挑选了典型的匿名内部类
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok");
}
};
}
}
// 额外生成的类
final class Candy11$1 implements Runnable {
Candy11$1() {
}
public void run() {
System.out.println("ok");
}
}
//外部类的代码
public class Candy11 {
public static void main(String[] args) {
Runnable runnable = new Candy11$1();
}
}
引入局部变量的匿名内部类
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Runnable() {
@Override
public void run() {
System.out.println("ok:" + 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);
}
}
public class Candy11 {
public static void test(final int x) {
Runnable runnable = new Candy11$1(x);
}
}
- 这同时解释了为什么匿名内部类引用局部变量时,局部变量必须是 final 的:因为在创建Candy11$1 对象时,将 x 的值赋值给了 Candy11 1 对 象 的 v a l 1 对象的 val 1对象的valx 属性,所以 x 不应该再发生变化了,如果变化,那么 val$x 属性没有机会再跟着一起变化
- 我们的JDK1.8,我们的编译器会默认帮我们加这个final,不需要我们手动去加,所以在局部内部类或者匿名内部类中不能修改我们的局部变量
首先我们的Inner和Outer各自会生成对应的lclass对象,那么就是内部类不会随着方法执行完毕就会消耗,那么当外部方法执行完毕,其局部变量也会销毁。但是内部类的对象还可能存在(只有没有人引用才会死亡)。这就出现了一个矛盾,那么内部类对象就访问了一个不存在的变量,所以我们的解决方法就是将局部变量复制一份给内部类作为成员变量,这样当方法死亡的时候,内部类依然可以访问,当时这样我们必须要保证其两个变量是一样的,可能我们内部类去做修改变量的操作,为了防止这种情况的出现,所以在内部类使用的局部变量必须是final的