Mrli
别装作很努力,
因为结局不会陪你演戏。
Contacts:
QQ博客园

重拾Java笔记

2021/12/01 java
Word count: 19,583 | Reading time: 80min

重拾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的文件名没有关系,而是跟类名一致

函数传参:

  • 如果参数是基本数据类型, 是会生成一个新的形参
  • 如果参数是引用数据类型(不包括封装数据类型), 那么会生成该对象的引用(类、 接口类型、 数组类型、 枚举类型、 注解类型、 字符串型)==>引用数据类型变量,调用方法时作为参数是按引用传递

参看: Java中的基本数据类型和引用数据类型的区别

代码块的分类

使用{}括起来的代码被称为代码块,根据其位置和声明的不同可以分为下面4种:

  • 局部代码块,在方法中出现,限定变量生命周期,及早释放,提高内存利用率
  • 构造代码块,在类中方法外出现;多个构造方法方法中相同的代码存放到一起,每次调用构造都执行,并且在构造方法前执行
  • 静态代码块, 在类中方法外出现,并加上static修饰;用于给类进行初始化,在加载的时候就执行,并且只执行一次。一般用于加载驱动。
  • 同步代码块(后面多线程部分会讲解)

执行顺序:

1.静态代码块,随着类加载而加载,且只执行一次
2.构造代码块,每创建一个对象就会执行一次,优先于构造方法执行
3.构造方法,每创建一个对象就会执行一次

this指针

▲注意点: 在构造函数中调用该对象的另一个构造方法时, this(实参)必须写在最前面

1
2
3
4
5
6
7
8
//构造方法
//需求:在创建日期对象的时候,默认的日期是:1970-1-1
MyDate(){
//通过this调用有参的构造方法
this(1970,1,1);//必须出现在第一行,否则将编译报错
//构造方法不能这样调用
//MyDate(1970,1,1);Error
}

继承问题:

静态代码块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(接口中的方法只能使用publicabstract修饰符 )==> 接口只是对一类事物属性和行为的更高次抽象;对修改关闭,对扩展开放,可以说是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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//根据需求规定重写equals方法
//s1.equals(s2);
public boolean equals(Object obj){

if(this==obj){
return true;
}

if(obj instanceof Star){
Star s = (Star)obj;
if(s.id == id && s.name.equals(name)){
return true;
}
}

return false;
}

内部类的分类

内部类,顾名思义就是在一个类的内部声明一个类。内部类主要分为:

  • 静态内部类
  • 匿名内部类
  • 成员内部类
  • 局部内部类

异常的分类

异常: 指的是程序在执行过程中,出现的非正常的情况,最终会导致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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MyException extends Exception{
/**
* 添加一个空参数的构造方法
*/
public MyException(){
super();
}

/**
* 添加一个带异常信息的构造方法
* 查看源码发现,所有的异常类都会有一个带异常信息的构造方法,方法内部会调用父类带异常信息的构造方法,让父类来处理这个异常信息
*/
public MyException(String s){
super(s);
}
}

catch

1
2
3
4
5
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

JVM内存图

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
2
3
4
5
6
7
8
9
10
11
12
List<String> list = new ArrayList<>();
list.add("a");
list.add("b");
list.add("c");
list.add("d");

//当集合转换数组时,数组长度如果是<=集合的size时,转换后的数组长度等于集合的size
//如果数组的长度大于了size,分配的数组长度就和你指定的长度一样
String[] array = list.toArray(new String[3]);
for(String s : array){
System.out.println(s);
}

数组转集合

1
2
3
4
5
6
7
8
9
10
11
12
13
// 注意转换后的集合不能调用其add方法向里面添加数据,否则会报出UnsupportedOperationException

//数组转集合
String[] arr = {"a","b","c"};
//将数组转换成集合
List<String> listArray = Arrays.asList(arr);
//不能添加
//listArray.add("d");
System.out.println(listArray);
//通过这种方式将listArray转换成真正的ArrayList
ArrayList<String> arrayList = new ArrayList<String>(listArray);

//通过Arrays.asList((T… a))的源码可以看到,这里面返回的ArrayList是在Arrays类里面定义的一个内部类,并非java.util包下的ArrayList。

基本数据类型的数组转换成集合,会将整个数组当作一个对象转换,下面程序将会打印出list的对象地址

1
2
3
4
int[] arr = {1,2,3,4,5};            
List<int[]> list = Arrays.asList(arr);
System.out.println(list);
// ==>[[I@282ba1e]

==>▲.将数组转换成集合,数组中的数据必须是引用数据类型

1
2
3
4
Integer[] arr = {11,22,33,44,55};                    
List<Integer> list = Arrays.asList(arr);
System.out.println(list);
// ==>[11, 22, 33, 44, 55]

Collection集合

Set的特点

Set里面存储的元素不能重复,没有索引,存取顺序不一致。

▲.这里需要注意:在向HashSet中存放自定义类型对象时,一定要重写hashCode和equals方法,原因是无重复的话需要比较, 所以得。

TreeSet简介

TreeSet的特点是可以对存放进去的元素进行排序

∴ 使用TreeSet存储自定义类型。这里还是存储之前定义的Person对象,需要实现Comparable接口并且重写compareTo方法,先根据name的字典顺序排序,然后再根据年龄进行排序。

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
package top.nymrli.day07;

import javafx.scene.PerspectiveCamera;
import javafx.util.converter.PercentageStringConverter;

import java.util.*;

/**
* @Program: testIDEA
* @Description: Set测试
* @Author: MrLi
* @Create: 2020-05-16 09:09
**/

