GuangchaoSun's Blog

面试题总结

总结的一些关于Java的面试题

1.Java中的原始数据类型有哪些,它们的大小及对应的封装类?

data type representation 封装类
byte 8-bit Byte
char 16-bit Character
short 16-bit Short
int 32-bit Integer
float 32-bit Float
long 64-bit Long
double 64-bit Double

boolean类型单独使用是4个字节,在数组中是一个字节。

面对对象的三个特性:封装,继承,多态

2.Java中实现多态的机制是什么?

靠的是父类或接口定义的引用变量可以指向子类或者具体实现类的实例对象,而程序的调用方法在运行期才动态绑定,就是引用变量指向的具体实例对象的方法,也就是内部里正在运行的那个对象的方法,而不是引用变量的类型中定义的方法。

如何实现多态:

  • 接口实现
  • 继承父类重写方法
  • 同一类中进行方法重载

虚拟机是如何实现多态的:
动态绑定技术(dynamic binding),执行期间判断所引用对象的实际类型,根据实际类型调用对应的方法。

参考:面向对象三大特性

3.short s1 = 1; s1 = s1 + 1;有什么错?short s1 = 1; s1 += 1;有什么错?

对于short s1 = 1; s1 = s1 + 1;由于s1 + 1运算时会自动提升表达式的类型,所以结果是int型,在赋值给short类型s1时,编译器将报需要强制转换类型的错误。

对于short s1 = 1; s1 += 1;由于 +=是java语言规定的运算符,java编译器会对它进行特殊处理,因此可以正确编译。

4.char型变量中能不能贮存一个中文汉字?为什么?

char型变量是用来存储Unicode编码的字符的,unicode编码字符集中包含了汉字,所以,char型变量当然可以存储汉字。不过,如果某个特殊的汉字没有被包含在unicode编码字符集中,那么这个char型变量中就不能存储这个特殊的汉字。补充:unicode编码占用两个字节,所以char类型的变量也是占用两个字节。

5.接口是否可以继承接口?抽象类是否可实现接口?抽象类是否可继承具体类?抽象类中是否可以有静态的main方法?

都可以有。
抽象类与普通类的唯一区别:就是不能创建实例和允许有abstract方法

6.构造器Constructor是否可被override?

构造器Constructor不能被继承,因此不能被重写Override,但可以被重载。

7.abstract class和interface的区别?

抽象类的意义:

  1. 为其他子类提供一个公共的类型
  2. 封装子类中重复定义的内容
  3. 定义抽象方法(为了被子类实现)

抽象类与接口的区别

比较 抽象类 接口
默认方法 抽象类可以有默认的方法实现 Java8之前,接口中不存在方法的实现
实现方式 子类使用extends关键字来继承抽象类,子类需要提供抽象类中所声明方法的实现。 子类使用implements来实现接口,需要提供接口中所有声明的实现
构造器 抽象类中可以有构造器 接口中不能
和正常类区别 抽象类不能被实例化 接口则是完全不同的类型
访问修饰符 抽象方法可以有public,protected和default等修饰符 接口默认是public,不能使用其他修饰符
添加新方法 向抽象类中添加新方法,可以提供默认的实现,因此可以不修改子类现有的代码 如果往接口中添加新方法,则子类中需要实现该方法

什么是不可变对象:
不可变对象指对象一旦被创建,状态就不能被改变。任何修改都会创建一个新的对象。如String、Integer及其他包装类

Java中创建对象的几种方式:

  1. 采用new语句创建对象
  2. 采用反射,调用java.lang.Class或者java.lang.reflect.Constructor类的newInstance()方法
  3. 调用对象的clone()方法
  4. 运用反序列手段,调用java.io.ObjectInputStream对象的readObject方法

8.Object中的公共方法有哪些:

  1. equals(),hashCode(),toString()
  2. clone():obj.clone().getClass() == obj.getClass()
  3. getClass()
  4. notify(),notifyAll(),wait()

9.谈一谈Java中==equals()的区别

Java中的数据类型,可分为两类:

  1. 基本数据类型,也称原始数据类型。byte,short,char,int,float,double,long,boolean他们之间的比较,应该用双等号==,比较的是他们的值。还有枚举类型也要用==

  2. 复合数据类型(类)
    当他们用==进行比较的时候,比较的是他们在内存中的存放地址,所以,除非是同一个new出来的对象,他们的比较后的结果为true,否则比较后结果为false。

Java当中所有的类都是继承于Object这个基类的,在Object中的基类中定义了一个equals的方法,这个方法的初始行为是比较对象的内存地址,但在一些类库当中这个方法被覆盖(重写)掉了,如String、Integer、Date,在这些类当中equals有其自身的实现,而不再是比较类在堆内存中的存放地址了。

总结:
对于复合数据类型之间进行equals比较,在没有覆写equals方法的情况下,他们之间的比较还是基于他们在内存中的存放位置的地址值的,因为Object的equals方法也是用双等号(==)进行比较的,所以比较后的结果跟双等号(==)的结果相同

