Java开发笔记
发表于:2024-10-04 |
字数统计: 3.1k | 阅读时长: 13分钟 | 阅读量:

Java开发笔记

鉴于现实原因,笔记仅局限于Java 8版本。

Stream 流

Java 8 引入了 Stream API,它提供了一种高效且表达性强的方式来处理数据集合。Stream API 支持将数据集合转换为流,然后对其进行各种操作,如筛选、排序、聚合等。

创建 Stream

可以通过多种方式创建 Stream:

从集合或数组创建:

List<String> words = Arrays.asList("apple", "banana", "cherry", "date");
Stream<String> stream = words.stream();

从值创建:

Stream<String> stream = Stream.of("apple", "banana", "cherry", "date");

使用 Stream 工厂方法:

Stream<Integer> evenNumbers = Stream.iterate(0, n -> n + 2).limit(10);
IntStream intStream = IntStream.range(0, 10);

中间操作

中间操作可以对 Stream 进行处理,但不会立即产生结果:

  • filter:筛选元素

    words.stream()
          .filter(s -> s.startsWith("a"))
          .forEach(System.out::println); // apple, cherry
  • map:转换元素

    words.stream()
          .map(String::toUpperCase)
          .forEach(System.out::println); // APPLE, BANANA, CHERRY, DATE
  • sorted:排序元素

    words.stream()
          .sorted()
          .forEach(System.out::println); // apple, banana, cherry, date

终止操作(Terminal Operations)

终止操作会消耗 Stream,并产生最终的结果:

  • forEach:对每个元素执行操作

    words.stream()
          .forEach(System.out::println);
  • collect:将元素收集到一个新的集合中

    List<String> filteredWords = words.stream()
                                     .filter(s -> s.startsWith("a"))
                                     .collect(Collectors.toList());
  • reduce:聚合元素

    String concatenated = words.stream()
                              .reduce("", (s1, s2) -> s1 + " " + s2);
  • count:返回 Stream 中元素的数量

    long count = words.stream().count();

并行 Stream

可以通过将顺序 Stream 转换为并行 Stream 来并行处理数据:

List<Integer> numbers = IntStream.rangeClosed(1, 1000).boxed().collect(Collectors.toList());
long sum = numbers.parallelStream().mapToInt(Integer::intValue).sum();

组合操作

可以将多个操作组合在一起,形成一个流水线:

List<String> filteredAndSorted = words.stream()
                                    .filter(s -> s.startsWith("a"))
                                    .sorted()
                                    .collect(Collectors.toList());

注意事项

  • Stream 操作是惰性的,只有当执行终止操作时才会执行。
  • Stream 只能被消费一次,一旦终止操作被执行,Stream 就会被关闭。
  • Stream API 是对集合操作的补充,而不是替代。

Stream API 提供了一种声明式的处理方式,可以写出更简洁、更易于理解的代码。

处理复杂对象(Stream API 案例)

在Java中,Stream API同样可以用来处理对象集合。处理对象时,你可以使用Stream的各种操作来执行复杂的查询、过滤、映射、聚合等操作。以下是一些处理对象的常见操作:

筛选(Filtering)

假设你有一个Person类,你想从Person对象的列表中筛选出所有年龄大于30岁的对象:

public class Person {
    private String name;
    private int age;

    // 构造函数、getter和setter
}

List<Person> people = Arrays.asList(
    new Person("Alice", 25),
    new Person("Bob", 35),
    new Person("Charlie", 30)
);

List<Person> filteredPeople = people.stream()
    .filter(person -> person.getAge() > 30)
    .collect(Collectors.toList());

映射(Mapping)

如果你想将Person对象的列表转换为名字的列表:

List<String> names = people.stream()
    .map(Person::getName)
    .collect(Collectors.toList());

排序(Sorting)

Person对象的列表按照年龄进行排序:

List<Person> sortedPeople = people.stream()
    .sorted(Comparator.comparingInt(Person::getAge))
    .collect(Collectors.toList());

聚合(Aggregating)

计算Person对象列表中所有人的年龄总和:

int totalAge = people.stream()
    .mapToInt(Person::getAge)
    .sum();

或者使用reduce方法:

int totalAge = people.stream()
    .mapToInt(Person::getAge)
    .reduce(0, (sum, age) -> sum + age);

查找(Finding)

查找第一个年龄大于30岁的Person对象:

Optional<Person> firstOldPerson = people.stream()
    .filter(person -> person.getAge() > 30)
    .findFirst();

收集(Collecting)

Person对象的列表收集到一个新的ArrayList中:

List<Person> personList = people.stream()
    .collect(Collectors.toCollection(ArrayList::new));

或者直接收集到列表:

List<Person> personList = people.stream()
    .collect(Collectors.toList());

分组(Grouping)

按照年龄分组Person对象:

Map<Integer, List<Person>> peopleByAge = people.stream()
    .collect(Collectors.groupingBy(Person::getAge));

部分聚合(Partial Aggregating)

计算Person对象列表中的最大年龄:

OptionalInt maxAge = people.stream()
    .mapToInt(Person::getAge)
    .max();

计数(Counting)

计算Person对象列表中满足条件的对象数量:

long count = people.stream()
    .filter(person -> person.getAge() > 30)
    .count();

去重(Distincting)

获取不重复的Person对象列表:

List<Person> distinctPeople = people.stream()
    .distinct()
    .collect(Collectors.toList());

这些操作可以组合使用,以实现更复杂的数据处理逻辑。使用Stream API处理对象时,你可以轻松地将操作链式调用,编写出更简洁、更易读的代码。

Optional

Optional 类是 Java 8 引入的一个容器类,它用于包含或不包含非空值。Optional 类的设计目的是减少程序中 null 引用的使用,从而减少 NullPointerException 的风险。以下是 Optional 类的一些核心用法:

创建 Optional 对象

  • Optional.of(T value): 创建一个非空的 Optional 对象。如果传递的参数为 null,会抛出 NullPointerException
  • Optional.ofNullable(T value): 创建一个可能为空的 Optional 对象。如果传递的参数为 null,会创建一个空的 Optional 对象。
  • Optional.empty(): 创建一个空的 Optional 对象。

使用 Optional 对象

  • isPresent(): 返回一个布尔值,指示 Optional 是否包含非空值。
  • get(): 如果 Optional 包含非空值,则返回该值;否则抛出 NoSuchElementException
  • ifPresent(Consumer<T> consumer): 如果 Optional 包含非空值,则将该值传递给 consumer 进行消费。
  • orElse(T other): 如果 Optional 包含非空值,则返回该值;否则返回指定的其它值。
  • orElseGet(Supplier<T> other): 如果 Optional 包含非空值,则返回该值;否则返回 other 供应器提供的值。
  • orElseThrow(Supplier<? extends X> exceptionSupplier): 如果 Optional 包含非空值,则返回该值;否则抛出 exceptionSupplier 提供的异常。

转换 Optional 对象

  • map(Function<T, U> mapper): 如果 Optional 包含非空值,则将其映射到另一个对象;否则返回一个空的 Optional
  • flatMap(Function<T, Optional<U>> mapper): 如果 Optional 包含非空值,则将其映射到另一个 Optional;否则返回一个空的 Optional

示例代码

public class OptionalExample {
    public static void main(String[] args) {
        Optional<String> optionalString = Optional.ofNullable("Hello, World!");

        // 使用 ifPresent 消费 Optional 中的值
        optionalString.ifPresent(System.out::println); // 输出 "Hello, World!"

        // 使用 get 获取 Optional 中的值
        String value = optionalString.get(); // 如果 optionalString 为空,这里会抛出异常

        // 使用 orElse 提供默认值
        String orElseValue = optionalString.orElse("Default Value");

        // 使用 orElseGet 获取 Supplier 提供的默认值
        String orElseGetValue = optionalString.orElseGet(() -> "Default Value from Supplier");

        // 使用 orElseThrow 抛出异常
        try {
            String orElseThrowValue = Optional.<String>empty()
                                                 .orElseThrow(IllegalStateException::new);
        } catch (IllegalStateException e) {
            e.printStackTrace();
        }

        // 使用 map 转换 Optional 中的值
        Optional<Integer> optionalLength = optionalString.map(String::length);
        int length = optionalLength.orElse(0); // 如果 optionalString 为空,返回 0

        // 使用 flatMap 处理嵌套的 Optional
        Optional<Optional<String>> nestedOptional = Optional.of(Optional.of("Nested"));
        Optional<String> flatOptional = nestedOptional.flatMap(outer -> outer);
    }
}

Optional 类的引入使得处理空值变得更加安全和方便,它鼓励开发者提前考虑和处理空值的情况,从而提高代码质量。

DTO转换

DTO(Data Transfer Object)转换是软件开发中的一个常见需求,特别是在分层架构中。DTO用于在应用程序的不同层之间传输数据,通常从数据库实体转换而来。以下是DTO转换的最佳案例,展示了如何在Java中使用DTO进行转换。

使用手动映射

在没有使用任何映射工具的情况下,手动映射是最基础的方法。

使用BeanUtils

Apache Commons BeanUtils 提供了一个快速复制属性的方法。

import org.apache.commons.beanutils.BeanUtils;

public class UserService {
    public UserDTO convertToUserDTO(User user) {
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(userDTO, user);
        return userDTO;
    }
}

使用ModelMapper

ModelMapper 是一个强大的映射库,可以轻松地在对象之间映射属性。

import com.rcz.webexample.domain.User;
import com.rcz.webexample.utils.DTO.UserDTO;
import org.modelmapper.ModelMapper;

/**
 * modelmapper 转换器
 */
public class UserModelMapperConvert {

    private final ModelMapper modelMapper = new ModelMapper();

    public UserDTO convert(User user) {
        return modelMapper.map(user, UserDTO.class);
    }

}

使用MapStruct

MapStruct 是一个代码生成器,它基于约定优于配置的原则,自动生成源对象和目标对象之间的映射代码。

import com.rcz.webexample.domain.User;
import com.rcz.webexample.utils.DTO.UserDTO;
import com.rcz.webexample.utils.VO.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;

import java.util.List;

@Mapper(componentModel = "spring")
public interface UserConvert {

    UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);

    User convert(UserDTO userDTO);

    List<UserVO> convert(List<User> user);
}

UserMapper 接口中定义了转换方法,MapStruct 会在编译时自动生成实现类。

这个库的使用和lombok有时会有冲突,需要指定注解处理的顺序才可以,并且jdk8和更高版本的引入方式不同,需要格外注意。

简单来说就是,虽然好用,因为他的映射是包含了类型安全和高性能的代码生成的,但是需要谨慎使用。

使用Spring Boot的转换服务

Spring Boot 提供了一个转换服务,可以很方便地在不同对象之间进行转换。

import com.rcz.webexample.domain.User;
import com.rcz.webexample.utils.DTO.UserDTO;
import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;

/**
 * Spring Boot的转换服务
 */
@Component
public class UserToUserDTOConverter implements Converter<User, UserDTO> {
    @Override
    public UserDTO convert(User user) {
        UserDTO userDTO = new UserDTO();
        userDTO.setUserNo(user.getUserNo());
        userDTO.setUsername(user.getUsername());
        userDTO.setNickname(user.getNickname());
        userDTO.setAddress(user.getAddress());
        userDTO.setAvatar(user.getAvatar());
        userDTO.setFamilyNo(user.getFamilyNo());
        userDTO.setCommunityNo(user.getCommunityNo());
        userDTO.setOccupation(user.getOccupation());
        userDTO.setPoliticalOutlook(user.getPoliticalOutlook());
        userDTO.setMaritalStatus(user.getMaritalStatus());
        userDTO.setGender(user.getGender());
        userDTO.setAge(user.getAge());
        userDTO.setPhone(user.getPhone());
        userDTO.setEmail(user.getEmail());
        userDTO.setRemark(user.getRemark());
        return userDTO;
    }
}

springboot本身的转换服务有一个很尴尬的点,那就是一个类只提供一组对象的单向转换,这就不是很方便了。但是好处是不需要引入额外的库,能在某种程度上简便一些。

API文档

这里主要讨论Swagger 3 在springboot 2.x与3.x版本下的差别。

一般的集成swagger是使用springfox的包,但是他已经不再维护,因为一般的推荐使用 SpringDoc,这是目前推荐的方式来集成 OpenAPI 3(即 Swagger 3)。

springboot 2.x模式下,还可以继续使用springfox的包.

<!-- swagger3-->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-boot-starter</artifactId>
    <version>3.0.0</version>
</dependency>

但是3.x模式无法使用,必须使用springdoc提供的包,并且其中关于字段注解、方法注解,也有了很多改变。

<!--     swagger   -->
<dependency>
    <groupId>org.springdoc</groupId>
    <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
    <version>2.5.0</version>
</dependency>

定时任务

定时任务需要考虑的内容包括是否只是简单的周期性任务,也就是业务具体内容,以及是否需要持久化,一些定时任务设计时考虑的因素,任务执行频率和到期时间检查的逻辑,特别是对于分布式的任务,尽量考虑使用分布式的任务调度系统。

场景:根据任务列表中的任务ID创建定时任务,以检查任务的到期时间并执行相应的操作。

java.util.Timer

使用 Timer 类,你可以为每个任务创建一个 TimerTask,并根据每个任务的到期时间安排任务。

List<Task> tasks = getTaskList(); // 假设这是你从数据库或其他地方获取的任务列表
tasks.forEach(task -> {
    Timer timer = new Timer();
    timer.schedule(new TimerTask() {
        @Override
        public void run() {
            checkAndCompleteTask(task.getId());
        }
    }, task.getDueTime().getTime());
});

ScheduledExecutorService 接口

使用 ScheduledExecutorService,你可以为每个任务安排一个单次执行的任务。

List<Task> tasks = getTaskList();
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(tasks.size());
tasks.forEach(task -> {
    scheduler.schedule(() -> checkAndCompleteTask(task.getId()), task.getDueTime().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
});

Spring 框架的 @Scheduled 注解

在 Spring 中,你可以创建一个方法,遍历任务列表,为每个任务安排一个定时任务。

@Component
public class TaskScheduler {

    @Scheduled(fixedDelay = 10000) // 每10秒检查一次
    public void checkTasks() {
        List<Task> tasks = getTaskList();
        tasks.forEach(task -> {
            if (isDue(task.getId())) {
                completeTask(task.getId());
            }
        });
    }
}

Quartz

使用 Quartz,你可以为每个任务创建一个 Job,并根据到期时间触发。

List<Task> tasks = getTaskList();
Scheduler scheduler = getScheduler(); // 获取 Quartz 的调度器
tasks.forEach(task -> {
    JobDetail job = buildJobDetail(task.getId());
    Trigger trigger = buildTrigger(task.getDueTime());
    scheduler.scheduleJob(job, trigger);
});

Spring Boot 的 @Scheduled 注解

在 Spring Boot 中,你可以使用 @Scheduled 注解来创建一个定时任务,该任务定期检查所有任务的到期时间。

@Component
public class TaskScheduler {

    @Scheduled(cron = "0 * * * * *") // 每分钟检查一次
    public void checkAndCompleteTasks() {
        List<Task> tasks = getTaskList();
        tasks.forEach(task -> {
            if (isDue(task.getId())) {
                completeTask(task.getId());
            }
        });
    }
}
  • TimerScheduledExecutorService 都适用于简单的周期性任务,但它们不支持持久化,如果应用重启,所有未执行的任务都会丢失。
  • Spring 的 @Scheduled 注解 提供了更简洁的代码和易于管理的配置,但它同样不支持任务持久化。
  • Quartz 是一个功能强大的调度库,支持任务持久化和复杂的调度策略,但它的配置相对复杂。
下一篇:
bug记录