文章目录
- Java核心类库(下)
- 异常机制(重点)
- File类(重点)
- IO流
- IO流的概念
- 基本分类
- 体系结构
- 相关流的详解
- FileWriter类(重点)
- FileReader类(重点)
- FileOutputStream类(重点)
- FileInputStream类(重点)
- BufferedOutputStream(重点)
- BufferedInputStream(重点)
- BufferedWriter类(重点)
- BufferedReader(重点)
- PrintStream类
- PrintWriter类
- OutputStreamWriter类
- InputStreamReader类
- 字符编码
- DataOutputStream类(了解)
- DataInputStream类(了解)
- ObjectOutputStream类(重点)
- ObjectInputStream类(重点)
- RandomAccessFile类
- 任务总结
- 多线程
- 网络编程
- 反射机制
Java核心类库(下)
异常机制(重点)
基本概念
- 异常就是“不正常”的含义,在Java语言中主要指程序执行中发生的不正常情况
- java.lang.Throwable类是Java语言中错误(Error)和异常(Exception)的超类
- 其中Error类主要用于描述Java虚拟机无法解决的严重错误,通常无法编译解决,如:JVM挂掉了等
- 其中Exception类主要用于描述因编程错误或偶然外在因素导致的轻微错误,通常可以编码解决,如:0作为除数等
异常的分类
- java.lang.Exception类是所有异常的超类,主要分为以下两种
- RuntimeException:运行时异常,也叫做非检测性异常
- IOException和其他异常:其他异常,也叫做检测性异常,所谓检测性异常就是指在编译阶段都能被编译器检测出来的异常
- 其中RuntimeException类的主要子类:
- ArithmeticException类:算术异常
- ArrayIndexOutOfBoundsException类:数组下标越界异常
- NullPointerException:空指针异常
- ClassCastException:类型转换异常
- NumberFormatException:数字格式异常
- 异常的框架结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rd8nc42I-1621532659828)(F:/Download_default_address/Typora/img/01 异常的框架结构.png)] - 注意:当程序执行过程中发生异常但有没有手动处理时,则由Java虚拟机采用默认方式处理异常,而默认处理方式就是:打印异常的名称、异常发生的原因、异常发生的位置以及终止程序(发生异常后不会接着往下执行)
异常的避免
- 在以后的开发中尽量使用if条件判断来避免异常的发生
- 但是过多的if条件判断会导致程序的代码加长、臃肿,可读性差
异常的捕获
-
语法格式
try { 编写可能发生异常的代码; } catch(异常类型 引用变量名) { 编写针对该异常的处理代码; } ... { ... } finally { 编写无论是否发生异常都要执行的代码; }
-
try-catch的执行顺序
//创建一个FileInputStream类型的对象与d:\a.txt文件关联,打开文件 FileInputStream fis = null; try { System.out.println("1"); 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 (IOException e) { System.out.println("7"); e.printStackTrace(); System.out.println("8"); } System.out.println("世界上最真情的相依就是你在try我在catch,无论你发神马脾气我都默默承受并静静处理," + "到那是我们再来期待我们的finally"); }
- 当程序中没有发生异常时的执行流程:1 2 5 6 世界上…
- 当程序第一个try-catch中catch正好是FileNotFoundException,而第二个应该是NullPointerException,可是catch中是IOException,因此第二个catch中将不会执行到,fis,close()发生异常后直接终止程序;并且try中执行到异常语句后就会直奔catch而去,故执行顺序:1 3 4 5
- 若第二个catch后面新增处理空指针异常,则将会像第一种try-catch,进入该catch语句执行捕获空指针异常
-
注意事项
-
当需要编写多个catch分支时,切记小类型应该放在大类型的前面
-
懒人的写法:(推荐还是写详细的异常)
catch(Exception e) { } -
finally{}代码块中的语句无论是否发生异常,无论是否有针对发生的异常做出相对应的手动处理,都会执行到
finally通常用于进行善后处理,如:关闭已经打开的文件等手动处理异常和没有处理异常的区别:代码发生异常后是否会往后继续执行(手动处理后代码发生异常后将会进入对应代码块中执行)
public static void main(String[] args) { try { int ia = 10; int ib = 0; System.out.println(ia / ib); } catch (ArithmeticException ae) { ae.printStackTrace(); String str = null; str.length(); //空指针异常 } finally { //无论是否有异常;算术异常和空指针异常是否有catch处理,finally语句中的代码都会执行 System.out.println("无论是否发生异常,是否有手动处理都会执行到"); } //若没有空指针异常,仅有算术异常,那么该句将会执行,因为算术异常有catch处理;若有空指针异常,代码在那里就结束了,因此该句不会执行到 System.out.println("Over"); }
-
执行流程小结:
try { a; b; c; } catch(Exception e) { d; } finally { e; }
-
当没有发生异常时的执行流程:a b c e
-
当发生异常时的执行流程:a b d e
笔试题考点:下面方法返回值是多少?
//笔试题考点 public static int test () { try { int[] arr = new int[5]; System.out.println(arr[5]); return 0; //发生异常,该语句不会执行 } catch (ArrayIndexOutOfBoundsException e) { e.printStackTrace(); //该语句执行完会马上去执行finally中的代码块 return 1; //该语句执行前先去执行finally中的代码 } finally { return 2; //因此最终返回结果是2 } }
-
-
异常的抛出
-
基本概念
在某些特殊情况下有些异常不能处理或者不便于处理时,就可以将该异常转移给该方法的调用者,这种方法就叫异常的抛出。当方法执行时出现异常,则底层生成一个异常类对象抛出,此时异常代码后续的代码就不再执行 -
语法格式
访问权限 返回值类型 方法名称(形参列表) throws 异常类型1, 异常类型2, … {方法体;}如:
public void show() throws IOException {}
-
注意
- 发生异常并且抛出异常后,方法体中的代码不会继续向下执行
- 不建议在main方法中抛出异常。因为main方法中抛出异常会将异常抛给JVM,JVM本身要做的工作已经很多,不要再增加负担
-
回顾–>方法重写的原则:
- 要求方法名相同、参数列表相同以及返回值类型相同,从jdk1.5开始支持返回子类类型
- 要求重写的方法的访问权限不能变小,可以相同或者变大
- 要求重写的方法不能抛出更大的异常
-
注意:
子类重写的方法不能抛出更大的异常、不能抛出平级不一样的异常,但可以抛出一样的异常、更小的异常以及不抛出异常 -
经验分享
- 若父类中被重写的方法没有抛出异常时,则子类中重写的方法只能进行异常的捕获处理
- 若一个方法内部又调用了好几个其他有异常的方法,则建议这些方法可以使用抛出的方法抛出到最后一层的方法后,再在调用最后一层方法时进行捕获方式处理
自定义异常
-
基本概念
当需要在程序中表达年龄不合理的情况时,而Java官方又没有提供这种针对性的异常,此时就需要程序员自定义异常加以描述 -
实现流程
- 自定义xxx.Exception异常类继承Exception类或其子类
- 提供两个版本的构造方法,一个是无参构造方法;另一个是字符串作为参数的构造方法
- 提供一个序列化版本号
-
异常的产生:当创建了自定义异常类后,若希望将某种情况定义为异常,直接抛出新的异常类即可
throw new 异常类型(实参:要抛出的异常信息);
如:
throw new AgeException("年龄不合理!");
-
Java采用的异常处理机制,是将异常处理的程序代码集中在一起,与正常的程序代码分开,使得程序简洁、优雅、并易于维护
-
自定义异常实现举例:
自定义年龄不合理异常:package com.lagou.task16; public class AgeException extends Exception { static final long serialVersionUID = -3387516993124229948L; //提供一个序列化的版本号,用于序列化操作 public AgeException() { } public AgeException(String message) { super(message); } }
Person类:
package com.lagou.task16; public class Person { private String name; private int age; public Person() { } public Person(String name, int age) throws AgeException { setName(name); setAge(age); //这边构造方法要用setAge(),这样才能在创建对象时直接抛出异常 } 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 + '}'; } }
PersonTest类:
package com.lagou.task16; public class PersonTest { public static void main(String[] args) { Person p1 = null; try { p1 = new Person("张飞", -30); } catch (AgeException e) { //年龄-30,直接出现年龄不合理异常 e.printStackTrace(); } System.out.println("p1 = " + p1); //或Person类中AgeException异常都是抛出,则这里是null,因为抛出到创建对象时才会捕获异常,故连对象都没创建;若是在Person类中捕获,则这里将会{name="张飞",age=0},因为已经在创建对象时直接捕获处理了异常,故而还是会生成对象 } }
异常机制总结
File类(重点)
基本概念
- java.io.File类主要用于描述文件或目录路径的抽象表示信息,可以获取文件或目录的特征信息,如:大小等
常用的方法
方法声明 | 功能概述 |
---|---|
File(String pathname) | 根据参数指定的路径名来构造对象 |
File(String parent, String child) | 根据参数指定的父路径和子路径信息构造对象 |
File(File parent, String child) | 根据参数指定的父抽象路径和子路径信息构造对象 |
boolean exists() | 测试此抽象路径名表示的文件或目录是否存在 |
String getName() | 用于获取文件的名称 |
long length() | 返回由此抽象路径名表示的文件的长度 |
long lastModified() | 用于获取文件的最后一次修改时间(返回毫秒值,可以使用 Date和SimpleDateFormat转换为指定格式的时间信息) |
String getAbsolutePath() | 用于获取绝对路径信息 |
boolean delete() | 用于删除文件,当删除目录时要求是空目录 |
boolean createNewFile() | 用于创建新的空文件 |
boolean mkdir() | 用于创建目录 |
boolean mkdirs() | 用于创建多级目录 |
File[] listFiles() | 获取该目录下的所有内容 |
boolean isFile() | 判断是否为文件 |
boolean isDirectory | 判断是否为目录 |
File[] listFiles(FileFilter filter) | 获取目录下满足筛选器的所有内容 |
-
案例题目:遍历指定目录以及子目录中的所有内容并打印出来
//自定义成员方法实现指定目录以及子目录中所有内容的打印 public static void show(File file) { File[] files = file.listFiles(); //遍历数组 for (File f : files) { String name = f.getName(); //判断是否为文件,若是则直接打印名字 if (f.isFile()) { System.out.println(name); } //判断是否为目录,若是则用[]名称括起来 if (f.isDirectory()) { System.out.println("[" + name + "]"); //若是目录,还要用递归的方法接着遍历里面的子目录 show(f); } } }
main()中调用show()方法实现
//调用show()方法遍历目录以及子目录中全部内容 File f4 = new File("E:\\FilesForTest\\LaGou_Java\\捣乱"); show(f4);
IO流
IO流的概念
- IO就是Input和Output的简写,也就是输入和输出的含义
- IO流就是指读写数据时像流水一样从一端流到另一端,因此得名为"流"
基本分类
- 按照读写数据的基本单位不同,分为字节流和字符流
其中字节流主要指以字节为单位进行数据读写的流,可以读写任意类型的文件
其中字符流主要指以字符(2个字节)为单位进行数据读写的流,只能读写文本文件(为何不干脆用字节流?因为有些文字,如汉字就是2个字节) - 按照读写数据的方向不同,分为输入流和输出流(输入和输出是站在程序的角度)
其中输入流主要指从文件中读取数据内容输入到程序中,也就是读文件
其中输出流主要指将程序中的数据内容输出到文件中,也就是写文件 - 按照流的角色不同分为节点流和处理流
其中节点流主要指直接和输入输出源对接的流
其中处理流主要指需要建立在节点流的基础之上的流
体系结构
相关流的详解
FileWriter类(重点)
基本概念
- java.io.Writer主要用于将文本内容写入到文本文件中
常用的方法
方法声明 | 功能介绍 |
---|---|
FileWriter(String fileName) | 根据参数指定的文件名构造对象 (若文件不存在会自动创建新文件;先清空再添加) |
FileWriter(String fileName, boolean append) | 以追加的方式根据参数指定的文件名来构造对象 (若文件不存在会自动创建新文件; append为true往后继续添加) |
void write(int c) | 写入单个字符 |
void write(char[] cbuf, int off, int len) | 将执行字符数组中从偏移量off开始的len个字符 写入此文件输出流 |
void write(char[] buff) | 将cbuf.length个字符从指定字符数组写入此文件输出流中 |
void flush() | 刷新流 |
void close() | 关闭流对象并释放有关的资源 |
FileReader类(重点)
基本概念
- java.io.FileReader类主要用于从文本文件读取文本数据内容
常用的方法
方法声明 | 功能介绍 |
---|---|
FileReader(String fileName) | 根据参数指定的文件名来构造对象 |
int read() | 读取单个字符的数据并返回,返回-1表示读取到末尾 |
int read(char[] cbuf, int offset, int length) | 从输入流中将最多len个字符的数据读入一个字符数组中(从offset下标位置开始存),返回读取到的字符个数,返回-1表示读取到末尾 |
int read(char[] cbuf) | 从此输入流中将最多cbuf.length个字符的数据读入字符数组中,返回读取到的字符个数,返回-1表示读取到末尾 |
void close() | 关闭流对象并释放有关的资源 |
FileOutputStream类(重点)
基本概念
- java.io.FileOutputStream类主要用于将图像数据之类的原始字节流写入到输出流中
常用的方法
方法声明 | 功能介绍 |
---|---|
FileOutputStream(String name) | 根据参数指定地文件名来构造对象 |
FileOutputStream(String name, boolean append) | 以追加的方式根据参数指定的文件名来构造对象 |
void write(int b) | 将指定字节写入此文件输出流 |
void write(byte[] b, int off, int len) | 将指定字节数组中从偏移量off开始的len个字节写入此文件输出流 |
void write(byte[] b) | 将b.length个字节从指定字节数组写入此文件输出流中 |
void flush() | 刷新此输出流并强制写出任何缓冲的输出字节 |
void close() | 关闭流对象并释放有关的资源 |
FileInputStream类(重点)
基本概念
- java.io.FileInputStream类主要用于从输入流中以字节流的方式读取图像数据等
常用的方法
方法声明 | 功能介绍 |
---|---|
FileInputStream(String name) | 根据参数指定的文件路径名来构造对象 |
int read() | 从输入流中读取单个字节的数据并返回,返回-1表示读取到末尾 |
int read(byte[] b, int off, int len) | 从此输入流中将最多len个字节的数据读入字节数组中(从数组off最值开始存储),返回读取到的字节个数,返回-1表示读取到末尾 |
int read(byte[] b) | 从此输入流中将最多b.length个字节的数据读入字节数组中,返回读取到的字节个数,返回-1表示读取到末尾 |
void close() | 关闭流对象并释放有关的资源 |
int available() | 获取输入流所关联文件的大小 |
题目:实现两个文件之间的拷贝功能
-
FileReader和FileWriter只能用于文本文件,其他文件不可用这两个类
package com.lagou.task17; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; /** * 文件字符流实现文件的拷贝 */ public class FileCharCopyTest { public static void main(String[] args) { FileReader fileReader = null; FileWriter fileWriter = null; try { //1、创建FileReader与文件关联 fileReader = new FileReader("E:\\FilesForTest\\LaGou_Java\\a.txt"); // fileReader = new FileReader("E:\\FilesForTest\\LaGou_Java\\03 IO流的框架图.png"); //2、创建FileWriter与文件关联 fileWriter = new FileWriter("E:\\FilesForTest\\LaGou_Java\\b.txt"); // fileWriter = new FileWriter("E:\\FilesForTest\\LaGou_Java\\IO流的框架图.png"); //拷贝文件图片失败! //FileReader和FileWriter只能用于读写文本文件 //3、不断地从输入流中读取数据内容并写入到输出流中 int res = 0; while ((res = fileReader.read()) != -1) { fileWriter.write(res); } } catch (IOException e) { e.printStackTrace(); } finally { //4、关闭流 if (null != fileWriter) { try { fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != fileReader) { try { fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
-
其他类型的文件实现拷贝:用字节输入输出流
package com.lagou.task17; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; public class FileByteCopyTest { public static void main(String[] args) { FileInputStream fileInputStream = null; FileOutputStream fileOutputStream = null; try { //1、创建FileInputStream与文件关联 fileInputStream = new FileInputStream("E:\\FilesForTest\\LaGou_Java\\a.mp4"); //2、创建FileOutputStream与文件关联 fileOutputStream = new FileOutputStream("E:\\FilesForTest\\LaGou_Java\\a_copy.mp4"); //3、不断地从输入流中读取数据内容并写入到输出流中 System.out.println("努力拷贝中....."); /* //方式一:以单个字节为单位进行拷贝,也就是每次读取一个字节后立刻写入一个字节 //缺点:文件稍大时,拷贝的效率很低 int res = 0; while ((res = fileInputStream.read()) != -1) { fileOutputStream.write(res); } */ /* //方式二:准备一个和文件一样大小的缓冲区,一次性将文件中的所有内容取出缓冲区后一次性写入进去 //缺点:文件过大时,无法申请和文件大小一样的缓冲区,物理内存空间不足 int len = fileInputStream.available(); //获取文件大小 System.out.println("获取到的文件大小是:" + len); byte[] bArr = new byte[len]; int res = fileInputStream.read(bArr); System.out.println("实际读取到的文件大小是:" + res); fileOutputStream.write(bArr); */ //方法三:准备一个相对适当的缓冲区,分多次将文件拷贝完成 byte[] bArr = new byte[1024]; int res = 0; while ((res = fileInputStream.read(bArr)) != -1) { fileOutputStream.write(bArr, 0, res); //注意:最后一次可能不足1024个字节,因此要指定从头开始的res个字节写入 } System.out.println("文件拷贝成功....."); } catch (IOException e) { e.printStackTrace(); } finally { //4、关闭流 if (null != fileOutputStream) { try { fileOutputStream.close(); } catch (IOException e) { e.printStackTrace(); } } if (null != fileInputStream) { try { fileInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
注意:以上有三种方式进行文件的拷贝,各有优缺点,注意看!
BufferedOutputStream(重点)
基本概念
- java.io.BufferedOutputStream类主要用于描述缓冲输出流,此时不用为写入的每个字节调用底层系统
常用的方法
方法声明 | 功能介绍 |
---|---|
BufferedOutputStream(OutputStream out) | 根据参数指定的引用来构造对象 |
BufferedOutputStream(OutputStream out, int size) | 根据参数指定的引用和缓冲区大小来构造对象 |
void write(int b) | 写入单个字符 |
void write(byte[] b, int off, int len) | 写入字节数组中的一部分数据 |
void write(byte[] b) | 写入参数指定的整个字节数组 |
void flush() | 刷新流 |
void close() | 关闭流对象并释放有关的资源 |
BufferedInputStream(重点)
基本概念
- java.io.BufferedInputStream类主要用于描述缓冲输入流
常用的方法
方法声明 | 功能介绍 |
---|---|
BufferedInputStream(InputStream in) | 根据参数指定的引用构造对象 |
BufferedInputStream(InputStream in, int size) | 根据参数指定的引用和缓冲区大小构造对象 |
int read() | 读取单个字节 |
int read(byte[] b, int off, int len) | 读取len个字节 |
int read(byte[] b) | 读取b.length个字节 |
void close() | 关闭流对象并释放有关的资源 |
BufferedWriter类(重点)
基本概念
- java.io.BufferedWriter类主要用于写入单个字符、字符数组与i及字符串到输出流中
常用的方法
方法声明 | 功能介绍 |
---|---|
BufferedWriter(Writer out) | 根据参数指定的引用来构造对象 |
BufferedWriter(Writer out, int size) | 根据参数指定的引用和缓冲区大小来构造对象 |
void write(int c) | 写入单个字符到输出流中 |
void write(char[] cbuf, int off, int len) | 将字符数组cbuf中从下标off开始的len个字符写入到输出流中 |
void write(char[] cbuf) | 将字符数组cbuf中所有内容写入到输出流中 |
void write(String s, int off, int len) | 将参数s中下标从off开始的len个字符写入到输出流中 |
void write(String str) | 将参数指定的字符串内容写入输出流中 |
void newLine() | 用于写入行分隔符到输出流中 |
void flush() | 刷新流 |
void close() | 关闭流对象并释放有关的资源 |
BufferedReader(重点)
基本概念
- java.io.BufferedReader类用于从输入流中读取单个字符、字符数组以及字符串
常用的方法
方法声明 | 功能介绍 |
---|---|
BufferedReader(Reader in) | 根据参数指定的引用来构造对象 |
BufferedReader(Reader in, int size) | 根据参数指定的引用和缓冲区大小来构造对象 |
int read() | 从输入流读取单个字符,读取到末尾则返回-1,否则返回实际读取到的字符内容 |
int read(char[] cbuf, int off, int len) | 从输入流中读取len个字符放入数组的cbuf中下标从off开始的位置上,若读取到末尾则返回-1,否则返回实际读取到的字符个数 |
int read(char[] cbuf) | 从输入流中读满整个数组cbuf |
String readLine() | 读取一行字符串并返回,返回null表示读取到末尾 |
void close() | 关闭流对象并释放有关的资源 |
PrintStream类
基本概念
- java.io.PrintStream类主要用于更加方便地打印各种数据内容
- PrintStream与PrintWriter区别:PrintStream是字节流
常用的方法
方法声明 | 功能介绍 |
---|---|
PrintStream(OutputStream out) | 根据参数指定的引用来构造对象 |
void print(String s) | 用于将参数指定的字符串内容打印出来 |
void println(String x) | 用于打印字符串后并终止该行 |
void flush() | 刷新流 |
void close() | 用于关闭输出流并释放有关的资源 |
PrintWriter类
基本概念
- java.io.PrintWriter类主要用于将对象的格式化形式打印到文本输出流
- PrintWriter与PrintStream区别:PrintWriter是字符流
常用的方法
方法声明 | 功能介绍 |
---|---|
PrintWriter(Writer out) | 根据参数指定的引用来构造对象 |
void print(String s) | 将参数指定的字符串内容打印出来 |
void println(String x) | 打印字符串后并终止该行 |
void flush() | 刷新流 |
void close() | 关闭流对象并释放有关的资源 |
-
案例题目
不断地提示用户输入要发送地内容,若发送的内容是“bye”则聊天结束,否则将用户输入的内容写入到文件a.txt中。
要求使用BufferedReader类来读取键盘的输入 System.in代表键盘输入
要求使用PrintStream类负责将输入写入文件package com.lagou.task17; import java.io.*; import java.text.SimpleDateFormat; import java.util.Date; public class PrintStreamChatTest { public static void main(String[] args) { BufferedReader br = null; PrintStream ps = null; try { //1、提示用户输入要发送的内容并用变量存储 //由手册可知:构造方法需要的是Reader类型的引用,但Reader类是个抽象类,实参只能传递子类的对象 //由手册可知:System.in代表键盘输入,而且是InputStream类型的字节流 br = new BufferedReader(new InputStreamReader(System.in)); ps = new PrintStream(new FileOutputStream("E:\\FilesForTest\\LaGou_Java\\a.txt", true)); boolean flag = true; //用于控制消息发送方 while (true) { System.out.println("请" + (flag == true ? "张三" : "李四") + "输入要发送的聊天内容:"); String str = br.readLine(); //2、判断用户输入的内容是否是bye,若是则聊天结束 if ("bye".equals(str)) { System.out.println("聊天结束"); break; } //给发送消息记录加上发送时间 Date date = new Date(); //获取当前系统时间 SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); ps.println(simpleDateFormat.format(date) + (flag == true ? " 张三说:" : " 李四说:") + str); flag = !flag; //转换聊天方 } } catch (IOException e) { e.printStackTrace(); } finally { //3、若不是则将用户输入的内容写入到文件中 if (ps == null) { ps.close(); } if (br == null) { try { br.close(); } catch (IOException e) { e.printStackTrace(); } } } } }
OutputStreamWriter类
基本概念
- java.io.OutputStreamWriter类主要用于实现从字符到字节流的转换
常用的方法
方法声明 | 功能介绍 |
---|---|
OutputStreamWriter(OutputStream out) | 根据参数指定的引用来构造对象 |
OutputStreamWriter(OutputStream out, String charsetName) | 根据参数指定的引用和编码构造对象 |
void write(String str) | 将参数指定的字符串写入 |
void flush() | 刷新流 |
void close() | 用于关闭输出流并释放有关的资源 |
InputStreamReader类
基本概念
- java.io.InputStreamReader类主要用于实现从字节流到字符流的转换
常用的方法
方法声明 | 功能介绍 |
---|---|
InputStreamReader(InputStream in) | 根据参数指定的引用来构造对象 |
InputStreamReader(InputStream in, String charsetName) | 根据参数指定的引用和编码来构造对象 |
int read(char[] cbuf) | 读取字符数据到参数指定的数组 |
void close() | 用于关闭输出流并释放有关的资源 |
字符编码
编码表的由来
- 计算机只能识别二进制数据,早期就是电信号。为了方便计算机可以识别各个国家的文字,就需要将各个国家的文字采用数字编号的方式进行描述并建立对应的关系表,该表就叫做编码表
常见的编码表
- ASCII:美国标准信息交换码,使用一个字节的低7位二进制进行表示
- ISO8859-1:拉丁码表,欧洲码表,使用一个字节的8位二进制进行表示
- GB2312:中国的中文编码表,最多使用两个字节16位二进制进行表示
- GBK:中国的中文编码表升级,融合了更多的中文文字符号,最多使用两个字节16位二进制位表示
- Unicode:国际标准码,融合了目前人类使用的所有字符,为每个字符分配唯一的字符码(目的是为了区分是何种字符)。所有的文字都用两个字节16位二进制位来表示
编码的发展
- 面向传输的众多UTF(UCS Transfer Format)标准出现了,UTF-8就是每次8个位传输数据,而UTF-16就是每次16个位。这是为传输而设计的编码并使编码无国界,这样就可以显示全世界上所有文化的字符了
- Unicode只是定义了一个庞大的、全球通用的字符集,并为每个字符规定了唯一确定的编号,具体存储什么样的字节流,取决于字符编码方案。推荐的Unicode编码是UTF-8和UTF-16
- UTF-8:变长的编码方式,可用1-4个字节来表示一个字符
DataOutputStream类(了解)
基本概念
- java.io.DataOutputStream类主要用于以适当的方式将基本数据类型写入输出流中
常用的方法
方法声明 | 功能介绍 |
---|---|
DataOutputStream(OutputStream out) | 根据参数指定的引用构造对象,OutputStream类是个抽象类,实参需要传递子类对象 |
void writeXxx(xxx v) | 用于将参数指定的xxx类型数字一次性写入输出流,优先写入高字节 |
void close() | 用于关闭文件输出流并释放有关的资源 |
DataInputStream类(了解)
基本概念
- java.io.DataInput类主要用于从输入流中读取基本数据类型的数据
常用的方法
方法声明 | 功能介绍 |
---|---|
DataInputStream(InputStream in) | 根据参数指定的引用来构造对象。InputStream类是抽象类,实参需要传递子类对象 |
int readInt() | 用于从输入流中一次性读取一个整数数据并返回 |
void close() | 用于关闭文件输出流并释放有关的资源 |
- 注意:writeInt()和write()是有区别的,writeInt()一次性写入整数的4个字节,write一次性只写一个字节。readInt()和read()同理
ObjectOutputStream类(重点)
基本概念
- java.io.ObjectOutputStream类主要用于将一个对象的所有内容整体写入到输出流中
- 只能将支持java.io.Serializable接口的对象写入流中
- 类通过实现java.io.Serializable接口以启用其序列化功能
- 所谓序列化主要指将一个对象需要存储的相关信息有效组织成字节序列的转化过程
常用的方法
方法声明 | 功能介绍 |
---|---|
ObjectOutputStream(OutputStream out) | 根据参数指定的引用来构造对象 |
void writeObject(Object obj) | 用于将参数指定的对象整体写入到输出流中 |
void close() | 用于关闭输出流并释放有关的资源 |
注意
要像能够进行序列化读写操作,被读写的对象类必须实现Serializable接口并执行序列号
ObjectInputStream类(重点)
基本概念
- java.io.ObjectInputStream类主要用于从输入流中一次性将对象整体读取出来
- 所谓反序列化主要指将有效组织的字节序列恢复为一个对象及相关信息的转化过程
常用的方法
方法声明 | 功能介绍 |
---|---|
ObjectInputStream(InputStream in) | 根据参数指定的引用来构造对象 |
Object readObject() | 主要用于从输入流中读取一个对象并返回,无法通过返回值来判断是否读取到文件的末尾 |
void close() | 用于关闭输入流并释放有关的资源 |
序列化版本号
- 序列化版本号是通过运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体类的serialVersionUID进行比较,如果相同就认为时一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常(InvalidCastException)
transient关键字
- transient关键字是Java语言的关键字,用来表示一个域不是该对象串行化的一部分。当一个对象被串行化的时候,transient型变量的值不包括在串行化的表示中,然而非transient型的变量是被包括进去的(说人话:就是被transient关键字修饰的成员变量不会作为序列化的一部分参与到序列化中,即被排除了)
经验:如何使用readObject()方法从文件中读取多个对象
- 由于readObject无法通过返回值判断是否读取到文件的末尾,因此当希望将多个对象写入文件时,通常建议将多个对象放入一个集合中,然后将集合这个整体看作一个对象写入输出流中,此时只需要调用一次readObject()方法就可以将整个集合的数据读取出来,从而避免了通过返回值判断是否到达文件末尾的判断
java.io.EOFException异常
-
该异常即END OF FILE的缩写。到达文件流末尾时有些不是返回一些特殊值,而是报出该项异常提示到达文件流末尾。此时可以捕获该异常并输出对应信息以做出相应处理。如下例:
try { ois = new ObjectInputStream(new FileInputStream("StudentsInfo.txt")); Student s = (Student) ois.readObject(); students.add(s); } catch (EOFException e) { //文件读取到末尾ois为空将会抛出此异常,因此需要在此处捕获异常处理 System.out.println("文件中的学生信息已经读取完毕..."); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } finally { if (ois != null) { try { ois.close(); } catch (IOException e) { e.printStackTrace(); } } }
RandomAccessFile类
基本概念
- java.io.RandomAccessFile类主要支持对随机访问文件的读写操作(需要进行跳跃式读写的时候可以用这个类)
常用的方法
方法声明 | 功能介绍 |
---|---|
RandomAccessFile(String name, String mode) | 根据参数指定的名称和模式构造对象 r:以只读方式打开 rw:打开以便读取和写入 rwd:打开以便读取和写入,同步文件内容的更新 rws:打开以便读取和写入,同步文件内容和元数据的更新 |
int read() | 读取单个字节的数据 |
void seek(long pos) | 用于设置从此文件的开头开始测量的文件指针偏移量 |
void write(int b) | 将参数指定的单个字节写入 |
void close() | 用于关闭流并释放有关的资源 |
使用举例:
package com.lagou.task17;
import java.io.IOException;
import java.io.RandomAccessFile;
public class RandomAccessFileTest {
public static void main(String[] args) {
RandomAccessFile raf = null;
try {
//1、创建RandomAccessFile类型的对象与对应文件相关联
raf = new RandomAccessFile("E:\\FilesForTest\\LaGou_Java\\a.txt", "rw");
//2、对文件内容进行随机读写操作
//设置距离文件开头位置的偏移量,从文件开头位置向后偏移3个字节
raf.seek(3);
int res = raf.read(); //这个时候到3位置(0开始)
System.out.println("读取到的单个字符是:" + (char) res);
res = raf.read(); //这个时候到4位置(0开始)
System.out.println("读取到的单个字符是:" + (char) res);
raf.write('1'); //将5位置上的字符覆盖变为1(0开始)
System.out.println("写入数据成功");
} catch (IOException e) {
e.printStackTrace();
} finally {
//3、关闭流
if (null != raf) {
try {
raf.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
任务总结
- IO流(重点)
概念、分类、体系结构、相关类的详解等
多线程
基本概念
进程的概念
- 程序=数据结构+算法,主要指存放在硬盘上的可执行文件
- 进程 - 主要指运行在内存中的可执行文件
- 目前主流的操作系统都支持多进程,为了让操作系统同时可以执行多个任务。但进程是重量级的,也就是新建一个进程会消耗CPU和内存空间等系统资源,因此进程的数量比较局限
线程的概念
- 为了解决上述问题就提出线程的概念,线程就是进程内部的程序流,也就是说操作系统内部支持多进程的,而每个进程的内部又是支持多线程的,线程是轻量的,新建线程会共享所在进程的系统资源,因此目前主流的开发都是采用多线程
- 多线程是采用时间片轮换法来保证多个线程的并发执行,所谓并发就是指宏观并行微观串行的机制
线程的创建
Thread类的概念
- java.lang.Thread类代表线程,任何线程对象都是Thread类(子类)的实例
- Thread类是线程的模板,封装了复杂的线程开启等操作,封装了操作系统的差异性
创建方式
- 自定义类继承Thread类并重写run方法,然后创建该类的对象调用start方法
- 自定义类实现Runable接口并重写run方法,创建该类的对象作为实参来构造Thread类型的对象,然后使用Thread类型的对象调用start方法
相关的方法
方法声明 | 功能介绍 |
---|---|
Thread() | 使用无参的方式构造对象 |
Thread(String name) | 根据参数指定的名称来构造对象 |
Thread(Runnable target) | 根据参数指定的引用来构造对象,其中Runnable是个接口类型 |
Thread(Runnable target, String name) | 根据参数指定引用和名称来构造对象 |
void run() | 若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本 若没有使用Runnable引用构造线程对象,调用该方法时则啥也不做 |
void start() | 用于启动线程,Java虚拟机会自动调用该线程的run方法 |
执行流程
- 执行main方法的线程叫做主线程,执行run方法的线程叫做新线程/子线程
- main方法是程序的入口,对于start方法之前的代码来说,由主线程执行一次,当start方法调用成功后线程的个数由1个变成2个,新启动的线程去执行run方法的代码,主线程继续向下执行,两个线程各自独立运行互不影响
- 当run方法执行完毕后子线程结束,当main方法执行完毕后主线程结束
- 两个线程执行没有明确的先后执行次序,由操作系统调度算法来决定(时间片轮转法)
两种实现线程方式举例
-
继承Thread类并重写run()方法
package com.lagou.task18; /** * 创建和启动线程方式一:继承Thread类并重写run方法 */ public class SubThreadRun extends Thread{ @Override public void run() { for (int i = 1; i <= 20; i++) { System.out.println("run方法中的i = " + i); } } }
package com.lagou.task18; public class SubThreadRunTest { public static void main(String[] args) { //1、声明子类对象指向父类引用 Thread t1 = new SubThreadRun(); //2、分别测试调用run方法和start方法,查看线程情况 // t1.run(); //run方法相当于普通成员方法的调用,因此要执行完run方法才会往下接着执行 t1.start(); //调用start方法,java虚拟机会自动调用run方法,并创建一个新线程,此时就存在t1的线程和main方法线程两个线程,会根据调度争夺计算机资源 //3、main中写类似循环,用于测试线程 for (int i = 1; i <= 20; i++) { System.out.println("main中的i = " + i); } } }
-
实现Runnable接口并重写run()方法
package com.lagou.task18; /** * 创建和启动线程方式二:实现Runnable接口并重写run方法 */ public class SubRunnableRun implements Runnable { @Override public void run() { for (int i = 1; i <= 20; i++) { System.out.println("自定义中的i = " + i); } } }
package com.lagou.task18; public class SubRunnableRunTest { public static void main(String[] args) { //1、创建自定义对象,也就是Runnable接口实现类 SubRunnableRun srr = new SubRunnableRun(); //2、使用该对象作为实参构造Thread类型的对象,因为Runnable接口实现类中无run方法 //有源码可知:经过构造方法的调用后,Thread类中的成员变量target的数值为srr Thread t1 = new Thread(srr); //3、使用Thread类型对象调用start()方法 //若使用Runnable引用构造了线程对象,调用该方法时最终调用接口中的版本 /*有run方法的源码可知:Override public void run() { if (target != null) { target.run(); } } 此时target为srr,不为空,因此此时会调用target.run()方法,也就是srr.run()方法*/ t1.start(); //验证是否有两个线程互相争夺计算机资源 for (int i = 1; i <= 20; i++) { System.out.println("main中的i = " + i); } } }
方式的比较
- 继承Thread类的方式代码简单,但是若该类继承Thread类后则无法继承其他类(因为java是单继承的),而实现Runnable接口的方式代码复杂,但不影响该类继承其他类以及实现其他接口,因此以后实际开发中推荐使用实现Runnable接口的方式
创建和启动线程的第三种方式
实现Callable接口
-
从java5开始新增加创建线程的第三种方式为实现java.util.concurrent.Callable接口
-
常用的方法如下:
方法声明 功能介绍 V call() 计算结果并返回
FutureTask类
-
java.util.concurrent.FutureTask类用于描述可取消的异步计算,该类提供了Future接口的基本实现,包括启动和取消计算、查询计算是否完成以及检索计算结果的方法,也可以用于获取方法调用后的返回结果(说人话:在这里主要用于1、获取call()方法的返回结果2、由于继承了Runnable类)
-
常用方法如下:
方法声明 功能介绍 FutureTask(Callable callable) 根据参数指定的引用来创建一个未来任务 V get() 获取call方法计算的结果
具体实现代码举例
package com.lagou.task18;
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 {
int sum = 0;
//计算1-10000之间数字的累加和
for (int i = 1; i <= 10000; i++) {
sum += i;
}
System.out.println("计算的累加和是:" + sum); //50005000
return sum;
}
public static void main(String[] args) {
ThreadCallableTest threadCallableTest = new ThreadCallableTest();
FutureTask futureTask = new FutureTask(threadCallableTest);
//由源码可知:FutureTask实现了RunnableFuture接口,而RunnableFuture继承了Runnable接口,
//因此要使用FutureTask对象作为参数传入Thread构造方法以启动线程
Thread thread = new Thread(futureTask); //和Runnable接口创建和启动线程类似
thread.start(); //启动线程
Object sum = null;
try {
sum = futureTask.get(); //获取call()方法返回的结果
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
System.out.println("线程处理方法的返回值是:" + sum); //50005000
}
}
匿名内部类的实现方式
-
使用匿名内部类的方式来创建和启动线程
package com.lagou.task18; public class ThreadAnonymousTest { public static void main(String[] args) { //匿名内部类的语法格式:父类/接口类型 引用变量名 = new 父类/接口类型(){方法重写} //1、使用继承Thread类+匿名内部类方式创建并启动线程 /*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+匿名内部类方式创建并启动线程 /*Runnable ra = new Runnable() { @Override public void run() { System.out.println("李四说:在"); } }; Thread t2 = new Thread(ra); t2.start();*/ //java8开始,接口的匿名内部类可以使用Lambda表达式实现 /*Runnable ra = () -> { System.out.println("李四说:在"); }; new Thread(ra).start();*/ new Thread(() -> { System.out.println("李四说:在"); }).start(); } }
线程的生命周期(熟悉)
- 新建状态:使用new关键字创建之后进入的状态,此时线程并没有开始执行
- 就绪状态:调用start方法后进入的状态,此时线程还是没有开始执行
- 运行状态:使用线程调度器调用该线程后进入的状态,此时线程开始执行,当线程的时间片执行完毕后任务没有完成时回到就绪状态
- 消亡状态:当线程的任务执行完成后进入的状态,此时线程已经终止
- 阻塞状态:当线程执行的过程中发生了阻塞事件进入的状态,如:sleep()方法。阻塞状态解除后进入就绪状态
线程的编号和名称(熟悉)
方法声明 | 功能介绍 |
---|---|
long getId() | 获取调用对象所表示线程的编号 |
String getName() | 获取调用对象所表示线程的名称 |
void setName(String name) | 设置/修改线程的名称为参数指定的数值 |
static Thread currentThread() | 获取当前正在执行线程的引用 |
-
案例题目
自定义类继承Thread类并重写run方法,在run方法中先打印当前线程的编号和名称,然后将线程的名称修改为“zhangfei”后再次打印编号和名称。要求在main方法中也要打印主线程的编号和名称package com.lagou.task18; public class ThreadNameIdTest extends Thread { public ThreadNameIdTest(String name) { super(name); //要实现有参构造方法指定名字,需要定义有参构造方法并且将参数传给父类Thread的有参构造方法中 } @Override public void run() { System.out.println("子线程编号是:" + getId() + ",名称是:" + getName()); setName("zhangfei"); //修改子线程名字 System.out.println("修改后子线程编号是:" + getId() + ",名称是:" + getName()); } public static void main(String[] args) { ThreadNameIdTest tit = new ThreadNameIdTest("guanyu"); tit.start(); //接下来实现打印main主线程中的id和名字 Thread t = Thread.currentThread(); //需要使用currentThread方法获取当前主线程生成一个Thread类才能获得id和名字 System.out.println("当前主线程编号是:" + t.getId() + ",名字是:" + t.getName()); } }
-
注意:若实现Runnable接口方式创建线程,要获取名字和id,需要先使用currentThread方法获取当前执行线程的引用
package com.lagou.task18; public class RunnableIdNameTest implements Runnable { @Override public void run() { //若实现Runnable接口方式创建线程,要获取名字和id,需要先使用currentThread方法获取当前执行线程的引用 Thread thread = Thread.currentThread(); System.out.println("当前子线程的编号是:" + thread.getId() + ",名字是:" + thread.getName()); thread.setName("zhangfei"); System.out.println("修改后子线程的编号是:" + thread.getId() + ",名字是:" + thread.getName()); } public static void main(String[] args) { RunnableIdNameTest rint = new RunnableIdNameTest(); Thread t1 = new Thread(rint, "guanyu"); //rint作为实参并且指定线程名字为guanyu t1.start(); } }
常用的方法
方法声明 | 功能介绍 |
---|---|
static void yeild() | 当前线程让出处理器(离开Running状态),使当前线程进入Runnable状态等待 |
static void sleep(times) | 使当前线程从Running放弃处理器进入Block状态,休眠times毫秒,再返回到Runnable。 如果其他线程打断当前线程的Block(sleep),就会发生InterruptedException |
int getPriority() | 获取线程的优先级 |
void setPriority(int newPriority) | 修改线程的优先级。 优先级越高的线程不一定执行,但该线程获取到的时间片机会更多 |
void join(times) | 无参:调用该方法的引用线程拿到控制权,直到该线程终止 |
void join(long millis) | 拿到控制权的时间最多仅为millis毫秒 |
boolean isDaemon() | 用于判断是否为守护线程 |
void setDaemon(boolean on) | 用于设置线程为守护线程(必须在线程start之前设置子线程为守护线程) 当子线程是守护线程时,主线程结束后,则子线程会随之结束 |
-
案例题目
编程创建两个线程,线程一负责打印1-100之间的所有奇数,其中线程二负责打印1-100之间的所有偶数。在main方法启动上述两个线程同时执行,主线程等待两个线程终止
两个线程类(以extends Thread为例)package com.lagou.task18; public class SubThread1 extends Thread { @Override public void run() { for (int i = 1; i <= 100; i+=2) { System.out.println("线程一中打印奇数:" + i); } } }
package com.lagou.task18; public class SubThread2 extends Thread{ @Override public void run() { for (int i = 2; i <= 100; i+=2) { System.out.println("线程二中打印偶数:" + i); } } }
main类
package com.lagou.task18; public class SubThreadTest { public static void main(String[] args) { SubThread1 s1 = new SubThread1(); SubThread2 s2 = new SubThread2(); //启动两个线程 s1.start(); s2.start(); //主线程等待两个线程执行完毕 System.out.println("主线程等待中..."); try { s1.join(); s2.join(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("主线程可以运行啦..."); } }
线程同步机制(重点)
基本概念
- 当多个线程同时访问同一种共享资源时,可能会造成数据的覆盖等不一致性问题,此时就需要对线程之间进行通信和协调,该机制就叫做线程的同步机制
- 多个线程并发读写同一个临界资源时会发生线程并发安全问题
- 异步操作:多线程并发的操作,各自独立运行
- 同步操作:多线程串行的操作,先后执行的顺序
解决方案
- 产生问题原因 --> 由程序可知:当两个线程同时对同一个账户进行取款时,导致最终的账户余额不合理
- 引发原因:线程一执行取款时还没来得及将取款后的余额更新写入后台,线程二就已经开始取款。这时候线程二的账户余额还是默认为原来的余额,因此导致取款余额出现错误
- 解决方案:让线程一执行完毕取款操作后,再让线程二执行即可,将线程的并发操作改为串行操作
- 经验分享:在以后的开发中尽量减少串行操作的范围,从而提高效率
实现方式
-
在Java语言中使用synchronized关键字来实现同步/对象锁机制从而保证线程执行的原子性(原子性即不可被更改抢夺),具体有如下两种方式
-
方式一:使用同步代码块的方式实现部分代码的锁定,格式如下
synchronized(类类型的引用) { //注意:是引用,不能放new对象!!! 编写所有需要锁定的代码; }
举例如下:
package com.lagou.task18; public class AccountRunnableTest implements Runnable { private int balance; //银行账户余额 //注意:当我们需要创建多个AccountRunnableTest对象并分别启动线程时,将会分别调用run()方法,这样无法达到锁的作用。如果需要锁,则需要将类类型引用用static修饰,使之隶属于类层级,所有对象共享同一个-->确保是唯一的锁 private static Locker locker = new Locker(); //创建类类型引用,给上锁 public AccountRunnableTest() { } public AccountRunnableTest(int balance) { this.balance = balance; } public int getbalance() { return balance; } private void setbalance(int balance) { this.balance = balance; } @Override public void run() { System.out.println("线程" + Thread.currentThread().getName() + "已启动..."); synchronized (locker) { //上锁(若是后面只有一个当前类对象,可以用this作为引用) //1、模拟从后台查询用户账户余额得过程 int temp = getbalance(); //当前账户余额 //2、模拟取200元的过程 if (temp > 200) { System.out.println("正在出钞,请稍后..."); temp -= 200; try { Thread.sleep(5000); //等待5秒,模拟出钞效果 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("出钞完毕,请取钱..."); } else { System.out.println("余额不足,请核对您的账户余额!"); } //3、模拟将最新的账户余额写入到后台 setbalance(temp); } } public static void main(String[] args) { AccountRunnableTest art = new AccountRunnableTest(1000); Thread t1 = new Thread(art); Thread t2 = new Thread(art); //启动线程 t1.start(); t2.start(); //开始取款操作 System.out.println("主线程等待中..."); try { t1.join(); t2.join(); } catch (InterruptedException e) { e.printStackTrace(); } //显示取款后余额 System.out.println("取款后账户余额为:" + art.getbalance()); //若无加锁,会发现两个线程分别取完200后还是剩下800 } } class Locker{} //用来创建锁的类类型引用,注意只能有一个主类,因此这里不能再用public修饰
-
方式二:使用同步方法的方式实现所有代码的锁定
直接使用synchronized关键字来修饰整个方法即可
该方式等价于:synchronized(this) {整个方法体的代码;}
举例如下:@Override public synchronized void run() {函数体;}
当我们是需要创建多个AccountRunnableTest对象并分别启动线程时,将会分别调用run()方法,这样无法达到锁的作用。此时需要使用静态方法。由于run()是重写的不能用static修饰,因此可以将函数体放到另一个方法中,该方法用static修饰,再在run()中调用该方法即可(详见”静态方法的锁定“)
-
-
使用synchronized锁与仅仅只是更改代码顺序使线程执行顺序出现先后之间的区别:使用synchronized锁时所有线程将会全部先跑起来,之后在抢夺到cpu资源后就会占用直到当前线程完全运行结束;使用更改代码顺序原理是让线程创建出现先后顺序–>这样就失去了创建多线程的意义了
静态方法的锁定
- 当我们需要对一个静态方法加锁,对应的两种方式如下:
- 方式一:使用同步代码块时
那么该方法锁的对象是类对象。每个类对象都有唯一的一个类对象。获取类对象的方式:类名.class。如:synchronized(AccountRunnableTest.class){被锁部分} //该类型对应的Class对象,由于类型是固定的,因此Class对象也是唯一的,因此可以实现同步
- 方式二:使用同步方法时
public synchronized static void xxx() {...}
- 方式一:使用同步代码块时
注意事项
- 使用synchronized保证线程同步应当注意:
- 多个需要同步的线程在访问同步块时,看到的应该是同一个锁对象引用
- 在使用同步块时应当尽量减少同步范围以提高并发的执行效率
线程安全类和线程不安全类
- StringBuffer类是线程安全的类,但StringBuilder类不是线程安全的类
- Vector类和Hashtable类是线程安全的类,但ArrayList类和HashMap类不是线程安全的类;Collections.synchronizedList()和Collections.synchronizedMap()等方法可以实现将不安全的ArrayList类和HashMap类变为线程安全–>Vector和Hashtable彻底淘汰
- 看源码可知:线程安全的类中方法都是有synchronized关键字修饰的;而线程不安全的类中没有
死锁的概念
-
线程一执行的代码:
public void run() { synchronized(a) { //持有对象锁a,等待对象锁b synchronized(b) { 编写锁定的代码; } } }
-
线程二执行的代码
public void run() { synchronized(b) { //持有对象锁b,等待对象锁a synchronized(a) { 编写锁定的代码; } } }
-
注意:在以后的开发中尽量减少同步的资源,减少同步代码块的嵌套结构的使用!
使用Lock(锁)实现线程同步
基本概念
- 从Java5开始提供了更强大的线程同步机制——使用显式定义的同步锁对象来实现
- java.util.concurrent.locks.Lock接口是控制多个线程对共享资源进行访问的工具
- 该接口中的主要实现类是ReentrantLock类(reentrant:可重入,可重入的),该类拥有与synchronized相同的并发性,经常使用ReentrantLock类显式加锁和释放锁
常用方法
方法声明 | 功能介绍 |
---|---|
ReentrantLock() | 使用无参方式构造对象 |
void lock() | 获取锁 |
void unlock() | 释放锁 |
使用举例
package com.lagou.task18;
import java.util.concurrent.locks.ReentrantLock;
public class AccountRunnableTest implements Runnable {
private int balance; //银行账户余额
private ReentrantLock lock = new ReentrantLock(); //准备好了一把锁
...构造方法、get和set方法
@Override
public void run() {
System.out.println("线程" + Thread.currentThread().getName() + "已启动...");
lock.lock(); //上锁
...需要上锁的代码
lock.unlock(); //解锁
}
public static void main(String[] args) {
AccountRunnableTest art = new AccountRunnableTest(1000);
Thread t1 = new Thread(art);
Thread t2 = new Thread(art);
//启动线程
t1.start();
t2.start();
//开始取款操作
System.out.println("主线程等待中...");
try {
t1.join();
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
//显示取款后余额
System.out.println("取款后账户余额为:" + art.getbalance()); //若无加锁,会发现两个线程分别取完200后还是剩下800
}
}
与synchronized方式的比较
- Lock是显式锁,需要手动实现开启和关闭操作,而synchronized是隐式锁,执行锁定代码后自动释放
- Lock只有同步代码块方式的锁,而synchronized有同步代码块方式和同步方法两种锁(方式)
- 使用Lock锁方式时,Java虚拟机将花费较少的时间来调度线程,因此性能更好
Object类实现线程间通信常用的方法
方法声明 | 功能介绍 |
---|---|
void wait() | 用于使得线程进入等待状态,直到其他线程调用notify()或notifyAll()方法 |
void wait(long timeout) | 用于进入等待状态,直到其他线程调用方法或参数指定的毫秒数已经过去为止 |
void notify() | 用于唤醒等待的单个线程 |
void notifyAll() | 用于唤醒等待的所有线程 |
注:sleep()和wait()方法间的区别(理解线程间通信和锁机制)
- 最主要的区别是sleep方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或者方法。
- wait,notify和notifyAll只能在同步控制方法或者同步控制块里面使用,而sleep可以在任何地方使用(使用范围)
- 这两个方法来自不同的类分别是Thread和Object
- sleep必须捕获异常,而wait,notify和notifyAll不需要捕获异常
- sleep是Thread类的静态方法。sleep的作用是让线程休眠制定的时间,在时间到达时恢复,也就是说sleep将在接到时间到达事件事恢复线程执行。wait是Object的方法,也就是说可以对任意一个对象调用wait方法,调用wait方法将会将调用者的线程挂起,直到其他线程调用同一个对象的notify方法才会重新激活调用者。
使用举例:
package com.lagou.task18;
public class ThreadCommunicateTest implements Runnable {
private int cnt = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//每当线程获得计算机资源后,就大喊唤醒另一个线程,让它开始来争夺计算机资源
notify();
if (cnt <= 100) {
//让当前线程打印cnt的数
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();
//将tct作为参数,创建两个线程并分别启动线程
Thread t1 = new Thread(tct);
t1.start();
Thread t2 = new Thread(tct);
t2.start();
}
}
生产者与消费者
生产者和消费者模型
注:可以将生产者和消费者分别看作一个线程
生产者和消费者模型的实现
仓库类:
package com.lagou.task18;
/**
* 编程实现仓库类
*/
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 consumeProduct() {
notify(); //消费前记得先唤醒,避免陷入死锁
if (cnt > 0) {
System.out.println("线程" + Thread.currentThread().getName() + "正在消费第" + (cnt + 1) + "个产品.....");
cnt--;
} else {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
生产者类:
package com.lagou.task18;
/**
* 编程实现生产者模型,不断地生产产品
*/
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(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
消费者类:
package com.lagou.task18;
/**
* 消费者线程
*/
public class ConsumerThread extends Thread {
//声明一个仓库类型的引用作为成员变量,是为了能调用仓库类中的生产方法 合成复用
private StoreHouse storeHouse;
//该构造方法为了确保两个线程共用同一个仓库
public ConsumerThread(StoreHouse storeHouse) {
this.storeHouse = storeHouse;
}
@Override
public void run() {
while (true) {
//从仓库中取出产品消费
storeHouse.consumeProduct();
//消费完一个让其休息一下
try {
Thread.sleep(1000); //消费者sleep时间比生产者时间多,因此生产会比消费来的快
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
main方法:
package com.lagou.task18;
public class StoreHouseTest {
public static void main(String[] args) {
StoreHouse storeHouse = new StoreHouse();
ProduceThread produceThread = new ProduceThread(storeHouse); //创建生产者线程
ConsumerThread consumerThread = new ConsumerThread(storeHouse); //创建消费者线程
produceThread.start();
consumerThread.start();
}
}
线程池的概念和使用
线程池的由来
- 在服务器编程模型的原理,每一个客户端连接用一个单独的线程为之服务,当与客户端的会话结束时,线程也就结束了,即每来一个客户端连接,服务器端就要创建一个新线程
- 如果访问服务器的客户端很多,那么服务器就要不断地创建和销毁线程,这将严重影响服务器地性能
概念和原理
- 线程池的概念:首先创建一些线程,它们的集合称为线程池,当服务器接收到一个客户请求后,就从线程池中取出一个空闲的线程为之服务,服务完成后不关闭该线程,而是将该线程还回到线程池中
- 在线程池的编程模式下,任务是提交给整个线程池,而不是直接交给某个线程,线程池在拿到任务后,它就在内部找有无空闲的线程,再把任务交给内部某个空闲的线程,任务是提交给整个线程池,一个线程同时只能执行一个任务,但可以同时向一个线程池提交多个任务
相关类和方法
-
从java5开始提供了线程池的相关类和接口:java.util.concurrent.Executors类和java.util.concurrent.ExecutorService接口
-
其中Executors是个工具类和线程池的工厂类,可以创建并返回不同类型的线程池,常用方法如下:
方法声明 功能介绍 static ExecutorService newCachedThreadPool() 创建一个可根据需要创建新线程的线程池 static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程数的线程池 static ExecutorService newSingleThreadExector() 创建一个只有一个线程的线程池 -
其中ExecutorService接口是真正的线程池接口,主要实现类是ThreadPoolExecutor,常用方法如下:
方法声明 功能介绍 void execute(Runnable command) 执行任务和命令,通常用于执行Runnable Future submit(Callable task) 执行任务和命令,通常用于执行Callable void shutdown() 启动有序关闭
任务总结
- 多线程(重点)
概念、创建方式、线程的生命周期、线程的编号和名称、常用的方法、线程的同步、synchronized关键字、线程的通信、生产者消费者模型、线程池等
网络编程
网路编程的常识
- 目前主流的网络通讯软件有:微信、QQ、飞信、阿里旺旺、陌陌、探探、…
七层网络模型
- OSI(Open System Interconnect),即开放式系统互联,是ISO(国际标准化组织)组织在1985年研究的网络互连模型
- OSI七层模型和TCP/IP五层模型的划分如下:
- 网络传输过程:层层加包–>运输–>层层拆包
- 当发送数据时,需要对发送的内容按照上述七层模型进行层层加包后发送出去
- 当接收数据时,需要对接收的内容按照上述七层模型相反的次序层层拆包并显示出来
相关的协议(笔试题)
协议的概念
- 计算机在网络中实现通信就必须有一些约定或者规则,这种约定或规则就叫做通信协议,通信协议可以对速率、传输代码、代码结构、传输控制步骤、出错控制等制定统一的标准
TCP协议
- 传输控制协议(Transmission Control Protocol),是一种面向连接的协议,类似于打电话
-
建立连接–>进行通信–>断开连接
-
在传输前采用“三次握手”方式建立连接
-
在传输后使用“四次挥手”方式断开连接
-
在通信的整个过程中全程保持连接,形成数据传输通道
-
保证了数据传输的可靠性和有序性
-
是一种全双工的字节流通信方式,可以进行大数据量的传输
-
UDP协议
- 用户数据协议(User Datagram Protocol),是一种非面向连接的协议,类似于写信
- 在通信的整个过程中不需要保持连接,其实是不需要建立连接
- 不保证数据传输的可靠性和有序性
- 是一种全双工的数据报通信方式,每个数据报的大小限制再64K内
- 发送数据完毕后无需释放资源,开销小,发送数据的效率比较高,速度快
IP地址与端口号(重点)
IP地址
- 192.168.1.1,是绝大多数路由器的登录地址,主要配置用户名和密码以及Mac过滤
- IP地址是互联网中的唯一地址标识,本质上是由32位二进制组成的整数,叫做IPv4。由于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
端口号
-
IP地址:可以定位到某一台设备
端口号:可以定位到该设备中具体某一个进程
-
端口号本质上是16位二进制组成的整数,表示范围是:0 ~ 65535,其中0 ~ 1024之间的端口号通常被系统占用,建议编程从1025开始使用
-
特殊的端口
HTTP:80 FTP:21 Oracle:1521 MySQL:3306 Tomcat:8080 -
网路编程需要提供:IP地址 + 端口号,组合在一起叫做网络套接字Socket
基于TCP协议的编程模型(重点)
C/S架构的简介
- C即Client,S即Server,C/S架构即客户端/服务器架构
- 在C/S模式下客户向服务器发送服务请求,服务器接收请求后提供服务
- 例如:在一个酒店中,顾客找服务员点菜,服务员把点菜单通知厨师,厨师按点菜单做好菜后让服务员端给客户,这就是一种C/S工作方式。如果把酒店看作一个系统,服务员就是客户端,厨师就是服务器。这种系统分工和协同工作的方式就是C/S的工作方式
- 客户端部分:为每个用户所专有的,负责执行前台功能
- 服务器部分:由多个用户共享的信息与功能,招待后台服务
编程模型
- 服务器端:
- 创建ServerSocket类型的对象并提供端口号
- 等待客户端的连接请求,调用accept()方法
- 使用输入输出流进行通信
- 关闭Socket
- 客户端
- 创建Socket类型的对象并提供服务器的IP地址和端口号
- 使用输入输出流进行通信
- 关闭Socket
相关类和方法的解析
ServerSocket类
-
java.net.ServerSocket类主要用于描述服务器套接字信息(大插排)
-
常用的方法如下:
方法声明 功能介绍 ServerSocket(int port) 根据参数指定的端口号来构造对象 Socket accept() 侦听并接收到此套接字的连接请求 void close() 用于关闭套接字
Socket类
-
java.net.Socket类主要用于描述客户端套接字,是两台机器间通信的端点(小插排)
-
常用的方法如下:
方法声明 功能介绍 Socket(String host, int port) 根据指定主机名和端口来构造对象 InputStream getInputStream() 用于获取当前套接字的输入流 OutputStream getOutputStream() 用于获取当前套接字的输出流 void close() 用于关闭套接字
ServerSocket和Socket类间的区别
主要是在服务器端进程中的Socket对象是由ServerSocket对象调用accept()方法获得的–>即当有一个客户端连接到服务器端才会创建一个Socket类对象(若没有客户端线程连接,则调用accpet()方法时陷入阻塞状态)
注意事项
- 客户端Socket与服务器Socket对应,都包含输入和输出流
- 客户端的socket.getInputStream() 连接于服务器socket.getOutputStream()
- 客户端的socket.getOutputStream() 连接于服务器socket.getInputStream()
实现举例
实现服务器端和客户端不断地通信
- 服务器端:
package com.lagou.task19;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
/**
* 服务器端
*/
public class ServerStringTest {
public static void main(String[] args) {
ServerSocket server = null;
Socket socket = null;
BufferedReader br = null;
PrintStream ps = null; //用来实现服务器向客户端写入数据
Scanner sc = new Scanner(System.in);
try {
//1、创建server类型的对象(服务器)并提供端口号
server = new ServerSocket(8888);
//2、等待客户端的连接请求,调用accept()方法
System.out.println("等待客户端的连接请求...");
socket = server.accept(); //当没有客户端连接时,则服务器阻塞在accept()方法的调用这里
System.out.println("客户端连接成功...");
br = new BufferedReader(new InputStreamReader(socket.getInputStream())); //注意要转换为字符流
ps = new PrintStream(socket.getOutputStream());
//3、使用输入输出流进行通信
while (true) {
//实现对客户端发来的字符串接收并打印
String receive = br.readLine(); //当没有数据发来时,会在这里形成阻塞
System.out.println("接收到客户端的字符串是:" + receive);
//当客户端发来的内容为bye时则聊天结束
if ("bye".equalsIgnoreCase(receive)) {
System.out.println("客户端下线!");
break;
}
//4、向客户端发送信息
System.out.println("请输入要向客户端发送的消息:");
String str = sc.next();
ps.println(str);
System.out.println("向客户端发送消息完毕...");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
//4、关闭Socket并释放资源
if (null != socket) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != server) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 客户端:
package com.lagou.task19;
import java.io.*;
import java.net.Socket;
import java.util.Scanner;
/**
* 客户端
*/
public class ClientStringTest {
public static void main(String[] args) {
Socket client = null;
PrintStream ps = null;
Scanner sc = new Scanner(System.in);
String str;
BufferedReader br = null; //用来接收服务器发送来的消息
try {
//1、创建Socket类型的的对象并提供服务器的主机名和端口号
//由于现在测试的服务器就是本机,因此服务器主机名使用本机主机名或127.0.0.1本机回环地址
client = new Socket("127.0.0.1", 8888);
System.out.println("连接服务器成功...");
ps = new PrintStream(client.getOutputStream()); //getOutputStream()返回OutputStream类型
br = new BufferedReader(new InputStreamReader(client.getInputStream()));
while (true) {
//2、使用输入输出流进行通信
// Thread.sleep(1000); //等待一下
//实现向服务器写入"hello world"字符串内容
System.out.println("请输入要向服务器发送的内容:");
str = sc.next(); //实现用户输入字符串
System.out.println("客户端发送数据成功...");
ps.println(str);
// ps.println("hello world");
if ("bye".equalsIgnoreCase(str)) {
System.out.println("聊天结束!");
break;
}
//3、接收服务器发送来的消息
String receive = br.readLine();
System.out.println("从服务器接收到的内容是:" + receive);
}
} catch (IOException /*| InterruptedException */e) {
e.printStackTrace();
} finally {
if (null != sc) {
sc.close();
}
if (null != ps) {
ps.close();
}
//3、关闭socket并释放有关资源
if (null != client) {
try {
client.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
- 若要实现服务器根据接收的客户端创建多线程,则可将服务器端的输入输出放入线程实现类run()中。在服务器类中使用循环,当连接到一个客户端就创建一个新线程。具体代码实现如下
线程实现类ServerThread:
package com.lagou.task19;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.InetAddress;
import java.net.Socket;
import java.util.Scanner;
public class ServerThread implements Runnable {
//由于socket是需要从ServerStringTest端传送而来的,因此这里使用合成复用原则:创建构造方法使得创建对象的时候传入即可
private Socket socket;
public ServerThread(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader br = null;
PrintStream ps = null; //用来实现服务器向客户端写入数据
try {
br = new BufferedReader(new InputStreamReader(socket.getInputStream())); //注意要转换为字符流
ps = new PrintStream(socket.getOutputStream());
Scanner sc = new Scanner(System.in);
//3、使用输入输出流进行通信
while (true) {
//实现对客户端发来的字符串接收并打印
String receive = br.readLine(); //当没有数据发来时,会在这里形成阻塞
InetAddress inetAddress = socket.getInetAddress();
System.out.println("接收到客户端" + inetAddress + "的字符串是:" + receive);
//当客户端发来的内容为bye时则聊天结束
if ("bye".equalsIgnoreCase(receive)) {
System.out.println("客户端" + inetAddress + "下线!");
break;
}
//4、向客户端发送信息
System.out.println("请输入要向客户端发送的消息:");
String str = sc.next();
ps.println(str);
System.out.println("向客户端发送消息完毕...");
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//关闭资源
if (null != br) {
try {
br.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != ps) {
ps.close();
}
}
}
}
服务器类:
package com.lagou.task19;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* 服务器端
*/
public class ServerStringTest {
public static void main(String[] args) {
ServerSocket server = null;
Socket socket = null;
try {
//实现有新的客户端连接后就在服务器中启动一个新线程
//1、创建server类型的对象(服务器)并提供端口号->注意这里只需要一个
server = new ServerSocket(8888);
while (true) {
//2、等待客户端的连接请求,调用accept()方法
System.out.println("等待客户端的连接请求...");
socket = server.accept(); //当没有客户端连接时,则服务器阻塞在accept()方法的调用这里
System.out.println("客户端" + socket.getInetAddress() + "连接成功...");
//每当有一个客户端连接成功,就要启动一个新的线程为之服务
new Thread(new ServerThread(socket)).start();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
//4、关闭Socket并释放资源
if (null != socket) {
try {
socket.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (null != server) {
try {
server.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
基于UDP协议的编程模型(熟悉)
编程模型
- 接收方
- 创建DatagramSocket类型的对象并提供端口号(相当于1、创建邮局)
- 创建DatagramPacket类型的对象并提供缓冲区(相当于2、创建包裹快递柜)
- 通过Socket接收数据内容放到Packet中,调用receive方法(相当于3、接收包裹)
- 关闭Socket
- 发送方
- 创建DatagramSocket类型的对象(相当于1、创建邮局邮局)
- 创建DatagramPacket类型的对象并提供接收方的通信地址(相当于2、将包裹写上接收地址信息后准备发送)
- 通过Socket将Packet中的数据内容发送出去,调用send方法(相当于3、邮局将包裹发送出去)
- 关闭Socket
相关类和方法的解析
DatagramSocket类
-
java.net.DatagramSocket类主要用于描述发送和接收数据报的套接字(邮局)。
换句话说,该类就是包裹投递服务的发送或接收点 -
常用的方法如下:
方法声明 功能介绍 DatagramSocket() 使用无参的方式构造对象 DatagramSocket(int port) 根据参数指定的端口号来构造对象 void receive(DatagramPacket p) 用于接收数据报存放到参数指定的位置 void send(DatagramPacket p) 用于将参数指定的数据报发送出去 void close() 关闭Socket并释放相关资源
DatagramPacket类
-
java.net.DatagramPacket类主要用于描述数据报,数据报用来实现无连接包裹投递服务
-
常用的方法如下:
方法声明 功能介绍 DatagramPacket(byte[] buf, int length) 根据参数指定的数组来构造对象,用于接收长度为length的数据报 DatagramPacket(byte[] buf, int length, InetAdress address, int port) 根据参数指定数组来构造对象,将数据报发送到指定地址和端口 inetAddress getAddress() 用于获取发送方或接收方的通信地址 int getPort() 用于获取发送发或接收方的端口号 int getLength() 用于获取发送数据或接收数据的长度
InetAddress类
-
java.net.InetAddress类主要用于描述互联网通信地址信息
-
常用的方法如下:
方法声明 功能介绍 static InetAddress getLocalHost() 用于获取当前主机的通信地址 static InetAddress getByName(String host) 根据参数指定的主机名获取通信地址
实现举例
实现接收方和发送方互相发送数据
-
先创建接收方(先运行)
package com.lagou.task19; import java.io.IOException; import java.net.DatagramPacket; import java.net.DatagramSocket; /** * 接收方:服务器 */ public class ReceiveTest { public static void main(String[] args) { DatagramSocket ds = null; try { //1、创建DatagramSocket类型的对象并提供端口号(1、创建接收邮局) ds = new DatagramSocket(8888); //2、创建DatagramPacket类型的对象并提供缓冲区(2、创建快递柜) byte[] bArr = new byte[20]; DatagramPacket dp = new DatagramPacket(bArr, bArr.length); //3、通过Socket接收数据内容存放到Packet里面,调用receive方法(3、接收包裹,放入快递柜) System.out.println("等待数据的到来..."); ds.receive(dp); //将接收到的数据放入dp中,也就是字符数组bArr中 System.out.println("接收到的数据内容是:" + new String(bArr, 0, dp.getLength())); //接收方接收到消息后向发送方回复消息“I received” bArr = "I received".getBytes(); dp = new DatagramPacket(bArr, bArr.length, dp.getAddress(), dp.getPort()); //根据发送方地址和端口发送信息给发送方 ds.send(dp); System.out.println("回发数据成功..."); } catch (IOException e) { e.printStackTrace(); } finally { //4、关闭Socket并释放有关资源 if (ds != null) { ds.close(); } } } }
-
再创建发送方(后运行)
package com.lagou.task19; 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对象(1、创建发送邮局) ds = new DatagramSocket(); //2、创建DatagramPacket对象并提供接收方的通信地址和端口号(2、给出接收地址) byte[] bArr = "hello".getBytes(); DatagramPacket dp = new DatagramPacket(bArr, bArr.length, InetAddress.getLocalHost(), 8888);//(3、创建包裹) //3、通过Socket发送Packet,调用send方法(发送包裹) ds.send(dp); System.out.println("发送数据成功!"); //回收接收方发来的消息 bArr = new byte[20]; dp = new DatagramPacket(bArr, bArr.length); ds.receive(dp); System.out.println("从接收方接收到的数据是:" + new String(bArr, 0, dp.getLength())); } catch (IOException e) { e.printStackTrace(); } finally { //4、关闭ds并释放相关资源 if (ds != null) { ds.close(); } } } }
URL类(熟悉)
基本概念
- java.net.URL(Uniform Resource Identifier)类主要用于表示统一得资源定位器,也就是指向了万维网上“资源”的指针(通过URL可以定位到互联网上的资源)。这个资源可以是简单的文件或目录,也可以是对复杂对象的引用,例如对数据库或搜索引擎的查询等
- 通过URL可以访问万维网上的网络资源,最常见的是www和ftp站点,浏览器通过解析给定的URL可以在网络上查找相应的资源
- URL的基本结构如下:
<传输协议>://<主机名>:<端口号>:/<资源地址>
常用的方法
方法声明 | 功能介绍 |
---|---|
URL(String spec) | 根据参数指定的字符串信息构造对象 |
String getProtocol() | 获取协议名称 |
String getHost() | 获取主机名称 |
int getPort() | 获取端口号 |
Stirng getPath() | 获取路径信息 |
String getFile() | 获取文件名 |
URLConnection openConnection() | 获取URLConnection类的实例 |
URLConnection类
基本概念
- java.net.URLConnection类是个抽象类,该类表示应用程序和URL之间的通信链接的所有类的超类,主要实现类有支持HTTP特有功能的HttpURLConnection类
HttpURLConnection类的常用方法
方法声明 | 功能介绍 |
---|---|
InputStream getInputStream() | 获取输入流(从网络上读取下载资源) |
void disconnect() | 断开连接 |
任务总结
- 网络编程
网络编程的常识、基于tcp协议的编程模型、基于udp协议的编程模型、URL类等
反射机制
基本概念
-
通常情况下编写代码都是固定的,无论运行多少次执行的结果是固定的,在某些特殊场合中编写代码时不确定要创建什么类型的对象,也不确定要调用什么样的方法,这些都希望通过运行时传递的参数来决定,该机制叫做动态编程技术,也就是反射机制
-
通俗来说,反射机制就是用于动态创建对象并且动态调用方法的机制
-
目前主流的框架底层都是采用反射机制来实现的
-
如以下两行代码都是固定代码,无法达到动态编程效果
Person p = new Person(); //表示声明Person类型的引用指向Person类型的对象(固定) p.show(); //表示调用Person类中的成员方法show()
Class类
基本概念
- java.lang.Class类的实例可以用于描述Java应用程序中的类和接口,也就是一种数据类型
- 该类没有公共构造方法(无法new实例),该类的实例由Java虚拟机和类加载器自动构造完成,本质上就是加载到内存中的运行时类
获取Class对象的方式
- 使用
数据类型.class
的方式可以获取对应类型的Class对象(掌握) - 使用
引用/对象.getClass()
的方式可以获取对应类型的Class对象 - 使用
包装类.TYPE
的方式可以获取对象基本数据类型的Class对象 - 使用
Class.forName()
的方式来获取参数指定类型的Class对象(掌握)- 注意forName()传入的实参必须是完整的名称
包名.类名
。如:Class.forName("java.lang.String")
- 该方法不能获取基本数据类型的Class对象。如:
Class.forName("int"); //这是错误的
- 注意forName()传入的实参必须是完整的名称
- 使用类加载器ClassLoader的方式获取指定类型的Class对象
常用的方法(掌握)
方法声明 | 功能介绍 |
---|---|
static Class<?> forName(String className) | 用于获取参数指定类型对应的Class对象并返回 |
T newInstance() | 用于创建一个该Class对象所表示类的新实例(已过时) |
Constructor类
基本概念
- java.lang.reflect.Constructor类主要用于描述获取到的构造方法信息(获取构造方法)
Class类的常用方法
方法声明 | 功能介绍 |
---|---|
Constructor getConstructor(Class<?>… parameterTypes) | 用于获取此Class对象与参数相对应的公共构造方法 |
Constructor<?>[] getConstructors() | 用于获取此Class对象所表示类型中所有的公共构造方法 |
Constructor类的常用方法
方法声明 | 功能介绍 |
---|---|
T newInstance(Object… initargs) | 使用此Constructor对象描述的构造方法来构造Class对象代表类型的新实例 |
int getModifiers() | 获取方法的访问修饰符 |
String getName() | 获取方法的名称 |
Class<?>[] getParameterTypes() | 获取方法所有参数的类型 |
代码举例
如何使用反射机制动态地获取实例
package com.lagou.task20;
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 p1 = new Person();
System.out.println("原始无参方式构造的对象是:" + p1);
System.out.println("-------------------------------------------------------");
//2、使用动态编程技术(反射)无参方式构造对象
//2-1、使用forName()获取参数指定的Class对象
/*
//方式一:forName()中参数直接指定要创建的对象路径
Class<?> aClass = Class.forName("com.lagou.task20.Person");
*/
/*
//方式二:forName()中参数使用输入方式得到
System.out.println("请输入要创建对象的全路径名称:");
Scanner scanner = new Scanner(System.in);
String string = scanner.next();
Class<?> aClass = Class.forName(string);
*/
//方式三:将forName()参数写入配置文件properties中,直接从配置文件中读取即可 -> 以后现实开发中都是用此方式
BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream("E:\\FilesForTest\\LaGou_Java\\reflection.txt")));
String path = br.readLine();
Class<?> aClass = Class.forName(path);
//2-2、使用创建出来的Class对象创建指定无参对象
// System.out.println("无参方式创建的对象是:" + aClass.newInstance()); //null, 0 --> newInstance()函数已经过时
//2-2-1、获取Class对象对应类中的无参构造方法,也就是Person类中的无参构造方法
Constructor<?> constructor = aClass.getConstructor();
//2-2-2、使用获取到的无参构造方法来构造对应类型的对象,也就是Person类型的对象
System.out.println("无参方式创建的对象是:" + constructor.newInstance());
System.out.println("------------------------------------------------------------");
//3、使用原始有参方式构造对象
Person p2 = new Person("张飞", 20);
System.out.println("原始有参方式构造的对象是:" + p2);
//4、使用反射机制以有参方式构造Person类型的对象并打印
//获取Class对象对应类中的有参构造方法,也就是Person类中的有参构造方法
constructor = aClass.getConstructor(String.class, int.class);
//使用获取到的有参构造方法来构造对应类型的对象,也就是Person类型的对象
//注意:newInstance方法中的实参是用于给有参构造方法的形参进行初始化的,也就是给name和age进行初始化的
Person p3 = (Person) constructor.newInstance("zhangfei", 30);
System.out.println("有参方式构造的对象是:" + p3);
br.close();
//5、使用反射机制获取Person中所有的公共构造方法并打印
System.out.println("-----------------------------------------------------");
Constructor<?>[] constructors = aClass.getConstructors();
for (Constructor ct : constructors) {
System.out.println("构造方法的修饰符是:" + ct.getModifiers());
System.out.println("构造方法的方法名称是:" + ct.getName());
Class[] parameterTypes = ct.getParameterTypes();
System.out.print("构造方法的所有参数类型是:");
for (Class pt : parameterTypes) {
System.out.print(pt + " ");
}
System.out.println();
}
}
}
Field类
基本概念
- java.lang.reflect.Field类主要用于描述获取到的单个成员变量信息
Class类的常用方法
方法声明 | 功能介绍 |
---|---|
Field getDaclaredField(String name) | 用于获取此Class对象所表示类中名字为name的成员变量信息 (注意:只能获取到public权限的成员变量) |
Field[] getDeclaredFields() | 用于获取此Class对象所表示类中所有成员变量信息 |
Field类的常用方法
方法声明 | 功能介绍 |
---|---|
Object get(Object obj) | 获取参数对象obj中此Field对象所表示成员变量的数值 |
void set(Object obj, Object value) | 将参数对象obj中此Field对象表示成员变量的数值修改为参数value的数值 |
void setAccessible(boolean flag) | 当实参传递true时,则反射对象在使用时应该取消Java语言访问检查 |
int getModifiers() | 获取成员变量的访问修饰符 |
Class<?> getType() | 获取成员变量的数据类型 |
String getName() | 获取成员变量的名称 |
Method类
基本概念
- java.lang.reflect.Method类主要用于描述获取到的单个成员方法信息
Class类的常用方法
方法声明 | 功能介绍 |
---|---|
Method getMethod(String name, Class<?>… parameterType) | 用于获取该Class对象表示类中名字为name,参数为parameterType的指定公共成员方法 |
Method[] getMethods() | 用于获取该Class对象表示类中所有公共成员方法 |
Method类的常用方法
方法声明 | 功能介绍 |
---|---|
Object invoke(Object obj, Object… args) | 使用对象obj来调用此Method对象所表示的成员方法和实参传递args并获取函数返回值 |
int getModifiers() | 获取方法的访问修饰符 |
Class<?> getReturnType() | 获取方法的返回值类型 |
String getName() | 获取方法的名称 |
Class<?> getParameterTypes() | 获取方法所有参数的类型 |
Class<?> getExceptionTypes() | 获取方法的异常信息 |
获取其他结构信息
方法声明 | 功能介绍 |
---|---|
Package getPackage() | 获取所在的包信息 |
Class<? super T> getSuperclass() | 获取继承的父类信息 |
Class<?>[] getInterfaces() | 获取实现的所有接口 |
Annotation[] getAnnotations() | 获取注解信息 |
Type[] getGenrericIntetfaces() | 获取泛型信息 |
注:以上方法都是Class类中的方法
代码举例
package com.lagou.task20;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
public class StudentFieldTest {
public static void main(String[] args) throws Exception {
//1、使用反射机制来构造对象以及获取对象的成员变量数值并打印
//1-1、获取Class对象
Class c1 = Class.forName("com.lagou.task20.Student");
//1-2、根据Class对象获取对应的有参构造方法
Constructor constructor = c1.getConstructor(String.class, int.class);
//1-3、根据有参构造方法来得到Student类型的对象
Student s1 = (Student) constructor.newInstance("小明", 10);
//1-4、根据Class对象获取对应的成员变量信息
Field field = c1.getDeclaredField("name");
//注意:我们只能获取到public权限的成员变量,要想获取private权限的,需要设置Java语言访问检查的取消 --> 暴力反射
field.setAccessible(true); //设置Java语言访问检查的取消
//1-5、使用Student类型的对象来获取成员变量的数值并打印
//注:获取对象s1中名字为field成员变量的数值,也就是成员变量name的数值
System.out.println("获取到的成员变量数值是:" + field.get(s1));
//2、使用反射机制修改指定对象中成员变量的数值后再次打印
field.set(s1,"小华");
System.out.println("修改后的成员变量是:" + field.get(s1));
//3、获取Class对象对应类中所有的成员变量
Field[] declaredFields = c1.getDeclaredFields();
for (Field f : declaredFields) {
System.out.println("----------------------------------------------");
System.out.println("获取到的访问修饰符为:" + f.getModifiers());
System.out.println("获取到的数据类型为:" + f.getType());
System.out.println("获取到的成员变量名称为:" + f.getName());
}
System.out.println("------------------------------------------------");
//4、获取对应的成员方法并打印出来
Method method = c1.getMethod("getName");
//使用method对象调用成员方法并打印
System.out.println("调用方法的返回值是:" + method.invoke(s1));
//5、使用反射机制获取所有成员方法
Method[] methods = c1.getMethods();
for (Method mt : methods) {
System.out.println("----------------------------------------------");
System.out.println("获取到的方法名称是:" + mt.getName());
System.out.println("获取到的方法访问修饰符是: " + mt.getModifiers());
System.out.println("获取到的方法返回值类型是:" + mt.getReturnType());
System.out.println("获取到的方法所有参数的类型有:");
Class<?>[] parameterTypes = mt.getParameterTypes();
for (Class aClass : parameterTypes) {
System.out.println(aClass + " ");
}
System.out.println("获取到的方法所有异常信息有:");
Class<?>[] exceptionTypes = mt.getExceptionTypes();
for (Class aClass : exceptionTypes) {
System.out.println(aClass + " ");
}
}
//6、测试获取其他结构信息
System.out.println("---------------------------------");
Package aPackage = c1.getPackage();
System.out.println("获取到的包名信息是:" + aPackage);
System.out.println("---------------------------------");
Class superclass = c1.getSuperclass();
System.out.println("获取到的继承父类信息是:" + superclass);
System.out.println("----------------------------------");
Class[] interfaces = c1.getInterfaces();
System.out.println("获取到的所有接口有:");
for (Class aInterface : interfaces) {
System.out.println(aInterface);
}
System.out.println("---------------------------------");
Annotation[] annotations = c1.getAnnotations();
System.out.println("获取到的所有注解信息有:");
for (Annotation at : annotations) {
System.out.println(at);
}
System.out.println("----------------------------------");
Type[] genericInterfaces = c1.getGenericInterfaces();
System.out.println("获取到的所有泛型信息有:");
for (Type t : genericInterfaces) {
System.out.println(t);
}
}
}
使用总结
- 使用Class.forName(string)方法可以创建出指定string类型的Class对象
- 使用Class对象调用方法可以获得对应类型的构造方法、成员变量、成员方法和其他结构等(调用对应的Class类型方法即可)
- 获得对应string类型的构造方法后还可以使用newInstance()方法创建对应类型的对象
- 获得的成员变量、成员方法和其他结构的对象后也可以进一步调用方法进行其他操作
任务总结
- 反射机制
概念、Class类、获取Class类的对象、获取指定的构造方法、获取所有的构造方法、获取指定的成员变量、获取所有的成员变量、获取指定的成员方法、获取所有的成员方法、获取其他结构等