欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java面试系列--java基础

程序员文章站 2022-05-06 18:25:28
...

Java基础总结

JAVA中的几种基本数据类型是什么,各自占用多少字节。

    八大基本数据类型,byte:8位,short:16位,int:32位,long:64位,float:32位,double:64位,boolean:1位,char:16位
    1字节=8位
    byte就是字节,有8位

String类能被继承吗,为什么。

    不能继承,因为String是final类,final类无法被继承。
    优势:无法被继承,同时内部字符数组也是final的,无法被修改,这种不变性这保证了字符串缓冲池的正常使用,也保证了其作为key的不变性条件

String,Stringbuffer,StringBuilder的区别。

    String表示不可变字符串,每个新的字符串都需要重新创建,当然可以重复指向缓冲池中的同一字面量,由于其不变性,导致其线程安全
    StringBuffer和StringBuilder是可变字符串,用于弥补String不可变导致的不便性,不用每次改变值都要重新创建一个新的对象。
    StringBuilder线程不安全,而StingBuffer内部使用synchronized实现线程安全性。

String常量池

    JDK7以前,字符串常量池在Perm区,JDK7之后位于堆中
    String s = "111";会直接在常量池中生成字符串字面量对象。
    String ss = new String("111");则是在堆中生成一个字符串对象
    如果实在jdk7以前,字符串常量池位于永久代,字符串对象则位于堆中,上面两种方式创建的对象将位于不同的区域和位置,如果这时调用ss.intern()方法,那么会在常量池中找是否存在与堆中字符串对象值相同的字符串字面量对象,无则创建返回,有则直接返回。
    如果是在jdk7之后,字符串常量池位于堆中,字符串对象也位于堆中,上面两种方式创建的对象将位于同区域的不同位置,如果这时调用ss.intern()方法,那么同样会在常量池中查找是否存在与ss字符串值相同的字符串字面量对象,无则创建返回,有则直接返回,但是此处,有一点不同,由于二者都在堆中,如果发现无的话创建的不是一个字面量对象,而是一个指向堆中已创建的字符串对象的引用。这时如果再执行String sss = "111",那么将会直接指向常量池中的那个引用。
    https://tech.meituan.com/in_depth_understanding_string_intern.html

ArrayList和LinkedList有什么区别。

    二者都是List列表的实现,区别在于二者实现的方式不同,前者底层采用数组来实现数据存储,采用数组需要连续的内存空间,而且由于数组是大小固定的结构,一旦数据多到一定程度,需要进行扩容,这会耗费一些时间。而后者采用双向链表实现,链表最显著的即使见缝插针,可以有效的利用内存,虽然每个节点要比前者大,因为要保存前后节点的指针。二者在操作上一致,但在处理速度上有显著差异:ArrayList的数组实现查询速度块,增删速度比较慢,而LinkedList的链表实现查询速度慢,增删速度要快。

讲讲类的实例化顺序,比如父类静态数据,构造函数,字段,子类静态数据,构造函数,字段,当new的时候,他们的执行顺序。

类的实例化顺序:
    加载class文件到方法区
    连接-准备阶段:父类静态字段初始化0值->子类静态字段初始化0值
    初始化阶段:父类静态字段初始化设定值->调用父类静态代码块执行父类初始化->子类静态字段初始化设定值->调用子类静态代码块执行子类初始化
    实例化阶段:调用父类非静态代码块执行父实例初始化->调用父类实例构造器创建父类实例->调用子类非静态代码块执行子类实例初始化->调用子类实例构造器创建子类实例

用过哪些Map类,都有什么区别,HashMap是线程安全的吗,并发下使用的Map是什么,他们内部原理分别是什么,比如存储方式,hashcode,扩容,默认容量等。

    Map表示映射集合,存储键值关系。
    Map的常用实现包括HashMap、TreeMap、LinkedHashMap、ConcurrentHashMap等
    HashMap:底层采用数组+链表+红黑树实现,采用hash表实现存储,继承自AbstractMap类,其内部重写了HashCode和equals方法,默认容量为16(指的是数组的容量),可进行扩容(扩容的是数组),线程不安全
    ConcurrentHashMap:同上,线程安全,并发下使用。
    TreeMap:底层采用红黑树实现,TreeMap没有调优选项,因为该树总处于平衡状态,线程不安全,删除、插入效率高
    LinkedHashMap:底层采用与HashMap一致的结构存储数据,不同之处在于在LinkedHashMap中增加了一个一双向链表用于数据的顺序存取(按插入排序),线程不安全

