华清远见重庆中心—Java高级技术总结/个人总结
- 一、String字符串
- 二、字符串String类中的常用方法
- 三、可变字符串
- 四、可变字符串与String之间的转换
- 五、可变字符串相关面试题
- 六、System类
- 七、Runtime类
- 八、Date类
- 九、SimpleDateFormat类
- 十、Calendar类
- 十一、包装类
- 十二、字符串与原始类型之间的转换
- 装箱和拆箱
- 异常
- 数组和集合
- Collection接口
- Map接口
- 遍历集合的方式
- 泛型
- Collections集合工具类
- Arrays数组工具类
- 集合和数组之间转化
- 文件类File
- IO
- 流Stream
- FileInputStream文件字节输入流(掌握)
- FileOutputStream文件字节输出流(掌握)
- FileReader文件字符输入流
- BufferedReader缓冲字符输入流(掌握)
- FileWriter文件字符输出流
- BufferedWriter缓冲字符输出流(掌握)
- 序列化ObjectOutputStream对象字节输出流(掌握)
- 反序列化ObjectInputStream对象字节输入流(掌握)
- 序列化和反序列化案例
- InputStreamReader
- OutputStreamWriter
- 进程和线程
- 并行和并发
- 同步和异步
- Java中的线程Thread类
- 实现多线程
- 线程生命周期
- 守护线程
- 多线程访问同一个资源
- 多线程相关面试题
- 模拟死锁出现的情况
- 死锁的解决方式
- 死锁的解决方式
一、String字符串
String是一个类,属于数据类型中的引用类型。
Java的一切使用""引起来的内容,都是这个类的实例,称为字符串对象。
字符串在定义后,值不可改变,是一个常量,实际是一个字符数组。
String类使用时注意
由此可见,如果要频繁改动String类型变量的值,会创建很多字符串对象,效率很低。
所以在频繁改动字符串时,不要使用String类。
如果要频繁改动字符串时,使用StringBuilder或StringBuffer类。
总结
在使用字符串时,字符串是对象,如果要比较其值是否相同,不能使用判断,因为判断的是内存地址。
所以在比较字符串是否相同时,要使用String类重写的equals方法进行判断。
String类中equals重写的原理大致是:
判断是否为同一个字符串,再判断是否是字符串类型,再将两个字符串转换为字节数组,逐一比较字节数组中的内容,全部一致,返回true
调用equals方法时,通常将已知非空字符串作为调用者
username.equals("admin");//username可能为空,会有空指针异常
"admin".equals(username)//能避免空指针异常
二、字符串String类中的常用方法
方法名 | 返回值 | 作用 |
---|---|---|
length() | int | 得到字符串长度 |
toLowerCase() | String | 转换为小写 |
toUpperCase() | String | 转换为大写 |
trim() | String | 去除字符串的首尾全部空格 |
isEmpty() | boolean | 判断字符串长度是否为0 |
getBytes() | byte[] | 转换为字节数组 |
toCharArray() | char[] | 转换为字符数组 |
equalsIgnoreCase(String str) | boolean | 忽略大小写比较字符串是否相同 |
equals(String str) | boolean | 判断两个字符串是否相同 |
charAt(int index) | char | 得到某个索引上的字符 |
indexOf(String str) | int | 得到某个字符串第一次出现的索引,不存在返回-1 |
lastIndexOf(String str) | int | 得到某个字符串最后一次出现的索引,不存在返回-1 |
contains(String str) | boolean | 判断是否存在某个字符串 |
startsWith(String str) | boolean | 判断是否以指定字符串开头 |
endsWith(String str) | boolean | 判断是否以指定字符串结尾 |
concat(String str) | String | 将指定字符串拼接到原字符串末尾 |
substring(int index) | String | 从索引index开始截取字符串至末尾 |
substring(int begin,int end) | String | 截取[begin,end)范围内的字符串 |
split(String regex) | String[] | 根据字符串或正则表达式切分原字符串。相当于split(regex,0) |
split(String regex,int limit) | String[] | 根据字符串或正则表达式切分原字符串,limit为正数,数组长度最大为limit;limit为0,数组长度无限制,但省略最后的空字符串;limit为负数,数组长度无限制。 |
replace(String oldStr,String newStr) | String | 将原字符串中的oldStr替换为newStr |
replaceFirst(String oldStr,String newStr) | String | 将原字符串中第一次出现的oldStr替换为newStr |
String.valueOf(参数) | String | 将参数转换为字符串。参数可以是任何数据。通常用于原始类型转换为字符串 |
String.format(String 格式,Object…obj) | String | 根据指定格式转换参数。常用与将浮点数据保留指定小数位数。\n如String.format(“%4.2f”,2.345)表示将2.345保留2位小数,整体占4位,输出为字符串格式。如果实际数字总位数大于4,原样输出,如果实际数字总位数小于4,会在最前补充空格。 |
三、可变字符串
String字符串对象是一个常量,在定义后,值不可改变。
如果使用String类的对象,对其频繁更新时,就会不停地创建新对象,不停引用给同一个变量。
StringBuilder
用于表示可变字符串的一个类,是非线程安全的,在单线程环境下使用,效率更高。
StringBuffer
用于表示可变字符串的一个类,是线程安全的,在多线程环境下使用。
StringBuilder和StringBuffer中的方法都一致,只不过StringBuffer中的方法使用了synchronized关键字修饰,表示是一个同步方法,在多线程环境下不会出现问题。
这里以StringBuilder为例
构造方法
常用构造方法 | 作用 |
---|---|
StringBuilder() | 创建一个大小为16的字节数组,表示一个空白字符串。 |
StringBuilder(int capacity) | 创建一个指定大小的字节数组,表示一个空白字符串。 |
StringBuilder(String str) | 创建一个str的长度+16大小的字节数组。表示str这个字符串。 |
常用方法
常用方法 | 作用 |
---|---|
append(Object obj) | 将任意数据添加到原可变字符串末尾 |
delete(int start,int end) | 删除[start,end)范围内的字符 |
deleteCharAt(int index) | 删除index索引上的字符 |
insert(int index,Object obj) | 将obj添加到index上 |
replace(int start,int end,String str) | 将[start,end)范围内的字符替换为str |
reverse() | 反转字符串 |
注意
-
String类中的所有方法调用后,都会创建一个新的String对象,即原本的String字符串不会改变
-
StringBuilder类中的所有方法都是在操作同一个字符串对象,每次调用方法,都会让原字符串发生变化
-
StringBuilder类中没有重写equals方法,所以判断两个可变字符串对象是否相同时,如果调用equals方法,
实际调用的是Object类中未重写的方法,即==判断。所以判断可变字符串是否相同时,需要将其转换为String对象再调用equals方法。
四、可变字符串与String之间的转换
String转换为可变字符串
String str="hello";
//使用构造方法将String对象转换为StringBuilder对象
StringBuilder sb = new StringBuider(str);
可变字符串转换为String(任意类型对象转换为String)
-
String.valueOf(Object obj)方法
StringBuilder sb = new StringBuider("hello"); //将任意类型对象转换为String对象 String str = String.valueOf(sb);
-
toString()方法
StringBuilder sb = new StringBuider("hello"); //调用任意对象的toString()方法 String str = sb.toString();
-
拼接空字符串
StringBuilder sb = new StringBuider("hello"); String str = sb+"";
五、可变字符串相关面试题
比较String、StringBuilder和StringBuffer的区别
相同点:
- 这三个类都可以表示字符串。都提供了一些操作字符串的方法。
- 这三个类中有相同的方法,如charAt(),indexOf()等。
- 这三个类都是被final修饰的类,不能被继承
不同点:
- String定义的字符串是一个常量。可变字符串定义的字符串是一个变量。
- String类中的方法调用后,不会改变原本字符串的值。可变字符串中的方法调用后,会改变原本字符串的值
- StringBuilder是非线程安全的可变字符串类,StringBuffer是线程安全的可变字符串类,其中的方法被synchronized修饰。
总结
- 在频繁操作同一个字符串时,一定使用StringBuilder或StringBuffer对象。
- 操作不经常改动的字符串,使用这三个类中相应的方法处理。
六、System类
这个类中包含了一些系统相关的信息和一些方法。其中的属性和方法都是静态的。
这类不能创建对象,不是因为它是以抽象类,而是因为它的构造方法是私有的。
常用方法和属性
常用方法和属性 | |
---|---|
System.ou | 获取标准输出流对象,用于打印信息 |
System.in | 获取标准输入流对象,用于获取输入的信息 |
System.err | 获取错误输出流对象,用于打印异常信息 |
System.exit(int statues) | 终止虚拟机运行,参数0表示正常终止 |
System.currentTimeMills() | 获取从1970/1/1 0:0:0至今经过了多少毫秒。中国是UTC(+8),所以实际是从1970/1/1 8:0:0至今经过了多少毫秒。返回值为long类型。通常称为时间戳。 |
System.arraycopy(原数组,原数组的起始位置,目标数组,目标数组的起始位置,要复制的元素数量) | 将原数组中指定数量的元素复制到新数组中 |
七、Runtime类
Runtime类的对象,表示程序运行时对象(程序运行环境对象)。
包含了程序运行环境相关的信息。常用语获取运行环境信息(如虚拟机信息),或执行某个命令。
特点
这个类不是一个抽象类,但不能创建对象,因为它的构造方法是私有的。
这个类提供了一个静态方法getRuntime(),通过该方法,可以获取一个Runtime类的对象。
这种方式可以保证该类只能创建一个对象,是Java中的一种设计模式:单例模式。
public class Runtime{
//定义了一个私有的静态成员,创建一个当前类的对象
private static Runtime currentRuntime = new Runtime();
//将构造方法私有,无法在外创建对象
private Runtime();
//定义了一个公开的静态方法,用于获取创建的唯一的当前类的对象
public static Runtime getRuntime(){
return currentRuntime;
}
}
使用
public class Test {
public static void main(String[] args) throws IOException, InterruptedException {
//如果要使用Runtime类中的方法,先通过其静态方法获取它的对象
Runtime runtime = Runtime.getRuntime();
System.out.println("当前虚拟机的空闲内存"+runtime.freeMemory() / 1024 / 1024 + "MB");//b kb MB
System.out.println("当前虚拟机的实际最大内存"+runtime.totalMemory()/ 1024 / 1024 + "MB");
System.out.println("当前虚拟机的支持的最大内存"+runtime.maxMemory()/ 1024 / 1024 + "MB");
//exec(String 指令名) 运行某个指令,返回运行的进程对象
//mspaint画图 calc计算器 notepad记事本
Process mspaint = runtime.exec("mspaint");
Thread.sleep(3000);
//销毁进程对象
mspaint.destroy();
/*runtime.exec("calc");
runtime.exec("notepad");*/
//定时关机
//runtime.exec("shutdown -s -t 600");
//runtime.exec("C:\\Program Files (x86)\\Tencent\\QQ\\Bin\\QQ.exe");
}
}
八、Date类
date日期 data数据
用于表示日期时间的类,位于java.util包下
构造方法
常用构造方法 | 说明 |
---|---|
Date() | 创建当前时间对应的日期对象 |
Date(long l) | 创建指定瞬间对应的日期对象 |
Date(int year,int month,int date) | 根据年月日创建日期对象。 |
常用方法
常用方法 | 作用 |
---|---|
getTime() | 得到Date对应对象的毫秒数 |
after(Date when) | 判断参数是否在调用日期之后 |
before(Date when) | 判断参数是否在调用日期之前 |
九、SimpleDateFormat类
用于格式化日期的类。
构造方法
常用构造方法 | 作用 |
---|---|
SimpleDateFormat(String pattern) | 创建一个指定日期模板的格式化日期对象 |
日期模板
特殊字符 | 作用 |
---|---|
yyyy | 年 |
MM | 月 |
dd | 日 |
hh | 12小时制 |
HH | 24小时制 |
mm | 分 |
ss | 秒 |
E | 星期 |
yyyy/MM/dd HH:mm:ss E | 2023/03/09 14:05:16 周四 |
两个字母都可以写成一个,如5月,M–5,MM–05
常用方法
常用方法 | 返回值 | 作用 |
---|---|---|
format(Date date) | String | 将Date对象按日期模板转换为字符串 |
parse(String str) | Date | 将满足日期模板的字符串转换为Date对象 |
package com.hqyj.dateTest;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Scanner;
public class DateFormatTest {
public static void main(String[] args) throws ParseException {
//根据指定模板创建格式化日期对象
//SimpleDateFormat sdf = new SimpleDateFormat("yyyy年MM/dd HH:mm:ss E");
//format(Date date)将Date对象格式化,返回格式化后的字符串
//String format = sdf.format(new Date());
//System.out.println(format);
//SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
//将满足日期模板的字符串转换为Date对象
//Date birthday = sdf.parse("1999/9/9");
//System.out.println(birthday);
//计算两个输入的日期之间相隔的天数
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd");
Scanner sc = new Scanner(System.in);
System.out.println("请输入第一个日期,格式如 1999/5/21");
String dateString1 = sc.next();
System.out.println("请输入第二个日期,格式如 1999/5/21");
String dateString2 = sc.next();
//将日期字符串转换为Date对象
Date date1 = sdf.parse(dateString1);
Date date2 = sdf.parse(dateString2);
//得到日期对应的毫秒数
long l = date1.getTime() - date2.getTime();
System.out.println("相隔" + (Math.abs(l) / 1000 / 3600 / 24) + "天");
}
}
十、Calendar类
表示日历的类,包含了很的多日历相关的信息。
是一个抽象类,无法创建对象,可以通过静态方法getInstrance()获取Calendar类的实例。
//获取Calendar类的实例
Calendar c = Calendar.getInstance();
日历字段
在Calendar类中,定义了很多被final static修饰的静态常量,称为日历字段。实际是一个数字,用于获取指定日历信息。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jSLB4HqS-1680263776744)(JavaAdv02.assets/image-20230309145131821.png)]
常用方法
常用方法 | 作用 |
---|---|
get(int field) | 根据日历字段获取对应的值 |
getMaximum(int field) | 获取指定日历字段的最大值,如日期最大为31 |
getActualMaximum(int field) | 获取指定日历字段的实际最大值,如11月的日期最大为30 |
getTime() | 将Calendar对象转换为Date对象 |
set(int field,int value) | 将指定的日历字段设置为指定值 |
set(int year,int month,int date) | 同时设置日历的年月日 |
setTime(Date date) | 将Date对象作为参数设置日历的信息 |
使用Calendar类实现万年历
package com.hqyj.dateTest;
import java.util.Calendar;
import java.util.Scanner;
public class Test2 {
public static void main(String[] args) {
//实现"万年历"
//输入年份和月份,输出
Calendar c = Calendar.getInstance();
Scanner sc = new Scanner(System.in);
System.out.println("输入年份");
int year = sc.nextInt();
System.out.println("输入月份");
int month = sc.nextInt();
//日历设置为当月1号
c.set(year, month - 1, 1);
//得到当月最大日期
int maxDate = c.getActualMaximum(Calendar.DATE);
//换行计数
int count = 0;
//输出空格
/*
* DAY_OF_WEEK 星期 空格数量
* 2 一 0
* 3 二 1
* 4 三 2
* 5 四 3
* 6 五 4
* 7 六 5
* 1 天 6
* //周天空6 其余空DAY_OF_WEEK-2
* */
System.out.println("一\t二\t三\t四\t五\t六\t日");
//获取当月1号是一周中的第几天
int week = c.get(Calendar.DAY_OF_WEEK);
//周天空6格
if (week == 1) {
System.out.print("\t\t\t\t\t\t");
//空格也需要计数
count += 6;
} else {
//其他情况空星期-2个格
for (int j = 1; j <= week - 2; j++) {
System.out.print("\t");
//空格也需要计数
count++;
}
}
//输出数字
for (int i = 1; i <= maxDate; i++) {
System.out.print(i + "\t");
//计数+1
count++;
//计数到7换行
if (count % 7 == 0) {
System.out.println();
}
}
}
}
方法调用时传值问题
总结
参数为引用类型(类、数组、接口),且在方法中直接操作该参数时,才会对实际参数造成影响。
public static void fun(char[] list,Person p){
list[0]='m';//方法内部直接操作实际参数,会影响实际参数
p = new Person();//方法内部创建了新对象给实参重新赋值,不会影响实际参数
p.setName("xxx");
}
public static void main(String[] args){
Person p = new Person();
p.setName("qwe");
char[] list ={'a','b','c'};
fun(list,p);
System.out.println(p.getName());//qwe
System.out.println(list[0]);//m
}
十一、包装类
Java是纯面向对象语言,宗旨是将一切事物视为对象处理。
但原始类型不属于对象,不满足面向对象的思想。但原始类型无需创建对象,保存在栈中,效率更高。
为了既保证效率又让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以就有了包装类的概念。
包装类就是原始类型对应的类类型。
包装类常用于字符串与原始类中之间的转换。
在web应用中,从浏览器页面中获取数据提交到服务器,全部都是String类型,所以一定要使用字符串转换为原始类型的方法。
包装类 | 原始类型 |
---|---|
Byte | byte |
Short | short |
Integer | int |
Long | long |
Float | float |
Double | double |
Character | char |
Boolean | boolean |
特点
-
八个原始类型中,除了int和char,其余包装类都是将原始类型的首字母改为大写。
int对应Integer,char对应Character
-
包装类都是被final修饰的,不能被继承
-
除了Character类,其余包装类都有两个过时的构造方法,参数为对应的原始类型或字符串
Character只有一个参数为char类型的构造方法
构造方法的目的都是将原始类型的数据转换为包装类的对象
-
除了Character类,其余包装类都有静态方法"parse原始类型单词(String str)",用于将字符串转换为相应的原始类型
- 数值型的包装类parseXXX()方法,如果参数不是对应的数字,就会抛出NumberFormat异常,如"123a"或"123.4"都会报错
- boolean的包装类Boolean的parseBoolean()方法,如果参数不是"true"这个单词的四个字母,转换结果都是false
-
除了Boolean类,其余包装类都有MAX_VALUE和MIN_VALUE这两个静态属性,用于获取对应类型支持的最大最小值
-
所有包装类都重写了toString(),用于将包装类对象转换为String对象
十二、字符串与原始类型之间的转换
字符串转换为原始类型
使用原始类型对应的包装类,调用parseXXX(String str)方法
String num="123";
byte b = Byte.parseByte(num);//123
short s = Short.parseShort(num);//123
int i = Integer.parseInt(num);//123
long l = Long.parseLong(num);//123
float f = Float.parseFloat(num);//123.0
double d = Double.parseDouble(num);//123.0
boolean flag = Boolean.parseBoolean(num);//false
原始类型转换为字符串
-
使用String类的String.valueOf(Object obj)
int num=12; String str = String.valueOf(num);
-
拼接空白字符串
int num=123; String str = num+"";
-
将原始类型转换为包装类后,调用toString()
int num=123; Integer integer=new Integer(num); String str = interger.toString();
装箱和拆箱
装箱
拆箱
自动装箱和拆箱
-
自动装箱缓冲区
//i1和i2保存的数字在byte范围[-127,127]内,这个值会共享,只会有一个"100"对象 Integer i1 = 100; Integer i2 = 100; System.out.println(i1 == i2);//i1和i2引用同一个地址,结果为true //i3和i4保存的数字不在byte范围[-127,127]内,会创建对象 Integer i3 = 128;//128对象 Integer i4 = 128;//128对象 System.out.println(i3 == i4);//i3和i4引用不同的地址,结果为false System.out.println(i3.equals(i4));//包装类重写了equals,会拆箱后判断,结果为ture
- 使用自动装箱给包装类对象赋值,值的范围在byte范围[-127,127]内,这个值会保存在缓冲区中,如果多个对象都使用这个值,共享这一个数据,使用同一个地址,==判断结果true;值的范围不在byte范围[-127,127]内,就会创建新的包装类对象,会有不同的地址,==判断结果false
- 引用类型对象比较相同时,不要使用==,包括包装类的对象。比较相同时,使用包装类重写的equals方法
异常
当程序没有按开发人员的意愿正常执行,中途出现错误导致程序中断,这种情况,就称为异常。
学习异常就是认识异常的种类,如何处理异常和避免异常出现。
异常的产生
异常在程序中以对象的形式存在。当代码执行过程中出现异常,虚拟机会自动创建一个异常对象,如果没有对该异常对象进行处理,就会导致程序中断,不再执行后续内容。
异常的分类
异常在程序中以对象的形式存在,就有相应的类。
所有的异常类,组成了"异常家族"。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CG28skgH-1680263776746)(JavaAdv03.assets/image-20230310152624064.png)]
Error错误
如果出现xxxError,如StackOverFlowError,栈溢出,无法通过额外的代码解决,只能修改源代码。
Exception异常
-
RunTimeExcetpion运行时异常
如果一个异常类属于RunTimeExcetpion异常类的子类,称这个异常为运行时异常,可以通过编译,运行时可能抛出异常对象
常见运行时异常 说明 出现的情景 NullPointerException 空指针异常 如用空对象null调用属性或方法 IndexOutOfBoundsException 索引越界异常 如当使用某个带有索引的对象超出范围 NumberFormatException 数字格式异常 如调用包装类的parseXX()方法,如果参数不能转换 InputMismatchException 输入不匹配异常 如使用Scanner接收控制台输入时,如果输入的数据不是对应的类型 ClassCastException 对象转型异常 如Person p = (Person)Dog dog; ArithmeticException 算术运算异常 如0当分母 -
编译时异常
如果一个异常类不属于RunTimeExcetpion异常类的子类,称这个异常为编译时异常,无法通过编译,必须要处理异常后才能编译运行。
常见编译时异常 说明 出现的情景 IOException 输入输出流异常 使用流对象 FileNotFoundException 文件未找到以 方法的参数为文件对象时 SQLException 数据库相关异常 操作数据库时
处理异常
通常所说的处理异常,指的是处理Exception类的子类异常。
处理异常的目的,就是保证程序正常执行。
方式一:try-catch-finally语句
这种方式处理异常,无论会不会抛出异常,都能让程序正常执行
try{
//可能出错的代码
}catch(异常类 异常对象){
//如果try中的代码抛出异常,异常对象属于catch中的异常类型,就会执行这里的代码
}catch(异常类 异常对象){
//如果try中的代码抛出异常,异常对象属于catch中的异常类型,就会执行这里的代码
}...{
}finally{
//无论程序是否会抛出异常,都要执行这里的代码
}
执行流程:先执行try中的内容,当出现异常,与后续每个catch中的异常类型进行匹配,如果匹配到对应的类型或异常父类时,执行后续大括号中的内容,最终一定执行finally中的内容。
try-catch-finally使用时注意
-
如果代码会抛出多个异常,可以使用多个catch进行捕获。需要将子类异常放在最前,父类异常放在最后
-
try、catch、finally都不能单独使用,try必须配合catch或finally或一起使用
-
无论try中的代码是否会抛出异常,finally中的代码一定会执行
-
执行try中的代码是,如果出现异常,就不再执行try中剩余代码
-
try中定义的内容,无法在try之外的地方使用
-
try中如果有return,不影响finally的执行,finally优先于return执行
方式二:throws关键字
这种方法,可以让编译时异常通过编译。
在定义方法的时候,通过该关键字声明方法可能抛出的异常。
用法:方法的参数部分后,添加 throws 异常类型1,异常类型2…
public class Test{
//这时该方法就会有一个声明:该方法可能会抛出InterruptedException异常
public void fun() throws InterruptedException{
//sleep()方法在源码中声明了可能会抛出InterruptedException异常,
//InterruptedException异常不是RuntimeException的子类异常,必须要处理才能通过编译
//要么使用try-catch处理,要么继续声明有异常
Thread.sleep(500);
}
}
throw和throws
-
throws表示用于声明方法有可能出现的异常。使用时写在方法的小括号之后
public void fun() throws InterruptedException{ Thread.sleep(500); }
-
throw用于手动抛出异常对象。使用时,写在方法体中,“throw 异常对象”。
常用于满足某种条件时,强制中断程序。
public void fun(){ throw }
自定义异常
如果需要在某种情况下中断程序,可以自定义一个异常类。再通过throw关键字手动抛出自定义异常。
自定义异常步骤
1.创建一个类,继承某个异常类
- 如果继承的是RuntimeException,表示自定义的异常类属于运行时异常,该异常对象可以不用处理
- 如果继承的是非RuntimeException,表示自定义的异常属于编译时异常,该异常对象必须要处理
2.可选操作。定义带参构造方法,参数为String类型的异常信息,调用父类中的构造方法
/*
* 自定义异常
* 只需继承某个异常类即可
* 是否定义构造方法根据实际情况决定
* */
public class MyException extends NullPointerException {
/*
* 带参构造,参数为异常信息
* */
public MyException(String msg){
super(msg);
}
/*
* 无参构造
* */
public MyException(){
super();
}
}
数组和集合
数组的特点
- 数组中保存的元素都是有序的,可以通过索引快速访问
- 数组中保存的元素都是同一种类型
- 数组的长度在定义后,无法改变
- 数组无法获取其中保存的元素实际数量
集合的特点
- 能保存一组数据,元素可以有序或无序(存入的顺序和读取的顺序不一致)
- 集合中保存的元素的数据类型可以不同
- 集合的容量可变
- 可以获取集合中保存的元素实际数量
Collection接口
该接口中有两个核心子接口:List和Set。
这两个接口都可以保存一组元素,List接口保存元素有序可重复,Set接口保存元素无序不重复。
Collection接口有一个父接口Iterable,它不是一个集合,而是用于遍历集合的工具接口,包含forEach()和iterator()方法
常用方法 | 返回值 | 作用 |
---|---|---|
add(Object obj) | boolean | 将元素添加到集合中 |
size() | int | 获取集合中的元素数量 |
isEmpty() | boolean | 判断集合是否为空 |
clear() | void | 清空集合 |
contains(Object obj) | boolean | 判断集合中是否包含指定元素 |
remove(Object obj) | boolean | 移除集合中的指定元素 |
toArray() | Object[] | 将集合转换为数组 |
stream() | Stream | 获取集合的流对象,用于遍历集合 |
iterator() | Iterator | 得到集合的迭代器对象,用于遍历集合 |
List接口(有序可重复)
有序集合,元素可以重复,允许保存Null,可以通过索引获取对应的元素。
List接口在继承Colletion接口后,又拓展了一些操作元素的方法。
拓展方法 | 返回值 | 作用 |
---|---|---|
get(int index) | Object | 得到指定索引的元素 |
set(int index,Object obj) | Object | 使用obj替换index上的元素,返回被替换的元素 |
add(int index,Object obj) | void | 将obj添加到index上 |
remove(int index) | Object | 移除指定索引的元素,返回被移除的元素 |
indexOf(Obejct obj) | int | 得到obj第一次出现的索引 |
lastIndexOf(Object obj) | int | 得到obj最后一次出现的索引 |
subList(int from,int to) | List | 截取[from,to)区间内的元素,返回子集合 |
List.of(E… element) | List | 根据参数创建一个不可变集合,该集合不能对其中的元素进行修改 |
ArrayList实现类(掌握)
- 采用数组实现的集合
- 可以通过索引访问元素,可以改变集合大小,如果要在其中插入或删除元素时,会影响后续元素
- 该集合查询效率高,中途添加和删除元素效率低
- 集合中保存的都是引用类型。如集合中保存123,保存的不是int类型的123,而是Integer类型的123
构造方法
构造方法 | 说明 |
---|---|
ArrayList() | 创建一个Object类型的空数组。在调后续添加方法时,才会初始化数组大小为10。 |
ArrayList(int initialCapacity) | 创建一个指定容量的Object类型数组,如果参数为负数,会抛出IllegalArgumentException异常 |
ArrayList(Collection c) | 根据指定集合创建Object类型数组 |
常用方法
主要以List接口和Collection接口中的方法为主。
LinkedList实现类
-
采用双向链表实现的集合
-
集合中保存的每个元素称为节点,除首尾节点外,其他节点既保存了自己的数据,还保存了其前后节点的地址
-
如果在双向链表的结构中进行插入和删除节点的操作时,不会影响其他节点现在的保存位置。
添加的节点只需记录前后节点的位置接口。
-
如果要查询某个节点的地址时,需要从头结点或尾节点开始搜索目标节点的位置
-
双向链表在中间插入和删除数据效率高,随机读取的效率低
构造方法
常用构造方法 | 说明 |
---|---|
LinkedList() | 创建一个空双向链表 |
常用方法
主要以List接口和Collection接口中的方法为主。由于还实现了Deque接口,所以还有一些Deque接口中的方法。如操作首尾节点的方法。
常用来自于Deque接口中的方法 | 作用 |
---|---|
addFirst(Object obj) | 添加obj为头结点 |
addLast(Object obj) | 添加obj为尾结点 |
getFirst() | 得到头结点 |
getLast() | 得到尾节点 |
removeFirst() | 移除头结点 |
removeLast() | 移除尾节点 |
ArrayList和LinkedList的区别
- 这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null
- ArrayList采用数组实现,随机读取效率高,插入和删除效率低,适用于查询
- LinkedList采用双向链表实现,插入和删除效率高,随机读取效率低,适用于频繁更新集合
Set接口(无序不重复)
无序集合,元素不能重复,允许保存null,没有索引
Set接口中的方法都是继承于Collection接口。
哈希表hash table
哈希表,也称为散列表,是一种数据结构,能更快速地访问数据。
假设原本的数据为左侧的数组,如要查询10,需要遍历数组,效率不高。
通过一个特定的函数"原始值%5",得到一组新数组,让新数据重新对应元素,保存到新数组中,这个新数组称为哈希表。
这时如果要查询10,由于哈希函数是10%5得到0,所以直接查询哈希表中0对应的元素即可。
整个过程中,这个函数称为哈希函数,得到的新数组称为哈希码,新数组称为哈希表,对应关系称为哈希映射。
这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实际值不同)。
为了解决哈希冲突,可以使用"拉链法",将冲突的数据保存在对应哈希码之后的链表中。
哈希码的特点
- 如果两个对象的hashcode不同,这两个对象一定不同
- 如果两个对象的hashcode相同,这两个对象不一定相同
- hashcode相同,对象不同,这种现象称为哈希冲突
- “通话”和"重地"这两个字符串的hashcode相同,但是两个不同的对象
HashSet实现类
- 采用哈希表实现
- 元素不能重复,无序保存,允许保存一个null
- 本质是HashMap对象
- 使用HashSet集合时,通常要重写实体类中的equals和hashcode方法
构造方法
常用构造方法 | 说明 |
---|---|
HashSet() | 创建一个空集合,实际是创建了一个HashMap对象 |
常用方法
HashSet中没有定义属于自定的方法,都是父接口Set和Collection中的方法。
HashSet添加数据的原理
如果添加的两个元素的equals方法结果为true且hashcode相同时,视为同一个对象,不能添加。
每次向集合中添加元素时,先判断该元素的hashcode是否存在
- 如果不存在,视为不同对象,直接添加
- 如果存在,再判断equals方法的结果
- 如果true,视为同一个对象,不能添加
- 如果false,视为不同对象,可以添加
由此可见,不能添加的条件是两个对象的hashcode相同且equals的结果为true。
可以只判断equals的结果。但是如果每次都判断equals,由于重写equals时会判断很多属性,效率不高。
如果只判断hashcode是否相同,效率高,但可能会出现哈希冲突。
所以先判断hashcode,再判断equals,既能保证效率,又能保证不会添加重复元素。
equals和hashcode的关系
-
如果两个对象的equals方法结果为true,在没有重写equals方法的前提下,hashcode相同吗?
- 如果没有重写equals方法,默认使用Object中的方法,使用==判断,如果结果为true,说明是同一个地址,同一个对象,hashcode一定相同
-
如果两个对象的hashcode不同,在没有重写equals方法的前提下,equals方法的结果为?
- hashcode不同,说明不是同一个对象,没有重写equals,说明使用Object中的方法,使用==判断,结果为false
-
如果两个对象的hashcode相同,equals方法的比较结果为?
-
可能为true或false
String str1="hello"; String str2="hello"; //str1和str2使用同一个地址,hashcode相同,equals结果为true String str3="通话"; String str4="重地"; //str3和str4不是同一个地址,但hashcode相同,这种情况称为哈希冲突,equals结果为false
-
HashSet的应用
如果要保存的对象保证不重复,且无关顺序,可以使用HashSet。重写要保存的元素的equals和hashcode方法。
Student类,保证添加对象时,不重复
public class Student {
private int id;
private String name;
private String major;
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", major='" + major + '\'' +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return id == student.id && Objects.equals(name, student.name) && Objects.equals(major, student.major);
}
/*
* 重写hashcode方法,根据所有属性生成哈希码
* */
@Override
public int hashCode() {
return Objects.hash(id, name, major);
}
public Student(int id, String name, String major) {
this.id = id;
this.name = name;
this.major = major;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getMajor() {
return major;
}
public void setMajor(String major) {
this.major = major;
}
}
StudentManager类
public class StudentManager {
private HashSet<Student> hs = new HashSet<>();
//添加时,如果对象的属性都一致,视为同一个对象,不能重复添加
public void addStudent(Student student){
hs.add(student);
}
}
TreeSet实现类
-
特殊的Set实现类,数据可以有序保存,可以重复,不能添加null
-
采用红黑树(自平衡二叉树)实现的集合
- 二叉树表示某个节点最多有两个子节点
- 某个节点右侧节点值都大于左侧节点值
- 红黑树会经过"变色"和旋转达到二叉树的平衡
-
只能添加同一种类型的对象且该对象实现了Comparable接口
- 每次调用add方法添加元素时,会自动调用Comparable接口中的方法compareTo()方法
- 实现Comparable接口后必须要重写compareTo()方法,用于决定新添加的元素放在旧元素之前或之后
-
compareTo方法的返回值决定了能否添加新元素和新元素的位置
- 如果返回0,视为每次添加都是同一个元素,不能重复添加
- 如果返回正数,将新元素添加到现有元素之后
- 如果返回负数数,将新元素添加到现有元素之前
-
添加的元素可以自动排序
构造方法
常用构造方法 | 说明 |
---|---|
TreeSet() | 创建一个空集合 |
常用方法
能使用Set和Collection接口中的方法,还定义了一些属于它的方法
独有方法 | 作用 |
---|---|
first() | 得到集合中的第一个元素 |
last() | 得到集合中的最后一个元素 |
ceiling(Object obj) | 得到集合中比参数大的元素中的最小元素 |
floor(Object obj) | 得到集合中比参数小的元素中的最大元素 |
TreeSet的应用
如果要保存的元素需要对其根据某个属性排序,使用该集合。
如在集合中保存整数,即Integer对象,Integer类已经实现了Comparable接口,
如要保存自定义的元素,必须要实现Comparable接口,重写compareTo方法,自定义排序规则。
Person类
public class Person implements Comparable {
private String name;
private int age;
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/*
根据age属性升序保存
*/
@Override
public int compareTo(Object o) {
return this.getAge() - ((Person) o).getAge();
}
}
Main类
public class Test {
public static void main(String[] args) {
//使用TreeSet保存Person对象,Person有age属性,按age升序保存到集合
TreeSet<Person> personSet = new TreeSet<>();
Person p1 = new Person("小王",20);
Person p2 = new Person("小李",15);
Person p3 = new Person("小赵",21);
personSet.add(p1);
personSet.add(p2);
personSet.add(p3);
System.out.println(personSet);
}
}
Map接口
Map称为映射,该集合中保存的数据是以键值对的形式保存,保存的键与值的映射关系。
键称为Key,值称为Value,键不能重复,允许出现一个null作为键,值没有限制。
键和值都必须是引用类型。
如,yyds – 永远单身 这就是一个映射关系,"yyds"就是键key,永远单身"就是值value
常用方法 | 作用 |
---|---|
size() | 得到键值对的数量 |
isEmpty() | 判断是否为空集合 |
clear() | 清空所有键值对 |
put(Object key,Object value) | 添加一组键值对 |
get(Object key) | 根据键得到对应的值 |
containsKey(Object key) | 判断是否存在某个键 |
containsValue(Object value) | 判断是否存在某个值 |
keyset() | 得到键的集合 |
values() | 得到值的集合 |
entrySet() | 得到键值对的集合 |
remove(Object key) | 删除指定的键值对 |
HashMap实现类
构造方法
常用构造方法 | 说明 |
---|---|
HashMap() | 创建一个大小为16,加载因子为0.75的空集合 |
常用方法
使用Map接口中的方法
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M6Cl7DDG-1680263776748)(JavaAdv04.assets/ce0bf8c595e14ecd2ee1206ce9f8cad5.png)]
- JDK1.8之后,HashMap采用"数组+链表+红黑树"实现
- 当没有出现哈希冲突时,元素保存在数组中
- 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
- 如果链表的长度大于8,将链表转换为红黑树
遍历集合的方式
遍历List集合
-
普通for循环
for(int i=0;i<集合.size();i++){ 元素 变量 = 集合.get(i); }
-
增强for循环
for(数据类型 变量名 : 集合){ 元素 变量 = 集合.get(i); }
-
forEach()方法
使用该方法遍历集合时,不要使用add或remove操作,遍历会抛出异常。
集合.forEach(obj -> { 元素 变量 = 集合.get(i); });
-
迭代器
//Collection接口有一个父接口Iterable,其中有一个iterator方法用于获取迭代器对象遍历集合 //所有Collection的子实现类都能调用该方法 Iterator it = Collection集合.iterator(); //hasNext()判断是否还有下一个元素 while(it.hasNext()){ //next()方法读取该元素 元素 变量 = it.next(); }
遍历Set集合
-
普通for循环无法遍历Set集合,因为元素没有索引
-
增强for循环
for(数据类型 变量名 : 集合){ 元素 变量 = 集合.get(i); }
-
forEach()方法
集合.forEach(obj -> { 元素 变量 = 集合.get(i); });
-
迭代器
//Collection接口有一个父接口Iterable,其中有一个iterator方法用于获取迭代器对象遍历集合 //所有Collection的子实现类都能调用该方法 Iterator it = Collection集合.iterator(); //hasNext()判断是否还有下一个元素 while(it.hasNext()){ //next()方法读取该元素 元素 变量 = it.next(); }
遍历HashMap集合
Set keySet = 集合.keySets();
for(Object key :keySet){
Object value=集合.get(key);
}
泛型
一种规范,常用于限制集合中的元素类型。省去遍历集合时转换Object对象的过程
//默认可以保存任意类型的元素,即Object类型
List list = new ArrayList();
list.add(123);
list.add("hello");
//遍历时只能使用Object类型获取
for(Object obj : list){
}
使用泛型
在定义集合时,在集合类或接口后写上**<引用数据类型>**
集合类或接口<引用数据类型> 集合遍历名 = new 集合实现类();
//定义只能保存整数的集合,要使用整数的包装类类型
List<Integer> list = new ArrayList();
list.add(123);
//不能添加非整数
//list.add("hello");
Collections集合工具类
-
Collection是集合的根接口,定义了操作集合中元素的方法。
-
Collections是集合的工具类,定义了操作集合中元素的静态方法。
Collections中的静态方法 | 说明 |
---|---|
Collections.shuffle(List list) | 打乱集合中元素的顺序 |
Collections.sort(List list) | 对集合中的元素进行排序。元素必须实现Comparable接口。 |
Collections.swap(List list,int a,int b) | 将集合中索引a和b的元素交换位置 |
Collections.reverse(List list) | 反转集合中的元素 |
Collections.max(Collection c) | 得到集合中元素的最大值。元素必须实现Comparable接口。 |
Collections.rotate(List list,int distance) | 将集合中最后distance个元素放在集合最前 |
Collections.fill(List list,Object obj) | 使用obj填充集合 |
Arrays数组工具类
包含了一些操作数组的静态方法
常用静态方法 | 说明 |
---|---|
Arrays.sort() | 对数组中的元素升序排序 |
Arrays.asList(T… obj) | 将可变参数转换为ArrayList集合 |
集合和数组之间转化
数组转换为集合
//调用Arrays工具类的asList(1,2,6,22,11)或asList(数组)
ArrayList<Integer> list = Arrays.asList(1,2,6,22,11);
集合转换为数组
ArrayList list = new ArrayList();
list.add("sdf");
list.add(123);
list.add(null);
//调用集合的toArray()方法
Object[] objs = list.toArray();
无论是数组转换集合还是集合转换数组,都可以进行遍历。
如果集合转换为数组,遍历集合,通过索引赋值。
如果数组转换为集合,遍历数组,通过add()添加元素
文件类File
Java中的File类,表示本地硬盘中的文件file或文件夹directory的一个类。
通过这个类创建对象,可以读取文件信息或操作对应文件。
构造方法
常用构造方法 | 说明 |
---|---|
File(String pathName) | 根据文件的完整路径创建File对象 |
File(String parent,String child) | 根据文件的父目录的路径和自身的名称创建File对象 |
File(File parent,String child) | 根据文件的父目录对应的File对象和自身的名称创建File对象 |
//使用不同构造方法表示C:\Users\Administrator\Desktop\230202.txt文件
//new File(String pathName);
File file1 = new File("C:\\Users\\Administrator\\Desktop\\230202.txt");
//new File(String parentPath,String childName);
File file2 = new File("C:\\Users\\Administrator\\Desktop", "230202.txt");
//new File(File parentFile,String childName)
File parent = new File("C:\\Users\\Administrator\\Desktop");
File file3 = new File(parent, "230202.txt");
System.out.println(file1.exists());
System.out.println(file2.exists());
System.out.println(file3.exists());
常用方法
常用方法 | 作用 |
---|---|
exists() | 判断文件是否存在 |
isFile() | 判断是否为文件 |
isDirectory() | 判断是否为文件夹 |
getName() | 获取文件名 |
getPath() | 获取文件相对路径 |
getAbsolutePath() | 获取文件绝对路径 |
getParent() | 获取文件父目录路径 |
getParentFile() | 获取文件父目录对象 |
lastModified() | 获取文件最后一次修改时间 |
length() | 获取文件字节大小 |
isHidden() | 判断是否为隐藏文件 |
delete() | 删除文件或空文件夹 |
renameTo(File newFile) | 移动文件到指定文件,可重命名 |
mkdir() | 创建文件夹 |
list() | 得到文件夹中的第一层子文件名称的数组,返回String[] |
listFiles() | 得到文件夹中的第一层子文件对象的数组,返回File[] |
斐波那契数列
public class Test2 {
public static void main(String[] args) {
//兔子问题
//有一对兔子,在第三个月开始,每个月都生一公一母两只小兔子
//假设所有兔子都不死亡,第10个月一共有多少只
//1月 2月 3月 4月 5月 6月 7月 8月 9月 10月
//1 1 2 3 5 8 13 21 34 55
//斐波那契数列
System.out.println(fun(3));
System.out.println(fun(10));
}
/*
* 递归调用
* */
public static int fun(int n) {
if (n > 2) {
return fun(n - 1) + fun(n - 2);
}
return 1;
}
}
递归遍历文件夹
public class Test3 {
public static void main(String[] args) {
//输出某个文件夹下的所有文件
File source = new File("D:\\GamePP Wonderful Moment");
showAllFile(source);
}
public static void showAllFile(File source){
//判断如果是文件夹
if(source.isDirectory()){
//展开第一层
for (File child : source.listFiles()) {
//child就是第一层的所有子文件,有可能还是文件夹,递归调用本方法
showAllFile(child);
}
}
//输出名称
System.out.println(source.getName());
}
}
IO
I:input输入
O:output输出
流Stream
在Java中,流表示计算机硬盘与内存之间传输数据的通道
将内存中的数据存入到硬盘中,称为写write,也称为输出Output
将硬盘中的数据存入到内存中,称为读read,也称为输入Input
流的分类
Java中的流也是类,以对象的实现表示流。流有"四大家族",是所有流的父类。
字节输入流InputStream
FileInputStream、ObjectInputStream
字节输出流OutputStream
FileOutputStream、ObjectOutputStream
字符输入流Reader
FileReader、BufferedReader、InputStreamReader
字符输出流Writer
FileWriter、BufferedWriter、OutputStreamWriter
按方向分类
- 输入流:InputStream、Reader
- 读硬盘中的数据到程序中
- 输出流:OutputStream、Writer
- 将程序中的数据写到硬盘中
按数据传输类型分类
- 字节流:InputStream、OutputStream
- 读写非文本类型文件。如图片、多媒体文件
- 字符流:Reader、Writer
- 读写纯文本文件。如txt、md等
如要将硬盘中的某个txt文件中的内容读取到程序中,使用Reader
如要将硬盘中的某个图片文件中的内容读取到程序中,使用InputStream
如要将程序中的文本写入到硬盘中,保存为txt文件时,使用Writer
如要将程序中的数据写入到硬盘中,保存为非文本文件时,使用OutputStream
流的四个父类的特点
-
这四个父类都是在java.io包下,都是抽象类,不能直接创建其对象,使用其子类对象
-
这四个类都有close()方法,用于关闭流对象,释放资源
-
输入流(InputStream和Reader)都有read()方法,用于读取数据,输出流(OutputStream和Writer)都有write()方法
-
输出流(OutputStream和Writer)都有flush()方法,用于将流中的数据冲刷到硬盘中
- 在使用输出流对象时,一定要调用flush()或close()方法后,才能真正将数据写入到硬盘中
-
所有流中,以Stream结尾,都是字节流,数据以字节传输;以Reader或Writer结尾,都是字符流,数据以字符传输
-
读取硬盘中的数据时,读取的文件必须存在;写入数据到硬盘中时,写入的文件可以不存在,但父目录必须存在。
FileInputStream文件字节输入流(掌握)
以字节的形式读取文件
构造方法
常用构造方法 | 作用 |
---|---|
FileInputStream(String filePath) | 根据文件完整路径创建流对象 |
FileInputStream(File file) | 根据文件对象创建流对象 |
常用方法
常用方法 | 作用 |
---|---|
read() | 读取一个字节,返回读取到的字节 |
read(byte[] bytes) | 读取指定数组大小的字节,返回读取到的字节数量 |
read(byte[] bytes,int off,int len) | 读取指定数组大小的字节,从off索引开始读取len个字节,返回读取到的字节数量 |
available() | 文件可读取的最大字节数量 |
close() | 关闭流对象 |
FileOutputStream文件字节输出流(掌握)
以字节的形式写入文件
构造方法
常用构造方法 | 说明 |
---|---|
FileOutputStream(String filePath) | 根据文件名创建流对象,覆盖写入 |
FileOutputStream(String filePath,boolean appen) | 根据文件名创建流对象,追加写入 |
FileOutputStream(File file) | 根据文件对象创建流对象,覆盖写入 |
FileOutputStream(File file,boolean appen) | 根据文件对象创建流对象,追加写入 |
常用方法
常用方法 | 作用 |
---|---|
write(int b) | 写入一个字节 |
write(byte[] bytes) | 写入一个字节数组 |
write(byte[] bytes,int off,int len) | 写入一个字节数组,从off开始的len个字节 |
flush() | 将流中的数据冲刷到硬盘中 |
close() | 关闭流对象 |
使用FileInputStream和FileOutputStream读写时的注意事项
-
在通过FileInputStream对象使用read(byte[] bytes)方法时,每次读取指定数组的字节,将读取到的字节保存在字节数组中。
该方法的返回值是读取到的字节数量,如果最后一次读取的字节数量不足字节数组的大小时,只会将读取到的内容覆盖数组中最前的几个元素。
所以最后一次读取时,读取的内容多于实际内容。
-
在通过FileOutputStream对象使用write(byte[] bytes),会将字节数组中的所有内容写入到输出流中,在最后一次写入时,会写入多余的内容。
所以在写入时,使用write(byte[] bytes,int off,int len)方法,表示将字节数组中的内容,从off开始写入len个
//如有test.txt中aaabbbccc //读取 FileInputStream fis = new FileInputStream("test.txt"); byte[] bytes=new bytes[4]; int count=0; //第一次读取到aaab,读取了4个字节,当前数组为aaab count=fis.read(bytes); //第一次读取到bbcc,读取了4个字节,当前数组为bbcc count=fis.read(bytes); //第一次读取到c,读取了1个字节,当前数组为cbcc count=fis.read(bytes); //写入 FileOutputStream fos = new FileOutputStream("copy.txt"); //如果使用fos.write(byte[]);方式,最后一次会写入多于的bcc fos.write(bytes); //如果使用fos.write(byte[],int,int);方式,最后一次只会写入c fos.write(bytes,0,count);
单文件复制
public class Test2 {
public static void main(String[] args) throws IOException {
//使用FileInputStream读取字节的同时使用FileOutputStream写入字节,实现单文件复制
//源文件
File source = new File("d:\\xxx.txt");
//读取
FileInputStream fis = new FileInputStream(source);
//写入到目标文件中
FileOutputStream fos = new FileOutputStream("f:\\copy.txt");
//定义字节数组,大小为8MB
byte[] bytes = new byte[1024*1024*8];
//读取指定数组大小的字节,返回读取到的字节数量
int count = fis.read(bytes);
while (count>0) {
//写入指定数组的字节
//fos.write(int b);写入一个字节
//fos.write(byte[] bytes);写入一个数组的字节
fos.write(bytes,0,count);//写入一个数组的字节,从0开始写入读取到的数量的字节
count = fis.read(bytes);
}
fis.close();
fos.close();
}
}
复制文件夹
public class Test3 {
public static void main(String[] args) throws IOException {
//复制文件夹
File file = new File("C:\\Users\\Administrator\\Desktop\\测试");
File target = new File("f:\\复制文件夹");
copyDir(file,target);
}
/*
* 复制单个文件
* */
public static void copyFile(File source,File target) throws IOException {
//读取源文件
FileInputStream fis = new FileInputStream(source);
//写入新文件
FileOutputStream fos = new FileOutputStream(target);
//读写字节数组
byte[] bytes=new byte[1024*1024*8];
//尽可能按数组大小读取
int count=fis.read(bytes);
//如果能读到,写入
while(count>0){
fos.write(bytes,0,count);
count= fis.read(bytes);
}
fis.close();
fos.close();
}
/*
* 复制文件夹
* */
public static void copyDir(File source,File target) throws IOException {
//如果是文件夹
if (source.isDirectory()) {
//创建目标文件夹,可以与源文件夹同名,也可以使用参数target名
target.mkdir();
//展开源文件,得到第一层子文件
for (File child : source.listFiles()) {
//需要将所有子文件保存在上一步创建的文件夹中
//使用构造方法File(File parent,String child),定义新目标文件的路径
//如source为桌面的"桌面/测试/a",复制到f盘的“复制文件夹”中,递归调用的target就为"f:/复制文件夹/a"
File newTarget = new File(target, child.getName());
//递归调用,这时的源文件为遍历出的子文件,目标文件为上一步构造的对象
copyDir(child,newTarget);
}
}else{
//如果是文件
copyFile(source,target);
}
}
}
/*
source 桌面:\测试
target F:\副本
1.调用copyDir(source,target),判断source为文件夹,创建target,F盘下“副本”文件夹创建成功
2.遍历source,如其中有test.txt文件,用child表示
递归调用copyDir(source,target),这时的source就是child,target得是"F:\副本\test.txt",
可以使用File(File parent,String child)构造方法表示这个target文件,即new File(target,child.getName())
*/
FileReader文件字符输入流
按字符读取文件
构造方法
常用构造方法 | 说明 |
---|---|
FileReader(String fileName) | 根据文件名创建文件字符输入流对象 |
FileReader(File file) | 根据文件对象创建文件字符输入流对象 |
常用方法
常用方法 | 作用 |
---|---|
ready() | 判断是否还有下一个字符 |
read() | 读取下一个字符,返回读取到的字符 |
read(char[] chars) | 按字符数组读取,返回读取到的字符数量,读取到的字符保存在字符数组中 |
close() | 关闭流对象 |
BufferedReader缓冲字符输入流(掌握)
自带缓冲区(字符数组)的字符输入流。默认字符数组大小为8192,每次最多读取8192个字符。
在读取纯文本文件(txt或md)时,首选该类。
构造方法
常用构造方法 | 作用 |
---|---|
BufferedReader(Reader in) | 创建一个带有缓冲区(大小为8192的char数组)的字符输入流对象,参数为Reader类型对象,Reader是抽象类,所以实际参数为Reader的子类,如FileReader,在FileReader对象中定义要读取的文件 |
BufferedReader(Reader in,int size) | 创建一个指定缓冲区(字符数组)大小的字符输入流对象 |
常用方法
常用方法 | 作用 |
---|---|
ready() | 判断是否还有字符 |
readLine() | 读取整行字符 |
close() | 关闭流对象 |
FileWriter文件字符输出流
按字符写入文件。必须在调用flush()或close()方法后才会写入。
构造方法
常用构造方法 | 说明 |
---|---|
FileWriter(String filePath) | 根据文件路径创建流对象,覆盖写入 |
FileWriter(String filePath,boolean append) | 根据文件路径创建流对象,append为true时追加写入 |
FileWriter(File file) | 根据文件对象创建流对象,覆盖写入 |
FileWriter(File file,boolean append) | 根据文件对象创建流对象,append为true时追加写入 |
常用方法
常用方法 | 作用 |
---|---|
writer(String str) | 写入字符串 |
flush() | 冲刷数据到硬盘 |
close() | 关闭流对象 |
BufferedWriter缓冲字符输出流(掌握)
按字符写入,带有缓冲区的输出流。必须在调用flush()或close()方法后才会写入。
构造方法
常用构造方法 | 说明 |
---|---|
BufferedWriter(Writer writer) | 根据Writer对象创建字符缓冲输出流 |
常用方法
常用方法 | 作用 |
---|---|
writer(String str) | 写入字符串 |
newLine() | 写入空白行 |
flush() | 冲刷数据到硬盘 |
close() | 关闭流对象 |
序列化ObjectOutputStream对象字节输出流(掌握)
序列化:将对象转换为文件的过程
被序列化的对象,其类必须要实现Serializable接口
这个接口是一个特殊的接口,其中没有定义任何方法,只是给类加上标记,表示该类可以被序列化
构造方法
构造方法 | 说明 |
---|---|
ObjectOutputStream(OutputStream os) | 根据字节输出流对象创建 |
常用方法
常用方法 | 作用 |
---|---|
writeObject(Object obj) | 将一个对象写入到本地文件 |
flush() | 冲刷数据到硬盘 |
close() | 关闭流对象 |
反序列化ObjectInputStream对象字节输入流(掌握)
反序列化:将文件转换为对象的过程
构造方法
构造方法 | 说明 |
---|---|
ObjectInputStream(InputStream is) | 根据字节输入流对象创建 |
常用方法
常用方法 | 作用 |
---|---|
readObject() | 读取序列化后的文件,返回Object类型 |
close() | 关闭流对象 |
序列化和反序列化案例
Person类,实现Serializable接口
/*
* 如果要序列化某个类的对象,该类必须要实现Serializable接口
* 该接口中没有定义任何方法,只是一个标记接口,表明该类可序列化
* */
public class Person implements Serializable {
private String name;
private int age;
private String sex;
//省略get/set/构造方法等
}
测试类
package com.hqyj.ObjectStreamTest;
import java.io.*;
import java.util.ArrayList;
public class Test {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person p1 = new Person("王海", 22, "男");
Person p2 = new Person("刘玉梅", 29, "女");
Person p3 = new Person("白伟强", 21, "男");
//将程序运行中的数据(对象)保存到本地,这个过程就称为序列化
ArrayList<Person> list = new ArrayList<>();
list.add(p1);
list.add(p3);
list.add(p2);
//创建 "对象输出字节流" 对象,参数为字节输出流对象,这里使用FileOutputStream子类
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("person.p"));
oos.writeObject(list);
oos.close();
//创建一个对象,将其保存为一个文件,再读取该文件中保存的对象
//ObjectInputStream 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("person.p"));
//读取序列化文件中的对象
Object obj = ois.readObject();
//需要转换为相应的类型后使用
ArrayList<Person> pList = (ArrayList<Person> ) obj;
for (Person person : pList) {
System.out.println(person);
}
oos.close();
ois.close();
}
}
InputStreamReader
属于转换流(将字节流转换为字符流)
OutputStreamWriter
属于转换流(将字节流转换为字符流)
案例参考网络编程套接字通信
进程和线程
进程Process
进程就是操作系统中正在执行的程序。
一个程序就是一个执行的进程实体对象。
每个运行中的进程,都有属于它独立的内存空间,各个进程之间互不影响。
线程Thread
线程是一个进程中的执行单元,一个进程中可以有多个线程。
多个线程之间可以访问同一个进程中的资源。
每个线程都有一块独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。
多线程
如果一个进程中,同时执行着多个线程,就称为多线程。
多线程可以提高程序执行效率。如多个窗口卖票,就可以加快卖票的效率。
其实每个执行的Java程序,都是多线程执行。main方法所在的线程称为主线程,还有gc线程(守护线程)在同时运行。
如工厂有很多车间,每个车间有很多流水线。
工厂就是内存,车间就是进程,每个流水线就是进程中的一个线程。
并行和并发
并行
各个进程同时执行,称为并行。
并发
多个线程同时执行,称为并发。
同步和异步
同步
所有任务排队执行,称为同步执行。
异步
在执行任务A的同时,执行任务B,称为异步执行。
Java中的线程Thread类
Java中,线程以对象的形式存在。
Thread类表示线程类。
获取线程对象
-
获取当前正在运行的线程对象
Thread thread = Thread.currentThread();
-
创建一个线程对象
常用构造方法 说明 Thread() 创建一个线程对象 Thread(String name) 创建一个指定线程名的线程对象 Thread(Runnable target) 根据Runnable对象创建线程对象 Thread(Runnable target,String name) 根据Runnable对象和指定线程名的线程对象
线程类常用方法
常用方法 | 作用 |
---|---|
getId() | 获取线程ID |
getName() | 获取线程名 |
getPriority() | 获取线程优先级,默认为5 |
getState() | 获取线程状态 |
setName(String name) | 设置线程名 |
setPriority(int i) | 设置线程优先级,范围在1~10,数字越大优先级越高,越先执行完 |
isDaemon() | 判断是否为守护线程 |
setDaemon(boolean on) | 参数为true表示设置线程为守护线程 |
start() | 让线程进入就绪状态,等待执行 |
run() | 在线程获得执行权时要执行的方法(线程要做的事情) |
Thread.sleep(long m) | 让线程休眠m毫秒后继续执行 |
Thread.currentThread() | 获取当前执行的线程对象 |
实现多线程
方式一:继承Thread类
- 1.创建一个类,继承Thread类
- 2.重写Thread类中的run()方法,该方法表示当该线程执行时要做的事情
- 3.创建自定义的线程对象后,调用start()方法,让线程就绪
自定义Thread类的子类(子线程)
/*
- 1.创建一个类,继承Thread类
- 2.重写Thread类中的run()方法,该方法表示当该线程执行时要做的事情
- 3.创建自定义的线程对象后,调用start()方法,让线程就绪
*/
public class MyThread extends Thread{
@Override
public void run() {
//该线程循环输出1-100
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
main类(主线程)
public class Main{
//如果main方法也要作为一个线程使用时,需要先将其他线程先就绪
public static void main(String[] args) {
//创建自定义线程对象,调用start()让线程就绪
new MyThread().start();
//主线程同样打印1-100
for (int i = 1; i <= 100; i++) {
System.out.println(Thread.currentThread().getName()+i);
}
}
}
方式二:实现Runnable接口(建议使用)
由于Java中是单继承,如果某个类已经使用了extends继承了另一个类,就无法再继承Thread类。
这时就可以让自定义线程类实现Runnable接口的方式实现多线程。
- 1.自定义一个类,实现Runnable接口
- 2.重写run()方法,将多线程要执行的内容写在该方法中
- 3.创建Runnable接口的实现类对象
- 4.使用Thread(Runnable target)构造方法,将Runnable接口的实现类对象包装为Thread对象
自定义Runnable接口的实现类
/*
* 如果一个类已经是其他类的子类,无法再继承Thread
* 这时实现Runnable接口,重写Run方法
* 再将该类对象当做Thread(Runnable target)构造方法的参数,包装为线程类
* */
public class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() +":"+ i);
}
}
}
main类
public class Main{
public static void main(String[] args) {
//创建两个子线程对象
//使用new Thread(Runnable target)构造方法,将自定义线程子类包装为Thread
Thread t1 = new Thread(new MyThread2());
Thread t2 = new Thread(new MyThread2());
t1.start();
t2.start();
}
}
方式三:使用匿名内部类
如果不想创建Thread的子类或Runnable的实现类,就可以将匿名内部类当做Runnable接口的实现类来使用。甚至可以配合lambda表达式更进一步简化。
public class Test {
public static void main(String[] args) {
// new Thread(Runnable target)参数为接口类型,可以使用匿名内部类实现
//使用匿名内部类实现多线程
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
},"线程A");
t1.start();
//使用Lambda表达式简化匿名内部类
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
},"线程B").start();
}
}
线程生命周期
线程的初始化到终止的整个过程,称为线程的生命周期
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56lb0AJs-1680263776750)(JavaAdv08.assets/u=1010133971,2879667515&fm=253&fmt=auto&app=138&f=JPEG)]
新生状态
当线程对象创建成功后,进入新生状态
就绪状态
当某个线程对象调用了start()方法后,进入就绪状态。
在这个状态下,线程对象不会做任何事情,只是在等待CPU分配时间片。
运行状态
当某个线程对象得到CPU时间片(CPU执行该线程时分配的时间),进入运行状态,开始执行run()方法。
不会等待run()方法执行完毕,只会在指定的时间内尽可能地执行run()方法。
如果多线程下,某个线程对象在调用完run()方法后,就会再进入就绪状态。
阻塞状态
如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞状态。
sleep()方法在指定时间后,会让线程重新就绪。
wait()方法只有在被调用notify()或notifyAll()方法将其唤醒后才能重新就绪。
终止状态
当某个线程的run()方法中的所有内容执行完,就会进入终止状态。
守护线程
如果将一个线程设置为setDeamon(true),表示该线程为守护线程。
守护线程会随着其他非守护线程终止而终止。
package com.hqyj.ThreadTest;
/*
* 测试守护线程
* */
public class DaemonTest {
public static void main(String[] args) {
//将含有死循环的线程设置为守护线程
Thread t1 = new Thread(() -> {
while (true) {
System.out.println("守护线程运行中。。。");
}
});
//设置为守护线程
t1.setDaemon(true);
t1.start();
/*
* 普通线程,有限循环
* */
new Thread(()->{
for (int i = 0; i < 100; i++) {
System.out.println("普通线程运行中。。。"+i);
}
}).start();
//随着有限循环结束,死循环线程也会结束
//非守护线程结束,守护线程也会结束
}
}
多线程访问同一个资源
可能出现的问题
如现有10张票,3个窗口同时卖票。
package com.hqyj.ticketTest;
/*
* 模拟多窗口卖票
* */
public class TicketShop implements Runnable {
//共10张票
private int ticket = 10;
/*
* 卖票的方法
* */
public void sell() throws InterruptedException {
while (true) {
if (ticket > 0) {
ticket = ticket - 1;
System.out.println(Thread.currentThread().getName() + "售出一张,剩余" + ticket + "张");
Thread.sleep(1000);
} else {
System.out.println(Thread.currentThread().getName() +"已售罄");
break;
}
}
}
/*
* 该线程的任务是调用sell()方法
* */
@Override
public void run() {
try {
sell();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
public static void main(String[] args) {
//创建一个票房对象,共10张票
TicketShop ts = new TicketShop();
//创建3个线程对象模拟3个窗口卖票
Thread t1 = new Thread(ts,"窗口A");
Thread t2 = new Thread(ts,"窗口B");
Thread t3 = new Thread(ts,"窗口C");
t1.start();
t2.start();
t3.start();
}
}
出现问题的原因
由于线程调用start()方法后,就会进入就绪状态。当该线程获得了CPU时间片,就开始执行run()方法,
并不会等待run()执行完毕,所以很有可能在线程A执行run()方法的途中,线程B也开始执行run()方法,造成数据共享时出错。
如何解决
如果要防止在线程A执行途中线程B插队的情况,可以让所有线程同步(排队)执行。
这样一来,某个线程执行run()方法的时候,其他线程就会等待run()执行完毕。
synchronized关键字
这个关键字可以修饰方法或代码块
修饰方法
写在方法的返回值前,这时该方法就称为同步方法
public synchronized void sell(){
//该方法执行途中,其他线程无法插队
}
修饰代码块
synchronized(要同步的对象){
//该方法执行途中,其他线程无法插队
}
原理
每个对象默认都有一个"锁"。当某个线程运行到被synchronized修饰的方法时,
该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其执行结束后,才会释放这把锁。
使用synchronized修饰后的锁称为"悲观锁"。
方法被synchronized修饰后,称为同步方法,会让原本多线程执行变成了单线程执行(异步变同步)。
多线程相关面试题
-
实现多线程的方式
- 继承Thread类,创建对象
- 实现Runnable接口后,包装成Thread对象
- 匿名内部类
-
为什么说StringBuilder或ArrayList、HashMap是非线程安全的
public class Test { public static void main(String[] args) throws InterruptedException { //使用StringBuilder时,很大概率最终数值不准确,非线程安全 //StringBuilder sb = new StringBuilder(); //使用StringBuffer时,没有问题,线程安全 StringBuffer sb = new StringBuffer(); //创建10个线程对象,创建后进入就绪状态 for (int i = 0; i < 10; i++) { new Thread(() -> { try { Thread.sleep(1); } catch (InterruptedException e) { throw new RuntimeException(e); } //每个线程对象添加字符串 for (int j = 0; j < 10; j++) { sb.append("hello"); } }).start(); } Thread.sleep(5000); //如果没有任何问题,每个线程拼接50个字母,10个线程拼接500个,sb的长度为500 System.out.println(sb.length()); } }
-
什么叫死锁?怎么产生?如何解决?
如两个人吃西餐,必须有刀和叉,当前只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待对方释放另一个工具,如果都不释放,就进入僵持的局面,这个局面就称为死锁,既不结束,也不继续。
模拟死锁出现的情况
定义两个线程类,线程A先获取资源A后,再获取资源B;线程B先获取资源B后,再获取资源A。
PersonA线程
public class PersonA implements Runnable {
//定义公用的成员变量,刀和叉
private Object knife;
private Object fork;
public PersonA(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run()方法时,先获取knife资源,在获取fork资源
* */
@Override
public void run() {
//先获取knife资源
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "已获得刀,2s后得到叉");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//再获取fork资源
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
}
}
}
}
PersonB线程
public class PersonB implements Runnable {
//定义公用的成员变量,刀和叉
private Object knife;
private Object fork;
public PersonA(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run()方法时,先获取fork资源,在获取knife资源
* */
@Override
public void run() {
//先获取fork资源
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "已获得叉,2s后得到刀");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//再获取knife资源
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
}
}
}
}
测试
public class DeathLockTest {
public static void main(String[] args) {
Object knife = new Object();
Object fork = new Object();
new Thread(new PersonA(knife,fork,paper),"小王").start();
new Thread(new PersonB(knife,fork,paper),"老李").start();
//运行后,程序就会卡住,既不继续,也不停止,也不报错。
}
}
死锁的解决方式
方式一:让两个线程获取资源的顺序保持一致
如两个线程都先获取knife,再获取fork
方式二:在两个线程获取的资源A和B之前,再获取第三个资源C,对这个资源C使用synchronized进行同步
这样在某个线程获取资源C后,继续执行后续的内容,知道执行完毕,其他线程才有机会开始执行。
如在获取knife和fork之前,先获取paper对象
;
//如果没有任何问题,每个线程拼接50个字母,10个线程拼接500个,sb的长度为500
System.out.println(sb.length());
}
}
- 什么叫死锁?怎么产生?如何解决?
如两个人吃西餐,必须有刀和叉,当前只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待对方释放另一个工具,如果都不释放,就进入僵持的局面,这个局面就称为死锁,既不结束,也不继续。
# 模拟死锁出现的情况
定义两个线程类,线程A先获取资源A后,再获取资源B;线程B先获取资源B后,再获取资源A。
**PersonA线程**
```java
public class PersonA implements Runnable {
//定义公用的成员变量,刀和叉
private Object knife;
private Object fork;
public PersonA(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run()方法时,先获取knife资源,在获取fork资源
* */
@Override
public void run() {
//先获取knife资源
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "已获得刀,2s后得到叉");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//再获取fork资源
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
}
}
}
}
PersonB线程
public class PersonB implements Runnable {
//定义公用的成员变量,刀和叉
private Object knife;
private Object fork;
public PersonA(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run()方法时,先获取fork资源,在获取knife资源
* */
@Override
public void run() {
//先获取fork资源
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "已获得叉,2s后得到刀");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//再获取knife资源
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "已获得刀和叉,可以吃饭了");
}
}
}
}
测试
public class DeathLockTest {
public static void main(String[] args) {
Object knife = new Object();
Object fork = new Object();
new Thread(new PersonA(knife,fork,paper),"小王").start();
new Thread(new PersonB(knife,fork,paper),"老李").start();
//运行后,程序就会卡住,既不继续,也不停止,也不报错。
}
}
死锁的解决方式
方式一:让两个线程获取资源的顺序保持一致
如两个线程都先获取knife,再获取fork
方式二:在两个线程获取的资源A和B之前,再获取第三个资源C,对这个资源C使用synchronized进行同步
这样在某个线程获取资源C后,继续执行后续的内容,知道执行完毕,其他线程才有机会开始执行。
如在获取knife和fork之前,先获取paper对象