step1
有如下代码
double d1 = 123d;
Map map = new HashMap();
map.put("key",d1);
double d2 = (double) map.get("key");
System.out.println(d2);
以上代码会正常打印出 d2 的值
123.0
上述代码将 map.get(“key”) 的返回值强制转换为了一个 double
类型,如果不写强制类型转换,编译器会直接提示错误警告
double d2 = map.get("key");
它告诉编写者,不能直接把一个 Object
类型的值赋值给一个 double
类型,也就是说 map.get 方法的返回值是一个 Object
类型
分析一下
Map 是一个以 键值对 的形式存放数据的数据结构,它允许以任何类型的值作为键(key)
,任何类型的值作为值(value)
。由于Object
是所有类型的超类,所以 map.put(key,value)
的两个形参都是 Object
类型的
执行以下代码时
map.put("key",d1);
map 把 key 和 value 都向上转型成了 Object
保存起来,当通过 get(key) 去取到数据时,也返回的是转型之后的 Object
对象。因为 map 自己并不会记录值的原始类型到底是哪一种,无论哪一种肯定都是 Object
,所以我们拿到 Object
对象之后需要进行一次对应的类型转换,成为我们需要的值
double d1 = 123d;
Map map = new HashMap();
map.put("key",d1);
double d2 = (double) map.get("key");
System.out.println(d2);
int i1 = (int)map.get("key");
以上代码在编辑器中不会有任何问题,但是如果执行,第6行就会出现异常
java.lang.Double cannot be cast to java.lang.Integer
只看第六行的代码完全没有任何问题
int i1 = (int)map.get("key");
将一个 Object
类型强转为一个 int
类型在 java 语法中是允许的,但是由于这个 key 中存放的实际值是一个 double
类型。所以实际运行时会变成 将一个 double
类型强转换为一个 int
类型,这在 java 中是不允许的,就会出现异常
double d1 = 123d;
Map map = new HashMap();
map.put("key",d1);
double d2 = (double) map.get("key");
System.out.println(d2);
上面的代码由于我们已经知道
key
对应的值类型为double
所以当我们通过 map.get(“key”)拿到返回的Object
时
已经知道该强转为哪一种类型,所以是完全没问题的
实际的工程代码中,想要查到这个 key 是在哪里、什么地方被 put 进 map 的,是不好查的,所以我们只能拿到 map.get 之后的 Object
对象。这个 Object
在被转换之前的原始类型是什么我们并不知道,但是由于语法上 Object
可以被转换成任何一个类型。所以编辑阶段编辑器是不会有任何提示的,只有运行时才能知道是否正确
Object o = map.get("key1");
// 可以将 o 转换成任意类型,编辑时不会有错误提示
int n = (int)o;
double d = (double)o;
YourClass y = (YourClass)o;
//... ...
能否在 map 定义之初就指定这个 map 中的 键和值 的类型呢?
调用 put、get 方法时的形参也按照指定的 键和值 的类型
这样在编码时就能知道 此map中的 键和值 应该是什么类型
step2
java1.5之后更新了泛型
现在定义一个 map 的方式
Map<Integer,String> map = new HashMap<>();
此种方式指定了这个 map 的键值只能是一个 Integer
类型,对应的值只能是一个 String
类型,这样在调用 map.put 方法时,可以直观的看到这个 map 的键值类型
现在往 map 中存放两条键值对数据
map.put(1,"firstValue");
map.put(2,"secondValue");
然后 get 数据
String s = map.get(1);
System.out.println(s);
// firstValue
由于定义 map 之初就指定了 key 和 value 的类型,通过查看 map 的定义语句我们就能知道,返回类型是什么,就用返回的类型同类型的变量去接收即可,并且如果我们用一个其他类型的变量去接收,编辑器直接会提示代码错误
解决了 step1中的问题
step3 定义泛型类型
自定义一个泛型
public class Gen <T>{
private T one;
private T two;
public Gen(){}
public Gen(T one,T two){
this.one = one;
this.two = two;
}
public T getOne() {
return one;
}
public void setOne(T one) {
this.one = one;
}
public T getTwo() {
return two;
}
public void setTwo(T two) {
this.two = two;
}
}
上述代码含义是:
一个泛型类型Gen
,它的泛型T
可以是任意一个类,属性 one、two 都是T
类型的对象
我们可以指定它的泛型
public static void main(String[] args) {
Gen<Number> numberGen = new Gen<Number>(123,456);
}
上面我们指定了 numberGen 的泛型为 Number
,现在 numberGen 的两个属性 one、two 就都是 Number
类型,且 set 方法的形参也都为 Number
类型
再定义一个针对于 参数为 泛型是 Number 类型的 Gen 的方法
public static <T> int sum(Gen<Number> gen){
Number one = gen.getOne();
Number two = gen.getTwo();
return one.intValue() + two.intValue();
}
这是一个简单的将 Gen 中的属性求和的方法,并且形参要求是一个 泛形为 Number 的 Gen 类型
public static void main(String[] args) {
Gen<Number> numberGen = new Gen<Number>(123,456);
System.out.println(sum(numberGen));
Gen<Integer> integerGen = new Gen<>(1,5);
System.out.println(sum(integerGen));// 此处会有错误警告
}
public static <T> int sum(Gen<Number> gen){
Number one = gen.getOne();
Number two = gen.getTwo();
return one.intValue() + two.intValue();
}
按照 java 的语法逻辑,Integer
是 Number
的子类,那么可以向上转型为 Number
。而 sum 方法的形参中的 泛型 就是 Number
,所以 integerGen 应该可以作为参数才对
为什么不可以这么做?
值得注意的是
虽然 Integer 是Number
的子类可以向上转型
但是 sum 方法的形参是一个 泛型类型为 Number 的泛型类,并不是单纯的Number
而Gen<Integer> integerGen
并不是Gen<Number> numberGen
的子类,所以不可作为形参
sum 方法的方法体对于一个 Number
或者 Number
的子类 同样适用
Number one = gen.getOne();
Number two = gen.getTwo();
return one.intValue() + two.intValue();
有没有一种方式能够让 泛型是 Number 或者 Number 的子类的泛型对象 Gen 也能作为形参传入呢?
step4 上界通配符
针对 step3 最后的问题,java 提供了一种方式可以做到
改写 sum 方法
public static <T> int sum(Gen<? extends Number> gen){
Number one = gen.getOne();
Number two = gen.getTwo();
return one.intValue() + two.intValue();
}
Gen<? extends Number> gen
的含义是
泛型类型形参 gen 的泛型可以是 Number
或者 Number
的子类。这种方式叫做 上界通配符,使用此种方式,所有 泛型为 Number
或者 Number
子类的 Gen 都可以作为参数传递进来,得到 intValue 的和
例如
Gen<Integer> integerGen = new Gen<>();
Gen<Double> doubleGen = new Gen<>();
Gen<Float> floatGen = new Gen<>();
// ... ...
现在 step3 中的代码就能正常执行了
public static void main(String[] args) {
Gen<Number> numberGen = new Gen<Number>(123,456);
System.out.println(sum(numberGen));// 579
Gen<Integer> integerGen = new Gen<>(1,5);
System.out.println(sum(integerGen));// 6
}
public static <T> int sum(Gen<? extends Number> gen){
Number one = gen.getOne();
Number two = gen.getTwo();
return one.intValue() + two.intValue();
}
注:
使用上界通配符有一个需要注意的地方
先修改一下 sum 方法代码
public static <T> int sum(Gen<? extends Number> gen){
Number one = gen.getOne();
Number two = gen.getTwo();
gen.setOne( new Integer(789) );// 此处会出现错误警告
return one.intValue() + two.intValue();
}
如果我们尝试在 sum 方法中修改 gen 的属性值,是不允许的,按照以往的经验,拿到了一个对象,通过 set 方法去修改对象的属性值是没有问题的
这里的原因是:
形参 gen 的泛型是
Number
或者它的子类,所以 set 方法的形参也是Number
或者 其子类
但是 Number 理论上可以有无数个子类,在 sum 方法中并不能知道此时的 gen 泛型到底是Number
的哪一个子类
所以 set 方法的形参到底是什么具体的类型在 sum 方法中无法知晓,而父类Number
是无法转型为子类的
set 方法就无法被正常调用
只可以将其 set 为 null
gen.setOne(null);
除非特殊需要才会这样做,因为这样会导致后面取值调用时空指针异常
由上面的分析得知
使用上界通配符的方式可以读取 泛型类型中的泛型,但是无法修改 泛型类型中的泛型 – 只读
step5 下界通配符
上界通配符意义是 泛型可以是指定类型本身或者其子类
下界通配符的意义是 泛型可以是指定类型本身或者其父类
定义一个方法,参数使用 下界通配符 Gen<? super Integer>
public static <T> void set(Gen<? super Integer> gen,Integer one ,Integer two){
gen.setOne(one);
gen.setTwo(two);
}
这是一个简单的将 泛型为 Integer 的 gen 中属性重新赋值的方法
测试一下
public static void main(String[] args) {
Gen<Number> numberGen = new Gen<Number>(123,456);
System.out.println(numberGen.getOne().intValue());// 123
// 重新赋值
set(numberGen,100,100);
System.out.println(numberGen.getOne().intValue());// 100
Gen<Integer> integerGen = new Gen<Integer>(1,5);
System.out.println(integerGen.getOne().intValue());// 1
// 重新赋值
set(integerGen,3,4);
System.out.println(integerGen.getOne().intValue());// 3
}
public static <T> void set(Gen<? super Integer> gen,Integer one ,Integer two){
gen.setOne(one);
gen.setTwo(two);
}
可以看出 对于泛型是 Integer
或者其父类 Number
,方法都可成功调用且修改属性 one 的值
注:
使用 下界通配符也有一个需要注意的地方
修改一下 set 方法
public static <T> void set(Gen<? super Integer> gen,Integer one ,Integer two){
gen.setOne(one);
gen.setTwo(two);
Integer integer = gen.getOne();// 此处会出现错误警告
}
当我们试图在 set 方法中取到 gen 的属性值时,会有错误警告
原因与 step4 中的上界通配符类似
形参 gen 的泛型是Integer
或者其父类,get 方法中返回的值也为Integer
及其父类,由于 set 方法中并不知道传递过来的 gen 实际的泛型是 Integer 还是其父类,虽然Integer
的父类只能有一个我们可以找到,但是Integer
的父类也可以有父类,父类也可以有父类 …,Gen<? super Integer> gen
其中的形参泛型可以是Integer
及其父类、父类的父类 、父类的父类的父类 …,所以无法确定 gen.get方法的返回值类型到底是什么,这就导致了 get 方法无法被正常调用,想要拿到返回值只能使用 所有类型的父类Object
去接收
Object o = gen.getOne();
但是这就导致了 step1 中相同的问题:无法知晓 Object 持有的到底是哪一种类型,如何做转换
由上面的分析得知
使用下界通配符的方式可以修改 泛型类型中的泛型,但是无法读取 泛型类型中的泛型 – 只写
step6 实践
如果现在需要一个方法:将一个Gen<Number>中的属性拷贝到另一个Gen<Number>中
,就应该考虑到 step4
和 step5
中的可读性和可写性问题
public static <T> void copyT(Gen<? super T> t1 , Gen<? extends T> t2){
t1.setOne( t2.getOne() );
t1.setTwo( t2.getTwo() );
}
要将 g2
中的属性拷贝到 g1
中
g2
的数据是要被读取的,不需要去修改它的属性 === 只读,g1
的数据是要被修改的,不需要取到它的属性 === 只写
public static void main(String[] args) {
Gen<Float> floatGen = new Gen<Float>(123f,345f);
Gen<Float> floatGen2 = new Gen<Float>(678f,9f);
System.out.println(floatGen2.getOne().floatValue());// 678。0
copyT(floatGen2,floatGen);
System.out.println(floatGen2.getOne().floatValue()); // 123.0
}
public static <T> void copyT(Gen<? super T> t1 , Gen<? extends T> t2){
t1.setOne( t2.getOne() );
t1.setTwo( t2.getTwo() );
}
成功复制
jdk源码中
Collentions
类中的copy
方法形参使用的就是此种形式
step7 总结
综上可知:java 泛型实际上是一种抽象,T 可以表示任意一个
除基本数据类型外的类
那么泛型类型就可以作为一个模板,持有除基本数据类型外的任意一个类
java 世界中基础的数据类型只有那么多种,满足不了复杂的需求
在这基础之上,设计了类作为一种程序员可以自己定义的数据类型,解决了很多问题
其他语言如 c,也提供了结构体这种方式
而后 java 基于泛型的思想,提供了多种容器去存放这些类
使得程序员可以灵活的存放和处理一系列数据
如最常使用的 List 系列 Map 系列
而这些容器又是如何设计,如何存放类的
使用数组、链表、还是二叉树,又是一个值得去研究的问题
知识都不是孤立的,它们紧密相连
评论区