Java面经
Java面经
世界上并没有完美的程序,但是我们并不因此而沮丧,因为写程序本来就是一个不断追求完美的过程。
整理者:YuanChuZiWen
基础部分
最基础那种
比较一下Java和JavaScript
回答:
- JavaScript和Java是两个不同的产品。Java是由Sun公司推出,现在Oracle旗下的一款面向对象编程语言。JavaScript是由Netscape公司开发的可嵌入在Web页面种的解释型编程语言。
- Java是强类型语言, 变量必须先声明再使用,广泛应用于PC端、手机端、互联网、数据中心等等 。JavaScript是弱类型的脚步语言,源代码不需编译即可解释执行,主要用于嵌入文本到HTML页面,读写HTML元素,控制cookies等 。
- 注意比较异同,相同点和差异点。可顺带输出 面向对象 的思想。
在Java中如何跳出多重循环
回答:
-
尽管Java语言中不像C/C++那样可以使用goto语句(在Java中以保留字的形式出现),但是可以使用带标签的break语句。譬如在最外层的循环前加一个标签 A,然后使用 break A;
-
探讨 goto语句。
以下内容节选自《Think in Java》第五章-控制流
- goto关键字起于汇编语言,事实上汇编语言中充斥了大量的跳转。
- 在 Edsger Dijkstra 发表著名的《Goto有害论》,以后goto便从此失宠。
- 事实上,问题不在于 goto,而在于过度使用goto。
- Java本身并不支持goto,goto仍是Java中的一个保留字,从未被正式启用。但实际上带标签的break语句本身就类似于goto
- Java 里需要使用标签的唯一理由就是因为有循环嵌套存 在,而且想从多层嵌套中 break 或 continue
- 标签和 goto 使得程序难以分析,Dijkstra 观察到 BUG 的数量似乎随着程序中标签的数量而增加
- 但是,Java 标签不会造成这方面的问题,因为它们的应用场景受到限制。
讲讲&和&&的区别
回答:
-
&:
- 表示 按位与 操作。是双目运算符, 两个当且仅当都为1的时候结果才为1 ,并且负数以补码的形式参与运算。
- 表示 逻辑与 操作。当运算符两边的表达式的结果都为 true时,整个运算结果才为 true
-
&&:
短路与操作。类似上方的逻辑与,但在左边的表达式已是false的情况下,不会再计算右边的表达式 -
很多时候我们可能都需要用&&而不是&,前者效率更好,且应用更广。
int 和 integer的区别
回答:
-
int 是 Java 中的基本数据类型,可以直接使用,占用空间少;Integer 是int 的包装类,必须实例化后使用,占用空间多。
-
int 默认是0,存储在常量池中;integer 默认是null,存储在堆中
-
两个同值的 int 数通过 == 运算结果为true;而integer对象则为 false,因为地址不同。此外,同值的 int 和 integer 进行比较也是false。
@Test public void test1() { Integer i1 = new Integer(10); Integer i2 = new Integer(10); System.out.println(i1 == i2); // false Integer i3 = Integer.valueOf(10); Integer i4 = Integer.valueOf(10); System.out.println(i3 == i4); // true Integer i5 = 10; System.out.println(i3 == i5); // true }
-
原始类和包装类:
- 原始类型:boolean(Java规范中没有给出具体的占用字节数,不同的 JVM有不同的实现),char(1个字节),byte(1个字节),short(2个字节),int(4个字节),long(8个字节),float(4个字节),double(8个字节)
- 包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
- 实际上 Java还有另外一种基本类型
void
,对应包装类Void
,不过我们无法直接对它们进行操作
-
自动装箱和自动拆箱(JDK5新特性)
可以直接将 int 型数据 赋值给 Integer 型变量,反之亦然。
-
java在编译 Integer i = 100; 时,会翻译成为
Integer i = Integer.valueOf(100)
因为,对于-128到127之间的数,Java会进行缓存
再写 Integer j = 100; 时,就会直接从缓存中取,就不会new了。- 解析原因:归结于 Java对于 Integer 与 int 的自动装箱与拆箱的设计,是一种模式:叫享元模式(flyweight)。旨在加大对简单数字的重复利用,-128~127之内的数值,它们被装箱为Integer对象后,会存在内存中被重用,始终只存在一个对象。
- Java语言规范中的缓存行为:
- -128 ~ 127的整数
- true和 false的布尔值
- \u0000至 \u007f之间的字符
-
源码分析:
public static Integer valueOf(String s, int radix) throws /** * 给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf */ NumberFormatException { return Integer.valueOf(parseInt(s,radix)); } /** * 在-128~127之内:静态常量池中cache数组是static final类型,cache数组对象会被存储于静态常量池中。 * cache数组里面的元素却不是static final类型,而是cache[k] = new Integer(j++), * 那么这些元素是存储于堆中,只是cache数组对象存储的是指向了堆中的Integer对象(引用地址) * */ public static Integer valueOf(int i) { assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high) { return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); } /** * 缓存支持自动装箱的对象标识语义 -128和127(含)。 * 缓存在第一次使用时初始化。 缓存的大小可以由-XX:AutoBoxCacheMax = <size>选项控制。 * 在VM初始化期间,java.lang.Integer.IntegerCache.high属性可以设置并保存在私有系统属性中 */ private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) { cache[k] = new Integer(j++); // 创建一个对象 } } private IntegerCache() {} }
-
整型数溢出以后,并不会报错,也没有任何提示
-
自动拆装箱的应用场景:
-
将基本数据类型放入集合类
List<Integer> li = new ArrayList<>(); for (int i = 1; i < 50; i ++){ li.add(i); }
-
包装类和基本类比大小
Integer a = 1; System.out.println(a == 1 ? "等于" : "不等于"); Boolean bool = false; System.out.println(bool ? "真" : "假");
-
包装类的运算
Integer i = 10; Integer j = 20; System.out.println(i+j);
-
三目运算符的使用
boolean flag = false; Integer i = 1; int j = 2; int k = flag ? i : j;
-
函数参数和返回值
public Integer getNum1(int i) { return i; } public int getNum2(Integer j) { return j; }
-
-
包装对象的值比较时,不能简单的通过 ==比较(虽然 -128 ~ 127之间的数字可以),但是在该范围之外都需要使用
equals()
方法 -
针对方法返回值定义(返回基本类型还是包装类型?如果是判断方法,是使用 isSuccess还是 success?)
-
阿里巴巴 Java开发手册中提到:
原因在于,不同的序列化工具实现序列化的策略是不同的,如 fastjson, jackson是通过 getter和setter方法反射获得属性;而像 Gson则是通过类属性进行反射获得的;所以如果使用 fastjson来序列化,然后使用 Gson反序列化,很有可能就会出问题
-
Boolean的默认值是 null,boolean的默认值是 false
-
说明String和StringBuffer、StringBuilder的区别
回答:
-
Java平台提供了两个类,String和StringBuffer、StringBuilder,它们可以存储和操作字符串,既包含多个字符的字符数据。
-
String类提供了数值不可改变的字符串,而StringBuffer类和StringBuilder类提供可变长字符串,可用于动态构造字符数据。
-
String类的内容一旦声明后不可改变,改变的只是其内存的指向。
String a = "你好";a = "hello";
- 这段代码的意思是声明一个String类型的引用变量命名为 a,并在内存(常量池)中床i教案一个String对象(值为“你好”),然后把这个对象的引用赋值给变量a
- 然后,又创建了一个String对象(值为“hello”),然后又把这个新对象的引用赋值给了变量 a,而不是把原来的内存中那个“你好”的String对象值直接变为“hello”
-
对于 StringBuffer 和 StringBuilder,不能像String那样通过直接赋值的方式完成对象的实例化,必须通过构造方法的方式完成。它们在进行字符串处理时不生成新的对象,在内存使用上优于String,所以在实际使用时,如果经常要对一个字符串进行修改,例如:插入、删除等操作,使用此二者会更加合适。
-
StringBuilder 和 StringBuffer均继承自AbstractStringBuilder,两个类型底层均通过char类型数组实现。StringBuffer在方法上添加了synchronized关键字,证明它的绝大多数方法都是线程同步的。也就是说在多线程的环境下,我们应该使用StringBuffer以保证线程安全,在单线程的环境下我们应使用StringBuilder以获得更高的效率。
-
关于底层原理,反编译代码结果等等,参考博客:
说明Array和ArrayList的区别
回答:
- Array和ArrayList都是Java中两个重要的数据结构,在Java程序中经常使用,ArrayList内部由数组支持。Array可以包含基本类型和对象类型,ArrayList只能包含对象类型,但由于自动装箱,这个差异从JDK5开始不再明显。
- ArrayList是Java Collection框架中的一个类,它是作为动态数组引入的。 由于数组本质上是静态的,即一旦创建后就无法更改数组的大小,因此,如果需要一个可以调整自身大小的数组,则应使用ArrayList。 这是数组和ArrayList之间的根本区别。
- 由于ArrayList是基于数组的,所以它和数组拥有相似的性能。具体存在的差异主要在内存和CPU时间方面
- 对于基于索引的访问,ArrayList和Array都提供O(1)性能,但是如果添加新元素会触发调整大小,则 add在ArrayList中可以为O(logN),这是由于它涉及在后台创建新数组并从旧数组中复制元素到新的数组。
- ArrayList的内存需求还不止于一个用于存储相同数量对象的数组,例如:由于ArrayList和 Wrapper类的对象元数据开销较大,因此 int[] arr 会比
ArrayList<Integer>
,占用更少内存来存储 int变量。
- ArrayList是类型安全的,因为它支持泛型,泛型允许编译器检查ArrayList中存储的所有对象的类型是否正确。另一方面,数组不支持Generic,当我们尝试将不适合的对象存储到数组中,则Array会抛出ArrayStoreException来提供运行时类型检查。
- ArrayList比普通的本地数组更灵活,因为它是动态的,可以在需要时自行增长。ArrayList允许我们实现数组无法做到的删除元素,这不仅是简单地将null分配给相应的索引,还意味着将其余元素向下复制一个索引。(在数组中删除元素需要循环遍历Array并为每个索引分配null)
- 数组仅提供一个length属性,该属性告诉我们数组中的插槽数,即可以存储多少个元素,它不提供任何方法来找出已填充的元素数和多少个插槽为空。ArrayList提供了size()方法,该方法告诉给定时间点存储在ArrayList中的对象数量,即容量。
- 数组可以是多维的,可用于表示矩阵和2D地形;而ArrayList不允许用户指定尺寸
- 相同处:
- 都是基于索引的数据结构,排序后可二分查找
- 都将保持元素添加到其中的顺序
- 都允许存储空值,都允许元素重复
- 索引都从零开始
- 最后总结:数组本质上是静态的,创建以后无法修改大小;ArrayList是动态的,如果ArrayList中的元素大于调整大小阈值,则可以改变自身大小。基于此,如果事先知道大小并确保它不会改变且够用,则应使用数组;否则使用ArrayList
解释值传递和引用传递
回答:
-
Java中数据类型分为两大类,分别是基本类型和对象类型。基本类型变量保存原始值,即它代表的值就是数值本身;引用类型的变量保存引用值,指向内存空间的地址,代表某个对象的引用而不是对象本身
-
基本数据类型在声明时系统就给它分配空间;引用类型则不同,它声明时只分配了引用空间,而不分配数据空间(单纯的声明引用而不实例化也是占空间的)
-
值传递(pass by value):方法调用时,函数接收的是原始值的一个副本,之后的所有操作针对的都是这个副本而不影响实际参数
-
引用传递(pass by reference):方法调用时,将实际参数的引用(即地址)传递给形参,函数接收到的是原始值的内存地址。在方法执行时,形参和实参内容相同,指向同一块内存地址,方法执行中对引用的操作将会影响到实际对象。
-
注意String、Integer、Double等几个基本类型的包装类,它们都是不可变类型,没有提供自身修改的函数,因此每次操作都是新生成一个对象,所以需要特殊对待,可认为是和基本数据类型相似的传值操作,对它们的操作不会修改实参对象。
-
严格意义上来说,对于所有值,不论是基本变量还是实例对象,其实都是 值传递
Primitive arguments, such as an int or a double, are passed into methods by value. This means that any changes to the values of the parameters exist only within the scope of the method. When the method returns, the parameters are gone and any changes to them are lost.
Reference data type parameters, such as objects, are also passed into methods by value. This means that when the method returns, the passed-in reference still references the same object as before. However, the values of the object’s fields can be changed in the method, if they have the proper access level.
-
拓展:不可变类型
- 如:String是不可变类型,每次对String对象的修改都将产生一个新的String对象,而原来的对象或被丢弃或保持不变
- 自己创建不可变类型:
- 所有成员都是 private final修饰
- 不提供改变成员的方法,即无 setXxx()方法
- 确保所有的方法不会被重写,可使用 final Class(强不可变类)或给所有类方法都修饰 final(弱不可变类)
- 如果某一个类成员不是原始变量或者不可变类型,则必须在成员初始化或使用get()方法是进行深拷贝,来确保类的不可变
- 优缺点:
- 使用不可变类型,对其频繁修改会产生大量的临时拷贝(需要垃圾回收)
- 使用可变类型可以获得更好的性能,适合在多个模块之间共享数据
- 对可变类型可能造成的风险,我们可以通过深度拷贝,给客户端返回一个全新的可变对象,但会造成大量的内存浪费
-
拓展:拷贝
-
引入拷贝:创建一个指向对象的引用变量的拷贝
User u1 = new User();User u2 = u1;
二者地址相同,肯定是一个对象(指向堆中的同一个对象),只是单纯的引用不同而已
-
对象拷贝:创建对象本身的一个副本
User u1 = new User();User u2 = (User)u1.clone();
二者地址不同,指向了堆中的两个对象。(深拷贝和浅拷贝都是对象拷贝)
-
浅拷贝:被复制对象的所有变量都含有与原来对象相同的值,内部对其他对象的引用也依旧保存(对象内的对象不会复制过来,因此内部的对象引用实际上是类型共享的存在)
Phone p = new Phone()User u1 = new User(p);User u2 = (User)u1.clone();
此时两个引用 u1和u2指向两个不同的对象,但内部的phone对象却是同一个。
-
深拷贝:彻底的拷贝,会拷贝所有的属性,并拷贝属性指向的动态分配的内存。即对象和其内部的引用对象全部拷贝,因此速度较慢。
Phone p = new Phone();User u1 = new User(p);User u2 = (User)u1.clone();
此时 u1 和 u2 内部的 p 对象已经不同了!
- 深拷贝的实现:手动赋值 | 序列化与反序列化 | 和 json 相互转化
-
谈一谈你对面向对象的理解
-
面向对象 和 面向过程 是两种软件开发的范式
-
面向过程(Procedure Oriented)是一种以过程为中心的编程思想,是一种自顶向下的编程模式,最典型的面向过程的编程语言就是 C语言
- 把问题分解成 一个一个的步骤,每个步骤用函数实现,一次调用即可
-
面向对象,最早出现在 1960年的 simula语言
- 当时的程序设计领域正面临着一种危机:在软硬件环境逐渐复杂的情况下,软件如何得到良好的维护?
- 面向对象程序设计在某种程度上通过强调 代码的复用等 解决了这一问题
- 目前较流行的面向对象语言主要有:Java、C#、C++、Python、PHP等
-
面向对象是一种事务高度抽象化的编程模式
- 将问题分解成一个一个步骤,对每个步骤进行相应的抽象,形成对象
- 通过不同对象的调用、组合来解决问题
-
面向对象的 三大基本特征
-
封装(Encapsulation)
- 所谓封装,指的是将客观事物封装成抽象的类,对外暴露部分属性及行为,实现信息的隐藏
- 简单来说就是 一个类就是一个封装了数据以及操作这些数据的代码的逻辑实体。在一个对象内部,某些代码是私有的,这种方式提供了对内部数据的不同级别的保护,防止无关变量意外的、错误的修改对象中的某些属性
- 所以,封装可以被认为是一个保护屏障,防止该类的代码和数据被外部类定义的代码随机访问。
- 优点
- 良好的封装能够减少耦合
- 类内部的结构可以自由修改
- 可以对成员变量进行更精确的控制
- 隐藏信息,实现细节
-
继承(Inheritance)
-
继承描述了这样一种能力:能够服用原有代码,并在无需修改原来的类的情况下对这些功能进行扩展
-
通过继承创建出来的类被称为 子类或 派生类;被继承的类称为 父类或基类
-
继承的过程就是从 一般 到 特殊的过程,产生了分等级、层次的类
-
注意:Java不支持多继承,但是支持多重继承
-
继承的特性:
- 子类拥有父类的 非 private的属性和方法
- 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展
- 子类可以用自己的方式实现父类中的 抽象方法
- 可能会导致 代码的耦合性上升
-
继承 与 实现
- 继承(Inheritance)是类和类之间的关系,强调的是功能增加和代码复用
- 实现(Implementation)是类和接口之间的关系,强调的是定义一个规范
-
继承 与 组合
-
继承可以实现代码的复用,提高开发效率,但是会提高代码的耦合度,使之变得难以维护(改了父亲代码,儿子代码自动修改了)
-
Java代码的复用有继承、组合、代理三种方式,继承是
is a
的关系,组合是has a
的关系,体现的是整体和部分的关系 -
在继承结构中,父类的内部细节对于子类是可见的,是一种白盒式的代码复用;组合是通过对现有的对象进行拼接产生新的、更复杂的功能,是黑盒式的代码复用,一般采用面向接口编程
-
二者对比
组合关系 继承关系 不破坏封装,整体类与内部类之间松耦合,彼此相互独立 破坏封装,子类与父类之间紧密耦合,子类依赖父类的实现 具有良好的扩展性 可扩展(再向下继承),但会增加系统的复杂程度 可动态组合,运行时选择不同的局部对象 不能动态继承呀 整体类可以对局部类进行包装,封装局部类的接口,提供新的接口 子类不能改变父类的实现 整体类不能获得局部类同样的接口 子类自动继承父类的接口 创建整体时,必须创建所有的局部类 创建子类时,可以不创建父类
-
-
-
多态(Polymorphism)
-
指一个类实例的相同方法在不同情形下有不同的表现
-
多态机制使得具有不同结构的对象可以共享相同的外部接口
-
这意味着,尽管针对不同对象的操作不同,但是通过一个公共的类,它们可以通过相同的方式予以调用
-
多态的优点:
- 降低类之间的耦合度,程序不必为每个派生类编写功能调用,只需要对抽象超类进行处理即可。
- 提高可替换性、可扩展性,派生类的功能可以被超类的方法或引用变量所调用
- 灵活、简化开发
-
多态的弊端:不能使用子类特有的方法
-
多态依靠的三个必要条件
- 类的继承或接口实现
- 方法的重写
- 父类引用指向子类对象
-
针对非静态成员函数,编译看左边,运行看右边;其他的都是全部看左边
-
对象间转型的问题
-
在 Java中,每个对象都属于一个类型,类型描述了这个变量所引用的以及能够引用的对象类型,将一个值存入变量时,编译器会检查是否运行该操作
-
向上转型:将一个子类的引用赋值给一个超类变量,此时不需要进行强制转换
Person p = new Student()
-
向下转型:将一个超类的引用赋值给一个子类变量,必须进行强制类型转换,这样才能通过运行时的检查
Studnet s = (Student) new Person()
\
-
-
instanceof操作符
- 格式:
boolean result = object instanceof class
- 如果 object是 一个 class的实例,则返回 true;否则返回 false
- 格式:
-
方法调用
-
静态绑定:如果是
private
方法、static
方法、final
方法或者构造器,那么编译器可以准确的知道应该调用哪个方法,我们将这种调用方式称为静态绑定,一般认为 Java函数重载也是一种静态多态,因为它需要在编译期决定具体调用哪个方法 -
动态绑定:调用的方法依赖于隐式参数的实际类型,并且在运行时实现调用那个方法,即只有在运行期才能知道真正调用的是哪个类的方法
-
举例:
Father f = new Son(); f.show();/*1. 编译器查看 f的声明类型是 Father2. 编译器将 Father类方法表和其超类中所有的名为 show()的方法列举出来3. 编译器查看调用方法时提供的参数类型,进行重载解析,如果所有名为 show()的方法中存在一个与提供的参数类型完全匹配,就选择这个方法4. 判断该方法是动态绑定还是静态绑定 如果是静态绑定,即方法是 private方法、static方法或构造器,Java虚拟机就调用这个方法 否则,在运行期间,虚拟机会提取 f引用对象的实际类型的方法表*/
-
-
-
-
-
面向对象的五大基本原则(SOLID)
- 单一职责原则(Single-Responsibility Principle)
- 一个类,只做好一件事就是功德圆满了
- 单一职责原则,可以看作高内聚、低耦合在面向对象原则上的引申,将职责定义为引起变化的缘由,提高内聚性来减少引起变化的原因。
- 单一职责用于控制类粒度的大小,一个类承担的职责越多,被复用的可能性就越低,而且多个职责的耦合可能导致互相之间运作的影响
- 优点:
- 降低类的复杂度
- 提高可读性和可维护性
- 降低代码变更可能引起的风险
- 开放封闭原则(Open-Closed Principle)
- 软件实体面向扩展开放、面向修改封闭
- 面向扩展开放表明当有新的需求或变化时,可以对已有的代码进行扩展,以适应新的情况
- 对修改封闭表示类的设计一旦完成,就可以独立完成其工作,而不要对其进行修改
- 核心思想即:对抽象编程而不对具体编程,因为抽象相对稳定
- 让类依赖于固定的抽象,所以修改就是封闭的,而通过面向对象的继承和多态等机制,又可以实现对抽象类的继承,通过覆写其方法来改变固有属性,实现新的拓展方法,因此面向拓展开放
- 优点:
- 保持软件产品的稳定性:开闭原则要求我们通过保持原有代码不变,添加新的代码来实现软件的变化,因为不涉及源代码的改动,所以可以避免为实现新功能而破坏已有功能的情况
- 不影响原有测试代码的运行
- 使代码更具模块化特点,易于维护
- 提高开发效率:即使不懂之前的代码细节,也可以在原有基础上继续开发
- 里氏替换原则(Liskov-Substitution Principle)
- 子类能够替换父类
- 这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保障系统在运行期内识别子类,这是保证继承服用的基础
- 里氏替换原则主要着眼于对抽象和多态建立在继承的基础上,因此只有遵循了里氏替换原则,才能保证继承和复用是可靠的
- 实现的方法是面向接口编程,将公共部分抽象为基类或抽象类
- 违背了里氏替换原则就必然导致违背开闭原则
- 接口隔离原则(Interface-Segregation Principle)
- 使用多个小而精的接口,不要使用大而全的接口
- 接口有效地将细节和抽象分离,接口隔离强调接口的单一性
- 用户不应该依赖他不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上
- 需要将胖接口分离成多个小而精的接口,具体实现方法:
- 委托分离,通过增加一个新的类型来委托客户的请求,隔离用户和接口的直接依赖,但会增加系统的开销
- 多重继承分离,通过接口多继承来实现客户的需求
- 依赖倒置原则(Dependency-Inversion Principle)
- 高层模块和底层模块之间不存在相互依赖,二者共同依赖于抽象的接口
- 在依赖之间定义抽象的接口,使得高层模块调用接口,底层模块实现接口的定义,以此来有效的控制耦合关系
- 抽象的稳定性决定了系统的稳定性,依赖于抽象就是面向接口编程,不要面向实现编程
你不必严格遵循这些原则,违背它们也不会被处以宗教刑罚,但你应当把这些原则视作警铃,若违背了其中一条,警铃就会响起!
- 单一职责原则(Single-Responsibility Principle)
对比 Java的重写与重载
-
重载(Overload),指的是在类中 方法具有相同的方法名 和 不同的参数列表的情形,这些同名不同参的方法之间互称为重载方法,编译器会通过参数匹配寻找最合适的方法。
- 重载规则:
- 被重载的方法必须改变参数列表(参数个数或参数类型)
- 可以改变返回值和方法的权限修饰符(改或不改都可以)
- 被重载的方法可以声明更大更多的异常
- 方法能够在同一个类或子类中被重载
- 好处:
- 提高可读性,比如
add(int a, int b) 和 add(double a, double b, double c)
,通过重载不需要额外命名
- 提高可读性,比如
- main()方法能否重载?
- 可以重载,main()方法也是普通方法
- 但是,Java虚拟机只会调用带字符串数组的静态公共方法,即
public static void main(String[] args)
- 我们可以自定义其他的 main()方法,如:
public static void main(String args), public static void main()
等等,单都需要手动调用
- 方法重载和类型提升问题:
- 如果调用重载方法时,没能找到匹配的数据类型,就会隐式地进行类型提升
- 如果调用重载方法时,没能找到匹配的数据类型,就会隐式地进行类型提升
- 重载规则:
-
重写(Override),子父类中各拥有一个方法标签相同的方法,即方法名相同、参数列表相同,由于具有相同的方法签名,故子方法会覆盖父方法
- 重写规则:
- 方法名、参数列表和返回值类型必须相同
- 访问权限可以扩大,但不能缩小(父类是 protected,子类可以是 public,但不能是 private)
- 抛出的异常不能变多(儿子不能比爸爸更坏)
- 不能重写被标记为 final的方法
- 理论上,声明为 static的方法也不能重写,但是可以被重新声明。(即子父类中同样方法签名的静态方法可以出现,但是不构成重写,IDEA也不会自动生成
@Override
注解) - 如果定义了重写方法后,依旧需要调用父类中的方法,可以使用
super
关键字
- 好处:
- 子类可以根据需要,定义特定于自己的行为
- 重写规则:
-
二者对比
重载 重写 是一个静态的概念,编译期发生 是一个动态的概念,运行期发生 重载遵循编译期绑定,即静态绑定,在编译期就根据参数的数量和类型判断应该调用哪个方法 重写遵循动态绑定,在运行时根据引用变量所指向的实际对象的类型来调用方法 因为在编译期就决定了方法调用,不是 多态 是多态 只有参数列表必须修改 参数列表和返回类型不能修改,异常不能变多,权限不能变小
成员变量和方法的作用域
自己 | 同包 | 子类(其他包下的子类) | 所有类 | |
---|---|---|---|---|
public | Y | Y | Y | Y |
protected | Y | Y | Y | |
default | Y | Y | ||
private | Y |
Java的平台无关性
-
平台无关性指的是一种语言在计算机上的运行不受平台的约束,一次编译,到处执行(Write Once, Run Anywhere)
-
好处:Java程序因为平台无关性的特点,可以运行在各种各样的设备上,减少了跨平台开发和部署的成本
-
如何实现:Java语言规范、Class文件、JVM虚拟机
-
具体步骤
- 前端编译:
javac xxx.java
,通过 .java文件生成 .class文件(一般由服务器端进行编译,生成与平台无关的字节码) - 后端编译:由 .class文件生成 二进制文件(客户端安装的 JVM虚拟机执行字节码,生成对应的机器码)
- 前端编译:
-
Java虚拟机 是与平台有关的,构建在不同的硬件和操作系统之上,可以根据具体的对应的硬件和操作系统生成对应的二进制指令。Java之所以能够做到跨平台,正是由于 JVM充当了桥梁的作用
-
字节码 是各种不同平台虚拟机都统一使用的程序存储格式,JVM只与由字节码组成的 class文件交互
-
同时,由于JVM只与字节码文件进行交互,所以可以跳出 Java的限制,开发其他可生成 class文件的语言,如:Scala, Groovy, Jython
Java并不只是一种语言,在此之前出现的那么多种语言也没有能够引起那么大的轰动。Java是一个完整的平台,有一个庞大的库,其中包含了许多可重用的代码和一个提供诸如安全性、跨操作系统的可以执行以及自动垃圾回收等服务的执行环境。
针对字符串的理解
-
String类被 final修饰,是不可变的类;如果是通过
String s = "abc"
方式创建的字符串,则字符串常量仅存于常量池,常量池在 JDK1.7前位于方法区,1.7后位于堆中(只创建一个);但如果是通过String s = new String("abc")
创建,则既会创建一个常量池中的字符串,同时也会在堆中创建一个对象(共创建两个)。 -
因为 String是不可变的,所以当调用
substring()
方法时,会指向一个全新的字符串-
在 JDK1.6中,当调用
substring()
方法时,会创建一个新的字符串对象,但仍指向原字符数组,只是修改了长度等信息。因此,当字符串很长,而需切割的长度很短时,就会导致性能问题,我们需要的只是一小段,但却引用了整个字符串使之无法回收 -
在 JDK1.7及之后,该问题得到了解决,调用
substring()
方法后会创建一个新的数组
-
-
replaceFirst(), replaceAll(), replace()
方法的区别- 三者都是 Java中常用的替换字符串的方法
replace(CharSequence target, CharSequence replacement)
,用 replacement替换所有的 target项,两者都是字符串replaceAll(String regex, String replacement)
,用 replacement替换所有符合 regex的字符串,regex是一个正则表达式replaceFirst(String regex, String replacement)
,类似前者,但只替换第一个匹配的地方
- 三者都是 Java中常用的替换字符串的方法
-
编译器对字符串的优化
String s = "a"+"bc";
编译器会进行常量折叠(“a”和“bc”都是编译期常量),即变成String s = "ab"
- 对于能够进行优化的字符串,会使用 StringBuilder的
append()
方法替代,最后调用toString()
方法
-
字符串拼接
- 用
+
进行拼接,如String s = "a"+"b";
这里的加号可以视作一种语法糖的效果- 其实底层也是会调用
StringBuffer
的append()
方法的,之后再调用toString()
返回
- 其实底层也是会调用
- 使用
concat()
进行拼接,如String s = "a".concat("b");
- 底层首先会创建一个数组,长度为 当前字符串+拼接字符串的长度之和
- 使用
StringBuilder
中的append()
进行拼接,如:StringBuffer sb = new StringBuffer("a"); sb.append("b");
,循环体内字符串的拼接一般都用append()
StringBuffer
和StrinbgBuilder
底层也维护了一个 char[]数组,但是与 String不同的是,它并不是 final的,是可修改的append()
会直接拷贝字符到内部的字符数组中,如果字符数组长度不够,会进行扩展
- 使用
apache.common
包下的StringUtils.join()
方法,如:String s = StringUtils.join("a", "-", "b");
- 底层也是通过
StringBuilder
实现的
- 底层也是通过
- 效率比较:StringBuilder > StringBuilder > concat > + > StringUtils.join()
- 用
-
String.valueOf()和 Integer.toString()的区别
- 常规的数字变字符串的方法:
Strinbg i1 = "" + i
String i2 = String.valueOf(i)
String i3 = Integer.toString(i)
- String.valueOf() 内部调用了 Integer.toString()
- 常规的数字变字符串的方法:
-
switch对 String的支持
- JDK7之后 switch参数可以是字符串了,目前 switch支持的内容有:byte, short, int, char, String, enum
- switch - case中对字符串的判断是通过
equals()和 hashcode()
方法实现的,因为可能发生哈希碰撞,故性能上肯定不如枚举和纯数字 - 实际上 switch只支持整型,其他类型都是先转换为整型再比较的
-
常量池
- 在 JVM中,为了减少反复创建相同字符串所产生的消耗,会单独开辟一块内存,用于保存字符串常量,称作 ”字符串常量池“
- 当代码中出现 双引号修饰的变量时,都会尝试在常量池中创建字符串对象,如果已创建,就直接返回
- 此外还可以使用
intern()
方法手动添加、查找字符串常量池 - JDK7之前,常量池在永久代中;JDK7时,元空间取代了永久代,常量池就放到了堆中;JDK8及以后,元空间彻底取代永久代,常量池放到了元空间中
- Java中一般有三种常量池:字符串常量池、Class常量池、运行时常量池
- Class常量池可以理解为 Class文件中的资源仓库,Class文件中除了包含类和版本、字段、方法、接口等描述信息外,还有一项信息就是常量池,用于存放编译器生成的各种字面量(Literal)和符号引用(Symbolic Reference);由于不同的 Class文件中包含的常量的个数不是固定的,所以在 Class文件的常量池入口会设置两个字节的计数器,记录其中的常量个数
- Class常量池中 主要存了两类:字面量和符号引用。
- 字面量(literal)是用于表达源代码中一个固定值的表示法(nonation),可以理解为,字面量就是有字母、数字等构成的字符串或数值
- 符号引用(symbolic reference)是相对于直接引用来说的,主要包括了:类和接口的全限定名、字段的名称和描述符、方法的名称和描述符
- Class常量池的意义
- Java在进行代码编译时,并不像 C/C++那样有 连接 这一步,而是在 JVM加载 Class文件时进行 连接。也就是说,在 Class文件中不会保存各个方法、字段的最终内存信息,因此必须通过符号引用实现运行期的转换,找到真正的内存入口。
- JVM运行时,需要从常量池获取对应的符号引用,之后在类创建或运行时解析(实际上是在类加载的 linking 的 resolution阶段替换符号引用)
- 运行时常量池 是每一个类或接口的常量池的运行时表示形式
- 包含了若干不同的常量:数值字面值、方法引用、字段引用、符号表等
- 每一个运行时常量池都分配在 JVM的方法区中,在类和接口被加载到虚拟机后,对应的运行时常量池就会被创建出来
- 在 JVM中,为了减少反复创建相同字符串所产生的消耗,会单独开辟一块内存,用于保存字符串常量,称作 ”字符串常量池“
-
字符串长度限制
- 常量池的限制,在 《Java虚拟机规范》中定义了常量池的
CONSTANT_String_info
,明确了String_index
项的值必须为对常量池的有效索引,常量池在该索引处的项必须是CONSTANT_Utf8_info
结构,表示一组 Unicode码点序列,其中定义了长度length
为两个字节的无符号整数,故最大长度为 65534(最后有一个 1bit标识位) - 运行期的限制,由于 String底层内部维护了一个 char[]数组,所以数组长度不能超过 int类型,即4个字节
- 常量池的限制,在 《Java虚拟机规范》中定义了常量池的
集合相关
ArrayList
可调节列表容量大小的,基于数组实现的容器,元素可以是 null
,该类大致上同 vector
相似,但没有同步
其中 size(), isEmpty(), get(), set(), iterator(), listIterator()
等方法都是 O(1)的时间复杂度;add()
方法是 O(N)的时间复杂度
每个 ArrayList实例都有一个 容量(Capacity),即内部维护的数组最多存储元素的数量
当有元素加入到 ArrayList中的时候,它的容量能实现自动增长,可以使用 ensureCapacity()
方法保证容量足够
我们不能保证通过 快速失败机制来维护它的线程安全
一些初始化的信息
/**
* 一些初始化的信息
*/
// 序列化 ID
private static final long serialVersionUID = 8683452581122892189L;
// 初始容量
private static final int DEFAULT_CAPACITY = 10;
// 用于空实例的共享数组实例
private static final Object[] EMPTY_ELEMENTDATA = {};
// 用于默认大小的空实例的数组,区别于 EMPTY_ELEMENTDATA[] 数组
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
// 真实存储 ArrayList元素的数组缓冲区,其长度即为 ArrayList的容量,在第一次添加元素时就会扩展为 DEFAULT_CAPACITY
transient Object[] elementData;
// ArrayList的实际长度(保存的元素的个数)
private int size;
// 当前 ArrayList被结构化修改的次数,结构化修改指的是 修改了列表长度或者以某种方式扰乱了列表使之在迭代时不安全的操作
// 该字段由 Iterator或者 ListIterator或返回以上二者的方法使用,如果这个值发生了意料之外的改变,迭代器就会报并发修改异常。这提供了快速失败解决方案
// 在其子类下使用 modCound是可选择的(protected修饰了),如果子类希望提供快速失败的迭代那么它也可以在它的 add()等结构化修改方法中修改 modCount的值,单次的结构修改方法的调用最多只能修改一次 modCount的值;但是如果不希望提供 快速失败 的迭代器,子类可以忽略 modCount
// 我之前一直以为 modCount定义在 迭代器中。。。,现在才知道定义在被迭代的对象内
protected transient int modCount; // 定义在 AbstractList中
// 列表最大能分配的量
private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
构造方法
/**
* 构造方法
*/
// 空参构造
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 有参构造,指定集合的长度创建一个 ArrayList
public ArrayList(int initialCapacity) {
if (initialCapacity > 0) {
// 依据输入的大小,初始化 elementData[] 数组(真实存储元素的数组)
this.elementData = new Object[initialCapacity];
} else if (initialCapacity == 0) {
// 使用默认的大小(也就是 10)
this.elementData = EMPTY_ELEMENTDATA;
} else {
// 针对负数等随便输入的东西,报错非法参数异常(非法的初始容量:xxx)
throw new IllegalArgumentException("Illegal Capacity: "+
initialCapacity);
}
}
// 有参构造,指定一个已有的集合创建 ArrayList(原集合内的元素会添加到 新建的ArrayList中),此时 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 {
// 如果传参的集合为空,当前 ArrayList的 elementData[]数组就是空的
this.elementData = EMPTY_ELEMENTDATA;
}
}
针对 第三种构造方法的实验:
- 传入非空的集合,发现 ArrayList的容量等于传入的集合内的元素个数(因为在 ArrayList内部 elementData是 private修饰的,且未提供 get方法,所以使用 反射暴力获取其长度)
- 传入空集合,ArrayList长度也为 0
-
有一个很有趣的点:
- 空参创建 ArrayList时,其 容量为 0,而一旦往里面加入元素之后,容量就会变成 10(默认容量)
- 而针对传入 集合作为参数的构造器创建的 ArrayList,当作为参数的集合为空时,ArrayList的容量为 0;但是向 ArrayList内添加元素之后,容量就是添加的元素的个数,不是 ArrayList默认的扩容规则
-
针对这个问题继续进行实验:
-
向两个arraylist内分别添加 13个元素,发现 由集合传参构造的 ArrayList的容量依旧等于传参个数;空参构造的 ArrayList容量符合正常的扩容规则,即 原容量 * 1.5
-
常见方法
void trimToSize()
缩减列表的大小为当前已存的元素的个数
public void trimToSize() { // 因为调整列表的容量是结构化修改,所以需要变更 modCount的值 modCount++; // 如果当前的元素数 小于 最大容量 if (size < elementData.length) { // 修改最大容量为当前元素的个数 elementData = (size == 0) ? EMPTY_ELEMENTDATA : Arrays.copyOf(elementData, size); // 使用这种写法来实现数组变小: elementData = Arrays.copyOf(elementData, size) 感觉好神奇啊。。 }}
void ensureCapacity(int minCapacity)
增加当前列表的容量,至少是达到能够容纳所有元素的容量
public void ensureCapacity(int minCapacity) { // elementData和 DEFAULTCAPACITY_EMPTY_ELEMENTDATA(就是一个空数组)相等时 minExpand = 10,否则等于 0 // 意思就是说,只有刚开始,ArrayList还没添加元素的时候,minExpand会是 10;添加过元素之后它就不干净了,minExpand = 0 了 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 ensureExplicitCapacity(int minCapacity) { // 因为是结构性的修改,所以需要改变 modCount的值 modCount++; // overflow-conscious code // 判断一下要不要扩容,如果改变之后的容量大于当前数组最大容量,就要扩容了呀 if (minCapacity - elementData.length > 0) // 定义放在下面 grow(minCapacity);}// 底层 ArrayList扩容的方法,其实就是 elementData[]数组扩大的方法private void grow(int minCapacity) { // overflow-conscious code // 获取原来的容量 int oldCapacity = elementData.length; // 获取常规的扩容大小 int newCapacity = oldCapacity + (oldCapacity >> 1); // 如果常规扩容之后还不够大,索性就你要多少就给你多少吧 if (newCapacity - minCapacity < 0) newCapacity = minCapacity; // MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8,也就是说如果你要的容量很大,那么就调用 另一个方法 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);}// 索求得太多了呀。。。private static int hugeCapacity(int minCapacity) { // 判断 minCapacity是否小于0 ? if (minCapacity < 0) // overflow throw new OutOfMemoryError(); // MAX_VALUE = 0x7fffffff;MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8 = 0x7fffffff - 8 ... return (minCapacity > MAX_ARRAY_SIZE) ? Integer.MAX_VALUE : MAX_ARRAY_SIZE;}
int size()
返回当前列表的元素个数
注意:
- 这里返回的是 元素的个数,而不是 列表的 容量
- 列表的 容量(Capacity)默认只能通过 elementData[]数组的 length获取,但是 elementData[]是 private修饰的,所以只能通过 反射(至少我只会反射获取)
public int size() { // 返回当前 list的 elment的数量 // private int size; 保存 list的实际元素个数 return size;}
// 反射获取 elementData[]数组的方法
public static int getArrayListCapacity(ArrayList<?> arrayList) {
Class<ArrayList> arrayListClass = ArrayList.class;
try {
Field field = arrayListClass.getDeclaredField("elementData");
field.setAccessible(true);
Object[] objects = (Object[]) field.get(arrayList);
return objects.length;
} catch (NoSuchFieldException e) {
e.printStackTrace();
return -1;
} catch (IllegalAccessException e) {
e.printStackTrace();
return -1;
}
}
boolean isEmpty()
判断 列表是否为空
public boolean isEmpty() {
return size == 0;
}
boolean contains(Object o)
判断列表中是否包含与指定值相同的元素,如果有,则返回 true;否则,返回 false
public boolean contains(Object o) { // 定位传参值的位置,具体解释在下一个 return indexOf(o) >= 0;}
int indexOf(Object o)
返回在列表中 与指定值相等的元素 第一次出现索引位置;如果 该值不存在,就返回 -1
public int indexOf(Object o) { // 先判断 o是否为 null if(o == null) { // 如果是 null,就遍历检索整个 elementData[]数组,寻找 null值,返回数组下标 for(int i = 0; i < size; i++) { if(elementData[i] == null) { return i; } } } else { // 否则,遍历 elementData[]数组,调用 equals()方法,判断是否存在等值元素,返回数组下标 for(int i = 0; i < size; i++) { if(o.equals(elmentData[i])) { return i; } } } // 如果都没找到,就返回 -1 return -1;}
int lastIndexOf(Object o)
类似 indexOf(Object o)
方法,只是返回的是 列表中元素最后出现的 数组下标;如果列表中不存在,则返回 -1
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;}
Object clone()
返回一个列表实例的浅克隆拷贝,即声明了另一个指向 堆中列表的引用
也就是说,如果你在 list1
中修改了某个元素,那么在 list
中也会变化,因为二者指向同一个地址
public Object clone() { try { // 调用 Object类下的 clone()方法,强转为一个 ArrayList ArrayList<?> v = (ArrayList<?>) super.clone(); // 拷贝新列表中的 elmentData[]数组 v.elementData = Arrays.copyOf(elmentData, size); // 设置新数组的 modCount次数为 0 v.modCount = 0; return v; } catch(CloneNotSupportedException e) { // this shouldn't happen, since we are cloneable throw new InternelError(e); }}
Debug后发现:
如果要实现深克隆,可以采用如下代码,将具体的内部的元素也克隆一份
@Testpublic void testClone2() throws CloneNotSupportedException { ArrayList<Student> list = new ArrayList<>(); list.add(new Student("张三", 23)); list.add(new Student("李四", 24)); ArrayList<Student> listCopy1 = (ArrayList<Student>) list.clone(); System.out.println(list == listCopy1); System.out.println(list.equals(listCopy1)); ArrayList<Student> listCopy2 = new ArrayList<>(); for (Student student : list) { listCopy2.add((Student) student.clone()); } System.out.println(listCopy2);}
Object[] toArray()
返回一个新的数组,包含了列表中的所有的元素(依照列表内的顺序排列)
返回的数组和原来的列表无关,也就是说,对数组内元素的修改不影响列表内元素的值
该方法充当了 集合类和 数组类之间的桥梁
public Object[] toArray() { return Arrays.copyOf(elementData, size);}
<T> T[] toArray(T[] a)
类似上面的那个方法,也会把 列表转换成 数组
区别在于,这个数组不再是 Object类型,而是指定的类型
- 如果列表内的元素符合传入的参数,那么就会以那个参数作为数组元素类型
- 如果不符合,则会新建一个以运行类型为数组类型的数组
如果传入的数组的容量大于列表的容量,则在数组中紧跟在列表最后一个元素之后的元素全部会被设置为 null
这样有助于确定列表的容量
@SuppressWarnings("unchecked")public <T> T[] toArray(T[] a) { // 如果传入的数组比列表小,就会返回一个和列表一样大的(注意 是 和列表一样大的)数组,也就是说会返回一个更大的数组 if(a.length < size) { return (T[]) Arrays.copyOf(elementData, size, a.getClass()); } // 如果数组的大小比列表大,就直接将 elementData[]中的值拷贝到 a[]即可,拷贝的长度为 size个,都是从 0开始拷贝 System.arrayCopy(elmentData, 0, a, 0, size); // 会有很神奇的一幕:如果数组比列表大(数组为10个单位,列表长3个单位),且数组已经初始化了,则:arr[0] ~ arr[3]都是列表中的值,arr[4]会变成 null,arr[5] ~ arr[9] 是数组的原来的值;估计是为了方便之后遍历判断吧 if(a.length > size) { a[size] = null; } return a;}
这部分源码比较难
-
泛型方法
-
定义泛型方法需要在返回值前加上泛型参数,如:
public <T> T[] method(T a) { ... }
-
描述 类时,使用
E
,如:ArrayList<E>
,它与泛型方法中的T
不同 -
父类对象能够显示的强制转换为子类对象的前提是:该对象本质上是子类(或者孙子类等)对象
// o是一个子类对象 String的引用Object o = new String("abc");// 此时,强转不会出错String s = (String) o;// 但是如果如下定义,o不是子类对象的引用o = new Object();// 此时就会出错s = (String) o;/*也就是说,至少要有多态那样的样子,父类引用指向子类对象之后,父类引用才能够使用强制类型转换变回子类引用*/
-
数组也是对象,由 JVM提供定义和初始化。但是 Object[] 和 String[] 之间没有继承关系,但是 它们之间存在协变,使得数组对象也能像子父类一样转换(Java中的强制类型转换一般针对的只是单个对象,数组的强转一般都不好使)
Object[] o = new String[]{"abc"};String[] s = (String[]) o; // 此时不会出错o = new Object[]{};s = (String[]) o; // 此时会出错
-
-
而之所以 将方法名定义为
public <T> T[] toArray(T[] a)
,是由于泛型会在编译期被擦除,实际上 ArrayList中 elementData[]数组就是一个 Object[]数组transient Object[] elementData;
- 那为什么不把方法名写成
public E toArray()
呢?因为 泛型E
会被擦除,方法实际返回的仍然是 Object[] 而不是 String[],最后是由编译器分析 列表对象声明后追加强制转换到结果上的,但是这样的转换是不能进行的! - 但是当加入了参数
T[] t
之后,会在插入时就把每个元素逐个强制转换为 String,再加入到一个 String[]数组中,然后返回 Object[],再由编译器强转为 String[],而这么做是可行的!
- 那为什么不把方法名写成
-
Arrays.copyOf()
这个方法有很多重载形式-
public static <T> T[] copyOf(T[] original, int newLength)
:指定长度进行数组拷贝,空的部分用 null来填充,这样能保证定长;两个数组能保证内部元素相同 -
public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType)
:将原来 U类型的数组,复制成指定长度的 T类型的数组public static <T,U> T[] copyOf(U[] original, int newLength, Class<? extends T[]> newType) { @SuppressWarnings("unchecked") // 先创建一个指定长度的空数组,类型依据泛型 T[] copy = ((Object)newType == (Object)Object[].class) ? (T[]) new Object[newLength] : (T[]) Array.newInstance(newType.getComponentType(), newLength); // 调用底层的数组内容复制方法 System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy;}
-
public static byte[] copyOf(byte[] original, int newLength
、public static short[] copyOf(short[] original, int newLength)
、public static int[] copyOf(int[] original, int newLength)
、public static long[] copyOf(long[] original, int newLength)
、public static char[] copyOf(char[] original, int newLength)
、public static float[] copyOf(float[] original, int newLength)
、public static double[] copyOf(double[] original, int newLength)
、public static boolean[] copyOf(boolean[] original, int newLength)
:就直接指定了数组拷贝的类型/*** 以 int型拷贝为例*/public static int[] copyOf(int[] original, int newLength) { int[] copy = new int[newLength]; System.arraycopy(original, 0, copy, 0, Math.min(original.length, newLength)); return copy;}
-
-
System.arraycopy(elmentData, 0, a, 0, size)
是一个本地方法,方法原型为:public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);
src
:源数组srcPos
:源数组中开始复制的下标值dest
:目标数组destPos
:目标数组中开始复制的下标值length
:复制多少个元素
E get(int index)
返回列表中 指定位置的元素
public E get(int index) { // 检查索引是否正常,定义放在下面 rangeCheck(index); // 返回 elementData[]数组中,该下标的元素 return elementData(index);}private void rangeCheck(int index) { // 如果指定的值大于数组的最大下标,就抛出异常 if(index >= size) { // 抛出异常,outOfBoudnsMsg()方法定义放在下面 throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }}private String outOfBoundsMsg(int index) { return "Index: " + index + ", Size: " + size;}
E set(int index, E element)
使用传入的参数,替换指定位置的元素,并返回原来的值
public E set(int index, E element) { // 同 get()方法一样,先检查 index是不是比 size小 rangeCheck(index); // 获取该位置上原来的值 E oldValue = elementData[index]; // 在 elementData[]数组中替换该位置下的值 elementData[index] = element; // 返回原来的值 return oldValue;}
boolean add(E e)
在列表的末尾添加元素,事实上就是在 elementData[]数组中添加下一个值
public boolean add(E e) {
// 判断列表容量够不够,实际判断 elementData[]数组里还有没有多余的空闲空间
ensureCapacityInternal(size + 1); // increments modCount
elementData[size++] = e;
return true;
}
private void ensureCapacityInternal(int minCapacity) {
// 确保容量够用,calculateCapacity()计算扩容
ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
}
private static int calculateCapacity(Object[] elementData, int minCapacity) {
// 如果列表是默认空参构造创建的,并且本次是第一次 add
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
// 那么会直接把容量加到 10
return Math.max(DEFAULT_CAPACITY, minCapacity);
}
// 否则就正常 +1就行
return minCapacity;
}
private void ensureExplicitCapacity(int minCapacity) {
// 结构化修改,增加该值,保证该集合是快速失败的
modCount++;
// overflow-conscious code
// 判断是否会溢出
if (minCapacity - elementData.length > 0)
// 如果会溢出,就需要扩容了(也就是说 原有的 size+1 > capacity了,装不下了)
grow(minCapacity);
}
// grow()方法在之前的 ensureCapacity(int minCapacity)方法中解释过了
void add(int index, E element)
在列表的指定位置插入指定的元素
public void add(int index, E element) { // 专门为这个 add()方法写了一个检查范围的函数,面子够大的。。 rangeCheckForAdd(index); // 确定容量 ensureCapacityInternal(size + 1); // increments modCount !! // 复制数组,将从插入位置之后开始的内容全部后移一位 System.arraycopy(elementData, index, elementData, index + 1, size - index); // 设置值 elementData[index] = element; size++;}private void rangeCheckForAdd(int index) { // 如果要插入的索引 大于 当前最大索引 或者 小于0 if (index > size || index < 0) // 抛出异常 throw new IndexOutOfBoundsException(outOfBoundsMsg(index));}
对比 E set(int index, E element)
方法:
- set()方法单纯的改变原来的值,add()方法会在原有的列表内部(注意是已有元素列表的里面,index<size的情况下)添加元素
- 看起来 add()方法更适合用来进行 插排等排序操作
- 因为 add()方法会对向数组内添加额外的元素所以,需要检验扩容 ensureCapacity()
E remove(int index)
移除列表中指定位置的元素,数组中后续的元素都会左移一位,并且返回被移除的元素
public E remove(int index) { // 检查索引越界 rangeCheck(index); // 结构化修改,变更 modCount的值 modCount++; // 获取旧的值 E oldValue = elementData(index); // 计算需要移动的数字的个数 int numMoved = size - index - 1; if(numMoved > 0) { // 通过数组拷贝的形式,将从待移除位置开始往后的所有元素前移一位 System.arraycopy(elementData, index+1, elementData, index, numMoved); } // 将最后一个元素置为 null(因为数组拷贝的关系,在 elementData[size-1] 和 elementData[size-2]中存的都是最后一个值,重复了) elementData[--size] = null; // clear to let GC do its work // 返回旧的值 return oldValue;}
boolean remove(Object o)
如果列表中存在和指定的值相同的元素,那么就移除其中第一个出现的元素,返回 true;
如果不存在,啥都不干,返回 false
public boolean remove(Object o) { // 如果待移除的元素是 null if(o == null) { // 遍历整个列表寻找 null值 for(int index = 0; index < size; index++) { if(elementData[index] == null) { // 通过索引移除,只是在 fastRemove()基本可以保证索引的合规 fastRemove(index); return true; } } // 如果元素不是 null } else { // 同样遍历列表,通过 equals()方法进行判断 for(int index = 0; index < size; index++) { if(o.equals(elementData[index])) { // 快速移除 fastRemove(index); return true; } } } // 没找到呀,那就返回 false吧 return false;}// 对比 E remove(int index)方法,删去了索引越界检查和旧值返回private void faseRemove(int index) { // 修改 modCount值 modCount++; // 计算需要移动的元素的个数 int numMoved = size - index - 1; if(numMoved > 0) { // 通过数组拷贝进行移动 System.arraycopy(elementData, index+1, elementData, index, numMoved); } // 把最后一个重复的项设置为 null elementData[--size] = null // clear to let GC do its work}
void clear()
清除列表中的所有元素
public void clear() { // 修改 modCount的值 modCount++; // 遍历整个数组,把把每个引用都设置为 null // clear to let GC do its work for(int i = 0; i < size; i++) { elementData[i] = null; } // 设置列表的长度为 0(列表为空) size = 0;}
.
boolean addAll(Collection<? extends E> C)
将作为参数传入的集合内的元素 全部添加到 elementData[]数组的尾部
public boolean addAll(Collection<? extends E> c) { // 将传入参数的集合转为数组 Object[] a = c.toArray(); // 获取待插入的元素的个数 int numNew = a.length; // 确保容量足够,也就是说保证 elementData[]数组能够存下 size+newNum个元素 ensureCapacityInternal(size + newNum); // Increments modCount // 通过数组复制进行插入,将 a[]数组从 0索引开始的 newNum个元素复制到 elementData[]数组中从 size开始的位置 System.arraycopy(a, 0, elementData, size, newNum); // 修改当前集合中元素的个数 size+=numNew; // 返回集合是否插入成功,如果传入的集合长度为0,则被认为插入失败 return numNew != 0;}
boolean addAll(int index, Collection<? extends E> c)
从当前列表的某个位置开始,将指定集合中的全部元素插入到当前列表中;该位置之后的所有元素全部右移
public boolean addAll(int index, Collection<? extends E> c) { // 判断这个索引位置是否合理 rangeCheckForAdd(index); // 将传入集合转换为数组 Object[] a = c.toArray(); // 获取该数组的长度 int numNew = a.length; // 确保列表中元素位置够用,即 capacity >= size+numNew ensureCapacityInternal(size + numNew); // Increments modCount // 计算指定索引之后的元素每个需要移动几位 int numMoved = size - index; if(numMoved > 0) { // 通过数组拷贝的方式,先将列表的 elementData[]数组中的部分元素后移 System.arraycopy(elementData, index, elementData, index + numNew, numMoved); } // 通过数组拷贝的方式,将指定集合中的元素拷贝到当前 elementData[]数组中,实现 addAll的效果 System.arraycopy(a, 0, elementData, index, numNew); // 更新列表中元素的个数 size += numNew; // 返回是否添加正常 return numNew != 0;}
boolean removeAll(Collection<?> c)
移除当前列表中,同时存在与指定集合中的元素
public boolean removeAll(Collection<?> c) { // 判断集合 c引用非空 Obejcts.retuireNonNull(c); // 调用批量删除的方法,并且返回最终结果 return batchRemove(c, false);}public static <T> T requireNonNull(T obj) { // 如果参数是 null,就抛出异常 if(obj == null) { throw new NullPointerException(); } return obj;}private boolean batchRemove(Collection<?> c, boolean complement) { // 创建一个局部变量,作为 elementData[]的副本 final Object[] elementData = this.elementData; // r用于遍历整个数组,w用于对具体的位置进行修改 int r = 0, w = 0; // 修改标志位 boolean modified = false; try { // 遍历整个 elementData[]数组 for(; r < size; r++) { // 传入集合中是否存在和 列表中值相同的元素,通过 complement决定是删去相同的元素还是不同的元素 if(c.contains(elementData[r]) == complement) { // 通过值的覆盖实现,如果 complement = false // 也就是说当满足以上条件时,数组内的元素不在集合中,将r处的值赋给w处,w和r都后移一位; // 如果不满足条件,即元素需要删除,就让 w不动,r后移 // 这样可以实现:把需要移除的数据都替换掉,不需要移除的数据前移 elementData[w++] = elementData[r]; } } } finally { // preserve behavioral compatibility with AbstractCollection, // even if c.contains() throws if(r != size) { System.arraycopy(elementData, r, elementData, w, size - r); w += size - r; } if(w != size) { // clear to let GC do its work // w之前的所有元素都是需要保留的元素,其之后的元素都可以重复的,就可以删去了 for(int i = w; i < size; i++) { elementData[i] = null; } modCount += size - w; size = w; modified = true; } } return modified;}
举个栗子
通过 Debug,我们能看到:
boolean retainAll(Collection<?> c)
在当前列表中,只保留在指定集合中出现过的元素
也就是说,保留交集
public boolean retainAll(Collection<?> c) {
// 保证 c不是 null
Objects.requireNonNull(c);
// 相比 removeAll(),这里将 complement值设置为 true,表示在执行 batchRemove()方法时,只会将指定集合中出现的内容保留
return batchRemove(c, true);
}
void writeObject(java.io.ObjectOutputStream s)
ArrayList的序列化方法,保存当前 list的实例状态并以流的形式写出去
private void writeObject(java.io.ObjectOutputStream s) throws java.io.IOException { // 先记录当前的 modCount值,方便与之后进行比较,保证在写出时内容没有改变过 // write out element count, and any hidden stuff int expectedModCount = modCount; // 写出流中的非静态和非瞬态的字段 s.defaultWriteObject(); // write out size as capacity for behavioural compatibility with clone() // 将列表容量写出 s.writeInt(size); // write out all elements in the proper order // 循环遍历列表,将其中的每一个元素都写出 for(int i = 0; i < size; i++) { s.writeObject(element[i]); } // 如果写完之后发现,中途有其他线程修改的了列表中的内容,就抛出并发修改异常 if(modCount != expectedModCount) { throw new ConcurrentModificationException(); }}
void readObject(java.io.ObjectOutputStream s)
通过对象输入流反序列化一个对象,即重新构建一个 list
private void readObject(java.io.ObjectOutputStream s) throws java.io.IOException, ClassNotFoundException { elementData = EMPTY_ELEMENTDATA; // Read in size, and any hidden stuff s.defaultReadObject(); // Read in capacity s.readInt(); // ignored if(size > 0) { // be like clone(), allocate array based upon size not capacity int capacity = calculateCapacity(elementData, size); SharedSecrets.getJavaOISAccess().checkArray(s, Object[].class, capacity); ensureCapacityInternal(size); Object[] a = elementData; // Read in all elements in the proper order for(int i = 0; i < size; i++) { a[i] = s.readObject(); } }}
ListIterator<E> listIterator(int index)
返回一个从指定位置开始的列表迭代器
public ListIterator<E> listIterator(int index) { // 判断索引值是否合理 if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index: " + index); } return new ListItr(index);}
ListIterator<E> listIterator()
返回一个包含全部元素的列表迭代器
public ListIterator<E> listIterator() { // 直接返回全部元素的迭代器 return new ListItr(0);}
Iterator<E> iterator()
返回一个包含全部元素的普通迭代器
public Iterator<E> iterator() { return new Itr();}
List<E> subList(int fromIndex, int toIndex)
根据给定的索引值,获取当前列表的子列表(两端元素都包含的,是闭区间)
但是,如果 fromIndex == toIndex,则会返回一个空集合
本质上是拷贝,子列表中对元素的非结构性修改会影响到父列表,反之同理
对子列表的操作和其他所有正常的 arraylist一样
可以采取如下方法移除列表中的部分元素:list.subList(from, to).clear()
public List<E> subList(int fromIndex, int toIndex) { // 检验起始索引和结束索引是否合理 subListRangeCheck(fromIndex, toIndex, size); // 构造一个新的列表类,SubList是 AbstractList的子类,和 ArrayList是兄弟关系 return new SubList(this, 0, fromIndex, toIndex);}static void subListRangeCheck(int fromIndex, int toIndex, int size) { if(fromIndex < 0) { throw new IndexOutOfBoundsException("fromIndex = " + fromIndex); } if(toIndex > size) { throw new IndexOutOfBoundsException("toIndex = " + toIndex); } if(fromIndex > toIndex) { throw new IllegalArgumentException("fromIndex(" + fromIndex + ") > toIndex(" + toIndex + ")"); }}
※void forEach(Consumer<? super E> action)
可以使用 forEach配合 lambda表达式
public void forEach(Consumer<? super E> action) { // 判断非空引用 Objects.requireNonNull(action); // 获取修改值 final int expectedModCount = modCount; final E[] elementData = (E[]) this.elementData; // 遍历每个元素,对每一个元素都调用 accpet()方法进行处理 for(int i = 0; modCount == expectedModCount && i < size; i++) { action.accept(elementData[i]); } if(modCount != expectedModCount) { throw new ConcurrentModificationException(); }}
※Spliterator<E> spliterator()
创建一个 延迟绑定和 快速失败的,确定大小的,且子迭代器也确定大小的,有序的一个可拆分迭代器,它也是用于迭代遍历数组的
但是有别于 Iterator,它用于并行操作,其数据源可以是数组、集合、IO通道或生成器函数
它可以单独遍历元素 tryAdvance()
,也可以批量遍历元素 forEachRemainning()
,并且可以调用trySplit()
方法进行迭代器拆分,使之变成上一个 Spliterator
的一半大小
每个 Spliterator
实例中都定义了 int characteristics()
方法,返回当前实例的一个特征集,这个集合包括:
public static final int ORDERED = 0x00000010
:表示迭代器会按照其原始顺序迭代其中的元素public static final int DISTINCT = 0x00000001
:表示迭代器中的元素是没有重复的public static final int SORTED = 0x00000004
:表示迭代器是按照某种方式排序后顺序迭代其中的元素public static final int SIZED = 0x00000040
:表示迭代器的元素是个数是确定的public static final int NONNULL = 0x00000100
:表示迭代器中没有 null元素public static final int IMMUTABLE = 0x00000400
:表示元素不可变public static final int CONCURRENT = 0x00001000
:表示迭代器可多线程操作public static final int SUBSIZED = 0x00004000
:表示子迭代器也是确定大小的
如果一个迭代器没有报告 IMMUTABLE
或者 CONCURRENT
特征时,其绑定数据源后会检查数据源的结构化改动
后绑定的迭代器指其 绑定数据源的行为 发生在其第一次遍历、第一次拆分、第一次查询大小时,而不是在迭代器创建后立刻绑定数据源。
和其它迭代器一样,绑定前对源的修改可以反应在迭代器遍历过程中,而绑定后的对源的修改会导致ConcurrentModificationException异常。
public Spliterator<E> spliterator() { return new ArrayListSpliterator<>(this, 0, -1, 0);}
boolean removeIf(Predicate<? super E> filter)
删除满足给定谓词条件的所有元素,默认使用 iterator遍历集合中的元素,并且使用 iterator.remove()删除匹配的元素
public boolean removeIf(Predicate<? super E> filter) { Objects.requireNonNull(filter); // figure out which elements are to be removed // any exception thrown from the filter predicate at this stage // will leave the collection unmodified int removeCount = 0; // BitMap的底层维护了一个 long[]数组 final BitSet removeSet = new BitSet(size); final int expectedModCount = modCount; final int size = this.size; // 遍历判断哪些元素符合删除条件,并且在 bitSet中记录其索引值 for (int i = 0; modCount == expectedModCount && i < size; i++) { @SuppressWarnings("unchecked") final E element = (E) elementData[i]; if (filter.test(element)) { removeSet.set(i); removeCount++; } } if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } // shift surviving elements left over the spaces left by removed elements final boolean anyToRemove = removeCount > 0; // 如果存在需要移除的元素 if (anyToRemove) { // 计算移除后,剩下的元素个数 final int newSize = size - removeCount; // 遍历 bitSet,取出之前存入的需要删除的元素的索引 for (int i = 0, j = 0; (i < size) && (j < newSize); i++, j++) { // nextClearBit()返回下一个空的位置,也就是说,把要保留的全部左移 i = removeSet.nextClearBit(i); elementData[j] = elementData[i]; } // 将多余的元素设置为null for (int k = newSize; k < size; k++) { elementData[k] = null; // Let gc do its work } this.size = newSize; if (modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++; } return anyToRemove;}
void replaceAll(UnaryOperator<E> operator)
通过 operator对每个元素进行加工,并将结果替换列表中原来的值
public void replaceAll(UnaryOperator<E> operator) { Objects.requireNonNull(operator); final int expectedModCount = modCount; final int size = this.size; // 遍历每一个元素,对其进行 apply()方法的操作,并且再赋值给自己 for(int i = 0; expectedModCount == modCount && i < size; i++) { elementData[i] = operator.apply((E) elementData[i]); } if(modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++;}// 函数式接口,代表对某一个操作数的一次操作,参数和返回值都必须是同一类型@FunctionalInterfacepublic interface UnaryOperator<T> extends Function<T, T> { static <T> UnaryOperator<T> identity() { return t -> t; }}
void sort(Comparator<? super E> c)
通过传入的比较器对列表进行排序
public void sort(Comparator<? super E> c) { final int expectedModCount = modCount; // 底层调用数组的排序方法 Arrays.sort((E[]) elementData, 0, c); if(modCount != expectedModCount) { throw new ConcurrentModificationException(); } modCount++;}// 根据给定的比较准则(Comparator),在指定的范围内(范围 前开后闭),对数组内的元素进行排序// 该排序是稳定的,相同值的元素不会发生交换public static <T> void sort(T[] a, int fromIndex, int toIndex, Comparator<? super T> c) { if(c == null) { // 该方法内部也和下面的判断类似,只不过是使用默认的比较器实现 sort(a, fromIndex, toIndex); } else { // 底层为 mergeSort()归并排序,分而治之 rangeCheck(a.length, fromIndex, toIndex); if(LegacyMergeSort.useRequested) { // 底层为 binarySort()二叉排序 legacyMergeSort(a, fromIndex, toIndex, c); } else { TimSort.sort(a, fromIndex, toIndex, c, null, 0, 0); } }}
常用内部类
private Class Itr implements Iterator<E>
是对 AbstractList.Itr的一个优化版本
private class Itr implements Iterator<E> {
// 下一个元素的索引值
int cursor; // index of next element to return
// 上一个返回的元素的索引,如果没有就是 -1
int lastRet = -1; // index of last element returned; -1 if no such
// 并发修改控制字段
int expectedModCount = modCount;
// 空参构造
Itr() {
}
// 判断是否还有下一个元素,正常索引值为 0 ~ size-1,如果 cursor == size,则表示已经全部迭代完了
public boolean hasNext() {
return cursor != size;
}
// 获取下一个元素
@SuppressWarnings("unchecked")
public E next() {
// 判断并发修改状态
checkForComodification();
// 获取下一个将要返回的索引值
int i = cursor;
// 如果下一个索引超出了最大位置,就抛出异常
if (i >= size) {
throw new NoSuchElementException();
}
// 获取当前列表中的 elementData[]数组
Object[] elementData = ArrayList.this.elementData;
// 再检索一遍索引位置,能走到这一步说明前面的 if()判断已经通过了,如果这里通不过则表明,其他线程对列表进行了修改,需要抛出并发修改异常
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
// 修改指向下一个元素的指针
cursor = i + 1;
// 返回数组中位于该索引位置的元素,同时将 lastRet设置为当前索引,并且由于 elementData[]为 Object数组,所以进行强转
return (E) elementData[lastRet = i];
}
// 移除当前元素
public void remove() {
// 如果还没有调用过 next(),就直接 remove()了,就会抛出异常,因为 lastRet默认是 -1
if (lastRet < 0)
throw new IllegalStateException();
// 检查并发修改异常
checkForComodification();
try {
// 调用 list的 remove(int index)方法
ArrayList.this.remove(lastRet);
// 修改下一个索引地址的位置
cursor = lastRet;
lastRet = -1;
// 因为出现了列表元素的移除,在list中肯定会修改 modCount,这里就需要更新 expectedModCount的值
expectedModCount = modCount;
// 思考为什么会有可能出现下标越界异常?
// 如果出问题,肯定在 list.this.remove()方法里,即 lastRet越界了
// 但是正常情况下,lastRet勤勤恳恳,我们之前的代码已经保证了 lastRet > 0了
// 在具体调用 remove()方法里面,也会有 rangeCheck(index)
// 所以猜测肯定是其他线程干的,那么就需要抛出 并发修改异常了呀
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// JDK8 流式编程
@Override
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
// Consumer过程必须定义,否则就抛出异常
Objects.requireNonNull(consumer);
final int size = ArrayList.this.size;
int i = cursor;
if (i >= size) {
return;
}
// 获取 elementData[]数组的引用
final Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length) {
throw new ConcurrentModificationException();
}
// 针对每个元素使用 accept()方法,具体的方法实现依据传入的参数 consumer
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[i++]);
}
// update once at end of iteration to reduce heap write traffic
cursor = i;
lastRet = i - 1;
checkForComodification();
}
// 判断是否出现并发修改情况
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
}
private class ListItr extends Itr implements ListIterator<E>
是 AbstractList.ListItr的一个优化版
private class ListItr extends Itr implements ListIterator<E> {
// 有参构造,设置开始迭代的位置
ListItr(int index) {
super();
cursor = index;
}
// 判断当前位置之前有没有元素了
public boolean hasPrevious() {
return cursor != 0;
}
// 获取下一个索引的位置(注意:cursor始终指向下一个索引)
public int nextIndex() {
return cursor;
}
// 前一个索引的位置
public int previousIndex() {
return cursor - 1;
}
// 获取前一个元素
@SuppressWarnings("unchecked")
public E previous() {
// 检查并发修改异常
checkForComodification();
// 设置前一个元素的索引
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[lastRet = i];
}
// 进行迭代时,可以直接对底层列表进行修改
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
// 通过调用 list的 set()方法,直接对 list中的值进行修改
ArrayList.this.set(lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
// 进行迭代时,在list中直接插入元素(注意:该位置之后的元素都会后移一位)
public void add(E e) {
checkForComodification();
try {
int i = cursor;
ArrayList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
// 修改 expectModCount,有种监守自盗的感觉,忽然
expectedModCount = modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
}
private class SubList extends AbstractList<E> implements RandomAccess
ArrayList的切片类
private class SubList extends AbstractList<E> implements RandomAccess {
// 虽然AbstractList是抽象类,但这里只是声明,还没有实例化,所以不会报错
private final AbstractList<E> parent;
private final int parentOffset;
private final int offset;
int size;
// 有参构造,一般 parent就是 ArrayList了,所以可以看作是多态
SubList(AbstractList<E> parent,
int offset, int fromIndex, int toIndex) {
this.parent = parent;
this.parentOffset = fromIndex;
this.offset = offset + fromIndex;
this.size = toIndex - fromIndex;
this.modCount = ArrayList.this.modCount;
}
// 切片类的设置方法,设置具体某一索引位置的值
public E set(int index, E e) {
rangeCheck(index);
checkForComodification();
E oldValue = ArrayList.this.elementData(offset + index);
ArrayList.this.elementData[offset + index] = e;
return oldValue;
}
// 获取某一索引位置的元素
public E get(int index) {
rangeCheck(index);
checkForComodification();
return ArrayList.this.elementData(offset + index);
}
// 获取当前切片类的元素个数
public int size() {
checkForComodification();
return this.size;
}
// 向切片类添加元素
public void add(int index, E e) {
rangeCheckForAdd(index);
checkForComodification();
// 实际上会在 arraylist中添加,并且是在指定索引的位置添加(arraylist中的索引值 = 切片类索引值 + 切片的起始值)
parent.add(parentOffset + index, e);
this.modCount = parent.modCount;
this.size++;
}
// 在切片类中移除元素
public E remove(int index) {
rangeCheck(index);
checkForComodification();
// 实际移除 arraylist中的元素
E result = parent.remove(parentOffset + index);
this.modCount = parent.modCount;
this.size--;
return result;
}
// 范围移除
protected void removeRange(int fromIndex, int toIndex) {
checkForComodification();
parent.removeRange(parentOffset + fromIndex,
parentOffset + toIndex);
this.modCount = parent.modCount;
this.size -= toIndex - fromIndex;
}
// 集合添加
public boolean addAll(Collection<? extends E> c) {
return addAll(this.size, c);
}
// 从指定索引开始集合添加
public boolean addAll(int index, Collection<? extends E> c) {
rangeCheckForAdd(index);
int cSize = c.size();
if (cSize == 0)
return false;
checkForComodification();
parent.addAll(parentOffset + index, c);
this.modCount = parent.modCount;
this.size += cSize;
return true;
}
// 迭代
public Iterator<E> iterator() {
return listIterator();
}
public ListIterator<E> listIterator(final int index) {
checkForComodification();
rangeCheckForAdd(index);
final int offset = this.offset;
// 匿名内部类
return new ListIterator<E>() {
int cursor = index;
int lastRet = -1;
int expectedModCount = ArrayList.this.modCount;
public boolean hasNext() {
return cursor != SubList.this.size;
}
@SuppressWarnings("unchecked")
public E next() {
checkForComodification();
int i = cursor;
if (i >= SubList.this.size)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i + 1;
return (E) elementData[offset + (lastRet = i)];
}
public boolean hasPrevious() {
return cursor != 0;
}
@SuppressWarnings("unchecked")
public E previous() {
checkForComodification();
int i = cursor - 1;
if (i < 0)
throw new NoSuchElementException();
Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length)
throw new ConcurrentModificationException();
cursor = i;
return (E) elementData[offset + (lastRet = i)];
}
@SuppressWarnings("unchecked")
public void forEachRemaining(Consumer<? super E> consumer) {
Objects.requireNonNull(consumer);
final int size = SubList.this.size;
int i = cursor;
if (i >= size) {
return;
}
final Object[] elementData = ArrayList.this.elementData;
if (offset + i >= elementData.length) {
throw new ConcurrentModificationException();
}
while (i != size && modCount == expectedModCount) {
consumer.accept((E) elementData[offset + (i++)]);
}
// update once at end of iteration to reduce heap write traffic
lastRet = cursor = i;
checkForComodification();
}
public int nextIndex() {
return cursor;
}
public int previousIndex() {
return cursor - 1;
}
public void remove() {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
SubList.this.remove(lastRet);
cursor = lastRet;
lastRet = -1;
expectedModCount = ArrayList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void set(E e) {
if (lastRet < 0)
throw new IllegalStateException();
checkForComodification();
try {
ArrayList.this.set(offset + lastRet, e);
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
public void add(E e) {
checkForComodification();
try {
int i = cursor;
SubList.this.add(i, e);
cursor = i + 1;
lastRet = -1;
expectedModCount = ArrayList.this.modCount;
} catch (IndexOutOfBoundsException ex) {
throw new ConcurrentModificationException();
}
}
final void checkForComodification() {
if (expectedModCount != ArrayList.this.modCount)
throw new ConcurrentModificationException();
}
};
}
// 对切片再做切片
public List<E> subList(int fromIndex, int toIndex) {
subListRangeCheck(fromIndex, toIndex, size);
return new SubList(this, offset, fromIndex, toIndex);
}
private void rangeCheck(int index) {
if (index < 0 || index >= this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private void rangeCheckForAdd(int index) {
if (index < 0 || index > this.size)
throw new IndexOutOfBoundsException(outOfBoundsMsg(index));
}
private String outOfBoundsMsg(int index) {
return "Index: " + index + ", Size: " + this.size;
}
private void checkForComodification() {
if (ArrayList.this.modCount != this.modCount)
throw new ConcurrentModificationException();
}
// 搭配 forEachRemaining()使用
public Spliterator<E> spliterator() {
checkForComodification();
return new ArrayListSpliterator<E>(ArrayList.this, offset,
offset + this.size, this.modCount);
}
}
※ static final class ArrayListSpliterator<E> implements Spliterator<E>
实力不够讲不好
static final class ArrayListSpliterator<E> implements Spliterator<E> {
/*
* If ArrayLists were immutable, or structurally immutable (no
* adds, removes, etc), we could implement their spliterators
* with Arrays.spliterator. Instead we detect as much
* interference during traversal as practical without
* sacrificing much performance. We rely primarily on
* modCounts. These are not guaranteed to detect concurrency
* violations, and are sometimes overly conservative about
* within-thread interference, but detect enough problems to
* be worthwhile in practice. To carry this out, we (1) lazily
* initialize fence and expectedModCount until the latest
* point that we need to commit to the state we are checking
* against; thus improving precision. (This doesn't apply to
* SubLists, that create spliterators with current non-lazy
* values). (2) We perform only a single
* ConcurrentModificationException check at the end of forEach
* (the most performance-sensitive method). When using forEach
* (as opposed to iterators), we can normally only detect
* interference after actions, not before. Further
* CME-triggering checks apply to all other possible
* violations of assumptions for example null or too-small
* elementData array given its size(), that could only have
* occurred due to interference. This allows the inner loop
* of forEach to run without any further checks, and
* simplifies lambda-resolution. While this does entail a
* number of checks, note that in the common case of
* list.stream().forEach(a), no checks or other computation
* occur anywhere other than inside forEach itself. The other
* less-often-used methods cannot take advantage of most of
* these streamlinings.
*/
private final ArrayList<E> list;
// 每次 split()或者 advance()时,这个值都会变
private int index; // current index, modified on advance/split
// 栅栏,即index可遍历的上限
private int fence; // -1 until used; then one past last index
private int expectedModCount; // initialized when fence set
/**
* Create new spliterator covering the given range
*/
ArrayListSpliterator(ArrayList<E> list, int origin, int fence,
int expectedModCount) {
this.list = list; // OK if null unless traversed
this.index = origin; // 起始位置
this.fence = fence; // 上限
this.expectedModCount = expectedModCount;
}
// 强制初始化 fence的值
private int getFence() { // initialize fence to size on first use
int hi; // (a specialized variant appears in method forEach)
ArrayList<E> lst;
// 如果 fence < 0的话,表示初始化
if ((hi = fence) < 0) {
// 如果集合为 null,那么 fence也需要为 0
if ((lst = list) == null)
hi = fence = 0;
else {
expectedModCount = lst.modCount;
hi = fence = lst.size;
}
}
return hi;
}
// 进行子划分
public ArrayListSpliterator<E> trySplit() {
// 从当前迭代器的中间元素进行划分
int hi = getFence(), lo = index, mid = (lo + hi) >>> 1;
// 返回从当前 index到 mid的子迭代器
return (lo >= mid) ? null : // divide range in half unless too small
new ArrayListSpliterator<E>(list, lo, index = mid,
expectedModCount);
}
// 针对当前索引指向的值,进行提前处理,Consumer是一个消费者
public boolean tryAdvance(Consumer<? super E> action) {
// 保证消费函数不可为 null
if (action == null)
throw new NullPointerException();
// 获取 fence和 index
int hi = getFence(), i = index;
if (i < hi) {
index = i + 1;
// 获取 i对应的元素
@SuppressWarnings("unchecked") E e = (E) list.elementData[i];
// 消费
action.accept(e);
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
// 为每个对象都进行一次操作
public void forEachRemaining(Consumer<? super E> action) {
// i为下标,hi为fence,mc为预期已改变的大小
int i, hi, mc; // hoist accesses and checks from loop
ArrayList<E> lst;
Object[] a;
if (action == null)
throw new NullPointerException();
if ((lst = list) != null && (a = lst.elementData) != null) {
// 初始化变量
if ((hi = fence) < 0) {
mc = lst.modCount;
hi = lst.size;
} else
mc = expectedModCount;
if ((i = index) >= 0 && (index = hi) <= a.length) {
// 遍历所有元素,对每个元素都进行一次处理
for (; i < hi; ++i) {
@SuppressWarnings("unchecked") E e = (E) a[i];
action.accept(e);
}
if (lst.modCount == mc)
return;
}
}
throw new ConcurrentModificationException();
}
// 计算剩余容量
public long estimateSize() {
// 最大容量 - 已经消费过了的数据
return (long) (getFence() - index);
}
// 返回当前迭代器底层列表的特征值
public int characteristics() {
// 表示是 有序的、定长、子列表也定长的
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}
}
LinkedList
它是双向链表,实现了所有可选的列表操作,各种元素(除了 null)都可以作为结点
这里的索引一般都需要从头开始遍历,并且它是线程不安全的
看完这部分代码之后,对用 Java来写链表题肯定是非常熟悉了的
常见属性
// 链表的长度transient int size = 0;// 指向首元结点的指针transient Node<E> first;// 指向尾结点的指针transient Node<E> last;// 序列化 IDprivate static final long serialVersionUID = 876323262645176354L;
构造方法
// 构造一个空链表public LinkedList(){}// 构造一个链表,其中的元素来自于某一个指定的集合,元素排列顺序由指定集合的迭代顺序决定public LinkedList(Collection<? extends E> c) { this(); addAll(c);}
常见方法
E getFirst()
返回链表中的首元结点中存储的元素
public E getFirst() { // 直接通过实例变量获取首元结点 final Node<E> f = first; // 当该链表为空链表时,调用该方法会报错 if(f == null) { throw new NoSuchElementException(); } // 返回首元结点内的存储的元素信息 return f.item;}
E getLast()
返回列表中最后一个结点存储的元素信息
public E getLast() { final Node<E> l = last; if(l == null) { throw new NoSuchElementException(); } return l.item;}
E removeFirst
移除首元结点并返回其元素
public E removeFirst() { final Node<E> f = first; if(f == null) { throw new NoSuchELementException(); } // 方法定义在下面 return unlinkFirst(f);}private E unlinkFirst(Node<E> f) { // assert f == first && f != null; // 获取首元结点的元素,准备返回 final E element = f.item; // 获取首元结点的下一个结点,该节点即将变成新的首元结点 final Node<E> next = f.next; // 清空原首元结点的元素 f.item = null; // 清空原首元结点 f.next = null; // help GC // 将下一个结点设置成新的首元首元结点 first = next; // 如果下一个结点是 null,说明原来这个链表就只有一个元素,现在移除后就没有多的元素了,那么 last结点也需要设置为 null(其实这个时候移除的既是 first结点,又是 last结点) if(next == null) { last = null; } else { // 正常情况下,因为 linkedList是双向链表,所以需要设置首元结点的前指针为 null, next.prev = null; } size--; // 注意是结构化修改 modCount++; return element;}
E removeLast()
移除最后一个结点并且返回其元素
public E removeLast() { final Node<E> l = last; if(l == null) { throw new NoSuchElementException(); } return unlinkLast(l);}private E unlinkLast(Node<E> l) { // assert l == last && l != null final E element = l.element; final Node<E> prev = l.prev; l.item = null; l.prev = null; // help GC last = prev; if(prev == null) { first = null; } else { prev.next = null; } size--; modCount++; return element;}
void addFirst(E e)
将指定元素插入到链表的头部
public void addFirst(E e) { // 方法定义在下面 linkFirst(e);}private void linkFirst(E e) { // 获取原首元结点 final Node<E> f = first; // 创建一个新结点,其 prev为 null, element为 e, next为 f(即原首元结点) final Node<E> newNode = new Node<E>(null, e, f); // 设置新结点为首元结点 first = newNode; // 如果原首元结点为 null,说明曾经该链表是空的,故需要设置尾结点也是新结点 if(f == null) { last = newNode; } else { // 正常情况下,设置元首元结点的前驱节点为新结点 f.prev = newNode; } size++; modCount++;}
void addLast(E e)
将指定的元素添加到链表的末尾
public void addLast(E e) { linkLast(e);}private void linkLast(E e) { final Node<E> l = last; final Node<E> newNode = new Node<>(l, e, null); last = newNode; if(l == null) { first = newNode; } else { l.next = newNode; } size++; modCount++;}
boolean contains(Object o)
返回结果为对链表内是否包含指定元素的判断
public boolean contains(Object o) { // 判断元素的索引值,如果为 -1则表示不存在,!=判断错误,返回 false;反之,返回 true return indexOf(o) != -1;}public int indexOf(Object o) { // 初始化索引下标 int index = 0; // 针对元素为 null进行遍历 if(o == null) { for(Node<E> x = first; x != null; x = x.next;) { if(x.item == null) { // 找到了,返回索引值 return index; } // 没找到,索引++,找下一个 index++; } } else { for(Node<E> x = first; x != null; x = x.next) { if(o.equals(x.item)) { return index; } index++; } } // 如果全部遍历完了也没找到,就返回 -1 return -1;}
int size()
返回链表中元素的个数
public int size() { // 返回实例中的 size字段 return size;}
boolean add(E e)
将传入的元素作为结点存到链表的尾部
public boolean add(E e) { // 和 addLast()方法一样,就多了一个返回值 linkLast(e); return true;}
boolean remove(Object o)
根据传入元素的值,移除链表中第一个值与之匹配的结点
如果该链表中不包含该元素,则不会做任何改变
public boolean remove(Object o) { if(o == null) { // 从首元结点开始遍历 for(Node<E> x = first; x != null; x = x.next) { if(x.item == null) { // 实际调用的移除方法 unlink(x); return true; } } } else { for(Node<E> x = first; x != null; x = x.next) { if(o.equals(x.item)) { unlink(x); return true; } } } return false;}E unlink(Node<E> x) { // assert x != null; final E element = x.element; // 获取当前结点的前驱和后继结点 final Node<E> next = x.next; final Node<E> prev = x.prev; // 如果前驱节点为空,说明当前结点就是首元结点,则只需要修改下一个结点为首元结点 if(prev == null) { first = next; } else { // 否则修改待删除结点的 前驱节点的后继 为 后继节点的前驱 prev.next = x.next; x.prev = null; } // 同理 if(next == null) { last = prev; } else { next.prev = x.prev; x.next = null; } x.item = null; size--; // 结构化修改 modCount++; return element;}
E remove(int index)
移除链表中处于指定索引位置的结点,并返回其中的元素
public E remove(int index) { // 检查索引的合理性 checkElementIndex(index); // 调用实际从链表中移除某个结点的方法 return unlink(node(index));}
boolean addAll(Collection<? extend E> c)
将参数集合内的所有元素,添加到链表的末尾
添加的顺序依据该集合的迭代顺序
public boolean addAll(Collection<? extend E> c) { // 将待插入的位置传过去,即调用下方的 return addAll(size, c);}
boolean addAll(int index, Collection<? extends E> c)
在指定位置之后插入集合内的元素
public boolean addAll(int index, Collection<? extends E> c) { checkPositionIndex(index); // 将集合转换为数组,方便取用 Object[] a = c.toArray(); // 明确待添加的元素个数 int numNew = a.length; if(numNew == 0) { return false; } // 声明结点,pred为待插入位置的前一个结点;succ是待插入位置的后一个结点 Node<E> pred, succ; // 如果 index == size,则表示要在链表末端追加,此时尾结点即 pred,因为此时没有下一个结点,所以 succ = null if(index == size) { succ = null; pred = last; } else { // 依据所以获取结点,该结点现在的位置就是将来插入的结点的位置,所以该结点会成为新节点的下一个结点 succ = node(index); pred = succ.prev; } // 遍历数组中的元素 for(Object o : a) { // 将元素包裹成结点对象 E e = (E) o; // 此时新构造的结点已经指定了 前驱指针 指向的元素 Node<E> newNode = new Node<>(pred, e, null); // 如果前驱结点为 null,表明是一个空链表,新插入的结点即为首元结点 if(pred == null) { first = newNode; } else { // 否则,就正常插入,设置前驱结点的后继 为新插入结点 pred.next = newNode; } // 指针后移,获取下一个结点 pred = newNode; } // 当结点全部插入之后,最后插入的那个结点和其前驱的关系已经彻底解决,接下来解决和其后继之间的关系 // 如果 succ == null,则表明这是在链表末尾追加 if(succ == null) { // 设置 last结点为最后一个新插入结点 last = pred; } else { // 否则在最后一个新插入的结点和后继之间创建联系 pred.next = succ; succ.prev = pred; } // 更新链表长度 size += numNew; // 结构化修改 modCount++; return true;}// 返回在指定索引处的结点Node<E> node(int index) { // assert isElementIndex(index) // 如果 index小于链表长度的一半,就从左端开始遍历 if(index < (size >>1)) { Node<E> x = first; for(int i = 0; i < index; i++) { x = x.next; } return x; } else { // 否则就从右端开始遍历 Node<E> x = last; for(int i = size - 1; i > index; i--) { x = x.prev; } return x; }}
void clear()
清空链表里的所有元素,之后该链表将会变成一个空链表
public void clear() { // clearing all of the links between nodes is unnecessary // but it helps a generational GC if the discarded nodes inhabit more than one generation // is sure to free memory even if there is a reachable Iterator // 如果结点之间存在跨代引用(一个在新生代,另一个在老年代),那么删去链接能帮助 GC for(Node<E> x = first; x != null; ) { Node<E> next = x.next(); x.item = null; x.prev = null; x.next = null; x = next; } first = last = null; size = 0; modCount++;}
E get(int index)
返回链表中位于指定索引处的结点内存储的元素
public E get(int index) { // 检验索引是否有效 checkElementIndex(index); // 通过索引找到结点,返回结点中存储的元素 return node(index).item;}private void checkElementIndex(int index) { // 如果索引不合理就抛出异常 if(!isElementIndex(index)) { throw new IndexOutOfBoundsException(outOfBoundsMsg(index)); }}private boolean isElementIndex(int index) { return index >= 0 && index < size;}
E set(int index, E element)
在链表的指定索引处用指定的值替换其结点内存储的元素
public E set(int index, E element) { // 检查索引是否合理 checkElementIndex(index); // 通过索引获取结点 Node<E> x = node(index); // 改变结点内存储的元素内容 E oldVal = x.item; x.item = element; return oldVal;}
void add(int index, E element)
在链表的指定位置处插入一个元素
其他结点顺次后移一位
public void add(int index, E element) { // 和 checkElementIndex()的代码完全一样的,都是检查索引的合理性 checkPositionIndex(index); // 判断如果是插到队尾,就使用 linkLast() if(index == size) { linkLast(element); } else { // 获取当前 index的结点,插入到该结点前 linkBefore(element, node(index)); }}// 在指定结点前添加一个结点void linkBefore(E e, Node<E> succ) { // assert succ != null; final Node<E> pred = succ.prev; final Node<E> newNode = new Node<>(pred, e, succ); succ.prev = newNode; // 如果 pred不存在,则表明 succ曾经是 first,现在 newNode在 succ之前,newNode就是 first了 if(pred == null) { first = newNode; } else { pred.next = newNode; } size++; modCount++;}
int lastIndexOf(Object o)
返回链表中值同参数一致的结点最后一次出现的索引位置
public int lastIndexOf(Object o) { int index = size; if (o == null) { for (Node<E> x = last; x != null; x = x.prev) { index--; if (x.item == null) return index; } } else { for (Node<E> x = last; x != null; x = x.prev) { index--; if (o.equals(x.item)) return index; } } return -1;}
E peek()
获取链表的首元结点的元素(队列方法),首元结点不存在时,返回 null
public E peek() { final Node<E> f = first; // 避免直接返回 f.item时的空指针异常 return (f == null) ? null : f.item;}
E element()
获取链表的首元结点的元素(队列方法),首元结点不存在时,报错
public E element() { return getFirst();}
E poll()
移除首元结点并返回其中存储的元素信息,如果链表为空,返回 null
public E poll() { final Node<E> f = first; return(f == null) ? null : unlinkFirst(f);}
E remove()
和上面那个方法差不多,只是如果链表为空,就报错
public E remove() { return removeFirst();}
boolean offer(E e)
在链表的尾部添加元素
public boolean offer(E e) { return add(e);}
boolean offerFirst(E e)
在链队的头部添加元素(双端队列的方法)
public boolean offerFirst(E e) { addFirst(e); return true;}
boolean offerLast(E e)
在链表的尾部添加元素(双端队列的方法)
public boolean offerLast(E e) { addLast(e); return true;}
E peekFirst()
获取但不移除链表首元结点的元素,如果链表为空就返回 null
public E peekFirst() { final Node<E> f = first; return (f == null) ? null : f.item;}
E peekLast()
获取但不移除链表尾结点的元素,如果链表为空就返回 null
public E peekLast() { final Node<E> l = last; return (l == null) ? null : l.item;}
E pollFirst()
获取并且移除链表首元结点的元素,如果链表为空就返回 null
public E pollFirst() { final Node<E> f = first; return (f == null) ? null : unlinkFirst(f);}
E pollLast()
获取并移除链表尾结点的元素,如果链表为空就返回 null
public E pollLast() { final Node<E> l = last; return (l == null) ? null : unlinkLast(l);}
void push(E e)
将一个新的元素压入由链表表示的栈中,其实就是在链表头部插入,只是语义上算作是压栈的方法
public void push(E e) { addFirst(e);}
E pop()
将由链表表示的栈中的栈顶元素弹栈并返回其元素值,其实就是移除首元结点,语义上看坐弹栈
public E pop() { return removeFirst();}
boolean removeFirstOccurence(Object o)
在从首元结点开始遍历链表时,移除指定元素第一次出现的那个结点,如果该结点不存在,则什么都不做
public boolean removeFirstOccurence(Object o) { return remove(o);}
boolean removeLastOccurence(Object o)
在从首元结点开始遍历链表时,移除指定元素最后一个出现的那个结点,如果该结点不存在,则什么都不
public boolean removeLastOccurrence(Object o) { if (o == null) { for (Node<E> x = last; x != null; x = x.prev) { if (x.item == null) { unlink(x); return true; } } } else { for (Node<E> x = last; x != null; x = x.prev) { if (o.equals(x.item)) { unlink(x); return true; } } } return false;}
ListIterator<E> listIterator(int index)
返回从某一个其实索引开始,由链表中元素组成的 列表迭代器
该迭代器是快速失败的,如果迭代时链表发生了不是由本迭代器进行的结构性修改,就会抛出一个 并发修改异常
public ListIterator<E> listIterator(int index) { // 检查索引合理性 checkPositionIndex(index); // 返回由该索引开始的列表迭代器 return new ListItr(index);}
Iterator<E> descendingIterator()
获取相对链表逆序的迭代器,迭代器中的第一个元素就是尾结点
public Iterator<E> descendingIterator() { return new DescendingIterator();}
Object clone()
返回链表的浅拷贝,其中的结点及其元素没有拷贝,引用指向的是同一个内容
public Object clone() { // 先获得一个空壳子 LinkedList<E> clone = superClone(); // Put clone into "virgin" state clone.first = clone.last = null; clone.size = 0; clone.modCount = 0; // Initialize clone with our elements for(Node<E> x = first; x != null; x = x.next) { clone.add(x.item); } return clone;}private LinkedList<E> superClone() { try { return (LinkedList<E>) super.clone(); } catch(CloneNotSupportedException e) { throw new InternalError(e); }}
Object[] toArray()
返回一个包含列表所有元素的数组,并且同链表中一致的顺序排列
public Object[] toArray() { Object[] result = new Object[size]; int i = 0; // 遍历每一个结点,取出后将内部元素赋值给数组元素 for(Node<E> x = first; x != null; x = x.next) { result[i++] = x.item; } return result;}
T[] toArray(T[] a)
同上,返回一个数组,但是类型和 元素类型一致
public <T> T[] toArray(T[] a) { if(a.length < size) { // 通过反射重新创建一个相同类型的数组 a = (T[]) java.lang.reflect.Array.newInstance(a.getClass().getComponentType(), size); } int i = 0; // result和 a指向同一个内存区域 Object[] result = a; for(Node<E> x = first; x != null; x = x.next) { result[i++] = x.item; } // 如果传入的数组的长度很长,就在最后一个元素之后设置一个 null if(a.length > size) { a[size] = null; } return a;}
Spliteratior<E> spliterator()
创建一个定长、有序的并行迭代器
public Spliterator<E> spliterator() { return new LLSpliterator<E>(list, -1, 0);}
常用内部类
private class ListItr implements ListIterator<E>
记得看到过一篇博客,批判了 next()方法里 lastReturned的赋值
// 列表专用的迭代器,运行程序员在任意方向上进行遍历和修改// 它的 cursor指针总是位于调用 previous()返回的元素和 调用 next()返回的元素之间// 所以针对 size为 n的列表,cursor的取值有 n+1个// remove()和 set()方法的对象都不是 cursor,而是上一个由 next()或 previoust()返回的对象private class ListItr implements ListIterator<E> { // 上一个返回的元素 private Node<E> lastReturned; // 下一个将返回的元素 private Node<E> next; //记录结点的索引位置 private int nextIndex; private int expectedModCount = modCount; // 根据索引创建迭代器,将从该索引之后的元素开始创建 ListItr(int index) { // assert isPositionIndex(index); next = (index == size) ? null : node(index); nextIndex = index; } // 判断是否还有下个元素 public boolean hasNext() { return nextIndex < size; } // 获取下一个元素 public E next() { // 检查 modCount checkForComodification(); // 判断有没有下一个元素 if (!hasNext()) throw new NoSuchElementException(); // 获取将返回的元素设置成已经返回的元素 lastReturned = next; // 设置成下一个 next = next.next; nextIndex++; return lastReturned.item; } // 判断是否有前一个元素 public boolean hasPrevious() { return nextIndex > 0; } // 获取前一个元素 public E previous() { checkForComodification(); if (!hasPrevious()) throw new NoSuchElementException(); // 获取带返回元素的上一个结点,但是之后,lastReturned 就等于 next了 lastReturned = next = (next == null) ? last : next.prev; nextIndex--; return lastReturned.item; } public int nextIndex() { return nextIndex; } public int previousIndex() { return nextIndex - 1; } // 移除一个元素 public void remove() { checkForComodification(); if (lastReturned == null) throw new IllegalStateException(); Node<E> lastNext = lastReturned.next; unlink(lastReturned); // 用过 previous()之后,二者就相等了,此时移除了 lastReturned的同时也移除了 next,所以需要对 next重新赋值 if (next == lastReturned) next = lastNext; else nextIndex--; lastReturned = null; expectedModCount++; } // 更新元素的值 public void set(E e) { if (lastReturned == null) throw new IllegalStateException(); checkForComodification(); lastReturned.item = e; } // 在当前迭代位置之后添加元素 public void add(E e) { checkForComodification(); lastReturned = null; // 链表空了 if (next == null) linkLast(e); else // 链表没空,新节点在 next之前 linkBefore(e, next); nextIndex++; expectedModCount++; } // 针对迭代器中的每一个元素都进行一次同一个操作 public void forEachRemaining(Consumer<? super E> action) { Objects.requireNonNull(action); while (modCount == expectedModCount && nextIndex < size) { action.accept(next.item); lastReturned = next; next = next.next; nextIndex++; } checkForComodification(); } final void checkForComodification() { if (modCount != expectedModCount) throw new ConcurrentModificationException(); }}
private static class Node<E>
// 链表的结点
private static class Node<E> {
E item;
Node<E> next;
Node<E> prev;
Node(Node<E> prev, E element, Node<E> next) {
this.item = element;
this.next = next;
this.prev = prev;
}
}
private class DescendingIterator implements Iterator<E>
// 逆序的迭代器
// 把原本的 hasPrevious()变成 hasNext(),把原本的 previous()改成 next()
private class DescendingIterator implements Iterator<E> {
private final ListItr itr = new ListItr(size());
public boolean hasNext() {
return itr.hasPrevious();
}
public E next() {
return itr.previous();
}
public void remove() {
itr.remove();
}
}
static final class LLSpliterator<E> implements Spliterator<E>
static final class LLSpliterator<E> implements Spliterator<E> {
// 批量操作的单元个数,也就是说当前对象划分后至少应当遍历的大小
static final int BATCH_UNIT = 1 << 10; // batch array size increment
// 最大单元的大小
static final int MAX_BATCH = 1 << 25; // max batch array size;
// 当前集合
final LinkedList<E> list; // null OK unless traversed
// 当前结点
Node<E> current; // current node; null until initialized
// 预估规模大小
int est; // size estimate; -1 until first needed
int expectedModCount; // initialized when est set
// 已遍历的大小
int batch; // batch size for splits
// 构造函数
LLSpliterator(LinkedList<E> list, int est, int expectedModCount) {
this.list = list;
this.est = est;
this.expectedModCount = expectedModCount;
}
final int getEst() {
int s; // force initialization
final LinkedList<E> lst;
// 如果还没有初始化,此时 est = -1
if ((s = est) < 0) {
if ((lst = list) == null)
s = est = 0;
else {
expectedModCount = lst.modCount;
// current结点直接等于 LinkedList的 first属性,也就是说之后是从链表的头部开始遍历的
current = lst.first;
s = est = lst.size;
}
}
return s;
}
// 获得预估大小
public long estimateSize() {
return (long) getEst();
}
// 子划分遍历
public Spliterator<E> trySplit() {
Node<E> p;
// 初始化 est
int s = getEst();
if (s > 1 && (p = current) != null) {
int n = batch + BATCH_UNIT;
// 如果 n超过了集合大小,就取集合最大值
if (n > s)
n = s;
// 如果 n超过了上限,就取上限
if (n > MAX_BATCH)
n = MAX_BATCH;
// 将链表中的元素放到数组中
Object[] a = new Object[n];
int j = 0;
do {
a[j++] = p.item;
} while ((p = p.next) != null && j < n);
// 当前结点等于遍历后的下一个结点
current = p;
// batch等于子遍历的大小
batch = j;
// 剩余估计大小需要减去已分配的值
est = s - j;
// 返回一个子对象,内部本质还是基于数组的
return Spliterators.spliterator(a, 0, j, Spliterator.ORDERED);
}
return null;
}
// 对每一个对象进行处理
public void forEachRemaining(Consumer<? super E> action) {
Node<E> p;
int n;
if (action == null)
throw new NullPointerException();
// 初始化 est
if ((n = getEst()) > 0 && (p = current) != null) {
current = null;
est = 0;
// 从头开始遍历
do {
E e = p.item;
p = p.next;
action.accept(e);
} while (p != null && --n > 0);
}
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
}
// 尝试预先处理
public boolean tryAdvance(Consumer<? super E> action) {
Node<E> p;
if (action == null)
throw new NullPointerException();
// 初始化 est,每消费一次,est的预估大小要减一
if (getEst() > 0 && (p = current) != null) {
--est;
E e = p.item;
// 消费完毕后,current结点就是下一个结点了,一觉醒来又是美好的一天
current = p.next;
action.accept(e);
if (list.modCount != expectedModCount)
throw new ConcurrentModificationException();
return true;
}
return false;
}
// 表示该迭代器是有序、定长、子类也定长的
public int characteristics() {
return Spliterator.ORDERED | Spliterator.SIZED | Spliterator.SUBSIZED;
}
}
HashMap
HashMap提供了 Map中的基本所有的可操作的选项,并且允许 null作为键和值(Hashtable不允许)
HashMap和 Hashtable基本一样,除了没有使用 synchrnized
关键字重复而已,并且它也不保证元素的顺序
该实现针对 get()和 put()方法提供了 基本恒定的时间性能,当然前提是 哈希函数正常的将各个元素分散在多个 哈希桶(buckets)中;所以实际上对 HashMap进行迭代时的消耗与 容量(capacity)、桶的数量(num of buckets)、数量(amount of key-value mappings)有关;所以,在 HashMap初始化时,不建议一下子把容量设置得很大
一个 HashMap实例的性能主要跟 初始容量(initial capacity)、扩容因子(load factor)有关;初始容量默认为 16,扩容因子默认为 0.75(较高的扩容因子虽然能够降低空间开销,但会提高查询消耗)
如果一开始就要装很多键值对,那么创建 HashMap实例时,就可以声明大容量,这样相比自动扩容能有更高的效率。使用大量 hashCode值相同的元素作为键也会降低效率
因为存在哈希碰撞,HashMap默认的方式是在桶下面维护一个链表,存储 hashCode值相同的元素,但是当桶维护的元素过多之后。链表就会转换为 二叉树。大部分情况下都会使用普通的桶结构,但是适时地使用 TreeNode结构,可以提供更快的查找。但是,当元素数量不多时,遍历检查树结点的延迟就会暴露,此时会恢复到原来的链表结构
常见属性
// 序列化 ID
private static final long serialVersionUID = 362498820763181265L;
// 默认的初始容量
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;
// HashMap的最大容量
static final int MAXIMUN_CAPACITY = 1 << 30;
// 默认的扩容因子大小,也就是说默认情况下,当容量达到 12,即将要存第 13个元素时会发生扩容
static final float DEFAULT_LOAD_FACTOR = 0.75f;
// 元素组织形式由 链表转换到树的 一个桶中元素数量的阈值
static final int TREEIFY_THRESHOLD = 8;
// 元素组织形式由 树转换回链表的 一个桶中元素数量的阈值
static final int UNTREEIFY_THRESHOLD = 6;
// 元素组织形式由 链表转换到树的 整个数组中元素数量的阈值;也就是说必须达到 数组容量 > 64 && 某个桶中元素数量 > 8才会发生树化
static final int MIN_THREEIFY_CAPACITY = 64;
// 第一次使用时才初始化,可在需要的时候调整大小的数组,并且其容量总是 2的 n次幂(初始时长度也可以指定位 0)
transient Node<K, V>[] table;
// 保留的 entrySet()方法的缓存
transient Set<Map.Entry<K, V>> entrySet;
// 键值对在 map中的数量
transient int size;
// 并发修改控制字段
transient int modCount;
// 进行扩容的阈值(capacity * load factor),也就是说如果当前 map存储的 k-v键值对的数量超过了 threshold,就会进行扩容
int threshold;
// 该 map的实际扩容因子
final float loadFactor;
构造方法
public HashMap(int initCapacity, float loadFactor)
构造一个指定大小和扩容因子的空哈希表
public HashMap(int initCapacity, float loadFactor) {
// 判断指定容量是否合理
if(initCapacity < 0) {
throw new IllegalArgumentException("Illegal initial capacity: " + initialCapacity);
}
if(initCapacity > MAXIMUM_CAPACITY) {
initCapacity = MAXIMUM_CAPACITY;
}
// 判断扩容因子是否合理
if(loadFactor <= 0 || Float.isNaN(loadFactor)) {
throw new IllegalArgumentException("Illegal load factor: " + loadFactor);
}
this.loadFactor = loadFactor;
// 设置阈值
this.threshold = tableSizeFor(initCapacity);
}
// 刚开始先返回分配的数组容量的大小,具体的效果是针对给定的数字返回最小二次幂, >>> 是无符号右移
static final int tableSizeFor(int cap) {
// 如果传进来的数已经是二次幂的形式了,就不会进行如下变换
int n = cap - 1;
// 右移1位再与移动前数字逐位异或,可以保证最高位和次高位均为1,结果中前2位均为1(如果最高位所在位数>1)
n |= n >>> 1;
// 右移2位再与移动前数字逐位异或,可以保证高2位和次高2位均为1,结果中前4位均为1(如果最高位所在位数>4,否则最高位以下全1)
n |= n >>> 2;
// 右移4位再与移动前数字逐位异或,可以保证高4位和次高4位均为1,结果中前8位均为1(如果最高位所在位数>8,否则最高位以下全1);
n |= n >>> 4;
// 右移8位再与移动前数字逐位异或,可以保证高8位和次高8位均为1,结果中前16位均为1(如果最高位所在位数>16,否则最高位以下全1);
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
public HashMap(int initialCapacity)
创建默认扩容因子为 0.75,但大小指定的 map
public HashMap(int initialCapacity) { this(initialCapacity, DEFAULT_LOAD_FACTOR);}
public HashMap()
创建所有值都是默认的 map,初始容量为 16,加载因子为 0.75
public HashMap() { this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted}
public HashMap(Map<? extends K, ? extends V> m)
创建一个新的 map,内部的键值对和传参的 map内的一致;
新的 map的基本值都是默认大小的
public HashMap(Map<? extends K, ? extends V> m) { this.loadFactor = DEFAULT_LOAD_FACTOR; // 将原 map中的所有 键值对都拷贝到当前 map中 putMapEntries(m, false);}final void pubMapEntries(Map<? extends K, ? extends V> m, boolean evict) { // 获取传入的 map中键值对的个数 int s = m.size(); if(s > 0) { // 如果 table为 null,则表明是通过构造函数来调用的 本方法,或者 map构造后但添加值 if(table == null) { // 先计算出一个数,使得其值刚好不大于阈值,但这样可能会计算出小数,所以向上取值,并加一,保证比 threshold的值大 float ft = ((float) s / loadFactor) + 1.0F; // 如果值小于最大容量,就取该值,否则取最大值 int t = ((ft < (float) MAXIMUM_CAPACITY) ? (int) ft : MAXIM_CAPACITY); // 把用 t计算出来的新的容量值,赋值给 threshold(这里的 threshold值实际存放的是 capacity的值,因为 table还没有初始化,用户给定的 capacity会暂存到 threshold,capacity是作为 table数组的大小隐式存在的) if(t > threshold) { threshold = tableSizeFor(t); } // 说明 table已经初始化过了;判断传入的 map的 size是否大于当前 map的 threshold,如果是就要预先扩大容量 } else if(s > threshold) { resize(); } for(Map.Entry<? extends K, ? extends V> e : m.entrySet()) { K key = e.getKey(); V value = e.getValue(); putVal(hash(key), key, value, false, evict); } }}
常见方法
final int hash(Object key)
计算键的哈希值,并且用高位和低位进行异或处理,好处是能够散列得更加均匀(一批Float类型的数据将会在一个小数据量的散列表中造成持续碰撞)
static final int hash(Object key) { int h; // 让键的原始 hashCode的低 16位与其高 16位进行异或操作 // 混合原始哈希码的高位和低位,以此来加大低位的随机性 // 而且混合后的低位掺杂了高位的部分特征,这样高位的信息也被变相的保留了下来 return (key == null) ? 0 : (h = key.hashCode()) ^ (h >> 16);}
int size()
返回 map中键值对的数量
public int size() { return size;}
boolean isEmpty()
当 map中没有键值对时,返回 true
public boolean isEmpty() { return size == 0;}
V get(Object key)
返回与键匹配的值,如果 map中不存在该键,就返回 null
注意:返回 null,并不一定就表示不存在该键值对, 因为 hashMap是允许 值为 null的,所以可能存在如下场景:键为 xxx,值为 null,get(xxx)时也会返回 null;所以不能通过 get()的值是否为 null来判断键是否存在
public V get(Object key) {
Node<K, V> e;
// 根据键的哈希值和键本身寻找在桶中存着的 结点
return (e = getNode(hash(key), key)) == null ? null : e.value;
}
final Node<K, V> getNode(int hash, Object key) {
// tab即 table
Node<K, V> tab;
// first为具体某个桶内的第一个结点,e为逐个遍历的结点
Node<K, V> first, e;
int n;
K k;
// (n-1) & hash获取 哈希桶的索引,first为该桶中的第一个结点
if((tab = table) != null && (n = tab.length) > 0 && (first = tab[(n - 1) & hash]) != null) {
// 判断第一个结点的值是否符合要求
if(first.hash == hash && ((k = first.key) == key || (key != null && key.equals(k)))) {
return first;
}
// 如果不符合,继续判断是否符合 树结点的形式
if((e = first.next) != null) {
// 按照 遍历树的方式去查找关键字
if(first instanceof TreeNode) {
return ((TreeNode<K, V>) first).getTreeNode(hash, key);
}
// 否则以遍历链表的方式查找关键字
do {
if(e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
return e;
}
} while((e = e.next) != null);
}
}
// 啥也没找到,就返回 null
return null;
}
// 尝试获取树结点
final TreeNode<K, V> getTreeNode(int h, Object k) {
// 从根节点开始找(根节点的 parent肯定是 null)
return ((parent != null) ? root() : this).find(h, k, null);
}
// 获取根结点
final TreeNode<K, V> root() {
for(TreeNode<K, V> r = this, p; ; ) {
if((p = r.parent) == null) {
return r;
}
r = p;
}
}
// 寻找从根结点开始,符合 hash和 key的匹配的结点,kc缓存了初次进行键的比较时 comparableClassFor(key)的值
final TreeNode<K, V> find(int h, Object k, Class<?> kc) {
// p表示当前结点
TreeNode<K, V> p = this;
do {
// 定义当前的哈希值
int ph, dir;
K pk;
// 获取当前结点的左右孩子,q用来存储并返回找到的结点
TreeNode<K, V> pl = p.left, pr = p.right, q;
// 如果给定值小于当前结点的哈希值,进入左结点
if((ph = p.hash) > h) {
p = pl;
// 如果大于,进入右结点
} else if(ph < h) {
p = pr;
// 如果哈希值相等,且键值相等,则返回当前结点
} else if((pk = p.key) == k || (k != null && k.equals(pk))) {
return p;
// 如果到了这儿,则表示以上的所有 if()条件都没有满足,但是考虑到 针对 hash的范围判断已经全部过了一遍
// 所以,肯定是前一个 else if()的第一个条件不满足,即 hash相同,但是 键名不同
// 如果左节点为空,进入右结点
} else if(pl == null) {
p = pr;
// 如果右结点为空,进入做左节点
} else if(pr == null) {
p = pl;
// 如果此时左右孩子都不为空,就不知道往哪里去了
// 这里需要通过 compare()方法 来比较 pk和 k的大小,来决定下一步往哪里继续遍历
} else if((kc != null || (kc = comparableClassFor(k)) != null)
&& (dir = compareComparables(kc, k, pk)) != 0) {
p = (dir < 0) ? pl : pr;
// 执行到这里说明无法通过 comparable进行鉴别
// 则在右孩子处递归查找,如果找到了匹配的就返回
} else if((q = pr.find(h, k, kc)) != null) {
return q;
// 进行到这里说明,右孩子处的递归没找到,那么就从左孩子开始,进行下一轮的循环
// 进入左节点
} else {
p = pl;
}
} while(p != null);
// 没找到
return null;
}
// 如果 x是 Comparable接口的实现类就返回它的 Class,否则返回 null
static Class<?> comparableClassFor(Object x) {
if(x instanceof Comparable) {
Class<?> c;
// 反射包下的东西
Type[] ts, as;
Type t;
// 参数化类型,是 Type的子接口
ParameterizedType p;
// 如果是字符串就可以直接返回,因为 String已经实现了 Comparable<String>
if((c = x.getClass()) == String.class) // bypass checks
return c;
// 如果存在实现的接口,就把接口传给 ts,包括参数化类型
if((ts = c.getGenericInterfaces()) != null) {
// 遍历每一个接口
for(int i = 0; i < ts.length; ++i) {
//
if(((t = ts[i]) instanceof ParameterizedType) && // 表示是一个参数化类型
((p = (ParameterizedType) t).getRawType() == Comparable.class) && //表示该接口为 Comparable
(as = p.getActualTypeArguments()) != null && // 实际类型参数的数组不为空
as.length ==1 && as[0] == c) // 长度为 1,且参数为 c
return c;
}
}
}
// 没找到就返回 null
return null;
}
// 当 x 匹配 kc时,返回 k.compareTo(x)
static int compareComparables(Class<?> kc, Object k, Object x) {
return (x == null || x.getClass() != kc ? 0 : ((Comparable) k).compareTo(x));
}
boolean containsKey(Object key)
当 map中包含了指定的 key时,返回 true
public boolean containsKey(Object key) {
// 如果能根据键获取对应的结点,就表示存在该键
return getNode(hash(key), key) != null;
}
※V put(K key, V value) / 比较难的
将指定的键和指定的值连接在一起;
如果该键已经存在了,旧的值会被替换并返回;如果键是新的,就会返回 null
public V put(K key, V value) {
// 调用实际添加值的方法
return putVal(hash(key), key, value, false, true);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
// 声明局部变量,tab即 table,p为结点,n为 table长度
Node<K, V>[] tab;
Node<K, V> p;
int n, i;
// 判断当前的哈希表是不是空的,或者长度是否为 0
if ((tab = table) == null || (n = tab.length) == 0) {
// 如果是的话,就表示现在还没有哈希表,所以需要创建新的哈希表,默认就是创建一个长度为 16的哈希表
n = (tab = resize()).length;
}
// 将当前哈希表中与要插入的数据位置对应的数据取出来,(n-1) & hash就是通过传入 hash值计算得到的数组下标
if ((p = tab[i = (n - 1) & hash]) == null) {
// 如果数组中该位置为 null,则表明这个结点是该桶中的第一个结点,直接把它放在那里就行了
tab[i] = newNode(hash, key, value, null);
} else {
// 否则,就表明这个桶里面是有内容的,就要进行遍历了
Node<K, V> e;
K k;
// 如果当前位置上的那个数据的哈希值和我们要插入的哈希值是一样的,代表没有放错位置
// 如果在上述前提下,连key都是一样,那么就要替换掉这个值了
// 此时,p为该桶中的第一个结点
if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k)))) {
e = p;
} else if(p instanceof TreeNode) {
//否则如果当前结点的 key和要插入的 key不同,就要判断当前结点是不是一个红黑树结点
// 也就是说,它现在是链表状态还是红黑树状态
// 如果是红黑树状态,那就调用红黑树相关的方法,具体定义在下面
e = ((TreeNode<K, V>) p).putTreeVal(this, tab, hash, key, value);
} else {
// 否则,就表示它还只是一个链表,那就遍历链表
for (int binCount = 0; ; ++binCount) {
// 如果当前结点是链表中的最后一个结点
if ((e = p.next) == null) {
// 那就把待插入结点放到它的后面
p.next = newNode(hash, key, value, null);
// 判断放入后,是否需要树化
if (binCount >= TREEIFY_THRESHOLD - 1) {
// 链表变成红黑树
treeifyBin(tab, hash);
}
// 退出
break;
}
// 如果不仅 哈希值相等,key也是一样的,那么就要进行值的替换了
if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))) {
break;
}
// 取下一个结点
p = e;
}
}
// 如果当前结点不等于空,则表示要进行值的替换
// 因为如果是正常插入的,那么 e应该是等于 p.next的,而在那个时候 p.next应该是等于 null的,所以 e正常插入时应该为 null
if(e != null) {
// 取旧的值
V oldValue = e.value;
// 赋值新的值
if (!onlyIfAbsent || oldValue == null) {
e.value = value;
}
afterNodeAccess(e); // 用于 LinkedHashMap
return oldValue;
}
}
// 增加长度
++modCount;
// 判断是否需要扩容
if (++size > threshold) {
resize();
}
afterNodeInsertion(evict);
return null;
}
// 创建一个常规的链表结点
Node<K, V> newNode(int hash, K key, V value, Node<K, V> next) {
return new Node<>(hash, key, value, next);
}
// 插入一个树结点,如果结果是覆盖原结点,就返回原结点;如果结果是完全新建了一个,就返回 null
final TreeNode<K, V> putTreeVal(HashMap<K, V> map,
Node<K, V>[] tab, int h, K k, V v) {
// kc用于存储 K的 Class
Class<?> kc = null;
// 标记是否已经遍历过一次树了
boolean searched = false;
// 确定整棵树的根节点
TreeNode<K, V> root = (parent != null) ? root() : this;
// 从根节点开始遍历所有结点
for (TreeNode<K, V> p = root; ; ) {
// dir为向左或向右的方向,ph为当前结点的 hash值
int dir, ph;
K pk;
// 如果当前结点的哈希值大于待插入结点的哈希值
if ((ph = p.hash) > h)
// 那么待插入的结点就应该在 红黑树的左侧
dir = -1;
// 反之,则在右侧
else if (ph < h)
dir = 1;
// 如果当前结点的键也相同,那么就返回当前结点对象,在之前的 putVal()方法中会进行值的替换的
else if ((pk = p.key) == k || (k != null && k.equals(pk)))
return p;
// 类似 find()方法里,到这之后,表示两个结点哈希值相等但是键名不同
else if ((kc == null && (kc = comparableClassFor(k)) == null) || (dir = compareComparables(kc, k, pk)) == 0) {
// 如果没有实现 comparable或者 将 k和 pk比较后相等(其实就是 &&后的条件)
if (!searched) {
// 如果还没搜索过
TreeNode<K, V> q, ch;
// 标记已经遍历过一次了
searched = true;
// 递归搜索,先从左边开始找,如果左边没找到,就从右边开始找
if (((ch = p.left) != null && (q = ch.find(h, k, kc)) != null) || ((ch = p.right) != null && (q = ch.find(h, k, kc)) != null))
return q;
}
// 至此, 遍历了所有子节点也没能找到 同名的键,说明之后需要进行插入
dir = tieBreakOrder(k, pk);
}
// xp指向当前结点
TreeNode<K, V> xp = p;
// 如果 dir<0,并且左子树为空,那么待插入元素就变成左结点;如果 dir>0并且右子树为空,那么待插入元素就变成右结点
if ((p = (dir <= 0) ? p.left : p.right) == null) {
Node<K, V> xpn = xp.next;
TreeNode<K, V> x = map.newTreeNode(h, k, v, xpn);
// 左孩子指向新的树结点
if (dir <= 0)
xp.left = x;
// 右孩子指向新的树结点
else
xp.right = x;
// 作为链表结点的 next指向新的树结点
xp.next = x;
// 设置父结点
x.parent = x.prev = xp;
if (xpn != null)
((TreeNode<K, V>) xpn).prev = x;
// 重新平衡
moveRootToFront(tab, balanceInsertion(root, x));
return null;
}
}
}
// 比较两个对象,返回值要么大于 0,要么小于 0;可用于确定要插入的结点是树的左节点还是右结点
// 该方法用于在无法使用 equal和 hashCode比较的时候,打破局面强行确定插入顺序
static int tieBreakOrder(Object a, Object b) {
int d;
if (a == null || b == null || (d = a.getClass().getName().compareTo(b.getClass().getName())) == 0) {
d = (System.identityHashCode(a) <= System.identityHashCode(b) ? -1 : 1);
}
return d;
}
// 将红黑树的根节点设置为哈希桶中的第一个元素
// TreeNode既是红黑树结点,同时也是链表结点
static <K, V> void moveRootToFront(Node<K, V>[] tab, TreeNode<K, V> root) {
int n;
// 根节点不为空,并且 hashMap的元素数组不为空
if (root != null && tab != null && (n = tab.length) > 0) {
// 根据哈希值和数组长度定位到桶的位置
int index = (n - 1) & root.hash;
// 先取得桶中的第一个对象
TreeNode<K, V> first = (TreeNode<K, V>) tab[index];
// 如果红黑树的根不同于桶中的第一个对象,就说明需要进行重新设置的操作
if (root != first) {
// 获取根对象的后一个对象
Node<K, V> rn;
// 先直接替换
tab[index] = root;
// 获取根对象的前一个对象
TreeNode<K, V> rp = root.prev;
// 如果后一个对象不为空,那么就设置 它的前一个对象为 root的前一个对象;即将 root从链表中移除
if ((rn = root.next) != null)
((TreeNode<K, V>) rn).prev = rp;
// 如果 root的前结点不为空,就设置它的后结点为 root的后一个对象
if (rp != null)
rp.next = rn;
// 如果该桶中的第一个元素不为空,则将root设置为它的前结点
if (first != null)
first.prev = root;
// 原来的第一个结点现在作为 root的下一个结点
root.next = first;
// 首结点没有前结点
root.prev = null;
}
// 校验是否满足红黑树的特性
assert checkInvariants(root);
}
}
// 在向红黑树中插入结点后,重新平衡
static <K, V> TreeNode<K, V> balanceInsertion(TreeNode<K, V> root,
TreeNode<K, V> x) {
// 将新插入的结点标记为红色
x.red = true;
// 开启循环
// xp:父节点;xxp:爷爷结点;xppl:左叔叔结点;xppr:右叔叔结点
for (TreeNode<K, V> xp, xpp, xppl, xppr; ; ) {
// 如果父节点为空,则表示当前结点为根节点,根节点需要为黑色
if ((xp = x.parent) == null) {
x.red = false;
return x;
// 如果父节点不为空
// 如果父节点是黑色的,或者父节点是红色但爷爷结点不存在
} else if (!xp.red || (xpp = xp.parent) == null)
return root;
// 如果父节点是爷爷结点的左孩子
if (xp == (xppl = xpp.left)) {
// 如果右叔叔不为空且是红色的
if ((xppr = xpp.right) != null && xppr.red) {
// 以下进行变色操作
// 右叔叔变黑
xppr.red = false;
// 爸爸变黑
xp.red = false;
// 爷爷变红
xpp.red = true;
// 从爷爷结点开始,再进行下一轮的遍历
x = xpp;
// 否则,如果叔叔结点不为空,或者是黑色的
} else {
// 如果当前结点是父节点的右孩子
if (x == xp.right) {
// 父节点左旋
root = rotateLeft(root, x = xp);
// 重新获取爷爷结点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 如果父节点不为空
if (xp != null) {
// 爸爸变黑
xp.red = false;
// 如果爷爷结点不为空
if (xpp != null) {
// 爷爷变红
xpp.red = true;
// 爷爷结点右旋
root = rotateRight(root, xpp);
}
}
}
// 如果父节点是爷爷结点的右孩子
} else {
// 如果左叔叔存在且为红色
if (xppl != null && xppl.red) {
// 则进行变色操作
// 左叔叔变黑
xppl.red = false;
// 爸爸变黑
xp.red = false;
// 爷爷变红
xpp.red = true;
// 同样的,从爷爷结点开始向上进行下一轮的循环
x = xpp;
// 否则,如果左叔叔不存在 或者 为黑色的
} else {
// 如果当前结点是左孩子
if (x == xp.left) {
// 父节点右旋
root = rotateRight(root, x = xp);
// 找到新的爷爷结点
xpp = (xp = x.parent) == null ? null : xp.parent;
}
// 如果父节点不为空
if (xp != null) {
// 爸爸变黑
xp.red = false;
// 如果爷爷结点不为空
if (xpp != null) {
// 爷爷变红
xpp.red = true;
// 爷爷结点左旋
root = rotateLeft(root, xpp);
}
}
}
}
}
}
// 红黑树结点左旋,p为待左旋的结点
static <K, V> TreeNode<K, V> rotateLeft(TreeNode<K, V> root,
TreeNode<K, V> p) {
TreeNode<K, V> r, pp, rl;
// 如果待旋结点及其右孩子非空
if (p != null && (r = p.right) != null) {
// 如果待旋结点的右孩子的左孩子非空,其中的赋值语句表示:待旋结点的右孩子的左孩子 赋值给 待旋结点的右孩子(孙子变儿子)
if ((rl = p.right = r.left) != null)
// 孙子也答应变成了我的儿子
rl.parent = p;
// 待旋结点的父节点赋值给待旋结点的右孩子结点的父节点(儿子变成我)
// 如果此时父节点已经为空了,说明 r已经是顶层结点,并且标记为 黑色
if ((pp = r.parent = p.parent) == null)
(root = r).red = false;
// 否则,如果父节点不为空且 待旋结点是其父节点的左儿子
else if (pp.left == p)
// 爹答应收孙子变左儿子
pp.left = r;
// 否则,孙子变成右儿子
else
pp.right = r;
// 待左旋结点成为了右儿子的左儿子
r.left = p;
p.parent = r;
}
// 返回根节点
return root;
}
// 红黑树结点右旋,类似左旋
static <K, V> TreeNode<K, V> rotateRight(TreeNode<K, V> root,
TreeNode<K, V> p) {
TreeNode<K, V> l, pp, lr;
if (p != null && (l = p.left) != null) {
if ((lr = p.left = l.right) != null)
lr.parent = p;
if ((pp = l.parent = p.parent) == null)
(root = l).red = false;
else if (pp.right == p)
pp.right = l;
else
pp.left = l;
l.right = p;
p.parent = l;
}
return root;
}
// 检查此时的红黑树是否符合红黑树的性值,一般都是从根节点出发,也就是说,t传参时一般都是 Root
static <K, V> boolean checkInvariants(TreeNode<K, V> t) {
TreeNode<K, V> tp = t.parent, tl = t.left, tr = t.right,
tb = t.prev, tn = (TreeNode<K, V>) t.next;
// t的前驱结点不为空 并且 前驱的后继不是 t
if (tb != null && tb.next != t)
return false;
// t的后继不为空 并且 后继的前驱不是 t
if (tn != null && tn.prev != t)
return false;
// t的父节点不为空 并且 父节点的左右子节点都不是 t
if (tp != null && t != tp.left && t != tp.right)
return false;
// t的左孩子非空 并且 左孩子的父节点不是 t 或者 左孩子的 hash值大于t的hash值
if (tl != null && (tl.parent != t || tl.hash > t.hash))
return false;
// t的右孩子的判断
if (tr != null && (tr.parent != t || tr.hash < t.hash))
return false;
// t是红色的 并且 左孩子也是红色的 并且 右孩子也是红色的
if (t.red && tl != null && tl.red && tr != null && tr.red)
return false;
// 左孩子非空,递归检查左孩子
if (tl != null && !checkInvariants(tl))
return false;
// 右孩子非空,递归检查右孩子
if (tr != null && !checkInvariants(tr))
return false;
return true;
}
// 在哈希表的指定桶中,修改元素的排布,将其转换为红黑树的形态
final void treeifyBin(Node<K, V>[] tab, int hash) {
int n, index;
Node<K, V> e;
// 如果 table为空或者未初始化,或者实际整个数组的容量未达到 64,那么就算单个链表长度达到了 8,也不会树化,会先扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
// 否则就进行树化操作,先获取当前桶中的第一个元素
else if ((e = tab[index = (n - 1) & hash]) != null) {
// hd为头结点,tl为尾结点
TreeNode<K, V> hd = null, tl = null;
do {
// 将该元素的对应链表结点,设置为树结点形式
TreeNode<K, V> p = replacementTreeNode(e, null);
// 尾结点为空,表示没有红黑树结构
if (tl == null)
// 直接将 p赋值为红黑树的头结点
hd = p;
else {
// 把 p放到 tl的后面
p.prev = tl;
tl.next = p;
}
// tl变成p,重新变回最后
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
// 真正进行树化操作
hd.treeify(tab);
}
}
// 真正的将结点组织成树化形态的函数
final void treeify(Node<K,V>[] tab) {
TreeNode<K,V> root = null;
// 将调用此方法的结点赋值给x,遍历双向的 TreeNode链表
for (TreeNode<K,V> x = this, next; x != null; x = next) {
next = (TreeNode<K,V>)x.next;
x.left = x.right = null;
// 如果 root结点为空,就将 x结点设置为根节点
if (root == null) {
x.parent = null;
// 根节点的颜色是黑色
x.red = false;
root = x;
}
// 否则,root结点不为空,直接将 x插入到树中即可
else {
K k = x.key;
int h = x.hash;
Class<?> kc = null;
// 从根节点开始遍历,找到插入新节点的位置
for (TreeNode<K,V> p = root;;) {
int dir, ph;
K pk = p.key;
// 确认新节点添加到树的左边还是右边
if ((ph = p.hash) > h)
dir = -1; // -1表示左边,1表示右边
else if (ph < h)
dir = 1;
// 如果两个结点的哈希值相等,就使用 compareTo方法比较 key
else if ((kc == null &&
(kc = comparableClassFor(k)) == null) ||
(dir = compareComparables(kc, k, pk)) == 0)
dir = tieBreakOrder(k, pk);
// xp为 x的父节点
TreeNode<K,V> xp = p;
// dir 小于 0就向左边查找,否则向右边查找,如果为 null,则表示该位置就是 x的目标位置
if ((p = (dir <= 0) ? p.left : p.right) == null) {
// x和 xp结点的属性设置
x.parent = xp; // x的父节点即为最后一次遍历的结点
if (dir <= 0)
xp.left = x;
else
xp.right = x;
// 进行红黑树的插入平衡
root = balanceInsertion(root, x);
break;
}
}
}
}
// 把根节点作为桶中的第一个结点
moveRootToFront(tab, root);
}
// 调整 hashMap的大小
final Node<K, V>[] resize() {
Node<K, V>[] oldTab = table;
// 获取老表的长度
int oldCap = (oldTab == null) ? 0 : oldTab.length;
// 获取老表的阈值
int oldThr = threshold;
int newCap, newThr = 0;
// 老表的长度大于 0,则表示老表不空
if (oldCap > 0) {
// 判断老表的长度是否超过了最大容量值
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
// 将 newCap设置为 oldCap的两倍,如果 newCap < 最大容量 并且 oldCap 大于默认初始值
// 就将阈值设置为原来的两倍
} else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
newThr = oldThr << 1; // double threshold
// 如果老表的长度为 0,但是阈值大于 0,则将新表的初始容量设置为老表的初始容量
} else if (oldThr > 0) // initial capacity was placed in threshold
newCap = oldThr;
// 否则如果 老表的长度和阈值都为 0,则表示这是一个默认空参构造创建的表,就把阈值和容量设置为默认值
else { // zero initial threshold signifies using defaults
newCap = DEFAULT_INITIAL_CAPACITY;
newThr = (int) (DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
}
// 如果新表的阈值为空,则通过 新表容量 * 负载因子 获得新的阈值
if (newThr == 0) {
float ft = (float) newCap * loadFactor;
newThr = (newCap < MAXIMUM_CAPACITY && ft < (float) MAXIMUM_CAPACITY ?
(int) ft : Integer.MAX_VALUE);
}
threshold = newThr;
@SuppressWarnings({"rawtypes", "unchecked"})
Node<K, V>[] newTab = (Node<K, V>[]) new Node[newCap];
table = newTab;
// 如果老表不为空,则需遍历所有结点,将结点赋值给新表
if (oldTab != null) {
// 逐个桶进行遍历
for (int j = 0; j < oldCap; ++j) {
Node<K, V> e;
// 获取索引值为 j的桶中的第一个结点
if ((e = oldTab[j]) != null) {
// 将老表结点的引用置空
oldTab[j] = null;
// 如果 e.next为空,则表示该桶中只有一个元素,即 e是桶中的唯一的元素
if (e.next == null)
//那么就直接计算它在新表中的桶位置,放入即可
newTab[e.hash & (newCap - 1)] = e;
// 否则如果它是红黑树结点,则进行 红黑树的哈希重分布
else if (e instanceof TreeNode)
((TreeNode<K, V>) e).split(this, newTab, j, oldCap);
// 如果是普通结点,就进行普通的哈希重分布
else { // preserve order
Node<K, V> loHead = null, loTail = null; // 留在原地的结点
Node<K, V> hiHead = null, hiTail = null; // 要迁移 oldCapacity距离的结点
Node<K, V> next;
do {
next = e.next;
// 如果 e的哈希值和老表的容量进行与运算为 0,则扩容后的索引位置跟老表的索引位置一样
if ((e.hash & oldCap) == 0) {
// 如果 loTail为空,则表示该结点是第一个结点
if (loTail == null)
loHead = e;
else
// 否则,就把新结点放到最后
loTail.next = e;
loTail = e;
// 否则如果 e的哈希值于老表容量的与运算非 0,则扩容后的索引为:老表索引 + oldCap
} else {
if (hiTail == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
}
} while ((e = next) != null);
// 如果 loTail不为空,说明存在部分数据不需要迁移
if (loTail != null) {
loTail.next = null;
newTab[j] = loHead;
}
// 如果 hiTail不为空,说明存在部分数据发生了迁移
if (hiTail != null) {
hiTail.next = null;
newTab[j + oldCap] = hiHead;
}
}
}
}
}
// 将新的数组返回
return newTab;
}
// 扩容时,针对红黑树进行重新哈希
final void split(HashMap<K, V> map, Node<K, V>[] tab, int index, int bit) {
// 获取调用此方法的结点
TreeNode<K, V> b = this;
// Relink into lo and hi lists, preserving order
TreeNode<K, V> loHead = null, loTail = null;
TreeNode<K, V> hiHead = null, hiTail = null;
int lc = 0, hc = 0;
// 以调用此方法的结点开始,遍历整个红黑树结点
for (TreeNode<K, V> e = b, next; e != null; e = next) {
// next赋值为 e的下一个结点
next = (TreeNode<K, V>) e.next;
// 清空老表的引用
e.next = null;
// 判断去留
if ((e.hash & bit) == 0) {
// 插入到留队列里,如果队尾元素不存在,说队列为空,当前结点直接成为第一个结点
if ((e.prev = loTail) == null)
loHead = e;
else
// 否则追加到留队列里的最后
loTail.next = e;
loTail = e;
// 统计数量
++lc;
// 否则就是要离开了
} else {
// 同样的,插入到走队列
if ((e.prev = hiTail) == null)
hiHead = e;
else
hiTail.next = e;
hiTail = e;
++hc;
}
}
// 如果原索引位置不空
if (loHead != null) {
// 如果结点个数 <= 6,则将红黑树转换为链表结构
if (lc <= UNTREEIFY_THRESHOLD)
tab[index] = loHead.untreeify(map);
else {
// 否则,将原索引位置的结点设置为对应的头结点
tab[index] = loHead;
// 如果 hiHead非空,则构建新的红黑树
if (hiHead != null) // (else is already treeified)
loHead.treeify(tab);
}
}
// 如果 新索引位置不为空
if (hiHead != null) {
// 退化为链表
if (hc <= UNTREEIFY_THRESHOLD)
tab[index + bit] = hiHead.untreeify(map);
else {
// 设置新索引的结点为对应头结点
tab[index + bit] = hiHead;
// 树化
if (loHead != null)
hiHead.treeify(tab);
}
}
}
// 从红黑树退化为链表
final Node<K,V> untreeify(HashMap<K,V> map) {
// hd指向头结点,tl指向尾结点
Node<K,V> hd = null, tl = null;
// 从链表的头结点开始遍历,将所有树结点全部转为链表结点
for (Node<K,V> q = this; q != null; q = q.next) {
// 调用 replacementNode()方法构建链表结点
Node<K,V> p = map.replacementNode(q, null);
// 如果 tl为null,表明当前结点为第一个结点,直接将 它赋值给 hd
if (tl == null)
hd = p;
// 否则,将插入到队尾
else
tl.next = p;
tl = p;
}
// 返回转换后链表的首元结点
return hd;
}
常用内部类
static class Node<K, V> implements Map.Entry<K, V>
// Hash Map中数据以 Node结点的形式存储
static class Node<K,V> implements Map.Entry<K,V> {
// 键的 hashCode值
final int hash;
// 键名
final K key;
// 值
V value;
// 指向下一个结点的指针
Node<K,V> next;
// 有参构造
Node(int hash, K key, V value, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.value = value;
this.next = next;
}
// 对属性的获取方法
public final K getKey() { return key; }
public final V getValue() { return value; }
public final String toString() { return key + "=" + value; }
// 返回 键的哈希值与 值的哈希值的 异或结果
public final int hashCode() {
return Objects.hashCode(key) ^ Objects.hashCode(value);
}
// 设置该结点的值的方法
// 在 Node结点中,只有 value和 next是在结点创建后还能修改的,其他都被 final修饰
public final V setValue(V newValue) {
V oldValue = value;
value = newValue;
return oldValue;
}
// 重写 equals()方法,注意这只是 Node结点的 equals()
public final boolean equals(Object o) {
// 地址相同 肯定是同一个
if (o == this)
return true;
// 类型相同
if (o instanceof Map.Entry) {
Map.Entry<?,?> e = (Map.Entry<?,?>)o;
// 键相同,值相同
if (Objects.equals(key, e.getKey()) &&
Objects.equals(value, e.getValue()))
return true;
}
return false;
}
}
Java虚拟机
这部分主要整理自 周志明老师的 深入理解Java虚拟机
走进Java
概述
Java不仅仅是一门编程语言,还是一个由一系列计算机软件和规范组成的技术体系,这个体系提供了完整的用于软件开发和跨平台部署的支持环境,并广泛应用于嵌入式系统、移动终端、企业服务器、大型机等多种场合。
Java能获得如此广泛的认可,除了它拥有一门结构严谨、面向对象的编程语言之外,还有许多不可忽视的优点:
- 摆脱了硬件平台的束缚,实现了“一次编译,到处运行”的理想
- 提供了一种相对安全的内存管理和访问机制,避免了大部分内存泄漏和指针越界的问题
- 实现了热点代码检测和运行时编译及优化,使得 Java应用能随着运行时间的增长而获得更高的性能
- 有一套完善的应用程序接口,还有众多第三方类库等等
Java技术体系
广义上来讲,Kotlin、Clojure、JRuby、Groovy等运行于 Java虚拟机上的编程语言及其相关的程序都属于 Java技术体系中的一员。
从传统意义上来看,根据 Java Community Process定义,Java技术体系包括:
- Java程序设计语言
- 各种硬件平台上的 Java虚拟机实现
- Class文件格式
- Java类库的 API
- 来自商业机构和开源社区的第三方 Java类库
我们可以把 Java语言、JVM虚拟机、Java类库这三个部分统称为 JDK(Java Development Kit),它是用于支持 Java程序开发的最小环境。
我们可以把 Java类库 API中的 Java SE API的子集和 JVM虚拟机这两部分统称为 JRE(Java Runtime Environment),它是支持 Java程序运行的标准环境
https://www.oracle.com/java/technologies/platform-glance.html
Java发展史
1991年 4月,由 James Gosling博士领导的绿色计划启动,旨在开发一种能够在各种消费性电子产品上运行的程序架构。这个计划的产品就是 Java的前身:Oak。
1995年 5月 23日,Oak语言改名为 Java,第一次提出了“Write Once, Run Anywhere”的口号
1996年 1月 23日,JDK 1.0发布,它提供了一个纯解释运行的 Java虚拟机实现(Sun Classic VM)
- JDK 1.0 版本的代表技术包括:Java虚拟机、Applet、AWT等
1997年 2月 19日,JDK 1.1发布,提供类许多最基础的技术支持点(如 JDBC等)
- JDK 1.1版本的代表技术包括:JAR文件格式、JDBC、JavaBeans、RMI等
- Java语言的语法也有了一定的增强,如 内部类(Inner Class) 和 反射(Reflection)都是在这个时候出现的
1998年 12月 4日,JDK 1.2发布,Sun公司将 Java技术体系拆分成三个方向,分别是 J2SE、J2EE 和 J2ME
- 这个版本的代表技术很多,如:EJB、Java Plug-in、Java IDL、Swing等
- 并且这个版本的 Java虚拟机第一次内置了 JIT即时编译器
- JDK 1.2中曾共存过三个虚拟机:Classic VM、HotSpot VM、Exact VM
2000年 5月 8日,JDK 1.3发布,它的改进主要体现在 Java类库上
- JNDI服务从此开始被作为以像平台级服务
- 使用 CORBA IIOP实现 RMI的通信协议
2002年 2月 13日,JDK 1.4发布,标志着 Java正式走向一个成熟的版本
- JDK 1.4带来了许多新的技术特性,如:正则表达式、异常链、NIO、日志类、XML解析器、XSLT转换器等
2004年 9月 30日,JDK 5发布,它在 Java语法的易用性上做出了巨大改进。
- 如:自动装箱、泛型、动态注解、枚举、可变长参数、forEach遍历循环等
- 在虚拟机层面,这个版本改进了 Java的内存模型、提供了 JUC包
2006年 12月 11日,JDK 6发布,Java正式开源。
- JDK 6的改进包括:提供初步的动态语言支持、提供编译期注解处理器和卫星 HTTP服务器 API
- 同时对 Java虚拟机内部做了诸多改进,包括锁与同步、垃圾收集、类加载等
2009年 2月 19日,JDK 7发布,Sun公司被 Oracle公司收购。
- JDK 7包含的改进有:提供新的 G1收集器、加强对非 Java语言的调用支持、可并行的类加载架构等
2014年 3月 18日,JDK 8发布,它提供了那些曾在 JDK7中规划过但是未能在 JDK7中完成的功能
- 包括:对 Lambda表达式的支持、内置对 JS引擎的支持、新的日期时间 API、彻底移除 HotSpot永久代
2017年 9月 21日,JDK 9发布,它增强了若干工具,如:JS Shell、JLink、JHSDB等。Oracle明确之后的 JDK将会每年两个大版本的形式
2018年 3月 20日,JDK 10发布,完成了内部的重构,如:统一了源仓库、统一了垃圾收集器接口、统一即时编译器接口等
2018年 9月 25日,JDK 11发布,包含了 ZGC这样的革命性的垃圾收集器。Oracle调整了 JDK的授权许可证,将原来的商业特性开源给 OpenJDK,同时推出收费的 OracleJDK
2019年 3月 20日,JDK 12发布,包含 JMH测试套件,并且加入了 Shenandoah作为首个非 Oracle开发的垃圾收集器,与 ZGC形成了竞争。
Java 面临的危机挑战前所未有的艰巨,属于 Java的未来也从未如此充满想象和可能
Java虚拟机家族
虚拟机始祖 : Sun Class/Exact VM
Sun Classic虚拟机是世界上第一款商用虚拟机,虽然其使命早已终结,但是值得历史的铭记。
JDK 1.0中所带的虚拟机就是 Classic VM,它只能使用纯解释器的方式来执行 Java代码,如果要使用即时编译器就必须外挂,但是如果外挂了即时编译器的话,即时编译器就会完全接管虚拟机的执行系统,解释器便不能再工作了。
由于解释器和编译器不能配合工作,这就意味着如果要使用编译执行,编译器就必须对每一个方法、每一行代码都进行编译,而不管其是否具有编译的价值(不考虑代码的执行频率)。
再 JDK 1.2时,曾推出过 Exact VM供 Solaris平台使用。它的编译执行系统已经具备了现代高性能虚拟机的雏形,具备了热点探测、两级即时编译、编译器与解释器混合工作等模式。
Exact VM因为使用了准确式内存管理而得名,具体指的是,虚拟机可以知道内存中某个位置的数据具体是什么类型的。这也是垃圾收集时准确判断堆上的垃圾是否还可能被引用的前提。使用准确式内存管理,Exact VM可以抛弃以前的基于 句柄(Handle) 的对象查找方式
武林盟主:HotSpot VM
HotSpot是 Sun/OracleJDK 和 OpenJDK中的默认 Java虚拟机,是目前使用范围最广的 Java虚拟机。
HotSpot的热点代码探测能力可以通过执行计数器找出最具有编译价值的代码,然后同志既是编译器以方法为单位进行编译。如果一个方法被频繁调用,或方法中有效循环次数很多,将会分别触发 标准即时编译 和 栈上替换编译 行为。通过编译器与解释器恰当的协同工作,可以在最优的程序响应时间与最佳执行性能中取得平衡。
小家碧玉:Mobile/ Embedded VM
Sun/Oracle公司同样也推出了针对移动和嵌入式市场的虚拟机产品。Oracle公司在 Java ME这条产品线上推出的虚拟机为 CDC-HI和 CLDC-HI,二者借鉴了部分 HotSpot的技术。在嵌入式设备中,Java ME Embedded面临着自家的 eJDK的直接竞争和侵蚀
天下第二:BEA JRocket/IBM J9 VM
BEA System公司的 JRockit和 IBM公司的 IBM J9曾经和 HotSpot并称为“三大商业 Java虚拟机”。
BEA将 JRocket发展为一款专门为服务器硬件和服务端应用场景高度优化的虚拟机,它不包含解释器的实现,全部代码都靠即时编译器编译后执行。此外 JRockit的垃圾收集器和 Java Mission Control故障处理套件等部分的实现,在当时的众多 Java虚拟机中也都属于领先水平。JRockit随着 BEA被 Oracle收购,现已不再继续发展。
IBM的 J9虚拟机的市场定位与 HotSpot比较接近,是一款在设计上全面考虑服务端、桌面应用,再到嵌入式的多用途虚拟机。IBM J9直至今日仍旧十分活跃,IBM J9虚拟机的职责分离与模块化做得比 HotSpot更优秀,由 J9虚拟机中抽象封装出来的核心组件库就单独构成了 IBM OMR项目,可以在其他语言平台如 Ruby、Python中快速组装成相应的功能。可以通过 AdoptOpenJDK来获得采用 OpenJDK搭配上 OpenJDK其他类库组成的完整 JDK。
软硬合璧:BEA Liquid VM/ Azul VM
存在一类与特定硬件平台绑定、软硬件配合工作的专有虚拟机,它们往往能够实现更高的执行性能,或提供某些特殊的功能属性。
Liquid VM也被称为 JRockit VE,可以直接运行在 BEA的 Hypervisor系统上的 JRockit虚拟机的虚拟化版本,它不需要操作系统的支持,或者说其本身就实现了一个专用操作系统的必要功能。
Azul VM是 Azul System公司在 HotSpot基础上进行大量改进,运行于 Vega系统上的 Java虚拟机,每个 Azul VM实例都可以管理至少数十个 CPU和数百 GB内存的硬件资源,并提供在巨大内存范围内停顿时间可控的垃圾收集器(业内赫赫有名的 PGC和 C4收集器)。
Zing虚拟机是 Azul公司基于 HotSpot的一个旧版本上实现的,在要求低延迟、快速预热等场景中,Zing VM都要比 HotSpot表现得更好。Zing的 PGC、C4收集器可以轻易支持 TB级别的 Java堆内存,而且保证暂停时间仍然可以维持在不超过 10毫秒的范围里,而 HotSpot要一直到 JDK11和 JDK12的 ZGC和 Shenandoah收集器才达到了相同的目标,而且目前效果远不如 C4。Zing能让普通用户无需了解垃圾收集等底层调优,就可以使得 Java应用享有低延迟、快速预热、易于监控的功能。
挑战者:Apache Harmony/ Google Android Dalvik VM
Harmony虚拟机和 Dalvik虚拟机一般只能称作“虚拟机”而非 Java虚拟机,但是这两款虚拟机背后所代表的技术体系都曾经对 Java世界产生了非常大的影响和挑战,当时甚至由悲观的人认为成熟的 Java生态系统都有面临分裂和崩溃的可能。
Apache Harmony是 Apache基金会下的一款开源的实际兼容于 JDK5和 JDK6的 Java程序运行平台,它含有自己的虚拟机和 Java类库 API,但是没有通过 Sun公司的 TCK认证授权。所以 Apache和 Sun公司关系闹僵,并愤然退出了 JCP组织,这是 Java社区有史以来最严重的分裂事件之一。
Android让 Java语言真正走进了移动数码设备领域,只是走的并非 Sun公司原本想象的那一条路。Dakvik随着 Android的成功迅速流行,在 Android 2.2中开始提供即时编译器的实现,在 Android 4.4之后支持提前编译的 ART虚拟机迅速崛起,并替代了 Dalvik虚拟机。
没有成功,但并非失败:Microsoft JVM及其他
在 Java虚拟机二十几年的发展历程中,还有许多其他虚拟机是不为人知的默默沉寂着,或者曾经绚丽过但最终夭折湮灭的。
展望 Java技术的未来
- 无语言倾向
- 2018年 Oracle Labs公开了一项黑科技:Graal VM,打着 “Run Programs Faster Anywhere”的口号。Graal VM是一个在 HotSpot虚拟机基础上增强而成为的 跨语言全栈虚拟机,可以作为任何语言的运行平台使用,包括 Java、Scala等基于 Java虚拟机的语言,也包括 C、C++、Rust等基于 LLVM的语言,同时也支持 JS、Ruby、Python等语言。
- Graal VM可以无额外开销地混合使用这些编程语言,甚至支持不同语言中混用对方的接口和对象。
- 其基本的工作原理为:将这些语言的源代码或编译后的中间格式文件 通过解释器转换成能被 Graal VM接收的表示形式(譬如,设计一个解释器专门针对 LLVM输出的字节码进行转换),这个过程被称为 程序特化(Specialized 或者 Partial Evaluation)
- Graal VM提供了 Truffle工具集来快速构建面向一种新的语言的解释器,并用它构建了一个称为 Sulong的高性能 LLVM字节码解释器
- Graal VM做到了只与机器特性相关,而不与某种高级语言特性相关。
- 如果 Java语言或者 HotSpot虚拟机真的有被取代的以添,现在看来 Graal VM是希望最大的一个候选项,这场革命很可能会在 Java使用者没有明显感觉的情况下悄然到来,Java世界所有的软件生态都没有丝毫变化,但天下第一的位置已经悄然迭代。
- 新一代即时编译器
- 对于需要长时间运行的应用来说,其经过充分预热后,热点代码会被 HotSpot的探测机制准确捕获,因此即时编译器输出的代码质量非常重要
- HotSpot中有两个即时编译器,分别是:
- 编译耗时短但输出代码优化程度低的客户端编译器(简称为 C1)
- 编译耗时长但输出代码优化质量也更高的服务端编译器(简称为 C2)
- 从 JDK 10起,又加入了 Graal编译器,替代 C2编译器。Graal能够做比 C2更加复杂的优化,如“部分逃逸分析”,也拥有比 C2更容易使用激进预测性优化的策略。
- 向 Native迈进
- 在大型单体应用架构向小型微服务应用架构发展的技术潮流下,Java启动时间较长,需要预热才能达到最高性能的特点与现实应用场景相悖。
- 在最近的几个 JDK版本中,Java已经陆续推出了跨进程的、可以面向用户程序的类型信息共享,逐步开始对提前编译提供支持。
- 提前编译的好处是 Java虚拟机加载预编译文件后能直接调用,无需再等待 JIT,可以减少预热时间。
- 但是提前编译也破坏了 Java“一次编译,到处运行”的口号,降低了 Java程序连接过程的动态性,需要为不同的硬件、操作系统编译对应的发行包。
- 直到 Substrate VM的出现,满足了人们对 Java提前编译的全部期待。Substrate VM是 Graal VM 0.20版本的一个极小型的运行时环境。它带来的好处是能显著降低内存占用及启动时间,方便小规模应用的运行。
- 灵活的胖子
- HotSpot年纪很大了,保持了二十几年的长盛不衰。
- 目前 HotSpot正不断得进行着重构,吸纳了 J9 VM的模块化功能,实现了诸多新的功能。
- HotSpot先后提供了 虚拟机调试接口、信息监控接口、虚拟机工具接口、语言级别的编译器接口、垃圾收集器接口等
- 语言语法持续增强
- 随着每半年一次更新的节奏,新版本的 Java中会出现越来越多其他语言里已有的优秀特性
自动内存管理
Java内存区域与内存溢出异常
Java与 C++之间有一堵由内存动态分配和垃圾收集技术所围成的高墙,墙外面的人想进去,墙里面的人想出来。
运行时数据区域
程序计数器
程序计数器是一块较小的内存空间,它可以看坐是当前线程所执行的字节码的行号指示器
字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的治时期,各种循环、跳转、异常、线程恢复等语句都需要依靠程序计数器。
程序计数器是线程私有的,各条线程之间的程序计数器互不影响,独立存储。
如果线程正在执行的是一个 Java方法,这个计数器记录的是正在执行的虚拟机字节码指令地址;如果执行的是一个本地方法(Native Method),这个计数器的值则应为空(Undefined)
它是唯一一个在《Java虚拟机规范》中没有规定任何 OOM情况的区域
Java虚拟机栈
与程序计数器一样,Java虚拟机栈也是线程私有的,它的生命周期与线程相同。
虚拟机栈描述的是 Java方法执行的线程内存模型:每个方法被执行的时候,Java虚拟机都会同步创建一个栈帧(Stack Frame)用于存放局部变量表、操作数栈、动态连接、方法出口等信息。每个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
局部变量表存放了编译期可知的各种 Java虚拟机****基本数据类型(boolean, byte, char, short, int ,float等)、对象引用(reference类型,可能是一个指向对象指向对象起始地址的引用指针,也可能是一个代表对象的句柄) 和 returnAddress类型(指向了一条字节码指令的地址)
这些数据类型在局部变量表中的存储空间以局部变量槽(slot)来表示,其中 64位长度的 long和 double类型的数据会占用两个变量槽,其余的类型只用一个。局部变量表所需的内存空间在在编译期期间完成分配,且在方法运行期间不会改变其大小。
在《Java虚拟机规范》中,对这个内存区域规定了两类异常状况:
- 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 StackOverFlowError异常
- 如果 Java虚拟机栈容量是可以动态扩展的,当栈扩展时无法申请到足够的内存会抛出 OOM异常