JavaSE面试题
面向对象
面向过程更加注重事情的每一个步骤及顺序,面向对象更加注重事情有哪些参与者(对象)、及各自需要做什么。
面向过程比较直接高效,而面向对象更易用于复用、扩展和维护
三大特性
封装:封装的意义,在于明确标识出允许外部使用的所有成员函数和数据项
- 自己的理解:内部的调用细节对外部透明,外部调用无需修改或者关心内部实现。(例如内部javabean中的属性,实现并提供了getset方法的实现,外部只需要知道这个属性的getset方法是干啥用的,不用关心这之中是如何实现的)
继承:继承基类的方法,并做出自己的改变或扩展
子类共性的方法或者属性直接使用父类的,而不需要自己再定义,只需要扩展自己个性化的
多态:基于对象所属类的不同,外部对同一个方法的调用,实际执行逻辑不同
条件:必须要继承父类,父类的方法要重写在子类中,父类引用指向子类对象
弊端:无法调用子类特有的功能
JDK、JRE、JVM三者区别和联系
jdk=jre+java工具(javac、java、jconsole),jre=jvm(jre里面的bin)+lib(类库)
jdk包含了jre,jre包含了jvm
==和equals
:对比的是栈中的值,基本数据类型(八大基本数据类型,例如int long byte double等)是变量值,引用数据类型(类、包装类、接口、数组)是堆中内存对象的地址
equls:object中默认也是采用比较,通常会重写,string类中的equlas()方法其实比较两个字符串的内容,不是栈中的地址
final
简述final的作用?
最终的,不可改变
- 修饰类:表示类不可被继承
- 修饰方法:表示方法不可被子类覆盖,但可以重载
- 修饰变量:表示变量一旦被赋值就不可以更改它的值。
(1) 修饰成员变量
- 如果final修饰的是类变量(用static修饰的变量),只能在静态初始化块中指定初始值或者声明该类变量时指定初始值。
- 如果final修饰的是成员变量,可以在非静态初始化块、声明该变量或者构造器中执行初始值。
(2)修饰局部变量
系统不会为局部变量进行初始化,局部变量必须由程序员显示初始化。因此在使用局部final修饰局部变量时,即可以在定义时指定默认值(后面的代码不能对变量再赋值),也可以不指定默认值,而在后面的代码中对final变量赋初值(仅一次)
(3)修饰基本数据类型和引用数据类型
如果是基本数据类型的变量,则其数值一旦初始化之后便不能更改
如果是引用类型的变量,则在对其初始化之后便不能再让其指定另一个对象。但是引用的值是可变的。
为什么局部变量内部类和匿名内部类只能访问局部final变量?
由于编译的时候会生成class文件,无论是外部类还是内部类都会生成class文件。当外部类的方法结束时,局部变量就会销毁了,但是内部类对象可能还存在。**然后就出现了一个矛盾:内部类访问了一个不存在的变量。**为了解决这个问题,就将局部变量复制了一份作为内部类的成员变量,这样局部变量死亡之后,内部类仍可以访问它,实际访问的是局部变量的“copy”。这样就延长了局部变量的生命周期。
将局部变量复制到内部类的成员变量时,必须保证这两个变量是一样的,也就是说,我们在内部类中修改了成员变量,方法中的局部变量也跟着改变,怎么解决这个问题呢?
所以就将局部变量设置为final,对它初始化后,我就不让你再去修改这个变量,就保证了内部类的成员变量和方法的局部变量的一致性。
String、StringBuffer、StringBuilder区别及使用场景
String是final修饰的,不可变,每次操作都会产生新的String对象
StringBuffer和StringBuilder都是在原对象上操作的
StringBuffer是线程安全的,StringBuilder是线程不安全的
StringBuffer方法都是synchronized修饰的
性能:StringBuilder>StringBuffer>String
场景:需要改变字符串内容时使用后面两个
优先使用StringBuilder,多线程使用共享变量时使用StringBuffer
当这个变量是作为共享变量,并且在多线程情况下,保证结果正确,使用StringBuffer
重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰范围大于等于父类;如果父类方法访问修饰符为private则子类就不能重写该方法。
接口和抽象类的区别
- 抽象类可以存在普通成员函数,而接口只能存在public abstract方法。
- 抽象类中的成员变量可以时各种类型的,而接口中的成员变量只能是public static final 类型的。
- 抽象类只能继承一个,接口可以实现多个。
抽象类是对类本质的抽象,表达的是is a的关系。
接口是对行为的抽象,表达的是like a 的关系
使用场景:当你关注一个事务的本质的时候,用抽象类;当你关注一个操作的时候,用接口。
抽象类的功能要远超过接口,但是,定义抽象类的代价高。因为高级语言来说每个类只能继承一个类。在这个类中,你必须继承或编写出其所有的子类的所有共性。虽然接口在功能上会弱化许多,但是它只是针对一个动作的描述。而且你可以在一个类中同时实现多个接口,在设计阶段会降低难度。
List和Set的区别
List:有序,按对象进入的顺序保存对象,可重复,允许多个Null元素对象,可以使用Iterator取出所有元素,在逐一遍历,还可以使用get(int index)获取指定下标的元素。
Set:无序,不可重复,最多允许一个Null元素对象,取元素时只能使用Iterator接口取得所有元素,在逐一遍历各个元素。
hashCode与equlas
hashCode()的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置
hashCode的特点:能根据"键"快速的检索出对应的”值“。这其中就利用到了散列码(可以快速找到所需要的对象)
为什么要有hashCode
以”HashSet如何检查重复“为例子说明为什么要有hashCode:
对象加入HashSet会先计算对象的hashcode值来判断对象加入的位置,如果没有、HashSet会假设对象没有重复出现。但是如果发现有值,这时会调用equlas()方法来检查两个对象是否真的相同。如果两者相同,HashSet就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样就大大减少了equls的次数,相应就大大提高了执行速度。
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都是返回true
- 两个对象相同的hashcode值,它们也不一定相等的
- 因此,equals方法被覆盖过,则hashCode方法也必须覆盖
- hashCode的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
ArrayList和LinkedList区别
ArrayList
概念:基于动态数组,连续内存存储,适合下标访问(随机访问)
扩容机制:因为数组长度固定,超出长度存数据时需要新建数组,然后将老数组的数据拷贝到新数组中,如果不是尾部插入数据还会涉及到元素的移动(往后复制一份,插入新元素),使用尾插法并指定初始容量可以极大提升性能、甚至超过linkedList(原因是linkedList需要创建大量的node对象)
LinkedList
概念:基于链表,可以存储在分散的内存中,适合做数据插入及删除操作,不适合查询,需要逐一遍历。
遍历LinkedList必须使用iterator不能使用for循环,因为每次for循环体内通过get(i) 取得某一元素时都需要对list重新进行遍历,性能消耗极大。
另外不要试图使用indexOf等返回元素索引,并利用其进行遍历,使用indexOf对list进行了遍历,当结果为空时会遍历整个列表。
HashMap和HashTable有什么区别?其底层实现是什么?
区别:
(1)HashMap方法没有synchronized修饰,线程非安全的,HashTable线程安全;
(2)HashMap允许key和value为null,而HashTable不允许
底层实现:数组+链表实现
jdk8开始链表高度到8、数组长度超过64时,链表转变为红黑树,元素以内部类Node节点存在
- 计算key的hash值,二次hash然后对数组长度取模,对应数组下标
- 如果没有产生hash冲突(下标位置没有元素),则直接创建Node存入数组
- 如果产生hash冲突,先进行equal比较,相同则取代该元素,不同,则判断链表高度插入链表,链表高度达到8,并且数组长度到64则转变为红黑树,长度低于6则将红黑树转回链表
- key为null,存在下标0的位置
ConcurrentHashMap原理,jdk7和jdk8版本的区别
jdk7
数据结构:ReentrantLock+Segment(锁的是链表)+HashEntry,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。
元素查询:二次hash,第一次Hash定位到Segment,第二次Hash定位到元素所在的链表的头部
锁:Segment分段锁,Segment继承了ReentrantLock,锁定操作的Segment,其他的Segment不受影响,并发度get方法无需加锁,由volatile(可见性)保证。
jdk8
数据结构:synchronized(解决hash冲突,扩容等)+CAS(乐观锁)+Node+红黑树,Node的val和next都用volatile修饰,保证可见性;查找,替换,赋值操作都用CAS
锁:锁链表的head节点,不影响其他元素的读写,锁的粒度更细,效率更高,扩容时,阻塞所有的读写操作、并发扩容
读操作无锁:
Node的val和next使用volatile修饰,读写线程对该变量互相可见
数组用volatile修饰,保证扩容时被读线程感知
如何实现一个IOC容器
1.配置文件配置包扫描
2.递归包扫描获取.class文件
3.反射、确定需要交给IOC管理的类
4.对需要注入的类进行依赖注入
详细的回答:
配置文件中指定需要扫描的包路径
定义一些注解,分别表示访问控制层、业务服务层、数据持久层、依赖注入注解、获取配置文件注解
以配置文件中获取需要扫描的包路径,获取到当前路径下的文件信息,我们将当前路径下所有以.class结尾的文件添加到一个Set集合中进行存储
遍历这个Set集合,获取在类上指定注解的类,并将其交给IOC容器,定义一个安全的Map用来存储这些对象
遍历这个IOC容器,获取到每一个类的实例,判断里面是否具有依赖其他的类的实例,然后进行递归注入
什么是字节码?采用字节码的好处是什么?
在Java中,这种提供虚拟机理解的代码叫做字节码(也就是扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
特点:Java是编译和解释并存的特点
流程:Java源代码—>编译器—>jvm可执行的Java字节码(即虚拟指令)—>jvm(虚拟机)—>jvm中的解释器—>机器可执行的二进制机器码—>程序运行。
采用字节码的好处?跨平台
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且由于字节码并不针对一种特定的机器,因此,java程序无须重新编译便可在多种不同的计算机上运行。简单来说就是采用字节码的好处是具有跨平台性,一个程序只需写一次代码,便可以处处运行。
Java类加载器
JDK自带有的三个类加载器:bootstrapClassLoadeer、ExtClassLoader、AppClassLoader
bootstrapClassLoadeer是ExtClassLoader的父类加载器,默认负责加载%JAVA_HOME%lib的jar包和class文件。
ExtClassLoader是AppClassLoader的父类加载器,负责加载%JAVA_HOME%/lib/ext文件夹下的jar包和class类。
AppClassLoader是自定义类加载器的父类,负责加载classpath下的类文件。系统类加载器(加载程序员的代码以及jar包),线程上下文加载器(贯穿于类加载器,它们都可以去访问它)
双亲委托模型(双亲委派模型)
双亲委派模型的好处:
主要为了安全性,避免用户自己编写的类动态替换JAVA的一些核心类,比如String
同时避免了类的重复加载,因为jvm中区分不同类,不仅仅是根据类名,相同的class文件被不同ClassLoader加载就是不同的两个类
Java中的异常体系
Java中的所有异常都来自顶级父类Throwable
Throwable下有两个子类Exception和Error
Error是程序无法处理的错误,一旦出现这个错误,则程序将被迫停止运行。
Exception不会导致程序停止,又分为两个部分RuntimeException运行时异常和CheckedException检查异常。
RuntimeException常常发生在程序运行过程中,会导致程序当前线程执行失败。CheckedException常常发生在程序编译过程中,会导致程序编译不通过,简单来说就是有语法错误。