Skip to content

java 基础

java的基本数据类型

Java 中有 8 种基本数据类型,分别为:

  • 6 种数字类型:

    • 4 种整数型:byteshortintlong

    • 2 种浮点型:floatdouble

  • 1 种字符类型:char

  • 1 种布尔型:boolean

这 8 种基本数据类型的默认值以及所占空间的大小如下:

基本类型和包装类型的区别?

用途:基本类型用来定义一些常量和局部变量,方法参数、对象属性。

包装类型可用于泛型,而基本类型不可以。

存储方式:基本数据类型的局部变量存放在 Java 虚拟机栈中的局部变量表中,基本数据类型的成员变量(未被 static 修饰 )存放在 Java 虚拟机的堆中。

包装类型属于对象类型,存在于堆中。

占用空间:相比于包装类型(对象类型), 基本数据类型占用的空间往往非常小。

默认值:成员变量中:包装类型不赋值就是 null ,而基本类型有默认值且不是 null

比较方式:对于基本数据类型来说,== 比较的是值。对于包装数据类型来说,== 比较的是对象的内存地址。所有整型包装类对象之间值的比较,全部使用 equals() 方法。

成员变量与局部变量的区别?

语法形式:成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;

成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。

存储方式:如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。

生存时间:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。

默认值:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

Java 只有值传递

  • 传递的值在栈中,直接拷贝一份值传递,改变的形参不会对实参造成影响

  • 传递的值在栈中存放的是地址(引用),把地址拷贝一份(拷贝的地址是一个值),此时形参和实参指向堆上同一个地址,形参的修改导致了实参的改变。

重载和重写(overwrite和overload区别)

重载

发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。

重写

是子类对父类的允许访问的方法的实现过程进行重新编写。

  1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。

  2. 如果父类方法访问修饰符为 private/final/static 则子类就不能重写该方法,但是被 static 修饰的方法能够被再次声明。

  3. 构造方法无法被重写

抽象类和接口的区别

  1. 定义:定义的关键字不同,抽象类是 abstract,而接口是 interface。

  2. 包含方法:抽象类可以包含抽象方法和普通方法,而接口只能包含抽象方法。

  3. 方法访问控制符:抽象类无限制,只是抽象类中的抽象方法不能被 private 修饰;而接口有限制,接口默认的是 public 控制符。

  4. 实现:一个类只能继承一个抽象类,但可以实现多个接口。

  5. 变量:抽象类可以包含实例变量和静态变量,而接口只能包含常量。

  6. 构造函数:抽象类可以有构造函数,而接口不能有构造函数。

面向对象的三大特征

封装

封装是指把一个对象的 属性 隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。

继承

继承是使用已存在的类的定义作为基础建立新类的技术,子类可以增加新的属性和方法,也可以用父类的方法。

关于继承如下 3 点请记住:

  • 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。

  • 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。

  • 子类可以用自己的方式实现父类的方法

多态

表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例,并在运行时根据实际对象的类型来确定调用哪个方法

实现原理:

1 动态绑定

动态绑定(Dynamic Binding):指的是在编译时,Java 编译器只能知道变量的声明类型,而无法确定其实际的对象类型。而在运行时,Java 虚拟机(JVM)会通过动态绑定来解析实际对象的类型。

2 虚拟方法调用

虚拟方法调用(Virtual Method Invocation):在 Java 中,所有的非私有、非静态和非 final 方法都是被隐式地指定为虚拟方法。虚拟方法调用是在运行时根据实际对象的类型来确定要调用的方法的机制。当通过父类类型的引用变量调用被子类重写的方法时,虚拟机会根据实际对象的类型来确定要调用的方法版本,而不是根据引用变量的声明类型。

3 实现原理综述

所以,多态的实现原理主要是依靠“动态绑定”和“虚拟方法调用”,它的实现流程如下:

  1. 创建父类类型的引用变量,并将其赋值为子类对象。

  2. 在运行时,通过动态绑定确定引用变量所指向的实际对象的类型。

  3. 根据实际对象的类型,调用相应的方法版本。

