اذهب إلى المحتوى

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

  1. Wael Aljamal

    Wael Aljamal

    الأعضاء


    • نقاط

      4

    • المساهمات

      6975


  2. Bader almansouer

    Bader almansouer

    الأعضاء


    • نقاط

      4

    • المساهمات

      65


  3. Ali Mezher

    Ali Mezher

    الأعضاء


    • نقاط

      4

    • المساهمات

      40


  4. Zeina Almakdisi

    Zeina Almakdisi

    الأعضاء


    • نقاط

      3

    • المساهمات

      168


المحتوى الأكثر حصولًا على سمعة جيدة

المحتوى الأعلى تقييمًا في 03/06/22 في كل الموقع

  1. مافرق بين فوتشوب و برمجه انا شتغل على برمجه واريد عرف هل برمجه افضل وبرمجه سوق لها وعائد المادي أعلى معنى انا اريد اسهل لي و الايسر
    2 نقاط
  2. أحاول أن أقوم بعرض رسالة تخبر المستخدم بأن يستعمل Google Chrome أو Firefox ، ولكن أريد أن تظهر هذه الرسالة فقط في حالة كان يستعمل Internet Explorer . كيف يمكنني معرفة نوع المتصفح الموجود لدى المستخدم؟ أعرف أن طلب http يحتوي على مثل هذه المعلومات (ترويسة الطلب request header). لكن كيف سأحصل على هذه المعلوماتن في جانغو Django؟
    1 نقطة
  3. السلام عليكم .. لدي موقع وهو عبارة عن متجر توجد به الكثير من المنتجات ، هل يجب ان اخزن هذه المنتجات في قاعدة البيانات ؟ اعلم ان المنتج ربما يتم استخدامه فيما بعد لكن عند تخزيني لهذه المنتجات في قاعدة البيانات ستأخذ مساحة كبيرة بالاضافة الى الصور التي تمثل المنتج.. هل لابأس من تخزيني لها في قاعدة البيانات ؟ شكرا.
    1 نقطة
  4. البرمجه ابي تعلمهت وانا مبتدأ لازم دش من اول دوره وتابعها وطبق سكرج وله ابدي من html يصير تمنى الافاده
    1 نقطة
  5. هل لديكم فيديو أو حتى آرتيكل يوضح من خلاله كيفية اإستعمال المتقدم أو المتعدد لposition (absolute,relative...) في لغة css3.
    1 نقطة
  6. كيف يمكنني دمج الأعمدة التي تم جلبها باستخدام تعليمة select
    1 نقطة
  7. يمكن دمج ناتج استعلام SQL في تعليمة SELECT حسب نوع الحقل، في حال كانت الحقول رقمية، يمكننا استخدام العمليات الحسابية المختلفة مثل + * / - بين الأعمدة SELECT col1 + col2 + col2 FROM table where id = 123134 ^^^^^^^^^^^^^^^^^^ col1 / col2 - col2 col1 * col2 * col2 ... حسب العملية المطلوبة. وفي حال كانت الأعمدة عبارة عن سلاسل نصية، تختلف الطريقة حسب نوع محرك قاعدة البيانات. SQL Server يستعمل || و الباقي منهم يستعمل الدالة CONCAT CONCAT('string1','string2')‎
    1 نقطة
  8. قمت بانشاء قائمة منسدلة بلغة HTML تتيح للمستخدم اختيار اللغة الخاصة به لكن اريد أن يكون العنصر المختار افتراضيا French كيف يمكنني فعل ذلك الكود الذي أريد التعديل عليه <select > <option value="arabic">Arabic</option> <option value="english">English</option> <option value="french">French</option> <option value="spanish">Spanish</option> </select>
    1 نقطة
  9. لتحديد عنصر بشكل افتراضي، نستخدم الكلمة selected في HTML <option value="..." selected >French </option> ^^^^^^^^ أحيانا يحصل مشكلة إن استخدمنا الحدث on change ولم بقم المستخدم بتغيير الخيار الافتراضي لأنه بريد تحديده، فتضع خيار افتراضي غير فعال <select > <option value="no-language" selected disabled>choose your language</option> ^^^^^^^^^^^^^^^^^ <option value="arabic">Arabic</option> <option value="english">English</option> <option value="french">French</option> <option value="spanish">Spanish</option> </select> حيث نقوم بتحديد الخيار إلغاء تفعليه بنفس الوقت disabled .. مما يجبر المستخدم على تحديد لغة و بذلك حصول الحدث onchange
    1 نقطة
  10. قمت بانشاء صفحتين بلغة html احتاج للمساعدة بحيث عند الضغط على الزر الموجود في الصفحة الأولى يتم فتح الصفحة الثانية <ul class="navbar-nav text-uppercase ml-auto " > <li class="nav-item"><a class="nav-link js-scroll-trigger" target="_blank" >Home</a></li> <li class="nav-item"><a class="nav-link js-scroll-trigger" target="_blank" >track</a></li> </ul>
    1 نقطة
  11. يمكن ملاحظة أنك لم تستخدمي الزر Button بل رابط تشعبي للعنصر a = anchor ولقد نسيتي وضع مسار الصفحة الثانية ضمن الخاصية href لهذا العنصر (a) <a class="nav-link js-scroll-trigger" href="home.html" target="_blank" >Home</a> ^^^^^^^^^^^^^^^ <a class="nav-link js-scroll-trigger" href="track.html" target="_blank" >track</a> ^^^^^^^^^^^^^^^ يبقى عليك التأكد من اسم الصفحة الثانية و مسار الملف بشكل صحيح، مثلاً إن كان ملف الصفحة التالية في مجلد آخر يتوجب تحديد المسار بشكل سليم المثال السابق يفترض أن جميع ملفات HTML بنفس المجلد الجذر لهم. في حال كان لديك مجلد خاص بالصفحات، يمكن تعديل المسار ليشير للملف بالطريقة التالية: <a class="nav-link js-scroll-trigger" href="./pages/home.html" target="_blank" >Home</a> ^^^^^^^^^^^^^^^^^^^^^^^ <a class="nav-link js-scroll-trigger" href="./pages/track.html" target="_blank" >track</a> ^^^^^^^^^^^^^^^^^^^^^^^^ حيث أن بنية المجلدات: أحيانا وضع نقطة ضمن المسار (تشير للمجلد الحالي) يسبب مشكلة، لذلك نكتب اسم المجلد مباشرة: <a class="nav-link js-scroll-trigger" href="pages/home.html" target="_blank" >Home</a> ^^^^^^^^^^^^^^^^^^^^^^^ <a class="nav-link js-scroll-trigger" href="pages/track.html" target="_blank" >track</a> ^^^^^^^^^^^^^^^^^^^^^^^^
    1 نقطة
  12. إن قاعدة البيانات تم اختراعها لتسمح لنا بتخزين كميات كبيرة من البيانات مع الأخذ بالحسبان تسهيل التعامل مع هذه البيانات و توفير آليات و لغات برمجة و مكتبات تعمل على تحسين الاستعلامات لهذه البيانات. تم قديما تخزين البيانات بطريقة عشوائية ضمن ملفات نصية و كان البحث او تعديل أي ششيء صعب جدا، ثم تطور مفهوم قواعد البيانات ليحل مشاكل قراءة وكتابة و التعديل البيانات بسرعة و بدون فقدان اي منها، أي المحافظة عليها، و تمكين النسخ الاحتياطي و إدارة عملي الوصول للبيانات عن طريق تعريف المستخدمين و الصلاحيات.. ومفهوم قواعد البيانات هو علم ضخم. يمكننا تجنب حشر الصور في قاعدة البيانات و الاكتفاء بالتعامل معها على شكل ملفات نقوم بادارتها ضمن ملفات نظام التشغيل لكي تخفف حجم قواعد البيانات التي نضع فيها روابط الملفات أي المسارات و نصل للملف لاحقا عن طريق مساره و ليس بجلب بياناته مباشرة ضمن الاستعلام. مقالات أكاديمية حسوب عن قواعد البيانات: cademy.hsoub/devops/database
    1 نقطة
  13. تحيه طيبه للجميع لو كان لدي عمود يحتوي على أسماء الدول ولنفترض لدي السعودية الأردن ومصر الخ.. وكنت ارغب بالحصول على كل الحقول ما عدا السعودية على سبيل المثال كيف ممكن نعمل هذا الامر؟ طبعا لا ارغب بتحديد جمله شرطيه فيها أسماء الدول المتبقية والسبب ان لدي مجموع كبير من الدول احتاج الى طريقة لتخطي الدول التي لا احتاجها الامر ابسط بشكل هذا هل توجد طريقة لفعل ذلك؟
    1 نقطة
  14. نعم يمكنك عمل ذلك من خلال إستعمال SQL التالي: SELECT * FROM `countries` WHERE name != "saudi arabia"; بهذا الشكل سوف يتم تحديد كل الدول ما عدا السعودية. وإذا كنت تريد تحديد كل الدول ما عدا السعودية ومصر على سبيل المثال فيمكنك أن تستخدم المعامل NOT IN: SELECT * FROM `countries` WHERE name NOT IN ("saudi arabia", "egypt"); بالتأكيد يمكنك أن تقوم بإضافة قدر ما تشاء من دول إلى الإستعلام السابق، وسوف يتم تحديد باقي الدول فقط.
    1 نقطة
  15. السلام عليكم ورحمة الله وبركاته، بعد إكمالي ل أساسيات لغة بايتون ،واشتغلت على problème solving, وبعض التمارين ،الان على أن اختار المجال الذي أريده، ما هو المجال الأنسب في نضركم؟ انا كنت افكر في مجال برمجة مواقع الويب ب Django, لكن أحدهم أخبرني ان اسعار مواقع الويب انخفضت، ف متلا قال لي ان تمن موقع تجاري مابين 200$ و 800$ فقط ،هل هذا صحيح ؟تم ماهو المجال الذي تنصحوني به؟
    1 نقطة
  16. عندما أستخدم الخاصية request.path للحصول على عنوان URL الحالي في جانغو Django، أحصل على عنوان بالشكل التالي: /products/list كيف أجعل هذه الخاصية تُعيد العنوان بدون علامة / الموجودة في البداية لكي تصبح النتيجة بالشكل التالي: products/list كيف أقوم بهذا الأمر؟
    1 نقطة
  17. السلام عليكم ورحمة الله وبركاته أستخدم هذه الحزمة مسئولة عن عربة التسوق : https://packagist.org/packages/hardevine/shoppingcart وأستعمل livewire component . عند أستخدام مجال الأسم namespace use Cart; وهذا غير مذكور في توثيق الحزمة ؟ تظهر أخطاء بداخل الملف وذلك لعدم أستدعائة بالشكل الصحيح . ولذلك لا تعمل عربة التسوق . <!-- جزء ملف العرض --> <div class="row"> <ul class="product-list grid-products equal-container"> @foreach ($products as $product) <li class="col-lg-4 col-md-6 col-sm-6 col-xs-6 "> <div class="product product-style-3 equal-elem "> <div class="product-thumnail"> <a href="{{ route('products.details', ['slug' => $product->slug ]) }}" title="{{$product->name}}"> <figure><img src="{{ asset ('assets/images/products') }}/{{ $product->image }}" alt="{{$product->name}}"></figure> </a> </div> <div class="product-info"> <a href="{{ route('products.details', ['slug' => $product->slug ]) }}" class="product-name"><span>{{ $product->name }}</span></a> <div class="wrap-price"><span class="product-price">{{ $product->regular_price }}</span></div> <a href="#" class="btn add-to-cart" wire:click.prevent="store({{ $product->id }},{{ $product->name }},{{ $product->regular_price }})">Add To Cart</a> </div> </div> </li> @endforeach </ul> </div> <?php //مكون livewire namespace App\Http\Livewire; use App\Models\Product; use Livewire\Component; use Livewire\WithPagination; use Cart; class ShopComponent extends Component { public function store($product_id,$product_name,$product_price){ Cart::add($product_id,$product_name,1,$product_price)->associate('App\Models\Product'); session()->flash('success_message','Item added in Cart'); return redirect()->route('product.cart'); } use WithPagination; public function render() { $products = Product::paginate(12); return view('livewire.shop-component' ,['products'=> $products])->layout('layouts.base'); } }
    1 نقطة
  18. حتى تتمكن من استخدام الصنف Cart بالشكل الصحيح، ضمن الملف config/app.php يجب إضافة السطر التالي للمفتاح aliases: 'aliases' => [ // ... 'Cart' => Gloudemans\Shoppingcart\Facades\Cart::class, ], أيضًا يجب نقل use WithPagination إلى الأعلى داخل الصنف كالتالي: class ShopComponent extends Component { use WithPagination; ... } يرجى إرفاق نص الخطأ في حال إمكانية ذلك
    1 نقطة
  19. 1 نقطة
  20. الخاصية position في CSS تصف كيف يجب أن يتموضع العنصر في المستند، والخاصيات top و right و bottom و left تُحدِّد المكان النهائي لتلك العناصر. وتحتوي هذه الخاصية على عدة قيم مثل /* الكلمات المفتاحية */ position: static; position: relative; position: absolute; position: fixed; position: sticky; /* القيم العامة */ position: inherit; position: initial; position: unset; القيمة static ، سيكون موضع العنصر محسوبًا بناءً على البنية التنظيمية للمستند، ولن يكون للخاصيات top و right و bottom و left و z-index أي أثر، وهذه هي القيمة الافتراضية. الخاصية relative ، سيكون موضع العنصر محسوبًا بناءً على البنية التنظيمية العادية للمستند، ثم سيتم إزاحته نسبةً إلى موضعه الأصلي اعتمادًا على الخاصيات top و right و bottom و left، ولن يكون للإزاحة تأثيرٌ على بقية العناصر، أي أنَّ المساحة المحجوزة للعنصر في تخطيط الصفحة هي نفس المساحة المحجوزة إذا كانت هذه الخاصية هي static. ويمكن استخدام الخاصية z-index مع العنصر. الخاصية absolute ، سيُزال العنصر من البنية التنظيمية للمستند، ولن يُحجَز له مكانٌ في تخطيط الصفحة، وإنما سيتم تحديد موضعه نسبةً إلى أقرب عنصر أب له موضع نسبي، أو إلى العنصر <body>، وسيُحدَّد موضعه النهائي عبر الخاصيات top و right و bottom و left، ويمكن استخدام الخاصية z-index مع العنصر. ويمكن أن يكون للعناصر المطلقة هوامش margin. الخاصية fixed ، سيُزال العنصر من البنية التنظيمية للمستند، ولن يُحجَز له مكانٌ في تخطيط الصفحة، وإنما سيتم تحديد موضعه نسبةً إلى إطار العرض (viewport)، وسيُحدَّد موضعه النهائي عبر الخاصيات top و right و bottom و left، ويمكن استخدام الخاصية z-index مع العنصر. إذا طُبِعَت الصفحة فسيتوضع العنصر في المكان نفسه في كل صفحة. الخاصية sticky ، سيكون موضع العنصر محسوبًا بناءً على البنية التنظيمية العادية للمستند، ثم سيتم إزاحته نسبةً إلى موضعه الأصلي اعتمادًا على الخاصيات top و right و bottom و left، ولن يكون للإزاحة تأثيرٌ على بقية العناصر. عند التمرير إلى حدٍّ معيّن فسيتحوّل سلوك العنصر إلى ما يشبه السلوك الناتج عن fixed. يمكنك إلى الرجوع إلى التوثيق الخاص بخاصية position على ويكي حسوب من هنا .
    1 نقطة
  21. لا أدري أي رسالة خطأ تظهر لك، لكن مبدئياً لديك خطأ في كتابة التابع: getElementByid لغة جافاسكربت حساسة لحالة الأحرف لذلك فالأصح هو getElementById document.getElementById('logout-form').submit();
    1 نقطة
  22. قم بإنشاء البياننات باستخدام الدالة writer من الوحدة csv واحفظها في الذاكرة باستخدام StringIO وبعدها قُم بإرسالها للمُستخدم على شكل ملف csv قابل للتحميل: import csv # نقوم باستدعاء الوحدة csv from flask import Flask from io import StringIO from werkzeug.wrappers import Response app = Flask(__name__) @app.route('/') def download_history(): def generate(): """ هذه الدالة تسمح بإنشاء البيانات على شكل CSV """ data = StringIO() w = csv.writer(data) # إنشاء البيانات التي نُريد إرسالها للمُستخدم history = [ ('/login', datetime(2022, 3, 4, 3, 38)), ('/' , datetime(2022, 3, 4, 3, 38)), ('/order', datetime(2022, 3, 4, 3, 39)), ('/logout', datetime(2022, 3, 4, 3, 42)) ] # حفظ البيانات داخل ملف # CSV for item in history: w.writerow(( item[0], item[1].isoformat() # تنسيق التاريخ كسلسلة نصية )) yield data.getvalue() data.seek(0) data.truncate(0) # إرسال البيانات للمُستخدم لتحميلها response = Response(generate(), mimetype='text/csv') # الملف اسمه # history.csv response.headers.set("Content-Disposition", "attachment", filename="history.csv") return response
    1 نقطة
  23. يمكن استخدام make_random_password بهذه الطريقة مثلا for user in users_list: password = User.objects.make_random_password() user.set_password(password) user.save(update_fields=['password']) بهذه الطريقة يمكنك أن تعين لكل مستخدم كلمة سر عشوائية
    1 نقطة
  24. عليك بنزع الأقواس المُجعدة {{ }} وعلامات الإقتباس "" من على request.build_absolute_uri ﻷنك قُمت باستدعاء وسم القالب {% if %} فلا يوجد أي داعي لإضافتهم. {% if 'ar' in request.build_absolute_uri %} 'AR' {% else %} 'EN' {% endif %}
    1 نقطة
  25. بالتأكيد , هو فقط سيكون تحديث للتطبيق , أي ليس تطبيق جديد , لن يؤثر على عدد التنزيلات أو ما شابه
    1 نقطة
  26. لا يوجد مبلغ محدد ثابت، الأمر يعتمد على نوع العمل الذي تقوم به والعرض والطلب، فمثلًا عند العمل كموظف ستحصل على راتب ثابت يعتمد على مدى احتياج الشركة لخدماتك، وعندما يكون عملك حرًا يكون دخلك تابعًا لعدد المشاريع التي بإمكانك العمل عليها وهنا أرباحك تزيد كلما كانت لديك المهارات التقنية والعملية ومهارات ادارة الوقت والتعامل مع زبائنك والتسويق لنفسك وجودة أعمالك. ركز بداية على تطوير نفسك والتطبيق على عدة مشاريع ولو كانت وهمية لتقوي مهاراتك ويكون لديك معرض أعمال تتمكن من خلاله إما الحصول على وظيفة أو الحصول على مشاريع تعمل عليها بنفسك. يمكنك الاستفادة من قراءة المقالات التالية:
    1 نقطة
  27. تُعدّ عمليتي البحث (searching) والترتيب (sorting) أكثر الأساليب شيوعًا لمعالجة المصفوفات. يُشير البحث (searching) هنا إلى محاولة العثور على عنصر بمواصفات معينة ضِمْن مصفوفة بينما يُشير الترتيب (sorting) إلى إعادة ترتيب عناصر المصفوفة ترتيبًا تصاعديًا أو تنازليًا. عادةً ما يَعتمِد المقصود بالترتيب التصاعدي والتنازلي على السياق المُستخدَم به الترتيب. في الواقع، تُوفِّر الجافا تُوفِّر بعض التوابع المَبنية مُسْبَقّا (built-in methods) الخاصة بعمليات البحث والترتيب -كما رأينا بالقسم الفرعي 7.2.2-، ومع ذلك ينبغي أن تَكُون على اطلاع ومعرفة بالخوارزميات (algorithms) التي تَستخدِمها تلك التوابع. ولهذا، سنَتعلَّم بعضًا من تلك الخوارزميات بهذا القسم. عادةً ما يُناقَش البحث (searching) والترتيب (sorting) نظريًا باِستخدَام مصفوفة من الأعداد. ولكن من الناحية العملية، هناك أمور أكثر تشويقًا بكثير. فمثلًا، قد تَكُون المصفوفة عبارة عن قائمة بريدية بحيث يَكُون كل عنصر بها عبارة عن كائن (object) يَحتوِي على اسم وعنوان. إذا كان لدينا اسم شخص معين، يُمكِننا العثور على عنوانه، وهو ما يُعدّ مثالًا على عملية البحث (searching)؛ لأنك ببساطة تَبحَث عن كائن ضِمْن مصفوفة بالاعتماد على اسم الشخص. سيَكُون من المفيد أيضًا لو تَمَكَّنا من ترتيب المصفوفات وفقًا لمعيار معين مثل أن نُرتِّب المصفوفة السابقة بحيث تَكُون الأسماء مُرتَّبة ترتيبًا أبجديًا أو قد نُرتِّبها وفقًا للرقم البريدي. سنُعمِّم الآن المثال السابق إلى تصور مُجرَّد بعض الشئ: لنتخيل أن لدينا مصفوفة تَحتوِي على عدة كائنات (objects)، وأننا نرغب بالبحث داخل تلك المصفوفة أو نرغب بترتيبها بالاعتماد على احدى مُتْغيِّرات النُسخ (instance variables) المُعرَّفة بتلك الكائنات. سنلجأ إلى اِستخدَام بعض المصطلحات الشائعة بقواعد البيانات (databases). سنُطلِق اسم "تسجيل (record)" على كل كائن ضِمْن المصفوفة بينما سنُطلِق اسم "الحقول (fields)" على مُتْغيِّرات النُسخ المُعرَّفة بتلك الكائنات. نستطيع الآن أن نُعيِد صياغة مثال القائمة البريدية إلى ما يلي: يَتَكوَّن كل تسجيل (record) من اسم وعنوان. قد تَتَكوَّن حقول التسجيل من الاسم الأول والأخير والعنوان والمدينة والدولة والرقم البريدي. ينبغي أن نختار إحدى تلك الحقول لتُصبِح "مفتاحًا (key)" لكي نَتَمكَّن من إجراء عمليتي البحث والترتيب. وفقًا لهذا التصور، سيُمثِل البحث محاولة العثور على تسجيل بالمصفوفة بحيث يَحتوِي مفتاحه على قيمة معينة بينما سيُمثِل الترتيب (sorting) تَبديِل مواضع تسجيلات المصفوفة إلى أن تُصبِح مفاتيحها (keys) مُرتَّبة ترتيبًا تصاعديًا أو تنازليًا. دورة تطوير التطبيقات باستخدام لغة Python احترف تطوير التطبيقات مع أكاديمية حسوب والتحق بسوق العمل فور انتهائك من الدورة اشترك الآن البحث (Searching) هناك خوارزمية واضحة للبحث عن عنصر معين داخل مصفوفة: افحص كل عنصر بالمصفوفة بالترتيب، واختبر ما إذا كانت قيمة ذلك العنصر هي نفسها القيمة التي تبحث عنها. إذا كان كذلك، فقد انتهت عملية البحث إذًا. أما إذا لم تعثر عليه بعد فحص جميع عناصر المصفوفة، فهو إذًا غير موجود بها. يُمكننا كتابة برنامج فرعي لتنفيذ تلك الخوارزمية (algorithm) بسهولة. بفرض أن المصفوفة التي تريد البحث بها عبارة عن مصفوفة من النوع int[]‎، يبحث التابع (method) التالي عن قيمة عددية ضمن مصفوفة. سيعيد التابع فهرس (index) العنصر بالمصفوفة إذا عثر عليه بينما سيعيد -1 إذا لم يعثر عليه كإشارة أن العدد الصحيح غير موجود: static int find(int[] A, int N) { for (int index = 0; index < A.length; index++) { if ( A[index] == N ) // ‫عثرنا على N بهذا الفهرس return index; } // ‫إذا وصلنا إلى هنا، فإن N غير موجودة بأي مكان ضمن المصفوفة // أعد القيمة -1 return -1; } يُطلَق على طريقة البحث السابقة اسم البحث الخطي (linear search). إذا لم يَكُن لدينا أي معلومة عن كيفية ترتيب العناصر ضِمْن المصفوفة، فإنها تُمثِل الخيار الأفضل. في المقابل، إذا كنا على علم بكَوْن عناصر المصفوفة مُرتَّبة تصاعديًا أو تنازليًا، يُمكِننا إذًا أن نستغل تلك الحقيقة، ونَستخدِم خوارزميات أسرع. إذا كانت عناصر مصفوفة معينة مرتبة، يُقال عليها أنها "مصفوفة مرتبة (sorted)". يَستغرق ترتيب عناصر مصفوفة بعض الوقت بالطبع، ولكن إذا كانت المصفوفة مرتبة بالفعل، فيُمكننا إذًا أن نستغل تلك الحقيقة. يُعدّ البحث الثنائي (binary search) أحد الطرائق المُتاحة للبحث عن عنصر ضِمْن مصفوفة مُرتَّبة (sorted). على الرغم من أن تّنْفِيذه (implement) ليس سهلًا تمامًا، فإن فكرته بسيطة: إذا كنت تَبحَث عن عنصر ضِمْن قائمة مُرتَّبة، يُمكِنك أن تَستبعِد نُصف العناصر ضِمْن القائمة بِفَحْص عنصر واحد فقط. على سبيل المثال، لنَفْترِض أننا نبحث عن العدد 42 ضِمْن مصفوفة مُرتَّبة مُكوَّنة من 1000 عدد صحيح، ولنَفْترِض أن المصفوفة مُرتَّبة ترتيبًا تصاعديًا، ولنَفْترِض أننا فَحصَنا العنصر الموجود بالفهرس 500، ووجدنا أن قيمته تُساوِي 93. لمّا كان العدد 42 أقل من 93، ولمّا كانت العناصر بالمصفوفة مُرتَّبة ترتيبًا تصاعديًا، يُمكِننا إذًا أن نَستنتج أنه في حالة كان العدد 42 موجودًا بتلك المصفوفة من الأساس، فإنه لابُدّ وأنه يَقَع بمَوضِع فهرسه أقل من 500. يُمكِننا إذًا أن نَستبِعد جميع العناصر الموجودة بمَواضِع فهرسها أكبر من 500؛ فهي بالضرورة أكبر من أو تُساوِي 93. الخطوة التالية ببساطة هي فَحْص قيمة العنصر بالمَوْضِع 250. إذا كان العدد بذلك المَوْضِع يُساوِي -21 مثلًا، يُمكِننا إذًا أن نَستبعِد جميع العناصر قَبل المَوْضِع 250، ونُقصِر بحثنا على المَواضِع من 251 إلى 499. سيُقصِر الاختبار التالي بحثنا إلى 125 مَوضِع فقط ثُمَّ إلى 62. سيَتَبقَّى مَوضِع واحد فقط بعد 10 خطوات. تُعدّ تلك الطريقة أفضل كثيرًا من فَحْص كل عنصر ضِمْن المصفوفة. فمثلًا، إذا كانت المصفوفة تَحتوِي على مليون عنصر، ستَستغرِق خوارزمية البحث الثنائي 20 خطوة فقط للبحث بكامل المصفوفة. في العموم، يُساوِي عدد الخطوات اللازمة للبحث بأي مصفوفة لوغاريتم عدد العناصر بتلك المصفوفة بالنسبة للأساس 2. لكي نَتَمكَّن من تَحْوِيل خوارزمية البحث الثنائي (binary search) إلى برنامج فرعي (subroutine) يَبحَث عن عنصر N ضِمْن مصفوفة A، ينبغي أن نَحتفِظ بنطاق المَوْاضِع التي يُحتمَل أن تَحتوِي على N بحيث نَستبعِد منها تدريجيًا المزيد من الاحتمالات، ونُقلِّل من حجم النطاق. سنَفْحَص دائمًا العنصر الموجود بمنتصف النطاق. إذا كانت قيمته أكبر من N، يُمكِننا إذًا أن نَستبعِد النصف الثاني من النطاق أما إذا كانت قيمته أقل من N، يُمكِننا أن نَستبعِد النصف الأول. أما إذا كانت قيمته تُساوي N، فإن البحث يَكُون قد انتهى. إذا لم يَتبقَّى أية عناصر، فإن العدد N غَيْر موجود إذًا بالمصفوفة. اُنظر شيفرة البرنامج الفرعي (subroutine): /** * Precondition: A must be sorted into increasing order. * Postcondition: If N is in the array, then the return value, i, * satisfies A[i] == N. If N is not in the array, then the * return value is -1. */ static int binarySearch(int[] A, int N) { int lowestPossibleLoc = 0; int highestPossibleLoc = A.length - 1; while (highestPossibleLoc >= lowestPossibleLoc) { int middle = (lowestPossibleLoc + highestPossibleLoc) / 2; if (A[middle] == N) { // ‫عثرنا على N بهذا الفهرس return middle; } else if (A[middle] > N) { // ‫استبعد المواضع الأكبر من أو تساوي middle highestPossibleLoc = middle - 1; } else { // ‫استبعد المواضع الأقل من أو تساوي middle lowestPossibleLoc = middle + 1; } } // ‫إذا وصلنا إلى هنا فإن highestPossibleLoc < LowestPossibleLoc // ‫أي أن N غير موجود بالمصفوفة. // أعد القيمة -1 لكي تشير إلى عدم وجود‫ N بالمصفوفة return -1; } القوائم الارتباطية (Association Lists) تُعدّ القوائم الارتباطية (association lists) مثل القاموس (dictionary) واحدة من أشهر التطبيقات على البحث (searching). يَربُط القاموس (dictionary) مجموعة من التعريفات مع مجموعة من الكلمات. يُمكِنك مثلًا أن تَستخدِم كلمة معينة لمعرفة تَعرِيفها. قد تُفكِر بالقاموس على أنه قائمة من الأزواج (pairs) على الهيئة (w,d) حيث تُمثِل w كلمة معينة بينما d هي تعريف تلك الكلمة. بالمثل، تَتَكوَّن القوائم الارتباطية (association list) من قائمة من الأزواج (k,v) حيث تُمثِل k مفتاحًا (key) معينًا بينما تُمثِل v القيمة (value) المرتبطة بذلك المفتاح. لا يُمكِن لزوجين (pairs) ضِمْن قائمة معينة أن يَملُكا نفس قيمة المفتاح (key). عادةً ما نُطبِق عمليتين أساسيتين على القوائم الارتباطية: أولًا، إذا كان لديك مفتاح معين k، يُمكِنك أن تَجلُب القيمة v المُرتبِطة به إن وجدت. ثانيًا، يُمكِننا أن نُضيِف زوجًا جديدًا (k,v) إلى قائمة ارتباطية (association list). لاحِظ أنه في حالة إضافة زوج (pair) إلى قائمة، وكانت تلك القائمة تَحتوِي على زوج له نفس المفتاح، فإن القيمة الجديدة المضافة تَحلّ محلّ القديمة. يُطلق عادةً على تلك العمليتين اسم الجَلْب (get) والإضافة (put). يُشاع اِستخدَام القوائم الارتباطية (association lists) في العموم بعلوم الحاسوب (computer science). على سبيل المثال، لابُدّ أن يَحتفِظ المُصرِّف (compiler) بمَوضِع الذاكرة (memory location) الخاص بكل مُْتْغيِّر. يستطيع المُصرِّف إذًا أن يَستخدِم قائمة ارتباطية بحيث يُمثِل كل مفتاح (key) بها اسمًا لمُتْغيِّر بينما تُمثِل قيمة (value) ذلك المفتاح عنوانه بالذاكرة. مثال آخر هو القوائم البريدية إذا كان العنوان مَربُوطًا باسم ضِمْن تلك القائمة. فمثلًا، يَربُط دليل الهاتف كل اسم برقم هاتف. سنَفْحَص فيما يَلي نُسخة مُبسَطة من ذلك المثال. يُمكِننا أن نُمثِل عناصر دليل الهاتف بالقائمة الارتباطية (association list) بهيئة كائنات تنتمي إلى الصَنْف التالي: class PhoneEntry { String name; String phoneNum; } تَتَكوَّن البيانات الموجودة بدليل الهاتف من مصفوفة من النوع PhoneEntry[]‎ بالإضافة إلى مُتْغيِّر من النوع العددي الصحيح (integer) للاحتفاظ بعدد المُدْخَلات المُخزَّنة فعليًا بذلك الدليل. يُمكِننا أيضًا أن نَستخدِم تقنية المصفوفات الديناميكية (dynamic arrays) -اُنظر القسم الفرعي 7.2.4- لكي نَتجنَّب وَضْع حد أقصى عشوائي على عدد المُدْخَلات التي يُمكِن لدليل الهاتف أن يَحتوِيه أو قد نَستخدِم النوع ArrayList. ينبغي أن يَحتوِي الصَنْف PhoneDirectory على توابع نسخ (instance methods) لتّنْفِيذ (implement) كلًا من عمليتي الجَلْب (get) والإضافة (put). تُمثِل الشيفرة التالية تعريفًا (definition) بسيطًا لذلك الصَنْف: public class PhoneDirectory { private static class PhoneEntry { String name; // الاسم String number; // رقم الهاتف } private PhoneEntry[] data; // مصفوفة لحمل أزواج مكونة من أسماء وأرقام هاتف private int dataCount; // عدد الأزواج ضمن المصفوفة /** * Constructor creates an initially empty directory. */ public PhoneDirectory() { data = new PhoneEntry[1]; dataCount = 0; } private int find( String name ) { for (int i = 0; i < dataCount; i++) { if (data[i].name.equals(name)) return i; // الاسم موجود بالموضع‫ i } return -1; // الاسم غير موجود بالمصفوفة } /** * @return The phone number associated with the name; if the name does * not occur in the phone directory, then the return value is null. */ public String getNumber( String name ) { int position = find(name); if (position == -1) return null; // لا يوجد بيانات لذلك الاسم else return data[position].number; } public void putNumber( String name, String number ) { if (name == null || number == null) throw new IllegalArgumentException("name and number cannot be null"); int i = find(name); if (i >= 0) { // ‫الاسم موجود بالفعل بالموضع i بالمصفوفة // استبدل العدد الجديد بالعدد القديم بذلك الموضع data[i].number = number; } else { // أضف زوج جديد مكون من اسم وعدد إلى المصفوفة // إذا كانت المصفوفة ممتلئة، أنشئ مصفوفة جديدة أكبر if (dataCount == data.length) { data = Arrays.copyOf( data, 2*data.length ); } PhoneEntry newEntry = new PhoneEntry(); // أنشئ زوجا جديدا newEntry.name = name; newEntry.number = number; data[dataCount] = newEntry; // أضف الزوج الجديد إلى المصفوفة dataCount++; } } } // end class PhoneDirectory يُعرِّف الصَنْف تابع النسخة find()‎. يَستخدِم ذلك التابع أسلوب البحث الخطي (linear search) للعثور على مَوْضِع اسم معين بالمصفوفة المُكوَّنة من أزواج من الأسماء والأرقام. يَعتمِد كُلًا من التابعين getNumber()‎ و putNumber()‎ على التابع find()‎. لاحِظ أن التابع putNumber(name,number)‎ يَفحَص أولًا ما إذا كان الاسم موجودًا بدليل الهاتف أم لا. إذا كان موجودًا، فإنه فقط يُغيِّر الرقم المُرتبِط بذلك الاسم أما إذا لم يَكُن موجودًا، فإنه يُنشِئ مُدخلًا جديدًا ويُضيفه إلى المصفوفة. قد نُضيِف الكثير من التحسينات بالطبع على الصَنْف المعرَّف بالأعلى. فمثلًا، قد نَستخدِم البحث الثنائي (binary search) بالتابع getNumber()‎ بدلًا من البحث الخطي، ولكن يَتَطلَّب ذلك أن تَكُون الأسماء المُخزَّنة بقائمة المُدْخَلات مُرتَّبة ترتيبًا أبجديًا، وهو ليس أمرًا صعبًا كما ستَرَى بالقسم الفرعي التالي. عادةً ما يُطلَق اسم الخارطة (maps) على القوائم الارتباطية (association lists)، وتُوفِّر الجافا صَنْفًا قياسيًا ذو معاملات غَيْر محددة النوع (parameterized) اسمه هو Map كتنفيذ (implementation) لها. تستطيع أن تَستخدِم ذلك الصَنْف لكي تُنشِئ قوائمًا ارتباطية مُكوَّنة من مفاتيح (keys) وقيم (values) من أي نوع. يُعدّ ذلك التنفيذ (implementation) أكفأ بكثير من أي شيء قد تَفعَلُه باِستخدَام مصفوفات بسيطة. سنَتعرَّض بالفصل العاشر لذلك الصَنْف. الترتيب بالإدراج (Insertion Sort) اتضح لنا الآن الحاجة إلى ترتيب المصفوفات. في الواقع، تَتَوفَّر الكثير من الخوارزميات المُخصَّصة لذلك الغرض، منها خوارزمية الترتيب بالإدراج (insertion sort). تُعدّ تلك الخوارزمية واحدة من أسهل الطرائق لترتيب مصفوفة، كما يُمكِننا أن نُطبِقها للإبقاء على المصفوفة مُرتَّبة بينما نُضيِف إليها عناصر جديدة. لنَفْحَص المثال التالي أولًا: لنَفْترِض أن لدينا قائمة مُرتَّبة ونريد إضافة عنصر جديد إليها. إذا كنا نريد التأكُّد من أنها ستظلّ مُرتَّبة، ينبغي أن نُضيِف ذلك العنصر بمَوْضِعه الصحيح بحيث تأتي قبله جميع العناصر الأصغر بينما تَحِلّ بَعْده جميع العناصر الأكبر. يَعنِي ذلك تَحرِيك جميع العناصر الأكبر بمقدار خانة واحدة لترك مساحة للعنصر الجديد. static void insert(int[] A, int itemsInArray, int newItem) { int loc = itemsInArray - 1; // ابدأ من نهاية المصفوفة // ‫حرك العناصر الأكبر من newItem للأعلى بمقدار مسافة واحدة // وتوقف عندما تقابل عنصر أصغر أو عندما تصل إلى بداية المصفوفة while (loc >= 0 && A[loc] > newItem) { A[loc + 1] = A[loc]; // Bump item from A[loc] up to loc+1. loc = loc - 1; // انتقل إلى الموضع التالي } // ‫ضع newItem بآخر موضع فارغ A[loc + 1] = newItem; } يُمكِننا أن نَمِد ذلك إلى تابع للترتيب (sorting) بأخذ جميع العناصر الموجودة بمصفوفة غَيْر مُرتَّبة (unsorted) ثم اضافتها تدريجيًا واحدًا تلو الآخر إلى مصفوفة آخرى مع الإبقاء عليها مُرتَّبة. نستطيع أن نَستخدِم البرنامج insert بالأعلى أثناء كل عملية إدراج (insertion) لعنصر جديد. ملحوظة: بالخوارزمية الفعليّة، لا نأخُذ جميع العناصر من المصفوفة، ولكننا فقط نتذكَّر الأجزاء المُرتَّبة منها: static void insertionSort(int[] A) { // ‫رتب المصفوفة A ترتيبًا تصاعديا int itemsSorted; // عدد العناصر المُرتَّبة إلى الآن for (itemsSorted = 1; itemsSorted < A.length; itemsSorted++) { // ‫افترض أن العناصر A[0]‎ و A[1]‎ .. إلخ // ‫مرتبة بالفعل. أضف A[itemsSorted]‎ إلى الجزء المرتب من // القائمة int temp = A[itemsSorted]; // العنصر المطلوب إضافته int loc = itemsSorted - 1; // ابدأ بنهاية القائمة while (loc >= 0 && A[loc] > temp) { A[loc + 1] = A[loc]; // Bump item from A[loc] up to loc+1. loc = loc - 1; // Go on to next location. } // ‫ضع temp بآخر موضع فارغ A[loc + 1] = temp; } } تُوضِح الصورة التالية مرحلة واحدة من عملية الترتيب بالإدراج (insertion sort) حيث تُبيِّن ما يحدث أثناء تّنفِيذ تَكْرار واحد من الحلقة for بالأعلى بالتحديد عندما يَكُون عدد العناصر ضِمْن المصفوفة itemsSorted مُساوِيًا للقيمة 5: الترتيب الانتقائي (Selection Sort) تَستخدِم خوارزمية ترتيب (sorting) آخرى فكرة العُثور على أكبر عنصر ضِمْن القائمة، وتَحرِيكه إلى نهايتها حيث ينبغي أن يتواجد إذا كنا نريد ترتيبها ترتيبًا تصاعديًا. بمُجرَّد تَحرِيك أكبر عنصر بالقائمة إلى مَوْضِعه الصحيح، يُمكِننا أن نُطبِق نفس الفكرة على العناصر المُتبقَاة أي أن نَعثُر على العنصر الأكبر التالي، ونُحرِكه إلى المَوْضِع قبل الأخير، وهكذا. يُطلَق اسم "الترتيب الانتقائي (selection sort)" على تلك الخوارزمية (algorithm). يُمكِننا كتابتها كما يلي: static void selectionSort(int[] A) { // رتب المصفوفة A ترتيبا تصاعديا باستخدام الترتيب الانتقائي for (int lastPlace = A.length-1; lastPlace > 0; lastPlace--) { int maxLoc = 0; // موضع أكبر عنصر إلى الآن for (int j = 1; j <= lastPlace; j++) { if (A[j] > A[maxLoc]) { // ‫لأن A[j]‎ أكبر من أكبر قيمة رأيناها إلى الآن، فإن j هو // الموضع الجديد لأكبر قيمة وجدناها إلى الآن maxLoc = j; } } int temp = A[maxLoc]; // Swap largest item with A[lastPlace]. A[maxLoc] = A[lastPlace]; A[lastPlace] = temp; } // end of for loop } يَستخدِم الصَنْف Hand الذي كتبناه بالقسم الفرعي 5.4.1 نسخة مختلفة قليلًا من الترتيب الانتقائي (selection sort). يُعرِّف الصَنْف Hand مُتْغيِّرًا من النوع ArrayList<Card>‎ لتَمثيِل اليد (hand) يحتوي بطبيعة الحال على كائنات من النوع Card. يَحتوِي أي كائن من النوع Card على توابع النسخ getSuit()‎ و getValue()‎، والتي يُمكِننا أن نَستخدِمها لمَعرِفة كُلًا من قيمة ورقة اللعب ورمزها (suit). يُنشِئ التابع (method) المسئول عن عملية الترتيب (sorting) قائمة جديدة، بحيث يَختار ورق اللعب من القائمة القديمة تدريجيًا وبترتيب مُتصاعِد ثم يُحرِكها من القائمة القديمة إلى الجديدة. يَستخدِم التابع (method) القائمة الجديدة بالنهاية لتمثيل اليد بدلًا من القديمة. قد لا يَكُون ذلك هو الأسلوب الأكفأ لإنجاز الأمر، ولكن نظرًا لأن عدد ورق اللعب ضِمْن أي يد (hand) عادةً ما يَكون صغيرًا، فإنه لا يُمثِل مشكلة كبيرة. اُنظر شيفرة ترتيب (sorting) ورق اللعب: public void sortBySuit() { ArrayList<Card> newHand = new ArrayList<Card>(); while (hand.size() > 0) { int pos = 0; // موضع البطاقة الأقل Card c = hand.get(0); // البطاقة الأقل for (int i = 1; i < hand.size(); i++) { Card c1 = hand.get(i); if ( c1.getSuit() < c.getSuit() || (c1.getSuit() == c.getSuit() && c1.getValue() < c.getValue()) ) { pos = i; // Update the minimal card and location. c = c1; } } hand.remove(pos); // احذف ورقة اللعب من اليد الأصلية newHand.add(c); // أضف ورقة اللعب إلى اليد الجديدة } hand = newHand; } لاحِظ أن موازنة العناصر ضِمْن قائمة ليست دائمًا ببساطة اِستخدَام < كالمثال بالأعلى. في هذه الحالة، تُعدّ ورقة لعب معينة أصغر من ورقة لعب آخرى إذا كان رمز (suit) الأولى أقل من رمز الثانية أو أن يَكُونا متساويين بشرط أن تَكُون قيمة ورقة اللعب الثانية أقل من قيمة الأولى. يتأكَّد الجزء الثاني من الاختبار من ترتيب ورق اللعب بنفس الرمز ترتيبًا صحيحًا بحسب قيمته. يُمثِل ترتيب قائمة من النوع String مشكلة مشابهة؛ فالعامل < غَيْر مُعرَّف للسَلاسِل النصية. في المقابل، يُعرِّف الصنف String التابع compareTo. إذا كان str1 و str2 من النوع String، يُمكِننا كتابة ما يلي: str1.compareTo(str2) يُعيِد التابع السابق قيمة من النوع int تُساوِي 0 إذا كان str1 يُساوِي str2 أو قيمة أقل من 0 عندما يأتي str1 قبل str2 أو أكبر من 0 عندما يأتي str1 بَعْد str2. تستطيع مثلًا أن تختبر ما إذا كان str1 يَسبِق str2 أو يُساويه باِستخدَام الاختبار التالي: if ( str1.compareTo(str2) <= 0 ) يُقصَد بكلمات مثل "يَسبِق" أو "يَتبَع" -عند اِستخدَامها مع السَلاسِل النصية (string)- ترتيبها وفقًا للترتيب المعجمي (lexicographic ordering)، والذي يَعتمِد على قيمة اليونيكود (Unicode) للمحارف (characters) المُكوِّنة للسِلسِلة النصية (strings). يَختلِف الترتيب المعجمي (lexicographic ordering) عن الترتيب الأبجدي (alphabetical) حتى بالنسبة للكلمات المُكوَّنة من أحرف أبجدية فقط؛ لأن جميع الأحرف بحالتها الكبيرة (upper case) تَسبِق جميع الأحرف بحالتها الصغيرة (lower case). في المقابل، يُعدّ الترتيب الأبجدي والمعجمي للكلمات المُقتصِرة على 26 حرف بحالتها الصغيرة فقط أو الكبيرة فقط هو نفسه. يُحوِّل التابع str1.compareToIgnoreCase(str2)‎ أحرف سِلسِلتين نصيتين (strings) إلى الحالة الصغيرة (lowercsae) أولًا قبل موازنتهما. يتناسب كُلًا من الترتيب الانتقائي (selection sort) والترتيب بالادراج (insertion sort) مع المصفوفات الصغيرة المُكوَّنة من مئات قليلة من العناصر. أما بالنسبة للمصفوفات الكبيرة، فتَتَوفَّر خوارزميات آخرى أكثر تعقيدًا ولكنها أسرع بكثير. هذه الخوارزميات أسرع بكثير ربما بنفس الكيفية التي يُعدّ بها البحث الثنائي (binary search) أسرع من البحث الخطي (linear search). يَعتمِد التابع القياسي Arrays.sort على الخوارزميات السريعة. سنَتعرَّض لإحدى تلك الخوارزميات بالفصل التاسع. الترتيب العشوائي (unsorting) يُعدّ الترتيب العشوائي لعناصر مصفوفة مشكلة أقل شيوعًا ولكنها شيقة. قد نحتاجها مثلًا لخَلْط مجموعة ورق لعب ضِمْن برنامج. تَتًوفَّر خوارزمية شبيهة بالترتيب الانتقائي (selection sort)، فبدلًا من تَحرِيك أكبر عنصر بالمصفوفة إلى نهايتها، سنختار عنصرًا عشوائيًا ونُحرِكه إلى نهايتها. يَخلِط البرنامج الفرعي (subroutine) التالي عناصر مصفوفة من النوع int: /** * Postcondition: The items in A have been rearranged into a random order. */ static void shuffle(int[] A) { for (int lastPlace = A.length-1; lastPlace > 0; lastPlace--) { // ‫اختر موضعا عشوائيًا بين 0 و 1 و... حتى lastPlace int randLoc = (int)(Math.random()*(lastPlace+1)); // ‫بدل العناصر بالموضعين randLoc و lastPlace int temp = A[randLoc]; A[randLoc] = A[lastPlace]; A[lastPlace] = temp; } } ترجمة -بتصرّف- للقسم Section 4: Searching and Sorting من فصل Chapter 7: Arrays and ArrayLists من كتاب Introduction to Programming Using Java.
    1 نقطة
  28. يُمكِننا أن نُضمِّن نمط المصفوفة الديناميكية (dynamic array) داخل صَنْف كما رأينا بالقسم الفرعي 7.2.4، ولكن بدا الأمر كما لو أننا سنحتاج إلى تعريف صَنْف مختلف لكل نوع من البيانات (data type). في الحقيقة، تُوفِّر جافا خاصية تُعرَف باسم "الأنواع ذات المعاملات غَيْر مُحدَّدة النوع (parameterized types)"، والتي يُمكِنها أن تُجنِّبنا مشكلة تعدد الأصناف كما تُوفِّر جافا الصَنْف ArrayList الذي يُنفِّذ (implement) نمط المصفوفة الديناميكية لجميع أنواع البيانات. الصنف ArrayList والأنواع ذات المعاملات غير محددة النوع (Parameterized Types) تُوفِّر جافا النوع القياسي ArrayList<String>‎ لتمثيل مصفوفة ديناميكية (dynamic arrays) من النوع String. وبالمثل، يُمثِل النوع ArrayList<Button>‎ مصفوفة ديناميكية من النوع Button. بنفس الكيفية، إذا كان Player صنفًا (class) يُمثِل اللاعبين ضِمْن لعبة، فإن النوع ArrayList<Player>‎ يُمثِل مصفوفة ديناميكية من النوع Player. قد يبدو الأمر كما لو أننا نَستخدِم أصنافًا كثيرة، ولكن، في الواقع، هنالك صَنْف (class) واحد فقط هو الصَنْف ArrayList المُعرَّف بحزمة java.util. يُعدّ ذلك الصَنْف صَنْفًا ذي مُعامِلات غَيْر مُحدَّدة النوع (parameterized type) -نوع يُمكِنه أن يَأخُذ معامل نوع (type parameter)-. يُمكِننا ذلك من اِستخدَام صَنْف واحد فقط للحصول على عدة أنواع مثل ArrayList<String>‎ و ArrayList<Button>‎ وحتى ArrayList<T>‎. لابُدّ أن يَكُون معامل النوع T نوعًا كائنيًا (object type) أي اسم صَنْف (class) أو اسم واجهة (interface)، ولا يُمكِنه أن يَكُون نوعًا أساسيًا (primitive type). يعني ذلك أنك لا تستطيع الحصول على ArrayList من النوع int أو ArrayList من النوع char. يُمكِننا مثلًا أن نَستخدِم النوع ArrayList<String>‎ للتّصرِيح (declare) عن مُتْغيِّر كالتالي: ArrayList<String> namelist; أو قد نَستخدِمه كنوع لمعامل صوري (formal parameter) ضِمْن تعريف برنامج فرعي (subroutine) أو كنوع مُعاد (return type) من برنامج فرعي. علاوة على ذلك، قد نَستخدِمه مع العامل new لإنشاء كائنات (objects): namelist = new ArrayList<String>(); يَكُون الكائن المُنشَئ في هذه الحالة من النوع ArrayList<String>‎، ويُمثِل قائمة ديناميكية من السَلاسِل النصية (strings). يُوفِّر الصَنْف مجموعة من توابع النُسخ (instance methods) مثل التابع namelist.add(str)‎ لإضافة سِلسِلة نصية من النوع String إلى القائمة، والتابع namelist.get(i)‎ لجَلْب السِلسِلة النصية الموجود برقم الفهرس i، والتابع namelist.size()‎ لحِسَاب عدد العناصر الموجودة بالقائمة حاليًا. إلى جانب ما سبق، يُستخدَم الصَنْف ArrayList مع أنواع آخرى أيضًا. فمثلًا، إذا كان الصَنْف Player يُمثِل اللاعبين ضِمْن لعبة، يُمكِننا أن نُنشِئ قائمة من اللاعبين كالتالي: ArrayList<Player> playerList = new ArrayList<Player>(); والآن، يُمكِنك أن تَستخدِم playerList.add(plr)‎ لإضافة لاعب plr إلى اللعبة أو قد تَستخدِم playerList.remove(k)‎ لحَذْف اللاعب الموجود برقم الفهرس k. علاوة على ذلك، إذا كان playerList مُتْغيِّرًا محليًا (local variable)، يُمكِنك أن تَستخدِم صيغة التّصرِيح (declaration) المختصرة -اُنظر القسم الفرعي 4.8.2- كما يلي: var playlerList = new ArrayList<Player>(); يَعتمِد مُصرِّف (compiler) الجافا على القيمة المبدئية المُسنَدة إلى playerList لاستنتاج أن نوعه هو ArrayList<Player>‎. عندما تَستخدِم نوعًا (type) مثل ArrayList<T>‎، فإن المُصرِّف يتأكد من أن جميع الكائنات (objects) المضافة إليه من النوع T، وستُعدّ أي محاولة لإضافة كائن (object) من نوع آخر خطأً في بناء الجملة (syntax error)، ولا يُصرَّف عندها البرنامج. لمّا كانت الكائنات المنتمية لصَنْف فرعي من T هي بالنهاية من النوع T، فإنه من المُمكن إضافتها أيضًا إلى القائمة. فمثلًا، يُمكِن لمُتْغيِّر من النوع ArrayList<Pane>‎ أن يَحمِل كائنات من النوع BorderPane أو TilePane أو GridPane أو أي صنف فرعي (subclass) آخر من الصَنْف Pane (في الواقع، يُشبِه ذلك طريقة عمل المصفوفات حيث يستطيع كائن من النوع T[]‎ أن يَحمِل أي كائنات تنتمي لصَنْف فرعي من T). بالمثل، إذا كان T عبارة عن واجهة (interface)، فمن الممكن إضافة أي كائن (objects) إلى القائمة طالما كان يُنفِّذ (implement) تلك الواجهة T. تَملُك الكائنات من النوع ArrayList<T>‎ جميع توابع النسخ (instance methods) التي قد تَتَوقَّعها من مصفوفة ديناميكية (dynamic array). بِفَرْض أن list عبارة عن مُتْغيِّر من النوع ArrayList<T>‎، اُنظر التوابع التالية: list.size()‎: يُعيد حجم القائمة أي عدد العناصر الموجودة بها حاليًا. قد يَكون حجم القائمة مُساويًا للصفر. فمثلًا، يُنشِئ باني الكائن الافتراضي new ArrayList<T>()‎ قائمة حجمها يُساوي صفر، وفي العموم، تتراوح أرقام المواضع الصالحة من 0 وحتى list.size()-1. list.add(obj)‎: يُضيِف كائنًا (object) إلى نهاية القائمة مع زيادة حجمها بمقدار الواحد. لاحِظ أن المُعامل obj لابُد أن يَكُون كائنًا من النوع T أو قد يَكُون فارغًا. list.get(N)‎: يَستقبِل المُعامل N الذي لابُدّ أن يكون عددًا صحيحًا (integer) يتراوح من 0 إلى list.size()-1 ثُمَّ يُعيد القيمة المُخزَّنة بالمَوْضِع N، والتي بطبيعة الحال تَكُون من النوع T. إذا كان N خارج النطاق المسموح به، يَقَع اعتراض من النوع IndexOutOfBoundsException. في الواقع، تُشبه تلك الدالة الأمر A[N]‎ -بِفَرْض أن A عبارة عن مصفوفة- بفارق أنه لا يُمكِن اِستخدَام list.get(N)‎ بالجانب الأيسر من أي تَعْليمَة إِسْناد (assignment statement). list.set(N, obj)‎: يُسنِد الكائن obj إلى عنصر المصفوفة بالمَوْضِع N أي يَحلّ ذلك الكائن محلّ الكائن المُخزَّن مُسْبَقًا بذلك الموضع. لابُدّ أن يَكُون المُعامل obj من النوع T كما ينبغي أن يَكُون N عددًا صحيحًا (integer) يتراوح بين 0 و list.size()-1. في الواقع، تُشبِه تلك الدالة الأمر A[N] = obj بِفَرْض أن A عبارة عن مصفوفة. list.clear()‎: يحذِف جميع العناصر الموجودة بالقائمة، ويَضبُط حجمها إلى الصفر. list.remove(N)‎: يَحذِف العنصر الموجود بالمَوْضِع N من القائمة، ويُنقِص حجمها بمقدار الواحد كما يَنقِل العناصر الموجودة بَعْد العنصر المحذوف مَوْضِعًا للأعلى. لاحِظ أن المُعامل N لابُدّ أن يَكُون عددًا صحيحًا (integer) يتراوح بين 0 و list.size()-1. list.remove(obj)‎: يَحذِف الكائن المُمرَّر من القائمة إذا كان موجودًا بها، ويُنقِص حجمها بمقدار الواحد كما يَنقِل العناصر الموجودة بَعْد العنصر المحذوف مَوضِعًا للأعلى. لاحِظ أنه في حالة وجود الكائن obj أكثر من مرة ضِمْن القائمة، فإنه يَحذِف أول حدوث له فقط أما إذا لم يَكُن موجودًا، فإنه لا يَفعَل أي شيء أي لا يُعدّ ذلك بمثابة خطأ. list.indexOf(obj)‎: يَبحَث عن الكائن obj داخل القائمة، ويُعيد أول مَوْضِع لحدوثه إذا كان موجودًا أما إذا لم يَكُن موجودًا، فإنه يُعيِد العدد -1. ملحوظة: يَستخدِم آخر تابعين الاستدعاء obj.equals(item)‎ لموازنة obj -لو لم يَكُن obj فارغًا- مع عناصر القائمة أي أنهما يَفْحَصا تَساوِي السَلاسِل النصية بِفْحَص محتوياتها وليس مَواضِعها بالذاكرة. تُوفِّر جافا أصنافًا كثيرة ذات مُعاملات غَيْر مُحدَّدة النوع (parameterized classes) لتمثيل هياكل البيانات المختلفة (data structure)، وتُشكِّل تلك الأصناف إطار جافا للتجميعات (Java Collection Framework). لقد ناقشنا هنا الصَنْف ArrayList فقط، ولكننا سنُعود لمناقشة هذا الموضوع على نحو أكثر تفصيلًا بالفصل 10. ملحوظة: يُستخدَم الصَنْف ArrayList أيضًا بشكل عادي بدون مُعاملات غَيْر مُحدَّدة النوع (non-parametrized) أي يُمكنِنا أن نُصرِّح (declare) عن مُتْغيرِّات، ونُنشِئ كائنات من النوع ArrayList كالتالي: ArrayList list = new ArrayList(); يُكافِئ ذلك التّصرِيح عن مُتْغيٍّر list من النوع ArrayList<Object>‎. يَعنِي ذلك أن list يُمكِنه أن يَحمِل أي كائن ينتمي إلى صَنْف فرعي من Object. ولأن أي صَنْف هو بالنهاية صَنْف فرعي (subclass) من Object، فإنه من الممكن إضافة أي كائن (object) إلى list. الأصناف المُغلِّفة (Wrapper Classes) كما أوضحنا مُسْبَقًا، لا تتوافق الأنواع ذات المعاملات غير محدَّدة النوع (parameterized types) مع الأنواع الأساسية (primitive types) أي لا يُمكِنك مثلًا أن تَستخدِم شيئًا مثل ArrayList<int>‎. مع ذلك، نظرًا لتوفُّر أصناف مُغلِّفة (wrapper classes) مثل Integer و Character، فلربما الأمر ليس مُقيَدًا تمامًا. لقد تَعرضنا بالقسم 2.5 للأصناف Double و Integer. تُعرِّف تلك الأصناف التوابع الساكنة Double.parseDouble()‎ و Integer.parseInteger()‎ المُستخدَمة لتَحْوِيل سِلسِلة نصية (string) إلى قيمة عددية. تَتَضمَّن تلك الأصناف أيضًا ثوابتًا (constants) مثل Integer.MAX_VALUE و Double.NaN. لقد تَعرضنا أيضًا للصَنْف Character ببعض الأمثلة. يُعرِّف ذلك الصَنْف التابع الساكن Character.isLetter()‎ المُستخدَم لفْحَص ما إذا كانت قيمة معينة من النوع char عبارة عن حرف أبجدي (letter) أم لا. تَتوفَّر في الواقع أصناف مشابهة لكل نوع أساسي (primitive type) مثل Long و Short و Byte و Float و Boolean. تُعدّ جميع تلك الأصناف أصنافًا مُغلِّفة (wrapper classes)، وعلى الرغم من أنها تَحوِي بعض الأعضاء الساكنة (static members) المفيدة عمومًا، فإنها تُستخدَم أيضًا لغرض آخر: تَمثيِل الأنواع الأساسية ككائنات (objects). تذكَّر دومًا أن الأنواع الأساسية ليست أصنافًا (classes)، وأن قيم تلك الأنواع ليست كائنات (objects). مع ذلك، قد يَكُون من المفيد أحيانًا التَعامُل مع قيمة من نوع أساسي كما لو كانت كائنًا (object)، فمثلًا قد نرغب بتَخْزِين قيمة من نوع أساسي (primitive type) داخل قائمة من النوع ArrayList. لا نستطيع في الواقع فِعِل ذلك حرفيًا، ولكن يُمكِننا التَحايُل على ذلك قليلًا بتَغْليف (wrap) تلك القيمة داخل كائن ينتمي إلى صَنْف مُغلِّف (wrapper classes). على سبيل المثال، يحتوي أي كائن من النوع Double على مُتْغيِّر نُسخة (instance variable) وحيد من النوع double. يَعمَل ذلك الكائن بمثابة مُغلِّف (wrapper) للقيمة العددية. يُمكِنك إذًا أن تُنشِئ كائنًا (object) لتَغليف قيمة من النوع double مثل 6.0221415e23 كالتالي: Double d = new Double(6.0221415e23); في الواقع، يَحتوِي d على نفس المعلومات التي تَحتوِيها قيمة من النوع double، ولكنه كائن (object). يُمكِنك أن تَستدِعي d.doubleValue()‎ لاسترجاع القيمة العددية المُغلَّفة داخل الكائن. بالمثل، يُمكِنك أن تُغلِّف قيمة من النوع int ضِمْن كائن من النوع Integer أو قيمة من النوع boolean ضِمْن كائن من النوع Boolean، وهكذا. إذا كنت تريد أن تُنشِئ كائنًًا من النوع Double لتَغْلِيف قيمة عددية x، يُفضَّل أن تَستخدِم التابع الساكن Double.valueOf(x)‎ بدلًا من أن تَستدعِي الباني new Double(x)‎؛ لأن استدعاء التابع Double.valueOf()‎ بنفس قيمة المُعامل (parameter) أكثر من مرة دائمًا ما يُعيِد نفس ذات الكائن. تحديدًا، بَعْد أول استدعاء لذلك التابع بقيمة مُعامِل معينة، فإنه سيُعيد نفس ذلك الكائن بالاستدعاءات المتتالية لنفس قيمة المُعامل. لا يُعدّ ذلك مشكلة كما قد تَظّن؛ لأن الكائنات من النوع Double غَيْر قابلة للتَعْدِيل (immutable) أي أن الكائنات التي لها نفس القيمة المبدئية ستَظِلّ دائمًا متطابقة. يُعدّ التابع Double.valueOf تابعًا مُصنِّعًا (factory method) مثل تلك التوابع التي تَعرَّضنا لها بالقسم 6.2 أثناء تَعامُلنا مع الصَنْفين Color و Font، وتحتوي جميع الأصناف المُغلِّفة عمومًا على تابع مُصنِّع مشابه مثل Character.valueOf(ch)‎ و Integer.valueOf(n)‎. يَتَوفَّر أيضًا تحويل أتوماتيكي بين كل نوع أساسي (primitive type) وصَنْفه المُغلِّف (wrapper class) لتسهيل الاِستخدَام. على سبيل المثال، إذا كنت تَستخدِم قيمة من النوع int ضِمْن سياق يَتَطلَّب كائنًا (object) من النوع Integer، يُمكِنك أن تُغلِّف تلك القيمة ضِمْن كائن من النوع Integer أتوماتيكيًا كالتالي: Integer answer = 42; سيقرأ الحاسوب التَعْليمَة بالأعلى كما لو كانت مَكْتُوبة كالتالي: Integer answer = Integer.valueOf(42); يُطلَق على ذلك اسم "التغليف الأوتوماتيكي (autoboxing)"، ويَعمَل من الجهة الآخرى أيضًا. فمثلًا، إذا كان d يُشير إلى كائن من النوع Double، يُمكِنك أن تَستخدِم d بتعبير عددي (numerical expression) مثل 2*d. تُفَك (unboxing) القيمة العددية داخل d أتوماتيكيًا، ويُحسَب حاصل ضربها مع العدد 2. كثيرًا ما يَسمَح لك التَغْلِيف الأتوماتيكي (autoboxing) وفكه بتَجاهُل الفارق بين الأنواع الأساسية (primitive types) والكائنات (objects). يُعدّ ما سبق صحيحًا فيما يَتَعلَّق بالأنواع ذات المعاملات غَيْر مُحدَّدة النوع (parameterized types). على الرغم من عدم توفُّر النوع ArrayList<int>‎، يَتَوفَّر ArrayList<Integer>‎ حيث تَحمِل مصفوفة من ذلك النوع كائنات من النوع Integer التي تُمثِل مُجرّد قيمة من النوع int ضِمْن مُغلِّف صغير. لنَفْترِض أن لدينا كائن (object) من النوع ArrayList<Integer>‎، اُنظر التالي: ArrayList<Integer> integerList; integerList = new ArrayList<Integer>(); يُمكِننا الآن أن نُضيف كائنًا مُمثلًا للعدد 42 مثلًا إلى integerList كالتالي: integerList.add( Integer.valueOf(42) ); أو قد نعتمد على التَغْلِيف الأتوماتيكي (autoboxing) كالتالي: integerList.add( 42 ); سيُغلِّف المُصرِّف (compiler) العدد 42 أتوماتيكيًا داخل كائن (object) من النوع Integer قبل إضافته إلى القائمة. بالمثل، يُمكِننا كتابة التالي: int num = integerList.get(3); يُعيد التابع integerList.get(3)‎ قيمة من النوع Integer، ولكن بِفَضل خاصية فك التَغْلِيف (unboxing)، سيُحوِّل المُصرِّف (compiler) القيمة المُعادة (return value) أتوماتيكيًا إلى قيمة من النوع int كما لو كنا قد كتبنا ما يلي: int num = integerList.get(3).intValue(); نستطيع إذًا أن نَستخدِم المصفوفة integerList كما لو كانت مصفوفة ديناميكية (dynamic array) من النوع int لا من النوع Integer. وفي العموم، يَنطبِق نفس الأمر على القوائم (lists) من الأصناف المُغلِّفة الآخرى مثل ArrayList<Double>‎ و ArrayList<Character>‎. يَتَبقَّى لنا مشكلة أخيرة: يُمكِن لأي قائمة أن تَحمِل القيمة null، ولكن لا تَتَوفَّر قيمة من النوع الأساسي (primitive type) مناظرة للقيمة null. وبالتالي، إذا أعاد استدعاء مثل integerList.get(3)‎ ضِمْن التَعْليمَة int num = integerList.get(3);‎ القيمة null، سيَحدُث اِعتراض مُتعلِّق بوجود مُؤشر فارغ (null pointer). ينبغي إذًا أن تَأخُذ ذلك بالحسبان إذا لم تَكُن متأكدًا بشأن وجود قيم فارغة ضِمْن القائمة. البرمجة باستخدام ArrayList كمثال بسيط، سنُعيِد كتابة البرنامج ReverseWithDynamicArray.java من القسم السابق باِستخدَام النوع ArrayList بدلًا من الصَنْف المُخصَّص (custom) الذي كنا قد كتبناه. نظرًا لأننا سنُخزِّن أعدادً صحيحة (integers) ضِمْن القائمة، سنَستخدِم إذًا النوع ArrayList<Integer>‎. اُنظر شيفرة البرنامج بالكامل: import textio.TextIO; import java.util.ArrayList; public class ReverseWithArrayList { public static void main(String[] args) { ArrayList<Integer> list; list = new ArrayList<Integer>(); System.out.println("Enter some non-zero integers. Enter 0 to end."); while (true) { System.out.print("? "); int number = TextIO.getlnInt(); if (number == 0) break; list.add(number); } System.out.println(); System.out.println("Your numbers in reverse are:"); for (int i = list.size() - 1; i >= 0; i--) { System.out.printf("%10d%n", list.get(i)); } } } كما أوضحنا مُسْبَقًا، عادةً ما تُستخدَم حَلْقة التَكْرار for لمعالجة المصفوفات الديناميكية من النوع ArrayList بنفس طريقة مُعالجة المصفوفات العادية. على سبيل المثال، تَطبَع حَلْقة التَكْرار (loop) التالية جميع العناصر بالمصفوفة namelist من النوع ArrayList<String>‎: for ( int i = 0; i < namelist.size(); i++ ) { String item = namelist.get(i); System.out.println(item); } يُمكِنك أيضًا أن تَستخدِم حَلْقة التَكْرار for-each مع المصفوفات من النوع ArrayList كالتالي: for ( String item : namelist ) { System.out.println(item); } عند التَعامُل مع الأصناف المُغلِّفة (wrapper classes)، يُمكن للمُتْغيِّر المُتحكِّم بحَلْقة التَكْرار for-each أن يَكُون من النوع الأساسي (primitive type) بفضل خاصية فك التغليف (unboxing). على سبيل المثال، إذا كان numbers مُتْغيِّر من النوع ArrayList<Double>‎، يُمكِنك أن تَستخدِم حَلْقة التَكْرار التالية لحِسَاب حاصل مجموع القيم بالقائمة: double sum = 0; for ( double num : numbers ) { sum = sum + num; } ستَعمَل الشيفرة بالأعلى طالما لم يَكُن هناك أي عنصر ضِمْن القائمة مُساوِي للقيمة null. إذا كان هنالك احتمالية لحُدوث ذلك، فينبغي أن تَستخدِم مُتْغيِّرًا مُتحكِّمًا بالحَلْقة (loop control variable) من النوع Double ثُمَّ تَفْحَص ما إذا كانت قيمته فارغة أم لا. على سبيل المثال، تَحسِب الشيفرة التالية حاصل مجموع القيم غَيْر الفارغة بالقائمة: double sum; for ( Double num : numbers ) { if ( num != null ) { // يتم فك التغليف للحصول على قيمة‫ double sum = sum + num; } } سنَفْحَص الآن البرنامج SimplePaint2.java الذي يُعدّ نسخة مُحسَّنة من البرنامج SimplePaint.java الذي كتبناه بالقسم الفرعي 6.3.3. بالبرنامج الجديد، يستطيع المُستخدِم أن يَرسِم منحنيات داخل مساحة رسم (drawing area) بالنَقْر على زر الفأرة والسَحب كما يستطيع أن يختار كُلًا من اللون المُستخدَم للرسم ولون خلفية مساحة الرسم من قائمة. تحتوي أيضًا قائمة "Control" على عدة أوامر منها: الأمر "Undo" المُستخدَم لحَذْف آخر منحنى رسمه المُستخدِم على الشاشة، والأمر "Clear" المُستخدَم لحَذْف جميع المنحنيات. يَتَوفَّر أيضًا مربع الاختيار "Use Symmetry" المسئول عن تَفْعِيل خاصية التماثلية أو تَعْطِيلها. تَنعكِس المنحنيات التي يَرسِمها المُستخدِم أثناء تَفْعِيل تلك الخاصية أفقيًا ورأسيًا لتُنتِج نمطًا متماثلًا. بخلاف البرنامج الأصلي SimplePaint، تَستخدِم النسخة الجديدة هيكلًا بيانيًا (data structure) لتَخْزِين عدة معلومات عما رَسمه المُستخدِم. عندما يختار المُستخدِم لون خلفية جديد، تُملَئ الحاوية (canvas) بذلك اللون ثم يُعاد رَسْم جميع المنحنيات مجددًا على الخلفية الجديدة، ولذلك لابُدّ أن نُخزِّن كل المعلومات اللازمة لإعادة رَسْم تلك المنحنيات. بالمثل، عندما يختار المُستخدِم الأمر "Undo"، تُحَذَف بيانات آخر منحنى رسمه المُستخدِم ثُمَّ يُعاد رَسْم الصورة بالكامل مُجددًا بالاعتماد على البيانات المُتبقية. سنعتمد في العموم على الهيكل البياني ArrayList لتَخْزِين تلك البيانات. تَتَكوَّن البيانات الأساسية لأي منحنى من قائمة من النقط الواقعة على المنحنى، والتي سنُخزِّنها بكائن من النوع ArrayList<Point2D>‎. لاحِظ أن الصَنْف Point2D هو صَنْف قياسي (standard) مُعرَّف بحزمة javafx.geometry، ويُمكِننا أن نُنشِئ كائنًا منه باِستخدَام قيمتين من النوع double يُمثِلان الإحداثي x والإحداثي y. يَمتلك أي كائن من النوع Point2D تابعي جَلْب pt.getX()‎ و pt.getY()‎ لاسترجاع قيمة x و y. إلى جانب ذلك، سنحتاج إلى مَعرِفة لون المنحنى، وما إذا كان ينبغي تطبيق خاصية التماثلية عليه أم لا. سنُخزِّن جميع البيانات المطلوبة لرَسْم منحنى ضِمْن كائن من النوع CurveData، والذي سنُعرِّفه كصَنْف مُتْدِاخل (nested class) بالبرنامج: private static class CurveData { Color color; // لون المنحنى boolean symmetric; // خاصية التماثلية ArrayList<Point2D> points; // نقاط المنحنى } نظرًا لأن الصورة قد تَحتوِي على عدة منحنيات وليس فقط منحنى وحيد، ولأننا نحتاج إلى تَخْزِين كل البيانات الضرورية لإعادة رَسْم الصورة بالكامل، سنحتاج إذًا إلى قائمة من الكائنات من النوع CurveData. سيَستخدِم البرنامج المُتْغيِّر curves من النوع ArrayList لذلك الغرض، وسنُصرِّح عنه كالتالي: ArrayList<CurveData> curves = new ArrayList<CurveData>(); لدينا هنا قائمة من الكائنات (objects)، ويَحتوِي كل كائن منها على قائمة من النقط كجزء من البيانات المُعرَّفة بداخله. سنَفْحَص الآن عدة أمثلة على معالجة ذلك الهيكل البياني (data structure). أولًا، عندما يَنقُر المُستخدِم بزر الفأرة على مساحة الرسم (drawing surface)، يَعنِي ذلك أنه يَرسِم منحنى جديدًا، ولذلك ينبغي أن نُنشِئ كائنًا جديدًا من النوع CurveData، ونُهيئ جميع مُتْغيِّرات النُسخ (instance variables) المُعرَّفة بداخله. بَفْرَض أن currentCurve هو مُتْغيِّر عام (global) من النوع CurveData، يُمكِننا تعريف البرنامج mousePressed()‎ كالتالي: currentCurve = new CurveData(); // ‫أنشئ كائن من النوع CurveData // ‫يُنسخ اللون المستخدم لرسم المنحنى من متغير النسخة الممثل للون // المستخدم للرسم currentCurve.color = currentColor; // ‫تنسخ خاصية التماثلية أيضًا من القيمة الحالية للمتغير useSymmetry currentCurve.symmetric = useSymmetry; currentCurve.points = new ArrayList<Point2D>(); // A new point list object. عندما يَسحَب المُستخدِم الفأرة، ينبغي أن نُضيف نقطًا جديدة إلى المُتْغيِّر currentCurve، وأن نَرسِم قطعًا مستقيمة بين تلك النقط أثناء إضافتها. في المقابل، عندما يُحرِّر المُستخدِم الفأرة، يَكُون المنحنى قد اكتمل، وينبغي إذًا أن نُضيِفه إلى قائمة المنحنيات باستدعاء ما يلي: curves.add( currentCurve ); عندما يُغيِّر المُستخدِم لون الخلفية أو يختار الأمر "Undo"، ينبغي أن نُعيِد رَسْم الصورة. يَحتوِي البرنامج على التابع redraw()‎ المسئول عن إعادة رَسْم الصورة بالكامل. يَعتمِد التابع على البيانات الموجودة بالمُتْغيِّر curves لإعادة رَسْم جميع المنحنيات، ويَستخدِم حَلْقة تَكْرار for-each لمعالجة بيانات كل منحنى على حدى كالتالي: for ( CurveData curve : curves ) { . // ‫ارسم المنحنى الذي يمثله الكائن curve من النوع CurveData . } لاحِظ أن curve.points عبارة عن مُتْغيِّر من النوع ArrayList<Point2D>‎ أي عبارة عن قائمة من النقط الواقعة على المنحنى. يُمكِننا على سبيل المثال أن نَسترجِع النقطة i على المنحنى باستدعاء التابع get()‎ المُعرَّف بالقائمة curve.points.get(i)‎. يُعيِد ذلك الاستدعاء قيمة من النوع Point2D التي تُعرِّف بدورها توابع الجَلْب getX()‎ و getY()‎. يُمكِننا إذًا أن نُشير إلى الإحداثي x للنقطة i على المنحنى مباشرةً كالتالي: curve.points.get(i).getX() قد يبدو الاستدعاء السابق مُعقدًا، ولكنه مثال جيد على الأسماء المركبة (complex names). يُخصِّص ذلك الاسم مسارًا إلى جزء من البيانات: اذهب إلى الكائن curve. بداخل ذلك الكائن، إذهب إلى points. بداخل points، إذهب إلى العنصر برقم المَوْضِع i. من هذا العنصر، اِسترجِع الإحداثي x من خلال استدعاء تابعه getX()‎. اُنظر تعريف التابع redraw()‎ بالكامل: private void redraw() { g.setFill(backgroundColor); g.fillRect(0,0,canvas.getWidth(),canvas.getHeight()); for ( CurveData curve : curves ) { g.setStroke(curve.color); for (int i = 1; i < curve.points.size(); i++) { // ‫ارسم قطعة مستقيمة من رقم الموضع i-1 إلى رقم الموضع i double x1 = curve.points.get(i-1).getX(); double y1 = curve.points.get(i-1).getY(); double x2 = curve.points.get(i).getX(); double y2 = curve.points.get(i).getY(); drawSegment(curve.symmetric,x1,y1,x2,y2); } } } يَرسِم التابع drawSegment()‎ قطعة مستقيمة من (x1,y1) إلى (x2,y2). بالإضافة إلى ذلك، إذا كان المُعامل (parameter) الأول يُساوِي true، فإنه أيضًا يَرسِم انعكاسًا أفقيًا ورأسيًا لتلك القطعة. لقد رَكَّزنا هنا على اِستخدَام الصَنْف ArrayList ضِمْن البرنامج، ولكن يُفضَّل بالطبع أن تَتَطلِع على الشيفرة الكاملة للبرنامج SimplePaint2.java. بالإضافة إلى كَوْنه مثال على اِستخدَام الأنواع ذات المُعاملات غَيْر محدَّدة النوع (parameterized types)، فإنه يُعدّ أيضًا مثالًا جيدًا على إنشاء القوائم (menus) واِستخدَامها. ينبغي عمومًا أن تَكُون قادرًا على فهم البرنامج بالكامل. ترجمة -بتصرّف- للقسم Section 3: ArrayList من فصل Chapter 7: Arrays and ArrayLists من كتاب Introduction to Programming Using Java.
    1 نقطة
×
×
  • أضف...