华清远见-重庆中心-JAVA高级阶段技术总结/知识点梳理/个人总结

文章目录

个人总结

       高级Java主要学习了java的一些类,通过这几天的学习我也明白了学习一个类的流程,首先看一下类的源代码,找一下关于类的注释,通过翻译软件了解下这些类能做的事情,然后再查看类的方法,并且对类的方法一一上手,观察该方法的作用。期间可以多参考一下先学者的博客,这有助于更全面地了解类和方法的作用,最好是能自己做一道题,从而可以熟悉和完整应用该类的一些常用方法,类似下面技术总结会有一些题目。最我觉得可以课外再看点数据结构之类的东西,或者多学点计算机硬件之类的知识,可以自己换一下电脑的配置等。


技术总结/知识梳理

String字符串

string是一个类,属于数据类型中的引用类型。

在Java中一切使用双引号引起来的内容,都是这个类的实例,称为字符串对象。

字符串在定义后,值不可改变,是一个常量,实际是一个字符数组

String name=“tom”; //name是字符串的变量,"tom"才是真正的字符串,存储到字符串常量池(字符串缓冲区)中

name=“ez”;//"ez"被存到name地址中。

Person p = new Person(“a”);

p = new Person(“b”);

//这句话执行时,创建一个“Tom”字符串对象,将其地址保存在变量name中
        String name = "Tom";
        //这句话在字符串常量池中创建了新的字符串“Jerry”,将地址保存到name里,整个过程是将name引用的地址从“Tom”的地址变成引用了“Jerry”的地址
        name="Jerry";
        //以上两句,在内存中,会有两个字符串对象“Tom”和“Jerry”,没有任何字符串发生了改变,知识name引用了不同的字符串地址

        //字符串可以当成数组使用
        String str1="hello";

        //字符串对象实际是一个字符数组对象“包装”而来
        char[] list={'h','e','l','l','o'};
        String str2 = new String(list);
        System.out.println(str1);
        System.out.println(str2);

请添加图片描述

String类使用时注意

由上方案例可见,如果频繁地将一个String类型变量的值进行更改时,会创建很多字符串对象。效率低,浪费内存空间。

所以在频繁更改字符串时,不要使用String类变量。

System.out.println("开始执行");
String str="";
//创建一万个,一万次的循环,就会创建5万个字符串对象,但最终只会有一个字符串对象被str引用
for(int i =0;i<=10000;i++){
    str+=i;
}
System.out.println("执行结束");

如果要频繁更改字符串,使用StringBuilder类或StringBuffer类

如何创建字符串对象

1.使用“”赋值创建

String str = "abc";

2.通过构造方法创建

常用构造方法说明
String()创建一个空白的字符串对象
String(String str)创建一个指定字符串的字符串对象。
String(char[] list)创建一个指定字符数组的字符串对象
String(byte[] list,String charsetName)按指定的编码格式创建一个指定字节数组的字符串对象

不同方式创建字符串的过程

使用“”赋值的形式过程

//这句话执行时,先判断字符串常量池(缓冲区)中是否存在“ab“,不存在则创建,将其保存到str1变量中
        String str1="ab";
        //这句话执行时,先判断字符串常量池(缓冲区)中是否存在“ab“,已存在,不用创建,将其地址保存到str2变量中
        String str2="ab";
        //这句话执行时,+两端如果都是“”定义的字符串,编译前会进行拼接,拼接后再判断字符串常量池中是否存在,存在,不用创建
        String str3="a"+"b";

        System.out.println(str1==str2);//true
        System.out.println(str1==str3);//true

请添加图片描述

可以用jdk中自带的反编译工具javap对class文件进行反编译

在class文件所在目录下,进入控制台,输入javap-c字节码文件名.class

请添加图片描述

使用构造方法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

请添加图片描述

使用+拼接“”和new出来的字符串对象

//在字符串常量池中创建“ab”
        String str1="ab";
        //1.创建StringBuilder()对象
        //2.在字符串常量池中创建“a”
        //3.在字符串常量池中创建“b”
        //4.创建String对象
        //5.调用StringBuilder()的append方法,将“a“和newString("b")拼接
        
        String str2="a"+new String("b");
		
		//两个不同的地址
        System.out.println(str1==str2);//false

请添加图片描述

总结

在使用字符串时,如果要比较其值是否相同,不要使用判断,因为判断的是内存地址。

所以在比较字符串是否相同时,要使用String类重写的equals方法进行判断

该方法判断的原理大致为:将两个字符串用字符数组保存,逐个判断字符数组中的每个字符,全部一致时返回true。

所以比较的是字面值。在使用equals方法时,通常将已知的非空字符串作为调用者。

username.equals("admin");//这样写,username变量可能为空,会抛出空指针异常
"admin".equals(username);//这样写能避免空指针异常

字符串相关面试题

 //题目一
        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个对象
        
        
        //题目五
        //两句话执行后,会创建几个对象?
        String s7=new String("wor");//常量池:“wor”,堆:new String()
        String s8=s7+"ld";//常量池:”ld“ 堆:new StringBuilder()
        //4个对象
        

字符串中String类中的常用方法

方法名返回值作用
length()int得到字符串的长度
toLowerCase()String将字符串转换为小写
toUpperCase()String将字符串转换为大写
trim()String去除字符串首尾所有空格
isEmpty()boolean判断字符串是否为空白字符串“”
getBytes()byte[]将字符串转换为字节数组
toCharArray()char[]将字符串转换为字符数组
equalsIgnoreCase()boolean忽略大小判断两个字符串是否相同
equals(String str)boolean判断两个字符串是否相同
charAt(int index)char得到字符串指定索引上的字符
indexOf(String str)int得到子字符串第一次出现的索引,如果不存在,返回-1
lastIndexOf(String str)int得到子字符串(可以是长字符串,相当于在字符串数组中找)最后一次出现的索引,如果不存在,返回-1
contains(字符序列)boolean判断子字符串中是否存在于指定字符串
concat(String str)String将参数字符串拼接到原字符串末尾
startsWith(String str)boolean判断字符串是否以指定字符串开头
endsWith(String str)boolean判断字符串是否以指定字符串结尾
subString(int beginIndex)String从指定索引位置开始截取字符串至末尾
subString(int beginIndex,int endIndex)String截取[begin,end)区间索引的字符串
split(String regex)String[]按执行字符串或正则表达式切分原字符串。如果指定内容不在末尾,n个指定字符能得到n+1个子串;如果指定内容在末尾,n个指定字符串能得到n个子串
repalce(char oldChar, char newChar)String将原字符串中的所有指定字符替换为新字符
String.valueOf(参数)String将任意参数转换为字符串。通常用于将原始类型转换为字符串。
String.format(String 格式,Object…obj)String根据指定格式转换参数。常用语将浮点数保留小数。如:String.format(“%a.bf”,10/3.0)表示输出小数,表示四舍五入保留b位小数,整体占a位,如果a大于最终数据位数,在最前补充空格,不够则原样输出

作业

  • 接收一个身份证号码,输出这个人的年龄和性别{倒数第二位奇数为男}
  • 接收一个手机号码,使用“”替换中间四位,如13915812345,输出139****2345
  • 接收一个字符串,输出其倒序形式,如how are you,输出,uoy era woh
  • 接收一个网站的全域名,输出其顶级域名属于什么类型
    • 顶级域名:www.baidu.com中,com就是顶级域名
    • .com商业网站,.edu教育网站,.gov政府机构, .org论坛

可变字符串

String字符串对象是一个常量,在定义后,值不可改变。