深拷贝和浅拷贝区别了解吗?什么是引用拷贝?

用来复制对象的两种方式

  • 浅拷贝():浅拷贝会复制对象的基本数据类型属性的值;对于引用类型属性,只复制引用地址,因此原对象和拷贝对象指向同一块内存区域。

实现方式:让类实现Cloneable 接口,并且重写 clone 方法。

  • 深拷贝:深拷贝会完全复制整个对象,包括这个对象所包含的内部对象,确保每个引用类型属性都分配到新的内存地址。

实现方式:重写 clone() 方法,并在其中调用嵌套对象的 clone() 方法,或使用序列化方式实现深拷贝。

  • 引用拷贝:两个不同的引用指向同一个对象。

同步调用和异步调用

  • 同步调用

是最基本的调用方式,对象A的方法直接调用对象B的方法,这个时候程序会等待对象B的方法执行完返回结果之后,才会继续往下进行,退回到对象A的方法, 执行后续代码。

  • 异步调用

对象A的方法调用对象B的方法,程序并不需要等待对象B的方法返回结果值,对象A的方法直接继续往下执行。

异步线程可通过Java的多线程机制来实现。

@Async的基本使用

这个注解的作用在于可以让被标注的方法异步执行,但是有两个前提条件

  1. 启动类上添加@EnableAsync注解

  2. 需要异步执行的方法的所在类由Spring管理

  3. 需要异步执行的方法上添加了@Async注解

  • 回调

对象A的methodA()方法调用对象B的methodB()方法,在对象B的methodB()方法中反过来调用对象A的callBack()方法,这个callBack()方法称为回调函数,这种调用方法称为回调。

== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。

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

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。

equals()方法存在于Object类中,而Object类是所有类的直接或间接父类,因此所有的类都有equals()方法。

equals() 方法存在两种使用情况:

类没有重写 equals()方法:等价于通过“==”比较这两个对象,比较的是对象的内存地址。

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

只重写 equals 没重写 hashcode,map put 的时候会发生什么?

如果只重写 equals 方法,没有重写 hashcode 方法,那么会导致 equals 相等的两个对象,hashcode 不相等,这样的话,这两个对象会被放到不同的桶中,这样就会导致 get 的时候,找不到对应的值

Lambda底层原理

  1. 在程序运行时,会在类中生成一个匿名内部类,匿名内部类会实现接口,并重写接口中的那个唯一抽象方法。

  2. 类中会生成一个静态方法,静态方法中的代码就是 Lambda 表达式中的代码。

  3. 匿名内部类重写的抽象方法,会调用上一步的静态方法,从而实现 Lambda 代码的执行。

BIO、NIO、AIO

  1. BIO(blocking I/O) : 就是传统的 IO,同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理。

BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4 以前的唯一选择,程序简单易理解。

  • NIO :全称 java non-blocking IO,被统称为 NIO(即 New IO)。

    NIO 是同步非阻塞的( I/O 多路复用模型),服务器可以用一个线程处理多个客户端连接,通过 Selector 监听多个 Channel 来实现多路复用,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有 IO 请求就进行处理:

  • AIO:是异步非阻塞的 IO。所有的IO操作都是异步的,不会阻塞任何线程,可以更好地利用系统资源。当有事件触发时,服务器端得到通知,进行相应的处理,完成后自动调用回调函数,通知服务端程序启动线程进行后续处理,一般适用于连接数较多且连接时间较长的应用

异常

Exception 类代表程序可以处理的异常。它分为两大类:编译时异常(Checked Exception)和运行时异常(Runtime Exception)。

①、编译时异常(Checked Exception):这类异常在编译时必须被显式处理(捕获或声明抛出)。

如果方法可能抛出某种编译时异常,但没有捕获它(try-catch)或没有在方法声明中用 throws 子句声明它,那么编译将不会通过。例如:IOException、SQLException、InterruptedException 等。

代表方法有:sleep(),wait(),join()

②、运行时异常(Runtime Exception):这类异常在运行时抛出,它们都是 RuntimeException 的子类。对于运行时异常,Java 编译器不要求必须处理它们(即不需要捕获也不需要声明抛出)。

运行时异常通常是由程序逻辑错误导致的,如 NullPointerException、IndexOutOfBoundsException 等。

  1. 只要异常被处理,异常处理之后的代码都可以正常执行。

  2. 异常被往上抛出,则抛出异常之后的代码将不被执行。

运行结果:

运行结果

运行结果

在项目中,我遇到最多的就是 NullPointerException,通常这是由于调用了一个为空(null)的对象的方法或属性所导致的。处理这类异常一般是通过提前做非null的判断来避免。 另外,我在操作数据库时,也会经常遇到SQLException,处理这种异常通常是捕获异常并抛出自己定义的异常,然后在上层统一处理,记录日志,返回合适的错误信息给用户。 在处理异常的时候,我的一个原则是尽量不要吞掉异常。如果捕获了异常但是没有进行处理(比如只是简单地打印出堆栈信息,然后没有进行任何操作),这可能会掩盖掉系统的真实问题,导致在出现问题时,无法追踪到异常的发生源。 所以我们在处理异常的时候,除了对异常进行适当的处理之外,还需要将异常通过日志记录下来,以便在需要的时候进行问题的追踪和定位。

JVM

JVM 的内存区域

JVM 的内存区域可以细分为程序计数器虚拟机栈本地方法栈方法区等。

其中方法区是线程共享区。虚拟机栈本地方法栈程序计数器是线程私有的。

  • JDK1.7 时将字符串常量池、静态变量,存放在堆上

  • 在 JDK1.8 直接内存中划出一块区域作为元空间,运行时常量池移动到元空间。

类加载器

类加载器(Class Loader)是 Java 虚拟机(JVM)的重要组成部分,负责将字节码文件加载到内存中并转换为可执行的类。

类加载总共分为以下四种:

  1. 启动类加载器(Bootstrap Class Loader):它是 JVM 的内部组件,负责加载 Java 核心类库(如java.lang)和其他被系统类加载器所需要的类。启动类加载器是由 JVM 实现提供的,通常使用本地代码来实现。

  2. 扩展类加载器(Extension Class Loader):它是 sun.misc.Launcher$ExtClassLoader 类的实例,负责加载 Java 的扩展类库(如 java.util、java.net)等。扩展类加载器通常从 java.ext.dirs 系统属性所指定的目录或 JDK 的扩展目录中加载类。

  3. 系统类加载器(System Class Loader):也称为应用类加载器(Application Class Loader),它是sun.misc.Launcher$AppClassLoader 类的实例,负责加载应用程序的类。系统类加载器通常从 CLASSPATH 环境变量所指定的目录或 JVM 的类路径中加载类。

  4. 用户自定义类加载器(User-defined Class Loader):这是开发人员根据需要自己实现的类加载器。用户自定义类加载器可以根据特定的加载策略和需求来加载类,例如从特定的网络位置、数据库或其他非传统来源加载类。


类加载机制

Java虚拟机的类加载机制是指在程序运行期间,将字节码(.class)文件加载到内存,然后对数据进行校验、解析和初始化,最终形成可以被Java虚拟机直接使用的Java类型。

Java的类加载过程主要包括以下步骤:

  1. 加载(Loading):在Java堆中生成一个代表这个类的java.lang.Class对象,作为对这个类的数据访问接口。

• 通过一个类的全限定名来获取定义此类的二进制字节流。

• 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  • 链接(Linking):链接包括验证、准备和解析三个阶段:

    • 验证:确认被加载的类符合Java虚拟机规范,没有安全问题。

    • 准备:为类中定义的静态变量分配内存并设置初始值。(这些变量所使用的内存都将在方法区进行分配)

    • 解析:将常量池内的符号引用替换为直接引用

  • 初始化(Initialization):执行类的初始化语句,为类变量赋正确的初始值。

类加载机制的特性:

  • 双亲委派模型:如果一个类加载器收到了类加载请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父类加载器反馈自己无法完成这个加载请求时(即父加载器搜索范围中没有找到该类),子加载器才会尝试自己去加载。

这种机制的好处包括:

  • 保证安全性:通过双亲委派机制,可以防止恶意类的加载和执行。

  • 避免重复加载:确保同一个类只被加载一次,节省内存空间。

  • 保证类的唯一性:在Java虚拟机中,一个类由其全限定类名和其类加载器共同确定其唯一性。

符号引用和直接引用

  1. 符号引用(Symbolic Reference)

符号引用是源代码中使用的抽象符号,它们并不直接指向内存中的具体地址,而是通过一些符号表示程序元素,比如类名、字段名、方法名等。符号引用是在编译时生成的,并保存在类文件的常量池中,常见的符号引用包括:

  • 类或接口的全限定名(例如 java/lang/Object

  • 字段名称和描述符(例如 java/lang/System.out

  • 方法名称和描述符(例如 java/lang/Object.toString:()Ljava/lang/String;

符号引用的特点:

  • 间接性:符号引用不会直接指向内存中的具体对象或方法地址。

  • 独立性:符号引用不依赖于运行时内存布局,因此可以在不同的运行时环境下加载。

符号引用的解析是在类加载阶段或运行时进行的,JVM通过类加载器将符号引用解析为具体的内存地址。

  • 直接引用(Direct Reference)

直接引用是运行时真正指向内存中具体对象或方法的指针、偏移量或其他直接地址引用。一旦符号引用被解析成直接引用,JVM可以通过直接引用访问对应的对象、字段或方法。

直接引用的特点:

  • 效率高:直接引用可以快速定位内存中的对象或方法,避免了符号查找的过程。

  • 依赖内存布局:直接引用通常依赖于运行时的内存布局,因此无法在不同的运行时环境中通用。

直接引用的生成通常是在类加载和字节码执行过程中,符号引用解析为直接引用后,JVM就可以直接使用它进行操作。直接引用可以是指向具体内存地址的指针,或者是某些偏移量等。

对象创建的过程了解吗?

在 JVM 中对象的创建,我们从一个 new 指令开始:

  • 检查类是否已被加载。如果没有,就先执行相应的类加载过程

  • 类加载检查通过后,接下来虚拟机将为新生对象分配内存。

  • 内存分配完成之后,虚拟机将分配到的内存空间(但不包括对象头)都初始化为零值。

  • 接下来设置对象头,其中包含了对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的 GC 分代年龄等信息。

这个过程大概图示如下:

对象创建过程

内存泄露和内存溢出

定义不同

  1. 内存溢出指的是在程序运行过程中申请的内存超出了可用内存资源的情况,导致无法继续分配所需的内存,从而引发异常。内存溢出会导致程序抛出 OutOfMemoryError 异常,程序无法继续执行。

  2. 内存泄漏指的是在程序中无意中保留了不再需要的对象引用,导致这些对象无法被垃圾回收机制回收,进而占用了不必要的内存空间。

产生原因不同

  1. 内存溢出通常是由于程序运行时需要的内存超过了可用的内存资源,或者是存在大量占用内存的对象无法被及时释放。包括创建过多的对象、递归调用导致栈溢出等。

内存泄漏则会导致内存资源的浪费,长时间运行下会导致可用内存逐渐减少,最终可能导致内存溢出。

  • 内存泄漏则是由于程序中存在不正确的对象引用管理,例如对象被误持有引用、缓存未清理等。

解决方案不同

  1. 对于内存溢出,可以通过增加可用内存、调整程序逻辑、优化资源使用等方式来解决。

  2. 而对于内存泄漏,需要通过检查和修复对象引用管理问题,确保不再使用的对象能够被垃圾回收机制正确释放

JVM调优

设置JVM调优参数

常见的 JVM 调优参数有以下几个:

  • 调整堆内存大小:通过设置 -Xms(初始堆大小)和 -Xmx(最大堆大小)参数来调整堆内存大小,避免频繁的垃圾回收。

  • 选择合适的垃圾回收器:根据应用程序的性能需求和特点,选择合适的垃圾回收器,如 Serial GC、Parallel GC、CMS GC、G1 GC 等。

  • 调整新生代和老年代比:通过设置 -XX:NewRatio 参数来调整新生代和老年代的比例,优化内存分配。

  • 设置合适的堆中的各个区域比例:通过设置 -XX:SurvivorRatio 参数和 -XX:MaxTenuringThreshold 参数来调整 Eden 区、Survivor 区和老年代的比例,避免过早晋升和过多频繁的垃圾回收。

  • 设置对象从年轻代进入老年代的年龄值:-XX:InitialTenuringThreshold=7 表示 7 次年轻代存活的对象就会进入老年代。

  • 设置元空间大小:在 JDK 1.8 版本中,元空间的默认大小会根据操作系统有所不同。具体来说,在 Windows 上,元空间的默认大小为 21MB;而在 Linux 上,其默认大小为 24MB。然而如果元空间不足也有可能触发 Full GC 从而导致程序执行变慢,因此我们可以通过 -XX:MaxMetaspaceSize=size 设置元空间的最大容量。

垃圾回收

在 JDK 7 版本及 JDK 7 版本之前,堆内存被通常分为下面三部分:

  1. 新生代内存(Young Generation)

  2. 老生代(Old Generation)

  3. 永久代(Permanent Generation)

下图所示的 Eden 区、两个 Survivor 区 S0 和 S1 都属于新生代,中间一层属于老年代,最下面一层属于永久代。

垃圾回收机制

  1. 内存分区:Java堆内存分为年轻代(Young Generation)和老年代(Old Generation)。

一般来说,新创建的对象会首先在 Eden 区(属于年轻代),经过多次Young GC后,如果对象还在被引用就会被移到老年代。在老年代的垃圾收集称为 Old GC 或 Full GC,它的速度通常会慢一些。

  • 对象的判断:首先,JVM需要确定哪些对象是“垃圾”,即不再被任何活动线程引用的对象。

    JVM默认采用“可达性分析算法”来判断对象是否存活。

  • 垃圾回收:确定哪些对象是垃圾后,JVM就会在合适的时间进行回收。垃圾收集器在回收对象时,会筛选出那些内存中已经成为垃圾的对象,释放掉它们占用的内存空间,以便这些空间可以被再次使用。

  • 垃圾收集算法:最常见的垃圾收集算法有:标记-清除(Mark-Sweep)、复制(Copying)、标记-整理(Mark-Compact)等。

可达性分析

可达性分析算法:从 根对象(GC Roots)开始,沿着这些根对象的引用链向下搜索。所能到达的对象被认为是存活的,即可达对象。如果一个对象到 GC Roots 没有任何引用链相连,则认为它是不可达的,可能会被回收。

根对象(GC Roots):首先,JVM 定义了一组特殊的对象,称为GC Roots。这些对象包括:

  • 虚拟机栈中引用的对象(如局部变量)

  • 方法区中类的静态引用

  • 方法区中常量引用

  • 本地方法栈中 JNI 的引用

可达性分析本身并不会对系统产生过大的资源消耗,因为 JVM 在实现垃圾回收时,已经对该算法进行了多种优化。。

  1. 主要优化:

    1. 分代回收:JVM 将堆内存分为年轻代、老年代等,根据对象的生命周期,垃圾回收主要针对短期对象。这减少了可达性分析的频率,因为大多数对象会在年轻代快速回收。

    2. 并行和并发回收:现代垃圾回收器(如 G1 和 ZGC)支持并行和并发的可达性分析,利用多个 CPU 核心加速分析过程,并尽量减少对应用程序的影响。

Minor GC/Young GC、Major GC/Old GC、Mixed GC、Full GC 都是什么意思?

  1. 部分收集(Partial GC):指目标不是完整收集整个 Java 堆的垃圾收集,其中又分为:
  • 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。

  • 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有CMS 收集器会有单独收集老年代的行为。

  • 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。

  • 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

Minor GC/Young GC 什么时候触发?

新创建的对象优先在新生代 Eden 区进行分配,如果 Eden 区没有足够的空间时,就会触发 Young GC 来清理新生代。

什么时候会触发 Full GC?

对象什么时候会进入老年代?

对象进入老年代

垃圾收集算法了解吗?

垃圾收集算法主要有三种:

  1. 标记-清除算法

见名知义,标记-清除(Mark-Sweep)算法分为两个阶段:

  • 标记 : 标记出所有需要回收的对象

  • 清除:回收所有被标记的对象

标记-清除算法比较基础,但是主要存在两个缺点:

  • 执行效率不稳定,如果 Java 堆中包含大量对象,而且其中大部分是需要被回收的,这时必须进行大量标记和清除的动作,导致标记和清除两个过程的执行效率都随对象数量增长而降低。

  • 内存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

  • 标记-复制算法

标记-复制算法解决了标记-清除算法面对大量可回收对象时执行效率低的问题。

过程也比较简单:将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这种算法存在一个明显的缺点:一部分空间没有使用,存在空间的浪费。

新生代垃圾收集主要采用这种算法,因为新生代的存活对象比较少,每次复制的只是少量的存活对象。当然,实际新生代的收集不是按照这个比例。

  • 标记-整理算法

为了降低内存的消耗,引入一种针对性的算法:标记-整理(Mark-Compact)算法。

其中的标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向内存空间一端移动,然后直接清理掉边界以外的内存。

标记-整理算法主要用于老年代,移动存活对象是个极为负重的操作。

反射

什么是反射

假如在编译期无法确定类的信息,但又想在运行时获取类的信息、创建类的实例、调用类的方法,这时候就要用到反射。

反射允许程序在运行时动态地获取类的完整信息,并且能够在运行时操作类、方法、属性等。通过反射,程序可以在运行时检查类的结构,调用对象的方法,甚至修改对象的属性,而无需在编译时知道这些类的具体信息。(可以获取私有的方法和属性)

反射的主要功能依赖于 java.lang.reflect 包中的类,如 ClassMethodFieldConstructor 等。通过这些类,程序可以在运行时访问和操作类的结构。

反射的原理是什么?

我们都知道 Java 程序的执行分为编译 和 运行两步,编译之后会生成字节码(.class)文件,JVM 进行类加载的时候,会加载字节码文件,将类型相关的所有信息加载进方法区,反射就是去获取这些信息,然后进行各种操作。

反射有哪些应用场景?

①、Spring 框架就大量使用了反射来动态加载和管理 Bean。

②、Java 的动态代理(Dynamic Proxy)机制就使用了反射来创建代理类。代理类可以在运行时动态处理方法调用,这在实现 AOP 和拦截器时非常有用。

③、JUnit 和 TestNG 等测试框架使用反射机制来发现和执行测试方法。反射允许框架扫描类,查找带有特定注解(如 @Test)的方法,并在运行时调用它们。

设计模式

什么是单例模式?

单例模式(Singleton Pattern)它确保一个类只有一个实例,并提供一个全局访问点来获取该实例。单例模式主要用于控制对某些共享资源的访问,例如配置管理器、连接池、线程池、日志对象等。

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器

实现单例模式的关键点:

  1. 私有构造方法:确保外部代码不能通过构造器创建类的实例。

  2. 私有静态实例:持有类的唯一实例。

  3. 公有静态方法:提供全局访问点以获取实例,如果实例不存在,则在内部创建。

01、饿汉式

饿汉式单例(Eager Initialization)在类加载时就急切地创建实例,不管你后续用不用得到,这也是饿汉式的来源,简单但不支持延迟加载实例。

02、懒汉式

懒汉式单例(Lazy Initialization)在实际使用时才创建实例,这种实现方式需要考虑线程安全问题,因此一般会带上 synchronized 关键字

什么是工厂模式?

工厂模式(Factory Pattern)主要用于创建对象,而不暴露创建对象的逻辑给客户端。

其在父类中提供一个创建对象的方法, 允许子类决定实例化对象的类型。

工厂模式的主要类型

①、简单工厂模式(Simple Factory):简单工厂模式包括一个工厂类,它提供一个方法用于创建对象。

②、工厂方法模式(Factory Method):定义一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类的实例化推迟到子类进行。

动物实体类:

猫实体类:

构建宠物的工厂:

猫工厂:

应用场景

  1. 工厂方法模式适用于数据库访问层,其中需要根据不同的数据库(如MySQL、PostgreSQL、Oracle)创建不同的数据库连接。工厂方法可以隐藏这些实例化逻辑,只提供一个统一的接口来获取数据库连接。

  2. 日志记录:当应用程序需要实现多种日志记录方式(如向文件记录、数据库记录或远程服务记录)时,可以使用工厂模式来设计一个灵活的日志系统,根据配置或环境动态决定具体使用哪种日志记录方式。

代理模式

主要目的是为了控制对对象的访问,通过在代理类中调用被代理类的方法实现功能增强

  1. 静态代理

  • 动态代理

    • JDK动态代理:JDK原生的实现方式,只能代理实现接口的类,需要被代理的目标类必须实现接口,使用反射机制来代理接口方法。

    • cglib动态代理:通过继承被代理的目标类实现代理,所以不需要目标类实现接口。(CGLIB 通过动态生成一个需要被代理类的子类(即被代理类作为父类),该子类重写被代理类的所有不是 final 修饰的方法,并在子类中采用方法拦截的技术拦截父类所有的方法调用,进而织入横切逻辑。)

    CGLib 实现步骤

    1. 创建一个实现接口 MethodInterceptor 的代理类,重写 intercept 方法;

    2. 创建获取被代理类的方法 getInstance(Object target);

    3. 获取代理类,通过代理调用方法。

    JDK Proxy 和 CGLib 的区别主要体现在以下方面:

    • JDK Proxy : Java 语言自带的功能,无需通过加载第三方类实现;通过拦截器加反射的方式实现的;只能代理实现接口的类;实现和调用起来比较简单;

    • CGLib: 第三方提供的工具,基于 ASM 实现的,性能比较高;无需通过接口来实现,它是针对类实现代理,主要是对指定的类生成一个子类,它是通过实现子类的方式来完成调用的。

装饰模式

主要目的是为了给对象添加额外的功能。通过在装饰器类中调用被装饰对象的方法,并在其前后添加额外的功能实现功能增强。

策略和工厂模式来优化if else代码

策略模式

首先,利用策略模式来优化doSomething。

第一步、创建策略接口类。

第二步、根据不同的逻辑去实现策略。

工厂模式

其次,利用工厂模式集中创建实现所需要的策略对象。此处,是通过map来实现不同的策略对象的创建,与此同时,创建工厂类时,需要向外部提供一个可以供外部调用的方法,即:getMedalService()方法。

实际应用

通过工厂类中的getMedalService方法来实现了不同逻辑策略的调用,从而有效地提升了代码的整洁度,也方便了开发人员去根据不同的策略逻辑,去实现IMedalService接口