数据来源:拉勾教育Java就业急训营
核心类库(下)
- 一. 异常机制和File类
- 二. IO流
- 1. IO流的概念
- 2. 基本分类
- 3. 体系结构
- 4. 相关流的详解
- 4.1 FileWriter类(重点)
- 4.2 FileReader类(重点)
- 4.3 FileOutputStream类(重点)
- 4.4 FileInputStream类(重点)
- 4.5 BufferedOutputStream类(重点)
- 4.6 BufferedInputStream类(重点)
- 4.7 BufferedWriter类(重点)
- 4.8 BufferedReader类(重点)
- 4.9 PrintStream类
- 4.10 PrintWriter类
- 4.11 OutputStreamWriter类
- 4.12 InputStreamReader类
- 4.13 字符编码
- 4.14 DataOutputStream类(了解)
- 4.15 DataInputStream类(了解)
- 4.16 ObjectOutputStream类(重点)
- 4.17 ObjectInputStream类(重点)
- 4.18 RandomAccessFile类
- 三. 多线程
- 四. 网络编程
- 五. 反射机制
一. 异常机制和File类
1. 异常机制(重点)
1.1 概念
● 异常就是"不正常"的含义,在Java语言中主要指程序执行中发生的不正常情况。
● java.lang.Throwable类是Java语言中错误(Error)和异常(Exception)的超类。
● 其中Error类主要用于描述Java虚拟机无法解决的严重错误,通常无法编码解决,如:JVM挂掉了等。
● 其中Exception类主要用于描述因编程错误或偶然外在因素导致的轻微错误,通常可以编码解决,如:0作为除数等。
1.2 异常的分类

