Java面试题基础

基础总结


Java

Java 平台无关性

主要通过三个方面实现.

  • Java 语言规范: 通过规定 Java 语言中基本数据类型的取值范围和行为,比如 int 长度为 4 字节,这是固定的。
  • Class 文件: 所有 Java 文件要通过 javac 或者其他一些 java 编译器编译成统一的 Class 文件。
  • Java 虚拟机:
    • 通过 Java 虚拟机将字节码文件 (.Class) 转成对应平台的二进制文件。
    • JVM 是平台相关的,需要在不同操作系统安装对应的虚拟机。

程序运行过程

  • 编译:java 源文件编译为 class 字节码文件
  • 类加载:类加载器把字节码加载到虚拟机的方法区。
  • 创建对象: 运行时创建对象
  • 方法调用: JVM 执行引擎解释为机器码
  • 执行: CPU 执行指令
  • running: 多线程切换上下文

Java 三大特性

  • 封装:将一系列的操作和数据组合在一个包中,使用者调用这个包的时候不必了解包中具体的方法是如何实现的。
  • 多态:父类的变量可以引用一个子类的对象,在运行时通过动态绑定来决定调用方法。
    作用:
    • 应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。大大提高程 序的可复用性。// 继承
    • Java的多态是指同一个方法在不同对象中可以表现出不同的行为。多态分为编译时多态(方法重载)和运行时多态(方法重写)。
    • Java接口的实现也是多态的一种表现形式。通过接口,多个类可以实现相同的方法,从而在运行时表现出不同的行为。
    • 派生类的功能可以被基类的方法或引用变量所调用,这叫向后兼容,可以提高可扩充性和可维护性。
  • 继承:一个类可以扩展出一个子类,子类可以继承父类的属性和方法,也可以添加自己的成员变量和方法。接口可以多继承,普通类只能单继承。

重载和重写(针对于方法)

  • 重写:子类具有和父类方法名和参数列表都相同的方法,返回值要不大于父类方法的返回值,抛出的异常要不大于父类抛出的异常,方法修饰符可见性要不小于父类。运行时多态。
    • 是运行时多态,因为程序运行时,会从调用方法的类中根据继承关系逐级往上寻找该方法,这是在运行时才能进行的。
  • 重载:同一个类中具有方法名相同但参数列表不同的方法,返回值不做要求。编译时多态。

Integer 和 int 区别

  • Integer 是 int 的包装类,所表示的变量是一个对象;而 int 所表示的变量是基本数据类型
  • 自动装箱 (valueOf) 指的是将基本数据类型包装为一个包装类对象,自动拆箱 (intValue) 指的是将一个包装类对象转换为一个基本数据类型。
  • 包装类的比较使用 equals,是对象间的比较。

基本数据类型(8种)

  • byte 1 字节;short 2 字节
  • int, float 4 字节
  • long, double 8 字节
  • boolean 单独出现时 4 字节,数组时单个元素 1 字节
  • char 英文都是 1 字节,GBK 中文 2 字节,UTF-8 中文 3 字节

值传递和引用传递

  • 值传递对基本数据类型而言的,传递的是变量值的一个副本,改变副本不影响原变量的值
  • 引用传递对于对象型变量而言,传递的是对象地址的副本,不是原变量本身,所以对引用对象的操作会改变原变量的值。

== 和 equals 区别

  • == 比较的对象如果是基本数据类型,就是两者的值进行比较;如果是引用对象的比较,是判断对象的地址值是否相同
  • equals 如果比较的是 String 对象,就是判断字符串的值是否相同;如果比较的是 Object 对象,比较的是引用的地址内存;可以通过重写 equals 方法来自定义比较规则,也需要同时重写 hashCode 方法
    -重写hashcode示例 👇
import java.util.Objects;

class Person {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // 重写equals方法
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Person person = (Person) o;
        return age == person.age && Objects.equals(name, person.name);
    }

    // 重写hashCode方法
    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }

    // 方便在测试时查看对象内容
    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

方法修饰符可见类型

  • public: 对本包和不同包都是可见的
  • protected: 对不同包不可见
  • default: 只对本包中子类和本类可见
  • private:只对本类可见

Object 类,equals 和 hashCode

