السابقالفهرسالتالي

الفصل 4

التعليمات الشرطية والتعاود

 

4.1 عامل باقي القسمة

يعمل عامل باقي القسمة (modulus operator) على الأعداد الصحيحة (والعبارات التي تعيد قيماً صحيحة) ويعطي باقي قسمة المعامل الأول على المعامل الثاني. في Java، عامل باقي القسمة هو علامة النسبة المئوية، %. بنية التعليمة المستخدمة مع هذا العامل مطابقة تماماً لتلك المستخدمة مع العوامل الأخرى:

int quotient = 7 / 3;
int remainder = 7 % 3;
العامل الأول، عامل القسمة الصحيحة، يعطي الناتج 2. العامل الثاني، باقي القسمة، ينتج 1. بالتالي، فإن 7 تقسيم 3 يساوي 2 والباقي 1.

يتبين لنا أن عامل باقي القسمة مفيد بشكل مدهش. مثلاً، يمكنك استخدامه للتحقق من قواسم عدد ما: إذا كان x % y يساوي الصفر، عندئذ يكون x يقبل القسمة على y، أي أن y أحد قواسم العدد x.

أيضاً، يمكنك استخدام عامل باقي القسمة لاستخراج خانات عدد وفصلها. مثلاً، x % 10 يعطي الخانة اليمنى الأولى من العدد x (في نظام العد العشري). بشكل مشابه، x % 100 سيعطي الخانتين الأخيرتين من العدد x.

4.2 التنفيذ المشروط

حتى نتمكن من كتابة برامج مفيدة، سنحتاج دائماً تقريباً للقدرة على التحقق من شروط معينة وتغيير سلوك البرنامج وفقاً لذلك. تعطينا التعليمات الشرطية (Conditional Statements) هذه القدرة. أبسط شكل لهذ التعليمات هي تعليمة if:

if (x > 0) {
   System.out.println ("x is positive");
}
العبارة بين قوسين تدعى بالشرط. إذا كان محققاً، سيتم تنفيذ التعليمات داخل الأقواس المنحنية. إذا لم يتحقق الشرط، فلا يحدث شيء.

يمكن أن يحتوي الشرط على أي واحد من عوامل المقارنة، أحياناً تدعى العوامل المنطقية (relational operators):

x == y    // x equals y
x != y    // x is not equal to y
x > y     // x is greater than y
x < y     // x is less than y
x >= y    // x is greater than or equal to y
x <= y    // x is less than or equal to y
بالرغم من أن هذه العمليات مألوفة إليك على الأغلب، فإن الطريقة التي تكتب بها هذه العمليات مختلفة قليلاً عن الرموز الرياضية مثل =، ≠ و≥. من الأخطاء الشائعة استعمال علامة = مفردة بدلاً من اثنتين ==. تذكر أن = هو عامل الإسناد، و== هو عامل المقارنة. أيضاً، لا يوجد شيء يكتب هكذا >= أو هكذا <=.

يجب أن يكون طرفي العامل الشرطي من نوع واحد. يمكنك المقارنة فقط بين int وint أو بين double وdouble. لسوء الحظ، لا يمكن استخدام هذه العوامل للمقارنة بين السلاسل المحرفية أبداً! توجد طريقة أخرى للمقارنة بين Strings، لكننا لن نصل إليها قبل فصلين تاليين.

4.3 الإجراء البديل

الشكل الآخر للتنفيذ الشرطي هو الإجراء البديل (alternative execution)، الذي يحتوي على احتمالين، ويحدد الشرط أي منهما سيتم تنفيذه. شكل التعليمة يبدو كما يلي:

if (x%2 == 0) {
   System.out.println ("x is even");
} else {
   System.out.println ("x is odd");
}
إذا كان باقي قسمة x على 2 يساوي الصفر، نعلم عندئذ أن x عدد زوجي، وهذه الشفرة تطبع رسالة نتيجة ذلك الأثر. إذا كان الشرط غير محقق، سيتم تنفيذ تعليمة الطباعة الأخرى. بما أن الشرط لا بد أن يكون إما محققاً وإما غير محقق (true or false)، فسيتم تنفيذ أحد البديلين حتماً.

