array - pass by value and reference in java




هل جافا "المرور بالمرور" أو "تمرير حسب القيمة"؟ (20)

يكون المرجع دائمًا قيمة عند تمثيله ، بغض النظر عن اللغة التي تستخدمها.

الحصول على خارج عرض مربع ، دعنا ننظر إلى الجمعية أو بعض إدارة الذاكرة منخفضة المستوى. على مستوى وحدة المعالجة المركزية ، تصبح الإشارة إلى أي شيء على الفور قيمة إذا تمت كتابتها إلى الذاكرة أو إلى أحد سجلات وحدة المعالجة المركزية. (هذا هو السبب في أن المؤشر هو تعريف جيد. إنها قيمة ، لها غرض في نفس الوقت).

البيانات في الذاكرة لديها موقع وفي هذا الموقع هناك قيمة (بايت ، كلمة ، أيا كان). في الجمعية لدينا حل مناسب لإعطاء اسم لبعض الموقع (ويعرف أيضا باسم متغير)، ولكن عندما ترجمة التعليمات البرمجية، المجمع ببساطة استبدال اسم مع مكان معين تماما مثل متصفحك يستبدل أسماء النطاقات مع عناوين IP.

وصولاً إلى المركز ، من المستحيل تقنياً تمرير إشارة إلى أي لغة بأي لغة دون تمثيلها (عندما تصبح قيمة على الفور).

دعونا نقول لدينا فو متغير، على الموقع هو في البايت 47th في الذاكرة ولها قيمة هو 5. لدينا متغير آخر Ref2Foo الذي هو في البايت 223rd في الذاكرة، وسوف تكون قيمته 47. هذا Ref2Foo قد يكون متغير الفني ، لم يتم إنشاؤه بشكل صريح من قبل البرنامج. إذا نظرت فقط إلى الرقمين 5 و 47 دون أي معلومات أخرى ، سترى قيمتين فقط . إذا كنت تستخدمها كمراجع ثم للوصول إلى 5لدينا للسفر:

(Name)[Location] -> [Value at the Location]
---------------------
(Ref2Foo)[223]  -> 47
(Foo)[47]       -> 5

هذه هي الطريقة التي تعمل بها جداول القفز.

إذا أردنا استدعاء طريقة / وظيفة / إجراء مع قيمة Foo ، فهناك بعض الطرق الممكنة لتمرير المتغير إلى الطريقة ، اعتمادًا على اللغة وأوضاع استدعائها للأسلوب العديدة:

  1. يتم نسخ 5 إلى واحد من سجلات وحدة المعالجة المركزية (أي. EAX).
  2. 5 يحصل على PUSHd إلى المكدس.
  3. 47 يتم نسخها إلى أحد سجلات وحدة المعالجة المركزية
  4. 47 دفع إلى المكدس.
  5. يتم نسخ 223 إلى أحد سجلات وحدة المعالجة المركزية.
  6. 223 يحصل PUSHd إلى المكدس.

في كل الحالات فوق القيمة - وهي نسخة من قيمة موجودة - تم إنشاؤها ، أصبحت الآن طريقة الاستلام للتعامل معها. عند كتابة "فو" داخل الأسلوب، فإنه إما تلا من EAX، أو تلقائيا ألغى الإشارة القيمة ، أو ألغى الإشارة القيمة مزدوج، عملية يعتمد على كيفية عمل اللغة و / أو ما نوع فو يملي. هذا مخفي عن المطور حتى تتحايل على عملية إلغاء الإشارة. إذن ، المرجع هو قيمة عند تمثيلها ، لأن المرجع هو قيمة يجب معالجتها (على مستوى اللغة).

