javase面试学习
javase八股文
基础
JDK,JRE,JVM的区别
JDK
JDK,全称Java Development Kit,是 Java 语言的软件开发工具包,主要用于移动设备、嵌入式设备上的Java应用程序。JDK是整个Java开发的核心
JRE
JRE,全称Java Runtime Environment,是指Java的运行环境,是可以在其上运行、测试和传输应用程序的Java平台。
JVM
JVM,全称Java Virtual Machine(Java虚拟机),是一种用于计算设备的规范,它是一个虚构出来的计算机,引入JVM后,Java语言在不同平台上运行时不需要重新编译。JVM是Java跨平台的核心。
三者的联系
JDK包含了JRE,JRE包含了JVM
克隆clone
[查了很久看到的一篇好一些的文章,点击跳转](深入浅出| java中的clone方法 - 知乎 (zhihu.com))
效率高
clone调用的是JVM的底层的原生方法(c++的代码),所以clone方法复制对象更快
实现clone
- 实现Cloneable接口并重写Object类中的clone()方法;
- 实现Serializable接口,通过对象的序列化和反序列化实现克隆,可以实现真正的深度克隆。对象序列化后写入流中,再从流中读取,生成新的对象,新对象和原对象之间也是完全互不影响的。
深拷贝和浅拷贝
- 浅拷贝是对地址的拷贝
- 浅拷贝是对对象的属性的全部的拷贝
==和equals()
理解
-
==对于八大基本数据类型(Byte,short,int,long,double,folat,boolean,char)是比较值,对引用数据类型是比较地址
-
equals()是在object里面定义的基本方法(用的是==来比较),不能对基本数据类型调用,可以重写
* Compares this string to the specified object. The result is {@code * true} if and only if the argument is not {@code null} and is a {@code * String} object that represents the same sequence of characters as this * object. * * @param anObject * The object to compare this {@code String} against * * @return {@code true} if the given object represents a {@code String} * equivalent to this string, {@code false} otherwise * * @see #compareTo(String) * @see #equalsIgnoreCase(String) */ public boolean equals(Object anObject) { if (this == anObject) { return true; } if (anObject instanceof String) { String anotherString = (String) anObject; int n = value.length; if (n == anotherString.value.length) { char v1[] = value; char v2[] = anotherString.value; int i = 0; while (n-- != 0) { if (v1[i] != v2[i]) return false; i++; } return true; } } return false; }
注意
-
对于常量池中的东西,使用equals()是一定相等的
-
比如Integer的常量池为:-128~127
-
Integer a=100; Integer b=1000; Integer c=100; Integer d=1000; // ==就相当于equals()方法,反正Integer没有重写 System.out.println(a==b); // false System.out.println(a==c); // true System.out.println(b==c); // false System.out.println(b==d); // false
Object类有的方法
- getClass():获取类的class对象。
- hashCode:获取对象的hashCode值
- equals():比较对象是否相等,比较的是值和地址,子类可重写以自定义。
- clone():克隆方法。
- toString():如果没有重写,应用对象将打印的是地址值。
- notify():随机选择一个在该对象上调用wait方法的线程,解除其阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
- notifyall():解除所有那些在该对象上调用wait方法的线程的阻塞状态。该方法只能在同步方法或同步块内部调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
- wait():导致线程进入等待状态,直到它被其他线程通过notify()或者notifyAll唤醒。该方法只能在同步方法中调用。如果当前线程不是锁的持有者,该方法抛出一个IllegalMonitorStateException异常。
- finalize():对象回收时调用
面向对象
面向对象和面向过程的理解
面向过程
将问题拆分成多个步骤,并一步一步将步骤实现,是对事情拆分的过程
面向对象
将问题中出现的事物独立出来,分析改事物在该事件中需要做的事情(方法)和使用到的自己的物件(属性),通过调用每个事物的方法,实现事情的推进和完成,每个事物都是独立的,不需要管别人做的事情。
面向对象的特点
封装
封装,就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码或某些数据可以是私有的,不能被外界访问。通过这种方式,对象对内部数据提供了不同级别的保护,以防止程序中无关的部分意外的改变或错误的使用了对象的私有部分。
继承
继承,指可以让某个类型的对象获得另一个类型的对象的属性的方法。它支持按级分类的概念。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。 通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。继承的过程,就是从一般到特殊的过程。要实现继承,可以通过 “继承”(Inheritance)和“组合”(Composition)来实现。继承概念的实现方式有二类:实现继承与接口继承。实现继承是指直接使用父类的属性和方法而无需额外编码的能力;接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力。
多态
多态,是指一个类实例的相同方法在不同情形有不同表现形式。多态机制使具有不同内部结构的对象可以共享相同的外部接口。这意味着,虽然针对不同对象的具体操作不同,但通过一个公共的类,它们(那些操作)可以通过相同的方式予以调用。
抽象类和接口
抽象类
模板模式:由于抽象类既能拥有普通的方法,又有抽象的方法,这样,抽象类既能自己完成一些功能,又给子类提供无限的可能。
接口
比抽象还抽象,或者说是一种特殊的抽象类
- 不能实例化
- 没有构造方法
- 方法默认public abstract修饰
- 变量默认public static final修饰
接口和抽象类的区别
- 接口是行为的抽象,是一种行为的规范,接口是like a 的关系;抽象是对类的抽象,是一种模板设计,抽象类是is a 的关系。
- 接口没有构造方法,而抽象类有构造方法,其方法一般给子类使用
- 接口只有定义,不能有方法的实现,java 1.8中可以定义default方法体,而抽象类可以有定义与实现,方法可在抽象类中实现。
- 抽象体现出了继承关系,继承只能单继承。接口提现出来了实现的关系,实现可以多实现。接口强调特定功能的实现,而抽象类强调所属关系。
- 接口成员变量默认为public static final,必须赋初值,不能被修改;其所有的成员方法都是public abstract的。抽象类中成员变量默认default,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract修饰,不能被private、static、synchronized和native等修饰,必须以分号结尾,不带花括号。
String
String的创建
string是不可变的一个类型
String s1 ="hello";
s1 ="Hello,world!";
/*
现在常量池中寻找hello这个对象,找到了就直接将地址赋值过去,否则就会创建一个新的对象并让s1指向这个对象
在第二段话进行时,先回重复上述操作,再指向
且“hello”这个对象不会立马被垃圾回收器回收,而是先回查询是否有其他的对象指向它
*/
深入理解
在Java中,所有的字符串字变量,例如“Hello”,都是在一个特殊的区域内存储的,我们通常称这个区域为String pool。这种机制的存在有利于减少内存占用并提高性能。
但如果我们用new关键字创建字符串,情况就会不同:
String s3 = new String("Hello");
这段代码会创建一个新的String对象,并且这个对象不会被放到String pool中。因此,s3指向的对象与s1和s2指向的对象不同,即使它们的内容相同。
如果我们希望把一个用new创建的字符串放到String pool中,可以使用intern方法:
String s4 = new String("Hello").intern();
String的连接和效率问题
string使用“+”来改变字符串的值时
先会创建一个新的对象,然后再创建string+i,再赋值
这个过程非常的耗费时间
String与其他类
String与StringBuffer和StringBuilder的对比
首先,我们需要理解String是不可变的,也就是说,当你创建一个String对象后,它的内容就不能改变。这是由于String的内部字符数组被声明为final,所以无法修改。相反,StringBuffer和StringBuilder都是可变的,它们内部的字符数组可以动态的改变。
由于String是不可变的,所以每次修改String都会产生新的对象,这在大量字符串操作时会带来性能问题。相反,StringBuffer和StringBuilder可以动态改变,所以它们在大量的字符串操作时更高效。
线程安全性
StringBuffer是线程安全的,它的大部分方法都是synchronized的,所以在多线程环境下也可以安全使用。但是,这种线程安全也是有代价的,它在性能上比StringBuilder慢。
相反,StringBuilder不是线程安全的,所以它的性能比StringBuffer快。如果你的代码只在单线程环境下执行,那么使用StringBuilder是一个更好地选择。
反射
(使用的前提条件:必须先得到代表的字节码的Class,Class类用于表示.class文件(字节码))
为什么要用反射
- 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
- 获取任意对象的属性,并且能改变对象的属性
- 调用任意对象的方法
- 判断任意一个对象所属的类
- 实例化任意一个类的对象
- 通过反射我们可以实现动态装配,降低代码的耦合度,动态代理
Java反射常用API
getMethods()
getMethods()获取本类以及父类中所有public修饰符修饰的方法,包括本类和父类实现的接口以及抽象方法。
getDeclaredMethods()
getDeclaredMethods()返回本类中的所有private、protected、默认的、public修饰符修饰的方法,包括实现的接口和抽象方法
getModifiers()
getModifiers()返回一个int类型的数值,该数值表示类、变量、方法被哪个修饰符所修饰
修饰符 | 数值 |
---|---|
默认 | 0 |
public | 1 |
private | 2 |
protected | 4 |
static | 8 |
final | 16 |
synchronized | 32 |
volatile | 64 |
transient | 128 |
native | 256 |
interface | 512 |
abstract | 1024 |
getParameterTypes()
getParameterTypes()返回方法中所有的参数类型
getInterfaces()
getInterfaces()返回某个类直接实现的接口
getSuperClass()
getSuperClass()返回某个类的直接父类,如果该类没有直接父类,那么返回null
invoke()
// invoke()通过反射执行一个不能直接被调用的方法,反射的方法必须是public修饰符修饰的
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ReflectTest reflectTest = new ReflectTest();
Class<?> clazz = reflectTest.getClass();
try {
// 第一个参数是方法名,第二个参数类型数组
Method method = clazz.getMethod("invokeMethod", String.class);
// 第一个参数是执行这个方法的对象,如果这个方法是static修饰的,可直接使用null,
// 第二个是参数数组
method.invoke(clazz.newInstance(),"通过反射传入的信息");
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
}
}
}
异常
- 所有异常类型都是内置类
Throwable
的子类,因而Throwable
在异常类的层次结构的顶层。 - 接下来
Throwable
分成了两个不同的分支,一个分支是Error
,它表示不希望被程序捕获或者是程序无法处理的错误。另一个分支是Exception
,它表示用户程序可能捕捉的异常情况或者说是程序可以处理的异常。其中异常类Exception
又分为运行时异常(RuntimeException)
和非运行时异常。 Error
和Exception
的区别:Error
通常是灾难性的致命的错误,是程序无法控制和处理的,当出现这些异常时,Java虚拟机(JVM)一般会选择终止线程;Exception
通常情况下是可以被程序处理的,并且在程序中应该尽可能的去处理这些异常。
常见异常错误
(1)IndexOutOfBoundsException:
ArrayIndexOutOfBoundsException
数组角标越界异常 角标不在数组范围内
StringfIndexOutOfBoundsException
字符串角标越界异常 角标不在字符串范围内
(2)NullPointerException
空指针异常 对null调用其成员。
(3)ArithmeticException
数学运算异常 非法的数学运算。
(4)ClassCastException
类型转换异常 将类进行错误的强制转换。
(5)NumberFormatException
数字格式化异常 将数字字符串进行解析。
(6)InputMismatchException
输入不匹配异常 在输入时输入非法数据。
(7)ParseException
时间解析异常 非法的时间格式。
(8)StackOverFlowError
栈内存溢出异常 函数递归。
(9)OutOfMemoryError
堆内存异常 数组空间开辟过大 程序中对象太多。
泛型
自我理解:当我还不能确定这个对象的类型或者这个地方对于不同的一些对象有相同的操作的时候,我就将这个地方先定义成一个泛型,能够更方便代码的书写,实现代码的复用,实现解耦
泛型概述
什么是泛型
泛型的本质是为了将类型参数化, 也就是说在泛型使用过程中,数据类型被设置为一个参数,在使用时再从外部传入一个数据类型;而一旦传入了具体的数据类型后,传入变量(实参)的数据类型如果不匹配,编译器就会直接报错。这种参数化类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
泛型类
-
定义方式
class 类名称 <泛型标识> { //尖括号 <> 中的 泛型标识被称作是类型参数,用于指代任何数据类型。 private 泛型标识 /*(成员变量类型)*/ 变量名; ..... } }
-
泛型标识是任意设置的(如果你想可以设置为 Hello都行),Java 常见的泛型标识以及其代表含义如下:
T :代表一般的任何类。 E :代表 Element 元素的意思,或者 Exception 异常的意思。 K :代表 Key 的意思。 V :代表 Value 的意思,通常与 K 一起配合使用。 S :代表 Subtype 的意思,文章后面部分会讲解示意。
-
泛型类中的静态方法和静态变量不可以使用泛型类所声明的类型参数
public class Test<T> { public static T one; // 编译错误 public static T show(T one){ // 编译错误 return null; } }
-
泛型类中的类型参数的确定是在创建泛型类对象的时候(例如 ArrayList< Integer >),而静态变量和静态方法在类加载时已经初始化,直接使用类名调用;在泛型类的类型参数未确定时,静态成员有可能被调用,因此泛型类的类型参数是不能在静态成员中使用的
泛型的擦除机制
泛型只是一个编译期技术,也就是说,编译MyClass时,主函数可能压根还没有编译,更别说知道传入什么类型了!!!
为了正常编译,所有的
class MyClas<T>{
T t;
}
//真正使用的时候其实会变成
class MyClas{
Object t;
}
通配符
通配符(?
)用于表示未知的类型,它提供了额外的灵活性。
- 无界通配符
// 无界通配符表示可以接受任何类型的参数。
public void addAll(List<?> list) {
// list contains elements of unknown type
}
-
上界通配符
// 上界通配符? extends T表示可以接受T及其子类型的参数 public void process(List<? extends Number> list) { // list contains elements that are instances of Number or its subclasses }
-
下界通配符
// 下界通配符? super T表示可以接受T及其超类型的参数 public void accept(List<? super Integer> list) { // list can accept any object, including Integer and its supertypes }
注解
注解的定义
注解通过 @interface 关键字进行定义。
public @interface TestAnnotation {
}
元注解
- 元注解是可以注解到注解上的注解,或者说元注解是一种基本注解,但是它能够应用到其它的注解上面。
- 元标签有 @Retention、@Documented、@Target、@Inherited、@Repeatable 5 种
@Retention
Retention 的英文意为保留期的意思。当 @Retention 应用到一个注解上的时候,它解释说明了这个注解的的存活时间。
- RetentionPolicy.SOURCE 注解只在源码阶段保留,在编译器进行编译时它将被丢弃忽视。
- RetentionPolicy.CLASS 注解只被保留到编译进行的时候,它并不会被加载到 JVM 中。
- RetentionPolicy.RUNTIME 注解可以保留到程序运行的时候,它会被加载进入到 JVM 中,所以在程序运行时可以获取到它们。
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnotation {
}
@Target
Target 是目标的意思,@Target 指定了注解运用的地方。
- ElementType.ANNOTATION_TYPE 可以给一个注解进行注解
- ElementType.CONSTRUCTOR 可以给构造方法进行注解
- ElementType.FIELD 可以给属性进行注解
- ElementType.LOCAL_VARIABLE 可以给局部变量进行注解
- ElementType.METHOD 可以给方法进行注解
- ElementType.PACKAGE 可以给一个包进行注解
- ElementType.PARAMETER 可以给一个方法内的参数进行注解
- ElementType.TYPE 可以给一个类型进行注解,比如类、接口、枚举
@Inherited
Inherited 是继承的意思,但是它并不是说注解本身可以继承,而是说如果一个超类被 @Inherited 注解过的注解进行注解的话,那么如果它的子类没有被任何注解应用的话,那么这个子类就继承了超类的注解。
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@interface Test {}
@Test
public class A {}
public class B extends A {}
// 注解 Test 被 @Inherited 修饰,之后类 A 被 Test 注解,类 B 继承 A,类 B 也拥有 Test 这个注解。
// 父类的子类不写这个注解也会被这个注解标记,相当于基因里面一定不会发生变化且一定会遗传给子类的那一部分
@Repeatable
Repeatable 自然是可重复的意思。
注解还有好多不懂的,太难了
IO
BIO
Blocking io
是最传统的IO模型,也称为同步阻塞IO
如果这个连接不做任何事情会造成不必要的线程开销,并且线程在进行IO操作期间是被阻塞的,无法进行其他任务。在高并发环境下,BIO的性能较差,因为它需要为每个连接创建一个线程,而且线程切换开销较大,不过可以通过线程池机制改善。BIO适合一些简单的、低频的、短连接的通信场景,例如HTTP请求。
优点:
- 简单易用: BIO模型的编程方式相对简单,易于理解和使用。
- 可靠性高: 由于阻塞特性,IO操作的结果是可靠的。
缺点:
- 阻塞等待: 当一个IO操作被阻塞时,线程会一直等待,无法执行其他任务,导致资源浪费。
- 并发能力有限: 每个连接都需要一个独立的线程,当连接数增加时,线程数量也会增加,造成资源消耗和性能下降。
- 由于I/O操作是同步的,客户端的连接需要等待服务器响应,会降低系统的整体性能。
NIO
相比于传统的BIO模型,NIO采用了Channel、Buffer和Selector等组件,线程可以对某个IO事件进行监听,并继续执行其他任务,不需要阻塞等待。
NIO适用于连接数目多且连接比较短(轻操作)的架构,例如聊天服务器、弹幕系统、服务器间通讯等。它通过引入非阻塞通道的概念,提高了系统的伸缩性和并发性能。同时,NIO的使用也简化了程序编写,提高了开发效率。NIO适合一些复杂的、高频的、长连接的通信场景,例如聊天室、网络游戏等。
优点:
- 高并发性: 使用选择器(Selector)和通道(Channel)的NIO模型可以在单个线程上处理多个连接,提供更高的并发性能。
- 节省资源: 相对于BIO,NIO需要更少的线程来处理相同数量的连接,节省了系统资源。
- 灵活性: NIO提供了多种类型的Channel和Buffer,可以根据需要选择适合的类型。NIO允许开发人员自定义协议、编解码器等组件,从而提高系统的灵活性和可扩展性。
- 高性能: NIO采用了基于通道和缓冲区的方式来读写数据,这种方式比传统的流模式更高效。可以减少数据拷贝次数,提高数据处理效率。
- 内存管理:NIO允许用户手动管理缓冲区的内存分配和回收,避免了传统I/O**模型中的内存泄漏问题。
缺点:
- 编程复杂: 相对于BIO,NIO的编程方式更加复杂,需要理解选择器和缓冲区等概念,也需要考虑多线程处理和同步问题。
- 可靠性较低: NIO模型中,一个连接的读写操作是非阻塞的,无法保证IO操作的结果是可靠的,可能会出现部分读写或者错误的数据。
AIO
Java AIO(Asynchronous I/O)是Java提供的异步非阻塞IO编程模型,从Java 7版本开始支持,AIO又称NIO 2.0。
AIO适合一些极端的、超高频的、超长连接的通信场景,例如云计算、大数据等。
需要注意的是,目前AIO模型还没有广泛应用,Netty等网络框架仍然是基于NIO模型。
优点:
- 非阻塞:AIO的主要优点是它是非阻塞的。这意味着在读写操作进行时,程序可以继续执行其他任务。这对于需要处理大量并发连接的高性能服务器来说是非常有用的。
- 高效:由于AIO可以处理大量并发连接,因此它通常比同步I/O(例如Java的传统I/O和NIO)更高效。
- 简化编程模型:AIO使用了回调函数,这使得编程模型相对简单。当一个操作完成时,会自动调用回调函数,无需程序员手动检查和等待操作的完成。
缺点:
- 复杂性:虽然AIO的编程模型相对简单,但是由于其非阻塞的特性,编程复杂性可能会增加。例如,需要处理操作完成的通知,以及可能的并发问题。
- 资源消耗:AIO可能会消耗更多的系统资源。因为每个操作都需要创建一个回调函数,如果并发连接数非常大,可能会消耗大量的系统资源。
- 可移植性:AIO在某些平台上可能不可用或者性能不佳。因此,如果需要跨平台的可移植性,可能需要考虑使用其他I/O模型。
JAVA集合
List
ArrayList
ArrayList
实现了List
接口,是顺序容器,即元素存放的数据与放进去的顺序相同,允许放入null
元素,底层通过数组实现。除该类未实现同步外,其余跟Vector
大致相同。每个ArrayList
都有一个容量(capacity)
,表示底层数组的实际大小,容器内存储元素的个数不能多于当前容量。当向容器中添加元素时,如果容量不足,容器会自动增大底层数组的大小。前面已经提过,Java
泛型只是编译器提供的语法糖,所以这里的数组是一个Object
数组,以便能够容纳任何类型的对象。
构造函数
/**
* 有参
*/
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
this.elementData = EMPTY_ELEMENTDATA; // EMPTY_ELEMENTDATA表示容量为0的空ArrayList
} else {
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
/**
* 无参
*/
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
} // 有默认的长度 private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// EMPTY_ELEMENTDATA表示容量为0的空ArrayList,而DEFAULTCAPACITY_EMPTY_ELEMENTDATA表示容量为默认值的空ArrayList。
/**
* 参数为集合
*/
public ArrayList(Collection<? extends E> c) {
elementData = c.toArray();
if ((size = elementData.length) != 0) {
// c.toArray might (incorrectly) not return Object[] (see 6260652)
if (elementData.getClass() != Object[].class)
elementData = Arrays.copyOf(elementData, size, Object[].class);
} else {
// replace with empty array.
this.elementData = EMPTY_ELEMENTDATA;
}
}
EMPTY_ELEMENTDATA是为了优化创建ArrayList空实例时产生不必要的空数组,使得所有ArrayList空实例都指向同一个空数组。DEFAULTCAPACITY_EMPTY_ELEMENTDATA是为了确保无参构成函数创建的实例在添加第一个元素时,*最小的容量*是默认大小10
自动扩容
每次扩容大于为之前的1.5倍
/**
* Increases the capacity of this <tt>ArrayList</tt> instance, if
* necessary, to ensure that it can hold at least the number of elements
* specified by the minimum capacity argument.
*
* @param minCapacity the desired minimum capacity
*/
public void ensureCapacity(int minCapacity) { // 这个是外部(自己)来进行扩容的方法
int minExpand = (elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA)
// any size if not default element table
? 0
// larger than default for default empty table. It's already
// supposed to be at default size.
: DEFAULT_CAPACITY;
if (minCapacity > minExpand) {
ensureExplicitCapacity(minCapacity);
}
}
private void ensureCapacityInternal(int minCapacity) { // 1. 进到这里
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { // 2. 本来就是空的
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); // 3. 判断需要扩大到10,还是更大的大小
}
ensureExplicitCapacity(minCapacity); // 4. 进行扩容
}
private void ensureExplicitCapacity(int minCapacity) { // 5. 来到了这里
modCount++; // 6. 对修改的次数统计??
// 7. 判断是否需要扩容
if (minCapacity - elementData.length > 0)
grow(minCapacity);
}
/**
* 设置数组的最大的扩容大小
*/
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
/**
* 增长
*/
private void grow(int minCapacity) { // 8. 扩容的核心实现
// 这里实现的就是扩大1。5倍的过程
int oldCapacity = elementData.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // 这段代码扩容1.5
if (newCapacity - minCapacity < 0)
newCapacity = minCapacity;
if (newCapacity - MAX_ARRAY_SIZE > 0) // 不能超过最大的大小
newCapacity = hugeCapacity(minCapacity);
// minCapacity is usually close to size, so this is a win:
elementData = Arrays.copyOf(elementData, newCapacity);
}
// 注意:newCapacity不一定等于minCapacity
private static int hugeCapacity(int minCapacity) { // 如果newCapacity超过了MAX_ARRAY_SIZE,就来到这里
if (minCapacity < 0) // overflow
throw new OutOfMemoryError();
return (minCapacity > MAX_ARRAY_SIZE) ?
Integer.MAX_VALUE : // java int 类整数的最大值是 2 的 31 次方 - 1 = 2147483648 - 1 = 2147483647
MAX_ARRAY_SIZE;
}
add(), addAll()
add()
/**
* 添加进来一个元素
*/
public boolean add(E e) {
ensureCapacityInternal(size + 1); // 检查是否需要扩容
elementData[size++] = e; // 直接添加,如果超过长度了,就会在上面的代码中直接抛出异常
return true;
}
/**
* 向指定的位置添加元素
*/
public void add(int index, E element) {
rangeCheckForAdd(index);
ensureCapacityInternal(size + 1); // 检查扩容,增大修改次数
System.arraycopy(elementData, index, elementData, index + 1,
size - index);
elementData[index] = element;
size++;
}
addAll()
/**
* 添加进来一个集合
*/
public boolean addAll(Collection<? extends E> c) {
Object[] a = c.toArray();
int numNew = a.length; // 需要添加的长度
ensureCapacityInternal(size + numNew); // 检查扩容
System.arraycopy(a, 0, elementData, size, numNew);
size += numNew;
return numNew != 0;
}
/**
* 指定下标添加集合
*/
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
Object[] a = c.toArray();
int numNew = a.length;
ensureCapacityInternal(size + numNew); // 检查扩容
int numMoved = size - index;
if (numMoved > 0)
System.arraycopy(elementData, index, elementData, index + numNew,
numMoved);
System.arraycopy(a, 0, elementData, index, numNew);
size += numNew;
return numNew != 0;
}
set()
改变传入位置的值
public E set(int index, E element) {
rangeCheck(index); // 下标越界检查
E oldValue = elementData(index);
elementData[index] = element;//赋值到指定位置,复制的仅仅是引用
return oldValue;
}
get()
public E get(int index) {
rangeCheck(index);
return (E) elementData[index];//注意类型转换
}
remove()
remove()
方法也有两个版本,一个是remove(int index)
删除指定位置的元素,另一个是remove(Object o)
删除第一个满足o.equals(elementData[index])
的元素。删除操作是add()
操作的逆过程,需要将删除点之后的元素向前移动一个位置。需要注意的是为了让GC起作用,必须显式的为最后一个位置赋null
值。
public E remove(int index) {
rangeCheck(index);
modCount++;
E oldValue = elementData(index);
int numMoved = size - index - 1;
if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // 清除该位置的引用,让GC起作用
return oldValue;
}
trimToSize()
/**
* 将当前数组后面不真正存储值的那些空位删除
*/
public void trimToSize() {
modCount++;
if (size < elementData.length) {
elementData = (size == 0)
? EMPTY_ELEMENTDATA
: Arrays.copyOf(elementData, size);
}
}
indexOf(), lastIndexOf()
/**
* 获取第一个等于o的元素的下标,复杂度O(n)
*/
public int indexOf(Object o) {
if (o == null) {
for (int i = 0; i < size; i++)
if (elementData[i]==null)
return i;
} else {
for (int i = 0; i < size; i++)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
/**
* 返回最后一个等于o的元素的下标,事件复杂度O(n)
*/
public int lastIndexOf(Object o) {
if (o == null) {
for (int i = size-1; i >= 0; i--)
if (elementData[i]==null)
return i;
} else {
for (int i = size-1; i >= 0; i--)
if (o.equals(elementData[i]))
return i;
}
return -1;
}
Fail-Fast机制
ArrayList也采用了快速失败的机制,通过记录modCount参数来实现。在面对并发的修改时,迭代器很快就会完全失败,而不是冒着在将来某个不确定时间发生任意不确定行为的风险
序列化
主要依赖重写readObject()和writeObject()方法
elementData设置成了transient,那ArrayList是怎么把元素序列化的呢?
java 的transient关键字为我们提供了便利,你只需要实现Serilizable接口,将不需要序列化的属性前添加关键字transient,序列化对象的时候,这个属性就不会序列化到指定的目的地中
虽然elementData被transient修饰,不能被序列化,但是我们可以将它的值取出来,然后将该值写入输出流。
-
为什么使用transient修饰elementData?
// 既然要将ArrayList的字段序列化(即将elementData序列化),那为什么又要用transient修饰elementData呢? // 回想ArrayList的自动扩容机制,elementData数组相当于容器,当容器不足时就会再扩充容量,但是容器的容量往往都是大于或者等于ArrayList所存元素的个数。 // 比如,现在实际有了8个元素,那么elementData数组的容量可能是8x1.5=12,如果直接序列化elementData数组,那么就会浪费4个元素的空间,特别是当元素个数非常多时,这种浪费是非常不合算的。 // 所以ArrayList的设计者将elementData设计为transient,然后在writeObject方法中手动将其序列化,并且只序列化了实际存储的那些元素,而不是整个数组。 // Write out all elements in the proper order. for (int i=0; i<size; i++) { s.writeObject(elementData[i]); } // 从源码中,可以观察到 循环时是使用i<size而不是 i<elementData.length,说明序列化时,只需实际存储的那些元素,而不是整个数组。
vector
- vector是线程安全的
- vector的默认的扩容是扩容1倍
- vector的性能比ArrayList低
LinkedList
底层是双向链表实现的
java中没有叫做Queue的类
对于站和队列,现在的首选是ArrayDeque,它有着比LinkedList(当作栈或队列使用时)有着更好的性能
LinkedList没有实现同步
// 1. 普通的构造函数
public LinkedList() {
}
// 2. 将集合变成LinkedList的构造函数
public LinkedList(Collection<? extends E> c) {
this();
addAll(c);
}
// 3. getFirst(), getLast()···获取第一个元素, 和获取最后一个元素
// 直接遍历的
// 4. removeFirst(), removeLast(), remove(e), remove(index)···
// remove()方法也有两个版本,一个是删除跟指定元素相等的第一个元素remove(Object o),另一个是删除指定下标处的元素remove(int index)
//删除元素 - 指的是删除第一次出现的这个元素, 如果没有这个元素,则返回false;判断的依据是equals方法, 如果equals,则直接unlink这个node;由于LinkedList可存放null元素,故也可以删除第一次出现null的元素
// 5. add()
// a. add(E e)
// b. add(int index, E element)
// 6. addAll()
// a. addAll(index, c) 实现方式并不是直接调用add(index,e)来实现,主要是因为效率的问题,另一个是fail-fast中modCount只会增加1次
// 7. clear()
// a. 为了让GC更快可以回收放置的元素,需要将node之间的引用关系赋空
.................
Stack & Queue
Java里有一个叫做Stack的类,却没有叫做Queue的类(它是个接口名字)
java不再推荐使用stack,而是使用更加高效的ArrayDeque
PriorityQueue
优先队列,有大顶堆(第一个为数组里面最大的)和小顶堆(出来的第一个元素为数组里面最小的)
该函数在算法竞赛的BFS的寻找最短路径中非常常见,dijistra算法就是基于该函数(算法)加贪心实现的
优先队列在脑袋里面的构造已经不再是一个数组了,而是一颗二叉树
HashMap
如果你在学习哈希map之前学习了平衡二叉树splay的知识,那么hashmap的底层——红黑树,就非常易学了
推荐[红黑树学习观看视频](红黑树 - 定义, 插入, 构建_哔哩哔哩_bilibili)
该网课后面还讲解了b-树,b+树的构建、删除、添加
个人认为,该网课还是比较好食用的
JDK1.7(数组加链表)
对于该版本的和JDK1.8中链表中元素长度小于8的时候使用的都是这个方法
JDK1.8(数组加链表加红黑树)
(这些不知道是不是面试题,是我看文章的时候看到的,能掌握就掌握吧)
为什么会使用红黑树作为该集合的扩大后的存储数据结构?
为啥不用splay?
答:红黑树能够提供更加强大快速的查找方法,能够更加快速的定位到查找的元素,红黑树的存储方式和排序二叉树大同小异
红黑树的很多方式和splay很相似,不同就是每条从根出发的路径的节点的长度不同,红黑树节点的最小个数和最大的节点个数能够最多相差一倍,但是splay的最多的节点个数和最少节点个数最多相差只能为1
这就奠定了splay的维护成本(时间复杂度)会要比红黑树的维护成本高出来很多
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理