前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
社区首页 >专栏 >死磕Java泛型(一篇就够)

死磕Java泛型(一篇就够)

作者头像
三好码农
发布于 2019-06-15 07:56:24
发布于 2019-06-15 07:56:24
1.2K00
代码可运行
举报
运行总次数:0
代码可运行

Java泛型,算是一个比较容易产生误解的知识点,因为Java的泛型基于擦除实现,在使用Java泛型时,往往会受到泛型实现机制的限制,如果不能深入全面的掌握泛型知识,就不能较好的驾驭使用泛型,同时在阅读开源项目时也会处处碰壁,这一篇就带大家全面深入的死磕Java泛型。

泛型擦除初探

相信泛型大家都使用过,所以一些基础的知识点就不废话了,以免显得啰嗦。 先看下面的一小段代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class FruitKata {
    class Fruit {}
    class Apple extends generic.Fruit {}
    
    public void eat(List fruitList) {}

    public void eat(List<Fruit> fruitList) { }   // error, both methods has the same erasure
}

我们在FruitKata类中定义了二个eat的方法,参数分别是List和List<Fruit>类型,这时候编译器报错了,并且很智能的给出了“ both methods has the same erasure” 这个错误提示。显然,编译器在抱怨,这二个方法具有同样的签名,嗯~~,这就是泛型擦除存在的一个证据,要进一步验证也很简单。我们通过ByteCode Outline这个插件,可以很方便的查看类被编译后的字节码,这里我们只贴出eat方法的字节码。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  // access flags 0x1
  // signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V
  // declaration: void eat(java.util.List<generic.FruitKata$Fruit>)
  public eat(Ljava/util/List;)V

可以看到参数确实已经被擦除为List类型,这里要明确一点是,这里擦除的只是方法内部的泛型信息,而泛型的元信息还是保存在类的class字节码文件中,相信细心的同学已经发现了上面我特意将方法的注释一并贴了出来

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
 // signature (Ljava/util/List<Lgeneric/FruitKata$Fruit;>;)V

这个signature字段大有玄机,后面会详细说明。 这里只是以泛型方法来做个说明,其实泛型类,泛型返回值都是类似的,兄弟们可以自己动手试试看。

为什么用擦除来实现泛型

要回答这个问题,需要知道泛型的历史,Java的泛型是在Jdk 1.5 引入的,在此之前Jdk中的容器类等都是用Object来保证框架的灵活性,然后在读取时强转。但是这样做有个很大的问题,那就是类型不安全,编译器不能帮我们提前发现类型转换错误,会将这个风险带到运行时。 引入泛型,也就是为解决类型不安全的问题,但是由于当时java已经被广泛使用,保证版本的向前兼容是必须的,所以为了兼容老版本jdk,泛型的设计者选择了基于擦除的实现。 由于Java的泛型擦除,在运行时,只有一个List类,那么相对于C#的基于膨胀的泛型实现,Java类的数量相对较少,方法区占用的内存就会小一点,也算是一个额外的小优点吧。

泛型擦除带来的问题

由于泛型擦除,下面这些代码都不能编译通过

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
T t = new T();
T[] arr = new T[10];
List<T> list = new ArrayList<T>();
T instanceof Object
通配符

作为泛型擦除的补偿,Java引入了通配符

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
List<? extends Fruit> fruitList;
List<? super Apple> appleList;

这二个通配符很多同学都存在误解。

? extends

?extends Fruit 表示Fruit是这个传入的泛型的基类(Fruit是泛型的上界),还是以上面的Fruit和Apple为例,看下面这段代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
List<? extends Fruit> fruitList = new ArrayList<>();
fruitList.add(new Fruit());  //error

按照我们上面对? extends的理解,fruitList应该是可以添加一个Fruit的,但是编译器却给我们报错了。我第一次看到这里时也感觉不太好理解,我们来看个例子就能理解了。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
List<? extends Fruit>  fruitList = new ArrayList<>();
List<Apple> appleList = new ArrayList<>();
fruitList = appleList;
fruitList.add(new Fruit());   //error

