java里程碑之泛型--类型通配符
- 1,泛型与数组在子类上面的对比
在整理java泛型的类型通配符之前,我们先来研究下java在数组设计上的一个不合理。我们来看下面代码:
public class Test
{ public static void main(String[] args)
{
//1。定义一个Integer数组
Integer[] intArray = new Integer[2];
//2。定义一个Number数组,将上面的Integer数组赋值给Number数组
Number[] numArray = intArray;
//3。给现在的数组赋值一个double类型
numArray[0] = 1.0;
//4。检查现在数组的第一个数据
System.out.println(numArray[0]);
//Exception in thread "main" java.lang.ArrayStoreException: java.lang.Double
} }
在数组中,程序可以直接把一个integer[]数组赋值给一个number[]变量。如果试图把一个Double对象保存到该Number[]数组中,编译可以通过,但是在运行时会抛出ArrayStoreException异常的。正如所有编程语言一样,一门设计优秀的语言不仅需要提供强大的功能,而且应该提供强大的错误提示和出错警告,这样子才能尽量避免开发者犯错。但是java允许将Integer[]数组赋值给Number[]变量这显然是一种不安全的设计。
也正是因为上面有可能出现的缺陷,java在设计泛型的时候就进行了改进,java规定:不可以把List<Integer>对象赋值给List<Number>变量。比如说下面的代码将会报错,编译就会报错:
List<Integer> intList = new ArrayList<>();
//Type mismatch: cannot convert from List<Integer> to List<Number>
List<Number> numList = intList;
其实,java泛型的设计原则就是只要代码在编译时没有出现警告,就不会遇到运行时ClassCastException异常。
上面2者对比:如果A是B的一个子类型,包括子类或者子接口,那么A[]依然是B[]的子类型,但是G<A>不是G<B>的子类型。
- 2,使用类型通配符
OK,那现在我们假设一种情景,我们需要定义一个方法,该方法里面有一个集合形参,集合形参的元素类型是不确定的,那我们应该要怎么写呢?
现在我们来写一段最普通的便利List集合的代码:
public void test(List list)
{
for (int i = 0; i < list.size(); i++)
{
System.out.println(list.get(i));
}
}
上面的代码很简单,但是有一个问题就是说我们在使用上面的list的时候没有传入实际的类型参数,这将引起泛型警告。其实我们也可以理解现在的泛型T就相当于Object类型,这样子可以解决实际的类型参数警告问题,但是我们在调用这个方法的时候传入的实际参数值可能不是我们所期望的,比如下面的调用代码就会报错:
public void test(List<Object> list)
{
for (int i = 0; i < list.size(); i++)
{
System.out.println(list.get(i));
}
} public static void main(String[] args)
{
Test test = new Test();
List<String> list = new ArrayList<>();
test.test(list);
}
上面的代码编译时候就不通过,报错说The method test(List<Object>) in the type Test is not applicable for the arguments (List<String>),也就是说List<String>对象不能被当成List<Object>对象来使用,我们前面说过了,List<String>类并不是List<Object>类的子类。这个时候我们应该要怎么办呢?使用类型通配符。
类型通配符就是一个问号,将一个问号作为类型实参传给list集合,写作List<?>就可以。这种语法的意思是:List<?>是一个元素类型未知的list,然后它的元素类型可以匹配任何类型。我们修改上面的代码:
public void test(List<?> list)
{
for (int i = 0; i < list.size(); i++)
{
System.out.println(list.get(i));
}
} public static void main(String[] args)
{
Test test = new Test();
List<String> list = new ArrayList<>();
test.test(list);
}
OK,现在没有问题了,编译和运行都可以通过。
- 关于使用类型通配符有2点注意:
1),这种带通配符的list仅仅表示它是各种泛型List的父类,但是并不能将元素添加在其中,除了null之外。其实这点也很好理解的,因为程序不能确定集合里面的元素的类型是什么,当然不能想其中添加对象。
2),程序可以调用get()方法来获得指定索引处的元素,但是其返回值是一个未知类型,其实也就是Object类型。
- 3,设定类型通配符的上限
我们前面已经代码演示了我们的情景,使用List<?>可以表示任何泛型list的父类。但是假如我们现在要用到更加详细的一种情况呢,比如说我不希望这个List<?>是用来表示任何泛型list的父类,只是希望它代表某一类泛型list的父类。我们使用通配符问号来表示某一类,但是因为我们没有加类型通配符的上限的限制,我们不可避免的在使用父类的方法时候还要做强转,将原来的Object类向下强转到我们的父类,然后才能调用我们定义好的父类的方法。为了满足这种需求,java泛型提供了被限制的泛型通配符。这里说的具体的被限制分为上限和下限2种情况,我们先来看上限,语法是List<?
extends 父类>具体的看下面的代码演示:
package com.linkin.maven.mavenTest; import java.util.ArrayList;
import java.util.List; public class Test
{ //这个方法太土了,使用了泛型但是要强转
public void test1(List<?> list)
{
for (Object obj : list)
{
if (obj instanceof A)
{
A a = (A) obj;
a.test();
}
}
} public void test(List<? extends A> list)
{
//list.add(new B());此时list代表的类型是未知的,当然不能往里面添加元素了
for (A a : list)
{
a.test();
}
} public static void main(String[] args)
{
Test test = new Test();
List<A> list = new ArrayList<>();
list.add(new B());
list.add(new C());
test.test(list);
//子类B实现抽象类A的方法。。。
//子类C实现抽象类A的方法。。。
} } abstract class A
{
abstract void test();
} class B extends A
{ @Override
void test()
{
System.out.println("子类B实现抽象类A的方法。。。");
} } class C extends A
{ @Override
void test()
{
System.out.println("子类C实现抽象类A的方法。。。");
} }
上面的代码中,List<? extends A>是受限制通配符的一个例子,此处的问号代表一个未知的类型,就想前面我们看到的通配符一样。但是此处的未知类型一定是A的子类,或者是A本身。同样的因为我们无法确定这个类型是什么,我们也不能将任何对象添加到这个集合中。
- 4,设定类型形参的上限
java泛型不仅可以在使用通配符形参时设定上限,而且还可以在定义类型形参的时候设定上限,用于表示传给该类型参数的实际类型要不就是该上限类型,要不就是该上限类型的子类。具体请看下面代码:
package com.linkin.maven.mavenTest; import java.util.ArrayList;
import java.util.List; public class Test<T extends A>
{ public static void main(String[] args)
{
Test<A> test1 = new Test<>();
Test<B> test2 = new Test<>();
//下面代码报错。
Test<Integer> test3 = new Test<>();
} } abstract class A
{
abstract void test();
} class B extends A
{ @Override
void test()
{
System.out.println("子类B实现抽象类A的方法。。。");
} } class C extends A
{ @Override
void test()
{
System.out.println("子类C实现抽象类A的方法。。。");
} }
在实际编码过程中,还有一种情况就是说我们为类型形参设定多个上限,至多有一个父类上限,但是可以有多个接口上限,表明该类型形参必须是其父类的子类,并且要实现多个上限接口。具体的语法是:public class Test<T extends A & Serializable>
- 5,设定类型通配符的下限
如果java仅仅提供类型通配符的上限的话,会在操作某些特定的情景时有缺陷的。完美的东西都是对应的,所以有上限也肯定有下限。现在我们有下面情景:
我们自己要实现一个工具方法,实现将src集合里的元素复制到dest集合里的功能。因为dest集合可以保存src集合里面的所有元素,所以dest集合里面元素的类型应该是src集合里面元素类型的父类。现在我们只使用上限来实现下上面的功能:
public static <T> void copy(List<? extends T> src, List<T> dest)
{
for (T t : src)
{
dest.add(t);
} }
OK,功能实现了。我们在改下情景,现在我们假设该方法需要一个返回值,返回最后一个被复制的元素。代码修改如下:
public static <T> T copy(List<? extends T> src, List<T> dest)
{
T lastt = null;
for (T t : src)
{
dest.add(t);
lastt = t;
}
return lastt;
}
表面上看起来上面的方法实现了这个功能,但是我们仔细研究下就会发现一个问题。我们上面代码复制过程中我们进行复制的元素的类型其实是T的子类,但是最后我们得到的这个元素的类型变成了T,也就是说程序在复制元素的过程中,丢失了src集合元素的类型。代码如下:
package com.linkin.maven.mavenTest; import java.util.ArrayList;
import java.util.List; public class Test
{
public static <T> T copy(List<? extends T> src, List<T> dest)
{
T lastt = null;
for (T t : src)
{
dest.add(t);
lastt = t;
}
System.out.println(lastt.getClass());
return lastt;
} public static void main(String[] args)
{
List<Integer> src = new ArrayList<>();
src.add(1);
List<Number> dest = new ArrayList<>();
Number number = Test.copy(src, dest);
//虽然实际类型都是Integer,但是这里还是需要强转的
Integer number1 = (Integer) Test.copy(src, dest);
} }
OK,那我们现在使用泛型通配符的下限,就可以很好的绕开这个问题。设定通配符的下限语法是:<? super type>,这个通配符表示它必须是Type本身,或是Type的父类。我们修改前面的方法,不会在发生丢失类型的这种情况了呢。
public class Test
{
public static <T> T copy(List<T> src, List<? super T> dest)
{
T lastt = null;
for (T t : src)
{
dest.add(t);
lastt = t;
}
System.out.println(lastt.getClass());
return lastt;
} public static void main(String[] args)
{
List<Integer> src = new ArrayList<>();
src.add(1);
List<Number> dest = new ArrayList<>();
Integer number1 = Test.copy(src, dest);
} }
关于这一点,我们可以通过使用这种通配符下限的方式定义某个类构造器的参数,就可以将所有可用的参数,或者参数的父类传入,增强了程序的灵活性。比如JDK中TreeSet和TreeMap都有这种用法。具体代码如下:
public TreeSet(Comparator<? super E> comparator) {
this(new TreeMap<>(comparator));
}
public interface Comparator<T> {}
下面是具体的使用代码:
public static void main(String[] args)
{
TreeSet<String> ts1 = new TreeSet<>(new Comparator<Object>()
{
@Override
public int compare(Object o1, Object o2)
{
return 0;
}
}); TreeSet<String> ts2 = new TreeSet<>(new Comparator<String>()
{
@Override
public int compare(String o1, String o2)
{
return 0;
}
}); TreeSet<Integer> ts3 = new TreeSet<>(new Comparator<Integer>()
{
@Override
public int compare(Integer o1, Integer o2)
{
return 0;
}
});
}
上面的代码使用这种带下限的通配符的语法,可以在创建TreeSet对象时候灵活的选择合适的Comparator,编程很灵活也很方便。
java里程碑之泛型--类型通配符的更多相关文章
- 黑马程序员——JAVA基础之泛型和通配符
------- android培训.java培训.期待与您交流! ---------- 泛型: JDK1.5版本以后出现新特性.用于解决安全问题,是一个类型安全机制. 泛型好处: ...
- java里程碑之泛型--泛型方法
前面我已经介绍过了,我们可以在定义类和接口的时候使用类型形参,在该类的方法定义中,成员变量定义中,这些类型形参都可以被当成普通类型来使用.但是如果我们在定义类和接口的时候没有使用类型形参,但是在定义方 ...
- Java 基础 - 泛型类/泛型方法/类型通配符'?' 的用法及栗子
笔记: /**1.定义一个PairTest泛型类, 测试泛型 类 Pair的用法 * class Pair<T>{ * private T first; * private T secon ...
- java里程碑之泛型--泛型注意的几点
1,泛型的基本语法:类名<具体类> 对象名 = new 类名<具体类>().类型参数规范如下: 1),K键,比如映射的键,key的类型 2),V值,比如Map的值,value类 ...
- java里程碑之泛型--擦除和转换
在严格的泛型代码里,带泛型声明的类总应该带着泛型参数.但是为了和古老的java代码保持一致,也就是说为了向下兼容,也允许在使用带泛型声明的类时不指定实际的类型参数.如果没有为这个泛型类指定实际的参数类 ...
- java里程碑之泛型--泛型基本语法
1,java7提供的泛型菱形语法 在java7之前,如果使用带泛型的接口和类定义变量初始化对象的时候,构造器后面也必须带上泛型,这有点恶心的.以前我在公司一直使用的java6,所以我也已经习惯了这种写 ...
- java里程碑之泛型--深入理解泛型
所谓泛型,就是允许在定义类,接口,方法时使用类型形参,这个类型形参将在声明变量,创建对象,调用方法的时候动态的指定.JAVA5之后修改了集合中所有的接口和类,为这些接口和类都提供了泛型的支持. 关于泛 ...
- java里程碑之泛型--使用泛型
现在重新整理下泛型,前面的整理好多的底层的东西理解不深.泛型很重要的,作为java的一个程碑式的改进,还是很有必要认真的理解下人家的JDK的良苦用心的. 1,什么是泛型?为什么要使用泛型? 一定要记住 ...
- Java基础之泛型——使用通配符类型参数(TryWildCard)
控制台程序 使用通配符类型参数可以设定方法的参数类型,其中的代码对于泛型类的实际类型参数不能有任何依赖.如果将方法的参数类型设定为Binary<?>,那么方法可以接受BinaryTree& ...
随机推荐
- mysql数据库-------基础
一 数据库是什么 把文件存放于一台机器,然后将多台机器通过网络去访问这台机器上的文件,即共享这台机器上的文件,共享则意味着竞争,会发生数据不安全,需要加锁处理,为了远程访问并处理这台共享机器上的文件, ...
- sql server存储过程实现批量删除
在项目中用到了存储过程来进行批量删除的操作,给大家分享一下 原理就是把id组成的字符串在数据库分割成数组放一张临时表,删除的时候与id进行对照 --删除会员信息 if OBJECT_ID('pro_D ...
- webpack基础打包安装分享
一.创建webpack-first文件夹作为站点,创建app文件夹存放js原始模块(main.js 和 Greeter.js) 创建 public文件夹存放index.html和打包后的bundle. ...
- contain_of宏定义
Container_of在Linux内核中是一个常用的宏,用于从包含在某个结构中的指针获得结构本身的指针,通俗地讲就是通过结构体变量中某个成员的首地址进而获得整个结构体变量的首地址. 实现方式: co ...
- Spring-Blog:个人博客(一)-Mybatis 读写分离
概述: 2018,在平(tou)静(lan)了一段时间后,开始找点事情来做.这一次准备开发一个个人博客,在开发过程之中完善一下自己的技术.本系列博客只会提出一些比较有价值的技术思路,不会像写流水账一样 ...
- 网络编程(sock)搞定!
前些日子写了一个网络编程的,纯新手,能优化的地方很多!但是,也算自己独立完成了这么一个东西,晚上发上来!!
- UVALive 3882 - And Then There Was One【约瑟夫问题】
题目链接:https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_ ...
- [51nod1440]迈克打电话
有n只熊,从1到n进行编号. 第i只熊的电话号码是si.每只熊会给那些电话号码是他的子串的熊打电话(可能会给自己打). call(i, j) 表示第i只熊给第j只熊打电话的次数,也就是第j个串在第i个 ...
- CodeForces801-A.Vicious Keyboard-暴力
A. Vicious Keyboard time limit per test 2 seconds memory limit per test 256 megabytes input standard ...
- hdu_3003Pupu(快速幂)
题目链接:http://acm.hdu.edu.cn/showproblem.php?pid=3003 Pupu Time Limit: 2000/1000 MS (Java/Others) M ...