JavaRush /مدونة جافا /Random-AR /جافا كور. أسئلة المقابلة الجزء الثاني
Andrey
مستوى

جافا كور. أسئلة المقابلة الجزء الثاني

نشرت في المجموعة
بالنسبة لأولئك الذين يسمعون كلمة Java Core لأول مرة، فهذه هي الأسس الأساسية للغة. مع هذه المعرفة، يمكنك الذهاب بأمان إلى التدريب / التدريب.
جافا كور.  أسئلة للمقابلة، الجزء 2 - 1
ستساعدك هذه الأسئلة على تحديث معلوماتك قبل المقابلة، أو تعلم شيء جديد لنفسك. لاكتساب المهارات العملية، ادرس في JavaRush . المقالة الأصلية روابط لأجزاء أخرى: Java Core. أسئلة المقابلة، الجزء 1 جافا الأساسية. أسئلة للمقابلة، الجزء 3

لماذا يجب تجنب طريقة الإنهاء ()؟

نعلم جميعًا العبارة التي مفادها أنه finalize()يتم استدعاء الطريقة بواسطة أداة تجميع البيانات المهملة قبل تحرير الذاكرة التي يشغلها كائن ما. فيما يلي مثال لبرنامج يثبت أن استدعاء الطريقة finalize()غير مضمون:
public class TryCatchFinallyTest implements Runnable {

	private void testMethod() throws InterruptedException
	{
		try
		{
			System.out.println("In try block");
			throw new NullPointerException();
		}
		catch(NullPointerException npe)
		{
			System.out.println("In catch block");
		}
		finally
		{
			System.out.println("In finally block");
		}
	}

	@Override
	protected void finalize() throws Throwable {
		System.out.println("In finalize block");
		super.finalize();
	}

