基本概念与常识
(chatGPT)Java和C++的区别
- 应用领域:C++主要用于开发系统软件、游戏等高性能、实时应用领域,而Java则主要用于Web开发、移动应用、企业应用、大数据等领域。
- 编程范式:C++是一种支持多种编程范式的语言,包括面向对象、泛型、过程式等,而Java则主要是一种面向对象的语言,也支持泛型和函数式编程。
- 内存管理:C++程序员需要手动管理内存,包括内存分配和释放,而Java具有垃圾回收机制,程序员无需手动管理内存。
- 平台相关性:C++代码在不同平台上需要编译成不同的二进制代码,而Java通过JVM(Java虚拟机)来实现跨平台,一份Java代码可以在任何支持JVM的平台上运行。
- 开发效率:Java具有更高的开发效率,因为它具有丰富的标准库、自动内存管理、强制异常处理等特性,而C++则需要程序员更多的手动管理。
- 性能:C++具有更高的性能,因为它可以直接访问内存,没有垃圾回收机制的开销,而Java的垃圾回收机制会对性能产生一定的影响。
基础语法
列举5个Java常用的包,以及每个包的应用
- java.lang包:该包包含Java语言的基础类,如Object、String、Boolean、Integer等。它是Java程序中最常用的包之一。
- java.util包:该包包含Java语言的各种实用工具类,如集合框架(List、Set、Map等)、日期时间类、随机数生成器、UUID生成器等。
- java.io包:该包包含Java语言的各种输入输出类,如文件读写类、字节流类、字符流类等。它可以让Java程序读写文件、网络传输数据等。
- java.net包:该包包含Java语言的网络编程类,如Socket、ServerSocket、URL等。它可以让Java程序与其他计算机通过网络进行通信。
- java.awt包:该包是Java语言的图形用户界面(GUI)包,包含各种图形用户界面组件类,如Button、TextField、Label、Frame等。它可以让Java程序创建图形化的用户界面。
Java的八大基本类型 +3
Integer 和 Character
自动类型转换、强制类型转换
Java 所有的数值型变量可以相互转换,当把一个范围小的数值或变量直接赋给另一个范围大的变量时,可以进行自动类型转换;反之,需要强制转换。
float f=3.4
,对吗?
不正确。3.4 是双精度数,将双精度型(double)赋值给浮点型(float)属于下转型(down-casting,也称为窄化)会造成精度损失,因此需要强制类型转换float f =(float)3.4;
或者写成float f =3.4F
short s1 = 1; s1 = s1 + 1;
对吗?short s1 = 1; s1 += 1;
对吗?
对于 short s1 = 1; s1 = s1 + 1;编译出错,由于 1 是 int 类型,因此 s1+1 运算结果也是 int 型,需要强制转换类型才能赋值给 short 型。
而 short s1 = 1; s1 += 1;可以正确编译,因为 s1+= 1;相当于 s1 = (short(s1 + 1);其中有隐含的强制类型转换
自动装箱和拆箱
Java 可以自动对基本数据类型和它们的包装类进行装箱和拆箱。
- 装箱:将基本类型转换为它们对应的包装类对象;
- 拆箱:将包装类对象转换为基本数据类型;
&和&&有什么区别?
&&之所以称为短路运算是因为,如果&&左边的表达式的值是 false,右边的表达式会被直接短路掉,不会进行运算。
基本数据类型和引用数据类型的区别
- 存储方式:基本数据类型的变量直接存储它们的值,而引用数据类型的变量存储的是对象的引用。
- 默认值:基本数据类型有默认值,而引用数据类型的默认值是null。
- 方法和属性:基本数据类型没有任何方法和属性,而引用数据类型可以有自己的方法和属性。
- 传递方式:基本数据类型的传递是值传递,而引用数据类型的传递是地址传递。
- 比较方式:基本数据类型的比较是值比较,而引用数据类型的比较是引用比较。
用最有效率的方法计算 2 乘以 8?
2 << 3。位运算,数字的二进制位左移三位相当于乘以 2 的三次方。
String 和 StringBuilder、StringBuffer 的区别?
- String:String 的值被创建后不能修改,任何对 String 的修改都会引发新的 String 对象的生成。
- StringBuffer:跟 String 类似,但是值可以被修改,使用 synchronized 来保证线程安全。
- StringBuilder:StringBuffer 的非线程安全版本,性能上更高一些。
intern 方法有什么作用?
- 如果当前字符串内容存在于字符串常量池(即 equals()方法为 true,也就是内容一样),直接返回字符串常量池中的字符串
- 否则,将此 String 对象添加到池中,并返回 String 对象的引用
new String[]{"a", "B"},这段语句创建了几个对象?
这段语句会创建三个对象,包括一个数组对象和两个字符串对象。
128 127 Integer的问题
在 Java 中,Integer 类型的对象值在 -128 到 127 之间时会被缓存。这意味着,如果我们创建一个 Integer 对象,并且它的值在 -128 和 127 之间,那么这个对象将会被重用。当我们使用 == 运算符比较两个 Integer 对象时,如果它们的值在 -128 和 127 之间,那么它们将被视为相等。
Object 类的常见方法?
equals(Object obj)
: 判断当前对象是否和另一个对象相等。hashCode()
: 返回当前对象的哈希码。toString()
: 返回当前对象的字符串表示形式。getClass()
: 返回当前对象的运行时类。wait()
: 等待其他线程通知当前对象的线程进入等待状态。notify()
: 唤醒当前对象的一个等待线程。notifyAll()
: 唤醒当前对象的所有等待线程。- protected void finalize() throws Throwable :通知垃圾收集器回收对象。
- clone()
面向对象基础
面向对象/面向过程
面向对象编程(Object-Oriented Programming,简称OOP)是一种基于对象的编程模式,我们将问题看作是由许多对象组成的,每个对象都有自己的数据和操作这些数据的方法,并通过对象之间的相互作用来实现程序功能。
面向过程编程(Procedural Programming)则是一种基于步骤的编程模式,通过定义一系列的步骤(函数/方法)来完成特定的任务。
面向对象的三个特点 +2
- 封装:封装是指把数据和行为包装在一起,对外部隐藏数据的实现细节,只暴露必要的接口供外部使用。在Java中,通过访问修饰符来实现封装,例如private、protected和public等。
- 继承:继承是指一个类可以从另一个类继承属性和方法,从而实现代码的重用和扩展。在Java中,可以通过extends关键字来实现类的继承。
- 多态:父类或接口定义的引用变量可以指向子类或具体实现类的实例对象。
重写重载认识 +2
重写是指在子类中定义一个与父类中同名、同返回值类型、同参数类型的方法,并重新实现该方法的功能。重写的目的是为了覆盖父类的方法,以实现子类自己的业务逻辑。
重载是指在同一个类中定义多个同名方法,但是参数列表不同,即参数类型、参数个数或参数顺序不同。重载的目的是为了实现同一方法名对不同参数的多重处理。
访问修饰符 public、private、protected、以及不写(默认)时的区别?
- default (即默认,什么也不写): 在同一包内可见,不使用任何修饰符。可以修饰在类、接口、变量、方法。
- private : 在同一类内可见。可以修饰变量、方法。注意:不能修饰类(外部类)
- public : 对所有类可见。可以修饰类、接口、变量、方法
- protected : 对同一包内的类和所有子类可见。可以修饰变量、方法。注意:不能修饰类(外部类)。
this 关键字有什么作用?
this 是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。
static和final的区别? +3
static
- static关键字用于表示静态的、类级别的属性或方法,与具体的实例对象无关,属于整个类。
- 可以通过类名来访问静态属性和方法,也可以通过实例对象来访问。
- 静态属性和方法在内存中只有一份,可以被所有实例对象共享。
final关键字可以用于变量、方法和类。
- 用于变量时,必须被显式的指定初始值
- 用于方法时,表示该方法不能被重写;
- 用于类时,表示该类不能被继承。
final和static的底层是如何实现的?
final变量的值存储在常量池中,而static变量和方法存储在类的静态区域中。
接口和抽象类的区别?应用场景? +4
区别:
- 抽象类通过
abstract
关键字定义,可以包含抽象方法和非抽象方法;接口通过interface
关键字定义,只能包含抽象方法和常量。 - 继承方式不同:子类继承抽象类可以使用
extends
关键字;子类实现接口需要使用implements
关键字。 - 多继承支持不同:Java中的类只支持单继承,但一个类可以实现多个接口。
- 接⼝⽅法默认修饰符是 public ,抽象⽅法可以有 public 、 protected 和 default 这些修饰符(抽象⽅法就是为了被重写所以不能使⽤ private 关键字修饰!)。
- 从设计层⾯来说,抽象是对类的抽象,是⼀种模板设计,⽽接⼝是对⾏为的抽象,是⼀种⾏为的规范。
2.应用场景:
- 抽象类适用于某些类的通用操作,但每个子类可能有不同的实现方式,因此可以将通用的实现放在抽象类中,而具体实现留给子类完成。
- 接口适用于不同类之间的通用操作,可以通过接口实现类之间的松耦合,让它们可以在没有关联性的情况下相互交互。
总的来说,抽象类更适合用于设计层次结构,而接口则更适合用于实现多态性。
总结⼀下 jdk7~jdk9 Java 中接口的变化:
- 在 jdk 7 或更早版本中,接⼝⾥⾯只能有常量变量和抽象⽅法。这些接⼝⽅法必须由选择实现接⼝的类实现。
- jdk 8 的时候接⼝可以有默认⽅法和静态⽅法功能。
- jdk 9 在接⼝中引⼊了私有⽅法和私有静态⽅法。
设计接口的优点(√)
√jdk8 的新特性 +2
-
Lambda表达式:Lambda表达式是一个匿名函数,它可以作为参数传递给方法或存储在变量中,使得代码更加简洁和易读。
-
Stream API:Stream API提供了一种函数式的方式来处理集合数据,对一个集合进行过滤、映射、排序、归约等操作,而这些操作都可以通过链式调用来完成。
List<Student> studentList = new ArrayList<>(); // 添加学生信息到列表中 List<String> nameList = studentList.stream() .filter(s -> s.getAge() > 18) // 过滤出年龄大于18岁的 .map(Student::getName) // 映射出学生的姓名 .collect(Collectors.toList()); // 收集结果到列表中
-
日期时间 API:Java 8引入了新的时间API,使得日期和时间处理更加简单和方便。
-
Optional类:用来解决空指针异常的问题
Person person = new Person("张三", 20, null); String city = Optional.ofNullable(person) .map(Person::getAddress) .map(Address::getCity) .orElse("未知");
首先,我们使用
Optional.ofNullable
方法来将person对象包装成Optional对象。然后,我们使用map
方法来对Optional对象进行操作,如果person对象不为null,则调用getAddress()方法,如果getAddress()方法返回值不为null,则调用getCity()方法获取城市属性,否则返回默认值"未知"。如果person对象为null,则直接返回默认值"未知"。这样,即使person对象的address属性为null,也不会抛出空指针异常,而是返回一个默认值。
-
接口默认方法:默认方法允许接口中包含具有默认实现的方法,使得接口的使用更加灵活。
-
重复注解:Java 8允许在同一个元素上重复使用相同的注解,使得代码更加简洁和易读。
-
并行数组:Java 8引入了新的并行数组操作,使得数组的处理更加高效和简单。
-
方法引用:方法引用提供了一种简洁的方式来引用现有方法或构造函数,使得代码更加简洁和易读。
-
Nashorn JavaScript引擎:Java 8引入了Nashorn JavaScript引擎,使得Java和JavaScript之间的交互更加容易和高效。
成员变量与局部变量分别是什么
成员变量是定义在类中,方法外的变量,也称为类变量或实例变量。成员变量可以被整个类的方法、构造方法和块访问。成员变量有默认的初始值。
局部变量是定义在方法、构造方法或者块中的变量,其作用域仅限于定义它的方法、构造方法或块。局部变量需要显式初始化,否则不能使用。
==和equals +4
基本数据类型 == 比较的是值,引⽤数据类型 == 比较的是内存地址
equals() : 如果没有重写那么和 == 等价
如果重写,那么就按照自定义的标准来比较是否相等
equal判断两个对象的内部属性相等怎么判断的
判断两个对象的内部属性相等,可以在自定义对象的类中覆盖equals()
方法,并且重写hashCode()
方法。
-
在
equals()
方法中,需要判断两个对象的内部属性是否相等。一般的做法是,先判断是否为同一对象,如果是直接返回true
,如果不是再判断是否为同一类型,如果不是返回false
,最后再逐一比较对象的内部属性是否相等,如果都相等则返回true
,否则返回false
。 -
在
hashCode()
方法中,需要根据对象的内部属性生成一个哈希值。一般的做法是,将对象的每个内部属性的哈希值进行异或运算,并将结果返回。这样做的目的是为了尽可能地保证哈希值的分布均匀,减小哈希冲突的可能性。
这样做的好处是,在使用一些数据结构时,比如HashSet
、HashMap
等,可以通过重写equals()
和hashCode()
方法,让这些数据结构正确地处理自定义对象的相等性。
Java 是值传递,还是引用传递
在 Java 中,所有的参数传递都是值传递,包括基本数据类型和引用数据类型。不同之处在于,对于基本数据类型,实际传递的是值本身;对于引用数据类型,实际传递的是引用所指向的对象的地址。
JVM 的内存分为堆和栈,其中栈中存储了基本数据类型和引用数据类型实例的地址,也就是对象地址。
而对象所占的空间是在堆中开辟的,所以传递的时候可以理解为把变量存储的对象地址给传递过去,因此引用类型也是值传递。
深拷贝和浅拷贝?
- 浅拷贝:仅拷贝被拷贝对象的成员变量的值,也就是基本数据类型变量的值,和引用数据类型变量的地址值,而对于引用类型变量指向的堆中的对象不会拷贝。
- 深拷贝:完全拷贝一个对象,拷贝被拷贝对象的成员变量的值,堆中的对象也会拷贝一份。
例如现在有一个 order 对象,里面有一个 products 列表,它的浅拷贝和深拷贝的示意图:
因此深拷贝是安全的,浅拷贝的话如果有引用类型,那么拷贝后对象,引用类型变量修改,会影响原对象。
浅拷贝如何实现呢?
Object 类提供的 clone()方法可以非常简单地实现对象的浅拷贝。
深拷贝如何实现呢?
- 重写克隆方法:重写克隆方法,引用类型变量单独克隆,这里可能会涉及多层递归。
- 序列化:可以先将原对象序列化,再反序列化成拷贝对象。
除了平时常用的new的方式创建对象,你还知道什么其他的方式吗
- 使用反射:可以通过 Class 类的 newInstance() 方法或 Constructor 类的 newInstance() 方法来创建对象。
- 使用 clone():通过实现 Cloneable 接口并重写 clone() 方法可以实现对象的浅拷贝。
- 使用反序列化:可以将对象序列化后再反序列化得到一个新的对象。
- 使用工厂模式:通过静态方法或工厂接口的实现类来创建对象,隐藏了对象的具体实现细节,提高了代码的可维护性。
- 使用单例模式:通过私有化构造器、提供静态方法或枚举类型实现单例模式,确保系统中只有一个实例对象存在。
- 使用注解:通过自定义注解并使用反射创建对象,可以实现基于注解的对象创建,例如 Spring 框架中基于注解的依赖注入。
内部类了解吗?匿名内部类了解吗?
内部类又称为嵌套类,可以把内部类理解为外部类的一个普通成员。
内部类之所以也是语法糖,是因为它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同。
访问特点
成员内部类可以直接访问外部类的成员变量和成员方法。
外部类不可以直接访问内部类的成员变量和成员方法。
外部类想要访问内部的成员: 得创建内部类对象来访问内部类成员。
异常
异常分为哪些类型
Throwable
是 Java 语言中所有错误或异常的基类。 Throwable 又分为Error
和Exception
,其中 Error 是系统内部错误,比如虚拟机异常,是程序无法处理的。Exception
是程序问题导致的异常,又分为两种:
- CheckedException 受检异常:编译器会强制检查并要求处理的异常。
- RuntimeException 运行时异常:程序运行中出现异常,比如我们熟悉的空指针、数组下标越界等等
异常的处理方式?
- 遇到异常不进行具体处理,而是继续抛给调用者 (throw,throws)
抛出异常有三种形式,一是 throw,一个 throws,还有一种系统自动抛异常。
throws 用在方法上,后面跟的是异常类,可以跟多个;而 throw 用在方法内,后面跟的是异常对象。
- try catch 捕获异常
在 catch 语句块中补货发生的异常,并进行处理。
try {
//包含可能会出现异常的代码以及声明异常的方法
}catch(Exception e) {
//捕获异常并进行处理
}finally { }
//可选,必执行的代码
}
try-catch 捕获异常的时候还可以选择加上 finally 语句块,finally 语句块不管程序是否正常执行,最终它都会必然执行。
常见的运行时异常
- NullPointerException(空指针异常):当一个变量或对象为null时,尝试使用它时,会引发此异常。
- ArrayIndexOutOfBoundsException(数组越界异常):当尝试访问数组中不存在的索引时,会引发此异常。
- ClassCastException(类型转换异常):当试图将一个对象转换为不兼容的类型时,会引发此异常。
- IllegalArgumentException(非法参数异常):当传递给方法的参数无效或不合法时,会引发此异常。
- StackOverflowError(栈溢出异常):当递归调用层数过多或程序调用栈超过了虚拟机所能提供的最大深度时,会引发此异常。
统一处理异常是什么
统一处理异常是指在应用程序中对所有异常进行统一的处理,从而使得异常处理更加规范和统一。在实际应用中,通过捕获所有可能出现的异常,并对其进行封装和处理,可以提高代码的健壮性和可维护性,避免因为异常导致程序崩溃或者运行异常。
在Java应用中,可以通过在代码中使用try-catch语句来捕获异常,然后在catch块中对异常进行处理,常见的处理方式包括记录日志、返回错误信息等。但是如果在应用中存在大量的try-catch语句,会导致代码的可读性和可维护性变差,而且代码量也会变得很大。
为了解决这个问题,可以采用统一处理异常的方式,通过在代码中集中处理异常,可以避免重复的代码,提高代码的可读性和可维护性。
在Java中,可以通过自定义异常处理器和使用框架提供的异常处理器来实现统一处理异常。例如,Spring框架提供了一个全局异常处理器,可以通过配置来统一处理应用程序中所有的异常。
三道经典异常处理代码题
题目 1
public class TryDemo {
public static void main(String[] args) {
System.out.println(test());
}
public static int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.print("3");
}
}
}
执行结果:31。
try、catch。finally 的基础用法,在 return 前会先执行 finally 语句块,所以是先输出 finally 里的 3,再输出 return 的 1。
题目 2
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
try {
return 2;
} finally {
return 3;
}
}
}
执行结果:3。
try 返回前先执行 finally,结果 finally 里不按套路出牌,直接 return 了,自然也就走不到 try 里面的 return 了。
finally 里面使用 return 仅存在于面试题中,实际开发这么写要挨吊的。
题目 3
public class TryDemo {
public static void main(String[] args) {
System.out.println(test1());
}
public static int test1() {
int i = 0;
try {
i = 2;
return i;
} finally {
i = 3;
}
}
}
执行结果:2。
- 统一处理异常
I/O 装饰器模式
什么是IO流
介绍一下java io ,Java 中 IO 流分为几种?
- 按照流的流向分,可以分为输入流和输出流;
- 按照操作单元划分,可以划分为字节流和字符流;
- 按照流的角色划分为节点流和处理流
Java I0 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
既然有了字节流,为什么还要有字符流?
I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
- IO和NIO
- IO 多路复用 +2
- 怎么理解io多路复用?
BIO、NIO、AIO?
BIO(blocking I/O) : 就是传统的 IO,同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过连接池机制改善(实现多个客户连接服务器)。
NIO 是同步非阻塞的,服务器端用一个线程处理多个连接,客户端发送的连接请求会注册到多路复用器上,多路复用器轮询到连接有 IO 请求就进行处理:
AIO (Asynchronous I/O)
AIO:JDK 7 引入了 Asynchronous I/O,是异步不阻塞的 IO。在进行 I/O 编程中,常用到两种模式:Reactor 和 Proactor。Java 的 NIO 就是 Reactor,当有事件触发时,服务器端得到通知,进行相应的处理,完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
泛型
Java 泛型了解么?什么是类型擦除?介绍一下常用的通配符?
泛型的核心思想是参数化类型,即将类型作为参数传入类或方法中,从而实现代码的重用和类型的安全检查。
泛型仅仅用于在编译期间检查类型的正确性。在运行期间,Java虚拟机不会保留泛型的类型信息。
常用的通配符为: T,E,K,V,?
- ? 表示不确定的 java 类型
- T (type) 表示具体的一个 java 类型
- K V (key value) 分别代表 java 键值中的 Key Value
- E (element) 代表 Element
反射
什么是反射?应用?原理?
反射(Reflection)是 Java 的一种基础特性,它提供了在运行时访问、检测和修改类、方法、属性等程序结构的能力。通过反射机制,可以在程序运行期间动态获取类的信息,并对类、对象进行操作。
反射的应用:
- 动态代理:反射可以动态创建代理类,从而实现 AOP 等功能。
- 依赖注入:框架(如 Spring)使用反射机制读取配置文件,从而将依赖注入到程序中。
- 反射调用方法:Java 的单元测试框架 JUnit 中,使用反射机制来调用被测试类中的测试方法。
反射的原理:
在 Java 中,每个类都有对应的 Class 对象,Class 对象记录了该类的信息,如类名、访问修饰符、实现的接口、构造器、方法、成员变量等。通过 Class 对象的一些方法,就可以在程序运行期间获取类的信息,并对类进行操作。
反射的原理主要是通过 JVM 在运行期间加载类,并在堆内存中动态地生成 Class 对象。JVM 将类的字节码文件读入内存,并对其进行解析,生成对应的 Class 对象,并保存在方法区中。当程序需要访问某个类的信息时,JVM 就会通过 ClassLoader 从方法区中获取对应的 Class 对象,从而实现对类信息的访问。
反射机制的效率
反射机制的效率较低。这是因为在使用反射机制时,需要进行额外的操作来获取类的信息、方法、构造函数、字段等,并且需要进行类型转换,这些操作都会消耗额外的时间。
序列化与反序列化
序列化与反序列化
序列化就是把 Java 对象转为二进制流,方便存储和传输。
反序列化就是把二进制流恢复成对象。
序列化的时候是不包含静态变量的。
对于不想进行序列化的变量,使用transient
关键字修饰。
transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。transient
只能修饰变量,不能修饰类和方法。
说说有几种序列化方式?
- Java自带的序列化:Java对象实现Serializable接口,使用ObjectOutputStream/ObjectInputStream进行序列化和反序列化。这种方式简单易用,但是序列化后的数据较大,且对版本兼容性要求较高。
- JSON序列化:将Java对象序列化为JSON字符串或将JSON字符串反序列化为Java对象。这种方式序列化后的数据相对较小,易于网络传输,且跨语言兼容性较好。常用的JSON序列化框架有Jackson、Gson等。
- XML序列化:将Java对象序列化为XML字符串或将XML字符串反序列化为Java对象。这种方式也是跨语言兼容性较好,但是序列化后的数据相对较大。常用的XML序列化框架有JAXB、XStream等。
- Protobuf序列化:Google开发的一种二进制序列化协议,序列化后的数据非常小,效率高。但是,该方式需要预先定义数据格式和协议,需要编写.proto文件并生成对应的Java代码。
注解
说一下你对注解的理解?
在代码中添加元数据标记的方法,可以用来为编译器和其他工具提供信息。
例如我们常见的 AOP,使用注解作为切点就是运行期注解的应用;比如 lombok,就是注解在编译期的运行。
再比如 Spring 常见的 Autowired ,就是 RUNTIME 的,所以在运行的时候可以通过反射得到注解的信息,还能拿到标记的值 required 。
注解生命周期有三大类,分别是:
- RetentionPolicy.SOURCE:给编译器用的,不会写入 class 文件
- RetentionPolicy.CLASS:会写入 class 文件,在类加载阶段丢弃,也就是运行的时候就没这个信息了
- RetentionPolicy.RUNTIME:会写入 class 文件,永久保存,可以通过反射获取注解信息
注解的底层实现原理
注解(Annotation)是一种在Java程序中使用的元数据(metadata),它们提供了一种不影响程序语义的注释机制,使得我们可以在代码中加入一些特定的信息,来告诉编译器、工具或者运行时环境一些信息。Java语言提供了多种内置注解,同时我们也可以自定义注解。
注解的底层实现原理与反射机制密切相关。Java中的注解本质上是一个接口类型,编译器会自动将该注解转换成一个继承自Annotation的接口的实现类。这个实现类中包含了注解中定义的属性,并且重写了hashCode、equals和toString方法。当我们在代码中使用注解时,编译器会自动将注解转换成一个Annotation实现类的实例,并将注解中的属性值填充到该实例中。
在运行时,我们可以使用Java的反射机制来访问注解及其属性值。Java中的反射机制可以让我们在运行时获取类的信息,并通过这些信息来调用类中的方法、访问类中的字段等。我们可以通过反射机制获取一个类、方法、字段等上的注解,并通过注解的属性值来判断该类、方法、字段等的一些特性,从而对程序进行处理。
总之,注解的底层实现原理是通过反射机制来访问注解及其属性值,从而为程序提供了一种声明式的编程方式。
SPI
Java SPI全称为Service Provider Interface,是Java提供的一种服务发现机制。SPI的作用是为特定接口寻找实现类,有点类似于IOC、依赖注入的感觉,不过这里找的不是具体的实现类,而是服务接口的实现类。在SPI中,服务提供者通过在class路径下的META-INF/services目录下的文件来说明它们提供的服务,而服务请求方则可以通过Java SPI来发现和加载服务实现。
Lambda 表达式
Lambda 表达式本质上是一段匿名内部类,也可以是一段可以传递的代码。它可以将一个代码块或者函数作为参数传递给方法或者变量
(parameters) -> { statements; }
比如我们以前使用 Runnable 创建并运行线程:
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("Thread is running before Java8!");
}
}).start();
这是通过内部类的方式来重写 run 方法,使用 Lambda 表达式,还可以更加简洁:
new Thread( () -> System.out.println("Thread is running since Java8!") ).start();
当然不是每个接口都可以缩写成 Lambda 表达式。只有那些函数式接口(Functional Interface)才能缩写成 Lambda 表示式。
所谓函数式接口(Functional Interface)就是只包含一个抽象方法的声明。针对该接口类型的所有 Lambda 表达式都会与这个抽象方法匹配。