JAVA8的ConcurrentHashMap为什么放弃了分段锁,有什么问题吗,如果你来设计,你如何设计。

    1.8版本的ConcurrentHashMap中放弃的分段锁,因为使用分段锁,虽然比直接对整个数组加锁要效率提高不少,但还是会阻塞住针对同一个Segment中的元素的操作,1.8中不再对数组进行分组,而是恢复为一个长数组,采用对每个数组位置的元素进行加锁,这样的锁比分段锁更加细粒度,控制更加灵活,加锁之后,只会阻塞hash值相同的key的数据对应的操作。虽然还会有阻塞的情况,但是比起1.7的分段锁粒度更细。
    问题???我。。。。。。。
    http://www.importnew.com/28263.html

有没有有顺序的Map实现类,如果有,他们是怎么保证有序的。

有序的Map实现类:
    LinkedHashMap:底层采用与HashMap一致的结构存储数据,不同之处在于在LinkedHashMap中增加了一个一双向链表用于数据的顺序存取(按插入排序)
    TreeMap:采用默认自然顺序或者自定义顺序(大小升序排序),可以使用Comparator进行自定义排序方式

抽象类和接口的区别,类可以继承多个类么,接口可以继承多个接口么,类可以实现多个接口么。

    抽象类中的自定义的构造器需要在子类中实现。
    抽象类可以定义属性和方法实现,接口只能定义抽象方法,当然现在也能定义default方法
    抽象类中定义抽象方法需要加abstract,接口中不用
    抽象类和类的区别仅仅在于不能实例化,当然接口更不能实例化了
    抽象类需要加abstract class定义,接口用interface定义
    抽象类有构造器,接口没有
    抽象类的方法修饰符可以是public、protected、default,接口全部是public,而且可省略
    抽象类可以有main方法,且可运行,接口没有
    接口的字段默认都是 static 和 final 的,接口体内只能声明常量字段和抽象方法,并且被隐式声明为public,static,final.

继承和聚合的区别在哪。

    继承需要使用extends关键字,而且只能继承一个类或接口,继承是一种父子关系,子类继承父类的所有非私有的成员,是一种is-A关系
    聚合不需要使用任何关键字,而且不限数量,聚合是一种拥有的关系,是关联关系的一种强关联,被拥有者是单独存在,不受主件的影响,而且同一部件可被多个主件共有,人与房子的关系,房子里可能住了许多个人,一个房子被多人共有,人离开后,房子还照样存在。
    组合也是关联关系的一种强关联,它是一种包含的关系,被包含的是主件的一部分,荣辱与共,共生,而且多个主件不能包含同一部件,比如,人和手的关系,手是人的一个部件,随人而存在,共生。
    https://blog.csdn.net/qq_31655965/article/details/54645220

IO模型有哪些,讲讲你理解的nio ,他和bio,aio的区别是啥,谈谈reactor模型。

Java BIO(同步阻塞IO):等待数据和读取数据都是阻塞的,等待数据的阻塞完全就是在浪费生命
    BIO操作,每个操作都需要创建一个线程进行执行,极耗系统资源。
Java NIO(同步非阻塞IO):等待数据是非阻塞的,读取数据阻塞的(这里的同步是指数据的处理是同步的,都是先收到数据然后在进行处理,只是收到数据的部分不再是BIO式的阻塞式接收,而是非阻塞式接收)
java AIO(异步非阻塞IO):
    上面的同步异步的概念指的是等待数据和读取数据的先后操作是同步的,必然是先等待数据,待出现数据之后才能读取数据,

Reactor模型:
        
    同步:发起调用必须等到调用的逻辑执行完毕,返回结果才能继续下面的执行,同步就是一步一步执行。
    异步:发起调用不等调用逻辑的执行,一般是由另一个线程去执行,原线程继续执行下面的逻辑。异步一般意味着多线程
    阻塞:线程执行到一个耗时的操作时,会阻塞停止执行等待耗时操作的完成
    非阻塞:如果线程执行遇到耗时操作,不会一直等待,而是先执行别的操作,待耗时操作执行完毕之后通知线程,线程才可能会继续执行这个操作的剩余逻辑
    阻塞一般发生在两个部件、两个线程之间,当一个线程需要获取另一个线程的一些数据时,如果这些数据没有准备好,那么阻塞的情况下,就是这个线程一致等待另一个线程准备好数据再去获取这些数据,不阻塞就是,线程不去傻傻的等待另一个线程准备数据,先去看看有没有别的事情可以做做,等另一个线程数据准备好之后,通知这个线程,这个线程再去获取。
    https://www.cnblogs.com/xingzc/p/5796413.html

类加载机制原理

    类加载过程其实就是将编译好的XXX.class的二进制字节码文件读取到JVM内存,并对其进行校验、解析、初始化的过程。
    整个类加载阶段分为如下几步:
        1-装载:根据类的全限定名找到类的二进制字节流数据,并将其加载到内存,在方法区中生成JVM规定的运行时数据结构,并在内存中(堆或者方法区)创建一个代表该类的Class对象。
        2-链接:
            2.1-校验:对类的二进制字节流进行文件格式、元数据、字节码、符号引用进行校验
            2.2-准备:处理类变量,分配内存(方法区)、设置初始值0,类常量除外(ConstantValue)
            2.3-解析:将常量池中的符号引用转换为直接引用
        3-初始化:执行类的初始化(执行类构造器<clinit>())。其实就是执行类变量的赋值操作和静态块初始化操作。
    在类的加载阶段存在两个类变量的初始化操作,首次在准备阶段,这里初始化是赋0值,第二次是在初始化阶段,这个初始化是赋程序员设置的值。
    JVM中的运行时数据结构中定然包含有一个常量池,这个常量池对应于每个类,用于保存其符号引用,而且在解析阶段会将符号引用转化为直接引用。

反射的原理,反射创建类实例的三种方式是什么。

    反射的实现是基于类的Class对象的。类的Class对象是类被加载的时候由其类加载器自动生成的。其内部拥有指向方法区中类数据结构中字段、方法、构造器等的指针,可以通过指针获取到这个类的内容。基于此我们可以通过反射的方式来生成类的实例、执行类的方法等
    反射创建类实例,其实就是获取类的Class对象:
        1-通过类的实例对象的getClass方法
        2-直接获取类的class
        3-通过Class类的forName方法指定类的全限定名

反射中,Class.forName和ClassLoader区别 。

    ClassLoader.loadClass()方法:该方法会调用loadClass(name,false),第二个参数false表示在加载完class二进制文件之后不进行连接,连接包括校验、准备、解析操作,其中准备阶段会进行静态字段的初始化0操作,之后的初始化操作会进行初始化设定值操作和静态代码块的执行,这些都不会执行。
    Class.forName(String className)方法:该方法等效于执行Class.forName(className,true,currentLoader),第二个参数true表示class加载之后会被执行初始化操作。

描述动态代理的几种实现方式,分别说出相应的优缺点。

    动态代理一般有两种实现方式:
        JDk动态代理,基于接口实现,JDK自带的,运行速度快,限制较多,只能基于接口实现。实现方式是通过创建接口的实现类的方式实现代理
        CGLIB动态代理,基于类实现,使用更为普遍,但由于底层采用ARM字节码工具实现,处理速度要慢。实现方式是通过创建目标类的子类,并在子类中采用方法拦截的技术拦截所有目标类的方法的调用的方式实现代理,所以要代理的方法不得是final的。

动态代理与cglib实现的区别。

    同上

为什么CGlib方式可以对接口实现代理。

    同上

final的用途。

    final表示不变性
    final可修饰类、方法、字段、参数等
    修饰类:表示类不可被继承,如String类一般,
    修饰方法:表示方法不可被重写
    修饰字段:表示字段值不能被修改,视为常量
    修饰参数:表示参数值不能被修改
    此处的值不能修改指的是引用的值(对象的地址)不能变化,但是对象内部的属性是否变化不在此范围之内。
    其实final是用于实现多线程并发编程中的一个关键字,被其修饰的值不能被修改,那么说明其具有线程安全性,可被安全的并发访问。
    注意:被final修饰的静态字段

写出三种单例模式实现。

    懒汉式单例实现
    饿汉式单例实现
    线程安全的懒汉式单例实现
    双重加锁机制式单例实现
    内部类式单例实现
    枚举式单例实现

如何在父类中为子类自动完成所有的hashcode和equals实现?这么做有何优劣。

同时重写hashCode和equals方法即可,重写时可以自定义逻辑,按照自己的要求或者业务需求来实现。

