Java模板,泛型,通配符使用

背景

模板,泛型,通配符的使用记录

下单示例

从一个下单的例子来记录下模板与通配符的使用

定义接口

下单一般流程为报文定义,报文核对,落表数据生成,落表,落表后操作等等这些,具体视业务情况而定,这里主要记录模板泛型通配符,后续就简化成只操作一个落表接口

报文接口

这里定义一个报文接口的目的是将不同渠道格式的报文定义一个统一的内容(比如直接返回部分初始化的数据表对象),这样实现类就不用依赖于具体的某个请求报文而是下单报文接口(依赖倒置)

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

/**
* 下单报文接口
* @author: east
* @date: 2023/11/23
*/
public interface OrderRequest<T extends OrderInfo> {
/**
* 将不同渠道报文转为T的统一格式数据(比如转为落表所需数据)
* 通过T extends OrderInfo限制模板类型为OrderInfo及其子类
*
* @return T 统一数据
*/
public T generateOrderInfo();
}

/**
* 账单通用数据
*/
public class OrderInfo {
protected BigDecimal amt;
protected String serial;
protected String rcvNo;

public OrderInfo(BigDecimal amt, String serial, String rcvNo) {
this.amt = amt;
this.serial = serial;
this.rcvNo = rcvNo;
}

public OrderInfo() {
}

@Override
public String toString() {
return "OrderInfo{" +
"amt=" + amt +
", serial='" + serial + '\'' +
", rcvNo='" + rcvNo + '\'' +
'}';
}
}

下单接口

这里简化一下,只定义落表接口。落表接口接收不同类型的要落表信息,但要落表信息都是基础落表信息(BaseSavedInfo.java)的子类,所以使用”extends SavedInfo”关键字去限制一下,具体的实现类则必须传入SavedInfo或者其子类。因此这里可以使用模板接口或者模板方法,定义方法,这两种方法各有其特点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 下单要落表的最基本信息,如果除了账单信息,其他渠道需要落特别的数据表,则基础本类并重写Save接口即可,满足开闭原则
* 账单信息
* 其他信息自行继承本类实现
*
* @author: east
* @date: 2023/11/25
*/
public class BaseSavedInfo {
protected OrderInfo orderInfo;

public BaseSavedInfo(OrderInfo orderInfo) {
this.orderInfo = orderInfo;
}

@Override
public String toString() {
return "SavedInfo{" +
"orderInfo=" + orderInfo +
'}';
}
}
模板方法接口Save

这里Save接口有一个模板方法save,他的方法模板类型限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 使用模板方法
*
* @author: east
* @date: 2023/11/23
*/
public interface Save {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public <T extends BaseSavedInfo> void save(T savedInfo);
}
模板接口SaveT

这里定义的为一个模板接口SaveT,模板范围限制在BaseSavedInfo及其子类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 保存下单相关信息, 模板类
*
* @author: east
* @date: 2023/11/23
*/
public interface SaveT<T extends BaseSavedInfo> {
/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
public void save(T savedInfo);
}

下单行为抽象类

这里设置一个下单行为的抽象类目的是为了将下单流程组装起来,因为涉及到Save接口作为依赖,所以接口不适用需要换为抽象类。这里抽象类的定义可以分为三种

使用Save接口的抽象下单类:方式一
1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class AbstractOrder<T extends OrderRequest<?>> {
// 使用Save接口作为其依赖
protected Save save;

public AbstractOrder(Save save) {
this.save = save;
}

/**
* 下单接口;T为实现了OrderRequest接口的请求报文
*/
public abstract void order(T orderRequest);
}
  • 这里的”<T extends OrderRequest<?>” 表示AbstractOrder是一个模板类,且其实现类的模板类型限制在OrderRequest接口,这样做的好处是所有下单虚函数的实现类都实现了OrderRequest接口,从而统一下单行为,满足同一个标准(当然也可以不指定),后面的’?’表示任意通配符,也就是OrderRequest接口的泛型这里不关心

  • AbstractOrder抽象下单类使用Save接口作为其依赖因此直接定义Save类型的成员变量即可。如果是SaveT类型则需要特别处理

  • “public abstract void order(T orderRequest);” 表示这是一个抽象下单接口,接收一个模板类型参数,也就是实现了OrderRequest接口的请求报文体。该报文体根据下单渠道自行定义

使用SaveT接口的抽象下单类: 方式二
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class AbstractOrderTR<T extends OrderRequest<?>, R extends BaseSavedInfo> {
// 使用SaveT接口作为其依赖
// 用模板指定了参数的类型为R
protected SaveT<R> save;

public AbstractOrderTR(SaveT<R> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,而且在类上通过模板”R extends BaseSavedInfo”指定SaveT模板类型为BaseSavedInfo及其子类
使用SaveT接口的抽象下单类: 方式三
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public abstract class AbstractOrderT<T extends OrderRequest<?>> {
// 使用SaveT接口作为其依赖
// 不通过了类模板指定,仅用通配符参数限定,
protected SaveT<? extends BaseSavedInfo> save; // note 如果实现类中不指定SaveT的类型,则save接口无法使用
// 原因: 编译器没那么聪明根据子类传入的saveT子对象推断出SaveT模板具体对应哪一个类,只能在这里明确指定或者子类重写

// 也可以为SaveT<?> 仅通配符不做限制
// protected SaveT<?> save;

// 也可以不限制,仅使用SaveT
// protected SaveT save;

public AbstractOrderT(SaveT<? extends BaseSavedInfo> save) {
this.save = save;
}

/**
* 下单
*/
public abstract void order(T orderRequest);
}
  • 这里AbstractOrderTR抽象下单类使用SaveT接口作为其依赖,但不像方式二通过模板来限制SaveT模板类型,而是使用SaveT<? extends BaseSavedInfo>通配符,来限制该模板类型仅为BaseSavedInfo及其子类,当然也可以不限制仅用SaveT<?>和SaveT。但是SaveT和(SaveT<?>,SaveT<? extends BaseSavedInfo>)不一样,SaveT并没有使用通配符,它更类似于方式一的用法,只是一个基类。而SaveT<?>和SaveT<? extends BaseSavedInfo>因为都作为通配符使用,所以在AbstractOrderTR的实现类AbstractOrderTRImp中不能直接使用,因为编译器并不能确定此处SaveT的具体类型,详见

定义实现

这里用外部渠道下单和内部渠道下单来模拟实现。一些实体类定义不具体展示,详细可在这里查看完整包

内部渠道下单实现类

InnerOrder类继承了抽象下单类AbstractOrder(方式一),并指定了模板类型为InnerOrderRequest(内部请求报文)

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
57
58
59
60
61
62
63
64
/**
* 内部下单实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerOrder extends AbstractOrder<InnerOrderRequest> {

// InnerSaveOrder为Save接口实现类
public InnerOrder(InnerSaveOrder save) {
super(save);
}

//InnerOrderRequest为OrderRequest实现类
@Override
public void order(InnerOrderRequest innerOrderRequest) {
try {

// 校验等接口调用....

// InnerBaseSavedInfo为内部渠道需要落表的数据,是BaseSavedInfo的子类
// 落表 这里save为模板方法
save.save(new InnerBaseSavedInfo(innerOrderRequest.generateOrderInfo(), "this is inner special message"));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// InnerSaveOrder innerSaveOrder = new InnerSaveOrder();
// InnerOrder innerOrder = new InnerOrder(innerSaveOrder);
// innerOrder.order(new InnerOrderRequest(BigDecimal.ONE, "innerSerial", "rcvNo"));
// }

}

/**
* 内部渠道保存账单
* Save接口的实现类
*
* @author: east
* @date: 2023/11/24
*/
public class InnerSaveOrder implements Save {

/**
* 保存下单数据
*
* @param savedInfo 相关信息:账单表,统计表
*/
@Override
public <T extends BaseSavedInfo> void save(T savedInfo) {

// 落表
System.out.println("内部渠道下单信息开始落表");

// todo 这里有个很大的问题就是如果不通过强制类型转换无法拿到特定种类的数据
// InnerSavedInfo innerSavedInfo=(InnerSavedInfo) savedInfo;
// System.out.println("内部渠道特有数据获取: "+innerSavedInfo.getSpecialMsg());

System.out.println(savedInfo.toString());
}
}
  • 从代码里面可以看到Save接口采用模板方法这种方式有个麻烦的地方在于实现类中因为是重写save方法所以也必须是模板方法,虽然这时已经明确知道参数类型为BaseSavedInfo的实现类InnerSavedInfo,但是在方法中无法直接获取其特定属性(比如获取InnerSavedInfo子类独有的字段),暂时没明白除了强制类型转换外该怎么解决

外部渠道下单实现类(AbstractOrderTR)

OuterOrderTR继承了下单虚函数AbstractOrderTR(方式二),并指定了AbstractOrderTR的两个模板为OuterOrderRequest和OuterBaseSavedInfo,从而明确指定了抽象类的SaveT这个成员函数的模板类型为OuterBaseSavedInfo

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
public class OuterOrderTR extends AbstractOrderTR<OuterOrderRequest, OuterBaseSavedInfo> {

public OuterOrderTR(OuterSaveOrderImp save) {
super(save);
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// note 这里通过模板类全量指定
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderTR orderAction = new OuterOrderTR(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialTR", "rcvNo"));
// }

}

外部渠道下单实现类(AbstractOrderT)

这里OuterOrderT继承了AbstractOrderT虚函数方式三,成员函数SaveT<? extends BaseSavedInfo> save只通过上界通配符限定,我一开始觉得编译器能通过传入的OuterBaseSavedInfo类对象来初始化SaveT对象时推断出SaveT的模板类型为OuterBaseSavedInfo而不是InnerBaseSavedInfo(BaseSavedInfo的另一个实现类),但实际上编译器好像无法推测出save的类型导致SaveT对象的public void save(T savedInfo)方法无法接收任何参数(除了null),这里和List<? extends Number> list无法list.add(1)问题很像,调用save方法时会导致类型安全问题,无法编译通过。

相关解释

解决办法为明确指定SaveT模板类型

1
protected SaveT<OuterBaseSavedInfo> save;

类定义如下

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
public class OuterOrderT extends AbstractOrderT<OuterOrderRequest> {

protected SaveT<OuterBaseSavedInfo> save; // note 通配符如果不在模板指定,则只有在这里明确指定,save的方法才能正常调用, 可以将这行去掉试试

public OuterOrderT(OuterSaveOrderImp save) {
super(save);
this.save = save;
}

@Override
public void order(OuterOrderRequest outerOrderRequest) {
try {

// 校验等逻辑....

// 落表
// 通过父类调用 note: 这里没有通过模板指定(如果也不在成员函数指定的话则会报错,? extends通配只是占位,不指定类型则无法操作
save.save(new OuterBaseSavedInfo(outerOrderRequest.generateOrderInfo()));

} catch (Exception e) {
throw new RuntimeException(e);
}
}

// public static void main(String[] args) {
// OuterSaveOrderImp saveOrderAction = new OuterSaveOrderImp();
// OuterOrderT orderAction = new OuterOrderT(saveOrderAction);
// orderAction.order(new OuterOrderRequest(BigDecimal.ONE, "outSerialT", "rcvNo"));
// }

}

总结

  • 上面的三个实现类大概展示了通配符与模板使用的示例,以及一些设计原则(依赖倒置,接口隔离,单一,开闭)的使用,以及其中的一些坑,建议大家结合具体的示例或实际的业务来理解。

  • 对于上界和下界通配符,个人感觉<? extends ClassA>这种用得多一点,这种相当于指定了一个规范ClassA,后续传入的参数都要实现这个规范ClassA或者继承ClassA。至于上下界的定义为什么那么叫,个人感觉没那么重要

完整代码见:

简略版

详细版