जावा में जेनेरिक में विलोपन की अवधारणा क्या है?


141

जावा में जेनेरिक में विलोपन की अवधारणा क्या है?

जवाबों:


200

यह मूल रूप से कंपाइलर प्रवंचना के माध्यम से जावा में जेनरिक को लागू करने का तरीका है। संकलित जेनेरिक कोड वास्तव में सिर्फ java.lang.Objectजहाँ आप T(या किसी अन्य प्रकार के पैरामीटर) के बारे में बात करते हैं - और कंपाइलर को यह बताने के लिए कुछ मेटाडेटा है कि यह वास्तव में एक सामान्य प्रकार है।

जब आप एक सामान्य प्रकार या विधि के खिलाफ कुछ कोड संकलित करते हैं, तो कंपाइलर काम करता है जिसका आप वास्तव में मतलब है (यानी किस प्रकार का तर्क Tहै) और संकलन समय पर पुष्टि करता है कि आप सही काम कर रहे हैं, लेकिन उत्सर्जित कोड फिर से बात करता है के संदर्भ में java.lang.Object- संकलक जहां आवश्यक हो अतिरिक्त जातियां उत्पन्न करता है। निष्पादन के समय, ए List<String> और ए List<Date>बिल्कुल समान हैं; अतिरिक्त प्रकार की जानकारी संकलक द्वारा मिटा दी गई है।

इसकी तुलना सी, # से करें, जहां जानकारी निष्पादन के समय में बरकरार रखी जाती है, जिससे कोड में अभिव्यक्तियाँ हो सकती हैं जैसे कि typeof(T)वह समतुल्य है T.class- जिसके सिवाय बाद वाला अमान्य है। (.NET जेनिक्स और जावा जेनरिक के बीच आगे के अंतर हैं, आप पर ध्यान दें।) जावा जेनेरिक के साथ काम करते समय टाइप इरेज़र कई "अजीब" चेतावनी / त्रुटि संदेशों का स्रोत है।

अन्य संसाधन:


6
@Rogerio: नहीं, वस्तुओं में विभिन्न सामान्य प्रकार नहीं होंगे। खेतों प्रकार पता है, लेकिन वस्तुओं नहीं है।
जॉन स्कीट

8
@Rogerio: बिल्कुल - निष्पादन समय पर यह पता लगाना बेहद आसान है कि क्या कुछ ऐसा है जो केवल Object(कमजोर रूप से टाइप किए गए परिदृश्य में) प्रदान किया जाता है List<String>, उदाहरण के लिए। जावा में यह सिर्फ संभव नहीं है - आप पता लगा सकते हैं कि यह एक है ArrayList, लेकिन यह नहीं कि मूल सामान्य प्रकार क्या था। इस तरह की बात क्रमबद्धता / deserialization स्थितियों में आ सकती है, उदाहरण के लिए। एक अन्य उदाहरण यह है कि एक कंटेनर को अपने सामान्य प्रकार के उदाहरणों का निर्माण करने में सक्षम होना चाहिए - आपको उस प्रकार को जावा (जैसे Class<T>) में अलग से पास करना होगा ।
जॉन स्कीट

6
मैं दावा किया कभी नहीं यह हमेशा या लगभग हमेशा एक मुद्दा था - लेकिन यह कम से कम है यथोचित अक्सर मेरे अनुभव में एक मुद्दा। ऐसे कई स्थान हैं जहाँ मुझे Class<T>एक रचनाकार (या सामान्य विधि) के लिए एक पैरामीटर जोड़ने के लिए मजबूर किया जाता है क्योंकि जावा उस जानकारी को बरकरार नहीं रखता है। को देखो EnumSet.allOfउदाहरण के लिए - विधि करने के लिए सामान्य प्रकार तर्क पर्याप्त होना चाहिए; मुझे "सामान्य" तर्क भी निर्दिष्ट करने की आवश्यकता है? उत्तर: टाइप इरेज़र। इस तरह की चीज एक एपीआई को प्रदूषित करती है। ब्याज से बाहर, क्या आपने .NET जेनरिक का ज्यादा इस्तेमाल किया है? (जारी)
जॉन स्कीट