如果fruitList允许添加Fruit,我们就将Fruit添加到了AppleList中了,这肯定是不能接受的。

? super

再来看个?super的例子

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
List<? super Apple> superAppleList = new ArrayList<>();
superAppleList.add(new Apple());
superAppleList.add(new Fruit());  // error

向superAppleList中添加Apple是可以的,添加Fruit还是会报错,好,上面我们说的这些就是 PECS 原则。

PECS

英文全称,Producer Extends Consumer Super,

  1. 如果需要一个只读的泛型集合,使用?extends T
  2. 如果需要一个只写的泛型集合,使用?super T

我自己是这样来理解通配符的

  1. 因为? extends T给外界的承诺语义是,这个集合内的元素都是T的子类型,但是到底是哪个子类型不知道,所以添加哪个子类型,编译器都认为是危险的,所以直接禁止添加。
  2. 因为? super T 给外界的承诺语义是,这个集合内的元素的下界是T,所以向集合中添加T以及T的子类型是安全的,不会破坏这个承诺语义。
  3. List<Fruit>, List<Apple> 都是List<? super Apple>的子类型。 List<Apple> 是List<? extends Apple>的子类型。

关于泛型的使用,Jdk中有很多经典的应用范例,比如Collections的copy方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public static <T> void copy(List<? super T> dest, List<? extends T> src) {
        int srcSize = src.size();
        if (srcSize > dest.size())
            throw new IndexOutOfBoundsException("Source does not fit in dest");

        if (srcSize < COPY_THRESHOLD ||
            (src instanceof RandomAccess && dest instanceof RandomAccess)) {
            for (int i=0; i<srcSize; i++)
                dest.set(i, src.get(i));
        } else {
            ListIterator<? super T> di=dest.listIterator();
            ListIterator<? extends T> si=src.listIterator();
            for (int i=0; i<srcSize; i++) {
                di.next();
                di.set(si.next());
            }
        }
    }
泛型擦除了,我们还能拿到泛型信息吗

前面我们提到过class字节码中会有个signature字段来保存泛型信息。我们新建一个泛型方法

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
    public <T extends Apple> T plant(T fruit) {
        return fruit;
    }

查看class文件的二进制信息,发现里面确实有Signature字段信息。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Signature�%<T:Lgeneric/FruitKata$Apple;>(TT;)TT;

既然泛型信息还是在class文件中,那我们有没有办法在运行时拿到呢? 办法肯定是有的。 来看一个例子

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  Class clazz = HashMap<String, Apple>(){}.getClass();
  Type superType = clazz.getGenericSuperclass();
  if (superType instanceof ParameterizedType) {
  ParameterizedType parameterizedType = (ParameterizedType) superType;
  Type[] actualTypes = parameterizedType.getActualTypeArguments();
   for (Type type : actualTypes) {
            System.out.println(type);
       }
   }

// 打印结果
class java.lang.String
class generic.FruitKata$Apple

可以看到我们拿到并打印了泛型的原始类型信息。为了加深对泛型使用的理解,我接下来再看几个小例子。

泛型在Gson解析中的使用
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
String jsonString = ".....";  // 这里省略json字符串
Apple apple = new Gson().fromJson(jsonString, Apple.class);

这是一段很简单的Gson解析使用代码,我们进一步去看它fromJson的方法实现

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  public <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
    Object object = fromJson(json, (Type) classOfT);
    return Primitives.wrap(classOfT).cast(object);
  }

最终会执行到

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
  TypeToken<T> typeToken = (TypeToken<T>) TypeToken.get(typeOfT);
  TypeAdapter<T> typeAdapter = getAdapter(typeToken);
  T object = typeAdapter.read(reader);

通过我们传入的Class类型构造TypeToken,然后通过TypeAdapter将json字符串转化为对象T,中间的细节这里就不继续深入了。