如果使用String类的对象,对其频繁更新时,就会不停地创建新的对象,不停引用给同一个变量。

如要执行10000次循环重新赋值的过程,就要创建10000个字符串对象,执行效率很低,这时就需要使用可变字符串对象。

package com.hqyj.StringBuilderTest;
/*
* 可变字符串
* */
public class Test1 {
    public static void main(String[] args) {
        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类

用于表示可变字符型串的一个类,是非线程安全的,建议在单线程环境下使用。

StringBuffer类

用于表示可变字符串的一个类,是线程安全的,建议在多线程环境下使用。

StringBuilder和StringBuffer中方法都一致,只不过StringBuffer中的方法使用了synchronized(已同步)关键字修饰,标号四是一个同步方法,在多线程环境下不会出现问题。


这里以StringBuilder为例

构造方法

常用构造方法作用
StringBuilder()创建一个大小为16的字符串数组,表示一个空白字符。类似于String str=“”;
StringBuilder(String str)创建一个str长度+16的字符数组后将str添加到其中。类似于String str=“xxx”;

普通方法()

方法名作用
append(Object obj)添加任意类型数据到字符串末尾
delete(int start,int end)删除指定范围[start,end)的字符
deleteCharAt(int index)删除指定索引的字符
insert(int offset,String str)指定索引处插入字符(数字等也可以)
replace(int start,int end,String str)在[start,end)索引处替换字符
reverse()倒序输出原字符串

注意

  • 以上表格中的方法都是直接操作字符串对象,每次调用方法后,原字符串都会发生变化

  • StringBuilder和StringBuffer并没有重写equals方法,所以可变字符的值判断是否相同时,调用的是equals中的原始==判断。如果要判断两个可变字符串的值是否相同时,需要将其转换为String后调用equals判断

可变字符串与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、StringBuilder和StringBuffer的区别

相同点:

  • 这三个类都可以表示字符串,都提供了一些操作字符串的方法。
  • 这三个类中有公共的方法,如charAt()、indexOf()等
  • 这三个类都是被final修饰的,所以不能被继承

不同点:

  • String定义的字符串是一个常量,可变字符串定义的字符串是一个常量
  • String类中的方法,调用后,不会改变原本字符串的值;可变字符串类中的方法,调用后,会改变原本字符串的值
  • StringBuilder是非线程安全的可变字符串,StringBuffer是线程安全的可变字符串,其中的方法被synchronized修饰。

总结

在频繁操作同一个字符串时,一定要使用可变字符串StringBuilder或StringBuffer类的对象,不能使用String类的对象。

System类

这个类中包含了一些系统相关的信息和一些方法,其中的属性和方法都是静态的。

该类不能创建对象,不是因为它是一个抽象类,而是因为它的构造符是私有的。

常用属性和方法
System.out获取系统打印输出流PrintStream对象,用于控制台打印信息
System.in获取输入流inputStream对象,用于获取输入的信息
System.err获取系统打印输出流PrintStream对象,用于控制台打印异常信息
System.exit(int status)终止虚拟机运行,参数0表示正常终止。
System.currentTimemilles()获取从1970,1,1 0:0:0至今过了多少毫秒,中国是UTC(+8),所以是从1970,1,1 8:0:0至今过了多少毫秒,返回long类型。
System.arraycopy(原数组,原数组起始位置,目标数组,目标数组起始位置,原数组要复制的元素数量)将原数组中指定长度的元素复制到新数组中

RunTime类(运行时)

Runtime类的对象,表示“程序运行时对象(程序运行环境对象)”。

包含了程序运行环境相关的信息。常用语获取运行环境信息(如虚拟机内存)或执行某个命令。

特点

这个类不是一个抽象类,但不能创建对象,因为它是private修饰的私有方法。

这个类提供了一个静态方法getRuntime(),通过这个方法,可以获取一个Runtime类的对象。

这是java中的一种设计模式——单例模式(一个类只能创建一个对象)

public class Runtime {
    //定义了一个静态成员:当前类的对象
    //由于静态成员只在类加载时执行一次,所以这里只会创建一个当前类的对象
    private static Runtime currentRuntime = new Runtime();
	
	//定义了一个公共的静态方法,用于获取创建的唯一的当前类的对象
    public static Runtime getRuntime() {
        return currentRuntime;
    }

	//构造方法是私有的,不能在当前类之外创建对象
    private Runtime() {}
}

使用

package com.hqyj.test;
import java.io.IOException;
方法调用时传值问题
public class RuntimeTest {
    public static void main(String[] args) throws IOException,InterruptedException {
    
        //通过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();
    }
}

方法调用时传值问题

package com.hqyj.test2;

public class Test {
    /*
    * 当方法的参数为原始类型,方法中对该参数做修改,不会影响实际参数
    * */
    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 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());//吴彦祖

        //对方法内new的Person对象进行操作,不改变实际参数
        Person p1 = new Person();
        p1.setName("赵敏");
        fun4(p1);//易烊千玺
        System.out.println(p1.getName());//赵敏

        //改变实际参数
        int[] list = {0,1,2};
        fun5(list);//123
        System.out.println(list[0]);//123
        
        //练习
        char[] list2={'a','b','c'};
		Person p2 = new Person();
        fun(list2,p2);
		System.out.println(list2[0]);//m
		System.out.println(p2.getName());//null
    }
}


总结

参数只有是引用类型(类、数组、接口),并且方法中在直接操作该参数时,才会对实际参数造成影响。

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[o]);//方法内部直接操作数组,会影响实际参数,输出m
    System.out.println(p.getName());//方法内部创建了新对象,不会影响实际参数,输出null
}

Date类

用于表示日期时间的类,位于java.util包下

构造方法

常用构造方法说明
Date()创建当前瞬间对应的日期对象
Date(long l)创建指定瞬间对应的日期对象
Date(int year,int month,int day)该方法已过时,创建指定年月日的日期对象(年是1900年起经过的年数,月用0-11表示1-12月)

常用方法

方法名作用
getTime()得到对应Date对象表示的毫秒数
setTime(long l)设置Date对应的毫秒数
after(Date when)判断调用日期对象是否在when之后
before(Date when)判断调用日期对象是否在when之前
package com.hqyj.DataTest;

import java.util.Date;

public class Test1 {
    public static void main(String[] args) {

        //可以直接创建Date类的无参对象,表示当前日期和时间
        Date now = new Date();
        System.out.println(now);

        //可以创建Date类型待参数的对象,参数为某一个毫秒值
        Date date = new Date(0);
        //date.setMonth(6);
        System.out.println(date);

        //Date用0-11表示1-12月
        //该构造方法的第一个参数表示年份,但从1900开始计数
        //定义一个人的生日2000/5/9
        Date birthday = new Date(101, 5, 28);
        System.out.println(birthday);
        //Date对象的getTime()方法可以获取对应的时间毫秒数
        System.out.println((-birthday.getTime() + now.getTime())/1000/3600/24/365);//年
        //a.after(b)表示判断Date对象a对应的时间是否在b之后
        System.out.println(birthday.after(now));
        System.out.println(birthday.before(now));
    }
}

SimpleDataFormat类

用于格式化日期的类。

构造方法

常用构造方法作用
SimpleDateFormat(String pattern);创建一个指定日期模板的格式化日期对象

日期模板

特殊字符作用
yyyy
MM月份
dd日期
HH24小时制
hh12小时制
mm分钟
ss
E星期
以上两个字母都可以写出一个,如5月份M:5,MM:05
yyyy/MM/dd HH:mm:ss E2022/11/24 16:39:19 星期四

