Java/计算机网络/操作系统面试题总结(未完待续)
Posted Icedzzz
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java/计算机网络/操作系统面试题总结(未完待续)相关的知识,希望对你有一定的参考价值。
Java基础
面向对象和面向过程
- **面向过程(Procedure Oriented)**是一种以过程为中心的编程思想。
- 面向过程就是分析出解决问题所需要的步骤,然后用函数把这些步骤一步一步实现,使用的时候一个一个依次调用就可以了。是一种思考问题的基础方法。
- 简单理解:面向过程就是任何事情都亲力亲为,很机械,像个步兵。
- 面向对象(Object Oriented)是一种对现实世界理解和抽象的方法;是思考问题相对高级的方法。
- 面向对象是把构成问题事务分解成各个对象,建立对象的目的不是为了完成一个步骤,而是为了描叙某个事物在整个解决问题的步骤中的行为。
- ⾯向过程性能⽐⾯向对象⾼。 因为类调⽤时需要实例化,开销⽐较⼤,⽐较消耗资源
- ⾯向过程没有⾯向对象易维护、易复⽤、易扩展
字符型常量和字符串常量的区别
- 形式上: 字符常量是单引号引起的⼀个字符; 字符串常量是双引号引起的若⼲个字符
- 含义上: 字符常量相当于⼀个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表⼀个地
址值(该字符串在内存中存放位置) - 占内存⼤⼩ 字符常量只占 2 个字节; 字符串常量占若⼲个字节 (注意: char 在 Java 中占两
个字节)
为什么java是值传递?
按值调用(call by value)表示方法接收的是调用者提供的值,而按引用调用(call by reference)表示方法接收的是调用者提供的变量地址。一个方法可以修改传递引用所对应的变量值,而不能修改传递值调用所对应的变量值。
基础类型传递:
一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
对象传递:
实现一个改变对象参数状态的方法并不是一件难事。理由很简单,方法得到的是对象引用的拷贝,对象引用及其他的拷贝同时引用同一个对象。
引用传递:
一个方法不能让对象参数引用一个新的对象。
方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,这个方法交换的是这两个拷贝
equals和hashcode
hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。
例如在HashMap中,因为我们如果在设计两个对象相等的逻辑时,如果不重写Equals方法,那么一个类有两个对象A1, A2,他们的A1.equals(A2)为true,A1.hashcode和A2.hashcode不一样,当将A1和A2都作为HashMap的key时, HashMap会认为它两不相等,因为HashMap在判断key值相不相等时会判断key的hashcode是不是一样, hashcode一样相等,所以在这种场景下会出现我们认为这两个对象相等,但是hashmap不这么认为,所以会有问题。
hashCode()与 equals()的相关规定:
- 如果两个对象相等,则 hashcode 一定也是相同的
- 两个对象相等,对两个对象分别调用 equals 方法都返回 true
- 两个对象有相同的 hashcode 值,它们也不一定是相等的
- 因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖
- hashCode()的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)
多态
来源:https://www.zhihu.com/question/21162041/answer/1268903417
在Java中,方法调用有两类,动态方法调用与静态方法调用。
- 静态方法调用是指对于类的静态方法的调用方式,是在编译时刻就已经确定好具体调用方法的情况,是静态绑定的。
- 动态方法调用需要有方法调用所作用的对象,是在调用的时候才确定具体的调用方法,是动态绑定的。我们这里所讲的多态就是后者—动态方法调用。
多态就是指程序中定义的引⽤变量所指向的具体类型和通过该引⽤变量发出的⽅法调⽤在编译时并不确定,⽽是在程序运⾏期间才确定,即⼀个引⽤变量到底会指向哪个类的实例对象,该引⽤变量发出的⽅法调⽤到底是哪个类中实现的⽅法,必须在由程序运⾏期间才能决定。
多态的具体实现:
(1) 方法重载(类内部之间的多态):就是在类中可以创建多个方法,它们具有相同的名字,但可具有不同的参数列表、返回值类型。
(2)方法重写(父类与子类之间的多态):子类可继承父类中的方法,但有时子类并不想原封不动地继承父类的方法,而是想作一定的修改,这就需要采用方法的重写。重写的参数列表和返回类型均不可修改。
多态分析:
从JVM的角度看:多态方法存在于方法区。java堆存的是就是我们建立的一个个实例对象,而方法区存的就是类的类型信息。而且这个方法区中的类型信息跟在堆中存放的class对象是不同的。在方法区中,这个class的类型信息只有唯一的实例(所以方法区是各个线程共享的内存区域),而在堆中可以有多个该class对象。也就是说方法区的类型信息就是像一个模板,那些class对象就好比通过这些模板创建的一个个实例。
整体流程:
第一步:虚拟机通过reference查询java栈中的本地变量表,得到堆中的对象类型数据的指针,
第二步:通过到对象的指针找到方法区中的对象类型数据
第三步:查询方法表定位到实际类的方法运行。
方法表存在于方法区中的,它是实现多态的关键所在,这里面保存的就是实例方法的引用,而且是直接引用。java虚拟机在执行程序的时候就是通过这个方法表来确定运行哪一个多态方法的。
当子类方法重写父类方法时,新的数据会覆盖原有的数据,也就是说原来指向父类的那个引用会被替换成指向子类的引用(占据原来表中的位置)
符号引用和直接引用的区别:
符号引用就是字符串,这个字符串包含足够的信息,以供实际使用时可以找到相应的位置。你比如说某个方法的符号引用,如:“java/io/PrintStream.println:(Ljava/lang/String;)V”。里面有类的信息,方法名,方法参数等信息。当第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。运行一次之后,符号引用会被替换为直接引用,下次就不用搜索了。直接引用就是偏移量,通过偏移量虚拟机可以直接在该类的内存区域中找到方法字节码的起始位置。
封装和继承
封装:
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
为什么需要封装?
封装将不需要对外提供的内容都隐藏起来**,把属性隐藏,提供公共方法对其访问**。好处就是:隐藏实现细节,提供公共的访问方式;提高了代码的复用性;提高了安全性。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,**但不能选择性地继承父类。**通过使用继承我们能够非常方便地复用以前的代码。
关于继承如下 3 点请记住:
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
String StringBuffer 和 StringBuilder 的区别是什么?
- String 类中使⽤ final 关键字修饰字符数组来保存字符串, private final char value[] ,所以 **String 对象是不可变的。**也就可以理解为常量,线程安全。
- StringBuilder 与 StringBuffer 都继承⾃ AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使⽤字符数组保存字符串 char[] value 但是没有⽤ final 关键字修饰,所以这两种对象都是可变的
- StringBuffer 对⽅法加了同步锁或者对调⽤的⽅法加了同步锁,所以是线程安全的。
- StringBuilder 并没有对⽅法进⾏加同步锁,所以是⾮线程安全的。
- 每次对 String 类型进⾏改变的时候,都会⽣成⼀个新的 String 对象,然后将指针指向新的 String对象。
- StringBuffer 每次都会对 StringBuffer 对象本身进⾏操作,⽽不是⽣成新的对象并改变对象引⽤。
- StringBuilder 相⽐使⽤ StringBuffer 仅能获得 10%~15% 左右的性能提升(线程切换),但却要冒多线程不安全的⻛险
- 操作少量的数据: 适⽤ String;单线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuilder;多线程操作字符串缓冲区下操作⼤量数据: 适⽤ StringBuffer
为什么String要设计为不可变的?
-
便于实现字符串池(String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。 -
使多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。 -
避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。 -
加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
反射
JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。
- 优点: 运行期类型的判断,动态加载类,提高代码灵活度。
- 缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的 java 代码要慢很多。
反射应用场景:
① 我们在使用 JDBC 连接数据库时使用 Class.forName()通过反射加载数据库的驱动程序;②Spring 框架也用到很多反射机制,最经典的就是 xml 的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中;2)Java 类里面解析 xml 或 properties 里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的 Class 实例; 4)动态配置实例的属性
自动装箱和拆箱
装箱: 把一个基本数据类型换成其对应的包装类,基本数据类型存在栈中,而包装类存在堆中,所以装箱时会在堆中分配内存空间,创建一个新对象,性能损耗较大。
拆箱: 把一个包装类转换成其对应的基本数据类型,包装类在堆中,基本数据类型在栈中,由于拆箱后返回的值为基本数据类型,存在栈中,性能损耗不大。
如何实现自动装箱/拆箱?
在**装箱时调用Integer.valueOf()方法,在拆箱时调用Integer.intValue()**方法。同理,其他的基本数据类型装箱时都执行 对应包装类名.valueOf(),拆箱时执行 对应包装类名.xxValue() (xx为基本数据类型标识)。
Int类型的自动装箱:
装箱操作就要麻烦一点,会先判断装箱操作的值是否在一个范围之间:如果在这个范围内,就从缓存中返回一个缓存对象(该范围通常为-128~127);若不在这个范围内,则再新建一个对象返回。
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
抽象类和接口
- 接口的方法默认是 public,所有方法在接口中不能有实现,抽象类可以有非抽象的方法
- 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
- 一个类可以实现多个接口,但最多只能实现一个抽象类
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
final
final 在 Java 中是一个保留的关键字,可以声明成员变量、方法、类以及本地变量。一旦你将引用声明作 final,你将不能改变这个引用了,编译器会检查代码,如果试图将变量再次初始化的话,编译器会报编译错误。
**final变量:**凡是对成员变量或者本地变量(在方法中的或者代码块中的变量称为本地变量)声明为 final 的都叫作 final 变量。final 变量是只读的。
final方法: Java 里用 final 修饰符去修饰一个方法的唯一正确用途就是表达:这个方法原本是一个虚方法,现在通过 final 来声明这个方法不允许在派生类中进一步被覆写(override)
final类: 使用 final 来修饰的类叫作 final 类,final类通常功能是完整的,它们不能被继承,Java 中有许多类是 final 的,比如 String, Interger 以及其他包装类。
内存模型中的final :
final 变量,编译器和处理器都要遵守两个重排序规则,保证 final 变量在对其他线程可见之前,能够正确的初始化完成:
- 构造函数内,对一个 final 变量的写入,与随后把这个被构造对象的引用赋值给一个变量,这两个操作之间不可重排序(保证,在对象引用对任意线程可见之前,对象的 final 变量已经正确初始化了)
- 首次读一个包含 final 变量的对象,与随后首次读这个 final 变量,这两个操作之间不可以重排序(保证在读一个对象的 final 变量之前,一定会先读这个对象的引用)
final关键字的好处:
- final 关键字提高了性能,JVM 和 Java 应用都会缓存 final 变量
- final 变量可以安全的在多线程环境下进行共享,而不需要额外的同步开销
异步/同步 堵塞/非堵塞
同步与异步是对应于调用者与被调用者,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的
- 同步操作时,调用者需要等待被调用者返回结果,才会进行下一步操作
- 而异步则相反,调用者不需要等待被调用者返回调用,即可进行下一步操作,被调用者通常依靠事件、回调等机制来通知调用者结果
- 同步和异步是指:发送方和接收方是否协调步调一致
- 同步通信是指:发送方和接收方通过一定机制,实现收发步调协调。如:发送方发出数据后,等接收方发回响应以后才发下一个数据包的通讯方式
- 异步通信是指:发送方的发送不管接收方的接收状态。 如:发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。
阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞
阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态:
- 阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。
- 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。
-
同步阻塞方式:
发送方发送请求之后一直等待响应。
接收方处理请求时进行的IO操作如果不能马上等到返回结果,就一直等到返回结果后,才响应发送方,期间不能进行其他工作。 -
同步非阻塞方式:
发送方发送请求之后,一直等待响应。
接受方处理请求时进行的IO操作如果不能马上的得到结果,就立即返回,取做其他事情。
但是由于没有得到请求处理结果,不响应发送方,发送方一直等待。
当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方,发送方才进入下一次请求过程。(实际不应用) -
异步阻塞方式:
发送方向接收方请求后,不等待响应,可以继续其他工作。
接收方处理请求时进行IO操作如果不能马上得到结果,就一直等到返回结果后,才响应发送方,期间不能进行其他操作。 (实际不应用) -
异步非阻塞方式:
发送方向接收方请求后,不等待响应,可以继续其他工作。
接收方处理请求时进行IO操作如果不能马上得到结果,也不等待,而是马上返回去做其他事情。
当IO操作完成以后,将完成状态和结果通知接收方,接收方再响应发送方。(效率最高)
设计模式
1. 单例模式
- **单例模式定义:**单例模式(Singleton Pattern)属于创建型模式,它提供了一种创建对象的最佳方式。**在当前进程中,通过单例模式创建的类有且只有一个实例。**这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
- 单例有如下几个特点:
- 在Java应用中,单例模式能保证在一个JVM中,该对象只有一个实例存在
- 构造器必须是私有的,外部类无法通过调用构造器方法创建该实例
- 没有公开的set方法,外部类无法调用set方法创建该实例
- 提供一个公开的get方法获取唯一的这个实例
- 那单例模式有什么好处呢?
- 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销
- 省去了new操作符,降低了系统内存的使用频率,减轻GC压力
- 系统中某些类,如spring里的controller,控制着处理流程,如果该类可以创建多个的话,系统完全乱了
- 避免了对资源的重复占用
- **缺点:**没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
1. 饿汉式:
**是否 Lazy 初始化:**否
**是否多线程安全:**是
**实现难度:**易
**描述:**这种方式比较常用,但容易产生垃圾对象。
**优点:**没有加锁,执行效率会提高。
**缺点:**类加载时就初始化,浪费内存。
它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。
public class Singleton {
// 创建一个实例对象
private static Singleton instance = new Singleton();
/**
* 私有构造方法,防止被实例化
*/
private Singleton(){}
/**
* 静态get方法
*/
public static Singleton getInstance(){
return instance;
}
}
2. 懒汉式:
线程不安全的:
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
对getInstance方法加锁即为线程安全的,但是,效率很低,99% 情况下不需要同步。
**懒汉和饿汉的对比:**大家可以发现两者的区别基本上就是第一次创作时候的开销问题,以及线程安全问题(线程不安全模式的懒汉)。
那有了这个对比,那他们的场景好理解了,在很多电商场景,如果这个数据是经常访问的热点数据,那我就可以在系统启动的时候使用饿汉模式提前加载(类似缓存的预热)这样哪怕是第一个用户调用都不会存在创建开销,而且调用频繁也不存在内存浪费了。
而懒汉式呢我们可以用在不怎么热的地方,比如那个数据你不确定很长一段时间是不是有人会调用,那就用懒汉,如果你使用了饿汉,但是过了几个月还没人调用,提前加载的类在内存中是有资源浪费的。
3. 双重校验锁模式
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
//先检查实例是否存在,如果不存在才进入下面的同步块
if(instance == null){
//同步块,线程安全的创建实例
synchronized (Singleton.class) {
//再次检查实例是否存在,如果不存在才真正的创建实例
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
另外,需要注意 instance 采⽤ volatile 关键字修饰也是很有必要。instance 采⽤ volatile 关键字修饰也是很有必要的, instance = new Singleton();
这段代码其实是分为三步执⾏:
- 为 instance 分配内存空间
- 初始化 instance
- 将 instance 指向分配的内存地址
但是由于 JVM 具有指令重排的特性,执⾏顺序有可能变成 1i>3i>2。指令重排在单线程环境下不会出现问题,但是多线程环境下会导致⼀个线程获得还没有初始化的实例。例如,线程 T1 执⾏了 1 和3,此时 T2 调⽤ getInstance() 后发现 instance 不为空,因此返回instance ,但此时 uniqueInstance 还未被初始化。使⽤ volatile 可以禁⽌ JVM 的指令重排,保证在多线程环境下也能正常运⾏。
2. 观察者模式
参考:设计模式之禅
观察者模式(Observer Pattern) 也叫做发布订阅模式(Publish/subscribe) ,它是一个在项目中经常使用的模式, 其定义如下:
Define a one-to-many dependency between objects so that when one object changes state,all its dependents are notified and updated automatically.(定义对象间一种一对多的依赖关系, 使得每当一个对象改变状态, 则所有依赖于它的对象都会得到通知并被自动更新)
● Subject被观察者
定义被观察者必须实现的职责, 它必须能够动态地增加、 取消观察者。 它一般是抽象类或者是实现类, 仅仅完成作为被观察者必须实现的职责: 管理观察者并通知观察者。
● Observer观察者
观察者接收到消息后, 即进行update(更新方法) 操作, 对接收到的信息进行处理。
● ConcreteSubject具体的被观察者
定义被观察者自己的业务逻辑, 同时定义对哪些事件进行通知。
● ConcreteObserver具体的观察者
每个观察在接收到消息后的处理反应是不同, 各个观察者有自己的处理逻辑。
被观察者:
public abstract class Subject {
//定义一个观察者数组
private Vector<Observer> obsVector = new Vector<Observer>();
//增加一个观察者
public void addObserver(Observer o){
this.obsVector.add(o);
}
//删除一个观察者
public void delObserver(Observer o){
this.obsVector.remove(o);
}
//通知所有观察者
public void notifyObservers(){
for(Observer o:this.obsVector){
o.update();
}
}
}
//具体被观察者
public class ConcreteSubject extends Subject {
//具体的业务
public void doSomething(){
/*
* do something
*/
super.notifyObservers();
}
}
观察者:
public interface Observer {
//更新方法
public void update();
}
public class ConcreteObserver implements Observer {
//实现更新方法
public void update() {
System.out.println("接收到信息, 并进行处理! ");
}
}
观察者模式的使用场景
● 关联行为场景。 需要注意的是, 关联行为是可拆分的, 而不是“组合”关系。
● 事件多级触发场景。
● 跨系统的消息交换场景, 如消息队列的处理机制
集合
ArrayList
- Arraylist 与 LinkedList 区别?
- 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不保证线程安全;
- 底层数据结构: Arraylist 底层使⽤的是 Object 数组; LinkedList 底层使⽤的是双向链表 数据结构
- 插入删除时间复杂度: ① 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)) 因为需要先移动到指定位置再插⼊。
- 查询复杂度: LinkedList 不⽀持⾼效的随机元素访问,时间复杂度为O(n),⽽ ArrayList ⽀持O(1)。
- 内存空间占⽤: ArrayList的空间浪费主要体现在在list列表的结尾会预留⼀定的容量空间,⽽LinkedList的空间花费则体现在它的每⼀个元素都需要消耗⽐ArrayList更多的空间(因为要存放直接后继和直接前驱以及数据)。
-
ArrayList和Vector的区别?
Vector 类的所有⽅法都是同步的(所有方法都添加了Synchronized关键字)。可以由两个线程安全地访问⼀个Vector对象、但是⼀个线程访问Vector的话代码要在同步操作上耗费⼤量的时间。而ArrayList不是线程安全的 -
ArrayList的扩容机制?
add方法:
public void add(int index, E element) {
//1. 检查是否错过数组大小/小于0
rangeCheckForAdd(index);
//2. 确保容量是否满足条件
ensureCapacityInternal(size + 1); // Increments modCount!!
//3. 创建新数组,将所需添加元素插入新数组
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
private void ensureCapacityInternal(int minCapacity) {
//如果当前数组为空数组,则取DEFAULT_CAPACITY(10),和minCapacity中最大值作为minCapacity扩容
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
}
ensureExplicitCapacity(minCapacity);
}
private void ensureExplicitCapacity(int minCapacity) {
modCount++;
// overflow-conscious code
//如果minCapacity大于数组长度
if (minCapacity - elementData.length > 0)
//数组扩容
grow(minCapacity);
}
扩容:
private void grow(int minCapacity) {
// overflow-conscious code
int oldCapacity = elementData.length;
//取原容量+原容量的一半之和作为新的容量
int newCapacity = oldCapacity + (oldCapacity >> 1);
//取最大容量
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
//如果满足条件,(minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE :MAX_ARRAY_SIZE;
if (newCapacity - MAX_ARRAY_SIZE > 0)
newCapacity = hugeCapacity(minCapacity);
// 创建新的数组
elementData = Arrays.copyOf(elementData, newCapacity);
}
HashMap
HashMap源码解析见 https://blog.csdn.net/Zeroowt/article/details/113819853
- HashMap 和 Hashtable 的区别
- HashMap 是⾮线程安全的, HashTable 是线程安全的; HashTable 内部的⽅法基本都经过 synchronized 修饰
- 因为线程安全的问题, HashMap 要⽐ HashTable 效率⾼⼀点。
- 对Null key 和Null value的⽀持: HashMap 中, null 可以作为键,这样的键只有⼀个,可以有⼀个或多个键所对应的值为 null。。但是在 HashTable 中 put 进的键值只要有⼀个 null,直接抛出 NullPointerException。
- 初始容量⼤⼩和每次扩充容量⼤⼩的不同 : 创建时如果不指定容量初始值, Hashtable 默认的初始⼤⼩为11,之后每次扩充,容量变为原来的2n+1。 HashMap 默认的初始化⼤⼩为16。之后每次扩充,容量变为原来的2倍。
- HashMap 和 HashSet区别
HashSet 底层就是基于 HashMap 实现的。
- HashMap 存储键值对 ,HashSet 仅存储对象
- HashSet使⽤成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()⽅法⽤来判断对象的相等性,HashMap使⽤键(Key)计算Hashcode
- 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,竞争会越来越激烈效率越低。
JDK1.7中,ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。
Segment 实现了 ReentrantLock,所以 Segment 是⼀种可重⼊锁,扮演锁的⻆⾊。 HashEntry ⽤于存储键值对数据。⼀个 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倍。
ConcurrentHashMap的put插入方法:
public V put(K key, V value) {
return putVal(key, value, false);
}
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
//1.计算hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//2.如果没有初始化,则进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 3.如果数组中无此节点,则直接插入
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
//通过cas插入节点
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransferJava/计算机网络/操作系统面试题总结(未完待续)