深入理解Java泛型(一.泛型的作用与定义)
泛型的作用与定义
类型的参数化,就是可以把类型像方法的参数那样传递
泛型使编译器可以在编译期间对类型进行检查以提高类型安全,减少运行时由于对象类型不匹配引发的异常。
1. 泛型是什么
一说到泛型,大伙肯定不会陌生,我们代码里面有很多类似这样的语句:
List<String> list=new ArrayList<>();
ArrayList就是个泛型类,我们通过设定不同的类型,可以往集合里面存储不同类型的数据类型(而且只能存储设定的数据类型,这是泛型的优势之一)。“泛型”简单的意思就是泛指的类型(参数化类型)。想象下这样的场景:如果我们现在要写一个容器类(支持数据增删查询的),我们写了支持String类型的,后面还需要写支持Integer类型的。然后呢?Doubel、Float、各种自定义类型?这样重复代码太多了,而且这些容器的算法都是一致的。我们可以通过泛指一种类型T,来代替我们之前需要的所有类型,把我们需要的类型作为参数传递到容器里面,这样我们算法只需要写一套就可以适应所有的类型。最典型的的例子就是ArrayList了,这个集合我们无论传递什么数据类型,它都能很好的工作。
聪明的同学看完上面的描述,灵机一动,写出了下面的代码:
class MyList{ private Object[] elements=new Object[10]; private int size; public void add(Object item) {
elements[size++]=item;
} public Object get(int index) { return elements[index];
}
}
这个代码灵活性很高,所有的类型都可以向上转型为Object类,这样我们就可以往里面存储各种类型的数据了。的确Java在泛型出现之前,也是这么做的。但是这样的有一个问题:如果集合里面数据很多,某一个数据转型出现错误,在编译期是无法发现的。但是在运行期会发生java.lang.ClassCastException。例如:
MyList myList=new MyList();
myList.add("A");
myList.add(1);
System.out.println(myList.get(0));
System.out.println((String)myList.get(1));
我们在这个集合里面存储了多个类型(某些情况下容器可能会存储多种类型的数据),如果数据量较多,转型的时候难免会出现异常,而这些都是无法在编译期得知的。而泛型一方面让我们只能往集合中添加一种类型的数据,同时可以让我们在编译期就发现这些错误,避免运行时异常的发生,提升代码的健壮性。
2. Java泛型介绍
下面我们来介绍Java泛型的相关内容,下面会介绍以下几个方面:
- Java泛型类
- Java泛型方法
- Java泛型接口
Java泛型类
类结构是面向对象中最基本的元素,如果我们的类需要有很好的扩展性,那么我们可以将其设置成泛型的。假设我们需要一个数据的包装类,通过传入不同类型的数据,可以存储相应类型的数据。我们看看这个简单的泛型类的设计:
class DataHolder<T>{
T item; public void setData(T t) { this.item=t;
} public T getData() { return this.item;
}
}
泛型类定义时只需要在类名后面加上类型参数即可,当然你也可以添加多个参数,类似于,等。这样我们就可以在类里面使用定义的类型参数。
泛型类最常用的使用场景就是“元组”的使用。我们知道方法return返回值只能返回单个对象。如果我们定义一个泛型类,定义2个甚至3个类型参数,这样我们return对象的时候,构建这样一个“元组”数据,通过泛型传入多个对象,这样我们就可以一次性方法多个数据了。
Java泛型方法
前面我们介绍的泛型是作用于整个类的,现在我们来介绍泛型方法。泛型方法既可以存在于泛型类中,也可以存在于普通的类中。如果使用泛型方法可以解决问题,那么应该尽量使用泛型方法。下面我们通过例子来看一下泛型方法的使用:
class DataHolder<T>{
T item; public void setData(T t) { this.item=t;
} public T getData() { return this.item;
} /**
* 泛型方法
* @param e
*/
public <E> void PrinterInfo(E e) {
System.out.println(e);
}
}
我们来看运行结果:
1AAAAA8.88
从上面的例子中,我们看到我们是在一个泛型类里面定义了一个泛型方法printInfo。通过传入不同的数据类型,我们都可以打印出来。在这个方法里面,我们定义了类型参数E。这个E和泛型类里面的T两者之间是没有关系的。哪怕我们将泛型方法设置成这样:
//注意这个T是一种全新的类型,可以与泛型类中声明的T不是同一种类型。public <T> void PrinterInfo(T e) {
System.out.println(e);
}//调用方法DataHolder<String> dataHolder=new DataHolder<>();
dataHolder.PrinterInfo(1);
dataHolder.PrinterInfo("AAAAA");
dataHolder.PrinterInfo(8.88f);
这个泛型方法依然可以传入Double、Float等类型的数据。泛型方法里面的类型参数T和泛型类里面的类型参数是不一样的类型,从上面的调用方式,我们也可以看出,泛型方法printInfo不受我们DataHolder中泛型类型参数是String的影响。我们来总结下泛型方法的几个基本特征:
- public与返回值中间非常重要,可以理解为声明此方法为泛型方法。
- 只有声明了的方法才是泛型方法,泛型类中的使用了泛型的成员方法并不是泛型方法。
- 表明该方法将使用泛型类型T,此时才可以在方法中使用泛型类型T。
- 与泛型类的定义一样,此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型。
Java泛型接口
Java泛型接口的定义和Java泛型类基本相同,下面是一个例子:
//定义一个泛型接口public interface Generator<T> { public T next();
}
此处有两点需要注意:
- 泛型接口未传入泛型实参时,与泛型类的定义相同,在声明类的时候,需将泛型的声明也一起加到类中。例子如下:
/* 即:class DataHolder implements Generator<T>{
* 如果不声明泛型,如:class DataHolder implements Generator<T>,编译器会报错:"Unknown class"
*/class FruitGenerator<T> implements Generator<T>{ @Override
public T next() { return null;
}
}
- 如果泛型接口传入类型参数时,实现该泛型接口的实现类,则所有使用泛型的地方都要替换成传入的实参类型。例子如下:
class DataHolder implements Generator<String>{ @Override
public String next() { return null;
}
}
从这个例子我们看到,实现类里面的所有T的地方都需要实现为String。
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- LeetCode74|有序矩阵中第K小的元素
- LeetCode73|根据字符出现频率排序
- LeetCode72|前K个高频元素
- LeetCode71|数组中第K个最大元素
- LeetCode70|最小K个数
- LeetCode69|消失的数字
- LeetCode68|和为s的两个数字
- LeetCode78|存在重复元素
- LeetCode77|排序链表
- LeetCode76|两颗二叉搜索树中的所有元素
- LeetCode75|二叉搜索树的第k大节点
- LeetCode86|只出现一次的数字II
- LeetCode85|只出现一次的数字III
- LeetCode84|只出现一次的数字
- LeetCode83|排序矩阵查找