跳到正文
hello world

Java 开发常用笔记

一、Spring Boot 接口三种入参请求和接收方式

Spring Boot 接口常见的入参方式主要有三种:

路径参数
查询参数
JSON 请求体

1. 路径参数:@PathVariable

请求地址:

GET /users/1001

Controller:

@GetMapping("/users/{id}")
public UserVO getUser(
        @PathVariable Long id
) {
    return userService.getUser(id);
}

也可以明确指定参数名:

@GetMapping("/users/{userId}")
public UserVO getUser(
        @PathVariable("userId") Long id
) {
    return userService.getUser(id);
}

适合场景:

根据 ID 查询
根据编号删除
访问明确的单个资源

2. 查询参数:@RequestParam

请求地址:

GET /users?page=1&size=10&status=1

Controller:

@GetMapping("/users")
public List<UserVO> list(
        @RequestParam Integer page,
        @RequestParam Integer size,
        @RequestParam(required = false) Integer status
) {
    return userService.list(page, size, status);
}

设置默认值:

@GetMapping("/users")
public List<UserVO> list(
        @RequestParam(defaultValue = "1") Integer page,
        @RequestParam(defaultValue = "10") Integer size
) {
    return userService.list(page, size);
}

参数较多时,可以直接使用对象接收:

@Data
public class UserQuery {

    private Integer page;

    private Integer size;

    private Integer status;
}
@GetMapping("/users")
public List<UserVO> list(UserQuery query) {
    return userService.list(query);
}

适合场景:

分页查询
条件筛选
简单表单参数

3. JSON 请求体:@RequestBody

请求:

{
  "username": "zhangsan",
  "age": 30
}

DTO:

@Data
public class UserCreateDTO {

    private String username;

    private Integer age;
}

Controller:

@PostMapping("/users")
public Long create(
        @RequestBody UserCreateDTO dto
) {
    return userService.create(dto);
}

适合场景:

新增数据
修改数据
复杂对象
嵌套对象
数组和集合

4. 三种方式怎么选

@PathVariable:资源标识
@RequestParam:查询条件
@RequestBody:复杂业务数据

例如:

@PutMapping("/users/{id}")
public void update(
        @PathVariable Long id,
        @RequestBody UserUpdateDTO dto
) {
}

这里:

id 表示修改哪个用户
dto 表示修改哪些内容

二、Comparable 和 Comparator 实现排序

Java 中对象排序常见有两种方式:

Comparable:对象自己定义排序规则
Comparator:外部定义排序规则

1. Comparable

实体类实现 Comparable

@Data
@AllArgsConstructor
public class User implements Comparable<User> {

    private Long id;

    private String username;

    private Integer age;

    @Override
    public int compareTo(User other) {
        return Integer.compare(this.age, other.age);
    }
}

排序:

List<User> users = new ArrayList<>();

users.add(new User(1L, "zhangsan", 30));
users.add(new User(2L, "lisi", 20));
users.add(new User(3L, "wangwu", 25));

Collections.sort(users);

按照年龄升序排列。

降序:

@Override
public int compareTo(User other) {
    return Integer.compare(other.age, this.age);
}

2. Comparator

不修改实体类,在外部指定排序规则:

users.sort(
        Comparator.comparing(User::getAge)
);

降序:

users.sort(
        Comparator.comparing(User::getAge)
                .reversed()
);

多字段排序:

users.sort(
        Comparator.comparing(User::getAge)
                .thenComparing(User::getUsername)
);

处理 null

users.sort(
        Comparator.comparing(
                User::getAge,
                Comparator.nullsLast(
                        Integer::compareTo
                )
        )
);

3. 两者区别

Comparable:类本身只有一种主要排序规则
Comparator:可以定义多种排序规则

实际开发中,Comparator 使用更加灵活。


三、遍历集合时 remove 或 add 的注意事项

遍历集合时直接执行 remove()add(),很容易出现:

ConcurrentModificationException

1. 错误示例

List<String> list =
        new ArrayList<>(
                List.of("A", "B", "C")
        );

for (String item : list) {

    if ("B".equals(item)) {
        list.remove(item);
    }
}

增强 for 底层使用迭代器。

遍历过程中直接修改原集合,会破坏迭代器状态。


