Java高频面试题精选(最新)
java中的编译器和解释器Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平
1 == 和equals的区别
总结:==对手基本类型来说是值比较,对于引用类型来说是比较的是引用;而equals 默认情况下是引用比较,只是很多类重新了equals方法,比如String,Integer 等把它变成了值比较,所以一般情况下equals比较的是值是否相等。
2 final在java中有什么作用
final修饰的类叫最终类,该类不能被继承。final修饰的方法不能被重写。final修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。
3 八种基本数据类型
基础类型有8种:
- byte:1字节
- boolean:2字节
- char:2字节
- short:2字节
- int:4字节
- float:4字节
- long:8字节
- double:8字节
4 String、StringBuffer、StringBuilder区别
String 和StringBuffer、StringBuilder 的区别在于String声明的是不可变的对象,位于字符串常量池中,每次操作都会生成新的String 对象,然后将指针指向新的String对象,而StringBuffer、StringBuilder可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用String。
StringBuffer 和StringBuilder最大的区别在于,StringBuffer 是线程安全的,而StringBuilder 是非线程安全的,但StringBuilder 的性能却高于StringBuffer,所以在单线程环境下推荐使用StringBuilder,多线程环境下推荐使用StringBuffer。
5 String str="i"与String str=new String(“i”)一样吗
不一样,因为内存的分配方式不一样。String str = "i"的方式,java虚拟机会将其分配到常量池中;而String str=new String(“i”)则会被分到堆内存中。
6 普通类和抽象类有哪些区别
普通类不能包含抽象方法,抽象类可以包含抽象方法。抽象类不能直接实例化,普通类可以直接实例化。
包含抽象方法的类一定是抽象类
抽象类可以没有抽象方法
7 抽象类能使用final修饰吗
不能,定义抽象类就是让其他类继承的,如果定义为final该类就不能被继承,这样彼此就会产生矛盾,所以final不能修饰抽象类,如下图所示,编辑器也会提示错误信息︰
8 接口和抽象类的区别
实现:抽象类的子类使用extends来继承;接口必须使用implements来实现接口。
构造函数:抽象类可以有构造函数;接口不能有。
main方法:抽象类可以有main方法,并且我们能运行它;接口不能有main方法。
实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
访问修饰符:接口中的方法默认使用public修饰;抽象类中的方法可以是任意访问修饰符。
9 BIO、NIO、AIO有什么区别
BIO:Block lO同步阻塞式IO,就是我们平常使用的传统IO,它的特点是模式简单使用方便,并发处理能力低。
NlO:New lO同步非阻塞IO,是传统IO的升级,客户端和服务器端通过Channel(通道)通讯,实现了多路复用。
AIO:Asynchronous-lO是NIO的升级,也叫NIO2,实现了异步非堵塞IO,异步IO_的操作基于事件和回调机制。
10 Array 与ArrayList有什么区别
1、ArrayList 与LinkedList都实现了List接口。
2、ArrayList是线性表,底层是使用数组实现的,它在尾端插入和访问数据时效率较高,
3、Linked是双向链表,他在中间插入或者头部插入时效率较高,在访问数据时效率较低
11 数据库连接池的作用
数据库连接是非常消耗资源的,影响到程序的性能指标。连接池是用来分配,管理,释放数据库连接的,可以使应用程序重复使用同一个数据库连接,而不是每次都创建一个新的数据库连接。通过释放空闲时间较长的数据库连接避免数据库因为创建太多的连接而造成的连接遗漏问题,提高了程序性能。
12 final、finalize()finally,作用
1、final为用于标识常量的关键字,final标识的关键字存储在常量池中(在这里final常量的具体用法将在下面进行介绍)。
2、finalize()方法在Object中进行了定义,用于在对象“消失”时,由JVM进行调用用于对对象进行垃圾回收,类似于C++中的析构函数;用户自定义时,用于释放对象占用的资源(比如进行I/O操作)。
3、 finally{}用于标识代码块,与try{}进行配合,不论try中的代码执行完或没有执行完(这里指有异常)。该代码块之中的程序必定会进行。
13 线程同步的方法
-
wait():让线程等待。将线程存储到一个线程池中。
-
notify():唤醒被等待的线程。通常都唤醒线程池中的第一个。让被唤醒的线程处于临时阻塞状态。
-
notifyAll():唤醒所有的等待线程。将线程池中的所有线程都唤醒。
14 线程和进程的概念及区别
进程是系统进行资源分配和调度的一个独立单位,线程是CPU调度和分派的基本单位
1、一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
2、资源分配给进程,同—进程的所有线程共享该进程的所有资源。
3、线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
4、线程是指进程内的一个执行单元,也是进程内的可调度实体。
区别:
1、调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
2、并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
3、拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
4、系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。钱程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。
15 &和&&的区别
&是位运算符。&&是布尔逻辑运算符,在进行逻辑判断时用&处理的前面为false后面的内容仍需处理,用&&处理的前面为false不再处理后面的内容。
16 重写和重载
-
Overload为重载,Override为重写
方法的重写和重载是Java多态性的不同表现。重写是父类与子类之间多态性的一种表现,重载是一个类中多态性的一种表现。
-
如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写(Override)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被"屏蔽"了。
-
如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overload)。
(举例)
17 什么是字节码?采用字节码的最大好处是什么?
-
java中的编译器和解释器:
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟的机器。这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。在Java中,这种供虚拟机理解的代码叫做字节码(即扩展名为.class的文件),它不面向任何特定的处理器,只面向虚拟机。每一种平台的解释器是不同的,但是实现的虚拟机是相同的。Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每―条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。
Java源代码---->编译器---->jvm可执行的Java字节码(即虚拟指令)---->jvm–>jvm 中解释器----->机器可执行的二进制机器码->程序运行。
-
采用字节码的好处
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以Java程序运行时比较高效,而且,l由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
18 什么是JDK?什么是JRE?
JDK:顾名思义它是给开发者提供的开发工具箱,是给程序开发者用的。它除了包括完整的JRE (Java Runtime Enyironment) , Java运行环境,还包含了其他供开发者使用的工具包。
JRE:普通用户而只需要安装JRE (Java Runtime Environment)来来运行Java程序。而程序开发者必须安装JDK来编译、调试程序。
19 环境变量Path和ClassPath的作用是什么? 如何设置这两个环
境变量?
20 字符型常量和字符串常量的区别
-
形式上:字符常量是单引号引起的一个字符字符串常量是双引号引起的若干个字符
-
合义上:字符常量相当于个整形值(ASCII,值),可以参加表达式运算字符串常量代表一个地址值(该字符串在内存中存放位置)
-
占内存大小字符常量只占一个字节字符串常量占若干个学节(至少一个字符结束标志)
21 构造器Constructor是否可被override
在讲继承的时候我们就知道父类的私有属性和构造方法并不能被继承,所以Constructor也就不能被 override,但是可以overload,所以你可以看到一个类中有多个构造函数的情况。
22 自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来。
拆箱:将包装类型转换为基本数据类型。
23 在一个静态方法内调用一个非静态成员为什么是非法的?
由于静态方法可以不通过对象进行调用。因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
24 静态方法和实例方法的区别
-
在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用"对象名.方法名"的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。
-
静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制
25 在Java中定义一个不做事且没有参数的构造方法的作用
Java程序在执行子类的构造方法之前,如果没有用super()来调用父类特定的构造方法,则会调用父类中“无参构造器”。因此,如果父类中只定义了有参数的构造方法,而在子类的构造方法中又没有用super()来调用父类中特定的构造方法,则编译时将发生错误,因为Java程序在父类中找不到无参构造器可供执行。因为不写构造器默认会创建无参构造器,但如果创建了个有参构造器,则不会默认创建无参构造器,需要手动添加。解决办法是在父类里加上个无参构造器。
26 成员变量与局部变量的区别有哪些
1、从语法形式上,看成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数。成员变量可以被public,private,static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰。成员变量和局部变量都能被final所修饰;
2、从变量在内存中的存储方式来看,成员变量是对象的一部分,而对象存在于堆内存,局部变量存在于栈内存(栈帧)。
3、从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
4、成员变量如果没有被赋初值,则会自动以类型的默认值而赋值(―种情况例外被final修饰但没有被static修饰的成员变量必须显示地赋值);而局部变量则不会自动赋值。
27 构造方法有哪些特性
1、名字与类名相同。
2、没有返回值,但不能用void声明构造函数。
3、生成类的对象时自动执行,无需调用。
28 在多线程环境下,SimpleDateFormat是线程安全的吗
不是,非常不幸,DateFormat 的所有实现,包括SimpleDateFormat都不是线程安全的,因此你不应该在多线程序中使用,除非是在对外线程安全的环境中使用,如将SimpleDateFormat限制在ThreadLocal 中。如果你不这么做,在解析或者格式化日期的时候,可能会获取到一个不正确的结果。因此,从日期、时间处理的所有实践来说,推荐joda-time库。
29 什么是“依赖注入”和“控制反转”
控制反转(IOC)
30 构造器注入和setter依赖注入,那种方式更好
每种方式都有它的缺点和优点。构造器注入保证所有的注入都被初始化,但是setter 注入提供更好的灵活性来设置可选依赖。如果使用XML来描述依赖,Setter注入的可读写会更强。经验法则是强制依赖使用构造器注入,可选依赖使用setter 注入。
31 依赖注入和工程模式之间有什么不同
虽然两种模式都是将对象的创建从应用的逻辑中分离,但是依赖注入比工程模式更清晰。通过依赖注入,你的类就是POJO,它只知道依赖而不关心它们怎么获取。使用工厂模式,你的类需要通过工厂来获取依赖。因此,使用Dl会比使用工厂模式更容易测试。
32 适配器模式和装饰器模式有什么区别
虽然适配器模式和装饰器模式的结构类似,但是每种模式的出现意图不同。
适配器模式被用于桥接两个接口,而装饰模式的目的是在不修改类的情况下给类增加新的功能。
33 继承和组合之间有什么不同
虽然两种都可以实现代码复用,但是组合比继承共灵活,因为组合允许你在运行时选择不同的实现。用组台实现的代码也比继承测试起来更加简单。
34 Java中,嵌套公共静态类与顶级类有什么不同
类的内部可以有多个嵌套公共静态类,但是一个Java源文件只能有一个顶级公共类,并且顶级公共类的名称与源文件名称必须一致。
35 OOP中的组合、聚合和关联有什么区别
如果两个对象彼此有关系,就说他们是彼此相关联的。组合和聚合是面向对象中的两种形式的关联。组合是一种比聚合更强力的关联。组合中,一个对象是另一个的拥有者,而聚合则是指一个对象使用另一个对象。如果对象A是由对象B组合的,则A不存在的话,B一定不存在,但是如果A对象聚合了一个对象B,则即使A不存在了,B也可以单独存在。
问:组合、聚合、关联是什么?
36 举一个符合开闭原则的设计模式的例子
开闭原则要求你的代码对扩展开放,对修改关闭。这个意思就是说,如果你想增加一个新的功能,你可以很容易的在不改变已测试过的代码的前提下增加新的代码。有好几个设计模式是基于开闭原则的,如策略模式,如果你需要一个新的策略,只需要实现接口,增加配置,不需要改变核心逻辑。一个正在工作的例子是Collections.sort()方法,这就是基于策略模式,遵循开闭原则的,你不需为新的对象修改sort()方法,你需要做的仅仅是实现你自己的 Comparator接口。
37 什么是B/S架构?什么是C/S架构
-
B/S(Browser/Server),浏览器/服务器程序
-
C/S(Client/Server),客户端/服务端,桌面应用程序
38 你所知道网络协议有那些
-
HTTP:超文本传输协议
-
FTP:文件传输协议
-
SMPT:简单邮件协议
-
TELNET:远程终端协议
-
POP3:邮件读取协议
39 Java都有那些开发平台
-
JAVA SE:主要用在客户端开发
-
JAVA EE:主要用在web 应用程序开发
-
JAVA ME:主要用在嵌入式应用程序开发
40 Static关键字有什么作用
Static可以修饰内部类、方法、变量、代码块
Static修饰的类是静态内部类
Static修饰的方法是静态方法,表示该方法属于当前类的,而不属于某个对象的,静态方法也不能被重写,可以直接使用类名来调用。在 static方法中不能使用this或者super关键字。
Static修饰变量是静态变量或者叫类变量,静态变量被所有实例所共享,不会依赖于对象。静态变量在内存中只有一份拷贝,在JVM加载类的时候,只为静态分配一次内存。
Static修饰的代码块叫静态代码块,通常用来做程序优化的。静态代码块中的代码在整个类加载的时候只会执行一次。静态代码块可以有多个,如果有多个,按照先后顺序依次执行。
41 创建一个子类对象的时候,那么父类的构造方法会执行吗
会执行。当创建一个子类对象,调用子类构造方法的时候,子类构造方法会默认调用父类的构造方法。
42 什么是父类引用指向子类对象
是java多态―种特殊的表现形式。创建父类引用,让该引用指向一个子类的对象。
例如:父类Animal,子类Cat,Dog。其中Animal可以是类也可以是接口,Cat和Dog是继承或实现Animal的子类。
Animal animal = new Cat();
若子类重写了父类方法和属性,访问的是父类的属性,调用的是子类的方法
43 集合的属性结构图
set集合是无序的
44 Map
-
HashMap: JDK1.8之前HashMap由数组+链表组成的,数组是 HashMap的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)
JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
-
LinkedHashMap: LinkedHashMap 继承自HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap在
上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
-
HashTable:数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的
-
TreeMap:红黑树(自平衡的排序二叉树)
45 哪些集合类是线程安全的
- Vector:就比Arraylist多了个synchronized(线程安全),因为效率较低,现在已经不太建议使用。
- hashTable:就比 hashMap多了个synchronized(线程安全),不建议使用。
- ConcurrentHashMap:是Java5中支持高并发、高吞吐量的线程安全HashMap实现。它由Segment数组结构和HashEntry数组结构组成。Segment数组在ConcurrentHashMap里扮演锁的角色,HashEntry 则用于存储键-值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构;个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素;每个Segment守护着一个HashEntry数组里的元素,当对 HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。(推荐使用)
46 ArrayList和LinkedList的区别是什么
-
数据结构实现:ArrayList是动态数组的数据结构实现,而LinkedList是双向链表的数据结构实现。
-
随机访问效率:ArrayList 比 LinkedList在随机访问的时候效率要高,因为LinkedList是线性的数据存储方式,所以需要移动指针从前往后依次查找。
-
增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList效率要高,因为ArrayList增删操作要影响数组内的其他数据的下标。
-
内存空间占用:LinkedList比 ArrayList更占内存,因为LinkedList的节点除了存储数据,还存储了两个引用,汇个指向前一个元素,一个指向后一个元素。
-
线程安全:ArrayList和LinkedList都是不同步的,也就是不保证线程安全。
-
综合来说,在需要频繁读取集合中的元素时,更推荐使用ArrayList,而在插入和删除操作较多时,更推荐使用LinkedList。
-
LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
47 List和Set的区别
List , Set都是继承自Collection接口
-
List 特点:一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有ArrayList、LinkedList和Vector。
-
Set特点:一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null 元素,必须保证元素唯—性。Set接口常用实现类是HashSet、LinkedHashSet以及TreeSet。
-
另外List支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。
Set和 List 对比
-
Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
-
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变。
48 说一下 HashSet的实现原理
HashSet是基于HashMap 实现的,HashSet的值存放于HashMap的key 上,HashMap的value统一为present,因此 HashSety的实现比较简单,相关HashSet的操作,基本上都是直接调用底层HashMap 的相关方法来完成,HashSet不允许重复的值。
HashSet的add源码:
// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();
public boolean add(E e) {
return map.put(e, PRESENT)==null;
}
49 HashSet如何检查重复?HashSet是如何保证数据不可重复的
-
向HashSet 中 add ()元素时,判断元素是否存在的依据,不仅要比较hash值,同时还要结合equles方法比较。
-
HashSet中的add()方法会使用HashMap的put()方法,所以就很好理解了,HashMap 的 key是唯一的,HashSet就一定也是唯一的。
-
由源码可以看出HashSet添加进去的值就是作为HashMap 的 key,并且在HashMap 中如果KV相同时,会用新的V覆盖掉旧的V,然后返回旧的V。所以不会重复( HashMap比较key是否相等是先比较hashcode再比较equals ) 。
50 hashCode与equals的相关规定
-
如果两个对象相等,则 hashcode一定也是相同的,hashCode是jdk根据对象的地址或者字符串或者数字算出来的int类型的数值。
-
两个对象相等,对两个equals方法返回true。
-
两个对象有相同的hashcode 值,它们也不一定是相等的。因为在散列表中,hashCode()相等,即两个键值对的哈希值相等。然而哈希值相等,并不一定能得出键值对相等,此时就出现所谓的哈希冲突场景。
-
综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
-
hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
总结:
- 如果hashcode不同,则两对象一定不同
- 如果hashcode相同,对象不一定相同,此时如果equals也相同,才相同
所以会先去比较hashcode,如果不同则直接false,如果相同,再比较equals,看相不相同。
https://blog.csdn.net/weixin_44664277/article/details/104289327
待补充
hashCode和hash算法
51 HashSet和HashMap的区别
52 HashMap在JDK1.7和JDK1.8中有哪些不同
底层实现:
在Java中,保存数据有两种比较简单的数据结构:数组和链表。数组的特点是:寻址容易,插入和删除困难。链表的特点是:寻址困难,但插入和删除容易;所以我们将数组和链表结合在一起,发挥两者各自的优势,使用一种叫做拉链法的方式可以解决哈希冲突。
HashMap JDK1.8之前
JDK1.8之前采用的是拉链法。拉链法:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
HashMap JDK1.8之后
相比于之前的版本,jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
53 JDK1.7 VS JDK1.8比较,针对于HashMap
1、resize_扩容优化
2、_引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
3、解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
54 什么是红黑树
说道红黑树先讲什么是二叉树,二叉树简单来说就是每一个节上可以关联俩个子节点
红黑树
1、红黑树是―种特殊的二叉查找树,红黑树的每个结点上都有存储位表示结点,的颜色,可以是红(Red)或黑(Black)。
2、红黑树的每个结点是黑色或者红色。当是不管怎么样他的根结点是黑色。每个叶子结点(叶子结点代表终结、结尾的节点)也是黑色
注意:这里叶子结点,是指为空(NIL或NULL)的叶子结点!
3、如果一个结点是红色的,则它的子结点必须是黑色的。
4、每个结点到叶子结点NIL所经过的黑色结点的个数一样的。
5、红黑树的基本操作是添加、删除。在对红黑树进行添加或删除之后,都会用到旋转方法。因为添加或删除红黑树中的结点之后,红黑树的结构就发生了变化,可能不满足上面三条性质,也就不再是一颗红黑树了,而是一颗普通的树。而通过旋转和变色,可以使这颗树重新成为红黑树。简单点说,旋转和变色的目的是让树保持红黑树的特性。
55 HashMap的put方法具体流程
1、判断键值对数组table[i]是否为空或为null,否则执行**resize()**进行扩容。
2、根据键值key计算hash值得到插入的数组索引 i,如果table[i]==null,直接新建节点添加,转向⑥,如果table[i]不为空,转向③。
(此时是在数组中查找有没有空位,如果有,则直接插入该位置,如果没有往下链)
3、判断table[i]的首个元素是否和key—样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals 。
(首个元素使数组中的元素,因为map特性,不允许有重复的key)
4、判断table[i]是否为treeNode,即 table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向⑤。
(此时说明已经发生了哈希冲突但还不确定现在是链表还是红黑树)
5、遍历table[li]/判断链表长度是否大于8(默认阈值为8)大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现 key已经存在直接覆盖value即可。
6、插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(临界值),如果超过,进行扩容。
源码如下:
public V put(K key, V value) {
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
//【1】 若未初始化先进行扩容
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
/**
* 【2】 计算index,并对null做处理
* l/(n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中)
*/
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
//数组位已存在
Node<K,V> e; K k;
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
// 【3】 判断key是否已经存在,存在则覆盖
e = p;
else if (p instanceof TreeNode)
// 【4】 判断是否是红黑树,如果是则插入红黑树
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//为链表
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
//此判断是获取到尾节点,在尾节点后插入
p.next = newNode(hash, key, value, null);
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
// 超过阈值转,链表结构转红黑树结构
treeifyBin(tab, hash);
break;
}
//遍历过过程中如果有相同的key,则跳出循环
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
}
if (e != null) { // existing mapping for key
V oldValue = e.value;
if (!onlyIfAbsent || oldValue == null)
e.value = value;
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
if (++size > threshold)
//加完后如果超过阈值,扩容
resize();
afterNodeInsertion(evict);
return null;
}
56 HashMap的扩容操作是怎么实现的?
1、在jdk1.8中resize方法是在 hashmap中的键值对大于阀值时或者初始化时,就调用resize方法进行扩容。
2、每次扩展的时候,都是扩展2倍。
3、扩展后Node对象的位置要么在原位置,要么移动到原偏移量两倍的位置。
在putVal()中,我们看到在这个函数里面使用到了2次resize()方法,resize()方法表示的在进行第一次初始化时会对其进行扩容,或者当该数组的实际大小大于其临界值(第一次为12),这个时候在扩容的同时也会伴随着桶上面的元素进行重新分发,这也是JDK1.8版本的一个优化的地方,在1.7中,扩容之后需要重新去计算其Hash 值,根据Hash值对其进行分发,但在1.8版本中,则是根据在同一个桶的位置中进行判断(e.hash & oldCap)是否为0,重新进行 hash分配后,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
int oldCap = (oldTab == null) ? 0 : oldTab.length;
int oldThr = threshold;
int newCap, newThr = 0;
if (oldCap > 0) {
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
}
else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
if (newThr == 0) {
float ft = (float)newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
(int)ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes","unchecked"})
Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
table = newTab;
if (oldTab != null) {
for (int j = 0; j < oldCap; ++j) {
Node<K,V> e;
if ((e = oldTab[j]) != null) {
oldTab[j] = null;
if (e.next == null)
newTab[e.hash & (newCap - 1)] = e;
else if (e instanceof TreeNode)
((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
else { // preserve order
Node<K,V> loHead = null, loTail = null;
Node<K,V> hiHead = null, hiTail = null;
Node<K,V> next;
do {
next = e.next;
if ((e.hash & oldCap) == 0) {
if (loTail == null)
loHead = e;
else
loTail.next = e;
loTail = e;
}
else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
return newTab;
}
57 HashMap是怎么解决哈希冲突的
注:在解决这个问题之前,我们首先需要知道什么是哈希冲突,而在了解哈希冲突之前我们还要知道什么是哈希。
什么是哈希?
Hash一般翻译为“散列”,也有直接音译为“哈希”的,Hash就是指使用哈希算法是指把任意长度的二进制映射为固定长度的较小的二进制值,这个较小的二进制值叫做哈希值。
什么是哈希冲突?
当两个不同的输入值,根据同―散列函数计算出相同的哈希值的现象,我们就把它叫做碰撞(哈希碰撞)
58 简单总结一下HashMap是使用了哪些方法来有效解决哈希冲突的:
1、链表法就是将相同hash 值的对象组织成一个链表放在hash 值对应的槽位。
2、开放地址法是通过一个探测算法,当某个槽位已经被占据的情况下继续查找下一个句以使用的槽位。
59 为什么HashMap中String、Integer这样的包装类适合作为key
String,lnteger等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率
1、都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
2、内部已重写了equals(). hashCode()等方法,遵守了HashMap内部的规范(不清楚可以去上面看看putValue 的过程),不容易出现Hash 值计算错误的情况。
60 HashMap 与HashTable有什么区别
-
线程安全:HashMap 是非线程安全的,HashTable是线程安全的;HashTable_内部的方法基本都经过synchronized修饰。
-
效率:因为线程安全的问题,HashMap要比 HashTable效率高一点。另外,HashTable基本被淘汰,不要在代码中使用它;(如果你要保证线程安全的话就使用ConcurrentHashMap ) 。
-
对Null key 和 Null value 的支持HashMap中,null可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null。但是在HashTable中put进的键值只要有一个nul,直接抛NullPointerException。
-
初始容量大小和每次扩充容量大小的不同:
- 创建时如果不指定容量初始值,Hashtable默认的初始大小为11%之后每次扩充,容量变为原来的2n+1。HashMap 默认的初始化大小为16。之后每次扩充,容量变为原来的2倍。
- 创建时如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小。也就是说HashMap总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。
JDK1.8以后的 HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。
61 什么是TreeMap
1、TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
2、TreeMap基于红黑树(Red-Black tree)实现,该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的Comparator进行排序,具体取决于使用的构造方法。
3、TreeMap是线程非同步的。
62 HashMap和ConcurrentHashMap 的区别
1、ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每—个分段上都用lock锁进行保护,相对于HashTable 的 synchronized锁的粒度更精细了一些,并发性能更好,而 HashMap没有锁机制,不是线程安全的。(JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法。)
2、HashMap的键值对允许有 null,但是ConCurrentHashMap 都不允许。
63 ConcurrentHashMap 和Hashtable 的区别
ConcurrentHashMap 和 Hashtable的区别主要体现在实现线程安全的方式上不同。
-
ConcurrentHashMap:在JDK1.7的时候,ConcurrentHashMap分段锁对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中―部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比 Hashtable 效率提高16倍)
到了JDK1.8的时候已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用synchronized和CAS来操作。(JDK1.6以后对synchronized锁做了很多优化)整个看起来就像是优化过且线程安全的 HashMap,虽然在JDK1.8中还能看到Segment 的数据结构,但是已经简化了属性,只是为了兼容旧版本。
-
Hashtable(同一把锁):使用synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用put添加元素,另一个线程不能使用put添加元素,也不能使用get,竞争会越来越激烈效率越低。
可以看出ConcurrentHashMap锁的粒度更细了
64 ConcurrentHashMap底层具体实现知道吗?实现原理是什么?
JDK1.7
1、首先将数据分为一段―段的存储,然后给每―段数据配—把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
2、在JDK1.7中,ConcurrentHashMap 采用Segment + HashEntry的方式进行实现,结构如下:
3、一个ConcurrentHashMap里包含一个Segment数组。Segment的结构和HashMap 类似,是一种数组和链表结构,一个Segment包含一个HashEntry 数组,每个HashEntry是一个链表结构的元素,每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得对应的Segment的锁。
JDK1.8
在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS +Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
65 volatile修饰符的有过什么实践?
一种实践是用volatile修饰 long和double变量,使其能按原子类型来读写。double 和long 都是64位宽。因此对这两种类型的读是分为两部分的,第一次读取第一个32位,然后再读剩下的 32位,这个过程不是原子的,但Java中volatile型的 long或 double变量的读写是原子的。volatile修复符的另一个作用是提供内存屏障(memory barrier),例如在分布式框架中的应用。简单的说,就是当你写一个volatile变量之前, Java内存模型会插入一个写屏障(writebarrier),读一个volatile 变量之前,会插入一个读屏障(read barrier)。意思就是说,在你写一个volatile域时,能保证任何线程都能看到你写的值,同时,在写之前,也能保证任何数值的更新对所有线程是可见的,因为内存屏障会将其他所有写的值更新到缓存。
66 volatile类型变量提供什么保证
volatile变量提供顺序和可见性保证,例如,JVM或者J川IT为了获得更好的性能会对语句重排序,但是volatile类型变量即使在没有同步块的情况下赋值也不会与其他语句重排序。volatile提供happens-before的保证,确保一个线程的修改能对其他线程是可见的。某些情况下,volatile 还能提供原子性,如读-64位数据类型,像long和 double 都不是原子的,但 volatile类型的double和long就是原子的。
67 什么是线程局部变量
线程局部变量是局限于线程内部的变量,属于线程自身所有,不在多个线程间共享。Java提供ThreadLocal类来支持线程局部变量,是一种实现线程安全的方式。但是在管理环境下(如web服务器)使用线程局部变量的时候要特别小心,在这种情况下,工作线程的生命周期比任何应用变量的生命周期都要长。任何线程局部变量一旦在工作完成后没有释放,Java应用就存在内存泄露的风险。
68 Java中sleep方法和wait方法的区别
虽然两者都是用来暂停当前运行的线程,但是sleep()实际上只是短暂停顿,因为它不会释放锁,而wait()意味着条件等待,这就是为什么该方法要释放锁,因为只有这样,其他等待的线程才能在满足条件时获取到该锁。
69 Java中++操作符是线程安全的吗
答案:不是线程安全的操作。它涉及到多个指令,如读取变量值,增加,然后存储回内存,这个过程可能会出现多个线程交差。
70 什么是线程池
在一个应用程序中初始化一个线程集合,然后在需要执行新的任务时重用线程池中的线程,而不是创建一个新的线程。线程池中的每个线程都有被分配一个任务,一旦任务完成,线程就回到线程池中,等待下一次的任务分配。
71 Java中 Synchronized关键字的使用
Synchronized 关键字在方法签名上,可以防止多个线程同时访问这个对象的synchronized修饰的方法。如果一个对象有多个synchronized方法,只要令个线程访问其中的一个同步方法,那么其他线程就不能访问对象其他的任何一个同步方法。不同对象实例的synchronize方法是互不干扰的,也就是说,其他对象还可以访问这个类中的同步方法。
Synchronized 如果修饰的是静态方法,防止多个线程同时访问这个类中的静态同步方法,它对类中所有对象都能起作用。也就是说,只有一个对象一个线程可以访问静态同步方法。
Synchronized修饰方法中的某段代码块,只对当前代码块实行互斥访问。当多个线程同步访问同步代码块,同一时间只能有一个线程得到执行,其他线程必须等待当前线程执行完代码块之后才能执行。当一个线程访问同步代码快时,其他线程可以访问非同步的代码。当一个线程访问同步代码块时,那么其他线程访问对其他同步代码块的访问将会被阻塞。
Synchronized 关键字是不能继承的,如果父类的 synchronized在继承时并不自动是synchronized修饰的,需要显示地声明。Synchronized 修饰this 时,会得到这个对象的对象锁,当一个线程访问时,那么其他线程访问对象的所有同步代码块或者同步方法,将会被阻塞。
72 Servlet的生命周期(待补充)
73 线程的生命周期
74 Session 与cookie的区别
-
Session保存在服务端,cookie保存在客户端。
-
Session保存是对象,cookie只能保存字符串。
-
Session不能设置路径,cookie可以设置保存路径。同一个网站不同网页的cookie可以保存到不通的路机构下,彼此是无法相互访问的。
-
Session在服务器关闭后会自动消失,cookie 则不会。
75 面向对象的特征有哪些方面
1、继承:
2、封装:
3、多态性:
76 访问修饰符public,private,protected,以及不写(默认)时的区别
78 float f=3.4;是否正确?
不正确。3.4是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4;或者写成float f3.4F。
79 阐述静态变量和实例变量的区别
静态变量是被static修饰符修饰的变量,也称为类变量,它属于类,不属于类的任何一个对象,一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝;
实例变量必须依存于某—实例,需要先创建对象然后通过对象才能访问到它。静态变量可以实现让多个对象共享内存。
补充:在Java开发中,上下文类和工具类中通常会有大量的静态成员。
80 什么时候用断言(assert)
断言在软件开发中是一种常用的调试方式,很多开发语言中都支持这种机制。会般来说,断言用于保证程序最基本、关键的正确性。断言检查通常在开发和测试时开启。为了保证程序的执行效率,在软件发布后断言检查通常是关闭的。断言是一个包含布尔表达式的语句,在执行这个语句时假定该表达式为true ;如果表达式的值为false,那么系统会报告一个AssertionError。断言的使用如下面的代码所示:
assert(a > 0); // throws an AssertionError if a<= 0
断言可以有两种形式:
- assert Expression1。
- assert Expression1 : Expression2。
Expression1应该总是产生一个布尔值。
Expression2可以是得出一个值的任意表达式;这个值用于生成显示更多调试信息的字符串消息。
要在运行时启用断言,可以在启动VM时使用-enableassertions或者-ea标记。要在运行时选择禁用断言,可以在启动JVM时使用-da或者-disableassertions标记。要在系统类中启用或禁用断言,可使用-esa或-dsa标记。还可以在包的基础上启用或者禁用断言。
注意:断言不应该以任何方式改变程序的状态。简单的说,如果希望在不满足某些条件时阻止代码的执行,就可以考虑用断言来阻止它。不要用断言作为程序逻辑判断的依据。
81 请说出与线程同步以及线程调度相关的方法
-
wait():使一个线程处于等待(阻塞)状态,并且释放所持有的对象的锁;
-
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要处理InterruptedException异常;
-
notify():唤醒一个处于等待状态的线程,当然在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由JVM确定唤醒哪个线程,而且与优先级无关;
-
notityAll():唤醒所有处于等待状态的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态;
82 Java中如何实现序列化,有什么意义
序列化就是一种用来处理对象流的机制,所谓对象流也就是将对象的内容进行流化。可以对流化后的对象进行读写操作,也可将流化后的对象传输于网络之间。序列化是为了解决对象流读写操作时可能引发的问题(如果不进行序列化可能会存在数据乱序的问题)。
要实现序列化,需要让一个类实现Serializable接口,该接口是一个标识性接口;标注该类对象是可被序列化的,然后使用一个输出流来构造一个对象输出流并通过writeObject(Object)方法就可以将实现对象写出(即保存其状态),如果需要反序列化则可以用一个输入流建立对象输入流,然后通过readObject方法从流中读取对象。序列化除了能够实现对象的持久化之外,还能够用于对象的深度克隆(可以参考第29题)。
更多推荐
所有评论(0)