जावा 8 में, क्या यह विधि संदर्भ अभिव्यक्तियों या कार्यात्मक इंटरफ़ेस के कार्यान्वयन को वापस करने वाले तरीकों का उपयोग करने के लिए शैलीगत रूप से बेहतर है?


11

जावा 8 ने कार्यात्मक इंटरफेस की अवधारणा को जोड़ा , साथ ही कई नए तरीके जो कार्यात्मक इंटरफेस लेने के लिए डिज़ाइन किए गए हैं। इन इंटरफेस के उदाहरणों को विधि संदर्भ अभिव्यक्ति (जैसे SomeClass::someMethod) और लैम्ब्डा एक्सप्रेशन (जैसे (x, y) -> x + y) का उपयोग करके आसानी से बनाया जा सकता है ।

एक सहकर्मी और मेरे बीच अलग-अलग राय है जब एक रूप या किसी अन्य का उपयोग करना सबसे अच्छा है (जहां इस मामले में "सबसे अच्छा" वास्तव में "सबसे पठनीय" और "सबसे अधिक मानक प्रथाओं के अनुरूप है", जैसा कि वे मूल रूप से अन्यथा हैं समकक्ष)। विशेष रूप से, इसमें वह मामला शामिल है जहां निम्नलिखित सभी सत्य हैं:

  • प्रश्न में फ़ंक्शन का उपयोग एक दायरे के बाहर नहीं किया जाता है
  • उदाहरण को नाम देने से पठनीयता में मदद मिलती है (उदाहरण के लिए तर्क का विरोध करना सरल है कि यह देखना आसान है कि एक नज़र में क्या हो रहा है)
  • अन्य प्रोग्रामिंग कारण नहीं हैं कि एक रूप दूसरे के लिए बेहतर क्यों होगा।

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

