JavaSE——高级篇
反射
-
目的:编译时动态获取信息
-
原理:JVM运行时,会对所有的对象都创建一个
Class
对象,这个Class
对象包含了类的所有信息,因此可以动态地获取类的信息 -
应用场景:动态创建实例,减少重复的代码
-
工作中反射的例子
- Json反序列化技术,将Jaon字符串转化为相应实例,使用反射就可以一次覆盖所有实体类的反序列化方法
- AOP使用JDK动态代理或CGLIB,通过反射创建目标对象的代理
- IOC识别带有
@Component
、@Service
等注解的类代替创建实例,覆盖所有类的new方法 - JMockit使用反射创建Mock对象,覆盖对应类的new方法
获取Class
//方法1(推荐)
Class<?> aClass = 类.class;
//方法2
Class<?> aClass = Class.forName("对象全类名");
//方法3
Class<?> aClass = 对象.getClass();
获取成员信息
-
成员信息分为三类
Field
:成员信息对象Method
:方法信息对象Constructor
:构造器成员对象
public final class Class<T>{public native Class<? super T> getSuperclass(); //获取父类public Field[] getFields(){...} //获取所有公共属性public Field getField(String name){...} //获取指定的公共属性public Field[] getDeclaredFields(){...} //获取所有的属性(包含私有)public Field getDeclaredField(String name){...} //获取指定的属性(包含私有)
}public final class Class<T>{public Method[] getMethods(){...} //获取所有公共方法public Method getMethod(String name, Class<?>... parameterTypes){...} //获取指定公共方法,参数是入参类型.classpublic Method[] getDeclaredMethods(){...} //获取所有的方法(包含私有)public Method getDeclaredMethod(String name, Class<?>... parameterTypes){...} //获取指定的方法(包含私有)public Constructor<?>[] getConstructors(){...} //获取所有的公共构造方法public Constructor<T> getConstructor(Class<?>... parameterTypes){...} //获取指定公共构造方法,无入参即无参构造public Constructor<?>[] getDeclaredConstructors(){...} //获取所有的构造方法(包含私有)public Constructor<T> getDeclaredConstructor(Class<?>... parameterTypes){...} //获取指定构造方法(私有)
}
调用方法
-
一般步骤
- 创建
Class
实例 - 通过
Constructor
创建对象 - 获取
Method
实例 - 调用
Method.invoke()
方法
- 创建
-
不建议使用
Class.newInstance()
方创建对象
public final class Method{public Object invoke(Object obj, Object... args) {...} // 第一个参数是创建的对象,之后是方法的入参,无参为nullpublic void setAccessible(boolean flag) {...} // 如果Method对象对应的方法权限是private,调用前需要先设置为truepublic Type[] getGenericParameterTypes() {...} // 获取方法入参类型public TypeVariable<Method>[] getTypeParameters() {...} // 获取方法的泛型信息
}
举例:覆盖测试类异常情形
- Dao层要对异常情况做检查(例如连接失败、SQL非法等),确保异常出现时都会抛出
- 所有的Dao类都要专门做一个
DaoExceptionTest
类,测试其中所有方法是否抛出异常,这很麻烦- 要求只做一个
DaoSQLExceptionTest
类,模拟数据库异常时,利用反射覆盖所有Dao类的所有方法,实现“代码瘦身”
<dependency><groupId>org.reflections</groupId><artifactId>reflections</artifactId> <!--Reflections封装了一些反射操作,用于扫描类和配置文件--><version>0.10.2</version>
</dependency>
public class DaoSQLExceptionTest {static private final Set<Class<? extends BaseDao>> daoClasses;static private final Map<Type, Object> vlueMap = new HashMap<>();static {// 创建 Reflections 实例并指定扫描器Reflections reflections = new Reflections("com.wyh.dao");// getSubTypesOf:获取所有子类daoClasses = reflections.getSubTypesOf(BaseDao.class);//初始化参数MapdefaultVlueMap.put(Integer.class, 0);defaultVlueMap.put(int.class, 0);defaultVlueMap.put(Long.class, 0L);defaultVlueMap.put(long.class, 0L);defaultVlueMap.put(Float.class, 0.0F);defaultVlueMap.put(float.class, 0.0F);defaultVlueMap.put(Double.class, 0.0);defaultVlueMap.put(double.class, 0.0);defaultVlueMap.put(Character.class, 'a');defaultVlueMap.put(char.class, 'a');defaultVlueMap.put(String.class, "abc");}@Injectableprivate DruidDataSource mockDataSource;@BeforeMethodpublic void setUp() throws SQLException {new Expectations() {{mockDataSource.getConnection(); //模拟连接异常result = new SQLException("Simulated connection failure");}};}@Testpublic void testException() throws Exception {for (Class<? extends BaseDao> daoClass : daoClasses) {// 创建 DAO 实例BaseDao dao= daoClass.getDeclaredConstructor().newInstance();// 使用反射获取所有方法for (Method method : daoClass.getDeclaredMethods()) {try {method.invoke(dao, getMethodParameters(method)); // 验证异常Assert.fail(); //防止mock失效} catch (Exception e) {Assert.assertTrue(e.getCause() instanceof PersistenceException);}}}}private Object[] getMethodParameters(Method method) {Type[] parameterTypes = method.getGenericParameterTypes(); //获取入参类型Object[] parameters = new Object[parameterTypes.length];for (int i = 0; i < parameterTypes.length; i++) { //根据入参类型构建实参数组Type parameterType = parameterTypes[i];parameters[i] = defaultVlueMap.getOrDefault(parameterType, null); //不在Map列举范围内的类型入参null}return parameters;}
}
File类
File
实例指代一个文件或目录,其方法可以对文件整体进行操作,但不能访问文件的内容,需要通过IO流才可以读写文件中的内容
创建File
-
路径格式写法:推荐使用相对路径,根目录就是工程根路径(即.idea所在目录)
- 正斜杠写法
"resource/file.txt"
- 反斜杠写法
"resource\\file.txt"
- File类提供了系统分隔符(推荐)
"resource"+File.separator+"file.txt"
- 正斜杠写法
public class File{public File(String pathname) {...} //创建File实例,系统会识别是绝对路径还是相对路径;若是相对路径,则根目录是当前项目public File(String parent, String child) {...} //相对路径创建File实例,根目录就是parentpublic File(File parent, String child) {...} //相对路径创建File实例,根目录就是parent
}
操作File
public class File{public boolean createNewFile() throws IOException {...} //在系统中真正创建文件,如果已存在返回falsepublic boolean mkdir() {...} //创建目录,如果已存在返回falsepublic boolean exists() {...} //检查路径下是否存在文件/目录public boolean isFile() {...} //检查路径下存在且是文件public boolean isDirectory() {...} //检查路径下存在且是目录public long length() {...} //获取文件大小字节,目录返回0public boolean delete() {...} //删除文件或空目录public String getName() {...} //获取文件或目录名public String getPath() {...} //获取相对路径public String getAbsolutePath() {...} //获取绝对路径(推荐)//创建临时文件,JVM关闭时销毁,prefix为文件名至少三个字符,suffix为后缀名,默认.tmp,目录系统自动指定public static File createTempFile(String prefix, String suffix) {...} public static File createTempFile(String prefix, String suffix, File directory) {...} //手动指定临时文件目录public File[] listFiles() {...} //返回目录下的所有子文件和子目录,如果对象是文件则返回nullpublic File[] listFiles(FileFilter filter) {...} //根据FileFilter接口实现类返回boolean,获取筛选的File数组
}
举例:遍历所有文件
需求:查询多层级目录中有多少文件
public class Main {static Long fileNum = 0L;public static void searchFiles(File file) { //递归,深度优先//递归终点if (!file.isDirectory()) {fileNum ++;return;}//递归调用for (File f : file.listFiles()) {searchFiles(f);}}public static void main(String[] args) throws Exception {searchFiles(new File("E:\\JavaCourse"));System.out.println(fileNum);}
}
Files类
- Files类是对于File的核心工具类,提供了丰富的静态方法
- 常见的文件操作建议使用此工具类,因为NIO方式更高效
List<String> lines = Files.readAllLines(Paths.get("example.txt")); //读取所有行byte[] bytes = Files.readAllBytes(Paths.get("example.txt")); //读取所有字节Files.write(Paths.get("output.txt"), List.of("Hello", "World"), StandardOpenOption.CREATE); //写入字符串Files.write(Paths.get("binary.dat"), "Hello, World!".getBytes()); //写入字节数组Files.size(Paths.get("example.txt")); //获取文件大小Files.copy(Paths.get("source.txt"), Paths.get("target.txt")); //复制文件Files.move(Paths.get("old.txt"), Paths.get("new.txt")); //移动文件Files.delete(Paths.get("example.txt")); //删除文件Stream<Path> stream = Files.walk(Paths.get("path/to/dir")); //递归遍历所有文件,返回Stream流Files.newInputStream(file.toPath()); //快速获取一个文件输入流
Path类
-
Path
类用于防护攻击者利用../
和分隔符组合尝试跳出或跳进目标目录 -
防护路径攻击步骤
- 使用
Paths.get()
表示路径 - 用户输入路径拼接使用
Path.resolve()
+Path.normalize()
,防止路径遍历 - 限制文件操作范围
Path.startsWith()
,确保路径在允许的目录内
- 使用
// Paths.get()创建Path对象,如果传入多个参数会自动拼接,且自动处理不同操作系统的路径分隔符(/ 或 \)
Path path1 = Paths.get("C:/data/file.txt"); // Windows 路径
Path path2 = Paths.get("/home/user/docs/file.txt"); // Linux/macOS 路径
Path path3 = Paths.get("data", "subdir", "file.txt"); // 相对路径(自动拼接)
Path relativePath = Paths.get("data/file.txt"); //如果没有根目录就默认相对路径,toAbsolutePath()转换为绝对路径
Path basePath = Paths.get("/home/user");
// 安全拼接路径,避免直接使用 +
Path path = basePath.resolve("docs/file.txt"); // /home/user/docs/file.txt
// 获取路径信息
System.out.println("文件名: " + path.getFileName());
System.out.println("父目录: " + path.getParent());
System.out.println("根目录: " + path.getRoot());
System.out.println("路径字符串: " + path.toString());
//移除多余的 .. 和 .,防止路径遍历攻击
Path normalizedPath = Paths.get("/home/user/../docs/file.txt").normalize(); // /home/docs/file.tx
// 检查 fullPath 是否仍在 basePath 下
if (!path.startsWith(basePath)) {throw new SecurityException("路径访问被禁止!");
}
举例:路径遍历防护
用户上传文件时,恶意输入
../../etc/passwd
试图覆盖系统文件
public class FileDownloadSecurity {public static void main(String[] args) throws Exception {String baseDir = "/var/www/uploads"; // 允许访问的目录String userInput = "../../etc/passwd"; // 用户恶意输入// 安全拼接路径Path basePath = Paths.get(baseDir).normalize().toRealPath();Path userPath = Paths.get(userInput).normalize();Path fullPath = basePath.resolve(userPath);// 检查路径是否在允许范围内if (!fullPath.normalize().startsWith(basePath)) {throw new SecurityException("非法路径访问!");}// 检查文件扩展名(可选)String fileName = fullPath.getFileName().toString();if (!fileName.matches(".*\\.(jpg|png|pdf)")) {throw new SecurityException("不支持的文件类型!");}System.out.println("安全路径: " + fullPath);}
}
IO流
-
IO流的目的:实现内存与外界数据相互通信
外界--->字节码--->内存--->字节码--->外界
|------输入流------|------输出流------|
-
IO流的使用场景
JVM
内部数据都是运行在内存中的,内存内部数据通信一般不需要使用IO流JVM
与外界数据通信需要IO流实现,例如内存与硬盘之间通信、服务器与客户端通信、压缩/解压缩
-
Java为IO流提供了原始类
InputStream
/OutputStream
,根据传输的数据类型不同,衍生出了许多子类- 文件字节流:针对于一般的文件类型
- 文件字符流:针对于纯文本的文件类型
- 数据字节流:针对于Java基本数据类型
- 转换字符流:针对于字符串类型的数据
- 缓冲字节/字符流:针对于数据量较大的场景
- 打印字节/字符流:针对于只需要输出打印字符串的场景
- 压缩/解压缩字节流:针对于压缩包文件类型
-
常见IO流类线程安全问题
流类型 常见类 线程安全? 关键原因 文件流 FileInputStream
、FileOutputStream
否 多线程同时读写同一文件流会导致数据混乱或文件指针冲突。 缓冲流 BufferedInputStream
、BufferedOutputStream
否 内部缓冲区非线程安全,多线程同时操作会导致数据错误。 打印流 PrintStream
(如System.out
)、PrintWriter
部分 PrintStream
的print()
/println()
线程安全,但write()
不安全;PrintWriter
不安全。转换流 InputStreamReader
、OutputStreamWriter
否 字符编码转换逻辑非线程安全,多线程同时操作会导致编码错误。 数据流 DataInputStream
、DataOutputStream
否 读写基本数据类型的方法非线程安全,多线程同时操作会导致数据解析错误。 Socket 流 Socket.getInputStream()
、Socket.getOutputStream()
否 Socket 流是网络通信的通道,多线程同时读写会导致数据混乱或协议错误。 压缩流 GZIPInputStream
、GZIPOutputStream
否 它们的设计基于单线程流式操作,内部状态(如压缩/解压缩缓冲区、位流指针)在多线程环境下会被并发
字符流与字节流
-
字节和字符
- 字节是一种二进制码,计算机底层只有二进制形式的数据
- 其他各种类型数据都是根据一定规则由字节码转换而来的“虚假数据”,真正的数据类型只有字节
- 字符是为了可读性,根据字符编码规则将字节码转换而来的一种文本类型
-
**字符和字节的转换原理:
字符 <---> Unicode <--编码规则--> 字节/字节数组 <---> 内存
**- 同一个字符的Unicode是唯一的,字节码因不同的编码可能不一致
- 字节流不需要考虑编码,字符流要注意编码问题
-
常见字符编码规则
- ASCII编码:仅包含英文及符号,一个英文字母对应一个字节(
'a' --- 97
) - GBK编码:包含汉字,兼容ASCII,一个中文对应长度为2的字节数组
- UTF-8编码(推荐):支持全球语言,兼容ACII,一个中文对应长度为3的字节数组(
'你' --- [-28, -67, -96]
)
- ASCII编码:仅包含英文及符号,一个英文字母对应一个字节(
文件流
- 文件写的方式
- 追加写:创建输出流时,如果文件存在且已有数据,则接着数据最后写
- 覆盖写:创建输出流时,如果文件存在且已有数据,则覆盖原有数据重新写
文件字节流
class FileInputStream extends InputStream{ //文件字节输入流public FileInputStream(String name) {...} //获取构造输入流,name是文件路径public FileInputStream(File file) {...} //获取构造输入流,file是文件对象public int read() {...} //每次读取一个字节,每调用一次都会读取下一个字节,返回获取的字节值,到流末就返回-1public int read(byte[] buffer) {...} //每次读取一组字节,更新在buffer中,返回字节数,到流末就返回-1public byte[] readAllBytes() {...} //读取所有字节保存在一个字节数组中,返回此数组,要求jdk9以上public void close() {...} //IO流使用完毕一定要关闭
}
class FileOutputStream extends OutputStream{ //文件字节输出流public FileOutputStream(String name) {...} //name是文件路径,不需要手动创建文件,但不能创建目录public FileOutputStream(String name, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写public FileOutputStream(File file) {...} //获取构造输出流,file是文件对象public FileOutputStream(File file, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写public void write(int b) {...} //写入一个字节,入参也可以是英文字母,其他符号字符会乱码public void write(byte b[], int off, int len) {...} //写入字节数组中的一部分,配合一次读取一组字节使用public void close() {...} //IO流使用完毕一定要关闭
}
举例:备份小文件
现有文件user.txt,要求对此文件做备份,备份文件名为userBackUp
try (FileInputStream fileInputStream = new FileInputStream("user.txt");FileOutputStream fileOutputStream = new FileOutputStream("userBackUp.txt")) {byte[] bytes = new byte[512]; //每次读取0.5kb字节while (true) {int len = fileInputStream.read(bytes);if (len == -1){break;}fileOutputStream.write(bytes,0,len); //只移动数据,不对数据有改动,不会乱码}fileOutputStream.write("\r\n".getBytes()); //最后换行
}
文件字符流
public class FileReader extends InputStreamReader{ //文件输入字符流public FileReader(String fileName) {...} //构造方法,参数是文件路径,默认编码规则与系统编码一致public FileReader(File file) {...} //构造方法,参数是文件对象public int read() {...} //一次读取一个字符,返回Unicode值,到流末返回-1public int read(char cbuf[]) {...} //一次读取一组字符,返回实际读取到的字符数,到流末返回-1public void close() {...} //IO流使用完毕一定要关闭
}
public class FileWriter extends InputStreamWriter{ //文件输出字符流public FileReader(String fileName) {...} //构造方法,参数是文件路径public FileReader(String name, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写public FileReader(File file) {...} //构造方法,参数是文件对象public FileReader(File file, boolean append) {...} //设置输出流写的方式是追加写,默认覆盖写public void write(int c) {...} //写入一个字符,入参是Unicode值public void write(char cbuf[], int off, int len) {...} //写入字符数组的一部分,配合一次读取一组字符使用public void write(String str) {...} //写入字符串public void write(String str, int off, int len) {...} //写入字符串一部分public void flush() {...} //刷新输入流,写入数据后,必须刷新或者关闭输入流,写入操作才会真正生效public void close() {...} //IO流使用完毕一定要关闭
}
举例:去除文件中的汉字
try (FileReader reader = new FileReader("properties.txt");FileWriter fileWriter = new FileWriter("newProperties.txt", true)) {while (true) {int read = reader.read();char c = (char) read;if (read == -1) {break;}if (c >= '\u4E00' && c <= '\u9FA5') { //字符合法才写入fileWriter.write(read);fileWriter.flush(); //可以实时显示写入情况}}
} catch (IOException e) {e.printStackTrace();
}
缓冲流
-
目的:极大地提高了原IO流的读取性能
-
原理:缓冲机制
- 内部维护一个字节数组(默认大小通常为 8KB),通过批量读取数据到缓冲区,减少 I/O 操作次数
- 普通字节流一次读取一定会触发IO接口,缓冲流只有缓冲区满了才会触发
- 小量数据情况下,缓冲流效率不一定比普通字节流高
-
缓冲流应用场景
- 数据量较大时建议加上缓冲流
- 如果需要一行一行地读数据时,用缓冲流的
readLine()
方法
字节缓冲流
public class BufferedInputStream{ //缓冲字节输入流:必须包装另一个 InputStream(如 FileInputStream),不能直接使用public BufferedInputStream(InputStream in) {...} //构造缓冲输入流,默认缓冲8kbpublic BufferedInputStream(InputStream in, int size) {...} //设置缓冲区大小public int read() {...} //读取一个字节public int read(byte b[]) {...} //读取一组字节public void close() {...} //包装的流都会关闭,执行一次即可
}
public class BufferOutputStream{ //缓冲字节输出流:必须包装另一个 OutputStream(如 FileOutputStream),不能直接使用public BufferedOutputStream(OutputStream out) {...} //构造缓冲输出流,默认缓冲8kb,写方式由OutputStream指定public BufferedOutputStream(OutputStream out, int size) {...} //设置缓冲区大小public void write(int b) {...}public void write(int b) {...} //写入一个字节,入参是ASCII码public void write(byte b[], int off, int len) {...} //写入字节数组中的一部分public void flush() {...} //刷新输入流,写入数据后,必须刷新或者关闭输入流,写入操作才会真正生效public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:备份大文件
try (BufferedInputStream bufferIn = new BufferedInputStream(new FileInputStream("E:"+File.separator+"长视频.mp4"));
BufferedOutputStream bufferOut = new BufferedOutputStream(new FileOutputStream("长视频备份.mp4"))) {byte[] bytes = new byte[2048];while (true){int len = bufferIn.read(bytes);if(len == -1){break;}bufferOut.write(bytes,0,len);}
} catch (IOException e) {throw new RuntimeException(e);
}
字符缓冲流
BufferedReader.readLine()
方法不会读取换行符,此方法的返回值作为写入数据时,需要手动换行或者加上换行符
public class BufferedReader{ //字符缓冲输入流:必须包装一个 `Reader` 对象(如 `FileReader`、`InputStreamReader`)public BufferedReader(Reader in) {...} //缓冲输入流构造方法,默认缓冲8kb,编码由Reader决定public BufferedReader(Reader in, int sz) {...} //设置缓冲区大小,单位bytepublic String readLine() {...} //读取一行字符串直至换行符(不含),返回内容,到流末返回nullpublic int read() {...} //一次读取一个字符,返回Unicode值,到流末返回-1public int read(char cbuf[]) {...} //一次读取多个字符,返回实际读取到的字符数,到流末返回-1public void close() {...} //包装的流都会关闭,执行一次即可
}
public class BufferWriter{ //字符缓冲输出流:必须包装一个 `Writer` 对象(如 `FileWriter`、`OutputStreamWriter`)public BufferedWriter(Writer out) {...} //缓冲输出流构造方法,默认缓冲8kb,编码、写入方式由Reader决定public BufferedWriter(Writer out, int sz) {...} //设置缓冲区大小,单位bytepublic void write(int c) {...} //写入一个字符,入参是Unicode值public void write(char cbuf[], int off, int len) {...} //写入字符数组的一部分public void write(String str) {...} //写入字符串public void write(String str, int off, int len) {...} //写入字符串一部分public void newLine() {...} //换行,相当于\n\rpublic void flush() {...} //刷新输入流,写入数据后,必须刷新或者关闭输入流,写入操作才会真正生效public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:删除配置文件注释
String inputFile = "config.yml";
String outputFile = "simpleConfig.yml"; //将删除注释后的文件保存到simpleConfig.yml
try (BufferedReader reader = new BufferedReader(new FileReader(inputFile));BufferedWriter writer = new BufferedWriter(new FileWriter(outputFile))) {String line;while (true) {line = reader.readLine();if (line == null) {break;}//注释行或者空行直接跳过if (line.trim().startsWith("#") || line.trim().isEmpty()) {continue;}//移除行内注释(假设注释以 # 开头)String cleanedLine = line.replaceAll("#.*", "");if (!cleanedLine.isEmpty()) {writer.write(cleanedLine);writer.newLine(); //readLine不读取换行符,需要手动换行}}
} catch (IOException e) {throw new RuntimeException(e);
}
打印流
-
打印流是文件输出流的包装,意在更方便地进行数据输出,例如记录日志
- 可以设置自动刷新
- 可以快接地格式化输出字符串
- 打印流内部包装了缓冲流,效率也很高
-
使用场景:只需快速打印文本类型数据时建议使用打印流
-
重定向打印流
sout
方法本质上就是使用PrintStream
打印流,只不过是打印到控制台里- 重定向打印流后,
sout
就不会打印在控制台,而是打印到指定的文件中
public final class System {public final static PrintStream out = null; //JVM启动时会调用静态代码块将out初始化到控制台public static void setOut(PrintStream out) {...} //重定向打印流 }
字节打印流
public class PrintStream extends FilterOutputStream{ //打印流必须包装一个已有的输出流,不能直接使用public PrintStream(OutputStream out) {...} //构造方法,默认不立即刷新,打印编码UTF-8public PrintStream(OutputStream out, boolean autoFlush) {...} //一次打印完是否立即刷新public PrintStream(OutputStream out, boolean autoFlush, String encoding) {...} //如果直接打印字符串,可以指定编码public void println(XXX x) {...} //打印x数据字符串形式(x可以是任意类型)对应的字节,并换行public PrintWriter printf(String format, Object ... args) {...} //格式化字符串并打印public void write(int b/byte b[], int off, int len) {...} //打印字节public void close() {...} //包装的流都会关闭,执行一次即可
}
字符打印流
public class PrintWriter{ //打印流必须包装一个已有的输出流,不能直接使用public PrintWriter (Writer out) {...} //构造方法,默认不立即刷新,打印编码UTF-8public PrintWriter(Writer out, boolean autoFlush) {...} //指定打印完立即刷新public PrintStream(OutputStream out, boolean autoFlush, String encoding) {...} //可以指定编码public void println(XXX x) {...} //打印x数据字符串形式(x可以是任意类型),并换行public void write(int c/char cbuf[], int off, int len) {...} //打印字符public void write(String str/String str, int off, int len) {...} //打印字符串public PrintWriter printf(String format, Object ... args) {...} //格式化字符串并打印public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:记录日志
public class LogHelper {private static final PrintWriter writer;private static final String fileName = "myProject.log";static {try {writer = new PrintWriter(new FileWriter(fileName, true), true);} catch (IOException e) {throw new RuntimeException(e);}}public static void info(String s) {//时间 [等级] 日志记录位置: 日志内容writer.printf("%s [INFO] %s: %s%n",LocalDateTime.now(),Thread.currentThread().getStackTrace()[2], //2游标返回上一级调用位置s);}public static void warn(Exception e) {//时间 [等级] 异常处理位置\n 异常调用链writer.printf("%s [WARN] %s%n",LocalDateTime.now(),Thread.currentThread().getStackTrace()[2]);e.printStackTrace(writer); //可以指定输出流}
}//效果
2025-06-19T19:21:40.961 [INFO] com.wyh.utils.Check.method(Check.java:14): 流程关键节点日志,方便后期维护
2025-06-19T19:21:40.970 [WARN] com.wyh.utils.Check.main(Check.java:9)
java.lang.RuntimeException: 原始异常at com.wyh.utils.Check.origin(Check.java:18)at com.wyh.utils.Check.method(Check.java:15)at com.wyh.utils.Check.main(Check.java:7)
转换流
-
目的:处理编码不一致的问题
-
转换原理
- 转换流是从字节流包装而来的字符流
外界--->编码规则--->内存--->编码规则--->外界
-
应用场景
- 跨平台文本处理时编码可能不一致
- 读写编码不一致的文本文件
- 与数据库进行数据交互时编码可能不一致
转换输入流
- 转换流必须包装一个已有的字节流(如
FileInputStream
),不能直接使用
public class InputStreamReader{public InputStreamReader(InputStream in) {...} //读取数据,默认UTF-8public InputStreamReader(InputStream in, Charset cs) {...} //读取数据,并声明该数据的编码public InputStreamReader(InputStream in, String charsetName) {...} //指定原数据的编码public int read() {...} //一次读取一个字符,返回Unicode值,到流末返回-1public int read(char cbuf[]) {...} //一次读取一组字符,返回实际读取到的字符数,到流末返回-1public void close() {...} //包装的流都会关闭,执行一次即可
}
转换输出流
- 转换流必须包装一个已有的字节流(如
FileOutputStream
),不能直接使用
public class OutputStreamWriter{public OutputStreamWriter(OutputStream out) {} //写数据,该数据默认以UTF-8格式转换为字节保存public OutputStreamWriter(OutputStream out, Charset cs) {} //可以指定写数据的编码方式public OutputStreamWriter(OutputStream in, String charsetName) {...} //可以指定写数据的编码方式public void write(int c) {...} //写入一个字符,入参是Unicode值public void write(char cbuf[], int off, int len) {...} //写入字符数组的一部分,配合一次读取一组字符使用public void write(String str) {...} //写入字符串public void write(String str, int off, int len) {...} //写入字符串一部分public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:结合缓冲流转换编码格式
老项目文件old.txt是GBK格式,现要求转换为UTF-8格式文件modern.txt
//缓冲流在最外层
try(InputStreamReader input = new InputStreamReader(new FileInputStream("old.txt"), "GBK");BufferedReader reader = new BufferedReader(input);OutputStreamWriter output = new OutputStreamWriter(new FileOutputStream("modern.txt"), StandardCharsets.UTF_8);BufferedWriter writer = new BufferedWriter(output)) {while (true){char[] chars = new char[1024];int len = reader.read(chars);if (len == -1){break;}writer.write(chars,0,len);}
}
数据流
- 目的:一种字节流,传输Java基本数据类型
数据输入流
public class DataInputStream{public DataInputStream(InputStream in) {...} //必须包装一个已有的输入流public final boolean readBoolean() {...} //读取布尔类型数据并返回public final byte readByte() {...} //读取Java字节类型数据并返回public final int readInt() {...} //读取整数类型数据并返回public final double readDouble() {...} //读取浮点类型数据并返回public final String readUTF() {...} //根据UTF-8编码,将字节转换为字符串,并返回public void close() {...} //包装的流都会关闭,执行一次即可
}
数据输出流
public class DataOutputStream{public DataOutputStream(OutputStream out) {...} //必须包装一个已有的输入流public final void writeByte(int v) {...} //写入Java字节型数据public final void writeBoolean(boolean v) {...} //写入布尔型数据public final void writeInt(int v) {...} //写入整型数据public final void writeDouble(double v) {...} //写入浮点型数据public final boolean readUTF(String str) {...} //根据UTF-8,将字符串转化为字节数组写入内存public void close() {...} //包装的流都会关闭,执行一次即可
}
举例:数据流必须保证输入输出顺序一致
- 使用数据流,必须保证数据类型写入顺序和读取顺序一致,否则会解析出错误的数据
try (DataOutputStream dos = new DataOutputStream(new FileOutputStream("data_stream.bin"))) {// 写入布尔值和整型值dos.writeBoolean(true); // 写入布尔值dos.writeInt(123); // 写入整型值dos.writeBoolean(false); // 写入另一个布尔值dos.writeInt(456); // 写入另一个整型值
} catch (IOException e) {e.printStackTrace();
}
// 从文件读取数据
try (DataInputStream dis = new DataInputStream(new FileInputStream("data_stream.bin"))) {// 读取布尔值和整型值(顺序必须与写入一致)System.out.println(dis.readInt()); //顺序不一致,读出来的结果变成了16777216 System.out.println(dis.readBoolean()); System.out.println(dis.readBoolean()); System.out.println(dis.readInt());
} catch (IOException e) {e.printStackTrace();
}
压缩/解压缩流
- 目的:压缩/解压文件,如果仅复制压缩文件用文件流即可
- 原理:将文件的字节序列中相同部分“合并”,极大地减少了空间消耗
ZipEntry
ZipEntry
类是表示 ZIP 文件中的一个条目,可以是文件或目录,通过名称中的分隔符识别文件层级
public class ZipEntry{public ZipEntry(String name) {...} //构造方法,入参以/结尾就是目录条目,否则为文件条目public String getName() {...} //返回条目基于zip根目录的相对路径public long getSize() {...} //获取条目未压缩的大小public long getCompressedSize() {...} //获取条目压缩后的大小public boolean isDirectory() {...} //条目是否为目录条目
}
压缩输入流(解压)
public class ZipInputStream{public ZipInputStream(InputStream in) {...} //必须包装一个已有的输入流,默认编码UTF-8public ZipInputStream(InputStream in, Charset charset) {...} //可以设置压缩文件编码格式public ZipEntry getNextEntry() {...} //获取压缩目录中的当前条目(目录/文件),每次调用就会读取下一个条目,直到返回nullpublic int read(byte[] b) {...} //解压当前文件条目,每次读取一组字节,结束返回-1,不能用于目录条目,在getNextEntry后用public void closeEntry() {...} //关闭当前条目public void close() {...} //IO流使用完毕一定要关闭
}
压缩输出流(压缩)
public class ZipOutputStream{public ZipOutputStream(OutputStream out) {...} //必须包装一个已有的输出流,默认编码UTF-8public ZipOutputStream(OutputStream out, Charset charset) {...} //可以设置压缩文件编码格式public void putNextEntry(ZipEntry e) {...} //开始写入一个新的ZIP条目,文件或目录由入参ZipEntry决定public void write(byte[] b, int off, int len) {...} //写入条目的数据,在putNextEntry文件条目后使用public void closeEntry() {...} //结束写入当前条目public void close() {...} //IO流使用完毕一定要关闭
}
压缩炸弹
-
压缩炸弹(Zip Bomb)是一种恶意构造的压缩文件,它解压前并不大,但一旦解压会变超级大,严重的可能会导致相同崩溃
-
压缩炸弹防护措施
- 在解压之前,检查压缩文件的大小
- 在解压过程中,监控解压后的文件大小,可以随时终止解压
- 使用经过验证的第三方库(如Apache Commons Compress)可以提供更多的安全功能和选项来防止压缩炸弹
private static final long MAX_ZIP_SIZE = 100 * 1024 * 1024; // 解压后一旦超过100MB就报异常public static void checkZipFile(File zipFile) throws IOException { //方法一:解压前检查压缩包大小try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile))) {ZipEntry entry;long totalSize = 0; //压缩包大小计数器while ((entry = zis.getNextEntry()) != null) { //遍历压缩目录if (entry.isDirectory()) {continue;}totalSize += entry.getCompressedSize();if (totalSize > MAX_ZIP_SIZE) {throw new IOException("Zip file is too large");}zis.closeEntry();}}
}public static void unzip(File zipFile, File srcFile) throws IOException { //方法二:边解压边检查try (FileInputStream fis = new FileInputStream(zipFile);ZipInputStream zis = new ZipInputStream(fis)) {ZipEntry entry;long totalSize = 0; //压缩包大小计数器while ((entry = zis.getNextEntry()) != null) {if (entry.isDirectory()) { continue;}try (FileOutputStream fos = new FileOutputStream(new File(srcFile, entry.getName()))) {byte[] buffer = new byte[1024];int len;while ((len = zis.read(buffer)) != -1) {fos.write(buffer, 0, len);totalSize += len;if (totalSize > MAX_UNZIPPED_SIZE) {throw new IOException("Unzipped size is too large");}}zis.closeEntry();}}}
}
举例:备份压缩日志
- 开启压缩流:流关闭前所有写入的条目都在此目录中
- 开启条目
- 在文件条目下写入数据
- 关闭流:压缩完成
try (ZipOutputStream zipOutputStream = new ZipOutputStream("backup.zip"); //所有写入的文件都会在backup.zip压缩包中FileInputStream fileInputStream = new FileInputStream("log.txt")) {LocalDateTime t = LocalDateTime.now(); //将日期作为压缩包内的层级依据String path = t.getYear() + File.separator + t.getMonth().getValue() + File.separator + t.getDayOfMonth();zipOutputStream.putNextEntry(new ZipEntry(path + File.separator + "log.txt")); //开启条目,准备写入log.txt文件byte[] bytes = new byte[1024];while (true) { //写入数据int read = fileInputStream.read(bytes);if (read == -1) {break;}zipOutputStream.write(bytes);}zipOutputStream.closeEntry();
}
举例:压缩指定类型文件
压缩一个多层级目录,目录中有各种各样的文件,现只保留.mp4文件
public static void main(String[] args) throws IOException {File outputFile = new File("E:\\video.zip");File inputFile = new File("E:\\JavaCourse\\02-二阶段");try (BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(new FileOutputStream(outputFile));ZipOutputStream zipOutputStream = new ZipOutputStream(bufferedOutputStream)) { //再包一个缓冲流,提升性能startZip(inputFile, zipOutputStream, ""); //压缩流输出抽离在外面,避免因递归频繁地开启关闭压缩流} catch (IOException e) {throw new RuntimeException(e);}
}private static void startZip(File inputFile, ZipOutputStream zipOutputStream, String parentPath) throws IOException {if (!inputFile.isDirectory() && inputFile.getName().endsWith(".mp4")) { //只压缩视频文件byte[] bytes = new byte[2048];int len;zipOutputStream.putNextEntry(new ZipEntry(parentPath + File.separator + inputFile.getName()));try (FileInputStream fileInputStream = new FileInputStream(inputFile);BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream)) {while ((len = bufferedInputStream.read(bytes)) != -1) {zipOutputStream.write(bytes, 0, len);}}zipOutputStream.closeEntry();return;}if (inputFile.isDirectory()) {parentPath = parentPath + File.separator + inputFile.getName(); //是目录,将目录层级记录下来,保证结构一致for (File f : inputFile.listFiles()) {startZip(f, zipOutputStream, parentPath);}}
}
序列化
-
目的:传输Java对象类型的数据
-
原理
- 目前没有针对Java对象类型的IO流,因此需要将对象转换为字节码/字符串,再通过对应的IO流传输
- 序列化仅指此转换的过程,后续IO流操作的实现另说
-
序列化分类
- Java原生序列化:使用字节流(如Socket流)
- Json序列化:对象转换为Json格式字符串,进而通过字符流实现
-
应用场景
- 原生序列化支持所有对象(
Thread
、Socket
、File
等类只能使用原生序列化),但性能、可读性低、有安全风险; - Json序列化性能高,可读性高,适合绝大部分场景
- 原生序列化支持所有对象(
JDK原生序列化
-
JDK原生序列化是将Java对象转换为字节数组
-
开启原生序列化:让一个类可序列化,只需实现
java.io.Serializable
接口即可@Data @AllArgsConstructor @NoArgsConstructor public class User implements Serializable {private Long id;private String name; }
-
如果没有开启序列化,直接将对象通过字节流传输,会报
NotSerializableException
异常public class Main {public static void main(String[] args) {User user = new User(30L,"Alice"); //假设User类没有实现Serializabletry (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {oos.writeObject(user);} catch (IOException e) {e.printStackTrace();}} }java.io.NotSerializableException: com.wyh.entity.User //序列化异常at java.io.ObjectOutputStream.writeObject0(ObjectOutputStream.java:1184)at java.io.ObjectOutputStream.writeObject(ObjectOutputStream.java:348)at Main.main(Main.java:24)
Json序列化
-
Json序列化本质是将对象转化为Json格式的字符串,常见第三方库有Jackson、Gson、FastJson
-
Jackson:spring生态默认封装Jackson
<dependency><groupId>com.fasterxml.jackson.core</groupId> <!--如果是spring boot项目会自带--><artifactId>jackson-databind</artifactId><version>2.15.2</version> <!-- 使用最新版本 --> </dependency>
/* **Json属性映射 1. @JsonProperty:设置映射别名 2. @JsonInclude:什么情况下不序列化此成员 3. @JsonFormat:对于时间数据类型,设置输出时间格式 4. @JsonIgnore:json序列化时忽略此成员 */ @Data @ToString @AllArgsConstructor public class User{@JsonProperty("id") //格式化后的json数据的对应key就是"ID"private Integer userId;@JsonInclude(JsonInclude.Include.NON_NULL) //姓名为空就忽略姓名private String userName;@JsonIgnore //不转换status属性private Integer status;@JsonFormat(pattern = "yyyy-MM-dd hh:mm:ss",locale = "zh") //指定格式,并指定时区为中国private Date time; }
public class ObjectMapper { // ObjectMapper:Jackson 的核心类public String writeValueAsString(Object object) {...} //转换为JSON字符串public <T> T readValue(String text, Class<T> clazz) {...} //根据class将JSON字符串反序列化为Java实例public <T> T readValue(JsonParser p, TypeReference<T> valueTypeRef) {...} //多个实例时可以反序列化为对象列表 }ObjectMapper objectMapper = new ObjectMapper(); User user = objectMapper.readValue("jsonStr",User.class); Map<String, String> map = objectMapper.readValue("jsonStr", new TypeReference<Map<String,String>>() {});
-
FastJson:当前最流行三方库
<dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version> <!-- 使用最新稳定版本 --> </dependency>
/* **Json属性映射@JSONField注解 1. name:映射别名 2. format:对于时间数据类型,设置输出时间格式 3. serialize:是否序列化 4. deserialize:是否反序列化 5. jsonDirect:是否直接序列化字段值(跳过 getter/setter,默认 false) */ public class User {@JSONField(name = "user_name") // 序列化为 JSON 时的字段名private String name;@JSONField(format = "yyyy-MM-dd") // 日期格式化private Date birthday; }
public abstract class JSON {public static String toJSONString(Object object) {...} //转换为JSON字符串public static <T> T parseObject(String text, Class<T> clazz) {...} //根据class将JSON字符串反序列化为Java实例public static <T> T parseObject(String text, TypeReference<T> type) {...} // 根据type将字符串转换为泛型,如Mappublic static <T> List<T> parseArray(String text, Class<T> clazz) {...} //多个实例时可以反序列化为对象列表 }User user = JSON.parseObject("jsonStr",User.class); Map<String, String> map = JSON.parseObject("jsonStr", new TypeReference<Map<String,String>>() {});
Apache Commons Lang 3
-
Apache Commons Lang 3 是 Apache Commons 的工具库,提供了大量用于简化 Java 开发的实用方法,比JDK原生API更好用
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.18.0</version> </dependency>
StringUtils
StringUtils
比String
类更安全、功能更强大, 避免NullPointerException
方法 | 说明 |
---|---|
StringUtils.isEmpty(str) | 检查字符串是否为 null 或空("" ) |
StringUtils.isBlank(str) | 检查字符串是否为 null 、空或仅包含空白字符 |
StringUtils.trim(str) | 去除字符串首尾空白字符 |
StringUtils.abbreviate(str, maxWidth) | 截断字符串(超出部分用 ... 代替) |
StringUtils.capitalize(str) | 首字母大写 |
StringUtils.uncapitalize(str) | 首字母小写 |
StringUtils.join(array, delimiter) | 拼接数组为字符串 |
StringUtils.split(str, delimiter) | 按分隔符拆分字符串 |
StringUtils.replace(str, searchStr, replacement) | 替换字符串 |
StringUtils.contains(str, searchStr) | 检查是否包含子串 |
StringUtils.substringBetween(str, open, close) | 提取两个标记之间的子串 |
ObjectUtils
方法 | 说明 |
---|---|
ObjectUtils.defaultIfNull(obj, defaultValue) | 如果对象为 null ,返回默认值 |
ObjectUtils.firstNonNull(obj1, obj2, ...) | 返回第一个非 null 对象 |
ObjectUtils.equals(obj1, obj2) | 安全比较两个对象是否相等 |
ObjectUtils.hashCode(obj) | 安全计算对象的 hashCode |
ObjectUtils.toString(obj) | 安全转换为字符串(null 返回空字符串) |
ObjectUtils.min(a, b, c...) / ObjectUtils.max(a, b, c...) | 返回最小/最大值(支持 Comparable ) |
ArrayUtils
方法 | 说明 |
---|---|
ArrayUtils.isEmpty(array) | 检查数组是否为 null 或空 |
ArrayUtils.contains(array, element) | 检查数组是否包含元素 |
ArrayUtils.add(array, element) | 向数组末尾添加元素 |
ArrayUtils.remove(array, index) | 移除指定索引的元素 |
CollectionUtils
ObjectUtils
提供对Object
的安全操作,避免NullPointerException
方法 | 说明 |
---|---|
CollectionUtils.isEmpty(collection) | 检查集合是否为 null 或空 |
CollectionUtils.isNotEmpty(collection) | 检查集合是否非空 |
CollectionUtils.union(a, b) | 合并两个集合 |
CollectionUtils.intersection(a, b) | 取两个集合的交集 |
CollectionUtils.filter(collection, predicate) | 过滤集合 |
EnumUtils
- 简化枚举的操作,包括枚举值的解析、验证、获取枚举列表等
方法 | 说明 |
---|---|
EnumUtils.getEnumIgnoreCase(Class<E> enumClass, String enumName) | 根据名称安全获取枚举(不区分大小写) |
EnumUtils.getEnum(Class<E> enumClass, String enumName) | 根据名称安全获取枚举(区分大小写) |
EnumUtils.getEnumMap(Class<E> enumClass) | 转为Map ,key为成员名称,value为枚举成员 |
ExceptionUtils
ExceptionUtils
提供异常堆栈信息的提取和格式化
方法 | 说明 |
---|---|
ExceptionUtils.getRootCause(throwable) | 获取异常的根原因 |
ExceptionUtils.getStackTrace(throwable) | 获取异常堆栈字符串 |
reflect
- 封装了 Java 原生反射 API 的复杂操作,简化了字段、方法、构造器的访问与调用,减少
NullPointerException
风险
方法签名 | 功能描述 |
---|---|
Field getField(Class<?> cls, String fieldName) | 获取类及其父类的公共字段(包括继承的) |
Field getDeclaredField(Class<?> cls, String fieldName) | 获取类自身的任意字段(包括私有,但不包括继承的) |
Object readField(Field field, Object target) | 读取字段值(自动处理权限) |
void writeField(Field field, Object target, Object value) | 修改字段值(自动处理权限) |
List<Field> getAllFieldsList(Class<?> cls) | 获取类及其父类的所有字段(按继承顺序返回) |
方法签名 | 功能描述 |
---|---|
Method getAccessibleMethod(Method method) | 确保方法可访问(处理私有方法) |
Object invokeMethod(Object object, String methodName, Object... args) | 调用方法(自动匹配参数类型,支持继承链) |
Object invokeExactMethod(Object object, String methodName, Object... args) | 精确匹配参数类型调用方法 |
List<Method> getAllMethodsList(Class<?> cls) | 获取类及其父类的所有方法(按继承顺序返回) |
方法签名 | 功能描述 |
---|---|
Object invokeConstructor(Class<?> cls, Object... args) | 通过参数创建实例(自动匹配构造器) |
Constructor<?> getAccessibleConstructor(Class<?> cls, Class<?>... parameterTypes) | 获取可访问的构造器 |