অবজেক্ট ইকুয়ালিটি আর equals মেথডের যত ঝামেলা - Object equality and equals method

অবজেক্ট ইকুয়ালিটি আর equals মেথডের যত ঝামেলা  - Object equality and equals method

Java তে সব অবজেক্টের একই পূর্ব পুরুষ - Object. সব ক্লাস আসলে Object ক্লাস -এরই সাব-ক্লাস। সেই হিসাবে আমরা জানি আর না জানি, আমাদের লেখা যেকোনো ক্লাস -ই Object ক্লাস-এ লেখা মেথড গুলো নিজের মধ্যে নিয়ে আসে।
 ধরা যাক, আমরা জ্যামিতির বিন্দু বা Point -এর জন্য একটা ক্লাস Point লিখেছি। আমাদের Point  ক্লাস অন্য কোনো ক্লাসকে ইনহেরিট না করলেও, Object  ক্লাস কে কিন্তু এমনি-এমনিই  ইনহেরিট করে বসে আছে।  UML ডায়াগ্রাম অনেকটা এইরকম হবে:






Object  ক্লাস-এর মেথড গুলো দেখে আন্দাজ করা যায় যে, জাভার ডিসাইনার রা চেয়েছিলেন যেন সব অবজেক্ট অন্য অবজেক্টের সাথে তুলনা করা যায় (equals), রানটাইম -এ যেন বলা যায় অবজেক্টটা কোন ক্লাস-এর (getClass), হ্যাশ-ভিত্তিক Collection -এ যেন ব্যবহার করা যায় (hashCode), মাল্টি-থ্রেড করা যায় (notify /wait), প্রিন্ট করা যায় (toString) - ইত্যাদি।  


আর এই Object ক্লাস সব জায়গায় ব্যবহার করা যায়।  যেমন, অবজেক্ট ক্লাস-এর ভ্যারিয়েবল, প্যারামিটার হিসাবে মেথডে এমনকি অবজেক্ট-এর Collection -হিসাবেও ব্যবহার করা যায়।

এখন প্রশ্ন হলো, দুটো অবজেক্ট একই কিনা তা জানা যায় কিভাবে? একটা উপায় হচ্ছে ‘==’ব্যবহার করা।  
নিচের বামদিকের ৩ লাইন কোডের জন্য ডানের ‘==’ অপারেটর কি রিটার্ন করবে?

Point p1 = new Point(5, 3);
Point p2 = new Point(5, 3);
Point p3 = p2;
p1 == p2: true or false?
P1 == p3: true or false?
P2 == p3: true or false?

৩য় লাইনের p2  == p3 true  রিটার্ন করবে, বাকি দুটো False করবে। মনে রাখতে হবে ‘==’ অপারেটর শুধু রেফারেন্স-ইকুয়ালিটি চেক করে।  অর্থাৎ, হীপ মেমরীতে(Heap memory)  দুটো অবজেক্টে যদি একই এড্রেস-এ থাকে, তবেই তা কাজ করবে।  অন্য কথায়, ‘==’ কাজ করবে যদি আপনি একটা অবজেক্টকে  নিজের সাথেই কম্পেয়ার বা তুলনা করেন। কাজেই, p2, p3 একই অবজেক্টকে রেফার করায় তাদের বেলায় true  রিটার্ন করবে। অন্য অবজেক্ট আসলে হীপ মেমরির অন্য জায়গায় রাখা আছে।  

আর আপাত দৃষ্টিতে p1, p2 একই জ্যামিতিক বিন্দু হলেও (তাদের x ও y-মান এক), তারা রেফারেন্স-ইকুয়ালিটি-র দিক দিয়ে আলাদা।

আচ্ছা, আমরা যদি অবজেক্ট ক্লাস-এর equals  মেথড কল করতাম, তখন কি হতো ? মানে, p1.equals(p2)?

তখনও আসলে একই ফল হত।  কারণ, অবজেক্ট ক্লাস-এ equals মেথড আসলে ওই একই ‘==’ অপারেটর ব্যবহার করেই ইকুয়ালিটির পরীক্ষা করে, যেমন:

public class Object {
  ...
    public boolean equals(Object o) {
    return this == o;
    }
}

তার মানে হচ্ছে, আমরা যদি একই x এবং y  মানের দুটো বিন্দুকে ইকুয়াল বা এক বলতে চাই, তাহলে আমাদের ইকুয়ালিটি’র  সঙ্গা পাল্টাতে হবে। অর্থাৎ, ইকুয়ালিটি হীপ মেমরির স্থান ভিত্তিক না হয়ে অবজেক্টের মান ভিত্তিক হতে হবে।  কীভাবে তা করা সম্ভব? একটাই উপায়, equals মেথড ওভাররাইড করে করতে হবে। আচ্ছা, তাহলে চলুন চেষ্টা করে দেখি:

public class Point {
 …
   public boolean equals (Point other) {
       return (x == other.x && y == other.y);
   }
}

আমাদের Point ক্লাস-এর equals  মেথড আরেকটা Point ক্লাস-এর অবজেক্ট ‘other ‘ নিয়ে x  আর y -এর মান পরীক্ষা করে true  অথবা False  রিটার্ন করবে। কিন্তু আসলে কোড-টাতে অনেকগুলা ভুল আছে।  null - রেফারেন্স এক্সেপশন বাদ দিয়ে আর কী কী ভুল থাকতে পারে, আন্দাজ করুন তো?

আমরা একটা একটা করে ভুল বের করবো, আর তা ঠিক করে আগাবো। প্রথম যেই ভুল টা আমাদের খেয়াল করা উচিত তা হচ্ছে, আমরা কিন্তু মেথড ওভাররাইড করি নাই, ওভারলোড করে ফেলেছি ! Object  ক্লাস-এর equals  মেথড Object  টাইপ প্যারামিটার নেয়, আমাদের equals  কিন্তু Point  টাইপ নিচ্ছে - প্যারামিটারের টাইপ ভিন্ন হওয়ায় তা ওভারলোড হয়ে গেছে। ওভাররাইড করতে হলে, একই প্যারামিটার টাইপ রাখতে হবে।  ওভারলোড - ওভাররাইড নিয়ে গড়বড় থাকলে আমার লেখা আরেকটা ব্লগ পোস্ট পড়ে দেখতে পারেন। চলুন, তাহলে ঠিক করে ফেলি:

public class Point {
 …
   public boolean equals (Object  other) {
       return (x == other.x && y == other.y);
   }
}

তাও ভুল!! এবার কিন্তু আমাদের কোড কম্পাইল-ই হবে না।  কারণ কি?
খেয়াল করুন, Object  টাইপ প্যারামিটার তো যেকোনো কিছুই হতে পারে, আর সব অবজেক্টের তো আর x কিংবা y  মান থাকবে না - তাই এক্ষেত্রে কম্পাইলার আমাদের ভুল ধরিয়ে দিবে। তাহলে কাস্ট (cast) করে নিলেই তো হওয়ার কথা:

আবার ঠিক করি:

public class Point {
 …
   public boolean equals (Object other) {
       Point o = (Point) other;       
       return (x == o.x && y == o.y);
   }
}

এখনও ভুল শেষ হয় নাই।  এবার কম্পাইলার ঝামেলা না করলেও, রানটাইম-এ এক্সেপশন হতে পারে।  রানটাইমে যদি Point  বাদে অন্য কোনো টাইপ অবজেক্ট প্যারামিটার হিসাবে পাস করা হয়, তাহলে ক্লাস কাস্ট এক্সেপশন (ClassCastException) হবে।  অর্থাৎ, আমরা জোর করে কোনো অবজেক্টকে তো আর Point টাইপ অবজেক্ট বানিয়ে ফেলতে পারি না।  তাহলে, কাস্ট করার আগে দেখতে হবে প্যারামিটার হিসাবে আসা অবজেক্ট Point টাইপ কিনা:

public class Point {
  public boolean equals (Object other) {
       if (other instanceOf Point) {
           Point o = (Point) other;
           return (x == o.x && y == o.y);
       }
       else
           return false;
  }
}

মনে হচ্ছে, এইবার বুঝি ঠিক হলো।  কিন্তু না, আসলে হয় নাই।  কেন হয় নাই বোঝার জন্য একটু ব্যাকগ্রাউন্ড দরকার। ব্যপারটা একটু জটিলই।  

খুলেই বলি। ধরা যাক, ভবিষ্যতে আমরা x, y -এর সাথে z -অক্ষ যোগ করে নতুন একটা ক্লাস লিখলাম - Point3D যা কিনা আমাদের আগের Point ক্লাসকে extend  করে:

public class Point3D extends Point {
  private int z;
  public Point3D( int x, int y, int z) { … }
  … ...
}



এখন যদি আমরা ৩ তা অবজেক্ট তৈরী করি নিচের মত:

Point3D p1 = new Point3D(4, 5, 0);
Point3D p2 = new Point3D(4, 5, 6);
Point      p3 = new Point(4, 5);

তাহলে সবগুলো অবজেক্ট-ই একে অন্যের সমান বা ইকুয়াল হয়ে যাবে। কারণ, equals  মেথড শুধু x আর y অক্ষের মান দেখে। z -অক্ষের মান আমলে নেয়না।  তাহলে?

Point3D - ক্লাস-এও equals  মেথড ওভাররাইড করতে হবে :

public class Point3D {
  public boolean equals (Object other) {
       if (other instanceOf Point3D) {
           Point3D o = (Point3D) other;
           return (super.equals(o) && z == o.z);
       }
       else
           return false;
  }
}

দুঃখের বিষয় এখনো কোড পুরাপুরি ঠিক হয় নাই :(

কেন বোঝার জন্য আরো কিছু জিনিস জানতে হবে: যেকোনো ইকুয়ালিটি মূলত ৪ ধরনের (আসলে ৫ ধরনের [১]) জিনিস বা সূত্র মেনে চলে:

১. Reflexive বা নিজে-নিজের মত হওয়া: x .equals(x) সবসময় true  রিটার্ন করবে।
২. Symmetric বা একে অন্যের মত হবে যদি তারা একই হয় : x.equals(y) true  হলে, y.equals(x) true  হবে।
৩. Transitive বা একজন অন্য জনের মত, আবার অন্য জন আরেকজনের মত হলে, প্রথম জন ওই আরেকজনের মত হবে: (x.equals(y) == true) এবং  (y.equals(z) == true) => x.equals(z) = true
৪. non -null বা কেউই নাল না: x.equals(null) সবসময় false হবে।    

বলুন তো, আমাদের সব শেষ কোড কোন সুত্র ভাঙ্গবে? উত্তর: ২ নম্বর  বা Symmetric. কিন্তু কীভাবে? উদাহরণ হিসাবে আগের অবজেক্ট গুলোর কথা চিন্তা করা যাক:

Point3D p2 = new Point3D(4, 5, 6);
Point      p3 = new Point(4, 5);


p3.equals(p2) = true কিন্তু p2.equals(p3) = false রিটার্ন করবে।  দুটো ক্লাস-এর equals মেথডের ইমপ্লিমেন্টেশন পাশাপাশি দেখে ব্যাপারটা বোঝা যাক:

Point
Point3D
public class Point {
  public boolean equals (Object other) {
       if (other instanceOf Point) {
           Point o = (Point) other;
           return (x == o.x && y == o.y);
       }
       else
           return false;
  }
}
public class Point3D {
  public boolean equals (Object other) {
       if (other instanceOf Point3D) {
           Point3D o = (Point3D) other;
           return (super.equals(o) && z == o.z);
        }
       else
           return false;
  }
}

যেহেতু Point3D ক্লাস Point ক্লাসকে extend  করে, সেহেতু Point3D ক্লাস-এর অবজেক্ট কিন্তু Point ক্লাস-এর অবজেক্ট হিসাবে চালানো যাবে। আর তাই, যখন Point ক্লাস-এর equals মেথড Point3D-এর অবজেক্ট হাতে পায়,  তখন instanceOf -এর চেক বা পরীক্ষা পাস করে, আর x, y -এর মান এক হওয়ায় true  রিটার্ন করে।  কিন্তু উল্টোটা কাজ করে না, কারণ Point টাইপ অবজেক্ট তো আর Point3D টাইপ না - তাই।  

উফ!! আর কত? আসল উত্তরটা  তাহলে কি? নিচে দিয়ে দিচ্ছি। আসলে রানটাইমে Object ক্লাস-এরই আরেকটা মেথড ‘getClass’ ব্যবহার করে অবজেক্টের আসল টাইপ বের করতে হবে :


Point - the correct one!
Point3D - the correct one!
public class Point {
  public boolean equals (Object o) {
     if (o != null && getClass() == o.getClass()) {
           Point other = (Point) o;
           return (x == other.x && y == other.y);
     }
       else
           return false;
  }
}
public class Point3D {
 public boolean equals (Object o) {
   if (o != null && getClass() == o.getClass()) {
           Point3D other = (Point3D) o;
           return (super.equals(other) && z == other.z);
    }
    else
       return false;
  }
}

যাক, এইবার কিছু ক্রেডিট দিয়ে শেষ করি।  পুরো লেখাটাই আমেরিকার বিখ্যাত ওয়াশিংটন ইউনিভার্সিটির কম্পিউটার সায়েন্সের প্রভাষক মার্টি স্টেপ-এর স্লাইড থেকে নেয়া। উনি অবশ্য এখন আরেক বিখ্যাত - স্ট্যানফোর্ড ইউনিভার্সিটিতে পড়ান।  আমি উনার অনুমতি নিয়েই স্লাইডগুলো আমার ক্লাস-এ ব্যবহার করেছিলাম। মূল স্লাইড গুলো পেতে গুগলে “object equality marty stepp” লিখে সার্চ করলেই পাওয়া যাবে।

স্লাইড গুলোতে উনি যশুয়া ব্লক-এর ইফেক্টিভ জাভা বইয়ের আইটেম ৮ কে রেফার করেছেন।

ধন্যবাদ।  - ইশতিয়াক হোসেন , ৭ম ব্যাচ - সিএসই - ডিইউ

রেফারেন্স

1 comment:

  1. Another nice article. Best line - "উফ!! আর কত? আসল উত্তরটা তাহলে কি?"

    ReplyDelete

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম

কাজের জায়গায় ভুল থেকে শেখা: regex 'র একটা খুব কমন বিষয় যেটা এতদিন ভুল জানতাম  ৩ ফেব্রুয়ারি, শনিবার, ২০২৪ রেগুলার এক্সপ্রেশন (Regular Exp...