Java面试题
2024-06-10 09:54:40 0 举报
AI智能生成
这是一道Java面试题,主要考察求职者对Java语言的理解和掌握程度。题目将涉及Java的核心概念,如面向对象编程、继承、封装、多态等,以及Java语言的各种特性,如异常处理、泛型、反射、并发编程等。此外,面试题还可能包括Java的内存管理、JVM性能调优等方面的内容。求职者需要对Java语言有深入的理解,并能够熟练运用在实际项目中。
作者其他创作
大纲/内容
异常&反射
error和exception有什么区别?
error表示系统级的错误,是java运行环境内部错误或者硬件问题,不能指望程序来处理这样的问题,除 了退出运行外别无选择,它是Java虚拟机抛出的。
exception 表示程序需要捕捉、需要处理的异常,是由与程序设计的不完善而出现的问题,程序必须处 理的问题。
5个常见的RuntimeException?
(1)Java.lang.NullPointerException 空指针异常;出现原因:调用了未经初始化的对象或者是不存在的对 象。
(2)Java.lang.NumberFormatException 字符串转换为数字异常;出现原因:字符型数据中包含非数字型字 符。
(3)Java.lang.IndexOutOfBoundsException 数组角标越界异常,常见于操作数组对象时发生。
(4)Java.lang.IllegalArgumentException 方法传递参数错误。
(5)Java.lang.ClassCastException 数据类型转换异常。
throw和throws的区别?
throw:
(1)throw 语句用在方法体内,表示抛出异常,由方法体内的语句处理。
(2)throw 是具体向外抛出异常的动作,所以它抛出的是一个异常实例,执行 throw一定是抛出了某 种异常。
throws:
(1)@throws 语句是用在方法声明后面,表示如果抛出异常,由该方法的调用者来进行异常的处理。
(2)throws 主要是声明这个方法会抛出某种类型的异常,让它的使用者要知道需要捕获的异常的类 型。
(3)throws 表示出现异常的一种可能性,并不一定会发生这种异常。
Java中异常分类
按照异常处理时机:
编译时异常(受控异常(CheckedException))和运行时异常(非受控异常(UnCheckedException))
如何自定义异常
继承Exception是检查性异常,继承RuntimeException是非检查性异常, 一般要复写两个构造方法,用 throw抛出新异常
如果同时有很多异常抛出,那可能就是异常链,就是一个异常引发另一个异常,另一个异常引发更多异 常, 一般我们会找它的原始异常来解决问题, 一般会在开头或结尾,异常可通过initCause串起来,可以 通过自定义异常
Java中异常处理
首先处理异常主要有两种方式:一种try catch ,一种是throws。
1. try catch:
. try{} 中放入可能发生异常的代码。 catch{}中放入对捕获到异常之后的处理。
2.throw throws:
. throw是语句抛出异常,出现于函数内部,用来抛出一个具体异常实例, throw被执行后面的语句不 起作用,直接转入异常处理阶段。
. throws是函数方法抛出异常, 一般写在方法的头部,抛出异常,给方法的调用者进行解决。
什么是Java反射机制?
Java的反射 (reflection)机制是指在程序的运行状态中,可以构造任意一个类的对象,可以了解任意一 个对象所属的类,可以了解任意一个类的成员变量和方法,可以调用任意一个对象的属性和方法。这种 动态获取程序信息以及动态调用对象的功能称为Java语言的反射机制。反射被视为动态语言的关键。
举例什么地方用到反射机制?
1. JDBC中,利用反射动态加载了数据库驱动程序。
2. Web服务器中利用反射调用了Sevlet的服务方法。
3. Eclispe等开发工具利用反射动态刨析对象的类型与结构,动态提示对象的属性和方法。 4. 很多框架都用到反射机制,注入属性,调用方法,如Spring。
java反射机制的作用
在运行时判定任意一个对象所属的类
在运行时构造任意一个类的对象;
在运行时判定任意一个类所具有的成员变量和方法;
在运行时调用任意一个对象的方法;
生成动态代理;
Java反射机制类
java. lang.Class; //类
java.lang. reflect .Constructor;//构造方法
java.lang. reflect . Field; //类的成员变量
java.lang. reflect .Method;//类的方法
java.lang. reflect .Modifier;//访问权限
反射机制优缺点?
优点:运行期类型的判断,动态加载类,提高代码灵活度。
缺点:性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很 多。
利用反射创建对象?
1.通过一个全限类名创建一个对象
Class.forName(“全限类名”); 例如: com.mysql.jdbc.Driver Driver类已经被加载到 jvm中,并且完成了 类的初始化工作就行了
类名.class; 获取Class<? > clz 对象
对象.getClass();
2.获取构造器对象,通过构造器new出一个对象
Clazz.getConstructor([String.class]);
Con.newInstance([参数]);
3.通过class对象创建一个实例对象(就相当与new类名()无参构造器)
Cls.newInstance();
IO&NIO
什么是IO流?
它是一种数据的流从源头流到目的地。比如文件拷贝,输入流和输出流都包括了。输入流从文件中读取 数据存储到进程(process)中,输出流从进程中读取数据然后写入到目标文件。
.java中有几种类型的流?
按照单位大小:字符流、字节流。按照流的方向:输出流、输入流。
字节流和字符流哪个好?怎么选择?
1. 缓大多数情况下使用字节流会更好,因为字节流是字符流的包装,而大多数时候 IO 操作都是直接操 作磁盘文件,所以这些流在传输时都是以字节的方式进行的(图片等都是按字节存储的)
2. 如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能
2. 如果对于操作需要通过 IO 在内存中频繁处理字符串的情况使用字符流会好些,因为字符流具备缓冲区,提高了性能
读取数据量大的文件时,速度会很慢,如何选择流?
字节流时,选择BufferedInputStream和BufferedOutputStream。
字符流时,选择BufferedReader 和 BufferedWriter
IO模型有几种?
阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。
阻塞IO (blocking IO)
应用程序调用一个IO函数,导致应用程序阻塞,如果数据已经准备好,从内核拷贝到用户空间,否则一 直等待下去。 一个典型的读操作流程大致如下图,当用户进程调用recvfrom这个系统调用时, kernel就 开始了IO的第一个阶段:准备数据,就是数据被拷贝到内核缓冲区中的一个过程(很多网络IO数据不会 那么快到达,如没收一个完整的UDP包),等数据到操作系统内核缓冲区了,就到了第二阶段:将数据 从内核缓冲区拷贝到用户内存,然后kernel返回结果,用户进程才会解除block状态,重新运行起来。
blocking IO 的特点就是在IO执行的两个阶段用户进程都会block住;
非阻塞I/O(nonblocking IO)
非阻塞I/O模型,我们把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不 要将进程睡眠,而是返回一个错误。这样我们的I/O操作函数将不断的测试数据是否已经准备好,如果 没有准备好,继续测试,直到数据准备好为止。在这个不断测试的过程中, 会大量的占用CPU 的时间。
当用户进程发出read操作时,如果kernel中数据还没准备好,那么并不会block用户进程,而是立即返 回error ,用户进程判断结果是error ,就知道数据还没准备好,用户可以再次发read,直到kernel中数据 准备好,并且用户再一次发read操作,产生system call,那么kernel 马上将数据拷贝到用户内存,然后 返回;所以nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
阻塞IO一个线程只能处理一个IO流事件,要想同时处理多个IO流事件要么多线程要么多进程,这样 做效率显然不会高,而非阻塞IO可以一个线程处理多个流事件,只要不停地询所有流事件即可,当然这 个方式也不好,当大多数流没数据时,也是会大量浪费CPU资源;为了避免CPU空转,引进代理(select 和poll,两种方式相差不大),代理可以观察多个流I/O事件,空闲时会把当前线程阻塞掉,当有一个或 多个I/O事件时,就从阻塞态醒过来,把所有IO流都轮询一遍,于是没有IO事件我们的程序就阻塞在
select方法处,即便这样依然存在问题,我们从select出只是知道有IO事件发生,却不知道是哪几个流, 还是只能轮询所有流, epoll这样的代理就可以把哪个流发生怎样的IO事件通知我们;
I/O多路复用模型(IO multiplexing)
I/O多路复用就在于单个进程可以同时处理多个网络连接IO,基本原理就是select ,poll ,epoll这些个函 数会不断轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程,这三个functon会阻 塞进程,但和IO阻塞不同,这些函数可以同时阻塞多个IO操作,而且可以同时对多个读操作,写操作IO 进行检验,直到有数据到达,才真正调用IO操作函数,调用过程如下图; 所以IO多路复用的特点是通过 一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中任意一个进入就 绪状态, select函数就可以返回。
IO多路复用的优势在于并发数比较高的IO操作情况,可以同时处理多个连接,和bloking IO一样 socket是被阻塞的,只不过在多路复用中socket是被select阻塞,而在阻塞IO中是被socket IO给阻塞。
信号驱动I/O模型
可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们,通过sigaction系统调用安装一个信号处 理函数。该系统调用将立即返回,我们的进程继续工作,也就是说它没有被阻塞。当数据报准备好读取 时,内核就为该进程产生一个SIGIO信号。我们随后既可以在信号处理函数中调用recvfrom读取数据
报,并通知主循环数据已经准备好待处理。特点:等待数据报到达期间进程不被阻塞。主循环可以继续 执行,只要等待来自信号处理函数的通知:既可以是数据已准备好被处理,也可以是数据报已准备好被 读取
异步 I/O(asynchronous IO)
异步IO告知内核启动某个操作,并让内核在整个操作(包括将内核数据复制到我们自己的缓冲区)完成后 通知我们,调用aio_read (Posix异步I/O函数以aio或lio开头)函数,给内核传递描述字、缓冲区指针、 缓冲区大小(与read相同的3个参数)、文件偏移以及通知的方式,然后系统立即返回。我们的进程不 阻塞于等待I/0操作的完成。当内核将数据拷贝到缓冲区后,再通知应用程序。
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到 一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后, kernel 会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后, kernel会给用户进程发送一 个signal,告诉它read操作完成了
NIO与IO 的区别?
NIO即New IO,这个库是在JDK1.4中才引入的。 NIO和IO有相同的作用和目的,但实现方式不同,
NIO主要用到的是块,所以NIO的效率要比IO高很多。在Java API中提供了两套NIO ,一套是针对标准 输入输出NIO,另一套就是网络编程NIO。
NIO和IO适用场景
NIO是为弥补传统IO的不足而诞生的,但是尺有所短寸有所长, NIO也有缺点,因为NIO是面向缓冲 区的操作,每一次的数据处理都是对缓冲区进行的,那么就会有一个问题,在数据处理之前必须要判断 缓冲区的数据是否完整或者已经读取完毕,如果没有,假设数据只读取了一部分,那么对不完整的数据 处理没有任何意义。所以每次数据处理之前都要检测缓冲区数据。
那么NIO和IO各适用的场景是什么呢?
如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器, 这时候用NIO处理数据可能是个很好的选择。
而如果只有少量的连接,而这些连接每次要发送大量的数据,这时候传统的IO更合适。使用哪种处 理数据,需要在数据的响应等待时间和检查缓冲区数据的时间上作比较来权衡选择。
NIO核心组件
channel 、buffer 、selector
什么是channel
一个Channel (通道)代表和某一实体的连接,这个实体可以是文件、网络套接字等。也就是说,通道 是Java NIO提供的一座桥梁,用于我们的程序和操作系统底层I/O服务进行交互。
通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具 体的有FileChannel 、SocketChannel等。
通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。
当然,也有区别,主要体现在如下两点: 一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream) 通道有非阻塞I/O模式
通道是一种很基本很抽象的描述,和不同的I/O服务交互,执行不同的I/O操作,实现不一样,因此具 体的有FileChannel 、SocketChannel等。
通道使用起来跟Stream比较像,可以读取数据到Buffer中,也可以把Buffer中的数据写入通道。
当然,也有区别,主要体现在如下两点: 一个通道,既可以读又可以写,而一个Stream是单向的(所以分 InputStream 和 OutputStream) 通道有非阻塞I/O模式
Java NIO中最常用的通道实现?
FileChannel:读写文件
DatagramChannel: UDP协议网络通信
SocketChannel : TCP协议网络通信
ServerSocketChannel:监听TCP连接
Buffer是什么?
NIO中所使用的缓冲区不是一个简单的byte数组,而是封装过的Buffer类,通过它提供的API,我们可以 灵活的操纵数据。
与Java基本类型相对应, NIO提供了多种 Buffer 类型,如ByteBuffer 、CharBuffer 、IntBuffer等,区别 就是读写缓冲区时的单位长度不一样(以对应类型的变量为单位进行读写)。
核心Buffer实现有哪些?
核心的buffer实现有这些: ByteBuffer 、CharBuffer 、DoubleBuffer 、FloatBuffer 、IntBuffer、
LongBuffer 、ShortBuffer,涵盖了所有的基本数据类型(4类8种,除了Boolean)。也有其他的buffer如 MappedByteBuffer。
buffer读写数据基本操作
1)、将数据写入buffer
2)、调用buffer.flip()
3)、将数据从buffer中读取出来
4)、调用buffer.clear()或者buffer.compact()
在写buffer的时候, buffer会跟踪写入了多少数据,需要读buffer的时候,需要调用flip()来将buffer从写 模式切换成读模式,读模式中只能读取写入的数据,而非整个buffer。
当数据都读完了,你需要清空buffer以供下次使用,可以有2种方法来操作:调用clear() 或者 调用 compact()。
区别: clear方法清空整个buffer ,compact方法只清除你已经读取的数据,未读取的数据会被移到 buffer的开头,此时写入数据会从当前数据的末尾开始。
// 创建一个容量为48的ByteBuffer
ByteBuffer buf = ByteBuffer.allocate (48 );
// 从channel中读(取数据然后写)入buffer
int bytesRead = inChannel . read (buf );
// 下面是读取buffer
while (bytesRead != -1 ) {
buf .flip(); // 转换buffer为读模式
System.out .print((char ) buf .get()); // 一次读取一个byte
buf .clear (); //清空buffer准备下一次写入
}
Selector是什么?
Selector (选择器)是一个特殊的组件,用于采集各个通道的状态(或者说事件)。我们先将通道注册 到选择器,并设置好关心的事件,然后就可以通过调用select()方法,静静地等待事件发生。
通道可以监听那几个事件?
通道有如下4个事件可供我们监听:
Accept:有可以接受的连接
Connect:连接成功
Read:有数据可读
Write:可以写入数据了
为什么要用Selector?
如果用阻塞I/O,需要多线程(浪费内存),如果用非阻塞I/O,需要不断重试(耗费CPU)。 Selector 的出现解决了这施起的问题,非阻塞模式下,通过Selector,我们的线程只为已就绪的通道工作,不用
盲目的重试了。比如,当所有通道都没有数据到达时,也就没有Read事件发生,我们的线程会在select() 方法处被挂起,从而让出了CPU资源。
Selector处理多Channel 图文说明
要使用一个Selector,你要先注册这个Selector的Channels。然后你调用Selector的select()方法。这个方 法会阻塞,直到它注册的Channels当中有一个准备好了的事件发生了。当select()方法返回的时候,线程 可以处理这些事件,如新的连接的到来,数据收到了等。
Java基础题
Java语言的三大特性
封装
属性可用来描述同一类事物的特征,方法可描述一类事物可做的操作。封装就是把属于同一类 事物的共性(包括属性与方法)归到一个类中,以方便使用。
1)概念:封装也称为信息隐藏,是指利用抽象数据类型将数据和基于数据的操作封装在一起,使其构 成一个不可分割的独立实体,数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留 一些对外接口使之与外部发生联系。系统的其他部分只有通过包裹在数据外面的被授权的操作来与这 个抽象数据类型交流与交互。也就是说,用户无需知道对象内部方法的实现细节,但可以根据对象提 供的外部接口(对象名和参数)访问该对象。
2)好处: (1)实现了专业的分工。将能实现某一特定功能的代码封装成一个独立的实体后,各程序员可 以在需要的时候调用,从而实现了专业的分工。 (2)隐藏信息,实现细节。通过控制访问权限可以将可 以将不想让客户端程序员看到的信息隐藏起来,如某客户的银行的密码需要保密,只能对该客户开发 权限。
继承
个性对共性的属性与方法的接受,并加入个性特有的属性与方法
1.概念: 一个类继承另一个类,则称继承的类为子类,被继承的类为父类。
2.目的:实现代码的复用。
3.理解:子类与父类的关系并不是日常生活中的父子关系,子类与父类而是一种特殊化与一般化的关 系,是is-a的关系,子类是父类更加详细的分类。如class dog extends animal,就可以理解为dog is a animal.注意设计继承的时候,若要让某个类能继承,父类需适当开放访问权限,遵循里氏代换原
则,即向修改关闭对扩展开放,也就是开- 闭原则。
4.结果:继承后子类自动拥有了父类的属性和方法,但特别注意的是,父类的私有属性和构造方法并 不能被继承。
另外子类可以写自己特有的属性和方法,目的是实现功能的扩展,子类也可以复写父类的方法即方法 的重写。
多态
多态的概念发展出来,是以封装和继承为基础的。
多态就是在抽象的层面上实施一个统一的行为,到个体(具体)的层面上时,这个统一的行为会因为 个体(具体)的形态特征而实施自己的特征行为。(针对一个抽象的事,对于内部个体又能找到其自 身的行为去执行。)
1.概念:相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同。
2.理解:子类以父类的身份出现,但做事情时还是以自己的方法实现。子类以父类的身份出现需要向 上转型(upcast),其中向上转型是由JVM自动实现的,是安全的,但向下转型(downcast)是不安全
的,需要强制转换。子类以父类的身份出现时自己特有的属性和方法将不能使用。
Java语言主要特性
1. Java语言是易学的。Java语言的语法与C语言和C++语言很接近,使得大多数程序员很容易学习和使 用Java。
2. Java语言是强制面向对象的。Java语言提供类、接口和继承等原语,为了简单起见,只支持类之间的 单继承,但支持接口之间的多继承,并支持类与接口之间的实现机制(关键字为implements)。
3. Java语言是分布式的 。Java语言支持Internet应用的开发,在基本的Java应用编程接口中有一个网络 应用编程接口 (java net),它提供了用于网络应用编程的类库,包括URL 、URLConnection、
Socket 、ServerSocket等。 Java的RMI (远程方法激活)机制也是开发分布式应用的重要手段。
4. Java语言是健壮的。Java的强类型机制、异常处理、垃圾的自动收集等是Java程序健壮性的重要保 证。对指针的丢弃是Java的明智选择。
5. Java语言是安全的。Java通常被用在网络环境中,为此, Java提供了一个安全机制以防恶意代码的攻 击。如:安全防范机制(类ClassLoader),如分配不同的名字空间以防替代本地的同名类、字节代 码检查。
6. Java语言是体系结构中立的。Java程序(后缀为java的文件)在Java平台上被编译为体系结构中立的 字节码格式(后缀为class的文件),然后可以在实现这个Java平台的任何系统中运行。
7. Java语言是解释型的。如前所述, Java程序在Java平台上被编译为字节码格式,然后可以在实现这个 Java平台的任何系统的解释器中运行。 (一次编译,到处运行)
8. Java是性能略高的。与那些解释型的高级脚本语言相比, Java的性能还是较优的。
9. Java语言是原生支持多线程的。在Java语言中,线程是一种特殊的对象,它必须由Thread类或其子 (孙)类来创建。
JDK 和 JRE 有什么区别
. JDK:Java Development Kit 的简称, Java 开发工具包,提供了 Java 的开发环境和运行环境。
. JRE:Java Runtime Environment 的简称, Java 运行环境,为 Java 的运行提供了所需环境。
具体来说 JDK 其实包含了 JRE,同时还包含了编译 Java 源码的编译器 Javac,还包含了很多 Java 程序 调试和分析的工具。简单来说:如果你需要运行 Java 程序,只需安装 JRE 就可以了,如果你需要编写 Java 程序,需要安装 JDK。
Java基本数据类型及其封装类
Tips:boolean类型占了单独使用是4个字节,在数组中又是1个字节
基本类型所占的存储空间是不变的。这种不变性也是Java具有可移植性的原因之一。 基本类型放在栈中,直接存储值。
所有数值类型都有正负号,没有无符号的数值类型。
为什么需要封装类?
因为泛型类包括预定义的集合,使用的参数都是对象类型,无法直接使用基本数据类型,所以Java又提 供了这些基本类型的封装类。
基本类型和对应的封装类由于本质的不同。具有一些区别:
1.基本类型只能按值传递,而封装类按引用传递。
2.基本类型会在栈中创建,而对于对象类型,对象在堆中创建,对象的引用在栈中创建,基本类型由于 在栈中,效率会比较高,但是可能存在内存泄漏的问题。
public static void main(String args[])这段声明里每个关键字的 作用
public: main方法是Java程序运行时调用的第一个方法,因此它必须对Java环境可见。所以可见性设置为 pulic.
static: Java平台调用这个方法时不会创建这个类的一个实例,因此这个方法必须声明为static。
void: main方法没有返回值。
String是命令行传进参数的类型, args是指命令行传进的字符串数组。
如果main方法被声明为private会怎样?
能正常编译,但运行的时候会提示”main方法不是public的” 。在idea中如果不用public修饰,则会自动 去掉可运行的按钮。
==与equals的区别
==比较两个对象在内存里是不是同一个对象,就是说在内存里的存储位置一致。两个String对象存储的 值是一样的,但有可能在内存里存储在不同的地方 .
==比较的是引用而equals方法比较的是内容。 public boolean equals(Object obj) 这个方法是由Object对 象提供的,可以由子类进行重写。默认的实现只有当对象和自身进行比较时才会返回true,这个时候和== 是等价的。 String, BitSet, Date, 和File都对equals方法进行了重写,对两个String对象 而言,值相等意味 着它们包含同样的字符序列。对于基本类型的包装类来说,值相等意味着对应的基本类型的值一样。
public class EqualsTest {
public static void main (String[] args ) {
String s1 = “abc” ;
String s2 = s1;
String s5 = “abc” ;
String s3 = new String(”abc” );
String s4 = new String(”abc” );
System.out .println(”== comparison : ” + (s1 == s5 ));
System.out .println(”== comparison : ” + (s1 == s2 ));
System.out .println(”Using equals method : ” + s1 .equals(s2 )); System.out .println(”== comparison : ” + s3 == s4 );
System.out .println(”Using equals method : ” + s3 .equals(s4 ));
}
}
== comparison : true
== comparison : true
Using equals method : true
false
Using equals method :true
Object有哪些公用方法
Object是所有类的父类,任何类都默认继承Object
clone 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出 CloneNotSupportedException异常。
equals 在Object中与==是一样的,子类一般需要重写该方法。
hashCode 该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有 哈希功能的Collection中用到。
getClass final方法,获得运行时类型
wait 使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。 wait() 方法一直等待,直到获得锁或者被中断。 wait(longtimeout) 设定一个超时间隔,如果在规定时间内没 有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生
1、其他线程调用了该对象的notify方法。
2、其他线程调用了该对象的notifyAll方法。
3、其他线程调用了interrupt中断该线程。
4、时间间隔到了。
5、此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
notify 唤醒在该对象上等待的某个线程。
notifyAll 唤醒在该对象上等待的所有线程。
toString 转换成字符串, 一般子类都有重写,否则打印句柄。
clone 保护方法,实现对象的浅复制,只有实现了Cloneable接口才可以调用该方法,否则抛出 CloneNotSupportedException异常。
equals 在Object中与==是一样的,子类一般需要重写该方法。
hashCode 该方法用于哈希查找,重写了equals方法一般都要重写hashCode方法。这个方法在一些具有 哈希功能的Collection中用到。
getClass final方法,获得运行时类型
wait 使当前线程等待该对象的锁,当前线程必须是该对象的拥有者,也就是具有该对象的锁。 wait() 方法一直等待,直到获得锁或者被中断。 wait(longtimeout) 设定一个超时间隔,如果在规定时间内没 有获得锁就返回。
调用该方法后当前线程进入睡眠状态,直到以下事件发生
1、其他线程调用了该对象的notify方法。
2、其他线程调用了该对象的notifyAll方法。
3、其他线程调用了interrupt中断该线程。
4、时间间隔到了。
5、此时该线程就可以被调度了,如果是被中断的话就抛出一个InterruptedException异常。
notify 唤醒在该对象上等待的某个线程。
notifyAll 唤醒在该对象上等待的所有线程。
toString 转换成字符串, 一般子类都有重写,否则打印句柄。
为什么Java里没有全局变量?
全局变量是全局可见的, Java不支持全局可见的变量,因为:全局变量破坏了引用透明性原则。全局变 量导致了命名空间的冲突。
while循环和do循环有什么不同?
while结构在循环的开始判断下一个选代是否应该继续。 do/while结构在循环的结尾来判断是否将继续 下一轮选代。 do结构至少会执行一次循环体。
char型变量中能不能存储一个中文汉字?为什么?
可以。 Java默认Unicode编码。 Unicode码占16位。 char两个字节刚好16位。
float f=3.4;是否正确?
不正确。 3.4是双精度数,将双精度型 (double)赋值给浮点型(float)属于下转型(down-casting,也 称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4; 或者写成float f =3.4F。
short s1 = 1; s1 = s1 + 1;有错吗? short s1 = 1; s1 += 1;有错吗?
对于short s1 = 1; s1 = s1 + 1;由于1是int类型,因此s1+1运算结果也是int 型,需要强制转换类型才能赋 值给short型。而short s1 = 1; s1 += 1;+=操作符会进行隐式自动类型转换,是 Java 语言规定的运算
符;Java编译器会对它进行特殊处理,因此可以正确编译。因为s1+= 1;相当于s1 = (short)(s1 + 1)。
&和&&的区别?
1. & :(1)按位与; (2)逻辑与。
按位与: 0 & 1 = 0 ; 0 & 0 = 0; 1 & 1 = 1
逻辑与: a == b & b ==c (即使a==b已经是 false了,程序还会继续判断b是否等于c)
2.&&: 短路与
a== b && b== c (当a==b 为false则不会继续判断b是否等与c)
比如判断某对象中的属性是否等于某值,则必须用&&,否则会出现空指针问题。
Locale类是什么?
Locale类用来根据语言环境来动态调整程序的输出。
Java中final、finally、finalize 的区别与用法
1. final
final是一个修饰符也是一个关键字。
被final修饰的类无法被继承
对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;
如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。但是它指向的对象的内 容是可变的。
被final修饰的方法将无法被重写,但允许重载
注意:类的private方法会隐式地被指定为final方法。
2. finally
finally是一个关键字。
finally在异常处理时提供finally块来执行任何清除操作。不管有没有异常被抛出或者捕获, finally块 都会执行,通常用于释放资源。
finally块正常情况下一定会被执行。但是有至少两个极端情况:
如果对应的try块没有执行,则这个try块的finally块并不会被执行
如果在try块中jvm关机,例如system.exit(n),则finally块也不会执行(都拔电源了,怎么执行)
finally块中如果有return语句,则会覆盖try或者catch中的return语句,导致二者无法return,所以强 烈建议finally块中不要存在return关键字
3. finalize
finalize()是Object类的protected方法,子类可以覆盖该方法以实现资源清理工作。
GC在回收对象之前都会调用该方法
finalize()方法是存在很多问题的:
java语言规范并不保证finalize方法会被及时地执行,更根本不会保证它们一定会被执行
finalize()方法可能带来性能问题,因为JVM通常在单独的低优先级线程中完成finalize的执行
finalize()方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的 finalize方法最多由GC执行一次(但可以手动调用对象的finalize方法)
hashCode()和equals()的区别
下边从两个角度介绍了他们的区别: 一个是性能, 一个是可靠性。他们之间的主要区别也基本体现在这 里。
1.equals()既然已经能实现对比的功能了,为什么还要hashCode()呢?
因为重写的equals ()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对 比,则只要生成一个hash值进行比较就可以了,效率很高。
2.hashCode()既然效率这么高为什么还要equals()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式
可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出 (PS:以下两条结论是重点,很多人面试的时候都说不出来):
equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。
1.equals()既然已经能实现对比的功能了,为什么还要hashCode()呢?
因为重写的equals ()里一般比较的比较全面比较复杂,这样效率就比较低,而利用hashCode()进行对 比,则只要生成一个hash值进行比较就可以了,效率很高。
2.hashCode()既然效率这么高为什么还要equals()呢?
因为hashCode()并不是完全可靠,有时候不同的对象他们生成的hashcode也会一样(生成hash值得公式
可能存在的问题),所以hashCode()只能说是大部分时候可靠,并不是绝对可靠,所以我们可以得出 (PS:以下两条结论是重点,很多人面试的时候都说不出来):
equals()相等的两个对象他们的hashCode()肯定相等,也就是用equals()对比是绝对可靠的。
hashCode()相等的两个对象他们的equals()不一定相等,也就是hashCode()不是绝对可靠的。
1.阿里巴巴开发规范明确规定:
只要重写 equals,就必须重写 hashCode;
因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这 两个方法;
如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals ;
String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用;
2、什么时候需要重写?
一般的地方不需要重载hashCode,只有当类需要放在HashTable 、HashMap 、HashSet等等hash结构的 集合时才会重载hashCode。
3、那么为什么要重载hashCode呢?
如果你重写了equals,比如说是基于对象的内容实现的,而保留hashCode的实现不变,那么很可能某两 个对象明明是“相等” ,而hashCode却不一样。
这样,当你用其中的一个作为键保存到hashMap 、hasoTable或hashSet中,再以“相等的”找另一个作为 键值去查找他们的时候,则根本找不到。
4、为什么equals()相等, hashCode就一定要相等,而hashCode相等,却不要求equals相等?
因为是按照hashCode来访问小内存块,所以hashCode必须相等。
HashMap获取一个对象是比较key的hashCode相等和equals为true。
之所以hashCode相等,却可以equal不等,就比如ObjectA和ObjectB他们都有属性name ,那么
hashCode都以name计算,所以hashCode一样,但是两个对象属于不同类型,所以equals为false。
5、为什么需要hashCode?
通过hashCode可以很快的查到小内存块。
通过hashCode比较比equals方法快,当get时先比较hashCode,如果hashCode不同,直接返回false。
深拷贝和浅拷贝的区别是什么?
浅拷贝
(1)、定义
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对
象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。 ”里面的对象“会在原来的 对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象
(1)、定义
被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对
象。即对象的浅拷贝会对“主”对象进行拷贝,但不会复制主对象里面的对象。 ”里面的对象“会在原来的 对象和它的副本之间共享。
简而言之,浅拷贝仅仅复制所考虑的对象,而不复制它所引用的对象
深拷贝
(1)、定义
深拷贝是一个整个独立的对象拷贝,深拷贝会拷贝所有的属性,并拷贝属性指向的动态分配的内存。当对 象和它所引用的对象一起拷贝时即发生深拷贝。深拷贝相比于浅拷贝速度较慢并且花销较大。
简而言之,深拷贝把要复制的对象所引用的对象都复制了一遍。
Java 中操作字符串都有哪些类?它们之间有什么区别?
String 、StringBuffer 、StringBuilder。
String 和 StringBuffer 、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新 的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer 、StringBuilder 可以在原有对象的基 础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于, StringBuffer 是线程安全的,而 StringBuilder 是非线程 安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多 线程环境下推荐使用 StringBuffer。
String str="a"与 String str=new String("a")一样吗?
不一样,因为内存的分配方式不一样。
Stringstr="a"; -> 常量池 String str=new String("a") -> 堆内存
抽象类能使用 final 修饰吗?
不能。定义抽象类就是让其他类继承的,而 final修饰的类不能被继承。
static关键字5连问
(1)抽象的(abstract)方法是否可同时是静态的(static)?
抽象方法将来是要被重写的,而静态方法是不能重写的,所以这个是错误的。
(2)是否可以从一个静态 (static)方法内部发出对非静态方法的调用?
不可以,静态方法只能访问静态成员,非静态方法的调用要先创建对象。
(3) static 可否用来修饰局部变量?
static 不允许用来修饰局部变量
(4)内部类与静态内部类的区别?
静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果 要 访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变
量、调用静态的方法;
普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方 法。
如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访 问属性或者调用方法。
如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内 部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性
如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即 可。
(5) Java中是否可以覆盖(override) 一个private或者是static的方法?
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定 的。 static方法跟类的任何实例都不相关,所以概念上不适用。
抽象方法将来是要被重写的,而静态方法是不能重写的,所以这个是错误的。
(2)是否可以从一个静态 (static)方法内部发出对非静态方法的调用?
不可以,静态方法只能访问静态成员,非静态方法的调用要先创建对象。
(3) static 可否用来修饰局部变量?
static 不允许用来修饰局部变量
(4)内部类与静态内部类的区别?
静态内部类相对与外部类是独立存在的,在静态内部类中无法直接访问外部类中变量、方法。如果 要 访问的话,必须要new一个外部类的对象,使用new出来的对象来访问。但是可以直接访问静态的变
量、调用静态的方法;
普通内部类作为外部类一个成员而存在,在普通内部类中可以直接访问外部类属性,调用外部类的方 法。
如果外部类要访问内部类的属性或者调用内部类的方法,必须要创建一个内部类的对象,使用该对象访 问属性或者调用方法。
如果其他的类要访问普通内部类的属性或者调用普通内部类的方法,必须要在外部类中创建一个普通内 部类的对象作为一个属性,外同类可以通过该属性调用普通内部类的方法或者访问普通内部类的属性
如果其他的类要访问静态内部类的属性或者调用静态内部类的方法,直接创建一个静态内部类对象即 可。
(5) Java中是否可以覆盖(override) 一个private或者是static的方法?
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定 的。 static方法跟类的任何实例都不相关,所以概念上不适用。
重载(Overload)和重写(Override )的区别。重载的方法能否根据
返回类型进行区分?
返回类型进行区分?
方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行 时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同 或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方 法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更 多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。
Java的四种引用
1、强引用
最普遍的一种引用方式,如String s = "abc",变量s就是字符串“abc”的强引用,只要强引用存在,则垃 圾回收器就不会回收这个对象。
2、软引用(SoftReference)
用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。 一般用于实现内存 敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,
JVM就会把这个软引用加入到与之关联的引用队列中。
3、弱引用(WeakReference)
弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 在垃圾回收器线程扫描它所管辖的内存区域的过程中, 一旦发现了只具有弱引用的对象,不管当前内存 空间足够与否,都会回收它的内存。
4、虚引用( PhantomReference )
就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引 用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被 垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现 它还有虚引,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
最普遍的一种引用方式,如String s = "abc",变量s就是字符串“abc”的强引用,只要强引用存在,则垃 圾回收器就不会回收这个对象。
2、软引用(SoftReference)
用于描述还有用但非必须的对象,如果内存足够,不回收,如果内存不足,则回收。 一般用于实现内存 敏感的高速缓存,软引用可以和引用队列ReferenceQueue联合使用,如果软引用的对象被垃圾回收,
JVM就会把这个软引用加入到与之关联的引用队列中。
3、弱引用(WeakReference)
弱引用和软引用大致相同,弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。 在垃圾回收器线程扫描它所管辖的内存区域的过程中, 一旦发现了只具有弱引用的对象,不管当前内存 空间足够与否,都会回收它的内存。
4、虚引用( PhantomReference )
就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引 用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被 垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:
虚引用必须和引用队列 (ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现 它还有虚引,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
Java 中Comparator 与Comparable 有什么不同?
Comparable 接口用于定义对象的自然顺序,是排序接口,而 comparator 通常用于定义用户定制的顺 序,是比较接口。我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现Comparable接 口),那么我们就可以建立一个“该类的比较器”来进行排序。 Comparable 总是只有一个,但是可以有多 个 comparator 来定义对象的顺序。
Java 序列化,反序列化?
Java 序列化就是指将对象转换为字节序列的过程,反序列化是指将字节序列转换成目标对象的过程。
什么情况需要Java序列化?
当 Java 对象需要在网络上传输 或者 持久化存储到文件中时。
序列化的实现?
让类实现Serializable接口,标注该类对象是可被序列。
如果某些数据不想序列化,如何处理?
在字段面前加 transient 关键字,例如:
transient private String phone;//不参与序列化
Java泛型和类型擦除?
泛型即参数化类型,在创建集合时,指定集合元素的类型,此集合只能传入该类型的参数。类型擦除: java编译器生成的字节码不包含泛型信息,所以在编译时擦除: 1.泛型用最顶级父类替换; 2.移除。
Java集合
Java集合框架的基础接口有哪些?
Collection为集合层级的根接口。 一个集合代表一组对象,这些对象即为它的元素。 Java平台不提供这 个接口任何直接的实现。
Set是一个不能包含重复元素的集合。这个接口对数学集合抽象进行建模,被用来代表集合,就如一副 牌。
List是一个有序集合,可以包含重复元素。你可以通过它的索引来访问任何元素。 List更像长度动态变换 的数组。
Map是一个将key映射到value的对象.一个Map不能包含重复的key:每个key最多只能映射一个value。
一些其它的接口有Queue 、Dequeue 、SortedSet 、SortedMap和ListIterator。
Collection 和 Collections 有什么区别?
Collection 是一个集合接口,它提供了对集合对象进行基本操作的通用接口方法,所有集合都是它的 子类,比如 List 、Set 等。
. Collections 是一个包装类,包含了很多静态方法,不能被实例化,就像一个工具类,比如提供的排序 方法: Collections. sort(list)。
List、 Set、Map是否继承自Collection接口?
List 、Set 是, Map 不是。 Map是键值对映射容器,与List和Set有明显的区别,而Set存储的零散的元素 且不允许有重复元素(数学中的集合也是如此), List是线性结构的容器,适用于按数值索引访问元素 的情形。
Collections.sort排序内部原理
在Java 6中Arrays.sort()和Collections.sort()使用的是MergeSort,而在Java 7中,内部实现换成了 TimSort,其对对象间比较的实现要求更加严格。
List、 Set、Map 之间的区别是什么?
ist 、Set 、Map 的区别主要体现在两个方面:元素是否有序、是否允许元素重复。
HashMap 和 Hashtable 有什么区别?
(1)HashMap允许key和value为null,而HashTable不允许。
(2)HashTable是同步的,而HashMap不是。所以HashMap适合单线程环境, HashTable适合多线程 环境。
(3)在Java1.4中引入了LinkedHashMap , HashMap的一个子类,假如你想要遍历顺序,你很容易从 HashMap转向LinkedHashMap,但是HashTable不是这样的,它的顺序是不可预知的。
(4)HashMap提供对key的Set进行遍历,因此它是fail-fast的,但HashTable提供对key的Enumeration 进行遍历,它不支持fail-fast。
(5)HashTable被认为是个遗留的类,如果你寻求在选代的时候修改Map,你应该使用 CocurrentHashMap。
如何决定使用 HashMap 还是 TreeMap?
对于在 Map 中插入、删除、定位一个元素这类操作, HashMap 是最好的选择,因为相对而言
HashMap 的插入会更快,但如果你要对一个 key 集合进行有序的遍历,那 TreeMap 是更好的选择。
说一下 HashMap 的实现原理?
HashMap 基于 Hash 算法实现的,我们通过 put(key,value)存储, get(key)来获取。当传入 key 时,
HashMap 会根据 key. hashCode() 计算出 hash 值,根据 hash 值将 value 保存在 bucket 里。当计算出 的 hash 值相同时,我们称之为 hash 冲突, HashMap 的做法是用链表和红黑树存储相同 hash 值的
value。当 hash 冲突的个数比较少时,使用链表否则使用红黑树。
说一下 HashSet 的实现原理?
HashSet 是基于 HashMap 实现的, HashSet 底层使用 HashMap 来保存所有元素,因此 HashSet 的实 现比较简单,相关 HashSet 的操作,基本上都是直接调用底层 HashMap 的相关方法来完成, HashSet 不允许重复的值。
ArrayList 和 LinkedList 的区别是什么?
. 数据结构实现: ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
. 随机访问效率: ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据 存储方式,所以需要移动指针从前往后依次查找。
. 增加和删除效率:在非首尾的增加和删除操作, LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时, 更推荐使用 LinkedList。
何Map接口不继承Collection接口?
尽管Map接口和它的实现也是集合框架的一部分,但Map不是集合,集合也不是Map。因此, Map继承 Collection毫无意义,反之亦然。
如果Map继承Collection接口,那么元素去哪儿? Map包含key-value对,它提供抽取key或value列表集 合的方法,但是它不适合“一组对象”规范。
ArrayList和Vector有何异同点?
ArrayList和Vector在很多时候都很类似。
(1)两者都是基于索引的,内部由一个数组支持。
(2)两者维护插入的顺序,我们可以根据插入顺序来获取元素。
(3)ArrayList和Vector的选代器实现都是fail-fast的。
(4)ArrayList和Vector两者允许null值,也可以使用索引值对元素进行随机访问。
以下是ArrayList和Vector的不同点。
(1)Vector是同步的,而ArrayList不是。然而,如果你寻求在选代的时候对列表进行改变,你应该使 用CopyOnWriteArrayList。
(2)ArrayList比Vector快,它因为有同步,不会过载。
(3)ArrayList更加通用,因为我们可以使用Collections工具类轻易地获取同步列表和只读列表。
Array 和 ArrayList 有何区别?
Array 可以存储基本数据类型和对象, ArrayList 只能存储对象。
Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
Array 内置方法没有 ArrayList 多,比如 addAll 、removeAll 、iteration 等方法只有 ArrayList 有。
在 Queue 中 poll()和 remove()有什么区别?
相同点知都是返回第一个元素,并在队列中删除返回的对象。
不同点知如果没有元素 poll()会返回 null,而 remove()会直接抛出 NoSuchElementException 异常。
LinkedHashMap有什么特点?
LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap 时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问次序排序。
HashMap的底层实现原理?(高频问题)
从结构实现来讲, HashMap是数组+链表+红黑树 (JDK1.8增加了红黑树部分)实现的
(1) 从源码可知, HashMap类中有一个非常重要的字段,就是 Node[] table,即哈希桶数组,明显它是 一个Node的数组。我们来看Node[JDK1.8]是何物。Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。上图中的每个 黑色圆点就是一个Node对象。
(2) HashMap就是使用哈希表来存储的。哈希表为解决冲突,可以采用开放地址法和链地址法等来解决 问题, Java中HashMap采用了链地址法。链地址法,简单来说,就是数组加链表的结合。在每个数组元 素上都一个链表结构,当数据被Hash后,得到数组下标,把数据放在对应下标元素的链表上。例如程序 执行下面代码:
map .put("美团" ,"小美 " );
系统将调用"美团"这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),然后 再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时 两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀, Hash碰撞 的概率就越小, map的存取效率就会越高。
如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash 算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈 希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得 Hash碰撞的概率又小,哈希桶数组 (Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制。
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码 可知,构造函数就是对下面几个字段进行初始化,源码如下:
int threshold;
final float loadFactor; int modCount;
int size;// 所能容纳的key-value对极限
// 负载因子
首先, Node[] table的初始化长度length(默认值是16) ,Load factor为负载因子(默认值是0.75),
threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。 也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。结合负载因子的定义公式可知, threshold就是在此Load factor和length(数组长度)对应下允许的最大元 素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因 子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存 空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳 最大键值对数量threshold的区别。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于选代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值 对,但是某个key对应的value值被覆盖不属于结构变化。在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设 计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,Hashtable初始化桶大小为11,就是桶 大小设计为素数的应用 (Hashtable扩容后不能保证还是素数)。 HashMap采用这种非常规设计,主要 是为了在取模和扩容时做优化,同时为了减少冲突, HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况, 一旦出 现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删 改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
map .put("美团" ,"小美 " );
系统将调用"美团"这个key的hashCode()方法得到其hashCode 值(该方法适用于每个Java对象),然后 再通过Hash算法的后两步运算(高位运算和取模运算,下文有介绍)来定位该键值对的存储位置,有时 两个key会定位到相同的位置,表示发生了Hash碰撞。当然Hash算法计算结果越分散均匀, Hash碰撞 的概率就越小, map的存取效率就会越高。
如果哈希桶数组很大,即使较差的Hash算法也会比较分散,如果哈希桶数组数组很小,即使好的Hash 算法也会出现较多碰撞,所以就需要在空间成本和时间成本之间权衡,其实就是在根据实际情况确定哈 希桶数组的大小,并在此基础上设计好的hash算法减少Hash碰撞。那么通过什么方式来控制map使得 Hash碰撞的概率又小,哈希桶数组 (Node[] table)占用空间又少呢?答案就是好的Hash算法和扩容机制。
在理解Hash和扩容流程之前,我们得先了解下HashMap的几个字段。从HashMap的默认构造函数源码 可知,构造函数就是对下面几个字段进行初始化,源码如下:
int threshold;
final float loadFactor; int modCount;
int size;// 所能容纳的key-value对极限
// 负载因子
首先, Node[] table的初始化长度length(默认值是16) ,Load factor为负载因子(默认值是0.75),
threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。 也就是说,在数组定义好长度之后,负载因子越大,所能容纳的键值对个数越多。结合负载因子的定义公式可知, threshold就是在此Load factor和length(数组长度)对应下允许的最大元 素数目,超过这个数目就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。默认的负载因 子0.75是对空间和时间效率的一个平衡选择,建议大家不要修改,除非在时间和空间比较特殊的情况下,如果内存空间很多而又对时间效率要求很高,可以降低负载因子Load factor的值;相反,如果内存 空间紧张而对时间效率要求不高,可以增加负载因子loadFactor的值,这个值可以大于1。size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳 最大键值对数量threshold的区别。而modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于选代的快速失败。强调一点,内部结构发生变化指的是结构发生变化,例如put新键值 对,但是某个key对应的value值被覆盖不属于结构变化。在HashMap中,哈希桶数组table的长度length大小必须为2的n次方(一定是合数),这是一种非常规的设 计,常规的设计是把桶的大小设计为素数。相对来说素数导致冲突的概率要小于合数,Hashtable初始化桶大小为11,就是桶 大小设计为素数的应用 (Hashtable扩容后不能保证还是素数)。 HashMap采用这种非常规设计,主要 是为了在取模和扩容时做优化,同时为了减少冲突, HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。
这里存在一个问题,即使负载因子和Hash算法设计的再合理,也免不了会出现拉链过长的情况, 一旦出 现拉链过长,则会严重影响HashMap的性能。于是,在JDK1.8版本中,对数据结构做了进一步的优化,引入了红黑树。而当链表长度太长(默认超过8)时,链表就转换为红黑树,利用红黑树快速增删 改查的特点提高HashMap的性能,其中会用到红黑树的插入、删除、查找等算法。
JDK1.8与JDK1.7的性能对比
HashMap中,如果key经过hash算法得出的数组索引位置全部不相同,即Hash算法非常好,那样的
话, getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,
所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,
或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn) 。 鉴于JDK1.8做了多方面的优化,总体性能优于 JDK1.7
话, getKey方法的时间复杂度就是O(1),如果Hash算法技术的结果碰撞非常多,假如Hash算极其差,
所有的Hash算法结果得出的索引位置一样,那样所有的键值对都集中到一个桶中,或者在一个链表中,
或者在一个红黑树中,时间复杂度分别为O(n)和O(lgn) 。 鉴于JDK1.8做了多方面的优化,总体性能优于 JDK1.7
HashMap操作注意事项以及优化?
(1) 扩容是一个特别耗性能的操作,所以当程序员在使用HashMap的时候,估算map的大小,初始化的 时候给一个大致的数值,避免map进行频繁的扩容。
(2) 负载因子是可以修改的,也可以大于1,但是建议不要轻易修改,除非情况非常特殊。
(3) HashMap是线程不安全的,不要在并发的环境中同时操作HashMap,建议使用
ConcurrentHashMap。
(4) JDK1.8引入红黑树大程度优化了HashMap的性能。
(5) 还没升级JDK1.8的,现在开始升级吧。 HashMap的性能提升仅仅是JDK1.8的冰山一角。
0 条评论
下一页