الآن مررنا بـ Foo إلى الأسلوب:

  • في حالة 1. و 2. إذا قمت بتغيير Foo ( Foo = 9) فإنه يؤثر فقط على النطاق المحلي لأن لديك نسخة من القيمة. من داخل الطريقة ، لا يمكننا حتى تحديد مكان الذاكرة الأصلية لموقع فو.
  • في حالة 3. و 4. إذا كنت تستخدم تركيبات اللغة الافتراضية وتغيير Foo ( Foo = 11) ، فيمكنه تغيير Foo عالميًا (يعتمد على اللغة ، أي ، Java أو مثل procedure findMin(x, y, z: integer; var 's's var m: integer); ). لكن إذا سمحت لك اللغة بالتحايل على عملية عدم التدخل ، فيمكنك التغيير 47، على سبيل المثال 49. عند هذه النقطة يبدو أن Foo قد تغيرت إذا قرأته ، لأنك قمت بتغيير المؤشر المحلي إليه. وإذا Foo = 12كنت ستقوم بتعديل هذا Foo داخل الطريقة ( ) فستفعل على الأرجح تنفيذ البرنامج (aka. segfault) لأنك ستكتب إلى ذاكرة مختلفة عن المتوقع ، يمكنك حتى تعديل منطقة مقدر لعقد الملف القابل للتنفيذ البرنامج والكتابة إليه سيتم تعديل تشغيل التعليمات البرمجية (فو ليست الآن في 47). لكن قيمة Foo ل47لم تتغير على الصعيد العالمي ، إلا واحد داخل الأسلوب ، لأنه 47كان أيضا نسخة إلى الأسلوب.
  • في حالة 5. و 6. إذا قمت بتعديل 223داخل الأسلوب فإنه يخلق نفس الفوضى كما في 3. أو 4. (مؤشر ، مشيرا إلى قيمة سيئة الآن ، التي يتم استخدامها مرة أخرى كمؤشر) ولكن هذا لا يزال المحلية المشكلة ، كما تم نسخ 223 . ومع ذلك ، إذا كنت قادرًا على التراجع Ref2Foo(أي 223) ، فعليك بالوصول إلى القيمة المدببة وتعديلها 47، على سبيل المثال ، إلى 49أنها ستؤثر على Foo عالميًا ، لأنه في هذه الحالة ، حصلت الطرق على نسخة من المرسل 223ولكن لم يتم العثور عليها 47إلا مرة واحدة وتغييرها ل 49سيؤدي كل Ref2Fooالمزدوج dereferencing إلى قيمة خاطئة.

نيتكبنغ على تفاصيل غير مهمة ، حتى اللغات التي لا تعبر عن طريق تمرير القيم إلى وظائف ، ولكن هذه الوظائف تعرف أنها يجب أن تستخدمها لأغراض إلغاء الإشارة. يتم إخفاء هذا المرجع كمرجع فقط من المبرمج لأنه غير مجدية من الناحية العملية والمصطلحات هي مجرد مرجع .

كما أن عدم التمريرة الصارمة غير مجدية أيضًا ، فهذا يعني أنه يجب نسخ مصفوفة 100 Mbyte في كل مرة نسمي فيها طريقة مع الصفيف كوسيطة ، وبالتالي لا يمكن أن تكون جافا بشكل جذري. كل لغة من شأنها تمرير إشارة إلى هذه المصفوفة الضخمة (كقيمة) وتوظف آلية النسخ عند الكتابة إذا كان من الممكن تغيير هذه المصفوفة محليًا داخل الطريقة أو تسمح للطريقة (كما تفعل Java) بتعديل المصفوفة عالميًا (من وجهة نظر المتصل) وعدد قليل من اللغات يسمح بتعديل قيمة المرجع نفسه.

باختصار وفي مصطلحات جافا نفسها ، جافا هي القيمة بالقيمة التي يمكن أن تكون القيمة : إما قيمة حقيقية أو قيمة تمثل إشارة مرجعية .

كنت أظن دائمًا أن جافا كانت متجاورة .

ومع ذلك ، فقد رأيت عددًا من مشاركات المدونات (على سبيل المثال ، هذه المدونة ) التي تدعي أنها ليست كذلك.

لا أعتقد أنني أفهم التمييز الذي يقومون به.

ما هو التفسير؟


أشعر أن الجدل حول "تمرير حسب المرجع مقابل تمرير القيمة" ليس مفيدًا للغاية.

إذا قلت ، "جافا هي كلمة مرور (أذكر / قيمة)" ، وفي كلتا الحالتين ، فإنك لا تقدم إجابة كاملة. إليك بعض المعلومات الإضافية التي نأمل أن تساعد في فهم ما يحدث في الذاكرة.

دورة تحطم على كومة / كومة قبل أن نصل إلى تنفيذ جافا: القيم تذهب وخارج الكومة بطريقة منظمة لطيفة ، مثل كومة من لوحات في الكافتيريا. الذاكرة في الكومة (المعروفة أيضًا بالذاكرة الديناميكية) هي عشوائية وغير منظمة. يجد JVM فقط مساحة حيثما أمكن ، ويحررها لأن المتغيرات التي تستخدمها لم تعد هناك حاجة إليها.

حسنا. أولا ، الأوليات المحلية تذهب على المكدس. هذا الكود

int x = 3;
float y = 101.1f;
boolean amIAwesome = true;

النتائج في هذا:

عندما تعلن وإنشاء كائن. يذهب الكائن الفعلي على كومة الذاكرة المؤقتة. ماذا يحدث في المكدس؟ عنوان الكائن على كومة الذاكرة المؤقتة. سيسميه مبرمجو C ++ هذا المؤشر ، لكن بعض مطوري جافا هم ضد كلمة "pointer". ايا كان. اعلم فقط أن عنوان الكائن ينتقل إلى المكدس.

مثل ذلك:

int problems = 99;
String name = "Jay-Z";

المصفوفة هي كائن ، لذا فإنها تظهر في الكومة أيضًا. وماذا عن الأشياء في الصفيف؟ يحصلون على مساحة الكومة الخاصة بهم ، ويخرج عنوان كل كائن داخل الصفيف.

JButton[] marxBros = new JButton[3];
marxBros[0] = new JButton("Groucho");
marxBros[1] = new JButton("Zeppo");
marxBros[2] = new JButton("Harpo");

إذن ، ما الذي يتم تمريره عند استدعاء طريقة؟ إذا قمت بتمرير كائن ، فإن ما تمر عليه هو عنوان الكائن. قد يقول البعض "قيمة" العنوان ، والبعض يقول إنها مجرد إشارة إلى الكائن. هذا هو نشأة الحرب المقدسة بين أنصار "المرجعية" و "القيمة". ما تسمونه ليس بنفس أهمية فهمك أن ما يتم تمريره هو عنوان الكائن.

private static void shout(String name){
    System.out.println("There goes " + name + "!");
}

public static void main(String[] args){
    String hisName = "John J. Jingleheimerschmitz";
    String myName = hisName;
    shout(myName);
}

يتم إنشاء سلسلة واحدة ويتم تخصيص المساحة لها في كومة الذاكرة المؤقتة ، ويتم تخزين العنوان إلى السلسلة في المكدس hisName المعرّف hisName ، نظرًا لأن عنوان السلسلة الثانية هو نفسه hisName ، لا يتم إنشاء سلسلة جديدة ولم يتم تخصيص مساحة كومة جديدة ، ولكن يتم إنشاء معرّف جديد في بنية تخزين العناصر. ثم ندعو shout() : يتم إنشاء إطار مكدس جديد ويتم إنشاء معرف جديد name وتعيين عنوان السلسلة الموجودة بالفعل.

إذن ، القيمة ، المرجع؟ أنت تقول "البطاطا".


جافا دائما على اساس القيمة . لسوء الحظ ، قرروا استدعاء موقع كائن "مرجع". عندما نمرر قيمة كائن ، فإننا نمرر الإشارة إليه. هذا هو مربك للمبتدئين.

يذهب مثل هذا:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    // we pass the object to foo
    foo(aDog);
    // aDog variable is still pointing to the "Max" dog when foo(...) returns
    aDog.getName().equals("Max"); // true
    aDog.getName().equals("Fifi"); // false 
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // change d inside of foo() to point to a new Dog instance "Fifi"
    d = new Dog("Fifi");
    d.getName().equals("Fifi"); // true
}

في المثال أعلاه ، aDog.getName() بإرجاع "Max" . لا يتم تغيير قيمة aDog داخل main في الدالة foo باستخدام Dog "Fifi" حيث يتم تمرير مرجع الكائن حسب القيمة. إذا تم تمريره بالإشارة ، فإن aDog.getName() في main سيعود "Fifi" بعد استدعاء foo .

بطريقة مماثلة:

public static void main(String[] args) {
    Dog aDog = new Dog("Max");
    foo(aDog);
    // when foo(...) returns, the name of the dog has been changed to "Fifi"
    aDog.getName().equals("Fifi"); // true
}

public static void foo(Dog d) {
    d.getName().equals("Max"); // true
    // this changes the name of d to be "Fifi"
    d.setName("Fifi");
}

في المثال أعلاه ، Fifi هو اسم الكلب بعد استدعاء foo(aDog) لأنه تم تعيين اسم الكائن داخل foo(...) . أي عمليات تقوم بها foo على d هي مثل ، لجميع الأغراض العملية ، يتم تنفيذها على aDog نفسها (باستثناء عندما يتم تغيير d للإشارة إلى مثيل Dog مختلف مثل d = new Dog("Boxer") aDog d = new Dog("Boxer") ).


جاوة تمر المراجع حسب القيمة.

لذلك لا يمكنك تغيير المرجع الذي يتم تمريره.


لقد لاحظت أنك أشرت إلى مقالي .

تفيد Java Spec أن كل شيء في Java يتعدى القيمة. لا يوجد شيء اسمه "تمرير حسب المرجع" في Java.

المفتاح لفهم هذا هو أن شيء من هذا القبيل

Dog myDog;

ليس كلب انها في الواقع مؤشر لكلب.