5
इससे पहले कि मैं .NET जेनरिक का उपयोग करता, मुझे लगता है कि जावा जेनरिक विभिन्न तरीकों से अजीब है (और वाइल्डकार्डिंग अभी भी सिरदर्द है, हालांकि "कॉलर-निर्दिष्ट" रूप में निश्चित रूप से फायदे हैं) - लेकिन यह केवल तब था जब मैंने .NET जेनरिक का उपयोग किया था। थोड़ी देर के लिए मैंने देखा कि जावा जेनरिक के साथ कितने पैटर्न अजीब या असंभव हो गए हैं। यह फिर से ब्लब विरोधाभास है। मैं यह नहीं कह रहा हूँ कि .NET जेनेरिक के पास डाउनसाइड्स नहीं है, btw - विभिन्न प्रकार के रिश्ते हैं जिन्हें व्यक्त नहीं किया जा सकता है, दुर्भाग्य से - लेकिन मैं इसे जावा जेनिक्स के लिए पसंद करता हूं।
जॉन स्कीट

5
@Rogerio: प्रतिबिंब के साथ आप बहुत कुछ कर सकते हैं - लेकिन मुझे यह पता नहीं है कि मैं उन चीजों को लगभग जितनी बार करना चाहता हूं , जितना मैं जावा जेनरिक के साथ नहीं कर सकता हूं। मैं किसी क्षेत्र के लिए टाइप तर्क को लगभग जानना नहीं चाहता क्योंकि मैं किसी वास्तविक वस्तु के प्रकार के तर्क का पता लगाना चाहता हूं।
जॉन स्कीट

41

एक साइड-नोट के रूप में, यह वास्तव में यह देखने के लिए एक दिलचस्प अभ्यास है कि कंपाइलर क्या कर रहा है जब यह मिटाता है - पूरी अवधारणा को समझाना थोड़ा आसान बनाता है। एक विशेष ध्वज है जिसे आप कंपाइलर को जावा फाइलों के आउटपुट में दे सकते हैं जिसमें जेनेरिक मिटाए गए और डाले गए हैं। एक उदाहरण:

javac -XD-printflat -d output_dir SomeFile.java

-printflatझंडा कि संकलक फ़ाइलें उत्पन्न करता है को हस्तांतरित कर दिया जाता है। ( -XDभाग वह है जो javacइसे निष्पादन योग्य जार को सौंपने के लिए कहता है जो वास्तव में संकलन को सिर्फ करने के बजाय करता है javac, लेकिन मैं पचाता हूं ...) यह -d output_dirआवश्यक है क्योंकि कंपाइलर को नई .java फाइलें डालने के लिए कुछ जगह की आवश्यकता होती है।

यह, ज़ाहिर है, सिर्फ मिटाने से अधिक करता है; सभी स्वचालित सामान संकलक यहाँ किया जाता है। उदाहरण के लिए, डिफॉल्ट कंस्ट्रक्टर को भी डाला जाता है, नए फॉर्च-स्टाइल forलूप्स को नियमित forलूप्स तक विस्तारित किया जाता है, आदि छोटी चीजें जो स्वचालित रूप से हो रही हैं, उन्हें देखना अच्छा है।


29

Erasure, का शाब्दिक अर्थ है कि स्रोत कोड में मौजूद प्रकार की जानकारी संकलित बायटेकोड से मिटा दी जाती है। इसे कुछ कोड के साथ समझते हैं।

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class GenericsErasure {
    public static void main(String args[]) {
        List<String> list = new ArrayList<String>();
        list.add("Hello");
        Iterator<String> iter = list.iterator();
        while(iter.hasNext()) {
            String s = iter.next();
            System.out.println(s);
        }
    }
}

यदि आप इस कोड को संकलित करते हैं और फिर इसे जावा डिकंपाइलर के साथ अपघटित करते हैं, तो आपको कुछ ऐसा मिलेगा। ध्यान दें कि विघटित कोड में मूल स्रोत कोड में मौजूद प्रकार की जानकारी का कोई निशान नहीं होता है।

import java.io.PrintStream;
import java.util.*;

public class GenericsErasure
{

    public GenericsErasure()
    {
    }

    public static void main(String args[])
    {
        List list = new ArrayList();
        list.add("Hello");
        String s;
        for(Iterator iter = list.iterator(); iter.hasNext(); System.out.println(s))
            s = (String)iter.next();

    }
} 