Object 的类是所有类的父类。equals,hashCode,toString 方法

  • equals 用来比较对象地址值是否相同
  • hashCode 返回由对象地址计算得出的一个哈希值
  • 两者要同时重写的原因
    • 使用 hashcode 方法提前校验,通过 hasCode 比较比较快,可以避免每一次比对都调用 equals 方法,提高效率
    • 保证是同一个对象,如果重写了 equals 方法,而没有重写 hashCode 方法,会出现 equals 比较时相等的对象,hashCode 不相等的情况,重写 hashcode 方法就是为了避免这种情况的出现。
  • 哈希值相同的对象 equals 比较不一定相等,存在两个对象计算得到 hashCode 相等的情况,这是哈希冲突。

避免哈希冲突?

  • 哈希表的特点:关键字在表中位置和它之间存在一种确定的关系。
  • 解决哈希冲突:
    • 开放定址法(线性探测再散列,二次探测再散列,伪随机探测再散列)
      • 线性探测再散列:放入元素,如果发生冲突,就往后找没有元素的位置;
      • 平方探测再散列:如果发生冲突,放到 (冲突 + 1 平方) 的位置,如果还发生冲突,就放到 (冲突 - 1 平方) 的位置;如果还有人就放到 (冲突 + 2 平方) 的位置,以此类推,要是负数就倒序数。
      • 随机探测再散列
    • 链地址法:如果发生冲突,就继续往前一个元素上链接,形成一个链表,Java 的 hashmap 就是这种方法。
    • 再哈希:用另一个方法再次进行一个哈希运算
    • 建立一个公共溢出区:将哈希表分为基本表和溢出表两部分,范式和基本表发生冲突的元素,一律填入溢出表。

深拷贝,浅拷贝

clone:clone 方法声明为 protected,类只能通过该方法克隆它自己的对象,如果希望其他类也能调用该方法必须定义该方法为 public。如果一个对象的类没有实现 Cloneable 接口,该对象调用 clone 方法会抛出一个 CloneNotSupport 异常。默认的 clone 方法是浅拷贝,一般重写 clone 方法需要实现 Cloneable 接口并指定访问修饰符为 public。

  • 浅拷贝:重新在堆中创建内存,将对象进行拷贝,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因为共享同一块内存,会相互影响。(被浅拷贝的对象是会重新生成一个新的对象,新的对象和原来的对象是没有任何关系的,)如果对象中的某个属性是引用类型的话,那么该属性对应的对象是不会重新生成的,浅拷贝只会重新当前拷贝的对象,并不会重新生成其属性引用的对象。

  • 深拷贝:从堆内存中开辟一个新的区域存放新对象,会把拷贝的对象和其属性引用的对象都重新生成新的对象。

    • 实现:对拷贝的对象中所引用的数据类型再进行以拷贝;使用序列化
      • 序列化实现样例
import java.io.*;

class Person implements Serializable {
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    // Getter和Setter
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    @Override
    public String toString() {
        return "Person{name='" + name + "', age=" + age + '}';
    }

    // 深拷贝方法
    public Person deepClone() {
        try {
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            ObjectOutputStream oos = new ObjectOutputStream(bos);
            oos.writeObject(this);
            oos.flush();
            ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
            ObjectInputStream ois = new ObjectInputStream(bis);
            return (Person) ois.readObject();
        } catch (IOException | ClassNotFoundException e) {
            e.printStackTrace();
            return null;
        }
    }
}

public class DeepCopyExample {
    public static void main(String[] args) {
        Person original = new Person("Alice", 30);
        Person copy = original.deepClone();

        System.out.println("Original: " + original);
        System.out.println("Copy: " + copy);
    }
}

内部类