ماذا يعني ذلك ، هو عندما يكون لديك

Dog myDog = new Dog("Rover");
foo(myDog);

كنت في الأساس تمرير عنوان كائن Dog تم إنشاؤه إلى طريقة foo .

(أقول أساسًا لأن مؤشرات Java ليست عناوينًا مباشرة ، ولكن من الأسهل التفكير بها بهذه الطريقة)

لنفترض أن كائن Dog موجود في عنوان الذاكرة 42. وهذا يعني أننا نمرر 42 إلى الطريقة.

إذا تم تعريف الطريقة كـ

public void foo(Dog someDog) {
    someDog.setName("Max");     // AAA
    someDog = new Dog("Fifi");  // BBB
    someDog.setName("Rowlf");   // CCC
}

دعونا ننظر إلى ما يحدث.

  • يتم تعيين المعلمة someDog إلى القيمة 42
  • في السطر "AAA"
    • يتم اتباع someDog إلى Dog يشير إلى (كائن Dog في العنوان 42)
    • يطلب من Dog (واحد في العنوان 42) لتغيير اسمه إلى ماكس
  • في السطر "BBB"
    • يتم إنشاء Dog جديد. لنفترض أنه في العنوان 74
    • نقوم بتعيين المعلمة someDog إلى 74
  • في خط "CCC"
    • يتم اتباع someDog إلى Dog يشير إلى (كائن Dog في العنوان 74)
    • يطلب من Dog (واحد في العنوان 74) تغيير اسمه إلى Rowlf
  • ثم نعود

الآن دعونا نفكر في ما يحدث خارج الأسلوب:

هل تغير myDog ؟

هناك المفتاح.

مع الأخذ في الاعتبار أن myDog هو مؤشر ، وليس Dog الفعلي ، فإن الجواب هو NO. myDog لا يزال لديه قيمة 42 ؛ انها لا تزال تشير إلى Dog الأصلي (ولكن لاحظ أنه بسبب خط "AAA" ، اسمها هو الآن "ماكس" - لا يزال نفس الكلب ؛ لم تتغير قيمة myDog ل.)

انها صالحة تماما لمتابعة عنوان وتغيير ما هو في نهاية الأمر ؛ التي لا تغير المتغير ، ومع ذلك.

تعمل Java تمامًا مثل C. يمكنك تعيين مؤشر ، تمرير المؤشر إلى طريقة ، تتبع المؤشر في الطريقة وتغيير البيانات التي تم الإشارة إليها. ومع ذلك ، لا يمكنك تغيير حيث يشير هذا المؤشر.

في لغة C ++ و Ada و Pascal وغيرها من اللغات التي تدعم المرور حسب المرجع ، يمكنك فعليًا تغيير المتغير الذي تم تمريره.

إذا كانت لغة جافا تحتوي على دلالات مرجعية ، فإن طريقة foo قمنا بتعريفها أعلاه قد تغيرت حيث كان myDog يشير عندما قام بتعيين بعض someDog على سطر BBB.

فكر في المعلمات المرجعية على أنها أسماء مستعارة للمتغير الذي تم تمريره. وعندما يتم تعيين هذا الاسم المستعار ، فإن المتغير الذي تم تمريره هو أيضًا.


يتم تمرير Java دائمًا بقيمة ، بدون استثناءات على الإطلاق .

إذن كيف يمكن لأي شخص أن يكون مرتبكًا من هذا ، ويؤمن أن جافا تمر بالإشارة ، أو يعتقد أن لديهم مثالًا على أن جافا تعمل كمرجع؟ النقطة الأساسية هي أن جافا لا توفر الوصول المباشر إلى قيم الأشياء نفسها ، في أي ظرف من الظروف. الوصول الوحيد إلى الكائنات عبر مرجع إلى ذلك الكائن. نظرًا لأن كائنات Java يتم الوصول إليها دائمًا من خلال مرجع ، وليس بشكل مباشر ، فمن الشائع الحديث عن الحقول والمتغيرات ووسائط الأسلوب ككائنات ، عندما تكون متحركة فقط كمراجع للكائنات . الارتباك ينبع من هذا التغيير (بالمعنى الدقيق للكلمة ، غير صحيح) في التسمية.

لذلك ، عند استدعاء طريقة

  • بالنسبة للحجج البدائية ( int ، long ، إلخ) ، يكون التمرير حسب القيمة هو القيمة الفعلية للبدائية (على سبيل المثال ، 3).
  • بالنسبة للكائنات ، القيمة التمريرية حسب القيمة هي قيمة المرجع إلى الكائن .

