Java基础

 这篇文章写的不好,请看下面这个进行学习:



♥Java8特性知识体系详解♥ | Java 全栈知识体系 (pdai.tech)

数据类型

基本数据类型,变量存储的是真实数据。引用数据类型,变量中存储的是地址值

在 Java 中,数组同样被视为一个类,它具有 Class 类型的对象来表示其类型。

对于任意的数组类型,无论是基本类型数组还是引用类型数组,都对应着一个 Class 对象。这个 Class 对象包含了描述该数组类型的信息,可以通过它来获取数组的元素类型、维度等信息。

由于数组类型也对应着一个 Class 对象,因此具有相同元素类型和维度的数组共享同一个 Class 对象。换句话说,对于同一种元素类型和维度的数组,无论创建了多少个数组实例,它们对应的 Class 对象都是相同的。这也意味着可以通过 Class 对象来判断两个数组是否具有相同的类型。

除了数组类型,Java 中的基本数据类型(如 boolean、byte、char、short、int、long、float、double)和关键字 void 也都有对应的 Class 对象来表示。这些基本数据类型的 Class 对象提供了对应数据类型的信息,如数据范围、大小等。

总的来说,数组类型和基本数据类型都可以通过 Class 对象来表示,通过这些 Class 对象可以获取类型的相关信息,进行类型检查和操作。这也体现了 Java 中一切皆对象的思想。

包装类

在 Java 中,基本数据类型(如 int、double、char 等)是不具备面向对象特性的,不能直接参与面向对象的操作。为了解决这个问题,Java 提供了对应的包装类,用于将基本数据类型封装成对象,从而可以在面向对象的环境中使用。

Java 中的包装类有以下几种:

  1. Boolean:用于封装布尔类型的值。
  2. Byte:用于封装 byte 类型的值。
  3. Short:用于封装 short 类型的值。
  4. Integer:用于封装 int 类型的值。
  5. Long:用于封装 long 类型的值。
  6. Float:用于封装 float 类型的值。
  7. Double:用于封装 double 类型的值。
  8. Character:用于封装 char 类型的值。

这些包装类提供了一系列方法,用于基本数据类型与对象之间的转换,以及进行基本数据类型的操作。例如,可以使用 Integer 类的 parseInt() 方法将字符串转换为整数,也可以使用 Integer 类的 valueOf() 方法将整数转换为 Integer 对象。

使用包装类的主要场景包括:

  1. 将基本数据类型转换为对象,以便在面向对象的环境中使用。
  2. 在集合类(如 ListMap 等)中存储基本数据类型的值,因为集合类只能存储对象。
  3. 提供了一些便利的方法,用于对基本数据类型进行操作,例如 Integer 类提供了 compareTo()equals() 等方法用于比较整数的大小和相等性。

1.因为多态的存在,所有的对象都可以用object表示,如果没有包装类,基本数据类型就无法接收。

2.集合中不能存储基本数据类型

以下是一些常用的包装类方法:

  1. valueOf(): 静态工厂方法,用于将基本数据类型转换为对应的包装类对象。例如,Integer.valueOf(int)Double.valueOf(double) 等。

  2. xxxValue(): 用于将包装类对象转换为基本数据类型。例如,Integer.intValue()Double.doubleValue() 等。

  3. parseXxx(): 静态方法,用于将字符串解析为基本数据类型。例如,Integer.parseInt(String)Double.parseDouble(String) 等。

  4. toString(): 将包装类对象转换为字符串。例如,Integer.toString()Double.toString() 等。

  5. compareTo(): 用于比较两个包装类对象的大小。例如,Integer.compareTo(Integer)Double.compareTo(Double) 等。

  6. equals(): 用于比较两个包装类对象是否相等。通常用于对象的相等性比较。例如,Integer.equals(Object)Double.equals(Object) 等。

  7. hashCode(): 返回包装类对象的哈希码值。通常与 equals() 方法一起使用。例如,Integer.hashCode()Double.hashCode() 等。

  8. valueOf(): 返回包装类对象的静态工厂方法,用于创建对象。例如,Integer.valueOf(int)Double.valueOf(double) 等。

  9. xxxXxx(): 其他一些辅助方法,例如 Integer.bitCount() 用于计算整数的二进制位数,Double.isNaN() 用于判断一个浮点数是否为 NaN 等。

自动装箱和自动拆箱

是 Java 5 引入的特性,用于简化基本数据类型和对应包装类之间的转换。

自动装箱在需要使用包装类对象的地方,如果直接使用基本数据类型,编译器会自动调用对应的 valueOf() 方法进行装箱操作。

Integer a = 100; // 自动装箱,相当于 Integer a = Integer.valueOf(100);
自动拆箱在需要使用基本数据类型的地方,如果直接使用包装类对象,编译器会自动调用对应的 xxxValue() 方法进行拆箱操作。

Integer a = 100;

int b = a; // 自动拆箱,相当于 int b = a.intValue();

需要注意的是,自动装箱和自动拆箱可以提高代码的简洁性和可读性,但在某些情况下可能会带来性能上的损耗。因为自动装箱和拆箱会涉及到对象的创建和销毁,可能会导致额外的内存分配和垃圾回收。因此,在性能敏感的场景中,应该尽量避免频繁使用自动装箱和拆箱。

在 Java 中,Integer 类提供了一个静态工厂方法 valueOf(int) 用于获取 Integer 对象。对于参数在 -128127 之间的整数,valueOf 方法会返回事先缓存好的 Integer 对象,而不是每次调用都新创建一个对象。这样可以节省内存,避免频繁创建和销毁 Integer 对象,提高性能。

Integer a = Integer.valueOf(100);
Integer b = Integer.valueOf(100);
System.out.println(a == b); // true,返回的是同一个对象,因为参数在缓存范围内

Integer c = Integer.valueOf(1000);
Integer d = Integer.valueOf(1000);
System.out.println(c == d); // false,返回的是不同的对象,因为参数超出了缓存范围
享元模式

是一种结构型设计模式,旨在通过共享对象来减少内存使用和提高性能。它适用于在系统中存在大量相似对象的情况,通过共享这些相似对象的部分状态,减少内存占用并提高系统效率。

在享元模式中,存在两种类型的对象:内部状态(Intrinsic State)和外部状态(Extrinsic State)。内部状态是可以共享的,而外部状态是随着对象的上下文而变化的。

享元模式包含以下几个关键角色:

  1. Flyweight(享元接口):声明了享元对象的接口,通过该接口可以接受和处理外部状态。

  2. ConcreteFlyweight(具体享元类):实现了享元接口,并且包含内部状态。具体享元对象可以被共享,并且在需要时可以接受外部状态。

  3. UnsharedConcreteFlyweight(非共享具体享元类):如果对象的外部状态无法被共享,或者享元对象本身不能被共享,那么就需要使用非共享具体享元类来创建这些对象。

  4. FlyweightFactory(享元工厂):负责创建和管理享元对象,通常实现了对象的池化管理,确保相同的享元对象只被创建一次,并且可以通过共享池来复用对象。

  5. Client(客户端):使用享元模式的客户端,通过享元工厂获取和使用享元对象。客户端通常需要维护外部状态,并在需要时将外部状态传递给享元对象。

以下是一个简单的 Java 代码示例,演示了如何使用享元模式:

// Flyweight 接口
interface Shape {
    void draw();
}

// ConcreteFlyweight 具体享元类
class Circle implements Shape {
    private String color;

    public Circle(String color) {
        this.color = color;
    }

    @Override
    public void draw() {
        System.out.println("Drawing a " + color + " circle.");
    }
}