请结合OO设计理念,谈谈访问修饰符public、private、protected、default在应用设计中的作用。

    这四种修饰符用于限定范围,public和default修饰类,四者均可修饰成员
    完美的使用四种修饰符可以实现模块的内部封装,对外只暴露接口,封装具体实现。有利于解耦,因为我们调用模块的时候也不需要是到那么多细节内容。
    public表示项目可见,表示在整个项目中都是公共的,这一般用于对外接口的设计和一些工具类的设计
    private表示类级私有,表示只在当前类中可见,子类也不可见,一般用于修饰字段,封装字段的直接修改权限,也用于一些类内部方法的细节实现,封装起来只对外提供接口,
    protected表示子类可见和包级可见,表示可在子类中访问,一般用于修饰字段和方法,修饰方法一般表示该方法可由子类重写自定义,提供一些自定义扩展,一般用于应用功能扩展。(即使子类不在同一个包也可见)
    default表示包级可见,表示只在同一个包内可见,通常一个包便是一个模块,所以,可以简单理解为模块可见,这样就好说了,我们定义为default表示这些类是针对这个模块的,是不对外的,属于封装在模块内部。
    这样我们就可以通过不同程度的封装实现不同级别的封装和隐藏,逐步对外暴露。
    类内部的细节方法可用private封装隐藏,对子类公开的方法可使用protected修饰,同时也对包级公开,如果只对包可见,采用default修饰,包外可见就需要使用public修饰了,表示公共方法,公共类,可看成为模块的接口。
    可见:范围分为:类级--包级(模块级)--项目级
    这四个表示范围的关键字实现了代码级的封装,这是Java面向对象的特性之一。

深拷贝和浅拷贝区别。

    浅拷贝就是只针对目标对象进行复制,目标对象引用的对象不做复制。
    深拷贝就是将目标对象及其引用的所有对象全部进行复制。
    实现拷贝需要重写clone方法,同时实现Cloneable接口。

数组和链表数据结构描述,各自的时间复杂度。

    数组是一组连续的内存空间,而且拥有固定的大小,要想实现变长,需要进行数组新建和数据迁移,查找元素时间复杂度O(1),增删元素时间复杂度O(n)。
    链表是一种链式节点的结构,每个节点中都存储着下一个节点的引用,依此来构成一个链条,它不需要连续的空间,可以见缝插针,有效的利用内存碎片,天生变长,无序扩容,查找元素时间复杂度O(n),增删元素时间复杂度O(1)。

error和exception的区别,CheckedException,RuntimeException的区别。

    error和exception都是Throwable的子接口,前者表示无法解决的错误,比如系统错误之类,后者表示程序执行发生的异常,可以被处理。系统一旦出现error错误,那么整个系统将无法继续运行。但是系统出现异常,只要处理得当,不会对系统造成多大影响
    异常又分为受检异常和运行时异常
    受检异常可以在编译时检测到,必须进行手动处理,否则无法通过编译。
    运行时异常属于代码逻辑性异常,需要在运行期出现,可以不进行处理,同样可以通过编译,但是在运行时一旦出现该异常,则当前线程停止执行。一般情况下我们都会对运行时异常进行捕捉处理,以保证程序的正常运行,对于无关紧要的部分可以直接catch住,但对于影响当前线程后续执行的异常必须立即抛出来结束线程执行,这样能有效保护数据,不会因为异常而产生错误数据,一般我们会在事务中执行,一旦抛出异常,执行回滚,保证数据安全。

请列出5个运行时异常。

    NullPointException:空指针异常
    NumberFormatException:数字格式化异常
    IndexOutOfBoundsException:下标越界异常
    ClassCastException:类型转换异常
    SecurityException:安全异常
    IllegalArgumentException:非法参数异常
    ConcurrentModificationException:多线程修改异常
    NoSuchElementException:无此元素异常
    RejectedExecutionException:拒绝执行异常
    SystemException:系统异常
    TypeNotPresentException:类型不存在异常

在自己的代码中,如果创建一个java.lang.String类,这个类是否可以被类加载器加载?为什么。

    基于类加载的双亲委托机制,自定义的String类永远不会被加载,因为String类最终会被委托到Root ClassLoader来进行加载,它在会在rt.jar包中加载这个类,加载一次之后,就不会再次进行加载,除非该类长期不使用被回收,但是String类可以算是Java中使用频率最高的类之一,基本上只要加载进内存,会一直留存到应用关闭。
    双亲委托机制:当前线程需要加载某个类,首先调用当前线程的类加载器(一般为AppClassLoader或者其他自定义的类加载器),该类加载器会首先委托其父类加载器进行该类的加载,这样逐级委托直到RootClassLoader,如果父类加载器在其管辖的范围内找不到该类,则通知其子类加载器自己无法加载,然后子类会自己尝试去加载,最后会直到这个类被加载为止,
    我们在自己代码中定义java.lang.String,这个委托首先逐级委托到root ClassLoader去加载,它发现在自己范围内有这个类,然后就从中加载,这时加载的是JDK自带的String类,然后告诉子类加载完成。自定义的这个String就不会被加载使用。

