Okay, let’s go through these software development principles with Java examples.
1. Write clearly - don’t be too clever. #
要写清楚,不要自作聪明
Explanation: Code is read more often than it’s written. Prioritize readability over overly compact or “ingenious” solutions that are hard to understand.
Example:
-
Too Clever (and less clear):
public class CleverSwap { public static void main(String[] args) { int a = 5; int b = 10; System.out.println("Before: a = " + a + ", b = " + b); a = a ^ b ^ (b = a); // Bitwise XOR swap - clever but obscure System.out.println("After: a = " + a + ", b = " + b); } } -
Clear:
public class ClearSwap { public static void main(String[] args) { int a = 5; int b = 10; System.out.println("Before: a = " + a + ", b = " + b); int temp = a; a = b; b = temp; System.out.println("After: a = " + a + ", b = " + b); } }
2. Say what you mean, simply and directly. #
简单、直截了当地表达你的意思
Explanation: Your code should reflect your intent as directly as possible. Avoid overly complex logic if a simpler approach achieves the same result.
Example:
-
Indirect (less clear):
public class IndirectCondition { public static void main(String[] args) { boolean isEligible = false; int age = 20; if (!(age < 18)) { // Double negative can be confusing isEligible = true; } System.out.println("Is eligible? " + isEligible); } } -
Simple and Direct:
public class DirectCondition { public static void main(String[] args) { int age = 20; boolean isEligible = (age >= 18); System.out.println("Is eligible? " + isEligible); } }
3. Use library functions whenever feasible. #
尽可能使用库函数
Explanation: Standard libraries are well-tested, optimized, and familiar to other developers. Reinventing the wheel can lead to bugs and wasted effort.
Example:
-
Reinventing (less ideal):
public class ManualSort { // Implementing a basic bubble sort public static void sortArray(int[] arr) { int n = arr.length; for (int i = 0; i < n - 1; i++) { for (int j = 0; j < n - i - 1; j++) { if (arr[j] > arr[j + 1]) { int temp = arr[j]; arr[j] = arr[j + 1]; arr[j + 1] = temp; } } } } public static void main(String[] args) { int[] numbers = {5, 1, 4, 2, 8}; sortArray(numbers); for (int num : numbers) { System.out.print(num + " "); // Output: 1 2 4 5 8 } } } -
Using Library Function:
import java.util.Arrays; public class LibrarySort { public static void main(String[] args) { int[] numbers = {5, 1, 4, 2, 8}; Arrays.sort(numbers); // Using built-in sort for (int num : numbers) { System.out.print(num + " "); // Output: 1 2 4 5 8 } } }
4. Avoid too many temporary variables. #
不要使用太多的临时变量
Explanation: While temporary variables can aid clarity, an excessive number can clutter the code and make it harder to follow the data flow.
Example:
-
Too Many Temporaries:
public class ManyTemporaries { public String formatUserDetails(String firstName, String lastName, int age) { String namePart = "Name: " + firstName + " " + lastName; String agePart = "Age: " + age; String fullDetails = namePart + ", " + agePart; return fullDetails; } public static void main(String[] args) { ManyTemporaries formatter = new ManyTemporaries(); System.out.println(formatter.formatUserDetails("John", "Doe", 30)); } } -
Fewer (Appropriate) Temporaries:
public class FewerTemporaries { public String formatUserDetails(String firstName, String lastName, int age) { // String concatenation is often optimized by the compiler anyway return "Name: " + firstName + " " + lastName + ", Age: " + age; } // Or, if intermediate steps are complex and genuinely add clarity: // public String formatUserDetailsComplex(String firstName, String lastName, int age) { // String fullName = firstName + " " + lastName; // Clear intermediate step // return "Name: " + fullName + ", Age: " + age; // } public static void main(String[] args) { FewerTemporaries formatter = new FewerTemporaries(); System.out.println(formatter.formatUserDetails("Jane", "Doe", 28)); } }Note: The goal isn’t to eliminate all temporaries, but to avoid unnecessary ones that don’t significantly improve readability.
5. Write clearly - don’t sacrifice clarity for “efficiency.” #
要写清楚-不要为所谓的“效率”牺牲清晰度
Explanation: Premature optimization is the root of much evil. Write clear, correct code first. Optimize only when and where profiling shows it’s necessary. Micro-optimizations often make code harder to read for negligible performance gains.
Example:
-
Sacrificing Clarity for (Perceived) Efficiency:
public class UnclearEfficiency { // Example: Trying to be "efficient" with bit shifts for multiplication by 2 // This is trivial and the JIT compiler does this anyway. public int doubleValue(int num) { return num << 1; // Might be confusing to someone unfamiliar with bitwise ops } public static void main(String[] args) { UnclearEfficiency ue = new UnclearEfficiency(); System.out.println("Double of 7: " + ue.doubleValue(7)); // Output: 14 } } -
Clear and Sufficiently Efficient:
public class ClearAndEfficient { public int doubleValue(int num) { return num * 2; // Perfectly clear and efficient } public static void main(String[] args) { ClearAndEfficient ce = new ClearAndEfficient(); System.out.println("Double of 7: " + ce.doubleValue(7)); // Output: 14 } }
6. Let the machine do the dirty work. #
让机器去做麻烦的事情
Explanation: Automate repetitive, error-prone, or complex tasks. Use build tools, IDE features, code generators, or higher-level language constructs.
Example: (Conceptual - String formatting)
-
Manual “Dirty Work” (String concatenation):
public class ManualStringConcat { public static void main(String[] args) { String item = "Laptop"; int quantity = 2; double price = 1200.50; String orderDetails = "Order details: Item - " + item + ", Quantity - " + quantity + ", Price per item - $" + price + ", Total - $" + (quantity * price); System.out.println(orderDetails); } } -
Letting the Machine Do It (String formatting):
public class FormattedString { public static void main(String[] args) { String item = "Laptop"; int quantity = 2; double price = 1200.50; // Using String.format or System.out.printf String orderDetails = String.format( "Order details: Item - %s, Quantity - %d, Price per item - $%.2f, Total - $%.2f", item, quantity, price, (quantity * price) ); System.out.println(orderDetails); // Or directly: // System.out.printf( // "Order details: Item - %s, Quantity - %d, Price per item - $%.2f, Total - $%.2f%n", // item, quantity, price, (quantity * price) // ); } }Another example is garbage collection in Java – the JVM handles memory deallocation, which is “dirty work.”
7. Replace repetitive expressions by calls to a common function. #
将重复代码替代为调用通用函数
Explanation: If you find yourself writing the same (or very similar) block of code multiple times, encapsulate it in a function/method. This promotes DRY (Don’t Repeat Yourself), improves maintainability, and reduces redundancy.
Example:
-
Repetitive Code:
public class RepetitiveCalculation { public static void main(String[] args) { double principal1 = 1000; double rate1 = 0.05; double time1 = 2; // Calculate simple interest double interest1 = (principal1 * rate1 * time1); System.out.println("Interest 1: " + interest1); double principal2 = 5000; double rate2 = 0.03; double time2 = 5; // Calculate simple interest again double interest2 = (principal2 * rate2 * time2); System.out.println("Interest 2: " + interest2); } } -
Using a Common Function:
public class CommonFunctionCalculation { public static double calculateSimpleInterest(double principal, double rate, double time) { return principal * rate * time; } public static void main(String[] args) { double principal1 = 1000; double rate1 = 0.05; double time1 = 2; double interest1 = calculateSimpleInterest(principal1, rate1, time1); System.out.println("Interest 1: " + interest1); double principal2 = 5000; double rate2 = 0.03; double time2 = 5; double interest2 = calculateSimpleInterest(principal2, rate2, time2); System.out.println("Interest 2: " + interest2); } }
8. Parenthesize to avoid ambiguity. #
使用括号避免歧义
Explanation: Even if operator precedence rules mean parentheses aren’t strictly necessary, adding them can significantly improve readability and prevent misunderstandings for complex expressions.
Example:
-
Potentially Ambiguous (relies on operator precedence):
public class AmbiguousExpression { public static void main(String[] args) { int a = 5, b = 3, c = 2, d = 4; // Is it (a + b) * c or a + (b * c)? (Operator precedence says multiplication first) // Is it (result1 - d) / 2 or result1 - (d / 2)? int result = a + b * c - d / 2; // Correct by precedence: 5 + (3*2) - (4/2) = 5 + 6 - 2 = 9 System.out.println("Result: " + result); } } -
Clear with Parentheses:
public class ParenthesizedExpression { public static void main(String[] args) { int a = 5, b = 3, c = 2, d = 4; // Explicitly shows the order of operations int result = (a + (b * c)) - (d / 2); // 5 + 6 - 2 = 9 System.out.println("Result: " + result); // If you meant something else, parentheses make it clear: // int resultAlternative = ((a + b) * c - d) / 2; // ((5+3)*2 - 4)/2 = (8*2 - 4)/2 = (16-4)/2 = 12/2 = 6 // System.out.println("Alternative Result: " + resultAlternative); } }
9. Choose variable names that won’t be confused. #
选择没有歧义的变量名
Explanation: Use descriptive names that clearly indicate the variable’s purpose. Avoid single-letter names (except for loop counters or very localized contexts) or names that are too similar to each other.
Example:
-
Confusing Names:
public class ConfusingNames { public void processData(int[] data, int val) { int temp = 0; // What is temp for? for (int d : data) { if (d > val) { // 'val' is okay, but could be more specific temp += d; } } // ... what does 'temp' represent now? Sum? Count? System.out.println("Processed value: " + temp); } public static void main(String[] args) { ConfusingNames cn = new ConfusingNames(); cn.processData(new int[]{10,20,5,15}, 10); } } -
Clear Names:
public class ClearVariableNames { public void sumElementsGreaterThanThreshold(int[] numbers, int threshold) { int sumOfElements = 0; // Clearly indicates purpose for (int number : numbers) { if (number > threshold) { sumOfElements += number; } } System.out.println("Sum of elements greater than " + threshold + ": " + sumOfElements); } public static void main(String[] args) { ClearVariableNames cvn = new ClearVariableNames(); cvn.sumElementsGreaterThanThreshold(new int[]{10,20,5,15}, 10); } }
10. Avoid unnecessary branches. #
避免使用不必要的分支
Explanation: Complex conditional logic (many if-else if-else statements or nested ifs) can make code hard to follow and test. Simplify boolean expressions or use alternative structures (like polymorphism or maps) where appropriate.
Example:
-
Unnecessary Branching (for boolean assignment):
public class UnnecessaryBranch { public boolean isAdult(int age) { boolean adult; if (age >= 18) { adult = true; } else { adult = false; } return adult; } public static void main(String[] args) { UnnecessaryBranch ub = new UnnecessaryBranch(); System.out.println("Is 25 adult? " + ub.isAdult(25)); // true System.out.println("Is 15 adult? " + ub.isAdult(15)); // false } } -
Simplified (No unnecessary branch):
public class SimplifiedBranch { public boolean isAdult(int age) { return age >= 18; // The expression itself evaluates to a boolean } public static void main(String[] args) { SimplifiedBranch sb = new SimplifiedBranch(); System.out.println("Is 25 adult? " + sb.isAdult(25)); // true System.out.println("Is 15 adult? " + sb.isAdult(15)); // false } }
11. If a logical expression is hard to understand, try transforming it. #
如果逻辑表达式不好理解,就试着做下变形
Explanation: Complex boolean expressions can be difficult to parse. Use De Morgan’s laws or introduce well-named boolean variables for sub-expressions to improve clarity.
Example:
-
Hard to Understand Expression:
public class ComplexLogic { public void checkAccess(boolean isUserLoggedIn, boolean isAdmin, boolean isAccountActive) { // Example: Access is denied if user is NOT logged in AND (is NOT admin OR account is NOT active) // This can be tricky to read directly. if (!isUserLoggedIn && (!isAdmin || !isAccountActive)) { System.out.println("Access Denied: Complex condition met."); } else { System.out.println("Access Granted or condition not met."); } } public static void main(String[] args) { ComplexLogic cl = new ComplexLogic(); cl.checkAccess(false, false, true); // Denied cl.checkAccess(true, false, false); // Granted (because loggedIn is true) cl.checkAccess(false, true, true); // Denied } } -
Transformed/Simplified Expression (using De Morgan’s or intermediate variables):
public class SimplifiedLogic { public void checkAccess(boolean isUserLoggedIn, boolean isAdmin, boolean isAccountActive) { // Original: !isUserLoggedIn && (!isAdmin || !isAccountActive) // Option 1: Introduce boolean variables for sub-expressions // boolean isPrivileged = isAdmin && isAccountActive; // if (!isUserLoggedIn && !isPrivileged) { // System.out.println("Access Denied: User not logged in and not privileged."); // } // Option 2: Applying De Morgan's Law to part of it // (!isAdmin || !isAccountActive) is equivalent to !(isAdmin && isAccountActive) boolean canBypassLogin = isAdmin && isAccountActive; // e.g. admin with active account can do something special if (!isUserLoggedIn && !canBypassLogin) { System.out.println("Access Denied: Must be logged in or have special bypass privileges."); } else { System.out.println("Access Granted or condition not met."); } // Alternative transformation to positive logic for the grant condition: // if (isUserLoggedIn || (isAdmin && isAccountActive)) { // System.out.println("Access Granted."); // } else { // System.out.println("Access Denied."); // } } public static void main(String[] args) { SimplifiedLogic sl = new SimplifiedLogic(); sl.checkAccess(false, false, true); // Denied sl.checkAccess(true, false, false); // Granted sl.checkAccess(false, true, true); // Denied (as per modified logic) // If we wanted false, true, true -> Granted, the logic needs to be: // if (isUserLoggedIn || (isAdmin && isAccountActive)) } }The best transformation depends on the underlying business logic. The key is making the intent clearer.
12. Choose a data representation that makes the program simple. #
采用让程序更简洁的数据表达形式
Explanation: The way you structure your data can greatly influence the complexity of your algorithms. Sometimes a different data structure (e.g., a Map instead of a List, a custom object instead of parallel arrays) can simplify your code.
Example:
-
Less Simple Representation (Parallel Arrays for User Data):
import java.util.ArrayList; import java.util.List; public class ParallelArrays { List<String> userNames = new ArrayList<>(); List<String> userEmails = new ArrayList<>(); List<Integer> userAges = new ArrayList<>(); public void addUser(String name, String email, int age) { userNames.add(name); userEmails.add(email); userAges.add(age); } public void printUserDetails(String name) { int userIndex = -1; for (int i = 0; i < userNames.size(); i++) { if (userNames.get(i).equals(name)) { userIndex = i; break; } } if (userIndex != -1) { System.out.println("Name: " + userNames.get(userIndex) + ", Email: " + userEmails.get(userIndex) + ", Age: " + userAges.get(userIndex)); } else { System.out.println("User not found."); } } public static void main(String[] args) { ParallelArrays pa = new ParallelArrays(); pa.addUser("Alice", "alice@example.com", 30); pa.printUserDetails("Alice"); } } -
Simpler Representation (Custom Object):
import java.util.ArrayList; import java.util.List; class User { // Data representation String name; String email; int age; public User(String name, String email, int age) { this.name = name; this.email = email; this.age = age; } @Override public String toString() { return "Name: " + name + ", Email: " + email + ", Age: " + age; } } public class UserObjectList { List<User> users = new ArrayList<>(); public void addUser(String name, String email, int age) { users.add(new User(name, email, age)); } public void printUserDetails(String name) { for (User user : users) { if (user.name.equals(name)) { System.out.println(user); return; } } System.out.println("User not found."); } public static void main(String[] args) { UserObjectList uol = new UserObjectList(); uol.addUser("Bob", "bob@example.com", 25); uol.printUserDetails("Bob"); } }
13. Write first in easy-to-understand pseudo language; then translate into whatever language you have to use. #
先用易于理解的伪代码写;再翻译成你使用的语言
Explanation: Before diving into the syntax of a specific language, outlining the logic in pseudocode can help clarify the algorithm and data flow, making the translation to actual code smoother.
Example: (Task: Find the largest number in a list)
-
Pseudocode:
FUNCTION findLargest(list_of_numbers) IF list_of_numbers is empty THEN RETURN error or null ENDIF largest_so_far = first element of list_of_numbers FOR EACH number IN list_of_numbers (starting from the second element) IF number > largest_so_far THEN largest_so_far = number ENDIF ENDFOR RETURN largest_so_far ENDFUNCTION -
Java Implementation (Translated from Pseudocode):
import java.util.List; import java.util.Collections; // For Collections.emptyList() and .max() public class FindLargest { public static Integer findLargestNumber(List<Integer> numbers) { if (numbers == null || numbers.isEmpty()) { // Or throw an IllegalArgumentException return null; } Integer largestSoFar = numbers.get(0); // In Java, List.get(0) for first element for (int i = 1; i < numbers.size(); i++) { // Loop from the second element Integer currentNumber = numbers.get(i); if (currentNumber > largestSoFar) { largestSoFar = currentNumber; } } return largestSoFar; } public static void main(String[] args) { List<Integer> myNumbers = List.of(10, 5, 25, 17, 30, 12); // Java 9+ List.of System.out.println("Largest number: " + findLargestNumber(myNumbers)); // 30 List<Integer> emptyList = Collections.emptyList(); System.out.println("Largest in empty list: " + findLargestNumber(emptyList)); // null // Of course, for this specific task, you'd use library functions (Principle 3) // System.out.println("Largest using library: " + Collections.max(myNumbers)); } }
14. Modularize. Use procedures and functions. #
模块化.使用过程和函数
Explanation: Break down large, complex problems into smaller, manageable, and reusable pieces of code (functions or methods). This improves readability, testability, and maintainability.
Example: (Calculating total price with tax and discount)
-
Less Modular (Monolithic):
public class MonolithicCalculation { public static void main(String[] args) { double itemPrice1 = 100.0; double itemPrice2 = 50.0; double taxRate = 0.07; // 7% double discountRate = 0.10; // 10% double subtotal = itemPrice1 + itemPrice2; System.out.println("Subtotal: $" + subtotal); double discountAmount = subtotal * discountRate; System.out.println("Discount: $" + discountAmount); double priceAfterDiscount = subtotal - discountAmount; double taxAmount = priceAfterDiscount * taxRate; System.out.println("Tax: $" + taxAmount); double total = priceAfterDiscount + taxAmount; System.out.println("Total Price: $" + String.format("%.2f", total)); } } -
Modularized:
public class ModularCalculation { public static double calculateSubtotal(double... prices) { double sum = 0; for (double price : prices) { sum += price; } return sum; } public static double applyDiscount(double amount, double discountRate) { return amount * (1 - discountRate); } public static double applyTax(double amount, double taxRate) { return amount * (1 + taxRate); } public static void main(String[] args) { double itemPrice1 = 100.0; double itemPrice2 = 50.0; double taxRate = 0.07; double discountRate = 0.10; double subtotal = calculateSubtotal(itemPrice1, itemPrice2); System.out.println("Subtotal: $" + subtotal); double priceAfterDiscount = applyDiscount(subtotal, discountRate); System.out.println("Price after discount: $" + String.format("%.2f",priceAfterDiscount)); double finalTotal = applyTax(priceAfterDiscount, taxRate); System.out.println("Total Price: $" + String.format("%.2f", finalTotal)); } }
15. Avoid gotos completely if you can keep the program readable. #
只要你能保证程序的可读性,能不用goto就别用
Explanation: Java does not have a goto statement, so this principle is inherently followed. goto statements can make program flow incredibly hard to follow (spaghetti code). Control flow structures like loops (for, while), conditionals (if, switch), and break/continue (used judiciously with labels for nested loops if absolutely necessary) are preferred.
Example: (Since Java has no goto, we’ll show how to avoid overly complex flow that goto might tempt one to use in other languages).
-
Simulating a situation where
gotomight be misused (e.g., error handling deep in loops):// In a language with goto, one might be tempted to jump out on error. // void processNestedData(int[][][] data) { // for (...) { // for (...) { // for (...) { // if (errorCondition) { // goto errorHandler; // } // } // } // } // return; // errorHandler: // handleTheError(); // } -
Java - Structured Approach (e.g., using exceptions or returning error codes/booleans):
public class StructuredControlFlow { // Using a boolean flag or returning early public boolean processDataMatrix(int[][] matrix) { for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { if (matrix[i][j] < 0) { // Error condition: negative value System.err.println("Error: Negative value found at [" + i + "][" + j + "]"); return false; // Exit early } // Process element System.out.print(matrix[i][j] + " "); } System.out.println(); } return true; // Success } // Using labeled break (use sparingly, only for nested loops) public void findValueInNestedArray(int[][][] data, int target) { boolean found = false; searchLoop: // Label for the outer loop for (int[][] plane : data) { for (int[] row : plane) { for (int value : row) { if (value == target) { System.out.println("Found target: " + target); found = true; break searchLoop; // Exits all three loops } } } } if (!found) { System.out.println("Target " + target + " not found."); } } public static void main(String[] args) { StructuredControlFlow scf = new StructuredControlFlow(); int[][] validMatrix = {{1, 2}, {3, 4}}; scf.processDataMatrix(validMatrix); int[][] invalidMatrix = {{1, 2}, {-1, 4}}; scf.processDataMatrix(invalidMatrix); int[][][] threedee = { {{1,2},{3,4}}, {{5,6},{7,8}} }; scf.findValueInNestedArray(threedee, 7); scf.findValueInNestedArray(threedee, 99); } }Java’s exception handling mechanism is another powerful way to handle exceptional flows without
goto.
16. Don’t patch bad code - rewrite it. #
不要给糟糕的代码打补丁 - 重写就是了.
Explanation: If a piece of code is fundamentally flawed, poorly designed, or overly complex, trying to apply small fixes (“patches”) often makes it worse. It’s usually better to refactor or rewrite it with a clearer design.
Example: (Conceptual)
-
Bad Code (e.g., a function with too many responsibilities and complex conditional logic):
// Imagine this function grew over time with many patches public class BadCode { public String processOrder(String productType, int quantity, String customerStatus, boolean isUrgent, boolean hasCoupon) { double price = 0; // Patch 1: Original pricing if (productType.equals("A")) price = 10.0 * quantity; else if (productType.equals("B")) price = 20.0 * quantity; // Patch 2: Urgent order surcharge if (isUrgent) price *= 1.1; // Patch 3: Customer status discount if (customerStatus.equals("GOLD")) price *= 0.9; else if (customerStatus.equals("SILVER")) price *= 0.95; // Patch 4: Coupon discount (applied after status discount) if (hasCoupon) price -= 5.0; // Fixed discount, careful with order of operations // Patch 5: Ensure price is not negative if (price < 0) price = 0; // Patch 6: Add shipping info based on urgency and product type (getting very messy) String shippingInfo = ""; if (isUrgent && productType.equals("A")) shippingInfo = "Express A"; else if (isUrgent) shippingInfo = "Express Generic"; else shippingInfo = "Standard"; return "Total: $" + String.format("%.2f", price) + ", Shipping: " + shippingInfo; } public static void main(String[] args) { BadCode bc = new BadCode(); System.out.println(bc.processOrder("A", 2, "GOLD", true, true)); } } -
Rewritten Code (e.g., using separate methods, strategy pattern, or clearer structure):
// This is a simplified rewrite. A real scenario might involve dedicated classes. public class RewrittenCode { private double getBasePrice(String productType, int quantity) { if (productType.equals("A")) return 10.0 * quantity; if (productType.equals("B")) return 20.0 * quantity; return 0.0; } private double applyUrgentSurcharge(double currentPrice, boolean isUrgent) { return isUrgent ? currentPrice * 1.1 : currentPrice; } private double applyCustomerDiscount(double currentPrice, String customerStatus) { if (customerStatus.equals("GOLD")) return currentPrice * 0.9; if (customerStatus.equals("SILVER")) return currentPrice * 0.95; return currentPrice; } private double applyCoupon(double currentPrice, boolean hasCoupon) { return hasCoupon ? Math.max(0, currentPrice - 5.0) : currentPrice; // Ensure not negative } private String getShippingInfo(boolean isUrgent, String productType) { if (isUrgent) { return productType.equals("A") ? "Express A" : "Express Generic"; } return "Standard"; } public String processOrder(String productType, int quantity, String customerStatus, boolean isUrgent, boolean hasCoupon) { double price = getBasePrice(productType, quantity); price = applyUrgentSurcharge(price, isUrgent); price = applyCustomerDiscount(price, customerStatus); price = applyCoupon(price, hasCoupon); String shipping = getShippingInfo(isUrgent, productType); return "Total: $" + String.format("%.2f", price) + ", Shipping: " + shipping; } public static void main(String[] args) { RewrittenCode rc = new RewrittenCode(); System.out.println(rc.processOrder("A", 2, "GOLD", true, true)); } }
17. Write and test a big program in small pieces. #
编写以及测试一个大型程序的时候,分块进行
Explanation: Develop incrementally. Write a small part of the functionality, test it thoroughly, then build the next piece. This makes debugging easier and provides constant feedback.
Example: (Conceptual: Building a simple calculator)
-
Piece 1: Addition Functionality
// Step 1: Create an Adder class or method public class Calculator { public int add(int a, int b) { return a + b; } } // Step 1 Test: // public class CalculatorTest { // @Test // public void testAdd() { // Calculator calc = new Calculator(); // assertEquals(5, calc.add(2, 3)); // assertEquals(-1, calc.add(-2, 1)); // } // } -
Piece 2: Subtraction Functionality (added to Calculator class)
public class Calculator { // Extended public int add(int a, int b) { return a + b; } public int subtract(int a, int b) { // New piece return a - b; } } // Step 2 Test: Add testSubtract() to CalculatorTest // public class CalculatorTest { // ... // @Test // public void testSubtract() { // Calculator calc = new Calculator(); // assertEquals(1, calc.subtract(3, 2)); // } // }And so on, adding multiplication, division, UI elements, etc., testing each piece as it’s developed.
18. Use recursive procedures for recursively-defined data structures. #
使用递归过程来处理递归定义的数据结构
Explanation: Data structures like trees or linked lists are often defined recursively (a node contains other nodes). Recursive functions naturally mirror this structure, often leading to more elegant and understandable code for operations like traversal or search.
Example: (Traversing a simple binary tree)
-
Recursively-Defined Data Structure (Node):
class TreeNode { int value; TreeNode left; TreeNode right; TreeNode(int value) { this.value = value; this.left = null; this.right = null; } } -
Recursive Procedure (In-order Traversal):
public class RecursiveTreeTraversal { public void inorderTraversal(TreeNode node) { if (node == null) { return; // Base case for recursion } inorderTraversal(node.left); // Recurse on left child System.out.print(node.value + " "); // Process current node inorderTraversal(node.right); // Recurse on right child } public static void main(String[] args) { // 4 // / \ // 2 5 // / \ // 1 3 TreeNode root = new TreeNode(4); root.left = new TreeNode(2); root.right = new TreeNode(5); root.left.left = new TreeNode(1); root.left.right = new TreeNode(3); RecursiveTreeTraversal traverser = new RecursiveTreeTraversal(); System.out.println("In-order traversal:"); traverser.inorderTraversal(root); // Output: 1 2 3 4 5 } }
19. Test input for plausibility and validity. #
始终要测试输入的正确性和有效性
Explanation: Don’t assume inputs will always be correct. Validate them against expected types, ranges, formats, and business rules.
Example:
public class InputValidation {
// Expects age to be a positive integer, plausibly within human lifespan.
public void processAge(int age) {
if (age <= 0) {
System.err.println("Error: Age must be positive.");
// throw new IllegalArgumentException("Age must be positive.");
return;
}
if (age > 130) { // Plausibility check
System.err.println("Warning: Age " + age + " seems implausibly high.");
// Potentially still process, or require confirmation
}
System.out.println("Processing age: " + age);
// ... further processing
}
// Expects email to have a basic format (not a full regex here for simplicity)
public void processEmail(String email) {
if (email == null || email.isEmpty()) {
System.err.println("Error: Email cannot be empty.");
return;
}
if (!email.contains("@") || !email.contains(".")) { // Basic validity
System.err.println("Error: Invalid email format for '" + email + "'.");
return;
}
System.out.println("Processing email: " + email);
// ... further processing
}
public static void main(String[] args) {
InputValidation validator = new InputValidation();
validator.processAge(25); // Valid
validator.processAge(0); // Invalid (Error)
validator.processAge(150); // Plausible but suspicious (Warning)
validator.processEmail("test@example.com"); // Valid
validator.processEmail("testexample.com"); // Invalid (Error)
validator.processEmail(null); // Invalid (Error)
}
}
20. Make sure input doesn’t violate the limits of the program. #
确保输入不会超出程序的限制
Explanation: Programs often have implicit or explicit limits (e.g., array sizes, maximum values for numeric types, string lengths). Inputs should be checked to prevent buffer overflows, out-of-memory errors, or other issues.
Example:
public class InputLimits {
private static final int MAX_NAME_LENGTH = 50;
private String[] userNames = new String[10]; // Fixed size array - limit on users
private int userCount = 0;
public boolean addUserName(String name) {
// Check string length limit
if (name.length() > MAX_NAME_LENGTH) {
System.err.println("Error: Name exceeds maximum length of " + MAX_NAME_LENGTH + " characters.");
return false;
}
// Check array capacity limit
if (userCount >= userNames.length) {
System.err.println("Error: Maximum number of users reached (" + userNames.length + ").");
return false;
}
userNames[userCount++] = name;
System.out.println("User '" + name + "' added.");
return true;
}
public static void main(String[] args) {
InputLimits limits = new InputLimits();
limits.addUserName("Alice");
limits.addUserName("Bob");
limits.addUserName("ThisNameIsDefinitelyWayTooLongForThePredefinedMaximumCharacterLimit"); // Too long
for (int i = 0; i < 10; i++) { // Try to fill up the array
if (!limits.addUserName("User" + i)) break; // Stop if add fails
}
limits.addUserName("OverflowUser"); // Should fail due to array limit
}
}
Using dynamic collections like ArrayList often mitigates fixed array size issues, but other limits (like memory or specific business rule limits) might still apply.
21. Terminate input by end-of-file marker, not by count. #
通过文件结束符来终止输入,而不是通过记数
Explanation: When reading data (especially from files or streams), it’s more robust to read until an end-of-file (EOF) marker or equivalent signal is encountered, rather than relying on a pre-defined count of items, which might be incorrect.
Example: (Reading lines from System.in or a file)
-
Less Robust (Relying on count):
import java.util.Scanner; // This example is a bit contrived for System.in, as you wouldn't typically // ask the user "how many lines" before they type. More relevant for file processing. public class InputByCount { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("How many lines will you enter? "); int lineCount = 0; if (scanner.hasNextInt()){ lineCount = scanner.nextInt(); } scanner.nextLine(); // Consume newline System.out.println("Enter " + lineCount + " lines:"); for (int i = 0; i < lineCount; i++) { if (scanner.hasNextLine()) { // Still good to check String line = scanner.nextLine(); System.out.println("You entered: " + line); } else { System.out.println("Unexpected end of input before " + lineCount + " lines."); break; } } scanner.close(); } } -
More Robust (Using EOF/hasNextLine):
import java.util.Scanner; public class InputByEOF { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.println("Enter lines of text (Ctrl+D or Ctrl+Z to end):"); // For System.in, scanner.hasNextLine() will return false when the stream is closed // (e.g., by EOF signal like Ctrl+D on Unix or Ctrl+Z then Enter on Windows). // For files, it returns false when the end of the file is reached. while (scanner.hasNextLine()) { String line = scanner.nextLine(); if (line.equalsIgnoreCase("exit")) { // Optional way to signal end break; } System.out.println("You entered: " + line); } System.out.println("Finished reading input."); scanner.close(); } }
22. Identify bad input; recover if possible. #
识别错误输入;并尽可能修复
Explanation: When invalid input is detected, the program should ideally handle it gracefully. This might involve informing the user, attempting to correct minor errors (e.g., trimming whitespace, correcting case), or providing default values.
Example:
import java.util.Scanner;
public class InputRecovery {
public static int getPositiveIntegerInput(Scanner scanner, String prompt) {
int number = -1;
while (number <= 0) {
System.out.print(prompt);
String inputLine = scanner.nextLine().trim(); // Trim whitespace (minor recovery)
try {
number = Integer.parseInt(inputLine);
if (number <= 0) {
System.out.println("Invalid input: Please enter a positive number.");
}
} catch (NumberFormatException e) {
// Attempt to recover from common typos, e.g., "12o" instead of "120"
// This is a very simple example; real recovery can be complex.
String correctedInput = inputLine.replaceAll("[oO]", "0").replaceAll("[lL]", "1");
try {
number = Integer.parseInt(correctedInput);
if (number > 0) {
System.out.println("Interpreted input as: " + number);
} else {
System.out.println("Invalid input: Not a valid positive number. Please try again.");
number = -1; // reset to ensure loop continues
}
} catch (NumberFormatException ex) {
System.out.println("Invalid input: Not a valid number. Please try again.");
number = -1; // reset to ensure loop continues
}
}
}
return number;
}
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int age = getPositiveIntegerInput(scanner, "Enter your age (must be positive): ");
System.out.println("Age entered: " + age);
// Example with a default if input is problematic after retries (not fully implemented above for brevity)
// String color = getUserPreference(scanner, "Enter favorite color (default: blue): ", "blue");
// System.out.println("Favorite color: " + color);
scanner.close();
}
}
23. Make input easy to prepare and output self-explanatory. #
让输入容易构造;让输出表述清楚
Explanation:
- Easy Input: Design input formats that are simple for users or other systems to generate. Provide clear prompts.
- Self-Explanatory Output: Output should be understandable on its own, with labels and units, without needing to look at the code.
Example:
-
Input/Output (Less Clear):
import java.util.Scanner; public class CrypticIO { public static void main(String[] args) { Scanner sc = new Scanner(System.in); System.out.print("> "); // What input is expected? double v1 = sc.nextDouble(); System.out.print("> "); double v2 = sc.nextDouble(); System.out.println(v1 * v2); // What does this output represent? sc.close(); } } -
Input/Output (Clearer):
import java.util.Scanner; public class ClearIO { public static void main(String[] args) { Scanner scanner = new Scanner(System.in); System.out.print("Enter the length of the rectangle (e.g., 10.5): "); double length = -1; if(scanner.hasNextDouble()) length = scanner.nextDouble(); else { System.out.println("Invalid length input."); scanner.close(); return;} System.out.print("Enter the width of the rectangle (e.g., 5.2): "); double width = -1; if(scanner.hasNextDouble()) width = scanner.nextDouble(); else { System.out.println("Invalid width input."); scanner.close(); return;} if (length > 0 && width > 0) { double area = length * width; System.out.println("\n--- Calculation Result ---"); System.out.println("Rectangle Length: " + length + " units"); System.out.println("Rectangle Width: " + width + " units"); System.out.println("Calculated Area: " + String.format("%.2f", area) + " square units"); } else { System.out.println("Invalid dimensions provided. Length and width must be positive."); } scanner.close(); } }
24. Use uniform input formats. #
使用统一的输入格式
Explanation: If your program takes multiple types of input or similar inputs in different places, try to keep their format consistent. This reduces user confusion and simplifies parsing logic.
Example: (Date input)
-
Inconsistent Format:
// Elsewhere in the code: // System.out.print("Enter start date (MM/DD/YYYY): "); // String startDate = scanner.nextLine(); // Here: // System.out.print("End date (YYYY-MM-DD): "); // String endDate = scanner.nextLine(); // This requires different parsing logic and can confuse users. public class InconsistentInputFormat { public static void main(String[] args) { System.out.println("Imagine a program asking for dates in different formats:"); System.out.println("Function A asks: MM/DD/YYYY"); System.out.println("Function B asks: YYYY-MM-DD"); System.out.println("This is confusing!"); } } -
Uniform Format:
import java.util.Scanner; // Consistently use one format, e.g., YYYY-MM-DD public class UniformInputFormat { public static String getDateInput(Scanner scanner, String prompt) { System.out.print(prompt + " (YYYY-MM-DD): "); return scanner.nextLine(); } public static void main(String[] args) { Scanner scanner = new Scanner(System.in); String startDate = getDateInput(scanner, "Enter start date"); String endDate = getDateInput(scanner, "Enter end date"); System.out.println("Start Date Entered: " + startDate); System.out.println("End Date Entered: " + endDate); // Parsing logic would then consistently expect "YYYY-MM-DD" scanner.close(); } }
25. Make input easy to proofread. #
让输入容易校对
Explanation: Design input formats that are human-readable and easy to visually check for errors. For example, using delimiters or clear keywords rather than relying purely on position.
Example: (Inputting a series of key-value pairs)
-
Harder to Proofread (Positional, no delimiters):
// Expected input: "JohnDoe30NewYork" (Name Age City) // If input is "JohnDoeNewYork30", it's hard to spot the error without knowing the exact format. public class HardToProofread { public static void main(String[] args) { String input = "ProductAX100true"; // ItemCode, Quantity, IsTaxable // Could be ItemCode, IsTaxable, Quantity System.out.println("Raw input: " + input); // Parsing this is error-prone if fields have variable length or order changes } } -
Easier to Proofread (Delimited, Keyed):
// Input as: "name:John Doe, age:30, city:New York" // Or for command line: --name "John Doe" --age 30 --city "New York" public class EasyToProofread { public static void main(String[] args) { // Option 1: Delimited String String input1 = "itemCode:ProductAX, quantity:100, isTaxable:true"; System.out.println("Delimited input: " + input1); // Parsing involves splitting by ',' then by ':' // Option 2: Separate lines with prompts (already shown in ClearIO) // Option 3: Configuration file style System.out.println("\nConfig file style (easier to read and proofread):"); System.out.println("itemCode = ProductAX"); System.out.println("quantity = 100"); System.out.println("isTaxable = true"); } }
26. Use self-identifying input. Allow defaults. Echo both on output. #
使用输入提示.接受默认值.并将其显示
Explanation:
- Self-identifying input: Input items should ideally indicate what they are (e.g.,
name=valuepairs rather than justvalue). - Allow defaults: For non-critical inputs, provide sensible default values if the user doesn’t supply them.
- Echo input: Display the values being used by the program (both user-supplied and defaults) so the user can verify them.
Example:
import java.util.Scanner;
import java.util.HashMap;
import java.util.Map;
public class SelfIdentifyingInput {
public static Map<String, String> parseConfig(String[] args, Map<String, String> defaults) {
Map<String, String> config = new HashMap<>(defaults); // Start with defaults
for (String arg : args) {
if (arg.contains("=")) {
String[] parts = arg.split("=", 2);
if (parts.length == 2) {
String key = parts[0].replaceFirst("--", ""); // Remove -- prefix
config.put(key, parts[1]); // User input overrides default
}
}
}
return config;
}
public static void main(String[] args) {
// 1. Define Defaults
Map<String, String> defaultSettings = new HashMap<>();
defaultSettings.put("port", "8080");
defaultSettings.put("environment", "development");
defaultSettings.put("logLevel", "INFO");
// Simulating command-line args: --port=9000 --logLevel=DEBUG
// Here, 'port' and 'logLevel' are self-identifying. 'environment' will use default.
String[] commandLineArgs = {"--port=9000", "--logLevel=DEBUG"};
// If args were: {"--newSetting=custom"}, it adds a new setting.
Map<String, String> effectiveSettings = parseConfig(commandLineArgs, defaultSettings);
// 2. Echo Input (Defaults and User-Supplied)
System.out.println("--- Effective Configuration ---");
for (Map.Entry<String, String> entry : effectiveSettings.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
String source = defaultSettings.containsKey(key) && defaultSettings.get(key).equals(value) &&
!isOverridden(commandLineArgs, key) ? "(default)" : "(user-supplied or new)";
if (isOverridden(commandLineArgs, key) && defaultSettings.containsKey(key) && !defaultSettings.get(key).equals(value)) {
source = "(user override of default: " + defaultSettings.get(key) + ")";
} else if (isOverridden(commandLineArgs, key) && !defaultSettings.containsKey(key)) {
source = "(user-supplied, new)";
} else if (defaultSettings.containsKey(key) && defaultSettings.get(key).equals(value) && !isOverridden(commandLineArgs, key)) {
source = "(default)";
}
System.out.println(key + ": " + value + " " + source);
}
System.out.println("-----------------------------");
// Program would now use effectiveSettings...
// System.out.println("Starting server on port: " + effectiveSettings.get("port"));
}
private static boolean isOverridden(String[] args, String keyToCheck) {
for (String arg : args) {
if (arg.startsWith("--" + keyToCheck + "=")) {
return true;
}
}
return false;
}
}
27. Make sure all variables are initialized before use. #
确保所有的变量在使用前都被初始化
Explanation: Using a variable before it has been assigned a value leads to unpredictable behavior or compile-time errors in Java (for local variables). Always initialize variables to a sensible default or with their intended first value.
Example:
-
Potential Issue (Java compiler often catches this for local variables):
public class UninitializedVariable { public void process() { String message; // Not initialized boolean sendNotification = true; // Example condition if (sendNotification) { message = "Notification to send."; } // If sendNotification is false, 'message' might not have been initialized. // System.out.println(message); // COMPILE ERROR: variable message might not have been initialized } } -
Corrected (Initialized):
public class InitializedVariable { private String instanceMessage; // Instance variables get default null/0/false public void process() { String localMessage = null; // Initialize local variable explicitly (or to a default string) // Or String localMessage = ""; boolean sendNotification = false; // Example condition if (sendNotification) { localMessage = "Notification to send."; } else { localMessage = "No notification needed."; // Ensure all paths initialize } System.out.println("Local Message: " + localMessage); // Instance variables are default initialized if (instanceMessage == null) { System.out.println("Instance message is currently null."); } instanceMessage = "Set instance message."; System.out.println("Instance Message: " + instanceMessage); } public static void main(String[] args) { InitializedVariable iv = new InitializedVariable(); iv.process(); } }Java’s compiler is good at enforcing this for local variables. For instance/class variables, they get default initial values (e.g.,
nullfor objects,0forint,falseforboolean), but it’s often good practice to initialize them explicitly in constructors or declaration if a specific starting value is needed.
28. Don’t stop at one bug. #
不要因某一bug而停止不前
Explanation: When you find a bug, fix it, but also consider if the same logical error or misunderstanding could have caused other bugs elsewhere in the code. Also, after fixing, re-test thoroughly as your fix might uncover or create new issues.
Example: (Conceptual) Imagine you find a bug where calculating a discount gives an incorrect result because of integer division:
// Initial Buggy Code
// int price = 100;
// int discountPercentage = 15; // 15%
// int discountAmount = price * discountPercentage / 100; // Bug: if price * discountPercentage < 100, this can be 0
// int finalPrice = price - discountAmount;
You fix it:
// double price = 100.0;
// double discountPercentage = 0.15; // Use decimal for percentage
// double discountAmount = price * discountPercentage;
// double finalPrice = price - discountAmount;
Don’t stop here!
- Ask: “Are there other places in the code where I perform similar percentage calculations or divisions that might suffer from the same integer division issue?”
- Look for: Other calculations involving rates, ratios, or scaling factors.
- Consider: If this error was due to a misunderstanding of type casting or arithmetic promotion, are there other subtle type-related bugs?
- Re-test: Test not only this specific discount calculation but related functionalities as well.
This principle is more about a debugging mindset than a specific code snippet.
29. Use debugging compilers. #
打开编译程序的调试选项
Explanation: Most modern Java IDEs (IntelliJ IDEA, Eclipse, VS Code with Java extensions) compile with debug information by default. This information is crucial for debuggers to allow step-by-step execution, inspecting variables, and setting breakpoints. If compiling manually with javac, the -g option includes debug information.
Example: (Manual compilation context)
-
Compiling without debug info (minimal):
# javac MyProgram.javaRunning a debugger on
MyProgram.classcompiled this way might offer limited variable inspection. -
Compiling with debug info (recommended for development):
# javac -g MyProgram.javaThis embeds line number information and local variable names into the class file, which debuggers like
jdb(command-line Java debugger) or IDE debuggers use.// MyProgram.java public class MyProgram { public static void main(String[] args) { String message = "Hello, Debugger!"; int count = 5; for (int i = 0; i < count; i++) { System.out.println(message + " - " + (i + 1)); } // A debugger can stop here and inspect i, count, message } }When you run this in an IDE’s debugger, you can set a breakpoint on the
System.out.printlnline and inspect the values ofmessage,count, andiat each iteration. This is possible because debug info is included.
30. Watch out for off-by-one errors. #
小心off-by-one错误
Explanation: These errors occur when a loop iterates one time too many or one time too few, or when an array index is off by one (e.g., accessing array[length] instead of array[length-1]). They are common in conditions involving <, <=, >, >=, and array indexing.
Example:
-
Incorrect (Potential Off-by-One):
public class OffByOneError { public static void main(String[] args) { int[] numbers = {10, 20, 30, 40, 50}; // Common error 1: Using <= with array.length for index System.out.println("Attempting to print (potential error):"); try { for (int i = 0; i <= numbers.length; i++) { // i goes from 0 to 5 (inclusive) System.out.println(numbers[i]); // numbers[5] will cause ArrayIndexOutOfBoundsException } } catch (ArrayIndexOutOfBoundsException e) { System.err.println("Error: " + e.getMessage()); } // Common error 2: Loop runs one less time than intended System.out.println("\nLooping one less time:"); int count = 5; // Intend to count 1 to 5 for (int i = 1; i < count; i++) { // i goes 1, 2, 3, 4. Misses 5. System.out.print(i + " "); } System.out.println(); } } -
Corrected:
public class OffByOneCorrected { public static void main(String[] args) { int[] numbers = {10, 20, 30, 40, 50}; // Corrected array iteration System.out.println("Correctly printing array elements:"); for (int i = 0; i < numbers.length; i++) { // i goes from 0 to 4 (exclusive of length) System.out.println(numbers[i]); } // Or using enhanced for-loop (safer) // for (int num : numbers) { // System.out.println(num); // } System.out.println("\nLooping correctly 1 to 5:"); int count = 5; for (int i = 1; i <= count; i++) { // i goes 1, 2, 3, 4, 5. System.out.print(i + " "); } System.out.println(); } }
31. Take care to branch the right way on equality. #
注意等价比较的分支
Explanation: When dealing with conditions that involve equality, ensure your logic correctly handles all cases, especially if the “equal” case has distinct behavior from “greater than” or “less than.” Also, be mindful of floating-point comparisons (see principle 38) and object comparisons (equals() vs ==).
Example: (Assigning a grade based on score)
-
Potentially Flawed Branching (if boundaries are critical):
public class EqualityBranchingFlaw { public String getGrade(int score) { String grade; if (score > 90) { // What if score is exactly 90? This misses it for 'A'. grade = "A"; } else if (score > 80) { // What if score is exactly 80? grade = "B"; } else if (score > 70) { grade = "C"; } else { grade = "D or F"; // Simplified } return grade; } public static void main(String[] args) { EqualityBranchingFlaw eg = new EqualityBranchingFlaw(); System.out.println("Score 90: " + eg.getGrade(90)); // Outputs B, might be intended as A System.out.println("Score 80: " + eg.getGrade(80)); // Outputs C, might be intended as B } } -
Corrected Branching (using
>=):public class EqualityBranchingCorrect { public String getGrade(int score) { String grade; if (score >= 90) { // Handles 90 correctly for 'A' grade = "A"; } else if (score >= 80) { // Handles 80 correctly for 'B' grade = "B"; } else if (score >= 70) { grade = "C"; } else { grade = "D or F"; } return grade; } public static void main(String[] args) { EqualityBranchingCorrect egc = new EqualityBranchingCorrect(); System.out.println("Score 90: " + egc.getGrade(90)); // Outputs A System.out.println("Score 80: " + egc.getGrade(80)); // Outputs B } }For object comparison:
String s1 = new String("hello"); String s2 = new String("hello"); // if (s1 == s2) { /* This compares references, likely false */ } // if (s1.equals(s2)) { /* This compares content, true */ }
32. Be careful if a loop exits to the same place from the middle and the bottom. #
当循环中有多个跳出点时要小心
Explanation: If a loop has multiple exit points (e.g., a break statement in the middle and the natural loop termination) that lead to the same subsequent code block, it can sometimes make the logic harder to follow or maintain. Consider if refactoring could simplify the flow.
Example:
-
Loop with Multiple Exits to Same Logic:
public class MultipleExits { public boolean findAndProcess(int[] data, int target, int processLimit) { boolean found = false; int processedCount = 0; for (int item : data) { if (item == target) { found = true; System.out.println("Target " + target + " found."); // Some common action after finding or reaching limit // break; // Exit from middle } // Process item System.out.println("Processing item: " + item); processedCount++; if (processedCount >= processLimit) { System.out.println("Processing limit reached."); // Same common action break; // Exit from middle due to limit } } // Common logic block that would be executed whether loop completed normally // or exited via one of the breaks if (found) { System.out.println("Finalizing based on target found."); } else { System.out.println("Finalizing based on target NOT found or limit reached before find."); } System.out.println("Processed " + processedCount + " items in total."); return found; } public static void main(String[] args) { MultipleExits me = new MultipleExits(); int[] myData = {1, 2, 3, 4, 5, 6, 7}; me.findAndProcess(myData, 5, 10); // Found before limit System.out.println("----"); me.findAndProcess(myData, 99, 3); // Limit reached before found } } -
Alternative (Potentially Clearer by refactoring or flags): One way to simplify is to ensure the loop condition itself handles one of the exits, or use flags and do a single check after the loop.
public class SingleExitFocus { public boolean findAndProcessRefactored(int[] data, int target, int processLimit) { boolean found = false; int processedCount = 0; // Loop continues as long as not found AND limit not reached AND data available for (int i = 0; i < data.length && !found && processedCount < processLimit; i++) { int item = data[i]; // Process item first System.out.println("Processing item: " + item); processedCount++; if (item == target) { found = true; System.out.println("Target " + target + " found."); // No break needed here, loop condition will handle it } } // Post-loop logic if (processedCount >= processLimit && !found) { System.out.println("Processing limit reached before target was found."); } if (found) { System.out.println("Finalizing based on target found."); } else { System.out.println("Finalizing based on target NOT found."); } System.out.println("Processed " + processedCount + " items in total."); return found; } public static void main(String[] args) { SingleExitFocus sef = new SingleExitFocus(); int[] myData = {1, 2, 3, 4, 5, 6, 7}; sef.findAndProcessRefactored(myData, 5, 10); System.out.println("----"); sef.findAndProcessRefactored(myData, 99, 3); } }The key is to evaluate if the multiple exit points genuinely simplify or complicate the understanding of when and why the loop terminates and what happens next. Sometimes, a
breakis the clearest way.
33. Make sure your code does “nothing” gracefully. #
如果什么都不做,那么也要优雅地表达这个意思
Explanation: If a method or function is called in a situation where it has no work to do (e.g., processing an empty list, an action is disabled), it should behave predictably and not cause errors. This might mean returning an empty result, doing nothing, or logging a message.
Example:
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
public class GracefulNothing {
// Processes a list of names. Should handle an empty list gracefully.
public void printNames(List<String> names) {
if (names == null || names.isEmpty()) {
System.out.println("No names to print."); // Gracefully does "nothing" relevant
return;
}
System.out.println("List of Names:");
for (String name : names) {
System.out.println("- " + name);
}
}
// Calculates average. Should handle no scores.
public double calculateAverage(List<Double> scores) {
if (scores == null || scores.isEmpty()) {
System.out.println("Cannot calculate average from an empty list of scores. Returning 0.0.");
return 0.0; // Or NaN, or throw exception, depending on requirements
}
double sum = 0;
for (double score : scores) {
sum += score;
}
return sum / scores.size();
}
public static void main(String[] args) {
GracefulNothing gn = new GracefulNothing();
List<String> nameList = List.of("Alice", "Bob");
gn.printNames(nameList);
List<String> emptyNameList = Collections.emptyList();
gn.printNames(emptyNameList); // Does "nothing" gracefully
gn.printNames(null); // Also handles null gracefully
List<Double> scoreList = List.of(85.5, 90.0, 77.5);
System.out.println("Average: " + gn.calculateAverage(scoreList));
List<Double> emptyScoreList = new ArrayList<>();
System.out.println("Average: " + gn.calculateAverage(emptyScoreList)); // Handles empty gracefully
}
}
34. Test programs at their boundary values. #
用边界值测试程序
Explanation: Errors often occur at the edges of valid input ranges (e.g., 0, -1, max value, empty string, null list). Boundary value analysis is a crucial testing technique.
Example: (A function that checks if a number is within a range [min, max])
public class BoundaryValueTesting {
// Checks if value is within [min, max] inclusive
public boolean isInRange(int value, int min, int max) {
return value >= min && value <= max;
}
public static void main(String[] args) {
BoundaryValueTesting bvt = new BoundaryValueTesting();
int testMin = 10;
int testMax = 20;
System.out.println("Testing range [" + testMin + ", " + testMax + "]");
// Values just outside the boundary
System.out.println("Is " + (testMin - 1) + " in range? " + bvt.isInRange(testMin - 1, testMin, testMax)); // false (9)
System.out.println("Is " + (testMax + 1) + " in range? " + bvt.isInRange(testMax + 1, testMin, testMax)); // false (21)
// Values exactly on the boundary
System.out.println("Is " + testMin + " in range? " + bvt.isInRange(testMin, testMin, testMax)); // true (10)
System.out.println("Is " + testMax + " in range? " + bvt.isInRange(testMax, testMin, testMax)); // true (20)
// Values just inside the boundary
System.out.println("Is " + (testMin + 1) + " in range? " + bvt.isInRange(testMin + 1, testMin, testMax)); // true (11)
System.out.println("Is " + (testMax - 1) + " in range? " + bvt.isInRange(testMax - 1, testMin, testMax)); // true (19)
// Nominal value within range
System.out.println("Is 15 in range? " + bvt.isInRange(15, testMin, testMax)); // true
// Test with min == max
System.out.println("\nTesting range [5, 5]");
System.out.println("Is 5 in range [5,5]? " + bvt.isInRange(5, 5, 5)); // true
System.out.println("Is 4 in range [5,5]? " + bvt.isInRange(4, 5, 5)); // false
System.out.println("Is 6 in range [5,5]? " + bvt.isInRange(6, 5, 5)); // false
// Consider empty collections, null strings, 0, max int values etc. for other functions.
}
}
35. Check some answers by hand. #
手工检查一些答案
Explanation: For complex calculations or logic, especially during development or debugging, manually compute the expected output for a few representative inputs and compare it with the program’s output. This can help catch logical errors.
Example: (Calculating compound interest)
Formula for compound interest: $A = P (1 + r/n)^{nt}$ Where: $P$ = principal amount $r$ = annual interest rate (decimal) $n$ = number of times interest is compounded per year $t$ = number of years
public class ManualCheck {
public static double calculateCompoundInterest(double principal, double rate, int compoundsPerYear, int years) {
// A = P (1 + r/n)^(nt)
return principal * Math.pow(1 + (rate / compoundsPerYear), (double)compoundsPerYear * years);
}
public static void main(String[] args) {
double principal = 1000; // P
double rate = 0.05; // r (5%)
int compoundsPerYear = 4; // n (quarterly)
int years = 2; // t
// Manual Calculation for one case:
// P = 1000, r = 0.05, n = 4, t = 2
// r/n = 0.05 / 4 = 0.0125
// 1 + r/n = 1.0125
// nt = 4 * 2 = 8
// (1.0125)^8 ≈ 1.104486
// A ≈ 1000 * 1.104486 ≈ 1104.486
double result = calculateCompoundInterest(principal, rate, compoundsPerYear, years);
System.out.println("Program calculated final amount: " + String.format("%.3f", result));
System.out.println("Manually calculated approximate amount: 1104.486");
if (Math.abs(result - 1104.4861011534084) < 0.0001) { // Compare with more precision
System.out.println("Program result matches manual check (within tolerance).");
} else {
System.err.println("Program result DOES NOT match manual check!");
}
}
}
This practice helps build confidence in the correctness of the algorithm and its implementation.
36. 10.0 times 0.1 is hardly ever 1.0. #
10.0乘以0.1很难保证永远是1.0
Explanation: Floating-point numbers (float, double) in computers are binary approximations of decimal numbers. This can lead to small precision errors, so 0.1 might not be stored exactly as 0.1. Thus, calculations like 10.0 * 0.1 might result in a value very close to 1.0 (e.g., 0.9999999999999999 or 1.0000000000000001), but not exactly 1.0.
Example:
public class FloatingPointImprecision {
public static void main(String[] args) {
double a = 0.1;
double b = 0.2;
double sum = a + b; // Expected 0.3
System.out.println("0.1 + 0.2 = " + sum); // Often prints 0.30000000000000004
if (sum == 0.3) {
System.out.println("Sum is exactly 0.3 (This is RARELY true for general FP math).");
} else {
System.out.println("Sum is NOT exactly 0.3.");
}
double ten = 10.0;
double pointOne = 0.1;
double product = ten * pointOne;
System.out.println("10.0 * 0.1 = " + product); // Often prints 0.9999999999999999 or 1.0
if (product == 1.0) {
System.out.println("Product is exactly 1.0.");
} else {
System.out.println("Product is NOT exactly 1.0. It is: " + String.format("%.20f", product));
}
// For precise arithmetic with decimals, use BigDecimal
java.math.BigDecimal bdTen = new java.math.BigDecimal("10.0");
java.math.BigDecimal bdPointOne = new java.math.BigDecimal("0.1");
java.math.BigDecimal bdProduct = bdTen.multiply(bdPointOne);
System.out.println("BigDecimal: 10.0 * 0.1 = " + bdProduct); // Prints 1.00 or 1.0
if (bdProduct.compareTo(new java.math.BigDecimal("1.0")) == 0) {
System.out.println("BigDecimal product is exactly 1.0.");
}
}
}
37. 7/8 is zero while 7.0/8.0 is not zero. #
7/8 等于0,而7.0/8.0不等于0
Explanation: In Java (and many other languages), if both operands of a division are integers, integer division is performed. This means the result is truncated to an integer (the fractional part is discarded). If one or both operands are floating-point numbers, floating-point division is performed, yielding a floating-point result.
Example:
public class IntegerVsFloatDivision {
public static void main(String[] args) {
int intNumerator = 7;
int intDenominator = 8;
int intResult = intNumerator / intDenominator;
System.out.println("Integer division: 7 / 8 = " + intResult); // Output: 0
double doubleNumerator = 7.0;
double doubleDenominator = 8.0;
double doubleResult = doubleNumerator / doubleDenominator;
System.out.println("Floating-point division: 7.0 / 8.0 = " + doubleResult); // Output: 0.875
// Mixing types promotes to floating point for the operation
double mixedResult1 = (double)intNumerator / intDenominator; // Cast one to double
System.out.println("Mixed division (cast): (double)7 / 8 = " + mixedResult1); // Output: 0.875
double mixedResult2 = intNumerator / doubleDenominator; // One is already double
System.out.println("Mixed division (implicit): 7 / 8.0 = " + mixedResult2); // Output: 0.875
// Careful:
double incorrectMixed = (double)(intNumerator / intDenominator); // Casts AFTER integer division
System.out.println("Incorrect mixed: (double)(7 / 8) = " + incorrectMixed); // Output: 0.0
}
}
38. Don’t compare floating point numbers solely for equality. #
不要简单地判断两个浮点数是否相等
Explanation: Due to the precision issues mentioned in principle 36, directly comparing two floating-point numbers for exact equality (==) is unreliable. Instead, check if their absolute difference is within a small tolerance (epsilon).
Example:
public class FloatingPointComparison {
public static void main(String[] args) {
double val1 = 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1 + 0.1; // Should be 1.0
double val2 = 1.0;
System.out.println("val1 (0.1 * 10 times summed): " + String.format("%.20f", val1));
System.out.println("val2 (1.0 directly): " + String.format("%.20f", val2));
// Unreliable direct comparison
if (val1 == val2) {
System.out.println("Direct comparison (==): val1 is equal to val2. (Might be true or false)");
} else {
System.out.println("Direct comparison (==): val1 is NOT equal to val2.");
}
// Reliable comparison with tolerance (epsilon)
double epsilon = 0.000001; // Define a small tolerance
if (Math.abs(val1 - val2) < epsilon) {
System.out.println("Tolerance comparison: val1 is effectively equal to val2 (within " + epsilon + ").");
} else {
System.out.println("Tolerance comparison: val1 is NOT effectively equal to val2 (within " + epsilon + ").");
}
// Using BigDecimal for exact comparisons if monetary or precision is critical
java.math.BigDecimal bdVal1 = new java.math.BigDecimal("0.0");
java.math.BigDecimal bdIncrement = new java.math.BigDecimal("0.1");
for(int i=0; i<10; i++) {
bdVal1 = bdVal1.add(bdIncrement);
}
java.math.BigDecimal bdVal2 = new java.math.BigDecimal("1.0");
System.out.println("BigDecimal val1: " + bdVal1);
System.out.println("BigDecimal val2: " + bdVal2);
if (bdVal1.compareTo(bdVal2) == 0) {
System.out.println("BigDecimal comparison: bdVal1 is equal to bdVal2.");
} else {
System.out.println("BigDecimal comparison: bdVal1 is NOT equal to bdVal2.");
}
}
}
39. Make it right before you make it faster. #
先做对,再做快
Explanation: The primary goal is correctness. An ultra-fast program that produces wrong results is useless. Focus on writing clear, correct code first. Optimization should come later, and only if performance is an actual issue identified by profiling.
Example: (Conceptual - e.g., choosing an algorithm)
- Scenario: You need to find an element in a collection.
- Step 1: Make it Right (Simple, Correct Approach)
Use a linear search on an
ArrayList. It’s easy to implement and verify for correctness.import java.util.List; public class CorrectFirst { public static int findElement(List<Integer> data, int target) { for (int i = 0; i < data.size(); i++) { if (data.get(i).equals(target)) { return i; // Found } } return -1; // Not found } // ... test cases to ensure correctness ... } - Step 2: Make it Faster (If Necessary and Proven by Profiling)
If profiling shows
findElementis a bottleneck for large lists, then consider optimizations.- If the list is sorted, use binary search.
- If lookups are very frequent, consider a
HashSetorHashMapfor O(1) average time complexity.
Don’t jump to complex algorithms prematurely if the simple, correct one is “fast enough.”// import java.util.Collections; // // ... If list is sorted and a bottleneck // public static int findElementFaster(List<Integer> sortedData, int target) { // return Collections.binarySearch(sortedData, target); // }
40. Make it fail-safe before you make it faster. #
先使其可靠,再让其更快
Explanation: Similar to “make it right,” reliability and robustness are paramount. Optimizations should not compromise the stability or error-handling capabilities of the program. A fast program that crashes frequently is not useful.
Example: (Conceptual - e.g., resource handling)
- Initial focus on fail-safety:
Ensure resources like files or network connections are always closed, even if errors occur. Use
try-finallyortry-with-resources.import java.io.*; public class FailSafeFirst { public void readFile(String filePath) { // try-with-resources ensures the reader is closed try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) { String line; while ((line = reader.readLine()) != null) { // Process line System.out.println(line); } } catch (FileNotFoundException e) { System.err.println("Error: File not found - " + filePath); // Handle error robustly } catch (IOException e) { System.err.println("Error reading file: " + e.getMessage()); // Handle error robustly } } } - Later, if performance is an issue for file reading (and it’s a proven bottleneck):
You might consider optimizations like using a larger buffer for
BufferedReader, memory-mapped files for very large files, or asynchronous I/O. But these optimizations must not break the fail-safe resource handling. For example, if you implement custom buffering, you still need to ensure data is flushed and files are closed correctly.
41. Make it clear before you make it faster. #
先把代码弄干净,再让它变快
Explanation: Clear, understandable code is easier to debug, maintain, and correctly optimize. If you optimize unclear code, you might introduce subtle bugs or make it even harder to understand. Refactor for clarity first.
Example: (Conceptual - a complex calculation)
-
Unclear code (perhaps with premature micro-optimizations):
// Hard to understand, maybe some bit-twiddling or unclear variable names // public class UnclearCode { // public int complexCalc(int x, int y, int z) { // int temp_a = (x << 2) + (y >> 1); // What does this mean? // int res = 0; // for (int i = z; i > 0; i = i - (z & 1 == 1 ? 1 : 2) ) { // Confusing loop decrement // res += temp_a * (i | y); // Why OR with y? // } // return res; // } // } -
Step 1: Make it Clear: Rewrite the logic with meaningful variable names, clear steps, and standard operations.
public class ClearerCode { // Suppose the unclear code was actually trying to do this: public double calculateWeightedSum(double value, int weight, int iterations) { if (iterations <= 0) return 0; double baseValue = value * 4.0 + (double)weight / 2.0; // Clarified 'temp_a' double totalSum = 0; // Clarified loop logic, assuming it was meant to iterate 'iterations' times // or based on some property of 'iterations' (e.g. only odd/even based on original z) // Let's assume a simple iteration for clarity here: for (int i = 0; i < iterations; i++) { // The 'i | y' part from original is still obscure and needs domain knowledge // For this example, let's assume it was a more straightforward calculation: totalSum += baseValue * (i + 1); // Example of a clearer operation } return totalSum; } } -
Step 2: Make it Faster (If Necessary): Once the
ClearerCode.calculateWeightedSumis confirmed correct and clear, if profiling shows it’s a bottleneck, then consider optimizations. Perhaps the loop can be parallelized, or a mathematical formula can replace the iterative sum. Optimizing clear code is safer.
42. Don’t sacrifice clarity for small gains in “efficiency.” #
别为一丁点”性能”就牺牲掉整洁
Explanation: This is a reiteration of principle 5. Minor performance improvements (often imperceptible to the user) that make the code significantly harder to read or maintain are generally not worth it. Focus on algorithmic efficiency for significant gains.
Example:
-
Less Clear (minor “optimization” that might be negligible):
public class MinorObscureOptimization { // Suppose we need to iterate a fixed number of times (e.g., 3) // and sum values from an array. public int sumFirstThree(int[] arr) { if (arr == null || arr.length < 3) return 0; // "Unrolling" a tiny loop. Compiler might do this anyway. // Adds little performance, slightly less flexible if N changes. return arr[0] + arr[1] + arr[2]; } } -
Clearer (and usually efficient enough):
public class ClearAndSufficient { public int sumFirstN(int[] arr, int n) { if (arr == null || n <= 0) return 0; int sum = 0; // Clear loop, adaptable if N changes. // For small N, performance difference is likely zero or negligible. for (int i = 0; i < n && i < arr.length; i++) { sum += arr[i]; } return sum; } }The “unrolled” version might be marginally faster in some microbenchmark, but the looped version is more readable, less error-prone if the number of elements changes, and modern JIT compilers are very good at optimizing simple loops.
43. Let your compiler do the simple optimizations. #
简单的优化让编译器去做
Explanation: Modern compilers (especially Java’s JIT - Just-In-Time compiler) are sophisticated. They perform many optimizations automatically, like loop unrolling, inlining, dead code elimination, and strength reduction (e.g., replacing x*2 with x<<1 where appropriate). Trying to manually do these often clutters the code for no real gain.
Example:
-
Manual “Optimization” (often unnecessary):
public class ManualMicroOptimization { public int calculate(int x, int y) { // Manually inlining a simple operation: // Instead of: int temp = add(x, y); return temp * 2; // (where add(a,b) {return a+b;} ) // one might write: int sum = x + y; // Manual inline of add return sum << 1; // Manual strength reduction (multiply by 2 using shift) } } -
Clear Code (Compiler will optimize):
public class CompilerOptimized { private int add(int a, int b) { // JIT can inline this call return a + b; } public int calculate(int x, int y) { int sum = add(x, y); return sum * 2; // JIT can perform strength reduction (sum << 1) if beneficial } }Trust the JIT. Write clear, straightforward code. The JIT is better at deciding when and how to apply low-level optimizations based on runtime characteristics.
44. Don’t strain to re-use code; reorganize instead. #
不要过分追求重用代码;下次用的时候重新组织一下即可
Explanation: While DRY (Don’t Repeat Yourself) is good, forcing code reuse when the functionalities are only superficially similar, or when it makes the reused component overly complex and hard to understand, can be detrimental. Sometimes it’s better to have two slightly different, clear pieces of code than one convoluted, “reusable” piece. If true commonality emerges later, refactor then.
Example: (Conceptual)
-
Strained Reuse: Imagine a
sendMessagefunction. Initially, it sends emails.// void sendMessage(String recipient, String subject, String body, boolean isHtmlEmail) { /* sends email */ }Later, you need to send SMS. Instead of a new function, you “strain”
sendMessage:// enum MessageType { EMAIL, SMS } // void sendMessage(MessageType type, String recipient, String subjectOrNull, String body, boolean isHtmlOrNull, String smsGatewayOrNull) { // if (type == MessageType.EMAIL) { /* email logic with subject, isHtml */ } // else if (type == MessageType.SMS) { /* SMS logic with smsGateway, ignoring subject/isHtml */ } // }This function becomes complex, with many parameters that are irrelevant depending on
type. -
Reorganize (Separate Functions): It’s clearer to have distinct functions if their core logic and parameters differ significantly.
public class ReorganizedCode { public void sendEmail(String recipientEmail, String subject, String body, boolean isHtml) { System.out.println("Sending Email to " + recipientEmail + " (HTML: " + isHtml + "): " + subject + " - " + body); // Actual email sending logic } public void sendSms(String phoneNumber, String message, String smsGateway) { System.out.println("Sending SMS via " + smsGateway + " to " + phoneNumber + ": " + message); // Actual SMS sending logic } // If there's truly common setup/logging, that can be a separate private method private void logMessageAttempt(String recipient, String message) { System.out.println("Attempting to send message to: " + recipient); } public void sendSystemNotificationEmail(String email, String content) { logMessageAttempt(email, content); sendEmail(email, "System Notification", content, false); } public static void main(String[] args) { ReorganizedCode sender = new ReorganizedCode(); sender.sendEmail("user@example.com", "Hello", "World", false); sender.sendSms("+1234567890", "Hi there!", "MyGateway"); } }If common underlying operations exist (e.g., logging, user lookup), those can be factored out into helper methods shared by
sendEmailandsendSms.
45. Make sure special cases are truly special. #
确保特殊的情况的确是特殊的
Explanation: Avoid cluttering your main logic with too many “special case” checks if they can be handled by a more general algorithm, or if the “specialness” can be encapsulated elsewhere (e.g., through configuration or data). Overuse of special case handling can make code complex and brittle.
Example: (Calculating shipping cost)
-
Too Many Apparent “Special Cases” in Main Logic:
public class OverSpecializedShipping { public double calculateShipping(String itemCode, String destinationCountry, double weight) { double cost = 0; // Base cost cost = weight * 5.0; // $5 per kg // "Special" handling for specific items if (itemCode.equals("FRAGILE_ITEM_001")) { cost += 10.0; // Extra for fragile } if (itemCode.equals("HEAVY_ITEM_XYZ")) { cost += weight * 2.0; // Extra for very heavy } // "Special" handling for specific destinations if (destinationCountry.equals("RemoteIsland")) { cost *= 1.5; // Surcharge } if (destinationCountry.equals("LocalMetro")) { cost *= 0.8; // Discount } // ... more and more specific if-statements ... return cost; } public static void main(String[] args) { OverSpecializedShipping oss = new OverSpecializedShipping(); System.out.println("Shipping FRAGILE_ITEM_001 (2kg) to RemoteIsland: " + oss.calculateShipping("FRAGILE_ITEM_001", "RemoteIsland", 2.0)); } } -
Generalized Approach (Data-driven or Strategy Pattern): Many “special cases” can be handled by data or by more abstract rules.
import java.util.Map; // This is a simplified data-driven example. A more robust system might use a rule engine or strategy pattern. public class GeneralizedShipping { private Map<String, Double> itemSurcharges = Map.of("FRAGILE_ITEM_001", 10.0); private Map<String, Double> itemWeightMultipliers = Map.of("HEAVY_ITEM_XYZ", 2.0); private Map<String, Double> destinationMultipliers = Map.of("RemoteIsland", 1.5, "LocalMetro", 0.8); private double baseRatePerKg = 5.0; public double calculateShipping(String itemCode, String destinationCountry, double weight) { double cost = weight * baseRatePerKg; // Apply item-specific adjustments from data cost += itemSurcharges.getOrDefault(itemCode, 0.0); cost += weight * itemWeightMultipliers.getOrDefault(itemCode, 0.0); // Note: original was base + extra, this adds extra based on weight again. Adjust logic as per real reqs. // For simplicity, let's assume the original logic was: // if (itemCode.equals("HEAVY_ITEM_XYZ")) cost += weight * 2.0 (additional cost) // which is what itemWeightMultipliers implies if its value is the *additional* per kg rate. // Or the HEAVY_ITEM_XYZ could have its own baseRate. // Apply destination-specific adjustments from data cost *= destinationMultipliers.getOrDefault(destinationCountry, 1.0); return cost; } public static void main(String[] args) { GeneralizedShipping gs = new GeneralizedShipping(); System.out.println("Shipping FRAGILE_ITEM_001 (2kg) to RemoteIsland: " + gs.calculateShipping("FRAGILE_ITEM_001", "RemoteIsland", 2.0)); System.out.println("Shipping NORMAL_ITEM (2kg) to OtherPlace: " + gs.calculateShipping("NORMAL_ITEM", "OtherPlace", 2.0)); } }The idea is to treat variations as data or configurable rules rather than hardcoded
ifstatements in the main logic, if possible. Truly special cases are those that fundamentally alter the algorithm, not just parameters.
46. Keep it simple to make it faster. #
保持简单以使其更快
Explanation: Simpler code often has fewer instructions, less overhead, and is easier for the compiler/JIT to optimize. Complex code can introduce inefficiencies that are hard to spot. This doesn’t mean avoiding efficient algorithms, but that the implementation of any algorithm should be kept as straightforward as possible.
Example: (Conceptual: Summing numbers in a list)
-
Overly Complex (and potentially slower due to indirection or unnecessary objects):
import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; // Using complex machinery for a simple task (contrived example) public class ComplexSum { public int sumList(List<Integer> numbers) { if (numbers == null || numbers.isEmpty()) return 0; // Unnecessarily complex for a simple sum AtomicInteger sum = new AtomicInteger(0); numbers.stream() .map(n -> (Runnable) () -> sum.addAndGet(n)) // Create Runnable for each .collect(Collectors.toList()) // Collect Runnables (not even running them here) .forEach(r -> { /* if we were to run them: r.run(); */ }); // This example is intentionally absurd to make the point. // A more realistic "complex" way might involve unnecessary intermediate collections // or custom iterator objects for a simple task. // Let's try a slightly more plausible "overly complex" way: return numbers.stream().reduce(0, (acc, val) -> Integer.sum(acc, val), Integer::sum); // Using parallel-capable reduce for a simple sum } } -
Simple and Fast:
import java.util.List; public class SimpleSum { public int sumList(List<Integer> numbers) { if (numbers == null || numbers.isEmpty()) return 0; int sum = 0; for (int number : numbers) { // Simple, direct, easy for JIT sum += number; } return sum; } // Even the stream version can be simpler if appropriate: // public int sumListWithStream(List<Integer> numbers) { // if (numbers == null) return 0; // return numbers.stream().mapToInt(Integer::intValue).sum(); // } }The simple
forloop is often very efficient for such tasks. While streams are powerful, for a basic sum, a direct loop is clear and performs well. The point is to avoid unnecessary complexity.
47. Don’t diddle code to make it faster - find a better algorithm. #
不要死磕代码来加快速度 - 找个更好的算法
Explanation: Micro-optimizing a fundamentally inefficient algorithm (e.g., tweaking a bubble sort) will yield marginal improvements at best. Significant performance gains usually come from choosing a more efficient algorithm (e.g., quicksort instead of bubble sort, hash map lookup instead of linear search).
Example: (Searching for an element)
-
Diddling an Inefficient Algorithm (Linear Search):
import java.util.List; public class DiddlingLinearSearch { // Linear search: O(n) public boolean contains(List<String> list, String target) { if (list == null || target == null) return false; int size = list.size(); // "Optimization": cache size (compiler likely does this) for (int i = 0; i < size; i++) { // "Optimization": compare target first (maybe minor effect, depends on equals impl) if (target.equals(list.get(i))) { return true; } } return false; } }These “optimizations” on linear search won’t change its O(n) nature.
-
Finding a Better Algorithm (e.g., using
HashSetfor frequent lookups):import java.util.List; import java.util.HashSet; import java.util.Set; public class BetterAlgorithmSearch { private Set<String> dataSet; // Pre-process list into a HashSet for O(1) average time lookups public BetterAlgorithmSearch(List<String> initialData) { if (initialData != null) { this.dataSet = new HashSet<>(initialData); } else { this.dataSet = new HashSet<>(); } } // HashSet.contains(): O(1) on average public boolean contains(String target) { if (target == null) return false; return dataSet.contains(target); } public static void main(String[] args) { List<String> myList = List.of("apple", "banana", "cherry", "date"); BetterAlgorithmSearch searcher = new BetterAlgorithmSearch(myList); System.out.println("Contains 'cherry'? " + searcher.contains("cherry")); // true System.out.println("Contains 'grape'? " + searcher.contains("grape")); // false } }If search is performed many times, the upfront cost of creating the
HashSetis amortized by much faster lookups. This is an algorithmic change, not just “diddling.”
48. Instrument your programs. Measure before making “efficiency” changes. #
用工具分析你的程序.在做”性能”改进前先评测一下
Explanation: Don’t guess where bottlenecks are. Use profiling tools (e.g., JProfiler, YourKit, VisualVM, or even simple System.nanoTime() for microbenchmarks) to identify the parts of your code that consume the most time or resources. Optimize those specific areas.
Example: (Conceptual - using System.nanoTime() for basic measurement)
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class PerformanceMeasurement {
public static void processList(List<Integer> list) {
// Simulate some processing by adding and removing from the beginning
// This is known to be slow for ArrayList, faster for LinkedList
for (int i = 0; i < 10000; i++) {
list.add(0, i); // Add at beginning
}
for (int i = 0; i < 10000; i++) {
list.remove(0); // Remove from beginning
}
}
public static void main(String[] args) {
List<Integer> arrayList = new ArrayList<>();
List<Integer> linkedList = new LinkedList<>();
// --- Measure ArrayList performance ---
long startTimeArrayList = System.nanoTime();
processList(arrayList);
long endTimeArrayList = System.nanoTime();
long durationArrayList = (endTimeArrayList - startTimeArrayList) / 1_000_000; // milliseconds
System.out.println("ArrayList processing time: " + durationArrayList + " ms");
// --- Measure LinkedList performance ---
long startTimeLinkedList = System.nanoTime();
processList(linkedList);
long endTimeLinkedList = System.nanoTime();
long durationLinkedList = (endTimeLinkedList - startTimeLinkedList) / 1_000_000; // milliseconds
System.out.println("LinkedList processing time: " + durationLinkedList + " ms");
// Based on measurement, if this was a bottleneck,
// you'd know LinkedList is better for THIS specific operation.
// A real profiler would give much more detailed insights into CPU usage,
// memory allocations, and method call times across the entire application.
}
}
Note: For proper microbenchmarking in Java, consider using a framework like JMH (Java Microbenchmark Harness) to avoid common pitfalls (JIT optimizations, dead code elimination, warmup effects etc.).
49. Make sure comments and code agree. #
确保注释和代码一致
Explanation: Outdated comments that no longer reflect what the code does are worse than no comments at all, as they can mislead readers. When you change code, update its comments too.
Example:
-
Mismatched Comment and Code:
public class MismatchedComment { public int calculateValue(int x) { // Comment: Returns x multiplied by 2 // Code was later changed, but comment was not updated: return x + 5; // Actual logic adds 5 } public static void main(String[] args) { MismatchedComment mc = new MismatchedComment(); // Someone reading the comment might expect 10 * 2 = 20 System.out.println("Result: " + mc.calculateValue(10)); // Outputs 15 } } -
Agreed Comment and Code:
public class AgreedComment { public int calculateValue(int x) { // Comment: Returns x plus 5 return x + 5; // Logic adds 5 } // Or if it was meant to be multiplication: // public int calculateValueTimesTwo(int x) { // // Comment: Returns x multiplied by 2 // return x * 2; // } public static void main(String[] args) { AgreedComment ac = new AgreedComment(); System.out.println("Result: " + ac.calculateValue(10)); // Outputs 15, comment is accurate } }
50. Don’t just echo the code with comments - make every comment count. #
不要在注释里仅仅重复代码 - 让每处注释都有价值
Explanation: Comments should explain why the code is doing something, or clarify complex logic. They should not simply restate what the code obviously does.
Example:
-
Redundant/Useless Comment:
public class RedundantComment { public void process(int count) { // Check if count is greater than zero if (count > 0) { // Print the count System.out.println("Count: " + count); } } } -
Valuable Comment (Explaining “Why” or “What if not obvious”):
public class ValuableComment { private static final int MAX_RETRIES = 3; public boolean performActionWithRetries(String actionId) { int attempt = 0; boolean success = false; while (attempt < MAX_RETRIES && !success) { // Why: We retry because the external service can be temporarily unavailable. // The number of retries is limited to avoid indefinite blocking. System.out.println("Attempting action " + actionId + ", try #" + (attempt + 1)); success = tryPerformAction(actionId); // Assume this method does the actual work if (!success) { attempt++; try { // Why: Wait a bit before retrying to give the service time to recover. Thread.sleep(1000 * attempt); // Exponential backoff (simple version) } catch (InterruptedException e) { Thread.currentThread().interrupt(); // Restore interrupt status return false; // Exit if interrupted } } } return success; } private boolean tryPerformAction(String actionId) { // Simulate action that might fail return Math.random() > 0.5; } }
51. Don’t comment bad code - rewrite it. #
不要给糟糕的代码做注释 - 应该重写它
Explanation: If code is so convoluted or poorly written that it needs extensive comments to explain what it does, it’s a sign the code itself should be improved. Refactor it to be self-explanatory. (This is related to principle 16).
Example:
-
Commenting Bad Code (Trying to explain a mess):
public class CommentingBadCode { // This function tries to figure out the discount level. // First, it checks if 'a' is true, which means it's a special customer. // Then, if 'b' (items bought) is more than 10, or 'c' (total amount) is over 100, // they get a high discount. // But if 'a' is false, then we check if 'b' is over 20 for a medium discount. // Otherwise, if 'd' (is anniversary) is true, they also get a medium discount. // It's complicated because of legacy rules. public int getDiscount(boolean a, int b, double c, boolean d) { int disc; if (a) { if (b > 10 || c > 100.0) { disc = 20; // High discount } else { disc = 5; // Low discount for special customer } } else { if (b > 20) { disc = 15; // Medium discount } else { if (d) { disc = 15; // Medium discount for anniversary } else { disc = 0; // No discount } } } return disc; } } -
Rewriting Bad Code (Making it clearer):
public class RewrittenClearCode { private static final int HIGH_DISCOUNT_PERCENT = 20; private static final int MEDIUM_DISCOUNT_PERCENT = 15; private static final int LOW_DISCOUNT_PERCENT = 5; private static final int NO_DISCOUNT_PERCENT = 0; public int calculateDiscountPercentage(boolean isSpecialCustomer, int itemsBought, double totalAmount, boolean isAnniversary) { if (isSpecialCustomer) { if (itemsBought > 10 || totalAmount > 100.0) { return HIGH_DISCOUNT_PERCENT; } else { return LOW_DISCOUNT_PERCENT; } } else { // Not a special customer if (itemsBought > 20 || isAnniversary) { // Simplified condition return MEDIUM_DISCOUNT_PERCENT; } else { return NO_DISCOUNT_PERCENT; } } } public static void main(String[] args) { RewrittenClearCode rcc = new RewrittenClearCode(); System.out.println("Discount: " + rcc.calculateDiscountPercentage(true, 12, 50, false)); // 20 } }The rewritten code is much easier to understand, reducing the need for explanatory comments. Any remaining comments can focus on why certain business rules exist, if necessary.
52. Use variable names that mean something. #
采用有意义的变量名
Explanation: Choose names that clearly describe the entity the variable represents. Avoid cryptic, single-letter (except for trivial loop counters), or overly generic names. (Related to principle 9).
Example:
-
Meaningless Names:
public class MeaninglessNames { public double calculate(double x, int y, double z) { double temp = x * y; // What is temp? if (y > 10) { return temp - z; // z is what? A penalty? A fixed fee? } return temp; } } -
Meaningful Names:
public class MeaningfulNames { public double calculateTotalOrderValue(double itemPrice, int quantity, double discountAmount) { double grossValue = itemPrice * quantity; if (quantity > 10) { // e.g., bulk order qualifies for using the discount return grossValue - discountAmount; } return grossValue; // No discount applied for smaller quantities } public static void main(String[] args) { MeaningfulNames mn = new MeaningfulNames(); System.out.println("Total: " + mn.calculateTotalOrderValue(10.0, 12, 5.0)); // 120 - 5 = 115 System.out.println("Total: " + mn.calculateTotalOrderValue(10.0, 5, 5.0)); // 50 } }
53. Use statement labels that mean something. #
使用有意义的语句标签
Explanation: In Java, labels are used with break or continue to target a specific outer loop. If you must use them (which should be rare), give them names that indicate the loop’s purpose.
Example:
-
Meaningless Label:
public class MeaninglessLabel { public void findValue(int[][] matrix, int target) { outer: // What does 'outer' signify? for (int i = 0; i < matrix.length; i++) { for (int j = 0; j < matrix[i].length; j++) { if (matrix[i][j] == target) { System.out.println("Found at [" + i + "][" + j + "]"); break outer; // Exits the 'outer' labeled loop } } } } } -
Meaningful Label:
public class MeaningfulLabel { public void searchMatrixForValue(int[][] matrix, int targetValue) { matrixSearchLoop: // Clearly indicates this label is for the matrix search for (int rowIndex = 0; rowIndex < matrix.length; rowIndex++) { for (int colIndex = 0; colIndex < matrix[rowIndex].length; colIndex++) { if (matrix[rowIndex][colIndex] == targetValue) { System.out.println("Target " + targetValue + " found at row " + rowIndex + ", column " + colIndex); break matrixSearchLoop; // Exits the loop specifically for matrix searching } } } // If not found, this point is reached. } public static void main(String[] args) { MeaningfulLabel ml = new MeaningfulLabel(); int[][] data = {{1,2,3},{4,5,6},{7,8,9}}; ml.searchMatrixForValue(data, 5); } }Note: Labeled breaks/continues should be used sparingly as they can sometimes make control flow harder to follow. Often, refactoring into smaller methods can eliminate the need for them.
54. Format a program to help the reader understand it. #
格式化程序让阅读代码的人更容易理解
Explanation: Consistent indentation, spacing, and use of blank lines to group related blocks of code significantly improve readability. Most IDEs have auto-formatting tools that enforce a standard style.
Example:
-
Poorly Formatted:
public class PoorlyFormatted{public static void main(String[]args){ int x=10;int y=20; if(x<y){System.out.println("x is less than y"); for(int i=0;i<x;i++){ System.out.print(i+" ");} }else{ System.out.println("x is not less than y");}}} -
Well Formatted:
public class WellFormatted { public static void main(String[] args) { int x = 10; int y = 20; if (x < y) { System.out.println("x is less than y"); for (int i = 0; i < x; i++) { System.out.print(i + " "); // 0 1 2 3 4 5 6 7 8 9 } System.out.println(); // Newline after loop } else { System.out.println("x is not less than y"); } } }Use your IDE’s auto-formatter (e.g., Ctrl+Alt+L in IntelliJ, Ctrl+Shift+F in Eclipse) and agree on a team-wide style guide.
55. Document your data layouts. #
为数据布局撰写文档
Explanation: If you’re working with complex data structures, custom file formats, or database schemas, document their layout, fields, types, relationships, and any constraints. For in-memory structures (classes), Javadoc on fields can serve this purpose.
Example: (Documenting a simple class representing a data record)
/**
* Represents a user record within the system.
* Each user has a unique ID, a username for login, an email address,
* and a status indicating if their account is active.
*/
public class UserRecord {
/**
* Unique identifier for the user. Integer, assigned by the system. Cannot be negative.
* Example: 10234
*/
private int userId;
/**
* Username used for login. String, max 50 characters. Must be unique across all users.
* Alphanumeric characters only.
* Example: "jdoe123"
*/
private String username;
/**
* User's email address. String, standard email format. Used for notifications.
* Example: "john.doe@example.com"
*/
private String email;
/**
* Account status. Boolean, true if active, false if inactive/suspended.
* Defaults to true on creation.
*/
private boolean isActive;
/**
* Constructs a new UserRecord.
* @param userId The unique ID for the user.
* @param username The username for login.
* @param email The user's email address.
* @param isActive The account status.
*/
public UserRecord(int userId, String username, String email, boolean isActive) {
// Add validation as per documented constraints (Principle 19, 20)
if (userId < 0) throw new IllegalArgumentException("User ID cannot be negative.");
if (username == null || username.isEmpty() || username.length() > 50) {
throw new IllegalArgumentException("Username is invalid.");
}
// Basic email validation could go here
this.userId = userId;
this.username = username;
this.email = email;
this.isActive = isActive;
}
// Getters and potentially setters would follow...
public int getUserId() { return userId; }
public String getUsername() { return username; }
public String getEmail() { return email; }
public boolean isActive() { return isActive; }
@Override
public String toString() {
return "UserRecord{" +
"userId=" + userId +
", username='" + username + '\'' +
", email='" + email + '\'' +
", isActive=" + isActive +
'}';
}
public static void main(String[] args) {
UserRecord user = new UserRecord(1, "testUser", "test@example.com", true);
System.out.println(user);
}
}
For external data (like a CSV file format):
// Documentation for users.csv file format:
//
// Each line represents a user.
// Fields are comma-separated.
// String fields containing commas must be enclosed in double quotes.
//
// Columns:
// 1. UserID (Integer): Unique numeric ID. e.g., 101
// 2. Username (String): Login name. e.g., "smithj"
// 3. Email (String): Contact email. e.g., "john.smith@email.com"
// 4. IsActive (Boolean): "true" or "false". e.g., true
// 5. LastLoginDate (String): ISO 8601 format (YYYY-MM-DD). e.g., "2023-10-26"
//
// Example:
// 101,"smithj","john.smith@email.com",true,"2023-10-26"
// 102,"doe_a","anna.doe@email.com",false,"2022-01-15"
56. Don’t over-comment. #
不要过分注释
Explanation: While comments are important, too many comments, especially those stating the obvious (see principle 50) or cluttering simple code, can make the code harder to read. Strive for self-documenting code where variable and method names are clear enough that extensive comments aren’t needed for basic understanding. Comment the “why,” not the “how” if the “how” is obvious.
Example:
-
Over-Commented (Obvious comments):
public class OverCommented { // Method to add two integers public int addNumbers(int num1, int num2) { // Declare a variable to store the sum int sum; // Add num1 and num2 and store in sum sum = num1 + num2; // Return the sum return sum; } public void processItems(java.util.List<String> items) { // Loop through the list of items for (String item : items) { // For each item in the items list // Print the current item to the console System.out.println(item); } } } -
Appropriately Commented (or Self-Documenting Code needing fewer comments):
public class AppropriatelyCommented { /** * Calculates the sum of two integers. * @param num1 The first integer. * @param num2 The second integer. * @return The sum of num1 and num2. */ public int addNumbers(int num1, int num2) { // Method name is clear return num1 + num2; // Obvious logic needs no inline comment } /** * Prints each item from the provided list to the console. * If the list is null, the method does nothing to prevent NullPointerException. * @param items The list of strings to be processed. */ public void printAllItems(java.util.List<String> items) { // Clear method name if (items == null) { // Important to handle null to avoid errors, could be a comment or in Javadoc. return; // Or log a warning } for (String item : items) { System.out.println(item); // Obvious code } } }Focus comments on non-obvious logic, preconditions, postconditions, “why” something is done a certain way, or important algorithmic details. Javadoc for public APIs is generally good practice.