基础总结
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 寻址,如果就在桶上那么直接返回值。
- 如果是红黑树那就按照树的方式获取值。
- 就不满足那就按照链表的方式遍历获取值。