大數據技術之_31_Java 面試題_02_== 和 equals 有什麼區別 + String 相關 + 多態 + 傳值 + static 加載機制 + 線程


程序員級別:碼龍 > 碼神 > 碼農 > 碼畜
學生級別:學神 > 學霸 > 學渣 > 學弱

IT/DT 是腦力密集型的高智商行業。

反覆的強化,反覆的強化,反覆的強化。
刻意的練習,刻意的練習,刻意的練習。

1、== 和 equals 有什麼區別?

== 既可以比較基本類型也可以比較引用類型。對於基本類型就是比較值,對於引用類型就是比較內存地址(值)(本質上來說也是值)。

equals 的話,它是屬於 java.lang.Object 類裏面的方法,在源代碼的 149 行,如果該方法沒有被重寫過默認也是 ==,我們可以看到 String 類的 equals 方法是被重寫過的,而且 String 類在日常開發中用的比較多,久而久之,形成了 equals 是比較值的錯誤觀點(是因爲覆寫了 equals 方法才比較值)。

具體要看這有沒有重寫 Object 的 hashCode 方法和 equals 方法來判斷。

-----------------------------------------------------------------

以 Person 爲例,何時需要重寫 equals()?

當一個類有自己特有的“邏輯相等”概念,當改寫 equals() 的時候,總是要改寫 hashCode(),根據一個類的 equals 方法(重寫後),兩個截然不同的實例有可能在邏輯上是相等的,但是,根據 Object.hashCode 方法,它們僅僅是兩個對象。

因此,違反了 “相等的對象必須具有相等的散列碼”。

結論:重寫 equals 方法的時候一般都需要同時重寫 hashCode 方法。

示例代碼如下:

package com.atguigu.test;

import java.util.HashSet;
import java.util.Set;

import com.atguigu.entities.Person;

public class TestEquals {
	public static void main(String[] args) {
		// String s1 = new String("abc");
		// String s2 = new String("abc");
		// System.out.println(s1 == s2); // false
		// System.out.println(s1.equals(s2));
		//
		// Set<String> set01 = new HashSet<String>();
		// set01.add(s1);
		// set01.add(s2);
		// System.out.println(s1.hashCode() + "\t" + s2.hashCode());
		// System.out.println(set01.size());

		System.out.println("================================");

		// 兩個截然不同的實例,有可能在邏輯上是相等的
		// (假設項目需要,自定義邏輯相等的概念,比如只要 name 屬性值一致我們就是認爲是同一個對象)
		// 即只要 name 屬性值一致,用 equals 比較得到 true,就是同一個對象,這是開發人員自己定製的業務規則,但是JVM 不認,因爲 JVM 只認識 hashCode
		Person p1 = new Person("abc");
		Person p2 = new Person("abc");
		System.out.println(p1 == p2); // false
		System.out.println(p1.equals(p2));
		Set<Person> set02 = new HashSet<Person>();
		set02.add(p1);
		set02.add(p2);
		System.out.println(p1.hashCode() + "\t" + p2.hashCode());
		System.out.println(set02.size());
	}
}

2、爲什麼需要同時覆寫 hashCode 和 equals 方法?

答:因爲假如一個類被用在集合類中,在該集合類中判斷是否爲同一個對象,判斷的是 hashCode 的值(即地址值)。即僅僅覆寫 equals 方法是不夠的!

3、爲什麼用 eclipse 重寫 hashCode 方法,有 31 這個數字?

  計算機的乘法涉及到移位計算。當一個數乘以 2 時,就直接拿該數左移一位即可!選擇 31 原因是因爲 31 是一個素數!所謂素數:質數又稱素數(在一個大於 1 的自然數中,除了 1 和此整數自身外,沒法被其他自然數整除的數)。

  在存儲數據計算 hash 地址的時候,我們希望儘量減少有同樣的 hash 地址,所謂 “衝突”。

  因爲任何數 n * 31 就可以被 JVM 優化爲 (n << 5) -n,移位和減法的操作效率要比乘法的操作效率高的多,對左移虛擬機裏面都有做相關優化,並且 31 只佔用 5 bits!

4、String 相關

示例代碼如下:

package com.atguigu.test;

