泛型
Java 泛型(Generics) 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。
编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型
ArrayList<Person> persons = new ArrayList<Person>()
这行代码就指明了该 ArrayList
对象只能传入 Person
对象,如果传入其他类型的对象就会报错
并且,原生 List
返回类型是 Object
,需要手动转换类型才能使用,使用泛型后编译器自动转换
泛型的使用方式有哪几种?
泛型一般有三种使用方式:泛型类、泛型接口、泛型方法
1.泛型类
//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型
//在实例化泛型类时,必须指定T的具体类型
public class Generic<T>{
private T key;
public Generic(T key) {
this.key = key;
}
public T getKey(){
return key;
}
}
那该怎么实例化泛型类呢?
Generic<Integer> genericInteger = new Generic<Integer>(123456);
2.泛型接口
public interface Generator<T> {
public T method();
}
无指定类型实现泛型接口
class GeneratorImpl<T> implements Generator<T>{
@Override
public T method() {
return null;
}
}
指定类型实现泛型接口
class GeneratorImpl implements Generator<String> {
@Override
public String method() {
return "hello";
}
}
3.泛型方法
public static < E > void printArray( E[] inputArray )
{
for ( E element : inputArray ){
System.out.printf( "%s ", element );
}
System.out.println();
}
怎么使用泛型方法呢?
// 创建不同类型数组:Integer, Double 和 Character
Integer[] intArray = { 1, 2, 3 };
String[] stringArray = { "Hello", "World" };
printArray( intArray );
printArray( stringArray );
注意:
public static < E > void printArray( E[] inputArray )
一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的<E>
将泛型运用到实际项目当中,又有哪里能用到泛型呢?
-
自定义接口通用返回结果
CommonResult<T>
通过参数T
可根据具体的返回类型动态指定结果的数据类型 -
定义
Excel
处理类ExcelUtil<T>
用于动态指定Excel
导出的数据类型 -
构建集合工具类(参考
Collections
中的sort
,binarySearch
方法)。 -
.........................................................
反射
什么是反射?
如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。
通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性,非常方便、灵活
像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。
但是,这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制,这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射,在Java的注解当中,也运用到了反射的机制
为啥在使用Spring框架时用一个@Component就能直接声明一个类为Bean?为啥使用@Value就能轻松读取配置文件中的配置项?这些都是得益于反射的机制,因为可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理
谈谈反射机制的优缺点
那么在反射带来了便利的同时,其实也有一些负面影响
优点:可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
缺点:让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)、会破坏封装性。另外,反射的性能也要稍差点,但其实对于框架来说实际是影响不大的
注解
什么是注解?
Annotation
(注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。
注解本质其实就是一个继承了Annotation
的特殊接口
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}
public interface Override extends Annotation{
}
JDK 提供了很多内置的注解(比如 @Override
、@Deprecated
),同时,我们还可以自定义注解
注解只有被解析了之后才会生效,有两种解析方法:
-
编译期直接扫描:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用
@Override
注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 -
运行期通过反射处理:像框架中自带的注解(比如 Spring 框架的
@Value
、@Component
)都是通过反射来进行处理的。
SPI
什么是SPI?
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等
SPI 和 API 有什么区别?
说到 SPI 就不得不说一下 API(Application Programming Interface) 了,从广义上来说它们都属于接口,而且很容易混淆
一般模块之间都是通过接口进行通讯,因此我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”
-
当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
-
当接口存在于调用方这边时,这就是 SPI 。由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
打个比方:英伟达公司生产了一块显卡4090,马上进入量产阶段,而市面上有很多种电脑主机的主板制造业公司,这时,只需要英伟达官方定义好显卡接口的标准规则(定义好了接口标准),那么这些合作公司(服务提供者)就直接按照标准实现主板的开发就好了,虽然他们生产的主板都不一样,但是最终的结果都是为了适配英伟达公司生产的这块显卡的接口。(提供不同方案的实现,但是给出来的结果是一样的)
SLF4J (Simple Logging Facade for Java)是 Java 的一个日志门面(接口),其具体实现有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了,其实这就是依赖 SPI 机制实现的
SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟我讲的这个有些细微的区别,不过整体的原理都是一致的
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
-
遍历加载所有的实现类,这样效率还是相对较低的;
-
当多个
ServiceLoader
同时load
时,会有并发问题。