Java基础面试题
文章目录
一:JDK,JRE,JVM
JDK
JDK :Jdk还包括了一些Jre之外的东西 ,就是这些东西帮我们编译Java代码的, 还有就是监控Jvm的一些工具 Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。
所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等
JRE
Jre大部分都是 C 和 C++ 语言编写的,他是我们在编译java时所需要的基础的类库 Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。
核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包
如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可
JVM
在倒数第二层 由他可以在(最后一层的)各种平台上运行 Java Virtual Machine是Java虚拟机
Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。
二:字节码(.class)
字节码是Java源代码经过虚拟机编译器编译后产生的文件(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机
Java语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。
所以Java程序运行时比较高效,而且,由于字节码并不专对一种特定的机器,因此,Java程序无须重新编译便可在多种不同的计算机上运行。
Java中引入了虚拟机的概念,即在机器和编译程序之间加入了一层抽象的虚拟机器。
这台虚拟的机器在任何平台上都提供给编译程序一个的共同的接口。
编译程序只需要面向虚拟机,生成虚拟机能够理解的代码,然后由解释器来将虚拟机代码转换为特定系统的机器码执行。
在Java中,这种供虚拟机理解的代码叫做字节码(即扩展为.class的文件),它不面向任何特定的处理器,只面向虚拟机。
每一种平台的解释器是不同的,但是实现的虚拟机是相同的。
Java源程序经过编译器编译后变成字节码,字节码由虚拟机解释执行,虚拟机将每一条要执行的字节码送给解释器,解释器将其翻译成特定机器上的机器码,然后在特定的机器上运行,这就是上面提到的Java的特点的编译与解释并存的解释。
三:八大基本类型
- 整型四个:Byte(1), short(2), int(4), long(8)
- 浮点型两个:float(4),double(8)
- 字符型一个:char(2)
- 布尔型一个:boolean(1)
类型 | 类型名称 | 关键字 | 占用内存 | 作为默认值 |
---|---|---|---|---|
整型 | 字节 | Byte | 1 | 0 |
整型 | 短整型 | short | 2 | 0 |
整型 | 整型 | int | 4 | 0 |
整型 | 长整型 | long | 8 | 0 |
浮点型 | 单精度浮点型 | float | 4 | 0.0 |
浮点型 | 双精度浮点型 | double | 8 | 0.0 |
字符型 | 字符型 | char | 2 | ‘\u0000’ |
布尔型 | 布尔型 | boolean | 1 | false |
四:访问修饰符的可见范围
修饰符 | 当前类 | 同包 | 子类 | 其他包 |
---|---|---|---|---|
private | × | × | × | × |
default | √ | × | × | × |
protected | √ | √ | √ | × |
public | √ | √ | √ | √ |
五:封装,继承,多态
抽象
抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。
抽象只关注对象有哪些属性和行为,并不关注这些行为的细节是什么。
封装
把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法
如果属性不想被外界访问,我们大可不必提供方法给外界访问。
但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能
新类不能选择性地继承父类。通过使用继承我们能够非常方便地复用以前的代码。
- 子类拥有父类非 private 的属性和方法
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展
- 子类可以用自己的方式实现父类的方法
多态
父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。提高了程序的拓展性。
在Java中有两种形式可以实现多态:
- 继承(多个子类对同一方法的重写)
- 接口(实现接口并覆盖接口中同一方法)
Java实现多态有三个必要条件:继承、重写、向上转型
- 继承:在多态中必须存在有继承关系的子类和父类。【inheritance】
- 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
- 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才能够具备技能调用父类的方法和子类的方法。
六:五大基本原则【设计模式相关】
- 单一职责原则SRP
- 类的功能要单一,不能包罗万象,跟杂货铺似的。
- 开放封闭原则OCP
- 拓展是开放的,对于修改是封闭的,想要增加功能热烈欢迎,想要修改
- 里式替换原则LSP
- 子类可以替换父类出现在父类能够出现的任何地方。比如你能代表你爸去你姥姥家干活
- 依赖倒置原则DIP
- 高层次的模块不应该依赖于低层次的模块,他们都应该依赖于抽象。
- 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。
- 就是你出国要说你是中国人,而不能说你是哪个村子的。
- 接口分离原则ISP
- 设计时采用多个与特定客户类有关的接口比采用一个通用的接口要好
- 比如一个手机拥有打电话,看视频,玩游戏等功能,把这几个功能拆分成不同的接口,比在一个接口里要好的多
七:接口和抽象类
抽象类是用来捕捉子类的通用特性的。接口是抽象方法的集合。
从设计层面来说,抽象类是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范
相同点
- 接口和抽象类都不能实例化
- 都位于继承的顶端,用于被其他实现或继承
- 都包含抽象方法,其子类都必须覆写这些抽象方法
不同点
参数 | 抽象类 | 接口 |
---|---|---|
声明 | 抽象类使用abstract关键字声明 | 接口使用interface关键字声明 |
实现 | 子类使用extends关键字来继承抽象类。 如果子类不是抽象类的话,它需要提供抽象类中所有声明的方法的实现 | 子类使用implements关键字来实现接口。 它需要提供接口中所有声明的方法的实现 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象类中的方法可以是任意访问修饰符 | 接口方法默认修饰符是public。 并且不允许定义为 private 或者 protected |
多继承 | 一个类最多只能继承一个抽象类 | 一个类可以实现多个接口 |
字段声明 | 抽象类的字段声明可以是任意的 | 接口的字段默认都是 static 和 final 的 |
🎉 Java8中接口中引入默认方法和静态方法,以此来减少抽象类和接口之间的差异。
🎉 抽象类是自下而上的 -> 有了狗,猫,老鼠这些特征才能有动物这个抽象类;
🎉 接口是自上而下的 -> 归类了一类方法,实现类依赖于接口的规约
🎉 不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类
八:内部类
在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。
内部类本身就是类的一个属性,与其他属性定义方式一致
- 一个内部类对象可以访问创建它的外部类对象的内容,包括私有数据!
- 内部类不为同一包的其他类所见,具有很好的封装性;
- 内部类有效实现了“多重继承”,优化 java 单继承的缺陷。
- 匿名内部类可以很方便的定义回调。
1:静态内部类
// 1:定义在类内部的静态类,就是静态内部类
// 2:静态内部类可以访问外部类所有的静态变量,而不可访问外部类的非静态变量;
// 3:静态内部类的创建方式,new 外部类.静态内部类()
public class Outer {
private static int radius = 1;
static class StaticInner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
}
}
}
// 使用方法
Outer.StaticInner inner = new Outer.StaticInner();
inner.visit();
2:成员内部类
// 1:定义在类内部,成员位置上的非静态类,就是成员内部类
// 2:成员内部类可以访问外部类所有的变量和方法,包括静态和非静态,私有和公有
// 3:成员内部类依赖于外部类的实例,它的创建方式外部类实例.new 内部类()
public class Outer {
private static int radius = 1;
private int count =2;
class Inner {
public void visit() {
System.out.println("visit outer static variable:" + radius);
System.out.println("visit outer variable:" + count);
}
}
}
// 使用方法
Outer outer = new Outer();
Outer.Inner inner = outer.new Inner();
inner.visit();
3:局部内部类
public class Outer {
private int out_a = 1;
private static int STATIC_b = 2;
public void testFunctionClass() {
int inner_c = 3;
class Inner {
private void fun() {
System.out.println(out_a);
System.out.println(STATIC_b);
System.out.println(inner_c);
}
}
Inner inner = new Inner();
inner.fun();
}
public static void testStaticFunctionClass(){
int d = 3;
class Inner {
private void fun() {
// System.out.println(out_a); 编译错误,定义在静态方法中的局部类不可以访问外部类的实例变量
System.out.println(STATIC_b);
System.out.println(d);
}
}
Inner inner = new Inner();
inner.fun();
}
}
// 定义在实例方法中的局部类可以访问外部类的所有变量和方法,定义在静态方法中的局部类只能访问外部类的静态变量和方法。
// 局部内部类的创建方式,在对应方法内,new 内部类()
public static void testStaticFunctionClass(){
class Inner {
}
Inner inner = new Inner();
}
4:匿名内部类
匿名内部类就是没有名字的内部类,日常开发中使用的比较多。
public class Outer {
private void test(final int i) {
// 匿名内部类
new Service() {
public void method() {
for (int j = 0; j < i; j++) {
System.out.println("匿名内部类" );
}
}
}.method();
}
}
//匿名内部类必须继承或实现一个已有的接口
interface Service{
void method();
}
- 匿名内部类必须继承一个抽象类或者实现一个接口。
- 匿名内部类不能定义任何静态成员和静态方法。
- 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。
- 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
九:重写Override和重载OverLoad
- 构造器不能被继承,因此不能被重写,但可以被重载。
- 方法的重载和重写都是实现多态的方式,区别在于重载实现的是编译时的多态性,而重写实现的是运行时的多态性
重载
- 发生在同一个类中,方法名相同参数列表不同(参数类型不同、个数不同、顺序不同)
- 与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分
重写
发生在父子类中,方法名、参数列表必须相同
返回值小于等于父类,抛出的异常小于等于父类,访问修饰符大于等于父类(里氏代换原则)
如果父类方法访问修饰符为private则子类中就不是重写。
十:==,equals,hashCode
1:==
它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象。
基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址
2:equals
它的作用也是判断两个对象是否相等。
但它一般有两种使用情况:
- 情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。
- 情况2:类覆盖了 equals() 方法。一般我们都覆盖 equals() 方法来两个对象的内容相等;若它们的内容相等,则返回 true
🎉 String中的equals方法是被重写过的,因为object的equals方法是比较的对象的内存地址,而String的equals方法比较的是对象的值。
当创建String类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个String对象。
3:hashCode
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。【非常快,O(1)】
这个哈希码的作用是确定该对象在哈希表中的索引位置。
hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”
4:为什么要有 hashCode
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现
但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。
这样可以大大减少equals的次数,相应就大大提高了执行速度【就是先通过hashCode筛查一遍】
5:相关规定
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true【自反性】
- 两个对象有相同的hashcode值,它们也不一定是相等的
- equals 方法被覆盖过,则 hashCode 方法也必须被覆盖[hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等]
6:常出如下考题
package com.cui.uid.helper;
/**
* <p>
* 功能描述:基础帮助类
* </p>
*
* @author cui haida
* @date 2025/04/22/7:24
*/
public class BaseHelper {
public static void main(String[] args) {
// 整数常量池 - 缓存池 - 范围是 -128 ~ 127
// 这个范围内的整数,在常量池中,所以i和j指向同一个对象
Integer i = 123;
Integer j = 123;
System.out.println(i == j); // true 值相同,对象地址相同
System.out.println(i.equals(j)); // true 值相同,对象地址相同
int k = 123;
System.out.println(i == k); // true 值相同,对象地址相同
System.out.println(i.equals(k)); // true 值相同,对象地址相同
// 129 不在 -128 ~ 127 范围,所以i2和j2指向不同的对象
Integer i2 = 129;
Integer j2 = 129;
System.out.println(i2 == j2); // false 值相同,对象地址不同
System.out.println(i2.equals(j2)); // true 值相同,对象地址不同
int k2 = 123;
System.out.println(i2 == k2); // false 对象地址不同
System.out.println(i2.equals(k2)); // false 也是不同
// String - 字符串池常考
String str1 = "cui";
String str2 = "cui";
System.out.println(str1 == str2); // true 对象地址相同,在字符串池中
System.out.println(str1.equals(str2)); // true
String str3 = new String("cui");
System.out.println(str1 == str3); // false, 对象地址不同
System.out.println(str1.equals(str3)); // true,对象的值相同
}
}
十一:反射
1:什么是反射
JAVA反射机制是:在运行状态中:
- 对于任意一个类,都能够知道这个类的所有属性和方法;
- 对于任意一个对象,都能够调用它的任意一个方法和属性;
这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。静态编译和动态编译
- 静态编译:在编译时确定类型,绑定对象
- 动态编译:运行时确定类型,绑定对象
- 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。
2:反射机制的应用场景有哪些
反射是框架设计的灵魂 -> Spring IoC工厂
其他常用:
- 我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序
- 使用反射机制,根据这个字符串获得某个类的Class实例
- 动态配置实例的属性
十二:集合相关的面试题
1:集合的快速失败“fail-fast”机制
是java集合的一种错误检测机制,当多个线程对集合进行结构上的改变的操作时,有可能会产生 fail-fast 机制
假设存在两个线程(线程1、线程2),线程1通过Iterator在遍历集合A中的元素,在某个时候线程2修改了集合A的结构(是结构上面的修改,而不是简单的修改集合元素的内容),那么这个时候程序就会抛出 ConcurrentModificationException 异常,从而产生fail-fast机制
迭代器在遍历时直接访问集合中的内容,并且在遍历过程中使用一个 modCount 变量。
集合在被遍历期间如果内容发生变化,就会改变modCount的值。
每当迭代器使用hashNext()/next()遍历下一个元素之前,都会检测modCount变量是否为expectedmodCount值,是的话就返回遍历;
否则抛出异常,终止遍历
解决方案
- 在遍历过程中,所有涉及到改变modCount值得地方全部加上synchronized
- 使用CopyOnWriteArrayList来替换ArrayList
2:迭代器 Iterator
Iterator 接口提供遍历任何 Collection 的接口。我们可以从一个 Collection 中使用迭代器方法来获取迭代器实例。
迭代器取代了 Java 集合框架中的 Enumeration,迭代器允许调用者在迭代过程中移除元素。
因为所有Collection接继承了Iterator迭代器
public interface Collection<E> extends Iterable<E> {
}
Iterator 的特点是只能单向遍历,但是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出并行修改的异常
边遍历边修改 Collection 的唯一正确方式是使用 Iterator.remove() 方法
// 下面是常见错误
for (Integer i : list) {
list.remove(i); // 运行以上错误代码会报 ConcurrentModificationException 异常
}
// 当使用 foreach(for(Integer i : list)) 语句时,会自动生成一个iterator 来遍历该 list
// 同时该 list 正在被 Iterator.remove() 修改。
// Java 一般不允许一个线程在遍历 Collection 时另一个线程修改它。
3:数组和List的转换
string[] -> list
// string[] -> list
String[] arr = new String[]{"hello", "world", "java"};
List<String> sList = new ArrayList<>();
sList = Arrays.asList(arr);
int[]/float[]/double[] -> list
int[] numbers = new int[]{1, 2, 3, 4, 5, 6};
// java 8+才可以使用
// 流化Arrays.stream(numbers) -> 装箱boxed -> 转成list:collect(Collectors.toList())
List<Integer> list = Arrays.stream(numbers).boxed().collect(Collectors.toList());
list -> String[]
String[] arr = list.toArray(new String[list.size()]);
list -> int[]
int[] numbers = list.stream().mapToInt(Integer::valueOf).toArray();
4:ArrayList和LinkedList的区别
LinkedList 的双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。
所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。
- 数据结构实现:ArrayList 是动态数组的数据结构实现,而 LinkedList 是双向链表的数据结构实现。
- 随机访问效率:ArrayList 比 LinkedList 在随机访问的时候效率要高,因为 LinkedList 是线性的数据存储方式,所以需要移动指针从前往后依次查找。
- 增加和删除效率:在非首尾的增加和删除操作,LinkedList 要比 ArrayList 效率要高,因为 ArrayList 增删操作要影响数组内的其他数据的下标。
- 内存空间占用:LinkedList 比 ArrayList 更占内存,因为 LinkedList 的节点除了存储数据,还存储了两个引用,一个指向前一个元素,一个指向后一个元素。
- 线程安全:ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
综合来说,在需要频繁读取集合中的元素时,更推荐使用 ArrayList,而在插入和删除操作较多时,更推荐使用 LinkedList
5:HashMap & HashSet比较
HashMap | HashCode |
---|---|
实现了Map接口 | 实现Set接口 |
存储键值对 | 仅存储对象 |
调用put()向map中添加元素 | 调用add()方法向Set中添加元素 |
HashMap使用键(Key)计算Hashcode | HashSet使用成员对象来计算hashcode值 对于两个对象来说hashcode可能相同 所以equals()方法用来判断对象的相等性 如果两个对象不同的话,那么返回false |
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 | HashSet较HashMap来说比较慢 |
6:1.7 & 1.8的HashMap
1.7
JDK1.8之前采用的是拉链法。
将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。
1.8
jdk1.8在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时,将链表转化为红黑树,以减少搜索时间。
具体更新
JDK1.8主要解决或优化了以下问题:
- resize 扩容优化
- 引入了红黑树,目的是避免单条链表过长而影响查询效率,红黑树算法请参考
- 解决了多线程死循环问题,但仍是非线程安全的,多线程时可能会造成数据丢失问题。
不同 | JDK1.7 | JDK1.8 |
---|---|---|
存储结构 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
初始化的方式 | inflateTable() | 直接继承到了扩容函数resize()中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次扰动 |
存放数据的规则 | 无冲突时,存放数组; 冲突时,存放链表 | 无冲突时,存放数组; 冲突 & 链表长度 < 8:存放单链表; 冲突 & 链表长度 > 8:树化并存放红黑树 |
插入数据方式 | 头插法 | 尾插法 |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算 | 按照扩容后的规律计算 |
7:红黑树概述
红黑树是一种特殊的二叉查找树。
红黑树的每个结点上都有存储位表示结点的颜色,可以是红(Red)或黑(Black)
- 根黑、叶黑、红子黑
- 每个结点到叶子结点NIL所经过的黑色结点的个数一样的【确保没有一条路径会比其他路径长出俩倍,相对接近平衡的二叉树的!】
- 红黑树的基本操作是添加、删除。 在对红黑树进行添加或删除之后,都会用到旋转方法[L旋,R旋比AVL快很多]
8:HashMap的put流程
当我们put的时候,首先计算 key
的hash
值,这里调用了 hash
方法,hash
方法实际是让key.hashCode()
与key.hashCode()>>>16
进行异或操作
hash 函数大概的作用就是:高16bit不变,低16bit和高16bit做了一个异或,目的是减少碰撞。
- 判断键值对数组table[i]是否为空或为null,否则执行resize()进行扩容;
- 根据键值key计算hash值得到插入的数组索引i,如果
table[i]==null
,直接新建节点添加,转向⑥,如果table[i]不为空,转向③; - 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,否则转向④,这里的相同指的是hashCode以及equals;
- 判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转向5;
- 遍历table[i],判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
9:HashMap扩容流程
1:数组的长度是不是大于64?-> 如果不足64,先扩容数组,重新hash
2:如果数组大小已经达到64,当前的Node下面的子没有达到8个,就是Node链表,否则是红黑树的Node
10:HashMap为什么是两次扰动
这样就是加大哈希值低位的随机性,使得分布更均匀,从而提高对应数组存储下标位置的随机性&均匀性,最终减少Hash冲突
而两次就够了,已经达到了高位低位同时参与运算的目的
11:HashMap 与 HashTable
线程安全方面
- HashMap 是非线程安全的,HashTable 是线程安全的
- HashTable 内部的方法基本都经过
synchronized
修饰【如果你要保证线程安全的话就使用 ConcurrentHashMap】
效率
- 因为线程安全的问题,HashMap 要比 HashTable 效率高一点
- HashTable 基本被淘汰,不要在代码中使用它【如果你要保证线程安全的话就使用 ConcurrentHashMap】
对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 没有这样的机制
12:TreeMap 与 HashMap
TreeMap 是一个有序的key-value集合,它是通过红黑树实现的。
TreeMap基于红黑树(Red-Black tree)实现。
该映射根据其键的自然顺序进行排序,或者根据创建映射时提供的 Comparator 进行排序,具体取决于使用的构造方法。
TreeMap是线程非同步的
假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择
13:ConcurrentHashMap
ConcurrentHashMap对整个桶数组进行了分割分段(Segment),然后在每一个分段上都用lock锁进行保护
相对于HashTable的synchronized锁的粒度更精细了一些,并发性能更好
JDK1.8之后ConcurrentHashMap启用了一种全新的方式实现,利用CAS算法
HashMap的键值对允许有null,但是ConCurrentHashMap都不允许
底层数据结构层面
JDK1.7的 ConcurrentHashMap 底层采用 分段的数组+链表 实现;
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树。
线程安全的实现方式
- 在JDK1.7的时候,ConcurrentHashMap(分段锁)对整个桶数组进行了分割分段(Segment),每一把锁只锁容器其中一部分数据,多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。(默认分配16个Segment,比Hashtable效率提高16倍。
到了 JDK1.8 的时候已经摒弃了Segment的概念,采用的是Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍
十三:辅助工具类
1:Comparable和Comparator的区别
Comparable接口出自java.lang
包,有一个compareTo(T t)
方法用来排序
Comparator接口实际上是出自 java.util
包,它有一个compare(Object obj1, Object obj2)
方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写compareTo方法或compare方法
当我们需要对某一个集合实现两种排序方式,比如一个song对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写compareTo方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的Collections.sort().
2:Collection和Collections的区别
Collection是一个结合接口【集合类的顶级接口】
它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。
Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set。
Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作。