Java基础

java 基础

视频资料:https://www.bilibili.com/video/BV1fh411y7R8/?spm_id_from=333.999.0.0

java 数据类型

基本数据类型

数值型

  1. 整数类型
    • byte 类型占 1 个字节
    • short 类型占 2 个字节
    • int 类型占 4 个字节
    • long 类型占 8 个字节
  2. 浮点类型
    • float 类型占 4 个字节
    • double 类型占 8 个字节

字符型

存放单个字符,占 2 个字节。

布尔型

存放 true 或 false,占 1 个字节。

引用类型

  • 类 class
  • 接口 interface
  • 数组 []

原码,反码,补码

  1. 二进制的最高位是符号位,0 表示正数,1 表示负数(技巧:1 旋转 90 度是个负号)。
  2. 正数的原码,反码,补码都一样(三码合一)。
  3. 负数的反码 = 它的原码符号位不变,其他按位取反。
  4. 负数的补码 = 它的反码 + 1,负数的反码 = 负数的补码 - 1。
  5. 0 的反码,补码都是 0。
  6. java 没有无符号的数,换言之 java 的数都是有符号的。
  7. 在计算机运算的时候,都是以补码的方式来运算的。
  8. 当我们看运算结果时,要看它的原码。

位运算

  1. 按位与 & : 两位全为 1 结果为 1,否则为 0。
  2. 按位或 |:两位只要有一个为 1,结果为 1,否则为 0。
  3. 按位异或 ^ : 两位一个为 0 ,一个为 1,结果为 1,否则为 0。
  4. 按位取反 ~ : 0 变为 1,1 变为 0。

java 内存布局

一般存放基本数据类型(局部变量)

存放对象,数组等

方法区

方法区中包含的内容:

  1. 常量池,存放常量,比如字符串
  2. 类加载信息

排序的介绍

  1. 内部排序

    指将需要处理的所有数据都加载到内部存储器中去进行排序。(包括交换式排序法,选择排序法和插入排序法)

  2. 外部排序法

    数据量过大,无法全部加载到内存中需要借助外部存储进行排序。(包括合并排序法和直接合并排序法)

java 类路径

Java 类路径是指 Java 虚拟机 (JVM) 在查找类时要搜索的位置。它是一组文件夹,其中包含 Java 类文件或 JAR 文件。当 Java 程序运行时,JVM 会搜索类路径中的各个目录和 JAR 文件,以查找需要的类。

Java 类路径可以分为两种类型:系统类路径和用户类路径。

