0%

Java基础

前言

  复习Java基础,记录一下一些重要的知识点。

  主要内容来自Github上的JavaGuide,仅作个人学习。

概念常识

  1. 什么是JVM?

    JVM是Java Virtual Machine(Java虚拟机),是运行Java字节码的虚拟机。针对不同系统有不同实现,目的是使用相同的字节码,给出相同的结果。字节码和不同系统的 JVM 实现是 Java 语言“一次编译,随处可以运行”的关键所在。

  2. 什么是字节码?

    JVM可以理解的代码叫字节码(.class文件),它不面向任何特定的处理器,只面向虚拟机。

  3. Java 程序从源代码到运行一般有哪些步骤?

    运行过程

  4. JDK 和 JRE的概念?

    JDK 是 Java Development Kit 缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。

    JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。

  5. 为什么说Java“编译与解释并存”?

    编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, 有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。

    Java 语言既具有编译型语言的特征,也具有解释型语言的特征,因为 Java 程序要经过先编译,后解释两个步骤,由 Java 编写的程序需要先经过编译步骤,生成字节码(\*.class 文件),这种字节码必须由 Java 解释器来解释执行。因此,我们可以认为 Java 语言编译与解释并存。

基础知识

  1. 字符型常量和字符串常量的区别?

    • 形式 : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符
    • 含义 : 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)
    • 占内存大小 : 字符常量只占 2 个字节; 字符串常量占若干个字节 (注意: char 在 Java 中占两个字节)
  2. 标识符和关键字的区别?

    简单来说,标识符就相当于名字。关键字就是具有特殊含义的标识符,已经有了特定的含义,只能用于特定的地方。

  3. 类和对象之间的关系?

    对象是类实例化出来的,对象中含有类的属性,类是对象的抽象。用static修饰的方法是静态的方法或称为共享方法,一般用类名直接调用。

  4. 什么是泛型?

    泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

    Java 的泛型是伪泛型,这是因为 Java 在编译期间,所有的泛型信息都会被擦掉,这也就是通常所说类型擦除

  5. 泛型的使用方式?

    • 泛型类,
    • 泛型接口
    • 泛型方法
  6. 常用的通配符?

    • ? 表示不确定的 java 类型
    • T (type) 表示具体的一个 java 类型
    • K V (key value) 分别代表 java 键值中的 Key Value
    • E (element) 代表 Element
  7. ==和 equals 的区别

    • 对于基本数据类型来说,==比较的是值。对于引用数据类型来说,==比较的是对象的内存地址。

    • 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

    • equals() 作用不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

    • 一般我们都覆盖 equals()方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。

    • ```java
      public class test1 {

      public static void main(String[] args) {
          String a = new String("ab"); // a 为一个引用
          String b = new String("ab"); // b为另一个引用,对象的内容一样
          String aa = "ab"; // 放在常量池中
          String bb = "ab"; // 从常量池中查找
          if (aa == bb) // true
              System.out.println("aa==bb");
          if (a == b) // false,非同一对象
              System.out.println("a==b");
          if (a.equals(b)) // true
              System.out.println("aEQb");
          if (42 == 42.0) { // true
              System.out.println("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
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56

      - `String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。

      - 当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。

      - **敲重点:==比较的是基本数据类型的值,引用类型对象的内存地址;equals比较的是对象是否相等,当调用的equals是被重写的时候,比较的是对象里面的值。**

      8. 什么是hashCode()?

      `hashCode()` 的作用是获取哈希码,也称为散列码;它实际上是返回一个 int 整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。`hashCode()`定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是: `Object` 的 hashcode 方法是本地方法,也就是用 c 语言或 c++ 实现的,该方法通常用来将对象的 内存地址 转换为整数之后返回。

      9. 为什么重写 equals 时必须重写 hashCode 方法??

      如果两个对象相等,则 hashcode 一定也是相同的。两个对象相等,对两个对象分别调用 equals 方法都返回 true。但是,两个对象有相同的 hashcode 值,它们也不一定是相等的 。**因此,equals 方法被覆盖过,则 `hashCode` 方法也必须被覆盖。**

      10. 相同hashCode值,他们不一定相等?

      因为 `hashCode()` 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓碰撞也就是指的是**不同的对象得到相同的 `hashCode`**)。



      ## 基本数据类型

      1. Java中8种基本数据类型有哪些?

      | 基本类型 | 位数 | 字节 | 默认值 |
      | --------- | ---- | ---- | ------- |
      | `int` | 32 | 4 | 0 |
      | `short` | 16 | 2 | 0 |
      | `long` | 64 | 8 | 0L |
      | `byte` | 8 | 1 | 0 |
      | `char` | 16 | 2 | 'u0000' |
      | `float` | 32 | 4 | 0f |
      | `double` | 64 | 8 | 0d |
      | `boolean` | 1 | | false |

      - Java 里使用 `long` 类型的数据一定要在数值后面加上 **L**,否则将作为整型解析。

      - `char a = 'h'`char :单引号,`String a = "hello"` :双引号。

      - 这八种基本类型都有对应的包装类分别为:`Byte`、`Short`、`Integer`、`Long`、`Float`、`Double`、`Character`、`Boolean` 。

      - 包装类型不赋值就是 `Null` ,而基本类型有默认值且不是 `Null`。

      2. 什么是自动装箱和拆箱?

      - **装箱**:将基本类型用它们对应的引用类型包装起来;

      - **拆箱**:将包装类型转换为基本数据类型;

      - ```java
      Integer i = 10; //装箱
      int n = i; //拆箱

      //Integer i = 10 等价于 Integer i = Integer.valueOf(10)
      //int n = i 等价于 int n = i.intValue();
  8. 8 种基本类型的包装类和常量池?

    Java 基本类型的包装类的大部分都实现了常量池技术。Byte,Short,Integer,Long 这 4 种包装类默认创建了数值 [-128,127] 的相应类型的缓存数据,Character 创建了数值在[0,127]范围的缓存数据,Boolean 直接返回 True Or False

    两种浮点数类型的包装类 Float,Double 并没有实现常量池技术。

  9. 整型包装类对象之间值的比较?

    img

  10. 什么是引用数据类型?有哪些?

    引用类型在堆里,基本类型在栈里。栈空间小且连续,往往会被放在缓存。引用类型cache miss率高且要多一次解引用。对象还要再多储存一个对象头,对基本数据类型来说空间浪费率太高

    引用数据类型主要包括:类,接口,数组

常见关键字

  1. final

    final 关键字,意思是最终的、不可修改的,最见不得变化 ,用来修饰类、方法和变量,具有以下特点:

    1. final 修饰的类不能被继承,final 类中的所有成员方法都会被隐式的指定为 final 方法;
    2. final 修饰的方法不能被重写;
    3. final 修饰的变量是常量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能让其指向另一个对象。
  2. static

    1. 修饰成员变量和成员方法:被 static 修饰的成员属于类,不属于单个这个类的某个对象,被类中所有对象共享,可以并且建议通过类名调用。被 static 声明的成员变量属于静态成员变量,静态变量 存放在 Java 内存区域的方法区。
    2. 静态代码块
    3. 修饰类(只能修饰内部类)
    4. 静态导包(用来导入类中的静态资源,1.5 之后的新特性)
  3. this

    this 关键字用于引用类的当前实例

  4. super

    super 关键字用于从子类访问父类的变量和方法

  5. this和super使用注意?

    • 在构造器中使用 super() 调用父类中的其他构造方法时,该语句必须处于构造器的首行,否则编译器会报错。另外,this 调用本类中的其他构造方法时,也要放在首行。
    • this、super 不能用在 static 方法中。 this 和 super 是属于对象范畴的东西,而静态方法是属于类范畴的东西

方法

  1. 在一个静态方法内调用一个非静态成员为什么是非法的?

    静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,然后通过类的实例对象去访问。在类的非静态成员不存在的时候静态成员就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。

  2. 静态方法与实例化方法的区别?

    • 调用方式不同
      • 在外部调用静态方法时,可以使用 类名.方法名 的方式,也可以使用 对象.方法名 的方式,而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象
    • 访问类成员是否存在限制
      • 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。
  3. Java中为什么只有值传递?

    Java 程序设计语言总是采用按值调用。也就是说,方法得到的是所有参数值的一个拷贝,也就是说,方法不能修改传递给它的任何参数变量的内容

  4. 方法参数的使用情况?

    • 一个方法不能修改一个基本数据类型的参数(即数值型或布尔型)。
    • 一个方法可以改变一个对象参数的状态。
    • 一个方法不能让对象参数引用一个新的对象。
  5. 重载和重写的区别?

    重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理

    重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法

    区别点 重载方法 重写方法
    发生范围 同一个类 子类
    参数列表 必须修改 一定不能修改
    返回类型 可修改 子类方法返回值类型应比父类方法返回值类型更小或相等
    异常 可修改 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等;
    访问修饰符 可修改 一定不能做更严格的限制(可以降低限制)
    发生阶段 编译期 运行期
  6. 深拷贝 vs 浅拷贝

    1. 浅拷贝:对基本数据类型进行值传递,对引用数据类型进行引用传递般的拷贝,此为浅拷贝。
    2. 深拷贝:对基本数据类型进行值传递,对引用数据类型,创建一个新的对象,并复制其内容,此为深拷贝。

面向对象

  1. 面向对象和面向过程的区别?

    • 面向过程面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,所以当性能是最重要的考量因素的时候,比如单片机、嵌入式开发、Linux/Unix 等一般采用面向过程开发。但是,面向过程没有面向对象易维护、易复用、易扩展。
    • 面向对象面向对象易维护、易复用、易扩展。 因为面向对象有封装、继承、多态性的特性,所以可以设计出低耦合的系统,使系统更加灵活、更加易于维护。但是,面向对象性能比面向过程低
  2. 成员变量与局部变量的区别有哪些?

    1. 从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
    2. 从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
    3. 从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
    4. 从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。
  3. 对象实体与对象引用的区别?

    new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中)。

    一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球);一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。

  4. 对象相等和指向他们的引向相等有何不同?

    对象的相等,比的是内存中存放的内容是否相等。而引用相等,比较的是他们指向的内存地址是否相等。

  5. 构造方法是否可以被重写?

    构造方法不可以被重写,但是可以重载。

  6. 面向对象的三大特征?

    • 封装 :封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。
    • 继承:继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。
      • 关于继承如下 3 点请记住:
        1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
        2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
        3. 子类可以用自己的方式实现父类的方法。
    • 多态:对于一个方法,我们有不同的操作方式。
      • 多态的特点:
        • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
        • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
        • 多态不能调用“只在子类存在但在父类不存在”的方法;
        • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。
  7. String StringBuffer 和 StringBuilder 的区别是什么?

    1. 可变性方面
      • string由final修饰,不可变
      • StringBuilder 和 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。
    2. 线程安全方面
      • string 中的对象是不可变的,也就可以理解为常量,线程安全。
      • StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。
      • StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。
    3. 性能
      • 每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
    4. 使用总结
      • 操作少量的数据: 适用 String
      • 单线程操作字符串缓冲区下操作大量数据: 适用 StringBuilder
      • 多线程操作字符串缓冲区下操作大量数据: 适用 StringBuffer

反射

  1. 什么是反射?

    Java 反射,就是在运行状态中。

    • 获取任意类的名称、package信息、所有属性、方法、注解、类型、类加载器等
    • 获取任意对象的属性,并且能改变对象的属性
    • 调用任意对象的方法
    • 判断任意一个对象所属的类
    • 实例化任意一个类的对象
  2. 反射机制的优缺点?

    • 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
    • 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。

异常

  1. 异常层次结构图?

    Java异常类层次结构图

    在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。Exception 能被程序本身处理(try-catch), Error 是无法处理的(只能尽量避免)。

    ExceptionError 二者都是 Java 异常处理的重要子类,各自都包含大量子类。

    • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获。Exception 又可以分为 受检查异常(必须处理) 和 不受检查异常(可以不处理)。
    • ErrorError 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,Java 虚拟机运行错误(Virtual MachineError)、虚拟机内存不够错误(OutOfMemoryError)、类定义错误(NoClassDefFoundError)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。
  2. 受检查异常和不受检查异常?

    Java异常类层次结构图2

    • 不受检查异常主要是RuntimeException及其子类;Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。
    • 除了RuntimeException及其子类以外,其他的Exception类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、ClassNotFoundExceptionSQLException…。Java 代码在编译过程中,如果受检查异常没有被 catch/throw 处理的话,就没办法通过编译 。
  3. Throwable 类常用方法?

    • public string getMessage():返回异常发生时的简要描述
    • public string toString():返回异常发生时的详细信息
    • public string getLocalizedMessage():返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
    • public void printStackTrace():在控制台上打印 Throwable 对象封装的异常信息
  4. try-catch-finally?

    • try块: 用于捕获异常。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块。
    • catch块: 用于处理 try 捕获到的异常。
    • finally 块: 无论是否捕获或处理异常,finally 块里的语句都会被执行。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。
      • 在以下 3 种特殊情况下,finally 块不会被执行:
        1. tryfinally块中用了 System.exit(int)退出程序。但是,如果 System.exit(int) 在异常语句之后,finally 还是会被执行
        2. 程序所在的线程死亡。
        3. 关闭 CPU。
  5. try-with-resourse(java7新增)?

    1. 适用范围(资源的定义): 任何实现 java.lang.AutoCloseable或者 java.io.Closeable 的对象
    2. 关闭资源和 finally 块的执行顺序:try-with-resources 语句中,任何 catch 或 finally 块在声明的资源关闭后运行

I/O流

  1. 什么是序列化?什么是反序列化?

    • 序列化: 将数据结构或对象转换成二进制字节流的过程
    • 反序列化:将在序列化过程中所生成的二进制字节流的过程转换成数据结构或者对象的过程
    • 综上:序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
  2. 有些字段不想进行序列化,怎么解决?

    使用 transient 关键字修饰。transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。

    关于 transient 还有几点注意:

    • transient 只能修饰变量,不能修饰类和方法。
    • transient 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 int 类型,那么反序列后结果就是 0
    • static 变量因为不属于任何对象(Object),所以无论有没有 transient 关键字修饰,均不会被序列化。
  3. Java中键盘输入的方式?

    方法 1:通过 Scanner

    1
    2
    3
    Scanner input = new Scanner(System.in);
    String s = input.nextLine();
    input.close();Copy to clipboardErrorCopied

    方法 2:通过 BufferedReader

    1
    2
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    String s = input.readLine();
  4. IO流的种类?

    IO-操作方式分类

  5. 字节流和字符流?

    字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。

疑难点

  1. equals的使用?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 不能使用一个值为null的引用类型变量来调用非静态方法,否则会抛出异常
    String str = null;
    if (str.equals("SnailClimb")) {
    ...
    } else {
    ..
    }

    //解决
    //1."SnailClimb".equals(str);// false
    //2.Objects.equals(null,"SnailClimb");// false
  2. BigDecimsl的作用?

    来定义浮点数的值,再进行浮点数的运算操作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    BigDecimal a = new BigDecimal("1.0");
    BigDecimal b = new BigDecimal("0.9");
    BigDecimal c = new BigDecimal("0.8");

    BigDecimal x = a.subtract(b);
    BigDecimal y = b.subtract(c);

    System.out.println(x); /* 0.1 */
    System.out.println(y); /* 0.1 */
    System.out.println(Objects.equals(x, y)); /* true */
  3. BigDecimal 的使用注意事项

    注意:我们在使用BigDecimal时,为了防止精度丢失,推荐使用它的 BigDecimal(String) 构造方法来创建对象。《阿里巴巴Java开发手册》对这部分内容也有提到如下图所示。

    《阿里巴巴Java开发手册》对这部分BigDecimal的描述

  4. 基本数据类型与包装数据类型的使用标准?

    Reference:《阿里巴巴Java开发手册》

    • 【强制】所有的 POJO 类属性必须使用包装数据类型。
    • 【强制】RPC 方法的返回值和参数必须使用包装数据类型。 (rpc 远程过程调用)
    • 【推荐】所有的局部变量使用基本数据类型。

参考资料

https://github.com/Snailclimb/JavaGuide

-------------本文结束感谢您的阅读-------------
您的支持将成为我创作的动力!