2. 使用 Iterator 删除

Iterator<String> iterator =
        list.iterator();

while (iterator.hasNext()) {

    String item = iterator.next();

    if ("B".equals(item)) {
        iterator.remove();
    }
}

这是遍历时删除元素的标准写法。


3. 使用 removeIf

条件删除可以直接写:

list.removeIf(
        item -> "B".equals(item)
);

这种写法更简洁。


4. 普通 for 循环倒序删除

for (int i = list.size() - 1; i >= 0; i--) {

    if ("B".equals(list.get(i))) {
        list.remove(i);
    }
}

倒序删除不会影响前面还没有遍历的下标。


5. 遍历时新增

不建议在增强 for 中直接新增:

for (String item : list) {
    list.add("D");
}

可以先保存待新增数据:

List<String> newItems =
        new ArrayList<>();

for (String item : list) {

    if ("A".equals(item)) {
        newItems.add("D");
    }
}

list.addAll(newItems);

四、BeanUtils.copyProperties 复制对象避坑

Spring 提供的:

BeanUtils.copyProperties(source, target);

可以复制两个对象中名称和类型相同的属性。


1. 基本使用

UserEntity entity = new UserEntity();

entity.setId(1001L);
entity.setUsername("zhangsan");

UserVO vo = new UserVO();

BeanUtils.copyProperties(entity, vo);

注意参数顺序:

BeanUtils.copyProperties(源对象, 目标对象);

很容易写反。


2. 只会浅拷贝

假设对象中有一个集合:

private List<String> roles;

复制以后:

source.getRoles() == target.getRoles()

结果可能是:

true

也就是说两个对象引用的是同一个集合。

修改其中一个,另一个也可能受到影响。


3. 属性名称相同但类型不同,不会复制

例如:

UserEntity:

private Long status;
UserVO:

private String status;

虽然字段名一样,但类型不同,不会自动转换。

需要手动处理:

vo.setStatus(
        String.valueOf(entity.getStatus())
);

4. null 会覆盖原值

例如更新对象:

UserUpdateDTO dto = new UserUpdateDTO();

dto.setUsername(null);

执行:

BeanUtils.copyProperties(dto, entity);

会把原来的 username 覆盖成 null

可以忽略指定字段:

BeanUtils.copyProperties(
        dto,
        entity,
        "username"
);

但如果需要自动忽略所有 null,通常要自己封装工具方法,或者使用 MapStruct。


5. 不要过度依赖 BeanUtils

字段少时,手动赋值更加清晰:

UserVO vo = new UserVO();

vo.setId(entity.getId());
vo.setUsername(entity.getUsername());

字段很多、转换复杂时,可以考虑:

MapStruct

五、Optional 常用方法实践

Optional 主要用于处理可能为 null 的返回值,减少简单的空判断。

但它不能完全替代 if-else


1. of

对象不能为空:

Optional<String> optional =
        Optional.of("hello");

如果传入 null,会抛出:

NullPointerException

2. ofNullable

允许传入 null

Optional<String> optional =
        Optional.ofNullable(username);

这是最常用的创建方式。


3. isPresent

判断是否有值:

if (optional.isPresent()) {
    System.out.println(optional.get());
}

不过这种写法和普通 if 区别不大。


4. ifPresent

有值时执行:

optional.ifPresent(
        value -> System.out.println(value)
);

例如:

Optional.ofNullable(user)
        .ifPresent(this::saveLog);

5. orElse

为空时返回默认值:

String username =
        optional.orElse("默认用户");

注意:

optional.orElse(createDefaultUser());

即使 optional 有值,createDefaultUser() 也可能被执行。


6. orElseGet

为空时才执行:

String username =
        optional.orElseGet(
                () -> createDefaultUsername()
        );

如果默认值需要计算或查询,优先使用:

orElseGet()

7. orElseThrow

为空时抛异常:

User user =
        Optional.ofNullable(
                userMapper.selectById(id)
        ).orElseThrow(
                () -> new RuntimeException(
                        "用户不存在"
                )
        );

这是业务查询中很常见的写法。


8. map

对内部值进行转换:

String username =
        Optional.ofNullable(user)
                .map(User::getUsername)
                .orElse("未知用户");