使用内部类主要有两个原因:内部类可以对同一个包中的其他类隐藏。内部类方法可以访问定义这个内部类的作用域中的数据,包括原本私有的数据。内部类是一个编译器现象,与虚拟机无关。编译器会把内部类转换成常规的类文件,用美元符号 $ 分隔外部类名与内部类名,而虚拟机对此一无所知。

  • 静态内部类:由 static 修饰,属于外部类本身,只加载一次。类可以定义的成分静态内部类都可以定义,可以访问外部类的静态变量和方法,通过 new 外部类.内部类构造器 来创建对象。只要内部类不需要访问外部类对象,就应该使用静态内部类。
  • 成员内部类:属于外部类的每个对象,随对象一起加载。不可以定义静态成员和方法,可以访问外部类的所有内容,通过 new 外部类构造器.new 内部类构造器 来创建对象。
  • 局部内部类:定义在方法、构造器、代码块、循环中。不能声明访问修饰符,只能定义实例成员变量和实例方法,作用范围仅在声明这个局部类的代码块中。
  • 匿名内部类:没有名字的局部内部类,可以简化代码,匿名内部类会立即创建一个匿名内部类的对象返回,对象类型相当于当前 new 的类的子类类型。匿名内部类一般用于实现事件监听器和其他回调。

类初始化的顺序

  • 父类静态变量和静态代码块
  • 子类静态变量和静态代码块
  • 父类普通变量和代码块
  • 父类构造器
  • 子类普通变量和代码块
  • 子类构造器
    在这里插入图片描述

String,StringBuilder,StringBuffer

String
  • 一旦被创建就不可被修改,所以修改 String 变量值的时候是新建了一个 String 对象,赋值给 原变量引用
  • 两种创建方法
    • 直接赋值一个字符串,就是将字符串放进常量池,位于栈中的变量直接引用常量池中的字符串。
    • new 方式创建先在堆中创建 String 对象,再去常量池中查找是否有赋值的字符串常量,找到了就直接使用,没找到就开辟空间存字符串。通过变量引用对象,对象引用字符串的形式创建。
StringBuilder & StringBuffer
  • 都继承自 AbstractStringBuilder 类,是可变类(这是加分项)
  • 前者线程不安全,后者通过 synchronized 锁保证线程安全
  • 因此 StringBuilder 执行效率高,StringBuffer 执行效率低
在Java中,将String类型放入常量池有以下几个作用:

节省内存:
常量池中的字符串是共享的。如果有多个相同的字符串字面量,它们会引用同一个对象,而不是创建多个对象。
提高性能:
比较字符串时,使用常量池中的字符串可以直接使用引用比较(==),而不是逐字符比较,从而提高性能。
线程安全:
常量池中的字符串是不可变的,因此是线程安全的,可以在多线程环境中安全使用。
简化垃圾回收:
由于常量池中的字符串是共享的,减少了需要垃圾回收的对象数量。
常量池的使用是通过String.intern()方法明确实现的,或者通过直接使用字符串字面量自动实现的。
在Java中,常量池中的字符串是共享的。如果有多个相同的字符串字面量,它们会引用同一个对象。这意味着相同的字符串字面量在常量池中只会存储一份,并且所有引用这些字面量的变量都会指向同一对象。

例如:
String s1 = "hello"; String s2 = "hello";
在这种情况下,s1和s2都引用常量池中的同一个字符串对象。

如果你使用new String(“hello”),则会创建一个新的String对象,而不是使用常量池中的对象。
s1和s2引用的是常量池中同一个字符串对象,所以它们的地址是相同的。换句话说,s1 == s2将返回true。

final 关键字

  • 所修饰的变量,是基本数据类型则值不能改变,访问时会被当做一个常量;是引用型变量的话,初始化后就不能指向另一个对象了。而且一定要显示地初始化赋值。
  • 所修饰的类,不能被继承,其中方法默认是 final 修饰
  • final 修饰的方法不可被重写,但可以被重载
  • final 关键字与变量的作用域有关,而不是直接与类或对象绑定。具体来说:
final 变量:
每个对象都有自己的 final 变量。如果你在类中定义了一个 final 实例变量,每个对象都会有自己的副本,并且这些副本在对象创建后不能被修改。
final 类:
如果一个类被声明为 final,则不能被继承。这是类的特性,而不是对象的特性。
final 方法:
如果一个方法被声明为 final,则不能被子类重写。

static 关键字

  • 修饰代码块,使这个代码块在 JVM 加载之处就开辟一块空间单独存放代码块内容,且只加载一次。执行得到的结果存储在方法区并被线程共享。静态类中的方法直接和这个类关联,而不是和这个对象关联。可以直接通过类名来使用方法。
  • 修饰非局部的成员变量,加载方式和静态代码块一样。由于在 JVM 内存中共享,会引起线程安全问题。解决:加 final;使用同步(volatile 关键字)。如下
