业务开发踩坑之路

1.参数判断错误,导致正则校验异常

原因

由于jdbcTemplate获取mysql表数据,如果参数存在空获取null,如果参数是一个空字符串,数据能查询出来,但是参数判断不等于null,这样就存在空数组。然后进行list的中方法包含进行对比,可能会导致导致匹配到所有数据。

代码

if(rs.getString("keyword") !=null){
	List keyword = Arrays.asList(rs.getString("keyword")
	.split(","));
	result.setKeyword(keyword);
}
Optional<String> keyword = results.stream().filter(e -> {
            if (CollectionUtils.isNotEmpty(e.getKeyword())) {
            return e.getKeyword().stream().anyMatch(question::contains); }
            return false;        
}).findFirst();
if (keyword .isPresent()) {
    return Collections.singletonList(KeyWord.get());
}

解决办法:

if(!StringUtils.isEmpty(rs.getString("keyword"))){
	List keyword = Arrays.asList(rs.getString("keyword")
	.split(","));
	result.setKeyword(keyword);
}

2.CopyOnWriteArrayList、ArrayList、Vector

使用场景

当我们往一个容器添加元素的时候,不直接往当前容器添加,而是先将当前容器进行Copy,复制出一个新的容器,然后新的容器里添加元素,添加完元素之后,再将原容器的引用指向新的容器。

区别

CopyOnWriteArrayListjava.util.concurrent包提供的方法,它实现了读操作无锁,写操作则通过操作底层数组的新副本来实现,是一种读写分离的并发策略。
1、ArrayList是线程不安全的;
2、Vector是比较古老的线程安全的,但性能不行;
3、CopyOnWriteArrayList在兼顾了线程安全的同时,又提高了并发性,性能比Vector有不少提高。
大量写的场景(10 万次 add 操作),CopyOnWriteArray 几乎比同步的 ArrayList 慢一百倍。
大量读的场景下(100 万次 get 操作),CopyOnWriteArray 又比同步的 ArrayList 快五倍以上。

3.JAVA 8 List的for循环的并行方法 parallelStream()、stream().parallel()

for循环并行的坑

Java 8 的 parallel stream 功能,可以让我们很方便地并行处理集合中的元素,其背后是共享同一个 ForkJoinPool,默认并行度是 CPU 核数 -1。

解决办法:

对于 CPU 绑定的任务来说,使用这样的配置比较合适,但如果集合操作涉及同步 IO 操作的话(比如数据库操作、外部服务调用等),建议自定义一个 ForkJoinPool(或普通线程池)

// 示例:自定义线程池
ForkJoinPool forkJoinPool = new ForkJoinPool(8);
// 这里是从数据库里查出来的一批代理 ip
List<ProxyList> records = new ArrayList<>();
// 找出失效的代理 ip
List<String> needDeleteList = forkJoinPool.submit(() -> records.parallelStream()
    .map(ProxyList::getIpPort)
    .filter(IProxyListTask::isFailed)
    .collect(Collectors.toList())
).fork().join();

4.响应式流(Reactive Streams) API

什么是响应式流:

对于Java程序员,Reactive Streams是一个API。Reactive Streams为我们提供了Java中的Reactive Programming的通用API。Reactive Streams已成为官方Java 9 API的一部分,Java9中Flow类下的内容与Reactive Streams完全一致。Reactive Streams 基于流进行处理可以更高效地使用内存,把业务逻辑从模板代码中抽离出来,把代码从并发、同步问题中解脱出来,同时还可以提高代码的可读性。Stream 更侧重于流的过滤、映射、整合、收集 。而 Flow 更侧重于流的产生与消费。

<dependency>
  <groupId>org.reactivestreams</groupId>
  <artifactId>reactive-streams</artifactId>
  <version>1.0.3</version>
</dependency>
<dependency>
  <groupId>org.reactivestreams</groupId>
  <artifactId>reactive-streams-tck</artifactId>
  <version>1.0.3</version>
  <scope>test</scope>
</dependency>

API方法:

Flow.Processor<T,R>
Flow.Publisher<T>
Flow.Subscriber<T>
Flow.Subscription
//发布者
public  interface  Publisher < T > {
    public  void  subscribe(Subscriber <super  T >  s);
}
//订阅者
public  interface  Subscriber < T > {
    public  void  onSubscribe(Subscription  s);
    public  void  onNext(T  t);
    public  void  onError(Throwable  t);
    public  void  onComplete();
}
//表示Subscriber消费Publisher发布的一个消息的生命周期
public interface Subscription {
    public void request(long n);
    public void cancel();
}
//处理器,表示一个处理阶段,它既是订阅者也是发布者,并且遵守两者的契约
public interface Processor<T, R> extends Subscriber<T>, Publisher<R> {  
}

5.使用 Arrays.asList 把数据转换为 List 的三个坑

例如:

1、不能直接使用 Arrays.asList 来转换基本类型数组。

int[] arr1 = {1, 2, 3};
List list1 = Arrays.stream(arr1).boxed().collect(Collectors.toList());
log.info("list:{} size:{} class:{}", list1, list1.size(), list1.get(0).getClass());
Integer[] arr2 = {1, 2, 3};
List list2 = Arrays.asList(arr2);
log.info("list:{} size:{} class:{}", list2, list2.size(), list2.get(0).getClass());

2、Arrays.asList 返回的 List 不支持增删操作。
Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类 ArrayList。ArrayList 内部类继承自 AbstractList 类,并没有覆写父类的 add 方法,而父类中 add 方法的实现,就是抛出 UnsupportedOperationException。

public static <T> List<T> asList(T... a) {
    return new ArrayList<>(a);
}
private static class ArrayList<E> extends AbstractList<E>
    implements RandomAccess, java.io.Serializable
{
    private final E[] a;
    ArrayList(E[] array) {
        a = Objects.requireNonNull(array);
    }
...
    @Override
    public E set(int index, E element) {
        E oldValue = a[index];
        a[index] = element;
        return oldValue;
    }
    ...
}
public abstract class AbstractList<E> extends AbstractCollection<E> implements List<E> {
...
public void add(int index, E element) {
        throw new UnsupportedOperationException();
    }
}

3、对原始数组的修改会影响到我们获得的那个 List。

String[] arr = {"1", "2", "3"};
List list = new ArrayList(Arrays.asList(arr));
arr[1] = "4";
try {
    list.add("5");
} catch (Exception ex) {
    ex.printStackTrace();
}
log.info("arr:{} list:{}", Arrays.toString(arr), list);

6.使用 List.subList 进行切片操作居然会导致 OOM

代码:

private static List<List<Integer>> data = new ArrayList<>();
private static void oom() {
    for (int i = 0; i < 1000; i++) {
        List<Integer> rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList());
        data.add(rawList.subList(0, 1));
    }
}

出现 OOM 的原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。
解决办法:
一种是,不直接使用 subList 方法返回的 SubList,而是重新使用 new ArrayList,在构造方法传入 SubList,来构建一个独立的 ArrayList;
一种是,对于 Java 8 使用 Stream 的 skip 和 limit API 来跳过流中的元素,以及限制流中元素的个数,同样可以达到 SubList 切片的目的。

//方式一:
List<Integer> subList = new ArrayList<>(list.subList(1, 4));
//方式二:
List<Integer> subList = list.stream().skip(1).limit(3).collect(Collectors.toList());

7.mapSearch、listSearch

搜索 ArrayList 的时间复杂度是 O(n),而 HashMap 的 get 操作的时间复杂度是 O(1)。所以,要对大 List 进行单值搜索的话,可以考虑使用 HashMap,其中 Key 是要搜索的值,Value 是原始对象,会比使用 ArrayList 有非常明显的性能优势。
代码:

import cn.hutool.core.date.StopWatch;
import jdk.nashorn.internal.ir.debug.ObjectSizeCalculator;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;   
private static Object mapSearch(int elementCount, int loopCount) {
        Map<Integer, Order> map = IntStream.rangeClosed(1, elementCount).boxed().collect(Collectors.toMap(Function.identity(), i -> new Order(i)));
        IntStream.rangeClosed(1, loopCount).forEach(i -> {
            int search = ThreadLocalRandom.current().nextInt(elementCount);
            Order result = map.get(search);
        });
        return map;
}
private static Object listSearch(int elementCount, int loopCount) {
        List<Order> list = IntStream.rangeClosed(1, elementCount).mapToObj(i -> new Order(i)).collect(Collectors.toList());
        IntStream.rangeClosed(1, loopCount).forEach(i -> {
            int search = ThreadLocalRandom.current().nextInt(elementCount);
            Order result = list.stream().filter(order -> order.getOrderId() == search).findFirst().orElse(null);
        });
        return list;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class Order { 
        private int orderId;
}
public static void main(String[] args) {
        int elementCount = 1000000;
        int loopCount = 1000;
        StopWatch stopWatch = new StopWatch();
        stopWatch.start("listSearch");
        Object list = listSearch(elementCount, loopCount);
        System.out.println(ObjectSizeCalculator.getObjectSize(list));
        stopWatch.stop();
        stopWatch.start("mapSearch");
        Object map = mapSearch(elementCount, loopCount);
        stopWatch.stop();
        System.out.println(ObjectSizeCalculator.getObjectSize(map));
        System.out.println(stopWatch.prettyPrint());
}
20861992
72388672
StopWatch '': running time = 3506699764 ns
---------------------------------------------
ns         %     Task name
---------------------------------------------
3398413176  097%  listSearch
108286588  003%  mapSearch

仅仅是 1000 次搜索,listSearch 方法耗时 3.3 秒,而 mapSearch 耗时仅仅 108 毫秒。

8.修复和定位恼人的空指针问题

常见的场景:

1、参数值是 Integer 等包装类型,使用时因为自动拆箱出现了空指针异常;
2、字符串比较出现空指针异常;
3、诸如 ConcurrentHashMap 这样的容器不支持 Key 和 Value 为 null,强行 put null 的 Key 或 Value 会出现空指针异常;
4、A 对象包含了 B,在通过 A 对象的字段获得 B 之后,没有对字段判空就级联调用 B 的方法出现空指针异常;
5、方法或远程服务返回的 List 不是空而是 null,没有进行判空就直接调用 List 的方法出现空指针异常。
解决办法:使用Optional方法、StringUtils方法、MapUtil方法、CollectionUtils方法进行判断。
6、MySQL 中 sum 函数没统计到任何记录时,会返回 null 而不是 0,可以使用 IFNULL 函数把 null 转换为 0;7、MySQL 中 count 字段不统计 null 值,COUNT(*) 才是统计所有记录数量的正确方式。
8、MySQL 中使用诸如 =、<、> 这样的算数比较操作符比较 NULL 的结果总是 NULL,这种比较就显得没有任何意义,需要使用 IS NULL、IS NOT NULL 或 ISNULL() 函数来比较。

9.用好Java 8的日期时间类,少踩一些坑

在 Java 8 之前,我们处理日期时间需求时,使用 Date、Calender 和 SimpleDateFormat,来声明时间戳、使用日历处理日期和格式化解析日期时间。但是,这些类的 API 的缺点比较明显,比如可读性差、易用性差、使用起来冗余繁琐,还有线程安全问题。
Java 8 推出了新的时间日期类 ZoneId、ZoneOffset、LocalDateTime、ZonedDateTime 和 DateTimeFormatter,处理时区问题更简单清晰。

System.out.println("//本月的第一天");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()));
System.out.println("//今年的程序员日");
System.out.println(LocalDate.now().with(TemporalAdjusters.firstDayOfYear()).plusDays(255));
System.out.println("//今天之前的一个周六");
System.out.println(LocalDate.now().with(TemporalAdjusters.previous(DayOfWeek.SATURDAY)));
System.out.println("//本月最后一个工作日");
System.out.println(LocalDate.now().with(TemporalAdjusters.lastInMonth(DayOfWeek.FRIDAY)));

日期工具二个坑:
1、定义的 static 的 SimpleDateFormat 可能会出现线程安全问题。
使用一个 100 线程的线程池,循环 20 次把时间格式化任务提交到线程池处理,每个任务中又循环 10 次解析 2020-08-03 11:12:13 这样一个时间表示、运行程序后大量报错,且没有报错的输出结果也不正常,比如 2020 年解析成了 1212 年。

ExecutorService threadPool = Executors.newFixedThreadPool(100);
for (int i = 0; i < 20; i++) {
    //提交20个并发解析时间的任务到线程池,模拟并发环境
    threadPool.execute(() -> {
        for (int j = 0; j < 10; j++) {
            try {
                System.out.println(simpleDateFormat.parse("2020-08-03 11:12:13"));
            } catch (ParseException e) {
                e.printStackTrace();
            }
        }
    });
}
threadPool.shutdown();
threadPool.awaitTermination(1, TimeUnit.HOURS);

2、当需要解析的字符串和格式不匹配的时候,SimpleDateFormat 表现得很宽容,还是能得到结果。比如,我们期望使用 yyyyMM 来解析 20160901 字符串:居然输出了 2091 年 1 月 1 日,原因是把 0901 当成了月份,相当于 75 年:

String dateString = "20160901";
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMM");
System.out.println("result:" + dateFormat.parse(dateString));

对于 SimpleDateFormat 的这二个坑,我们使用 Java 8 中的 DateTimeFormatter 就可以避过去。

10.搞定代码重复的三个绝招

原因:

如果多处重复代码实现完全相同的功能,很容易修改一处忘记修改另一处,造成 Bug;
有一些代码并不是完全重复,而是相似度很高,修改这些类似的代码容易改(复制粘贴)错,把原本有区别的地方改为了一样。

解决办法:

1、利用工厂模式 + 模板方法模式,消除 if…else 和重复代码
2、利用注解 + 反射消除重复代码
3、利用属性拷贝工具消除重复代码

ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, "id");
return orderDO;

可以使用类似 BeanUtils 这种 Mapping 工具来做 Bean 的转换,copyProperties 方法还允许我们提供需要忽略的属性。

11.如何尽量避免踩坑

第一,遇到自己不熟悉的新类,在不了解之前不要随意使用。
第二,尽量使用更高层次的框架。
第三,关注各种框架和组件的安全补丁和版本更新。
第四,尽量少自己造轮子,使用流行的框架。
第五,开发的时候遇到错误,除了搜索解决方案外,更重要的是理解原理。
第六,网络上的资料有很多,但不一定可靠,最可靠的还是官方文档。
第七,做好单元测试和性能测试。
第八,做好设计评审和代码审查工作。
第九,借助工具帮我们避坑。
第十,做好完善的监控报警。

12.Jmap、Jcmd

在JDK1.7以后,新增了一个命令行工具 jcmd。他是一个多功能的工具,可以用它来导出堆、查看Java进程、导出线程信息、执行GC、还可以进行采样分析(jmc 工具的飞行记录器)。
jmap命令是一个可以输出所有内存中对象的工具,甚至可以将VM 中的heap,以二进制输出成文本。打印出某个java进程(使用pid)内存内的,所有"对象"的情况(如:产生那些对象,及其数量)。

#在没有jcmd的时候,我们也有很多找出JVM进程的方法
jcmd -l
#列出所有的JVM程序,找到你感兴趣的进程id
jcmd 9350 help

你可能感兴趣的:(算法,面试,JAVA开发,业务开发踩坑之路,多线程,stream,面试)