	@Override
	public void run() {
		try {
			testMethod();
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
}
public class TestMain
{
	@SuppressWarnings("deprecation")
	public static void main(String[] args) {
	for(int i=1;i< =3;i++)
	{
		new Thread(new TryCatchFinallyTest()).start();
	}
	}
}
الإخراج: في محاولة كتلة في كتلة الصيد في كتلة أخيرا في محاولة كتلة في كتلة الصيد في كتلة أخيرا في محاولة كتلة في كتلة الصيد في كتلة أخيرا والمثير للدهشة أنه لم يتم تنفيذ الطريقة finalizeلأي موضوع. وهذا يثبت كلامي أعتقد أن السبب هو أن أدوات الإنهاء يتم تنفيذها بواسطة خيط منفصل لأداة تجميع البيانات المهملة. إذا تم إنهاء Java Virtual Machine مبكرًا جدًا، فلن يكون لدى أداة تجميع البيانات المهملة الوقت الكافي لإنشاء أدوات الإنهاء وتنفيذها. finalize()قد تكون الأسباب الأخرى لعدم استخدام الطريقة هي :
  1. الطريقة finalize()لا تعمل مع سلاسل مثل المنشئين. هذا يعني أنه عند استدعاء منشئ فئة، سيتم استدعاء منشئي الطبقة الفائقة دون قيد أو شرط. لكن في حالة الطريقة finalize()، لن يتبع ذلك. يجب أن يتم استدعاء أسلوب الطبقة الفائقة finalize()بشكل صريح.
  2. يتم تجاهل أي استثناء يتم طرحه بواسطة الطريقة finalizeبواسطة مؤشر ترابط أداة تجميع البيانات المهملة ولن يتم نشره بشكل أكبر، مما يعني أنه لن يتم تسجيل الحدث في سجلاتك. هذا سيء للغاية، أليس كذلك؟
  3. ستحصل أيضًا على عقوبة أداء كبيرة إذا كانت الطريقة finalize()موجودة في فصلك. في البرمجة الفعالة (الطبعة الثانية)، قال جوشوا بلوخ:
    "نعم، وشيء آخر: هناك عقوبة كبيرة على الأداء عند استخدام أدوات الإنهاء. على جهازي، يبلغ الوقت اللازم لإنشاء وتدمير الأشياء البسيطة حوالي 5.6 نانو ثانية.
    تؤدي إضافة أداة الإنهاء إلى زيادة الوقت إلى 2400 نانو ثانية. بمعنى آخر، يكون إنشاء كائن وحذفه باستخدام أداة الإنهاء أبطأ بحوالي 430 مرة.

لماذا لا ينبغي استخدام HashMap في بيئة متعددة الخيوط؟ هل يمكن أن يسبب هذا حلقة لا نهائية؟

نحن نعلم أن HashMapهذه مجموعة غير متزامنة، ونظيرها المتزامن هو HashTable. لذلك، عند الوصول إلى مجموعة وفي بيئة متعددة الخيوط حيث تتمتع كافة سلاسل الرسائل بإمكانية الوصول إلى مثيل واحد من المجموعة، يكون الاستخدام أكثر أمانًا HashTableلأسباب واضحة، مثل تجنب القراءات القذرة وضمان اتساق البيانات. في أسوأ الحالات، ستتسبب هذه البيئة متعددة الخيوط في حدوث حلقة لا نهائية. نعم هذا صحيح. HashMap.get()قد يسبب حلقة لا نهائية. دعونا نرى كيف؟ إذا نظرت إلى الكود المصدري للطريقة HashMap.get(Object key)، فستبدو كما يلي:
public Object get(Object key) {
    Object k = maskNull(key);
    int hash = hash(k);
    int i = indexFor(hash, table.length);
    Entry e = table[i];
    while (true) {
        if (e == null)
            return e;
        if (e.hash == hash && eq(k, e.key))
            return e.value;
        e = e.next;
    }
}
while(true)يمكن أن يقع دائمًا ضحية لحلقة لا نهائية في بيئة تشغيل متعددة الخيوط إذا كان e.nextبإمكانه الإشارة إلى نفسه لسبب ما. سيؤدي هذا إلى حلقة لا نهاية لها، ولكن كيف e.nextستشير إلى نفسها (أي إلى e)؟ يمكن أن يحدث هذا بطريقة void transfer(Entry[] newTable)يتم استدعاؤها أثناء HashMapتغيير حجمها.
do {
    Entry next = e.next;
    int i = indexFor(e.hash, newCapacity);
    e.next = newTable[i];
    newTable[i] = e;
    e = next;
} while (e != null);
هذا الجزء من التعليمات البرمجية عرضة لإنشاء حلقة لا نهائية إذا حدث تغيير الحجم في نفس الوقت الذي يحاول فيه مؤشر ترابط آخر تغيير مثيل الخريطة ( HashMap). الطريقة الوحيدة لتجنب هذا السيناريو هي استخدام المزامنة في التعليمات البرمجية الخاصة بك، أو الأفضل من ذلك، استخدام مجموعة متزامنة.

شرح التجريد والتغليف. كيف يتم توصيلهم؟

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

الاختلافات بين الواجهة والطبقة المجردة؟

يمكن سرد الاختلافات الرئيسية على النحو التالي:
  • لا يمكن للواجهة تنفيذ أي أساليب، لكن يمكن للفئات المجردة القيام بذلك.
  • يمكن للفئة تنفيذ العديد من الواجهات، ولكن يمكن أن تحتوي على فئة فائقة واحدة فقط (مجردة أو غير مجردة)
  • الواجهة ليست جزءًا من التسلسل الهرمي للفئة. يمكن للفئات غير المرتبطة تنفيذ نفس الواجهة.
ما عليك أن تتذكره هو ما يلي: "عندما يمكنك وصف مفهوم ما بشكل كامل من حيث "ماذا يفعل" دون الحاجة إلى تحديد "كيف يفعل ذلك"، فيجب عليك استخدام واجهة. إذا كنت بحاجة إلى تضمين بعض تفاصيل التنفيذ، فأنت بحاجة إلى تمثيل مفهومك في فصل دراسي مجرد." وبعبارة أخرى: هل هناك العديد من الفئات التي يمكن "تجميعها معًا" ووصفها باسم واحد؟ إذا كان الأمر كذلك، قم بإنشاء فئة مجردة باسم هذا الاسم، وورث الطبقات منه. على سبيل المثال، Catيمكن Dogأن ترث من فئة مجردة Animal، وهذه الفئة الأساسية المجردة ستنفذ طريقة void Breathe()- التنفس، والتي ستؤديها جميع الحيوانات بنفس الطريقة. ما هي الأفعال التي يمكن تطبيقها على صفي ويمكن تطبيقها على الآخرين؟ إنشاء واجهة لكل من هذه الأفعال. على سبيل المثال، يمكن لجميع الحيوانات أن تأكل، لذلك سأقوم بإنشاء واجهة IFeedableوأجعلها Animalتنفذ تلك الواجهة. فقط جيد بما يكفي لتنفيذ واجهة Dog( قادرة على الإعجاب بي)، ولكن ليس كلها. قال أحدهم: الفرق الرئيسي هو المكان الذي تريد تنفيذك فيه. عندما تقوم بإنشاء واجهة، يمكنك نقل التنفيذ إلى أي فئة تقوم بتنفيذ الواجهة الخاصة بك. من خلال إنشاء فئة مجردة، يمكنك مشاركة تنفيذ جميع الفئات المشتقة في مكان واحد وتجنب الكثير من الأشياء السيئة مثل تكرار التعليمات البرمجية. HorseILikeable

كيف يقوم StringBuffer بحفظ الذاكرة؟

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

لماذا يتم الإعلان عن طرق الانتظار والإخطار في فئة الكائن بدلاً من الموضوع؟

تكون الأساليب wait، notify، notifyAllمطلوبة فقط عندما تريد أن يكون لسلاسلك حق الوصول إلى الموارد المشتركة ويمكن أن يكون المورد المشترك أي كائن Java في الكومة. وبالتالي، يتم تعريف هذه الأساليب على الفئة الأساسية Objectبحيث يكون لكل كائن عنصر تحكم يسمح للخيوط بالانتظار على شاشتها. لا تحتوي Java على أي كائن خاص يُستخدم لمشاركة مورد مشترك. لم يتم تعريف بنية البيانات هذه. لذلك، تقع على عاتق الفصل مسؤولية Objectأن يكون قادرًا على أن يصبح موردًا مشتركًا، ويوفر طرقًا مساعدة مثل wait()، notify()و، و notifyAll(). تعتمد Java على فكرة الشاشات التي وضعها تشارلز هور. في Java، تحتوي جميع الكائنات على شاشة. تنتظر الخيوط على الشاشات، لذا لإجراء الانتظار نحتاج إلى معلمتين:
  • خيط
  • مراقبة (أي كائن).
في تصميم Java، لا يمكن تعريف الخيط بدقة؛ فهو دائمًا الخيط الحالي الذي ينفذ التعليمات البرمجية. ومع ذلك، يمكننا تعريف الشاشة (وهي كائن يمكننا أن نطلق عليه طريقة wait). يعد هذا تصميمًا جيدًا لأنه إذا تمكنا من إجبار أي مؤشر ترابط آخر على الانتظار على شاشة معينة، فسيؤدي ذلك إلى "غزو"، مما يجعل تصميم/برمجة البرامج المتوازية أمرًا صعبًا. تذكر أنه في Java، يتم إهمال جميع العمليات التي تتداخل مع سلاسل الرسائل الأخرى (على سبيل المثال، stop()).

اكتب برنامجًا لإنشاء حالة توقف تام في Java وإصلاحه

في Java deadlock، هذا هو الموقف الذي يحتفظ فيه خيطان على الأقل بكتلة على موارد مختلفة، وكلاهما ينتظران أن يصبح المورد الآخر متاحًا لإكمال مهمتهما. ولا يستطيع أي منهم ترك قفل على المورد المحتجز. جافا كور.  أسئلة للمقابلة، الجزء 2 - 2 برنامج المثال:
package thread;

public class ResolveDeadLockTest {

	public static void main(String[] args) {
		ResolveDeadLockTest test = new ResolveDeadLockTest();

		final A a = test.new A();
		final B b = test.new B();

		// Thread-1
		Runnable block1 = new Runnable() {
			public void run() {
				synchronized (a) {
					try {
					// Добавляем задержку, чтобы обе нити могли начать попытки
					// блокирования ресурсов
						Thread.sleep(100);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
					// Thread-1 заняла A но также нуждается в B
					synchronized (b) {
						System.out.println("In block 1");
					}
				}
			}
		};

		// Thread-2
		Runnable block2 = new Runnable() {
			public void run() {
				synchronized (b) {
					// Thread-2 заняла B но также нуждается в A
					synchronized (a) {
						System.out.println("In block 2");
					}
				}
			}
		};

		new Thread(block1).start();
		new Thread(block2).start();
	}

	// Resource A
	private class A {
		private int i = 10;

		public int getI() {
			return i;
		}

		public void setI(int i) {
			this.i = i;
		}
	}

	// Resource B
	private class B {
		private int i = 20;

		public int getI() {
			return i;
		}

		public void setI(int i) {
			this.i = i;
		}
	}
}
سيؤدي تشغيل الكود أعلاه إلى طريق مسدود لأسباب واضحة جدًا (موضحة أعلاه). الآن نحن بحاجة إلى حل هذه المشكلة. أعتقد أن حل أي مشكلة يكمن في جذر المشكلة نفسها. في حالتنا، نموذج الوصول إلى A وB هو المشكلة الرئيسية. لذلك، لحلها، نقوم ببساطة بتغيير ترتيب عوامل الوصول إلى الموارد المشتركة. بعد التغيير سيصبح بالشكل التالي:
// Thread-1
Runnable block1 = new Runnable() {
	public void run() {
		synchronized (b) {
			try {
				// Добавляем задержку, чтобы обе нити могли начать попытки
				// блокирования ресурсов
				Thread.sleep(100);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			// Thread-1 заняла B но также нуждается в А
			synchronized (a) {
				System.out.println("In block 1");
			}
		}
	}
};

// Thread-2
Runnable block2 = new Runnable() {
	public void run() {
		synchronized (b) {
			// Thread-2 заняла B но также нуждается в А
			synchronized (a) {
				System.out.println("In block 2");
			}
		}
	}
};
قم بتشغيل هذا الفصل مرة أخرى والآن لن ترى حالة الجمود. آمل أن يساعدك هذا على تجنب الجمود والتخلص منه إذا واجهته.

ماذا يحدث إذا كان فصلك الذي يقوم بتنفيذ الواجهة القابلة للتسلسل يحتوي على مكون غير قابل للتسلسل؟ كيف يمكن اصلاح هذا؟

في هذه الحالة، سيتم طرحه NotSerializableExceptionأثناء التنفيذ. لإصلاح هذه المشكلة، يوجد حل بسيط جدًا - حدد هذه المربعات transient. وهذا يعني أن الحقول المحددة لن يتم إجراء تسلسل لها. إذا كنت تريد أيضًا تخزين حالة هذه الحقول، فأنت بحاجة إلى مراعاة المتغيرات المرجعية، التي تنفذ بالفعل Serializable. قد تحتاج أيضًا إلى استخدام الأساليب readResolve()والطرق writeResolve(). دعونا نلخص:
  • أولاً، اجعل حقلك غير قابل للتسلسل transient.
  • أولاً writeObject، قم باستدعاء defaultWriteObjectمؤشر الترابط لحفظ كافة transientالحقول غير القابلة للتسلسل، ثم قم باستدعاء الطرق المتبقية لإجراء تسلسل للخصائص الفردية للكائن غير القابل للتسلسل.
  • في readObject، قم أولاً باستدعاء defaultReadObjectالدفق لقراءة جميع transientالحقول غير الموجودة، ثم اتصل بالطرق الأخرى (المقابلة لتلك التي أضفتها في writeObject) لإلغاء تسلسل غير transientالكائن الخاص بك.

شرح الكلمات الرئيسية العابرة والمتقلبة في Java

"يتم استخدام الكلمة الأساسية transientللإشارة إلى الحقول التي لن يتم إجراء تسلسل لها." وفقًا لمواصفات لغة Java: يمكن تمييز المتغيرات بمؤشر عابر للإشارة إلى أنها ليست جزءًا من الحالة المستمرة للكائن. على سبيل المثال، قد تحتوي على حقول مشتقة من حقول أخرى، ويفضل الحصول عليها برمجياً بدلاً من استعادة حالتها من خلال التسلسل. على سبيل المثال، في الفصل الدراسي، يمكن إجراء تسلسل BankPayment.javaلحقول مثل principal(المدير) و (السعر)، ويمكن حساب (الفائدة المتراكمة) في أي وقت، حتى بعد إلغاء التسلسل. إذا تذكرنا، فإن كل مؤشر ترابط في Java لديه ذاكرته المحلية الخاصة ويقوم بعمليات القراءة/الكتابة على هذه الذاكرة المحلية. عند الانتهاء من جميع العمليات، فإنه يكتب الحالة المعدلة للمتغير في الذاكرة المشتركة، حيث تصل جميع مؤشرات الترابط إلى المتغير. عادةً ما يكون هذا مؤشر ترابط عادي داخل جهاز ظاهري. لكن المعدل المتطاير يخبر الجهاز الظاهري أن وصول الخيط إلى هذا المتغير يجب أن يتطابق دائمًا مع نسخته الخاصة من هذا المتغير مع النسخة الرئيسية للمتغير في الذاكرة. هذا يعني أنه في كل مرة يريد فيها الخيط قراءة حالة المتغير، يجب عليه مسح حالة الذاكرة الداخلية وتحديث المتغير من الذاكرة الرئيسية. الأكثر فائدة في الخوارزميات الخالية من القفل. يمكنك وضع علامة على متغير يقوم بتخزين البيانات المشتركة على أنه متطاير، ثم لا تستخدم الأقفال للوصول إلى هذا المتغير، وستكون جميع التغييرات التي أجراها مؤشر ترابط واحد مرئية للآخرين. أو إذا كنت تريد إنشاء علاقة "حدث بعد" لضمان عدم تكرار العمليات الحسابية، مرة أخرى لضمان ظهور التغييرات في الوقت الفعلي. يجب استخدام المتطايرة لنشر الكائنات غير القابلة للتغيير بأمان في بيئة متعددة الخيوط. يضمن إعلان الحقل أن جميع سلاسل الرسائل ترى دائمًا المرجع المتاح حاليًا للمثيل. rateinterestVolatilepublic volatile ImmutableObject

الفرق بين التكرار و ListIterator؟

يمكننا استخدام أو للتكرار Iteratorعلى العناصر . ولكن لا يمكن استخدامه إلا للتكرار على العناصر . يتم وصف الاختلافات الأخرى أدناه. أنت تستطيع: SetListMapListIteratorList
  1. التكرار بترتيب عكسي.
  2. الحصول على الفهرس في أي مكان.
  3. أضف أي قيمة في أي مكان.
  4. تعيين أي قيمة في الموضع الحالي.
حظا موفقا في دراستك!! مؤلف المقال Lokesh Gupta المقال الأصلي Java Core. أسئلة المقابلة، الجزء 1 جافا الأساسية. أسئلة للمقابلة، الجزء 3
تعليقات
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION