一. 泛型是什么
“泛型”,顾名思义,“泛指的类型”。我们提供了泛指的概念,但具体执行的时候却可以有具体的规则来约束,比如我们用的非常多的ArrayList就是个泛型类,ArrayList作为集合可以存放各种元素,如Integer, String,自定义的各种类型等,但在我们使用的时候通过具体的规则来约束,如我们可以约束集合中只存放Integer类型的元素,如List<Integer> iniData = new ArrayList<>()
。
二. 使用泛型有什么好处
以集合来举例,使用泛型的好处是我们不必因为添加元素类型的不同而定义不同类型的集合,如整型集合类,浮点型集合类,字符串集合类,我们可以定义一个集合来存放整型、浮点型,字符串型数据,而这并不是最重要的,因为我们只要把底层存储设置了Object即可,添加的数据全部都可向上转型为Object。 更重要的是我们可以通过规则按照自己的想法控制存储的数据类型。
我们以ArrayList为例,假如我们要将本月截至今天的日期放到ArrayList中,如果我们不使用泛型,此时我们定义一个ArrayList.
List monthDays = new ArrayList();
我们向其中加入1号到4号日期
public static List addMonthDays(){
List monthDays = new ArrayList();
monthDays.add(LocalDate.now().withDayOfMonth(1));
monthDays.add(LocalDate.now().withDayOfMonth(2));
monthDays.add(LocalDate.now().withDayOfMonth(3));
monthDays.add(new Date());
return monthDays;
}
这样有没有问题?大家也看出来了,当然有,虽然都可以表示日期,但却用了Date,LocalDate,我们调用方法直接打印出来,就是这样
public static void main(String[] args) {
List monthDays = addMonthDays();
for(Object day : monthDays){
System.out.println(day);
}
}
2019-08-01
2019-08-02
2019-08-03
Sun Aug 04 10:27:10 CST 2019
我们肯定不想要这样的结果,我们想要的是
2019-08-01
2019-08-02
2019-08-03
2019-08-04
如果存储的元素类型只是这两种(假如我们知道),这个时候我们就手动判断一下
public static void main(String[] args) {
List monthDays = addMonthDays();
for(Object day : monthDays){
if (day instanceof Date){
Date date = (Date) day;
System.out.println(LocalDate.of(date.getYear(), date.getMonth(), date.getDay()));
}else {
System.out.println(day);
}
}
}
这个时候我们就可以达成上述目的了,但大家也知道,这种写法问题问题很大
- 我们无法控制存储的元素到底是否和日期相关,如我们存储了“1”,65536等非日期,定义的方法也不会报错,但在调用进行类型转换的时候必然会报错;
- 我们不知道集合中到底存储了哪些类型的元素,比如还有“2019/08/04”这种日期字符串、java.sql.Date这种类型呢,我们很难保证可以穷尽;
- 代码过于复杂,太难维护;
这时泛型就提供了很好的解决方案,从源头上就制定好规则,只能添加LocalDate类型,那么上述的问题就都得以解决了。
public static List<LocalDate> addFormatMonthDays(){
List<LocalDate> monthDays = new ArrayList<>();
monthDays.add(LocalDate.now().withDayOfMonth(1));
monthDays.add(LocalDate.now().withDayOfMonth(2));
monthDays.add(LocalDate.now().withDayOfMonth(3));
monthDays.add(new Date());//这个代码在编译期间就无法通过了
return monthDays;
}
不仅提高了代码的可读性,一眼即可看出我们存储的是LocalDate类型。同时编译器也可很好的利用该信息,编译期间就可进行类型检查,保证了安全性,在get的时候,无需进行强制类型转换。
三. 泛型类
为使类适应更多的情况,具备更好的扩展性,我们可以将其设置为泛型类,即具有一个或多个类型变量的类,写法如下:
public class ClassName<泛型标识,可以是字母、中文字符等,不过一般用大写英文字符> {
private 泛型标识 property;
}
对于泛型标识符,一般有一些约定俗称的写法,如果是表示集合的元素类型,一般用字母E,如我们常用的ArrayList,public class ArrayList<E>
,我们自定义如下:
//车库
public class Garage<E> {
//向车库中添加车
public void add(E car){
//...
}
}
我们还可以用字符K和V表示关键字和值的类型,如我们常用的HashMap,public class HashMap<K,V>
,我们也可以自定义如下:
//映射关系
//K,V: 蔬菜,是否有机; 水果,产地; 服装,类型; 汽车,品牌
public class Mapping<K, V> {
private K key;
private V value;
}
我们还经常用一个字符T表示类型
public class Person<T> {
private T t;
public Person(T t){
this.t = t;
}
public void run(){
System.out.println(t);
}
}
如何使用泛型类,类型如何实例化,我们只需要保证传入的实参类型和泛型参数类型相同即可。
//正常用法
Person<String> s1 = new Person<String>("张三");
//jdk7.0后可以省略后面的参数类型
Person<String> s2 = new Person<>("张三");
s2.run();
//当然泛型的定义是可以帮助我们按照某种规则去做事,如果不做限制,也不会编译错误,但泛型就毫无意义了
Person t = new Person(111);
t.run();
//泛型的类型不能是八大基本类型,下面会编译出错
Person<int> s = new Person<>(1);
四. 泛型接口
泛型接口和泛型类的定义基本一致,定义如下:
public interface Person<T> {
public T parent();
public String eat();
}
当我们定义了一个类要实现该接口时,那么该类的泛型类型必须和接口类的泛型类型一致,未传递实参的情况下,继续使用泛型类型T,传递了实参的情况下,泛型类型必须使用实参类型
public class Teacher<T> implements Person<T> {
@Override
public T parent() {
return null;
}
@Override
public String eat() {
return null;
}
}
//Teacher不必再定义类型了,因为泛型类型在Person处已经定义好了
public class Teacher implements Person<Integer> {
//这里的返回类型必须为Integer,否则必须出错
@Override
public Integer parent() {
return null;
}
@Override
public String eat() {
return null;
}
}
五. 泛型方法
泛型方法可以定义在普通类和泛型类中,比如泛型类更为常用,一般能用泛型方法解决的问题优先使用泛型方法而不使用泛型类,类型变量放在修饰符的后面,如public static ,public final等的后面。
public class Teacher {
public static <T> T println(T t){
System.out.println(t);
return t;
}
}
调用很简单,很一般方法调用是一样的,更方便的是类型不像一般方法做了限定。
String s = Teancher.println("str");
另外需要说明的是,定义在泛型类中的泛型方法的泛型变量之间是没有关系的,如这样的代码
public class Teacher<T> {
T teacher;
public Teacher(T t){
this.teacher = t;
}
public <T> T println(T t){
System.out.println(t);
return t;
}
}
Teacher<String> teacher = new Teacher<>("张三");
Integer in = teacher.println(123456);
类泛型类型为String,方法的泛型类型为Integer,虽然都是用T来表示的。
同时关于泛型方法需要说明的是:
在修饰符public xx与方法名之间非常重要,有< T >这样的才算是泛型方法;仅仅使用了泛型变量并不算是泛型方法。
六. 限定类型变量
不论是泛型类还是泛型方法,目前来说其实都是没有做类型限定,无论我们传递什么样类型的变量进去都可以,因为我们在处理逻辑中并没有使用到该类型特有的东西(成员变量、方法等)。假如我们想传递的参数类型仅仅是某个大类(父类)下面的一些小类(子类),那么怎么做呢?
public class ArrayFlag {
public static <T> T getMax(T[] array){
if(array == null || array.length == 0){
return null;
}
T maxValue = array[0];
for(int i = 0; i < array.length; i++){
if(array[i].compareTo(maxValue) > 0){
maxValue = array[i];
}
}
return maxValue;
}
}
大家也看到了我们在getMax方法中使用了compareTo方法进行比较,但如果我们传入的类型T没有compareTo方法呢,岂不是要报错,因此我们需要做限定,只要限定了是Comparable接口的必然具备compareTo方法,那么改造后就成了这样
public class ArrayFlag {
public static <T extends Comparable> T getMax(T[] array){
if(array == null || array.length == 0){
return null;
}
T maxValue = array[0];
for(int i = 0; i < array.length; i++){
if(array[i].compareTo(maxValue) > 0){
maxValue = array[i];
}
}
return maxValue;
}
}
同时需要说明的是,此处用的是extends关键字,extends在这里是表示的是绑定了Comparable接口及其子类型,是“绑定、限定”的意思,非“继承”的意思,后面也可以是接口或者类,如果有多个限制,可以使用&分隔,如:
public static <T extends Comparable & Serializable> T getMax(T[] array)
七. 泛型通配符
举个例子,定义了一个书籍类和一个小说类
//定义了一个书籍类
public class Book {}
//定义了一个小说书籍类继承书籍类
public class Novel extends Book {}
我们再定义一个书柜类用来装书籍以及小说
//定义了一个书柜类用来装书
public class Bookcase<T> {
T b;
public Bookcase(T t){
this.b = t;
}
public void set(T t) {
b=t;
}
public T get() {
System.out.println(b.getClass());
return b;
}
}
下面我们就用书柜来装小说
//以前的写法,无法编译通过,提示Incompatible types, Required Book Found Novel
Bookcase<Book> bc = new Bookcase<Novel>(new Novel());
但在jdk7.0后,new Bookcase的时候是可以不用给出泛型类型的,省略的类型可以从变量的类型推断得出,因此如果下面这种写法
Bookcase<Book> bc = new Bookcase<>(new Novel());
System.out.println(bc.getClass());
bc.get();
此时可以编译通过,我们执行后得出的结果是:
class generic.Bookcase
class generic.Novel
当然我们还可以通过通配符来解决该问题,通配符包括以下几种:
上界通配符、下界通配符、无限定通配符
7.1 上界通配符
上界通配符定义方式如下:用extends 关键字,含义是该书柜只能放置小说类书籍(如什么都市小说、爱情小说、玄幻小说都可以),但不能放置父类书籍、其他类如史书、职场类书籍、财经类书籍等,是在使用的时候进行限定,如:
Bookcase<? extends Book> bc = new Bookcase<Novel>(new Novel());
这种定义方式就不会编译错误了。另外关于上界通配符的特点,对上有限制,根据java多态向上造型的原则,不适合频繁插入数据,适合频繁读取数据的场景。
7.2 下界通配符
下界通配符定义方式如下:用super关键字,含义就是书柜放置设置了下限,我们只能放置Book书籍以及Novel书籍,却无法再将细分的都市小说、爱情小说类书籍放进去
Bookcase<? super Novel> bc = new Bookcase<Novel>(new Novel());
另外关于下界通配符的特点,和上界通配符正好相反,不适合频繁读取数据,适合频繁插入数据的场景。
7.3 无限定通配符
无限定通配符意味着可以使用任何对象,因此使用它类似于使用原生类型。但它是有作用的,原生类型可以持有任何类型,而无限定通配符修饰的容器持有的是某种具体的类型。
举个例子:
List<?> list = new ArrayList<>();
//无法编译通过
list.add(new Object());
//下面这样的却可以添加任何类型
List<Object> list = new ArrayList<>();
list.add(new Object());
再说一下< T > 和< ? >之间的区别,初看好像他们都可以表示泛型变量,都可以extends,但它们确实有不同的使用场景
- 类型参数< T >声明一个泛型类或泛型方法
- 无限定通配符< ? >使用泛型类或泛型方法
八. 总结
泛型在java中可以说很常用,我们前面提到的集合类,如ArrayList,HashSet,以及Map都使用到了泛型,泛型也是也是我们再进行一些组件封装经常用到的,本文主要介绍了泛型基本概念,使用泛型的好处,泛型类、接口、方法、通配符的简单介绍以及使用方法,最后泛型一般和反射集合使用,通过泛型可以进行类型的灵活传递,通过反射可获取到实体以及类的数据信息,从而实现一些框架、组件的封装,若有不对之处,请批评指正,望共同进步,谢谢!