PDA

View Full Version : مبتدی: ابهام در مورد کار با آرایه ها و فیلدهای یک کلاس



ehsan_faal
سه شنبه 22 اردیبهشت 1394, 01:42 صبح
سلام دوستان من دارم از روی کتاب دایتل ویرایش 9 جاوا رو دنبال میکنم .توی فصل 7 گفته اعلان آرایه ها از نوع ارجاعی هستش یعنی با کلمه کلیدی new باید اونا رو بسازیم.
حالا با توجه به این حرف من بعضی قسمتهای این کد رو نمیفهمم:
کلاس Card:
public class Card { private String face,suite;
public Card(String face,String suite){
this.face=face;
this.suite=suite;
}

@Override
public String toString(){
return face+" of "+suite;
}
}




کلاس DeckOfCards:

import java.util.Random;

public class DeckOfCard {
private Card[] deck;
private int currentCard;
private static final int NUMBER_OF_CARDS=52;
private static final Random rnd=new Random();
public DeckOfCard(){
String[] faces={"Ace","Deuce","Three","Four","Five","Six","Seven","Eight","Nine","Ten","Jack","Queen","King"};
String[] suits={"Herats","Diamonds","Clubs","Spades"};
deck =new Card[NUMBER_OF_CARDS];
for(int counter=0;counter<deck.length;counter++){
deck[counter]=new Card(faces[counter%13], suits[counter/13]);
}
}
public void Shuffle(){
currentCard=0;
for(int first=0;first<deck.length;first++){
int second=rnd.nextInt(NUMBER_OF_CARDS);
Card tmp=deck[first];
deck[first]=deck[second];
deck[second]=tmp;
}
}
public Card dealCard(){
if (currentCard<deck.length){
return deck[currentCard++];
}else
return null;
}
}




کلاس اصلی:


public class DeckOfCardTest {


public static void main(String[] args) {
DeckOfCard doc=new DeckOfCard();
doc.Shuffle();
for(int counter=1;counter<52;counter++){
System.out.print(String.format("%-20s", doc.dealCard()));
if (counter%4==0){
System.out.println();
}
}
}


}




مشکلم اینجاست که توی کلاس دوم و توی متد shuffle اول یه خونه از آرایه رو ریخته توی tmp و بعد محتویات اون خونه عوض شده،من قبلا توی کار با لیستها این مشکل رو داشتم که یه لیست رو میریختم توی یه لیست دیگه بعد هر تغییری توی لیست اول روی دومی هم اعمال میشد:
http://barnamenevis.org/showthread.php?487656-%D9%85%D8%B4%DA%A9%D9%84-%D8%AF%D8%B1-%DA%A9%D8%A7%D8%B1-%D8%A8%D8%A7-%D9%84%DB%8C%D8%B3%D8%AA%D9%87%D8%A7
سوالم اینه که اینجا هم باید همون منطق باشه ولی نیست،چرا؟

سوال دیگه هم این که توی همین کلاس چرا دوتا فیلد استاتیک رو private کرده،مگه یکی از اهداف فیلد استاتیک دسترسی به اون فیلد بدون ساخت شی نیست،الان با اینکار دیگه به اون فیلد دسترسی نداریم.

ممنون میشم یه توضیح مفصل در این دو مورد بهم بدین.

با تشکر

ahmad.mo74
سه شنبه 22 اردیبهشت 1394, 15:55 عصر
سلام، این لینک ها رو حتما ببین :

http://www.programmerinterview.com/index.php/java-questions/does-java-pass-by-reference-or-by-value/
http://stackoverflow.com/questions/12757841/are-arrays-passed-by-value-or-passed-by-reference-in-java
http://stackoverflow.com/questions/40480/is-java-pass-by-reference-or-pass-by-value

خودمم یه توضیحی میدم.

اول یه مثال میزنم.


public class StringHolder {


private String value;


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


public String getValue() {
return value;
}


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


@Override
public String toString() {
return getValue();
}


}


این کلاس همونطور که از اسمش هم پیداس قراره یه String ای رو برامون نگه داری کنه.

حالا به این دوتا تستی که انجام میدم دقت کن.

تست اول :


public static void main(String[] args) {
StringHolder[] holders = new StringHolder[]{new StringHolder("string 1")};
StringHolder tmp = holders[0];
holders[0].setValue("string 2");
System.out.println(tmp);
System.out.println(holders[0]);
}


خروجی :


string 2
string 2


تست دوم :


public static void main(String[] args) {
StringHolder[] holders = new StringHolder[]{new StringHolder("string 1")};
StringHolder tmp = holders[0];
holders[0] = new StringHolder("string 2");
System.out.println(tmp);
System.out.println(holders[0]);
}


خروجی :


string 1
string 2


:))