常用方法

方法名返回值作用
format(Date date)String将Data对象按日期模板转换为字符串
parse(String str)Date将满足日期模板的字符串转换为Date对象
package com.hqyj.DataTest;

import java.text.SimpleDateFormat;
import java.util.Date;

public class SimpleDataFormatTest {
    public static void main(String[] args) {
        //定义格式化日期所需要的时间模板
        /*
        * yyyy  年
        * MM    月份
        * dd    日期
        * HH    24小时制
        * hh    12小时制
        * mm    分钟
        * ss    秒
        * E     星期
        *
        * 两个字母都可以写成一个,如月份MM和M
        * MM    5月实际为05
        * M     5月实际为5,11月为11,没有影响
        * */
        String pattern = "yyyy/M/dd HH:mm:ss E";
        //创建格式化日期类对象,参数为日期模板
        SimpleDateFormat sdf = new SimpleDateFormat(pattern);
        //创建当前日期
        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()获取该类的一个实例。

 // 调用静态方法getInstance()获取该类的一个实例
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)获取指定日历字段支持的最大值,如:getMaximum(Calendar.DATE)最大为31
getActuralMaximum(int field)获取指定日历字段在当前日期下的实际最大值,如11月,Calendar.DATE最大30
set(int field,int value)将指定的日历字段设置为指定值
set(int year,int month,int date)同时设置日历对象的年月日
setTime(Date date)将Date对象作为参数设置日历对象的信息

实现万年历

package com.hqyj;

import java.util.Calendar;
import java.util.Scanner;

public class Test1 {
    public static void main(String[] args) {
        Calendar cal = Calendar.getInstance();

        //设置指定年月,默认为1号
        Scanner sc = new Scanner(System.in);
        System.out.println("请输入年");
        int year = sc.nextInt();
        System.out.println("请输入月");
        int month = sc.nextInt();
        cal.set(year, month-1 , 1);
        System.out.println(cal);

        int count = 0;
        System.out.println("========" + year + "月" + month + "月========");
        System.out.println("一\t二\t三\t四\t五\t六\t日");
        //打印1号是所在周的第几天
        int week = cal.get(Calendar.DAY_OF_WEEK);
        //根据第几天打印空格
        if (week == 1) {
            System.out.print("\t\t\t\t\t\t");
            count += 6;
        } else {
            for (int i = 1; i <= week - 2; i++) {
                System.out.print("\t");
                count++;
            }
        }
        //遍历当月每一天
        for (int i = 1; i < cal.getActualMaximum(Calendar.DATE); i++) {
            System.out.print(i + "\t");
            count++;
            if (count % 7 == 0) {
                System.out.println();
            }
        }
    }
}

作业

  • 输入年月,输出该月所有的工作日和周末

    • 如输入2022年11月

      // 输出工作日:1 2 3 4 7 8 9 10 11 14 15 16 17 18 21 22 23 24 25 28 29 30
      
      // 输出周末:5 6 12 13 19 20 26 27
      
  • 假设2022年12月1日起,开始执行"三天打鱼两天晒网"计划。输入一个新的日期,输出这天该打鱼还是晒网

  • 实现“员工入职记录”

    • 添加员工时输入姓名、性别、部门、职位,自动记录入职时间

    • 输出员工信息时,输出所有

包装类

Java是纯面向对象语言,宗旨是将一切事物视为对象处理。

但原始类型不属于对象,不满足面向对象的思想,但原始类型在使用时无需创建对象,保存在栈中,效率高。

为了让原始类型也有对应的类类型,达到"万物皆对象"的理念,所以有了包装类的概念

包装类就是原始类型对应的类类型。包装类通常用于字符串与原始类型之间的转换。

在web应用中,从浏览器页面中获取到后台的数据,全部都是String类型,所以一定要使用转换为原始类型的方法。

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

特点

  • 八个原始类型中,除了int和char之外,其余类型的包装类,都是将首字母改为大写,int对应的是Integer,char对应的是Character
  • 包装类都是被final修饰的,不能被继承
  • 除了Character类,其余包装类都有两个构造方法:参数为原始类型或String的构造方法。Character的构造方法只有一个,参数为char类型.这些构造方法用于将原始类型或字符串对象转换为对应的包装类对象。
  • 除了Character类,其余类都有静态方法parse原始类型(String str),用于将字符串转换为相应的原始类型
    • 数值型的包装类的parseXXX()方法,如果不是对应的数字,转换时就会抛出NumberFormatException异常,如“123bc”,或“123.4”,在使用Integer.parsrint()时都会抛出异常。
    • Boolean类型中的parseBoolean()方法,参数如果是“true”这四个字母,不区分大小写,都能转换为正在boolean类型的true,只要不是“true”这个单词,转换结果都是false
  • 除了Boolean类型,其余包装类都有MAX_VALUEMIN_VALUE这两个静态方法,用于获取对应类型支持的最大值、最小值。
  • 所有包装类都重写了toString()方法,用于将包装类对象转换为String对象。

字符串与原始类型之间的转换

字符串转换为原始类型

使用原始类型对应的包装类,调用parse原始类型(字符串)方法

String num = "123";
byte b = Byte.parseByte(num);
short s = Short.ParseShort(num);
int i = Integer.parseInt(num);
long i = Long.parseLong(num);
float f = Float.parseFloat(num);
double d = Double.parseDouble(num);
boolean flag = Boolean.parseBoolean(num);

原始类型转换为字符串

  • 使用+拼接一个空白字符串
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 = Integer.valueOf(i);//调用包装类Integer的静态方法valueOf()将原始类型转换为包装类对象
  • 所有包装类都有一个原始类型Value()方法,用于将包装类对象转换为原始类型。这个工程称为拆箱。
//手动拆箱
Integer integer = new Integer(123);//定义包装类对象
int i = integer.intValue();///调用包装类对象的intValue()方法,将包装类对象转换为原始类型对象
  • 自动装箱和拆箱。在jdk1.5之后,为了方便原始类型和包装类之间做转换,加入了自动装箱拆箱的概念,可以将原始类型和包装类对象之间互相赋值
 //自动装箱
Integer anInt = 123;
//自动拆箱
int i = anInt;
  • 自动装箱缓冲区

    //integer5和integer6保存的是创建的不同对象的地址,==判断结果为false
    Integer integer5 = new Integer(100);
    Integer integer6 = new Integer(100);
    
    //自动装箱
    Integer integer1 = 100;
    Integer integer2 = 100;
    
    //自动装箱,如果值在byte的范围[-128,127),这个值会共享,只会有一个对象“100”
    //如果值超出了byte的范围,会创建对象
    Integer integer3 = 200;//大于127或小于-128就false
    Integer integer4 = 200;//byte的范围
    
    //new的对象不同,地址不同
    System.out.println(integer1 == integer2);//true
    //byte范围内,共享对象,同一个地址
    System.out.println(integer3 == integer4);//false
    //byte范围外,new对象不同,地址不同
    System.out.println(integer5 == integer6);//false
    