मैंने .class फ़ाइल से इरेज़ के बाद कोड को देखने के लिए जावा डिकंपाइलर का उपयोग करने की कोशिश की, लेकिन .class फ़ाइल में अभी भी टाइप जानकारी है। मैंने jigawotकहा, यह काम करता है।
फ्रैंक

25

जॉन स्केट के पहले से ही पूर्ण उत्तर को पूरा करने के लिए, आपको जावा के पिछले संस्करणों के साथ संगतता की आवश्यकता से टाइप इरेज़र व्युत्पन्न की अवधारणा का एहसास करना होगा ।

प्रारंभ में एक्लिप्सन 2007 में प्रस्तुत किया गया (अब उपलब्ध नहीं), संगतता में वे बिंदु शामिल हैं:

  • स्रोत संगतता (अच्छा लगा ...)
  • बाइनरी संगतता (होनी चाहिए!)
  • प्रवासन की अनुकूलता
    • मौजूदा कार्यक्रमों को काम करना जारी रखना चाहिए
    • मौजूदा पुस्तकालयों को सामान्य प्रकारों का उपयोग करने में सक्षम होना चाहिए
    • होना आवश्यक है!

मूल उत्तर:

अत:

new ArrayList<String>() => new ArrayList()

अधिक से अधिक संशोधन के लिए प्रस्ताव हैं । "वास्तविक के रूप में एक अमूर्त अवधारणा को फिर से पाएं" का संदर्भ दें, जहां भाषा निर्माण केवल अवधारणा चीनी होना चाहिए, न कि अवधारणाएं।

मुझे checkCollectionजावा 6 की विधि का भी उल्लेख करना चाहिए , जो निर्दिष्ट संग्रह के एक गतिशील प्रकार के दृश्य को लौटाता है। गलत प्रकार के तत्व को सम्मिलित करने के किसी भी प्रयास का परिणाम तत्काल होगा ClassCastException

भाषा में जेनरिक तंत्र संकलन-समय (स्थैतिक) प्रकार की जाँच प्रदान करता है, लेकिन अनियंत्रित जातियों के साथ इस तंत्र को हराना संभव है

आमतौर पर यह कोई समस्या नहीं है, क्योंकि कंपाइलर ऐसे सभी अनियंत्रित परिचालन पर चेतावनी जारी करता है।

हालाँकि, ऐसे समय जब अकेले स्थैतिक प्रकार की जाँच पर्याप्त नहीं है, जैसे:

  • जब किसी संग्रह को किसी तृतीय-पक्ष लाइब्रेरी में पास किया जाता है और यह अनिवार्य है कि लाइब्रेरी कोड गलत प्रकार के तत्व को सम्मिलित करके संग्रह को दूषित न करे।
  • एक प्रोग्राम एक के साथ विफल हो जाता है ClassCastException, यह दर्शाता है कि एक गलत तरीके से टाइप किया गया तत्व एक मानकीकृत संग्रह में रखा गया था। दुर्भाग्य से, अपवाद किसी भी समय हो सकता है जब गलत तत्व डाला जाता है, तो यह आमतौर पर समस्या के वास्तविक स्रोत के रूप में बहुत कम या कोई जानकारी प्रदान करता है।

जुलाई 2012 को अपडेट करें, लगभग चार साल बाद:

अब यह (2012) " एपीआई माइग्रेशन कम्पेटिबिलिटी रूल्स (सिग्नेचर टेस्ट) " में विस्तृत है

जावा प्रोग्रामिंग लैंग्वेज इरेज़र का उपयोग करके जेनरिक को लागू करती है, जो यह सुनिश्चित करती है कि विरासत और जेनेरिक संस्करण आमतौर पर समान श्रेणी की फाइलें उत्पन्न करते हैं, जो कि प्रकारों के बारे में कुछ सहायक जानकारी को छोड़कर। बाइनरी संगतता नहीं टूटी है क्योंकि किसी भी क्लाइंट कोड को बदलने या पुन: स्थापित किए बिना एक जेनेरिक क्लास फ़ाइल के साथ एक विरासत वर्ग फ़ाइल को बदलना संभव है।