避免写成:

if (user != null) {
    if (user.getUsername() != null) {
    }
}

9. filter

根据条件过滤:

User user =
        Optional.ofNullable(
                userMapper.selectById(id)
        ).filter(
                item -> item.getStatus() == 1
        ).orElseThrow(
                () -> new RuntimeException(
                        "用户不可用"
                )
        );

10. flatMap

当转换结果本身也是 Optional 时使用:

Optional<String> result =
        optionalUser.flatMap(
                this::getUsernameOptional
        );

避免出现:

Optional<Optional<String>>

11. Optional 不适合滥用

不建议用于实体字段:

private Optional<String> username;

也不建议方法参数大量使用:

public void save(Optional<User> user)

更适合作为:

方法返回值

六、serialVersionUID 为什么要手动赋值

实现 Serializable 后,通常会看到:

private static final long serialVersionUID = 1L;

完整示例:

@Data
public class User
        implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String username;
}

1. serialVersionUID 的作用

它相当于序列化类的版本号。

序列化时会记录版本号。

反序列化时会检查:

当前类版本号
是否和序列化数据中的版本号一致

不一致可能抛出:

InvalidClassException

2. 不手动赋值的问题

如果没有手动定义,Java 会根据类结构自动生成一个值。

当类发生变化时,例如:

新增字段
删除字段
修改方法
调整结构

自动生成的版本号可能变化。

之前序列化的数据就可能无法反序列化。


3. 手动定义的意义

private static final long serialVersionUID = 1L;

表示开发者主动控制版本。

普通新增字段时,可以继续保持:

1L

如果类结构发生重大变化,并且不希望兼容旧数据,可以改成:

2L

4. 常见使用场景

Java 原生序列化
Session 持久化
对象写入文件
RPC
缓存对象

如果只是普通 DTO,并且项目完全不使用 Java 原生序列化,实际影响可能不大。


七、引用拷贝、浅拷贝和深拷贝

Java 对象复制时,需要区分:

引用拷贝
浅拷贝
深拷贝

1. 引用拷贝

User user1 = new User();

User user2 = user1;

这里没有创建新对象。

user1 == user2

结果是:

true

两个变量指向同一个对象。

修改:

user2.setUsername("lisi");

user1 也会变化。


2. 浅拷贝

浅拷贝会创建一个新对象,但对象内部的引用类型字段仍然共享。

例如:

@Data
public class User implements Cloneable {

    private String username;

    private Address address;

    @Override
    protected User clone()
            throws CloneNotSupportedException {

        return (User) super.clone();
    }
}

使用:

User user1 = new User();

user1.setUsername("zhangsan");
user1.setAddress(new Address("昆明"));

User user2 = user1.clone();

此时:

user1 == user2

是:

false

但:

user1.getAddress() == user2.getAddress()

可能是:

true

修改 user2 的地址,会影响 user1


3. 深拷贝

深拷贝不仅创建外层对象,也会复制内部引用对象。

手动实现:

@Override
protected User clone()
        throws CloneNotSupportedException {

    User user = (User) super.clone();

    user.setAddress(
            this.address.clone()
    );

    return user;
}

这样:

user1.getAddress()
        == user2.getAddress()

结果是:

false

4. BeanUtils 属于哪种

BeanUtils.copyProperties(
        source,
        target
);

通常属于浅拷贝。

基本类型和不可变对象看不出问题,但集合、自定义对象仍可能共享引用。


5. 常见深拷贝方式

手动创建新对象
实现 clone
JSON 序列化再反序列化
MapStruct 自定义转换
复制构造方法

实际业务中,推荐使用:

手动转换
复制构造方法
MapStruct

逻辑更加清晰。


八、@Async 使用避坑

Spring 的 @Async 可以让方法在线程池中异步执行。


1. 开启异步

启动类或配置类添加:

@EnableAsync
@Configuration
public class AsyncConfig {
}

2. 基本使用

@Service
public class MessageService {

    @Async
    public void sendMessage() {

        System.out.println(
                Thread.currentThread().getName()
        );
    }
}

调用:

messageService.sendMessage();

调用线程不会等待方法执行结束。


3. 建议指定线程池

不要长期依赖默认线程池。

