পাঠ ৮: জেনেরিকস

জেনেরিকস ইন জাভা (Generics in Java)

আমরা জাভা-এর টাইপ সিস্টেম সর্ম্পকে জানি। আমরা জানি জাভাতে কোন প্রোগ্রাম লিখতে হলে আমাদের কে টাইপ বলে দিতে হয় । যেমন আমরা যদি একটি মেথড লিখি তাহলে মেথডটি কি টাইপ প্যারামিটার এক্সেপ্ট করবে তা বলে দিতে হয়।

তবে জাভাতে একটি চমৎকার ফিচার আছে যাতে করে আমরা অনেক সময় টাইপ না বলে দিয়েই কোড লিখতে পারি। আমরা জেনেরিকস শুরু করার আগে একটি গুরুত্বপূর্ণ তথ্য জেনে নিই- জাভা প্রোগ্রামিং ল্যাংগুয়েজ এ সব ক্লাস java.lang.Object ক্লাসটিকে ইনহেরিট করে। আমরা এটি নিয়ে অন্য কোন চ্যাপ্টারে আলোচনা করবো, তবে এখন আমাদের শুধু এই তথ্যটুকু মনে রাখলেই চলবে।

সহজ কথায় যদি বলি, তাহলে জেনেরিকস দিয়ে আমরা যখন অবজেক্ট তৈরি করবো তখন টাইপ প্যারিমিটারাইজ করতে পারি। অর্থাৎ আমরা যখন new অপারেটর দিয়ে অবজেক্ট তৈরি করবো তখন আসলে সিন্ধান্ত নেবো এটির টাইপ কি হবে। এর আগে আমরা এমন ভাবে একটা ক্লাস বা মেথড লিখে ফেলতে পারি যাতে করে এটি যে কোন টাইপ এর জন্যে কাজ করে।

বরং একটা উদাহরণ দেখা যাক-

    //একটি সিম্পল ক্লাস , এখানে  T হচ্ছে টাইপ প্যারামিটার যা অবজেক্ট তৈরি করার সময় রিয়েল টাইপ দিয়ে রিপ্লেস হবে
    public class Generic<T> {
        T obj;
     // একটা টাইপ ভ্যারিয়বল ডিক্লেয়ার করা হলো 


       // কনস্ট্রাকটর – যে একটি রিয়েল অবজেক্ট আর্গুমেন্ট হিসেবে নেয়     
        public Generic(T obj) {
            this.obj = obj;
        }

    //  অবজেক্টটি একসেস করার জন্যে একটি মেথড 
        public T getObj() {
            return obj;
        }

       // রানটাইমে অবজেক্ট-এর টাইপ আসলে কি , তা প্রিণ্ট করে দেখি
        public void showType() {
            System.out.println("Type of T is: " + obj.getClass().getName());
        }


        public static void main(String[] args) {

        // একটি ইন্টিজার এর রেফারেন্স   
        Generic<Integer> iObj;

        // অবজেক্ট তৈরি করি এবং iObj রেফারেন্স এ এসাইন করি এবং কনস্ট্রাকটর আর্গুমেন্ট হিসেবে 88 পাস করি 
            iObj = new Generic<Integer>(88);

        // রানটাইম-এ তাহলে জেনেরিক ক্লাসটিতে T obj একটি ইন্টিজার হয়ে যাওয়ার কথা, প্রিন্ট করে দেখা যাক  
        iObj.showType();


        int v = iObj.getObj();
     // ইন্টিজার ভ্যালুটি এর ভ্যালু একসেস ককরে v তে রাখা হল

        System.out.println("value: " + v);
     // প্রিন্ট করি, যেখা যাক, আমরা এর ভ্যালু ঠিক ঠাক মতো পাওয়া যায় কিনা 


    //এভাবে আমরা একটি স্ট্রিং টাইপ দিয়েও পরীক্ষা করতে পারি। 
            Generic<String> strOb = new Generic<String>("This is a Generics Test");
            strOb.showType();
            String str = strOb.getObj();
            System.out.println("value: " + str);
        }
    }
`

এই প্রোগ্রামটি যদি রান করা হয়, তাহলে নিচের আউটপুট গুলো দেখা যাবে -

Type of T is: java.lang.Integer value: 88 Type of T is: java.lang.String value: This is a Generics Test

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

এভাবে আমরা আরও অন্যান্য টাইপ-ও প্যারামিটারাইজড করে পারি।

এবার আরও ভালভাবে এই প্রোগ্রামটি খেয়াল করা যাক-

    public class Generic<T> {
    }
`