    System.out.println(integer5.equals(integer6));//true
    System.out.println(integer3.equals(integer4));//true
    
    
    • 如果通过构造方法创建的包装类对象,会有不同的内存地址,==判断结果为false
    • 如果装箱方式创建包装类对象,赋值范围在byte范围[-128,127)内,将这个值保存在缓冲区中,如果多个对象使用同一个数值,共享这个数据,使用同一个地址,使用判断结果为true,如果不在byte范围内,就会创建新的包装类对象,会有不同的内存地址,使用判断结果为false
    • 引用类型对象比较值是否相同时,不要使用==,而是使用重写的equals方法。

异常

当程序没有按照开发人员的意愿正常执行,中途出现错误导致程序中断,出现这种情况,就称为异常。

学习异常就是认识异常的种类,如何处理异常和避免异常出现。

异常的产生

异常在程序中以对象的形式存在,当代码执行过程中出现异常,虚拟机会自动创建一个异常对象,如果没有对该异常对象进行处理,就会导致程序中断。

异常的分类

异常在程序中以对象的形式存在,就有相应的类。

所有的异常类,组成了一个“异常家族”。

请添加图片描述

Error错误

如果出现xxxError,如StackOverflowError,栈空间溢出,无法通过额外的代码解决,只能修改代码。

Exception异常

如果出现xxxException,如空指针异常,可以通过添加额外的代码解决。

  • 运行时异常RuntimeException

    如果一个异常类属于RuntimeException异常类的子类,称为运行时异常,是可以通过编译的,可以不用处理,运行时可能抛出异常。

    常见运行时异常说明出现的情景
    NullPointerException空指针异常用空对象null调用属性或方法
    ArrayIndexOutOfBoundsException数组下标越界异常使用数组时,下标超出范围
    NumberFormatException数字格式异常使用包装类调用parse方法做转换时,无法将参数转换。
    InputMismatchException输入不匹配异常使用Scanner接收控制台输入时,如果不满足接收的类型。
    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使用时注意

  • try、catch、finally都不能单独使用,try需要配合catch或finally或catch和finally一起使用
  • 执行try中的内容是,当某行代码抛出异常,不再执行try中该行代码后续内容
  • 无论try中的代码是否会抛出异常,finally中的代码一定对执行
  • 如果代码抛出多个异常,可以使用多个catch进行捕获,但需要将异常子类放在最前,异常父类放在最后,否则父类异常已经捕获子类的异常,从而导致子类异常无需捕获
  • 在try中定义的内容,无法在try的大括号外使用
  • try中如果有return,不影响finally的执行,finally优先于return执行

异常相关面试题

  • 以下代码输出的结果是
package com.hqyj.exceptionTest;

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()方法位于try结构中,抛出的异常被catch捕获,输出Exception
        //3.执行main方法里的finally代码块,输出Finished
        
        //finally
        //Exception
        //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 InterruptedException {
            Thread.sleep(3000);
    }
    
  • throw用于手动抛出异常对象。使用时,写在方法体中,常用于满足某种情况时,强制中断程序

    用法:throw异常对象;

    public void fun2(){
            for (int i = 0; i < 10; i++) {
                System.out.println(i);
                if (i==5){
                    //创建一个空指针异常对象
                    NullPointerException nullPointerException = new NullPointerException();
                    //throw 异常对象,表示手动抛出一个异常对象
                    throw nullPointerException;
                    
                    //手动抛异常
                    //throw new NullPointerException();
                }
            }
        }
    

自定义异常

(例如输密码,只能输错误三次,可以定义一个异常)

如果需要在某种情况下中断程序,可以自定义一个异常类,再通过throw关键字手动抛出。

自定义异常步骤

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

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

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

public class PasswordErrorException extends RuntimeException{


    /*
     * 可选操作
     * 定义不带参数的构造方法,调用父类中不带参数的构造方法
     * */
    public PasswordErrorException(){
        super();
    }

    /*
    * 可选操作
    * 定义带参数的构造方法,调用父类中带参数的构造方法
    * 参数为异常信息
    * */
    public PasswordErrorException(String msg){
        super(msg);
    }
}

package com.hqyj.MyException;

import java.util.Scanner;

public class ATM {

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        String pwd="123qwe";

        //输入错误次数
        int count = 0;
        while (true){
            //如果错误3次
            if (count==3){
                //手动抛出异常
                /*PasswordErrorException exception = new PasswordErrorException("输入密码次数过多,程序已终止");
                throw exception;*/
                throw new PasswordErrorException("输入密码次数过多,程序已终止");
            }
            //输入判断
            System.out.println("请输入密码");
            String input = sc.next();
            if (pwd.equals(input)){
                System.out.println("登录成功");
                break;
            }else {
                System.out.println("输入错误");
                count++;
            }
        }
    }
}

作业

  • 使用面向对象思想,模拟用户使用ATM,如果密码输出次数过多或存钱取钱时输入有误时,抛出自定义异常。

    • 用户类:账号、密码、余额

    • ATM类:登录、取钱、存钱、查看信息方法

    • 自定义异常类

数组和集合

数组的特点

[]

  • 数组中保存的元素都是有序的,可以通过下标快速访问
  • 数组中保存的数据都是同一种类型
  • 数组的长度在定义后,无法改变
  • 数组无法获取其中保存的元素实际数量

集合的特点

  • 能保存一组数据,可以有序可以无序
  • 集合的容量可变
  • 集合中可以保存不同类型的数据
  • 可以获取集合中保存的元素实际数量

集合框架(集合家族)

Connection还有父接口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)boolean溢出集合中的指定元素
toArray()Object[]将集合转换为数组
iterator()iterator获取集合的迭代器对象,用于遍历集合

List接口(有序可重复)

有序集合,元素可以重复,允许保存null,可以通过索引获取对应位置上的元素。

在该接口继承Collection接口的同时,又拓展了一些操作元素的方法,如添加到指定索引、根据索引删除、获取指定索引的元素、截取子集合的方法等

常用方法返回值作用
get(int index)Object根据指定索引获取对应的元素
set(int index,Object)Object使用obj替换index上的元素,返回被替换的元素
add(int index,Object)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的常用方法就是Collecion接口和List接口中的方法对应的实现方法

LinkedList实现类

  • 采用双向链表实现的集合
  • 集合中保存的每个元素也称为节点,除首尾节点外,其余节点都保存了自己的信息外,还保存了其前一个和后一个节点的地址
  • 如果在双向链表的数据结构中插入和删除操作节点时,不会影响其他节点的位置,如添加新节点时,只需要重新定义新节点的前后节点位置即可
  • 如果要查询某个节点时,需要从头节点或尾结点开始一步步得到目标节点的位置
  • 双向链表在中间插入和删除的效率高,随即读取的效率低

请添加图片描述

构造方法
常用构造方法说明
LinkedList()创建一个空链表
常用方法

由于LinkedList既实现了List接口,又实现了Deque接口,所以还有eque接口中的一些方法。

实现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采用双向链表实现,插入删除时不影响其他元素,效率高,随机读取效率低,适合用于频繁更新集合

Set接口(无序不重复)

无序集合,元素不可以重复,允许保存null,没有索引。

Set接口中没有自己定义的方法,都是继承与Collection里面的方法

哈希表 hash table

哈希表,也称为散列表,是一种数据结构,能更快地访问数据。

要保存的数据称为原始值,这个原始值通过一个函数得到一个新的数据,这个函数称为哈希函数,这个新数据称为哈希码,哈希码和原始值之间有一个映射关系,这个关系称为哈希映射,可以构造一张映射表,这个表称为哈希表。在哈希表中,可以通过哈希码快速地访问对应的原始值。

请添加图片描述

假设原本的数据为左侧的数组。

如果要查询10,需要遍历数组,效率不高。

通过一个特定的函数“原始值%5”,得到一组新数据,让新数据重新对应元素,保存到"新数组"中。

这是如果要查询10,由于哈希函数是通过%5得到了0,索引直接查询哈希表中0对应的元素即可。

整个过程中,这个函数称为哈希函数,得到的新数据称为哈希码,新数组称为哈希表,对应关系称为哈希映射。

