IO
节点流
FileInputStream
- 文件字节输入流
- 通常使用
read(byte[] b)
方法,自定义一个数组作为缓冲数组,一次性读取若干字节存入数组,并返回字节数
java.lang.Object
java.io.InputStream
java.io.FileInputStream
/**
* IO的父类子类非常多,所以要面向父类、接口编程,面向多态编程
* 标准步骤:【分段读取存放在byte数组里面】【文件字节输入流】FileInputStream
*
* 先读取到字节数组中,返回值为读取的字节个数, len = is.read(bytes)
* 然后根据字节数组和读取的个数进行解码 new String(bytes,0,len)
*
* 1. 创建源
* 2. 选择流
* 3. 操作
* 4. 释放资源
* @author: Mango
* @create: 2020-03-16 17:15
**/
使用FileInputStream进行文件字节读取,并输出
public class demo03 {
public static void main(String[] args) {
File file = new File("IO/src/testFile/test.txt");
InputStream is = null;
try {
is = new FileInputStream(file);
//缓存容器 字节数组
byte[] bytes = new byte[1024]; //缓冲容器
//接收长度 len 每次读取多少个,最后一次没有读取到内容,返回-1
int len = -1;
while ((len = is.read(bytes)) != -1) {
//从第0个开始读取len个,len就是缓存数组每次从文件读取的字节个数
String str = new String(bytes,0,len);
System.out.print(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileOutputStream
- 文件字节输出流
- 将字节输出写入到文件
java.lang.Object
java.io.OutputStream
java.io.FileOutputStream
/**
* 四个步骤:
* 1. 创建源
* 2. 选择流
* 3. 操作
* 4. 释放资源
*
* flush()方法:在关闭流之前调用此方法
* @author: Mango
* @create: 2020-03-17 15:36
**/
public class demo01 {
public static void main(String[] args) {
//创建源
File dest = new File("IO/src/testFile/dest.txt");
//选择流
OutputStream os = null;
try {
//如果文件不存在则自动创建文件
os = new FileOutputStream(dest,true); //append:是否追加写入,默认重写
String str = "like hhhh";
byte[] data = str.getBytes(); //字符串 --> 字节数组(编码)
//操作 将字节数组写出到文件
os.write(data,0,data.length);
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}finally {
if(os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileIO…Stream实现文件的拷贝
/**
* @description:
* 文件的赋值拷贝
* 利用文件字节输入流和输出流拷贝文件
* 源文件 --> 程序 --> 写出文件
* 从源文件每次读取一部分数据存放在缓存数组中,然后将缓存数组一次次的写出到文件
* @author: Mango
* @create: 2020-03-17 16:11
**/
public class demo02Copy {
public static void main(String[] args) {
//创建源
File source = new File("IO/src/testFile/mango.jpg");
File target = new File("IO/src/testFile/target.jpg");
InputStream is = null;
OutputStream os = null;
try {
//选择流
is = new FileInputStream(source);
os = new FileOutputStream(target);
//操作(拷贝)
//缓存数组data 读取长度len
byte[] data = new byte[1024];
int len = -1;
//读写,注意循环读取,一次读取可能读取不完
while ((len=is.read(data)) != -1) {
os.write(data,0,len);
}
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
//释放资源 先打开的后关闭
if(os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileReader
- 文件字符输入流
- 用来读取字符文件的便捷类
java.lang.Object
java.io.Reader
java.io.InputStreamReader
java.io.FileReader
/**
* @description:
* 【文件字符】输入流 【FileReader】【节点流】
* 标准步骤:
* 1. 创建源
* 2. 选择流
* 3. 操作
* 4. 释放资源
* @author: Mango
* @create: 2020-03-18 15:24
**/
使用FileReader将纯文本内容读取,每次读取若干字符存储到字符数组
public class demo01 {
public static void main(String[] args) {
//创建源
File src = new File("IO/src/testFile/test.txt");
Reader reader = null;
try {
//选择流
reader = new FileReader(src);
char[] chars = new char[10];
int len = -1;
//操作
while ((len = reader.read(chars)) != -1) {
//字符数组到字符串
String str = new String(chars,0,len);
System.out.println(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileWriter
- 文件字符输出流
- 用来写入字符文件的便捷类
java.lang.Object
java.io.Writer
java.io.OutputStreamWriter
java.io.FileWriter
/**
* @description:
* 【文件字符】输出流 【FileWriter】【节点流】
* 标准步骤:
* 1. 创建源
* 2. 选择流
* 3. 操作
* 4. 释放资源
*
* @author: Mango
* @create: 2020-03-18 16:19
**/
使用FileWriter将字符写出到文件
public class demo01 {
public static void main(String[] args) {
//创建源
File src = new File("IO/src/testFile/fileWriter.txt");
Writer writer = null;
try {
//选择流
writer = new FileWriter(src);
writer.write("诶嘿嘿嘿");
writer.write("嘿嘿嘿");
writer.flush();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
FileRW…实现纯文本文件拷贝
/**
* @description:
* 使用文件字符输入输出流进行纯文本的拷贝
* @author: Mango
* @create: 2020-03-18 16:47
**/
public class demo02Copy {
public static void main(String[] args) {
File src = new File("IO/src/testFile/src.txt");
File dest = new File("IO/src/testFile/dest.txt");
Reader reader = null;
Writer writer = null;
try {
reader = new FileReader(src);
writer = new FileWriter(dest);
char[] chars = new char[1024];
int len = -1;
while ((len = reader.read(chars)) != -1) {
writer.write(chars,0,len);
}
writer.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(writer != null) {
try {
writer.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if(reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
ByteArrayInputStream
- 字节数组输入流
- 不是与文件操作,所以不需要关闭
- 将一个源头字节数组读取到自定义的缓冲字节数组中
java.lang.Object
java.io.InputStream
java.io.ByteArrayInputStream
/**
* @description:
* 字节数组输入流(内存流) 【ByteArrayInputStream】【节点流】
* 程序 <-- ByteArrayInputStream <-- ByteArray read
* 程序 --> ByteArrayOutputStream --> ByteArray write
* 不是跟File文件打交道,所以是不需要关闭的
*
* 标准步骤:
* 1. 创建源:源头是字节数组,不要太大
* 2. 选择流
* 3. 操作
* 4. 释放资源:可以不用处理释放资源
*
* @author: Mango
* @create: 2020-03-18 17:07
**/
将一个源头字节数组读取到缓冲字节数组,并每次输出这个缓冲字节数组的内容
public class demo01 {
public static void main(String[] args) {
//创建源
byte[] data = "嘿嘿嘿".getBytes();
InputStream is = null;
try {
//选择流 将data字节数组读取到缓存数组中
is = new ByteArrayInputStream(data);
byte[] flush = new byte[1024];
int len = -1;
while((len = is.read(flush)) != -1) {
String str = new String(flush,0,len);
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
ByteArrayOutputStream
- 字节数组输出流
- 将一个个字节数组数据累计输出到内部维护的字节数组,最后可以通过toByteArray()方法获得这个完整的数组
- 有特殊方法,不要发生多态
java.lang.Object
java.io.OutputStream
java.io.ByteArrayOutputStream
/**
* @description:
* 字节数组输出流(内存流) 【ByteArrayOutputStream】【节点流】
* 程序 <-- ByteArrayInputStream <-- ByteArray read
* 程序 --> ByteArrayOutputStream --> ByteArray write
* 不是跟File文件打交道,所以是不需要关闭的
* 标准步骤:
* 1. 创建源:源头是字节数组,不要太大
* 2. 选择流
* 3. 操作 有特殊方法,不能发生多态,最后通过toByteArray()方法获得这个完整的数组
* 4. 释放资源:可以不用处理释放资源
*
* @author: Mango
* @create: 2020-03-19 10:50
**/
通过FileInputStream将文件多次读取到缓存数组,再通过ByteArrayOutputStream将读取到的数据输出整合到自己内部维护的数组中,最后通过toByteArray()方法获得这个完整的数组
public class demo01 {
public static void main(String[] args) {
//创建源
byte[] data = "嘿嘿嘿".getBytes();
InputStream is = null;
try {
//选择流 将data字节数组读取到缓存数组中
is = new ByteArrayInputStream(data);
byte[] flush = new byte[1024];
int len = -1;
while((len = is.read(flush)) != -1) {
String str = new String(flush,0,len);
System.out.println(str);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
ByteArrayIO…Stream实现文件复制
- 文件转字节数组,字节数组转文件
/**
* @description:
* 使用文件字节输入输出流和字节数组输入输出流进行文件的复制
* 1. 文件每次读取一部分存放在缓存数组,再将缓存数组累计写出在ByteArrayOutputStream的内部缓存数组,
* 最后得到一个完整的文件字节数组
* 2. ByteArrayInputStream将完整的文件字节数组每次读取到缓存数组中,FileOutputStream将缓存数组
* 的数据一次次写出到文件
*
* 当然,已经获取到文件的字节数组,可以直接使用FileOutputStream直接将字节数组写出到文件,
* 不需要通过ByteArrayInputStream
* @author: Mango
* @create: 2020-03-19 11:00
public class demo03Copy {
public static void main(String[] args) {
byte[] fileByteArray = fileToByteArray("IO/src/testFile/mango.jpg");
byteArrayToFile(fileByteArray,"IO/src/testFile/mango-copy.jpg");
}
/**
* @Description 文件到字节数组
**/
public static byte[] fileToByteArray(String srcPath) {
File src = new File(srcPath);
FileInputStream is = null;
ByteArrayOutputStream baos = null;
try {
is = new FileInputStream(src);
baos = new ByteArrayOutputStream();
byte[] flush = new byte[1024];
int len = -1;
while ((len = is.read(flush)) != -1) {
baos.write(flush,0,len);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(is != null) {
try {
is.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
//获取文件的完整字节数组返回
return baos.toByteArray();
}
/**
* @Description 字节数组到文件
**/
public static void byteArrayToFile(byte[] datas, String destPath) {
File destFile = new File(destPath);
InputStream bais = null;
FileOutputStream os = null;
try {
bais = new ByteArrayInputStream(datas);
os = new FileOutputStream(destFile);
byte[] flush = new byte[1024*10];
int len = -1;
while ((len = bais.read(flush)) != -1) {
os.write(flush,0,len);
}
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(os != null) {
try {
os.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
缓冲处理流
BufferedInputStream
BufferedOutputStream
- BufferedInputStream、BufferedOutputStream
- BufferedIO…Stream是装饰设计模式,以提高IO…Stream性能
- BufferedIO…Stream()需要一个IO…Stream参数
java.lang.Object
java.io.InputStream
java.io.FilterInputStream
java.io.BufferedInputStream
java.lang.Object
java.io.OutputStream
java.io.FilterOutputStream
java.io.BufferedOutputStream
/**
* 【原理】
* 原理是BufferedInputStream底层用了一个数组,默认大小是8192个字节,
* 也就是BufferedInputStream一次性会从文件读取8192个字节,
然后返回给程序byte[b](自定义)个字节,
* 程序在读取8192个字节之前不需要再次去访问和读取文件,直接从缓冲区读。
* 直到缓冲区所有的都读取过,然后再去读取8192个字节,一直这样进行下去,直到任务结束。
* 同样的BufferedOutputStream也是内置一个数组,默认大小也是8192字节。
* 程序向流中写出字节时,不会直接写到文件,而是先写入到缓冲区,直到缓冲区写满,
* BufferedInputStream才会一次性把数据写入到文件中。
* 【原理总结】
* 通过实现原理,buffI和buffO都会先经过默认缓存数组,当缓存数组满了之后再进行下一次的读写,
* 极大的减少了频繁访问硬盘的时间
* 【实测总结】
* 通过FileIO...Stream和BufferedIO...Stream进行问价的拷贝,
当自定义缓存数组为8kb(1024*8)时,FileIO...Stream略胜一点,当缓存数组过大或过小,
FileIO...Stream耗时明显高于BufferedIO...Stream,
* FileIO...Stream耗时随缓存数组的大小改变,BufferedIO...Stream不随自定义缓存数组大小改变
* @author: Mango
* @create: 2020-03-21 17:02
**/
文件的拷贝
public static void fileCopy(String src, String dest) {
File srcFile = new File(src);
File destFile = new File(dest);
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(srcFile));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile));) {
int len = -1;
byte[] flush = new byte[1024*8];
while ((len = bis.read(flush)) != -1) {
bos.write(flush,0,len);
}
bos.flush(); //刷新缓冲输出流,在关闭前刷新一次即可,否则会影响性能
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
BufferedReader
-
字符输出缓冲流
-
从字符输入流读取文本,缓冲字符,以提供字符,数组和行的高效读取。
-
装饰模式,有新增方法readLine(),不要发生多态
继承树
java.lang.Object
java.io.Reader
java.io.BufferedReader
使用方法:将字符输入流FileReader作为BufferedReader的参数传入
从文件每次读取一行字符并输出
public class demo01BufferedReader {
public static void main(String[] args) {
File file = new File("IO/src/testFile/test.txt");
try(BufferedReader br = new BufferedReader(new FileReader(file))) {
String sb = new String();
//注意,读取一行返回的是一个字符串,如果没有读到内容就返回null,所以跟null作比较判断
while ((sb = br.readLine()) != null) {
System.out.println(sb);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
###BufferedWriter
- 将文本写入字符输出流,缓冲字符,以提供单个字符,数组和字符串的高效写入。
- 装饰模式,有新增方法newLine(),不要发生多态
java.lang.Object
java.io.Writer
java.io.BufferedWriter
使用方法:将字符输入流FileWriter作为BufferedWriter的参数传入
将字符高效写入文件,并且添加换行
public class demo02BufferedWriter {
public static void main(String[] args) {
File file = new File("IO/src/testFile/out.txt");
try(BufferedWriter bw = new BufferedWriter(new FileWriter(file))) {
bw.append("芒果小洛"); //追加文字
bw.newLine(); //添加一个换行符
bw.append("特备厉害");
bw.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
BufferedRW…实现纯文本的拷贝
public class demo03Copy {
public static void main(String[] args) {
copy("IO/src/testFile/test.txt","IO/src/testFile/dest.txt");
}
public static void copy(String fileSrc,String fileDest) {
try(BufferedReader br = new BufferedReader(new FileReader(fileSrc));
BufferedWriter bw = new BufferedWriter(new FileWriter(fileDest))) {
String str = new String();
//逐行读取
while ((str = br.readLine()) != null) {
//逐行写入,并追加换行
bw.write(str);
bw.newLine();
}
bw.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
转换流
InputStreamReader
OutputStreamWriter
- 是字节流与字符流之间的桥梁
- Reader解码:字节 --> 字符;Writer编码:字符 -->字节
- 解码与编码可以指定字符集
java.lang.Object
java.io.Reader
java.io.InputStreamReader
java.lang.Object
java.io.Writer
java.io.OutputStreamWriter
InputStreamReader部分构造方法 | ||
---|---|---|
InputStreamReader(InputStream in) | 创建一个使用默认字符集的 | |
InputStreamReader(InputStream in, String charsetName) | 创建使用指定字符集的 | |
InputStreamReader方法 | ||
int | read() | 读取一个字符,返回此字符的,未读到返回-1 |
int | read(char[] cbuf, int offset, int length) | 读取字节流中部分字符存储到char[],返回读取的字符数量 |
从以下几个例子深入理解
- 从系统输入System.in读取字节流,并获取输入的前第2个字符开始连续读取10个字符,存储到字符数组中,再将字符数组转为字节流通过System.out系统输出
public class demo01TransCoding {
public static void main(String[] args) {
//通过System.in系统输入字节流,将其解码为字符流
//将字符编码为字节流,通过系统输出
//可以使用BufferdeReader和BufferedWriter提升性能
try(InputStreamReader is = new InputStreamReader(System.in);
OutputStreamWriter os = new OutputStreamWriter(System.out)) {
char[] c = new char[10];
int len = is.read(c,2,10);
os.write(c,0,len);
//此类内部存在缓冲区,如果需要多次输出需要手动刷新,此处仅输出了一次
os.flush();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 从系统输入System.in读取字节流,并获取输入的前第2个字符开始连续读取10个字符,存储到字符数组中,再将字符数组转为【特定编码GBK】的字节流通过System.out系统输出;发现当输入中文时,输出就乱码了,因为工程默认字符集为UTF-8,所以以GBK输出中文就会乱码
public class demo02 {
public static void main(String[] args) {
//通过System.in系统输入字节流,将其解码为字符流,再通过BufferedReader字符缓冲流提升性能
//将字符编码为字节流,通过系统输出,使用字符缓冲流提升性能
try(InputStreamReader is = new InputStreamReader(System.in);
OutputStreamWriter os = new OutputStreamWriter(System.out,"GBK")) {
char[] c = new char[10];
int len = is.read(c,0,10);
os.write(c,0,len);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 通过URL返回此网站的InputStream作为字节输入流(网络流),通过InputStreamReader转换流转换为字符流,www.163.com的默认字符编码为GBK,所以要指定字符集,不然默认字符集UTF-8显示就会中文乱码
public class demo03 {
public static void main(String[] args) {
//通过URL返回此网站的InputStream输入流,通过InputStreamReader转换流转换为字符流
//BufferedReader为提高性能
try(BufferedReader is = new BufferedReader(new InputStreamReader(
new URL("https://www.163.com").openStream(),"GBK"))) {
String str = new String();
int len = -1;
is.readLine();
while ((str = is.readLine()) != null) {
System.out.println(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 将baidu.com网页以UTF-8字符集从字节流转换为字符流,再通过UTF-8字符集将字符存为网页文件,当然,保存一个网页完全可以直接通过字节输入输出流对接完成,此处只是为了演示而演示
public class demo04 {
public static void main(String[] args) {
//通过URL返回此网站的InputStream输入流,通过InputStreamReader转换流转换为字符流
//BufferedReader为提高性能
try(BufferedReader reader = new BufferedReader(new InputStreamReader(
new URL("https://www.baidu.com").openStream(),"UTF-8"));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(
new FileOutputStream("IO/src/testFile/baidu.html"),"UTF-8"))) {
String str = null;
while ((str = reader.readLine()) != null) {
writer.write(str);
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
数据流
DataInputStream
DataOutputStream
- 数据输入输出流
- 数据输入流允许应用程序以与机器无关方式从底层输入流中读取基本 Java 数据类型。
- 应用程序可以使用数据输出流写入稍后由数据输入流读取的数据。
- 输入和输出的顺序要保持一致
java.lang.Object
java.io.InputStream
java.io.FilterInputStream
java.io.DataInputStream
java.lang.Object
java.io.OutputStream
java.io.FilterOutputStream
java.io.DataOutputStream
DataInputStream构造方法 | |
---|---|
DataInputStream(InputStream in) | 使用指定的底层 InputStream 创建一个 DataInputStream |
以下是DataInputStream的部分方法,包含了Java的基本数据类型和String,其他更多方法详见API,
DataOutputStream的方法与此是成对的,为write开头,如writeChar(),故不一一列举
部分方法,DataInputStream和DataOutputStream方法成对 | ||
---|---|---|
boolean | readBoolean() | 读取一个输入字节,如果该字节不是零,则返回 true ,如果是零,则返回 false 。 |
byte | readByte() | 读取返回一个输入字节 |
char | readChar() | 读取两个字节并返回一个char 值 |
double | readDobule() | 读取八个输入字节并返回一个 double 值 |
float | readFloat() | 读取四个输入字节并返回一个 float 值 |
String | readUTF() | 读取一个使用UTF-8编码的字符串 |
- 从下面的例子有助于理解
(当时疑惑字节数组的写入与写出不应该是使用字节数组IO流本身的方法来完成吗,为什么字节数组IO流作为参数,通过DataIO…Stream的方法就能对字节数组IO流读写数据呢?后来明白,DataIO…Stream是装饰流,它有更加强大的方法,通过它就可以对ByteArrayIO…Stream进行读写,就相对于FileReader套了BufferedReader一样,提升了性能,直接对BufferedReader操作即可)
将数据写出到ByteArrayOutputStream内部维护的字节数组然后再获取这个完整的字节数组,作为ByteArrayInputStream 的参数,DataInputStream读取并输出;当然为了提高性能,可以在字节数组IO流外面加一个BufferedIO…Stream来提高性能
public class demo01 {
public static void main(String[] args) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
//DataOutputStream为装饰流,构造需要一个ByteArrayOutputStream,
//所以直接使用DataOutputStream的方法即可,就和FileWriter套一个BufferedWriter一样
DataOutputStream dos = new DataOutputStream(bos);
dos.writeUTF("like you"); //写出字符串
dos.writeBoolean(true); //写出boolean值
dos.writeInt(18); //写出int值
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
//DataInputStream为装饰流,构造需要一个ByteArrayInputStream,
//所以直接使用DataInputStream的方法即可,就和FileWriter套一个BufferedWriter一样
//注意读取的顺序要和写入的顺序一样
DataInputStream dis = new DataInputStream(bis);
String msg = dis.readUTF();
boolean flag = dis.readBoolean();
int age = dis.readInt();
System.out.println(msg+flag+age);
}
}
将数据写出到文件,再读取输出,并输出;当然,可以在FileIO…Stream外面套一个BufferedIO…Stream提升性能
public class demo02 {
public static void main(String[] args) {
File file = new File("IO/src/testFile/data.txt");
try(DataOutputStream dos = new DataOutputStream(new FileOutputStream(file));
DataInputStream dis = new DataInputStream(new FileInputStream(file));) {
dos.writeUTF("like you");
dos.writeBoolean(true);
dos.writeChar('h');
dos.flush();
String msg = dis.readUTF();
Boolean flag = dis.readBoolean();
char c = dis.readChar();
System.out.println(msg+" "+flag+" "+c);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
对象流
ObjectInputStream
ObjectOutputStream
- 对象输入输出流
- 对象输入输出流:此流不仅可以输出Java的8大基本数据类型和String,还可以将类的实例化对象转为输出流,以及在输入流中读取对象
- ObjectInputStream中的writeObject()会将对象转为字节,称为序列化,并存储到文件、数据库、内存
- ObjectOutputStream中的readObject()会将字节还原为对象,称为反序列化
- 只有实现java.io.Serializable接口的类,其实例对象才能进行序列化和反序列化,此接口实际上只是一个空接口,是给虚拟机看的,作为可以序列化的标识
- 使用
transient
关键字修饰的类的成员属性则不进行序列化
java.lang.Object
java.io.InputStream
java.io.ObjectInputStream
java.lang.Object
java.io.OutputStream
java.io.ObjectOutputStream
通过以下的例子有助于理解
- 从文件写出读取;将基本数据类型和String进行序列化存储到文件,以及对实现Serializable接口的类的实例对象进行序列化存储到文件
public class demo01 {
public static void main(String[] args) {
File file = new File("IO/src/testFile/obj.obj");
//为了提高性能,可以加入BufferedIO...Stream
try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) {
/**
* 写出
*/
//基本操作,写出基本数据类型和String
oos.writeUTF("like you");
oos.writeBoolean(true);
//写出对象
student stu = new student("小明",18);
oos.writeObject(stu);
oos.flush();
/**
* 读取
*/
//按照写出顺序进行读取
String msg = ois.readUTF();
boolean flag = ois.readBoolean();
Object obj = ois.readObject();
System.out.println(msg+"\t"+flag);
//测试一个对象是否为一个类的实例
if(obj instanceof student) {
student stuObj = (student)obj;
System.out.println(stuObj.getName()+"\t"+stuObj.getAge());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
//实现Serializable接口的类才能序列化,为减少代码,删去了set方法和无参构造
class student implements Serializable {
//使用transient关键字的属性不进行序列化
private transient String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public student(String name, int age) {
this.name = name;
this.age = age;
}
}
从内存写出读取;将基本数据类型和String进行序列化存储到文件,以及对实现Serializable接口的类的实例对象进行序列化存储到文件
public class demo02 {
//为了减少代码量,这里异常就直接抛出
public static void main(String[] args) throws IOException, ClassNotFoundException {
/**
* 从内存写出读取
*/
//为了提高性能,可以加入BufferedIO...Stream
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
//写出
//基本操作,写出基本数据类型和String
oos.writeUTF("like you");
oos.writeBoolean(true);
//写出对象
teacher stu = new teacher("小明", 18);
oos.writeObject(stu);
oos.flush();
//获取字节数组数据
byte[] datas = bos.toByteArray();
//读取
//按照写出顺序进行读取
//为了提高性能,可以加入BufferedIO...Stream
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(datas));
String msg = ois.readUTF();
boolean flag = ois.readBoolean();
Object obj = ois.readObject();
System.out.println(msg + "\t" + flag);
//测试一个对象是否为一个类的实例
if (obj instanceof teacher) {
teacher stuObj = (teacher) obj;
System.out.println(stuObj.getName() + "\t" + stuObj.getAge());
}
}
}
//实现Serializable接口的类才能序列化,为减少代码,删去了set方法和无参构造
class teacher implements Serializable {
//使用transient关键字的属性不进行序列化
private transient String name;
private int age;
public String getName() {
return name;
}
public int getAge() {
return age;
}
public teacher(String name, int age) {
this.name = name;
this.age = age;
}
}
Java比较器
Java中对象比较只能是地址值的比较 ==,!=, 通常我们对对象的比较其实是对对象的其中一个属性进行比较就需要使用两个接口
Comparable 自然排序
类默认的排序规则
Comparator 定制排序
默认的排序规则不满足自己的需求临时制定的排序规则
Comparable自然排序
类默认的排序规则
-
像String、包装类等,他们都实现类Comparable接口,进而实现类compareTo()方法,给出了比较两个对象大小的方式
-
重写compareTo(obj)方法的规则
- 当前对象this > 形参对象,返回正整数
- 当前对象this < 形参对象,返回负整数
- 当前对象this = 形参对象,返回0
-
如果需要对自定义类进行排序,可以让自定义类实现Comparable接口,重写CompareTo()方法在compareTo()方法中指明如何排序
-
像String或包装类等,重写CompareTo()之后,进行了从小到大的顺序排列的规则
自定义类按照成绩由小到大排序
@Test
public void test2() {
Student[] stu = new Student[5];
stu[0] = new Student("小明",77);
stu[1] = new Student("小红",94);
stu[2] = new Student("小刚",65);
stu[3] = new Student("小强",87);
stu[4] = new Student("小丽",98);
Arrays.sort(stu);
System.out.println(Arrays.toString(stu));
}
/**
自定义学生类,实现Comparable接口
已省略get/set方法
*/
public class Student implements Comparable<Student> {
private String name;
private int score;
public Student(String name, int score) {
this.name = name;
this.score = score;
}
//重写的compareTo()方法,指明如何排序
@Override
public int compareTo(Student student) {
return this.score - student.score;
}
@Override
public String toString() {
return "{name=" + name +", score=" + score + '}';
}
}
Comparator 定制排序
如果需要自定义的排序规则,在不该动原有的代码,临时使用的一种排序规则,那么可以通过创建Comparator对象,将对象这个排序规则传入sort()方法
@Test
public void test3() {
//自定义的一个数组
String[] str = {"dd","mm","gg","jj","tt","vv"};
Arrays.sort(str, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
//默认是从小到大,现在需求从大到小,所以返回默认排序的相反值
return -o1.compareTo(o2);
}
});
System.out.println(Arrays.toString(str));
}
Collection单列集合
- Collection接口,单列集合,用来存储一个一个的对象
- List接口,存储有序、可重复的数据(动态数组)
- ArrayList
- LinkedList:
- Vector:
- Set接口,存储无序、不可重复的元素
- HashSet
- LinkedHashSet
- TreeSet
Collection接口常用方法
- boolean add(E e):添加元素
-
boolean addAll(Collection c):将指定 collection 中的所有元素都添加到此 collection 中
-
void clear():移除此 collection 中的所有元素(可选操作)。
-
int size():获取集合长度(元素个数)
-
boolean isEmpty():判断集合是否为空
-
boolean contains(Object obj):判断集合是否有指定的obj元素,会自动调用对象的equals()方法进行比较,会从容器的第一个元素开始比较,直到存在比较相同的原始,或查找到容器的结尾
-
containsAll(Collection coll):判断参数容器内的元素是否都在此容器中,
注意:此方法也会表用元素对象类中的equals()方法,不分元素的顺序,只要全部包含即可
-
remove():移除指定的元素,同样会调用equals()方法进行比较判断是否相同,仅移除第一次出现此元素的元素
-
removeAll(Collection c):从集合中移除参数集合中所有元素(只能移除共有的部分)
-
retainAll(Collection c):仅保留指定集合中共有的元素
-
equals(Object obj):比较此 collection 与指定对象是否相等,比较的是元素
-
hashCode():返回hash值
-
toArray():将集合转为数组
-
Iterator iterator():返回在此 collection 的元素上进行迭代的迭代器
Iterator迭代器
Iterator迭代器主要用来遍历Collection容器的,不包括Map
遍历集合
Iterator iterator = coll.iterator();
next():下一个元素,每次调用后指向下一个元素
hasNext():判断是否还有更多的元素
使用迭代器遍历集合与删除元素
@Test
public void test1() {
Collection coll = new ArrayList();
coll.add(11);
coll.add(22);
coll.add(33);
coll.add(44);
//返回此元素的迭代器
Iterator iterator = coll.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
@Test
public void test2() {
Collection coll = new ArrayList();
coll.add(11);
coll.add(22);
coll.add(33);
coll.add("Tom");
//iterator.remove():从迭代器指向的 collection 中移除迭代器返回的最后一个元素(可选操作)
// 每次调用 next 只能调用一次此方法
Iterator iterator = coll.iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
if("Tom".equals(next)) {
iterator.remove();
}
System.out.println(next);
}
System.out.println("—————");
iterator = coll.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
List
List实现类与区别
- ArrayList
- 作为List接口的主要实现类,线程不安全的,效率高,底层使用Object[]存储插入是将后面的元素都往后移动一位,删除是将后面的元素都往前移动,增删效率底,查找效率高如果知道存储的长度,推荐实例化的时候就指定长度
**LinkedList** * 底层使用双向链表存储,内存是连续的,对应频繁的插入和移除操作,此类效率比ArrayList高,查找效率低
**Vector** * 作为List接口的古老实现类,线程安全的,效率底
List接口新增方法
List接口中,在Collection接口基础上,主要增加了一些带索引的方法
void add(int index, E element):在此列表中的指定位置插入指定的元素
boolean addAll(int index, Collection c):将指定集合中的所有元素插入到此列表中,从指定的位置开始。
E get(int index):返回此列表中指定位置的元素
int indexOf(Object o):返回此列表中指定元素的第一次出现的索引,如果此列表不包含元素,则返回-1
int lastIndexOf(Object o):返回此列表中指定元素的最后一次出现的索引,如果此列表不包含元素,则返回-1
E remove(int index):删除该列表中指定位置的元素
E set(int index, E element):用指定的元素替换此列表中指定位置的元素
List subList(int fromIndex, int toIndex):返回此列表中指定的 fromIndex (包括)和 toIndex之间的子集合
ArrayList
作为List接口的主要实现类,线程不安全的,效率高,底层使用Object[]存储插入是将后面的元素都往后移动一位,删除是将后面的元素都往前移动,增删效率底,查找效率高如果知道存储的长度,推荐实例化的时候就指定长度
遍历
iterator() 迭代器遍历
增强for循环 forEach
普通for循环
注意
在add()方法中,参数是Object类型,所以如果存储的是基本数据类型,会自动装箱,在remove()方法中,有重载的方法,如果写remove(2),就是删除索引为2的元素,因为重载的方法有参数为int类型的,如果要删除值为2的元素,要使用包装类装箱new Integer(2)或 Integer.valueOf(2);
要求
作为List接口的子类容器,存储的对象要重写equals()方法,因为有remove()、contains()等方法需要使用到
示例
@Test
public void test() {
ArrayList<Integer> list = new ArrayList<>(10);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
List<Integer> list1 = Arrays.asList(3, 4, 5, 6);
//addAll(int index, Collection c):将指定集合中的所有元素插入到此列表中,从指定的位置开始
list.addAll(2,list1);
System.out.println("addAll "+list);
//add(int index, E element):在此列表中的指定位置插入指定的元素
list.add(1,8);
System.out.println("add "+list);
//indexOf(Object o):返回此列表中指定元素的第一次出现的索引
//lastIndexOf(Object o):返回此列表中指定元素的最后一次出现的索引
System.out.println("indexOf "+list.indexOf(3));
System.out.println("lastIndexOf "+list.lastIndexOf(3));
//remove(int index):删除该列表中指定位置的元素
list.remove(1);
System.out.println("remove "+list);
//get(int index):返回此列表中指定位置的元素
System.out.println("get "+list.get(1));
//set(int index, E element):用指定的元素替换此列表中指定位置的元素
list.set(2,Integer.valueOf(9));
System.out.println("set "+list);
List<Integer> subList = list.subList(2, 6);
System.out.println("subList "+subList);
}
LinkedList
使用同ArrayList,底层使用的是双向链表,增删效率高,查询效率低
要求
作为List接口的子类容器,存储的对象要重写equals()方法,因为有remove()、contains()等方法需要使用到
Vector
作为List接口的古老实现类,线程安全的,效率底
使用同ArrayList,底层使用的是数组,线程安全,一般都使用ArrayList
要求
作为List接口的子类容器,存储的对象要重写equals()方法,因为有remove()、contains()等方法需要使用到
Set
Set特点
Set接口容器存放的元素具有 无序性、不可重复性
Set实现类与区别
- HashSet:Set接口的主要实现类,线程不安全的,可以存储null值,底层是HashMap
- LinkedHashSet:作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历对于频繁的遍历,LinkedHashSet效率高于HashSet
**TreeSet**:底层是红黑树存储的,存储的必须是同一个类的对象。可以按照添加的对象按照指定的属性进行排序
新增方法
Ste接口中没有新增的方法,因为Ste是无序的,不需要索引,使用的都是Collection中声明的方法
HashSet
无序性、不可重复性
Set接口的主要实现类,线程不安全的,可以存储null值,底层是HashMap
理解
在HashSet中理解无序性、不可重复性
无序性
不等于随机性;有序性,像数组,存放数据时是一个挨着一个的,而实现Set接口的容器,存放数据时是无序的,存储的位置并非按照添加的顺序添加的,是按照hashCode值决定元素在数组中存储的位置。
不可重复性
当元素的hashCode不同时,必定是不同的元素,存储在不同的位置,当hashCode相同时那么存储的位置也是同一个地方,发现此位置已经有了元素,那么就比较equals(),如果是相同的,新元素存储失败,如果比较不同,则在此位置使用链表的方式存储;
添加元素存储的过程(以HashSet为例)
add()元素,hashCode决定存储位置
此位置是否有元素,如果无元素,存储成功,如果此位置有元素,比较equals(),如果equals()不同,则使用链表的方式在此位置存储元素,如果equals()相同,认为是同一个元素存储失败其实此过程都是底层HashMap完成的
jdk7:新的元素在数组中,指向原来的元素
jdk8:原来的元素放在数组中,指向新的元素
要求
HashSet和LinkedHashSet容器中的元素必须重写hashCode()和equals()
实例
//HashSet
@Test
public void test1() {
HashSet set = new HashSet();
set.add(456);
set.add(123);
set.add("AA");
set.add("CC");
set.add(789);
Iterator iterator = set.iterator();
//遍历的数据顺序与添加的顺序不同 AA CC 789 456 123
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
LinkedHashSet
作为HashSet的子类,遍历其内部数据时,可以按照添加的顺序遍历对于频繁的遍历,LinkedHashSet效率高于HashSet
要求
HashSet和LinkedHashSet容器中的元素必须重写hashCode()和equals()
使用方法于HashSet一致
TreeSet
底层是红黑树存储的,可以按照添加的对象按照指定的属性自动调用元素的比较器进行排序
注意
- 存储的必须是同一个类的对象
- 存储的对象,其类要实现Comparable接口比较器
- 在TreeSet中,存储元素的时候会先调用对象的比较器中的比较方法进行比较和排序,如果比较方法比较两个对象返回的是0,就认为是同一个对象,添加失败
- 自然比较排序:调用compareTo()方法进行比较排序,而不再是equals()
- 定制比较排序:调用compare()方法,而不再是equals(),在new实例化的时候就需要传入Comparator比较器对象
要求
TreeSet容器中存储的元素必须实现Comparable比较器接口,重写compareTo()
示例
//添加的元素自定义类实现Comparable接口,添加数据时,内部使用Comparable进行自然排序
@Test
public void test2() {
TreeSet set = new TreeSet();
//添加的对象必须是同一个类的对象
set.add(new Person("Tom",16));
set.add(new Person("Jerry",18));
set.add(new Person("Jim",19));
set.add(new Person("Timi",24));
set.add(new Person("Timi",25));
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
//自定义类实现Comparator接口,添加数据时,内部使用Comparator进行定制排序
//使用定制排序需要再new实例化对象的时候传入Comparator实例对象
@Test
public void test3() {
Comparator<Person> comparator = new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
//仅按照年龄升序排序
return o1.getAge() - o2.getAge();
}
};
TreeSet set = new TreeSet(comparator);
//添加的对象必须是同一个类的对象
set.add(new Person("Tom",16));
set.add(new Person("Jerry",18));
set.add(new Person("Jim",19));
set.add(new Person("Timi",24));
set.add(new Person("Timi",25));
Iterator iterator = set.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
//自定义的Person类,重写了compareTo()方法部分
@Override
public int compareTo(Person o) {
//按照姓名从小到大排序,如果姓名一样再按照年龄
// 直接调用String类中的compareTo()方法
int flag = this.name.compareTo(o.name);
if(flag != 0) {
return flag;
} else {
flag = this.age - o.age;
return flag;
}
}
Map双列集合
Map:存储双列数据,存储key-value键值对数据
HashMap:作为Map的主要实现类;线程不安全,效率高;可以存储null的key和value
LinkedHashMap:保证在遍历元素时可以按照添加的顺序遍历。
原因:在原有的HashMap底层结构基础上,添加了一对指针,指向前一个和后一个元素。
对于频繁遍历的操作,此类执行效率高于HashMap
TreeMap:保证按照添加的key-value进行排序,实现排序遍历,此时考虑key的自然排序和定制排序,底层使用红黑树
hashtable:作为古老的实现类;线程安全,效率底;不能存储null的key和value
- Properties:常用来处理配置文件。key和value都是String类型
HashMap底层:数组+链表(jdk7及之前)
数组+链表+红黑树(jdk8)
HashSet底层使用的是HashMap,Set的值存储在Map的key位置,而value位置存储的是一个类对象的Object,无实际意义
HashMap结构理解
- HashMap中的key:无序的、不可重复的 ————> key所在的类要重写equals()和hashCode()
- HashMap中的value:无序的、可重复的 ————> value所在的类要重写equals()
- 一个键值对:key-value构成了一个Entry对象,Map中的Entry是无序的,不可重复复的
注意
- key所在的类要重写equals()和hashCode()
- value所在的类要重写equals()
- 当key比较一致的时候,新的value会替换旧的value
Map中定义的方法
增、删、改
- V put(K key, V value):将指定的key-value添加到(或修改)当前map对象中
- void putAll(Map m):将参数的m中所有的key-value添加到map中
- V remove(Object key):删除指定key的key-value,并返回value
- void clear():清空map中所有数据
元素查询的方法
- V get(Object key):返回key对应的value,无则返回null;
- boolean containsKey(Object key):是否包含指定key
- boolean containsValue(Object value):是否包含指定的value
- int size():返回Map中键值对的个数
- boolean isEmpty():判断map是否为空
- boolean equals(Object o):判断当前map与参数对象是否相等
元视图的操作方法
- Set keySet() 获取map中包含所有key的Set对象集合
- Collection values() 获取map中包含的value的Collection集合
- Set<Map.Entry<K,V>> entrySet() 获取map中所有key-value构成Entry的Set集合
HashMap
增删改
@Test
public void test1() {
HashMap<String,Integer> map = new HashMap<>();
//put(K key, V value)
map.put("小明",18);
map.put("小红",16);
map.put("小军",17);
map.put("小明",16); //第二次添加覆盖了原来的value值,相当于修改了数据
System.out.println(map);
HashMap<String,Integer> map1 = new HashMap<>();
map1.put("小强",19);
map1.put("小兰",15);
//putAll(Map m)
map.putAll(map1);
System.out.println(map);
//remove(Object key)
map.remove("小明");
System.out.println(map);
//clear()
map.clear();
System.out.println(map);
}
元素的查询方法
@Test
public void test2() {
HashMap<String,Integer> map = new HashMap<>();
map.put("小明",18);
map.put("小红",17);
map.put("小兰",17);
//V get(Object key)
Integer value = map.get("小明");
System.out.println(value);
//containsKey(Object key):是否包含指定key
boolean flag;
flag = map.containsKey("小红");
System.out.println(flag);
flag = map.containsKey("小娜");
System.out.println(flag);
//containsValue(Object value):是否包含指定的value
flag = map.containsValue(17);
System.out.println(flag);
flag = map.containsValue(19);
System.out.println(flag);
//size()
int size = map.size();
System.out.println(size);
//isEmpty():判断map是否为空
flag = map.isEmpty();
System.out.println(flag);
//equals(Object o):判断当前map与参数对象是否相等
HashMap<String,Integer> map1 = new HashMap<>();
map1.put("小明",18);
map1.put("小兰",17);
map1.put("小红",17);
flag = map.equals(map1);
System.out.println(flag);
}
元视图的操作方法,遍历map元素
@Test
public void test3() {
HashMap<String,Integer> map = new HashMap<>();
map.put("小明",18);
map.put("小红",17);
map.put("小兰",17);
//遍历所有的key
//Set<K> keySet() 获取map中包含所有key的Set对象集合
Set<String> keySet = map.keySet();
Iterator<String> keyIterator = keySet.iterator();
while (keyIterator.hasNext()) {
System.out.println(keyIterator.next());
}
//遍历所有的value
//Collection<V> values() 获取map中包含的value的Collection集合
Collection<Integer> values = map.values();
for (Integer value : values) {
System.out.println(value);
}
//遍历所有的key-value
//方式一:通过Set<Map.Entry<K,V>> entrySet() 获取map中所有key-value构成Entry的Set集合
Set<Map.Entry<String, Integer>> entries = map.entrySet();
Iterator<Map.Entry<String, Integer>> entryIterator = entries.iterator();
while (entryIterator.hasNext()) {
System.out.println(entryIterator.next());
}
//方式二:通过key,在map中查找value
Set<String> keySet1 = map.keySet();
Iterator<String> keyIterator1 = keySet1.iterator();
while (keyIterator1.hasNext()) {
String nextKey = keyIterator1.next();
System.out.println(nextKey + " --> " + map.get(nextKey));
}
}
HashMap实现原理
jdk7为例
(1)HashMap map = new HashMap();
- 实例化后,底层创建了长度为16的一维数组Entry[] table
(2)map.put(key1,value1);
首先调用key1所在类的hashCode()计算出key1的哈希值,此哈希值经过某种算法计算之后,得到在Entry[]数组中的存放位置。
如果此位置上没用数据,此时key1-value1 添加成功。————情况1
如果此位置上有一个或多个数据(以链表的形式进行存储),比较key1和已经存在的数据的哈希值:
- 如果key1的哈希值与已经存在的元素的key的哈希值不同,此时key1-value1添加成功 ————情况2
- 如果key1的哈希值与预警存在的某个元素的key哈希值相同,继续比较:调用key所在类的equals(),比较key值
- 如果equals()返回false:此时key1-value1添加成功。
- 如果equals()返回true:使用value1替换旧的value
补充
一个位置上存在多个元素,元素使用链表的方式进行存储
在不断的添加过程中,会涉及到扩容问题,默认的扩容方式:扩容为原来容量的2倍,并将原有的数据复制过来
jdk8 相交于jdk7在底层实现方面的不同
new HashMap():底层没有立即创建一个长度为16的数组
jdk8底层的数组是:Node[],而非Entry[]
首次调用put()时,底层创建长度为16的Node[]数组
jdk7底层结构只有:数组+链表。jdk8底层结构:数组+链表+红黑树。
当数组的某个索引位置上的元素以链表的形式存储的数据 > 8且当前数组长度 > 64时,此索引位置上的所有数据改为红黑树存储
TreeMap
注意
TreeMap 按照key进行排序的,通过Java的比较器进行比较排序,所以key的类需要实现Comparable接口,并且存储的元素key都是同一个类的对象
排序
- 自然排序:key实现Comparable接口,重写compareTo()方法
- 定制排序:在实例化key的时候构造器传入自定义的比较器Comparator对象
自然排序
添加的元素的key所在的类需要实现Comparable接口,重写compareTo()方法
@Test
public void test1() {
//Person类中已经按照姓名从小到大,年龄从小到大的排序
TreeMap<Person,String> map = new TreeMap<>();
map.put(new Person("小明",18),"香蕉");
map.put(new Person("小红",16),"苹果");
map.put(new Person("小强",16),"猕猴桃");
map.put(new Person("小钢",17),"菠萝");
System.out.println(map);
}
定制排序
在创建Map对象的时候需要在构造器中传入自定义的比较器Comparator对象
//定制排序
@Test
public void test2() {
//先按照年龄从小到大排序,然后按照姓名从小到大排序
TreeMap<Person,String> map = new TreeMap<>(new Comparator<Person>() {
@Override
public int compare(Person o1, Person o2) {
int flag = o1.getAge() - o2.getAge();
if(flag != 0) {
return flag;
} else {
return o1.getName().compareTo(o2.getName());
}
}
});
map.put(new Person("小明",18),"香蕉");
map.put(new Person("小红",16),"苹果");
map.put(new Person("小强",16),"猕猴桃");
map.put(new Person("小钢",17),"菠萝");
System.out.println(map);
}
Properties
Properties:常用来处理配置文件。key和value都是String类型
使用Properties加载配置文件
@Test
public void test1() {
Properties properties = new Properties();
try(InputStream is = new FileInputStream("src/Map/jdbc.properties")) {
properties.load(is);
String name = properties.getProperty("name");
String age = properties.getProperty("age");
System.out.println(name + "-->" + age);
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
jdbc.properties文件
name=mango
age=19
Collections容器工具类
可用于Collections接口的实现类,以及Map接口的实现类
//常用方法
* T max(Collection coll):根据其元素的 自然顺序返回给定集合的最大元素。
* T max(Collection coll, Comparator comp):根据指定的比较器引发的顺序返回给定集合的最大元素。
* T min(Collection coll):根据其元素的 自然顺序返回给定集合的最小元素。
* T min(Collection coll, Comparator comp):根据指定的比较器引发的顺序返回给定集合的最小元素。
* int frequency(Collection coll, Object obj):返回指定集合中与指定对象相等的元素数。
* void copy(List dest, List src):将所有元素从一个列表复制到另一个列表中。
* boolean replaceAll(List list, T oldValue, T newValue):将列表中一个指定值的所有出现替换为另一个。
* void reverse(List list):反转指定列表中元素的顺序。
* void shuffle(List list):使用默认的随机源随机排列指定的列表
* void sort(List<T> list):根据其元素的自然排序对指定的列表进行排序。
* void sort(List<T> list, Comparator c):根据指定的比较器引起的顺序对指定的列表进行排序。
特别注意copy()方法的使用
@Test
public void test1() {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(1);
list.add(2);
list.add(3);
list.add(4);
//特别注意,copy()需要提供一个可以装下容器,所以传入的容器必须有足够的空间,即size()够大
List<Object> list1 = Arrays.asList(new Object[10]);
Collections.copy(list1,list);
System.out.println(list1);
}
线程
方式一: 继承Thread类
步骤
-
创建一个继承于Thread类的对象
-
重写Thread类的run()方法 --> 此线程将要执行的操作声明在run()中
-
创建Thread类子类对象
-
通过实例对象调用start()方法,(1)启动当前线程 (2)调用当前线程的run()方法
注意
- run()方法只是一个普通方法,只有调用start()方法才能创建新的线程
- 一个Thread子类对象实例只能start一次,只能创建启动一次线程
Thread类中常用方法
void | start() | 使该线程开始执行;Java 虚拟机调用该线程的 run() 方法。 |
void | run() | 通常要重写此方法,将创建的线程要执行的操作写在此方法中 |
static Thread | currentThread() | 返回对当前正在执行的线程对象的引用 |
void | setName() | 设置当前线程名称 |
String | getName() | 获取当前线程名称 |
static void | yield() | 暂停当前正在执行的线程对象进入就绪状态,并执行其他线程,静态方法,不会释放锁对象 |
void | join() | 等待该线程终止,在线程a中调用线程b的join()方法,线程a进入阻塞状态, 直到b指向完毕或等待规定的时间之后,join()方法有三种重载形式,其余两种可设置时间, 注意不要调用自己的join()方法,否则自己的线程就会进入等待 |
void | sleep() | 在指定的毫秒(或毫秒加纳秒)数内休眠线程,不会释放锁对象 |
从以下示例去理解创建和启动线程
- 创建一个线程变量100以内的偶数
public class demo01 {
public static void main(String[] args) {
//3. 创建Thread类子类对象
TestThread testThread = new TestThread();
//4. 通过实例对象调用start()方法,启动线程
testThread.start();
System.out.println("hello_1");
System.out.println("hello_2");
System.out.println("hello_3");
}
}
//1. 创建一个继承于Thread类的对象
class TestThread extends Thread {
//2. 重写Thread类的run()方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(i);
}
}
}
}
- 创建三条线程同时下载三张图片
public class demo02 {
public static void main(String[] args) {
//启动三个线程下载三张图片
new DownThread("Thread/src/file/海绵宝宝.jpg",
"http://www.qkmango.top/file/img/hmbb.jpg").start();
new DownThread("Thread/src/file/派大星.jpg",
"http://www.qkmango.top/file/img/pdx.jpg").start();
new DownThread("Thread/src/file/章鱼哥.jpg",
"http://www.qkmango.top/file/img/zyg.jpg").start();
}
}
/**
* <p>下载图片线程类</p>
* <p>此类启动线程后调用进行下载图片</p>
*/
class DownThread extends Thread {
private String name;
private String path;
@Override
public void run() {
new DownImg().download(name, path);
}
public DownThread(String name, String url) {
this.name = name;
this.path = url;
}
}
/**
* 下载图片类
*/
class DownImg {
public void download(String path, String url) {
URL webUrl = null;
try {
webUrl = new URL(url);
} catch (MalformedURLException e) {
e.printStackTrace();
}
try(BufferedInputStream is = new BufferedInputStream(webUrl.openStream());
BufferedOutputStream os =
new BufferedOutputStream(new FileOutputStream(path))) {
byte[] flush = new byte[1024*8];
int len = -1;
while ((len = is.read(flush)) !=-1) {
os.write(flush,0,len);
}
os.flush();
System.out.println(path+" 图片下载完成!");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 部分常用方法
public class demo01 {
public static void main(String[] args) {
//5. setName()
Thread.currentThread().setName("main线程");
TestThreadMethod t = new TestThreadMethod("遍历线程");
t.start();
for (int i = 0; i < 100; i++) {
if(i % 2 ==0) {
//3. currentThread()
//4. getName()
System.out.println(Thread.currentThread().getName()+": "+i);
}
if(i == 20) {
try {
//join(),当主线程1==20时调用t.join(),主线程进入阻塞状态,等待t线程执行完毕
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
class TestThreadMethod extends Thread {
//调用父类构造器传入线程名字
TestThreadMethod(String name) {
super(name);
}
//2. run()
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 ==0) {
//3. currentThread()
//4. getName()
System.out.println(Thread.currentThread().getName()+": "+i);
}
if(i % 20 ==0) {
//6. yield()
Thread.yield();
}
}
}
}
方式二:实现Runnable接口
步骤
-
创建一个实现了Runnable接口的类
-
实现类去实现Runnable接口中的抽象方法run()方法
-
创建实现类对象
-
创建Thread类的对象,将实现类对象作为参数传入Thread的构造方法
-
通过Thread类对象调用start()方法
注意
这种方式创建多个Thread类对象,可将同一个实现类对象作为一个参数传递进去,那么此时多个线程都是公用的同一个对象同一份数据
从以下实例去理解创建多线程的第二种方式
- 三个线程共同使用同一个对象的数据,共同使用一份数据,共同售卖100张票(此时会有线程安全问题)
public class demo01 {
public static void main(String[] args) {
//创建三个线程,共用同一个对象
Window window = new Window();
new Thread(window,"窗口1").start();
new Thread(window,"窗口2").start();
new Thread(window,"窗口3").start();
}
}
/**
* 售票窗口
*/
class Window implements Runnable {
//票数
private int ticket = 100;
@Override
public void run() {
while (true) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
"售卖第" + ticket + "张票");
ticket--;
} else {
System.out.println("售卖完毕");
break;
}
}
}
}
线程优先级
线程优先级有10挡
Thread类中提供了3种内置的常量
MAX_PRIORITY: 10 线程可以具有的最高优先级
MIN_PRIORITY: 1 线程可以具有的最低优先级
NORM_PRIORITY:5 分配给线程的默认优先级
设置和获取线程的优先级
setPriority(int newPriority) 更改线程的优先级
getPriority() 获取线程的优先级
注意
高优先级的线程抢占低优先级线程的CPU的执行权,但只是从概率上讲高优先级的的线程高概率被执行,
并不意味着高优先级的线程执行完低优先级的线程才执行,只是概率问题
从以下实例掌握线程优先级的设置的获取
public class demo01 {
public static void main(String[] args) {
TestThreadPriority t = new TestThreadPriority();
//设置t线程为最大优先级
t.setPriority(Thread.MAX_PRIORITY);
t.start();
//设置当前主线程为最低优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+
": "+Thread.currentThread().getPriority()+": "+i);
}
}
}
}
/**
* 测试线程优先级类
*/
class TestThreadPriority extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(getName()+": "+getPriority()+": "+i);
}
}
}
}
线程的生命周期
线程的生命周期一共有五种状态
- 新建
- 就绪
- 运行
- 阻塞
- 死亡
线程安全
使用同步机制解决线程安全问题
引例
一下代码,三个线程共同使用同一份对象的数据,但是在运行时会出现线程安全问题
问题:会出现重票和错票 --> 出现了线程安全问题
* 原因:在某个线程操作车票时,尚未操作完成时,其他线程参与进来,也操作车票
* 如何解决:当一个线程操作ticket时,其他线程不能参与进来,直到此线程操作完ticket时其他线程才能参与进来,
* 即使此线程发生了阻塞,也不能被改变
public class demo01 {
public static void main(String[] args) {
TestThreadPriority t = new TestThreadPriority();
//设置t线程为最大优先级
t.setPriority(Thread.MAX_PRIORITY);
t.start();
//设置当前主线程为最低优先级
Thread.currentThread().setPriority(Thread.MIN_PRIORITY);
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(Thread.currentThread().getName()+
": "+Thread.currentThread().getPriority()+": "+i);
}
}
}
}
/**
* 测试线程优先级类
*/
class TestThreadPriority extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if(i % 2 == 0) {
System.out.println(getName()+": "+getPriority()+": "+i);
}
}
}
}
方式一:synchronized同步代码块
* synchronized(同步监视器) {
* //需要被同步的代码
* }
* 说明:1. 操作共享数据的代码,即为需要被同步的代码
* 2. 共享数据:多个线程共同操作的数据。比如本问题当中的ticket就是共享数据
* 3. 同步监视器,俗称“锁”,任何一个类的对象都可以充当锁;
* 要求:多个线程都要使用同一把锁
* 注意:将操作共享数据的代码包含起来,但是不能包含多了,也不能包含少了
提示:
“锁”也可以是当前对象this
,只要在合适的情况下满足多个线程共用一把锁就可以了
"锁"也可以是当前这个类,类也是对象,例如Window.class
就是Window类这个对象,需要反射的知识
示例 解决实现Runnable接口的线程安全问题
public class demo01 {
public static void main(String[] args) {
//创建三个线程,共用同一个对象
Window window = new Window();
new Thread(window,"窗口1").start();
new Thread(window,"窗口2").start();
new Thread(window,"窗口3").start();
}
}
/**
* 售票窗口
*/
class Window implements Runnable {
//票数
private int ticket = 100;
@Override
public void run() {
while (true) {
//synchronized包裹住操作共享数据的代码
//此时多个线程使用同一个对象,所以this是唯一的,满足要求
synchronized (this) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
"售卖第" + ticket + "张票");
ticket--;
} else {
System.out.println("售卖完毕");
break;
}
}
}
}
}
示例 解决继承Thread类的线程安全问题
大同小异,只要保证"锁"是唯一的就行了,当然,ticket数据要是共享的
public class demo02 {
public static void main(String[] args) {
//创建三个Thread子类对象
new Window1().start();
new Window1().start();
new Window1().start();
}
}
/**
* 售票窗口
*/
class Window1 extends Thread {
//票数
//因为实现Thread方式创建多线程,各线程使用的不是同一个对象,所以为了保证票数,使用static
private static int ticket = 100;
//同一把锁:obj,任何类对象都行
//private static Object obj = new Object();
@Override
public void run() {
while (true) {
//synchronized包裹住操作共享数据的代码
//synchronized (obj) {
//此处也可以使用Window类这个对象,类也是对象
synchronized (Window1.class) {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
"售卖第" + ticket + "张票");
ticket--;
} else {
System.out.println("售卖完毕");
break;
}
}
}
}
}
方式一同步线程的有点与局限性
- 解决了线程安全问题
- 在同步代码块的区域,只允许一个线程执行,相当于此时是单线程
- 当多个线程尝试获取锁时,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,这样会造成性能消耗
方式一:synchronized同步方法
* 操作共享数据的的代码完整的声明在一个方法中,不妨将方法声明为同步的
* 注意:
* 1. synchronized修饰的非静态方法时,默认的锁为this,叫做方法锁,也叫做对象锁;
* 2. synchronized修饰的静态方法时,默认的锁为Window.class,即类这个对象,叫做类锁
示例 解决实现Runnable接口的线程安全问题
public class demo01 {
public static void main(String[] args) {
//创建三个Thread子类对象,共用一个window对象
Window w = new Window();
new Thread(w).start();
new Thread(w).start();
new Thread(w).start();
}
}
class Window implements Runnable {
private static int ticket = 100;
private boolean flag = true;
@Override
public void run() {
while (true) {
if(flag) {
show();
} else {
break;
}
}
}
//将操作共享数据的代码封装到方法里面,声明方法为synchronized
//注意,此时的锁默认为this(称为方法锁,也叫对象锁)
private synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
"售卖第" + ticket + "张票");
ticket--;
} else {
flag = false;
System.out.println("售卖完毕");
}
}
}
示例 解决继承Thread类的线程安全问题
public class demo02 {
public static void main(String[] args) {
//创建三个子类对象
new Window().start();
new Window().start();
new Window().start();
}
}
class Window extends Thread {
private static int ticket = 100;
private static boolean flag = true;
@Override
public void run() {
while (true) {
if(flag) {
show();
} else {
break;
}
}
}
//将操作共享数据的代码封装到方法里面,声明方法为synchronized
//注意,此时方法为static,锁默认为Window.class,称为类锁
private static synchronized void show() {
if (ticket > 0) {
System.out.println(Thread.currentThread().getName() +
"售卖第" + ticket + "张票");
ticket--;
} else {
flag = false;
System.out.println("售卖完毕");
}
}
}
解决懒汉式单例模式的线程安全问题
- 以下为单利模式的懒汉式,此时当多个线程获取
Bank
的同一个示例对象时,调用getInstance()
方法,当A线程执行到if(instance == null)
后cpu切换到B线程,B线程在进行判断if(instance == null)
,后new一个对象,之后得到实例对象,再之后A线程再次被执行,此时也new了一个对象,导致对象不是唯一的,线程不安全
class Bank {
private static Bank instance = null;
private Bank(){}
public static Bank getInstance() {
if(instance == null) {
instance = new Bank();
}
return instance;
}
}
- 使用
synchronized
解决线程安全 方式一,效率略低
多个线程执行getInstance()
时都会发生阻塞,但其实只需要第一个线程执行new Bank()
即可,其他线程直接返回instance
即可,但此时每个线程执行都会阻塞,效率略底
class Bank {
private static Bank instance = null;
private Bank(){}
/* 方式一:但是效率稍低
* 此时解决了线程安全问题,但是效率稍低,当instance不为null时,
* 可以直接返回instance,而不需要进行synchronized里面的代码块,
* 执行synchronized代码块其他线程会阻塞
*/
public static Bank getInstance() {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
return instance;
}
}
- 使用
synchronized
解决线程安全 方式二,效率稍高
此时,即使多个线程都进入了第一个if(instance == null)
内,但是synchronized
代码块线程是安全的,其中一个线程去执行instance = new Bank()
后instance
不为null
,刚开始一起进入的if(instance == null)
内部的线程会进行进行第二次判断,此时不为null
,直接跳出,返回instance
;之后再来的线程直接进行第一个判断即可跳过synchronized
执行最后一条return instance
语句,不需要每个线程每次都去执行synchronized
发生线程阻塞。
class Bank {
private static Bank instance = null;
private Bank(){}
//方式二:效率略高
public static Bank getInstance() {
if(instance == null) {
synchronized (Bank.class) {
if (instance == null) {
instance = new Bank();
}
}
}
return instance;
}
}
- 指令重排导致线程安全问题扩展
但是,双重检查加锁并不代码百分百一定没有线程安全问题了。因为,这里会涉及到一个指令重排序问题。
instance = new Bank();其实可以分为下面的步骤:
1.申请一块内存空间;
2.在这块空间里实例化对象;
3.instance的引用指向这块空间地址;
指令重排序存在的问题是:
对于以上步骤,指令重排序很有可能不是按上面123步骤依次执行的。比如,先执行1申请一块内存空间,然后执行3步骤,instance的引用去指向刚刚申请的内存空间地址,那么,当它再去执行2步骤,判断instance时,由于instance已经指向了某一地址,它就不会再为null了,因此,也就不会实例化对象了。这就是所谓的指令重排序安全问题。那么,如何解决这个问题呢?
加上volatile关键字,因为volatile可以禁止指令重排序。
多线程死锁问题
描述
- 不同的线程分别占用对方需要的同步资源(需要的锁)不放弃,
- 都在等待对方放弃自己需要的同步资源(需要的锁),就形成了线程的死锁;
- 出现死锁后,不会出现异常,不会提示,只是线程都处于阻塞状态,无法继续执行
解决方法
-
专门的算法、原则
-
尽量减少同步资源的定义
-
尽量避免嵌套同步(嵌套使用synchronized)
死锁示例
- 例子 1
* 示例中使用匿名内部类创建了两个线程,
* A线程执行到外synchronized时获得了s1锁,然后休眠100毫秒,此时CPU执行B线程,
* B线程执行外synchronized时获得了s2锁,然后也休眠100毫秒,此时CPU又回到A线程,
* 执行内synchronized,但是需要的s2锁已经被B线程拿到,于是A线程等待;CPU又切换执行B线程时,
* 执行内synchronized需要s1锁,但是也已经被A线程拿到,于是两个线程进入僵持状态
public class demo01 {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s1) {
s1.append("aaa");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2) {
s2.append(111);
}
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2) {
s1.append(222);
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1) {
s2.append("bbb");
}
}
}
}).start();
}
}
- 例子 2
public class demo01 {
public static void main(String[] args) {
DeadLock dl = new DeadLock();
new Thread(dl).start();
new Thread(dl).start();
}
}
class DeadLock implements Runnable {
private StringBuffer s1 = new StringBuffer();
private StringBuffer s2 = new StringBuffer();
@Override
public void run() {
//如果是线程t1,执行methodA(),否则执行methodB()
if(Thread.currentThread().getName().equals("t1")) {
methodA();
} else {
methodB();
}
}
public void methodA() {
synchronized(s1) {
s1.append("111");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
methodB();
}
}
public void methodB() {
synchronized(s2) {
s2.append("222");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
methodA();
}
}
}
方式二:Lock锁
ReentrantLock
类
java.lang.Object
java.util.concurrent.locks.ReentrantLock
步骤
- 创建锁
ReentrantLock lock = new ReentrantLock()
- 获取锁
lock()
- 释放锁
unlock()
注意
-
获取锁与释放锁需要手动
-
如果忘记释放锁就会产生死锁的问题,所以,通常需要在
finally
中进行锁的释放,即需要操作共享数据的代码放在
try
中,在try
中获取锁,最后在finally
中释放锁 -
此时多个线程的锁
ReentrantLock lock
也要保证唯一
示例
- 实现
Runnable
接口使用Lock
锁,此时多个线程共用一个对象,锁唯一
public class demo01 {
public static void main(String[] args) {
Window w = new Window();
new Thread(w).start();
new Thread(w).start();
new Thread(w).start();
}
}
class Window implements Runnable {
private int ticket = 100;
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
//手动获取锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName()
+ " 票号为:" + ticket);
ticket--;
} else {
System.out.println(Thread.currentThread().getName() + "已售完");
break;
}
} finally {
//手动释放锁
lock.unlock();
}
}
}
}
- 实现继承
Thread
类使用Lock
锁,此时多个线程有多个对象,保证锁唯一可以声明为static
public class demo02 {
public static void main(String[] args) {
new Window().start();
new Window().start();
new Window().start();
}
}
class Window extends Thread {
private int ticket = 100;
private static ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true) {
try {
//手动获取锁
lock.lock();
if (ticket > 0) {
System.out.println(Thread.currentThread().getName()
+ " 票号为:" + ticket);
ticket--;
} else {
System.out.println(Thread.currentThread().getName() + "已售完");
break;
}
} finally {
//手动释放锁
lock.unlock();
}
}
}
}
synchronized
与Lock
异同对比
- 面试题 解决线程安全的方式有几种?
synchroized
同步代码块synchroized
同步方法Lock
锁
练习
* 有一个银行账户,有两个储户分别存3000元,分三次存储,每次存储完打印余额
* 分析:两个储户为两个线程
共享数据为账户,保证存钱操作时账户余额要线程安全
public class demo02 {
public static void main(String[] args) {
//同一个账户
Account account = new Account(0);
//使用extends Thread方式
//new User(account,"用户1").start();
//new User(account,"用户2").start();
//使用implements Runnable接口方式
new Thread(new User(account,"用户1")).start();
new Thread(new User(account,"用户2")).start();
}
}
/**
* 用户类
*/
class User implements Runnable {
private Account account;
@Override
public void run() {
//存钱3次,每次1000
for (int i = 0; i < 3; i++) {
account.saveMoney(1000);
}
}
public User(Account account, String name) {
this.account = account;
}
}
/**
* 账户类
*/
class Account {
private double money;
private ReentrantLock lock = new ReentrantLock();
public double getMoney() {
return money;
}
public Account(double money) {
this.money = money;
}
/**
* 存钱方法
* @param money
*/
public void saveMoney(double money) {
try {
lock.lock();
Thread.sleep(100);
this.money += money;
System.out.println(Thread.currentThread().getName()
+"存钱成功,余额为:"+this.money);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
线程通讯
线程通讯涉及到三个方法
wait() | 导致当前线程等待,进入阻塞,释放同步监视器 | |
notify() | 唤醒在此对象监视器上等待的单个线程 | |
notifyAll() | 唤醒在此对象监视器上等待的所有线程 |
注意
-
wait()
、notify()
、notifyAll()
三个方法必须使用在同步方法或同步代码块当中(synchronized
) -
wait()
、notify()
、notifyAll()
三个方法的调用者必须是同步代码块或同步方法中的同步监视器,否则会报错 -
wait()
、notify()
、notifyAll()
三个方法是定义在Object
类中的
示例
-
使用线程通讯,使两个线程交替打印1-100数字
* 原理: * 打印前将一个等待的线程唤醒,打印后当前线程进入等待阻塞状态,释放同步锁
public class demo01 {
public static void main(String[] args) {
PrintNum printNum = new PrintNum();
new Thread(printNum).start();
new Thread(printNum).start();
}
}
class PrintNum implements Runnable {
private int num = 1;
@Override
public void run() {
while (true) {
synchronized (this) {
//唤醒一个在等待的线程
this.notify();
if(num < 100) {
System.out.println(Thread.currentThread().getName()+": "+num++);
try {
//阻塞当前线程,释放同步监视器
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
} else {
break;
}
}
}
}
}
面试题:sleep()
和wait()
方法异同
相同
- 调用这两个方法后,当前线程都会进入阻塞状态
不同
-
方法的声明位置不同
sleep()
声明在Thread
类中wait()
声明在Object
类中
-
调用的要求不一样
sleep()
可以在任何场景下调用wait()
必须在同步代码块或同步方法中调用,调用者必须是同步监视器对象
-
是否释放同步监视器
sleep()
不会释放同步监视器wait()
会释放同步监视器
方式三:实现Callable接口
使用Callable接口和Future接口创建线程JDK1.5新增
具体是创建Callable接口的实现类,并实现clall()方法。并使用FutureTask类来包装Callable实现类的对象,且以此FutureTask对象作为Thread对象的target来创建线程。
步骤
-
创建Callable借口的实现类,重写call()方法,并创建对象
-
创建一个FutureTask类对象,将Callable实现类对象作为构造器的参数传入
-
创建一个Thread类对象,将FutureTask类对象作为参数传入构造器
-
启动Thread线程
-
如果需要此线程的返回值,可以通过FutureTask类对象的get()方法获取
与实现Runnable接口创建的线程的不同点
通过实现Callable接口的方式比实现Runnable接口的方式创建多线程更强大:
- 实现Callable接口,重写call()方法可以有返回值
- 可以抛出异常,被外面的操作捕获,获取异常信息
- Callable支持泛型(正是返回值类型)
实例
- 通过一个线程实现1-100的和,并返回和值
public class demo01 {
public static void main(String[] args) {
//创建一个FutureTask类对象,将实现类对象作为参数传入构造器
FutureTask futureTask = new FutureTask(new NewThreadIC());
//创建一个Thread线程类,并start(),FutureTask类对象作为构造器参数传入
new Thread(futureTask).start();
try {
//通过FutureTask类对象调用get()方法获取线程的call方法的返回值
System.out.println(futureTask.get());
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
/**
* 创建Callable借口的实现类,重写call()方法
*/
class NewThreadIC implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
sum += i;
}
return sum;
}
}
方式四:使用线程池
守护线程
了解
Java分为两种线程:用户线程和守护线程
所谓守护线程是指在程序运行的时候在后台提供一种通用服务的线程,比如垃圾回收线程就是一个很称职的守护者,并且这种线程并不属于程序中不可或缺的部分。因此,当所有的非守护线程结束时,程序也就终止了,同时会杀死进程中的所有守护线程,JVM不会等待守护线程执行完毕再停止。
使用
将线程转换为守护线程可以通过调用Thread
对象的setDaemon(true)
方法来实现。在使用守护线程时需要注意一下几点:
注意
thread.setDaemon(true)
必须在thread.start()
之前设置,否则会跑出一个IllegalThreadStateException
异常。你不能把正在运行的常规线程设置为守护线程。- 在Daemon线程中产生的新线程也是Daemon的。
- 守护线程应该永远不去访问固有资源,如文件、数据库,因为它会在任何时候甚至在一个操作的中间发生中断。
示例
- 男孩线程一直默默地守护这女孩线程,当女孩线程结束毕业后,男孩线程就也跟着结束了
public class demo01 {
public static void main(String[] args) {
Girl girl = new Girl();
Boy boy = new Boy();
Thread t1 = new Thread(girl);
Thread t2 = new Thread(boy);
//将t2设置为守护线程
t2.setDaemon(true);
t1.start();
t2.start();
}
}
/**
* 女孩
*/
class Girl implements Runnable {
@Override
public void run() {
for (int i = 1; i < 12*3; i++) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("再学校的第" + i +"月");
}
System.out.println("毕业");
}
}
/**
* 男孩,一直默默地守护着女孩
*/
class Boy implements Runnable {
@Override
public void run() {
while (true) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("\t默默地守护着你");
}
}
}
Thread和Runnable方式总结
实现Runnable接口类去创建多线程,需要将子类实例对象丢到Thread类的实例对象的构造器中,通过Thread实例对象去启动线程,因为实现Runnable接口,没有start()方法,只能借助Thread类中的start()方法启动线程。当然,如果如果多个线程去共享同一份数据,大可把一个子类对象丢到多个Thread类的构造方法中,创建的多个线程,使用的都是同一个对象的数据。
继承Thread类的方式创建多线程,因为继承的Thread类中已有start()方法,所以可以通过子类实例对象的start()方法启动线程,如果需要多个线程使用同一份数据,那么可以将共享数据声明为start类型,这样多个线程就可以使用同一份数据了。
另外,继承Thread类去创建多线程,也可以通过Thread类的实例去启动线程,这样也可以多个线程共用同一个对象的数据,但是继承Thread的方式创建的对象,本身就含有一个线程,完全可以使用自己的线程执行run()方法,如果使用Thread类实例对象的方式去启动线程,此时实际上我们只需要一个线程,但是存在了两线程,但是只有调用start()方法的对象的线程才是启动的,白白的浪费了一个线程。
总结:Thread类的对象以及继承Thread类的对象在new的时候就创建了线程,只需要自身调用start()方法即可启动线程;而实现Runnable接口的子类,new的时候是没有创建线程的,需要通过Thread()的实例对象的线程去执行子类的run()方法;继承Thread类的实例对象不需要借助Thread对象代理,实现Runnable接口的需要借助代理。
网络编程
IP
inetAddress类,此类表示Internet协议(IP)地址
IP是用来定位节点/电脑的
InetAddress类常用方法
返回值类型 | 方法名 | 解释 |
---|---|---|
InetAddress | getLocalHost() | 静态方法,返回本机的InetAddress对象 |
String | getHostAddress() | 返回IP地址 |
String | getHostName() | 返回计算机名 |
InetAddress | getByName() | 给定主机名,返回主机的InetAddress对象,主机名可以是www地址,或者是ip地址 |
示例代码
public class demo01 {
public static void main(String[] args) throws UnknownHostException {
//获取本地主机InetAddress对象
InetAddress inetAddress = InetAddress.getLocalHost();
//打印主机地址和主机名
System.out.println("本机IP:"+inetAddress.getHostAddress());
System.out.println("本机名:"+inetAddress.getHostName());
//通过主机名获取主机InetAddress对象
inetAddress = InetAddress.getByName("www.qkmango.top");
System.out.println("主机IP:"+inetAddress.getHostAddress());
System.out.println("主机名:"+inetAddress.getHostName());
}
}
端口
端口是区分软件的 ,区分计算机中不同的进程
了解端口
InetSocketAddress类常用方法
返回值类型 | 方法名 | 解释 |
---|---|---|
InetSocketAddress | InetSocketAddress(“网址/IP地址”,端口) | 构造方法 |
inetAddress | getAddress() | 返回inetAddress类对象 |
String | getHostName() | 返回主机名 |
int | getPort() | 返回主机端口号 |
URL
了解URL
URL是互联网三大基石之一:html、http、url
- URI = Universal Resource Identifier 统一资源标志符,用来标识抽象或物理资源的一个紧凑字符串。
- URL = Universal Resource Locator 统一资源定位符,一种定位资源的主要访问机制的字符串,一个标准的URL必须包括:protocol、host、port、path、parameter、anchor。
- URN = Universal Resource Name 统一资源名称,通过特定命名空间中的唯一名称或ID来标识资源。
在www上,每个资源都有一个统一且唯一的地址,即统一资源定位符URL,
如:http://www.baidu.com:80/index.html?name=mango&age=19#a
协议 主机名 端口 资源路径 参数列表
- 协议 http
- 主机名 www.baidu.com
- 端口号 80
- 资源路径 index.html
- 参数列表
常用方法
返回值类型 | 方法名 | 解释 |
---|---|---|
String | getProtocol() | 获取协议 |
String | getHost() | 获取主机名(域名、IP) |
int | getPort() | 获取端口号 |
String | getFile() | 请求资源,获取资源文件名,有路径,包括参数 |
String | getPath() | 请求资源,获取资源文件路径,无参数 |
String | getQuery() | 获取参数,获取此URL的查询名 |
String | getRef() | 获取锚点 |
示例
public class demo01 {
public static void main(String[] args) throws MalformedURLException {
//创建一个URL
URL url = new URL("http://www.baidu.com:80/index.html?name=mango&age=19#a");
System.out.println("协议"+url.getProtocol());
System.out.println("域名/IP"+url.getHost());
System.out.println("端口号"+url.getPort());
System.out.println("请求资源"+url.getFile()); //获取资源文件名,包括参数
System.out.println("请求资源"+url.getPath()); //获取资源文件名
System.out.println("参数"+url.getQuery()); //获取此URL的查询部分
System.out.println("锚点"+url.getRef()); //获取锚点
}
}
TCP编程
TCP的优点
可靠,稳定 TCP的可靠体现在TCP在传递数据之前,会有三次握手来建立连接,而且在数据传递时,有确认、窗口、重传、拥塞控制机制,在数据传完后,还会断开连接用来节约系统资源。 TCP的缺点: 慢,效率低,占用系统资源高,易被攻击 TCP在传递数据之前,要先建连接,这会消耗时间,而且在数据传递时,确认机制、重传机制、拥塞控制机制等都会消耗大量的时间,而且要在每台设备上维护所有的传输连接,事实上,每个连接都会占用系统的CPU、内存等硬件资源。
Socket类
socket即套接字,由IP/www和端口号构成的
常用构造方法
Socket(String host, int port)
传入主机地址和端口号
常用重要方法
返回值类型 | 方法 | 说明 |
---|---|---|
InputStream | getInputStream() | 返回此套接字的输入流 |
OutputStream | getOutputStream() | 返回此套接字的输出流 |
void | close() | 关闭此套接字 |
void | shutdownOutput() | 禁用此套接字的输出流 |
void | shutdownInput() | 禁用此套接字的输入流 |
当然还有其他的基本方法,如获取端口号等等,不一一列举
ServerSocket类
服务器端套接字,这个类实现了服务器套接字。 服务器套接字等待通过网络进入的请求
常用构造方法
ServerSocket(int port)
指定端口创建服务端套接字对象
常用方法
返回值类型 | 方法 | 说明 |
---|---|---|
Socket | accept() | 获取客户端套接字,侦听要连接到此套接字并接受它 |
void | close() | 关闭此套接字 |
客户端向服务端发送一个请求,服务端通过accept()接收取出请求,获得一个socket对象,服务端可客户端通过操作socket的输入输出流进行通信,客户端向服务端通过socket发送数据,发送完毕后,服务端要返回客户端数据的话,客户端要主动关闭socket的输出流,不然服务端会一直监听接收来自客户端的数据
- TCP网络编程:客户端发送信息给服务端,并打印出来
public class demo01 {
public static void main(String[] args) {
new Thread(new SocketThread()).start();
new Thread(new ServerSocketThread()).start();
}
}
class SocketThread implements Runnable {
@Override
public void run() {
//创建客户端套接字对象,指定服务端的IP和端口
try(Socket socket = new Socket("127.0.0.1",3307);
//通过客户端套接字获取输出流
OutputStream os = socket.getOutputStream();
BufferedReader isr = new BufferedReader(new InputStreamReader(System.in))) {
while (true) {
String msg = isr.readLine();
//通过输出流写出信息
os.write(msg.getBytes());
if(msg.equals("exit")) {
break;
}
}
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
class ServerSocketThread implements Runnable {
@Override
public void run() {
//服务端指定端口,创建服务端套接字对象
try(ServerSocket serverSocket = new ServerSocket(3307);
//通过accept()方法获取客户端的套接字对象
Socket socket = serverSocket.accept();
//通过获取的套接字获取输入流
InputStream is = socket.getInputStream();) {
int len = -1;
//写出信息,通过套接字将信息输出到客户端
byte[] flush = new byte[1024];
while ((len = is.read(flush)) != -1) {
System.out.println("__________________");
System.out.println("| "+new String(flush,0,len));
System.out.println("| from:"+socket.getInetAddress());
System.out.println("¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯¯");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- TCP网络编程:客户端发送文件给服务端,并保存在本地,服务器并返回一个数据告诉客户端上传成功了
public class demo02 {
public static void main(String[] args) {
new Thread(new SocketThread()).start();
new Thread(new ServerSocketThread()).start();
}
}
//客户端
class SocketThread implements Runnable {
@Override
public void run() {
//创建套接字对象
try(Socket socket = new Socket("127.0.0.1",3307);
BufferedInputStream bis = new BufferedInputStream(new FileInputStream("Net/src/testfile/test.txt"));
//获取套接字的输出流
BufferedOutputStream bos = new BufferedOutputStream(socket.getOutputStream());
//获取套接字的输入流
InputStream is = socket.getInputStream();) {
//数据写出
byte[] bytes = new byte[1024];
int len = -1;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes,0,len);
}
System.out.println("传输完成");
bos.flush();
//数据写出完毕后关闭输出流,否则服务端会一直进行接收数据
socket.shutdownOutput();
//读取服务器发回的信息
len = is.read(bytes);
System.out.println(new String(bytes,0,len));
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 服务端
*/
class ServerSocketThread implements Runnable {
@Override
public void run() {
try(ServerSocket serverSocket = new ServerSocket(3307);
Socket socket = serverSocket.accept();
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("Net/src/testfile/test2.txt"));
BufferedInputStream bis = new BufferedInputStream(socket.getInputStream());
OutputStream os = socket.getOutputStream();) {
byte[] bytes = new byte[1024];
int len = -1;
while ((len = bis.read(bytes)) != -1) {
bos.write(bytes,0,len);
}
bos.flush();
//传输完成后,客户端主动关闭客户端的输出流,那么服务端的输入流就会结束,
// 否则服务端一直会接收数据不停
//返回给客户端信息
os.write("已经收到了,谢谢".getBytes());
} catch (IOException e) {
e.printStackTrace();
}
}
}
UDP编程
UDP优点
快,比TCP稍安全 UDP没有TCP的握手、确认、窗口、重传、拥塞控制等机制,系统不保证UDP数据包一定能到达,也不保证什么时候能到达,UDP是一个无状态的传输协议,所以它在传递数据时非常快。
Java使用DatagramSocket代表UDP协议的Socket,DatagramSocket本身只是码头,不维护状态,不能产生IO流,它的唯一作用就是接收和发送数据报,Java使用DatagramPacket来代表数据报,DatagramSocket接收和发送的数据都是通过DatagramPacket对象完成的。
发送端使用步骤
- 创建服务端+端口
- 准备数据
- 打包(发送的地点及端口)
- 发送资源
- 关闭资源
接收端使用步骤
-
创建服务端+端口
-
准备接收容器
-
封装成包
new DatagramPacket(byte[] b,int length)
-
接收数据,使用
DatagramSocket
的实例的receive(DatagramPacket)
方法进行接收阻塞式接收,直到接收到一个数据为止
-
分析数据
-
关闭资源
示例
- 发送端发生一句话,接收端接收并显示
public class demo01 {
public static void main(String[] args) {
new Thread(new Send()).start();
new Thread(new Receive()).start();
}
}
//发送端
class Send implements Runnable {
@Override
public void run() {
//1. 创建客户端,DatagramSocket只负责收发数据
DatagramSocket socket = null;
//2. 创建数据报,DatagramPacket来代表数据报
DatagramPacket packet = null;
//创建数据
byte[] data = "嘿嘿嘿,你好呀".getBytes();
try {
InetAddress inet = InetAddress.getLocalHost();
socket = new DatagramSocket();
//3. 打包数据,指明主机地址和端口号
packet = new DatagramPacket(data,0,data.length,inet,3307);
//4. 发送数据
socket.send(packet);
//5. 关闭数据
socket.close();
} catch (UnknownHostException e) {
e.printStackTrace();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null) {
socket.close();
}
}
}
}
//接收端
class Receive implements Runnable {
@Override
public void run() {
//1. 创建服务端,DatagramSocket只负责收发数据
DatagramSocket socket = null;
//2. 创建数据报,DatagramPacket来代表数据报
DatagramPacket packet = null;
try {
//3. 数据接收容器
byte[] data = new byte[100];
//4. 指明服务端端口
socket = new DatagramSocket(3307);
//5. 数据封装程包
packet = new DatagramPacket(data,0,data.length);
//6. 接收数据
socket.receive(packet);
//输出数据,通过getData()方法获取数据报存储的数据,getLength()获取数据的实际接收的长度
System.out.println(new String(packet.getData(),0,packet.getLength()));
//7. 关闭数据
socket.close();
} catch (SocketException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if(socket != null) {
socket.close();
}
}
}
}
反射
关于java.lang.Class类的理解
类的加载过程
程序经过javac.exe命令以后,会生一个或多个字节码文件(.class),接着我们使用java.exe命令对某个字节码文件进行解释运行,相当于将某个字节码文件记载到内存当中。此过程成为类的加载,加载到内存中的类,我们就成为运行时类,此运行时类就作为Class的一个实例。
换句话说,Class的实例就对应着一个加载到内存中的运行时类
加载到内存中的运行时类,会缓存一段时间。在此时间之内,我们可以通过不同的方式去获取此运行时类
哪些结构属于Class实例
- class普通的类,以及内部类等是类的结构的都算
- enum 枚举
- interface 接口
- annotation 注解@interface
- [] 数组
- 基本数据类型 boolean、byte、char、short、int、long、float 和 double
- void
获取Class的实例的三种方式
- 调用运行时类的属性: .class
Class c1 = Person.class;
- 通过运行时类的对象:调用getClass()
Person person = new Person();
Class c2 = person.getClass();
- 调用Class静态方法:forName(String classPath) (常用)
Class c3 = Class.forName("demo01.Person");
-
了解 通过类的加载器(了解)
通过获取demo02的类的加载器,使用类的加载器去加载Person类
ClassLoader classLoader = demo02.class.getClassLoader();
Class c4 = classLoader.loadClass("demo01.Person");
注意
System.out.println(c1 == c2); //true System.out.println(c1 == c3); //true
说明c1和c2以及c3都是指向的同一个对象,虽然获取的方式不同,但是获取的都是内存当中同一个运行时类,加载到内存中的运行时类,会缓存一段时间,在此时间之内,我们可以通过不同的方式获取这个运行时类;运行时类既然是个对象,那么就也会被JVM进行垃圾回收,在回首之前,运行时类只存在这一份。即c1c2,c1c3
注意:数组的类型和纬度一样的话,对应的运行时类也是相同的
Class aClass = new int[10].getClass(); Class bClass = new int[100].getClass(); System.out.println(aClass==bClass); //true
使用类的加载器加载配置文件
/**
* Properties类表示一组持久的属性。
* Properties可以保存到流中或从流中加载。
* 属性列表中的每个键及其对应的值都是一个字符串
*
* 使用单元测试方法@Test,默认是在模块内的,如src/jdbc.properties
*/
public class demo03ClassLoad {
@Test
public void test1() throws IOException {
//读取配置文件的方式1,使用IO流加载配置文件
//1. 配置文件源
InputStream is1 = new FileInputStream("src/jdbc.properties");
//2. 载入,从输入字节流读取属性列表(键和元素对)
Properties properties = new Properties();
properties.load(is1);
//3. 获取属性,填入需要获取的字段
String user1 = (String)properties.get("user");
String pwd1 = (String)properties.get("pwd");
System.out.println("user= "+user1+"pwd= "+pwd1);
//读取配置文件的方式2,通过类的加载器加载配置文件
//1. 获取类的加载器
ClassLoader classLoader = demo03ClassLoad.class.getClassLoader();
//2. 创建配置文件的输入流,并通过properties载入
InputStream is2 = classLoader.getResourceAsStream("jdbc.properties");
properties.load(is2);
//3. 获取属性,填入需要获取的字段
String user2 = (String)properties.get("user");
String pwd2 = (String)properties.get("pwd");
System.out.println("user= "+user1+"pwd= "+pwd1);
}
}
获取运行时类的属性结构
Class类
getFields() 获取当前运行时类及其父类中声明为public的属性,返回Field[]数组
getDeclaredFields() 方法仅获取当前运行时类中所有属性结构,返回Field[]数组
Field类,此类是描述运行时类中的属性字段,Field类提供了一些方法,不限于以下方法
getName() 返回名称
getType() 返回类型
getModifiers() 返回权限修饰修饰,返回值类型为int型,不同的数组代表了不同的权限,值定义在Modifier类中
获取运行时类的属性结构
//伪代码
@Test
public void test1() {
//首先获取Class对象
Class clazz = Student.class;
//getFields()方法获取当前运行时类及其父类中声明为public的属性
Field[] fs = clazz.getFields();
for (Field field : fs) {
System.out.println(field);
}
System.out.println();
//getDeclaredFields()方法仅获取当前运行时类中所有属性结构
Field[] dfs = clazz.getDeclaredFields();
for (Field df : dfs) {
System.out.println(df);
}
}
运行时类的属性的具体结构
权限修饰符、变量类型、变量名(变量值,非静态的变量值还是拿不到的)
//伪代码
@Test
public void test2() {
//首先获取Class对象
Class clazz = Student.class
Field[] dfs = clazz.getDeclaredFields();
for (Field df : dfs) {
//权限修饰符
//返回的是int型数字,不同数字代表不同类型,值定义在Modifier类中,
//可以通过Modifier类中的toString()方法获取值所代表不不同修饰符字符串
//注意,默认权限值为0,字符串返回的为空字符串
int modifier = df.getModifiers();
//变量名
String name = df.getName();
//变量类型
Class type = df.getType();
System.out.println("修饰符 " + modifier + "-" + Modifier.toString(modifier));
System.out.println("变量名 " + name);
System.out.println("类型 " + type);
System.out.println();
}
}
- 自定义类
Student extends Person implements Runnable
获取运行时类的属性结构 中使用
Person类
public class Person<T> {
public String name; //公共权限
int age; //默认权限
private boolean sex; //私有权限
public Person() {
}
@MyAnnotation("Person-constructor")
public Person(String name, int age, boolean sex) {
this.name = name;
this.age = age;
this.sex = sex;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
//公共方法
@MyAnnotation("Person-Method")
public void show() {
System.out.println("你好,我叫" + this.name);
}
//私有方法
private void nation() {
System.out.println("我是中国人");
}
//默认权限
void sayHell() {
System.out.println("你好,hello");
}
}
Student类
@MyAnnotation("Student-class")
class Student extends Person<String> implements Runnable, MyInterface {
public int score; //公共权限 分数
private int weight; //私有权限 体重
int height; //默认权限 身高
public void run() {
}
@MyAnnotation("Student-method")
public void showMe(String hobby, int year) throws Exception {
System.out.println("你好,我的兴趣是" + hobby + "兴趣有" + year + "年了");
}
@MyAnnotation("Student-constructor")
public Student(String name, int age, boolean sex,
int score, int weight, int height) {
super(name, age, sex);
this.score = score;
this.weight = weight;
this.height = height;
}
private Student() {
super();
}
}
注解
@Target({TYPE,FIELD,METHOD,PARAMETER,CONSTRUCTOR,LOCAL_VARIABLE})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
String value() default "hello";
}
接口
public interface MyInterface {
}
获取当前运行时类中的方法
Class类中
Method[] getMethods():获取当前运行时类及其父类的声明为public的方法 Method[] getDeclaredMethods():获取当前运行时类的所有方法(不包含父类)
Method类中
getAnnotations():返回此元素上的所有注解 getModifiers():返回权限修饰符,使用int值表达 值定义在Modifier类中,可以使用Modifier类中的toString()方法转换 Class getReturnType():获取方法的返回值类型,注意,返回值类型也是Class实例对象 getName() Class[] getParameterTypes():获取方法的参数列表的类型,注意,类型也是Class实例对象
获取当前运行时类中的方法
@Test
public void test1() {
//先获取运行时类的对象
Class clazz = Student.class;
//getMethods():获取当前运行时类及其父类的声明为public的方法
Method[] methods = clazz.getMethods();
for (Method m : methods) {
System.out.println(m);
}
//getDeclaredMethods():获取当前运行时类的所有方法(不包含父类)
Method[] declaredMethods = clazz.getDeclaredMethods();
for (Method m : declaredMethods) {
System.out.println(m);
}
}
获取运行时类中的方法的具体结构
获取运行时类中的方法的具体结构 展开查看代码//方法的结构
* @Xxx
* 权限修饰符 返回值类型 方法名(参数类型 形参名,...) throws XxxException {}
@Test
public void test2() {
Class clazz = Student.class;
Method[] methods = clazz.getDeclaredMethods();
for (Method m : methods) {
//获取注解
Annotation[] annos = m.getAnnotations();
for (Annotation anno : annos) {
System.out.println("注解:" + anno);
}
//获取权限修饰符
int modifiers = m.getModifiers();
System.out.println("权限修饰符:"+Modifier.toString(modifiers));
//返回值类型,注意返回值类型也是Class实例
Class returnType = m.getReturnType();
System.out.println("返回值类型:"+returnType);
//获取方法名
String name = m.getName();
System.out.println("方法名:"+name);
//获取参数列表(参数的类型),注意类型也是Class实例
Class[] parameterTypes = m.getParameterTypes();
for (Class parameterType : parameterTypes) {
System.out.println("参数:" + parameterType);
}
//获取异常类型
//返回一个 Class 对象数组,该数组表示由该方法对象抛出的异常对象
//注意,异常在编写的时候也是class类,所以返回的类型也是Class,异常类的运行时对象
Class[] exceptionTypes = m.getExceptionTypes();
for (Class exceptionType : exceptionTypes) {
System.out.println("异常"+exceptionType);
}
System.out.println();
}
}
获取运行时类中的其他结构
获取运行时类中的构造器
@Test public void test1() { Class clazz = Student.class; //获取构造器,仅限于本类中声明为public的构造器 Constructor[] constructors = clazz.getConstructors(); for (Constructor c : constructors) { System.out.println(c); } System.out.println(); //获取构造器,获取当前运行时类中声明的所有构造器 Constructor[] DeclaredConstructors = clazz.getDeclaredConstructors(); for (Constructor c : DeclaredConstructors) { System.out.println(c); } }
获取运行时类的父类
@Test public void test2() { Class clazz = Student.class; Class superclass = clazz.getSuperclass(); System.out.println(superclass); }
获取运行时类的父类(带泛型)
@Test public void test3() { Class clazz = Student.class; Type genericSuperclass = clazz.getGenericSuperclass(); System.out.println(genericSuperclass); }
获取运行时类的父类的泛型
@Test public void test4() { Class clazz = Student.class; Type genericSuperclass = clazz.getGenericSuperclass(); //Type中已带泛型,强转一下,ParameterizedType(参数化类型) ParameterizedType parameterizedType = (ParameterizedType)genericSuperclass; //获取泛型的类型 Type[] arguments = parameterizedType.getActualTypeArguments(); for (Type a : arguments) { System.out.println(a); } }
获取运行时类实现的接口
@Test public void test5() { Class clazz = Student.class; Class[] interfaces = clazz.getInterfaces(); for (Class a : interfaces) { System.out.println(a); } }
获取运行时类所在的包
@Test public void test6() { Class clazz = Student.class; Package pack = clazz.getPackage(); System.out.println(pack); }
获取运行时类声明的注解
@Test public void test7() { Class clazz = Student.class; Annotation[] annotations = clazz.getAnnotations(); for (Annotation a : annotations) { System.out.println(a); } }
调用操作运行时类中的指定属性Field
步骤
获取Class对象 Class clazz = Student.class;
如果是非静态属性需要依托于对象,需要创建对象 clazz.newInstance();
获取Class对象的指定的属性 clazz.getField(“score”);
取消访问检查 如果是非public的属性,需要先取消访问检查 setAccessible(true),true为取消访问检查
调用操作属性
示例
- 如果需要对私有属性进行修改,那么就要先对属性设置访问检查
- setAccessible(true),true为取消访问检查
@Test
public void testField2() throws Exception {
Class clazz = Student.class;
//获取指定结构score分数
//getDeclaredField()获取本运行时类中的所有属性结构
Field weight = clazz.getDeclaredField("weight");
//非静态属性依托于对象,所以要有一个对象,通过反射创建一个对象
Student stu = (Student) clazz.newInstance();
//因为weight属性是私有的,默认是不能够修改的,所以如果需要修改,需要先设置访问检查,true为取消访问检查
weight.setAccessible(true);
//设置score属性,对象是stu,值为88
//第一个参数为,指明是哪个对象的,第二个参数是设置的值
weight.set(stu,101);
//获取score属性值
//指明对象
int stuWeight = (int)weight.get(stu);
System.out.println(stuWeight);
}
获取使用运行时类中的方法Method
步骤
获取Class对象 Class clazz = Student.class;
如果是非静态方法需要依托于对象,需要创建对象 clazz.newInstance();
获取Class对象的指定的方法 clazz.
getDeclaredMethod
(“showMe”,String.class,int.class);取消访问检查 如果是非public的方法,需要先取消访问检查 setAccessible(true),true为取消访问检查
调用操作方法
调用使用非静态方法
@Test
public void test1() throws Exception {
Class clazz = Student.class;
/**
* 获取指定的方法
* 参数1,指明方法名
* 参数2,形参列表,因为可能有很多同名的方法
*/
Method showMe = clazz.getDeclaredMethod("showMe",String.class,int.class);
//取消访问检查
showMe.setAccessible(true);
//创建对象
Student stu = (Student)clazz.newInstance();
/**
* invoke()调用方法
* 参数1 调用的对象,参数2 参数列表
* invoke()方法的返回值,即为调用对应的方法的返回值
*/
Object returnValue = showMe.invoke(stu, "乒乓球", 5);
System.out.println(returnValue);
}
调用使用静态方法
@Test
public void test2() throws Exception {
Class clazz = Student.class;
Method showDesc = clazz.getDeclaredMethod("showDesc");
showDesc.setAccessible(true);
//调用静态方法,此时调用的对象就是运行时类的实例
showDesc.invoke(Student.class); //invoke(clazz)
//此时调用对象填null也是可以的,因为showDesc是通过clazz对象拿到的,
// 而且是静态的,所以直接就知道用谁去调用了,非静态方法才需要指明正确的调用对象
showDesc.invoke(null);
}
获取使用运行时类指定的构造器
获取当前运行时类的指定构造器clazz.getDeclaredConstructor(String.class);
@Test
public void test1() throws Exception {
Class clazz = Student.class;
//获取构造器,指明参数列表的类型
//getDeclaredConstructor()获取本运行时类中所有的构造器
Constructor constructor = clazz.getDeclaredConstructor(String.class);
//设置可访问,取消访问检查
constructor.setAccessible(true);
//调用此构造器的newInstance()方法创建对象
Student stu = (Student) constructor.newInstance("小明");
System.out.println(stu);
}
创建运行时类的对象
方式1
通过Class.newInstance()方法调用无参构造器创建对象
/**
* 创建运行时类的对象
*
* 通过反射,创建对应的运行时类的对象
*/
public class demo01NewInstances {
@Test //非法访问异常 //实例化异常
public void test1() throws IllegalAccessException, InstantiationException {
//获取Class对象
Class clazz = Person.class;
/**
* newInstance(),调用此方法,获取对应的运行时类的对象
* 此方法内部使用了无参构造器
* 要想此方法正常的创建运行时类的对象,要求:
* 1. IllegalAccessException,无参构造器权限要足够
* 2. InstantiationException,必须有无参构造器
*
* 在JavaBean中要求,类提供一个public的午餐构造器:
* 原因: 1. 便于通反射,创建运行时类的对象
* 2. 便于子类继承此运行时父类时,默认调用super()时,要保证有此构造器
*/
Person obj = (Person)clazz.newInstance();
}
}
方式2
获取运行时类的构造器,通过调用构造器的newInstance()方法创建对象
public class demo03UseConstructor {
@Test
public void test1() throws Exception {
Class clazz = Student.class;
//获取构造器,指明参数列表的类型
//getDeclaredConstructor()获取本运行时类中所有构造器(指定的其中一种)
Constructor constructor = clazz.getDeclaredConstructor(String.class);
//设置可访问,取消访问检查
constructor.setAccessible(true);
//调用此构造器的newInstance()方法创建对象
Student stu = (Student) constructor.newInstance("小明");
System.out.println(stu);
}
}
总结
反射机制能提供的功能
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时获取泛型信息
- 在运行时调用任意一个类的成员变量和方法
- 在运行时处理注解生成动态代理
相关API
-
java.lang.Class 源头
-
java.lang.reflect.Method
-
java.lang.reflect.Field
-
java.lang.reflect.Constructor