public class day07 {
public static void main(String[] args) {
// HashSet<String> hs = new HashSet<>();
// boolean b1 = hs.add("a");
// System.out.println(hs);
// boolean c1 = hs.add("b");
// System.out.println(hs);
// for (String s : hs) {
// System.out.println(s);
// }

// Q1:
// HashSet<Integer> hashSet = new HashSet<>();
// Random random = new Random();
// while (hashSet.size() < 10) {
// int num = random.nextInt(20);
// hashSet.add(num);
// }
//
// for (Integer i : hashSet) {
// System.out.println(i);
// }

// Q2;Treeset
TreeSet<Person> ts = new TreeSet<>();
ts.add(new Person("cl", 30));
ts.add(new Person("qsy", 25));
ts.add(new Person("sxh", 30));
ts.add(new Person("ll", 15));
for (Person p : ts) {
System.out.println(p);
}


}
}


class Person implements Comparable<Person> {
private String name;
private int age;

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}


@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age &&
Objects.equals(getName(), person.getName());
}

@Override
public int hashCode() {
return Objects.hash(getName(), age);
}


@Override
public int compareTo(Person o) {
int nameSame = this.name.compareTo(o.name);
if (nameSame != 0){ // 如果name不相等
// 当compareTo方法返回正数的时候,系统将元素存储到右边,所以集合存取顺序一致
return nameSame;
}else{
return this.age - o.age;
}
}

}

Map接口概述

map中的元素是以键-值的方式存在的,通过键可以获取到值,键是不可以重复的,跟地图比较像,通过一个坐标就可以找到具体的位置。该接口由三个类实现: HashMap / HashTable / AbstractMap

▲与前两个相比,添加元素的函数由add => put

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
public class day08_Map {
public static void main(String[] args) {
HashMap<String, Integer> hashmap = new HashMap<>();
hashmap.put("Cl", 18);
hashmap.put("GJQ", 15);
hashmap.put("SXH", 10);
hashmap.put("HSR", 12);
System.out.println(hashmap);
System.out.println(hashmap.containsValue("CL"));
System.out.println(hashmap.containsKey("GJQ"));
System.out.println("------");
Collection<Integer> values = hashmap.values();
System.out.println(values);
System.out.println("----------");
Integer res = hashmap.remove("SXH");
System.out.println(res);



// Map的遍历
// 方法一
Set<String> keySet = hashmap.keySet();
Iterator<String> iterator = keySet.iterator();
System.out.println(keySet);

while (iterator.hasNext()) {
String key = iterator.next();
System.out.println("Key: " + key + " value: " + hashmap.get(key));
}
System.out.println("---------");
// 上述的代替写法
// for (String key : keySet) {
// System.out.println("Key:" + key + "value: " + hashmap.get(key));
// }

// 方法二
// Map中的键和值被封装成了Entry对象,并存储在Set集合中,通过entrySet()可以获取到这个Set集合。
Set<Map.Entry<String, Integer>> entries = hashmap.entrySet();
// Iterator<Map.Entry<String, Integer>> entriesIterator = entries.iterator();
for (Map.Entry<String, Integer> en : entries) {
System.out.println("Key:" + en.getKey() + " value: " + en.getValue());
}
System.out.println("_______________");
}
}

LinkedHashMap

  • LinkedHashMap的特点:存取顺序一致

TreeMap

  • TreeMap的特点:可以对存储的元素进行排序

HashMap和Hashtable的区别

  • Hashtable是JDK1.0版本出现的,是线程安全的,效率低,不可以存储null键和null值
  • HashMap是JDK1.2版本出现的,是线程不安全,效率高,可以存储null键和null值

Collection工具

1
2
3
4
Collections.sort(list);
Collections.shuffle(list);
Collections.reverse(list);
Collections.binarySearch(list, 6);

Collection总结

  • List(存取有序,有索引,可以重复)

    • ArrayList
      底层是数组实现的,线程不安全,查找和修改快,增和删比较慢
    • LinkedList
      底层是链表实现的,线程不安全,增和删比较快,查找和修改比较慢
    • Vector
      底层是数组实现的,线程安全的,无论增删改查都慢

如果查找和修改多,用ArrayList
如果增和删多,用LinkedList
如果都多,用ArrayList

  • Set(存取无序,无索引,不可以重复)

    • HashSet
      底层是哈希算法实现
    • LinkedHashSet
      底层是链表实现,可以保证元素唯一,存取顺序一致
    • TreeSet
      底层是二叉树算法实现,可以排序,存储自定义类型时需要注意实现Comparable接口并重写compareTo方法

    一般在开发的时候不需要对存储的元素排序,所以在开发的时候大多用HashSet,HashSet的效率比较高.TreeSet在面试的时候比较多

  • Map

    • HashMap
      底层是哈希算法
    • LinkedHashMap
      底层是链表,存取顺序一致
    • TreeMap
      底层是二叉树算法,可以排序

    开发中用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
2
3
4
5
6
7
8
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + age;
result = prime * result + ((name == null) ? 0 : name.hashCode());
return result;
}

为什么上面的prime的值是31?其实这个值改成别的也可以,只不过定义为31之后有一些好处:

  • 31是一个质数,质数是能被1和自己本身整除的数,并且这个数不大也不小
  • 31这个数好算,2的五次方-1,2向左移动5位

关于重写HashCode方法的一些说明

  • 任何时候对同一对象多次调用 hashCode 方法,都必须一直返回同样的整数。
  • 如果两个对象通过 equals(Object) 方法来比较相等,那么这两个对象的 hashCode的值必须相等。
  • 如果两个对象通过 equals(Object) 方法比较结果不等,可以相等也可以不相等。

Properties类

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
public class PropTest {
public static void main(String[] args) throws IOException {
Properties properties = new Properties();
try {
properties.load(new FileReader("store.txt"));
} catch (IOException e) {
e.printStackTrace();
}

Set<String> set = properties.stringPropertyNames();
for (String k : set) {
String value = properties.getProperty(k);
System.out.println(k + "=" + value);
}
}


public static void loadInto() {

}

public static void storeInto() {
FileWriter fileWriter = null;
try {
final Properties properties = new Properties();
properties.setProperty("陈力", "18");
properties.setProperty("SQY", "19");
properties.store(fileWriter, "Save data");
fileWriter = new FileWriter("store.txt");
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
fileWriter.close();
} catch (IOException e) {
e.printStackTrace();
}
}

}
}

