JavSE笔记
编者的话
这个笔记是本人在重新学习javaSE后,大部分是在bilibili动力结点杜老师的视频上学习,本笔记中大量引用整合了其中的笔记,截图,代码等等非常感谢,感觉收获颇多,但是收获的知识点又太过分散,且担心忘记这些来之不易的精华,特做此总结,因为是个人笔记,难免会有一些这样那样的问题,如果读者发现错误或者影响到您正常的知识架构,望正在读此的您见谅!
————————tyong
文章目录
- JavSE笔记
- 编者的话
- 数组
- 常用类
- 异常
- 集合
- IO流
- 线程
- 反射
- 注解
- 注解
数组
须知
- Java语言中,数组一种引用数据类型,不属于基本数据类型,其父类是Object;
- 数组实际上是一个容器,可以容纳多个元素;
- 数组中可以存储基本类型的数据,也可以存储引用类型的数据;
- 数组因为是引用类型,所以数组对象是堆内存当中的(数组是存储在堆当中的);
- 数组在内存中方面是什么样的图形?[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZQFxnvxs-1619533734421)(javaSE学习笔记.assets/image-20210314220748986.png)]
因此可以看出,数组当中如果存储的是**“Java对象”的话,实际上存储的是“对象的引用(内存地址)”**;
- Java中规定,数组一旦创建长度不可变;
- 数组的分类,包括一维数组、二维数组、多维数组、…(实际开发中一维数组较多,二维数组偶尔使用,其他基本不用);
- 所有的数组对象都有length属性注意是属性,用来获取数组中元素的个数;
- Java中的数组要求数组内的元素必须统一,如int类型的数组只能存储int,String类型的数组只能存储String,…;
- 数组在内存方面存储时,数组中的元素内存地址是连续的元素都是有规则的紧挨着排列的,这是数组存储元素的特点;
- 数组实际上是一种简单的数据结构;
- 数组都是使用第一个元素首元素的内存地址作为数组对象的内存地址;
- 数组中每一个元素都是有下标的,从0开始,以1递增,最后一个元素的下标是
数组对象.length - 1
;
优点和缺点
优点:查找某个下标上的元素时,效率极高,可以说是查询效率最高的数据结构
**为什么?**因为:[1].每个元素的内存地址在空间上是连续的;[2].每个元素类型相同,所有占用空间大小一样;[3].此时,知道第一个元素内存地址,知道每个元素占用内存空间的大小,知道下标,所以可以直接通过数学表达式计算定位出该元素的位置,所以数组的检索效率高;
引出一个问题,数组中存储100个元素,或者存储100万个元素,在元素查询/检索方面效率是相同的,因为数组在查找元素时不是一个一个的找,而是通过数学表达式计算出来直接定位的;
缺点:[1].因为数组中每个元素的内存地址连续,所以在数组上随机删除或者增加元素时,效率较低,因为这两种操作会改变数组的结构,使数组内的元素会进行统一的向前或向后位移操作;[2].数组不能存储大数据量,因为很难在内存空间上找到一块很大的连续内存空间
注意:数组对最后一个元素进行删除或者添加时,效率是没有任何影响的,因为其不会涉及其他元素的位移;
代码演示与详解
声明数组
语法格式:
int[] array1;
double[] array2;
String[] array3;
Object[] array4;
int[][] array5;
初始化数组:
// 方式1:静态初始化一维数组
// int[] array = new int[]{1, 5, 8, 1, 9, 7}
int[] array1 = {1, 5, 9, 0, 6}; // 以上可以通过编译器的自动类型推换,所有可以省略如此
// 方式2:动态初始化一维数组
String[] array2 = new String[6];/*注意:这里的6表示初始化6个长度的String类型数组,每个元素默认为null*/
int[] array3 = new int[4]; /*注意:这里的4表示初始化4个长度的int类型数组,每个元素默认为0*/
Object[][] array4 = new Object[4][6];
可以看出,在动态初始化数组时,数组中元素的默认值会根据声明的数据类型的默认值创建,基本数据类型的数组在动态初始化时按照基本数据类型进行默认取值;8种基本数据类型和引用数据类型的默认值如下表所示:
数据类型 | 默认值 |
---|---|
byte | 0 |
short | 0 |
int | 0 |
long | 0L |
float | 0.0F |
double | 0.0 |
boolean | false |
char | \u0000 |
引用数据类型 | null |
引出:对于以上介绍的两种初始化数组的方式,我们如何选择?
当创建数组时,确定数组存储哪些具体的元素,采用静态初始化方式;当创建数组时,不确定将来数组存储的具体数据,可以采用动态初始化方式预先分配内存空间;
数组作为参数
// 1.1、可以使用如下这种方式将静态初始化的数组传递给方法的参数
int[] array1 = {1, 2, 5, 1};
getArray(array1);
// 1.2、也可以使用如下方式将静态初始化的数组将传递给方法的参数
getArray(int[]{1, 5, 8, 1});
// 2.1、可以传入动态初始化的数组
int[] array2 = new int[6];
getArray(array2);
// 方法的参数是数组
public static void getArray(int[] arrays) {...}
main方法的String[] args 参数
public static void main(String[] args) {
System.out.println("args参数的长度:" + args.length); // 会发现输出为0
/*因此可以得出结论:1、args不是null(因为可以调用属性[.length]);2、args长度为0;*/
}
分析:JVM负责调用main方法,JVM调用main方法时,会自动传入一个String类型的数组过来,长度默认为0,且不是null,这个数组是留给用户的,用户可以通过控制台输入参数,这个参数会自动被转换为"String[] args"
,如:运行这样的程序:java ArrayTest abc def ghi
,此时JVM会自动将abc
、def
、ghi
通过空格进行分离,自动放到String[] args
数组中,即String[] args = {"abc", "def", "ghi"}
,所以,String[] args
这个数组主要是用来接收用户输入的参数的;
示例代码:
package edu.hebeu.resources;
/**
* 这个例子展示main方法的 String[] args 参数的使用
* @author tyong
*
*/
public class MainMethodOfStringArrayArgs {
public static void main(String[] args) {
if(args.length != 2) {
System.err.println("请输入用户名和密码,如:root tyong");
return;
}
if("root".equals(args[0]) & "tyong".equals(args[1])) System.out.println("身份验证成功!欢迎您:username = " + args[0] + ", password = " + args[1]);
else System.out.println("身份验证失败!");
}
}
对于上述的代码,我们直接使用run只会进入到 args.length != 2
的条件,我们可以使用编译器的run configuration选项为这个 String[] args
参数去赋值以空格作为参数之间的间隔,才能正确的验证身份;也可以在控制台使用 java root tyong MainMethodOfStringArrayArgs
去正确的验证身份;
数组的扩容
关于一维数组的扩容问题,在Java中,数组长度一旦确定就不可改变,当数组满了只后,就要进行扩容;
Java中对数组的扩容这样要求:先创建一个大容量数组,然后将小容量数组的数据一个一个拷贝到当前大数组当中;
结论:数组扩容效率较低,因为涉及到数组拷贝,所以在以后开发中要尽可能少的进行数组的拷贝,可以在创建数组对象时估计一下多长合适,以减少数组的扩容次数,提高效率;
数组的拷贝
实例代码
int[] srcArray = {1, 5, 1, 8, 9}; // 拷贝源数组
int[] destArray = new int[10]; // 拷贝目标数组
/*进行数组的拷贝,
* 参数1:拷贝源数组
* 参数2:从原数组的第几个下标开始拷贝
* 参数3:拷贝目标数组
* 参数4:将拷贝元素拷贝至从目标数组的第几个下标
* 参数5:拷贝的长度*/
System.arraycopy(srcArray, 2, destArray, 4, 2);
// 遍历目标数组查看结果为:0, 0, 0, 0, 1, 8, 0, 0, 0, 0,
for(int i = 0; i < destArray.length; i++) {
System.out.print(destArray[i] + ", ");
}
图示:
注意:当数组内存储的元素是引用数据类型时,进行数组的拷贝,不会创建一个新的引用对象,而是将该引用对象的内存地址拷贝至目标数组,如下图所示:
关于二维数组和其他的多维数组
- 二维数组其实是一个特殊的一维数组,特殊在这个一维数组当中的每个元素是一个一维数组;
- 三维数组其实是一个特殊的二维数组,特殊在这个二维数组的每个元素是一个一维数组;
- …
小项目
数组模拟栈数据结构
示例代码:
package edu.hebeu.java;
/**
* 用数组模拟一个栈,能通过push()方法实现压栈、pop()方法实现弹栈
* 栈的默认长度为10
*/
public class MyStack {
private Object[] stack; // 栈
private int stackFrame; // 栈帧
/**
* 构造方法,默认栈长度为10,栈帧为-1
*/
public MyStack() {
this.stack = new Object[10];
this.stackFrame = -1;
}
/**
* push方法,进行压栈
* @param stackElement
*/
public void push(Object stackElement) {
if(this.stackFrame >= this.stack.length - 1) { // 如果当前栈帧大于当前栈的数组长度减一,即栈满,不可进行压栈操作
System.out.println("压栈失败!当前栈已满");
return;
}
/**程序执行到此,表示可以压栈*/
this.stackFrame++; // 当前栈帧++
this.stack[this.stackFrame] = stackElement; // 压栈
System.out.println("压栈成功,压入" + stackElement + "元素,当前栈帧为" + this.stackFrame);
}
/**
* pop方法,进行弹栈
* @return
*/
public Object pop() {
if(this.stackFrame < 0) {
System.out.println("弹栈失败!当前栈空");
return null;
}
/**程序执行到此,表示可以弹栈*/
System.out.print("弹出栈帧为" + this.stackFrame + "的" + this.stack[this.stackFrame] + "栈元素");
this.stackFrame--;
System.out.println("弹出成功,当前栈为" + this.stackFrame);
return this.stack[this.stackFrame + 1]; // 返回出弹出的栈元素
}
public Object[] getStack() {
return stack;
}
public void setStack(Object[] stack) {
this.stack = stack;
}
public int getStackFrame() {
return stackFrame;
}
public void setStackFrame(int stackFrame) {
this.stackFrame = stackFrame;
}
}
测试代码:
package edu.hebeu.java;
public class MyStackTest {
public static void main(String[] args) {
MyStack myStack = new MyStack();
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
myStack.push(new Object());
Object obj = new Object();
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
obj = myStack.pop();System.out.println(obj);
}
}
酒店管理系统
示例代码:
房间类的创建:
package edu.hebeu.entity;
/**
* 房间类
*/
public class Room {
private int code; // 房间编号
private boolean isFree; // 空闲与否
private String type; // 房间类型
public Room() {
super();
}
public Room(int code, boolean isFree, String type) {
super();
this.code = code;
this.isFree = isFree;
this.type = type;
}
@Override
public boolean equals(Object obj) {
if(obj == null || !(obj instanceof Room)) return false; // 如果传入的obj为空或者obj的类型不时room,则返回false
if(obj == this) return true; // 如果传入的obj与this的地址码相同,即调用equals方法比较的是同一个
/**程序到此,表示传入的obj不为空且obj是Room类型*/
Room room = (Room)obj; // 把传入的obj向下转型为Room
return room.code == this.getCode(); // 当传入的obj的code与当前Room对象的code相同,返回true,表示同一个房间;反之,返回flase,表示不为同一个房间;
}
@Override
public String toString() {
return "Room [code=" + code + ", isFree=" + (isFree ? "空闲" : "占用") + ", type=" + type + "]";
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public boolean isFree() {
return isFree;
}
public void setFree(boolean isFree) {
this.isFree = isFree;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
}
酒店类的创建
package edu.hebeu.entity;
import java.util.Arrays;
/**
* 酒店类
*/
public class Hotel {
private Room[][] rooms; // 所有的房间
public Hotel(Room[][] rooms) {
super();
this.rooms = rooms;
}
public Hotel() {
rooms = new Room[5][10]; // 创建一个4层,每层10个房间的酒店
for(int floor = 0; floor < rooms.length; floor++) { // 循环遍历每一层
for(int room = 0; room < rooms[floor].length; room++) { // 循环每一层的房间
if(floor + 1 == 1 || floor + 1 == 5) { // 第一层和第五层表示VIP
rooms[floor][room] = new Room((floor + 1) * 100 + room + 1, true, "VIP"); // 给每一个房间创建一个Room对象
} else if(floor + 1 == 2) { // 第二层表示双人间
rooms[floor][room] = new Room((floor + 1) * 100 + room + 1, true, "双人间"); // 给每一个房间创建一个Room对象
} else { // 其他层次,三、四表示单人间
rooms[floor][room] = new Room((floor + 1) * 100 + room + 1, true, "单人间"); // 给每一个房间创建一个Room对象
}
}
}
}
/**
* 展示所有的房间
*/
public void showAllRooms() {
for(int floor = 0; floor < rooms.length; floor++) {
for(int room = 0; room < rooms[floor].length; room++) {
System.out.println(rooms[floor][room]);
}
}
}
/**
* 订房
* @param code
*/
public void orderRoom(int code) {
rooms[code / 100 - 1][code % 100 - 1].setFree(false);
System.out.println("订房成功!!!");
}
/**
* 退房
* @param code
*/
public void returnRoom(int code) {
rooms[code / 100 - 1][code % 100 - 1].setFree(true);
System.out.println("退房成功!!!");
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + Arrays.deepHashCode(rooms);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
Hotel other = (Hotel) obj;
if (!Arrays.deepEquals(rooms, other.rooms))
return false;
return true;
}
@Override
public String toString() {
return "Hotel [rooms=" + Arrays.toString(rooms) + "]";
}
public Room[][] getRooms() {
return rooms;
}
public void setRooms(Room[][] rooms) {
this.rooms = rooms;
}
}
测试代码:
package edu.hebeu.test;
import java.util.Scanner;
import edu.hebeu.entity.Hotel;
public class HotelTest {
@SuppressWarnings("resource")
public static void main (String[] args) {
Hotel hotel = new Hotel();
System.out.println("欢迎使用订房系统,[1]、展示所有房间,[2]、订房,[3]、退房,[4]、退出程序");
Scanner scanner = new Scanner(System.in);
while(true) {
System.out.println("请输入功能编号:"); int i = scanner.nextInt();
if(i == 1) {
hotel.showAllRooms();
} else if(i == 2) {
System.out.println("请输入订的房间号"); int code = scanner.nextInt();
hotel.orderRoom(code);
} else if(i == 3) {
System.out.println("请输入退的房间号"); int code = scanner.nextInt();
hotel.returnRoom(code);
} else if(i == 0) {
System.out.println("拜拜,欢迎下次再来!!!"); break;
} else {
System.out.println("输入有误,请重新输入功能编号");
}
}
}
}
引出算法
冒泡排序
/**
* 冒泡排序:每次使用当前的数据a与当前数据的后一个数据b比较,如果a > b, 则交换;反之,不交换
* 第一次循环下来,最右边的值就是本数组内的最大值,在进行之后的循环,比较其他的数据,
* 如此将大的数以此排放至右侧,实现从左到右的从小到大数据排列
*
* 如,原始数据为:{1, 5, 8, 0, 99, 52, 31}
* 第一次循环:
* 第一次比较:"1", "5", 8, 0, 99, 52, 31 (1 < 5, 不交换)
* 第二次比较:1, "5", "8", 0, 99, 52, 31 (5 < 8, 不交换)
* 第三次比较:1, 5, "8", "0", 99, 52, 31 (8 > 0, 交换)
* 第四次比较:1, 5, 0, "8", "99", 52, 31 (8 < 99, 不交换)
* 第五次比较:1, 5, 0, 8, "99", "52", 31 (99 > 52, 交换)
* 第六次比较:1, 5, 0, 8, 52, "99", "31" (99 > 31, 交换)
* 第一次循环比较的结果:{1, 5, 0, 8, 52, 31, 99}
*
* 此时待比较的数据:{1, 5, 0, 8, 52, 31}
* 第二次循环:
* 第一次比较:"1", "5", 0, 8, 52, 31 (1 < 5, 不交换)
* 第二次比较:1, "5", "0", 8, 52, 31 (5 > 0, 交换)
* 第三次比较:1, 0, "5", "8", 52, 31 (5 < 8, 不交换)
* 第四次比较:1, 0, 5, "8", "52", 31 (8 < 52, 不交换)
* 第五次比较:1, 0, 5, 8, "52", "31" (52 > 31, 交换)
* 第二次循环比较的结果:{1, 0, 5, 8, 31, 52}
*
* 此时待比较的数据:{1, 0, 5, 8, 31}
* 第三次循环:
* 第一次比较:"1", "0", 5, 8, 31 (1 > 0, 交换)
* 第二次比较:0, "1", "5", 8, 31 (1 < 5, 不交换)
* 第三次比较:0, 1, "5", "8", 31 (5 < 8, 不交换)
* 第四次比较:0, 1, 5, "8", "31" (8 < 31, 不交换)
* 第三次循环比较的结果:{0, 1, 5, 8, 31}
*
* 此时待比较的数据:{0, 1, 5, 8}
* 第四次循环:
* 第一次比较:"0", "1", 5, 8 (0 < 1, 不交换)
* 第二次比较:"0", "1", 5, 8 (0 < 1, 不交换)
* 第三次比较:"0", "1", 5, 8 (0 < 1, 不交换)
* 第四次循环比较的结果:{0, 1, 5, 8}
*
* 此时待比较的数据:{0, 1, 5}
* 第五次循环:
* 第一次比较:"0", "1", 5 (0 < 1, 不交换)
* 第二次比较:0, "1", "5" (1 < 5, 不交换)
* 第五次循环比较多结果:{0, 1, 5}
*
* 此时待比较的数据:{0, 1}
* 第六次循环:
* 第一次比较:"0", "1" (0 < 1, 不交换)
* 第六次循环比较的结果:{0, 1}
*
* 综上分析:冒泡排序算法共进行 (数组的长度 - 1) 次循环,每次循环共进行 (数组长度 - 当前循环的次数) 次比较
*
* 最终排序结果:{0, 1, 5, 8, 31, 52, 99}
* @param array
*/
public static void bubblingSort(int[] array) {
for(int i = array.length - 1; i > 0; i--) { // 进行 (数组长度 - 1) 次循环
for(int j = 0; j < i; j++) { // 当前循环进行 (数组长度 - 当前循环次数) 次比较
int temp = 0;
if(array[j] > array[j + 1]) { // 如果当前的元素比其后一个元素大
/*进行交换*/
temp = array[j + 1];
array[j + 1] = array[j];
array[j] = temp;
}
}
}
}
选择排序
/**
* 选择排序:每次从待排序的数据中找到值最小的数据,将此数据与待排序数据的最左边数据进行交换,直至左边的数据都经过比较
* 相比于冒泡排序,选择排序的效率更高,高在交换次数上,因为选择排序每次的交换都是有意义的
*
* 如:原始数据为:{0, 5, 88, 3, 6, 7}
* 第一次循环:"0", 5, 88, 3, 6, 7 (最左边的数据为0,其下标为0;最小数据为0,其下标为0,进行交换)
* 第一次循环结果:{0, 5, 88, 3, 6, 7}
*
* 此时待排序的数据为:{5, 88, 3, 6, 7}
* 第二次循环:5, 88, "3", 6, 7 (最左边数据为5,其下标为1;最小数据为3,其下标为3,进行交换)
* 第二次循环结果:{3, 88, 5, 6, 7}
*
* 此时待排序数据为:{88, 5, 6, 7}
* 第三次循环:88, "5", 6, 7 (最左边数据为88,其小标为2;最小数据为5,其小标为3,进行交换)
* 第三次循环结果:{5, 88, 6, 7}
*
* 此时待排序数据为:{88, 6, 7}
* 第四次循环:88, "6", 7 (最左边数据为88,其下标为3;最小数据为6,其下标为4,进行交换)
* 第四次循环结果:{6, 88, 7}
*
* 此时待排序数据为:{88, 7}
* 第五次循环:88, "7" (最左边数据为88,其下标为4;最小数据为7,其下标为5,进行交换)
* 第五次循环结果:{7, 88}
*
* 最终结果,从左至右从小到大为:{0, 3, 5, 6, 7, 88}
*
* 分析:待排序区数据的最左边数据的下标可以通过for循环取到,
*
* @param array
*/
public static void selectSort(int[] array) {
for(int i = 0; i < array.length - 1; i++) { // 获取待排序区最左边的下标
int minSubScript = i; // 待排序区的最小值的下标,猜想为当前待排序数据的最左边下标为i(即猜想待排序区最左边下标值就是待排序区元素最小值的下标)
for(int j = i + 1; j < array.length; j++) { // 遍历待排序区除最左边数据的其他数据,用来选出最小值的下标
// System.out.println("array[j]=" + array[j]);
// System.out.println("array[minSubScript]=" + array[minSubScript]);
if(array[j] < array[minSubScript]) { // 如果有元素小于最初猜想的最左边的数据,即猜想失败,将最小下标修改为此元素
minSubScript = j;
}
}
// System.out.println("最左边下标=" + i);
// System.out.println("minSubScript=" + minSubScript);
/**实现待排序区的最小值与待排序区的最左边值进行交换*/
if(minSubScript != i) { // 如果最小值的下标不等于最初猜想的最左边元素i,则将最左边元素i与最小元素minSubScript进行交换
int temp = 0;
temp = array[minSubScript];
array[minSubScript] = array[i];
array[i] = temp;
}
}
}
二分查找
/**
* 二分查找(折半查找):在数组是有序的前提下(本方法是升序),通过 中间下标的值 与 待查询的元素值 进行比较,将 待查询的数组 一分为二,
*
* 如果 中间元素的值 > 待查询元素值,则表示 待查询的元素在中间元素的左边,
* 开始下标不变,结束下标变成中间下标 - 1,中间下标变成(开始下标 + 结束下标) / 2
* 此时应该对左边的数组采用上述的方法;
* 如果 中间元素的值 < 待查询元素值,则表示 待查询的元素在中间元素的右边,
* 开始下标变成中间下标 + 1,结束下标不变,中间下标变成(开始下标 + 结束下标) / 2
* 此时应该对右边的数组采用上述的方法;
*
* 分析:定义一个startSubScript开始下标初始为0,endSubScript结束下标初始为数组的长度 - 1,中间下标midSubScript初始为(开始下标 + 结束下标) / 2
* @param array 待查询的数组
* @param dest 待查询值
* @return 返回查询出的下标,如果 >= 0, 则表示找到了此元素的下标;如果 == -1, 表示未找到此元素
*/
public static int dichotomySelect(int[] array, int dest) {
int startSubScript = 0; // 开始下标
int endSubScript = array.length - 1; // 结束下标
while(startSubScript <= endSubScript) { // 如果开始下标小于或者等于结束下标,即表示数组已经切分完了,此时就结束循环也就代表了没有找到要查询的元素
int midSubScript = (startSubScript + endSubScript) / 2; // 中间下标
if(array[midSubScript] == dest) { // 如果中间下标的值等于要查找的元素值,即找到了此元素
return midSubScript; // 返回此下标
} else if(array[midSubScript] > dest) { // 如果中间下标的值大于要查找的元素值,即要查找的元素在中间下标的左边
// System.out.println("在左边");
endSubScript = midSubScript - 1; // 将结束下标变成中间下标 - 1
} else if(array[midSubScript] < dest) { // 如果中间下标的值小于要查找的元素值,即要查找的元素在中间下标的右边
// System.out.println("在右边");
startSubScript = midSubScript + 1; // 将开始下标变成中间下标 + 1
}
}
/**程序到此表示未找到要查找的元素*/
return -1;
}
常用类
String
- String表示字符串类型,属于引用数据类型,不是基本数据类型;
- 在Java中随便使用双引号括起来的都是String对象,如:“abc”、“def”、“你好”、“好”、…;
- 在Java中规定,双引号括起来的字符串是不可改变的,也就是说"abc",自出生到最终死亡都是不可变的;
- 在JDK当中,双引号括起来的字符串,如"abf"、“年”、“String”、…等都是直接存储在方法区的字符串常量池中;**为什么SUN公司将字符串放到方法区的字符串常量池中?**因为字符串在开发中使用非常频繁,这样做是为了提高执行效率!
代码演示与详解
String的存储原理
前言:int i = 1; String s = "abc;"
对上面的程序而言,因为i是int基本数据类型的变量,所以内部直接保存1这个值,二s是String类型引用数据类型的变量,所以内部保存这个值的内存地址;
两种创建String类型的对象的方式,分析下面代码,得出如下图所示的内存图:
/* 1、如下面的两行代码,实际上在底层创建了3个字符串对象,都在字符串常量池当中。
下面代码在方法区的字符串常量池中存在双引号中的内容
在堆内存未开辟空间。无内容
在栈中存放的地址是方法区的字符串常量池的内存地址
*/
String s1 = "abcdef";
String s2 = "abcdef" + "xy";
/* 2、如下代码,使用new对象创建出的字符串:
凡是使用双引号的都在方法区的字符串常量池中有一份
凡是使用new对象的一定会在堆内存中开辟空间
下面的代码在方法区的字符串常量池中存在字符串内容(即双引号内的东西)
并且在堆内存中存在字符串常量池的内存地址,以使堆内存中内获取到字符串内容
栈中存放的地址是堆内存的地址
*/
String s3 = new String("xy");
再分析如下代码,User
是一个实体类,内含id
int类型、name
==String类型==属性,此时对于引用数据类型的对象内的属性而言,String类型的属性还是保存了对象的内存地址,如下图所示:
String类型的数据进行比较的深入研究
代码:
String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出true
/*首先 == 比较的是String对象存储的内存地址,因为这种方式没有new对象,则不会在堆中重新开辟空间,
* s1和s2存储的是方法区的字符串数据常量池中的"hello"内存地址,又因为在字符串数据常量池中会公用相同的值,
* 即只创建一个"hello",s1和s2公用"hello",因此s1 == s2对比的内存地址相同,为true*/
String x = new String("xyz");
String y = new String("xyz");
System.out.println(x == y);
/*首先 == 比较的是String对象存储的内存地址,因为这种方式new对象了,所以会在堆中开辟空间(有几个new就开辟几个),
* x和y存储的是堆的对应地址,堆中存储的是方法区内的字符串数据常量池中的地址(因为都是"xyz",则方法区内的字符串数据常量池会公用
* 此"xyz",队中两个对象new出的空间内数据指向的都是"xyz"的地址),但是x和y中存储的是堆地址,且每次new对象都会开辟地址,则x == 必定是false*/
/**
* 由此案例可以得出:在字符串比较时,最好使用equals方法,尽量不适应 == ,并且写的格式也要注意,如下:
*/
String k = null;
System.out.println("abc".equals(k));
// System.out.println(k.equals("abc")); // 这样子写如果k为空会报空指针异常
图示上述的代码:
构造方法只列举出部分
代码演示:
/**
* 当String类的构造方法传入的是byte类型的数组时
* @param array byte类型的数组
*/
public void argsIsIsByteArray(byte[] array) {
String s = new String(array);
System.out.println(s);
}
/**
* 当String类的构造方法传入的是byte类型的数组、偏移量、长度时。
* @param array byte类型的数组
* @param offset 偏移量(最小为0,最大为数组长度 - 1,即可以理解为数组下标)
* @param length 长度,即元素的个数
*/
public void argsIsIsByteArrayAndOffsetAndLength(byte[] array, int offset, int length) {
String s = new String(array, offset, length);
System.out.println(s);
}
/*其他类型的数组(如char、String、...)也有上述同样的方法*/
/**
* 通过输出可以得出结论,String类内的toString()方法已经重写了,
* 因为toString()方法在Object中默认是输出内存地址的,这里本应输出byte[]数组的内存地址(即首元素的内存地址),
* 但是发现实际输出的是将byte[]数组元素拼接成字符串的值
*/
测试代码:
package edu.hebeu.test;
import edu.hebeu.sources.StringConstructor;
/**
* 测试StringConstructor方法
*/
public class StringConstructorTest {
public static void main(String[] args) {
StringConstructor stringConstructor = new StringConstructor();
/**
* 测试String类的构造方法参数是byte[]类型的数组
*/
byte[] array = {97, 98, 99}; // byte[]类型的数组,97是a,98是b,99是c
stringConstructor.argsIsIsByteArray(array);
/**
* 测试String类的构造方法参数是byte[]数组、偏移量、长度
*/
byte[] array2 = {97, 98, 99, 100, 101};
stringConstructor.argsIsIsByteArrayAndOffsetAndLength(array2, 2, 3);
}
}
常用方法这里只列举出部分
package edu.hebeu.sources;
/**
* String类内部的常用方法
*/
public class StringMethods {
/**
* charAt()方法测试,会返回指定索引的一个char类型数据
* @param s 字符串
* @param index 字符串对应的索引,从0开始
*/
public void charAtTest(String s, int index) {
System.out.println(s.charAt(index));
}
/**
* 按照字典的<顺序(从左至右)>比较两个字符串的大小,发现不相等就return值,相等就继续比较,直至比较完毕
* 相同相同(前.compareTo(后)):0
* 前小后大(前.compareTo(后)):负数(-1)
* 前大后小(前.compareTo(后)):正数(1)
* @param s1
* @param s2
*/
public void compareToTest(String s1, String s2) {
System.out.println(s1.compareTo(s2));
}
/**
* 判断当前的字符串是否包含后面的子字符串,存在为true,反之为false
* @param s 字符串
* @param sChild 子字符串
*/
public void containsTest(String s, String sChild) {
System.out.println(s.contains(sChild));
}
/**
* 判断当前字符串是否以某一字符结尾
* @param s 字符串
* @param endS 结尾字符串
*/
public void endsWithTest(String s, String endS) {
System.out.println(s.endsWith(endS));
}
/**
* 判断两个字符串是否相等
* @param s1
* @param s2
*/
public void equalsTest(String s1, String s2) {
System.out.println(s1.equals(s2));
}
/**
* 判断两个字符串是否相等(忽略大小写)
* @param s1
* @param s2
*/
public void equalsIgnoreCaseTest(String s1, String s2) {
System.out.println(s1.equalsIgnoreCase(s2));
}
/**
* 返回一个byte[]数组,此数组是将字符串对象转化为字节的数组
* @param s
*/
public void getBytesTest(String s) {
byte[] sBytes = s.getBytes();
System.out.print(s + "的字节数组为:[");
for(int i = 0; i < sBytes.length; i++) {
System.out.print(sBytes[i]);
if(i < sBytes.length - 1) {
System.out.print(", ");
} else {
System.out.println("]");
}
}
}
/**
* 判断某个子字符串在当前字符串首次出现的第一个字符的索引<类似于数组下标>,没有就返回 -1
* @param s 字符串
* @param sChild 子字符串
*/
public void indexOfTest(String s, String sChild) {
System.out.println(s.indexOf(sChild));
}
/**
* 判断字符串是否为空,空为true,非空为false
* 注意s为null时会出现空指针异常
* @param s
*/
public void isEmptyTest(String s) {
System.out.println(s.isEmpty());
}
/**
* 获取字符串的长度
* 注意获取数组长度的length是属性,获取字符串长度的length()是方法,两者是不一样的
* @param s
*/
public void lengthTest(String s) {
System.out.println(s.length());
}
/**
* 判断某个子字符串在当前字符串最后出现的第一个字符的索引<类似于数组下标>,没有就返回 -1
* @param s
* @param sChild
*/
public void lastIndexOfTest(String s, String sChild) {
System.out.println(s.lastIndexOf(sChild));
}
/**
* 返回一个新字符串,通过一个newChar替换一个oldChar得到<newChar替换此字符串中出现的所有oldChar>
* @param s
*/
public void replaceArgsIsCharTest(String s, char oldChar, char newChar) {
System.out.println(s.replace(oldChar, newChar));
}
/**
* 返回一个新的字符串,通过oldString替换newString得到<newString替换字符串中出现的oldString>
* CharSequence是String类的父接口
* @param s
* @param oldString
* @param newString
*/
public void replaceArgsIsCharSequenceTest(String s, CharSequence oldString, CharSequence newString) {
System.out.println(s.replace(oldString, newString));
}
/**
* 返回一个String[] 数组,数组通过split传入的参数为分割线进行分割
* @param s 被切割的字符串
* @param regex 可以是正则表达式,也可以是普通字符串
*/
public void splitTest(String s, String regex) {
String[] array = s.split(regex);
System.out.print(s + "字符串被切割为字符串数组:[");
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if(i < array.length - 1) {
System.out.print(", ");
} else {
System.out.println("]");
}
}
}
/**
* 判断当前字符串是否以某个子字符串开始
* @param s 字符串
* @param sChild 子字符串
*/
public void startsWith(String s, String sChild) {
System.out.println(s.startsWith(sChild));
}
/**
* 返回一个字符串,通过起始下标开始截取到的字符串
* @param s 字符串
* @param startSubScript 开始截取的下标 <截取时包括>
*/
public void subStringTest(String s, int startSubScript) {
System.out.println(s.substring(startSubScript));
}
/**
* 返回一个字符串,通过起始下标和结束下标截取到的字符串
* @param s 字符串
* @param startSubScript 开始截取的下标 <截取时包括>
* @param endSubScript 终止截取的下标的后一位*********************** <截取时不包括>
*/
public void subStringTest(String s, int startSubScript, int endSubScript) {
System.out.println(s.substring(startSubScript, endSubScript));
}
/**
* 将一个字符串转换为char[] 数组
* @param s
*/
public void toCharArrayTest(String s) {
char[] array = s.toCharArray();
System.out.print(s + "字符串被转换为字符数组:[");
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if(i < array.length - 1) {
System.out.print(", ");
} else {
System.out.println("]");
}
}
}
/**
* 将一个字符串转都转换为小写
* @param s
*/
public void toLowerCaseTest(String s) {
System.out.println(s.toLowerCase());
}
/**
* 将一个字符串转都转换为大写
* @param s
*/
public void toUpperCaseTest(String s) {
System.out.println(s.toUpperCase());
}
/**
* 去除字符串前后空白
* @param s
*/
public void trimTest(String s) {
System.out.println(s.trim());
}
/**
* valueOf()方法,String类中的静态方法
* 将boolean类型的数据转化为字符串
* @param flag
*/
public void valueOfTest(boolean flag) {
System.out.println(String.valueOf(flag));
}
/**将int[]数组转换为字符串*/
public void valueOfTest(int[] array) {
System.out.println(String.valueOf(array));
}
/**将int类型的数据转换为字符串*/
public void valueOfTest(int value) {
System.out.println(String.valueOf(value));
}
/**如果是对象会默认调用对象的toString()方法*/
public void valueOfTest(User user) {
System.out.println(String.valueOf(user));
}
/**
* System.out.println()方法本质上在输出任何数据时都会调用String.valueOf()静态方法,进而把输出的任何类型数据变成String类型的数据,
* 因此控制台上输出的任何数据都是转换为String类型后的!!!
*/
}
测试代码:
package edu.hebeu.test;
import edu.hebeu.sources.StringMethods;
import edu.hebeu.sources.User;
/**
* 测试String类的方法的方法
*/
public class StringMethodsTest {
public static void main(String[] args) {
StringMethods stringMethods = new StringMethods();
/**
* 测试charAtTest()方法
*/
String s = "中国人";
stringMethods.charAtTest(s, 1);
/**
* 测试compareToTest()方法
*/
stringMethods.compareToTest("abc", "abb");
/**
* 测试containsTest()方法
*/
stringMethods.containsTest("abc", "d");
/**
* 测试endsWithTest()方法
*/
stringMethods.endsWithTest("abc", "bc");
/**
* 测试equalsTest()方法
*/
stringMethods.equalsTest("abc", "abd");
/**
* 测试equalsIgnoreCaseTest()方法
*/
stringMethods.equalsIgnoreCaseTest("abc", "AbC");
/**
* 测试getBytesTest()方法
*/
stringMethods.getBytesTest("abcvdh");
/**
* 测试indexOfTest()方法
*/
stringMethods.indexOfTest("abcdefg", "1");
/**
* 测试isEmptyTest()方法
*/
// String s2 = null;
stringMethods.isEmptyTest("");
/**
* 测试lengthTest()方法
*/
stringMethods.lengthTest("12ssad");
/**
* 测试lastIndexOfTest()方法
*/
stringMethods.lastIndexOfTest("javapythonnodevuereact", "java");
/**
* 测试replaceArgsIsCharTest()方法
*/
stringMethods.replaceArgsIsCharTest("abadkakaakxa", 'a', 'w');
/**
* 测试replaceArgsIsCharSequenceTest()方法
*/
stringMethods.replaceArgsIsCharSequenceTest("http://127.0.0.1:3000", "http://", "https://");
/**
* 测试splitTest()方法
*/
stringMethods.splitTest("2000-10-02", "-");
/**
* 测试startsWithTest()方法
*/
stringMethods.startsWith("javapythonnodevuereact", "ja");
/**
* 测试subStringTest()方法
*/
stringMethods.subStringTest("abcdefg", 2); // cdefg
/**
* 测试由终止下标的subStringTest()方法
*/
stringMethods.subStringTest("abcdefghigklmno", 2, 5); // cde
/**
* 测试toCharArrayTest()方法
*/
stringMethods.toCharArrayTest("abcdefghigklmn");
/**
* 测试toLowerCaseTest()方法
*/
stringMethods.toLowerCaseTest("ABHcvvddvKFDKFD");
/**
* 测试toUpperCaseTest()方法
*/
stringMethods.toUpperCaseTest("HJDFHJDFHjskdjsdjfdjnnJKCnc");
/**
* 测试trimTest()方法
*/
stringMethods.trimTest(" assaj jkjkcxn jjc j ");
/**
* 测试valueOfTest()方法
*/
stringMethods.valueOfTest(true);
stringMethods.valueOfTest(2);
// int[] array = {1, 5, 9, 8, 8, 4, 2};
stringMethods.valueOfTest(new int[]{1, 2, 5, 6, 8});
stringMethods.valueOfTest(new User(180, "tyong"));
}
}
StringBuffer和StringBuilder的引入
在进行字符串的拼接时,常常会使用 + 操作,但是,这种方式在追加大量的字符串时会导致方法区内的字符串常量池中创建
大量的Strng对象,导致空间的浪费。
如代码:String s = "abc"; s += "vb";
这两行代码就会在方法区内的字符常量池中创建3个对象,分别是:“abc”、“vb”、“abcvb”;
因此,最好使用StringBuffer类的append()方法实现字符串的追加操作。
StringBuffer类:StringBuffer底层实际上是一个byte[]数组,默认为16位,超出会自动扩容,往StringBuffer中放字符串,实际上是放到byte[]数组中了。
注意:因为底层是数组,在Java中,数组一旦创建,长度是不可变的,因此,扩容实际上是通过创建一个更大的数组,并把原数组一个一个的拷贝到那个更大的数组中实现的,因此在创建StringBuffer对象时,最好能保证预估好byte[]数组的使用长度以得到容量,再通过构造方法进行创建,保证程序的执行效率;
StringBuffer()构造方法:
使用方法:StringBuffer stringBuffer = new StringBuffer();
作用:构造一个不带字符的字符串缓冲区,初始化容量为16位的byte[]数组,如果超出容量会自动扩充;
StringBuffer(int capacity)构造方法:
使用方法:StringBuffer stringBuffer = new StringBuffer(100);
作用:构造一个不带字符的字符串缓冲区,容量位100的byte[]数组,如果超出,会自动扩充;
StringBuffer使用的优化方法:
StringBuffer在byte[]数组满时会自动扩容,因为是数组,在Java中数组一旦创建长度就不可再变了,所以扩容的本质是通过创建一个更大的数组,再把原先的数组元素一个一个的拷贝至那个更大的数组,以此实现扩容;因此在StringBuffer对象时应预估计一下,尽可能给定一个合适初始化容量,减少扩容(减少byte[]数组的创建拷贝)。
如代码:
StringBuffer stringBuffer = new StringBuffer(); // 创建StringBuffer对象,初始化容量为16(byte[]数组为16位)
// StringBuffer stringBuffer2 = new StringBuffer(100); // 创建StringBuffer对象,初始化容量为100(byte[]数组为100位)
/**StringBuffer的append()方法时可以传递任何类型的参数进行拼接的,并且在byte[]数组满时会自动进行扩充*/
stringBuffer.append(false);
stringBuffer.append("aghgh");
stringBuffer.append('a');
stringBuffer.append(20);
stringBuffer.append(3.1415926);
stringBuffer.append(1112L);
// System.out.println(stringBuffer.toString());
System.out.println(stringBuffer); // 引用数据类型,即输出时会默认调用对象的toString()方法
StringBuffer与StringBuilder的特性是相同的,只不过在两者内部,StringBuffer有synchronized关键字,StringBuilder没有synchronized关键字;
StringBuffer和StringBuilder的区别:
* StringBuffer内的方法都有 synchronized 关键字修饰,表示StringBuffer在多线程环境下运行是安全的;
* StringBuilder内的方法都没有 synchronized 关键字,表示StringBuilder在多线程环境下运行是不安全的;
* 结论:StringBuffer是线程安全的;StringBuilder是非线程安全的;
面试题
分析如下程序一共创建了几个对象:
/**
* 下面的代码中,一共创建了几个对象?
* 3个,一个在方法区的字符串数据常量池中,为"abc",
* 另外两个是在堆中的String对象,两个的值都是方法区的字符串数据常量池中存储"abc"的内存地址
*/
String s1 = new String("abc");
String s2 = new String("abc");
/**
* 下面代码中,一共创建了几个对象?
* 4个,
* 两个在方法区的字符串数据常量池中,分别为"ty"、"test"
* 另外两个是在堆中的String对象,两个的值分别是方法区的字符串数据常量池中存储"ty"和"test"的内存地址
*/
String s3 = new String("ty");
String s4 = new String("test");
/**
* 下面代码中,一共创建了几个对象?
* 2个,
* 两个对象均在方法区的字符串数据常量池中,分别为"sdf"和"fg"
*/
String s5 = "sdf";
String s6 = "fg";
/**
* 下面代码中,一共创建了几个对象?
* 1个,
* 在方法区的字符串数据常量池中,为"tyong"
*/
String s7 = "tyong";
String s8 = "tyong";
/**
* 注意判断数组长度和字符串长度的length是不一样的
* 数组长度的length是属性,字符串长度的length()是方法
*/
八个包装类
Java为8种基本数据类型又封装了8种包装类型,这8种包装类型都是引用数据类型,父类(Number和Object)的父类都是Object;
为这8种基本数据类型提供包装类的目的:
当出现这样的需求时:public void test(Object obj) {}
如果需要为这个方法传递一个基本数据类型,在不使用包装类的前提下,除了给这个方法加参数外,显然没有更好的解决方法,就出现了基本数据类型使用受限的情况;因此,可以把基本数据类型放入一个对象中进行传入,Sun公司因此为这8个基本数据类型提供了各自的包装类;
八种基本数据类型 | 对应的八个包装类 |
---|---|
byte | java.lang.Byte(父类Number) |
short | java.lang.Short(父类Number) |
int | java.lang.Integer(父类Number) |
long | java.lang.Long(父类Number) |
float | java.lang.Float(父类Number) |
double | java.lang.Double(父类Number) |
boolean | java.lang.Boolean(父类Object) |
char | java.lang.Character(父类Object) |
重点演示包装类:java.lang.Integer包装类,其他类型参照java.lang.Integer即可
代码演示:
Number类
Number类是一个抽象类,无法实例化为对象,Number类中有如下这样的方法:
-
byte byteValue() // 以byte形式返回指定数值
-
abstract double doubleValue() // 以double形式返回指定数值
-
abstract float floatValue() // 以float形式返回指定数值
-
abstract int intValue() // 以int形式返回指定数值
-
abstract long longValue() // 以long形式返回指定数值
-
short shortValue() // 以short形式返回指定数值
这些方法都是在所有的数字包装类的子类都有,这些方法是负责 “拆箱” 的;
装箱与拆箱的概念
如下代码展示:
Integer integer = new Integer(123); // 将 123 这个int基本数据类型转换为Integer引用数据类型(手动装箱) Float floa = new Float(236); // 将 236 这个int基本数据类型转换为Float引用数据类型(手动装箱) float f = integer.floatValue(); // 将Integer引用数据类型转换为float基本数据类型(手动拆箱) int a = integer.intValue(); // 将Integer引用数据类型转换为int基本数据类型(手动拆箱) byte bytee = floa.byteValue(); // 将Float引用数据类型转换为byte基本数据类型(手动拆箱)
装箱与拆箱注意的问题:
/*构造方法,可以传入一个该包装类型的数据或者一个字符串<字符串必须是能够转换为数字的形式>*/ // Integer integerr = new Integer(567); Integer integerr = new Integer("56565"); // 可以转换为数字 // Integer integerr = new Integer("8855.6"); // 不能转换为数字<编译没问题,运行出问题> // Integer integerr = new Integer("dds测试"); // 不能转换为数字<编译没问题,运行出问题> // Double doublee = new Double(3.14); // Double doublee = new Double("sss"); // 不能转化为数字,会报错<编译没问题,运行出问题> Double doublee = new Double("1589"); // 可以转换为数字
自动装箱与自动拆箱:
Integer x = 100; // 自动装箱(int基本数据类型包装为Integer引用数据类型),等同于 Integer x = new Integer(100);
float f = x; // 自动拆箱(Integer引用数据类型拆分为float基本数据类型),等同于 float f = x.floatValue();
System.out.println("x=" + x + "; f=" + f);
Integer a = 1000;
Integer b = 1000;
System.out.println(a == b); // false
/**可见 == 运算符是不能触发自动拆箱机制的*/
/**
* 结论: == 运算符比较对象、引用时,比较的时内存地址
* == 运算符不会触发自动拆箱机制(只有 +、-、*、/等运算符时才会触发)
*/
整数型常量池的引入
/**通过常量可以自动获取最大值和最小值*/
System.out.println(Integer.MAX_VALUE); // 获取Integer的最大值
System.out.println(Integer.MIN_VALUE); // 获取Integer的最小值
System.out.println(Byte.MAX_VALUE); // 获取Byte的最大值
System.out.println(Byte.MIN_VALUE); // 获取Byte的最小值
/**整数型常量池概念的引入*/
Integer a1 = 128;
Integer a2 = 128;
System.out.println(a1 == a2); // false
Integer b1 = 127;
Integer b2 = 127;
System.out.println(b1 == b2); // true
对上述情况的分析:
Java中为了提高效率,在方法区内存在一个整数型常量池,这个池中有256个Integer对象(-128到127),这些对象随着类加载而加载,即用户在使用Integer时会加载,如果用户创建的Integer类型数据在 -128到127之间,就不会再堆中开辟空间,而是直接使用该池的数据;反之,就要在堆中创建新的Integer对象;
上述的几行代码中之所以出现那种情况:因为 == 运算符在比较对象、引用数据类型时,永远比较的所示内存地址,128不在-128到127之间,所以会在堆内存中开辟空间;127在 -128到127之间,所以不会再堆中开辟空间;在对象中存储的是引用的内存地址,所以,a1和a2因为不在 -127到127 之间而使用的是在堆中新建的两个Integer对象地址;b1和b2因为在 -128到127 之间而使用的是在方法区内的整数型常量池中的地址,因此会出现false、true
整数型常量池的图示:
Integer类的常用方法
package edu.hebeu.sources;
/**
* 介绍Integer类的方法,可以借此推出其他7个包装类的方法以及这些方法的性质
*/
public class IntegerMethods {
/**
* Integer类的parseInt()方法,静态方法
* 返回一个整数型 int 数据,通过String类型的数据得到
* @param s
*/
public void parseIntTest(String s) {
int i = Integer.parseInt(s);
System.out.println(i);
}
/**
* 返回一个二进制的String类型数据
* Integer类的toBinaryString()方法,静态方法
* 将十进制的int转换为二进制的String类型数据
* @param i
*/
public void toBinaryStringTest(int i) {
String s = Integer.toBinaryString(i);
System.out.println(s);
}
/**
* 返回一个十六进制的String类型数据
* Integer类的toHexString()方法,静态方法
* 将十进制的int转换为十六进制的String类型数据
* @param i
*
* 1 2 3 4 5 6 7 8 9 a b c d e f
*/
public void toHexStringTest(int i) {
String s = Integer.toHexString(i);
System.out.print(s);
}
// public void toHexStringTest(long i) {
// System.out.println(Long.toHexString(i));
// }
/**
* 返回一个八进制的String类型字符串
* Integer类的toOctalString()方法,静态方法
* 将十进制 的int转换为八进制的String类型数据
* @param i
*/
public void toOctalStringTest(int i) {
String s = Integer.toOctalString(i);
System.out.println(s);
}
/**
* 返回一个Integer类型的数据,通过传入的 int 类型数据
* Integer的valueOf()方法,静态方法
* @param i
*/
public void valueOfTest(int i) {
Integer integer = Integer.valueOf(i);
System.out.println(integer);
}
/**
* 返回一个Integer类型的数据,通过传入的 String 类型数据
* Integer的valueOf()方法,静态方法
* @param s
*/
public void valueOf(String s) {
Integer integer = Integer.valueOf(s);
System.out.println(integer);
}
}
Integer和int和String类型之间的互转
图示:
代码:
/**String转int*/
String s1 = "123";
int i1 = Integer.parseInt(s1);
System.out.println("s1=" + s1 + "; i1=" + i1);
/**int转String*/
String s2 = i1 + "";
// String + int = String
String s3 = s2 + 1; //
System.out.println("s2=" + s2 + "; s3=" + s3);
/**int转Integer*/
Integer integer1 = i1; // 自动装箱
System.out.println("i1=" + i1);
/**Integer转int*/
int i2 = integer1; // 自动拆箱
System.out.println("i2=" + i2);
/**String转Integer*/
Integer integer2 = Integer.valueOf(s1);
System.out.println("integer2=" + integer2);
/**Integer转String*/
String s4 = String.valueOf(integer2);
System.out.println("s4=" + s4);
Date
package edu.hebeu.sources;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 学习Java中的Date包装类
*/
public class DateClass {
public static void main(String[] args) throws Exception {
/**
* 获取系统当前时间
*/
Date nowDate = new Date(); // 获取系统当前时间,精确到毫秒
System.out.println(nowDate); // 通过打印可以看出Dtae类的toString()方法已经被重写了
/**
* 格式化日期
* yyyy 年
* MM 月
* dd 日
* HH 时
* mm 分
* ss 秒
* SSS 毫秒
* 注意:在格式化日期时,除了y M d H m s S不能随便写以外,其他的符号填充格式随意组织
*/
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");// 创建进行日期格式化的格式对象
SimpleDateFormat simpleDateFormat1 = new SimpleDateFormat("yyyyMMddHHmmssSSS");
String nowDateS = simpleDateFormat1.format(nowDate); // 通过格式化的对象调用format()方法传入时间参数,该方法会返回一个同对象格式相同的时间字符串
System.out.println(nowDateS);
/**
* 通过日期字符串解析出日期
*/
String dateTime = "2000=10=02 21=59=56 555";
SimpleDateFormat simpleDateFormat2 = new SimpleDateFormat("yyyy=MM=dd HH=mm=ss SSS"); // 注意,这个对象构建时传入的参数要与解析的String类型的日期格式一致!!!
Date date = simpleDateFormat2.parse(dateTime); // 通过 创建的格式化对象调用parse()方法传入String类型的日期,该方法会返回一个Date类型的值
System.out.println(date); // Mon Oct 02 21:59:56 CST 2000
/**
* 获取自1970年1月1日 00:00:00 000到当前系统时间的总毫秒数
*/
long nowTimeMills = System.currentTimeMillis();
System.out.println(nowTimeMills);
/**
* 统计方法执行时间
*/
long doMethodFrontTime = System.currentTimeMillis();
method();
long doMethodEndTime = System.currentTimeMillis();
System.out.println("method()方法共执行" + (doMethodEndTime - doMethodFrontTime) + "ms");
/**
* 有参数的构造函数
* 参数是从1970年1月1日 0:0:0 000开始到系统当前时间的毫秒数,单位是毫秒
*/
Date dateTime2 = new Date(1); // 即构造日期为1970年1月1日 0:0:0 001的Date对象
String time1 = simpleDateFormat.format(dateTime2);
System.out.println("time1=" + time1); // 会多8个小时,没有问题,因为北京是东8区
/**
* 获取昨天的日期
*/
Date dateTime3 = new Date(System.currentTimeMillis() - 1000 * 60 * 60 * 24);
String time2 = simpleDateFormat.format(dateTime3);
System.out.println("time2=" + time2);
}
// 模仿执行时间
public static void method() {
for(int i = 0; i < 1000000000; i++) {
// System.out.println("i=" + i);
}
}
}
DecimalFormat
/**
* 数字格式化类DecimalFormat类的使用
* # 任意数字
* , 千分位
* . 小数点
* 0 不够补0
*/
DecimalFormat decimalFormat = new DecimalFormat("###,###.###");
String number1 = decimalFormat.format(12345685355.23560555);
System.out.println("number1=" + number1); // number1=12,345,685,355.236
DecimalFormat decimalFormat2 = new DecimalFormat("###,###.00000000");
String number2 = decimalFormat2.format(2552.322);
System.out.println("number2=" + number2); // number2=2,552.32200000
BigDecimal
- BigDecimal属于 大数据,精度极高。不属于基本数据类型,属于Java对象(属于引用数据类型)是SUN提供的一个类,专门用于财务软件当中;
- 注意:处理财务软件时,double类型的数据是不够精确的,而是要借助于java.math.BigDecimal类
BigDecimal a = new BigDecimal(400); // 创建一个高精度的400
BigDecimal b = new BigDecimal(200); // 创建一个高精度的200
/**
* 注意:BigDecimal生成的是对象,所以要进行基本数据运算时,传统的 + - * / 符号是不能使用的
*/
BigDecimal c1 = a.add(b); // 实现a + b
System.out.println("c1=" + c1); // c1=600
BigDecimal c2 = a.subtract(b); // 实现a - b
System.out.println("c2=" + c2); // c2=200
BigDecimal c3 = a.divide(b); // 实现a / b
System.out.println("c3 = " + c3); // c3=2
// BigDecimal c3 = a.abs(new MathContext(2));
// System.out.println("c3=" + c3);
Random
- 这个类用来生成随机数
如下代码:
Random random = new Random();
int a1 = random.nextInt(); // 产生一个int类型的随机数
System.out.println("a1=" + a1);
int a2 = random.nextInt(101); // 产生一个0-100的int类型随机数(注意没有101)
System.out.println("a2=" + a2);
案例
产生一组不重复的随机数,并将这些数字放到数组中
代码:
/**
* 在small到big范围内获取num个随机数
* @param num
* @param small
* @param big
* @return
*/
public int[] getRandomNumbers(int num, int small, int big) {
int[] array = new int[num];
Random random = new Random();
int index = 0;
int whileNum = 0; // 记录循环次数
while(index < array.length) {
int temp = random.nextInt(big); // 生成0-big个随机数(不包括big)
System.out.println("第" + ++whileNum + "次循环,temp=" + temp);
if(!selectEleByArray(temp, array)) {
array[index] = temp;
index++;
}
if(index == array.length - 1) break;
}
return array;
}
/**
* 查询数组中是否存在某个元素
* @param elem
* @param array
* @return
*/
public static boolean selectEleByArray(int elem, int[] array) {
for(int index = 0; index < array.length; index++) {
if(elem == array[index]) return true;
}
return false;
}
测试代码:
RandomStu randomStu = new RandomStu();
int[] array = randomStu.getRandomNumbers(10, 0, 10); // 生成10个随机整数,从 [0,10]的范围内
System.out.print("[");
for(int i = 0; i < array.length; i++) {
System.out.print(array[i]);
if(i == array.length - 1) {
System.out.print("]");
} else {
System.out.print(", ");
}
}
Enum
-
枚举:
-
1、枚举是一种引用数据类型
-
枚举类型的定义、语法:
enum 枚举类名 { 枚举值1、枚举值2、...... } // 结果只有两者情况的,建议使用boolean类型;结果存在多种情况且能够一枚一枚的列举出来的建议使用枚举;
-
4、枚举编译之后也是生成class文件
-
5、枚举的每一个值可以看作是常量
-
6、可以使用switch语句
-
需要注意:switch语句可以使用enum枚举,也可以使用int、String;
- byte, short, boolean, char也都可以使用(但是存在自动类型转换)
- 低版本的JDK,switch只能使用int
代码演示
定义一个Enum枚举:
package edu.hebeu.enums;
public enum Season {
SPRING, SUMMER, AUTUMN, WINTER
}
switch使用Enum枚举:
package edu.hebeu.enums;
public class SwitchUseEnumTest {
public static void main(String[] args) {
switch(Season.SUMMER) {
case SPRING:
System.out.println("春");
break;
case SUMMER:
System.out.println("夏");
break;
case AUTUMN:
System.out.println("秋");
break;
case WINTER:
System.out.println("冬");
break;
}
}
}
异常
须知
何为异常:程序执行过程中的不正常情况异常的作用:增强程序的建壮性;Java中的异常:Java共存在Error和Exception两种,此两者就是实现的Throwable接口,Throwable接口是继承的Object类
**注意:**1.异常在Java中以类的形式存在,每一个异常类都可以创建异常对象;[2].Java打印异常堆栈追踪信息时,是异步的,因为是另一个线程;
Java中的两种异常
- Error: 错误。一旦发生,不可处理,程序只能终止,退出JVM;
- Exception: 异常。发生后可以处理;
- Error和Exception都可以抛出;
- Error包括: VirtualMachineError(子类: StackOverflowError)、IOError、ThreadDeath、…等;
- Exception包括: ExceptionSubClass<非类名,而是Exception的直接子类,被称为编译时异常>,所有ExceptionSubClass和它的直接子类都被称为编译时异常/受检异常/受控异常/CheckedException;
- 所有RuntimeException和它的直接子类都被称为运行时异常/未受检异常/非受控异常/UnCheckedException;RuntimeException(子类: NullPointerException、ClassCastException、IllegalArgumentException(子类: NumberFormatException、…))、…,
编译时异常和运行时异常
- 编译时异常:编译时异常表示必须在编写程序的时候预先对这种异常进行处理,如果不处理编译器会报错,因此而得名编译时异常;
- 运行时异常:运行时异常表示在编写程序阶段可以选择处理或不处理;
- 编译时异常和运行时异常(即所有异常)都是发生在运行阶段的,编译阶段异常是不会发生的,因为异常的发生就是new异常对象;
**编译时异常和运行时异常的区别:**编译时异常发生的概率一般较高;运行时异常发生的概率一般较低;
代码演示
异常的两个重要的方法
- String msg = 异常对象.getMessage(); // 获取异常的简单信息描述
- 异常对象.printStackTrace(); // 打印异常追踪的堆栈信息
示例代码:
package edu.hebeu.sources;
/**
* 异常类的两个非常重要的常用方法:
*/
public class ExceptionMethods {
public static void main(String[] args) {
NullPointerException nulllPointerException = new NullPointerException("****这是空指针异常****");
// 获取异常的简单描述信息,此信息实际上是创建异常对象传入构造方法的String参数
String nulllPointerExceptionMSG = nulllPointerException.getMessage();
System.out.println(nulllPointerExceptionMSG);
// 打印异常堆栈信息
nulllPointerException.printStackTrace();
/**可以看出Java打印异常堆栈追踪信息时,是异步的,因为是另一个线程*/
for(int i = 0; i <= 1000; i++) {
System.out.println("i=" + i);
}
System.out.println("完毕!!!");
}
}
处理异常的两种方式
处理异常的两种方式:
- 在方法声明的位置使用throws关键字抛给上一级(调用者),但是注意异常之后的代码块就无法执行了,但是抛给上一级(调用者)后,上一级还要进行处理(任有两种方式:throws或者try {}catch() {});
- 使用try {}catch() {} 语句进行异常的捕获处理,try内出异常的语句之后的代码就不执行了,而是直接执行catch(){}内的语句块,带执行完后,再向后执行try {}catch() {}外的代码;
- 总结:
- 一个方法内的代码出现异常,如果采用上报方式(throws),异常之后的代码就不会执行了(可以理解为return掉了),但是如果使用try {} catch() {}方式捕获解决掉,try{}内异常之后的代码就不会执行,而是转入catch(){}中,待catch(){}内的代码执行完毕后再向后执行代码块;
- 使用上报、try {}catch() {}两种方式处理异常时,可以处理本异常,也可以处理本异常的父异常;
- try {}catch() {}内的catch(异常类的父类 异常类的父类对象)是借用多态实现的;
- 一个try {}后可以跟多个catch() {}进行特定的异常处理,建议把异常精确处理,而非直接调用异常类的父类Exception类;
- 一个try {}后所跟的catch() {}处理异常为多个时,从上到下必须遵守从小到大(保证处理的异常唯一),且唯一;
注意:Java异常发生之后如果一直上抛,最终会抛给main方法,main方法继续上抛给调用者JVM,JVM知道这个异常发生,只有一个结果,即终止Java程序;
自定义异常
自定义异常的过程:
- 选择继承Exception(编译时异常)或者RuntimeException(运行时异常)
- 创建无参的构造方法和有参的构造方法,此时自定义异常就创建完成了!
自定义编译时异常:
package edu.hebeu.sources;
/**
* 自定义编译时异常
*/
public class CustomException extends Exception{
public CustomException() {
super();
}
public CustomException(String s) {
super(s);
}
}
自定义运行时异常:
package edu.hebeu.sources;
/**
* 自定义运行时异常
*/
public class CustomRuntimeException extends RuntimeException{
public CustomRuntimeException() {
super();
}
public CustomRuntimeException(String s) {
super(s);
}
}
测试代码:
package edu.hebeu.test;
import edu.hebeu.sources.*;
public class CustomExceptionsTest {
public static void main(String[] args) {
/**编译时异常测试*/
CustomException customException = new CustomException("我的编译时异常");
// 测试getMessage()方法
String s1 = customException.getMessage();
System.out.println(s1);
// 测试printStackTrace()方法
customException.printStackTrace();
/**测试运行时异常*/
CustomRuntimeException customRuntimeException = new CustomRuntimeException("我的运行时异常");
// 测试getMessage()方法
String s2 = customRuntimeException.getMessage();
System.out.println(s2);
// 测试printStackTrace()方法
customRuntimeException.printStackTrace();
}
}
finally 字句的使用
- finally语句在try {}catch() {}语句块最后执行,并且一定会执行(无论try {}内的语句是否出现异常都会执行),finally和try必须一起出现,不能单独编写;
- finally语句通常用于释放(关闭)资源;try内不论是什么情况(就算是有 return),都会执行finally内的代码,仅仅是执行顺序改变,即此时finally内的代码执行在return语句执行之前(非try {}之前);
- return语句只要执行,方法必然结束,try内有return时,其块内的 try {}catch() {}finally {} 之后的代码就不能被执行了!!!try内有
System.exit(0) // 退出Java虚拟机 JVM
语句时,finally内的语句就不能执行了!!!其块内的 try {}catch() {}finally {} 之后的代码也就不能被执行了!!! - try不可单独使用;finally和try{}语句也可以连用,类似于 try {}finally {},
引入一个面试题:
package edu.hebeu.sources;
/**
* 关于finally的一个面试题
*/
public class FinallyInterview {
public static void main(String[] args) {
int res = m1();
System.out.println(res);; // 问,此时输出多少? 输出100
}
/**
* Java中的语法规则一:
* 方法体中的代码必须从上到下依次执行(不可违背!!!)
* Java中的语法规则二:
* return语句一旦执行,方法必须结束(不可违背!!!)
*
* 下面的这个方法理论来说应该是返回101,但是为了能保证上述的两条语法规则不可违背,因此返回100而非101;
* 但是注意:finally{}内的代码还是在 “return i;” 之前执行的,“return i;” 也还是在最后执行的,但
* 是为了保证上述两条语法规则i仍为100;
*
* 借助工具反编译解析出的class文件如下:
* public static int m1() {
* int i = 100;
* int j = i;
* i++;
* return j;
*
* // 异常相关,无需分析
* Exception exception;
* exception;
* i++;
* throw exception;
* }
* 可以发现此代码即遵守了上述的两个Java语法规则(保证了return 语句最后执行),因此得到返回值100而非101 !!!
* @return
*/
public static int m1() {
int i = 100;
try {
return i;
} finally {
i++;
}
}
/**
* 面试题二:final、finally、finalize的区别
* final是关键字,表示最终的、不可变的;
* final修饰的变量只能赋一次值(不能重新赋值)
* final修饰的方法不能被覆盖
* final修饰的类不能被继承
*
* finally是关键字,和try联合使用,使用在异常处理机制中,
* 在finally{}内的代码一定会执行;
*
* finalize是Object类的方法,是由JVM的垃圾回收器GC负责调用
*/
}
集合
在实际开发中大量使用,数组其实就是一个集合,集合实际上就是一个容器,可以容纳其他类型的数据;集合是一个容器、一个载体,可以一次容纳多个对象。
注意:
-
Java中集合不能存储基本数据类型,不能直接存储Java对象,集合当中存储的都是Java对象的内存地址(或者说集合中存储的是引用),
list.add(100); // 注意,此处是应用的自动装箱,把int基本数据类型100 装箱为Integer类型;
-
集合中任何时候存储的都是引用;
-
集合也是一个对象,也有内存地址;
-
Java中每个不同的集合底层会对应不同的数据结构,往不同的集合中存储元素,等于将数据放到了不同的数据结构中。
-
综上:往集合c1放数据,可能放到了数组上,往集合c2放数据,可能放到了二叉树上,往集合c3放数据,可能放到了哈希表上,…
即使用不同的集合等同于使用了不同的数据结构;
new ArrayList(); // 创建一个集合,底层是数组;
new LinkedList(); // 创建一个集合,底层是双向链表;
new TreeSet(); // 创建一个集合,底层是二叉树; …
-
集合中存储与的是对象的引用:
Java中的两类集合
- 以单个方式存储元素,此方式的超级父接口:java.util.Collection;
- 以键值对存储元素,此方式的超级父接口:java.util.Map;
Java中常用集合的特点和其底层数据结构
- ArrayList: 底层是数组;
- Vector: 底层是数组,只不过是线程安全的,效率较低,很少使用;
- LinkedList: 底层是双向链表;
- HashSet: 底层是HashMap,HashSet存储的数据元素相当于HashMap的key部分;
- TreeSet: 底层是TreeMap,TreeSet存储的数据元素相当于TreeMap的key部分;
- HashMap: 底层是Hash表/散列表(单向链表 + 数组);
- Hashtable: 底层是Hash表,只不过是线程安全的,效率较低,很少使用;
- Properties: 是线程安全的并且key和value只能存储字符串String;
- TreeMap: 底层是二叉树,TreeMap集合的key可以自动按照大小顺序排序;
List集合存储元素的特点:
有序可重复;
有序:存入的顺序和取出的顺序一样,每个元素都有下标;
可重复:存进去1后,还能再存储1;
Set(Map)集合存储元素的特点:
无序不可重复;
无序:存入的顺序和取出的顺序不一样,并且Set集合中元素没有下标;
不可重复:存进去1,就不能再存入1了(再存储1会覆盖掉之前存储的1);
SortedSet(SortedMap)集合存储元素的特点:
首先是无序不可重复的,但是SortedSet集合中的元素是可排序的;
无序:存入的顺序和取出的顺序不一样,并且Set集合中元素没有下标;
不可重复:存进去1,就不能再存入1了;
可排序:可以 按照大小顺序排列;
注意:观察底层源码,Map集合的key就是一个Set集合,往Set集合中存放数据实际上放到了Map集合的key部分;但是Map集合与Collection集合没有任何关系,仅仅是Collection类型的set添加方法add()借用了Map类型添加元素的put()方法;Map集合是顶级接口,List集合(接口)和Set集合(接口)的父接口是Collection;
Collection类型的集合只列举出部分重要的
图示:
Collection中能存储什么元素?
没使用泛型之前,能存入Object的所有子类型,使用泛型后,Collection只能存储某个具体的类型,集合当中不能直接存储基本数据类型,也不能村4存Java对象,只能存Java对象的内存地址;
Collection中的常用方法
- boolean add(Object object); // 向集合中添加元素,添加至集合的最后一位
- int size(); // 获取集合中元素的个数
- void clear(); // 清空集合
- boolean contains(Object object); // 判断当前集合中是否包含此元素,包含 ? true : false
- boolean remove(Object object); // 删除集合中的对应元素 删除成功 ? true : false
- boolean isEmpty(); // 判断集合是否为空(集合中是否存在元素) 为空 ? true : false
- Object[] toArray(); // 将集合转换数组
- void addAll(Collection<? extends E> c); // 将将collection内的所有元素都添加到此collection中
Iterable迭代器
迭代方式/遍历方式,对所有的Collection来说是通用的一种方式;但是,对Map集合不能使用,仅能在Collection以及其子类中使用;
工作原理:初始调用boolean hasNext()方法,判断迭代器当前所指元素的下面是否还存在元素,如果存在返回true;反之返回false;此时,迭代器对象会根据hasNext()方法的返回值决定后续的执行,如果是true,就会调用next()方法;反之终止迭代;Object next()方法在hasNext()方法为true时被调用,其会将迭代器指向当前元素的下一个元素,然后将此时指向的元素返回出去,再调用boolean hasNext()方法,由hasNext()决定程序的下一执行步骤;
迭代器原理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HPRHL8Eu-1619533734431)(javaSE学习笔记.assets/迭代器的原理.PNG)]
**注意点:**迭代器对象获取(通过Iterator it = new Iterator()),迭代器用来遍历集合,此时相当于对当前集合的状态拍了一个快照,迭代器迭代时会参照这个快照进行迭代,所以,在迭代过程中,集合的结构不能发生改变(不能删除元素),否则会出现异常(java.util.ConcurrentModificationException),因此如果必须要在迭代时改变集合结构(删除集合内的元素),可以使用迭代器的remove(Object o)方法进行删除,这个方法会将快照和集合内的元素都删除,以保证不会出现异常;
总结:
- 在迭代集合元素的过程中,不能调用集合对象的remove()方法删除元素,因为调用这个方法会改变集合的结构,通过上述注意点会出现java.util.ConcurrentModificationException异常;
- 如果一定要在迭代过程中删除元素,可以调用迭代器对象的remove()方法,此方法会将迭代器当前指向的元素删除;不要使用集合自带的remove()方法删除元素!!!
加上有一个集合Collection hashC = new HashSet();
,内含若干元素,使用迭代器遍历集合的代码如下:
// 对集合进行遍历
// 第一步:获取集合对象的迭代器对象hashIterator
Iterator hashIterator = hashC.iterator();
// 第二步:通过上面的迭代器结合迭代器工作原理进行遍历/迭代集合
while(hashIterator.hasNext()) { // 判断当前元素的下面是否还有元素(判断还能否迭代)
Object obj = hashIterator.next(); // 将迭代器指向下一个元素,并将当前指向的元素返回出去(进行迭代)
System.out.print(obj);
}
boolean contains(Object object)方法深入
**结合源代码得出结论:**contains()方法用来比较的是元素的内容而非内存地址!!!contains()方法内部调用了object参数的equals()方法,实现比较内容,而不是 == 比较内存地址;
boolean remove(Object object)方法深入
**源码分析:**remove()方法底层调用了equals()方法,通过object参数的equals()方法根据内容来判断要删除哪个元素;
List类型的集合
List集合存储元素的特点,有序可重复,有下标:List集合中的元素有下标;从0开始,以1递增;可重复:存储一个6还可再存储6;
相对于Collection集合的特有方法
- void add(int index, Object obj); // 向特定的索引处添加元素
- Object get(int index); // 通过索引获取集合中指定的元素
- int indexOf(Object obj); // 通过元素获取其在集合中第一次出现的下标,没有返回-1
- int lastIndexOf(Object obj); // 通过元素获取其在集合中最后一次出现的下标,没有返回-1
- Object remove(inde index); // 移除集合中指定位置的元素
- Object set(int index ,Object obj); // 通过指定的元素替换集合中指定位置的元素
ArrayList集合
默认初始长度为10;(注意:JDK8~13中底层先创建长度为0的空数组,当添加第一个元素时,长度变为10!!!),底层是一个Object类型的数组;ArrayList集合是非线程安全的;
构造方法
- new ArrayList();
- new ArrayList(20);
- new ArrayList(Collection<? extends E> c); // 将指定Collection类型集合通过构造方法转换为ArryList集合
ArrayList集合的扩容
ArrayList集合的扩容是扩容到原容量的1.5倍(增长到1.5倍);ArrayList底层是数组,**如何优化?**尽可能少的扩容,因为数组扩容效率低,建议在使用ArrayList集合时给定一个预估计的初始容量,减少数组的扩容次数;
面试:这么多集合,那个集合的应用最多?
ArrarList集合,因为集合最常用的就是添加和检索元素,ArrayList集合添加元素是往数组末尾添加元素,效率不受影响,并且ArrayList集合的数据结构是数组,数组具有检索效率高的优点;
LinkedList集合
**优点:**由于链表中的元素空间存储上内存地址不连续,所有随机增删元素时不会涉及到大量的元素位移,因此增删效率高;在以后的开发中,如果集合随机增删操作较多,建议使用LinkedList集合;
**缺点:**不能通过数学表达式计算被查找元素的内存地址,每一次查找都是从头结点开始遍历,直到找到为止,所以LinkedList集合检索效率较低;
注意:
- LinkedList集合的底层虽然是双向链表,但是其也是有下标的;因此ArrayList集合的检索效率高不是单纯的因为其有下标,而是因为其底层是数组;
- LinkedList集合虽然同样有下标,但是检索/查找某个元素的效率确比较低,因为底层是链表,只能从头结点一个一个开始遍历;
- LinkedList集合没有初始化容量一说,最初这个链表的first和last都是null;
单向链表:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wulYfMQ0-1619533734431)(javaSE学习笔记.assets/单向链表数据结构-1615809011178.PNG)]
双向链表:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JUEMU43m-1619533734432)(javaSE学习笔记.assets/双向链表数据结构.PNG)]
Vector集合
特点:
- 底层是数组;
- 默认初始化容量为10;
- **如何扩容?**扩容之后的容量是原容量的2倍
- Vector内所有的方法都是线程同步的,都带有synchronized关键字,是线程安全的,效率比较低,使用较少;
- 使用方式与ArrayList等其他List数组一致;
Connections工具类
常用方法:
- synchronizedList() // 静态方法,将List类型的集合变成线程安全的
- sort(List l); // 静态方法,将List类型的集合排序
- 对于实现Comparable接口且重写了CompareTo()方法之后的自定义类,同样可以调用Collections集合工具类的sort()排序方法;如果未实现该接口和方法,可以在传入要排序的集合对象后在传入第二个参数比较器对象,如:
Collections.sort(List l, Comparable<T>)
- 对于实现Comparable接口且重写了CompareTo()方法之后的自定义类,同样可以调用Collections集合工具类的sort()排序方法;如果未实现该接口和方法,可以在传入要排序的集合对象后在传入第二个参数比较器对象,如:
代码如下
/*演示将非线程安全的ArrayList集合对象变成线程安全的*/
List l = new ArrayList(); // 创建ArrayList集合对象,此时是非线程安全的
// 变成线程安全的
Collections.synchronizedList(l); // 执行完此方法后集合l就变成线程安全的
/**演示如何将List类型的集合排序*/
List<Student> students = new ArrayList<>();// 假定该类(Student)实现了Comparable<T>接口和其内部方法
/*添加元素......
.............*/
Collection.sort(students) // 可以直接传入这个集合对象 对添加的元素进行排序
List<Student2> students2 = new ArrayList<>(); // 假定该类(Student2)未实现了Comparable<T>接口和其内部方法
/*添加元素......
.............*/
Collection.sort(students2, new studentComparator()) // 可以传入这个集合和对应是比较器对象(这个比较器对象可以使用匿名内部类的方式创建) 对添加的元素进行排序
class studentComparator implements Comparator<Student2> { // 比较器
@Override
public int compare(Student2 s1, Student2 s2) {
return s1.num.compareTo(s2.num); // 升序排序
}
}
Set类型的集合
HashSet集合
**特点:**无序不可重复,1、存储的存储时的顺序和取出时的顺序不一样;2、集合内的元素不可重复;3、存储到HashSet集合内的元素实际上是存储到HashMap集合的key部分了;初始化容量、扩容细节等各种扩容机制同HashMap集合;
构造方法
-
new HashSet();
-
new HashSet(int initialCapacity); // 创建初始化容量为initialCapacity的集合,加载因子为0.75
-
new HashSet(Colllection<? extends E> c); // 将指定的Collection类型集合转换为HashSet集合
TreeSet集合
特点:
-
无序不可重复,但是存储到集合内的元素可以按照大小顺序排列;又被称为可排序集合;
-
TreeSet集合底层是一个TreeMap;放到TreeSet集合的元素等同于放到TreeMap集合的key部分;
-
对自定义类无法使用TreeSet集合,因为没有指定自定义对象之间的比较规则(谁大谁小没有说明),添加元素至集合会出现类型转换异常:
Exception in thread "main" java.lang.ClassCastException: edu.hebeu.collection.set.User cannot be cast to java.lang.Comparable
;-
出现上述异常的原因是自定义类,是因为没有实现java.lang.Comparable接口导致TreeSet底层将自定义类型转换成java.lang.Comparable类型失败;
-
-
TreeSet/TreeMap集合的构造方法可以不传入任何参数(此时没有比较器,会将比较器置为null,添加元素时底层会使用添加的元素的compareTo()方法实现边排序边添加,此时就要求该元素必须实现Comparable接口,重写compareTo()方法;);TreeSet/TreeMap集合的构造方法也可以传入一个比较器为参数(此时有比较器对象,底层会使用这个比较器对象内的compare()方法内的比较规则实现排序添加元素),注意比较器是一个类,并且比较器要实现Comparator接口,且重写compare()方法;
自平衡二叉树TreeSet集合TreeMap集合都可参照
存放元素的两种方式
- 可以将放在集合的元素实现java.lang.Conmparable接口,重写内部的compareTo()方法,在该方法内部编写排序规则;
- 可以通过构造方法创建这两种集合对象时,通过构造方法传入一个比较器对象给集合对象,构造器是一个类,其继承Comparator接口,重写compare()方法,该方法内有排序规则;
方式一的代码实现:
/**自定义类实现Comparable<T>接口的compareTo()方法*/
class User implements Comparable<User> {
private String name;
private int age;
public User(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "User [name=" + name + ", age=" + age + "]";
}
/** 需要在这个方法中编写比较的逻辑/规则(按照什么进行比较),
*
* 比较规则由程序员决定:
* 如按照年龄升序、按照年龄降序;
*
* 因为TreeSet底层会调用这个方法,拿着参数k与集合内的每个k进行比较,返回值可能大于0、小于0、等于0
* 返回0,表示相同,value会覆盖;(l - r = 0),左右相等,进行覆盖
* 返回值大于0,会在右子树上找;(l - r > 0),左大于右,所以在右子树上找
* 返回值小于0,会在左子树上找;(l - r < 0),右大于左,所以在左子树上找
*
* 本排序规则:先按照年龄升序,如果年龄相同再按照姓名升序;
*/
@Override
public int compareTo(User u) {
/* // 此方式是按照年龄升序
if(this.age == u.age) return 0; // 如果相等返回0
if(this.age > u.age) return 1; // 如果当前age大于参数的age,返回1(大于0的数)
else return -1; // 如果当前age小于参数age,返回-1(小于0的数)
*/
// return this.age - u.age; // 以上代码可以简写成此(按照年龄升序)
// return u.age - this.age; // 此方式是按照年龄降序
// 先按照年龄升序,如果年龄相同再按照姓名升序
if(this.age == u.age) { // 如果年龄相等
return this.name.compareTo(u.name); // 按照姓名排序(因为name是String类型,String类型已经继承了Comparable接口,String内部已经实现了String的比较方法compareTo()方法))
} else return this.age - u.age; // 如果年龄不相等,就返回差值(按照年龄升序)
}
}
/**测试自定义类实现Comparable接口,重写compareTo()能否使用TreeSet/TreeMap集合排序*/
TreeSet<User> treeSet3 = new TreeSet<>();
User u1 = new User("zhangsan", 20);
User u2 = new User("zhangsi", 50);
User u3 = new User("wangwu", 46);
// 添加自定义类至TreeSet集合
treeSet3.add(u1);
treeSet3.add(u2);
treeSet3.add(u3);
for(User user: treeSet3) {
System.out.println(user);
}
方式二的代码实现:
/*这个自定义类没有实现Comparable接口,没有重写conmpareTo()方法*/
class Student {
String name;
int age;
public Student(String name, int age) {
super();
this.name = name;
this.age = age;
}
@Override
public String toString() {
return "Student [name=" + name + ", age=" + age + "]";
}
}
/*自定义一个比较器,比较器要实现Comparator接口,同时重写compare()方法,此方法内指定比较规则,类似于自定义类实现Comparable接口重写compareTo()方法*/
class studentComparator implements Comparator<Student> {
@Override
public int compare(Student s1, Student s2) {
// 先按照年龄升序,如果年龄相同再按照姓名升序
if(s1.age == s2.age) { // 如果年龄相等
return s1.name.compareTo(s2.name); // 按照姓名排序(因为name是String类型,String类型已经继承了Comparable接口,String内部已经实现了String的比较方法compareTo()方法))
} else return s1.age - s2.age; // 如果年龄不相等,就返回差值(按照年龄升序)
}
}
/**测试TreeSet/TreeMap集合使用比较器排序添加自定义类为集合的元素*/
TreeSet<Student> treeSet4 = new TreeSet<>(new studentComparator()); // 使用自定义的比较器创建TreeSet/TreeMap集合对象排序添加自定义类为元素,这里也可以不用创建比较器类,而是使用匿名内部类的方式创建该集合对象
Student s1 = new Student("zhangsan", 16);
Student s2 = new Student("zhangsi", 17);
Student s3 = new Student("李四", 21);
// 添加自定义类至TreeSet集合
treeSet4.add(s1);
treeSet4.add(s2);
treeSet4.add(s3);
for(Student student: treeSet4) {
System.out.println(student);
}
Map类型的集合
图示:
Map集合与Collection集合没有继承关系;Map集合以key和value对存储数据,即键值对key和value都是引用数据类型;key和value都是存储对象的内存地址;key起到主导作用,value是key的附庸品;
常用方法
-
V put(K key, V value); // 往Map集合中添加元素
-
V get(Object key); // 通过key获取value,如果获取不到返回null
-
void clear(); // 清空Map集合
-
boolean containsKey(Object key); // 判断Map集合中是否存在某个key
-
boolean containsValue(Object value); // 判断Map集合中是否存在某个value
-
boolean equals(Object o); // 比较指定对象与此映射是否相等
-
boolean isEmpty(); // 判断Map集合是否为空(元素个数是否为0) 空 ? true : false
-
Set keySet(); // 获取Map集合所有的key
-
V reomve(Object key); // 通过key删除集合中的元素(删除键值对)
-
int size(); // 获取集合的键值对关系数(集合的元素个数)
-
Collection values(); // 获取Map集合所有的value
-
Set<Map.Entry<K, V>> entrySet(); // 将Map集合转换成Set集合,Set集合内的每个元素变成
key=value
的形式- 如:
-
Map集合对象:Map map1 = ???; // map1集合对象
Key Value 1 张三 2 李四 3 王五 调用entrySet()方法:Set set = map1.entrySet(); // 调用entrySet()方法变成Set集合,
1 = 张三 // 这是Set集合中的一个元素,是一个对象
2 = 李四
3 = 王五
4 = 赵六
**注意:**Map集合通过entrySet()方法转换成的这个Set集合,Set集合元素的类型是 Map.Entry<K, V>,其中Map.Entry是类名,可以理解为是一种类型的名字,Map.Entry是静态内部类,是Map下的静态内部类;Set集合下的对象(元素)的类型是Map.Entry
代码演示:
Map<Integer, String> map = new HashMap<>(); // 创建HashMap集合对象 /**向Map集合中添加键值对(元素)*/ map.put(1, "张三"); // 因为泛型的原因,这里的1是Integer类型,自动装箱机制实现 map.put(2, "李四"); map.put(3, "王五"); Set<Map.Entry<Integer, String>> mapToSet = map.entrySet();// 将Map集合全部转换为Set集合 // 通过迭代器遍历转换后的Map集合(已经变成Set集合) Iterator<Map.Entry<Integer, String>> it = mapToSet.iterator(); // 创建此Set集合的迭代器对象,注意迭代器的泛型要与Set集合的泛型保持一致 while(it.hasNext()) { Map.Entry<Integer, String> mapEntry = it.next(); // 获取这个结点 Integer k = mapEntry.getKey(); // 通过结点获取key String v = mapEntry.getValue(); // 通过结点获取value System.out.print(k + "=" + v);//相当于直接输出结点,即:System.out.print(mapEntry); if(it.hasNext()) System.out.print(", "); }
-
- 如:
HashMap集合
哈希表/散列表
![Hash表_散列表的数据结构以及HashMap的put(k, v)]方法和get(k)方法的实现原理-1615814245292](https://img-blog.csdnimg.cn/20210427224552471.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDM5NTA4,size_16,color_FFFFFF,t_70#pic_center)
可见,哈希表/散列表这种数据结构的特点:是一个一维数组,这个数组中的每个元素是一个单向链表;(数组和链表的结合体);数组:在查询方面效率高,但是在随机增删方面效率很低;单向链表:在随机增删方面效率较高,但是在查询方面效率很低;哈希表/散列表就是将以上两种数据结构融合在一起!!!
散列分布均匀
假设有100个元素,其最好能划分为10个单向链表,每个单向链表上有10个元素,这种情况是最好的,是散列分布均匀的;
哈希碰撞的产生
如果两个对象a1和a2的哈希值相同,这两个一定是在同一个链表上的(哈希值会经过哈希算法/哈希函数 转换成数组下标,对应的就是某个链表);注意:当a1和a2的哈希值不同,这两个任然有可能在同一个链表上,因为两个不同的哈希值经过哈希算法/哈希函数 有可能会转换为同一个数组下标(这就是 “哈希碰撞”);
HashMap集合的特点
- HashMap集合底层是哈希表/散列表的数据结构
- HashMap集合是非线程安全的;
- HashMap集合的Key和Value都可以为null;
- HashMap集合无序不可重复;
- 无序:因为HashMap底层是哈希表/散列表,其是数组与单向链表的结合体,虽然数组在内存空间内地址空间连续,但是在添加元素时(单向链表的结点)不能保证被添加到那个单向链表上,因此其是无序的;
- 不可重复:通过equals()方法来保证HashMap的key不可重复,如果key重复了,value会被覆盖;
- 注意:放到HashMap集合key部分的元素实际上是HashSet集合的元素,因此放在HashMap集合的key部分的元素,以及HashSet集合的元素,需要同时重写equals()和hashCode()方法;
- HashMap集合的默认初始化容量是16,默认加载因子是0.75
- 这个默认加载因子是当HashMap集合底层的数组占用容量达到容量的75%时,数组开始扩容;一次扩容之后的容量是原容量的2倍;
-
HashMap集合初始化容量必须是2的倍数,这也是官方推荐的,这是因为达到散列均匀,为了提高HashMap集合的存取效率所必须的。
- HashMap在JDK8之后的改进:为了提高检索效率,如果哈希表中的某个单向链表的元素大于8个时(static final int TREEIFY_THRESHOLD = 8),此单向链表这种数据结构就会变成红黑树(二叉树)数据结构;如果红黑树(二叉树)这种数据结构内的元素小于6个(static final int UNTREEIFY_THRESHOLD = 6),红黑树(二叉树)这种数据结构又会变成单向链表数据结构;这种机制是为了提高检索效率,二叉树的检索会再次缩小扫描范围,提高效率;
HashMap底层的源代码
public class HashMap {
Node<K, V>[] table; // HashMap底层实际上是一个数组(且是一维数组)
// 静态的内部类HashMap.Node
static class Node<K, V> implements Map.Entry<K, V> {
final int hash; // 哈希值(哈希值是通过key执行hashCode()方法的结果),哈希值可以通过哈希函数/算法能转化成数组的下标
final K key; // 存储到Map集合中的key
V value; // 存储到Map集合中的value
Node<K, V> next; // 下一个结点的内存地址 *
}
}
HashMap使用不当时无法发挥其性能
假设所有的hashCode()方法返回值为某个固定的值,那么会导致底层哈希表/散列表变成纯单向链表,这种情况就是散列分布不均匀;假设所有的haxshCode()返回值都设定为不一样的值,其底层会变成一个一维数组,没有链表的概念,也是散列分布不均匀;因此,散列分布均匀需要在重写hashCode()方法时有一定的技巧;
上述几种集合的元素重写hashCode()和equals()方法引出的问题和结论
向Map集合中存,以及从Map集合中取都会先调用key的hashCode()方法,再调用key的equals()方法!但是注意:equals()方法可能不会调用;
**以put(k, v)方法举例,什么时候equals()方法不会调用?**k.hashCode()方法获取哈希值,哈希值经过 哈希算法/函数 转换为数组下标,该下标为null时会直接将此结点添加,不会调用equals()方法;
**以get(k)方法举例,什么时候equals()方法不会调用?**k.hashCode()方法获取哈希值,哈希值经过 哈希算法/函数 转换为数组下标,该下标为null时会直接返回nulll,不会调用equals()方法;
结论
- 如果一个类的equals()方法重写了,那么这个类的hashCode()方法也必须重写,并且equals()方法返回值如果为true,hashCode()方法返回值必须要一样。(equals()方法返回true,表示两个对象相同,应该去同一个单向链表上比较,那么就必须要求同于同一个单向链表上的结点来说,其数组下标必须是一样的,而数组下标是通过哈希值经过 哈希函数/哈希算法转换而来的,那么就要求哈希值一定相同,该类的所有对象的hashCode()方法发返回值要求必须是相同的);
- 放在HashMap集合key部分的元素已经HashSet集合的元素,以及HashSet集合的元素,必须要重写equals()方法和hashCode()方法;
Hashtable集合
特点
- Hashtable集合内的元素的key和value都是不能为null的;
- Hashtable底层也是哈希表/散列表 数据结构
- 初始化容量为11,默认加载因子为0.75;扩容后新的容量 = 原容量 * 2 + 1;
- Hashtable是线程安全的;
- 但是线程安全有其他更好的方案,并且由于Hashtable集合对线程安全的处理,导致其效率较低,现在使用Hashtable已经很少了;
Properties集合
特点
- Properties是Map集合,继承Hashtable集合,Properties集合的key和value都是String类型;
- Properties被称为属性类对象,Properties是线程安全的;
常用方法
- void setProperty(String key, String value); // 会调用Hashtable的put()方法,存入一个元素
- String value = getProperty(String key); // 通过key获取value
TreeMap集合
- TreeMap底层是一个二叉树;
其他点可具体参照TreeSet集合和其他Map集合的共性
泛型
JDK5.0之后推出新特性:泛型,泛型这种语法机制,只在程序编译阶段起作用,只是给编译器参考的(运行阶段泛型没用)
**使用泛型的好处:**第一:集合中存储的元素类型统一了;第二:从集合中取出的元素是泛型指定的类型,不需要大量的向下转型;
**泛型的缺点:**导致集合中存储的元缺乏多样性;但是大多数业务中,集合内的元素是统一的,所以这种特性得到推广;
自动类型推断机制(钻石表达式)
JDK8.0之后引入了自动类型推断机制(钻石表达式)如:List<Animal> l = new ArrayList<>();
自定义泛型
自定义泛型时 <> 中是一个标识符,随便写;泛型 <> 内最常使用的英文是E和T;E表示Element,T表示Type;
如下代码是一个自定义泛型和使用的例子:
package edu.hebeu.generic;
/**
* 自定义泛型
*/
public class GenericsCustom<随便写> {
public void doSome(随便写 t) {
System.out.println("doSome()方法:" + t);
}
public 随便写 returnSome(随便写 t) {
return t;
}
public static void main(String[] args) {
GenericsCustom<String> gc = new GenericsCustom<>();
// gc.doSome(210); // 此时编译器会报错
gc.doSome("泛型学习");
// Integer a = gc.returnSome(200); // 此时编译器会报错
// int b = gc.returnSome(58); // 此时编译器会报错
// String c = gc.returnSome(5.8); // 此时编译器会报错
String d = gc.returnSome("泛型,泛型,泛型");
System.out.println(d);
/**如果定义泛型,但是没有使用,Java会将默认为Object类型*/
GenericsCustom gc2 = new GenericsCustom();
gc2.doSome("泛型");
// String a = gc2.returnSome("你好"); // 此时编译器会报错
// String b = gc2.returnSome(500); // 此时编译器会报错
// String c = gc2.returnSome(new Object()); // 此时编译器会报错
Object o = gc2.returnSome(new Object());
System.out.println(o);
}
}
IO流
什么是IO?:Input; O:Output;通过IO可以完成文件的读和写;
IO流的分类,多种分类方式
-
方式一:按照流的方向分类,以内存作为参照物,往内存中去叫做 输入/读,从内存中出来叫做 输出/写;
-
方式二:按照读取数据方式不同进行分类,有的流是按照字节的方式读取,一次读取一个字节byte,等同于一次读取8个二进制位,这种流是万能的,什么流都能读取(包括文本文件、图片、声音、视频等等),叫做字节流;
- 如:有一个普通文本文件 a.txt 内部有 ab普通123 数据,通过字符流是这样读的:“第一次读一个字节:正好读到 a;第二次读一个字节,正好读到 b;第三次读一个字节,正好读到 普 的一半;第四次读一个字节,正好读到 普 的另外一半;…”
-
方式三:按照字符的方式读取,一次读取一个字符,这种流是为了方便读取普通文本文件而存在的,这种流不能读取图片、声音、视频等(甚至是word等文件都无法读取),只能读取普通文本文件,叫做字符流;
- 如:有一个普通文本文件 a.txt 内部有 ab普通123 数据,通过字符流是这样读的:“第一次读取 a;第二次读取 b;第三次读取 普 ;第四次读取 通;第五次读取 1;第六次读取 2;第七次读取 3;”
- 注意:字母、数字在windows系统中占用1个字节;汉字在windows系统中占用2个字节;
综上所示流的分类包括:输入流、输出流、字节流、字符流;注意:Java中的IO流都已经写好了,程序员不用关系底层,只要调用,Java所有的流都在java.io包下;
Java中IO流的四大家族
JavaIO流的四大家族:Java中以 Stream结尾的都是字节流;以 Reader、Writer结尾的都是字符流!!!四大家族的顶级类如下表所示:
类 | 流类型 |
---|---|
java.io.InputStream | 字节输入流 |
java.io.OutputStream | 字节输出流 |
java.io.Reader | 字符输入流 |
java.io.Writer | 字符输出流 |
上述的四大流的顶级类都是抽象类(InputStream、OutputStream、Reader、Writer)
四个顶级流都实现了:java.io.Closeable接口,因此所有流都是可关闭的,都实现了java.io.Closeable接口,所有流都有close()方法;还要注意:流是管道,所以所有流用完都要close()关闭不然会耗费很多资源;
所有的输出类型的顶级流都实现了:java.io.Flushable接口,因此所有输出流都是可刷新的,都实现了java.io.Flushable接口,所有输出流都有flush()方法;输出流在最终输出之后,一定要flush()刷新一下,这个刷新表示将管道内剩余为输出的数据强行输出完(清空管道),刷新的作用就是清空管道;注意:如果没有flush()可能会导致丢数据;
重点掌握的16个流
文件专属流
- java.io.FileInputStream
- java.io.FileOutputStream
- java.io.FileReader
- java.io.FileWriter
转换流(将字节流转换为字符流的)
- java.io.InputStreamReader
- java.io.OutputStreamWriter
缓冲流专属
- java.io.BufferedReader
- java.io.BufferedWriter
- java.io.BufferedInputStream
- java.io.BufferedOutputStream
数据流专属
- java.io.DataInputStream
- java.io.DataOutputStream
标准输出流
- java.io.PrintWriter
- java.io.PrintStream
对象专属流
- java.io.ObjectInputStream
- java.io.ObjectOutputStream
FileInputStream
特点:
- 这是文件字节输入流,是万能的,任何类型的文件都可以采用这个类型的流来读;
- 以字节的方式完成输入操作,即完成读的操作(硬盘 —> 内存);
构造方法:
FileInputStream fis = new FileInputStream(String readFilePath); // 注意:路径的\
在Java中是转义符,应该使用 \\
或/
常用方法:
- close(); // 关闭流,如代码:
fis.close(); // 关闭流
- int available(); // 返回流当中剩余的未读到的字节数量,如代码:
System.err.println("fis本次读了" + readBytesCount2 + "个字节;还剩" + fis.available() + "个字节未读");
- long skip[(long n); // 跳过几个字节不读,如代码:
fis.skip(5); // 跳过5个字节不读
- int read(); // 读一个字节,将栈帧后移1位,并将当前栈帧指向的字节读取,返回该字节的值,如果流读取完了就返回 -1;如代码:
int readData = fis.read();
- int read(byte[] b); // 一次最多读取b.length个字节至byte数组b,如果流读取完了就返回 -1;如代码:
byte[] bytes3 = new byte[10];fis.read(bytes3)) != -1
代码演示关键部分:
FileInputStream fis = new FileInputStream("data/streamData/数据1");
byte[] bytes3 = new byte[10]; // 准备一个长度为6的byte数组,表示每次读取6个字节的内容
int readBytesCount3 = 0; // 声明一个读取字节的数量,默认为0
while((readBytesCount3 = fis.read(bytes3)) != -1) { // 表示如果能读到字节
System.out.print(new String(bytes3, 0, readBytesCount3)); // 将读到的字节打印到控制台
}
FileOutPutStream
**特点:**参考FileInputStream
构造方法:
- FileOutputStream fos = new FileOutputStream(String writeFilepath));
- 写入文件时如果不存在此文件会自动创建,但是这种构造方法创建的对象在写入文件时,会先将原文件清空掉,在将新数据写入文件,注意路径格式同FileInputStream ;
- FileOutputStream fos = new FileOutputStream(String writeFilepath, boolean isReWriter);
- 同上述的构造方法,但是这种构造方法创建的对象在写入文件时,会讲所写的内容追加到原文件末尾,不会清空原文件内容;
常用方法:
- write(byte[] bytes); // 将byte数组的每个元素对应的值写入文件
- flush(); // 刷新管道
- close(); // 关闭流资源
代码演示关键部分:
FileOutputStream fos = new FileOutputStream("data/streamData/写入数据", true); //
/**写入操作**/
// 1.1、通过int类型的数据(即byte的值)写入文件
/*fos.write(98);*/
// 1.2、创建byte数组写入文件(将byte数组全部写入)
/*byte[] bytes = {97, 98, 99, 100, 101, 102, 103, 104};
fos.write(bytes); // 将此数组写入文件
*/
// 1.3、将byte数组部分写入文件
/*byte[] bytes = {97, 98, 99, 100, 101, 102, 103, 104};
fos.write(bytes, 0, 3); // 表示将byte数组从下标为0的位置,写入长度是3,写入文件
*/
String s = "你好!我要成功了!!!";
byte[] bytes = s.getBytes(); // 讲String字符串转换为byte数组
fos.write(bytes); // 将字符串转换成的byte数组写入到文件中
// 写完之后一定要刷新一下,以清空管道
fos.flush();
文件拷贝原理
文件复制的案例
这个例子演示如何通过前面两个Stream类型的流实现文件拷贝,代码如下:
package edu.hebeu.copy;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/**
* 这个例子展示如何通过FiltInputStream和FileOutputStream拷贝文件
* 这个例子使用的是Stream流,什么样的文件都能拷贝
*/
public class FileCopyUseStream {
public static void main(String[] args) {
FileInputStream fis = null; // 声明一个输入流对象
FileOutputStream fos = null; // 声明一个输出流对象
try {
fis = new FileInputStream("F:\\学习\\javaEE\\第二次课\\第6周(第2次课).mp4"); // 创建一个输入流对象
fos = new FileOutputStream("D:\\programme\\code\\java\\neon\\IOStu\\data\\streamData\\拷贝视频.mp4"); // 创建一个输出流对象
/**
* 进行读写操作(边读边写)
*/
byte[] bytes = new byte[1024 * 1024]; // 一次最多读取 1MB 字节
int readBytesCount = 0; // 用来存放每次读取入byte数组的字节数量
while((readBytesCount = fis.read(bytes)) != - 1) {
fos.write(bytes, 0, readBytesCount); // 将每次读取到的字节写入
}
fos.flush(); // 输出流对象使用完后要进行刷新
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fos != null) {
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
FileReader
特点:
- 文件字符输入流,只能读取普通文本文件;
- 读取文件内容时,比较方便快捷
构造方法:
- FileReader fr = new FileReader(String readFilePath)
常用方法:
- int read(char[] chars); // 每次最多读取 chars.length 个字符,如果流读取完了就返回-1
- close();
- 其他方法可类比其他流
代码演示:只演示关键代码
fr = new FileReader("D:\\programme\\code\\java\\neon\\IOStu\\data\\read_writerData\\数据1"); // 创建字符输入流
char[] chars = new char[4]; // 创建一个字节数组,用来存放每次读取的数据,每次最多读取4个字符
int readCharsCount = 0; // 用来记录每次读取到的字符数
while((readCharsCount = fr.read(chars)) != -1) {
System.out.print(new String(chars, 0, readCharsCount)); // 将每次读取入字符数组的字符转换为String类型输出
}
FileWriter
特点:
- 字符输出流,只能输出普通文本文件;
- 其他可以参考FileReader
构造方法:
- FileWriter fw = new FileWriter(String writerFilePath); // 可以参考FileOutPutStream
- FileWriter fw = new FileWriter(String writerFilePath, boolean isReWriter); // 可以参考FileOutPutStream
常用方法:
- write(char[] chars, int subScript, int writerLength)
- 表示写入一个char数组,也可以从该数组下标为subScript开始,长度writerLength开始写入;注意:write("\n"),表示写入换行
- writer(String s); // 直接写入一个字符串
- flush(); // 刷新管道
- close(); // 关闭流资源
代码演示关键部分:
FileWriter fw = new FileWriter("data\\read_writerData\\写入数据", true); // 创建字符输出流对象,这个对象写入文件不会清空原文件,而是追加至原文件内容后面;
/*写入操作*/
char[] chars = {'你', '好', 'J', 'a', 'v', 'a', '!', '!', '!'};
// fw.write(chars); // 写入字符数组的全部内容
fw.write(chars, 1, 2); // 写入字符数组从下标 1 开始,长度为2的内容
fw.write("\n"); // 写入换行
String s = "通过String类型数据随便写点";
// fw.write(s); // 直接传入字符串
fw.write(s, 0, 5); // 写入String类型数据的部分内容
fw.flush(); // 输出流用完之后记得要刷新,以清空管道
普通文本文件复制案例
通过前面的连个字符流实现的普通文本文件复制案例,代码如下:
package edu.hebeu.copy;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
/**
* 这个例子展示如何使用FileReader和FileWriter拷贝普通文本文件,这两个流只能操作普通的文本文件
*/
public class FileCopyUseReaderWriter {
public static void main(String[] args) {
FileReader fr = null; // 声明字符输入流
FileWriter fw = null; // 声明字符输出流
try {
fr = new FileReader("F:\\学习\\javaEE\\第十一次课\\backup"); // 创建字符输入流对象
fw = new FileWriter("D:\\programme\\code\\java\\neon\\IOStu\\data\\read_writerData\\拷贝文件"); // 创建字符输出流对象
/**
* 进行拷贝操作
*/
char[] chars = new char[1024 * 512]; // 创建字符数组,存储每次读取的字符,表示一次最多读取1MB
int readCharsCount = 0; // 用来存放每次读取入字符数组的字符数
while((readCharsCount = fr.read(chars)) != -1) {
fw.write(chars, 0, readCharsCount); // 将每次读取到的字符写入
}
fw.flush(); // 字符输出流对象使用完之后要进行刷新,以清空管道
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(fr != null) {
try {
fr.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fw != null) {
try {
fw.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
BufferedReader
**特点:**带有缓冲区的输入流;使用这个流不用自定义char/byte数组,自带缓冲;构造方法只能传入Reader,即只能使用字符流,对于字节流,要先转换成字符流才能使用BufferedReader;
构造方法:
- new BufferedReader(Reader r);
- 创建BufferedReader对象需要Reader对象(字符流),但是Reader是抽象的,因此Reader可以借助其子类创建对象;
特有方法:
- String readLine(); // 读取一行文本行,但是不会读到最后的换行(不带换行符)
- 其他的通用方法可以参考前面的几种流得出
引入概念:当一个流的构造方法需要一个流时,被传进来的流叫做==“节点流”;外部负责包装的流叫做"包装流"==;
代码演示:
package edu.hebeu.bufferedreader;
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
/**
* BufferedReader:带有缓冲区的输入流;
*/
public class BufferedReaderStu {
public static void main(String[] args) {
BufferedReader br = null; // 声明一个包装流对象
try {
/**对于字符流*/
// Reader reader = new FileReader("data\\bufferedData\\数据1"); // 创建Reader对象
// FileReader reader = new FileReader("data\\bufferedData\\数据1"); // 也可以创建Reader的子类的对象
// br = new BufferedReader(reader); // 通过Reader对象创建BufferedReader对象
// 可以合并为
// br = new BufferedReader(new FileReader("data\\bufferedData\\数据1"));
/**对于字节流*/
// FileInputStream fis = new FileInputStream("data\\bufferedData\\数据1"); // 创建字节流对象
// InputStreamReader isr = new InputStreamReader(fis); // 将字节流对象(InputStream)转换成字符流对象(Reader)
// br = new BufferedReader(isr); // 将字节流FileInputStream转换成的Reader字符流对象传入BufferedReader对象,创建BufferedReader对象
// 可以合并为
br = new BufferedReader(new InputStreamReader(new FileInputStream("data\\bufferedData\\数据1")));
/**
* 上述中,相对于BufferedReader,Reader或者Reader的子类FileReader等的叫做节点流;
* BufferedReader叫做包装流或者处理流;
* 相对于InputStreamReader,FileInputStream是节点流;FileInputStream叫做包装流或
* 者处理流;
*
* 但是在关闭流时,只需要关闭最外面的包装流即可,因为观察源代码结点流会在包装流内部
* 被处理关闭关闭;
*/
/**
* 读操作
*/
// String textLine = br.readLine(); // 读取第一行数据,但是不会读到最后的换行
// System.out.println(textLine);
String textLine = null; // 存储读出的每一行文本数据
while((textLine = br.readLine()) != null) {
System.out.println(textLine);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
/**关闭这种流时,只需要关闭包装流/处理流即可,节点流不用关,因为节点流在包装流内部已经处理关闭了*/
if(br != null) {
try {
br.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
BufferedWriter
**特点:**参考BufferdReader;
构造方法:
- BufferedWriter bw = new BufferedWriter(Writer writer);
**方法:**参考BufferedReader以及其他的输出流
代码演示:
package edu.hebeu.bufferedwriter;
import java.io.BufferedWriter;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
/* BufferedWriter:带有缓冲区的字符输出流;*/
public class BufferedWriterStu {
public static void main(String[] args) {
BufferedWriter bw = null;
try {
/**创建BufferedWriter对象*/
// bw = new BufferedWriter(new FileWriter("data\\bufferedData\\写入数据", true)); // 创建BufferedWriter对象(当节点流是字符流时),并且不会清空原文件的数据
bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream("data\\bufferedData\\写入数据", true))); // 创建BufferedWriter对象(当节点流是字节流时,要将字节流通过OutputStreamWriter转换成字符流),并且不会清空原文件的数据
/**
* 写操作,write()方法的使用同FileOutStream的write()方法一样
*/
char[] chars = {'c', 'h', 'a', 'r', 'a', 'r', 'r', 'a', 'y'};
bw.write(chars); // 写入一个完整的char数组
bw.write("\n"); // 写入一个换行
bw.write(chars, 1, 5); // 写入一个部分的char数组,从下标为1开始,5个长度结束
bw.write("\n"); // 写入一个换行
bw.write(97); // 写入一个byte值为97对应的数据(即a)
bw.write("\n"); // 写入一个换行
bw.write("你好!Java的流;"); // 写入一个字符串
bw.write("你好!Java的流;", 0, 2); // 写入一个字符串的一部分从下标0开始,2个长度为止
bw.write("\n"); // 写入一个换行
bw.flush(); // 写完之后,刷新一下,清空管道
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(bw != null) {
try {
bw.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
DataInputStream
**特点:**数据字节输入流;DataOutputStream写的文件只能用DataInputStream读,并且要提前知道写入的顺序;加密方式一致(读的顺序与写的顺序要一致),才可以正常读取出数据;
代码演示:
package edu.hebeu.datastream;
import java.io.DataInputStream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
/*java.io.DataInputStream:数据字节输入流;*/
public class DataInputStreamStu {
public static void main(String[] args) {
DataInputStream dis = null;
try {
dis = new DataInputStream(new FileInputStream("data//dataStreamData//写入数据")); // 创建DataInputStream读文件对象
/**
* 进行读操作
*/
byte b = dis.readByte();
short s = dis.readShort();
int i = dis.readInt();
long l = dis.readLong();
float f = dis.readFloat();
double d = dis.readDouble();
boolean boo = dis.readBoolean();
char c = dis.readChar();
System.out.println(b);
System.out.println(s);
System.out.println(i);
System.out.println(l);
System.out.println(f);
System.out.println(d);
System.out.println(boo);
System.out.println(c);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(dis != null) {
try {
dis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
DataOutputStream
**特点:**java.io.DataOutputStream这个流可以将数据连同数据类型一并写入文件当中。注意:这个写入的文件不是普通文本文档(记事本打不开,打开之后是乱码的);
代码演示:
package edu.hebeu.datastream;
import java.io.DataOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/*java.io.DataOutputStream:*/
public class DataOutputStreamStu {
public static void main(String[] args) {
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new FileOutputStream("data\\dataStreamData\\写入数据"));
/**
* 写入操作
*/
byte b = 98;
short s = 101;
int i = 1111;
long l = 3045655L;
float f = 3.1F;
double d = 3.1415926;
boolean bo = false;
char c = 't';
// 写入各种类型的数据
dos.writeByte(b);
dos.writeShort(s);
dos.writeInt(i);
dos.writeLong(l);
dos.writeFloat(f);
dos.writeDouble(d);
dos.writeBoolean(bo);
dos.writeChar(c);
dos.flush(); // 写入操作完成后调用刷新方法,清空管道
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(dos != null) {
try {
dos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
PrintStream
**特点:**标准字节输出流java.io.PrintStream,默认输出到控制台;标准字节输出流不用手动关闭;
代码演示:
package edu.hebeu.printstream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.SimpleDateFormat;
import java.util.Date;
/*本例子学习标准字节输出流 java.io.PrintStream*/
public class PrintStreamStu {
public static void main(String[] args) {
System.out.println("Hello"); // 这就是使用的标准字节输出流
// 另一种写法
PrintStream pStream = System.out;
pStream.println("你好!");
pStream.println("Java");
/**
* 以上都是输出至控制台,也可以改变标准字节输出流的输出方向
*/
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("data\\printData\\log", true)); // 将此输出流的方向改变至 data\\printData\\log 文件,不在指向控制台
System.setOut(ps); // 通过上面的对象修改输出方向,将此输出流的方向改变至 data\\printData\\log 文件,不在指向控制台
/**
* 写操作,再输出
*/
Date now = new Date(); // 获取系统当前时间
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS"); // 创建时间格式化对象
String nowStr = sdf.format(now); // 将时间格式化为一个字符串
System.out.println("当前时间:" + nowStr);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
PrintWriter
这个流和上面的流类似,不要求掌握,可以参考其他类型的流或查阅文档学习
File类的理解
**File类和流的四大家族没有关系,所以其不能完成文件的读写,File对象表示什么?**如:“File是一个文件或目录路径名的抽象表示形式;D:\programme 是一个File对象;C:\index.txt 也是一个对象;因此:File对象有可能是目录,也可能是文件,File只是一个路径的抽象表示形式”
构造方法:
- File file = new File(String path); // path路径可以是目录,也可以是文件
File类常用的方法
-
boolean exists(); // 判断是否存在File对象
-
void createNewFile(); // 以文件的形式创建此File对象(即在File对象的指定路径下创建文件)
-
void mkdir(); // 以目录的形式创建此File对象(即在File对象的指定路径下创建文件)
-
void mkdirs(); // 以多重目录的形式创建此File对象(即在File对象的指定路径下创建文件)
-
String getParent(); // 获取当前File对象的父路径
-
File getParentFlie(); // 获取当前File对象的父File对象
-
String getAbsolutePath(); // 获取当前File对象的绝对路径
-
boolean delete(); // 删除当前File对象(由File对象指定的文件或目录)
-
String getName(); // 获取当前File对象的文件名
-
boolean isDirectory(); // 判断当前File对象是否为目录
-
boolean isFile(); // 判断当前File对象是否为文件
-
boolean isHidden(); // 判断当前File对象是否为隐藏文件
-
long lastModified(); // 获取当前File对象最后一次的修改时间(毫秒:1970-1-1 0:0:0 000开始到现在的毫秒数)
-
long length(); // 获取当前File对象的大小(字节)
-
boolean renameTo(File dest); // 充命名当前File对象的路径 成 dest路径
-
File[] listFiles(); // 获取当前File对象下的所有子文件对象
目录拷贝案例
这个例子展示通过File和流配合使用完成目录的拷贝,代码如下:*
package edu.hebeu.copy;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
public class MkdirCopy {
public static void main(String[] args) {
File srcFile = new File("F:\\学习\\C++"); // 创建拷贝源对象
File destFile = new File("D:\\programme\\code\\java\\neon\\IOStu\\data\\mkdir\\dest"); // 创建拷贝目标
copyMkdir(srcFile, destFile); // 调用拷贝目录静态方法
}
/**
* 拷贝目录方法
* @param srcFile 拷贝源
* @param destFile 拷贝目标
*/
public static void copyMkdir(File srcFile, File destFile) {
if(srcFile.isFile()) { /**如果拷贝源文件是文件*/
/**进行拷贝文件*/
FileInputStream fis = null;
FileOutputStream fos = null;
try {
fis = new FileInputStream(srcFile); // 通过拷贝源的File对象创建FileInputStream字节输入流对象
String destFilePath = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsoluteFile() + "\\") + srcFile.getAbsolutePath().substring(3);// 获取拷贝的目标文件路径,substring(3)表示将拷贝源文件对象的路径从第3位开始截取(即不包含转义符,截取盘符,如截取:" D:/ ")
fos = new FileOutputStream(destFilePath); // 通过拷贝目标的File对象创建FileOutputStream字节输出流对象
byte[] bytes = new byte[1024 * 1024]; // 一次最多读取1MB字节
int readBytesCount = 0; // 存储每次读取到字节数组的字节数
while((readBytesCount = fis.read(bytes)) != -1) {
fos.write(bytes, 0, readBytesCount); // 进行写操作
}
fos.flush(); // 输出流用完之后进行刷新,清空管道
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(fis != null) {
try {
fis.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
if(fos != null) {
try {
fos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
return; // 结束递归
}
System.out.println("长度============:");
/**程序执行到此说明拷贝源是目录*/
File[] childFiles = srcFile.listFiles();
System.out.println("长度:" + childFiles.length);
for(File childFile : childFiles) { // 获取拷贝源目录的子文件对象
// System.out.println(childFile.getAbsolutePath());
if(childFile.isDirectory()) { // 如果拷贝源文件的这个子文件是目录
// 新建对应目录
String srcMkdir = childFile.getAbsolutePath(); // 获取拷贝的源目录
String destMkdir = (destFile.getAbsolutePath().endsWith("\\") ? destFile.getAbsolutePath() : destFile.getAbsoluteFile() + "\\") + srcMkdir.substring(3); // 获取拷贝的目标目录,substring(3)表示将拷贝源文件对象的路径从第3位开始截取(即不包含转义符,截取盘符,如截取:" D:/ ")
File newFile = new File(destMkdir); // 通过目标目录创建File对象
if(!newFile.exists()) { // 如果这个File对象不存在
newFile.mkdirs(); // 通过这个File对象创建目录
}
}
copyMkdir(childFile, destFile); // 进行递归
}
}
}
关于路径
问题的引入
在前面的代码中路径都是这样写的:".../.../db.properties"
,这种方式直接填写路径的缺点是:移植性差,如果程序的位置发生改变甚至是操作系统发生了改变,这个填入的路径将面临失效的问题,此时还要修改源代码来保证程序的正常执行!
解决
方案一、我们可以使用这种方式解决上面的问题,获取该文件的绝对路径:String absPath = Thread.currentThread().getContextClassLoader().getResource("db.properties").getPath(); // 获取在类路径下(src下)的 config\\db.properties 文件的绝对路径
,使用一种比较通用的方式获取路径,即使是代码位置改变甚至操作系统改变,也不会出现路径失效的问题;
代码的解读:
-
Thread.currentThread(); // 获取当前线程
-
getContextClassLoader(); // 线程对象的方法,获取当前线程的类加载器对象
-
getResource(String classPath); // 这是类加载器对象的方法,通过当前线程的类加载器对象获取 classPath 文件对象(注意:当前线程的类加载器默认从类的根路径下(src下)加载资源!)
-
getPath(); // 文件对象的方法,获取这个文件对象的绝对路径
方案二、可以直接获取指定路径的文件内容的字节流:
InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream("db.properties"); // 直接以字节流的形式返回文件的内容
代码的解读:
- Thread.currentThread(); // 获取当前线程
- getContextClassLoader(); // 线程对象的方法,获取当前线程的类加载器对象
- getResourceAsStream(String classPath); // 类加载器对象的方法,通过当前线程的类加载器对象获取 classPath 文件对象内容的字节流(注意:当前线程的类加载器默认从类的根路径下(src下)加载资源!)
ResourceBundle资源绑定器的引入
Sun公司提供的ResourceBundle资源绑定器,在java.util包下;使用这种方式获取资源时的要求:
- 这个文件必须是以properties结尾的;
- 这个文件必须在类路径下(src下);
- 加载xxx.properties文件时,写路径只能写到 xxx,后缀 properties不能写;
代码演示:
package edu.hebeu.aboutpath;
import java.util.ResourceBundle;
/*这个例子讲解Sun公司提供的ResourceBundle资源绑定器的使用*/
public class ResourceBundleStu {
public static void main(String[] args) {
ResourceBundle bundle = ResourceBundle.getBundle("info\\classInfo"); // 注意写到文件的名字就行了,不能写后缀
String classInfoOfClassName = bundle.getString("className"); // 通过properties文件的 = 左边的属性值(key),获取 = 右边的属性值(value)
System.out.println(classInfoOfClassName);
}
}
classInfo.properties文件的内容:
className=edu.hebeu.entity.Student
序列化与反序列化
参与序列化和反序列化的类如下代码所示:
package edu.hebeu.objectstream;
import java.io.Serializable;
/*参与序列化和反序列化的类 学生类*/
public class ObjStudent implements Serializable{
private static final long serivalVersionUID = 1L; // 手动生成一个序列化版本号
private String num;
private transient String name; // 如果不希望这个属性序列化,可以加关键字transient,表示游离的
private int age;
// 序列化完成之后添加的代码
private int sex;
public ObjStudent() {
super();
}
public ObjStudent(String num, String name, int age) {
super();
this.num = num;
this.name = name;
this.age = age;
}
public String getNum() {
return num;
}
public void setNum(String num) {
this.num = num;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "ObjStudent [num=" + num + ", name=" + name + ", age=" + age + "]";
}
}
注意点:
- 如果没有没有手动写出来序列化版本号,Java默认提供序列化版本号
- 通过源代码发现,Serializable接口只是一个标志接口,内部什么代码也没有,起到标识符作用,Java虚拟机可能会对这个类特殊待遇;
- Serializable这个接口是给Java虚拟机参考的,Java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号;
- 如果不希望某个属性序列化,可以给这个属性加关键字transient,表示游离的,不参与序列化操作
序列化的实现以及ObjectOutputStream
**序列化的实现:**使用ObjectOutputStream流实现,参与序列化和反序列化的类必须实现Serializable接口,否则会出现异常:java.io.NotSerializableException;(即不能序列化)
序列化的实现代码:
package edu.hebeu.objectstream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
/*使用ObjectOutputStream流实现 序列化 */
public class ObjectOutputStreamStu {
public static void main(String[] args) {
ObjStudent os = new ObjStudent("1365", "test", 20);
ObjectOutputStream oos = null; // 声明ObjectOutputStream对象
try {
oos = new ObjectOutputStream(new FileOutputStream("data\\serializable\\student"));// 创建ObjectOutputStream对象
oos.writeObject(os); // 序列化对象(序列化一个)
oos.flush(); // 使用完输出流之后要刷新,清空管道
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(oos != null) {
try {
oos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
反序列化的实现以及ObjectInputStream
**反序列化的实现:**使用ObjectInputStream流实现,参与序列化和反序列化的类必须实现Serializable接口,否则会出现异常:java.io.NotSerializableException;(即不能序列化)
反序列化实现的代码:
package edu.hebeu.objectstream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
/*ObjectInputStream流实现 反序列化 */
public class ObjectInputStreamStu {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("data\\serializable\\student"));
Object obj = ois.readObject(); // 进行反序列化
System.out.println("反序列化之后的结果:" + obj);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(ois != null) {
try {
ois.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
**注意:**通过源代码发现,Serializable接口只是一个标志接口,内部什么代码也没有,起到标识符作用,Java虚拟机可能会对这个类特殊待遇;即Serializable这个接口是给Java虚拟机参考的,Java虚拟机看到这个接口之后,会为该类自动生成一个序列化版本号;
序列化版本号的作用
先看一个现象:**当序列化完成后,又改动代码,**改动代码如下:
stream classdesc serialVersionUID = -2623336853608940719, // 这是改动之后
local class serialVersionUID = -1160564518340679496 // 这是改动之前
在进行反序列化会出现如下异常:
`java.io.InvalidClassException: edu.hebeu.objectstream.ObjStudent; local class incompatible:
在看一个例子:
A程序员编写了类 edu.hebeu.test.Test
;B程序员编写了类 edu.hebeu.test.Test
;
不同的人编写了同一个名字的类(这两个类确实是不一样的),此时同名的类要让JVM区分,序列化版本号就派上用场了。对于Java虚拟机JVM来说,它是可以区分这两个类的,因为这两个类都实现了Serializable接口,都有默认的序列化版本号,它们的序列化版本号不同,就区分开了(不可否认这是自动生成序列化版本号的好处),但是自动生成序列化版本号这种方式是不建议的,其缺陷:1、一旦代码生成之后(序列化之后),不能进行后续的更改,因为只要更改,必然会重新编译,此时JVM会生成全新的序列化版本号,此时Java虚拟机就会将其认为是一个全新的类(这样子不好),如果不重新序列化,JVM会认为修改后的类是全新的,不能进行反序列化操作了;
再提出问题:**Java靠什么区分类?**第一:首先通过类名比较,如果类名不一样,肯定不是同一个类;第二:如果类名一样,会通过序列化版本号比较这两个类是不是一个类;
结论:
- 如果序列化版本号是通过JVM虚拟机自动生成的,在序列化完成之后改动代码,class运行之后JVM在就会重新生成序列化版本号,因为版本号的不同JVM就不会识别之前序列化之后的对象;就会出现以上的异常信息;
- 序列化版本号建议程序员写出来;(可以放心的写,只要保证同类名的类的序列化版本号不一样就可以);因为这样做会在一个类的对象序列化之后,改动代码,再进行反序列化不会出现上面的异常;
- 凡是一个类实现了Serializable接口,建议给这个类手动提供一个不变的序列化版本号,而不是通过JVM自动生成的序列化版本号;这样,这个类在序列化之后,即使更改代码,但是版本号不变,JVM还会认为其是同一个类,保证反序列化能够成功;
序列化多个对象
**序列化多个对象的实现:**将需要进行序列化的对象放到集合中,序列化集合即可;
注意:
- 参与序列化的集合、类都需要实现Serializable接口!!!
- 序列化对象时,存储多个对象,存储第二个会报错!!!
- 集合类已经实现了Serializable接口(所以可以借助集合类实现多个对象的序列化)!!!
代码实现:
package edu.hebeu.objectstream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.List;
/*序列化多个对象*/
public class SerrializableMoreObj {
public static void main(String[] args) {
List<ObjStudent> studentList = new ArrayList<>(); // 创建ObjStudent泛型的集合
// 创建对象
ObjStudent os1 = new ObjStudent("4155", "test01", 21);
ObjStudent os2 = new ObjStudent("4556", "test02", 20);
ObjStudent os3 = new ObjStudent("4542", "test03", 20);
ObjStudent os4 = new ObjStudent("2323", "test04", 22);
ObjStudent os5 = new ObjStudent("4155", "test05", 21);
ObjStudent os6 = new ObjStudent("1242", "test06", 24);
ObjStudent os7 = new ObjStudent("1215", "test07", 19);
ObjStudent os8 = new ObjStudent("5674", "test08", 20);
// 添加至集合
studentList.add(os1);
studentList.add(os2);
studentList.add(os3);
studentList.add(os4);
studentList.add(os5);
studentList.add(os6);
studentList.add(os7);
studentList.add(os8);
ObjectOutputStream oos = null; // 声明ObjectOutputStream类对象
try {
oos = new ObjectOutputStream(new FileOutputStream("data\\serializable\\students")); // 创建ObjectOutputStream类对象
// 序列化集合(序列化多个对象)
oos.writeObject(studentList);
oos.flush(); // 输出流用完之后要刷新,清空管道
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(oos != null) {
try {
oos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
反序列化多个对象
实现与注意点参照上述;
实现代码:
package edu.hebeu.objectstream;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.List;
/*反序列化多个对象*/
public class DeSerrializableMoreObj {
public static void main(String[] args) {
ObjectInputStream ois = null;
try {
ois = new ObjectInputStream(new FileInputStream("data\\serializable\\students"));
// 反序列化多个对象
// Object obj = ois.readObject();
// System.out.print("反序列化后的返回值是否为集合:"); System.out.println(obj instanceof List);
System.out.println("反序列化多个对象的结果:");
List<ObjStudent> studentList = (List<ObjStudent>)ois.readObject(); // 将返回值为Object的类向下转型为List集合
for(ObjStudent student : studentList) {
System.out.println(student);
}
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(ois != null) {
try {
ois.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
IO与Properties集合的联用
设计开发理念:以后经常改变的数据,可以单独写到一个文件中,使用程序动态读取;将来在进行修改时只要修改文件的内容,不用更改Java代码,不需要重新编译,部署服务器即可拿到修改后的信息;类似于上述的文件是配置文件,并且当文件内容是:
key1=value1
key2=value2
......
我们称上述类型的文件为属性配置文件,建议属性配置文件以 properties结尾(但不是必须的),其中Properties Map集合是专门存储属性配置文件内容的;
属性配置文件须知:[1]. # 是注释;[2]. key重复,value会自动覆盖;[3]. 以等号为界,等号左边是key,右边是value;等号两边可以有空格(但最好不要有空格);
代码实现:
package edu.hebeu.ioandproperties;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
/*Properties和IO的联合使用;*/
public class IOANDProperties {
public static void main(String[] args) {
FileReader fr = null;
try {
fr = new FileReader("data\\properties\\dataBaseInfo");
Properties properties = new Properties(); // 创建Properties的Map集合
properties.load(fr); // 将流通过Properties的load()方法加载到Properties集合
/**Properties集合的key和value都要求是String类型,并且传入的流以等号为分割线,等号左边做key,右边做value,key重复,value覆盖!!*/
String uName = properties.getProperty("username");
String pwd = properties.getProperty("password");
String driver = properties.getProperty("driver");
String url = properties.getProperty("url");
System.out.println("用户名:" + uName + "; 密码:" + pwd + "; 驱动:" + driver + "; URL:" + url);
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
dataBaseInfo文件的内容:
username=root
password=0727316052
driver=com.mysql.jdbc.Driver
url=jdbc:mysql//127.0.0.1:3306/test
password=072731
线程
概述:**一个进程是一个应用程序;线程是一个进程中的执行场景/执行单元;一个进程可以启动多个线程;**例如:对于Java程序来说,如当在DOS命令窗口中输入:java HelloWord,并回车之后,会先启动JVM,而JVM就是一个进程;JVM再启动一个主线程调用main方法,同时再启动一个垃圾回收线程负责看护、回收垃圾,因此在Java程序中至少有两个线程;一个是执行main方法的主线程,一个是垃圾回收线程;
进程和线程的关系
进程A和进程B的内存独立不共享;在Java中,线程A和线程B堆内存和方法区内存共享;但是栈内存独立,一个线程一个栈;(即启动10个线程,会有10个栈空间,每个栈于每个栈之间互不干扰,各自执行各自的,这就是多线程并发);Java中之所以有多线程机制,目的就是为了提高程序的执行效率;
*结论:*使用了多线程机制后,main方法结束只是主线程结束,主栈空了,其他的栈(线程)可能还在压栈弹栈等操作;
多线程并发
什么是真正的多线程并发?
t1执行t1的;t2执行t2的;t1与t2之间互不影响,互不干涉;这叫做真正的多线程并发。
对于单核的CPU计算机来说,真的可以做到真正的多线程并发吗?
对于多核CPU的计算机来说,真正的多线程并发是没有问题的(如4和CPU计算机表示同一个时间点上,可以真正的有4个进程并发执行);对于单核CPU的计算机来说,不能够做的真正的多线程并发,但是可以给人一种多线程并发的感觉!对于单核CPU计算机来说,在某一个时间点上只能处理一件事情,但是由于CPU处理速度极快,多个线程之间频繁切换执行,给人们的感觉就是多个事情同时在做!!!如:线程A:玩游戏;线程B:播放音乐;线程A与线程B频繁切换操作执行,人会感觉游戏一直在运行,音乐一直在播放,给我们的感觉就是并发的!!!
Java中有三种方式创建多线程
- 第一种:编写一个类,让这个类继承java.lang.Thread类,并重写run()方法;{此时这个类就是线程类}然后通过这个线程类创建对象,通过线程对象调用start()方法开辟新空间,启动线程;
- 第二种:编写一个类,实现java.lang.Runnable接口。实现run()方法;{此时这个类不是线程类,而是可运行类}通过Thread类的构造方法,将上面的可运行类的对象传入,创建线程对象;通过线程对象调用start()方法开辟新空间,启动线程;
- 建议使用第二种(实现接口)方式,因为一个类实现了接口,它还能去继承其他类,更灵活;
- 第三种:采用匿名内部类方式;
第一种方式的代码实现:
package edu.hebeu.multithreading;
/**
* 通过第一种方式实现线程:
* 编写一个类,继承java.long.Thread类(此时这个类就是线程类了)
* 使用 new 创建线程对象;
* 使用start()方法启动线程()
* 注意:
* Java中方法体内的代码都是自顶向下依次逐行执行的,上面的代码不执行完,下面的代码就不可能执行!!!(亘古不变)
*/
public class Multithreading01 {
public static void main(String[] args) { // main方法内的代码是在主栈中运行的
// 新建一个线程对象
MyThread myThread = new MyThread();
// 启动线程
// myThread.run(); /**错误启动线程示范,这样写是不会启动线程的(不会分配新的栈空间),导致run()方法内的代码只能在Main主栈中执行(相当于调用了run()方法),就是单线程*/
myThread.start(); /**这段代码是运行在主线程中,start()方法的作用,启动一个分支线程,在JVM中开辟新的栈空间,
这段代码的任务是开辟一个新的栈空间,只要新的栈空间开出来,start()方法就结束了,线程就启动成功了,任务完成之
后这段代码瞬间就结束了,启动成功的线程会自动调用run()方法,并且run()方法在分支栈的栈底部(压栈);
注意:run()方法在分支栈的最底部,main()方法在主栈的最底部,这两个方法是"平级的",*/
for(int i = 0; i < 1000; i++) {
System.out.println("主线程Main,i=" + i);
}
}
}
/*定义线程类*/
class MyThread extends Thread {
@Override
public void run() {
// 在这个方法内的代码会运行在分支线程中(分支栈中)
for(int i = 0; i < 1000; i++) {
System.out.println("分支线程Thread1,i=" + i);
}
}
}
调用run()方法启动线程的错误示意图:
![Multithreading01在main()]中调用run()方法《非正确启动线程,而是单线程》](https://img-blog.csdnimg.cn/20210427225014712.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDM5NTA4,size_16,color_FFFFFF,t_70#pic_center)
调用start()方法正确启动线程的示意图:
![Multithreading01在main()]中调用start()方法《正确启动线程》](https://img-blog.csdnimg.cn/20210427225037478.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDM5NTA4,size_16,color_FFFFFF,t_70#pic_center)
第二种方式的代码实现:
package edu.hebeu.multithreading;
/**
* 通过第二种方式实现线程:
* 编写一个类,实现java.lang.Runnable接口,重写run()方法;(注意此时这个类不是线程类,而是一个可运行类,它还不是一个线程)
* 通过Thread类的构造方法,将上面的可运行类的对象传入,创建线程对象;
* 调用start()方法启动线程
*/
public class Multithreading02 {
public static void main(String[] args) {
// MyRunnable mr = new MyRunnable(); // 创建可运行类的对象
// Thread myThread = new Thread(mr); // 通过可运行类的对象创建线程对象
Thread myThread = new Thread(new MyRunnable()); // 合并上述代码
myThread.start(); // 启动线程
for(int i = 0; i < 1000; i++) {
System.out.println("Main主栈线程,i=" + i);
}
}
}
// 创建可运行类
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
System.out.println("可运行类(分支线程),i=" + i);
}
}
}
第三种方式直接newRunnable接口使用匿名内部类方式的代码实现:
package edu.hebeu.multithreading;
/**
* 采用第三种方式实现线程,匿名内部类
*/
public class Multithreading03 {
public static void main (String[] args) {
/**创建线程类对象,采用匿名内部类方式*/
Thread myThread = new Thread(new Runnable() { // Runnable是接口,new对象时要加上大括号
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
System.out.println("支线程,i=" + i);
}
}
});
myThread.start(); // 启动线程
for(int i = 0; i < 1000; i++) {
System.out.println("Main主线程,i=" + i);
}
}
}
FutureTask方式,实现Callable接口。(JDK8新特性);代码实现
package edu.hebeu.multithreading;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask; // JDK包下的,属于Java的并发包,老版的JDK没有这个包,是新特性
/**
* 实现线程的第三种方式,FutureTask方式,实现Callable接口。(JDK8新特性);
* 这种方式实现的线程可以获取线程的返回值,之前的两种方式(1、继承Thread,重写run()方法;
* 2、实现Runnable,重写run()方法,编写可运行类,通过可运行类创建线程对象;这两种方法都
* 不能够获取线程返回值,因为run()方法是void)
*
* 如系统委派一个线程去执行一个任务,该线程执行完任务之后,可能会有一个执行结果,我们要拿到这个结果,
* 如果使用前两种方式创建的线程必然是无法完成这个业务的,因此可以使用第三种方式创建的线程,FutureTask方式,实现Callable接口。(JDK8新特性);
*
* 缺点:取到当前线程中获取另一个线程的返回值结果时,当前线程会产生阻塞,导致效率较低
*/
public class Multithreading04 {
public static void main(String[] args) {
// 1、创建未来任务类对象
FutureTask<Object> task = new FutureTask<>(new MyCallable());
// FutureTask<Object> task = new FutureTask<Object>(new Callable<Object>(){ // 使用匿名内部类的方式
// @Override
// public Object call() throws Exception {
// // TODO Auto-generated method stub
// return null;
// }
// });
// 创建线程对象
Thread t = new Thread(task);
// 启动线程
t.start();
/**
* 注意:这里是main()方法内,所有是在主线程中,通过FutureTask的对象 .get()方法可以获取call()方法的返回值;
*
* 这个get()方法的执行会导致当前线程(即主线程,main()方法)阻塞!
* 因为call()方法有返回值,而get()方法是在当前线程(主线程,main()方法)内获取另一个线程的返回值,所以当前线程(主线程,main()方法)要等待另一个线程的call()方法执行完毕(call()方法相当于run()方法,即等待另一个线程执行完毕!!!)
*/
Object obj = null;
try {
obj = task.get(); // 获取线程的返回值结果
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("拿到了线程返回值结果!");
}
}
// 实现Callable接口类
class MyCallable implements Callable<Object> {
@Override
public Object call() throws Exception { // 相当于run()方法,只不过call()方法有返回值
// 这里写线程执行的任务代码,执行完成之后可能会获取一个结果作为返回值
System.out.println("call start");
Thread.sleep(1000 * 3); // 睡眠3秒,模拟执行
System.out.println("call end");
int a = 100;
int b = 200;
return a + b; // 自动装箱机制
}
}
线程生命周期
线程信息的获取
须知:
- String getName(); // 获取线程名字,默认为:Thread-n,注意:n从0开始,以1递增
- void setName(String name); // 设置线程名字
- static Thread currentThread(); // 获取当前线程对象
代码实现如下:
package edu.hebeu.multithreading;
/*获取线程信息:*/
public class GetThreadInfo {
public static void main(String[] args) {
Thread currentThread = Thread.currentThread(); // 获取当前线程对象,因为这段代码在main()方法种,所以当前线程对象就主线程
System.out.println("当前线程" + currentThread.getName()); // main
MyThread4 mr1 = new MyThread4(); // 创建线程对象
System.out.println("线程名:" + mr1.getName()); // 获取线程名字
mr1.setName("线程1"); // 设置线程名字
System.out.println("修改线程名之后,线程名字:" + mr1.getName());
mr1.start(); // 启动线程1
MyThread4 mr2 = new MyThread4(); // 创建线程对象
System.out.println("线程名:" + mr2.getName()); // 获取线程名字
mr2.setName("线程2"); // 设置线程名字
System.out.println("修改线程名之后,线程名字:" + mr2.getName());
mr2.start(); // 启动线程2
}
}
class MyThread4 extends Thread {
@Override
public void run() {
for(int i = 0; i < 1000; i++) {
Thread currentThread = Thread.currentThread(); // 获取当前线程对象
System.out.println("当前线程《" + currentThread.getName() + "》,i=" + i);
// super和this也可以
// System.out.println("当前线程《" + super.getName() + "》,i=" + i);
// System.out.println("当前线程《" + this.getName() + "》,i=" + i);
}
}
}
线程睡眠和终止线程睡眠
- static void sleep(long sleepTime); // 让当前线程进入睡眠状态(阻塞状态),放弃占有的CPU时间片,将资源让给其他线程使用,参数是睡眠时间,单位是毫秒;
- 这行代码出现在A线程,A线程就进入睡眠;出现在B线程,B线程就进入睡眠;
- void interrupt(); // 干扰,这段代码会让线程调用sleep()方法的代码抛出异常,此时异常被try后,自然会进入catch(){},执行完catch(){}内的代码后,自然就会往下执行,以此打断线程的睡眠;唤醒哪个线程,就使用哪个线程对象调用这个方法
- 因此这种中断睡眠的方式是靠Java的异常机制实现的!!!!
关于线程sleep()方法的一个面试题
package edu.hebeu.multithreading;
/**
* 这个例子引出线程的sleep()方法的面试题;
* 如下所示,下面的代码会让线程 t1 进入休眠状态吗?
* 不会,sleep()方法是静态的,在执行上面的这一行代码 t.sleep() 时还是会转换成 Thread.sleep();
这行代码的作用是:让当前线程进入睡眠状态,这行代码出现在main()方法中,也就是说main线程进入睡眠状态;
*/
public class SleepMethodInterview {
public static void main(String[] args) {
Thread t = new MyThread5(); // 使用多态创建线程对象
t.setName("t1"); // 设置线程名字
t.start(); // 启动线程
try {
t.sleep(1000 * 5); // 将线程睡眠5秒
/**注意,sleep()方法是静态的,在执行上面的这一行代码 t.sleep() 时还是会转换成 Thread.sleep();
* 这行代码的作用是:让当前线程进入睡眠状态,这行代码出现在main()方法中,也就是说main线程进入睡眠状态;*/
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("END!!!");
}
}
class MyThread5 extends Thread {
@Override
public void run() {
for(int i = 0; i < 10000; i++) {
System.out.println("线程---->" + i);
}
}
}
终止线程
- void stop(); // 直接销毁(杀死)掉这个线程;这种方式已经过时,不建议使用,因为这种方式直接将线程杀死了,线程没有保存的数据会丢失!!
可以使用这种方式将原先强行终止线程的方式改进:
在可运行类中设置一个线程运行的标记,如果标记为true,就进行线程业务;否则就return掉 run()方法,结束线程;此时要想结束线程只需要通过更改可运行类的对象的标记为false即可return掉 run()方法,结束线程,并且在结束线程之前可以进行结束之前的业务,如保存数据等;
上面两种终止线程方式的代码实现和类比:
package edu.hebeu.multithreading;
/*这个例子演示如何分别使用 stop()方法 和 改进终止的策略 终止一个线程*/
public class ForceFinishThread {
public static void main(String[] args) {
/**没有使用标记创建的可运行类创建的线程对象*/
// Thread thread = new Thread(new MyRunnable3()); // 创建线程对象
// thread.setName("t1"); // 设置线程名字
// thread.start(); // 启动线程
/**使用标记创建的可运行类创建的线程对象*/
MyRunnable4 mr = new MyRunnable4(); // 创建可运行类对象
Thread thread2 = new Thread(mr); // 通过可运行类对象创建线程对象
thread2.setName("t2"); // 设置线程名字
thread2.start(); // 启动线程
try {
Thread.sleep(1000 * 6); // 当前线程(主线程main)睡眠5秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 强行终止线程,直接销毁(杀死)掉这个线程
// thread.stop(); // 已过时,不建议使用;因为这种方式直接将线程杀死了,线程没有保存的数据会丢失!!
/**将可运行类的对象的标记修改为false,正常的结束线程*/
mr.runnFlag = false; // 将线程对象的标记改为false,合理的终止掉这个线程
System.out.println("END!!!");
}
}
class MyRunnable3 implements Runnable {
@Override
public void run() {
// 以下代码表示每一秒循环一次
for(int i = 0; i < 20; i++) {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i);
try {
Thread.sleep(1000); // 睡眠一秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class MyRunnable4 implements Runnable {
boolean runnFlag = true; // 做一个线程运行布尔标记
@Override
public void run() {
// 以下代码表示每一秒循环一次
for(int i = 0; i < 20; i++) {
if(runnFlag) { // 如果标记为true,就进行线程的业务代码
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i);
try {
Thread.sleep(1000); // 睡眠一秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
} else { // 否则
/**在return之前将要保存的数据进行保存等业务*/
return; // 就结束run()方法,即终止当前线程
}
}
}
}
线程调度与线程优先级
线程调度的有关方法:
- void setPriority(int newPriority); // 设置线程的优先级
- int getPriority(); // 获取线程的优先级
*注意:*Java中最低优先级是1,最高优先级是10,默认优先级是5;优先级别高的获取CPU时间片可能会多一些(但也不完全是,大概率是多的)
代码演示:
package edu.hebeu.threadscheduling;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
/*这个例子学习Java中的线程调度的线程优先级有关的方法*/
public class ThreadsSchedulingOfPriority {
public static void main(String[] args) {
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("信息\\线程调度\\线程优先级", true));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 将此输出流的方向改变至 信息\\线程调度\\线程优先级 文件,不在指向控制台
System.setOut(ps); // 通过上面的对象修改输出方向,将此输出流的方向改变至 信息\\线程调度\\线程优先级 文件,不在指向控制台
System.out.println("线程优先级最高:" + Thread.MAX_PRIORITY);
System.out.println("线程优先级最低:" + Thread.MIN_PRIORITY);
System.out.println("线程优先级默认:" + Thread.NORM_PRIORITY);
Thread currentThread = Thread.currentThread(); // 获取当前线程,即主线程main
System.out.println("主线程main的默认优先级:" + currentThread.getPriority());
currentThread.setPriority(1); // 设置主线程main的优先级为1
System.out.println("主线程main此时的优先级:" + currentThread.getPriority());
Thread t1 = new Thread(new MyRunnable5()); // 通过可运行类的对象创建线程对象
t1.setName("t1"); // 设置线程名字
System.out.println("t1线程的默认优先级:" + t1.getPriority());
t1.setPriority(10); // 设置t1线程的优先级为10
System.out.println("t1线程此时的优先级:" + t1.getPriority());
t1.start(); // 启动t1线程
for(int i = 0; i < 10000; i++) {
System.out.println("当前线程《" + currentThread.getName() + "》,i=" + i);
}
}
}
class MyRunnable5 implements Runnable {
@Override
public void run() {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》的优先级" + Thread.currentThread().getPriority());
for(int i = 0; i < 10000; i++) {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i);
}
}
}
线程让位
相关方法:
- static void yield(); // 线程让位,暂停当前正在执行的线程,并执行其他线程
- yield()方法不是阻塞,让当前线程让位,让给其他线程使用;yield()方法的执行会让当前线程从 运行状态 回到 就绪状态;注意:在回到就绪状态之后,有可能还会再抢到
代码演示:
package edu.hebeu.threadscheduling;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
/*这个例子展示学习线程调度的线程让位问题 */
public class ThreadSchedulingOfAbdication {
public static void main(String[] args) {
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("信息\\线程调度\\线程让位", true));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 将此输出流的方向改变至 信息\\线程调度\\线程让位 文件,不在指向控制台
System.setOut(ps); // 通过上面的对象修改输出方向,将此输出流的方向改变至 信息\\线程调度\\线程让位 文件,不在指向控制台
Thread t1 = new Thread(new MyRunnable()); // 通过可运行类的对象创建线程对象
t1.setName("t1"); // 设置线程的名字
t1.start(); // 启动t1线程
for(int i = 0; i < 10000; i++) {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i);
}
}
}
class MyRunnable implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10000; i++) {
if(i % 100 == 0) { // 当除100的余数是0(即每100次)
System.out.println("当前线程《" + Thread.currentThread().getName() + "》已经让位!从运行状态变成就绪状态");
Thread.yield(); // 进行让位
}
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i);
}
}
}
线程合并
相关方法:
- void join(); // 合并线程,等待该线程终止;
- void join(long millisecond) // 参数是毫秒;合并线程,等待该线程终止的时间最长为millisecond毫秒;
- void join(long millisecond, long nanosecond) // 参数1是毫秒,参数二是纳秒;合并线程,等待该线程终止的时间最长为millisecond毫秒 + nanosecond纳秒;
例子:
// 合并线程(多线程变成单线程)的例子
class MyThread1 extends Thread { // MyThread1线程类
public void doSonme() {
MyThread2 mt2 = new MyThread2(); // 创建MyThread2线程类的对象
mt2.join(); // 当前线程进入阻塞,mt2线程执行,直到mt2线程执行完毕,当前线程才可以继续继续执行
}
}
class MyThread2 extends Thread { // MyThread2线程类
}
*注意:*以上这种方式的线程合并不是将线程的两个栈合并了,而是两个栈之间产生了协调等待的关系!!!
代码演示:
package edu.hebeu.threadscheduling;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
/*这个例子演示学习线程合并*/
public class ThreadSchedulingOfMerging {
public static void main(String[] args) {
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("信息\\线程调度\\线程合并", true));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 将此输出流的方向改变至 信息\\线程调度\\线程合并 文件,不在指向控制台
System.setOut(ps); // 通过上面的对象修改输出方向,将此输出流的方向改变至 信息\\线程调度\\线程合并 文件,不在指向控制台
Thread t1 = new Thread(new MyRunnable3()); // 通过可运行类的对象创建线程对象
t1.setName("t1"); // 设置线程的名字为t1
t1.setPriority(1); // 设置t1的线程优先级为1
t1.start(); // 启动t1线程
for(int i = 0; i < 1000; i++) {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i); if(i == 998) { // 如果主线程main还剩1次循环完毕
try {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》将受阻");
t1.join(); // t1合并到当前线程中,当前线程受阻,直到t1线程执行结束
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
}
class MyRunnable3 implements Runnable {
@Override
public void run() {
for(int i = 0; i < 10000; i++) {
System.out.println("当前线程《" + Thread.currentThread().getName() + "》,i=" + i);
}
System.out.println(Thread.currentThread().getName() + "执行完毕!!!");
}
}
线程安全
多线程并发环境下数据的安全问题
在以后发开发中,我们的项目都是运行在服务器当中,而服务器已经将线程的定义、线程对象的创建、线程的启动等,都已经实现完成了,这些代码我们都不需要编写;因此,最重要的,程序员要知道编写的程序需要放在一个多线程的环境下运行,因此更需要关注的是这些数据在多线程并发环境下是否安全的问题;
什么时候数据在多线程并发环境下会存在安全问题?
三个条件:1、多线程并发;2、有共享数据;3、共享数据有修改行为;满足一上三个条件之后,就会存在线程安全问题!!!
怎么解决线程安全的问题?
在多线程环境下,有共享数据,并且数据还会被修改,此时就存在线程安全问题;解决这个问题的方法:线程排队执行(不能并发),即用排队执行解决线程安全问题,这种机制被称为"线程同步机制";注意:线程同步机制就是线程排队机制,线程同步会牺牲一部分效率;
同步与异步理解
- 异步编程模型:线程t1和线程t2,各自执行各自的,t1不管t2,t2不管t1,谁也不用等谁,效率较高,即多线程并发;
- 同步编程模型:线程t1和线程t2,在线程t1执行时,必须等待t2线程执行结束;或者线程t2执行时,必须等待t1线程执行结束,
*注意:*两个线程之间发生了等待关系,效率较低,线程排队执行;
synchronized
*Java中线程同步使用的synchronized(){}语法:*在synchronized(){}内的代码会线程排队执行(即一个线程把这里面的代码全部执行结束之后,另一个线程才能进来),不能并发;线程同步机制的语法:
synchronized() {
// 线程同步代码块......
}
*注意:*synchronized表示“排他锁”;synchronized关键字后面的小括号中传入的这个"数据"是相当关键的,这个数据必须是多线程共享的数据,这样才能实现多线程排队执行;
上述的代码中synchronized()中写什么?怎么写?
主要看想让那些线程同步,如t1、t2、t3、t4、t5五个线程,若只希望t1、t2、t3这3个线程排队,t4、t5这2个线程不排队,那么就要在synchronized(){}的()内写入t1、t2、t3这3个线程共享的对象,而且这个对象对于t4、t5这2个线程来说是不共享的;
锁池的图示:
![线程生命周期-锁池(非状态)]的引入](https://img-blog.csdnimg.cn/20210427225207166.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L20wXzQ5MDM5NTA4,size_16,color_FFFFFF,t_70#pic_center)
synchronized(){}的线程同步的执行原理
Java语言中,每个对象都有一把锁(100个对象100把锁),其实这把锁就是一个标记;如:
- 假设现有t1、t2两个线程并发,开始执行synchronized(){}内的代码时肯定有先有后;
- 假设t1线程先遇到synchronized关键字,这时会自动找synchronized的()内的共享对象的对象锁,在找到之后,其会占有这把锁,然后执行synchronized(){}的{}内的代码,在执行内部的代码的过程中一直会占有这把锁,直到{}内的代码执行完毕,这把锁才会释放;
- 此时如果t2线程也遇到synchronized关键字,这时会自动找synchronized的()内的共享对象的对象锁,在找到之后,其会占有这把锁,但是此时这把锁已经被t1占有,t2只能在同步代码块外面等待t1的结束,直到t1将同步代码块执行完毕,t1就归还这把锁,此时t2等到这把锁,然后t2占有这把锁,进入同步代码块执行程序;
通过上述,可以看到实现了线程排队执行,这里需要注意:这个共享对象一定要选好,这个共享对象一定是你需要排队执行的这些线程对象所共享的;需要注意:同步代码块越小,即{}内的代码越少,效率越高;反之效率越低;
开发中怎么解决线程安全问题?
synchronized会让程序的执行效率降低,用户体验不好,系统的用户吞吐量(并发量)降低,用户体验差,在不得已的情况下再选择线程同步机制(synchronized)
优先考虑:尽量使用 “局部变量” 代替 “实例变量和静态变量”;
其次考虑:如果不能使用 “局部变量” ,只能是 “实例变量” ,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。(即1个线程对应1个对象,100个线程对应100个对象,
数据不共享就没有数据安全问题了);
最次考虑:如果不能使用局部变量,并且不能创建多个对象,此时只能选择线程同步机制(synchronized);
synchronized的几种写法:
- 第一种:同步代码块:
使用较灵活
synchronized(线程同步的共享对象) {
// 同步代码块
} - 第二种:在实例方法上使用synchronized关键字:
表示共享对象一定且只能是this,并且同步代码块是整个方法体
public/private/protected synchronized 返回值类型 方法名(参数列表) {
}- 缺点: 1、synchronized出现在实例方法上只能锁this,这种方法不灵活;2、synchronized出现在实例方法上,表示整个方法体都需要同步,可能会无故扩大同步范围,导致程序的执行效率降低,这种方式不常用;
- 优点:代码节俭了;
- 第三种:在静态方法上使用synchronized关键字:
表示找类锁;
注意:类锁永远只有1把,就算这个类创建了100个对象,类锁也只有1把!!!
类锁是保证静态变量的安全,对象锁保证实例变量的安全;
死锁的案例
图示:
代码:
package edu.hebeu.threadsafety;
/*死锁的例子*/
public class Deadlock {
public static void main(String[] args) {
Object obj1 = new Object();
Object obj2 = new Object();
// 线程t1和t2共享obj1和obj2对象
MThread1 t1 = new MThread1(obj1, obj2);
MThread2 t2 = new MThread2(obj1, obj2);
t1.setName("t1");
t2.setName("t2");
t1.start();
t2.start();
/**
*此时运行程序会发现代码不会出错,不会出异常,但是代码不会再往下执行了,分析原因,这是由于对象锁死锁;
* 总结:开发中尽量少使用synchronized嵌套;
*/
}
}
// MThread1线程类
class MThread1 extends Thread {
private Object obj1;
private Object obj2;
public MThread1(Object obj1, Object obj2) {
super();
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized(obj1) {
System.out.println("MThread1类:obj1对象锁定!");
try {
Thread.sleep(1000); // 睡眠1秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(obj2) {
System.out.println("MThread1类:obj2对象锁定!");
}
}
System.out.println("MThread1类:run()方法执行完毕");
}
}
// MThread2线程类
class MThread2 extends Thread {
private Object obj1;
private Object obj2;
public MThread2(Object obj1, Object obj2) {
super();
this.obj1 = obj1;
this.obj2 = obj2;
}
@Override
public void run() {
synchronized(obj2) {
System.out.println("MThread2类:obj2对象锁定!");
try {
Thread.sleep(1000); // 睡眠1秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
synchronized(obj1) {
System.out.println("MThread2类:obj2对象锁定!");
}
}
System.out.println("MThread2类:run()方法执行完毕");
}
}
*结论:*为了避免出现上述的程序对象锁死锁,在开发中应尽量少使用synchronized嵌套;
Java中的三大变量与线程安全问题
Java中的三大变量:实例变量:存储在堆中;静态变量:存储在方法区中;局部变量:存储在栈中;
以上的三大变量中:
- 局部变量永远都不会存在线程安全问题,因为局部变量不共享(一个线程一个栈),局部变量存储在栈中,因此局部变量永远都不会共享,所以局部变量永远都不会存在线程安全问题;
- 而实例变量存储在堆中,且堆只有一个;堆和方法区都是多线程共享的,所以可能存在线程安全问题;
- 静态变量存储在方法区中,且方法区只有一个;堆和方法区都是多线程共享的,所以可能存在线程安全问题;
*结论:*对String字符串,如果是局部变量使用(局部变量没有线程安全问题),建议使用StringBuilder(非线程安全),避免影响效率;
多线程共享与不共享图示:
几个面试题
第一题:
package edu.hebeu.threadsafety;
/**
* 关于synchronized的面试题:
* 问,如下代码所示,t2线程执行的doOther()方法会等待t1线程执行完doSome()方法结束才执行吗?
* 答案:不会等待,虽然两个线程的共享对象一样(对象锁一样),都是MyClass类的myClass对象;doSome()方法使用了synchronized关键字,但是doOther()方法没有使用synchronized关键字修饰,所以不会出现等待关系;
*/
public class SynchronizedInterview1 {
public static void main(String[] args) {
MyClass myClass = new MyClass(); // 创建MyClass对象
Thread t1 = new MyThread(myClass); // 创建线程t1
Thread t2 = new MyThread(myClass); // 创建线程t2
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000 * 1); // 睡眠1秒,这个睡眠是为了让t1线程先启动,以调用doSome()方法[通过synchronized关键字修饰的,对象锁是this,即myClass对象],以在执行t2线程之前拿到对象锁myClass
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
}
}
class MyThread extends Thread {
private MyClass myClass;
public MyThread(MyClass myClass) {
super();
this.myClass = myClass;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
myClass.doSome();
}
if(Thread.currentThread().getName().equals("t2")) {
myClass.doOther();
}
}
}
class MyClass {
/*这个方法是synchronized关键字修饰,对象锁是this*/
public synchronized void doSome() {
System.out.println("doSome 开始执行");
try {
Thread.sleep(1000 * 6); // 当前线程睡眠6秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("doSome 执行结束");
}
public void doOther() {
System.out.println("doOther 开始执行");
System.out.println("doOther 开始执行");
}
}
第二题:
package edu.hebeu.threadsafety;
/**
* 关于synchronized的面试题:
* 问,如下代码所示,t2线程执行的doOther()方法会等待t1线程执行完doSome()方法结束才执行吗?
* 答案:会等待,两个线程的共享对象一样(对象锁一样),都是MyClass2的对象myClass;doOther()、doOther()方法都有synchronized关键字修饰,所以会出现等待关系;
*/
public class SynchronizedInterview2 {
public static void main(String[] args) {
MyClass2 myClass = new MyClass2(); // 创建MyClass对象
Thread t1 = new MyThread2(myClass); // 创建线程t1
Thread t2 = new MyThread2(myClass); // 创建线程t2
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000 * 1); // 睡眠1秒,这个睡眠是为了让t1线程先启动,以调用doSome()方法[通过synchronized关键字修饰的,对象锁是this,即myClass对象],以在执行t2线程之前拿到对象锁myClass
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
}
}
class MyThread2 extends Thread {
private MyClass2 myClass;
public MyThread2(MyClass2 myClass) {
super();
this.myClass = myClass;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
myClass.doSome();
}
if(Thread.currentThread().getName().equals("t2")) {
myClass.doOther();
}
}
}
class MyClass2 {
/*这个方法是synchronized关键字修饰,对象锁是this */
public synchronized void doSome() {
System.out.println("doSome 开始执行");
try {
Thread.sleep(1000 * 6); // 当前线程睡眠6秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("doSome 执行结束");
}
public synchronized void doOther() {
System.out.println("doOther 开始执行");
System.out.println("doOther 开始执行");
}
}
第三题:
package edu.hebeu.threadsafety;
/**
* 关于synchronized的面试题:
* 问,如下代码所示,t2线程执行的doOther()方法会等待t1线程执行完doSome()方法结束才执行吗?
* 答案:不会等待,两个线程的共享对象不一样(对象锁不一样),一个是MyClass3类的myClass1对象、另一个是MyClass3类的myClass2对象,虽然doSome()和doOther()方法都有synchronized关键字修饰,但是对象锁不一样,所以不会出现等待关系;
*/
public class SynchronizedInterview3 {
public static void main(String[] args) {
MyClass3 myClass1 = new MyClass3(); // 创建MyClass1对象
MyClass3 myClass2 = new MyClass3(); // 创建MyClass1对象
Thread t1 = new MyThread3(myClass1); // 创建线程t1
Thread t2 = new MyThread3(myClass2); // 创建线程t2
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000 * 1); // 睡眠1秒,这个睡眠是为了让t1线程先启动,以调用doSome()方法[通过synchronized关键字修饰的,对象锁是this,即myClass对象],以在执行t2线程之前拿到对象锁myClass
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
}
}
class MyThread3 extends Thread {
private MyClass3 myClass;
public MyThread3(MyClass3 myClass) {
super();
this.myClass = myClass;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
myClass.doSome();
}
if(Thread.currentThread().getName().equals("t2")) {
myClass.doOther();
}
}
}
class MyClass3 {
/*这个方法是synchronized关键字修饰,对象锁是this*/
public synchronized void doSome() {
System.out.println("doSome 开始执行");
try {
Thread.sleep(1000 * 6); // 当前线程睡眠6秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("doSome 执行结束");
}
public synchronized void doOther() {
System.out.println("doOther 开始执行");
System.out.println("doOther 开始执行");
}
}
第四题:
package edu.hebeu.threadsafety;
/**
* 关于synchronized的面试题:
* 问,如下代码所示,t2线程执行的doOther()方法会等待t1线程执行完doSome()方法结束才执行吗?
* 答案:会等待,doSome()和doOther()方法都是静态方法且都有synchronized关键字修饰,则锁是类锁,一个类不论new多少个对象,都只有一把类锁;所以会出现等待关系;
*/
public class SynchronizedInterview4 {
public static void main(String[] args) {
MyClass4 myClass1 = new MyClass4(); // 创建MyClass4的对象
MyClass4 myClass2 = new MyClass4(); // 创建MyClass4的对象
Thread t1 = new MyThread4(myClass1); // 创建线程t1
Thread t2 = new MyThread4(myClass2); // 创建线程t2
t1.setName("t1");
t2.setName("t2");
t1.start();
try {
Thread.sleep(1000 * 1); // 睡眠1秒,这个睡眠是为了让t1线程先启动,以调用doSome()方法[通过synchronized关键字修饰的,对象锁是this,即myClass对象],以在执行t2线程之前拿到对象锁myClass
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
}
}
class MyThread4 extends Thread {
private MyClass4 myClass;
public MyThread4(MyClass4 myClass) {
super();
this.myClass = myClass;
}
@Override
public void run() {
if(Thread.currentThread().getName().equals("t1")) {
myClass.doSome();
}
if(Thread.currentThread().getName().equals("t2")) {
myClass.doOther();
}
}
}
class MyClass4 {
/*这个静态方法是synchronized关键字修饰,锁就是类锁*/
public synchronized static void doSome() {
System.out.println("doSome 开始执行");
try {
Thread.sleep(1000 * 6); // 当前线程睡眠6秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("doSome 执行结束");
}
/*这个静态方法是synchronized关键字修饰,锁就是类锁*/
public synchronized static void doOther() {
System.out.println("doOther 开始执行");
System.out.println("doOther 开始执行");
}
}
守护线程
须知:Java语言中的两大类线程:守护线程和用户线程;用户线程:如main方法主线程等;守护线程:又称为后台线程,如垃圾回收线程等;
特点:
- 一般守护线程是一个死循环;
- 所有的用户线程只要结束,守护线程自动结束;
一般用在什么地方?
如系统每天00:00时数据自动备份,这个时候需要使用定时器,并且将定时器设置为守护线程,一直监视着,每到00:00就备份一次;如果所有的用户线程结束了,守护线程自动退出,没有必要进行数据备份了;
代码实现一个守护线程案例:
package edu.hebeu.daemonthread;
/*实现一个守护线程*/
public class DaemonThread {
public static void main(String[] args) {
Thread daemonThread = new BackupThread(); // 创建线程对象
daemonThread.setName("守护线程1(备份)");
daemonThread.setDaemon(true); // 启动线程之前设置这个线程对象为守护线程,当用户线程都运行完毕,这个线程对象就会强行自动结束
daemonThread.start(); // 启动线程
for(int i = 0; i < 20; i++) {
System.out.println("主线程执行,第" + (i + 1) + "次");
try {
Thread.sleep(1000); // 睡眠1秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
class BackupThread extends Thread {
@Override
public void run() {
int i = 0;
while(true) { // 如果将这个线程类创建的线程对象设置为守护线程,当用户线程执行完毕,这里即使是永真循环也会结束!!!
System.out.println(Thread.currentThread().getName() + "线程,备份次数:" + (++i) + ",备份中...");
try {
Thread.sleep(3000); // 睡眠3秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
定时器
间隔特定的时间,执行特定的程序;如,每周银行账户的总账操作;每天进行数据的备份操作等;这种需求在实际的开发中很常见,Java中可以有多种方式实现定时器,如:
- 使用sleep()方法,这是最原始的;
- 使用Java的java.util.Timer类,但是在实际的开发中这种方式也很少了,因为很多高级框架都支持自动任务的;
实际开发中,目前使用较多的是Spring框架中提供的SpringTask框架(底层还是java.util.Timer),这个框架只有进行简单的配置就能完成定时任务;
构造方法:
-
Timer(); // 构造一个定时器
-
Timer(boolean isDaemon); // 创建一个定时器,其相关的线程是否作为守护线程运行,true表示后台线程(守护线程),false表示用户线程
-
Timer(String name); // 创建一个定时器,其相关的线程具有指定的名称
-
Timer(String name, boolean isDaemon); // 创建一个定时器,其相关的线程具有指定的名称,并且其相关的线程是否作为守护线程运行,true表示后台线程(守护线程),false表示用户线程
常用方法:
- schedule(TimerTask task, Date firstTime, long millisecond); // 给定时器对象委派任务,参数2、第一次执行时间;参数3、固定延迟多久执行,单位毫秒;
定时器的代码实现:
package edu.hebeu.timer;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
/*这个例子学习java.util.Timer类*/
public class TimerStu {
public static void main(String[] args) {
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("信息\\定时器\\日志信息", true));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 将此输出流的方向改变至 信息\\定时器\\日志信息 文件,不在指向控制台
System.setOut(ps); // 通过上面的对象修改输出方向,将此输出流的方向改变至 信息\\定时器\\日志信息 文件,不在指向控制台
// Timer timer = new Timer(); // 创建定时器对象
// Timer timer = new Timer("任务定时器"); // 创建名为 "任务定时器" 的定时器对象
// Timer timer = new Timer(true); // 创建定时器对象(守护线程的方式),此时为了保证这个守护线程不结束,要使用户线程不结束
Timer timer = new Timer("任务定时器", true); // 创建名为 "任务定时器" 的定时器对象(守护线程的方式),此时为了保证这个守护线程不结束,要使用户线程不结束
/**任务第一次的执行时间*/
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
Date firstTime = new Date(); // 声明任务第一次的执行时间
try {
firstTime = sdf.parse("2021-02-04 08:50:30 000");
} catch (ParseException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 给定时器委托任务
// timer.schedule(new LogTask(), firstTime, 1000 * 10); // 委托任务为LogTask类的对象,首次从firstTime执行,每10秒执行一次委托事件
timer.schedule(new TimerTask(){ // 使用匿名内部类的方式创建要委托的任务进行委托
@Override
public void run() {
// 这里编写需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String nowDate = sdf.format(new Date()); // 获取当前时间
System.out.println("当前时间:" + nowDate + ";LogTask执行《" + Thread.currentThread().getName() + "》,数据备份中...");
}
}, firstTime, 1000 * 60 * 2); // 委托任务为LogTask类的对象,首次从firstTime执行,每2分钟执行一次委托事件
for(int i = 0; i < 10000; i++) {
System.out.println("main主栈<用户线程>,《" + Thread.currentThread().getName() + "》运行中,运行次数:" + (i+1) );
try {
Thread.sleep(1000); // 睡眠1秒
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
/*定时器的任务类,TimerTask是抽象类,不能new,所有要创建一个定时器任务类继承它*/
class LogTask extends TimerTask {
@Override
public void run() {
// 这里编写需要执行的任务
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss SSS");
String nowDate = sdf.format(new Date()); // 获取当前时间
System.out.println("当前时间:" + nowDate + ";LogTask执行《" + Thread.currentThread().getName() + "》,数据备份中...");
}
}
关于Object类中的wait()方法和notify()方法
- wait()方法的作用:Object o = new Object(); o.wait(); 表示让正在o对象上活动的线程t进入等待状态;并且释放掉t线程之前占有的o对象的锁;
- notify()方法的作用:Object o = new Object(); o.notify(); 表示让正在o对象上等待的线程t唤醒,只是通知,不会释放o对象之前占有的锁;
*注意:*wait()方法和notify()方法不是线程对象的方法,而是普通Java对象都有的方法;wait()方法和notify()方法建立在线程同步的基础之上。因为多线程要同时操作一个仓库,有线程安全问题;
什么是 “生产者和消费者模式” ?
生产线程负责生产,消费线程负责消费;生产线程和消费线程要达到均衡;这是一种特殊的业务需求,在这种特殊的情况下需要使用wait()方法和notify()方法;
生产者消费者模式的图示:
使用wait()方法和notify()方法实现 “生产者和消费者模式”,模拟需求如下:仓库采用List集合,List集合假设只能存储1个元素,1个元素就表示仓库满了,0个元素就表示仓库空了,保证List集合永远都是最多存储1个元素,实现这种效果:生产一个,消费一个;代码如下:
package edu.hebeu.objof.wait_notify;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.ArrayList;
import java.util.List;
/*使用wait()方法和notify()方法实现 "生产者和消费者模式";*/
public class ProducerConsumer {
public static void main(String[] args) {
PrintStream ps = null;
try {
ps = new PrintStream(new FileOutputStream("信息\\生产者和消费者模式\\日志信息", true));
} catch (FileNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} // 将此输出流的方向改变至 信息\\生产者和消费者模式\\日志信息 文件,不在指向控制台
System.setOut(ps); // 通过上面的对象修改输出方向,将此输出流的方向改变至 信息\\生产者和消费者模式\\日志信息 文件,不在指向控制台
List<Object> list = new ArrayList<>(); // 创建一个仓库对象,一2使生产线程对象和消费线程对象共享这个仓库
Thread producerThread = new Thread(new Producer(list)); // 创建生产者线程对象
Thread consumerThread = new Thread(new Consumer(list)); // 创建消费者线程对象
producerThread.setName("-生产者线程-");
consumerThread.setName("-消费者线程-");
consumerThread.start(); // 消费者线程对象启动
producerThread.start(); // 生产者线程对象启动
// consumerThread.start(); // 消费者线程对象启动
}
}
// 生产线程
class Producer implements Runnable {
private List<Object> list; // 仓库
public Producer(List<Object> list) {
this.list = list;
}
@Override
public void run() {
while(true) {
synchronized(list) {
if(list.size() > 0) { // 如果仓库内的元素大于0,即仓库满了
// if(list.size() == 10) { // 如果仓库内的等于10,即仓库满了
try {
System.out.println(Thread.currentThread().getName() + "等待,仓库元素:" + list.size() + ",释放仓库锁...");
list.wait(); // 生产线程进入等待状态,并且释放掉list对象锁
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 程序执行到此,表示仓库没有满,则生产线程还能继续生产
Object obj = new Object();
list.add(obj);
System.out.println(Thread.currentThread().getName() + "运作,生产元素:" + obj + ",仓库元素:" + list.size() + ";唤醒消费者线程");
// list.notify(); // 唤醒消费者线程
list.notifyAll(); // 唤醒消费者线程
}
}
}
}
// 消费线程
class Consumer implements Runnable {
private List<Object> list; // 仓库
public Consumer(List<Object> list) {
this.list = list;
}
@Override
public void run() {
while(true) {
synchronized(list) {
if(list.size() == 0) { // 如果仓库内的元素等于0,表示仓库空了
try {
System.out.println(Thread.currentThread().getName() + "等待,仓库元素:" + list.size() + ",释放仓库锁...");
list.wait(); // 消费者线程进入等待,释放list对象锁
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// 程序执行到此,表示仓库不为空,消费者线程还能继续消费
Object obj = list.remove(0);
System.out.println(Thread.currentThread().getName() + "运作,消费元素:" + obj + ",仓库元素:" + list.size() + ";唤醒生产者线程");
// list.notify(); // 唤醒生产者线程
list.notifyAll(); // 唤醒生产者线程
}
}
}
}
反射
*概述:反射机制:通过Java语言的反射机制可以操作字节码文件(读和修改字节码文件);在哪个包下:java.lang.reflect.;
*作用:*通过Java语言的反射机制可以操作字节码文件(读和修改字节码文件);即操作代码片段(class文件),让程序更加灵活;
反射机制相关的重要类有哪些?
- java.lang.Class // 代表整个字节码,代表一个类型,代表整个类
- java.lang.reflect.Constructor // 代表字节码中的构造方法字节码,代表类中的构造方法
- java.lang.reflect.Field // 代表字节码中的属性字节码,代表类中的属性(成员变量)即包括静态变量和实例变量
- java.lang.reflect.Method // 代表字节码中的方法字节码,代表类中的方法
可以通过如下进行记忆:
public class User { // java.lang.Class
// java.lang.reflect.Field
private String num;
// java.lang.reflect.Constructor
public User() {}
// java.lang.reflect.Constructor
public User(String num) {
this.num = num;
}
// java.lang.reflect.Method
public void doSome() {
System.out.println("我是一个方法");
}
}
获取字节码的三种方式
- 第一种方式获取类的字节码:Class c = Class.forName(“完整类名带包名”);
- java.lang.Class类的静态方法forName(String classFullName);
- 如:Class static forName(String classFullName); // 获取classFullName类的字节码,注意classFullName类必须是类的全限定名(即包括包名);Class c1 = Class.forName(“java.lang.String”); // 获取String类的字节码
- 注意:这个方法会导致传入的这个类进行 类加载,并返回这个类的字节码,类加载会导致静态代码块执行,并且静态代码块只执行一次;
- 第二种方式获取类的字节码: Class c = 对象.getClass();
- java中任何一个对象都有getClass()方法
- 如:String s = “abc”; Class c = s.getClass();
- 第三种方式获取类的字节码:Java语言中,任何一种类型,包括基本数据类型,都有class属性;
- 如: Class c1 = int.class; Class c2 = Object.class; Class c3 = Date.class; Class c4 = String.class;
通过类的字节码实例化对象
- E newInstance(); // 这个方法会调用字节码对应类的无参构造方法进行创建对象;E是Class指定的泛型,未指定为Object
- 可以通过IO流读取配置信息,再与Class类的forName(String fullName)方法联用,通过读取配置文件的信息创建类对象,这样做Java的代码写一遍,再之后不改变Java源代码的基础之上,直接修改属性配置文件内容,做到不同对象的实例化,非常灵活;符合OCP开闭原则(对扩展开放,对修改关闭);
字节码的内存图:
类加载器的引入
关于JDK自带的类加载器:ClassLoader
,类加载器时专门负责加载类的命令或工具;JDK中自带3个类加载器:启动类加载器:jre下的 lib/rt.jar;扩展类加载器:jre下的 lib/ext/*.jar;应用类加载器:classpath;
如下例子:
假设有一行代码:String s = "abcd";
,代码开始执行之前,会将所需要类全部加载到JVM中;通过类加载器加载,看到以上代码类加载器会找String.class文件,找到就加载,如何进行加载?如下:
- 首先通过"启动类加载器"加载
- 须知:"启动类加载器"专门加载:jre下的 lib/rt.jar 文件,rt.jar中都是JDK最核心的类库;
- 如果"启动类加载器"加载不到时,会通过"扩展类加载器"加载
- 须知:"扩展类加载器"专门加载:jre下的 lib/ext/*.jar;
- 如果"扩展类加载器"加载不到时,会通过"应用类加载器"加载
- 须知:"应用类加载器"专门加载:jre下的 classpath中(环境变量内的classpath)的jar包(class文件);
字节码的内存图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BaLgwrRY-1619533734439)(javaSE学习笔记.assets/字节码的内存图.PNG)]
双亲委派机制
Java中为了保证类加载的安全,使用了双亲委派机制。优先从"启动类加载器"中加载,这个称为"父";“父"无法加载到时,会从"扩展类加载器"中加载,这个称为"母”。双亲委派,如果都加载不到,才会考虑从"应用类加载器"中加载,直到加载到为止;
可变长度参数的使用
*语法:*类型… 名(如 int… args)
注意:
- 可变长度的参数要求参数的个数是0-n个;
- 可变长度参数在参数列表中必须是在最后一个位置上(那么可变长度参数就只能有1个);
- 可变长度参数可以当作一个数组来看待;
- 即在传参时可以传入一个和可变参数类型一样的数组
Class类的常用方法
- String fullName getName(); // 获取Class对象的全类名(包括包名)
- String simpleName getSimpleName(); // 获取Class对象的简写类名(不包括包名)
- Field[] getFields(); // 获取这个类中所有的由public修饰的属性字段
- Field[] getDeclaredFields(); // 获取这个Class对应的类中所有的属性字段
- Field getField(String fieldName); // 获取这个Class对应的类中由public修饰的,属性名为fileName的属性字段
- Field getDeclaredField(String fieldName); // 获取这个Class对应的类的属性名为fileName的属性字段(不论什么修饰符)
- Method[] getMethods(); // 获取这个字节码类的所有public修饰的方法
- Method[] getDeclaredMethods(); // 获取这个类所有的方法
- Method getMethod(String methodName, Class<?>… classes); // // 通过方法名和参数类型获取这个Class对应的类的由public修饰的方法;参数1:方法名;参数2(Class类型的可变长度参数):方法的参数的字节码类型
- Method getDeclaredMethod(String methodName, Class<?>… classes ) // 通过方法名和参数类型获取这个Class对应的类的方法(不论什么修饰符);参数1:方法名;参数2(Class类型的可变长度参数):方法的参数的字节码类型
- Constructor[] getConstructors(); // 获取这个类的由public修饰的所有构造方法
- Constructor[] getDeclaredConstructors(); // 获取这个类的所有的构造方法
- Constructor getConstructor(Class<?>… classes); // 获取这个类的指定的由public修饰的构造方法,参数是指定的构造方法对应参数类型的class字节码
- Constructor getDeclaredConstructor(Class<?>… classes); // 获取这个类的指定构造方法(不论什么修饰符),参数是指定的构造方法对应参数类型的class字节码
- Class getSuperclass(); // 获取这个Class对象对应的类的父类的字节码(因为直接父类只能是一个,所以返回值类型是Class)
- Class[] getInterfaces(); // 获取这个Class对象对应的类的所有实现的接口的字节码(因为接口可能有多个,所以返回值类型是Class[]数组)
Modifier
常用方法:
- String toString(modifiersCode); // 通过修饰符的int类型码转变为String类型的修饰符名
- 如:String modifierName = Modifier.toString(2); // private
Field
常用方法:
- String getName(); // 获取Field对象对应的属性名
- Class getType(); // 获取Field对象对应的属性类型
- int getModifiers(); // 获取Field对象对应的属性修饰符列表对应的int值
- Object get(Object obj); // 获取obj对象的Field对象(属性字段)的值
- void set(Class objClass, Object value); // 将objClass字节码对象的这个Field对象对应的属性赋值为value
- 注意,只能访问public修饰符的属性(默认的、protected、private都不能访问);
- setAccessible(boolean); // 将Field对象打破封装,使Field对象的四种修饰都能访问
- 如果想要对默认的、protected、private的属性字段进行set(obj, value);即四种修饰都能访问,就需要将要访问的属性字段Field对象打破封装
- 注意:这样做会产生问题(反射机制的缺点):打破封装可能会给不法分子留下机会!
- 如:
Field fieldAge = fieldEntityClass.getDeclaredField("age"); // 获取age属性对应的Field对象fieldAge.setAccessible(true); // 将fieldAge Field对象打破封装,因为age属性是protected的
案例:获取将指定类的所有属性字段,包括修饰符列表、类型、等
package edu.hebeu.decompile;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/*这个例子展示如何通过反射机制实现反编译字节码文件中的Field*/
public class DecompileField {
public static void main(String[] args) {
StringBuilder cString = new StringBuilder(); // 创建一个StringBuilder对象,用来存放反编译class文件之后的字符串信息
Class<?> cClass = null; // 声明一个类的字节码
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
System.out.print("请输入要反编译的类:");
String className = scanner.next();
cClass = Class.forName(className); // 获取整个类的字节码
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(null != scanner) {
scanner.close();
}
}
cString.append(Modifier.toString(cClass.getModifiers())); // 获取类的修饰符信息
cString.append(" class ");
cString.append(cClass.getSimpleName()); // 获取类的简写类名(不包括包名)
cString.append(" {\n");
Field[] fields = cClass.getDeclaredFields(); // 获取这个类的全部Field对象
for(Field f : fields) {
cString.append("\t");
int modifiersCode = f.getModifiers(); // 获取这个属性对象的修饰符列表的int数字
String modifiersName = Modifier.toString(modifiersCode); // 通过上面的int数字获取这个int数字对应的String类型修饰符列表
cString.append(modifiersName);
cString.append(" ");
Class<?> fieldTypeClass = f.getType(); // 获取这个属性对象的Class类型的类型名
String fieldTypeName = fieldTypeClass.getSimpleName(); // 通过Class类型的类型名获取类型的名字
cString.append(fieldTypeName);
cString.append(" ");
String fieldName = f.getName(); // 获取这个属性对象的名字
cString.append(fieldName);
cString.append(";\n");
}
cString.append("}");
System.out.println(cString);
}
}
Method
常用方法:
- String getName(); // 获取Method对象(此方法对象)的名称,以String形式返回
- int getModifiers(); // 获取Method对象的修饰符列表对应的int值
- Class getReturnType(); // 获取Method对象(此方法对象)的返回值类型,以Class形式返回
- Class[] getParameterTypes(); // 获取Method对象的(此方法对象)的参数类型列表,以Class[] 形式返回
- Object invoke(IObj obj, Obj… objs); // 执行这个Method对应的方法(该方法是obj对象的实例方法,参数值是objs对应的值),返回值就是执行的方法的返回值;
案例:获取指定类的所有方法,包括方法的返回值类型,参数类型,方法名等信息,不用获取方法的内部信息
package edu.hebeu.decompile;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/*这个例子展示反编译方法*/
public class DecompileMethod {
public static void main(String[] args) {
StringBuilder cString = new StringBuilder(); // 创建一个StringBuilder对象,用来存放反编译class文件之后的字符串信息
Class<?> cClass = null; // 声明一个类的字节码
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
System.out.print("请输入要反编译的类:");
String className = scanner.next();
cClass = Class.forName(className); // 获取整个类的字节码
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(null != scanner) {
scanner.close();
}
}
cString.append(Modifier.toString(cClass.getModifiers())); // 获取类的修饰符信息
cString.append(" class ");
cString.append(cClass.getSimpleName()); // 获取类的简写类名(不包括包名)
cString.append(" {\n");
Method[] methods = cClass.getDeclaredMethods(); // 获取这个类的全部Method对象
for(Method m : methods) {
cString.append("\t");
int modifiersCode = m.getModifiers(); // 获取这个属性对象的修饰符列表的int数字
String modifiersName = Modifier.toString(modifiersCode); // 通过上面的int数字获取这个int数字对应的String类型修饰符列表
cString.append(modifiersName);
cString.append(" ");
Class<?> methodReturnTypeClass = m.getReturnType(); // 获取这个方法对象的返回值的Class类型的类型名
String methodReturnTypeName = methodReturnTypeClass.getSimpleName(); // 通过Class类型的类型名获取类型的名字
cString.append(methodReturnTypeName);
cString.append(" ");
String methodName = m.getName(); // 获取这个方法对象(Method对象)的名字
cString.append(methodName);
cString.append("(");
Class[] paramClassTypes = m.getParameterTypes(); // 获取这个方法的形参类型,Class数组形式接收
for(Class paramClassType : paramClassTypes) {
cString.append(paramClassType.getSimpleName());
cString.append(",");
}
int commaSubScript = cString.lastIndexOf(","); // 获取最后一个 "," 的下标,如果没有 "," 就返回 -1
if(commaSubScript != -1) cString.deleteCharAt(commaSubScript); // 截取掉倒数第一位的下标(即截取掉最后的 "," 1位字符)
cString.append(") {}\n");
}
cString.append("}");
System.out.println(cString);
}
}
Constructor
常用方法:
- String getName(); // 获取Constructor对象(此方法对象)的名称,以String形式返回
- int getModifiers(); // 获取Constructor对象的修饰符列表对应的int值
- Class[] getParameterTypes(); // 获取Constructor对象的(此方法对象)的参数类型列表,以Class[] 形式返回
- Object newInstance(Object… objs); // 执行构造函数(根据传入的可变类型参数决定执行该Constructor对象对应类的哪个构造函数),并将得到的对象返回
案例:获取指定类的所有构造方法,包括方法的返回值类型,参数类型,方法名等信息,不用获取方法的内部信息
package edu.hebeu.decompile;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/*这个例子展示反编译构造方法*/
public class DecompileConstructor {
public static void main(String[] args) {
StringBuilder cString = new StringBuilder(); // 创建一个StringBuilder对象,用来存放反编译class文件之后的字符串信息
Class<?> cClass = null; // 声明一个类的字节码
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
System.out.print("请输入要反编译的类:");
String className = scanner.next();
cClass = Class.forName(className); // 获取整个类的字节码
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(null != scanner) {
scanner.close();
}
}
/**获取类的修饰符列表*/
cString.append(Modifier.toString(cClass.getModifiers())); // 获取类的修饰符信息
cString.append(" class ");
/**获取类的简写名*/
cString.append(cClass.getSimpleName()); // 获取类的简写类名(不包括包名)
cString.append(" {\n");
/**获取所有的构造方法*/
Constructor[] constructors = cClass.getDeclaredConstructors();
for(Constructor c : constructors) {
cString.append("\t");
/**获取构造方法的修饰符列表*/
int modifiersCode = c.getModifiers();
String modifiers = Modifier.toString(modifiersCode);
cString.append(modifiers);
cString.append(" ");
/**获取构造方法名(构造方法名就是类的简写名)*/
String constructorName = cClass.getSimpleName();
cString.append(constructorName);
cString.append(" ");
/**获取形参列表*/
cString.append("(");
Class[] paramTypesClass = c.getParameterTypes();
for(Class<?> paramTypeClass : paramTypesClass) {
String paramType = paramTypeClass.getSimpleName(); // 获取参数类型简写名
cString.append(paramType);
cString.append(",");
}
int endCommaSubScript = cString.lastIndexOf(","); // 获取最后一个 "," 的下标,如果没有 "," 就返回 -1
if(endCommaSubScript != -1) cString.deleteCharAt(endCommaSubScript); // 截取掉倒数第一位的下标(即截取掉最后的 "," 1位字符)
cString.append(") {}\n");
}
cString.append("}");
System.out.println(cString);
}
}
注解
注解,又叫注释类型,注解是一种引用数据类型,编译之后生成 xxx.class 文件;
如何自定义注解?
[修饰符列表] @interface 注解名 {
// …
}
如何使用注解?
注解使用的语法格式: @注解名即可使用;注解可以使用在很多位置上:如类、属性、构造方法、方法、形参、变量上等,注解甚至可以使用在注解类型上;
JDK内置的重要注解
在java.lang包下的注解类型:
- Deprecated:不鼓励程序员使用这种方式的注解,因为它很危险或存在更好的选择;
- Override:表示一个方法声明打算重写超类中的另一个方法声明;
- SuppressWarnings:指示应该在注解元素(以及包含在该注解元素中的所有程序元素)中取消显示指定的编译器警告;
注解中有属性
属性的类型有哪些?
byte、short、int、long、float、double、boolean、char、String、Class、枚举enum,以及前面每种类型的数组形式;
使用注解时的注意
- 如果注解有属性且该属性没有默认值,就必须要对属性赋值;
- 属性赋值的形式为: 属性名 = 值(注意值要与属性指定的类型相同);
- 如果注解中的属性名为value时且注解只有这一个属性时,属性名可以不写,直接赋值,如 “123”,即 值;
元注解
什么是元注解?
用来标注 “注解类型” 的 “注解” 称为 “元注解” ;常见的元注解有:Target、Retention;
关于Target注解
这是一个元注解,即标注 “注解类型” 的 “注解” ,Target注解用来标注 “被标注的注解” 可以出现在哪些位置;
如:
@Target(ElementType.METHOD)
public @interface Override {
}
此时,Target就是元注解,Override是标注注解;这个例子表示Override注解只能出现在方法上;
如何判断出现在哪?
@Target(CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE)依次表示 “被标识的注解” 可以出现在 构造方法、属性、局部变量、方法、包、模块、参数、类上;
关于Retention注解
这是一个元注解,即标注 “注解类型” 的 “注解” ,Retention注解用来标注 “被标注的注解” 最终保存到哪;
如:
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
此时Retention就是元注解,Override是标注注解,这个例子表示Override注解保存在Java源文件中;
Retention的源码分析
public @interface Retention {
RetentionPolicy value(); // 名为 vaule,类型为 Retention的枚举类型 的属性
}// RetentionPolicy的源码
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
保持性策略有哪些?如何区分?
- @Retention(RetentionPolicy.SOURCE) 表示 “被标注的注解” 被保存至Java源文件中
- @Retention(RetentionPolicy.CLASS) 表示 “被标注的注解” 被保存在class文件中
- @Retention(RetentionPolicy.RUNTIME) 表示 “被标注的注解” 被保存在class文件中,并且可以被反射机制所读取到
Override注解
在 java.lang包下的注解;源代码如下:
public @interface Override {
}
注意:
- Override这个注解只能注解方法;
- Override这个注解只能给编译器参考,和运行阶段没有关系;
- 凡是在Java中的方法带有这个注解,编译器对注解的方法会进行编译检查,如果这个方法不是重写父类的方法,编译器会报错;
*结论:*由此可见Override这个注解是为了防止程序员在重写父类方法时出错,是标识性注解;
Deprecated注解
这个注解标注的元素,表示该元素已过时(会通过中划线标识);
如下代码:
public class MyClass {
@Deprecated
public void m1() {
System.out.println("m1");
}
@Deprecated
public int m2() {
System.out.println("m2");
return 0;
}
@Deprecated
public static void m3() {
System.out.println("static m3");
}
@Deprecated
public void mAll() {
System.out.println("mAll");
m3();
}
}
此时如果使用该类中被@Deprecated注解标注的方法,该方法上会出现中划线;
int commaSubScript = cString.lastIndexOf(","); // 获取最后一个 “,” 的下标,如果没有 “,” 就返回 -1
if(commaSubScript != -1) cString.deleteCharAt(commaSubScript); // 截取掉倒数第一位的下标(即截取掉最后的 “,” 1位字符)
cString.append(") {}\n");
}
cString.append("}");
System.out.println(cString);
}
}
### Constructor
*常用方法:*
* String getName(); // 获取Constructor对象(此方法对象)的名称,以String形式返回
* int getModifiers(); // 获取Constructor对象的修饰符列表对应的int值
* Class[] getParameterTypes(); // 获取Constructor对象的(此方法对象)的参数类型列表,以Class[] 形式返回
* Object newInstance(Object... objs); // 执行构造函数(根据传入的可变类型参数决定执行该Constructor对象对应类的哪个构造函数),并将得到的对象返回
*案例:获取指定类的所有构造方法,包括方法的返回值类型,参数类型,方法名等信息,不用获取方法的内部信息*
```java
package edu.hebeu.decompile;
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.util.Scanner;
/*这个例子展示反编译构造方法*/
public class DecompileConstructor {
public static void main(String[] args) {
StringBuilder cString = new StringBuilder(); // 创建一个StringBuilder对象,用来存放反编译class文件之后的字符串信息
Class<?> cClass = null; // 声明一个类的字节码
Scanner scanner = null;
try {
scanner = new Scanner(System.in);
System.out.print("请输入要反编译的类:");
String className = scanner.next();
cClass = Class.forName(className); // 获取整个类的字节码
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
if(null != scanner) {
scanner.close();
}
}
/**获取类的修饰符列表*/
cString.append(Modifier.toString(cClass.getModifiers())); // 获取类的修饰符信息
cString.append(" class ");
/**获取类的简写名*/
cString.append(cClass.getSimpleName()); // 获取类的简写类名(不包括包名)
cString.append(" {\n");
/**获取所有的构造方法*/
Constructor[] constructors = cClass.getDeclaredConstructors();
for(Constructor c : constructors) {
cString.append("\t");
/**获取构造方法的修饰符列表*/
int modifiersCode = c.getModifiers();
String modifiers = Modifier.toString(modifiersCode);
cString.append(modifiers);
cString.append(" ");
/**获取构造方法名(构造方法名就是类的简写名)*/
String constructorName = cClass.getSimpleName();
cString.append(constructorName);
cString.append(" ");
/**获取形参列表*/
cString.append("(");
Class[] paramTypesClass = c.getParameterTypes();
for(Class<?> paramTypeClass : paramTypesClass) {
String paramType = paramTypeClass.getSimpleName(); // 获取参数类型简写名
cString.append(paramType);
cString.append(",");
}
int endCommaSubScript = cString.lastIndexOf(","); // 获取最后一个 "," 的下标,如果没有 "," 就返回 -1
if(endCommaSubScript != -1) cString.deleteCharAt(endCommaSubScript); // 截取掉倒数第一位的下标(即截取掉最后的 "," 1位字符)
cString.append(") {}\n");
}
cString.append("}");
System.out.println(cString);
}
}
注解
注解,又叫注释类型,注解是一种引用数据类型,编译之后生成 xxx.class 文件;
如何自定义注解?
[修饰符列表] @interface 注解名 {
// …
}
如何使用注解?
注解使用的语法格式: @注解名即可使用;注解可以使用在很多位置上:如类、属性、构造方法、方法、形参、变量上等,注解甚至可以使用在注解类型上;
JDK内置的重要注解
在java.lang包下的注解类型:
- Deprecated:不鼓励程序员使用这种方式的注解,因为它很危险或存在更好的选择;
- Override:表示一个方法声明打算重写超类中的另一个方法声明;
- SuppressWarnings:指示应该在注解元素(以及包含在该注解元素中的所有程序元素)中取消显示指定的编译器警告;
注解中有属性
属性的类型有哪些?
byte、short、int、long、float、double、boolean、char、String、Class、枚举enum,以及前面每种类型的数组形式;
使用注解时的注意
- 如果注解有属性且该属性没有默认值,就必须要对属性赋值;
- 属性赋值的形式为: 属性名 = 值(注意值要与属性指定的类型相同);
- 如果注解中的属性名为value时且注解只有这一个属性时,属性名可以不写,直接赋值,如 “123”,即 值;
元注解
什么是元注解?
用来标注 “注解类型” 的 “注解” 称为 “元注解” ;常见的元注解有:Target、Retention;
关于Target注解
这是一个元注解,即标注 “注解类型” 的 “注解” ,Target注解用来标注 “被标注的注解” 可以出现在哪些位置;
如:
@Target(ElementType.METHOD)
public @interface Override {
}
此时,Target就是元注解,Override是标注注解;这个例子表示Override注解只能出现在方法上;
如何判断出现在哪?
@Target(CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, MODULE, PARAMETER, TYPE)依次表示 “被标识的注解” 可以出现在 构造方法、属性、局部变量、方法、包、模块、参数、类上;
关于Retention注解
这是一个元注解,即标注 “注解类型” 的 “注解” ,Retention注解用来标注 “被标注的注解” 最终保存到哪;
如:
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
此时Retention就是元注解,Override是标注注解,这个例子表示Override注解保存在Java源文件中;
Retention的源码分析
public @interface Retention {
RetentionPolicy value(); // 名为 vaule,类型为 Retention的枚举类型 的属性
}// RetentionPolicy的源码
public enum RetentionPolicy {
SOURCE,
CLASS,
RUNTIME
}
保持性策略有哪些?如何区分?
- @Retention(RetentionPolicy.SOURCE) 表示 “被标注的注解” 被保存至Java源文件中
- @Retention(RetentionPolicy.CLASS) 表示 “被标注的注解” 被保存在class文件中
- @Retention(RetentionPolicy.RUNTIME) 表示 “被标注的注解” 被保存在class文件中,并且可以被反射机制所读取到
Override注解
在 java.lang包下的注解;源代码如下:
public @interface Override {
}
注意:
- Override这个注解只能注解方法;
- Override这个注解只能给编译器参考,和运行阶段没有关系;
- 凡是在Java中的方法带有这个注解,编译器对注解的方法会进行编译检查,如果这个方法不是重写父类的方法,编译器会报错;
*结论:*由此可见Override这个注解是为了防止程序员在重写父类方法时出错,是标识性注解;
Deprecated注解
这个注解标注的元素,表示该元素已过时(会通过中划线标识);
如下代码:
public class MyClass {
@Deprecated
public void m1() {
System.out.println("m1");
}
@Deprecated
public int m2() {
System.out.println("m2");
return 0;
}
@Deprecated
public static void m3() {
System.out.println("static m3");
}
@Deprecated
public void mAll() {
System.out.println("mAll");
m3();
}
}
此时如果使用该类中被@Deprecated注解标注的方法,该方法上会出现中划线;