• JavaSE面试


    文章目录

    JavaSE基础部分

    1.数据类型

    基本数据类型:

    数值型:

    • 整数类型:byte short int long
    • 浮点类型:float double

    字符型:char

    布尔型:boolean

    引用数据类型:

    class

    interface接口

    数组

    switch是否能作用在byte上,是否能作用long上,是否能作用在String上?

    Java5以前,Switch(expr)中,expr只能是byte shrot char int ,从java5开始,java中引入了枚举类型,expr也可以是enum类型,从java7开始,expr还可以是字符串String,但是是长整型long在目前的版本中都是不可以的

    2.final关键字

    1.final修饰类时,该类是不能被继承的

    • 例如:String StringBuffer [线程安全] StringBuilder[线程不安全] 八大包装类

    2.final修饰方法时,该方法不能被重写,但是可以被继承,可以被重载

    • 例如ObjectgetClass方法就是使用了final关键字修饰的,比如:当我们希望某个类中的某个方法,不被重写时,就可以使用final关键字修饰的

    3.final可以修饰变量,1》如果是基本数据类型的变量,则数值一旦在初始化后就不能被修改了,一般情况下都会使用staticfinal来修饰基本数据类型变量,达到常量的效果,2》如果 是引用类型变量,则在对其初始化之后便不能再指向另一个对象,但是是指向的内容是可以被修饰的【例如:一个学生对象是final修饰的,所以它就是不能重新指定另一个学生对象,但是学生对象中有name属性,指,但是学生对象中name属性的值是可以被修改的【前提这个name属性是没有使用final修饰的】】

    • 例如:在String类中的value就是使用value修改的private final char value[];
    • final修饰变量可以在初始化直接进行赋值,也可以在代码块中赋值,也在构造器中赋值[String类的value属性就是在构造器进行赋值的],如果这个属性同时也使用static修饰,那么这个属性赋值只能在静态代码中或定义时直接赋值,final在局部代码块修饰变量的话必须可以在声明时直接赋值,也在后面的代码块中赋值【这个可以省略不答】

    4.当一个类使用final了关键字修饰,就没有必须又在方法中使用final关键字修饰了

    3.final finally finalize区别

    • final可以修饰类,变量,方法,修饰类表示该类不能被继承,修饰方法表示该方法不能被重写,修饰变量表示该变量是一个常量不能被重新赋值
    • finally一般作用在try-catch代码块中,在处理异常处理的时候,通常我们将一定要执行的代码放到finally代码块中,表示不管是否出现异常,该代码都会被执行,一般用来关闭资源
    • finalize是一个方法,属于Object类中一个方法,而Object类是所有类的基类,该方法一般由垃圾回收器来调用,当我们调用system.ge()方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收最后判断

    4.this关键字

    this关键字代表是该类对象的引用,就是指向该对象本身,this是每个对象都有的并且是隐藏的【谁调用代表谁】

    使用this注意事项和细节(必须掌握,记住)

    1. this关键字可以用来访问本类的属性,方法,构造器
    2. this用于区分当前类的属性(全局变量)和局部变量
    3. 访问成员方法的语法:this.方法名(参数列表)
    4. 访问构造器语法:this(参数列表);注意是只能在构造器中使用,并且要放在构造器的第一条语句,不能在普通方法中使用(注意是在构造器中使用必须放在第一条语句,)
    5. this不能在类定义的外部使用,只能在类定义的方法中使用(意思就是只能在本类中使用)

    5.Super关键字

    super关键字代表是父类引用 ,用于访问父类的属性,方法,构造器

    基本语法 记住

    1.访问父类的属性,但不能访问父类的private属性 super.属性名;

    2.访问父类的方法,不能访问父类的private方法 super.方法名(参数列表);

    3.访问父类的构造器 super(参数列表); 只能放在构造器的第一语句,只能出现一句!

    super关键字细节(记住)

    1.调用父类的构造器的好处(分工明确,父类属性由父类初始化,子类的属性由子类初始化)

    2.当子类中有和父类中的成员(属性和方法)重名时,为了访问父类的成员,必须通过super。如果没有重名,使用superthis直接访问是一样的效果

    3.super的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用super去访问爷爷类的成员;如果多个基类(上级类)中都有同名的成员,使用super访问遵循就近原则。

    A->B->C->...->Object,当然也需要遵守访问权限的相关规则

    superthis比区别记下

    区别点thissuper
    访问属性访问本类中属性,如果本类没有此属性则从父类中继续查找访问父类中的属性
    调用方法访问本类中的方法,如果本类没有此方法则从父类继续查找直接访问父类中的方法
    调用构造器调用本类构造器,必须放在构造器的首行调用父类构造器,必须放在子类构造器的首行
    特殊点表示当前对象子类中访问父类对象

    6.异常处理

    • getMessage()返回异常发生时详细信息
    • toString()返回异常发生时简要描述
    • getLocalizedMessage()返回异常对象本地化信息
    1》 请介绍Java异常接口

    参考答案:

    Throwable是异常的顶层父类,代表所有的非正常情况。它有两个直接子类,分别是Error Exception

    Error是错误,一般是指系统崩溃,如:StackOverflowError【栈溢出】和内存不足OOMout of memory),Error是严重错误,程序会崩溃,并且不能使用try-catch块来捕获Error对象,也不需要使用throws子句声明该方法可能抛出Error及其任何子类

    Exception是异常,它被分为两类,分别是checked异常【编译时异常】和Runtime异常【运行时异常】,所有的RuntimeException类及其子类的实例被称为Runtime异常,不是RuntimeException类及其子类的异常实例则被称为checked异常【编译时异常】,在java中编译时异常,是必须处理的,否则无法通过编译。而Runtime异常则更加灵活,Runtime异常无须显式声明抛出或使用try-catch来捕获

    2》Java异常处理机制

    关于异常处理:在Java中有两种方式:方式一是:try-catch-finally 方式二是:throws抛出

    Java中,处理异常的语句由try-catch-finally三部组分组成,其中,try块用于处理业务的,catch块用于捕获并处理异常的,而finally是用于回收资源,并且不管是否发生异常finally块一定被执行

    使用throws抛出异常,当程序出现错误时,系统自动抛出异常。一般在java中判断某项错误的条件成立时,可以使用throw关键字向外抛出异常,让调用者来处理该异常,可以一直向上抛出异常让JVM处理【JVM处理方式一般只是打印异常信息】,使用方式是:如果当前方法不知道该如何处理这个异常时就可以将异常抛出,让调用者来处理

    3》finally是无条件执行的吗?

    不管在try中的代码是否现出异常,也不管哪个catch块被执行,甚至在try块或catch块中执行了finally块总会被执行

    注意事项:

    如果在trycatch块中使用System.exit()方法,来退出java虚拟机,则finally块将不会被执行,我们一般不会退出java虚拟机,因为我们想要程序一直正常的执行下去

    4》在finally中return会发生什么?

    参考答案

    在通常情况下,不要在finally块中使用return、throw等导致方法终止的语句,一旦在finally块中使用了return、throw语句,将会导致try块、catch块中的return、throw语句失效。

    详细解析

    当Java程序执行try块、catch块时遇到了return或throw语句,这两个语句都会导致该方法立即结束,但是系统执行这两个语句并不会结束该方法,而是去寻找该异常处理流程中是否包含finally块,如果没有finally块,程序立即执行return或throw语句,方法终止;如果有finally块,系统立即开始执行finally块。只有当finally块执行完成后,系统才会再次跳回来执行try块、catch块里的return或throw语句;如果finally块里也使用了return或throw等导致方法终止的语句,finally块已经终止了方法,系统将不会跳回去执行try块、catch块里的任何代码。

    5》常见的运行时异常
    1. NullPointerException:空指针异常
    2. ArithmeicException:数学运算异常
    3. ArrayIndexOutOfBoundsException:数组下标越界异常
    4. ClassCastException:类型转换异常
    5. NumberFormatException:数字格式不正确异常
    6》编译异常

    介绍:编译异常是指在编译期间,就必须处理的异常,否则代码不能通过编译

    常见的编译异常:

    SQLException:操作数据库时,查询表可能发生异常

    IOException:操作文件时,发生的异常

    FileNotFoundException:当操作一个不存在的文件时,发生异常

    ClassNotFoundException:加载类,而该类不存在时,异常

    EOFException:操作文件,到文件未尾,发生异常

    ILLegaIArguementException:参数异常

    7.包装类

    1》为什么有包装类?

    Java语言面向对象语言,其设计理念是一切皆对象,但是8种基本数据类型却出现了例外,它们不具备对象的特性,所以Java每一个基本数据类型都定义了一个对应的引用类型,就是包装类

    2》包装类和基本数据的转换

    演示:包装类和基本数据类型的相互转换,这里以intInteger演示:

    1. jdk5前的手动装箱和拆箱方式,**装箱:**基本类型 --》包装类型,反之,拆箱
    2. jdk5以后(包含jdk5)的自动装箱和拆箱方式
    3. 自动装箱底层调用的是valueOf方法,比如Integer.valueOf()
    4. 自动拆箱底层调用的是intValue方法,比如对象名.intValue()
    5. 其它的包装类用法类似
    valueOf源码:
    public static Integer valueOf(int i) {
    	//low:范围是:-128
    	//high:范围是:127
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    题目:
    
    示例1:引用数据类型
    Integer i1 = new Integer(127);
    Integer i2 = new Integer(127);
    System.out.println(i1==i2);//false
    
    示例2:引用数据类型
    Integer i3 = new Integer(128);
    Integer i4 = new Integer(128);
    System.out.println(i3==i4);//false
    
    示例3:引用数据类型:底层调用了 Integer.valueOf();
    Integer i5 = 127;
    Integer i6 = 127;
    System.out.println(i5==i6);//true
    
    示例4:引用数据类型
    Integer i7 = 128;
    Integer i8 = 128;
    System.out.println(i7==i8);//false
    
    示例5:引用数据类型
    Integer i9 = 127;
    Integer i10 = new Integer(127);
    System.out.println(i9==i10);//false
    
    示例6:一个基本数据类型一个引用数据类型 == 号比较就是值
    Integer i11 = 127;
    int i12 = 127;
    System.out.ptintln(i11==i12);//true
    
    示例7:一个基本数据类型一个引用数据类型 == 号比较就是值
    Integer i13 = 128;
    int i14 = 128;
    System.out.println(i13==14);//true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36

    只要是基本数据类型,"=="号判断是值是否相等

    只要是引用数据类型,"=="号判断是地址值是否相等

    如果一个是基本数据类型,另一个是引用数据类型,则判断还是值是否相等

    8.String

    1. String对象用于保存字符串,也就是一组字符序列
    2. String是一个final类,代表不可变的字符序列
    3. 字符串是不可变的,一个字符串对象一旦被分配,其内容是不可变的
    4. 字符串的字符使用unicode字符编码,一个字符【不区分字母还是汉字】占两个字节
    5. String类中有一个非常重要的属性是private final char value[];value指向的地址是不可以改变的 ,而的存在的单个字符是可以改变的
    6. String类中的value值是存在常量池中地址的,就是说value值是用于维护常量池的地址,是不能改变的,改变的只能是常量池的内容,如用new的方式创建对象【才有用value属性维护常量池的地址】
    创建String对象的两种方式

    方式一:直接赋值 String s = "hsp";

    方式二:调用构造器String s = new String("hsp");

    两种创建String对象的区别

    1.方式一:先从常量池查看是否有"hsp"数据空间,如果有,直接指向;如果没有重新创建,然后指向。s(对象引用名)最终指向的是常量池的空间地址

    2.方式二:先在堆中创建空间,里面维护了一个value属性值【是用于存在常量的空间地址的】,指向常量池的hsp空间。如果常量池没有hsp,重新创建,如果有,直接通过value指向。最终指向的是堆中的空间地址

    重要规则:
    String c = “ab”+“cd”;常量相加,看的是常量池
    String c = a + b;变量相加,是在堆中【一个变量一个常量相加,也是堆中】

    题1:

    String  a = "hello" + "abc";
    创建了几个对象?只有1对象。
    因为是: String a = "hello" + "abc";//底层优化成 String a = "helloabc";
    
    分析是:
    1.底层编译器做一个优化,判断创建的常量池对象,是否有引用指向
    2.String a = "hello"+"abc"; => String a = "helloabc";
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    题2

    String  a = "hello";//创建了对象a
    String  b = "abc";//创建了b对象
    String c = a + b;//创建了几个对象?画出内存图?
    关键就是分析String  c = a + b;到底是如何执行的
    一共有3对象,如图。
    
    总结:底层是:1.StringBuilder sb = new StringBuilder();
    2.sb.append(a);
    3.sb.append(b);
    4.sb是在堆中,并且append是在原来字符串的基础上追加的
    5.再调用StringBuilder类中的toString方法
        【所以指向的地址是指向堆中的,堆有一个value维护着常量池的属性】
    
    重要规则:
    String c = "ab"+"cd";常量相加,看的是常量池
    String c = a + b;变量相加,是在堆中【一个变量一个常量相加,也是堆中】
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    String常用方法

    equals

    equalslgnoreCase

    length

    indexOf

    lastIndexOf

    substring

    trim

    charAt:获取某个索引处字符,注意是不能使用str[index]的方式,必须使用charAt获取单个字符

    toUpperCase

    toLowerCase

    replace:替换

    split:分割

    concat:拼接字符串

    compareTo:比较两个字符串

    toCharArray:转换字符数组

    format:格式字符串

    9.StringBufferStringBuilder

    StringBufferStringBuilder的区别:

    StringBufferStringBuilder都代表可变的字符串对象,它们共同的父类是AbstractStringBuilder,并且两个类构造器和方法也是相同的,只是StringBuffer是线程安全的,而StringBuilder是线程不安全的,效率更高

    StringBufferStringBuilder是可变的字符串对象,因为是:在父类中 AbstractStringBuffer有属性 char[] value,不是final 该 value 数组存放在字符串内容,引出存在在堆中的

    StringBufferStringBuilder常用方法:

    append

    delete

    replace

    indexOf

    insert

    length

    1》使用字符串时,new ""推荐使用哪种方式?

    先看hellonew String("hello")的区别:

    • java直接使用hello字符串直接量时,jvm将使用常量池管理这个字符串
    • 当使用new String("hello")时,jvm先使用常量池来管理hello直接量,再调用String类的构造器创建一个新的String对象,新创建的String对象保存在堆内存中

    显然,采用new的方式会多创建一个对象,会占用更多的内存,所以一般建议使用直接量方式创建字符串

    2》 StringStringBufferStringBuilder

    1.StringBuilderStringBuffer非常类似,均代表可变的字符序列,而且方法也一样的

    2.String:是不可变字符序列,效率低,但是复用率高

    3.StringBuffer:可变字符序列,效率较高【增删】,线程安全,看源码

    4.StringBuilder:可变字符序列,效率最高,线程不安全

    5.String使用注意说明

    • String s = "a";//创建了一个字符串
    • s += "b";//实际上原来的a字符串已经丢弃了,现在又产生了一个字符串s+b【也就是"ab"】。如果多次执行这些改变串内容的操作,会导致大量副本字符串对象存留在内存中,降低效率。如果这样的操作放到循环中,会极大影响程序的性能 ==》结论:如果我们对String做大量修改,不要使用String
    3》 StringStringBufferStringBuilder的选择
    1. 如果字符串存在大量的修改操作,一般使用 StringBuffer【多线程情况下】或 StringBuilder【单线程情况下】
    2. 如果字符串存在大量的修改,并在单线程的情况下,使用StringBuilder
    3. 如果字符串存在大量的修改,并在多线程的情况下,使用StringBuffer
    4. 如果我们字符串很少修改,被多个对象引用,使用String,比如配置文件等

    String StringBuffer StringBuilder区别:

    String底层是使用final修饰的char[]代表是不可变的字符序列,只要每次操作String字符串,则每次都会生成新的对象,消耗内存

    StringBufferStringBuilder都代表可变的字符串对象,它们共同的父类是AbstractStringBuilder,并且两个类构造器和方法也是相同的,只是StringBuffer是线程安全的,而StringBuilder是线程不安全的,效率更高

    从效率上:StringBuilder大于StringBuffer大于String

    10.Math工具类

    ceil
    floor
    round
    sqrt
    random

    11.Arrays

    Arrays类常见方法案例

    Arrays里面包含了一系列静态方法,用于管理或操作数组【比如排序和搜索】

    1.toString 返回数组的字符串形式
    	Arrays.toString(arr);
    
    2.sort 排序【自然排序和定制排序】[重点]
    
    3.binarySearch 通过二分搜索法进行查找,要求必须排好序
    int index = Arrays.binarySearch(arr,3);
    
    4.copyOf 数组元素的复制
    Integer newArr = Arrays.copyOf(arr,arr.length);
    
    5.fill数组元素的填充
    Integer[] num = new Integer[]{9,6,3};
    Arrays.fill(num,8);//将数组num中的所有元素都替换成8
    
    6.equals 比较两个数组元素内容是否完全一致
    boolean equals = Arrays.equals(arr,arr2);//如果数组arr和arr2完全一样返回true,否则返回false
    
    7.asList 将一组值,转换成List集合
    List<Integer> asList = Arrays.asList(2,3,43,5,6,78,168);
    System.out.println("asList="+asList);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    12.System

    System类常见方法和案例

    1.exit: 退出当前程序

    2.arraycopy:复制数组元素,比较适合底层调用,一般使用Arrays.copyOf完成复制数组

    int[] scr = {1,2,3};

    int[] dest = new int[3];

    System.arraycopy(src,0,dest,0,3);

    3.currentTimeMillens :返回当前时间距离1970-1-1的毫秒数

    4.gc:运行垃圾回收机制System.gc();

    JavaSE补充部分

    1.构造器

    1. 构造器必须顺序与类名字相同,并且不能有返回值(返回值也不能为void
    2. 每个类可以有多个构造方法,形成构造器的重载,当我们没有编写构造器系统默认会提供一个无参构造器
    3. 构造器的作用是完成对成员变量的初始化
    4. 构造器总是随着new操作被系统调用,并且对于一个对象而言,只会被调用一次,所以说构造器是系统调用的
    5. 构造器不能被继承,所以倒致构造器不能被重写
    6. 子类可以通过super关键字来显式调用父类构造器,完成对父类的初始化,默认情况下调用是父类无参构造器,当父类没有无参构造器时,子类必须使用super关键字指定调用父类那个构造器完成父类初始化

    2.多态的实现机制

    多态就是代码重用的一种机制,表示当同一个对象操作在不同的对象的时候,会产生不同的结果

    1)重载

    重载是指同一个对象中有多个同名的方法时,但是这些方法参数不同,在通过不同的参数调用不同的方法,就是可以体现方法的多态性

    2)重写

    子类可以重写父类的方法,可以看出同样的方法会在父类与子类中有着不同的表现形式,比如:父类的引用可以指向子类实例对象,同时接口引用也可以指向子类实例对象,在程序运行时运行期才确定调用是哪个方法【这种该用方法的方式就是动态绑定机制】,就是可以体现重写实现多态

    3.重载(Overload)和重写(Override)

    重载:

    • 方法名必须相同
    • 形参列表必须不同,可以是形参类型不同,个数不同,顺序不同
    • 与返回值无关

    重写:

    • 1.子类的方法的参数和方法名必须要和父类方法的参数,方法名完全一样
    • 2.子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类
    • 比如:父类返回是Object,子类方法可以返回类型是String
    • 3.子类方法不能缩小父类方法的访问权限,但是可以相同或扩大

    方法重载overLoad和方法重写override

    名称发生范围方法名参数列表返回类型修饰符
    重载overLoad本类必须相同类型,个数或者顺序至少有一个不同无要求无要求
    重写override父子类必须相同必须相同子类重写的方法,返回的类型和父类返回的类型一样,或者是其子类子类方法不能缩小父类方法的访问范围

    3.abstract抽象和interface接口

    相同点:

    • 抽象类和接口都不能被实例化
    • 都可以包含抽象方法
    • 都生成.Class文件

    抽象类:

    • 抽象类不一定要包含有abstract方法,也就是说,抽象类可以没有abstract方法,并且抽象方法可以有构造器

    • 一旦类中包含有abstract方法,则这个类必须声明成abstract

    • abstract只能修饰类和方法,不能修饰属性

    • 抽象类可以有任意成员【因为抽象类本质还是类,只是多了抽象方法】

    • 抽象方法不能有方法体

    • 如果一个类继承了抽象类,要么将所有抽象方法都实现,要么将自己声明为abstract

    • 抽象类是单继承关系

    接口:

    • jdk7前接口里的所有方法都没有方法体,即都是抽象方法,并且变量必须是public static final修饰的,方法都是public abstract修饰也是默认的
    • jdk8以后可以有静态方法,默认方法default修饰的,也就是说接口中可以有方法具体实现
    • 注意:接口不能成员属性,只有常量,由于jdb8.0后有默认方法,但是可以在默认方法中定义局部变量
    • 接口是多继承关系

    4.hashCodeequals==关系

    hashCode用于获取哈希码(散列码),equals是用于比较两个对象是否相等,

    • 如果两个对象相等,则它们必须有相同的哈尔码
    • 如果两个对象相同的哈希码,则它们未必相等

    ==运算符

    • 如果比较是基本数据类型,是比较两个数值是否相等
    • 如果比较是引用数据类型,比较是两个对象内存地址是否相等,判断它们是否是同一个对象

    equals方法

    • 没有重写时,默认是Object中的equals方法是以==来实现的,比较两个对象内存地址是否相同,往往我们都进行重写,用于判断两个对象的内容是否相同,如果相同则认为对象相等,否则不相等

    为什么equals方法和hashCode方法一起重写?

    原因是对于hashCode相同对象,才会进一步比较是否equals来判断他们是否相等,这是实现快速查找的关键

    5.浅拷贝和深拷贝

    步骤克隆方法的步骤:

    第一步:实现Cloneable接口

    第二步:重写clone方法

    浅拷贝:

    被复制对象的所有变量都含有与原来的对象相同的值,而所有的对其他对象的引用仍然指向原来的对象,就是说:浅拷贝仅仅复制了所有考虑的对象,而不是复制它所引用的对象

    深拷贝:

    被复制对象的所有变量都含有与原来的对象相同的值,而那些引用其他对象的变量将指向被复制边的新对象,而不再是原有的那些被引用 的对象,就是说:深拷贝把要复制的对象所引用的对象都复制了一遍

    6.面向对象特征有哪些?

    抽象:抽象是将一类对象的共同特征总结出来构造类的过程,包括数据抽象和行为抽象两方面。抽象只关注对象有哪些属性和行为,并不关注这些行为的 细节是什么。

    继承:继承是从已有类得到继承信息创建新类的过程。提供继承信息的类 被称为父类(超类、基类);得到继承信息的类被称为子类(派生类)。继承让 变化中的软件系统有了一定的延续性,同时继承也是封装程序中可变因素的重要 手段。

    封装:通常认为封装是把数据和操作数据的方法绑定起来,对数据的访问 只能通过已定义的接口。面向对象的本质就是将现实世界描绘成一系列完全自治、封闭的对象。我们在类中编写的方法就是对实现细节的一种封装;我们编写 一个类就是对数据和数据操作的封装。可以说,封装就是隐藏一切可隐藏的东西, 只向外界提供最简单的编程接口(可以想想普通洗衣机和全自动洗衣机的差别, 明显全自动洗衣机封装更好因此操作起来更简单;我们现在使用的智能手机也是 封装得足够好的,因为几个按键就搞定了所有的事情)。

    多态性:多态性是指允许不同子类型的对象对同一消息作出不同的响应。 简单的说就是用同样的对象引用调用同样的方法但是做了不同的事情。多态性分 为编译时的多态性和运行时的多态性。如果将对象的方法视为对象向外界提供的 服务,那么运行时的多态性可以解释为:当 A 系统访问 B 系统提供的服务时,B 系统有多种提供服务的方式,但一切对 A 系统来说都是透明的(就像电动剃须 刀是 A 系统,它的供电系统是 B 系统,B 系统可以使用电池供电或者用交流电, 甚至还有可能是太阳能,A 系统只会通过 B 类对象调用供电的方法,但并不知道 供电系统的底层实现是什么,究竟通过何种方式获得了动力)。方法重载 (overload)实现的是编译时的多态性(也称为前绑定),而方法重写(override) 实现的是运行时的多态性(也称为后绑定)。运行时的多态是面向对象最精髓的 东西,要实现多态需要做两件事:1). 方法重写(子类继承父类并重写父类中已 有的或抽象的方法);2). 对象造型(用父类型引用引用子类型对象,这样同样 的引用调用同样的方法就会根据子类对象的不同而表现出不同的行为。

    7.数组有没有 length()方法?String 有没有 length()方法?

    答: 数组没有 length()方法,有 length 的属性。String 有 length()方法。JavaScript 中,获得字符串的长度是通过 length 属性得到的,这一点容易和 Java 混淆。

    泛型介绍【都是引用数据类型】

    1.泛型又称为参数化类型,是jdk5出现的新特性,解决数据类型的安全性问题

    2.在类声明或实例化时只要指定好需要的具体的类型即可

    3.java泛型可以保证如果程序在编译时没有发出警告,运行时就不会产生ClassCastException异常,同时,代码更加简洁,健壮

    4.泛型的作用是:可以在类声明时通过一个标识表示类中某个属性的类型,或者是某个方法的返回国值的类型,或者是参数类型【重要点】

    泛型是作用范围是:

    1.标识类中某个属性

    2.某个方法返回值类型

    3.参数类型

    特别注意是:E类型在编译时就已经确定了,才能起到检查数据类型的作用

    package collection_.collectionP.generic_;
    
    /**
     * @author: 海康
     * @version: 1.0
     */
    public class Generic03 {
        public static void main(String[] args) {
    //       注意是: E 此类型是在编译期间就确定了,所以才能起到检查数据安全性
            Person<Integer> integerPerson = new Person<>(100);
            integerPerson.show();
            /**
             * 加入泛型代码相当于下面
             * class Person {// E表示是标识某个属性的类型,即在编译期间就确定了,所以才能起到检查数据安全性
             *
             *     Integer  e;// E 此类型是在编译期间就确定了,所以才能起到检查数据安全性
             *
             *     public Person(Integer e){ // 泛型可以作为一个参数
             *         this.e = e;
             *     }
             *
             *     public Integer f(){
             *         return e;// 泛型可以作为一个返回值类型
             *     }
             *
             *     public void show() {
             *         System.out.println(e.getClass());//查看e运行类型
             *     }
             * }
             */
        }
    }
    
    /**
     * 泛型的作用是:可以在类声明时通过一个标识表示某个属性的类型,
     * 或者是某个方法返回值的类型,或者是参数类型
     * @param 
     */
    class Person<E> {// E表示是标识某个属性的类型,即在编译期间就确定了,所以才能起到检查数据安全性
    
        E  e;// E 此类型是在编译期间就确定了,所以才能起到检查数据安全性
    
        public Person(E e){ // 泛型可以作为一个参数
            this.e = e;
        }
    
        public E f(){
            return e;// 泛型可以作为一个返回值类型
        }
    
        public void show() {
            System.out.println(e.getClass());//查看e运行类型
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    泛型的好处:

    1. 编译时,检查添加元素的类型,提高了数据安全性
    2. 减少类型转换的操作,提高效率

    面试回答:

    1. 泛型是jdk5出现的新特性,解决向下强转的操作,达到解决数据类型的安全性问题
    2. 在类声明或实例化时只要指定好需要的具体类型即可,并且泛型都是引用数据类型的
    3. 泛型可以保证在编译就是数据类型,达到检查数据类型的作用
    4. 如果某个类或方法或参数需要指定泛型,当我们没有指定泛型时默认就是Object类型
    5. 并且在给泛型指定具体类型后,可以传入该类型或子类类型
    6. 泛型使用形式可以使用菱形泛型形式
    7. 泛型不支持继承

    自定义泛型类

    基本语法:

    class  类名<T,R,...>{// 表示可以有多个泛型
    	成员
    }
    
    • 1
    • 2
    • 3

    注意细节:

    1.普通成员可以使用泛型【属性,方法,返回值类型】

    2.使用泛型的数组,不能初始化【就是不能直接new,因为不能确定数据类型,无法分配空间,可以声明不能new

    3.静态方法和静态属性中不能使用类的泛型【静态方法和静态属性是随着类加载而加载的】

    4.泛型类的类型,是在创建对象时确定的【因为创建对象时,需要指定确定类型】

    5.如果在创建对象时,没有指定类型,默认是Object

    自定义泛型接口

    基本语法:

    interface 接口<T,R,...>{
    	成员
    }
    
    • 1
    • 2
    • 3

    注意细节:

    1.接口中,静态成员【静态成员方法和静态成员属性,但是接口中的属性都是静态的所以属性不能使用泛型】不能使用泛型【和泛型类规定一样】

    2.泛型接口的类型,在继承接口或实现接口时确定

    3.没有指定类型,默认为Object

    必须理解下面代码:

    package collection_.collectionP.generic_;
    
    /**
     * @author: 海康
     * @version: 1.0
     */
    public class Generic05 {
        public static void main(String[] args) {
    
        }
    }
    
    interface IUsb<T,R> {
        //普通成员方法中,可以使用泛型
    //    T num ; 错误的,因为在接口中的属性不能使用泛型
    
        // 普通方法中使用泛型
        T getT();
    
        default R method(){//默认方法使用泛型
            return null;
        }
    }
    
    // 泛型接口的类型,在继承接口或实现接口时确定
    // 在继承接口时,确定泛型
    // String会自己替换T Integer会自己替换R
    interface myInt extends IUsb<String,Integer>{}
    class myClass implements myInt{
        @Override
        public String getT() {
            return null;
        }
    }
    // 实现接口类指定泛型
    class A implements IUsb<String ,Integer>{
        @Override
        public String getT() {
            return null;
        }
    }
    
    // 如果没有指定泛型默认是Object
    class B implements IUsb{
        @Override
        public Object getT() {
            return null;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49

    自定义泛型方法

    在调用泛型方法时,传入的参数,编译器就可以确定类型

    基本语法:

    修饰符<T,R...>返回类型 方法名(参数列表){}
    
    • 1

    注意细节:

    1.泛型方法中,可以定义在普通类中,也可以定义在泛型类中

    2.当泛型方法被调用时,类型会确定

    3.一定要理解下面的细节

    如果在泛型类定义了泛型方法,一般泛型类的泛型标识符是和泛型方法的泛型标识符是不一样的【也可以一样】

    一般在泛型方法中使用的参数要和泛型方法标识一致

    public void eat(T t){}   修饰符后没有泛型占位符<T,R>,说明eat方法不是泛型方法,只是使用了泛型
    
    • 1

    非常注意是:一般泛型方法要传入参数,而传入的参数一般是泛型方法的标识 ,如果是在泛型类中定义的泛型方法也可以传入泛型类的标识【但是一般会的都是泛型方法的标识 】

    一定要理解下面的代码

    package collection_.collectionP.generic_;
    
    /**
     * @author: 海康
     * @version: 1.0
     */
    public class CustomMethodGeneric {
        public static void main(String[] args) {
    
        }
    }
    // 泛型方法可以定义在普通类中,也可以定义在泛型类中
    class myGeneric {
        // 定义泛型方法【定义在普通类中】
        public <T,R> void fi(T t,R r){}//一般泛型方法的泛型和传入的参数的泛型一致
        // 【就是 和 fi(T t,R r)】
    }
    
    // 在泛型类中定义泛型方法
    class MG<T,R>{
    
        //定义泛型方法
        public <K,V> void getKV(K k,V v){//一般泛型方法的标识和泛型类的标识是不一致的
            // 一般使用的泛型参数就泛型方法标识 ,也可以使用泛型类的标识
        }
        public <K,V> void getKV2(T t,V v){//一般泛型方法的标识和泛型类的标识是不一致的
            // 一般使用的泛型参数就泛型方法标识 ,也可以使用泛型类的标识
        }
    
        // 注意下面的方法不是泛型方法,只是使用了泛型
        public void get(T t){}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    package com.hspedu.customgeneric;
    
    import java.util.ArrayList;
    
    /**
     * 泛型方法的使用
     */
    @SuppressWarnings({"all"})
    public class CustomMethodGeneric {
        public static void main(String[] args) {
            Car car = new Car();
            car.fly("宝马", 100);//当调用方法时,传入参数,编译器,就会确定类型
            System.out.println("=======");
            car.fly(300, 100.1);//当调用方法时,传入参数,编译器,就会确定类型
    
            //测试
            //T->String, R-> ArrayList
            Fish<String, ArrayList> fish = new Fish<>();
            fish.hello(new ArrayList(), 11.3f);
        }
    }
    
    //泛型方法,可以定义在普通类中, 也可以定义在泛型类中
    class Car {//普通类
    
        public void run() {//普通方法
        }
        //说明 泛型方法
        //1.  就是泛型
        //2. 是提供给 fly使用的
        public <T, R> void fly(T t, R r) {//泛型方法
            System.out.println(t.getClass());//String
            System.out.println(r.getClass());//Integer
        }
    }
    
    class Fish<T, R> {//泛型类
        public void run() {//普通方法
        }
        public<U,M> void eat(U u, M m) {//泛型方法
    
        }
        //说明
        //1. 下面hi方法不是泛型方法
        //2. 是hi方法使用了类声明的 泛型
        public void hi(T t) {
        }
        //泛型方法,可以使用类声明的泛型,也可以使用自己声明泛型
        public<K> void hello(R r, K k) {
            System.out.println(r.getClass());//ArrayList
            System.out.println(k.getClass());//Float
        }
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54

    泛型的继承和通配符说明【必须理解重点】

    在这里插入图片描述

    1.泛型不具备继承性

    List list = new ArrayList();这里错误的两边的泛型必须一致,否则编译不通过

    2.:支持任意泛型类型

    3.:支持A类以及A类的子类,规定了泛型的上限

    4.:支持A类以及A类的父类,不限直接父类,规定了泛型的下限

    支持任意通配符 支持A类以及A类的子类,规定是上限 支持A类的父类,规定了下限

    在这里插入图片描述

    package collection_.collectionP.generic_.generic_01;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author: 海康
     * @version: 1.0
     */
    public class GenericExtends {
        public static void main(String[] args) {
            ArrayList<Object> list1 = new ArrayList<>();
            ArrayList<String> list2 = new ArrayList<>();
            ArrayList<AA> list3 = new ArrayList<>();
            ArrayList<BB> list4 = new ArrayList<>();
            ArrayList<CC> list5 = new ArrayList<>();
            
            // ? 表示了可以接受任意类型 是一个通配符
            printCollection01(list1);
            printCollection01(list2);
            printCollection01(list3);
            printCollection01(list4);
            printCollection01(list5);
    
            // ? extends AA 表示上限,可以接受 AA 或 AA子类
    //        printCollection02(list1); 错误
    //        printCollection02(list2); 错误
            printCollection02(list3);
            printCollection02(list4);
            printCollection02(list5);
    
            // ? super AA 表示下限,可以接受 AA 的父类,不限于直接父类,规定下限
            printCollection03(list1);
    //        printCollection03(list2); 错误
            printCollection03(list3);
    //        printCollection03(list4); 错误
    //        printCollection03(list5); 错误
        }
    
        // 通配符,取出时,就是Object
        public static void printCollection01(List<?> list) { }
    
        // ? extends AA 表示上限,可以接受 AA 或 AA子类
        public static void printCollection02(List<? extends AA> list){ }
    
        // ? super AA 表示下限,可以接受 AA 的父类,不限于直接父类,规定下限
        public static void printCollection03(List<? super AA> list){ }
    }
    
    class AA {}
    class BB extends AA{}
    class CC extends BB{}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    集合面试

    1.集合继承体系图

    Collection接口 特点 方法

    Collection接口的子接口:List实现类:

    ArrayList LinkedList Vector

    Collection接口的子接口:Set实现类 :HashSet TressSet LinkedHashSet

    注意是:Collection接口的父接口是Iterable接口

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vbc8knV0-1658239723736)(C:\Users\海康\Desktop\面试文档\img\1658196141871.png)]

    Map接口 特点 方法 遍历方式

    Map接口的实现类 :HashMap Hashtable

    Collections工具类的使用

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HuBxa0U1-1658239723738)(C:\Users\海康\Desktop\面试文档\img\1658196845121.png)]

    特点:

    1. Collection 实现子类可以存放多个元素,每个元素可以是Object
    2. 有些Collection的实现类,可以存放重复的元素,有些不可以
    3. 有些Collection的实现类,有些是有序的【List】,有些是无序的【Set
    4. Collection接口没有直接的实现子类,是通过它的子接口SetList来实现的

    2.Collection接口常用方法

    1.  add:添加单个元素
    2.  remove:删除指定元素
    3.  contaions:查找元素是否为空
    4.  size:获取元素个数 
    5.  isEmppty:判断是否为空
    6.  clear:清空
    7.  addAll:添加多个元素
    8.  contaionsAll:查找多个元素是否都存在
    9.  removeAll:删除多个元素
    10. toArray:返回Object[]
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    使用Collections工具类提供的synchronizedxxx()方法,将这些集合类包装成线程安全的集合类

    3.List接口和常见方法

    List接口基本介绍:

    1. List集合类中元素有序【即添加顺序和取出顺序一致】并且可以重复
    2. List集合中的每个元素都有其对应的顺序索引,即支持索引
    3. List容器中的元素都对应一个整数型的序号记载其在容器中的位置,可以根据序号存取容器中的元素
    4. List接口可以存放null
    List接口常用方法

    List集合里添加了一些根据索引来操作集合元素的方法

    插入
    1.   boolean add(Object ele):在集合中尾部插入ele元素
    1.   void add(int index,Object ele):在index位置插入ele元素    
    2.   boolean  addAll(int index,Collection eles):在index位置开始将eles中的所有元素添加进来
    
    查询
    3.   Object get(int index):获取指定index位置的元素
    4.   int indexOf(Object obj):返回obj在集合中首次出现的位置
    5.   int lastIndexOf(Object obj):返回obj在当前集合中未次出现位置
    
    移除
    6.   Object remove(int index):移除指定index位置的元素,并返回此元素
      
    替换
    7.   Object set(int index,Object ele):设置指定index位置的元素为ele,相当于是替换
    
    截取    
    8.   List  subList(int fromIndex,int toIndex):rfromIndex到toIndex位置的子集合【相当于截取成子集合】
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    List的三种遍历方式
    1. 方式一:使用iterator【迭代器方式】

    2. 方式二:使用增强for循环

    3. 方式三:使用普通for循环

      【使用普通 for 循环需要使用两个方法:一个获取多少个元素【size()方法】,一个是获取元素方法【get(int index)】】

    4.ArrayList底层

        ArrayList<String> integers = new ArrayList<>();
                for (int i = 0; i < 3; i++) {
                integers.add("A_"+i);
        }
        
        源码分析:
        1.先创建一个 ArrayList 对象 并完成初始化大小为0,当第一次添加元素时,扩容为10大小,以后扩容时,按照1.5倍扩容 
        
        private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {}; 创建一个空数组
        transient Object[] elementData;null 说明ArrayList底层是维护了一个 elementData 数组
        public ArrayList() {
            this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;将空数组赋给 elementData
            所以使用是无参构造创建 ArrayList 对象时 开始大小是 0
        }
        
        2.添加元素
        private int size; // 当集合中没有元素时 size 为 0
        public boolean add(E e) {
            在添加前先确定当前容量是否能够添加一个元素
            ensureCapacityInternal(size + 1);  // Increments modCount!!
            elementData[size++] = e; 将当前的元素添加到elementData数组指定索引的位置
            return true;
        }
        
        3.确保当前的容量大小
        private void ensureCapacityInternal(int minCapacity) {
            ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));
        }
        
        
        private static final int DEFAULT_CAPACITY = 10;
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
            如果是第一次添加时,先确定elementData是否是一个空数组,则 if 语句成立,将返回一个 DEFAULT_CAPACITY 的值就是 10
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
        }
        
        确保是否需要扩容 
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++; 记录当前ArrayList集合被修改的次数
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        
        扩容代码如下
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length; 第一次添加时 elementData是0
            int newCapacity = oldCapacity + (oldCapacity >> 1); 
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            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);
        }
    
    
    所以结论是:当使用无参构造器创建对象时,第一次添加时,扩容的大小为10,当再次扩容时,按照以前的1.5
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63

    总结:

    第一步:先创建一个ArrayList对象,并且ArrayList底层维护是一个Object[]数组,初始化大小为0,

    第二步:当我们添加一个元素时,就是调用add方法时,在add方法中先调用ensureCapacityInternal(size + 1);方法判断当前容量是否能够添加一个元素

    第三步:由于是第一次添加元素时,Object数组为空,所以if成立,返回一个默认大小为10

    private static final int DEFAULT_CAPACITY = 10;
        private static int calculateCapacity(Object[] elementData, int minCapacity) {
            如果是第一次添加时,先确定elementData是否是一个空数组,则 if 语句成立,将返回一个 DEFAULT_CAPACITY 的值就是 10
            if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
                return Math.max(DEFAULT_CAPACITY, minCapacity);
            }
            return minCapacity;
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第四步:

    确保是否需要扩容 
        private void ensureExplicitCapacity(int minCapacity) {
            modCount++; 记录当前ArrayList集合被修改的次数
    
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    第五步:

        扩容代码如下
        private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length; 第一次添加时 elementData是0
            int newCapacity = oldCapacity + (oldCapacity >> 1); 
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            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);
        }
    
    如果使用的是指定大小的构造器,则初始 elementData 容量为指定大小,如果需要扩容,则直接扩容 elementData 为 1.5
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    ArrayList的底层操作机制源码分析【必须掌握】【背下】

    结论:

    1.ArrayList中维护了一个Object类型的数组elementDatatransient Object[] elementData; transient表示瞬间,短暂的,表示该属性不会被序列号

    2.当创建ArrayList对象时,如果使用的是无参构造,则被始elementData容量为0,第1次添加,则扩容elementData为10,如需要再次扩容,则扩容elementData为1.5倍

    3.如果使用的是指定大小的构造器,则初始elementData容器为指定大小,如果需要扩容,则直接扩容elementData为1.5倍

    面试回答:

    ArrayList底层是用数组实现的存储的,并且在底层维护着一个Object[]数组,ArrayList特点是查询效率高,查询效率高的因为是提交get()方法根据索引查询,增删效率低原因是要移到数组元素的位置,并且ArrayList是线程不安全的,在一般情况下,如果大多数业务都是查询,则可以使用ArrayList因为效率高,【如果涉及到增删比较频繁则就可以选择LinkedList,如果需要线程安全则可以使用Vector

    例如:添加元素

    第一步:ArrayList底层是维护着一个Object[]数组 ,初始化大小为0

    第二步:当我们添加一个元素时,就是调用add方法时,在add方法中先调用ensureCapacityInternal(size + 1);【英说可怕S体英讨了:】方法判断当前容量是否能够添加一个元素

    第三步:在ensureCapacityInternal方法中,会先调用calculateCapacity【客Q里可怕S体】方法,比如是第一次添加,返回是默认大小10,如果不是第一次添加,则返回是当前容量大小

    第四步:调用确定是否扩容方法ensureExplicitCapacity【英说 姨:S婆喜:讨 可怕S体】方法传入当前calculateCapacity返回的大小,在该方法调用grow【哥老】方法确定是否要扩容

    5.Vector底层

       
       步骤1:
      使用是无参构造创建 Vector 对象 默认 在底层创建一个 大小为10 protected  Object[] elementData;数组
       public Vector() {
            this(10);
        }
        
        步骤2public synchronized boolean add(E e) {
            modCount++;// 对象 Vector修改的次数
            ensureCapacityHelper(elementCount + 1);
            elementData[elementCount++] = e;
            return true;
        }
        
        步骤3: 确定是否需要扩容 
        private void ensureCapacityHelper(int minCapacity) {
            // overflow-conscious code
            if (minCapacity - elementData.length > 0)
                grow(minCapacity);
        }
        
        扩容算法:;当大小不够添加元素时,是按照两倍来扩容的
       private void grow(int minCapacity) {
            // overflow-conscious code
            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                             capacityIncrement : oldCapacity);
            if (newCapacity - minCapacity < 0)
                newCapacity = minCapacity;
            if (newCapacity - MAX_ARRAY_SIZE > 0)
                newCapacity = hugeCapacity(minCapacity);
            elementData = Arrays.copyOf(elementData, newCapacity);//将原来的数组拷贝到新的数组中
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    面试回答:

    Vector底层在底层维护是一个Object[]数组

    Vector是线程同步的,即线程安全的,Vector类中操作方法都带有synchronized关键字

    如果在开发中,需要线程同步安全时,就可以考虑使用Vector

    Vector集合可以添加null

    例如:添加元素

    第一步:使用是无参构造创建 Vector 对象 默认 在底层创建一个 大小为10 protected Object[] elementData;数组,在构造器使用 this(10)

    第二步:调用add方法,并且在add方法中有一个属性modCount记录对象Vector修改的次数

    第三步:确定是否需要扩容 ,如果当前容器大小再加1,大于当前容器大小时,就进行扩容 ,扩容调用grow方法

    第四步:扩容算法,大小不够添加元素时,是按照两位扩容 的

            int oldCapacity = elementData.length;
            int newCapacity = oldCapacity + ((capacityIncrement > 0) ?
                                             capacityIncrement : oldCapacity);
    
    • 1
    • 2
    • 3

    其实在ArrayListVector底层扩容方法要都到Arrays工具类中的cpoy方法,在cpoy方法中其实使用到了System.arraycopy方法

    6.LinkedList底层

    LinkedList的全面说明:

    1. LinkedList底层实现了双向链表和双端队列特点
    2. 可以添加任意元素【元素可以重复】,包括null
    3. 线程不安全,没有实现同步

    LinkedList的底层操作机制

    1. LinkedList底层维护了一个双向链表
    2. LinkedList中维护了两个属性first【宝s】和last【辣s】分别指向 首节点 和 尾节点
    3. 每个节点(Node对象),里面又维护了prev【婆s】【指向上一个节点】 next【奶s】【指向下一个节点】 item【爱等】三个属性,其中通过prev指向前一个,通过next指向后一个节点,最终实现双向链表
    4. 所以LinkedList元素添加和删除,不是通过数组完成的,相对来说效率较高。
    	步骤1:使用无参构造器创建对象 
    	public LinkedList() {
        }
        会对 transient int size = 0; 属性进行初始化,表示当前大小为0
        
        
        步骤2:添加方法
            public boolean add(E e) {
           		 linkLast(e);
            	 return true;
       	    }
       	    
       	 步骤3:添加方法
       	 transient Node<E> last; 
       	 void linkLast(E e) {
            final Node<E> l = last;链表最后一个元素赋给 l
            final Node<E> newNode = new Node<>(l, e, null);
            last = newNode; 将last指向新添加元素
            if (l == null) 当前集合为空,就使first指向第一个元素
                first = newNode;
            else 否则 l 指向下一个元素
                l.next = newNode;
            size++; 表示当前集合的大小
            modCount++; 表示当前集合被修改的次数
        }
        
        调用内容类,完成相关属性初始化
        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;
            }
        }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    7.Set集合

    Set接口基本介绍
    1. 无序【添加和取出的顺序不一致】

    2. 不允许重复元素,所以最多包含一个null【就是可以存放一个null

    注意:取出的顺序虽然不是添加的顺序,但是它取出顺序是固定的

    8.List集合和Set集合区别

    List代表是有序的,元素可以重复

    Set代表是无序的,元素不可以重复

    9.HashSet集合

    面试回答:

    分析HashSet底层是HashMapHashMap底层是【数组+链表+红黑树】

    下面的结论非常重要必须记住【背下】

    1. HashSet底层是HasMap,因为在HashSet的构造器创建了HashMap对象,在HashhMap中维护了一个Node节点的table数组,并且默认初始化大小为16,负载因子大小为0.75【决定了链表达到多少时进行扩容】
    2. 先获取元素的哈希值【hashCode方法】
    3. 对象哈希值进行运算,得出一个索引值即为要存放在哈希表中的位置号
    4. 如果该位置上没有其他元素,则直接存放,如果该位置上已经有其他元素,则需要进行equals判断,如果相等,则不再添加。如果不相等,则以链表的方式添加
    5. java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLO【默认是8】,并且table的大小>=MIN_TREEIFY_CAPACITY【默认64】,就会进行树化【红黑树】
    6. HashSet底层就是HashMap,第一次添加时,table数组扩容到16,临界值threshold16*加载因子(loadFactor)是 0.75=12
    7. 如果table数组使用到了临界值12,就会扩容到16*2=32,新的临界值就是32*0.75=24,依次类推
    8. java8中,如果一条链表的元素个数到达了TREEIFY_THRESHOLD(默认是8),并且是table的大小>=MIN_TREEIFY_CAPACITY(默认是64),就会进行树化【红黑树】,否则仍然采用数组扩容机制
    9. 扩容是以2倍的方式进行扩容的
    HastSet添加元素

    第一步:当我们调用add方法时,在add方法中却调用了HashMap中的put方法,并且key为我们传入元素,而value值是一个占位符private static final Object PRESENT = new Object();

    第二步:在put方法中会先调用hash方法获取元素哈尔值

    第三步:调用putVal方法将获取元素哈尔值作为参数传入,在putVal方法先判断当前表是否为空和表长度是否为0,如果是调用resize方法进行扩容,就是创建一个数组大小为16

    第四步:根据元素的哈尔值与当前数组进行运算得到在table表中的索引位置,如果该位置没有元素则直接添加,如果该元素已经有元素继续调用equals方法,如果equals值不相等时,则以链表的形式添加,如果equals相等时,以不添加,并且将原来的value值替换掉

    	步骤1:使用无参构造器:HashSet集合底层维护下面两个属性
            private transient HashMap<E,Object> map;
    
            // Dummy value to associate with an Object in the backing Map
            private static final Object PRESENT = new Object();// 这个属性在添加时当占位符使用
    		下面是使用无参构造创建一个HashSet对象,其实底层在HashSet构造器中创建一个 HashMap集合赋给 map
    		public HashSet() {
            	map = new HashMap<>();
       		}
    
    	步骤2:添加方法
        	public boolean add(E e) {
                 return map.put(e, PRESENT)==null;
        	}
    		调用 HashMap 中 put 方法 并且 PRESENT 是一个占位符,是直接 new Object()对象赋给 PRESENT
                
                
        步骤3:调用 HashMap 中 put 方法
            public V put(K key, V value) {
           		 return putVal(hash(key), key, value, false, true);
        	}
    
    		调用 hash 方法 获取一个 哈希值
           static final int hash(Object key) {
                int h;
                return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
       		 }
    
    	步骤4:真正添加方法
            在 HashMap 类中 维护一个transient Node<K,V>[] table;属性第一添加时为null
            final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                       boolean evict) {
            Node<K,V>[] tab; Node<K,V> p; int n, i;
            if ((tab = table) == null || (n = tab.length) == 0)判断当前表是否为空和表长度是否为0
                如果是就调用 resize()方法进行扩容,就是创建一个数组
                n = (tab = resize()).length;
            if ((p = tab[i = (n - 1) & hash]) == null)如果当前位置没有元素直接添加
                tab[i] = newNode(hash, key, value, null);
            else {如果有元素进行如下判断 
                Node<K,V> e; K k;
                  如果 hash 值和key指定向相同位置或equals值相同时 则不添加
                if (p.hash == hash &&
                    ((k = p.key) == key || (key != null && key.equals(k))))
                    e = p;
                else if (p instanceof TreeNode)如果是二叉树进行如下操作
                    e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
                else {
                    无限循环,进行如下判断
                    for (int binCount = 0; ; ++binCount) {
                        if ((e = p.next) == null) {如果当前下一个位置为null时直接添加
                            p.next = newNode(hash, key, value, null);
                            if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st、
                                判断一条链是否达到了8个,调用 treeifBin 方法,进行树化,注意同时还要table达到64
                                treeifyBin(tab, hash);
                            break;
                        }
                        如果两者相同则不添加
                        if (e.hash == hash &&
                            ((k = e.key) == key || (key != null && key.equals(k))))
                            break;
                        p = e;
                    }
                }
                  如果 key 相同时就替换值
                if (e != null) { // existing mapping for key
                    V oldValue = e.value;
                    if (!onlyIfAbsent || oldValue == null)
                        e.value = value;
                    afterNodeAccess(e);
                    return oldValue;
                }
            }
            ++modCount;记录修改的次数
            if (++size > threshold)判断是否达到了临界值,如果是进行扩容 
                resize();
            afterNodeInsertion(evict);
            return null;
        }
    
    	扩容算法
    	final Node<K,V>[] resize() {
            Node<K,V>[] oldTab = table;
            int oldCap = (oldTab == null) ? 0 : oldTab.length;
            int oldThr = threshold;
            int newCap, newThr = 0;
            if (oldCap > 0) {
                if (oldCap >= MAXIMUM_CAPACITY) {
                    threshold = Integer.MAX_VALUE;
                    return oldTab;
                }
                else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                         oldCap >= DEFAULT_INITIAL_CAPACITY)
                    newThr = oldThr << 1; // double threshold
            }
            else if (oldThr > 0) // initial capacity was placed in threshold
                newCap = oldThr;
            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;
                    if ((e = oldTab[j]) != null) {
                        oldTab[j] = null;
                        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;
                            Node<K,V> next;
                            do {
                                next = e.next;
                                if ((e.hash & oldCap) == 0) {
                                    if (loTail == null)
                                        loHead = e;
                                    else
                                        loTail.next = e;
                                    loTail = e;
                                }
                                else {
                                    if (hiTail == null)
                                        hiHead = e;
                                    else
                                        hiTail.next = e;
                                    hiTail = e;
                                }
                            } while ((e = next) != null);
                            if (loTail != null) {
                                loTail.next = null;
                                newTab[j] = loHead;
                            }
                            if (hiTail != null) {
                                hiTail.next = null;
                                newTab[j + oldCap] = hiHead;
                            }
                        }
                    }
                }
            }
            return newTab;
        }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115
    • 116
    • 117
    • 118
    • 119
    • 120
    • 121
    • 122
    • 123
    • 124
    • 125
    • 126
    • 127
    • 128
    • 129
    • 130
    • 131
    • 132
    • 133
    • 134
    • 135
    • 136
    • 137
    • 138
    • 139
    • 140
    • 141
    • 142
    • 143
    • 144
    • 145
    • 146
    • 147
    • 148
    • 149
    • 150
    • 151
    • 152
    • 153
    • 154
    • 155
    • 156

    10LinkedHashSet的全面说明

    1. LinkedHashSetHashSet的子类

    2. LinkedHashSet底层是一个LinkedHashMap,在LinkedHashSet使用super(16,0.75,true)调用父类HashSet,在HashSet的构造中创建LinkedHashMap对象,底层维护了一个数组+双向链表

    3. LinkedHashSet根据元素的hashCOde值来决定元素的存储位置,同时使用链表维护元素的次序,这使元素看起来以插入顺序保存的

    4. LinkedHashSet不允许添加重复元素

    5. LinkedHashSet中维护一个hash表和双向链表【LinkedHashSetheadtail

    6. 每一个节点有beforeafter属性,这样可以形成双向链表

    7. 在添加一个元素时,先求hash值,在求索引,确定该元素在hashtable的位置,然后将添加的元素加入到双向链表【如果已经存在,则不添加【原则和HashSet一样】】

      tail.next = new Element;//简单指定
      newElement.prev = tail;
      tail = newElement;
      
      
      • 1
      • 2
      • 3
      • 4
    8. 这样的话,我们遍历LinkedHashSet也能确保插入顺序和遍历顺序一致

    注意是由于底层维护着是一个双向链表和数组,所以插入和取出的顺序是一致的,原因是用了链表维护元素添加的顺序

    添加元素:

    第一步:添加add方法时在底层是调用了 LinkedHashMap的父类HashMap中的put方法

    第二步:在put方法中会先调用hash方法获取元素哈尔值

    第三步:调用putVal方法将获取元素哈尔值作为参数传入,在putVal方法先判断当前表是否为空和表长度是否为0,如果是调用resize方法进行扩容,就是创建一个数组大小为16

    第四步:根据元素的哈尔值与当前数组进行运算得到在table表中的索引位置,如果该位置没有元素,则创建newNode节点并且动态绑定到LinkedHashMapnewNode方法完成双向链表之间的连接,如果该元素已经有元素继续调用equals方法,如果equals值不相等时,则以链表的形式添加,如果equals相等时,以不添加,并且将原来的value值替换掉

    11.Map集合

    1. Map集合是一个双列集合,一个元素包含两个值就是key-value
    2. Map集合中的元素,keyvalue可以为任何引用类型数据
    3. map集合中key不允许重复,而value值可以重复,key值可以为null但是只有有一个,而value值可以为任意值
    4. keyvalue之间存在单向一对一关系,即通过指定的key总能找到对应的value

    12.HashMap集合

    扩容机制【和HashSet相同】到达临界值都是以2倍的方式进行扩容

    1. HashMap底层维护了Node类型的数组table,默认为null
    2. 当创建对象时,将加载因为loadfactor初始化为0.75
    3. 当添加key-value时,通过key的哈希值得到在table的索引位置,然后判断该索引处是否有元素,如果没有元素直接添加。如果该索引处有元素,继续判断该元素的key和准备加入的key是否相等,如果相等,则直接替换掉value值,如果不相等需要判断,是树结构还是链表结构,做出相应处理。如果添加时发现容量不够,则需要扩容
    4. 第1次添加,则需要扩容table容量为16,临界值threshole为12(16*0.75)
    5. 以后再扩容,则需要扩容table容量为原来的2倍(32),临界值为原来的2倍,即24,依次类推
    6. java8中如果一条链表的元素个数超过TREEIFY_THRESHOLD(默认是8),并且table的大小>=MIN_TREEIFY_CAPACITY(默认64),就会进行树化【红黑树】

    13.Hashtable集合

    1. 存放的元素是键值对象:即K-V,底层维护是Entry【n吹】数组
    2. Hashtable的键和值都不能存放null,否则会报空指针异常
    3. Hashtable使用方法基本上和HashMap一样
    4. Hashtable是线程安全的,HashMap是线程不安全的
    5. 扩容机制是以2倍再加1的方式进行扩容
    int newCapacity = (oldCapacity << 1) + 1;// 左移再加1
    
    
    • 1
    • 2

    添加元素:

    第一步:在创建Hashtable对象时,使用无参构造器创建对象,会在底层创建一个大小为11table=new Entry[initialCapacity];的数组

    第二步:调用put方法,在put方法中称判断value值是否为null,如果为空则会抛出NullPointerException()异常

    第三步:获取key的哈尔值,并且计算在table数组中的位置(hash & 0x7FFFFFFF) % tab.length;计算出在数组中索引位置

    第四步:获取当前索引位置的元素是,是否为null,如果不为null,则进入for进行相关的判断

    第五步:如果要添加的元素和原来的元素 哈希值和equals值,相同时,则将原来的value替换掉,并返回

    14.Properties集合

    1. Properties类继承自Hashtable类并且实现了Map接口,也是使用一种键值对的形式来保存数据
    2. 它使用特点和Hashtable类似
    3. PropertiesHashtable的子类所以键值都不可以存放null值,否则会报空指针异常
    4. Properties可以有相同的key,则原先的value值也会被替换掉
    5. Properties还可以用于从xxx.properties文件中,加载数据到Properties类对象,并进行读取和修改
    6. 说明:工作中xxx.properties文件通常作为配置文件,这个知识占在IO流举例

    15.TreeSet集合

    基本介绍:

    当我们使用无参构造器,创建TreeSet时,仍然是无序的

    当我们需要按照某种方式进行排序时,需要使用TreeSet提供的一个构造器,可以传入一个比较器【匿名内部类】并指定排序规则

    添加元素:

    第一步:TreeSet底层是TreeMap,在TreeSet的构造中会创建TreeMap对象,并且对比较器初始化private final Comparator comparator;

    第二步:执行add方法,在add方法中调用了TreeMap中的put方法,传入元素和PRESENT[占位符] //PRESENT其实是 :private static final Object PRESENT = new Object();

    第三步:判断当前集合是否为空时,如果为空将key传入compare(key,key)方法中,比较,将元素添加到集合中,记录集合大小为1,modCount加1

    第四步:判断比较器的值是否为空,如果不为null,则使用比较器的规则进行比较,比较值返回小于

    0时,则放在左边,大于0放在右边,相等时,则将原来的替换掉,并且返回原来的值

    第五步:判断比较器的值为空时,则使用默认的比较规则

    注意:当使用比较器时,比较器比较后返回的值相等,则将原来的value值替换掉,并返回原来的value值

            
              源码剖析:
              第一步:
              TreeSet底层是TreeMap,所以在TreeMap中完成:对比较器的初始化:private final Comparator<? super K> comparator;
              在追TreeSet的构造顺底层其实TreeMap
                  public TreeMap(Comparator<? super K> comparator) {
                      this.comparator = comparator;
                  }
             
                  public TreeSet(Comparator<? super E> comparator) {
                      this(new TreeMap<>(comparator));
                  }
             
              第二步:执行add方法
                  public boolean add(E e) {
                      return m.put(e, PRESENT)==null;
                      //PRESENT其实是 :private static final Object PRESENT = new Object();
                  }
             
              往put方法中继续追
                  public V put(K key, V value) {//传入key 和 PRESENT [PRESENT其实是占位符]
                      Entry<K,V> t = root;// private transient Entry root; 当集合为空时,root必须为空
                      if (t == null) {//这里是当集合为空时,才会执行
                          compare(key, key); // type (and possibly null) check
             
                          root = new Entry<>(key, value, null);
                          size = 1; 表示集合大小为了1
                          modCount++; 表示修改了一次
                          return null; 返回null值表示修改成功
                      }
                      int cmp;
                      Entry<K,V> parent;
                      // split comparator and comparable paths
                      Comparator<? super K> cpr = comparator; // 将比较器引用赋给 cpr
                      if (cpr != null) { 如果比较器不为 null
                          do {
                              parent = t;
                              cmp = cpr.compare(key, t.key);
                              if (cmp < 0) 当比较小于 0 则放在左边
                                  t = t.left;
                              else if (cmp > 0) 当比较大于 0 则放在右边
                                  t = t.right;
                              else  //如果比较器返回的值相等,则将原来的替换掉,并返回原来的值
                                  return t.setValue(value);
                          } while (t != null);
                      }
                      else { 如果比较器为空时 则执行这里
                          if (key == null)
                              throw new NullPointerException();
                          @SuppressWarnings("unchecked")
                              Comparable<? super K> k = (Comparable<? super K>) key;
                          do {
                              parent = t;
                              cmp = k.compareTo(t.key);
                              if (cmp < 0)
                                  t = t.left;
                              else if (cmp > 0)
                                  t = t.right;
                              else
                                  return t.setValue(value);
                          } while (t != null);
                      }
                      Entry<K,V> e = new Entry<>(key, value, parent);
                      if (cmp < 0)
                          parent.left = e;
                      else
                          parent.right = e;
                      fixAfterInsertion(e);
                      size++;
                      modCount++;
                      return null;
                  }
             
             
                   public V setValue(V value) { //如果比较器返回的值相等,则将原来value的替换掉,并返回原来的值value
                      V oldValue = this.value;
                      this.value = value;
                      return oldValue;
                      }
             
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83

    16.TreeMap集合

    基本介绍:

    当我们使用无参构造器,创建TreeMap时,仍然是无序的

    当我们需要按照某种方式进行排序时,需要使用TreeMap提供的一个构造器,可以传入一个比较器【匿名内部类】并指定排序规则

    添加元素:

    第一步:创建TressMap时,并且对比较器初始化private final Comparator comparator;

    第二步:执行add方法,在add方法中调用了TreeMap中的put方法,传入keyvalue

    第三步:判断当前集合是否为空时,如果为空将key传入compare(key,key)方法中,比较,将元素添加到集合中,记录集合大小为1,modCount加1

    第四步:判断比较器的值是否为空,如果不为null,则使用比较器的规则进行比较,比较值返回小于

    0时,则放在左边,大于0放在右边,相等时,则将原来的替换掉,并且返回原来的值

    第五步:判断比较器的值为空时,则使用默认的比较规则

    注意:当使用比较器时,比较器比较后返回的值相等,则将原来的value值替换掉,并返回原来的value值

            
    
                解读源码:
                1. 构造器. 把传入的实现了 Comparator接口的匿名内部类(对象),传给给TreeMap的comparator
                 public TreeMap(Comparator<? super K> comparator) {
                    this.comparator = comparator;
                }
                2. 调用put方法
                2.1 第一次添加, 把k-v 封装到 Entry对象,放入root
                Entry<K,V> t = root;
                if (t == null) {
                    compare(key, key); // type (and possibly null) check
    
                    root = new Entry<>(key, value, null);
                    size = 1;
                    modCount++;
                    return null;
                }
                2.2 以后添加
                Comparator<? super K> cpr = comparator;
                if (cpr != null) {
                    do { //遍历所有的key , 给当前key找到适当位置
                        parent = t;
                        cmp = cpr.compare(key, t.key);//动态绑定到我们的匿名内部类的compare
                        if (cmp < 0)
                            t = t.left;
                        else if (cmp > 0)
                            t = t.right;
                        else  //如果遍历过程中,发现准备添加Key 和当前已有的Key 相等,就不添加
                            return t.setValue(value);
                    } while (t != null);
                }
             
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    Collections工具类

    基本介绍:

    1. Collections 是一个操作Set ListMap等集合的工具类
    2. Collections 中提供一系静态的方法对集合元素进行排序 , 查询和修改等操作
    排序操作
    1.		reverse(List):反转List中元素的顺序
    2.		shuffle(List):对List集合元素进行随机排序
    3.		sort(List):根据元素的自然顺序对指定List集合元素按升序排序
    4.		sort(List,Comparator):根据指定的Comparator产生的顺序对List集合元素进行排序
    
    替换 和 查找操作
    5.		swap(List, int ,int ):将指定List集合中的 i 处元素 和 j 处元素进行交换
    6.		Object max(Collection):根据元素的自然顺序,返回给定集合中的最大元素
    7.      Object max(Collection, Comparator):根据Comparator指定的顺序,返回给集合中的最大元素
    8.		Object min(Collection):根据元素的自然顺序,返回给定集合中的最小元素
    9.		Object min(Collection, Comparator):根据Comparator指定的顺序,返回给集合中的最大元素
    10.		int frequency(Collection,Object):返回指定集合中指定元素的出现次数
    11.		void copy(List dest,List src):将src中的内容复制到dest中
            注意复制的目标集合的长度必须大于源集合,否则会抛出空指针异常
        
    12.     boolean replaceAll(List list,Object oldVal,Object newVal):使用新值替换List对象的所有旧值
    
    13.将线程不安全的集合转成线程安全
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
  • 相关阅读:
    网络安全黑客技术自学
    Vue组件通信(组件的自定义事件、全局事件总线、消息订阅与发布、插槽、props)(八)
    Redis——》数据类型:list(列表)
    Jprofiler/ VisualVM 定位内存溢出OOM
    JAVA自定义注解记录操作日志
    当数据量越来越大,优化时是分库分表还是使用newSQL
    DSA之查找(2):树表的查找
    MYSQL高可用架构之MHA实战二 安装和配置MHA架构(真实可用)
    【2022 Q2&Q3 华为机试真题 C++】字符串子序列II
    Win11 22623.891更新了什么?
  • 原文地址:https://blog.csdn.net/weixin_47267628/article/details/125878224