Java基础面试题

目录

1. Java语言有哪些特点

2. 面向对象和面向过程的理解

3. 面向对象三大特性

4. 抽象类和接口的对比

5. 重载(Overload)和重写(Override)的区别

6. hashCode 与 equals

7. == 和 equals 区别是什么

8. String,StringBuffer, StringBuilder 的区别是什么

9. String有哪些特性

10. String为什么要设计成不可变的

11. int和Integer的区别

12. 使用泛型的好处是什么

13. Error 和 Exception 区别是什么

14. ArrayList 和 LinkedList 的区别是什么

15. HashMap在jdk7中实现原理

16. HashMap在jdk8中相较于jdk7在底层实现方面的不同

17. HashMap 与 HashTable 有什么区别

18. ConcurrentHashMap 的实现原理是什么

19. ConcurrentHashMap的put方法执行逻辑是什么

20. ConcurrentHashMap 的 get 方法是否要加锁,为什么


1. Java语言有哪些特点

  • 是一种面对对象的语言,主要有三大特点,即封装、继承、多态

  • 跨平台性

2. 面向对象和面向过程的理解

  • 面对对象

    • 面向对象是把业务逻辑或者问题分解抽象为对象,分析需求中有哪些对象,分析每个对象各自需要做什么事情,通过对象完成各自部分功能实现,从而实现整体的一个业务逻辑,易于复用、扩展和维护

  • 面对过程

    • 面向过程分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了,需要做什么直接就开始干,比较直接高效,只思考事情的本身

3. 面向对象三大特性

  • 封装,也就是把客观事物封装成抽象的类,隐藏对象的属性和实现细节,仅对外提供公共访问方式,便于使用,提高复用性和安全性

  • 继承,存在于子父类关系中,主要不满足父类的功能实现,可以对父类的功能进行扩展,继承是多态的前提

  • 多态性,父类引用指向子类对象,提高了程序的拓展性,基于对象所属类的不同,外部对同一个方法的调用,实际执行的逻辑不同

4. 抽象类和接口的对比

  • 从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范

  • 备注:Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异

  • 现在,我们可以为接口提供默认实现的方法了,并且不用强制子类来实现它

参数抽象类接口
声明抽象类使用abstract关键字声明接口使用interface关键字声明
实现子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现子类使用implements关键字来实现接口。它需要提供接口中所有声明的方法的实现
构造器抽象类可以有构造器接口不能有构造器
访问修饰符抽象类中的方法可以是任意访问修饰符,默认修饰符是default接口方法默认修饰符是public。并且不允许定义为 private 或者 protected,1.8之后可以是default和static
多继承一个类最多只能继承一个抽象类一个类可以实现多个接口
字段声明抽象类的字段声明可以是任意的接口的字段默认都是 static 和 final 的

5. 重载(Overload)和重写(Override)的区别

  • 重载:发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同),与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分

  • 重写:发生在父子类中,方法名、参数列表必须相同,返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则);如果父类方法访问修饰符为private则子类中就不是重写

6. hashCode 与 equals

  • hashcode的作用是获取哈希码,返回的是一个int整数,就是确定的对象在哈希表中的索引位置,特点就是能够快速根据key检索出value,对象是在堆中,实际上就是维护了一张hash表,索引就是hashcode,这个方法属于object类中

  • 先通过比较hash值,然后比较equals方法,这个哈希码的作用是确定该对象在哈希表中的索引位置,这样提高了执行速度,以及工作效率,直接通过equals方法比较也可以,效率没有hashcode比较来的快,如果hashcode不同,这两个值肯定就不同,如果hashcode相同再去比较equals可以大大提高效率

  • 散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

  • 如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度

7. == 和 equals 区别是什么

  • ==常用于相同的基本数据类型之间的比较,也可用于相同类型的对象之间的比较;

    • 如果==比较的是基本数据类型,那么比较的是两个基本数据类型的值是否相等;

    • 如果==是比较的两个对象,那么比较的是两个对象的引用,也就是判断两个对象是否指向了同一块内存区域;

  • equals方法主要用于两个对象之间,检测一个对象是否等于另一个对象

    • 它的作用也是判断两个对象是否相等,般有两种使用情况:

    • 情况1,类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过“==”比较这两个对象。

    • 情况2,类覆盖了equals()方法。一般,我们都覆盖equals()方法来两个对象的内容相等;若它们的内容相等,则返回true(即,认为这两个对象相等)