@Bean("messageExecutor")
public Executor messageExecutor() {

    ThreadPoolTaskExecutor executor =
            new ThreadPoolTaskExecutor();

    executor.setCorePoolSize(2);
    executor.setMaxPoolSize(5);
    executor.setQueueCapacity(100);
    executor.setThreadNamePrefix("message-");
    executor.initialize();

    return executor;
}

使用:

@Async("messageExecutor")
public void sendMessage() {
}

4. 同类内部调用会失效

错误示例:

@Service
public class OrderService {

    public void createOrder() {
        this.sendMessage();
    }

    @Async
    public void sendMessage() {
    }
}

this.sendMessage() 没有经过 Spring 代理对象,异步不会生效。

推荐拆分到另一个 Bean:

@Service
@RequiredArgsConstructor
public class OrderService {

    private final MessageService messageService;

    public void createOrder() {
        messageService.sendMessage();
    }
}

5. private 方法不会生效

错误写法:

@Async
private void sendMessage() {
}

代理对象无法正常代理私有方法。

一般应该使用:

public

6. 异常不容易被调用者捕获

返回 void 时:

@Async
public void execute() {
    throw new RuntimeException("异步异常");
}

调用方无法通过普通 try-catch 捕获。

可以返回:

CompletableFuture<T>

例如:

@Async("messageExecutor")
public CompletableFuture<String> execute() {

    return CompletableFuture.completedFuture(
            "success"
    );
}

7. 事务上下文不会自动传递

异步方法运行在另一个线程。

原线程中的:

事务
ThreadLocal
请求上下文
部分安全上下文

不一定自动传递。

因此不要默认认为异步方法和调用方处于同一个事务。


8. 异步任务可能丢失

如果应用刚提交异步任务就关闭,任务可能没有完成。

重要业务不要只依赖内存异步线程。

例如:

支付通知
订单状态同步
重要消息发送

更适合使用消息队列。


九、MyBatis 一级缓存避坑

MyBatis 一级缓存默认存在,作用范围是:

同一个 SqlSession

在 Spring 项目中,通常可以理解为:

同一个事务中的同一个 SqlSession

1. 基本效果

User user1 =
        userMapper.selectById(1001L);

User user2 =
        userMapper.selectById(1001L);

如果处于同一个 SqlSession,第二次查询可能直接从一级缓存获取,不再执行 SQL。


2. 增删改会清空缓存

例如:

User user1 =
        userMapper.selectById(1001L);

userMapper.updateById(user);

User user2 =
        userMapper.selectById(1001L);

执行更新后,一级缓存通常会被清空。

第二次查询会重新访问数据库。


3. 不同 SqlSession 不共享

两个不同请求、两个不同事务,通常不共享一级缓存。

因此一级缓存不能当成全局缓存使用。

它和 Redis 完全不是一个概念。


4. 同一个对象引用可能带来误解

第一次查询:

User user1 =
        userMapper.selectById(1001L);

修改对象但没有执行更新:

user1.setUsername("lisi");

再次查询:

User user2 =
        userMapper.selectById(1001L);

在某些情况下,可能直接从一级缓存返回对象。

此时:

user1 == user2

可能是:

true

看起来像数据库已经变化,实际上可能只是内存中的缓存对象被修改。


5. 批量操作和循环查询要注意

例如:

for (Long id : ids) {
    userMapper.selectById(id);
}

如果数据量很大,会产生大量查询,也可能让本地缓存占用更多内存。

更合理的方式是批量查询:

userMapper.selectBatchIds(ids);

6. 手动清理一级缓存

可以调用:

sqlSession.clearCache();

但一般不需要手动清理。

如果业务频繁依赖手动清缓存,通常说明事务边界或查询设计需要调整。


7. 一级缓存的核心注意点

可以简单记住:

作用域是 SqlSession
不能跨请求共享
增删改后通常清空
返回的可能是同一个对象引用
不能替代 Redis

MyBatis 一级缓只在当前 SqlSession 内生效,不能当成业务缓存
```

这些知识本身不复杂,但如果不了解边界,就很容易在实际项目中出现一些比较隐蔽的问题。

评论

填写昵称与邮箱即可评论,无需登录。

推荐阅读