泛型的概念

在编写集合相关代码时在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
2
3
4
5
6
7
8
9
10
11
12
/**
* ? extends E 向下限定,E及其子类,可以存储当前类型的子类
* ? super E 向上限定,E及其父类,可以存储当前类型的父类
*/
public class GenericTest03 {
public static void main(String[] args) {
List<Person> personList = new ArrayList<>();
List<Student> studentList = new ArrayList<>();
//因为studentList中存放的Student是Person类的子类,所以可以将studentList放入personList中
personList.addAll(studentList);
}
}

集合框架中的三种迭代方式删除数据

  • 普通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
2
3
4
5
6
7
// 修饰符 返回值类型 方法名(数据类型…  变量名){}
//可变参数其实是一个数组
public static void print(int ... arr) {
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i]);
}
}

注解:

之前也看过一些Java-Spring Boot的视频, 有些讲的详细的会去讲源码,然后当时就是看到有很多注解, 就不懂是什么意思, 形成了理解障碍。 所以这次特地又去了解了一下

Spring-Boot的入口函数是由@SpringBootApplication注解的main,无疑@SpringBootApplication这个注解是非常重要的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// ---
@SpringBootConfiguration
@EnableAutoConfiguration
// ---
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}

上述一共七个注解,一共可以分成三类: 元注解、配置注解、Component组件注解。之前学的时候就是被这么多注解给吓到了,而且课上对注解的讲解也特别小, 所以一直感觉注解是个高深莫测的东西。

  • 元注解讲解: Java 元注解

    • @Target: 描述注解的范围,即注解在哪用 —— 最重要的
      • CONSTRUCTOR:用于描述构造器
      • FIELD:用于描述域即类成员变量
      • METHOD:用于描述方法
      • PACKAGE:用于描述包
      • TYPE:用于描述类、接口(包括注解类型) 或enum声明
    • @Retention: 描述注解的生命周期
    • @Documented: 标记注解,没有参数
    • @Inherited使用该注解的注解父类的子类可以继承父类的注解。请注意,如果使用注释类型来注释除类之外的任何内容,则此元注释类型不起作用。 还要注意,这个元注释只会导致从超类继承注释; 已实现的接口上的注释无效。——比较少用
  • 配置注解源码讲解: 尚硅谷SpringBoot顶尖教程(springboot之idea版spring boot)

    • @AutoConfigurationPackage@Import({Registrar.class})注解,其中Registrar

      1
      2
      3
      public 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
2
3
4
5
6
//每次调用的时候会读取一个字节的数据,如果read返回结果是-1,则说明读取完毕
int temp;//保存当前读取的字节数据
//将读取的数据赋值给temp,然后再判断
while ((temp = f.read()) != -1) {
System.out.print((char)temp);
}

使用File.separator解决不同系统的路径问题

在windos中的文件路径是以"“来分隔
在linux中的文件路径是以”/"来分隔
如果将上面代码部署到linux中会读取不到文件,为了保证编写的代码跨平台需要使用java.io包下的File.separator来替代文件路径的分隔符,如下:fis = new FileInputStream("file" + File.separator + "monkey.txt");

文件IO读写FileInputStream

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

public class copy_io {
public static void main(String[] args) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream("C:\\Users\\10630\\Desktop\\TODO\\ok.yml");
File file = new File("good.txt");
if (!file.exists()) {
boolean newFile = file.createNewFile();
System.out.println("Yes, Create it~");
}
fos = new FileOutputStream("good.txt");


byte[] arr= new byte[1024];
int length;
while ((length = fis.read(arr)) != -1) {
// f.read(arr)和f.read()不一样,
// f.read(arr)会一次性读取arr大小的数据, 然后长度用length来记录读取了多少字符
// write写的时候, 将数组arr中length写入文件
// 在while中输出了length的大小, 为1024, 842
fos.write(arr, 0, length);
System.out.println(length);
}

} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e){
e.printStackTrace();
} finally{
try {
fis.close();
fos.close();
} catch (IOException e) {
e.printStackTrace();
}

}
}
}

使用缓冲流进行文件拷贝BufferedInputStream

Java中提供了BufferedInputStreamBufferedOutputStream缓冲流用来读取和写出, BufferedInputStream读取时会创建一个长度为8192的byte类型数组,程序一次读取8192个字节数据到数组中 使用缓冲流之后就不用再自定义byte类型数组了。

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

