今天你写 Bug 了吗?

360 阅读3分钟

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。今天又是写 bug 的一天,这是很多程序员经常拿来自嘲的一句话,但是只要多对自己所犯错误做总结,总能在很大程度上避免让人痛心疾首的 bug,本文将对容易出错的代码片段进行剖析并给出一个较优解,同时也会归纳一些有助于代码性能提升的小细节,看看有没有你们踩过的坑呢?

使用 Arrays.asList() 将数组转成 List 后增删元素

错误示范

// 数组转 List 集合
List<String> list = Arrays.asList(arr);

list.add("200"); 
list.remove(1);

分析

  • 使用这种方法转换后的 List ,只能用 set()get()contains() 等方法
  • 不能对集合中的元素进行增删操作,否则报 UnsupportedOperationException 不支持的操作异常 UnsupportedOperationException

改进

  • 如果要创建一个能够添删元素的 ArrayList ,你可以使用下面这种创建方式:
ArrayList<String> strList = new ArrayList<>(Arrays.asList(arr));

strList.add("hello");
strList.remove("hello");

使用 ArrayList 构造器传入数组转换后的 List 集合来创建,底层实则也是在操作数组。

使用循环判断去除 Array / List 中的重复元素

错误示范

String[] arrStr = {"Java", "C++", "Php", "C#", "Python", "C++", "Java"};

List<String> list = new ArrayList<>();
for (int i = 0; i < arrStr.length; i++) {
  if(!list.contains(arrStr[i])) {
      list.add(arrStr[i]);
  }
}

分析

  • 虽然不能说错,就是看着不舒服,代码冗长了些
  • 使用 Set 无重复元素的特性,一行代码就能去重

改进

  • 使用 Set 去重,将数组转换后的 List 集合传到构造器中进行创建:
HashSet<String> set = new HashSet<>(Arrays.asList(arrStr));
arrStr = set.toArray();

使用 for 循环判断数组中是否包含目标值

错误示范

String targetStr = "Python";
String[] arrStr = {"Java", "C++", "Php", "C#", "Python", "C++", "Java"};

// 包含标志
boolean flag = false;

for (int i = 0; i < arrStr.length; i++) {
    if(arrStr[i].equals(targetStr)) {
        flag = true; // 数组中包含
        break;
    }
}

// 或者使用增强 for 循环
for(String str : arrStr) {
    if (str.equals(targetValue)) {
        flag = true; // 数组中包含
        break;
    }
}

System.out.println(targetStr + (flag ? " 包含" : " 不包含") + "在数组中!");

分析

  • 虽然不能说错,就是看着不舒服,代码冗长了些
  • 使用 List 中的静态方法 contains() 查看目标元素是否包含在集合中

改进

  • 调用 Arrays.asList().contains() 判断:
boolean flag = Arrays.asList(arrStr).contains(targetStr);
System.out.println(targetStr + (flag ? " 包含" : " 不包含") + "在数组中!");

循环中多次删除集合中的元素

错误示范

请思考:集合内的元素全部都被移除了吗?

ArrayList<String> list = new ArrayList<>(Arrays.asList("a", "b", "c", "d"));

for (int i = 0; i < list.size(); i++) {
    list.remove(i);
}

System.out.println(list);

答案是否定的!!

分析

  • 第一次循环时,确实把索引 0 位置上的元素移除了,注意下一轮循环时,list.size() => 3

  • 第二次循环时,i => 1,元素 b 原先在索引为 1 的位置上,但是移除 a 元素后,索引发生改变,a 后面的所有元素索引向前移动,即 index - 1,所以在这次循环中并没有把元素 b 移除,而是把它后一个元素给移除了

  • 第三次循环时,i => 2,此时 list["b", "d"] 容量为 2,判断循环条件 2 < list.size = 2 不成立,跳出循环

  • 最终输出结果为:[b, d],你答对了吗?

改进

  • list.size() 会动态获取当前集合中元素个数,也会动态影响循环次数

  • 元素被移除后在此之后的元素会向前移动,即 index - 1

// 清空集合中的所有元素
list.clear();
// 满足条件移除元素
list.removeIf(current -> current.equals(targetElement));
// 或者使用循环
int listSize = list.size();
for (int i = 0; i < listSize; i++) {
    if (list.get(i).equals(targetElement)) {
        list.remove(i);
        break; // 删除完目标元素后,要立马跳出,否则报 IndexOutOfBoundsException 索引越界异常
    }
}

