前言
在写到集合篇的一篇map集合时,发现泛型对于我们定义集合类型有着很好的作用,可以让我在运行时产生的错误转到编译时,这样就可以降低我们代码的出错率,所以小编来写一下关于java中泛型的使用🎉
1、什么是泛型?
泛型是一种编程范式,它允许我们在编写程序时使用类型参数来表示其他类型。可以将类型参数看作是一种占位符,用于表示使用该类或方法时可能遇到的其他具体类型,从而实现编写更加通用和可复用的代码。Java的泛型机制广泛应用于集合框架、IO流、反射等方面,是Java编程中不可或缺的一部分。
2、泛型的优点有哪些?
-
可读性更佳:使用泛型可以使代码更加通用,易于理解和使用。
-
类型安全:泛型可以在编译时进行类型检查,从而避免运行时出现类型转换错误。
-
可重用性更好:编写泛型代码可以增加代码的可重用性,同时也可以减少代码的冗余。
3、如何使用泛型?
3.1、泛型的基本用法
// 集合的声明
List list = new ArrayList();
list.add("test");
list.add(1);
// 集合的使用
String str = (String) list.get(1);
在上面的代码中我们可以看到,我们是在编译的时候是不会产生错误的,但是我们看看下面的运行错误
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
//我们可以看到,这里报出了一个错误,就是无法将这个int类型的数据转成String类型
// 声明泛型集合的时候指定元素的类型
List<String> list = new ArrayList<String>();
list.add("test");
// list.add(1);// 编译时期直接报错
String str = list.get(1);
在这段代码 中,我们声明了集合的泛型类型,所以在添加数据的时候,立马就报错了,这就体现出了泛型的作用,把我们运行时候的错误,转到了编译时
注意:泛型的类型参数只能是类类型(包括自定义类),不能是简单类型(如int)
3.2、泛型擦除
泛型擦除是编译器在编译阶段将泛型类型参数擦除,再用上限或 Object 类型来代替。泛型擦除的目的是为了保证代码能够兼容旧版的 Java 虚拟机,因为泛型是在 Java 5 中引入。以下是一个泛型擦除的例子:
3.2.1、例子1:
public class Test<E> {
private E[] elements;
public Test(int capacity) {
this.elements = (E[]) new Object[capacity];
}
}
在编译阶段 E 变量信息被擦除,所以编译后的代码会变成:
public class Test<E> {
private E[] elements;
public Test(int capacity) {
this.elements = new Object[capacity];
}
}
注意到在泛型擦除后,elements 被重新赋值为 Object[],而不是 E[],所以编译器会在编译时忽略泛型类型 E,并且允许初始化 Object 数组类型
3.2.2、例子2:
// 泛型擦除实例
public void save(List<Person> p){
}
public void save(List<Dept> d){ // 报错: 与上面方法编译后一样
}
在例子2中两个重名的方法看似是方法的重载,其实参数类型一致,会报错。因为在编译过程中,正确检验泛型结果后,会将泛型的相关信息擦出,并且在对象进入和离开方法的边界处添加类型检查和类型转换的方法。也就是说,泛型信息不会进入到运行时阶段。
总结就是,泛型类型在逻辑上看似是多个不同的类型,实际上都是相同的基本类型。
3.3、泛型的使用
3.3.1、泛型方法
public class GenericDemo {
// 定义泛型方法
public <T> T save(T t) {
return null;
}
// 测试方法
@Test
public void testMethod() throws Exception {
// 使用泛型方法: 在使用泛型方法的时候,确定泛型类型
save(1);
}
}
泛型方法的泛型声明必须是在修饰符之前返回值类型之后定义。例如上面的 “public <> T”;
只有声明了”<>”的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
还有一点要注意的是:泛型方法在使用的时候,才确定泛型类型。如上面代码save(1);被调用的的时候才确定类型为Integer.
泛型方法中的T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。并且泛型的数量也可以为任意多个。
public class GenericDemo {
// 定义泛型方法
public <K,T> T save(T t,K k) {
return null;
}
// 测试方法
@Test
public void testMethod() throws Exception {
// 使用泛型方法: 在使用泛型方法的时候,确定泛型类型
save(1.0f, 1);//Double,Integer类型
}
}
3.3.2、泛型类
public class GenericDemo<T> {
// 定义泛型方法
public <K> T save(T t,K k) {
return null;
}
public void update(T t) {
}
// 测试方法
@Test
public void testMethod() throws Exception {
// 泛型类: 在创建泛型类对象的时候,确定类型
GenericDemo<String> demo = new GenericDemo<String>();
demo.save("test", 1);
}
}
从上面例子中我们可以看到 泛型类的类型声明是在类名之后的。并且在方法中如果需要用到这个T类型可以不用再次声明,直接可以用了。但是K类型还需要在方法中声明一下。
还有就是 当创建泛型类对象的实例时,指定类型为String,才确定类型。
3.3.3、泛型接口
首先我们定义一个泛型的接口
//定义一个泛型接口
public interface Generator<T> {
public T next();
}
然后实现
//当实现泛型接口的类,未传入泛型实参时:
/** * 未传入泛型实参时,与泛型类的定义相同,
在声明类的时候,需将泛型的声明也一起加到类中 *
即 class FruitGenerator<T> implements Generator<T>{
* 如果不声明泛型,如:class FruitGenerator implements Generator<T>,编译器会报错:"Unknown class" */
如下正确:
class FruitGenerator<T> implements Generator<T>{
@Override
public T next() {
return null;
}
}
当实现泛型接口的类传入泛型实参的时候
public class FruitGenerator implements Generator<String> {
@Override
public String next() {
return "fruit";
} }
//注意 当实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 即:Generator<>,public T next();中的的T都要替换成传入的String类型。
注意 当实现类实现泛型接口时,如已将泛型类型传入实参类型,则所有使用泛型的地方都要替换成传入的实参类型 即:Generator<>,public T next();中的的T都要替换成传入的String类型。
3.4、泛型的关键字
3.4.1、? (通配符:接收值)
当我们在调用方法执行之后返回值的时候,或者在方法传递参数的时候,我们不清楚到底返回什么类型,或者传递什么类型,这个时候可以用 ?来代替泛型类型,可以实现扩展性。
public class Test {
// 只带泛型特征的方法
public void save(List<?> list) {
// 只能获取、迭代list; 不能修改list
}
public static void main(String[] args) {
// ? 可以接收任何泛型集合, 但是不能修改集合元素; 所以一般在方法参数中用
List<?> list1 = new ArrayList<String>();
List<?> list2 = new ArrayList<Integer>();
Test test = new Test();
test.save(list1);
test.save(list2);
// list.add("");// 会报错
}
}
- 代码中 List 可以接受两种泛型类型的集合。但是不能修改它
- 上面不用再写两个方法来接受不同泛型集合的传递参数,而是只用一个方法来接受所有泛型类型的集合参数。
- 之前看到的,类型通配符一般是使用?代替具体的类型实参,注意了,此处’?’是类型实参,而不是类型形参 。再直白点的意思就是,此处的?和Number、String、Integer一样都是一种实际的类型,可以把?看成所有类型的父类。是一种真实的类型。
3.4.2、extends(上线)
extends关键字的意思就是类型实参只准传入某种类型的子类包括自身。也就是会限定范围。如果不是该父类类型的子类,则报错。
public class App_extends {
/**
* list集合只能处理 Double/Float/Integer等类型
* 限定元素范围:元素的类型要继承自Number类 (上限)
* @param list
*/
public void save(List<? extends Number> list) {
}
@Test
public void testGeneric() throws Exception {
List<Double> list_1 = new ArrayList<Double>();
List<Float> list_2 = new ArrayList<Float>();
List<Integer> list_3 = new ArrayList<Integer>();
List<String> list_4 = new ArrayList<String>();
// 调用
save(list_1);//通过
save(list_2);//通过
save(list_3);//通过
//save(list_4);//会报错 因为String不是Number的子类
}
}
3.4.3、super(下限)
super关键字的意思就是类型实参只准传入某种类型的父类包括自身。
public class App_super {
/**
* super限定元素范围:必须是String父类 【下限】
* @param list
*/
public void save(List<? super String> list) {
}
@Test
public void testGeneric() throws Exception {
// 调用上面方法,必须传入String的父类
List<Object> list1 = new ArrayList<Object>();
List<String> list2 = new ArrayList<String>();
List<Integer> list3 = new ArrayList<Integer>();
//save(list1);//通过
//save(list2);//通过
//save(list3);//报错 因为Integer 不是String的父类
}
}
4、使用泛型编写简单的Basedao
通用的BaseDao,里面有通用的增删改查等常用数据库操作方法,然后让其他模块继承此类,这样不用每次去写一遍了,提高效率。
要想写一个通用的数据库操作方法,必须知道数据库中的表名和操作的对象类型。
因此,要想写一个通用类,我们必须约定数据库中的表名必须和类名一直。
那么类名在哪里可以获得,只能通过泛型类来实现,然后让子类继承BaseDao,在子类中指定父类的泛型类型。
例如:
Class BaseDao<T>{
//根据主键查询通用方法
Public T findById(int id){
//获取对象
//获取表
}
}
Class AdminDao extends BaseDao<Admin>{}
因此在创建子类AdminDao的实例时,将Admin通过泛型传递给BaseDao,然后帮助我们实现数据库通用操作。那么在父类中如何拿到子类的类型。首先我们要知道
什么是参数化类型?例如:“ArrayList《String》 ” 为参数化类型
还需要知道一个类。 ParameterizedType 通过这个类 我们能获取子类的类型。
public class AdminDao extends BaseDao<Admin> {}
public class AccountDao extends BaseDao<Account> {}
/**
* 所有dao的通用方法
*
*/
public class BaseDao<T>{
// 保存当前运行类的参数化类型中的实际的类型
private Class clazz;
// 表名
private String tableName;
// 构造函数: 1. 获取当前运行类的参数化类型;
//2. 获取参数化类型中实际类型的定义(class)
public BaseDao(){
// this 表示当前运行类 (AccountDao/AdminDao)
// this.getClass() 当前运行类的字节码 (AccountDao.class/AdminDao.class)
// this.getClass().getGenericSuperclass(); 当前运行类的父类,即为BaseDao<Account>
// 其实就是“参数化类型”, ParameterizedType
Type type = this.getClass().getGenericSuperclass();
// 强制转换为“参数化类型” 【BaseDao<Account>】
ParameterizedType pt = (ParameterizedType) type;
// 获取参数化类型中,实际类型的定义 【new Type[]{Account.class}】
Type types[] = pt.getActualTypeArguments();
// 获取数据的第一个元素:Accout.class
clazz = (Class) types[0];
// 表名 (与类名一样,只要获取类名就可以)
tableName = clazz.getSimpleName();
}
/**
* 主键查询
* @param id 主键值
* @return 返回封装后的对象
*/
public T findById(int id){
/*
* 1. 知道封装的对象的类型
* 2. 表名【表名与对象名称一样, 且主键都为id】
*
* 即,
* ---》得到当前运行类继承的父类 BaseDao<Account>
* ----》 得到Account.class
*/
String sql = "select * from " + tableName + " where id=? ";
try {
return JdbcUtils.getQuerrRunner().query(sql, new BeanHandler<T>(clazz), id);
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 查询全部
* @return
*/
public List<T> getAll(){
String sql = "select * from " + tableName ;
try {
return JdbcUtils.getQuerrRunner().query(sql, new BeanListHandler<T>(clazz));
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
好啦本期小编就写到这里啦,觉得还有什么要进行补充的记得提醒小编哟,不介意的话来波一键三连吧~😊