系统类路径:默认情况下,Java 虚拟机会在以下位置搜索类文件:
Java 安装目录下的 lib 目录;
系统环境变量 CLASSPATH 指定的路径。
用户类路径:如果我们的程序需要使用一些自定义的类或第三方库,可以将这些类放在用户类路径中。这可以通过以下方式之一来设置:
在启动脚本中使用 -classpath 或 -cp 参数指定;
在代码中使用 System.setProperty() 方法设置;
使用 CLASSPATH 环境变量设置;
在 Eclipse 或其他 IDE 中设置。
无论哪种方式,用户类路径都可以包含多个目录或 JAR 文件,多个路径之间用分号 (😉 分隔。在搜索类时,JVM 会按照类路径的顺序依次搜索,直到找到对应的类为止

继承

父类构造器的调用不限于直接父类,将一直往上追溯直到 Object 类。

继承的查找关系:

  1. 首先看子类是否有该属性。
  2. 如果子类有该属性,并且可以访问,则返回信息。
  3. 如果子类没有这个属性,就看父类有没有这个属性。(如果父类有该属性,并且可以访问,就返回该信息)
  4. 如果父类没有这个属性,重复 3 的步骤继续在父类的父类查找,知道 Object 类为止。

super

super 的访问不限于直接父类,如果爷爷类和本类中有同名的成员,也可以使用 super 去访问爷爷类的成员,如果多个上级类都有相同的名称的成员,使用 super 访问,遵循就近原则。当然也需要遵守访问权限的规则。

方法重写override

方法重写需要满足以下条件:

  1. 子类的方法的参数,方法名称要和父类的方法的参数和方法名称完全一样。
  2. 子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类。
  3. 子类方法不能缩小父类方法的访问权限。

重载与重写

  1. 名称

    重载是 overload,重写是 override。

  2. 发生范围

    重载对本类的方法,重载是父子类或者爷爷类。

  3. 方法名

    重载和重写方法名都必须一样。

  4. 形参列表

    重载要求形参的类型个数或者顺序至少有一个不同。而重写必须完全相同。

  5. 返回类型

    重载对方法的返回类型无要求。而重写,子类方法的返回类型和父类方法返回类型一样,或者是父类返回类型的子类。

  6. 访问修饰符

    重载对应访问修饰符无要求,而重写。子类方法不能缩小父类方法的访问权限。

this 和 super区别

  1. 访问属性

    this 访问本类中的属性,如果本类没有此属性则从父类中继续查找。如果找到一个,但访问权限不够,编译器会报错。

    super 访问父类中的属性。

  2. 调用方法

    this 访问本类的方法,如果本类没有此方法则从父类继续查找。如果找到一个,但访问权限不够,编译器会报错。

    super 直接访问父类中的方法。

  3. 调用构造器

    this 调用本类构造器,必须放在构造器的首行。

    super 调用父类构造器,必须放在子类构造器的首行。如果子类没有显式的调用父类构造器,系统默认会调用父类的无参构造器。

  4. 特殊

    this 表示当前对象。

    super 表示子类中访问父类的对象。

Object

== 与 equals

== 是一个比较运算符

  1. == 既可以判断基本类型,又可以判断引用类型。
  2. == 如果判断基本类型,判断的是值是否相等。
  3. == 如果不是判断引用类型,判断地址是否相等,即判定是不是同一个对象。

equals 方法是 Object 类的方法,只能判断引用类型。默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等,比如:String,Integer 都重写了 Object 的 equals 方法。

hashCode

返回对象的 hash 码值。

  1. 提高具有 hash 结构的容器的效率。
  2. 两个引用,如果指向的是同一个对象,hashCode 肯定是一样的。
  3. 两个引用,如果指向的是不同对象,hashCode 是不一样的。
  4. hashCode 主要根据地址来实现的,但 hashCode 不等价地址。
  5. 在集合中 hashCode 如有需要也会重写。

toString

默认返回全类名 + @ + 哈希值的十六进制值。

重写 toString 方法,打印对象或者拼接对象时,都会自动调用该对象的 toString 方法。

断点调试快捷键

  • F7

    Step Into跳入(进入方法)

  • F8

    Step Over 跳过,逐行执行代码

  • F9

    Resume Program 执行到下一个断点

  • Shift + F8

    Step Out 跳出执行,跳出当前栈。跳出方法

  • Alt + Shift + F7

    Force Step Into,单步执行,并且会进入 jdk 等的内部方法。

  • Alt + F8

    Evaluate Expression,查看我们鼠标选中的变量值。

  • Alt + F9

    Run to Cursor 运行到光标位置

  • Alt + F10 Show

    Execution Point 显示断点当前位置。

  • Drop Frame 按钮

    销毁当前方法的栈帧,回到上一级调用方法时的状态,所有的变量的值和程序当前的环境都会被还原到调用该方法之前的状态。

  • 条件断点

    在 idea 红色的断点上右键即可进行条件断点,直接写java判断就行了。

递归

  1. 执行一个方法时,就创建一个新的受保护的独立空间。(栈空间)。
  2. 方法的局部变量是独立的,不会相互影响。
  3. 如果方法中使用的是引用类型变量(比如数组,对象),就会共享该引用类型的数据。
  4. 递归必须向退出递归的条件逼近,否则就是无限递归。会造成 StackOverflowError。
  5. 当一个方法执行完毕,或者遇到 return,就会返回,遵守谁调用就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

什么是多态

方法或对象具有多种形态,是 OOP 的第三大特征,是建立在封装和继承的基础上。

多态的表现:

  1. 方法的多态

    • 方法重载提现多态
    • 方法重写体现多态
  2. 对象多态

    • 对象的编译类型和运行类型可以不一致,编译类型在定义时,就确定,不能改变。

    • 对象的运行类型是可以改变的,可以通过 getClass() 来查看运行类型。

    • 编译类型看定义时 = 号的左边,运行类型看 = 号右边。例如:Object obj = new String(“hello”);

      其中 Object 为编译类型,String 为运行类型。

动态绑定机制

  1. 当调用对象的方法时,该方法会和对象的内存地址/运行类型绑定。
  2. 当调用对象的属性时,没有动态绑定机制,哪里声明,哪里使用。

单例模式

  1. 饿汉式

    • 先将构造器私有化
    • 在类的内部直接创建静态对象
    • 再提供一个公共的静态方法,返回对象实例

    Runtime 是一个经典的饿汉式案例。

    public class Ts{
        private Ts(){
            
        }
        private static Ts INSTANCE = new Ts();
        public static Ts getInstance(){
            return INSTANCE;
        }
    }
    
  2. 懒汉式

    • 先将构造器私有化
    • 定义一个静态属性对象
    • 提供一个公共的方法,返回实例对象。

    只有用户使用 getInstance 方法时,如果对象为空,才会创建。

    线程不安全版本

    class Cat{
        private Cat(){
            
        }
        private static Cat INSTANCE;
        public static Cat getInstance(){
            if(INSTANCE == null){
                INSTANCE = new Cat();
            }
            return INSTANCE;
        }
    }
    

    线程安全版本

    class Cat{
        private Cat(){
            
        }
        private static Cat INSTANCE;
        public static Cat getInstance(){
            if(INSTANCE == null){
                synchronized(this){
                    if(INSTANCE == null){
                        INSTANCE = new Cat();
                    }
                }
            }
            return INSTANCE;
        }
    }
    

懒汉式和饿汉式对比

  1. 二者最主要的区别在于创建对象的时机不同。饿汉式是在类加载时就直接创建对象实例而懒汉式是在使用时才创建对象。
  2. 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。
  3. 饿汉式存在浪费资源的可能。如果程序的一个对象实例都没有使用,那么饿汉式创建对象就浪费了,懒汉式在使用时才创建对象,不存在这个问题。

final

  1. final 修饰类,表示该类不能被继承。
  2. final 修饰方法,表示该方法不能被覆盖。
  3. final 修饰属性或方法形参,表示该属性或方法形参不能被修改。final 修饰的属性必须被初始化。

static 搭配 final 使用,效率更高,不会导致类加载。底层编译器做了优化处理。

Integer,Double,Boolean 等包装类包括 String 类都是 final 类。无法被继承。

抽象类

当父类的一些方法不能确定时,可以用 abstract 关键字来修饰该方法,这个方法就是抽象方法,用 abstract 来修饰的类就是抽象类。

抽象方法只能存在于抽象类或者接口中

抽象方法没有方法体。

抽象类定义语法:访问修饰符 abstract 类名 {}

抽象方法定义语法:访问修饰符 abstract 返回类型 方法名(形参列表);

抽象类的特点

  1. 抽象类不一定有包含抽象方法。抽象类可以有实现方法。
  2. abstract 只能修饰类和方法。
  3. 抽象类可以有任意成员(抽象类本质还是类),比如非抽象方法,构造器,属性等。
  4. 抽象方法不能有主体(方法实现)。
  5. 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己声明为抽象类。
  6. 抽象方法不能使用 private,final,static 来修饰。因为这些关键字都是与方法重写相违背的。

模板设计模式

接口

接口就是给出一些没有实现的方法,封装到一起,到某个类使用的时候,再根据具体情况把这些方法实现出来。

语法:interface 接口名 {属性,方法}

实现接口语法:class 类名 implements 接口名{ 本类的属性和方法,必须实现的接口的抽象方法}

注意:

jdk 7 前,接口里的所有方法都没有方法体。

jdk 8 后,接口可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现。带实现的非静态方法需要使用 default 修饰。

interface A{
    public static void print(){
        ...
    }
    default public void test(){
        ...
    }
}

**在接口中的抽象方法可以省略 abstract **

接口特点:

  1. 接口不能被实例化。

  2. 接口中的所有方法是 public 方法,接口中的抽象方法,可以不用 abstract 修饰。

  3. 一个普通类实现接口,必须实现接口的所有未实现的方法。

  4. 抽象类实现接口,可以不用实现接口的方法。

  5. 一个类可以实现多个接口。

  6. 接口中的属性只能是 final 的。而且是 public static final 修饰符。比如 int a = 1; 实际是

    public static final int a = 1; (必须初始化)

  7. 接口中属性的访问形式是:接口名.属性名

  8. 接口不能继承其他类,但是可以继承多个别的接口。

  9. 接口的修饰符只能是 public 和默认。这个和类的修饰符是一样的。

接口对比继承

继承的加载主要在于:解决代码的复用性和可维护性。

接口的加载主要在于:设计好各种规范,让其他类去实现这些方法。

接口比继承更加灵活,继承满足的是 ‘is - a’ 的关系,而接口只需满足 ‘like - a’ 的关系。

接口在一定程度上实现代码解耦。

接口的多态

  1. 多态参数

  2. 多态数组

  3. 接口传递

    Ts 实现了 IB 接口,IB 接口继承了 IA 接口,相当于 Ts 类中实现了 IA 接口。具有传递性。

    interface IA{
        
    }
    interface IB extends IA{
        
    }
    class Ts implements IB{
        
    }
    
    class Test{
        public static void main(String[] args) {
            IB b = new Ts();
            IA a = new Ts();
        }
    }
    

父类和实现接口属性重复问题

interface IA{
 int x = 100;   
}
class B {
    int x = 200;
}
class C extends B implements IA{
    public void printX(){
        //注意这里的 x 指向不明确,不知道来源于父类 B 还是接口 IA
        System.out.println(x);
        //修改后
        System.out.println("父类x: "+super.x + " 接口x: " + IA.x);
    }
}


Math

常用方法

  • double abs(double n)
    

    返回 n 的绝对值。

  • int pow(int x,int y)
    

    返回 x 的 y 次方。

  • int ceil(double n)
    

    返回大于 n 的最小整数。

  • int floor(double n)
    

    返回小于 n 的最大整数。

  • int round(double n)
    

    四舍五入。

  • double sqrt(double n)
    

    返回 n 的平方根。

  • double random()
    

    返回 [0,1) 之间的随机数。

    //返回 a - b 之间的随机数
    int num = (int)(a + Math.random() * (b - a + 1));
    //如返回 2 - 7 之间的随机数
    int num2 = (int)(2 + 6 * Math.random());
    
  • double max(double x,double y)
    

    返回 x 与 y 之间的最大值。

  • double min(double x,double y)
    

    返回 x 与 y 之间的最小值。

Arrays

常用方法

  • String toString()
    

    数组的 toString 方法。如果是自定义对象的数组,需要在自定义类中重写 toString 方法。

  • void sort(xxx[] arr)
    

    返回从小到大排序后的数组。其中 xxx 为 byte ,char,short,int,long,float,double,Object。

    使用自定义对象数组来排序,该对象必须实现 Comparable 接口,因为底层使用的是 ComparableTimSort 类基于 Comparable 接口的排序方法

    // 自定义类型数组实现 Comparable 接口,使用 sort 排序
    class Book implements Comparable<Book> {    
        private String name;
        private double price;
    
        public Book(String name, double price) {
            this.name = name;
            this.price = price;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public double getPrice() {
            return price;
        }
    
        public void setPrice(double price) {
            this.price = price;
        }
    
    
        @Override
        public int compareTo(Book o) {
    //        return (int) (this.getPrice() - o.getPrice());
            return (int) (o.getPrice() - this.getPrice());
        }
    
        @Override
        public String toString() {
            return "Book{" +
                    "name='" + name + '\'' +
                    ", price=" + price +
                    '}';
        }
    }
    
     Book[]books = {
         new Book("a",101),
         new Book("b",88),
         new Book("c",125),
         new Book("d",50)
     };
    Arrays.sort(books);
    
  • void sort(Object[] arr, Comparator<? super T> c)
    

    可以传一个自定义的比较器。

    底层通过 binarySort 方法中调用了 compare 方法,会影响排序结果。

    自定义冒泡排序练习:

    int[] arr = {1, 10, 5, 3, 29};
    sort(arr, new Comparator<Integer>() {
        @Override
        public int compare(Integer o1, Integer o2) {
            //o1 - o2 为升序,o2 - o1 为降序
            return o2 - o1;
        }
    });
    System.out.println(Arrays.toString(arr));
    public static void sort(int[] arr, Comparator<Integer> comparator) {
        int temp = 0;
        for (int i = 0; i < arr.length - 1; i++) {
            for (int j = 0; j < arr.length - 1 - i; j++) {
                if (comparator.compare(arr[j], arr[j + 1]) > 0) {
                    temp = arr[j];
                    arr[j] = arr[j + 1];
                    arr[j + 1] = temp;
                }
            }
        }
    }
    
  • int binarySearch(xxx[]arr,xxx x)
    

    二叉查找需要 arr 是一个有序的数组。xxx 可以是 int,long,float,double,byte,char,short,Object。

    找到返回索引,没找到就返回 -1。

  • public static <T> T[] copyOf(T[] arr, int newLength)
    

    数组拷贝,T 可以是 int,long,float,double,byte,char,short,Object。该方法底层使用的是

    System.arrayCopy()

  • public static <T> T[] copyOfRange(T[] original, int from, int to)
    

    数组拷贝,拷贝索引范围 [start,end),T 可以是 int,long,float,double,byte,char,short,Object。

  • public static void fill(xxx[] arr, xxx x)
    

    数组填充,使用 x 的值去替换所有值。xxx 可以是 int,long,float,double,byte,char,short,Object。

  • boolean equals(xxx[] arr1, xxx[] arr2)
    

    比较 2 个数组的元素是否一样

  • public static <T> List<T> asList(T... a)
    

    把数组转为集合。返回的是 Arrays 类中的静态内部类 ArrayList 对象。

System

常用方法

  • void exit(int status)
    

    退出程序。status 为 0 表示正常退出

  • public static native void arraycopy(
        Object src,  int  srcPos,Object dest, int destPos,int length)
    

    src: 原始数组

    srcPos:从原数组的哪个索引开始拷贝

    dest:目标数组

    destPos:把原数组拷贝到,目标数组的那个索引位置

    length:从原数组拷贝多少个数据到目标数组

  • currentTimeMillis()

    返回从 1970年1月1日到现在所经历的毫秒数

BigInteger和BigDecimal

BigInteger 表示更大的整数。

BigDeccimal 表示更高精度的浮点数。

BigInteger bigInteger1 = new BigInteger("100");
BigInteger bigInteger2 = new BigInteger("10");
BigInteger add = bigInteger1.add(bigInteger2);
BigInteger sub = bigInteger1.subtract(bigInteger2);
BigInteger multiply = bigInteger1.multiply(bigInteger2);
BigInteger divide = bigInteger1.divide(bigInteger2);
System.out.println(add);
System.out.println(sub);
System.out.println(multiply);
System.out.println(divide);
BigDecimal bigDecimal1 = new BigDecimal("10.0");
BigDecimal bigDecimal2 = new BigDecimal("3.0");
BigDecimal add = bigDecimal1.add(bigDecimal2);
BigDecimal sub = bigDecimal1.subtract(bigDecimal2);
BigDecimal multiply = bigDecimal1.multiply(bigDecimal2);
//无法除尽时,需要指定精度
//BigDecimal divide = bigDecimal1.divide(bigDecimal2);
BigDecimal divide = bigDecimal1.divide(bigDecimal2, RoundingMode.HALF_EVEN);
System.out.println(add);
System.out.println(sub);
System.out.println(multiply);
System.out.println(divide);

Date

  1. 使用 new Date() 可以获取当前系统时间。

  2. 使用 new Date(long time) 指定时间对象。

  3. 使用 SimpleDateFormat(String pattern) 可以格式化时间。pattern 格式:“yyyy MM dd HH:mm:ss E”

    对应年月日时分秒星期

    Date date = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss E");
    // 格式化时间
    String s = sdf.format(date);
    System.out.println(s);
    try {
        //解析时间
        Date date1 = sdf.parse("2023-04-04 15:53:04 周二");
        System.out.println(date1);
    } catch (ParseException e) {
        throw new RuntimeException(e);
    }
    

Calendar

  1. getInstance()

    获取一个 Calendar 对象。

  2. get(int field)

    获取指定字段的属性。

Calendar calendar = Calendar.getInstance();
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH) + 1;
int day = calendar.get(Calendar.DAY_OF_MONTH);
int hour = calendar.get(Calendar.HOUR_OF_DAY);
int minute = calendar.get(Calendar.MINUTE);
int second = calendar.get(Calendar.SECOND);
String f = String.format("%d年%d月%d日 %d:%d:%d",year,month,day,hour,minute,second);
System.out.println(f);

LocalDate

Date 类的大多数方法在 JDK 1.1 引入 Calendar 类后就弃用了,而 Calendar 也存在一些问题:

  1. 可变性 像日期和时间这样的类应该是不可变的。
  2. 偏移性 Date 中的年份是 1900开始的,而月份是从 0 开始的。
  3. 格式化。没有专门的格式化类。
  4. 此外,Calendar 也是不安全的,也不能处理闰秒(每2天多1秒)。

LocalDate 是 JDK 8 引入的。

一般使用 LocalDateTime,它包含所有时间信息。而 LocalDate 只包含年月日。LocalTime 只包含时分秒。

LocalDateTime now = LocalDateTime.now();
String format = now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
System.out.println(format);
int year = now.getYear();
int monthValue = now.getMonthValue();
int dayOfMonth = now.getDayOfMonth();
int hour = now.getHour();
int minute = now.getMinute();
int second = now.getSecond();
String f = String.format("%d年%d月%d日 %d:%d:%d",year,monthValue,dayOfMonth,hour,minute,second);
System.out.println(f);

LocalDateTime 的 plusDays,plusMinutes,minusDays,minusMinutes等方法,会返回一个新的时间。

  • public static LocalDate now()
    

    可以获取年月日

  • public static LocalTime now()
    

    可以获取时分秒

Instant

时间戳

Instant instant = Instant.now();
System.out.println(instant);
//instant转date
Date date = Date.from(instant);
System.out.println(date);
//date 转 instant
Instant instant1 = date.toInstant();

String

String 重写了 hashCode 方法

public int hashCode() {
    // The hash or hashIsZero fields are subject to a benign data race,
    // making it crucial to ensure that any observable result of the
    // calculation in this method stays correct under any possible read of
    // these fields. Necessary restrictions to allow this to be correct
    // without explicit memory fences or similar concurrency primitives is
    // that we can ever only write to one of these two fields for a given
    // String instance, and that the computation is idempotent and derived
    // from immutable state
    int h = hash;
    if (h == 0 && !hashIsZero) {
        h = isLatin1() ? StringLatin1.hashCode(value)
            : StringUTF16.hashCode(value);
        if (h == 0) {
            hashIsZero = true;
        } else {
            hash = h;
        }
    }
    return h;
}
public static int hashCode(byte[] value) {
    int h = 0;
    for (byte v : value) {
        h = 31 * h + (v & 0xff);
    }
    return h;
}

String 类型的 hash 值是根据字符串内容来决定的,并不是内存地址,只要两个 String 类型的字符串内容一致,那么两者的 hashCode 就是相同的。

所有为啥 hashSet 中无法存放 2 个 内容一样的字符串对象。

String s1 = new String("Tom");
String s2 = new String("Tom");
HashSet<String> st = new HashSet<String>();
st.add(s1);
st.add(s2);
System.out.println(st);
//输出 [Tom],因为 s1和s2对象的hashCode相同

HashSet

HashSet 添加元素底层如何实现?

HashMap 底层是 数组(table)+ 链表 + 红黑树实现的。

  1. HashSet 底层是 HashMap
  2. 添加一个元素时,先得到 hash 值会转成索引值。
  3. 找到存储数据表 table,看到这个索引位置是否已经存放有元素
  4. 如果没有直接加入
  5. 如果有,调用 equals 方法比较,如果相同就放弃添加,如果不相同就添加到链表的后面
  6. 在 java8 中如果一条链表的元素个数达到 TREEIFY_THRESHOLD( 默认是 8 ),并且 table 的大小 >= MIN_TREEFIY_CAPACITY( 默认 64 ),就会进行树化(红黑树)。否则仍然采用数组扩容机制。

源码:

public HashSet() {
    map = new HashMap<>();
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//把 key 进行计算得到一个 hash 值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //table 为 Node<K,V>[]数组
    //如当前table为null或者size为0,就进行resize扩容到16
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //根据key得到hash值,去计算该key存放到table表的那个位置i
    //并把这个对象赋值给p变量
    if ((p = tab[i = (n - 1) & hash]) == null)
        //p为null,创建一个Node对象,并把它放到上面计算出来的i的位置上
        //如果是HashSet调用的add,这里的newNode调用的是HashMap里面的newNode方法
        //此时tab[i]的实际类型为Node<K,V>
        //如果是LinkedHashSet调用的add方法,这里的newNode调用的是LinkedHashMap
        //里面的newNode方法,此时tab[i]的实际类型为LinkedHashMap.Entry<K,V>
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //当前索引位置对应链表第一个元素和准备添加的key的hash一样
        //并且满足以下2个条件之一
        //1.节点对象p中的key和需要添加的key为同一个对象
        //2.节点对象p中的key的equals准备添加的key为true时
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            //进入这里,插入元素已经存在了,就不能在插入了
            e = p;
        //判断p是否是一颗红黑树
        else if (p instanceof TreeNode)
            //这里如果数据成功插入红黑树后,putTreeVal会返回null,此时e=null
            //也表示插入成功了
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //这里是死循环
            for (int binCount = 0; ; ++binCount) {
                //如果依次比较完成后,依然没有相同的元素,就把需要插入的key放到p的后面
                //这里注意数据正常插入到链表后,此时的e=null的,所以返回null,也表示插入成功了
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    //在把元素添加到链表末尾后,立即判断该链表的元素是否达到了8个
                    //就调用treeifyBin对当前这个链表进行树化。
                    //注意:在转成红黑树时,要进行判断,当table表的大小小于
                    //MIN_TREEIFY_CAPACITY(64)时,先对table表进行扩容
                    //如果大于等于64,就进把链表转为红黑树
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    //treeifyBin判断如下:
                    //if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
            		//	resize();
                    break;
                }
                //这里是找到相同的对象(就是元素是否重复的判断)
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                //每比较一次,就p就会移动到下一个节点
                p = e;
            }
        }
        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);
    //返回null表示添加成功
    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;//DEFAULT_INITIAL_CAPACITY=16
        //DEFAULT_LOAD_FACTOR=0.75
        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;
}


 final TreeNode<K,V> putTreeVal(HashMap<K,V> map, Node<K,V>[] tab,
                                int h, K k, V v) {
     Class<?> kc = null;
     boolean searched = false;
     TreeNode<K,V> root = (parent != null) ? root() : this;
     for (TreeNode<K,V> p = root;;) {
         int dir, ph; K pk;
         if ((ph = p.hash) > h)
             dir = -1;
         else if (ph < h)
             dir = 1;
         else if ((pk = p.key) == k || (k != null && k.equals(pk)))
             return p;
         else if ((kc == null &&
                   (kc = comparableClassFor(k)) == null) ||
                  (dir = compareComparables(kc, k, pk)) == 0) {
             if (!searched) {
                 TreeNode<K,V> q, ch;
                 searched = true;
                 if (((ch = p.left) != null &&
                      (q = ch.find(h, k, kc)) != null) ||
                     ((ch = p.right) != null &&
                      (q = ch.find(h, k, kc)) != null))
                     return q;
             }
             dir = tieBreakOrder(k, pk);
         }

         TreeNode<K,V> xp = p;
         if ((p = (dir <= 0) ? p.left : p.right) == null) {
             Node<K,V> xpn = xp.next;
             TreeNode<K,V> x = map.newTreeNode(h, k, v, xpn);
             if (dir <= 0)
                 xp.left = x;
             else
                 xp.right = x;
             xp.next = x;
             x.parent = x.prev = xp;
             if (xpn != null)
                 ((TreeNode<K,V>)xpn).prev = x;
             moveRootToFront(tab, balanceInsertion(root, x));
             //这里数据插入成功依然返回null
             return null;
         }
     }
 }

