1.技术面试指导
本文从“必备项”和“加分项”两个角度分析,如何拿下高薪offer。
一、必备项
0.自我介绍
表达流畅,不要太差即可
1.基础
坑:【答案很标准】面试时的回答,一定不要背网上《面试大全》中的标准答案,一定要有自己的思想 (哪怕有少量错误) 。
常见的题,一定要提前准备好。例如,以下列举的几乎都是必考题目:
arraylist/hashmap的源码、实现原理 ,冒泡排序/快速排序、 单例模式/工厂模式/动态工厂、谈谈你对面向对象的理解, 事务ACID/隔离级别 ,Spring IOC/AOP
建议:自己的理解 ,或者搜博客/githug上大神的博文。也就是说,可以将 面试题中的问题 在博客、github上搜答案,而不要死记“XXX面试大全”中附带的答案(那些答案往往很浅)。总之,要在自己写答案时,向面试官传达“我的答案是自己写的,我是一个有独立思维的人,而不是网上抄的”。
2.技术列表
掌握程度
坑:“精通” (3年以内的开发者,几乎没人敢说“精通”哪一门技术)
建议:掌握、熟练、理解 ,会使用
坑:个人掌握的技能过于“标准化”,明显就是培训、或者看某套视频学出来。如:java + 数据库+web前端+jsp/servlet+ssm +boot/cloud
建议:一般而言,自学成才的人比培训出来的学生 更具有独立思考的能力,因此在相同的条件下,企业更喜欢没有参加过培训的学生。
建议写上2-3门非培训机构标配课程,如service mesh、netty等(最好写与高并发、分布式有关的,技术的名字相对“少见”但又很重要的)。
对面试而言,这些“少见”的技术,只要你写上了,并且能把其中任意一个核心知识点说明白,就已经非常加分了。(假设Spring是一个“少见”的技术,那么你只要在面试时解释一下什么是IoC就可以了)
坑:简历上写一大堆牛B的技术,显得自己很厉害
建议:技术点宁可少写,也别多写。面试官经常都很忙,没时间精心准备对你的面试,甚至有时候是一边神游一边在提问,所以很可能从你简历里随便挑几个你写上的技术来问你。因此简历上写到的技术,都很可能被问到。
(本条建议与上一个“2-3门非标准课程”并不冲突)
3.项目
坑:项目名叫“Xxx电商项目”、“Xxx管理系统”,这些“项目”简直就是培训机构的标配,缺乏真实项目的感觉
建议:
(1)提前准备好回答“项目”的剧本。
“你做过什么样的项目?”或者根据你简历中的项目来提问,几乎是技术面试官必须做、并且非常喜欢做的事。所以,如果你没有充足的项目经验,就提前准备好台词吧。
(2)关于项目,经常会被问到的点是:某个技术本身的不足,以及如何弥补。因为这样问,能够检验你是否真的做过这个“项目”,至少能说明你是否深入思考过。举例如下:
a.你项目中用到了Mysql :如果数据超过的Mysql的容量怎么处理?(弥补MySQL自身的不足)
b.你做的这个项目是高并发吧?缓存用了吗?在哪些场景 你见过缓存失效?怎么解决?(还是在问你缓存自身的问题如何解决)
c.看你的项目用到了MQ?MQ可以用来解耦合,具体讲讲你项目中到底哪些场景用到了解耦合?(在考你的项目是真的,还是假的)
(3)项目的重难点。
每个项目都有自己的重难点,这些重难点也就是必问点,
举例如下:
a.分布式项目:如何共享数据?什么是CAP原则?分布式锁、分布式事务、分布式缓存怎么实现?
b.高并发项目:几级缓存,如何限流,如何熔断,用docker了没?
(4)真实性:实际的使用场景
a.简历上写的“用到了人脸识别技术” :哪些场景用到了?人脸识别是自己公司写的,还是调用的三方API?自己写的话,用的什么算法?调用API的话,每次调用需要付费多少钱?识别时的光线强度有什么要求?
b.多线程、设计模式、算法:用来处理什么业务?场景?
c.大数据的项目:数据从哪来的?d.项目能否访问?
(5)描述方式:技术列表 + 文字 (如果绘图功底不错,可以加上架构图)
项目周期:半年以上
简历上的项目个数:3个以内(如果是才毕业3年以内,写1-2个就可以了)
4.表达沟通能力
二、加分项
1.高并发/分布式/调优
a.多线程(juc、aqs、线程安全、锁机制、生产消费者、线程依赖问题)
b.数据处理SQL优化 , 常见高性能数据库架构(如mysql+mycat+haproxy+keepalived)
c.JVM调优
2.实际的解决问题能力
这点需要自己在面试时主动将话题引入。
例如在回答项目时,主动说一下你在做项目时遇到过什么问题。具体是如何发现、排查、分析、解决问题的。
3.绝杀
ACM竞赛、蓝桥杯等全国性竞赛(学生专享)
有过书籍、论文等出版物在github发布过项目(star很多)
博客、微信公众号公众号、 个人在阿里云等部署的可访问项目(这一条大部分人都能做到)。如果是电子简历,附上链接地址;如果是纸质简历,将链接封装在二维码里。
研究过JDK/spring/mybatis等源码
三、注意/建议事项
1.在描述时,多使用“数字”:几个项目、几篇博客 、排名第几
2.工资:不要写面议 ,至少给个薪资范围,如1.5w – 2.0w3.简历:1-2页(每一页写满,尽量不要空半页),不要包书皮,
格式使用PDF(不要wps或word,可能出现兼容问题),
外观简洁大方即可,不要太过绚丽
4.细节:毕业时间、年龄、工作履历、期望薪资等要相互匹配。例如,不要“毕业5年”,但“工作履历加起来只有3年”。
5.沟通:注意人文素养 ,不要抱怨问题, 要体现解决问题、愿意承担责任的态度建议:个人解决问题的能力、团队感、沟通能力
2.重载和重写的区别
方法名 | 参数列表 | 返回值 | 访问修饰符 | 抛出异常 | |
---|---|---|---|---|---|
方法重写 | 相同 | 相同 | 相同或是其子类 | 不能比父类更严格 | 不能比父类更宽泛 |
方法重载 | 相同 | 不相同 | 无关 | 无关 | 无关 |
3.常见集合框架的底层数据结构
题外话1:List和Set的上级接口是Collection,但Collection与Map之间不存在继承/实现关系。
题外话2:本文中的是否“唯一”,是指集合中的元素值是否可以重复。
有序/无序,是指输入集合的顺序 是否和 集合的存储顺序一致(或理解为输出顺序),例如,向集合依次输入a、b、c后,如果打印时也输出a、b、c就代表“有序”;如果打印时输出的是a、c、b(或b、a、c等)就代表“无序”。
Collection(不唯一、无序)
List(不唯一、有序)
- Arraylist: 线程不安全,Object数组 ,默认长度是10。
扩容机制:当超过数组容量时,新数组是原来数组的1.5倍。JDK中的源码是:
private void grow(int minCapacity)
{
...
int newCapacity = oldCapacity + (oldCapacity >> 1);
}
- Vector: 线程安全,Object数组,默认长度是10。
扩容机制:当超过数组容量时,新数组是原来数组的2倍。JDK中的源码是:
private void grow(int minCapacity) {
...
int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
capacityIncrement : oldCapacity);
LinkedList: 双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环) 。通过first和last引用分别指向链表的第一个和最后一个元素(元素用Node表示),Node的源码如下所示。
private static class Node<E> {
E item;
Node<E> next;//下一个
Node<E> prev;//上一个
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
Set
HashSet(唯一,无序): 线程不安全,基于HashMap实现的,底层采用 HashMap 来保存元素
LinkedHashSet: LinkedHashSet继承自HashSet,并且其内部是通过 LinkedHashMap来实现的。
TreeSet(有序,唯一): 红黑树
Map(以key-value形式存储数据,通过key取value。key不能重复,value可以重复。)
HashMap:线程不安全,通过ConcurrentHashMap解决。JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体(默认初始容量为16),数组中的每个元素是链表的形式。JDK1.8以后,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。 HashMap的加载因子为0.75:当元素个数 超过 容量长度的0.75倍 时,进行扩容。扩容增量:原容量的2倍.
LinkedHashMap: LinkedHashMap 继承自HashMap。LinkedHashMap在上面结构的基础上,增加了一条双向链表,使得HashMap的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
HashTable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 TreeMap: 红黑树
ConcurrentHashMap:JDK8以前的ConcurrentHashMap间接的实现了Map<K,V>,并将每一个元素称为一个segment(默认16个),每个segment都是一个HashEntry<K,V>数组,数组的每个元素都是一个HashEntry的单向队列。JDK8以后,HashMap/ConcurrentHashMap的存储结构发生了改变:增加了条件性的“红黑树”。为了优化查询,当链表中的元素超过 8 个时,HashMap就会将该链表转换为红黑树,即采用了数组+链表/红黑树的存储结构。
4.Arraylist 与 LinkedList 异同
(1)是否保证线程安全: ArrayList 和 LinkedList 都是线程不安全;
(2)底层数据结构: Arraylist 底层使用的是Object数组;LinkedList 底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环)
(3)插入和删除是否受元素位置的影响:
ArrayList 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。
LinkedList采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
(4)是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而 ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index) 方法)。
(5)内存空间占用: ArrayList的空 间浪费主要体现在在list列表的结尾会预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
5.试述Forward和Redirect的区别
(1)转发(Forword)是服务器行为,重定向(Redirect)是客户端行为
转发可以通过HttpServletRequest对象的getRequestDispatcher()方法链式调用forward()方法实现,如request.getRequestDispatcher().forward()。 重定向是利用服务器返回的状态吗来实现的。客户端浏览器请求服务器的时候,服务器会返回一个状态码。服务器通过HttpServletRequestResponse的setStatus(int status)方法设置状态码。如果服务器返回301或者302,则浏览器会到新的网址重新请求该资源。转发可以通过HttpServletResponse对象的sendRedirect()方法实现。
(2)地址栏显示: 转发是服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来, 然后把这些内容再发给浏览器.浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址. 重定向是服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址.所以地址栏显示的是新的URL.
(3)数据共享: 转发:转发页面和转发到的页面可以共享request里面的数据. 重定向:不能共享数据
(4)实际运用: 转发:一般用于用户登陆的时候,根据角色转发到相应的模块. 重定向:一般用于用户注销登陆时返回主页面和跳转到其它的网站等
(5)效率: 转发:高. 重定向:低.
6.如何设计一个秒杀系统?
什么秒杀: 流量很大,库存少
架构设计原则: 限流、缓存、隔离、降级和熔断
限流(多重):尽量上级限流,负载 ,MQ流量削峰
缓存(多级):请求->redis->DB
隔离:将秒杀的服务器隔离开
降级和熔断:
熔断:客户端 -> service() , beiYong(){return “未响应,请重试”} 降级:服务端 : 20个服务->减少到10个服务
防止重复消费行为:幂等性
zs :支付->支付服务(扣款)->返回(支付成功) 幂等性实现:去重表,在每次支付前,先查看去重表中 是否有扣款记录。如果有,则返回文字提示“已扣款,请稍等响应页面”;如果没有,再执行扣款
7.简述nginx
Nginx是一款轻量级的web服务器(反向代理服务器,负载均衡服务器,动静分离服务器)。
(1)反向代理
a.正向代理(vpn):为客户端做代理,相当于客户端中介。代理客户端去访问服务器。
(客户端->正向代理)->服务器,例如,(用户->VPN)->google,VPN就是用户的正向代理。
b.反向代理(nginx/apache):为服务器做代理,相当于服务端中介,代理服务器接受客户端请求。
客户端-> (反向代理->服务端) ,就如,客户端 -> (nginx-> tomcat),nginx就是tomcat的反向代理服务器。
假设:买房的人是客户端,卖方的人是服务端:
zs想买房,zs找了一个朋友ls帮忙去买,那么ls就是zs的正向代理服务区),(zs->ls)->卖房方
zl有一套房打算卖掉,zl找了一个朋友sq去卖,那么sq就是反向代理服务器,买房方->(sq->zl)
(2)负载均衡:分流客户端请求,均衡集群服务端压力。 Nginx支持的weight轮询(默认)、ip_hash、fair、url_hash四种负载均衡调度算法。
(3)动静分离:分离静态请求和动态请求,将动态请求发送给web服务器,并给静态请求做缓存(或cdn加速)。
客户端请求 -> 静态请求/动态请求
静态请求:静态缓存/cdn加速
动态请求: tomcat(web服务器)
Nginx优点:
(1)高并发连接: 官方测试Nginx能够支撑5万并发连接,实际生产环境中可以支撑2~4万并发连接数。
(2)Nginx为开源软件,成本低廉
(3)稳定性高:用于反向代理时,非常稳定
(4)支持热部署能够在不间断服务的情况下,进行维护。
8.两种以上方法实现“多线程交替打印123123123…”
建立三个线程,第一个线程打印1、第二个线程打印2、第三个线程打印3;要求三个线程交替打印,123123123123……
方法一
package interview;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//多线程交替打印123123123...(两种以上方法实现)
锁方式一: lock.lock();/unlock() : await() signal()
锁方式二: synchronized :wait() notify
public class LoopPrint {
int num = 1;
//只有一个变量,因此只需要一把锁。但需要通知三个线程,因此需要三个“条件通知”
Lock lock = new ReentrantLock();//一把锁
//三个线程 的通知
Condition condition1 = lock.newCondition();//通知线程1打印
Condition condition2 = lock.newCondition();
Condition condition3 = lock.newCondition();
/*
num : 打印的数字1/2/3
线程1: 判断num是否为1,如果不是,则等待;如果是,则打印,打印完后 [通知]线程2
线程2:判断num是否为2,如果不是,则等待;如果是,则打印,打印完后 [通知]线程3
线程3:判断num是否为3,如果不是,则等待;如果是,则打印,打印完后 [通知]线程1
123123123...
*/
public static void main(String[] args) {
LoopPrint print = new LoopPrint() ;
//打印1的线程
new Thread( ()-> {
while(true){
print.print1();
}
} ).start(); //lambda表达式
//打印2的线程
new Thread( ()-> {
while(true){
print.print2();
}
} ).start();; //lambda表达式
//打印3的线程
new Thread( ()-> {
while(true){
print.print3();
}
} ).start();; //lambda表达式
}
//打印“1”
public void print1() {
//a b c -> num
lock.lock();
try {
//线程1: 判断num是否为1,如果不是,则等待;如果是,则打印,打印完后 [通知]线程2
if (num != 1) {
condition1.await();
}
System.out.println(1);
num = 2 ;
//通知线程2
condition2.signal(); // signal()相当于notify() ,await()相当于wait()
}catch (Exception e){
System.out.println(e);
}finally {
lock.unlock();
}
}
//打印“2”
public void print2() {
//a b c -> num
lock.lock();
try {
if (num != 2) {
condition2.await();
}
System.out.println(2);
num =3 ;
//通知线程3
condition3.signal(); // signal()相当于notify() ,await()相当于wait()
}catch (Exception e){
System.out.println(e);
}finally {
lock.unlock();
}
}
public void print3() {
//a b c -> num
lock.lock();
try {
if (num != 3) {
condition3.await();
}
System.out.println(3);
num = 1;
//通知线程1
condition1.signal(); // signal()相当于notify() ,await()相当于wait()
}catch (Exception e){
System.out.println(e);
}finally {
lock.unlock();
}
}
}
方法二
package interview;
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
//1 2 3
//三个信号量
Semaphore sem1 = new Semaphore(1);
Semaphore sem2 = new Semaphore(0);
Semaphore sem3 = new Semaphore(0);
public static void main(String[] args) {
SemaphoreDemo sem = new SemaphoreDemo();
new Thread( ()-> sem.print1() ).start();
new Thread( ()-> sem.print2() ).start();
new Thread( ()-> sem.print3() ).start();
}
public void print1(){
print("1",sem1,sem2 ) ;
}
public void print2(){
print("2",sem2,sem3 ) ;
}
public void print3(){
print("3",sem3,sem1 ) ;
}
/*
value:当前值 1 2 3?
current:当前谁用于许可证
next:下一个谁那许可证
*/
private void print(String value , Semaphore current,Semaphore next ){
while(true){
try {
current.acquire();//当前信号量 获取许可证
System.out.println( Thread.currentThread().getName()+"***"+ value);
Thread.sleep(1000);
next.release();//将许可证传递给下一个
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
9. 一个”.java”源文件中是否可以包括多个类?
可以有多个类,但只能有一个public的类,并且public的类名必须与文件名相一致。此外,类中还可以包含内部类。
10.一个类的声明能否 既是abstract,又是final?如下所示。
abstract final class A
{
...
}
不能。原因如下: abstract :抽象类。抽象类不能实例化(new),因此我们只能使用其子类,即会使用到“继承”,如下。
B extends A{
}
B b = new B();
而final修饰的类,不能被继承,如下。
final class A {…}。 综上,在能否“继承”方面,abstract和final是矛盾的语义,所以不能同时使用。
11. 如何给final修饰成员变量的初始化赋值?
这个问题有两种情况。
情况一:没有static
只有final,没有static时,既可以通过=直接赋值,也可以通过构造方法赋值,如下。
a.通过=直接赋值
final int num = 10 ;
b.通过构造方法给final赋初值(注意:所有的构造方法 都必须给final变量赋值)
public class FinalDemo01 {
final int num ;//final修饰的变量,没有默认值
public FinalDemo(){
this.num = 10 ;
}
public FinalDemo(int num ){
this.num = num ;
}
//构造方法的作用? 实例化对象new(任意一个构造方法都可以实现)
public static void main(String[] args) {
// FinalDemo demo = new FinalDemo();
// System.out.println(demo.num);
new FinalDemo(10);
}
}
情况二:有static static final int num = 10 ;
既有final,又有static时,只能通过=初始化值,不能同构方法赋值。 总体是因为static变量是在构造方法之前执行的,具体的原因见以下三点:
(1)
static修饰的变量,执行时机:类加载过程中执行,并且会被初始化为 默认值0 null .. class A{
static int a = 10 ; //a ->0 -> 10
}
(2)
final修饰的变量,执行时机:运行时被初始化(直接赋值,也可以通过构造方法赋值)
(3)
static final修饰的变量,执行时机: 在javac时(编译)生成ConstantValue属性,在类加载的过程中根据ConstantValue属性值为该字段赋值。ConstantValue属性值 没有默认值,必须显示的通过=赋值。
12. 为什么对于一个public及final修饰的变量,一般建议声明为static?
public class A{
public final static int num = 10 ;
}
如果没有static修饰:假设在10个类中要使用num变量,就必须在这10个类中先实例化A的对象,然后通过“对象.num”来使用num变量,因此至少需要new10次。
加上static可以节约内存。static是共享变量,因此只会在内存中 拥有该变量的一份存储,之后就可以直接通过“类名.变量” 使用(例如 “A.num”),而不用先new后使用。 由于final修饰的变量不可改变,因此不用考虑并发问题。
13. InterfaceA是接口, InterfaceA []a = new InterfaceA[2];是否正确?
正确。
考点是对“对象数组”的理解。 {x,x,x,x,x,x},在对象数组中 实际存放的不是对象本身,而放的是对象引用的地址。
14. 异常分类
以下程序,能否成功运行?
public class ExceptionDemo01 {
void test() throws NullPointerException{
System.out.println("A...");
}
public static void main(String[] args) {
new ExceptionDemo01().test();
}
}
解答:
异常:运行异常和检查异常
运行异常:RuntimeException,运行时才会发现的异常(编译时不会进行检查)。
检查异常:CheckedException(泛指 除了RuntimeException以外的其他所有异常) :编译时会进行检查。
- 如果是CheckedException,在编写代码时,必须try..catch或者throws二选一。
- 但是RuntimeException在编写时不会进行异常检查,因此本题目中抛出的NullPointerException在编译阶段不会被检查。而在运行时,本题中并不会造成NullPointerException,因此无需任何处理,也不会报错。
简言之,本题目在编译阶段不会进行异常检查(本题是RuntimeException),而在运行时又不会发生异常,因此是正确的,会正常运行。
15. ==和equals()的区别?
==比较运算符
equals()最初是在Object中定义的一个方法。
Object中定义的equals()就是== ,只不过 一般来说 其子类都会重写equals()方法 将其重写为 比较“内容”是否相等,例如String.
==比较的对象的引用地址(内存地址),而一般情况下equals()比较的是内容相等。
==还可以比较基本的数据类型。
16. 举例说明,如何重写equals()方法?
如果person对象的name和age相同,则返回true;否则返回false。
public class Person {
private String name;
private int age ;
public Person() {
}
public Person(int age, String name) {
this.age = age;
this.name = name;
}
...
//约定: 如果name和age相同,则返回true
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
//name age
if(obj instanceof Person){
Person per = (Person)obj ;
//用传入的per,和当前对象this比较
if(this.name.equals(per.getName()) && this.age==per.getAge() ){
return true ;
}
}
return false;
}
}
17. 重写equals方法为什么要重写hashcode方法?
两个对象相等(引用地址),则hashcode一定相同
如果两个对象的hashcod相同,这俩对象不一定相同。
两个对象不等(引用地址),hashcode不一定不等
如果两个对象的hashcod不同,这俩对象一定不同。
18. 完整的总结hashcode和equals()
判断元素内容是否相等:
1.根据hashcode 快速定位 (提高效率,避免了在大量集合中 由于遍历带来的效率问题)
2.根据equals判断内容是否相同 (判断 正确性)
如果要判断元素的内容 是否相同,就要重写hashcode和equals()
19. HashSet和HashMap之间的关系
public HashSet() {
map = new HashMap<>();
}
可以发现,hashset底层是由hashmap实现的
hashmap.put(key,value );
hashset.add(key,常量值); 所有hashset中增加的Value 都是一个常量值 PRESENT (new Object())
对于hashset: 在增加元素时,如果集合中不存在,则返回true ;如果已经存在则返回false。
对于hashmap: 在增加元素时,如果key已经存在,则将该key对应的value值覆盖掉
set.add(a) ->true
set.add(a) ->false
map集合:a,c
map.put(a,b)
map.put (a,c)
map.put(key,value)返回值:是在本次增加元素之前,key对应的value值、
set.put()返回值:是之前是否已经存在。如果不存在:返回true;如果存在,返回false
二者差异的根本原因,是因为二者的研究对象不一致:map.put 第二次 会覆盖掉一次 相同key对应的“value”值; set.add()第二次 会直接忽略掉,忽略的是key.
20. String 和 StringBuffer、StringBuilder 的区别是什么?String 为什么是不可变的
可变性
常见错误回答1:
String类是final修饰的,因此String中的字符串不可变。实际上,final修饰的类,只能保证该类不能被继承,与字符串是否改变没有关系;
常见错误回答2:
在String源码中,定义存储字符串的源码是private final char value[] , 由于value[]是final修饰的,因此因此String中的字符串不可变。实际上,此时final修饰的是引用类型,只能保证引用的对象地址不能改变,但对象的内容(即字符串的内容)是可以改变的。
正确回答:
String类的不可变性实际在于作者的精心设计。例如,如果让你设计一个getXxx(String name)方法,你既可以设计成以下形式:
String getXxx(String name){
return name ;
}
也可以设计成以下形式:
String getXxx(String name){
return new Other(name) ;
}
如果设计成形式一,那么取到的值就是输入值本身;如果设计成形式二,取到的值就是一个新对象。简言之,如何设计一个方法的返回值,归根节点还是“看作者心情”。String不可变的原因也是一样的,是由于编写String的作者精心设计,所以导致了String类的不可变性。
如果要刨根问底,研究“作者的心情,究竟是如何的?”,即到底是如何设计成String类的不可变性的,也可以参阅《Effective Java》中 “使可变性最小化” 中对 “设计不可变类” 的解释,具体如下:
不可变类只是其实例不能被修改的类。每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。为了使类不可变,要遵循下面五条规则:
- 不要提供任何会修改对象状态的方法。
- 保证类不会被扩展。 一般的做法是让这个类称为 final 的,防止子类化,破坏该类的不可变行为。
- 使所有的域都是 final 的。
- 使所有的域都成为私有的。 防止客户端获得访问被域引用的可变对象的权限,并防止客户端直接修改这些对象。
- 确保对于任何可变性组件的互斥访问。 如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。
在 Java 平台类库中,包含许多不可变类,例如 String , 基本类型的包装类,BigInteger, BigDecimal 等等。综上所述,不可变类具有一些显著的通用特征:类本身是 final 修饰的;所有的域几乎都是私有 final 的;不会对外暴露可以修改对象属性的方法。通过查阅 String 的源码,可以清晰的看到这些特征。
此外,还要注意以下两点:
- StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类。
- String覆盖了equals方法和hashCode方法,而StringBuffer/StringBuilder没有覆盖equals方法和hashCode方法,所以,将StringBuffer/StringBuilder对象存储进Java集合类中时会出现问题。
21.说说&和&&的区别
&和&&都可以用作逻辑与的运算符,表示逻辑与(and),当运算符两边的表达式的结果都为true时,整个运算结果才为true,否则,只要有一方为false,则结果为false。
&&还具有短路的功能,即如果第一个表达式为false,则不再计算第二个表达式,例如,对于if(str != null && !str.equals(“”))表达式,当str为null时,后面的表达式不会执行,所以不会出现NullPointerException如果将&&改为&,则会抛出NullPointerException异常。If(x==33 & ++y>0) y会增长,If(x==33 && ++y>0)不会增长
&还可以用作位运算符,当&操作符两边的表达式不是boolean类型时,&表示按位与操作,我们通常使用0x0f来与一个整数进行&运算,来获取该整数的最低4个bit位,例如,0x31 & 0x0f的结果为0x01。
|与||同理。
22.试述TCP 三次握手和四次挥手
原因:任何数据传输都无法保证绝对的可靠性
三次:建立连接
四次:关闭连接 (多了一次:服务端在收到客户端的关闭请求后,不能立刻也关闭还需要处理 最后一次请求的数据,请全部数据处理完毕之后,才能再向客户端发出 关闭请求)
23.内部类
匿名内部类:隐藏的继承了一个类(可以是普通类、抽象类,不能是被final修饰的类),或者实现了一个接口
24.泛型
List objs = new ArrayList (); 对
List objs = new ArrayList ();错
List<?> objs = new ArrayList ();对
List<? extends Object> objs = new ArrayList ();对
List<? extends String> objs = new ArrayList ();对,在考虑泛型时,不用考虑 final
25.包装类
int | Integer |
byte | Byte |
short | Short |
long | Long |
float | Float |
double | Double |
boolean | Boolean |
char | Character |
自动装箱和拆箱:
Integer i = 10 ; //自动装箱 : 包装类 = 基本类型
int j = i ;//自动拆箱 : 基本类型 = 包装类
(Integer + int) -> int类型
当包装类 遇到基本类型时,会自动转为基本类型
题目:
public static void demo01() {
Integer f1 = 100, f2 = 100, f3 = 200, f4 = 200;
System.out.println(f1 == f2);
System.out.println(f3 == f4);
}
true false
以下是Integer类中“自动装箱”的源码:
public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i);}
其中IntegerCache.low的是值是-128,IntegerCache.high的值是127。也就是说,Integer在自动装箱时,如果判断整数值的范围在[-128,127]之间,则直接使用整型常量池中的值;如果不在此范围,则会new 一个新的Integer()。因此,本题f1和f2都在[-128,127]范围内,使用的是常量池中同一个值。而f3和f4不在[-128,127]范围内,二者的值都是new出来的,因此f3和f4不是同一个对象。
题目:
private static Integer i;
public static void demo02() {
if (i == 0) {
System.out.println("A");
} else {
System.out.println("B");
}
}
答案:NullPointerException
Integer i 的默认值是null。当执行 i==0 时,等号右侧是数字,因此为了进行比较操作,Integer会进行自动拆箱(也就是将Integer转为int类型)。很明显,如果对null进行拆箱(将null转为数字),就会报NullPointerException。
26.默认值
整数的默认类型是int,小数的默认类型是double。但是在赋值时(在数值范围内),“=”可以自动转为相应的整数类型(byte/short/int/long);但却不会转为小数类型,如下所示。
byte num = 10 ;//正确,10会自动转为byte类型
float f = 3.14; //错误,3.14是double类型,不会自动转为float
答:不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F;。
27.JVM类加载问题
package com.yanqun.pojo;
class MyClass{
static int num1 = 100 ;
static MyClass myClass = new MyClass();
public MyClass(){
num1 = 200 ;
num2 = 200 ;
}
static int num2 = 100 ;
public static MyClass getMyClass(){
return myClass ;
}
@Override
public String toString() {
return this.num1 + "\t" + this.num2 ;
}
}
public class MyClassLoader {
public static void main(String[] args) {
MyClass myc = MyClass.getMyClass() ;
System.out.println(myc);
}
}
运行结果:200 100
解析:
JVM使用“类”的生命周期是:
类的加载->连接->初始化->使用->卸载
各阶段主要完成的工作如下:
1.类的加载:
(1)寻找并加载类的二进制数据(class文件)
(2)将硬盘上的class文件 加载到jvm内存中
2.连接
该阶段又包含了验证、准备和解析3个过程,如下。
(1)验证
校验.class文件的正确性
(2)准备
给static静态变量分配内存,并初始化static的默认值。
因此,本题在此阶段各变量的值如下:
static int num1 = 0 ;
static MyClass myClass = null ;
static int num2 = 0 ;
(3)解析:把类中符号引用,转为直接引用
举个例子,在加载阶段,JVM还不知道类的具体内存地址,只能使用“com.yanqun.pojo.MyClass ”来替代MyClass类,“com.yanqun.pojo.MyClass ”就称为符号引用;但在解析阶段,JVM就可以将 “com.yanqun.pojo.MyClass ”映射成实际的内存地址,因此就可以用 内存地址来代替MyClass,这种使用 内存地址来使用 类的方法 称为直接引用。
3.初始化:给static变量 赋予实际的值
因此,本题在此阶段各变量的值如下:
static int num1 = 100 ;
static MyClass myClass = new MyClass();此句调用了构造方法,构造方法会进行如下赋值:
public MyClass(){
num1 = 200 ;
num2 = 200 ;
}
static int num2 = 100 ;
根据程序 自上而下执行的特点,num1最终的值是200,num2最终的值是100。
4.使用:对象的初始化、对象的垃圾回收、对象的销毁
5.卸载
28.答疑 | synchronized有指令重排序的功能吗?
1.问:volatile可以禁止指令的重排序功能。那么synchronized有这个功能吗?我百度、谷歌都查不到准确的说法。
答:百度、谷歌都查不到,很大程度说明这个问题没有意义。
重排序是指JVM为了提高执行效率,会对我们编写的代码进行一些额外的优化。
敲重点:重排序所实现的优化不会影响单线程程序执行结果
1. int a = 100 ;
2. int b ;
3. b = 200 ;
4. int c = a * b ;
根据重排序,以上代码的实际执行顺序可以是1、2、3、4,也可以是2、3、1、4,还可以是2、1、3、4等,因为这几种可能的最终执行结果都是相同的。(实际上第4句还可以再拆)
而synchronized的作用是加锁,可以保证串行执行,即可以让并发环境 转为单线程环境。因此加了synchronized就已经是单线程环境了。既然是单线程,那么无论是否进行了重排序,最终的结果都不会有影响,即都可以保证线程安全。所以说,在使用synchronized时根本不用关心“重排序”这个问题,无论它支持或不支持,都已经不重要了。
29.答疑
之前有听说:Java 中String定义的变量值不可改变,例如String str=”a”,str=”b”,则表示 第一次str指向”a”,第二次str指向”b”。但源码里String是final修饰的,str的“指向”应该不能变的吧?
答:String是final修饰的,说明String这个“类”是final的,这一点只能说明String不能被继承(概念:final修饰的类不能被继承);而str指向什么,跟“final 类”没有任何关系,所以你把二者搞混了。
30.答疑
问:“如果一个对象存在着指向它的引用,那么这个对象就不会被GC回收”,这句话对吗?
不对。JVM中存在着四种类型的引用:强引用、软引用、弱引用和虚引用。
你这句话只适用于“强引用”,Object ref = new Object()中的ref就是一个强引用。但除此以外,还有以下三个:
软引用:当JVM的内存足够时,GC不会主动回收软引用对象;但当JVM的内存不足时,GC就会去主动回收软引用对象。
弱引用:当GC进行垃圾回收时,无论是否当时JVM的内存是否充足,都会去主动回收弱引用对象。
虚引用:是否使用虚引用对于一个对象本身来说都没有任何区别。虚引用的价值在于和引用队列一起使用。
综上,软引用、弱引用和虚引用都是你那句话的反例。