● java.lang.Exception类是所有异常的超类,主要分为以下两种:
RuntimeException - 运行时异常,也叫作非检测性异常
IOException和其它异常 - 其它异常,也叫作检测性异常,所谓检测性异常就是指在编译阶段都能被编译器检测出来的异常。
● 其中RuntimeException类的主要子类:
ArithmeticException类 - 算术异常
ArrayIndexOutOfBoundsException类 - 数组下标越界异常
NullPointerException - 空指针异常
ClassCastException - 类型转换异常
NumberFormatException - 数字格式异常
● 注意:
当程序执行过程中发生异常但又没有手动处理时,则由Java虚拟机采用默认方式处理异常,而默认处理方式就是:打印异常的名称、异常发生的原因、异常发生的位置以及终止程序。
1.3 异常的避免
public class ExceptionPreventTest {
public static void main(String[] args) {
// 会发生算术异常
int ia = 10;
int ib = 0;
if (0 != ib) {
System.out.println(ia / ib);
}
// 数组下标越界异常
int[] arr = new int[5];
int pos = 5;
if (pos >= 0 && pos < 5) {
System.out.println(arr[pos]);
}
// 发生空指针异常
String str = null;
if (null != str) {
System.out.println(str.length());
}
// 类型转换异常
Exception ex = new Exception();
if (ex instanceof IOException) {
IOException ie = (IOException) ex;
}
// 数字格式异常(用正则表达式判断)
String str2 = "123a";
if (str2.matches("\\d+")) {
System.out.println(Integer.parseInt(str2));
}
System.out.println("程序总算正常结束了!");
}
}
● 在以后的开发中尽量使用if条件判断来避免异常的发生。
● 但是过多的if条件判断会导致程序的代码加长、臃肿,可读性差。
1.4 异常的捕获
语法格式
try {
编写可能发生异常的代码;
}
catch(异常类型 引用变量名) {
编写针对该类异常的处理代码;
}
...
finally {
编写无论是否发生异常都要执行的代码;
}
注意事项
a.当需要编写多个catch分支时,切记小类型应该放在大类型的前面;
b.懒人的写法:用多态的方式捕获所有小异常的父类Exception
catch(Exception e) {}
c.finally通常用于进行善后处理,如:关闭已经打开的文件等
执行流程
try {
a;
b; - 可能发生异常的语句
c;
}catch(Exception ex) {
d;
}finally {
e;
}
当没有发生异常时的执行流程:a b c e;
当发生异常时的执行流程:a b d e;
案例:
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
public class ExceptionCatchTest {
public static void main(String[] args) {
// 创建一个FileInputStream类型的对象与d:/a.txt文件关联,打开文件
FileInputStream fis = null;
try {
System.out.println("1");
// 当程序执行过程中发生了异常后直奔catch分支进行处理
fis = new FileInputStream("d:/a.txt");
System.out.println("2");
} catch (FileNotFoundException e) {
System.out.println("3");
e.printStackTrace();
System.out.println("4");
}
// 关闭文件
try {
System.out.println("5");
fis.close();
System.out.println("6");
} /*catch (Exception e) {
e.printStackTrace();
}*/ catch (IOException e) {
System.out.println("7");
e.printStackTrace();
System.out.println("8");
} catch (NullPointerException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("世界上最真情的相依就是你在try我在catch,无论你发神马脾气我都默默承受并静静的处理,到那时再来期待我们的finally!");
// 当程序执行过程中没有发生异常时的执行流程:1 2 5 6 世界上...
// 当程序执行过程中发生异常又没有手动处理空指针异常时的执行流程:1 3 4 5 空指针异常导致程序终止
// 当程序执行过程中发生异常并且手动处理空指针异常时的执行流程: 1 3 4 5 世界上...
// 手动处理异常和没有处理的区别:代码是否可以继续向下执行
}
}
手动处理异常和没有处理的区别:代码是否可以继续向下执行
笔试考点
public class ExceptionFinallyTest {
// 笔试考点
public static int test() {
try {
int[] arr = new int[5];
System.out.println(arr[5]);
return 0;
} catch (ArrayIndexOutOfBoundsException e) {
e.printStackTrace();
return 1;
} finally {
return 2; // 提交结束方法并返回数据
}
}
public static void main(String[] args) {
int test = test();
System.out.println("test = " + test); // 2
}
}
解析:test方法中发生数组下标越界异常,进入catch语句,在执行return 1;语句时,本应返回1并结束方法体,但下面有finally语句必,因此finally语句会在catch语句结束之前强行执行,finally语句中return 2;返回2并结束方法体,即抢先结束方法体,因此最后test = 2
1.5 异常的抛出(甩锅)
基本概念
在某些特殊情况下有些异常不能处理或者不便于处理时,就可以将该异常转移给该方法的调用者,这种方法就叫异常的抛出。当方法执行时出现异常,则底层生成一个异常类对象抛出,此时异常代码后续的代码就不再执行。
语法格式
// 访问权限 返回值类型 方法名称(形参列表) throws 异常类型1,异常类型2,...{ 方法体; }
public void show() throws IOException{}
方法重写的原则
a.要求方法名相同、参数列表相同以及返回值类型相同,从jdk1.5开始支持返回子类类型;
b.要求方法的访问权限不能变小,可以相同或者变大;
c.要求方法不能抛出更大的异常;
注意
子类重写的方法不能抛出更大的异常、不能抛出平级不一样的异常,但可以抛出一样的异常、更小的异常以及不抛出异常。
经验分享
若父类中被重写的方法没有抛出异常时,则子类中重写的方法只能进行异常的捕获处理。
若一个方法内部又以递进方式分别调用了好几个其它方法,则建议这些方法内可以使用抛出的方法处理到最后一层进行捕获方式处理。
1.6 自定义异常
基本概念
当需要在程序中表达年龄不合理的情况时,而Java官方又没有提供这种针对性的异常,此时就需要程序员自定义异常加以描述。
实现流程
a.自定义xxxException异常类继承Exception类或者其子类。
b.提供两个版本的构造方法,一个是无参构造方法,另外一个是字符串作为参数的构造方法。
public class AgeException extends Exception {
static final long serialVersionUID = 7818375828146090155L; // 序列化的版本号 与序列化操作有关系
public AgeException() {
}
public AgeException(String message) {
super(message);
}
}
person类:
public class Person {
private String name;
private int age;
public Person() {
}
public Person(String name, int age) throws AgeException {
setName(name);
setAge(age);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) throws AgeException {
if (age > 0 && age < 150) {
this.age = age;
} else {
//System.out.println("年龄不合理哦!!!");
throw new AgeException("年龄不合理哦!!!");
}
}
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
测试类:
public class PersonTest {
public static void main(String[] args) {
Person p1 = null;
try {
p1 = new Person("zhangfei", -30);
} catch (AgeException e) {
e.printStackTrace();
}
System.out.println("p1 = " + p1); // p1 = null
}
}
分析:
Person类中setAge方法抛出异常,谁调用就抛给谁,构造方法调用,继续抛出异常,接着测试类中main方法调用,new对象时发生异常,因为不建议面main方法抛出异常,所以进行异常的手动处理,即try/catch语句,最后并没有new出对象,故p1 = null
若就地处理,即在setAge方法时,不抛出异常,而是直接进行手动处理,那么后续的构造方法和main方法也不用抛出异常,可正常执行,那么是可以new对象的,不过年龄是默认值0,p1 = Person{name=‘zhangfei’, age=0}
异常的产生
// 格式:throw new 异常类型(实参);
throw new AgeException("年龄不合理!!!");
Java采用的异常处理机制是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅,并易于维护。
异常小结:
2. File类(重点)
2.1 概念
java.io.File类主要用于描述文件或目录路径的抽象表示信息,可以获取文件或目录的特征信息,如:大小等。
2.2 常用的方法

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
public class FileTest {
// 自定义成员方法实现指定目录以及子目录中所有内容的打印
public static void show(File file) {
// 获取目录f3下的所有内容并记录到一维数组中
File[] filesArray = file.listFiles();
// 遍历数组
for (File tf: filesArray) {
String name = tf.getName();
// 判断是否为文件,若是则直接打印文件名称
if (tf.isFile()) {
System.out.println(name);
}
// 若是目录,则使用[]将目录名称括起来,递归打印
if (tf.isDirectory()) {
System.out.println("[" + name + "]");
show(tf);
}
}
}
public static void main(String[] args) throws IOException {
// 1.构造File类型的对象与d:/a.txt文件关联
File f1 = new File("d:/a.txt");
// 2.若文件存在则获取文件的相关特征信息并打印后删除文件
if (f1.exists()) {
System.out.println("文件的名称是:" + f1.getName());
System.out.println("文件的大小是:" + f1.length());
Date d1 = new Date(f1.lastModified());
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("文件的最后一次修改时间:" + sdf.format(d1));
// 绝对路径: 主要指以根目录开始的路径信息,如:c:/ d:/ /..
// 相对路径: 主要指以当前目录所在位置开始的路径信息,如:./ ../ 相对路径
System.out.println("文件的绝对路径信息是:" + f1.getAbsolutePath());
System.out.println(f1.delete()? "文件删除成功": "文件删除失败");
} else {
// 3.若文件不存在则创建新的空文件
System.out.println(f1.createNewFile()? "文件创建成功": "文件创建失败!");
}
System.out.println("---------------------------------------------------------");
// 4.实现目录的删除和创建
File f2 = new File("d:/捣乱/猜猜我是谁/你猜我猜不猜/死鬼");
if (f2.exists()) {
System.out.println("目录名称是:" + f2.getName());
System.out.println(f2.delete()? "目录删除成功": "目录删除失败");
} else {
//System.out.println(f2.mkdir()? "目录创建成功": "目录创建失败"); // 创建单层目录
System.out.println(f2.mkdirs()? "目录创建成功": "目录创建失败"); // 创建多层目录
}
System.out.println("---------------------------------------------------------");
// 5.实现将指定目录中的所有内容打印出来
File f3 = new File("d:/捣乱");
// 获取目录f3下的所有内容并记录到一维数组中
File[] filesArray = f3.listFiles();
// 遍历数组
for (File tf: filesArray) {
String name = tf.getName();
// 判断是否为文件,若是则直接打印文件名称
if (tf.isFile()) {
System.out.println(name);
}
// 若是目录,则使用[]将目录名称括起来
if (tf.isDirectory()) {
System.out.println("[" + name + "]");
}
}
System.out.println("---------------------------------------------------------");
// 6.实现目录中所有内容获取的同时进行过滤
// 匿名内部类的语法格式:接口/父类类型 引用变量名 = new 接口/父类类型() { 方法的重写 };
/*FileFilter fileFilter = new FileFilter() {
@Override
public boolean accept(File pathname) {
// 若文件名是以.avi为结尾,则返回true表示保留 否则返回false就是表示丢弃
return pathname.getName().endsWith(".avi");
}
};*/
// Lambda表达式的格式:(参数列表) -> {方法体}
FileFilter fileFilter = (File pathname) -> {return pathname.getName().endsWith(".avi");};
File[] filesArray2 = f3.listFiles(fileFilter);
for (File tf : filesArray2) {
System.out.println(tf);
}
System.out.println("---------------------------------------------------------");
// 7.使用递归的思想获取目录以及子目录中的内容
show(new File("d:/捣乱"));
}
}
二. IO流
1. IO流的概念
● IO就是Input和Output的简写,也就是输入和输出的含义。
● IO流就是指读写数据时像流水一样从一端流到另外一端,因此得名为“流"。
2. 基本分类
● 按照读写数据的基本单位不同,分为 字节流 和 字符流。
其中字节流主要指以字节为单位进行数据读写的流,可以读写任意类型的文件。
其中字符流主要指以字符(2个字节)为单位进行数据读写的流,只能读写文本文件。
● 按照读写数据的方向不同,分为 输入流 和 输出流(站在程序的角度)。
其中输入流主要指从文件中读取数据内容输入到程序中,也就是读文件。
其中输出流主要指将程序中的数据内容输出到文件中,也就是写文件。
● 按照流的角色不同分为节点流和处理流。
其中节点流主要指直接和输入输出源对接的流。
其中处理流主要指需要建立在节点流的基础之上的流。
3. 体系结构

重点掌握:
4. 相关流的详解
4.1 FileWriter类(重点)
基本概念
java.io.FileWriter类主要用于将文本内容写入到文本文件
常用的方法
FileWriter fw = new FileWriter(“d:/a.txt”);
新建一个输出流对象,相当于向指定目录文件搭建一个管道,通过管道向文件输入数据
void flush() 方法,用于清除管道残留,刷新管道
void close() 方法,关闭流对象,相当于撤走管道,同时有刷新管道的功能
import java.io.FileWriter;
import java.io.IOException;
public class FileWriterTest {
public static void main(String[] args) {
// 选中代码后可以使用 ctrl+alt+t 来生成异常的捕获代码等
FileWriter fw = null;
try {
// 1.构造FileWrite类型的对象与d:/a.txt文件关联
// 若文件不存在,该流会自动创建新的空文件
// 若文件存在,该流会清空文件中的原有内容
fw = new FileWriter("d:/a.txt");
// 以追加的方式创建对象去关联文件
// 若文件不存在则自动创建新的空文件,若文件存在则保留原有数据内容
// fw = new FileWriter("d:/a.txt", true);
// 2.通过流对象写入数据内容 每当写入一个字符后则文件中的读写位置向后移动一位
fw.write('a');
// 准备一个字符数组
char[] cArr = new char[]{'h', 'e', 'l', 'l', 'o'};
// 将字符数组中的一部分内容写入进去
fw.write(cArr, 1, 3); // ell
// 将整个字符数组写进去
fw.write(cArr); // hello
// 刷新流
fw.flush();
System.out.println("写入数据成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流对象并释放有关的资源
if (null != fw) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.2 FileReader类(重点)
基本概念
java.io.FileReader类主要用于从文本文件读取文本数据内容
常用的方法
import java.io.FileReader;
import java.io.IOException;
public class FileReaderTest {
public static void main(String[] args) {
FileReader fr = null;
try {
// 1.构造FileReader类型的对象与d:/a.txt文件关联
//fr = new FileReader("d:/a.txt");
fr = new FileReader("d:/b.txt");
// 2.读取数据内容并打印
/*
int res = fr.read();
System.out.println("读取到的单个字符是:" + (char)res); // 'a'
*/
int res = 0;
while ((res = fr.read()) != -1) {
System.out.println("读取到的单个字符是:" + (char)res + ",对应的编号是:" + res);
}
// 准备一个字符数组来保存读取到的数据内容
// char[] cArr = new char[5];
// 期望读满字符数组中的一部分空间,也就是读取3个字符放入数组cArr中下标从1开始的位置上
/*int res = fr.read(cArr, 1, 3);
System.out.println("实际读取到的字符个数是:" + res); // 3
for (char cv : cArr) {
System.out.println("读取到的单个字符是:" + (char)cv); // 啥也没有 a e l 啥也没有
}*/
// 期望读满整个字符数组
/*int res = fr.read(cArr);
System.out.println("实际读取到的字符个数是:" + res); // 5
for (char cv : cArr) {
System.out.println("读取到的单个字符是:" + (char)cv); // a e l l h
}*/
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流对象并释放有关的资源
if (null != fr) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
字符流实现文件拷贝:
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class FileCharCopyTest {
public static void main(String[] args) {
FileReader fr = null;
FileWriter fw = null;
try {
// 1.创建FileReader类型的对象与d:/a.txt文件关联
fr = new FileReader("d:/a.txt");
//fr = new FileReader("d:/03 IO流的框架图.png");
// 2.创建FileWriter类型的对象与d:/b.txt文件关联
fw = new FileWriter("d:/b.txt");
//fw = new FileWriter("d:/IO流的框架图.png"); 拷贝图片文件失败!!!
// 3.不断地从输入流中读取数据内容并写入到输出流中
System.out.println("正在玩命地拷贝...");
int res = 0;
while ((res = fr.read()) != -1) {
fw.write(res);
}
System.out.println("拷贝文件成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源
if (null != fw) {
try {
fw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != fr) {
try {
fr.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.3 FileOutputStream类(重点)
基本概念
java.io.FileOutputStream类主要用于将图像数据之类的原始字节流写入到输出流中
常用的方法
4.4 FileInputStream类(重点)
基本概念
java.io.FileInputStream类主要用于从输入流中以字节流的方式读取图像数据等
常用的方法
字节流实现文件的拷贝
三种方式进行拷贝:
方式一:以单个字节为单位进行拷贝,也就是每次读取一个字节后再写入一个字节
缺点:文件稍大时,拷贝的效率很低
方式二:准备一个和文件大小一样的缓冲区,一次性将文件中的所有内容取出到缓冲区然后一次性写入进去
缺点:若文件过大时,无法申请和文件大小一样的缓冲区,真实物理内存不足
方式三:准备一个相对适当的缓冲区,分多次将文件拷贝完成
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class FileByteCopyTest {
public static void main(String[] args) {
// 获取当前系统时间距离1970年1月1日0时0分0秒的毫秒数
long g1 = System.currentTimeMillis();
FileInputStream fis = null;
FileOutputStream fos = null;
try {
// 1.创建FileInputStream类型的对象与d:/03 IO流的框架图.png文件关联
//fis = new FileInputStream("d:/03 IO流的框架图.png");
fis = new FileInputStream("d:/02_IO流的框架结构.mp4");
// 2.创建FileOutputStream类型的对象与d:/IO流的框架图.png文件关联
//fos = new FileOutputStream("d:/IO流的框架图.png");
fos = new FileOutputStream("d:/IO流的框架结构.mp4");
// 3.不断地从输入流中读取数据内容并写入到输出流中
System.out.println("正在玩命地拷贝...");
// 方式一:以单个字节为单位进行拷贝,也就是每次读取一个字节后再写入一个字节
// 缺点:文件稍大时,拷贝的效率很低
/*int res = 0;
while ((res = fis.read()) != -1) {
fos.write(res);
}*/
// 方式二:准备一个和文件大小一样的缓冲区,一次性将文件中的所有内容取出到缓冲区然后一次性写入进去
// 缺点:若文件过大时,无法申请和文件大小一样的缓冲区,真实物理内存不足
/*int len = fis.available();
System.out.println("获取到的文件大小是:" + len);
byte[] bArr = new byte[len];
int res = fis.read(bArr);
System.out.println("实际读取到的文件大小是:" + res);
fos.write(bArr);*/
// 方式三:准备一个相对适当的缓冲区,分多次将文件拷贝完成
byte[] bArr = new byte[1024];
int res = 0;
while ((res = fis.read(bArr)) != -1) {
fos.write(bArr, 0, res);
}
System.out.println("拷贝文件成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源
if (null != fos) {
try {
fos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != fis) {
try {
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
long g2 = System.currentTimeMillis();
System.out.println("使用文件流拷贝视频文件消耗的时间为:" + (g2-g1)); // 165
}
}
4.5 BufferedOutputStream类(重点)
基本概念
java.io.BufferedOutputStream类主要用于描述缓冲输出流,此时不用为写入的每个字节调用底层系统。
常用的方法
4.6 BufferedInputStream类(重点)
基本概念
java.io.BufferedInputStream类主要用于描述缓冲输入流
常用的方法
缓冲字节流实现文件拷贝(效率较高):
import java.io.*;
public class BufferedByteCopyTest {
public static void main(String[] args) {
// 获取当前系统时间距离1970年1月1日0时0分0秒的毫秒数
long g1 = System.currentTimeMillis();
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
try {
// 1.创建BufferedInputStream类型的对象与d:/02_IO流的框架结构.mp4文件关联
bis = new BufferedInputStream(new FileInputStream("d:/02_IO流的框架结构.mp4"));
// 2.创建BufferedOuputStream类型的对象与d:/IO流的框架结构.mp4文件关联
bos = new BufferedOutputStream(new FileOutputStream("d:/IO流的框架结构.mp4"));
// 3.不断地从输入流中读取数据并写入到输出流中
System.out.println("正在玩命地拷贝...");
byte[] bArr = new byte[1024];
int res = 0;
while ((res = bis.read(bArr)) != -1) {
bos.write(bArr, 0, res);
}
System.out.println("拷贝文件成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源
if (null != bos) {
try {
bos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != bis) {
try {
bis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
long g2 = System.currentTimeMillis();
System.out.println("使用缓冲区拷贝视频文件消耗的时间为:" + (g2-g1)); // 44
}
}
4.7 BufferedWriter类(重点)
基本概念
java.io.BufferedWriter类主要用于写入单个字符、字符数组以及字符串到输出流中
常用的方法
4.8 BufferedReader类(重点)
基本概念
java.io.BufferedReader类用于从输入流中读取单个字符、字符数组以及字符串
常用的方法
缓冲字符流实现文件拷贝(效率高):
import java.io.*;
public class BufferedCharCopyTest {
public static void main(String[] args) {
BufferedReader br = null;
BufferedWriter bw = null;
try {
// 1.创建BufferedReader类型的对象与d:/a.txt文件关联
br = new BufferedReader(new FileReader("d:/a.txt"));
// 2.创建BufferedWriter类型的对象与d:/b.txt文件关联
bw = new BufferedWriter(new FileWriter("d:/b.txt"));
// 3.不断地从输入流中读取一行字符串并写入到输出流中
System.out.println("正在玩命地拷贝...");
String str = null;
while ((str = br.readLine()) != null) {
bw.write(str);
bw.newLine(); // 当前系统中的行分隔符是:\r\n
}
System.out.println("拷贝文件成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源
if (null != bw) {
try {
bw.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.9 PrintStream类
基本概念
java.io.PrintStream类主要用于更加方便地打印各种数据内容(字节流打印)
常用的方法
4.10 PrintWriter类
基本概念
java.io.PrintWriter类主要用于将对象的格式化形式打印到文本输出流(字符流打印)
常用的方法
4.11 OutputStreamWriter类
基本概念
java.io.OutputStreamWriter类主要用于实现从字符流到字节流的转换
常用的方法
4.12 InputStreamReader类
基本概念
java.io.InputStreamReader类主要用于实现从字节流到字符流的转换
常用的方法
案例:聊天记录的文件显示
不断地提示用户输入要发送的内容,若发送的内容是"bye"则聊天结束,否则将用户输入的内容写入到文件d:/a.txt中。
要求使用BufferedReader类来读取键盘的输入 System.in代表键盘输入
要求使用PrintStream类负责将数据写入文件
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.Date;
public class PrintStreamChatTest {
public static void main(String[] args) {
// 由手册可知:构造方法需要的是Reader类型的引用,但Reader类是个抽象类,实参只能传递子类的对象 字符流
// 由手册可知: System.in代表键盘输入, 而且是InputStream类型的 字节流
BufferedReader br = null;
PrintStream ps = null;
try {
br = new BufferedReader(new InputStreamReader(System.in));
ps = new PrintStream(new FileOutputStream("d:/a.txt", true));
// 声明一个boolean类型的变量作为发送方的代表
boolean flag = true;
while(true) {
// 1.提示用户输入要发送的聊天内容并使用变量记录
System.out.println("请" + (flag? "张三": "李四") + "输入要发送的聊天内容:");
String str = br.readLine();
// 2.判断用户输入的内容是否为"bye",若是则聊天结束
if ("bye".equals(str)) {
System.out.println("聊天结束!");
break;
}
// 3.若不是则将用户输入的内容写入到文件d:/a.txt中
//else {
// 获取当前系统时间并调整格式
Date d1 = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
ps.println(sdf.format(d1) + (flag?" 张三说:":" 李四说:") + str);
//}
flag = !flag;
}
ps.println(); // 写入空行 与之前的聊天记录隔开
ps.println();
ps.println();
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源
if (null != ps) {
ps.close();
}
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.13 字符编码
(1)编码表的由来
计算机只能识别二进制数据,早期就是电信号。为了方便计算机可以识别各个国家的文字,就需要将各个国家的文字采用数字编号的方式进行描述并建立对应的关系表,该表就叫做编码表。
(2)常见的编码表
● ASCII:美国标准信息交换码, 使用一个字节的低7位二位进制进行表示。
● ISO8859-1:拉丁码表,欧洲码表,使用一个字节的8位二进制进行表示。
● GB2312:中国的中文编码表,最多使用两个字节16位二进制为进行表示。
● GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多使用两个字节16位二进制位表示。
● Unicode:国际标准码,融合了目前人类使用的所有字符,为每个字符分配唯一的字符码。所有的文字都用两个字节16位二进制位来表示。
(3)编码的发展
● 面向传输的众多 UTF(UCS Transfer Format)标准出现了,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码并使编码无国界,这样就可以显示全世界上所有文化的字符了。
● Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储成什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16。
● UTF-8:变长的编码方式,可用1-4个字节来表示一个字符。
4.14 DataOutputStream类(了解)
基本概念
java.io.DataOutputStream类主要用于以适当的方式将基本数据类型写入输出流中
常用的方法
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class DataOutputStreamTest {
public static void main(String[] args) {
DataOutputStream dos = null;
try {
// 1.创建DataOutputStream类型的对象与d:/a.txt文件关联
dos = new DataOutputStream(new FileOutputStream("d:/a.txt"));
// 2.准备一个整数数据66并写入输出流
// 66: 0000 0000 ... 0100 0010 => B
int num = 66;
//dos.writeInt(num); // 写入4个字节
dos.write(num); // 写入1个字节
System.out.println("写入数据成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流对象并释放有关的资源
if (null != dos) {
try {
dos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.15 DataInputStream类(了解)
基本概念
java.io.DataInputStream类主要用于从输入流中读取基本数据类型的数据
常用的方法
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.IOException;
public class DataInputStreamTest {
public static void main(String[] args) {
DataInputStream dis = null;
try {
// 1.创建DataInputStream类型的对象与d:/a.txt文件关联
dis = new DataInputStream(new FileInputStream("d:/a.txt"));
// 2.从输入流中读取一个整数并打印
//int res = dis.readInt(); // 读取4个字节
int res = dis.read(); // 读取1个字节
System.out.println("读取到的整数数据是:" + res); // 66
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流对象并释放有关的资源
if (null != dis) {
try {
dis.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.16 ObjectOutputStream类(重点)
基本概念
java.io.ObjectOutputStream类主要用于将一个对象的所有内容整体写入到输出流中。
只能将支持 java.io.Serializable 接口的对象写入流中。
类通过实现 java.io.Serializable 接口以启用其序列化功能。
所谓序列化主要指将一个对象需要存储的相关信息有效组织成字节序列的转化过程。
常用的方法
User类:
public class User implements java.io.Serializable {
private static final long serialVersionUID = -5814716593800822421L;
private String userName; // 用户名
private String password; // 密码
private transient String phoneNum; // 手机号 表示该成员变量不参与序列化操作
public User() {
}
public User(String userName, String password, String phoneNum) {
this.userName = userName;
this.password = password;
this.phoneNum = phoneNum;
}
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getPhoneNum() {
return phoneNum;
}
public void setPhoneNum(String phoneNum) {
this.phoneNum = phoneNum;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", password='" + password + '\'' +
", phoneNum='" + phoneNum + '\'' +
'}';
}
}
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
public class ObjectOutputStreamTest {
public static void main(String[] args) {
ObjectOutputStream oos = null;
try {
// 1.创建ObjectOutputStream类型的对象与d:/a.txt文件关联
oos = new ObjectOutputStream(new FileOutputStream("d:/a.txt"));
// 2.准备一个User类型的对象并初始化
User user = new User("qidian", "123456", "13511258688");
// 3.将整个User类型的对象写入输出流
oos.writeObject(user);
System.out.println("写入对象成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭流对象并释放有关的资源
if (null != oos) {
try {
oos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
4.17 ObjectInputStream类(重点)
基本概念
java.io.ObjectInputStream类主要用于从输入流中一次性将对象整体读取出来。
所谓反序列化主要指将有效组织的字节序列恢复为一个对象及相关信息的转化过程。
常用的方法
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
public class ObjectInputStreamTest {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
// 1.创建ObjectInputStream类型的对象与d:/a.txt文件关联
ois = new ObjectInputStream(new FileInputStream("d:/a.txt"));
// 2.从输入流中读取一个对象并打印
Object obj = ois.readObject();
System.out.println("读取到的对象是:" + obj); // qidian 123456 13511258688 null
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} finally {
// 3.关闭流对象并释放有关的资源
if (null != ois) {
try {
ois.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
序列化版本号
序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)。
transient关键字
transient是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的。
经验的分享
当希望将多个对象写入文件时,通常建议将多个对象放入一个集合中,然后将集合这个整体看做一个对象写入输出流中,此时只需要调用一次readObject方法就可以将整个集合的数据读取出来,从而避免了通过返回值进行是否达到文件末尾的判断。
4.18 RandomAccessFile类
基本概念
java.io.RandomAccessFile类主要支持对随机访问文件的读写操作
常用的方法
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) {
RandomAccessFile raf = null;
try {
// 1.创建RandomAccessFile类型的对象与d:/a.txt文件关联
raf = new RandomAccessFile("d:/a.txt", "rw");
// 2.对文件内容进行随机读写操作
// 设置距离文件开头位置的偏移量,从文件开头位置向后偏移3个字节 aellhello
raf.seek(3);
int res = raf.read();
System.out.println("读取到的单个字符是:" + (char)res); // a l
res = raf.read();
System.out.println("读取到的单个字符是:" + (char)res); // h 指向了e
raf.write('2'); // 执行该行代码后覆盖了字符'e'
System.out.println("写入数据成功!");
} catch (IOException e) {
e.printStackTrace();
} finally {
// 3.关闭流对象并释放有关的资源
if (null != raf) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
三. 多线程
1. 基本概念
程序和进程的概念
● 程序 - 数据结构 + 算法,主要指存放在硬盘上的可执行文件。
● 进程 - 主要指运行在内存中的可执行文件。
● 目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务,但进程是重量级的,也就是新建一个进程会消耗CPU和内存空间等系统资源,因此进程的数量比较局限。
线程的概念
● 为了解决上述问题就提出线程的概念,线程就是进程内部的程序流,也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的,线程是轻量的,新建线程会共享所在进程的系统资源,因此目前主流的开发都是采用多线程。
● 多线程是采用时间片轮转法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制。
2. 线程的创建(重中之重)
2.1 Thread类的概念
java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例。
Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性。
2.2 创建方式
(1)自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法。
public class SubThreadRun extends Thread {
@Override
public void run() {
// 打印1 ~ 20之间的所有整数
for (int i = 1; i <= 20; i++) {
System.out.println("run方法中:i = " + i); // 1 2 ... 20
}
}
}
public class SubThreadRunTest {
public static void main(String[] args) {
// 1.声明Thread类型的引用指向子类类型的对象
Thread t1 = new SubThreadRun();
// 2.调用run方法测试,本质上就是相当于对普通成员方法的调用,因此执行流程就是run方法的代码执行完毕后才能继续向下执行
//t1.run();
// 用于启动线程,Java虚拟机会自动调用该线程类中的run方法
// 相当于又启动了一个线程,加上执行main方法的线程是两个线程
t1.start();
// 打印1 ~ 20之间的所有整数
for (int i = 1; i <= 20; i++) {
System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20
}
}
}
(2)自定义类实现Runnable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法。
public class SubRunnableRun implements Runnable {
@Override
public void run() {
// 打印1 ~ 20之间的所有整数
for (int i = 1; i <= 20; i++) {
System.out.println("run方法中:i = " + i); // 1 2 ... 20
}
}
}
public class SubRunnableRunTest {
public static void main(String[] args) {
// 1.创建自定义类型的对象,也就是实现Runnable接口类的对象
SubRunnableRun srr = new SubRunnableRun();
// 2.使用该对象作为实参构造Thread类型的对象
// 由源码可知:经过构造方法的调用之后,Thread类中的成员变量target的数值为srr。
Thread t1 = new Thread(srr);
// 3.使用Thread类型的对象调用start方法
// 若使用Runnable引用构造了线程对象,调用该方法(run)时最终调用接口中的版本
// 由run方法的源码可知:if (target != null) {
// target.run();
// }
// 此时target的数值不为空这个条件成立,执行target.run()的代码,也就是srr.run()的代码
t1.start();
//srr.start(); Error
// 打印1 ~ 20之间的所有整数
for (int i = 1; i <= 20; i++) {
System.out.println("-----------------main方法中:i = " + i); // 1 2 ... 20
}
}
}
2.3 相关的方法

2.4 执行流程
● 执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程。
● main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成了2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响。
● 当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束。
● 两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定。
2.5 方式的比较
继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其它类,而实现Runnable接口的方式代码复杂,但不影响该类继承其它类以及实现其它接口,因此以后的开发中推荐使用第二种方式。
2.6 匿名内部类的方式
使用匿名内部类的方式来创建和启动线程。
public class ThreadNoNameTest {
public static void main(String[] args) {
// 匿名内部类的语法格式:父类/接口类型 引用变量名 = new 父类/接口类型() { 方法的重写 };
// 1.使用继承加匿名内部类的方式创建并启动线程
/*Thread t1 = new Thread() {
@Override
public void run() {
System.out.println("张三说:在吗?");
}
};
t1.start();*/
// 代码优化
new Thread() {
@Override
public void run() {
System.out.println("张三说:在吗?");
}
}.start();
// 2.使用实现接口加匿名内部类的方式创建并启动线程
/*Runnable ra = new Runnable() {
@Override
public void run() {
System.out.println("李四说:不在。");
}
};
Thread t2 = new Thread(ra);
t2.start();*/
// 代码优化
/*new Thread(new Runnable() {
@Override
public void run() {
System.out.println("李四说:不在。");
}
}).start();*/
// Java8开始支持lambda表达式: (形参列表)->{方法体;}
/*Runnable ra = ()-> System.out.println("李四说:不在。");
new Thread(ra).start();*/
// 进一步优化
new Thread(()-> System.out.println("李四说:不在。")).start();
}
}
3. 线程的生命周期(熟悉)

● 新建状态 - 使用new关键字创建之后进入的状态,此时线程并没有开始执行。
● 就绪状态 - 调用start方法后进入的状态,此时线程还是没有开始执行。
● 运行状态 - 使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完毕后任务没有完成时回到就绪状态。
● 消亡状态 - 当线程的任务执行完成后进入的状态,此时线程已经终止。
● 阻塞状态 - 当线程执行的过程中发生了阻塞事件进入的状态,如:sleep方法。
阻塞状态解除后进入就绪状态。
4. 线程的编号和名称(熟悉)

案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称,然后将线程的名称修改为"zhangfei"后再次打印编号和名称。
要求在main方法中也要打印主线程的编号和名称。
public class ThreadIdNameTest extends Thread {
public ThreadIdNameTest(String name) {
super(name); // 表示调用父类的构造方法
}
@Override
public void run() {
System.out.println("子线程的编号是:" + getId() + ",名称是:" + getName()); // 14 Thread-0 guanyu
// 修改名称为"zhangfei"
setName("zhangfei");
System.out.println("修改后子线程的编号是:" + getId() + ",名称是:" + getName()); // 14 zhangfei
}
public static void main(String[] args) {
ThreadIdNameTest tint = new ThreadIdNameTest("guanyu");
tint.start();
// 获取当前正在执行线程的引用,当前正在执行的线程是主线程,也就是获取主线程的引用
Thread t1 = Thread.currentThread();
System.out.println("主线程的编号是:" + t1.getId() + ", 名称是:" + t1.getName());
}
}
5. 常用的方法(重点)

sleep方法测试:
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Date;
public class ThreadSleepTest extends Thread {
// 声明一个布尔类型的变量作为循环是否执行的条件
private boolean flag = true;
// 子类中重写的方法不能抛出更大的异常
@Override
public void run() {
// 每隔一秒获取一次系统时间并打印,模拟时钟的效果
while (flag) {
// 获取当前系统时间并调整格式打印
/*LocalDateTime now = LocalDateTime.now();
DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
System.out.println(now.format(dtf));*/
Date d1 = new Date();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println(sdf.format(d1));
// 睡眠1秒钟
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadSleepTest tst = new ThreadSleepTest();
tst.start();
// 主线程等待5秒后结束子线程
System.out.println("主线程开始等待...");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 停止子线程 过时 不建议使用
//tst.stop();
tst.flag = false;
System.out.println("主线程等待结束!");
}
}
getPriority方法测试:
public class ThreadPriorityTest extends Thread {
@Override
public void run() {
//System.out.println("子线程的优先级是:" + getPriority()); // 5 10 优先级越高的线程不一定先执行。
for (int i = 0; i < 20; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
ThreadPriorityTest tpt = new ThreadPriorityTest();
// 设置子线程的优先级
tpt.setPriority(Thread.MAX_PRIORITY);
tpt.start();
Thread t1 = Thread.currentThread();
//System.out.println("主线程的优先级是:" + t1.getPriority()); // 5 普通的优先级
for (int i = 0; i < 20; i++) {
System.out.println("--主线程中:i = " + i);
}
}
}
优先级越高的线程不一定先执行。
join方法测试:
public class ThreadJoinTest extends Thread {
@Override
public void run() {
// 模拟倒数10个数的效果
System.out.println("倒计时开始...");
for (int i = 10; i > 0; i--) {
System.out.println(i);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("新年快乐!");
}
public static void main(String[] args) {
ThreadJoinTest tjt = new ThreadJoinTest();
tjt.start();
// 主线程开始等待
System.out.println("主线程开始等待...");
try {
// 表示当前正在执行的线程对象等待调用线程对象,也就是主线程等待子线程终止
//tjt.join();
tjt.join(5000); // 最多等待5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
//System.out.println("终于等到你,还好没放弃!");
System.out.println("可惜不是你,陪我到最后!");
}
}
守护线程相关方法测试:
public class ThreadDaemonTest extends Thread {
@Override
public void run() {
//System.out.println(isDaemon()? "该线程是守护线程": "该线程不是守护线程"); // 默认不是守护线程
// 当子线程不是守护线程时,虽然主线程先结束了,但是子线程依然会继续执行,直到打印完毕所有数据为止
// 当子线程是守护线程时,当主线程结束后,则子线程随之结束
for (int i = 0; i < 50; i++) {
System.out.println("子线程中:i = " + i);
}
}
public static void main(String[] args) {
ThreadDaemonTest tdt = new ThreadDaemonTest();
// 必须在线程启动之前设置子线程为守护线程
tdt.setDaemon(true);
tdt.start();
for (int i = 0; i < 20; i++) {
System.out.println("-------主线程中:i = " + i);
}
}
}
案例题目
编程创建两个线程,线程一负责打印1 ~ 100之间的所有奇数,其中线程二负责打印1 ~ 100之间的所有偶数。
在main方法启动上述两个线程同时执行,主线程等待两个线程终止。
方法一(继承Thread类的方法):
public class SubThread1 extends Thread {
@Override
public void run() {
// 打印1 ~ 100之间的所有奇数
for (int i = 1; i <= 100; i += 2) {
System.out.println("子线程一中: i = " + i);
}
}
}
public class SubThread2 extends Thread {
@Override
public void run() {
// 打印1 ~ 100之间的所有偶数
for (int i = 2; i <= 100; i += 2) {
System.out.println("------子线程二中: i = " + i);
}
}
}
public class SubThreadTest {
public static void main(String[] args) {
SubThread1 st1 = new SubThread1();
SubThread2 st2 = new SubThread2();
st1.start();
st2.start();
System.out.println("主线程开始等待...");
try {
st1.join();
st2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程等待结束!");
}
}
方式二(实现接口Runnable的方法):
public class SubRunnable1 implements Runnable {
@Override
public void run() {
// 打印1 ~ 100之间的所有奇数
for (int i = 1; i <= 100; i += 2) {
System.out.println("子线程一中: i = " + i);
}
}
}
public class SubRunnable2 implements Runnable {
@Override
public void run() {
// 打印1 ~ 100之间的所有偶数
for (int i = 2; i <= 100; i += 2) {
System.out.println("------子线程二中: i = " + i);
}
}
}
public class SubRunnableTest {
public static void main(String[] args) {
SubRunnable1 sr1 = new SubRunnable1();
SubRunnable2 sr2 = new SubRunnable2();
Thread t1 = new Thread(sr1);
Thread t2 = new Thread(sr2);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程等待结束!");
}
}
6. 线程同步机制(重点)
6.1 基本概念
● 当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制。
● 多个线程并发读写同一个临界资源时会发生线程并发安全问题。
● 异步操作:多线程并发的操作,各自独立运行。
● 同步操作:多线程串行的操作,先后执行的顺序。
public class AccountRunnableTest implements Runnable {
private int balance; // 用于描述账户的余额
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance()); // 600 800
}
}
运行结果:
6.2 解决方案
由程序结果可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理。
引发原因:线程一执行取款时还没来得及将取款后的余额写入后台,线程二就已经开始取款。
解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作。
经验分享:在以后的开发尽量减少串行操作的范围,从而提高效率。
6.3 实现方式
● 在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性,具体方式如下:
● 使用同步代码块的方式实现部分代码的锁定,格式如下:
synchronized(类类型的引用) {
编写所有需要锁定的代码;
}
public class AccountRunnableTest implements Runnable {
private int balance; // 用于描述账户的余额
private Demo dm = new Demo();
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 800
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 600
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 600
}
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance()); // 600
}
}
class Demo{}
运行结果:
用继承Thread类方法实现同步:
public class AccountThreadTest extends Thread {
private int balance; // 用于描述账户的余额
//private Demo dm = new Demo();
private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个
public AccountThreadTest() {
}
public AccountThreadTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
}
}
public static void main(String[] args) {
AccountThreadTest att1 = new AccountThreadTest(1000);
att1.start();
AccountThreadTest att2 = new AccountThreadTest(1000);
att2.start();
System.out.println("主线程开始等待...");
try {
att1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
att2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + att1.getBalance()); // 800
}
}
运行结果:
此时相当于创建了两个账户,余额的确是800
注意:dm必须要求是同一个对象,才能锁住,故dm需要用static关键字修饰
● 使用同步方法的方式实现所有代码的锁定。
直接使用synchronized关键字来修饰整个方法即可
该方式等价于:
synchronized(this) { 整个方法体的代码 }
public class AccountRunnableTest implements Runnable {
private int balance; // 用于描述账户的余额
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public /*synchronized*/ void run() {
// 由源码可知:最终是account对象来调用run方法,因此当前正在调用的对象就是account,也就是说this就是account
synchronized (this) {
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp); // balance = 800 balance = 800
}
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance()); // 600
}
}
运行结果:
两线程调用的是同一个对象account,可以锁住
若调用的不是同一对象,则锁不住
另外此方法不用新建Demo类
AccountRunnableTest account = new AccountRunnableTest(1000);
AccountRunnableTest account2 = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account2);
t1.start();
t2.start();
6.4 静态方法的锁定
当我们对一个静态方法加锁,如:
public synchronized static void xxx(){….}
那么该方法锁的对象是类对象。每个类都有唯一的一个类对象。获取类对象的方式:类名.class
静态方法与非静态方法同时使用了synchronized后它们之间是非互斥关系的。
原因在于:静态方法锁的是类对象而非静态方法锁的是当前方法所属对象。
对于继承Thread方式使用同步方法锁定:
public class AccountThreadTest extends Thread {
private int balance; // 用于描述账户的余额
private static Demo dm = new Demo(); // 隶属于类层级,所有对象共享同一个
public AccountThreadTest() {
}
public AccountThreadTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
// 用synchronized修饰run方法并不能锁住,因为是创建了两个对象,每个对象都可以调用自己的run方法,故需要将run方法提升为类层级
// 又因为run是重写父类方法,不可以加static修饰
// 所以在下面创建test方法,使run方法调用test即可,test方法可以用static和synchronized修饰
@Override
public /*static*/ /*synchronized*/ void run() {
test();
}
public /*synchronized*/ static void test() {
synchronized (AccountThreadTest.class) { // 该类型对应的Class对象,由于类型是固定的,因此Class对象也是唯一的,因此可以实现同步
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
//synchronized (dm) { // ok
//synchronized (new Demo()) { // 锁不住 要求必须是同一个对象
// 1.模拟从后台查询账户余额的过程
int temp = 1000; //getBalance();
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200;
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
//setBalance(temp); // balance = 800 balance = 800
}
}
public static void main(String[] args) {
AccountThreadTest att1 = new AccountThreadTest(1000);
att1.start();
AccountThreadTest att2 = new AccountThreadTest(1000);
att2.start();
System.out.println("主线程开始等待...");
try {
att1.join();
//t2.start(); // 也就是等待线程一取款操作结束后再启动线程二
att2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + att1.getBalance()); // 800
}
}
6.5 注意事项
使用synchronized保证线程同步应当注意:
● 多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用。
● 在使用同步块时应当尽量减少同步范围以提高并发的执行效率。
6.6 线程安全类和不安全类
● StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类。
● Vector类和 Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类。
● Collections.synchronizedList() 和 Collections.synchronizedMap()等方法实现安全。
6.7 死锁的概念
● 线程一执行的代码:
public void run(){
synchronized(a){ //持有对象锁a,等待对象锁b
synchronized(b){
编写锁定的代码;
}
}
}
● 线程二执行的代码:
public void run(){
synchronized(b){ //持有对象锁b,等待对象锁a
synchronized(a){
编写锁定的代码;
}
}
}
● 注意:
在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
6.8 使用Lock(锁)实现线程同步
(1)基本概念
● 从Java5开始提供了更强大的线程同步机制—使用显式定义的同步锁对象来实现。
● java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具。
● 该接口的主要实现类是ReentrantLock类,该类拥有与synchronized相同的并发性,在以后的线程安全控制中,经常使用ReentrantLock类显式加锁和释放锁。
(2)常用的方法

import java.util.concurrent.locks.ReentrantLock;
public class AccountRunnableTest implements Runnable {
private int balance; // 用于描述账户的余额
private ReentrantLock lock = new ReentrantLock(); // 准备了一把锁
public AccountRunnableTest() {
}
public AccountRunnableTest(int balance) {
this.balance = balance;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
@Override
public void run() {
// 开始加锁
lock.lock();
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
// 1.模拟从后台查询账户余额的过程
int temp = getBalance(); // temp = 1000 temp = 1000
// 2.模拟取款200元的过程
if (temp >= 200) {
System.out.println("正在出钞,请稍后...");
temp -= 200; // temp = 800 temp = 800
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("请取走您的钞票!");
} else {
System.out.println("余额不足,请核对您的账户余额!");
}
// 3.模拟将最新的账户余额写入到后台
setBalance(temp);
lock.unlock(); // 实现解锁
}
public static void main(String[] args) {
AccountRunnableTest account = new AccountRunnableTest(1000);
Thread t1 = new Thread(account);
Thread t2 = new Thread(account);
t1.start();
t2.start();
System.out.println("主线程开始等待...");
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("最终的账户余额为:" + account.getBalance()); // 600
}
}
运行结果:
(3)与synchronized方式的比较
● Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动释放。
● Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁。
● 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好。
6.9 Object类常用的方法

案例一:线程之间的通信实现
两个线程轮流打印1~100之间的整数
public class ThreadCommunicateTest implements Runnable {
private int cnt = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
// 每当有一个线程进来后先大喊一声,调用notify方法
notify();
if (cnt <= 100) {
System.out.println("线程" + Thread.currentThread().getName() + "中:cnt = " + cnt);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
cnt++;
// 当前线程打印完毕一个整数后,为了防止继续打印下一个数据,则调用wait方法
try {
wait(); // 当前线程进入阻塞状态,自动释放对象锁,必须在锁定的代码中调用
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
public static void main(String[] args) {
ThreadCommunicateTest tct = new ThreadCommunicateTest();
Thread t1 = new Thread(tct);
t1.start();
Thread t2 = new Thread(tct);
t2.start();
}
}
案例二:生产者消费者模型

仓库类StoreHouse:
/**
* 编程实现仓库类
*/
public class StoreHouse {
private int cnt = 0; // 用于记录产品的数量
public synchronized void produceProduct() {
notify();
if (cnt < 10) {
System.out.println("线程" + Thread.currentThread().getName() + "正在生产第" + (cnt+1) + "个产品...");
cnt++;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public synchronized void consumerProduct() {
notify();
if (cnt > 0) {
System.out.println("线程" + Thread.currentThread().getName() + "消费第" + cnt + "个产品");
cnt--;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者线程类ProduceThread:
/**
* 编程实现生产者线程,不断地生产产品
*/
public class ProduceThread extends Thread {
// 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法 合成复用原则
private StoreHouse storeHouse;
// 为了确保两个线程共用同一个仓库
public ProduceThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
@Override
public void run() {
while (true) {
storeHouse.produceProduct();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者线程类ConsumerThread:
public class ConsumerThread extends Thread {
// 声明一个仓库类型的引用作为成员变量,是为了能调用调用仓库类中的生产方法 合成复用原则
private StoreHouse storeHouse;
// 为了确保两个线程共用同一个仓库
public ConsumerThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
@Override
public void run() {
while (true) {
storeHouse.consumerProduct();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
测试类StoreHouseTest:
public class StoreHouseTest {
public static void main(String[] args) {
// 创建仓库类的对象
StoreHouse storeHouse = new StoreHouse();
// 创建线程类对象并启动
ProduceThread t1 = new ProduceThread(storeHouse);
ConsumerThread t2 = new ConsumerThread(storeHouse);
t1.start();
t2.start();
}
}
运行结果:
6.10 线程池(熟悉)
(1)实现Callable接口
● 从Java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口。
● 常用的方法如下:
(2)FutureTask类
● java.util.concurrent.FutureTask类用于描述可取消的异步计算,该类提供了Future接口的基本实现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果。
● 常用的方法如下:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class ThreadCallableTest implements Callable {
@Override
public Object call() throws Exception {
// 计算1 ~ 10000之间的累加和并打印返回
int sum = 0;
for (int i = 1; i <= 10000; i++) {
sum +=i;
}
System.out.println("计算的累加和是:" + sum); // 50005000
return sum;
}
public static void main(String[] args) {
ThreadCallableTest tct = new ThreadCallableTest();
FutureTask ft = new FutureTask(tct);
Thread t1 = new Thread(ft);
t1.start();
Object obj = null;
try {
obj = ft.get();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程处理方法的返回值是:" + obj); // 50005000
}
}
(3)线程池的由来
● 在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了,即每来一个客户端连接,服务器端就要创建一个新线程。
● 如果访问服务器的客户端很多,那么服务器要不断地创建和销毁线程,这将严重影响服务器的性能。
(4)概念和原理
● 线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接受到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完后不关闭该线程,而是将该线程还回到线程池中。
● 在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务。
(5)相关类和方法
● 从Java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和java.util.concurrent.ExecutorService接口。
● 其中Executors是个工具类和线程池的工厂类,可以创建并返回不同类型的线程池,常用方法如下:
● 其中ExecutorService接口是真正的线程池接口,主要实现类是ThreadPoolExecutor,常用方法如下:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadPoolTest {
public static void main(String[] args) {
// 1.创建一个线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 2.向线程池中布置任务
executorService.submit(new ThreadCallableTest());
// 3.关闭线程池
executorService.shutdown();
}
}
四. 网络编程
1. 网络编程的常识
1.1 七层网络模型
● OSI(Open System Interconnect),即开放式系统互联,是ISO(国际标准化组织)组织在1985年研究的网络互连模型。
● OSI七层模型和TCP/IP五层模型的划分如下:
● 当发送数据时,需要对发送的内容按照上述七层模型进行层层加包后发送出去。
● 当接收数据时,需要对接收的内容按照上述七层模型相反的次序层层拆包并显示出来。
1.2 相关的协议(笔试题)
(1)协议的概念
计算机在网络中实现通信就必须有一些约定或者规则,这种约定和规则就叫做通信协议,通信协议可以对速率、传输代码、代码结构、传输控制步骤、出错控制等制定统一的标准。
(2)TCP协议
传输控制协议(Transmission Control Protocol),是一种面向连接的协议,类似于打电话。
● 建立连接 => 进行通信 => 断开连接
● 在传输前采用"三次握手"方式。
● 在通信的整个过程中全程保持连接,形成数据传输通道。
● 保证了数据传输的可靠性和有序性。
● 是一种全双工的字节流通信方式,可以进行大数据量的传输。
● 传输完毕后需要释放已建立的连接(“四次挥手”),发送数据的效率比较低。
(3)UDP协议
● 用户数据报协议(User Datagram Protocol),是一种非面向连接的协议,类似于写信。
● 在通信的整个过程中不需要保持连接,其实是不需要建立连接。
● 不保证数据传输的可靠性和有序性。
● 是一种全双工的数据报通信方式,每个数据报的大小限制在64K内。
● 发送数据完毕后无需释放资源,开销小,发送数据的效率比较高,速度快。
1.3 IP地址(重点)
● 192.168.1.1 - 是绝大多数路由器的登录地址,主要配置用户名和密码以及Mac过滤。
● IP地址是互联网中的唯一地址标识,本质上是由32位二进制组成的整数,叫做IPv4,当然也有128位二进制组成的整数,叫做IPv6,目前主流的还是IPv4。
● 日常生活中采用点分十进制表示法来进行IP地址的描述,将每个字节的二进制转化为一个十进制整数,不同的整数之间采用小数点隔开。
如:0x01020304 => 1.2.3.4
● 查看IP地址的方式:
Windows系统:在dos窗口中使用ipconfig或ipconfig/all命令即可
Unix/linux系统:在终端窗口中使用ifconfig或/sbin/ifconfig命令即可
● 特殊的地址
本地回环地址(hostAddress):127.0.0.1 主机名(hostName):localhost
1.4 端口号(重点)
● IP地址 - 可以定位到具体某一台设备。
● 端口号 - 可以定位到该设备中具体某一个进程。
● 端口号本质上是16位二进制组成的整数,表示范围是:0 ~ 65535,其中0 ~ 1024之间的端口号通常被系统占用,建议编程从1025开始使用。
● 特殊的端口:
HTTP:80
FTP:21
Oracle:1521
MySQL:3306
Tomcat:8080
● 网络编程需要提供:IP地址 + 端口号,组合在一起叫做网络套接字:Socket。
2. 基于tcp协议的编程模型(重点)
2.1 C/S架构的简介
● 在C/S模式下客户向服务器发出服务请求,服务器接收请求后提供服务。
例如:在一个酒店中,顾客找服务员点菜,服务员把点菜单通知厨师,厨师按点菜单做好菜后让服务员端给客户,这就是一种C/S工作方式。如果把酒店看作一个系统,服务员就是客户端,厨师就是服务器。这种系统分工和协同工作的方式就是C/S的工作方式。
● 客户端部分:为每个用户所专有的,负责执行前台功能。
● 服务器部分:由多个用户共享的信息与功能,招待后台服务。
2.2 编程模型

● 服务器:
(1)创建ServerSocket类型的对象并提供端口号;
(2)等待客户端的连接请求,调用accept()方法;
(3)使用输入输出流进行通信;
(4)关闭Socket;
● 客户端:
(1)创建Socket类型的对象并提供服务器的IP地址和端口号;
(2)使用输入输出流进行通信;
(3)关闭Socket;
2.3 相关类和方法的解析
(1)ServerSocket类
● java.net.ServerSocket类主要用于描述服务器套接字信息(大插排)。
● 常用的方法如下:
(2)Socket类
● java.net.Socket类主要用于描述客户端套接字,是两台机器间通信的端点(小插排)。
● 常用的方法如下:
(3)注意事项
● 客户端 Socket 与服务器端 Socket 对应, 都包含输入和输出流。
● 客户端的socket.getInputStream() 连接于服务器socket.getOutputStream()。
● 客户端的socket.getOutputStream()连接于服务器socket.getInputStream()
案例:多线程实现C/S模型
服务器ServerStringTest:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
public class ServerStringTest {
public static void main(String[] args) {
ServerSocket ss = null;
Socket s = null;
try {
// 1.创建ServerSocket类型的对象并提供端口号
ss = new ServerSocket(8888);
// 2.等待客户端的连接请求,调用accept方法
while(true) {
System.out.println("等待客户端的连接请求...");
// 当没有客户端连接时,则服务器阻塞在accept方法的调用这里
s = ss.accept();
System.out.println("客户端" + s.getInetAddress() + "连接成功!");
// 每当有一个客户端连接成功,则需要启动一个新的线程为之服务
new ServerThread(s).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭Socket并释放有关的资源
if (null != ss) {
try {
ss.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
线程类ServerThread:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
public class ServerThread extends Thread {
private Socket s;
public ServerThread(Socket s) {
this.s = s;
}
@Override
public void run () {
BufferedReader br = null;
PrintStream ps = null;
try {
//3.使用输入输出流进行通信;
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
ps = new PrintStream(s.getOutputStream());
while (true) {
// 实现对客户端发来字符串内容的接受并打印
String s1 = br.readLine();
InetAddress inetAddress = s.getInetAddress();
System.out.println("客户端" + inetAddress + "发来的字符串内容是:" + s1);
if ("bye".equalsIgnoreCase(s1)) {
System.out.println("客户端"+ inetAddress +"已下线!");
break;
}
// 实现服务器向客户端回发字符串内容"I received!"
ps.println("I received!");
System.out.println("服务器发送数据成功!");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != ps) {
ps.close();
}
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != s) {
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
客户端ClientStringTest:
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.Scanner;
public class ClientStringTest {
public static void main(String[] args) {
Socket s = null;
PrintStream ps = null;
Scanner sc = null;
BufferedReader br = null;
try {
// 1.创建Socket类型的对象并提供服务器的主机名和端口号
s = new Socket("127.0.0.1", 8888);
System.out.println("连接服务器成功!");
// 2.使用输入输出流进行通信
sc = new Scanner(System.in);
ps = new PrintStream(s.getOutputStream());
br = new BufferedReader(new InputStreamReader(s.getInputStream()));
while(true) {
//Thread.sleep(10000);
// 实现客户端发送的内容由用户从键盘输入
System.out.println("请输入要发送的数据内容:");
String str1 = sc.next();
// 实现客户端向服务器发送字符串内容"hello"
//ps.println("hello");
ps.println(str1);
System.out.println("客户端发送数据内容成功!");
// 当发送的数据内容为"bye"时,则聊天结束
if ("bye".equalsIgnoreCase(str1)) {
System.out.println("聊天结束!");
break;
}
// 实现接收服务器发来的字符串内容并打印
String str2 = br.readLine();
System.out.println("服务器回发的消息是:" + str2);
}
} catch (IOException /*| InterruptedException*/ e) {
e.printStackTrace();
} finally {
// 3.关闭Socket并释放有关的资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != ps) {
ps.close();
}
if (null != sc) {
sc.close();
}
if (null != s) {
try {
s.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
运行结果:


3. 基于udp协议的编程模型(熟悉)
3.1 编程模型
● 接收方:
(1)创建DatagramSocket类型的对象并提供端口号;
(2)创建DatagramPacket类型的对象并提供缓冲区;
(3)通过Socket接收数据内容存放到Packet中,调用receive方法;
(4)关闭Socket;
● 发送方:
(1)创建DatagramSocket类型的对象;
(2)创建DatagramPacket类型的对象并提供接收方的通信地址;
(3)通过Socket将Packet中的数据内容发送出去,调用send方法;
(4)关闭Socket;
3.2 相关类和方法的解析
(1)DatagramSocket类
● java.net.DatagramSocket类主要用于描述发送和接收数据报的套接字(邮局)。
换句话说,该类就是包裹投递服务的发送或接收点。
● 常用的方法如下:
(2)DatagramPacket类
● java.net.DatagramPacket类主要用于描述数据报,数据报用来实现无连接包裹投递服务。
● 常用的方法如下:
(3)InetAddress类
● java.net.InetAddress类主要用于描述互联网通信地址信息。
● 常用的方法如下:
案例:编程实现udp协议模型
接收方SendTest:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class SendTest {
public static void main(String[] args) {
DatagramSocket ds = null;
try {
// 1.创建DatagramSocket类型的对象
ds = new DatagramSocket();
// 2.创建DatagramPacket类型的对象并提供接收方的通信地址和端口号
byte[] bArr = "hello".getBytes();
DatagramPacket dp = new DatagramPacket(bArr, bArr.length, InetAddress.getLocalHost(), 8888);
// 3.通过Socket发送Packet,调用send方法
ds.send(dp);
System.out.println("发送数据成功!");
// 接收回发的数据内容
byte[] bArr2 = new byte[20];
DatagramPacket dp2 = new DatagramPacket(bArr2, bArr2.length);
ds.receive(dp2);
System.out.println("接收到的回发消息是:" + new String(bArr2, 0, dp2.getLength()));
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭Socket并释放有关的资源
if (null != ds) {
ds.close();
}
}
}
}
发送方SendTest:
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class SendTest {
public static void main(String[] args) {
DatagramSocket ds = null;
try {
// 1.创建DatagramSocket类型的对象
ds = new DatagramSocket();
// 2.创建DatagramPacket类型的对象并提供接收方的通信地址和端口号
byte[] bArr = "hello".getBytes();
DatagramPacket dp = new DatagramPacket(bArr, bArr.length, InetAddress.getLocalHost(), 8888);
// 3.通过Socket发送Packet,调用send方法
ds.send(dp);
System.out.println("发送数据成功!");
// 接收回发的数据内容
byte[] bArr2 = new byte[20];
DatagramPacket dp2 = new DatagramPacket(bArr2, bArr2.length);
ds.receive(dp2);
System.out.println("接收到的回发消息是:" + new String(bArr2, 0, dp2.getLength()));
} catch (IOException e) {
e.printStackTrace();
} finally {
// 4.关闭Socket并释放有关的资源
if (null != ds) {
ds.close();
}
}
}
}
运行结果(先运行接收方,再运行发送方):

4. URL类(熟悉)
4.1 基本概念
● java.net.URL(Uniform Resource Identifier)类主要用于表示统一的资源定位器,也就是指向万维网上“资源”的指针。这个资源可以是简单的文件或目录,也可以是对复杂对象的引用,例如对数据库或搜索引擎的查询等。
● 通过URL可以访问万维网上的网络资源,最常见的就是www和ftp站点,浏览器通过解析给定的URL可以在网络上查找相应的资源。
● URL的基本结构如下:
<传输协议>://<主机名>:<端口号>/<资源地址>
4.2 常用的方法

4.3 URLConnection类
(1)基本概念
java.net.URLConnection类是个抽象类,该类表示应用程序和URL之间的通信链接的所有类的超类,主要实现类有支持HTTP特有功能的HttpURLConnection类。
(2)HttpURLConnection类的常用方法

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
public class URLTest {
public static void main(String[] args) {
try {
// 1.使用参数指定的字符串来构造对象
URL url = new URL("https://www.lagou.com/");
// 2.获取相关信息并打印出来
System.out.println("获取到的协议名称是:" + url.getProtocol());
System.out.println("获取到的主机名称是:" + url.getHost());
System.out.println("获取到的端口号是:" + url.getPort());
// 3.建立连接并读取相关信息打印出来
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
InputStream inputStream = urlConnection.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
String str = null;
while ((str = br.readLine()) != null) {
System.out.println(str);
}
br.close();
// 断开连接
urlConnection.disconnect();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
五. 反射机制
1. 基本概念
● 通常情况下编写代码都是固定的,无论运行多少次执行的结果也是固定的,在某些特殊场合中编写代码时不确定要创建什么类型的对象,也不确定要调用什么样的方法,这些都希望通过运行时传递的参数来决定,该机制叫做动态编程技术,也就是反射机制。
● 通俗来说,反射机制就是用于动态创建对象并且动态调用方法的机制。
● 目前主流的框架底层都是采用反射机制实现的。
如:
Person p = new Person(); - 表示声明Person类型的引用指向Person类型的对象
p.show(); - 表示调用Person类中的成员方法show
2. Class类
2.1 基本概念
● java.lang.Class类的实例可以用于描述Java应用程序中的类和接口,也就是一种数据类型。
● 该类没有公共构造方法,该类的实例由Java虚拟机和类加载器自动构造完成,本质上就是加载到内存中的运行时类。
2.2 获取Class对象的方式
● 使用数据类型.class的方式可以获取对应类型的Class对象(掌握)。
● 使用引用/对象.getClass()的方式可以获取对应类型的Class对象。
● 使用包装类.TYPE的方式可以获取对应基本数据类型的Class对象。
● 使用Class.forName()的方式来获取参数指定类型的Class对象(掌握)。
● 使用类加载器ClassLoader的方式获取指定类型的Class对象。
2.3 常用的方法(掌握)

public class ClassTest {
public static void main(String[] args) throws ClassNotFoundException {
// 1.使用数据类型.class的方式可以获取对应类型的Class对象
Class c1 = String.class;
System.out.println("c1 = " + c1); // 自动调用toString方法 class java.lang.String
c1 = int.class;
System.out.println("c1 = " + c1); // int
c1 = void.class;
System.out.println("c1 = " + c1); // void
System.out.println("---------------------------------------------------");
// 2.使用对象.getClass()的方式获取对应的Class对象
String str1 = new String("hello");
c1 = str1.getClass();
System.out.println("c1 = " + c1); // class java.lang.String
Integer it1 = 20;
c1 = it1.getClass();
System.out.println("c1 = " + c1); // class java.lang.Integer
int num = 5;
//num.getClass(); Error: 基本数据类型的变量不能调用方法
System.out.println("---------------------------------------------------");
// 3.使用包装类.TYPE的方式来获取对应基本数据类型的Class对象
c1 = Integer.TYPE;
System.out.println("c1 = " + c1); // int
c1 = Integer.class;
System.out.println("c1 = " + c1); // class java.lang.Integer
System.out.println("---------------------------------------------------");
// 4.调用Class类中的forName方法来获取对应的Class对象
//c1 = Class.forName("String"); // Error 要求写完整的名称:包名.类名
c1 = Class.forName("java.lang.String");
System.out.println("c1 = " + c1); // class java.lang.String
c1 = Class.forName("java.util.Date");
System.out.println("c1 = " + c1); // class java.util.Date
//c1 = Class.forName("int");
//System.out.println("c1 = " + c1); // 不能获取基本数据类型的Class对象
System.out.println("---------------------------------------------------");
// 5.使用类加载器的方式来获取Class对象
ClassLoader classLoader = ClassTest.class.getClassLoader(); // 调用本类获取加载器
System.out.println("classLoader = " + classLoader);
c1 = classLoader.loadClass("java.lang.String");
System.out.println("c1 = " + c1); // class java.lang.String
}
}
3. Constructor类
3.1 基本概念
java.lang.reflect.Constructor类主要用于描述获取到的构造方法信息
3.2 Class类的常用方法

3.3 Constructor类的常用方法

Person类
import java.io.IOException;
public class Person {
private String name;
//public String name;
private int age;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) throws IOException {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
Class类实现获取构造方法:
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.util.Scanner;
public class PersonConstructorTest {
public static void main(String[] args) throws Exception {
// 1.使用原始方式以无参形式构造Person类型的对象并打印
Person p1 = new Person();
System.out.println("无参方式创建的对象是:" + p1); // null 0
System.out.println("---------------------------------------------------");
// 2.使用反射机制以无参形式构造Person类型的对象并打印
// 创建对象的类型可以从键盘输入
//System.out.println("请输入要创建对象的类型:");
//Scanner sc = new Scanner(System.in);
//String str1 = sc.next();
//Class c1 = Class.forName(str1);
//sc.close();
// 创建对象的类型可以从配置文件中读取
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("d:/a.txt")));
String str1 = br.readLine();
Class c1 = Class.forName(str1);
//System.out.println("无参方式创建的对象是:" + c1.newInstance()); // null 0
// 获取Class对象对应类中的无参构造方法,也就是Person类中的无参构造方法
Constructor constructor = c1.getConstructor();
// 使用获取到的无参构造方法来构造对应类型的对象,也就是Person类型的对象
System.out.println("无参方式创建的对象是:" + constructor.newInstance());
br.close();
System.out.println("---------------------------------------------------");
// 3.使用原始方式以有参方式构造Person类型的对象并打印
Person p2 = new Person("zhangfei", 30);
System.out.println("有参方式构造的对象是:" + p2); // zhangfei 30
System.out.println("---------------------------------------------------");
// 4.使用反射机制以有参方式构造Person类型的对象并打印
// 获取Class对象对应类中的有参构造方法,也就是Person类中的有参构造方法
Constructor constructor1 = c1.getConstructor(String.class, int.class);
// 使用获取到的有参构造方法来构造对应类型的对象,也就是Person类型的对象
// newInstance方法中的实参是用于给有参构造方法的形参进行初始化的,也就是给name和age进行初始化的
System.out.println("有参方式构造的对象是:" + constructor1.newInstance("zhangfei", 30)); // zhangfei 30
System.out.println("---------------------------------------------------");
// 5.使用反射机制获取Person类中所有的公共构造方法并打印
Constructor[] constructors = c1.getConstructors();
for (Constructor ct : constructors) {
System.out.println("构造方法的访问修饰符是:" + ct.getModifiers());
System.out.println("构造方法的方法名称是:" + ct.getName());
Class[] parameterTypes = ct.getParameterTypes();
System.out.print("构造方法的所有参数类型是:");
for (Class cs : parameterTypes) {
System.out.print(cs + " ");
}
System.out.println();
System.out.println("-------------------------------------------------");
}
}
}
4. Field类
4.1 基本概念
java.lang.reflect.Field类主要用于描述获取到的单个成员变量信息。
4.2 Class类的常用方法

4.3 Field类的常用方法

Class类实现获取成员变量:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
public class PersonFieldTest {
public static void main(String[] args) throws Exception {
// 1.使用原始方式来构造对象以及获取成员变量的数值并打印
Person p1 = new Person("zhangfei", 30);
//System.out.println("获取到的成员变量数值为:" + p1.name); // zhangfei
System.out.println("-------------------------------------------------------");
// 2.使用反射机制来构造对象以及获取成员变量的数值并打印
// 2.1 获取Class对象
Class c1 = Class.forName("com.lagou.task20.Person");
// 2.2 根据Class对象获取对应的有参构造方法
Constructor constructor = c1.getConstructor(String.class, int.class);
// 2.3 使用有参构造方法来得到Person类型的对象
Object object = constructor.newInstance("zhangfei", 30);
// 2.4 根据Class对象获取对应的成员变量信息
Field field = c1.getDeclaredField("name");
// 设置Java语言访问检查的取消 暴力反射
field.setAccessible(true);
// 2.5 使用Person类型的对象来获取成员变量的数值并打印
// 获取对象object中名字为field成员变量的数值,也就是成员变量name的数值
System.out.println("获取到的成员变量数值为:" + field.get(object)); // zhangfei
System.out.println("-------------------------------------------------------");
// 3.使用原始方式修改指定对象中成员变量的数值后再次打印
//p1.name = "guanyu";
//System.out.println("修改后成员变量的数值为:" + p1.name); // guanyu
System.out.println("-------------------------------------------------------");
// 4.使用反射机制修改指定对象中成员变量的数值后再次打印
// 表示修改对象object中名字为field成员变量的数值为guanyu,也就是成员变量name的数值为guanyu
field.set(object, "guanyu");
System.out.println("修改后成员变量的数值为:" + field.get(object)); // guanyu
System.out.println("-------------------------------------------------------");
// 5.获取Class对象对应类中所有的成员变量
Field[] declaredFields = c1.getDeclaredFields();
for (Field ft : declaredFields) {
System.out.println("获取到的访问修饰符为:" + ft.getModifiers());
System.out.println("获取到的数据类型为:" + ft.getType());
System.out.println("获取到的成员变量名称是:" + ft.getName());
System.out.println("---------------------------------------------");
}
}
}
5. Method类
5.1 基本概念
java.lang.reflect.Method类主要用于描述获取到的单个成员方法信息。
5.2 Class类的常用方法

5.3 Method类的常用方法

Class类实现获取成员方法:
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
public class PersonMethodTest {
public static void main(String[] args) throws Exception {
// 1.使用原始方式构造对象并调用方法打印结果
Person p1 = new Person("zhangfei", 30);
System.out.println("调用方法的返回值是:" + p1.getName()); // zhangfei
System.out.println("------------------------------------------------------");
// 2.使用反射机制构造对象并调用方法打印结果
// 2.1 获取Class对象
Class c1 = Class.forName("com.lagou.task20.Person");
// 2.2 根据Class对象来获取对应的有参构造方法
Constructor constructor = c1.getConstructor(String.class, int.class);
// 2.3 使用有参构造方法构造对象并记录
Object object = constructor.newInstance("zhangfei", 30);
// 2.4 根据Class对象来获取对应的成员方法
Method method = c1.getMethod("getName");
// 2.5 使用对象调用成员方法进行打印
// 表示使用object对象调用method表示的方法,也就是调用getName方法来获取姓名
System.out.println("调用方法的返回值是:" + method.invoke(object)); // zhangfei
System.out.println("------------------------------------------------------");
// 3.使用反射机制来获取类中的所有成员方法并打印
Method[] methods = c1.getMethods();
for (Method mt : methods) {
System.out.println("成员方法的修饰符是:" + mt.getModifiers());
System.out.println("成员方法的返回值类型是:" + mt.getReturnType());
System.out.println("成员方法的名称是:" + mt.getName());
System.out.println("成员方法形参列表的类型是:");
Class<?>[] parameterTypes = mt.getParameterTypes();
for (Class ct : parameterTypes) {
System.out.print(ct + " ");
}
System.out.println();
System.out.println("成员方法的异常类型列表是:");
Class<?>[] exceptionTypes = mt.getExceptionTypes();
for (Class ct: exceptionTypes) {
System.out.print(ct + " ");
}
System.out.println();
System.out.println("---------------------------------------------------");
}
}
}
6. 获取其它结构信息

注解:
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}
继承类Student:
package com.lagou.task20;
import java.io.Serializable;
@MyAnnotation
public class Student<T, E> extends Person implements Comparable<String>, Serializable {
@Override
public int compareTo(String o) {
return 0;
}
}
测试类:
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
public class StudentTest {
public static void main(String[] args) throws Exception {
// 获取Student类型的Class对象
Class c1 = Class.forName("com.lagou.task20.Student");
System.out.println("获取到的包信息是:" + c1.getPackage());
System.out.println("获取到的父类信息是:" + c1.getSuperclass());
System.out.println("-------------------------------------------------");
System.out.println("获取到的接口信息是:");
Class[] interfaces = c1.getInterfaces();
for (Class ct : interfaces) {
System.out.print(ct + " ");
}
System.out.println();
System.out.println("-------------------------------------------------");
System.out.println("获取到的注解信息是:");
Annotation[] annotations = c1.getAnnotations();
for (Annotation at : annotations) {
System.out.print(at + " ");
}
System.out.println();
System.out.println("-------------------------------------------------");
System.out.println("获取到的泛型信息是:");
Type[] genericInterfaces = c1.getGenericInterfaces();
for (Type tt : genericInterfaces) {
System.out.print(tt + " ");
}
System.out.println();
}
}
运行结果: