Stream 中 filter
引用类型列表,使用stream filter筛选数据后,如果对筛选后的数据进行操作,会影响原来的列表,例如
List<User> list = Lists.newArrayList(new User("a", "1"), new User("b", "2"));
List<User> filterList = list.stream().filter(e -> e.getName().equals("a")).collect(Collectors.toList());
filterList.forEach(e -> e.setName("c"));
System.out.println(list);
System.out.println(filterList);
// [DemoApplicationTests.User(name=c, age=1), DemoApplicationTests.User(name=b, age=2)]
// [DemoApplicationTests.User(name=c, age=1)]
Stream 中 map、peek、foreach 方法区别
map 和 peek 都是 Stream 提供的流处理方法, peek这个方法主要用于支持 debug 调试,当你想看处于某个特定点的流元素可以使用下面的例子
@Test
public void () {
Stream.of("one", "two", "three", "four")
.filter(e -> e.length() > 3)
.peek(e -> System.out.println("Filtered value: " + e))
.map(String::toUpperCase)
.peek(e -> System.out.println("Mapped value: " + e))
.collect(Collectors.toList());
}
peek 不能修改流中的元素,只能对元素进行打印输出或者其他外部处理操作,但流元素如果是引用类型,peek 却可以达到 map 的效果,例如
private List<User> userList = new ArrayList<User>() {{
add(new User("张三"));
add(new User("李四"));
add(new User("王五"));
add(new User("赵六"));
}};
@Test
public void () {
userList.stream()
.peek(user -> user.setName("peek: " + user.getName()))
.forEach(System.out::println);
}
/*
SteamPeekTest.User(name=peek: 张三)
SteamPeekTest.User(name=peek: 李四)
SteamPeekTest.User(name=peek: 王五)
SteamPeekTest.User(name=peek: 赵六)
*/
Stream 中toMap问题
- key重复: 在后面添加(v1, v2) -> v1 指定选取第一个值, 当key值重复的时候,根据情况而定选取第一个还是第二个
Map<String, String> userMap = userList.stream().collect(Collectors.toMap(User::getUserCode, User::getUserName, (v1, v2) -> v1));
// 合并函数传null会把连个key相同的数据直接丢弃
Map<String, String> userMap = userList.stream().collect(Collectors.toMap(User::getUserCode, User::getUserName, (v1, v2) -> null));
- 空指针: 自定义一个Map来接收,不使用Collectors.toMap(),如果对转换后的顺序有要求,这里还可以使用LinkedHashMap
Map<String, String> userMap = userList.stream().collect(HashMap::new, (map, user) -> map.put(user.getUserCode(), user.getUserName()), HashMap::putAll);
泛型属性拷贝
示例代码如下
@Test
void test() {
UserDO<String> userDO = new UserDO<>();
userDO.setName("张三");
userDO.setAge("20");
UserVO userVO = new UserVO();
BeanUtils.copyProperties(userDO, userVO);
System.out.println(userVO);
}
@Data
class UserDO<T> {
private T name;
private String age;
}
@ToString
@Data
class UserVO {
private String name;
private String age;
}
// DemoApplicationTests.UserVO(name=null, age=20)
三元表达式拆包
boolean condition = false;
Double value1 = 1.0D;
Double value2 = 2.0D;
Double value3 = null;
Double result = condition ? value1 * value2 : value3; // 抛出空指针异常
分析:
三元表达式的类型转化规则:
- 若两个表达式类型相同,返回值类型为该类型;
- 若两个表达式类型不同,但类型不可转换,返回值类型为 Object 类型;
- 若两个表达式类型不同,但类型可以转化,先把包装数据类型转化为基本数据类型,然后按照基本数据类型的转换规则 (byte < short(char) < int < long < float < double) 来转化,返回值类型为优先级最高的基本数据类型。
根据规则分析,表示1(value1 * value2)的类型为基础数据类型double,表达式2(value3)的数据类型为包装类型Double,根据三元表达式的类型转换规则判断,最终的表达式类型为技术数据类型double。所以,当条件表达式condition为false时,需要把控Double对象value3转化为基础数据类型double,于是就调用了value3的doubleValue方法进行拆包,就会抛出空指针异常。
日期格式化
public class DateTest {
public static void main(String[] args) {
Calendar calendar = Calendar.getInstance();
calendar.set(2019, Calendar.AUGUST, 31);
Date strDate = calendar.getTime();
DateFormat formatUpperCase = new SimpleDateFormat("yyyy-MM-dd");
System.out.println("2019-12-31 to yyyy-MM-dd: " + formatUpperCase.format(strDate));
formatUpperCase = new SimpleDateFormat("YYYY-MM-dd");
System.out.println("2019-12-31 to YYYY-MM-dd: " + formatUpperCase.format(strDate));
}
}
// 2019-12-31 to yyyy-MM-dd: 2019-12-31
// 2019-12-31 to YYYY-MM-dd: 2020-12-31
原因:
y:year-of-yera;正正经经的年,即元旦过后;Y:week-based-year;只要本周跨年,那么这周就算入下一年;就比如说今年(2019-2020) 12.31 这一周是跨年的一周,而 12.31 是周二,那使用 YYYY 的话会显示 2020,使用 yyyy 则会从 1.1 才开始算是 2020。
double除以0问题
看一下代码
@Test
void test() {
double d1 = 1.0;
double d2 = 0.0;
int i = 0;
System.out.println(d1 / i);
System.out.println(d2 / i);
}
// Infinity
// NaN
观察结果是,double除以0是不会报“除以0”异常的,但是结果又不是我们想要的,所以需要注意,可以使用Bigdecimal
Executors.newFixedThreadPool
public class NewFixedTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(() -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
}
}
}
使用newFixedThreadPool创建的线程池,它默认是无界的阻塞队列,如果任务过多,会导致OOM问题。
//阻塞队列是LinkedBlockingQueue,并且是使用的是无参构造函数
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//无参构造函数,默认最大容量是Integer.MAX_VALUE,相当于无界的阻塞队列的了
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
类似的,使用newCachedThreadPool创建线程池也会存在任务过多导致的OOM问题
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
线程池拒绝策略的坑,使用不当导致阻塞
线程池主要有四种拒绝策略,如下:
- AbortPolicy: 丢弃任务并抛出RejectedExecutionException异常。(默认拒绝策略)
- DiscardPolicy:丢弃任务,但是不抛出异常。
- DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务。
- CallerRunsPolicy:由调用方线程处理该任务。
如果线程池拒绝策略设置不合理,就容易有坑。我们把拒绝策略设置为DiscardPolicy或DiscardOldestPolicy并且在被拒绝的任务,Future对象调用get()方法,那么调用线程会一直被阻塞。
public class DiscardThreadPoolTest {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 一个核心线程,队列最大为1,最大线程数也是1.拒绝策略是DiscardPolicy
ThreadPoolExecutor executorService = new ThreadPoolExecutor(1, 1, 1L, TimeUnit.MINUTES,
new ArrayBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
Future f1 = executorService.submit(()-> {
System.out.println("提交任务1");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Future f2 = executorService.submit(()->{
System.out.println("提交任务2");
});
Future f3 = executorService.submit(()->{
System.out.println("提交任务3");
});
System.out.println("任务1完成 " + f1.get());// 等待任务1执行完毕
System.out.println("任务2完成" + f2.get());// 等待任务2执行完毕
System.out.println("任务3完成" + f3.get());// 等待任务3执行完毕
executorService.shutdown();// 关闭线程池,阻塞直到所有任务执行完毕
}
}
运行代码,会发现程序一直在阻塞中…这是因为DiscardPolicy拒绝策略,是什么都没做
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Creates a {@code DiscardPolicy}.
*/
public DiscardPolicy() { }
/**
* Does nothing, which has the effect of discarding task r.
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
再来看看线程池 submit 的方法
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
//把Runnable任务包装为Future对象
RunnableFuture<Void> ftask = newTaskFor(task, null);
//执行任务
execute(ftask);
//返回Future对象
return ftask;
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; //Future的初始化状态是New
}
再来看看Future的get() 方法
//状态大于COMPLETING,才会返回,要不然都会阻塞等待
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}
FutureTask的状态枚举
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
}
FutureTask的状态大于COMPLETING才会返回,要不然都会一直阻塞等待。又因为拒绝策略啥没做,没有修改FutureTask的状态,因此FutureTask的状态一直是NEW,所以它不会返回,会一直等待。
这个问题,可以使用别的拒绝策略,比如CallerRunsPolicy,它让主线程去执行拒绝的任务,会更新FutureTask状态。如果确实想用DiscardPolicy,则需要重写DiscardPolicy的拒绝策略。
提醒: 使用自定义线程池,最好给一个命名,方便后面排查问题,例如
public class ThreadTest {
public static void main(String[] args) throws Exception {
ThreadPoolExecutor executorOne = new ThreadPoolExecutor(5, 5, 1,TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(20),new CustomizableThreadFactory("Tianluo-Thread-pool"));
executorOne.execute(()->{
System.out.println("关注公众号:捡田螺的小男孩");
throw new NullPointerException();
});
}
}
ThreadLocal与线程池搭配,线程复用,导致信息错乱
private static final ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null);
public Map getUserInfoById(@RequestParam("userId") Integer userId) {
//设置用户信息之前先查询一次ThreadLocal中的用户信息
String before = Thread.currentThread().getName() + ":" + currentUser.get();
//设置用户信息到ThreadLocal
currentUser.set(userId);
//设置用户信息之后再查询一次ThreadLocal中的用户信息
String after = Thread.currentThread().getName() + ":" + currentUser.get();
//汇总输出两次查询结果
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
return result;
}
按理说,每次获取的before应该都是null,但是呢,程序运行在 Tomcat 中,执行程序的线程是Tomcat的工作线程,而Tomcat的工作线程是基于线程池的,线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从 ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息。
如果把tomcat的工作线程设置为1,再运行程序,结果就正常了,所以,使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据,例如:
@GetMapping("right")
public Map right(@RequestParam("userId") Integer userId) {
String before = Thread.currentThread().getName() + ":" + currentUser.get();
currentUser.set(userId);
try {
String after = Thread.currentThread().getName() + ":" + currentUser.get();
Map result = new HashMap();
result.put("before", before);
result.put("after", after);
return result;
} finally {
//在finally代码块中删除ThreadLocal中的数据,确保数据不串
currentUser.remove();
}
}
List.subList
有如下代码,猜猜运行结果
// 初始化 list 为 { 1, 2, 3, 4, 5 }
List<Integer> list = new ArrayList<>();
for (int i = 1; i <= 5; i++) {
list.add(i);
}
// 取前 3 个元素作为 subList,操作 subList
List<Integer> subList = list.subList(0, 3);
subList.add(6);
System.out.println(list.size());
实际的运行结果是6,而不是我们一眼看出的5,这是因为:
subList 返回的是原 List 的一个 视图,而不是一个新的 List,所以对 subList 的操作会反映到原 List 上,反之亦然;并且,如果原 List 在 subList 操作期间发生了结构修改,那么 subList 的行为就是未定义的(实际表现为抛异常)。
private class SubList extends AbstractList<E> implements RandomAccess {
private final AbstractList<E> parent;
// ...
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
// ...
this.modCount = ArrayList.this.modCount;
}
public E set(int index, E e) {
// ...
checkForComodification();
// ...
ArrayList.this.elementData[offset + index] = e;
// ...
}
public E get(int index) {
// ...
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
public void add(int index, E e) {
// ...
checkForComodification();
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
// ...
}
public E remove(int index) {
// ...
checkForComodification();
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
// ...
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
// ...
}
可以看到几乎所有的读写操作都是映射到 ArrayList.this、或者 parent(即原 List)上的,包括 size、add、remove、set、get、removeRange、addAll 等等。我们还可以试下,在声明 subList 后,如果对原 List 进行元素增删操作,然后再读写 subList,基本都会抛出此异常。因为 subList 里的所有读写操作里都调用了 checkForComodification(),这个方法里检验了 subList 和 List 的 modCount 字段值是否相等,如果不相等则抛出异常。modCount 字段定义在 AbstractList 中,记录所属 List 发生 结构修改 的次数。结构修改 包括修改 List 大小(如 add、remove 等)、或者会使正在进行的迭代器操作出错的修改(如 sort、replaceAll 等)。
Mybatis if test
在写sql xml时,平时写的都是以判断空属性是否为空,例如
<if test="type != null and type !=''">
and status = 1
</if>
最近刚好要写一个与上边不一样的写法,但是还是以上面的为基础,是对属性的值进行判断: 判断type属性的值
<if test="type !=null and type !=''">
<if test="type == '1'">
and status = "xxx"
</if>
<if test="type == '2'">
and status = "xxx"
</if>
</if>
结果就是无论我传什么值,SQL中的status都会生效。 有两种解决方案
<if test='type == "1"'>
and status = "xxx"
</if>
<if test="type == '1'.toString()">
and status = "xxx"
</if>
Finally重新抛出异常
先看示例代码
public void wrong() {
try {
log.info("try");
//异常丢失
throw new RuntimeException("try");
} finally {
log.info("finally");
throw new RuntimeException("finally");
}
}
运行程序会发现,只抛出了finally异常,这是因为一个方法不会出现两个异常,所以finally的异常会把try的异常覆盖。正确的使用方式应该是,finally 代码块负责自己的异常捕获和处理。
public void right() {
try {
log.info("try");
throw new RuntimeException("try");
} finally {
log.info("finally");
try {
throw new RuntimeException("finally");
} catch (Exception ex) {
log.error("finally", ex);
}
}
}
JSON序列化Long类型被转成Integer类型
public class JSONTest {
public static void main(String[] args) {
Long idValue = 3000L;
Map<String, Object> data = new HashMap<>(2);
data.put("id", idValue);
data.put("name", "张三");
Assert.assertEquals(idValue, (Long) data.get("id"));
String jsonString = JSON.toJSONString(data);
// 反序列化时Long被转为了Integer
Map map = JSON.parseObject(jsonString, Map.class);
Object idObj = map.get("id");
System.out.println("反序列化的类型是否为Integer:"+(idObj instanceof Integer));
Assert.assertEquals(idValue, (Long) idObj);
}
}
// ture
这是因为:序列化为Json字符串后,Josn字符串是没有Long类型的,而且反序列化回来如果也是Object接收,数字小于Interger最大值的话,给会转成Integer
@Builder注解问题
@Builder 是lombook中的一个注解,加在类上,可以帮我创建对象,但是他有一些坑:
- 使用@Builder构建对象的时候如果不显式的对某变量赋值的话默认就是null,因为这个变量此时是在Builder类里的,通过调用build()方法生成具体该类则是通过私有构造函数来实例化,默认是全参数的构造函数,所以一般情况下@Builder 和@AllArgsConstructor,@NoArgsConstructor这三注解要配合使用
- 继承关系时,子类需要使用 @SuperBuilder。对象继承后,子类的 Builder 因为构造函数的问题,使用不当大概率会报错,并且无法设置父类的属性,还需要使用 @SuperBuilder 来解决问题
- 设置默认值需要使用 @Builder.Default。很容易因为对此不了解,导致默认值不符合预期导致出现问题