当前位置: 首页 >> 面试题 >> 技术 >> 后端开发 >> Java >>

2020年春招JAVA基础面试题库

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();
    }
}

解答:

1565339819579

异常:运行异常和检查异常

运行异常: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 的源码,可以清晰的看到这些特征。

​ 此外,还要注意以下两点:

  1. StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类。
  2. 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的内存是否充足,都会去主动回收弱引用对象。

虚引用:是否使用虚引用对于一个对象本身来说都没有任何区别。虚引用的价值在于和引用队列一起使用。

综上,软引用、弱引用和虚引用都是你那句话的反例。

Loading