3·1

النصوص وإنشاؤها

حتى الآن طبعنا جملًا جاهزة وخزّنا أرقامًا — لكن أغلب ما نتعامل معه في البرامج أسماء وكلمات وجمل. هذا الدرس يعلّمك النوع الذي يحملها كلها: String.

تخيّل النص سلسلة حروف معلّقة في خيط: "سارة" أو "أهلاً بالعالم". في جافا نكتب النص بين علامتي تنصيص مزدوجتين " "، ونخزّنه في متغير من نوع String — بحرف S كبير دائمًا.

ولأن البرامج نادرًا ما تكتفي بنص واحد، توفّر جافا عملية الالتصاق +: نضع نصًا بجانب نص أو نصًا بجانب رقم فتُلصق القيم ببعضها في نص جديد.

StringsIntro.java
public class StringsIntro {    public static void main(String[] args) {        String name = "سارة";        String greeting = "مرحبًا يا ";        System.out.println(greeting + name);        System.out.println("علامة تنصيص داخل النص: \"جافا\"");        System.out.println("نص + رقم: " + 2024);        System.out.println("بدون أقواس: " + 2 + 3);        System.out.println("مع أقواس: " + (2 + 3));        System.out.println("الطول: " + name.length());        System.out.println("النص الفارغ طوله: " + "".length());    }}

اقرأ المثال سطرًا سطرًا

  • String name = "سارة"; ينشئ متغير نصيًا ويخزّن فيه السلسلة — النوع String والقيمة بين مزدوجتين.
  • greeting + name تلصق النصين: «مرحبًا يا سارة».
  • لإظهار علامة تنصيص داخل النص نسبقها بشرطة مائلة: \" — وإلا ظنّت جافا أن النص انتهى.
  • "نص + رقم: " + 2024 يلصق الرقم كنص — أي رقم بجانب نص يتحول تلقائيًا إلى نص.
  • name.length() تعيد عدد الحروف — تنبيه مهم: مع النصوص تُكتب length() بقوسين، خلافًا للمصفوفات التي تُكتب بلا قوسين.
  • النص الفارغ "" نص مشروع تمامًا — طوله صفر، وسنبني عليه نصوصًا جديدة في الدرس الثالث.
انتبهالالتصاق يتم من اليسار إلى اليمين: "عدد: " + 2 + 3 تعطي عدد: 23 لأن النص يلتصق أولًا بالـ 2 فيصبح نصًا، ثم يلتصق بالـ 3 كنص. إذا أردت الجمع الحسابي ضعه في أقواس: "عدد: " + (2 + 3) فتعطي عدد: 5. شغّل المثال وراقب الفرق.
"Sum: " نص + 2 رقم + 3 رقم ↓ "Sum: 2" + 3 ↓ "Sum: 23" "Sum: " + (2 + 3) ➡ "Sum: 5" 2 + 3 + " items" ➡ "5 items" الأقواس تُحسب أولًا، والأرقام التي تسبق أول نص تُجمع حسابيًا
التعبير "Sum: " + 2 + 3 يُقرأ من اليسار إلى اليمين — أول زوج هو النص مع 2. الزوج الأول "Sum: " + 2: طرفه الأيسر نص، فالإشارة + تلصق وتعطي "Sum: 2". الآن "Sum: 2" + 3: الطرف الأيسر صار نصًا، فالإشارة + تلصق مجددًا وتعطي "Sum: 23" — القاعدة: + تُقرأ من اليسار، وما إن يصير الطرف الأيسر نصًا تصير + لاصقة. للمقارنة: الأقواس تجمع 2 + 3 أولًا فيصير الناتج "Sum: 5"، وعندما يسبق الرقمان النص يُجمعان حسابيًا فيصير "5 items".

توقّع الناتج قبل التالي

تلميحهذا الدرس مثالي لزر «⏭ تدريجي»: شغّله خطوة خطوة وراقب صناديق المتغيرات وهي تتلصق، ثم جرّب «✎ تعديل» وبدّل اسم سارة باسمك وشغّل من جديد.
اختبر نفسك · +5 نقاط
ما ناتج System.out.println("عدد: " + 2 + 3); ؟
اختبر نفسك ٢ · حتى +5 نقاط
أي سطر يعرّف متغيرًا نصيًا بشكل صحيح؟
اختبر نفسك ٣ · حتى +5 نقاط
ما ناتج System.out.println(1 + 2 + "م"); ؟
3·2

قراءة النص: الحروف والمقاطع والبحث

النص ليس كتلة واحدة — إنه شارع فيه بيوت مرقّمة. هذا الدرس يعطيك مفاتيح البيوت: charAt للوصول لحرف واحد، وindexOf للبحث عن موضع، وsubstring لقصّ مقطع كامل.

لكل حرف في النص فهرس (رقم موضع) يبدأ من 0 وليس من 1 — تمامًا كما بدأنا العد في المصفوفات. فنص "محمد" حرفه الأول charAt(0) وحرفه الأخير charAt(3)، أي length() - 1.

charAt(1) = 'a' charAt(4) 0 1 2 3 4 J a v a 0 1 2 3 4 substring(1, 3) = "av" النهاية لا تُحتسب خطأ: خارج الحدود length() = 4 آخر فهرس = 3
الأرقام فوق الحروف هي الفهارس، والأرقام تحت الحواف هي حدود القص: substring تأخذ من حدّ البداية حتى حدّ النهاية دون الحرف الذي بعده.
توقّف وفكّرلماذا تُرجع substring(0, 3) ثلاثة أحرف؟
اكشف الإجابةلأن الطول = النهاية − البداية = 3 − 0 = 3: تبدأ من الحدّ 0 وتتوقف عند الحدّ 3 دون أخذ الحرف الذي فهرسه 3، فتحصل على الفهارس 0 و1 و2 فقط (مثلًا من "Java" تحصل على "Jav").

وللبحث عن مقطع داخل نص نستخدم indexOf: تعيد موضع أول ظهور، أو -1 إذا لم تجده — والرقم -1 هو إشارة «غير موجود» التي ستميّزها في كل برامجك.

ReadText.java
public class ReadText {    public static void main(String[] args) {        String name = "محمد أحمد";        System.out.println("أول حرف: " + name.charAt(0));        System.out.println("آخر حرف: " + name.charAt(name.length() - 1));        int space = name.indexOf(" ");        System.out.println("موضع المسافة: " + space);        System.out.println("الاسم الأول: " + name.substring(0, space));        System.out.println("بعد المسافة: " + name.substring(space + 1));        System.out.println("هل يحتوي x؟ " + name.indexOf("x"));        String a = "جافا";        String b = "جافا";        System.out.println("بالمحتوى equals: " + a.equals(b));    }}

اقرأ المثال سطرًا سطرًا

  • charAt(0) يعيد الحرف الأول، وcharAt(length() - 1) يعيد الحرف الأخير — الفهرس الأخير دائمًا «الطول ناقص 1».
  • indexOf(" ") وجدت المسافة عند الفهرس 4، وindexOf("x") لم تجدها فعادت بـ -1.
  • substring(0, space) تقصّ من الفهرس 0 حتى ما قبل المسافة — المعامل الثاني غير مشمول في المقطع (يُستبعد).
  • substring(space + 1) بمعامل واحد تقصّ من الموضع المحدد حتى نهاية النص — تعطي «أحمد».
  • a.equals(b) تقارن المحتوى حرفًا حرفًا وعادت بـ true.
انتبه — الفهرس خارج النطاقلو طلبت charAt(الطول) أو substring بمؤشر أكبر من الطول سيوقف البرنامج خطأ واضح: «الفهرس خارج حدود النص». جرّبها بنفسك: عدّل المثال واكتب name.charAt(9) ثم «▶ تشغيل» واقرأ رسالة الخطأ — آخر فهرس مشروع هو length() - 1 دائمًا.
تلميح — لماذا equals وليس == ؟عندما تكتب a == b على نصوص، في جافا الحقيقية تقارن مكانَي التخزين لا المحتوى — فيمكن أن يحمل نصان متطابقان مكانين مختلفين فتقول لك == «غير متساويين» بمحتوى متطابق! لذلك اعتد منذ اليوم الأول على equals للنصوص — إنها القاعدة الصحيحة في كل بيئة جافا، وتعطيك النتيجة المتوقعة هنا أيضًا.
لماذا قد تبدو == سليمة أحيانًا؟عندما تكتب نصين متطابقين مباشرة في الكود (مثل "جافا" في مكانين) قد تعيد جافا استخدام الكائن نفسه لهما، فتعطي == النتيجة true وكأن كل شيء سليم — لكن هذه مصادفة وليست قاعدة. أما النصوص التي تأتي من Scanner أو تُنشأ بـ new String(...) فتكون كائنات مختلفة، وتفشل == معها حتى لو تطابق المحتوى. الخلاصة: استخدم equals دائمًا ولا تعتمد على هذه المصادفة.

المقارنة المتساهلة مع حالة الأحرف

تخيّل طالبًا كتب إجابته "JAVa" بينما النموذج "java"! equals تقارن حرفًا حرفًا بحساسية كاملة لحالة الأحرف، أما equalsIgnoreCase فتتجاهل الفرق بين الكبيرة والصغيرة:

AnswerCheck.java
public class AnswerCheck {    public static void main(String[] args) {        String student = "JAVa";        System.out.println("بالمحتوى الدقيق: " + student.equals("java"));        System.out.println("متجاهلة حالة الأحرف: " + student.equalsIgnoreCase("java"));    }}
اقرأ المخرجاتequals("java") تعطي false لأن الحروف لا تتطابق تمامًا، بينما equalsIgnoreCase("java") تعطي true. استخدم الدقيقة عندما يهمك التطابق الكامل، والمتساهلة عند مقارنة إدخال المستخدم بإجابة نموذج.
اختبر نفسك · +5 نقاط
ما ناتج "محمد".substring(0, 3) ؟
اختبر نفسك ٢ · حتى +5 نقاط
إذا كان String s = "Java"; فما ناتج System.out.println(s.charAt(1)); ؟
اختبر نفسك ٣ · حتى +5 نقاط
أي سطر يقارن محتوى النصين a و b مقارنة صحيحة؟
3·3

تغيير النصوص وبناؤها

هنا مفاجأة جافا الكبرى: النص لا يتغيّر أبدًا بعد إنشائه — كل «تعديل» يبني نصًا جديدًا. سنفهم هذه الفكرة ثم نستخدمها لنبني نصوص جديدة من مقاطع جاهزة.

تخيّل النص صخرة منقوشة لا تُمحى: عندما نكتب s.toUpperCase() لا تتغير الصخرة، بل يُنحت منها نسخة جديدة بحروف كبيرة ونعيدها لمتغير. لهذا السبب نكتب دائمًا String big = s.toUpperCase(); — نلتقط الناتج الجديد في متغير، ولو اكتفينا بكتابة s.toUpperCase(); وحده لضاع النص الجديد ولا أثر له على s.

متغير s String s = "java"; s.toUpperCase(); s = s.toUpperCase(); "java" "java" القديم: لم يعد أحد يشير إليه نسخة جديدة "JAVA" لا أحد يحفظه "JAVA"
المتغير s يحمل سهمًا إلى كائن النص "java" — النص نفسه صخرة لا تتغير. toUpperCase() لا تعدّل "java": تبني نصًا جديدًا "JAVA" وتعيده، لكن لا أحد خزّنه — فبقيت s تشير إلى "java". بالإسناد s = ... انتقل سهم s إلى النص الجديد. النصوص لا تتغير أبدًا؛ الميثود تعيد نصًا جديدًا ونحن نقرر من يحمله.

ماذا تطبع println(s) الآن؟

وأجمل تطبيق على البناء هو توليد نصوص مركّبة من قطع: نلصق الاسم الأول والأخير بنقطة بينهما لنصنع «اسم مستخدم»، ثم نشتق منه صورته الصغيرة والكبيرة — كلها نصوص جديدة لا تمس الأصل.

BuildUser.java
public class BuildUser {    public static void main(String[] args) {        String first = "Sara";        String last = "Ali";        String user = first + "." + last;        System.out.println("الاسم: " + first + " " + last);        System.out.println("اسم المستخدم: " + user.toLowerCase());        System.out.println("الرمز: " + user.toUpperCase() + "-2026");    }}

اقرأ المثال سطرًا سطرًا

  • first + "." + last تلصق القطع الثلاثة في نص واحد جديد: "Sara.Ali" — خزّنّاه في user.
  • user.toLowerCase() تعيد نسخة جديدة كلها صغيرة: "sara.ali" — أما user نفسه فلم يتغير.
  • user.toUpperCase() + "-2026" تجمع تحويلًا آخر مع لصق رقم — الناتج: "SARA.ALI-2026".
  • القاعدة الذهبية: كل ميثود تعيد نصًا جديدًا؛ من أراد الناتج خزّنه في متغير أو اطبعه فورًا.
انتبهالنص لا يتعدّل في مكانه أبدًا — toUpperCase() وغيرها تبني نصًا جديدًا خلف الكواليس. استدعاؤها وحدها دون تخزين أو طباعة يعني أن الناتج يُبنى ثم يضيع فورًا.
تلميحهذا الأسلوب — لصق القطع ثم تحويلها — هو أساس «بطاقة الطالب» في المشروع التجميعي بالوحدة الرابعة. شغّل المثال بزر «⏭ تدريجي» وراقب كيف يبقى user ثابتًا بينما تتولد نسخ جديدة حوله.
اختبر نفسك · +5 نقاط
بعد تنفيذ هذا الكود — ما الذي يُطبع؟
String s = "java";
s.toUpperCase();
System.out.println(s);
اختبر نفسك ٢ · حتى +5 نقاط
ما ناتج هذا الكود؟
String s = "Ali";
String t = s.toUpperCase();
System.out.println(s + t);
اختبر نفسك ٣ · حتى +5 نقاط
أي سطر يبني اسم المستخدم sara.ali بحروف صغيرة من المتغيرين first = "Sara" وlast = "Ali" ؟
★

تحدي: أين الخطأ؟

ثلاثة برامج عن النصوص — أخطاء النصوص لا توقف البرنامج دائمًا: بعضها يعطي نتيجة صامتة خاطئة! اقرأ الكود بتمعّن وتتبّع الناتج سطرًا سطرًا، ثم اضغط على السطر المخالف.

المقطع ١: برنامج ترحيب بسيط لا يعمل من الأساس. أين الخطأ؟

public class Test1 { public static void main(String[] args) { String s = 'أهلا'; System.out.println(s); } }

اقرأ رسالة الخطأ بتمعّن: ماذا قال عن علامتي التنصيص؟ وما نوع القيمة التي تُكتب بين مفردتين في جافا؟

المقطع ٢: نريد آخر حرف من النص لكن البرنامج يتوقف بخطأ. أين الخطأ؟

public class Test2 { public static void main(String[] args) { String s = "جافا"; System.out.println("آخر حرف: " + s.charAt(s.length())); } }

كم عدد الحروف في "جافا"؟ وما أكبر فهرس مشروع إذا بدأ العد من صفر؟ راجع فخ «الفهرس خارج النطاق» في الدرس 3·2.

المقطع ٣: نريد مجموع عددين لكن الناتج «المجموع: 23»! أين الخطأ؟

public class Test3 { public static void main(String[] args) { int a = 2; int b = 3; System.out.println("المجموع: " + a + b); } }

لا خطأ هنا — البرنامج يعمل تمامًا لكن ناتجه خاطئ. تتبّع التنفيذ من اليسار إلى اليمين: متى يتحول التعبير إلى نص؟

لماذا هذا التحدي مهم؟أخطاء النصوص من أخدع الأخطاء: بعضها يوقف البرنامج بصوت عالٍ، وبعضها يعطي نتيجة صامتة خاطئة تمرّ دون أن ينتبه لها أحد. تدريب عينك على التمييز بينهما هو مهارة التصحيح الحقيقية — انسخ هذه المقاطع إلى محرر أي درس وشغّلها وقارن الناتج المتوقع بالفعلي.
✎

تمارين الوحدة ٣

تمارين على التعامل مع النصوص (الإنشاء والالتصاق، قراءة الحروف والمقاطع، وبناء نصوص جديدة بالمقاطع). كل تمرين أمامه كود ناقص: اضغط «✎ تعديل»، اكتب الكود المطلوب مكان التعليقات، اضغط «✔ حفظ» ثم «▶ تشغيل» — وقارن نتيجتك بالهدف المكتوب أسفل كل تمرين.

اسم الطالب مخزّن في المتغير name. أكمل سطرين: السطر الأول يطبع ترحيبًا بالاسم «مرحبًا بك يا ...»، والثاني يطبع عدد أحرف الاسم باستخدام length() بالصيغة «اسمك فيه ... أحرف».

Welcome.java
public class Welcome {    public static void main(String[] args) {        String name = "سارة";        // ١) اطبع: مرحبًا بك يا (الاسم)        // ٢) اطبع: اسمك فيه (الطول) أحرف    }}
الهدف
مرحبًا بك يا سارة
اسمك فيه 4 أحرف
تلميحالتصاق النص بالمتغير يتم بـ + — والنداء على الميثود يُكتب name.length() بقوسين.

الاسم الأول في first والأخير في last. أكمل سطرًا واحدًا يعرّف المتغير full الذي يجمعهما بمسافة بينهما — تذكر أن المسافة نص منفصل تلتصقه بينهما.

FullName.java
public class FullName {    public static void main(String[] args) {        String first = "خالد";        String last = "العمري";        // عرّف full = first + مسافة + last        System.out.println("الاسم الكامل: " + full);    }}
الهدف
الاسم الكامل: خالد العمري
تلميحالمسافة ليست موجودة تلقائيًا — اكتبها صراحةً كنص: " " بين الاسمين.

الاسم الكامل "نورة سعد" مخزّن في full. أكمل المتغيرين الناقصين: space يخزّن موضع المسافة باستخدام indexOf، وfirst يخزّن الاسم الأول بقصّ النص من البداية حتى المسافة بـ substring.

FirstName.java
public class FirstName {    public static void main(String[] args) {        String full = "نورة سعد";        // ١) موضع المسافة في متغير space (int) باستخدام indexOf        // ٢) الاسم الأول في متغير first باستخدام substring        System.out.println("الاسم الأول: " + first);    }}
الهدف
الاسم الأول: نورة
تلميحsubstring(0, space) تقصّ من الفهرس 0 حتى ما قبل المسافة مباشرة — المعامل الثاني يُستبعد من المقطع كما رأيت في الدرس 3·2.
أنهيت تمارين الوحدة؟انتقل إلى الصفحة الرئيسية وتابع تقدمك في بطاقات الوحدات، أو راجع دروس الوحدة الثالثة من القائمة الجانبية.