JAVA基础相关知识点(二)

File,IO流

file

  • File是java.iO.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹
  • 注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据,
  • File对象既可以代表文件、也可以代表文件夹。
  • File封装的对象仅仅是一个路径名,这个路径可以是存在的,也允许是不存在的。

常用方法

  • 判断文件类型获取文件信息
java 复制代码
package org.example.File和IO;
​
import java.io.File;
import java.text.SimpleDateFormat;
​
public class Test {
    public static void main(String[] args) {
        // 创建对象
        // 根据文件路径创建文件对象
        File file = new File("E:\studyproject\text1\demo02\src\aaa.txt");
        System.out.println(file.getName()); // 文件名
        System.out.println(file.length()); // 文件字节大小
        // 也可以指向文件夹
        File file1 = new File("E:\studyproject\text1\demo02\src");
        System.out.println(file1.length()); // 文件夹的大小
        // 注意:File对象可以指代一个不存在的文件路径
        File file2 = new File("E:\studyproject\text1\demo02\src\bbb.txt");
        System.out.println(file2.length()); // 不存在的文件默认0
        System.out.println(file2.exists()); // false 判断文件是否存在
        System.out.println("-----------");
        // 判断当前文件对象对应的文件路径是否存在 存在返回true
        System.out.println(file.exists()); // true
​
        // 判断当前文件对象指代的是否是文件,是文件返回true
        System.out.println(file.isFile()); // true
​
        // 判断当前文件对象指代的是否是文件夹,是文件夹返回true
        System.out.println(file.isDirectory()); // false
​
        // 获取文件的名称(包含后缀)
        System.out.println(file.getName());
​
        // 获取文件的大小,返回字节个数
        System.out.println(file.length());
​
        // 获取文件的最后修改时间(毫秒时间戳)
        long time = file.lastModified();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
        System.out.println(simpleDateFormat.format(time));
​
        // 获取创建文件对象时,使用的路径
        System.out.println(file.getPath());
​
        // 获取绝对路径
        System.out.println(file.getAbsolutePath());
    }
}
​
  • 创建文件删除文件
java 复制代码
package org.example.File和IO;
​
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
​
public class Test1 {
    public static void main(String[] args) throws IOException {
        // 创建文件,创建成功返回true(文件内容为空)
        File file = new File("E:\studyproject\java\aaa.txt");
        System.out.println(file.createNewFile()); // true,可以看到文件夹下创建了aaa.txt(存在就创建失败,不存在则创建成功)
​
        // 创建文件夹,注意只能创建一级文件夹
        File file1 = new File("E:\studyproject\java\bbb\ccc");
        System.out.println(file1.mkdir()); // fasle 因为创建了两层bbb和ccc文件夹
​
        // 创建文件夹,注意可以创建多级文件夹
        File file2 = new File("E:\studyproject\java\bbb\ccc");
        System.out.println(file1.mkdirs()); // true
​
        // 删除文件或者空文件,注意:不能删除非空文件夹(删除文件不进入回收站)
        File file3 = new File("E:\studyproject\java\aaa.txt");
        System.out.println(file3.delete()); // true
    }
}
  • 遍历文件夹
arduino 复制代码
package org.example.File和IO;
​
import java.io.File;
import java.io.IOException;
​
public class Test2 {
    public static void main(String[] args) throws IOException {
        // 获取当前目录下所有的1一级文件名称"到一个字符电数组中去返回。
        File file = new File("E:\studyproject\java");
        String[] list = file.list();
        for (String s : list) {
            System.out.println(s); // 目录名
        }
​
        // (重点)获取当前目录下所有的"一级文件对象"到一个文件对象数组中去返回(重点)
        // 没有权限的话会返回null
        // 主调是文件,或者路径不存在时返回null
        // 当为空文件夹时,返回空数组
        // 当为文件夹时,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在file数组中返回,包含隐藏文件
        // 当主调是一个有内容的文件夹时,且里面有隐藏文件时,将里面所有一级文件和文件夹的路径放在file数组中返回
        File[] files = file.listFiles();
        for (File file1 : files) {
            // file1.delete(); // 删除遍历到的每个文件 // 不建议用
            System.out.println(file1);  // 全部一级文件对象的路径
            System.out.println(file1.getAbsolutePath()); // 获取绝对路径
        }
​
    }
}

IO流

  • 用于读写数据的(可以读写文件,或网络中的数据...)
  • I指Input,称为输入流:负责把数据读到内存中去
  • O指Output,称为输出流:负责写数据出去

**分类

  • 字节流:适合操作所有类型的文件 比如:音频、视频、图片 文本文件的复制,转移等
  • 字符流: 只适合操作纯文本文件 比如:读写txt、java文件等
scss 复制代码
分为四大类
   字节输入流(InputStream,FilelnputStream),
   字节输出流(Outputstream,FileOutputSteam)
   字符输入流(Reader,FileReader),
   字符输出流(Writer,FileWriter)
   括号内是(抽象类,实现类)

字节流

字节输入流

字节输出流

java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test3 {
    public static void main(String[] args) throws IOException {
        // 字节流
        // 创建管道
        FileInputStream is = new FileInputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji.txt");
​
        // 文件字节输入流: 每次读取一个字节(下面文件有:ab)
        // System.out.println((char) is.read()); // a
        // System.out.println((char) is.read()); // b
        // System.out.println(is.read()); // -1  读取不到返回-1
​
        // 文件字节输入流: 每次读取多个字节
        // 1.读取性能差的案例
        // 读取汉字会乱码,无法避免的
        // int b;//用于记录已经读取的字节
        // while ((b=is.read())!=-1){
        //     System.out.println((char) b); // a b
        // }
        // // 流使用之后必须关闭,释放系统资源
        // is.close();
​
​
        // 2.文件字节输入流: 一次读取完全部字节
        // 不做演示代码了,直接使用循环改造
        // 性能直接得到提升,依旧读取汉字会乱码,无法避免的
        // byte[] bufer = new byte[3]; // 记住每次读取了多少个字节。
        // int len;
        // while ((len= is.read(bufer))!=-1){
        //     // 注意:读取多少,倒出多少。
        //     String rs = new String(bufer,0,len);
        //     System.out.println(rs);
        // }
​
​
​
        // 3.一次读取多行文件,更优雅写法(中文不会乱码)
        // 准备一个字节数组,大小与文件的大小正好一样大
        // File file = new File("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji.txt");
        // long s = file.length(); // 获取文件的字节
        // byte[] buff = new byte[(int) s]; // 不适合读取超大文件
        // int len1 = is.read(buff);
        // System.out.println(new String(buff));
        // System.out.println(s);
        // System.out.println(len1);
​
        // 4. 一次读取全部字节(中文不会有乱码,推荐使用)
        byte[] bytes = is.readAllBytes();
        System.out.println(new String(bytes));
​
        is.close();
​
​
        // 文件字节输出流: 写字节出去
        // 创建管道(创建一个电脑内不存在的文件,会自动生成)
        // 覆盖管道,每次写入数据都会覆盖里面原有的内容
        // OutputStream os = new FileOutputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
        // 追加管道,每次写入数据都会向里面追加内容(只需要将第二个参数改为true)
        OutputStream os = new FileOutputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt",true);
        os.write(97); // 代表a
        os.write('b'); // 代表b
        os.write('靳'); // 汉字会发生乱码
        os.write("我爱你".getBytes()); // 将字节数组作为参数传入进去,字符串.getBytes会转成byte字节数组
        os.write("从字节数组哪个地方开始读取到哪个地方结束".getBytes(),0,15); // 一个汉字占用三个字节
        // 写完数据后换行(如果是对文件进行追加内容的话,建议加个换行符)
        os.write("\r\n".getBytes());
        os.close(); // 执行完新创建的目标文件应该就有了:abs我爱你从字节数组
    }
}