HashSet扩容说明

  • HashSet 底层是 HashMap。第一次添加时,table 数组扩容到 16,临界值 (threshold )为 16 * 加载因子(loadFactory默认为0.75)=12。
  • 如果 table 数组使用到了临界值 12,就会扩容到 16 * 2 = 32,新的临界值就是 32*0.75=24,以此类推。
  • 在 java8 中如果一条链表的元素个数达到 TREEIFY_THRESHOLD(默认是8),并且 table 的大小 >= MIN_TREEFIY_CAPACITY(默认64),就会进行树化(红黑树)。否则仍然采用数组扩容机制。

LinkedHashSet

LinkedHashSet 添加元素和取出元素的顺序一致。

LinkedHashSet 底层维护的是一个 LinkedHashMap。底层结构是 数组 + 双向链表

第一次 add 元素时,也是扩容到 16,也有临界值为 12。table 数组中存放的是 LinkedHashMap.Entry<K,V>

static class Entry<K,V> extends HashMap.Node<K,V> LinkedHashMap.Entry 是继承 HashMap.Node

源码分析添加过程

public LinkedHashSet() {
    super(16, .75f, true);
}

HashSet(int initialCapacity, float loadFactor, boolean dummy) {
    map = new LinkedHashMap<>(initialCapacity, loadFactor);
}

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

//这里的putVal见上面的HashSet的putVal,唯一不同的是putVal中的newNode是调用
//LinkedHashMap里面的newNode
          
Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
    LinkedHashMap.Entry<K,V> p =
        new LinkedHashMap.Entry<>(hash, key, value, e);
    linkNodeLast(p);
    return p;
}

//这里形成一个双向链表
private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
    //这里把last也就是上一次添加的元素保存起来
    LinkedHashMap.Entry<K,V> last = tail;
    //现在需要添加一个节点p,所有现在tail就变为p
    tail = p;
    if (last == null)
        //添加第一个节点是确定头
        head = p;
    else {
        //把p(现在的尾部节点)的before指向last(前一个节点), p.before->last
        p.before = last;
        //把last(前一个节点)的after指向p(现在的尾部节点) last.after->p
        last.after = p;
    }
}

Map

Map 与 Collection 并列存在。用于保存有映射关系的数据。

Map 中的 key 和 value 可以是任何引用类型数据,会封装到 HashMap$Node对象中

Map 中的 key 不允许重复,和 HashSet 原理一样。

Map 中的 value 允许为 null

Map 中的 key 和 value 可以为null,key 只能由一个 null(不允许重复),value 可以有多个 null

常用 String 作为 key

HashMap

HashMap 的添加和扩容和 HashSet 一样。因为 HashSet 底层就是维护了一个 HashMap。

HashMap values,keySet,entrySet 源码

public Collection<V> values() {
    Collection<V> vs = values;
    if (vs == null) {
        vs = new Values();
        values = vs;
    }
    return vs;
}

public Set<K> keySet() {
    Set<K> ks = keySet;
    if (ks == null) {
        ks = new KeySet();
        keySet = ks;
    }
    return ks;
}

public Set<Map.Entry<K,V>> entrySet() {
    Set<Map.Entry<K,V>> es;
    return (es = entrySet) == null ? (entrySet = new EntrySet()) : es;
}

//在遍历上面3个方法返回值时,底层会调用个自的 Iterator。
//values方法的返回值在遍历时,会调用Values类的iterator方法
final class Values extends AbstractCollection<V> {
    ...省略了部分代码
    public final Iterator<V> iterator()     { return new ValueIterator(); }
    ...省略了部分代码
}

//keySet方法的返回值在遍历时,会调用KeySet类的iterator方法
final class KeySet extends AbstractCollection<V> {
    ...省略了部分代码
    public final Iterator<V> iterator()     { return new KeyIterator(); }
    ...省略了部分代码
}


//entrySet方法的返回值在遍历时,会调用EntrySet类的iterator方法
final class EntrySet extends AbstractSet<Map.Entry<K,V>> {
    ...省略了部分代码
    public final Iterator<Map.Entry<K,V>> iterator() {
    	return new EntryIterator();
	}
    ...省略了部分代码
}

//在遍历时next方法会返回value的值
final class ValueIterator extends HashIterator
    implements Iterator<V> {
    public final V next() { return nextNode().value; }
}

//在遍历时next方法会返回key的值
final class KeyIterator extends HashIterator
    implements Iterator<K> {
    public final K next() { return nextNode().key; }
}

//在遍历时next方法会返回一个节点Entry的值,实际类型是Node
final class EntryIterator extends HashIterator
    implements Iterator<Map.Entry<K,V>> {
    public final Map.Entry<K,V> next() { return nextNode(); }
}

//这个类很关键保存了entrySet引用的数据
abstract class HashIterator {
    Node<K,V> next;        // next entry to return
    Node<K,V> current;     // current entry
    int expectedModCount;  // for fast-fail
    int index;             // current slot

    HashIterator() {
        expectedModCount = modCount;
        Node<K,V>[] t = table;
        current = next = null;
        index = 0;
        if (t != null && size > 0) { // advance to first entry
            do {} while (index < t.length && (next = t[index++]) == null);
        }
    }

    public final boolean hasNext() {
        return next != null;
    }

    //这里就是一个个entrySet对象Map.Entry<K,V>,Node是实现了Map.Entry接口的
    //因此Node类型可以使用Map.Entry接收
    final Node<K,V> nextNode() {
        Node<K,V>[] t;
        Node<K,V> e = next;
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        if (e == null)
            throw new NoSuchElementException();
        if ((next = (current = e).next) == null && (t = table) != null) {
            do {} while (index < t.length && (next = t[index++]) == null);
        }
        return e;
    }

    public final void remove() {
        Node<K,V> p = current;
        if (p == null)
            throw new IllegalStateException();
        if (modCount != expectedModCount)
            throw new ConcurrentModificationException();
        current = null;
        removeNode(p.hash, p.key, null, false, false);
        expectedModCount = modCount;
    }
}


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值