8. String,StringBuffer, StringBuilder 的区别是什么

  • 可变性

    • String类中使用字符数组保存字符串,private final char value[],所以string对象是不可变的,string每次操作都是生成新的一个对象。

    • StringBuilder与StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串,char[] value,这两种对象都是可变的,都是在原对象上进行操作。

  • 线程安全性

    • String中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder是StringBuilder与StringBuffer的公共父类,定义了一些字符串的基本操作,如expandCapacity、append、insert、indexOf等公共方法。StringBuffer对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder并没有对方法进行加同步锁,所以是非线程安全的

  • 性能

    • 每次对String 类型进行改变的时候,都会生成一个新的String对象,然后将指针指向新的String 对象。StringBuffer每次都会对StringBuffer对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用StirngBuilder 相比使用StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险

  • 对于三者使用的总结

    • 如果要操作少量的数据用 = String单线程操作字符串缓冲区下操作大量数据 = StringBuilder多线程操作字符串缓冲区 下操作大量数据 = StringBuffer

9. String有哪些特性

  • 不变性:String 是只读字符串,是一个典型的 immutable 对象,对它进行任何操作,其实都是创建一个新的对象,再把引用指向该对象。不变模式的主要作用在于当一个对象需要被多线程共享并频繁访问时,可以保证数据的一致性。

  • 常量池优化:String 对象创建之后,会在字符串常量池中进行缓存,如果下次创建同样的对象时,会直接返回缓存的引用

  • final:使用 final 来定义 String 类,表示 String 类不能被继承,提高了系统的安全性

10. String为什么要设计成不可变的

1.便于实现字符串池(String pool)

在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。

2.使多线程安全

在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。

3.避免安全问题

在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。

4.加快字符串处理速度

由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String

11. int和Integer的区别

  • Integer是int的包装类;int是基本数据类型

  • Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值

  • Integer的默认值是null;int的默认值是0

  • 非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。因为非new生成的Integer变量指向的是静态常量池中cache数组中存储的指向了堆中的Integer对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的对象引用(地址)不同

  • 对于第4条的原因: java在编译Integer i = 100 ;时,会翻译成为Integer i = Integer.valueOf(100)。而java API中对Integer类型的valueOf的定义如下,对于-128到127之间的数,会进行缓存,Integer i = 127时,会将127这个Integer对象进行缓存,下次再写Integer j = 127时,就会直接从缓存中取,就不会new了

  • 这里面可以看到在-128到127之间是在常量池中取数据,但是在范围外面是进行new生成新的对象

12. 使用泛型的好处是什么

  • 每次使用时都需要强制转换成想要的类型

  • 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全

  • 类型安全

    • 泛型的主要目标是提高 Java 程序的类型安全

    • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常

13. Error 和 Exception 区别是什么

  • ava中,所有的异常都有一个共同的祖先java.lang包中的Throwable类。Throwable类有两个重要的子类 Exception(异常)和 Error(错误)。

  • Exception和Error二者都是 Java 异常处理的重要子类,各自都包含大量子类。

  • Exception:程序本身可以处理的异常,可以通过catch来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。

  • Exception又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常) 。 Error:Error属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复

14. ArrayList 和 LinkedList 的区别是什么

  • 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。

  • 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。

  • LinkedList 使用双向链表实现存储,按序号索引数据需要进行前向或后向遍历,但插入数据时只需要记录当前项的前后项即可,所以 LinkedList 插入速度较快

  • 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。

  • 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。

  • 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全

  • 综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList

15. HashMap在jdk7中实现原理

在实例化HashMap后,
底层创建了长度是16的一维数组Entry[] table
经过put操作添加数据
首先,计算key的哈希值,此哈希值经过hash和length-1做与操作计算以后,确定在数组中的存放的位置
 
情况1:如果此位置上的数据为空,此时的key-value添加成功
 
情况2:如果此位置上的数据不为空,代表着此位置上已存在一个或多个数据,通过链表形式存在,链表则是主要为了解决哈希冲突而存在的
依次比较key的HashCode:
如果哈希值与已经存在的数据的哈希值都不相同,此时数据添加成功
 
情况3:如果哈希值和已经存在的某一个数据的哈希值相同,继续比较:
key值通过equals方法进行比较:
如果equals()返回false:此时数据添加成功
如果equals()返回true:value值进行替代
 
补充:关于情况2和情况3:此时新添加的数据和原来的数据以链表的方式存储。
 
扩容:在不断的添加过程中,会涉及到扩容问题,当超出临界值(且要存放的位置非空)时,扩容。默认的扩容方式:扩容为原来容量的2倍,并将原的数据复制过来

16. HashMap在jdk8中相较于jdk7在底层实现方面的不同

  1. new HashMap():底层没创建一个长度为16的数组

  2. jdk 8底层的数组是:Node[],而非Entry[]

  3. 首次调用put()方法时,底层创建长度为16的数组

  4. jdk7底层结构:数组+链表。jdk8中底层结构:数组+链表+红黑树 4.1 形成链表时,七上八下(jdk7:新的元素指向旧的元素。jdk8:旧的元素指向新的元素) 4.2 当数组的某一个索引位置上的元素以链表形式存在的数据个数 > 8 且当前数组的长度 > 64时,此时此索引位置上的所数据改为使用红黑树存储。

  5. 长度低于6转换为链表

  6. 扩容机制:长度乘以扩容因子,将原来数据复制到新的数组上