गैर-जेनेरिक विरासत कोड के साथ इंटरफेस की सुविधा के लिए, एक प्रकार के रूप में एक मानकीकृत प्रकार के मिटाने का उपयोग करना संभव है। इस तरह के एक प्रकार को एक कच्चा प्रकार कहा जाता है ( जावा भाषा विनिर्देश 3 / 4.8 )। कच्चे प्रकार की अनुमति देने से स्रोत कोड के लिए पिछड़ी संगतता भी सुनिश्चित होती है।

इसके अनुसार, java.util.Iteratorकक्षा के निम्नलिखित संस्करण द्विआधारी और स्रोत कोड पिछड़े संगत दोनों हैं:

Class java.util.Iterator as it is defined in Java SE version 1.4:

public interface Iterator {
    boolean hasNext();
    Object next();
    void remove();
}

Class java.util.Iterator as it is defined in Java SE version 5.0:

public interface Iterator<E> {
    boolean hasNext();
    E next();
    void remove();
}

2
ध्यान दें कि प्रकार के क्षरण के बिना पश्चगामी संगतता हासिल की जा सकती थी, लेकिन जावा प्रोग्रामर के बिना संग्रह का एक नया सेट सीखने के बिना नहीं। ठीक यही मार्ग है कि .NET गया। दूसरे शब्दों में, यह यह तीसरी गोली है जो महत्वपूर्ण है। (जारी।)
जॉन स्कीट

15
व्यक्तिगत रूप से मुझे लगता है कि यह एक Myopic गलती थी - इसने एक अल्पकालिक लाभ और दीर्घकालिक नुकसान दिया।
जॉन स्कीट

8

पहले से ही पूरक जॉन स्कीट उत्तर को लागू करना ...

यह उल्लेख किया गया है कि उन्मूलन के माध्यम से जेनेरिक को लागू करना कुछ कष्टप्रद सीमाओं (जैसे नहीं new T[42]) की ओर जाता है । यह भी उल्लेख किया गया है कि चीजों को इस तरह से करने का प्राथमिक कारण बाईटकोड में पश्चगामी संगतता थी। यह (ज्यादातर) सच भी है। बाइटकोड जेनरेट किया गया -target 1.5 सिर्फ डी-सुगरेड कास्टिंग -target 1.4 से कुछ अलग है। तकनीकी रूप से, यह रनटाइम में सामान्य प्रकार के इंस्टेंटिएशन तक पहुंच प्राप्त करने के लिए (अपार प्रवंचना के माध्यम से) संभव है, यह साबित करता है कि वास्तव में बाईटकोड में कुछ है।

अधिक दिलचस्प बिंदु (जो उठाया नहीं गया है) यह है कि उन्मूलन का उपयोग करके जेनरिक को लागू करना उच्च-स्तरीय प्रकार प्रणाली को पूरा करने में काफी अधिक लचीलापन प्रदान करता है। इसका एक अच्छा उदाहरण स्काला का जेवीएम कार्यान्वयन बनाम सीएलआर होगा। जेवीएम पर, इस तथ्य के कारण सीधे उच्च-प्रकार को लागू करना संभव है कि जेवीएम स्वयं जेनेरिक प्रकारों पर कोई प्रतिबंध नहीं लगाता है (क्योंकि ये "प्रकार" प्रभावी रूप से अनुपस्थित हैं)। यह सीएलआर के साथ विरोधाभास है, जिसमें पैरामीटर इंस्टेंटिएशन का रनटाइम ज्ञान है। इस वजह से, सीएलआर को स्वयं इस बात की कुछ अवधारणा होनी चाहिए कि कैसे जेनरिक का उपयोग किया जाना चाहिए, सिस्टम को अप्रत्याशित नियमों के साथ विस्तारित करने के प्रयासों को शून्य कर सकता है। परिणामस्वरूप, सीएलआर पर स्काला के उच्च-प्रकार को कंपाइलर के भीतर उत्सर्जित मिटाए जाने के एक अजीब रूप का उपयोग करके लागू किया जाता है,

जब आप रनटाइम में शरारती चीजें करना चाहते हैं, तो इरेज़र असुविधाजनक हो सकता है, लेकिन यह कंपाइलर लेखकों को सबसे अधिक लचीलापन प्रदान करता है। मैं अंदाजा लगा रहा हूं कि यह जल्द ही दूर क्यों नहीं हो रहा है।


6
जब आप निष्पादन के समय "शरारती" चीजें करना चाहते हैं तो असुविधा नहीं होती है। यह तब है जब आप निष्पादन के समय पूरी तरह से उचित चीजें करना चाहते हैं। वास्तव में, टाइप इरेज़र आपको दूर की चीज़ों को करने की अनुमति देता है - जैसे कि लिस्ट <स्ट्रिंग> को लिस्ट में डालना और फिर केवल चेतावनियों के साथ लिस्ट <डेट>।
जॉन स्कीट

5

जैसा कि मैं इसे समझता हूं (एक .NET पुरुष होने के नाते ) जेवीएम में जेनरिक की कोई अवधारणा नहीं है, इसलिए कंपाइलर ऑब्जेक्ट के साथ प्रकार के मापदंडों को बदल देता है और आपके लिए सभी कास्टिंग करता है।

इसका मतलब यह है कि जावा जेनरिक सिंटैक्स शुगर के अलावा कुछ भी नहीं है और मूल्य प्रकारों के लिए किसी भी प्रदर्शन में सुधार की पेशकश नहीं करते हैं जो संदर्भ के दौरान पास होने पर बॉक्सिंग / अनबॉक्सिंग की आवश्यकता होती है।


3
जावा जेनेरिक वैसे भी मूल्य प्रकार का प्रतिनिधित्व नहीं कर सकता है - सूची <int> जैसी कोई चीज नहीं है। हालांकि, जावा में कोई पास-पास-संदर्भ नहीं है - यह सख्ती से मूल्य से गुजरता है (जहां मूल्य एक संदर्भ हो सकता है।)
जॉन स्कीट

2

अच्छी व्याख्याएँ हैं। मैं केवल यह दिखाने के लिए एक उदाहरण जोड़ता हूं कि प्रकार का क्षय एक डीकॉम्पेलर के साथ कैसे काम करता है।

मूल वर्ग,

import java.util.ArrayList;
import java.util.List;


public class S<T> {

    T obj; 

    S(T o) {
        obj = o;
    }

    T getob() {
        return obj;
    }

    public static void main(String args[]) {
        List<String> list = new ArrayList<>();
        list.add("Hello");

        // for-each
        for(String s : list) {
            String temp = s;
            System.out.println(temp);
        }

        // stream
        list.forEach(System.out::println);
    }
}

अपने बायोटेक से डिकम्पोज्ड कोड,

import java.io.PrintStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.Objects;
import java.util.function.Consumer;

public class S {

   Object obj;


   S(Object var1) {
      this.obj = var1;
   }

   Object getob() {
      return this.obj;
   }

   public static void main(String[] var0) {

   ArrayList var1 = new ArrayList();
   var1.add("Hello");


   // for-each
   Iterator iterator = var1.iterator();

   while (iterator.hasNext()) {
         String string;
         String string2 = string = (String)iterator.next();
         System.out.println(string2);
   }


   // stream
   PrintStream printStream = System.out;
   Objects.requireNonNull(printStream);
   var1.forEach(printStream::println);


   }
}

2

जेनेरिक का उपयोग क्यों करें

संक्षेप में, जेनेरिक प्रकार (वर्गों और इंटरफेस) को सक्षम करते हैं जब वर्गों, इंटरफेस और विधियों को परिभाषित करते समय पैरामीटर होते हैं। विधि घोषणाओं में उपयोग किए जाने वाले अधिक परिचित औपचारिक मापदंडों की तरह, प्रकार के पैरामीटर आपके लिए अलग-अलग इनपुट के साथ एक ही कोड को फिर से उपयोग करने का एक तरीका प्रदान करते हैं। अंतर यह है कि औपचारिक मापदंडों के लिए इनपुट मूल्य हैं, जबकि टाइप पैरामीटर के लिए इनपुट प्रकार हैं। गैर-जेनेरिक कोड पर जेनरिक का उपयोग करने वाले ode के कई लाभ हैं:

  • संकलन के समय मजबूत प्रकार की जाँच।
  • जातियों का उन्मूलन।
  • सामान्य एल्गोरिदम को लागू करने के लिए प्रोग्रामर को सक्षम करना।

टाइप एरासुरे क्या है

