2021.0827 快手摸底(一)JVM

简历面 高频基础----java后端实习生,效率应用Team,简历内推。

1,JDK、JRE、JVM介绍?

  • JDK(Java Development Kit) Java开发工具包
    1,JDK是java语言的软件开发工具包(SDK),是整个java开发的核心,它包括java运行环境JRE(JVM+类库)+java工具
    2,JDK安装目录下有一个jre的目录,里面有两个目录bin和lib,在这里bin可以认为是JVM,lib可以认为是JVM运行所需要的类库,JVM+类库合起来称为JRE。

  • JRE(Java Runtime Environment)Java运行环境
    1,开发一个java程序所需要的最小环境为JDK运行一个java程序所需要的最小环境为JRE。JRE没有任何开发工具,如编译器、调试器。

  • JVM(Java Visual Machine) Java虚拟机
    1,源文件:未编译成.class文件的java代码就是源文件,一个源文件内可以有很多类,但只能有一个public类。如果有一个类是public类,那么源文件的名字就是这个public类的名字;如果源文件没有public类,那么源文件的名字只要和某个类的名字相同即可。
    2,使用java虚拟机屏蔽了与具体平台相关的的信息,java实现跨平台最核心的部分,任何语言只要装有该平台的java虚拟机,字节码(.class文件,以8个字节为基础单位的二进制流)文件就可以在该平台上运行。
    3,java编译器(如eclipse、JDK安装目录里面的javac.exe)将java代码(.java源文件)编译为字节码(.class文件),JVM中的java解释器(java.exe文件),把字节码转换成机器码在特定的平台运行。
    4,java.exe和javaw.exe都可以运行.class文件,不同点在于 java.exe是控制台应用,而javaw.exe是GUI应用(图形用户接口)。如果程序是纯粹的图形化的并且要提升速度,就用javaw.exe.
    在这里插入图片描述

2,类加载机制?

<1>什么是类加载?

  • 通过编译器将java源码编译为class文件之后,将class文件加载到JVM内存中的一个过程。

  • class文件文件包含了描述类的各种信息,类加载过程包括加载、验证、准备、解析、初始化。

  • JVM并不是一开始就把所有类都加载进内存中,只有第一次遇到某个需要运行的类时才会加载,且只加载一次。
    在这里插入图片描述
    <2>类加载过程解析

  • 加载
    将.class文件从磁盘读取到内存中,此阶段程序员可以干预,可以通过自定义类加载器来完成加载,JVM主要完成三件事:
    ① 通过类的全域限定名来获取其定义的二进制字节流。
    ②将二进制字节码转化为方法区所理解的数据结构。
    ③在堆中生成一个代表这个类的java.lang.class对象,便于用户调用。

  • 验证
    确保class文件中的二进制流中所包含的信息符合当前虚拟机的要求,并且不会损害虚拟机的安全。
    ①对文件格式的验证。确保这是一个可加载的class文件。
    ②元数据验证。元数据就是描述数据的数据,类似于说明书。对类的元数据信息进行语义验证,保证不存在不符合java语言规范的元数据信息。如:
    <1>这个类是否有父类(除了java.lang.object之外,所有的类都有父类)
    <2>该类是否继承了被final修饰的类
    <3>如果该类不是抽象类,是否实现了其父类或者接口中要求实现的所有方法。
    ③字节码验证。整个验证过程中最复杂的阶段,通过数据流和控制流,对类的方法体进行验证分析,确保被校验的类的方法在运行时不会做出伤害虚拟机的行为。如:
    <1>保证方法体中类型转化是有效的,子类对象可以向父类类型转换,但是父类对象不可以转化为子类甚至不相干的类型。
    <2>保证跳转指令不会跳转到方法体之外的字节码指令上。
    ④符号引用验证。发生在虚拟机将符号引用转化为直接引用的时候,确保解析动作能够正确执行,这个动作在解析阶段发生。
    <1>全域限定名能否找到对应的类。
    <2>符号引用中的访问性(private、public等)能否被当前类访问。

  • 准备
    方法区中为类变量分配内存(static修饰的变量),并初始化值。

public static int a=100;
//此时a在准备阶段的值时0,在初始化阶段的时候才会被赋值为5
public final static int b=1;
//被final修饰的static在准备阶段就是100,存放在方法区中的字符串常量池中。
  • 解析
    将常量池中的符号引用转化为直接引用,解析动作主要针对类、接口、类方法、接口方法、字段(类成员变量)等。
    类A中引用了类B,在编译阶段,类A是不知道类B的实际地址,就用一个字符串来代表B的地址;在运行阶段时,触发了类A的类加载,此时发现类B没有加载,此时就对类B进行加载,A中的代表B的字符串就会被B地址代替。
    <1>静态解析:如果上面提到的B是一个具体的实体类,那么就是静态解析,因为解析的目标类很明确。
    <2>动态解析:如果B是个接口或者抽象类,A不知道要用哪个来替换,只有等到实际调用的时候,虚拟机的调用栈会得到具体的类型信息,这个时候再进行解析得到具体的直接引用,这就是为什么有的解析发生在初始化之后。
    在这里插入图片描述

<3>符号引用:一串字符串,这个字符串包含足够的信息,以供使用时可以找到相应的位置。
<4>直接引用:偏移量,通过偏移量虚拟机可以直接在该类的内存中找到改方法的字节码的起始位置。
第一次运行时,要根据字符串的内容,到该类的方法表中搜索这个方法。第一次运行完后,符号引用就会被替换为直接引用。

  • 初始化

<1>初始化工作内容:对类变量(static)修饰的变量进行初始化。

//初始化的两个方式:
//①声明static 变量时的初始值
private static int a=10;//之前a的值为0  现在初始化为1
//②使用静态代码块为static变量指定初始值
private static int b;  
    static {  
        b = 10;  
    }

<2>初始化时机

① 使用new关键字实例化该类的对象。
② 访问某个类或者接口的静态变量,或者对改静态变量进行赋值。
③ 调用类的静态方法。
④ 使用java.lang.reflect包的方法进行反射调用的时候,如Class.forName(com.yzd.Test)。如果该类没有被初始化,就要先初始化。
Class.forName()得到的class是已经初始化完成的。
Classloader.loaderClass得到的class是还没有链接(验证,准备,解析三个过程被称为链接)的。
⑤ JVM启动时,会先初始化要执行的主类(main).
⑥ 初始化某个类的时候,如果父类没有初始化的话,会先初始化父类。 

<3>初始化顺序

① 如果该类还没有加载和连接,先加载和连接该类。
② 如果该类存在父类,且父类没有被初始化,先初始化父类。父类static方法、static变量赋值
③ 初始化该类。static方法、static变量赋值。

<3>类加载器?
① 在类加载阶段:“通过一个类的全限定名来获取描述此类的二进制字节流” 这个动作是由JVM外部的类加载器完成的。
在这里插入图片描述
<4>双亲委派机制?
如果一个类记载器收到了加载类的请求,它不会先自己加载,而是先将请求委派给父类加载器加载(所以最终所用的请求都会到顶层的BootStrap ClassLoader中,父子类加载器并不是继承关系,而是一种组合关系),如果父类记载器无法完成加载,子类加载器才自己加载。
在这里插入图片描述

好处:
① 避免一个类被多次加载,当父类加载器加载该类的时候,子类加载器就不会加载该类。
② 安全。如果自己写了一个名为java.lang.Integer的类时,类加载器通过向上委托,发现有两个Integer,但JDK的Integer已经被加载了,所以自定义的Integr类不会被加载,可以防止核心类库被修改。

缺点:上层ClassLoader无法访问下层ClassLoader加载的类。

<5>可以打破双亲委派模型吗?
可以打破。自定义一个类加载器,重写其中loadClass方法,使其不进行双亲委派即可。

3,intern()函数?

  • 在jdk1.6中,先判断常量池中是否有该字符串,如果有,则intern()指向常量池(即字符串的引用);如果不存在,则在常量池中加入该字符串,然后intern()指向常量池。
  • 在jdk1.7中,先判断常量池中是否有该字符串,如果有,inern()指向常量池;如果没有,说明字符串常量在堆中,则将堆区该对象的引用加入到字符串常量池中,intern()实际指向堆。
	      //代码基于JDK 1.8
	    	
	      //s1指向字符串常量池中的"自由之路"
	      String s1 = "自由之路";//常量池
	      //s2也指向字符串常量池中的"自由之路"
	      String s2 = "自由之路";//常量池
	      //s3指向堆中的某个对象
	      String s3 = new String("自由之路");//堆3
	      //因为字符串常量池中已经存在"自由之路"的引用,直接返回这个引用
	      String s4 = s3.intern();//常量池

	      //创建一个字符串对象
	      String s5 = new String("ddd");//堆5
	      //常量池中不存在指向"ddd"的引用,创建一个"ddd"对象,并将其引用存入常量池
	      String s6 = s5.intern();//常量池6
	      //创建一个字符串对象
	      String s7 = new String("ddd");//堆7
	      //常量池中存在指向"ddd"的引用,直接返回
	      String s8 = s7.intern();//常量池6

	      System.out.println("s1==s2:"+(s1==s2));//true
	      System.out.println("s1==s3:"+(s1==s3));//false
	      System.out.println("s1==s4:"+(s1==s4));//true

	      System.out.println("s5==s6:"+(s5==s6));//false
	      System.out.println("s6==s8:"+(s6==s8));//true
	      System.out.println("s7==s8:"+(s7==s8));//false

4,JVM的存储?

  • jdk1.6:运行时常量池逻辑包含字符串常量池存放在方法区,hotspot虚拟机对方法区的实现为永久代

  • jdk1.7:字符串常量池被拿到了堆中,其他没有改变。

  • jdk1.8:取消了方法区的概念,取而代之在本地内存中的元数据空间。此时字符串常量池还在堆中,运行时常量池在元数据空间中。方法区的实现从永久代变成了元数据空间
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    JVM初始运行的时候会分配线程共享区的内存,而JVM每遇到一个线程就为其分配线程区私有内存,当线程终止时,线程私有区所占用的内存空间会被释放掉。线程私有区的生命周期和所属线程的生命周期相同,而线程共享区域的生命周期和java程序运行的生命周期相同,所以系统垃圾回收的场所发生在线程共享区域内(大部分是发生在堆中)。

  • 程序计数器
    代码运行,执行命令,每个命令都是有行号的,会使用程序计数器来记录命令执行到多少行了。

  • :函数运行过程中的临时变量,当函数执行完毕后就自动释放。

  • :存对象(new),垃圾回收的主要场所。

  • 本地方法栈:加载native修饰的方法,native修饰的方法表示不是java语言实现的方法。

  • 元数据空间:static方法和static变量,常量(final修饰),classloader等全局信息。

  • 直接内存:不受java堆内存大小限制,受本机内存大小限制。不受垃圾回收的影响,但是有对象在堆中引用这块内存,所以间接收到影响。
    4,区分对象、实例、对象的引用?

Student stu = new Student("小明");
  • new Student();是一个对象,存放在(heap)中。
  • 小明属于new Student()对象中的一个实例,放在常量池中。
  • stu是指向new Student对象的引用,对对象的操作通过引用进行,放在(stack)中。

5,如何判断从新生代到老年代?

对象都是在堆内存中分配的。

堆内存 = 新生代 + 老年代
新生代 = Eden区 + survivor from区 + survivor to区 = 811
Eden:用来存放新的对象
两块survivor主要是用来防止内存碎片化,垃圾回收中的"复制算法"

<1> survivor存在的意义?
如果没有survivor,eden区每进行一次Minor GC,存活的对象就会被送到老年代,老年代很快就会被填满没出发Full GC,Full GC消耗的时间和Minor GC相比起来特别长,会影响程序执行效率。所以增加survivor,可以减少被送到老年代的对象,进而减少Full GC的发生。
<2> 为什么要两个survivor?
刚刚新建的对象在Eden中,经历一次Minor GC,Eden中存活的对象就会被移动到第一块survivor from中,Eden区被清空。等到Eden区再次填满,再一次触发Minor GC,Eden区和survivor from中存活的对象就被复制到survivor to中,Eden区和survivor from区域被清空。然后下一轮survivor from和survivor to交换位置,这样可以保证内存连续,但总有一个survivor为空。

  • 大对象直接进入老年代
    大对象的大小是由用户指定的,使用jvm参数指定。
-XX:PretenureSizeThreshold=对象大小(单位:byte)

这个参数默认值是0,也就是说,所有的对象默认创建出来都是存放在新生代中的。当我们指定了大小之后,只要创建出来的对象超过这个设定值,这个对象就会直接晋升到老年代。

  • 长期存活的对象
    熬过15次Minor GC而没有被回收,就会晋升到老年代。

  • 动态年龄判断
    当 Survivor 空间中相同年龄所有对象的大小总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,而不需要达到默认的分代年龄。

6,JVM内存溢出和内存泄漏?

  • 内存溢出
    out of memory,JVM中没有空闲内存,垃圾回收器回收垃圾之后也无法提供足够的内存。在发生内存溢出之前一般都会进行垃圾回收,如果能回收足够的内存,就不会出现OOM的异常;如果垃圾回收之后也不能提供足够的空间,就会出现OOM异常。
    但是并不是任何OOM之前都会进行垃圾回收,比如要分配一个超大的对象空间,超过堆的大小,那么即使进行垃圾回收之后也不能解决问题,因此就不会进行垃圾回收。
  • 内存泄漏
    对象不会再被调用,但是又GC不掉。
    内存泄漏并不会立刻引起程序崩溃,但随着泄漏的内存逐渐增多,知道内存耗尽,最终出现OOM异常。
  • 内存泄漏的几种情况
    <1>堆内存溢出
java.lang.OutOfMemoryError: Java heap space
  • 原因:
    ① 虚拟机的堆内存设置太小
    ② 内存中创建了很多大的对象,垃圾回收之后也不能回收足够的空间。
  • 解决方案:
    ① 适当调整-Xmx(JVM运行时可申请的最大Heap值)和-Xms(JVM启动时申请的初始Heap值)参数。
    ② 尽量避免大的对象的申请,比如文件上传、大批量从数据库中获取数据等,尽量分块或者分批处理。
    ③ 尽量提高一次请求的速度,垃圾回收越早越好,否则大量的并发来了的时候,再来新的请求就无法分配新的内存了。
    <2>垃圾回收本身超时
java.lang.OutOfMemoryError:GC overhead limit exceeded
  • 原因:
    JVM花费大量时间进行GC,但是收效甚微。
  • 解决方案:
    减少对象生命周期,尽量能够快速地垃圾回收。
    <3>元数据空间内催溢出
    java.lang.OutOfMemoryError: Metaspace
  • 原因:
    ① 引用了很多第三方库。
    ② 通过动态代码生成了类加载方法。
  • 解决方案:
    ① 优化参数配置。默认情况下,元数据空间的大小收到本地内存的限制,但是尽量还是要对该项进行配置。
MaxMetaspaceSize:  最大空间,默认是没有限制的。
MetaspaceSize:  元空间首次使用不够而触发GC的阈值。同时GC会对该值进行调整:如果释放了大量空间,就适当降低该值;如果释放了很少的空间,在不超过MaxMetaspaceSize的情况下,适当提高该值。

② 慎重引用第三方包,不需要的包就去掉。
③ 对于大量使用动态生成类的框架,要做好压力测试,验证动态生成的类是否超出内存的需求。
<4> 栈内存溢出

java.lang.StackOverflowError
  • 原因:
    在JVM中一个线程对应一个JVM栈,JVM栈中记录了线程运行状态。JVM栈以栈帧为单位组成,一个栈帧代表一个方法调用。栈帧由三部分组成:局部变量区、操作数区、帧数据区。
    在这里插入图片描述
    一个线程执行一个方法时,JVM将新建一个栈帧,并将它推到栈顶中,此时新的栈帧就变成了当前栈帧。当一个方法递归调用自己的时候,会拷贝一份当前方法的数据生成一份新的栈帧push到栈顶中,因此递归的每层调用都会创建一份新的栈帧,这样栈中越来越多的内存空间随着递归调用而被消耗,这样就会造成栈的内存溢出。
  • 解决方案:
    ① 递归调用避免死循环。
    ② 调高-Xss(一个线程的初始空间)。
    <5> 数组超限
java.lang.OutOfMemoryError: Requested array size exceeds VM limit
  • 原因:
    应用程序试图分配大于JVM可以支持的数组,一般来说java对应用程序所能分配的数组最大大小是有限制的。

  • 解决方法:
    ① 修复应用程序中分配巨大数组的bug。
    ② 使用-Xmx(JVM运行时可申请的最大Heap值)增加堆的大小。
    <6> 无法创建本机线程,无法进一步创建新的Java线程时引发的错误

java.lang.OutOfMemoryError: unable to create new native thread
  • 原因:
    JVM创建的Java线程需要来自操作系统的本机内存,系统内存耗尽,没有办法为新线程分配内存,或者创建线程数超过操作系统内存数。

  • 解决方案:
    ① 操作系统会限制进程创建的线程数,如果是这个问题可以调大操作系统线程数阈值。
    ② 排查应用程序是否创建了过多的线程?超量创建的堆栈信息是怎样的?谁创建了这些线程?一旦明确了这些问题,便很容易解决。
    ③ 正常增长的业务确实需要更多内存来创建更多的线程,如果是这种情况,增加机器内存。
    ④ 减小堆内存。线程不在堆内存区域内创建,线程在堆内存之外的内存上创建,如果堆空间设置的太大了,导致剩余的内存不多,没有足够的空间用于创建新的线程。
    ⑤ 减小线程栈大小。线程会占用内存,如果每个线程都占用更多的内存,整体上将消耗更多的内存。
    <7> 杀死进程
    操作系统层面的,不是JVM层面的。

Out of memory:Kill process or sacrifice child

原因:操作系统是建立在进程基础上,这些进程在内核中作业,其中有一个非常特殊的进程----“内存杀手”(out of memory killer)。当内核检测到系统内存不足时,oomkiller被激活,检查当前谁占用内存最多,就杀死谁。
解决方法:
① 最直接有效的:升级系统内存。
② 调整oomkiller配置。
<8>超出交换区

java.lang.OutOfMemoryError:Out of swap space

虚拟内存: 抽象概念,将地址连续化。
虚拟内存 = 物理内存 + 交换空间
交换空间:物理内存的补充,虚拟内存中保留位临时存储位置的部分。

  • 原因:交换空间在物理内存被充满时使用,如果系统需要更多的内存资源,而物理内存已经充满,内存中不活跃的页就会被移到交换空间去。虽然交换空间可以为带有少量内存的机器提供帮助,但是这种方法不应该被当做是对内存的取代。交换空间位于硬盘驱动器上,它比进入物理内存要慢。
    ① 应用程序本地内存泄漏,某个程序不断地申请本地内存,但是不释放。
    ② 交换区分区大小不足。
    ③ 其他进程消耗了所有的内存。
  • 解决方案:
    ① 增加系统交换区大小。
    <9> 直接内存溢出
java.lang.OutOfMemoryError:Direct buffer memory

ByteBuffer类:缓冲区,可将读写的数据缓存于内存,等待合适的实际再做实际IO,从而减少实际的物理读写次数。
allocatDirect:直接分配缓冲区,脱离JVM内存,在系统级别分配的字节缓冲区。在缓冲区较大并且长期存在,或者需要经常使用的时候这么做,系统开销较大。

  • 原因:在直接或间接使用了ByteBuffer中的allocateDirect方法的时候,而不做clear的时候就会出现类似的问题。
    如果不断分配本地内存,堆内存很少使用,那么JVM就不需要执行GC,DirectByteBuffer对象就不会被回收,此时如果继续分配堆外内存, 可能堆外内存已经被耗光了无法继续分配,此时程序就会抛出OutOfMemoryError,直接崩溃。
  • 解决方法:
    ① 通过启动参数 -XX:MaxDirectMemorySize 调整 Direct ByteBuffer 的上限值。
    ② 检查堆外内存使用代码,确认是否存在内存泄漏;或者通过反射调用 sun.misc.Cleaner 的 clean() 方法来主动释放被 Direct ByteBuffer 持有的内存空间。
    ③ Java 只能通过 ByteBuffer.allocateDirect 方法使用 Direct ByteBuffer,因此,可以通过 Arthas 等在线诊断工具拦截该方法进行排查。
    ④ 内存容量确实不足,升级配置

7,Java数据类型有哪些?

在这里插入图片描述

8,区分变量与常量?

  • 变量:成员变量、局部变量。
    1,成员变量是在类中定义的变量,可以不初始化直接使用,使用的时候其值就是对应类型的默认值。
    2,局部变量是在方法中申明的变量,要使用必须先初始化。

  • 常量:final修饰
    整个运行过程中保持不变的量,用关键词final修饰,必须初始化,一般用大写字母命名。

9,String是个类?String变量存贮在哪里?==和euqals的区别?

  • 直接创建一个字符串对象和new一个字符串对象
String str1  = new String("acb");

三步:先在常量池创建一个abc对象,再在开辟一个空间,存放常量池中的复制abc对象,最后在中开辟一个空间str1指向堆中的abc。如果常量池中已经由abc对象,则第一步省略,直接在堆中开辟空间存放对象的复制。

String str2 = "abc";

两步:先在常量池中创建了一个abc对象,然后在中创建str1,str1指向常量池中的abc(str1存的是abc的地址);如果常量池中已经由abc对象,则直接在栈中开辟空间str1指向abc。
在这里插入图片描述

  • ==和equals的区别
    ==:对地址进行比较
    equals:对内容进行比较
package Test;


public class TestArrayList {
	
	public static void main(String[] args) {
		String str1 = new String("abc");
		String str2 = new String("abc");
		
		String str3 = "abc";
		String str4 = "abc";
		
		if (str1==str2) {//flase
			System.out.println("第一次比较:"+"str1==str2");
		}		
		if(str1.equals(str2)) {//true
			System.out.println("第二次比较:"+"str1=str2");
		}
		
		
		
		if(str3==str4) {//true
			System.out.println("第三次比较:"+"str3==str4");
		}
		if(str3.equals(str4)) {//true
			System.out.println("第四次比较:"+"str3=str4");
		}
		
		
		if(str1==str3) {//flase
			System.out.println("第五次比较:"+"str1==str3");
		}
		if(str1.equals(str3)) {//true
			System.out.println("第六次比较:"+"str1=str3");
		}
	}

}

在这里插入图片描述

  • +号拼接字符串时用"=="比较是否是同一地址
		String str11 = "Hello";
	    String str22 = "java";
	    
		String str33 = "Hellojava";//栈
		String str44 = str11+str22;//堆
		String str = "Hello"+"java";//栈
		
		System.out.println(str33==str44);//false
		System.out.println(str33==str);//true
		System.out.println(str44==str);//false

<1>str44由两个String变量相加得到,不能在编译的时候就确定,故不能直接指向常量池中的对象。编译器在执行的时候会自动引入StringBuilder对象,调用append()方法,然后调用toString()方法返回其在堆中的值。str33指向常量池中,str44指向堆中,故结果为false。

	/**
	* StringBuilder 类的 toSTring() 方法
	*/
    @Override
    public String toString() {
        // Create a copy, don't share the array
        return new String(value, 0, count);
    }

<2>str33是常量池中的常量,str也是由两个常量拼接而成,常量值在编译时就被确定下来了,因此str33和str指向常量池中的同一常量。
<3>同理,结果为false。

		final String str11 = "Hello";
	    final String str22 = "java";
	    
		String str33 = "Hellojava";
		String str44 = str11+str22;
		String str = "Hello"+"java";
		
		System.out.println(str33==str44);//true
		System.out.println(str33==str);//true
		System.out.println(str44==str);//true

<4>当给str11和str22加上final修饰时,编译器在所有用到该常量的地方都用该常量代替,str44在编译时就确定了,指向常量池中的Hellojava常量,故str33、str44、str都是量池中的同一常量。

10,List的实现类?

List是个接口,其实现类有如下:

  • ArrayList:动态数组,查询方便,增加删除操作不方便,线程不安全。本质是通过定义一个新的更大的数组,将旧数组的内容复制到新数组,实现扩容。
    <1>查询方便:直接定位查询。 找到ArrayList首地址,然后直接定位到索引地址。
    <2>增加数据不方便:如果增加数据超过了数组大小,需要扩容至1.5倍之后再将原来的数据复制过去。
    <3>删除数据不方便:删除某个数据后,后面的数据会自动向前补位。如果要连续删除的话,可能忽略某些索引的数据。
    在这里插入图片描述
    <4>线程不安全:没有synchronized关键字,增加操作时先增加,再扩容。
    线程A向arraylist增加数据1时,先增加,然后线程暂停;线程B获得机会,向数组增加数据2,覆盖了线程1的数据,然后对数组扩容;线程A又获得机会对数组扩容。即增加了一个数据,但扩容了两次。
  • LinkedList:双向链表结构,每一个节点存储了前一个节点引用、该节点数据、后一个节点引用。数据增、删、改方便,但查询不方便。线程不安全。
    <1>遍历查询:使用一般for循环遍历查询时间复杂度高,每一个节点数据都是从头开始一个一个往下查。使用迭代器进行查询可以提高效率,每取一个元素就将游标指向下一个节点,根据游标可以得到下一个元素,不用从头开始查询。
package Test;

import java.util.Iterator;
import java.util.LinkedList;

public class TestArrayList {
	
	    public static void main(String[] args) {
	    	LinkedList<String> list = new LinkedList<String>();
	    	//往链表中加入10000个数据
	    	for(int i=0;i<1000;i++) {
	    		list.add(""+i);
	    	}
	    	
	    	//1,使用for循环遍历将链表中的数据全部取出   并计算整个操作时间
	    	long startTime1 = System.currentTimeMillis();//获取开始时间
	    	for(int i=0;i<list.size();i++) {
	    		System.out.println(list.get(i));
	    	}
	    	long endTime1 = System.currentTimeMillis();//获取结束时间
	    	System.out.println("for循环运行时间:"+(endTime1-startTime1));
	    	
	    	//2,使用迭代器将链表中的数据全部取出,并计算整个操作时间
	    	Iterator<String> iterator = list.iterator();//创建该链表的构造器
	    	long startTime2 = System.currentTimeMillis();//获取开始时间
	    	while(iterator.hasNext()) {
	    		System.out.println(iterator.next());//取出list数据,并将游标向下移	    		
	    	}
	    	long endTime2 = System.currentTimeMillis();//获取结束时间
	    	System.out.println("迭代器循环运行时间:"+(endTime2-startTime2));
	    }
	

}

在这里插入图片描述
<2>查询某个元素
①返回指定元素索引

list.indexOf(数据o);//返回第一次数据为o的索引
list.lastIndexOf(数据o);//返回最后一次数据为o的索引

②返回指定元素

//1,list自带函数
list.get(index);//返回索引为index的数据

//2,在迭代器中加判断条件 
	    	while(iterator.hasNext()){
	    		if(iterator.next()...) {
	    			...
	    		}
	    	}

<3>插入

package Test;



public class TestArrayList {
	private Node first;//头节点
	private Node last;//尾节点
	private static int size;//链表长度
	
	
	//取出指定位置的节点   分前后查找   提高效率
	public Node getNode(int index) {
		Node temp = null;
		//如果索引在链表的前半部分    从头开始查找
		if(index<=(size>>1)) {
			temp = first ;//从第一个节点开始
			for(int i=0;i<index;i++) {
				temp = temp.next;//当i=index-1时,temp取到第index个节点
			}
			
		}
		else {//如果索引在链表后半部分   从尾节点开始查找
			temp = last;
			for(int i=size-1;i>index;i--) {
				temp = temp.previous;
			}
		}
		return temp;
	} 
	
	//增加函数   顺序添加
	public void add(Object obj) {		
		Node node = new Node(obj);//需要添加的数据    创建该节点
		//第一次添加节点   首节点和尾节点都是同一个
		if(first==null) {
			first = node;
			last = node;
		}else {//不是第一次添加节点   每一个节点都添加到链表的最后
			node.previous = last;//建立连接关系
			last.next = node;
			node.next = null;
			last = node;//将尾节点指向最新添加的节点
		}
		size++;//增加之后链表长度+1
	}
	
	//插入函数   在指定位置插入
	public void insert(int index,Object object) {
		if((0<=index)&&(index<=size)) {
			Node newNode = new Node(object);//创建需要插入的新节点
			//1,在首部插入
			if(index==0) {
				newNode.next = first;//建立连接关系
				first.previous = newNode;
				newNode.previous = null;//新节点之前的节点为空!!!
				first = newNode;//更新头节点!!
			}
			//2,在尾部插入
			else if(index==size) {
				last.next=newNode;//建立连接关系
				newNode.previous = last;
				newNode.next = null;//尾节点的后一个节点为空!!
				last = newNode;//更新尾节点
			
			}
			//3,在中间插入
			else {
				Node temp = getNode(index);//取出当前索引的节点
				Node up = getNode(index-1);//取出当前索引的前一个节点
				newNode.previous = up;//建立新的连接  注意等号"="两边不要写反!!!
				up.next = newNode;
				newNode.next = temp;
				temp.previous = newNode;

			}
			size++;//插入完成之后size++;
		}
		else {
			System.out.println("插入操作的索引不合法");
		}
		
	}

	
	    public static void main(String[] args) {
	    	TestArrayList  list = new TestArrayList();
	    	list.add("a");
	    	list.add("b");
	    	list.add("c");
	    	list.insert(1, "h");
	    	//将链表内全部数据打印出来
	    	for(int i=0;i<size;i++) {
	    		Object object = list.getNode(i).data;
	    		System.out.println(object);
	    	}
	    }
	

}

在这里插入图片描述
<4>删除

	//删除函数   在指定位置删除指定节点
	public void delete(int index) {
		if((0<=index)&&(index<size)) {
			Node delNode = getNode(index);
			//1,删除头节点   直接将首届点指向第二个节点
			if(index==0) {
				first=delNode.next;
			}
			//2,删除尾节点   直接将尾节点指向倒数第二个节点
			else if(index==size-1) {
				last = delNode.previous;
			}
			//3,删除中间节点   忽略要删除的点   在要删除的点的前一个和后一个建立连接关系
			else {
				Node up = delNode.previous;
				Node down = delNode.next;
				up.next = down;
				down.previous = up;
			}
			
			size--;
		}
		else {
			System.out.println("删除操作的索引不合法");
		}
		
	}

	    public static void main(String[] args) {
	    	TestArrayList  list = new TestArrayList();
	    	list.add("a");
	    	list.add("b");
	    	list.add("c");
	    	list.add("d");
	    	list.delete(1);
	    	//list.delete(1);
	    	//将链表内全部数据打印出来
	    	for(int i=0;i<size;i++) {
	    		Object object = list.getNode(i).data;
	    		System.out.println(object);
	    	}
	    }
	

在这里插入图片描述
<5>线程不安全。没有synchronized关键字。
假设有N0、N1、N3三个节点,头节点为N0,尾节点为N2,线程A、B同时对该链表进行添加数据。
①线程A获得资源,建立连接关系:N2.next=N3,N3.previos=N2,然后暂停,释放资源。
②线程B获得资源,建立连接关系:N2.next=N4,N4.previos=N2,然后暂停,释放资源。
③线程A重获资源,size++,size=4,线程A工作结束,释放资源。
④线程B重获资源,size++,size=5,线程B工作结束,释放资源。
结果:节点N3的连接关系由于节点N4的加入被破坏了,只有节点N4被加入到链表中,节点N3无法被找到。最后整个链表只加入了一个节点,但是长度却增加了2。
在这里插入图片描述

  • Vector:向量类,底层是动态数组。线程安全,除了这一点外和ArrayList没什么区别。
  • Stack:栈,继承于Vector,线程安全,实现一个先进后出的栈。Stack额外提供5个方法使得Vector得以被当作堆使用。
    <1>push()函数,相当于add函数,将元素添加到栈顶。
    <2>int search(Object o),判断元素o是否在栈中,在的话返回元素的位置;不在的话返回-1。栈顶位置为1,往底依次增加。
	    	//栈
	    	Stack stack = new Stack();
	    	stack.add(0);
	    	stack.add(1);
	    	stack.add(2);
	    	stack.add(3);
	    	System.out.println("生成的栈为:"+stack);
	    	//查找栈顶、栈底的位置
	    	int ele1 = 0;
	    	int ele2 = 3;
	    	int index1=stack.search(ele1);
	    	int index2=stack.search(ele2);
	    	System.out.println("元素"+ele1+"在栈中的位置为:"+index1);
	    	System.out.println("元素"+ele2+"在栈中的位置为:"+index2);

在这里插入图片描述
在这里插入图片描述
<3>boolean empty()。如果栈顶没有任何内容,即栈为空,返回true;否则返回false。

	    	//栈
	    	Stack stack = new Stack();
	    	stack.add(0);
	    	stack.add(1);
	    	stack.add(2);
	    	stack.add(3);
	    	System.out.println("生成的栈为:"+stack);
	    		    	
	    	//2,判断栈是否为空
	    	System.out.println("添加完元素后栈是否为空?"+"  "+stack.empty());//栈顶有元素时   即栈不为空时为false
	    	stack.removeAll(stack);//清空栈
	    	System.out.println("删除完全部元素后栈是否为空?"+"  "+stack.empty());//栈顶无元素时   即栈为空时为true

在这里插入图片描述
<4>Object pop()。返回栈顶元素并将该元素从栈中删除。

	    	//栈
	    	Stack stack = new Stack();
	    	stack.add(0);
	    	stack.add(1);
	    	stack.add(2);
	    	stack.add(3);
            System.out.println("生成的栈为:"+stack);
           //3,取出栈顶元素并删除
	        while(!stack.isEmpty()){
	            System.out.println("栈:" + stack+ "\t栈大小为:" + stack.size() + "\t出栈元素为:" + stack.pop());
	          }
            

在这里插入图片描述
<>Object peek()。返回栈顶元素但并不删除。

	    	//栈
	    	Stack stack = new Stack();
	    	stack.add(0);
	    	stack.add(1);
	    	stack.add(2);
	    	stack.add(3);
	    	System.out.println("生成的栈为:"+stack);

	    	//4,取出栈顶元素不删除
	    	stack.peek();
	    	System.out.println("栈:"+stack);

在这里插入图片描述

11,final、static、abstract?

  • final:最终的,即不可更改。
    <1>修饰类:表示该类不能被继承。
    <2>修饰方法:表示该方法在子类中不能被重写。
    如果父类中的public方法被final修饰,子类可以继承到该方法,然后重写,会导致编译错误。
    如果父类中private方法被final修饰,子类不会继承到该方法,不会编译出错。子类如果要使用该方法的话,可以自己定义一个和父类一样的方法。
    <3>修饰变量:final可以修饰成员变量和局部变量,一旦被赋值,值不可以被更改。
    当final修饰一个基本数据类型的时候,必须要初始化,一旦赋值,不可更改。
    当final修饰一个引用类型时,在初始化之后不能指向其他对象,但对象的内容是可以更改的。

  • static:静态的,在不用创建对象的情况下调用方法/变量。
    <1>静态内部类或者嵌套内部类。
    <1>修饰方法。static方法只能访问static变量,非static方法可以访问static变量。
    <2>修饰代码块:优化代码性能,在类初次加载的时候执行,并且只执行一次。
    <3>修饰变量。final只可以修饰成员变量,不可以修饰局部变量。

  • abstract:抽象的,只能单独使用或者和public abstract。
    <1>修饰方法:只有方法名,没有方法体,以分号结尾,主要用来被重写。

public abstract void doSome();

<2>修饰类,表示该类是抽象的,不能实例化对象。**抽象类中可以有抽象方法也可以有非抽象方法,但非抽象类不能有抽象方法。**抽象类的设计是用来被继承的,子类可以是抽象类,也可以是非抽象类。
①当子类是抽象类时,不需要重写父类的抽象方法(因为子类自身也是个抽象类,方法可以不用实现),也可以有自身的抽象方法和具体方法。
②当子类是非抽象类时,这样子类就有了创建实例的能力,子类必须重写父类全部的抽象方法,可以有自己的方法,但必须是具体方法。

//抽象父类
abstract class Animal(){
    public void eat();//父类中的抽象方法,只有方法名,没有方法体
}
//子类继承父类,重写父类中所有的public抽象方法
class Cat extends Animal(){
    //重写父类中的抽象方法
    public abstract void eat(){
        System.out.println("猫吃东西");
    }
    	public static void main(String[] args) {
		Animal cat = new Cat();//子类具有了创建对象的能力。定义了一个父类引用指向子类对象,发生了多态。
		cat.eat();//调用的是子类Cat的具体实现方法
	}
}

在这里插入图片描述

12,public、protected、deflaut(friendly)、private?

访问权限符,4个都可以用来修饰变量和方法,但只有public和deflaut可以用来修饰类,不写任何关键词时默认为deflaut。
在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值