من جهة أخرى، إذا كنت تفكر بأنك قد تحتاج للتحقق من الأعداد الزوجية والفردية كثيراً، فقد ترغب بجمع هذه الشفرة في عملية، كما يلي:

public static void printParity (int x) {
  if (x%2 == 0) {
      System.out.println ("x is even");
  } else {
      System.out.println ("x is odd");
  }
}
الآن صرت تملك عملية باسم printParity تطبع رسالة مناسبة لأي عدد صحيح تعطيه لها. في main يمكنك استدعاء هذه العملية كما يلي:
printParity (17);
تذكر دائماً أنك لا تحتاج للتصريح عن أنواع المتحولات عند استدعاء عملية ما. Java استنتاج أنواعها. عليك مقاومة جاذبية كتابة أشياء مثل هذه:
int number = 17;
printParity (int number);	// WRONG!!!

4.4 التعليمات الشرطية المترابطة

ستحتاج أحياناً للتحقق من مجموعة من الشروط المترابطة واختيار واحد من عدة أفعال. إحدى الطرق المستعملة لعمل هذا هي ربط (chain) سلسلة من عدة ifs and elses:

if (x > 0) {
    System.out.println ("x is positive");
} else if (x < 0) {
    System.out.println ("x is negative");
} else {
    System.out.println ("x is zero");
}
يمكن أن تكون هذه السلاسل بالطول الذي تريده، إلا أنها سرعان ما تصبح صعبة القراءة إذا خرجت عن السيطرة. إحدى الوسائل المتبعة لجعل هذه السلاسل أسهل للقراءة هي استعمال الترتيب القياسي عند كتابتها، كما هو موضح بهذه الأمثلة. إذا أبقيت جميع التعليمات والأقواس المنحنية مرتبة، فستنخفض احتمالات ارتكابك للأخطاء النحوية وستتمكن من إيجادها أسرع في حال وجودها.

4.5 التعليمات الشرطية المتداخلة

بالإضافة إلى الربط، يمكن أيضاً شبك تعليمة شرطية ضمن تعليمة أخرى. كان بإمكاننا كتابة المثال السابق كما يلي:

if (x == 0) {
    System.out.println ("x is zero");
} else {
    if (x > 0) {
          System.out.println ("x is positive");
    } else {
          System.out.println ("x is negative");
    }
}
الآن يوجد تعليمة شرطية خارجية لها فرعين. الفرع الأول يحوي تعليمة طباعة بسيطة، لكن الفرع الثاني يحتوي على تعليمة شرطية ثانية، والتي تملك فرعين بدورها هي الأخرى. لحسن الحظ، كلا هذين الفرعين يحتوي على تعليمة طباعة، مع أنه يمكن أن يكونا تعليمات شرطية أخرى أيضاً.

لاحظ أيضاً أن الترتيب جعل البنية واضحة، ومع هذا فإن التعليمات الشرطية المتداخلة تصعب قراءتها سريعاً. بشكل عام، ستكون فكرة جيدة أن تتفادى هذه التعليمات المتداخلة بقدر المستطاع.

من جهة أخرى، هذا النوع من البنية المتداخلة (nested structure) شائع، وسنراه ثانية فيما بعد، فمن الأفضل لك إذاً أن تعتاد عليه.

4.6 تعليمة العودة

تسمح لك تعليمة العودة بإنهاء تنفيذ عملية قبل الوصول إلى نهايتها. أحد الأسباب التي نستخدم هذه التعليمة لأجلها هو في حال اكتشاف شرط خاطئ:

public static void printLogarithm (double x) {
   if (x <= 0.0) {
      System.out.println ("Positive numbers only, please.");
      return;
   }

   double result = Math.log (x);
   System.out.println ("The log of x is " + result);
}
في هذا المثال تعرَّف عملية باسم printLogarithm تأخذ معامل واحد من نوع double اسمه x. أول شيء تفعله هو التحقق ما إذا كان x أصغر من أو يساوي الصفر، حيث تطبع رسالة خطأ في هذه الحالة ثم تستعمل تعليمة العودة return للخروج من العملية. يعود مسار التنفيذ مباشرة إلى مستدعي العملية ولا يتم تنفيذ الأسطر الباقية من العملية.