说一说你对java.lang.Object对象中hashCode和equals方法的理解。在什么场景下需要重新实现这两个方法。

    这两个方法其实是一脉相承的,在Object中都拥有默认的实现,其中equals的默认实现是比较两个对象的地址(即等效于==),当我们自定义的类需要实现不同的等值判断逻辑时,就需要重写这两个方法,比如String中的重写逻辑,不再是比较地址,而是比较两个字符串对象的字面值是否相等,这种方式也是我们一直认同的等值比较方式。
        equals方法主要目的是定义对象比较方式,而hashCode方法主要目的是为了计算对象的hash值,而这个hash值只在当前对象保存到hash数据结构的时候才会使用到。其余情况下基本无用。
        至于二者关系:equals的两个对象hashCode一定相同,但是hashCode一致的两个对象却不一定equals(即hash冲突)。

在jdk1.5中,引入了泛型,泛型的存在是用来解决什么问题。

    泛型:
        解决的类似于以前集合中可以无限制的存放任何类型的元素,这样在真正使用的时候我们也不知道元素的具体类型是什么。使用泛型,确保在一个集合中只存放固定类型的元素,便于开发。因为他只作用于编译期,所以也就是语法糖的一种了。
        有效去除重复代码,使用泛型之后,可以适用多种类型,泛型可以看成是再抽象。
        泛型可作用于类(接口)和方法,分别为泛型类(接口),泛型方法
    泛型的类型擦除:
        泛型作用于编译期,编译完成后,类型参数会被擦除。
        泛型的作用也就明了了,那就是在编译期强制保证确切的类型。

这样的a.hashcode() 有什么用,与a.equals(b)有什么关系。

    这是计算hash值对方法,这个方法一般会用在equals实现中用于比较两个对象的等值比较,两个等值对象的hashCode一定相同,两个不等值对象的hashCode也有可能相同,但是HashCode不同的两个对象一定是不等值的。

有没有可能2个不相等的对象有相同的hashcode。

    有,hashCode采用的是一种算法实现的,同样的原始数据计算结果一定是一样的,但是不同的原始数据计算结果不一定就不一样。hash算法的特点就是如此,将不同的数据尽可能的平均分布在一个固定的范围之内,在这个范围之内的每个值之上都有可能有不止一个数据存在,存在多个数据的那个hashCode就是上面这种情况。

Java中的HashSet内部是如何工作的。

    HashSet底层使用的就是HashMap实现的,HashSet就是value永远为一个空Object对象的HashMap,HashMap用键保存着HashSet的值。默认大小为16。

什么是序列化,怎么序列化,为什么序列化,反序列化会遇到什么问题,如何解决。

    序列化是实现对象持久化的一种方式,一个类实现了Serialization接口之后,就拥有了序列化的能力。
    执行序列化的时候,会借助ObjectOutputStream和ObjectInputStream来实现。
    序列化使用ObjectOutputStream的writeObject方法实现
    反序列化使用ObjectInputStream的readObject方法实现
    transient关键字修饰的字段不会被持久化
    当我们需要在网络中传递对象或者需要将对象保存到持久化存储中时,就需要使用序列化功能。
    序列化的时候有一个序列化ID的东西,当我们没有指定的时候,会自动生成,这样在反序列化的时候可以找到对应的类进行反序列化
    那么问题来啦,当我们序列化之后对类进行了更改,这时在进行反序列化时,就会报错,说类找不到,这是因为在修改类之后,其序列化Id也发生了变化,原来序列化的数据采用的是旧的序列化ID,用这个旧ID查找不到这个类,因为类的序列化Id已经发生了变化。
    怎么解决呢,很简单,手动添加一个固定的序列化ID值,这样即使类结构发生了变化,这个序列化Id也是不变的,这样旧的序列化数据也可以顺利的找到这个类来完成反序列化。

java8的新特性。

    函数式编程
    Lambda表达式
    行为参数化(方法作为参数)

static变量

    static修饰的变量叫类变量,超脱于对象的存在,也可见简单地看作公共变量,类变量在类加载的第二个阶段链接的第二阶段准备阶段进行空间分配和赋0值,然后在类加载第三阶段初始化阶段进行初始化赋值。