反射机制概述

是一套类库

  1. 反射机制是JDK中的一套类库,这套类库可以帮助我们操作/读取 class 字节码文件

  2. 后期学习的大量的java框架,底层都是基于反射机制实现的,所以必须掌握(要能够数量的使用反射机制中的方法)。

  3. 反射机制可以让程序更加灵活。怎么灵活????

  4. 反射机制最核心的几个类:

1
2
3
4
java.lang.Class:Class类型的实例代表硬盘上的某个class文件。或者说代表某一种类型。
java.lang.reflect.Filed:Filed类型的实例代表类中的属性/字段
java.lang.reflect.Constructor: Constructor类型的实例代表类中的构造方法
java.lang.reflect.Method: Method类型的实例代表类中的方法

反射机制的缺点:容易打破封装

获取Class

  1. 在java语言中获取Class的三种方式:

==某种类型的字节码文件在内存当中只有一份==

1
2
3
4
5
6
7
8
9
10
11
12
13
// 获取 User 类型
Class userClass = Class.forName("com.powernode.javase.reflect.User");

String s1 = "动力节点";
Class stringClass2 = s1.getClass();

// 某种类型的字节码文件在内存当中只有一份。
// stringClass 和 stringClass2 都代表了同一种类型:String类型
System.out.println(stringClass == stringClass2); // true

User user = new User("zhangsan", 20);
Class userClass2 = user.getClass();
System.out.println(userClass2 == userClass); // true

第一种方式:Class c = Class.forName(“完整的全限定类名”);

注意:

  • 1.全限定类名是带有包名的。
  • 2.是lang包下的,java.lang也不能省略。
  • 3.这是个字符串参数。
  • 4.如果这个类根本不存在,运行时会报异常:java.lang.ClassNotFoundException
  • 5.这个方法的执行会导致类的加载动作的发生。、
1
2
3
4
5
6
// stringClass 就代表 String类型。
// stringClass 就代表硬盘上的 String.class文件。
Class stringClass = Class.forName("java.lang.String");

// 获取 User 类型
Class userClass = Class.forName("com.powernode.javase.reflect.User");

第二种方式:Class c = obj.getClass();

注意:这个方法是通过引用去调用的。

1
2
3
4
5
6
7
String s1 = "动力节点";
Class stringClass2 = s1.getClass();

// 某种类型的字节码文件在内存当中只有一份。
// stringClass 和 stringClass2 都代表了同一种类型:String类型
System.out.println(stringClass == stringClass2); // true

第三种方式:在java语言中,任何一种类型,包括基本数据类型,都有 .class 属性。用这个属性可以获取Class实例。

1
2
3
4
5
// intClass 代表的就是基本数据类型 int类型
Class intClass = int.class;
Class doubleClass = double.class;
Class stringClass3 = String.class;
Class userClass3 = User.class;

第四种方式:①通过类加载器获取

1
2
3
ClassLoader classLoader = ClassLoader.getSystemClassLoader();

Class clazz = classLoader.loadClass(“全限定类名”);

类加载器也是对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 获取类加载器对象(获取的是 系统类加载器/应用类加载器 )
ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();

// jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c
// 这个类加载器是负责加载 classpath 中的字节码文件的。
//System.out.println(systemClassLoader);

// 加载类:但是这个加载过程只是将类加载过程中的前两步完成了,第三步的初始化没做。
// 什么时候做初始化?在这个类真正的被第一次使用的时候。
Class<?> aClass = systemClassLoader.loadClass("com.powernode.javase.reflect.User");

System.out.println(aClass.newInstance());

// 这种方式会走完类加载的全部过程,三步齐全
//Class clazz = Class.forName("com.powernode.javase.reflect.User");

Class.forName和classLoader.loadClass()的区别?

==Class.forName():类加载时会进行初始化。==

==classLoader.loadClass():类加载时不会进行初始化,直到第一次使用该类。==

反射作用的体现

实例化对象

1
2
3
4
5
6
7
8
// 获取到Class类型的实例之后,可以实例化对象
// 通过反射机制实例化对象
Class userClass = Class.forName("com.powernode.javase.reflect.User"); // userClass 代表的就是 User类型。

// 通过userClass来实例化User类型的对象
// 底层实现原理是:调用了User类的无参数构造方法完成了对象的实例化。
// 要使用这个方法实例化对象的话,必须保证这个类中是存在无参数构造方法的。如果没有无参数构造方法,则出现异常:java.lang.InstantiationException
User user = (User)userClass.newInstance();

读取属性配置文件

==读取属性配置文件,获取类名,通过反射机制实例化对象。==

通过这个案例的演示就知道反射机制是灵活的。这个程序可以做到对象的动态创建。

只要修改属性配置文件就可以完成不同对象的实例化。

classObj.newInstance();已经过时

1
2
3
4
5
6
7
8
9
10
11
12
13
// 资源绑定器
ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.javase.reflect.classInfo");
//上面这个是一个属性的配置文件
// 通过key获取value
String className = bundle.getString("className");

// 通过反射机制实例化对象
Class classObj = Class.forName(className);

// 实例化
Object obj = classObj.newInstance();

System.out.println(obj);

反射Field

==关于反射机制中的 java.lang.reflect.Field(代表的是一个类中的字段/属性)==

Field[] fields = vipClass.getDeclaredFields();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Vip {
// Field
public String name;

private int age;

protected String birth;

boolean gender;

public static String address = "北京海淀";

public static final String GRADE = "金牌";
}
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
// 获取Vip类
Class vipClass = Class.forName("com.powernode.javase.reflect.Vip");

/*// 获取Vip类中所有 public 修饰的属性/字段
Field[] fields = vipClass.getFields();
System.out.println(fields.length);

// 遍历数组
for(Field field : fields){
System.out.println(field.getName());
}*/

// 获取Vip类中所有的属性/字段,包括私有的
Field[] fields = vipClass.getDeclaredFields();

for(Field field : fields){
// 获取属性名
System.out.println(field.getName());
// 获取属性类型
Class fieldType = field.getType();
// 获取属性类型的简单名称(不带包名的)
System.out.println(fieldType.getSimpleName());
// 获取属性的修饰符
//System.out.println(field.getModifiers());
System.out.println(Modifier.toString(field.getModifiers()));
}

==System.out.println(Modifier.toString(field.getModifiers()));==

反编译(反射)类的字段

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
// 获取String类
Class stringClass = Class.forName("com.powernode.javase.reflect.Vip");

// 字符串拼接
StringBuilder sb = new StringBuilder();

// 获取类的修饰符
sb.append(Modifier.toString(stringClass.getModifiers()));

sb.append(" class ");

//sb.append(stringClass.getSimpleName());
sb.append(stringClass.getName());

sb.append(" extends ");

// 获取当前类的父类
sb.append(stringClass.getSuperclass().getName());

// 获取当前类的实现的所有接口
Class[] interfaces = stringClass.getInterfaces();
if(interfaces.length > 0){
sb.append(" implements ");
for (int i = 0; i < interfaces.length; i++) {
Class interfaceClass = interfaces[i];
sb.append(interfaceClass.getName());
if(i != interfaces.length - 1){
sb.append(",");
}
}
}

sb.append("{\n");

// 获取所有属性
Field[] fields = stringClass.getDeclaredFields();
for (Field field : fields){
sb.append("\t");
sb.append(Modifier.toString(field.getModifiers()));
sb.append(" ");
sb.append(field.getType().getName());
sb.append(" ");
sb.append(field.getName());
sb.append(";\n");
}

sb.append("}");

// 输出
System.out.println(sb);

通过反射机制如何访问Field,如何给属性赋值,如何读取属性的值

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
// 如果不使用反射机制,怎么访问对象的属性?
Customer customer = new Customer();

// 修改属性的值(set动作)
// customer要素一
// name要素二
// "张三"要素三
customer.name = "张三";

// 读取属性的值(get动作)
// 读取哪个对象的哪个属性
System.out.println(customer.name);

// 如果使用反射机制,怎么访问对象的属性?
// 获取类
Class clazz = Class.forName("com.powernode.javase.reflect.Customer");

// 获取对应的Field
Field ageField = clazz.getDeclaredField("age");

// 调用方法打破封装
ageField.setAccessible(true);
//是的age这个field是可以被获得的

// 修改属性的值
// 给对象属性赋值三要素:给哪个对象 的 哪个属性 赋什么值
ageField.set(customer, 30);
//属性 对象 值

// 读取属性的值
System.out.println("年龄:" + ageField.set(customer, 30););

// 通过反射机制给name属性赋值,和读取name属性的值
Field nameField = clazz.getDeclaredField("name");
// 修改属性name的值
nameField.set(customer, "李四");
// 读取属性name的值
System.out.println(nameField.get(customer));

1.获取类

1
Class clazz = Class.forName("com.powernode.javase.reflect.Customer");

2.获取相对于的field

1
Field ageField = clazz.getDeclaredField("age");

3.调用方法打破封装

1
2
3
// 调用方法打破封装
ageField.setAccessible(true);
//是的age这个field是可以被获得的

4.修改属性的值

1
ageField.set(customer, 30);

5.读取属性的值

1
ageField.get(customer);

反射Method

反编译(反射)类的Method

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 UserService {

/*public void login(){

}*/

/**
* 登录系统的方法
* @param username 用户名
* @param password 密码
* @return true表示登录成功,false表示失败
*/
public boolean login(String username, String password){
/*if("admin".equals(username) && "123456".equals(password)){
return true;
}
return false;*/
return "admin".equals(username) && "123456".equals(password);
}

public String concat(String s1, String s2, String s3){
return s1 + s2 + s3;
}

/**
* 退出系统的方法
*/
public void logout(){
System.out.println("系统已安全退出!");
}
}
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
// 获取类
Class clazz = Class.forName("com.powernode.javase.reflect.UserService");

// 获取所有的方法,包含私有的方法
Method[] methods = clazz.getDeclaredMethods();
//System.out.println(methods.length);

// 遍历数组
for(Method method : methods){
// 方法修饰符
System.out.println(Modifier.toString(method.getModifiers()));
// 方法返回值类型
System.out.println(method.getReturnType().getName());
// 方法名
System.out.println(method.getName());
// 方法的参数列表
/*Class<?>[] parameterTypes = method.getParameterTypes();
for (Class parameterType : parameterTypes){
System.out.println(parameterType.getName());
}*/

Parameter[] parameters = method.getParameters();
for (Parameter parameter : parameters){
System.out.println(parameter.getType().getName());
System.out.println(parameter.getName());// arg0, arg1, arg2......
}
}
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
/**
* 反射一个类中所有的方法,然后进行拼接字符串。
*/
public class ReflectTest09 {
public static void main(String[] args) throws Exception{
StringBuilder sb = new StringBuilder();
Class stringClass = Class.forName("com.powernode.javase.reflect.UserService");
// 获取类的修饰符
sb.append(Modifier.toString(stringClass.getModifiers()));
sb.append(" class ");
// 获取类名
sb.append(stringClass.getName());
// 获取父类名
sb.append(" extends ");
sb.append(stringClass.getSuperclass().getName());
// 获取父接口名
Class[] interfaces = stringClass.getInterfaces();
if(interfaces.length > 0){
sb.append(" implements ");
for (int i = 0; i < interfaces.length; i++) {
sb.append(interfaces[i].getName());
if(i != interfaces.length - 1){
sb.append(",");
}
}
}
sb.append("{\n");

// 类体
// 获取所有的方法
Method[] methods = stringClass.getDeclaredMethods();
for(Method method : methods){
sb.append("\t");
// 追加修饰符
sb.append(Modifier.toString(method.getModifiers()));
// 追加返回值类型
sb.append(" ");
sb.append(method.getReturnType().getName());
// 追加方法名
sb.append(" ");
sb.append(method.getName());
sb.append("(");
// 追加参数列表
Parameter[] parameters = method.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
sb.append(parameter.getType().getName());
sb.append(" ");
sb.append(parameter.getName());
if(i != parameters.length - 1){
sb.append(",");
}
}
sb.append("){}\n");
}

sb.append("}");

// 输出
System.out.println(sb);

}
}

用反射调用类的方法

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
// 不使用反射机制怎么调用方法?
// 创建对象
UserService userService = new UserService();

// 调用方法
// 分析:调用一个方法需要几个要素?四要素
// 调用哪个对象的哪个方法,传什么参数,返回什么值
boolean isSuccess = userService.login("admin", "123456");
System.out.println(isSuccess ? "登录成功" : "登录失败");

// 调用方法
userService.logout();



/******************************************************************************************************/

// 通过反射机制调用login方法
// 获取Class
Class clazz = Class.forName("com.powernode.javase.reflect.UserService");

// 获取login方法,提供方法名,提供形参列表
Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);

// 调用login方法
Object retValue = loginMethod.invoke(userService, "admin", "123456");
System.out.println(retValue);

// 调用logout方法
Method logoutMethod = clazz.getDeclaredMethod("logout");
logoutMethod.invoke(userService);
// 获取Class
Class clazz = Class.forName("com.powernode.javase.reflect.UserService");

1.获取login方法(提供方法名,提供形参列表)

1
Method loginMethod = clazz.getDeclaredMethod("login", String.class, String.class);

2.调用login方法()

1
2
3
//返回值            方法                对象         形参1     形参2
Object retValue = loginMethod.invoke(userService, "admin", "123456");
System.out.println(retValue);

3.调用logout方法

1
2
Method logoutMethod = clazz.getDeclaredMethod("logout");
logoutMethod.invoke(userService);

反射Constructor

通过反射机制获取一个类中所有的构造方法

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
	StringBuilder sb = new StringBuilder();
// 获取类
Class clazz = Class.forName("java.lang.String");
// 类的修饰符
sb.append(Modifier.toString(clazz.getModifiers()));
sb.append(" class ");
// 类名
sb.append(clazz.getName());
sb.append(" extends ");
// 父类名
sb.append(clazz.getSuperclass().getName());
// 实现的接口
Class[] interfaces = clazz.getInterfaces();
if(interfaces.length > 0) {
sb.append(" implements ");
for (int i = 0; i < interfaces.length; i++) {
sb.append(interfaces[i].getName());
if(i != interfaces.length - 1){
sb.append(",");
}
}
}
sb.append("{\n");

//类体
// 获取所有的构造方法
Constructor[] cons = clazz.getDeclaredConstructors();
// 遍历所有的构造方法
for(Constructor con : cons){
sb.append("\t");
// 构造方法修饰符
sb.append(Modifier.toString(con.getModifiers()));
sb.append(" ");
// 构造方法名
sb.append(con.getName());
sb.append("(");

// 构造方法参数列表
Parameter[] parameters = con.getParameters();
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
sb.append(parameter.getType().getName());
sb.append(" ");
sb.append(parameter.getName());
if(i != parameters.length - 1){
sb.append(",");
}
}

sb.append("){}\n");
}

sb.append("}");

System.out.println(sb);
}

通过反射机制调用构造方法来创建对象

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
// 不使用反射机制的时候,怎么创建的对象?
Order order1 = new Order();
System.out.println(order1);

Order order2 = new Order("1111122222", 3650.5, "已完成");
System.out.println(order2);



/***************************************************************************************************/

// 通过反射机制来实例化对象?
Class clazz = Class.forName("com.powernode.javase.reflect.Order");
// 这种方式依赖的是必须有一个无参数构造方法。如果没有会出现异常!
// 在Java9的时候,这个方法被标注了已过时。不建议使用了。
/*Object obj = clazz.newInstance();
System.out.println(obj);*/

// 获取Order的无参数构造方法
Constructor defaultCon = clazz.getDeclaredConstructor();
// 调用无参数构造方法实例化对象
Object obj = defaultCon.newInstance();
System.out.println(obj);

// 获取三个参数的构造方法
Constructor threeArgsCon = clazz.getDeclaredConstructor(String.class, double.class, String.class);
// 调用三个参数的构造方法
Object obj1 = threeArgsCon.newInstance("5552454222", 6985.0, "未完成");
System.out.println(obj1);

1. 通过反射机制来实例化对象?

1
Class clazz = Class.forName("com.powernode.javase.reflect.Order");

2. 获取Order的无参数构造方法

1
Constructor defaultCon = clazz.getDeclaredConstructor();

3. 获取三个参数的构造方法

1
Constructor threeArgsCon = clazz.getDeclaredConstructor(String.class, double.class, String.class);

4.调用无参数构造方法实例化对象

1
2
Object obj = defaultCon.newInstance();
System.out.println(obj);

5.调用三个参数的构造方法

1
2
Object obj1 = threeArgsCon.newInstance("5552454222", 6985.0, "未完成");
System.out.println(obj1);

模拟框架的部分实现

config.properties

1
2
3
4
className=com.powernode.javase.reflect.UserService
methodName=concat
parameterTypes=java.lang.String,java.lang.String,java.lang.String
parameterValues=abc,def,xyz

模拟框架的部分代码。通过读取属性配置文件,获取类信息,方法信息,然后通过反射机制调用方法。

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
// 读取属性配置文件
ResourceBundle bundle = ResourceBundle.getBundle("com.powernode.javase.reflect.config");
String className = bundle.getString("className");
String methodName = bundle.getString("methodName");
String parameterTypes = bundle.getString("parameterTypes");
String parameterValues = bundle.getString("parameterValues");

// 通过反射机制调用方法
// 创建对象(依赖无参数构造方法)
Class<?> clazz = Class.forName(className);
Constructor<?> defaultCon = clazz.getDeclaredConstructor();
Object obj = defaultCon.newInstance();

// 获取方法
// java.lang.String,java.lang.String
String[] strParameterTypes = parameterTypes.split(",");
Class[] classParameterTypes = new Class[strParameterTypes.length];
for (int i = 0; i < strParameterTypes.length; i++) {
classParameterTypes[i] = Class.forName(strParameterTypes[i]);
}
Method method = clazz.getDeclaredMethod(methodName, classParameterTypes);

// 调用方法
// parameterValues=admin,123456
Object retValue = method.invoke(obj, parameterValues.split(","));

System.out.println(retValue);

类加载及双亲委派机制

类的加载过程

image-20240304215603299

image-20240304215317903

①装载(loading)

​ 类加载器负责将类的class文件读入内存,并创建一个java.lang.Class对象

②连接(linking)

1.验证(Verify)

1
确保加载类的信息符合JVM规范。

2.准备(Prepare)

1
2
3
4
5
正式为静态变量在方法区中开辟存储空间并设置默认值

public static int k = 10; 此时:k会赋值0

public static final int f = 10; 此时: f会赋值10

3.解析(Resolve)

1
将虚拟机常量池内的符号引用替换为直接引用(地址)的过程。

③初始化(initialization)

1
静态变量赋值,静态代码块执行

低版本的JDK中类加载器的名字:

​ 启动类加载器:负责加载rt.jar

​ 扩展类加载器:ext/*.jar

​ 系统类加载器:classpath

image-20240304215407135

类加载器

①虚拟机内部提供了三种类加载器(Java9+):

  • 启动类加载器(BootstrapClassLoader):加载Java最核心的类,例如String
  • 平台类加载器(PlatformClassLoader):加载Java平台扩展的类库,例如解析XML的
  • 应用类加载器(AppClassLoader):加载classpath中的自己写的 (系统类加载器/应用类加载器 )
  • 同时我们还可以自定义一个类加载器(UserClassLoader)用户类加载器

②获取类加载器可以通过 getParent()方法一级一级获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 通过自定义的类获取的类加载器是:应用类加载器。
ClassLoader appClassLoader = ReflectTest15.class.getClassLoader();
System.out.println("应用类加载器:" + appClassLoader);

// 获取应用类加载器
ClassLoader appClassLoader2 = ClassLoader.getSystemClassLoader();
System.out.println("应用类加载器:" + appClassLoader2);

// 获取应用类加载器
ClassLoader appClassLoader3 = Thread.currentThread().getContextClassLoader();
System.out.println("应用类加载器:" + appClassLoader3);




/*******************************************************************************************/
// 通过 getParent() 方法可以获取当前类加载器的 “父 类加载器”。
// 获取平台类加载器。
System.out.println("平台类加载器:" + appClassLoader.getParent());
/***********************************************************************************************/
// 获取启动类加载器。
// 注意:启动类加载器负责加载的是JDK核心类库,这个类加载器的名字看不到,直接输出的时候,结果是null。
System.out.println("启动类加载器:" + appClassLoader.getParent().getParent())

启动类加载器

1
System.out.println("启动类加载器:" + appClassLoader.getParent().getParent())

平台类加载器

1
System.out.println("平台类加载器:" + appClassLoader.getParent());

应用类加载器

1
2
3
ClassLoader appClassLoader = ReflectTest15.class.getClassLoader();
ClassLoader appClassLoader2 = ClassLoader.getSystemClassLoader();
ClassLoader appClassLoader3 = Thread.currentThread().getContextClassLoader();

双亲委派机制

image-20240304221342040

①某个类加载器接收到加载类的任务时,通常委托给“父 类加载”完成加载。

②最“父 类加载器”无法加载时,一级一级向下委托加载任务。

③作用:

​ 保护程序的安全。

​ 防止类加载重复。

通过反射获取父类的泛型

1.Animal

1
2
3
4
5
6
7
8
/**
* 在类上定义泛型
* @param <X>
* @param <Y>
* @param <Z>
*/
public class Animal<X, Y, Z> {
}

2.cat

1
2
public class Cat extends Animal<String, Integer, Double>{
}

3.获取父类的泛型(x,y,z)Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();

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
/**
* 获取父类的泛型信息
*/
public class Test {
public static void main(String[] args) {
// 获取类
Class<Cat> catClass = Cat.class;

// 获取当前类的父类泛型
Type genericSuperclass = catClass.getGenericSuperclass();
//System.out.println(genericSuperclass instanceof Class);//true
//System.out.println(genericSuperclass instanceof ParameterizedType);//false


// 如果父类使用了泛型
if(genericSuperclass instanceof ParameterizedType){
// 转型为参数化类型
ParameterizedType parameterizedType = (ParameterizedType) genericSuperclass;
// 获取泛型数组
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
// 遍历泛型数组
for(Type a : actualTypeArguments){
// 获取泛型的具体类型名
System.out.println(a.getTypeName());
}
}
}
}

通过反射获取接口的泛型

1.flyable

1
2
public interface Flyable<X, Y> {
}

2.Mouse

1
2
3
4
5
6
public class Mouse implements Flyable<String, Integer>, Comparable<Mouse>{
@Override
public int compareTo(Mouse o) {
return 0;
}
}

3.获取Type[] genericInterfaces = mouseClass.getGenericInterfaces();

1
2
3
4
5
6
7
8
9
10
11
12
13
Class<Mouse> mouseClass = Mouse.class;
// 获取接口上的泛型
Type[] genericInterfaces = mouseClass.getGenericInterfaces();
for (Type g : genericInterfaces) {
// 使用了泛型
if(g instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) g;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}

通过反射获取属性的泛型

1.user

1
2
3
public class User {
private Map<Integer, String> map;
}

2.获取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Test {
public static void main(String[] args) throws Exception{
// 获取这个类
Class<User> userClass = User.class;
// 获取属性上的泛型,需要先获取到属性
Field mapField = userClass.getDeclaredField("map"); // 获取公开的以及私有的
// 获取这个属性上的泛型
Type genericType = mapField.getGenericType();
// 用泛型了
if(genericType instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) genericType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}
}

通过反射获取方法参数的泛型

1.Myclass

1
2
3
4
5
6
7
public class MyClass {

public Map<Integer, Integer> m(List<String> list, List<Integer> list2){
return null;
}

}

2.获取

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
// 获取类
Class<MyClass> myClassClass = MyClass.class;

// 获取方法
Method mMethod = myClassClass.getDeclaredMethod("m", List.class, List.class);

// 获取方法参数上的泛型
Type[] genericParameterTypes = mMethod.getGenericParameterTypes();
for(Type g : genericParameterTypes){
// 如果这个参数使用了泛型
if(g instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) g;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}

// 获取方法返回值上的泛型
Type genericReturnType = mMethod.getGenericReturnType();
if(genericReturnType instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) genericReturnType;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}

通过反射获取构造函数的泛型

1
2
public User(Map<String ,Integer> map){
}
1
2
3
4
5
6
7
8
9
10
11
12
Class<User> userClass = User.class;
Constructor<User> con = userClass.getDeclaredConstructor(Map.class);
Type[] genericParameterTypes = con.getGenericParameterTypes();
for(Type g :genericParameterTypes){
if(g instanceof ParameterizedType){
ParameterizedType parameterizedType = (ParameterizedType) g;
Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();
for(Type a : actualTypeArguments){
System.out.println(a.getTypeName());
}
}
}

image-20240303154755001

路线图

线程概述(线程安全和线程通信)

什么是进程?什么是线程?它们的区别?

1.进程是指操作系统中的一段程序,它是一个正在执行中的程序实例,具有独立的内存空间和系统资源,如文件、网络端口等。在计算机程序执行时,先创建进程,再在进程中进行程序的执行。一般来说,一个进程可以包含多个线程。

可以理解为是一个软件

2.线程是指进程中的一个执行单元,是进程的一部分,它负责在进程中执行程序代码。每个线程都有自己的栈和程序计数器,并且可以共享进程的资源。多个线程可以在同一时刻执行不同的操作,从而提高了程序的执行效率。

3.现代的操作系统是支持多进程的,也就是可以启动多个软件,一个软件就是一个进程。称为:多进程并发。

4.通常一个进程都是可以启动多个线程的。称为:多线程并发。

多线程的作用?

1.提高处理效率。(多线程的优点之一是能够使 CPU 在处理一个任务时同时处理多个线程,这样可以充分利用 CPU 的资源,提高 CPU 的利用效率。)

JVM规范中规定:

1.堆内存、方法区 是线程共享的。

2.虚拟机栈、本地方法栈、程序计数器 是每个线程私有的。

image-20240303155758392

堆和方法区都是线程共享的,Java虚拟机栈是每个线程都有的,如果有10个线程,就有10个jvmstacks,还有一个主线程的方法,一共11个。

10个线程 ,每个线程也都有一个属于自己的本地方法栈。

局部变量,独立的,

关于Java程序的运行原理

1.“java HelloWorld”执行后,会启动JVM,JVM的启动表示一个进程启动了。

2.JVM进程会首先启动一个主线程(main-thread),主线程负责调用main方法。因此main方法是在主线程中运行的。

3.除了主线程之外,还启动了一个垃圾回收线程。因此启动JVM,至少启动了两个线程。

4.在main方法的执行过程中,程序员可以手动创建其他线程对象并启动。

image-20240303161755250

这个除了GC(垃圾回收线程之外)只有一个主线程

并发与并行

并发(concurrency)

①使用单核CPU的时候,同一时刻只能有一条指令执行,但多个指令被快速的轮换执行,使得在宏观上具有多个指令同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干端,使多个指令快速交替的执行

②如上图所示,假设只有一个CPU资源,线程之间要竞争得到执行机会。图中的第一个阶段,在A执行的过程中,B、C不会执行,因为这段时间内这个CPU资源被A竞争到了,同理,第二阶段只有B在执行,第三阶段只有C在执行。其实,并发过程中,A、B、C并不是同时进行的(微观角度),但又是同时进行的(宏观角度)。

③在同一个时间点上,一个CPU只能支持一个线程在执行。因为CPU运行的速度很快,CPU使用抢占式调度模式在多个线程间进行着高速的切换,因此我们看起来的感觉就像是多线程一样,也就是看上去就是在同一时刻运行。

并行(parallellism)

①使用多核CPU的时候,同一时刻,有多条指令在多个CPU上同时执行。

②如图所示,在同一时刻,ABC都是同时执行(微观、宏观)。

并发编程与并行编程

①在CPU比较繁忙(假设为单核CPU),如果开启了很多个线程,则只能为一个线程分配仅有的CPU资源,这些线程就会为自己尽量多抢时间片,这就是通过多线程实现并发,线程之间会竞争CPU资源争取执行机会。

②在CPU资源比较充足的时候,一个进程内的多个线程,可以被分配到不同的CPU资源,这就是通过多线程实现并行。

③至于多线程实现的是并发还是并行?上面所说,所写多线程可能被分配到一个CPU内核中执行,也可能被分配到不同CPU执行,分配过程是操作系统所为,不可人为控制。所以,如果有人问我我所写的多线程是并发还是并行的?我会说,都有可能。

④总结:单核CPU上的多线程,只是由操作系统来完成多任务间对CPU的运行切换,并非真正意义上的并发。随着多核CPU的出现,也就意味着不同的线程能被不同的CPU核得到真正意义的并行执行,故而多线程技术得到广泛应用。