泛型在retrofit中的使用

我们在使用retrofit时,一般都会定义一个或多个ApiService接口类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);

接口方法的返回值都使用了泛型,所以注定在编译期是要被擦除的,那retrofit是如何得到原始泛型信息的呢。其实有上面的泛型知识以及Gson的使用说明,相信大家以及有答案了。 retrofit框架本身设计的很优雅,细节这里我们不深入展开,这里我们只关心泛型数据转换为返回值的过程。 我们需要定义如下几个类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// ApiService.class
public interface ApiService {
    Observable<List<Apple>> getAppleList();
}

// Apple.class
class Apple extends Fruit {
    private int color;
    private String name;
    public Apple() {}

    public Apple(int color, String name) {
        this.color = color;
        this.name = name;
    }

    @Override
    public String toString() {
        return "color:" + this.color + "; name:" + name;
    }
}

接下来,我定义一个动态代理,

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
InvocationHandler handler = new InvocationHandler() {
       @Override
       public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            Type returnType = method.getGenericReturnType();
            if (returnType instanceof ParameterizedType) {
               ParameterizedType parameterizedType = (ParameterizedType) returnType;
               Type[] types = parameterizedType.getActualTypeArguments();
               if (types.length > 0) {
                   Type type = types[0];
                   Object object = new Gson().fromJson(mockAppleJsonString(), type);
                   return Observable.just(object);
             }
           }
          return null;
     }
  };

// mock json数据
public static String mockAppleJsonString() {
   List<Apple> apples = new ArrayList<>();
   apples.add(new Apple(1, "红富士"));
   apples.add(new Apple(2, "青苹果"));
   return new Gson().toJson(apples);
}

接下来就是正常的调用了,这里模拟了retrofit数据转换的过程。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
ApiService apiService = (ApiService) Proxy.newProxyInstance(ProxyKata.class.getClassLoader(),
                new Class[] {ApiService.class}, handler);

Observable<List<Apple>> call = apiService.getAppleList();
if (call != null) {
      call.subscribe(apples -> {
           if (apples != null) {
              for (Apple apple : apples) {
                 System.out.println(apple);
              }
         }
     });
}

// 输出结果
color:1; name:红富士
color:2; name:青苹果
泛型在MVP中的应用

MVP模式相信做Android开发的没人不知道,假设我们有这样几个类

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class BaseActivity<V extends IView, P extends IPresenter<V>> extends AppCompatActivity {
   protected P mPresenter;
  //....
}
public class MainActivity extends BaseActivity<MainView, MainPresenter> implements MainView {
  //....
}

由于泛型擦除的关系,我们不能在BaseActivity中直接新建Presenter来初始化mPresenter,所以一般通常的做法是暴露一个createPresenter方法让子类重写。但是今天我们介绍另外一种方法,直接看代码

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// BaseActivity.class
        Type superType = getClass().getGenericSuperclass();
        if (superType instanceof ParameterizedType) {
            ParameterizedType parameterizedType = (ParameterizedType) superType;
            Type[] types = parameterizedType.getActualTypeArguments();
            for (Type type : types) {
                if (type instanceof Class) {
                    Class clazz = (Class) type;
                    try {
                        mPresenter = (P) clazz.newInstance();
                        mPresenter.bindView((V) this);
                    } catch (IllegalAccessException e) {
                        e.printStackTrace();
                    } catch (InstantiationException e) {
                        e.printStackTrace();
                    }
                }
            }
        }

我们通过在BaseActivity中是能够拿到泛型的原始信息的,通过反射初始化出来mPresenter,并调用bindView来绑定我们的视图接口。通过这种方式,我们利用泛型的能力,基类包办了所有的初始化任务,不但逻辑简单,而且也体现了高内聚,在实际项目中可以尝试使用。

总结