17. HashMap 与 HashTable 有什么区别

  • 线程安全: HashMap 是非线程安全的,HashTable 是线程安全的;HashTable 内部的方法基本都经过 synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!)

  • 效率: 因为线程安全的问题,HashMap 要比 HashTable 效率高一点。另外,HashTable 基本被淘汰,不要在代码中使用它; 对Null key 和Null value的支持: HashMap 中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为 null。但是在 HashTable 中 put 进的键值只要有一个 null,直接抛NullPointerException

  • 初始容量大小和每次扩充容量大小的不同 : ①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。②创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为2的幂次方大小

  • 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制

  • 推荐使用:在 Hashtable 的类注释可以看到,Hashtable 是保留类不建议使用,推荐在单线程环境下使用 HashMap 替代,如果需要多线程使用则用 ConcurrentHashMap 替代

18. ConcurrentHashMap 的实现原理是什么

先来看下JDK1.7

  • JDK1.7中的ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成,即ConcurrentHashMap 把哈希桶切分成小数组(Segment ),每个小数组有n个HashEntry组成

  • 其中,Segment 继承了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色;HashEntry 用于存储键值对数据

img

首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问,能够实现真正的并发访问。

再来看下JDK1.8

  • 在数据结构上, JDK1.8 中的ConcurrentHashMap 选择了与 HashMap 相同的数组+链表+红黑树结构;在锁的实现上,抛弃了原有的 Segment 分段锁,采用CAS + synchronized实现更加低粒度的锁。

  • 将锁的级别控制在了更细粒度的哈希桶元素级别,也就是说只需要锁住这个链表头结点(红黑树的根节点),就不会影响其他的哈希桶元素的读写,大大提高了并发度。

img

19. ConcurrentHashMap的put方法执行逻辑是什么

先来看JDK1.7

  • 首先,会尝试获取锁,如果获取失败,利用自旋获取锁;如果自旋重试的次数超过 64 次,则改为阻塞获取锁。

  • 获取到锁后:

    • 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry。

    • 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。

    • 不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。 释放 Segment 的锁。

再来看JDK1.8

大致可以分为以下步骤:

  • 根据 key 计算出 hash值。

  • 判断是否需要进行初始化。

  • 定位到 Node,拿到首节点 f,判断首节点 f:

    • 如果为 null ,则通过cas的方式尝试添加。

    • 如果为 f.hash = MOVED = -1 ,说明其他线程在扩容,参与一起扩容。

    • 如果都不满足 ,synchronized 锁住 f 节点,判断是链表还是红黑树,遍历插入。

  • 当在链表长度达到8的时候,数组扩容或者将链表转换为红黑树。

20. ConcurrentHashMap 的 get 方法是否要加锁,为什么

  • get 方法不需要加锁。因为 Node 的元素 val 和指针 next 是用 volatile 修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。

  • 这也是它比其他并发集合比如 Hashtable、用 Collections.synchronizedMap()包装的 HashMap 安全效率高的原因之一。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    //可以看到这些都用了volatile修饰
    volatile V val;
    volatile Node<K,V> next;
}

21. 关于HashMap的时间复杂度

Hashmap链表和红黑树转换前置条件

HashMap在jdk1.8之后引入了红黑树的概念,表示若桶中链表元素超过8时,会自动转化成红黑树;

若桶中元素小于等于6时,树结构还原成链表形式。

原因

红黑树的平均查找长度是log(n),长度为8,查找长度为log(8)=3,链表的平均查找长度为n/2,当长度为8时,平均查找长度为8/2=4,这才有转换成树的必要;链表长度如果是小于等于6,6/2=3,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短。

还有选择6和8的原因是:

中间有个差值7可以防止链表和树之间频繁的转换。假设一下,如果设计成链表个数超过8则链表转换成树结构,链表个数小于8则树结构转换成链表,如果一个HashMap不停的插入、删除元素,链表个数在8左右徘徊,就会频繁的发生树转链表、链表转树,效率会很低。

不用管太多,就是在6和8的时候,就当做是链表和红黑树查找时间相同的界限

时间复杂度

不管插入还是查找,由key获取hash值然后定位到桶的时间复杂度都是O(1),那么真正决定时间复杂度的实际上是桶里面链表/红黑树的情况

如果桶里面没有元素,那么直接将元素插入/或者直接返回未查找到,时间复杂度就是O(1),如果里面有元素,那么就沿着链表进行遍历,时间复杂度就是O(n),链表越短时间复杂度越低,如果是红黑树的话那就是O(logn)

所以平均复杂度很难说,只能说在最优的情况下是O(1)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值