华清远见-重庆中心-JAVA高级阶段技术总结

华清远见-重庆中心-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)

  1. 方法一:String.valueOf(Object obj)方法

    StringBuilder sb = new StringBuilder("你好");
    //调用静态方法
    String str = String.valueOf(sb);
    
  2. 方法二:对象.toString();

    StringBuilder sb = new StringBuilder("你好");
    //调用toString()
    String str = sb.toString();
    
  3. 方法三:+“”

    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星期
以上两个字母都可以写成一个,如月份5M:5,MM:05
yyyy/MM/dd HH:mm:ss E2022/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.HOUR12进制小时
Calendar.HOUR_OF_DAY24进制小时
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类型,所以一定要使用转换为原始 类型的方法。

包装类原始类型
Integerint
Floatfloat
Doubledouble
Characterchar
Booleanboolean
Longlong
Shortshort
Bytebyte

特点

  • 八个原始类型中,除了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));
    

注意

  1. 如果通过构造方法创建的包装类对象,会有不同的内存地址,使用==判断结果为false。
  2. 自动装箱方式创建包装类对象,赋值范围在byte范围[-128,127]内,将这个值保存在缓冲区 中,如果多个对象使用同一个数值,共享这个数据,使用同一个地址,使用== 判断结果为 true;如果不再byte范围内,就会创建新的包装类对象,会有不同的内存地址,使用==判断结 果为false。
  3. 引用类型对象比较值是否相同时,不要使用==,而是要使用重写的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关键字手动抛出。

步骤

  1. 定义一个类,继承某个异常。

    • 如果继承的是RuntimeException,表示自定义的异常类属于运行时异常,该异常对象可以不用处理。

    • 如果继承的是非RuntimeException,表示自定义的异常类属于编译时异常,该异常对象必须要处理。

  2. 可选操作。定义带参构造方法,参数为异常信息,调用父类中的构造方法。

/*
* 自定义一个异常类,用于密码输入次数过多时抛出该异常
* 这里继承的是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实现类(掌握)

img

  • 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

FileInpuStreamObjectInputStream

字节输出流OutputStream

FileOutputStreamObjectOutputStream

字符输入流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);
        }
    }

}
线程的生命周期

线程的初始化到终止的整个过程,称为线程的生命周期。

img

新生状态

当线程对象被创建后,就进入了新生状态。

就绪状态

  • 当某个线程对象调用了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,可以吃饭了");
            }
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值