JAVA面试常考

本文深入解析Java中的HashMap与Hashtable、StringBuilder与StringBuffer的区别,探讨Java垃圾回收机制,以及super与this、抽象类与接口的概念,适合Java开发者进阶学习。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1.HashMap 和 Hashtable的区别

1.Hashtable是Java一开始发布时就提供的键值对映射的数据结构,HashMap产生于JDK1.2。

2.Hashtable是线程安全的,效率低,它的每个方法中都加入了Synchronize方法。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步。
虽然HashMap不是线程安全的,但是它的效率会比Hashtable要好很多。这样设计是合理的。在我们的日常使用当中,大部分时间是单线程操作的。HashMap把这部分操作解放出来了。当需要多线程操作的时候可以使用线程安全的ConcurrentHashMap。ConcurrentHashMap虽然也是线程安全的,但是它的效率比Hashtable要高好多倍。因为ConcurrentHashMap使用了分段锁,并不对整个数据进行锁定。

3.父类不同:Hashtable继承Dictionary(已经废弃),HashMap继承自AbstractMap类。

4.null值问题,Hashtable 不支持null key和null value。HashMap 中,null值可以作为键,这样的键只有一个;可以有一个或者多个键对应的value值为null。
因此在HashMap中不可以使用get()方法来判断HashMap中是否存在某个键,而应该用containsKey() 方法来判断。
//Map.get(里面只能跟key),因为key是唯一的,用key去找值value

5、遍历方式不同:Hashtable、HashMap都使用了Iterator。而由于历史原因,Hashtable还使用了Enumeration的方式 。

HashMap的Iterator是fail-fast迭代器。当有其它线程改变了HashMap的结构(增加,删除,修改元素),将会抛出ConcurrentModificationException。不过,通过Iterator的remove()方法移除元素则不会抛出ConcurrentModificationException异常。但这并不是一个一定发生的行为,要看JVM。

5、初始容量不同:Hashtable的初始长度是11,之后每次扩充容量变为之前的2n+1(n为上一次的长度);而HashMap的初始长度为16,之后每次扩充变为原来的两倍

创建时,如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。

6、计算哈希值的方法不同:为了得到元素的位置,首先需要根据元素的 KEY计算出一个hash值,然后再用这个hash值来计算得到最终的位置

Hashtable直接使用对象的hashCode。hashCode是JDK根据对象的地址或者字符串或者数字算出来的int类型的数值。然后再使用除留余数发来获得最终的位置。 然而除法运算是非常耗费时间的。效率很低

HashMap为了提高计算效率,将哈希表的大小固定为了2的幂,这样在取模预算时,不需要做除法,只需要做位运算。位运算比除法的效率要高很多。

HashTable与HashMap的区别联系
(1)HashTable和HashMap都实现了Map接口,但是HashTable的实现是基于Dictionary抽象类。

(2)在HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,既可以表示HashMap中没有该键,也可以表示该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键,而应该用containsKey()方法来判断。而在HashTable中,无论是key还是value都不能为null。

(3)HashTable是线程安全的,它的方法是同步了的,可以直接用在多线程环境中。HashMap则不是线程安全的。在多线程环境中,需要手动实现同步机制。

(4)底层实现细节不同:HashMap底层是数组 + 链表的形式,当阈值超过8,会转为数组+红黑树。HashMap初始化时,会建立一个16位的数组,数组内存储的是键值对,也就是entry,每个entry的在数组中存储的位置(下标值)通过key的hash值%数组的长度得到,如果有两个entry的下标值相同,则会将其插入在对应的链表头部;
计算index:hash(key.hashCode())%len
打个比方, 第一个键值对A进来,通过计算其key的hash得到的index=0,记做:Entry[0] = A。一会后又进来一个键值对B,通过计算其index也等于0,现在怎么办?HashMap会这样做:B.next = A,Entry[0] = B,如果又进来C,index也等于0,那么C.next = B,Entry[0] = C;这样我们发现index=0的地方其实存取了A,B,C三个键值对,他们通过next这个属性链接在一起。所以疑问不用担心。

在这里插入图片描述
扩容机制: HashMap内存储数据的Entry数组默认是16,如果没有对Entry扩容机制的话,当存储的数据一多,Entry内部的链表会很长,这就失去了HashMap的存储意义了。所以HasnMap内部有自己的扩容机制。HashMap内部有:
变量size,记录hashmap底层数组中已用槽的数量;
变量threshold,它是hashmap的阈值,用于判断是否需要调整hashmap的容量(threshold = 容量*加载因子),加载因子默认是0.75;
当size大于threshold时,默认扩容,也就是3/4的size被用完时,就会扩容,每次扩容为oldsize *2;

HashTable底层:数组+链表,初始化数组容量为11,下标索引则是key的hash值与0x7FFFFFFF进行与运算,然后再对tab.length取模
(最大整数值)
扩容:newsize = olesize*2+1
计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length

