Java基础(面试题)高频重点面试题

  • final关键字
    • 用来修饰数据:包括成员变量和局部变量,该变量只能被赋值一次且它的值无法被改变。对于成员变量来说,修饰的类变量,必须在声明时被初始化。修饰的实例变量,必须在声明时或构造方法中对它进行赋值。
    • 用来修饰方法参数:表示在变量的生存期中它的值不能被改变。
    • 用来修饰方法:表示该方法不能被重写
    • 用来修饰类:表示该类不能被继承
    • 引申:为什么说String类是final修饰的?    因为它不能被修改!
  • static关键字的作用是什么?
    • 修饰变量:因为类加载进方法区,所以多个对象是共享的。( 引申:JVM类加载过程?    加载-验证-准备-解析-初始化)
    • 修饰方法:工具类的方法,不需要建立对象,直接使用“类名.方法名”的方式调用。
    • 修饰静态代码块:只会在类被初次加载的时候执行一次,可以用于初始化等操作。
    • 修饰内部类:注意:一般方法可以访问静态方法,但静态的方法必须访问静态的方法。
    • 引申:静态属性和静态方法可以被继承嘛?    
      • 首先是可以被子类继承的,但静态方法无法被子类重写!重写的本质是动态绑定,即根据运行时对象的类型来决定调用哪个方法,而不是根据编译时的类型。静态方法属于类,在编译阶段已经被绑定到了类上,表现出来的是通过类名.方法名进行访问;所以静态方法无法被子类重写。
  • java中方法的参数传递机制(都是传递副本)
    • 问:当一个对象被当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回改变后的结果,那么这里面是值传递还是引用传递?
      • 是值传递,Java编程语言只有值传递参数
    • 如果参数类型是基本数据类型,那么传过来的就是这个参数的一个副本,也就是这个原始参数的值,如果在函数中改变了副本的值不会改变原始的值。
    • 如果参数类型是引用类型(对象的引用),那么传过来的就是这个引用参数的副本,这个副本存放的就是参数的地址,如果在函数中没有改变这个副本的地址,而是改变了地址中的值,那么在函数中的改变会影响到传入的参数(比如:传入对象的引用,如果函数内对对象的属性进行了修改,那么传入的对象属性就发生了改变),如果在函数中改变了副本的地址,如new了一个,那么副本就指向一个新的地址,此时传入的参数还是原来的地址,所以不会改变参数的值。
    • 基不变! 引变!
    • 总之,不管传递什么类型的参数,都是传递的副本,原始类型就传递值的副本,引用类型就传递地址的副本,如果在方法内修改了地址指向的内容,那么就会影响传入地址的内容(浅拷贝)。但是如果在方法内new了一个新的指向,副本指向新的内容,那么副本就不会改变原地址的内容(深拷贝)。
    • 浅拷贝: 复制地址!     深拷贝:复制原对象的值
  • 什么是序列化和反序列化?
    • 序列化:将对象写入到IO流中      反序列化:从IO流中恢复对象
    • 意义:序列化机制允许将实现序列化的java对象转换字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
    • 使用场景:所有可在网络上传输的对象都必须是可序列化的( 引申:RPC机制),比如RMI、PRC调用,传入的参数或返回的对象都是可序列化的,否则会出错,所有需要保存到磁盘的java对象都必须是可序列化的。
    • 1、所有需要网络传输的对象都需要实现序列化接口,通过建议所有的javaBean都实现Serializable接口。
    • 2、对象的类名、实例变量(包括基本类型,数组,对其他对象的引用)都会被序列化;方法、类变量、transient实例变量都不会被序列化。如果想让某个变量不被序列化,使用transient修饰。
    • 3、序列化对象的引用类型成员变量,也必须是可序列化的,否则会报错,反序列化时必须有序列化对象的class文件。
    • 4、同一对象序列化多次,只有第一次序列化为二进制编码,以后都只是保存序列化编号,不会重复序列化。
  • java中“==” 与 “equals”的区别
    • 1、“==”:如果是基本数据类型,则直接对值进行比较,如果是引用型数据类型,则是对他们的地址进行比较(遇到运算符时才会自动拆箱)
    • 2、equals方法继承自Object类,在具体实现时可以覆盖父类中的实现。看一下Object中equals的源码发现,它的实现也是对对象的地址进行比较,本质就是“==”,而JDK类中有一些类覆盖了Object的equals()方法,比较规则为:如果两个对象的类型一致,并且内容一致,则返回true,这些类有:java.io.file、java.util.Date、java.lang.string,包装类:Inreage、Double等。
    • 3、在实际开发中总结:
      • 类未复写equals方法,则使用equals方法比较两个对象时,相当于==比较,即两个对象地址是否相等,地址相等返回true,不相等返回false。
      • 类复写equals方法,比较两个对象时,则走复写之后的判断方式。通常,我们会将equals复写成:当两个对象内容相同时,则equals返回true,内容不相同时返回false
  • String、StringBuilder、StringBuffer的区别
    • String是不可变字符串对象(final修饰了char数组),StringBuilder和StringBuffer(线程安全,因为synchronize修饰了方法)是可变字符串对象(其内部的char数组长度可变)。区别在于底层是否是final修饰了char数组
    • 当字符串相加操作或改动较少的情况下,建议使用String str = “hello”这种形式
    • 当字符串相加操作较多的情况下(2个以上),建议使用StringBuilder,如果采用了多线程,则使用StringBuffer(线程安全的)
    • 为什么说String类是final修饰的
      • 为了实现字符串池,因为只有当字符串是不可变的,字符串池才有可能实现
      • 为了线程安全
      • 为了实现String可以创建HashCode不可变性,因为字符串是不可变的,所以在它创建的时候HashCode就被缓存了,不需要重新计算
  • Object类有哪些方法
    • 1、clone方法(浅拷贝)
      • 保护方法,实现对象的浅复制(只对基本数据类型复制),只有实现了Cloneable接口才可以调用该方法,否则抛出CloneNotSupportedException异常。主要是Java里除了8种基本数据类型传参数是值传递,其他的类对象传参数都是引用传递,我们有时候不希望在方法里的参数改变,这里就需要在类中复写clone方法
    • 2、getClass方法
      • final方法,获得运行时类型
    • 3、toString方法
      • 该方法用得很多,一般子类都有覆盖,打印输出对象数据
    • 4、finalize方法(在JVM章节:如何确定对象已死中有用到)
      • 该方法用于释放资源。因为无法确定该方法什么时候被调用,很少使用
    • 5、equals方法
      • Object中就是==,比较地址相等
    • 6、HashCode方法( 引申:HashCode源码实现
      • 该方法用于哈希查找,可以减少在查找中使用equals次数,重写了equals方法一般都要重写HashCode方法。这个方法在一些具有哈希功能的Collection用到
    • 7、wait方法
      • wait方法就是使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。wait()方法一直等待,直到获得锁或被中断。wait(long timeout)设定一个超时间隔,如果在规定时间内没有获得锁就返回
    • 8、notify方法
      • 该方法唤醒在该对象上等待的某个线程
    • 9、notifyAll方法
      • 该方法唤醒在该对象上等待的所有线程
  • 异常体系
    • Exception分为两类:运行时异常和编译异常
      • 1、运行时异常(不受检异常):RuntimeException类极其子类表示JVM在运行期间可能出现的错误。比如说试图使用空值对象的引用(NullPointerException)、数组下标越界(ArraryIndexOutBoundException),此类异常属于不可查异常,一般是由程序逻辑错误引起的,在程序中可以现在捕获处理
      • 2、编译异常(受检异常):Exception中除了RuntimeException极其子类之外的异常。如果程序中出现此类异常,比如说IOException,必须对该异常进行处理(try  catch、throw),否则编译不通过
  • Java中BIO、NIO、AIO
    • 同步与异步(看结果)
      • 同步:同步就是发起一个调用后,被调用者未处理完请求之前,调用不返回
      • 异步:异步就是发起一个调用后,立刻得到被调用者的回应表示以接收请求,但是被调用者并没有返回结果,此时我们可以处理其他的请求,被调用者通常依靠事件,回调等机制来通知调用者其返回结果
      • 同步与阻塞的区别在于异步的被调用者会通过回调等机制来获得调用者的返回结果
    • 阻塞与非阻塞(看过程)
      • 阻塞:阻塞就是发起一个请求,调用者一直等待请求结果返回,也就是当前线程会被挂起,无法从事其他任务,只有当条件就绪才能继续
      • 非阻塞:非阻塞就是发起一个请求,调用者不用一直等着结果返回
    • 如何理解同步阻塞、同步非阻塞、异步非阻塞
      • 举个生活中的例子,你妈妈让你去烧水,小时候比较笨,在那里傻等着水开(同步阻塞),等你稍微长大一点,你知道每次烧水的空隙可以去干点别的事情,然后只需要时不时回来看一下水有没有烧开(同步非阻塞),在后面,你们家用上了水烧开就会报警的水壶,这样你只需要听到报警时就知道水开了,在这期间你可以随便干自己的事情(异步非阻塞)
    • BIO(Blocking IO)
      • 同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等着其完成
    • NIO(New IO)
      • NIO是一种同步非阻塞的I/O模型,它支持面向缓冲的,基于通道的I/O操作方法
      • Channels and Buffer(通道和缓冲区):标准的IO基于字节流和字符流进行操作的,而NIO是基于通道(Channels)和缓冲区(Buffer)进行操作,主数据总是从通道读取到缓冲区,或者从缓存区写入到通道中
      • Asynchronous IO(异步IO):java NIO可以让你异步的使用IO,例如:当线程从通道读取数据到缓冲区时,线程还可以进行其他事情,当数据被写入到缓冲区时,线程可以继续处理它,从缓存区写入通道也类似
      • Selectors(选择器):java NIO引入了选择器的概念,选择器用于监听多个通道的事情(比如:连接打开,数据到达),因此单个的线程可以监听多个数据通道
      • IO(BIO)与NIO的区别
        • 1、面向流和面向缓冲
          • IO是面向流的,NIO是面向缓冲区。Java IO面向流意味着每次从流中读取一个或多个字节,直至读取完所有字节,它们没有被缓冲存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取数据,需要先将它缓冲到一个缓冲区。
          • Java NIO的缓冲导向方法略有不同,数据读取到一个稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,但是还需要检查是否该缓冲区中包含所需要处理的数据,而且需要确保当更多的数据读入缓冲区时,不要覆盖缓冲区里尚未处理的数据
        • 2、阻塞与非阻塞
          • Java IO的各种流是阻塞的,这意味着当一个线程调用read()或write()时,该线程被阻塞,直到有一些数据被读取,或数据完全写入,该线程在此期间不能再干任何事情了。
          • Java NIO的非阻塞模式,单线程从通道读取数据到buffer,同时可以继续做别的事情,当数据读取到buffer中后,线程再继续处理数据,写数据也是一样的
      • 选择器(Selectors)
        • Java NIO的选择器允许一个单独的线程来监视多个输入通道,你可以注册多个通道使用一个选择器,然后使用一个单独的线程来“选择”通道,这些通道里已经有处理的输入,或者选择已准备写入的通道。这些选择机制,使得一个单独的线程很容易来管理多个通道
    • AIO(Asynchronous IO)
      • AIO是在Java7引入了NIO的改进版,异步非阻塞的IO模型。异步IO是基于事情和回调机制实现的,也就是应用操作之后会直接返回,不会阻塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。
      • 如何理解NIO同步和AIO异步
        • 虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的,对于NIO来说,业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。
  • Java的反射机制
    • 反射基本概念:
      • Java反射就是在运行状态中,对于任何一个类,都能够知道这个类的所有属性和方法,对于任意一个对象,都能够调用它的任意方法和属性,并且能够改变它的属性,这种动态获取的信息以及动态的调用对象的方法的功能称为Java语言的反射机制。( 引申:动态代理设计模式也采用了反射机制
      • 优点:运行期类型的判断,动态加载类,提高代码灵活度
      • 缺点:性能瓶颈,反射相当于一系列解释操作,通知JVM要做的事情,性能比直接的Java代码要慢很多,安全问题,可以动态操作改变类的属性同时也增加了类的安全隐患
    • Class.forName 和 classloader 的区别(结合JVM类加载)
      • Class.forName除了将类的.class文件加载到JVM中之外,还会对类进行解释,执行类中的static块。
      • 而classloader只干一件事情,就是将.class文件加载到JVM中,不会执行static中的内容,只有newInstance才会去执行static块。forName(“”)得到的class是已经初始化完成的
      • 最重要的区别就是forName会初始化Class,而loadClass不会,因此如果要求加载时类的静态变量被初始化或静态块里的代码被执行就只能用forName,而用loadClass只有等创建类实例是才会进行这些初始化
  • 简述面向对象三大特征(封装、继承、多态)
    • 1、封装
      • 在面向对象思想中,封装指数据(类成员属性)和对数据的操作(类的方法)捆绑到一起,形成对外界的隐藏,同时对外提供可操作的接口(供外部访问的类成员)
      • 封装的意义在于保护或者防止代码(数据)被我们无意中破坏,保护成员属性,不让类以外的程序直接访问和修改,隐藏方法的细节
    • 2、继承
      • 继承是从已有的类中派生出新的类,新的类能吸收已有类的数据属性和行为,并能扩展新的能力,为什么要继承:反映现实的真实关系,减少代码冗余,对父类的属性和方法进行扩展和重写。继承中子类不可以选择性的继承父类的东西,而是全部继承父类的属性和方法,其中父类又叫超类和基类,子类又叫派生类。父类是子类的一般化,子类是父类的具体化,Java中不支持多继承,一个类最多只能有一个父类,而在Java中多继承是通过接口实现的
      • 引申:父类的静态属性和方法是否可以被子类继承和重写?
        • 首先是可以被子类继承的,但静态方法无法被子类重写!重写的本质是动态绑定,即根据运行时对象的类型来决定调用哪个方法,而不是根据编译时的类型。静态方法属于类,在编译阶段已经被绑定到了类上,表现出来的是通过类名.方法名进行访问;所以静态方法无法被子类重写。
    • 3、多态
      • 父类引用指向不同子类对象
      • Java实现多态有三个必要条件:继承、重写、向上转型。
      • 继承:在多态中必须存在有继承关系的子类和父类
      • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法
      • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备能够调用父类的方法和子类的方法
      • 实现方式:接口多态性、继承多态性、抽象类实现的多态性
      • 实现原理:多态绑定
  • Java中方法重载和重写的区别(多态体现)
    • 重载:同一类中同名函数,具有不同的参数个数或类型(返回值不参与),是一个类中多态性的体现。是由静态类型确定,在类加载的时候就确定,属于静态分派
    • 重写:子类中含有与父类相同名字、返回类型和参数表,则重写,是在继承中多态性的体现,属于多态分派
    • 方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时多态性,后者实现的是运行时的动态性,重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写的方法与父类被重写的方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写的方法声明更多的异常。重写对返回类型没有特殊的要求,不能根据返回类型进行区分
    • 引申:构造器可以被重写吗
      • 构造器不能被继承,所以不能被重写,但可以被重载。
  • 抽象类和接口的区别
    • 抽象类(为了继承而存在)
      • 抽象方法必须为public或protected(如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public
      • 抽象类不能用来创建对象
      • 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法,如果子类没有实现父类的抽象方法,则必须将子类也定义为abstract类
    • 接口(更加抽象)
      • 变量只能定义为public static final
      • 方法只能为抽象的
    • 区别:
      • 语法层面:
        • 抽象类可以提供成员方法和实现细节,而接口中只能存在public abstract方法
        • 抽象类中的成员变量可以是各种类型的,而接口中的成员变量只能是public static final类型的
        • 接口中不能含有静态代码块以及静态方法,而抽象类可以有静态代码块和静态方法
        • 一个类只能继承一个抽象类,而一个类却可以实现多个接口
      • 设计层面:
        • 类是:是不是关系          接口是:有没有关系
        • 抽象类作为很多子类的父类,它是一种模板式设计,而接口是一种行为规范,它是一种辐射式设计
  • ArrayList 和 LinkedList区别
    • 1、底层数据结构:
      • ArrayList底层使用的是数组;
      • LinkedList底层是双向链表数据结构
    • 2、插入和删除是否受元素位置的影响
      • ArrayList采用数组存储,所以插入和删除元素的时间复杂度受元素位置影响,比如执行add方法的时候。ArrayList会默认将指定元素追加至列表末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置i插入和删除元素的话时间复杂度就为O(n-i),因为在进行上述操作的时候集合中第i和第i个元素之后的(n-i)个元素都要执行向后/向前移一位的操作
      • LinkedList采用链表存储,所以删除元素时间复杂度不会守元素位置影响,近似O(1),如果是要在指定位置i插入和删除元素的话时间复杂度近似为O(n),因为需要位移到指定位置再插入
    • 3、是否支持快速随机访问
      • LinkedList不支持高效的随机元素访问,
      • 而ArrayList支持快速随机访问就是通过元素的序号快速获取元素对象
    • 4、内存空间占用
      • ArrayList的空间浪费主要体现在list列表的结尾会预留一定的容量空间
      • LinkedList的空间花费则主要体现在它的每一个元素都需要消耗比Arraylist更多的空间,因为要存放结点的直接前驱和直接后继以及数据
  • Arraylist扩容过程
    • 1、构造函数:以无参数构造方法创建ArrayList时,实际上初始化赋值的是一个空数组。当真正对数组进行添加元素操作时,才真正分配容量,即向数组中添加第一个元素时,数组容量扩为10,这样可以达到在真正使用数组时才会分配内存
    • 2、添加元素:首先会进行容量控制,确保数组容量是满足的,如果当前数组元素个数+1 > 数组长度,则进行1.5倍扩容
    • 3、扩容:扩容并不是严格的1.5倍,是扩容前的数组长度右移一位 + 扩容前的数组长度, 将旧数组内容通过Array.copyOf全部复制到新数组,此时新旧列表的size大小相同,但elementData的长度即容量不同
  • HashMap底层实现(面试必问)
    • HashMap原理,内部数据结构
      • HashMap的内部存储结构其实是数组+链表的结合(1.8之后就加入了红黑树),当实例化一个HashMap时,系统会创建一个长度为Capacity的Entry数组,这个长度被称为容量(Capacity),在这个数组中可以存放元素的位置我们称之为“桶”(bucket),每个buctet都有自己的索引,系统可以根据索引快速的查找bucket中的元素,每个bucket中存储一个元素,即一个Entry对象,但每一个Entry对象可以带一个引用变量,用于指向下一个元素,因此在一个桶中就有可能生成一个Entry链,说白了就是单链表
    • HashMap中的put方法过程
      • 对key求哈希值然后计算下标,如果没有哈希值碰撞就直接放入槽中,如果碰撞了以链表的形式连接到后面,如果链表的长度超过了阈值(默认阈值是8),就把链表转化为红黑树,大前提就是这个HashMap的长度要大于64。如果节点以存在就替换旧值,如果槽满了(容量*加载因子(默认0.75))就需要resize扩容
    • HashMap中哈希函数是怎么实现的?还有哪些hash实现方式?
      • (n-1)&(h ^ (h >>> 16)):  h ^ (h >>> 16): 高16bit不变,低16bit和高16bit做异或获得hash,然后再 (n-1)&hash得到下标。 (h >>> 16) 获得低16位能够更好的均匀散列,减少碰撞,减少冲突
      • 直接寻址法:取关键字或关键字的某个线性函数值为散列地址,H(key) = key 或 H(key) = a?key + b  其中ab都为常数(这种叫做自身函数)
      • 平方取中法:取关键字平方后的中间几位作为散列地址
      • 折叠法:将关键字分割成位数相同的几部分,最后一部分位数可以不同,然后取这几部分的叠加和(去除进位)作为散列地址
      • 随机数法:选择一随机函数,取关键字的随机值作为散列地址,通常用于关键字长度不同的场合
      • 除留余数法:去关键字被某个不大于散列表表长m的数p除后所得到的余数作为散列地址
    • 为什么HashMap中 哈希函数中&位必须为奇数(length-1)
      • 长度是16或者其他2的幂,length-1的值是所有二进制位全为1,这种情况下,index的结果等同于HashMap后几位的值,只要输入的HashMap本身分布均匀,Hash算法的结果就是均匀的。然后使用 ( length -1)&HashCode,相当于求出余数,比%快,确定在桶中的位置tab【 (n-1)&hash 】。
    • 如何解决hash冲突?
      • Hash冲突:hashcode一样,但是key不一样,就导致了冲突。(如果key一样,就是相同值需要覆盖了)。HashMap的冲突处理是用的链地址法,将所有哈希地址相同的记录都连接在同一链表中
    • HashMap线程并发安全性问题
      • HashMap在并发时可能出现的问题主要是两方面:
        • 如果多个线程同时使用put()方法添加元素,而且假设正好存在两个put的key发生了碰撞,那么根据HashMap的实现,这两个key会添加到同一个位置,这样最终就会发生其中一个线程put的数据被覆盖
        • 如果多个线程同时检测到元素个数超过数组大小,这样就会发生多个线程同时对Node数组进行扩容,都在重新计算元素位置以及复制数据,但是最终只有一个线程扩容后的数组会赋给table,也就是说其他线程的都会丢失,并且各自线程put的数据也丢失
    • HashMap底层用到了红黑树,简述红黑树的五大特征
      • 节点要么为红色,要么为黑色
      • 根节点为黑色
      • 叶子节点为黑色
      • 每个红色节点的左右孩子都是黑色(保证了从根节点到叶子结点不会出现连续两个红色节点)
      • 从任意节点到其每个叶子结点所有路径,都包含相同数目的黑色节点
    • HashMap扩容的优化,是否会重新计算hash
      • 1.7扩容会重新计算,这个key值(key%新的长度)在新桶的什么位置,这样就会有大量的重复计算
      • 1.8扩容优化:
        • 桶中的链表不需要像1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap(旧表table长度)”
        • 引入红黑树,仍然会采用原索引+oldCap的方式去重新重构链表,如果重构长度大于一定值,就会转为红黑树
    • 为什么要扩容
      • 在当前哈希表元素很多的情况下,如果get元素时间复杂度很高,减少哈希冲突
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值