作者:FIN技术铺
文章分类:研发技术工具/SONAR扫描 投稿日期: 2024-7-19
SONAR系统基于项目配置的扫描规则识别交付代码当中存在的问题。本文针对日常扫描环节经常出现的问题进行梳理,盘点日常用户支持过程中发现典型案例,向用户解释为什么会被识别以及识别之后如何修改的问题。文章从风格&格式类、OOP类、集合类等典型场景进行归类,盘点各类场景下的典型问题,针对被规则命中的问题进行解释、基于示例说明原因并且给出解决办法。在与用户进行讨论过程当中,有意识就问题的根本原因进行分析记录,并摘选成文,供编码参考。
01 集合类
1. HashMap和ConcurrentHashMap的Key为自定义对象时,必须重写对象的equals方法和hashCode方法;Set中的元素为自定义对象时也必须重写这两个方法;
【解释】:
当我们使用Java中的HashMap, ConcurrentHashMap或Set(如HashSet和LinkedHashSet)等集合来存储自定义对象时,这些集合内部使用equals()方法来判断两个对象是否相等,并使用hashCode()方法来计算对象的哈希码。因此,为了确保这些集合能正确地工作,我们通常需要为自定义对象重写这两个方法。
- 为什么需要重写equals()和hashCode()方法?
默认情况下,Object类提供的equals()方法比较的是两个对象的引用是否相等,而不是它们的内容。同样,默认的hashCode()方法返回的是对象的内存地址的某种整数表示。这意味着,如果你不重写这些方法,当你尝试在集合中查找、删除或检查对象是否存在时,集合将使用默认的equals()和hashCode()方法,这可能导致不正确的行为。
2. 举例解释
假设我们有一个简单的Person类,如下所示:
public class Person {
private String name;
private int age;
// 构造方法、getter和setter方法省略...
}
现在,如果我们创建一个HashMap<Person, String>来存储人员及其相关信息,并尝试查找某个特定的人:
HashMap<Person, String> people = new HashMap<>();
Person p1 = new Person("Alice", 25);
Person p2 = new Person("Alice", 25); // 与p1有相同的内容,但引用不同
people.put(p1, "Info about Alice");
System.out.println(people.get(p2)); // 期望输出 "Info about Alice",但实际输出为null
在上述代码中,尽管p1和p2有相同的内容,但由于我们没有重写equals()和hashCode()方法,所以它们在HashMap中被视为不同的键。因此,当我们尝试使用p2作为键来获取值时,返回的是null。
为了解决这个问题,我们需要为Person类重写equals()和hashCode()方法:
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && name.equals(person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
现在,当我们在HashMap中使用p1和p2时,它们将被视为相同的键,因为它们的equals()方法返回true并且它们的hashCode()方法有相同的输出。所以上述的查找代码现在会正确输出“Info about Alice”。
同样的原则也适用于Set集合。例如,如果你尝试将一个对象添加到HashSet中两次(基于其内容),而你没有重写这两个方法,那么该对象可能会出现两次,因为默认的equals()和hashCode()方法不会识别它们为相同的对象。
2. ArrayList的subList结果不可强转成ArrayList,否则会抛出 ClassCastException异常,即java.util.RandomAccessSubList cannot be cast to java.util.ArrayList
【解释】:ArrayList 的 subList 方法返回的是 List 接口的一个视图,它实际上是 AbstractList 的一个内部类 SubList 的实例,而不是 ArrayList。因此,尝试将 subList 的结果强制转换为 ArrayList 会导致 ClassCastException。
举一个例子来详细说明:
import java.util.ArrayList;
import java.util.List;
public class SubListExample {
public static void main(String[] args) {
ArrayList<String> originalList = new ArrayList<>();
originalList.add("A");
originalList.add("B");
originalList.add("C");
originalList.add("D");
// 使用subList获取原始列表的一个子列表
List<String> subList = originalList.subList(1, 3);
// 打印子列表的内容
System.out.println("Sublist: " + subList); // 输出: Sublist: [B, C]
// 尝试将子列表强制转换为ArrayList - 这将抛出ClassCastException
ArrayList<String> castedSubList = (ArrayList<String>) subList; // 这里会抛出异常
}
}
当你运行上面的代码时,会抛出以下异常:
Exception in thread "main" java.lang.ClassCastException: java.util.ArrayList$SubList cannot be cast to java.util.ArrayList
at SubListExample.main(SubListExample.java:17)
如果你需要一个 ArrayList 类型的子列表,你可以通过创建一个新的 ArrayList 来达到这个目的:
ArrayList<String> newArrayList = new ArrayList<>(subList);
这样,newArrayList 就是一个新的 ArrayList 实例,包含与 subList 相同的元素。
3. 使用Map的方法keySet()/values()/entrySet()返回集合对象时,不可以对其进行添加元素操作,否则会抛出UnsupportedOperationException异常
【解释】:当我们使用 Map 接口的实现类(如 HashMap、TreeMap 等)的 keySet()、values() 或 entrySet() 方法时,这些方法分别返回该映射中的键集、值集或键-值对集。但需要注意的是,这些方法返回的集合是原始映射的视图,这意味着它们的内容会随原始映射的改变而改变。
然而,这些返回的集合并不支持所有的 Collection 操作。特别是,它们不支持 add 和 addAll 方法,因为映射的键和值有其特定的约束(例如键的唯一性)。如果你尝试在这些集合上调用添加元素的方法,会抛出 UnsupportedOperationException。
下面是一个示例,说明这个行为:
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class MapExample {
public static void main(String[] args) {
Map<String, Integer> map = new HashMap<>();
map.put("one", 1);
map.put("two", 2);
map.put("three", 3);
// 获取键集
Set<String> keys = map.keySet();
System.out.println("Keys: " + keys); // 输出: Keys: [one, two, three]
// 尝试在键集上添加元素 - 这将抛出 UnsupportedOperationException
try {
keys.add("four"); // 这里尝试添加一个新键 "four"
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
}
}
在上面的示例中,我们创建了一个包含三个键值对的 HashMap。然后,我们通过调用 keySet() 方法获取了一个包含映射中所有键的集合。当我们尝试在这个键集上调用 add 方法时,程序抛出了一个 UnsupportedOperationException,因为这个操作是不被支持的。如果你想在映射中添加新的键值对,应该直接调用映射的 put 方法,而不是尝试修改通过这些方法返回的集合。
4. JDK的Collections类返回的对象,如:emptyList()/singletonList()等都是immutable list,不可对其进行添加或者删除元素的操作
【解释】:在Java的Collections类中,提供了一些静态方法用于返回特殊的不可变集合对象,例如emptyList()、singletonList(T o)、unmodifiableList(List<? extends T> list)等。这些方法返回的集合对象都是不可变的(immutable),意味着一旦创建,就不能再修改它们的内容。具体来说,不能添加、删除或更改集合中的元素。
下面是一个示例,演示了如何使用这些方法以及尝试修改这些不可变集合时会发生什么:
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
public class ImmutableListExample {
public static void main(String[] args) {
// 使用Collections.emptyList()创建一个空的不可变列表
List<String> emptyList = Collections.emptyList();
System.out.println("Empty list: " + emptyList); // 输出: Empty list: []
// 尝试向emptyList添加元素 - 抛出UnsupportedOperationException
try {
emptyList.add("item");
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
// 使用Collections.singletonList()创建一个只包含一个元素的不可变列表
List<String> singletonList = Collections.singletonList("singleItem");
System.out.println("Singleton list: " + singletonList); // 输出: Singleton list: [singleItem]
// 尝试向singletonList添加元素 - 抛出UnsupportedOperationException
try {
singletonList.add("anotherItem");
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
// 创建一个可变的ArrayList
List<String> mutableList = new ArrayList<>();
mutableList.add("item1");
mutableList.add("item2");
System.out.println("Mutable list: " + mutableList); // 输出: Mutable list: [item1, item2]
// 使用Collections.unmodifiableList()将可变列表转换为不可变列表
List<String> unmodifiableList = Collections.unmodifiableList(mutableList);
System.out.println("Unmodifiable list: " + unmodifiableList); // 输出: Unmodifiable list: [item1, item2]
// 尝试向unmodifiableList添加元素 - 抛出UnsupportedOperationException
try {
unmodifiableList.add("item3");
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
}
}
在这个示例中,我们创建了三种不同类型的不可变列表:空的不可变列表、只包含一个元素的不可变列表和从可变列表转换而来的不可变列表。然后,我们尝试向这些列表添加元素,每次都会抛出UnsupportedOperationException异常,因为这些列表是不可变的。
5. 使用集合转数组的方法,必须使用集合的toArray(T[] array)方法,传入的是类型完全一致、长度为0的空数组
【解释】:当我们使用Java中的集合(如List、Set等)并希望将其转换为数组时,通常使用集合的toArray()方法。这个方法有两种形式:
- Object[] toArray();
- <T> T[] toArray(T[] a);
其中,第二种形式更为常用,因为它允许我们指定返回数组的类型。
现在,让我们讨论为什么在使用toArray(T[] array)方法时,我们通常传入一个类型完全一致、长度为0的空数组。
原因如下:
- 类型安全:通过传入一个类型化的空数组,我们可以确保返回的数组具有正确的类型。如果不这样做,编译器可能无法推断出正确的类型,从而可能导致ClassCastException。
- 避免不必要的数组创建:如果我们知道集合的大小,我们可以创建一个相应大小的数组来传递给toArray()方法。但是,在很多情况下,我们可能不知道集合的确切大小。通过传递一个长度为0的空数组,我们可以让toArray()方法内部为我们创建一个适当大小的数组,而不必我们自己创建和管理它。
- 代码简洁:传递一个长度为0的空数组比创建一个具有初始大小的数组更简单、更直观。这也有助于保持代码的整洁和易读。
下面是一个示例来说明这一点:
import java.util.ArrayList;
import java.util.List;
public class CollectionToArrayExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
// 使用toArray()方法并传入一个长度为0的空数组
String[] array = list.toArray(new String[0]);
for (String fruit : array) {
System.out.println(fruit);
}
}
}
在上面的示例中,我们创建了一个包含三个字符串的ArrayList。然后,我们使用toArray()方法并传入一个长度为0的空字符串数组来将其转换为字符串数组。最后,我们遍历并打印数组中的每个元素。
6. 使用JDK工具类Arrays.asList()把数组转换成集合时,不能使用其修改集合相关的方法,它的add/remove/clear方法会抛出UnsupportedOperationException异常
【解释】: Arrays.asList()是Java中的一个实用方法,用于将数组快速转换为List。但需要注意的是,这个返回的List是固定大小的,也就是说,不能更改这个列表的大小(例如添加或删除元素)。
原因是Arrays.asList()返回的实际上是java.util.Arrays$ArrayList的一个实例,而不是我们经常使用的java.util.ArrayList。这个内部的ArrayList类没有重写add()、remove()和clear()方法,因此这些方法的行为是从AbstractList类中继承的。在AbstractList中,这些方法会尝试修改列表的大小,但由于Arrays$ArrayList是固定大小的,所以这些方法会抛出UnsupportedOperationException。
下面是一个示例来说明这一点:
import java.util.ArrayList;
import java.util.List;
public class CollectionToArrayExample {
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("Apple");
list.add("Banana");
list.add("Cherry");
// 使用toArray()方法并传入一个长度为0的空数组
String[] array = list.toArray(new String[0]);
for (String fruit : array) {
System.out.println(fruit);
}
}
}
import java.util.Arrays;
import java.util.List;
public class AsListExample {
public static void main(String[] args) {
// 使用Arrays.asList()将数组转换为列表
List<String> list = Arrays.asList("apple", "banana", "cherry");
System.out.println("Initial list: " + list);
// 尝试添加元素到列表 - 抛出UnsupportedOperationException
try {
list.add("orange");
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
// 尝试从列表中删除元素 - 抛出UnsupportedOperationException
try {
list.remove("banana");
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
// 尝试清空列表 - 抛出UnsupportedOperationException
try {
list.clear();
} catch (UnsupportedOperationException e) {
e.printStackTrace(); // 输出异常信息
}
}
}
在上述代码中,我们试图对通过Arrays.asList()方法得到的列表进行添加、删除和清空操作。每次尝试时,都会抛出UnsupportedOperationException。这是因为返回的列表是基于原始数组的,而数组的大小是固定的,所以不能更改。如果你需要一个可以修改的列表,可以考虑使用java.util.ArrayList来构造一个新的列表,如下所示:
List<String> modifiableList = new ArrayList<>(Arrays.asList("apple", "banana", "cherry"));
7. 禁止在业务方法中或者在条件不可控的循环中向全局集合对象添加元素的行为
【解释】在Java编程中,向全局集合对象添加元素的行为应该受到严格的控制,尤其是在业务方法或条件不可控的循环中。下面是一个例子来说明为什么这样做可能会导致问题,以及如何避免这种情况。
【问题示例】
假设我们有一个全局的List对象,用于存储系统中的用户信息。在某个业务方法中,我们根据某些条件向这个列表添加用户。
import java.util.ArrayList;
import java.util.List;
public class UserService {
// 全局的用户列表
public static List<String> users = new ArrayList<>();
// 业务方法,用于处理用户注册
public void registerUser(String userName) {
// 一些业务逻辑...
// 将用户添加到全局列表
users.add(userName);
}
}
在上面的例子中,registerUser方法将用户名添加到全局的users列表。如果这个方法在条件不可控的循环中被调用,或者在多线程环境中,全局列表可能会迅速增长,导致内存溢出或其他不可预测的行为。
【为什么这是一个问题?】
- 不可预测性:全局状态的变化可能难以追踪和预测,尤其是在复杂的系统中。
- 性能问题:如果全局集合不断增长,可能会导致内存消耗过大,影响系统性能。
- 线程安全性:在多线程环境中,对全局集合的并发访问可能导致数据不一致或其他线程安全问题。
- 维护困难:全局变量的使用可能会增加代码的耦合度,使维护变得更加困难。
【如何避免?】
- 避免使用全局变量:尽量使用局部变量或方法参数来传递数据。
- 封装集合访问:如果确实需要使用全局集合,应该通过适当的封装来控制对它的访问。例如,可以使用getter和setter方法,并在这些方法中添加必要的同步和验证逻辑。
- 使用适当的数据结构:根据具体需求选择适当的数据结构,例如使用Set来避免重复元素。
- 考虑线程安全:在多线程环境中,使用线程安全的集合实现,如CopyOnWriteArrayList或ConcurrentHashMap。
- 限制集合大小:如果可能的话,设置集合的最大大小限制,并处理超出限制的情况。
02 结语
限于篇幅原因,问题盘点计划按照多个批次内容进行输出,针对Sonar用户日常扫描过程当中识别的5大类场景常见的问题进行盘点,针对问题的原因进行分析总结,结合具体实现案例帮忙大家理解并且在日常的工作中规避这些问题。