public class Counter {
    // 静态变量
    static int count = 0;

    // 构造函数
    public Counter() {
        count++; // 每次创建实例时,count自增
    }

    // 静态方法
    public static void displayCount() {
        System.out.println("当前计数: " + count);
    }

    public static void main(String[] args) {
        Counter c1 = new Counter();
        Counter c2 = new Counter();
        Counter c3 = new Counter();

        // 显示计数
        Counter.displayCount(); // 输出: 当前计数: 3
    }
}
  • 修饰方法,通过类名调用。静态方法不可以直接调用其他成员方法、成员变量。

抽象类和接口

接口只能用 *public * 和 abstract * 修饰
区别分为四个方面:

  • 成员变量:接口中默认 public static final
  • 成员方法:java8 之前接口中默认是 public,java8 加入了 static 和 default,java9 中加入了 private,方法不能用 final 修饰,因为需要实现类重写;抽象类无限制
  • 构造器:接口和抽象类都不能被实例化,但接口中没有构造器,抽象类中有
  • 继承:接口可以多继承,抽象类只能单继承
抽象类和接口的选择?

如果知道某个类应该成为基类,那么第一选择应该是让它成为一个接口,只有在必须要有方法定义和成员变量的时候,才应该选择抽象类。在接口和抽象类的选择上,必须遵守这样一个原则:行为模型应该总是通过接口而不是抽象类定义。通过抽象类建立行为模型会出现的问题:如果有一个产品类 A,有两个子类 B 和 C 分别有自己的功能,如果出现一个既有 B 产品功能又有 C 产品功能的新产品需求,由于 Java 不允许多继承就出现了问题,而如果是接口的话只需要同时实现两个接口即可。

异常

所有的异常都继承自 Throwable 类的,分为 Error 和 Exception。

  • Error 类描述了 Java 运行时系统的内部错误和资源耗尽错误,如果出现了这种错误,一般无能为力。
  • Error 和 RuntimeException 的异常属于非检查型异常,其他的都是检查型异常。

常见的 RuntimeException 异常:

  • ClassCastException,错误的强制类型转换。

  • ArrayIndexOutOfBoundsException,数组访问越界。

  • NullPointerException,空指针异常。

常见的检查型异常:

  • FileNotFoundException,试图打开不存在的文件。

  • ClassNotFoundException,试图根据指定字符串查找 Class 对象,而这个类并不存在。

  • IOException,试图超越文件末尾继续读取数据。

异常处理:

  • 抛出异常:遇到异常不进行具体处理,而是将异常抛出给调用者,由调用者根据情况处理。抛出异常有 2 种形式,一种是 throws 关键字声明抛出的异常,作用在方法上,一种是使用 throw 语句直接抛出异常,作用在方法内。
  • 捕获异常:使用 try/catch 进行异常的捕获,try 中发生的异常会被 catch 代码块捕获,根据情况进行处理,如果有 finally 代码块无论是否发生异常都会执行,一般用于释放资源,Java 7 开始可以将资源定义在 try 代码块中自动释放资源。
  • try-catch-finally
    • finally 对 try 块中打开的物理资源进行回收 (JVM 垃圾回收机制回收对象占用的内存)。
    • 这个回收如果放在 catch 中执行,不发生异常则不会被执行;放在 try 中,如发生异常前就被回收,那么 catch 就不会被执行。
    • java7 可以在 try () 圆括号中初始化或声明资源,会自动回收。但资源需要实现 AutoCloseable 接口

序列化

Java 对象在 JVM 运行时被创建,JVM 退出时存活对象被销毁。为了保证对象及其状态的持久化,就需要使用序列化了。序列化就是将对象通过 ObjectOutputStream 保存为字节流;反序列化就是将字节流还原为对象。

  • 要实现 Serializable 接口来进行序列化。
  • 序列化和反序列化必须保持序列化 ID 的一致。
  • 静态、transient 修饰的变量和方法不能被序列化。
  • 实现 Externalizable 可以自行决定哪些属性可以被序列化