这个哈希函数,有一定的几率让多个原始值得到相同的哈希码,这种情况称为哈希冲突(哈希码一致,实际值不同。

为了解决哈希冲突,可以使用“拉链法”,将2这个哈希码所在的位置像链表一样进行延伸。

哈希码的特点

  • 如果两个对象的hashCode不同,这两个对象一定不同
  • 如果两个对象的hashCode相同,这两个对象不一定相同
    • hashCode相同,对象不同,这种现象称为哈希冲突
    • 通话”和“重地”这两个字符串的hashCode相同,但是两个不同的对象

HashSet实现类

  • 采用哈希表实现
  • 元素不能重复,无序保存,允许保存一个null
  • 本质是一个HashMap对象
  • 使用hashSet集合时,一般要重写hashCode方法
构造方法
常用构造方法说明
HashSet()创建一个空集合,实际是创建一个HashMap对象。
常用方法

HashSet中没有属于自定义的方法,都是重写了父接口Set和Collection中的方法,这里参考Collection中的方法即可。

没有与索引相关的方法

HashSet添加数据的原理

如果两个元素的hashCode相同且equals结果为true,视为同一个对象,不能添加。

每次向集合中添加元素时,先判断该元素的hashCode是否存在

  • 如果不存在,视为不同对象,直接添加
  • 如果存在,再判断equals方法的结果
    • 如果false,视为不同对象,可以添加
    • 如果true,视为同一对象,不能添加

由此可见,不能添加的条件是两个对象的hashCode相同且equals的结果为true。

为什么不只用equals呢?

答:如果每次只判断equals,由于equals方法通常重写时会判断很多属性,效率不高。如果每次只判断hashCode的话,有可能会有哈希冲突。所以先判断hashCode,再判断equals,既能保证效率,又能保证不添加重复元素。

equals和hashCode的关系
  • 如果两个对象的equals方法结果为true,再没有重写equals方法的前提下,hashCode相同吗

    • 如果没有重写equals,默认是Object中使用“==”判断,如果结果为true,说明是同一个对象,hashCode一定相同
  • 如果两个对象的hashCode不同,在没有equals方法的前提下,equals方法的结果为?

    • hashCode不同,说明不是同一个对象,没有重写equals,说明使用Object中equals的“==”判断,结果为false
  • 如果两个对象的hashCode相同,equals方法的比较结果为?

    • 可能为true,也可能为false
    String str1 = "hello";
    String str2 = "hello";
    //以上两个字符串使用同一个地址,hashCode相同,equals方法为true
    
    String str3="通话";
    String str3 = "重地";
    //以上两个字符串时不同地址,但hashCode相同,因为哈希冲突,equals方法为false
    
HashSet的应用

如果想要保存的对象保证不重复,且无关顺序,可以使用HashSet,如学生管理

Goods类

package com.hqyj.hashSetTest;

import java.util.Objects;

/*
* 定义商品类
* 品牌、名称、价格
* */
public class Goods {
    private String brand;
    private String name;
    private int price;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Goods goods = (Goods) o;
        return price == goods.price && Objects.equals(brand, goods.brand) && Objects.equals(name, goods.name);
    }

    @Override
    public String toString() {
        return "Goods{" +
                "brand='" + brand + '\'' +
                ", name='" + name + '\'' +
                ", price=" + price +
                '}';
    }

    /*
    * 根据所有属性生成哈希码
    * 如果两个对象的所有属性都一致,生成的哈希码就一致
    * */
    @Override
    public int hashCode() {
        return Objects.hash(brand, name, price);
    }

    public String getBrand() {
        return brand;
    }

    public void setBrand(String brand) {
        this.brand = brand;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getPrice() {
        return price;
    }

    public void setPrice(int price) {
        this.price = price;
    }

    public Goods(String brand, String name, int price) {
        this.brand = brand;
        this.name = name;
        this.price = price;
    }
}

Main类

package com.hqyj.hashSetTest;

import java.util.HashSet;
import java.util.Iterator;

public class Main {
    public static void main(String[] args) {
        //创建一个HashSet集合
        HashSet<Goods> hs = new HashSet<>();
        //创建几个Goods对象
        //g1、g2、g3的属性不同,生成的哈希码不同,都能添加
        Goods g1 = new Goods("康师傅", "冰红茶", 3);
        Goods g2 = new Goods("康师傅", "红烧牛肉面", 5);
        Goods g3 = new Goods("农夫山泉", "矿物质水", 2);
        //g3与g4的属性相同,生成的哈希码相同,继续判断equals
        Goods g4 = new Goods("农夫山泉", "矿物质水", 2);

        //第一次添加,一定可以添加
        hs.add(g1);
        //第二次添加,对象的属性不同,hashCode不同,可以添加
        hs.add(g2);
        //第三次添加,对象的属性不同,hashCode不同,可以添加
        hs.add(g3);
        //第四次添加,对象的相同,hashCode相同,equqls也为true,视为已存在,不可以添加
        hs.add(g4);

        /*可以使用for循环*/
        for (Goods g : hs) {
            System.out.println(g);
        }
        /*HashSet没有可以通过索引获取对象的方法,所以无法使用普通for循环*/
        /*for (int i = 0; i < hs.size(); i++) {
            System.out.println(hs);
        }*/
        /*迭代器也可以*/
        Iterator<Goods> it = hs.iterator();
        while (it.hasNext()){
            Goods g = it.next();
            System.out.println(g);
        }
    }
}

TreeSet实现类

  • 特殊的Set实现类,数据可以有序保存,可以重复,不能添加null
  • 采用红黑树(自平衡二叉树)实现的集合
    • 二叉树表示某个节点最多有两个子节点,
    • 某个节点右侧节点值都大于左侧节点值
    • 红黑树会经过不停的“变色”和“旋转”达到二叉树的平衡
  • 只能添加同一种类型的对象,且该类实现了Comparable接口
    • 实现Comparable接口后必须要重写compareTo()方法
    • 每次调用添加add()方法时,就会自动调用参数的compareTo()方法
  • compareTo()方法的返回值决定了能否添加新元素和新元素的位置
    • 如果返回0,视为每次添加的是同一个元素,不能重复添加
    • 如果返回正数,将新元素添加到现有元素之后
    • 如果返回负数,将新元素添加到现有元素之前
  • 添加的元素可以自动排序
构造方法
常用构造方法说明
treeSet()创建一个空集合,实际是创建了一个TreeMao对象
常用方法

属于Set的实现类,所以能使用Collection和Set中的方法,除此之外,还有独有的方法

常用方法作业
first()得到集合中的第一个元素
last()得到集合中的最后一个元素
ceil(Object obj)得到比指定元素obj大的元素中的最小元素
floor(Object obj)得到比指定元素obj小的元素中的最大元素
TreeSet的应用

如果要保存的元素需要对其排序,使用该集合。

保存在其中的元素必须要实现Comparable接口,且冲洗诶compareTo()方法,自定义排序规则

Employee类

package com.hqyj.TreeSetTest;

public class Employee implements Comparable {
    private String no;
    private String name;
    private String dept;

    public Employee(String no, String name, String dept) {
        this.no = no;
        this.name = name;
        this.dept = dept;
    }

    @Override
    public String toString() {
        return "Employee{" +
                "no='" + no + '\'' +
                ", name='" + name + '\'' +
                ", dept='" + dept + '\'' +
                '}';
    }

    public String getNo() {
        return no;
    }

    public void setNo(String no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDept() {
        return dept;
    }

    public void setDept(String dept) {
        this.dept = dept;
    }

    @Override
    public int compareTo(Object o) {
        //o是已存在的集合中的对象
        Employee emp = (Employee) o;
        //此时this是当前添加的对象

        return emp.getNo().compareTo(this.getNo());

        //如果no是int类型
        //这里用已有员工号-当前员工号进行比较,可以换位置
        // return emp.getNo()-this.getNo();
    }
}

Main类

package com.hqyj.TreeSetTest;

import java.util.TreeSet;

public class Main {
    public static void main(String[] args) {

        TreeSet<Employee> emps = new TreeSet<>();

        Employee e1 = new Employee("dff001", "丈夫", "市场部");
        Employee e2 = new Employee("dff021","法海","市场部");
        Employee e3 = new Employee("dff027","老扎","市场部");
        Employee e4 = new Employee("dff027","手动","市场部");

        //第一个元素直接添加
        emps.add(e1);
        //第二个元素添加时,调用compareTo()方法,返回结果为正、负、0之一,正则添加在之后,负之前,0不添加
        emps.add(e2);
        emps.add(e3);
        emps.add(e4);


        for (Employee emp : emps) {
            System.out.println(emp);
        }
    }
}

Map接口

Map称为映射,数据以键值对的形式保存。

键称为Key,值称为Value,键不能重复,键允许出现一个null作为键,值无限制。

键和值都是引用类型。

如,yyds就是一个键,代表了一个含义:"永远单身/永远的神"即为值value。

常用方法作用
size()得到键值对的数量
clear()清空所有键值对
put(Object key,Object value)向集合中添加一组键值对
get(Object key)在集合中根据键得到对应的值
remove(Object key)/remove(Object key,Object value)根据键/键值对删除
keyset()得到键的集合
values()得到值的集合
containsKey(Object key)判断是否存在某个键
containsValue(Object value)判断是否存在某个值
entrySet()得到键值对集合

HashMap实现类(掌握)

请添加图片描述

  • JDK1.8后,HashMap采用“数组+链表+红黑树”实现
    • 当没有哈希冲突时,元素保存到数组里
    • 如果出现哈希冲突,在对应的位置上创建链表,元素保存到链表中
    • 如果链表的长度大于8,将链表转换为红黑树
  • 数据采用键值对key-value的形式保存,键不能重复,能用null

构造方法

常用构造方法说明
HashMap()创建一个空的映射集合,默认大小为16,加载因子(load factor)为0.75

常用方法

常用方法参考Map中的方法

向HashMap集合中添加元素时,原理同HashSet

遍历集合中元素的方式

遍历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集合

//创建一个HashSet集合
HashSet<Goods> hs = new HashSet<>();
//g1、g2、g3的属性不同,生成的哈希码不同,都能添加
Goods g1 = new Goods("康师傅", "冰红茶", 3);
Goods g2 = new Goods("康师傅", "红烧牛肉面", 5);
Goods g3 = new Goods("农夫山泉", "矿物质水", 2);
//g3与g4的属性相同,生成的哈希码相同,继续判断equals
Goods g4 = new Goods("农夫山泉", "矿物质水", 2);
//第一次添加,一定可以添加
hs.add(g1);
//第二次添加,对象的属性不同,hashCode不同,可以添加
hs.add(g2);
//第三次添加,对象的属性不同,hashCode不同,可以添加
hs.add(g3);
//第四次添加,对象的相同,hashCode相同,equqls也为true,视为已存在,不可以添加
hs.add(g4);

方式一:增强for循环

for (Goods g : hs) {
            System.out.println(g);
        }

方式二:迭代器

Iterator<Goods> it = hs.iterator();
        while (it.hasNext()){
            Goods g = it.next();
            System.out.println(g);
        }

遍历map集合

 Map<Integer, User> mp = new HashMap<>();
User user1 = new User("1001", "yhj", "qwer1234");
User user2 = new User("1002", "yhj", "qwer1234");

mp.put(1001, user2);
mp.put(1002, user1);
mp.put(1003, user2);
System.out.println("编号\t\t用户信息");

for (Integer id : mp.keySet()) {
    System.out.println(id + "\t"+mp.get(id));
}

//得到当前hashmap对象中的所有键值对的集合
Set<Map.Entry<Integer, User>> entries = mp.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("adad");
//list.add(123);//无法添加

 List<Integer> list2 = new ArrayList();
list2.add(123);

Collections 结合工具类

  • Collection是集合的根接口,定义了集合操作元素的方法
  • 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)得到集合中的最大/最小值,集合中的元素必须

集合和数组之间的转换

  • 集合转换为数组:使用Collection接口中的toArray()方法

    Object[] obj = 集合对象.toAtrray();
    
    List<Integer> list = new ArrayList();
    list.add(123);
    list.add(53);
    list.add(13);
    
    Integer[] nums = (Integer[])list2.toArray();
    
  • 数组转换为集合

    //一个数组对象
    int[] nums = {11,2,6,58,16,3};
    //定义集合对象
    List list = new ArrayList();
    //遍历数组的同时,将元素添加到集合中
    for(int i : nums){
        list.add(i);
    }
    System.out.println(list);
    
  • 一组数据转换为集合:使用Arrays工具类中的asList(一组数据)方法

    //通常将数组中的数据直接作为参数
    List<Integer> integers = Arrays.asList(13,12,55);
    

作业

  • 使用List集合实现"新闻管理系统"

    • 添加新闻

    • 删除新闻

    • 修改新闻

    • 查看新闻

    新闻类{
        标题;
        内容;
        编辑;
        发布时间;
        //发布时间是根据创建新闻对象时自动赋值
    }
    新闻管理类{
        添加(新闻对象){}
        删除(编号){}
        查看所有(){
        //查看所有时只需显示编号、标题
        }
        查看详情(编号){
        //查看详情时显示信息所有信息
        }
        修改(编号,内容){}
    }
    
  • 模拟"斗地主"游戏发牌

    //运行程序后,打印3套手牌和1套底牌
    //如输出:
    玩家1:♥AK25 大王
    玩家2:♥5A3Q
    玩家3:♥4JA7
    底牌:♥9KA
    

    文件类File

Java中的File类,表示本地硬盘中的文件(文件和目录)的一个类。

通过这个类创建的对象,可以操作对应的文件。

构造方法

常用构造方法说明
File(String pathName)根据文件的完整路径创建File对象,如new File(“C:\JavaLearn\xxx.png”)
File(String parent,String child)根据文件的父目录路径和自身路径创建File对象,如:new File(“C:\JavaLearn”,“xxx.png”)
File(File parent,String child)根据文件的父目录对应的File对象和自身路径创建File对象
 //如想要表示 "D:\aaa老师每天结束发的文件\JavaAdv02\JavaAdv02.pdf"的目录文件

// File(String pathName)
File file1 = new File("D:\\aaa老师每天结束发的文件\\JavaAdv02\\JavaAdv02.pdf");

//File(String parent,String child)
File file2 = new File("D:\\aaa老师每天结束发的文件\\JavaAdv02", "JavaAdv02.pdf");

//Fiel(File parent,String child)
File parent = new File("D:\\aaa老师每天结束发的文件\\JavaAdv02");
File file3 = new File(parent, "JavaAdv02.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;
public class Test2 {
public static void main(String[] args) {
    //兔子问题
    //有一对兔子在第三个月开始,每个月都能生一对小兔子
    //如果所有兔子不死亡,且每次生下的都是一雌一雄,问10个月后共有多少对兔子
    //1月 2月 3月 4月 5月 6月 7月 8月 9月 10月
    //1 1 2 3 5 8 13 21 34 55
    //斐波那契数列
    //f(n)=f(n-1)+f(n-2) n>2
	Test2 t = new Test2();
	System.out.println(t.f(20));
}
    /*
    * 递归方法
    * */
    public int f(int n) {
        if (n > 2) {
            return f(n - 1) + f(n - 2);
        }
        return 1;
        }
    }

递归遍历文件夹

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("E:\\adobe");
    Test3 t = new Test3();
    t.fun(source);
}
/*
* 递归遍历文件夹
* */
public void fun(File source) {
    //输出某个目录中超过3个月未使用且大于500MB的文件
    /*
    long start = source.lastModified();
    long end = System.currentTimeMillis();
    if ((end - start) / 1000 / 3600 / 24 > 90 && source.length() / 1024 / 1024 > 500) {
    	System.out.println(source.getName() + "\t" + new Date(source.lastModified()) + "\t" + source.length() / 1024 / 1024);
    }*/
    //判断是否为目录
    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

    • 读写非文本类型文件,如图片、视频等。
  • 字符流:Writer、Reader

    • 读写纯文本类型文件,如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(Stringpathname)根据文件名创建输出流对象,写入时覆盖原内容
FileOutputStream(Stringpathname,boolean append)根据文件名创建输出流对象,第二个参数为true,写入时追加在原内容之后
FileOutputStream(File file)根据文件对象创建输出流对象,写入时覆盖原内容
FileOutputStream(File file,booleanappend)根据文件对象创建输出流对象,第二个参数为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];
//第一次读取4个字节,即aaab,count为4
int count=fis.read(bytes);
//写入数组中的全部内容
fos.write(bytes);
//第二次读取4个字节,即bbcc,count为4
count=fis.read(bytes);
//写入数组中的全部内容
fos.write(bytes);
//第三次读取1个字节c,覆盖数组中的第一个元素,即数组现在为cbcc,count为1
count=fis.read(bytes);
//写入数组中的全部内容
fos.write(bytes);//最终会写入aaabbbcccbcc
fos.write(bytes,0,count);//这样最后一次只会写入实际读取到的c
fos.close();
fis.close();

使用FileInputStream和FileOutputStream实现单文件的复制

package com.hqyj.IOTest;
import java.io.*;
public class CopyFile {
public static void main(String[] args) throws IOException {
        //定义原文件和目标文件
        File source = new File("F:\\221001\\录屏		\\FileInputStream和FileOutputStream.mp4");
        
        File target = new File("F:\\221001\\copy.mp4");
        //定义文件字节输入流,用于读取原文件
        FileInputStream fis = new FileInputStream(source);
        //定义文件字节输出流,用于写入文件
        FileOutputStream fos = new FileOutputStream(target);
        /*
        //调用无参的read()方法,表示读取一个字节,返回读取到的字节
        int read = fis.read();
        //如果能读取到内容
        while (read > -1) {
            //将读取到的内容写入到文件中
            fos.write(read);
            //继续读取
            文件夹的复制
            read = fis.read();
        }
        */
        //定义一个字节数组,大小为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("复制成功");
    	}
    }
}