لذا إذا كان لديك doSomething(foo) public void doSomething(Foo foo) { .. } فقد قام فوسان بنسخ المراجع التي تشير إلى نفس الأشياء.

وبطبيعة الحال ، بالمرور بالقيمة ، تبدو الإشارة إلى كائن تشبه إلى حد كبير (ولا يمكن تمييزها عمليًا عن) مرور كائن حسب المرجع.


في Java يتم تمرير المراجع فقط ويتم تمريرها حسب القيمة:

يتم تمرير جميع معاملات Java بالقيمة (يتم نسخ المرجع عند استخدامه بواسطة الطريقة):

في حالة الأنواع البدائية ، يكون سلوك Java بسيطًا: يتم نسخ القيمة في مثيل آخر من النوع البدائي.

في حالة الكائنات ، يكون هذا هو نفسه: متغيرات الكائنات هي مؤشرات (مجموعات) تحتوي فقط على عنوان الكائن الذي تم إنشاؤه باستخدام الكلمة الأساسية "الجديدة" ، ويتم نسخها مثل الأنواع البدائية.

يمكن أن يبدو السلوك مختلفًا عن الأنواع البدائية: نظرًا لأن متغير الكائن المنسوخ يحتوي على نفس العنوان (لنفس الكائن) فقد يستمر تعديل محتوى / أعضاء الكائن داخل طريقة ما ومن ثم الوصول إلى الخارج في وقت لاحق ، مما يمنحهم الوهم بأن (المحتوي) الكائن تم تمرير نفسه من خلال الإشارة.

تبدو "String" Objects كمثال مضاد مثالي للأسطورة الحضرية تقول "يتم تمرير الكائنات حسب المرجع":

في الواقع ، لن تتمكن أبدًا من تحديث إحدى السلاسل التي تم تمريرها كوسيطة ، في إطار طريقة لن تتمكن مطلقًا من القيام بها.

كائن سلسلة ، يحمل الأحرف عن طريق مصفوفة معلنة نهائية لا يمكن تعديلها. قد يتم استبدال عنوان الكائن فقط بأخرى باستخدام "جديد". إن استخدام "جديد" لتحديث المتغير ، لن يسمح بالدخول إلى الكائن من الخارج ، حيث أن المتغير قد تم تمريره في البداية بواسطة القيمة ونسخه.


يتم تمرير Java دائمًا حسب القيمة ، ولا تمر بالرجوع إليها

بادئ ذي بدء ، نحن بحاجة إلى فهم ما يمر بالقيمة وتمرير حسب المرجع.

يعني التمرير حسب القيمة أنك تقوم بعمل نسخة في الذاكرة الخاصة بقيمة المعلمة الفعلية التي تم تمريرها. هذه نسخة من محتويات المعلمة الفعلية .

يعني التمرير حسب المرجع (ويسمى أيضًا التمرير حسب العنوان) أنه يتم تخزين نسخة من عنوان المعلمة الفعلية .

في بعض الأحيان يمكن أن تعطي جافا وهم تمرير حسب المرجع. دعونا نرى كيف يعمل عن طريق استخدام المثال أدناه:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeValue(t);
        System.out.println(t.name);
    }

    public void changeValue(Test f) {
        f.name = "changevalue";
    }
}

class Test {
    String name;
}

ناتج هذا البرنامج هو:

changevalue

دعونا نفهم خطوة بخطوة:

Test t = new Test();

كما نعلم جميعًا ، سيؤدي إلى إنشاء كائن في كومة الذاكرة المؤقتة وإرجاع القيمة المرجعية إلى t. على سبيل المثال ، لنفترض أن قيمة t 0x100234(لا نعرف قيمة JVM الداخلية الفعلية ، فهذا مجرد مثال).

new PassByValue().changeValue(t);

عند تمرير الدالة t إلى الدالة ، لن يتم تمرير القيمة المرجعية الفعلية لاختبار الكائن مباشرةً ، ولكن سيتم إنشاء نسخة من t ثم تمريرها إلى الدالة. بما أنه يمر بقيمة ، فإنه يمرر نسخة من المتغير بدلاً من المرجع الفعلي له. وبما أننا قلنا أن قيمة t 0x100234، سيكون لكل من t و f نفس القيمة ومن ثم سيشيران إلى نفس الكائن.

إذا قمت بتغيير أي شيء في الوظيفة باستخدام المرجع f ، فسيتم تعديل محتويات الكائن الموجودة. هذا هو السبب في أننا حصلنا على الإخراج changevalue، والذي يتم تحديثه في الوظيفة.

لفهم هذا بشكل أكثر وضوحًا ، خذ بعين الاعتبار المثال التالي:

public class PassByValue {
    public static void main(String[] args) {
        Test t = new Test();
        t.name = "initialvalue";
        new PassByValue().changeRefence(t);
        System.out.println(t.name);
    }

    public void changeRefence(Test f) {
        f = null;
    }
}

class Test {
    String name;
}

سوف هذا رمي NullPointerException؟ لا ، لأنه لا يمر سوى نسخة من المرجع. في حالة المرور بالإشارة ، يمكن أن يكون قد ألقيت NullPointerException، كما هو موضح أدناه:

نأمل أن هذا سوف يساعد.


تقوم Java بتمرير المراجع إلى الكائنات حسب القيمة.


جافا هي دعوة من حيث القيمة.

كيف تعمل

  • أنت تمر دائمًا بنسخة من بتات قيمة المرجع!

  • إذا كان نوع بيانات أولية تحتوي هذه البتات على قيمة نوع البيانات البدائي نفسه ، لهذا السبب إذا قمنا بتغيير قيمة الرأس داخل الطريقة ، فهذا لا يعكس التغييرات الخارجية.

  • إذا كان نوع بيانات كائن مثل Foo foo = new Foo () ، في هذه الحالة ، تمر نسخة من عنوان الكائن مثل اختصار الملف ، لنفترض أن لدينا ملفًا نصيًا abc.txt في C: \ desktop ونفترض أننا نصنع اختصارًا نفس الملف ووضع هذا داخل اختصار C: \ desktop \ abc حتى عند الوصول إلى الملف من C: \ desktop \ abc.txt والكتابة '' وإغلاق الملف ومرة ​​أخرى قمت بفتح الملف من الاختصار ثم كتابة 'هو أكبر مجتمع على الإنترنت للمبرمجين للتعلم' ثم سيكون التغيير الكلي للملفات ' هو أكبر مجتمع على الإنترنت للمبرمجين ليتعلموا'مما يعني أنه لا يهم من حيث تفتح الملف ، في كل مرة كنا ندخل فيها إلى الملف نفسه ، هنا يمكننا افتراض Foo كملف وافترض أن foo مخزنة على 123hd7h (العنوان الأصلي مثل C: \ desktop \ abc.txt ) عنوان و 234 jdid (عنوان نسخ مثل C: \ desktop \ abc- الاختصار الذي يحتوي في الواقع على العنوان الأصلي للملف داخل) .. لذلك من أجل فهم أفضل جعل ملف الاختصار ويشعر ...


في الأساس ، لا يؤثر إعادة تعيين معلمات الكائن على الحجة ، على سبيل المثال ،

private void foo(Object bar) {
    bar = null;
}

public static void main(String[] args) {
    String baz = "Hah!";
    foo(baz);
    System.out.println(baz);
}

ستطبع "Hah!"بدلا من null. السبب في أن هذا العمل هو barنسخة من قيمة baz، وهي مجرد إشارة إلى "Hah!". إذا كانت الإشارة الفعلية نفسها، ثم fooقد أعاد bazل null.


لا ، لا يمر بالرجوع إليه.

يتم تمرير Java حسب القيمة طبقًا لمواصفات Java Language:

عندما يتم استدعاء الأسلوب أو المُنشئ (الفقرة 12.12) ، تشرع قيم تعبيرات الوسيطة الفعلية متغيرات المعلمات الناشئة حديثًا ، كل من النوع المعلن ، قبل تنفيذ نص الأسلوب أو المُنشئ. يمكن استخدام المعرّف الذي يظهر في DeclaratorId كاسم بسيط في نص الأسلوب أو المُنشئ للإشارة إلى المعلمة الرسمية .


إن التمييز ، أو ربما الطريقة التي أتذكرها كما اعتدت أن أكون على نفس الانطباع الذي يحمله الملصق الأصلي ، هو: جاوة دائماً ما تُمر بالقيمة. جميع الكائنات (في Java ، أي شيء باستثناء الأوليات) في Java هي مراجع. يتم تمرير هذه المراجع حسب القيمة.


بقدر ما أعرف ، تعرف Java فقط المكالمة حسب القيمة. هذا يعني بالنسبة إلى أنواع البيانات البدائية التي ستعمل بها مع نسخة لكائنات ستعمل مع نسخة من الإشارة إلى الكائنات. ومع ذلك أعتقد أن هناك بعض العثرات. على سبيل المثال ، لن يعمل هذا:

public static void swap(StringBuffer s1, StringBuffer s2) {
    StringBuffer temp = s1;
    s1 = s2;
    s2 = temp;
}


