Java踩坑记录

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问题

  1. 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));
  1. 空指针: 自定义一个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; // 抛出空指针异常

分析:
三元表达式的类型转化规则:

  1. 若两个表达式类型相同,返回值类型为该类型;
  2. 若两个表达式类型不同,但类型不可转换,返回值类型为 Object 类型;
  3. 若两个表达式类型不同,但类型可以转化,先把包装数据类型转化为基本数据类型,然后按照基本数据类型的转换规则 (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。很容易因为对此不了解,导致默认值不符合预期导致出现问题
腾讯TBS是一款基于Chromium内核的浏览器内核,可以在Android应用实现WebView的功能。如果在使用TBS时出现加载失败的问题,可能是由于以下原因造成的: 1. TBS内核未正确初始化:TBS内核必须在应用启动时进行初始化,否则会导致后续的加载失败。您可以在Application的onCreate()方法添加以下代码进行初始化: ```java QbSdk.initX5Environment(getApplicationContext(), null); ``` 2. TBS内核版本不兼容:如果您的应用使用的TBS内核版本与当前设备上安装的Chrome或WebView版本不兼容,可能会导致TBS内核加载失败。您可以尝试升级或降级TBS内核版本,以解决兼容性问题。 3. 缺少必要的权限:TBS内核需要读取设备存储的权限,如果您的应用未获取相关权限,可能会导致内核加载失败。您可以在Manifest文件添加以下权限声明: ```xml <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> ``` 4. 设备不支持TBS内核:部分设备可能不支持TBS内核,导致加载失败。您可以在加载TBS内核前,使用以下代码检查当前设备是否支持TBS内核: ```java if (QbSdk.isTbsCoreInited()) { // TBS内核已经初始化 } else { // TBS内核未初始化,需要进行初始化 } ``` 如果设备不支持TBS内核,您可以使用系统自带的WebView或其他第三方的WebView替代TBS内核。 5. 其他原因:TBS内核加载失败可能还有其他原因,例如网络连接问题、内存不足等。您可以查看日志信息,寻找更详细的错误信息,以便进一步排查问题。 总之,TBS内核加载失败可能是由于多种原因造成的,需要根据具体情况进行排查和解决。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值