// FlyweightFactory 享元工厂
class ShapeFactory {
    private static final Map<String, Shape> circleMap = new HashMap<>();

    public static Shape getCircle(String color) {
        Shape circle = circleMap.get(color);
        if (circle == null) {
            circle = new Circle(color);
            circleMap.put(color, circle);
            System.out.println("Creating a new " + color + " circle.");
        }
        return circle;
    }
}

// Client 客户端
public class Client {
    private static final String[] colors = {"Red", "Green", "Blue"};

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Circle circle = (Circle) ShapeFactory.getCircle(getRandomColor());
            circle.draw();
        }
    }

    private static String getRandomColor() {
        return colors[(int) (Math.random() * colors.length)];
    }
}

Circle 类是具体享元类,表示一个圆形。ShapeFactory 是享元工厂,负责创建和管理享元对象。Client 类是客户端,通过享元工厂获取和使用享元对象。

泛型

统一数据类型。把运行时期的问题提前到了编译期间,避免了强制类型转换可能出现的异常,因为在编译阶段类型就能确定下来。

泛型中不能写基本数据类型,指定泛型的具体类型后,传递数据时,可以传入该类类型或者其子类类型,如果不写泛型,类型默认是Object,泛型不具备继承性,但是数据具备继承性

Java 中的泛型擦除(Type Erasure)是指在编译期间,泛型类型信息会被擦除,使得泛型类型在运行时变为原始类型(raw type)。这个过程是为了向后兼容 Java 5 之前的版本,因为 Java 5 中引入了泛型,但是为了与之前的版本兼容,编译器需要将泛型信息擦除,以便在运行时能够与之前的代码交互。

具体来说,泛型擦除发生在编译器将源代码编译成字节码的过程中。在这个过程中,所有的泛型类型都会被擦除为原始类型,即被替换为其原始类型。例如,泛型类、接口、方法等中的类型参数会被擦除为 Object 类型,泛型方法中的类型参数会被擦除为其边界类型(如果没有指定边界类型,则被擦除为 Object 类型)。

泛型擦除的主要影响包括:

  1. 类型擦除:泛型类型信息在运行时被擦除,无法通过反射获取泛型类型的具体信息。例如,无法在运行时获取泛型类的具体类型参数。

  2. 类型转换:泛型类型在运行时被擦除为原始类型,因此泛型类型的方法调用和类型转换都是通过擦除后的类型进行的。

  3. 泛型限制:由于泛型类型在运行时被擦除为原始类型,因此无法在运行时检查泛型类型的参数化类型是否符合限制。例如,对于泛型方法,无法在运行时检查传递的参数是否符合泛型参数的限制。

  4. 兼容性:泛型擦除使得 Java 泛型代码能够与之前的版本兼容,因为泛型类型信息在运行时被擦除,编译后的字节码与之前的代码能够正常交互。

尽管泛型信息在运行时被擦除,但编译器会在编译期间进行类型检查,并插入必要的类型转换以确保类型安全。因此,虽然泛型信息在运行时被擦除,但在编译期间依然可以进行类型检查。

String

String 被声明为 final,因此它不可被继承。

内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。

字符串常量池,储存直接赋值的字符串

当使用双引号直接赋值时,系统会检查该字符串在串池中是否存在。

不存在:创建新的,存在:复用

  

每new一次相同的字符串,都会在堆中重新创建,不会复用

扩展底层原理1:字符串存储的内存原理
直接赋值会复用字符串常量池中的,而new出来不会复用,而是开辟一个新的空间

扩展底层原理2:基本数据类型比较的是数据值,引用数据类型比较的是地址值

==比较基本类型,比较的是值,==比较引用类型,比较的是内存地址

equlas是Object类的方法,本质上与==一样,但是有些类重写了equals方法,比如String的equals被重写后,比较的是字符值,另外重写了equlas后,也必须重写hashcode()方法

扩展底层原理3:字符串拼接的底层原理
如果没有变量参与,都是字符串直接相加,编译之后就是拼接之后的结果,会复用串池中的字符串如果有变量参与,每一行拼接的代码,都会在内存中创建新的字符串,浪费内存。

JDK8以前字符串的拼接每次都会产生新的对象

JDK8以后字符串拼接会进行预估,先创建byte数组,在进行拼接

字符串拼接的时候,如果有变量:
JDK8以前:系统底层会自动创建一个StringBuilder对象,然后再调用其append方法完成拼接拼接后,再调用其toString方法转换为String类型,而toString方法的底层是直接new了一个字符串对象。

JDK8版本:系统会预估要字符串拼接之后的总大小,把要拼接的内容都放在数组中,此时也是产生一个新的字符串。

字符串拼接的时候有变量参与:在内存中创建了很多对象浪费空间,时间也非常慢。

结论:如果很多字符串变量拼接,不要直接+。在底层会创建多个对象,浪费时间,浪费性能。

扩展底层原理4:StringBuilder提高效率原理图
所有要拼接的内容都会往StringBuilder中放,不会创建很多无用的空间,节约内存

扩展底层原理5:StringBuilder源码分析

  1. 默认创建一个长度为16的字节数组
  2. 添加的内容长度小于16,直接存
  3. 添加的内容大于16会扩容(原来的容量*2+2)
  4. 如果扩容之后还不够,以实际长度为准
  5. StringBuilder的存储上限为整数的最大值

当对字符串进行修改的时候,需要使用 StringBuffer 和 StringBuilder 类。

在使用 StringBuffer 类时,每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象,

它和 StringBuffer 之间的最大不同在于 StringBuilder 的方法不是线程安全的(不能同步访问)。

  1. StringBuffer

    • StringBuffer 是 Java 中用于处理可变字符串的类之一,它的设计是为了支持多线程环境下的操作,所有的公共方法都是同步的,即它们都使用了 synchronized 关键字来确保在多线程环境中的线程安全性。
    • 由于所有的公共方法都进行了同步,这导致了在多线程环境下使用 StringBuffer 会带来一定的性能损失,因为每个操作都需要进行同步锁的获取和释放。
  2. StringBuilder

    • StringBuilder 也是用于处理可变字符串的类,但是它是在 Java 5 中引入的,相比于 StringBuffer,它的设计目标是提供一个更高效的可变字符串操作类。
    • StringBuffer 不同,StringBuilder 的方法都不是同步的,即它们没有使用 synchronized 关键字进行同步,因此在单线程环境下的操作会更加高效。
    • 由于 StringBuilder 的方法没有进行同步,因此在多线程环境下使用 StringBuilder 可能会导致线程安全问题,需要开发人员自行保证线程安全性。

综上所述,StringBuffer 是线程安全的类,因为它的所有公共方法都进行了同步,而 StringBuilder 是非线程安全的类,因为它的方法没有进行同步。在选择使用哪一个类时,需要根据具体的需求和场景来进行考虑。如果在多线程环境下需要进行可变字符串的操作,应该选择 StringBuffer;如果在单线程环境下需要进行可变字符串的操作,并且希望获得更好的性能,可以选择 StringBuilder

StringBuilder 可以看成是一个容器,创建之后里面的内容是可变的

StringJoiner跟StringBuilder一样,也可以看成是一个容器,创建之后里面的内容是可变的,。

作用:提高字符串的操作效率,而且代码编写特别简洁,JDK8出现的 

BigInteger 

如果BigInteger表示的数字没有超出long的范围,可以用静态方法获取。

  • public static BigInteger valueOf(long val),静态方法获取BigInteger的对象,内部有优化。
  • 在内部对常用的数字:-16 ~ 16 进行了优化。提前把-16~16 先创建好BigInteger的对象,如果多次获取不会重新创建新的。

