Java学习笔记03

Scanner 类

Scannerjava.util 包下的工具类,用来读取控制台输入、读取文件、字符串数据,最常用于接收键盘输入。

需要导包:import java.util.Scanner;

1. 基本使用

import java.util.Scanner;

public class Test {
public static void main(String[] args) {
// 1. 创建Scanner对象,接收键盘输入
Scanner sc = new Scanner(System.in);

System.out.print("请输入一个整数:");
int num = sc.nextInt(); // 读取int整数

System.out.println("你输入的数字:" + num);

sc.close(); // 关闭扫描器,释放资源
}
}

2. 常用方法

方法 作用
nextInt() 读取int 整数
nextDouble() 读取小数 double
nextBoolean() 读取布尔 true/false
next() 读取字符串,遇到空格停止
nextLine() 读取一整行字符串,包含空格

next() 和 nextLine() 重点区别

  • next():读到空格、Tab、回车就结束,不能获取带空格句子
  • nextLine():读到回车结束,可以读取带空格完整一行

坑:nextInt()之后直接调用nextLine()会读到空字符串! 原因:nextInt()只读取数字,遗留回车符留在缓冲区,nextLine 直接读到回车返回空。

解决办法:

int a = sc.nextInt();
sc.nextLine(); // 吃掉遗留的回车
String s = sc.nextLine();

3. hasNext() 判断是否还有输入

循环读取输入,判断是否有数据:

Scanner sc = new Scanner(System.in);
while(sc.hasNext()){
String str = sc.next();
System.out.println(str);
}

4. Scanner 读取文件(拓展)

// Scanner sc = new Scanner(new File("test.txt"));

5. 常见报错

  1. InputMismatchException:输入的数据类型不匹配,比如要 int,你输入字母。
  2. 忘记sc.close():不会报错,但会资源泄漏。
  3. nextInt 后 nextLine 得到空字符串:上面说的缓冲区回车问题。

枚举和注解

枚举

枚举是一种特殊类,用来固定、列出有限、确定的常量。

适合场景:季节、星期、订单状态、性别、方向。

解决普通常量public static final的缺陷(无法限制传入非法值)。

语法定义:

enum Season {
// 枚举常量(必须写在最前面)
SPRING, SUMMER, AUTUMN, WINTER;
}

等价理解:每个常量都是 Season静态常量实例对象

枚举的实现方式

  1. 自定义类实现枚举

    1) 构造器私有化
    2) 本类内部创建一组对象
    3) 对外暴露对象(通过为对象添加 public final static 修饰符)
    4) 可以提供 get 方法,但是不要提供 set

  2. 使用enum关键字实现枚举

    1) 当我们使用 enum 关键字开发一个枚举类时,默认会继承 Enum 类, 而且是一个 final 类

    2) 传统的 public static final Season2 SPRING = new Season2("春天", "温暖"); 简化成 SPRING("春天", "温暖"), 这里必须知道,它调用的是哪个构造器.

    3) 如果使用无参构造器 创建 枚举对象,则实参列表和小括号都可以省略
    4) 当有多个枚举对象时,使用,间隔,最后有一个分号结尾
    5) 枚举对象必须放在枚举类的行首

    enum 定义的类,编译器自动处理:

    1. 隐式继承 java.lang.Enum不能再继承其他类
    2. 所有枚举实例:public static final
    3. 构造方法强制私有 private,就算不写也是 private,禁止外部new

    误区:不能手动写 extends;构造方法不能用 public /protected

    示例

    enum Season {
    //常量对象,调用构造方法
    SPRING("春天", "温暖"),
    SUMMER("夏天", "炎热"),
    AUTUMN("秋天", "凉爽"),
    WINTER("冬天", "寒冷");

    //成员变量
    private String name;
    private String desc;

    //【构造方法:默认private,不能写public】
    Season(String name, String desc) {
    this.name = name;
    this.desc = desc;
    }

    //getter,不能提供setter(枚举常量一般不可修改)
    public String getName() {
    return name;
    }
    public String getDesc() {
    return desc;
    }
    }

    Enum 自带核心方法

    1. values()

    返回枚举所有常量的数组,用于遍历

    Season[] arr = Season.values();
    for(Season s : arr){
    System.out.println(s);
    }
    1. valueOf(String name)

    根据名字字符串获取枚举常量;名字不存在抛出异常IllegalArgumentException

    Season s = Season.valueOf("SPRING");
    1. ordinal()

    返回常量序号,从 0 开始(定义顺序)

    System.out.println(Season.SPRING.ordinal()); //0
    1. name() / toString()
    • name():返回常量定义名称(SPRING),final 无法重写
    • toString():默认和 name() 一样,可以重写自定义输出
    System.out.println(Season.SPRING.name()); //SPRING

    枚举可以拥有的特性

    构造方法(只能 private)

    成员变量、普通方法、静态方法

    抽象方法(每个枚举常量必须实现!)

    可以实现接口

    不能继承其他类(隐式继承 Enum,Java 单继承)

    外部无法 new 枚举对象

    每个枚举实例单独实现方法:

    enum Season {
    SPRING{
    @Override
    public void show() {
    System.out.println("春暖花开");
    }
    },
    SUMMER{
    @Override
    public void show() {
    System.out.println("夏日炎炎");
    }
    };
    //抽象方法
    public abstract void show();
    }

    高频易错总结

    1. 枚举构造器只能 private,省略不写也是 private
    2. 枚举常量必须放在类最开头,分号可选(后面有代码必须加分号)
    3. 枚举不能被继承;不能继承别的类,可以实现接口
    4. valueOf() 参数名称错误会抛运行异常
    5. ordinal() 顺序依赖代码定义顺序,业务不要重度依赖!
    6. 枚举常量是单例,全局只有一个对象

注解

1) 注解(Annotation)也被称为元数据(Metadata),用于修饰解释 包、类、方法、属性、构造器、局部变量等数据信息。
2) 和注释一样,注解不影响程序逻辑,但注解可以被编译或运行,相当于嵌入在代码中的补充信息。
3) 在 JavaSE 中,注解的使用目的比较简单,例如标记过时的功能,忽略警告等。在 JavaEE 中注解占据了更重要的角色,例如用来配置应用程序的任何切面,代替 java EE 旧版中所遗留的繁冗代码和 XML 配置等。

注解 作用
@Override 标记方法是重写父类 / 接口方法;编译器校验,如果不是重写直接编译报错
@Deprecated 标记【已过时】元素,使用时编译器弹出警告
@SuppressWarnings("all") 压制编译警告;all 代表抑制全部警告,也可以指定 “unchecked” 等
@SafeVarargs JDK7,抑制可变泛型参数警告(了解)
@FunctionalInterface JDK8,标记函数式接口:接口只能有一个抽象方法,供 Lambda 使用

元注解(用来修饰【自定义注解】的注解)

元注解:注解的注解,专门定义自定义注解的行为,一共 5 个,重点 4 个

  1. @Target

    可选值(ElementType 枚举):

  • TYPE:类、接口、枚举
  • METHOD:方法
  • FIELD:成员变量
  • PARAMETER:方法参数
  • CONSTRUCTOR:构造方法
  • LOCAL_VARIABLE:局部变量
@Target({ElementType.METHOD,ElementType.TYPE})
  1. @Retention

指定注解,生命周期(保留到哪个阶段)

SOURCE  → 源码阶段,编译后丢弃(注释级别,仅编译器看见,class不存在)
CLASS → 编译保留在class,运行时JVM丢弃【默认策略】
RUNTIME → 运行时仍然保留,可以通过反射读取(自定义框架注解几乎都用这个!)

想在运行时通过反射获取注解信息,必须写:@Retention(RetentionPolicy.RUNTIME)

  1. @Documented

使用 javadoc 生成 API 文档时,把注解信息纳入文档

  1. @Inherited

可继承:父类标记该注解,子类自动继承这个注解

注意:只对【类】有效;方法、属性无效

  1. @Repeatable(JDK8):可重复注解(拓展了解)

自定义注解(自己创建注解)

语法:@interface

// 元注解约束
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
// 注解的【属性】,格式:类型 名称() [default 默认值];
String value() default "";
int age() default 18;
String[] tags() ;
}

使用自定义注解

// 只有一个属性名叫value,可以省略属性名
@MyAnnotation("测试")
public void func(){

}
// 多个属性
@MyAnnotation(value="demo",age=20,tags={"a","b"})