文件夹的复制

package com.hqyj.IOTest;
import java.io.*;
public class CopyDirectory {
	public static void main(String[] args) {
		/*File source = new File("F:\\221001\\录屏\\流的基本概念.mp4");
		File target = new File("F:\\221001\\copy.mp4");
		copyFile(source, target);*/
        File source = new File("F:\\221001\\笔记");
        File target = new File("F:\\221001\\笔记副本");
        /*
        * source F:\221001\笔记
        * target F:\221001\笔记副本
        * 1.调用copyDir方法,判断发现source是一个文件夹,创建目标文件夹
        target:“F:\221001\笔记副本”
        * 2.遍历source,如其中有xxx.md文件,即child
        * 此时的source是F:\221001\笔记\xxx.md,即child
        * 此时的target是F:\221001\笔记副本\xxx.md,用File(File parent,String child)
        构造方法表示这个目标文件
        * 所以创建File newTarget = new File(target,child.getName())
        *
        * */
		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);
			}
		}
	}
}

断点调试执行细节

请添加图片描述

遍历输出

//创建要读取的硬盘的原文件
File file = new File("D:\\Java_Study\\JavaAdv\\JavaAdv06\\hh.txt");
//创建文件字节输入流对象,按字节读取硬盘中的文件
FileInputStream fis = new FileInputStream(file);
// read()方法返回值为int,如果-1,则表示没有内容可读了
// System.out.println(fis.read());
/*int i = fis.read();
        while (i!=-1){
            System.out.println(i);
            i=fis.read();
        }*/
