【Java】2025版一天学会Java基础到高级(下)

226 阅读40分钟

Stream流

在Java中,Stream(流)是一种用于处理集合数据的抽象方式。通过流,我们可以执行复杂的查询操作,如过滤、排序、映射等,而无需编写冗长的循环和条件语句。流的操作可以链式调用,使得代码更加简洁易读。

生活类比:

想象一下你有一个装满不同水果的篮子,你想从中挑选出所有的苹果并计算他们的总重量。如果没有流,你可能需要手动遍历整个篮子,检查每个水果是否为苹果,并累加苹果的重量。如果使用了流,这就会像给这个篮子添加了一个只能助手,自动帮你完成筛选和计算任务

语法结构

要创建一个流,通常从一个集合或数组开始,然后通过调用.stream()方法来生成流对象。

stream = collection.stream(); // 创建流

流的操作分为两种类型:中间操作(如过滤、映射)和终端操作(如收集结果)。

  • 中间操作返回一个新的流,允许链式调用,
  • 终端操作则触发实际的数据处理过程。

Stream的常用中间方法

  • filter():过滤流中的元素

    stream.filter(s -> s.startsWith("a"));
    
  • map():对流中的每个元素应用一个函数

    stream.map(String::toUpperCase);
    
  • sorted():对流中的元素进行排序

    stream.sorted();
    
  • collect():对流的结果收集到一个集合中

    stream.collect(Collectors.toList());
    
  • distinct():去除流中的重复元素

使用案例

下面的例子演示了如何使用流从一个字符串列表中筛选出所有长度大于3的字符串,并将它们转换为大写形式:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class LambdaExample {
    public static void main(String[] args) {
        // 创建一个包含一些水果名称的列表
        List<String> items = Arrays.asList("apple", "banana", "avocado", "kiwi");

        // 使用Stream API和Lambda表达式进行数据处理
        List<String> result = items.stream()  // 转换列表为流
                                   .filter((String s) -> s.startsWith("a")) // 过滤:只保留以"a"开头的字符串
                                   .map((String s) -> s.toUpperCase())       // 映射:将过滤后的字符串转换为大写
                                   .collect(Collectors.toList());  // 收集:将处理结果收集到一个新的列表中

        // 打印处理后的结果
        System.out.println(result); // 输出 [APPLE, AVOCADO]
    }
}

Stream的常用终结方法

  • forEach():对流中的每个元素执行某个操作

    stream.forEach(System.out::println);
    
  • collect():将流的结果收集到一个集合或其他数据结构中

    List<String> collectedResult = stream.collect(Collectors.toList());
    
  • reduce():通过累加器函数将流中的元素组合起来

    Optional<String> reduced = stream.reduce((s1, s2) -> s1 + "-" + s2);
    
  • count():返回流中元素的数量

    long count = stream.count();
    
  • findFirst():返回当前流中的第一个元素

    Optional<String> first = stream.findFirst();
    
  • findAny():返回当前流中的任意一个元素(在并行流中使用时特别有用)

    Optional<String> any = stream.findAny();
    

数据存储

File类

File是java.io.包下的类,File类的对象,用于代表当前操作系统的文件(可以是文件、或文件夹)

⚠注意:File类只能对文件本身进行操作,不能读写文件里面存储的数据。

📖知识补充:相对路径绝对路径

  • 带有盘符的都是绝对路径
  • 不带有盘符,默认是到你的工程下直接寻找文件的

创建File对象

File 对象名 = new File("绝对路径")

File常用方法

  • 创建新文件或目录

    • createNewFile():当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。
    • mkdir():创建由此抽象路径名命名的目录。
    • mkdirs():创建由这个抽象路径名命名的目录,包括任何必需但不存在的父目录。
  • 删除文件或目录

    • delete():删除此抽象路径名表示的文件或目录。(⚠注意:默认只能删除文件和空文件夹,删除后文件不会进入回收站)
    • deleteOnExit():在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。
  • 获取信息

    • exists():测试此抽象路径名表示的文件或目录是否存在。
    • isFile()isDirectory():分别测试该抽象路径名是否为文件或目录。
    • length():返回由此抽象路径名表示的文件的长度。
    • lastModified():返回文件最后修改时间。
  • 遍历目录

    • list():获取当前目录下所有一级文件名称到一个字符串数组中返回

      File f = new File("文件路径");
      String[] names = f.list();
      
      // 遍历出所有文件名
      for(String name : names){
          sout(name)
      }
      
    • listFiles():去当前目录下所以的一级文件名称到一个File文件对象数组中去返回

      File f = new File("文件路径");
      File[] files = f.listFiles();
      
方法名描述是否可能抛出异常
createNewFile()当且仅当不存在具有此抽象路径名指定名称的文件时,不可分地创建一个新的空文件。IOException
mkdir()创建由此抽象路径名命名的目录。SecurityException (如果安全管理器存在并且其 checkRead 方法不允许该操作)
mkdirs()创建由这个抽象路径名命名的目录,包括任何必需但不存在的父目录。SecurityException
delete()删除此抽象路径名表示的文件或目录。SecurityException
deleteOnExit()在虚拟机终止时,请求删除此抽象路径名表示的文件或目录。
exists()测试此抽象路径名表示的文件或目录是否存在。
isFile()测试该抽象路径名是否为文件。
isDirectory()测试该抽象路径名是否为目录。
length()返回由此抽象路径名表示的文件的长度。
lastModified()返回文件最后修改时间。
listFiles()返回一个抽象路径名数组,这些路径名表示此抽象路径名表示的目录中的文件。SecurityException, NullPointerException (如果抽象路径名为空)

listFiles方法注意事项

  • 当主调是文件时候,或者路径不存在时候,返回null
  • 当主调是空文件夹时,返回一个长度为0的数组
  • 当主调是一个有内容的文件夹时,将其里面所有以及文件和文件夹的路径都放在File数组中返回
  • 当主调是一个文件夹时,且里面有隐藏文件时,将里面所有文件和文件夹的路径放在File数组中返回(包含隐藏文件)
  • 当主调是一个文件夹时,但是没有访问权限,则返回null

实例代码

import java.io.File;
import java.io.IOException;

public class Main {
    public static void main(String[] args) {
        // 创建一个File对象指向目标文件
        File file = new File("example.txt");

        try {
            // 尝试创建新文件
            if (file.createNewFile()) {
                System.out.println("文件已创建:" + file.getName());
            } else {
                System.out.println("文件已存在。");
            }
        } catch (IOException e) {
            System.out.println("发生错误!");
            e.printStackTrace();
        }

        // 检查文件是否存在
        if (file.exists()) {
            System.out.println("文件名为: " + file.getName());
            System.out.println("绝对路径: " + file.getAbsolutePath());
            System.out.println("是否为文件: " + file.isFile());
        } else {
            System.out.println("文件不存在。");
        }
    }
}

