仅简单总结~辅助快速回忆~
一、JVM
1,Java类加载机制
Java程序由多个类文件组成,按需加载。
Java的动态扩展是由运行期动态加载和动态链接实现的。——动态绑定,多态。
加载步骤:
1)装载:查找和导入Class文件。
a) 根据一个类的全限定名来获取二进制字节流
b) 将这个字节流代表的静态存储结构转化为方法区的运行时数据结构。
c)在Java堆中生成一个代表这个类的Class对象,作为方法区数据的访问入口。
2)链接:把类的二进制数据合并到JRE中。
a) 校验:检查Class文件的正确性
b) 准备:给类的静态变量分配存储空间
正式为类变量分配并设置类变量初始值,这些内存都将在方法区中分配。初始值指的是零值。
注意,这时参与内存分配的只包括STATIC类变量,而不包括实例变量。实例变量会随着对象一起分配到堆中。
c) 解析:将符号引用转成直接引用
3)初始化:对类的静态变量、静态代码块进行初始化。
a) 时机:使用new关键字实例化对象、读取或设置一个类的静态字段(除final常量已在编译期放入常量池之外)、调用静态方法。
b) 反射:如果类没有初始化,就先触发其初始化。
c)初始化子类:如果发现其父类未初始化,就先初始化父类。
d) 虚拟机启动:首先初始化用户指定的主类,包含main()的那个类。
特别注意的是:通过子类来调用父类的静态字段时,只会初始化父类,而和子类没有什么关系=,=,因为这个字段不是他自己的静态字段。
对常量的引用 ,实际上都会被转化成对类自身的常量池的引用,也和类初始化没有什么关系=,=
2,类加载器 ClassLoader
类加载器,寻找类的字节码文件,构造出类在JVM内部的表示的对象。
(1)ClassLoader与双亲委派模型
寻找JRE目录,寻找jvm.dll,初始化JVM。
产生一个Bootstrap ClassLoader,自动加载Extension ClassLoader,设置其父Loader为Bootstrap ClassLoader.
BootstrapLoader自动加载App ClassLoader,设置其父Loader为Extension ClassLoader。
Bootstrap ClassLoader: 加载<JAVA_HOME>\lib目录中的类库,按文件名识别,文件名不符的即使放在该目录也不会加载。该加载器无法被Java程序直接引用。
Extension ClassLoader:加载扩展API,<JAVA_HOME>\lib\ext目录下的所有类库。
AppClassLoader:加载用户机上的自定义类,CLASSPATH目录。
【委派模型工作过程】
如果一个类加载器接收到了类加载的请求,它首先把这个请求委托给他的父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它在搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。
【好处】
1. 类加载器之间存在优先级的层次关系。父加载器优先可以保证核心API的正确加载,提高安全性。
2. 避免重复加载,父类已经加载过的类,子类无需再加载。
【loadClass的主要代码】
(2)区分Class类的加载方法forName()
ClassLoader中的loadClass()和Class的静态方法forName()都是用来加载类的。
用法一般为:
Class clazz = Class.forName(“name”);
或:
ClassLoader loader = Thread.currentThread().getContextClassLoader();
Class clazz = loader.loadClass(“name”);
注意,在使用类加载器加载类时,只传入了类名,而没有初始化标志resolve,第二个参数默认为FALSE,即不对该类进行解释,也不初始化该类,仅仅是加载。
而Class.forName也只传入了类名,不过默认是初始化的,会将Class进行解释和初始化。
3. JVM字符编码和IO
JVM内部的字符只有一种形式:UNICODE。编码转换只发生在JVM和OS的交界处,即IO流。
在JVM内部,字符统一用Unicode表示,但需要输出到JVM外部时,就使用编码转换,为具体的字符。
(1)IO流与编码转换
分为两大阵营:面向字符和面向字节。
面向字节:文件中的二进制内容和读入JVM内部的内容一致。不能变换任何01顺序。这种输入输出适合读入音频,视频文件,或不需要任何变换的文件。
面向字符:文件中的字符和读入JVM的字符一致。因此,面向字符的IO类隐式地做了编码转换。输入时,将文件中的字符转换为Unicode字符,输出时再编码为字符。
注意,编码转换方案只能使用默认的,如果需要指定转换编码,只能在字节字符流转换的地方,即InputStreamReader和OutputStreamWriter。
(2)装饰器模式
装饰器模式允许向一个现有的对象添加新的功能,同时又不改变原有的结构。
要求装饰器必须和装饰对象有相同的接口。
InputStream抽象类定义了抽象方法read()。它的一个子类是FilterInputStream,这是装饰器类的核心。
FilterInputStream类在内部维护了一个InputStream类的成员对象,它的所有方法都是调用该类的同名方法。所以,这个类只起到了委托作用。
但是,它的子类都是装饰器类,可以提供各种不同的功能。例如,BufferedInputStream,提供了缓冲功能。在它的read()方法中,检测当前位置pos是否超出缓冲区大小,如果是,就调用fill()把缓冲区内容输出。
【实体流】直接连接数据源的流。可单独使用。
如FileInputStream,FileInputStreamReader, FileOutputStream,FileoutputStreamReader.
【装饰流】不直接连接数据源,而是以实体流对象或装饰流对象为基础,增强实体流的读写能力。不可单独使用,需要流嵌套。
如BufferedInputStream, BufferedOutputStream, BufferedReader, BufferedWriter.
【桥接流】字节流转换为字符流
装饰流在嵌套时,只能嵌套相同类型的流。InputStream和Reader是两个不同的体系。需要桥接流进行转换。如InputStreamReader和OutputStreamReader类。
(3) IO类的关系图
二、语法细节
1. 自增自减运算
注意区分先增后增情况下的取值。
中间变量缓存机制
这里涉及Java的中间缓存变量机制。等效于:tmp=j, j=j+1, j=tmp。所以该表达式运行完之后, j=j,不会发生改变。
2. 数据类型转换
Java基本数据类型分为三大类:布尔型,字符型,数值型。数值型又分为浮点型和整型。
8种基本类型:boolean, char, byte, short, int, long, float, double。
对应的包装类:Boolean,Character, Byte, Short, Integer, Long, Float, Double.
此外,比较常用的类是String和Date。
Java的基本数据类型与平台无关,固定字节数:boolean未规定,byte—1, char—2, short—2, int—4, long—8, float—4, double--8
(1)基本数据类型之间的转换
从低级到高级可自动转换,从高级到低级需要强制类型转换。同级之间也需要强转,如char,byte,short。
从低到高:(byte, short, char)—int—long—float—double
强制类型转换可能会导致溢出或精度下降。
(2)包装类提供的类型转换函数
在进行基本数据类型之间的转换时,可以利用包装类作为中间过渡。然后利用包装类提供的方法进行类型转换。
一般包装类提供的该类函数有:doubleValue(), intValue(),….toString()。
(3)字符型变量转换为数值类型
一种是char直接对应到ASCII码。
另一种是数学意义的转换,Character提供的getNumericValue(char ch)方法。
3. 断言assert()
当布尔表达式为假时,产生一个Assertionerror。用于调试目的。
可以在预计正常情况下不会到达的任何位置上放置断言。
断言不应以任何方式改变程序的状态。
4. 运算符
(1)类型提升
在运算时,如果表达式中有低级和高级的基本数据类型,那么会自动把低级的提升为高级的。这一点切勿忘记!
(2)常量类型
如果表达式中出现常量,那么运算时类型以非常量的类型为准。例如,一个表达式是常量10,另一个是类型T,则输出结果是类型T的。
(3)布尔逻辑运算符
位运算符 & | ^
逻辑运算符 && ||
布尔逻辑运算符:当运算对象是布尔类型时,可以使用位运算符作为布尔逻辑运算符。
布尔逻辑运算符的优先级别比逻辑运算符高,重要的区别是,布尔逻辑运算符是非短路的,而逻辑运算符经过优化,是短路的。
上例中,因为逻辑运算符是短路的,所以第一个表达式为FALSE就不会执行第二个了。而布尔逻辑运算符比较傻,它一定会执行两个表达式,才给出最终结果,所以n++被执行了一次。
三、异常体系
Java唯一的正式报告错误的机制。由编译器强制执行。
分离正常代码和异常处理代码,结构清晰简洁。
把每件事看作一个事务,由异常守护这些事务。
可以看作一种内建的恢复系统。多个恢复点。
1. 抛出异常的过程
首先,用new关键字在堆上创建一个异常对象。
当前路径被终止,弹出异常对象的引用。
由异常处理程序接管,从错误中恢复,要么继续运行,要么换一种方式运行。
2. throw和return的区别
从某种意义上说,throw是一种特殊的返回机制。只是和普通的return的返回点不同。
异常返回的位置是异常处理程序,可能离抛出异常的位置很远,甚至跨越方法调用栈的多层。而return只会返回一层。
3. Java标准异常
根类 Throwable。
两种类型:Error和Exception。
Error表示编译时和系统错误,一般由JVM抛出。
Exception是应用程序中的异常。一般分为受检查异常和不受检查的异常。
RuntimeException是唯一的不受检查的异常。自动被JVM抛出,通常不用捕获,代表的是编程错误,程序员应该检查代码。
与之相对的是受检查的异常,不容许在代码中忽略,由编译器强制执行。
【常见的RuntimeException】
BufferOverflowException
ClassCastException
ConcurrentModificationException
EmptyStackException
IllegalArgumentException
IndexOutOfBoundsException
NullPointerException
SystemException
4. 关键字
try—catch—finally均不能单独使用。三个代码块中变量的作用域为代码块内部,不能相互访问,如果要在三个块中都访问,应把变量定义在块外。
若有多个catch块,按顺序匹配,只匹配一个Exception。
throw关键字用于方法体内部,用于抛出一个异常。如果在方法内抛出了异常,还应当在方法头用throws声明可能抛出的异常。
方法的调用者应处理异常,除了Error和RuntimeException之外。
四、区别:final, finally, finalize
1. final
用于修饰成员、方法、方法参数、类。
(1)final成员
成员变量一旦被初始化,其值就不再改变。如果是引用,则引用不能改变,但引用指向的对象的字段可以改变。
初始化可以在两个位置:一是定义处,二是构造函数处。
(2)final参数
注意,函数传参是按值传递。加上final后,意思是不允许在方法内部修改该参数。
对于基本类型变量,没有什么影响,因为基本类型本身就是按值传递的,在内部修改本来就会影响原参数。
但对于引用类型变量,在方法中就无法修改所指向的对象了,可以防止意外修改。
(3)final方法
两个含义:
1)不允许覆盖和继承。可以重载。
2)允许编译器将所有对此方法的调用转化为inline调用机制。在调用该方法时,直接把方法体插入调用处。免去例行的方法调用,如保存断点、压栈等。
注意,Inline调用机制虽然可能提高程序效率,但在多处调用该方法的情况下,调用主体代码会迅速膨胀,反而会影响效率。要慎用final方法。
(4)final类
不允许继承,在继承树中是一个叶子类。所有方法默认是final的。
这一点和abstract相矛盾,所以一个类不能同时被final和abstract修饰。
2. finally
是异常处理模型的最佳补充。
finally结构使代码总会被执行,不管有无异常发生。即使在try块中有return语句,finally也一定会在return之前执行。
一般用于维护对象状态,清理非内存资源。
3. finalize()
是一个和JVM垃圾回收有关的Object方法。垃圾回收器准备回收某个对象时,可能会调用该方法。
通常用于一些不容易控制,并且非常重要的资源的释放,例如,一些IO操作,数据连接。
注意,在应用程序中,不应仅仅依靠finalize来释放资源,而要通过程序本身管理资源为主、finalize函数释放资源为辅的双保险管理机制。因为JVM不保证这个函数一定会被调用,即使调用也只能调用一次。
五、反射
反射的概念不是Java独有的。Objective-C就支持反射,但c++不支持。
允许运行中的程序对自身进行检查,并能直接操作程序的内部属性。
反射API提供的动态代理是非常强大的功能,可以原生实现AOP中的方法拦截功能。
优点是灵活性强。弊端是性能比较差。相同的操作,用反射API所需的时间大概比直接调用来慢一两个数量级。
因此,使用反射时要权衡灵活性和性能。
1. 获取运行时的内部结构
只要取到了一个 Class对象,就可以遍历一个Java类的构造方法、声明的域、定义的方法。
对应的方法分别是 getConstructor, getField, getMethod。——只能获取public方法。
还有相应的getDeclaredConstructor, getDeclaredField, getDeclaredMethod。——可以获取到所有方法,包括protected, private的。
2. 运行时操作一个Java对象
动态创建一个类的对象,某取某个域的值,调用某个方法。
注意,在获取到类的内部结构以后,可以通过获取到的Method对象、Constructor对象和Field对象,调用 setAccessible(true)来绕过Java默认的访问控制检查,这样就可以访问私有的方法、构造函数和域了。
六、动态代理机制
1. 代理模式
提供一个代理,控制对某个对象的访问(隐藏和保护委托类)。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的处理。
为了保持行为的一致性,代理对象和被代理对象一般实现相同的接口,调用者与代理对象交互。
代理的存在对调用者来说是透明的,调用者看到的是接口。
代理对象可以封装一些内部的处理逻辑,如访问控制、远程通信、日志、缓存等。
这种模式在RMI和EJB中都得到了广泛的使用。
2. JDK5的动态代理机制
在运行时,可以动态创建出一个实现了多个接口的代理类。每个代理类的对象都会关联一个表示内部处理逻辑的InvocationHandler接口实现。
当使用者调用了代理对象的接口中的方法的时候,这个调用的信息会被传递给InvocationHandler的invoke方法。在invoke方法的参数中可以获取到代理对象、方法对应的Method对象和调用的实际参数。invoke方法的返回值被返回给使用者。
这种做法实际上相当于对方法调用进行了拦截。
【主类:java.lang.reflect.Proxy】提供一组静态方法来为一组接口动态生成代理类及其对象。
【处理器接口:java.lang.reflect.InvocationHandler】定义了一个invoke方法,集中处理在动态代理类对象上的方法调用。
第一个参数是代理类实例,第二个参数是被调用的方法对象,第三个是调用参数。
每次生成动态代理类对象,都需要指定一个实现了该接口的调用处理器对象、指定一个类装载器。
Proxy静态方法生成的动态代理类同样需要通过类装载器来进行装载才能使用,它与普通类的唯一区别是,其字节码是由JVM在运行时动态生成的,不预存在任何.CLASS文件中。
【动态代理的创建过程】
1)实现InvocationHandler接口,创建自己的调用处理器。
2)为Proxy类指定一个ClassLoader对象,和一组Interface,创建动态代理类。
3)通过反射机制,获得动态代理类的构造函数,唯一参数类型是调用处理器接口类型。
4)通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数被传入。
【实际应用中的创建动态代理】
Proxy类中的静态方法newProxyInstance已经封装了步骤2到4的过程,所以使用时可以简化。
3. Spring AOP
使用了两种代理机制:JDK的动态代理(基于接口),CGLib的动态代理(基于类)。
待学习和总结。
七、传递与引用
无论传值还是传引用,都是传递参数的副本。
特别注意的是,String类型也是对象型变量,所以必须是传引用副本。只不是String是个final类,所以传值和传引用显得没有什么分别。
八、输入输出流
1. RandomAccessFile类
支持对随机访问文件的读取和写入。
随机访问文件的行为类似存储在文件系统中的一个大型byte数组。
存在指向该隐含数组的光标——文件指针;输入操作从文件指针开始读取字节,随着读取而向前移动该指针。输出操作从文件指针开始写入字节,并随着写入而向前移动该指针。文件指针可通过getFilePointer方法读取,并通过seek方法设置。
除了实现DataInput和Dataoutput接口,该类与Stream、Reader继承体系没有任何关系。
RandomAccessFile类不支持装饰。必须假定已经被正确地缓冲。
从本质上来说,工作方式类似于把DataInputStream和DataoutputStream组合起来使用。
只有RandomAccessFile支持搜寻方法,并且只适用于文件。
在java1.4中,RandomAccessFile的大多数功能由nio存储映射文件所取代。
2. 一个简单的读写文件代码
2. 序列化
将实现了Serializable接口的对象转换成一个字节序列。
轻量级持久性。持久意味着生命周期不依赖于程序是否正在执行。轻量级是因为不能用某种persistent关键字来简单地定义一个对象,让系统自动维护其他细节问题。对象必须在程序中显式地序列化和反序列化。
对象序列化的概念加入语言是为了支持两种主要特性:一是RMI,远程方法调用。二是JavaBean的支持。
Serializable接口是一个标记接口,不包含任何方法。
要序列化一个对象,首先要创建某些OutputStream对象,如FileOutputStream,将其封装在一个ObjectOutputStream对象内,只需调用writeObject(obj)即可将对象序列化。
反序列化时,只将InputStream封装在ObjectInputStream内,调用readObject()即可拿到一个Object引用,根据具体情况向下转型。
对象序列化不仅保存了对象的全景图,而且能追踪到对象内包含的所有引用,并保存那些对象。——深度复制对象网。