3、Java的异常体系
https://www.jianshu.com/p/49d2c3975c56
4、Java中实现多态的机制是什么?
多态就是指一个引用变量倒底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
因为在程序运行时才确定具体的类,这样,不用修改源程序代码,就可以让引用变量绑定到各种不同的类实现上,从而导致该引用调用的具体方法随之改变,即不修改程序代码就可以改变程序运行时所绑定的具体代码,让程序可以选择多个运行状态,这就是多态性。
特点:
- 指向子类的父类引用由于向上转型了,它只能访问父类中拥有的方法和属性,而对于子类中存在而父类中不存在的方法,该引用是不能使用的,尽管是重载该方法。
- 若子类重写了父类中的某些方法,在调用该些方法的时候,必定是使用子类中定义的这些方法(动态连接、动态调用)。
Java实现多态有三个必要条件:继承、重写、向上转型。
调用的优先级方法,该优先级为:this.show(O)、super.show(O)、this.show((super)O)、super.show((super)O)。
多态的实现原理
对象方法的调用是依靠类信息里的方法表实现的。
总体而言,当调用对象某个方法时,JVM查找该对象类的方法表以确定该方法的直接引用地址,有了地址后才真正调用该方法。
超类继承父类的方法,如果不Overriding该方法,那么调用时会指向父类的方法。如果Overrding该方法,那么指向该类的代码区。
但是超类会存有父类的方法表。
我们知道java程序运行时,类的相关信息放在方法区,在这些信息中有个叫方法表的区域,该表包含有该类型所定义的所有方法的信息和指向这些方法实际代码的指针
当Bird、Cock、Parrot和CrazyParrot这四个类被加载到 Java 虚拟机之方法区后,方法区中就包含了这四个类的信息,下图示例了各个类的方法表。
从图我们可以看到Cock、Parrot和CrazyParrot的类信息方法表包含了继承自Bird的方法。CrazyParrot的方法表包含了继承自Parrot的方法。此外各个类也有自己的方法。
注意看,方法表条目指向的具体方法代码区。对于多态Overriding的方法courtship(),虽然Cock、Parrot和CrazyParrot的方法表里的courtship()条目所在位置是属于继承自Bird方法表的部分,但指向不同的方法代码区了。
5、说说你对Java反射的理解
什么是java的反射机制?
反射(Reflection)机制:其实就是程序在运行的时候可以获取任意一个类的内部信息,也可以说是动态获取吧。
那么我们怎么利用反射(Reflection)去获取类的相关信息呢?
首先我们必须要了解什么是静态加载类,什么是动态加载类?
静态加载类:其实说的通俗一点的话,就是程序编译的的过程,这就是静态加载类。
动态加载类:其实说的就是程序运行的时候,可以说动态加载。
简单一点的说法就是:编译--------静态 运行---------动态
我们在控制台运行java程序的时候是不是要执行一下:javac *.java(其实这个时候会生成一个该类的.class文件) 然后再java *(这个时候其实运行加载的就是这个.class文件),而所谓的动态加载,就是绕过编译,在调用的时候就运行。下面通过一个简单的例子来说明一下:
比如我新建一个类:
- package com.mytest01;
- public class showObj {
- public void showObject(){
- Person p = new person();
- system.out.println(p);
- }
还没有运行的时候在eclispe等软件是不是会提示报错?然后你在控制台中编译是不是不通过?说找不到这个Person这个类?
那么我们稍微改一下代码看看是否还会出错:
package com.mytest01;
public class showObj {
public void showObject(){
Class p = person.class;
System.out.println(p);
}
class person{
}
}
看编译的时候还会不会出错?
下面我们再来谈一下我们到底应该怎么去获取类的内部信息呢?
在获取类的内部信息之前,我想说的是一点点面向对象的思想。首先什么是对象?我们常说一个实体就是一个对象,那么我们在想想java里面我们新建的类是不是一个对象呢?你可能会有疑问?对象不就还是类的实例化吗?为什么说类也是一个对象?其实也不难理解,因为我们所有新建的类其实都是java里面java.lang.Class的实例对象来的,新建一个类,其实就是实例化java.lang.Class类;
明白了这一点,我们就可以继续的往下走了,下面我们就来谈谈怎么去获取一个类的内部信息:
第一步:获取该类的类类型(这一步非常关键)
那么我们怎么去获取呢:
其实很简答,一共有三种方法:
1.利用Class c = Class.forName("")----------传就来要获取类的路径,会出现异常。
2.利用Class c = A.class ---------------------A代表该类的类名
3.利用Class c = a.getClass();---------------a代表的就是该类的实例化对象,也就是 A a = new A();这一步
通过上面的方法我们就获取了该类的一个实例对象,那么我们怎么调用类里面的成员函数,成员变量呢?(注意都是public的)如果要访问私有的要在你要获取的变量后加上setAccessible(true);例如 Filed[] filed = c.getFields(); field.setAccessible(true);
其实还是很简单:
1.c.getName()-------------------获取该类的类名;返回String类型的值
2.c.getFileds()------------------------获取该类的所有成员变量 c.getDeclaredFields()--------自己声明的类的成员变量;
3.c.getType()---------------------------获取该类的类型
4.c.getMethods()----------------------获取所有的方法 c.getDeclaredMethod()-----------所有声明过的方法。
.......
.....
..
还有很多可以去查阅官方的文档。
那么我们怎么反向的去执行方法呢?
分为三步:
第一:获取类的;类类型
Class c = a.getClass();
第二步:获取类的方法
Method m = c.getMethod("方法的名称",参数的类类型new Class[]{...,.....});
第三部:传入参数,利用invoke()函数
Object o = m.invoke(a(实例化对象),执行函数要传进来的参数);
6、说说你对Java注解的理解
注解,也叫元数据。一种代码级别的说明,在JDK1.5之后引入的特性,与类、接口、枚举同一层次。可以声明在包、类、字段、方法、局部变量、方法参数等前面,来对这些元素进行说明,注释等。
作用分类:
1)编写文档:通过代码里的标识的元数据生成文档【生成文档doc文档】
2)代码分析:通过代码里的标识的元数据对代码进行分析【使用反射】
3)编译检查:通过代码里的标识的元数据让编译器能过实现基本的编译检查【Override】
元注解:
java提供了四个元注解,所谓元注解就是负责注解其他注解。
1.@Target :规定注解所修饰的对象范围。
1)ElementType.CONSTRUCTIR; 构造器声明
2)ElementType.FIELD; 成员变量,对象,属性(包括enum实例)
3)ElementType.LOCAL_VARIABLE; 局部变量声明
4)ElementType.METHOD ; 方法声明
5)ElementType.PACKAGE; 包声明
6)ElementType.PARAMETER;参数声明
7)ElementType.TYPE; 类、接口(包括注解类型)或enum声明
2.@Retention : 表示注解的生命周期
1)RetentionPolicy.SOUREC: 在源文件中有效
2)RetentionPolicy.CLASS; 在class文件中有效
3)RetentionPolicy.RUNTIME;在运行时有效
3.@Inherited : 标记注解,主要说明了一种继承性,意思是子类可以继承父类中的该注解(注意:只有当被贴上@Inherited标签的注解被用在类上的时候子类才能获得这个注解)。
3.@Documented : 用于描述其它类型的annotation应该被作为被标注的程序成员的公共API,因此可以被例如javadoc此类的工具文档化。
7、java 控制反转和依赖注入的理解
控制反转(IOC)是Spring框架的核心思想,用我自己的话说,就是你要做一件事,别自己可劲new了,你就说你要干啥,然后外包出去就好~
依赖注入(DI) 在我浅薄的想法中,就是通过接口的引用和构造方法的表达,将一些事情整好了反过来传给需要用到的地方~
这样做得好处:做到了单一职责,并且提高了复用性,解耦了之后,任你如何实现,使用接口的引用调用的方法,永远不需要改变
举一个栗子:
写个接口,说咱们购物去~
public interface IShopping { void drive(); String Money(); }
在实现它两下:有钱人购物,没钱人购物 - -!这栗子举的
public class RichMan implements IShopping{
@Override
public void drive() {
System.out.println("Drive By Benz");
}
@Override
public String Money() {
System.out.println("lot`s of money");
return "10000";
}
}
然后我们出去玩,顺便shopping一下吧~
public class Play { private IShopping shoppingSomething;
//使用构造方法,将实现传入 public Play(IShopping shoppingSomething){ this.shoppingSomething = shoppingSomething; } public void withgirlFriend(){ shoppingSomething.drive(); shoppingSomething.Money(); } }
将想用的实现方式,用容器注入进来,这里就模拟下下怎么注入:
public class Containner { public Play getShopping(){ return new Play(new RichMan()); } }
8、说一下泛型原理,并举例说明
https://www.cnblogs.com/xunzhi/p/5683709.html
9、String为什么要设计成不可变的?
- 字符串常量池的需要
字符串常量池(String pool, String intern pool, String保留池) 是Java堆内存中一个特殊的存储区域, 当创建一个String对象时,假如此字符串值已经存在于常量池中,则不会创建一个新的对象,而是引用已经存在的对象。
如下面的代码所示,将会在堆内存中只创建一个实际String对象.
String s1 = "abcd";
String s2 = "abcd";
示意图如下所示:
假若字符串对象允许改变,那么将会导致各种逻辑错误,比如改变一个对象会影响到另一个独立对象. 严格来说,这种常量池的思想,是一种优化手段.
请思考: 假若代码如下所示,s1和s2还会指向同一个实际的String对象吗?
String s1= "ab" + "cd";
String s2= "abc" + "d";
也许这个问题违反新手的直觉, 但是考虑到现代编译器会进行常规的优化, 所以他们都会指向常量池中的同一个对象. 或者,你可以用 jd-gui 之类的工具查看一下编译后的class文件.
- 允许String对象缓存HashCode
Java中String对象的哈希码被频繁地使用, 比如在hashMap 等容器中。
字符串不变性保证了hash码的唯一性,因此可以放心地进行缓存.这也是一种性能优化手段,意味着不必每次都去计算新的哈希码. 在String类的定义中有如下代码:
private int hash;//用来缓存HashCode
- . 安全性
String被许多的Java类(库)用来当做参数,例如 网络连接地址URL,文件路径path,还有反射机制所需要的String参数等, 假若String不是固定不变的,将会引起各种安全隐患。
假如有如下的代码:
boolean connect(string s){
- if (!isSecure(s)) {
- throw new SecurityException();
- }
- // 如果在其他地方可以修改String,那么此处就会引起各种预料不到的问题/错误
- causeProblem(s);
- }
10、Object类的equal和hashCode方法重写,为什么?
https://blog.csdn.net/m0_37462976/article/details/77866572
(三) 数据结构
1、常用数据结构简介
数据结构(也称为集合类)大致分类如下:
Map图接口(包含HashMap和TreeMap);
Collection集合接口(包含List接口和Set接口):
List线性表接口(包含ArrayList和LinkedList);
Set集合接口(包含HashSet和TreeSet);
2、并发集合了解哪些?
非阻塞式列表对应的实现类:ConcurrentLinkedDeque
阻塞式列表对应的实现类:LinkedBlockingDeque
用于数据生成或者消费的阻塞式列表对应的实现类:LinkedTransferQueue
按优先级排序列表元素的阻塞式列表对应的实现类:PriorityBlockingQueue
带有延迟列表元素的阻塞式列表对应的实现类:DelayQueue
非阻塞式列表可遍历映射对应的饿实现类:ConcurrentSkipListMap
随机数字对应的实现类:ThreadLockRandom
原子变量对应的实现类:AtomicLong和AtomicIntegerArray
3、列举java的集合以及集合之间的继承关系
集合类存放的都是对象的引用,而非对象本身,出于表达上的便利,我们称集合中的对象就是指集合中对象的引用(reference)。集合类型主要有3种:set(集)、list(列表)和map(映射)。
Collection和Map最大的区别就是Collection存储的是一组对象;Map是以“键值对”的形式对对象进行的管理。
Iterator是迭代器,Iterable是接口。很多类,像List、Set、HashMap不直接实现迭代器接口Iterator,而是去实现Iterable接口。
Collection是一个集合接口。它提供了对集合对象进行进本操作的通用接口方法。
Collections是一个工具类。内有多个对集合对象进行操作的静态方法,不能实例化。
4、集合类以及集合框架
集合是一组复合元素的容器,用来存储,检索,控制聚合数据并提供它们之间的通信方法。
java的集合框架提供了表示和操控集合的统一架构。所有的集合框架都包含下面三个方面:
接口:即集合的抽象数据结构。接口允许我们独立地操纵集合而不用考虑集合的具体实现
实现:即接口的具体实现类。从本质上来讲,它们是可重用的数据结构
算法:即在实现了集合接口的对象上执行有用的计算,比如排序和搜索,的方法。算法是多态的:同名的方法可以被任何合适的接口实现类调用,从本质上来讲,算法是可重用的功能
核心集合接口封装了不同类型的集合,它们是java集合框架的基础,形成了下图所示的层级结构
从上图中可以看到,Set是一种特殊的Collection,而SortedSet是一种特殊的Set……诸如此类。
需要注意,上图中有两个不同的树,Map并不是Collection的子接口,而是一个独立的接口
所有的集合接口都是泛化的。比如下面是Collection接口的声明:
public interface Collection<E>...
“<E>”表示该接口是通用的。当我们声明一个Collection接口时,最好指定该接口包含的对象类型,以便让编译器在编译时检验输入的对象是否正确,从而减少运行时抛出的错误。
对于接口中的方法,有很多是可选择实现的,就是说,它的实现类可以实现该方法,也可以不实现,根据具体需要来决定
我们先来看一下核心的集合接口,对它们有一个整体的感性认识:
Collection接口:集合框架的根接口。它是集合类框架中最具一般性的顶层接口。Java平台没有提供任何该接口的直接具体实现类,但是提供了具有各种不同特性的子接口
Set接口:不允许包含重复值的集合
List接口:可索引的集合,可以包含重复值。使用该接口时我们通过索引对元素进行精准的插入和查找
Queue接口:该集合适用于组织一个队列,队列中的元素按照优先级进行处理。除了继承自Collection接口的方法,该接口还提供了另外的插入、提取和检验方法。典型的队列是符合“先进先出”(FIFO:First In,First Out)原则的,优先级队列是一种例外,它按照元素的优先级顺序排列元素。无论按照什么原则排序,队头元素总是首先被检出。每个Queue接口的实现类必须指定它的排序原则
Deque接口:与Queue的不同之处在于它是一个双端队列,在两端都能插入和移除元素,它继承并扩展了Queue接口
Map接口:提供了键值对(key/value)的映射关系的集合。关键字不能有重复值,每个关键字至多可映射一个值
SortedSet接口:以升序的原则维持着集合中的元素顺序。
SortedMap接口:以关键字升序的原则维持着集合中的元素顺序
以上接口的通用实现类(这里的通用实现类一般是指适用于单线程环境下的实现类,在JDK中有针对多线程并发环境下的特殊实现类)总结如下
可以发现,LinkedList同时实现了List、Queue、Deque三个接口。
SortedSet接口和SortedMap接口没有在上表中列出,在上面的层次结构图中可以看到,它们分别是Set和Map的子接口,TreeSet和TreeMap就是它们的实现类
以上提到的所有通用实现类都支持为null的元素(或者键/值),不过有的只能包含一个null,有的可以包含多个null;所有通用实现类都实现了Serializable,是可序列化的对象;所有通用实现类都提供了一个clone方法用于复制对象;所有通用实现类都是线程不安全的(即是不同步的);所有通用实现类都提供了”fail-fast”机制的迭代器
关于“fail-fast”机制,来看一个实例:
Map<String,String> map = new HashMap<String,String>();
map.put("first", "Jay");
map.put("second","Jack");
map.put("third", "Jim");
Iterator<String> it= map.keySet().iterator();
while(it.hasNext()){
System.out.println(map.get(it.next()));
if(map.containsKey("second")){
map.remove("second");
}
}
以上代码试图使用Iterator迭代Map里的键值,如果键值为“second”则删除Map中的该元素。运行一下可以发现,执行删除操作时会报java.util.ConcurrentModificationException异常,即便这是单线程。
Iterator 是工作在一个独立的线程中,并且拥有一个 mutex 锁。 Iterator 被创建之后会建立一个指向原来对象的单链索引表,当原来的对象数量发生变化时,这个索引表的内容不会同步改变,所以当索引指针往后移动的时候就找不到要迭代的对象,所以按照 fail-fast 原则 Iterator 会马上抛出 java.util.ConcurrentModificationException 异常。
如果想要成功的执行删除操作,就需要先对Iterator进行对应的删除操作
Map<String,String> map = new HashMap<String,String>();
map.put("first","Jay");
map.put("second","Jack");
map.put("third","Jim");
Iterator<String> it= map.keySet().iterator();
while(it.hasNext()){
System.out.println(map.get(it.next()));
if(map.containsKey("second")){
it.remove(); //先对Iterator进行删除
map.remove("second");
}
}
5、容器类介绍以及之间的区别
容器类估计很多人没听这个词,Java容器主要可以划分为4个部分:List列表、Set集合、Map映射、工具类(Iterator迭代器、Enumeration枚举类、Arrays和Collections)
Collection是List和Set两个接口的基接口
List在Collection之上增加了"有序"
Set在Collection之上增加了"唯一"
ArrayList是实现List的类...所以他是有序的。它里边存放的元素在排列上存在一定的先后顺序,是采用数组存放元素 。
List LinkedList采用的则是链表。
Collection和Map接口之间的主要区别在于:
Collection中存储了一组对象,而Map存储关键字/值对(在Map对象中,每一个关键字最多有一个关联的值)。
Map:不能包括两个相同的键,一个键最多能绑定一个值。null可以作为键,这样的键只有一个;可以有一个或多个键所对应的 值为null。当get()方法返回null值时,即可以表示Map中没有该键,也可以表示该键所对应的值为null。因此,在Map中不能由get()方法来判断Map中是否存在某个键,而应该用containsKey()方法来判断。
继承Map的类有:HashMap,HashTable
HashMap:Map的实现类,缺省情况下是非同步的,可以通过Map Collections.synchronizedMap(Map m)来达到线程同步
HashTable:Dictionary的子类,确省是线程同步的。不允许关键字或值为null
6、List,Set,Map的区别
- list和set是继承了collection接口,而map不是
- list可以存放有序重复元素
- set可以存放无序不重复元素
- map可以存放键值对
7、List和Map的实现方式以及存储方式
List:
常用实现方式有:ArrayList和LinkedList
ArrayList 的存储方式:数组,查询快
LinkedList的存储方式:链表,插入,删除快
Set:
常用实现方式有:HashSet和TreeSet
HashSet的存储方式:哈希码算法,加入的对象需要实现hashcode()方法,快速查找元素
TreeSet的存储方式:按序存放,想要有序就要实现Comparable接口
附加:
集合框架提供了2个实用类:collections(排序,复制、查找)和Arrays对数组进行(排序,复制、查找)
Map:
常用实现方式有:HashMap和TreeMap
HashMap的存储方式:哈希码算法,快速查找键值
TreeMap存储方式:对键按序存放
8、HashMap的实现原理
HashMap基于hashing原理,我们通过put()和get()方法储存和获取对象。当我们将键值对传递给put()方法时,它调用键对象的hashCode()方法来计算hashcode,让后找到bucket位置来储存值对象。当获取对象时,通过键对象的equals()方法找到正确的键值对,然后返回值对象。HashMap使用链表来解决碰撞问题,当发生碰撞了,对象将会储存在链表的下一个节点中。 HashMap在每个链表节点中储存键值对对象。
当两个不同的键对象的hashcode相同时会发生什么? 它们会储存在同一个bucket位置的链表中。键对象的equals()方法用来找到键值对。
因为HashMap的好处非常多,我曾经在电子商务的应用中使用HashMap作为缓存。因为金融领域非常多的运用Java,也出于性能的考虑,我们会经常用到HashMap和ConcurrentHashMap。
9、HashMap数据结构?
基本结构:
数组+单向链表
元素:
Node,包含key value Node(next)
基本限制
数组默认长度:1<<4 即 16
数组最大值 :1<<30
默认加载因子:0.75 ------ 数组容量*0.75 = 12 (使用大小触发点:使用到12时做扩大数组容量的操作)
链表变形为红黑树的触发大小:8 -------- 注:变形目的是降低查找的时间复杂度。
红黑树变形为链表的触发大小:6
添加元素的方式
先存数组,如果重叠则往下存成链表
1.计算出在数组的位置
目的:得到0-15的某个值,方式:哈希算法,通过key的hashCode取数组大小的模-------key.hashCode()%数组大小=下标值(即数组位置)
源码中的具体细节
1.如果为空则resize() 初始化
2.根据hash进行运算,
3.node中的hash记录
10、HashMap源码理解
https://www.jianshu.com/p/d4fee00fe2f8
11、HashMap怎么手写实现?
https://www.jianshu.com/p/b638f19aeb64
12、ArrayMap和HashMap的对比
HashMap和ArrayMap各自的优势
1.查找效率
HashMap因为其根据hashcode的值直接算出index,所以其查找效率是随着数组长度增大而增加的。
ArrayMap使用的是二分法查找,所以当数组长度每增加一倍时,就需要多进行一次判断,效率下降。
所以对于Map数量比较大的情况下,推荐使用
2.扩容数量
HashMap初始值16个长度,每次扩容的时候,直接申请双倍的数组空间。
ArrayMap每次扩容的时候,如果size长度大于8时申请size*1.5个长度,大于4小于8时申请8个,小于4时申请4个。
这样比较ArrayMap其实是申请了更少的内存空间,但是扩容的频率会更高。因此,如果当数据量比较大的时候,还是使用HashMap更合适,因为其扩容的次数要比ArrayMap少很多。
3.扩容效率
HashMap每次扩容的时候时重新计算每个数组成员的位置,然后放到新的位置。
ArrayMap则是直接使用System.arraycopy。
所以效率上肯定是ArrayMap更占优势。
这里需要说明一下,网上有一种传闻说因为ArrayMap使用System.arraycopy更省内存空间,这一点我真的没有看出来。arraycopy也是把老的数组的对象一个一个的赋给新的数组。当然效率上肯定arraycopy更高,因为是直接调用的c层的代码。
4.内存耗费
以ArrayMap采用了一种独特的方式,能够重复的利用因为数据扩容而遗留下来的数组空间,方便下一个ArrayMap的使用。而HashMap没有这种设计。
由于ArrayMap只缓存了长度是4和8的时候,所以如果频繁的使用到Map,而且数据量都比较小的时候,ArrayMap无疑是相当的节省内存的。
5.总结
综上所述,
数据量比较小,并且需要频繁的使用Map存储数据的时候,推荐使用ArrayMap。
而数据量比较大的时候,则推荐使用HashMap。
13、HashMap和HashTable的区别
HashMap不是线程安全的,效率高一点、方法不是Synchronize的要提供外同步,有containsvalue和containsKey方法。
hashtable是,线程安全,不允许有null的键和值,效率稍低,方法是是Synchronize的。有contains方法方法。Hashtable 继承于Dictionary 类
https://www.cnblogs.com/williamjie/p/9099141.html
14、HashMap与HashSet的区别
HashSet:
HashSet实现了Set接口,它不允许集合中出现重复元素。当我们提到HashSet时,第一件事就是在将对象存储在
HashSet之前,要确保重写hashCode()方法和equals()方法,这样才能比较对象的值是否相等,确保集合中没有
储存相同的对象。如果不重写上述两个方法,那么将使用下面方法默认实现:
public boolean add(Object obj)方法用在Set添加元素时,如果元素值重复时返回 "false",如果添加成功则返回"true"
HashMap:
HashMap实现了Map接口,Map接口对键值对进行映射。Map中不允许出现重复的键(Key)。Map接口有两个基本的实现
TreeMap和HashMap。TreeMap保存了对象的排列次序,而HashMap不能。HashMap可以有空的键值对(Key(null)-Value(null))
HashMap是非线程安全的(非Synchronize),要想实现线程安全,那么需要调用collections类的静态方法synchronizeMap()实现。
public Object put(Object Key,Object value)方法用来将元素添加到map中。
HashSet与HashMap的区别:
15、ArrayList和LinkedList的区别,以及应用场景
区别
ArrayList底层是用数组实现的,可以认为ArrayList是一个可改变大小的数组。随着越来越多的元素被添加到ArrayList中,其规模是动态增加的。
LinkedList底层是通过双向链表实现的, LinkedList和ArrayList相比,增删的速度较快。但是查询和修改值的速度较慢。同时LinkedList还实现了Queue接口,所以他还提供了offer(),peek(), poll()等方法。
在内存中这两者也是有区别的,因为底层分别用数组和链表实现的,所以在内存中ArrayList是连续的,而linkedList在底层中是用链表实现的所以在内存中可以不是连续的内存。
使用场景
LinkedList更适合大量的循环,并且循环时进行插入或者删除。
ArrayList更适合大量的存取和删除操作