反射

在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取信息以及动态调用对象方法的功能就是 Java 的反射机制。优点是运行时动态获取类的全部信息,缺点是破坏了类的封装性,泛型的约束性。

  • Class 类保存对象运行时信息,可以通过①类名.class ②对象名.getClass ()③Class.forName (类的全限定名) 方式获取 Class 实例
  • Class 类中的 getFields () 返回这个类支持的公共字段;getMethods () 返回公共方法;- getCosntructors () 返回构造器数组(包括父类公共成员)
  • xxxDeclaredxxx () 可以返回全部字段、方法和构造器的数组(不包括父类的成员)

反射是Java语言本身提供的特性,不是Spring框架做的。Java的反射机制允许程序在运行时检查类的属性、方法和构造函数,并可以动态地调用它们。
Spring框架利用了Java的反射机制来实现许多功能,比如依赖注入和AOP(面向切面编程)。但反射的基础功能是Java语言自带的。

IOC控制反转

在Java中,IOC(控制反转)是一种设计原则,主要用于实现对象之间的解耦。IOC的核心思想是将对象的创建和管理交给容器,而不是由对象自身控制。这种方式使得对象的依赖关系更加灵活,便于测试和维护。

主要概念

  • 依赖注入(DI): IOC的常用实现方式,通过构造函数、属性或方法注入依赖对象。
  • 容器: 管理对象生命周期和依赖关系的框架,如Spring框架。
  • 解耦: 通过将对象的依赖关系移交给外部容器,从而减少类之间的耦合度。

优点

  • 可测试性: 方便进行单元测试,因为可以轻松替换依赖。
  • 灵活性: 可以根据需要更改依赖实现,而不需要修改使用它们的代码。
  • 维护性: 代码清晰,逻辑分离,易于维护和扩展。
动态代理

动态代理

示例
在Spring中,使用@Autowired注解进行依赖注入:

@Service
public class UserService {
    @Autowired
    private UserRepository userRepository;

    // 业务逻辑方法
}

在这个示例中,UserRepository的实例由Spring容器管理,UserService不需要自己创建或管理它。

注解

可以给类、接口或者方法、变量添加一些额外信息;帮助编译器和 JVM 完成一些特定功能。

  • 元注解:我们可以自定义一个注解,这时就需要在自定义注解中使用元注解来标识一些信息
    • @Target:约束注解作用位置:METHOD,VARIABLE,TYPE,PARAMETER,CONSTRUCTORS,LOACL_VARIABLE
    • @Rentention:约束注解作用的生命周期:SOURCE 源码,CLASS 字节码,RUNTIME 运行时
    • @Documented:表明这个注解应该被 javadoc 工具记录
    • @Inherited:表面某个被标注的类型是被继承

Java中的注解是Java语言自带的特性,首次引入于Java 5。它们允许开发者在代码中添加元数据,用于配置和处理。
Spring框架则使用了Java注解来提供一些功能,例如:
@Autowired 用于自动装配依赖。
@Component 指示一个类是Spring的组件。
总的来说,注解是Java自带的,而Spring框架只是利用了这些注解来实现其功能。

泛型

泛型的提出是为了编写重用性更好的代码。泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。

那么 Java 之所以引入它我认为主要有三个作用

  • 类型检查,它将运行时类型转换的 ClassCastException 通过泛型提前到编译时期。
    避免类型强转。
  • 泛型可以泛型算法,增加代码的复用性。
    实现
  • 泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List 在运行时仅用一个 List 来表示。这样做的目的,是确保能和 Java 5 之前的版本开发二进制类库进行兼容。

Java 的泛型是如何工作的?什么是类型擦除?如何工作?
1、类型检查:在生成字节码之前提供类型检查
2、类型擦除:所有类型参数都用他们的限定类型替换,包括类、变量和方法(类型擦除)
3、如果类型擦除和多态性发生了冲突时,则在子类中生成桥方法解决
4、如果调用泛型方法的返回类型被擦除,则在调用该方法时插入强制类型转换

hashmap和concurrentHashmap

ConcurrentHashMap