⑤不管并发还是并行,都提高了程序对CPU资源的利用率,最大限度地利用CPU资源,而我们使用多线程的目的就是为了提高CPU资源的利用率。

线程的调度模型

时间片就是执行权

①如果多个线程被分配到一个CPU内核中执行,则同一时刻只能允许有一个线程能获得CPU的执行权,那么进程中的多个线程就会抢夺CPU的执行权,这就是涉及到线程调度策略。

②分时调度模型

所有线程轮流使用CPU的执行权,并且平均的分配每个线程占用的CPU的时间。

③抢占式调度模型

==让优先级高的线程以较大的概率优先获得CPU的执行权,如果线程的优先级相同,那么就会随机选择一个线程获得CPU的执行权,而Java采用的就是抢占式调用。==

实现线程

第一种方式:继承Thread

①编写一个类继承Thread,重写run方法。

②创建线程对象:Thread t = new MyThread();

③启动线程:t.start();

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
package com.ljy.CollectionTest.ThreadTest;

/**
* 1 编写一个类继承java.lang.Thread
* 2 重写run方法
* 3 new线程对象
* 4 调用线程对象的start方法来启动线程
*/
public class ThreadTest01 {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start();

//这里编写的代码是在main方法中,因此这里的代码是属于主线程中进行的

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

class MyThread extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(i);
}
}
}

image-20240303172013923

铁律:自上而下的执行,在大括号中,上面执行不完,下面也不会执行

mt.start()是启动代码,瞬间就结束

第二种方式:实现Runnable接口

①编写一个类实现Runnable接口,实现run方法。

②创建线程对象:Thread t = new Thread(new MyRunnable());

③启动线程:t.start();

Runnable和线程没有任何的关系,只是一个实现了runnable接口的普通类。

推荐使用这种方式,在实现接口的同时,保留了类的继承

优先选择第二种方式:因为实现接口的同时,保留了类的继承。

第二种方式也可以使用匿名内部类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ThreadTest04 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println("t----》"+i);

}
}
}).start();



for (int i = 0; i < 100; i++) {
System.out.println("main-->"+i);

}
}
}

t.start()和t.run()的本质区别?

①本质上没有区别,都是普通方法调用。只不过两个方法完成的任务不同。

②t.run()是调用run方法。执行run方法中的业务代码。

③t.start()是启动线程,只要线程启动了,start()方法就执行结束了。

run()

image-20240303172736672

这个说白了还是个单线程

start

image-20240303175456466

相当于,start会进入栈,然后启动一个新进程之后,弹出栈,在另外一个线程栈里面继续跑。有两个栈互不干扰。然后两个开始抢cpu的运行权

线程常用的三个方法:

1
2
3
4
5
6
①实例方法:
String getName();
void setName(String name);

②静态方法:
static Thread currentThread();//获取当前线程的对象
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
package com.ljy.CollectionTest.Thread01;

public class ThreadTest {

public static void main(String[] args) {
Thread maintest = Thread.currentThread();
System.out.println("主线程的名字"+maintest.getName());
Thread t = new MyThread();
t.start();
//



}

}

class MyThread extends Thread{
@Override
public void run() {
// super.run();
Thread thread = Thread.currentThread();
System.out.println("分支线程的名字:"+thread.getName());
}
}

线程生命周期

①线程生命周期指的是:从线程对象新建,到最终线程死亡的整个过程。

②线程生命周期包括七个重要阶段:

1.新建状态(NEW)

运行状态

​ 2.就绪状态(RUNNABLE)

​ 3.运行状态(RUNNABLE)

4.超时等待状态(TIMED_WAITING)

5.等待状态(WAITING)

6.阻塞状态(BLOCKED)

7.死亡状态(TERMINATED)

image-20240303193011256

线程的休眠与终止(Thread.sleep)

1
2
3
4
5
try {
Thread.sleep(1000*5);// 出现在哪里阻塞哪个线程
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

关于sleep的面试题

image-20240304094727010

这个t.sleep其实等同于是Thread.sleep(1000*5),

所以这个其实是阻塞主线程的,不是让t线程睡眠,让当前线程睡眠。

结果:5s足够run()跑完,所以是0-99输出出来,然后等完5s后,跑主线程

如何中断一个线程的休眠

1
t.interrupt();

利用哪个线程,就中止哪个线程的睡眠,这是一个实例方法

1
2
3
4
5
6
7
// Thread-0起来干活了。
// 这行代码的作用是终止 t 线程的睡眠。
// interrupt方法是一个实例方法。
// 以下代码含义:t线程别睡了。
// 底层实现原理是利用了:异常处理机制。
// 当调用这个方法的时候,如果t线程正在睡眠,必然会抛出:InterruptedException,然后捕捉异常,终止睡眠。
t.interrupt();

如何中止一个线程的休眠

1.方法1:t.stop()

1
t.stop()//这个已经废除了1.2

这种方式是强行终止线程,容易导致数据的丢失;没有保存的数据在内存中的数据一定会因此而丢失

2.方式2:打标记

在run()设置局部变量: boolean run = true;

并在主函数中设置中止线程: mr.run = false;

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
package com.ljy.CollectionTest.Thread03;

public class ThreadTest {
public static void main(String[] args) {
// Thread t = new Thread(new MyRunnable());
MyRunnable mr = new MyRunnable();
Thread t = new Thread(mr);
t.setName("t");
t.start();
// 5秒之后终止线程t的执行
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

//终止线程t的执行。
mr.run = false;
}
}


class MyRunnable implements Runnable{

boolean run = true;
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(run){

System.out.println(Thread.currentThread().getName() + "==>"+i);
// try {
// Thread.sleep(1000*5);
// } catch (InterruptedException e) {
// throw new RuntimeException(e);
// }
}
else{
return;
}
}
}
}

守护线程(后台线程)

线程一共分为两大类:用户线程与守护线程

所谓用户线程就是非守护线程,垃圾回收线程GC就是守护线程

守护线程的特点 ,所有的用户线程结束之后,守护线程会自动的退出/结束

1
myThread.setDaemon(true);
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
package com.powernode.javase.thread07;

/**
* 1. 在Java语言中,线程被分为两大类:
* 第一类:用户线程(非守护线程)
* 第二类:守护线程(后台线程)
*
* 2. 在JVM当中,有一个隐藏的守护线程一直在守护者,它就是GC线程。
*
* 3. 守护线程的特点:所有的用户线程结束之后,守护线程自动退出/结束。
*
* 4. 如何将一个线程设置为守护线程?
* t.setDaemon(true);
*/
public class ThreadTest {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.setName("t");

// 在启动线程之前,设置线程为守护线程
myThread.setDaemon(true);

myThread.start();

// 10s结束!
// main线程中,main线程是一个用户线程。
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "===>" + i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}

}
}

class MyThread extends Thread {
@Override
public void run() {
int i = 0;
while(true){
System.out.println(Thread.currentThread().getName() + "===>" + (++i));
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
}

守护线程会在用户线程结束后结束,也就是在JVM结束后结束,

这段代码的主线程是一个运行5s会结束的代码,分线程是一个死循环,如果不停止会一直 的运行下去。

但是在这个代码的实际结果中,主线程在运行5s后就会结束,分线程设置为一个守护线程,所以这个线程也会在主线程结束后结束,所以这个死循环的线程会在5s之后结束。

定时任务

JDK中提供的定时任务

java.util.Timer 定时器

java.util,TimerTask 定时任务

  1. 定时器 + 定时任务,可以帮助我们在程序中完成,每间隔多久执行一次某段程序。

  2. Timer的构造方法

    1
    2
    Timer()
    Timer(boolean isDaemon) isDaemon是true表示该定时器是一个守护线程

    Timer本身就是继承了Runnable的,所以本质上可以理解为是一个线程

    1
       

线程的合并join

1.t.join()实现的效果喝Thread.sleep()差不多,但是t.join()会把这个线程合并到当前线程,并且阻塞当前线程。

​ 直到t线程结束了,才会接触当前线程的阻塞状态,就是把这个线程插入,跑完,再让主线程跑

2.join是一个实例方法,不是静态方法

3.假设在main方法中,调用了t.join(),后果是什么?

​ t线程合并到主线程中,主线程进入到了阻塞状态,直到t线程执行结束之后,主线程阻塞解除。

4.t.join方法其实是让当前线程进入阻塞状态,直到t线程结束,当前线程阻塞状态接触。

5.和sleep方法很像,但是不一样

​ 第一:sleep方法是静态方法,join是实例方法

​ 第二:sleep方法可以指定睡眠的时长,join方法不能保证阻塞的时长

​ 第三:sleep和join方法都是让当前线程进入到了一个阻塞的状态;

​ 第四:sleep方法的阻塞解除条件?时间过去了,join的方法的阻塞解除条件?调用join方法的那个线程结束了

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 ThreadTest {
public static void main(String[] args) throws InterruptedException {
Thread t = new MyThread();
t.setName("t");
t.start();

System.out.println("main begin");

// 合并线程
// t合并到main线程中。
// main线程受到阻塞(当前线程受到阻塞)
// t线程继续执行,直到t线程结束。main线程阻塞解除(当前线程阻塞解除)。
//t.join();

// join方法也可以有参数,参数是毫秒。
// 以下代码表示 t 线程合并到 当前线程,合并时长 10 毫秒
// 阻塞当前线程 10 毫秒
//t.join(10);

// 调用这个方法,是想让当前线程受阻10秒
// 但不一定,如果在指定的阻塞时间内,t线程结束了。当前线程阻塞也会解除。
t.join(1000 * 10);



// 当前线程休眠10秒。
//Thread.sleep(1000 * 10);

// 主线程
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}

System.out.println("main over");
}
}

class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}

线程的调度

  1. 优先级

  2. 线程是可以设置优先级的,优先级较高的,获得CPU时间片的总体概率会高一些

  3. JVM采用的是抢占式调度模型,谁的优先级高,获取CPU时间片的总体概率就高。

  4. 默认情况下,一个线程的优先级是5

  5. 最低是1,最高是10

设置优先级

1
mainThread.setPriority(Thread.MAX_PRIORITY)

线程让位

yield,静态方法,这个代码出现在哪里,就是当前线程让位,当前线程把占领的cpu的时间片放弃掉,直接回到就绪状态,继续抢夺CPU时间片的状态,不会进入阻塞状态。

1
2
3
4
5
6
7
8
/**
* 关于JVM的调度:
* 1. 让位
* 2. 静态方法:Thread.yield()
* 3. 让当前线程让位。
* 4. 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
* 5. 只能保证大方向上的,大概率,到了某个点让位一次。
*/
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
/**
* 关于JVM的调度:
* 1. 让位
* 2. 静态方法:Thread.yield()
* 3. 让当前线程让位。
* 4. 注意:让位不会让其进入阻塞状态。只是放弃目前占有的CPU时间片,进入就绪状态,继续抢夺CPU时间片。
* 5. 只能保证大方向上的,大概率,到了某个点让位一次。
*/
public class ThreadTest {
public static void main(String[] args) {
Thread t1 = new MyThread();
t1.setName("t1");

Thread t2 = new MyThread();
t2.setName("t2");

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

class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 500; i++) {
if(Thread.currentThread().getName().equals("t1") && i % 10 == 0){
System.out.println(Thread.currentThread().getName() + "让位了,此时的i下标是:" + i);
// 当前线程让位,这个当前线程一定是t1
// t1会让位一次
Thread.yield();
}
System.out.println(Thread.currentThread().getName() + "==>" + i);
}
}
}

线程安全问题

1. 什么情况下要考虑线程的安全问题?

条件1:多线程的并发环境下

条件2:有共享的数据

条件3:共享数据涉及到修改的操作

2.一般情况下:

  1. 局部变量不存在线程的安全问题(尤其是基本数据类型,如果是引用数据类型就另外说了),局部变量是在栈里面的

  2. 实例变量可能会出现线程安全问题,因为实例变量是堆共享的

  3. 静态变量容易出现线程安全问题,静态变量是在堆中的,堆是多线程共享的

我们把线程排队执行,叫做线程的同步机制,(t1和t2线程,t1线程在执行的时候必须等待t2线程执行到某个位置之后,t1线程才开始执行。)

如果不排队,我们将其称为:线程的异步机制。(t1和t2各自执行各自的,谁也不需要等待对方,并发的,就认为是一个异步的)

异步:效率高,但是可能存在一个安全的隐患

同步:效率低,需要排队,但是可以保证线程的安全问题

xxxxxxxxxx Class userClass = User.class;Constructor con = userClass.getDeclaredConstructor(Map.class);Type[] genericParameterTypes = con.getGenericParameterTypes();for(Type g :genericParameterTypes){    if(g instanceof ParameterizedType){        ParameterizedType parameterizedType = (ParameterizedType) g;        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();        for(Type a : actualTypeArguments){            System.out.println(a.getTypeName());       }   }}java

image-20240304144429743

以上的操作是线程不安全的,如果要保证线程安全,就要让读取操作和写操作是不可分的

线程同步机制

要使用线程的同步机制,来保证多线程并发环境下的数据安全问题

线程同步的本质:线程排队执行就是同步机制

  1. 语法格式:
1
2
3
4
synchronized(必须是需要排队的这几个线程共享共享的对象)
{
需要同步的代码
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 使用线程同步机制,来保证多线程并发环境下的数据安全问题:
* 1. 线程同步的本质是:线程排队执行就是同步机制。
* 2. 语法格式:
* synchronized(必须是需要排队的这几个线程共享的对象){
* // 需要同步的代码
* }
*
* “必须是需要排队的这几个线程共享的对象” 这个必须选对了。
* 这个如果选错了,可能会无故增加同步的线程数量,导致效率降低。
* 3. 原理是什么?
* synchronized(obj){
* // 同步代码块
* }
* 假设obj是t1 t2两个线程共享的。
* t1和t2执行这个代码的时候,一定是有一个先抢到了CPU时间片。一定是有先后顺序的。
* 假设t1先抢到了CPU时间片。t1线程找共享对象obj的对象锁,找到之后,则占有这把锁。只要能够占有obj对象的对象锁,就有权利进入同步代码块执行代码。
* 当t1线程执行完同步代码块之后,会释放之前占有的对象锁(归还锁)。
* 同样,t2线程抢到CPU时间片之后,也开始执行,也会去找共享对象obj的对象锁,但由于t1线程占有这把锁,t2线程只能在同步代码块之外等待。
*
* 4. 注意同步代码块的范围,不要无故扩大同步的范围,同步代码块范围越小,效率越高。
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void withdraw(double money){
// this是当前账户对象
// 当前账户对象act,就是t1和t2共享的对象。
synchronized (this){
//synchronized (obj) {
// 第一步:读取余额
double before = this.getBalance();
System.out.println(Thread.currentThread().getName() + "线程正在取款"+money+",当前"+this.getActNo()+"账户余额" + before);

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

// 第二步:修改余额
this.setBalance(before - money);
System.out.println(Thread.currentThread().getName() + "线程取款成功,当前"+this.getActNo()+"账户余额" + this.getBalance());
}
}

关键是找共享对象

加到实例代码块中,锁的就是this

比如

1
2
3
public synchronized void withdraw(double money){

}

按照上面的操作,同步代码块就是这个大的整体,

如果写的是普通的同步代码块,可以保证这个同步的范围,同时这个括号里面的内容也是可以写的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 在实例方法上也可以添加 synchronized 关键字:
* 1. 在实例方法上添加了synchronized关键字之后,整个方法体是一个同步代码块。
* 2. 在实例方法上添加了synchronized关键字之后,共享对象的对象锁一定是this的。
*
* 这种方式相对于之前所讲的局部同步代码块的方式要差一些:
* 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
/**
* 1. 内容是关于:线程通信。
*
* 2. 线程通信涉及到三个方法:
* wait()、notify()、notifyAll()
*
* 3. 以上三个方法都是Object类的方法。
*
* 4. 其中wait()方法重载了三个:
* wait():调用此方法,线程进入“等待状态”
* wait(毫秒):调用此方法,线程进入“超时等待状态”
* wait(毫秒, 纳秒):调用此方法,线程进入“超时等待状态”
*
* 5. 调用wait方法和notify相关方法的,不是通过线程对象去调用,而是通过共享对象去调用。
*
* 6. 例如调用了:obj.wait(),什么效果?
* obj是多线程共享的对象。
* 当调用了obj.wait()之后,在obj对象上活跃的所有线程进入无期限等待。直到调用了该共享对象的 obj.notify() 方法进行了唤醒。
* 而且唤醒后,会接着上一次调用wait()方法的位置继续向下执行。
*
* 7. obj.wait()方法调用之后,会释放之前占用的对象锁。
*
* 8. 关于notify和notifyAll方法:

*

1.wait()

wait方法无参数的会进入等待状态,有参数的会进入超时等待状态

image-20240304153614044

当调用了obj.wait()之后,在obj对象上活跃的所有线程都会进入等待,直到调用了该共享对象的notify方法进行唤醒。

2.notify()和notify()

  •  共享对象.notify(); 调用之后效果是什么?唤醒优先级最高的等待线程。如果优先级一样,则随机唤醒一个。
    
  •  共享对象.notifyAll(); 调用之后效果是什么?唤醒所有在该共享对象上等待的线程。
    

单例模式的线程安全问题

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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package com.powernode.javase.thread23;

import java.util.concurrent.locks.ReentrantLock;

class SingletonTest {

// 静态变量
private static Singleton s1;
private static Singleton s2;

public static void main(String[] args) {

// 获取某个类。这是反射机制中的内容。
/*Class stringClass = String.class;
Class singletonClass = Singleton.class;
Class dateClass = java.util.Date.class;*/

// 创建线程对象t1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
s1 = Singleton.getSingleton();
}
});

// 创建线程对象t2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
s2 = Singleton.getSingleton();
}
});

// 启动线程
t1.start();
t2.start();

try {
t1.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
try {
t2.join();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

// 判断这两个Singleton对象是否一样。
System.out.println(s1);
System.out.println(s2);
System.out.println(s1 == s2);

}
}

/**
* 懒汉式单例模式
*/
public class Singleton {
private static Singleton singleton;

private Singleton() {
System.out.println("构造方法执行了!");
}

// 非线程安全的。
/*public static Singleton getSingleton() {
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
return singleton;
}*/

// 线程安全的:第一种方案(同步方法),找类锁。
/*public static synchronized Singleton getSingleton() {
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
return singleton;
}*/

// 线程安全的:第二种方案(同步代码块),找的类锁
/*public static Singleton getSingleton() {
// 这里有一个知识点是反射机制中的内容。可以获取某个类。
synchronized (Singleton.class){
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
return singleton;
}*/

// 线程安全的:这个方案对上一个方案进行优化,提升效率。
/*public static Singleton getSingleton() {
if(singleton == null){
synchronized (Singleton.class){
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
}
}
return singleton;
}*/

// 使用Lock来实现线程安全
// Lock是接口,从JDK5开始引入的。
// Lock接口下有一个实现类:可重入锁(ReentrantLock)
// 注意:要想使用ReentrantLock达到线程安全,假设要让t1 t2 t3线程同步,就需要让t1 t2 t3共享同一个lock。
// Lock 和 synchronized 哪个好?Lock更好。为什么?因为更加灵活。
private static final ReentrantLock lock = new ReentrantLock();

public static Singleton getSingleton() {
if(singleton == null){

try {
// 加锁
lock.lock();
if (singleton == null) {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
singleton = new Singleton();
}
} finally {
// 解锁(需要100%保证解锁,怎么办?finally)
lock.unlock();
}

}
return singleton;
}
}

可重入锁

Callable实现线程

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
package com.powernode.javase.thread24;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

/**
* 实现线程的第三种方式:实现Callable接口,实现call方法。
* 这种方式实现的线程,是可以获取到线程返回值的。
*/
public class ThreadTest {
public static void main(String[] args) {
// 创建“未来任务”对象
FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
@Override
public Integer call() throws Exception {
// 处理业务......
Thread.sleep(1000 * 5);
return 1;
}
});

// 创建线程对象
Thread t = new Thread(task);
t.setName("t");

// 启动线程
t.start();

try {
// 获取“未来任务”线程的返回值
// 阻塞当前线程,等待“未来任务”结束并返回值。
// 拿到返回值,当前线程的阻塞才会解除。继续执行。
Integer i = task.get();
System.out.println(i);
} catch (Exception e) {
e.printStackTrace();
}
}
}

/*class MyRunnable implements Runnable {

@Override
public void run() {

}
}

class MyThread extends Thread {
@Override
public void run() {

}
}*/

线程池实现线程ExecutorService

高并发的商城项目,一定是会用到的

1
2
3
4
5
* 创建线程的第四种方式:使用线程池技术。
* 线程池本质上就是一个缓存:cache
* 一般都是服务器在启动的时候,初始化线程池,
* 也就是说服务器在启动的时候,创建N多个线程对象,
* 直接放到线程池中,需要使用线程对象的时候,直接从线程池中获取。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 创建一个线程池对象(线程池中有3个线程)
ExecutorService executorService = Executors.newFixedThreadPool(3);

// 将任务交给线程池(你不需要触碰到这个线程对象,你只需要将要处理的任务交给线程池即可。)
executorService.submit(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10; i++) {
System.out.println(Thread.currentThread().getName() + "--->" + i);
}
}
});

// 最后记得关闭线程池
executorService.shutdown();

集合

集合概述

①什么是集合,有什么用?

1.集合是一种容器,用来组织和管理数据的。非常重要。

2.Java的集合框架对应的这套类库其实就是对各种数据结构的实现。

3.每一个集合类底层采用的数据结构不同,例如ArrayList集合底层采用了数组,LinkedList集合底层采用了双向链表,HashMap集合底层采用了哈希表,TreeMap集合底层采用了红黑树。

4.我们不用写数据结构的实现了。直接用就行了。但我们需要知道的是在哪种场合下选择哪一个集合效率是最高的。

②集合中==存储的是引用==,不是把堆中的对象存储到集合中,是把==对象的地址==存储到集合中。

③默认情况下,==如果不使用泛型的话,集合中可以存储任何类型的引用,只要是Object的子类都可以存储。==

1
2
3
4
5
6
7
8
9
10
public class CollectionTest01 {
public static void main(String[] args) {
Collection collection = new ArrayList();
collection.add(1);
collection.add(new Object());
collection.add("A");


}
}

④Java集合框架相关的类都在 java.util 包下。

⑤Java集合框架分为两部分:

1.Collection结构:元素以单个形式存储。

2.Map结构:元素以键值对的映射关系存储。

Collection继承结构

Collection接口是一个所有以单个方式存储元素的所有集合的超级接口,所有的以单个方式存储元素的这些集合都继承了Collection接口

Collection接口继承了iterable接口,所以都是可以遍历的,接口返回值iterator

Collection和iterator,依赖关系。

List重复,set不可以重复

image-20240228161736123

SequencedCollectionSequencedSet接口都是Java21新增的接口。

②图中蓝色的是实现类。其它的都是接口。

③6个实现类中只有HashSet是无序集合。剩下的都是有序集合

1.有序集合:集合中存储的元素**==有下标==或者集合中存储的元素是可排序的**。

2.无序集合:集合中存储的元素**==没有下标==并且集合中存储的元素也没有排序**。

④每个集合实现类对应的数据结构如下:

1.LinkedList:双向链表(不是队列数据结构,但使用它可以模拟队列)

2.ArrayList:数组

3.Vector:数组(线程安全的)

4.HashSet:哈希表

5.LinkedHashSet:双向链表和哈希表结合体

6.TreeSet:红黑树

==⑤List集合中存储的元素可重复。Set集合中存储的元素不可重复。==

Collection接口(超级接口)

Collection接口的通用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
boolean add(E e); //向集合中添加元素

int size(); //获取集合中元素个数

boolean addAll(Collection c); //将参数集合中所有元素全部加入当前集合

boolean contains(Object o); //判断集合中是否包含对象o

boolean remove(Object o); //从集合中删除对象o

void clear(); //清空集合

boolean isEmpty(); //判断集合中元素个数是否为0

Object[] toArray(); //将集合转换成一维数组

Collection的遍历(集合的通用遍历方式)

①第一步:获取当前集合依赖的迭代器对象

1
2
Iterator it = collection.iterator();
//获取到迭代器之后,迭代器中有一个光标cursor,这个光标起初默认指向集合的第一个位置

②第二步:编写循环,循环条件是:当前光标指向的位置是否存在元素。

1
2
3
4
while(it.hasNext()){}
//boolean has = it.hasNext();
//has->true,表示光标指向的位置有数据;
//has->false,表示光标指向的位置没有数据。

③第三步:如果有,将光标指向的当前元素返回,并且将光标向下移动一位。

1
2
3
4
Object obj = it.next();
//做了两件事情,
//1:取出当前光标只向位置的元素
//2:将光标向下移动一位

image-20240228210734004

1
2
3
4
5
6
7
8
9
for(Iterator it = collection.iterator();it.hasNext();){
Object obj = it.next();
System.out.println(obj);
}
Iterator i = collection.iterator();
while (i.hasNext()){
Object obj2 = i.next();//这里存在隐式的多态
System.out.println(obj2);
}

SequencedCollection接口**==有序集合==**

所有的**==有序集合==**都实现了SequencedCollection接口

①SequencedCollection接口是Java21版本新增的。

②SequencedCollection接口==(有序集合)==中的方法:

1
2
3
4
5
6
7
8
9
10
11
12
void addFirst(Object o)//向头部添加

void addLast(Object o)//向末尾添加
Object removeFirst()//删除头部

Object removeLast()//删除末尾

Object getFirst()//获取头部节点

Object getLast()//获取末尾节点

SequencedCollection reversed(); //反转集合中的元素

==③ArrayList,LinkedList,Vector,LinkedHashSet,TreeSet,Stack 都可以调用这个接口中的方法。==

泛型(编译阶段的功能)

①泛型是Java5的新特性,属于编译阶段的功能。

②泛型可以让开发者在编写代码时指定集合中存储的数据类型

③泛型作用:

1.类型安全:指定了集合中元素的类型之后,**==编译器会在编译时进行类型检查==如果尝试将错误类型的元素添加到集合中就会在编译时报错,避免了在运行时出现类型错误的问题。**

2.代码简洁:使用泛型可以简化代码,避免了繁琐的类型转换操作。比如,在没有泛型的时候,需要使用 Object 类型来保存集合中的元素,并在使用时==强制类型转换成实际类型==(向下转型),而有了泛型之后,只需要在定义集合时指定类型即可。

④在集合中使用泛型

1
2
3
4
5
6
//没有使用多态
Iterator i = collection.iterator();
while (i.hasNext()){
Object obj2 = i.next();//这里存在隐式的多态
System.out.println(obj2);
}
1
2
//表示创建一个集合,而且这个集合中只能存储String
Collection<String> strs = new ArrayList<String>();

这就表示该集合只能存储字符串,存储其它类型时编译器报错。

并且以上代码使用泛型后,避免了繁琐的类型转换,集合中的元素可以直接调用String类特有的方法。

image-20240228212839842

⑤Java7的新特性:钻石表达式

1
2
3
Collection<String> strs = new ArrayList<String>();
//下面是钻石表达式,省了后面的尖括号内的东西
Collection<String> strs = new ArrayList<>();

泛型的擦除与补偿(了解)

①泛型的出现提高了编译时的安全性,正因为**==编译时对添加的数据做了检查,则程序运行时才不会抛出类型转换异常。==**因此泛型本质上是编译时期的技术,是专门给编译器用的。

==泛型只是给Javac看的,在运行的时候,加载类的时候,会将泛型擦除掉(擦除之后的类型为Object类型),这个称为泛型擦除。==

②为什么要有泛型擦除呢?兼容低版本的JDK其本质是为了让JDK1.4和JDK1.5能够兼容同一个类加载器。在JDK1.5版本中,程序编译时期会对集合添加的元素进行安全检查,如果检查完是安全的、没有错误的,那么就意味着添加的元素都属于同一种数据类型,则加载类时就可以把这个泛型擦除掉,将泛型擦除后的类型就是Object类,这样擦除之后的代码就与JDK1.4的代码一致。

③由于加载类的时候,会默认将类中的泛型擦除为Object类型,所以添加的元素就被转化为Object类型,同时取出的元素也默认为Object类型。而我们获得集合中的元素时,按理说取出的元素应该是Object类型,为什么取出的元素却是实际添加的元素类型呢?

④这里又做了一个默认的操作,我们称之为**==泛型的补偿====在程序运行时,通过获取元素的实际类型进行强转,这就叫做泛型补偿(不必手动实现强制转换)。获得集合中的元素时,虚拟机 会根据获得元素的实际类型进行向下转型,也就是会恢复获得元素的实际类型,因此我们就无需手动执行向下转型操作,从本质上避免了抛出类型转换异常。==**

自定义泛型

在类上定义泛型

image-20240228214144852image-20240228214151875

在方法上定义泛型

实例方法可以,静态方法不能直接用

因为实例方法跑的时候要new实例,刚好在创建的时候会确定泛型的类型。而静态方法不需要创建实例,不能确定类型

①在类上定义的泛型,在**==静态方法中无法使用==**。如果在静态方法中使用泛型,则需要再方法返回值类型前面进行泛型的声明。

②语法格式:<泛型1, 泛型2, 泛型3, …> 返回值类型 方法名(形参列表) {}

image-20240228215823508

image-20240228215931140

在接口上定义泛型

①语法格式:

1
2
interface 接口名<泛型1,泛型2,...> {}

②例如:

1
2
3
4
5
6
7
public interface Flayable<T>{}

public interface MyComparable <T>{

int compareTo(T o);
}

③实现接口时,如果知道具体的类型,则:

1
2
public class MyClass implements Flyable<Bird>{}
public class CollectionTest03 implements MyComparable<CollectionTest03>{}

④实现接口时,如果不知道具体的类型,则:

1
public class MyClass<T> implements Flyable<T>{}

泛型通配符

这个是从使用泛型的角度,不是从泛型定义的角度

别人把泛型定义好了,我来用

①泛型是在限定数据类型,当在集合或者其他地方使用到泛型后,那么这时一旦明确泛型的数据类型,那么在使用的时候只能给其传递和数据类型匹配的类型,否则就会报错。

②有的情况下,我们在定义方法时,根本无法确定集合中存储元素的类型是什么。为了解决这个“无法确定集合中存储元素类型”问题,那么Java语言就提供了泛型的通配符。

③通配符的几种形式:

  1. 无限定通配符,<?>,此处“?”可以为任意引用数据类型。不确定类型

image-20240229001042986

  1. 上限通配符,<? extends Number>,此处“?”必须为Number及其子类。

  2. 下限通配符,<? super Number>,此处“?”必须为Number及其父类。

集合迭代时删除元素

①迭代集合时删除元素

使用“集合对象.remove(元素)”:会出现ConcurrentModificationException异常。(并发修改异常)

使用“迭代器对象.remove()”:不会出现异常。

it.next前后的变化

image-20240229001752728

②关于集合的并发修改问题

想象一下,有两个线程:A和B。A线程负责迭代遍历集合,B线程负责删除集合中的某个元素。当这两个线程同时执行时会有什么问题?

③如何解决并发修改问题:==fail-fast机制==

fail-fast机制又被称为:快速失败机制。也就是说只要程序发现了程序对集合进行了并发修改。就会立即让其失败,以防出现错误。

④fail-fast机制是如何实现的?以下是源码中的实现原理:

集合中设置了一个modCount属性,用来记录修改次数,使用集合对象执行增,删,改中任意一个操作时,modCount就会自动加1。

获取迭代器对象的时候,会给迭代器对象初始化一个expectedModCount属性。并且将expectedModCount初始化为modCount,即:int expectedModCount = modCount;

当使用集合对象删除元素时:modCount会加1。但是迭代器中的expectedModCount不会加1。而当迭代器对象的next()方法执行时,会检测expectedModCount和modCount是否相等,如果不相等,则抛出:ConcurrentModificationException异常。

当使用迭代器删除元素的时候:modCount会加1,并且expectedModCount也会加1。这样当迭代器对象的next()方法执行时,检测到的expectedModCount和modCount相等,则不会出现ConcurrentModificationException异常。

⑤注意:虽然我们当前写的程序是单线程的程序,并没有使用多线程,但是通过迭代器去遍历的同时使用集合去删除元素,这个行为将被认定为并发修改。

⑥结论:迭代集合时,删除元素要使用“迭代器对象.remove()”方法来删除,避免使用“集合对象.remove(元素)”。主要是为了避免ConcurrentModificationException异常的发生。

注意:迭代器的remove()方法删除的是next()方法的返回的那个数据。remove()方法调用之前一定是先调用了next()方法,如果不是这样的,就会报错。

List接口 有下标、有序

List接口常用方法

①List集合存储元素特点:有序可重复。

  1. 有序:是因为List集合中的元素都是有下标的,从0开始,以1递增。

存进去顺序

  1. 可重复:存进去1,还可以再存一个1。

②List接口下常见的实现类有:

  1. ArrayList:数组
  2. Vector、Stack:数组(线程安全的)
  3. LinkedList:双向链表

③List接口特有方法:(在Collection和SequencedCollection中没有的方法,只适合List家族使用的方法,这些方法都和下标有关系。)

  • 1.void add(int index, E element) 在指定索引处插入元素
  • 2.E set(int index, E element); 修改索引处的元素
  • 3.E get(int index); 根据索引获取元素(通过这个方法List集合具有自己特殊的遍历方式:根据下标遍历)
  • 4.E remove(int index); 删除索引处的元素
  • 5.int indexOf(Object o); 获取对象o在当前集合中第一次出现时的索引。
  • 6.int lastIndexOf(Object o); 获取对象o在当前集合中最后一次出现时的索引。
  • 7.List subList(int fromIndex, int toIndex); 截取子List集合生成一个新集合(对原集合无影响)。[fromIndex, toIndex)

image-20240229003834620

image-20240229003848999

  • 8.static List of(E… elements); 静态方法,返回包含任意数量元素的不可修改列表。(获取的集合是只读的,不可修改的。)

image-20240229003945010

List接口特有迭代

①特有的迭代方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
ListIterator<E> listIterator(); //获取List集合特有的迭代器(该迭代器功能更加强大,但只适合于List集合使用)

ListIterator<E> listIterator(int index); //从列表中的指定位置开始,返回列表中元素的列表迭代器

②ListIterator//接口中的常用方法:

boolean hasNext(); // 判断光标当前指向的位置是否存在元素。

E next(); //将当前光标指向的元素返回,然后将光标向下移动一位。

void remove(); //删除上一次next()方法返回的那个数据(删除的是集合中的)。remove()方法调用的前提是:你先调用next()方法。不然会报错。

void add(E e); //添加元素(将元素添加到光标指向的位置,然后光标向下移动一位。)
//所以指向的内容没有改变

boolean hasPrevious(); //判断当前光标指向位置的上一个位置是否存在元素。

E previous(); //获取上一个元素(将光标向上移动一位,然后将光标指向的元素返回)

int nextIndex(); //获取光标指向的那个位置的下标

int previousIndex(); //获取光标指向的那个位置的上一个位置的下标

void set(E e); //修改的是上一次next()方法返回的那个数据(修改的是集合中的)。set()方法调用的前提是:你先调用了next()方法。不然会报错。

image-20240229150508050

调用迭代器之前的remove也需要调用next()

List接口使用Comparator排序

①回顾数组中自定义类型是如何排序的?

所有自定义类型排序时必须指定排序规则。(int不需要指定,String不需要指定,因为他们都有固定的排序规则。int按照数字大小。String按照字典中的顺序)

如何给自定义类型指定排序规则?让自定义类型实现java.lang.Comparable接口,然后重写compareTo方法,在该方法中指定比较规则。

1
2
3
4
5
6
7
8
9
// 关于compareTo方法的返回值
//a - b == 0 ;
//a - b > 0; a>b

//a - b <0
@Override
public int compareTo(CollectionTest03 o) {
return this.age - o.age;
}

②List集合的排序

两种方法:

1.一种是类继承comparable

1
2
3
4
public class Person implements Comparable<Person>{

}

2.额外新建一个类

1
2
3
4
5
6
7
8
public class PersonComparator implements Comparator<Person> {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()- o2.getAge();//升序
}
}
persionList.sort(new PersonComparator());
//这样进行调用

default void sort(Comparator<? super E> c); 对List集合中元素排序可以调用此方法。

 Class userClass = User.class;Constructor con = userClass.getDeclaredConstructor(Map.class);Type[] genericParameterTypes = con.getGenericParameterTypes();for(Type g :genericParameterTypes){    if(g instanceof ParameterizedType){        ParameterizedType parameterizedType = (ParameterizedType) g;        Type[] actualTypeArguments = parameterizedType.getActualTypeArguments();        for(Type a : actualTypeArguments){            System.out.println(a.getTypeName());       }   }}java

如何给自定义类型指定比较规则?可以对Comparator提供一个实现类,并重写compare方法来指定比较规则。

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
package com.ljy.CollectionTest.ListTest;

import java.util.ArrayList;
import java.util.List;

public class ListSort {
public static void main(String[] args) {
Person p1 = new Person("张三",18);
Person p2 = new Person("李四",20);
Person p3 = new Person("王五",15);
Person p4 = new Person("赵六",19);
Person p5 = new Person("孙七",23);


List<Person> list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);
list.add(p4);
list.add(p5);

list.sort(new PersonComparator());

for (Person person : list){
System.out.println(person.getName()+":"+person.getAge());
}

}
}
1
2
3
4
5
6
persionList.sort(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
return o1.getAge()-o2.getAge();
}
});

当然,Comparator接口的实现类也可以采用**==匿名内部类==**的方式。

ArrayList1.5倍扩容

①ArrayList集合底层采用了数组这种数据结构。

②ArrayList集合优点:

​ 底层是数组,因此根据下标查找元素的时间复杂度是O(1)。因此检索效率高。

底层是数组,内存存储是连续的,有下标、有偏移量,就可以用数学表达式很快的实现各种元素的访问

③ArrayList集合缺点:

​ 随机增删元素效率较低。不过只要数组的容量还没满,对末尾元素进行增删,效率不受影响。

④ArrayList集合适用场景:

​ 需要频繁的检索元素,并且很少的进行随机增删元素时建议使用。

⑤ArrayList默认初始化容量?

从源码角度可以看到,当调用无参数构造方法时,初始化容量0

​ 当第一次调用add方法时将ArrayList容量初始化为10个长度。

⑥ArrayList集合扩容策略?

​ 底层扩容会创建一个新的数组,然后使用数组拷贝。扩容之后的新容量是原容量的1.5倍。

如果正常情况下,就是10+5;15+15/2

⑦ArrayList集合源码分析:

  • 属性分析
  • 构造方法分析(使用ArrayList集合时最好也是预测大概数量,给定初始化容量,减少扩容次数。)
  • 添加元素:向末尾加add
  • 修改元素 set 返回olddata
  • 插入元素 中间插入,存在位移
  • 删除元素

添加删除都调用了arraycopy,增加效率

Vector2倍扩容

vector和stack都是线程安全的,但是效率不是很高,现在也有更好的控制线程安全的方法

①Vector底层也是数组,和ArrayList相同。

②不同的是Vector几乎所有的方法都是线程同步的

vector所有方法都被synchronized修饰:线程排队执行,不能并发),因此Vector是**==线程安全==**的,但由于效率较低,很少使用。因为控制线程安全有新方式。

③Vector初始化容量:10

④Vector扩容策略:扩容之后的容量是原容量的2倍。

链表

xxxxxxxxxx // 创建一个线程池对象(线程池中有3个线程)ExecutorService executorService = Executors.newFixedThreadPool(3);​// 将任务交给线程池(你不需要触碰到这个线程对象,你只需要将要处理的任务交给线程池即可。)executorService.submit(new Runnable() {    @Override    public void run() {        for (int i = 0; i < 10; i++) {            System.out.println(Thread.currentThread().getName() + “—>” + i);       }   }});​// 最后记得关闭线程池executorService.shutdown();java

image-20240229174036307

每个节点维护两个部分,一个头一个指针指向下一个节点。

内存地址不连续

优点:,增删效率O(1)

缺点:检索的效率很慢,挨个找,越大越慢

image-20240229174606521

删除结点O(1)

image-20240229174655439

新增结点O(1)

image-20240229174713907

②双向链表

image-20240229174432545

优点:查找效率比单向的快

缺点:查找效率虽然比单向的快,但是还是慢

③环形链表

​ 环形单链表

image-20240229175051590

​ 环形双链表

image-20240229175101942

④链表优点:

因为链表节点在空间存储上,内存地址不是连续的。因此删除某个节点时不需要涉及到元素位移的问题。==因此随机增删元素效率较高。时间复杂度O(1)==

⑤链表缺点:

==链表中元素在查找时,只能从某个节点开始顺序查找,因为链表节点的内存地址在空间上不是连续的。链表查找元素效率较低,时间复杂度O(n)==

⑥链表的适用场景:

​ 需要频繁进行随机增删,但很少的查找的操作时。

LinkedList

①LinkedList是一个**==双向链表==**

注意,虽然是双向链表,但是还是只能从头或者从尾开始寻找,效率还是非常的慢

②源码分析:

​ 属性分析

​ 构造方法分析

​ 添加元素:

add ,向末尾添加元素 addlast

​ 修改元素

set: 查找、修改

set()方法返回的是改前的值oldvalue

​ 插入元素

add(index):

​ 删除元素

remove()

手写单向链表

栈数据结构//使用的很少了,不建议使用了

这里是使用一个数组实现的

①LIFO原则(Last In,First Out):后进先出

②实现栈数据结构,可以用数组来实现,也可以用双向链表来实现。

③用数组实现的代表是:Stack、ArrayDeque

  1. Stack:Vetor的子类,实现了栈数据结构,除了具有Vetor的方法,还扩展了其它方法,完成了栈结构的模拟。不过在JDK1.6(Java6)之后就不建议使用了,因为它是线程安全的,太慢了。Stack中的方法如下:
1
2
3
4
5
6
7
E push(E item)//压栈

E pop()//弹栈(将栈顶元素删除,并返回被删除的引用)

int search(Object o)//查找栈中元素(返回值的意思是:以1为开始,从栈顶往下数第几个)

E peek()//窥视栈顶元素(不会将栈顶元素删除,只是看看栈顶元素是什么。注意:如果栈为空时会报异常。)

image-20240229185252920

返回4,search()栈顶是1,下一个是2……..

  1. ArrayDeque
1
2
E push(E item)
E pop()

④用链表实现的代表是:LinkedList

LinkedList实现了栈,可以直接创建Linklist进行pushback

1
2
3
E push(E item)

E pop()

队列数据结构queue(FIFO)

image-20240229185618282

①queue的删除插入

queue队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作(poll),而在表的后端(rear)进行插入操作(offer)。

队列是一种操作受限制的线性表。进行插入操作(入口)的端称为队尾,进行删除操作(出口)的端称为队头。

②队列是一种先进先出(First In First Out)的线性表,简称FIFO表

队列的插入操作只能在队尾操作,队列的删除操作只能在队头操作,因此

③Queue接口是一种基于FIFO(先进先出)的数据结构,而Deque接口则同时支持FIFO和LIFO(后进先出)两种操作。因此Deque接口也被称为“双端队列”。

④Java集合框架中队列的实现:

  • ①链表实现方式:LinkedList
  • ②数组实现方式:ArrayDeque
  • ③LinkedList和ArrayDeque都实现了Queue、Deque接口,因此这两个类都具备队列和双端队列的特性。
  • ④LinkedList底层是基于双向链表实现的,因此它天然就是一个双端队列,既支持从队尾入队,从队头出队,也支持从队头入队,从队尾出队。
  • 用Deque的实现方式来说,就是它既实现了队列的offer()和poll()方法,也实现了双端队列的offerFirst()、offerLast()、pollFirst()和pollLast()方法等。
  • ⑤ArrayDeque底层是使用环形数组实现的,也是一个双端队列。它比LinkedList更加高效,因为在数组中随机访问元素的时间复杂度是O(1),而链表中需要从头或尾部遍历链表寻找元素,时间复杂度是O(N)。
  • 循环数组:index = (start + i) % capacity

image-20240229190619501

①Queue接口基于Collection扩展的方法包括:

1
2
3
4
5
6
7
8
9
boolean offer(E e); 入队。

E poll(); 出队,如果队列为空,返回null

E remove(); 出队,如果队列为空,抛异常。

E peek(); 查看队头元素,如果为空则返回null

E element(); 查看对头元素,如果为空则抛异常。

①Deque接口基于Queen接口扩展的方法包括:

以下2个方法可模拟队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
boolean offerLast(E e);  从队尾入队

E pollFirst(); 从队头出队

以下4个方法可模拟双端队列:

boolean offerLast(E e); 从队尾入队

E pollFirst(); 从队头出队

boolean offerFirst(E e); 从队头入队

E pollLast(); 从队尾出队

另外offerLast+pollLast或者pollFirst+offerFirst可以模拟栈数据结构。或者也可以直接调用push/pop方法。

image-20240229191036405

Map继承结构(独立的)

set集合是map集合的一部分

map和collection之间没有继承关系

无序指的是插入顺序和实际的存储顺序不一致

image-20240229192702174

  • ①Map集合以key和value的键值对形式存储。key和value存储的都是引用。
  • ②Map集合中key起主导作用。value是附属在key上的。
  • ③SequencedMap是Java21新增的。
  • ④LinkedHashMap和TreeMap都是有序集合。(key是有序的)
  • ⑤HashMap,Hashtable,Properties都是无序集合。(key是无序的)
  • ⑥Map集合的key都是不可重复的key重复的话,value会覆盖。
  • ⑦HashSet集合底层是new了一个HashMap。往HashSet集合中存储元素实际上是将元素存储到HashMap集合的key部分。HashMap集合的key是无序不可重复的,因此HashSet集合就是无序不可重复的。HashMap集合底层是哈希表/散列表数据结构,因此HashSet底层也是哈希表/散列表。
  • TreeSet集合底层是new了一个TreeMap。往TreeSet集合中存储元素实际上是将元素存储到TreeMap集合的key部分。TreeMap集合的key是不可重复但可排序的,因此TreeSet集合就是不可重复但可排序的。TreeMap集合底层是红黑树,因此TreeSet底层也是红黑树。它们的排序通过java.lang.Comparable和java.util.Comparator均可实现。
  • ⑨LinkedHashSet集合底层是new了一个LinkedHashMap。LinkedHashMap集合只是为了保证元素的插入顺序,效率比HashSet低,底层采用的哈希表+双向链表实现。
  • ⑩根据源码可以看到向Set集合中add时,底层会向Map中put。value只是一个固定不变的常量,只是起到一个占位符的作用。主要是key。

Map接口常用方法

image-20240229193024307 image-20240229193404374

image-20240229193500437

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
V put(K key, V value); //添加键值对
//在底层会创建一个node结点,然后把kv作为结点的属性

void putAll(Map<? extends K,? extends V> m); //添加多个键值对

V get(Object key); //通过key获取value,不存在返回null

boolean containsKey(Object key); //是否包含某个key

boolean containsValue(Object value); //是否包含某个value
//一定用了equals

V remove(Object key); //通过key删除key-value

void clear(); //清空Map

int size(); //键值对个数

boolean isEmpty(); //判断是否为空Map(判断键值对的个数是否为0)
//空了是false
Collection<V> values(); //获取所有的value
// 取出所有的value,不管key了,生成一个collection集合
for(String value: values){
//这里就可以得到value
}
Set<K> keySet(); //获取所有的key

Set<Map.Entry<K,V>> entrySet(); //获取所有键值对的Set视图。

static <K,V> Map<K,V> of(K k1, V v1, K k2, V v2, K k3, V v3); //静态方法,使用现有的key-value构造Map

image-20240229192847865

Map怎么进行遍历

遍历Map集合的所有key,然后遍历每个key,通过key获得value。

Set keySet(); //获取所有的key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//方法一
Set<String> set = map.keySet();
Iterator<String> it = set.iterator();
while (it.hasNext()){
String key = it.next();
System.out.println(key + "=" + map.get(key));
}

System.out.println("----------------");
for (String key : set) {
System.out.println(key);
}


Set<Map.Entry<K,V>> entrySet(); //获取所有键值对的Set视图。

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

//遍历Map的第二种方法;这种方法效率比较的高
// Set<Map.Entry<K,V>> entrySet(); //获取所有键值对的Set视图。
// entry 是一个键值对的对象
Set<Map.Entry<String, String>> entrySet = mapself.entrySet();
Iterator<Map.Entry<String, String>> it2 = entrySet.iterator();
while (it2.hasNext()) {
Map.Entry<String, String> entry = it2.next();
String key = entry.getKey();
String value = entry.getValue();
System.out.println(key + "=" + value);
// System.out.println(entry.getKey() + "=" + entry.getValue());
}

HashMap(散列表)

hashset底层就是hashmap,把map中的所有k取出来就是set

hash是什么,是key的hashcode执行结果

transient Node<k,v>[] table;

get()方法:给出一个key,先计算hashcode,然后到相应的单链表里找,最多可以n/k 效率提高

image-20240301105201886

key进行hashCode编码,取模。

HashMap(散列表)

①HashMap集合的key是==无序不可重复==的。

  1. 无序:插入顺序和取出顺序不一定相同。

  2. 不可重复:key具有唯一性。

②向HashMap集合中put时,key如果重复的话,value会覆盖。

③HashMap集合的key具有唯一性,向key部分插入自定义的类型会怎样?如果自定义的类型重写equals之后会怎样???

如果没有重写equals,就会调用Object的equals,就是用==进行比较的,就是比较内存地址。

按照加双引号的字面量会在JVM开始的时候就在字符串常量池中创建的规则,相同的都放进去了

image-20240229232150946

④HashMap底层的数据结构是:哈希表/散列表

hash是key调Object的hashcode方法得到的

  • ①哈希表是一种查询和增删效率都很高的一种数据结构,非常重要,在很多场合使用,并且面试也很常见。必须掌握。
  • ②哈希表如何做到的查询和增删效率都好的呢,因为哈希表是“数组 + 链表”的结合体。数组和链表的结合不是绝对的。
  • ③哈希表可能是:数组 + 链表,数组 + 红黑树, 数组 + 链表 + 红黑树等。

⑤HashMap集合底层部分源码:

image-20240229235156266

长度永远是2的倍数,一维数组挂链表

哈希表存储原理

①概念:

1. 哈希表:一种数据结构的名字。
2. 哈希函数:
  • ①通过哈希函数可以将一个Java对象映射为一个数字。(就像现实世界中,每个人(对象)都会映射一个身份证号(哈希值)一样。)
  • ②也就是说通过哈希函数的执行可以得到一个哈希值。
  • ③在Java中,hashCode()方法就是哈希函数。
  • ④也就是说hashCode()方法的返回值就是哈希值。
  • ⑤一个好的哈希函数,可以让散列分布均匀。
3.哈希值:也叫做哈希码。是哈希函数执行的结果。
4.哈希碰撞:也叫做哈希冲突。
  • ①当两个对象“哈希值%数组长度”之后得到的下标相同时,就发生了哈希冲突。
  • ②如何解决哈希冲突?将冲突的挂到同一个链表上或同一个红黑树上。
5.以上描述凡是“哈希”都可以换为“散列”。

②重点:

  • ①存放在HashMap集合key部分的元素必须同时重写hashCode+equals方法。

如果是

  • ②equals返回true时,hashCode必须相同。

Hashcode和equals要同时重写

image-20240301123921129

手写HashMap的put方法

①【第一步】:处理key为null的情况

key可以是null,null只能有一个

如果添加键值对的key就是null,则将该键值对存储到table数组索引为0的位置。

②【第二步】:获得key对象的哈希值

如果添加键值对的key不是null,则就调用key的hashcode()方法,获得key的哈希值。

③【第三步】:获得键值对的存储位置

因为获得的哈希值在数组合法索引范围之外,因此我们就需要将获得的哈希值转化为[0,数组长度-1]范围的整数,

那么可以通过取模法来实现,也就是通过“哈希值 % 数组长度”来获得索引位置(i)。

④【第四步】:将键值对添加到table数组中

当table[i]返回结果为null时,则键键值对封装为Node对象并存入到table[i]的位置。

当table[i]返回结果不为null时,则意味着table[i]存储的是单链表。我们首先遍历单链表,如果遍历出来节点的

key和添加键值对的key相同,那么就执行覆盖操作;如果遍历出来节点的key和添加键值对的key都不同,则就将键键

值对封装为Node对象并插入到单链表末尾。

HashMap在Java8后的改进(包含Java8)

image-20240301130406549

image-20240301130412465

①初始化时机:

  • ①Java8之前,构造方法执行时初始化table数组。
  • ②Java8之后,第一次调用put方法时初始化table数组。

②插入法:

  • ①Java8之前,头插法
  • ②Java8之后,尾插法

③数据结构:

  • ①Java8之前:数组 + 单向链表
  • ②Java8之后:数组 + 单向链表 + 红黑树。
  • ③最开始使用单向链表解决哈希冲突。如果结点数量 >= 8,并且table的长度 >= 64。单向链表转换为红黑树。
  • ④当删除红黑树上的结点时,结点数量 <= 6 时。红黑树转换为单向链表。

HashMap初始化容量永远都是2的次幂

①HashMap集合初始化容量16(第一次调用put方法时初始化)

②HashMap集合的容量永远都是2的次幂,假如给定初始化容量为31,它底层也会变成32的容量。

③将容量设置为2的次幂作用是:加快哈希计算,减少哈希冲突。

④为什么会加快哈希计算?

  • ①首先你要知道,使用二进制运算是最快的。
  • ②当一个数字是2的次幂时,例如数组的长度是2的次幂:

①hash & (length-1) 的结果和 hash % length的结果相同。

②注意:只有是2的次幂时,以上等式才会成立。因为了使用 & 运算符,让效率提升,因此建议容量一直是2的次幂。

⑤为什么会减少哈希冲突?

  • ①底层运算是:hash & length - 1
  • ②如果length是偶数:length-1后一定是奇数,奇数二进制位最后一位一定是1,1和其他二进制位进行与运算,结果可能是1,也可能是0,这样可以减少哈希冲突,让散列分布更加均匀。
  • ③如果length是奇数:length-1后一定是偶数,偶数二进制位最后一位一定是0,0和任何数进行与运算,结果一定是0,这样就会导致发生大量的哈希冲突,白白浪费了一半的空间。

image-20240301134520761

  • LinkedHashMap

Hashtable

Properties

二叉树与红黑二叉树

TreeMap(底层红黑树)

Set接口(无序、不可重复)

image-20240229191216355

Collections工具类

IO流

IO流概述

①什么是IO流?

水分子的移动形成了水流。

IO流指的是:程序中数据的流动。数据可以从内存流动到硬盘,也可以从硬盘流动到内存。

Java中IO流最基本的作用是:完成文件的读和写。

②IO流的分类?

根据数据流向分为:输入和输出是相对于内存而言的。

​ ①输入流:从硬盘到内存。(输入又叫做读:read)

​ ②输出流:从内存到硬盘。(输出又叫做写:write)

根据读写数据形式分为:

​ ①字节流:一次读取一个字节。适合读取非文本数据。例如图片、声音、视频等文件。(当然字节流是万能的。什么都可以读和写。)

​ ②字符流:一次读取一个字符。只适合读取普通文本。不适合读取二进制文件。因为字符流统一使用Unicode编码,可以避免出现编码混乱的问题。

==注意:Java的所有IO流中凡是以Stream结尾的都是字节流。凡是以Reader和Writer结尾的都是字符流。==

根据流在IO操作中的作用和实现方式来分类:

​ ①节点流:节点流负责数据源和数据目的地的连接,是IO中最基本的组成部分。

​ ②处理流:处理流对节点流进行装饰/包装,提供更多高级处理操作,方便用户进行数据处理。

​ ③Java中已经将io流实现了,在java.io包下,可以直接使用。

IO流体系结构

image-20240301144110666

image-20240301144135589

IO流的体系结构

①右图是常用的IO流。实际上IO流远远不止这些。

②InputStream:字节输入流

③OutputStream:字节输出流

④Reader:字符输入流

⑤Writer:字符输出流

⑥以上4个流都是抽象类,是所有IO流的四大头领!!!

⑦所有的流都实现了Closeable接口,都有close()方法,流用完要关闭。

finally{

close()

}

⑧所有的输出流都实现了Flushable接口,都有flush()方法,flush方法的作用是,将缓存清空,全部写出。养成好习惯,以防数据丢失。

FileInputStream

①文件字节输入流,可以读取任何文件。

②常用构造方法

​ FileInputStream(String name):创建一个文件字节输入流对象,参数是文件的路径

image-20240301161928552

③常用方法

1
2
3
4
5
6
7
8
9
10
11
12
int read();//从文件读取一个字节(8个二进制位),返回值读取到的字节本身,如果读不到任何数据返回-1

int read(byte[] b); //一次读取多个字节,如果文件内容足够多,则一次最多读取b.length个字节。返回值是读取到字节总数。如果没有读取到任何数据,则返回 -1

