前言
主要是记录学习Java时的笔记,这篇学习的是的常用API、正则表达式、Lambda、算法和集合(Collection、数据结构、LIst、泛型深入、Set、Collection、Map、集合嵌套、不可变集合、Stream、异常
提示:以下是本篇文章正文内容
一、常用API
什么是API?:应用程序编程接口(就是Java帮我们写好的一些方法,我们直接拿过来用就可以了)
1.Object(祖宗类)
父类toString()方法存在的意义是为了被子类重写,以便返回对象的内容信息,而不是地址信息!!
父类equals()方法存在的意义就是为了被子类重写,以便子类自己来定制比较规制(如比较两个子类对象的内容是否相同)!!,如果比较两个对象的地址可以用“==”代替
代码实例如下
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", sex=" + sex +
", age=" + age +
'}';
}
}
public class Test2 {
public static void main(String[] args) {
Student s1 = new Student("周雄", '男', 19);
Student s2 = new Student("周雄", '男', 19);
// equals默认是比较2个对象的地址是否相同,子类重写后会调用子类重写的来比较内容是否相同。
System.out.println(s1.equals(s2));
System.out.println(s1 == s2);
System.out.println(Objects.equals(s1, s2));
}
}
// 比较者:s1 == this
// 被比较者: s2 ==> o
@Override
public boolean equals(Object o){
// 1、判断o是不是学生类型
if(o instanceof Student){
Student s2 = (Student) o;
// 2、判断2个对象的内容是否一样。
// if(this.name.equals(s2.name) &&
// this.age == s2.age && this.sex == s2.sex){
// return true;
// }else {
// return false;
// }
return this.name.equals(s2.name) && this.age == s2.age&& this.sex == s2.sex ;
}else {
// 学生只能和学生比较,否则结果一定是false
return false;
}
}
// 比较者:s1 == this
// 被比较者: s2 ==> o
@Override//改进版--IDEA自动生成的代码
public boolean equals(Object o) {
// 1、判断是否是同一个对象比较,如果是返回true。
if (this == o) return true;
// 2、如果o是null返回false 如果o不是学生类型返回false ...Student != ..Pig
if (o == null || this.getClass() != o.getClass()) return false;
// 3、说明o一定是学生类型而且不为null
Student student = (Student) o;
return sex == student.sex && age == student.age && Objects.equals(name, student.name);
}
2.Objects(工具类-提供一些方法完成一些功能)
官方在进行字符串比较时,没有用字符串对象的equals方法,而是选择equals方法来比较(更安全-内部会进行非空校验)
实例代码
public class Test {
public static void main(String[] args) {
String s1 = null;
String s2 = new String("itheima");
// System.out.println(s1.equals(s2)); // 留下了隐患,可能出现空指针异常。
System.out.println(Objects.equals(s1, s2)); // 更安全,结果也是对的!
/**
Objects:
public static boolean equals(Object a, Object b) {
return (a == b) || (a != null && a.equals(b));
}
*/
System.out.println(Objects.isNull(s1)); // true
System.out.println(s1 == null); // true
System.out.println(Objects.isNull(s2)); // false
System.out.println(s2 == null); // false
}
}
3.StringBuilder(不可变的字符串类)
可以把它看成是一个对象容器,提高字符串的操作效率,如拼接、修改等
注意:StringBuilder只是拼接字符串的手段,效率好(支持追加,反转,链式编程),最终的目的还是要恢复成String类型
实例代码
public class StringBuilderDemo1 {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder(); // ""
sb.append("a");
sb.append(1);
sb.append(false);
sb.append(3.3);
sb.append("abc");
System.out.println(sb);
StringBuilder sb1 = new StringBuilder();
// 支持链式编程
sb1.append("a").append("b").append("c").append("我爱你中国");
System.out.println(sb1);
// 反转
sb1.reverse().append("110");
System.out.println(sb1);
System.out.println(sb1.length());
// 注意:StringBuilder只是拼接字符串的手段:效率好。
// 最终的目的还是要恢复成String类型。
StringBuilder sb2 = new StringBuilder();
sb2.append("123").append("456");
// 恢复成String类型
String rs = sb2.toString();
check(rs);
}
public static void check(String data){
System.out.println(data);
}
}
定义字符串使用String,拼接、修改等操作字符串使用StringBuilder
String类拼接字符串原理图-String内容时不可变的,拼接字符串性能差
StringBuilder提高效率原理图-内容时可变的,拼接字符串性能好,代码优雅
案例-打印数组内容
public class StringBuilderTest2 {
public static void main(String[] args) {
int[] arr1 = null;
System.out.println(toString(arr1));
int[] arr2 = {10, 88, 99};
System.out.println(toString(arr2));
int[] arr3 = {};
System.out.println(toString(arr3));
}
/**
1、定义方法接收任意整型数组,返回数组内容格式
*/
public static String toString(int[] arr){
if(arr != null){
// 2、开始拼接内容。
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
sb.append(arr[i] ).append(i == arr.length - 1 ? "" : ", ");
}
sb.append("]");
return sb.toString();//最后转换回字符串
}else {
return null;
}
}
}
//输出结果
//null
//[10,88,99]
//[]
4.Math(基本数字运算的方法)
Math类(工具类)没有提供公开的构造器,如果类中的成员是静态的话,可以用类名直接调用
实例代码
System.out.println(Math.random()); // 0.0 - 1.0 (包前不包后)
// 拓展: 3 - 9 之间的随机数 (0 - 6) + 3
// [0 - 6] + 3
int data = (int)(Math.random() * 7) + 3;
System.out.println(data);
5.System(工具类)
代表了当前系统,提供了一些与系统相关的方法
实例代码
public class SystemDemo {
public static void main(String[] args) {
System.out.println("程序开始。。。");
// System.exit(0); // JVM终止!
// 2、计算机认为时间有起源:返回1970-1-1 00:00:00 走到此刻的总的毫秒值:时间毫秒值。
long time = System.currentTimeMillis();
System.out.println(time);
long startTime = System.currentTimeMillis();
// 进行时间的计算:性能分析
for (int i = 0; i < 100000; i++) {
System.out.println("输出:" + i);
}
long endTime = System.currentTimeMillis();
System.out.println((endTime - startTime)/1000.0 + "s");
// 3、做数组拷贝(了解)
/**
arraycopy(Object src, int srcPos,
Object dest, int destPos,
int length)
参数一:被拷贝的数组
参数二:从哪个索引位置开始拷贝
参数三:复制的目标数组
参数四:粘贴位置
参数五:拷贝元素的个数
*/
int[] arr1 = {10, 20, 30, 40, 50, 60, 70};
int[] arr2 = new int[6]; // [0, 0, 0, 0, 0, 0] ==> [0, 0, 40, 50, 60, 0]
System.arraycopy(arr1, 3, arr2, 2, 3);
System.out.println(Arrays.toString(arr2));
System.out.println("-------------------");
double i = 10.0;
double j = 3.0;
// System.out.println(k1);
System.out.println("程序结束。。。。");
}
}
6.BigDecimal(用于解决浮点型运算精度失真的问题)
实例代码
public class BigDecimalDemo {
public static void main(String[] args) {
// 浮点型运算的时候直接+ * / 可能会出现数据失真(精度问题)。
System.out.println(0.09 + 0.01);
System.out.println(1.0 - 0.32);
System.out.println(1.015 * 100);
System.out.println(1.301 / 100);
System.out.println("-------------------------");
double a = 0.1;
double b = 0.2;
double c = a + b;
System.out.println(c);
System.out.println("--------------------------");
// 包装浮点型数据成为大数据对象 BigDeciaml
BigDecimal a1 = BigDecimal.valueOf(a);
BigDecimal b1 = BigDecimal.valueOf(b);
BigDecimal c1 = a1.add(b1);
// BigDecimal c1 = a1.subtract(b1);
// BigDecimal c1 = a1.multiply(b1);
// BigDecimal c1 = a1.divide(b1);
System.out.println(c1);
// 目的:double
double rs = c1.doubleValue();
System.out.println(rs);
// 注意事项:BigDecimal是一定要精度运算的
BigDecimal a11 = BigDecimal.valueOf(10.0);//获取BigDecimal对象
BigDecimal b11 = BigDecimal.valueOf(3.0);
/**
参数一:除数 参数二:保留小数位数 参数三:四舍五入模式(其它还有进一法、去尾法)
*/
BigDecimal c11 = a11.divide(b11, 2, RoundingMode.HALF_UP); // 3.3333333333
System.out.println(c11);
System.out.println("-------------------");
}
}
7.Date
Date类代表当前所在系统的日期时间信息
案例–请计算出当前时间往后走1小时121秒之后的时间是多少(获取时间毫秒值,然后恢复成日期对象)
public static void main(String[] args) {
// 1、得到当前时间
Date d1 = new Date();
System.out.println(d1);
// 2、当前时间往后走 1小时 121s
long time2 = System.currentTimeMillis();
time2 += (60 * 60 + 121) * 1000;
// 3、把时间毫秒值转换成对应的日期对象。
// Date d2 = new Date(time2);
// System.out.println(d2);
Date d3 = new Date();
d3.setTime(time2);
System.out.println(d3);
}
8.SimpleDateFormat
可以完成日期时间的格式化操作
实例代码—格式化标准的时间形式
public static void main(String[] args) {
// 1、日期对象
Date d = new Date();
System.out.println(d);
// 2、格式化这个日期对象 (指定最终格式化的形式)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss EEE a");
// 3、开始格式化日期对象成为喜欢的字符串形式
String rs = sdf.format(d);
System.out.println(rs);
System.out.println("----------------------------");
// 4、格式化时间毫秒值
// 需求:请问121秒后的时间是多少
long time1 = System.currentTimeMillis() + 121 * 1000;
String rs2 = sdf.format(time1);
System.out.println(rs2);
}
案例
示例代码----解析时间
public class SimpleDateFormatDemo2 {
public static void main(String[] args) throws ParseException {
// 目标: 学会使用SimpleDateFormat解析字符串时间成为日期对象。
// 有一个时间 2021年08月06日 11:11:11 往后 2天 14小时 49分 06秒后的时间是多少。
// 1、把字符串时间拿到程序中来
String dateStr = "2021年08月06日 11:11:11";
// 2、把字符串时间解析成日期对象(本节的重点):形式必须与被解析时间的形式完全一样,否则运行时解析报错!
SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss");
Date d = sdf.parse(dateStr);
// 3、往后走2天 14小时 49分 06秒
long time = d.getTime() + (2L*24*60*60 + 14*60*60 + 49*60 + 6) * 1000;//要加L防止数据越界
// 4、格式化这个时间毫秒值就是结果
System.out.println(sdf.format(time));
}
}
案例—秒杀活动
实例代码
public static void main(String[] args) throws ParseException {
// 1、开始 和 结束时间
String startTime = "2021-11-11 00:00:00";
String endTime = "2021-11-11 00:10:00";
// 2、小贾 小皮
String xiaoJia = "2021-11-11 00:03:47";
String xiaoPi = "2021-11-11 00:10:11";
// 3、解析他们的时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date d1 = sdf.parse(startTime);
Date d2 = sdf.parse(endTime);
Date d3 = sdf.parse(xiaoJia);
Date d4 = sdf.parse(xiaoPi);
if(d3.after(d1) && d3.before(d2)){
System.out.println("小贾秒杀成功,可以发货了!");
}else {
System.out.println("小贾秒杀失败!");
}
if(d4.after(d1) && d4.before(d2)){
System.out.println("小皮秒杀成功,可以发货了!");
}else {
System.out.println("小皮秒杀失败!");
}
}
9.Calendar
Calendar代表了系统此刻日期对应的日历对象,它是一个抽象类不能创建对象
calendar是可变日期对象,一旦修改后其对象本身表示的时间将产生变化
实例代码
public static void main(String[] args) {
// 1、拿到系统此刻日历对象
Calendar cal = Calendar.getInstance();
System.out.println(cal);
// 2、获取日历的信息:public int get(int field):取日期中的某个字段信息。
int year = cal.get(Calendar.YEAR);
System.out.println(year);
int mm = cal.get(Calendar.MONTH) + 1;
System.out.println(mm);
int days = cal.get(Calendar.DAY_OF_YEAR) ;
System.out.println(days);
// 3、public void set(int field,int value):修改日历的某个字段信息。
// cal.set(Calendar.HOUR , 12);
// System.out.println(cal);
// 4.public void add(int field,int amount):为某个字段增加/减少指定的值
// 请问64天后是什么时间
cal.add(Calendar.DAY_OF_YEAR , 64);
cal.add(Calendar.MINUTE , 59);
// 5.public final Date getTime(): 拿到此刻日期对象。
Date d = cal.getTime();
System.out.println(d);
// 6.public long getTimeInMillis(): 拿到此刻时间毫秒值
long time = cal.getTimeInMillis();
System.out.println(time);
10.包装类
就是8种基本数据类型对应的引用类型
Java为了实现一切皆对象,为8种基本类型提供了对应的引用类型]
、
集合和泛型只能支持包装类型,而不支持基本数据类型
自动装箱
:基本类型的数据和变量可以直接赋值给包装类型的变量。
自动拆箱
:包装类型的变量可以直接赋值给基本数据类型的变量
包装类特有的功能:
1、包装类的变量默认值可以是null,容错率更高。
2、可以把基本数据类型转换成字符串类型。
3、可以把字符串类型的数值转换成真实的数据类型
String number = "23";
//转换成整数
// int age = Integer.parseInt(number);
int age = Integer.valueOf(number);
System.out.println(age + 1);
String number1 = "99.9";
//转换成小数
// double score = Double.parseDouble(number1);
double score = Double.valueOf(number1);
System.out.println(score + 0.1);
二、正则表达式、Arrays类、Lambda表达式
1、正则表达式
正则表达式可以用一些规定的字符来制定规制,并用来检验数据格式的合法性
案例
示例代码
public static void main(String[] args) {
// 需求:校验qq号码,必须全部数字 6 - 20位
System.out.println(checkQQ("251425998"));//true
System.out.println(checkQQ("2514259a98"));//false
System.out.println(checkQQ(null));//false
System.out.println(checkQQ("2344"));//false
System.out.println("-------------------------");
// 正则表达式的初体验:
System.out.println(checkQQ2("251425998"));
System.out.println(checkQQ2("2514259a98"));
System.out.println(checkQQ2(null));
System.out.println(checkQQ2("2344"));
}
//正则表达式判断
public static boolean checkQQ2(String qq){
//\d代表都是数字,\转译字符,
return qq != null && qq.matches("\\d{6,20}");
}
public static boolean checkQQ(String qq){
// 1、判断qq号码的长度是否满足要求
if(qq == null || qq.length() < 6 || qq.length() > 20 ) {
return false;
}
// 2、判断qq中是否全部是数字,不是返回false
// 251425a87
for (int i = 0; i < qq.length(); i++) {
// 获取每位字符
char ch = qq.charAt(i);
// 判断这个字符是否不是数字,不是数字直接返回false
if(ch < '0' || ch > '9') {
return false;
}
}
return true; // 肯定合法了!
}
正则表达式的匹配规则
字符串对象提供了匹配正则表达式规则的API
示例代码
// 校验密码
// 必须是数字 字母 下划线 至少 6位
System.out.println("2442fsfsf".matches("\\w{6,}"));//true
System.out.println("244f".matches("\\w{6,}"));//false
// 验证码 必须是数字和字符 必须是4位
System.out.println("23dF".matches("[a-zA-Z0-9]{4}"));//true
System.out.println("23_F".matches("[a-zA-Z0-9]{4}"));//false
System.out.println("23dF".matches("[\\w&&[^_]]{4}"));//true
System.out.println("23_F".matches("[\\w&&[^_]]{4}"));//false
案例
示例代码
public static void main(String[] args) {
// 目标:校验 手机号码 邮箱 电话号码
// checkPhone();
// checkEmail();
// checkTel();
// 同学可以完成校验金额是否格式金额: 99 0.5 99.5 019 | 0.3.3
int[] arr = {10, 4, 5,3, 4,6, 2};
System.out.println(Arrays.binarySearch(arr, 2));
}
public static void checkTel(){
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请您输入您的电话号码:");
String tel = sc.next();
// 判断邮箱格式是否正确 027-3572457 0273572457
if(tel.matches("0\\d{2,6}-?\\d{5,20}")){
System.out.println("格式正确,注册完成!");
break;
}else {
System.out.println("格式有误!");
}
}
}
public static void checkEmail(){
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请您输入您的注册邮箱:");
String email = sc.next();
// 判断邮箱格式是否正确 3268847878@qq.com
// 判断邮箱格式是否正确 3268847dsda878@163.com
// 判断邮箱格式是否正确 3268847dsda878@pci.com.cn
if(email.matches("\\w{1,30}@[a-zA-Z0-9]{2,20}(\\.[a-zA-Z0-9]{2,20}){1,2}")){
System.out.println("邮箱格式正确,注册完成!");
break;
}else {
System.out.println("格式有误!");
}
}
}
public static void checkPhone(){
Scanner sc = new Scanner(System.in);
while (true) {
System.out.println("请您输入您的注册手机号码:");
String phone = sc.next();
// 判断手机号码的格式是否正确
if(phone.matches("1[3-9]\\d{9}")){
System.out.println("手机号码格式正确,注册完成!");
break;
}else {
System.out.println("格式有误!");
}
}
}
正则表达式在字符串方法中的使用
示例代码
public static void main(String[] args) {
String names = "小路dhdfhdf342蓉儿43fdffdfbjdfaf小何";
String[] arrs = names.split("\\w+");//从字符数字下划线开始分割
for (int i = 0; i < arrs.length; i++) {
System.out.println(arrs[i]);//小路-蓉儿-小何
}
String names2 = names.replaceAll("\\w+", " ");//小路 蓉儿 小何
System.out.println(names2);
}
正则表达式支持
爬取信息
public static void main(String[] args) {
String rs = "跟黑马程序学习Java,电话020-43422424,或者联系邮箱" +
"itcast@itcast.cn,电话18762832633,0203232323" +
"邮箱bozai@itcast.cn,400-100-3233 ,4001003232";
// 需求:从上面的内容中爬取出 电话号码和邮箱。
// 1、定义爬取规则,字符串形式
String regex = "(\\w{1,30}@[a-zA-Z0-9]{2,20}(\\.[a-zA-Z0-9]{2,20}){1,2})|(1[3-9]\\d{9})" +
"|(0\\d{2,6}-?\\d{5,20})|(400-?\\d{3,9}-?\\d{3,9})";
// 2、把这个爬取规则编译成匹配对象。
Pattern pattern = Pattern.compile(regex);
// 3、得到一个内容匹配器对象
Matcher matcher = pattern.matcher(rs);
// 4、开始找了
while (matcher.find()) {
String rs1 = matcher.group();//每找到一组取一组
System.out.println(rs1);
}
}
2.Arrays类
数组操作工具类,专门用于操作数组元素的。
示例代码
@Override//需重写toString(),不然返回的是地址
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
", height=" + height +
'}';
}
Student[] students = new Student[3];
students[0] = new Student("吴磊",23 , 175.5);
students[1] = new Student("谢鑫",18 , 185.5);
students[2] = new Student("王亮",20 , 195.5);
System.out.println(Arrays.toString(students));
// Arrays.sort(students); // 直接运行奔溃
/**
参数一:被排序的数组 必须是引用类型的元素
参数二:匿名内部类对象,代表了一个比较器对象。
*/
//Lambda简化形式
Arrays.sort(students,(Student o1,Student o2)->{return Double.compare(o2.getHeight(), o1.getHeight()); });
/
Arrays.sort(students, new Comparator<Student>() {
@Override
public int compare(Student o1, Student o2) {
// 自己指定比较规则
// return o1.getAge() - o2.getAge(); // 按照年龄升序排序!
// return o2.getAge() - o1.getAge(); // 按照年龄降序排序!!
// return Double.compare(o1.getHeight(), o2.getHeight()); // 比较浮点型可以这样写 升序
return Double.compare(o2.getHeight(), o1.getHeight()); // 比较浮点型可以这样写 降序
}
});
System.out.println(Arrays.toString(students));
3.Lambda表达式
Lambda表达式是JDK8开始后的一种新语法形式
主要是简化匿名内部类的代码写法(只能简化函数式接口的匿名内部类的写法形式)
函数式接口:必须是接口,其次接口中有且仅有一个抽象方法的形式
示例代码–简化过程
public class LambdaDemo2 {
public static void main(String[] args) {
// 目标:学会使用Lambda的标准格式简化匿名内部类的代码形式
// 注意:Lambda只能简化接口中只有一个抽象方法的匿名内部类形式(函数式接口)
// Swimming s1 = new Swimming() {
// @Override
// public void swim() {
// System.out.println("老师游泳贼溜~~~~~");
// }
// };
// Swimming s1 = () -> {
// System.out.println("老师游泳贼溜~~~~~");
// };
Swimming s1 = () -> System.out.println("老师游泳贼溜~~~~~");
go(s1);
System.out.println("---------------------");
// go(new Swimming() {
// @Override
// public void swim() {
// System.out.println("学生游泳很开心~~~");
// }
// });
// go(() ->{
// System.out.println("学生游泳很开心~~~");
// });
go(() -> System.out.println("学生游泳很开心~~~"));
}
public static void go(Swimming s){
System.out.println("开始。。。");
s.swim();
System.out.println("结束。。。");
}
}
@FunctionalInterface // 一旦加上这个注解必须是函数式接口,里面只能有一个抽象方法
interface Swimming{
void swim();
}
Lambda表达式的省略写法
1、参数类型可以省略不写
,如果只有一个参数,同时()也可以省略不写
2、如果方法体代码只有一行代码,可以省略大括号不写
,同时省略分号如果这行代码是return语句,必须省略return不写
,同时也必须省略“;”
不写。
示例代码
Arrays.sort(ages1, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2 - o1; // 降序
}
});
//Lambda简化
Arrays.sort(ages1, (Integer o1, Integer o2) -> {
return o2 - o1; // 降序
});
Arrays.sort(ages1, ( o1, o2) -> {
return o2 - o1; // 降序
});
//return不写,分号不写
Arrays.sort(ages1, ( o1, o2 ) -> o2 - o1 );
btn.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.out.println("有人点我,点我,点我!!");
}
});
//Lambda简化
btn.addActionListener((ActionEvent e) -> {
System.out.println("有人点我,点我,点我!!");
});
//一个参数的话,参数类型可以不写
btn.addActionListener(( e) -> {
System.out.println("有人点我,点我,点我!!");
});
//一个参数的话,()可以不写
btn.addActionListener( e -> {
System.out.println("有人点我,点我,点我!!");
});
//大括号不写,分号不写
btn.addActionListener( e -> System.out.println("有人点我,点我,点我!!") );
三、集合
集合的类型不固定,大小是可变的,并且只能储存引用类型的数据.集合适合做数据个数不确定,且要做增删元素的场景
集合都是支持泛型的,可以在编译阶段约束集合只能操作某种数据类型(JDK1.7开始后面的泛型类型声明可以省略不写)
集合和泛型都只能支持引用数据类型,不支持基本数据类型,所以集合中存储的元素都认为是对象
1.Collection(单列集合的祖宗接口)
每个元素只包含一个值,它的功能是全部单列集合都可以继承使用的
集合的遍历方式
1、迭代器(Iterator):
默认指向当前集合的索引0,如果取元素越界会出现NoSuchElenmentException
异常
2、foreach
(增强for循环JDK5之后)既可以遍历集合也可以遍历数组
示例代码
public static void main(String[] args) {
Collection<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
lists.add("殷素素");
lists.add("周芷若");
System.out.println(lists);
// [赵敏, 小昭, 殷素素, 周芷若]
// ele
for (String ele : lists) {
System.out.println(ele);
}
System.out.println("------------------");
double[] scores = {100, 99.5 , 59.5};
for (double score : scores) {
System.out.println(score);
// if(score == 59.5){//修改第三方变量的值不会影响到集合中的值
// score = 100.0; // 修改无意义,不会影响数组的元素值。
// }
}
System.out.println(Arrays.toString(scores));
}
3、Lambda表达式(JDK 8开始):
示例代码
public static void main(String[] args) {
Collection<String> lists = new ArrayList<>();
lists.add("赵敏");
lists.add("小昭");
lists.add("殷素素");
lists.add("周芷若");
System.out.println(lists);
// [赵敏, 小昭, 殷素素, 周芷若]
// s
lists.forEach(new Consumer<String>() {
@Override
public void accept(String s) {
System.out.println(s);
}
});
//Lambda表达式遍历的改进版
lists.forEach(s -> { System.out.println(s); });
lists.forEach(s -> System.out.println(s) );
//方法引用遍历方式
lists.forEach(System.out::println );
}
集合中存储的是元素对象的地址
2.数据结构(红黑树)
是计算机底层存储,组织数据的方式
红黑树是一种
自平衡的二叉查找树
,是计算机科学中用到的一种数据结构
它的节点可以是红或者黑根节点、叶节点Nil必须是黑色的,且不能出现两个红色节点相连的情况
,红黑树不是通过高度平衡
的,它的平衡是通过红黑规则
进行的。对每一个节点,从该节点到其所有后代节点的简单路径上,均包含相同数目的黑色节点
红黑树添加节点
默认用红色效率高
3.List(ArrayList,LinkedList)
添加的元素是有序的,可重复,有索引
ArrayList底层的数据结构是数组
LinkedList底层的数据结构是双链表,定位前后的元素是非常快的,可以实现栈和队列的功能
示例代码–集合遍历并删除元素可能出现的异常及改进方式
public static void main(String[] args) {
// 1、准备数据
ArrayList<String> list = new ArrayList<>();
list.add("黑黑");
list.add("Java");
list.add("Java");
list.add("赵敏");
list.add("赵敏");
list.add("素素");
System.out.println(list);
// [黑黑, Java, Java, 赵敏, 赵敏, 素素]
// it
// 需求:删除全部的Java信息。
// a、迭代器遍历删除
Iterator<String> it = list.iterator();
while (it.hasNext()){
String ele = it.next();
if("Java".equals(ele)){
// 删除Java
// list.remove(ele); // 集合删除会出毛病
it.remove(); // 删除迭代器所在位置的元素值(没毛病)
}
}
System.out.println(list);
// b、foreach遍历删除 (会出现问题,这种无法解决的,foreach不能边遍历边删除,会出bug)
for (String s : list) {
if("Java".equals(s)){
list.remove(s);
}
}
// c、lambda表达式(会出现问题,这种无法解决的,Lambda遍历不能边遍历边删除,会出bug)
list.forEach(s -> {//底层的逻辑是foreach()
if("Java".equals(s)){
list.remove(s);
}
});
// d、for循环(边遍历边删除集合没毛病,但是必须从后面开始遍历删除才不会出现漏掉应该删除的元素)
for (int i = list.size() - 1; i >= 0 ; i--) {
String ele = list.get(i);
if("Java".equals(ele)){
list.remove(ele);
}
}
System.out.println(list);
}
4.泛型(泛型类、泛型方法、泛型接口、通配符)
在编译阶段约束操作的数据类型,并进行检查
泛型只能支持引用数据类型,集合体系的全部接口和实现类都是支持泛型的使用的
泛型的好处是统一数据类型,把运行时期的问题提前到编译期间,避免了强制类型转换可能出现的异常,因为编译阶段类型就能确定下来
泛型类
—定义类的同时定义了泛型的类就是泛型类
示例代码
public class MyArrayList<E> {
private ArrayList lists = new ArrayList();
//套壳子,内部用的还是ArrayList//装饰模式的思想-外部对象包一个内部对象
public void add(E e){
lists.add(e);
}
public void remove(E e){//这个方法不是泛型方法,e是类上面定义的
lists.remove(e);
}
@Override
public String toString() {
return lists.toString();
}
}
public static void main(String[] args) {
// 需求:模拟ArrayList定义一个MyArrayList ,关注泛型设计
MyArrayList<String> list = new MyArrayList<>();
list.add("Java");
list.add("Java");
list.add("MySQL");
list.remove("MySQL");
System.out.println(list);
MyArrayList<Integer> list2 = new MyArrayList<>();
list2.add(23);
list2.add(24);
list2.add(25);
list2.remove(25);
System.out.println(list2);
}
泛型方法
—定义方法时的同时定义了泛型的方法就是泛型方法`
示例代码
public class GenericDemo {
public static void main(String[] args) {
String[] names = {"小璐", "蓉容", "小何"};
printArray(names);
Integer[] ages = {10, 20, 30};
printArray(ages);
Integer[] ages2 = getArr(ages);
String[] names2 = getArr(names);
}
public static <T> T[] getArr(T[] arr){
return arr;
}
public static <T> void printArray(T[] arr){
if(arr != null){
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < arr.length; i++) {
sb.append(arr[i]).append(i == arr.length - 1 ? "" : ", ");
}
sb.append("]");
System.out.println(sb);
}else {
System.out.println(arr);
}
}
}
泛型接口
使用了泛型定义的接口就是泛型接口,可以让实现类选择当前功能需要操作的数据类型
示例代码
泛型接口可以约束实现类,实现类可以在实现接口的时候传入自己操作的数据类型,这样重写的方法都是针对该类型的操作
//定义了一个泛型接口
public interface Data<E> {
void add(E e);
void delete(int id);
void update(E e);
E queryById(int id);
}
public class Student {
}
public class StudentData implements Data<Student>{
@Override
public void add(Student student) {
}
@Override
public void delete(int id) {
}
@Override
public void update(Student student) {
}
@Override
public Student queryById(int id) {
return null;
}
}
通配符
?可以在使用泛型
的时候代表一切类型,ETKV实在定义
泛型的时候使用的
案例-开发一个极品飞车的游戏,所有汽车都能一起参加比赛。
示例代码
public class GenericDemo {
public static void main(String[] args) {
ArrayList<BMW> bmws = new ArrayList<>();
bmws.add(new BMW());
bmws.add(new BMW());
bmws.add(new BMW());
go(bmws);
ArrayList<BENZ> benzs = new ArrayList<>();
benzs.add(new BENZ());
benzs.add(new BENZ());
benzs.add(new BENZ());
go(benzs);
ArrayList<Dog> dogs = new ArrayList<>();
dogs.add(new Dog());
dogs.add(new Dog());
dogs.add(new Dog());
// go(dogs);
}
/**
所有车比赛
*/
//虽然BMW和BENZ都继承了Car但是 ArrayList<BENZ>, ArrayList<BENZ>,ArrayList< Car>是没有关系的
//这时BWM和BENZ是不能同时调用go()的
public static void go(ArrayList< Car> cars){
}
//于是出现了以下的解决方法,通配符?,但是此时的写法,定义的Dog也可以调用go()方法参加比赛
public static void go(ArrayList<?> cars){
}
//所以出现以下正确的写法-泛型上限,必须是Car或者子类才能调用go()方法
public static void go(ArrayList<? extends Car> cars){
}
}
class Dog{
}
class BENZ extends Car{
}
class BMW extends Car{
}
// 父类
class Car{
}
? extends Car
: ?必须是Car或者其子类 ,泛型上限
? super Car
: ?必须是Car或者其父类,泛型下限
5.Set(HashSet,LInkedHashSet,TreeSet)
HashSet:
无序,不重复,无索引,LInkedHashSet:有序.
不重复,无索引
TreeSet:
按照大小默认升序排序,不重复,无索引所以不能使用普遍for循环遍历,也不能用索引来获取元素
1、HashSet(底层采取哈希表存储数据)哈希表是增删改查性能较好的结构
JDK8之前,底层使用数组+链表组成
JDK之后,底层采用数组+链表+红黑树组成
2、LInkedHashSet
有序
(HashSet的子类,底层依然是哈希表,只是每个元素又额外的多了一个双链表的机制记录存储的顺序)
3、TreeSet
可排序
(默认从小到大排序,底层基于红黑树的数据结构实现排序的)
示例代码-
自定义比较规则
-如果TreeSet集合存储的对象有实现比较规则,集合也自带比较器,默认使用集合自带的比较器排序
//方式一:让自定义类实现Comparable接口,重写里面的compareTo方法来定制比较规则
public class Apple implements Comparable<Apple>{
private String name;
private String color;
private double price;
private int weight;
public Apple() {
}
public Apple(String name, String color, double price, int weight) {
this.name = name;
this.color = color;
this.price = price;
this.weight = weight;
}
....
....
....
@Override
public int compareTo(Apple o) {
// 按照重量进行比较的
return this.weight - o.weight ; // 去重重量重复的元素
// return this.weight - o.weight >= 0 ? 1 : -1; // 保留重量重复的元素
}
}
}
//方式二:TreeSet集合有参数构造器,可以设置Comparator接口对应的比较器对象来制定比较规则
Set<Apple> apples = new TreeSet<>(new Comparator<Apple>() {
@Override
public int compare(Apple o1, Apple o2) {
// return o1.getWeight() - o2.getWeight(); // 升序
// 注意:浮点型建议直接使用Double.compare进行比较(内部已经写好比较规则了)
return Double.compare(o1.getPrice() , o2.getPrice()); // 升序
}
});
//Lambda省略写法
Set<Apple> apples = new TreeSet<>(( o1, o2) -> Double.compare(o2.getPrice() , o1.getPrice()) );
Collection大总结
6.Collections(集合工具类)
Collections并不属于集合,是用来操作集合的工具类
示例代码
Collections.addAll(list, 12, 23, 2, 4);
Collections.sort(apples, ( o1, o2) -> Double.compare(o1.getPrice() , o2.getPrice()) );
案例-斗地主游戏
示例代码
/
public class Card {
private String size;
private String color;
private int index; // 牌的真正大小
public Card(){
}
public Card(String size, String color, int index) {
this.size = size;
this.color = color;
this.index = index;
}
...
...
@Override
public String toString() {
return size + color;
}
}
/
public class GameDemo {
/**
1、定义一个静态的集合存储54张牌对象
*/
//多态的定义方式
public static List<Card> allCards = new ArrayList<>();
/**
2、做牌:定义静态代码块初始化牌数据
*/
static {
// 3、定义点数:个数确定,类型确定,使用数组
String[] sizes = {"3","4","5","6","7","8","9","10","J","Q","K","A","2"};
// 4、定义花色:个数确定,类型确定,使用数组
String[] colors = {"♠", "♥", "♣", "♦"};
// 5、组合点数和花色
int index = 0; // 记录牌的大小
for (String size : sizes) {
index++;
for (String color : colors) {
// 6、封装成一个牌对象。
Card c = new Card(size, color, index);
// 7、存入到集合容器中去
allCards.add(c);
}
}
// 8 大小王存入到集合对象中去 "👲" , "🃏"
Card c1 = new Card("" , "🃏", ++index);
Card c2 = new Card("" , "👲",++index);
Collections.addAll(allCards , c1 , c2);
System.out.println("新牌:" + allCards);
}
public static void main(String[] args) {
// 9、洗牌
Collections.shuffle(allCards);//可以打乱集合中的顺序
System.out.println("洗牌后:" + allCards);
// 10、发牌(定义三个玩家,每个玩家的牌也是一个集合容器)
List<Card> linhuchong = new ArrayList<>();
List<Card> jiumozhi = new ArrayList<>();
List<Card> renyingying = new ArrayList<>();
// 11、开始发牌(从牌集合中发出51张牌给三个玩家,剩余3张作为底牌)
// allCards = [🃏, A♠, 5♥, 2♠, 2♣, Q♣, 👲, Q♠ ...
// i 0 1 2 3 4 5 6 7 % 3
//经典算法-轮询就求余
for (int i = 0; i < allCards.size() - 3; i++) {
// 先拿到当前牌对象
Card c = allCards.get(i);
if(i % 3 == 0) {
// 请阿冲接牌
linhuchong.add(c);
}else if(i % 3 == 1){
// 请阿鸠
jiumozhi.add(c);
}else if(i % 3 == 2){
// 请盈盈接牌
renyingying.add(c);
}
}
// 12、拿到最后三张底牌(把最后三张牌截取成一个子集合)subList--开始索引-结束索引可以截取一些牌
List<Card> lastThreeCards = allCards.subList(allCards.size() - 3 , allCards.size());
// 13、给玩家的牌排序(从大到小 可以自己先试试怎么实现)
sortCards(linhuchong);
sortCards(jiumozhi);
sortCards(renyingying);
// 14、输出玩家的牌:
System.out.println("啊冲:" + linhuchong);
System.out.println("啊鸠:" + jiumozhi);
System.out.println("盈盈:" + renyingying);
System.out.println("三张底牌:" + lastThreeCards);
}
/**
给牌排序
* @param cards
*/
private static void sortCards(List<Card> cards) {
// cards = [J♥, A♦, 3♥, 🃏, 5♦, Q♥, 2♥
// 调用Collection的sort方法
Collections.sort(cards, new Comparator<Card>() {
@Override
public int compare(Card o1, Card o2) {
// o1 = J♥
// o2 = A♦
// 知道牌的大小,才可以指定规则
return o2.getIndex() - o1.getIndex();
}
});
}
}
7.Map(键值对集合)
实现类:HashMap、LinkedHashMap、TreeMap:无序、有序、排序(对键排序)
(不重复、无索引)
键和值都可以存null
Set系列集合(存键数据,不要值数据)的底层是Map实现的
Map是双列集合的祖宗接口,它的功能是全部双列集合都可以继承使用的
Map集合的遍历方式
1、键找值
先获取Map集合全部的键,再根据遍历键找值
示例代码
// 1、键找值:第一步:先拿到集合的全部键。
Set<String> keys = maps.keySet();
// 2、第二步:遍历每个键,根据键提取值
for (String key : keys) {
int value = maps.get(key);
System.out.println(key + "===>" + value);
}
2、键值对
把“键值对”看成一个整体
示例代码
// 1、把Map集合转换成Set集合
Set<Map.Entry<String, Integer>> entries = maps.entrySet();
// 2、开始遍历
for(Map.Entry<String, Integer> entry : entries){
String key = entry.getKey();
int value = entry.getValue();
System.out.println(key + "====>" + value);
}
3、Lambda表达式
(JDK1.8开始之后的新技术)
示例代码
//内部帮助了遍历
maps.forEach(new BiConsumer<String, Integer>() {
@Override
public void accept(String key, Integer value) {
System.out.println(key + "--->" + value);
}
});
maps.forEach((k, v) -> {
System.out.println(k + "--->" + v);
});
案例-统计投票人数
示例代码
public class MapTest1 {
public static void main(String[] args) {
// 1、把80个学生选择的数据拿进来。
String[] selects = {"A" , "B", "C", "D"};
StringBuilder sb = new StringBuilder();
Random r = new Random();
for (int i = 0; i < 80; i++) {
sb.append(selects[r.nextInt(selects.length)]);
}
System.out.println(sb);
// 2、定义一个Map集合记录最终统计的结果: A=30 B=20 C=20 D=10 键是景点 值是选择的数量
Map<Character, Integer> infos = new HashMap<>(); //
// 3、遍历80个学生选择的数据
for (int i = 0; i < sb.length(); i++) {
// 4、提取当前选择景点字符
char ch = sb.charAt(i);
// 5、判断Map集合中是否存在这个键
if(infos.containsKey(ch)){
// 让其值 + 1
infos.put(ch , infos.get(ch) + 1);
}else {
// 说明此景点是第一次被选
infos.put(ch , 1);
}
}
// 4、输出集合
System.out.println(infos);
}
}
8.集合嵌套
案例-统计投票人数
示例代码
public class MapTest4 {
public static void main(String[] args) {
// 1、要求程序记录每个学生选择的情况。
// 使用一个Map集合存储。
Map<String, List<String>> data = new HashMap<>();
// 2、把学生选择的数据存入进去。
List<String> selects = new ArrayList<>();
Collections.addAll(selects, "A", "C");
data.put("罗勇", selects);
List<String> selects1 = new ArrayList<>();
Collections.addAll(selects1, "B", "C" , "D");
data.put("胡涛", selects1);
List<String> selects2 = new ArrayList<>();
Collections.addAll(selects2 , "A", "B", "C" , "D");
data.put("刘军", selects2);
System.out.println(data);
// 3、统计每个景点选择的人数。
Map<String, Integer> infos = new HashMap<>(); // {}
// 4、提取所有人选择的景点的信息。
Collection<List<String>> values = data.values();
System.out.println(values);
// values = [[A, B, C, D], [B, C, D], [A, C]]
// value
for (List<String> value : values) {
for (String s : value) {
// 有没有包含这个景点
if(infos.containsKey(s)){
infos.put(s, infos.get(s) + 1);
}else {
infos.put(s , 1);
}
}
}
System.out.println(infos);
}
}
四、不可变集合、Stream、异常
1.不可变集合
不重复、不修改
集合的数据项在创建的时候提供,并且在整个生命周期中都不可改变,否则报错
2.Stream
简化集合、数组操作的API。结合了Lambda表达式
示例代码-集合获取流的方法
/** --------------------Collection集合获取流------------------------------- */
Collection<String> list = new ArrayList<>();
Stream<String> s = list.stream();
/** --------------------Map集合获取流------------------------------- */
Map<String, Integer> maps = new HashMap<>();
// 键流
Stream<String> keyStream = maps.keySet().stream();
// 值流
Stream<Integer> valueStream = maps.values().stream();
// 键值对流(拿整体)
Stream<Map.Entry<String,Integer>> keyAndValueStream = maps.entrySet().stream();
/** ---------------------数组获取流------------------------------ */
String[] names = {"赵敏","小昭","灭绝","周芷若"};
Stream<String> nameStream = Arrays.stream(names);
Stream<String> nameStream2 = Stream.of(names);
终结流调用后无法继续使用了,原因是不会返回Stream,中间方法可以继续使用,支持链式编程
案例
示例代码
//员工类
public class Employee {
private String name;
private char sex;
private double salary;
private double bonus;
private String punish; // 处罚信息
...
...
...
}
//优秀员工类
public class Topperformer {
private String name;
private double money; // 月薪
}
public class StreamDemo04 {
public static double allMoney ;//共享变量属于类,来辅助求和操作
public static double allMoney2 ; // 2个部门去掉最高工资,最低工资的总和
public static void main(String[] args) {
List<Employee> one = new ArrayList<>();
one.add(new Employee("猪八戒",'男',30000 , 25000, null));
one.add(new Employee("孙悟空",'男',25000 , 1000, "顶撞上司"));
one.add(new Employee("沙僧",'男',20000 , 20000, null));
one.add(new Employee("小白龙",'男',20000 , 25000, null));
List<Employee> two = new ArrayList<>();
two.add(new Employee("武松",'男',15000 , 9000, null));
two.add(new Employee("李逵",'男',20000 , 10000, null));
two.add(new Employee("西门庆",'男',50000 , 100000, "被打"));
two.add(new Employee("潘金莲",'女',3500 , 1000, "被打"));
two.add(new Employee("武大郎",'女',20000 , 0, "下毒"));
// 1、开发一部的最高工资的员工。(API)
// 指定大小规则了
// Employee e = one.stream().max((e1, e2) -> Double.compare(e1.getSalary() + e1.getBonus(), e2.getSalary() + e2.getBonus()))
// .get();
// System.out.println(e);
//找出优秀员工以后封装成一个优秀员工
Topperformer t = one.stream().max((e1, e2) -> Double.compare(e1.getSalary() + e1.getBonus(), e2.getSalary() + e2.getBonus()))
.map(e -> new Topperformer(e.getName(), e.getSalary() + e.getBonus())).get();
System.out.println(t);
// 2、统计平均工资,去掉最高工资和最低工资
one.stream().sorted((e1, e2) -> Double.compare(e1.getSalary() + e1.getBonus(), e2.getSalary() + e2.getBonus()))
.skip(1).limit(one.size() - 2).forEach(e -> {
//先排序,跳过第一个,截取前two.size() - 2的位置,刚好就把最低、最高工资去掉了
// 求出总和:剩余员工的工资总和
allMoney += (e.getSalary() + e.getBonus());
});
System.out.println("开发一部的平均工资是:" + allMoney / (one.size() - 2));
// 3、合并2个集合流,再统计
Stream<Employee> s1 = one.stream();
Stream<Employee> s2 = two.stream();
Stream<Employee> s3 = Stream.concat(s1 , s2);
s3.sorted((e1, e2) -> Double.compare(e1.getSalary() + e1.getBonus(), e2.getSalary() + e2.getBonus()))
.skip(1).limit(one.size() + two.size() - 2).forEach(e -> {
// 求出总和:剩余员工的工资总和
allMoney2 += (e.getSalary() + e.getBonus());
});
// BigDecimal-可以解决精度问题
BigDecimal a = BigDecimal.valueOf(allMoney2);//封装起来
BigDecimal b = BigDecimal.valueOf(one.size() + two.size() - 2);
System.out.println("开发部的平均工资是:" + a.divide(b,2, RoundingMode.HALF_UP));//四舍五入进行留位
}
}
Stream流的收集:
把操作后的结果数据转回到集合或者数组中去
Stream流只是方便操作集合、数组的手段
,集合、数组才是开发中的目的
示例代码
public class StreamDemo05 {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("张无忌");
list.add("周芷若");
list.add("赵敏");
list.add("张强");
list.add("张三丰");
list.add("张三丰");
Stream<String> s1 = list.stream().filter(s -> s.startsWith("张"));
// 收集到可变集合中去 //遍历六中的数据封装到ArrayList中去再赋值给List
List<String> zhangList = s1.collect(Collectors.toList());
zhangList.add("java1");
System.out.println(zhangList);
// 得到不可变集合
// toList();是JDK16开始的API
// List<String> list1 = s1.toList();
// list1.add("java");
// System.out.println(list1);
// 注意注意注意:“流只能使用一次”
//拿一个新流
Stream<String> s2 = list.stream().filter(s -> s.startsWith("张"));
Set<String> zhangSet = s2.collect(Collectors.toSet());
System.out.println(zhangSet);
//收集到数组中
Stream<String> s3 = list.stream().filter(s -> s.startsWith("张"));
// Object[] arrs = s3.toArray();//流中可能有其它类型,不光是字符串类型,所以用Object
//申明一个对象告诉电脑我就是字符串类型
// String[] arrs = s3.toArray(new IntFunction<String[]>() {
// @Override
// public String[] apply(int value) {
// return new String[value];//
// }
// });
// 简化
String[] arrs = s3.toArray(s-> new String[s]);
// 简化
// String[] arrs = s3.toArray(String[]::new);
System.out.println("Arrays数组内容:" + Arrays.toString(arrs));
}
}
3.异常
异常是程序在“编译”或者“执行”的过程中可能出现的问题
语法错误不算在异常体系中
异常一旦出现了,如果没有提前处理,程序就会退出JVM虚拟机而终止
研究、处理异常,体现的是程序的安全、健壮性
异常默认的处理流程
编译时异常的处理机制
异常处理的第三种,方法直接将异常通过throws抛出去给调用者,调用者收到异常后直接捕获异常
运行时异常的处理机制
不需主动抛异常
运行时异常编译阶段不会出错,是运行时才可能出错,所以编译阶段不处理也可以。按规范,再外层调用处集中捕获处理即可。
案例
示例代码
public class Test2 {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
while (true) {
try {//实际开发中也可以使用正则表达式来校验输入是否合法
System.out.println("请您输入合法的价格:");
String priceStr = sc.nextLine();//接收一行的数据
// 转换成double类型的价格
double price = Double.valueOf(priceStr);
// 判断价格是否大于 0
if(price > 0) {
System.out.println("定价:" + price);
break;
}else {
System.out.println("价格必须是正数~~~");
}
} catch (Exception e) {
System.out.println("用户输入的数据有毛病,请您输入合法的数值,建议为正数~~");
}
}
}
}
自定义异常类
Java无法为所有的问题提供异常类,若开发中需要,则需自定义异常类
自定义异常类,可以使用异常的机制管理业务问题,同时一旦出现bug,可以用异常的形式清晰的指出出错的地方
示例代码
/**
自定义的编译时异常
1、继承Exception
2、重写构造器
*/
public class ItheimaAgeIlleagalException extends Exception{
public ItheimaAgeIlleagalException() {
}
public ItheimaAgeIlleagalException(String message) {
super(message);
}
}
/**
自定义的运行时异常
1、继承RuntimeException
2、重写构造器
*/
public class ItheimaAgeIlleagalRuntimeException extends RuntimeException{
public ItheimaAgeIlleagalRuntimeException() {
}
public ItheimaAgeIlleagalRuntimeException(String message) {
super(message);
}
}
总结
以上就是在网上学习java做的笔记,用于以后的巩固复习,若能帮助到网络上的同行者就更好啦!