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

الفصل 9

الكائنات القابلة للتعديل

 

9.1 Points وRectangles

بالرغم من أن السلاسل المحرفية كائنات، إلا أنها ليست مثيرة للاهتمام حقاً، وذلك لأنها

في هذا الفصل، سنستخدم نوعين جديدين من الكائنات الذين يشكلان جزءاً من لغة Java، Point (نقطة) وRectangle (مستطيل). من البداية، أود توضيح أن هذه النقط والمستطيلات ليست كائنات رسومية تظهر على الشاشة. بل هي متغيرات تحتوي على معطيات، مثل المتغيرات من نوع int وdouble تماماً. ومثل المتغيرات الأخرى، يتم استخدام هذه الكائنات داخلياً لإنجاز الحسابات.

9.2 الحزم

إن الأصناف المبنية مسبقاً في Java مقسمة إلى عدد من الحزم (packages)، من بينها java.lang، التي تحتوي على جميع الأصناف التي شاهدناها حتى الآن تقريباً، وjava.awt، التي تحتوي على الأصناف الخاصة بأدوات النوافذ المجردة (AWT) (Java Abstract Window Toolkit)، التي تحتوي على أصناف النوافذ، والأزرار، الرسومات، الخ.

لاستعمال حزمة ما، عليك استيرادها (import). إن الصنفين Point وRectangle موجودين في حزمة java.awt، لذلك سيتوجب علينا استيرادها، كما يلي:

import java.awt.Point;
import java.awt.Rectangle;
كافة تعليمات الاستيراد تظهر في بداية البرنامج، خارج تعريف الصنف.

يتم استيراد الأصناف الموجودة في java.lang، مثل Math وString، تلقائياً. ولذلك لم تكن هناك حاجة لاستخدام تعليمة الاستيراد import حتى الآن.

9.3 كائنات Point

النقطة هي رقمين (إحداثيين) نعاملهما معاً على أنهما كائن واحد. في التدوين الرياضي، تتم كتابة النقط عادة بين قوسين، مع فاصلة واحدة تفصل الإحداثيين. مثلاً، (0,0) تعبر عن مبدأ الإحداثيات، و(x,y) تعبر عن النقطة الموجودة على بعد x وحدة قياس إلى يمين المبدأ وy وحدة قياس فوقه.

في Java، يتم تمثيل نقطة بكائن Point. لإنشاء نقطة جديدة، عليك استعمال أمر new:

Point blank;
blank = new Point (3, 4);
السطر الأول هو تصريح عادي عن متحول: اسمه blank وهو من نوع Point. أما السطر الثاني فيبدو غريباً نوعاً ما: يستدعي السطر الثاني الأمر new، ويحدد نوع الكائن الجديد، ويعطيه المتحولات. تلك المتحولات هي إحداثيات النقطة الجديدة، (4 ,3).

إن ناتج الأمر new هو مرجع (reference) إلى النقطة الجديدة، إذن فالمتغير blank يحتوي على مرجع إلى كائن حديث الولادة. توجد طريقة قياسية للتعبير عن تعليمة الإسناد هذه بشكل بياني، مبينة في الشكل.

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

يمثل الصندوق الكبير الكائن المنشأ حديثاً والقيمتين الموجودتين داخله. x وy هما اسمي متغيرات الحالة (instance variables).

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

9.4 متغيرات الحالة

إن أجزاء البيانات التي تشكل الكائن تدعى بمتغيرات الحالة لأن كل كائن، والذي يشكل حالة (instance) من نوعه، يملك نسخته الخاصة من المتغيرات.

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

بشكل مشابه، إذا أردت قراءة قيمة من متغير حالة، عليك تحديد الكائن الذي ترغب بالحصول على القيمة منه. يتم عمل ذلك في Java باستخدام "النقطة – dot notation".

int x = blank.x;
تعني العبارة blank.x "اذهب إلى الكائن الذي يشير إليه المتغير blank، واحصل على القيمة المخزنة في x". في هذه الحالة قمنا بإسناد تلك القيمة إلى متغير محلي اسمه x. لاحظ عدم وجود تعارض بين المتغير المحلي x وبين متغير الحالة x. إن الغرض من كتابة النقطة هو تعريف المتغير (متغير الحالة) الذي تشير إليه بشكل واضح.

يمكنك استعمال النقطة في أي جزء من العبارات في Java، لذا فإن ما يلي مشروع.

System.out.println (blank.x + ", " + blank.y);
int distance = blank.x * blank.x + blank.y * blank.y;
يطبع السطر الأول 4 ,3؛ ويحسب السطر الثاني القيمة 25.

9.5 استخدام الكائنات كمعاملات

يمكنك تمرير الكائنات كمعاملات باستخدام الطريقة المعتادة. مثلاً

public static void printPoint (Point p) {
   System.out.println ("(" + p.x + ", " + p.y + ")");
}
هي عملية تأخذ نقطة كمتحول وتطبعها بالصيغة القياسية. إذا استدعيت printPoint (blank)، فستطبع (4 ,3). في الحقيقة، توجد عملية جاهزة في Java لطباعة النقط. إذا استدعيت System.out.println (blank)، فستحصل على
java.awt.Point[x=3,y=4]
هذا هو التنسيق القياسي الذي تستخدمه Java لطباعة الكائنات. تطبع اسم نوع الكائن، متبوعاً بمحتوياته، بما في ذلك أسماء متغيرات الحالة وقيمها.

كمثال آخر، يمكننا إعادة كتابة العملية distance من القسم 6.2 بحيث تأخذ نقطتين كمعاملات بدلاً من أربع أعداد عشرية.

public static double distance (Point p1, Point p2) {
  double dx = (double)(p2.x - p1.x);
  double dy = (double)(p2.y - p1.y);
  return Math.sqrt (dx*dx + dy*dy);
}
إن قولبة الأنماط ليست ضرورية هنا؛ لقد وضعتها لأذكرك فقط بأن متغيرات الحالة في Point هي أعداد صحيحة.

9.6 المستطيلات

تشبه المستطيلات النقط، عدا أنها تملك أربعة متغيرات حالة، أسماؤها x, y, width, height. فيما عدا ذلك، فكل شيء آخر هو نفسه تقريباً.

المثال التالي ينشئ كائناً جديداً من نوع Rectangle ويجعل المتغير box يشير إليه.

Rectangle box = new Rectangle (0, 0, 100, 200);
يبين هذا الشكل تأثير تعليمة الإسناد هذه.

إذا طبعت box، ستحصل على

java.awt.Rectangle[x=0,y=0,width=100,height=200]
ثانيةً، هذه هي نتيجة العملية الجاهزة في Java التي تعرف كيفية طباعة كائنات Rectangle.

9.7 استخدام الكائنات كنوع إرجاع

يمكنك كتابة عملية ترجع كائناً. مثلاً، findCenter تأخذ مستطيلاً كمتحول وترجع نقطة تحتوي على إحداثيات مركز المستطيل:

public static Point findCenter (Rectangle box) {
  int x = box.x + box.width/2;
  int y = box.y + box.height/2;
  return new Point (x, y);
}
لاحظ أنك تستطيع استعمال new لإنشاء كائن جديد، واستخدام النتيجة مباشرة كقيمة معادة.

9.8 الكائنات قابلة للتغيير

يمكنك تغيير محتويات كائن بعمل إسناد لأحد متغيرات الحالة الخاصة به. مثلاً، "لتحريك" مستطيل بدون تغيير حجمه، يمكنك تغيير قيم x وy:

box.x = box.x + 50;
box.y = box.y + 100;
تظهر النتيجة في الشكل:

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

public static void moveRect (Rectangle box, int dx, int dy) {
  box.x = box.x + dx;
  box.y = box.y + dy;
}
يشير المتغيران dx وdy إلى المسافة المطلوب تحريك المستطيل إليها في كل اتجاه. إن استدعاء هذه العملية يغير المستطيل المرر إليها كمتحول.
Rectangle box = new Rectangle (0, 0, 100, 200);
moveRect (box, 50, 100);
System.out.println (box);
تطبع java.awt.Rectangle[x=50,y=100,width=100,height=200].

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

توفر Java عمليات تشتغل على النقاط (كائنات Points) والمستطيلات (كائنات Rectangle). يمكنك قراءة الوثائق على http://download.oracle.com/javase/6/docs/api/java/awt/Rectangle.html

