自己学习JAVAse的笔记
JAVA炒面炒米粉
初级
public class Main{}
psvm,sout,sout打出来println把ln删掉就不会换行了
1键盘录入
Scanner sc=new Scanner(System.in);
int a=sc.nextInt();
键盘录入nextInt()Double 和next遇到空格制表符就停止接收,没有接收的数据留到下一次键盘录入,nextLine可以接收
两套体系不能混用
学习包装类后,以后键盘录入都用nextLine 然后再用Integer.parseInt(line)转换
2运算符
加的过程有字符串,拼接,单字符按码表值,++a,先加后用,a++先用后加
swap(a,b)
静态初始化 int[] arr={} 动态初始化(只给了长度)int[]arr=new int[5]
3数组
数组索引sout(arr[i])
数组遍历arr.length为总数组长度
快速遍历数组 数组名.fori
4循环
一个循环尽量只做一件事 while(true)无限循环
while循环知道停止条件,for知道次数和范围
foreach增强for循环for(数据类型 名字:数组名){方法体}
java生成随机数Random random =new Random();
int num=random.nextInt(最大值-最小值+1)+最小值
5方法
最小的程序运行单元 格式为pvs+方法名 带参数的方法在小括号中只定义不赋值 带返回值的方法格式:ps 返回值类型 方法名(参数)最后一行加个return 返回值
带返回值的方法需赋值调用,不能直接调用
形参:方法定义的参数 实参:方法调用的参数
方法重载:相同功能的方法可以同名但不能参数相同
调用方法的时候如果出现方法重载,优先调用实参和形参类型一致的那个方法
方法引用通常用于简化函数式接口的实现
1简化lambda表达式,当Lambda表达式仅仅是调用一个已有的方法时,可以用方法引用来替代
// Lambda表达式 list.forEach(s -> System.out.println(s)); // 方法引用 list.forEach(System.out::println);
return和break的区别:return结束方法,break结束循环
定义方法:我要干嘛,需要什么才能完成,需不需要返回值
强转换(int)(需要转换的数值)
IDEA中自动抽取方法ctrl+alt+m,把一段具有代表性的代码提取为通用的方法
变量的批量修改shift+f6 CTRL+alt+l格式化代码
6 类和对象
类:对象共同特征的描述 对象:真实存在的具体东西,先设计类再有对象
成员变量(属性,名词) 成员方法(行为,动词)成员方法不用s 只用p+数据类型+名字
得到类的对象: 类名 对象名 =new 类名(); 使用对象 对象.成员变量 对象.成员方法
用来描述一类事物的类:JavaBean类 不写 main方法 写main方法的类 叫测试类 可以在测试类中创建Javabean类的对象并进行赋值调用
封装:对象代表什么,就封装对应数据,并提供对应行为, 人画园,画在园内
private 成员变量私有,只能在本类中访问 被私有化的成员变量要提供set和get方法 set赋值,get调用 把成员变量的地址值换成成员变量
私有化可以过渡一下数据,对数据区分防止非法数据
在方法外,类里面的变量为成员变量,在方法内的为局部变量,成员变量与局部变量名字相同时,就近调用
this.调用成员变量 构造方法,在创建对象的时候给成员变量初始化,格式:修饰符public 类名(参数){方法体},方法名与类名相同,没有返回值
构造方法手动写上空参和带全部参数的构造方法
快捷键alt+insert,选无选择,生成一个空参,按住shift选最下面的,生成带所有参数的
标准JavaBean类:属性+alt insert空参加全部参数加set和get,先写属性,右键ptg可生成全部代码
API帮助文档,查找,索引
7字符串
String,构造:1直接2需要改变某一条件 char[]chs={}
String s =new String(chs) 或者byte
比较字符串 ==号比基础数据类型(整浮字布),比具体值,比引用数据类型,比地址值
boolean s=s1.equals(s2) equalslgnoreCase()不区分大小写
Ctrl+alt+v自动生成左边,sc.next()的左边,字符串遍历str.length().fori
索引str.charAt(i) char类型变量计算时变为int 查ASCII码表
c>=‘a’&&c<=‘z’ 代表小写
把一个字符类型的数字转换成数字需要减零 -’0’ 字符0的as表值为48 1为
49,一减就是1
字符串的拼接和反转需要用到StringBuilder
StringBuilder sb=new StringBuilder(String str)快速拼接字符串,运行效率高
sb.append(1)添加元素sb.reverse反转元素sb.length()获取长度 变为字符串sb.toString 链式编程sb.append(1).append(2)
Stringjoiner拼接字符串
Stringjoiner sj =new Stringjoiner(间隔符号,起始符号,结束符号) 添加元素用sj.add 总长度为sj.length
alt 加回车,拆分为声明和赋值,如果写了方法名,可以alt加回车直接创建
CTRL+alt+t 可以用循环把一段代码包起来
8集合,泛型
集合:ArrayList 自动扩容,不能存储基本数据类型 创建ArrayList<String> list=new ArrayList<>();
泛型:限定集合中存储数据的类型 集合存储基本数据类型时要进行包装类 char->Character int->Integer其他的把首字母大写
成员方法添加元素List.add() 删除元素List.remove() 删除的括号里输入数字可以删除索引对应的元素
修改List.set(索引,修改后的字符串) 查询List.get(索引)
List<String> names = Arrays.asList("Alice", "Bob", "Charlie");
直接用as把数据存起来
不用set一个一个加了
遍历List.fori List.get(i)表示遍历的元素 添加对象时,遍历的结束条件不能写集合长度,因为集合长度会变化
如果要返回多个数据,可以把数据先存到集合,数组中,再一起返回
System.exit(0)停止虚拟机运行
CopyOfRange拷贝数组时,包头不包尾,包左不包右
Sort默认使用快速排序,升序,需要降序使用第二种
第二种sort只能给引用数据类型的数组排序,基本数据类型需要包装类
*Lambda表达式*
能够简化匿名内部类,把上面的代码从new到compare删掉再加一个switch里case的箭头
Arrays.sort(arr,(Integer o1,Integer o2)->{
return o1- o2;
}
);
函数式编程,强调做什么,而不是谁去做,强调方法体的逻辑
Jdk8以后的新语法
( ) ->{
}
( )对应方法的形参,->固定格式 {方法体}
Lambda表达式的进一步省略
Collection单列集合(每次存储一个数据)
Map 双列集合(每次存储两个数据)
有序:放进去和拿出来的顺序一样,不是从小到大那种顺序
重写equals方法:alt+insert可以直接生成,重写之后的equals方法,比较的是属性值就不是地址值了
Collection的遍历方式
*1迭代器(Iterator)遍历*: 不依赖索引
指针到数组外面,不能获取元素会报错
4只能用迭代器的方法 it.remove
*2 增强for遍历*
数组名.for(快捷键)
修改增强for中的变量,不能改变集合中原数据 中间的是第三方变量
*3 lambda表达式遍历*
forEach
*4 list集合*
List集合:有序,有索引,可重复
Add插入元素后,插入后面的元素会往后移
第二种遍历方式能够在遍历中添加元素
第一种迭代器可以删除元素
遍历时想要操作索引,可以使用普通for
5, ArrayList集合的分析
6, LinkedList集合
ArrayList和LinkedList的源码分析(较难,以后再看)
在使用迭代器或者增强for遍历集合的过程中,不要用集合的方法去添加或删除元素
7, 泛型深入
泛型可以定义在很多地方,类后面(泛型类),方法上面(泛型方法),接口后面(泛型接口大佬编写源码时使用
*Set系列集合*
*HashSet*
*linkedHashSet*
有序,不重复,无索引,存储和取出的元素顺序相同
底层数据结构依然是哈希表,只是每个元素又额外多了个双链表的机制记录存储顺序
*TreeSet*
不重复,无索引,可排序,底层是基于红黑树的数据结构来实现排序的
Treeset集合添加数据时排序的规则(默认排序,JavaBean类实现Comparable接口指定比较规则)
比较器排序
总结
集合的使用选择
9static
静态变量:被static修饰的成员变量,1被该类所有对象共享,2不属于对象属于类,3随类的加载而加载,优先于对象存在
静态方法多用于测试类,没有this关键字,只能访问静态,不能调用成员方法
静态的东西用类名调用,public static String teacherName 类名->Student.teacherName
工具类->用来做事,但不描述事物,1类名见名知意,2私有化构造方法,3方法定义为静态
正则表达式
作用:校验字符串是否满足规则,在一段文本中查找满足要求的内容
“需要比较的字符串”.matches(正则表达式的规则)
注意注意注意,其他语言中\表示一个\ 没有特殊意义
Java中\表示插入一个正则表达式的反斜杠然后后面的字符有特殊意义
API帮助文档中搜pattern可以找到所有正则表达式的规则
正则表达式书写时拿着一个正确的数字从左到右去看,按位数来
例:手机号码:””.matches(“1[3-9]\d{9}”)
1表示手机号码只能以1开头
[3-9]第二位只能是3-9之间的
\d{9}任意数字可以出现九次,也只能是九次
例:\d{2,3}任意数字可以出现2-3次,大括号是出现的次数
\w+任意字母数字下划线至少出现一次,+是至少出现一次
[\w&&_]任意字母数字去掉下划线,^去除的意思
\.添加一个点
小括号可以分成把字符一组,再选择组出现的次数
(内容){}
A(?i)bc 问号i后面的东西匹配的时候忽略大小写
Any-rule插件把^(从开头开始匹配)和$(一直匹配到末尾)删掉然后把\补上
*正则表达式第二种作用(爬虫)*
在一段文本中查找满足要求的内容
import java.util.regex.Matcher; import java.util.regex.Pattern; public class RegexExample { public static void main(String[] args) { // 定义正则表达式 String regex = "正则表达式"; // 替换为实际的正则表达式 // 定义待匹配的大串 String str = "待匹配的大串"; // 替换为实际的字符串 // 获取正则表达式的对象 Pattern p = Pattern.compile(regex); // 获取文本匹配器的对象 Matcher m = p.matcher(str); // 拿着文本匹配器从头开始读取,寻找是否有满足规则的子串 boolean b = m.find(); // 如果有匹配的子串,返回true,并在底层记录子串的起始索引和结束索引+1 if (b) { // 方法底层会根据find方法记录的索引进行字符串的截取 // subString(起始索引,结束索引);包头不包尾 String s1 = m.group(); System.out.println(s1); // 输出匹配的子串 } else { System.out.println("没有找到匹配的子串"); } } }
标准的三步
// 定义正则表达式 String regex = "你的正则表达式"; // 替换为实际的正则表达式 // 定义待匹配的字符串 String s = "待匹配的字符串"; // 替换为实际的字符串 // 1. 获取正则表达式的对象 Pattern p = Pattern.compile(regex); // 2. 获取文本匹配器的对象 // 利用m去读取s,会按照p的规则找里面的小串 Matcher m = p.matcher(s); // 3. 利用循环获取每一个匹配的数据 while (m.find()) { String str = m.group(); // 获取匹配的子串 System.out.println(str); // 打印匹配的子串 }
*贪婪爬取和非贪婪爬取*
Ab+在爬取的时候尽可能多的爬取b,非贪婪爬取加?
*分组*
组号:以左括号为基准,一个一个分 123为第一组,23为第二组,3为第三组
(123(23))(3)
捕获分组
\组号n,把第n组的数据拿出来再用一次
如果在正则表达式外也要用到捕获分组里的数据可以用 $组号来使用
非捕获分组
面向对象
10继承
:extends 把子类重复的代码抽取到父类 public class 子类 extends 父类 { }
子类可以得到父类的属性,行为 ,可以增加新功能,Java中只能单继承,不能多继承,但是可以多层继承 所有的类都直接或间接继承于Object类
子类只能访问父类中非私有的成员 构造方法不能被继承 非私有的成员变量和成员方法可以被继承 private修饰的成员变量能继承但是不能用
内存方面的问题,栈,与方法有关 方法运行进栈 运行完出栈 。堆,与new关键字有关 创建对象在堆区。 方法区,与类的字节码文件有关
方法重写,当父类的方法不满足子类的需求时,需要进行方法重写,吃饭(父类)-吃剩饭(子类),重写的方法尽量与父类保持一致,方法上面加@Oberride,检查语法错误,this调用,就近原则,super调用,直接调用父类,只有加载到虚方法表(非p,s,f)中的方法可以被重写
this,super的理解:this是一个变量,表示方法调用者的地址值,this常用于区分成员变量和局部变量,super代表父类的储存空间,this.访问成员方法,super.访问父类的成员方法,访问构造方法加个括号,必须写在第一行
构造方法和成员方法的区别
11多态
:同类型的变量 不同形态,工作->敲代码 or 搬砖
多态表现形式:父类的类型 对象名 =子类对象
多态的前提:有继承关系,有父类引用指向子类对象,有方法重写
多态调用成员变量的特点:编译看左边,运行看左边(1javac编译代码时,会看左边父类中有没有这个变量,如果有编译成功,没有则失败,2Java运行代码的时候,实际获取的是左边父类中的成员变量的值)调用成员方法的特点:编译看左边,运行看右边(1看左边父类中有没有这个方法,有则编译成功,2运行子类中的方法)继承时子类的方法加载到虚方法表中被重写,被重写的子类会覆盖父类
多态的优势和弊端:
优势:使用父类作为参数,可以接收所有的子类对象
弊端:不能调用子类的特有功能,因为编译看左边,先检查父类中有没有方法
Animal a=new Dog;,a.不能调用狗的特有功能,可以通过Dog d=(Dog)a;来把狗转换回子类,但是不能转换成其他的类,可以通过if循环instanceof关键字来判断自己有没有转错,if(对象名 instanceof 类),jdk14后新特性,可以把判断和强转写在同一行,if(对象名 instanceof 类名 要转换成的对象名)
包:文件夹,可以存储不同的Java类
Final:可以修饰方法(不能被重写),类(不能被继承),变量(变为常量,只能被赋值一次)基本数据类型的值不能被修改,引用数据类型的地址值不能修改,内部的属性值可以修改
CTRL+shift+u把一键小写换为大写
权限修饰符,成员变量私有,方法公开
代码块:局部代码块,构造代码块,静态代码块,代码块就是大括号内的东西,静态代码块可以用来初始化数据,并只能运行一次,比如学生管理系统中先添加登入的信息
12抽象
抽象类:含抽象方法的类,抽象类的作用:抽取共性时,无法确定方法体,就把方法定义为抽象的,强制让子类按照某种格式重写
抽象方法:格式:public abstract返回值类型 方法名(参数) 只有方法的定义,没有具体的实现,也没有方法体,定义一个吃东西,但是不同的动物吃的东西不同,需要用子类实现,抽象方法为多态提供了基础,多态通过方法重写来实现抽象方法
注意事项:1抽象类不能创建对象(实例化)2抽象类的子类需要重写所有的抽象方法,或者是抽象类
抽象方法和多态的区别与联系
13接口
接口:interface,接口是一个玩具说明书,告诉一个东西怎么用,只定义规则,不实现功能,接口定义:public interface 接口名{} 接口不能实例化 ,类和接口是实现关系,通过implements(实施) public class 类名 implements 接口名{}
接口的子类(实现类)要么重写接口的所有抽象方法,要么是抽象类
1一个类可以实现多个接口public class 类名 implements 接口名1,接口名2{}
2实现类可以继承一个类同时实现多个接口public class 类名extends 父类 implements 接口名1,接口名2{}
接口中成员的特点:成员变量:只能是常量,默认修饰符public static final
构造方法:没有
成员方法:抽象方法,默认修饰符:public abstract,之前只能有抽象方法默认方法在jdk8以后新增,格式:public default 返回值类型 方法名(参数){}; 默认方法不是抽象方法,所以不强制被重写,多个接口中相同名字的默认方法需要重写,jdk8后新增的静态方法,public static 返回值类型 方法名(参数){} 静态方法只能通过接口名调用
Jkd9新增私有方法,和静态的私有方法,private 返回值类型 方法名 (){}
静态的私有方法private static 返回值类型 方法名,静态方法才能调用静态的私有方法
接口和类的关系:类与类的关系:继承关系,只能单继承不能多继承,但是可以多层继承
类与接口的关系:实现关系,可以单实现,多实现,可以继承一个类再多实现
接口与接口的关系:继承关系,可以单继承和多继承
当一个方法的参数是接口时,可以传递接口所有实现类的对象,称为接口的多态
Public void 搬家(运输的接口 c){} 搬家(车的对象) 搬家(搬家公司)
适配器设计模式: 一个接口中抽象方法过多,但是我们只要使用其中一部分的时候,需要用适配器设计模式
书写步骤:1先编写一个中间类 XXXAdapter 实现对应的接口 2对接口中的抽象方法进行空实现 就是没有方法体 ,3让真正的实现类(需要用的方法)继承中间类,然后重写需要的方法,为了避免其他类创建适配器类的对象,需要用abstract修饰
14内部类
内部类:写在类的内部,比如写一个汽车的JavaBean类需要把汽车发动机写成内部类,
什么时候用到:一类事物是另一类事物的一部分,但是它单独存在没有意义
内部类可以直接访问外部类的成员,包括私有,外部类访问内部类时需要创建对象
*不重要的内部类*
*成员内部类*:写在成员位置的,属于外部类的成员,获取内部类对象的方式:1当成员内部类被private修饰,在外部类编写方法,对外提供内部类的对象,2非私有可以直接创建对象:Outer.Inner oi=new Outer().new Inner();
外部类.内部类 变量名 =new 外部类().new 内部类();
1的解释 在外部类写public Inner getInstance(){
return new Inner();
} 然后在外部创建对象 进行调用
*静态内部类*:一种特殊的成员内部类,创建方式:Outer.Inner oi=new Outer.Inner();
如何调用静态内部类的方法,非静态:先创建对象,用对象调用,静态方法:外部类名.内部类名.方法名();
*局部内部类:*将内部类定义在方法里面,类似于方法里面的局部变量,外界无法直接使用,需要在方法内部创建对象并使用,可以直接访问外部类的成员,也可以访问方法内的局部变量
*匿名内部类(重要)*1需要继承/实现关系2重写方法3创建对象
new 接口名/父类名(){
重写的方法
};
当你只需要用某个类一次,不想为它专门写一个完整的类时,用来简化代码
New的不是接口,是接口后面没有名字的对象,叫做实现类对象
15异常
程序执行过程中发生的非正常事件
try {
// 可能抛出异常的代码,调用下一级的方法
} catch (ExceptionType e) {
// 处理异常的代码,捉住下一级方法发出的异常(必须同类型)
} finally {
// 清理资源的代码,无论是否异常都会执行
}
异常继承关系
Catch捕捉异常的时候看看捕捉的异常能不能向上转型成他的父类
实际抛出的异常和catch捕捉的异常符合关系就行
Catch后面常用exception,不用throw able是为了排除error(比较严重的异常)
处理异常的两种方式:写try catch语句,throw抛出给上级
16Io流
输出流的操作是“写”,即程序向外部传输数据
输入流的操作是“读”,即程序从外部获取数据
文件字节流
utf-8中一个英文一个字节,一个中文三个字节
文件字节输入流:FileInputStream
正常的文件输入流,需要有个close关闭,如果想方便的写出来
就使用try后面写到()里面FileInputStream inputStream =new FileInputStream(“路径”)
这种写法叫做try-with-resource,需要这个类实现AutoCloseablie接口可以扒一下父类的源码
路劲只能是纯文本,有一些常用方法,read()读取路径里的字符然后交给一个int类型的值,如果没有值会为-1,需要用char强转,skip可以跳过几个字符进行操作,available是文件的长度
文件输出流:FileOutputStream
Write方法可以写入数据,有这些形式:write(‘c’),write(‘lbwnb’.getBytes()),write(‘c’.getBytes,0,1)最后一种形式是从0开始添加,添加一个字符
文件的拷贝
public class test { public static void main(String[] args) { try(FileInputStream fis = new FileInputStream("test.txt"); FileOutputStream fos = new FileOutputStream("test2.txt")) { byte[] buf = new byte[1024]; int len; while ((len = fis.read(buf)) != -1) { fos.write(buf, 0, len); } } catch (IOException e) { e.printStackTrace(); } } }
byte[] bytes = new byte[1024];
解释: 创建一个大小为 1024 字节的字节数组 bytes,用于存储每次从文件中读取的数据。
作用: 作为缓冲区,用于临时存储读取的文件内容。为了提高运输效率,一次读取1024个字节,一个字节byte是1024分之一kb
文件字符流
FileReader相当于FileInputStream,不过是以字符为单位,纯文本文件用字符流更方便
FileWriter相当于FileOutputStream,一些方法同上面的字节流
数据拷贝
try (FileReader reader = new FileReader("test.txt"); FileWriter writer = new FileWriter("test2.txt")) { // 定义字符数组缓冲区 char[] buf = new char[3]; int len; // 循环读取文件内容并写入目标文件 while ((len = reader.read(buf)) != -1) { writer.write(buf, 0, len); } } catch (IOException e) { e.printStackTrace(); // 捕获并打印异常 }
文件对象
File file=new File(路径);
file.mkdir创建文件夹 这些方法最后会返回一个布尔
File file = new File("test.txt"); //直接创建文件对象,可以是相对路径,也可以是绝对路径 file.exists(); //此文件是否存在 file.length(); //获取文件的大小 字节数
file.isDirectory(); //是否为一个文件夹 file.canRead(); //是否可读
file.canWrite(); //是否可写 file.canExecute(); //是否可执行
文件夹的操作 File file = new File("/");Arrays.toString(file.list()); //快速获取文件夹下的文件名称列表for (File f : file.listFiles()){ //所有子文件的File对象
f.getAbsolutePath(); //获取文件的绝对路径}
加进度的拷贝
import java.io.*; public class Main { public static void main(String[] args) { // 创建源文件对象 File file = new File("channel.next.mod"); // 使用 try-with-resources 确保流自动关闭 try (FileInputStream in = new FileInputStream(file); FileOutputStream out = new FileOutputStream("xxx.mov")) { // 定义缓冲区大小(1MB) byte[] bytes = new byte[1024 * 1024]; int len; long total = file.length(), sum = 0; // 读取文件并写入目标文件 while ((len = in.read(bytes)) != -1) { out.write(bytes, 0, len); sum += len; // 打印拷贝进度 System.out.println("文件已拷贝:" + (sum * 100 / total) + "%"); } } catch (IOException e) { e.printStackTrace(); // 捕获并打印异常 } } }
缓冲流
要创建一个缓冲字节流,只需要将原本的流作为构造参数传入BufferedInputStream即可 BufferedInputStream bufferedInputStream = new BufferedInputStream(new FileInputStream("test.txt"),size缓冲区大小默认8100多)//传入FileInputStream
System.out.println((char) bufferedInputStream.read()); //操作和原来的流是一样的
stream.mark(读取限制数量)标记一下,会保留之后读取的括号里的数量,超过之后不会保留,当调用reset时,会回到mark调用的位置,如果括号里面填0,使用默认缓冲区大小,8100多
缓冲字符流,BufferedReader stream =new BufferedReader(new FileReader(路径))
stream.readline(,'\n')直接读取一行,readLine()读取数据遇到换行符才会停止,读取的时候必须加一个换行符
转换流
转换流用于在字节流和字符流之间进行转换
InputStreamReader:将字节输入流(InputStream)转换为字符输入流(Reader)。 OutputStreamWriter:将字符输出流(Writer)转换为字节输出流(OutputStream)
字节流(如 OutputStream)只能处理字节数据,而字符流(如 Writer)可以处理字符数据。 在网络编程中,通常需要发送字符数据(如字符串),因此需要使用 OutputStreamWriter 将字符流转换为字节流。
字符编码 OutputStreamWriter 可以指定字符编码(如 UTF-8、GBK 等),以确保字符数据能够正确转换为字节数据。
OutputStreamWriter writer = new OutputStreamWriter(stream, "UTF-8");
打印流
普通的sout打印到控制台,可以打印到文件
try (PrintStream stream = new PrintStream(new FileOutputStream("test.txt"))) { stream.println("1wlnb"); // 写入文本 "1wlnb" // 其实 System.out 就是一个 PrintStream System.out.println("这段文本将打印到控制台"); } catch (IOException e) { e.printStackTrace(); // 捕获并打印 IOException }
内部机制
数据和对象流
数据流dataInputStream专门用来处理基本数据类型,用的不多
对象流:ObjectOutputStream,能够读取和写入对象,通过序列化操作,以某种格式保存对象,集合都实现了Serializable(可序列化)的接口,自己写的对象需要手动实现
import java.io.*; public class Main { public static void main(String[] args) { // 使用 try-with-resources 确保流自动关闭 try (ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("output.txt")); ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("output.txt"))) { // 创建一个 People 对象 People people = new People("lbW"); // 将对象序列化并写入文件 outputStream.writeObject(people); // 从文件中反序列化对象 People deserializedPeople = (People) inputStream.readObject(); // 打印反序列化后的对象属性 System.out.println(deserializedPeople.name); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); // 捕获并打印异常 } } // 定义 People 类,实现 Serializable 接口 static class People implements Serializable { String name; // 构造方法 public People(String name) { this.name = name; } } }
在序列化之后想要更改自己写的对象的数据,版本更新一下,支持序列化操作的都有
private static final long serialVersionUID=123456
如果暂时不需要数据被序列化保存,可以添加transient
17多线程
线程的创建和启动
Thread thread = new Thread(new Runnable() { @Override public void run() { System.out.println("Hello World"); } }) ; 可以替换为lambda Thread thread = new Thread(() -> System.out.println("Hello World")) ; 线程启动通过start()方法调用 Thread.start(); public class test { public static void main(String[] args) { Thread thread1 = new Thread(() -> { for (int i = 0; i < 50; i++) { System.out.println("Thread1 " + i); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 50; i++) { System.out.println("Thread2 " + i); } }); thread1.start(); thread2.start(); } }两个线程是同时进行的
线程的休眠和中断
sleep方法休眠 Thread.sleep(毫秒) 每一个Thread对象中,都有一个interrupt()方法,调用此方法后,会给指定线程添加一个中断标记以告知线程需要立即停止运行或是进行其他操作,由线程来响应此中断并进行相应的处理,我们前面提到的stop()方法是强制终止线程,这样的做法虽然简单粗暴,但是很有可能导致资源不能完全释放,而类似这样的发送通知来告知线程需要中断,让线程自行处理后续,会更加合理一些
public static void main(String[] args) { Thread t = new Thread(() -> { System.out.println("线程开始运行!"); while (true) { if (Thread.currentThread().isInterrupted()) { // 判断是否存在中断标志 System.out.println("发现中断信号,复位,继续运行..."); Thread.interrupted(); // 复位中断标记(返回值是当前是否有中断标记,这里不用管) } } }); t.start(); try { Thread.sleep(3000); // 休眠3秒,一定比线程t先醒来 t.interrupt(); // 调用t的interrupt方法 } catch (InterruptedException e) { e.printStackTrace(); } }
线程优先级
Thread.setPriority()
MIN_PRIORITY 最低优先级
MAX_PRIORITY 最高优先级
NOM_PRIORITY 常规优先级
线程的礼让和加入
我们还可以在当前线程的工作不重要时,将CPU资源让位给其他线程,通过使用yield()方法来将当前资源让位给其他同优先级线程
在让位之后,尽可能多的在执行线程2的内容。
当我们希望一个线程等待另一个线程执行完成后再继续进行,我们可以使用join()方法来实现线程的加入
线程锁和线程同步
锁用来保证多个线程不会同时操作同一份数据,避免出错。
通过synchronized关键字来创造一个线程锁,synchronized(){}代码块它需要在括号中填入一个内容,必须是一个对象或是一个类,synchronized相当于一个vip卡,谁有这张卡谁可以进入代码块
两个线程要使用同一把锁,东西只有一个,很多人去抢,也可以在方法上上锁,加在返回值前面
当一个线程进入同步代码块时,它会拿到锁。如果其他线程也想用同样的锁进入同步代码块,它们必须等当前线程执行完并释放锁后才能拿到锁并执行。
public class Michael { public static void main(String[] args) throws InterruptedException { // 创建一个对象 o 用于同步 Object o = new Object(); // 创建并启动线程1 new Thread(() -> { System.out.println("线程1开始"); synchronized (o) { // 同步块 for (int j = 0; j < 5; j++) { try { System.out.println("线程1正在运行..."); Thread.sleep(1000); // 线程休眠 1000 毫秒 } catch (InterruptedException e) { throw new RuntimeException(e); } } } System.out.println("线程1结束"); }).start(); // 创建并启动线程2 new Thread(() -> { System.out.println("线程2开始"); synchronized (o) { // 同步块 for (int j = 0; j < 5; j++) { try { System.out.println("线程2正在运行..."); Thread.sleep(1000); // 线程休眠 1000 毫秒 } catch (InterruptedException e) { throw new RuntimeException(e); } } } System.out.println("线程2结束"); }).start(); } }
死锁
public class Main { public static void main(String[] args) { // 创建两个对象 o1 和 o2 Object o1 = new Object(); Object o2 = new Object(); // 创建线程 t1 Thread t1 = new Thread(() -> { synchronized (o1) { System.out.println("线程1拿到锁1"); try { Thread.sleep(1000); // 线程休眠 1000 毫秒 System.out.println("线程1等待锁2"); synchronized (o2) { System.out.println("线程1"); } } catch (InterruptedException e) { e.printStackTrace(); } } }); // 创建线程 t2 Thread t2 = new Thread(() -> { synchronized (o2) { System.out.println("线程2拿到锁2"); try { Thread.sleep(1000); // 线程休眠 1000 毫秒 System.out.println("线程2等待锁1"); synchronized (o1) { System.out.println("线程2"); } } catch (InterruptedException e) { e.printStackTrace(); } } }); // 启动线程 t1 和 t2 t1.start(); t2.start(); } }
检测死锁,打开终端jstack
wait和notify方法
public class Main { public static void main(String[] args) throws InterruptedException { // 创建一个对象 o1 Object o1 = new Object(); // 创建线程 t1 Thread t1 = new Thread(() -> { synchronized (o1) { try { System.out.println("开始等待"); o1.wait(); // 进入等待状态并释放锁 System.out.println("等待结束!"); } catch (InterruptedException e) { e.printStackTrace(); } } }); // 创建线程 t2 Thread t2 = new Thread(() -> { synchronized (o1) { System.out.println("开始唤醒"); o1.notify(); // 唤醒处于等待状态的线程 for (int i = 0; i < 50; i++) { System.out.println(i); } // 唤醒后依然需要等待这里的锁释放,之前等待的线程才能继续 } }); // 启动线程 t1 t1.start(); // 主线程休眠 1000 毫秒 Thread.sleep(1000); // 启动线程 t2 t2.start(); } }
对象的wait()方法会暂时使得此线程进入等待状态,同时会释放当前代码块持有的锁,这时其他线程可以获取到此对象的锁,当其他线程调用对象的notify()方法后,会唤醒刚才变成等待状态的线程(这时并没有立即释放锁)。注意,必须是在持有锁(同步代码块内部)的情况下使用,否则会抛出异常!
notifyAll其实和notify一样,也是用于唤醒,但是前者是唤醒所有调用wait()后处于等待的线程,而后者是看运气随机选择一个。
Threadlocal
package com.test; public class Main { public static void main(String[] args) throws InterruptedException { // 创建一个ThreadLocal对象并设置初始值 ThreadLocal<String> local = new ThreadLocal<>(); local.set("Hello World!"); // 创建并启动一个新线程,在线程中打印ThreadLocal的值 new Thread(() -> { System.out.println(local.get()); }).start(); } }
子线程中的sout无法打印主线程的local添加的字符串
计时器
import java.util.Timer; import java.util.TimerTask; public class Main { public static void main(String[] args) throws InterruptedException { Timer timer = new Timer(); timer.schedule(new TimerTask() { @Override public void run() { System.out.println("我是延迟任务"); } }, 1060, 1000); // delay: 1060ms, period: 1000ms } }
timer,delay休息一秒后执行,period隔一秒执行一次
lambda表达式只能用在函数式接口,timer是个抽象类
使用完成后,调用Timer的cancel()方法退出
守护线程
public class Main { public static void main(String[] args) throws InterruptedException { Thread main = Thread.currentThread(); Thread t = new Thread(() -> { try { while (true) { System.out.println("我是守护线程!"); Thread.sleep(1000); } } catch (InterruptedException e) { throw new RuntimeException(e); } }); // t.setDaemon(true); t.start(); Thread.sleep(3000); } }
通过setDaemon来设置为守护线程
Java中所有的线程都执行完毕后,守护线程自动结束,守护线程不适合进行IO操作,只适合打打杂
大部分集合类没法在多线程下使用,如果使用需要加锁
练习:生产者与消费者
通过多线程编程,来模拟一个餐厅的2个厨师和3个顾客,假设厨师炒出一个菜的时间为3秒,顾客吃掉菜品的时间为4秒。
import java.util.*; public class test { private static final Queue<Object> queue = new LinkedList<>(); public static void main(String[] args) { new Thread(test::add, "厨师1").start(); new Thread(test::add, "厨师2").start(); new Thread(test::take,"消费者1").start(); new Thread(test::take,"消费者2").start(); new Thread(test::take,"消费者3").start(); } private static void add() { while (true) { try { Thread.sleep(3000); synchronized (queue) { String name = Thread.currentThread().getName(); System.out.println(new Date() + "" + name + "出餐了!"); queue.offer(new Object()); queue.notifyAll(); } } catch (InterruptedException e) { e.printStackTrace(); } } } private static void take() { while (true) { try { synchronized (queue) { while (queue.isEmpty()) queue.wait(); queue.poll(); String name = Thread.currentThread().getName(); System.out.println(new Date()+""+name+"拿到了餐品,正在享用"); } Thread.sleep(4000); } catch (InterruptedException e) { e.printStackTrace(); } } } }
18反射
类加载器
类加载器就是 Java 用来加载类的工具,它负责把 .class 文件(字节码)从硬盘、网络或内存加载到 JVM 里,变成可以运行的代码。
通俗版理解: 类加载器 ≈ 快递员
你写的代码(.java)编译后变成 .class 文件(像网购的包裹)。
类加载器就是快递员,把包裹从仓库(硬盘/网络)送到你家(JVM 内存)。
分类(3个主要快递员)
Bootstrap(老板):送最核心的包裹(JDK 自带的类,比如 String)。
Extension(扩展专员):送扩展包裹(javax.* 这类额外功能)。
Application(普通快递员):送你自己的包裹(com.xxx 你的代码)。
双亲委派(快递员的工作规则) 快递员收到包裹后,先问上级:“你能送吗?” 如果上级都送不了,自己才送。
好处:避免重复送包裹(防止类被加载多次),也防止你冒充老板送假货(安全)。
举个例子:
public class Test {
public static void main(String[] args) {
// 1. String 是老板送的(Bootstrap 加载,返回 null)
System.out.println(String.class.getClassLoader()); // null
// 2. 你的 Test 类是普通快递员送的(AppClassLoader)
System.out.println(Test.class.getClassLoader()); // sun.misc.Launcher$AppClassLoader
}
什么时候需要自定义类加载器?
热部署:不重启程序,动态替换类(比如开发时改代码立刻生效)。
隔离加载:不同模块用不同的类加载器(比如 Tomcat 隔离多个 Web 应用)。
加密类:先解密再加载(防止别人反编译你的代码)。
总结: 类加载器干什么的? → 加载类到内存。
谁在加载? → Bootstrap、Extension、Application 三个快递员。
怎么工作的? → 先问上级,不行再自己来(双亲委派)。
为啥这么设计? → 安全 + 避免重复加载。
Java动态代理机制(Proxy + InvocationHandler)
代理加代理逻辑控制器,动态代理就是找个"中介"帮你干活,中介能在你做事前后加自己的操作
Proxy:JDK提供的代理工厂,专门生产动态代理
InvocationHandler:代理类的大脑,所有方法调用都交给它的invoke()处理
动态代理流程
1定义接口(必须!JDK动态代理只能代理接口
public interface UserService {
void save();
}
public interface 卖房 {
void 卖();
}
2实现InvocationHandler: 代理逻辑写在这里(如加日志、事务)
public class MyHandler implements InvocationHandler {
private Object target; // 被代理的真实对象(如UserServiceImpl)
public MyHandler(Object target) {
this.target = target;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("【代理】方法调用前...");
Object result = method.invoke(target, args); // 调用真实对象的方法
System.out.println("【代理】方法调用后...");
return result;
}
}
public class 房产中介 implements InvocationHandler {
private Object 真正的房主; // 记住真正的房主是谁
@Override
public Object invoke(Object 代理, Method 方法, Object[] 参数) throws Throwable {
System.out.println("【中介】先带客户看房..."); // 代理做的事
Object 结果 = 方法.invoke(真正的房主, 参数); // 让真正的房主签合同
System.out.println("【中介】收中介费..."); // 代理做的事
return 结果;
}
}
3生成代理对象: 通过Proxy.newProxyInstance()创建代理实例:
UserService realService = new UserServiceImpl(); // 真实对象
UserService proxy = (UserService) Proxy.newProxyInstance(
realService.getClass().getClassLoader(),
realService.getClass().getInterfaces(), // 代理的接口
new MyHandler(realService) // 传入InvocationHandler
);
proxy.save(); // 调用代理方法
卖房 房主 = new 真正的房主();
卖房 代理 = (卖房) Proxy.newProxyInstance(
房主.getClass().getClassLoader(),
new Class[]{卖房.class}, // 告诉中介要代理什么业务
new 房产中介(房主) // 把房主委托给中介
);
代理.卖(); // 客户以为在和房主交易,实际是中介在操作
输出结果
【中介】先带客户看房... 【房主】签合同卖房... 【中介】收中介费...