大数据技术之_31_Java 面试题_02_== 和 equals 有什么区别 + String 相关 + 多态 + 传值 + static 加载机制 + 线程
1、== 和 equals 有什么区别?2、为什么需要同时覆写 hashCode 和 equals 方法?3、为什么用 eclipse 重写 hashCode 方法,有 31 这个数字?4、String 相关5、多态6、传值7、static 加载机制8、谈谈你对 HashMap 中 put/get 方法的认识?如果了解再谈谈 HashMap 的扩容机制?默认大小是多少?什么是负载因子?什么是吞吐临界值?JDK1.7 版本为例9、请问 ArrayList/LinkedList/Vector 的区别?谈谈你的理解?ArrayList 底层是什么?扩容机制?Vector 和 ArrayList 的最大区别?JDK1.710、线程
程序员级别:码龙 > 码神 > 码农 > 码畜
学生级别:学神 > 学霸 > 学渣 > 学弱
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
【转载文章务必保留出处和署名,谢谢!】