一、Java基础
1、基础知识
1.1重载和重写的区别
重载:发生在同一个类中,方法名必须相同,参数类型不同,个数不同,顺序不同时,方法返回值和访问修饰符可以不同,发生在编译时。
重写:发生在父子类中,方法名,参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类。访问修饰符范围大于等于父类,如果父类方法访问修饰符为private则子类就不能重写该方法。
1.2 String和StringBuffer,StringBuilder的区别是什么?String为什么是不可变的?
可变性:
简单的来说:String类中使用final关键字字符数组保存字符串,private final char value[] ,所以String对象时不可变的。而StringBuilder和StringBuffer都继承自AbstractStringBuilder类,在AbstractStringBuilder中也是使用字符数组保存字符串 char value[] 但是没有用fianl修饰,所以这两种对象都是可变的。
StringBuilder 与 StringBuffer 的构造方法都是调用父类构造方法也就是
AbstractStringBuilder 实现的.
AbstractStringBuilder.java
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];
}
线程安全性
String 中的对象是不可变的,也就可以理解为常量,线程安全。
AbstractStringBuilder 是 StringBuilder 与 StringBuffer 的公共父类,定义
了一些字符串的基本操作,如 expandCapacity.append.insert.indexOf 等公
共 方法。StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以
是线程安全的。StringBuilder 并没有对
方法进行加同步锁,所以是非线程安全的。
性能
abstract class AbstractStringBuilder implements Appendable, CharSequence {
char[] value;
int count;
AbstractStringBuilder() {
}
AbstractStringBuilder(int capacity) {
value = new char[capacity];}
每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然
后将指针指向新的 String 对象。
StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新
的对象并改变对象引用。相同情况下使用 StirngBuilder 相比使用
StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的
风险。
对于三者使用的总结:
- 操作少量的数据 = String
- 单线程操作字符串缓冲区下操作大量数据 = StringBuilder
- 多线程操作字符串缓冲区下操作大量数据 = StringBuffer
1.3 自动装箱与拆箱
装箱:将基本类型用它们对应的引用类型包装起来;
拆箱:将包装类型转换为基本数据类型;
1.4 == 与 equals
== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不
是同一个对象。(基本数据类型比较的是值,引用数据类型比较的是内存
地址)
equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情
况:
情况 1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对
象时,等价于通过“==”比较这两个对象。
情况 2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来两
个对象的内容相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。
public class test1 {
public static void main(String[] args) {
String a = new String("ab"); // a 为一个引用
String b = new String("ab"); // b 为另一个引用,对象的内容一样
String aa = "ab"; // 放在常量池中
String bb = "ab"; // 从常量池中查找
if (aa == bb) // true
System.out.println("aa==bb");
if (a == b) // false,非同一对象
System.out.println("a==b");
if (a.equals(b)) // true
System.out.println("aEQb");
if (42 == 42.0) {
// true
System.out.println("true");
}
}
}
说明:
String 中的 equals 方法是被重写过的,因为 object 的 equals 方法是
比较的对象的内存地址,而 String 的 equals 方法比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的
值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池
中重新创建一个 String 对象。
1.5 关于 final 关键字的一些总结
final 关键字主要用在三个地方:变量、方法、类。
- 对于一个 final 变量,如果是基本数据类型的变量,则其数值一旦在初始
化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。 - 当用 final 修饰一个类时,表明这个类不能被继承。final 类中的所有成
员方法都会被隐式地指定为 final 方法。 - 使用 final 方法的原因有两个。第一个原因是把方法锁定,以防任何继承
类修改它的含义;第二个原因是效率。
在早期的 Java 实现版本中,会将 final 方法转为内嵌调用。但是如果方法过
于庞大,可能看不到内嵌调用带来的任何性能提升(现在的 Java 版本已经不需要使用 final 方法进行这些优化了)。类中所有的 private 方法都隐式地指定为
final。
1.6 Object 类的常见方法总结
Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法:
方法 1: public final native Class<?> getClass()
// native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键
字修饰,故不允许子类重写。
方法 2: public native int hashCode()
// native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK
中的 HashMap。
方法 3:public boolean equals(Object obj)
// 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写
用户比较字符串的值是否相等。
方法 4:protected native Object clone() throws CloneNotSupportedException
// naitive 方法,用于创建并返回当前对象的一份拷贝。一般情况下,对于
任何对象 x,表达式 x.clone() != x 为 true,x.clone().getClass() == x.getClass() 为true。Object 本身没有实现 Cloneable 接口,所以不重写 clone 方法并且进行调用的话会发生 CloneNotSupportedException 异常。
方法 5:public String toString()
// 返回类的名字@实例的哈希码的 16 进制的字符串。建议 Object 所有的
子类都重写这个方法。
方法 6:public final native void notify()
// native 方法,并且不能重写。必须锁对象调用该方法,不会释放锁资源,唤醒一个在此对象监视器上等待的线程(监
视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。
方法 7: public final native void notifyAll()
// native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在
此对象监视器上等待的所有线程,而不是一个线程。
方法 8:public final native void wait(long timeout) throws InterruptedException
// native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释
放锁,而 wait 方法释放了锁 。timeout 是等待时间。
方法 9:public final void wait(long time, int nanos) throws InterruptedException
// 多了 nanos 参数,这个参数表示额外时间(以毫微秒为单位,范围是
0-999999)。 所以超时的时间还需要加上 nanos 毫秒。
方法 10:public final void wait() throws InterruptedException
// 跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这
个概念。必须是锁对象调用,会释放锁资源。
方法 11:protected void finalize() throws Throwable { }
// 实例被垃圾回收器回收的时候触发的操作。
1.7 Java 中的异常处理
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的
Throwable 类。Throwable: 有两个重要的子类:
Exception(异常) 和 Error(错误) ,二者都是 Java 异常处理的重要
子类,各自都包含大量子类。
Error(错误):是程序无法处理的错误,表示运行应用程序中较严重问题。
大多数错误与代码编写者执行的操作无关,而表示代码运行时 JVM(Java 虚拟
机)出现的问题。例如,Java 虚拟机运行错误(Virtual MachineError),当
JVM 不再有继续执行操作所需的内存资源时,将出现 OutOfMemoryError。
这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
这些错误表示故障发生于虚拟机自身.或者发生在虚拟机试图执行应用时,
如 Java 虚拟机运行错误(Virtual MachineError).类定义错误
(NoClassDefFoundError)等。这些错误是不可查的,因为它们在应用程序的
控制和处理能力之 外,而且绝大多数是程序运行时不允许出现的状况。对于设
计合理的应用程序来说,即使确实发生了错误,本质上也不应该试图去处理它所引起的异常状况。在 Java 中,错误通过 Error 的子类描述。
Exception(异常):是程序本身可以处理的异常。Exception 类有一个重
要的子类 RuntimeException。
RuntimeException 异常由 Java 虚拟机抛出。NullPointerException(要
访问的变量没有引用任何对象时,抛出该 异常).ArithmeticException(算术
运算异常,一个整数除以 0 时,抛出该异常)和
ArrayIndexOutOfBoundsException (下标越界异常)。
注意:异常和错误的区别:异常能被程序本身可以处理,错误是无法处理。
Throwable 类常用方法
public string getMessage():返回异常发生时的详细信息
public string toString():返回异常发生时的简要描述
public string getLocalizedMessage():返回异常对象的本地化信息。使
用 Throwable 的子类覆盖这个方法,可以声称本地化信息。如果子类没有覆盖
该方法,则该方法返回的信息与 getMessage()返回的结果相同
public void printStackTrace():在控制台上打印 Throwable 对象封装的
异常信息
异常处理总结
try 块:用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,
则必须跟一个 finally 块。
catch 块:用于处理 try 捕获到的异常。
finally 块:无论是否捕获或处理异常,finally 块里的语句都会被执行。当
在 try 块或 catch 块中遇到 return 语句
时,finally 语句块将在方法返回之前被执行。
在以下 4 种特殊情况下,finally 块不会被执行:
- 在 finally 语句块中发生了异常。
- 在前面的代码中用了 System.exit()退出程序。
- 程序所在的线程死亡。
- 关闭 CPU。
1.8 获取用键盘输入常用的的两种方法
方法 1:通过 Scanner
Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close();
方法 2:通过 BufferedReader
BufferedReader input = new BufferedReader(new
InputStreamReader(System.in));
String s = input.readLine();
1.9 接口和抽象类的区别是什么
- 接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始
接口方法可以有默认实现),抽象类可以有非抽象的方法 - 接口中的实例变量默认是 final 类型的,而抽象类中则不一定
- 一个类可以实现多个接口,但最多只能实现一个抽象类
- 一个类实现接口的话要实现接口的所有方法,而抽象类不一定
- 接口不能用 new 实例化,但可以声明,但是必须引用一个实现该接口
的对象 从设计层面来说,抽象是对类的抽象,是一种模板设计,接口是行为的抽象,是一种行为的规范。
备注:在 JDK8 中,接口也可以定义静态方法,可以直接用接口名调用。实
现类和实现是不可以调用的。如果同时实现两个接口,接口中定义了一样的默认方法,必须重写,不然会报错。
1.10 JVM 调优及内存泄漏
1.10.1 JVM 调优工具
Jconsole,JProfile,VisualVM
Jconsole : JDK 自带,功能简单,但是可以在系统有一定负荷的情况下使
用。对垃圾回收算法有很详细的跟踪。
JProfiler:商业软件,需要付费。功能强大。
VisualVM:JDK 自带,功能强大,与 JProfiler 类似。推荐。
1.10.2 如何调优
观察内存释放情况.集合类检查.对象树
上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功
能
堆信息查看
可查看堆空间大小分配(年轻代.年老代.持久代分配)
提供即时的垃圾回收功能
垃圾监控(长时间监控回收情况)
查看堆内类.对象信息查看:数量.类型等
对象引用情况查看
有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:
- 年老代年轻代大小划分是否合理
- 内存泄漏
- 垃圾回收法设置是否合理
1.10.3 线程监控
线程信息监控:系统线程数量。
线程状态监控:各个线程都处在什么样的状态下
Dump 线程详细信息:查看线程内部运行情况
死锁检查
热点分析
CPU 热点:检查系统哪些方法占用的大量 CPU 时间
内存热点:检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对
象一起统计)
这两个东西对于系统优化很有帮助。我们可以根据找到的热点,有针对性的
进行系统的瓶颈查找和进行系统优化,而不是漫无目的的进行所有代码的优化。
快照
快照是系统运行到某一时刻的一个定格。在我们进行调优的时候,不可能用
眼睛去跟踪所有系统变化,依赖快照功能,我们就可以进行系统两个不同运行时
刻,对象(或类.线程等)的不同,以便快速找到问题
举例说,我要检查系统进行垃圾回收以后,是否还有该收回的对象被遗漏下
来的了。那么,我可以在进行垃圾回收前后,分别进行一次堆情况的快照,然后
对比两次快照的对象情况。
1.10.4 内存泄漏检查
内存泄漏是比较常见的问题,而且解决方法也比较通用,这里可以重点说一
下,而线程.热点方面的问题则是具体问题具体分析了。
内存泄漏一般可以理解为系统资源(各方面的资源,堆.栈.线程等)在错误
使用的情况下,导致使用完毕的资源无法回收(或没有回收),从而导致新的资
源分配请求无法完成,引起系统错误。
内存泄漏对系统危害比较大,因为他可以直接导致系统的崩溃。
需要区别一下,内存泄漏和系统超负荷两者是有区别的,虽然可能导致的最
终结果是一样的。内存泄漏是用完的资源没有回收引起错误,而系统超负荷则是
系统确实没有那么多资源可以分配了(其他的资源都在使用)。
年老代堆空间被占满
异常: java.lang.OutOfMemoryError: Java heap space
说明:
这是最典型的内存泄漏方式,简单说就是所有堆空间都被无法回收的垃圾对
象占满,虚拟机无法再在分配新空间。
如上图所示,这是非常典型的内存泄漏的垃圾回收情况图。所有峰值部分都
是一次垃圾回收点,所有谷底部分表示是一次垃圾回收后剩余的内存。连接所有
谷底的点,可以发现一条由底到高的线,这说明,随时间的推移,系统的堆空间
被不断占满,最终会占满整个堆空间。因此可以初步认为系统内部可能有内存泄
漏。(上面的图仅供示例,在实际情况下收集数据的时间需要更长,比如几个小
时或者几天)
解决:这种方式解决起来也比较容易,一般就是根据垃圾回收前后情况对比,同时根据对象引用情况(常见的集合对象引用)分析,基本都可以找到泄漏点。
持久代被占满
异常:java.lang.OutOfMemoryError: PermGen space
说明:Perm 空间被占满。无法为新的 class 分配存储空间而引发的异常。
这个异常以前是没有的,但是在 Java 反射大量使用的今天这个异常比较常见了。
主要原因就是大量动态反射生成的类不断被加载,最终导致 Perm 区被占满。
更可怕的是,不同的 classLoader 即便使用了相同的类,但是都会对其
进行加载,相当于同一个东西,如果有 N 个 classLoader 那么他将会被加载 N
次。因此,某些情况下,这个问题基本视为无解。当然,存在大量 classLoader
和大量反射类的情况其实也不多。
解决:
- -XX:MaxPermSize=16m
- 换用 JDK。比如 JRocket
堆栈溢出
异常:java.lang.StackOverflowError
说明:这个就不多说了,一般就是递归没返回,或者循环调用造成
线程堆栈满
异常:Fatal: Stack size too small
说明:java 中一个线程的空间大小是有限制的。JDK5.0 以后这个值是 1M。
与这个线程相关的数据将会保存在其中。但是当线程空间满了以后,将会出现上
面异常。
解决:增加线程栈大小。-Xss2m。但这个配置无法解决根本问题,还要看
代码部分是否有造成泄漏的部分。
系统内存被占满
异常:java.lang.OutOfMemoryError: unable to create new native thread
说明:这个异常是由于操作系统没有足够的资源来产生这个线程造成的。系
统创建线程时,除了要在 Java 堆中分配内存外,操作系统本身也需要分配资源
来创建线程。因此,当线程数量大到一定程度以后,堆中或许还有空间,但是操
作系统分配不出资源来了,就出现这个异常了。
分配给 Java 虚拟机的内存愈多,系统剩余的资源就越少,因此,当系统内
存固定时,分配给 Java 虚拟机的内存越多,那么,系统总共能够产生的线程也
就越少,两者成反比的关系。同时,可以通过修改-Xss 来减少分配给单个线程
的空间,也可以增加系统总共内生产的线程数。
解决: - 重新设计系统减少线程数量。
- 线程数量不能减少的情况下,通过-Xss 减小单个线程大小。以便能生产
更多的线程
1.11 JVM 内存管理
JVM 将内存划分为 6 个部分:PC 寄存器(也叫程序计数器).虚拟机栈.堆. 方法区.运行时常量池.本地方法栈
PC 寄存器(程序计数器):用于记录当前线程运行时的位置,每一个线程
都有一个独立的程序计数器,线程的阻塞.恢复.挂起等一系列操作都需要程
序计数器的参与,因此必须是线程私有的。
java 虚拟机栈:在创建线程时创建的,用来存储栈帧,因此也是线程私有
的。java 程序中的方法在执行时,会创建一个栈帧,用于存储方法运行时的
临时数据和中间结果,包括局部变量表.操作数栈.动态链接.方法出口等信息。
这些栈帧就存储在栈中。如果栈深度大于虚拟机允许的最大深度,则抛出 S
tackOverflowError 异常。
局部变量表:方法的局部变量列表,在编译时就写入了 class 文件
操作数栈:int x = 1; 就需要将 1 压入操作数栈,再将 1 赋值给变量 x
java 堆:java 堆被所有线程共享,堆的主要作用就是存储对象。如果堆空间不够,但扩展时又不能申请到足够的内存时,则抛出 OutOfMemoryError 异常。
StackOverflowError | OutOfMemoryError |
---|---|
java 栈 | java 堆 |
栈深度超过范围了(比如:递归层数太多了) | 内存空间不够了(需要及时释放内存) |
方法区:方发区被各个线程共享,用于存储静态变量.运行时常量池等信息。
本地方法栈:本地方法栈的主要作用就是支持 native 方法,比如在 java 中
调用 C/C+
1.12 GC 回收机制
- 哪些内存需要回收?—— who
- 什么时候回收?—— when
- 怎么回收?—— how
1. 哪些内存需要回收?
Java 堆方法区的内存
2. 什么时候回收?
引用计数法
可达性分析
2.1 引用计数法
给对象添加一个引用计数器,每当有一个地方引用它时,计数器加一。反
之每当一个引用失效时,计数器减一。当计数器为 0 时,则表示对象不被引用。
举个例子:
Object a = new Object(); // a 的引用计数为 1
a = null; // a 的引用计数为 0,等待 GC 回收
但是,引用计数法不能解决对象之间的循环引用,见下例
Object a = new Object(); // a 的引用计数为 1
Object b = new Object(); // b 的引用计数为 1
a.next = b; // a 的引用计数为 2
b.next = a; // b 的引用计数为 2
a = null; // a 的引用计数为 1,尽管已经显示地将 a 赋值为 null,但是由于引用计数为 1,GC 无法回收 a
b = null; // b 的引用计数为 1,同理,GC 也不回收 b
2.2 可达性分析
设立若干根对象(GC Root),每个对象都是一个子节点,当一个对象找
不到根时,就认为该对象不可达。
没有一条从根到 Object4 和 Object5 的路径,说明这两个对象到根是不可
达的,可以被回收
补充:java 中,可以作为 GC Roots 的对象包括:
java 虚拟机栈中引用的对象
方法区中静态变量引用的对象
方法区中常量引用的对象
本地方法栈中引用的对象
3. 怎么回收?
标记-清除算法
复制算法
标记-整理算法
分代算法
3.1 标记——清除算法
遍历所有的 GC Root,分别标记处可达的对象和不可达的对象,然后将不
可达的对象回收。
缺点是:效率低.回收得到的空间不连续
3.2 复制算法
将内存分为两块,每次只使用一块。当这一块内存满了,就将还存活的对象
复制到另一块上,并且严格按照内存地址排列,然后把已使用的那块内存统一回
收。
优点是:能够得到连续的内存空间
缺点是:浪费了一半内存
3.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对
象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外
的内存,“标记-整理”算法的示意图如下图所示。
标记-整理算法是一种老年代的回收算法,它在标记-清除算法的基础上做了
一些优化。也首先需要从根节点开始对所有可达对象做一次标记,但之后,它并
不简单地 清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。
3.3 分代算法
在 java 中,把内存中的对象按生命长短分为:
新生代:活不了多久就 go die 了,比如局部变量
老年代:老不死的,活的久但也会 go die,比如一些生命周期长的对象
永久代:千年王八万年龟,不死,比如加载的 class 信息
有一点需要注意:新生代和老年代存储在 java 虚拟机堆上 ;永久代存储
在方法区上
补充:java finalize()方法:
在被 GC 回收前,可以做一些操作,比如释放资源。有点像析构函数,但是
一个对象只能调用一次 finalize()方法。
2. Java 集合框架
2.1 ArrayList 与 LinkedList 异同
1. 是否保证线程安全: ArrayList 和 LinkedList 都是不同步的,也就是不
保证线程安全;
2. 底层数据结构: ArrayList 底层使用的是 Object 数组;LinkedList 底
层使用的是双向链表数据结构(JDK1.6 之 前为循环链表,JDK1.7 取消了循环。
注意双向链表和双向循环链表的区别:);
3. 插入和删除是否受元素位置的影响: ① ArrayList 采用数组存储,所
以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行 add(E e) 方法的时候, ArrayList 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话( add(int index, E element) )时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② LinkedList 采用链表存储,所以插入,删除元素时间复杂度不受元素位置的影响,都是近似 O(1)而数组为近似 O(n)。
4. 是否支持快速随机访问: LinkedList 不支持高效的随机元素访问,而
ArrayList 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于
get(int index) 方法)。
5. 内存空间占用: ArrayList 的空 间浪费主要体现在在 list 列表的结尾会
预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需
要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
补充内容:RandomAccess 接口
public interface RandomAccess {
}
查看源码我们发现实际上 RandomAccess 接口中什么都没有定义。所以,
在我看来 RandomAccess 接口不过是一个标识罢了。标识什么? 标识实现这
个接口的类具有随机访问功能。
在 binarySearch()方法中,它要判断传入的 list 是否 RamdomAccess
的实例,如果是,调用
indexedBinarySearch()方法,如果不是,那么调用 iteratorBinarySearch()方法
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
ArrayList 实现了 RandomAccess 接口, 而 LinkedList 没有实现。为
什么呢?我觉得还是和底层数据结构有关!ArrayList 底层是数组,而
LinkedList 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所
以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间
复杂度为 O(n),所以不支持快速随机访问。,
ArrayList 实现了 RandomAccess 接口,就表明了他具有快速随机访问功
能。 RandomAccess 接口只是标识,并不是说 ArrayList 实现
RandomAccess 接口才具有快速随机访问功能的!
下面再总结一下 list 的遍历方式选择:
实现了 RandomAccess 接口的 list,优先选择普通 for 循环 ,其次 foreach, 未实现 RandomAccess 接口的 list, 优先选择 iterator 遍历(foreach 遍历底
层也是通过 iterator 实现的),大 size 的数据,千万不要使用普通 for 循环.补
充:数据结构基础之双向链表
双向链表也叫双链表,是链表的一种,它的每个数据结点中都有两个指针,
分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可
以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表,如
下图所示,同时下图也是 LinkedList 底层使用的是双向循环链表数据结构。
2.2 ArrayList 与 Vector 区别
Vector 类的所有方法都是同步的。可以由两个线程安全地访问一个 Vector
对象.但是一个线程访问 Vector 的话代码要在同步操作上耗费大量的时间。
ArrayList不是同步的,所以在不需要保证线程安全时时建议使用ArrayList。
2.3 HashMap 的底层实现
JDK1.8 之前
JDK1.8 之前 HashMap 底层是 数组和链表</