关键语法规则

  1. 属性支持类型:基本类型、String、枚举、注解、以上类型数组

  2. 如果属性没有设置default默认值,使用注解时必须显式赋值

  3. 特殊约定:属性名叫做value,并且只有这一个属性,赋值时可以省略value=

注解如何生效?

注解本身只是标记!必须配合反射机制解析

示例:通过反射读取运行时注解

// 获取方法
Method method = Test.class.getMethod("func");
// 判断是否存在注解
if(method.isAnnotationPresent(MyAnnotation.class)){
MyAnnotation anno = method.getAnnotation(MyAnnotation.class);
System.out.println(anno.value());
}

Spring、Mybatis 框架底层大量使用:自定义注解 + 反射实现功能。

异常-Exception

一、什么是异常

程序运行过程中出现的不正常情况(错误),会打断正常指令流。

Java 使用面向对象方式管理异常:异常本身就是一个对象。

区分:

  • 编译期异常:写代码编译器就报错
  • 运行时异常:编译正常,运行程序崩溃

二、异常体系结构图(核心)

java.lang.Throwable
├─ Error 【错误】系统级严重问题,程序无力处理,不用捕获
│ StackOverflowError、OutOfMemoryError
└─ Exception 【异常】程序层面,可以捕获处理
├─ RuntimeException 运行时异常(非受检异常)
│ NullPointerException、ArrayIndexOutOfBoundsException、ClassCastException
└─ 其他Exception:编译期异常(受检异常 CheckedException)
IOException、FileNotFoundException、SQLException

概念区分

  1. Error(错误)

    虚拟机内部故障、资源耗尽。程序代码无法修复,不需要 try-catch 捕获。

  2. Exception(异常)

    程序逻辑问题,可以处理。分为两类:

    运行时异常(RuntimeException)【非受检异常】

    编译不强制要求捕获 / 声明;一般由代码书写 bug 导致。

    编译期异常(受检异常 CheckedException)

    编译器强制要求:要么try-catch捕获,要么throws抛出,否则编译报错。


三、异常处理两大方案

方案 1:try-catch-finally 捕获处理

语法模板

try{
// 可能出现异常的代码
}catch(异常类型 e){
// 异常发生后的处理逻辑
}finally{
// 无论是否异常,【一定会执行】,唯一例外:System.exit(0)
}

细节考点

  1. try不能单独存在,必须搭配 catch 或 finally
  2. 多个 catch:小范围异常写前面,大范围异常写后面(子类在前,父类在后)
try{

}catch(NullPointerException e){

}catch(Exception e){ //父类放最后
}
  1. finally 典型用途:释放资源(文件流、数据库连接、IO 流)

try-catch-resources(JDK7 新特性)

自动实现资源关闭,不用手写 finally 关闭流

try(InputStream is = new FileInputStream("a.txt")){

}catch(IOException e){

}

方案 2:throws 向上抛出异常

如果当前方法不想处理异常,可以把异常抛给调用者

// 抛出受检异常,调用者必须处理
public void readFile() throws IOException{

}

区别:

  • try-catch:自己处理异常,异常到此结束
  • throws:不处理,甩锅给上层调用方法

四、主动抛出异常 throw

手动创建异常对象并抛出

int age = -5;
if(age < 0){
throw new RuntimeException("年龄不能为负数");
}

throw(小写):方法内部主动抛出对象

throws(复数):方法声明上,标识该方法可能抛出的异常

关键字 位置 作用
throw 方法内部 主动抛出一个异常实例
throws 方法签名后 声明该方法可能产生哪些异常,向上传递

五、自定义异常

业务场景内置异常不够用时,自己定义异常类

  1. 自定义受检异常:继承 Exception

  2. 自定义运行时异常:继承 RuntimeException(项目最常用)

    示例:

//自定义运行时异常
public class AgeIllegalException extends RuntimeException{
public AgeIllegalException(String msg){
super(msg);
}
}

使用:

if(age < 0){
throw new AgeIllegalException("年龄非法");
}

六、重点知识点

1.finally 一定会执行吗?

绝大多数情况一定会执行

唯一打断情况:System.exit(0); 终止 JVM,finally 不再执行

2.try 里有 return,finally 执行时机

先执行 finally 代码,再执行 return

public static int test(){
try{
return 10;
}finally{
System.out.println("finally执行");
}
}
//输出:finally执行 然后返回10

注意:不要在 finally 中写 return!会覆盖 try 的返回值,屏蔽异常,属于不规范代码。

  1. 受检异常 vs 运行时异常总结
  • 运行时异常 (RuntimeException):编译不强制处理;属于代码 bug,尽量提前判断避免
  • 受检异常:编译器强制处理;属于外部不可控情况(文件不存在、网络故障)
  1. 常用异常举例

运行时异常:

NullPointerException空指针

当应用程序试图在需要对象的地方使用 null 时

ArrayIndexOutOfBoundsException数组越界

用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引

ClassCastException类型转换异常

当试图将对象强制转换为不是实例的子类时,抛出该异常。

ArithmeticException数学运算异常

当出现异常的运算条件时,抛出此异常。

NumberFormatException数字格式不正确异常

编译异常:

IOExceptionFileNotFoundExceptionSQLException

  1. 异常链

捕获异常后抛出新异常时,可以把原始异常作为 cause 保存,保留完整堆栈信息

throw new 业务异常("操作失败",e);

异常链(Exception Chaining) 是一种将一个异常传递给另一个异常的技术。它允许我们在捕获一个异常后,抛出另一个新的异常,同时把原始异常(底层原因)完整地保留并记录在新的异常内部

  1. 为什么需要异常链?(痛点)

在复杂的系统中,异常通常具有层次性。假设这样一个场景:

  1. 底层(数据库层):执行 SQL 时抛出了 SQLException

  2. 中层(服务层):捕捉到 SQLException,如果直接把它往外抛,会显得太底层、太专业(业务调用方不关心你是不是 SQL 错误)。或者如果你把它吞掉,换成抛出一个自定义的 ServiceException

    catch (SQLException e) {
    throw new ServiceException("用户注册失败");
    }

    问题来了:虽然外层成功抛出了符合业务的 ServiceException,但原本的 SQLException 被丢弃了。如果数据库连接串写错或者表名写错,由于丢失了底层异常的堆栈信息(Stack Trace),排查错误会变得异常困难。

异常链完美解决了这个问题:它在抛出新异常的同时,把旧异常“栓”在新异常身上,形成一条链条。

  1. Java 是如何支持异常链的?

在 Java 中,所有的异常最终都继承自 Throwable 类。Throwable 及其子类在设计时就内置了对异常链的支持:

  1. 构造方法支持传入 cause(原因): 几乎所有标准异常类和自定义异常类,都提供了一个接收 Throwable 参数的构造方法:

    public MyException(String message, Throwable cause) {
    super(message, cause); // 传给父类 Throwable 保存
    }
  2. initCause() 方法: 如果使用的异常类没有提供带 cause 参数的构造方法,可以通过调用对象的 initCause(Throwable cause) 方法来手动绑定。

  3. 代码示例

步骤 1:定义一个自定义业务异常

public class UserServiceException extends RuntimeException {
// 提供带有 cause 参数的构造器
public UserServiceException(String message, Throwable cause) {
super(message, cause);
}
}

步骤 2:在业务代码中捕获并链式抛出

public class UserService {
public void registerUser() {
try {
// 模拟底层代码,抛出了底层的 SQLException
// throw new java.sql.SQLException("数据库连接超时");

// 模拟发生异常
int i = 1 / 0;

} catch (Exception e) {
// 捕获底层异常,包装成高层的业务异常抛出,并传入 e 作为 cause
throw new UserServiceException("用户注册模块发生内部错误", e);
}
}
}

步骤 3:查看运行效果

当外层程序捕获到 UserServiceException 并打印堆栈信息(e.printStackTrace())时,你会看到类似下面这样的输出:

com.example.UserServiceException: 用户注册模块发生内部错误
at com.example.UserService.registerUser(UserService.java:15)
... (省略中间调用栈)
Caused by: java.lang.ArithmeticException: / by zero <--- 底层原因清晰可见
at com.example.UserService.registerUser(UserService.java:11)
...
  • 注意看 Caused by::这就是异常链的核心威力所在。它不仅告诉调用者上层业务出错了,还顺藤摸瓜地把底层的罪魁祸首完整展示了出来。
  1. 总结与应用场景
  • 核心作用屏蔽底层实现细节,同时保留底层错误根源,实现“高层抛出业务异常,底层保留技术异常”。
  • 常见应用:在现代企业级框架(如 Spring、MyBatis 等)中非常普遍。例如,当你使用 MyBatis 查询数据库出错时,它捕获到底层的 SQLException,并会将其包装并抛出它自己的 DataAccessException(带有异常链),方便开发者通过日志一眼看出是底层 SQL 语法错了、主键冲突了还是连接断了。