لقد قمت باستخدام قيمة عشرية في الطرف الأيمن من الشرط بسبب وجود متغير عشري في الطرف الأيسر.

4.7 تحويل الأنواع

قد تتساءل عن الطريقة التي استطعنا من خلالها أن نكتب شيئاً مثل "The log of x is " + result، بما أن أحد الطرفين هو String والآخر double. حسناً، في هذه الحالة Java تتذاكى علينا، وتحول النوع من double إلى String تلقائياً قبل أن تربط السلسلتين معاً.

كلما حاولت "جمع" عبارتين معاً، وكانت إحداهما String، ستحول Java العبارة الأخرى إلى String ثم تجري عملية ربط السلاسل (string concatenation). ماذا تعتقد أنه سيحدث لو أنك أجريت عملية بين قيمة صحيحة وقيمة عشرية؟

4.8 التعاود

لقد ذكرت في الفصل السابق أنه يسمح لعملية ما أن تستدعي عملية أخرى، كما رأينا عدة أمثلة على ذلك. لقد أغفلت ذكر مشروعية استدعاء العملية لنفسها أيضاً. إن السبب الذي يجعل من هذه الإمكانية شيئاً جيداً قد لا يكون واضحاً، لكنه واحد من أكثر الأشياء التي تستطيع البرامج عملها سحراً وإثارة.

مثلاً، انظر إلى العملية التالية:

public static void countdown (int n) {
  if (n == 0)
    System.out.println ("Blastoff!");
  } else {
    System.out.println (n);
    countdown (n-1);
  }
}
اسم العملية هو countdown وهي تأخذ عدد صحيح واحد كمعامل. إذا كان المعامل صفراً، فستطبع الكلمة "!Blastoff". وإلا، فستطبع العدد ثم تستدعي عملية اسمها countdown – نفسها– وتمرر لها n-1 كمعامل.

ماذا يحدث لو استدعينا هذه العملية في main، هكذا مثلاً:

countdown (3);

يبدأ تنفيذ countdown مع n=3، وبما أن n ليس صفراً، تطبع العملية القيمة 3، ثم تستدعي نفسها...

  يبدأ تنفيذ countdown مع n=2، وبما أن n ليس صفراً، تطبع العملية القيمة 2، ثم تستدعي نفسها...

    يبدأ تنفيذ countdown مع n=1، وبما أن n ليس صفراً، تطبع العملية القيمة 1، ثم تستدعي نفسها...

      يبدأ تنفيذ countdown مع n=0، وبما أن n صفر، ستطبع العملية الكلمة "!Blastoff" وبعدها ترجع إلى العملية
      السابقة.

    ترجع العملية countdown ذات المتحول n=1.

  ترجع العملية countdown ذات المتحول n=2.

ترجع العملية countdown ذات المتحول n=3.

وبعدها تجد نفسك في main ثانية (يا لها من رحلة). ويكون الخرج النهائي للبرنامج كما يلي:

3
2
1
Blastoff!
كمثال آخر، دعنا ننظر ثانية إلى العمليتين newLine وthreeLine.
public static void newLine () {
  System.out.println ("");
}

public static void threeLine () {
  newLine (); newLine (); newLine ();
}
بالرغم من أنهما تعملان، إلا أنهما لن تكونا مفيدتين فعلاً إذا أردت طباعة سطرين جديدين، أو 106. إن حلاً أفضل سيكون مثل هذا:
public static void nLines (int n) {
  if (n > 0) {
    System.out.println ("");
    nLines (n-1);
  }
}
هذا البرنامج شبيه جداً بالسابق؛ طالما أن n أكبر من الصفر، سيطبع سطراً جديداً واحداً، ثم يستدعي نفسه لطباعة n-1 سطر إضافي. بالتالي، فإن العدد الكلي للأسطر المطبوعة سيكون 1 + (n-1)، وهو عادة ما يساوي إلى n تقريباً.