总的来说,==用于比较是不是一个人,equals比较的是长得是否一样。

参考:Java中equals和==的区别

10.equals()和hashCode()的联系

hashCode()是Object类的一个方法,返回一个哈希值。如果两个对象根据equals()方法比较相等,那么调用这两个对象中任意一个对象的hashCode()方法必须产生相同的hash值。
如果两个对象根据equals()方法比较不相等,那么产生的哈希值不一定相等(碰撞的情况下还是会相等的)

11.a.hashCode()有什么用?与a.equals(b)有什么关系

hashCode()方法是相应对象整型的 hash 值。它常用于基于hash的集合类,如 Hashtable、HashMap、LinkedHashMap等等。它与equals()方法关系特别紧密。根据 Java 规范,两个使用equals()方法来判断相等的对象,必须具有相同的 hashcode
将对象放入到集合中时,首先判断要放入对象的hashcode是否已经在集合中存在,不存在则直接放入集合.如果hashcode相等,然后通过equals()方法判断要放入对象与集合中的任意对象是否相等:如果equals()判断不相等,直接将该元素放入集合中,否则不放入.

12.内部类的作用:

内部类可以有多个实例,每个实例都有自己的状态信息,并与其它外围对象的信息相互独立。
内部类提供了更好的封装,除了该外围类,其他类都不能访问。

13.String,StringBuilder和StringBuffer区别


进程,线程之间的区别
进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少上下文切换次数,从而效率更高。同一个进程中的多个线程可以并发执行。

守护线程
程序运行完毕,jvm会等待非守护线程完成后关闭,但是jvm不会等待守护线程。守护线程最典型的的例子就是GC线程。

多线程的上下文切换
指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。

Runnable和Callable的区别:
Runnable接口中的run方法的返回值是void,它做的事只是去执行run方法中的代码;Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。Callable+Future/FutureTask可以获得多线程运行的结果,在等待时间太长没获取到需要的数据的情况下取消该线程的任务。

14.什么导致线程阻塞

方法 说明
sleep() sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止
suspend()和resume() 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。
yield() yield() 使得线程放弃当前分得的 CPU 时间,但是不使线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程
wait()和notify() 两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用.

Java多线程suspend()、resume()和wait()、notify()的区别

为什么wait(),notify(),notifyAll()必须在同步方法/代码块中调用?

wait方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别?
wait()方法会立刻释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

15.wait()和sleep()的区别

  • sleep()来自Thread类,wait()来自Object类。调用sleep()方法的过程中,线程不会释放对象锁。而调用wait()方法线程会释放对象锁(前提是当前线程为对象锁的持有者)。
  • sleep()睡眠后不出让系统资源,wait让其他线程可以占用CPU
  • sleep(milliseconds)需要制定一个睡眠时间,时间一到会自动唤醒。而wait()需要notify()/notifyAll()配合使用。