int read(byte[] b, int off, int len); 读到数据后向byte数组中存放时,从off开始存放,最多读取len个字节。读取不到任何数据则返回 -1

long skip(long n); 跳过n个字节

int available(); //返回流中剩余的估计字节数量。

void close() //关闭流。
flush 把缓存里的内容全部写出 全部清空

④使用FileInputStream读取的文件中有中文时,有可能读取到中文某个汉字的一半,在将byte[]数组转换为String时可能出现乱码问题,因此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
public static void main(String[] args) throws IOException {
FileInputStream in = new FileInputStream("src/hello.txt");
//一个字符一个字符读,读取ascii码
int readByte = 0 ;
while((readByte = in.read() )!= -1){
System.out.println(readByte);
}

System.out.println("Hello world!");


}

FileInputStream fis = null;
try {
fis = new FileInputStream("src/hello.txt");
int readByte = 0;
byte[] bytes = new byte[4];
// 这步就是把读取的4个字节放到这个byte数组当中
int readCount = fis.read(bytes);
String s1 = new Strin (bytes);
System.out.println(s1);
System.out.println("the first time:"+readCount);

} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}

FileOutputStream

文件字节输出流。

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

常用构造方法:
FileOutputStream(String name) //创建输出流,先将文件清空,再不断写入。
FileOutputStream(String name, boolean append) //创建输出流,在原文件最后面以追加形式不断写入。
append = true 不会清空文件的内容,在源文件的末尾追加写入
append =false 会清空源文件的内容,然后写入
常用方法:
write(int b) //写一个字节
void write(byte[] b); //将字节数组中所有数据全部写出
void write(byte[] b, int off, int len); //将字节数组的一部分写出
void close() //关闭流
void flush() //刷新
使用FileInputStream和FileOutputStream完成文件的复制。

image-20240302140356077

copy文件

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
package com.ljy.CollectionTest.IOstream;

import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;

