在处理大数据量查询时,数据库通常会对sql语句中的参数数量有所限制。例如,一个in子句可能只接受最多500个参数。这意味着当我们需要根据一个包含数千个键的列表从数据库中获取数据时,必须将这个大列表分割成多个小批次进行查询。
原始代码示例展示了这种分批查询的常见实现方式:
AtomicInteger counter = new AtomicInteger(); ListcatList = new ArrayList<>(); // 共享的可变集合 List dogList = new ArrayList<>(); // 共享的可变集合 List numbers = Stream.iterate(1, e -> e + 1) .limit(5000) .collect(Collectors.toList()); // 将大列表分割成多个大小为500的子列表 Collection > partitionedListOfNumbers = numbers.stream() .collect(Collectors.groupingBy(num -> counter.getAndIncrement() / 500)) .values(); // 遍历分批列表,并修改外部集合 partitionedListOfNumbers.stream() .forEach(list -> { List
interimCatList = catRepo.fetchCats(list); // 从数据库获取Cat catList.addAll(interimCatList); // 修改外部catList List interimDogList = dogRepo.fetchDogs(list); // 从数据库获取Dog dogList.addAll(interimDogList); // 修改外部dogList });
上述代码的核心问题在于其使用了共享可变性(Shared Mutability)。catList和dogList是在forEach循环外部声明的,并在循环内部通过addAll方法不断被修改。这种模式在函数式编程中被视为“不纯”的操作,因为它产生了副作用(Side Effect)。共享可变性带来的弊端包括:
Java 8引入的Stream API提供了一种声明式、函数式的数据处理方式,可以有效地避免共享可变性。通过结合map、flatMap和collect操作,我们可以在不修改任何外部状态的情况下,将分批查询的结果聚合到新的集合中。
// 更简洁的数字列表生成方式 Collection> partitionedListOfNumbers = IntStream.rangeClosed(1, 5000) .boxed() // 将IntStream转换为Stream
.collect(Collectors.groupingBy(num -> (num - 1) / 500)) // 修正分组逻辑,确保从0开始计数 .values(); // 获取Cat列表:使用Stream API进行无副作用聚合 List catList = partitionedListOfNumbers.stream() .map(list -> catRepo.fetchCats(list)) // 将每个小批次键列表映射为List .flatMap(List::stream) // 将Stream >扁平化为Stream
.collect(Collectors.toList()); // 收集所有Cat对象到新的List // 获取Dog列表:同样使用Stream API进行无副作用聚合 List dogList = partitionedListOfNumbers.stream() .map(list -> dogRepo.fetchDogs(list)) // 将每个小批次键列表映射为List .flatMap(List::stream) // 将Stream >扁平化为Stream
.collect(Collectors.toList()); // 收集所有Dog对象到新的List
代码解析:
通过这种方式,catList和dogList都是通过流操作的最终结果创建的新集合,避免了在处理过程中对外部共享状态的修改。
由于获取Cat列表和Dog列表的逻辑结构非常相似,我们可以进一步抽象,减少代码重复。这可以通过创建一个通用的辅助方法来实现,该方法接受一个函数作为参数,用于指定如何从数据库获取特定类型的实体。
import java.util.Collection;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
// 假设Cat和Dog类以及catRepo, dogRepo已定义
public class AnimalService {
// 辅助方法,用于通用地从分批的键列表中获取实体
private static List fetchEntities(
Collection> partitionedKeys,
Function, List> fetchFunction) {
return partitionedKeys.stream()
.map(fetchFunction) // 应用传入的获取函数
.flatMap(List::stream)
.collect(Collectors.toList());
}
public static void main(String[] args) {
// 模拟数据仓库
CatRepository catRepo = new CatRepository();
DogRepository dogRepo = new DogRepository();
// 生成并分区键列表
Collection> partitionedListOfNumbers = IntStream.rangeClosed(1, 5000)
.boxed()
.collect(Collectors.groupingBy(num -> (num - 1) / 500))
.values();
// 使用辅助方法获取Cat列表
List catList = fetchEntities(partitionedListOfNumb
ers, catRepo::fetchCats);
System.out.println("Fetched " + catList.size() + " cats.");
// 使用辅助方法获取Dog列表
List dogList = fetchEntities(partitionedListOfNumbers, dogRepo::fetchDogs);
System.out.println("Fetched " + dogList.size() + " dogs.");
}
}
// 模拟实体类和仓库接口
class Cat {}
class Dog {}
class CatRepository {
public List fetchCats(List keys) {
// 模拟数据库查询
return keys.stream().map(k -> new Cat()).collect(Collectors.toList());
}
}
class DogRepository {
public List fetchDogs(List keys) {
// 模拟数据库查询
return keys.stream().map(k -> new Dog()).collect(Collectors.toList());
}
}
通过引入fetchEntities辅助方法,我们不仅避免了共享可变性,还提高了代码的模块化和复用性。Function, List
通过Java Stream API的map、flatMap和collect操作,我们能够以声明式、函数式的方式重构代码,彻底避免了共享可变性问题。这种方法带来了多方面的好处:
注意事项:
通过采纳这些函数式编程原则和Stream API的最佳实践,开发者可以编写出更健壮、更易于维护和扩展的Java应用程序。