sleep是线程类的方法,导致此线程暂停执行指定时间,把执行机会给其他线程,但是监控状态依然保持,到时后会自动恢复。调用sleep不会释放对象锁。
wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
public class MutiThread {
public static void main(String[] args) {
new Thread(new Thread1()).start();
try{
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
new Thread(new Thread2()).start();
}
private static class Thread1 implements Runnable{
@Override
public void run(){
synchronized (MutiThread.class){
System.out.println("enter thread1");
System.out.println("thread1 is waiting");
try {
MutiThread.class.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("thread1 is going on...");
System.out.println("thread1 is being over!");
}
}
}
private static class Thread2 implements Runnable{
@Override
public void run(){
synchronized (MutiThread.class){
System.out.println("enter thread2...");
System.out.println("thread2 notify other thread can release wait status...");
MutiThread.class.notify();
System.out.println("thread2 is sleeping ten millisecond...");
try {
Thread.sleep(10);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println("thread2 is going on...");
System.out.println("thread2 is being over!");
}
}
}
}

16.当一个线程如果一个对象的一个synchronized方法后,其他线程是否可以进入此对象的其他方法?

  1. 其他方法前是否加了synchronized关键字,如果没加,则能。
  2. 如果这个方法内部调用了wait,则可以进入其他synchronized方法
  3. 如果其他各个方法都加了synchronized关键字,并且内部没有调用wait,则不能
  4. 如果其他方法是static,它用的同步锁是当前类的字节码,与非静态类的方法不能同步,因为非静态的方法用的是this。

17.线程的基本概念、线程的基本状态及状态之间的关系

状态:就绪,运行,synchronized阻塞,wait和sleep挂起,结束。wait必须在synchronized内部调用
调用线程的start方法后线程进入就绪状态,线程调度系统将就绪状态的线程转为运行状态,遇到synchronized语句时,由运行状态转为阻塞,当synchronized获得锁后,由阻塞转为运行,这种情况可以调用wait方法转为挂起状态,当线程关联的代码执行完毕后,线层变为结束状态。

18.设计四个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
public class ThreadTest {
private int j;
public static void main(String[] args) {
ThreadTest test = new ThreadTest();
Inc inc = test.new Inc();
Dec dec = test.new Dec();
for (int i=0; i < 2; i++){
Thread t = new Thread(inc);
t.start();
t = new Thread(dec);
t.start();
}
}
private synchronized void inc(){
j++;
System.out.println(Thread.currentThread().getName()
+ "-inc:" + j);
}
private synchronized void dec(){
j--;
System.out.println(Thread.currentThread().getName()
+ "-dec:" + j);
}
class Inc implements Runnable{
public void run(){
for (int i=0; i < 100; i++){
inc();
}
}
}
class Dec implements Runnable{
public void run(){
for (int i = 0; i < 100; i++){
dec();
}
}
}
}

19.内存泄漏

java中内存泄漏的情况:长生命周期的对象持有短生命周期对象的引用就很可能发生内存泄漏,尽管短生命周期对象已经不再需要,但是因为长生命周期对象持有它的引用而导致不能被回收,这就是Java中内存泄漏的发生场景。

如果一个外部类的实例对象的方法返回了一个内部类的实例对象,这个内部类对象被长期引用了,即使那个外部类实例对象不再被引用,但由于内部类持久外部类的实例对象,这个外部类对象将不会被垃圾回收,这也会造成内存泄漏。

ReentrantLock

1
2
3
4
5
6
7
8
9
10
11
12
13
class X{
private final ReentrantLock lock = new ReentrantLock();
//...
public void m(){
lock.lock();//block until condition holds
try{
//...method body
}finally{
lock.unlock()
}
}
}

FutureTask是什么?
FutureTask表示一个异步运算的任务。FutureTask里面可以传入一个Callable的具体实现类,可以对这个异步运算的任务的结果进行等待获取、判断是否已经完成、取消任务等操作。当然,由于FutureTask也是Runnable接口的实现类,所以FutureTask也可以放到线程池中。

如何正确地使用wait()?

1
2
3
4
5
synchronized(obj){
while(condition does not hold)
obj.wait();//Release lock,and reacquires on wake up
...//Perform action appropriate to conditon
}

什么是线程局部变量(ThreadLocal)?
当工作于多线程的对象使用ThreadLocal维护变量时,ThreadLocal为每一个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不影响其他线程对应的变量副本;
ThreadLocal是一种以空间换时间的做法,在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然没有线程安全方面的问题。
参考:深入研究java.lang.ThreadLocal类

线程池的作用

在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫做工作线程

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
  3. 提高线程的可管理性
    常用线程池:ExecutorService

生产者消费者模型的作用是什么?

  • 通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者模型最重要的作用。
  • 解耦,这是生产者消费者模型的附带作用,解耦意味着生产者和消费者之间联系少,联系越少越可以独自发展而不需要受到相互的制约。

参考:聊聊并发——生产者消费者模式

通过阻塞队列实现模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue q){
this.queue = q;
}
@Override
public void run(){
try {
while (true){
Thread.sleep(1000);
queue.put(produce());
}
}catch (InterruptedException e){
}
}
private int produce(){
int n = new Random().nextInt(10000);
System.out.println("Thread: " + Thread.currentThread().getId() + " produce: " + n);
return n;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue q){
this.queue = q;
}
@Override
public void run(){
while (true){
try {
Thread.sleep(2000);
consume(queue.take());
}catch (InterruptedException e){
}
}
}
private void consume(Integer n){
System.out.println("Thread: " + Thread.currentThread().getId() + " consume: " + n);
}
public static void main(String[] args){
BlockingQueue<Integer> q1 = new ArrayBlockingQueue<Integer>(100);
Producer p = new Producer(q1);
Consumer c1 = new Consumer(q1);
Consumer c2 = new Consumer(q1);
new Thread(p).start();
new Thread(c1).start();
new Thread(c2).start();
}
}

volatile关键字

volatile如何保证内存可见性

  1. 当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存。
  2. 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

synchronized关键字

fail-fast机制

fail-fast机制是Java集合(Collection)中的一种错误机制。当多个线程对一个集合的内容进行操作时,就可能会产生fail-fast事件,并抛出ConcurrentModificationException异常

happens-before

如果两个操作之间具有happens-before关系,那么前一个操作的结果就会对会面一个可见

  • 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
  • 监视器锁规则:对一个监视器的解锁,happens-before于随后对这个监视器的加锁
  • volatile变量规则:对一个volatile域的写,happens-before于任意后续对这这个volatile域的读
  • 传递性:如果A happens-before B,且 B happens-before C,那么 A happens-before C
  • 线程启动规则:Thread对象的start()方法happens-before于此线程的每一个动作

其他参考: