JavaSE 基础 21
1. 字符串常量和字符串常量的区别?
- 形式上:字符常量是单引号引起的一个字符;字符串常量是双引号引起的若干字符
- 含以上:字符常量相当于一个整型值(ASCII值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)
- 占内存大小:字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节)
2. 构造器Constructor 是否可被 override?
Constructor 不能被 override(重写),但是可以 overload(重载),所以你可以看到一个类中有多个构造函数的情况。
3.重载和重写的区别?
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法
区别点 | 重载方法 | 重写方法 |
---|---|---|
发生范围 | 同一个类 | 子类 中 |
参数列表 | 必须修改 | 一定不能修改 |
返回类型 | 可修改 | 一定不能修改 |
异常 | 可修改 | 可以减少或删除,一定不能抛出新的或者更广的异常 |
访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) |
发生阶段 | 编译期 | 运行期 |
4. Java 面向对象编程的三大特征:封装 继承 多态
封装
封装把一个对象的属性私有化,同时提供一些可以被外部访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
继承是使已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前地代码。
关于继承如下 3点请记住:
- 子类拥有父类所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问的。只是拥有。
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
多态
多态性是指允许不同子类型的对象对同一消息作出不同的响应。简单地说就是同样的对象引用调用同样的方法但是做了不同的事情。
5.String、StringBuffer 和 StringBuilder 的区别是什么?String为什么是不可变的?
可变性
简单来说,String类中使用了final关键字修饰字符数组来保存字符串,private final char values[]
,所以String 对象是不可变的。
补充:在Java 9后,String 类的实现改用 byte 数组存储字符串
private final byte[] vlaue
而 StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在AbstractStringBuilder 中也是使用字符数组保存字符串 char[] value
,但是没有用 final 关键字修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造函数也就是 AbstractStringBuilder 实现的,大家可以自行查阅源码。
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
/**
* The value is used for character storage.
*/
char[] value;
/**
* The count is the number of characters used.
*/
int count;
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义了一些字符串的基本操作。如expandCapacity、append、insert、indexOf 等公共方法。StringBuffer 对方法加了同步锁或者对调用方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
性能
每次对String类型进行改变的时候,都会生成一个新的String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
对于三者使用的总结:
- 操作少量的数据:使用 String
- 单线程操作字符串缓冲区操作大量数据:使用StringBuilder
- 多线程操作字符串缓冲区操作大量数据:使用StringBuffer
6. 自动装箱与拆箱
- 装箱:将基本类型用它们对应的包装类包装起来。
- 拆箱:将包装类转换成对应的基本数据类型。
7.在一个静态方法内调用一个非静态成员为什么是非法的?
因为静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
8.在 Java 中定义一个不做事且没有参数的构造方法的作用
Java程序中在执行子类的构造方法之前,如果没有使用super()
来调用父类特定的构造方法,则会调用父类“没有参数的构造方法”,因此,如果父类中只定义了有参数的构造方法,而子类的构造方法又没有super()
来调用父类特定的构造方法,则编译时将会发生错误,因为 Java 程序在父类中找不到没有参数的构造方法可供执行。而解决办法则在父类中声明一个无参的构造器。如果父类未定义有参数的构造器,编译器会自动生成一个无参的构造器。
9.接口和抽象类的区别是什么?
- 接口的默认方法是public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。
- 接口除了 static、final 变量,不能有其他变量。而抽象类中则不一定。
- 一个类可以实现多个接口,但只能继承一个抽象类。接口自己本身可以通过 extends 关键字扩展多个接口。
- 接口方法默认修饰符是public,抽象方法可以有public、protected 和 default 这些修饰符(抽象类就是为了被重写所以不能使用private 关键字修饰!)
- 从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为规范。
备注:
- 在JDK8中,接口也可以定义静态方法,可以直接用接口名调用。实现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,则必须重写。不然报错。
JDK9 的接口被允许定义私有方法。
总结一下jdk7~jdk9 Java 中接口概念的变化:
- 在JDK7或更早的版本中,接口里面只能由常量变量和抽象方法。这些接口方法必须由选择实现接口的类实现。
- JDK8的时候接口可以有默认方法和静态方法的功能。
- JDK9在接口中引入了私有方法和私有静态方法。
10.成员变量与局部变量的区别有哪些?
- 从语法形式上看:成员变量是属于类的,而局部变量是方法中定义的变量或是方法的参数;成员变量可以被 public、private、static 等修饰符所修饰,而局部变量不能被访问修饰符及static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
- 从变量在内存中的方式来看:如果成员变量是使用
static
修饰的,那么这个成员变量是属于类的,如果没有使用static
修饰,这个成员变量是属于实例的。对象存于堆内存,如果局部变量类型未基本数据类型,那么存储在栈内存,如果为引用数据类型,那么存放的是指向堆内存对象的引用或是指向常量池中的地址。 - 从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而产生和消失。
- 成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
11.构造方法有什么特点?
- 名字和类名相同。
- 没有返回值,但不能用 void 声明构造函数。
- 生成类地对象时自动执行,无需调用。
12.静态方法和实例方法有何不同?
- 在外部调用静态方法时,可以使用"类名.方法名"的方式,也可以使用“对象.方法名”的方式。而实例方法只有后者这种方式。也就是说,调用静态方法无需创建对象。
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法。实例方法则无法限制。
13.==与equals
==
:基本数据类型比较的是内容是否相等。对象则判断两个对象的地址是否相等。即判断两个对象是不是同一个对象。
equals
:默认比较的是两个对象是否相等。即比较的是对象的地址是否相等。除了重写equals方法的个别类是比较的是内容,如:File类、String类
14.hashCode 与 equals
15.简述线程、程序、进程的基本概念。以及它们之间的关系?
16.线程有哪些基本状态?
Java 线程在运行的生命周期中的指定时刻只可能处于下面的6种不同状态的其中一种状态
状态名称 | 说明 |
---|---|
NEW | 初始状态,线程被构建,但是还没有调用start()方法 |
RUNNABLE | 运行状态,Java线程将操作系统中的就绪和运行两种状态笼统地称作“运行中” |
BLOCKED | 阻塞状态,表示线程阻塞于锁 |
WAITTING | 等待状态,表示线程进入等待状态,进入该状态表示当前线程需要等待其他线程做出一些特定的通知(通知或中断) |
TIME_WAITING | 超时等待状态,该状态不同于WAITING,它是可以在指定地时间自行返回的 |
TERMINATED | 终止状态,表示当前线程已经执行完毕 |
线程在生命周期中并不是固定处于某一个状态而是随着代码的执行在不同状态之间切换。Java 线程状态变迁如下图所示:
由上图可以看除出:
线程创建之后将处于NEW(新建)状态,调用start()
方法后开始运行,线程这时候处于READY(就绪)状态。可运行状态线程获得了cpu时间片(timeslice)后就处于RUNNING(运行)状态。
操作系统隐藏Java虚拟机(JVM)中的READY和 RUNNING状态,它只能看到RUNNALBE状态。所以,Java系统一般将这两个状态统称为RUNNABLE(运行中)状态。
当线程执行wait()
方法后,线程进入WAITING(等待)状态。进入等待状态的线程需要依靠其他线程的通知才能够返回到运行状态,而TIME_WAITING(超时等待)状态相当于在等待状态的基础上增加了超时限制,比如通过sleep(long millis)
方法或 wait(long millis)
方法可以将Java 线程置于TIMED WAITING 状态。当超时时间到达后 Java 线程将会返回到 RUNNABLE 状态。当线程调用同步方法时,在没有获取到锁的情况下,线程将会进入到 BLOCKED(阻塞) 状态。线程在执行 Runnable 的run()
方法之后将会进入到 TERMINATED(终止) 状态。
17. 关于final 关键字的一些总结
final 关键字主要用在三个地方:变量、方法、类
- 对于一个final 变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。
- 当一个final 修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为 final 方法。
- 使用final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为 final。
18. Java 中的异常处理
Java 异常类层次结构图
在Java 中,所有的异常都有一个共同的祖先 java.lang包中的Throwable类。Throwable:有两个重要的子类:Exception(异常)和Error(错误)。二者都是 Java 异常处理的重要子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM(Java 虚拟机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当 JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身、或者发生在虚拟机试图执行应用时,如 Java 虚拟机运行错误(Virtual MachineError)、类定义错误(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception类有一个重要的子类RuntimeException。RuntimeException 异常由Java 虚拟机抛出。NullPointerException(要访问的变量没有引用任何对象时,抛出该异常)、ArithmeticException(算术运算异常,一个整数除以 0 时,抛出该异常)和 ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常本身能被程序处理,错误是无法处理。
Throwable 类常用方法
public string getMessage()
:返回异常发生时的简要描述public string toString()
:返回异常发生时的详细信息public string getLocalizedMessage():
返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同public void printStackTrace()
:在控制台上打印 Throwable 对象封装的异常信息
异常处理总结
- try 块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
- catch 块: 用于处理 try 捕获到的异常。
- finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
在以下 4 种特殊情况下,finally 块不会被执行:
- 在 finally 语句块第一行发生了异常。 因为在其他行,finally 块还是会得到执行
- 在前面的代码中用了 System.exit(int)已退出程序。 exit 是带参函数 ;若该语句在异常语句之后,finally 会执行
- 程序所在的线程死亡。
- 关闭 CPU。
**注意:**当 try 语句和 finally 语句中都有 return 语句时,在方法返回之前,finally 语句的内容将被执行,并且 finally 语句的返回值将会覆盖原始的返回值。如下:
public static int f(int value) {
try {
return value * value;
} finally {
if (value == 2) {
return 0;
}
}
}
如果调用 f(2)
,返回值将是 0,因为 finally 语句的返回值覆盖了 try 语句块的返回值。
19.Java序列化中如果有些字段不想进行序列化,怎么办?
对于不想进行序列化的变量,使用 transient
关键字进行修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的变量序列化。被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
20. Java 中的 IO 流
Java 中 IO流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以分为字节流和字符流;
- 按照流的角色划分为节点流和处理流;
Java IO流共涉及40多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
- InputStream/Reader:所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer:所有输出流的基类,前者是字节输出流,后者是字符输出流。
操作方式分类结构图:
按操作对象分类结构图:
既然有了字节流,为什么还要有字符流?
问题本质想问:不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作分为字节流操作和字符流操作呢?
回答:字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
BIO,NIO,AIO 有什么区别?
- BIO(Blocking I/O):同步阻塞 I/O 模式,数据的读取写入必须阻塞在一个线程内等待其完成。在活动连接数不是特别高(小于单机 1000)的情况下,这种模型是比较不错的,可以让每一个连接专注于自己的 I/O 并且编程模型简单,也不用过多考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲一些系统处理不了的连接或请求。但是,当面对十万甚至百万级连接的时候,传统的 BIO 模型是无能为力的。因此,我们需要一种更高效的 I/O 处理模型来应对更高的并发量。
- NIO (Non-blocking/New I/O): NIO 是一种同步非阻塞的 I/O 模型,在Java 1.4 中引入了 NIO 框架,对应 java.nio 包,提供了Channel,Selector,Buffer 等抽象。NIO 的N可以理解为 None-blocking,不单纯是New。它支持面向缓冲的,基于通道的I/O操作方法。NIO提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发
- AIO (Asynchronous I/O): AIO 也就是 NIO 2。在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型。异步 IO 是基于事件和回调机制实现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操作系统会通知相应的线程进行后续的操作。AIO 是异步 IO 的缩写,虽然 NIO 在网络操作中,提供了非阻塞的方法,但是 NIO 的 IO 行为还是同步的。对于 NIO 来说,我们的业务线程是在 IO 操作准备好时,得到通知,接着就由这个线程自行进行 IO 操作,IO 操作本身是同步的。查阅网上相关资料,我发现就目前来说 AIO 的应用还不是很广泛,Netty 之前也尝试使用过 AIO,不过又放弃了。
21. 深拷贝 vs 浅拷贝
- 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
- 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。
Java 集合
1. 说说List、Set、Map 三者的区别?
- List:有序、可重复、有索引的集合,继承了Collection集合全部功能,除了Collection三种遍历方式外,可用索引遍历。
- Set: 无序、不可重复的集合,Set 的实现类 LinkedHashSet和 TreeSet 是有序的,LinkedHashSet可以按照元素插入的顺序排序,也可以按照元素操作的时间排序,TreeSet可以按照默认的比较规则或者自定义比较规则排序。
- Map:是无序、以key-value的键值对形式存储元素的集合,键不可重复,值无要求,重复的键对应的值会覆盖之前的值。
2.ArrayList 和 LinkedList 区别?
- 1 . 是否线程安全:
ArrayList
和LinkedList
都是不同步的,也就是不保证线程安全; - 2 .底层数据结构:
ArrayList
底层使用的是Object
数组;LinkedList
底层使用的是双向链表数据结构(JDK1.6之前为循环链表,JDK1.7取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) - 3 .插入和删除是否受元素位置的影响:①
ArrayList
采用数组存储,所以插入和删除的时间复杂度受元素位置的影响。比如:执行add(E e)
方法的时候,ArrayList
会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是O(1)。但是如果要在指定位置 i 插入和删除元素的话(add(int index, E element)
)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。②LinkedList
采用链表存储,所以对于add(E e)
方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置i
插入和删除元素的话((add(int index, E element)
) 时间复杂度近似为o(n))因为需要先移动到指定位置再插入。 - 4 .是否支持快速访问:
LinkedList
不支持高效的随机元素访问,而ArrayList
支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于get(int index)
方法)。 - 5 .内存空间占用:ArrayList 的空间浪费主要体现在list列表的结尾会预留一定的容量空间。而LinkedList的空间花费则体现在它的每一个元素都需要消耗比ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
补充内容:RandomAccess 接口
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess
接口中什么都没有定义。所以,在我看来 RandomAccess
接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
在 binarySearch()
方法中,它要判断传入的list 是否RamdomAccess
的实例,如果是,调用indexedBinarySearch()
方法,如果不是,那么调用iteratorBinarySearch()
方法
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
ArrayList
实现了RandomAccess
接口,而LinkedList
没有实现。为什么呢?我觉得还是和底层数据结构有关!ArrayList
底层是数组,而 LinkedList
底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,ArrayList
实现了 RandomAccess
接口,就表明了他具有快速随机访问功能。 RandomAccess
接口只是标识,并不是说 ArrayList
实现 RandomAccess
接口才具有快速随机访问功能的!
下面再总结以下 list的遍历方式选择:
- 实现了
RandomAccess
接口的list,优先选择普通 for 循环 ,其次 foreach, - 未实现
RandomAccess
接口的list,优先选择iterator遍历(foreach遍历底层也是通过iterator实现的,),大size的数据,千万不要使用普通for循环
3.ArrayList 与 Vector 区别呢?为什么要用ArrayList取代Vector呢?
Vector
类的所有方法都是同步的。可以由两个线程安全地访问一个Vector对象、但是一个线程访问Vector的话代码要在同步操作上耗费大量的时间。
ArrayList
不是同步的,所以在不需要保证线程安全时建议使用ArrayList。
4. 说一说ArrayList的扩容机制
后面会写一篇文章详细讲
5.HashMap 和 Hashtable 的区别?
- 线程是否安全:HashMap是非线程安全的,Hashtable 是线程安全的;Hashtable 内部的方法基本都经过
synchronized
修饰。(如果你要保证线程安全的话可以使用ConcurrentHashMap吧) - 效率:因为线程安全问题,HashMap要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它。
- 对Null key 和 Null value 的支持:HashMap中,null 可以作为键,这样的键只有一个,可以有一个或多个键所对应的值为null,但是,Hashtable中键和值都不支持null,只要put进去的键值有一个null,直接抛出NullPoniterException。
- 初始容量大小和每次扩容大小的不同:①创建时如果不指定容量初始值,Hashtable 默认的初始大小为11,之后每次扩容,容量变为原来的2n+1,HashMap默认的初始化大小为16,之后每次扩容,容量为原来的2倍。②创建时如果给定了容量初始值,那么Hashtable会直接使用你给定的大小,而HashMap会将其扩充为2的幂次方大小(HashMap 中的
tableSizefor()
方法保证,下面给出了源代码)。也就是说 HashMap 总是使用2的幂作为哈希表的大小,后面会介绍到为什么是2的幂次方。 - 底层数据结构:JDK1.8以后的HashMap在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。Hashtable没有这样的机制。
HashMap中带有初始容量的构造函数:
public HashMap(int initialCapacity, float loadFactor) {
if (initialCapacity < 0)
throw new IllegalArgumentException("Illegal initial capacity: " +
initialCapacity);
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
if (loadFactor <= 0 || Float.isNaN(loadFactor))
throw new IllegalArgumentException("Illegal load factor: " +
loadFactor);
this.loadFactor = loadFactor;
this.threshold = tableSizeFor(initialCapacity);
}
public HashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR);
}
下面这个方法保证了 HashMap 总是使用2的幂作为哈希表的大小。
/**
* Returns a power of two size for the given target capacity.
*/
static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
6.HashMap 和HashSet 区别
如果你看过HashSet
源码的话就应该知道:HashSet底层就是基于HashMap实现的。(HashSet 的源码非常非常少,因为除了 clone()、writeObject()、readObject()是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
HashMap | HashSet |
---|---|
实现了Map接口 | 实现了Set接口 |
存储键值对 | 仅存对象 |
调用put() 向map中添加元素 | 调用add() 方法向Set中添加元素 |
HashMap使用键(Key)计算HashCode | HashSet使用成员对象来计算HashCode,对于两个对象来说hashcode可能相同,使用equals()方法用来判断对象的相等性 |
7.HashSet如何检查重复
当你把对象加入HashSet
时,HashSet会先计算对象的hashcode
值来判断对象加入的位置,同时也会与其他加入的对象的hashcode值比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同的hashcode值的对象,这时会调用equals()
方法来检查hashcode相等的对象是否真的相同。如果两者相同,HashSet就不会让加入操作成功。
hashcode()与equals()的相关规定:
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个equals方法返回true
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 综上,equals方法被覆盖过,则hashCode方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写hashCode(),则该class的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)。
8.HashMap的底层实现
JDK1.8之前
JDK1.8之前HashMap
底层是数组和链表结合在一起使用也就是链表散列。HashMap 通过key 的 hashcode 经过扰动函数处理过后得到hash值,然后通过(n-1) & hash 判断当前元素存放的位置(这里的n指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。
所谓扰动函数指的就是 HashMap 的hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的hashcode() 方法,换句话说使用扰动函数之后可以减少碰撞。
JDK1.8 HashMap 的 hash 方法源码:
JDK1.8 的 hash 方法相比于 JDK1.7 hash 方法更加简化,但是原理不变。
static final int hash(Object key) {
int h;
// key.hashCode():返回散列值也就是hashcode
// ^ :按位异或
// >>>:无符号右移,忽略符号位,空位都以0补齐
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
对比一下 JDK 1.7 的 HashMap 的hash 方法源码:
static int hash(int h) {
// This function ensures that hashCodes that differ only by
// constant multiples at each bit position have a bounded
// number of collisions (approximately 8 at default load factor).
h ^= (h >>> 20) ^ (h >>> 12);
return h ^ (h >>> 7) ^ (h >>> 4);
}
相比于 JDK1.8的 hash 方法,JDK1.7的 hash 方法的性能会稍差一点点,因为毕竟扰动了4次。
所谓拉链法就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
JDK1.8之后
相比之前的版本,JDK1.8之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
TreeMap、TreeSet以及JDK1.8之后的HashMap底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。
9.HashMap的长度为什么是2的幂次方
为了能让HashMap存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,我们也讲到过了,Hash的范围值在-2147483648到2147483647,前后加起来大概40亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个40亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ (n - 1) & hash”
。(n代表数组长度)。这也就解释了为什么HashMap的长度为什么是2的幂次方。
这个算法应该如何设计呢?
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:“取余(%)操作中如果除数是2的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是2的 n 次方;)。” 并且 采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是2的幂次方。
10. HashMap 多线程操作导致死循环问题
主要原因在于 并发下Rehash 会造成元素之间形成一个循环链表。不过,JDK1.8解决了这个问题,但是还是不建议在多线程下使用HashMap,因为多线程下使用HashMap还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap。
11.ConcurrentHashMap 和 HashTable 的区别
ConcurrentHashMap 和 HashTable 的区别主要体现在实现线程安全的方式上的不同。
- 底层数据结构:JDK1.7的ConcurrentHashMap 底层采用分段的数组+链表实现,JDK1.8采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。Hashtable 和 JDK1.8之前的HashMap 的底层数据结构类似都是采用 数组+链表的形式,数组是HashMap的主体,链表是主要为了解决哈希冲突而存在的。
- 实现线程安全的方式(重要):①在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。到了JDK1.8的时候以及摒弃了Segment的概念,而是直接用Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK1.6以后对 synchronized 锁做了很多优化)整个看起来就像是优化且线程安全的HashMap,虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本;② HashTable(同一把锁):使用synchronized 来保证线程安全,效率非常低下,当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一线程不能用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。
两者的对比图
图片来源:http://www.cnblogs.com/chengxiao/p/6842045.html
JDK1.7的 ConcurrentHashMap:
JDK1.8的ConcurrentHashMap(TreeBin:红黑二叉树节点,Node:链表节点):
12.ConcurrentHashMap线程安全的具体实现方式/底层具体实现
JDK1.7(上面有示意图)
首先将数据分为一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。
ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是一种可重入锁,扮演锁的角色。HashEntry 用于存储键值对数据。
static class Segment<K,V> extends ReentrantLock implements Serializable {
}
一个ConcurrentHashMap 里面包含一个Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment的锁。
JDK1.8(上面有示意图)
ConcurrentHashMap取消了Segment分段锁,采用CAS和synchronized来保证并发安全。数据结构跟HashMap1.8的结构类似,数组+链表/红黑二叉树。Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(log(N)))
synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。
13. comparable 和 Comparator 的区别
- comparable 接口实际上是出自 java.lang 包,它有一个
compareTo(Object obj)
方法用来排序 - comparator 接口实际上是出自 java.util包,它有一个
compare(Object obj1,Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo()
方法或compare()
方法,当我们需要对某一个集合实现两种排序方式,比如song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo()
方法和使用自制的Comparator 方法或者以两个Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort()
Comparator定制排序
ArrayList<Integer> arrayList = new ArrayList<Integer>();
arrayList.add(-1);
arrayList.add(3);
arrayList.add(3);
arrayList.add(-5);
arrayList.add(7);
arrayList.add(4);
arrayList.add(-9);
arrayList.add(-7);
System.out.println("原始数组:");
System.out.println(arrayList);
// void reverse(List list):反转
Collections.reverse(arrayList);
System.out.println("Collections.reverse(arrayList):");
System.out.println(arrayList);
// void sort(List list),按自然排序的升序排序
Collections.sort(arrayList);
System.out.println("Collections.sort(arrayList):");
System.out.println(arrayList);
// 定制排序的用法
Collections.sort(arrayList, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
System.out.println("定制排序后:");
System.out.println(arrayList);
Output:
原始数组:
[-1, 3, 3, -5, 7, 4, -9, -7]
Collections.reverse(arrayList):
[-7, -9, 4, 7, -5, 3, 3, -1]
Collections.sort(arrayList):
[-9, -7, -5, -1, 3, 3, 4, 7]
定制排序后:
[7, 4, 3, 3, -1, -5, -7, -9]
重写compareTo方法实现按年龄来排序
// person对象没有实现Comparable接口,所以必须实现,这样才不会出错,才可以使treemap中的数据按顺序排列
// 前面一个例子的String类已经默认实现了Comparable接口,详细可以查看String类的API文档,另外其他
// 像Integer类等都已经实现了Comparable接口,所以不需要另外实现了
public class Person implements Comparable<Person> {
private String name;
private int age;
public Person(String name, int age) {
super();
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
/**
* TODO重写compareTo方法实现按年龄来排序
*/
@Override
public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) {
return 1;
} else if (this.age < o.getAge()) {
return -1;
}
return age;
}
}
public static void main(String[] args) {
TreeMap<Person, String> pdata = new TreeMap<Person, String>();
pdata.put(new Person("张三", 30), "zhangsan");
pdata.put(new Person("李四", 20), "lisi");
pdata.put(new Person("王五", 10), "wangwu");
pdata.put(new Person("小红", 5), "xiaohong");
// 得到key的值的同时得到key所对应的值
Set<Person> keys = pdata.keySet();
for (Person key : keys) {
System.out.println(key.getAge() + "-" + key.getName());
}
}
Output:
5-小红
10-王五
20-李四
30-张三
14. 集合框架底层数据结构总结
Collection
1、List
- ArrayList:Object数组
- Vector:Object数组
- LinkedList:双向链表(JDK1.6之前为循环链表,JDK1.7取消了循环)
2、Set
- HashSet(无序,唯一):基于HashMap 实现的,底层采用 HashMap 来保存元素
- LinkedHashSet:LinkedHashSet 继承于 HashSet,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是是基于 HashMap 实现一样,不过还是有一点点区别的
- TreeSet(有序、唯一):红黑树(自平衡的排序二叉树)
Map
- HashMap:JDK1.8之前HashMap由数组+链表组成的,数组是HashMap的主体,链表则是主要为了解决哈希冲突而存在的("拉链法"解决冲突)。JDK1.8以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间
- LinkedHashMap: LinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。
- Hashtable: 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- TreeMap: 红黑树(自平衡的排序二叉树)