集合

一、集合概述

集合:java.util包下,容器,用来存储多个对象。

区分数组和集合:

  1. 数组:长度固定;可以存基本类型 + 引用类型
  2. 集合:长度可变;只能存储引用类型(对象),基本类型会自动装箱

集合两大根接口:

  1. Collection 单列集合:一次存一个对象
  2. Map 双列集合:一次存一对对象(key=value 键值对)

集合整体架构图

Collection(单列)
├─ List【有序、可重复、有索引】
│ ├─ ArrayList 数组实现
│ ├─ LinkedList 双向链表
│ └─ Vector 数组实现(古老、同步)
├─ Set【无序、不可重复、无索引】
│ ├─ HashSet 哈希表
│ │ └─ LinkedHashSet 哈希表+双向链表(有序)
│ └─ TreeSet 红黑树(自然排序)
└─ Queue【队列】
└─ ArrayDeque、PriorityQueue

Map(双列 key-value)
├─ HashMap 哈希表
│ └─ LinkedHashMap 哈希表+链表(有序)
├─ TreeMap 红黑树,按键排序
├─ Hashtable 古老,同步(淘汰)
└─ Properties 键值对,用于配置文件

第一部分:Collection 接口

通用常用方法(所有 List、Set 都拥有)

boolean add(E e)                添加元素
boolean remove(Object o) 删除指定元素
void clear() 清空集合
boolean contains(Object o) 判断是否包含元素
boolean isEmpty() 判断是否为空
int size() 获取元素个数
Object[] toArray() 转数组
  1. List 接口

特点:有序、可重复、带有索引,支持根据索引增删查改。

特有方法(带索引操作)

get(int index)
set(int index, E element)
add(int index, E element)
remove(int index)

① ArrayList(重点)

底层:可变 Object 数组

  • 默认初始容量:10

  • 扩容机制:原容量 1.5倍

  • 查询快、随机访问快;中间增删慢(需要移动元素)

  • 线程不安全;效率高

    适用:大量查询,少量增删

② LinkedList

底层:双向链表

  • 没有扩容概念,按需创建节点

  • 查询慢(需要遍历);首尾增删速度快

  • 线程不安全

    特有方法(队列 / 栈操作)

addFirst() 
addLast()
getFirst()
getLast()
removeFirst()
removeLast()

③ Vector(了解,基本淘汰)

底层数组,扩容 2 倍;方法加synchronized,线程安全,性能差。

特性 ArrayList LinkedList
底层 动态数组 双向链表
随机访问 快(下标直接定位) 慢(遍历)
中间插入删除 慢(移动大量元素) 快(修改指针)
内存 连续内存,有预留容量 每个节点额外存前后指针

开发 90% 场景优先 ArrayList!

  1. Set 接口

特点:无序、不可重复、无索引,不能用普通 for 循环遍历,只能迭代器 / 增强 for。

① HashSet

底层:哈希表(数组 + 链表 + 红黑树)

元素唯一依靠:hashCode() + equals()

判断重复规则:

  1. 先比较哈希值 hashCode()
  2. 哈希值不同 → 直接不重复
  3. 哈希值相同 → 再调用 equals(),true 判定重复

自定义类存入 HashSet,必须重写 hashCode 和 equals!

特性:无序,不保证存储顺序;线程不安全。

② LinkedHashSet

继承 HashSet;底层哈希表 + 双向链表

存取有序;依然不能重复。

适合:需要去重同时保留添加顺序。

③ TreeSet

底层:红黑树

自动排序(自然排序)

要求:存储元素实现 Comparable 接口,或者构造传入Comparator

元素不能为 null;根据比较规则去重,不是 equals。

  1. Queue 队列(简单了解)

先进先出 FIFO

  • ArrayDeque:数组双端队列
  • PriorityQueue:优先队列(自动排序)

Collection 遍历方式

  1. 增强 for 循环(底层迭代器)
for(Object obj : list){}
  1. Iterator 迭代器
Iterator<String> it = list.iterator();
while(it.hasNext()){
String s = it.next();
}

并发异常:迭代遍历集合时,不要用集合 add/remove;想要删除用迭代器it.remove()

  1. List 额外支持普通 for 循环(有索引)

第二部分:Map 双列集合

存储格式:key=value,键唯一,值可重复;

一个 key 只能对应一个 value;后存入的 value 覆盖旧 value。

Map 通用方法

put(k,v)        添加/覆盖键值对
remove(key) 根据key删除
get(key) 根据key获取value
containsKey(key) 判断是否存在键
containsValue(value) 判断是否存在值
keySet() 获取所有key【重点】
entrySet() 获取所有键值对对象【重点】
clear()
size()

Map 三种遍历方式

  1. keySet() 先拿所有键,再 get 取值
Set<String> keys = map.keySet();
for(String key : keys){
map.get(key);
}
  1. entrySet() 获取 Entry 键值对(效率更高,推荐)
Set<Map.Entry<String,Integer>> entries = map.entrySet();
for(Map.Entry entry : entries){
entry.getKey();
entry.getValue();
}
  1. values() 只获取所有 value(无法拿到 key)

常用实现类

① HashMap(重中之重)

底层:哈希表(数组 + 链表 + 红黑树)JDK1.8 优化

参数:

  • 默认容量:16

  • 负载因子:0.75

  • 扩容条件:元素个数 > 容量 * 负载因子 → 容量扩大 2 倍

  • 树化阈值:链表长度 ≥8,数组长度≥64 → 链表转为红黑树;

    元素≤6 退化为链表。

特点:

  • key 无序;key 允许一个 null,value 允许多个 null

  • 线程不安全

    去重规则:key 对象重写

    hashCode() + equals()

② LinkedHashMap

继承 HashMap;哈希表 + 双向链表

存取有序;其他特性和 HashMap 一致。

可用来实现 LRU 缓存。

③ TreeMap

底层红黑树;按键自动排序

key 必须实现 Comparable 或传入比较器;key 不能为 null。

④ Hashtable(淘汰)

线程安全;key 和 value 都不允许 null;扩容 2 倍;性能差,基本不用。

⑤ Properties

继承 Hashtable;专门读取配置文件 xxx.properties;key、value 只能是字符串。

三、重要对比

HashMap vs HashTable

  1. HashMap 线程不安全;Hashtable 方法加 synchronized,安全
  2. HashMap 允许 key 一个 null;Hashtable 键值都不能 null
  3. HashMap 初始容量 16,扩容 ×2;Hashtable 初始 11,扩容 ×2+1
  4. Hashtable 古老,不推荐使用;多线程可用 ConcurrentHashMap

HashSet 和 HashMap 关系

  1. HashSet 底层就是 HashMap!

  2. HashSet 存储元素,作为 HashMap 的 key,value 统一是一个静态 Object 常量。

四、Collections 工具类(区分 Collection 接口!)

java.util.Collections 操作集合的静态工具类

Collections.sort(list);        //排序
Collections.shuffle(list); //打乱顺序
Collections.max/min(list);
Collections.unmodifiableList() //返回不可变集合
//线程安全集合
Collections.synchronizedList(list);

注意:只是方法加锁,性能一般;高并发优先 JUC 工具。

五、高频坑 & 考点总结

  1. 迭代遍历集合时,不能直接集合 add/remove → ConcurrentModificationException 并发修改异常
  2. HashSet/HashMap 自定义对象作为 key,必须重写 hashCode 和 equals
  3. ArrayList 初始容量 10,第一次 add 才初始化数组;扩容 1.5 倍
  4. HashMap 负载因子 0.75,树化阈值 8,链表转红黑树
  5. List 有序可重复;Set 无序不可重复;Map 键唯一
  6. TreeSet/TreeMap 依靠比较器排序,不是 equals 去重
  7. 集合存储的是对象引用地址,不是对象本身
  8. 自动装箱:集合不能存基本类型 List<int> 报错,只能包装类 List<Integer>

六、线程安全小结

不安全:ArrayList、LinkedList、HashMap、HashSet

安全方案:

  1. Collections.synchronizedXxx() 包装
  2. JUC 并发包:CopyOnWriteArrayList、ConcurrentHashMap

泛型

一、什么是泛型

泛型:参数化类型,把类型当作参数传入类、接口、方法。

<>内的标识符代表未知类型,编译期确定。

核心目标:

  1. 编译期类型检查,防止类型转换异常
  2. 消除强制类型转换
  3. 代码复用,一套代码支持多种数据类型

诞生:JDK 1.5 引入;泛型只存在编译期,运行时被擦除(类型擦除)

二、为什么需要泛型

没有泛型时,集合默认存储 Object:

List list = new ArrayList();
list.add("abc");
list.add(123); //编译不报错
String s = (String) list.get(1); //运行时报 ClassCastException

使用泛型:

List<String> list = new ArrayList<>();
list.add("abc");
// list.add(123); 编译直接报错!
String s = list.get(0); //无需强转

三、泛型的三种使用形式

  1. 泛型类

格式:类名<泛型标识>

常用标识符约定(只是名字,无语法要求)

  • E Element 元素(集合)
  • T Type 类型
  • K Key 键 、V Value 值(Map)
  • R Return 返回值
public class MyBox<T> {
private T data;
public T getData(){ return data; }
public void setData(T data){ this.data = data; }
}

//使用
MyBox<String> box = new MyBox<>();
box.setData("hello");

注意:创建对象时指定类型,这个类里面所有 T 都会被替换为指定类型。

  1. 泛型接口
interface IStorage<T> {
void save(T data);
}
//方式1:实现类确定类型
class StringStorage implements IStorage<String>{
@Override
public void save(String data) {}
}
//方式2:实现类继续保留泛型
class AnyStorage<T> implements IStorage<T>{
@Override
public void save(T data) {}
}
  1. 泛型方法(重点:和泛型类区分开)

泛型方法可以定义在普通类 / 泛型类上,方法自己拥有独立泛型参数

格式:返回值前声明泛型 <T>

//泛型方法
public static <T> T getFirst(List<T> list){
return list.get(0);
}

关键区分:

  • 泛型类的 T:创建对象时确定
  • 泛型方法的 T:调用方法时自动推导类型

四、类型擦除

泛型是编译期概念,.class 字节码中不存在泛型信息!

编译流程:

  1. 编译器利用泛型做类型校验
  2. 擦除泛型参数,替换为原始类型(上限 Object / 指定边界类型)
  3. 自动插入强制类型转换代码

示例:

List<String> list = new ArrayList<>();
list.add("aaa");
String s = list.get(0);

编译后字节码等价于:

List list = new ArrayList();
list.add("aaa");
String s = (String) list.get(0);

原始类型 RawType

不加泛型参数的集合:List list = new ArrayList();

原始类型会关闭泛型检查,尽量不要使用。

擦除规则

  1. 无边界泛型 <T> → 擦除为 Object
  2. 有上限 <T extends Number> → 擦除为 Number
  3. 有下限不能单独用于类泛型(只能通配符)

五、通配符 ? Wildcard

? 代表任意未知类型,主要用于方法参数,不能用于定义泛型类 / 泛型方法。

  1. 无界通配符 <?>
public void printList(List<?> list){
for(Object o : list) System.out.println(o);
}

限制:

可以读取元素,读到 Object

不能 add 除 null 以外的任何元素(编译器不知道真实类型,禁止写入)

  1. 上限通配符 ? extends T

? extends Animal:代表T 及其所有子类

public void test(List<? extends Animal> list){
Animal a = list.get(0); //可读,向上转型
// list.add(new Dog()); //编译报错!禁止写入
}

读取安全,写入不安全。生产者场景(读数据)

  1. 下限通配符 ? super T

? super Dog:代表Dog 及其所有父类

public void test(List<? super Dog> list){
list.add(new Dog()); //可以存入Dog及子类
//Dog dog = list.get(0); //读取只能拿到Object,无法向下转型
}

写入安全,读取不安全。消费者场景(写入数据)

PECS 原则(高频面试)

Producer Extends,Consumer Super

  • 只读取数据(生产者):? extends T
  • 只写入数据(消费者):? super T
  • 既要读又要写:不能使用通配符,直接确定泛型<T>

六、泛型边界(限定类型范围)

上限限定 <T extends 类/接口>

T 只能是该类或子类

// T必须是Number或者Number子类(Integer/Double等)
public <T extends Number> void method(T num){}

泛型没有多上限直接写法;想要同时多个约束,需要 T extends A & B

<T extends Serializable & Comparable>

注意:没有 <T super xxx> 写法!

下限只能用于通配符?,不能用于泛型声明。

七、泛型常见限制

  1. 不能使用泛型创建实例
T t = new T(); //编译报错!擦除后无法确定类型
  1. 不能定义泛型数组
T[] arr = new T[10]; //报错
//可以通过反射创建:Array.newInstance
  1. 静态方法不能使用类上定义的泛型
public class Test<T>{
public static void fun(T data){} //报错!
}

原因:类泛型在创建对象时确定,静态方法不需要对象。

解决方案:使用独立泛型方法<E>

  1. 基本类型不能作为泛型参数

    List<int> ❌;必须使用包装类 List<Integer>

  2. 泛型不支持继承多态误区

List<String> list1 = new ArrayList<>();
List<Object> list2 = list1; //编译报错!

虽然 String 是 Object 子类,但是 List<String>不是 List<Object> 的子类。泛型容器之间不存在继承关系!

  1. 重载不能依靠泛型区分(擦除后方法签名一样)
void fun(List<String> list){}
void fun(List<Integer> list){} //编译冲突,擦除后都是List

八、泛型方法 vs 通配符怎么选?

示例需求:把 list 所有元素添加到另一个集合

//方案1:泛型方法
public static <T> void copy(List<T> src, List<T> dest){
dest.addAll(src);
}

//方案2:通配符
public static <T> void copy2(List<? extends T> src, List<T> dest)
  • 需要同时读写集合、类型互相约束 → 使用泛型<T>
  • 只单纯读取或写入 → 使用通配符

九、易错知识点

  1. 泛型实现原理?

    答:类型擦除,编译期校验,运行时无泛型信息。

  2. List<String>List<Object> 有继承关系吗?

    没有,泛型容器不存在多态,擦除后都是 List。

十、知识点总结

泛型参数化类型,JDK1.5 推出;

编译检查免强转,运行类型被擦除;

三类用法:泛型类、泛型接口、泛型方法;

任意未知类型,extends上界super下界;

PECS 口诀:读用 extends,写用 super;

不能 new 泛型对象、不能泛型数组;

静态方法不能使用类泛型;

容器泛型无继承,List<String>不是 List<Object>

基本类型不能做泛型,需要包装类。

JUnit

一、JUnit 是什么

JUnit 是 Java 单元测试框架,用于对方法 / 模块进行自动化单元测试,属于 xUnit 体系。

作用:

  1. 独立测试单个方法,验证代码逻辑是否正确
  2. 自动化执行,快速回归,避免手动 main 方法测试
  3. 支持断言、前置 / 后置操作、忽略测试、异常测试、超时测试

两大主流版本:

  1. JUnit4:JDK1.5+,核心注解 @Test,基于方法
  2. JUnit5(推荐新项目),别名 Jupiter,JDK8+,架构重构,功能更强

Maven 坐标区分:

  • JUnit4:junit:junit:4.13.2
  • JUnit5:由多个模块组成 junit-jupiter-api(编写)、junit-jupiter-engine(运行)

二、JUnit4 核心知识点(老项目大量使用)

  1. 核心注解
注解 作用
@Test 标记该方法为测试方法;方法必须 public void,无参数
@Before 每个测试方法执行前运行(每个 @Test 都会触发一次)
@After 每个测试方法执行完成后运行
@BeforeClass 所有测试方法执行前只执行一次;方法必须 public static void
@AfterClass 所有测试执行完毕后执行一次;方法必须 public static void
@Ignore 忽略该测试方法,不执行
@Test(expected = 异常.class) 测试预期抛出指定异常
@Test(timeout = 毫秒) 设置测试超时时间,超时判定失败

执行顺序示例:

@BeforeClass
@Before@Test@After
@Before@Test@After
@AfterClass
  1. Assert 断言(单元测试核心)

org.junit.Assert,断言失败测试直接报错

Assert.assertEquals(预期值,实际值);    //相等
Assert.assertNotEquals();
Assert.assertTrue(布尔条件);
Assert.assertFalse();
Assert.assertNull(obj);
Assert.assertNotNull(obj);
Assert.assertSame(对象1,对象2); //== 引用相同
Assert.assertNotSame();

规则:单元测试必须使用断言,不能靠 System.out 打印判断结果!

  1. JUnit4 代码示例
import org.junit.*;
public class DemoTest {

@BeforeClass
public static void initAll(){
System.out.println("所有测试开始前执行一次");
}

@Before
public void init(){
System.out.println("每个测试方法之前执行");
}

@Test
public void testAdd(){
int res = 1+2;
Assert.assertEquals(3,res);
}

@Test(expected = ArithmeticException.class)
public void testException(){
int i = 1/0; //预期抛出算术异常
}

@After
public void close(){
System.out.println("每个测试方法结束执行");
}

@AfterClass
public static void destroyAll(){
System.out.println("全部测试结束执行一次");
}
}

三、JUnit5

重要变化

  1. 包名更换:org.junit.jupiter.api
  2. 注解名称大量调整(去掉旧版部分歧义命名)
  3. 不再强制要求方法 public(private 以外都可以)
  4. 提供更灵活断言、嵌套测试、参数化测试
  5. 支持扩展模型替代 JUnit4 的 Rule

JUnit5 核心注解

JUnit5 等效 JUnit4 说明
@Test @Test 测试方法
@BeforeEach @Before 每个测试方法前执行
@AfterEach @After 每个测试方法后执行
@BeforeAll @BeforeClass 所有测试前执行一次,建议 static
@AfterAll @AfterClass 所有测试后执行一次
@Disabled @Ignore 禁用 / 忽略测试
@DisplayName("测试名称") 自定义测试显示名称,可读性强
@Timeout @Test(timeout) 超时限制
@ExtendWith @Rule/@ClassRule 扩展机制

JUnit5 断言:Assertions

org.junit.jupiter.api.Assertions

支持 lambda,失败信息更友好

Assertions.assertEquals(3, 1+2,"相加结果错误");
Assertions.assertTrue(2>1);
//分组断言,全部执行后统一报错,不会遇到第一个失败就终止
Assertions.assertAll(
()->Assertions.assertEquals(2,1+1),
()->Assertions.assertTrue(5>2)
);
//断言异常
Assertions.assertThrows(ArithmeticException.class,()->{
int a = 1/0;
});

JUnit5 简单示例

import org.junit.jupiter.api.*;
@DisplayName("计算器单元测试")
public class CalcTest {

@BeforeAll
static void beforeAll(){
System.out.println("全局初始化");
}

@BeforeEach
void beforeEach(){
System.out.println("单个测试前置");
}

@Test
@DisplayName("测试加法")
void testPlus(){
Assertions.assertEquals(10,5+5);
}

@Disabled
@Test
void skipTest(){
//该方法被忽略
}

@AfterEach
void afterEach(){
System.out.println("单个测试后置");
}

@AfterAll
static void afterAll(){
System.out.println("全局销毁");
}
}

四、重要概念区分

  1. 单元测试 vs 集成测试
  • 单元测试:测试单个类 / 单个方法,尽量不依赖数据库、网络;可以 Mock 依赖(Mockito)
  • 集成测试:多个模块、数据库、接口联调
  1. 测试规范(开发通用规范)
  • 测试类命名:XxxTest
  • 测试方法命名:test方法名 / shouldXXXWhenXXX
  • 一个测试方法只测一个场景
  • 尽量独立:测试方法之间不能互相依赖,执行顺序不影响结果

五、配套常用扩展

  1. Mockito:配合 JUnit,模拟对象(Mock),解除外部依赖
  2. ParameterizedTest 参数化测试(JUnit5):一组多组输入批量测试,不用重复写多个 @Test

六、重要知识点区分

  1. @Before@BeforeClass 区别?

    Before:每个测试方法执行前;BeforeClass 全局只执行一次,必须 static。

    JUnit5 对应:BeforeEach / BeforeAll。

  2. 单元测试中 Assert 断言作用?

    验证预期结果;断言失败测试用例标记失败。不能只用打印输出。

  3. JUnit4 和 JUnit5 主要区别?

    包路径不同、注解改名、方法不必 public、强大的分组断言、扩展机制替代 Rule。

  4. 单元测试方法有什么要求?

    JUnit4:public void、无参数;

    JUnit5:返回 void,无参,访问修饰符不限。

  5. @Test(expected=xxx) 缺点?

    只能捕获任意位置该异常,无法精准定位哪行代码抛出;JUnit5 推荐 assertThrows。

多线程编程

概念

  • 进程:操作系统资源分配的最小单位(独立内存、CPU 资源),一个程序就是一个进程。
  • 线程:进程内独立执行路径,CPU 调度最小单位,共享进程资源。
  • 关系:一个进程至少包含 1 个主线程;多线程 = 一个进程多条执行路径。

优缺点

优点:

  1. 充分利用多核 CPU,提高程序吞吐量
  2. 异步处理,提升响应速度(GUI、接口)

缺点:

  1. 线程切换消耗 CPU 资源
  2. 共享资源竞争 → 线程安全问题
  3. 并发带来死锁、可见性、有序性问题

并发 & 并行

  • 并发:单核 CPU,时间片轮转,宏观同时执行,微观交替执行
  • 并行:多核 CPU,多个线程真正同时运行

创建线程的 4 种方式

方式 1:继承 Thread 类

class MyThread extends Thread{
@Override
public void run() {
//线程执行任务
}
}
//启动
new MyThread().start();

调用 start(),不是 run()!

run() 只是普通方法;start() 向操作系统申请创建新线程,自动调用 run()

方式 2:实现 Runnable 接口(推荐)

解耦:线程类和任务分离;避免单继承限制

class Task implements Runnable{
@Override
public void run() {
}
}
new Thread(new Task()).start();

方式 3:Callable + FutureTask(有返回值、可抛异常)

Runnable 无返回值、不能抛出受检异常;Callable 弥补该缺陷

Callable<Integer> call = ()->{
return 100;
};
FutureTask<Integer> task = new FutureTask<>(call);
new Thread(task).start();
Integer res = task.get(); //阻塞获取结果

方式 4:线程池 ExecutorService(生产环境强制使用)

频繁创建销毁线程开销巨大,池化复用线程。

Executors工具类 / ThreadPoolExecutor手动创建(阿里规范推荐)

面试重点区分:

Thread 是线程载体;Runnable/Callable 是任务

线程的五大状态(操作系统层面)

  1. 新建 NEW:new Thread(),尚未调用 start()
  2. 就绪 RUNNABLE:调用 start(),等待 CPU 时间片
  3. 运行 RUNNING:拿到 CPU,执行 run 方法
  4. 阻塞 BLOCKED / WAITING / TIMED_WAITING
    • BLOCKED:等待 synchronized 锁
    • WAITING:无限等待 wait()
    • TIMED_WAITING:限时等待 sleep(time)、wait(time)
  5. 终止 TERMINATED:run 执行完毕 / 异常退出,线程结束

Java Thread.State 将【就绪 + 运行】合并为 RUNNABLE,JVM 层面看不到操作系统细分状态!

线程常用方法

