الفصل 5: تصحيح الأخطاء ومراجعة الكود
تحاكي هذه التمارين أسئلة المقابلات حيث يتم تقديم كود به أخطاء أو سيء الكتابة ويُطلب منك العثور على المشاكل وتصحيحها. إنه تنسيق شائع جدًا لدى الشركات الكبرى (على غرار Sonar/Codility).
التمرين 1 — العثور على 4 أخطاء وتصحيحها
"هذا الكود يحتوي على 4 أخطاء. اعثر عليها وصححها."
الكود الذي يحتوي على أخطاء
public class DiscountCalculator {
public double calculateDiscount(List<Product> products, String coupon) {
double total = 0;
for (int i = 0; i <= products.size(); i++) { // BUG 1
Product p = products.get(i);
if (p.getPrice() > 0) {
total =+ p.getPrice(); // BUG 2
}
}
double discount = 0;
if (coupon == "SAVE20") { // BUG 3
discount = total * 0.20;
}
return total - discount; // BUG 4: pas de null check
}
}
الكود المصحح
public class DiscountCalculator {
public double calculateDiscount(List<Product> products, String coupon) {
if (products == null || products.isEmpty()) return 0; // FIX 4
double total = 0;
for (int i = 0; i < products.size(); i++) { // FIX 1: < au lieu de <=
Product p = products.get(i);
if (p.getPrice() > 0) {
total += p.getPrice(); // FIX 2: += au lieu de =+
}
}
double discount = 0;
if ("SAVE20".equals(coupon)) { // FIX 3: .equals() au lieu de ==
discount = total * 0.20;
}
return total - discount;
}
}
شرح الأخطاء
| الخطأ | المشكلة | النتيجة | التصحيح |
|---|---|---|---|
| 1 | <= بدلاً من < | ArrayIndexOutOfBoundsException | التغيير إلى < |
| 2 | =+ بدلاً من += | =+ تعني تعيين +القيمة (موجب)، لا تجمع | التغيير إلى += |
| 3 | == يقارن المراجع | يمكن أن يكون لسلسلتين متطابقتين مراجع مختلفة | استخدام .equals() |
| 4 | لا يوجد تحقق من null | NullPointerException إذا كانت products تساوي null | إضافة شرط حماية |
=+مقابل+=: أحدهما يعين، والآخر يجمع. دقيق جدًا بصريًا.==مقابل.equals(): في Java،==يقارن عناوين الذاكرة، وليس المحتوى.- خطأ off-by-one (
<=مقابل<) : الخطأ الأكثر شيوعًا في البرمجة.
"I found four bugs: off-by-one error in the loop, assignment instead of addition, reference comparison instead of .equals(), and missing null safety. These are classic Java pitfalls."
التمرين 2 — مراجعة كود كاملة
"قم بمراجعة هذا الكود. ماذا ستُحسن؟"
الكود المراد مراجعته
public class DataProcessor {
public static ArrayList<String> process(ArrayList<String> data) {
ArrayList<String> result = new ArrayList<>();
for (int i = 0; i < data.size(); i++) {
String s = data.get(i);
if (s != null) {
if (s.length() > 0) {
if (!s.equals("SKIP")) {
result.add(s.toUpperCase());
}
}
}
}
return result;
}
}
الكود المحسن
public class DataProcessor {
public List<String> process(List<String> data) {
if (data == null) return List.of();
return data.stream()
.filter(Objects::nonNull)
.filter(s -> !s.isEmpty())
.filter(s -> !"SKIP".equals(s))
.map(String::toUpperCase)
.toList();
}
}
جميع المشاكل المحددة
| # | المشكلة | التصحيح | المبدأ |
|---|---|---|---|
| 1 | ArrayList في التوقيع | استخدام List (واجهة) | البرمجة ضد الواجهة |
| 2 | طريقة static | إزالة static | إمكانية الاختبار (mocking) |
| 3 | لا يوجد شرط حماية لـ null | إضافة if (data == null) return List.of() | سلامة null |
| 4 | if المتداخلة (3 مستويات) | Stream pipeline | القابلية للقراءة |
| 5 | s.equals("SKIP") | "SKIP".equals(s) | تجنب NPE |
| 6 | s.length() > 0 | s.isEmpty() | كود اصطلاحي |
| 7 | ArrayList قابلة للتغيير معادة | toList() يعيد قائمة غير قابلة للتغيير | عدم القابلية للتغيير |
"I'd refactor to use streams for readability, program to interfaces, and add null safety. This is exactly the kind of code Sonar would flag — nested conditionals and mutable state."
التمرين 3 — العثور على تسرب الذاكرة
"هذا الكود يسبب تسربًا في الذاكرة. اعثر عليه."
الكود الإشكالي
@Service
public class CacheService {
private final Map<String, Object> cache = new HashMap<>();
public void put(String key, Object value) {
cache.put(key, value);
}
public Object get(String key) {
return cache.get(key);
}
// Pas de méthode pour vider le cache !
// Le Map grandit indéfiniment → OutOfMemoryError
}
الكود المصحح
@Service
public class CacheService {
private final Map<String, Object> cache = new LinkedHashMap<>() {
@Override
protected boolean removeEldestEntry(Map.Entry<String, Object> eldest) {
return size() > 1000; // Limite à 1000 entrées
}
};
// Ou mieux, utiliser Caffeine/Redis
}
الأفضل: استخدام Spring Cache
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product findById(Long id) {
return productRepository.findById(id).orElseThrow();
}
@CacheEvict(value = "products", key = "#id")
public void delete(Long id) {
productRepository.deleteById(id);
}
}
"A HashMap cache without eviction policy is a memory leak waiting to happen. I'd use Spring Cache with Caffeine or Redis, which handle TTL and max size automatically."
التمرين 4 — العثور على مشكلة التزامن
"هل هذا الكود آمن للخيوط؟ اعثر على المشكلة."
الكود الإشكالي
@Service
public class CounterService {
private int count = 0;
public void increment() {
count++; // PAS thread-safe !
}
public int getCount() {
return count;
}
}
المشكلة: count++ ليست ذرية. في الواقع، هي 3 عمليات:
- قراءة
count - إضافة 1
- كتابة القيمة الجديدة
إذا قام خيطان بتنفيذ count++ في نفس الوقت، يمكن فقدان زيادة.
الحلول
// Solution 1 : AtomicInteger
@Service
public class CounterService {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
// Solution 2 : synchronized
@Service
public class CounterService {
private int count = 0;
public synchronized void increment() {
count++;
}
public synchronized int getCount() {
return count;
}
}
"count++ is not atomic — it's read-modify-write. I'd use AtomicInteger for lock-free thread safety, or synchronized if I need to protect a larger critical section."
التمرين 5 — تصحيح إدارة الاستثناءات السيئة
"ما الخطأ في إدارة الاستثناءات هذه؟"
الكود السيء
@Service
public class PaymentService {
public void processPayment(PaymentRequest req) {
try {
gateway.charge(req.getAmount());
orderService.updateStatus(req.getOrderId(), "PAID");
} catch (Exception e) {
// PROBLÈME 1 : catch trop large
// PROBLÈME 2 : avale l'exception silencieusement
// PROBLÈME 3 : log inutile
System.out.println("Error: " + e.getMessage());
}
}
}
الكود المصحح
@Service
@Slf4j
public class PaymentService {
@Transactional
public void processPayment(PaymentRequest req) {
try {
gateway.charge(req.getAmount());
} catch (PaymentDeclinedException e) {
log.warn("Payment declined for order {}: {}", req.getOrderId(), e.getMessage());
throw new BusinessException("Payment was declined", e);
} catch (GatewayTimeoutException e) {
log.error("Gateway timeout for order {}", req.getOrderId(), e);
throw new ServiceUnavailableException("Payment gateway is down", e);
}
orderService.updateStatus(req.getOrderId(), "PAID");
}
}
المشاكل المحددة
| # | المشكلة | لماذا هي خطيرة |
|---|---|---|
| 1 | catch (Exception e) | يلتقط كل شيء، بما في ذلك NullPointerException, OutOfMemoryError |
| 2 | استثناء مبلوع | المتصل لا يعرف أبدًا أن الدفع قد فشل |
| 3 | System.out.println | لا يوجد سجل منظم، لا يوجد تتبع للمكدس، مفقود في الإنتاج |
| 4 | لا يوجد تراجع | إذا نجح الشحن ولكن فشل updateStatus، بيانات غير متسقة |
"Never catch generic Exception. Catch specific exceptions, log with proper severity (warn vs error), re-throw as business exceptions, and use @Transactional for data consistency."
التمرين 6 — تحديد الروائح في الكود
"اذكر جميع المشاكل في هذا الكود."
الكود المراد تحليله
public class Utils {
public static String x(String s) {
String r = "";
for (int i = s.length() - 1; i >= 0; i--) {
r = r + s.charAt(i);
}
return r;
}
public static boolean check(String email) {
if (email.contains("@") == true) {
if (email.contains(".") == true) {
return true;
} else {
return false;
}
} else {
return false;
}
}
public static int calc(int a, int b, String op) {
if (op == "+") return a + b;
if (op == "-") return a - b;
if (op == "*") return a * b;
if (op == "/") return a / b;
return 0;
}
}
المشاكل
| الطريقة | المشاكل |
|---|---|
x() | اسم غير مفهوم، دمج في حلقة (O(n²))، لا يوجد تحقق من null |
check() | == true زائدة، if/else return true/false يمكن استبدالها بتعبير |
calc() | == بدلاً من .equals(), قسمة على الصفر، تعيد 0 بصمت |
الكود المصحح
public class StringUtils {
public static String reverse(String input) {
if (input == null) return null;
return new StringBuilder(input).reverse().toString();
}
}
public class EmailValidator {
public static boolean isValid(String email) {
return email != null && email.contains("@") && email.contains(".");
}
}
public class Calculator {
public static int calculate(int a, int b, String operator) {
return switch (operator) {
case "+" -> a + b;
case "-" -> a - b;
case "*" -> a * b;
case "/" -> {
if (b == 0) throw new ArithmeticException("Division by zero");
yield a / b;
}
default -> throw new IllegalArgumentException("Unknown operator: " + operator);
};
}
}
قائمة مراجعة الكود
عندما يُطلب منك "مراجعة" الكود في مقابلة، تحقق بشكل منهجي من:
| الفئة | النقاط التي يجب التحقق منها |
|---|---|
| سلامة null | هل هناك تحقق من null عند الإدخال؟ هل تم استخدام Optional؟ |
| التسمية | هل الأسماء واضحة؟ هل تتبع اتفاقيات Java (camelCase)؟ |
| الاستثناءات | هل هناك catch محدد؟ هل لا يوجد استثناء مبلوع؟ |
| السلاسل | هل تم استخدام .equals() وليس ==؟ هل تم استخدام StringBuilder للدمج في حلقة؟ |
| المجموعات | هل تم استخدام الواجهات (List وليس ArrayList)? هل هي غير قابلة للتغيير عند الإمكان؟ |
| SOLID | هل هناك مسؤولية واحدة؟ هل تعتمد على التجريدات؟ |
| سلامة الخيوط | هل المتغيرات المشتركة محمية؟ |
| الأداء | لا يوجد N+1؟ لا توجد حلقة O(n²) مخفية؟ |
| الأمان | لا يوجد حقن SQL؟ هل تم التحقق من صحة المدخلات؟ |