华清远见-重庆中心-JAVA高级阶段技术总结
String字符串
String是一个类,属于数据类型中的引用类型。
String的实例化对象都需要使用 “” 引起来。
String字符串在定义后是一个常量,值不可改变,其实际是一个字符数组。
如何创建字符串对象
1.使用“”赋值创建
string str="amy";
2.使用构造方法创建
常用构造方法 | 说明 |
---|---|
String() | 创建一个空白字符串对象 |
String(String str) | 创建一个指定字符串的字符串对象 |
String(char[] list) | 创建一个指定字符数组的字符串对象 |
String(byte[] list,String charsetName) | 按指定的编码格式创建一个指定字节数组的字符串对象 |
不同方式创建字符串的过程
1.使用“”赋值的形式创建
//这句话执行时,先判断字符串常量池(缓冲区)中是否存在"st",不存在则创建,将其地址保存到str1变量中
String str1 = "st";
//这句话执行时,先判断字符串常量池(缓冲区)中是否存在"st"。已存在,不用创建,将其地址保存到str2变量中
String str2 = "st";
//这句话执行时,+两端如果都是""定义的字符串,拼接后再判断字符串常量池(缓冲区)中是否存在
//拼接后的"st"依然存在,将其地址保存到str3变量中
String str3 = "s" + "t";
//以上三句话,只会在内存中的字符串常量池(缓冲区)创建一个字符串对象"st",分别引用给3个变量
System.out.println(str1 == str2); //返回true
System.out.println(str1 == str3); //返回true
注意
(1)使用“”定义字符串,流程都是先判断字符串缓冲区中是否存在,不存在则创建,存在则直接引用其地址。
(2)使用+拼接两个由“”定义的字符串,拼接过程发生在编译前,最终保存的是拼接后的字符串对象,此时也要先判断拼接后的字符串是否在字符串缓冲区存在,不存在则创建,存在则直接引用其地址。
2.使用构造方法String(String str)创建
//这句话执行时的流程
//1.在字符串常量池中寻找"ab",不存在,创建
//2.在堆中new String(),将字符串常量池中的"ab"保存到new出来的区域
//3.将堆中new出来的地址保存到栈中变量str1中
String str1 = new String("ab");
//这句话执行时的流程
//1.在字符串常量池中寻找"ab",存在,直接引用
//2.在堆中new String(),将字符串常量池中的"ab"保存到new出来的区域
//3.将堆中new出来的地址保存到栈中变量str2中
String str2 = new String("ab");
//由于str1和str2是堆中的两个区域,所以结果为false
System.out.println(str1 == str2);//返回false
3.使用+拼接“”和new出来的字符串对象创建
//在字符串常量池中创建"ab"
String str1 = "ab";
//1.创建StringBuilder对象
//2.在字符串常量池中创建"a"
//3.在字符串常量池中创建"b"
//4.创建String对象
//5.调用StringBuilder的append方法,将"a"和new String("b")拼接
String str2 = "a" + new String("b");//一共创建了"a","b",String,StringBuilder这四个对象
//str1和str2是两个不同的地址
System.out.println(str1 == str2);//返回false
注意
在使用字符串时,如果要比较其值是否相同,不要使用==判断,因为==判断的是内存地址。
比较字符串是否相同时,要使用String类重写的equals方法进行判断。
该方法判断的原理大致为:将两个字符串用字符数组保存,逐个判断字符数组中的每个字符,全部一致 时返回true, 所以比较的是字面值。在使用equals方法时,通常将已知的非空字符串作为调用者。
username.equals("admin");//这样写,username变量可能为空,会抛出空指针异常
"admin".equals(username);//这样写能避免空指针异常
String类使用时注意
如果频繁地将一个String类型变量的值进行更改时,会创建很多字符串对象。效率低,浪费内存空间。所以在频繁更改字符串时,不要使用String类变量。
如果要频繁更改字符串,使用StringBuilder类或StringBuffer类。
String str = "";
public static void main(String[] args) {
long start = System.currentTimeMillis();
String str = ""
for (int i = 0; i < 50000; i++) {
str += i;
}//5万次的循环,就会创建5万个字符串对象,但最终只会有一个字符串对象被str引用
long end = System.currentTimeMillis();
System.out.println((end - start)/1000);//输出5,运行时长大概在5秒左右。
}
可变字符串
String字符串对象是一个常量,在定义后,值不可改变。
如果使用String类的对象,对其频繁更新时,就会不停地创建新的对象,不停引用给同一个变量。
如要执行10000次循环重新赋值的过程,就要创建10000个字符串对象,执行效率很低,这时就需要使用可变字符串对象。
System.out.println("程序开始执行");
//System.currentTimeMillis();用于获取当前时间对应的毫秒数
//从1970 1 1 0:0:0这一刻开始,到这句话执行时间隔的毫秒数
long startTime = System.currentTimeMillis();
/*
//循环"更新"字符串,实际是在不停创建新的字符串
String str = "";
for (int i = 0; i < 50000; i++) {
str += i;
}*/
//使用可变字符串StringBuilder对象,真正更新字符串
//因为全程只有一个对象StringBuilder,每次循环只是在不停操作该对象,不会创建新对象,
所以效率很高
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 500000; i++) {
sb.append(i);
}
long endTime = System.currentTimeMillis();
System.out.println("程序执行结束,用时" + (endTime - startTime) + "毫秒");
StringBuilder类
用于表示可变字符串的一个类,是非线程安全的,建议在单线程环境下使用。
构造方法
常用构造方法 | 作用 |
---|---|
StringBuilder() | 创建一个大小为16的字符串数组,表示一个空白字符。类似于String |
str=“”; | |
StringBuilder(String str) | 创建一个str长度+16的字符数组后,将str添加到其中。类似于String |
str=“初始值”; |
普通方法
常用方法 | 作用 |
---|---|
append(Object obj) | 将任意类型的参数添加到原可变字符串末尾 |
delete(int start, int end) | 删除[start,end)区间的字符 |
deleteCharAt(int index) | 删除index索引上的字符 |
insert(int index, Object obj) | 在索引index上插入obj |
replace(int start, int end, String str) | 将[start, end)区间内的字符替换为str |
reverse() | 反转字符串 |
注意
(1)以上表格中的方法都是在直接操作同一个字符串对象,每次调用方法后,原字符串都会发生变化。
(2)StringBuffer和StringBuilder并没有重写equals方法,所以可变字符串的值是否相同时,调用的是equals中原始的==判断。如果要判断两个可变字符串的值是否相同时,需要将其转换为String后调用equals判断。
StringBuffer类
用于表示可变字符串的一个类,是线程安全的,建议在多线程环境下使用。
StringBuilder和StringBuffer区别
StringBuilder和StringBuffer中的方法都一致,只不过StringBuffer中的方法使用了synchoronized关键字修饰,表示是一个同步方法,在多线程环境下不会出现问题。此处不再列举。
可变字符串与String之间的转换
String转换为可变字符串
String str="hello";
//通过构造方法将String"包装"为可变字符串对象
StringBuilder sb = new StringBuilder(str);
可变字符串转换为String(任意类型对象转换为String)
-
方法一:String.valueOf(Object obj)方法
StringBuilder sb = new StringBuilder("你好"); //调用静态方法 String str = String.valueOf(sb);
-
方法二:对象.toString();
StringBuilder sb = new StringBuilder("你好"); //调用toString() String str = sb.toString();
-
方法三:+“”
StringBuilder sb = new StringBuilder("你好"); //拼接一个空字符串 String str = sb + "";
String字符串和可变字符串总结
//题目一
String str1 = "ab";//常量池中创建"ab"
String str2 = new String("ab");//堆中new String()保存常量池中已有的"ab"
String str3 = "a" + "b";//用常量池已有的"ab"
String str4 = "a" + new String("b");//常量池中创建"a"和"b",堆中new String()和new
StringBuilder()
String str5 = "ab";//用常量池已有的"ab"
System.out.println(str1 == str2);//false
System.out.println(str1 == str3);//true
System.out.println(str1 == str4);//false
System.out.println(str1 == str5);//true
//题目二
//这两句话执行后,会创建几个对象
String s1 = "abc";
String s2 = "a" + "b" + "c";
//在字符串常量池中创建一个对象"abc"
//题目三
//这两句话执行后,会创建几个对象
String s3 = new String("你好");//常量池:"你好",堆中:new String()
String s4 = new String("你好");//堆中:new String()
//3个对象:堆中两个new String(),常量池中"你好"
//题目四
//这两句话执行后,会创建几个对象
String s5 = "hello";//常量池:"hello"
String s6 = "hel" + new String("lo");//常量池:"hel"和"lo" 堆:new String()和new
StringBuilder
//5个对象:常量池:"hello"、"hel"和"lo",堆:new String()和new StringBuilder
//题目五
String s7 = new String("wor");//常量池:"wor",堆:new String()
String s8 = s7 + "ld";//常量池:"ld" 堆:new StringBuilder()
//4个对象:常量池:”wor"和"ld",堆:new String()和new StringBuilder
比较String、StringBuilder和StringBuffer的区别
相同点
- 这三个类都可以表示字符串。都提供了一些操作字符串的方法。
- 这三个类中有相同的方法,如charAt()、indexOf()等 这三个类都是被final修饰的类,不能被继承。
不同点
- String定义的字符串是一个常量。可变字符串定义的字符串是一个变量。
- String类中的方法,调用后,不会改变原本字符串的值;可变字符串类中的方法,调用后,会改变 原本字符串的值。
- StringBuilder是非线程安全的可变字符串类,StringBuffer是线程安全的可变字符串类,其中的方 法被synchronized修饰。
System类
这个类中包含了一些系统相关的信息和一些方法。其中的属性和方法都是静态的。
该类不能创建对象,不是因为它是一个抽象类,而是因为它的构造方法是私有的。
常用属性和方法 | 作用 |
---|---|
System.out | 获取打印输出流PrintStream对象,用于控制台打印信息 |
System.in | 获取输入流InputStream对象,用于获取输入的信息 |
System.err | 获取打印输出流PrintStream对象,用于控制台打印异常信息 |
System.exit(int status) | 终止虚拟机运行,参数0表示正常终止 |
System.currentTimeMillis() | 获取从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();
//定义了一个公共的静态方法,用于获取创建的唯一的当前类的对象
public static Runtime getRuntime() {
return currentRuntime;
}
//构造方法是私有的,不能在当前类之外创建对象
private Runtime() {}
}
使用
//通过Runtime类的静态方法getRuntime()获取唯一的Runtime类的实例
Runtime runtime = Runtime.getRuntime();
System.out.println("当前虚拟机空闲内存" + runtime.freeMemory() / 1024 / 1024
+ "MB");
System.out.println("当前虚拟机实际最大内存" + runtime.totalMemory() / 1024 /
1024 + "MB");
System.out.println("当前虚拟机支持的最大内存" + runtime.maxMemory() / 1024 /
1024 + "MB");
//exec(String 指令名)运行某个指令,返回运行的进程对象
//在指定秒后关机
// Process process = runtime.exec("shutdown -s -t 300");
//取消关机任务
// Process process = runtime.exec("shutdown -a");
//mspaint画图 calc计算器 notepad记事本
Process process = runtime.exec("mspaint");
Thread.sleep(2000);
//通过进程对象调用销毁功能,从而关闭
process.destroy();
方法调用时传值问题
/*
* 当方法的参数为原始类型,方法中对该参数做修改,不会影响实际参数
* */
public static void fun1(int i) {
i = 123;
System.out.println(i);
}
/*
* 当方法的参数为字符串时,方法中对字符串"重新赋值",实际是创建了一个新的字符串对象,不会
影响实际参数
* */
public static void fun2(String str) {
str = "new";
System.out.println(str);
}
/*
* 如果参数为引用类型,方法中直接操作该参数,操作的就是实际参数的内存地址,会影响实际参数
* */
public static void fun3(Person p) {
p.setName("吴彦祖");
System.out.println(p.getName());
}
/*
* 如果参数为引用类型,方法中创建了一个新对象对其赋值,操作的是创建的新对象,不会影响实际参
数
* */
public static void fun4(Person p) {
p = new Person();
p.setName("易烊千玺");
System.out.println(p.getName());
}
/*
* 如果参数为数组,也属于引用类型,方法中直接操作数组,操作的是实参数组,会影响实际参数
* */
public static void fun5(int[] list) {
list[0] = 123;
System.out.println(list[0]);
}
public static void fun(char[] list,Person p){
list[0]='m';//这里在直接操作实际参数,会影响实参
p = new Person();//这里创建了一个新的对象,操作的是方法中的对象,不会影响实参
p.setName("刘鑫");
}
public static void main(String[] args) {
//方法参数为原始类型,方法中对参数做修改,不会改变实际参数
int i = 0;
fun1(i);//123
System.out.println(i);//0
//方法参数为字符串,方法中对字符串重新赋值,不会改变实际参数
String str = "old";
fun2(str);//new
System.out.println(str);//old
//方法参数为引用类型,方法中对参数直接修改,会改变实际参数
Person p = new Person();
p.setName("王海");
fun3(p);
System.out.println(p.getName());
//方法参数为引用类型,方法中创建新对象后赋值给实际参数,操作的是方法中的对象,不会改变
实际参数
Person p1 = new Person();
p1.setName("赵敏");
fun4(p1);
System.out.println(p1.getName());
//方法参数为数组,属于引用类型,方法中对参数直接修改,会改变实际参数
int[] list = {0,1,2};
fun5(list);
System.out.println(list[0]);
//练习
char[] list2={'a','b','c'};
Person p2 = new Person();
fun(list2,p2);
System.out.println(list2[0]);//m
System.out.println(p2.getName());//null
}
RunTime类总结
参数只有是引用类型(类、数组、接口),并且方法中在直接操作该参数时,才会对实际参数造成影响。
fun3(Person p)参数为Person对象,方法中直接调用参数p的xxx方法,是在操作实际参数。
fun5(int[] list)参数为数组,方法中直接操作数组某个索引对应的元素,是在操作实际参数。
fun2(String str)和fun4(Person p)都在方法中创建了一个新的对象,是在操作方法中的参数,不影响实 际参数。
public static void fun(char[] list,Person p){
list[0]='m';//这里在直接操作实际参数,会影响实参
p = new Person();//这里创建了一个新的对象,操作的是方法中的对象,不会影响实参
p.setName("刘鑫");
}
public static void main(String[] args){
char[] list={'a','b','c'};
Person p = new Person();
fun(list,p);
System.out.println(list[0]);//方法内部直接操作数组,会影响实际参数,输出m
System.out.println(p.getName());//方法内部创建了新对象,不会影响实际参数,输出null
}
Date类
用于表示日期时间的类,位于java.util包下。
构造方法
常用构造方法 | 说明 |
---|---|
Date() | 创建当前瞬间对应的日期对象 |
Date(long l) | 创建指定瞬间对应的日期对象 |
Date(int year, int month, int dat) | 该构造方法已过时。创建指定年月日的日期对象(年是1900年起经过的年数,月用0-11表示1到12月) |
常用方法
常用方法 | 作用 |
---|---|
getTime() | 得到对应Date对象表示的毫秒数 |
setTime(long l) | 设置Date对象的毫秒数 |
after(Date when) | 判断调用日期对象是否在when之后 |
before(Date when) | 判断调用日期对象是否在when之前 |
SimpleDateFormat类
用于格式化日期的类。
构造方法
常用构造方法 | 作用 |
---|---|
SimpleDateFormat(String pattern) | 创建一个指定日期模板的格式化日期对象 |
常用方法
常用方法 | 返回值 | 作用 |
---|---|---|
format(Date date) | String | 将Date对象按日期模板转换为字符串 |
parse(String str) | Date | 将满足日期模板的字符串转换为Date对象 |
日期模板
特殊字符 | 作用 |
---|---|
yyyy | 年份 |
MM | 月份 |
dd | 日期 |
HH | 小时 |
mm | 分钟 |
ss | 秒 |
E | 星期 |
以上两个字母都可以写成一个,如月份5 | M:5,MM:05 |
yyyy/MM/dd HH:mm:ss E | 2022/11/24 16:24:09 星期四 |
public class SimpleDateFormatTest {
public static void main(String[] args) throws ParseException {
//定义格式化日期类所需的时间模板
/*
* yyyy 年
* MM 月份
* dd 日期
* HH 24小时制
* hh 12小时制
* mm 分钟
* ss 秒
* E 星期
*
* 两个字母都可以写成一个,如月份MM和M
* MM 5月实际为05
* M 5月实际为5
* */
String patten = "yyyy/MM/dd HH:mm:ss E";//年/月/日 时:分:秒 星期
//创建格式化日期类对象,参数为日期模板
SimpleDateFormat sdf = new SimpleDateFormat(patten);
//创建当前日期对象
Date now = new Date();
//调用格式化日期对象的format(Date date),将Date对象转换为指定日期格式的字符串
String format = sdf.format(now);
//输出
System.out.println(format);
//parse(String str)将指定日期模板的字符串转换为Date对象
Date date = sdf.parse("2000/5/3 2:1:3 星期一");
System.out.println(date);
}
}
Calendar类
表示日历的类,包含了很多日历相关的信息。
是一个抽象类,无法创建对象。可以通过静态方法getInstance()获取该类的一个实例。
//获取Calendar类的对象
Calendar cal = Calendar.getInstance();
日历字段
在Calendar类中,定义了很多被final和static修饰的常量,称为日历字段,实际是一个数字,用于获取指定信息。
值 | 作用 |
---|---|
Calendar.YEAR | 年份 |
Calendar.MONTH | 月份(下标0—11表示1—12月) |
Calendar.DATE | 日期 |
Calendar.DAY_OF_WEEK | 星期(下标1—7表示周日到周六) |
Calendar.HOUR | 12进制小时 |
Calendar.HOUR_OF_DAY | 24进制小时 |
Calendar.MINUTE | 分钟 |
Calendar.SECOND | 秒 |
Calendar.DAY_OF_MONTH | 本月第几天 |
Calendar.DAY_OF_YEAR | 本年第几天 |
Calendar.WEEK_OF_MONTH | 本月第几周 |
Calendar.WEEK_OF_YEAR | 本年第几周 |
常用方法
常用方法 | 作用 |
---|---|
get(int field) | 根据日历字段获取对应的值 |
getTime() | 获取对应的Date对象(Calendar对象转换为Date对象) |
getMaximum(int field) | 获取指定日历字段支持的最大值,如Calendar.DATE最大31 |
getActualMaximum(int field) | 获取指定日历字段在当前日期下的实际最大值,如11月,Calendar.DATE最大30 |
set(int field,int value) | 将指定的日历字段设置为指定值 |
set(int year,int month,int date) | 同时设置日历对象的年月日 |
setTime(Date date) | 将Date对象作为参数设置日历对象的信息 |
输入年月,输出该月所有的工作日和周末
//获取一个日历对象
Calendar cal = Calendar.getInstance();
//设置年月
Scanner sc = new Scanner(System.in);
System.out.println("请输入年份(如:2022)");
int year = sc.nextInt();
System.out.println("请输入月份(如:11)");
int month = sc.nextInt();
//设置年月
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month-1);
//循环遍历每天
for (int i = 1; i <= cal.getActualMaximum(Calendar.DATE); i++) {
//将日历设置为每一天
cal.set(Calendar.DATE, i);
//得到日期对应的星期
int week = cal.get(Calendar.DAY_OF_WEEK);
if (week == 1 || week == 7) {
System.out.print(i + "\t");
}
}
包装类
Java是纯面向对象语言,宗旨是将一切事物视为对象处理。但原始类型不属于对象,不满足面向对象的思想。但原始类型在使用时无需创建对象,保存在栈中,效率高。
为了让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以就有了包装类的概念。
包装类就是原始类型对应的类类型。 包装类通常用于字符串与原始类型之间的转换。
在web应用中,从浏览器页面中获取到后台的数据,全部都是String类型,所以一定要使用转换为原始 类型的方法。
包装类 | 原始类型 |
---|---|
Integer | int |
Float | float |
Double | double |
Character | char |
Boolean | boolean |
Long | long |
Short | short |
Byte | byte |
特点
-
八个原始类型中,除了int和char,其余类型的包装类,都是将首字母改为大写。int对应Integer,char对应Character。
-
包装类都是被final修饰的,不能被继承。
-
除了Character类,其余包装类都有两个构造方法:参数为原始类型或String的构造方法。
-
Character的构造方法只有一个,参数为char类型。这些构造方法用于将原始类型或字符串转换为包装类对象。
-
除了Character类,其余类都有静态方法parse原始类型(String str),用于将字符串转换为相应的原始类型。
(1)数值型的包装类的parseXXX()方法,如果参数不是对应的数字,转换时就会抛出NumberFormat异常。如"123abc",或"123.4",在使用Integer.parseInt()时都会抛出异常。
(2)Boolean类型中的parseBoolean()方法,参数如果是"true"这四个字母,不区分大小写,都能转换为真正boolean类型的true,只要不是"true"这个单词,转换结果都为false。
-
除了Boolean类,其余包装类都有MAX_VALUE和MIN_VALUE这两个静态属性,用于获取对应类型支持的最大最小值。
-
所有包装类都重写了toString()方法,用于将包装类对象转换为String对象。
字符串与原始类型之间的转换
字符串转换为原始类型
使用原始类型对应的包装类,调用parse原始类型(字符串)方法。
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
原始类型转换为字符串
-
使用+拼接一个空白字符串
int num = 123; String str = num + "";
-
将原始类型转换为包装类后,调用toString()方法
int num = 123; Integer integer = new Integer(num); String str = integer.toString();
-
String.valueOf(原始类型数据)
int num = 123; String str = String.valueOf(num);
装箱和拆箱
-
所有包装类都有一个静态方法valueOf(原始类型),将某个原始类型的数据转换为相应的包装类对 象。这个过程称为装箱boxing。
//手动装箱 int i = 123;//定义原始类型数据 Integer integer = Interger.valueOf(i);//调用包装类Integer的静态方法valueOf()将原 始类型转换为包装类对象
-
所有包装类都有一个原始类型Value()方法,用于将包装类对象转换为原始类型。这个过程称为拆 箱unboxing。
//手动拆箱 Integer integer = new Integer(123);//创建一个包装类对象 int i = integer.intValue();//调用包装类对象的intValue()方法将包装类对象转换为原始类型
-
自动装箱和拆箱。在jdk1.5之后,为了方便原始类型和包装类之间做转换,加入了自动装箱拆箱的 概念,可以直接将原始类型和包装类对象之间互相赋值。
//自动装箱 Integer anInt = 345; //自动拆箱 int i = anInt;
-
自动装箱缓冲区。
//i5和i6保存的是创建的不同对象的地址,==判断结果为false Integer i5 = new Integer(100); Integer i6 = new Integer(100); //自动装箱,如果值在byte的范围内[-128,127],这个值会共享,只会有一个对象"100" Integer i1 = 100; Integer i2 = 100; //自动装箱,如果值不在byte的范围内[-128,127],会创建对象 Integer i3 = 200;//相当于new了对象 Integer i4 = 200;//相当于new了对象 //new的不同对象,地址不同 System.out.println(i5 == i6);//false //byte范围内,共享对象,同一个地址 System.out.println(i1 == i2);//true //byte范围外,new不同对象,地址不同 System.out.println(i3 == i4);//false System.out.println(i5.equals(i6)); System.out.println(i3.equals(i4));
注意
- 如果通过构造方法创建的包装类对象,会有不同的内存地址,使用==判断结果为false。
- 自动装箱方式创建包装类对象,赋值范围在byte范围[-128,127]内,将这个值保存在缓冲区 中,如果多个对象使用同一个数值,共享这个数据,使用同一个地址,使用== 判断结果为 true;如果不再byte范围内,就会创建新的包装类对象,会有不同的内存地址,使用==判断结 果为false。
- 引用类型对象比较值是否相同时,不要使用==,而是要使用重写的equals方法。
异常
当程序没有按开发人员的意愿正常执行,中途出现错误导致程序中断,出现这种情况,就称为异常。
学习异常就是认识异常的种类,如何处理异常和避免异常出现。
异常的产生
异常在程序中以对象的形式存在。当代码执行过程中出现异常,虚拟机会自动创建一个异常对象,如果 没有对象该异常对象进行处理,就会导致程序中断,不再执行后续代码。
异常的分类
异常在程序中以对象的形式存在,就有相应的类。
所有的异常类,组成了一个"异常家族"。
Error错误
如果出现xxxxError,如StactOverFlowError,栈空间溢出,无法通过额外的代码解决,只能修改源码。
Exception异常
如果出现xxxxException,如NullPointerException,空指针异常,可以通过额外的代码去解决。
-
运行时异常RuntimeException
- 如果一个异常类属于RuntimeExcption异常类的子类,称为运行时异常,可以通过编译,可以不用处理,运行时可能抛出异常对象。
常见运行时异常 | 说明 | 出现的情景 |
---|---|---|
NullPointerException | 空指针异常 | 用空对象null调用属性或方法 |
ArrayIndexOutOfBoundsException | 数组下标越界异常 | 使用数组时,下标超出范围 |
NumberFormatException | 数字各式异常 | 使用包装类调用parse方法做转换时,无法将参数转换。如:String str = “123abc”;int i = Integer.paeseInt(str); |
InputMismatchException | 输入不匹配异常 | 使用Scanner接受控制台输入时,不满足接收的类型,如:int i = sc.nextInt(); 在控制台输入a |
ClassCastException | 对象转型异常 | Person p = (Person)Object obj |
ArithmeticException | 算术运算异常 | 0当分母 |
-
编译时异常
- 如果一个异常类属于Exception异常类的子类,称为编译时异常,无法通过编译,必须处理异常后才能编译运行。
常见编译时异常 | 说明 | 出现的情景 |
---|---|---|
SQLException | 数据库SQL相关异常 | 操作数据库时 |
IOException | 输入输出流异常 | 使用流对象时 |
FileNotFoundException | 文件未找到异常 | 方法的参数为文件时 |
处理异常
通常所说的处理异常,是指处理Exception类的子类异常。
处理异常的目的,就是保证程序正常执行。
try-catch-finally
- 这种方式处理异常,无论会不会抛出异常,都能让程序正常执行。
try{
//可能出现异常的代码
}catch(异常类 异常对象){
//如果出现异常对象,且与catch小括号中的异常类型匹配,就会执行这里的代码
}catch(异常类 异常对象){
//如果出现异常对象,且与catch小括号中的异常类型匹配,就会执行这里的代码
}finally{
//无论程序是否会抛出异常,都要执行的代码
}
- 执行流程:先执行try中的代码,当出现异常,与后续catch中的异常类型进行匹配,如果匹配到对应的类型或异常父类型,则执行大括号中的代码,最终一定执行finally中的内容。
注意
-
try、catch、finally都不能单独使用,try需要配合catch或finally或catch和finally一起使用。
-
执行try中的内容时,当某行代码抛出异常,不再执行try中该行代码后续的内容。
-
无论try中的代码是否会抛出异常,finally中的代码一定会执行。
-
如果代码会抛出多个异常,可以使用多个catch进行捕获,需要将异常子类放在最前,异常父类放在最后。
-
在try中定义的内容,无法在try之外的地方使用。
-
try中如果有return,不影响finally的执行,finally优先于return执行。
public class Test3 {
public static void fun() {
try {
System.out.println(10 / 0);
} finally {
System.out.println("Finally");
}
}
public static void main(String[] args) {
try{
fun();
} catch (Exception e) {
System.out.println("Exception");
} finally {
System.out.println("Finished");
}
//最终输出什么?
//1.调用fun(),该方法会抛出异常,没有catch,执行finally,输出Finally
//2.fun()方法位于main方法的try结构中,抛出的异常被catch捕获,输出Exception
//3.执行main方法中try结构的finally,输出Finished
}
}
- final,finally,finalize的区别
-
final是一个修饰符,被final修饰的属性称为常量,方法不能被重写,类不能被继承。
-
finally是try-catch-finally结构中的关键字,在无论是否抛出异常,都会执行的代码块。
-
finalize是Object类中的方法,finalize()在某个对象被回收前调用的方法。
-
throws关键字
-
这种方式,可以让编译时异常通过编译。
-
在定义方法的时候,通过该关键字声明可能抛出的异常。
-
用法:方法的参数部分之后,添加"throws 异常类型1,异常类型2…"
public class Test{
public void fun() throws InterruptException{//这时该方法就会有一个声明:该方法可能会抛出异常
//这句话直接写完后,会报错,因为sleep()方法可能会抛出InterruptException异常,属于编译时异常,必须要处理
Thread.sleep(500);
}
}
throw和throws的区别
- throws表示用于声明方法有可能出现的异常。使用时写在方法的小括号之后。
public void fun() throws InterruptException{
Thread.sleep(500);
}
- throw用于手动抛出异常对象。使用时,写在方法体中,常用于满足某种情况时,强制中断程序用法:throw 异常对象;
public void fun2(){
for(int i=0;i<10;i++){
if(i==5){
//手动抛出异常
throw new NullPointerException();
}
}
}
自定义异常
如果需要在某种情况下中断程序,可以自定义一个异常类,再通过throw关键字手动抛出。
步骤
-
定义一个类,继承某个异常。
-
如果继承的是RuntimeException,表示自定义的异常类属于运行时异常,该异常对象可以不用处理。
-
如果继承的是非RuntimeException,表示自定义的异常类属于编译时异常,该异常对象必须要处理。
-
-
可选操作。定义带参构造方法,参数为异常信息,调用父类中的构造方法。
/*
* 自定义一个异常类,用于密码输入次数过多时抛出该异常
* 这里继承的是RuntimeException,表示运行时异常,可以不用处理该类的对象
* */
public class PasswordErrorException extends RuntimeException{
/*
* 可选操作
* 定义无参数的构造方法,调用父类中无参数的构造方法
* */
public PasswordErrorException (){
super();
}
/*
* 可选操作
* 定义带参数的构造方法,调用父类中带参数的构造方法
* 参数为异常信息
* */
public PasswordErrorException(String msg){
super(msg);
}
}
public class Main {
public static void main(args[] String){
throw new PasswordErrorException("密码输入错误");
} //抛出异常:密码输入错误ag-0-1gjasfj0vag-1-1gjasfj0v
}
数组和集合
数组的特点
- 数组中保存的元素都是有序的,可以通过下标快速访问。
- 数组中保存的数据都是同一种类型。
- 数组的长度在定义后,无法改变。
- 数组无法获取其中保存的元素实际数量。
集合的特点
- 能保存一组数据,可以有序可以无序。
- 集合的容量可变。
- 集合中可以保存不同类型的数据。
- 可以获取集合中保存的元素实际数量。
集合框架(集合家族)
Collection还有父接口Iterable,但Iterable接口不算严格意义上的集合的根接口。它称为迭代器,是用于遍历集合元素的一个工具接口。
所以集合的根接口为Collection接口和Map接口,位于java.util包中
Collection接口
-
该接口有两个核心子接口:List和Set。
-
这两个接口都可以保存一组元素,List接口保存元素时,是有序可重复的;Set接口保存元素时,是无序不重复的。
常用方法 | 返回值 | 作用 |
---|---|---|
add(Object obj) | boolean | 将元素添加到集合中 |
size() | int | 获取集合中的元素数量 |
isEmpty() | boolean | 判断集合是否为空 |
clear() | void | 清空集合 |
contains(Object obj) | boolean | 判断集合中是否存在指定元素 |
remove(Object obj) | boolean | 移除集合中的指定元素 |
toArray() | Object[] | 将集合转换为数组 |
iterator() | Iterator | 获取集合的迭代器对象,用于遍历集合 |
List接口(有序可重复)
有序集合,元素可以重复,允许保存null,可以通过索引获取对应位置上的元素。
在该接口继承Collection接口的同时,又拓展了一些操作元素的方法,如添加到指定索引、根据索引删除、获取指定索引的元素、截取子集合的方法等。
常用方法 | 返回值 | 作用 |
---|---|---|
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(Object obj) | int | 得到某元素第一次出现的索引,没有返回-1 |
lastIndexOf(Object obj) | int | 得到某元素最后一次出现的索引,没有返回-1 |
subList(int from,int to) | List | 截取[from,to)区间内的元素,返回子集合 |
ArrayList实现类(掌握)
- 采用数组实现的集合。
- 可以通过索引访问元素,可以改变集合大小。如果要在其中插入或删除元素时,会影响后续元素。
- 该集合中保存的都是引用类型,即便保存了数组123,也保存的是Integer类型的123,而不是int类型的123。
- 该集合查询效率高,中途增加和删除元素效率低。
构造方法
常用构造方法 | 说明 |
---|---|
ArrayList() | 创建一个Object类型的空数组。在调用添加方法后,才会更改该数组大小为10 |
ArrayList(int initialCapacity) | 创建一个指定容量的Object数组,如果参数为负,会抛出IllegalArgumentException异常 |
常用方法
ArrayList中的常用方法,就是Collection接口和List接口中定义的方法。
LinkedList实现类
- 采用双向链表实现的集合
- 集合中保存的每个元素也称为节点,除首尾节点外,其余节点都保存了自己的信息外,还保存了其前一个和后一个节点的地址。
- 如果在双向链表的数据结构中插入和删除操作节点时,不会影响其他节点的位置。如添加时新节点时,只需要重写定义新节点的前后节点位置即可。
- 如果要查询某个节点时,需要从头结点或尾结点开始一步步得到目标节点的位置。
- 双向链表在中间插入和删除的效率高,随机读取的效率低。
构造方法
常用构造方法 | 说明 |
---|---|
LinkedList() | 创建一个空链表 |
常用方法
由于LinkedList既实现了List接口,又实现了Deque接口,所以还有Deque接口中的一些方法。
实现Deque接口的方法 | 说明 |
---|---|
addFirst(Object obj) | 添加头元素 |
addLast(Object obj) | 添加尾元素 |
removeFirst() | 移除头元素 |
removeLast() | 移除尾元素 |
getFirst() | 得到头元素 |
getLast() | 得到尾元素 |
remove() | 移除头元素 |
pop() | 移除头元素 |
push(Object obj) | 添加头元素 |
peek() | 得到头元素 |
poll() | 移除头元素 |
offer(Object obj) | 添加尾元素 |
ArrayList和LinkedList的区别
- 这两个类都是List接口的实现类,保存的元素有序可重复,允许保存null。
- ArrayList采用数组实现,随机读取效率高,插入删除效率低,适合用于查询。
- LinkedList采用双向链表实现,插入删除时不影响其他元素,效率高,随机读取效率低,适合用于频繁更新集合。
用List接口写新闻的增删改查
News类
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Objects;
public class News {
private String newsId;
private String title;
private String content;
private String editor;
private String date;
Calendar cd = Calendar.getInstance();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
//存新闻信息
public News(String title, String content, String editor) {
this.title = title;
this.content = content;
this.editor = editor;
this.date = sdf.format(cd.getTime());
}
//用于修改新闻(编号,内容)
public News(String newsId, String content) {
this.newsId = newsId;
this.content = content;
}
public String getNewsId() {
return newsId;
}
public void setNewsId(String newsId) {
this.newsId = newsId;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getEditor() {
return editor;
}
public void setEditor(String editor) {
this.editor = editor;
}
public String getDate() {
return date;
}
public void setDate(String date) {
this.date = date;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
News news = (News) o;
return Objects.equals(title, news.title) && Objects.equals(content, news.content) && Objects.equals(editor, news.editor);
}
@Override
public String toString() {
return "News{" +
"newsId='" + (Integer.parseInt(newsId)+1) + '\'' +
", title='" + title + '\'' +
", content='" + content + '\'' +
", editor='" + editor + '\'' +
", date='" + date + '\'' +
'}';
}
}
NewsManage类
import com.sun.xml.internal.ws.api.model.wsdl.WSDLOutput;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.List;
import java.util.Scanner;
public class NewsManage {
List<News> list = new ArrayList();
Scanner sc = new Scanner(System.in);
//添加
public void addNews(){
System.out.print("请输入标题:");
String title = sc.next();
System.out.print("请输入内容:");
String content = sc.next();
System.out.print("请输入编辑:");
String editor = sc.next();
if (list.contains(new News(title, content, editor))) {
System.out.println("添加失败!");
} else {
list.add(new News(title, content, editor));
list.get(list.size()-1).setNewsId(String.valueOf(list.size()-1));
System.out.println("添加成功!");
return;
}
}
//删除
public void deleteNews(){
showAll();
System.out.print("请输入删除的编号:");
int num = sc.nextInt();
for (int i = num; i < list.size(); i++) {
//删除后将后面的序号全部-1
list.get(i).setNewsId(String.valueOf(Integer.parseInt(list.get(i).getNewsId())-1));
}
list.remove(num-1);
}
//查看全部
public void showAll(){
for (int i = 0; i < list.size(); i++) {
System.out.println("编号:" + (Integer.parseInt(list.get(i).getNewsId())+1) + "\t标题:" + list.get(i).getContent());
}
}
//查看详情
public void searchNews(){
showAll();
System.out.print("请输入查看的编号:");
int num = sc.nextInt();
System.out.println(list.get(num-1));
}
public void changeNews(){
showAll();
System.out.print("请输入修改的编号:");
int num = sc.nextInt();
System.out.println(list.get(num-1));
System.out.print("请输入修改的内容:");
String content = sc.next();
list.get(num-1).setContent(content);
System.out.println("修改成功!");
System.out.println("修改后为:" + list.get(num - 1));
}
}
Main类
import java.util.Scanner;
public class Main {
Scanner sc = new Scanner(System.in);
public static void main(String[] args) {
Main main = new Main();
main.menu();
}
public void menu(){
NewsManage newsManage = new NewsManage();
while (true) {
System.out.println("----------------");
System.out.print("1.添加新闻\n2.删除新闻\n3.修改新闻\n4.查看所有\n5.查看详情\n0.退出\n请选择操作:");
int num = sc.nextInt();
if (num == 0) break;
switch (num) {
case 1:
newsManage.addNews();
continue;
case 2:
newsManage.deleteNews();
continue;
case 3:
newsManage.changeNews();
continue;
case 4:
newsManage.showAll();
continue;
case 5:
newsManage.searchNews();
continue;
}
}
}
}
Set接口(无序不重复)
无序集合,元素不可以重复,允许保存null,没有索引。
Set接口中没有自己定义的方法,都是继承于Collection接口中的方法。
哈希表hash table
哈希表,也称为散列表,是一种数据结构,能更快地访问数据。
要保存的数据称为原始值,这个原始值通过一个函数得到一个新的数据,这个函数称为哈希函数,这个新数据称为哈希码,哈希码和原始值之间有一个映射关系,这个关系称为哈希映射,可以构造一张映射表,这个表称为哈希表。在哈希表中,可以通过哈希码快速地访问对应的原始值。
假设原本的数据为左侧的数组。
如果要查询10,需要遍历数组,效率不高。
通过一个特定的函数"原始值%5",得到一组新数据,让新数据重新对应元素,保存到“新数组”中,这个“新数组”称为哈希表。
这时如果要查询10,由于哈希函数是通过%5得到了0,所以直接查询哈希表中0对应的元素即可。
整个过程中,这个函数称为哈希函数,得到的新数据称为哈希码,新数组称为哈希表,对应关系称为哈希映射。
这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实际值不同),为了解决哈希冲突,可以使用"拉链法",将2这个哈希码所在的位置向链表一样进行延伸。
哈希码的特点
- 如果两个对象的hashCode不同,这两个对象一定不同。
- 如果两个对象的hashCode相同,这两个对象不一定相同。
- hashCode相同,对象不同,这种现象称为哈希冲突。
- "通话"和"重地"这两个字符串的hashCode相同,但是两个不同的对象。
HashSet
- 采用哈希表实现。
- 元素不能重复,无序保存,允许保存一个null。
- 本质是一个HashMap对象。
- 使用HashSet集合时,通常要重写实体类中的equals和hashcode方法。
构造方法
常用构造方法 | 说明 |
---|---|
HashSet() | 创建一个空集合,实际是创建一个HashMap对象。 |
常用方法
HashSet中没有属于自定义的方法,都是重写了父接口Set和Collection中的方法。这里参考Collection中的方法即可。
没有与索引相关的方法。
HashSet添加数据的原理
如果两个元素的hashCode相同且equals结果为true,视为同一个对象,不能添加。
每次向集合中添加元素时,先判断该元素的hashCode是否存在
- 如果不存在,视为不同对象,直接添加。
- 如果存在,再判断equals方法的结果。
- 如果false,视为不同对象,可以添加。
- 如果true,视为同一对象,不能添加。
由此可见,不能添加的条件是两个对象的hashCode相同且equals的结果为true。
如果每次只判断equals的话,由于equals方法通常重写时会判断很多属性,效率不高。
如果每次只判断hashCode的话,效率高,但有可能会有哈希冲突,所以先判断hashCode,再判断equals,技能保证效率,又能保证不添加重复元素。
equals方法和hashCode的关系
-
如果两个对象的equals方法结果为true,在没有重写equals方法的前提下,hashCode相同吗?
- 如果没有重写equals,默认是Object中使用==判断,如果结果为true,说明是同一个对象,hashCode一定相同。
-
如果两个对象的hashCode不同,在没有重写equals方法的前提下,equals方法的结果为?
- hashCode不同,说明不是同一个对象,没有重写equals,说明使用Object中equals的==判断,结果为false。
-
如果两个对象的hashCode相同,equals方法的比较结果为?
-
可能为true也可能为false。
String str1="hello"; String str2="hello"; //以上两个字符串使用同一个地址,hashCode相同,equals方法为true String str3="通话"; String str4="重地"; //以上连个字符串是不同地址,但hashCode相同,因为哈希冲突,equals方法为falseag-0-1gjasfj0vag-1-1gjasfj0v
-
如果想要保存的对象保证不重复,且无关顺序,可以使用HashSet。
//创建一个HashSet集合
HashSet<Goods> hs = new HashSet<>();
//创建几个Goods对象
//g1、g2、g3的属性不同,生成的hashcode不同,都能添加
Goods g1 = new Goods("康师傅", "冰红茶", 3);
Goods g2 = new Goods("康师傅", "红烧牛肉面", 5);
Goods g3 = new Goods("农夫山泉", "矿物质水", 2);
//g3与g4的属性相同,生成的hashcode相同,继续判断equals
Goods g4 = new Goods("农夫山泉", "矿物质水", 2);
//第一次添加,一定可以添加
hs.add(g1);
//第二次添加,对象的属性不同,hashcode不同,可以添加
hs.add(g2);
//第三次添加,对象的属性不同,hashcode不同,可以添加
hs.add(g3);
//第四次添加,对象的属性相同,hashcode相同,再判断equals结果,true,视为已存在,无法添加
hs.add(g4);
TreeSet实现类
- 特殊的Set实现类,数据可以有序保存,可以重复,不能添加null。
- 采用红黑树(自平衡二叉树)实现的集合。
- 二叉树表示某个节点最多有两个子节点。
- 某个节点右侧节点值都大于左侧节点值。
- 红黑树会经过不停的"变色"、"旋转"达到二叉树的平衡。
- 只能添加同一种类型的对象且该类实现了Comparable接口。
- 实现Comparable接口后必须要重写compareTo()方法。
- 每次调用添加add(Object obj)方法时,就会自动调用参数的compareTo()方法。
- compareTo()方法的返回值决定了能否添加新元素和新元素的位置。
- 如果返回0,视为每次添加的是同一个元素,不能重复添加。
- 如果返回正数,将新元素添加到现有元素之后。
- 如果返回负数,将新元素添加到现有元素之前。
- 添加的元素可以自动排序。
构造方法
常用构造方法 | 说明 |
---|---|
TreeSet() | 创建一个空集合,实际是创建了一个TreeMap对象 |
常用方法
属于Set的实现类,所以能使用Collection和Set中的方法,除此之外,还有独有的方法。
常用方法 | 作用 |
---|---|
fisrt() | 得到集合中的第一个元素 |
last() | 得到集合中的最后一个元素 |
ceil(Object obj) | 得到比指定元素obj大的元素中的最小元素 |
floor(Object obj) | 得到比指定元素obj小的元素中的最大元素 |
TreeSet的应用
如果要保存的元素需要对其排序,使用该集合。
保存在其中的元素必须要实现Comparable接口,且重写compareTo()方法,自定义排序规则。
//Main方法中部分代码
TreeSet<Employee> emps = new TreeSet<>();
Employee e1 = new Employee(10023,"aaa","市场部");
Employee e2 = new Employee(10025,"ccc","市场部");
Employee e3 = new Employee(10028,"bbb","市场部");
Employee e4 = new Employee(10028,"xxx","市场部");
//第一个元素直接添加
emps.add(e1);
//第二个元素添加时,调用compareTo()方法 e2.compareTo(e1) e2.10025 - e1.10023 结果为正,添加在现有元素之后
emps.add(e2);
emps.add(e3);
//添加该元素时,调用compareTo()方法 e4.10028 - e3.10028 结果为0,不添加
emps.add(e4);
//Employee类中部分代码
//首先实现Comparable接口,然后重写compareTo()方法
public int compareTo(Object o) {
//此时this是当前添加的对象
//o是已存在的集合中的对象
Employee emp = (Employee) o;
//这里用当前员工号-已有员工号,根据员工号升序
return this.getNo()-emp.getNo();
//根据姓名字符串的排序方式排序
// return emp.getName().compareTo(this.getName());
}
Map接口
Map称为映射,数据以键值对的形式保存。保存的是键与值的对应关系。
键称为Key,值称为Value,键不能重复,键允许出现一个null作为键,值无限制。
键和值都是引用类型。
常用方法 | 作用 |
---|---|
size() | 得到键值对的数量 |
clear() | 清空所有键值对 |
put(Object key,Object value) | 向集合中添加一组键值对 |
get(Object key) | 在集合中根据键得到对应的值 |
remove(Object key)/remove(Object key,Object key) | 根据键或键值对移除 |
keyset() | 获取键的集合 |
values() | 获取值的集合 |
containsKey(Object key) | 判断是否存在某个键 |
containsValue(Object value) | 判断是否存在某个值 |
entrySet() | 得到键值对的集合 |
HashMap实现类(掌握)
- JDK1.8之后,HashMap采用"数组+链表+红黑树"实现。
- 当没有哈希冲突时,元素保存到数组中。
- 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中。
- 如果链表的长度大于8,将链表转换为红黑树。
- 数据采用键值对key-value的形式保存,键不能重复,能用null作为键;值没有限制,键和值都是引用类型。
- 向HashMap集合中添加元素时,原理同HashSet。
构造方法
常用构造方法 | 说明 |
---|---|
HashMap() | 创建一个空的映射集合,默认大小为16,加载因子为0.75 |
常用方法
常用方法参考Map中的方法。
遍历集合中元素的方式
遍历List集合
ArrayList<String> nameList = new ArrayList();
nameList.add("Tom");
nameList.add("Jerry");
nameList.add("LiHua");
nameList.add("Danny");
方式一:普通for循环
System.out.println("使用普通for循环遍历");
//方式一:普通for循环
for (int i = 0; i < nameList.size(); i++) {//从0遍历到size()
String name = nameList.get(i);//通过get(int index)获取指定索引的元素
System.out.println(name);
}
方式二:增强for循环
System.out.println("使用增强for循环遍历");
//方式二:增强for循环
for (String name : nameList) {
System.out.println(name);
}
方式三:迭代器
System.out.println("使用迭代器遍历");
//方式三:迭代器
//Collection类型的集合对象.iterator(),获取迭代器
Iterator<String> iterator = nameList.iterator();
// iterator.hasNext()判断集合中是否还有下一个元素
// iterator.next();获取下一个元素
while (iterator.hasNext()) {
String name = iterator.next();
System.out.println(name);
}
遍历Set集合
Set hs = new HashSet();
hs.add(123);
hs.add("hello");
hs.add(null);
hs.add(987);
方式一:增强for循环
for(Object o : hs){
System.out.println(o);
}
方式二:迭代器
Iterator<Object> it = hs.iterator();
while(it.hasNext()){
System.out.println(it.next());
}
遍历Map集合
Map<Integer, User> hm = new HashMap<>();
User u1 = new User("admin", "123123");
User u2 = new User("tom", "123123");
User u3 = new User("jerry", "123123");
hm.put(1001, u1);
hm.put(1002, u2);
hm.put(1003, u3);
//遍历hashMap
for (Integer id : hm.keySet()) {//遍历键
//根据键得到对应的值
System.out.println(id + "\t" + hm.get(id).getUsername());
}
//得到当前hashmap对象中的所有键值对的集合
Set<Map.Entry<Integer, User>> entries = hm.entrySet();
//遍历键值对
for (Map.Entry<Integer, User> entry : entries) {
System.out.println(entry);
}
泛型
一种规范,常用于限制集合中元素的类型,省去遍历元素时判断是否为对应类型和转型的过程。
//集合在定义后,默认可以添加任意类型的数据,但通常情况下,都是保存同一种类型
List list = new ArrayList();
list.add(123);
list.add(null);
list.add("hello");
//这时如果没有限制类型,使用增强for循环遍历集合中的元素时,就只能使用Object类型变量接收
for(Object o : list){
}
用法
在定义集合遍历时,在类后面写上<引用数据类型>
集合类或接口<引用数据类型> 集合变量名 = new 集合实现类();
List<String> list = new ArrayList();
//当前集合只能保存String类型的元素
list.add("sdfsdf");
//list.add(123);//无法添加
List<Integer> list2 = new ArrayList();
list2.add(123);
Collections集合工具类
Collections是集合的根接口,定义了集合操作元素的方法。
Collections是集合的工具类,定义了集合操作元素的静态方法。
常用方法
常用方法 | 说明 |
---|---|
Collections.shuffle(List list) | 打乱List集合中元素的顺序 |
Collections.sort(List list) | 对List集合中的元素进行排序,元素必须实现Comparable接口 |
Collections.swap(List list,int a,int b) | 交换List集合中元素的索引 |
Collections.replaceAll(List list,Object oldObj,Object newObj) | 替换List集合中的旧元素为新元素 |
Collections.reverse(List list) | 将List集合中的元素反转 |
Collections.fill(List list , Object obj) | 使用指定元素填充List集合 |
Collections.rotate(List list , int n) | 将集合中最后n个元素放在最前 |
Collections.max(Collection col)/min(Collection col) | 得到集合中的最大/最小值,集合中的元素必须实现Comparable接口 |
集合和数组之间的转换
-
集合转换为数组:使用Collection接口中的toArray()方法
Object[] obj = 集合对象.toArray(); List<Integer> list = new ArrayList(); list.add(123); list.add(63); list.add(3); Integer[] nums =(Integer[]) list.toArray();
-
数组转换为集合
//一个数组对象 int[] nums ={11,2,66,3,6,21}; //定义集合对象 List list = new ArrayList(); //遍历数组的同时添加到集合中 for(int i:nums){ list.add(i); }
-
一组数据转换为集合:使用Arrays工具类中的asList(一组数据)方法
//通常将数组中的数据直接作为参数 List<String> strings = Arrays.asList("XX", "aa", "qq", "xx");
使用hashMap模拟商城购物车
Book类
import java.util.Objects;
/*
* 定义图书类
* 如果两个图书对象的所有属性相同,视为同一个对象
* 一定重写equals和hashcode方法,带所有参数
* */
public class Book {
private String name;
private String author;
private String type;
private int price;
@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", type='" + type + '\'' +
", price=" + price +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Book book = (Book) o;
return price == book.price &&
Objects.equals(name, book.name) &&
Objects.equals(author, book.author) &&
Objects.equals(type, book.type);
}
@Override
public int hashCode() {
return Objects.hash(name, author, type, price);
}
public Book(String name, String author, String type, int price) {
this.name = name;
this.author = author;
this.type = type;
this.price = price;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public int getPrice() {
return price;
}
public void setPrice(int price) {
this.price = price;
}
}
BookShop类
import java.util.HashSet;
import java.util.Iterator;
import java.util.Scanner;
/*
* 图书商城类
* */
public class BookShop {
private HashSet<Book> bookSet;
/*
* 无参构造方法初始化集合,添加对象到集合中
* */
public BookShop() {
bookSet = new HashSet<>();
Book book1 = new Book("天龙八部", "金庸", "小说", 66);
Book book2 = new Book("火影忍者", "岸本齐史", "漫画", 66);
Book book3 = new Book("小时代", "郭敬明", "小说", 29);
Book book4 = new Book("斗罗大陆", "唐家三少", "小说", 36);
Book book5 = new Book("斗破苍穹", "天蚕土豆", "小说", 36);
Book book6 = new Book("海贼王", "尾田荣一郎", "漫画", 36);
bookSet.add(book1);
bookSet.add(book2);
bookSet.add(book3);
bookSet.add(book4);
bookSet.add(book5);
bookSet.add(book6);
}
/*
* 根据图书名称获取对应的图书对象
* */
public Book findByName(String name) {
for (Book book : bookSet) {
if (book.getName().equals(name)) {
return book;
}
}
return null;
}
/*
* 遍历
* */
public void showAll() {
Iterator<Book> it = bookSet.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
public void menu(Cart cart) {
Scanner sc = new Scanner(System.in);
System.out.println("请选择功能");
System.out.println("1.查看所有图书");
System.out.println("2.添加图书到购物车");
System.out.println("3.查看购物车");
System.out.println("4.移除图书");
System.out.println("5.修改图书数量");
System.out.println("6.清空购物车");
switch (sc.nextInt()) {
case 1:
showAll();
break;
case 2:
showAll();
System.out.println("请输入要购买的图书名");
//根据书名获取对应的图书对象
Book book = findByName(sc.next());
System.out.println("请输入购买数量");
int buyNum = sc.nextInt();
//调用添加到购物车的方法
if (book != null) {
cart.addToCart(book, buyNum);
} else {
System.out.println("该图书不存在");
}
break;
case 3:
cart.showAll();
break;
case 4:
System.out.println("请输入要移除的图书名");
Book removeBook = findByName(sc.next());
if (removeBook != null) {
cart.removeFromCart(removeBook);
}
break;
case 5:
System.out.println("请输入要修改的图书名");
Book updateBook = findByName(sc.next());
System.out.println("请输入数量");
int newNum = sc.nextInt();
if(updateBook!=null){
cart.update(updateBook,newNum);
}
break;
case 6:
cart.clearCart();
break;
}
menu(cart);
}
}
Car类
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
/*
* 使用HashMap实现购物车保存图书功能
* 购物车特点:每次添加商品到购物车中时,如果该商品不存在,直接添加,如果存在,数量+1
* 使用HashMap保存购物车信息时,可以将商品对象作为键,保证不重复,将商品数量作为值
* 每次添加商品时,调用HashMap的put(商品,购买数量)
*
* */
public class Cart {
//购物车采用HashMap实现,商品作为键,数量作为值
private Map<Book, Integer> map;
/*
* 添加商品到购物车
* 参数为要添加的图书对象和数量
* */
public void addToCart(Book book, int buyNum) {
//先判断当前商品是否存在,即判断键是否存在
if (map.containsKey(book)) {
//已存在,根据键得到值
Integer num = map.get(book);
//计算最终数量后,覆盖键对应的原值
map.put(book, num + buyNum);
} else {
//不存在,添加一组新的键值对
map.put(book, buyNum);
}
}
/*
* 修改购物车中的商品数量
* */
public void update(Book book, int num) {
map.put(book, num);
}
/*
* 查看购物车
* */
public void showAll() {
System.out.println("图书名\t数量");
Set<Book> books = map.keySet();
for (Book book : books) {
System.out.println(book.getName() + "\t" + map.get(book));
}
}
/*
* 移除商品
* */
public void removeFromCart(Book book) {
map.remove(book);
}
/*
* 清空购物车
* */
public void clearCart(){
map.clear();
}
public Cart() {
map = new HashMap<>();
}
public Map getMap() {
return map;
}
public void setMap(Map map) {
this.map = map;
}
}
Main类
public class Main {
public static void main(String[] args) {
BookShop bookShop = new BookShop();
Cart cart = new Cart();
bookShop.menu(cart);
}
}
文件类File
Java中的File类,表示本地硬盘中的文件(文件和目录)的一个类。
通过这个类创建的对象,可以操作对应的文件。
构造方法
常用构造方法 | 说明 |
---|---|
File(String pathName) | 根据文件的完整路径创建File对象 |
File(String parent,String child) | 根据文件的父目录路径和自身路径创建File对象 |
File(File parent,String child) | 根据文件的父目录对应的File对象和自身路径创建File对象 |
//如想要表示 “F:\221001\笔记\面向对象部分回顾.pdf” 这个文件
//File(String pathName)
File file1 = new File("F:\\221001\\笔记\\面向对象部分回顾.pdf");
//File(String parent,String child)
File file2 = new File("F:\\221001\\笔记", "面向对象部分回顾.pdf");
//File(File parent,String child)
File parent = new File("F:\\221001\\笔记");
File file3 = new File(parent, "面向对象部分回顾.pdf");
//file1、file2、file3都表示同一个文件
常用方法
常用方法 | 说明 |
---|---|
exists() | 判断文件是否存在 |
isFile() | 判断是否为文件 |
isDirectory() | 判断是否为目录 |
getName() | 获取文件名 |
getPath() | 获取文件相对路径 |
getAbsolutePath() | 获取文件绝对路径 |
getParent() | 获取父目录的名称 |
getParentFile() | 获取父目录对象 |
lastModified() | 获取最后一次修改时间对应的毫秒数 |
length() | 获取文件所占字节 |
isHidden() | 判断文件是否隐藏 |
delete() | 删除文件或空目录 |
renameTo(File newFile) | 将原文件重命名且移动到指定目录 |
mkdir() | 创建目录 |
list() | 获取某个目录下的第一层子文件的名称的数组 |
listFiles() | 获取某个目录下的第一层子文件对象的数组 |
递归遍历文件夹
package com.hqyj.FileTest;
import java.io.File;
import java.util.Date;
public class Test3 {
//查看某个目录下的所有文件
public static void main(String[] args) {
File source = new File("文件路径");
Test3 t = new Test3();
t.fun(source);
}
/*
* 递归遍历文件夹
* */
public void fun(File source) {
//判断是否为目录
if (source.isDirectory()) {
//将其展开
for (File child : source.listFiles()) {
//因为子文件有可能是目录,继续调用本方法
fun(child);
}
}
}
}
IO
I:Input输入。
O:Output输出。
流Stream
-
在Java中,流用于表示计算机硬盘与内存之间传输数据的通道。
-
将内存中的数据存入到硬盘中,称为写write,也称为输出Output。
-
将硬盘中的数据存入到内存中,称为读read,也称为输入Input。
流的分类
Java中将流定义为类,以对象的形式表现流。流有"四大家族",是所有流的父类。
字节输入流InputStream
FileInpuStream、ObjectInputStream
字节输出流OutputStream
FileOutputStream、ObjectOutputStream
字符输入流Reader
FileReader、BufferedReader、OutputStreamWriter
字符输出流Writer
FileWriter、BufferedWriter、InputStreamReader
按方向分类
-
输入流:InputStream、Reader
- 将硬盘中的数据读取到内存中
-
输出流:OutputStream、Writer
- 将内存中的数据写入到硬盘中
按类型分
- 字节流:InputStream、OutputStream
- 读写非文本类型文件。如图片、音视频、其他文件等。
- 字符流:Reader、Writer
- 读写纯文本类型文件。如txt、md等。
如要将硬盘中某个txt文件中的内容读取到程序中,使用Reader。
如要将硬盘中的某个图片读取到程序中,使用InputStream。
如要将程序中的文本写入到硬盘中为txt类型文件时,使用Writer。
如要将程序中的数据写入到硬盘中为非文本文件时,使用OutputStream。
流的四个父类的特点
- 这四个父类都是在java.io包下,都是抽象类,不能直接创建其对象,使用其子类创建对象。
- 这四个父类中都定义了close()方法,用于关闭流对象,释放资源。
- 输入流(InputStream和Reader)都有read()方法读取数据到内存中,输出流都有write()方法写入数据到硬盘中。
- 输出流(OutputStream和Writer)都有flush()方法,用于将流中的数据冲刷到硬盘中。
- 在使用输出流对象时,一定要调用flush()或close()方法后,才能真正将数据写入到硬盘中。
- 所有的流中,以Stream结尾,都是字节流,数据以字节传输;以Reader或Writer结尾的,都是字符流,数据以字符传输。
- 读取硬盘中的数据,使用输入流,读取的文件必须存在;将数据写入到硬盘中,使用输出流,文件可以不存在,但父目录必须存在。
- 读入或写入文本时,使用字符流;读取或写入非文本时,使用字节流。
FileInputStream文件字节输入流
按字节读取硬盘中的文件。
构造方法
常用构造方法 | 说明 |
---|---|
FileInputStream(String pathName) | 根据文件名创建流对象 |
FileInputStream(File file) | 根据文件对象创建流对象 |
常用方法
常用方法 | 说明 |
---|---|
read() | 读取一个字节,返回读取到的字节 |
read(byte[] bytes) | 按字节数组读取,返回读取到的字节数量,读取到的内容保存在字节数组中 |
close() | 关闭流对象 |
FileOutputStream文件字节输出流
按字节将内存中的数据写入到硬盘中。
构造方法
常用构造方法 | 说明 |
---|---|
FileOutputStream(String pathname) | 根据文件名创建输出流对象,写入时覆盖原内容 |
FileOutputStream(String pathname,boolean append) | 根据文件名创建输出流对象,第二个参数为true,写入时追加在原内容之后 |
FileOutputStream(File file) | 根据文件对象创建输出流对象,写入时覆盖原内容 |
FileOutputStream(File file,boolean append) | 根据文件对象创建输出流对象,第二个参数为true,写入时追加在原内容之后 |
常用方法
常用方法 | 作用 |
---|---|
write(int i) | 写入一个指定字节 |
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 lef)方法,表示将字节数组中的内容,从off开始写入len个。
如有word.txt文件,其中保存aaabbbccc。
FileInputStream fis = new FileInputStream("d:/word.txt"); FileOutputStream fos = new FileOutputStream("d:/copy.txt"); byte[] bytes = new byte[4]; int count = fis.read(bytes); while (count != -1) { fos.write(bytes, 0, count) count = fis.read(bytes); } //第一次读取4个字节,即aaab,count为4 //第二次读取4个字节,即bbcc,count为4 //第三次读取1个字节c,覆盖数组中的第一个元素,即数组现在为cbcc,count为1 //这样最后一次只会写入实际读取到的c fos.close(); fis.close();
使用FileInputStream和FileOutputStream实现单文件的复制
import java.io.*;
public class CopyFile {
public static void main(String[] args) throws IOException {
//定义原文件和目标文件
File source = new File("被复制的文件路径");
File target = new File("复制到的文件路径");
//定义文件字节输入流,用于读取原文件
FileInputStream fis = new FileInputStream(source);
//定义文件字节输出流,用于写入文件
FileOutputStream fos = new FileOutputStream(target);
//定义一个字节数组,大小为8MB
byte[] bytes = new byte[1024 * 1024 * 8];
//按字节数组读取,返回读取到的字节数量
int count = fis.read(bytes);
//循环读取写入
while (count > -1) {
//将读取的字节数组写入到文件中
// fos.write(bytes);//如果调用该方法,最后一次会多写入上一次残留的数据
fos.write(bytes,0,count);//如果调用该方法,实际读取到了多少字节就写入多少
count = fis.read(bytes);
}
fis.close();
fos.close();
if (target.exists()) {
System.out.println("复制成功");
}
}
}
文件夹的复制
import java.io.*;
public class CopyDirectory {
public static void main(String[] args) {
File source = new File("被复制的文件夹路径");
File target = new File("复制到的文件夹路径");
copyDir(source, target);
}
/*
* 定义复制文件夹的方法
* */
public static void copyDir(File source, File target) {
//如果是文件,调用单文件复制的方法
if (source.isFile()) {
copyFile(source, target);
} else {//如果是文件夹
//创建要复制的目标文件夹
target.mkdir();
//展开原文件夹
for (File child : source.listFiles()) {
//定义复制后的新目标文件
//如source为F:\221001\笔记\day1.md时,递归调用的target为F:\221001\笔记副本\day1.md
File newTarget = new File(target, child.getName());//这里使用File(File parent,String child)构造方法创建target对象
//递归调用的原文件依然是当前遍历出来的子文件,目标文件就是最终复制的F:\221001\笔记副本\day1.md
copyDir(child, newTarget);
}
}
}
/*
* 定义单文件复制的方法
* */
public static void copyFile(File source, File target) {
FileInputStream fis = null;
FileOutputStream fos = null;
try {
//创建用于输入输出的流对象
fis = new FileInputStream(source);
fos = new FileOutputStream(target);
//定义字节数组
byte[] bytes = new byte[1024 * 1024 * 8];
//按数组读取
int count = fis.read(bytes);
while (count != -1) {
fos.write(bytes, 0, count);
count = fis.read(bytes);
}
} catch (FileNotFoundException e) {
System.out.println("文件不存在" + e);
} catch (IOException e) {
System.out.println("读写异常" + e);
} finally {
try {
if (fis != null) {
fis.close();
}
if (fos != null) {
fos.close();
}
} catch (IOException e) {
System.out.println("关闭流对象异常" + e);
}
}
}
}
断点调试执行细节。
FileReader文件字符输入流
按字符读取文件。
构造方法
常用构造方法 | 说明 |
---|---|
FileReader(String fileName) | 根据文件名创建文件字符输入流对象 |
FileReader(File file) | 根据文件对象创建文件字符输入流对象 |
常用方法
常用方法 | 作用 |
---|---|
ready() | 判断是否还有下一个字符 |
read() | 读取下一个字符,返回读取到的字符 |
read(char[] chars) | 按字符数组读取,返回读取到的字符数量,读取到的字符保存在字符数组中 |
close() | 关闭流对象 |
FileWriter文件字符输出流
按字符写入文件。
构造方法
常用构造方法 | 作用 |
---|---|
FileWriter(String fileName) | 按文件名创建字符输出流对象,覆盖写入 |
FileWriter(String fileName,boolean append) | 按文件名创建字符输出流对象,如果append为true,表示追加写入 |
FileWriter(File file) | 按文件对象创建字符输出流对象,覆盖写入 |
FileWriter(File file,boolean append) | 按文件对象创建字符输出流对象,如果append为true,表示追加写入 |
常用方法
常用方法 | 作用 |
---|---|
write(String str) | 按字符串写入 |
flush() | 将流中的数据冲刷到硬盘中的文件,必须调用该方法或close方法后,才能真正写入 |
close() | 关闭流对象 |
BufferedReader缓冲字符输入流(掌握)
自带缓冲区(字符数组)的字符输入流。默认字符数组大小为8192,每次最多读取8192个字符。
在读取纯文本文件(txt或md)时,首选该类。
构造方法
常用构造方法 | 作用 |
---|---|
BufferedReader(Reader in) | 创建一个带有缓冲区(大小为8192的char数组)的字符输入流对象,参数为Reader类型对象,Reader是抽象类,所以实际参数为Reader的子类,如FileReader,在FileReader对象中定义要读取的文件 |
BufferedReader(Reader in,int size) | 创建一个指定缓冲区(字符数组)大小的字符输入流对象 |
常用方法
常用方法 | 作用 |
---|---|
ready() | 判断是否还有字符 |
readLine() | 读取整行字符 |
close() | 关闭流对象 |
读取文本练习
import java.io.*;
public class Test2 {
public static void main(String[] args) throws IOException {
//创建带有缓冲区的字符输入流对象
BufferedReader br = new BufferedReader(new FileReader("F:\\221001\\笔记\\Java基础回顾.md"));
//循环判断是否还有字符
while (br.ready()) {
//读取整行
System.out.println(br.readLine());
}
//关闭最大的流对象即可
br.close();
}
}
BufferedWriter缓冲字符输出流(掌握)
自带缓冲区(字符数组)的字符输出流。
构造方法
常用构造方法 | 说明 |
---|---|
BufferedWriter(Writer writer) | 创建一个自带缓冲区的字符输出流对象,参数为一个Writer对象,Writer是一个抽象类,实际参数为Writer的子类,如FileWriter,在FileWriter中定义要将输入写入的目标文件 |
BufferedWriter(Writer writer,int size) | 创建一个指定缓冲区大小的字符输出流对象 |
常用方法
常用方法 | 作用 |
---|---|
write(String str) | 写入字符串 |
newLine() | 换行 |
flush() | 冲刷流中的数据到硬盘 |
close() | 关闭流对象 |
写入文本练习
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.SimpleFormatter;
public class Test3 {
public static void main(String[] args) throws IOException {
File file = new File("221001.txt");
//创建缓冲字符输入流对象,读取文本
BufferedReader br = new BufferedReader(new FileReader(file));
//创建集合,保存读取到的姓名
ArrayList<String> list = new ArrayList<>();
//循环读取文件中的所有字符
while (br.ready()) {
String name = br.readLine();
list.add(name);
}
//关闭
br.close();
//打乱集合中的元素
Collections.shuffle(list);
//创建日期字符串
String today = new SimpleDateFormat("yyyy.MM.dd").format(new Date());
//创建缓冲字符输出流,用于写文本,文件名为"日期+作业情况.txt",如果每次都是新建,这样写
// BufferedWriter bw = new BufferedWriter(new FileWriter(today + "作业情况.txt"));
//如果要追加,在new FileWriter("文件名",true)设置
BufferedWriter bw = new BufferedWriter(new FileWriter(today + "作业情况.txt",true));
//写入字符串
bw.write("姓名\t\t是否完成");
//换行
bw.newLine();
Scanner sc = new Scanner(System.in);
//随机3个人
for (int i = 0; i < 3; i++) {
String name = list.get(i);
System.out.println(name + "完成情况:");
String str = sc.next();
//写入读取到的内容
bw.write(name + "\t\t" + str);
//换行
bw.newLine();
}
bw.close();
}
}
ObjectOutputStream对象字节输出流(序列化)
序列化:将对象转换为文件的过程。
被序列化的对象,必须要实现Serializable接口。
这个接口是一个特殊的接口,没有定义任何方法,只是给该类加上标记,表示该类可以被序列化。
构造方法
构造方法 | 说明 |
---|---|
ObjectOutputStream(OutputStream os) | 创建一个对象字节输出流对象,参数为一个字节输出流对象,由于OutputStream是抽象类,所以使用其子类,如FileOutputStream对象,在其中定义要写入的文件 |
常用方法
常用方法 | 作用 |
---|---|
writeObject(Object obj) | 将一个对象写入到本地文件中 |
close() | 关闭流对象 |
ObjectInputStream对象字节输入流(反序列化)(掌握)
反序列化:将文件转换为对象的过程。
构造方法
常用构造方法 | 说明 |
---|---|
ObjectInputStream(InputStream is) | 创建一个对象字节输入流对象,参数为一个字节输入流对象,由于InputStream是抽象类,所以使用其子类,如FileInputStream对象,在其中定义要读取的文件 |
常用方法
常用方法 | 作用 |
---|---|
readObject() | 读取序列化后的文件,返回类型为Object |
close() | 关闭流对象 |
序列化和反序列化案例
Person类,实现Serializable接口。
import java.io.Serializable;
/*
* 如果希望该类的对象能序列化,写入对象到本地,必须要实现Serializable接口
* Serializable接口中没有任何方法,是一个标记接口,表示该类的对象可以被序列化
* */
public class Person implements Serializable {
private String name;
private int age;
private String sex;
//省略getter/setter和toString()
}
转换流
实际属于字符流,作用为将一个字节流对象转换为字符流对象。
OutputStreamWriter
将字节输出流转换为字符输出流。
InputStreamReader
将字节输入流转换为字符输入流。
转换流的使用
如果只提供了一个字节流,但要向其中写入或读取字符时,就可以使用转换流将字节流转换为字符流。
使用字符流读写字符时比字节流更方便。
Main类
import javax.swing.text.Style;
import java.io.*;
import java.util.ArrayList;
import java.util.Scanner;
//请选择功能
/*
1.读取信息
读取"信息"文件夹中的所有文件,获取其文件名,该文件名由"学号+姓名"组成。
2.保存信息
将获取到的文件名拆解为学号和姓名后,作为学生的属性,创建学生对象进行保存。
将学生对象保存到集合中,将该集合序列化,保存为一个文件。
3.加载信息
反序列化之前保存的集合文件。读取该文件,输出所有的学生信息*/
public class Main {
ArrayList<Student> list = new ArrayList<>();
public static void main(String[] args) throws IOException, ClassNotFoundException {
Main main = new Main();
File file = new File("E:\\huaqing\\第一阶段\\信息");
while (true) {
Scanner sc = new Scanner(System.in);
System.out.println("1.读取信息\n2.保存信息\n3.加载信息\n0.退出");
System.out.print("请输入你的选择:");
int num = sc.nextInt();
if (num == 0) {
break;
}
switch (num) {
case 1:
main.showAll(file);
continue;
case 2:
main.saveMassage(file);
System.out.println("保存成功");
continue;
case 3:
main.readSru();
continue;
}
}
}
public void showAll(File file){
if (file.isDirectory()) {
for (File listFile : file.listFiles()) {
showAll(listFile);
}
} else {
try {
String stu = file.getName().substring(0, file.getName().lastIndexOf("."));
System.out.println(stu);
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("找不到文件");
}
}
}
public void saveMassage(File file) throws IOException {
if (file.isDirectory()) {
for (File listFile : file.listFiles()) {
saveMassage(listFile);
}
} else {
try {
String[] stu = file.getName().substring(0, file.getName().lastIndexOf(".")).split(" ");
Student student = new Student(stu[0], stu[1]);
list.add(student);
}catch (ArrayIndexOutOfBoundsException e){
System.out.println("找不到文件");
}
}
FileOutputStream fos = new FileOutputStream("学生信息.s");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(list);
oos.close();
}
public void readSru() throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("学生信息.s"));
ArrayList<Student> sList = (ArrayList<Student>) ois.readObject();
for (Student student : sList) {
System.out.println(student);
}
ois.close();
}
}
Student类
import java.io.Serializable;
public class Student implements Serializable {
private String id;
private String name;
public Student(String id, String name) {
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Student{" +
"id='" + id + '\'' +
", name='" + name + '\'' +
'}';
}
}
网络编程
InetAddress类
表示IP对象的一个类。
public static void main(String[] args) throws UnknownHostException {
//获取本机的ip对象
// InetAddress ip = InetAddress.getLocalHost();
//获取域名
// System.out.println(ip.getHostName());
//获取真实ip地址
// System.out.println(ip.getHostAddress());
//getByName(域名) 得到域名对应的ip对象
//localhost域名表示本机,对应的ip地址为127.0.0.1
InetAddress ip = InetAddress.getByName("localhost");
//获取域名
System.out.println(ip.getHostName());
//获取ip地址
System.out.println(ip.getHostAddress());
}
Socket类和ServerSocket类
都属于Socket(套接字)对象,表示网络中的某个端点。
- Socket指普通端。
- ServerSocket指服务器端。
使用套接字对象实现两个端点(Socket和ServerSocket)之间发送文件
服务器端
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.UnknownHostException;
/*
* 使用套接字对象,实现客户端向服务端发送文件
*
* 定义服务端套接字对象
* */
public class Server {
public static void main(String[] args) throws IOException {
//以本机创建服务端套接字对象
ServerSocket server = new ServerSocket(8899, 100, InetAddress.getLocalHost());
//等待客户端连接,返回连接的客户端套接字对象
Socket client = server.accept();
//定义要将读取到的数据写入到本地的文件字节输出流对象
FileOutputStream fos = new FileOutputStream("上传文件.md");
//获取客户端与服务端的输入流对象,读取发送的数据
InputStream is = client.getInputStream();
//定义读取的字节数组
byte[] bytes = new byte[1024 * 1024 * 8];
int count = is.read(bytes);
while (count != -1) {
//将读取到的数据写入到本地
fos.write(bytes, 0, count);
count = is.read(bytes);
}
fos.close();
is.close();
}
}
客户端
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.net.Socket;
/*
* 定义客户端套接字对象
* */
public class Client {
public static void main(String[] args) throws IOException {
//创建客户端套接字对象,连接指定的服务端套接字对象
Socket client = new Socket("192.168.31.39", 8899);
//获取客户端与服务端的输出流对象
OutputStream os = client.getOutputStream();
//成功连接后,将某个文件发送给服务端
//定义要发送的文件对象
File file = new File("F:\\221001\\笔记\\面向对象部分回顾.md");
//读取要发送的文件
FileInputStream fis = new FileInputStream(file);
//定义字节数组
byte[] bytes = new byte[1024 * 1024 * 8];
//循环读取要发送的文件
int count = fis.read(bytes);
while (count != -1) {
//将读取到的数据写入到客户端套接字与服务端套接字的通道中
os.write(bytes,0,count);
count = fis.read(bytes);
}
fis.close();
os.close();
}
}
进程和线程
进程Process
-
进程就是操作系统中执行的程序。一个程序就是一个执行的进程实体。
-
每个运行中的进程,都有属于它独立的内存空间,各个进程互不影响。
线程Thread
-
线程是一个进程中的执行单元,一个进程中可以有多个线程。
-
多个线程,可以访问同一个进程中的资源。
-
每个线程都有一个独立的栈空间,这些线程所在的栈空间位于同一个进程空间中。
多线程
-
如果一个进程中,同时在执行着多个线程,就称为多线程。
-
多线程可以提高程序执行效率。如多个窗口卖票,可以加快卖票的效率。
-
其实每个执行的Java程序,都是多线程执行,main方法称为主线程,还有gc线程(守护线程)在同时运行。如有一个工厂,工厂中有很多车间,每个车间有很多流水线。工厂就是内存,车间就是各个进程,每个流水线都是一个进程中的一个线程。
并行和并发
并行
各个进程同时执行,称为并行。
并发
多个线程同时执行,称为并发。
同步和异步
同步
所有的任务排队执行,称为同步执行。
异步
在执行任务A的同时,执行任务B,称为异步执行。
Java中的线程Thread类
-
Java中,线程以对象的形式存在。
-
Thread类表示线程类。
获取线程对象
-
获取当前正在运行的线程对象。
Thread ct = Thread.cuurentThread();
-
创建一个线程对象。
构造方法
常用构造方法 说明 Thread() 创建一个默认的线程对象 Thread(String name) 创建一个指定名称的线程对象 Thread(Runnable target) 将一个Runnable对象包装为线程对象 Thread(Runnable target,String name) 将一个Runnable对象包装为线程对象同时设置线程名 线程常用方法
方法 作用 getId() 获取线程id getName() 获取线程名,默认Thread-n getPriority() 获取线程优先级,默认为5 getState() 获取线程状态 setName(String str) 设置线程名 setPriority(int priority) 设置线程优先级,范围在1-10,值越大越优先执行 isDaemon() 判断线程是否为守护线程 setDaemon(boolean f) 参数为true表示设置线程为守护线程 start() 让线程进入就绪状态 run() 线程获得执行权时执行的方法(线程要做的事情) Thread.sleep(long m) 设置当前线程休眠m毫秒 Thread.currentThread() 获取当前执行的线程对象 Thread.yield() 线程让步
实现多线程
方式一:继承Thread类
- 1.创建一个类,继承Thread类。
- 2.重写Thread类中的run()方法。
- 3.创建自定义的线程子类对象后,调用start()方法。
自定义Thread线程的子类
/*
* 实现多线程步骤
* 1.成为Thread的子类
* 2.重写run()方法
* 3.创建当前类对象后,调用start()方法
* */
public class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//让该线程输出0-99
System.out.println(getName() + ":" + i);
}
}
public MyThread(String name) {
super(name);
}
public MyThread() {
}
}
main类
public class Test2 {
public static void main(String[] args) {
//创建无参数的自定义线程对象
MyThread t1 = new MyThread();
t1.setName("线程A");
//创建自定义线程对象,参数为线程名
MyThread t2 = new MyThread("线程B");
//让两个线程自动执行,必须调用start()
t1.start();
t2.start();
}
}
方式二:实现Runnable接口(建议使用)
由于Java中是单继承,如果某个类已经使用了extends关键字去继承了另一个类,这时就不能再通过extends继承Thread实现多线程。
就需要实现Runnable接口的方式实现多线程。
- 1.自定义一个类,实现Runnable接口。
- 2.重写run()方法,将多线程要执行的内容写在该方法中。
- 3.创建Runnable接口的实现类对象。
- 4.使用构造方法Thread(Runnable target)或Thread(Runnable target,String name)将上一步创建的Runnable实现类对象包装为Thread对象。
自定义Runnable接口的实现类
/*
* 实现多线程步骤
* 1.成为Runnable的实现类
* 2.重写run()方法
* 3.创建该类对象
* 4.将其包装为Thread对象
* */
public class MyThread2 implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
//让该线程输出0-99
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
main类
public class Test2 {
public static void main(String[] args) {
//创建Runnable接口的实现类
Runnable target = new MyThread2();
//由于启动多线程必须要通过Thread的start()方法,所以一定要创建Thread对象
Thread mt = new Thread(target,"线程A");//这里使用Thread(Runnable target)构造方法创建Thread对象
//让线程就绪
mt.start();
//创建另一个线程对象,让线程就绪
new Thread(new MyThread2(),"线程B").start();
}
}
方式三:使用匿名内部类
如果不想创建一个Runnable接口的实现类,就可以使用匿名内部类充当Runnable接口的实现类。
/*
* 实现多线程的方式三:
* 使用匿名内部类
* */
public class Test3 {
public static void main(String[] args) {
//使用Thread(Runnable target ,String name)构造方法创建线程对象
//此时new Runnable() { @Override public void run() {}}就是一个匿名内部类
new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}, "自定义线程").start();
//如果main方法当做一个线程时,需要先启动其他线程后,在执行main方法中的内容,否则依然是按顺序执行
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
线程的生命周期
线程的初始化到终止的整个过程,称为线程的生命周期。
新生状态
当线程对象被创建后,就进入了新生状态。
就绪状态
-
当某个线程对象调用了start()方法后,就进入了就绪状态。
-
在这个状态下,线程对象不会做任何事情,只在等他CPU调度。
运行状态
-
当某个线程对象得到CPU时间片(CPU执行这个线程的机会所给的时间),则进入运行状态,开始执行run()方法。
-
不会等待run()方法执行完毕,只会在指定的时间内尽可能地执行run()方法。只要调用玩run()方法后,就会再进入就绪状态。
阻塞状态
-
如果某个线程遇到了sleep()方法或wait()方法时,就会进入阻塞状态。
-
sleep()方法会在指定时间后,让线程重新就绪。
-
wait()方法只有在被调用notify()或notifyAll()方法唤醒后才能重新就绪。
终止状态
当某个线程的run()方法中的所有内容都执行完,就会进入终止状态,意味着该线程的使命已经完成。
守护线程
-
如果将一个线程设置setDeamon(true),表示该线程为守护线程。
-
守护线程会随着其他非守护线程终止而终止。
/*
* Test类是一个自定义线程类,死循环输出
* */
public class Test implements Runnable {
public static void main(String[] args) {
Thread thread = new Thread(new Test());
//将自定义线程类设置为守护线程
thread.setDaemon(true);
thread.start();
//main线程终止,守护线程也会终止
for (int i = 0; i < 100; i++) {
System.out.println("main方法中的循环执行中");
}
}
@Override
public void run() {
while (true) {
System.out.println("守护线程执行中。。。");
}
}
}
多线程访问同一个资源
可能出现的问题
-
如银行存款100,同一时刻在手机和ATM一起取出,如果用多线程模拟,可能会出现两个线程都取出100的情况。要避免这种情况发生。
-
本应该大于售出后再减,再打印剩余,由于线程A在打印"售出一张"后,还没来得及执行后续内容,其他线程就开始执行了。
出现问题的原因
-
由于线程调用start()方法后,就进入就绪状态。如果获得了CPU时间片,就开始调用run()方法,调用run()方法后,就会再次进入就绪状态,不会等待run()方法执行完毕,所以在线程A执行run()方法的时候,线程B也开始执行了,这样就会出现数据共享的问题。
-
因为现在所有的线程都是异步(同时)执行。
如何解决
- 让线程同步(排队)执行即可。这样一来,某个线程执行run()方法的时候,让其他线程等待run()方法的内容执行完毕。
synchronized关键字
这个关键字可以修饰方法或代码块。
修饰方法
写在方法的返回值之前,这时该方法就称为同步方法。
public synchronized void fun(){
//会排队执行的代码
}
修饰代码块
写在一个独立的{}前,这时该段内容称为同步代码块。
synchronized(要同步的对象或this){
//会排队执行的代码
}
原理
-
每个对象默认都有一把"锁",当某个线程运行到被synchronized修饰的方法时,该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其结束后,才会释放这把锁。
-
使用synchronized修饰后的锁称为"悲观锁"。
-
方法被synchronized修饰后,称为同步方法,就会让原本多线程变成了单线程(异步变为同步)。
实现多线程的方式
- 继承Thread类。
- 实现Runnable接口后,包装为Thread对象。
- 匿名内部类。
为什么说StringBuilder或ArrayList、HashMap是非线程安全的?
public class Test { public static void main(String[] args) throws InterruptedException { // StringBuilder sb = new StringBuilder(); StringBuffer sb = new StringBuffer(); //循环10次创建10个线程对象 for (int i = 0; i < 10; i++) { //创建线程对象 new Thread(new Runnable() { @Override public void run() { //每个线程都向StringBuilder对象中添加100次字符串 try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } for (int j = 0; j < 10; j++) { sb.append("hello"); } } }).start(); } Thread.sleep(5000); //如果正常,应该长度为10线程*10次添加*每次5个字母 长度为500 System.out.println(sb.length()); //如果用StringBuilder,最终的长度可能不为500 //如果用StringBuffer,最终的长度一定为500 //所有StringBuffer是线程安全的,适用于多线程 //所有StringBuilder是非线程安全的,适用于单线程 } }
-
什么叫死锁?怎么产生?如何解决?
如果有两个人吃西餐,必须有刀和叉,此时只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己拥有的,这时就会造成僵持的局面,这个局面就称为死锁,既不结束,也不继续。
模拟死锁出现的情况
定义两个线程类,线程A先获取资源A后,在获取资源B;线程B先获取资源B后,再获取资源A。
如果对资源A和资源B使用了synchronized进行同步,就会在线程A获取资源A的时候,线程B无法获取资源A,相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。
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对象,等待3s后获取fork对象
*
* */
@Override
public void run() {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}
PersonB线程
public class PersonB implements Runnable {
//定义两个共享的成员变量,刀、叉
private Object knife;
private Object fork;
public PersonB(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run方法时,先获取fork对象,等待3s后获取对象knife
*
* */
@Override
public void run() {
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,3s后获取knife");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,可以吃饭了");
}
}
}
}
死锁的解决方式
方式一
让两个线程获取资源的顺序保持一致。
如两个线程都先获取knife,再获取fork。
@Override
public void run() {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
方式二
让两个线程在获取资源A和B之前,再获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个资源后,将后续内容执行完毕,其他线程才能开始执行。
如在获取knife和fork之前,先获取paper对象。
@Override
public void run() {
//先获取paper,再进行后续操作
synchronized (paper) {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}
.length());
//如果用StringBuilder,最终的长度可能不为500
//如果用StringBuffer,最终的长度一定为500
//所有StringBuffer是线程安全的,适用于多线程
//所有StringBuilder是非线程安全的,适用于单线程
}
}
-
什么叫死锁?怎么产生?如何解决?
如果有两个人吃西餐,必须有刀和叉,此时只有一副刀叉。
如果A拿到了刀,B拿到了叉,互相都在等待另一个工具,但都不释放自己拥有的,这时就会造成僵持的局面,这个局面就称为死锁,既不结束,也不继续。
模拟死锁出现的情况
定义两个线程类,线程A先获取资源A后,在获取资源B;线程B先获取资源B后,再获取资源A。
如果对资源A和资源B使用了synchronized进行同步,就会在线程A获取资源A的时候,线程B无法获取资源A,相反线程B在获取资源B的时候,线程A无法获取资源B,所以两个线程都不会得到另一个资源。
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对象,等待3s后获取fork对象
*
* */
@Override
public void run() {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}
PersonB线程
public class PersonB implements Runnable {
//定义两个共享的成员变量,刀、叉
private Object knife;
private Object fork;
public PersonB(Object knife, Object fork) {
this.knife = knife;
this.fork = fork;
}
/*
* 该线程执行run方法时,先获取fork对象,等待3s后获取对象knife
*
* */
@Override
public void run() {
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,3s后获取knife");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,可以吃饭了");
}
}
}
}
死锁的解决方式
方式一
让两个线程获取资源的顺序保持一致。
如两个线程都先获取knife,再获取fork。
@Override
public void run() {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
方式二
让两个线程在获取资源A和B之前,再获取第三个资源,对第三个资源使用synchronized进行同步,这样某个线程在获取第三个资源后,将后续内容执行完毕,其他线程才能开始执行。
如在获取knife和fork之前,先获取paper对象。
@Override
public void run() {
//先获取paper,再进行后续操作
synchronized (paper) {
synchronized (knife) {
System.out.println(Thread.currentThread().getName() + "获取了knife,3s后获取fork");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fork) {
System.out.println(Thread.currentThread().getName() + "获取了fork,可以吃饭了");
}
}
}
}