/**
* 文件拷贝的原理:
* 使用FileInputStream读取文件,然后使用FileOutputStream写文件
* 一边读一边写
*
*
*/
public class FileInputOutputStreamCopy {
public static void main(String[] args) {
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream("D:\\Blog-hexo\\source\\_posts\\Java基础部分4-集合.md");
out = new FileOutputStream("D:\\job\\code\\Java-Test\\src\\Java基础部分4-集合.md");

byte[] bytes= new byte[1024];
int readCount = 0;
while((readCount = in.read(bytes))!=-1){
out.write(bytes,0,readCount);
}


} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
// 分别try...catch
if (in != null){
try {
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(out != null){
try {
out.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}

}

Try-With-Resource

资源自动关闭

image-20240302144025280

1
2
3
4
5
6
7
try (FileInputStream in = new FileInputStream(("D:\\job\\code\\Java-Test\\src\\hello.txt"))){
in2 = in ;
} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}

这样可以实现资源的自动管理,如果在trycatch外面对于流进行读取,就会报“流已经关闭错误”

FileReader

FileInputStream 内部是byte[],这个内部是char[]

用法非常的相似

image-20240302145913644

1
2
3
4
5
6
7
8
9
10

常用的构造方法:
FileReader(String fileName)
常用的方法:
int read()
int read(char[] cbuf);
int read(char[] cbuf, int off, int len);
long skip(long n);
void close()

FileWriter

文件字符输出流,写普通文本用的。

image-20240302150241002

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

常用的构造方法:
FileWriter(String fileName)
FileWriter(String fileName, boolean append)
常用的方法:
void write(char[] cbuf)
void write(char[] cbuf, int off, int len);
void write(String str);
void write(String str, int off, int len);
void flush();
void close();
Writer append(CharSequence csq, int start, int end)
使用FileReader和FileWriter拷贝普通文本文件

copy文件

用这种方式只能复制普通文本文件

image-20240302150533402

读写文件的路径问题

从类路径加载资源

1
2
String path =  Thread.currentThread().getContextClassLoader().getResource("自动从类中加载资源").getPath();
System.out.println(path);

image-20240302151047885src

如果这个资源放在类路径之外,就不合适了

image-20240302151334502

缓冲流(读大文件的时候建议)

带缓冲区的,效率会比较的高

FileInputStream是一个节点流

BufferedInputStream是一个缓冲流,效率高,自带缓冲区

①BufferedInputStream、BufferedOutputStream(适合读写非普通文本文件)

②BufferedReader、BufferedWriter(适合读写普通文本文件。)

image-20240302152302843

image-20240302154753142

BufferedInputStream\BufferedOutputStream

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    BufferedInputStream bis = null;

try {
try {
bis = new BufferedInputStream(new FileInputStream("D:\\job\\code\\Java-Test\\src\\hello.txt"));
} catch (FileNotFoundException ex) {
throw new RuntimeException(ex);
}
} catch(Exception e){
e.printStackTrace();
}
}


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

long begin = System.currentTimeMillis();

try(BufferedInputStream bis = new BufferedInputStream(new FileInputStream("D:\\job\\code\\Java-Test\\src\\hello.txt"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("D:\\job\\code\\Java-Test\\src\\hello3.txt"))){
byte[] bytes = new byte[1024];
int readCount = 0;
while ((readCount = bis.read(bytes))!=-1){
bos.write(bytes,0,readCount);
}


bos.flush();
}
catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
long end = System.currentTimeMillis();
// double time = ();
System.out.println(end-begin);

image-20240302160450774

image-20240302161703690

③缓冲流的读写速度快原理

在内存中准备了一个缓存。读的时候从缓存中读。写的时候将缓存中的数据一次写出。都是在减少和磁盘的交互次数。如何理解缓冲区?家里盖房子,有一堆砖头要搬在工地100米外,单字节的读取就好比你一个人每次搬一块砖头,从堆砖头的地方搬到工地,这样肯定效率低下。然而聪明的人类会用小推车,每次先搬砖头搬到小车上,再利用小推车运到工地上去,这样你再从小推车上取砖头是不是方便多了呀!这样效率就会大大提高,缓冲流就好比我们的小推车,给数据暂时提供一个可存放的空间。

④缓冲流都是处理流/包装流。FileInputStream/FileOutputStream是节点流。

⑤关闭流只需要关闭最外层的处理流即可,通过源码就可以看到,当关闭处理流时,底层节点流也会关闭。

⑥输出效率是如何提高的?

在缓冲区中先将字符数据存储起来,当缓冲区达到一定大小或者需要刷新缓冲区时,再将数据一次性输出到目标设备。

⑦输入效率是如何提高的?

read()方法从缓冲区中读取数据。当缓冲区中的数据不足时,它会自动从底层输入流中读取一定大小的数据,并将数据存储到缓冲区中。大部分情况下,我们调用read()方法时,都是从缓冲区中读取,而不需要和硬盘交互。

⑧可以编写拷贝的程序测试一下缓冲流的效率是否提高了!

⑨缓冲流的特有方法(输入流):以下两个方法的作用是允许我们在读取数据流时回退到原来的位置(重复读取数据时用)

  • ①void mark(int readAheadLimit); 标记位置(在Java21版本中,参数无意义。

    低版本JDK中参数表示在标记处最多可以读取的字符数量,如果你读取的字符数超出的上限值,则调用reset()方法时出现IOException。)

  • ②void reset(); 重新回到上一次标记的位置

  • ③这两个方法有先后顺序:先mark再reset,另外这两个方法不是在所有流中都能用。有些流中有这个方法,但是不能用。

image-20240302162351497

image-20240302162641340

转换流InputStreamReader,OutputStreamWriter(解决乱码问题)

字符流 、转换流,是一个输入的过程,是一个解码的过程

节点流直接连接两个设备

image-20240302180011775

InputStreamReader(主要解决读的乱码问题)

①InputStreamReader为转换流,属于字符流。

②作用是将文件中的字节转换为程序中的字符。转换过程是一个解码的过程。

③常用的构造方法:

  1.  InputStreamReader(InputStream in, String charsetName) // 指定字符集
     InputStreamReader(InputStream in) // 采用平台默认字符集
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    ![image-20240302180542368](../blog_images/Java%E5%9F%BA%E7%A1%80%E9%83%A8%E5%88%864-%E9%9B%86%E5%90%88/image-20240302180542368.png)

    ④乱码是如何产生的?文件的字符集和构造方法上指定的字符集不一致。

    ⑤**FileReader**是InputStreamReader的子类。本质上以下代码是一样的:

    ```java
    Reader reader = new InputStreamReader(new FileInputStream(“file.txt”)); //采用平台默认字符集

    Reader reader = new FileReader(“file.txt”); //采用平台默认字符集

因此FileReader的出现简化了代码的编写。

以下代码本质上也是一样的:

1
2
3
Reader reader = new InputStreamReader(new FileInputStream(“file.txt”), “GBK”);

Reader reader = new FileReader("e:/file1.txt", Charset.forName("GBK"));

OutputStreamWriter(主要解决写的乱码问题)

①OutputStreamWriter是转换流,属于字符流。

作用是将程序中的字符转换为文件中的字节。这个过程是一个编码的过程。

③常用构造方法:

1
2
3
4
OutputStreamWriter(OutputStream out, String charsetName) // 使用指定的字符集

OutputStreamWriter(OutputStream out) //采用平台默认字符集

④乱码是如何产生的?文件的字符集与程序中构造方法上的字符集不一致。

FileWriter是OutputStreamWriter的子类。以下代码本质上是一样的:

1
2
3
4
5
Writer writer = new OutputStreamWriter(new FileOutputStream(“file1.txt”)); // 采用平台默认字符集

Writer writer = new FileWriter(“file1.txt”); // 采用平台默认字符集


因此FileWriter的出现,简化了代码。

以下代码本质上也是一样的:

1
2
3
Writer writer = new OutputStreamWriter(new FileOutputStream(“file1.txt”), “GBK”);

Writer writer = new FileWriter(“file1.txt”, Charset.forName(“GBK”));

数据流(DataOutputStream)

不涉及到任何的转换 ,二进制输入二进制出。

如果要恢复DataOutputStream的数据,要用DatainputStream

①这两个流都是包装流,读写数据专用的流。

②DataOutputStream直接将java程序中的数据写入文件,不需要转码,效率高。程序中是什么样子,原封不动的写出去。写完后,文件是打不开的。即使打开也是乱码,文件中直接存储的是二进制。

③使用DataOutputStream写的文件,只能使用DataInputStream去读取。并且读取的顺序需要和写入的顺序一致,这样才能保证数据恢复原样。

一次读八个比特位

④构造方法:

1
2
3
DataInputStream(InputStream in)

DataOutputStream(OutputStream out)

①写的方法:

writeByte()、writeShort()、writeInt()、writeLong()、writeFloat()、writeDouble()、writeBoolean()、writeChar()、writeUTF(String)

②读的方法:

readByte()、readShort()、readInt()、readLong()、readFloat()、readDouble()、readBoolean()、readChar()、readUTF()

image-20240302181915656

image-20240302182124009

对象流(对象字节输出流)(ObjectOutputStream/ObjectInputStream)

负责java对象Object的输入与输出,传输的其实还是是二进制,是对象的二进制,字节序列所以存在一个序列化与反序列化

image-20240302184337118

序列化

将Java对象转换成字节序列的过程(字节序列可以在网络中传输)

ObjectInputStream

1
2
3
4
5
6
7
8
9
try (ObjectInputStream oos = new ObjectInputStream(new FileInputStream("Object"))) {
Date nowTime = new Date();
Object o = oos.readObject();
System.out.println(o);


} catch (IOException | ClassNotFoundException e) {
throw new RuntimeException(e);
}

①通过这两个流,可以完成对象的序列化和反序列化。

②序列化(Serial):将Java对象转换为字节序列。(为了方便在网络中传输),使用ObjectOutputStream序列化。

如果要序列化多个,会序列化一个集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        Date date = new Date();
Date date1 = new Date();
Date date2 = new Date();
Date date3 = new Date();
Date date4 = new Date();
List<Date> list = new ArrayList<>();
list.add(date1);
list.add(date2);
list.add(date3);
list.add(date4);
// list.add(date5);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("date"));
oos.writeObject(list);
oos.flush();
oos.close();

③反序列化(DeSerial):将字节序列转换为Java对象。使用ObjectInputStream进行反序列化。

1
2
3
4
5
6
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("date"));
List<Date> dates = (List<Date>) ois.readObject();
for(Date d:dates){
System.out.println(d);
}
ois.close();

④参与序列化和反序列化的必须实现一个标志性接口:java.io.Serializable,自定义对象

image-20240302192027724

⑤实现了Serializable接口的类,编译器会自动给该类添加序列化版本号的属性:**==serialVersionUID==**

这是一个标志接口,没有任何的方法,只是起到一个标志类型的作用

⑥在java中,是通过“类名 + 序列化版本号”来进行类的区分的。

serialVersionUID实际上是一种安全机制。在反序列化的时候,JVM会去检查存储Java对象的文件中的class的序列化版本号是否和当前Java程序中的class的序列化版本号是否一致。如果一致则可以反序列化。如果不一致则报错。

同一个实现了serializatable接口的类,存在变化,它的序列号也会存在变换,这样才可以是的这个 序列化与反序列化可以保持一致。

如果只通过类名来进行类的区分,这样太过于危险

⑧如果一个类实现了Serializable接口,还是建议将序列化版本号固定死,原因是:

类有可能在开发中升级(改动),升级后会重新编译,如果没有固定死,编译器会重新分配一个新的序列化版本号,导致之前序列化的对象无法反序列化。显示定义序列化版本号的语法:

1
private static final long serialVersionUID = XXL;

image-20240302194529991

加了这个语句,可以让这个类的序列号保持一致,可以让这个类的对象反序列化正常

⑨为了保证显示定义的序列化版本号不会写错,建议使用 @java.io.Serial 注解进行标注。并且使用它还可以帮助我们随机生成序列化版本号。

image-20240302195434569

⑩不参与序列化的属性需要使用瞬时关键字修饰:transient

有些属性不想让他参与序列化

image-20240302195633121

ObjectOutputStream

1
2
3
4
5
6
7
8
9
10
11
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("Object"))) {
Date nowTime = new Date();
oos.writeObject(nowTime);



} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}

打印流(PrintStream PrintWriter)

PrintStream

①打印流(字节形式)

②主要用在打印方面,提供便捷的打印方法和格式化输出。主要打印内容到文件或控制台。

③常用方法:

​ 1. print(Type x)

2. println(Type x)

image-20240302202657284

④便捷在哪里?

​ 1.直接输出各种数据类型

​ 2.自动刷新和自动换行(println方法)

​ 3.支持字符串转义

​ 4.自动编码(自动根据环境选择合适的编码方式)

⑤格式化输出?调用printf方法。

​ 1.%s 表示字符串

​ 2.%d 表示整数

​ 3.%f 表示小数(%.2f 这个格式就代表保留两位小数的数字。)

​ 4.%c 表示字符

PrintWriter

①打印流(字符形式)注意PrintWriter使用时需要手动调用flush()方法进行刷新。

②比PrintStream多一个构造方法,PrintStream参数只能是OutputStream类型,但PrintWriter参数可以是OutputStream,也可以是Writer。

③常用方法:

1
2
3
print(Type x)

println(Type x)

④同样,也可以支持格式化输出,调用printf方法。

构造方法

1
2
3
PrintStream(OutputStream)
PrintWriter(OutputStream)
PrintWriter(Writer)

image-20240302203434842

标准输入流&标准输出流

标准输入流

①System.in获取到的InputStream就是一个标准输入流。

1
InputStream in = System.in;//直接可以获取一个标准

②**==标准输入流是用来接收用户在控制台上的输入的==。(==普通的输入流,是获得文件或网络中的数据==**)

③标准输入流不需要关闭。(它是一个系统级的全局的流,JVM负责最后的关闭。)

image-20240302204207119

④也可以使用BufferedReader对标准输入流进行包装。这样可以方便的接收用户在控制台上的输入。(这种方式太麻烦了,因此JDK中提供了更好用的Scanner。)

1
2
3
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));

String s = br.readLine();

⑤当然,你也可以修改输入流的方向(System.setIn())。让其指向文件。

image-20240302204259068

标准输出流

①**==System.out==获取到的==PrintStream==**就是一个标准输出流。

②标准输出流是用来向控制台上输出的。(普通的输出流,是向文件和网络等输出的。)

③标准输出流不需要关闭(它是一个系统级的全局的流,JVM负责最后的关闭。)也不需要手动刷新。

④当然,你也可以修改输出流的方向(System.setOut())。让其指向文件。

1
2
PrintStream out = System.out;
out.println("hello")

image-20240302204919884

这样会输出到文件log里面,通常用这种方式来记录日志

image-20240302205026504

Scanner

image-20240302204523731

image-20240302204606573

File类(父类是object)

①File类不是IO流,和IO的四个头领没有关系。因此通过File是无法读写文件。

②**==File类是路径的抽象表示形式==,这个路径可能是目录,也可能是文件。因此==File代表了某个文件或某个目录==**。

③File类常用的构造方法:

1
File(String pathname);

④File类的常用方法:

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
boolean createNewFile(); 

boolean delete();

boolean exists();

String getAbsolutePath();//获取绝对路径

String getName();

String getParent();//获取父路径

boolean isAbsolute(); //判断该路径是不是一个绝对路径

boolean isDirectory();//是不是个目录

boolean isFile(); //是不是个文件

boolean isHidden();//是否是一个隐藏文件

long lastModified(); //文件最后的修改时间

long length();//总字节数

File[] listFiles(); //获得所有的子文件,包括子目录

File[] listFiles(FilenameFilter filter);//对于子文件有选择的过滤,

boolean mkdir();

boolean mkdirs();

boolean renameTo(File dest);//重命名

boolean setReadOnly();

boolean setWritable(boolean writable);

⑤编写程序要求可以完成目录的拷贝。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
        File file = new File("file");
System.out.println(file.exists()?"存在":"不存在");
/*
if(!file.exists()){
file.createNewFile();
}*/
if(!file.exists()){
file.mkdir();
}
File file1 = new File("D:/A/B/C");
if(!file1.exists()){
file1.mkdirs();
}
if(file1.exists()){
file1.delete();
}
File file2 = new File("hello.txt");
System.out.println(file2.getAbsolutePath());

File[] files = file1.listFiles();
for(File f:files){
System.out.println(f.getName());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
File[] files1 = file3.listFiles(new FileFilter() {
@Override
public boolean accept(File pathname) {
if(pathname.getName().endsWith(".json"))
{
return true;
}

return false;
}
});
for(File f:files1){
System.out.println(f.getName());
}

File复制

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
package com.ljy.CollectionTest.IOstream;

import java.io.*;

public class FileTestcopy {
public void deleteFile(File file){
// File file1 = new File();
if(file.isFile()){
file.delete();
return;
}

File[] files = file.listFiles();
int length = files.length;
if(length==0){
file.delete();
return;
}
for(File fileitem : files){
deleteFile(fileitem);
}

}
public void copyFile(String fileRoot,File fileOri ,String distPath){
//fileDst是一直不改变的,就是copy的根目录
//fileOri现在就是一个文件,不是一个文件夹了,
String relativePath = fileOri.getAbsolutePath().substring(fileRoot.length());
// System.out.println(relativePath);
String distPathFile = distPath + relativePath;
File fileDist = new File(distPathFile);
FileInputStream in = null;
FileOutputStream out = null;
try {
in = new FileInputStream(fileOri);
out = new FileOutputStream(fileDist);

byte[] bytes= new byte[1024];
int readCount = 0;
while((readCount = in.read(bytes))!=-1){
out.write(bytes,0,readCount);
}


} catch (FileNotFoundException e) {
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}finally {
// 分别try...catch
if (in != null){
try {
in.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
if(out != null){
try {
out.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
public void copyFiles(String fileRoot,File file ,String distPath) throws IOException {
if(file.isFile()){
copyFile(fileRoot,file,distPath);
// file.delete();
return;
}

File[] files = file.listFiles();
int length = files.length;
for(File fileitem : files){
if(fileitem.isDirectory()){
String relativePath = fileitem.getAbsolutePath().substring(fileRoot.length());
// System.out.println(relativePath);
String distPathFile = distPath + relativePath;
File fileDist = new File(distPathFile);
fileDist.mkdir();
}
copyFiles(fileRoot,fileitem,distPath);
}
}
public static void main(String[] args) throws IOException {
File file1 = new File("D:\\job\\dddd");
File file = new File("D:\\job\\cccc");
file.mkdir();
String distPath = "D:\\job\\cccc";
System.out.println(file1.exists()?"存在":"不存在");
//递归的找出全部的文件,然后全部一个个的复制a
// File[] files = file1.listFiles();
// int length = files.length;

FileTestcopy fileTest = new FileTestcopy();
// fileTest.deleteFile(file1,file);
fileTest.copyFiles(file1.getAbsolutePath(),file1,distPath);
}
}

读取属性配置文件

Properties + IO

①xxx.properties文件称为属性配置文件。

②属性配置文件可以配置一些简单的信息,例如连接数据库的信息通常配置到属性文件中。这样可以做到在不修改java代码的前提下,切换数据库。

③属性配置文件的格式:

1
2
3
4
5
key1=value1

key2=value2

key3=value3

注意:使用 # 进行注释。key不能重复,key重复则value覆盖。key和value之间用等号分割。等号两边不要有空格。

④Java中如何读取属性配置文件?

⑤当然,也可以使用Java中的工具类快速获取配置信息:ResourceBundle

​ 这种方式要求文件必须是xxx.properties

​ 属性配置文件必须放在类路径当中\

image-20240303095924672

image-20240303095943567

image-20240303095951419

image-20240303102321936

装饰器设计模式(Decorator Pattern)

思考:如何扩展一个类的功能?继承确实也可以扩展对象的功能,但是接口下的实现类很多,每一个子类都需要提供一个子类。就需要编写大量的子类来重写父类的方法。会导致子类数量至少翻倍,会导致类爆炸问题。

②装饰器设计模式是GoF23种设计模式之一,属于结构型设计模式。(结构型设计模式通常处理对象和类之间的关系,使程序员能够更好地组织代码并更好地利用现有代码。)

③IO流中使用了大量的装饰器设计模式。

④装饰器设计模式作用:装饰器模式可以做到在不修改原有代码的基础之上,完成功能扩展,符合OCP原则。并且避免了使用继承带来的类爆炸问题。

⑤装饰器设计模式中涉及到的角色包括:

​ ①抽象的装饰者

​ ②具体的装饰者1、具体的装饰者2

​ ③被装饰者

​ ④装饰者和被装饰者的公共接口/公共抽象类

装饰者和被装饰者,应该实现同一个接口/同一些接口,继承同一个抽象类

原因:因为实现了同一个接口之后,对于客户端程序来说,使用装饰者的时候就像是使用被装饰者一样

在松耦合的前提下完成功能的扩展

装饰者含有被装饰者的引用

压缩和解压缩流

①使用GZIPOutputStream可以将文件制作为压缩文件,压缩文件的格式为 .gz 格式。

②核心代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
1.FileInputStream fis = new FileInputStream("d:/test.txt"); // 被压缩的文件:test.txt

2.GZIPOutputStream gzos = new GZIPOutputStream(new FileOutputStream("d:/test.txt.gz")) // 压缩后的文件

3.接下来就是边读边写:

int length;

while ((length = fis.read(buffer)) > 0) {

gzos.write(buffer, 0, length);

}

4.gzos.finish(); // 在压缩完所有数据之后调用finish()方法,以确保所有未压缩的数据都被刷新到输出流中,并生成必要的 Gzip 结束标记,标志着压缩数据的结束

③注意(补充):实际上所有的输出流中,只有带有缓冲区的流才需要手动刷新,节点流是不需要手动刷新的,节点流在关闭的时候会自动刷新。

字节数组流

①ByteArrayInputStream和ByteArrayOutputStream都是内存操作流,不需要打开和关闭文件等操作。这些流是非常常用的,可以将它们看作开发中的常用工具,能够方便地读写字节数组、图像数据等内存中的数据。

②ByteArrayInputStream和ByteArrayOutputStream都是节点流。

③ByteArrayOutputStream,将数据写入到内存中的字节数组当中。

④ByteArrayInputStream,读取内存中某个字节数组中的数据。

image-20240303150009397

image-20240303150013627

对象克隆

对象的深克隆

①除了我们之前所讲的深克隆方式(之前的深克隆是重写clone()方法)。使用字节数组流也可以完成对象的深克隆。

②原理是:将要克隆的Java对象写到内存中的字节数组中,再从内存中的字节数组中读取对象,读取到的对象就是一个深克隆。

③目前为止,对象拷贝方式:

①调用Object的clone方法,默认是浅克隆,需要深克隆的话,就需要重写clone方法。

②可以通过序列化和反序列化完成对象的克隆。

③也可以通过ByteArrayInputStream和ByteArrayOutputStream完成深克隆。

数组

数组概述

①什么是数组?

1.在Java中,数组是一种用于存储多个相同数据类型元素的容器。

2.例如一个存储整数的数组:int[] nums = {100, 200, 300};

3.例如一个存储字符串的数组:String[] names = {“jack”,“lucy”,“lisi”};

4.数组是一种引用数据类型,隐式继承Object。因此数组也可以调用Object类中的方法。

5.数组对象存储在堆内存中。

②数组的分类?

1.根据维数进行分类:一维数组,二维数组,三维数组,多维数组。

2.根据数组中存储的元素类型分类:基本类型数组,引用类型数组。

3.根据数组初始化方式不同分类:静态数组,动态数组。

③Java数组存储元素的特点?

1.数组长度一旦确定不可变。

2.数组中元素数据类型一致,每个元素占用空间大小相同。

3.数组中每个元素在空间存储上,内存地址是连续的。

4.每个元素有索引,首元素索引0,以1递增。

5.以首元素的内存地址作为数组对象在堆内存中的地址。

6.所有数组对象都有length属性用来获取数组元素个数。末尾元素下标:length-1

①数组优点?

1.根据下标查询某个元素的效率极高。数组中有100个元素和有100万个元素,查询效率相同。时间复杂度O(1)。也就是说在数组中根据下标查询某个元素时,不管数组的长短,耗费时间是固定不变的。

原因:知道首元素内存地址,元素在空间存储上内存地址又是连续的,每个元素占用空间大小相同,只要知道下标,就可以通过数学表达式计算出来要查找元素的内存地址。直接通过内存地址定位元素。

②数组缺点?

1.随机增删元素的效率较低。因为随机增删元素时,为了保证数组中元素的内存地址连续,就需要涉及到后续元素的位移问题。时间复杂度O(n)。O(n)表示的是线性阶,随着问题规模n的不断增大,时间复杂度不断增大,算法的执行效率越低。(不过需要注意的是:对数组末尾元素的增删效率是不受影响的。)

2.无法存储大量数据,因为很难在内存上找到非常大的一块连续的内存。

一维数组

①一维数组是线性结构。二维数组,三维数组,多维数组是非线性结构。

②如何静态初始化一维数组?

1.第一种:int[] arr = {55,67,22}; 或者 int arr[] = {55,67,22};

2.第二种:int[] arr = new int[]{55,67,22};

1
2
Animal[] animals = {a1,a2,new Animal()};
//静态初始化

③如何访问数组中的元素?

1.通过下标来访问。

2.注意ArrayIndexOutOfBoundsException异常的发生。

④如何遍历数组?

1.普通for循环遍历

2.for-each遍历(优点是代码简洁。缺点是没有下标。)

1
2
3
4
5
6
7
8
for(int i = 0 ; i < citys.length;i++){
System.out.println(citys[i]);
}
//for each
for(String name:names){
System.out.println(name);
}

⑤如何动态初始化一维数组?

在创建数组的时候,不确定具体存储那些数据,但是确定长度(就是开辟空间)

数据类型[] 变量名 = new 数据类型[长度]

1.int[] arr = new int[4];

2.Object[] objs = new Object[5];

3.数组动态初始化的时候,确定长度,并且数组中每个元素采用默认值。

⑥方法在调用时如何给方法传一个数组对象?

1
2
3
4
5
6
7
8
9
10
11
   public static void display(int[] arr){
for(int i : arr){
System.out.println(i);
}
}

int[] a = {1,2,3};
display(a);
display({1,2,3})//error
display(new int[]{1,2,3});//静态初始化
display(new int[10]);//动态初始化

⑦当一维数组中存储引用时的内存图?

1
2
3
4
5
6
Animals[] animals = new Animals[5];

Object[] test = {new Bird(),new Cat()}
public class Animal {
}

image-20240227110419147

数组中存储的每个元素的空间大小都是一样的,所以空间是不可能一样的,所以是不可能存储引用对象的,所以是存储的是引用(对象在堆内存中的地址)

image-20240227111451718

image-20240227111729307

⑧如何获取数组中的最大值?

​ 假设首元素是最大的,然后遍历数组中所有元素,只要有更大的,就将其作为最大值。

​ 思考:找出最大值的下标怎么做?

⑨如果知道值,如何通过值找它的下标?

⑩如何将数组中的所有元素反转?

第一种方式:创建一个新的数组。

就是倒着遍历第一个,然后赋值过去

第二种方式:首尾交换。

就是第一个和倒数第一个交换,以此类推

⑾关于main方法的形参args?

​ 接收命令行参数,

​ JVM 负责调用这个类名.main()方法的

image-20240227112624635

JVM会把以上字符串以“空格”进行拆分,生成一个新的数组对象

最后这个数组对象是String[] args = {“abc”,”def”,”xyz”}

​ 在DOS命令窗口中怎么传?在IDEA中怎么传?

image-20240227112947509

⑿关于方法的可变长度参数?

​ 可变长参数只能出现在形参列表中的最后一个位置。

​ 可变长参数可以当做数组来处理。

1
2
3
4
5
6
public static void mi(int... nums)
{
for(int i = m0; i < nums;i++){
System.out.println(nums[i])
}
}

⒀一维数组的扩容

①数组长度一旦确定不可变。

②那数组应该如何扩容?

==只能创建一个更大的数组将原数组中的数据全部拷贝到新数组中==

==可以使用System.arraycopy()方法完成数组的拷贝。==

1
System.arraycopy(src,srcpos:0,dest,destpos:0,src.length)//注意越界问题

③数组扩容会影响程序的执行效率,因此尽可能预测数据量,创建一个接近数量的数组,减少扩容次数。

二维数组

①二维数组是一个特殊的一维数组,特殊在:这个一维数组中每个元素是一个一维数组(相当于存的还是一个地址,是一维数组的首位地址)。

②二维数组的静态初始化

1
2
3
int[][] arr = new int[][]{{},{},{}};

int[][] arr = {{},{},{}};

③二维数组的动态初始化(等长)

1
int[][] arr = new int[2][3];

④二维数组的动态初始化(不等长)

1
int[][] arr = new int[3][];

⑤二维数组中元素的访问

1
2
3
第一个元素:arr[0][0]

最后一个元素:arr[arr.length-1][arr[arr.length-1].length-1]

⑥二维数组中元素的遍历

IDEA的Debug

image-20240227122020670

image-20240227122031704

image-20240227122037930

image-20240227122045388

image-20240227122051856

image-20240227122056862

单元测试

image-20240227123624933

①什么是单元测试,为什么要进行单元测试?

1.一个项目是巨大的,只有保证你写的每一块都是正确的,最后整个项目才能正常运行。这里所谓的每一块就是一个单元。

②做单元测试需要引入JUnit框架,JUnit框架在JDK中没有,需要额外引入,也就是引入JUnit框架的class文件(jar包)

step1:文件目录下新建lib文件夹;

step2:将三个jar包复制进去,

step3:全选右键,Add as library

image-20240227123922794

③单元测试类(测试用例)怎么写?

1.单元测试类名:XxxTest

image-20240227133137828

image-20240227133302806

④单元测试方法怎么写?

1.单元测试方法需要使用@Test注解标注。

2.单元测试方法返回值类型必须是void

3.单元测试方法形参个数为0

4.建议单元测试方法名:testXxx

⑤什么是期望值,什么是实际值?

1.期望值就是在程序执行之前,你觉得正确的输出结果应该是多少

2.实际值就是程序在实际运行之后得到的结果

⑥常见注解:

1.@BeforeAll @AfterAll 主要用于在测试开始之前/之后执行必要的代码。被标注的方法需要是静态的。

2.@BeforeEach @AfterEach 主要用于在每个测试方法执行前/后执行必要的代码。

⑦单元测试中使用Scanner失效怎么办?

1.选中导航栏的“Help”,然后选中“Edit Custom VM Options…”,接着在“IDEA64.exe.vmoptions”文件中添加内容“-Deditable.java.test.console=true”,最后在重启IDEA即可解决

数据结构

跳过

Arrays工具类

java.util

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
Arrays.toString()方法:将数组转换成字符串

这个和Object.toString不同,
/*************************************************/
Object.toString()
Arrays.toString(att)//很显然,这个是一个静态数组
/*************************************************/

Arrays.deepToString()方法:可以将二维数组转换成字符串

Arrays.equals(int[] arr1, int[] arr2)方法:判断两个数组是否相等

Arrays.equals(Object[] arr1, Object[] arr2)方法

Arrays.deepEquals(Object[] arr1, Object[] arr2)方法:判断两个二维数组是否相等

Arrays.sort(int[] arr)方法:基于快速排序算法,适合小型数据量排序。

Arrays.sort(String[] arr)方法

Arrays.parallelSort(int[] arr)方法:基于分治的归并排序算法,支持多核CPU排序,适合大数据量排序。//多个线程

int binarySearch(int[] arr, int elt)方法:二分法查找

Arrays.fill(int[] arr, int data)方法:填充数组

Arrays.fill(int[] a, int fromIndex, int toIndex, int val)方法

int[] Arrays.copyOf(int[] original, int newLength)方法:数组拷贝
int[] arr = {1,2,3};
int[] arr2 = Arrays.copyOf(arr,newLength:3)

//拷贝一个范围内的数组,to不包含
int[] Arrays.copyOfRange(int[] original, int from, int to)

Arrays.asList(T... data)方法:将一组数据转换成List集合。

异常Exception

异常概述

①什么是异常?有什么用?

1.Java中的异常是指程序运行时出现了错误或异常情况,导致程序无法继续正常执行的现象。例如,数组下标越界、空指针异常、类型转换异常等都属于异常情况。

2.Java提供了异常处理机制,即在程序中对可能出现的异常情况进行捕捉和处理。异常机制可以帮助程序员更好地管理程序的错误和异常情况,避免程序崩溃或出现不可预测的行为。

3.没有异常机制的话,程序中就可能会出现一些难以调试和预测的异常行为,可能导致程序崩溃,甚至可能造成数据损失或损害用户利益。因此,异常机制是一项非常重要的功能,是编写可靠程序的基础。

②异常在Java中以类和对象的形式存在。

1.现实生活中也有异常,比如地震,火灾就是异常。也可以提取出类和对象,例如:

1.地震是类:512大地震、唐山大地震就是对象。

2.空指针异常是类:发生在第52行的空指针异常、发生在第100行的空指针异常就是对象。

2.也就是说:在第52行和第100行发生空指针异常的时候,底层一定分别new了一个NullPointerException对象。在程序中异常是如何发生的?

image-20240227140425131

image-20240227140431623

异常继承结构

①所有的异常和错误都是可抛出的。都继承了Throwable类。

②Error是无法处理的,出现后只有一个结果:JVM终止。

③Exception是可以处理的。

④Exception的分类:

1.所有的RuntimeException的子类:运行时异常/未检查异常(UncheckedException)/非受控异常

2.Exception的子类(除RuntimeException之外):编译时异常/检查异常(CheckedException)/受控异常

⑤编译时异常和运行时异常区别:

  • 1.编译时异常特点:在编译阶段必须提前处理,如果不处理编译器报错。
  • 2.运行时异常特点:在编译阶段可以选择处理,也可以不处理,没有硬性要求。
  • 3.编译时异常一般是由外部环境或外在条件引起的,如网络故障、磁盘空间不足、文件找不到等
  • 4.运行时异常一般是由程序员的错误引起的,并且不需要强制进行异常处理

注意:编译时异常并不是在编译阶段发生的异常,所有的异常发生都是在运行阶段的,因为每个异常发生都是会new异常对象的,new异常对象只能在运行阶段完成。那为什么叫做编译时异常呢?这是因为这种异常必须在编译阶段提前预处理,如果不处理编译器报错,因此而得名编译时异常。

image-20240227141452414

image-20240227141713987

1
2
3
NullPointerException e = new NullPointerException();
throw e;
throw new NullPointerException();

自定义异常

step1:继承Exception(编译时异常)/RuntimeException

step2: 提供一个无参数构造方法,再提供一个带String msg参数的构造方法,在构造方法中调用父类的构造方法。

image-20240227143030302

异常的处理包括两种方式:

1.声明异常:类似于推卸责任的处理方式

在方法定义时使用throws关键字声明异常,**==告知调用者,调用这个方法可能会出现异常==**。这种处理方式的态度是:如果出现了异常则会抛给调用者来处理。

如果一个异常发生后希望调用者来处理的,使用声明异常(俗话说:交给上级处理)

==注意这里是throws,然后在后面需要throw e的时候是没有s的==

1
public void m() throws AException, BException... {}

如果Exception和Exception都继承了Exception,那么也可以这样写:

1
public void m() throws XException{}

==调用者在调用m()方法时,编译器会检测到该方法上用throws声明了异常,表示可能会抛出异常,编译器会继续检测该异常是否为编译时异常,如果为编译时异常则必须在编译阶段进行处理,如果不处理编译器就会报错。==

如果所有位置都采用throws,包括main方法的处理态度也是throws,如果运行时出现了异常,最终异常是抛给了main方法的调用者(JVM),JVM则会终止程序的执行。==因此为了保证程序在出现异常后不被中断,至少main方法不要再使用throws进行声明了。==

发生异常后,在发生异常的位置上,往下的代码是不会执行的,除非进行了异常的捕捉。

2.捕捉异常:真正的处理捕捉异常(真正的处理异常 (try…catch…关键字))

在可能出现异常的代码上使用try..catch进行捕捉处理。这种处理方式的态度是:把异常抓住。其它方法如果调用这个方法,对于调用者来说是不知道这个异常发生的。因为这个异常被抓住并处理掉了。

如果一个异常发生后,不需要调用者知道,也不需要调用者来处理,选择使用捕捉方式处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
try{
// 尝试执行可能会出现异常的代码
// try块中的代码如果执行出现异常,出现异常的位置往下的代码是不会执行的,直接进入catch块执行
}catch(AException e){
// 如果捕捉到AException类型的异常,在这里处理
}catch(BException e){
// 如果捕捉到BException类型的异常,在这里处理
}catch(XException e){
// 如果捕捉到XException类型的异常,在这里处理
}
// 当try..catch..将所有发生的异常捕捉后,这里的代码是会继续往下执行的。




try{
// 尝试执行可能会出现异常的代码
// try块中的代码如果执行出现异常,出现异常的位置往下的代码是不会执行的,直接进入catch块执行
}catch(AException e|BException e2){
// 如果捕捉到AException类型的异常,在这里处理
}catch(XException e){
//

catch可以写多个。并且遵循自上而下,从小到大。

因为,如果最大的在上面,后面的小的异常都不会抛出了,就会导致异常抛出的不够精确

异常在处理的整个过程中应该是:声明和捕捉联合使用。

什么时候捕捉?什么时候声明?

如果**==异常发生后需要调用者来处理的,需要调用者知道的,则采用声明方式。==**否则采用捕捉。

异常的常用方法

获取异常的简单描述信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	exception.getMessage();

try{
// 尝试执行可能会出现异常的代码
// try块中的代码如果执行出现异常,出现异常的位置往下的代码是不会执行的,直接进入catch块执行
}catch(AException e){
// 如果捕捉到AException类型的异常,在这里处理
}catch(BException e){
// 如果捕捉到BException类型的异常,在这里处理
}catch(XException e){
String message = e.getMessage();
System.out.println(message);
// 如果捕捉到XException类型的异常,在这里处理
}
         获取的message是通过构造方法创建异常对象时传递过去的message。

打印异常堆栈信息:

1
exception.printStackTrace();

要会看异常的堆栈信息:

​ 异常信息的打印是符合栈数据结构的。

​ 看异常信息主要看最开始的描述信息。看栈顶信息。

image-20240227152502694

finally语句块

finally语句块中的代码是一定会执行的。

finally语句块不能单独使用,至少需要配合try语句块一起使用:

1
2
3
try...finally

try...catch...finally

通常在finally语句块中完成资源的释放

资源释放的工作比较重要,如果资源没有释放会一直占用内存。

为了保证资源的关闭,也就是说:不管程序是否出现异常,关闭资源的代码一定要保证执行。

因此在finally语句块中通常进行资源的释放。

final、finally、finalize分别是什么?

final是一个关键字,修饰的类无法继承,修饰的方法无法覆盖,修饰的变量不能修改。

finally是一个关键字,和try一起使用,finally语句块中的代码一定会执行。\

finalize是一个标识符,它是Object类中的一个方法名。

image-20240227152745145

innotnull

image-20240227153843258

面试经典题

1.image-20240227154130653

i为多少? 答:100

原因:

两个原则,(1)程序是自上而下执行的,(2)finally肯定是要执行的

所以,i返回的是100,但是会新建一个临时变量去执行i++

执行流程是先用一个临时变量做i++;然后做return,所以如果是return true;返回的就是true

2.image-20240227213044733

false:从上往下,finally一定会在最后执行

3.image-20240227213351296

执行下一个

原则:就近原则(从继承关系的角度上讲)

null引用类型,距离String更近,String是Object的子类

4.image-20240227213539958
5.image-20240227214002828

常理来说:a==10;

因为static块是在类加载的时候运行的,最先运行的,按照顺序运行的.

在创建static int a的时候,(method这个方法还没有运行到???运行到了吗),b还没有赋值,默认值0.所以a没有被这个方法成功赋值,此外,由于静态变量即使不赋值也会赋值默认值,所以赋值了0

6.image-20240227214340480

代码的执行顺序

我答:答不出来一点

答案:

image-20240227215936922

所以static不是一定要在这个B使用之前就跑了,就想B的静态代码就是最后跑的,但是此刻已经实现了new B()

方法覆盖与异常

方法重写之后,不能比父类方法抛出更多的异常,可以更少。

就比如父类抛的是一个RuntimeExceptipn ,子类不能抛Exception

image-20240227154550203

常用类

String类(注意:引用数据类型)

String初识

String类是无法继承的;

jdk9 之后引入了一种字符串压缩机制

Java中的String属于引用数据类型,代表字符串。
Java专门在==堆==中为字符串准备了一个==字符串常量池==。(JDK8)

因为字符串使用比较频繁,放在字符串常量池中省去了对象的创建过程,从而提高程序的执行效率。(常量池属于一种缓存技术,缓存技术是提高程序执行效率的重要手段。)

在编译阶段,就会把” “起来的字符串全部创建到字符串常量池里面(必须要有一份),然后用的时候直接拿

JVM在启动的时候,会进行一系列的初始化,其中就包括字符串常量池的初始化会在类加载的过程中就初始化出来了,程序在真正运行中,是不用创建字符串对象的// 一种缓存技术

1
2
3
4
5
String s1 = “hello”; 
String s2 = “hello”;
System.out.println(s1 == s2); // true 说明s1和s2指向了字符串常量池中的同一个字符串对象。
注意:字符串字面量在编译的时候就已经确定了将其放到字符串常量池中。JVM启动时会立即程序中带有双引号的字符串全部放入字符串常量池。

Java8之后字符串常量池在堆中。Java8之前字符串常量池在永久代。
字符串字面量一旦创建是不可变的。(底层String源码中有一个属性:private final byte[] value;)
1
String s = “hello”; 其中“hello”存储在字符串常量池中。

“hello”不可变。不能变成“hello123”。如果进行字符串拼接,必然创建新的字符串对象。
是 “hello”不可变,不是s不可变,s可以指向其它的字符串对象:s = “xyz”;

从底层源码看,底层String源码中有一个属性:private final byte[] value;底层是个byte[]数组,数组长度不可变,所以长度不可变,

此外,这个数组是用private final修饰的,所以这个变量不可以被访问和继承修改

所以String的字面量一旦创建不可变

StringBuilder是可变的字符串数组,它的底层是byte[] value,所以可以改变,可以创建一个更大的数组,然后这个value就可以指向新的数组

String的拼接

①动态拼接之后的新字符串不会自动放到字符串常量池中:
1
2
3
4
5
6
7
1.String s1 = “abc”;

2.String s2 = “def”;

3.String s3 = s1 + s2;

4.String s4 = “abcdef”;

==5.System.out.println(s3 == s4); // false 说明拼接后的字符串并没有放到字符串常量池==

==6.以上程序中字符串常量中有三个: “abc” “def” “abcdef”==

==7.以上程序中除了字符串常量池的字符串之外,在堆中还有一个字符串对象 “abcdef”==

②两个字符串字面量拼接会做编译阶段的优化,在编译阶段就会进行字符串的拼接。
1
1.String s1 = “aaa” + “bbb”;

以上程序会在编译阶段进行拼接,因此以上程序在字符串常量池中只有一个: “aaabbb”

常量池可以改变吗?

可以,语句:s3.intern()

但是我们不能去删除常量池中的内容,系统自己有一些调整的操作

此时,s4和s5都在字符串常量池中

image-20240227193208940

String类常用的构造方法有以下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String(char[] value)://根据字符数组创建一个新的字符串对象。
String(char[] value, int offset, int count)://根据字符数组的指定部分创建一个新的字符串对象。
String(byte[] bytes)://根据字节数组创建一个新的字符串对象,默认使用平台默认的字符集进行解码。
String(byte[] bytes, int offset, int length)://根据字节数组的指定部分创建一个新的字符串对象,默认使用平台默认的字符集进行解码。
String(byte[] bytes, Charset charset):
//根据字节数组和指定的字符集创建一个新的字符串对象。
new String(bytes, Charset.defaultCharset());
String(byte[] bytes, String charsetName):
//根据字节数组和指定的字符集名称创建一个新的字符串对象。
//这是一个解码的过程。你需要提前知道“byte[] bytes”是通过哪个编码方式进行编码得到的。
//如果通过GBK的方式进行编码得到的“byte[] bytes”,调用以上构造方法时采用UTF-8的方式进行解码。就会出现乱码。
String(String original):
//通过复制现有字符串创建一个新的字符串对象。
//这个方法被@IntrinsicCandidate标注,这个注解的作用是告诉编译器,该方法或构造函数是一个内在的候选方法,可以被优化和替换为更高效的代码。因此它是不建议使用的。
new String(“hello”); 这个代码会让常量池中有一个 “hello”,并且在堆中也有有一个String对象。
1
2
3
4
5
6
7
8
9
10
11
char[] chars = new char[]('a','b','c','d','e','f','g');
String s1 = new String(chars);
String s2 = new String(chars,offset=1,count=4);

byte[] bytes = {99,98,99,100}
String s2 = new String(bytes);//这是一个解码的过程,也是采用的平台默认的字符集.

byte[] bs2 = "sssssfda".getBytes(Charset.defaultCharset());

String s6 = new String(bs2,Charset.defaultCharset());
String s7 = new String(bs2,StanderCharsets.UTF_8);

image-20240227195325953

这样会乱码

String的常用方法

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
char charAt(int index); //返回索引处的char值
int length(); //获取字符串长度

boolean isEmpty(); //判断字符串是否为空字符串,如果length()是0就是空字符串。//

boolean equals(Object anObject); //判断两个字符串是否相等。

boolean equalsIgnoreCase(String anotherString); //判断两个字符串是否相等,忽略大小写。

boolean contains(CharSequence s); //判断当前字符串中是否包含某个子字符串

boolean startsWith(String prefix); //判断当前字符串是否以某个字符串开头

boolean endsWith(String suffix); //判断当前字符串是否以某个字符串结尾

int compareTo(String anotherString); //两个字符串按照字典顺序比较大小

int compareToIgnoreCase(String str); //两个字符串按照字典顺序比较大小,比较时忽略大小写

int indexOf(String str);// 获取当前字符串中str字符串的第一次出现处的下标。

int indexOf(String str, int fromIndex); //从当前字符串的fromIndex下标开始往右搜索,获取当前字符串中str字符串的第一次出现处的下标。

int lastIndexOf(String str); //获取当前字符串中str字符串的最后一次出现处的下标。

int lastIndexOf(String str, int fromIndex); //从当前字符串的fromIndex下标开始往左搜索,获取当前字符串中str字符串的最后一次出现处的下标。

String stripLeading(); 去除前空白
String stripTrailing(); 去除后空白
String toString();
String intern(); 获取字符串常量池中的字符串,如果常量池中没有,则将字符串加入常量池并返回。
byte[] bytes = {97,98,99,100}; String s = new String(bytes);
String s2 = s.intern(); // 将字符串 “abcd”放入字符串常量池并返回常量池中的字符串 “abcd”
static String join(CharSequence d, CharSequence... elements); //将多个字符串以某个分隔符连接(Java8新增)
static String join(CharSequence delimiter, Iterable<? extends CharSequence> elements);
static String valueOf(boolean b); //以下所有的静态方法valueOf作用是将非字符串类型的数据转换为字符串形式。
static String valueOf(char c);
static String valueOf(char[] data);
static String valueOf(char[] data, int offset, int count);
static String valueOf(double d);
static String valueOf(float f);
static String valueOf(int i);
static String valueOf(long l);
static String valueOf(Object obj);
byte[] getBytes(); //将字符串转换成字节数组。其实就是对字符串进行编码。默认按照系统默认字符集。
byte[] getBytes(String charsetName); //将字符串按照指定字符集的方式进行编码。
byte[] getBytes(Charset charset);
char[] toCharArray(); //将字符串转换字符数组。
String toLowerCase(); //转小写
String toUpperCase(); //转大写
String concat(String str); //进行字符串的拼接操作。和 + 的区别?
//+ 既可以进行求和,也可以进行字符串的拼接,底层拼接时会创建StringBuilder对象进行拼接。+ 拼接null时不会出现空指针异常。
//concat方法参数只能时字符串类型,拼接时不会创建StringBuilder对象,拼接完成后返回一个新的String对象。拼接null会出现空指针异常。
//+ 使用较多。如果进行大量字符串拼接,这两个都不合适。
String substring(int beginIndex); //从指定下标beginIndex开始截取子字符串
String substring(int beginIndex, int endIndex);
String trim(); //去除字符串前后空白(只能去除ASCII码中的空格和制表符)
String strip(); //去除字符串前后空白(支持所有的编码形式的空白,可以将全角空格去除,\u3000是全角空格,Java11新增)

image-20240227202458188

前>后;负

前<后;正

相等;0

右键 show Diagram

正则表达式

①正则表达式(regular expression),简称为regex或regexp,是一种用于描述特定模式的表达式。它可以匹配、查找、替换文本中与该模式匹配的内容,被广泛应用于各种文本处理和匹配相关的应用中。

②正则表达式的应用:

1.验证输入内容的格式是否正确。例如,邮箱,手机号,密码等

2.在文本编辑器中进行搜索和替换。例如,在代码编辑器中查找指定字符串或替换错误的代码成为正确的代码块

3.数据挖掘和信息提取。正则表达式可以从HTML、XML、JSON等格式的数据中提取所需的信息

4.用于编写脚本语言,如awk,grep和sed

5.服务器端编程。正则表达式在处理数据和字符串时具有高效的性能,可以在开发Web应用程序时被广泛应用

③正则表达式和Java语言的关系?

Java语言中可以使用正则表达式。C语言以及其它大部分编程语言都是支持正则表达式的。

==String的正则表达式相关的方法:==

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String replace(CharSequence target, CharSequence replacement);

//将当前字符串中所有的target替换成replacement,返回一个新的字符串。

String replaceAll(String regex, String replacement);

//将当前字符串中所有符合正则表达式的regex替换成replacement。

String[] split(String regex);

//将当前字符串以某个正则表达式表示的子字符串进行分割,返回一个字符串数组。

boolean matches(String regex);

//判断当前字符串是否符合正则表达式regex。

String的面试题

1.
image-20240227204034015

false;

true

2.image-20240227204118497

true;因为这种拼接会在编译阶段完成,编译器优化策略

true

3.image-20240227204222677

false:yin_yang:这个和上面那个不一样,上面那个是字面量相加,是可以在编译阶段实现,这个是变量,不在编译阶段实现,是存放在堆里的字符串变量;

如果s3.intern(),那就是一样了

true

4.image-20240227204622761

3个对象,a一个b一个,s一个,放在字符串常量区里

5.image-20240227204749867
1
2
3
4
5
6
7
8
9
//字符串常量池中一个"a"
String s1 = "a";

//字符串常量池中一个"b",堆中一个s2

String s2 = new String("b");

//s3-->堆中两个,一个StringBuilder(在拼接的时候自动创建的),一个string(StringBuilder调用toString转换的)
String s3 = s1+s2
6.image-20240227210701818

字符串常量区:“a”,”b”

堆:new的a一个,new的b一个,拼接生成的StringBuilder一个,调用toString生成的一个String一个

一共6个对象

7.image-20240227210824500

答案:不会出现异常,底层会默认调用valueOf,将非字符串类型的数据转换为字符串形式。结果null

8.image-20240227211247488

没有final的话,false,s3不在字符串常量池里

///*********************

true

final修饰后,s2不可变,就是个常量,所以这个拼接是在编译时候完成的,没有创建StringBuilder对象

所以是true

9.image-20240227211841286

false;

因为getB()是一个方法,只能在运行阶段,不能在编译阶段,所以虽然s2是个常量,但是他不在字符串常量区,在堆里面。

所以s3这个拼接操作也是在运行时候执行的,地址不一样,所以是false

10.image-20240227212157813

false

equals 的前提,两种类型要一样,这俩类型不一样

StringBuffer与StringBuilder

StringBuffer和StringBuilder:可变长度字符串

image-20240227220453859
  • 使用情况:这两个类是专门为频繁进行字符串拼接而准备

  • 二者差别:StringBuffer先出现的,Java5的时候新增了StringBuilder。StringBuffer是线程安全的。在不需要考虑线程安全问题的情况下优先选择StringBuilder,效率较高一些。

  • 底层: 是 byte[] 数组,并且这个 byte[] 数组没有被final修饰,这说明如果byte[]数组满了,可以创建一个更大的新数组来达到扩容,然后它可以重新指向这个新的数组对象。

  • 优化策略:==创建StringBuilder对象时,预估计字符串的长度,给定一个合适的初始化容量,减少底层数组的扩容。==

  • StringBuilder默认初始化容量:16

  • StringBuilder一次扩容多少?可以通过Debug跟踪一下append方法。扩容策略是:从当前容量开始,**==每次扩容为原来的2倍再加上2==**

原来是i,扩容后就是2i+2,如果是默认的,就开始2*16+2 ,然后2*(2+16)+2

如果拼接的字符串很大,大于了预期增长值,就会增大到拼接的字符串长度

假如是初始化len:50的字符串:

l16+minCapacity(34)==50

34>16+2,所以扩容大小为34

count 目前真实的存储数量

StringBuffer和StringBuilder构造方法

1
2
3
4
5
StringBuilder() //构造一个字符串生成器,其中不包含任何字符,初始容量为16个字符。

StringBuilder(int capacity) //构造一个字符串生成器,其中不包含任何字符,并且具有由容量参数指定的初始容量。

StringBuilder(String str)// 构造初始化为指定字符串内容的字符串生成器

image-20240227222910252

StringBuffer和StringBuilder常用方法

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
StringBuilder append(Type data);

StringBuilder delete(int start, int end);

StringBuilder deleteCharAt(int index);

StringBuilder insert(int offset, String str);

StringBuilder replace(int start, int end, String str)

StringBuilder reverse();

void setCharAt(int index, char ch);

void setLength(int newLength);//会把已有的数据抹掉


char charAt(int index);

int indexOf(String str);

int indexOf(String str, int fromIndex);

int lastIndexOf(String str);

int lastIndexOf(String str, int fromIndex);

int length();

String substring(int start);

String substring(int start, int end);

String toString();

long begin = System.current.TimeMills();

从1970年1月1日 0时0分0秒开始 的总毫秒数

1
2
3
4
5
6
7
8
9
10
11
12
//以下这种写法尽量避免,效率太低:
String s = “”;
for(int i = 0; i < 100000; i++){
// 优化策略:底层会新建一个StringBuilder对象
// 然后调用StringBuilder的append(i)方法进行追加
// 然后再调用StringBuilder toString()方法转成String类型
// 也就是说:这里会频繁的创建String对象,导致效率很低
// 同时给GC带来巨大压力。
s += i;
}
因此建议使用下面的方式,只创建一个StringBuilder对象

image-20240228101346290

包装类

什么是包装类?有什么用?

我的理解,将基础类型进行包装,形成一个引用类型,方便编程

image-20240228101801236

比如在一个函数中,其接受的参数是引用类型(Object),实际应用起来是对于数字操作的,直接传进去数字是不行的,是会报错的,所以我们把这个int的10,包装成引用类型的10.

image-20240228102517804

包装类中的6个数字类型都继承了Number类

boolean 和character没有继承

①Byte、Short、Integer、Long、Float、Double都继承了Number类,因此这些类中都有以下这些方法:

1
2
3
4
5
6
byteValue()
shortValue()
intValue()
longValue()
floatValue()
doubleValue()

==这些方法的作用就是将包装类型的数据转换为基本数据类型。==

包装类转换成基本数据类型的过程我们称为:==拆箱 unboxing==

Boolean的拆箱方法:booleanValue();

Character的拆箱方法:charValue();

image-20240228102834512

Integer的常量(为例子)

①通过Integer提供的常量可以获取int的最大值和最小值:

1
2
3
①最大值:Integer.MAX_VALUE

②最小值:Integer.MIN_VALUE

②当然,其它5个数字包装类也有对应的常量:

1
2
3
4
5
6
7
8
9
①byte最大值:Byte.MAX_VALUE

②byte最小值:Byte.MIN_VALUE

③short最大值:Short.MAX_VALUE

④short最小值:Short.MIN_VALUE

⑤......

Integer的构造方法

①Integer(int value)

1.Java9之后标记已过时,不建议使用。

2.该构造方法可以将基本数据类型转换成包装类。这个过程我们称为装箱boxing

1
2
3
4
 int i = 100;
Integer i1 = new Integer(i);
String s = "100";
Integer i2 = new Integer(s);
②Integer(String s)

1.Java9之后标记已过时,不建议使用。

2.该构造方法可以将字符串数字转换成包装类。但字符串必须是整数数字,如果不是会出现异常:NumberFormatException

Integer的常用方法

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
static int compare(int x, int y); //比较大小

static int max(int a, int b); //最大值

static int min(int a, int b); //最小值

static int parseInt(String s); //将字符串数字转换成数字类型。其它包装类也有这个方法:
int num1 = Integer.parseInt("123");
Double.parseDouble(String s)**

static String toBinaryString(int i); //获取数字二进制的字符串表示形式

static String toHexString(int i);// 获取数字十六进制的字符串表示形式

static String toOctalString(int i); //获取数字八进制的字符串表示形式

int compareTo(Integer anotherInteger); //比较大小,可见实现了Comparable接口

boolean equals(Object obj); //包装类已经重写了equals()方法。

String toString(); //包装类已经重写了toString()方法。
int intValue(); //将包装类拆箱为基本数据类型

//装箱
Integer i = new Integer(100);
//拆箱
int num = i.intValue();

static String toString(int i); //将基本数据类型转换成字符串

static Integer valueOf(int i); //将基本数据类型转换成Integer

static Integer valueOf(String s) /将字符串转换成Integer(这个字符串必须是数字字符串才行,不然出现NumberFormatException)

image-20240228111527964

Java5新特性:自动装箱和自动拆箱

编译阶段的功能

①Java5之后为了开发方便,引入了新特性:自动拆箱和自动装箱。

②自动装箱:auto boxing

1
Integer a = 10000;

③自动拆箱:auto unboxing

1
2
3
int b = a;

System.out.println(a + 1); 这里的a也会做自动拆箱。

⑤注意空指针异常:

1
2
3
Integer a = null;

System.out.println(a + 1);

以上代码出现空指针异常的原因是a在进行自动拆箱时,会调用 a.intValue()方法。

因为a是null,访问实例方法会出现空指针异常,因此使用时应注意。
image-20240228111945428

image-20240228112019609

整数型常量池

image-20240228112206054

①**==[-128 ~ 127] Java为这个区间的Integer对象创建了整数型常量池。==**

②也就是说如果整数没有超出范围的话,直接从整数型常量池获取Integer对象。

③以下是一个面试题:请说出它的输出结果:

1
2
3
4
5
6
7
8
9
10
11
Integer x = 128;

Integer y = 128;

System.out.println(x == y); // false

Integer a = 127;

Integer b = 127;

System.out.println(a == b); // true

大数字

如果整数超过long的最大值怎么办?

这个是引用数据类型

①java中提供了一种引用数据类型来解决这个问题:**==java.math.BigInteger==。它的父类是==Number。==**

②常用构造方法:BigInteger(String val)

③常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
BigInteger add(BigInteger val); 求和

BigInteger subtract(BigInteger val); 相减

BigInteger multiply(BigInteger val); 乘积

BigInteger divide(BigInteger val); 商

int compareTo(BigInteger val); 比较

BigInteger abs(); 绝对值

BigInteger max(BigInteger val); 最大值

BigInteger min(BigInteger val); 最小值

BigInteger pow(int exponent); 次幂

BigInteger sqrt(); 平方根

如果浮点型数据超过double的最大值怎么办?

①java中提供了一种引用数据类型来解决这个问题**==:java.math.BigDecimal==经常用在财务软件中)。它的父类是==Number。==**

②构造方法:**==BigDecimal(String val)==**

③常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
BigDecimal add(BigDecimal augend); 求和

BigDecimal subtract(BigDecimal subtrahend); 相减

BigDecimal multiply(BigDecimal multiplicand); 乘积

BigDecimal divide(BigDecimal divisor); 商

BigDecimal max(BigDecimal val); 最大值

BigDecimal min(BigDecimal val); 最小值

BigDecimal movePointLeft(int n); 向左移动小数点

BigDecimal movePointRight(int n); 向右移动小数点

数字格式化

image-20240228113359145

日期处理

日期相关API

①**==long l = System.currentTimeMillis();==** // 获取自1970年1月1日0时0分0秒到系统当前时间的总毫秒数。

②java.util.Date 日期类

​ ①构造方法:Date()

​ ②构造方法:Date(long 毫秒)

③java.util.SimpleDateFormat 日期格式化类

​ ①日期转换成字符串(java.util.Date -> java.lang.String)

​ ②字符串转换成日期(java.lang.String -> java.util.Date)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//Date ---》string  format
Date now = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String str = sdf.format(now);

//string--》Date parse
String strDate = "2008-08-08 08:08:08 888";//字符串格式和下面这个format格式要对上,不然就会报解析异常
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date date = sdf2.parse(strDate)

//获取指定的时间,单位为毫秒级别
Date date1 = new Date(1000);
System.out.println(date1);

//获取当前系统的前十分钟
Date date2 = new Date(System.currentTimeMillis() - 1000 * 60 * 10);
System.out.println(date2);

④java.util.Calendar 日历类

​ ①获取当前时间的日历对象:Calendar c = Calendar.getInstance();

​ ②获取日历中的某部分:int year = c.get(Calendar.YEAR);

1
2
3
4
5
6
7
8
9
Calendar.YEAR //获取年份 Calendar.MONTH 获取月份,0表示1月,1表示2月,...,11表示12月

Calendar.DAY_OF_MONTH //获取本月的第几天 Calendar.DAY_OF_YEAR 获取本年的第几天

Calendar.HOUR_OF_DAY //小时,24小时制 Calendar.HOUR 小时,12小时制

Calendar.MINUTE //获取分钟 Calendar.SECOND 获取秒

Calendar.MILLISECOND //获取毫秒 Calendar.DAY_OF_WEEK 获取星期几,1表示星期日,...,7表示星期六

image-20240228130311011

①java.util.Calendar 日历类

​ 1.日历的set方法:设置日历

静态变量

​ 1.calendar.set(Calendar.YEAR, 2023);

​ 2.calendar.set(2008, Calendar.SEPTEMBER,8);

​ 2.日历的add方法(日历中各个部分的加减):

​ 1.calendar.add(Calendar.YEAR, 1);

​ 3.日历对象的setTime()让日历关联具体的时间

​ 1.calendar.setTime(new Date());

​ 4.日历对象的getTime()方法获取日历的具体时间:

​ 1.Date time = calendar.getTime();

image-20240228131052018

1
2
3
4
5
6
7
Calendar cal = new Calendar();
String strDate = "2008-05-12 15:30:30";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = sdf.parse(strDate);
cal.setTime(date);
System.out.println(cal.get(Calendar.HOUR_OF_DAY));
System.out.println(cal.get(Calendar.MINUTE));

Java8的新日期API

传统的日期API存在线程问题

新的API在java.time包

传统的日期API存在线程安全问题,Java8又提供了一套全新的日期API

  • ①java.time.LocalDate、java.time.LocalTime、java.time.LocalDateTime 日期、时间、日期时间
  • ②java.time.Instant 时间戳信息197001010000到现在的时间毫秒
  • ③java.time.Duration 计算两个时间对象之间的时间间隔,精度为纳秒
  • ④java.time.Period 计算两个日期之间的时间间隔,以年、月、日为单位。
  • ⑤java.time.temporal.TemporalAdjusters 提供了一些方法用于方便的进行日期时间调整
  • ⑥java.time.format.DateTimeFormatter 用于进行日期时间格式化和解析

LocalDate日期、LocalTime时间、LocalDateTime日期时间

①获取当前时间(精确到纳秒,1秒=1000毫秒,1毫秒=1000微秒,1微秒=1000纳秒)
1
LocalDateTime now = LocalDateTime.now(); 
②获取指定日期时间
1
LocalDateTime ldt = LocalDateTime.of(2008,8,8,8,8,8,8); // 获取指定的日期时间
③加日期和加时间
1
LocalDateTime localDateTime = ldt.plusYears(1).plusMonths(1).plusDays(1).plusHours(1).plusMinutes(1).plusSeconds(1).plusNanos(1);
④减日期和减时间
1
LocalDateTime localDateTime = ldt.minusYears(1).minusMonths(1).minusDays(1).minusHours(1).minusMinutes(1).minusSeconds(1).minusNanos(1);
⑤获取年月日时分秒
1
2
3
4
5
6
7
8
9
int year = now.getYear(); // 年 int month = now.getMonth().getValue(); // 月

int dayOfMonth = now.getDayOfMonth(); // 一个月的第几天 int dayOfWeek = now.getDayOfWeek().getValue(); // 一个周第几天

int dayOfYear = now.getDayOfYear(); // 一年的第几天 int hour = now.getHour(); // 时

int minute = now.getMinute(); // 分 int second = now.getSecond(); // 秒

int nano = now.getNano(); // 纳秒

==Instant 时间戳(获取1970年1月1日 0时0分0秒到某个时间的时间戳)==

①获取系统当前时间(UTC:全球标准时间)
1
Instant instant = Instant.now(); 
②获取时间戳
1
long epochMilli = instant.toEpochMilli(); 

Duration 计算时间间隔

①计算两个时间相差时间间隔Duration.between(now1, now2);
1
2
3
4
5
6
7
8
9
10
11
12
13
LocalDateTime now1 = LocalDateTime.of(2008,8,8,8,8,8);

LocalDateTime now2 = LocalDateTime.of(2009,9,9,9,9,9);

Duration between = Duration.between(now1, now2);

// 两个时间差多少个小时

System.out.println(between.toHours());

// 两个时间差多少天

System.out.println(between.toDays());

Period 计算日期间隔

①计算两个日期间隔Period.between(now1, now2);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    LocalDate now1 = LocalDate.of(2007,7,7);

LocalDate now2 = LocalDate.of(2008,8,8);

Period between = Period.between(now1, now2);

// 相差年数

​ System.out.println(between.getYears());

// 相差月数

​ System.out.println(between.getMonths());

// 相差天数

​ System.out.println(between.getDays());

TemporalAdjusters 时间矫正器

1
2
3
4
5
6
7
8
9
10
11
12
13
LocalDateTime now = LocalDateTime.now(); // 获取系统当前时间

②now.with(TemporalAdjusters.firstDayOfMonth()); // 当前月的第一天

③now.with(TemporalAdjusters.firstDayOfNextYear()); // 下一年的第一天

④now.with(TemporalAdjusters.lastDayOfYear()); // 本年最后一天

⑤now.with(TemporalAdjusters.lastDayOfMonth()); // 本月最后一天

⑥now.with(TemporalAdjusters.next(DayOfWeek.MONDAY)); // 下周一

⑦......

DateTimeFormatter 日期格式化

①日期格式化 (LocalDateTime –> String)
1
2
3
4
5
LocalDateTime now = LocalDateTime.now();

DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

String s = dtf.format(now);
②将字符串转换成日期(String –> LocalDateTime)
1
2
3
4
5
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

LocalDateTime localDateTime = LocalDateTime.parse("2008-08-08 08:08:08", dtf);

System.out.println(localDateTime);

Math

java.lang.Math 数学工具类,都是**==静态方法==**

①常用属性:static final double PI(圆周率)
②常用方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
static int abs(int a); //绝对值

static double ceil(double a); //向上取整

static double floor(double a); //向下取整

static int max(int a, int b); //最大值

static int min(int a, int b);// 最小值

static double random(); //随机数[0.0, 1.0) int num = (int)(Math.random() * 100);可以获取[0-100)的随机数

static long round(double a); //四舍五入

static double sqrt(double a); //平方根

static double pow(double a, double b); //a的b次幂

枚举

枚举(Java5新特性)

①枚举类型在Java中是一种==引用数据类型。==

②合理使用枚举类型可以让代码更加清晰、可读性更高,可以有效地避免一些常见的错误。

③什么情况下考虑使用枚举类型?

​ 1.这个数据是有限的,并且可以一枚一枚列举出来的。

​ 2.枚举类型是类型安全的,它可以有效地防止使用错误的类型进行赋值。

④枚举如何定义?以下是最基本的格式:

1
2
3
4
5
enum 枚举类型名 {

枚举值1, 枚举值2, 枚举值3, 枚举值4

}

⑤通过反编译(javap)可以看到:

1.所有枚举类型默认继承java.lang.Enum,因此枚举类型无法继承其他类。

2.所有的枚举类型都被final修饰,所以枚举类型是无法继承的

3.所有的枚举值都是常量

4.所有的枚举类型中都有一个values数组(可以通过values()获取所有枚举值并遍历)

image-20240228134745351

image-20240228134809247

枚举的高级用法

①普通类中可以编写的元素,枚举类型中也可以编写。

静态代码块,构造代码块

实例方法,静态方法

实例变量,静态变量

②枚举类中的==构造方法是私有化==的(默认就是私有化的,只能在本类中调用)

构造方法调用时不能用new。直接使用“枚举值(实参);”调用。

每一个枚举值相当于枚举类型的实例。

枚举类最开始的时候 定义的枚举值就是枚举类对象,会调用构造函数,默认会调用无参构造函数

如果定义了有参构造函数,没有修改在上面定义的枚举值的话,就会报错

所以需要对于枚举值修改一下构造函数

image-20240228144551282

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 enum Season {
SPRING("春天") ,SUMMER("夏天"),AUTUMN("秋天"),WINTER("冬天");
public static int i = 10;
private String name = "春天";
Season(String name){
this.name = name;
}
}

public class SeasonTest {
public static void main(String[] args) {
Season season = get();
switch (season){
case SPRING -> System.out.println(Season.SPRING.name());
case SUMMER -> System.out.println(Season.SUMMER.name());
case AUTUMN -> System.out.println(Season.AUTUMN.name());
case WINTER -> System.out.println(Season.WINTER.name());
}
}
public static Season get(){
return Season.SPRING;
}
}

③枚举类型中如果编写了其他代码,==必须要有枚举值,枚举值的定义要放到最上面,==

==最后一个枚举值的分号不能省略。==

④枚举类因为默认继承了==java.lang.Enum==,因此不能再继承其他类,但可以实现接口。

第一种实现方式:在枚举类中实现。

第二种实现方式:让每一个枚举值实现接口。

image-20240228134927319
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
public enum Season implements Eatable{
SPRING("春天"){
@Override
public void eat() {
System.out.println("apple");
}
},SUMMER("夏天"){
@Override
public void eat() {
System.out.println("lalala");
}
},AUTUMN("秋天"){
@Override
public void eat() {
System.out.println("111");
}
},WINTER("冬天"){
@Override
public void eat() {
System.out.println("111");
}
};

或者
public enum Season implements Eatable{
SPRING("春天"),SUMMER("夏天"),AUTUMN("秋天"),WINTER("冬天");
public static int i = 10;
private String name = "春天";
Season(String name){
this.name = name;
}


@Override
public void eat() {

}
}

Random

java.util.Random 随机数生成器(生成随机数的工具类)

①常用构造方法:
1
Random()
②常用方法:
1
2
3
4
5
int nextInt(); 获取一个int类型取值范围内的随机int

int nextInt(int bound); 获取[0,bound)区间的随机数

double nextDouble(); 获取[0.0, 1.0)的随机数。

xxxxxxxxxx package com.powernode.javase.oop45;​/** * 匿名内部类:没有名字的类。只能使用一次。 */public class Test {    public static void main(String[] args) {        // 创建电脑对象        Computer computer = new Computer();        //computer.conn(new Printer());​        // 以下conn方法参数上的代码做了两件事:        // 第一:完成了匿名内部类的定义。        // 第二:同时实例化了一个匿名内部类的对象。        computer.conn(new Usb(){            // 接口的实现            @Override            public void read() {                System.out.println(“read…..”);           }​            @Override            public void write() {                System.out.println(“write…..”);           }       });   }}​class Computer {    public void conn(Usb usb){        usb.read();        usb.write();   }}​interface Usb {    void read();    void write();}​// 编写一个接口的实现类/*class Printer implements Usb {​    @Override    public void read() {        System.out.println(“打印机开始读取数据”);    }​    @Override    public void write() {        System.out.println(“打印机开始打印”);    }} */java

java.lang.System类的常用方法:

①常用属性:
1
2
3
4
5
static final PrintStream err 标准错误输出流(System.err.println(“错误信息”);输出红色字体)

static final InputStream in 标准输入流

static final PrintStream out 标准输出流
②常用方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void arraycopy(Object src, int srcPos, Object dest, int destPos, int length); 数组拷贝

static void exit(int status); 退出虚拟机

static void gc(); 建议启动垃圾回收器

static long currentTimeMillis(); 获取自1970-01-01 00:00:00 000到系统当前时间的总毫秒数

static long nanoTime(); 获取自197011000秒以来,当前时间的纳秒数

static Map<String,String> getenv(); 获取当前系统的环境变量,例如Path,JAVA_HOME,CLASSPATH等。

static Properties getProperties(); 获取当前系统的属性。

static String getProperty(String key); 通过key获取指定的系统属性。

UUID

具有全球唯一性的标识

UUID(通用唯一标识符)是一种软件构建的标准,用来生成具有唯一性的ID。

UUID具有以下特点:

​ ①UUID可以在分布式系统中生成唯一的标识符,避免因为主键冲突等问题带来的麻烦。

​ ==②UUID具有足够的唯一性,重复的概率相当低。UUID使用的是128位数字,除了传统的16进制表示之外(32位的16进制表示),还有基于62进制的表示,可以更加简洁紧凑。==

③UUID生成时不需要依赖任何中央控制器或数据库服务器,可以在本地方便、快速地生成唯一标识符。

④UUID生成后可以被许多编程语言支持并方便地转化为字符串表示形式,适用于多种应用场景。

在Java开发中,UUID的使用是非常普遍的。它可以用于生成数据表主键、场景标识、链路追踪、缓存Key等。使用UUID可以方便地避免主键、缓存Key等因冲突而产生的问题,同时能够实现多种功能,例如追踪、缓存、日志记录等。

Java中的java.util.UUID类提供对UUID的支持

①生成UUID:static UUID randomUUID();

②将UUID转换为字符串:String toString();

Java 基础知识

一、 package 和 import

package

其实包就是把很多很多的代码进行整理,便于代码的统一管理。

①怎么定义包:在java源码第一行编写 package 语句。注意:package语句只能出现在java代码第一行。

②包名命名规范中要求是全部小写。

③包名命名规范:公司域名倒序 + 项目名 + 模块名 + 功能名。例如:

①com.powernode.oa.empgt.service

④如果带包编译:

①javac -d 编译后的存放目录 java源文件路径

⑥有了包机制后,完整类名是包含包名的,例如类名是:com.powernode.javase.chapter02.PackageTest

import

①import语句用来引入其他类。

②A类中使用B类,A类和B类不在同一个包下时,就需要在A类中使用import引入B类。

③java.lang包下的不需要手动引入。

④import语句只能出现在package语句之下,class定义之前。

⑤import语句可以编写多个。

⑥import语句可以模糊导入:java.util.*;

⑦import静态导入:import static java.lang.System.*;

image-20240223194626584image-20240223194657595

二、面向对象概述

1. 面向对象三大特征

面向对象三大特征

①封装(Encapsulation)

②继承(Inheritance)

③多态(Polymorphism)

2.对象的创建与使用

1
2
3
4
5
6
7
8
9
10
11
public class Student {
// 姓名
String name; // 实例变量
// 年龄
int age;
// 性别
boolean gender;
// 学习
public void study(){ System.out.println(“正在学习”); } // 实例方法
}

①对象的创建

1
Student s = new Student();

在Java中,使用class定义的类,属于引用数据类型。所以Student属于引用数据类型。类型名为:Student。

Student s; 表示定义一个变量。数据类型是Student。变量名是s。

②对象的使用

读取属性值:s.name

修改属性值:s.name = “jackson”;

③通过一个类可以实例化多个对象

Student s1 = new Student();

Student s2 = new Student();

④ public static void functionA(),直接用类名.functionA()来访问

这个叫做静态方法

如果描述对象的动作,就不加static ,这种方法叫做实例方法

3.JVM内存结构图

image-20240223195906110

  1. 元空间(mataspace)存储的是类的元信息,字节码等。元空间是在java8后面引入的。

JVM java虚拟机中定义的规范叫做方法区。

image-20240223202022775

==一旦引用为NULL,表示引用不再指向对象了==

image-20240223202252613

4.封装

什么是封装

封装是一种将数据和方法加以包装,使之成为一个独立的实体,并且把它与外部对象隔离开来的机制。具体来说,封装是将一个对象的所有“状态(属性)”以及“行为(方法)”统一封装到一个类中,从而隐藏了对象内部的具体实现细节,向外界提供了有限的访问接口,以实现对对象的保护和隔离。

封装的好处

封装通过限制外部对对象内部的直接访问和修改,保证了数据的安全性,并提高了代码的可维护性和可复用性。

在代码上如何实现封装

1. 属性私有化,对外提供getter和setter方法。

属性私有化:使用private来进行修饰。

属性私有化的作用是禁止外部程序随意访问。

2. 为了保证外部程序以然可以访问age属性,还是需要提供一个公开的外部访问的入口。

访问一般分为两个方法,一个负责读,一个负责修改。

读取方法的格式:

1
public int getAge(){}

改方法的格式:

1
public void setAge(int age){}

实例方法调用实例方法

image-20240225145941617

还是一个this.pay()

5. 构造方法(构造器)constructor

其实就是C里面的构造函数

构造方法的作用

  1. 实现对象的出啊关键,通过调用的构造方法啊可以完成对于对象的创建
  2. 实现对于对象的各种属性的赋值。

构造方法怎么定义?

image-20240225150328362

构造方法最后执行结束之后,会返回这个new出来的实例的内存地址,但是构造方法中不需要提供return语句

构造方法名一定要和类名保持一致!

构造方法不需要返回值,如果有返回值就变成了普通方法

缺省构造器

如果一个类中没有显示的去定义构造方法,就会提供一个默认的方法,叫做缺省构造器。

当在调用 new Classname的时候,其实就是在调用构造函数

image-20240225151158129

完成构建,属性赋默认值。

image-20240225154836218

如果一个类里面提供了构造函数,那么就不会再创建缺省的构造方法,所以最好还是写一个无参数的构造方法。

构造方法Constructor(构造器)

①构造方法有什么作用?

1.构造方法的执行分为两个阶段:对象的创建和对象的初始化。这两个阶段不能颠倒,也不可分割。

2.在Java中,当我们使用关键字new时,就会在内存中创建一个新的对象,虽然对象已经被创建出来了,但还没有被初始化。而初始化则是在执行构造方法体时进行的。

②构造方法如何定义?

[修饰符列表] 构造方法名(形参){}

③构造方法如何调用?new 构造方法名(实参);

④关于无参数构造方法:如果一个类没有显示的定义任何构造方法,系统会默认提供一个无参数构造方法,也被称为缺省构造器。一旦显示的定义了构造方法,则缺省构造器将不存在。为了方便对象的创建,建议将缺省构造器显示的定义出来。

⑤构造方法支持重载机制。

⑥关于构造代码块。对象的创建和初始化过程梳理:

①new的时候在堆内存中开辟空间,给所有属性赋默认值

②执行构造代码块进行初始化

③执行构造方法体进行初始化

④构造方法执行结束,对象初始化完毕。

注意,set是修改,构造方法是初始化

6. this关键字

this关键字

①this是一个关键字。

②this出现在实例方法中,代表当前对象。语法是:this.

③this本质上是一个==引用==,该引用保存当前对象的内存地址。

④通过“this.”可以访问实例变量,可以调用实例方法。

⑤this存储在:栈帧的局部变量表的第0个槽位上。

⑥this. 大部分情况下可以省略,用于区分局部变量和实例变量时不能省略。

⑦this不能出现在静态方法中。

public static vois functionstatic(){}

这里面不可以用this,因为static方法中没有当前对象,所以不可以用this。

但是在静态方法中可以调用另外一个静态方法。

⑧“this(实参)”语法:

①只能出现在构造方法的第一行。

②通过当前构造方法去调用本类中其他的构造方法。

③作用是:代码复用。

实例方法只可以引用.

7.static关键字

final变量不能修改,static变量可以在这个类里头修改

①static是一个关键字,翻译为:静态的。

②static修饰的变量叫做静态变量。当所有对象的某个属性的值是相同的,建议将该属性定义为静态变量,来节省内存的开销。

③静态变量在类加载时初始化,存储在堆中。

④static修饰的方法叫做静态方法。

所有static级别的,都是类级别的,直接用类名来访问

⑤所有静态变量和静态方法,统一使用“类名.”调用。==虽然可以使用“引用.”来调用,但实际运行时和对象无关,所以不建议这样写,因为这样写会给其他人造成疑惑。==

⑥使用“引用.”访问静态相关的,即使引用为null,也不会出现空指针异常。

⑦静态方法中不能使用this关键字。因此无法直接访问实例变量和调用实例方法。

⑧静态代码块在类加载时执行,一个类中可以编写多个静态代码块,遵循自上而下的顺序依次执行。

⑨静态代码块代表了类加载时刻,如果你有代码需要在此时刻执行,可以将该代码放到静态代码块中。

静态变量内存图

image-20240225194625857

image-20240225194714666

空指针异常

image-20240225195106209

静态方法中无法直接访问实例相关的数据。

静态代码块

image-20240225195527691

语法结构
  1. 语法格式:

    1
    2
    3
    static{
    这个括号叫做静态上下文,
    }
  2. 静态代码块在类加载的时候执行,并且只执行了一次

  3. 静态代码块可以编写多个,并且遵循自上而下的顺序执行。

image-20240225212935175

按照以上的这个代码,main方法也是最后执行的

此外

image-20240225213020734

按照这个,会报错。连main方法都没有执行,name也没有执行,

:在静态上下文中无法直接访问实例相关的数据,但是可以访问在这个静态代码块前面定义的静态数据。

在这个静态代码块之后定义的就不能访问了。因为静态的数据是按照顺序进行的。

image-20240225213131315

8.JVMjava虚拟机的体系结构

==JVM对应了一套规范(Java虚拟机规范),它可以有不同的实现==

①JVM规范是一种抽象的概念,它可以有多种不同的实现。例如:

1.HotSpot:HotSpot 由 Oracle 公司开发,是目前最常用的虚拟机实现,也是默认的 Java 虚拟机,默认包含在 Oracle JDK 和 OpenJDK 中

2.JRockit:JRockit 也是由 Oracle 公司开发。它是一款针对生产环境优化的 JVM 实现,能够提供高性能和可伸缩性

3.IBM JDK:IBM JDK 是 IBM 公司开发的 Java 环境,采用了与 HotSpot 不同的 J9 VM,能够提供更小的内存占用和更迅速的启动时间

4.Azul Zing:Azul Zing 是针对生产环境优化的虚拟机实现,能够提供高性能和实时处理能力,适合于高负载的企业应用和实时分析等场景

5.OpenJ9:OpenJ9 是由 IBM 开发的优化的 Java 虚拟机实现,支持高度轻量级、低时延的 GC、优化的 JIT 编译器和用于健康度测试的可观察性仪表板

②右图是从oracle官网上截取的Java虚拟机规范中的一部分。(大家也可以找一下oracle官方文档)

③我们主要研究运行时数据区。运行时数据区包括6部分:

  1. The pc Register(程序计数器):下一个你要执行的字节码指令

  2. Java Virtual Machine Stacks(Java虚拟机栈)

  3. Heap(堆)

  4. Method Area(方法区)

  5. Run-Time Constant Pool(运行时常量池)

  6. Native Method Stacks(本地方法栈)

1
2
3
4
5
6
7
8
9
10
11
JVM规范中的运行时数据区
The pc Register(程序计数器):是一块较小的内存空间,此计数器记录的是正在执行的虚拟机字节码指令的地址;
Java Virtual Machine Stacks(Java虚拟机栈):Java虚拟机栈用于存储栈帧。栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
Heap(堆):是Java虚拟机所管理的最大的一块内存。堆内存用于存放Java对象实例以及数组。堆是垃圾收集器收集垃圾的主要区域。
Method Area(方法区):用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
这个是规范,但是在实现中有些会加到堆里
Run-Time Constant Pool(运行时常量池):是方法区的一部分,用于存放编译期生成的各种字面量与符号引用(类名、属性名等)。
Native Method Stacks(本地方法栈):在本地方法的执行过程中,会使用到本地方法栈。和 Java 虚拟机栈十分相似。


总结:这些运行时数据区虽然在功能上有所区别,但在整个 Java 虚拟机启动时都需要被创建,并且在虚拟机运行期间始终存在,直到虚拟机停止运行时被销毁。同时,不同的 JVM 实现对运行时数据区的分配和管理方式也可能不同,会对性能和功能产生影响。

**image-20240225214223891

image-20240225214331825

9、设计模式概述

单例模式(==GoF==23种设计模式之一,最简单的设计模式:==如何保证某种类型的对象只创建一个)==

image-20240225220833968

//饿汉式单例模式:不管这个对象用还是不用,提前把对象给创建好了。

step1:构造函数私有化//不让new,但是又可以获得方法

step2:对外提供一个公开的静态方法,用这个方法获取单个实例

step3: 定义一个静态变量,在类加载的时候,初始化静态变量(只初始化一次)。

//懒汉式单例模式:等到用的时候再创建,不用不创建。

step1:构造方法私有化

step2:对外提供一个静态方法,可以通过这个方法获取到对象;

这个是初步的,等到以后多线程的时候,还需要 改进

10.继承

① 面向对象三大特征之一:继承

② 继承作用?

l基本作用:代码复用

l重要作用:有了继承,才有了方法覆盖和多态机制。

③继承在java中如何实现?

l[修饰符列表] class 类名 extends 父类名{}

lextends翻译为扩展。表示子类继承父类后,子类是对父类的扩展。

④继承相关的术语:当B类继承A类时

lA类称为:父类、超类、基类、superclass

lB类称为:子类、派生类、subclass

⑤Java只支持单继承,一个类只能直接继承一个类。

⑥Java不支持多继承,但支持多重继承(多层继承)。

⑦子类继承父类后,除私有的不支持继承、构造方法不支持继承。其它的全部会继承。

==⑧一个类没有显示继承任何类时,默认继承java.lang.Object类。==比如tostring就是object的,任何一个类都可以调用

image-20240225221336300

11.方法覆盖

方法覆盖/override/方法重写/overwrite,是在编译器层面的功能,方法重载机制是给编译器看的

①什么情况下考虑使用方法覆盖?

1.当从父类中继承过来的方法无法满足当前子类的业务需求时。

②发生方法覆盖的条件?

  1. 具有==继承关系的父子类之间==

  2. ==相同的返回值类型,相同的方法名,相同的形式参数列表==

  3. 访问权限不能变低,可以变高。

image-20240225225653075

高———————>低

  1. 抛出异常不能变多,可以变少。

父类抛了,子类不抛,比如

  1. 返回值类型可以是父类方法返回值类型的子类。

③方法覆盖的小细节:

  1. @Override注解标注的方法会在编译阶段检查该方法是否重写了父类的方法。

  2. ==私有方法不能继承,所以不能覆盖。==

  3. ==构造方法不能继承,所以不能覆盖。==

  4. ==静态方法不存在方法覆盖,方法覆盖针对的是实例方法。==

  5. ==方法覆盖说的实例方法,和实例变量无关。(==可以写程序测试一下)

image-20240225224843910

image-20240225224915679

在java中也有个注解,在编译的时候可以检查这个方法是否是重写了父类的方法@Override

@Override 只在编译阶段会有用,和运行期无关.

如果返回值类型是引用类型,那么这个返回值类型可以是原类型的子类型

image-20240225225250788

12.多态

多态的基础语法

①什么是向上转型和向下转型?

  1. java允许具有继承关系的父子类型之间的类型转换。

  2. ==向上转型(upcasting):子–>父==

==l子类型的对象可以赋值给一个父类型的引用。==

  1. ==向下转型(downcasting):父–>子==

一般来说,想要调用的方法是子类当中特有的方法的时候,才会想要向下转型.

==l父类型的引用可以转换为子==类型的引用。但是需要加强制类型转换符。

  1. 无论是向上转型还是向下转型,前提条件是:==两种类型之间必须存在继承关系。这样编译器才能编译通过。==

②什么是多态?

编译过程中是一个形态,运行的时候又是另外一个形态.

  1. 父类型引用指向子类对象。Animal a = new Cat(); a.move();

  2. 程序分为编译阶段和运行阶段:

l编译阶段:编译器只知道a是Animal类型,因此去Animal类中找move()方法,找到之后,绑定成功,编译通过。这个过程通常被称为静态绑定。

l运行阶段:运行时和JVM堆内存中的真实Java对象有关,所以运行时会自动调用真实对象的move()方法。这个过程通常被称为动态绑定。

  1. 多态指的是:多种形态,编译阶段一种形态,运行阶段另一种形态,因此叫做多态。

image-20240225231010553

image-20240225231835927

③ instanceof运算符可以解决类型转换问题

  1. instanceof运算符的结果一定是:true/false
  2. 语法结构
1
2
3
(a instanceof cat)
true表示:a引用指向的对象是cat类型
false表示:a引用的指向的对象不是cat

这个运算符主要就是用来进行判断.

1
2
3
if(x instanceof Bird){
Bird y =(Bird)x;
}

④ 软件开发七大原则

软件开发原则旨在引导软件行业的从业者在代码设计和开发过程中,遵循一些基本原则,以达到高质量、易维护、易扩展、安全性强等目标。软件开发原则与具体的编程语言无关的,属于软件设计方面的知识。

  1. ==开闭原则 (Open-Closed Principle,OCP):一个软件实体应该对扩展开放,对修改关闭。即在不修改原有代码的基础上,通过添加新的代码来扩展功能。(最基本的原则,其它原则都是为这个原则服务的。)==

  2. 单一职责原则:一个类只负责单一的职责,也就是一个类只有一个引起它变化的原因。

  3. 里氏替换原则:子类对象可以替换其基类对象出现的任何地方,并且保证原有程序的正确性。

  4. 接口隔离原则:客户端不应该依赖它不需要的接口。

  5. 依赖倒置原则:高层模块不应该依赖底层模块,它们都应该依赖于抽象接口。换言之,面向接口编程。

  6. 迪米特法则:一个对象应该对其它对象保持最少的了解。即一个类应该对自己需要耦合或调用的类知道得最少。

  7. 合成复用原则:尽量使用对象组合和聚合,而不是继承来达到复用的目的。组合和聚合可以在获取外部对象的方法中被调用,是一种运行时关联,而继承则是一种编译时关联。

⑤ 多态在开发中的作用

  1. 降低程序的耦合度,提高程序的扩展力。

  2. 尽量使用多态,面向抽象编程,不要面向具体编程。

⑥多态的基础语法

向下转型我们需要注意什么?

  1. 向下转型时,使用不当,容易发生类型转换异常:ClassCastException。

  2. 在向下转型时,一般建议使用instanceof运算符进行判断来避免ClassCastException的发生。

instanceof运算符的使用

  1. 语法格式:(引用 instanceof 类型)

  2. 执行结果是true或者false

  3. 例如:(a instanceof Cat)

l如果结果是true:表示a引用指向的对象是Cat类型的。

l如果结果是false:表示a引用指向的对象不是Cat类型的。

13 . 抽象类

注意!抽象类不一定有抽象方法,但是有抽象方法的类一定是抽象类。

此外,public和abstract没有顺序关系

①什么时候考虑将类定义为抽象类?

如果类中有些方法无法实现或者没有意义,可以将方法定义为抽象方法。类定义为抽象类。这样在抽象类中只提供公共代码,具体的实现强行交给子类去做。比如一个Person类有一个问候的方法greet(),但是不同国家的人问候的方式不同,因此greet()方法具体实现应该交给子类。再比如主人喂养宠物的例子中的宠物Pet,Pet中的eat()方法的方法体就是没有意义的。

②抽象类如何定义?

abstract class 类名{}

③抽象类有构造方法,但无法实例化。抽象类的构造方法是给子类使用的。

④抽象方法如何定义?

abstract 方法返回值类型 方法名(形参);

没有方法体

⑤抽象类中不一定有抽象方法,但如果有抽象方法那么类要求必须是抽象类。

==⑥一个非抽象的类继承extend抽象类,要求必须将抽象方法进行实现/重写。==

需要把全部的抽象方法全部给实现了

⑦abstract关键字不能和private,final,static关键字共存。

private: private是不能被继承覆盖的,而abstract修饰的是一定要被继承实现的,存在冲突.

final: 不能继承

static:不能被覆盖

⑧抽象类需要构造方法,但是无法实现实例化,这个构造方法是给其子类使用的.

14. super关键字

①super关键字和this关键字对比来学习。this代表的是当前对象。super代表的是当前对象中的父类型特征。

②super不能使用在静态上下文中。就是在编写

③“super.”大部分情况下是可以省略的。什么时候不能省略?

==当父类和子类中定义了相同的属性(实例变量)或者相同方法(实例方法)时,如果需要在子类中访问父类的属性或方法时,super.不能省略。==

④this可以单独输出,super不能单独输出。

this本身是一个引用,所以可以直接输出,super不是引用,不能直接输出

1
2
system.out.println(this);//√
System.out.println(super);//×

⑤super(实参); 通过子类的构造方法调用父类的构造方法,目的是为了完成父类型特征的初始化。

⑥当一个构造方法第一行没有显示的调用“super(实参);”,也没有显示的调用“this(实参)”,系统会自动调用super()。因此一个类中的无参数构造方法建议显示的定义出来。

⑦super(实参); 这个语法只能出现在构造方法第一行。

this()和super()

⑧在Java语言中只要new对象,Object的无参数构造方法一定会执行。

image-20240226111210195

image-20240226112229142

子类的构造方法中,就算没有写super();,也是会跑这个代码.

1
2
3
4
5
6
super();
会跑父类的构造方法.也会给父类的字段赋值.
当一个构造方法,没有显示的写this(),也没有显示的写super(),系统会自动的调用super()
当然也可以是:super(actno , balance);
调用父类的方法: super.doSome();

15.final关键字

这个关键字表示是最终的,被final所修饰的类不能被 继承

①final修饰的类不能被继承

②final修饰的方法不能被覆盖

③final修饰的变量,一旦赋值不能重新赋值

④final修饰的实例变量必须==在对象初始化时==手动赋值

必须在构造方法执行完之前,手动附上值.不允许采用系统的默认值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
final class Mystring{
public final void m(){

}
}
//比如String就是不能被继承

final String name;//×
或者
final String name;
public User(String name , int age){
this.name = name;
this.age = age;

}

⑤final修饰的实例变量一般和static联合使用:==称为常量==

因为类中定义的final 修饰的变量不能修改,最直接的理解就是所有的实例变量的这个变量都是一个值,因此,假设new出来了100个,那么这100个类的这个字段也都是同一个,所以可以直接加上static,设置成一个静态的,更方便,也更省堆空间.

1
static final double MATH_PAI = 3.1415926

⑥final修饰的引用,一旦指向某个对象后,不能再指向其它对象。但指向的对象内部的数据是可以修改的。

1
final 修饰对象,内存地址不能改,但是内存地址里面的东西还是可以修改的

image-20240226150228776

16.接口

接口的基础语法

==①接口(interface)在Java中表示一种规范或契约,它定义了一组抽象方法和常量,用来描述一些实现这个接口的类应该具有哪些行为和属性==接口和类一样,也是一种引用数据类型

②接口怎么定义?[修饰符列表] interface 接口名{}

1
2
3
public interface Myinterface{
里面的方法可不能有方法体
}

③抽象类是半抽象的(抽象类可以定义抽象方法也可以定义非抽象方法 ,有构造方法不能new对象),

​ 接口是完全抽象的。==接口没有构造方法,也无法实例化。==

④接口中只能定义:常量+抽象方法。==接口中的常量的static final可以省略==。接口中的抽象方法的abstract可以省略。接口中所有的方法和变量都是public修饰的。(JDK8)

⑤接口和接口之间可以多继承。

⑥类和接口的关系我们叫做实现(**==这里的实现也可以等同看做继承==)。使用==implements==**关键字进行接口的实现。

⑦一个非抽象的类实现接口必须将接口中所有的抽象方法全部实现。

⑧一个类可以实现多个接口。语法是:class 类 implements 接口A,接口B{}

image-20240226161310540

⑨Java8之后,接口中允许出现默认方法和静态方法(JDK8新特性)

引入默认方式是为了解决接口演变问题:接口可以定义抽象方法,但是不能实现这些方法。所有实现接口的类都必须实现这些抽象方法。这会导致接口升级的问题:当我们向接口添加或删除一个抽象方法时,这会破坏该接口的所有实现,并且所有该接口的用户都必须修改其代码才能适应更改。这就是所谓的”接口演变”问题。

如果还是按照之前的,那么如果需要在接口中加入一个新的方法,按照接口的实现类需要把借口中的全部都给实现了这一个要求,这个接口的所有实现类都需要实现这个接口,这太麻烦了.

加入默认方法后,实现该接口的实现类中都有这个默认方法,但是不需要去实现

引入的静态方法只能使用本接口名来访问,无法使用实现类的类名访问。

原因:接口也可以当工具使用,渐渐变成了工具类

默认方法:

image-20240226162231265

静态方法:

1
2
3
4
5
6
7
8
9
static void staticmethod(){


}

//调用
MyInterface.staticmethod();
//不可以
MyInterfaceImpl.staticmethod();

image-20240226162707551⑩JDK9之后允许接口中定义私有private的实例方法(为默认方法服务的)和私有的静态方法(为静态方法服务的)。

⑪所有的接口隐式的继承Object。因此接口也可以调用Object类的相关方法。

接口的作用

①面向接口调用的称为:接口调用者

②面向接口实现的称为:接口实现者

③调用者和实现者通过接口达到了解耦合。也就是说调用者不需要关心具体的实现者,实现者也不需要关心具体的调用者,双方都遵循规范,面向接口进行开发。

面向抽象编程,面向接口编程,可以降低程序的耦合度,提高程序的==扩展力==。

⑤例如定义一个Usb接口,提供read()和write()方法,通过read()方法读,通过write()方法写:

①定义一个电脑类Computer,它是调用者,面向Usb接口来调用。

②Usb接口的实现可以有很多,例如:打印机(Printer),硬盘(HardDrive)。

1
2
3
4
5
6
7
8
9
10
11
public class Computer{

public void conn(Usb usb){

usb.read();

usb.write();

}

}

⑦再想想,我们平时去饭店吃饭,这个场景中有没有接口呢?食谱菜单就是接口。顾客是调用者。厨师是实现者。

接口与抽象类如何选择

①抽象类和接口虽然在代码角度都能达到同样的效果,但适用场景不同:

​ 抽象类主要适用于公共代码的提取。当多个类中有共同的属性和方法时,为了达到代码的复用,建议为这几个类提取出来一个父类,在该父类中编写公共的代码。如果有一些方法无法在该类中实现,可以延迟到子类中实现。这样的类就应该使用抽象类。

接口主要用于功能的扩展。例如有很多类,一些类需要这个方法,另外一些类不需要这个方法时,可以将该方法定义到接口中。需要这个方法的类就去实现这个接口,不需要这个方法的就可以不实现这个接口。接口主要规定的是行为。

注意:一个类继承某个类的同时可以实现多个接口:class 类 extends 父类 implements 接口A,接口B{}

注意:当某种类型向下转型为某个接口类型时,接口类型和该类之间可以没有继承关系,编译器不会报错的。

17. UML(统一建模语言概述)

考虑一下类之间的关系.

①UML(Unified Modeling Language,统一建模语言)是一种用于面向对象软件开发的图形化的建模语言。它由Grady Booch、James Rumbaugh和Ivar Jacobson等三位著名的软件工程师所开发,并于1997年正式发布。UML提供了一套通用的图形化符号和规范,帮助开发人员以图形化的形式表达软件设计和编写的所有关键方面,从而更好地展示软件系统的设计和实现过程。

②UML是一种图形化的语言,类似于现实生活中建筑工程师画的建筑图纸,图纸上有特定的符号代表特殊的含义。

③UML不是专门为java语言准备的。只要是面向对象的编程语言,开发前的设计,都需要画UML图进行系统设计。(设计模式、软件开发七大原则等同样也不是只为java语言准备的。)

④UML图包括:

  • 类图(Class Diagram):描述软件系统中的类、接口、关系和其属性等;
  • 用例图(Use Case Diagram):描述系统的功能需求和用户与系统之间的关系;
  • 序列图(Sequence Diagram):描述对象之间的交互、消息传递和时序约束等;
  • 状态图(Statechart Diagram):描述类或对象的生命周期以及状态之间的转换;
  • 对象图(Object Diagram):表示特定时间的系统状态,并显示其包含的对象及其属性;
  • 协作图(Collaboration Diagram):描述对象之间的协作,表示对象之间相互合作来完成任务的关系;
  • 活动图(Activity Diagram):描述系统的动态行为和流程,包括控制流和对象流;
  • 部署图(Deployment Diagram):描述软件或系统在不同物理设备上部署的情况,包括计算机、网络、中间件、应用程序等。

⑤常见的UML建模工具有:StarUML,Rational Rose等。

类之间的关系

  1. 泛化关系(is a)//继承关系
1
2
3
4
泛化关系(继承关系)is a关系。Cat is a Animal
public class Animal{ }
public class Cat extends Animal{ }
public class Dog extends Animal{ }

image-20240226173319940

  1. 实现关系(is like a)//定义一个接口,然后有一个实现接口类
1
2
3
4
实现关系 is like a 关系。
public interface Usb{}
public class Printer implements Usb{}
public class HardDrive implements Usb{}

image-20240226173405538

  1. 关联关系(has a)
1
2
3
4
5
6
7
8
9
关联关系 has a
public class Course{

}
public class Student {
// 实例变量(属性)
Course course;
}
A里面有B

image-20240226173521101

  1. 聚合关系

聚合关系指的是一个类包含、合成或者拥有另一个类的实例,而这个实例是可以独立存在的。聚合关系是一种弱关联关系,表示整体与部分之间的关系。例如一个教室有多个学生

image-20240226174835150

==生命不绑定在一起==

  1. 组合关系(Composition)

组合关系是聚合关系的一种特殊情况,表示整体与部分之间的关系更加强烈。组合关系指的是一个类包含、合成或者拥有另一个类的实例,而这个实例只能同时存在于一个整体对象中。如果整体对象被销毁,那么部分对象也会被销毁。==例如一个人对应四个肢体==。

1
2
3
4
5
6
7
public class Limbs{

}

public class Person{
List<Limbs> limbs;
}

image-20240226175009462

  1. 依赖关系(Dependency)

依赖关系是一种临时性的关系,**==当一个类使用另一个类的功能时==**,就会产生依赖关系。如果一个类的改变会影响到另一个类的功能,那么这两个类之间就存在依赖关系。依赖关系是一种较弱的关系,可以存在多个依赖于同一个类的对象。==例如A类中使用了B类,但是B类作为A类的方法参数或者局部变量等。==

image-20240226175047202

1
2
3
4
5
6
7
8
9
10
public class User{
public void doSome(A a){

}
public void doOther(){
A a;
}
}

public class A {}

==18. 访问控制权限==

image-20240226175206352

①private:私有的,只能在本类中访问。

②缺省:默认的,同一个包下可以访问。

③protected:受保护的,子类中可以访问。(==受保护的通常就是给子孙用的。==)clone就是protected的

④public:公共的,在任何位置都可以访问。

==①类中的属性和方法访问权限共有四种:private、缺省、protected和public。==

==②类的访问权限只有两种:public和 缺省。==

1
2
3
4
5
6
7
8
9
10
public class A{

}
class B{

}
只有以上两种是正确的
protected class C{
//这个就是错误的
}

③访问权限控制符不能修饰局部变量。

注意:class里面的属性,方法的访问权限是四个;但是对于定义类,是只有两种访问权限的.!

image-20240226175919594

19.Object类

①java.lang.Object是所有类的超类。java中所有类都实现了这个类中的方法。

②Object类是我们学习JDK类库的第一个类。通过这个类的学习要求掌握会查阅API帮助文档。

③现阶段Object类中需要掌握的方法:

toString:将java对象转换成字符串。

image-20240226180456350

equals:判断两个对象是否相等。//返回一个布尔类型

1
2
3
4
5
6
7
8
9
10
11
12
== 的作用:

  基本类型:比较的就是值是否相同

  引用类型:比较的就是地址值是否相同
  
equals 的作用:

  引用类型:默认情况下,比较的是地址值。

注:不过,我们可以根据情况自己重写该方法。一般重写都是自动生成,比较对象的成员变量值是否相同
String类中被复写的equals()方法其实是比较两个字符串的内容

④现阶段Object类中需要了解的方法:

hashCode:返回一个对象的哈希值,通常作为在哈希表中查找该对象的键值。Object类的默认实现是根据对象的内存地址生成一个哈希码(即将对象的内存地址转换为整数作为哈希值)。hashCode()方法是为了HashMap、Hashtable、HashSet等集合类进行优化而设置的,以便更快地查找和存储对象。

finalize:当java对象被回收时,由GC自动调用被回收对象的finalize方法,通常在该方法中完成销毁前的准备。

==clone:对象的拷贝。(浅拷贝,深拷贝)==

Java深入理解深拷贝和浅拷贝区别_java深拷贝浅拷贝-CSDN博客

浅拷贝:

Object的默认方法,专门给子类使用的,受保护的,c++实现的

image-20240226181415375

user和usertest不符合三个要求,不能进行clone,那么怎么实现呢?

重写!

1.==建议把重写的修饰符改为public==

2.要实现接口 ==implements Cloneable==(这是一个标志接口,给java虚拟机看的)

image-20240226181815569

protected修饰的只能在同一个包下或者子类中访问。

只有实现了Cloneable接口的对象才能被克隆。

image-20240226182500898

image-20240226182936458

深拷贝:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//这是浅拷贝
public Object clone() throws CloneNotSupportedException {
Object object = super.clone();
return object;
}
//这是深拷贝
public Object clone() throws CloneNotSupportedException {
// 浅复制时:
// Object object = super.clone();
// return object;

// 改为深复制:
Student student = (Student) super.clone();
// 本来是浅复制,现在将Teacher对象复制一份并重新set进来
student.setTeacher((Teacher) student.getTeacher().clone());
return student;

}

20.clone

直接赋值

直接赋值的方式没有生产新的对象,只是生新增了一个对象引用**,直接赋值在 Java 内存中的模型大概是这样的

image-20240226184438712

浅拷贝

如果原型对象的成员变量是值类型,将复制一份给克隆对象,也就是说在堆中拥有独立的空间;

如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说原型对象和克隆对象的成员变量指向相同的内存地址。

换句话说,在浅克隆中,当对象被复制时只复制它本身和其中包含的值类型的成员变量,而引用类型的成员对象并没有复制。

image-20240226184739067

也就是新开一个空间,放克隆的对象,但是这个地址里放的还是原来的引用地址

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 Person implements Cloneable {
// 姓名
private String name;
// 年龄
private int age;
// 邮件
private String email;
// 描述
private String desc;

/*
* 重写 clone 方法,需要将权限改成 public ,直接调用父类的 clone 方法就好了
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
...省略...
}\\
***************
person对象:Person{name='张三', age=20, email='123456@qq.com', desc='我是张三'}

person1对象:Person{name='我是张三的克隆对象', age=22, email='123456@qq.com', desc='我是张三'}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class PersonApp {
public static void main(String[] args) throws Exception {
// 初始化一个对象
Person person = new Person("张三",20,"123456@qq.com","我是张三");
// 复制对象
Person person1 = (Person) person.clone();
// 改变 person1 的属性值
person1.setName("我是张三的克隆对象");
// 修改 person age 的值
person1.setAge(22);
System.out.println("person对象:"+person);
System.out.println();
System.out.println("person1对象:"+person1);

}
}

String、Integer 等包装类都是不可变的对象,当需要修改不可变对象的值时,需要在内存中生成一个新的对象来存放新的值,然后将原来的引用指向新的地址,所以在这里我们修改了 person1 对象的 name 属性值,person1 对象的 name 字段指向了内存中新的 name 对象,

但是我们并没有改变 person 对象的 name 字段的指向,所以 person 对象的 name 还是指向内存中原来的 name 地址,也就没有变化

深拷贝

深拷贝也是对象克隆的一种方式,相对于浅拷贝,深拷贝是一种完全拷贝,无论是值类型还是引用类型都会完完全全的拷贝一份,在内存中生成一个新的对象,简单点说就是拷贝对象和被拷贝对象没有任何关系,互不影响。深拷贝的通用模型如下

img

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
public class PersonDesc implements Cloneable{

// 描述
private String desc;
...省略...
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
public class Person implements Cloneable {
// 姓名
private String name;
// 年龄
private int age;
// 邮件
private String email;

private PersonDesc personDesc;

/**
* clone 方法不是简单的调用super的clone 就好,
*/
@Override
public Object clone() throws CloneNotSupportedException {
Person person = (Person)super.clone();
// 需要将引用对象也克隆一次
person.personDesc = (PersonDesc) personDesc.clone();
return person;
}
...省略...
}

21.内部类

image-20240226185936183

静态内部类:

1
2
3
4
5
6
7
8
9
对于静态内部类来说:访问控制权限修饰符在这里都可以使用。
private static class InnerClass{

}

//怎么创建内部类对象
OuterClass.InnerClass innerClass = new OuterClass.InnerClass();
OuterClass.InnerClass.m4();
innerClass.m3();

由于在创建静态内部类的时候,不用创建外部的那个类,所以不能访问外部类所定义的实例变量和实例方法,但是可以访问静态变量和静态方法,因为静态的(static)是刚开始运行的时候就会跑的,写入编译文件中的

实例内部类

1
2
3
* 实例内部类:等同可以看做实例变量。
* 结论:实例内部类中可以直接访问外部类中实例成员和静态成员。
就是在创建这个实例内部类中,会创建外部类//自己的理解
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
public class OuterClass {
// 实例变量
public int i = 100;
// 实例方法
public void m1(){
System.out.println("外部类的实例方法m1执行了");
}

// 静态变量
private static int j = 200;
// 静态方法
public static void m2(){
System.out.println("外部类的静态方法m2执行了");
}

// 实例内部类
// 也可以使用访问权限修饰符修饰。
public class InnerClass {
public void x(){
System.out.println(i);
System.out.println(j);
m1();
m2();
}
}

}

public class OuterClassTest {
public static void main(String[] args) {
OuterClass outerClass = new OuterClass();
System.out.println(outerClass.i);
OuterClass.InnerClass innerClass = outerClass.new InnerClass();
innerClass.x();
}
}

局部内部类

①局部内部类:和局部变量一个级别

局部内部类方外类外部的局部变量时,局部变量需要被final修饰。

从JDK8开始,不需要手动添加final了,但JVM会自动添加。

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
public class OuterClass {

// 静态变量
private static int k = 1;
// 实例变量
private int f = 2;

public void m1(){
// 局部变量
int i = 100;
// 局部内部类
class InnerClass {
// 实例方法
public void x(){
System.out.println(k);
System.out.println(f);
System.out.println(i);
}
}
// new对象
InnerClass innerClass = new InnerClass();
innerClass.x();
//这两行执行了,这个内部类就要执行,内部类执行就需要这个实例方法执行,这个实例方法执行,就要这个外部类创建,所以在不受限制的情况下,实例变量/方法和静态变量/方法是都可以访问的,但是不可以改,是默认加final的

}
}
/**
* 局部内部类:等同于局部变量。
*
* 结论:局部内部类能不能访问外部类的数据,取决于局部内部类所在的方法。
* 如果这个方法是静态的:只能访问外部类中静态的。
* 如果这个方法是实例的:可以都访问。
*
* 局部内部类不能使用访问权限修饰符修饰。
*
* 局部内部类在访问外部的局部变量时,这个局部变量必须是final的。只不过从JDK8开始。这个final关键字不需要提供了。系统自动提供。
*/

==匿名内部类==

特殊的局部内部类,没有名字,只能用一次。

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
package com.powernode.javase.oop45;

/**
* 匿名内部类:没有名字的类。只能使用一次。
*/
public class Test {
public static void main(String[] args) {
// 创建电脑对象
Computer computer = new Computer();
//computer.conn(new Printer());

// 以下conn方法参数上的代码做了两件事:
// 第一:完成了匿名内部类的定义。
// 第二:同时实例化了一个匿名内部类的对象。
computer.conn(new Usb(){
// 接口的实现
@Override
public void read() {
System.out.println("read.....");
}

@Override
public void write() {
System.out.println("write.....");
}
});
}
}

class Computer {
public void conn(Usb usb){
usb.read();
usb.write();
}
}

interface Usb {
void read();
void write();
}

// 编写一个接口的实现类
/*
class Printer implements Usb {

@Override
public void read() {
System.out.println("打印机开始读取数据");
}

@Override
public void write() {
System.out.println("打印机开始打印");
}
}
*/

这个是从原理开始讲的,这个我不照着写,我就是学

springboot内置了tomcat,所以可以直接打包成jar包

什么是tomcat

Tomcat是常见的免费的web服务器。

1. 不使用tomcat访问html

可以在浏览器的地址里看到 file:d:/test.html 这样的格式,是通过打开本地文件的形式打开的

但是我们平时上网看到的html网址一般都是: http://12306.com/index.html 这样的形式
这是因为有web服务器的存在

2.使用tomcat后,访问html

使用tomcat后,可以像127.0.0.1:8080/test.html这样访问一个网站,而不是访问一个html文件。因为tomcat本身就是一个web服务器,test.html部署在了这个web服务器上

img

SpringBoot启动

一、创建DashboardView.vue界面

这个我没有全听,就是复制过来了,大致理解了 p28-29

1. 布局

2.菜单制作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<el-menu
active-text-color="#ffd04b"
background-color="#334157"
class="el-menu-vertical-demo"
:default-active="currentRouterPath"
text-color="#fff"
style="border-right: solid 0px;"
:collapse="isCollapse"
:collapse-transition="false"
:router="true"
:unique-opened="true">

<el-sub-menu :index="index" v-for="(menuPermission, index) in user.menuPermissionList" :key="menuPermission.id">
<template #title>
<el-icon><component :is="menuPermission.icon"></component></el-icon>
<span> {{menuPermission.name}} </span>
</template>
<el-menu-item v-for="subPermission in menuPermission.subPermissionList" :key="subPermission.id" :index="subPermission.url">
<el-icon><component :is="subPermission.icon"></component></el-icon>
{{subPermission.name}}
</el-menu-item>
</el-sub-menu>
</el-menu>

3.菜单加入图标/美观

(1) 安装图标

npm install @element-plus/icons-vue –save

(2)注册所有图标,在main.js中注册:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createApp } from 'vue'//createApp这个是一个函数
//import './style.css'
import App from './App.vue'//从一个单文件组件中导入根组件
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//从router.js导入组件
import router from './router/router.js'

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
//在这里先创建了,然后再后续的操作
let app = createApp(App);

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {

app.component(key, component)

}
import loginView from "./view/LoginView.vue";
app.use(ElementPlus).use(router).mount('#app')//创建一个vue对象//挂载到页面的#app这个id下

(3)使用图标,复制图标的代码,粘贴到项目中即可;

Icon 图标 | Element Plus (gitee.io)

Vue中:

1
2
3
$refs 拿到页面上ref属性的那个dom元素;

$router vue路由对象,里面提供了一些方法供什么使用;

image-20240222220629465

(4)添加图标的动作

1
<el-icon class="show" @click="showMenu"><Fold /></el-icon>
1
2
3
4
5
methods : {
//左侧菜单左右展开和折叠
showMenu() {
this.isCollapse = !this.isCollapse;
},

isCollapse定义

image-20240222221224747

设置变量:

image-20240222221302322

调整宽度

image-20240222221350959

4.加入下拉菜单、vue函数钩子

函数钩子:就是在渲染页面的时候,就把用户的名字等信息获得到,然后再渲染到页面当中去

(1)写script

1
2
3
4
5
6
7
8
写script
//vue的生命周期中的一个函数钩子,该钩子是在页面渲染后执行
mounted() {
//加载当前登录用户
this.loadLoginUser();
this.loadCurrentRouterPath();
},

(2)写templete 显示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
写templete 显示
<!--右侧:上-->
<el-header>
<el-icon class="show" @click="showMenu"><Fold /></el-icon>

<el-dropdown :hide-on-click="false">
<span class="el-dropdown-link">
{{ user.name }}
<el-icon class="el-icon--right"><arrow-down /></el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>我的资料</el-dropdown-item>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item divided @click="logout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>

</el-header>

(3)写方法

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
写方法!
methods : {
//左侧菜单左右展开和折叠
showMenu() {
this.isCollapse = !this.isCollapse;
},

//加载当前登录用户
loadLoginUser() {

doGet("/api/login/info", {}).then( (resp) => {
console.log(resp)
this.user = resp.data.data;
})
},

//退出登录
logout() {
doGet("/api/logout", {}).then(resp => {
if (resp.data.code === 200) {
removeToken();
messageTip("退出成功", "success");
//跳到登录页
window.location.href = "/";
} else {
messageConfirm("退出异常,是否要强制退出?").then(() => { //用户点击“确定”按钮就会触发then函数
//既然后端验证token未通过,那么前端的token肯定是有问题的,那没必要存储在浏览器中,直接删除一下
removeToken();
//跳到登录页
window.location.href = "/";
}).catch(() => { //用户点击“取消”按钮就会触发then函数
messageTip("取消强制退出", "warning");
})
}
})
},

//加载当前路由路径
loadCurrentRouterPath() {
let path = this.$route.path; // /dashboard/activity/add
let arr = path.split("/"); // [ ,dashboard, activity, add]
if (arr.length > 3) {
this.currentRouterPath = "/" + arr[1] + "/" + arr[2];
} else {
this.currentRouterPath = path;
}
}
}

==下面这个调用的就是httpRequest.js里面的doGet的axios==

doGet("/api/login/info", {}).then( (resp) => {
  console.log(resp)
  this.user = resp.data.data;
})

!!**==因此要写后端的接口==**,UserController

5.获取登录人信息后端代码实现

image-20240222222945744 image-20240222223051935

(1)编写UserController.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
//UserController.java

package com.bjpowernode.web;

import com.bjpowernode.model.TUser;
import com.bjpowernode.result.CodeEnum;
import com.bjpowernode.result.R;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

// 因为是查询请求 所以写getmapping
@GetMapping(value = "/api/login/info")
// 登录后的用户信息怎么拿?
public R loginInfo(Authentication authentication){
TUser tUser = (TUser)authentication.getPrincipal() ;
return R.OK(tUser);

// public static R OK(Object data) {
// return R.builder()
// .code(CodeEnum.OK.getCode())
// .msg(CodeEnum.OK.getMsg())
// .data(data)
// .build();
// }

}
}

(2)apifox测试成功

image-20240222224207981

(3)前台调用测试

没改 嗯,不一定能成

二、用JWT替换sessionP36

第一章 Redis 概述

一、Redis(Remote Dictionary Server)远程字典服务

  1. 使用 ==ANSI C 语言==编写、==支持网络、可基于内存亦可持久化的日志型、NoSQL 开源内存数据库==,其提供多种语言的 API。

  2. Redis被称为字典服务的原因在于,redis是一个key-value的存储系统。

    支持存储的value类型有很多,包括String(字符串)、List(链表)、Set(集合)、Zset(sorted set– 有序集合)和哈希类型等

二、NoSQL

Nosql(non-relational,Not Only SQL),泛指的是非关系型的数据库。

比如社交类的等等

==NoSQL 数据库的产生就是为了解决大规模数据集合多重数据种类带来的挑战,特别是大数据应用难题==

传统方法不适合这种情况。

1. 键值存储数据库

类似Map的key-value对。

典型代表redis

2. 列存储数据库

注意的是 ,关系数据库是行关系库,其存在的问题在于,按照行存储的数据在物理层面占用的是连续的存储空间,==是不适合海量的数据存储==的。

而按列存储则可以实现一个分布式存储,适合海量的存储

典型的代表就是HBase

3. 文档型数据库

NoSQL与关系型数据的结合,最想关系型数据库的NoSQL

典型代表就是MongoDB。

4. 图像(Graph)数据库

用于存放一个节点关系的数据库,例如描述不同人间的关系。典型代表是 Neo4J。

三、Redis的用途

1.数据缓存

Redis在生产中用的最多的场景:==数据缓存==。

正常情况下,用户->服务器->数据库 (query),然后再返回。

但是当用户很多的时候,就是高并发的时候,解决不了了。

因此提出解决方案: 用户->服务器->缓存-> 数据库 (query)

image-20240223001703204

这样DBMS压力就会减轻。

即客户端从 DBMS 中查询出的数据首先写入到 Redis 中,后续无论哪个客户端再需要访问该数据,直接读取 Redis 中的即可,不仅减小了 RT,而且降低了 DBMS 的压力。

而且,DBMS一般存放在磁盘中,而Redis作为内存数据库是放在内存中的,且直接通过key来查询,缓存层的查询非常的快,高并发的问题可以解决。

数据不一致问题?:

当一个用户提出要修改,修改了DBMS,没有修改缓存层,就会存在一个脏数据/

image-20240223074739083

预热:服务器不是刚开就提供服务,会有一个预热,把一些基础数据先保存到缓存中

四、Redis 特性

1.性能极高:

Redis 读的速度可以达到 11w 次/s,写的速度可以达到 8w 次/s。只所以具有,这么高的性能,因为以下几点原因:

1)Redis 的所有操作都是在内存中发生的。

2)Redis 是用 C 语言开发的。

3)Redis 源码非常精细(集性能与优雅于一身)。

2.简单稳定

Redis 源码很少。早期版本只有 2w 行左右。从 3.0 版本开始,增加了集群功能,代码变为了 5w 行左右。

3.持久化

数据缓存是暂时的,要保存到磁盘,才能保证数据安全。Redis 内存中的数据可以进行持久化,其有两种方式:RDB 与 AOF。

4.高可用集群

Redis 提供了高可用的主从集群功能,可以确保系统的安全性

5.丰富的数据类型

Redis 是一个 key-value 存储系统。支持存储的 value 类型很多,包括String(字符串)、List(链表)、Set(集合)、Zset(sorted set –有序集合)和 Hash(哈希类型)等,还有 BitMap、HyperLogLog、Geospatial 类型

  1.  BitMap:一般用于大数据量的二值性统计。
    比如员工是否打卡

  2.  HyperLogLog:其是 Hyperlog Log,用于对数据量超级庞大的日志做去重统计。

  3.  Geospatial:地理空间,其主要用于地理位置相关的计算。

    比如“附近的人”

6.强大的功能

Redis 提供了数据过期功能、发布/订阅功能、简单事务功能,还支持 Lua脚本扩展功能。

7.客户端语言广泛

Redis提供了简单的 TCP 通信协议,编程语言可以方便地的接入 Redis。所以,有很多的开源社区、大公司等开发出了很多语言的 Redis 客户端。

8.支持 ACL 权限控制

之前的权限控制非常笨拙。从 Redis6 开始引入了 ACL 模块,可以为不同用户定制不同的用户权限。

image-20240223081342029

linux 的drwxr-xr-x :d表示目录/l表示链接,rwx表示用户(创建者User)读写可执行;Group r-x后三位表示和创建者同组用户读可执行。Other r-x

9.支持多线程 IO 模型

Redis 之前版本采用的是单线程模型,从 6.0 版本开始支持了多线程模型。

五.Redis IO 模型

单线程模型 多线程模型 混合模型

Redis 客户端提交的各种请求是如何最终被 Redis 处理的?Redis 处理客户端请求所采用的处理架构,称为 Redis 的 IO 模型。不同版本的 Redis 采用的 IO 模型是不同的。

1.单线程模型

性能还是很强,写8w/qps,读11w/qps–>内存\单线程可维护性比较高(多线程存在线程切换问题,顾及到数据安全问题\死锁问题)

问题:持久化等操作和客户端没太多联系,但是又比较费时,于是提出了混合线程模型

对于 Redis 3.0 及其以前版本,Redis 的 IO 模型采用的是纯粹的单线程模型。即所有客户端的请求全部由一个线程处理。

image-20240223082516763

每个客户端若要向 Redis 提交请求,都需要与 Redis 建立一个 socket 连接,并向事件分发器注册一个事件。

一旦该事件发生就表明该连接已经就绪。而一旦连接就绪,事件分发器就会感知到,然后获取客户端通过该连接发送的请求,并将由该事件分发器所绑定的这个唯一的线程来处理。

如果该线程还在处理多个任务,则将该任务写入到任务队列等待线程处理。只所以称为事件分发器,是因为它会根据不同的就绪事件,将任务交由不同的事件处理器去处理

==Redis 的单线程模型采用了多路复用技术。==

对于多路复用器的多路选择算法常见的有三种:select 模型、poll 模型、epoll 模型。

  1. poll 模型的选择算法:采用的是轮询算法。该模型对客户端的就绪处理是有延迟的。

  2. epoll 模型的选择算法:采用的是回调方式。根据就绪事件发生后的处理方式的不同,又可分为 LT 模型与 ET 模型。

2.混合线程模型

从 Redis 4.0 版本开始,Redis 中就开始加入了多线程元素。处理客户端请求的仍是单线

程模型,但对于一些比较耗时但又不影响对客户端的响应的操作,就由后台其它线程来处理。

例如,持久化、对 AOF 的 rewrite、对失效连接的清理等

3.多线程模型

image-20240223083447687

内存响应时间(RT)100ns

Redis 每秒处理读写请求 达到:1s/100ns = =1kw次,实际情况是,redis8w/qps,读11w/qps,差距那么大的原因 就是redis的单线程

是将单线程和多线程各取其优点.

image-20240223083854779

这个主要是登录页面后端项目,包括一些基础的搭建工作

一、项目分层

image-20240222121622494

这个就是最常见的,这张图来自于阿里规范

  1. 终端显示层:Vue

  2. 开放接口:给其他公司调用

  3. 请求处理层:web层

image-20240222130745201

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3. 【参考】分层领域模型规约:
• DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
这个do对应的就是项目中的model

• DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
相当于是service和manager层,但是在项目中没有使用

• BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
没有使用


• Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
controller ->service->manager->DO 传参数,这个参数是用query来封装的

• VO(View Object):显示层对象,通常是 Web 向模板渲染引擎层传输的对象。
等价于result的我们的R对象

step1:按照上图构建文件目录

image-20240222130603666

在result里面加入封装的代码

image-20240222132938591

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
package com.bjpowernode.result;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 统一封装web层向前端页面返回的结果
*
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class R {

//表示返回的结果码,比如200成功,500失败
private int code;

//表示返回的结果信息,比如 用户登录状态失效了,请求参数格式有误.......
private String msg;

//表示返回的结果数据,数据可能是一个对象,也可以是一个List集合.....
private Object data;

public static R OK() {
return R.builder()
.code(CodeEnum.OK.getCode())
.msg(CodeEnum.OK.getMsg())
.build();
}

public static R OK(int code, String msg) {
return R.builder()
.code(code)
.msg(msg)
.build();
}

public static R OK(Object data) {
return R.builder()
.code(CodeEnum.OK.getCode())
.msg(CodeEnum.OK.getMsg())
.data(data)
.build();
}

public static R OK(CodeEnum codeEnum) {
return R.builder()
.code(CodeEnum.OK.getCode())
.msg(codeEnum.getMsg())
.build();
}

public static R FAIL() {
return R.builder()
.code(CodeEnum.FAIL.getCode())
.msg(CodeEnum.FAIL.getMsg())
.build();
}

public static R FAIL(String msg) {
return R.builder()
.code(CodeEnum.FAIL.getCode())
.msg(msg)
.build();
}

public static R FAIL(CodeEnum codeEnum) {
return R.builder()
.code(codeEnum.getCode())
.msg(codeEnum.getMsg())
.build();
}
}


image-20240222133207911

传参数是统统都用query来传输

step2:后端登录代码实现

后台提供一个spring security的接口

(1)创建UserService层

image-20240222133807376

(2)extends UserDetailsService

1
2
public interface UserService extends UserDetailsService {
}

UserDetailsService 里面有关于数据库查找的一些相关的内容

(3)写(2)接口的实现

image-20240222134110834

image-20240222134241729

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.bjpowernode.service.impl;

import com.bjpowernode.service.UserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {


@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return null;
}
}

注入 Mapper

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Service
public class UserServiceImpl implements UserService {


@Resource
private TUserMapper tUserMapper;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser tUser = tUserMapper.selectByLoginAct(username);

if (tUser == null){
throw new UsernameNotFoundException("登录账号不存在!");
}

// 按照username查找对象,返回一个TUser对象
return null;
}
}

==selectByLoginAct没有实现==,所以需要去实现一下

image-20240222134902977

image-20240222135129842

1
2
3
4
5
6
<select id="selectByLoginAct" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from t_user
where login_act = #{username,jdbcType=VARCHAR}
</select>

==这样就完成了,相当于定义一个函数(这个函数要进行的是一些sql命令),然后去xml中实现这个函数==

(4)改写return的内容

关于UserDeitails接口,详见

【详解】Spring Security的GrantedAuthority(已授予的权限)_org.springframework.security.core.grantedauthority-CSDN博客

==getAuthorities()方法将返回此用户的所拥有的权限。这个集合将用于用户的访问控制,也就是Authorization。==

==所谓权限,就是一个字符串。一般不会重复。==

==所谓权限检查,就是查看用户权限列表中是否含有匹配的字符串。==

之后学习Spring Security

1. 准备工作

image-20240222135527190

这个return按理来说是需要返回一个UserDetails的对象,但是现在是只有一个函数返回的tUser 的对象,所以我们需要去TUser.java处去实现这个接口。如下图所示:

image-20240222135713974

注意,在UserDetails这个接口中存在7个方法,所以我们需要在TUser中把这7个方法全部都实现了。

image-20240222135807457

在TUser.java处 快捷键alt+insert,把所有的接口函数导入一下

image-20240222140250334

image-20240222140327393

2. 编写实现
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
//    *********************************实现UserDetails的七个方法**************************************************
// 这个是角色list
private List<String> permissionList;
// 权限标识符list
private List<String> roleList;

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 先定义一个list 用来返回得到的角色对象
List<GrantedAuthority> list = new ArrayList<>();

//获得角色list,forEach遍历,遍历得到list的每个对象是role,然后将这个role add到list中
this.getRoleList().forEach(role ->{
list.add(new SimpleGrantedAuthority(role));
});
//获得权限标识符
this.getPermissionList().forEach(permission ->{
list.add(new SimpleGrantedAuthority(permission));
});
return list;
}

@Override
public String getPassword() {

return this.getPassword();
}

@Override
public String getUsername() {
return this.getUsername();
}

@Override
public boolean isAccountNonExpired() {
// 这个是账号有无过期,这是这个项目的数据库表的设计 ,数据库里个字段,保证可行就是没有过期
return this.getAccountNoExpired()==1;
}

@Override
public boolean isAccountNonLocked() {
return this.getAccountNoLocked()==1;
}

@Override
public boolean isCredentialsNonExpired() {
return this.getCredentialsNoExpired()==1;
}

@Override
public boolean isEnabled() {
return this.getAccountEnabled()==1;
}
}
3.生成jwt ,忽略,全部+@JsonIgnore

image-20240222142026445

不把返回值字段当成js字段

4.完成 到UserServiceImpl.java处,return tUser
5.写一个配置类

image-20240222142430458

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, CorsConfigurationSource configurationSource) throws Exception {
//禁用跨站请求伪造
return httpSecurity
// 链式编程,这边是要做一个登录
.formLogin( (formLogin) -> {
formLogin.loginProcessingUrl(Constants.LOGIN_URI) //登录处理地址,不需要写Controller
.usernameParameter("loginAct")
.passwordParameter("loginPwd")
// 以上是登录,如果登录成功了,就要返回一个结果
.successHandler(myAuthenticationSuccessHandler)
// 如果失败了也要返回结果
.failureHandler(myAuthenticationFailureHandler);
})
// 以上的handle是直接用的别的,之后如需要详细了解,再研究
//需要对于请求拦截
.authorizeHttpRequests( (authorize) -> {
authorize.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated(); //其它任何请求都需要登录后才能访问
})

==里面用到了一些handler的一些文件,直接用的,需要进一步学习==

因为用到了这些,所以需要注入一下

1
2
3
4
5
6
7
8
9
10
11
12
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;

@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;

因为是外面复制进来的,所以需要改写一下:

(在com.bjpowernode目录下创建package util,在里面加入JSONUtils工具类、ResponseUtils。)

MyAuthenticationSuccessHandler

就是成功后需要做什么事情

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

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {


@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//通过request、response,得到authentication 登录成功后的用户信息
// 登录成功,执行该方法,在该方法中返回json给前端,就行了
TUser tUser = (TUser) authentication.getPrincipal();

//登录成功,用户信息返回前台
R result = R.OK(tUser);

//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);

//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
6.完成最简单的实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity, CorsConfigurationSource configurationSource) throws Exception {
//禁用跨站请求伪造
return httpSecurity
// 链式编程,这边是要做一个登录
.formLogin( (formLogin) -> {
formLogin.loginProcessingUrl("/api/login") //登录处理地址,不需要写Controller
.usernameParameter("loginAct")
.passwordParameter("loginPwd")
// 以上是登录,如果登录成功了,就要返回一个结果
.successHandler(myAuthenticationSuccessHandler)
// 如果失败了也要返回结果
.failureHandler(myAuthenticationFailureHandler);
})
// 以上的handle是直接用的别的,之后如需要详细了解,再研究
//需要对于请求拦截
.authorizeHttpRequests( (authorize) -> {
authorize.requestMatchers("/api/login").permitAll()
.anyRequest().authenticated(); //其它任何请求都需要登录后才能访问
})


.build();
}
7.用Apifox验证是否成功

使用网页版,如下操作

image-20240222152059742

image-20240222152137062

image-20240222152456724

image-20240222152604580

最后选择开发环境

image-20240222152626036

image-20240222154805491

8. 一些错误需要修改
a.密码加密了。所以需要注入一个//SecurityConfig
1
2
3
4
5
@Bean //There is no PasswordEncoder mapped for the id "null"
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

细的需要再学习一下java se—

b. 需要排除一下list为空的问题//TUser.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 先定义一个list 用来返回得到的角色对象
List<GrantedAuthority> list = new ArrayList<>();

//获得角色list,forEach遍历,遍历得到list的每个对象是role,然后将这个role add到list中
if(!ObjectUtils.isEmpty(this.getRoleList())){
this.getRoleList().forEach(role ->{
list.add(new SimpleGrantedAuthority(role));
});
}

//获得权限标识符
if(!ObjectUtils.isEmpty(this.getPermissionList())){
this.getPermissionList().forEach(permission ->{
list.add(new SimpleGrantedAuthority(permission));
});
}

return list;
}
9.前后端联通–>解决跨域问题

image-20240222203338737

此时启动前后端系统,输入账号密码会出现这个错误,这个cors错误叫做跨域错误

跨域报错:Access to XMLHttpRequest at ‘http://localhost:8089/api/login‘ from origin ‘http://localhost:8081‘ has been blocked by CORS policy;

跨域:

源头:http://localhost:8081 目的地:http://localhost:8089/api/login

1、协议不同, http://localhost:8080 https://localhost:8080

2、域名不同, http://localhost:8080 http://dlyk.bjpowernode.com:8080

3、端口不同 http://localhost:8080 http://localhost:8082

三个里面只要有任何一个不同就是跨域;

image-20240222203608653

==如何解决!==

image-20240222203942321

1
2
3
4
5
securityFilterChain这个函数里面
//支持跨域请求
.cors( (cors) -> {
cors.configurationSource(configurationSource);
})
1
2
3
4
5
6
7
8
9
10
11
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); //允许任何来源,http://localhost:8081
configuration.setAllowedMethods(Arrays.asList("*")); //允许任何请求方法,post、get、put、delete
configuration.setAllowedHeaders(Arrays.asList("*")); //允许任何的请求头

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

ctrl+h 看接口的几个实现

image-20240222204459230

10.axios请求后返回的响应对象

image-20240222205748168

其中data就是后端springboot响应返回的那个R对象;

11.修改前端接到后端返回数据后的操作

a.创建返回的util//因为后续可能很多地方都需要使用

image-20240222210454177

1
2
3
4
5
6
7
8
9
export function messageTip(msg, type) {
ElMessage({
showClose: true, //是否显示关闭按钮
center: true, //文字是否居中
duration: 3000, //显示时间,单位为毫秒
message: msg, //提示的消息内容
type: type, //消息类型:'success' | 'warning' | 'info' | 'error'
})
}
b.修改登录成功后返回的消息
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
//这个是loginvView.vue
//比如一些消息提醒等
methods: {
//登录函数
login() {
//提交前验证输入框的合法性
this.$refs.loginRefForm.validate( (isValid) => {
//isValid是验证后的结果,如果是true表示验证通过,否则未通过
if (isValid) {
//验证通过,可以提交登录
//这是个js对象,可用来传输数据,用键值对传输对象
let formData = new FormData();
formData.append("loginAct", this.user.loginAct);
formData.append("loginPwd", this.user.loginPwd);
formData.append("rememberMe", this.user.rememberMe);

doPost("/api/login", formData).then( (resp) => {
//用了==>才能使用this这个变量
console.log(resp);
if (resp.data.code === 200) {
//登录成功,提示一下
messageTip("登录成功", "success");
} else {
//登录失败,也提示一下
messageTip("登录失败", "error");
}
});
}
})
},

//免登录(自动登录)
freeLogin() {
let token = window.localStorage.getItem(getTokenName());
if (token) { //表示token有值,token不是空,token存在
doGet("/api/login/free", {}).then(resp => {
if (resp.data.code === 200) {
//token验证通过了,那么可以免登录
window.location.href = "/dashboard";
}
})
}
}
}

12.登录成功后跳转系统的主页面

a.跳转页面
1
window.location.href = "/dashboard";

image-20240222211409570

b.添加路由

路由:就是vue访问的路径;

比如: /

比如:/dashboard

1、用npm命令安装路由

npm install vue-router –save

2、在src下面新建一个router文件夹,在router文件夹下新建一个router.js文件,在router.js文件里面写入以下代码:

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
//从vue-router这个依赖库中导入createRouter()函数, createWebHistory()函数
import { createRouter, createWebHistory } from "vue-router";
//定义一个变量
let router = createRouter({
//路由的历史,可以实现一些前进后退
history:createWebHistory(),
//p配置路由,里面可以配置多个路由,
//路由就是vue访问的路径
routes:[
{
//路由的路径
path:'/',
//路由路径所对应的页面
component : () => import('../view/LoginView.vue'),
},
{
path:'/dashboard',
//路由路径所对应的页面
component : () => import('../view/dashboard.vue'),
}
],


})
//这个是导出路由,导出之后才能用
export default router;


image-20240222212744356

c. 在使用路由时导入路由:

在main.js中导入:

1
import router from './router/router.js'
d.在main.js中使用路由

app.use(router);

1
2
3
4
5
6
7
8
9
10
import { createApp } from 'vue'//createApp这个是一个函数
//import './style.css'
//import App from './App.vue'//从一个单文件组件中导入根组件
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//从router.js导入组件
import router from './router/router.js'
import loginView from "./view/LoginView.vue";
createApp(loginView).use(ElementPlus).use(router).mount('#app')//创建一个vue对象//挂载到页面的#app这个id下

e. 使用<router-view/>标签渲染路由地址所对应的页面组件;

和上面的区别是导入了App ,且createApp(App)

1
2
3
4
5
6
7
8
9
import { createApp } from 'vue'//createApp这个是一个函数
//import './style.css'
import App from './App.vue'//从一个单文件组件中导入根组件
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
//从router.js导入组件
import router from './router/router.js'
import loginView from "./view/LoginView.vue";
createApp(App).use(ElementPlus).use(router).mount('#app')//创建一个vue对象//挂载到页面的#app这个id下

image-20240222213354364

如果访问/. ,那么就渲染LoginView.vue,如果是/dashBoard,那么就渲染dashboard.vue

0%