2、更好的选择:ConcurrentHashMap Java 5中新增了ConcurrentMap接口和它的实现类ConcurrentHashMap。 ConcurrentHashMap提供了和HashTable以及SynchronizedMap中所不同的锁机制。HashTable中采用的锁机制是一次锁住整个hash表,从而同一时刻只能有一个线程对其进行操作;而ConcurrentHashMap中则是一次锁住一个桶。ConcurrentHashMap默认将hash表分为16个桶,诸如get,put,remove等常用操作只锁住当前需要用到的桶。这样,原来只能一个线程进入,现在却能同时有16个写线程执行,并发性能的提升是显而易见的。 上面说到的16个线程指的是写线程,而读操作大部分时候都不需要用到锁。只有在size等操作时才需要锁住整个hash表。 在迭代方面,ConcurrentHashMap使用了一种不同的迭代方式。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是,在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator可以使用原来老的数据,而写线程也可以并发的完成改变。

2.StringBuilder 和 StringBuffer的区别

1.String是字符串常量,而StringBuffer 和StringBuilder均为字符串变量。String一旦创建之后,该对象是不可以更改的。但是StringBuffer和StringBuilder的对象是变量,是可以更改的。
JAVA中对String对象进行的操作实际上是一个不断new新的对象并且将旧的对象回收的一个过程,所以执行速度很慢。而StringBuilder和StringBuffer的对象是变量,对变量进行的操作就是直接进行更改,而不进行创建和回收,所以速度要比String快很多。

2.线程安全:StringBuilder是线程不安全的,而StringBuffer是线程安全的
如果一个StringBuffer对象在字符串缓冲区被多个线程使用时,StringBuffer中很多方法可以带有synchronized关键字,所以可以保证线程是安全的,但StringBuilder的方法则没有该关键字,所以不能保证线程安全,有可能会出现一些错误的操作。所以如果要进行的操作是多线程的,那么就要使用StringBuffer,但是在单线程的情况下,还是建议使用速度比较快的StringBuilder。

3.使用场景

String:适用于少量的字符串操作的情况

StringBuilder:适用于单线程下在字符缓冲区进行大量操作的情况

StringBuffer:适用多线程下在字符缓冲区进行大量操作的情况

3.java垃圾回收机制(GC)

C没有GC(Garbage collection) 机制,需要自己手动释放和分配内存,比较麻烦。
GC是发生在堆里面的(heap)–因为对象一般都创建在堆发生地点:一般发生在堆内存中,因为大部分的对象都储存在堆内存中。
GC:发现无用的对象,回收无用对象占用的内存空间。

  • 次数上频繁收集的Young区
  • 次数上较少收集的old区
  • 基本不动Perm区

对应四种常用算法:

  • 引用计数法(已经弃用):只要对象被引用,就是用计数器计数,当计数器为0时,就认为该对象是垃圾,但是如果遇到A引用B,B引用A的情况,计数器会一直计数,因此已经废弃。
    在这里插入图片描述
  • 复制算法(copying):将内存按容量分为大小相等的两块,每次只使用其中的一块(对象面),当这一块的内存用完了,就将还存活着的对象复制到另外一块内存上面(空闲面),然后再把已使用过的内存空间一次清理掉。(发生在年轻代)
    在这里插入图片描述
  • 标记—清除算法(Tracing Collector):分为“标记”和“清除”两个阶段:首先标记出所需回收的对象,在标记完成后统一回收掉所有被标记的对象,它的标记过程其实就是前面的根搜索算法中判定垃圾对象的标记过程。(主要针对的是老年代)
    在这里插入图片描述

-标记—整理算法 (Compacting Collector):标记的过程与标记—清除算法中的标记过程一样,但对标记后出的垃圾对象的处理情况有所不同,它不是直接对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。在基于Compacting算法的收集器的实现中,一般增加句柄和句柄表。(也适用于老年区)
在这里插入图片描述

  • 标记清除压缩算法(Adaptive Collector):监控当前堆的使用情况,并将选择适当算法的垃圾收集器,当进行多次标记清楚之后再进行一次压缩
    在这里插入图片描述
    (堆内存为了配合垃圾回收有什么不同区域划分,各区域有什么不同?)

Java的堆内存基于Generation算法(Generational Collector)划分为新生代、年老代和持久代。新生代又被进一步划分为Eden和Survivor区,最后Survivor由FromSpace(Survivor0)和ToSpace(Survivor1)组成。所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制。分代收集基于这样一个事实:不同的对象的生命周期是不一样的。因此,可以将不同生命周期的对象分代,不同的代采取不同的回收算法进行垃圾回收(GC),以便提高回收效率。

GC可达性分析算法:GC可达性分析算法

4. if语句比较int boolean float

5.Java的特性–继承/多态/封装

5.1 封装

封装:遵循“开闭原则”,就是将类的某些信息隐藏在类内部,不允许外部程序直接访问,而是通过该类提供的方法来实现对隐藏信息的操作和访问,比如getter,setter;

