java 泛型
一、概念
Java 泛型(generics)是 JDK 5 中引入的一个新特性,提供了编译时类型安全检测机制,该机制允许程序员在编译时检测到非法的类型。
泛型的本质是参数化类型,所操作的数据类型被指定为一个参数。
常用以下字符表示参数类型:
- E - Element (在集合中使用,因为集合中存放的是元素)
- T - Type(Java 类)
- K - Key(键)
- V - Value(值)
- N - Number(数值类型)
- ? - 表示不确定的java类型
1. 泛型方法
具有一个或多个类型变量的方法,称之为泛型方法,类型变量也叫类型参数。
public static <E> void printArray(E[] e ); public static <T extends Comparable<T>> T maximum(T x, T y, T z);//有界(上届)的类型参数
- 返回值之前类型变量声明,多个类型参数声明时用逗号隔开;
- 泛型方法可以存在于非泛型类中;
- 类型参数能被用来声明返回值类型;
- 类型参数只能代表引用型类型,不能是原始类型(int、double、char...)。
有界的类型参数:
要声明一个有界的类型参数,首先列出类型参数的名称,后跟extends关键字,最后紧跟它的上界。
1) 泛型入参
public static <E> void printArray(E[] inputArray);
2)泛型返回值
static <T> WatermarkStrategy<T> forMonotonousTimestamps() {} //调动 WatermarkStrategy.<Event>forMonotonousTimestamps()
3)泛型入参,泛型返回值
//有界的泛型方法: 比较三个值并返回最大值,参数的类型必须实现了Comparable接口,即必须是可比较的 public static <T extends Comparable<T>> T maximum(T x, T y, T z) { T max = x; // 假设x是初始最大值 if (y.compareTo(max) > 0) { max = y; //y 更大 } if (z.compareTo(max) > 0) { max = z; // 现在 z 更大 } return max; // 返回最大对象 } public static <T> Set<T> test(HashMap<String, T> param) { Set<T> set = new HashSet<>(); for (T value : param.values()) { set.add(value); } return set; } public <R> SingleOutputStreamOperator<R> map(MapFunction<T, R> mapper) {}
//方法调用 SingleOutputStreamOperator<String> result = kafkaStream.map(new MapFunction<String, String>() { @Override public String map(String value) throws Exception { String[] fields = value.split(","); return new Event(fields[0].trim(), fields[1].trim(), Long.valueOf(fields[2].trim())).toString(); } });
2. 泛型类
泛型类需要在类名后面添加了类型参数声明部分。
public class Box<T> { private T t; public void add(T t) { this.t = t; } public T get() { return t; } public static void main(String[] args) { Box<String> stringBox = new Box<String>(); stringBox.add(new String("陈数")); } }
3. 类型通配符
类型通配符一般是使用?代替具体的类型参数。
有界的类型参数:
- <? extends T>表示该通配符所代表的类型是T类型的子类。 [上限]
- <? super T>表示该通配符所代表的类型是T类型的父类。 [下限]
public static void getData(List<?> data) {} //有上界的类型通配符:上届:通配符泛型只接受Number及其下层子类类型
public static void getUperNumber(List<? extends Number> data) {}
List<T>和List<?>区别:
- 写入数据:List<T> 可以添加指定类型的元素,List<?> 不允许写入数据,因为它表示未知类型,你不能向其中添加元素,因为你不知道元素的确切类型是什么。
- 读取数据: List<T>可以安全地读取元素, List<?>只能读取 Object 类型的元素,因为你不知道元素的具体类型。
- 通用性和灵活性:List<T> 提供了更多的类型安全性,适用于需要明确指定类型的情况。List<?> 提供了更大的灵活性,适用于在方法或类中对泛型类型的具体参数不关心的情况。
4. Class<T> 和 Class<?> 区别
- T :一个确定的类型,用于泛型类和泛型方法的定义
- ?:一个不确定的类型,用于泛型方法的调用代码和形参,不能用于定义类和泛型方法。
区别1:通过 T 来 确保泛型参数的一致性
//通过T来确保泛型参数的一致性 public <T extends Number> void test(List<T> dest, List<T> src)
//通配符是不确定的,不能保证dest、src具有相同的元素类型 public void test(List<? extends Number> dest, List<? extends Number> src)
区别2:类型参数可以多重限定而通配符不行
使用 & 符号设定多重边界,指定泛型类型T必须是A、B的共有子类型,此时变量 T就具有了所有限定的方法和属性。
对于通配符来说,因为它不是一个确定的类型,所以不能进行多重限定,只能有一个上边界(? extends T)或一个下边界(? super T)。
//类型参数 T 必须是 Number 的子类型,并且实现了 Comparable 接口 class Example<T extends Number & Comparable<T>> {}
区别3:通配符可以使用超类限定而类型参数不行
//类型参数T只具有一种类型限定方式: T extends A //但是通配符 ? 可以进行两种限定: ? extends A ? super A
示例:
//只有成员变量中用到时才需要在类型中申明泛型T,否则不用指定 public class A { public static Class<?> clazz;
//调用:f1(new Event()); public static <T> T f1(T t) { System.out.println("f1 = " + t); return null; }
/** * 区别1:Class<T>在实例化的时候,T要替换成具体类,传入的是一个类型对象
* 调用:f2(Event.class); */ public static <T> void f2(Class<T> t) { T t1 = t.newInstance(); System.out.println("f2 = " + t1); //com.atguigu.chapter05.Event@2a33fae0 } //调用:f3(new Event()); public static void f3(Class<?> t) { clazz = t; Object o = t.newInstance(); System.out.println("o = " + o); //com.atguigu.chapter05.Event@707f7052 } /** * 区别2:Class<?>它是个通配泛型,?可以代表任何类型,主要用于声明时的限制情况 * 当不知道声明什么类型的Class的时候可以定义一个Class<?>,Class<?>可以用于参数类型定义,方法返回值定义等。
* 调用:f4(B.Class) */ public static void f4(Class<? extends Map> t) { System.out.println("f4 = " + t); //class com.atguigu.chapter09.A$B
}
}
class B extends HashMap {}
}
5. 类型擦除
Java的泛型是伪泛型,因为Java在编译期间,所有的泛型信息都会被擦掉。Java的泛型基本上都是在编译器这个层次上实现的,在生成的字节码中是不包含泛型中的类型信息的,使用泛型的时候加上类型参数,在编译器编译的时候会去掉,这个过程成为类型擦除(type erasure)。
List<Object>
和List<String>
在编译后都会变成List
,JVM看到的只是List,由泛型附加的类型信息对JVM是看不到的。Java编译器会在编译时尽可能的发现可能出错的地方,但是仍然无法避免在运行时刻出现的类型转换异常的情况,类型擦除也是Java的泛型与C++模板机制实现方式之间的重要区别。
ArrayList<String> list1 = new ArrayList<String>(); list1.add("abc"); ArrayList<Integer> list2 = new ArrayList<Integer>(); list2.add(123); System.out.println(list1.getClass() == list2.getClass()); //true,说明泛型类型String
和Integer
都被擦除掉了,只剩下原始类型。
存在问题:
通过反射添加其它类型元素
List<Integer> list = new ArrayList<Integer>(); list.add(1); list.getClass().getMethod("add", Object.class).invoke(list, "asd"); for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }
定义一个ArrayList
泛型类型实例化为Integer
对象,如果直接调用add()
方法,那么只能存储整数数据,不过当我们利用反射调用add()
方法的时候,却可以存储字符串,这说明了Integer
泛型实例在编译之后被擦除掉了,只保留了原始类型。
二、类型擦除
ava泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。
泛型的本质是为了参数化类型(在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型)。也就是说在泛型使用过程中,操作的数据类型被指定为一个参数,这种参数类型可以用在类、接口和方法中,分别被称为泛型类、泛型接口、泛型方法。
- 适用于多种数据类型执行相同的代码(代码复用)
- 泛型中的类型在使用时指定,不需要强制类型转换(类型安全,编译器会检查类型)
1.泛型上下限
为了解决泛型中隐含的转换问题,Java泛型加入了类型参数的上下边界机制。<? extends A>表示该类型参数可以是A(上边界)或者A的子类类型。编译时擦除到类型A,即用A类型代替类型参数。这种方法可以解决开始遇到的问题,编译器知道类型参数的范围,如果传入的实例类型B是在这个范围内的话允许转换,这时只要一次类型转换就可以了,运行时会把对象当做A的实例看待。
2.如何理解Java中的泛型是伪泛型?泛型中类型擦除
Java泛型这个特性是从JDK 1.5才开始加入的,因此为了兼容之前的版本,Java泛型的实现采取了“伪泛型”的策略,即Java在语法上支持泛型,但是在编译阶段会进行所谓的“类型擦除”(Type Erasure),将所有的泛型表示(尖括号中的内容)都替换为具体的类型(其对应的原生态类型),就像完全没有泛型一样。理解类型擦除对于用好泛型是很有帮助的,尤其是一些看起来“疑难杂症”的问题,弄明白了类型擦除也就迎刃而解了。
泛型的类型擦除原则是:
- 消除类型参数声明,即删除
<>
及其包围的部分。
- 根据类型参数的上下界推断并替换所有的类型参数为原生态类型:如果类型参数是无限制通配符或没有上下界限定则替换为Object,如果存在上下界限定则根据子类替换原则取类型参数的最左边限定类型(即父类)。
- 为了保证类型安全,必要时插入强制类型转换代码。
- 自动产生“桥接方法”以保证擦除类型后的代码仍然具有泛型的“多态性”。
3.那么如何进行擦除的呢?
- 擦除类定义中的类型参数 - 无限制类型擦除
当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如<T>
和<?>
的类型参数都被替换为Object。
- 擦除类定义中的类型参数 - 有限制类型擦除
当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界,比如形如<T extends Number>
和<? extends Number>
的类型参数被替换为Number
,<? super Number>
被替换为Object。
- 擦除方法定义中的类型参数
擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,这里仅以擦除方法定义中的有限制类型参数为例。
三、使用教程
1、泛型接口
public interface WatermarkStrategy<T> extends TimestampAssignerSupplier<T>, WatermarkGeneratorSupplier<T> { @Override WatermarkGenerator<T> createWatermarkGenerator(WatermarkGeneratorSupplier.Context context); @Override default TimestampAssigner<T> createTimestampAssigner(TimestampAssignerSupplier.Context context) { return new RecordTimestampAssigner<>(); } default WatermarkStrategy<T> withTimestampAssigner( SerializableTimestampAssigner<T> timestampAssigner) { checkNotNull(timestampAssigner, "timestampAssigner"); return new WatermarkStrategyWithTimestampAssigner<>(this, TimestampAssignerSupplier.of(timestampAssigner)); } static <T> WatermarkStrategy<T> forMonotonousTimestamps() { return (ctx) -> new AscendingTimestampsWatermarks<>(); }
static <T> WatermarkStrategy<T> forBoundedOutOfOrderness(Duration maxOutOfOrderness) { return (ctx) -> new BoundedOutOfOrdernessWatermarks<>(maxOutOfOrderness); } }
使用示例
//1.获取登录数据流 SingleOutputStreamOperator<LoginEvent> loginEventStream = env.fromElements( new LoginEvent("user_1", "192.168.0.1", "fail", 2000L), new LoginEvent("user_1", "192.168.0.2", "fail", 3000L) ).assignTimestampsAndWatermarks(WatermarkStrategy.<LoginEvent>forBoundedOutOfOrderness(Duration.ZERO) .withTimestampAssigner(new SerializableTimestampAssigner<LoginEvent>() { @Override public long extractTimestamp(LoginEvent element, long recordTimestamp) { return element.timestamp; } }));
2、泛型方法
@Data @Accessors(chain = true) public class EvaluateRespDTO<T> implements Serializable { private String C_Response_Desc; private String C_API_Status; private T C_Response_Body; private String C_Response_Code; @SneakyThrows public static <T> EvaluateRespDTO parceResult(String result, Class<T> cls) { JSONObject obj = JSONObject.parseObject(result); T t = cls.newInstance(); if (obj.containsKey("C-Response-Body")) { t = JSONObject.parseObject(obj.getString("C-Response-Body"), cls); } EvaluateRespDTO respDTO = new EvaluateRespDTO() .setC_Response_Desc(obj.getString("C-Response-Desc")) .setC_API_Status(obj.getString("C-API-Status")) .setC_Response_Body(t) .setC_Response_Code(obj.getString("C-Response-Code")); return respDTO; } public static void main(String[] args) { String s = "{\n" + "\t\"C-Response-Desc\": \"success\",\n" + "\t\"C-API-Status\": \"00\",\n" + "\t\"C-Response-Body\": {\n" + "\t\t\"dacVerf\": \"AAAAAA\",\n" + "\t\t\"message\": \"评价保存成功!\",\n" + "\t\t\"status\": \"true\",\n" + "\t\t\"tenant\": \"530000000000\",\n" + "\t\t\"tms\": 1663920228011,\n" + "\t\t\"vno\": 1\n" + "\t},\n" + "\t\"C-Response-Code\": \"000000000000\"\n" + "}"; EvaluateRespDTO<EvaluateDataSendRespBody> evaluateRespDTO = parceResult(s, EvaluateDataSendRespBody.class); EvaluateDataSendRespBody c_response_body = evaluateRespDTO.getC_Response_Body(); } }
3、泛型+抽象方法
泛型是为了让工具类写的更加的通用,但是每个对象又有其不同的特性,再没有实际使用之前并不能确定其特性,所以可以使用一个抽象方法,让其使用时进行实现。
public interface DimAsyncJoinFunction<T> { String getKey(T input); void join(T input, JSONObject dimInfo) throws ParseException; } public abstract class DimAsyncFunction<T> extends RichAsyncFunction<T, T> implements DimAsyncJoinFunction<T> { private Connection connection; private ThreadPoolExecutor threadPoolExecutor; private String tableName; public DimAsyncFunction(String tableName) { this.tableName = tableName; } @Override public void open(Configuration parameters) throws Exception { Class.forName(GmallConfig.PHOENIX_DRIVER); connection = DriverManager.getConnection(GmallConfig.PHOENIX_SERVER); threadPoolExecutor = ThreadPoolUtil.getThreadPool(); } @Override public void asyncInvoke(T input, ResultFuture<T> resultFuture) throws Exception { threadPoolExecutor.submit(new Runnable() { @Override public void run() { try { //获取查询的主键 String id = getKey(input); //查询维度信息 JSONObject dimInfo = DimUtil.getDimInfo(connection, tableName, id); //补充维度信息 if (dimInfo != null) { join(input, dimInfo); } //将数据输出 resultFuture.complete(Collections.singletonList(input)); } catch (Exception e) { e.printStackTrace(); } } }); } @Override public void timeout(T input, ResultFuture<T> resultFuture) throws Exception { System.out.println("TimeOut:" + input); } }
使用示例
public class OrderWideApp { public static void main(String[] args) throws Exception {//TODO 3.双流JOIN SingleOutputStreamOperator<OrderWide> orderWideWithNoDimDS = orderInfoDS.keyBy(OrderInfo::getId);//TODO 4.关联维度信息 HBase Phoenix //4.1 关联用户维度 SingleOutputStreamOperator<OrderWide> orderWideWithUserDS = AsyncDataStream.unorderedWait( orderWideWithNoDimDS, new DimAsyncFunction<OrderWide>("DIM_USER_INFO") { @Override public String getKey(OrderWide orderWide) { return orderWide.getUser_id().toString(); } @Override public void join(OrderWide orderWide, JSONObject dimInfo) throws ParseException { orderWide.setUser_gender(dimInfo.getString("GENDER")); String birthday = dimInfo.getString("BIRTHDAY"); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd"); long currentTs = System.currentTimeMillis(); long ts = sdf.parse(birthday).getTime(); long age = (currentTs - ts) / (1000 * 60 * 60 * 24 * 365L); orderWide.setUser_age((int) age); } }, 60, TimeUnit.SECONDS); } }
参考:
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· go语言实现终端里的倒计时
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现