一)为什么要使用克隆?
克隆指的是创建一个对象的副本,使得副本具有和原始对象相同的属性和状态,在计算机编程中,克隆是一种常见的操作,用于复制数据对象和数据结构,以便在不影响原始数据结构的情况下进行操作和修改和分发
1)实现原型设计模式,假设现在在项目中设置了Bean的作用域Scope等于prototype,这是典型的使用克隆的案例,克隆在设计模式中有一个很重要的角色,就是原型设计模式,原型设计模式用于对象的克隆,以用来避免创建复杂对象,她通过复制现有对象来创建新对象,从而减少了对象的构造时间和资源消耗
2)备份与恢复:在某一些情况下,我们可能需要对对象进行备份,来防止后续过程中对其修改导致数据缺失,通过克隆原始对象,我们可以在需要的时候恢复原始状态,从而避免数据丢失,比如说IDEA设置一样的功能,你可以在任何时刻设置恢复原始状态,之所以能够恢复原始状态是因为再进行设置的是克隆的而不是直接修改的原信息,假设此时这个系统是支持用户进行配置的,是支持用户设置皮肤的颜色,设置系统的快捷键,此时就需要使用原型设计模式,不能自己设置把别人的设置都给改了,还不能修改默认的设置,就是为了防止用户有一天进行reset操作,这个时候就需要针对于原来的系统默认设置克隆出一份,然后在克隆的新系统上进行修改,这样即使有一天回退到历史版本也是十分方便的
二)克隆的分类:
深克隆:
package DButil;import java.lang.reflect.Field; class Money implements Cloneable{ public int money=10; @Override protected Object clone() throws CloneNotSupportedException { Money m= (Money) super.clone(); return m; } } class Person implements Cloneable { Money m=new Money(); @Override protected Object clone() throws CloneNotSupportedException { Person p = (Person) super.clone(); p.m= (Money) p.m.clone(); return p; } } public class HelloWorld{ public static void main(String[] args) throws CloneNotSupportedException, NoSuchFieldException, IllegalAccessException { Person person1=new Person(); Person person2= (Person) person1.clone(); person1.m.money=900; System.out.println(person1.m.money); System.out.println(person2.m.money); } }
三)深克隆实现的原理是什么,如何实现深克隆?
深克隆实现的原理是先将原始对象序列化到内存中的字节流中,再从字节流中反序列化出刚刚存储的对象,这个新对象就和老对象不会存在任何地址上的共享,这样就实现了深克隆
1)手动实现深克隆,手动进行遍历对象的引用成员变量,并对其进行克隆操作
2)使用序列化和反序列化,将对象写入到字节流(序列化),然后从字节流中读取对象(反序列化),就是第三种方式实现的底层原理;
3)使用第三方库,JSON序列化工具;
StringBuilder sb=new StringBuider(); sb.append("123"); sb.append("456"); 这个过程中始终都是一个stringBuilder对象
四)如何实现序列化?
序列化就是把对象的状态信息转化为可存储或传输的形式过程,也就是把对象转化为字节序列的过程称为对象的序列化
对象序列化的两种用途:
1)对象的持久化:将对象的字节序列永久的保存在磁盘上面,比如说存放在一个文件里面
2)网络上传送对象的字节序列,可以通过序列化把主机A进程上的对象序列化为二进制序列,传输到主机B上的进程从序列中重构出该对象;
当前类非敏感类,非当前组件进行交互,网络,redis
1)实现Serializable接口,Serializable接口本身没有任何的实现,就是一个标记接口,用于告诉编译器和运行时环境这个类可以被序列化
2)设置一个唯一标识的serialVersionUID,serialVersionUID,是用来进行版本控制的静态字段,是为了防止不同版本类冲突,以及类安全的,具体来说当进行反序列化的时候,JVM就会将传过来的字节流中的serialVersionUID和本地相应的实体类的serialVersonID来做比较,如果是相同的就认为是一致的,可以进行反序列化,如果对应的serialVersionUID不相同,那么就可能会出现序列化版本不一致的异常,那么就是InvaildCastException,这样做就是为了保证安全,否则文件存储的内容可能被修改,举个例子,1.0版本的时候有一个name字段,并且name存在数据,然后改成nickname,此时就需要验证传输字节流的版本和当前本地的版本是否一致,如果版本不一致可能解析失败,如果没有版本信息,序列化数据可能会出错
如果文件流中的serialVersionID和本地的ID不相同,程序判断会直接报错,那么说明要么接口变了,要么存储的数据被篡改了,就不可以进行解析了,给IDEA进行配置,会给每一个类生成一个UID,这个UID要是IEDA自动生成的,防止UID太简单被别人猜出来;
3)给不想要序列化的字段前面加上一个修饰字段transient,过滤一些敏感信息
五)为什么一定要实现Serializable接口才能被序列化?
如果所有的默认的类都可以被序列化和反序列化的话,那么会存在性能和安全问题
1)安全问题:假设如果所有默认的类都可以被序列化,有一些类里面有敏感信息,但是此时默认就可以被序列化,可能会造成敏感信息泄露的问题,所以需要开发者指明哪些类可以被序列化,从而来保证安全性,那么所有对象通过序列化存储到硬盘上后,都可以在序列化得到的文件中看到属性对应的值,所以最后为了安全性(即不让一些对象中私有属性的值被外露),不能让所有对象都可以序列化。要让用户自己来选择是否可以序列化,因此需要一个接口来标记该类是否可序列化。
2)性能开销:序列化和反序列化会涉及到复杂的字节转换和信息处理
在JAVA中实现Serializable这个接口是为了支持对象的序列化和反序列化操作,这个接口是JAVA提供的一个标记接口,没有定义任何方法,只是起到一个标记作用,当一个类实现了Serializable接口的时候证明这个类可以被序列化成字节流,或者是从字节流反序列化成对象
可以确保那些被设计成可序列化的类的对象才可以被序列化,这是一种安全性的保障,防止程序员犯蠢,对那些不可以进行序列化的对象进行序列化操作
java中到底是值传递还是引用传递:
值传递:不会修改原来的实参里面存放的内容,无论形参如何变化,实参都是不会改变的
引用传递:
会修改原来的实参里面存放的内容(基本数据类型改变的是值,引用数据类型会改变地址)
JAVA中还是值传递,因为引用传递?
Java中的参数传递,到底是值传递还是引用传递?_ava是值传递还是引用传递-CSDN博客
六)JAVA中的异常有几种?它们有什么区别呢?
在我们的Java中,将程序执行过程中发生的不正常行为称为异常,Java不同类型的异常,都有对应的类来进行描述,程序发生异常本质上是new了一个异常对象然后抛出去,再由我们的程序和JVM处理
异常的概念和体系结构
1)Throwable:是我们异常体系的顶层类,这样就派生出两个重要的子类,Error和Exception
在Java中,所有的异常类型都是java.lang.Throwable的子类,在Throwable下面有两个异常的分支,一个是Exception一个是error;
2)Error:指的是Java虚拟机都无法进行解决的严重问题,JVM内部错误
指的是java运行时的内部错误和资源耗尽错误,应用程序不会抛出此类异常,这种内部错误一旦出现,除了告知用户程序终止之外,其他的也无能为力,例如堆栈溢出是这种错误的一例StackOverflowError,最后必须由程序原来自己进行解决,出现error的时候是没有办法在程序运行期间进行处理的,只有出现这个问题,通过日志在对代码进行修改;
3)Exception:异常产生后端程序员可以通过代码来进行处理,从而使我们的程序可以继续执行
在JAVA中异常分为两种:
1)checkedException:是继承于java.lang.Exception类,通常表示外部环境因素导致的错误或者是其他异常情况,比如说文件没有找到,网络连接失败等等,开发者在编码过程中一定要处理这些异常,在方法的声明中必须要显示的声明或者捕获,否则编译器就会报错的
IOException,ClassNotFoundException,CloneNotSupportedException
但是IOException又可以分为EOFException,FileNotFoundException
2)UnchekedException:
RuntimeException 类及其子类异常,例如NullPointerException,ClassNotFoundException,IndexOutOfBoundsException,ArithmeticException,这是点击运行程序之后抛出的异常,这些异常是不检查异常,程序中可以选择捕获处理,也可以不处理,这些异常一般由程序逻辑错误引起,程序应该从逻辑角度尽可能避免这类异常的发生
就是类型不能转换的异常,学习接口的时候,自定义类型发现不能转化成Compareable[]类型,会出现类型转换异常,没有实现比较接口
throw和throws两者都是消极处理异常的方式,只是抛出或者可能抛出异常,但是不会由方法去处理异常,真正的处理异常由方法的上层调用处理,throw一般是抛出自定义异常
七)==和equals有什么区别?
Object中的equals和==没有区别,但是大部分类都重写equals来实现对象中的内容的比较,就比如说String类来说==仍然比较的是内存地址,但是equals是比较字符串中的内容是否相等
八)final关键字
1)使用final修饰的类,就不能被其他类继承了,表示此类设计得很完美,不需要进行被修改和扩展
2)final修饰的方法表示不允许此类继承的类重写该方法,表示此方法提供的功能已经满足当前需求,不需要进行扩展
3)final修饰变量的时候,表示该属性一旦被初始化之后就不可以被修改
4)修饰参数的时候,此参数在整个方法内部不允许被修改,修饰实参
九)finally语句块一定会被执行吗?当然不一定
finally更多用于try finally和try catch finally来进行关闭类似于JDBC连接和保证释放锁等动作
1)try语句没有执行到,如果在try语句执行之前就return返回了,这样finally语句块就不会执行
2)当一个线程再进行执行try语句块的时候或者catch语句块的时候被打断(interrupted)或者被终止(killed),那么与其相应的finally代码可能不会被执行;
3)当我们的try语句块或者catch语句块里面有System.exit(0)这样的语句块,主要是终止JAVA虚拟机的,连JVM都停止了,所有都结束了
4)finally代码块实在守护线程里面,所有线程都执行完成了,那么守护线程就会立即停止,退出JAVA虚拟机
此外我们还需要注意一下在我们的try-catch-finally语句块里面
1)try catch 语句块里面有return,finally也会被执行
2)finlly语句块里面有return语句会把try catch语句块里面的return 语句效果给覆盖掉
装饰器模式:对目标对象中的方法进行功能性增强
RPC内部系统的调用
try catch的工作过程:
1)程序会先进行执行try中的代码
2)如果try中的代码出现了异常,就会立即结束try中的代码,看看是否与catch中的异常类型是否匹配,如果能够找到可以进行匹配的异常类型,那么就会执行catch里面的代码
3)如果没有找到匹配的异常类型,就会沿着异常信息调用栈异常向上传递到上层调用者;
4)无论是否找到匹配的异常类型,无论是否出现了异常,finally中的代码都会被执行掉,在该方法结束之前执行
5)如果上层调用者也没有处理了的异常,就会继续执行向上传递,一旦main()方法也没有合适的代码处理异常,就会交给JVM来进行处理,此时代码就会异常终止
九)说说finalize方法
1)当我们的对象回收的时候,系统会默认调用该对象的finalize方法,子类可以重写该方法,做一些释放资源的操作,但是finalize是Object类的方法,在JAVA中由于垃圾回收器的机制时自动回收,所以回收的时机具有不确定性,所以这个方法可能自始至终都不会被调用
2)那么什么情况下对象会被回收呢?是当某个对象没有任何引用来指向这个对象的时候,JVM就认为这个对象是一个垃圾对象,当使用垃圾回收机制销毁该对象的时候,会先调用finalize方法,垃圾回收机制的调用,是由系统来进行决定的,也是可以通过System.Gc()来触发垃圾回收机制
public class HelloWorld { static class Student{ public String username; public int age; public Student(String username, int age) { this.username = username; this.age = age; } @Override protected void finalize() throws Throwable { System.out.println("这个对象要被回收了"); } } public static void main(String[] args) { Student student=new Student("李佳伟",19); student=null; //这个时候new Student()对象就是一个垃圾,垃圾回收期就会进行回收,在进行销毁对象之前会进行调用finalize //方法,程序员就可以在finalize来写一些自己的业务逻辑代码,比如说释放资源,数据库连接,文件关闭操作 //如果程序员不重写这个方法,默认就会调用Object类的finalize,进行默认处理 System.gc(); } }
1)finalize是Object中的类的一个基础方法,他的设定是保证对象在垃圾回收之前完成特定资源的回收,但是在JDK9之前已经被默认为弃用的方法
2)但是在实际开发中并不推荐使用finalize方法,他虽然被创造出来但是无法保证这个方法一定会被执行,所以不要依赖它释放任何资源,因为他的运行极其的不稳定
3)finalize除了执行不稳定之外,还有一定的性能问题,因为finalize的设计是和垃圾收集关联到一起的,一旦出现了非空的finalize方法,就会导致对象回收呈现数量级上面的变慢,有人做过benchmark,大概是40-50倍的下降
4)所以说finalize被设计成在对象被垃圾回收之前调用,这就意味着实现finalize的方法的对象是一个特殊公民,JVM要额外对它进行处理,finalize本质上成为了快速回收垃圾的阻碍者,可能会导致你的对象经过多个垃圾回收周期才可以被使用
十)Collection与Collections的区别?
1)Collection是一个List Set和queue的上层接口
2)Collections是操作集合的工具的类,包含一些静态方法,对集合进行操作,reverse(List list):反转list中的顺序,sort(List list):对list中的顺序进行自然排序,升序
Listlist=new ArrayList<>(); System.out.println(list); list.add(1); list.add(2); list.add(3); Collections.swap(list,1,2);Object中的方法:clone(),toString(),equals(),hashcode(),wait,notify(),notifyAll(),finalize()
十一)动态代理是什么?动态代理适用的场景有哪些?
调用者想要调用目标对象的目标方法,现在不行,必须通过代理类去调用
动态代理是在程序运行期间,动态目标对象的代理对象,并将创建对象的目标中的方法进行功能性增强的一种技术,在生成代理对象的过程中,目标对象不变,代理对象中的方法是目标对象方法中的增强方法,可以理解成在运行期间,对象中方法的动态拦截,再拦截方法前后进行功能增强等操作;
保持原来方法功能不变,在方法基础上进行功能增强的设计模式叫做装饰器模式;
1)SpringAop面向切面编程,针对于某一个功能进行处理,动态代理是实现AOP的关键技术之一,通过代理可以在目标方法前后添加额外的逻辑比如说记录日志,声明式事务,在方法执行前开启事务,方法结束后提交事务,方法执行出现异常回滚事务,统一数据格式的返回
2)远程方法调用RPC:和HTTP协议一样,rpc是远程调用协议,调用远程的一些方法就是依靠代理对象,代理对象实现远程方法去调用,但是对于调用者来说就好像是用以本地方法一样,HTTP是速度比较慢,HTTP协议本质上在服务端开辟一个HTTP服务器,请求端要连接HTTP服务器,RPC将本地的依赖包拿到当前项目中使用,用于内部系统方法之间的调用和交互
十二)动态代理和静态代理有什么区别?
动态代理就是事先先写好代理类,可以手动编写一可以依靠工具生成,它的缺点是每一个业务对象都是需要生成一个代理类的,特别不灵活也不方便,每一次新加一个类,都需要写静态代理类
interface Person{ public abstract void display(); } class Father implements Person{ private String username; public Father(String username){ this.username=username; start(); } public void start(){ System.out.println("爸爸的实例已经创建完成了"); } public void display(){ System.out.println("我爱我的祖国因为这是来自于我自信心的源泉"); } } class ProxyFather implements Person{ private Father father; private String username; public ProxyFather(String username){ this.username=username; } public void display(){ if(father==null) this.father=new Father("哈哈"); System.out.println("执行前置通知"); father.display(); System.out.println("执行后置通知"); } } public class HelloWorld { public static void main(String[] args) { Person f= (Person) new ProxyFather("哈哈"); f.display(); } }动态代理实在程序运行期间动态,动态的创建目标对象的代理对象,并且针对与目标对象中的方法进行功能性增强的一种技术
所以动态代理和静态代理效果是一样的,但是动态代理使用麻烦,静态代理使用简单
代理对象可以在代理类执行目标方法,就可以在代理方法实现功能增强,本来是要直接执行目标方法的,现在不是,直接使用代理类去调用目标对象的方法
十三)动态代理是如何进行实现的?
1)JDK动态代理是通过自己写一个处理器实现InnovationHandler接口并且实现invoke方法实现动态代理,在invoke方法里面做方法的增强,在mathod.invoke方法里面通过反射得到目标类的目标方法并且进行调用即可
2)并且要求被代理必须实现接口
十四)动态代理底层是如何实现的?
这是一个很坑的面试题,没有问JDK的动态代理是怎么实现的,而是问动态代理是怎么是实现的
1)JDK的动态代理是依靠反射进行实现的,JAVA的反射机制允许在程序运行的时候动态的获取到类的信息,比如说成员变量,成员方法和构造函数,并且运行的时候动态的创建对象,并调用对象的方法
2)CGLIB是依靠字节码生成库生成被代理类的子类来实现动态代理的
十五)JDK proxy和CGLIB有什么区别?
1)出身不同:JDK proxy是java语言自带的类,无需加载第三方库来实现,JAVA语言本身对于JDK proxy提供了稳定的支持,JAVA语言并且也在持续不断地进行升级JDK proxy,CGLIB是第三方提供的工具,但是SpringAOP都内置了
2)使用场景不同:JDK proxy只能代理实现接口的类,而CGLIB不需要实现接口,
CGLIB是基于ASM一个字节码操作框架来实现的,是基于继承于被代理类实现的,所以被代理类不能使用final修饰,因为被final修饰的类不能有子类;
3)性能不同:在JDK7之前,JDK proxy性能远远不如CGLIB,但是JDK7之后,经过JAVA稳定的版本的升级,JDKproxy的性能就远远高于CGLIB了;
十六)什么是反射?如何实现反射?
JAVA的反射机制允许在程序运行的时候动态的获取到类的信息,比如说成员变量,成员方法和构造函数,并且运行的时候动态的创建对象,并调用对象的方法
import java.io.File; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; class Person{ public String username="李四"; private String password="123456"; public Person(String username){ this.username=username; } public Person(String username,String password){ this.username=username; this.password=password; } @Override public String toString() { return "Person{" + "username='" + username + '\'' + ", password='" + password + '\'' + '}'; } } public class HelloWorld{ public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException { //1.首先获取到类对象 ClassClassPerson= (Class ) Class.forName("Person"); //2.获取到类的名称 System.out.println("此时要反射的类的名字是"+ClassPerson.getName()); //3.获取到类的所有字段 System.out.println("字段列表"); Field[] fields=ClassPerson.getDeclaredFields(); for(Field f:fields){ System.out.println(f.getName()); } //4.获取到类的所有方法 System.out.println("方法列表"); Method[] methods=ClassPerson.getDeclaredMethods(); for(Method m:methods){ System.out.println(m.getName()); } //5.创建对象 Constructorconstructor=ClassPerson.getDeclaredConstructor(String.class,String.class); constructor.setAccessible(true); Person p=constructor.newInstance("王二麻子","3344"); System.out.println(p.toString()); } }
十七)反射的优缺点:
1)动态性:反射可以是程序在运行期间动态的获取到类的属性和方法,并且可以通过反射进行动态调用,是代码变得更加灵活
2)通用性:反射可以处理不同类的对象,是得代码变得更加的通用和复用
反射的缺点:
1)性能比较低:是在运行期间动态获取类的所有信息,所有方法和私有属性,反射会涉及到动态类型的解析,所以JVM无法针对对于这些代码进行优化,导致性能要比非反射代码低
2)有安全性问题:破坏封装性,破坏可读性
十八)反射获取到类对象的方式
1)使用.class语法,可以直接通过类名.class就直接可以获取到类对象了
2)通过Class.forName("类的完整路径")从而来获取到类对象
3)直接创建对象,通过getClass来获取到类对象
4)通过类加载器的loadClass方法,里面的参数也是完整的包名
十九)反射的应用场景:
1)框架和库:Spring的动态代理,依赖注入,动态代理
2)插件化:lommbok中的Getter和Setter方法提示,就是通过反射获取到字段名字的
面向对象:找对象,创建对象,使用对象,依据对象之间的相互配合共同完成一件事情
二十)什么是SPI,有什么应用场景?
比如说现在要实现数据库做增删改查操作,但是数据库是由不同的公司和厂商来做的,不同公司厂商的数据库所支持的CRUD是不同的,MYSQL,Oracle都是不同的写法,此时对于开发者问题就来了,只调用一种方式,有不同的厂商实现不同的接口,只提供了一个机制,让不同的数据库厂商去实现这个接口,负责不同系统的对接,否则自己对接就很麻烦,即使后来再来了好几种数据库,那么这几个厂商还是要对接JDBC的接口
SPI是JDK内置的一种服务提供发现机制,SPI允许服务提供者动态地将实现类注入到系统中从而实现组件的可插拔性和扩展性
1)服务接口,定义一组抽象类或者接口,表示一种服务或者是一种功能
2)服务提供者接口:就是相当于是数据库的不同的厂商,是服务器接口的具体实现,是用来提供服务的具体功能的,厂商来进行实现接口的具体实现
3)服务加载器:是用来负责加载并且实例化服务提供者,并将其注册到系统中,来供服务接口使用
一)数据库驱动:在JAVA中,数据库驱动就是一个典型的SPI使用场景,不同的数据库厂商都提供了自己的数据库驱动实现,这些实现都提供了同一个JDBC接口,JVM可以在运行的时候动态加载合适的数据库驱动,使得开发者在不修改代码的情况下切换不同的数据库
二)日志框架:日志框架中SLF4J也使用了SPI机制,开发者可以选择不同的日志实现,例如logback,log4J等,并将其注入到日志框架中从而很灵活的切换日志实现
public class SmsMessageService implements MessageService{ @Override public void sendMessage(String message) { System.out.println("我是message"); } } public class EmailMessageService implements MessageService{ @Override public void sendMessage(String message) { System.out.println("我是email"); } }
一)数组和链表的区别:
集合框架就是将所有的数据结构,进行封装成了Java自己的类,例如以后想实现一个顺序表,直接使用ArrayList就可以的;
主要是对数据的组织形式和描述方法是不一样的
数据结构的组织:
1)顺序表的底层是一个数组,是在逻辑上面和真实的物理内存上面都是连续的
2)链表是一个由若干节点组成的一个数据结构,逻辑上面是连续的,但是在物理(内存上)上面不一定是连续的;
数据结构的操作:
3)顺序表适合查找相关的操作,因为可以通过使用下标直接获取到某个位置的元素,但是在链表中就没法通过下标来进行确定到底是哪一个位置的元素,所以链表不支持随机访问;
4)链表适合用于频繁的插入和删除操作,此时不需要像顺序表一样移动元素,顺序表插入元素,都要把元素放到后面去,顺序表删除元素,都要把元素移动到前面去;
链表的插入,只需要修改指向即可;
空间利用率:顺序表还有不好的地方,那就是说看你的顺序表慢不慢,满了要进行扩容,扩容了之后,不一定能够放满,所以他的空间利用率不高
5)链表随用随取,不支持随机访问,要一个new一个节点,这个节点属于对象,头插,尾插时间复杂度是O(1)
6)但是顺序表,但是顺序表满了需要进行扩容,扩容后的空间也有可能是有些无法利用的,浪废掉了
二)ArrayList和LinkedList的区别?
1)容量:new ArrayList() 初始化容量为0,存入1个元素时,首次扩容至默认值10,之后按1.5倍扩容,ArrayList需要手动设定具体大小的容量,可以通过数组下标来进行访问,但是LinkedLIst他的自由度比较高,可以动态的随着数据量的变化而变化
2)实现接口不同:ArrayLIst底层的实现是一个动态数组,实现了list接口,collection接口,Iterable接口,后者是一个LinkedList是一个双向链表,实现了iterable接口,list接口,collection接口,queue和dequeue接口
3)增删改查操作:当对数据进行增加和删除的操作时(add和remove操作),LinkedList比ArrayList的效率更高,因为ArrayList是数组,所以在其中进行增删操作时,会对操作点之后所有数据的下标索引造成影响,需要进行数据的移动,
4)但是对于访问元素的时候,ArrayList比LinkedList更加的高效,ArrayList直接可以通过数组下标进行访问,按照下标操作元素的时间复杂度是O(1),LinkedList还需要遍历链表来进行查找;
5)物理内存ArrayList数据是存放在连续的内存空间里面的,但是LinkedList不是存放在连续的内存空间里面的
对于这里面的插入和删除,仍然需要进行注意一些问题:
linkedList指定位置的插入add方法也是需要通过位置来进行确定下标的位置的,取下标的操作的时间复杂度仍然是O(N)
6)对于我们的add(index,element)来说,如果是ArrayList那么去取下标的过程的时间复杂度是O(1),插入过程的时间复杂度是O(N),但是如果说对于LinkedList来说,取下标的过程的时间复杂度是O(N),进行插入过程中的时间复杂度是O(1)