方法递归

认识递归

方法调用自身的形式称为方法递归。

递归算法三要素

  • 递归公式

  • 递归终结点

  • 递归方向走向终结点

递归的形式

  • 直接递归:方法自己调用自己

    // 计算n的阶乘
    public static int factorial(int n) {
        if (n == 1) {
            return 1; // 基本情况
        } else {
            return n * factorial(n - 1); // 递归调用
        }
    }
    
  • 间接递归:方法调用其他方法,其方法又回调自己

    public class SimpleRecursion {
          
        // 判断是否为偶数
        public static boolean isEven(int number) {
            if (number == 0) {
                return true;
            } else {
                return isOdd(number - 1); // 调用isOdd方法
            }
        }
          
        // 判断是否为奇数
        public static boolean isOdd(int number) {
            if (number == 0) {
                return false;
            } else {
                return isEven(number - 1); // 回调isEven方法
            }
        }
    }
    

递归在文件搜索中的思路:

  • 先拿到文件夹中的一级文件对象
  • 遍历全部一级文件对象,判断是否符号条件
  • 如果是文件夹,则进入文件夹继续遍历
File dir = new File("文件目录")
searchFile("需要搜索的文件名")

/**
 * 搜索指定文件名的文件
 * @param dir 要搜索的目录
 * @param fileName 搜索的文件名称
 */
public static void searchFile(File 搜索目录, String 搜索的文件名){
    // 1. 判断极端情况
    if(dirr == null || !dir.exists() || dir.isFile()){
        return;
    }
    // 2. 获取目录下的所有以及文件或者文件夹对象
    File[] files = dir.listFiles();
    // 3. 判断当前目录下是否存在一级文件对象,存在才可以遍历
    if(files != null && files.length > 0){
        // 4. 遍历一级文件对象
        for(File file : files){
            // 5. 判断当前一级文件对象是否是文件
            if(file.isFile()){
                // 6. 判断文件名是否和目标文件名称一致
                if(file.getName().contains(fileName)){
                    sout("找到目标文件:" + file.getAbsolutePath());
                }
            } else {
                // 7、如果当前一级文件对象是文件夹,则继续递归调用
                searchFile(file, fileName);
            }
        }
    }
}

IO流

前置知识:

字符集

  • ASCI字符集:只有英文、数字、符号等,占1个字节。

  • GBK字符集:汉字占2个字节,英文、数字占1个字节

  • UTF-8字符集:汉字占3个字节,英文、数字占1个字节。

字符集的编码、解码操作

image-20251028154114133

对字符编码

  • getBytes():使用平台的默认字符集将该String编码为一系列字节,将结果存储到新的直接数组中

    String name = 'AAA'
    byte[] bytes = name.getBytes(); 不写默认就是编译器默认编码
    
  • 使用指定的字符集将该String编码为一系列字节,将结果存储到新的字节数组中

    String name = 'AAA'
    byte[] bytes = name.getBytes("GBK"); // 指定编码
    

对字符的解码

  • 通过使用平台的默认字符集解码指定的字节数组来构造新的String

    String names2 = new String(bytes);
    sout(name2)
    
  • 通过指定的字符集解码指定的字节数组来构造新的String

    String names2 = new String(bytes,"GBK");
    sout(name2)
    

IO流是用于处理输入输出操作的基础工具。

通过使用IO流,程序可以读取外部数据(如文件或网络连接)到内存中,或者将内存中的数据写入外部存储介质。

字节流 vs 字符流:

  • 字节流主要用于处理二进制数据(例如图像、音频文件等),它们包括InputStreamOutputStream及其子类。
  • 字符流适用于处理文本数据,它们基于字符编码(如UTF-8),包括ReaderWriter及其子类。

image-20251028172222979

文件字节输入流

作用:可以把磁盘文件中的数据以字节的形式读入到内存在中。

构造器

  • FileInputStream(File file):创建直接输入流管道与源文件接通

    InputStream is = new FileInputStream(File文件对象)
    
  • FileInputStream(String pathname):创建字节输入流管道与源文件接通

    InputStream is = new FileInputStream("绝对路径")
    

常用方法

  • read():每次读取一个字节返回,如果发现没有数据则返回-1

    int b;
    while(( b = is.read()) != -1){
        sout((char) b);
    }
    

    ⚠读取汉字会出现乱码

  • read(byte[] buffer):每次用一个字节数据去读取数据,返回字节数组读取了多少个字节,如果没有数据则返回-1

    // 定义一个字节数组
    byte[] buffer = new byte[3];
    // 定义一个变量记住每次读取了多少个字节
    int len;
    while((len = is.read(buffer))!= -1){
        // 把读取的字节数组转换成字符串输出
        String str = new String(buffer,0,len);
        sout(str)
    }
    

    参数说明:

    • 0,表示从第一个读

    • len,表示读几个

    ⚠依然无法解决汉字输出乱码问题

使用字节流读取中文,如何保证输出不乱码,怎么解决?

  • readAllBytes():定义一个与文件一样大的字节数组,一次性读取完文件的全部字节,如果文件过大,创建的字节数组也会过大,可能引起内存溢出。

文件字节输出流

作用:以内存为基准,把内存中的数据以字节的形式写出到文件中去。

构造器

  • FileOutputStream(File file):使用 File 对象创建字节输出流,将数据写入该文件。如果文件不存在,则会自动创建;如果存在,则默认覆盖原有内容。

     // 使用 File 对象创建流(覆盖模式)
                File file = new File("output.txt");
                FileOutputStream fos1 = new FileOutputStream(file);
    
  • FileOutputStream(String filepath):使用文件路径字符串创建字节输出流。功能与第一个构造器类似,但直接传入路径字符串。

  • FileOutputStream(File file, boolean append):创建输出流,并指定是否以追加模式写入。如果 appendtrue,则在文件末尾追加数据;否则覆盖原内容。

     // 使用路径字符串创建流(追加模式)
                FileOutputStream fos2 = new FileOutputStream("output.txt", true);
                fos2.write("\nAppend more text.".getBytes());
    
  • FileOutputStream(String filepath, boolean append):使用文件路径和追加标志创建输出流。若 appendtrue,则在文件末尾追加数据;否则覆盖原内容。