إن استدعاء العملية لنفسها يدعى بالتعاود (recursion)، ومثل هذه العمليات تدعى تعاودية (recursive).

4.9 المخططات الهرمية للعمليات التعاودية

في الفصل السابق استخدمنا مخططاً هرمياً لتمثيل حالة البرنامج عند استدعاء عملية. نفس النوع من المخططات يمكن أن يسهل تفسير عملية تعاودية.

تذكر أنه في كل مرة يتم فيها استدعاء عملية، تنشئ العملية حالة جديدة لنفسها تحتوي على نسخ جديدة لمتغيرات العملية ومعاملاتها.

الشكل التالي هو مخطط هرمي للعملية countdown، عند استدعائها مع n=3:

يوجد شكل واحد للعملية main وأربع أشكال للعملية countdown، كل واحد منها له قيمة مختلفة للمعامل n. قاع الهرم: العملية countdown ذات n=0 هو الحالة القاعدية. لا تستدعي هذه العملية نفسها مرة أخرى، لذلك فلا يوجد المزيد من الأشكال للعملية countdown.

إن الشكل المقابل للعملية main فارغ لأن main لا تملك أية معاملات أو متغيرات محلية.

تمرين 4.1

ارسم مخططاً هرمياً يظهر حالة البرنامج بعد استدعاء main للعملية nLines مع المعامل n=4، قبيل عودة آخر نسخة مستدعاة من nLines.

4.10 المصطلحات

باقي القسمة: عملية يتم تنفيذها على الأعداد الصحيحة وتعطي باقي قسمة العدد الأول على الثاني. في Java يرمز لهذه العملية برمز النسبة المئوية %.

التعليمة الشرطية: مجموعة من التعليمات التي قد يتم تنفيذها أو لا يتم اعتماداً على شرط ما.

الربط: طريقة لجمع عدة تعليمات شرطية بشكل متعاقب.

التداخل: وضع تعليمة شرطية ضمن فرع واحد أو أكثر من تعليمة شرطية أخرى.

الإحداثي: متغير أو قيمة تحدد موقعاً في نافذة بيانية ثنائية البعد.

البكسل (النقطة الإلكترونية): واحدة قياس الإحداثيات.

إطار مستطيل: طريقة شائعة لتحديد إحداثيات منطقة مستطيلة.

قولبة الأنماط: عملية تحوّل القيم من نوع إلى آخر. في Java تكتب هذه العملية بشكل اسم النوع بين قوسين، مثل (int).

الواجهة: وصف للمعاملات وأنواعها التي تتطلبها عملية ما.

النموذج الأولي: طريقة لوصف واجهة عملية ما باستخدام بنية مشابهة لتعليمات Java.

التعاود: هو استدعاء نفس العملية التي يتم تنفيذها حالياً.

التعاود اللانهائية: هي عملية تستدعي نفسها تعاودياً بدون الوصول إلى حالة أساسية أبداً. النتيجة المعتادة لعملية كهذه هي StackOverflowException.

الحالة القاعدية: شرط يؤدي لعدم استدعاء العملية التعاودية نفسها مرة أخرى.

modulus: An operator that works on integers and yields the remainder when one number is divided by another. In Java it is denoted with a percent sign (%).

conditional: A block of statements that may or may not be executed depending on some condition.

chaining: A way of joining several conditional statements in sequence.

nesting: Putting a conditional statement inside one or both branches of another conditional statement.

coordinate: A variable or value that specifies a location in a two-dimensional graphical window.

pixel: The unit in which coordinates are measured.

bounding box: A common way to specify the coordinates of a rectangular area.

typecast: An operator that converts from one type to another. In Java it appears as a type name in parentheses, like (int).

interface: A description of the parameters required by a method and their types.

prototype: A way of describing the interface to a method using Java-like syntax.

recursion: The process of invoking the same method you are currently executing.

infinite recursion: A method that invokes itself recursively without ever reaching the base case. The usual result is a StackOverflowException.

base case: A condition that causes a recursive method not to make a recursive call.

4.11 تمرينات

تمرين 4.2

هذا التمرين هو مراجعة لمجرى تنفيذ برنامج ذو عمليات متعددة. اقرأ الشفرة التالية وأجب عن الأسئلة أدناها.