Thread 类实例方法

  1. start():启动线程
  2. run():任务逻辑
  3. interrupt():中断线程(设置中断标记,不是立刻终止
  4. isInterrupted():判断是否被中断(不清空标记)
  5. join():等待其他线程执行完成(底层 wait)

Thread 静态方法

  1. Thread.sleep(ms)限时阻塞,不释放锁

    主动让出 CPU,时间到自动恢复就绪;会抛出 InterruptedException,捕获异常时中断标记会被清除

  2. Thread.yield():礼让 CPU,当前线程回到就绪态(不一定生效)

  3. Thread.currentThread():获取当前执行线程

Object 监视器方法(必须在 synchronized 内调用!)

  1. wait():无限等待,释放锁
  2. wait(long):限时等待,释放锁
  3. notify():随机唤醒一个等待线程
  4. notifyAll():唤醒全部等待线程

synchronized

基础概念

synchronized 是 Java 内置监视器锁(互斥锁),属于 JVM 层面实现,用来解决多线程原子性、可见性、有序性问题,保证同一时刻最多只有一个线程执行同步代码。

别称:隐式锁、内置锁、monitor 锁。

作用:

  1. 原子性:同步代码块同一时间仅一个线程执行
  2. 可见性:解锁前强制把共享变量刷新到主内存;加锁清空本地缓存
  3. 有序性:禁止临界区内指令重排序

三种使用方式

  1. 修饰【实例方法】
public synchronized void test() {
}

锁对象:当前实例对象 this

同一个对象多个同步方法互斥;不同对象互不影响。

  1. 修饰【静态方法】
public static synchronized void test() {
}

锁对象:当前类的 Class 对象(类锁)

所有实例共用同一把锁,不管创建多少对象,串行执行。

重点区分:

synchronized静态方法synchronized实例方法互不阻塞!两把不同的锁

  1. 修饰【同步代码块】(推荐,粒度可控)
//锁任意对象
synchronized (obj) {
}
//类锁写法
synchronized (ClassName.class) {
}
//当前对象锁
synchronized (this) {
}

优势:缩小锁范围,提高并发效率。

什么是线程同步

多个线程同时访问共享资源,控制访问顺序,保证数据安全,避免并发冲突。

根本问题:多线程并发操作共享变量,产生数据竞争

三大并发问题:原子性、可见性、有序性

同步本质:让并发操作串行化(部分串行),牺牲吞吐量换取正确性。

二、什么时候需要同步?

同时满足下面 3 点,一定会出现线程安全问题:

  1. 多线程

  2. 共享变量(共享资源)

  3. 存在写操作(修改)

    只读场景不需要同步。

示例不安全代码:

class Count {
int num = 0;
public void add(){
num++; //读取、+1、回写,三步,不具备原子性
}
}

三、线程同步的四大实现方案

方案 1:synchronized 内置同步锁(监视器锁)

前面详细讲过,简单回顾同步用法

  1. 同步实例方法:锁 this
  2. 同步静态方法:锁 Class 对象
  3. 同步代码块:自定义锁对象
//标准写法,锁对象建议 private final
private final Object lock = new Object();
public void add(){
synchronized (lock) {
num++;
}
}

特性:可重入、自动释放锁、异常自动解锁;非公平锁。

配套等待唤醒机制:wait() / notify() / notifyAll()

典型模型:生产者消费者。

方案 2:Lock 显式锁(AQS)

ReentrantLock

Lock lock = new ReentrantLock();
public void add(){
lock.lock();
try{
num++;
}finally {
lock.unlock(); //必须finally释放
}
}

扩展能力:

  • 公平锁 / 非公平锁
  • tryLock() 非阻塞尝试获取锁
  • lockInterruptibly() 支持中断等待
  • 多个 Condition 条件变量,精准唤醒线程

方案 3:volatile(轻量级同步手段)

volatile ≠ 锁,不能实现完整同步!

作用:

保证可见性、禁止指令重排序

无法保证原子性(不能解决 num++)

适用场景:状态标记、DCL 单例。

方案 4:无锁同步:CAS 原子类(乐观同步)

java.util.concurrent.atomic
AtomicInteger、AtomicLong

底层:CAS(CompareAndSwap)乐观锁,不阻塞线程。

AtomicInteger num = new AtomicInteger(0);
num.incrementAndGet(); //原子自增

优点:无锁、性能高;

问题:存在 ABA 问题,解决方案:AtomicStampedReference增加版本号。

四、同步两大模型:悲观锁 VS 乐观锁

  1. 悲观锁

    默认一定会发生竞争,访问资源先上锁。

    代表:synchronized、ReentrantLock

    线程竞争失败阻塞。

  2. 乐观锁

    默认竞争很少,不加锁;修改时校验数据是否被别人修改,冲突则重试。

    代表:CAS 原子类、数据库版本号机制。

五、线程间同步通信(等待唤醒机制)

  1. synchronized + wait/notify
synchronized(lock){
//必须while判断,防止虚假唤醒!!
while(条件不满足){
lock.wait(); //释放锁,等待
}
//业务操作
lock.notifyAll(); //唤醒等待线程
}

关键点:

  • wait 释放锁;sleep 不释放锁
  • 必须在同步代码块中调用
  • 推荐 notifyAll,避免信号丢失
  1. Lock + Condition
Condition condition = lock.newCondition();
lock.lock();
try{
while(条件不满足){
condition.await();
}
condition.signalAll();
}finally {
lock.unlock();
}

优势:可以创建多个 Condition,实现精准分组唤醒,粒度更细。

死锁

  1. 死锁四大必要条件(全部满足才会死锁)

    1. 互斥条件:锁同一时间只能一个线程持有

    2. 请求并持有:线程持有锁,同时申请其他锁

    3. 不可剥夺:锁不能被其他线程强行抢走

    4. 循环等待:线程互相持有对方需要的锁

  2. 解决思路:破坏任意一个条件

最常用:破坏循环等待 → 统一锁获取顺序

释放锁

一、会释放 synchronized 锁的 4 种情况

  1. 同步代码 / 同步方法正常执行完毕

    线程执行走出 synchronized{}代码块,或者同步方法运行结束,自动释放监视器锁。

  2. 同步区域内遇到 break、return

    提前退出同步代码块 / 方法,锁依旧释放。

  3. 同步区域抛出未捕获的 Exception / Error

    异常导致线程终止,JVM 自动释放锁(这是 synchronized 优势,不会死锁)。

  4. 同步区域调用 wait()

    线程立刻释放当前 monitor 锁,进入等待池;后续被notify()唤醒后,需要重新竞争锁才能继续执行。

    二、不会释放 synchronized 锁的场景

  5. 调用 Thread.sleep() / Thread.yield()线程暂停执行,但仍然持有锁,其他线程无法进入同步区。

    • sleep:休眠固定时长,抱着锁睡觉
    • yield:让出 CPU 时间片,回到就绪态,不释放锁
  6. 其他线程调用 suspend() 挂起当前线程

    线程被暂停,但持续持有锁。

官方废弃 API!不推荐使用,容易引发死锁。

操作 是否释放 synchronized 锁
正常执行完同步代码 释放
return /break 退出同步区 释放
同步代码抛出异常 释放
wait() 释放
sleep() / yield() 不释放
notify()/notifyAll() 不释放
suspend()挂起线程 不释放

IO 流

一、IO 概述

IO:Input (输入) / Output (输出),以程序 (内存) 为参照物

  • 输入流:外部 → 内存(读数据)

  • 输出流:内存 → 外部(写数据)

    外部数据源:文件、网络、内存数组、控制台等

Java IO 分为三大体系:

  1. BIO 同步阻塞 IO(传统 java.io,重点学习)
  2. NIO 同步非阻塞 IO(java.nio,通道、缓冲区)
  3. AIO 异步非阻塞 IO(NIO.2)

二、BIO 四大核心抽象父类(java.io)

  1. 字节流(处理一切文件:视频、图片、音频、文档)

基类:InputStream(输入) / OutputStream(输出)

单位:byte 字节,8 位

常用实现类:

  • FileInputStream / FileOutputStream:文件原始字节流
  • BufferedInputStream / BufferedOutputStream:缓冲字节流(提升性能)
  • ByteArrayInputStream / ByteArrayOutputStream:内存数组流
  • ObjectInputStream / ObjectOutputStream:对象序列化流
  1. 字符流(仅处理文本文件:txt、java 等)

基类:Reader(输入) / Writer(输出)

单位:char 字符,适配编码(UTF-8/GBK)

常用实现类:

  • FileReader / FileWriter:文件字符流(底层依然包装字节流)
  • BufferedReader / BufferedWriter:缓冲字符流,BufferedReader 独有readLine()
  • InputStreamReader / OutputStreamWriter:转换流(字节↔字符桥梁)
  • PrintWriter:打印流,自动换行、自动刷新

核心区分:

图片、视频、压缩包 → 字节流

纯文本 → 字符流(更方便)

三、流的分类方式

  1. 流向:输入流、输出流
  2. 数据单位:字节流、字符流
  3. 功能
    • 节点流:直接对接数据源(FileInputStream、FileReader)
    • 处理流(包装流):包裹其他流,增加功能(BufferedXX、ObjectXX)

装饰器设计模式:创建流时,层层包装。

示例:

//节点流
FileInputStream fis = new FileInputStream("a.txt");
//缓冲处理流包装节点流
BufferedInputStream bis = new BufferedInputStream(fis);

四、重要编码问题

FileReader/FileWriter默认使用系统编码,无法手动指定编码,极易乱码!

规范做法:使用转换流指定编码

//字节流 + 转换流 = 指定编码读取文本
InputStreamReader isr = new InputStreamReader(new FileInputStream("a.txt"),"UTF-8");

五、缓冲流原理

普通流:每次读写直接访问磁盘,频繁 IO,性能差

缓冲流:内置缓冲区数组

读取:一次性从磁盘读取一批数据存入缓冲区,程序优先读缓冲区;

写入:数据先存入缓冲区,缓冲区满 / 手动 flush ()/ 关闭流时,一次性写入磁盘。

  • flush():刷新缓冲区,强制把缓冲数据输出
  • close():内部会自动调用 flush ()

六、对象流 & 序列化

ObjectInputStream / ObjectOutputStream

作用:把 Java 对象写入文件 / 网络传输(序列化)

序列化要求

  1. 对象所属类必须实现接口:java.io.Serializable(标记接口,无方法)
  2. transient修饰的成员变量不会参与序列化(临时数据,不保存)
  3. 静态变量不属于对象,不会序列化

反序列化不需要调用构造方法创建对象。

七、IO 流标准操作步骤

  1. 创建流对象(可能抛出 FileNotFoundException)
  2. 读写操作(read/write,IOException)
  3. 关闭流!释放资源

流占用操作系统文件句柄,不关闭会资源泄漏。

资源关闭写法演进

  1. 原始:try-catch-finally 手动 close

  2. 推荐:try-with-resources(JDK1.7+)

    AutoCloseable接口的流,代码执行完毕自动关闭,无需手动 finally

try(BufferedReader br = new BufferedReader(new FileReader("a.txt"))){
String line;
while((line = br.readLine()) != null){
System.out.println(line);
}
}catch (IOException e){
e.printStackTrace();
}

八、常用方法汇总

字节流 InputStream

  • read():读取 1 字节,返回 int;末尾返回 - 1
  • read(byte[] buf):批量读取到字节数组,返回读取长度

字符流 Reader

  • read():读取单个字符
  • read(char[] buf)
  • BufferedReader:readLine() 读取一行,读到末尾返回 null

输出通用

  • write ():写入数据
  • flush ():刷新缓冲区
  • close ():关闭流

九、IO 常见坑

  1. 文件复制使用字符流:图片视频损坏
  2. FileReader 不指定编码,跨平台乱码
  3. 忘记关闭流 / 忘记 flush,缓冲数据丢失
  4. 读取循环判断条件错误(没有判断返回 - 1/null,死循环)
  5. 序列化类新增属性,未定义serialVersionUID导致反序列化失败

十、BIO、NIO 简单区分

类型 模型 特点
BIO 同步阻塞 一连接一线程;代码简单,高并发性能差
NIO 同步非阻塞 通道 + 缓冲区;单线程轮询,多路复用
AIO 异步非阻塞 操作系统通知回调,适合高并发长连接
  1. 基础字节流 文件复制(普通 FileStream)

适用:任意文件(图片 / 视频 / 压缩包)

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

public class FileStreamDemo {
public static void main(String[] args) {
// 源文件、目标文件
String src = "test.jpg";
String dest = "copy.jpg";

try (FileInputStream fis = new FileInputStream(src);
FileOutputStream fos = new FileOutputStream(dest)) {

byte[] buffer = new byte[1024]; // 缓冲区数组
int len; // 本次读到的字节数量
// len=-1 代表读取到文件末尾
while ((len = fis.read(buffer)) != -1) {
fos.write(buffer, 0, len);
}
System.out.println("文件复制完成");
} catch (IOException e) {
e.printStackTrace();
}
}
}
  1. 缓冲字节流复制(性能更高,开发常用)
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class BufferedByteDemo {
public static void main(String[] args) {
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("test.mp4"));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("copy.mp4"))) {

byte[] buf = new byte[4096];
int len;
while ((len = bis.read(buf)) != -1) {
bos.write(buf, 0, len);
}
//缓冲流close()内部自动flush;如果不关闭流,需要手动bos.flush()
System.out.println("缓冲流复制成功");
} catch (IOException e) {
e.printStackTrace();
}
}
}
  1. 字符流:读取文本文件 FileReader