文件复制(适用于大多数文件复制操作)

java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test5 {
    public static void main(String[] args) throws IOException {
        InputStream is = new FileInputStream("E:\studyproject\java\JAVA相关知识点.md"); // 源文件路径
        OutputStream os = new FileOutputStream("E:\studyproject\java\222.md"); // 复制到目标文件的路径
        // 读取源文件数据写入到目标文件
        // 创建一个字节数组,负责转移字节数据
        byte[] bytes = new byte[1024]; // 1kb
        // 读多少写多少
        int len; // 记住每次读取了多少字节
        while ((len=is.read(bytes))!=-1){
            os.write(bytes,0,len); // 向目标文件写入读取到的数据
        }
        // 关闭资源(先创建谁的流就最后再关,最后创建的先关)
        os.close();
        is.close();
        System.out.println("复制完成,此方法适用于大部分文件复制操作");
​
    }
}

字节流非常适合做一切文件的复制操作

  • 任何文件的底层都是字节,字节流做复制,是一字不漏的转移完全部字节,只要复制后的文件格式一致就没问题!

资源释放的方式

  • 上面代码释放流会有问题,例如读取写出流过程中出异常了,则程序不会执行到下面的释放流代码(之前做的方式都是直接抛出异常了)
  • 使用try-catch-finally和try-catch-resource解决
对上面的文件复制进行修改,确保不会有问题
  • try-catch-finally的方式