/*int i;
        while ((i= fis.read())!=-1){
            System.out.println(i);
        }*/
//根据文件字节大小读取,内存大的文件不用这种方法
for (long i = 0; i < file.length(); i++) {
    System.out.println(fis.read());
}

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()关闭流对象

读取文本练习

package com.hqyj.ReaderAndWriter;

import java.io.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Random;

public class Test2 {
    public static void main(String[] args) throws IOException {

        // BufferedReader;
        // BufferedWriter;
        /*
        File file = new File("D:\\aaa老师每天结束发的文件\\Java基础回顾.md");
        Reader fr = new FileReader(file);
        BufferedReader br = new BufferedReader(fr);
        */
        //创建带有缓冲区的字符输入流对象
        BufferedReader br = new BufferedReader(new FileReader("D:\\aaa老师每天结束发的文件\\221001.txt"));


        //循环判断是否还有字符
        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()关闭流对象

写入文本练习

package com.hqyj.ReaderAndWriter;
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()关闭流对象

ObjectIutputStream对象字节输入流(反序列化)(掌握)

反序列化:将文件转换为对象的过程

构造方法

常用构造方法说明
ObjectIutputStream(IutputStream Is)创建一个对象字节输入流对象,参数为一个字节输入流对象,由于InputStream是抽象类,所以使用其子类,如FileInputStream对象,在其中定义要读取的文件

常用方法

常用方法作用
readObject()读取序列化后的文件,返回类型为Object
close()关闭流对象

序列化和反序列化案例

Person类,实现Serializable接口

package com.hqyj.ObjectStream;
import java.io.Serializable;
/*
* 如果希望该类的对象能序列化,写入对象到本地,必须要实现Serializable接口
* Serializable接口中没有任何方法,是一个标记接口,表示该类的对象可以被序列化
* */
public class Person implements Serializable {
    private String name;
    private int age;
    private String sex;
    //省略getter/setter和toString()
}

Main类

package com.hqyj.ObjectStream;
import java.io.*;
import java.util.ArrayList;
public class Test1 {
	public static void main(String[] args) throws IOException,ClassNotFoundException {
        Person p1 = new Person("王海", 22, "男");
        Person p2 = new Person("赵敏", 24, "女");
        Person p3 = new Person("刘涛", 21, "女");
        ArrayList<Person> list = new ArrayList<>();
        list.add(p1);
        list.add(p2);
        list.add(p3);
        //创建OutStream的实现类,设置写入的文件路径
        OutputStream os = new FileOutputStream("F:\\221001\\person.p");
        //创建对象输出字节流,参数为OutStream类型
		ObjectOutputStream oos = new ObjectOutputStream(os);
		//调用writeObject(Object obj)方法,将对象写入到硬盘中(序列化)
		oos.writeObject(list);
		oos.close();
		//创建对象输入字节流,将上一步保存的文件进行反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("F:\\221001\\person.p"));
		//使用readObject()方法,将写入的文件进行读取(反序列化)
		ArrayList<Person> pList = (ArrayList<Person>) ois.readObject();
        for (Person person : pList) {
        	System.out.println(person);
        }
		ois.close();
	}
}

转换流

实际属于字符流,作用为将一个字节流对象转换为字符流对象

OutputStreamWriter

将字节输出流转换为字符输出流

InputStreamReader

将字节输入流转换为字符输入流

转换流的使用

如果只提供了一个字节流,但要向其中写入或读取字符时,就可以使用转换流将字节流转换为字符流。

使用字符流读写字符时比字节流更方便

//假如只提供一个字节输出流对象
FileOutputStream fos = new FileOutputStream("文件路径");
//fos.write(97);//这时如果写入数据,只能按字节写入,不方便

//使用转换流,将字节流对象fos转换为字符流对象
Writer writer = OutputStreamWriter(fos);
//将字符流对象writer包装成缓冲字符流对象
BufferedWriter bw = new BufferedWriter(writer);
bw.write("hello你好");
bw.newLine();

bw.close();
//只提供字节输入流对象
FileInputStream fis = new FileInputStream("221001.txt");

// fis.read()每次只能读取一个字节
//将字节流转换为字符流
Reader reader = new InputStreamReader(fis);
//创建缓冲字符流,将字符流包装为缓冲流
BufferedReader br = new BufferedReader(reader);
//整行读取
while (br.ready()) {
	System.out.println(br.readLine());
}
br.close()

网络编程

InetAdress类

表示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)之间发送文件

服务器端

package com.hqyj.uploadTest;
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();
    }
}