public class bufferStream_io {
public static void main(String[] args) throws IOException {
BufferedInputStream ` = null;
BufferedOutputStream bos = null;

try {
bis = new BufferedInputStream(new FileInputStream("C:\\Users\\10630\\Desktop\\TODO\\ok.yml"));
bos = new BufferedOutputStream(new FileOutputStream("test.txt"));
// 其实不存在会自动创建, 不需要下面的代码
File f = new File("text.txt");
if (!f.exists()) {
boolean newFile = f.createNewFile();
System.out.println("创建成功~");
}
int tmp;
while ((tmp = bis.read()) != -1) {
bos.write(tmp);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (bis != null) {
bis.close();
}
if (bos != null) {
bos.close();
}
}
}
}

使用自定义数组和buffer的图解

jdk7的新写法

在jdk7中新加入了AutoCloseable接口,IO流中的类都实现了这个接口,这样在读取或者写出操作结束之后,系统会自动close相关资源,开发者不需要再手动close了

1
2
3
4
5
6
7
8
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("C:\\Users\\10630\\Desktop\\TODO\\ok.yml")); BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("auto.txt"));) {
int tmp;
while ((tmp = bis.read()) != -1) {
bos.write(tmp);
}
} catch (IOException e) {
e.printStackTrace();
}

使用字符流解决乱码问题FileReader

字符流FileReader主要用来读取字符的IO流,使用字符流读取文本文件可以解决乱码问题。

1
2
3
4
5
6
7
8
9
10
11
12
13

public class FileReader_io {
public static void main(String[] args) {
try (FileReader fileReader = new FileReader("G:\\C与C++、\\java\\testIDEA\\src\\testForChinests.txt")) {
int c;
while ((c = fileReader.read()) != -1) {
System.out.print((char) c);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}

使用缓冲流BufferedReader可以一次读取一行的文字:

1
2
3
4
5
6
7
8
9
10

// 读取
try (BufferedReader bufferedReader = new BufferedReader(new FileReader("G:\\C与C++、\\java\\testIDEA\\src\\testForChinests.txt"))) {
String s;
while ((s = bufferedReader.readLine()) != null){
System.out.print(s);
}
} catch (IOException e) {
e.printStackTrace();
}

写入

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

// 写入
try(FileWriter fw = new FileWriter("newword.txt");){
fw.write("我喜欢学习java");
fw.write(32); // 空格
fw.write(97);
} catch (IOException e) {
e.printStackTrace();
}


try(BufferedWriter bw = new BufferedWriter(new FileWriter("newword.txt"));){
bw.write("我喜欢打篮球");
bw.newLine();//换行
bw.write("我喜欢踢足球");
bw.flush();
} catch (IOException e) {
e.printStackTrace();
}
  • BufferedWriter、BufferedInputStream内的参数都是原有的FileWrite、FileInputStream,实际上使用了装饰模式(设计模式)
  • BufferedWriter、BufferedReader多了writeLine、readLine方法

装饰者设计模式的优点:
不用修改被装饰对象的源码,装饰者与被装饰者耦合度不高。

转换流——编码格式转换InputStreamReader:

字节->字符

如果要解决上面问题,需要使用InputStreamReader和OutputStreamWriter指明文本文件的编码,这两个类都属于字符流,可以将字节流输出为字符流。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
// 使用FileInputStream读取文本内容,然后通过InputStreamReader和指定的编码将字符转换为字节
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("utf-8.txt"), "utf-8"));
BufferedWriter bw = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream("gbk.txt"), "gbk"));) {
String msg;
while((msg = br.readLine()) != null){
bw.write(msg);
}
bw.flush();
} catch (UnsupportedEncodingException | FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e1) {
e1.printStackTrace();
}
}

上面的代码中再FileInputStream对象上使用了InputStreamReader装饰,从而将字节转换为字符,之后再InputStreamReader对象上又使用了BufferedReader将字符进行缓冲,从而提高。==>都有reader

输出指定目录下的所有文件名称

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

public class OutputAllFiles {
private static int cnt = 0;

public static void main(String[] args) {
final File file = getFile();
getListFiles(file);
}


public static void getListFiles(File f) {
final File[] files = f.listFiles();
for (int i = 0; i < files.length; i++) {
for (int j = 0; j < cnt; j++) {
System.out.print('\t');
}

System.out.println(files[i]);

if (files[i].isDirectory()) {
cnt++;
getListFiles(files[i]);
cnt--;
}
}

}

public static File getFile() {
System.out.print("请输入要遍历的目录: ");
final Scanner scanner = new Scanner(System.in);
while (true) {
final String next = scanner.nextLine();
// next()不会吸取字符前/后的空格/Tab键,只吸取字符,开始吸取字符(字符前后不算)直到遇到空格/Tab键/回车截止吸取;
// nextLine()吸取字符前后的空格/Tab键,回车键截止。
final File file = new File(next);
if (!file.exists()) {
System.out.println("输出的路径错误, 请重新输入");
} else if (file.isFile()) {
System.out.println("请输入一个文件夹路径");
} else {
return file;
}
}
}
}

序列化和反序列化

  • 在工作中有可能遇到多台机器远程通信的情况,如果要将机器A中的某个java对象传输到机器B上面,需要将这个java对象转换为字节序列然后进行传输。将对象转换为字节序列的过程叫做序列化,反之叫做反序列化。
  • 使用序列化还可以将一个对象保存到硬盘中,然后再通过反序列化将该对象读取到内存里面。

一个对象如果支持序列化,需要实现Serializable的接口,这个接口中没有任何方法,实现该接口后,JVM会给这个对象做特殊待遇

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SerializableTest {
public static void main(String[] args) {
final Student s = new Student("张三");
try (ObjectOutputStream zhangsan = new ObjectOutputStream(new FileOutputStream("zhangsan"))) {
// try()括号中,如果是多句, 则加;, 单句不需要加;
// ObjectOutputStream也是一个装饰模式
zhangsan.writeObject(s);
zhangsan.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}

在一个类实现Serializable接口后,系统会给每个对象一个序列化版本号,当这个类的源码被修改后,系统会重新分配一个新的序列化版本号,这样做的好处就是保证序列化和反序列化的对象内容一致。例如将一个对象序列化到硬盘之后,修改这个对象所对应类的源码,在进行反序列化是就会报出InvalidClassException异常。如果手动编写序列化版本号之后,就不会出现这个异常了。

1
2
3
4
/**
* 自动生成序列化版本号
*/
private static final long serialVersionUID = -716323668524282676L;

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方法
    @Override
    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
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

public class NewWayCreateThread {
/**
* @Description: 匿名内部类
* @Author: MrLi
* @Param: [args]
* @Return: void
* @Date: 2020/5/18
*/
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 1000; i++) {
System.out.println( Thread.currentThread().getName() + " " + i);
System.out.println( getClass() + " " + i);
}
}
}).start();
for (int i = 0; i < 1000; i++) {
System.out.println("main" + i);
}
}

/**
* @Description: labmbda表达式
* @Author: MrLi
* @Param: [args]
* @Return: void
* @Date: 2020/5/18
*/
public static void main(String[] args) {
new Thread(() ->{
for (int i = 0; i < 1000; i++) {
System.out.println(Thread.currentThread().getName() + " " + i);
}
}).start();

for (int i = 0; i < 1000; i++) {
System.out.println("main " + i);
}
}
}

synchronized同步方法与同步代码块

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
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
public class SleepThread {

public static long begin1;
public static long end1;
public static long begin2;
public static long end2;

public static void main(String[] args) {
LongTask ts = new LongTask();
Thread t1 =new Thread(() -> {
begin1 = System.currentTimeMillis();
ts.add();
end1 = System.currentTimeMillis();
});

Thread t2 = new Thread(() -> {
// new Thread(syncValue::add).start();
begin2 = System.currentTimeMillis();
ts.add();
end2 = System.currentTimeMillis();
});

t1.start();
t2.start();


try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}

long begin = 0;
long end = 0;

if(begin1 > begin2){
begin = begin2;
}else{
begin = begin1;
}

if(end1 > end2){
end = end1;
}else{
end = end2;
}

System.out.println("两个线程总共耗时:" + (end -begin) + "ms");
}
}

class LongTask {
private static int num = 0;
Object obj = new Object();

// public synchronized void add() {
// try {
// Thread.sleep(3000L);
// System.out.println("执行耗时任务");
// } catch (InterruptedException e) {
// e.printStackTrace();
// }
// num++;
// System.out.println(num);
// }
//两个线程总共耗时:6001ms

public void add() {
try {
Thread.sleep(3000L);
System.out.println("执行耗时任务");
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (obj){
num++;
System.out.println(num);
}
}
//两个线程总共耗时:3001ms
}

修改后将需要同步的代码放到synchronized代码块中,再次运行SynchronizedTest02类,打印结果是3秒,因为那段耗时较长的代码是在异步情况下运行,所以节省了一些时间。

注意:多个线程在执行synchronized同步代码块时,代码块括号里面可以传入任意对象,但一定要保证多个线程访问的是同一个对象。(这里代码只有一个实例, 这个实例的obj是相同的)

单例模式

饿汉式

  • 构造方法私有化
  • 创建当前类对象
  • 对外提供公共的访问方法将SingletonHungary对象暴露给外部

懒汉式

  • 构造方法私有化
  • 创建当前类的引用
  • 对外提供公共的访问方法将SingletonHungary对象暴露给外部

单例模式的案例Runtime

java.lang包下的Runtime类使用了单例模式,使用该类可以执行windows系统里面的一些命令,例如:mspaint(打开画图软件),shutdown(关机)等等。

1
2
3
4
public static void main(String[] args) throws IOException {
Runtime rt = Runtime.getRuntime();
rt.exec("mspaint");
}

使用Timer类来实现定时任务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

public class TimerTest {
public static void main(String[] args) throws ParseException {
final Timer t = new Timer();
// t.schedule(new TimerTask() {
// @Override
// public void run() {
// final Date date = new Date();
// System.out.println(date);
// }
// }, new SimpleDateFormat("yyyy-MM-dd hh:mm:ss SSS").parse("2017-07-03 18:09:00 000"), 5000);
//第一个参数接收TimerTask对象,即上面创建的MyTimerTask
//第二参数的Date类型是定时任务执行的开始时间
//第三个参数指定定时任务每隔多少毫秒执行一次


t.schedule(new TimerTask() {
@Override
public void run() {
System.out.println(new Date());
}
}, 0, 1000);
}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// way1: Class.forName
Class cls1 = Class.forName("top.nymrli.day17_reflect.Person");
System.out.println(cls1);

// way2:
Class<Person> cls2 = Person.class;
System.out.println(cls2);

// way3:
Person person = new Person();
Class cls3 = person.getClass();
System.out.println(cls3);

System.out.println(cls3 == cls2);
System.out.println(cls1 == cls2);

Class对象功能:

获取功能:

  1. 获得成员变量

    • Filed[] getFields()——获得public修饰的字段

    • Field getField(String name)——获得所有字段,无视修饰符

    • Field[] getDeclaredFields()

    • Field getDeclaredField(String name)

  2. 获得构造方法

    • Constructor constructor = cls1.getConstructor(String name);

    • Constructor[] constructors = cls1.getConstructors();

    • Constructor declaredConstructor = cls1.getDeclaredConstructor(String name);

    • Constructor[] declaredConstructors = cls1.getDeclaredConstructors();

  3. 获得成员方法

    • Method method = cls1.getMethod(String name);

    • Method[] methods = cls1.getMethods();

    • Method declaredMethod = cls1.getDeclaredMethod(String name);

      Method[] declaredMethods = cls1.getDeclaredMethods();

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
Class cls1 = Class.forName("top.nymrli.day17_reflect.Person");
System.out.println(cls1);

Person p = new Person();
Field name = null;
try {
name = cls1.getDeclaredField("name");
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
Object o = null;
try {
name.setAccessible(true); // private私有方法, 暴力反射
o = name.get(p);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(o);
try {
name.set(p, "cl");
o = name.get(p);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
System.out.println(o);

案例:

*需求:写一个“框架”,不能改变该类的任何代码的前提下,可以帮我们创建任意类的对象,并且执行其中任意方法

实现:
1.配置文件
2.反射

步骤
1.捋需要创建的对象的全类名和需要执行的方法定义在配置文件中
2.在程序中加载读取配置文件
3.使用反射技术来加载类文件进内存
4.创建对象
5.执行方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

public class Example {
// public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
public static void main(String[] args) throws Exception {

Properties properties = new Properties();
ClassLoader classLoader = Example.class.getClassLoader();
InputStream is = classLoader.getResourceAsStream("prop.properties");
properties.load(is);

String className = properties.getProperty("className");
String classMethod = properties.getProperty("classMethod");

Class cls = Class.forName(className);
// 直接newInstance在java 9已被弃用
Object o = cls.getDeclaredConstructor().newInstance();

Method method = cls.getMethod(classMethod);
method.invoke(o);
}
}

类的加载与初始化

类加载器(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
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
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 这边是递归的过程,会去找父加载器, 直至parent为null即用bootstrap加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}

if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name);

// this is the defining class loader; record the stats
PerfCounter.getParentDelegationTime().addTime(t1 - t0);
PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}

类加载

作用:

  • 避免类的重复加载
  • 保护程序安全,防止核心的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根下获取;

正则匹配

java正则

字符 说明
. 匹配除"\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
2
3
4
5
6
Pattern compile = Pattern.compile(regex, Pattern.CASE_INSENSITIVE);
/**
private static final int ALL_FLAGS = CASE_INSENSITIVE | MULTILINE |
DOTALL | UNICODE_CASE | CANON_EQ | UNIX_LINES | LITERAL |
UNICODE_CHARACTER_CLASS | COMMENTS;
*/

Java泛型体系

通配符

上边界extends、下边界super

  1. 为什么限制参数的范围
  2. 为了限制参数的写入或者写出权限

? 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
FormatElement:
{ ArgumentIndex }:是从0开始的入参位置索引。
{ ArgumentIndex , FormatType }
{ ArgumentIndex , FormatType , FormatStyle }

FormatType: :指定使用不同的Format子类对入参进行格式化处理。值范围如下:
number:调用NumberFormat进行格式化
date:调用DateFormat进行格式化
time:调用DateFormat进行格式化
choice:调用ChoiceFormat进行格式化

FormatStyle::设置FormatType中使用的格式化样式。值范围如下:
short
medium
long
full
integer
currency
percent
SubformatPattern (子格式模式,形如#.##)

子模式:
String value = MessageFormat.format("oh, {0,number,#.#} is good num", Double.valueOf("3.1415"));
System.out.println(value); // 输出:oh, 3.1 is good num
  • 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
2
3
4
public static String getType(Object o){  //通过反射来获取变量类型方法
return o.getClass().toString(); //使用int类型的getClass()方法
// return o.getClass().getName(); //使用int类型的getClass()方法
}

▲. 基本数据类型无效, 如int, 但可以查看包装数据类型。

JDBC链接MYSQL

Maven的pom.xml中的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.example</groupId>
<artifactId>top.nymrli.jdbc</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<!-- 引入mysql驱动jar包-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.25</version>
</dependency>

</dependencies>


</project>

java文件内容

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

public class JDBCTest {
public static void main(String[] args) throws Exception{
// 1.加载驱动程序, 这个包名是固定的
Class.forName("com.mysql.jdbc.Driver");
// 2.创建连接
String url="jdbc:mysql://localhost:3306/fortest?useSSL=false&serverTimezone=UTC";
String username="root";
String userpwd="cl123123";
Connection conn = DriverManager.getConnection(url,username,userpwd);

// 3.定义SQL语句
String table = "stu";
// String sql = String.format("SELECT * FROM %s;", table);
String sql = String.format("UPDATE %s set age = 50 where name = 'cl';", table);

// 4. 获取执行sql的对象
Statement stat = conn.createStatement();

// 5.执行SQL语句
// ResultSet resultSet = stat.executeQuery(sql);
int res = stat.executeUpdate(sql);
// System.out.println(resultSet);

// 6. 查看执行结果
System.out.println(res);

// 7.释放资源
stat.close();
conn.close();

}
}

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public int newT(){
try{
System.out.println("yes");
return 1;
}catch (Exception e){
System.out.println("error");
}finally { // 就算在try中执行了return 1,这边的finally也会执行,hh最终会被输出
System.out.println("hh");
}
return 2;
}
// 当写catch的时候,最后需要写return 语句,如果没有catch则不需要==>执行不到
public int newT(){
try{
System.out.println("yes");
return 1;
}
finally {
System.out.println("hh");
}
}

总结:

1、finally中的代码总会被执行。

2、当try、catch中有return时,也会执行finally。return的时候,要注意返回值的类型,是否受到finally中代码的影响。

3、finally中有return时,会直接在finally中退出,导致try、catch中的return失效。==>最好不要在finally中使用return——IDEA中会报请不要在finally中使用return

动态代理

Q:首先得知道什么是代理?

A:代理模式是Java常见的设计模式之一。所谓代理模式是指客户端并不直接调用实际的对象,而是通过调用代理,来间接的调用实际的对象

为什么要采用这种间接的形式来调用对象呢?一般是因为客户端不想直接访问实际的对象,或者访问实际的对象存在困难,因此通过一个代理对象来完成间接的访问。
在现实生活中,这种情形非常的常见,比如请一个律师代理来打官司。

代理模式

代理模式的主要角色如下。

  1. 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  2. 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  3. 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。 <----外部接口主要调用对象

代理模式

从UML图中,可以看出代理类与真正实现的类都是继承了抽象的主题类,这样的好处在于代理类可以与实际的类有相同的方法,可以保证客户端使用的透明性。

代理模式实现三要素

  • 有接口定义
  • 目标对象与代理对象必须实现统一接口
  • 代理对象持有目标对象的引用增强目标对象行为

静态代理和动态代理

代理模式可以有两种实现的方式,一种是静态代理类,另一种是各大框架都喜欢的动态代理。

静态代理

静态代理模式的特点,代理类接受一个实现了Subject接口的对象,由于任何实现该接口的对象都可以通过代理类进行代理,从而增加了通用性。但是也有缺点,每一个代理类都必须实现一遍委托类(也就是real subject)的接口,如果接口增加方法,则代理类也必须跟着修改。其次,代理类每一个接口对象对应一个委托对象,如果委托对象非常多,则静态代理类就非常臃肿,难以胜任。

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
interface Person{
void say();
}

class CommonPerson implements Person{
@Override
public void say() {
System.out.println("I am just a normal person");
}
}

class Lawyer implements Person{
private Person commonPerson;
Lawyer(Person p){
commonPerson = p;
}

@Override
public void say() {
System.out.println("I am Lawyer");
commonPerson.say();
System.out.println("end");
}
}

class Speaker{
public static void main(String[] args) {
Lawyer lawyer = new Lawyer(new CommonPerson());
lawyer.say();
}
}

静态代理的缺点

虽然静态代理实现简单,且不侵入原代码,但是,当场景稍微复杂一些的时候,静态代理的缺点也会暴露出来(需要实现Subject主题接口的缺点)。

1、 当需要代理多个类的时候,由于代理对象要实现与目标对象一致的接口,有两种方式:

  • 只维护一个代理类,由这个代理类实现多个接口,但是这样就导致代理类过于庞大
  • 新建多个代理类,每个目标对象对应一个代理类,但是这样会产生过多的代理类

2、 当接口需要增加、删除、修改方法的时候,目标对象与代理类都要同时修改,不易维护。

动态代理

动态代理有别于静态代理,是根据代理的对象,动态创建代理类。这样,就可以避免静态代理中代理类接口过多的问题。动态代理是实现方式,是通过反射来实现的,1. 可以借助Java自带的java.lang.reflect.Proxy,通过固定的规则生成。 2. 通过CGLli库来实现

为什么类可以动态的生成?

这就涉及到Java虚拟机的类加载机制了,推荐翻看《深入理解Java虚拟机》7.3节 类加载的过程。

Java虚拟机类加载过程主要分为五个阶段:加载、验证、准备、解析、初始化。其中加载阶段需要完成以下3件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  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加载之前修改类文件。

实现动态代理的思考方向

为了让生成的代理类与目标对象(真实主题角色)保持一致性,从现在开始将介绍以下两种最常见的方式:

  1. 通过实现接口的方式 -> JDK动态代理
  2. 通过继承类的方式 -> CGLIB动态代理

注:使用ASM对使用者要求比较高,使用Javassist会比较麻烦。

JDK动态代理

JDK提供的动态代理实现步骤如下:

  1. 编写一个委托类的接口,即静态代理的(Subject接口)
  2. 实现一个真正的委托类,即静态代理的(RealSubject类)
  3. 创建一个动态代理类,实现InvocationHandler接口,并重写该invoke方法
  4. 在测试类中,生成动态代理的对象。
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
interface Subject {
void say();
}

class RealSubject implements Subject {
@Override
public void say() {
System.out.println("I am just a normal person");
}
}

class DynasticProxy implements InvocationHandler {
private Object o;

public DynasticProxy(Object o) {
this.o = o;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object invoke = method.invoke(o, args);
return invoke;
}
}

public class Dynastic {
public static void main(String[] args) {
DynasticProxy dynastic_proxy = new DynasticProxy(new RealSubject());
Subject p =(Subject)Proxy.newProxyInstance(Dynastic.class.getClassLoader(),
new Class[]{
Subject.class
}, dynastic_proxy);
p.say();
}
}

创建动态代理的对象,需要借助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(拦截)。

实现步骤如下:

  1. 导入CGlib库
  2. 定义委托类
  3. 创建代理类:实现一个MethodInterceptor接口,方法调用会被转发到该类重写的intercept()方法中。
  4. 使用
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
class User {
public String say(String msg){
System.out.println("早上好"+msg);
return msg;
}
}

public class CGDynastic implements MethodInterceptor {
@Override
// Object为由CGLib动态生成的代理类实例, Method为上文中实体类所调用的被代理的方法引用, Object[]为参数值列表, MethodProxy为生成的代理类对方法的代理引用。 变量名可以修改
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("这里可以插入执行关键代码之前的逻辑");
// 调用代理类实例上的methodProxy方法的父类方法(即实体类SomeService中对应的方法),然后返回目标方法的返回值result,然后实现增强的逻辑
Object o1 = methodProxy.invokeSuper(o, objects);//关键代码:
System.out.println("这里可以插入执行关键代码之后的逻辑");
return o1;
}

public static void main(String[] args) {
// 代理类class文件存入本地磁盘方便我们反编译查看源码
// System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "D:\\code");
Enhancer enhancer = new Enhancer(); // 通过CGLIB动态代理获取代理对象的过程
enhancer.setSuperclass(User.class); // 设置enhancer对象的父类
enhancer.setCallback(new CGDynastic()); // 设置enhancer的回调对象
User user = (User) enhancer.create(); // 创建代理对象
String world = user.say("world"); // 通过代理对象调用目标方法
System.out.println(world);
}
}


// 或者
class CGDynasticInterceptor implements MethodInterceptor {
private Object object;
public CGDynasticInterceptor(Object object) {
this.object = object;
}
@Override
public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("这里可以插入执行关键代码之前的逻辑");
method.invoke(object, args);
System.out.println("这里可以插入执行关键代码之后的逻辑");
return null;
}
}

class CGDynastic {
public static void main(String[] args) {
CGDynasticInterceptor cgDynasticInterceptor = new CGDynasticInterceptor(new User());
User user = (User)Enhancer.create(User.class, cgDynasticInterceptor);
user.say("good");
}
}

CGLIB 创建动态代理类的模式是:

  1. 查找目标类上的所有非final 的public类型的方法定义;
  2. 将这些方法的定义转换成字节码;
  3. 将组成的字节码转换成相应的代理的class对象;
  4. 实现 MethodInterceptor接口,用来处理对代理类上所有方法的请求

原理:

CGLib采用底层的字节码技术ASM, 可以为一个类创建子类, 在子类中采用方法拦截的技术拦截所有父类方法的调用, 并织入横切逻辑。(代理对象执行目标方法时会调用回调接口的方法即CGDynasticInterceptor的intercept)

流程:我们通过CGLIBEnhancer来指定要代理的目标对象、实际处理代理逻辑的对象,最终通过调用create()方法得到代理对象,对这个代理对象所有final方法的调用都会转发给MethodInterceptor.intercept()方法,在intercept()方法里我们可以加入任何逻辑,比如修改方法参数,加入日志功能、安全检查功能等;通过调用MethodProxy.invokeSuper()方法,我们将调用转发给原始对象调用其具体方法。CGLIGMethodInterceptor的作用跟JDK动态代理代理中的InvocationHandler很类似,都是方法调用的中转站。

总结

代理模式实现分类以及对应区别

1.静态代理:手动为目标对象制作代理对象,即在程序编译阶段完成代理对象的创建
2.动态代理:在程序运行期动态创建目标对象对应代理对象
3.jdk动态代理:被代理目标对象必须实现某一或某一组接口实现方式通过回调创建代理对象。
4.cglib动态代理:被代理目标对象可以不必实现接口,继承的方式实现。

动态代理相比较静态代理,提高开发效率,可以批量化创建代理,提高代码复用率。

我们学习了通过CGLIB实现动态增强,但是CGLIB也有其缺陷,那就是必须目标类必须是可以继承的,如果目标类不可继承,那么我们就无法使用CGLIB来增强该类(因为CGLib是通过继承来实现的),现在我们已经学习完了Spring AOP中两种AOP的实现机制,我们可以称JDK动态代理实现的AOP为面向接口的动态增强,将CGLIB实现的AOP称为面向子类的动态增强

参考:

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.

< PreviousPost
IDEA配置——自定义快捷键、生成注释
NextPost >
华为春招4.29笔试题
CATALOG
  1. 1. 重拾Java笔记
    1. 1.1. 命名规范:
    2. 1.2. class与文件名:
    3. 1.3. 函数传参:
    4. 1.4. 代码块的分类
      1. 1.4.1. 执行顺序:
    5. 1.5. this指针
    6. 1.6. 继承问题:
    7. 1.7. 多态:
    8. 1.8. 重写
    9. 1.9. Super关键字:
    10. 1.10. Object类之finalize方法
    11. 1.11. 访问控制权限
      1. 1.11.1. 构造函数的权限问题:
    12. 1.12. Final关键字:
    13. 1.13. static关键字
      1. 1.13.1. static的作用
    14. 1.14. 抽象类的特点
    15. 1.15. 接口:
    16. 1.16. equals
    17. 1.17. 内部类的分类
    18. 1.18. 异常的分类
      1. 1.18.1. JVM是如何处理异常的
      2. 1.18.2. throw和throws
      3. 1.18.3. 自定义异常
      4. 1.18.4. catch
    19. 1.19. 字符串的不可变性
      1. 1.19.1. String类不能被继承
      2. 1.19.2. 字符串(String)的不可变性
      3. 1.19.3. String、StringBuffer、StringBuilder
      4. 1.19.4. 字符串常量池
    20. 1.20. String、StringBuffer、StringBuilder
      1. 1.20.1. StringBuffer
      2. 1.20.2. StringBuilder和StringBuffer的区别
    21. 1.21. 什么是自动拆箱和自动装箱?
    22. 1.22. 集合
      1. 1.22.1. 集合的由来
      2. 1.22.2. 集合类的一些特点
      3. 1.22.3. 区别
    23. 1.23. List两个子类的特点
      1. 1.23.1. ArrayList线程安全的方案
    24. 1.24. 集合数组的互转
    25. 1.25. Collection集合
      1. 1.25.1. Set的特点
      2. 1.25.2. TreeSet简介
      3. 1.25.3. Map接口概述
        1. 1.25.3.1. HashMap和Hashtable的区别
      4. 1.25.4. Collection工具
      5. 1.25.5. Collection总结
    26. 1.26. HashCode
      1. 1.26.1. HashCode方法的作用
      2. 1.26.2. 如何重写HashCode
      3. 1.26.3. 关于重写HashCode方法的一些说明
    27. 1.27. Properties类
    28. 1.28. 泛型的概念
      1. 1.28.1. 泛型的优点
      2. 1.28.2. 自定义泛型
      3. 1.28.3. 泛型通配符
    29. 1.29. 集合框架中的三种迭代方式删除数据
    30. 1.30. 可变参数
    31. 1.31. 注解:
    32. 1.32. IO流
      1. 1.32.1. 使用File.separator解决不同系统的路径问题
      2. 1.32.2. 文件IO读写FileInputStream
      3. 1.32.3. 使用缓冲流进行文件拷贝BufferedInputStream
      4. 1.32.4. jdk7的新写法
    33. 1.33. 使用字符流解决乱码问题FileReader
      1. 1.33.1. 使用缓冲流BufferedReader可以一次读取一行的文字:
      2. 1.33.2. 转换流——编码格式转换InputStreamReader:
    34. 1.34. 输出指定目录下的所有文件名称
    35. 1.35. 序列化和反序列化
      1. 1.35.1. transient关键字
    36. 1.36. 多线程
      1. 1.36.1. 三种创建方式
      2. 1.36.2. 线程池
    37. 1.37. synchronized同步方法与同步代码块
    38. 1.38. 单例模式
      1. 1.38.1. 饿汉式
      2. 1.38.2. 懒汉式
      3. 1.38.3. 单例模式的案例Runtime
    39. 1.39. 使用Timer类来实现定时任务
    40. 1.40. Lambda表达式和匿名内部类
    41. 1.41. 反射
      1. 1.41.1. Class对象功能:
      2. 1.41.2. 案例:
    42. 1.42. 类的加载与初始化
      1. 1.42.0.1. 类加载器(ClassLoader)
    43. 1.42.1. Class.getResource和ClassLoader.getResource的区别
  2. 1.43. 正则匹配
  3. 1.44. Java泛型体系
    1. 1.44.1. 通配符
    2. 1.44.2. 上边界extends、下边界super
  4. 1.45. MessageFormat
  • 2. 附录
    1. 2.1. Q:java 定义long和float为什么要加L和F?
    2. 2.2. Java中@SuppressWarnings的作用
    3. 2.3. 输出变量类型——Python中type关键字
    4. 2.4. JDBC链接MYSQL
      1. 2.4.1. Maven的pom.xml中的配置
      2. 2.4.2. java文件内容
    5. 2.5. SQL操作
      1. 2.5.1. 分页操作
      2. 2.5.2. 插入语句:
    6. 2.6. try-catch-finally中return
    7. 2.7. 动态代理
      1. 2.7.1. 代理模式
      2. 2.7.2. 静态代理和动态代理
        1. 2.7.2.1. 静态代理
        2. 2.7.2.2. 动态代理
          1. 2.7.2.2.1. JDK动态代理
          2. 2.7.2.2.2. CGLib动态代理
        3. 2.7.2.3. 总结