循环 ArrayList 集合移除指定元素

错误示范

// list = {"a", "b", "c", "d"}
for (String s : list) {
    if (s.equals("d")) {
        list.remove(s); // Exception in thread "main" java.util.
    }
}

分析

  • 移除完最后一个元素时,会造成其内部结构和游标的改变,从而引发 ConcurrentModificationException 并发修改异常

改进

  • 非要使用循环迭代的话,就使用迭代器移除指定元素
// 移除的目标元素
String targetElement = "d";

// 用 list 创建出一个迭代器
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
    // iterator.remove(); // Exception in thread "main" java.lang.IllegalStateException(非法状态异常)
    
    String currentElement = iterator.next();
    
    if (currentElement.equals(targetElement)) {
        // list.remove(currentElement); throw ConcurrentModificationException
        iterator.remove();
    }
}

ArrayList 加入大量数据时,不初始化容量

错误示范

public static int length = 100000; // 10 万条数据量
public static List<Integer> list = new ArrayList<>();

public static void addList() {
    for (int i = 0; i < length; i++) {
        list.add(i);
    }
}

分析

  • ArrayList 的默认初始容量为 10 ,要插入大量数据的时候需要不断扩容,而扩容是非常影响性能的。

  • 因此,如果明确了 10 万条数据了,我们可以直接在初始化的时候就应该设置 ArrayList 的容量。

  • 经过测试,10 万条数据插入指定初始容量为 100000 大约比无参构造情况下快 2~3 毫秒,100 万条数据快大约 7~9 毫秒(此处插入数据均为 int 整数)

改进

  • 使用 ArrayList 存储大量数据时,应在初始化时指定容量大小,防止不断扩容,影响性能
public static int length = 100000; // 10 万条数据量
public static List<Integer> list = new ArrayList<>(length);

...

ArrayListLinkedList 选择困难症

区别

  • ArrayList

    • 优点:ArrayList 基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高,因为在内存里是连着放的

    • 缺点:因为地址连续,ArrayList 要移动数据,所以插入和删除操作效率比较低

  • LinkedList

    • 优点:LinkedList 基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作 addremoveLinedList 比较占优势LinkedList 适用于要头尾操作或插入指定位置的场景

    • 缺点:因为 LinkedList 要移动指针,所以查询操作性能比较低

总结

  • 当需要对数据进行对随机访问的情况下,选用 ArrayList

  • 当需要对数据进行多次增加删除修改时,采用 LinkedList

  • 如果容量固定,并且只会添加到尾部,不会引起扩容,优先采用 ArrayList

  • 当然,绝大数业务的场景下,使用 ArrayList 就够了。主要是要注意好避免 ArrayList扩容,以及非顺序的插入

使用不可变对象拼接字符串

错误示范

List<String> words = Arrays.asList("i", "am", "a", "robot", ".", "how", "are", "you", "?");

String sayWord = "";
for (String word : words) {
    sayWord += word + " ";
}

System.out.println(sayWord); // i am a robot . how are you ?

分析

  • 非基本数据类型即引用数据类型 String 类是不可变的,即一旦一个 String 对象被创建后,包含在这个对象中的字符序列是不可改变的(容易产生很多临时变量),直到这个不可变对象被销毁

  • 对应地,会产生很多立即进行垃圾回收的对象,这样一来就会浪费 CPU 的时间和精力,造成性能损耗

改进

  • 创建一个内容可变的字符串对象 StringBuilder 用来生成一个拼接后的字符串
StringBuilder builder = new StringBuilder();
for (String word : words) {
    builder.append(word).append(" ");
}

System.out.println(builder.toString()); // i am a robot . how are you ?

StringBuffer 也是可变对象,为什么不用它呢?和 StringBuilder 有什么区别?大多数情况用哪一个?

  • 其实用哪一个都比使用 + 加号直接拼接字符串强,毕竟它们两都是内容可变的字符串对象,不会产生过多的临时变量

  • StringBufferStringBuilder 构造方法和方法基本相同,两者基本相似,但在性能、线程安全上面却有所不同

  • StringBuffer线程安全的,而 StringBuilder没有实现线程安全功能

  • StringBuffer 性能略低于 StringBuilder

  • 综上,在大多数情况下,则应该优先考虑使用 StringBuilder

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。