मेरे सहकर्मी की प्राथमिकता में एक विधि है जो इंटरफ़ेस का उदाहरण लौटाता है (उदाहरण के लिए "परिणाम को आगे बढ़ाएं) ()"। मेरे लिए, यह महसूस करता है कि यह बिल्कुल नहीं है कि इस सुविधा का उपयोग करने का इरादा कैसे था, थोड़ा अव्यवस्थित लगता है, और ऐसा लगता है कि नामकरण के माध्यम से वास्तव में इरादे का संचार करना कठिन है।

इस उदाहरण को ठोस बनाने के लिए, यहाँ एक ही कोड है, जो विभिन्न शैलियों में लिखा गया है:

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(this::isResultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private boolean isResultInFuture(Result result) {
    someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

बनाम

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    results.filter(resultInFuture()).forEach({ result ->
      // Do something important with each future result line
    });
  }

  private Predicate<Result> resultInFuture() {
    return result -> someOtherService.getResultDateFromDatabase(result).after(new Date());
  }
}

बनाम

public class ResultProcessor {
  public void doSomethingImportant(List<Result> results) {
    Predicate<Result> resultInFuture = result -> someOtherService.getResultDateFromDatabase(result).after(new Date());

    results.filter(resultInFuture).forEach({ result ->
      // Do something important with each future result line
    });
  }
}

क्या कोई आधिकारिक या अर्ध-आधिकारिक दस्तावेज या टिप्पणी है कि क्या एक दृष्टिकोण अधिक पसंदीदा है, भाषा डिजाइनरों के इरादों के साथ अधिक इन-लाइन, या अधिक पठनीय है? एक आधिकारिक स्रोत को छोड़कर, क्या कोई स्पष्ट कारण हैं कि कोई बेहतर दृष्टिकोण क्यों होगा?


1
लैम्बदास को एक कारण से भाषा में जोड़ा गया था, और यह जावा को बदतर बनाने के लिए नहीं था।

@Snowman जो बिना कहे चला जाता है। इसलिए मुझे लगता है कि आपकी टिप्पणी और मेरे प्रश्न के बीच कुछ निहित संबंध हैं जो मैं काफी नहीं पकड़ रहा हूं। वह बिंदु जो आप बनाना चाह रहे हैं?
एम। जस्टिन

2
मुझे लगता है कि वह जो बिंदु बना रहा है, वह यह है कि लंबोदर ने यथास्थिति में सुधार नहीं किया था, उन्होंने उन्हें पेश करने की जहमत नहीं उठाई होगी।
रॉबर्ट हार्वे

2
@ रोबर्टहर्वे ज़रूर। उस बिंदु पर, हालांकि, लैम्बदास और विधि संदर्भ दोनों को एक ही समय में जोड़ा गया था, इसलिए पहले कभी भी यथास्थिति नहीं थी। इस विशेष मामले के बिना भी, ऐसे समय हैं जब विधि संदर्भ अभी भी बेहतर होंगे; उदाहरण के लिए, एक मौजूदा सार्वजनिक विधि (जैसे Object::toString)। तो मेरा प्रश्न इस बारे में अधिक है कि क्या इस विशेष प्रकार के उदाहरण में यह बेहतर है कि मैं यहां से बाहर कर रहा हूं, क्या ऐसे उदाहरण मौजूद हैं जहां एक दूसरे से बेहतर है, या इसके विपरीत।
एम। जस्टिन

जवाबों:


8

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

Predicate<Result> pred = result -> this.isResultInFuture(result);
Predicate<Result> pred = this::isResultInFuture;

ये ऑपरेशनल रूप से समतुल्य हैं, और पहले को पॉइंटफुल स्टाइल कहा जाता है जबकि दूसरा पॉइंट फ्री स्टाइल है। शब्द "बिंदु" एक नामित फ़ंक्शन तर्क (यह टोपोलॉजी से आता है) को संदर्भित करता है जो बाद के मामले में गायब है।

अधिक सामान्यतः, सभी कार्यों के लिए एफ, एक रैपर लैम्ब्डा जो दिए गए सभी तर्कों को लेता है और उन्हें एफ अपरिवर्तित करता है, फिर एफ अपरिवर्तित का परिणाम एफ के समान होता है। लैम्ब्डा को हटाना और सीधे एफ का उपयोग करना (बिंदु से मुक्त शैली से ऊपर की ओर बिंदु शैली तक जाना) को एटा कमी कहा जाता है , एक शब्द जिसे लैम्ब्डा पथरी से उपजा है

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

public static Function<A, C> compose(Function<A, B> first, Function<B, C> second) {
  return (value) -> second(first(value));
}

अब मान लीजिए कि हमारे पास एक तरीका है Result deriveResult(Foo foo)। चूंकि एक विधेय एक कार्य है, हम एक नए विधेय का निर्माण कर सकते हैं जो पहले कॉल करता है deriveResultऔर फिर व्युत्पन्न परिणाम का परीक्षण करता है:

Predicate<Foo> isFoosResultInFuture =
    compose(this::deriveResult, this::isResultInFuture);

यद्यपि लैंबडास के साथ बिंदुगत शैली के उपयोग का कार्यान्वयनcompose , परिभाषित करने के लिए रचना का उपयोगisFoosResultInFuture बिंदु मुक्त है क्योंकि किसी भी तर्क का उल्लेख करने की आवश्यकता नहीं है।

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


1
मुझे लगता है कि मेरे सहकर्मी और मैं जिस पर चर्चा कर रहे हैं, वह इन दो कामों में अधिक है: Predicate<Result> pred = this::isResultInFuture;और Predicate<Result> pred = resultInFuture();जहां resultInFuture () रिटर्न ए Predicate<Result>। इसलिए जब यह विधि संदर्भ बनाम लैम्ब्डा अभिव्यक्ति का उपयोग करने के लिए एक महान विवरण है, तो यह इस तीसरे मामले को काफी कवर नहीं करता है।
एम। जस्टिन

4
@ एम। जस्टिन: resultInFuture();असाइनमेंट मेरे द्वारा प्रस्तुत किए गए लैम्ब्डा असाइनमेंट के समान है, मेरे मामले को छोड़कर आपका resultInFuture()तरीका इनबिल्ट है। इसलिए उस दृष्टिकोण में अप्रत्यक्ष की दो परतें हैं (जिनमें से कुछ भी नहीं है) - लैम्बडा-कंस्ट्रक्शन विधि और लैम्बडा स्वयं। और भी बदतर!
जैक

5

वर्तमान में से कोई भी उत्तर वास्तव में प्रश्न के मूल को संबोधित नहीं करता है, जो यह है कि क्या कक्षा में एक private boolean isResultInFuture(Result result)विधि या एक private Predicate<Result> resultInFuture()विधि होनी चाहिए । जबकि वे काफी हद तक समतुल्य हैं, प्रत्येक के लिए फायदे और नुकसान हैं।

पहला तरीका अधिक स्वाभाविक है यदि आप विधि संदर्भ बनाने के अलावा सीधे तरीके से कॉल करते हैं। उदाहरण के लिए, यदि आप इस पद्धति का परीक्षण करते हैं, तो पहले दृष्टिकोण का उपयोग करना आसान हो सकता है। पहले दृष्टिकोण के लिए एक और फायदा यह है कि इस पद्धति की परवाह नहीं की जाती है कि इसका उपयोग कैसे किया जाता है, इसकी कॉल साइट से इसे हटाने से। यदि आप बाद में कक्षा को बदलते हैं ताकि आप सीधे तरीके से कॉल करें, तो यह डिकॉउलिंग बहुत कम तरीके से भुगतान करेगा।

पहला तरीका भी बेहतर है यदि आप इस पद्धति को विभिन्न कार्यात्मक इंटरफेस या इंटरफेस के विभिन्न यूनियनों के अनुकूल बनाना चाहते हैं। उदाहरण के लिए, आप चाहते हैं कि विधि संदर्भ प्रकार की हो Predicate<Result> & Serializable, जिसे दूसरा दृष्टिकोण आपको प्राप्त करने की सुविधा नहीं देता है।

दूसरी ओर, दूसरा दृष्टिकोण अधिक स्वाभाविक है यदि आप विधेय पर उच्च क्रम संचालन करने का इरादा रखते हैं। प्रेडिकेट के पास इस पर कई तरीके हैं जिन्हें सीधे विधि संदर्भ में नहीं कहा जा सकता है। यदि आप इन विधियों का उपयोग करने का इरादा रखते हैं, तो दूसरा तरीका बेहतर है।

अंततः निर्णय व्यक्तिपरक है। लेकिन अगर आप विधेय के तरीकों को बुलाने का इरादा नहीं रखते हैं, तो मैं पहले दृष्टिकोण को प्राथमिकता देने की ओर झुकूंगा।


विधेय पर उच्च आदेश संचालन अभी भी विधि संदर्भ कास्टिंग द्वारा उपलब्ध हैं:((Predicate<Resutl>) this::isResultInFuture).whatever(...)
जैक

4

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

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

इसका मतलब है कि अगर आपका लैम्ब्डा छोटा होने के बाद आसानी से पढ़ा जा सकता है, जैसे @JimmyJames की टिप्पणी से, तो इसके लिए जाएं:

people = sort(people, (a,b) -> b.age() - a.age());

जब आप अपने उदाहरण लैम्बडा को इनलाइन करने की कोशिश करते हैं, तो इसे पठनीयता के साथ तुलना करें:

results.filter(result -> someOtherService.getResultDateFromDatabase(result).after(new Date()))...

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


2
पुन: वर्बोसिटी - जबकि नए कार्यात्मक परिवर्धन स्वयं द्वारा बहुत अधिक शून्यता को कम नहीं करते हैं, वे ऐसा करने के तरीकों की अनुमति देते हैं। उदाहरण के लिए आप परिभाषित कर सकते हैं: public static final <T> List<T> sort(List<T> list, Comparator<T> comparator)स्पष्ट कार्यान्वयन के साथ और फिर आप इस तरह कोड लिख सकते हैं: people = sort(people, (a,b) -> b.age() - a.age());जो एक बड़ा सुधार आईएमओ है।
जिमीजैम 20

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

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