संकलन समय पर तंग प्रकार की जांच प्रदान करने और सामान्य प्रोग्रामिंग का समर्थन करने के लिए जावा भाषा में जेनरिक की शुरुआत की गई। जेनरिक को लागू करने के लिए, जावा कंपाइलर निम्न प्रकार पर लागू होता है:

  • सभी प्रकार के मापदंडों को जेनेरिक प्रकारों में उनकी सीमा या ऑब्जेक्ट के साथ बदलें यदि प्रकार पैरामीटर अनबाउंड हैं। इसलिए, उत्पादित बायोटेक में केवल सामान्य वर्ग, इंटरफेस और विधियां शामिल हैं।
  • यदि आवश्यक हो तो टाइप सेफ्टी को सुरक्षित रखने के लिए टाइप कास्ट डालें।
  • विस्तारित सामान्य प्रकारों में बहुरूपता को संरक्षित करने के लिए सेतु विधियाँ उत्पन्न करें।

[एनबी] पुल की विधि क्या है? शीघ्र ही, जैसे कि एक पैरामीटर इंटरफ़ेस के मामले में Comparable<T>, यह अतिरिक्त तरीकों को संकलक द्वारा डाला जा सकता है; इन अतिरिक्त तरीकों को पुल कहा जाता है।

कैसे काम करता है Erasure

एक प्रकार की इरेज़र को इस प्रकार परिभाषित किया गया है: सभी प्रकार के मापदंडों को पैरामीटर प्रकार से ड्रॉप करें, और किसी भी प्रकार के वेरिएबल को उसकी बाध्यता के उन्मूलन के साथ बदलें, या ऑब्जेक्ट के साथ यदि इसकी कोई बाध्यता नहीं है, या सबसे बाईं ओर के इरेज़र के साथ यदि यह है तो कई सीमाएँ। यहाँ कुछ उदाहरण हैं:

  • का विलोपन List<Integer>, List<String>और List<List<String>>है List
  • का मिटना List<Integer>[]है List[]
  • का विलोपन Listस्वयं है, इसी तरह किसी भी कच्चे प्रकार के लिए।
  • इंट का उन्मूलन स्वयं है, इसी तरह किसी भी आदिम प्रकार के लिए।
  • अपने आप को मिटाना Integer, इसी तरह बिना किसी प्रकार के मापदंडों के लिए होता है।
  • की Tपरिभाषा में मिटना asListहै Object, क्योंकि T कोई बाध्य नहीं है।
  • की Tपरिभाषा में मिटना maxहै Comparable, क्योंकि T बाध्य है Comparable<? super T>
  • का विलोपन Tके अंतिम परिभाषा maxहै Object, क्योंकि Tबाध्य है Objectऔर Comparable<T>और हम सबसे बाईं ओर के लिए बाध्य का विलोपन ले।

जेनरिक का उपयोग करते समय सावधानी बरतने की जरूरत है

जावा में, दो अलग-अलग विधियों में समान हस्ताक्षर नहीं हो सकते हैं। चूंकि जेनेरिक को इरेज़र द्वारा लागू किया जाता है, इसलिए यह भी कहा जाता है कि दो अलग-अलग तरीकों में एक ही इरेज़र के साथ हस्ताक्षर नहीं हो सकते हैं। एक वर्ग दो तरीकों को अधिभारित नहीं कर सकता है जिनके हस्ताक्षर समान मिटाए गए हैं, और एक वर्ग दो इंटरफेस को लागू नहीं कर सकता है जिनके पास एक ही मिटा है।

    class Overloaded2 {
        // compile-time error, cannot overload two methods with same erasure
        public static boolean allZero(List<Integer> ints) {
            for (int i : ints) if (i != 0) return false;
            return true;
        }
        public static boolean allZero(List<String> strings) {
            for (String s : strings) if (s.length() != 0) return false;
            return true;
        }
    }

हम इस कोड को इस प्रकार काम करना चाहते हैं:

assert allZero(Arrays.asList(0,0,0));
assert allZero(Arrays.asList("","",""));

हालाँकि, इस मामले में दोनों विधियों के हस्ताक्षर एक समान हैं:

boolean allZero(List)

इसलिए, संकलन समय पर एक नाम क्लैश की सूचना है। दोनों विधियों को एक ही नाम देना संभव नहीं है और ओवरलोडिंग द्वारा उनके बीच अंतर करने की कोशिश करें, क्योंकि उन्मूलन के बाद एक विधि कॉल को दूसरे से अलग करना असंभव है।

उम्मीद है, पाठक मज़ा आएगा :)

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.