public static void main(String[] args) {
    StringBuffer s1 = new StringBuffer("Hello");
    StringBuffer s2 = new StringBuffer("World");
    swap(s1, s2);
    System.out.println(s1);
    System.out.println(s2);
}

سيؤدي ذلك إلى ملء Hello World وليس World Hello لأنه في دالة التبديل تستخدم copys التي ليس لها تأثير على المراجع الرئيسية. ولكن إذا كانت الأشياء غير قابلة للتغيير ، فيمكنك تغييرها على سبيل المثال:

public static void appendWorld(StringBuffer s1) {
    s1.append(" World");
}

public static void main(String[] args) {
    StringBuffer s = new StringBuffer("Hello");
    appendWorld(s);
    System.out.println(s);
}

سيؤدي ذلك إلى ملء Hello World في سطر الأوامر. إذا قمت بتغيير StringBuffer إلى String فسوف ينتج فقط Hello لأن String غير قابل للتغيير. فمثلا:

public static void appendWorld(String s){
    s = s+" World";
}

public static void main(String[] args) {
    String s = new String("Hello");
    appendWorld(s);
    System.out.println(s);
}

ومع ذلك ، يمكنك إنشاء غلاف لمثل هذه السلسلة مما يجعله قادرًا على استخدامه مع السلاسل:

class StringWrapper {
    public String value;

    public StringWrapper(String value) {
        this.value = value;
    }
}

public static void appendWorld(StringWrapper s){
    s.value = s.value +" World";
}

public static void main(String[] args) {
    StringWrapper s = new StringWrapper("Hello");
    appendWorld(s);
    System.out.println(s.value);
}

تحرير: وأعتقد أن هذا هو السبب أيضا لاستخدام StringBuffer عندما يتعلق الأمر "إضافة" اثنين من سلاسل لأنه يمكنك تعديل الكائن الأصلي الذي لا يمكنك مع كائنات غير قابلة للتغيير مثل سلسلة.


جوهر الموضوع هو أن كلمة مرجعية في التعبير "تمرير حسب المرجع" تعني شيئًا مختلفًا تمامًا عن المعنى المعتاد لمرجع الكلمة في Java.

عادة ما يكون في مرجع Java يعني المرجع إلى كائن . لكن المصطلحات التقنية تمر بالإشارة / القيمة من نظرية لغة البرمجة تتحدث عن إشارة إلى خلية الذاكرة التي تحمل المتغير ، وهو شيء مختلف تمامًا.


دعوني أحاول أن أشرح فهمي بمساعدة أربعة أمثلة. جافا هي القيمة بالمرور ، وليس تمرير حسب المرجع

/ **

تمر حسب القيمة

في Java ، يتم تمرير جميع المعلمات حسب القيمة ، أي أن تعيين وسيطة أسلوب غير مرئي للمتصل.

* /

مثال 1:

public class PassByValueString {
    public static void main(String[] args) {
        new PassByValueString().caller();
    }

