深入浅出泛型,框架设计的基础
泛型在 Java 5 出现,实现了参数化类型,主要作用是使得类或接口更加通用。比如 Java 中的容器类,通过泛型实现了对各种类型的兼容,成为极其通用的类库。如果我们要设计自己的框架,泛型基本上已经算是标配了。
类使用泛型
在类型上使用泛型,非常简单,只需要使用 <> 声明类型即可。如下 TestDO 类型声明了泛型 T,并使用 T 声明了字段 value。这样做的好处便是,TestDO 可以承载各种各样的数据,而不用定义具体的类型。这也说明了,泛型不仅可以用于类型,也可以用于字段或变量。
Java 在运行时对泛型进行擦除,运行时所有的泛型类型都退化为 Object 类型。这样的设计主要是为了保证对 Java 5 之前的代码进行兼容。尽管如此,使用泛型后,在编译阶段仍会对类型进行检查,确保使用泛型的地方都是类型正确的。
@Getter
@Setter
public class TestDO<T> {
T value;
}
如下代码中,SubTestDO 传递给父类的具体类型为默认类型 Object 类型,而SubTestDOSimple 传递给父类的具体类型则是 String 类型。这两个子类都对泛型做了具体化,是泛型类型的常规使用。
/** 无泛型,即为 Object */
class SubTestDO extends TestDO {
}
/** 继承时指定父类实际类型 */
class SubTestDOSimple extends TestDO<String> {
}
在定义 TestDO 的子类时,子类也可以使用泛型。子类的泛型可以独自使用,也可以传递给父类。如 SubTestDOTrans 定义了泛型 T 并把 T 传递给父类,而 SubTestDOComp 定义了泛型 T 要求必须是 HashMap 的子类,但传递给父类的泛型变成了 List<T>。
/** 继承时传递泛型 */
class SubTestDOTrans<T> extends TestDO<T> {
}
/** 继承时传递复杂类型 */
public class SubTestDOComp<T extends HashMap> extends SubTestDOTrans<List<T>> {
}
接口使用泛型
接口使用泛型可以定义适用于各种场景的通用接口,在设计模式中具有重要作用。如工厂模式就可以实现根据返回值类型自动转型,涉及到方法的泛型,在后文给出解释。
如下代码中,IGeneric 接口使用了泛型,并用泛型定义了方法。如此定义后,IGeneric 接口成为了一个通用接口,方法的入参类型与实现类或子接口对接口的定义保持一致,大大提高了通用性。接口可以继承,在继承中泛型可以有形式的变化,并定义新的泛型。
/** 普通泛型接口,泛型 T */
public interface IGeneric<T> {
public void test(T t);
}
ISubGenericSimple 接口在继承接口时,直接确定了泛型的类型为 String 类型,ISubGenericSimple 接口退化为一个普通接口,子接口或实现类不再需要对泛型进行处理。这是通过子接口对泛型进行具体化,也是架构设计中的常用手段。
/** 继承时指定泛型 */
interface ISubGenericSimple extends IGeneric<String>{}
ISubGenericTrans 接口同样使用了泛型,并把泛型传递给父接口。这种一般用于父接口的扩展,在子接口中可以定义一些特有方法,同时又完全兼容父接口的引用。
/** 继承时传递泛型 */
interface ISubGenericTrans<T> extends IGeneric<T>{}
ISubGenericSubType 接口在泛型定义中,明确声明了泛型类型的范围必须是 TestDO 及其子类,这也是对父接口的一种退化,缩小了泛型的范围。这种情况一般用于比较复杂的接口体系,父接口完成顶层定义,多个子接口进行类型具体化实现分层设计。
/** 继承时指定泛型为特定类型的子类 */
interface ISubGenericSubType<T extends TestDO> extends IGeneric<T>{}
ISubGenericCompType 接口使用了一个相对复杂的泛型定义。子接口定义了泛型 T,而传递给父接口的泛型变成了 TestDO<T>。这种接口继承方式在大型软件架构中有比较广泛的应用,通俗理解为父接口使用容器但元素类型为泛型,而子接口定义容器的元素类型,在组件化继承体系有比较明确的应用。这样的继承关系,可以确保父接口完成容器的操作方法而不用关心具体元素类型,子接口可以有少许定制逻辑甚至可以不定义方法。
/** 继承时指定复杂的泛型类型 */
interface ISubGenericCompType<T> extends IGeneric<TestDO<T>>{}
方法使用泛型
方法也可以使用泛型,并且有很大的现实意义,在工厂模式、建造器模式中都可以使用泛型方法。
toString 方法定义了一个简单的泛型方法。在访问权限和返回值类型之间使用 <T> 声明泛型类型为 T,然后把入参类型设置为 T。在这个泛型方法中,入参类型可以是任何类型,这个通用方法可以在任何场景下使用。
/** 泛型 T 仅用于入参 */
static <T> String toString(T t){
return null;
}
getInstance 方法不但在入参中使用了泛型,也把这个泛型作为返回值的类型。类似 getInstance 方法的定义有很多,比如 Spring 的 getBean 方法就重载了这种形式的应用。在确保方法通用性的基础上,也保证了返回值类型与入参的一致性。
/** 返回 T 类型 */
static <T> T getInstance(Class<T> clazz) throws Exception {
return clazz.newInstance();
}
getTestDO 方法是另一种形式的泛型方法,这里定义了返回值的泛型必须是 TestDO 及其子类。这种泛型方法常用于继承体系明确,需要对类型进行向下转型的场景。在方法内部需要对返回值进行强制转型,如果类型不匹配便会抛出类型转换异常,因此要求调用者必须了解方法的定义和使用要求。
/** 返回 TestDO 的子类类型 */
static <T extends TestDO> T getTestDO(){
return (T) new SubTestDOTrans<>();
}
下面 getTestDO 的重载方法对泛型做了一定程度的退化,虽然定义了泛型 T,但这个泛型仅仅是容器的元素类型,方法的返回值是 TestDO<T>。这种类型的泛型方法可用于复杂对象的组装,比上面的重载方法更安全。
/** 返回 TestDO<T> 类型 */
static <T> TestDO<T> getTestDO(T v){
TestDO<T> testDO = new TestDO<>();
testDO.setValue(v);
return testDO;
}
泛型擦除
前面提到过 Java 会在运行时对泛型进行擦除,也就是抹去泛型信息,全部变为 Object 类型。因此,在运行时,TestDO<String> 与 TestDO<Integer> 在类型上并没有多大区别。虽然实际存储的 value 对象的真正类型是不同的,但是都会当做 Object 进行处理。也是因为擦除,引用类型对象时,我们可以使用 TestDO.class 却不能使用 TestDO<String>.class。
泛型通配
前面的泛型都明确了泛型的类型为 T,并使用 T 定义字段、参数和返回值。在对具体类型依赖不那么强烈的情况下,比如,我们仅仅是要求类型是某个类型的子类即可,最终处理时使用父类类型而不依赖于子类类型,则可以使用通配符 ? 来定义泛型。
在如下代码中,set1 的元素类型为 TestDO 的子类。我们只关心 Set 中的元素是 TestDO 类型即可,并不关心实际元素是 SubTestDOSimple 还是 SubTestDOComp。而 set2 的元素类型可以是任何类型。
Set<? extends TestDO> set1 = new HashSet<>();
Set<?> set2 = new HashSet<>();
泛型逆变
前面提到的泛型定义用到了 extends 来确定泛型的边界,要求其必须是某个类型的子类。实际上,泛型还支持向上确定边界,也就是泛型为某个类型的父类。在如下代码中,定义了 Set 的元素类型为 SubTestDOComp 的父类。
Set<? super SubTestDOComp> testDOs = new HashSet<>();
使用泛型创建类型安全的分层结构
在架构设计中,常常涉及复杂的数据结构与设计分层,使用泛型对类型进行定义和约束,可以确保抽象层的设计有足够的通用性,同时最终产生的具体类型依然保持正确的数据类型。
比如在一个架构设计中,可能会分为抽象层、基础层、实现层三个层次。
在抽象层定义极度抽象的业务流程,需要这里的类型定义都具有较好的通用性,这时可以大量使用泛型和抽象类,只定义流程而不涉及具体类型。
在基础层定义一些基础服务,可能会在 core 层类型的基础上做一定程度的扩展,封装出多个适用范围不同的业务处理流程。在这一层,也会使用泛型,但会更加具体。
而在实现层不会再使用泛型,而是给出真正的类型,并实现具体的逻辑。
/** 抽象层,指定泛型 K,V */
abstract class AbstractGenericImpl<K,V> implements IGeneric<Map<K,V>> {
public Map<K,V> map = new HashMap<>();
}
/** 基础层,继承 AbstractGenericImpl */
class BasicGenericImpl<T> extends AbstractGenericImpl<String,TestDO<T>>{
@Override
public void test(Map<String,TestDO<T>> data) {
map.putAll(data);
}
}
/** 实现层,继承 BasicGenericImpl */
public class ComplexGenericImpl<T extends HashSet> extends BasicGenericImpl<T> {
@Override
public void test(Map<String, TestDO<T>> data) {
super.test(data);
}
}
结语
泛型在当今时代的 Java 开发中已经被广泛应用,如 Spring、MyBatis、Dubbo 等开源框架都使用了大量泛型来实现通用功能。在开发中,既要掌握开源框架的泛型使用方法,也要熟悉泛型的定义和使用,并对泛型有足够深刻的理解。如果你要开发自己的框架,那么泛型一定是标配,需要好好理解和掌握。
本文首发于公众号程序之心,每天给你诚意满满的干货,欢迎关注。