StringBuilder مقابل StringBuffer: ماذا تختار ولماذا هذا مهم؟
\n\nإذا كنت قد كتبت يومًا كودًا بلغة Java يعمل بكثافة مع السلاسل النصية (مثل تجميع استعلام SQL، أو إنشاء JSON، أو مجرد لصق عشرات القطع النصية)، فمن المحتمل أنك واجهت معضلة: StringBuilder أم StringBuffer؟ على Stack Overflow، حصد هذا السؤال مئات الآلاف من المشاهدات، وليس بدون سبب. للوهلة الأولى، تبدو الفئتان متشابهتين كتوأمين - كلاهما يوسع AbstractStringBuilder، وكلاهما يستخدم مصفوفة أحرف قابلة للتغيير، وكلاهما يدعم append() و insert() و delete(). لكن الشيطان، كما هو الحال دائمًا، يكمن في التفاصيل.
في هذه المقالة، لن نكتفي بسرد الاختلافات. سنقوم بتحليلها بأمثلة واقعية، ونلقي نظرة تحت غطاء JVM، ونجيب على الأسئلة التي سيُطرح عليك بالتأكيد في مقابلات العمل. هيا بنا!
\n\n1. الفرق الرئيسي: المزامنة
\n\nهذه هي النقطة الأهم، والتي من أجلها، في الواقع، تم إثارة كل هذه الضجة. StringBuffer آمن للاستخدام في بيئة متعددة الخيوط (thread-safe). جميع طرقه العامة تقريبًا متزامنة باستخدام الكلمة المفتاحية synchronized. StringBuilder غير متزامن. هذا كل شيء.
دعنا نلقي نظرة على الكود (نسخة مبسطة من JDK الفعلي):
\n\n// StringBuffer (Java 17)\npublic synchronized StringBuffer append(String str) {\n toStringCache = null;\n super.append(str);\n return this;\n}\n\n// StringBuilder\npublic StringBuilder append(String str) {\n super.append(str);\n return this;\n}\n\n\nسطر واحد synchronized - وهذا هو كل الفرق. لكن هذا السطر يغير كل شيء: الأداء، مجال الاستخدام، وحتى بنية التطبيق.
متى تستخدم StringBuffer؟
\n- \n
- عند كتابة تطبيق متعدد الخيوط. \n
- عند استخدام نفس النسخة من
StringBufferمن عدة خيوط بدون مزامنة خارجية. \n - مثال: مسجل (logger) يجمع الرسائل من خيوط مختلفة في مخزن مؤقت واحد. \n
متى تستخدم StringBuilder؟
\n- \n
- الكود أحادي الخيط (90% من الحالات). \n
- المتغيرات المحلية داخل الدالة. \n
- الحلقات التكرارية مع دمج السلاسل (المترجم سيقترح StringBuilder بنفسه). \n
2. الأداء: من الأسرع؟
\n\nكل شيء متوقع هنا: StringBuilder أسرع من StringBuffer. بكم؟ يعتمد على الحمل، لكن في المتوسط من 2 إلى 5 مرات. دعنا نتحقق من ذلك باختبار أداء بسيط.
\n\npublic class PerformanceTest {\n private static final int ITERATIONS = 10_000_000;\n\n public static void main(String[] args) {\n // StringBuilder\n long start = System.nanoTime();\n StringBuilder sb = new StringBuilder();\n for (int i = 0; i < ITERATIONS; i++) {\n sb.append("a");\n }\n long sbTime = System.nanoTime() - start;\n\n // StringBuffer\n start = System.nanoTime();\n StringBuffer sbf = new StringBuffer();\n for (int i = 0; i < ITERATIONS; i++) {\n sbf.append("a");\n }\n long sbfTime = System.nanoTime() - start;\n\n System.out.println("StringBuilder: " + sbTime / 1_000_000 + " ms");\n System.out.println("StringBuffer: " + sbfTime / 1_000_000 + " ms");\n System.out.println("Buffer أبطأ بمقدار " + (sbfTime / (double) sbTime) + " مرة");\n }\n}\n\n\nعلى جهاز الكمبيوتر المحمول الخاص بي (Intel i7، 16GB RAM، Java 17)، النتيجة ثابتة: StringBuilder ~180 مللي ثانية، StringBuffer ~620 مللي ثانية. الفرق يقارب 3.5 مرات. وإذا أضفنا عملاً حقيقيًا (تحليل، تنسيق)، قد يزداد الفارق. لماذا؟ لأن كل استدعاء لـ synchronized هو فحص للشاشة (monitor)، وأخذ قفل (lock) وتحريره. في الكود أحادي الخيط، هذا مضيعة للوقت الخالص.