public class TestString {
	public static void main(String[] args) {
		String s1 = new String("abc"); // 生成了兩個對象:一個在字符串常量池中,一個在堆中被 s1 指向
		String s2 = "abc"; // s2 指向字符串常量池的 "abc"
		String s3 = new String("abc"); // 生成了一個對象:s3 指向堆中另一個 "abc"

		System.out.println(s1 == s2); // false
		System.out.println(s1 == s3); // false
		System.out.println(s2 == s3); // false
		
		System.out.println("====================");
		
		// String s2 = "abc"; // s2 指向字符串常量池的 "abc"
		// String s1 = new String("abc"); // 生成了一個對象:s2 指向堆中一個 "abc"
		// String s3 = new String("abc"); // 生成了一個對象:s3 指向堆中另一個 "abc"
		//
		// System.out.println(s1 == s2); // false
		// System.out.println(s1 == s3); // false
		// System.out.println(s2 == s3); // false
		//
		// System.out.println("====================");

		/*
		 * 返回字符串對象的規範化表示形式。 一個初始爲空的字符串池,它由類 String 私有地維護。 
		 * 當調用 intern 方法時,如果池已經包含一個等於此 String 對象的字符串(用  equals(Object) 方法確定),
		 * 則返回池中的字符串。否則,將此 String 對象添加到池中,並返回此 String 對象的引用。 
		 * 它遵循以下規則:對於任意兩個字符串  s 和  t,當且僅當 s.equals(t) 爲 true 時,s.intern() == t.intern() 才爲 true。
		 * 所有字面值字符串和字符串賦值常量表達式都使用 intern 方法進行操作。
		 * 字符串字面值在 Java Language Specification 的 §3.10.5 定義。 
		 * 返回:一個字符串,內容與此字符串相同,但一定取自具有唯一字符串的池。
		 */
		System.out.println(s1 == s1.intern()); // false
		System.out.println(s2 == s2.intern()); // true
		System.out.println(s1.intern() == s2.intern()); // true    簡言之:intern() 表示找池中常量的地址
		
		System.out.println("====================");

		String s4 = "java";
		String s5 = "ja";
		String s6 = "va";
		System.out.println(s4 == "java"); 		// true 	常量找池(池對象)
		System.out.println(s4 == (s5 + s6)); 	// false	拼接找堆(會在堆中生成新的對象,堆對象)
		System.out.println(s4 == "ja" + s6); 	// false
	}
}

5、多態

什麼是多態?
Java 裏通過方法重載和方法重寫來體現多態是否正確?答:錯誤,方法重載跟多態沒有任何關係。
多態是編譯時行爲還是運行時行爲?答:運行時行爲。因爲只有在實際調用運行時才能確定具體的對象。

示例代碼如下:

package com.atguigu.test;

import java.util.Random;

interface Animal {
	public void eat();
}

class Dog implements Animal {
	@Override
	public void eat() {
		System.out.println("dog eat bone---111");
	}
}

class Cat implements Animal {
	@Override
	public void eat() {
		System.out.println("cat eat fish---222");
	}
}

class Sheep implements Animal {
	@Override
	public void eat() {
		System.out.println("sheep eat grass---333");
	}
}

public class TestPolymorphism {

	public static Animal getInstance(int key) {
		Animal result = null;

		switch (key) {
		case 0:
			result = new Dog();
			break;
		case 1:
			result = new Cat();
			break;
		default:
			result = new Sheep();
			break;
		}
		return result;
	}

	public static void main(String[] args) {
		Animal animal = TestPolymorphism.getInstance(new Random().nextInt(3));
		animal.eat();
	}
}

6、傳值

示例代碼如下:

package com.atguigu.test;

import com.atguigu.entities.Person;

public class TestTransferValue {
	
	public void changeValue1(int age) {
		age = 30;
	}

	public void changeValue2(Person person) {
		person.setPersonName("xxx");
	}

	public void changeValue3(String str) {
		str = "xxx";
	}

	public static void main(String[] args) {
		
		TestTransferValue test = new TestTransferValue();
		int age = 20;
		test.changeValue1(age);
		System.out.println("age---" + age); // age---20

		Person person = new Person("abc");
		test.changeValue2(person);
		System.out.println("personName---" + person.getPersonName()); // personName---xxx

		String str = "abc";
		test.changeValue3(str);
		System.out.println("String---" + str); // String---abc
	}
}

7、static 加載機制

示例代碼如下:

package com.atguigu.test;

class Father {
	public Father() {
		System.out.println("111111");
	}

	{ // 非靜態代碼塊每次實例化的時候都加載
		System.out.println("222222");
	}
	
	static { // 靜態的東西只加載一次
		System.out.println("333333");
	}
}

class Son extends Father {
	public Son() {
		System.out.println("444444");
	}

	{ // 非靜態代碼塊每次實例化的時候都加載
		System.out.println("555555");
	}
	
	static { // 靜態的東西只加載一次
		System.out.println("666666");
	}
}

public class TestStaticSeq {
	public static void main(String[] args) {
	    // 特別注意:最開始加載靜態的內容,有無以下創建對象都會加載(因爲靜態內容屬於類的東西)
		new Son();
		System.out.println("-------------------");
		new Son();
		System.out.println("-------------------");
		new Father();
	}
}

輸出結果如下如下:

333333
666666
222222
111111
555555
444444
-------------------
222222
111111
555555
444444
-------------------
222222
111111

口訣static 加載機制:由父到子,靜態先行,子方法先行,非靜態代碼塊先於構造方法,構造方法最後

8、談談你對 HashMap 中 put/get 方法的認識?如果瞭解再談談 HashMap 的擴容機制?默認大小是多少?什麼是負載因子?什麼是吞吐臨界值?JDK1.7 版本爲例

1、HashSet 底層是採用 HashMap 實現
2、集合裏面放置的永遠是對象的引用而不是對象本身
3、當你在 HashSet 裏 add 對象的時候,實際是 HashMap 裏面 put 了 key-value 鍵值對,其中 key 就是你 add 進來的對象,value 是一個固定的 Object 常量
4、HashMap 底層是個 Entry 類型的,名字叫 table 的數組
5、put:當程序試圖將一個 key-value 對放入 HashMap 中時,程序首先根據該 key 的 hashCode() 返回值決定該 Entry 的存儲位置:如果兩個 Entry 的 key 的 hashCode() 返回值相同,那它們的存儲位置相同。如果這兩個 Entry 的 key 通過 equals 比較返回 true,則新添加 Entry 的 value 將覆蓋集合中原有 Entry 的 value,但 key 不會覆蓋。如果這兩個Entry 的 key 通過 equals 比較返回 false,則新添加的 Entry 將與集合中原有 Entry 形成 Entry 鏈,而且新添加的 Entry 位於 Entry 鏈的頭部--具體說明繼續看 addEntry() 方法的說明。

順便複習下 hashCode、equals、HashSet、HashMap 之間到底有什麼樣的關係?

9、請問 ArrayList/LinkedList/Vector 的區別?談談你的理解?ArrayList 底層是什麼?擴容機制?Vector 和 ArrayList 的最大區別?JDK1.7

ArrayList 定義

ArrayList/LinkedList/Vector的區別

10、線程

java8 中的 JUC = java.util.concurrent

題目1:3 個售票員賣出 30 張票 賣票。
示例代碼如下:

package com.atguigu.thread;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock; // 可重入鎖

/**
 * 題目1:3 個售票員賣出 30 張票 賣票 
 * synchronized: 鎖提供了對共享資源的獨佔訪問,一次只能有一個線程獲得鎖,對共享資源的所有訪問都需要首先獲得鎖。
 * 
 * 1	線程	   操作   資源類 
 * 2	高內聚+低耦合
 * 
 * java8 中的 JUC = java.util.concurrent
 */
public class ThreadDemo01 {
	
	public static void main(String[] args) {
		
		Ticket ticket = new Ticket();

		// 實際開發中使用 匿名內部類 的方式(代碼冗餘度下降)
//		new Thread(new Runnable() {
//			@Override
//			public void run() {
//				for (int i = 1; i <= 40; i++) {
//					ticket.sale();
//				}
//			}
//		}, "AA").start();
		
		// java 8 中使用 lambda 表達式代替 匿名內部類
		new Thread(() -> {
			for (int i = 1; i <= 40; i++) {
				ticket.sale();
			}
		}, "AA").start();

		new Thread(() -> {
			for (int i = 1; i <= 40; i++) {
				ticket.sale();
			}
		}, "BB").start();

		new Thread(() -> {
			for (int i = 1; i <= 40; i++) {
				ticket.sale();
			} // 聯想到 Scala 的 map 操作
		}, "CC").start();
	}
}

class Ticket implements Runnable {
	
	private int number = 30;
	
	private Lock lock = new ReentrantLock(); // Lock 代替 synchronized,但 Lock 更強大

	public void sale() {
		lock.lock();
		try {
			if (number > 0) {
				System.out.println(Thread.currentThread().getName() + "\t" + (number--) + "\t 還剩下:" + number);
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	@Override
	public void run() {
		// TODO Auto-generated method stub
		
	}
}

多線程的三種方式:

  • 第一種:Ticket extend Thread {},此法最搓,強烈不推薦!因爲 Java 中要少用繼承,繼承是很寶貴的資源,儘量要面向接口編程。
  • 第二種:Ticket implements Runnable {} 實現 run() 方法,面向接口編程。不夠好,使得 Ticket 和 Runnable 有關係了,沒有實現 高內聚+低耦合。
  • 第三種:使用 java.util.concurrent.locks.Lock; (JUC 中的 Lock 接口,實現使用匿名內部類的方式),非常好,實現了 高內聚+低耦合+低冗餘度。

題目2:兩個線程對一個初始值爲零的變量操作,實現一個線程加一,另一個線程減一,來 10 輪。
示例代碼如下:

package com.atguigu.thread;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 題目2:兩個線程對一個初始值爲零的變量操作,實現一個線程加一,另一個線程減一,來 10 輪
 * 
 * 1 線程   操作   資源類 
 * 2 高內聚+低耦合
 */
public class ThreadDemo02 {

	public static void main(String[] args) {

		// 新建資源類對象
		ShareData sd = new ShareData();

		// 線程操作資源類的方法
		new Thread(() -> {
			for (int i = 1; i <= 10; i++) {
				try {
					Thread.sleep(200);
					sd.increment();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "A").start();

		new Thread(() -> {
			for (int i = 1; i <= 10; i++) {
				try {
					Thread.sleep(300);
					sd.decrement();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "B").start();

		new Thread(() -> {
			for (int i = 1; i <= 10; i++) {
				try {
					Thread.sleep(400);
					sd.increment();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "C").start();

		new Thread(() -> {
			for (int i = 1; i <= 10; i++) {
				try {
					Thread.sleep(500);
					sd.decrement();
				} catch (Exception e) {
					e.printStackTrace();
				}
			}
		}, "D").start();
	}
}

// 資源類
class ShareData {
	private int number = 0;
	private Lock lock = new ReentrantLock(); // Lock 取代了  synchronized
	private Condition condition = lock.newCondition(); // Condition 取代了 wait,notify,notifyAll

	public void increment() throws InterruptedException {
		lock.lock();
		try {
			while (number != 0) {
				condition.await(); // this.wait();
			}
			// 幹活
			++number;
			System.out.println(Thread.currentThread().getName() + "\t" + number);
			// 通知喚醒
			condition.signalAll(); // this.notifyAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

	public void decrement() throws InterruptedException {
		lock.lock();
		try {
			while (number == 0) {
				condition.await(); // this.wait();
			}
			// 幹活
			--number;
			System.out.println(Thread.currentThread().getName() + "\t" + number);
			// 通知喚醒
			condition.signalAll(); // this.notifyAll();
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			lock.unlock();
		}
	}

//	public synchronized void increment() throws InterruptedException { // 判斷
//		while (number != 0) {
//			this.wait(); // A C
//		}
//		// 幹活 
//		++number;
//		System.out.println(Thread.currentThread().getName() + "\t" + number); 
//		// 通知喚醒
//		this.notifyAll();
//	}
//
//	public synchronized void decrement() throws InterruptedException {
//		while (number == 0) {
//			this.wait();
//		}
//		--number;
//		System.out.println(Thread.currentThread().getName() + "\t" + number);
//		this.notifyAll();
//	}
}

注意:資源類中要用 where 作爲多線程的判斷,不能用 if,這樣能避免線程的虛假喚醒。

題目3:8鎖
示例代碼如下:

package com.atguigu.thread;

import java.util.concurrent.TimeUnit;

/**
 * 8鎖
 * 1	一部手機,正常訪問,先打印蘋果還是Android?答:先蘋果再Android
 * 2	新增 TimeUnit,先打印蘋果還是Android?答:先蘋果再Android
 * 3	新增 hello 方法,先打印蘋果還是hello?答:先hello再蘋果
 * 
 * 4	兩部手機,先打印蘋果還是Android?答:先Android再蘋果
 * 
 * 5	兩個靜態同步方法,一部手機,先打印蘋果還是Android?答:先蘋果再Android
 * 6	兩個靜態同步方法,兩部手機,先打印蘋果還是Android?答:先蘋果再Android
 * 
 * 7	1個靜態同步方法,1個普通同步方法,一部手機,先打印蘋果還是Android?答:先Android再蘋果
 * 8	1個靜態同步方法,1個普通同步方法,兩部手機,先打印蘋果還是Android?答:先Android再蘋果
 * 
 * 一個對象裏面如果有多個 synchronized 方法,某一個時刻內,只有一個線程去調用其中的一個 synchronized 方法了,其它的線程都只能等待,
 * 換句話說,
 * 某一個時刻內,只能有唯一一個線程去訪問這些 synchronized 方法。
 * 
 * 鎖的是當前對象 this,被鎖定後,其它的線程都不能進入到當前對象的其它的 synchronized 方法
 * 
 * 加個普通方法後發現和同步鎖無關
 * 
 * 換成兩個對象後,不是同一把鎖了,情況立刻變化。
 * 
 * 都換成靜態同步方法後,情況又變化
 * 
 * 所有的非靜態同步方法用的都是同一把鎖--實例對象本身,也就是說如果一個實例對象的非靜態同步方法獲取鎖後,--鎖的是當前對象 this
 * 該實例對象的其他非靜態同步方法必須等待獲取鎖的方法釋放鎖後才能獲取鎖,
 * 可是別的實例對象的非靜態同步方法因爲跟該實例對象的非靜態同步方法用的是不同的鎖,
 * 所以毋須等待該實例對象已獲取鎖的非靜態同步方法釋放鎖就可以獲取他們自己的鎖。
 * 
 * 所有的靜態同步方法用的也是同一把鎖--類對象本身,這兩把鎖是兩個不同的對象,--鎖的是當前對象的模板 class
 * 所以靜態同步方法與非靜態同步方法之間是不會有競態條件的。
 * 但是一旦一個靜態同步方法獲取鎖後,其他的靜態同步方法都必須等待該方法釋放鎖後才能獲取鎖,
 * 而不管是同一個實例對象的靜態同步方法之間,還是不同的實例對象的靜態同步方法之間,只要它們同一個類的實例對象!

 */
public class ThreadDemo03 {
	public static void main(String[] args) {
		Phone phone = new Phone();
		Phone phone2 = new Phone();
		
		new Thread(() -> {
			try {
				phone.getIOS();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}, "AA").start();
		
		new Thread(() -> {
			try {
				// phone.getAndroid();
				// phone.getHello();
				phone2.getAndroid();
			} catch (Exception e) {
				e.printStackTrace();
			}
		}, "BB").start();
	}
}

class Phone {
	public static synchronized void getIOS() throws Exception {
		TimeUnit.SECONDS.sleep(4);
		System.out.println("-----getIOS");
	}
	
	public synchronized void getAndroid() throws Exception {
		System.out.println("-----getAndroid");
	}
	
	public void getHello() {
		System.out.println("-----getHello");
	}
}

Linux 下查詢 java 進程的個數:top -H -p {pid} 或者 ps huH p {PID} | wc -l

我的GitHub地址:https://github.com/heizemingjun
我的博客園地址:https://www.cnblogs.com/chenmingjun
我的CSDN地址:https://blog.csdn.net/u012990179
我的螞蟻筆記博客地址:https://blog.leanote.com/chenmingjun
Copyright ©2018~2019 黑澤君
【轉載文章務必保留出處和署名,謝謝!】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章