الكسالى ليسوا الوحيدين الذين يكتبون عن المقارنات والمقارنات في جافا. أنا لست كسولًا، لذا يرجى الحب والتذمر بشأن تفسير آخر. آمل ألا يكون غير ضروري. ونعم، هذه المقالة هي إجابة السؤال: " هل يمكنك كتابة مقارن من الذاكرة؟ " أتمنى أن يتمكن الجميع من كتابة مقارن من الذاكرة بعد قراءة هذا المقال.
مقدمة
كما تعلمون، جافا هي لغة كائنية التوجه. ونتيجة لذلك، فمن المعتاد التعامل مع الكائنات في جافا. ولكن عاجلاً أم آجلاً، ستواجه مهمة مقارنة الأشياء بناءً على بعض الخصائص. على سبيل المثال : لنفترض أن لدينا بعض الرسائل التي وصفها الفصلMessage
:
public static class Message {
private String message;
private int id;
public Message(String message) {
this.message = message;
this.id = new Random().nextInt(1000);
}
public String getMessage() {
return message;
}
public Integer getId() {
return id;
}
public String toString() {
return "[" + id + "] " + message;
}
}
ضع هذه الفئة في برنامج التحويل البرمجي Tutorialspoint Java
. لا تنس إضافة بيانات الاستيراد أيضًا:
import java.util.Random;
import java.util.ArrayList;
import java.util.List;
في main
الطريقة، قم بإنشاء عدة رسائل:
public static void main(String[] args){
List<Message> messages = new ArrayList();
messages.add(new Message("Hello, World!"));
messages.add(new Message("Hello, Sun!"));
System.out.println(messages);
}
دعونا نفكر فيما سنفعله إذا أردنا المقارنة بينهما؟ على سبيل المثال، نريد الفرز حسب المعرف. ولإنشاء ترتيب، نحتاج إلى مقارنة الكائنات بطريقة أو بأخرى لكي نفهم أي كائن يجب أن يأتي أولاً (أي الأصغر) وأي كائن يجب أن يتبعه (أي الأكبر). لنبدأ بفصل مثل java.lang.Object
. نحن نعلم أن جميع الفئات ترث Object
الفئة ضمنيًا. وهذا منطقي لأنه يعكس مفهوم أن "كل شيء هو كائن" ويوفر سلوكًا مشتركًا لجميع الطبقات. تنص هذه الفئة على أن كل فئة لها طريقتان: → تقوم hashCode
الطريقة hashCode
بإرجاع بعض int
التمثيل الرقمي ( ) للكائن. ماذا يعني ذالك؟ هذا يعني أنه إذا قمت بإنشاء مثيلين مختلفين لفئة ما، فيجب أن يكون لهما hashCode
s مختلفة. يقول وصف الطريقة ما يلي: "بقدر ما هو عملي إلى حد معقول، فإن طريقة hashCode المحددة بواسطة فئة Object تقوم بإرجاع أعداد صحيحة مميزة لكائنات مميزة". وبعبارة أخرى، بالنسبة إلى instance
s مختلفين، يجب أن يكون هناك hashCode
s مختلفان. أي أن هذه الطريقة غير مناسبة لمقارنتنا. → equals
. تجيب الطريقة equals
على السؤال "هل هذه الكائنات متساوية؟" وترجع " boolean
." افتراضيًا، تحتوي هذه الطريقة على الكود التالي:
public boolean equals(Object obj) {
return (this == obj);
}
أي أنه إذا لم يتم تجاوز هذه الطريقة، فإنها توضح بشكل أساسي ما إذا كانت مراجع الكائنات متطابقة أم لا. ليس هذا ما نريده لرسائلنا، لأننا مهتمون بمعرفات الرسائل، وليس مراجع الكائنات. وحتى لو تجاوزنا هذه equals
الطريقة، فإن أقصى ما يمكن أن نأمله هو معرفة ما إذا كانا متساويين أم لا. وهذا لا يكفي بالنسبة لنا لتحديد الترتيب. فماذا نحتاج إذن؟ نحن بحاجة إلى شيء يقارن. ومن يقارن هو Comparator
. افتح Java API
وابحث عن Comparator
. في الواقع، هناك java.util.Comparator
واجهة java.util.Comparator and java.util.Comparable
كما ترون، مثل هذه الواجهة موجودة. تقول الفئة التي تنفذها: "أقوم بتنفيذ طريقة لمقارنة الكائنات." الشيء الوحيد الذي عليك أن تتذكره حقًا هو عقد المقارنة، والذي يتم التعبير عنه على النحو التالي:
Comparator returns an int according to the following rules:
- It returns a negative int if the first object is smaller
- It returns a positive int if the first object is larger
- It returns zero if the objects are equal
الآن دعونا نكتب المقارنة. سنحتاج إلى استيراد java.util.Comparator
. بعد عبارة الاستيراد، أضف ما يلي إلى main
الطريقة: Comparator<Message> comparator = new Comparator<Message>();
بالطبع لن ينجح هذا، لأنها Comparator
واجهة. لذلك نضيف الأقواس المعقوفة {}
بعد القوسين. اكتب الطريقة التالية داخل الأقواس:
public int compare(Message o1, Message o2) {
return o1.getId().compareTo(o2.getId());
}
لا تحتاج حتى إلى تذكر التهجئة. والمقارن هو الذي يجري المقارنة، أي أنه يقارن. للإشارة إلى الترتيب النسبي للكائنات، نعيد int
. هذا هو الأساس. جميل وسهل. كما ترون من المثال، بالإضافة إلى المقارنة، هناك واجهة أخرى — java.lang.Comparable
والتي تتطلب منا تنفيذ compareTo
الطريقة. تقول هذه الواجهة: "الفصل الذي يطبقني يجعل من الممكن مقارنة مثيلات الفصل." على سبيل المثال، Integer
تنفيذ compare
To هو كما يلي:
(x < y) ? -1 : ((x == y) ? 0 : 1)
قدمت Java 8 بعض التغييرات الرائعة. إذا ألقيت نظرة فاحصة على الواجهة Comparator
، فسترى التعليق @FunctionalInterface
التوضيحي الموجود فوقها. هذا التعليق التوضيحي مخصص لأغراض إعلامية ويخبرنا أن هذه الواجهة فعالة. هذا يعني أن هذه الواجهة تحتوي على طريقة مجردة واحدة فقط، وهي طريقة بدون تطبيق. ماذا يعطينا هذا؟ الآن يمكننا كتابة كود المقارنة مثل هذا:
Comparator<Message> comparator = (o1, o2) -> o1.getId().compareTo(o2.getId());
نحن نسمي المتغيرات بين قوسين. سترى Java أنه نظرًا لوجود طريقة واحدة فقط، فإن العدد المطلوب وأنواع معلمات الإدخال تكون واضحة. ثم نستخدم عامل السهم لتمريرها إلى هذا الجزء من الكود. والأكثر من ذلك، بفضل Java 8، لدينا الآن طرق افتراضية في الواجهات. تظهر هذه الطرق افتراضيًا عندما نقوم بتنفيذ واجهة ما. الواجهة Comparator
لديها عدة. على سبيل المثال:
Comparator moreImportant = Comparator.reverseOrder();
Comparator lessImportant = Comparator.naturalOrder();
هناك طريقة أخرى من شأنها أن تجعل التعليمات البرمجية الخاصة بك أكثر نظافة. ألق نظرة على المثال أعلاه، حيث قمنا بتعريف المقارنة لدينا. ماذا تعمل، أو ماذا تفعل؟ إنها بدائية تمامًا. إنه ببساطة يأخذ كائنًا ويستخرج بعض القيمة "القابلة للمقارنة". على سبيل المثال، Integer
Implements comparable
، لذلك نحن قادرون على إجراء عملية CompareTo على قيم حقول معرف الرسالة. يمكن كتابة دالة المقارنة البسيطة هذه على النحو التالي:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
بمعنى آخر، لدينا "a" Comparator
الذي يقارن على النحو التالي: فهو يأخذ الأشياء، ويستخدم الطريقة getId()
للحصول على "a" Comparable
منها، ثم يستخدم compareTo
للمقارنة. وليس هناك المزيد من الهياكل الرهيبة. وأخيرا، أريد أن أشير إلى ميزة أخرى. يمكن أن تكون بالسلاسل المقارنات. على سبيل المثال:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
comparator = comparator.thenComparing(obj -> obj.getMessage().length());
طلب
تبين أن الإعلان عن المقارنة أمر منطقي تمامًا، ألا تعتقد ذلك؟ الآن نحن بحاجة إلى معرفة كيف وأين نستخدمها. →Collections.sort(java.util.Collections)
يمكننا بالطبع فرز المجموعات بهذه الطريقة. ولكن ليس كل مجموعة، قوائم فقط. لا يوجد شيء غير عادي هنا، لأن القوائم هي نوع المجموعات التي يمكنك من خلالها الوصول إلى العناصر من خلال فهرسها. وهذا يسمح باستبدال العنصر الثاني بالعنصر الثالث. ولهذا السبب فإن طريقة الفرز التالية مخصصة للقوائم فقط:
Comparator<Message> comparator = Comparator.comparing(obj -> obj.getId());
Collections.sort(messages, comparator);
→ Arrays.sort(java.util.Arrays)
من السهل أيضًا فرز المصفوفات. مرة أخرى، لنفس السبب، يتم الوصول إلى عناصرها عن طريق الفهرس. → Descendants of java.util.SortedSet and java.util.SortedMap
سوف تتذكر ذلك Set
ولا Map
تضمن الترتيب الذي يتم به تخزين العناصر. ولكن لدينا تطبيقات خاصة تضمن الطلب. وإذا لم يتم تنفيذ عناصر المجموعة java.util.Comparable
، فيمكننا تمرير a Comparator
إلى مُنشئها:
Set<Message> msgSet = new TreeSet(comparator);
→ Stream API
في Stream API، التي ظهرت في Java 8، تتيح لك المقارنات تبسيط العمل مع عناصر الدفق. على سبيل المثال، لنفترض أننا بحاجة إلى سلسلة من الأرقام العشوائية من 0 إلى 999، بما في ذلك:
Supplier<Integer> randomizer = () -> new Random().nextInt(1000);
Stream.generate(randomizer)
.limit(10)
.sorted(Comparator.naturalOrder())
.forEach(e -> System.out.println(e));
يمكننا أن نتوقف هنا، ولكن هناك مشاكل أكثر إثارة للاهتمام. على سبيل المثال، لنفترض أنك بحاجة إلى إعداد ملف Map
، حيث يكون المفتاح هو معرف الرسالة. بالإضافة إلى ذلك، نريد فرز هذه المفاتيح، لذلك سنبدأ بالكود التالي:
Map<Integer, Message> collected = Arrays.stream(messages)
.sorted(Comparator.comparing(msg -> msg.getId()))
.collect(Collectors.toMap(msg -> msg.getId(), msg -> msg));
في الواقع حصلنا على HashMap
هنا. وكما نعلم، فإنه لا يضمن أي أمر. ونتيجة لذلك، فإن عناصرنا، التي تم فرزها حسب المعرف، تفقد ترتيبها ببساطة. ليس جيدا. سيتعين علينا تغيير جامعنا قليلاً:
Map<Integer, Message> collected = Arrays.stream(messages)
.sorted(Comparator.comparing(msg -> msg.getId()))
.collect(Collectors.toMap(msg -> msg.getId(), msg -> msg, (oldValue, newValue) -> oldValue, TreeMap::new));
بدأ الرمز يبدو مخيفًا بعض الشيء، ولكن تم الآن حل المشكلة بشكل صحيح. اقرأ المزيد عن المجموعات المختلفة هنا:
يمكنك إنشاء المجمع الخاص بك. اقرأ المزيد هنا: "إنشاء مجمع مخصص في Java 8"
. وستستفيد من قراءة المناقشة هنا: "قائمة Java 8 للتعيين باستخدام الدفق"
.
فخ السقوط
Comparator
وهي Comparable
جيدة. ولكن هناك فارق بسيط يجب أن تتذكره. عندما يقوم الفصل بالفرز، فإنه يتوقع أنه يمكن تحويل فصلك إلى ملف Comparable
. إذا لم يكن الأمر كذلك، فسوف تتلقى خطأ في وقت التشغيل. لنلقي نظرة على مثال:
SortedSet<Message> msg = new TreeSet<>();
msg.add(new Message(2, "Developer".getBytes()));
يبدو أنه لا يوجد شيء خاطئ هنا. لكن في الواقع، في مثالنا، سيفشل بسبب وجود خطأ: java.lang.ClassCastException: Message cannot be cast to java.lang.Comparable
وكل ذلك لأنه حاول فرز العناصر (إنه SortedSet
في النهاية)...لكنه لم يستطع. لا تنس هذا عند العمل مع SortedMap
و SortedSet
.
المزيد من القراءة: |
---|
GO TO FULL VERSION