1.7版本
  • 相比HashMap,增加了Segment数组,每个Segement数组有对应的HashEntry数组。
  • 区别:Segement类其中的核心数据如 value ,以及链表Entry<K,V>都是 volatile 修饰的,保证了获取时的可见性,使用get()方法能够随时获取最新的值;其他结构和 HashMap 类似。
  • 采用分段锁技术,Segment 继承于 ReentrantLock,每当一个线程占用锁访问一个 Segment 时,不会影响到其他的 Segment。好处是能够保证线程安全,也不会像 HashTable 那样整张表加锁,以致于 put 和 get 操作都需要做同步处理,所以 ConcurrentHashMap 效率更高。
  • 理论上 ConcurrentHashMap 支持 CurrencyLevel (Segment 数组数量)的线程并发。
put 方法
  • HashEntry前的volatile关键字不能保证并发的原子性,所以执行put前需要加锁,Segment实现了ReentrantLock,通过Segment对象的put方法,别的线程无法对这个Segement中的hash进行操作,可以实现线程安全。
    1、通过 key 计算得到的哈希值找到Segment,在Segment对象中进行具体的put操作。
    2、插入数据前要获取锁。获取到锁之后,就寻找对应的HashEntry进行数据插入操作。
    首先key值不能为空
    如果传入的key值已存在,就进行覆盖原来的值
    如果不存在,首先判断是否需要扩容,然后插入数据
    数据插入完成,释放锁。
get方法

1、通过key值计算出的哈希值,找到对应的Segment
2、从Segment对应的HashEntry中找到值

  • 过程无需获取锁,而且value是volatile修饰,保证了获取时一定是最新值。
1.8版本
结构上变化:
  • 在容器安全上,1.8 中的 ConcurrentHashMap 放弃了 JDK1.7 中的分段技术,而是采用了 CAS 机制 + synchronized 来保证并发安全性,但是在 ConcurrentHashMap 实现里保留了 Segment 定义,这仅仅是为了保证序列化时的兼容性而已,并没有任何结构上的用处。
  • 不再使用Segement,HashEntry改为了Node,作用和HashEntry类似,其中的value和next(链表)都使用volatile修饰。Node数组中元素个数大于8则改用红黑树存储数据。
  • 添加了一个空参构造函数,好处就是实现了懒加载,减少了初始化开销。
    • 初始化是在put操作中完成的。
    • 调用initTable()执行初始化,使用CAS机制

1.判断 sizeCtl 值是否小于 0,如果小于 0 则表示 ConcurrentHashMap 正在执行初始化操作,所以需要先等待一会,如果其它线程初始化失败还可以顶替上去
2.如果 sizeCtl 值大于等于 0,则基于 CAS 策略抢占标记 sizeCtl 为 -1,表示 ConcurrentHashMap 正在执行初始化,然后构造 table,并更新 sizeCtl 的值
3.CAS 通过将内存中的值与期望值进行比较,只有在两者相等时才会对内存中的值进行修改,CAS 不用加锁,但可以在保证性能的同时提供并发场景下的线程安全性。

put 方法(实际上通过putVal实现)
  • 校验key是否为空,不为空计算出hashcode
  • 判断是否要初始化
  • 使用CAS策略或者syncronized锁添加数据到对应的链表或者红黑树中
    • 根据计算得出的 hash 值找到对应的table数组下标位置,如果该位置未存放节点,也就是说不存在 hash 冲突,则使用 CAS 无锁的方式将数据添加到容器中,并且结束循环。
    • 如果前面的条件没满足,则会判断容器是否正在被其他线程进行扩容操作,如果正在被其他线程扩容,则帮助扩容。(扩容中的数据迁移会将头结点使用同步锁锁住,防止别的线程putVal对该链表进行操作,保证了线程安全)
    • 如果上面都不满足,说明发生了 hash 冲突,就进行链表操作或者红黑树操作),在进行链表或者红黑树操作时,会使用 synchronized 锁把头节点被锁住,保证了同时只有一个线程修改链表,防止出现链表成环。
  • 最后更新链表大小,由此判断是否需要扩容。
get方法
  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。

数据结构

Java数据结构


JVM

JVM八股


并发编程

并发编程


Java框架

框架

计算机网络

RPC和http的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值