常用方法

  • write(int a):将一个字节(int 的低8位)写入输出流。【注意:虽然参数是 int,但只使用其最低的8位】
  • write(byte[] buffer): 将整个字节数组写入输出流
  • write(byte[] buffer, int pos, int len):将字节数组从指定位置开始,写入指定长度的数据。pos 是起始索引,len 是要写入的字节数。
  • close() throws IOException:关闭此输出流并释放与之关联的系统资源。必须调用,否则可能导致资源泄漏。

示例代码

import java.io.FileOutputStream;
import java.io.IOException;

public class WriteExample {
    public static void main(String[] args) {
        String data = "Hello, Java IO!";
        byte[] bytes = data.getBytes(); // 将字符串转换为字节数组

        try (FileOutputStream fos = new FileOutputStream("output.txt")) {
            // 方法1:写单个字节
            for (byte b : bytes) {
                fos.write(b);
            }

            // 方法2:写整个字节数组
            // fos.write(bytes);

            // 方法3:写数组的一部分
            // fos.write(bytes, 0, bytes.length);

        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

字节输出流如何实现写出去的数据可以换行?

  • os.write( "\r\n".getBytes());

代码案例:字节流文件复制

public class CopyDemo1 {
    public static void main(String[] args) {
        // 目标:使用字节流完成文件的复制操作。
        // 源文件:E:\resource\jt.jpg
        // 目标文件:D:\jt_new.jpg (复制过去的时候必须带文件名的,无法自动生成文件名。)
        try {
            copyFile(srcPath: "E:\\resource\\jt.jpg", destPath: "D:\\jt_new.jpg");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    // 复制文件
    public static void copyFile(String srcPath, String destPath) throws Exception {
        // 1、创建一个文件字节输入流管道与源文件接通
        InputStream fis = new FileInputStream(srcPath);
        OutputStream fos = new FileOutputStream(destPath);
        // 2、读取一个字节数组,写入一个字节数组  1024 + 1024 + 3
        byte[] buffer = new byte[1024];
        int len;
        while ((len = fis.read(buffer)) != -1) {
            fos.write(buffer, off: 0, len); // 读取多少个字节,就写入多少个字节
        }
        System.out.println("复制成功!");
    }
}

JDK7开始提供了更简单的资源释放方案:try-with-resource,该资源使用完毕后,会自动调用其close()方法,完成对资源的释放!

public class CopyDemo1 {
    public static void main(String[] args) {
        
        try {
            copyFile(srcPath: "E:\\resource\\jt.jpg", destPath: "D:\\jt_new.jpg");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void copyFile(String srcPath, String destPath) throws Exception {
        // 使用 try-with-resources 语句自动管理资源,确保流在使用后自动关闭
        try (InputStream fis = new FileInputStream(srcPath);
             OutputStream fos = new FileOutputStream(destPath)) {
            byte[] buffer = new byte[1024];
            int len;
            while ((len = fis.read(buffer)) != -1) {
                fos.write(buffer, off: 0, len);
            }
            System.out.println("复制成功!");
        }
        // try-with-resources 会自动处理资源的关闭,无需显式调用 close()
    }
}

文件字符输入流

作用:以内存为基准,可以把文件中的数据以字符的形式读入到内存中。

构造器

  • FileReader(File file)/FileReader(String pathname):创建字符输入流管道与源文件接通

常用方法

  • read()/read(char[] buffer):读取字符输出

文件字符输出流

作用:以内存为基准,把内存中的数据以字符的形式写出到文件中去。

构造器

构造器说明
public FileWriter(File file)创建字节输出流管道与源文件对象接通
public FileWriter(String filepath)创建字节输出流管道与源文件路径接通
public FileWriter(File file, boolean append)创建字节输出流管道与源文件对象接通,可追加数据
public FileWriter(String filepath, boolean append)创建字节输出流管道与源文件路径接通,可追加数据

方法名称

方法名称说明
void write(int c)写一个字符
void write(String str)写一个字符串
void write(String str, int off, int len)写一个字符串的一部分
void write(char[] cbuf)写入一个字符数组
void write(char[] cbuf, int off, int len)写入字符数组的一部分

示例代码

public static void main(String[] args) {
    try (
        // 1. 创建一个字符输出流对象,指定写出的目的地。
        FileWriter fw = new FileWriter("day03-file-io/src/dlei07-out.txt")
    ) {
        // 2. 写一个字符出去:public void write(int c)
        fw.write('a');
        fw.write(98); // ASCII码对应字符'b'
        fw.write('磊');

        // 3. 写一个字符串出去:public void write(String str)
        fw.write("java");
        fw.write("我爱Java,虽然Java不是最好的编程之一,但是可以挣钱");

        // 4. 写一个字符数组出去:public void write(char[] cbuf)
        char[] chars = "java".toCharArray();
        fw.write(chars);

        // 5. 写字符数组的一部分出去:public void write(char[] cbuf, int off, int len)
        fw.write(chars, 1, 2); // 从索引1开始,写入2个字符:"va"
    } catch (Exception e) {
        e.printStackTrace();
    }
}

字符输出流注意事项:

字符输出流写出数据后,必须刷新流,或者关闭流,写出去的数据才能生效。

方法名称说明
public void flush() throws IOException刷新流,就是将内存中缓存的数据立即写到文件中去生效!
public void close() throws IOException关闭流的操作,包含了刷新!
import java.io.FileWriter;
import java.io.IOException;

public class FlushAndCloseExample {
    public static void main(String[] args) {
        try (
            // 创建 FileWriter 对象,指定输出文件路径
            FileWriter fw = new FileWriter("example.txt")
        ) {
            // 写入一些数据到缓冲区
            fw.write("Hello, ");
            fw.write("world!");
            
            // 1. 使用 flush() - 立即将缓冲区的数据写入文件,但不关闭流
            System.out.println("正在执行 flush()...");
            fw.flush(); // 此时 "Hello, world!" 已经被写入 example.txt 文件中
            System.out.println("flush() 执行完毕,数据已写入文件。");

            // 可以继续写入更多数据(因为流还未关闭)
            fw.write(" 这是追加的内容。");
            
            // 2. 使用 close() - 关闭流并自动刷新剩余数据
            // 注意:try-with-resources 会自动调用 close()
            // 如果手动调用,可以这样写:
            // fw.close();
            
        } catch (IOException e) {
            e.printStackTrace();
        }
        
        // 当程序执行到这里时,fw 已经被自动 close()
        // close() 内部已经包含了 flush() 操作
        System.out.println("程序结束,资源已释放。");
    }
}

缓冲流

image-20251029104947103

缓冲字节流

作用:可可以提高字节输入流读取数据的性能

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

构造器说明
public BufferedInputStream(InputStream is)把低级的字节输入流包装成一个高级的缓冲字节输入流,从而提高读取数据的性能
public BufferedOutputStream(OutputStream os)把低级的字节输出流包装成一个高级的缓冲字节输出流,从而提高写数据的性能

代码示例

import java.io.*;

public class BufferedStreamExample {
    public static void main(String[] args) {
        try (
            // 1. 创建一个低级字节输入流(FileInputStream)
            InputStream fis = new FileInputStream("input.txt");
            
            // 2. 使用 BufferedInputStream 包装低级输入流,提升读取性能
            BufferedInputStream bis = new BufferedInputStream(fis)
        ) {
            int data;
            while ((data = bis.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        try (
            // 3. 创建一个低级字节输出流(FileOutputStream)
            OutputStream fos = new FileOutputStream("output.txt");
            
            // 4. 使用 BufferedOutputStream 包装低级输出流,提升写入性能
            BufferedOutputStream bos = new BufferedOutputStream(fos)
        ) {
            String content = "Hello, Buffered Output Stream!";
            byte[] bytes = content.getBytes();
            bos.write(bytes);
            bos.flush(); // 手动刷新缓冲区(可选,close() 会自动刷新)
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

缓冲字符输入流

作用:自带8K(8192)的字符缓冲池,可以提高字符输入流读取字符数据的性能。

构造器

构造器说明
public BufferedReader(Reader r)把低级的字符输入流包装成字符缓冲输入流管道,从而提高字符输入流读取字符数据的性能

新增功能

字符缓冲输入流新增的功能:按照行读取字符

方法说明
public String readLine()读取一行数据返回,如果没有数据可读就返回null

代码示例

import java.io.*;

public class BufferedReaderExample {
    public static void main(String[] args) {
        try (
            // 1. 创建低级字符输入流( FileReader 是 Reader 的子类)
            Reader reader = new FileReader("input.txt");
            
            // 2. 使用 BufferedReader 包装低级字符流,提升读取性能
            BufferedReader br = new BufferedReader(reader)
        ) {
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

缓存字符输入流

作用:自带8K的字符缓冲池,可以提高字符输出流写字符数据的性能。

构造器

构造器说明
public BufferedWriter(Writer r)把低级的字符输出流包装成一个高级的缓冲字符输出流管道,从而提高字符输出流写数据的性能

新增方法:换行

方法说明
public void newLine()换行(自动使用系统默认的换行符)

代码示例

import java.io.*;

public class BufferedWriterExample {
    public static void main(String[] args) {
        try (
            // 1. 创建低级字符输出流(FileWriter 是 Writer 的子类)
            Writer writer = new FileWriter("output.txt");
            
            // 2. 使用 BufferedWriter 包装低级字符流,提升写入性能
            BufferedWriter bw = new BufferedWriter(writer)
        ) {
            // 写入多行内容
            bw.write("第一行内容");
            bw.newLine(); // 换行

            bw.write("第二行内容");
            bw.newLine();

            bw.write("第三行内容");
            bw.newLine();

            // 或者直接写入带换行的内容
            bw.write("第四行内容");
            bw.newLine();

            System.out.println("数据已写入文件!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

其他流

字符输入转换流

解决不同编码时,字符流读取文本内容乱码的问题。

解决思路:先获取文件的原始字节流,再将其按真实的字符集编码转成字符输入流,这样字符输入流中的字符就不乱码了。

构造器

构造器说明
public InputStreamReader(InputStream is)把原始的字节输入流,按照代码默认编码转成字符输入流(与直接用 FileReader 的效果一样)
public InputStreamReader(InputStream is, String charset)把原始的字节输入流,按照指定字符集编码转成字符输入流(重点)

示例代码

import java.io.*;

public class InputStreamReaderExample {
    public static void main(String[] args) {
        try (
            // 1. 创建字节输入流(读取文件)
            InputStream fis = new FileInputStream("input.txt");
            
            // 2. 使用 InputStreamReader 将字节流转换为字符流(指定编码)
            InputStreamReader isr = new InputStreamReader(fis, "UTF-8") // 指定 UTF-8 编码
        ) {
            int data;
            while ((data = isr.read()) != -1) {
                System.out.print((char) data);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

打印流

作用:打印流可以实现更方便、更高效的打印数据出去,能能实现打印啥出去就是啥出去。

构造器

构造器说明
public PrintStream(OutputStream/File/String)打印流直接通向字节输出流/文件/文件路径
public PrintStream(String fileName, Charset charset)可以指定写出去的字符编码
public PrintStream(OutputStream out, boolean autoFlush)可以指定实现自动刷新
public PrintStream(OutputStream out, boolean autoFlush, String encoding)可以指定实现自动刷新,并可指定字符的编码

示例代码

import java.io.*;

public class PrintStreamExample {
    public static void main(String[] args) {
        try (
            // 1. 直接通过文件路径创建 PrintStream(默认 UTF-8 编码)
            PrintStream ps1 = new PrintStream("output1.txt");

            // 2. 指定编码(如 GBK)创建 PrintStream
            PrintStream ps2 = new PrintStream("output2.txt", "GBK");

            // 3. 通过 OutputStream 创建并开启自动刷新
            FileOutputStream fos = new FileOutputStream("output3.txt");
            PrintStream ps3 = new PrintStream(fos, true); // autoFlush = true

            // 4. 通过 OutputStream + 编码 + 自动刷新
            PrintStream ps4 = new PrintStream(fos, true, "UTF-8")
        ) {
            // 使用 ps1:基本打印
            ps1.println("Hello, World!");
            ps1.println("这是第一行内容。");

            // 使用 ps2:指定编码(如 GBK)
            ps2.println("中文测试,使用 GBK 编码写入。");

            // 使用 ps3:自动刷新(每写一行就刷新到磁盘)
            ps3.println("这行会立即写入文件,因为开启了 autoFlush。");
            ps3.println("无需手动 flush()。");

            // 使用 ps4:同时设置编码和自动刷新
            ps4.println("UTF-8 编码 + 自动刷新,高效写入。");

            System.out.println("所有数据已写入文件!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

特殊数据流

允许把数据和其类型一并写出去,持直接写入 int, double, boolean, String 等类型,无需手动转换为字节。

构造器

构造器说明
public DataOutputStream(OutputStream out)创建新数据输出流,包装基础的字节输出流

方法

方法说明
public final void writeByte(int v) throws IOExceptionbyte 类型的数据写入基础的字节输出流
public final void writeInt(int v) throws IOExceptionint 类型的数据写入基础的字节输出流
public final void writeDouble(Double v) throws IOExceptiondouble 类型的数据写入基础的字节输出流
public final void writeUTF(String str) throws IOException将字符串数据以 UTF-8 编码成字节写入基础的字节输出流
void write(int/byte[]/byte[]一部分)支持写字节数据出去

示例代码

import java.io.*;

public class DataOutputStreamExample {
    public static void main(String[] args) {
        try (
            // 1. 创建基础字节输出流(写入文件)
            FileOutputStream fos = new FileOutputStream("data.out");
            
            // 2. 使用 DataOutputStream 包装基础流,支持写基本数据类型
            DataOutputStream dos = new DataOutputStream(fos)
        ) {
            // 写入各种基本数据类型
            dos.writeByte((byte) 100);         // 写 byte
            dos.writeInt(12345);               // 写 int
            dos.writeDouble(3.14159);          // 写 double
            dos.writeUTF("Hello, Java!");      // 写字符串(UTF-8 编码)

            // 写入字节数组
            byte[] bytes = {1, 2, 3, 4, 5};
            dos.write(bytes);                  // 写整个数组

            // 写入字节数组的一部分
            dos.write(bytes, 1, 3);            // 从索引1开始,写3个字节

            System.out.println("数据已写入文件!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

IO框架

封装了JaVa提供的对文件、数据进行操作的代码,对外提供了更简单的方式来对文件进行操作,对数据进行读写等。

导入commons-io.jar框架到项目方法:

  1. 在项目中创建一个文件夹lib
  2. commons-io.jar文件复制到lib文件夹中
  3. 在jar文件上点击右键,选择Add as Library
  4. 在类中导入包使用

FileUtils 类提供的部分方法展示

方法说明
public static void copyFile(File srcFile, File destFile)复制文件
public static void copyDirectory(File srcDir, File destDir)复制文件夹
public static void deleteDirectory(File directory)删除文件夹
public static String readFileToString(File file, String encoding)读取文件内容为字符串
public static void writeStringToFile(File file, String data, String charset, boolean append)将字符串写入文件

代码示例

import org.apache.commons.io.FileUtils;
import java.io.File;
import java.io.IOException;

public class FileUtilsExample {
    public static void main(String[] args) {
        try {
            // 定义源文件和目标路径
            File srcFile = new File("src.txt");
            File destFile = new File("dest.txt");

            File srcDir = new File("srcFolder");
            File destDir = new File("destFolder");

            // 1. 复制文件
            FileUtils.copyFile(srcFile, destFile);
            System.out.println("文件复制成功!");

            // 2. 复制文件夹(递归)
            FileUtils.copyDirectory(srcDir, destDir);
            System.out.println("文件夹复制成功!");

            // 3. 删除文件夹(递归删除所有内容)
            FileUtils.deleteDirectory(destDir);
            System.out.println("文件夹删除成功!");

            // 4. 读取文件内容为字符串(指定编码)
            String content = FileUtils.readFileToString(new File("data.txt"), "UTF-8");
            System.out.println("文件内容:" + content);

            // 5. 写字符串到文件(指定编码和是否追加)
            FileUtils.writeStringToFile(new File("output.txt"), "Hello, Commons IO!", "UTF-8", false);
            System.out.println("数据写入成功!");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

多线程

线程就是一个程序内部的一条执行流程。

多线程是指从软硬件上实现的多条执行流程的技术(多条线程由CPU负责调度执行)。

创建线程

继承Thread类创建

  1. 定义一个子类继承Thread类,作为线程类

    class MyThread extends Thread {}
    
  2. 重写Thread类的run方法

    class MyThread extends Thread {
        @Override
        public void run() {}
    }
    
  3. run方法中编写线程的任务代码

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("线程开始执行");
        }
    }
    
  4. 创建线程类的对象

    Thread t = new MyThread();
    
  5. 调用start方法,启动线程

    t.start();
    

完整代码:

public class Main {

    public static void main(String[] args) {
        // 4. 创建线程类的对象
        Thread t = new MyThread();
        // 5. 调用`start`方法,启动线程
        t.start();
        // 对比主线程
        for (int i = 0; i < 5; i++){
            System.out.println("主线程开始执行"+ i);
        }
    }
}
// 1. 定义一个子类继承`Thread`类,作为线程类
class MyThread extends Thread {
    @Override
    // 2. 重写`Thread`类的`run`方法
    public void run() {
        // 3. 在`run`方法中编写线程的任务代码
        for (int i = 0; i < 5; i++){
            System.out.println("子线程开始执行" + i);
        }
    }
}

⚠注意事项:

  • 不要把主线程放在启动子线程之前,不然永远都是主线程先跑

实现Runnable接口创建

  1. 定义一个线程任务类实现Runnable接口
class MyRunnable implements Runnable{}
  1. 重写run方法,设置线程任务

    class MyRunnable implements Runnable{
        @Override
        public void run() {}
        }
    }
    
  2. 创建线程任务类的对象

    Runnable r = new MyRunnable();
    
  3. 把线程任务对象交给线程对象来处理

    Thread t = new Thread(r);
    
  4. 启动线程start方法

    t.start();
    

完整代码:

public class Main {

    public static void main(String[] args) {
        // 3. 创建线程任务类的对象
        Runnable r = new MyRunnable();
        // 4. 把线程任务对象交给线程对象来处理
        Thread t = new Thread(r);
        // 5. 启动线程
        t.start();

        // 对比主线程
        for (int i = 0; i < 5; i++){
            System.out.println("主线程开始执行"+ i);
        }
    }
}
// 1. 定义一个线程任务类实现`Runnable`接口
class MyRunnable implements Runnable{
    @Override
    // 2. 重写`run方法`,设置线程任务
    public void run() {
        for (int i = 0; i < 5; i++){
            System.out.println("子线程开始执行"+ i);
        }
    }
}

匿名内部类写法:

public class Main {
    public static void main(String[] args) {
        // 匿名内部类简化写法
        new Thread(() -> {
                for (int i = 0; i < 5; i++){
                    System.out.println("子线程开始执行"+ i);
                }
        }).start();

        // 对比主线程
        for (int i = 0; i < 5; i++){
            System.out.println("主线程开始执行"+ i);
        }
    }
}

实现Callable接口创建

前两种线程创建方式都存在的一个问题:他们都是重写run方法均不能直接返回结果。

  1. 定义一个类实现Callable接口

    // 1. 定义一个类实现`Callable`接口
    class Task implements Callable<Integer> {
        
    }
    
  2. 重写call方法,定义线程执行体

    private int n;
        public MyCallable(int n) {
            this.n = n;
        }
        // 2. 实现call方法,定义线程执行体
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 0; i < n; i++) {
                sum += i;
            }
            return sum;
        }
    
  3. 创建一个Callable接口的实现类对象

    Callable<Integer> c = new MyCallable(传参);
    
  4. Callable类型的对象封装成FutureTask(线程任务对象)

    FutureTask<Integer> ft = new FutureTask<Integer>(c);
    
  5. 把线程任务对象交给Thread对象

    Thread t = new Thread(ft);
    
  6. 调用start方法启动线程

    t.start();
    
  7. 线程执行完毕后,通过FutureTask对象的get方法去获取线程任务执行的结果

    ft.get()
    

完整代码:

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

public class Main {
    public static void main(String[] args) {
        // 3. 创建一个Callable接口的实现类对象
        Callable<Integer> c = new MyCallable(344);
        // 4. 把Callable对象封装成一个真正的线程任务对象FutureTask对象
        FutureTask<Integer> ft = new FutureTask<Integer>(c);
        // 5. 把FutureTask对象作为参数传递给`Thread`对象
        Thread t = new Thread(ft);
        // 6. 启动线程
        t.start();
        // 7. 获取线程执行结果
        try {
            System.out.println("子线程计算结果为:" + ft.get());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 1. 定义一个类实现`Callable`接口
class MyCallable implements Callable<Integer> {
    private int n;
    public MyCallable(int n) {
        this.n = n;
    }
    // 2. 实现call方法,定义线程执行体
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 0; i < n; i++) {
            sum += i;
        }
        return sum;
    }
}

三种创建线程方法对比

方式优点缺点
继承 Thread 类编程比较简单,可以直接使用 Thread 类中的方法扩展性较差,不能再继承其他的类,不能返回线程执行的结果
实现 Runnable 接口扩展性强,实现该接口的同时还可以继承其他的类编程相对复杂,不能返回线程执行的结果
实现 Callable 接口扩展性强,实现该接口的同时还可以继承其他的类。可以得到线程执行的结果编程相对复杂

多线程常用方法

⭕总结:

  • run() 是线程要执行的核心逻辑。
  • start() 是启动线程的关键方法。
  • getName() / setName() 用于管理线程名称。
  • currentThread() 用于获取当前运行的线程。
  • sleep() 实现线程暂停。
  • join() 实现线程之间的协调与同步。
  • 构造器支持通过 Runnable 实现多线程,也支持命名线程。

Thread常用方法

方法说明
public void run()线程的任务方法。当线程启动后,会自动执行此方法中的代码。
public void start()启动线程,调用此方法后,JVM 会创建一个新的线程并执行 run() 方法。
public String getName()获取当前线程的名称,默认名称为 Thread-索引(如 Thread-0, Thread-1)。
public void setName(String name)为线程设置自定义名称,便于调试和识别。
public static Thread currentThread()返回当前正在执行的线程对象,常用于获取当前线程的信息。
public static void sleep(long time)让当前线程休眠指定的毫秒数,之后继续执行。如果中断,会抛出 InterruptedException
public final void join()...让调用该方法的线程等待当前线程执行完毕后再继续执行。可以传入超时时间。

Thread 的常见构造器

构造器说明
public Thread(String name)创建一个线程,并为其指定名称。
public Thread(Runnable target)将一个实现了 Runnable 接口的对象封装为线程对象,由该线程执行 Runnablerun() 方法。
public Thread(Runnable target, String name)封装 Runnable 对象的同时,指定线程名称。

线程安全

多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题。

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

线程同步

线程同步是线程安全问题的解决方案。

线程同步的核心思想:让多个线程先后依次访问共享资源,这样就可以避免出现线程安全问题。

线程同步的常见方案:

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

同步代码块

作用:把访问共享资源的核心代码给上锁,以此保证线程安全。

synchronized(同步锁){
    访问共享资源的核心代码
}

⚠注意实现:

  • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象。

示例代码:

public class Counter {
    private int count = 0;
    private Object lock = new Object(); // 同步锁对象

    public void increment() {
        synchronized (lock) {
            count++; // 访问共享资源
        }
    }

    public int getCount() {
        synchronized (lock) {
            return count;
        }
    }
}
  • synchronized (lock):使用 lock 对象作为锁,保护共享资源。
  • count++ 是共享资源操作,被同步代码块保护,避免多线程同时修改。
  • 每次只有一个线程能进入该代码块,确保线程安全。

同步方法

作用:把访问共享资源的核心方法给上锁,以此保证线程安全。

修饰符 synchronized 返回值类型 方法名称(形参列表){
    操作共享资源代码
}

lock锁

Lock锁是JDK5开始提供的一个新的锁定操作,通过它可以创建出锁对象进行加锁和解锁,更灵活、更方便、更强大。

Lock是接口,不能直接实例化,可以采用它的实现类ReentrantLock来构建Lock锁对象。

构造器:

构造器说明
public ReentrantLock()获得Lock锁的实现类对象

常用方法:

方法名说明
void lock()获得锁
void unlock()释放锁

线程池

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

创建线程池

方法一:使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象。

构造器

public ThreadPoolExecutor(
    int corePoolSize,           // 核心线程数
    int maximumPoolSize,        // 最大线程数
    long keepAliveTime,         // 非核心线程空闲存活时间
    TimeUnit unit,              // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory, // 线程工厂(可选)
    RejectedExecutionHandler handler     // 拒绝策略(可选)
)

各参数详解

参数说明
corePoolSize线程池中常驻的核心线程数量。即使这些线程空闲,也不会被回收(除非设置了 allowCoreThreadTimeOut(true))。
maximumPoolSize线程池中允许存在的最大线程数。当任务过多、队列满时,会创建额外线程,最多到此值。
keepAliveTime超过 corePoolSize非核心线程在空闲时等待新任务的最长时间。超时则被销毁。
unitkeepAliveTime 的时间单位,如 TimeUnit.SECONDSMILLISECONDS 等。
workQueue存放待执行任务的阻塞队列。常见有: - LinkedBlockingQueue(无界) - ArrayBlockingQueue(有界) - SynchronousQueue(不存储任务,直接移交)
threadFactory(可选)用于创建新线程的工厂。可自定义线程名、优先级等。常用 Executors.defaultThreadFactory()
handler(可选)当线程池饱和(线程数达上限且队列满)时,对新提交任务的拒绝策略

常用方法

方法名称说明
void execute(Runnable command)执行一个 Runnable 任务(无返回值)
Future<T> submit(Callable<T> task)提交一个 Callable 任务,返回 Future 对象,用于获取结果
void shutdown()等待所有任务执行完毕后,关闭线程池
List<Runnable> shutdownNow()立即关闭线程池,停止正在执行的任务,返回未执行的任务列表

示例代码

import java.util.concurrent.*;

// 创建一个自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                                // 核心线程数
    4,                                // 最大线程数
    10,                               // 空闲线程存活时间
    TimeUnit.SECONDS,                 // 时间单位
    new ArrayBlockingQueue<>(10),     // 任务队列,最多10个任务等待
    new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);

// 提交任务
executor.execute(() -> System.out.println("Task running in thread: " + Thread.currentThread().getName()));

// 关闭线程池
executor.shutdown();

方法二:使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象。

Executors类是一个线程池的工具类,提供了很多静态方法用于返回不同特点的线程池对象。

方法名称说明
newFixedThreadPool(int nThreads)创建固定大小的线程池,线程数不变,任务多时进入队列等待。
newSingleThreadExecutor()创建只有一个线程的线程池,保证任务顺序执行。
newCachedThreadPool()创建缓存线程池,线程数量动态增长,空闲超过60秒会被回收。
newScheduledThreadPool(int corePoolSize)创建支持定时或周期性任务的线程池。

并发和并行

进程:正在运行的程序(软件)就是一个独立的进程。

线程是属于进程的,一个进程中可以同时运行很多个线程,进程中的多个线程其实是并发并行执行的。

并发:进程中的线程是由CPU负责调度执行的,但CPU能同时处理线程的数量有限,为了保证全部线程都能往前执行,CPU会轮询为系统的每个线程服务,由于CPU切换的速度很快,给我们的感觉这些线程在同时执行,这就是并发。

并行:在同一个时刻上,同时有多个线程在被CPU调度执行。

网络编程

可以让设备中的程序与网络上其他设备中的程序进行数据交互的技术(实现网络通信)

基本通信架构

基本的通信架构有2种形式:

  • CS架构(Client客户端/Server服务端)
  • BS架构(Browser浏览器/Server服务端)

⚠无论是CS架构,还是BS架构的软件都必须依赖网络编程!

网络编程三要素

IP:设备在网络中的地址,是设备在网络中的唯一标识

端口:应用程序在设备中的唯一标识

协议:连接和数据在网络中传输的规则

IP

IP:全称”互联网协议地址”,是分配给上网设备的唯一标识。目前,被广泛采用的IP地址形式有两种:IPv4、IPv6。

  • IPv4,它使用32位地址,通常以点分十进制表示。
  • IPv6,它使用128位地址,号称可以为地球上的每一粒沙子编号。
方法名称说明
getLocalHost()获取本机的 IP 地址和主机名,返回一个 InetAddress 对象。
getHostName()获取当前 InetAddress 对象对应的主机名(如:localhost)。
getHostAddress()获取当前 InetAddress 对象对应的 IP 地址(如:127.0.0.1)。
getByName(String host)根据域名或 IP 地址字符串,解析出对应的 InetAddress 对象。
isReachable(int timeout)判断指定主机是否可达(可连通),超时时间单位为毫秒。

总结

通过这张图,你可以快速掌握 InetAddress 的核心功能:

  • ✅ 获取本机信息 → getLocalHost()
  • ✅ 获取主机名/IP → getHostName() / getHostAddress()
  • ✅ 域名解析 → getByName()
  • ✅ 检测连通性 → isReachable()

端口

用来标记标记正在计算机设备上运行的应用程序,被规定为一个16位的二进制,范围是0~65535。

端口分类:

  • 周知端口:0~1023,被预先定义的知名应用占用(如:HTTP占用80,FTP占用21)
  • 注册端口:1024~49151,分配给用户进程或某些应用程序。
  • 动态端口:49152到65535,之所以称为动态端口,是因为它一般不固定分配某种进程,而是动态分配。

⚠注意:我们自己开发的程序般选择使用程序的端口号一样,否则报错。

协议

网络上通信的设备,事先规定的连接规则,以及传输数据的规则被称为网络通信协议。

开放式网络互联标准:OSI网络参考模型

OSI 层TCP/IP 层各层对应协议面向操作(程序员关注点)
应用层应用层HTTP、FTP、SMTP 等开发浏览器、邮箱等应用
表示层——加密、压缩、格式转换数据编码/解码(如 JSON/XML)
会话层——建立、管理会话连接状态维护(如登录态)
传输层传输层TCP、UDP选择可靠(TCP)或不可靠(UDP)传输
网络层网络层IP、ICMP、ARP封装源 IP 和目标 IP 地址
数据链路层数据链路层 + 物理层MAC 地址、以太网帧二进制数据在物理设备中传输
物理层——电缆、光纤、无线电波信号传输(硬件层面)

传输层的2个通信协议:

  • UDP:用户数据报协议。
  • TCP:传输控制协议。

UDP通信

(挖坑)

TCP通信

(挖坑)

Java高级技术

单元测试

就是针对最小的功能单元:方法,编写测试代码对其进行正确性测试。

Junit单元测试框架:

  • 可以灵活的编写测试代码,可以针对某个方法执行测试,也支持一键完成对全部方法的自动化测试,且各自独立。
  • 不需要程序员去分析测试的结果,会自动生成测试报告出来。

具体步骤:

  1. 将Junit框架的jar包导入到项目中(注意:IDEA集成了Junit框架,不需要我们自己手工导入了)
  2. 为需要测试的业务类,定义对应的测试类,并为每个业务方法,编写对应的测试方法(必须:公共、无参、无返回值)
  3. 测试方法上必须声明@Test注解,然然后在测试方法中,编写代码调用被测试的业务方法进行测试
  4. 开始测试:选中测试方法,右键选择“JUnit运行””,如果测试通过则是绿色;如果测试失败,则是红色

断言:

assertEquals(返回信息,期望值,测试方法() )

案例:

public class Main {
    public static void main(String[] args) {
        System.out.println("1到100的和为:" + sumFunction(100));
    }
    
    /**
     * 计算从1到n的整数和
     * @param n 正整数
     * @return 从1到n的和,如果n小于0则返回-1
     */
    public static int sumFunction(int n){
        int sum = 0;
        if (n < 0) {
            System.out.println("-----出 ❌ 错-----");
            System.out.println("输入的数字必须大于0");
            return -1;
        }
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        System.out.println("1到" + n + "的和为:" + sum);
        return sum;
    }
    
}
import org.junit.Test;
import static org.junit.Assert.*;

public class TestClass {
    @Test
    public void testMainMethods() {
        // 测试Main类中的各种数学计算方法
        int sumResult = Main.sumFunction(100);
        System.out.println("测试求和结果: " + sumResult);
        assertEquals(-1, Main.sumFunction(-1));
    }
}

反射

反射就是:加载类,并允许以编程的方式解剖类中的各种成分(成员变量、方法、构造器等)。

反射调用

  1. 加载类,获取类的字节码Class对象(获取Class对象的三种方式)

    • Class c1 = 类名.class

    • 调用Class提供的方法

      Class forName(类的全类名);
      
    • Object提供的方法:

      对象.getClass();
      
  2. 获取类的信息

    // 获取类的全类名
    getName()
    // 获取类的简名
    getSimpleName()
    
  3. 获取类的构造器:Constructor对象

    // 获取构造器
    getDeclaredConstructers()
    // 获取构造器(只能拿public修饰的)
    getConstructers()
    
  4. 获取类的成员变量:Field对象

    // 获取成员变量
    getDeclaredFields()
    // 获取单个成员变量
    getDeclaredField("变量名")
    
  5. 获取类的成员方法:Method对象

    // 获取成员方法
    getDeclaredMethods()
    // 获取单个成员方法
    getDeclaredMethod("方法名")
    getDeclaredMethod("方法名",参数类型)
    

反射使用

暴力反射setAccessible(true)

构造器对象.setAccessible(true)

构造器反射调用

  • 构造器对象.newInstance():调用此构造器对象

成员变量反射调用

  • 成员变量对象.set(对象,内容)
  • 成员变量对象.get(对象)

方法反射调用

  • 成员方法对象.invoke(对象,?参数)

反射作用

基本作用:

  1. 可以得到一个类的全部成分然后操作
  2. 可以破换封装性
  3. 可以绕过泛型的约束
  4. 最重要的用途是:适合做Java的框架,基本上,主流的框架都会基于反射设计出一些通用的功能。

注解

就是Java代码里的特殊标记,比如:@Override、@Test等,作用是:让其他程序根据注解信息来决定怎么执行该程序

自定义注解

语法格式:

public @interface 注解名称{
    属性类型 属性名() ?default 默认值;

}

特殊属性名:value(在使用时属性必须只有一个value属性)

public @interface 注解名称{
	属性类型 value;
}

使用:

@注解名称(属性名=属性值)
@注解名称(属性值) // 特殊属性名的时候

如果注解中只有一个value属性,使用注解时,value名称可以不写!!

元注解

元注解是注解注解的注解。

@Target(ElementType.Type)
@Retention(RetentionPolicy.RUNTIME)
public @interface 注解名称{

}

@Target

作用:声明被修饰的注解只能在哪些位置使用

@Target(ElementType.Type)
  1. Type:类或者接口
  2. FIELD:成员变量
  3. METHOD:成员方法
  4. PARAMETER:方法参数
  5. CONSTRUCTOR:构造器
  6. LOCAL_VARIABLE:局部变量

@Retention

作用:声明注解的保留周期。

@Retention(RetentionPolicy.RUNTIME)
  1. SOURCE:只作用于源码阶段,字节码文件中不存在。
  2. CLASS(默认值):保留到字节码文件阶段,运行阶段不存在。
  3. RUNTIME(开发常用):一直保留到运行阶段。

注解的解析

就是判断类上、方法上成员变量上否存在注解并把注解里的内容给解析出来。

如何解析注解?

指导思想:要解析谁上面的注解,京就应该先拿到谁,比如要解析类上面的注解,则应该先获取该类的Class对象,再通过Class对象解析其上面的注解。比如要解析成员方法上的注解,则应该获取到该成员方法的Method对象,再通过Method对象解析其上面的注解。

ClassMethodFieldConstructor、都实现了AnnotatedElement接口,它们都拥有解析注解的能力。

方法返回值作用
getDeclaredAnnotations()Annotation[]获取当前元素(类、方法、字段等)自己声明的所有注解(不包括继承的)
getDeclaredAnnotation(Class<T> annotationClass)T获取当前元素上指定类型的注解对象,如果不存在则返回 null
isAnnotationPresent(Class<? extends Annotation> annotationClass)boolean判断当前元素是否标注了指定类型的注解

动态代理

java.lang.reflect.Proxy类:提供了为对象产生代理对象的方法:newProxyInstance()

public static Object newProxyInstance(
    ClassLoader loader,           // 类加载器
    Class<?>[] interfaces,        // 代理对象需要实现的接口数组
    InvocationHandler h           // 调用处理器
)

三个参数的详细说明:

  1. ClassLoader loader
    • 用于加载代理类的类加载器
    • 通常使用被代理对象的类加载器:target.getClass().getClassLoader()
  2. Class<?>[] interfaces
    • 代理对象需要实现的接口数组
    • 必须是接口,不能是类
    • 代理对象会实现这些接口,因此可以转换为这些接口类型
  3. InvocationHandler h
    • 调用处理器,实现 InvocationHandler 接口
    • 当代理对象的方法被调用时,会执行处理器中的 invoke 方法

返回值:

  • 返回一个实现了指定接口的代理对象
  • 需要强制转换为相应的接口类型

示例代码:

// 学生行为接口
public interface StudentBehavior {
    void study(String name);
    String play();
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Student implements StudentBehavior {
    private String name;

    @Override
    public void study(String knowledge) {
        System.out.println(name + "要开始学习" + knowledge + "啦!!!.");
        System.out.println("");
    }

    @Override
    public String play() {
        System.out.println(name + " 开始游戏啦!!!.");
        System.out.println("");
        return "游戏中...🤗";
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyUtils {
    // 创建一个代理对象
    public static StudentBehavior createProxy(Student s) {
        /**
         * 参数1:用于执行哪个类加载器生成的代理类
         * 参数2:用于指定代理对象需要实现的接口
         * 参数3:用于指定代理类需要如何去代理(代理要做哪些事情)
         */
        StudentBehavior proxy = (StudentBehavior) Proxy.newProxyInstance(
                ProxyUtils.class.getClassLoader(),
                s.getClass().getInterfaces(),
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        /**
                         * 用来声明代理对象要干那些事情
                         * 参数一:proxy接受代理对象本身(暂时用处不大)
                         * 参数二:method表示代理对象要调用的方法(正在被代理的方法)
                         * 参数三:args表示代理对象要调用方法时实际参数
                         */
                        System.out.println("代理对象开始...");
                        String methodName = method.getName();
                        if ("study".equals(methodName)) {
                            System.out.println("代理对象将提醒你学习📖...");
                        }else if("play".equals(methodName)){
                            System.out.println("代理对象将提醒你游戏🔮...");
                        }
                        System.out.println("代理对象完毕...");
                        System.out.println("---------------");
                        Object result = method.invoke(s, args);
                        return result;
                    }
                }
        );
        return proxy;
    }
}
public class Main {
    public static void main(String[] args) {
        Student s = new Student("张三");
        StudentBehavior proxy = ProxyUtils.createProxy(s);
        proxy.study("语文");
        System.out.println(proxy.play());
    }
}