public class Buzz {
  public static void baffle (String blimp) {
    System.out.println (blimp);
    zippo ("ping", -5);
  }
  public static void zippo (String quince, int flag) {
    if (flag < 0) {
      System.out.println (quince + " zoop");
    } else {
      System.out.println ("ik");
      baffle (quince);
      System.out.println ("boo-wa-ha-ha");
    }
  }
  public static void main (String[] args) {
    zippo ("rattle", 13);
  }
}
  1. اكتب الرقم 1 بجوار أول تعليمة سيتم تنفيذها من هذا البرنامج. كن حذراً ولا تخلط بين التعليمات والأشياء الأخرى.
  2. اكتب الرقم 2 بجوار التعليمة الثانية في التنفيذ، وتابع حتى ينتهي البرنامج. إذا تم تنفيذ تعليمة أكثر من مرة، فيمكن وضع أكثر من رقم بجوارها.
  3. ما هي قيمة المعامل blimp عندما تم استدعاء baffle؟
  4. ما هو خرج هذا البرنامج؟
  5. تمرين 4.3

    المقطع الأول من أغنية "99 Bottles of Beer" هو:

    99 bottles of beer on the wall, 99 bottles of beer, ya’ take one down,
    ya’ pass it around, 98 bottles of beer on the wall.

    المقاطع التالية مطابقة ما عدا أن عدد الزجاجات يقل بمقدار واحد في كل مقطع، حتى المقطع الأخير:

    No bottles of beer on the wall, no bottles of beer, ya’ can’t take one down, ya’
    can’t pass it around, ’cause there are no more bottles of beer on the wall!

    وبعدها تنتهي الأغنية (أخيراً).

    اكتب برنامجاً يطبع الكلمات الكاملة لأغنية "99 Bottles of Beer". يجب أن يحتوي برنامجك على عملية تعاودية تنفذ الجزء الصعب، لكنك قد ترغب أيضاً بكتابة عملية إضافية لفصل العمل الكلي للبرنامج.

    أثناء تطوير الشفرة، قد ترغب باختبارها مع عدد أقل من المقاطع، مثل "3 Bottles of Beer".

    الغرض من هذا التمرين هو أخذ مشكلة وفصلها إلى مشاكل أصغر، وحل المشاكل الصغيرة بكتابة عمليات بسيطة، سهلة التنقيح.

    تمرين 4.4

    ما هو خرج البرنامج التالي؟

    public class Narf {
      public static void zoop (String fred, int bob) {
        System.out.println (fred);
        if (bob == 5) {
          ping ("not ");
        } else {
          System.out.println ("!");
        }
      }
      public static void main (String[] args) {
        int bizz = 5;
        int buzz = 2;
        zoop ("just for", bizz);
        clink (2*buzz);
      }
      public static void clink (int fork) {
        System.out.print ("It’s ");
        zoop ("breakfast ", fork) ;
      }
      public static void ping (String strangStrung) {
        System.out.println ("any " + strangStrung + "more ");
      }
    }
    

    تمرين 4.5

    آخر نظرية لفيرما (Fermat's Last Theorem) تقول بأنه لا يوجد أعداد صحيحة a، b، وc بحيث تحقق

    an + bn = cn
    ما عدا الحالة التي يكون فيها n=2.

    اكتب عملية اسمها checkFermat تأخذ أربعة أعداد صحيحة كمعاملات —a, b, c and n— وتتحقق ما إذا كانت نظرية فيرما صحيحة. إذا كان n أكبر من 2 وتبين أن an + bn = cn، يجب أن يطبع البرنامج "Holy smokes, Fermat was wrong!" وبخلاف ذلك يجب أن يطبع البرنامج "No, that doesn't work". عليك أن تفترض وجود عملية اسمها raiseToPow تأخذ عددين صحيحين كمتحولات وترفع العدد الأول إلى قوة العدد الثاني. مثلاً

    int x = raiseToPow (2, 3);
    
    ستسند هذه التعليمة القيمة 8 إلى x، لأن 23 = 8.

    السابقالفهرسالتالي