在本文中,我们将深入探讨Java编程中常见的陷阱和优化策略,涵盖死循环风险、线程安全问题、泛型使用、集合类比较以及精度处理等方面。通过具体代码示例和详细的分析,我们揭示了潜在的编程错误,并提供了有效的解决方案。无论是避免死循环和栈溢出,还是正确使用并发集合类,本文都为读者提供了实用的指导。同时,我们还展示了如何确保自定义对象在集合中的唯一性,以及如何使用 BigDecimal 处理精度问题。通过这些优化策略,开发者可以编写更健壮、更高效的Java代码。
死循环风险代码分析与优化
示例代码1
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
while (list.iterator().hasNext()) {
System.out.println(list.iterator().next());
}
}
问题分析:
- 每次
while
循环都会重新创建一个新的Iterator
,并且每个Iterator
的next()
方法只能调用一次,否则会抛出NoSuchElementException
。 - 由于
hasNext()
总是返回true
,这导致死循环。
优化建议:
- 应在循环外创建一次
Iterator
,然后使用该Iterator
进行遍历。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("aa");
list.add("bb");
Iterator<String> iterator = list.iterator();
while (iterator.hasNext()) {
System.out.println(iterator.next());
}
}
示例代码2
public static void main(String[] args) {
int count = 0;
while (count < 10) {
if (count == 4) {
continue;
}
System.out.println(count);
count++;
}
}
问题分析:
continue
会导致count
不增加,从而导致死循环。
优化建议:
- 在
continue
之前增加count++
。
public static void main(String[] args) {
int count = 0;
while (count < 10) {
if (count == 4) {
count++;
continue;
}
System.out.println(count);
count++;
}
}
示例代码3
public class Tmp {
private boolean flag = true;
public void setFlag(boolean flag) {
this.flag = flag;
}
public void fun() {
while (flag) {
}
System.out.println("done");
}
public static void main(String[] args) throws InterruptedException {
final Tmp flagTest = new Tmp();
new Thread(() -> flagTest.fun()).start();
Thread.sleep(200);
flagTest.setFlag(false);
}
}
问题分析:
flag
变量未声明为volatile
,可能会导致线程不更新该变量的值,导致死循环。
优化建议:
- 将
flag
变量声明为volatile
。
public class Tmp {
private volatile boolean flag = true;
public void setFlag(boolean flag) {
this.flag = flag;
}
public void fun() {
while (flag) {
}
System.out.println("done");
}
public static void main(String[] args) throws InterruptedException {
final Tmp flagTest = new Tmp();
new Thread(() -> flagTest.fun()).start();
Thread.sleep(200);
flagTest.setFlag(false);
}
}
示例代码4
public Category findRoot(long categoryId) {
Category category = categoryMapper.findCategoryById(categoryId);
if(category == null) {
throw new BusinessException("分类不存在");
}
long parentId = category.getParentId();
if(parentId == null || parentId == 0) {
return category;
}
return findRoot(parentId);
}
问题分析:
- 如果存在环形链表结构,会导致无限递归,最终导致栈溢出。
优化建议:
- 使用集合记录访问过的节点,避免循环引用。
public Category findRoot(long categoryId) {
Set<Long> visited = new HashSet<>();
return findRoot(categoryId, visited);
}
private Category findRoot(long categoryId, Set<Long> visited) {
if (!visited.add(categoryId)) {
throw new BusinessException("循环引用检测到");
}
Category category = categoryMapper.findCategoryById(categoryId);
if (category == null) {
throw new BusinessException("分类不存在");
}
long parentId = category.getParentId();
if (parentId == 0) {
return category;
}
return findRoot(parentId, visited);
}
线程安全问题与代码优化
private static void extracted() {
ConcurrentHashMap<String, Student> map = new ConcurrentHashMap<>();
String key = "test";
Student student = map.get(key);
if (student == null) {
student = new Student();
map.put(key, student);
}
}
问题分析:
- 存在多线程下的竞态条件,可能导致多个线程同时创建
Student
实例并覆盖彼此的值。
优化建议:
- 使用
putIfAbsent
方法确保线程安全。
private static void extracted() {
ConcurrentHashMap<String, Student> map = new ConcurrentHashMap<>();
String key = "test";
map.putIfAbsent(key, new Student());
Student student = map.get(key);
}
编译错误分析
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("ab");
printList(list);
}
public static void printList(List<Object> list) {
Iterator it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
问题分析:
List<String>
不能传递给List<Object>
,因为 Java 的泛型是不可协变的。
优化建议:
- 使用通配符
?
来处理泛型。
public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("ab");
printList(list);
}
public static void printList(List<?> list) {
Iterator<?> it = list.iterator();
while (it.hasNext()) {
System.out.println(it.next());
}
}
hashCode 和 equals 的处理
正确的说法
AC
- A. 只要覆写
equals
,就必须覆写hashCode
。(正确,保证相等的对象有相同的哈希码) - C. 如果自定义对象作为
Map
的键,那么必须覆写hashCode
和equals
。(正确,确保键的唯一性和正确的检索)
示例代码优化
public class Tmp {
public static void main(String[] args) {
Student zs1 = new Student("zs");
Student zs2 = new Student("zs");
HashSet<Student> set = new HashSet<>();
set.add(zs1);
boolean addSuccess = set.add(zs2);
if (addSuccess) {
System.out.println("add 成功了");
}
Map<Student, Integer> map = new HashMap<>();
map.put(zs1, 1);
Integer put = map.put(zs2, 2);
if (put == null) {
System.out.println("put 成功了");
}
}
}
class Student {
String name;
public Student(String name) {
this.name = name;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(name, student.name);
}
@Override
public int hashCode() {
return Objects.hash(name);
}
}
BigDecimal 创建方法的精度风险
精度风险分析
- A.
BigDecimal a = new BigDecimal(0.1F)
: 存在精度风险,因为浮点数不能精确表示。 - B.
BigDecimal b = new BigDecimal("0.1")
: 安全,无精度损失。 - C.
BigDecimal c = BigDecimal.valueOf(0.1)
: 推荐使用,内部实现了对double
类型的处理。
方法调用输出分析
List 对象的多态方法调用
class A {
public void hello(List list) {
System.out.println("list");
}
public void hello(ArrayList list) {
System.out.println("arrayList");
}
public static void main(String[] args) {
A a = new A();
List list = new ArrayList();
a.hello(list);
}
}
输出分析:
- 输出
list
,因为方法的选择是在编译时基于参数的静态类型(List
)。
双精度比较与 BigDecimal
比较
private static void main(String[] args) {
double a = 1.0D - 0.9D;
double b = 0.1D;
Double x = Double.valueOf(a);
Double y = Double.valueOf(b);
System.out.println(a == b);
System.out.println(x.equals(y));
}
输出分析:
a == b
输出false
,因为浮点数比较存在精度问题。x.equals(y)
输出false
,因为Double
类型包装类同样存在精度问题。
使用 org.apache.commons.lang3
包的 StringUtils
private static void main(String[] args) {
String str = " ";
boolean empty = StringUtils.isEmpty(str);
System.out.println("empty = " + empty);
boolean blank = StringUtils.isBlank(str);
System.out.println("blank = " + blank);
}
输出分析:
empty = false
,因为isEmpty
检查字符串是否为null
或空串。blank = true
,因为isBlank
检查字符串是否为null
或仅包含空白字符。
包含多个集合比较的输出分析
List 比较
private static void extracted5() {
List<String> list1 = new ArrayList<>();
list1.add("eee");
list1.add("www");
List<String> list2 = new LinkedList<>();
list2.add("eee");
list2.add("www");
System.out.println(list1.equals(list2));
}
输出分析:
true
,因为List
的equals
方法只比较元素和顺序,不考虑具体实现类型。
Set 比较
private static void extracted6() {
Set<String> set1 = new HashSet<>();
Set<String> set2 = new LinkedHashSet<>();
set1.add("www");
set2.add("www");
System.out.println(set1.equals(set2));
}
输出分析:
true
,因为Set
的equals
方法只比较元素,不考虑具体实现类型和顺序。
Map 比较
private static void extracted7() {
Map<Integer, String> m1 = new TreeMap<>();
Map<Integer, String> m2 = new LinkedHashMap<>();
m1.put(1, "www");
m1.put(2, "eee");
m2.put(2, "eee");
m2.put(1, "www");
System.out.println(m1.equals(m2));
}
输出分析:
false
,因为TreeMap
和LinkedHashMap
的比较会考虑键值对的顺序。
Integer 与 String 的 equals 方法比较
private static void extracted8() {
Integer integer = new Integer(3);
System.out.println(integer.equals("3"));
}
输出分析:
false
,因为Integer
和String
类型不同,equals
方法返回false
。
优化总结
- 在编写代码时,应充分考虑可能出现的死循环、线程安全问题和其他潜在的逻辑错误。
- 使用合适的同步机制或线程安全类来避免多线程问题。
- 对于集合类的比较,应了解其具体的
equals
和hashCode
实现逻辑。 - 对于涉及浮点数和精度的问题,推荐使用
BigDecimal
并采取适当的初始化方式。