Java 面试总结(初级)
一、基础相关概念
面向对象的三个特征
- 封装:将某事物的属性和行为包装到对象中。
- 继承:子对象可以继承父对象的属性和行为。
- 多态:父对象中的同一个行为能在其多个子对象中有不同的表现。
- 抽象:是从众多的事物中抽取出共同的、本质性的特征,而舍弃其非本质的特征。
多态存在的三个必要条件
- 要有继承。
- 要有重写。
- 父类引用指向子类对象。
重载与重写的区别
- override(重写)
1、方法名、参数、返回值相同。
2、子类方法不能缩小父类方法的访问权限。
3、子类方法不能抛出比父类方法更多的异常(但子类方法可以不抛出异常)。
4、存在于父类和子类之间。
5、方法被定义为final不能被重写。
- overload(重载)
1、参数类型、个数、顺序至少有一个不相同。
2、不能重载只有返回值不同的方法名。
3、存在于父类和子类、同类中。
接口的意义
- 规范,扩展,回调。
接口与抽象的区别
- 抽象类要被子类继承,接口要被类实现。
- 接口类只能做方法声明,抽象类既可以做方法声明,也可以做方法实现。
- 接口方法定义的变量是公共的静态的常量,抽象类中的变量是普通变量。
- 接口是设计的结果,抽象是重构的结果。
- 接口是用来抽象功能,抽象是用来抽象类别。
父类的静态方法能否被子类重写
- 不能。重写只适用于实例方法,不能用于静态方法,而子类当中含有和父类相同签名的静态方法,我们一般称之为隐藏。
什么是不可变对象
- 不可变对象指,对象一旦被创建,状态就不能被改变了。
静态变量和实例变量的区别?
- 静态变量存储在方法区,属于类所有。实例变量存储在堆当中,其引用存在当前线程栈。
能否创建一个包含可变对象的不可变对象?
- 可以创建,但是不要共享可变对象的引用。
你对String对象的intern()熟悉么?
- intern()进行操作时,会先从常量池中查询是否存在该常量值,如果不存在则在现常量池创建,如果存在直接返回结果。
Object中有哪些公共的方法?
- equals();
- clone();
- getClass();
- toString();
Java中==和equals()的区别?
- == 是运算符,用于比较两个变量是否相等,equals()是Object中的方法,用于比较对象是否相等。基本数据类型用== 比较;是比较的两个值,对象用==是比较的内存地址。
equals()与hashCode()
- hashCode()也是Object中的方法,调用它会返回一个哈希值,如果两个对象使用了equals()方法比较结果为true,那么使用hashCode()比较后的哈希值必须相等。如果equals()方法不相等,那么hashCode()方法的哈希值不一定相等。
有没有可能两个不相等的对象有相同的hashCode?
- 有可能,两个不相等的对象有相同的hashCode值,这就是说为什么在HashMap中有冲突。
可以在hashCode中使用随机数吗?
- 不可以,因为同一对象的hashCode值的必须一致。
& 和 && 的区别
- & 是位操作,&& 是逻辑运算符,逻辑运算符会有短路特性,& 没有。
逻辑运算符的短路特性
&& 的短路特性:因为程序是从左往右执行的,当判断左边为false的时候,&& 的返回结果就是false。两边必须都为true才满足条件。
|| 的短路特性:当判断左边或者右边有true时,返回结果就是true。
内部类的作用
- 内部类可以有多个实例,每个实例都有自己的状态信息,并且与其他外围对象的信息相互独立.在单个外围类当中,可以让多个内部类以不同的方式实现同一接口,或者继承同一个类.创建内部类对象的时刻不依赖于外部类对象的创建。内部类并没有令人疑惑的”is-a”管系,它就像是一个独立的实体。
- 内部类提供了更好的封装,除了该外围类,其他类都不能访问。
final, finalize和finally的不同之处
final是一个修饰符,可以修饰变量、方法和类,被final 修饰变量的值在初始化后不能被改变,被final修饰过的类不能被继承。
finalize方法是在对象被回收之前调用的方法,给对象自己最后一个复活的机会,但是什么时候调用 finalize 没有保证。
finally 是一个关键字,与 try 和 catch 一起用于异常的处理。finally 块一定会被执行,无论在 try 块中是否有发生异常。
final有哪些用法
- 被final修饰的类不可以被继承
- 被final修饰的方法不可以被重写
- 被final修饰的变量不可以被改变。如果修饰引用,那么表示引用不可变,引用指向的内容可变。
- 被final修饰的方法,JVM会尝试将其内联,以提高运行效率
- 被final修饰的常量,在编译阶段会存入常量池中。
深拷贝和浅拷贝的区别是什么?
- 浅拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象。换言之,<font color='red'>浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象</font>。
- 深拷贝:被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制过的新对象,而不再是原有的那些被引用的对象。换言之,<font color='red'>深拷贝把要复制的对象所引用的对象都复制了一遍</font>。
static都有哪些用法?
- static两个基本用法:静态变量和静态方法。也就是被static所修饰的变量/方法都属于类的静态资源,类实例所共享。类中最先加载的变量或者方法。
- 除了静态变量和静态方法之外,static也用于静态块,可用于初始化操作:
public calss PreCache{
static{
//执行相关操作
}
}
- 此外static也可用于修饰内部类,此时称之为静态内部类。
- 最后一种用法就是静态导包,即import static是在JDK 1.5之后引入的新特性,可以用来指定导入某个类中的静态资源,并且不需要使用类名。资源名,可以直接使用资源名,比如:
import static java.lang.Math.*;
public class Test{
public static void main(String[] args){
//System.out.println(Math.sin(20));传统做法
System.out.println(sin(20));
}
}
二、数据类型相关
java中int char,long等各占多少字节?
| 类型 | 字节数 位数 |
|---|---|
| short | 2 16 |
| int | 4 32 |
| long | 8 64 |
| folat | 4 32 |
| double | 8 64 |
| char | 2 16 |
int和Integer的区别
- int 是基本数据类型,Integer 是一个对象,但是在编译后 Integer 自动转换成 int。
int 和Integer谁占用的内存更多?
- 当然是Integer占用的内存多了,因为 Integer 是一个对象,需要存储对象的元数据,int 是原始的基本数据类型。
String, StringBuffer和StringBuilder区别
- String 是字符串常量;final修饰,StringBuffer(线程安全)和 StringBuilder(线程不安全)都是字符串变量。
String,StringBuffer区别
String 和 StringBuffer 主要区别是性能:String 是不可变对象,每次对 String 类型操作都等同于产生一个新的 String 对象,然后指向新的 String对象。所以尽量不在对 String 进行大量的拼接操作,否则会产生很多临时对象,导致GC开始工作,影响系统性能。
StringBuffer 是对对象本身操作,而不是产生新的对象,因此在有大量拼接的情况下,我们建议使用StringBuffer。
<font color='red'>但是需要注意现在JVM会对 String 拼接做一定的优化:</font>String s=“This is only ”+”simple”+”test”会被虚拟机直接优化成String s=“This is only simple test”,此时就不存在拼接过程。
StringBuffer和StringBuilder
- StringBuffer 是线程安全的可变字符串,其内部实现是可变数组。StringBuilder 是 Java5 新增的,其功能和 StringBuffer 类似,但是线程不安全。因此,在没有多线程问题的前提下,使用 StringBuilder 会取得更好的性能。
java当中使用什么类型表示价格比较好?
- 如果不是特别关心内存和性能的话,使用BigDecimal,否则使用预定义精度的 double 类型。
如何将 byte 转为 String
- 可以使用 String 接收 byte[] 参数的构造器来进行转换,需要注意的点是要使用的正确的编码,否则会使用平台默认编码,这个编码可能跟原来的编码相同,也可能不同。
可以将int强转为byte类型么?会产生什么问题?
- 我们可以强制转换,但是Java中int是32位的而byte是8 位的,所以,如果强制转化int类型的高24位将会被丢弃。
三、关于垃圾回收
你知道哪些垃圾回收算法?
- 垃圾回收从理论上非常容易理解,具体的方法有以下几种:
- 标记——清除算法(Mark-Sweep)
- 标记——复制算法(Mark——Copy)
- 标记——整理算法(Mark——Compat)
- 分代收集算法
- 增量收集算法
更详细的内容参见:
深入理解JVM垃圾回收算法
如何判断一个对象是否应该被回收?
- 这就是所谓的对象存活性判断,常用的方法有两种:1.引用计数法; 2.对象可达性分析。由于引用计数法存在互相引用导致无法进行GC的问题,所以目前JVM虚拟机多使用对象可达性分析算法。
简单的解释一下垃圾回收
- Java 垃圾回收机制最基本的做法是==分代回收==。
- 内存中的区域被划分成不同的世代,对象根据其存活的时间被保存在对应世代的区域中。
- 一般的实现是划分成3个世代:年轻、年老和永久。内存的分配是发生在年轻世代中的。
- 当一个对象存活时间足够长的时候,它就会被复制到年老世代中。对于不同的世代可以使用不同的垃圾回收算法。
- 进行世代划分的出发点是对应用中对象存活时间进行研究之后得出的统计规律。
- 一般来说,一个应用中的大部分对象的存活时间都很短。
- 比如局部变量的存活时间就只在方法的执行过程中。
- 基于这一点,对于年轻世代的垃圾回收算法就可以很有针对性。
调用System.gc()会发生什么?
- 通知GC开始工作,但是GC真正开始的时间不确定。
四、进程,线程相关
说说进程,线程,协程之间的区别
- 简而言之,进程是程序运行和资源分配的基本单位,一个程序至少有一个进程,一个进程至少有一个线程。进程在执行过程中拥有独立的内存单元,而多个线程共享内存资源,减少切换次数,从而效率更高。线程是进程的一个实体,是cpu调度和分派的基本单位,是比程序更小的能独立运行的基本单位。同一进程中的多个线程之间可以并发执行。
你了解守护线程吗?它和非守护线程有什么区别
- 程序运行完毕,jvm 会等待非守护线程完成后关闭,但是 jvm 不会等待守护线程。守护线程最典型的例子就是GC线程。
java中创建线程的三种方法以及区别
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程
更详细的内容参见: java中创建线程的三种方法以及区别
Thread类中的start()和run()方法有什么区别?
- start()方法被用来启动新创建的线程,而且start()内部调用了run()方法,这和直接调用run()方法的效果不一样。当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动,start()方法才会启动新线程。
怎么检测一个线程是否持有对象监视器
- Thread类提供了一个holdsLock(Object obj)方法,当且仅当对象 obj 的监视器被某条线程持有的时候才会返回true,注意这是一个static方法,这意味着“某条线程”指的是当前线程。
什么导致线程阻塞
阻塞指的是暂停一个线程的执行以等待某个条件发生(如某资源就绪)。
| 方法 | 说明 |
|---|---|
| sleep() | sleep() 允许 指定以毫秒为单位的一段时间作为参数,它使得线程在指定的时间内进入阻塞状态,不能得到CPU 时间,指定的时间一过,线程重新进入可执行状态。 典型地,sleep() 被用在等待某个资源就绪的情形:测试发现条件不满足后,让线程阻塞一段时间后重新测试,直到条件满足为止 |
| suspend() 和 resume() | 两个方法配套使用,suspend()使得线程进入阻塞状态,并且不会自动恢复,必须其对应的resume() 被调用,才能使得线程重新进入可执行状态。典型地,suspend() 和 resume() 被用在等待另一个线程产生的结果的情形:测试发现结果还没有产生后,让线程阻塞,另一个线程产生了结果后,调用 resume() 使其恢复。 |
| yield() | yield() 使当前线程放弃当前已经分得的CPU 时间,但不使当前线程阻塞,即线程仍处于可执行状态,随时可能再次分得 CPU 时间。调用 yield() 的效果等价于调度程序认为该线程已执行了足够的时间从而转到另一个线程 |
| wait() 和 notify() | 两个方法配套使用,wait() 使得线程进入阻塞状态,它有两种形式,一种允许 指定以毫秒为单位的一段时间作为参数,另一种没有参数,前者当对应的 notify() 被调用或者超出指定时间时线程重新进入可执行状态,后者则必须对应的 notify() 被调用。 |
wait(),notify()和suspend(),resume()之间的区别
初看起来它们与 suspend() 和 resume() 方法对没有什么分别,但是事实上它们是截然不同的。区别的核心在于,前面叙述的所有方法,阻塞时都不会释放占用的锁(如果占用了的话),而这一对方法则相反。上述的核心区别导致了一系列的细节上的区别。
首先,前面叙述的所有方法都隶属于 Thread 类,但是这一对却直接隶属于 Object 类,也就是说,所有对象都拥有这一对方法。初看起来这十分不可思议,但是实际上却是很自然的,因为这一对方法阻塞时要释放占用的锁,而锁是任何对象都具有的,调用任意对象的 wait() 方法导致线程阻塞,并且该对象上的锁被释放。而调用 任意对象的notify()方法则导致从调用该对象的 wait() 方法而阻塞的线程中随机选择的一个解除阻塞(但要等到获得锁后才真正可执行)。
其次,前面叙述的所有方法都可在任何位置调用,但是这一对方法却必须在 synchronized 方法或块中调用,理由也很简单,只有在synchronized 方法或块中当前线程才占有锁,才有锁可以释放。同样的道理,调用这一对方法的对象上的锁必须为当前线程所拥有,这样才有锁可以释放。因此,这一对方法调用必须放置在这样的 synchronized 方法或块中,该方法或块的上锁对象就是调用这一对方法的对象。若不满足这一条件,则程序虽然仍能编译,但在运行时会出现IllegalMonitorStateException 异常。
wait() 和 notify() 方法的上述特性决定了它们经常和synchronized关键字一起使用,将它们和操作系统进程间通信机制作一个比较就会发现它们的相似性:synchronized方法或块提供了类似于操作系统原语的功能,它们的执行不会受到多线程机制的干扰,而这一对方法则相当于 block 和wakeup 原语(这一对方法均声明为 synchronized)。它们的结合使得我们可以实现操作系统上一系列精妙的进程间通信的算法(如信号量算法),并用于解决各种复杂的线程间通信问题。
关于 wait() 和 notify() 方法最后再说明两点:
第一:调用 notify() 方法导致解除阻塞的线程是从因调用该对象的 wait() 方法而阻塞的线程中随机选取的,我们无法预料哪一个线程将会被选择,所以编程时要特别小心,避免因这种不确定性而产生问题。
第二:除了 notify(),还有一个方法 notifyAll() 也可起到类似作用,唯一的区别在于,调用 notifyAll() 方法将把因调用该对象的 wait() 方法而阻塞的所有线程一次性全部解除阻塞。当然,只有获得锁的那一个线程才能进入可执行状态。
谈到阻塞,就不能不谈一谈死锁,略一分析就能发现,suspend() 方法和不指定超时期限的 wait() 方法的调用都可能产生死锁。遗憾的是,Java 并不在语言级别上支持死锁的避免,我们在编程中必须小心地避免死锁。
以上我们对 Java 中实现线程阻塞的各种方法作了一番分析,我们重点分析了 wait() 和 notify() 方法,因为它们的功能最强大,使用也最灵活,但是这也导致了它们的效率较低,较容易出错。实际使用中我们应该灵活使用各种方法,以便更好地达到我们的目的。
产生死锁的条件
- 1.互斥条件:一个资源每次只能被一个进程使用。
- 2.请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
- 3.不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。
- 4.循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
为什么wait()方法和notify()/notifyAll()方法要在同步块中被调用
- 这是JDK强制的,wait()方法和notify()/notifyAll()方法在调用前都必须先获得对象的锁
wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别
- wait()方法立即释放对象监视器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。
sleep()与wait()的区别
- sleep()来自Thread类,wait()来自Object类。调用sleep()方法的过程中,线程不会释放对象锁。而调用 wait 方法线程会释放对象锁
- sleep()睡眠后不出让系统资源,wait让其他线程可以占用CPU
- sleep(milliseconds)需要指定一个睡眠时间,时间一到会自动唤醒.而wait()需要配合notify()或者notifyAll()使用
为什么wait, nofity和nofityAll这些方法不放在Thread类当中
- JAVA提供的锁是对象级的而不是线程级的,每个对象都有锁,通过线程获得。如果线程需要等待某些锁那么调用对象中的wait()方法就有意义了。如果wait()方法定义在Thread类中,线程正在等待的是哪个锁就不明显了。简单的说,由于wait,notify和notifyAll都是锁级别的操作,所以把他们定义在Object类中因为锁属于对象。
怎么唤醒一个阻塞的线程
- 如果线程是因为调用了wait()、sleep()或者join()方法而导致的阻塞,可以中断线程,并且通过抛出InterruptedException来唤醒它;如果线程遇到了IO阻塞,无能为力,因为IO是操作系统实现的,Java代码并没有办法直接接触到操作系统。
什么是多线程的上下文切换
- 多线程的上下文切换是指CPU控制权由一个已经正在运行的线程切换到另外一个就绪并等待获取CPU执行权的线程的过程。
synchronized和ReentrantLock的区别
-
synchronized是和if、else、for、while一样的关键字,ReentrantLock是类,这是二者的本质区别。既然ReentrantLock是类,那么它就提供了比synchronized更多更灵活的特性,可以被继承、可以有方法、可以有各种各样的类变量,ReentrantLock比synchronized的扩展性体现在几点上:
(1)ReentrantLock可以对获取锁的等待时间进行设置,这样就避免了死锁
(2)ReentrantLock可以获取各种锁的信息
(3)ReentrantLock可以灵活地实现多路通知,另外,二者的锁机制其实也是不一样的:ReentrantLock底层调用的是Unsafe的park方法加锁,synchronized操作的应该是对象头中mark word。
如何在两个线程间共享数据
- 通过在线程之间共享对象就可以了,然后通过wait/notify/notifyAll、await/signal/signalAll进行唤起和等待,比方说阻塞队列BlockingQueue就是为线程之间共享数据而设计的。
一个线程如果出现了运行时异常怎么办?
- 如果这个异常没有被捕获的话,这个线程就停止执行了。另外重要的一点是:如果这个线程持有某个某个对象的监视器,那么这个对象监视器会被立即释放。
如何正确的使用wait()?使用if还是while?
- wait() 方法应该在循环调用,因为当线程获取到 CPU 开始执行的时候,其他条件可能还没有满足,所以在处理前,循环检测条件是否满足会更好。下面是一段标准的使用 wait 和 notify 方法的代码:
synchronized (obj) {
while (condition does not hold)
obj.wait(); // (Releases lock, and reacquires on wakeup)
... // Perform action appropriate to condition
}
什么是线程局部变量ThreadLocal
- 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
ThreadLoal的作用是什么?
- 简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
什么是线程局部变量ThreadLocal
- 线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如 web 服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java 应用就存在内存泄露的风险。
ThreadLoal的作用是什么?
- 简单说ThreadLocal就是一种以空间换时间的做法在每个Thread里面维护了一个ThreadLocal.ThreadLocalMap把数据进行隔离,数据不共享,自然就没有线程安全方面的问题了。
生产者消费者模型的作用是什么?
- (1)通过平衡生产者的生产能力和消费者的消费能力来提升整个系统的运行效率,这是生产者消费者模型最重要的作用。
- (2)解耦,这是生产者消费者模型附带的作用,解耦意味着生产者和消费者之间的联系少,联系越少越可以独自发展而不需要收到相互的制约。
如果你提交任务时,线程池队列已满,这时会发生什么
- 如果你使用的LinkedBlockingQueue,也就是无界队列的话,没关系,继续添加任务到阻塞队列中等待执行,因为LinkedBlockingQueue可以近乎认为是一个无穷大的队列,可以无限存放任务;如果你使用的是有界队列比方说ArrayBlockingQueue的话,任务首先会被添加到ArrayBlockingQueue中,ArrayBlockingQueue满了,则会使用拒绝策略RejectedExecutionHandler处理满了的任务,默认是AbortPolicy。
为什么要使用线程池
- 避免频繁地创建和销毁线程,达到线程对象的重用。另外,使用线程池还可以根据项目灵活地控制并发的数目。
java中用到的线程调度算法是什么
- 抢占式。一个线程用完CPU之后,操作系统会根据线程优先级、线程饥饿情况等数据算出一个总的优先级并分配下一个时间片给某个线程执行。
Thread.sleep(0)的作用是什么
- 由于Java采用抢占式的线程调度算法,因此可能会出现某条线程常常获取到CPU控制权的情况,为了让某些优先级比较低的线程也能获取到CPU控制权,可以使用Thread.sleep(0)手动触发一次操作系统分配时间片的操作,这也是平衡CPU控制权的一种操作。
什么是CAS
- CAS,全称为Compare and Swap,即比较-替换。假设有三个操作数:内存值V、旧的预期值A、要修改的值B,当且仅当预期值A和内存值V相同时,才会将内存值修改为B并返回true,否则什么都不做并返回false。当然CAS一定要volatile变量配合,这样才能保证每次拿到的变量是主内存中最新的那个值,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功。
什么是乐观锁和悲观锁
乐观锁:乐观锁认为竞争不总是会发生,因此它不需要持有锁,将比较-替换这两个动作作为一个原子操作尝试去修改内存中的变量,如果失败则表示发生冲突,那么就应该有相应的重试逻辑。
悲观锁:悲观锁认为竞争总是会发生,因此每次对某资源进行操作时,都会持有一个独占的锁,就像synchronized,不管三七二十一,直接上了锁就操作资源了。
java中的++操作符线程安全么?
- 不是线程安全的。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。
五、关于集合那点事
如何实现集合排序?
- 你可以使用有序集合,如 TreeSet 或 TreeMap,你也可以使用有顺序的的集合,如 list,然后通过 Collections.sort() 来排序。
HashMap的实现原理
- HashMap 是基于哈希表的 Map 接口的非同步实现,允许使用null值和null键。
- HashMap 的数据结构由 数组+链表 的形式存储。
哈希表的实现
- 数据结构的物理存储只有两种:==顺序存储结构==和==链式存储结构==,在数组中根据下标查询某个元素,一次定位就可以完成,哈希表也是利用了这种特性,所以哈希表的主干也是数组。
为何HashMap的数组长度一定是2的次幂?
Hashmap的成员是Entry数组 数组大小16,2的次方;因为put方法的实现是根据key的hashCode进行hash运算,得到值hash;根据hash值去确定数组的位置,length是2的次方公式成立。
ConcurrentHashMap的并发度是什么?
- ConcurrentHashMap的并发度就是segment的大小,默认为16,这意味着最多同时可以有16条线程操作ConcurrentHashMap,这也是ConcurrentHashMap对Hashtable的最大优势,任何情况下,Hashtable能同时有两条线程获取Hashtable中的数据吗?
ConcurrentHashMap的工作原理
-
ConcurrentHashMap在jdk 1.6和jdk 1.8实现原理是不同的。
Jdk 1.6:
ConcurrentHashMap是线程安全的,但是与Hashtablea相比,实现线程安全的方式不同。Hashtable是通过对hash表结构进行锁定,是阻塞式的,当一个线程占有这个锁时,其他线程必须阻塞等待其释放锁。ConcurrentHashMap是采用分离锁的方式,它并没有对整个hash表进行锁定,而是局部锁定,也就是说当一个线程占有这个局部锁时,不影响其他线程对hash表其他地方的访问。
具体实现:ConcurrentHashMap内部有一个Segment.Jdk 1.8
在jdk 8中,ConcurrentHashMap不再使用Segment分离锁,而是采用一种乐观锁CAS算法来实现同步问题,但其底层还是“数组+链表->红黑树”的实现。
Java 中,Serializable 与 Externalizable 的区别
- Serializable 接口是一个序列化 Java 类的接口,以便于它们可以在网络上传输或者可以将它们的状态保存在磁盘上,是 JVM 内嵌的默认序列化方式,成本高、脆弱而且不安全。Externalizable 允许你控制整个序列化过程,指定特定的二进制格式,增加安全机制。
详细集合介绍请参考 Collection 集合类
六、关于 Servlet
Servlet 的执行流程
- Servlet 的执行流程也就是servlet的生命周期,当服务器启动的时候生命周期开始,然后通过init()(启动顺序根据web.xml里的startup-on-load来确定加载顺序)方法初始化servlet,再根据不同请求调用 doGet 或 doPost 方法,最后再通过 destroy() 方法进行销毁。
Servlet 之 doGet & doPost
- doGet 和 doPost 都是接受用户请求的方法,doGet处理get请求,doPost处理post请求,doGet用于地址栏提交,doPost用于表单提交,在页面提交数据时,get的数据大小有限制4k,post没有限制,get请求提交的数据会在地址栏显示,post不显示,所以post 比 get安全.
如何处理 Servlet 的线程不安全问题
- 线程安全就是多线程操作同一个对象不会有问题,线程同步一般来保护线程安全,所以可以在Servlet的线程里面加上同步方法或同步块。(Synchronized)可以保证在同一时间只有一个线程访问,(使用同步块会导致性能变差,最好不去使用实例变量)
会话跟踪有哪些,他们的区别是什么
- Cookie:Cookie 需要通过实例化创建使用的,cookie是http对象,客户端与服务端都可以操纵,cookie是保存在客户端本地的,所以数据很容易被窃取。
- Session:Session 不能通过实例化创建,session是在服务端保存状态,当访问量很多时,使用session会降低服务器的性能。底层是由 Cookie 实现。
- Application:application 的作用域是整个工程里只有一个,可以在不同浏览器之间共享数据,所有人都可以共享,因此 application 也不是安全的。
request,response,session 和 application是怎么用的
- Request 是客户端向服务端发送请求。
- Response 是服务端对客户端请求做出响应。
- Session 通过 getSession() 创建,如果没有设定它的生命周期,或者通过invildate()方法销毁,关闭浏览器 session 就会消失。
- Application 不能直接创建,存在于服务器的内存中,由服务器创建和销毁。
为什么在session少放对象
- 因为 session 底层是由 cookie 实现的,当客户端的 cookie 被禁用后,session 也会失效,应尽量少向 session 中保存信息,session 的数据保存在服务器端,当有大量 session 时,会降低服务器的性能。
Servlet 是单例还是多例
- 单例的,可以提高性能。
Filter 与 拦截器怎么执行的
- 首先初始化过滤器,然后服务器组织过滤器链,所有的请求都必须通过过滤器链,过滤器链是一个栈,遵循先进后出的原则 ,所有的请求经过过滤器,执行顺序根据web.xml里配置的<filter-mapping>先后执行,每个过滤器之间通过chain.doFilter连接,最后调用到资源,执行完再从过滤器链退出。
Java 面试总结(中级)
常用设计模式
设计模式一共有33种。
- 工厂模式:定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类。
- 抽象工厂模式:是围绕一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂。
- 单例模式:保证一个类仅有一个实例。创建方式有懒汉式、饿汉式。
- 建造者模式:使用多个简单的对象一步一步构建成一个复杂的对象。
- 原型模式:是用于创建重复的对象,同时又能保证性能,通过拷贝这些原型创建新的对象。
- 适配器模式:将一个类的接口转换成客户希望的另外一个接口。
- 桥接模式:将抽象部分与实现部分分离,使它们都可以独立的变化。
- 过滤器模式:开发者以一种标准来过滤对象。
- 组合模式:把一组类似的对象当成一个单一对象。
- 装饰器模式:动态地给一个对象添加一些额外的职责。
- 外观模式:为子系统中的一组接口提供一个一致的界面。
- 享元模式:运用共享技术有效地支持大量细粒度的对象。
- 代理模式:一个类代表另一个类做一些事。
- 责任链模式:避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。
- 命令模式:将一个请求封装成一个对象,从而使您可以用不同的请求对客户进行参数化。
- 解释器模式:给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。
- 迭代器模式:提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。
- 中介者模式:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。
- 备忘录模式:在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。
- 观察者模式:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
- 状态模式:允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。
- 空对象模式:一个空对象取代 NULL 对象实例的检查。Null 对象不是检查空值,而是反应一个不做任何动作的关系。
- 策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。
- 模板模式:定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。
- 访问者模式:主要将数据结构与数据操作分离。
- MVC 模式:
Model(模型) - 模型代表一个存取数据的对象或 JAVA POJO,它也可以带有逻辑,在数据变化时更新控制器。
View(视图) - 视图代表模型包含的数据的可视化。
Controller(控制器) - 控制器作用于模型和视图上。它控制数据流向模型对象,并在数据变化时更新视图。它使视图与模型分离开。
- 业务代表模式:用于对表示层和业务层解耦。
- 组合实体模式:
- 数据访问对象模式:
- 前端控制器模式:
- 拦截过滤器模式:
- 服务定位器模式:
- 传输对象模式:
缓存型数据库
Redis的持久化的机制,aof和rdb的区别。
- aof:是一个持续的用日志记录写操作,crash(崩溃)后利用日志恢复;
- rdb:是一个平时写操作的时候不触发写,只有手动提交save命令,或者是关闭命令时,才触发备份操作。
Redis的list结构相关的操作
LRANGE(l-rendʒ):返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定
LPUSH:将一个或多个值 value 插入到列表 key 的表头
LPOP:移除并返回列表 key 的头元素
RPUSH:将一个或多个值 value 插入到列表 key 的表尾 (最右边)
RPOP:移除并返回列表 key 的尾元素
-
RPOPLPUSH:
(1) 将列表中的最后一个元素 (尾元素) 弹出,并返回给客户端
(2) 将列表弹出的元素插入到列表目的地,作为目的地列表的的头元素
Redis的数据结构都有哪些
| 结构类型 | 结构存储的值 | 结构的读写能力 |
|---|---|---|
| String | 可以是字符串、整数、浮点数 | 对整个字符串或者字符串的其中一部分进行操作,对整数和浮点数执行自增或者自减操作 |
| List | 一个链表,链表上每个节点都包含了一个字符串 | 从链表的两端推入或者弹出元素,根据便移量对链表进行修剪,读取单个或多个元素,根据值查找或者移除元素 |
| Set | 包含字符串的无序收集器,并且对被包含的每个字符串都是独一无二各不相同的 | 添加、获取、移除单个元素,检查一个元素是否在集合种存在,计算交集、并集、差集,从集合种随机获取元素 |
| Hash | 包含键值对的无序散列表 | 添加、获取、移除单个键值对。获取所以键值对 |
| zSet | 字符串与浮点数分值之间的有序映射,元素的排序顺序由分值的大小决定 | 添加、获取、移除单个元素,根据分值范围来获取元素 |
Redis2和Redis3的区别
- 一个是完全无中心化的设计,节点之间通过闲聊的方式传递集群信息,数据保证最终一致性。
- 一个是以中心化的方案设计,通过类似一个分布式锁服务来保证强一致性,数据写入先写内存和redo log,然后定期兼容归并到磁盘上,将随机写优化为顺序写,提高写入性能。
了解Redis事务的CAS操作吗
- (1) 单独的隔离操作:事务中的所有命令会被序列化、按顺序执行,在执行的过程中不会被其他客户端发送来的命令打断
- (2) 没有隔离级别的概念:队列中的命令在事务没有被提交之前不会被实际执行
- (3) 不保证原子性:redis中的一个事务中如果存在命令执行失败,那么其他命令依然会被执行,没有回滚机制
Redis集群节点选举
Redis 集群的键空间被分割为16383个槽,集群的最大节点数也是16484个。
-
集群选举流程:
- 例如:我们创建三主三从的Redis集群,主节点为6379、6380、6381;从节点为6382、6383、6384
- 启动Redis服务器,主节点6379的从节点为6382,将6379服务器添加元素,将6379模拟挂掉,按照Redis集群原理,将6379节点的从节点选举6382为主节点, 并且可以从6382节点中得到和6379节点相同的数据内容。如果再次启动6379服务器,它将变成6382的从节点。
Redis的集群怎么同步数据的
前端面试(初、中级)
JSP
Jsp如何处理json
- 在 jsp 中处理 JSON,通常需要配套使用 JQuery 控件,并且导入一些 Common jar 包。使用 JQuery 控件是因为它能有效的解析并且展示 JSON 数据,导入Common 则是因为 Java 中的对象并不是纯粹的数据,需要通过这些 Jar 包的处理使之转化成真实数据。
Jsp的重定向和转发的流程有什么区别
- 重定向是客户端行为,转发是服务器端行为。
- 重定向时服务器产生两次请求,转发产生一次请求,重定向可以到项目以外的任何网址,转发只能在当前项目里转发。
- 重定向会导致 request 对象信息丢失。转发则不会。
- 转发的 url 不会变, request.getRequestDispatch().forward();
- 重定向的 url 会改变, response.getRedirect();
Jsp和servlet的区别
- Jsp 经过编译后就会变成 Servlet。
- jsp 更擅长表现于页面显示, servlet 更擅长于逻辑控制。
- Servlet 中没有内置对象,Jsp中的内置对象都是必须通过域对象得到。Jsp是Servlet的一种简化,使用Jsp只需要完成程序员需要输出到客户端的内容,Jsp中的Java脚本如何镶嵌到一个类中,由Jsp容器完成。
而Servlet则是个完整的Java类,这个类的Service方法用于生成对客户端的响应。
JSP 的九大内置对象,三大指令,七大动作的具体功能
-
JSP九大内置对象:
pageContext :只对当前jsp页面有效,里面封装了基本的request和session的对象
Request :对当前请求进行封装
Session :浏览器会话对象,浏览器范围内有效
Application :应用程序对象,对整个web工程都有效
Out :页面打印对象,在jsp页面打印字符串
Response :返回服务器端信息给用户
Config :单个servlet的配置对象,相当于servletConfig对象
Page :当前页面对象,也就是this
Exception :错误页面的exception对象,如果指定的是错误页面,这个就是异常对象
-
三大指令:
Page :指令是针对当前页面的指令
Include :用于指定如何包含另一个页面
Taglib :用于定义和指定自定义标签
-
七大动作:
Forward,执行页面跳转,将请求的处理转发到另一个页面
Param :用于传递参数
Include :用于动态引入一个jsp页面
Plugin :用于下载javaBean或applet到客户端执行
useBean :使用的javaBean
setProperty :修改javaBean实例的属性值
getProperty :获取javaBean实例的属性值
JSP 的执行原理
- 客户端发出请求(request),jsp引擎将jsp页面翻译成servlet的 Java 源文件,在Tomcat中将源文件编译成class文件,并加载到内存中执行,把结果返回(response)给客户端。
JSP 页面跳转
- Jsp页面跳转有两种方式,forward和redirect(转发和重定向)
- Forward 只能在当前项目里跳转,只产生一次请求,request保存的变量不会丢失,url地址不会改变
- Redirect 可跳转到项目以外的任何页面,产生两次请求,request保存的变量会全部丢失,url地址会发生改变,变化为第二个请求的地址
写出几种JSTL常用标签
- <c:if>,<c:item>,<c:foreach>,<c:out>,<c:set>
说明一下jsp中<jsp: include page..>和<%@ include file%>的区别
<jsp:include page=""/> 动态导入,是行为元素。
<%@ include file="" %> 静态导入,是指令元素。
pageContext有什么作用
- 可以使用 pageContext 对象来设定属性,并指定属性的作用范围,提供了对JSP页面内所有的对象及名字空间的访问