JAVA面试题整理更新

  1. Map的底层结构?(HashMap)
  • 必须熟记
  • Map是以键值对存储对象,底层实际上是由数组和链表组成,如下图中所示:


    map结构
  • 当使用put方法时,先查找出数组位置是否存在对象,通过key.hashcode对数组长度取余,存在,则把里面的链表拿出来,然后判断链表里面是否存在key值一样的对象,存在,则把传递过来的value取代链表key对应的value,不存在,则直接通过链表的add()方法加到链表后面。
  • 当使用get方法时,先查找出数组位置是否存在对象,通过key.hashcode对数组长度取余;如果不存在,则返回为空,如果存在,则遍历链表,判断链表里面是否存在key值与传递过来的key值一样的对象,存在,则把key值对应的value取出返回,不存在,则返回为空;
  1. 线程安全的Map
  • 老题目,需要了解解决HashMap进程安全的各种方法以及原理。
  • jdk1.7中采用Segment+HashEntry的方式进行实现,Segment数组的意义就是将一个大的table分割成多个小的table来进行加锁,而每一个Segment元素存储的是HashEntry数组+链表,这个和HashMap的数据存储结构一样,而jdk1.8中取出Segment+HashEntry+Unsafe的实现,改为Synchronized+CAS + Nodew + Unsafe的实现,结构如下所示:


    1.7HashMap
  • jdk1.8实现结构图如下中所示:


    jdk1.8实现
  • 如上图中所示,jdk1.8中取消了Segment字段,数组中存储的是Node。它与HashMap中的HashEntry定义很相似,但是有一些差别。它对value和next属性设置了volatile同步锁,它不允许调用setValue方法直接改变Node的value域。此外,将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构,在hash碰撞过多的情况下会将链表转化为红黑树。
  1. 数据库的索引实现和非主键的二级索引
  • 从数据结构的角度:B-Tree索引,数据结构就是一颗B+树。Hash索引,Hash索引比较的是进行Hash运算之后的Hash值,所以它只能用于等值的过滤,不能用于基于范围的过滤。基本不用。R-Tree索引,仅支持geometry数据类型,也基本不用!
  • 非主键的二级索引,这个实际上问的就是非聚簇索引!非聚簇索引本身就是一颗B+树,其根节点指向聚簇索引的B+树,具体的请看<MySQL索引的原理>
  1. 项目用的是SpringBoot, Spring Boot和Spring的区别
  • Spring Boot可以建立独立的Spring应用程序;
  • 内嵌了如Tomcat,Jetty和Undertow这样的容器,也就是说可以直接跑起来,用不着再做部署工作了。
  • 无需再像Spring那样搞一堆繁琐的xml文件的配置;
  • 可以自动配置Spring;
  • 提供了一些现有的功能,如量度工具,表单数据验证以及一些外部配置这样的一些第三方功能;
  • 提供的POM可以简化Maven的配置
  1. SprintBoot的自动配置是怎么做的
  • 为什么要自动配置:自动配置的意义是利用这种模式代替了配置 XML 繁琐模式。以前使用 Spring MVC ,需要进行配置组件扫描、调度器、视图解析器等,使用 Spring Boot 自动配置后,只需要添加 MVC 组件即可自动配置所需要的 Bean。所有自动配置的实现都在 spring-boot-autoconfigure 依赖中,包括 Spring MVC 、Data 和其它框架的自动配置。
  • spring-boot-autoconfigure依赖的工作原理:spring-boot-autoconfigure 依赖的工作原理很简单,通过 @EnableAutoConfiguration 核心注解初始化,并扫描 ClassPath 目录中自动配置类对应依赖。比如工程中有木有添加 Thymeleaf 的 Starter 组件依赖。如果有,就按按一定规则获取默认配置并自动初始化所需要的 Bean
  1. MyBatis定义的接口,怎么找到实现的
  • Mapper 接口在初始SqlSessionFactory 注册的。
  • Mapper 接口注册在了名为 MapperRegistry 类的 HashMap中, key = Mapper class value = 创建当前Mapper的工厂。
  • Mapper 注册之后,可以从SqlSession中get
  • SqlSession.getMapper 运用了 JDK动态代理,产生了目标Mapper接口的代理对象。
  • 动态代理的 代理类是 MapperProxy ,这里边最终完成了增删改查方法的调用。
  1. Java内存结构
  • JVM内存结构主要有三大块:堆内存、方法区和栈。堆内存是JVM中最大的一块由年轻代和老年代组成,而年轻代内存又被分成三部分,Eden空间、From Survivor空间、To Survivor空间,默认情况下年轻代按照8:1:1的比例来分配;
  • 方法区存储类信息、常量、静态变量等数据,是线程共享的区域,为与Java堆区分,方法区还有一个别名Non-Heap(非堆);栈又分为java虚拟机栈和本地方法栈主要用于方法的执行。
  1. 对象是否可GC
  • 这个问题就是在问,JVM如何判断对象是否需要被回收!不用答引用计数法,答可达性分析算法就行。
  • 这个算法的基本思路是通过一些列称为“GC Roots”的对象作为起始点,从这些点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明对象需要被回收.


    GC回收图
  • 上图中o3,o4对象没有任何GC Roots可达到,所有这两个对象不可用了,需要被GC回收
    Java可作为GC Roots的对象包括下面几种:
    • 虚拟机栈中引用的对象
    • 方法区中类静态属性引用的对象
    • 方法区中产量引用的对象
    • 本地方法栈中JNI引用的对象
  1. Minor GC和Full GC
  • 堆内存是JVM中最大的一块由年轻代和老年代组成。
    那么,从年轻代空间(包括 Eden 和 Survivor 区域)回收内存被称为 Minor GC。Major GC 是清理老年代。Full GC 是清理整个堆空间—包括年轻代和老年代
  1. 垃圾回收算法
  • 标记-清除算法、标记整理算法、复制算法、分代收集算法
  1. 垃圾回收器G1
  • G1 GC是Jdk7的新特性之一、Jdk7+版本都可以自主配置G1作为JVM GC选项。 G1 将整个堆划分为一个个大小相等的小块(每一块称为一个region),每一块的内存是连续的,每个块也会充当 Eden、Survivor、Old三种角色,但是它们不是固定的,这使得内存使用更加地灵活。如下图所示:


    G1
  • 执行垃圾收集时,收集线程在标记阶段和应用程序线程并发执行,标记结束后,G1 也就知道哪些区块基本上是垃圾,存活对象极少,G1 会先从这些区块下手,因为从这些区块能很快释放得到很大的可用空间,这也是为什么 G1 被取名为 Garbage-First 的原因
  1. 描述下网页一个 Http 请求,到后端的整个请求过程
  • 利用DNS进行域名解析 --> 发起TCP的3次握手 --> 建立TCP连接后发起http请求 --> 服务器响应http请求,浏览器得到html代码 --> 浏览器解析html代码,并请求html代码中的资源(如js、css、图片等) --> 浏览器对页面进行渲染呈现给用户
  1. 多线程的常用方法和接口类及线程池的机制
  • 常用方法:start,run,sleep,wait,notify,notifyAll,join,isAlive,currentThread,interrupt
  • 常用接口:Runnable、Callable、Future、FutureTask
  • 线程池的机制:在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,所以出现了池化技术!。
  • 简单的线程池包括如下四个组成部分即可:
    • 线程池管理器(ThreadPoolManager):用于创建并管理线程池
    • 工作线程(WorkThread): 线程池中线程
    • 任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行
    • 任务队列:用于存放没有处理的任务。提供一种缓冲机制
  1. 死锁(牛客网)
  • 死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,如果系统资源充足,进程的资源请求都能够得到满足,死锁出现的可能性就很低,否则就会因争夺有限的资源而陷入死锁
  • 产生原因: i) 因为系统资源不足; ii) 进程运行推进的顺序不合适 iii) 资源分配不当等
  1. DNS用的UDP协议,属于应用层(区域传输用TCP)
  2. 关于equals和==
  • 在基本数据类型比较的时候==比较的是值(boolean/float/double/byte/short/int/long/char),对于非基本数据类型的变量,比较的是关于对象在内存中的地址。
  • equals方法是基类Object中的方法,源码中指示的是equals方法比较两个对象的引用是否相等,即是否来自于同一个对象。String/Double/Date/Integer等对equals方法进行了重写比较指向的对象所存储的内容,所以用来判断对象内容是否相等。
  1. 关于HashMap的数组长度一定是2的次幂
  • resize源代码如下:
void resize(int newCapacity) {
        Entry[] oldTable = table;
        int oldCapacity = oldTable.length;
        if (oldCapacity == MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return;
        }

        Entry[] newTable = new Entry[newCapacity];
        transfer(newTable, initHashSeedAsNeeded(newCapacity));
        table = newTable;
        threshold = (int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1);
    }
  • 当数组进行扩容的时候,数组长度发生变化,存储位置index=h&(length-1),index也可能会发生变化,需要重新计算index,再重新计算过程中,因为要用到length-1而重新计算的话只需要左边一位表示进行调换,减少之前已经散列良好的老数组的数据位置重新调换
  1. 重写equals方法需要同时重写hashCode方法
  • 测试案例如下:
public class MyTest {
    private static class Person{
        int idCard;
        String name;

        public Person(int idCard, String name) {
            this.idCard = idCard;
            this.name = name;
        }
        @Override
        public boolean equals(Object o) {
            if (this == o) {
                return true;
            }
            if (o == null || getClass() != o.getClass()){
                return false;
            }
            Person person = (Person) o;
            //两个对象是否等值,通过idCard来确定
            return this.idCard == person.idCard;
        }

    }
    public static void main(String []args){
        HashMap<Person,String> map = new HashMap<Person, String>();
        Person person = new Person(1234,"乔峰");
        //put到hashmap中去
        map.put(person,"天龙八部");
        //get取出,从逻辑上讲应该能输出“天龙八部”
        System.out.println("结果:"+map.get(new Person(1234,"萧峰")));
    }
}
  • 如果我们已经对HashMap的原理有了一定了解,这个结果就不难理解了。尽管我们在进行get和put操作的时候,使用的key从逻辑上讲是等值的(通过equals比较是相等的),但由于没有重写hashCode方法,所以put操作时,key(hashcode1)-->hash-->indexFor-->最终索引位置 ,而通过key取出value的时候 key(hashcode1)-->hash-->indexFor-->最终索引位置,由于hashcode1不等于hashcode2,导致没有定位到一个数组位置而返回逻辑上错误的值null(也有可能碰巧定位到一个数组位置,但是也会判断其entry的hash值是否相等,上面get方法中有提到。)

  • 所以,在重写equals的方法的时候,必须注意重写hashCode方法,同时还要保证通过equals判断相等的两个对象,调用hashCode方法要返回同样的整数值。而如果equals判断不相等的两个对象,其hashCode可以相同(只不过会发生哈希冲突,应尽量避免)

  1. 散列表
  • 拉链法:数组+链表(HashMap实现方式)
  • 线性探测法:首先取散列值,如果没有占用就命中,否则就加1探测是否空闲,直到找到一个位置实现,查找的过程就是与这个类似的查找,也是先计算散列值找到对应节点,如果和查找的key符合则返回,否则加加1进行查找直到找到最终结果
  1. concurrentHashMap实现
  2. 并发编程三大特性
  • 原子性:要么做要么不做
  • 可见性:多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
  • 有序性:代码执行是从前往后,但是JVM执行过程中,处理器为了提高程序运行效率,可能对输入代码进行优化,不能保证各个语句的执行先后顺序通代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。重排序过程中会考虑指令之间的数据依赖性,如果一个指令2必须用到指令1的结果,那么处理器会保证指令1在指令2之前执行。在单线程过程中这个重排是没有问题的,但是在多线程中会出现问题,一个多线程程序如下所示:
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);
  • 在上述过程中就有可能先执行语句2,根据线程的原子性,那么就会执行while循环及后面,但是此时context并没有初始化就会报错
  • JAVA提供volatile修饰时,可以保证被修改的值立即更新到主存中,会去内存中读取新值
  • volatile也可以保证一定的有序性,和synchronized、lock类似
  1. Java的线程实现方式:内核线程实现、用户线程实现、用户线程加轻量级进程实现(JVM);
  • Thread实现:
public class Main {
    public static void main(String[] args) {
        MyThread T1 = new MyThread("A");
        MyThread T2 = new MyThread("B");
        T1.start();
        T2.start();
    }
}

class MyThread extends Thread {
    private String name;
    public MyThread(String name) {
        this.name = name;
    }
    
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(name + ":" + i);
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
  • Runnable实现
public class Main {

    public static void main(String[] args) {

        //测试Runnable
        MyThread1 t1 = new MyThread1();
        new Thread(t1).start();//同一个t1,如果在Thread中就不行,会报错
        new Thread(t1).start();
        new Thread(t1).start();

    }

}
class MyThread1 implements Runnable{
    private int ticket = 10;
    @Override
    //记得要资源公共,要在run方法之前加上synchronized关键字,要不然会出现抢资源的情况
    public synchronized  void  run() {
        for (int i = 0; i <10; i++) {
            if (this.ticket>0) {
                System.out.println("卖票:ticket"+this.ticket--);
            }
        }
        
    }
    
}
  1. Java Theread和Runnable的区别
  • 区别:Runnable适合多个相同的程序代码的线程去处理同一个资源,可以避免Java单继承限制,增加程序健壮性,代码可以被多个线程共享,代码和数据独立
  1. MTV模式和MVC模式
  2. Java类加载过程(包含静态代码块的执行):
  • 一个Java文件从编码完成到最终执行,一般主要包括编译和执行两个过程,编译是指把写好的java文件通过javac命令编译成字节码,也就是我们常说的.class文件,运行则是把编译得到的.class文件交给Java虚拟机(JVM)执行
  • 首先Java的类从加载开始,到卸载为止,整个生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个阶段。
  • 类加载包括加载、验证、准备、解析、初始化五个过程(验证、准备、解析可以合为一个大类链接)
  • 加载:把class字节码文件从各个来源通过类加载器装载到内存中。字节码来源包括从本地路径下编译生成的.class文件,从jar包中的.class文件,从远程网络、以及动态代理实时编译。类加载器:一般包括启动类加载器,扩展类加载器,应用类加载器,以及用户的自定义类加载器。
  • 验证:保证加载进来的字节流符合虚拟机规范,不会造成安全错误。包括对于文件格式的验证,比如常量中是否有不被支持的常量,文件中是否有不规范的或者附加的其他信息。对于元数据的验证,比如该类是否继承了呗final修饰的类?类中的字段、方法是否与父类冲突?是否出现了不合理的重载?对于字节码的验证,保证程序语义的合理性,比如要保证类型转换的合理性。对于符号引用的论证,比如校验符号引用中通过全限定名是否能够找到对应的类?校验符号引用中的访问性是否可被当前类访问?
  • 准备:主要是为类变量分配内存,并且赋予初值。初值,不是代码中具体写的初始化的值,而是Java虚拟机根据不同变量类型的默认初始值。
  • 解析:将常量池内的符号引用替换为直接引用的过程。符号引用指一个字符串,但是这个字符串能给出了一些能够唯一识别一个方法,一个变量,一个类的相关信息。直接饮用理解为一个内存地址,或者一个偏移量,比如类方法,类变量的直接饮用是指向方法区的指针;而实例方法实例变量的直接引用则是从实例的头指针开始算起到这个实例变量位置的偏移量。
  • 初始化:这个阶段主要是对类变量的初始化,是执行类构造器的过程。换句话说,只对static修饰的变量或语句进行初始化,如果初始化一个类的时候,其父类尚未初始化,则优先初始化其父类,如果同时包含多个静态变量和静态代码块,则按照自上而下的顺序依次执行。
  1. Collection下有哪些、
  • Collection是集合的顶层接口
  • 集合的特点:用于存储对象的容器、集合的长度是可变的、集合是不可以存储基本数据类型的。
  • 两大体系:List和Set。List特点是元素有序,元素可重复,元素有索引。Set特点是元素无序,元素不可以重复。
  1. 进程与线程的区别于联系
  • 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。
  • 线程是进程的一个实体,是CPU调度和分派的基本单位它是比进程更小的能独立运行的基本单位。
  • 关系:
    • 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程,线程是操作系统可识别的最小执行和调度单位。
    • 资源分配给进程,同一进程的所有线程共享该进程的所有资源。同一进程中的多个线程共享代码段(代码和常量),数据段(全局变量和静态变量),扩展段(堆存储)。但是每个线程拥有自己的栈段,栈段又叫运行时段,用来存放所有局部变量和临时变量。
    • 处理机分给线程,即真正在处理机运行的是线程
    • 线程在执行过程中,需要协作同步,不同进程的线程间要利用消息通信的办法实现同步
    • 进程有自己的独立地址空间,线程没有
    • 进程是资源分配的最小单位,线程是CPU调度的最小单位
    • 进程和线程通信方式不同,同一进程下的线程共享数据,通过这些数据来通信不仅快捷且方便,当然如何处理好这些访问的同步与互斥是编写多线程程序的难点。而进程之间的通信只能通过进程通信的方式进行。
    • 进程上下文切换开销大,线程开销小
    • 一个进程挂掉了不会影响其他进程,而一个线程挂掉了会影响其他线程
    • 对进程操作一般开销都比较大,对线程开销就小了
  1. Java中public/protected/default/private区别:
  • public:具有最大的访问权限,可以访问任何一个在classpath下的类、接口、异常等。它往往用于对外的情况,也就是对象或类对外的一种接口的形式
  • protected:主要的作用就是用来保护子类。它的含义在于子类可以用它修饰的成员,其他的不可以,它相当于传递给子类的一种继承的东西
  • default:有时候也称为friendly,它是针对本包访问而设计的,任何处于本包下的类、接口、异常等,都可以相互访问,即使是父类没有用protected修饰的成员也可以
  • private:访问权限仅限于类的内部,是一种封装的体现,例如,大多数成员变量都是修饰符为private,它们不希望被其他任何外部的类访问。
  • JAVA访问控制符的含义和使用情况:
属性 类内部 本包 子类 外部包
public o o o o
protected o o o x
default o o x x
private o x x x
  1. http协议是属于无连接,在两个http进行交换的过程中是不需要进行建立连接的,而这个过程是通过TCP控制的。无状态表示的是同一个客户第二次访问同一个服务器上的页面时,服务器的响应与第一次被访问时的相同,因为服务器并不记得曾经访问过这个客户,也不记得为这个客户曾经服务过多少次。HTTP无状态设计特性简化了服务器的设计,使服务器更容易支持大量并发的http请求。
  2. 关于协议的有状态和无状态的理解:协议的状态是指下一次传输数据的时候能考虑到这次传输信息的状态,也就是说有状态协议以前的请求导致协议所处的状态会影响后续的状态,协议会根据上一个状态创建上下文。http协议就是属于无状态协议不需要考虑上下文,每个请求都是与服务器的独立连接,即下一次请求到来的时候协议不用维护这次请求中客户机-服务器间交互的信息。
  3. 分布式网络特点:可靠性高,网内节点共享资源容易,可改善线路的信息流量分配,可选择最佳路径,传输延时小,控制复杂,软件复杂,线路费用高,不易扩充。(分布式是因为实验室网络相关所以才找的,一般面试不问)
  4. 分布式操作系统的特性:分布式系统是一个硬件或者软件分布在不同的网络计算机上,彼此之间仅仅通过消息传递进行通信和协调的系统。分布式操作系统具有以下几个特性:
    • 副本:数据副本指不同节点上持久同一份数据,档期一个节点上存储的数据丢失时,可以从副本上读取到该数据,这是解决分布式系统数据丢失问题的有效手段。服务副本指多个节点提供同样的服务,每个节点都有能力接受来自外部的请求并进行相应的处理。
    • 并发性:程序运行过程中的并发性操作是非常常见的行为,分布式操作系统中的多个节点,可能会并发地操作一些共享的资源,如何准确并高效的协调分布式并发操作也成为了分布式系统架构与设计中最大的挑战之一
    • 全局时钟:分布式系统是有一系列在空间上随意分布的多个进程组成的,在这些进程之间通过交换消息来进行相互通信。因此,在分布式系统中,很难定义两个事件究竟谁先谁后,原因就是分布式系统缺乏一个全局的时钟序列控制。
    • 故障总会发生:任何在设计阶段考虑到的异常情况,一定会在系统实际运行中发生,并且,在系统实际运行过程中还会遇到很多在设计时未能考虑到的异常故障,所以,除非需求指标允许,在系统设计时不能放过任何异常情况。
  5. 线程启动为什么使用的是start而不是run方法(两次面试问到,JVM详解):
  • 线程有5个状态:创建、就绪、执行、阻塞、销毁,start执行的是就绪和执行两个状态,run执行的是执行状态。
  • start方法是开启一个线程进行执行,直接调用run方法可以执行,但是是在主线程中执行这一段代码,如果有两个线程同时调用run方法则需要前一个执行完了才能执行后一个,并不是同时执行的。
  1. 对象设计模式

推荐阅读更多精彩内容