重拾Java笔记
工作主要用Java, 因此开始准备Java基础再补补。根据小猴子1024-JAVA基础整理笔记
命名规范:
没怎么写, 所以一直忘, 这次写在最前面, 便于翻阅。
大驼峰命名(UpperCamelCase):每个单词的第一个字母大写,其他字母小写。e.g.MyException
小驼峰命名(lowerCamelCase):如果仅有一个单词,那么所有字母全部小写,如果是两个及以上的单词组成的名称,那么除了第一个单词是全部小写外,其他都是的首字母大写,其他字母小写。e.g.getMyName
1.1 包的命名
包的命名由全部小写的单词组成。一般使用公司的域名的作为自己程序包的唯一前缀,使用倒域名规则,例如:com.baidu.项目名,然后针对每个具体的模块在区分每个模块包名,例如:论坛模块的整体包名:com.baidu.项目名.tribune
(域名倒写)
1.2 类的命名
类的命名遵循大驼峰命名的规则
1.3 接口的命名
接口命名遵循大驼峰命名的规则,以大写的I开头,表示这是一个接口,以able或ible截尾。
1.4 变量命名
变量的命名遵循小驼峰命名的规则,其中控件的变量建议使用控件缩写+逻辑名称的格式,例如:
1.5 常量的命名
常量名称的每个单词都大写,并且每个单词之间通过下划线(_)连接,例如:
1.6 方法的命名
方法的命名遵循小驼峰命名的规则,以动词+名词的方式组成,例如初始化view:initView()。
1.7 资源文件命名
全部小写,并通过下划线连接。
class与文件名:
在一个Java文件里面,可以声明多个class,但是只能声明一个public class
- 如果使用class来声明类,文件名可以是任何合法的文件名称,文件名不需要和Class类一致
- 如果采用public class来声明class,那么文件名必须和类名一致
结论: 使用javac命令所编译出的class文件的名称跟java的文件名没有关系,而是跟类名一致。
函数传参:
- 如果参数是基本数据类型, 是会生成一个新的形参
- 如果参数是引用数据类型(不包括封装数据类型), 那么会生成该对象的引用(类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型)==>引用数据类型变量,调用方法时作为参数是按引用传递的
代码块的分类
使用{}括起来的代码被称为代码块,根据其位置和声明的不同可以分为下面4种:
- 局部代码块,在方法中出现,限定变量生命周期,及早释放,提高内存利用率
- 构造代码块,在类中方法外出现;多个构造方法方法中相同的代码存放到一起,每次调用构造都执行,并且在构造方法前执行
- 静态代码块, 在类中方法外出现,并加上static修饰;用于给类进行初始化,在加载的时候就执行,并且只执行一次。一般用于加载驱动。
- 同步代码块(后面多线程部分会讲解)
执行顺序:
1.静态代码块,随着类加载而加载,且只执行一次
2.构造代码块,每创建一个对象就会执行一次,优先于构造方法执行
3.构造方法,每创建一个对象就会执行一次
this指针
▲注意点: 在构造函数中调用该对象的另一个构造方法时, this(实参)必须写在最前面
1 | //构造方法 |
继承问题:
静态代码块Fu
静态代码块Zi
构造代码块Fu
构造方法Fu
构造代码块Zi
构造方法Zi
分析:
1.系统将Fu.class和Zi.class分别加载到方法区的内存里面,静态代码会随着.class文件一块加载到方法区里面,所以先打印出了静态代码块中的内容。
2.构造代码块优先于构造方法执行,父类初始化之前,所以打印出父类中的构造代码块和构造方法中的内容。
多态:
在工作当中尽量面向抽象编程,不要面向具体编程,即合理利用多态——SOLID原则中依赖倒置:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。换言之,要针对接口编程,而不是针对实现编程。
多态的优点
- 提高程序的扩展性
- 降低代码之间的耦合
用法
- 向上转型:上面代码中子类向父类型进行转换,是自动类型转换。
- 向下转型: 父类向子类型转换,是强制类型转换。
重写
重写,也叫做覆盖,当父类中的方法无法满足子类需求时,子类可以将父类的方法进行重写编写来满足需求。比如孩子继承了父亲的房子,可以将房子重新装修。
方法重写的条件:
- 两个类必须是继承关系
- 必须具有相同的方法名,相同的返回值类型,相同的参数列表.
- 重写的方法不能比被重写的方法拥有更低的访问权限。
- 重写的方法不能比被重写的方法抛出更宽泛的异常。(关于异常后面的章节再讲。)
- 私有的方法不能被重写。
- 构造方法无法被重写,因为构造方法无法被继承。
- 静态的方法不存在重写。
- 重写指的是成员方法,和成员变量无关。
Super关键字:
什么时候使用super?
- 子类和父类中都有某个数据,例如,子类和父类中都有name这个属性。如果要再子类中访问父类中的name属性,需要使用super。例1
- 子类重写了父类的某个方法(假设这个方法名叫m1),如果在子类中需要调用父类中的m1方法时,需要使用super。例1
- 子类调用父类中的构造方法时,需要使用super。
Object类之finalize方法
java对象如果没有更多的引用指向它(引用技术),则该java对象成为垃圾数据,等待垃圾回收器的回收,垃圾回收器在回收这个java对象之前会自动调用该对象的finalize方法==>可以理解为解析函数
访问控制权限
方法访问控制权限
修饰词 | 本类 | 同一个包的类 | 子类 | 任何地方 |
---|---|---|---|---|
private | √ | × | × | × |
default(默认) | √ | √ | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
方法访问控制权限
▲。注意以上对类的修饰只有:public和default,内部类除外(只有内部类可以设置为protected/private)
priavte和public都比较好理解和记忆,这里就不演示了,主要演示一下不同包下的两个具有父子关系的类里面使用protected和default的区别。
构造函数的权限问题:
public是一个访问权限(访问修复饰符)。一般构造函数可加可不加public。
- 如果加上制public,就代表此类可以对外开放,其他的类可以继承它,外部也可以实例化该对象。
- 如果不加public,则默认的修饰词是default,表示可以被这个类的子类或者和这个类同包的类调用。
除了这两个,你还可以添加private和default
记录一下默认修饰符:
- 类(class): ****缺省****就是没有修饰符,在同一个包中的类中可见,在其他包中不能用import导入。
- 变量(variable): 缺省在同一个包中可见,子类不在一个包中,子类中也不可见
- 方法(method):缺省在同一个包中可见,子类不在一个包中,子类中也不可见
- 接口(interface): 缺省同一个包中可见
- Java的interface中,成员变量的默认修饰符为:public static final;方法的默认修饰符,方法的默认修饰符是:public abstract(接口中的方法只能使用public和abstract修饰符 )==> 接口只是对一类事物属性和行为的更高次抽象;对修改关闭,对扩展开放,可以说是java中开闭原则的一种体现吧。
Final关键字:
特点为确定不可变
- final修饰的类无法被继承。
- final修饰的方法无法被重写。
- final修饰的局部变量,一旦赋值,不可再改变。
- final修饰的成员变量必须初始化值。
static关键字
static的作用
- static可以修饰变量,被static修饰的变量叫做静态变量,静态变量在类加载阶段赋值,并且只赋值一次。请看例1
- static可以修饰方法,被static修饰的方法叫做静态方法,不用创建对象就能能直接访问该方法,即使用类名.静态方法名的方式。静态方法不能访问非静态的数据,静态方法不能使用this。请看例2
- static可以定义静态语句块,静态语句块在类加载阶段执行,并且只执行一次,并且是自上而下的顺序执行,在构造方法之前执行。请看例3
static修饰的变量、方法、代码块都是隶属于**类(class)**级别的,跟对象无关。某一类物体如果可以被多个其他物体所共享,那么可以将这类物体使用static修饰。
比如wifi,多个人可以共同使用同一个wifi,所以wifi可以使用static修饰。手机是每人使用自己的,就不能用static修饰。
抽象类的特点
- 抽象类无法被实例化,无法创建抽象类的对象。
- 虽然抽象类没有办法实例化,但是抽象类也有构造方法,该构造方法是给子类创建对象用的。这也算是多态的一种。
- 抽象类中不一定有抽象方法,但抽象方法必须出现在抽象类中。
- 抽象类中的子类可以是抽象类,如果不是抽象类的话必须对抽象类中的抽象方法进行重写。
- 抽象类和抽象方法不能被final修饰
接口:
- 接口中只能出现常量和抽象方法(jdk8之后可以有default方法)
- 接口里面没有构造方法,无法创建接口的对象
- 接口和接口之间支持多继承,即一个接口可以有多个父接口
- 一个类可以实现多个接口,即一个类可以有多个父接口
- 一个类如果实现了接口,那么这个类需要重写接口中所有的抽象方法(建议),如果不重写则这个类需要声明为抽象类(不建议)
equals
== 两边如果是引用类型,则比较内存地址,地址相同则是true,反之则false.
- Object中的equals方法比较的是两个引用的内存地址。
- 但是在现实的业务逻辑当中,不应该比较内存地址,应该比较地址里面的内容,所以需要对equals方法进行重写。
==>▲注意:在使用自己创建的类进行equals比较时,一定要先重写equals方法
1 | //根据需求规定重写equals方法 |
内部类的分类
内部类,顾名思义就是在一个类的内部声明一个类。内部类主要分为:
- 静态内部类
- 匿名内部类
- 成员内部类
- 局部内部类
异常的分类
异常: 指的是程序在执行过程中,出现的非正常的情况,最终会导致JVM的非正常停止——JVM处理异常的方式是中断处理。
异常主要分为:Error、一般性异常、RuntimeException
- Error(强制中断错误):如果程序出现了Error,那么将无法恢复,只能重新启动程序,最典型的Error的异常是:OutOfMemoryError
- Exception(一般性异常(编译时异常):出现了这种异常必须在程序里面显示的处理,否则程序无法编译通过
- RuntimeException(运行时异常):此种异常可以不用显示的处理,例如被0除异常,java没有要求我们一定要处理。
JVM是如何处理异常的
- main方法自己将该问题处理,然后继续运行
- 自己没有针对的处理方式,只有交给调用main的jvm来处理,jvm有一个默认的异常处理机制。例如上面出现的ArithmeticException,jvm在控制台里面打印出来了异常信息。
大致流程: native method自己解决->交给Main解决->交给JVM解决
更好的讲解: B站视频
throw和throws
throws
- 用在方法声明后面,跟的是异常类名
public void m1() throws Exception
- 可以跟多个异常类名,用逗号隔开
- 表示抛出异常,由该方法的调用者来处理 (向上抛出指定异常)
throw
- 用在方法体内,跟的是异常对象名 ==>
throw new Exception()
- 只能抛出一个异常对象名
- 表示抛出异常,由方法体内的语句处理,需要直接在此处解决异常(在当前语句抛出指定异常)
自定义异常
1.自定义异常类一般都是以Exception结尾,说明该类是一个异常类
2.自定义异常类,必须的继承Exception或者RuntimeException
- 继承Exception:那么自定义的异常类就是一个编译期异常,如果方法内部抛出了编译期异常,就必须处理这个异常,要么throws,要么try…catch
- 继承RuntimeException:那么自定义的异常类就是一个运行期异常,无需处理,交给虚拟机处理(中断处理)
1 | public class MyException extends Exception{ |
catch
1 | catch (ArithmeticException | ArrayIndexOutOfBoundsException e) { |
字符串的不可变性
String类不能被继承
通过源码可以看到String类前面加了final修饰,因此String类是不能够被继承的。将其设置为不能被继承的原因是为了减少歧义。
字符串(String)的不可变性
String创建好之后值是不可以被改变的,这里指的是在堆中的字符串的值是不可以被改变。
String不可变的主要原因是其底层使用了一个final修饰的byte数组(jdk9之后版本中),final修饰的变量是不能被改变的。在jdk8版本中,String底层使用的是final修饰的char数组。这个版本之间的变化。
String、StringBuffer、StringBuilder
-
拼接执行效率:
String < StringBuffer < StringBuilder
-
线程安全:
线程安全 原因 String 安全 常量无线程安全问题 stringBuffer 安全 方法全为syncronized关键字修饰 stringbuilder 不安全 无
字符串常量池
我们声明的字符串会放到一个叫做字符串常量池的地方,这样可以减少内存的使用,字符串常量池是堆的一部分。
如果用new String("monkey")
会在字符串常量池中再建一个monkey, 其实是浪费了内存。所以开发中建议使用String s = “monkey1024”;这种方式创建字符串对象,可以减少堆内存的使用。==>比较两个字符串是否一致最好使用equals方法(看引用的内存地址是否一致)
详细请看: http://www.monkey1024.com/javase/481
String、StringBuffer、StringBuilder
- 如果需要对字符串进行频繁拼接的话,建议使用StringBuffer或者StringBuilder
StringBuffer
- StringBuffer是一个字符串缓冲区,如果需要频繁的对字符串进行拼接时,建议使用StringBuffer。
- StringBuffer的底层是byte数组(jdk9之后),jdk8中底层是char数组,如果没有明确设定,则系统会默认创建一个长度为16的byte类型数组,在使用时如果数组容量不够了,则会通过数组的拷贝对数组进行扩容,所以在使用StringBuffer时最好预测并手动初始化长度,这样能够减少数组的拷贝,从而提高效率。
StringBuilder和StringBuffer的区别
通过API可以看到StringBuilder和StringBuffer里面的方法是一样的,那他们有什么区别呢?
StringBuffer是jdk1.0版本中加入的,是线程安全的,效率低
StringBuilder是jdk5版本加入的,是线程不安全的,效率高
什么是自动拆箱和自动装箱?
- 自动装箱:把基本类型转换为包装类类型
- 自动拆箱:把包装类类型转换为基本类型
集合
接口Collection: 由三个接口组成——List / Set / Queue
集合的由来
数组长度是固定,如果要改变数组的长度需要创建新的数组将旧数组里面的元素拷贝过去,使用起来不方便。
java给开发者提供了一些集合类,能够存储任意长度的对象,长度可以随着元素的增加而增加,随着元素的减少而减少,使用起来方便一些。
集合类的一些特点
- List:里面存放的数据是有顺序的,可以存放重复的数据。
- Set:里面存放的数据是没有顺序的,不能存放重复的数据。
- Queue:是一个队列,里面的数据是先进先出,可以存放重复的数据。
区别
- 区别1:
- 数组既可以存储基本数据类型,又可以存储引用数据类型,基本数据类型存储的是值,引用数据类型存储的是地址值
- 集合只能存储引用数据类型(对象),如果存储基本数据类型时,会自动装箱变成相应的包装类
- 区别2:
- 数组长度是固定的,不能自动增长
- 集合的长度的是可变的,可以根据元素的增加而自动增长
List两个子类的特点
ArrayList:
- 底层数据结构是数组,查询快,增删慢。
LinkedList:
- 底层数据结构是链表,查询慢,增删快。
ArrayList和LinkedList的区别
- ArrayList底层是数组结果,查询和修改快
- LinkedList底层是链表结构的,增和删比较快,查询和修改比较慢
- 共同点:都是线程不安全的
ArrayList线程安全的方案
如果使用ArrayList需要考虑线程安全的问题,有两种方案:
-
可以使用Collections工具类中的synchronizedList方法可以将ArrayList变成线程安全的
1
List list = Collections.synchronizedList(new ArrayList());
-
使用java.util.concurrent包下面的CopyOnWriteArrayList,使用方式跟ArrayList一样
集合数组的互转
集合转数组:
1 | List<String> list = new ArrayList<>(); |
数组转集合
1 | // 注意转换后的集合不能调用其add方法向里面添加数据,否则会报出UnsupportedOperationException |
基本数据类型的数组转换成集合,会将整个数组当作一个对象转换,下面程序将会打印出list的对象地址
1 | int[] arr = {1,2,3,4,5}; |
==>▲.将数组转换成集合,数组中的数据必须是引用数据类型
1 | Integer[] arr = {11,22,33,44,55}; |
Collection集合
Set的特点
Set里面存储的元素不能重复,没有索引,存取顺序不一致。
▲.这里需要注意:在向HashSet中存放自定义类型对象时,一定要重写hashCode和equals方法,原因是无重复的话需要比较, 所以得。
TreeSet简介
TreeSet的特点是可以对存放进去的元素进行排序。
∴ 使用TreeSet存储自定义类型。这里还是存储之前定义的Person对象,需要实现Comparable接口并且重写compareTo方法,先根据name的字典顺序排序,然后再根据年龄进行排序。
1 | package top.nymrli.day07; |
Map接口概述
map中的元素是以键-值的方式存在的,通过键可以获取到值,键是不可以重复的,跟地图比较像,通过一个坐标就可以找到具体的位置。该接口由三个类实现:
HashMap / HashTable / AbstractMap
▲与前两个相比,添加元素的函数由add => put
1 | public class day08_Map { |
LinkedHashMap
- LinkedHashMap的特点:存取顺序一致
TreeMap
- TreeMap的特点:可以对存储的元素进行排序
HashMap和Hashtable的区别
- Hashtable是JDK1.0版本出现的,是线程安全的,效率低,不可以存储null键和null值
- HashMap是JDK1.2版本出现的,是线程不安全,效率高,可以存储null键和null值
Collection工具
1 | Collections.sort(list); |
Collection总结
-
List(存取有序,有索引,可以重复)
- ArrayList
底层是数组实现的,线程不安全,查找和修改快,增和删比较慢 - LinkedList
底层是链表实现的,线程不安全,增和删比较快,查找和修改比较慢 - Vector
底层是数组实现的,线程安全的,无论增删改查都慢
- ArrayList
如果查找和修改多,用ArrayList
如果增和删多,用LinkedList
如果都多,用ArrayList
-
Set(存取无序,无索引,不可以重复)
- HashSet
底层是哈希算法实现 - LinkedHashSet
底层是链表实现,可以保证元素唯一,存取顺序一致 - TreeSet
底层是二叉树算法实现,可以排序,存储自定义类型时需要注意实现Comparable接口并重写compareTo方法
一般在开发的时候不需要对存储的元素排序,所以在开发的时候大多用HashSet,HashSet的效率比较高.TreeSet在面试的时候比较多
- HashSet
-
Map
- HashMap
底层是哈希算法 - LinkedHashMap
底层是链表,存取顺序一致 - TreeMap
底层是二叉树算法,可以排序
开发中用HashMap比较多
- HashMap
HashCode
HashCode方法的作用
在HashSet中的元素是不能重复的,jvm可以通过equals方法来判断两个对象是否相同,假设自定义一个Person类里面有10个成员变量,每调用一次equals方法需要做10次if判断分别比较这10个成员变量是否相等,如果想HashSet中存放100个对象,那就会做1000次if判断,数据量大的话会严重影响性能。
要解决这个问题的话可以这样做,将一些特征相似或相近的对象归类放到一起给他们一个编号,在做equals判断时,先比较这些编号,编号相同的话再去比较equals,这样可以减少一些比较次数。这个编号可以通过HashCode方法获得。HashCode方法的作用就是将对象进行分类,然后获取到编号值。
举个例子,图书馆里面的书都是分好类的,想找《java编程思想》这本书,先找到计算机类的书架,然后再去找就行,倘若图书馆里面的书籍没有分类,那找起来就如大海捞针。
如何重写HashCode
HashCode算法决定了对象的归类,如果算法编写的不好可能不会对性能有所提升。在编写时最好可以让对象均匀的散列开,这里假设可以将对象分为10个种类,那么每个种类中存放的对象的数量最好不要相差太多。
将Person的name和age属性都加上了,可以将Person进行细分,开发中建议使用:
1 |
|
为什么上面的prime的值是31?其实这个值改成别的也可以,只不过定义为31之后有一些好处:
- 31是一个质数,质数是能被1和自己本身整除的数,并且这个数不大也不小
- 31这个数好算,2的五次方-1,2向左移动5位
关于重写HashCode方法的一些说明
- 任何时候对同一对象多次调用 hashCode 方法,都必须一直返回同样的整数。
- 如果两个对象通过 equals(Object) 方法来比较相等,那么这两个对象的 hashCode的值必须相等。
- 如果两个对象通过 equals(Object) 方法比较结果不等,可以相等也可以不相等。
Properties类
1 | public class PropTest { |
泛型的概念
在编写集合相关代码时在eclipse里面总有一些黄色警告,在不使用注解的情况下,使用泛型之后,就不会有这些黄色警告了。
通过API可以看到Collection,List,ArrayList,这几个类里面都有,这个就是泛型,里面的E可以是任何引用数据类型,使用泛型指明了数据类型之后,这个集合里面只能存储这种数据类型的对象。
不使用泛型时,要进行多次类型强制转换。如List list = new ArrayList();
使用泛型: List<Person> list = new ArrayList<Person>();
泛型的优点
- 可以统一集合中的数据类型,提高安全性
- 可以减少强制类型转换
自定义泛型
通过JDK的源码可以看到,泛型一般写的都是或者,里面的T和E就是表示使用者指定的类型。可以自己定义一个使用泛型的类
泛型通配符
在实际工作当中,有可能通过调用某个方法来接受一个返回值List的数据,这样就不太好确定返回值中的数据类型,这样可以使用泛型通配符<?>
1 | List<?> list = new ArrayList<Integer>();//=号右边可能是通过调用某个方法返回的List |
使用泛型通配符限定子类或者父类
? extends E
向下限定,E及其子类,可以存储当前类型的子类? super E
向上限定,E及其父类,可以存储当前类型的父类
1 | /** |
集合框架中的三种迭代方式删除数据
-
普通for循环,可以删除,注意让索引做自减运算
1
2
3
4
5
6
7//1,普通for循环删除,索引做自减运算
for(int i = 0; i < list.size(); i++) {
if("b".equals(list.get(i))) {
list.remove(i);
i--;
}
} -
迭代器,可以删除,但是必须使用迭代器自身的remove方法,否则会出现并发修改异常
-
增强for循环不能删除
可变参数
注意:如果一个方法有可变参数,并且有多个参数,那么,可变参数肯定是最后一个。
1 | // 修饰符 返回值类型 方法名(数据类型… 变量名){} |
注解:
之前也看过一些Java-Spring Boot的视频, 有些讲的详细的会去讲源码,然后当时就是看到有很多注解, 就不懂是什么意思, 形成了理解障碍。 所以这次特地又去了解了一下
Spring-Boot的入口函数是由@SpringBootApplication
注解的main,无疑@SpringBootApplication
这个注解是非常重要的。
1 |
|
上述一共七个注解,一共可以分成三类: 元注解、配置注解、Component组件注解。之前学的时候就是被这么多注解给吓到了,而且课上对注解的讲解也特别小, 所以一直感觉注解是个高深莫测的东西。
-
元注解讲解: Java 元注解
- @Target: 描述注解的范围,即注解在哪用 —— 最重要的
- CONSTRUCTOR:用于描述构造器
- FIELD:用于描述域即类成员变量
- METHOD:用于描述方法
- PACKAGE:用于描述包
- TYPE:用于描述类、接口(包括注解类型) 或enum声明
- @Retention: 描述注解的生命周期
- @Documented: 标记注解,没有参数
@Inherited
使用该注解的注解父类的子类可以继承父类的注解。请注意,如果使用注释类型来注释除类之外的任何内容,则此元注释类型不起作用。 还要注意,这个元注释只会导致从超类继承注释; 已实现的接口上的注释无效。——比较少用
- @Target: 描述注解的范围,即注解在哪用 —— 最重要的
-
配置注解源码讲解: 尚硅谷SpringBoot顶尖教程(springboot之idea版spring boot)
-
@AutoConfigurationPackage
由@Import({Registrar.class})
注解,其中Registrar
1
2
3public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])); // 后面第二个参数的结果是metadata中的所有包==>即获得主配置类所在包及以下子包
}将主配置类(
@SpringBootApplication
标注的类)所在包及下面子包里面的所有组件扫描到Spring容器中
-
注解(Annontion)是Java5开始引入的新特征。它提供了一种安全的类似注释的机制,用来将任何的信息或元数据(metadata)与程序元素(类、方法、成员变量等)进行关联。==>类似注释,但能将内容传递给程序,对修饰对象有约束作用。
代码demo: 注解Annotation实现原理与自定义注解例子
IO流
1 | //每次调用的时候会读取一个字节的数据,如果read返回结果是-1,则说明读取完毕 |
使用File.separator解决不同系统的路径问题
在windos中的文件路径是以"“来分隔
在linux中的文件路径是以”/"来分隔
如果将上面代码部署到linux中会读取不到文件,为了保证编写的代码跨平台需要使用java.io包下的File.separator来替代文件路径的分隔符,如下:fis = new FileInputStream("file" + File.separator + "monkey.txt");
文件IO读写FileInputStream
1 |
|
使用缓冲流进行文件拷贝BufferedInputStream
Java中提供了BufferedInputStream
和BufferedOutputStream
缓冲流用来读取和写出, BufferedInputStream
读取时会创建一个长度为8192的byte类型数组,程序一次读取8192个字节数据到数组中 使用缓冲流之后就不用再自定义byte类型数组了。
1 |
|
jdk7的新写法
在jdk7中新加入了AutoCloseable接口,IO流中的类都实现了这个接口,这样在读取或者写出操作结束之后,系统会自动close相关资源,开发者不需要再手动close了
1 | try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\Users\\10630\\Desktop\\TODO\\ok.yml")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("auto.txt"));) { |
使用字符流解决乱码问题FileReader
字符流FileReader主要用来读取字符的IO流,使用字符流读取文本文件可以解决乱码问题。
1 |
|
使用缓冲流BufferedReader可以一次读取一行的文字:
1 |
|
写入
1 |
|
- BufferedWriter、BufferedInputStream内的参数都是原有的FileWrite、FileInputStream,实际上使用了装饰模式(设计模式)
- BufferedWriter、BufferedReader多了writeLine、readLine方法
装饰者设计模式的优点:
不用修改被装饰对象的源码,装饰者与被装饰者耦合度不高。
转换流——编码格式转换InputStreamReader:
字节->字符
如果要解决上面问题,需要使用InputStreamReader和OutputStreamWriter指明文本文件的编码,这两个类都属于字符流,可以将字节流输出为字符流。
1 | public static void main(String[] args) { |
上面的代码中再FileInputStream对象上使用了InputStreamReader装饰,从而将字节转换为字符,之后再InputStreamReader对象上又使用了BufferedReader将字符进行缓冲,从而提高。==>都有reader
输出指定目录下的所有文件名称
1 |
|
序列化和反序列化
- 在工作中有可能遇到多台机器远程通信的情况,如果要将机器A中的某个java对象传输到机器B上面,需要将这个java对象转换为字节序列然后进行传输。将对象转换为字节序列的过程叫做序列化,反之叫做反序列化。
- 使用序列化还可以将一个对象保存到硬盘中,然后再通过反序列化将该对象读取到内存里面。
一个对象如果支持序列化,需要实现Serializable的接口,这个接口中没有任何方法,实现该接口后,JVM会给这个对象做特殊待遇
1 | public class SerializableTest { |
在一个类实现Serializable接口后,系统会给每个对象一个序列化版本号,当这个类的源码被修改后,系统会重新分配一个新的序列化版本号,这样做的好处就是保证序列化和反序列化的对象内容一致。例如将一个对象序列化到硬盘之后,修改这个对象所对应类的源码,在进行反序列化是就会报出InvalidClassException异常。如果手动编写序列化版本号之后,就不会出现这个异常了。
1 | /** |
transient关键字
如果不希望将Student类中的age属性序列化,可以使用transient声明该属性,在序列化时将忽略这个属性。transient private int age;
多线程
三种创建方式
-
继承Thread类, 重写run方法
- 优点:可以直接使用Thread类中的方法,代码简单
- 缺点:继承Thread类之后就不能继承其他的类
-
实现runnable接口, 重写run方法
- 优点:即时自定义类已经有父类了也不受影响,因为可以实现多个接口
- 缺点:在run方法内部需要获取到当前线程的Thread对象后才能使用Thread中的方法
-
实现Callable接口创建线程
-
优点:可以获取返回值,可以抛出异常
-
缺点:代码编写较为复杂
1.自定义一个类实现java.util.concurrent包下的Callable接口
2.重写call方法
3.将要在线程中执行的代码编写在call方法中
4.创建ExecutorService
线程池
5.将自定义类的对象放入线程池里面
6.获取线程的返回结果
7.关闭线程池,不再接收新的线程,未执行完的线程不会被关闭
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55//1.定义一个类实现Callable<V>接口
class MyCallable implements Callable<Integer> {
// 2.重写call方法
public Integer call() throws Exception {
// 3.将要执行的代码写在call方法中
//返回一个随机数
Random r = new Random();
int num = r.nextInt(100);
return num;
}
}
public class MultiThread_callable {
public static void main(String[] args) {
//4.创建ExecutorService线程池
ExecutorService exec = Executors.newCachedThreadPool();
//5.将自定义类的对象放入线程池里面
//开启两个线程
Future<Integer> result1 = exec.submit(new MyCallable());
Future<Integer> result2 = exec.submit(new MyCallable());
//判断线程是否计算完毕
while (!result1.isDone() && !result2.isDone()) {
System.out.println("等待线程计算完毕");
}
//6.获取线程的返回结果
Integer i1 = null;
try {
i1 = result1.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
Integer i2 = null;
try {
i2 = result2.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println(i1);
System.out.println(i2);
//7.关闭线程池,不再接收新的线程,未执行完的线程不会被关闭
exec.shutdown();
}
} -
线程池
线程池是初始化一个多线程应用程序过程中创建一个线程集合,即一次创建多个线程,然后在需要执行新的任务时直接去这个线程集合中获取,而不是重新创建一个线程。任务执行结束后,线程放回到池子中等待下一次的分配。
线程池的作用
解决创建单个线程耗费时间和资源的问题。
创建线程池
上面代码中演示了两种方式创建线程池
Executors.newFixedThreadPool(int nThreads);
通过传入的int类型参数来指定创建线程池中的线程数,如果任务数量大于线程数量,则任务会进行等待。Executors.newCachedThreadPool();
会根据需要创建新线程的线程池,如果线程池中的线程数量小于任务数时,会创建新的线程,线程池中的线程最大数量是Integer.MAX_VALUE,int类型的最大值。如果线程的处理速度小于任务的提交速度时,会不断创建新的线程来执行任务,这样有可能会因为创建过多线程而耗尽CPU 和内存资源。
匿名内部类、labmbda表达式创建
1 |
|
synchronized同步方法与同步代码块
1 | public class SleepThread { |
修改后将需要同步的代码放到synchronized代码块中,再次运行SynchronizedTest02类,打印结果是3秒,因为那段耗时较长的代码是在异步情况下运行,所以节省了一些时间。
注意:多个线程在执行synchronized同步代码块时,代码块括号里面可以传入任意对象,但一定要保证多个线程访问的是同一个对象。(这里代码只有一个实例, 这个实例的obj是相同的)
单例模式
饿汉式
- 构造方法私有化
- 创建当前类对象
- 对外提供公共的访问方法将SingletonHungary对象暴露给外部
懒汉式
- 构造方法私有化
- 创建当前类的引用
- 对外提供公共的访问方法将SingletonHungary对象暴露给外部
单例模式的案例Runtime
java.lang包下的Runtime类使用了单例模式,使用该类可以执行windows系统里面的一些命令,例如:mspaint(打开画图软件),shutdown(关机)等等。
1 | public static void main(String[] args) throws IOException { |
使用Timer类来实现定时任务
1 |
|
Lambda表达式和匿名内部类
使用lambda表达式的前提是: 必须为函数式接口(有且只有一个抽象方法的接口,可以用
@FunctionalInterface
,接口中可以包含默认、静态、私有方法)
- 匿名内部类会生成一个
xxxx$1.class
文件, 而lambda表达式不会生成 - lambda 有延迟加载的效果,从而不存在性能浪费——优化日志
From: https://www.bilibili.com/video/BV1A4411K7Gx?p=417
反射
获得字节码class的三种方式
- Source源代码阶段=>
Class.forName("全类名")
- 多用于配i文件,捋类名定义在配文件中。读取文件,加载类
- Class类对象阶段=>
类名.class
- 多用于参数的传递
- Runtime运行阶段=>
对象.getClass()
- 多用于对象的获取字节码的方式
▲结论: 同一个字节码文件(*.class)在一次程序运行过程中,只会被加载一次,不论通过哪一种方式获取的Class对象都是同一个。
1 | // way1: Class.forName |
Class对象功能:
获取功能:
-
获得成员变量
-
Filed[] getFields()
——获得public修饰的字段 -
Field getField(String name)
——获得所有字段,无视修饰符 -
Field[] getDeclaredFields()
-
Field getDeclaredField(String name)
-
-
获得构造方法
-
Constructor constructor = cls1.getConstructor(String name);
-
Constructor[] constructors = cls1.getConstructors();
-
Constructor declaredConstructor = cls1.getDeclaredConstructor(String name);
-
Constructor[] declaredConstructors = cls1.getDeclaredConstructors();
-
-
获得成员方法
-
Method method = cls1.getMethod(String name);
-
Method[] methods = cls1.getMethods();
-
Method declaredMethod = cls1.getDeclaredMethod(String name);
Method[] declaredMethods = cls1.getDeclaredMethods();
-
1 | Class cls1 = Class.forName("top.nymrli.day17_reflect.Person"); |
案例:
*需求:写一个“框架”,不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法
实现:
1.配置文件
2.反射
步骤
1.捋需要创建的对象的全类名和需要执行的方法定义在配置文件中
2.在程序中加载读取配置文件
3.使用反射技术来加载类文件进内存
4.创建对象
5.执行方法
1 |
|
类的加载与初始化
类加载器(ClassLoader)
我们都知道 Java 文件被运行,第一步,需要通过 javac 编译器编译为 class 文件;第二步,JVM 运行 class 文件,实现跨平台。而 JVM 虚拟机第一步肯定是 加载 class 文件,所以,类加载器实现的就是:——通过一个类的全限定名来获取描述此类的二进制字节流(来自《深入理解Java虚拟机》)
类加载器有几个重要的特性:
- 每个类加载器都有自己的预定义的搜索范围,用来加载 class 文件;
- 每个类和加载它的类加载器共同确定了这个类的唯一性,也就是说如果一个 class 文件被不同的类加载器加载到了 JVM 中,那么这两个类就是不同的类,虽然他们都来自同一份 class 文件;
- 双亲委派模型。
类的加载过程:
加载–>链接(验证、准备(为静态量开辟空间并赋予初始值)、解析(将class中的符号引用转变为运行时的地址的直接引用))–>初始化
为类的静态变量赋值,然后执行类的初始化(static)语句
初始化的详细过程:
- 如果类还没有被加载和链接,那就先进行加载和链接
- 如果类存在父类,并且父类还没有初始化,那就先初始化直接父类
- 如果类中存在初始化语句,顺序执行初始化语句
class初始化时机
- 创建类的实例(四种方式)
- 访问类中的某个静态变量,或者对静态变量进行赋值
- 主动调用类的静态方法
- Class.forName(“包类名")完成子类的初始化,也会完成对本类的初始化(接口例外)
- 该类是程序引导入口(mian入口或者test入口)
双亲委派机制
bootstrap的加载过程是用c来完成的,在java中输出bootstrap加载器结果为null
加载类的过程: 不断将加载任务交给父类加载器,是个递归的过程。如果父类能够加载,那么就加载,如果不能加载,那么就交给子类去加载。
- bootstrap:提供核心环境 、extension classloader负责拓展内容、application classloader负责程序运行期间自己写的class对象
1 | protected Class<?> loadClass(String name, boolean resolve) |
作用:
- 避免类的重复加载
- 保护程序安全,防止核心的JAVA语言环境遭受破坏
Class.getResource和ClassLoader.getResource的区别
Class.getResource和ClassLoader.getResource 最终调用的是ClassLoader 类的getResource方法
Class.getResource(String path)
- path不以’/'开头时,默认是从此类所在的包下取资源;
- path 以’/'开头时,则是从ClassPath根下获取;
Class.getClassLoader().getResource(String path)
- path不能以’/'开头时;
- path是从ClassPath根下获取;
正则匹配
字符 | 说明 |
---|---|
. | 匹配除"\r\n"之外的任何单个字符。若要匹配包括"\r\n"在内的任意字符,请使用诸如"[\s\S]"之类的模式。 |
* | 零次或多次匹配前面的字符或子表达式。例如,zo* 匹配"z"和"zoo"。* 等效于 {0,}。 |
+ | 一次或多次匹配前面的字符或子表达式。例如,"zo+"与"zo"和"zoo"匹配,但与"z"不匹配。+ 等效于 {1,}。 |
? | 零次或一次匹配前面的字符或子表达式。例如,"do(es)?“匹配"do"或"does"中的"do”。? 等效于 {0,1}。 |
方法说明:
- matches :尝试将整个区域与模式匹配。
- lookingAt: 方法虽然不需要整句都匹配,但是需要从第一个字符开始匹配。
- find: 尝试查找与该模式匹配的输入序列的下一个子序列。
- find(int start):重置此匹配器,然后尝试查找匹配该模式、从指定索引开始的输入序列的下一个子序列。
- 替换: replaceFirst 替换首次匹配,replaceAll 替换所有匹配。
▲强调: 使用group()
之前一定得指定个以上的匹配方法
匹配模式:
1 | Pattern compile = Pattern.compile(regex, Pattern.CASE_INSENSITIVE); |
Java泛型体系
通配符
上边界extends、下边界super
- 为什么限制参数的范围
- 为了限制参数的写入或者写出权限
? extends T
代表的是泛型可以传入T和T的子类的类型上边界
?super T
代表的是传入的必须是T和T的父类类型下边界
Q:什么时候用上边界什么时候用下边界?——会使容器性质改变
- 上边界: 在读取T这个类型数据的时候,但不写入数据的时候使用上边界——可以看父亲做的事, 但不能改变
- 下边界: 需要写入T这个类型数据的时候,但不获取的时候使用下边界——已经是在教儿子写数据了,他也不知道未来会是什么样子的。
- 如果既要读又要写,则不要使用通配符了, 直接传父类。
请记住PECS原则:生产者(Producer)使用extends,消费者(Consumer)使用super;如果一个列表即要生产,又要消费,则不能使用泛型通配符声明列表,比如List<Integer>
。
https://www.cnblogs.com/cangqinglang/p/11626410.html
MessageFormat
在Python中format使用起来非常方便,而Java中得使用字符串拼接或者占位符都比较麻烦,而且特别是针对占位字符串的问题更是没有好的解决方案,因此找了找Java中类似的功能:一个是String.format另一个是MessageFormat,据了解MessageFormat性能高于String.format,因此直接学MessageFormat使用。
函数原型为:MessageFormat.format(String pattern, Object ... arguments)
MessageFormat模式,指花括号内可填写项
1 | FormatElement: |
- ArgumentIndex必须是非负整数,它的个数不只限于0到9这10个,它可以用0到9的数字组成,因此可以有好多个,如:
- 格式化字符串时,两个单引号才表示一个单引号,单个单引号会被省略,除非中文单引号不会被省略。如果需要显示双引号要进行转义,比如:String msg = “oh, {0} is \”a\” pig”;
- 单引号会使其后面的占位符均失效,导致直接输出占位符。(采坑)
- 花括号的输出:
MessageFormat.format("oh, '{', }},''{0}'' is a pig", "ZhangSan");
,注:右括号可以通过两个}}
or'}'
得到,但是左括号不行,只能通过'{}'
得到 - 因此对于简单的格式化或字符串组装,以及若要多次格式同一个模式的字符串,那么创建一个MessageFormat实例在执行格式化操作比较好些。但要格式化处理更丰富的话要是用 String.format方法.
1 | System.out.println(MessageFormat.format("{0}输出结果{1}", traceLocation, aBoolean)); |
参考:https://blog.csdn.net/GarfieldEr007/article/details/89397843
附录
Q:java 定义long和float为什么要加L和F?
A:整形默认值为int,如果定义long 必须要加L来区分,浮点型默认值为double双精度,定义单精度float要加F来区分。
Java中@SuppressWarnings的作用
A:作用:告诉编译器忽略指定的警告,不用在编译完成后出现警告信息。如@SuppressWarnings("unchecked", "deprecation")
等同于@SuppressWarnings(“unchecked”, “deprecation”)
输出变量类型——Python中type关键字
1 | public static String getType(Object o){ //通过反射来获取变量类型方法 |
▲. 基本数据类型无效, 如int, 但可以查看包装数据类型。
JDBC链接MYSQL
Maven的pom.xml中的配置
1 | "1.0" encoding="UTF-8" xml version= |
java文件内容
1 |
|
DriverManager:驱动管理对象
Connection:数据库连接对象
statement:执行sql的对象
Resultset:结果集对象
Preparedstatement:执行sq1的对象roper
SQL操作
分页操作
语法:limit开始索引,每页查询的记录数
注:索引从0开始
公式:开始索引=(当前页码-1)*每页查询的记录数
即 index = (nowPageNum - 1) * pageSize
插入语句:
insert into `train`.`student` (`name`, `age`) values ("gb", 19);
▲注意这边是`train`.`student`
,如果写成`train.student`
是会找不到表的
try-catch-finally中return
try中没有异常,则顺序为try→finally,如果try中有异常,则顺序为try→catch→finally。但是当try、catch、finally中加入return之后,那么return会变成什么样呢。
1 | public int newT(){ |
1、finally中的代码总会被执行。
2、当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。
3、finally中有return时,会直接在finally中退出,导致try、catch中的return失效。==>最好不要在finally中使用return——IDEA中会报请不要在finally中使用return
动态代理
Q:首先得知道什么是代理?
A:代理模式是Java常见的设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象。
为什么要采用这种间接的形式来调用对象呢?一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。
在现实生活中,这种情形非常的常见,比如请一个律师代理来打官司。
代理模式
代理模式的主要角色如下。
- 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
- 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
- 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。 <----外部接口主要调用对象
从UML图中,可以看出代理类与真正实现的类都是继承了抽象的主题类,这样的好处在于代理类可以与实际的类有相同的方法,可以保证客户端使用的透明性。
代理模式实现三要素
- 有接口定义
- 目标对象与代理对象必须实现统一接口
- 代理对象持有目标对象的引用增强目标对象行为
静态代理和动态代理
代理模式可以有两种实现的方式,一种是静态代理类,另一种是各大框架都喜欢的动态代理。
静态代理
静态代理模式的特点,代理类接受一个实现了Subject接口的对象,由于任何实现该接口的对象都可以通过代理类进行代理,从而增加了通用性。但是也有缺点,每一个代理类都必须实现一遍委托类(也就是real subject)的接口,如果接口增加方法,则代理类也必须跟着修改。其次,代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。
1 | interface Person{ |
静态代理的缺点
虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来(需要实现Subject主题接口的缺点)。
1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:
- 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
- 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类
2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。
动态代理
动态代理有别于静态代理,是根据代理的对象,动态创建代理类。这样,就可以避免静态代理中代理类接口过多的问题。动态代理是实现方式,是通过反射来实现的,1. 可以借助Java自带的java.lang.reflect.Proxy
,通过固定的规则生成。 2. 通过CGLli库来实现
为什么类可以动态的生成?
这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。
Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:
- 通过一个类的全限定名来获取定义此类的二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的
java.lang.Class
对象,作为方法区这个类的各种数据访问入口
由于虚拟机规范对这3点要求并不具体,所以实际的实现是非常灵活的,关于第1点,获取类的二进制字节流(class字节码)就有很多途径:
- 从ZIP包获取,这是JAR、EAR、WAR等格式的基础
- 从网络中获取,典型的应用是 Applet
- 运行时计算生成,这种场景使用最多的是动态代理技术,在 java.lang.reflect.Proxy 类中,就是用了 ProxyGenerator.generateProxyClass 来为特定接口生成形式为
*$Proxy
的代理类的二进制字节流 - 由其它文件生成,典型应用是JSP,即由JSP文件生成对应的Class类
- 从数据库中获取等等
所以,动态代理就是想办法,根据接口或目标对象,计算出代理类的字节码,然后再加载到JVM中使用。但是如何计算?如何生成?情况也许比想象的复杂得多,我们需要借助现有的方案。
常见的字节码操作类库
这里有一些介绍:java-source.net/open-source…
- Apache BCEL (Byte Code Engineering Library):是Java classworking广泛使用的一种框架,它可以深入到JVM汇编语言进行类操作的细节。
- ObjectWeb ASM:是一个Java字节码操作框架。它可以用于直接以二进制形式动态生成stub根类或其他代理类,或者在加载时动态修改类。
- CGLIB(Code Generation Library):是一个功能强大,高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。
- Javassist:是Java的加载时反射系统,它是一个用于在Java中编辑字节码的类库; 它使Java程序能够在运行时定义新类,并在JVM加载之前修改类文件。
实现动态代理的思考方向
为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:
- 通过实现接口的方式 -> JDK动态代理
- 通过继承类的方式 -> CGLIB动态代理
注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦。
JDK动态代理
JDK提供的动态代理实现步骤如下:
- 编写一个委托类的接口,即静态代理的(Subject接口)
- 实现一个真正的委托类,即静态代理的(RealSubject类)
- 创建一个动态代理类,实现
InvocationHandler
接口,并重写该invoke
方法 - 在测试类中,生成动态代理的对象。
1 | interface Subject { |
创建动态代理的对象,需要借助Proxy.newProxyInstance
。该方法的三个参数分别是:
- ClassLoader loader表示当前使用到的appClassloader。
- Class<?>[] interfaces表示目标对象实现的一组接口。
- InvocationHandler h表示当前的InvocationHandler实现实例对象。
此外, 使用JDK实现的代理,还有一个问题:Proxy.newProxyInstance的参数中有一个是委托类的接口,也就是说,▲如果使用JDK实现动态代理,则必须有委托接口(本体必须实现接口),而无法针对没有接口的类。对此情况,CGlib可以解决(是JDK代理的补充,不要求本体实现接口)
CGLib动态代理
什么是CGLib
CGLib是一个强大的、高性能的代码生成库,它可以在运行期扩展Java类与实现Java接口。Hibernate支持它来实现PO(Persistent Object 持久化对象)字节码的动态生成。其被广泛应用于AOP框架(Spring、dynaop)中,用以提供方法拦截操作。例如Spring AOP就是他们提供方法的interception(拦截)。
实现步骤如下:
- 导入CGlib库
- 定义委托类
- 创建代理类:实现一个MethodInterceptor接口,方法调用会被转发到该类重写的intercept()方法中。
- 使用
1 | class User { |
CGLIB 创建动态代理类的模式是:
- 查找目标类上的所有非final 的public类型的方法定义;
- 将这些方法的定义转换成字节码;
- 将组成的字节码转换成相应的代理的class对象;
- 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求
原理:
CGLib采用底层的字节码技术ASM, 可以为一个类创建子类, 在子类中采用方法拦截的技术拦截所有父类方法的调用, 并织入横切逻辑。(代理对象执行目标方法时会调用回调接口的方法即CGDynasticInterceptor的intercept)
流程:我们通过CGLIB
的Enhancer
来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()
方法得到代理对象,对这个代理对象所有非final
方法的调用都会转发给MethodInterceptor.intercept()
方法,在intercept()
方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()
方法,我们将调用转发给原始对象调用其具体方法。CGLIG
中MethodInterceptor
的作用跟JDK动态代理代理中的InvocationHandler
很类似,都是方法调用的中转站。
总结
代理模式实现分类以及对应区别
1.静态代理:手动为目标对象制作代理对象,即在程序编译阶段完成代理对象的创建
2.动态代理:在程序运行期动态创建目标对象对应代理对象。
3.jdk动态代理:被代理目标对象必须实现某一或某一组接口实现方式通过回调创建代理对象。
4.cglib动态代理:被代理目标对象可以不必实现接口,继承的方式实现。
动态代理相比较静态代理,提高开发效率,可以批量化创建代理,提高代码复用率。
我们学习了通过CGLIB实现动态增强,但是CGLIB也有其缺陷,那就是必须目标类必须是可以继承的,如果目标类不可继承,那么我们就无法使用CGLIB来增强该类(因为CGLib是通过继承来实现的),现在我们已经学习完了Spring AOP中两种AOP的实现机制,我们可以称JDK动态代理实现的AOP为面向接口的动态增强
,将CGLIB实现的AOP称为面向子类的动态增强
。
参考:
- ★源代码级别解析JDK动态代理——深入理解Java动态代理
- 基于MAVEN项目的CGLib动态代理原理及实现——https://blog.csdn.net/weixin_46460843/article/details/112649900
- 细说Spring——AOP详解(使用CGLIB实现AOP)——https://zhuanlan.zhihu.com/p/37886319
Author: Mrli
Link: https://nymrli.top/2020/05/15/重拾Java笔记/
Copyright: All articles in this blog are licensed under CC BY-NC-SA 3.0 unless stating additionally.