اتفاقی که میفته اینه که تو تست اول هم tmp و هم [0]holders رفرنس جفتشون به یک آبجکته. یعنی صدا زدن setValue از هر کدوم، value اون یکی رو هم تغییر میده. مثلا :


public static void main(String[] args) {
StringHolder[] holders = new StringHolder[]{new StringHolder("string 1")};
StringHolder tmp = holders[0];
tmp.setValue("string 2");
System.out.println(tmp);
System.out.println(holders[0]);
}


که باز جفتشون string 2 رو چاپ میکنن.

اما توی تست دوم، رفرنس توی tmp باقی میمونه و وقتی [0]holders رو new میکنی، tmp خودش میشه آبجکت اصلی و دیگه هیچ ربطی به [0]holders نداره. یعنی اگر قبلش [0]holders رو توی tmp نمیریختیم، بعد از اینکه توی خط بعدش new کردیم، همه رفرنس ها به آبجکت قبلی از بین میرفت و garbage collector میتونست حذفش کنه.

تو اینجا هم همینه :


Card tmp=deck[first];
deck[first]=deck[second];
deck[second]=tmp;


یعنی وقتی [deck[first رو میریزی تو tmp، رفرنسش توی tmp باقی میمونه و وقتی توی خط بعدی مقدار دیگه ای رو به [deck[first اساین میکنی، خود tmp میشه آبجکت اصلی و هیچ رفرنس دیگه هم بهش وجود نداره. در مورد [deck[second هم همین قضیه تکرار میشه.

در مورد سوال دومت، هدف از تعریف کردن فیلد استاتیک علاوه بر دسترسی با استفاده از نام کلاس، دلیل مهم تری هم داره، یعنی؛ اون فیلد یا متد وابسته به آبجکت نباشه و متعلق به خود کلاس باشه و بین همه آبجکت ها یکسان باشه. private یا public بودنش دست خود شماست.

ahmad.mo74
سه شنبه 22 اردیبهشت 1394, 21:32 عصر
در ضمن این رو هم بگم که ما توی جاوا pointer داریم! (اتفاقا جلوی چشممونم هست)

این یه چیز اشتباهه که بین همه جاوا کارا جا افتاده و همه فکر میکنن جاوا فقط با رفرنس ها کار میکنه و آبجکت ها pass-by-reference هستن و ... (بعضیا میگن کلا همه چیز رفرنسه!!!)
خودمم تو پست قبلی از کلمه reference استفاده کردم :))

در مورد primitive ها که همه چیز واضحه و میدونیم pass-by-value هستن، اما بحث سر object هاس. البته مسببش خود Sun بوده که خیلی جاها از کلمه reference به جای pointer استفاده کرده.

در واقع توی جاوا ما به هیچ وجه به خود آبجکت اصلی دسترسی نداریم و همش با pointer ها کار میکنیم.

برای مثال :


String s = null;


اصلا تا حالا با خودتون فکر کردید که مگه میشه آبجکت (یا اصلا همون رفرنس) null باشه؟ اصلا چنین چیزی نمیشه و بی معنیه. فقط یک pointer میتونه null باشه.

یا مثلا NullPointerException! چرا به جاش از اسم NullReferenceException استفاده نکردن؟

یا مثلا همین instance گرفتن :


Object o = new Object();


دقیقا معادل این توی ++C ه :


Object *o = new Object();


و چیزی که توی o هست یه آدرسی از فضای heap ه و به آبجکت اصلی اشاره میکنه. (به هیچ عنوان خود آبجکت نیست)

بازم مثال :


void myMethod(Object o) {
o.toString();
}


Object o = new Object();
myMethod(o);


معادلش توی ++C :


void myMethod(Object *o) {
o->toString();
}


Object *o = new Object();
myMethod(o);


پس دقیقا تمام رفتارهای pointer ها توی ++C ، اینجا هم صدق میکنه.

یعنی وقتی o رو به تابعی پاس میدی، در واقع پوینتر رو (آدرس o) رو بهش دادی و این یعنی pass-by-value.
یا وقتی دوتا آبجکت رو مساوی هم قرار میدی، دقیقا اتفاقی که توی pointer های ++C میفتاد اینجام میفته و جفتشون به یک آدرس اشاره میکنن.

در ضمن توی پست قبلی منظورم از این اینکه "tmp خودش میشه آبجکت اصلی" این بود که به جز tmp دیگه هیچ پوینتری به آبجکت اصلی وجود نداره.

فکر کنم با این توضیحاتی که دادم هیچ ابهامی باقی نمونه.

ehsan_faal
سه شنبه 22 اردیبهشت 1394, 22:22 عصر
ممنون.توضیحاتتون کامل بود:تشویق:
ولی من هنوزم نتونستم بفهمم چرا متد shuffle کارش رو درست انجام میده.
در مرحله اول tmp , first هر دو به یه جا اشاره میکنن،هر تغییر روی یکی روی دیگری هم اعمال میشه در مرحله دوم محلی که first بهش اشاره میکنه عوض میشه در حالی که tmp همون محل قبلی رو داره.
اون لینکی که گذاشته بودم رو اگه ببینید ،میبینید که دقیقا توی کار با آبجکت هایی از نوع لیست هم من همین کارو میکردم و نتیجه اشتباه میشد.
این منطق واسم قابل درک نیست :
وقتی یه داده از نوع غیر اصلی(منظورم ابجکته) رو میریزیم توی یه داده ی دیگه از نوع خودش فقط داریم آدرس اولی توی حافظه رو به دومی هم میدیم.خب اگه این اصل رو قبول داشته باشیم آیا نباید وقتی برای بار دوم که یه متغیر دیگه ای میاد و آدرسش رو به اولین متغیر میده ،سه تا آبجکت داشته باشیم با یه آدرس ثابت؟

ahmad.mo74
سه شنبه 22 اردیبهشت 1394, 22:58 عصر
ممنون.

من این 3 خط رو یه بار دیگه توضیح بدم :


Card tmp = deck[first];


یه پوینتری از نوع Card ساخته میشه و به جایی که [deck[first اشاره میکنه، اشاره میکنه.


deck[first] = deck[second];


حالا [deck[first به جایی که [deck[second اشاره میکنه، اشاره میکنه.

این خط هیچ تاثیری رو خود آبجکت اصلی و tmp نداره (اصلا برای چی باید تاثیر داشته باشه؟؟؟)

خط بعد :


deck[second] = tmp;


در نهایت [deck[second هم به جایی که tmp اشاره میکرده اشاره میکنه.

ما الان اصلا به آبجکت اصلی دست نزدیم و فقط با پوینترها بازی کردیم.

اینو نباید با چنین چیزی اشتباه بگیری :


stringHolder.setValue("aaa");


دقیقا همون مفاهیم پوینتر توی ++C ه.

-سیّد-
چهارشنبه 23 اردیبهشت 1394, 12:07 عصر
در ضمن این رو هم بگم که ما توی جاوا pointer داریم! (اتفاقا جلوی چشممونم هست)

این یه چیز اشتباهه که بین همه جاوا کارا جا افتاده و همه فکر میکنن جاوا فقط با رفرنس ها کار میکنه و آبجکت ها pass-by-reference هستن و ... (بعضیا میگن کلا همه چیز رفرنسه!!!)
خودمم تو پست قبلی از کلمه reference استفاده کردم :))

در مورد primitive ها که همه چیز واضحه و میدونیم pass-by-value هستن، اما بحث سر object هاس. البته مسببش خود Sun بوده که خیلی جاها از کلمه reference به جای pointer استفاده کرده.

در واقع توی جاوا ما به هیچ وجه به خود آبجکت اصلی دسترسی نداریم و همش با pointer ها کار میکنیم.

برای مثال :


String s = null;


اصلا تا حالا با خودتون فکر کردید که مگه میشه آبجکت (یا اصلا همون رفرنس) null باشه؟ اصلا چنین چیزی نمیشه و بی معنیه. فقط یک pointer میتونه null باشه.

یا مثلا NullPointerException! چرا به جاش از اسم NullReferenceException استفاده نکردن؟

یا مثلا همین instance گرفتن :


Object o = new Object();


دقیقا معادل این توی ++C ه :


Object *o = new Object();


و چیزی که توی o هست یه آدرسی از فضای heap ه و به آبجکت اصلی اشاره میکنه. (به هیچ عنوان خود آبجکت نیست)

بازم مثال :


void myMethod(Object o) {
o.toString();
}


Object o = new Object();
myMethod(o);


معادلش توی ++C :


void myMethod(Object *o) {
o->toString();
}


Object *o = new Object();
myMethod(o);


پس دقیقا تمام رفتارهای pointer ها توی ++C ، اینجا هم صدق میکنه.

یعنی وقتی o رو به تابعی پاس میدی، در واقع پوینتر رو (آدرس o) رو بهش دادی و این یعنی pass-by-value.
یا وقتی دوتا آبجکت رو مساوی هم قرار میدی، دقیقا اتفاقی که توی pointer های ++C میفتاد اینجام میفته و جفتشون به یک آدرس اشاره میکنن.

در ضمن توی پست قبلی منظورم از این اینکه "tmp خودش میشه آبجکت اصلی" این بود که به جز tmp دیگه هیچ پوینتری به آبجکت اصلی وجود نداره.

فکر کنم با این توضیحاتی که دادم هیچ ابهامی باقی نمونه.
حرفاتون به طور کلی درسته، ولی یه کم نادقیقه.
توی جاوا همه چیز Reference هست، نه Pointer. معادل Reference توی ++C هم وجود داره. اون مثالی که زدید:

Object o = new Object();


معادلش توی ++C اینه:


Object &o = new Object();


که اون علامت & به معنای Reference هست.

یا اون یکی مثال:

void myMethod(Object o) {
o.toString();
}


Object o = new Object();
myMethod(o);


معادلش توی ++C اینجوری می‌شه (البته دقیقاً معادل نیست و یه فرقای ریزی داره):


void myMethod(Object &o) {
o.toString();
}


Object &o = new Object();
myMethod(o);


در واقع دقیقش این می‌شه:
توی جاوا همه‌ی اشیاء با Reference نگهداری می‌شن، و توی توابع فقط call by value داریم. یعنی reference شما به صورت call by value به تابع فرستاده می‌شه.
این معادل دقیقی توی ++C نداره، ولی به Reference های ++C شباهت داره.

تفاوت‌های Pointer و Reference در ++C (از Stackoverflow):
http://stackoverflow.com/questions/57483/what-are-the-differences-between-a-pointer-variable-and-a-reference-variable-in

****
همونطور که دوستمون پایینتر (http://barnamenevis.org/showthread.php?494915-%D8%A7%D8%A8%D9%87%D8%A7%D9%85-%D8%AF%D8%B1-%D9%85%D9%88%D8%B1%D8%AF-%DA%A9%D8%A7%D8%B1-%D8%A8%D8%A7-%D8%A2%D8%B1%D8%A7%DB%8C%D9%87-%D9%87%D8%A7-%D9%88-%D9%81%DB%8C%D9%84%D8%AF%D9%87%D8%A7%DB%8C-%DB%8C%DA%A9-%DA%A9%D9%84%D8%A7%D8%B3&p=2212423&viewfull=1#post2212423) اشاره کردن، من اینجا اشتباه کردم. در نتیجه بند پایین رو جدی نگیرید! :)
****
این جمله هم خیلی درست نیست:


پس دقیقا تمام رفتارهای pointer ها توی ++C ، اینجا هم صدق میکنه.

یکی از رفتارهای متفاوتشون اینه: توی ++C اگه یه pointer رو به یه تابع پاس کنید و توی اون تابع مقدارش رو عوض کنید، بیرون هم مقدار اون pointer عوض می‌شه (چون آدرس همون خونه‌ی stack که pointer توش قرار داره به اون تابع داده می‌شه و همون خونه‌ی stack رونویسی می‌شه):

void createObject(MyObject* o) {
o = new MyObject(10);
}

MyObject* myObj = new MyObject(5);
cout << myObj->getValue(); // 5
createObject(myObj);
cout << myObj->getValue(); // 10


ولی توی جاوا چون اشاره‌گر by-value به تابع پاس می‌شه، اگه داخل تابع مقدار خود reference رو عوض کنید، بیرونش عوض نمی‌شه:

void createObject(MyObject o) {
o = new MyObject(10);
}

MyObject myObj = new MyObject(5);
System.out.println(myObj.getValue()); // 5
createObject(myObj);
System.out.println(myObj.getValue()); // 5


حالا اگه این رو بخوایم با Reference های ++C شبیه‌سازی کنیم:

void createObject(MyObject& o) {
o = new MyObject(10); // اینجا خطای کامپایل می‌ده
}

MyObject& myObj = new MyObject(5);
cout << myObj.getValue(); // 5
createObject(myObj);
cout << myObj.getValue();

که این یکی از تفاوت‌های Pointer و Reference توی ++C هست: Reference رو نمی‌شه re-assign کرد.

****
در واقع درست بند بالا این هست:
از نظر مفهومی تفاوتی بین reference های جاوا و pointer های ++C یا reference های ++C وجود نداره. تفاوت فقط توی syntax ها و نحوه‌ی استفاده ازشون هست.
****

ahmad.mo74
چهارشنبه 23 اردیبهشت 1394, 12:23 عصر
سلام. من از این که میگم پوینتر کاملا (100%) مطمئنم و هیچ شکی توش ندارم. اون نشانه هایی ام که آوردم اینو ثابت میکنه :)

به طور کلی قضیه اینه :

توی جاوا همه چیز (یعنی همه چیز) pass-by-value هست.
ما به هیچ وجه به خود آبجکت ها دسترسی نداریم، بلکه پوینتری که به اون آبجکت اشاره میکنه در اختیار ماست.
وقتی آبجکتی رو (همون پوینتره) به تابعی پاس میدیم، یک کپی از اون پوینتر (یعنی یک پوینتر جدید که تو حافظه فضای مخصوص خودشو داره) به تابع فرستاده میشه (pass-by-value). پس اگر داخل اون تابع مقدار جدیدی به پوینتر اساین بشه، تاثیری روی پوینتر قبلی نداره.

اون مثال ++C هم که زدید اشتباهه، مثلا :


class Integer {
int value;
public:
Integer(int _value = 0) : value(_value) {}
int get() const { return value; }
void set(const int &_value) { value = _value; }
};


void myMethod(Integer *i) {
i = new Integer(10);
}


int main() {
Integer *i = new Integer(5);
cout << i->get() << endl;
myMethod(i);
cout << i->get() << endl;
return 0;
}


که جفتش 5 چاپ میشه، یعنی دقیقا همون اتفاقی که توی جاوا میفته.

ولی اگر داخل myMethod این باشه :


void myMethod(Integer *i) {
//i = new Integer(10);
i->set(10);
}


اولی 5 و دومی 10 چاپ میشه.

-سیّد-
چهارشنبه 23 اردیبهشت 1394, 12:59 عصر
سلام. من از این که میگم پوینتر کاملا (100%) مطمئنم و هیچ شکی توش ندارم. اون نشانه هایی ام که آوردم اینو ثابت میکنه :)

نکته اینه که pointer و reference از نظر مفهومی فرق چندانی با هم ندارن. شما بهش بگو pointer، جاوا بهش می‌گه reference. توی ++C بینشون فرق‌های ریزی هست، ولی تفاوت مفهومی با هم ندارن. در واقع reference توی ++C انگار همون pointer هست که انگار می‌خواسته کار رو راحت‌تر کنه (چون توی syntax دیگه لازم نیست از <- استفاده کنید و مثل یه شیء معمولی نگاهش می‌کنید). البته برای جلوگیری از اینجور ابهامات معمولاً ترجیح می‌دن به جای reference از همون pointer استفاده کنن!



به طور کلی قضیه اینه :

توی جاوا همه چیز (یعنی همه چیز) pass-by-value هست.
ما به هیچ وجه به خود آبجکت ها دسترسی نداریم، بلکه پوینتری که به اون آبجکت اشاره میکنه در اختیار ماست.

خوب شما هم که داری حرف منو می‌زنی!
یه مقدار دقیقش کنیم:
منظور شما از «خود آبجکت ها» چیه؟ بالاخره آخرش ما یه حافظه داریم، توش هم داده هست. حالا این حافظه stack باشه یا heap، در هر صورت آخرش یه آرایه از بایت هست (به صورت سخت‌افزاری). شما هم یه شیء دارید که مثلاً ۱۰۰ بایت فضا می‌گیره. حالا فرق ++C و جاوا چیه؟ توی جاوا شما فقط یه حالت دارید و اون اینه که این شیء رو توی heap بسازید و با reference اش (یا pointer اش) کار کنید:

MyObject o = new MyObject(5);

و البته خود این متغیر (که یه اشاره‌گر هست) توی stack هست.

اما توی ++C شما ۲ تا گزینه دارید: یا مثل جاوا شیء رو توی heap می‌سازید و توی stack بهش یه اشاره‌گر می‌گیرید:

MyObject* o = new MyObject(5);

(اینجا دقیقاً همون اتفاقی که توی جاوا افتاد می‌افته، تفاوت فقط توی syntax ها و کارهایی هست که بعدش می‌تونید با اون متغیر بکنید).

یا می‌تونید شیء رو مستقیماً توی stack بگیرید و کاری با heap نداشته باشید (که این امکان رو توی جاوا نداریم):

MyObject o(5);


حالا اون حالت reference توی ++C که گفتم چی می‌شه؟

MyObject& o = new MyObject(5);

این هم فرقی با حالت اول نداره و دقیقاً همون اتفاق می‌افته، و فقط تفاوت توی syntax و نوع نگاه به متغیر هست. وگرنه اینجا هم یه شیء توی heap ساخته می‌شه و یه متغیر از نوع اشاره‌گر توی stack ساخته می‌شه و به اون شیء اشاره می‌کنه. فقط شما اشاره‌گرش رو نمی‌بینید.



اون مثال ++C هم که زدید اشتباهه، مثلا :


class Integer {
int value;
public:
Integer(int _value = 0) : value(_value) {}
int get() const { return value; }
void set(const int &_value) { value = _value; }
};


void myMethod(Integer *i) {
i = new Integer(10);
}


int main() {
Integer *i = new Integer(5);
cout << i->get() << endl;
myMethod(i);
cout << i->get() << endl;
return 0;
}


که جفتش 5 چاپ میشه، یعنی دقیقا همون اتفاقی که توی جاوا میفته.

ولی اگر داخل myMethod این باشه :


void myMethod(Integer *i) {
//i = new Integer(10);
i->set(10);
}


اولی 5 و دومی 10 چاپ میشه.
بله حق با شماس، من یه سوتی دادم. اگه بخوایم اون متغیر خودش عوض بشه، باید اون pointer رو به صورت reference بدیم به تابع:

void myMethod(Integer *&i) {
...

اگه این علامت reference رو توی این تابع اضافه کنید، خروجی ۱۰ می‌شه.

در نهایت می‌تونم اینجوری جمع‌بندی کنم:
همه چیز توی جاوا pointer (یا reference، چون فرق خاصی از نظر مفهومی نمی‌کنن) هست.

ehsan_faal
چهارشنبه 23 اردیبهشت 1394, 13:31 عصر
توی کتاب دایتل گفته که توی جاوا فقط انتقال از طریق مقدار رو داریم.
یعنی وقتی یه متدی رو احضار میکنیم بسته به آرگومان ورودیش که میتونه نوع داده اصلی باشه یا یه شی یه کپی از اون به متد فرستاده میشه،منتها چیزی که به نظر من گیج کنندس اینه که اگه ما یه نوع داده اصلی رو به یه متد بفرستیم ،اون متد کارش رو روی کپی اون داده انجام میده و داده اصلی دست نخورده باقی میمونه ولی اگه یه آبجکت رو به یه متد بفرستیم یه کپی از محل ارجاعش رو به متد دادیم که حالا هر بلایی سر این آبجکت داخل متد احضار شوده بیاد داخل اون آبجکت اصلی هم میاد،تناقض اینجا پیش میاد که جاوا فقط مقدار رو به متدهاش پاس میده ولی وقتی یه آبجکت رو به یه متد پاس بدیم یه کپی از آدرسش رو دادیم به اون متد.
خب اگه منظورشون این بوده که داخل متدها بتونیم آبجکتها رو دستکاری کنیم پس دیگه چرا گفتن فقط فراخوانی با مقدار یا اصلا چرا باید دو برابر حافظه اشغال بشه در صورتی که هر دو قرار باشه به یه آبجکت اشاره کنن؟:متفکر:

محمد فدوی
چهارشنبه 23 اردیبهشت 1394, 14:24 عصر
توی کتاب دایتل گفته که توی جاوا فقط انتقال از طریق مقدار رو داریم.
یعنی وقتی یه متدی رو احضار میکنیم بسته به آرگومان ورودیش که میتونه نوع داده اصلی باشه یا یه شی یه کپی از اون به متد فرستاده میشه،منتها چیزی که به نظر من گیج کنندس اینه که اگه ما یه نوع داده اصلی رو به یه متد بفرستیم ،اون متد کارش رو روی کپی اون داده انجام میده و داده اصلی دست نخورده باقی میمونه ولی اگه یه آبجکت رو به یه متد بفرستیم یه کپی از محل ارجاعش رو به متد دادیم که حالا هر بلایی سر این آبجکت داخل متد احضار شوده بیاد داخل اون آبجکت اصلی هم میاد،تناقض اینجا پیش میاد که جاوا فقط مقدار رو به متدهاش پاس میده ولی وقتی یه آبجکت رو به یه متد پاس بدیم یه کپی از آدرسش رو دادیم به اون متد.
خب اگه منظورشون این بوده که داخل متدها بتونیم آبجکتها رو دستکاری کنیم پس دیگه چرا گفتن فقط فراخوانی با مقدار یا اصلا چرا باید دو برابر حافظه اشغال بشه در صورتی که هر دو قرار باشه به یه آبجکت اشاره کنن؟:متفکر:

خیر. هیچ تناقضی وجود نداره. توی جاوا همه‌چیز با مقدار پاس می‌شه، در مورد انواع داده‌ای پایه مقدار خودشون پاس می‌شه و در مورد اشیاء مقدار اشاره‌گرشون پاس می‌شه که هیچ تناقضی هم وجود نداره.

class Dog {}

// ...

public static void method(Dog pet) {
pet = new Dog();
}

// ...

Dog woofi = new Dog();
method(woofi);

موقعی که ()new Dog فراخوانی می‌شه یه شیء ساخته می‌شه و رفرنسش توی woofi قرار می‌گیره (نه خودش). حالا موقعی که method فراخوانی می‌شه مقدار این رفرنس به pet منتقل می‌شه.
واسه همینه که وقتی pet رو به یه شیء جدید منتسب می‌کنیم هیچ تغییری توی woofi رخ نمی‌ده، چون این دوتا فقط دوتا رفرنس بودن که مربوط به یه شیء بودن و بعد از تغییر pet، دیگه مربوط به یه شیء نیستن.
پس جاوا همیشه بصورت by-val پاس‌ها رو انجام می‌ده و هیچ استثناءای هم نداره.

در مورد بحثی هم که پیش اومد در مورد اینکه جاوا با اشاره‌گر کار می‌کنه یا رفرنس باید بگم که هیچ تفاوتی وجود نداره! مفهوم هردوشون یکیه، یا بهتره بگیم رفرنس نتیجه‌ی وجود اشاره‌گره!
اما اگه بخوام بگم کدوم کلمه در بحث چگونگی پاس دادن اشياء در جاوا دقیق‌تره باید بگم رفرنس. چون همونطور که گفتم اشاره‌گر یه مفهوم کلی‌تره و تنها کاربردش اینجا نیست و رفرنس‌گیری یکی از چندین کاربردشه. اما کلمه‌ی رفرنس دقیقا چیزیه که منظور ما رو در این مورد می‌رسونه.
هرچند اگه بخوایم بیش از این حساسیت نشون بدیم باید توجه کنیم که ماشین مجازی جاوا با C پیاده‌سازی شده و توی C چیزی به اسم رفرنس یا Refrence data type وجود نداره و همه‌چیز اشاره‌گره...
ولی همونطور که گفتم در نهایت تفاوتی وجود نداره بین این دوتا.

ehsan_faal
چهارشنبه 23 اردیبهشت 1394, 14:38 عصر
با عرض معذرت چون بحث یه مقدار گیج کننده شده فهمش مشکل شده برای من.
الان بعد از فراخوانیه متد ما نهایتا دو تا شی داریم دیگه؟
یه دونه pet و یه دونه هم woofi؟
خلرج از این متد به woofi که دسترسی داریم اما به pet دسترسی نداریم دیگه؟منظورم اینه که متغیر محلی حساب میشه واسه متد و بعد از خروج از متد مقدارش از بین میره بدون اینکه هیچ تاثیری روی woofi بذاره؟

این سوال رو چطور توجیه میکنید :
بالفرض یه متدی هست که مقدار بازگشتی نداره و یه آرایه رو میگیره و تمام المانهای اون آرایه رو یه تغییری میده،اگه هنگام پاس دادن این آرایه به متد مقدار رفرنسشه که میاد تو متد پس چرا بعد از اتمام کار متد اعضای آرایه واقعا تغییر میکنن؟


این تصویر همون حرفیه که شما میزنی،یعنی woofi که تا الان محل نگهداری اشاره گر به شی اول بوده از این به بعد محل نگهداریه شی دومی میشه که توی متد بوجود میاد، اما خب باز هم سوال قبلیم بی جواب میمونه!

131191

-سیّد-
چهارشنبه 23 اردیبهشت 1394, 16:45 عصر
با عرض معذرت چون بحث یه مقدار گیج کننده شده فهمش مشکل شده برای من.
الان بعد از فراخوانیه متد ما نهایتا دو تا شی داریم دیگه؟
یه دونه pet و یه دونه هم woofi؟
خلرج از این متد به woofi که دسترسی داریم اما به pet دسترسی نداریم دیگه؟منظورم اینه که متغیر محلی حساب میشه واسه متد و بعد از خروج از متد مقدارش از بین میره بدون اینکه هیچ تاثیری روی woofi بذاره؟

این سوال رو چطور توجیه میکنید :
بالفرض یه متدی هست که مقدار بازگشتی نداره و یه آرایه رو میگیره و تمام المانهای اون آرایه رو یه تغییری میده،اگه هنگام پاس دادن این آرایه به متد مقدار رفرنسشه که میاد تو متد پس چرا بعد از اتمام کار متد اعضای آرایه واقعا تغییر میکنن؟


این تصویر همون حرفیه که شما میزنی،یعنی woofi که تا الان محل نگهداری اشاره گر به شی اول بوده از این به بعد محل نگهداریه شی دومی میشه که توی متد بوجود میاد، اما خب باز هم سوال قبلیم بی جواب میمونه!

131191

من سعی می‌کنم یه دور کل ماجرا رو بگم، امیدوارم ابهام برطرف بشه:
ما یه حافظه‌ی heap داریم که هر شیئی new می‌کنید اونجا براش فضا در نظر گرفته می‌شه.
یه حافظه‌ی stack هم داریم که هر thread برای خودش یه بخشی از حافظه رو به عنوان stack می‌گیره. تمام متغیرهایی که توی یه تابع تعریف می‌شن، بعلاوه‌ی پارامترهای تابع، توی stack قرار می‌گیرن.

فرض کنید یه کلاس دارید به نام MyClass که ۱۰۰ بایت فضا اشغال می‌کنه. حالا کد زیر رو در نظر بگیرید:

MyClass o = new MyClass();

این یه خط کد، در واقع ۲ تا دستور هست. دستور اول، new هست که بر اثرش یه شیء new می‌شه و مثلاً در خونه‌ی ۱۰۰ در heap قرار می‌گیره (یعنی از خونه‌ی ۱۰۰ تا ۲۰۰ مرتبط به این متغیر هست. دقت کنید که این که «تا خونه‌ی ۲۰۰» مال این شیء هست، معمولاً هیچ جایی ذخیره نمیشه. معمولاً فقط آدرس شروع ذخیره می‌شه و بقیه‌اش از روی تعریف کلاس به دست میاد). دستور دوم، تعریف متغیر o هست و مقداردهی اون. در اثر اجرای این دستور، یه متغیر به نام o در خونه‌ی مثلاً ۵۰۰ در stack ساخته می‌شه و مقدارش برابر با ۱۰۰ قرار داده می‌شه. نوع این متغیر اشاره‌گر هست، در نتیجه براش یا ۴ بایت و یا ۸ بایت حافظه در نظر گرفته می‌شه (این موضوع بستگی به این داره که jvm تون اشاره‌گرها رو به صورت ۳۲ بیتی استفاده کنه یا ۶۴ بیتی (http://stackoverflow.com/questions/5839434/pointer-size-how-big-is-an-object-reference و http://stackoverflow.com/questions/981073/how-big-is-an-object-reference-in-java-and-precisely-what-information-does-it-co?lq=1)).

حالا ادامه‌ی کد:

void f(MyClass x) {
x = new MyClass();
}

f(o);

وقتی تابع f رو فراخوانی می‌کنیم و پارامتر o رو بهش می‌دیم، مقدار متغیر o، یعنی ۱۰۰، به این تابع داده می‌شه.
موقع اجرای تابع f، اول کار توی stack، یه فضایی allocate می‌شه (مثلاً به آدرس ۶۰۰) که نوعش اشاره‌گر هست و در نتیجه ۴ یا ۸ بایت هست و اسمش می‌شه x. حالا در اثر pass-by-value، مقدار ۱۰۰ توی این متغیر ذخیره می‌شه. یعنی مقدار متغیر o به این تابع داده شده.
پس موقع اجرای این تابع، هم متغیر o و هم متغیر x (که در دو بخش مختلف حافظه‌ی stack قرار دارن) به خونه‌ی ۱۰۰ حافظه اشاره می‌کنن.
حالا شما میاین توی تابع مقدار این متغیر رو عوض می‌کنین. یعنی یه شیء جدید می‌سازید توی heap مثلاً آدرسش می‌شه ۲۰۰ (تا ۳۰۰) و این مقدار رو توی متغیر x می‌ریزید، یعنی توی خونه‌ی ۶۰۰ تا ۶۰۴ (اگه اشاره‌گرها ۴ بایتی باشن) نوشته می‌شه ۲۰۰. خوب همینجا می‌بینید که این هیچ ربطی به متغیر o که بیرون تابع تعریف شده نداره و توی متغیر o (یعنی خونه‌ی ۵۰۰ تا ۵۰۴ حافظه) همون مقدار قبلی یعنی ۱۰۰ نوشته شده.
دقت کنید که بلافاصله بعد از اجرای این تابع، شیء ساخته شده توی تابع (آدرس ۲۰۰) تبدیل به garbage می‌شه، چون تنها reference ای که بهش وجود داره (متغیر x) از بین می‌ره.

خوب پس در موردی که گفتید، ۲ تا شیء به نامهای pet و woofi نداریم. اصلاً شیء ها اسم ندارن! متغیرها اسم دارن. اونجا شما ۲ تا متغیر دارید به نامهای pet و woofi. اول متغیر woofi ساخته شده و به یه شیء توی heap مقداردهی شده. بعد این مقدار رو دادیم به اون تابع، اونجا یه متغیر جدید داریم به نام pet که اولش به همون شیء اشاره می‌کنه. بعد که توی تابع new می‌کنیم، مقدارش عوض می‌شه و به یه شیء جدید اشاره می‌کنه.

در مورد آرایه هم هیچ تفاوتی وجود نداره. شما آرایه رو یه کلاس فرض کنید (که واقعاً هست، فقط یه فرقای خیلی کوچیکی با کلاسای دیگه داره) و بقیه‌ی ماجرا عیناً مثل کلاسهای دیگه هست. اینجا قبلاً بحثش شده:
http://barnamenevis.org/showthread.php?489315-%D9%85%D8%AA%D8%BA%DB%8C%D8%B1-length-%D8%AF%D8%B1-%D8%A2%D8%B1%D8%A7%DB%8C%D9%87-%D9%87%D8%A7

ehsan_faal
چهارشنبه 23 اردیبهشت 1394, 18:32 عصر
واقعا متشکر از همه ی اساتید. واقعا توضیحات شیوایی رو ارائه دادید.:تشویق: