JavaRush /مدونة جافا /Random-AR /مقارنة الكائنات: الممارسة
articles
مستوى

مقارنة الكائنات: الممارسة

نشرت في المجموعة
هذه هي المقالة الثانية المخصصة لمقارنة الأشياء. ناقش الأول منهم الأساس النظري للمقارنة - كيف يتم ذلك ولماذا وأين يتم استخدامه. في هذه المقالة سنتحدث مباشرة عن مقارنة الأرقام والأشياء والحالات الخاصة والدقائق والنقاط غير الواضحة. وبتعبير أدق، إليك ما سنتحدث عنه:
مقارنة الأشياء: الممارسة - 1
  • مقارنة السلسلة: ' ==' وequals
  • طريقةString.intern
  • مقارنة البدائيين الحقيقيين
  • +0.0و-0.0
  • معنىNaN
  • جافا 5.0. طرق التوليد والمقارنة عبر ' =='
  • جافا 5.0. العلبة التلقائية/إلغاء العلبة: ' ==' و ' >=' و ' <=' لأغلفة الكائنات.
  • جافا 5.0. مقارنة عناصر التعداد (النوع enum)
اذا هيا بنا نبدأ!

مقارنة السلسلة: ' ==' وequals

اه هذه الخطوط... من أكثر الأنواع استخداما و التي تسبب الكثير من المشاكل. من حيث المبدأ، هناك مقال منفصل عنهم . وهنا سأتطرق إلى قضايا المقارنة. بالطبع، يمكن مقارنة السلاسل باستخدام equals. علاوة على ذلك، يجب مقارنتها عبر equals. ومع ذلك، هناك الفروق الدقيقة التي تستحق المعرفة. أولًا، السلاسل المتطابقة هي في الواقع كائن واحد. يمكن التحقق من ذلك بسهولة عن طريق تشغيل الكود التالي:
String str1 = "string";
String str2 = "string";
System.out.println(str1==str2 ? "the same" : "not the same");
وستكون النتيجة نفسها" . مما يعني أن مراجع السلسلة متساوية. ويتم ذلك على مستوى المترجم، ومن الواضح أنه لحفظ الذاكرة. يقوم المترجم بإنشاء مثيل واحد من السلسلة، ويقوم بتعيين str1مرجع str2لهذا المثيل. ومع ذلك، ينطبق هذا فقط على السلاسل التي تم تعريفها كأحرف حرفية في التعليمات البرمجية. إذا قمت بتأليف سلسلة من القطع، فإن الرابط إليها سيكون مختلفًا. التأكيد - هذا المثال:
String str1 = "string";
String str2 = "str";
String str3 = "ing";
System.out.println(str1==(str2+str3) ? "the same" : "not the same");
وستكون النتيجة "ليست هي نفسها" . يمكنك أيضًا إنشاء كائن جديد باستخدام مُنشئ النسخ:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1==str2 ? "the same" : "not the same");
وستكون النتيجة أيضًا "ليست هي نفسها" . وبالتالي، في بعض الأحيان يمكن مقارنة السلاسل من خلال المقارنة المرجعية. لكن من الأفضل عدم الاعتماد على هذا. أود أن أتطرق إلى طريقة مثيرة جدًا للاهتمام تتيح لك الحصول على ما يسمى بالتمثيل الأساسي للسلسلة - String.intern. دعونا نتحدث عن ذلك بمزيد من التفصيل.

طريقة String.intern

لنبدأ بحقيقة أن الفصل Stringيدعم تجمع السلاسل. تتم إضافة كافة القيم الحرفية للسلسلة المحددة في الفئات، وليس فقط هذه، إلى هذا التجمع. لذا، تسمح لك الطريقة internبالحصول على سلسلة من هذا التجمع تساوي السلسلة الموجودة (التي تسمى عليها الطريقة intern) من وجهة نظر equals. إذا لم يكن هذا الصف موجودًا في التجمع، فسيتم وضع الصف الموجود هناك وإرجاع رابط إليه. وبالتالي، حتى لو كانت المراجع إلى سلسلتين متساويتين مختلفتين (كما في المثالين أعلاه)، فإن استدعاءات هذه السلاسل internسترجع مرجعًا إلى نفس الكائن:
String str1 = "string";
String str2 = new String("string");
System.out.println(str1.intern()==str2.intern() ? "the same" : "not the same");
ستكون نتيجة تنفيذ هذا الجزء من التعليمات البرمجية "نفسها" . لا أستطيع أن أقول بالضبط لماذا تم القيام بذلك بهذه الطريقة. هذه الطريقة internأصلية، وبصراحة، لا أريد الدخول في براري كود C. على الأرجح يتم ذلك لتحسين استهلاك الذاكرة والأداء. على أية حال، من المفيد معرفة ميزة التنفيذ هذه. دعنا ننتقل إلى الجزء التالي.

مقارنة البدائيين الحقيقيين

في البداية، أريد أن أطرح سؤالا. بسيط جدا. ما هو المبلغ التالي - 0.3f + 0.4f؟ لماذا؟ 0.7f؟ دعونا تحقق:
float f1 = 0.7f;
float f2 = 0.3f + 0.4f;
System.out.println("f1==f2: "+(f1==f2));
نتيجة ل؟ يحب؟ أنا أيضاً. ولمن لم يكمل هذا الجزء أقول أن النتيجة ستكون...
f1==f2: false
لماذا يحدث هذا؟.. فلنجري اختبارًا آخر:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("f1="+(double)f1);
System.out.println("f2="+(double)f2);
System.out.println("f3="+(double)f3);
System.out.println("f4="+(double)f4);
لاحظ التحويل إلى double. يتم ذلك من أجل إخراج المزيد من المنازل العشرية. نتيجة:
f1=0.30000001192092896
f2=0.4000000059604645
f3=0.7000000476837158
f4=0.699999988079071
بالمعنى الدقيق للكلمة، النتيجة يمكن التنبؤ بها. يتم تمثيل الجزء الكسري باستخدام سلسلة محدودة 2-ن، وبالتالي ليست هناك حاجة للحديث عن التمثيل الدقيق لعدد تم اختياره بشكل تعسفي. كما يتبين من المثال، دقة التمثيل floatهي 7 منازل عشرية. بالمعنى الدقيق للكلمة، يخصص التمثيل float 24 بت للجزء العشري. وبالتالي، فإن الحد الأدنى للعدد المطلق الذي يمكن تمثيله باستخدام float (دون مراعاة الدرجة، لأننا نتحدث عن الدقة) هو 2-24≈6*10-8. وبهذه الخطوة تنتقل القيم الموجودة في التمثيل فعليًا float. وبما أن هناك تكميمًا، فهناك خطأ أيضًا. ومن هنا الاستنتاج: لا يمكن مقارنة الأرقام الموجودة في التمثيل floatإلا بدقة معينة. أوصي بتقريبها إلى المنزلة العشرية السادسة (10-6)، أو يفضل التحقق من القيمة المطلقة للفرق بينهما:
float f1 = 0.3f;
float f2 = 0.4f;
float f3 = f1 + f2;
float f4 = 0.7f;
System.out.println("|f3-f4|<1e-6: "+( Math.abs(f3-f4) < 1e-6 ));
وفي هذه الحالة النتيجة مشجعة:
|f3-f4|<1e-6: true
وبطبيعة الحال، الصورة هي نفسها تماما مع النوع double. والفرق الوحيد هو أنه تم تخصيص 53 بت للجزء العشري، وبالتالي فإن دقة التمثيل هي 2-53≈10-16. نعم، قيمة التكميم أصغر بكثير، لكنها موجودة. ويمكن أن تلعب نكتة قاسية. بالمناسبة، في مكتبة اختبار JUnit ، في طرق مقارنة الأعداد الحقيقية، يتم تحديد الدقة بشكل صريح. أولئك. تحتوي طريقة المقارنة على ثلاث معلمات - الرقم وما يجب أن يساويه ودقة المقارنة. بالمناسبة أود أن أذكر الخفايا المرتبطة بكتابة الأرقام بشكل علمي مع الإشارة إلى الدرجة. سؤال. كيف تكتب 10-6؟ الممارسة تبين أن أكثر من 80% يجيبون – 10e-6. وفي الوقت نفسه، الإجابة الصحيحة هي 1e-6! و10e-6 هو 10-5! لقد صعدنا على هذه أشعل النار في أحد المشاريع، بشكل غير متوقع تمامًا. لقد بحثوا عن الخطأ لفترة طويلة جدًا، ونظروا إلى الثوابت 20 مرة، ولم يكن لدى أحد أدنى شك في صحتها، حتى يوم واحد، عن طريق الصدفة إلى حد كبير، تمت طباعة الثابت 10e-3 ووجدوا اثنين أرقام بعد العلامة العشرية بدلاً من الثلاثة المتوقعة. لذلك كن حذرا! هيا لنذهب.

+0.0 و -0.0