客户端

package com.hqyj.uploadTest;
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.currentThread();
    
  • 创建一个线程对象

    构造方法

    常用构造方法说明
    Thread()创建一个默认的线程对象
    Thread(String name)创建一个指定名称的线程对象
    Thread(Runnable target)将一个Runnable对象包装为线程对象
    Thread(Runnable target,String name)将一个Runnable对象包装为线程对象同时设置线程名

线程常用方法

方法作业
getid()获取线程id
getName()获取线程名,默认
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线程的子类

package com.hqyj.ThreadTest;
/*
* 实现多线程步骤
* 1.称为Thread子类
* 2.重写run()方法
* 3.创建当前类对象后,调用start()方法
* */
public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            try {
                Thread.sleep(1);//这里只能try-catch,因为父类没有异常
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(getName()+":"+i);
        }
    }
// 时间片
    public MyThread(String name) {
        super(name);
    }

    public MyThread() {
    }
}

Main类

package com.hqyj.ThreadTest;

public class Test2 extends Thread{

    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中是单继承,如果某个类已经继承了另一个类,这时不能通过继承Thread实现多线程,即方式一不行了。

  • 1.自定义一个类,实现Runnable接口
  • 2.重写run()方法,将多线程要执行的内容写在该方法中
  • 3.创建Runnable接口的实现类对象
  • 4.使用构造方法Thread(Runnable target)或Thread(Runnable target,String name)将上一步创建的Runnable实现类对象包装为Thread对象

自定义Runnable接口的实现类

package com.hqyj.ThreadTest;
/*
 * 实现多线程步骤
 * 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类

package com.hqyj.ThreadTest;

public class Test2 extends Thread{
    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接口的实现列

package com.hqyj.ThreadTest;
/*
* 实现多线程的方式三:
* 使用匿名内部类
*
* */
public class Test3 {
    public static void main(String[] args) {

        //使用Thread(Runnable target,String name)构造方法创建爱你线程对象
        //new Thread(new Runnable() { @Overridepublic 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),表示该线程为守护线程。

守护线程会随着其他非守护线程终止而终止。

package com.hqyj.DaemonTest;
/*
* 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 < 5; i++) {
            System.out.println("main方法中的循环执行中");
        }

    }

    @Override
    public void run() {
        while (true){
            System.out.println("守护线程执行中。。。。。。");
        }
    }
}
//发现守护线程可能会多执行

多线程访问同一资源

可能出现的问题

如银行存款100,同一时刻在手机和ATM一起取出,如果用多线程模拟,可能会出现两个线程都取出100的情况,要避免这种情况发生。

再比如3个售票窗口一共卖10张票:结果如下

请添加图片描述

本应该打印售出后再进行减,再打印剩余,由于线程A在打印“售出一张”后,还没来得及执行后续内容,其他线程就开始执行了。所以卖超出了2张票。

出现问题的原因

由于线程调用start()方法后,就进入就绪状态,如果获得了CPU时间片,就开始调用run()方法,调用run()方法后,就会再次进入就绪状态,不会等待run()方法执行完毕,所以在线程A执行run()方法的时候,线程B也开始执行了,这样就会出现数据共享的问题。

因为现在所有的线程都是异步(同时)执行。

如何解决

让线程同步(排队)执行即可。这样一来,某个线程执行run()方法时,让其他线程等待run()方法的内容执行完毕。

synchronize关键字

这个关键字可以修饰方法或代码块

修饰方法

写在方法的返回值之前,这时该方法就称为同步方法

public synchronized void fun(){
    //会排队执行的代码
}

修饰代码块

写在一个独立的{}前,这时该内容称为同步代码块。

/*代码块,在类创建对象时执行*/
synchronized(要同步的对象或this){
    //会排队执行的代码
}

原理

每个对象默认都有一把“锁”,当某个线程运行到被synchronized修饰的方法时,该对象就会拥有这把锁,在拥有锁的过程中,其他线程不能同时访问该方法,只有等待其结束后,才会释放这把锁。

使用synchronized修饰后的锁称为“悲观锁”。

方法被synchronized修饰后,称为同步方法,就会让原本多线程变成了单线程(异步变为同步)。

多线程相关面试题

  • 实现多线程的方式

    • 继承Thread类
    • 实现Runnable接口,包装为Thread对象
    • 匿名内部类
  • 为什么说StringBuilder或ArrayList是非线程安全的

    package com.hqyj.ThreadSafe;
    
    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线程

package com.hqyj.deadlock;

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线程

package com.hqyj.deadlock;

public class PersonB implements Runnable{

    //定义两个共享的成员变量。刀和叉
    private Object knife;
    private Object fork;

    public PersonB(Object knife, Object fork) {
        this.knife = knife;
        this.fork = fork;
    }

    /*
    * 让该线程执行run()方法时,先获取knife对象,等待3S后获取fork对象
    * */
    @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,可以吃饭了");

            }
        }
    }
}

Main类

package com.hqyj.deadlock;

public class Main {
    public static void main(String[] args) {
        Object knife = new Object();
        Object fork = new Object();

        new Thread(new PersonA(knife,fork),"张量").start();
        new Thread(new PersonB(knife,fork),"李白").start();
    }
}

死锁的解决方式

方式一:

让两个线程获取资源的顺序保持一致

如两个线程都先获取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,可以吃饭了");
            ![请添加图片描述](https://img-blog.csdnimg.cn/04794d40e31c48d99a613c5e0cece76a.png)

        }
    }
}

作业

class Student{
private String no;
private String name;
}
//请选择功能
//1.读取信息
	读取"信息"文件夹中的所有文件,获取其文件名,该文件名由"学号+姓名"组成。
//2.保存信息
	将获取到的文件名拆解为学号和姓名后,作为学生的属性,创建学生对象进行保存。
    将学生对象保存到集合中,将该集合序列化,保存为一个文件。
//3.加载信息
	反序列化之前保存的集合文件。读取该文件,输出所有的学生信息
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值