مثلاً، translate، التي تنفذ نفس وظيفة moveRect تماماً، لكن بدلاً من تمرير المستطيل كمتحول، نستخدم النقطة:

box.translate (50, 100);
النتيجة هي نفسها تماماً.

9.9 تعدد الأسماء

تذكر أنك عندما تسند شيئاً إلى متغير كائني، فأنت تسند مرجعاً إلى كائن. من الممكن وجود عدة متغيرات تشير إلى الكائن نفسه. مثلاً، هذه الشفرة:

Rectangle box1 = new Rectangle (0, 0, 100, 200);
Rectangle box2 = box1;
تولد مخطط حالة يبدو كهذا:

كلا المتغيرين box1 وbox2 يشيران إلى الكائن نفسه. بكلمات أخرى، هذا الكائن له اسمين، box1 وbox2. عندما يستخدم شخص ما اسمين، يدعى ذلك aliasing. نفس الشيء مع الكائنات.

عندما يشير متغيران إلى كائن واحد، فإن أي تغييرات تؤثر على أحدهما ستؤثر على الآخر أيضاً. مثلاً:

System.out.println (box2.width);
box1.grow (50, 50);
System.out.println (box2.width);
يطبع السطر الأول القيمة 100، وهي عرض المستطيل المشار إليه بالمتغير box2. يستدعي السطر الثاني عملية grow على box1، التي توسع المستطيل بمقدار 50 بكسل في كل جهة (انظر في الوثائق للمزيد من التفاصيل). تم تمثيل أثر ذلك في الشكل:

يجب أن يتضح من الشكل، أن أية تعديلات تجري على box1 ستطبق على box2 أيضاً. بالتالي، ستكون القيمة المطبوعة في السطر الثالث 200، عرض المستطيل بعد التوسيع. ( من جهة أخرى، من المشروع تماماً أن تكون إحداثيات المستطيل سالبة).

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

9.10 العدم

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

إن التصريح Point blank; مماثل لهذه التهيئة

Point blank = null;
يوضح مخطط الحالة التالي أثر هذه التعليمة:

تمثل القيمة null بمربع صغير بدون سهم.

إذا حاولت استعمال كائن معدوم (null object)، سواء بمحاولة الوصول إلى متغير حالة أو استدعاء عملية، فستحصل على NullPointerException. سيطبع النظام رسالة خطأ وينهي البرنامج.

Point blank = null;
int x = blank.x; 			// NullPointerException
blank.translate (50, 50); 		// NullPointerException
من جهة أخرى، من المسموح تمرير كائن معدوم كمتحول أو استقباله كقيمة معادة. في الواقع، من الشائع عمل مثل ذلك، مثلاً للتعبير عن مجموعة خالية أو للإشارة إلى حالة خطأ.

9.11 جمع القمامة

في القسم 9.9 تحدثنا عما يحدث عندما يشير أكثر من متغير إلى نفس الكائن. ماذا يحدث عندما لا يشير أي متغير إلى الكائن؟ مثلاً:

Point blank = new Point (3, 4);
blank = null;
ينشئ السطر الأول كائناً جديداً من الصنف Point ويجعل المتغير blank يشير إليه. يعدل السطر الثاني المتغير blank فبدلاً من الإشارة إلى الكائن، لا يشير إلى شيء (الكائن المعدوم).

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

لا توجد حاجة لعمل أي شيء حتى تجعل عملية جمع القمامة تبدأ، وبشكل عام لن تشعر بتلك العملية أبداً.

9.12 الأنواع الكائنية والأنواع البسيطة

يوجد نمطين للأنواع في Java، الأنواع البسيطة والأنواع الكائنية. الأنواع البسيطة (أو البدائية - primitive)، مثل int وboolean تبدأ بحروف صغيرة؛ الأنواع الكائنية تبدأ بحروف كبيرة. هذا التمييز مفيد لأنه يذكرنا ببعض الاختلافات بين هذين النمطين:

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

9.13 المصطلحات

حزمة: مجموعة من الأصناف. يتم تنظيم الأصناف الجاهزة في Java ضمن حزم.

