Java编程的那些屎山代码分析之一
在编程行业干过这么多年,已经学会了从代码看人,代码会告诉我们,这个人在这一行能否吃饱饭,能吃到多少肉。
以下是个人总结的一些代码习惯问题和优化,单独一个也许不起眼,但堆积起来,就让一个项目代码变成一座屎山。
这样一座屎山代码,对后续接手的人来说,是多大的伤害啊!
1. 数据库查询判断存在性优化
- 重要性:在很多业务场景中,仅仅需要判断记录是否存在,而不需要获取具体的记录内容。使用 select count 可以减少数据库返回的数据量,降低网络传输开销,提高查询效率。
- 应用场景举例:判断某个用户是否是会员、某个订单是否存在等场景。在这些场景下,只需要知道是否存在满足特定条件的记录,而不需要获取记录的详细信息。
举个正例:
// 使用select count(*)来判断记录是否存在
String sql = "SELECT COUNT(*) FROM users WHERE user_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
if (rs.next() && rs.getInt(1) > 0) {
// 记录存在
}
}
举个反例:
// 使用select *查询所有字段,效率低
String sql = "SELECT * FROM users WHERE user_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
if (rs.next()) {
// 记录存在
}
}
2. 查询Sql字段选择优化
- 重要性:只查询需要用到的字段可以减少数据库返回的数据量,降低网络传输开销和内存占用,提高查询效率。同时,避免使用 select * 可以防止不必要的数据泄露和提高代码的可维护性。
- 应用场景举例:在一个用户信息展示页面中,只需要显示用户的姓名、年龄和性别等几个字段,此时就不应该使用 select * ,而应该明确指定需要查询的字段。
举个正例:
// 只查询需要的字段
String sql = "SELECT name, age, gender FROM users WHERE user_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
// 处理结果
}
举个反例:
// 使用select *查询所有字段
String sql = "SELECT * FROM users WHERE user_id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setInt(1, userId);
ResultSet rs = pstmt.executeQuery();
// 处理结果
}
3. 避免创建不必要对象
- 重要性:在Java中,对象的创建会消耗一定的内存和时间。如果变量后续逻辑判断一定会被赋值,或者只是一个字符串常量,直接初始化字符串常量可以避免不必要的对象创建,提高程序的性能。
- 应用场景举例:在一个循环中,如果某个变量的初始值在循环过程中一定会被覆盖,可以不进行初始化,直接在需要赋值的地方进行赋值。
举个正例:
// 直接使用字符串常量
String greeting = "Hello, world!";
举个反例:
// 无谓地创建新对象
String greeting = new String("Hello, world!");
4.集合初始化指定容量
- 重要性:在向集合中添加元素时,如果集合的初始容量过小,可能会导致集合频繁扩容,从而消耗更多的时间和内存。指定合适的初始容量可以减少集合扩容的次数,提高程序的性能。
- 应用场景举例:在已知集合中大概会存储多少个元素的情况下,可以根据这个数量指定集合的初始容量。例如,要存储一个班级的学生信息,已知班级人数为50人左右,可以在创建集合时指定初始容量为50。
举个正例:
// 指定初始容量
List list = new ArrayList<>(50);
举个反例:
// 未指定初始容量,可能导致频繁扩容
List list = new ArrayList<>();
5.异常处理打印具体信息
- 重要性:当程序出现异常时,如果只打印简单的提示信息,很难快速定位问题的根源。打印具体的异常信息,包括异常类型、异常消息和堆栈跟踪,可以帮助开发人员更快地找到问题所在,提高程序的可维护性。
- 应用场景举例:在一个Web应用中,如果用户提交表单时出现错误,打印具体的异常信息可以帮助开发人员快速确定是表单数据格式错误、数据库连接问题还是其他原因导致的错误。
举个正例:
try {
// 代码逻辑
} catch (Exception e) {
e.printStackTrace(); // 打印堆栈跟踪
}
举个反例:
try {
// 代码逻辑
} catch (Exception e) {
System.out.println("An error occurred."); // 没有提供足够的错误信息
}
6.对象toString方法覆盖重写
- 重要性:默认的 toString 方法返回的信息不够直观,难以理解对象的具体内容。覆盖重写 toString 方法可以返回更有意义的信息,方便在日志打印、调试等场景中查看对象的状态,提高代码的可读性和可维护性。
- 应用场景举例:在一个订单管理系统中,当打印订单对象时,如果不重写 toString 方法,可能只能看到对象的哈希码,而重写后可以看到订单的编号、金额、状态等关键信息。
举个正例:
@Override
public String toString() {
return "Order{" +
"id=" + id +
", amount=" + amount +
", status=" + status +
'}';
}
举个反例:
// 没有重写toString方法,使用默认实现
System.out.println(order); // 输出对象的哈希码
7.缩短方法参数列表
- 重要性:方法参数列表过长会降低代码的可读性和可维护性,同时也会增加接口升级时的兼容性问题。使用DTO对象包装参数可以使方法的参数更加清晰、简洁,提高代码的质量。
- 应用场景举例:在一个用户注册的方法中,如果需要传入用户的姓名、年龄、性别、地址、电话等多个参数,可以创建一个用户信息DTO对象,将这些参数封装在里面,然后将DTO对象作为方法的参数。
举个正例:
public void registerUser(UserInfoDto userInfo) {
// 注册逻辑
}
举个反例:
public void registerUser(String name, int age, String gender, String address, String phone) {
// 注册逻辑
}
8.使用缓冲流减少IO操作
- 重要性:缓冲流可以减少IO操作的次数,提高文件读写等IO操作的效率。在处理大量数据时,这种效率提升尤为明显。
- 应用场景举例:在一个文件备份系统中,需要读取一个大文件并将其复制到另一个位置。使用缓冲流可以大大提高复制文件的速度。
举个正例:
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(destFile))) {
byte[] buffer = new byte[1024];
int length;
while ((length = bis.read(buffer)) != -1) {
bos.write(buffer, 0, length);
}
}
举个反例:
try (InputStream is = new FileInputStream(file);
OutputStream os = new FileOutputStream(destFile)) {
byte[] buffer = new byte[1024];
int length;
while ((length = is.read(buffer)) != -1) {
os.write(buffer, 0, length);
}
}
9.程序逻辑优化传参复用
- 重要性:避免重复查询和方法调用可以减少数据库访问次数和方法执行时间,提高程序的性能。同时,也可以使代码更加简洁、清晰,提高代码的可维护性。
- 应用场景举例:在一个用户信息处理的方法中,如果已经查询到了用户的信息,并且在后续的方法中也需要使用这个信息,可以将用户信息作为参数传递给后续的方法,而不是再次查询数据库。
举个正例:
public void processUserInfo(User user) {
// 处理用户信息
}
public void someMethod() {
User user = getUserFromDatabase();
processUserInfo(user);
}
举个反例:
public void someMethod() {
User user = getUserFromDatabase();
// 处理用户信息
getUserFromDatabase(); // 重复查询
}
10.避免使用魔法值
- 重要性:魔法值(直接使用的数字、字符串等常量)会使代码难以理解和维护。使用 enum 枚举代替魔法值可以提高代码的可读性和可维护性,同时也可以避免因魔法值的错误使用而导致的问题。
- 应用场景举例:在一个用户状态管理的系统中,如果使用数字0和1表示用户的状态,很难理解其具体含义。
使用枚举可以明确表示不同的状态,如 UserStatus.ACTIVE 和 UserStatus.INACTIVE 。
举个正例:
public enum UserStatus {
ACTIVE, INACTIVE
}
public void activateUser(User user, UserStatus status) {
// 激活用户逻辑
}
举个反例:
public void activateUser(User user, int status) {
// 魔法值0和1,难以理解
}
11.定义静态常量
- 重要性:如果一个成员变量的值在类的整个生命周期内都不会改变,将其定义为静态常量可以节省内存空间,因为只有一份存储。同时,也可以方便在代码中统一管理和使用,提高代码的可读性和可维护性。
- 应用场景举例:在一个数学计算类中,如果有一个常量 PI ,可以将其定义为静态常量 public static final double PI = 3.1415926; 。
举个正例:
public static final double PI = 3.1415926;
举个反例:
double pi = 3.1415926; // 非静态常量
12.空指针检查
- 重要性: NullPointerException 是Java中最常见的异常之一。在代码中进行空指针检查可以防止因空对象调用方法或访问属性而引发的异常,提高程序的稳定性。
- 应用场景举例:在一个用户信息展示的方法中,如果用户对象可能为 null ,在访问用户的属性之前应该进行空指针检查,如 if (user!= null && user.getName()!= null) { System.out.println(user.getName()); } 。
举个正例:
if (user != null && user.getName() != null) {
System.out.println(user.getName());
}
举个反例:
System.out.println(user.getName()); // 可能抛出NullPointerException
13. 异常日志记录
- 重要性:捕获到的异常如果不进行记录,很难在程序出现问题时快速定位原因。记录异常日志可以留下异常发生时的线索,方便后续分析和定位问题所在,提高程序的可维护性。
- 应用场景举例:在一个Web服务中,如果出现数据库连接错误,应该记录异常日志,包括异常类型、异常消息和发生时间等信息,以便后续排查问题。
举个正例:
try {
// 代码逻辑
} catch (Exception e) {
log.error("Exception occurred", e); // 记录异常日志
}
举个反例:
try {
// 代码逻辑
} catch (Exception e) {
// 没有记录日志
}
14.Lambda表达式替代匿名内部类
- 重要性:Lambda表达式比匿名内部类更加简洁、紧凑,并且在大多数虚拟机中效率更高。使用Lambda表达式可以使代码更加优雅、易读,提高开发效率。
- 应用场景举例:在一个集合排序的场景中,如果使用匿名内部类实现比较器可能会比较繁琐,而使用Lambda表达式可以更加简洁地实现相同的功能,如 list.sort((o1, o2) -> o1.compareTo(o2)); 。
举个正例:
list.sort((o1, o2) -> o1.compareTo(o2));
举个反例:
list.sort(new Comparator<Object>() {
@Override
public int compare(Object o1, Object o2) {
return o1.compareTo(o2);
}
});
15.通知类代码宜异步处理
- 重要性:对于通知类操作,如发送邮件、短信等,如果采用同步处理,可能会阻塞主线程,影响整体程序的响应速度和性能。异步处理可以让这些操作在后台进行,不影响主流程的执行,提高程序的效率和用户体验。
- 应用场景举例:在一个用户注册成功后发送邮件通知的场景中,可以使用异步方式发送邮件,避免用户等待邮件发送完成,提高注册流程的响应速度。
举个正例:
// 使用异步方式发送邮件
CompletableFuture.runAsync(() -> sendEmail(user));
举个反例:
// 同步发送邮件,可能阻塞主线程
sendEmail(user);
16.Java日期格式处理
- 重要性:在处理Java日期时,正确选择日期格式非常重要。使用错误的日期格式可能会导致日期解析错误、格式转换异常等问题,影响程序的正确性。
- 应用场景举例:在一个日期格式化输出的场景中,如果使用 YYYY 格式可能会在跨年等特殊时间点出现不符合预期的情况(按照周统计年份),而使用 yyyy 格式可以避免这种问题。
举个正例:
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
String formattedDate = sdf.format(date);
举个反例:
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
// 可能在跨年时出现不符合预期的情况
String formattedDate = sdf.format(date);
17.注意使用final修饰类
- 重要性:使用 final 修饰类可以防止该类被其他类继承,保证类的设计完整性和安全性。同时,编译器可以对 final 修饰的类进行一些特定优化,提高程序的性能。
- 应用场景举例:在一个工具类中,如果不希望该类被继承,可以使用 final 修饰类,如 public final class MathUtils {... } 。
举个正例:
public final class MathUtils {
// 工具类方法
}
举个反例:
public class MathUtils {
// 可以被继承,可能被修改
}
18.静态变量与Spring实例化关系处理
- 重要性:如果静态变量直接依赖于Spring实例化变量,可能会因为Spring容器还没完成初始化或者bean加载顺序等问题导致初始化失败。正确处理静态变量与Spring实例化的关系可以确保程序的稳定性。
- 应用场景举例:在一个使用Spring框架的项目中,如果有一个静态变量需要获取Spring容器中的bean,应该采用延迟加载的方式,避免在类加载时就尝试获取bean。
举个正例:
@Component
public class SomeService {
private static SomeBean someBean;
@PostConstruct
public void init() {
someBean = context.getBean(SomeBean.class);
}
}
举个反例:
@Component
public class SomeService {
private static SomeBean someBean = context.getBean(SomeBean.class);
// 可能在Spring容器初始化前就尝试获取bean
}
19.声明静态方法
- 重要性:与类成员变量无关的方法声明为静态方法,可以直接通过类名调用,方便使用。同时,也可以避免因为实例化对象等操作带来的额外开销,提高程序的性能。
- 应用场景举例:在一个数学计算工具类中,如果有一个方法只需要传入参数进行计算,不需要访问类的成员变量,可以将其声明为静态方法,如 public static double add(double num1, double num2) { return num1 + num2; } 。
举个正例:
public static double add(double num1, double num2) {
return num1 + num2;
}
举个反例:
public double add(double num1, double num2) {
return num1 + num2;
}
// 需要实例化对象才能调用
20.滥用全局变量
重要性:全局变量使得代码的可维护性和可读性降低,因为它们可以在代码的任何地方被修改,这增加了代码出错的风险。应用场景:全局变量适用于那些确实需要在程序的多个部分共享的状态,例如配置参数或全局状态。正例代码:
public class Application {
private static final int BUFFER_SIZE = 1024; // 常量全局变量,不可变
public static void main(String[] args) {
byte[] buffer = new byte[BUFFER_SIZE];
// 使用buffer进行操作
}
}
在这个例子中,`BUFFER_SIZE`是一个常量全局变量,它被用作缓冲区的大小,这个值在程序的任何地方都是一致的,并且是不可变的。反例代码:
public class Counter {
public static int count = 0; // 可变的全局变量
public static void increment() {
count++;
}
public static void decrement() {
count--;
}
}
在这个例子中,`count`是一个全局变量,可以在程序的任何地方被修改,这可能导致计数器的值变得不可预测。
21异常处理不当
重要性:异常处理是确保程序稳定性和可靠性的关键。应用场景:任何可能抛出异常的代码块,特别是在I/O操作、网络通信和数据库交互中。正例代码:
import java.io.FileReader;
import java.io.IOException;
public class FileProcessor {
public void readFile(String fileName) {
try (FileReader reader = new FileReader(fileName)) {
int data = reader.read();
while (data != -1) {
// 处理数据
data = reader.read();
}
} catch (IOException e) {
e.printStackTrace();
// 进一步的错误处理
}
}
}
在这个例子中,使用`try-with-resources`语句确保`FileReader`被正确关闭,即使发生异常也是如此,并且捕获了`IOException`以处理可能的I/O错误。反例代码:
import java.io.FileReader;
import java.io.IOException;
public class FileProcessor {
public void readFile(String fileName) {
FileReader reader = new FileReader(fileName);
int data = reader.read();
while (data != -1) {
// 处理数据
data = reader.read();
}
reader.close(); // 可能在发生异常时忘记关闭
}
}
在这个例子中,如果`read`方法抛出异常,`FileReader`可能不会被关闭,导致资源泄露。
22.不遵循单一职责原则
重要性:违反单一职责原则会导致类变得过于复杂,难以理解和维护。应用场景:类设计和方法实现。正例代码:
public class UserProcessor {
public void addUser(String username) {
// 添加用户逻辑
}
public void removeUser(String username) {
// 删除用户逻辑
}
}
在这个例子中,`UserProcessor`类只负责处理用户相关的操作,符合单一职责原则。反例代码:
public class UserProcessor {
public void addUser(String username) {
// 添加用户逻辑
}
public void sendEmail(String email) {
// 发送邮件逻辑
}
public void logEvent(String event) {
// 日志记录逻辑
}
}
在这个例子中,`UserProcessor`类承担了添加用户、发送邮件和日志记录的职责,违反了单一职责原则。
哈
23.不使用泛型
重要性:泛型提供了类型安全性和代码重用。应用场景:需要处理不同数据类型的集合或类。正例代码:
public class GenericBox {
private T t;
public void set(T t) {
this.t = t;
}
public T get() {
return t;
}
}
在这个例子中,`GenericBox`是一个泛型类,可以存储任何类型的数据,提供了类型安全性。反例代码:
public class Box {
private Object object;
public void set(Object object) {
this.object = object;
}
public Object get() {
return object;
}
}
在这个例子中,`Box`类使用了`Object`类型,失去了类型安全性,并且每次使用时都需要进行类型转换。
以上就是第一批总结的问题,感谢大家观看!
看到屎山代码时,你同事的反应