泛型类与泛型方法

作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

联系qq:184480602,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬

回顾泛型类

我们来回顾一下泛型类是怎么出现的。

话说JDK1.5以前,还没引入泛型时ArrayList大概是这样的:

public class ArrayList {
    private Object[] array;
    private int size;
    public void add(Object e) {...}
    public void remove(int index) {...}
    public Object get(int index) {...}
}

它有这个最大的问题是:无法限制传入的元素类型、取出时又容易发生ClassCastException。最直观的做法是使用期望类型的元素数组,比如:

public class StringArrayList {
    // 因为这种ArrayList只存String,所以不需要用Object[]兼容所有类型,只要String[]即可
    private String[] array;
    private int size;
    public void add(String e) {...}
    public void remove(int index) {...}
    public String get(int index) {...}
}

但不可能为所有类型都编写专门的XxxArrayList,于是JDK就推出了泛型:抽取一种类模板,方法、变量都写好了,但变量的类型抽取成“形式类型参数”:

public class ArrayList<T> {
    private Object[] array; // 本质还是Object数组,只对add/get做约束。就好比垃圾桶本身并没有变化,只是加了检测器做投递约束
    private int size;
    public void add(T e) {...}
    public void remove(int index) {...}
    public T get(int index) {...}
}

再配合编译器做约束:比如ArrayList<User>表示告诉编译器,帮我盯着点,我只存取User类型的元素。

简而言之,泛型类的出现就是为了“通用”,而且还能在编译期约束元素的类型。

“泛型类的方法”并不是泛型方法

以前使用Hibernate时,基本都会抽取BaseDao:

class BaseDao<T> {
    public T get(T t) {
        
    }
    
    public boolean save(T t) {
        
    }
}

方法里的T和类上的T是同一个,也正因为多个方法操作的POJO类型一致,所以才会被抽取到类上统一声明。

这个T具体是什么类型,或者说里面的get()、save()到底操作什么类型的POJO,取决于BaseDao到底被谁继承。比如:

class UserDao extends BaseDao<User> {
}

那么UserDao从BaseDao继承过来的get()、save()其实已经被约束为“只能操作User类型的元素”。

但要清楚,上面的get()、save()只是“泛型类的方法”,而不是所谓的“泛型方法”。

什么是泛型方法

泛型类上的<T>其实是一种“声明”,如果把类型参数T也看做一种特殊的变量,那么<T>就是变量声明(不是我们一般概念中的变量声明,一个作用于运行期,一个作用于编译期)。

由于泛型类上已经声明了T,所以类中的字段、方法都可以自由使用T。但是当T被“赋值”为某种类型后,就会在编译器的帮助下形成一种强制类型约束,此时这个通用的代码模板也就不再通用了。因而你会发现,对于编译器而言UserDao extends BaseDao<User>里的方法只能操作User,CarDao extends BaseDao<Car>里的方法只能操作Car。

这好吗?一般来说,这很好,因为一个Dao操作的肯定是同一张表同一个对象,限定为某个类型反而能避免出错。但大家想想,如果不是BaseDao,而是一个工具类呢?BaseUtils提供通用的操作,XxUtils extends BaseUtils<Xx>固然没问题,但如果XxUtils希望提供一个方法处理Yy怎么办?如果还想处理Zz呢?

问题的症结并不在于后期想要处理什么类型或者有多少种类型,而是T被过早确定了,从而早早地放弃了“可变性”。

因为对于泛型类的T来说,当UserDao继承BaseDao或者XxUtils继承BaseUtils时,T就被确定为User和Xx了,且已经拒绝了其他可能性,也就无法复用于其他类型。

那么,有没有办法延迟T的确定呢?

有,但泛型类的T已经没办法了,需要另辟蹊径,引入泛型方法。

和泛型类一样,泛型方法使用类型参数前也需要“声明”(<T>要放在返回值的前面):

public class DemoForGenericMethod {

    public static void main(String[] args) {
        List<String> list = new ArrayList<>(Arrays.asList("1", "2", "3", "4", "5"));
        List<String> stringList = reverseList(list);
    }

    /**
     * 一个普通类,也可以定义泛型方法(比如静态泛型方法)
     * @param list
     * @param <T>
     * @return
     */
    public static <T> List<T> reverseList(List<T> list) {
        List<T> newList = new ArrayList<>();
        for (int i = list.size() - 1; i >= 0; i--) {
            newList.add(list.get(i));
        }
        return newList;
    }

}

泛型方法可以大致分为两种:静态泛型方法、普通泛型方法。上面定义静态泛型方法主要是因为main是static的,为了方便调用而已。

泛型方法中T的确定时机是使用方法时。

泛型方法和泛型类没有必然联系,你可以理解为这两个东西可以各自使用,也可以硬把它们凑在一块,不冲突:

class BaseDao<T> {
    // 泛型类的方法
    public T get(T t) {

    }

    /**
     * 泛型方法,无返回值,所以是void。<E>出现在返回值前,表示声明E变量
     * @param e
     * @param <E>
     */
    public <E> void methodWithoutReturn(E e) {
        
    }

    /**
     * 泛型方法,有返回值。入参和返回值都是V。注意,即使这个方法也用E,也和上面的E不是同一个
     * @param v
     * @param <V>
     * @return
     */
    public <V> V methodWithReturn(V v) {
        return v;
    }
}

泛型类与泛型方法的使用场景

简单来说,一个类拥有多种同类型方法时使用泛型类,一个方法处理多种类型时使用泛型方法。

比如,在做数据访问层的时候,对一种类型的实体有一系列统一的访问方法,此时采用泛型类会比较合适,而对于接口的统一结果封装则采用泛型方法比较合适,比如:

@Data
@NoArgsConstructor
public class Result<T> implements Serializable {

    private Integer code;
    private String message;
    private T data;

    private Result(Integer code, String message, T data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    private Result(Integer code, String message) {
        this.code = code;
        this.message = message;
        this.data = null;
    }

    /**
     * 带数据成功返回
     * 请注意,虽然泛型方法也用了T,但和Result<T>里的没关系
     * 这里之所以这么写,是因为实际开发时你们会见到很多这种“迷惑性”的写法,放出来作为“反例”,推荐最好使用其他符号,比如K
     *
     * @param data
     * @param <T>
     * @return
     */
    public static <T> Result<T> success(T data) {
        return new Result<>(ExceptionCodeEnum.SUCCESS.getCode(), ExceptionCodeEnum.SUCCESS.getDesc(), data);
    }
}

关于统一结果封装,请参考小册其他章节。

又比如封装工具类,也非常常用:

public class ConvertUtil {

    private ConvertUtil() {
    }

    /**
     * 将List转为Map
     *
     * @param list         原数据
     * @param keyExtractor Key的抽取规则
     * @param <K>          Key
     * @param <V>          Value
     * @return
     */
    public static <K, V> Map<K, V> listToMap(List<V> list, Function<V, K> keyExtractor) {
        if (list == null || list.isEmpty()) {
            return new HashMap<>();
        }
        Map<K, V> map = new HashMap<>(list.size());
        for (V element : list) {
            K key = keyExtractor.apply(element);
            if (key == null) {
                continue;
            }
            map.put(key, element);
        }
        return map;
    }
}

静态方法无法使用泛型类的类型参数,换言之,静态方法如果想要使用泛型,只能是静态泛型方法,此时类型参数是自己声明的。

一个要留心的骚操作

在后面我们会学习到自己基于Redis做一个分布式锁:

public interface RedisService {

    // 省略其他方法...

    /**
     * 从队列取出消息
     *
     * @param queue    自定义队列名称
     * @param timeout  最长阻塞等待时间
     * @param timeUnit 时间单位
     * @return
     */
    Object popQueue(String queue, long timeout, TimeUnit timeUnit);
    
    // 省略其他方法...
}

使用时:

但如果使用泛型方法:

public interface RedisService {

    // 省略其他方法...

    /**
     * 从队列取出消息
     *
     * @param queue    自定义队列名称
     * @param timeout  最长阻塞等待时间
     * @param timeUnit 时间单位
     * @return
     */
     <T> T popQueue(String queue, long timeout, TimeUnit timeUnit);
    
    // 省略其他方法...
}

就不用强制转换了。

但这并不保险,只是骗过了编译器而已。你会发现你用任意类型接收都是不会编译报错的:

个人不推荐这种写法。

补充:泛型数组

在《泛型概述(上)》的末尾我提到过,Java并不支持泛型数组,所以对于ArrayList,底层仍旧采用Object[]。但在日常开发中又确实可能遇到需要new T[]的场景。

public class Test {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 4, 5, 9, 7, 1, 1, 6};
        Integer[] copyArray = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        // 创建同类型数组
        E[] arrayTemp = ...
        
        for (int i = 0; i < array.length; i++) {
            arrayTemp[i] = array[i];
        }
        return arrayTemp;
    }
    
}

和new T()一样,new T[]也是不合法的。

要想new T(),我们之前介绍过,要么方法传入Class<T>,要么通过GenericType获取,最终要通过反射创建T对象(总之要明确Class对象)。那么对于T[],如何获取数组的元素类型呢?

JDK另外提供了所谓的ComponentType指代数组的元素类型:

泛型机制对普通对象、容器对象都很好,唯独对数组苛刻了些,以至于我们回想泛型,基本都想不起数组和泛型有啥关系。这不,为了弥补对数组的亏欠,Java特别给数组搞了ComponentType,其他两个可没有哦。

好了,既然知道了数组元素的类型,那就可以创建对应类型的数组咯:

public class Test {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 4, 5, 9, 7, 1, 1, 6};
        Integer[] copyArray = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        Class<?> componentType = array.getClass().getComponentType();
        
        // 创建同类型数组
        E[] arrayTemp = (E[]) Array.newInstance(componentType, array.length);
        
        for (int i = 0; i < array.length; i++) {
            arrayTemp[i] = array[i];
        }

        return arrayTemp;
    }
    
}

当然,实际开发中要想拷贝数组,有很多其他简单的方式:

public class Test {

    public static void main(String[] args) {
        Integer[] array = new Integer[]{2, 4, 5, 9, 7, 1, 1, 6};
        Integer[] copyArray = copy(array);
    }

    private static <E> E[] copy(E[] array) {
        return Arrays.copyOfRange(array, 0, array.length);
    }
    
}
作者简介:大家好,我是smart哥,前中兴通讯、美团架构师,现某互联网公司CTO

进群,大家一起学习,一起进步,一起对抗互联网寒冬

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/180452.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Dubbo引入Zookeeper等注册中心简介以及DubboAdmin简要介绍,为后续详解Dubbo各种注册中心做铺垫!

文章目录 一&#xff1a;Dubbo注册中心引言 1&#xff1a;什么是Dubbo的注册中心&#xff1f; 2&#xff1a;注册中心关系图解 3&#xff1a;引入注册中心服务执行流程 4&#xff1a;Dubbo注册中心好处 5&#xff1a;注册中心核心作用 二&#xff1a;注册中心实现方案 …

YOLOv5改进: Inner-IoU基于辅助边框的IoU损失,高效结合 GIoU, DIoU, CIoU,SIoU 等 | 2023.11

💡💡💡本文独家改进:Inner-IoU引入尺度因子 ratio 控制辅助边框的尺度大小用于计算损失,并与现有的基于 IoU ( GIoU, DIoU, CIoU,SIoU )损失进行有效结合 推荐指数:5颗星 新颖指数:5颗星 💡💡💡Yolov5/Yolov7魔术师,独家首发创新(原创),适用于…

低压配电柜浪涌保护器综合选型方案

地凯科技低压配电柜是指在额定电压不超过1000V的交流电力系统中&#xff0c;用于接受和分配电能&#xff0c;控制、保护和监测电路的装置。低压配电柜广泛应用于工业、商业、住宅等领域&#xff0c;是电力系统的重要组成部分。 然而&#xff0c;低压配电柜也面临着来自外部和内…

【深度学习】神经网络术语:Epoch、Batch Size和迭代

batchsize&#xff1a;中文翻译为批大小&#xff08;批尺寸&#xff09;。 简单点说&#xff0c;批量大小将决定我们一次训练的样本数目。 batch_size将影响到模型的优化程度和速度。 为什么需要有 Batch_Size : batchsize 的正确选择是为了在内存效率和内存容量之间寻找最…

大白话DDD(DDD黑话终结者)

大白话DDD&#xff08;DDD黑话终结者&#xff09; 一、吐槽的话 相信听过DDD的人有很大一部分都不知道这玩意具体是干嘛的&#xff0c;甚至觉得它有那么一些虚无缥缈。原因之一是但凡讲DDD的&#xff0c;都是一堆特别高大上的概念&#xff0c;然后冠之以一堆让人看不懂的解释…

好用的团队协同办公软件推荐!企业办公必备!

有什么好用的团队协同办公软件可以推荐&#xff1f; 想要的办公软件需要满足“即时通讯”、“多端适配”、“项目管理”、“文件传输”这4大能力。 下面就给大家分享3大类能够满足题主需求的企业级办公软件&#xff0c;免费的付费的都有&#xff0c;也都是侧重的不同领域&…

算法的奥秘:种类、特性及应用详解(算法导论笔记1)

算法&#xff0c;是计算机科学领域的灵魂&#xff0c;是解决问题的重要工具。在算法的世界里&#xff0c;有着各种各样的种类和特性。今天&#xff0c;我将带各位踏上一段探索算法种类的旅程&#xff0c;分享一些常见的算法种类&#xff0c;并给出相应的实践和案例分析。希望通…

opencv-分水岭算法分割

