泛型
泛型定义
泛型,主要是用来将类型参数化。比如说我们定义一个方法可以设置参数,但是这个参数类型是确定的。而泛型就是将参数类型也变成变量。这样在调用方法时可以依据传入参数类型来动态确定参数类型。有点类似于重载,但是比重载灵活,重载需要写多个方法,每个方法参数类型不同,如果使用泛型就可以写一个方法。来达到重载的目的。
当然,还有其他使用场景,比如说我定义一个ArrayList集合,不使用泛型。那么我在添加时可以添加字符串类型,整数类型的参数。这样会导致无法确定集合中数据类型。那么在对集合进行遍历时,接收遍历结果的变量就容易出错,毕竟不可能都使用Object来接收。而在ArrayList 对象上使用泛型( List<String> list = new ArrayList<String>() ),就可以很好明确集合存储的数据类型,同时也保证了如果类型不一致会在编译期就报错。
但是上面两个例子都有一个问题,即:为什么调用方法时会根据输入参数类型来动态确认泛型的类型。以及为什么设置了泛型的ArrayList对象当输入与泛型类型不一致的数据类型会在编译期报错?
类型推断,类型安全检查,插入类型转换和泛型擦除
这时候我们就需要提到一个概念:泛型擦除。
当我们在代码中编写好泛型后,代码编译为字节码文件时,会先进行类型推断,类型安全检查,插入类型转换和泛型擦除,类型推断就相当于判断它应该是什么类型,比如说方法输入的参数是String类型 ArrayList 我们设置的String类型。那么编译器就知道了他们的类型,然后就会进行插入类型转换,相当于在原有调用方法基础上增加一个强制类型转换的代码类似于(String)method("hello");ArrayList的get方法也类似于:(String)arrayList.get(0);,类型安全检查就是判断一些添加操作中添加数据的类型是否与定义的泛型类型一致,通过后再将代码中所有泛型类型替换为Object类型。当然默认情况下是使用Object替换,但是如果使用了有界类型参数(extends和super)就具体分析了。
由此就解决了,为什么能自动识别方法参数类型与不能插入其他类型。
但是这里有个问题,虽然进行了类型擦除,但java编译器还是会将原来类型信息存储。
字节码文件(.class)中包含特殊元数据属性Signature,保存完整泛型签名
Signature: <T:Ljava/lang/Object;>Ljava/lang/Object;
那么既然我们有了 类型推断,类型安全检查,插入类型转换,为什么还需要存储这个信息呢?
主要有两方面作用:1.方便反射获取类型信息。
补充:反射获取泛型类型(如ParameterizedType)依赖Signature,例如GSON反序列化时需获取List<Person>的真实类型。
Type type = new TypeToken<List<String>>(){}.getType(); // 依赖Signature
2.子类继承父类时,编译器需检查泛型参数匹配。子类覆盖父类方法时,需验证泛型签名一致性。
interface Box<T> { T get(); }
class StringBox implements Box<String> {
@Override
public Integer get() { return 1; } // 编译错误:返回类型不兼容
}
泛型通配符
我们可以使用更直观的例子:
List<Number> list01 = new ArrayList<Integer>();// 编译错误
ArrayList<Number> list02 = new ArrayList<Integer>();// 编译错误
上面两种方式都会编译失败。为什么呢,明明Number是Integer的父类?那么下面给个案例:
我创建一个集合对象ArrayList<Integer> integerList = new ArrayList<>();,然后添加一个 Integer 数据。这个没问题,但此时我将他向上转型成为 ArrayList<Number> numberList = integerList;因为是Number的缘故,我反而可以添加一个 Float 类型数据到集合里面。也就是说 integerList指向的集合中出现了 Float 类型的数据。这显然是不对的。
// 创建一个 ArrayList<Integer> 集合
ArrayList<Integer> integerList = new ArrayList<>();
// 添加一个 Integer 对象
integerList.add(new Integer(123));
// “向上转型”为 ArrayList<Number>
ArrayList<Number> numberList = integerList;
// 添加一个 Float 对象,Float 也是 Number 的子类,编译器不报错
numberList.add(new Float(12.34));
// 从 ArrayList<Integer> 集合中获取索引为 1 的元素(即添加的 Float 对象):
Integer n = integerList.get(1); // ClassCastException,运行出错
但有时候我们又有这方面需求,希望集合中添加的数据能够有限的多样化。因为类有子类。如果集合中存储的是类本身类型或者其子类类型。那么在get时是完全没有问题的。(强制类型转换)如:
Integer integer = new Integer(1);
Number number = new Integer(1); // 用到了多态中向上转型
所以思考上上面案例出现问题的根本原因是什么?
就是他进行向上转型,导致他无法控制存储数据的类型。也就是说我们只需要保证存储数据类型是自身或者自身的子类就能够完全避免这个问题。
ArrayList<Number> numberList = new ArrayList<Integer>(); // 相当于向上转型了,导致集合中的值可以不是Integer
**所以 <? super T> 出场了。**简称:下界通配符。
具体作用很简单,不让它能够向上转型。使用这个后会限制它接收的对象只能是他本身类型或者其父类类型。
// 这样设置后,下面的会报错。因为Integer不是Number的父类或者Number类型
ArrayList<? super Number> list = new ArrayList<Integer>();
// 下面的是可以的
ArrayList<? super Number> list = new ArrayList<Number>();
ArrayList<? super Number> list = new ArrayList<Object>();
// 正确案例
public static void main(String[] args) {
// 创建一个 ArrayList<? super Number> 集合
ArrayList<Number> list = new ArrayList();
// 往集合中添加 Number 类及其子类对象
list.add(new Integer(1));
list.add(new Float(1.1));
// 调用 fillNumList() 方法,传入 ArrayList<Number> 集合
fillNumList(list);
System.out.println(list);
}
public static void fillNumList(ArrayList<? super Number> list) {
list.add(new Integer(0));
list.add(new Float(1.0));
}
这样就解决了写的问题,但是注意,这里是没办法读的,或者说只能使用Object来接收列表中数据信息。因为super的范围是其本身或者父类或者Object。也就是说如果它有多个父类,你是没办法确定他创建的对象是自身还是其中某个父类,那么自然也不能使用其某个父类来接收get后的数据。只能是Object来接收,但是这样就没啥意义
// 比如说现在以Integer为例,它可以是
ArrayList<? super Integer> list = new ArrayList<Integer>();
ArrayList<? super Integer> list = new ArrayList<Number>();
ArrayList<? super Integer> list = new ArrayList<Object>();
// 现在看我好像可以直接使用Number来接收
Number n = list.get(0)
// 这样想是没错,但是这并不代表全部情况,只是因为Number没有父类了,如果Number也有父类呢?也就是说我们无法判断new出的对象是什么类型的,因此也无法确定使用什么类型来接收了
那有没有什么方法来解决呢?这就用到了上界通配符。
上界通配符 <? extends T>,他主要限制new出的对象只能是 T 本身或者其子类,如:
ArrayList<? extends Number> list02 = new ArrayList<Integer>();
ArrayList<? extends Number> list02 = new ArrayList<Number>();
ArrayList<? extends Number> list02 = new ArrayList<Float>();
这样有什么用?显而易见,因为我们限定了集合中类型最大范围就是 T,也就是上面代码中的Number,那么此时我们自然能够使用Number来接收get方法的返回值。
那这个上界通配符能不能用来写呢?还是那句话,你可以将上界通配符看着可以进行向上转型,下界通配符能进展向上转型。而下界通配符之所以需要禁止,就是为了防止写入一些超出范围的数据,而之所以会出现超出范围的数据,就是因为有向上转型的存在。因此上界通配符是不能进行写的只能读。
我们最后以java中Collections类中的copy方法来结尾,它结合上界与下界通配符,完成的复制操作
public class Collections {
// 把 src 的每个元素复制到 dest 中:
public static <T> void copy(List<? super T> dest, List<? extends T> src) {
for (int i = 0; i < src.size(); i++) {
// 获取 src 集合中的元素,并赋值给变量 t,其数据类型为 T
T t = src.get(i);
// 将变量 t 添加进 dest 集合中
dest.add(t);// 添加元素进入 dest 集合中
}
}
}
反射
我们都知道,通过反射可以在运行时获取类的信息,从而操作类,对象,方法,字段等等。
那具体有什么用呢?我将从以下几个方面来叙述
1.修改String类的值。(访问私有变量)
2.给泛型集合中添加不符合原类型的数据
叙述之前,我们先明确反射的三种实现方式:
// 方式1:类名.class : 类必须在编译时存在,不会执行静态代码块,直接绑定具体类型
Class<String> strClass = String.class;
// 方式2:对象.getClass(),运行时确定:依赖对象实例,触发类初始化:类必须已完成加载和初始化,丢失泛型信息:返回Class<?>,无法获取原始泛型类型
"Hello".getClass();
// 方式3:Class.forName()(最灵活)完全动态:类名可以是运行时字符串,触发类初始化:执行静态代码块,需要全限定名:包名+类名(如java.util.ArrayList)
Class<?> clazz = Class.forName("java.util.ArrayList");
// 获取所有公共方法(含继承)
Method[] methods = clazz.getMethods();
// 获取所有声明字段(含私有)
Field[] fields = clazz.getDeclaredFields();
// 获取父类信息
Class<?> superClass = clazz.getSuperclass();
2.修改String字符串值
我们知道,String类型变量的值在进行赋值后是不可以改变的,现在阶段的改变都是更换变量指向的存储地址。
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
...}
value被final修饰,所以是改变不了value指向的数组地址的。又加上是私有的。使用外部也无法直接操作value。但是反射可以。
我们通过反射获取私有的value变量。虽然变量指向的地址无法改变。但是我们可以改变数组中的值
// Java 8 及之前:char[]
Field valueField = String.class.getDeclaredField("value");
valueField.setAccessible(true);
String original = "Hello";
System.out.println("原始值: " + original); // Hello
// 获取内部 char 数组
char[] value = (char[]) valueField.get(original);
// 修改数组内容
value[0] = 'J';
System.out.println("修改后: " + original); // Jello
// 验证哈希码变化
System.out.println("哈希码: " + original.hashCode()); // 已改变
当然,实际中不要这么搞,会出事。首先字符串常量池中存储的是字符串对象的引用,这些引用指向的是堆内存中的String对象。且字符串常量池中的引用是可以被复用的,也就是说如果定义两个字符串变量且他们值相同。那么他们会使用同一个对象引用。此时如果修改这个引用对象指向的字符串对象。那么这两个变量值都会被修改!
String s1 = "Hello";
String s2 = "Hello"; // 如果使用反射将s1的值修改为Jello。那么s2也会变成Jello。但是s3不会,因为s3是直接new了一个对象。他没有使用字符串常量池的对象引用而是直接在堆内存中创建对象
String s3 = new String("Hello");
2.给泛型集合中添加不符合原类型的数据
我们知道,如果给集合设置泛型,在编译器编译是会进行类型擦除,也就是最终类型默认是Object。那么此时如果使用反射来堆集合添加数据,理论上是可以添加任何类型的。这会导致集合类型与我们开始定义的类型不一样
List<Integer> intList = new ArrayList<>();
intList.add(100);
// 获取Class对象(泛型擦除后只剩List)
Class<?> listClass = intList.getClass();
// 定位add方法
Method add = listClass.getMethod("add", Object.class);
// 突破泛型限制插入字符串!
add.invoke(intList, "我是字符串"); // 不会报错!
System.out.println(intList); // 输出: [100, 我是字符串]
上面代码没问题,但是当我们执行:Integer i = intList.get(1)时会抛出ClassCastException!
这里提一点,之所以不使用反射时,虽然类型擦除了,但是添加数据与get数据都没问题。是因为在类型擦除前,编译器还会做类型安全检查和插入类型转换操作,简单将就是插入时会判断类型是不是Integer,不是就直接报错了。同样如果有Integer i = intList.get(1)代码,编译器会直接加一个强制类型转换Integer i = (Integer)intList.get(1),但是注意通过反射获取的get方法是没有这个强制转换的。因为前者是代码中写好的代码,所以编译器可以直接改为强转,但是反射是运行时调用,并没有事先有这行代码,所以使用的是原来没强转的get方法。
动态代理
动态代理和静态代理目的一致,就是他需要帮被代理的类去做一些事。比如通过代理类来调用被代理类的方法。普遍使用方式在不改变被代理类源码的情况下去增加一些东西。如在调用被代理类方法的前后增加日志输出。
而静态代理和动态代理实现方式不一致,说具体实现方式之前需要先讨论一个东西。
两者实现上都需要让被代理类与代理类实现相同接口,当然是jdk版本的代理。如果是CGLIB就是代理类继承被代理类了
为什么JDK动态代理要求实现接口?
根本原因,代理类相当于是被代理类的替代品。那么正常情况下被代理类有的代理类也必须有且会在基础上增加东西。那么我们又知道接口的定义主要是用来定义规范的,简单理解就是这个实现这个接口的类需要干啥,应该干啥。按照他定义的来就行。那么就很好理解了为啥代理类要与被代理类实现同一接口。本质上是保证他们都有最基本的规范。
这个条件更像是一个约定,恰好接口有这个规范的性质,所以有了这个条件。实际上你想不实现同一接口也能实现。就像CGLIB他没用接口而是继承了。
实现方式
静态代理是直接编写好代理类的代码,而动态代理是使用反射来动态的在运行时生成代理类。
以ArrayList为例,编写静态代理和动态代理的案例代码,实现添加日志的需求
静态代理:
// 1. 定义代理类(需实现相同接口)
class LoggingListProxy implements List<String> {
private final List<String> target; // 被代理对象
public LoggingListProxy(List<String> target) {
this.target = target;
}
// 2. 为每个方法添加日志
@Override
public boolean add(String item) {
System.out.println("[LOG] 添加元素: " + item);
return target.add(item);
}
@Override
public String remove(int index) {
System.out.println("[LOG] 删除索引: " + index);
return target.remove(index);
}
// 3. 必须实现接口所有方法(约24个方法)
@Override
public int size() { /* 重复模板代码 */ }
@Override
public boolean isEmpty() { /* 重复模板代码 */ }
// ... 其他20+方法
}
// 使用静态代理
List<String> realList = new ArrayList<>();
List<String> proxyList = new LoggingListProxy(realList);
proxyList.add("Java");
proxyList.remove(0);
其实也看得出来,静态代理必须要实现所有接口中的方法,List中太多方法了,但是我们不可能需要给每个方法都加上日志,所以有很多代码冗余。也不够灵活。所以这种情况下使用动态代理反而效果更好
动态代理
// 1. 创建调用处理器(核心逻辑)
class LoggingHandler implements InvocationHandler {
private final Object target; // 被代理的真实对象,target字段:存储被代理的真实对象(本例中的ArrayList实例)
public LoggingHandler(Object target) {
this.target = target; // 构造函数注入真实对象
}
// invoke方法:代理对象所有方法调用的统一入口
//proxy:代理对象本身(本例中的proxyList)
//method:被调用的方法对象(如add()方法)
//args:方法参数数组(如["Dynamic"])
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 前置增强:日志记录
System.out.printf("[LOG] 方法: %s, 参数: %s%n",
method.getName(), Arrays.toString(args));
try {
// 核心:反射调用真实对象的方法 反射调用:method.invoke(target, args) 将调用转发给真实对象
return method.invoke(target, args);
} catch (Exception e) {
// 异常处理
throw new RuntimeException(e);
}
}
}
// 3. 动态创建代理实例
List<String> realList = new ArrayList<>();
List<String> proxyList = (List<String>) Proxy.newProxyInstance(
ArrayList.class.getClassLoader(), // // ① 类加载器,用于加载动态生成的代理类,通常使用被代理类的类加载器
new Class[]{List.class}, // ② 代理接口数组 指定代理类要实现的接口 可同时代理多个接口:new Class[]{List.class, Serializable.class}
new LoggingHandler(realList) // ③ 调用处理器 包含业务增强逻辑的核心组件 绑定真实对象realList
);
// 使用方式与静态代理相同
proxyList.add("Dynamic");
proxyList.remove(0);
// proxyList.add("Dynamic");
1.调用代理对象的 add("Dynamic") 方法
2.代理对象内部调用 InvocationHandler.invoke()
3.执行日志记录(前置增强)
4.通过反射调用真实对象的 add("Dynamic")
5.返回结果给代理对象
6.代理对象返回结果给调用者
868

被折叠的 条评论
为什么被折叠?