এখানেT হচ্ছে টাইপ প্যারামিটার। এটি মূলত একটি প্লেস হোল্ডার।

লক্ষ্য করুন – এর T কিন্তু <> এর মধ্যে থাকে।

আমরা সাধারণত যেভাবে ভ্যারিয়েবল ডিক্লেয়ার করি, সেভাবেই আমরা জেনেরিক্স-এ ভ্যারিয়েবল ডিক্লেয়ার করতে পারি। এর জন্যে আলাদা কোন নিয়ম নেই।

     T obj;

এখানে T অবজেক্ট তৈরি করার সময় একটি রিয়েল অবজেক্ট অর্থাৎ আমরা যে অবজেক্ট প্যারিমিটারাইড করবো তা দ্বারা প্রতিস্থাপিত(replaced) হবে ।

আমরা জানি যে জাভা একটি স্ট্যাটিক টাইপ অর্থাৎ টাইপ সেইফ ল্যাংগুয়েজ। অর্থাৎ জাভা কোড কম্পাইল করার সময় এর টাইপ ইনফরমেশন ঠিক ঠাক আছে কিনা তা চেক করে নেয়।

অর্থাৎ -

        Generic<Integer> iObj;

এখানে iObj একটি ইন্টিজার প্যারমিটাইরজড অবজেক্ট রেফারেন্স ।

    iObj = new Generic<Double>(88.0); // Error!
`

এখন অবজেক্ট তৈরি করার সময় যদি ডাবল প্যারমিটাইরজড করি এবং iObj তে এসাইন করি, তাহলে

Error:(24, 16) java: incompatible types: Generic<java.lang.Double> cannot be converted to Generic<java.lang.Integer>
`

কম্পাইল করার সময় উপরের ইররটি দেখতে পাবো।

জেনেরিকস শুধুমাত্র অজজেক্ট নিয়ে কাজ করে-

আমারা জানি যে, জাভা দুই ধরণের টাইপ সাপোর্ট করে- PrimitiveType এবং ReferenceType। জেনেরিকস শুধুমাত্র ReferenceType অর্থাৎ শুধু মাত্র অবজেক্ট নিয়ে কাজ রে।

তাই-

     Generic<int> intObj = new Generic<int>(50);
`

এই স্ট্যাটমেন্ট টি ভ্যালিড নয়। অর্থাৎ প্রিমিটিভ টাই এর ক্ষেত্রে জেনেরিকস কাজ করবে না।

জেনেরিক ক্লাস এর সিনট্যাক্স-

class class-name<type-param-list > {}
`

জেনেরিক ক্লাস ইনসটেনসিয়েট করার সিনটেক্স-

class-name<type-arg-list > var-name = new class-name<type-arg-list >(cons-arg-list);
`

আমরা চাইলে একাধিক জেনেরিক টাইপ প্যারমিটাইরজড করতে পারি।

এবার তাহলে দুটি টাইপ প্যারামিটার নিয়ে একটি উদাহরণ দেখা যাক-

      public class Tuple<X, Y> {
          private X x;
          private Y y;

          public Tuple(X x, Y y) {
              this.x = x;
              this.y = y;
          }

          public X getX() {
              return x;
          }

          public Y getY() {
              return y;
          }

          public void showTypes() {
              System.out.println("Type of T is " +
                      x.getClass().getName() + " and Value: " + x);
              System.out.println("Type of V is " +
                      y.getClass().getName() + " and Value: " + y);
          }

          public static void main(String[] args) {
              Tuple<String, String> tuple = new Tuple<String, String>("Hello", "world");
              tuple.showTypes();

              Tuple<String, Integer> person = new Tuple<>("Rahim", 45);
              person.showTypes();
          }
       }

এই প্রোগ্রামটি রান করলে নিচের আউটপুট-টি পাওয়া যাবে –

Type of T is java.lang.String and Value: Hello Type of V is java.lang.String and Value: world Type of T is java.lang.String and Value: Rahim Type of V is java.lang.Integer and Value: 45

একটি টাপলের মধ্যে আমরা চাইলে আরেকটি টাপল রাখে পারি - নিচের উদাহরণটি চমৎকার-

    Tuple<String, Tuple<Integer, Integer>> tupleInsideTuple = new Tuple<String, Tuple<Integer, Integer>>("Tuple", new Tuple<Integer, Integer>(45, 89));

তবে আমরা যদি জাভা ৭ অথবা ৮ ব্যবহার করি তাহলে উপরের লাইনটি সংক্ষিপ্তভাবে লিখতে পারি –

        Tuple<String, Tuple<Integer, Integer>> tupleInsideTuple = new Tuple<>("Tuple", new Tuple<>(45, 89));
`

জাভা ৭ এ একটি নতুন অপারেটর সংযুক্ত হয়েছে যাকে বলা হয় ডায়মন্ড অপারেটর। এটি ব্যবহার করে আমরা জেনেরকস এ verbosity কিছুটা কমানো যায়। অর্থাৎ

        Map<String, List<String>> anagrams = new HashMap<String, List<String>>();

এই স্ট্যাটমেন্ট-টি অনেকটাই বড়। এটি আমরা এভাবে লিখতে পারি –

        Map<String, List<String>> anagrams = new HashMap<>();

অর্থাৎ জেনেরিকস লেখার সময় বাম পাশে টাইপ প্যারামিটার ইনফরমেশন গুলো লিখলে ডান পাশে লিখতে হয় না । এটি অটোম্যাটিক্যালী ইনফার করতে পারে।

Bounded Types

আমরা উপরে দুটি উদাহরণ দেখেছি যেগুলোতে আমরা যে কোন ধরণের টাইপ প্যারামিটারাইউজড করতে পারি। কিন্তু কখনো কখনো আমাদের টাইপ restrict করতে হয়। যেমন- আমরা একটি জেনেরিক ক্লাস লিখতে চাই যা কিনা একটি এরে-তে রাখা কতগুলো নাম্বার-এর গড়(average) রিটার্ন করবে এবং আমরা চাই, এই এরে তে যে কোন ধরণের নাম্বার থাকতে পারে, যেমন- ইন্টিজার, ফ্লোটিং পয়েন্ট, ডাবল ইত্যাদি। আমরা টাইপ প্যারামিটার দিয়ে বলে দিতে চাই কখন কোনটা থাকবে। উদারহরণ দেখা যাক-

      public class Stats<T> {
          T[] nums;

          public Stats(T[] nums) {
              this.nums = nums;
          }

          // Return type double in all cases.
          double average() {
              double sum = 0.0;
              for (T num : nums) {
                  sum += num.doubleValue(); // Error!!!
              }

              return sum / nums.length;
          }
      }
`

এভারেজ ক্যালকুলেট করার জন্য আমাদের এভারেজ মেথড সবসময় এরে থেকে ডাবল ভ্যালু এক্সেপেক্ট করে। কিন্তু আমাদের এরে-এর টাইপ যেহেতু যে কোন রকম হতে পারে, সুতরাং সব অবজেক্ট থেকে ডাবল ভ্যালু পাওয়ার উপায় নেই।

ইনফ্যাক্ট এই ক্লাসটি কিন্তু কম্পাইল হবে না।

এই ক্লাসটিতে আমরা একটি restriction এড করতে পারি যাতে করে এই টাইপ প্যারামিটার শুধুমাত্র নাম্বার(ইন্টিজার, ফ্লোটিং পয়েন্ট,ডাবল) হবে, নতুবা এটি কাজ করবে না।

আমরা জানি যে সব নিউমেরিক অবজেক্ট গুলোর সুপার ক্লাস হচ্ছে Number. এবং NumberdoubleValue() মেথড ডিফাইন করা আছে। সুতরাং আমাদের ক্লাসটিকে একটু পরিবর্তন করি।

    public class Stats<T extends Number> {
        T[] nums;

        public Stats(T[] nums) {
            this.nums = nums;
        }

        // Return type double in all cases.
        double average() {
            double sum = 0.0;
            for (T num : nums) {
                sum += num.doubleValue(); // Error!!!
            }

            return sum / nums.length;
        }
    }

একটু লক্ষ্য করুন-

    public class Stats<T extends Number>{
    }

আমরা ক্লাস ডেফিনেশনে আমাদের টাইপ প্লেসহোল্ডার T নাম্বারকে extend করে। এটি আমাদের টাইপ প্যারামিটার পাস করতে restrict করে । অর্থাৎ আমরা শুধু মাত্র সেসব টাইপ পাস করতে পারবো যারা Number এর সাব টাইপ।

সুতরাং আমাদের এই Stats ক্লাস এখন Integer, Double, Float, Long, Short, BigInteger, BigDecimal, Byte ইত্যাদি অবজেক্ট এর জন্যে কাজ করবে।

সুতরাং দেখা যাচ্ছে যে, জেনেরিকস এর সুবিধা ব্যবহার করে আমরা এই স্ট্যাট ক্লাসটি আলাদা আলাদা করে অনেকগুলো না লিখে একটি দিয়েই কাজ করে ফেলা সম্ভব হল।

Wildcard Arguments

নিচের উদাহরণটি লক্ষ্য করি-

        ArrayList<Object> lst = new ArrayList<String>();

এটি যদি কম্পাইল করতে চেষ্টা করি, তাহলে কম্পাইলার incompatible types ইরর দেবে। কিন্তু আমরা জানি যে, সকল অবজেক্ট এর সুপার ক্লাস Object। তাছাড়া আমরা polymorphism থেকে জানি যে
আমরা সাব ক্লাসের রেফারেন্স কে সুপার ক্লাসের রেফারেন্স এ এসাইন করতে পারি। সুতরাং উপরের স্ট্যাটমেন্ট-টি কাজ করার কথা।

নিচের উদাহরণ দুটি লক্ষ্য করি -

    List<String> strLst = new ArrayList<String>();   // 1
    List<Object> objLst = strList;                   // 2 - Compilation Error
`

২ নাম্বার লাইনটি কাজ করছে না । যদিও বা এটি কাজ করে এবং আর্বিট্রারি কোন একটি অবজেক্ট যদি objLst এড করা হয় তাহলে কিন্তু strList করাপ্টেড হয়ে যাবে এবং সেটি আর স্ট্রিং থাকবে না।

ধরা যাক, আমরা একটা print মেথড লিখতে চাই যা কিনা একটি লিস্ট এর ইলিমেন্ট গুলো প্রিন্ট করে।

    public static void print(List<Object> lst) {  // accept List of Objects only,
            // not List of subclasses of object
            for (Object o : lst) {
                System.out.println(o);
            }
    }

এটি কিন্তু শুধুমাত্র List<Object> একসেপ্ট করবে , List<String> অথবা List<Integer> করবে না।

উদাহরণ-

    public static void main(String[] args) {
          List<Object> objLst = new ArrayList<Object>();
          objLst.add(new Integer(55));
          printList(objLst);   // matches

          List<String> strLst = new ArrayList<String>();
          strLst.add("one");
          printList(strLst);  // compilation error
       }

এই সমস্যা দূর করার জন্যে জাভাতে একটি একটি অপারেটর ব্যবহার করা হয় – যার নাম wildcard (?)।

আমরা যদি আমাদের print() মেথডটি নিচের মতো করে লিখি, তাহলে কিন্তু আমাদের সমস্যা দূর হয়ে যাবে।

    public static void print(List<?> lst) {  // accept List of Objects only,
            // not List of subclasses of object
            for (Object o : lst) {
                System.out.println(o);
            }
    }

List<?> lst এর মানে হচ্ছে আমরা এর টাইপ আমাদের জানা নেই, এটি যে কোন টাইপ হতে পারে। যেহেতু সব টাইপ এর সুপার ক্লাস Object সুতরাং এটি যেকোন টাইপ এর জন্যে কাজ করবে।

Bounded Types এর মতো আমরা Wildcard Arguments কেও Bounded করে ফেলতে পারি ।

উদাহরণ -

        public static void process(List<? extends Foo> list) { /* ... */ }

এটি শুধু মাত্রে Foo এর সাব ক্লাস গুলো কে প্রসেস করতে পারবে। একে Upper Bounded Wildcards বলে ।

আমরা যদি এমন কোন মেথড লিখতে চাই যা শুধু মাত্র Integer, Number, and Object প্রসেস করবে অর্থাৎ Integerএবং এর সুপার ক্লাস প্রসেস করবে তাহলে -

        public static void addNumbers(List<? super Integer> list) {
        }

একে Lower Bounded Wildcards বলে।

Generic Methods

আমরা মূলত এতোক্ষণ জেনেরিক ক্লাস নিয়ে কথা বলেছি। আমরা একটি ক্লাসকে জেনেরিক না করে শুধুমাত্রে এর একটি বা একাধিক মেথড কে জেনেরিক করে লিখতে পারি।

উদহরণ-

    public class Util {
        // Generic static method
        public static <K, V> boolean compare(Pair<K, V> p1, Pair<K, V> p2) {
            return p1.getKey().equals(p2.getKey()) &&
                   p1.getValue().equals(p2.getValue());
        }
    }

এটি একটি জেনেরিকম মেথড।

জেনেরিক মেথড-এ রিটার্নটাইপ এর আগে টাইপ-প্লেস হোল্ডার <> লিখতে হয়।

আমরা এবার চেষ্টা করবো কিভাবে আমরা একটি জেনেরিক সিংগলি লিংকলিস্ট লিখতে পারি --


    /**
     * @author Bazlur Rahman Rokon
     * @date 2/4/15.
     */
    public class SinglyLinkedList<Type> {
        private long size;

        private Node<Type> head;
        private Node<Type> tail;

        public void addFirst(Type value) {
            addFirst(new Node<>(value));
        }

        public void addLast(Type value) {
            addLast(new Node<>(value));
        }

        private void addLast(Node<Type> node) {
            if (size == 0) {
                head = node;
            } else {
                tail.setNext(node);
            }
            tail = node;
            size++;
        }

        public void addFirst(Node<Type> node) {
            Node<Type> temp = head;
            head = node;
            head.setNext(temp);

            size++;

            if (size == 1) {
                tail = head;
            }
        }

        public Node<Type> getHead() {
            return head;
        }

        public Node<Type> getTail() {
            return tail;
        }

        public void removeFirst() {
            if (size != 0) {
                head = head.getNext();
                size--;
            }

            if (size == 0) {
                tail = null;
            }
        }

        public void removeLast() {
            if (size != 0) {
                if (size == 1) {
                    head = null;
                    tail = null;
                } else {
                    Node<Type> current = head;

                    while (current.getNext() != tail) {
                        current = current.getNext();
                    }
                    current.setNext(null);
                    tail = current;

                }
                size--;
            }
        }

        public Type getFirst() {

            return getHead().getValue();
        }

        // four scenario
        // 1. empty list-  do nothing
        // 2. single node : ( previous is null)
        // 3. Many nodes
        //      a. node to remove is first node
        //      b. node to remove is the middle or last

        public boolean remove(Type type) {
            Node<Type> prev = null;
            Node<Type> current = head;

            while (current != null) {
                if (current.getValue().equals(type)) {
                    if (prev != null) {

                        // just skip the current node. it works fine
                        prev.setNext(current.getNext());

                        if (current.getNext() == null) {
                            tail = prev;
                        }

                        size--;
                    } else {
                        removeFirst();
                    }

                    return true;
                }

                prev = current;
                current = current.getNext();
            }

            return false;
        }


        public long getSize() {

            return size;
        }

        public void print() {
            System.out.print("Total elements : " + size + " -> ");
            Node node = head;
            while (node != null) {
                System.out.print(node.getValue().toString() + " ,");
                node = node.getNext();
            }
            System.out.println();
        }

        public void clear() {
            for (Node<Type> x = head; x != null; ) {
                Node<Type> next = x.next;
                x.next = null;
                x.value = null;
                x = next;
            }

            head = tail = null;
            size = 0;
        }


        private class Node<Type> {
            private Type value;
            private Node<Type> next;

            public Node(Type value) {
                this.value = value;
            }

            public Type getValue() {
                return value;
            }

            public void setValue(Type value) {
                this.value = value;
            }

            public Node<Type> getNext() {
                return next;
            }

            public void setNext(Node<Type> next) {
                this.next = next;
            }
        }
    }

এবার আমরা এটিকে রান করে দেখি-

    /**
     * @author Bazlur Rahman Rokon
     * @date 2/4/15.
     */
    public class LinkedListDemo {
        public static void main(String[] args) {
            SinglyLinkedList<Integer> integers = new SinglyLinkedList<>();
            integers.addFirst(4);
            integers.addFirst(3);
            integers.addFirst(2);
            integers.addFirst(1);

            integers.print();

            System.out.println("Remove first and last elements..");
            integers.removeFirst();
            integers.removeLast();
            integers.print();

            System.out.println("add elements at last ");
            integers.addLast(5);
            integers.addLast(6);
            integers.addLast(7);
            integers.print();

            SinglyLinkedList<String> stringLinkedList = new SinglyLinkedList<>();
            stringLinkedList.addFirst("abcd");
            stringLinkedList.addFirst("efgh");
            stringLinkedList.addFirst("ijkl");
            stringLinkedList.addFirst("mnop");
            stringLinkedList.addFirst("qrst");
            stringLinkedList.print();
        }
    }

Output:

Total elements : 4 - 1 ,2 ,3 ,4 , Remove first and last elements.. Total elements : 2 - 2 ,3 , add elements at last Total elements : 5 - 2 ,3 ,5 ,6 ,7 , Total elements : 5 - qrst ,mnop ,ijkl ,efgh ,abcd ,