PART1:语法糖(Syntactic Sygar),也叫糖衣语法,是由英国计算机科学家彼得约翰兰达发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能没影响但是更方便咱们这些码农使用。。(语法糖可以看作编译器实现的用来提升效率的一些小把戏)
- 泛型与类型擦除:咱们Java还没有泛型时咱们一般就只能通过
Object是所有类型的父类
和类型强制转换
两个特点的配合来实现类型泛化。
- 泛型:本质是参数化类型(也就是所操作的数据类型被指定为一个参数,这种参数类型可用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法)的应用。(把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型)
- Java中的泛型只在程序源码中存在,在编译后的字节码文件中就已经被替换为原来的原生类型(Raw Type)了,并且在相应的地方插入了强制转型代码。
- 在项目中泛型的实际使用:
定义 Excel 处理类 ExcelUtil<T> 用于动态指定 Excel 导出的数据类型
编译器可以对泛型参数进行检测
,并且通过泛型参数可以指定传入的对象类型。比如 ArrayList persons = new ArrayList()这行代码就指明了该 ArrayList 对象只能传入 Persion 对象,如果传入其他类型的对象就会报错。
- 不同的编译器对于泛型的处理方式是不同的,通常情况下,
一个编译器处理泛型有两种方式:Code specialization和Code sharing
。- C++和 C#是使用Code specialization的处理机制
- Java 使用的是Code sharing的机制。
- Code sharing 方式
为每个泛型类型创建唯一的字节码表示
,并且将该泛型类型的实例都映射到这个唯一的字节码表示上。将多种泛型类形实例映射到唯一的字节码表示是通过类型擦除(type erasue)实现的
。【也就是说,对于 Java 虚拟机来说,他根本不认识Map<String, String> map这样的语法。需要在编译阶段通过类型擦除的方式进行解语法糖。】
- Code sharing 方式
- 泛型一般有三种使用方式:泛型类、泛型接口、泛型方法。
- public class Generic{}
- 此处
T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
在实例化泛型类时,必须指定T的具体类型
- 此处
- 泛型接口:public interface Generator {}
- 实现泛型接口,不指定类型:class GeneratorImpl implements Generator{}
- 实现泛型接口,指定类型:class GeneratorImpl implements Generator{}
- 泛型方法 :public static < E > void printArray( E[] inputArray ){}
在 java 中泛型只是一个占位符,必须在传递类型后才能使用
。类在实例化时才能真正
的传递类型参数,由于静态方法的加载先于类的实例化
,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。静态泛型方法只能使用自己声明的 <E>
- public class Generic{}
- 泛型擦除是什么?
- 因为泛型其实只是在编译器中实现的而
虚拟机并不认识泛型类项
,所以要在虚拟机中将泛型类型进行擦除。也就是说,在编译阶段使用泛型,运行阶段取消泛型,即擦除
。擦除是将泛型类型以其父类代替,如String 变成了Object等
。其实在使用的时候还是进行带强制类型的转化,只不过这是比较安全的转换,因为在编译阶段已经确保了数据的一致性。- 虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,
泛型类并没有自己独有的Class类对象
。比如并不存在List.class或是List.class,而只有List.class
- 虚拟机中没有泛型,只有普通类和普通方法,所有泛型类的类型参数在编译时都会被擦除,
- 因为泛型其实只是在编译器中实现的而
- 类型擦除:Java中的泛型实现方法成为类型擦除,基于这种方法实现的泛型也叫伪泛型。
这段代码编译后会怎样?(把这段Java代码编译成为Class文件,然后用字节码反编译工具进行反编译后发现泛型都不见了,也就是程序又变回了Java泛型出现之前的写法)public static void mian(String[] args){ Map<String, String> map = new HashMap<String, String>(); map.put("hhb", "老公"); map.put("minqaqq", "老婆"); System.out.println(map.get("hhb")); System.out.println(map.get("minqaqq")); }
那为什么要用类型擦除呢,关于原因众说纷纭。如作者所说,通过擦除法来实现泛型丧失了一些泛型的优雅。public static void mian(String[] args){ Map map = new HashMap(); map.put("hhb", "老公"); map.put("minqaqq", "老婆"); System.out.println((String)map.get("hhb")); System.out.println((String)map.get("minqaqq")); }
- 当泛型遇见方法重载时,如下图,这两个方法没办法共存在一个.Class文件中【两个重载的函数,因为他们的参数类型不同,一个是List另一个是List ,
这段代码是编译通不过的
。因为参数List<Integer>和List<String>编译之后都被擦除了,变成了一样的原生类型 List,擦除动作导致这两个方法的特征签名变得一模一样
。】
- 但是这两个方法,加入了不同的返回值后,方法重载可以成功,这段代码就可以被编译和执行了,因为加了不同的返回值后这俩方法可以共存在一个.Class文件之中
- 但是这两个方法,加入了不同的返回值后,方法重载可以成功,这段代码就可以被编译和执行了,因为加了不同的返回值后这俩方法可以共存在一个.Class文件之中
- 当泛型遇到 catch:
- 泛型的类型参数不能用在 Java 异常处理的 catch 语句中。因为异常处理是由 JVM 在运行时刻来进行的。由于类型信息被擦除,JVM 是无法区分两个异常类型MyException和MyException的
- 当泛型内包含静态变量
- 由于经过类型擦除,
所有的泛型类实例都关联到同一份字节码上,泛型类的所有静态变量是共享的
- 由于经过类型擦除,
- 泛型:本质是参数化类型(也就是所操作的数据类型被指定为一个参数,这种参数类型可用在类、接口和方法的创建中,分别称为泛型类、泛型接口和泛型方法)的应用。(把类型明确的工作推迟到创建对象或调用方法的时候才去明确的特殊的类型)
- 自动装箱、自动拆箱、遍历循环。继续上面的代码编译前后的故事
- 包装类的"=="运算在不遇到算术运算时不会自动拆箱
- equals()方法不处理数据转型的关系
- 自动装箱就是 Java 自动将原始类型值转换成对应的对象,
比如将 int 的变量转换成 Integer 对象,这个过程叫做装箱,反之将 Integer 对象转换成 int 类型值,这个过程叫做拆箱
。因为这里的装箱和拆箱是自动进行的非人为转换,所以就称作为自动装箱和拆箱。原始类型 byte, short, char, int, long, float, double 和 boolean 对应的封装类为 Byte, Short, Character, Integer, Long, Float, Double, Boolean。- 自动装箱:
在装箱的时候自动调用的是Integer的valueOf(int)方法
- 自动拆箱:
在拆箱的时候自动调用的是Integer的intValue方法
。
- 自动装箱:
public static void mian(String[] args){
List<Integer> list = Arrays.asList(1, 2, 3, 4);
int sum = 0;
for(int i : list){
sum += i;
}
System.out.println(sum);
}
这段代码编译后会怎样?(把这段Java代码编译成为Class文件,然后用字节码反编译工具进行反编译后发现泛型都不见了,也就是程序又变回了Java泛型出现之前的写法)
- 自动装箱、自动拆箱在编译之后被转化成为了对应的包装和还原方法
- 遍历循环则把代码还原成为了迭代器的实现,这也是为何遍历循环需要被遍历的类实现Iterable接口的原因
- 变长参数在调用时变成了一个数组类型的参数,在变长参数出现之前,类似功能就也只能用数组完成,落叶归根哪
public static void mian(String[] args){ List list = Arrays.asList(new Integer[]{ Integer.valueOf(1);//自动装箱、自动拆箱在编译之后被转化成为了对应的包装和还原方法 Integer.valueOf(2);//自动装箱、自动拆箱在编译之后被转化成为了对应的包装和还原方法 Integer.valueOf(3);//自动装箱、自动拆箱在编译之后被转化成为了对应的包装和还原方法 Integer.valueOf(4);//自动装箱、自动拆箱在编译之后被转化成为了对应的包装和还原方法 }); int sum = 0; //遍历循环则把代码还原成为了迭代器的实现 for(Iterator localIterator = list.iterator(); localIterator.hasNext();){ int i = ((Integer) localIterator.next()).intValue();//自动装箱、自动拆箱在编译之后被转化成为了对应的包装和还原方法 sum += i; } System.out.println(sum); }
- 可变参数在被使用的时候,他首先会
创建一个数组
,数组的长度就是调用该方法是传递的实参的个数,然后再把参数值全部放到这个数组当中,然后再把这个数组作为参数传递到被调用的方法中
。
- 可变参数在被使用的时候,他首先会
- switch开始支持String:
- Java 中的switch自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其 ascii 码。所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii 码是整型)以及int。
- Java 中的switch自身原本就支持基本类型。比如int、char等。对于int类型,直接进行数值的比较。对于char类型则是比较其 ascii 码。所以,对于编译器来说,switch中其实只能使用整型,任何类型的比较都要转换成整型。比如byte。short,char(ackii 码是整型)以及int。
- 枚举:Java SE5 提供了一种新的类型就是Java 的枚举类型,
关键字enum可以将一组具名的值的有限集合创建为一种新的类型
,而这些具名的值可以作为常规的程序组件使用,这是一种非常有用的功能。- num就和class一样,只是一个关键字,他并不是一个类
- 比如,通过反编译后代码我们可以看到,
public final class T extends Enum说明该类是继承了Enum类的,同时final关键字告诉我们,这个类也是不能被继承的。或者说当我们使用enum来定义一个枚举类型的时候,编译器会自动帮我们创建一个final类型的类继承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 }); } }
- 比如,通过反编译后代码我们可以看到,
- num就和class一样,只是一个关键字,他并不是一个类
- 内部类:
- 内部类又称为嵌套类,
可以把内部类理解为外部类的一个普通成员
。 - outer.java里面定义了一个内部类inner,
一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class
。所以内部类的名字完全可以和它的外部类名字相同。
- 内部类又称为嵌套类,
- 条件编译:
- —般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,
希望只对其中一部分内容进行编译
,此时就需要在程序中加上条件,让编译器只对满足条件的代码进行编译,将不满足条件的代码舍弃,这就是条件编译。如在 C 或 CPP 中,可以通过预处理语句来实现条件编译。其实在 Java 中也可实现条件编译。Java 语法的条件编译,是通过判断条件为常量的 if 语句实现的。其原理也是 Java 语言的语法糖。根据 if 判断条件的真假,编译器直接把分支为 false 的代码块消除。通过该方式实现的条件编译,必须在方法体内实现,而无法在正整个 Java 类的结构或者类的属性上进行条件编译
,这与 C/C++的条件编译相比,确实更有局限性。在 Java 语言设计之初并没有引入条件编译的功能,虽有局限,但是总比没有更强
- —般情况下,程序中的每一行代码都要参加编译。但有时候出于对程序代码优化的考虑,
- 断言:
- 在 Java 中,assert关键字是从 JAVA SE 1.4 引入的,为了避免和老版本的 Java 代码中使用了assert关键字导致错误,
Java 在执行的时候默认是不启动断言检查的(这个时候,所有的断言语句都将忽略!),如果要开启断言检查,则需要用开关-enableassertions或-ea来开启【断言的底层实现就是 if 语言,如果断言结果为 true,则什么都不做,程序继续执行,如果断言结果为 false,则程序抛出 AssertError 来打断程序的执行。-enableassertions会设置$assertionsDisabled 字段的值】
- 在 Java 中,assert关键字是从 JAVA SE 1.4 引入的,为了避免和老版本的 Java 代码中使用了assert关键字导致错误,
- 数值字面量:
- 在 java 7 中,数值字面量不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,
目的就是方便阅读:int i = 10_000;
。反编译后就是把_删除了。也就是说 编译器并不认识在数字字面量中的_,需要在编译阶段把他去掉
- 在 java 7 中,数值字面量不管是整数还是浮点数,都允许在数字之间插入任意多个下划线。这些下划线不会对字面量的数值产生影响,
- for-each:
- 增强 for 循环(for-each)他会比 for 循环要少写很多代码,
for-each 的实现原理其实就是使用了普通的 for 循环和迭代器
- Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出java.util.ConcurrentModificationException异常。
- 所以 Iterator 在工作的时候是不允许被迭代的对象被改变的。但可以使用 Iterator 本身的方法remove()来删除对象,Iterator.remove() 方法会在删除当前迭代对象的同时维护索引的一致性。
- 增强 for 循环(for-each)他会比 for 循环要少写很多代码,
- Lambda 表达式:
实现方式其实是依赖了几个 JVM 底层提供的 lambda 相关 api【lambda 表达式的实现其实是依赖了一些底层的 api,在编译阶段,编译器会把 lambda 表达式进行解糖,转换成调用内部 api 的方式】
。- 内部类在编译之后会有两个 class 文件,但是包含 lambda 表达式的类编译后只有一个文件。
PART2:JDK 监控和故障处理工具
- JDK 命令行工具:这些命令在 JDK 安装目录下的 bin 目录下:
- jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(Local Virtual Machine Identifier,LVMID)。
- jps -q :只输出进程的本地虚拟机唯一 ID。
- jps -l:输出主类的全名,如果进程执行的是 Jar 包,输出 Jar 路径。
- jps -v:输出虚拟机进程启动时 JVM 参数
- …
- jstat(JVM Statistics Monitoring Tool): 用于收集 HotSpot 虚拟机各方面的运行数据;
jstat(JVM Statistics Monitoring Tool) 使用于监视虚拟机各种运行状态信息的命令行工具
。 它可以显示本地或者远程(需要远程主机提供 RMI 支持)虚拟机进程中的类信息、内存、垃圾收集、JIT 编译等运行数据,在没有 GUI,只提供了纯文本控制台环境的服务器上,jstat(JVM Statistics Monitoring Tool) 将是运行期间定位虚拟机性能问题的首选工具
- jstat - [-t] [-h] [ []];
比如 jstat -gc -h3 31736 1000 10表示分析进程 id 为 31736 的 gc 情况,每隔 1000ms 打印一次记录,打印 10 次停止,每 3 行后打印指标头部。
- jinfo (Configuration Info for Java) : Configuration Info for Java,显示虚拟机配置信息;实时地查看和调整虚拟机各项参数
- jmap (Memory Map for Java) : 生成堆转储快照;
- jhat (JVM Heap Dump Browser) : 用于分析 heapdump 文件,它会建立一个 HTTP/HTML 服务器,让用户可以在浏览器上查看分析结果;
- jstack (Stack Trace for Java) : 生成虚拟机当前时刻的线程快照,线程快照就是当前虚拟机内每一条线程正在执行的方法堆栈的集合。
- 举例,例子来自javaGuide。下面是一个
线程死锁
的代码。我们下面会通过 jstack 命令进行死锁检查,输出死锁信息,找到发生死锁的线程。
- 通过 jstack 命令分析:
- JDK 可视化分析工具:
JConsole 是基于 JMX 的可视化监视、管理工具。可以很方便的监视本地及远程服务器的 java 进程的内存使用情况。你可以在控制台输出console命令启动或者在 JDK 目录下的 bin 目录找到jconsole.exe然后双击启动
- Visual VM:多合一故障处理工具
- 通过 jstack 命令分析:
- 举例,例子来自javaGuide。下面是一个
- jps (JVM Process Status): 类似 UNIX 的 ps 命令。用于查看所有 Java 进程的启动类、传入参数和 Java 虚拟机参数等信息;显示虚拟机执行主类名称以及这些进程的本地虚拟机唯一 ID(Local Virtual Machine Identifier,LVMID)。
巨人的肩膀:
深入理解Java虚拟机
javaGuide
小林coding