5.2 继承

继承是类与类之间的一种关系,子类拥有父类的所有属性和方法(除了private修饰的属性),从而实现代码的复用。

继承与实现的区别:
(1)概念不同:
继承:子类与父类的继承。如果多个类的某个部分的功能相同,那么可以抽象出一个类出来,把他们相同的部分都放到父类里,让他们都继承这个类。
实现:接口的实现。如果多个类都有一个行为,但是处理的方法不同,那么就定义一个标准,让各个类实现这个接口,各自实现自己具体的处理方法。
(2)关键词不同:
继承:extends 实现:implements
(3)数量不同:单继承,多实现。
(4)属性不同:某个接口被实现时,实现类一定要实现(重写)接口中的抽象方法,而继承不需要。

5.3 多态

JAVA中的多态主要指引用多态和方法多态。

引用多态是指:父类引用可以指向本类对象,也可以指向子类对象。引用多态的强大主要体现在强调属性、方法时,可以根据具体的对象去调用,例如:子类重写了父类方法。例如Map<Integer,Integer> map = new HashMap<>();

方法多态:主要是重写和重载。
重写是父类与子类之间多态性的一种表现,而重载可以理解称为多态的具体表现形式。
子类可以重写父类的方法。在调用方法时根据指向的子类对象决定调用哪个具体的方法。
方法多态的强大主要体现在可以根据调用时参数的不同,而自主匹配调用的方法,例如重载。

Java实现多态有三个必要条件:继承、重写、向上转型。

继承:在多态中必须存在有继承关系的子类和父类。

重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。

向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。

Java中有两种形式可以实现多态,继承和接口:

基于继承的实现机制主要表现在父类和继承该父类的一个或多个子类对某些方法的重写,多个子类对同一方法的重写可以表现出不同的行为。

基于接口的多态中,指向接口的引用必须是指定这实现了该接口的一个类的实例程序,在运行时,根据对象引用的实际类型来执行对应的方法。

6.super 和 this

super和this的含义
super :代表父类的存储空间标识(可以理解为父亲的引用)。
this :代表当前对象的引用(谁调用就代表谁)。

7. 抽象类和接口的区别

抽象类的由来:父类中的方法,被他的子类所重写,子类各自的实现都不尽相同。那么父类的方法声明和方法主体,只有声明还有意义,方法主题已经没有存在的意义了。我们把没有方法主体的方法称为抽象方法。Java语言规定,包含抽象类的方法就是抽象类。

没有方法体的方法就是抽象方法。

接口实际上是特殊的抽象类。而抽象类除了你不能实例化抽象类之外,其他的没有任何区别

抽象类接口
关键字是abstract class关键字是Interface
继承抽象类的关键字是extends实现接口的关键字是implements
继承是单继承接口是多实现,当然接口之间可以多继承
抽象类中可以有构造方法接口不可以有构造方法
抽象类可以有成员变量接口只可以有常量
抽象类中可以有成员方法接口中只可以有抽象方法
抽象类中增加方法不影响子类接口中增加方法通常都影响子类

从JDK1.8开始允许接口中出现非抽象方法,但需要用default关键字去修饰。

final关键字

在Java中,final关键字可以用来修饰类、方法和变量(包括成员变量和局部变量)。
当用final修饰一个类时,表明这个类不能被继承。也就是说,如果一个类你永远不会让他被继承,就可以用final进行修饰。final类中的成员变量可以根据需要设为final,但是要注意final类中的所有成员方法都会被隐式地指定为final方法。

当final修饰方法时,方法不能被重写。使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升。在最近的Java版本中,不需要使用final方法进行这些优化了。“

因此,如果只有在想明确禁止该方法在子类中被覆盖的情况下才将方法设置为final的。即父类的final方法是不能被子类所覆盖的,也就是说子类是不能够存在和父类一模一样的方法的。

final成员变量表示常量,只能被赋值一次,赋值后值不再改变。
当final修饰一个基本数据类型时,表示该基本数据类型的值一旦在初始化后便不能发生变化;如果final修饰一个引用类型时,则在对其初始化之后便不能再让其指向其他对象了,但该引用所指向的对象的内容是可以发生变化的。本质上是一回事,因为引用的值是一个地址,final要求值,即地址的值不发生变化。
final修饰一个成员变量(属性),必须要显示初始化。这里有两种初始化方式,一种是在变量声明的时候初始化;第二种方法是在声明变量的时候不赋初值,但是要在这个变量所在的类的所有的构造函数中对这个变量赋初值。

equals 和 ==

Java数据类型分为基本数据类型和引用数据类型。
对于基本数据类型来说,只能用 == 进行基础数值的比较;对于引用数据类型来说,等等 比较的是引用的实例对象是否相等, 而equals则在于是否被重写,没被重写就和等等的比较规则一样,被重写了则按照重写的规则来定义。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值