前言
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。今天又是写
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);
...
ArrayList 和 LinkedList 选择困难症
区别
-
ArrayList-
优点:
ArrayList基于动态数组的数据结构,因为地址连续,一旦数据存储好了,查询操作效率会比较高,因为在内存里是连着放的 -
缺点:因为地址连续,
ArrayList要移动数据,所以插入和删除操作效率比较低
-
-
LinkedList-
优点:
LinkedList基于链表的数据结构,地址是任意的,所以在开辟内存空间的时候不需要等一个连续的地址。对于新增和删除操作add和remove,LinedList比较占优势。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 有什么区别?大多数情况用哪一个?
-
其实用哪一个都比使用
+加号直接拼接字符串强,毕竟它们两都是内容可变的字符串对象,不会产生过多的临时变量 -
StringBuffer和StringBuilder构造方法和方法基本相同,两者基本相似,但在性能、线程安全上面却有所不同 -
StringBuffer是线程安全的,而StringBuilder则没有实现线程安全功能 -
StringBuffer性能略低于StringBuilder -
综上,在大多数情况下,则应该优先考虑使用
StringBuilder类
结尾
撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。