如果BigInteger表示的超出long的范围,可以用构造方法获取。

  • public BigInteger(string val)获取指定的大整数,

对象一旦创建,BigInteger内部记录的值不能发生改变。

只要进行计算都会产生一个新的BigInteger对象。

底层存储方式:

它的底层存储方式是使用数组来存储整数的每个位。具体来说,BigInteger 使用一个 int 数组来存储整数的各个位,数组中的每个元素表示整数的一段位(32 位),每个元素的取值范围是 -2^31 到 2^31-1。其中,数组的第一个元素存储整数的最低有效位,数组的最后一个元素存储整数的最高有效位。由于数组大小有上限,是int的最大值,所以,BigInteger某种意义上说有上限。

BigInteger 类中,mag 数组存储整数的各个位,而 signum 表示整数的符号,取值为 -1、0 或 1,分别表示负数、零和正数。bitCount 表示整数的位数,bitLength 表示整数的字节数,hashCode 缓存了整数的哈希码值。由于 BigInteger 的底层存储方式采用了数组,所以它能够表示任意精度的整数,不受固定位数限制。

BigDecima

如果要表示的数字不大,没有超出double的取值范围,建议使用静态方法

  • public static BigDEcima valueOf(double val);将传递的数据变成字符串,如果我们传递的是0~10之间的整数,包含0,包含10,那么方法会返回已经创建好的对象,不会重新new

如果要表示的数字比较大,超出了double的取值范围,建议使用构造方法

  • public BigDEcima (string val);

底层实现原理:BigDecimal使用的是byte数组存每个字符在ASCII码表中对应的数字,如-1.5是byte[15,49,46,53]

BigDecimal 类提供了一系列方法来对十进制数进行精确的运算,包括加法、减法、乘法、除法等。由于 BigDecimal 是以十进制的方式表示数字,而不是以二进制方式表示(像 doublefloat),因此它可以避免浮点数运算中的精度丢失问题

集合

迭代器Iterator

报错NoSuchElementException

迭代器遍历完毕,指针不会复位

循环中只能用一次next方法

迭代器遍历时,不能用集合的方法进行增加或者删除

增强for的底层就是迭代器,为了简化迭代器的代码书的。是JDK5之后出现的,其内部原理就是一个Iterator迭代器,单列集合和数组才能用增强for进行遍历。

单列集合

以下是单列集合常见的 API:

  1. 添加元素

    • boolean add(E e): 将指定的元素添加到集合中,如果元素之前不存在,则返回 true,否则返回 false。
  2. 删除元素

    • boolean remove(Object o): 从集合中移除指定的元素,如果元素存在并成功移除,则返回 true,否则返回 false。
    • void clear(): 清空集合中的所有元素,使集合变为空集合。
  3. 查询元素

    • boolean contains(Object o): 判断集合是否包含指定的元素,如果包含则返回 true,否则返回 false。
    • int size(): 返回集合中的元素个数。
  4. 遍历元素

    • Iterator<E> iterator(): 返回一个迭代器,用于遍历集合中的元素。
    • forEach(Consumer<? super E> action): 使用指定的操作(Consumer)对集合中的每个元素执行操作。
    • void forEachOrdered(Consumer<? super E> action): 按照元素的顺序执行指定的操作。
  5. 集合操作

    • boolean isEmpty(): 判断集合是否为空,如果集合不包含任何元素,则返回 true。
    • Object[] toArray(): 将集合中的元素转换为一个数组。

ArratList

LinkedList

底层数据结构是双链表,查询慢,首尾操作的速度是极快的,所以多了很多首尾操作的特有API。

HashSet

哈希值:根据hashcode方法算出来的int类型的整数,该方法定义在0bject类中,所有对象都可以调用,默认使用地址值进行计算一般情况下,会重写hashcode方法,利用对象内部的属性值计算哈希值

1.Hashset集合的底层数据结构是什么样的?

2.Hashset添加元素的过程?

3.Hashset为什么存和取的顺序不一样?

4.Hashset为什么没有索引?

5.Hashset是利用什么机制保证去重的?

HashSet 是基于哈希表实现的集合类,它的底层数据结构是哈希表。具体来说,HashSet 使用了一个 HashMap 来存储元素,其中的 key 表示集合中的元素,而 value 则是一个固定的常量对象(例如 PRESENT)。HashSet 实际上是通过 HashMap 来实现的,只是 HashMap 中的 value 部分被固定为一个常量对象。

HashSet 添加元素的过程如下:

1. 将要添加的元素使用哈希函数进行哈希计算,得到其哈希值。
2. 根据哈希值确定元素应该存放在哈希表的哪个位置。
3. 如果该位置上已经存在元素,则进行碰撞解决(通常是链地址法),将新元素插入到链表的末尾。
4. 如果该位置上没有元素,则直接将新元素插入到该位置。

HashSet 存储和取出元素的顺序不一样,这是因为 HashSet 是基于哈希表实现的,哈希表中的元素是按照哈希值来存储的,而不是按照插入顺序或者自然顺序。因此,HashSet 中的元素顺序是不可预测的。

HashSet 没有索引是因为它是基于哈希表实现的,哈希表中的元素是按照哈希值来存储的,而不是按照索引顺序。因此,HashSet 不支持通过索引来访问元素。

HashSet 通过哈希函数来计算元素的哈希值,并根据哈希值确定元素在哈希表中的存储位置。当添加新元素时,HashSet 首先会计算元素的哈希值,然后根据哈希值确定元素应该存放在哈希表的哪个位置。如果该位置已经存在元素,则进行碰撞解决;如果该位置上没有元素,则直接插入新元素。由于哈希表的特性,相同的元素经过哈希计算后会得到相同的哈希值,因此 HashSet 能够通过哈希表来保证元素的唯一性,从而实现去重的功能。

 

LinkedHashSet

LinkedHashMap 是 HashMap 的一个子类,它在内部使用一个双向链表来维护元素的插入顺序。因此,LinkedHashSet 继承了 LinkedHashMap 的有序特性,保证了元素的插入顺序和迭代顺序一致。

LinkedHashSet 的底层数据结构由 LinkedHashMap 维护,它使用哈希表来存储元素,同时使用双向链表来维护元素的插入顺序。在 LinkedHashMap 中,哈希表的每个桶中的元素以链表的形式连接起来,保证了元素的哈希顺序,而双向链表则负责维护元素的插入顺序。这样一来,LinkedHashSet 即实现了哈希表的高效查找、插入和删除操作,又保留了元素的有序性。LinkedHashSet 并不会使用红黑树作为其底层数据结构,而是通过 LinkedHashMap 实现了有序的哈希集合。

TreeSet

对于数值类型:Integer,Double,默认按照从小到大的顺序进行排序。对于字符、字符串类型:按照字符在ASCII码表中的数字升序进行排序

TreeSet 是基于红黑树(Red-Black Tree)实现的有序集合,它是 Set 接口的一个实现类。红黑树是一种自平衡的二叉搜索树,具有良好的平衡性能,能够保证在最坏情况下的时间复杂度为 O(log n)。

TreeSet 的底层数据结构是一个红黑树,具体来说,是一个用红黑树实现的有序集合。在 TreeSet 中,元素是按照自然顺序或者通过比较器(Comparator)来排序的。当元素被添加到 TreeSet 中时,它会根据比较规则将元素插入到适当的位置,以保持集合的有序性。

TreeSet 的主要特点包括:

  1. 有序性:TreeSet 中的元素是按照自然顺序或者通过比较器排序的,因此它是有序的。

  2. 唯一性:TreeSet 中不允许重复元素,每个元素在集合中只能出现一次。

  3. 高效性:由于 TreeSet 是基于红黑树实现的,它具有良好的平衡性能,能够保证在最坏情况下的时间复杂度为 O(log n),因此具有高效的插入、删除和查找操作。

TreeSet 的基本操作包括插入、删除和查找,这些操作都能够在 O(lo

双列集合

以下是双列集合常见的 API:

  1. 添加和修改操作

    • V put(K key, V value): 将指定的键值对添加到 Map 中,如果键已经存在,则更新对应的值,并返回之前的值;如果键不存在,则返回 null。
    • void putAll(Map<? extends K, ? extends V> m): 将指定 Map 中的所有键值对添加到当前 Map 中。
  2. 删除操作

    • V remove(Object key): 根据键移除对应的键值对,如果键存在则返回对应的值,如果键不存在则返回 null。
    • void clear(): 清空 Map 中的所有键值对。
  3. 查询操作

    • V get(Object key): 根据键获取对应的值,如果键存在则返回对应的值,如果键不存在则返回 null。
    • boolean containsKey(Object key): 判断 Map 中是否包含指定的键
    • boolean containsValue(Object value): 判断 Map 中是否包含指定的值。
    • int size(): 返回 Map 中键值对的数量。
  4. 遍历操作

    • Set<K> keySet(): 返回包含所有键的 Set 集合,可以通过遍历该集合来访问所有的键。
    • Collection<V> values(): 返回包含所有值的 Collection 集合,可以通过遍历该集合来访问所有的值。
    • Set<Map.Entry<K, V>> entrySet(): 返回包含所有键值对的 Set 集合,可以通过遍历该集合来访问所有的键值对。每个键值对都表示为 Map.Entry 对象,可以通过 getKey() 和 getValue() 方法获取键和值。Entry是Map中的接口。
    • forEach,底层是使用entrySet(),得到键和值,在调用accept方法。
  5. 其他操作

    • boolean isEmpty(): 判断 Map 是否为空,如果 Map 不包含任何键值对,则返回 true。
    • boolean equals(Object obj): 判断当前 Map 是否与指定对象相等,如果指定对象也是一个 Map,并且包含相同的键值对,则返回 true。
    • int hashCode(): 返回 Map 的哈希码值。

通过以上 API,可以方便地对双列集合进行键值对的添加、删除、查询、遍历等操作。需要注意的是,双列集合中的键是唯一的,但值可以重复。

HashMap

HashMap 是 Java 中常用的基于哈希表实现的 Map 接口的实现类,它以键值对的形式存储数据,并提供了高效的插入、删除和查找操作。

  1. 特点

    • HashMap 允许 null 键和 null 值。在内部实现中,当插入一个 null 键时,HashMap 会特别处理它,不调用 hashCode() 方法(因为 null 没有 hashCode()),而是直接将它放在表的第一个桶位置。
    • HashMap 不保证元素的顺序,即元素的存储顺序是不确定的。
    • HashMap 是非同步的,不是线程安全的,如果需要在多线程环境中使用,可以考虑使用 ConcurrentHashMap。
    • HashMap 的性能表现受到初始容量和负载因子的影响。可以通过构造函数指定初始容量和负载因子来优化 HashMap 的性能。

在HashMap中,每个元素Node<K,V>都是一个Entry<K,V>对象

LinkedHashMap

Java 中的一个特殊的哈希表实现,它继承自 HashMap 类,并且保留了键值对的插入顺序。与普通的 HashMap 不同,LinkedHashMap 内部使用一个双向链表来维护元素的插入顺序,从而使得元素在遍历时保持插入顺序。

以下是 LinkedHashMap 的特点

  1. 特点

    • LinkedHashMap 继承自 HashMap,因此具有 HashMap 的所有特性,包括高效的插入、删除和查找操作。
    • LinkedHashMap 内部使用一个双向链表来维护元素的插入顺序,因此能够保持元素的插入顺序。
    • LinkedHashMap 是有序的,它可以按照插入顺序或者访问顺序进行迭代。可以通过构造函数指定按照访问顺序来排序,当访问一个已经存在的键时,该键值对会被移到链表的末尾。

LinkedHashMap 提供了 HashMap 的所有功能,并且额外增加了保持插入顺序的特性。通过 LinkedHashMap,可以在保持 HashMap 的高效性的同时,按照插入顺序来迭代元素,方便地实现 LRU(Least Recently Used)缓存等功能。

TreeMap

TreeMap 是 Java 中实现了 SortedMap 接口的基于红黑树(Red-Black Tree)实现的有序映射。它按照键的自然顺序或者指定的比较器对键进行排序,并且保持了键值对的有序性。

以下是 TreeMap 的特点:

  1. 特点

    • TreeMap 实现了 SortedMap 接口,因此它是有序的。
    • TreeMap 内部使用红黑树来存储键值对,红黑树是一种自平衡的二叉搜索树,能够保持键值对的有序性,并且保证了插入、删除和查找操作的时间复杂度为 O(log n)。
    • TreeMap 允许 null 键和 null 值,但只有在自然排序时才允许 null 键。
    • TreeMap 是非同步的,不是线程安全的,如果需要在多线程环境中使用,可以考虑使用 ConcurrentSkipListMap。
    • TreeMap(Comparator<? super K> comparator): 创建一个空的 TreeMap,并且使用指定的比较器来进行排序。
    • Javabean类实现Comparable接口,指定比较规则
    • 创建集合时,自定义Comparator比较器对象,指定比较规则                                                                             

HashMap源码

1.看源码之前需要了解的一些内容

Node<K,V>[] table   哈希表结构中数组的名字

DEFAULT_INITIAL_CAPACITY:   数组默认长度16

DEFAULT_LOAD_FACTOR:        默认加载因子0.75



HashMap里面每一个对象包含以下内容:
1.1 链表中的键值对对象
    包含:  
			int hash;         //键的哈希值
            final K key;      //键
            V value;          //值
            Node<K,V> next;   //下一个节点的地址值
			
			
1.2 红黑树中的键值对对象
	包含:
			int hash;         		//键的哈希值
            final K key;      		//键
            V value;         	 	//值
            TreeNode<K,V> parent;  	//父节点的地址值
			TreeNode<K,V> left;		//左子节点的地址值
			TreeNode<K,V> right;	//右子节点的地址值
			boolean red;			//节点的颜色
					


2.添加元素
HashMap<String,Integer> hm = new HashMap<>();
hm.put("aaa" , 111);
hm.put("bbb" , 222);
hm.put("ccc" , 333);
hm.put("ddd" , 444);
hm.put("eee" , 555);

添加元素的时候至少考虑三种情况:
2.1数组位置为null
2.2数组位置不为null,键不重复,挂在下面形成链表或者红黑树
2.3数组位置不为null,键重复,元素覆盖



//参数一:键
//参数二:值

//返回值:被覆盖元素的值,如果没有覆盖,返回null
public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}


//利用键计算出对应的哈希值,再把哈希值进行一些额外的处理
//简单理解:返回值就是返回键的哈希值
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

//参数一:键的哈希值
//参数二:键
//参数三:值
//参数四:如果键重复了是否保留
//		   true,表示老元素的值保留,不会覆盖
//		   false,表示老元素的值不保留,会进行覆盖
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,boolean evict) {
	    //定义一个局部变量,用来记录哈希表中数组的地址值。
        Node<K,V>[] tab;
		
		//临时的第三方变量,用来记录键值对对象的地址值
        Node<K,V> p;
        
		//表示当前数组的长度
		int n;
		
		//表示索引
        int i;
		
		//把哈希表中数组的地址值,赋值给局部变量tab
		tab = table;

        if (tab == null || (n = tab.length) == 0){
			//1.如果当前是第一次添加数据,底层会创建一个默认长度为16,加载因子为0.75的数组
			//2.如果不是第一次添加数据,会看数组中的元素是否达到了扩容的条件
			//如果没有达到扩容条件,底层不会做任何操作
			//如果达到了扩容条件,底层会把数组扩容为原先的两倍,并把数据全部转移到新的哈希表中
			tab = resize();
			//表示把当前数组的长度赋值给n
            n = tab.length;
        }

		//拿着数组的长度跟键的哈希值进行计算,计算出当前键值对对象,在数组中应存入的位置
		i = (n - 1) & hash;//index
		//获取数组中对应元素的数据
		p = tab[i];
		
		
        if (p == null){
			//底层会创建一个键值对对象,直接放到数组当中
            tab[i] = newNode(hash, key, value, null);
        }else {
            Node<K,V> e;
            K k;
			
			//等号的左边:数组中键值对的哈希值
			//等号的右边:当前要添加键值对的哈希值
			//如果键不一样,此时返回false
			//如果键一样,返回true
			boolean b1 = p.hash == hash;
			
            if (b1 && ((k = p.key) == key || (key != null && key.equals(k)))){
                e = p;
            } else if (p instanceof TreeNode){
				//判断数组中获取出来的键值对是不是红黑树中的节点
				//如果是,则调用方法putTreeVal,把当前的节点按照红黑树的规则添加到树当中。
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            } else {
				//如果从数组中获取出来的键值对不是红黑树中的节点
				//表示此时下面挂的是链表
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
						//此时就会创建一个新的节点,挂在下面形成链表
                        p.next = newNode(hash, key, value, null);
						//判断当前链表长度是否超过8,如果超过8,就会调用方法treeifyBin
						//treeifyBin方法的底层还会继续判断
						//判断数组的长度是否大于等于64
						//如果同时满足这两个条件,就会把这个链表转成红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1)
                            treeifyBin(tab, hash);
                        break;
                    }
					//e:			  0x0044  ddd  444
					//要添加的元素: 0x0055   ddd   555
					//如果哈希值一样,就会调用equals方法比较内部的属性值是否相同
                    if (e.hash == hash && ((k = e.key) == key || (key != null && key.equals(k)))){
						 break;
					}

                    p = e;
                }
            }
			
			//如果e为null,表示当前不需要覆盖任何元素
			//如果e不为null,表示当前的键是一样的,值会被覆盖
			//e:0x0044  ddd  555
			//要添加的元素: 0x0055   ddd   555
            if (e != null) {
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null){
					
					//等号的右边:当前要添加的值
					//等号的左边:0x0044的值
					e.value = value;
				}
                afterNodeAccess(e);
                return oldValue;
            }
        }
		
        //threshold:记录的就是数组的长度 * 0.75,哈希表的扩容时机  16 * 0.75 = 12
        if (++size > threshold){
			 resize();
		}
        
		//表示当前没有覆盖任何元素,返回null
        return null;
    }

TreeMap源码

1.TreeMap中每一个节点的内部属性
K key;					//键
V value;				//值
Entry<K,V> left;		//左子节点
Entry<K,V> right;		//右子节点
Entry<K,V> parent;		//父节点
boolean color;			//节点的颜色




2.TreeMap类中中要知道的一些成员变量
public class TreeMap<K,V>{
   
    //比较器对象
    private final Comparator<? super K> comparator;

	//根节点
    private transient Entry<K,V> root;

	//集合的长度
    private transient int size = 0;

   

3.空参构造
	//空参构造就是没有传递比较器对象
	 public TreeMap() {
        comparator = null;
    }
	
	
	
4.带参构造
	//带参构造就是传递了比较器对象。
	public TreeMap(Comparator<? super K> comparator) {
        this.comparator = comparator;
    }
	
	
5.添加元素
	public V put(K key, V value) {
        return put(key, value, true);
    }

参数一:键
参数二:值
参数三:当键重复的时候,是否需要覆盖值
		true:覆盖
		false:不覆盖
		
	private V put(K key, V value, boolean replaceOld) {
		//获取根节点的地址值,赋值给局部变量t
        Entry<K,V> t = root;
		//判断根节点是否为null
		//如果为null,表示当前是第一次添加,会把当前要添加的元素,当做根节点
		//如果不为null,表示当前不是第一次添加,跳过这个判断继续执行下面的代码
        if (t == null) {
			//方法的底层,会创建一个Entry对象,把他当做根节点
            addEntryToEmptyMap(key, value);
			//表示此时没有覆盖任何的元素
            return null;
        }
		//表示两个元素的键比较之后的结果
        int cmp;
		//表示当前要添加节点的父节点
        Entry<K,V> parent;
		
		//表示当前的比较规则
		//如果我们是采取默认的自然排序,那么此时comparator记录的是null,cpr记录的也是null
		//如果我们是采取比较去排序方式,那么此时comparator记录的是就是比较器
        Comparator<? super K> cpr = comparator;
		//表示判断当前是否有比较器对象
		//如果传递了比较器对象,就执行if里面的代码,此时以比较器的规则为准
		//如果没有传递比较器对象,就执行else里面的代码,此时以自然排序的规则为准
        if (cpr != null) {
            do {
                parent = t;
                cmp = cpr.compare(key, t.key);
                if (cmp < 0)
                    t = t.left;
                else if (cmp > 0)
                    t = t.right;
                else {
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        } else {
			//把键进行强转,强转成Comparable类型的
			//要求:键必须要实现Comparable接口,如果没有实现这个接口
			//此时在强转的时候,就会报错。
            Comparable<? super K> k = (Comparable<? super K>) key;
            do {
				//把根节点当做当前节点的父节点
                parent = t;
				//调用compareTo方法,比较根节点和当前要添加节点的大小关系
                cmp = k.compareTo(t.key);
				
                if (cmp < 0)
					//如果比较的结果为负数
					//那么继续到根节点的左边去找
                    t = t.left;
                else if (cmp > 0)
					//如果比较的结果为正数
					//那么继续到根节点的右边去找
                    t = t.right;
                else {
					//如果比较的结果为0,会覆盖
                    V oldValue = t.value;
                    if (replaceOld || oldValue == null) {
                        t.value = value;
                    }
                    return oldValue;
                }
            } while (t != null);
        }
		//就会把当前节点按照指定的规则进行添加
        addEntry(key, value, parent, cmp < 0);
        return null;
    }	
	
	
	
	 private void addEntry(K key, V value, Entry<K, V> parent, boolean addToLeft) {
        Entry<K,V> e = new Entry<>(key, value, parent);
        if (addToLeft)
            parent.left = e;
        else
            parent.right = e;
		//添加完毕之后,需要按照红黑树的规则进行调整
        fixAfterInsertion(e);
        size++;
        modCount++;
    }
	
	
	
	private void fixAfterInsertion(Entry<K,V> x) {
		//因为红黑树的节点默认就是红色的
        x.color = RED;

		//按照红黑规则进行调整
		
		//parentOf:获取x的父节点
		//parentOf(parentOf(x)):获取x的爷爷节点
		//leftOf:获取左子节点
        while (x != null && x != root && x.parent.color == RED) {
			
			
			//判断当前节点的父节点是爷爷节点的左子节点还是右子节点
			//目的:为了获取当前节点的叔叔节点
            if (parentOf(x) == leftOf(parentOf(parentOf(x)))) {
				//表示当前节点的父节点是爷爷节点的左子节点
				//那么下面就可以用rightOf获取到当前节点的叔叔节点
                Entry<K,V> y = rightOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
					//叔叔节点为红色的处理方案
					
					//把父节点设置为黑色
                    setColor(parentOf(x), BLACK);
					//把叔叔节点设置为黑色
                    setColor(y, BLACK);
					//把爷爷节点设置为红色
                    setColor(parentOf(parentOf(x)), RED);
					
					//把爷爷节点设置为当前节点
                    x = parentOf(parentOf(x));
                } else {
					
					//叔叔节点为黑色的处理方案
					
					
					//表示判断当前节点是否为父节点的右子节点
                    if (x == rightOf(parentOf(x))) {
						
						//表示当前节点是父节点的右子节点
                        x = parentOf(x);
						//左旋
                        rotateLeft(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateRight(parentOf(parentOf(x)));
                }
            } else {
				//表示当前节点的父节点是爷爷节点的右子节点
				//那么下面就可以用leftOf获取到当前节点的叔叔节点
                Entry<K,V> y = leftOf(parentOf(parentOf(x)));
                if (colorOf(y) == RED) {
                    setColor(parentOf(x), BLACK);
                    setColor(y, BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    x = parentOf(parentOf(x));
                } else {
                    if (x == leftOf(parentOf(x))) {
                        x = parentOf(x);
                        rotateRight(x);
                    }
                    setColor(parentOf(x), BLACK);
                    setColor(parentOf(parentOf(x)), RED);
                    rotateLeft(parentOf(parentOf(x)));
                }
            }
        }
		
		//把根节点设置为黑色
        root.color = BLACK;
    }
TreeMap添加元素的时候,键是否需要重写hashCode和equals方法?
此时是不需要重写的。
HashMap是哈希表结构的,JDK8开始由数组,链表,红黑树组成的。
既然有红黑树,HashMap的键是否需要实现Compareable接口或者传递比较器对象呢?
不需要的。因为在HashMap的底层,默认是利用哈希值的大小关系来创建红黑树的。
TreeMap和HashMap谁的效率更高?
如果是最坏情况,添加了8个元素,这8个元素形成了链表,此时TreeMap的效率要更高
但是这种情况出现的几率非常的少。
一般而言,还是HashMap的效率要更高。
三种双列集合,以后如何选择?
    HashMap LinkedHashMap TreeMap
    默认:HashMap(效率最高)
    如果要保证存取有序:LinkedHashMap
    如果要进行排序:TreeMap

不可变集合

对象

在 Java 中,对象的创建可以通过以下几种方式来实现:

  1. 使用 new 关键字:最常见的方式是使用 new 关键字直接调用构造方法来创建对象。例如:

    MyClass obj = new MyClass();
    
  2. 通过反射:可以使用 Java 的反射机制来动态地创建对象。通过获取类的 Class 对象,然后调用 newInstance() 方法来实现。例如:

    MyClass obj = (MyClass) Class.forName("com.example.MyClass").newInstance();
    
  3. 使用 clone() 方法:如果类实现了 Cloneable 接口,并重写了 clone() 方法,可以通过调用 clone() 方法来创建对象的副本。例如:

    MyClass obj1 = new MyClass();
    MyClass obj2 = (MyClass) obj1.clone();
    
  4. 通过反序列化:可以将对象序列化为字节流,然后再将其反序列化为对象。这种方式可以用来实现对象的深拷贝。Java 序列化和反序列化(一)Serializable 使用场景 - binarylei - 博客园 (cnblogs.com)

  5. 例如:

    // 序列化
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("object.ser"));
    out.writeObject(obj);
    out.close();
    
    // 反序列化
    ObjectInputStream in = new ObjectInputStream(new FileInputStream("object.ser"));
    MyClass obj = (MyClass) in.readObject();
    in.close();
    
  6. 使用工厂方法:可以定义一个工厂类来负责对象的创建,隐藏对象的具体实现细节。例如:

    public class MyClassFactory {
        public static MyClass createInstance() {
            return new MyClass();
        }
    }
    
    MyClass obj = MyClassFactory.createInstance();
    

static

静态变量是随着类的加载而加载的,优先于对象出现的

  1. 静态方法只能访问静态变量和静态方法
  2. 非静态方法可以访问静态变量或者静态方法,也可以访问非静态的成员变量和非静态的成员方法
  3. 静态方法中是没有this关键字

静态方法中,只能访问静态。非静态方法可以访问所有。静态方法中没有this关键字

 

继承

构造方法:在 Java 中,子类默认会调用父类的无参构造方法(如果父类有),但是父类的构造方法本身并不会被继承。这是因为构造方法不是普通的类成员,它们具有特殊的作用,用于对象的初始化。因此,构造方法不适合被继承。

以下是一些原因解释为什么父类的构造方法不能被子类继承:

  1. 构造方法的特殊性: 构造方法用于创建对象并进行初始化,它们在对象创建时被调用,用于初始化对象的状态。由于构造方法的调用与方法的继承机制有所不同,因此它们不适合被继承。

  2. 构造方法不是普通的成员方法: 构造方法与普通的成员方法不同,它们具有特殊的语法和作用。构造方法不会被继承,因为它们是用于创建对象的,而子类的对象是由子类自己的构造方法创建的。

  3. 构造方法不能被子类覆盖: 在 Java 中,子类可以通过覆盖(重写)父类的方法来改变方法的行为,但是构造方法不能被覆盖。因此,即使子类可以调用父类的构造方法,但它们并不会继承父类的构造方法。

子类在初始化的时候,有可能会使用到父类中的数据,如果父类没有完成初始化,子类将无法使用父类的数据。子类初始化之前,一定要调用父类构造方法先完成父类数据空间的初始化。

尽管子类不能继承父类的构造方法,但是在子类的构造方法中可以使用 super() 来调用父类的构造方法,以完成对父类部分的初始化。这种方式可以确保子类对象在创建时也能进行父类部分的初始化工作。子类不能继承父类的构造方法,但是可以通过super调用,子类构造方法的第一行,有一个默认的super();,默认先访问父类中无参的构造方法(原因是在顶级父类object中没有成员变量,只有无参构造方法),再执行自己,如果想要方法文父类有参构造,必须手动书写

成员变量:在 Java 中,子类会继承父类的所有成员变量,包括私有的成员变量。但是,私有的成员变量对于子类来说是不可见的,因此子类无法直接访问或者使用父类的私有成员变量。

成员方法: 能被加载到虚方法表的方法可以被子类继承。

Object 类是所有类的根类,因此它定义了一些通用的方法,这些方法可以加入到虚方法表中。

  1. equals(Object obj): 用于比较两个对象是否相等。默认实现是比较两个对象的引用是否相等(即是否指向同一个对象),但可以被子类重写以提供更具体的相等性比较方法。(在stringBuilder当中,没有重写equals方法使用的是object中的equals方法,在object当中默认是使用==号比较两个对象的地址值,字符串中的equals方法,先判断参数是否为字符串,如果是字符串,再比较内部的属性,但是如果参数不是字符串,直接返回false

  2. hashCode(): 返回对象的哈希码值。默认实现是返回对象的内存地址的哈希码,但可以被子类重写以提供更具体的哈希码计算方法。

  3. toString(): 返回对象的字符串表示。默认实现是返回对象的类名,后跟 "@" 符号和对象的哈希码值的无符号十六进制表示形式。(当我们打印一个对象的时候,底层会调用对象的tostring方法,把对象变成字符串。然后再打印在控制台上,打印完毕换行处理。

  4. getClass(): 返回对象的运行时类。通常用于获取对象的类信息,例如判断对象的类型。

  5. clone(): 创建并返回此对象的一个副本。默认实现是浅拷贝,但可以被子类重写以提供更复杂的拷贝行为。

  6. finalize(): 在垃圾回收器从对象回收内存之前调用。通常用于释放对象所占用的资源。

重写

子类覆盖了从父类虚方法表中的方法

  • 重写方法的名称、形参列表必须与父类中的一致。
  • 子类重写父类方法时,访问权限子类必须大于等于父类(<protected<public)
  • 子类重写父类方法时,返回值类型子类必须小于等于父类
  • 建议:重写的方法尽量和父类保持一致。
  • 只有被添加到虚方法表中的方法才能被重写

重载和重写的区别

重载发生在同一个类中,方法名相同、参数列表、返回类型、权限修饰符可以不同

重写发生在子类中,方法名相、参数列表、返回类型都相同,权限修饰符要大于父类方法,声明异常范围要小于父类方法,但是final和private修饰的方法不可重写

多态

多态就是对象的多种形态,前提是有继承/实现关系,有父类引用指向子类对象,有方法的重写。

多态的好处是使用父类型作为参数,可以接收所有子类对象体现多态的扩展性与便利。

Java加载字节码文件,先加载父类,在加载子类

多态调用成员的特点:

  • 变量调用:编译看左边,运行也看左边。
  • 方法调用:编译看左边,运行看右边。

多态的弊端:不能调用子类的特有功能,此时可以使用instanceof转换类型

final

不能被继承:当一个类被声明为 final 时,它不可以被其他类继承。这意味着不能创建这个 final 类的子类。这通常用于防止类的进一步扩展或修改,以确保类的不可变性。

不可修改final 类中的成员变量通常也会使用 final 关键字修饰,这意味着这些成员变量的值在对象创建后不能被修改。这样可以确保对象的状态在创建后保持不变,从而避免了对象的状态发生变化,提高了代码的可靠性和可维护性。

不可变性final 类通常用于表示不可变的类,即其实例的状态不可被修改。这有助于确保类的安全性和一致性,特别是在多线程环境下。

提高性能:编译器可以对 final 类进行优化,因为它不需要考虑类的扩展。这可能会导致更高效的代码生成。

安全性final 类通常用于关键类,如字符串、基本数据类型的包装类、不可变集合类等,以确保它们的行为不会被子类修改,从而提高程序的安全性和稳定性。

需要注意的是,虽然类被声明为 final,但类中的字段和方法是否可以被继承类或子类覆盖(final 方法和字段)仍然取决于这些字段和方法自身是否被声明为 finalfinal 修饰的类仍然可以创建多个实例对象。

1.抽象类的作用是什么样的?
抽取共性时,无法确定方法体,就把方法定义为抽象的,强制让子类按照某种格式重写抽象方法所在的类,必须是抽象类。
2.抽象类和抽象方法的格式?
public abstract 返回值类型 方法名(参数列表),
public abstract class 类名{
3.继承抽象类有哪些要注意?
要么重写抽象类中的所有抽象方法
要么是抽象类

接口和类之间的关系
类和类的关系
继承关系,只能单继承,不能多继承,但是可以多层继承类和接口的关系
实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口接口和接口的关系
继承关系,可以单继承,也可以多继承

浅克隆和深克隆

浅克隆:object中的clone是浅克隆

  1. 重写object中的clone方法
  2. 让javabean类实现Cloneable接口
  3. 创建原对象并调用clone就可以了。

深克隆:重写clone方法,或者使用第三方工具gson

可变参数(Varargs)

Java 中的一种特殊语法,允许方法接受数量可变的参数。在方法声明中,使用三个连续的点号(...)来表示可变参数,这个参数会被视为数组,在方法内部可以像操作数组一样访问。

以下是可变参数的特点和用法:

  1. 特点

    • 可变参数允许方法接受任意数量的参数,包括零个或多个参数。
    • 可变参数必须是方法参数列表中的最后一个参数。
    • 方法中最多只能有一个可变参数。
    • 可变参数实际上是一个数组,在方法内部可以像操作数组一样进行操作。
  2. 语法

    • 在方法声明中使用三个连续的点号(...)来表示可变参数,例如:void methodName(Type... variableName)
    • 调用可变参数的方法时,可以传入任意数量的参数,这些参数会被自动封装为一个数组。

在方法中,只能写一个可变参数,如果还有其他形参,可变参数要写在最后

正则表达式

正则表达式是一种用于描述字符串模式的工具,它可以用来匹配、搜索和替换文本中的字符串。正则表达式由普通字符和元字符组成,它们组合在一起可以形成一个模式,用来匹配目标字符串。

作用:校验字符串是否满足规则或者在一段文本中查找满足要求的内容。 

在 Java 中,可以使用 java.util.regex 包中的 PatternMatcher 类来使用正则表达式。IDEA有一个any-rule插件,包含常用正则表达式。

       
        String text = "Hello, world! This is a test string.";
        String pattern = "world";
        //创建模式对象[即正则表达式对象]
        Pattern p = Pattern.compile(pattern);
        // 创建匹配器,创建匹配器 matcher, 按照 正则表达式的规则 去匹配 text 字符串
        Matcher m = p.matcher(text);
        //4.开始匹配
        if (m.find()) {
            System.out.println("Found: " + m.group());
        } else {
            System.out.println("Not found.");
        }


String regex="\\d{0,16}";
"text 01".matches(regex);

转义字符: 使用反斜杠 \ 来转义元字符,使其失去特殊含义,匹配字面值。在Java中\\表示一个\。

元字符

  • .:匹配任意单个字符,除了换行符。
  • ^:匹配字符串的开头。
  • $:匹配字符串的结尾。
  • *:匹配前一个元素零次或多次。
  • +:匹配前一个元素一次或多次。
  • ?:匹配前一个元素零次或一次。
  • []:字符类,匹配括号内的任意一个字符。
  • |:或,用于在模式中指定多个可选项。
  • ():分组,用于将多个元素组合在一起。

@Pattern 注解的使用

@Pattern 注解可以与其他验证注解一起使用,例如 @NotBlank@Size 等,以确保字段不仅符合正则表达式,还满足其他约束条件。

@Pattern 注解可以与其他验证注解一起使用,例如 @NotBlank@Size 等,以确保字段不仅符合正则表达式,还满足其他约束条件。

常用属性
  • regexp:指定要匹配的正则表达式。
  • flags:指定匹配时使用的标志,例如 Pattern.CASE_INSENSITIVE 表示忽略大小写。
  • message:指定验证失败时的错误消息。

Lambda 

在 lambda 表达式内部,它是通过函数接口的抽象方法来调用方法的。当编写一个 lambda 表达式时,编译器会根据 lambda 表达式中的参数、方法体等信息,以及 lambda 表达式所在的上下文,推断出 lambda 表达式对应的函数接口类型。然后,编译器会将 lambda 表达式转换为该函数接口的实例。因此,lambda 表达式中调用的方法的具体实现,是在 lambda 表达式被创建的上下文中确定的。

1、Lambda表达式的基本作用?
简化函数式接口的匿名内部类的写法,

2、Lambda表达式有什么使用前提?
必须是接口的匿名内部类,接口中只能有一个抽象方法

3、Lambda的好处?
Lambda是一个匿名函数,我们可以把Lambda表达式理解为是一段可以传递的代码,它可以写出更简洁、更灵活的代码,作为一种更紧凑的代码风格,使Java语言表达能力得到了提升, 

1ambda的省略规则:
1.参数类型可以省略不写。
2.如果只有一个参数,参数类型可以省略,同时()也可以省略。
3.如果Lambda表达式的方法体只有一行,大括号,分号,return可以省略不写,需要同时省略。

泛型

<?> 无限制通配符

<? extends E> extends 关键字声明了类型的上界,表示参数化的类型可能是所指定的类型,或者是此类型的子类

<? super E> super 关键字声明了类型的下界,表示参数化的类型可能是指定的类型,或者是此类型的父类

异常

JVM默认的处理方式:
把异常的名称,异常原因及异常出现的位置等信息输出在了控制台程序停止执行,下面的代码不会再执行了

自己捕捉异常

  • 当try没有捕获到异常时:try语句块中的语句逐一被执行,程序将跳过catch语句块,执行finally语句块和其后的语句;
  • 当try捕获到异常,catch语句块里没有处理此异常的情况:当try语句块里的某条语句出现异常时,而没有处理此异常的catch语句块时,此异常将会抛给JVM处理,finally语句块里的语句还是会被执行,但finally语句块后的语句不会被执行;
  • 当try捕获到异常,catch语句块里有处理此异常的情况:在try语句块中是按照顺序来执行的,当执行到某一条语句出现异常时,程序将跳到catch语句块,并与catch语句块逐一匹配,找到与之对应的处理程序,其他的catch语句块将不会被执行,而try语句块中,出现异常之后的语句也不会被执行,catch语句块执行完后,执行finally语句块里的语句,最后执行finally语句块后的语句;

若是父类的方法没有声明异常,则子类继承方法后,也不能声明异常。

  • try – 用于监听。将要被监听的代码(可能抛出异常的代码)放在try语句块之内,当try语句块内发生异常时,异常就被抛出。
  • catch – 用于捕获异常。catch用来捕获try语句块中发生的异常。
  • finallyfinally语句块总是会被执行。它主要用于回收在try块里打开的物力资源(如数据库连接、网络连接和磁盘文件)。只有finally块,执行完成之后,才会回来执行try或者catch块中的return或者throw语句,如果finally中使用了return或者throw等终止方法的语句,则就不会跳回执行,直接停止。
  • throw – 用于抛出异常。写在方法内,结束方法。手动抛出异常对象,交给调用者。方法中下面的代码不再执行了。
  • throws – 用在方法签名中,用于声明该方法可能抛出的异常。写在方法定义处,表示声明一个异常。告诉调用者,使用本方法可能会有哪些异常。

try-finally可用在不需要捕获异常的代码,可以保证资源在使用后被关闭。例如IO流中执行完相应操作后,关闭相应资源;使用Lock对象保证线程同步,通过finally可以保证锁会被释放;数据库连接代码时,关闭连接操作等等。

finally遇见如下情况不会执行

  • 在前面的代码中用了System.exit()退出程序。
  • finally语句块中发生了异常。
  • 程序所在的线程死亡。
  • 关闭CPU。
  • 如果你的资源实现了 AutoCloseable 接口,你可以使用这个语法。大多数的 Java 标准资源都继承了这个接口。当你在 try 子句中打开资源,资源会在 try 代码块执行后或异常处理后自动关闭。
    try 代码块退出时,会自动调用 scanner.close 方法,和把 scanner.close 方法放在 finally 代码块中不同的是,若 scanner.close 抛出异常,则会被抑制,抛出的仍然为原始异常。被抑制的异常会由 addSusppressed 方法添加到原来的异常,如果想要获取被抑制的异常列表,可以调用 getSuppressed 方法来获取。
  • 著作权归@pdai所有 原文链接:https://pdai.tech/md/java/basic/java-basic-x-exception.html

如果在 catch 块中出现异常,这个异常会被抛出,然后继续执行 finally 块中的代码,最终该异常会被抛出到调用栈的上一级。这意味着 finally 块中的代码仍然会被执行,即使在 catch 块中出现了异常。

        

public class Main {
    public static void main(String[] args) {
        try {
            System.out.println(testMethod());
        } catch (Exception e) {
            System.out.println("Caught exception in main: " + e.getMessage());
        }
    }
    
    public static int testMethod() {
        try {
            System.out.println("In try block");
            throw new RuntimeException("Exception in try block");
        } catch (Exception e) {
            System.out.println("Caught exception in catch: " + e.getMessage());
            throw new RuntimeException("Exception in catch block");
        } finally {
            System.out.println("In finally block");
        }
    }
}


In try block
Caught exception in catch: Exception in try block
Exception in thread "main" java.lang.RuntimeException: Exception in catch block
	at Main.testMethod(Main.java:14)
	at Main.main(Main.java:4)

如果在 catch 块中出现异常,并且该异常没有被捕获(即没有被 try-catch 或者 throws 子句捕获),那么该异常会被抛出到调用栈的上一级。这个过程会一直持续,直到有合适的地方捕获该异常或者该异常抛出到顶层,导致程序终止。

异常表中包含了一个或多个异常处理者(Exception Handler)的信息,这些信息包含如下

  • from 可能发生异常的起始点
  • to 可能发生异常的结束点
  • target 上述from和to之前发生异常后的异常处理者的位置
  • type 异常处理者处理的异常的类信息

当一个异常发生时

  • 1.JVM会在当前出现异常的方法中,查找异常表,是否有合适的处理者来处理
  • 2.如果当前方法异常表不为空,并且异常符合处理者的from和to节点,并且type也匹配,则JVM调用位于target的调用者来处理。
  • 3.如果上一条未找到合理的处理者,则继续查找异常表中的剩余条目
  • 4.如果当前方法的异常表无法处理,则向上查找(弹栈处理)刚刚调用该方法的调用处,并重复上面的操作。
  • 5.如果所有的栈帧被弹出,仍然没有处理,则抛给当前的Thread,Thread则会终止。
  • 6.如果当前Thread为最后一个非守护线程,且未处理异常,则会导致JVM终止运行。

著作权归@pdai所有 原文链接:https://pdai.tech/md/java/basic/java-basic-x-exception.html

javap 工具用于查看编译后的类文件,它并不会反编译源代码。因此,输出的内容是类的结构和字节码指令,需要理解 Java 字节码的格式和含义。

例如,要查看一个名为 Test.class 的类文件的字节码内容,可以使用以下命令:

javap -c Test
  • -c:打印类的字节码指令。
  • -s:打印内部类型签名。
  • -verbose:显示类的详细信息,包括版本、常量池、访问标志等。
  • -private:显示私有成员。
  • -classpath-cp:指定类路径。

反射

反射允许对成员变量,成员方法和构造方法的信息进行编程访问。

反射是一种在运行时动态获取类的信息、操作类和对象的能力。在 Java 中,反射机制允许程序在运行时获取类的属性、方法、构造函数等信息,并且可以动态调用对象的方法、访问对象的属性,甚至可以动态创建对象实例。

Java 的反射机制主要围绕着以下几个核心类展开:

  1. Class 类:Class 类是 Java 反射机制的核心类之一,它代表着 Java 类的实例。通过 Class 类可以获取类的各种信息,如类的名称、父类、接口、方法、字段等。

  2. Constructor 类:Constructor 类表示类的构造函数。通过 Constructor 类可以动态创建类的对象实例。

  3. Method 类:Method 类表示类的方法。通过 Method 类可以动态调用类的方法。

  4. Field 类:Field 类表示类的字段。通过 Field 类可以动态访问和修改类的字段。

class对象是可以说是反射中最常用的,获取class对象的方式的主要有三种

  • 根据全限定类名:Class.forName(全限定类名),源代码阶段
  • 根据类名:类名.class,加载阶段
  • 根据对象:对象.getClass(),运行阶段

动态代理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值