深入理解Java泛型是工程师进阶的必备技能,希望你看了这篇文章,在今后,不论是面试还是其他的时候,谈到Java泛型时都能够云淡风轻,在使用泛型编写代码时也能够信手拈来。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2019.06.14 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redis数据库集群搭建 | 实践篇
前言 理论不多说了,图书馆又快要关门了。 -- 课设需要的环境,搭建记录分享! 还是说一句吧!redis跑内存的数据库,是解决数据交互高并发的解决方案。 ---- 场景 服务器系统:ubuntu 14.04 redis版本:redis 3.2.8 说明:为了方便我只在一台机器模拟多台主机 端口:6379、6380、6381、6382、6383、6384 ---- 下载Redis Redis官网,自行下载最新稳定版本。 wget -c http://download.redis.io/rele
AlicFeng
2018/06/08
7020
Redis从单机到集群,一步步教你环境部署以及使用
Redis作为缓存系统来说还是很有价值的,在大数据方向里,也是需要有缓存系统的。一般可以考虑tachyon或者redis,由于redis安装以及使用更简单,所以还是优先考虑了它。那么在一些场景下为了保证数据的可靠性,就需要采用集群的模式部署,因此本篇文章就基于Redis Cluster的背景讲解下部署以及后期的使用。 大致会包括下面的内容: Redis单机版的安装以及验证 Redis集群版的安装以及验证 使用图形化工具访问Redis 使用Jedis访问Redis 使用JedisCluster访问Red
用户1154259
2018/01/17
2K0
Redis从单机到集群,一步步教你环境部署以及使用
Redis-3.2.9集群配置(redis cluster)
本文参考官方文档而成:http://redis.io/topics/cluster-tutorial。经测试,安装过程也适用于redis-3.2.0。
一见
2018/08/06
2K0
Redis 实战篇之搭建集群
Redis Cluster 即 Redis 集群,是 Redis 官方在 3.0 版本推出的一套分布式存储方案。完全去中心化,由多个节点组成,所有节点彼此互联。Redis 客户端可以直接连接任何一节点获取集群中的键值对,不需要中间代理,如果该节点不存在用户所指定的键值,其内部会自动把客户端重定向到键值所在的节点。
Esofar
2019/03/20
7630
Redis 实战篇之搭建集群
手把手带你搭建redis集群
redis-cluster是redis官方提供的分布式数据库解决方案,集群通过分片进行数据共享,并提供复制和故障转移功能。
MySQL数据库技术栈
2020/08/13
4100
Redis Cluster 3.0集群部署与使用
在说明使用和安装之前,先大概介绍下,Redis 集群是一个可以在多个 Redis 节点之间进行数据共享的设施。通过分区(partition)来提供一定程度的可用性(availability),即使集群中有一部分节点失效或者无法进行通讯,集群也可以继续处理命令请求。
Linux运维技术之路
2022/06/07
4680
Redis Cluster 3.0集群部署与使用
redis(4) -- 集群
redis集群的可扩展性(scalability是线性的, 即增加节点会带来实际的效果提示. 集群节点间使用异步冗余备份,所以安全些.
大大刺猬
2020/12/21
4750
大数据篇---redis学习Redis安装Redis启动redis集群搭建
Redis安装 1 第一步:安装 C 语言需要的 GCC 环境 yum install -y gcc-c++ yum install -y wget 第二步:下载并解压缩 Redis 源码压缩包 wg
用户2337871
2020/12/22
8860
大数据篇---redis学习Redis安装Redis启动redis集群搭建
docker 实现redis集群搭建
摘要:接触docker以来,似乎养成了一种习惯,安装什么应用软件都想往docker方向做,今天就想来尝试下使用docker搭建redis集群。
老梁
2019/09/10
1.3K0
docker 实现redis集群搭建
如何部署 Redis 集群
Redis 是我们目前大规模使用的缓存中间件,由于它强大高效而又便捷的功能,得到了广泛的使用。单节点的Redis已经就达到了很高的性能,为了提高可用性我们可以使用Redis集群。本文参考了Rdis的官方文档和使用Redis官方提供的Redis Cluster工具搭建Rdis集群。
大数据弄潮儿
2018/10/30
8.5K0
如何部署 Redis 集群
linux虚拟机环境快速搭建redis5.x版本的主从集群总结
我在云服务器上曾参与过公司redis集群的搭建,但时间久了,都快忘记当时的搭建过程了,故而决定在虚拟机centOS 7的环境,自行搭建一套redis5.x版本的集群,该版本集群的搭建比较方便,不用再像以前版本那样还得再搞一个 Ruby,5.x版本可直接使用 redis-cli的方式来构建集群。
朱季谦
2021/08/20
3700
Redis 高可用性解决方案之哨兵与集群
在开始本章的讲解之前,我们首先从宏观角度回顾一下 Redis 实现高可用相关的技术。它们包括:持久化、复制、哨兵和集群,在本系列的前篇文章介绍了持久化以及复制的原理以及实现。本文将对剩下的两种高可用技术哨兵、集群进行讲解,讲一讲它们是如何进一步提高系统的高可用性?
BUG弄潮儿
2021/03/22
7490
Redis 高可用性解决方案之哨兵与集群
Redis的三种集群方式
redis有三种集群方式:主从复制,哨兵模式(Sentinel)和集群(Redis Cluster)。
王先森sec
2023/04/24
4.5K0
Redis的三种集群方式
Redis3.0.7集群部署完整版
Redis集群没有出来前,一直使用Codis集群,现在部署Redis集群看看效果如何。
三杯水Plus
2018/11/14
1.2K0
深入理解Redis Cluster
Redis Cluster采用虚拟槽分区,所有的key根据哈希函数映射到0~16383槽内,计算公式:
星哥玩云
2022/08/18
3630
Redis安装、开发、集群看这一篇就够了!
Redis使用c语言开发的程序,需要使用gcc编译程序编译redis。 安装gcc编译程序命令:
I Teach You 我教你
2023/07/18
7710
Redis安装、开发、集群看这一篇就够了!
Redis-4.0.11集群配置
本文参考官方文档而成:http://redis.io/topics/cluster-tutorial。经测试,安装过程也适用于redis-3.2.0、redis-4.0.11等。
一见
2018/09/30
2.7K0
深入剖析Redis系列: Redis集群模式搭建与原理详解
在 Redis 3.0 之前,使用 哨兵(sentinel)机制来监控各个节点之间的状态。Redis Cluster 是 Redis 的 分布式解决方案,在 3.0 版本正式推出,有效地解决了 Redis 在 分布式 方面的需求。当遇到 单机内存、并发、流量 等瓶颈时,可以采用 Cluster 架构方案达到 负载均衡 的目的。
用户5546570
2019/06/06
8820
深入剖析Redis系列: Redis集群模式搭建与原理详解
如何在Ubuntu 16.04上安装和配置Redis集群
Redis集群已经发展成为缓存,队列等的流行工具,因为它具有可扩展性和速度的潜力。本指南旨在使用三个Linode创建一个集群来演示分片。然后,如果发生故障,您将需要把一个从节点变为主节点。
双愚
2018/08/29
1K0
如何在Ubuntu 16.04上安装和配置Redis集群
4.Redis基础运维之哨兵和集群安装配置
描述: 哨兵模式是主从的升级版,因为主从的出现故障后,不会自动恢复,需要人为干预,这就很蛋疼啊。在主从的基础上,实现哨兵模式就是为了监控主从的运行状况,对主从的健壮进行监控,就好像哨兵一样,只要有异常就发出警告,对异常状况进行处理。
全栈工程师修炼指南
2022/09/28
1.2K0
4.Redis基础运维之哨兵和集群安装配置
相关推荐
Redis数据库集群搭建 | 实践篇
更多 >
LV.1
这个人很懒,什么都没有留下~
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档