# String字符串
String是一个类,属于数据类型中的引用类型。
Java中一切使用""引起来的内容,都是这个类的实例,称为字符串对象。
**字符串**在定义后,值不可改变,是**一个常量**,实际是一个**字符数组**。
## 总结
在使用字符串时,如果要比较其值是否相同,不要使用==判断,因为==判断的是内存地址。
**所以在比较字符串是否相同时,要使用String类重写的equals方法进行判断。**
该方法判断的原理大致为:将两个字符串用字符数组保存,逐个判断字符数组中的每个字符,全部一致时返回true,
所以比较的是字面值。在使用equals方法时,通常将已知的非空字符串作为调用者。
```java
username.equals("admin");//这样写,username变量可能为空,会抛出空指针异常
"admin".equals(username);//这样写能避免空指针异常
```
# 可变字符串
String字符串对象是一个常量,在定义后,值不可改变。
如果使用String类的对象,对其频繁更新时,就会不停地创建新的对象,不停引用给同一个变量。
如要执行10000次循环重新赋值的过程,就要创建10000个字符串对象,执行效率很低,这时就需要使用可变字符串对象。
/*
* 可变字符串StringBuilder
* */
public class Test1 {
public static void main(String[] args) {
System.out.println("程序开始执行");
//System.currentTimeMillis();用于获取当前时间对应的毫秒数
//从1970 1 1 0:0:0这一刻开始,到这句话执行时间隔的毫秒数
long startTime = System.currentTimeMillis();
/*
//循环"更新"字符串,实际是在不停创建新的字符串
String str = "";
for (int i = 0; i < 50000; i++) {
str += i;
}*/
//使用可变字符串StringBuilder对象,真正更新字符串
//因为全程只有一个对象StringBuilder,每次循环只是在不停操作该对象,不会创建新对象,所以效率很高
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 500000; i++) {
sb.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("程序执行结束,用时" + (endTime - startTime) + "毫秒");
}
}
```
## StringBuilder类
用于表示可变字符串的一个类,是**非线程安全**的,建议在单线程环境下使用。
## StringBuffer类
用于表示可变字符串的一个类,是**线程安全**的,建议在多线程环境下使用。
StringBuilder和StringBuffer中的方法都一致,只不过StringBuffer中的方法使用了synchoronized关键字修饰,表示是一个同步方法,在多线程环境下不会出现问题。
这里以StringBuilder为例
|
## 注意
-都是在直接操作同一个字符串对象,每次调用方法后,原字符串都会发生变化- StringBuffer和StringBuilder并没有重写equals方法,所以可变字符串的值是否相同时,调用的是equals中原始的==判断。如果要判断两个可变字符串的值是否相同时,需要将其转换为String后调用equals判断
**总结**
在频繁操作同一个字符串时,一定要使用可变字符串StringBuidler或StringBuffer类的对象,不能使用String类的对象。
# System类
这个类中包含了一些系统相关的信息和一些方法。其中的属性和方法都是静态的。
该类不能创建对象,不是因为它是一个抽象类,而是因为它的构造方法是私有的。 |
# RunTime类
Runtime类的对象,表示**程序运行时对象**(程序运行环境对象)。
包含了程序运行环境相关的信息。常用于获取运行环境信息(如虚拟机内存)或执行某个命令。
## 特点
这个类不是一个抽象类,但不能创建对象,因为它的构造方法是私有的。
这个类提供了一个静态方法getRuntime(),通过这个方法,可以获取一个Runtime类的对象。
这是Java中的一种设计模式--单例模式(一个类只能有一个创建对象)。
```java
public class Runtime {
//定义了私有的一个静态成员:当前类的对象
//由于静态成员只在类加载时执行一次,所以这里只会创建唯一一个当前类的对象
private static Runtime currentRuntime = new Runtime();
//定义了一个公共的静态方法,用于获取创建的唯一的当前类的对象
public static Runtime getRuntime() {
return currentRuntime;
}
//构造方法是私有的,不能在当前类之外创建对象
private Runtime() {}
}
```
# 方法调用时传值问题
```java
package com.hqyj.test2;
public class Test {
/*
* 当方法的参数为原始类型,方法中对该参数做修改,不会影响实际参数
* */
public static void fun1(int i) {
i = 123;
System.out.println(i);
}
/*
* 当方法的参数为字符串时,方法中对字符串"重新赋值",实际是创建了一个新的字符串对象,不会影响实际参数
* */
public static void fun2(String str) {
str = "new";
System.out.println(str);
}
/*
* 如果参数为引用类型,方法中直接操作该参数,操作的就是实际参数的内存地址,会影响实际参数
* */
public static void fun3(Person p) {
p.setName("吴彦祖");
System.out.println(p.getName());
}
/*
* 如果参数为引用类型,方法中创建了一个新对象对其赋值,操作的是创建的新对象,不会影响实际参数
* */
public static void fun4(Person p) {
p = new Person();
p.setName("易烊千玺");
System.out.println(p.getName());
}
/*
* 如果参数为数组,也属于引用类型,方法中直接操作数组,操作的是实参数组,会影响实际参数
* */
public static void fun5(int[] list) {
list[0] = 123;
System.out.println(list[0]);
}
public static void fun(char[] list,Person p){
list[0]='m';//这里在直接操作实际参数,会影响实参
p = new Person();//这里创建了一个新的对象,操作的是方法中的对象,不会影响实参
p.setName("刘鑫");
}
public static void main(String[] args) {
//方法参数为原始类型,方法中对参数做修改,不会改变实际参数
int i = 0;
fun1(i);//123
System.out.println(i);//0
//方法参数为字符串,方法中对字符串重新赋值,不会改变实际参数
String str = "old";
fun2(str);//new
System.out.println(str);//old
//方法参数为引用类型,方法中对参数直接修改,会改变实际参数
Person p = new Person();
p.setName("王海");
fun3(p);
System.out.println(p.getName());
//方法参数为引用类型,方法中创建新对象后赋值给实际参数,操作的是方法中的对象,不会改变实际参数
Person p1 = new Person();
p1.setName("赵敏");
fun4(p1);
System.out.println(p1.getName());
//方法参数为数组,属于引用类型,方法中对参数直接修改,会改变实际参数
int[] list = {0,1,2};
fun5(list);
System.out.println(list[0]);
//练习
char[] list2={'a','b','c'};
Person p2 = new Person();
fun(list2,p2);
System.out.println(list2[0]);//m
System.out.println(p2.getName());//null
}
}
```
## 总结
**参数只有是引用类型(类、数组、接口),并且方法中在直接操作该参数时,才会对实际参数造成影响。**
fun3(Person p)参数为Person对象,方法中直接调用参数p的xxx方法,是在操作实际参数。
fun5(int[] list)参数为数组,方法中直接操作数组某个索引对应的元素,是在操作实际参数。
fun2(String str)和fun4(Person p)都在方法中创建了一个新的对象,是在操作方法中的参数,不影响实际参数。
|
# 包装类
Java是纯面向对象语言,宗旨是将一切事物视为对象处理。但原始类型不属于对象,不满足面向对象的思想。但原始类型在使用时无需创建对象,保存在栈中,效率高。为了让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以就有了包装类的概念。
**包装类就是原始类型对应的类类型**。包装类通常用于字符串与原始类型之间的转换。在web应用中,从浏览器页面中获取到后台的数据,全部都是String类型,所以一定要使用转换为原始
类型的方法。
## 特点
- 八个原始类型中,除了int和char,其余类型的包装类,都是将首字母改为大写。int对于Integer,char对应Character
- 包装类都是被final修饰的,不能被继承
- 除了Character类,其余包装类都有两个构造方法:参数为原始类型或String的构造方法。
Character的构造方法只有一个,参数为char类型。这些构造方法用于将原始类型或字符串转换为包装类对象
- 除了Character类,其余类都有静态方法parse原始类型(String str),用于将字符串转换为相应的原始类型
- 数值型的包装类的parseXXX()方法,如果参数不是对应的数字,转换时就会抛出
NumberFormat异常。如"123abc",或"123.4",在使用Integer.parseInt()时都会抛出异常
- Boolean类型中的parseBoolean()方法,参数如果是"true"这四个字母,不区分大小写,都能转换为真正boolean类型的true,只要不是"true"这个单词,转换结果都为false
- 除了Boolean类,其余包装类都有MAX_VALUE和MIN_VALUE这两个静态属性,用于获取对应类型支持的最大最小值
- 所有包装类都重写了toString()方法,用于将包装类对象转换为String对象
# 异常
当程序没有按开发人员的意愿正常执行,中途出现错误导致程序中断,出现这种情况,就称为异常。
学习异常就是认识异常的种类,如何处理异常和避免异常出现。
## 异常的产生
异常在程序中以对象的形式存在。当代码执行过程中出现异常,虚拟机会自动创建一个异常对象,如果没有对象该异常对象进行处理,就会导致程序中断,不再执行后续代码。
## 处理异常
通常所说的处理异常,是指处理Exception类的子类异常。
处理异常的目的,就是保证程序正常执行。
### 方式一:try-catch-finally语句
这种方式处理异常,无论会不会抛出异常,都能让程序正常执行。
```java
try{
//可能出现异常的代码
}catch(异常类 异常对象){
//如果出现异常对象,且与catch小括号中的异常类型匹配,就会执行这里的代码
}catch(异常类 异常对象){
//如果出现异常对象,且与catch小括号中的异常类型匹配,就会执行这里的代码
}finally{
//无论程序是否会抛出异常,都要执行的代码
}
```
执行流程:先执行try中的代码,当出现异常,与后续catch中的异常类型进行匹配,如果匹配到对应的类型或异常父类型,则执行大括号中的代码,最终一定执行finally中的内容。
### 方式二:throws关键字
这种方式,可以让编译时异常通过编译。
在定义方法的时候,通过该关键字声明可能抛出的异常。
用法:方法的参数部分之后,添加"throws 异常类型1,异常类型2..."
```java
public class Test{
public void fun() throws InterruptException{//这时该方法就会有一个声明:该方法可能
会抛出异常
//这句话直接写完后,会报错,因为sleep()方法可能会抛出InterruptException异常,属于
编译时异常,必须要处理
Thread.sleep(500);
}
}
```
#### throw和throws
- throws表示用于声明方法有可能出现的异常。使用时写在方法的小括号之后
- throw用于手动抛出异常对象。使用时,写在方法体中,常用于满足某种情况时,强制中断程序
用法:throw 异常对象;
# 数组和集合
## 数组的特点
- 数组中保存的元素都是有序的,可以通过下标快速访问
- 数组中保存的数据都是同一种类型
- 数组的长度在定义后,无法改变
- 数组无法获取其中保存的元素实际数量
## 集合的特点
- 能保存一组数据,可以有序可以无序
- 集合的容量可变
- 集合中可以保存不同类型的数据
- 可以获取集合中保存的元素实际数量
# Collection接口
该接口有两个核心子接口:List和Set。
这两个接口都可以保存一组元素,List接口保存元素时,是有序可重复的;Set接口保存元素时,是无序不重复的。
## List接口(有序可重复)
有序集合,元素可以重复,允许保存null,可以通过索引获取对应位置上的元素。
在该接口继承Collection接口的同时,又拓展了一些操作元素的方法,如添加到指定索引、根据索引删除、获取指定索引的元素、截取子集合的方法等。
### ArrayList实现类(掌握)
- **采用数组实现的集合**
- 可以通过索引访问元素,可以改变集合大小。如果要在其中插入或删除元素时,会影响后续元素
- 该集合中保存的都是引用类型,即便保存了数组123,也保存的是Integer类型的123,而不是int类型的123
- 该集合查询效率高,中途增加和删除元素效率低
#### 常用方法
ArrayList中的常用方法,就是Collection接口和List接口中定义的方法。
### LinkedList实现类
- **采用双向链表实现的集合**
- 集合中保存的每个元素也称为节点,除首尾节点外,其余节点都保存了自己的信息外,还保存了其前一个和后一个节点的地址
- 如果在双向链表的数据结构中插入和删除操作节点时,不会影响其他节点的位置。如添加时新节点时,只需要重写定义新节点的前后节点位置即可
- 如果要查询某个节点时,需要从头结点或尾结点开始一步步得到目标节点的位置
- 双向链表在中间插入和删除的效率高,随机读取的效率低
### ArrayList和LinkedList的区别
- 这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null
- ArrayList采用数组实现,随机读取效率高,插入删除效率低,适合用于查询
- LinkedList采用双向链表实现,插入删除时不影响其他元素,效率高,随机读取效率低,适合用于频繁更新集合
## Set接口(无序不重复)
无序集合,元素不可以重复,允许保存null,没有索引。
Set接口中没有自己定义的方法,都是继承于Collection接口中的方法
### 哈希表hash table
哈希表,也称为散列表,是一种数据结构,能更快地访问数据。
要保存的数据称为原始值,这个原始值通过一个函数得到一个新的数据,这个函数称为**哈希函数**,这个新数据称为**哈希码**,哈希码和原始值之间有一个映射关系,这个关系称为哈希映射,可以构造一张映射表,这个表称为哈希表。在哈希表中,可以通过哈希码快速地访问对应的原始值。
### HashSet实现类
- 采用哈希表实现
- 元素不能重复,无序保存,允许保存一个null
- 本质是一个HashMap对象
- 使用HashSet集合时,通常要重写实体类中的equals和hashcode方法
#### 常用方法
HashSet中没有属于自定义的方法,都是重写了父接口Set和Collection中的方法。这里参考Collection中的方法即可。
没有与索引相关的方法。
#### HashSet添加数据的原理
如果两个元素的hashCode相同且equals结果为true,视为同一个对象,不能添加。
每次向集合中添加元素时,先判断该元素的hashCode是否存在
- 如果不存在,视为不同对象,直接添加
- 如果存在,再判断equals方法的结果
- 如果false,视为不同对象,可以添加
- 如果true,视为同一对象,不能添加
由此可见,不能添加的条件是两个对象的hashCode相同且equals的结果为true。
如果每次只判断equals的话,由于equals方法通常重写时会判断很多属性,效率不高。
如果每次只判断hashCode的话,效率高,但有可能会有哈希冲突,
所以先判断hashCode,再判断equals,技能保证效率,又能保证不添加重复元素。
### TreeSet实现类
- 特殊的Set实现类,数据可以有序保存,可以重复,不能添加null
- 采用红黑树(自平衡二叉树)实现的集合
- 二叉树表示某个节点最多有两个子节点
- 某个节点右侧节点值都大于左侧节点值
- 红黑树会经过不停的"变色"、"旋转"达到二叉树的平衡
- 只能添加**同一种类型**的对象且该类**实现了Comparable接口**
- 实现Comparable接口后必须要重写compareTo()方法
- 每次调用添加add(Object obj)方法时,就会自动调用参数的compareTo()方法
- compareTo()方法的返回值决定了能否添加新元素和新元素的位置
- 如果返回0,视为每次添加的是同一个元素,不能重复添加
- 如果返回正数,将新元素添加到现有元素之后
- 如果返回负数,将新元素添加到现有元素之前
- 添加的元素可以自动排序
#### TreeSet的应用
如果要保存的元素需要对其排序,使用该集合。
保存在其中的元素必须要实现Comparable接口,且重写compareTo()方法,自定义排序规则
# Map接口
Map称为映射,数据以**键值对**的形式保存。保存的是键与值的对应关系。
键称为Key,值称为Value,键不能重复,键允许出现一个null作为键,值无限制。
键和值都是引用类型。
如,yyds就是一个键key,代表了一个含义:“永远单身”即为值value。
## HashMap实现类(掌握)

- JDK1.8之后,HashMap采用"**数组+链表+红黑树**"实现
- 当没有哈希冲突时,元素保存到数组中
- 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
- 如果链表的长度大于8,将链表转换为红黑树
- 数据采用键值对key-value的形式保存,键不能重复,能用null作为键;值没有限制,键和值都是引用类型
- 向HashMap集合中添加元素时,原理同HashSet
### 常用方法
常用方法参考Map中的方法
# 遍历集合中元素的方式
## 遍历List集合
```java
ArrayList nameList = new ArrayList();
nameList.add("Tom");
nameList.add("Jerry");
nameList.add("LiHua");
nameList.add("Danny");
```
### 方式一:普通for循环
```java
System.out.println("使用普通for循环遍历");
//方式一:普通for循环
for (int i = 0; i < nameList.size(); i++) {//从0遍历到size()
String name = nameList.get(i);//通过get(int index)获取指定索引的元素
System.out.println(name);
}
```
### 方式二:增强for循环
```java
System.out.println("使用增强for循环遍历");
//方式二:增强for循环
for (String name : nameList) {
System.out.println(name);
}
```
### 方式三:迭代器
```java
System.out.println("使用迭代器遍历");
//方式三:迭代器
//Collection类型的集合对象.iterator(),获取迭代器
Iterator iterator = nameList.iterator();
// iterator.hasNext()判断集合中是否还有下一个元素
// iterator.next();获取下一个元素
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
```
## 遍历Set集合
```java
Set hs = new HashSet();
hs.add(123);
hs.add("hello");
hs.add(null);
hs.add(987);
```
### 方式一:增强for循环
```java
for(Object o : hs){
System.out.println(o);
}
```
### 方式二:迭代器
```java
Iterator
# Collections集合工具类
- **Collection**是集合的根**接口**,定义了集合操作元素的方法
- **Collections**是集合的工具**类**,定义了集合操作元素的静态方法
# 集合和数组之间的转换
- **集合转换为数组**:使用Collection接口中的toArray()方法
```java
Object[] obj = 集合对象.toArray();
List list = new ArrayList();
list.add(123);
list.add(63);
list.add(3);
Integer[] nums =(Integer[]) list.toArray();
```
- **数组转换为集合**
```java
//一个数组对象
int[] nums ={11,2,66,3,6,21};
//定义集合对象
List list = new ArrayList();
//遍历数组的同时添加到集合中
for(int i:nums){
list.add(i);
}
```
# 文件类File
Java中的File类,表示本地硬盘中的文件(文件和目录)的一个类。
通过这个类创建的对象,可以操作对应的文件。
## 递归遍历文件夹
```java
package com.hqyj.FileTest;
import java.io.File;
import java.util.Date;
public class Test3 {
//查看某个目录下的所有文件
public static void main(String[] args) {
File source = new File("E:\\adobe");
Test3 t = new Test3();
t.fun(source);
}
/*
* 递归遍历文件夹
* */
public void fun(File source) {
//输出某个目录中超过3个月未使用且大于500MB的文件
/*
long start = source.lastModified();
long end = System.currentTimeMillis();
if ((end - start) / 1000 / 3600 / 24 > 90 && source.length() / 1024 / 1024 > 500) {
System.out.println(source.getName() + "\t" + new Date(source.lastModified()) + "\t" + source.length() / 1024 / 1024);
}*/
//判断是否为目录
if (source.isDirectory()) {
//将其展开
for (File child : source.listFiles()) {
//因为子文件有可能是目录,继续调用本方法
fun(child);
}
}
}
}
```
# IO
I:Input输入
O:Output输出
# 流Stream
在Java中,**流用于表示计算机硬盘与内存之间传输数据的通道。**
将**内存**中的数据存入**到硬盘**中,称为**写write**,也称为**输出Output**。
将**硬盘**中的数据存入**到内存**中,称为**读read**,也称为**输入Input**。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7MwXIFFF-1670151305764)(F:\221001\笔记\JavaAdv06.assets\image-20221130160037535.png)]
## 流的分类
Java中将流定义为类,以对象的形式表现流。流有"四大家族",是所有流的父类。
### 字节输入流InputStream
**FileInpuStream**、**ObjectInputStream**
### 字节输出流OutputStream
**FileOutputStream**、**ObjectOutputStream**
### 字符输入流Reader
FileReader、**BufferedReader**、OutputStreamWriter
### 字符输出流Writer
FileWriter、**BufferedWriter**、InputStreamReader
**按方向分类**
- 输入流:InputStream、Reader
- 将硬盘中的数据读取到内存中
- 输出流:OutputStream、Writer
- 将内存中的数据写入到硬盘中
**按类型分**
- 字节流:InputStream、OutputStream
- 读写非文本类型文件。如图片、音视频、其他文件等。
- 字符流:Reader、Writer
- 读写纯文本类型文件。如txt、md等
1.如要将硬盘中某个txt文件中的内容读取到程序中,使用Reader
2.如要将硬盘中的某个图片读取到程序中,使用InputStream
3.如要将程序中的文本写入到硬盘中为txt类型文件时,使用Writer
4.如要将程序中的数据写入到硬盘中为非文本文件时,使用OutputStream
## 流的四个父类的特点
- 这四个父类都是在java.io包下,都是抽象类,不能直接创建其对象,使用其子类创建对象
- 这四个父类中都定义了close()方法,用于关闭流对象,释放资源
- 输入流(InputStream和Reader)都有read()方法读取数据到内存中,输出流都有write()方法写入数据到硬盘中
- 输出流(OutputStream和Writer)都有flush()方法,用于将流中的数据冲刷到硬盘中
- 在使用输出流对象时,一定要调用flush()或close()方法后,才能真正将数据写入到硬盘中
- 所有的流中,以Stream结尾,都是字节流,数据以字节传输;以Reader或Writer结尾的,都是字符流,数据以字符传输
- 读取硬盘中的数据,使用输入流,读取的文件必须存在;将数据写入到硬盘中,使用输出流,文件可以不存在,但父目录必须存在。
- 读入或写入文本时,使用字符流;读取或写入非文本时,使用字节流
# FileInputStream文件字节输入流(掌握)
按字节读取硬盘中的文件 |
# FileOutputStream文件字节输出流(掌握)
按字节将内存中的数据写入到硬盘中。
* 1.调用copyDir方法,判断发现source是一个文件夹,创建目标文件夹target:“F:\221001\笔记副本”
* 2.遍历source,如其中有xxx.md文件,即child
* 此时的source是F:\221001\笔记\xxx.md,即child
* 此时的target是F:\221001\笔记副本\xxx.md,用File(File parent,String child)构造方法表示这个目标文件
* 所以创建File newTarget = new File(target,child.getName())
*
* */
copyDir(source, target);
}
/*
* 定义复制文件夹的方法
* */
public static void copyDir(File source, File target) {
//如果是文件,调用单文件复制的方法
if (source.isFile()) {
copyFile(source, target);
} else {//如果是文件夹
//创建要复制的目标文件夹
target.mkdir();
//展开原文件夹
for (File child : source.listFiles()) {
//定义复制后的新目标文件
//如source为F:\221001\笔记\day1.md时,递归调用的target为F:\221001\笔记副本\day1.md
File newTarget = new File(target, child.getName());//这里使用File(File parent,String child)构造方法创建target对象
//递归调用的原文件依然是当前遍历出来的子文件,目标文件就是最终复制的F:\221001\笔记副本\day1.md
copyDir(child, newTarget);
}
}
}
/*
* 定义单文件复制的方法
* */
public static void copyFile(File source, File target) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//创建用于输入输出的流对象
fis = new FileInputStream(source);
fos = new FileOutputStream(target);
//定义字节数组
byte[] bytes = new byte[1024 * 1024 * 8];
//按数组读取
int count = fis.read(bytes);
while (count != -1) {
fos.write(bytes, 0, count);
count = fis.read(bytes);
}
} catch (FileNotFoundException e) {
System.out.println("文件不存在" + e);
} catch (IOException e) {
System.out.println("读写异常" + e);
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
System.out.println("关闭流对象异常" + e);
}
}
}
}
```
# FileReader文件字符输入流
按字符读取文件。
## 构造方法
|
# FileWriter文件字符输出流 |
# BufferedReader缓冲字符输入流(掌握)
自带缓冲区(字符数组)的字符输入流。默认字符数组大小为8192,每次最多读取8192个字符。
在读取纯文本文件(txt或md)时,首选该类。
# ObjectOutputStream对象字节输出流(序列化)(掌握)
**序列化:将对象转换为文件的过程**
**被序列化的对象,必须要实现Serializable接口。**
这个接口是一个特殊的接口,没有定义任何方法,只是给该类加上标记,表示该类可以被序列化
**反序列化:将文件转换为对象的过程**
# 序列化和反序列化案例
Person类,实现Serializable接口
```java
package com.hqyj.ObjectStream;
import java.io.Serializable;
/*
* 如果希望该类的对象能序列化,写入对象到本地,必须要实现Serializable接口
* Serializable接口中没有任何方法,是一个标记接口,表示该类的对象可以被序列化
* */
public class Person implements Serializable {
private String name;
private int age;
private String sex;
//省略getter/setter和toString()
}
```
Main类
```java
package com.hqyj.ObjectStream;
import java.io.*;
import java.util.ArrayList;
public class Test1 {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person p1 = new Person("王海", 22, "男");
Person p2 = new Person("赵敏", 24, "女");
Person p3 = new Person("刘涛", 21, "女");
ArrayList list = new ArrayList<>();
list.add(p1);
list.add(p2);
list.add(p3);
//创建OutStream的实现类,设置写入的文件路径
OutputStream os = new FileOutputStream("F:\\221001\\person.p");
//创建对象输出字节流,参数为OutStream类型
ObjectOutputStream oos = new ObjectOutputStream(os);
//调用writeObject(Object obj)方法,将对象写入到硬盘中(序列化)
oos.writeObject(list);
oos.close();
//创建对象输入字节流,将上一步保存的文件进行反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\221001\\person.p"));
//使用readObject()方法,将写入的文件进行读取(反序列化)
ArrayList pList = (ArrayList) ois.readObject();
for (Person person : pList) {
System.out.println(person);
}
ois.close();
}
}
```
# 转换流
实际属于字符流,作用为将一个字节流对象转换为字符流对象
## OutputStreamWriter
将字节输出流转换为字符输出流
## InputStreamReader
将字节输入流转换为字符输入流
## 转换流的使用
如果只提供了一个字节流,但要向其中写入或读取字符时,就可以使用转换流将字节流转换为字符流。
使用字符流读写字符时比字节流更方便。
```java
//假如只提供一个字节输出流对象
FileOutputStream fos = new FileOutputStream("文件路径");
//fos.write(97);//这时如果写入数据,只能按字节写入,不方便
//使用转换流,将字节流对象fos转换为字符流对象
Writer writer = OutputStreamWriter(fos);
//将字符流对象writer包装成缓冲字符流对象
BufferedWriter bw = new BufferedWriter(writer);
bw.write("hello你好");
bw.newLine();
bw.close();
//只提供字节输入流对象
FileInputStream fis = new FileInputStream("221001.txt");
// fis.read()每次只能读取一个字节
//将字节流转换为字符流
Reader reader = new InputStreamReader(fis);
//创建缓冲字符流,将字符流包装为缓冲流
BufferedReader br = new BufferedReader(reader);
//整行读取
while (br.ready()) {
System.out.println(br.readLine());
}
br.close();
```
# 网络编程
## InetAddress类
表示IP对象的一个类
```java
public static void main(String[] args) throws UnknownHostException {
//获取本机的ip对象
// InetAddress ip = InetAddress.getLocalHost();
//获取域名
// System.out.println(ip.getHostName());
//获取真实ip地址
// System.out.println(ip.getHostAddress());
//getByName(域名) 得到域名对应的ip对象
//localhost域名表示本机,对应的ip地址为127.0.0.1
InetAddress ip = InetAddress.getByName("localhost");
//获取域名
System.out.println(ip.getHostName());
//获取ip地址
System.out.println(ip.getHostAddress());
}
```
## Socket类和ServerSocket类
都属于Socket(套接字)对象,表示网络中的某个端点
- Socket指普通端
- ServerSocket指服务器端
# 使用套接字对象实现两个端点(Socket和ServerSocket)之间发送文件
## 服务器端
```java
package com.hqyj.uploadTest;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
/*
* 使用套接字对象,实现客户端向服务端发送文件
*
* 定义服务端套接字对象
* */
public class Server {
public static void main(String[] args) throws IOException {
//以本机创建服务端套接字对象
ServerSocket server = new ServerSocket(8899, 100, InetAddress.getLocalHost());
//等待客户端连接,返回连接的客户端套接字对象
Socket client = server.accept();
//定义要将读取到的数据写入到本地的文件字节输出流对象
FileOutputStream fos = new FileOutputStream("上传文件.md");
//获取客户端与服务端的输入流对象,读取发送的数据
InputStream is = client.getInputStream();
//定义读取的字节数组
byte[] bytes = new byte[1024 * 1024 * 8];
int count = is.read(bytes);
while (count != -1) {
//将读取到的数据写入到本地
fos.write(bytes, 0, count);
count = is.read(bytes);
}
fos.close();
is.close();
}
}
```
## 客户端
```java
package com.hqyj.uploadTest;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
/*
* 定义客户端套接字对象
* */
public class Client {
public static void main(String[] args) throws IOException {
//创建客户端套接字对象,连接指定的服务端套接字对象
Socket client = new Socket("192.168.31.39", 8899);
//获取客户端与服务端的输出流对象
OutputStream os = client.getOutputStream();
//成功连接后,将某个文件发送给服务端
//定义要发送的文件对象
File file = new File("F:\\221001\\笔记\\面向对象部分回顾.md");
//读取要发送的文件
FileInputStream fis = new FileInputStream(file);
//定义字节数组
byte[] bytes = new byte[1024 * 1024 * 8];
//循环读取要发送的文件
int count = fis.read(bytes);
while (count != -1) {
//将读取到的数据写入到客户端套接字与服务端套接字的通道中
os.write(bytes,0,count);
count = fis.read(bytes);
}
fis.close();
os.close();
}
}
```
# 进程和线程
## 进程Process
进程就是操作系统中执行的程序。一个程序就是一个执行的进程实体。
每个运行中的进程,都有属于它独立的内存空间,各个进程互不影响。
## 线程Thread
线程是一个进程中的执行单元,一个进程中可以有多个线程。
多个线程,可以访问同一个进程中的资源。
每个线程都有一个独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。
## 多线程
如果一个进程中,同时在执行着多个线程,就称为多线程。
多线程可以提高程序执行效率。如多个窗口卖票,可以加快卖票的效率。
其实每个执行的Java程序,都是多线程执行,main方法称为主线程,还有gc线程(守护线程)在同时运行。
如有一个工厂,工厂中有很多车间,每个车间有很多流水线。
工厂就是内存,车间就是各个进程,每个流水线都是一个进程中的一个线程。
# 并行和并发
## 并行
各个进程同时执行,称为并行。
## 并发
多个线程同时执行,称为并发。
# 同步和异步
## 同步
所有的任务排队执行,称为同步执行。
## 异步
在执行任务A的同时,执行任务B,称为异步执行。
# Java中的线程Thread类
Java中,线程以对象的形式存在。
Thread类表示线程类
# 实现多线程
## 方式一:继承Thread类
- 1.创建一个类,继承Thread类
- 2.重写Thread类中的run()方法
- 3.创建自定义的线程子类对象后,调用start()方法
## 方式二:实现Runnable接口(建议使用)
由于Java中是单继承,如果某个类已经使用了extends关键字去继承了另一个类,这时就不能再通过extends继承Thread实现多线程。
就需要实现Runnable接口的方式实现多线程。
- 1.自定义一个类,实现Runnable接口
- 2.重写run()方法,将多线程要执行的内容写在该方法中
- 3.创建Runnable接口的实现类对象
- 4.使用构造方法Thread(Runnable target)或Thread(Runnable target,String name)将上一步创建的Runnable实现类对象包装为Thread对象
## 方式三:使用匿名内部类
如果不想创建一个Runnable接口的实现类,就可以使用匿名内部类充当Runnable接口的实现类
# 线程的生命周期
线程的初始化到终止的整个过程,称为线程的生命周期。

## 新生状态
当线程对象被创建后,就进入了新生状态。
## 就绪状态
当某个线程对象调用了start()方法后,就进入了就绪状态。
在这个状态下,线程对象不会做任何事情,只在等他CPU调度。
## 运行状态
当某个线程对象得到CPU时间片(CPU执行这个线程的机会所给的时间),则进入运行状态,开始执行run()方法。
不会等待run()方法执行完毕,只会在指定的时间内尽可能地执行run()方法。只要调用玩run()方法后,就会再进入就绪状态。
## 阻塞状态
如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞状态。
sleep()方法会在指定时间后,让线程重新就绪。
wait()方法只有在被调用notify()或notifyAll()方法唤醒后才能重新就绪。
## 终止状态
当某个线程的run()方法中的所有内容都执行完,就会进入终止状态,意味着该线程的使命已经完成。
# 守护线程
如果将一个线程设置setDeamon(true),表示该线程为守护线程。
守护线程会随着其他非守护线程终止而终止。
```java
package com.hqyj.DaemonTest;
/*
* Test类是一个自定义线程类,死循环输出
* */
public class Test implements Runnable {
public static void main(String[] args) {
Thread thread = new Thread(new Test());
//将自定义线程类设置为守护线程
thread.setDaemon(true);
thread.start();
//main线程终止,守护线程也会终止
for (int i = 0; i < 100; i++) {
System.out.println("main方法中的循环执行中");
}
}
@Override
public void run() {
while (true) {
System.out.println("守护线程执行中。。。");
}
}
}
```
# 多线程访问同一个资源
## 可能出现的问题
如银行存款100,同一时刻在手机和ATM一起取出,如果用多线程模拟,可能会出现两个线程都取出100的情况。要避免这种情况发生。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xmjaT6FX-1670151385957)(F:\221001\笔记\JavaAdv07.assets\image-20221202145400278.png)]
本应该大于售出后再减,再打印剩余,由于线程A在打印"售出一张"后,还没来得及执行后续内容,其他线程就开始执行了。
## 出现问题的原因
由于线程调用start()方法后,就进入就绪状态。如果获得了CPU时间片,就开始调用run()方法,调用run()方法后,就会再次进入就绪状态,不会等待run()方法执行完毕,所以在线程A执行run()方法的时候,线程B也开始执行了,这样就会出现数据共享的问题。
因为现在所有的线程都是异步(同时)执行。
## 如何解决
让线程同步(排队)执行即可。这样一来,某个线程执行run()方法的时候,让其他线程等待run()方法的内容执行完毕。
### synchronized关键字
这个关键字可以修饰方法或代码块
#### 修饰方法
写在方法的返回值之前,这时该方法就称为同步方法。
```java
public synchronized void fun(){
//会排队执行的代码
}
```
#### 修饰代码块
写在一个独立的{}前,这时该段内容称为同步代码块。
```java
synchronized(要同步的对象或this){
//会排队执行的代码
}
```
#### 原理
每个对象默认都有一把"锁",当某个线程运行到被synchronized修饰的方法时,该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其结束后,才会释放这把锁。
使用synchronized修饰后的锁称为"悲观锁"。
方法被synchronized修饰后,称为同步方法,就会让原本多线程变成了单线程(异步变为同步)。
- 什么叫死锁?怎么产生?如何解决?
如果有两个人吃西餐,必须有刀和叉,此时只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己拥有的,这时就会造成僵持的局面,这个局面就称为死锁,既不结束,也不继续。
## 模拟死锁出现的情况
定义两个线程类,线程A先获取资源A后,在获取资源B;线程B先获取资源B后,再获取资源A。
如果对资源A和资源B使用了synchronized进行同步,就会在线程A获取资源A的时候,线程B无法获取资源A,相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。
**PersonA线程**
```java
package com.hqyj.deadlock;
public class PersonA implements Runnable {
//定义两个共享的成员变量,刀、叉
private Object knife;
private Object fork;
public PersonA(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run方法时,先获取knife对象,等待3s后获取fork对象
*
* */
@Override
public void run() {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}
```
**PersonB线程**
```java
package com.hqyj.deadlock;
public class PersonB implements Runnable {
//定义两个共享的成员变量,刀、叉
private Object knife;
private Object fork;
public PersonB(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run方法时,先获取fork对象,等待3s后获取对象knife
*
* */
@Override
public void run() {
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,3s后获取knife");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,可以吃饭了");
}
}
}
}
```
## 死锁的解决方式
### 方式一
**让两个线程获取资源的顺序保持一致。**
如两个线程都先获取knife,再获取fork
```java
@Override
public void run() {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
```
### 方式二
让两个线程在获取资源A和B之前,再获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个资源后,将后续内容执行完毕,其他线程才能开始执行。
如在获取knife和fork之前,先获取paper对象
```java
@Override
public void run() {
//先获取paper,再进行后续操作
synchronized (paper) {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}
```
# 面试题解析
**1. Java容器都有哪些?**
- List 容器:ArrayList、LinkedList
- Set 容器:HashSet、LinkedHashSet、TreeSet
- Map 容器:LinkedHashMap、TreeMap、ConcurrentHashMap、Hashtable
**2.Collection 和 Collections 有什么区别?**
- Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。List,Set,Queue接口都继承Collection。
直接实现该接口的类只有AbstractCollection类,该类也只是一个抽象类,提供了对集合类操作的一些基本实现。List和Set的具体实现类基本上都直接或间接的继承了该类。
- Collections 是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。
**3.List、Set、Map 之间的区别是什么?**
- List:有序集合、元素可重复
- Set:无序集合、元素不可重复
- Map:键值对集合、储存键、值和之间的映射,Key无序,唯一;Value不要求有序,允许重复。
**4.HashMap 和 Hashtable 有什么区别?**
- 线程安全性不同。HashMap线程不安全;Hashtable 中的方法是Synchronize的。
- key、value是否允许null。HashMap的key和value都是可以是null,key只允许一个null;Hashtable的key和value都不可为null。
- 迭代器不同。HashMap的Iterator是fail-fast迭代器;Hashtable还使用了enumerator迭代器。
- hash的计算方式不同。HashMap计算了hash值;Hashtable使用了key的hashCode方法。
- 默认初始大小和扩容方式不同。HashMap默认初始大小16,容量必须是2的整数次幂,扩容时将容量变为原来的2倍;Hashtable默认初始大小11,扩容时将容量变为原来的2倍加1。
- 是否有contains方法。HashMap没有contains方法;Hashtable包含contains方法,类似于containsValue。
- 父类不同。HashMap继承自AbstractMap;Hashtable继承自Dictionary。
**5.如何决定使用 HashMap 还是 TreeMap?**
- 对于在 Map 中插入、删除和定位元素这类操作, 选择HashMap。
- 对一个有序 的 key 集合进行遍历, 选择TreeMap。
**6.说一下 HashMap 的实现原理?**
- JDK1.8之后,HashMap采用"**数组+链表+红黑树**"实现
- 当没有哈希冲突时,元素保存到数组中
- 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
- 如果链表的长度大于8,将链表转换为红黑树
- 数据采用键值对key-value的形式保存,键不能重复,能用null作为键;值没有限制,键和值都是引用类型
- 向HashMap集合中添加元素时,原理同HashSet
**7.说一下 HashSet 的实现原理?**
- 是基于HashMap实现的,默认构造函数是构建一个初始容量为16,负载因子为0.75 的HashMap。封装了一个 HashMap 对象来存储所有的集合元素,所有放入 HashSet 中的集合元素实际上由 HashMap 的 key 来保存,而 HashMap 的 value 则存储了一个 PRESENT,它是一个静态的 Object 对象。
**8.ArrayList 和 LinkedList 的区别是什么?**
- ArrayList 是基于数组实现的,LinkedList 是基于链表实现的
- 随机访问 List 时,ArrayList 比 LinkedList 的效率高
- 当对数据进行增加和删除的操作时,LinkedList 比 ArrayList的效率更高
- ArrayList主要控件开销在于需要在lList列表预留一定空间;而LinkList主要控件开销在于需要存储结点信息以及结点指针信息。
- ArrayList自由性较低,因为它需要手动的设置固定大小的容量,但是它的使用比较方便,只需要创建,然后添加数据,通过调用下标进行使用;而LinkedList自由性较高,能够动态的随数据量的变化而变化,但是它不便于使用。
**9.如何实现数组和 List 之间的转换?**
- **集合转换为数组**:使用Collection接口中的toArray()方法
- **一组数据转换为集合**:使用Arrays工具类中的asList(一组数据)方法
**10.Array 和 ArrayList 有何区别?**
- Array可以包含基本类型和对象类型,ArrayList只能包含对象类型。
- Array大小是固定的,ArrayList的大小是动态变化的。
- ArrayList提供了更多的方法和特性,比如:addAll(),removeAll(),iterator()等等。
- 对于基本类型数据,ArrayList 使用自动装箱来减少编码工作量;而当处理固定大小的基本数据类型的时候,这种方式相对比较慢,这时候应该使用Array。
**11.哪些集合类是线程安全的?**
Vector、HashTable、ConcurrentHashMap
**12.迭代器 Iterator 是什么**
- terator 是可以遍历集合的对象,为各种容器提供了公共的操作接口,隔离对容器的遍历操作和底层实现,从而解耦。缺点是增加新的集合类需要对应增加新的迭代器类,迭代器类与集合类成对增加。
- 迭代器模式,它是 Java 中常用的设计模式之一。用于顺序访问集合对象的元素,无需知道集合对象的底层实现。
**13. Iterator 怎么使用?有什么特点?**
- Iterator的使用
(1)Iterator()要求容器返回一个Iterator。Iterator将准备好返回序列的第一个元素。
(2)使用next()获得序列中的下一个元素
(3)使用hasNext()检查序列中是否还有元素。
(4)使用remove()将迭代器新近返回的元素删除。
- Iterator的特点
(1) Iterator遍历集合元素的过程中不允许线程对集合元素进行修改,否则会抛出ConcurrentModificationEception的异常。
(2)Iterator遍历集合元素的过程中可以通过remove方法来移除集合中的元素,删除的是上一次Iterator.next()方法返回的对象。
(3)Iterator必须依附于一个集合类对象而存在,Iterator本身不具有装载数据对象的功能。
(4)next()方法,该方法通过游标指向的形式返回Iterator下一个元素。
**14.Iterator 和 ListIterator 有什么区别?**
- Iterator可用来遍历Set和List集合,但是ListIterator只能用来遍历List。
- Iterator对集合只能是前向遍历,ListIterator既可以前向也可以后向。
- ListIterator实现了Iterator接口,并包含其他的功能,比如:增加元素,替换元素,获取前一个和后一个元素的索引。
**15.怎么确保一个集合不能被修改?**
- 可使用 Collections. unmodifiableCollection (Collection c) 方法来创建 一个 只读集合
**16. 线程和进程的区别?**
- 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位
- 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可并发执行
- 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源.
- 系统开销:在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。.
**17.创建线程有哪几种方式?有什么区别?**
- 方式
- 继承Thread类
- 实现Runnable接口
- Runnable 接口的匿名内部类
- 使用Callable接口
- 区别
- 继承Thread类的线程类不能再继承其他父类
- 实现接口的方式多个线程可以共享一个target对象
**18.说一下 runnable 和 callable 有什么区别?**
- callable的核心是call方法,允许返回值,runnable的核心是run方法,没有返回值
- call方法可以抛出异常,但是run方法不行
- callable和runnable都可以应用于executors。而thread类只支持runnable
**19.在 Java 程序中怎么保证多线程的运行安全?**
- 使用手动锁lock
- 使用线程安全的类
- 使用自动锁 synchronized 关键字
- 使用 volatile 关键字
**20.什么是死锁?**
- 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去;此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
**21.多线程产生死锁的 4 个必要条件**
- 互斥条件:任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
- 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
- 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
- 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
**22. 怎么防止死锁?**
- 破坏互斥条件 使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁。 只读数据文件、磁盘等软硬件资源均可采用这种办法管理。
- 破坏占有和等待条件 采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行。 实现简单,但是严重的减低了资源利用率。
- 破坏不剥夺条件 剥夺调度能够防止死锁,但是只适用于内存和处理器资源。 占有资源的进程若要申请新资源,必须主动释放已占有资源,若需要此资源,应该向系统重新申请。
- 破坏循环等待条件 给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。 采用层次分配策略,将系统中所有的资源排列到不同层次中
**23.synchronized 和 Lock 有什么区别?**
- Synchronized是关键字,内置语言实现,Lock是接口。
- Synchronized在线程发生异常时会自动释放锁,因此不会发生异常死锁。Lock异常时不会自动释放锁,所以需要在finally中实现释放锁。
- Lock是可以中断锁,Synchronized是非中断锁,必须等待线程执行完成释放锁。
- Lock可以使用读锁提高多线程读效率。
**24.sleep() 和 wait() 有什么区别?**
- sleep 是 Thread 的方法,wait 是 Object 的方法
- sleep 不释放锁,wait0 0释放锁
- sleep 用于一定时间内暂停线程执行,wait 常用于线程间交互和通信
- sleep 方法指定时间之后,线程会自动苏醒,wait 可以通过 notify 或者 notifyAll 来唤醒 wait 的线程
**25.对象的四种应用方式?**
- 强引用:在我们日常编码中,使用最广的引用方式。只要强引用还存在,对象就不会被垃圾回收器回收。可以通过将引用置空的方式,让JVM回收该对象。
- 软引用:有用但是并非必须的对象可以使用弱引用。对于软引用关联着的对象,只有在内存不足的时候JVM才会回收该对象。
- 弱引用:只具有弱引用的生命周期较软引用更加短暂。垃圾回收器线程扫描发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。因此,弱引用更适合想缓存数据,又不想影响对象收集的地方
- 虚引用(幻影引用):该应用并不能获取到任何对象,也不会影响对象生命周期。虚引用必须和引用队列联合使用,虚引用主要用来跟踪对象被垃圾回收的活动。
**26.什么是 Java 序列化?什么情况下需要序列化?**
- 序列化:将 Java 对象转换成字节流的过程。
- 使用:当Java 对象需要在网络上传输 或者 持久化存储到文件中时,就需要对 Java 对象进行序列化处理。