AWT: أدوات النوافذ المجردة، أحد أكبر حزم Java وأكثرها استخداماً.

الحالة (instance): مثال عن فئة ما. قطتي هي حالة من فئة "السنوريات". كل كائن يمثل حالة عن صنف معين.

متغير الحالة: أحد عناصر البيانات المسماة التي تكون الكائن. كل كائن (حالة) له نسخة خاصة من متغيرات الحالة المعرفة في صنفه.

مرجع: قيمة تشير إلى كائن. في مخططات الحالة، يمثل المرجع بسهم.

تعدد الأسماء: الحالة التي يشير فيها متغيرين أو أكثر إلى نفس الكائن.

جمع القمامة: عملية البحث عن كائنات لا تملك أي مرجعيات واستعادة المساحة التخزينية التي تشغلها.

الحالة (state): وصف كامل لكافة المتغيرات والكائنات وقيمها، عند نقطة معينة من تنفيذ البرنامج.

مخطط الحالة: صورة لحالة البرنامج موضحة بيانياً.

package: A collection of classes. The built-in Java classes are organized in packages.

AWT: The Abstract Window Toolkit, one of the biggest and most commonly-used Java packages.

instance: An example from a category. My cat is an instance of the category “feline things.” Every object is an instance of some class.

instance variable: One of the named data items that make up an object. Each object (instance) has its own copy of the instance variables for its class.

reference: A value that indicates an object. In a state diagram, a reference appears as an arrow.

aliasing: The condition when two or more variables refer to the same object.

garbage collection: The process of finding objects that have no references and reclaiming their storage space.

state: A complete description of all the variables and objects and their values, at a given point during the execution of a program.

state diagram: A snapshot of the state of a program, shown graphically.

9.14 تمرينات

تمرين 9.1

  1. ارسم مخططاً هرمياً للبرنامج التالي، يبين المتغيرات المحلية ومعاملات العمليتين main وriddle، وبين أية كائنات تشير إليها هذه المتغيرات.
  2. ما هو خرج هذا البرنامج؟
    public static void main (String[] args)
    {
      int x = 5;
      Point blank = new Point (1, 2);
      System.out.println (riddle(x, blank));
      System.out.println (x);
      System.out.println (blank.x);
      System.out.println (blank.y);
    }
    public static int riddle(int x, Point p)
    {
      x = x + 7;
      return x + p.x + p.y;
    }
    

إن الغرض من هذا التمرين هو الـتأكد من فهمك لآلية تمرير الكائنات كمعاملات.

تمرين 9.2

  1. ارسم مخططاً هرمياً للبرنامج التالي، يبين حالة البرنامج قبيل عودة العملية distance. قم بتضمين كافة المتغيرات والمعاملات والكائنات التي تشير إليها هذه المتغيرات.
  2. ما هو خرج هذا البرنامج؟
    public static double distance (Point p1, Point p2) {
      int dx = p1.x - p2.x;
      int dy = p1.y - p2.y;
      return Math.sqrt (dx*dx + dy*dy);
    }
    public static Point findCenter (Rectangle box) {
      int x = box.x + box.width/2;
      int y = box.y + box.height/2;
      return new Point (x, y);
    }
    public static void main (String[] args) {
      Point blank = new Point (5, 8);
      Rectangle rect = new Rectangle (0, 2, 4, 4);
      Point center = findCenter (rect);
      double dist = distance (center, blank);
      System.out.println (dist);
    }
    

تمرين 9.3

العملية grow هي جزء من الصنف الجاهز Rectangle. اقرأ وثائقها على
http://download.oracle.com/javase/6/docs/api/java/awt/Rectangle.html#grow(int,int)
ثم أجب عن الأسئلة

  1. ما هو خرج البرنامج التالي؟
  2. ارسم مخطط حالة يبين حالة البرنامج قبيل انتهاء main. قم بتضمين كافة المتغيرات المحلية والكائنات التي تشير إليها.
  3. عند نهاية main، هل يشير المتغيرين p1 وp2 إلى الكائن نفسه؟ لماذا أو لم لا؟
    public static void printPoint (Point p) {
       System.out.println ("(" + p.x + ", " + p.y + ")");
    }
    public static Point findCenter (Rectangle box) {
       int x = box.x + box.width/2;
       int y = box.y + box.height/2;
       return new Point (x, y);
    }
    public static void main (String[] args) {
       Rectangle box1 = new Rectangle (2, 4, 7, 9);
       Point p1 = findCenter (box1);
       printPoint (p1);
       box1.grow (1, 1);
       Point p2 = findCenter (box1);
       printPoint (p2);
    }
    