缺陷:无法指定编码,容易乱码!

import java.io.FileReader;
import java.io.IOException;

public class FileReaderDemo {
public static void main(String[] args) {
try (FileReader fr = new FileReader("demo.txt")) {
char[] chars = new char[1024];
int len;
while ((len = fr.read(chars)) != -1) {
System.out.print(new String(chars, 0, len));
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
  1. 缓冲字符流 BufferedReader(readLine 读取一行)
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;

public class BufferedReadDemo {
public static void main(String[] args) {
try (BufferedReader br = new BufferedReader(new FileReader("demo.txt"))) {
String line;
//读到文件末尾返回 null
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
  1. 转换流(重点!解决文本乱码,可以指定 UTF-8 编码)

InputStreamReader / OutputStreamWriter 字节→字符桥梁

项目标准读写文本写法

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class ConvertStreamDemo {
public static void main(String[] args) {
//指定UTF-8编码读取文本
try (BufferedReader br = new BufferedReader(
new InputStreamReader(new FileInputStream("demo.txt"), "UTF-8")
)) {
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
  1. 对象序列化 & 反序列化 ObjectStream Demo

第一步:实体类(实现 Serializable)

import java.io.Serializable;

//标记接口,开启序列化
public class User implements Serializable {
//序列化版本号,建议手动定义,防止升级类后反序列化失败
private static final long serialVersionUID = 1L;

private String name;
private int age;
//transient修饰:不会序列化保存
private transient String tempData;

public User(String name, int age, String tempData) {
this.name = name;
this.age = age;
this.tempData = tempData;
}

@Override
public String toString() {
return "User{name='" + name + "', age=" + age + ", tempData='" + tempData + "'}";
}
}

第二步:序列化(对象写入文件)+ 反序列化(读取对象)

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class ObjectIoDemo {
public static void main(String[] args) {
//序列化
try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.dat"))) {
User user = new User("张三", 20, "临时信息");
oos.writeObject(user);
System.out.println("对象序列化完成");
} catch (IOException e) {
e.printStackTrace();
}

//反序列化
try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.dat"))) {
User user = (User) ois.readObject();
System.out.println("读取对象:" + user);
//输出 tempData=null,transient数据丢失
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
  1. PrintWriter 打印流(方便输出文本,自动换行刷新)
import java.io.FileOutputStream;
import java.io.PrintWriter;

public class PrintWriterDemo {
public static void main(String[] args) {
try (PrintWriter pw = new PrintWriter(new FileOutputStream("out.txt"))) {
pw.println("第一行文字");
pw.print("不换行内容");
pw.println(666);
} catch (Exception e) {
e.printStackTrace();
}
}
}

Properties

一、概述

java.util.Properties
  1. 继承 Hashtable,属于 Map 集合,是线程安全的
  2. 键和值只能是 String 字符串
  3. 专门用来读取 properties 配置文件.properties
  4. 文件格式:key=value,注释 #开头

区别普通 Map:

Map 可以存任意 Object;Properties 强制 key/value 为字符串,自带 IO 加载配置文件方法。

二、核心常用方法

  1. 存取方法(推荐,自带类型约束)
  • setProperty(String key, String value):存入键值对
  • getProperty(String key):根据 key 获取 value,找不到返回 null
  • getProperty(String key, String defaultValue):找不到 key 返回默认值

不推荐直接使用父类 put() / get():可以存入非字符串,运行时异常!

  1. IO 加载配置文件(重点)
  • load(InputStream inStream):加载 properties 文件(字节流,推荐)
  • load(Reader reader):字符流加载
  • store(OutputStream out, String comments):把集合数据写出到配置文件
  1. 遍历
  • propertyNames() 返回所有 key 枚举
  • stringPropertyNames() 返回 Set(JDK1.6 + 推荐)

三、文件格式示例(db.properties)

#数据库配置
username=root
password=123456
url=jdbc:mysql://localhost:3306/test
driver=com.mysql.cj.jdbc.Driver

四、完整 Demo

Demo1:加载配置文件(最常用)

db.properties 放在 项目根目录

import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

public class PropertiesLoadDemo {
public static void main(String[] args) {
Properties prop = new Properties();

try(FileInputStream fis = new FileInputStream("db.properties")){
//加载配置文件
prop.load(fis);

//读取数据
String username = prop.getProperty("username");
String password = prop.getProperty("password");
//不存在的key,使用默认值
String test = prop.getProperty("noneKey", "默认值");

System.out.println(username);
System.out.println(password);
System.out.println(test);

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

Demo2:存入数据 + 写出到配置文件

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Properties;

public class PropertiesStoreDemo {
public static void main(String[] args) {
Properties prop = new Properties();

//设置键值对
prop.setProperty("name","张三");
prop.setProperty("age","20");
prop.setProperty("gender","男");

try(FileOutputStream fos = new FileOutputStream("user.properties")){
//参数2:文件注释
prop.store(fos, "用户信息配置");
}catch (IOException e){
e.printStackTrace();
}
}
}

Demo3:遍历 Properties

import java.util.Properties;
import java.util.Set;

public class PropertiesLoopDemo {
public static void main(String[] args) {
Properties prop = new Properties();
prop.setProperty("k1","v1");
prop.setProperty("k2","v2");

Set<String> keys = prop.stringPropertyNames();
for(String key : keys){
String value = prop.getProperty(key);
System.out.println(key + " = " + value);
}
}
}

高频注意事项

  1. 不要使用 put/get
prop.put("a",123); //错误!存入Integer
prop.getProperty("a"); //会返回null,埋下bug
  1. 配置文件默认编码:ISO-8859-1

文件包含中文会乱码!

解决方案:

  • 使用 Reader 配合转换流指定 UTF-8
InputStreamReader isr = new InputStreamReader(new FileInputStream("db.properties"),"UTF-8");
prop.load(isr);
  1. 路径问题
  • new FileInputStream("xxx.properties"):相对项目根目录
  • 如果放在 resources 资源文件夹(Maven 项目),使用类加载器读取(标准开发写法)
//读取resources下的配置文件
Properties prop = new Properties();
prop.load(Properties.class.getResourceAsStream("/db.properties"));
  1. properties 文件写法
  • key=value 也可以 key:value
  • # 注释,不能放在键值同一行右侧(部分版本识别异常)
  • 等号两边不要随意加空格

网络编程

一、基础概念

网络通信三要素

  1. IP 地址:定位网络上一台主机

    • IPv4:4 字节;IPv6:16 字节
    • 本机地址:127.0.0.1 / localhost
  2. 端口号:定位主机上进程,范围 0~65535

    • 0~1023:公认端口(80 http,21 ftp,22 ssh)
    • 1024~65535:自定义端口

    一个进程可以占用多个端口;一个端口同一时间只能被一个进程占用

  3. 通信协议:数据传输规则

    • TCP:面向连接、可靠、有序、流式、三次握手四次挥手;不丢包;适合文件传输、聊天
    • UDP:无连接、不可靠、数据报、速度快;可能丢包;适合直播、语音通话

两个核心协议对比

特性 TCP UDP
连接 面向连接(通信前建立连接) 无连接,直接发数据包
可靠性 可靠,重传机制、确认应答 不可靠,不确认、不重传
数据形式 字节流 数据包(Datagram)
流量 / 拥塞控制
适用场景 文件上传下载、网页、数据库 直播、视频通话、游戏

二、核心类

Java 网络包:java.net

  1. InetAddress 代表 IP 地址

不能 new 对象,使用静态方法获取实例

//获取本机
InetAddress local = InetAddress.getLocalHost();
//根据域名/ip获取
InetAddress server = InetAddress.getByName("127.0.0.1");

String hostIp = server.getHostAddress(); //获取ip字符串
String hostName = server.getHostName(); //获取主机名
  1. Socket 编程(BIO 阻塞通信,重点)

模型:客户端 Socket | 服务端 ServerSocket

  • ServerSocket:服务端,监听指定端口,等待客户端连接
  • Socket:代表一端通信套接字,连接建立后,两端都持有 Socket
  • Socket 底层封装 InputStream / OutputStream,使用 IO 流收发数据

通信流程(TCP):

  1. 服务端创建ServerSocket(端口),调用accept() 阻塞等待客户端连接
  2. 客户端创建Socket(ip,端口)发起连接请求
  3. 服务端accept()收到连接,返回一个 Socket 对象,代表与当前客户端通道
  4. 两端通过 Socket 的输入输出流收发数据
  5. 通信完毕关闭流、关闭 Socket

accept()read() 都是阻塞方法,没有连接 / 没有数据时线程卡住。

TCP 代码 Demo

服务端 ServerSocket

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.ServerSocket;
import java.net.Socket;

public class TcpServer {
public static void main(String[] args) throws IOException {
ServerSocket serverSocket = new ServerSocket(8888);
System.out.println("服务端启动,监听8888端口...");

//阻塞等待客户端连接
Socket socket = serverSocket.accept();
System.out.println("客户端已连接:" + socket.getInetAddress());

//获取输入流,读取客户端消息
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String msg = br.readLine();
System.out.println("收到客户端:" + msg);

//关闭资源
br.close();
socket.close();
serverSocket.close();
}
}

客户端 Socket

import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.net.Socket;

public class TcpClient {
public static void main(String[] args) throws Exception {
//连接服务端 ip+端口
Socket socket = new Socket("127.0.0.1", 8888);

//输出流向服务端发送消息
PrintWriter pw = new PrintWriter(new OutputStreamWriter(socket.getOutputStream()));
pw.println("Hello 服务端!");
pw.flush(); //缓冲流必须刷新

pw.close();
socket.close();
}
}
  1. UDP 编程:DatagramSocket + DatagramPacket

UDP 没有建立连接概念,数据包为单位传输

  • DatagramSocket:收发数据包的套接字
  • DatagramPacket:数据包(存放字节数据、目标地址端口)

UDP Demo

//UDP 发送端
public class UDPSender {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket();
byte[] data = "udp消息".getBytes();
//封装数据包:数据,长度,目标ip,端口
DatagramPacket packet = new DatagramPacket(
data,data.length,
InetAddress.getByName("127.0.0.1"),9999
);
socket.send(packet);
socket.close();
}
}

//UDP接收端
public class UDPReceiver {
public static void main(String[] args) throws IOException {
DatagramSocket socket = new DatagramSocket(9999);
byte[] buf = new byte[1024];
DatagramPacket packet = new DatagramPacket(buf,buf.length);
socket.receive(packet); //阻塞等待数据包

String msg = new String(packet.getData(),0,packet.getLength());
System.out.println("收到:"+msg);
socket.close();
}
}

三、BIO 网络编程存在的问题

原生 BIO:一个客户端连接占用一个独立线程

  • 客户端很多 → 创建大量线程,内存暴涨、线程切换开销大

  • 线程大部分时间阻塞在accept()/read(),资源浪费

    解决方案:

  1. NIO(同步非阻塞 java.nio)多路复用(Selector、Channel、Buffer)
  2. 框架:Netty(封装 NIO,企业主流)

区分:

  • BIO:同步阻塞 IO
  • NIO:同步非阻塞(JDK1.4+)
  • AIO:异步非阻塞(NIO.2 JDK1.7+)

四、重要常见问题

三次握手(TCP 建立连接)

  1. 客户端 → 服务端:SYN(请求连接)

  2. 服务端 → 客户端:SYN+ACK(同意连接,确认)

  3. 客户端 → 服务端:ACK(确认收到)

    目的:协商收发能力,防止旧连接报文干扰

四次挥手(TCP 断开连接)

  1. 客户端发 FIN:我不再发送数据
  2. 服务端回复 ACK:收到关闭请求(服务端还可以继续发剩余数据)
  3. 服务端数据发完,发送 FIN:服务端也不再发送
  4. 客户端回复 ACK 确认断开

为什么挥手四次?TCP 是全双工,两个方向通道独立关闭。

Socket 关闭注意事项

  1. shutdownOutput():半关闭,只关闭输出流,仍然可以接收对方数据
  2. close ():同时关闭输入输出流

粘包问题

TCP 是字节流,无消息边界:

多条数据被操作系统合并发送,接收方无法区分第一条、第二条消息。

解决办法:

  1. 固定消息长度
  2. 特殊分隔符(换行)
  3. 消息头部携带数据长度(Netty 常用方案)

UDP 不存在粘包,UDP 基于独立数据包

五、URL & URLConnection(访问网络资源)

java.net.URL 定位网络资源(http/https/file)

URLConnection 发起连接,获取网页流

URL url = new URL("https://www.baidu.com");
URLConnection conn = url.openConnection();
//获取网页输入流
InputStream is = conn.getInputStream();