本篇文章主要是为了记录工作中经常使用的一些工具,以及解决常见问题的一些常用套路。
一、Java实现List分隔成子List
在工作中经常遇到需要将数组分割成多个子数组,然后进行批量处理的需求。那有没有比较优雅的实现呢?
经过多次实践,总结出 ListUtils.partition 和 Lists.partition 两种较好实现 。下面对这两种实现分别进行说明。
1.1 ListUtils.partition 方法
1.1.1 引入依赖
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
1.1.2 代码演示
public static void main(String[] args) {
//初始化数组
List<Integer> parList = new ArrayList<>();
IntStream.range(0, 30).forEach(parList::add);
//分割成子数组
List<List<Integer>> subList = ListUtils.partition(parList, 10);
//遍历子数组
subList.forEach(list -> {
System.out.println(String.format("subList size:%s", list.size()));
System.out.println(String.format("list:%s", list.toString()));
});
}
1.1.3 输出结果
1.2 Lists.partition 方法
1.2.1 引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
1.2.2 代码演示
public static void main(String[] args) {
//初始化数组
List<Integer> parList = new ArrayList<>();
IntStream.range(0, 30).forEach(parList::add);
//分割成子数组
List<List<Integer>> subList = Lists.partition(parList, 10);
//遍历子数组
subList.forEach(list -> {
System.out.println(String.format("subList size:%s", list.size()));
System.out.println(String.format("list:%s", list.toString()));
});
}
1.2.3 输出结果
1.3 源码分析
1.3.1 ListUtils.partition 源码分析
public static <T> List<List<T>> partition(List<T> list, int size) {
if (list == null) {
throw new NullPointerException("List must not be null");
} else if (size <= 0) {
throw new IllegalArgumentException("Size must be greater than 0");
} else {
return new ListUtils.Partition(list, size);
}
}
最终 ListUtils.partition 调用 ListUtils.Partition 方法来处理。
ListUtils.Partition 源码如下:
private static class Partition<T> extends AbstractList<List<T>>
private final List<T> list;
private final int size;
private Partition(List<T> list, int size) {
this.list = list
this.size = size
}
public List<T> get(int index) {
int listSize = this.size();
ir (index < 0) {
throw new IndexOutOfBoundsException("Index " + index + " must not be negative");
} else ir (index >= listSize) {
throw new IndexOutOfBoundsException("Index " + index + " must be less than size " + listSize);
} else {
int start = index * this.size;
int end = Math.min(start + this.size, this.list.size());
return this.list.subList(start, end);
}
}
public int size() {
return (int)Math.ceil((double)this.list.size() / (double)this.size)
}
public boolean isEmpty() {
return this.list.isEmpty();
}
}
Partition 类作为 ListUtils 静态内部类继承 AbstractList 类。重写了 get 和 size方法。
1.3.2 Lists.partition 源码分析
public static <T> List<List<T>> partition(List<T> list, int size) {
checkNorNull (list) ;
checkArgumenr(size > 0) ;
return (list instanceof RandomAccess)
? new RandomAccessPartition<>(list, size)
: new Partition<>(list, size);
}
Lists.partition 方法最终会调用 new Partition<>(list, size)。
Partition 类源码如下:
Partition 类作为 Lists 静态内部类继承 AbstractList 类。重写了 get 、 size、isEmpty 方法。
二、Future 多线程处理任务
你是否遇到过这种场景,有大量数据需要进行处理,比如几百万、几千万,甚至上亿的数据?
如果使用单线程来处理大量的数据,耗时将会很长,效率太低。你也许会说,这简单啊,用多线程啊。是的,多线程相当于多个单线程同时处理,确实快很多。
那么如何使用多线程来处理?
你会说用线程池哈。哈哈,线程池确实很常用,使用线程池确实能达到预期效果。
如果需要获取线程处理得到的结果,又该如何呢?
Runnable 接口,它的方法没有返回值,不符合要求。
Callable 接口,和 Runnable 接口比,多了一个返回值,符合要求。
2.1 定义线程池
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@Configuration
public class TaskThreadPoolConfig {
private int corePoolSize = 8;
private int maxPoolSize = 8;
private int queueCapacity = 1000;
@Bean(name = "taskExecutor")
public ThreadPoolTaskExecutor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(corePoolSize);
executor.setMaxPoolSize(maxPoolSize);
executor.setQueueCapacity(queueCapacity);
return executor;
}
}
2.2 Future 实践应用
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.IntStream;
import org.springframework.stereotype.Service;
@Service
public class TaskExecutor {
@Resource(name = "taskExecutor")
ThreadPoolTaskExecutor threadPoolTaskExecutor;
public List<Integer> futureList() throws ExecutionException, InterruptedException {
//初始化任务数组
List<Integer> initList = new ArrayList<>();
IntStream.range(0, 30).forEach(initList::add);
List<Future> futureList = new ArrayList<>();
//多线程执行
initList.forEach(id -> {
Future<Integer> future = threadPoolTaskExecutor.submit(() -> getUserId(id));
//将future写入list
futureList.add(future);
});
//获取多线程执行结果
List<Integer> resultList = new ArrayList<>();
for (Future<Integer> future : futureList) {
resultList.add(future.get());
}
return resultList;
}
public Integer getUserId(Integer i) {
return i;
}
}
从上面的例子可以看到:
1 当我们提交一个 Callable 任务后,我们会同时获得一个 Future 对象,
2 主线程某个时刻调用 Future 对象的 get() 方法,就可以获得异步执行的结果。
3 在调用 get()时,如果异步任务已经完成,我们就直接获得结果;如果异步任务还没有完成,那么 get()会阻塞,直到任务完成后才返回结果。
综上所述:我们可以看到,使用 Future 对象的 get() 方法,可能会阻塞,系统的吞吐率很难提高。CompletableFuture 是 Future 的升级版,这里不做深入研究。
三 内存分页
在实际工作中,经常会遇到这样一种场景,需要展示列表,出于对系统性能的考虑,需要对列表进行分页渲染。
3.1 分页代码
import org.apache.commons.lang3.tuple.Pair;
public static void main(String[] args) {
List<Integer> list = Arrays.asList(1, 2, 3, 4, 5, 6);
Pair<Integer, List<Integer>> pair = page(list, 0, 2);
System.out.println(pair.getLeft());
System.out.println(pair.getRight());
}
public static Pair<Integer, List<Integer>> page(List<Integer> list, Integer offset, Integer limit) {
// offset偏移量与total比较
int total = CollectionUtils.isNotEmpty(list) ? list.size() : 0;
if (offset > total) {
return Pair.of(total, Lists.newArrayList());
}
// 计算开始偏移量和最终偏移量
int end = offset + limit;
// 最终偏移量不能大于total
end = Math.min(end, total);
// 分页取子集
List<Integer> result = list.subList(offset, end);
return Pair.of(total, result);
}
3.2 执行结果
四、Java8 stream 实现多字段排序
工作中经常会遇到对字段进行排序的操作,如果不是需要单独定义排序规则的话,可以使用 stream 提供的排序 API,支持多字段排序,简单易用。
4.1 代码实现
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
private Long id;
private String name;
private Integer age;
public static void main(String[] args) {
List<User> userList = new ArrayList<>();
userList.add(User.builder().id(10L).name("zs").age(11).build());
userList.add(User.builder().id(10L).name("zs").age(10).build());
userList.add(User.builder().id(5L).name("ls").age(9).build());
userList.add(User.builder().id(5L).name("mz").age(9).build());
userList.add(User.builder().id(19L).name("ww").age(10).build());
System.out.println("排序之前:");
for (User u:userList) {
System.out.println(u);
}
// 假设我们以id降序、name降序、age升序对数组进行排序
List<User> sort = userList.stream().sorted(Comparator.comparing(User::getId, Comparator.reverseOrder())
.thenComparing(User::getName, Comparator.reverseOrder())
.thenComparing(User::getAge, Comparator.naturalOrder())).collect(Collectors.toList());
System.out.println("排序之后:");
for (User u : sort) {
System.out.println(u);
}
}
}
4.2 运行结果
五、Search Scroll API
主要解决从索引里面取大数据量的场景,Scroll API 分批取数据的方式提高了查询效率。
5.1、Scroll API 代码
// 设置索引
SearchRequest searchRequest = new SearchRequest("posts");
// 设置scroll生命周期
final Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1L));
searchRequest.scroll(scroll);
// 设置搜索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(matchQuery("title", "Elasticsearch"));
// 设置单次查询数目
searchSourceBuilder.size(size);
searchRequest.source(searchSourceBuilder);
// 第一次查询获取结果
SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
String scrollId = searchResponse.getScrollId();
SearchHit[] searchHits = searchResponse.getHits().getHits();
while (searchHits != null && searchHits.length > 0) {
// 构造scroll请求,查询下一页
SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
scrollRequest.scroll(scroll);
searchResponse = client.scroll(scrollRequest, RequestOptions.DEFAULT);
scrollId = searchResponse.getScrollId();
searchHits = searchResponse.getHits().getHits();
}
// 从上下文清除数据
ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
clearScrollRequest.addScrollId(scrollId);
ClearScrollResponse clearScrollResponse = client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
boolean succeeded = clearScrollResponse.isSucceeded();
5.2、Scroll API 流程梳理
1. 通过发送初始 SearchRequest 初始化搜索上下文。
2. 通过在循环中调用 Search Scroll api 检索所有搜索命中,直到不返回任何文档。
3. 处理返回的搜索结果。
4. 创建一个新的 SearchScrollRequest,其中包含最后返回的滚动标识符和滚动间隔。
5. 滚动完成后清除滚动上下文。