csharp 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test6 {
    public static void main(String[] args) {
        InputStream is = null;
        OutputStream os = null;
        try {
            is = new FileInputStream("E:\studyproject\java\JAVA相关知识点.md"); // 源文件路径
            os = new FileOutputStream("E:\studyproject\java\222.md"); // 复制到目标文件的路径
            // 读取源文件数据写入到目标文件
            // 创建一个字节数组,负责转移字节数据
            byte[] bytes = new byte[1024]; // 1kb
            // 读多少写多少
            int len; // 记住每次读取了多少字节
            while ((len=is.read(bytes))!=-1){
                os.write(bytes,0,len); // 向目标文件写入读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            // 关闭资源(先创建谁的流就最后再关,最后创建的先关)
            try {
                if(os!=null) os.close();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            try {
                if(is!=null) is.close();
            } catch (IOException ex) {
                throw new RuntimeException(ex);
            }
            System.out.println("复制完成,此方法适用于大部分文件复制操作");
        }
​
    }
}
  • try-catch-resource的方式
java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test7 {
    public static void main(String[] args) {
        try (
                // 资源异常处理的资源类都放到这里
                // 使用资源异常处理不需要手动关闭释放资源,会自动释放,也就是说我们只需要关心代码逻辑即可
                InputStream is = new FileInputStream("E:\studyproject\java\JAVA相关知识点.md"); // 源文件路径
                OutputStream os = new FileOutputStream("E:\studyproject\java\222.md"); // 复制到目标文件的路径
                ){
            // 读取源文件数据写入到目标文件
            // 创建一个字节数组,负责转移字节数据
            byte[] bytes = new byte[1024]; // 1kb
            // 读多少写多少
            int len; // 记住每次读取了多少字节
            while ((len=is.read(bytes))!=-1){
                os.write(bytes,0,len); // 向目标文件写入读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字符流

字符输入流

arduino 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test8 {
    public static void main(String[] args) {
        // 字符输入流
        // 创建文件字符流道
        try (
                Reader fr = new FileReader("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
                ){
            // 第一种方式
            // int c; // 记住每次读取的字符编号
            // while ((c=fr.read())!=-1){
            //     System.out.print((char) c); // 这里打印的即使有中文也不会乱码,每次读取一个字符,性能较差
            // }
​
            // 第二种方式
            // 每次读取多个字符,性能较好
            char[] buffer = new char[3];
            int len; // 记录每次读取的字符数
            while ((len=fr.read(buffer))!=-1){
                System.out.print(new String(buffer,0,len));
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
​
    }
}

字符输出流

java 复制代码
package org.example.File和IO;
​
import java.io.FileReader;
import java.io.FileWriter;
import java.io.Reader;
import java.io.Writer;
​
public class Test9 {
    public static void main(String[] args) {
        // 字符输出流
        // 创建文件字符流道
        try (
                // 会覆盖原有数据
                // Writer fr = new FileWriter("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
                // 追加内容,而不是覆盖内容
                Writer fr = new FileWriter("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt",true);
                ){
            // 1. 写一个字符出去
            fr.write('a'); // a
            fr.write(97); // a
            // 2. 写一个汉字
            fr.write('靳');
            // 3. 写一个字符串
            fr.write("萧寂173");
            // 4. 写字符串的一部分出去,从n-m
            fr.write("我爱你",0,2); // 我爱
            // 5.写一个字符数组出去
            char[] buffer = {'萧','寂','1','7','3'};
            fr.write(buffer); // 萧寂173
            // 6. 写一个字符数组的一部分出去
            fr.write(buffer,0,2); // 萧寂
            // 7.写一个换行符
            fr.write("\r\n");
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
​
    }
}

因为这里使用了资源异常处理了,会自动回收流,如果使用try-catch-finally或者其它代码内使用一定要close()关闭流或者调用flush()这个进行刷新流,否则读取或者写出不生效,切记

缓冲流

  • 字节缓冲输入流
  • 字节缓冲输出流
  • 字符缓冲输入流
  • 字符缓冲输出流

对原始流进行包装,以提高原始流读写数据的性能

原理:字节缓冲输入流自带了8KB缓冲池;字节缓冲输出流也自带了8KB缓冲池

字节缓冲输入输出流

  • 字节缓冲输入流

还是对上面的文件复制进行改造

java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test10 {
    public static void main(String[] args) {
        try (
                InputStream is = new FileInputStream("E:\studyproject\java\JAVA相关知识点.md"); // 源文件路径
                // 创建字节缓冲输入流包装原始的输入流,参数二是自定义缓冲池大小,默认是有8kb
                InputStream bis = new BufferedInputStream(is,8192*2);
                OutputStream os = new FileOutputStream("E:\studyproject\java\222.md"); // 复制到目标文件的路径
                // 创建字节缓冲输出流包装原始的输出流,参数二是自定义缓冲池大小,默认是有8kb
                OutputStream bos = new BufferedOutputStream(os,8192*2);
        ) {
            // 读取源文件数据写入到目标文件
            // 创建一个字节数组,负责转移字节数据
            byte[] bytes = new byte[1024]; // 1kb
            // 读多少写多少
            int len; // 记住每次读取了多少字节
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len); // 向目标文件写入读取到的数据
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
​

字符缓冲输入输出流

java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test11 {
    public static void main(String[] args) {
        // 字符输入流
        // 创建文件字符流道
        try (
                Reader fr = new FileReader("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
                // 创建字符缓冲输入流包装原始的输入流,参数二是自定义缓冲池大小,默认是有8kb
                BufferedReader br = new BufferedReader(fr,8192*2);
        ){
            // 按字符读取
            // char[] buffer = new char[3];
            // int len; // 记录每次读取的字符数
            // while ((len=br.read(buffer))!=-1){
            //     System.out.print(new String(buffer,0,len));
            // }
​
            // 按行读取
            String line; // 记录每次读取的一行数据
            while ((line=br.readLine())!=null){
                System.out.println(line);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
​
    }
}
  • 字符缓冲输出流
java 复制代码
package org.example.File和IO;

import java.io.*;

public class Test12 {
    public static void main(String[] args) {
        // 字符输出流
        // 创建文件字符流道
        try (
                Writer fr = new FileWriter("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt",true);
                // 创建字符缓冲输出流包装原始的输入流,参数二是自定义缓冲池大小,默认是有8kb
                BufferedWriter bw = new BufferedWriter(fr,8192*2);
        ){

            bw.write('a');
            bw.write(97);
            // 新增换行符
            bw.newLine();
            bw.write('靳');
            bw.write("萧寂173");

        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

转换流

引出问题-不同编码读取时会乱码,如下
java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test13 {
    public static void main(String[] args) {
        // 缓冲字符输入流
        // 创建文件字符流道
        try (
                // 代码编码为utf-8 文件编码是GBK 就乱码了
                // 必须代码编码和文件编码一致
                Reader fr = new FileReader("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
                // 创建字符输入流包装成缓冲字符输入流
                BufferedReader br = new BufferedReader(fr);
        ){
​
            String line;
            while ((line=br.readLine())!=null){
                System.out.println(line);
            }
​
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
字符输入转换流
  • 解决不同编码时,字符流读取文本内容乱码的问题。
  • 解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了
java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test14 {
    public static void main(String[] args) {
        // 字符输入转换流
        // 创建文件字符流道
        try (
                // 1. 得到文件原始字节流(GBK字节流形式)
                InputStream fr = new FileInputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
                // 2、把原始的字节输入流按照指定的字符渠编码转换成字符输入
                Reader isr = new InputStreamReader(fr, "GBK");
                // 3.把字符输入流包装成缓冲字符输入流
                BufferedReader br = new BufferedReader(isr);
        ) {
​
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line); // 不同编码不会乱码了
            }
​
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
字符转换输出流
  • 作用:可以控制写出去的字符使用什么字符集编码。
  • 解决思路:获取字节输出流,再按照指定的字符集编码将其转换成字符输出流,以后写出去的字符就会用该字符集编码了
java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test15 {
    public static void main(String[] args) {
        // 字符输出转换流
        // 创建文件字符流道
        try (
                // 指定写出去的字符编码
                // 1.创建一个文件字节输出流
                OutputStream fr = new FileOutputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji2.txt");
                // 2. 把原始的字节输出流按照指定的字符集编码转换成字符输出转换流
                Writer osw = new OutputStreamWriter(fr, "GBK");
                // 3. 将字符输出流包装成缓冲字符输出流
                BufferedWriter bw = new BufferedWriter(osw);
        ) {
​
            bw.write("我爱中国");
            bw.write("我是xiaoji");
​
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

打印流

  • 作用:打印流可以实现更方便、更高效的打印数据出去,能实现打印啥出去就是啥出去。
java 复制代码
package org.example.File和IO;
​
import java.io.*;
import java.nio.charset.Charset;
​
public class Test16 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 1.创建一个打印流管道(写入数据)
                // PrintStream ps = new PrintStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt");
                // 2. 指定字符集编码(写入数据)
                // PrintStream ps = new PrintStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt", Charset.forName("GBK"));
​
                // 3. 使用PrintWriter(和上面的PrintStream用法一致)
                // PrintWriter ps = new PrintWriter("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt", Charset.forName("GBK"))
                // 4. 由于高级流不支持追加,只支持覆盖,因此如果需要追加的话需要把参数转换为低级流再追加(参数二true代表追加数据)
                PrintWriter ps = new PrintWriter(new FileOutputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt",true))
        ) {
            // 注意:不想换行就不要加ln
            // 写入什么就是什么
            ps.println("97");
            ps.println("a");
            ps.println("我爱你");
            ps.println("爱着你");
            ps.println(true);
            ps.println(99.5);
​
            // 这个是写入字节
            ps.write(97); // a
​
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

案例,将系统的打印语句改到某一文件内

java 复制代码
package org.example.File和IO;
​
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
​
public class Test17 {
    public static void main(String[] args) {
        // 将系统的System.out.println();打印改到打印到某一文件内
        System.out.println("111你好");
        System.out.println("222你好");
        
        try (
                PrintStream ps = new PrintStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji.txt");
        ) {
            // 把系统默认的打印流对象改成自己设置的
            System.setOut(ps);
            System.out.println("333你好");
            System.out.println("444你好");
​
            // 可以发现系统只打印了111你好和222你好,而下面的打印语句都在指定的文件内
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

数据流

数据输出流

  • 允许把数据和其类型一并写出去。
java 复制代码
package org.example.File和IO;
​
import java.io.DataOutputStream;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.io.PrintWriter;
​
public class Test18 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 1.创建一个数据输出流包装低级的字节输出流
                DataOutputStream dos = new DataOutputStream(new FileOutputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt"));
        ) {
​
            dos.writeInt(97);
            dos.writeDouble(99.5);
            dos.writeBoolean(true);
            dos.writeUTF("萧寂真帅");
            // 注意:里面数据不是乱码,可以通过此数据流读取出来
​
            
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

数据输入流

  • 用于读取数据输出流写出去的数据。
java 复制代码
package org.example.File和IO;
​
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
​
public class Test19 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 1.创建一个数据输入流包装低级的字节输出流
                DataInputStream dos = new DataInputStream(new FileInputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt"));
        ) {
            // 下面读取的顺序要和写入时的数据一致,否则会报错
            int i = dos.readInt();
            System.out.println(i);
​
            double i1 = dos.readDouble();
            System.out.println(i1);
​
            boolean i2 = dos.readBoolean();
            System.out.println(i2);
​
            String i3 = dos.readUTF();
            System.out.println(i3);
​
​
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

序列化流

  • 对象序列化:把Java对象写入到文件中去
  • 对象反序列化:把文件里的ava对象读出来
  • 可以把Java对象进行序列化:把Java对象存入到文件中去。

对象字节输出流

typescript 复制代码
package org.example.File和IO;

import java.io.*;

// 注意:对象如果需要序列化,必须实现序列化接口。
class User implements Serializable {
    private String loginname;
    private String username;
    private int age;
    // transient代表不被序列化,此时就算密码有值也是为null
    private transient String password;


    public User() {
    }

    public User(String loginname, String username, int age, String password) {
        this.loginname = loginname;
        this.username = username;
        this.age = age;
        this.password = password;
    }

    /**
     * 获取
     *
     * @return loginname
     */
    public String getLoginname() {
        return loginname;
    }

    /**
     * 设置
     *
     * @param loginname
     */
    public void setLoginname(String loginname) {
        this.loginname = loginname;
    }

    /**
     * 获取
     *
     * @return username
     */
    public String getUsername() {
        return username;
    }

    /**
     * 设置
     *
     * @param username
     */
    public void setUsername(String username) {
        this.username = username;
    }

    /**
     * 获取
     *
     * @return age
     */
    public int getAge() {
        return age;
    }

    /**
     * 设置
     *
     * @param age
     */
    public void setAge(int age) {
        this.age = age;
    }

    /**
     * 获取
     *
     * @return password
     */
    public String getPassword() {
        return password;
    }

    /**
     * 设置
     *
     * @param password
     */
    public void setPassword(String password) {
        this.password = password;
    }

    public String toString() {
        return "User{loginname = " + loginname + ", username = " + username + ", age = " + age + ", password = " + password + "}";
    }
}

public class Test20 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 创建对象字节输出流包装原始的字节输出流
                ObjectOutputStream dos = new ObjectOutputStream(new FileOutputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt"));
        ) {
            // 创建对象
            User u = new User("admin", "张三", 32, "666");

            // 序列化对象到文件中(注意:对象如果需要序列化,必须实现序列化接口。)
            dos.writeObject(u);
            System.out.println("序列化对象成功");


        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

对象字节输入流

  • 可以把Java对象进行反序列化:把存储在文件中的Java对象读入到内存中来
java 复制代码
package org.example.File和IO;
​
import java.io.*;
​
public class Test21 {
    public static void main(String[] args) {
        // 打印流
        try (
                // 创建对象字节输入流包装原始的字节输入流
                ObjectInputStream dos = new ObjectInputStream(new FileInputStream("E:\studyproject\text1\demo02\src\main\java\org\example\File和IO\xiaoji1.txt"));
        ) {
            User u = (User) dos.readObject();
            System.out.println(u); // 写入的对象
​
​
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
​

一次性序列化多个对象的方案:将多个对象使用ArrayList进行存储,直接对集合序列化即可

ArrayList集合已经实现了序列化接口!

IO框架

推荐一个框架,名字为: commons-io

官方网站

参考视频

特殊文件-日志

  • 普通文件.txt
  • 属性文件.properties(本质也是文本文件,因此txt文本内部如果也是键值对形式的也可以使用properties相关的方法相关的操作)
  • XML文件.xml

properties文件概述

markdown 复制代码
- 特点
    1、都只能是键值对
	2、键不能重复
	3、文件后缀一般是.properties结尾的
    
Properties
    是一个Map集合(键值对集合),但是我们一般不会当集合使用。
    核心作用:Properties是用来代表属性文件的,通过Properties可以读写属性文件里的内容
     使用Properties读取属性文件里的键值对数据

读取Properties文件

java 复制代码
package org.example.特殊文件;
​
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.Properties;
import java.util.Set;
​
public class Test {
    public static void main(String[] args) throws Exception {
        // 1. 创建一个properties的对象出来(键值对集合,空容器)
        Properties properties = new Properties();
        System.out.println(properties);
​
        // 2. 开始加载属性文件中的键值对数据到properties对象中去
        properties.load(new FileReader("E:\studyproject\text1\demo02\src\aaa.properties"));
​
        System.out.println(properties); // // 拿到了全部Properties文件键值对转成的对象
​
        // 3.根据键取值
        System.out.println(properties.getProperty("username"));
        System.out.println(properties.getProperty("password"));
​
        // 4.遍历全部的键和值
        Set<String> keys = properties.stringPropertyNames();
        for (String key : keys) {
            String value = properties.getProperty(key);
            System.out.println(key+"---->"+value);
        }
        System.out.println("---------------");
        // 也可以通过foreach
        properties.forEach((k,v)->{
            System.out.println(k+"---->"+v);
        });
​
    }
}

将键值对数据存储到Properties文件中去

java 复制代码
package org.example.特殊文件;
​
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Properties;
import java.util.Set;
​
public class Test2 {
    public static void main(String[] args) throws Exception {
        // 1. 将键值对数据存入到Properties文件中去
        Properties properties = new Properties();
        properties.setProperty("张无忌","minmin");
        properties.setProperty("张翠山","susu");
        properties.setProperty("name","你好");
        properties.setProperty("age","18");
​
        // 2. 将Properties对象中的键值对数据存入到属性文件中去
        // 参数二是注释信息,随便填写就行(这个管道不需要关闭,内部会自动关闭)
        properties.store(new FileWriter("E:\studyproject\text1\demo02\src\aaa.properties"),"练习");
    }
}

多线程

多线程的创建方式一:继承Thread类

  • 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  • 创建MyThread类的对象
  • 调用线程对象的start()方法启动线程(启动后还是执行run方法的)

优缺点:

  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类,不利于功能的扩展。

多线程的注意事项

  • 1、启动线程必须是调用start方法,不是调用run方法。
  • 2、不要把主线程任务放在启动子线程之前。
java 复制代码
package org.example.多线程;

// 1.子类继承Thread线程类
class MyThread extends Thread{
    // 2. 必须重写run方法
    @Override
    public void run() {
        // 描述线程的执行任务
        for (int i = 0; i <= 100; i++) {
            System.out.println("子线程MyThread输出"+i);
        }
    }
}

class Test {
    public static void main(String[] args) {
        // 目标:掌握线程的创建方式一:继承Thread类

        // 3. 创建MyThread线程类的对象代表一个线程
        Thread t = new MyThread();
        t.start(); // 4. 启动线程(目前两个线程,main线程和t线程)(start实际调用了run方法)

        for (int i = 0; i <= 100; i++) {
            System.out.println("主线程main输出"+i);
        }
        
        // 上述代码可以发现每次执行代码主线程和子线程随机不定输出,即相互不影响各自执行各自的
    }
} 

多线程的创建方式二:实现Runnable接口

  • 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  • 创建MyRunnable任务对象
  • 把MyRunnable任务对象交给Thread处理 Thread类提供的构造器(Thread)封装Runnable对象成为线程对象
  • 调用线程对象的start()方法启动线程

方式二的优缺点

  • 优点:任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
  • 缺点:需要多一个Runnable对象。
java 复制代码
package org.example.多线程;
// 1、定义一个任务类,实现Runnable接口
class MyRunnable implements Runnable{
    // 2、重写runnable的run方法
​
    @Override
    public void run() {
        for (int i = 0; i < 55; i++) {
            System.out.println("子线程输出"+i);
        }
    }
}
​
public class Test2 {
    public static void main(String[] args) {
        // 3.创建任务对象
        Runnable target = new MyRunnable();
        // 4.把任务对象交给线程对象来处理
        new Thread(target).start();
​
        // 主线程执行
        for (int i = 0; i < 55; i++) {
            System.out.println("主线程main输出 ==="+i);
        }
    }
}

线程创建方式二的匿名内部类写法

  • 可以创建Runnable的匿名内部类对象。
  • 再交给Thread线程对象。
  • 再调用线程对象的start()启动线程。
csharp 复制代码
package org.example.多线程;
​
public class Test2_1 {
    public static void main(String[] args) {
        // 1. 直接创建Runnable接口的匿名内部类形式(任务对象)
        Runnable target = new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    System.out.println("子线程1输出"+i);
                }
            }
        };
​
        new Thread(target).start();
​
        // 1-1: 简化形式
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 1; i < 5; i++) {
                    System.out.println("子线程2输出"+i);
                }
            }
        }).start();
        
        // 1-2: 再次简化的形式
        new Thread(()->{
            for (int i = 1; i < 5; i++) {
                System.out.println("子线程3输出"+i);
            }
        }).start();
​
        // 主线程执行代码
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程main输出"+i);
        }
    }
} 

多线程的第三种创建方式Callable

前两种线程创建方式都存在的一个问题

  • 假如线程执行完毕后有一些数据需要返回,他们重写的run方法均不能直接返回结果

怎么解决这个问题?

  • JDK 5.0提供了Callable接口和FutureTask类来实现(多线程的第三种创建方式)
  • 这种方式最大的优点:可以返回线程执行完毕后的结果

多线程的第三种创建方式:利用Callable接口、FutureTask类来实现

  • 创建任务对象

    • 定义一个类实现Callable接口,重写call方法,封装要做的事情,和要返回的数据。
    • 把Callable类型的对象封装成FutureTask(线程任务对象)
  • 把线程任务对象交给Thread对象。

  • 调用Thread对象的start方法启动线程。

  • 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果。

java 复制代码
package org.example.多线程;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;

// 1、让这个类实现Callable接口
class MyCallable implements Callable<String> {
    private int n;

    public MyCallable(int n) {
        this.n = n;
    }

    // 2. 重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果,
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return "线程求出了1-" + n + "的和是: " + sum;
    }
}

class Test3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 3.创建一个Callable对象
        Callable<String> call = new MyCallable(5);
        // 4.把Callable的对象封装成一个FutureTask对象(任务对象)
        // 未来任务对象的作用?
        // 1、是一个任务对象,实现了Runnable对象
        // 2、可以在线程执行完毕之后,用未来任务对象调用get方法获取线程执行完毕后的结果。
        FutureTask<String> f1 = new FutureTask<>(call);
        // 5. 把任务对象交给一个Thread对象
        new Thread(f1).start();


        // 3.创建一个Callable对象
        Callable<String> call2 = new MyCallable(10);
        FutureTask<String> f2 = new FutureTask<>(call2);
        new Thread(f2).start();


        // 6. 获取线程执行完毕后返回的结果
        // 注意:如果执行到这儿,假如上面的线程还没有执行完毕这里的代码会暂停,等待上面线程执行完毕后才会获取结果。
        System.out.println(f2.get()); // 实际调用的就是cell方法
        System.out.println(f1.get()); // 实际调用的就是cell方法
    }
}

线程创建方式三的优缺点

  • 优点:线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强;可以在线程执行完毕后去获取线程执行的结果。
  • 缺点:编码复杂一点。

Thread提供的常用方法

scss 复制代码
package org.example.多线程;
class MyThread1 extends Thread{
    // 无参构造,方便使用默认名字(有的不想传参数)
    public MyThread1(){
    }
    // 有参构造器
    public MyThread1(String name){
        // 给线程设置名字(将名字传递给父类调用)
        super(name);
    }
    @Override
    public void run() {
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        for (int i = 0; i <= 5; i++) {
            System.out.println(m.getName()+"子线程输出"+i);
        }
    }
}
​
class Test4 {
    public static void main(String[] args) throws InterruptedException {
        // 目标:掌握sleep方法,join方法的作用。
        for (int i = 0; i < 5; i++) {
            System.out.println(i);
            // 到3时休眠五秒再继续执行
            if(i==3){
                Thread.sleep(5000);
            }
        }
​
        // join方法作用:让当前调用这个方法的线程先执行完。
        // 这段代码意思就是当thread1执行完毕后会去执行thread2,当thread2执行完毕后才会去执行thread3,相当于一个异步等待
        // 哪个线程带有join后面的代码都要等这个线程结束才执行
        Thread thread1 = new MyThread1("子线程1");
        thread1.start();
        thread1.join();
        Thread thread2 = new MyThread1("子线程2");
        thread2.start();
        thread2.join();
        Thread thread3 = new MyThread1("子线程3");
        thread3.start();
        thread3.join();
​
​
        Thread t1 = new MyThread1("子线程");
        // 给子线程设置名字(上面也是相同效果,使用了有参构造,需要在类上使用有参构造),必须设置在start之前,默认名为Thread-0
        // t1.setName("子线程1");
        t1.start();
        // 打印子线程对象的名字
        System.out.println(t1.getName()); // 子线程1
​
        Thread t2 = new MyThread1();
        t2.start();
        System.out.println(t2.getName()); // Thread-1
​
        // 主线程对象
        // 哪个线程执行它,它就会得到哪个线程对象。
        Thread m = Thread.currentThread();
        System.out.println(m); // Thread[main,5,main]
        // 主线程对象的名字
        System.out.println(m.getName()); // main
​
        for (int i = 0; i <= 5; i++) {
            System.out.println(m.getName()+"主线程输出"+i);
        }
    }
}

线程安全问题

  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。
  • 取钱的线程安全问题 场景:小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,如果小明和小红同时来取钱,并且2人各自都在取钱10万元,可能会出现什么问题呢?

线程安全问题出现的原因?

  • 存在多个线程在同时执行
  • 同时访问一个共享资源
  • 存在修改该共享资源

上面取钱案例的bug复现(小红小明都取100000,银行只有100000,导致之后结果为-100000)

分析:

  • 需要提供一个账户类,接着创建一个账户对象代表2个人的共享账户
  • 需要定义一个线程类(用于创建两个线程,分别代表小明和小红)。
  • 创建2个线程,传入同一个账户对象给2个线程处理。
  • 启动2个线程,同时去同一个账户对象中取钱10万
arduino 复制代码
package org.example.多线程;
​
// 账户类
class Account {
    private String CardId; // 卡号(这个案例没啥用)
    private double money; // 余额
​
    public Account(String cardId, double money) {
        CardId = cardId;
        this.money = money;
    }
​
    public Account() {
    }
​
    public void setCardId(String cardId) {
        CardId = cardId;
    }
​
    public void setMoney(double money) {
        this.money = money;
    }
​
    public String getCardId() {
        return CardId;
    }
​
    public double getMoney() {
        return money;
    }
​
    @Override
    public String toString() {
        return "Account{" +
                "CardId='" + CardId + ''' +
                ", money=" + money +
                '}';
    }
​
    // 小明和小红同时来取钱
    public void drawMoney(double money) {
        // money代表的是取钱的数量
        // 先搞明白谁先来取钱(取线程名字)
        String name = Thread.currentThread().getName();
        System.out.println(name); // 得到线程名称
        // 1. 判断余额是否足够
        if (this.money >= money) {
            System.out.println(name + "来取钱" + money + "成功!");
            this.money -= money;
            System.out.println(name + "取钱后余额剩余" + this.money);
        } else {
            System.out.println(name + "来取钱,余额不足");
        }
​
    }
}
​
// 线程类
class DrawThead extends Thread {
    private Account acc;
​
    // 有参构造器
    public DrawThead(Account acc, String name) {
        // 设置线程名字,用于保存记录是谁在取钱
        super(name);
        this.acc = acc;
    }
​
    @Override
    public void run() {
        // 取钱(小明和小红)
        acc.drawMoney(100000);
    }
}
​
class Test5 {
    public static void main(String[] args) {
        // 模拟取钱(模拟线程安全问题)
        // 1. 创建一个账户对象,代表两个人的共享账户.
        Account acc = new Account("TCBC-110", 100000);
​
        // 2. 创建两个线程, 分别代表小明和小红,再去同一个账户对象中取10000
        new DrawThead(acc, "小明").start(); // 小明
        new DrawThead(acc, "小红").start(); // 小红
    }
}

线程同步(解决线程安全问题)

认识线程同步

  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

线程同步的常见方案

  • 加锁:每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来。

方式一:同步代码块

  • 作用:把访问共享资源的核心代码给上锁,以此保证线程安全
javascript 复制代码
synchronized(同步锁){
    访问共享资源的核心代码
}
  • 原理:每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行。

同步锁的注意事项

  • 对于当前同时执行的线程来说,同步锁必须是同一把(同一个对象),否则会出bug。

把上面取钱的案例稍加改造,将下面这段代码替换到上面的取钱的案例里面即可,然后功能就正常了

arduino 复制代码
package org.example.多线程;
​
// 账户类
class Account {
    private String CardId; // 卡号(这个案例没啥用)
    private double money; // 余额
​
    public Account(String cardId, double money) {
        CardId = cardId;
        this.money = money;
    }
​
    public Account() {
    }
​
    public void setCardId(String cardId) {
        CardId = cardId;
    }
​
    public void setMoney(double money) {
        this.money = money;
    }
​
    public String getCardId() {
        return CardId;
    }
​
    public double getMoney() {
        return money;
    }
​
    @Override
    public String toString() {
        return "Account{" +
                "CardId='" + CardId + ''' +
                ", money=" + money +
                '}';
    }
​
    public static void test(){
        synchronized (Account.class){
            // 由于静态方法在计算机里面只有一份,因此官方建议静态方法使用类名.class作为锁对象
        }
    }
​
    // 小明和小红同时来取钱
    public void drawMoney(double money) {
        // money代表的是取钱的数量
        // 先搞明白谁先来取钱(取线程名字)
        String name = Thread.currentThread().getName();
        System.out.println(name); // 得到线程名称
        // 1. 判断余额是否足够
​
        // 加锁
        // "萧寂" 字符串在计算机里面只存一份,也就是相当于给锁对象,然后这个锁在计算机里面也只有一份,后面无论创建多少对象,只要是这个线程的都会是同一把锁,例如下面的小黑和小白就是新创建了的对象但是由于钱被小明和小红取完了,导致小黑和小白没钱取了,但是对象是新创建的,按照逻辑来讲,小黑和小白应该是针对新一轮的10万元去取钱,但是由于走的同一线程,同一把锁,导致数据错乱
        // 为了解决上面的问题,官方建议使用账户类对象作为锁对象,这样,小红和小明共用同一个账户,小黑和小白共用同一个账户
        synchronized (this) {
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "取钱后余额剩余" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
        }
​
    }
}
​
// 线程类
class DrawThead extends Thread {
    private Account acc;
​
    // 有参构造器
    public DrawThead(Account acc, String name) {
        // 设置线程名字,用于保存记录是谁在取钱
        super(name);
        this.acc = acc;
    }
​
    @Override
    public void run() {
        // 取钱(小明和小红)
        acc.drawMoney(50000);
    }
}
​
class Test5 {
    public static void main(String[] args) {
        // 模拟取钱(模拟线程安全问题)
        // 1. 创建一个账户对象,代表两个人的共享账户.
        Account acc = new Account("TCBC-110", 100000);
        // 2. 创建两个线程, 分别代表小明和小红,再去同一个账户对象中取10000
        new DrawThead(acc, "小明").start();// 小明
        new DrawThead(acc, "小红").start(); // 小红
​
​
        Account acc1 = new Account("TCBC-110", 100000);
        new DrawThead(acc1, "小黑").start();
        new DrawThead(acc1, "小白").start();
    }
}

锁对象随便选择一个唯一的对象好不好呢?

  • 不好,会影响其他无关线程的执行。

锁对象的使用规范

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象,
  • 对于静态方法建议使用字节码(类名.class)对象作为锁对象。

方式二:同步方法

  • 作用:把访问共享资源的核心方法给上锁,以此保证线程安全.
arduino 复制代码
修饰符 synchronized 返回值类型 方法名称(形参列表){
    操作共享资源的代码
}
  • 原理:每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行。

把上面的取钱的方法替换成下面这个,完美解决(即使和上面的小黑小白一样新创建账户类对象也是没问题的),只需要在方法前面加synchronized

csharp 复制代码
    // 小明和小红同时来取钱
    public synchronized void drawMoney(double money) {
        // money代表的是取钱的数量
        // 先搞明白谁先来取钱(取线程名字)
        String name = Thread.currentThread().getName();
        System.out.println(name); // 得到线程名称
        // 1. 判断余额是否足够
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "取钱后余额剩余" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
​
    }

同步方法底层原理

  • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码.
  • 如果方法是实例方法:同步方法默认用this作为的锁对象.
  • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象

同步代码块范围小点,性能稍微好点,同步方法范围大,性能稍微差点,但是这点性能计算机几乎忽略不计

可读性:同步方法更好。

方法三:Lock锁

  • Lock锁是IDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。
  • Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

将上面的账户类替换成下面这个

arduino 复制代码
// 账户类
class Account {
    private String CardId; // 卡号(这个案例没啥用)
    private double money; // 余额
    private final Lock lk = new ReentrantLock(); // 1.创建锁对象(每个账户都应该有自己的锁对象,并且不能被替换,因此使用final修饰)

    public Account(String cardId, double money) {
        CardId = cardId;
        this.money = money;
    }

    public Account() {
    }

    public void setCardId(String cardId) {
        CardId = cardId;
    }

    public void setMoney(double money) {
        this.money = money;
    }

    public String getCardId() {
        return CardId;
    }

    public double getMoney() {
        return money;
    }

    @Override
    public String toString() {
        return "Account{" +
                "CardId='" + CardId + ''' +
                ", money=" + money +
                '}';
    }

    // 小明和小红同时来取钱
    public void drawMoney(double money) {
        String name = Thread.currentThread().getName();

        // 2.加锁(程序运行到这里先加锁,执行完毕后再解锁,这里要考虑异常,因为如果加锁和解锁中间的程序出现了bug,就直接跳走了,不会执行到下面的解锁操作,因此需要使用try-catch-finally包裹)
        lk.lock();
        System.out.println(name); // 得到线程名称
            try {
            // 1. 判断余额是否足够
            if (this.money >= money) {
                System.out.println(name + "来取钱" + money + "成功!");
                this.money -= money;
                System.out.println(name + "取钱后余额剩余" + this.money);
            } else {
                System.out.println(name + "来取钱,余额不足");
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            // 3. 程序运行完解锁
            lk.unlock();
        }
    }
}

线程池

什么是线程池?

  • 线程池就是一个可以复用线程的技术。

不使用线程池的问题

  • 用户每发起一个请求,后台就需要创建一个新线程来处理,下次新任务来了肯定又要创建新线程处理的, 而创
  • 建新线程的开销是很大的,并且请求过多时,肯定会产生大量的线程出来,这样会严重影响系统的性能,

如何创建线程池

谁代表线程池?

  • JDK 5.0起提供了代表线程池的接口:ExecutorService。

如何得到线程池对象?

  • 方式一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。
  • 方式二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

方式一的使用

ThreadPoolExecutor构造器

java 复制代码
public ThreadPoolExecutor(int corePoolsize, int maximumPoolsize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable>workQueue, ThreadFactory threadFactory,RejectedExecutionHandler handler)
  • 参数一:corePoolSize:指定线程池的核心线程的数量,(正式工:3人)
  • 参数二:maximumPoolSize:指定线程池的最大线程数量。(最大员工数量:5人 临时工:2人)
  • 参数三:keepAliveTime:指定临时线程的存活时间。(临时工空闲多久被开除)
  • 参数四:unit:指定临时线程存活的时间单位(秒、分、时、天) (临时工工期)
  • 参数五:workQueue:指定线程池的任务队列。(客人排队的地方)
  • 参数六:threadFactory:指定线程池的线程工厂。(负责招聘员工的(HR))
  • 参数七:handler:指定线程池的任务拒绝策略(线程都在忙,任务队列也满了的时候,新任务来了该怎么处理) (忙不过来怎么办?)

创建线程池对象

java 复制代码
package org.example.多线程;

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class Test7 {
    public static void main(String[] args) {
        // 通过ThreadPoolExecutor创建一个线程池对象。
        // 参数一:核心线程数量(复用线程数量)
        // 参数二:最大线程数量(临时线程 = 最大线程 - 核心线程)
        // 参数三:临时线程存活时间
        // 参数四:临时线程存活时间的单位(秒,分,时,天) TimeUnit.SECONDS 是秒
        // 参数五:声明任务队列(为线程池缓存处理任务) ArrayBlockingQueue是基于数组实现的,也是常用的,参数是任务队列的大小,代表最多只能缓存几个任务
        // 参数六:线程工厂,是负责为线程池创建线程的 Executors.defaultThreadFactory() 是获取默认的线程池工厂
        // 参数七:任务拒绝策略(当核心线程在工作,临时线程也在工作,那么多余的任务拒绝的处理方式)  new ThreadPoolExecutor.AbortPolicy() 代表新任务来了之后直接抛出异常告诉新任务无法处理
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
    }
}

线程池的注意事项 1、临时线程什么时候创建?

  • 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

2、什么时候会开始拒绝新任务?

  • 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始拒绝任务。

3、任务拒绝策略的方案

scss 复制代码
new ThreadPoolExecutor.AbortPolicy();  丢弃任务并抛出RejectedExecutionException异常。是默认的策略
new ThreadPoolExecutor.DiscardPolicy();  丢弃任务,但是不抛出异常 这是不推荐的做法
new ThreadPoolExecutor.DiscardOldestPolicy();  抛弃队列中等待最久的任务 然后把当前任务加入队列中
new ThreadPoolExecutor.CallerRunsPolicy();  由主线程负责调用任务的run()方法从而绕过线程池直接执行

线程池处理Runnable任务

java 复制代码
package org.example.多线程;
​
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
​
class MyRunnables implements Runnable{
    @Override
    public void run() {
        // 任务是干啥的
        System.out.println(Thread.currentThread().getName()+"===>输出666"); // 获取当前线程名字,可以清晰看到哪一个线程在执行
        try {
            // Thread.sleep(1000); // 每个线程都工作一秒就开始下一轮任务处理
            Thread.sleep(Integer.MAX_VALUE); // 这样就可以把核心线程停留到最长时间,以便模拟当核心线程都在忙时临时线程会出来工作
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }
}
​
class Test7 {
    public static void main(String[] args) {
        // 通过ThreadPoolExecutor创建一个线程池对象。
        // 参数一:核心线程数量(复用线程数量)
        // 参数二:最大线程数量(临时线程 = 最大线程 - 核心线程) 临时线程执行条件(线程池溢出后执行,例如有8个任务,核心线程执行了3个,此时线程池还剩下5个,我们设置线程池最大数量为4,就相当于超出线程池了,这时,临时线程会处理多出部分的,例如当前代码临时线程为2,设置任务条数为10个,则核心线程处理3个,四个放在线程池,多余出来3个,但是临时线程由于只有两条则只能处理两个任务,那多出来的一个任务则会被参数七的任务拒绝策略处理( 可以在第七个参数设置拒绝处理策略 ))
        // 参数三:临时线程存活时间
        // 参数四:临时线程存活时间的单位(秒,分,时,天) TimeUnit.SECONDS 是秒
        // 参数五:声明任务队列(为线程池缓存处理任务) ArrayBlockingQueue是基于数组实现的,也是常用的,参数是任务队列的大小,代表最多只能缓存几个任务
        // 参数六:线程工厂,是负责为线程池创建线程的 Executors.defaultThreadFactory() 是获取默认的线程池工厂
        // 参数七:任务拒绝策略(当核心线程在工作,临时线程也在工作,那么多余的任务拒绝的处理方式)
​
        // 参数七的拒绝策略:
        // new ThreadPoolExecutor.AbortPolicy();  丢弃任务并抛出RejectedExecutionException异常。是默认的策略
        // new ThreadPoolExecutor.DiscardPolicy();  丢弃任务,但是不抛出异常 这是不推荐的做法
        // new ThreadPoolExecutor.DiscardOldestPolicy();  抛弃队列中等待最久的任务 然后把当前任务加入队列中
        // new ThreadPoolExecutor.CallerRunsPolicy();  由主线程负责调用任务的run()方法从而绕过线程池直接执行
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
​
        // 创建任务对象
        MyRunnables myRunnables = new MyRunnables();
        pool.execute(myRunnables); // 线程池会自动创建一个新线程,自动处理这个任务,自动执行的!
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        // 到了临时线程的创建时机
        pool.execute(myRunnables);
        pool.execute(myRunnables);
        pool.execute(myRunnables); // 这个超出的任务会被任务拒绝策略处理
        pool.execute(myRunnables); // 这个超出的任务会被任务拒绝策略处理
​
        // 当任务执行完毕后关闭线程池
        // pool.shutdown(); // 可以发现当以上6个任务都结束才关闭线程池
        // 无论任务是否执行完毕,立即关闭线程池
        // pool.shutdownNow(); // 会报错 "pool-1-thread-2" Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-3" java.lang.RuntimeException: java.lang.InterruptedException: sleep interrupted
    }
}

线程池处理Callable任务

csharp 复制代码
package org.example.多线程;

import java.util.concurrent.*;

// 1、让这个类实现Callable接口
class MyCallable1 implements Callable<String> {
    private int n;

    public MyCallable1(int n) {
        this.n = n;
    }

    // 2. 重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果,
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"线程求出了1-" + n + "的和是: " + sum;
    }
}
public class Test8 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 创建线程池对象
        ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 8, TimeUnit.SECONDS, new ArrayBlockingQueue<>(4), Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());
        // 使用线程处理Callable任务
        Future<String> f1 = pool.submit(new MyCallable1(100));
        Future<String> f2 = pool.submit(new MyCallable1(200));
        Future<String> f3 = pool.submit(new MyCallable1(300));
        Future<String> f4 = pool.submit(new MyCallable1(400));
        Future<String> f5 = pool.submit(new MyCallable1(500));

        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
        // 上面打印的都是123线程的,此时还未使用到临时线程,因为线程池未溢出
    }
}

工具类:Executors

注意:这些方法的底层,都是通过线程池的实现类ThreadPoolExecutor创建的线程池对象。

scss 复制代码
Executors.newFixedThreadPool(int nThreads)  创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
Executors.newSingleThreadExecutor()  创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
Executors.newCachedThreadPool()  线程数量随着任务增加而增加,如果线程任务执行完与且空闲了60s则会被回收掉。
Executors.newScheduledThreadPool(int corePoolSize)  创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
csharp 复制代码
package org.example.多线程;
​
import java.util.concurrent.*;
​
// 1、让这个类实现Callable接口
class MyCallable1 implements Callable<String> {
    private int n;
​
    public MyCallable1(int n) {
        this.n = n;
    }
​
    // 2. 重写call方法
    @Override
    public String call() throws Exception {
        // 描述线程的任务,返回线程执行返回后的结果,
        // 需求:求1-n的和返回。
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return Thread.currentThread().getName()+"线程求出了1-" + n + "的和是: " + sum;
    }
}
public class Test8 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // Executors.newFixedThreadPool(int nThreads)  创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
        // Executors.newSingleThreadExecutor()  创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
        // Executors.newCachedThreadPool()  线程数量随着任务增加而增加,如果线程任务执行完与且空闲了60s则会被回收掉。
        // Executors.newScheduledThreadPool(int corePoolSize)  创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。
​
        // 创建固定数量的线程池对象
        ExecutorService pool = Executors.newFixedThreadPool(3);
        
        // 核心线程数量应该配置多少???
        // 计算密集型的任务: 核心线程数量 = CPU的核数 + 1
        // IO密集型的任务: 核心线程数量 = CPU的核数 + 2
        
        // 使用线程处理Callable任务
        Future<String> f1 = pool.submit(new MyCallable1(100));
        Future<String> f2 = pool.submit(new MyCallable1(200));
        Future<String> f3 = pool.submit(new MyCallable1(300));
        Future<String> f4 = pool.submit(new MyCallable1(400));
        Future<String> f5 = pool.submit(new MyCallable1(500));
​
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
        System.out.println(f4.get());
        System.out.println(f5.get());
        // 上面打印的都是123线程的,此时还未使用到临时线程,因为线程池未溢出
    }
}

核心线程数量应该配置多少???

ruby 复制代码
计算密集型的任务: 核心线程数量 = CPU的核数 + 1
IO密集型的任务: 核心线程数量 = CPU的核数 + 2

CPU核数查看:
   windows电脑:Ctrl+Shift+Esc调出任务管理器,点击性能,点击CPU,界面有个逻辑处理器(也就是线程的数量),这就是核数

Executors使用可能存在的陷阱 大型并发系统环境中使用Executors如果不注意可能会出现系统风险

乐观锁悲观锁概念

悲观锁: 一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差! 乐观锁: 开始不上锁,认为是没有问题的,大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

带有线程安全问题的代码,后面的乐观悲观锁会在这个代码基础上改造

java 复制代码
package org.example.多线程;
​
class MyTest9 implements Runnable{
    private int count; // 记录浏览人次
    @Override
    public void run() {
        // 100次
        for (int i = 1; i <= 100; i++) {
            System.out.println("count========>"+(++count));
        }
    }
}
​
public class Test9 {
    public static void main(String[] args) {
        // 需求:1变量,100个线程,每个线程对其加100次。
        Runnable runnable = new MyTest9();
​
        // 主线程100次(一共10000次,多次启动代码发现有时候打印10000有时候打印9999,有时候是9998,这就造成了线程安全问题)
        // 多个线程访问统一资源会造成线程安全问题
        for (int i = 1; i <= 100; i++) {
            new Thread(runnable).start();
        }
​
​
    }
}

悲观锁示例代码

一上来就加锁,没有安全感。每次只能一个线程进入访问完毕后,再解锁。线程安全,性能较差!

java 复制代码
package org.example.多线程;
​
class MyTest9 implements Runnable{
    private int count; // 记录浏览人次
    @Override
    public void run() {
        // 100次
        for (int i = 1; i <= 100; i++) {
            // 悲观锁(每次循环都上锁都要排队,相当于10000个人排了一万次队,性能较差)
            synchronized (this){
                System.out.println(Thread.currentThread().getName()+"count========>"+(++count));
            }
        }
    }
}
​
public class Test9 {
    public static void main(String[] args) {
        // 需求:1变量,100个线程,每个线程对其加100次。
        Runnable runnable = new MyTest9();
​
        // 主线程100次(一共10000次,多次启动代码发现有时候打印10000有时候打印9999,有时候是9998,这就造成了线程安全问题)
        // 多个线程访问统一资源会造成线程安全问题
        for (int i = 1; i <= 100; i++) {
            new Thread(runnable).start();
        }
    }
}

乐观锁示例代码

开始不上锁,认为是没有问题的(因为计算机计算性能很高,例如刚刚的代码计算一万次,可能运行两三次才会出现一次线程不安全),大家一起跑,等要出现线程安全问题的时候才开始控制。线程安全,性能较好。

利用了CSA算法,相当于一个单向链表,每次循环都记住上一次累加的值,如果下一次循环得到的值跟期望的值不一致,就会把当前修改的值作废,就会加锁,重新匹配所记录的值

java 复制代码
package org.example.多线程;
​
import java.util.concurrent.atomic.AtomicInteger;
​
class MyTest9 implements Runnable {
    // 整数修改的乐观锁:原子类实现的。
    private AtomicInteger count = new AtomicInteger(); // 记录浏览人次
​
    @Override
    public void run() {
        // 100次
        for (int i = 1; i <= 100; i++) {
            // incrementAndGet()加一后再返回(乐观锁的方法)
            System.out.println(Thread.currentThread().getName() + "count========>" + count.incrementAndGet());
        }
    }
}
​
public class Test9 {
    public static void main(String[] args) {
        // 需求:1变量,100个线程,每个线程对其加100次。
        Runnable runnable = new MyTest9();
​
        // 主线程100次(一共10000次,多次启动代码发现有时候打印10000有时候打印9999,有时候是9998,这就造成了线程安全问题)
        // 多个线程访问统一资源会造成线程安全问题
        for (int i = 1; i <= 100; i++) {
            new Thread(runnable).start();
        }
    }
}

网络编程

后面有框架封装了,这里我只学习不做笔记了

Java高级

Junit单元测试

  • 就是针对最小的功能单元(方法),编写测试代码对其进行正确性测试
  • 可以用来对方法进行测试,它是第三方公司开源出来的(很多开发工具已经集成了Junit框架,比如IDEA)
  • 可以灵活的编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。
  • 不需要程序员去分析测试的结果,会自动生成测试报告出来不需要程序员去分析测试的结果,会自动生成测试报告出来

具体步骤

  • 将junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
  • 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
  • 测试方法上必须声明@Test注解,然后在测试方法中,编写代码调用被测试的业务方法进行测试;
  • 开始测试:选中测试方法,右键选择"JUnit运行",如果测试通过则是绿色;如果测试失败,则是红色
相关推荐
egekm_sefg21 分钟前
一个基于Rust适用于 Web、桌面、移动设备等的全栈应用程序框架
开发语言·前端·rust
ObjectX前端实验室43 分钟前
交互式md文档渲染实现
前端·github·markdown
励志成为大佬的小杨2 小时前
c语言中的枚举类型
java·c语言·前端
前端熊猫2 小时前
Element Plus 日期时间选择器大于当天时间置灰
前端·javascript·vue.js
傻小胖2 小时前
React 组件通信完整指南 以及 自定义事件发布订阅系统
前端·javascript·react.js
JaxNext2 小时前
开发 AI 应用的无敌配方,半小时手搓学英语利器
前端·javascript·aigc
万亿少女的梦1682 小时前
高校网络安全存在的问题与对策研究
java·开发语言·前端·网络·数据库·python
Python私教3 小时前
Vue3中的`ref`与`reactive`:定义、区别、适用场景及总结
前端·javascript·vue.js
CQU_JIAKE3 小时前
12.12【java exp4】react table全局搜索tailwindcss 布局 (Layout) css美化 3. (rowId: number
前端·javascript·react.js
Jiude3 小时前
调试Cesium源码分析并解决在Vite中使用遇到的问题
前端·架构·cesium