原理 任何一副灰度图像都可以被看成拓扑平面&#xff0c;灰度值高的区域可以被看成是山峰&#xff0c;灰度值低的区域可以被看成是山谷。我们向每一个山谷中灌不同颜色的水。随着水的位的升高&#xff0c;不同山谷的水就会相遇汇合&#xff0c;为了防止不同山谷的水汇合&#x…

年轻有为!2023两院院士增选揭榜 45岁颜宁当选

大家好&#xff0c;我是极智视界&#xff0c;欢迎关注我的公众号&#xff0c;获取我的更多前沿科技分享 邀您加入我的知识星球「极智视界」&#xff0c;星球内有超多好玩的项目实战源码和资源下载&#xff0c;链接&#xff1a;https://t.zsxq.com/0aiNxERDq 通常&#xff0c;两…

梨花声音教育,美食视频配音再次挑战味蕾

在为美食视频进行配音时&#xff0c;配音艺术家的目标是通过声音来激活观众的感官&#xff0c;唤起他们对美味佳肴的渴望&#xff0c;同时展现食物的诱人特色和烹饪的艺术性。配音应当能够描绘美食的丰富细节&#xff0c;传达烹饪的趣味性以及食材的高品质。以下是一些为美食视…

都被“锟斤拷”毒害过,那么究竟是为什么会出现这些奇怪的字符?

不管是在工作中还是生活中&#xff0c;都被“锟斤拷”毒害过&#xff0c;比如这样&#xff1a; 或者这样&#xff1a; 还有这样&#xff1a; 那么究竟是为什么会出现这些奇怪的字符&#xff1f; ASCII编码 在计算机底层都是用0和1进行存储的&#xff0c;ASCII编码将所有的字母…

vivado联合modelsim测试覆盖率

&#xff08;1&#xff09;配置环境 安装modelsim和vivado。点击vivado菜单栏中的tools&#xff0c;在下拉选项中选择compile simulation libraries。simulator选项选择&#xff1a;modelsim simulator。compile library location表示编译库存放的路径。simulator executable p…

【Pytorch】Visualization of Feature Maps(3)

学习参考来自&#xff1a; Image Style Transform–关于图像风格迁移的介绍github&#xff1a;https://github.com/wmn7/ML_Practice/tree/master/2019_06_03 文章目录 风格迁移 风格迁移 风格迁移出处&#xff1a; 《A Neural Algorithm of Artistic Style》&#xff08;ar…

JS 判断元素是否为空

判断元素是否为空&#xff1a; /*** 判断是否为空*/ export function validatenull(val) {if (typeof val boolean) {return false}if (typeof val number) {return false}if (val instanceof Array) {if (val.length0) return true} else if (val instanceof Object) {if (…

PC8223(CC/CV控制)高耐压输入5V/3.4A同步降压电路内建补偿带恒流恒压输出

概述 PC8233&#xff08;替代CX8853&#xff09;是一款同步降压调节器,输出电流高达3.4A,操作范围从8V到32V的宽电源电压。内部补偿要求最低数量现成的标准外部组件。PC8233在CC&#xff08;恒定输出电流&#xff09;模式或CV&#xff08;恒定输出电压&#xff09;模式&#x…

uniapp项目开发的功能点

一.手机 判断什么手机 const platform uni.getSystemInfoSync().platform;//platform ios什么机型 const model uni.getSystemInfoSync().model //model.toindex(iPhone)二.授权登录 授权登录有2种方式 &#xff08;一&#xff09;静默授权 就直接通过uni.login 获取c…

大模型AI Agent 前沿调研

前言 大模型技术百花齐放&#xff0c;越来越多&#xff0c;同时大模型的落地也在紧锣密鼓的进行着&#xff0c;其中Agent智能体这个概念可谓是火的一滩糊涂。 今天就分享一些Agent相关的前沿研究&#xff08;仅限基于大模型的AI Agent研究&#xff09;&#xff0c;包括一些论…

解决kubernetes中微服务pod之间调用失败报错connection refused的问题

现象&#xff1a; 从这里可以看到是当前服务在调用product service服务是出现了连接拒绝connection refused 走读一下原始代码&#xff1a; 可以看到请求是由FeignClient代理发出的 &#xff0c;但问题在于为什么Feign请求的时候会产生connection refused错误&#xff1f; 上…

2014年9月26日 Go生态洞察:使用Docker部署Go服务器

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

最近整理一份steam搬砖的项目操作细节和详细要求

csgo饰品搬砖Steam饰品搬砖全套操作流程之如何卖货 一、国外Steam游戏装备汇率差项目 这个项目的基本原理是 购买国外Steam游戏平台上的装备&#xff0c;再在国内网易Buff平台上或国际站csgo饰品平台进行售卖。从充值汇率和两个平台的装备价格差中获得利润。 二、需要准备的硬…