Java学习笔记03

Scanner 类

Scanner 是 java.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数字格式不正确异常

编译异常:

IOException、FileNotFoundException、SQLException

  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();