根据一个java泛型使用问题思考的泛型模式设计总结

根据一个java泛型使用问题思考的泛型模式设计总结

[TOC]

0x01. 问题的背景

最近在写一个网联联合运维的前置程序,由于TPS和系统的稳定性要求不像实时交易系统那么高,功能也比较简单,因此针对通讯部分尝试gitee上一个使用了aio的开源项目(smartsocket)来代替netty做tcp通讯的处理。

考虑到小框架可能带来的不稳定性,所以编写的时候,把tcpServer和tcpClient都抽象成为单独的interface,然后使用smartsocket来进行接口的实现。这样万一框架出现问题,可以在不影响功能的情况下,单独替换实现类。

在抽象TcpClient接口的时候,使用了如下写法:

//TcpClient的实现接口,目前只使用了smartsocket来实现,如果不稳,后续可以改为netty或者直接写socket也可以
public interface TcpServer{

    public void listen(int port, int maxRead) throws IOException;

    <T> void send(T session,  String sendMessage) throws IOException;

    <T> String recv(T session) throws IOException;

    public void close();
}

这里没有使用常见的类或接口上泛型,而是使用了泛型方法 ,网上也有帖子总结的写法也很到位,写法如下:

public interface Test {
    <E> int aaa(E e);
}

public class Demo implements Test {
    @Override
    public <E> int aaa(E e) {
        return 0;
    }
}
————————————————
版权声明:本文为CSDN博主「CodingBugs」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_33061377/article/details/79617513

然后在implements接口的具体方法,使用了如下写法:

public class TcpServerSmartSocket implements TcpServer{
    private final Logger logger = LoggerFactory.getLogger(TcpServerSmartSocket.class);

    //省略其他方法

    @Override
    public <AioSession> void send(AioSession aioSession, String sendMessage) throws IOException {
        // 报文体
        byte[] msgBody = sendMessage.getBytes();
        // 报文头
        byte[] msgHead = String.format("%08d", msgBody.length).getBytes();
        logger.info("行内发送报文头:[{}][{}]", new String(msgHead), msgHead.length);

        aioSession.writeBuffer().write(msgHead);
        aioSession.writeBuffer().write(msgBody);
        aioSession.writeBuffer().flush();
    }
}

IDEA提示:

aioSession.writeBuffer() 的方法是红色的,编译也会报错。

0x02. 问题的分析解决

IDEA点击会发现提示他是继承于Objects的,也就是AioSession完全没有被作为一个实际对象。

进一步思考和分析网上的例子,会发现这种写法实际上,还是对类型的声明,也就是说没有区别,就是起的泛型的名字不同。而泛型的使用,应该是在类型的调用时(实例化)生效。也就是说,我可以在调用send接口实现的时候,传入具体的类型,但是不能在send实现声明的时候约定类型。

考虑到我的需求很简单,就是希望AioSession不被外部调用者知道,因此使用类似C语言的 void*传值,就可以解决问题,因此修改的一种办法就是,去掉泛型声明,接口直接使用Object session来传值,实现类中进行强制转换,代码如下:

//TcpClient的实现接口,目前只使用了smartsocket来实现,如果不稳,后续可以改为netty或者直接写socket也可以
public interface TcpServer{

    public void listen(int port, int maxRead) throws IOException;

    void send(Object session,  String sendMessage) throws IOException;

    public void close();
}
public class TcpServerSmartSocket implements TcpServer{
    private final Logger logger = LoggerFactory.getLogger(TcpServerSmartSocket.class);

    //省略其他方法

    @Override
    public void send(Object session, String sendMessage) throws IOException {
        aioSession = (AioSession)session; // 强制换换成AioSession
        // 报文体
        byte[] msgBody = sendMessage.getBytes();
        // 报文头
        byte[] msgHead = String.format("%08d", msgBody.length).getBytes();
        logger.info("行内发送报文头:[{}][{}]", new String(msgHead), msgHead.length);

        aioSession.writeBuffer().write(msgHead);
        aioSession.writeBuffer().write(msgBody);
        aioSession.writeBuffer().flush();
    }
}

问题解决了,和C语言的写法类似,但是这样使用Object强制转换的方式,明显的不符合java的编程手法。于是使用了通用的泛型写法,第二种方法就是在类或接口上泛型,具体的代码如下:

//TcpClient的实现接口,目前只使用了smartsocket来实现,如果不稳,后续可以改为netty或者直接写socket也可以
public interface TcpServer<T> {

    public void listen(int port, int maxRead) throws IOException;

    void send(T session,  String sendMessage) throws IOException;

    String recv(T session) throws IOException;

    public void close();
}
public class TcpServerSmartSocket implements TcpServer<AioSession> { //实现接口的时候,指定类型
    private final Logger logger = LoggerFactory.getLogger(TcpServerSmartSocket.class);

    @Override
    public void send(AioSession aioSession, String sendMessage) throws IOException {
        // 报文体
        byte[] msgBody = sendMessage.getBytes();
        // 报文头
        byte[] msgHead = String.format("%08d", msgBody.length).getBytes();
        logger.info("行内发送报文头:[{}][{}]", new String(msgHead), msgHead.length);

        aioSession.writeBuffer().write(msgHead);
        aioSession.writeBuffer().write(msgBody);
        aioSession.writeBuffer().flush();
    }
}

这里有一个小点implements TcpServer<AioSession>声明类的时候,直接指定了类型,而不是在TcpServerSmartSocket初始化的时候指定的,这样本质写法和方法的声明很相似,但是java针对这种类接口实现指定类型,内部应该有关联的机制或者语法糖,请教java语言专家可能有更深入的理解,这里不做无谓的深究。

总结起来,如果使用泛型的模式,那么从java角度,主要是为了限定同一个接口或者类的多个方法的参数或者返回值的类型。因此如果如果只有一个send方法,其实用Object来进行参数转换,也是可以的,但是如果 send,recv,new的时候都用到了同一个类型,则应该加以限定。

0x03. C语言的不透明指针

java泛型的使用场景,与之前在编写C语言的一些开源组件的时候,用到的C语言的不透明指针有异曲同工的作用。

首先针对通用类型,一般会写成void *类型,来减少头文件的依赖,如下文举的hashmap的实现库的最初方法:

定义:(返回void *)

void * XipHashmapInit( int opacity, float factor, int malloc_flag)
{
    TxipHashmap * map = ( TxipHashmap *)hmap_malloc(sizeof( TxipHashmap));

    //各种申请动作

    return (void *)map;
}

使用: (传入void *)

void * XipHashmapPut( void * hashmap, char * key, void * value, int size)
{
    TxipHashmap * map = (TxipHashmap *)hashmap;
    //...
}

void * XipHashmapGet( void * hashmap, char *key)
{
    TxipHashmap * map = (TxipHashmap *) hashmap;
    //..
}

这样的好处是,TxipHashmap可以完全不暴露给外部调用者使用,但是坏处也比较明显,就是如果void * 调用者如果传入的不是TxipHashmap,也是无法检查的,很容易会产生core dump的问题。因此增加不透明指针来解决这个问题的方法如下:

  1. 不透明指针头文件声明:
// 不透明指针
typedef struct _TxipHashmap TxipHashmap;
typedef TxipHashmap * TxipHashmapPtr;

TxipHashmap * XipHashmapNew();
  1. 定义
TxipHashmap * XipHashmapInit( int opacity, float factor, int malloc_flag)
{
    TxipHashmap * map = ( TxipHashmap *)hmap_malloc(sizeof( TxipHashmap));

    //各种申请动作

    return map;
}
  1. 使用
void * XipHashmapPut( TxipHashmap * hashmap, char * key, void * value, int size)
{
    //...
}

void * XipHashmapGet( TxipHashmap * hashmap, char *key)
{
    //..
}

这样就可以限定,在使用函数的时候,传入的参数必须是Init或者New的返回值,可以极大的在编写和编译时检查和减少传值错误。

0x04. 泛型的设计思考

通过JAVA的泛型思考和C的指针来进一步思考,进一步抽象为泛型程序设计的使用场景,目前总结如下;

  1. 泛型的定义应该是在强类型语言中,尤其在编写框架和组件的时候,兼容多种类型数据的一种简单写法。
  2. 泛型的实现本质上,是允许声明时不指定类型,但是在具体使用时(实例化)时再指定针对于该场景的类型。
  3. 泛型的使用场景,应该是约定一个接口中多个方法的参数,将公用的类型做一个集合式的限定。最常见的如流程是:构造或初始化返回,后续使用的时候要求传入
  4. 单个方法的泛型,使用的价值很小,可以不用或者使用Object/void*等做替换。