    public void caller() {
        String value = "Nikhil";
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

نتيجة

output : output
value : Nikhil
valueflag : false

المثال 2:

/ ** * * تمرير حسب القيمة * * /

public class PassByValueNewString {
    public static void main(String[] args) {
        new PassByValueNewString().caller();
    }

    public void caller() {
        String value = new String("Nikhil");
        boolean valueflag = false;
        String output = method(value, valueflag);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'value' and 'valueflag'
         */
        System.out.println("output : " + output);
        System.out.println("value : " + value);
        System.out.println("valueflag : " + valueflag);

    }

    public String method(String value, boolean valueflag) {
        value = "Anand";
        valueflag = true;
        return "output";
    }
}

نتيجة

output : output
value : Nikhil
valueflag : false

المثال 3:

/ ** هذا "Pass By Value" لديه شعور بـ "Pass By Reference"

بعض الناس يقولون أن الأنواع البدائية و "السلسلة" هي "تمر بقيمة" والأشياء "تمر بالإشارة".

ولكن من هذا المثال ، يمكننا أن نفهم أنه لا يتم تمرير القيمة إلا من خلال القيمة ، مع الأخذ في الاعتبار أننا هنا نجتاز المرجع كقيمة. أي: يتم تمرير المرجع حسب القيمة. هذا هو السبب في أن تكون قادرة على تغيير ولا يزال صحيحا بعد النطاق المحلي. لكن لا يمكننا تغيير المرجع الفعلي خارج النطاق الأصلي. يتم توضيح هذا يعني من المثال التالي من PassByValueObjectCase2.

* /

public class PassByValueObjectCase1 {

    private class Student {
        int id;
        String name;
        public Student() {
        }
        public Student(int id, String name) {
            super();
            this.id = id;
            this.name = name;
        }
        public int getId() {
            return id;
        }
        public void setId(int id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        @Override
        public String toString() {
            return "Student [id=" + id + ", name=" + name + "]";
        }
    }

    public static void main(String[] args) {
        new PassByValueObjectCase1().caller();
    }

    public void caller() {
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student);
    }

    public String method(Student student) {
        student.setName("Anand");
        return "output";
    }
}

نتيجة

output : output
student : Student [id=10, name=Anand]

المثال 4:

/ **

بالإضافة إلى ما ورد في Example3 (PassByValueObjectCase1.java) ، لا يمكننا تغيير المرجع الفعلي خارج النطاق الأصلي. "

ملاحظة: أنا لا ألصق الرمز لـ private class Student. تعريف الفئة لـ Studentهو نفسه Example3.

* /

public class PassByValueObjectCase2 {

    public static void main(String[] args) {
        new PassByValueObjectCase2().caller();
    }

    public void caller() {
        // student has the actual reference to a Student object created
        // can we change this actual reference outside the local scope? Let's see
        Student student = new Student(10, "Nikhil");
        String output = method(student);
        /*
         * 'output' is insignificant in this example. we are more interested in
         * 'student'
         */
        System.out.println("output : " + output);
        System.out.println("student : " + student); // Will it print Nikhil or Anand?
    }

    public String method(Student student) {
        student = new Student(20, "Anand");
        return "output";
    }

}

نتيجة

output : output
student : Student [id=10, name=Nikhil]

لا أستطيع أن أصدق أن أحدا لم يذكر باربرا ليسكوف بعد. عندما صمم CLU في عام 1974، وقالت انها واجهت هذه المشكلة المصطلحات نفسها، وأنها اخترع مصطلح دعوة تقاسم (المعروف أيضا باسم الدعوة عن طريق تبادل كائن و دعوة وجوه ) لهذه الحالة المحددة من "الدعوة من حيث القيمة حيث القيمة مرجع".


لا يمكنك المرور حسب المرجع في Java ، وأحد الطرق الواضحة هو عندما تريد إرجاع أكثر من قيمة واحدة من استدعاء الأسلوب. خذ بعين الاعتبار البت التالي من التعليمات البرمجية في C ++:

void getValues(int& arg1, int& arg2) {
    arg1 = 1;
    arg2 = 2;
}
void caller() {
    int x;
    int y;
    getValues(x, y);
    cout << "Result: " << x << " " << y << endl;
}

في بعض الأحيان تريد استخدام نفس النمط في Java ، ولكن لا يمكنك ذلك. على الأقل ليس مباشرة. بدلاً من ذلك يمكنك فعل شيء كالتالي:

void getValues(int[] arg1, int[] arg2) {
    arg1[0] = 1;
    arg2[0] = 2;
}
void caller() {
    int[] x = new int[1];
    int[] y = new int[1];
    getValues(x, y);
    System.out.println("Result: " + x[0] + " " + y[0]);
}

كما تم توضيحه في الإجابات السابقة ، في Java يتم تمرير مؤشر إلى الصفيف كقيمة في getValues. وهذا يكفي ، لأن الطريقة تقوم بعد ذلك بتعديل عنصر الصفيف ، وبالاتفاق فإنك تتوقع أن يحتوي العنصر 0 على قيمة الإرجاع. من الواضح أنه يمكنك القيام بذلك بطرق أخرى ، مثل تنظيم التعليمات البرمجية الخاصة بك بحيث لا يكون ذلك ضروريًا ، أو إنشاء فئة يمكن أن تحتوي على قيمة الإرجاع أو السماح بتعيينها. لكن النمط البسيط المتاح لك في C ++ أعلاه غير متوفر في Java.


لقد اجتازت جافا القيمة فقط. مثال بسيط جدا للتحقق من صحة هذا.

public void test() {
    MyClass obj = null;
    init(obj);
    //After calling init method, obj still points to null
    //this is because obj is passed as value and not as reference.
}
private void init(MyClass objVar) {
    objVar = new MyClass();
}

لقد قمت بإنشاء موضوع مخصص لهذا النوع من الأسئلة لأي لغات برمجة here .

تم ذكر جافا أيضا . هنا هو ملخص قصير:

  • جاوة تمر بها المعلمات من حيث القيمة
  • "بالقيمة" هي الطريقة الوحيدة في جافا لتمرير المعلمة إلى طريقة
  • باستخدام أساليب من الكائن المحدد كمعلمة سوف يغير الكائن كما تشير المراجع إلى الكائنات الأصلية. (إذا كان هذا الأسلوب نفسه يغير بعض القيم)




pass-by-value