تمرين 9.4

على الأغلب أنك مرضت من عملية العاملي الآن، لكننا سنعمل نسخة أخرى منها أيضاً.

  1. أنشئ برنامجاً باسم Big.java وابدأ بكتابة نسخة تكرارية من factorial.
  2. اطبع جدولاً للأعداد الصحيحة من 0 إلى 30 مع عاملي كل منها. عند مكان ما بالقرب من 15، ستلاحظ على الأغلب أن الإجابات لم تعد صحيحة. لم حدث ذلك؟
  3. BigIntegers هي كائنات جاهزة في Java يمكنها أن تمثل أعداداً صحيحة ذات حجم اختياري. لا سقف لها سوى محدودية الذاكرة وسرعة المعالجة. اقرأ وثائق صنف BigIntegers من http://download.oracle.com/javase/6/docs/api/java/math/BigInteger.html
  4. توجد طرق عدة لإنشاء BigInteger جديد، لكنني أنصح بالطريقة التي تستخدم valueOf. الشفرة التالية تحول عدداً صحيحاً إلى BigInteger:
    int x = 17;
    BigInteger big = BigInteger.valueOf (x);
    
    اكتب هذه الشفرة وجرب بعض الحالات البسيطة مثل إنشاء BigInteger وطباعته. لاحظ أن println تعرف كيف تطبع الBigIntegers! لا تنسى إضافة import java.math.BigInteger; إلى بداية برنامجك.
  5. لسوء الحظ، لا يمكننا استخدام العوامل الرياضية المعتادة على الBigIntegers، لأنها ليست نوعاً بسيطاً. بل يجب علينا استخدام عمليات الكائن مثل add. لجمع كائنين BigInteger معاً، عليك استدعاء add على أحدهما وتمرير الآخر كمتحول. مثلاً:
    BigInteger small = BigInteger.valueOf (17);
    BigInteger big = BigInteger.valueOf (1700000000);
    BigInteger total = small.add (big);
    
    جرب بعض العمليات الأخرى، مثل multiply وpow.
  6. حول factorial حتى تستخدم BigIntegers في حساباتها، ثم تعيد BigInteger كنتيجة. يمكنك ترك المعامل كما هو – سيبقى عدداً صحيحاً.
  7. جرب طباعة الجدول ثانية باستخدام تابع العاملي المعدل. هل هو صحيح حتى 30؟ لأي رقم يمكنك حساب العاملي؟ أنا حسبت العاملي لكل الأعداد من 0 حتى 999، لكن جهازي بطيء فعلاً، وقد استغرقت العملية بعض الوقت. آخر رقم، !999، يتألف من 2565 خانة.

تمرين 9.5

العديد من خوارزميات التشفير تعتمد على القدرة على رفع الأعداد الصحيحة الكبيرة إلى قوة صحيحة. إليك عملية تجري خوارزمية سريعة (بشكل معقول) لحساب القوى الصحيحة:

public static int pow (int x, int n) {
	if (n==0) return 1;

	// find x to the n/2 recursively
	int t = pow (x, n/2);

	// if n is even, the result is t squared
	// if n is odd, the result is t squared times x
	if (n%2 == 0) {
		return t*t;
	} else {
		return t*t*x;
	}
}
مشكلة هذه الخوارزمية أنها فعالة فقط طالما أن النتيجة أصغر من 2 مليار. أعد كتابة العملية بحيث تكون النتيجة BigInteger. لكن يجب أن يبقى المعامل integer مع ذلك.

يمكنك استخدام عمليتي add وmultiply الخاصتين بالBigInteger، لكن لا تستعمل عملية pow الجاهزة، حتى لا تفسد المرح.

ملاحظة: إذا كنت مهتماً بالرسوميات، فيمكنك أن تقرأ الملحق A الآن، وأن تحل التمرينات الموجودة فيه.

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