في تمثيل الأعداد الحقيقية، يتم توقيع الجزء الأكثر أهمية. ماذا يحدث إذا كانت جميع البتات الأخرى 0؟ على عكس الأعداد الصحيحة، حيث تكون النتيجة في مثل هذه الحالة رقمًا سالبًا يقع عند الحد الأدنى لنطاق التمثيل، فإن الرقم الحقيقي الذي يحتوي على البت الأكثر أهمية فقط على 1 يعني أيضًا 0، فقط مع علامة الطرح. وبالتالي، لدينا صفرين - +0.0 و -0.0. يطرح سؤال منطقي: هل يجب اعتبار هذه الأرقام متساوية؟ الآلة الافتراضية تفكر بهذه الطريقة بالضبط. ومع ذلك، فإن هذين رقمين مختلفين ، لأنه نتيجة للعمليات معهم يتم الحصول على قيم مختلفة:
float f1 = 0.0f/1.0f;
float f2 = 0.0f/-1.0f;
System.out.println("f1="+f1);
System.out.println("f2="+f2);
System.out.println("f1==f2: "+(f1==f2));
float f3 = 1.0f / f1;
float f4 = 1.0f / f2;
System.out.println("f3="+f3);
System.out.println("f4="+f4);
...والنتيجة:
f1=0.0
f2=-0.0
f1==f2: true
f3=Infinity
f4=-Infinity
لذلك، في بعض الحالات يكون من المنطقي التعامل مع +0.0 و-0.0 كرقمين مختلفين. وإذا كان لدينا كائنين، في أحدهما الحقل +0.0، وفي الآخر -0.0، فيمكن أيضًا اعتبار هذين الكائنين غير متساويين. السؤال الذي يطرح نفسه - كيف يمكنك أن تفهم أن الأرقام غير متساوية إذا كانت مقارنتها المباشرة بالجهاز الظاهري تعطيك true؟ الجواب هو هذا. على الرغم من أن الآلة الافتراضية تعتبر هذه الأرقام متساوية، إلا أن تمثيلها لا يزال مختلفًا. ولذلك، فإن الشيء الوحيد الذي يمكن القيام به هو مقارنة وجهات النظر. ومن أجل الحصول عليه، هناك طرق int Float.floatToIntBits(float)و long Double.doubleToLongBits(double)، والتي ترجع تمثيل البت في النموذج intوعلى longالتوالي (استمرار للمثال السابق):
int i1 = Float.floatToIntBits(f1);
int i2 = Float.floatToIntBits(f2);
System.out.println("i1 (+0.0):"+ Integer.toBinaryString(i1));
System.out.println("i2 (-0.0):"+ Integer.toBinaryString(i2));
System.out.println("i1==i2: "+(i1 == i2));
ستكون النتيجة
i1 (+0.0):0
i2 (-0.0):10000000000000000000000000000000
i1==i2: false
وبالتالي، إذا كان لديك رقمين مختلفين +0.0 و-0.0، فيجب عليك مقارنة المتغيرات الحقيقية من خلال تمثيل البت الخاص بها. يبدو أننا قمنا بفرز +0.0 و -0.0. لكن -0.0 ليست المفاجأة الوحيدة. وهناك أيضا شيء من هذا القبيل...

قيمة نان

NaNتمثل Not-a-Number. تظهر هذه القيمة نتيجة لعمليات حسابية غير صحيحة، على سبيل المثال، قسمة 0.0 على 0.0، أو ما لا نهاية على ما لا نهاية، وما إلى ذلك. خصوصية هذه القيمة هي أنها لا تساوي نفسها. أولئك.:
float x = 0.0f/0.0f;
System.out.println("x="+x);
System.out.println("x==x: "+(x==x));
.. سينتج ...
x=NaN
x==x: false
كيف يمكن أن يحدث هذا عند مقارنة الأشياء؟ إذا كان مجال الكائن يساوي NaN، فستعطي المقارنة false، أي. يتم ضمان اعتبار الأشياء غير متساوية. على الرغم من أننا، منطقيا، قد نريد العكس تماما. يمكنك تحقيق النتيجة المرجوة باستخدام الطريقة Float.isNaN(float). يتم إرجاعه trueإذا كانت الوسيطة NaN. في هذه الحالة، لن أعتمد على مقارنة تمثيلات البتات، لأن لم يتم توحيدها. ربما هذا يكفي بشأن البدائيين. دعنا ننتقل الآن إلى التفاصيل الدقيقة التي ظهرت في Java منذ الإصدار 5.0. والنقطة الأولى التي أود أن أتطرق إليها هي

جافا 5.0. طرق التوليد والمقارنة عبر ' =='

هناك نمط في التصميم يسمى طريقة الإنتاج. في بعض الأحيان يكون استخدامه أكثر ربحية من استخدام المنشئ. اسمحوا لي أن أقدم لكم مثالا. أعتقد أنني أعرف قذيفة الكائن جيدًا Boolean. هذه الفئة غير قابلة للتغيير ويمكن أن تحتوي على قيمتين فقط. وهذا هو، في الواقع، لأي احتياجات، نسختان فقط كافية. وإذا قمت بإنشائها مسبقًا ثم قمت بإعادتها ببساطة، فسيكون ذلك أسرع بكثير من استخدام المنشئ. هناك مثل هذه الطريقة Boolean: valueOf(boolean). ظهرت في الإصدار 1.4. تم تقديم طرق إنتاج مماثلة في الإصدار 5.0 في الفئات Byteو Characterو Shortو Integerو Long. عندما يتم تحميل هذه الفئات، يتم إنشاء صفائف مثيلاتها المقابلة لنطاقات معينة من القيم الأولية. هذه النطاقات هي كما يلي:
مقارنة الأشياء: الممارسة - 2
هذا يعني أنه عند استخدام الطريقة، valueOf(...)إذا كانت الوسيطة تقع ضمن النطاق المحدد، فسيتم إرجاع نفس الكائن دائمًا. ربما هذا يعطي بعض الزيادة في السرعة. ولكن في الوقت نفسه، تنشأ مشاكل ذات طبيعة تجعل من الصعب جدًا الوصول إلى جوهرها. اقرأ المزيد عنها. من الناحية النظرية، تمت إضافة طريقة الإنتاج إلى كل من الفئتين و . يقول وصفهم أنه إذا لم تكن بحاجة إلى نسخة جديدة، فمن الأفضل استخدام هذه الطريقة، لأنه يمكن أن يعطي زيادة في السرعة، الخ. وما إلى ذلك وهلم جرا. ومع ذلك، في التنفيذ الحالي (Java 5.0)، يتم إنشاء مثيل جديد بهذه الطريقة، أي. استخدامه غير مضمون لإعطاء زيادة في السرعة. علاوة على ذلك، من الصعب علي أن أتخيل كيف يمكن تسريع هذه الطريقة، لأنه بسبب استمرارية القيم، لا يمكن تنظيم ذاكرة التخزين المؤقت هناك. باستثناء الأعداد الصحيحة. يعني بدون الجزء الكسري.valueOfFloatDouble

جافا 5.0. العلبة التلقائية/إلغاء العلبة: ' ==' و ' >=' و ' <=' لأغلفة الكائنات.

أظن أنه تمت إضافة أساليب الإنتاج وذاكرة التخزين المؤقت للمثيلات إلى الأغلفة للأعداد الصحيحة الأولية لتحسين العمليات autoboxing/unboxing. اسمحوا لي أن أذكركم ما هو عليه. إذا كان يجب أن يكون هناك كائن متضمن في عملية ما، ولكن هناك عنصرًا أساسيًا، فسيتم تغليف هذا الكائن تلقائيًا في غلاف الكائن. هذا autoboxing. والعكس صحيح - إذا كان من الضروري إشراك البدائي في العملية، فيمكنك استبدال غلاف الكائن هناك، وسيتم توسيع القيمة تلقائيًا منه. هذا unboxing. وبطبيعة الحال، عليك أن تدفع ثمن هذه الراحة. تعمل عمليات التحويل التلقائي على إبطاء التطبيق إلى حد ما. لكن هذا لا علاقة له بالموضوع الحالي، فلنترك هذا السؤال. كل شيء على ما يرام طالما أننا نتعامل مع العمليات التي ترتبط بشكل واضح بالأوليات أو الأصداف. ماذا سيحدث للعملية ' =='؟ لنفترض أن لدينا كائنين Integerلهما نفس القيمة في الداخل. كيف سيتم المقارنة؟
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1==i2: "+(i1==i2));
نتيجة:
i1==i2: false

Кто бы сомневался... Сравниваются они How an objectы. А если так:Integer i1 = 1;
Integer i2 = 1;
System.out.println("i1==i2: "+(i1==i2));
نتيجة:
i1==i2: true
الآن هذا أكثر إثارة للاهتمام! إذا autoboxingتم إرجاع نفس الكائنات! هذا هو المكان الذي يكمن فيه الفخ. بمجرد أن نكتشف أن نفس الكائنات قد تم إرجاعها، سنبدأ في إجراء التجارب لمعرفة ما إذا كان هذا هو الحال دائمًا. وكم عدد القيم التي سنتحقق منها؟ واحد؟ عشرة؟ مائة؟ على الأرجح سنقتصر على مائة في كل اتجاه حول الصفر. ونحصل على المساواة في كل مكان. يبدو أن كل شيء على ما يرام. ومع ذلك، ننظر إلى الوراء قليلا، هنا . هل خمنت ما هو المصيد؟.. نعم، يتم إنشاء مثيلات الكائنات أثناء العلبة التلقائية باستخدام طرق الإنتاج. ويتضح ذلك جيدًا من خلال الاختبار التالي:
public class AutoboxingTest {

    private static final int numbers[] = new int[]{-129,-128,127,128};

    public static void main(String[] args) {
        for (int number : numbers) {
            Integer i1 = number;
            Integer i2 = number;
            System.out.println("number=" + number + ": " + (i1 == i2));
        }
    }
}
وستكون النتيجة مثل هذا:
number=-129: false
number=-128: true
number=127: true
number=128: false
بالنسبة للقيم التي تقع ضمن نطاق التخزين المؤقت ، يتم إرجاع كائنات متطابقة، أما بالنسبة للقيم التي تقع خارج نطاق التخزين المؤقت، فسيتم إرجاع كائنات مختلفة. وبالتالي، إذا تمت مقارنة الأصداف في مكان ما في التطبيق بدلاً من البدائيات، فهناك فرصة للحصول على الخطأ الأكثر فظاعة: خطأ عائم. لأنه من المرجح أيضًا أن يتم اختبار الكود على نطاق محدود من القيم التي لن يظهر فيها هذا الخطأ. لكن في العمل الحقيقي، إما أن تظهر أو تختفي، اعتمادًا على نتائج بعض الحسابات. من الأسهل أن تصاب بالجنون بدلاً من العثور على مثل هذا الخطأ. لذلك أنصحك بتجنب التشغيل التلقائي قدر الإمكان. وهذا ليس كل شيء. دعونا نتذكر الرياضيات، ليس أبعد من الصف الخامس. السماح لعدم المساواة A>=Bو А<=B. ماذا يمكن أن يقال عن العلاقة Aو B؟ هناك شيء واحد فقط - إنهم متساوون. هل توافق؟ اعتقد نعم. لنجري الاختبار:
Integer i1 = new Integer(1);
Integer i2 = new Integer(1);
System.out.println("i1>=i2: "+(i1>=i2));
System.out.println("i1<=i2: "+(i1<=i2));
System.out.println("i1==i2: "+(i1==i2));
نتيجة:
i1>=i2: true
i1<=i2: true
i1==i2: false
وهذا هو أكبر شيء غريب بالنسبة لي. لا أفهم على الإطلاق سبب إدخال هذه الميزة في اللغة إذا كانت تقدم مثل هذه التناقضات. بشكل عام، سأكرر مرة أخرى - إذا كان من الممكن الاستغناء عنها autoboxing/unboxing، فإن الأمر يستحق استغلال هذه الفرصة على أكمل وجه. الموضوع الأخير الذي أود أن أتطرق إليه هو... Java 5.0. مقارنة عناصر التعداد (نوع التعداد) كما تعلم، منذ الإصدار 5.0، قدمت Java نوعًا مثل التعداد - التعداد. تحتوي مثيلاتها بشكل افتراضي على الاسم ورقم التسلسل في إعلان المثيل في الفصل. وعليه، عندما يتغير ترتيب الإعلان تتغير الأرقام. ومع ذلك، كما قلت في مقال "التسلسل كما هو" ، فإن هذا لا يسبب مشاكل. جميع عناصر التعداد موجودة في نسخة واحدة، ويتم التحكم في ذلك على مستوى الجهاز الظاهري. لذلك، يمكن مقارنتها مباشرة باستخدام الروابط. * * * ربما يكون هذا كل ما لدينا اليوم فيما يتعلق بالجانب العملي لتنفيذ مقارنة الكائنات. ربما فاتني شيء ما. كما هو الحال دائما، وأنا أتطلع إلى تعليقاتكم! في الوقت الحالي، اسمحوا لي أن آخذ إجازتي. شكرا لكم جميعا على اهتمامكم! رابط إلى المصدر: مقارنة الأشياء: ممارسة
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION