概念
定义类、接口、方法时,同时声明了一个或多个类型变量(如<E>),称为泛型类、泛型接口、泛型方法、它们统称为泛型。
语法
public class ArrayList<E>{
}
E可以接收不同类型的数据,可以是字符串,也可以是学生类等东西。
作用:泛型提供了在编译阶段约束所能操作的数据类型,并自动进行检查的能力,这样可以避免强制类型转换,及其可能出现的异常。
接下来用常见API,ArrayList集合举例
package GenericityDemo;
import java.util.ArrayList;
public class GenericDemo1 {
public static void main(String[] args) {
// //认识泛型
// ArrayList list = new ArrayList(); //没有使用泛型,存储任意类型数据
// list.add("hello");
// list.add(100);
// list.add(true);
// //获取数据
// for (int i = 0; i < list.size(); i++)
// {
// Object obj = list.get(i); //必须用Object接收,因为存储的任意类型数据
// //数据转型处理
// String s = (String) list.get(i);//运行到整型时程序会崩,因此最好要统一数据类型
// System.out.println(s);
// }
ArrayList<String> list = new ArrayList<String>(); //泛型约束,只能存储String类型数据
list.add("hello");
list.add("world");
//list.add(90); //类型不是泛型约束的类型,报错
for (int i = 0; i < list.size(); i++)
{
String s = list.get(i); //类型确定,不用转型
System.out.println(s); //不会出现异常
}
}
}
一、泛型类
泛型类(Generic Class) 是通过类型参数化定义的类,其核心作用是通过类型抽象实现代码的通用性、类型安全性和灵活性。
语法
修饰符 class 类名<类型变量,类型变量....>{
}
public class ArrayList<E>{
......
}
示例
创建一个ArrayList集合类(其实就是在自己的类里用之前的API,但接收的数据类型由我们限制)
package GenericityDemo;
import java.util.ArrayList;
public class MyArrayList <E>{
private ArrayList list = new ArrayList();
public boolean add(E e){
list.add(e);
return true;
}
public boolean remove(E e){
return list.remove(e);
}
@Override
public String toString() {
return list.toString();
}
}
package GenericityDemo;
public class GenericDemo2 {
public static void main(String[] args) {
//目标:学会自定义泛型类
//需求:模拟ArrayList集合,自定义一个集合MyArrayList
MyArrayList<String> list = new MyArrayList<>();//jdk7支持后面不写泛型类型
list.add("java");
list.add("后端");
System.out.println(list);
}
}
类型变量建议使用大写的英文字母,常用的有:E、T、K、V等
E:常用于集合类(如 List
、Set
),强调泛型参数是容器内的元素。
T:当泛型类或方法不限制具体类型时,常用 T
作为默认类型占位符。
K/V:用于键值对数据结构(如 Map
)中,明确区分键和值的类型。
二、泛型接口
泛型接口(Generic Interface) 是通过类型参数化定义的接口,其核心作用是通过类型抽象提高代码的复用性、类型安全性和灵活性。
修饰符 interface 接口名<类型变量,类型变量...>{
}
public interface A<E>{
...
}
示例
对学生数据/老师数据都要进行增删查改操作。
定义一个泛型接口可以接学生或老师对象进行操作,可以实现一个方法操作两个角色。
package GenericityDemo3;
public interface Data<T> {
// void add(Student s);
// void add(Teacher t);
//类型约定死了,,只能存Student或Teacher
//利用泛型接口
void add(T t); //此时泛型可以为Teacher也能为Student
void delete(T t);
void update(T t);
T query(int id);
}
(此示例并没有真正实现增删查改操作,只为示范)
package GenericityDemo3;
public class Student {
}
package GenericityDemo3;
public class Teacher {
}
创造两个类用于实现Data,一个实现老师操作,一个实现学生操作
package GenericityDemo3;
public class TeacherData implements Data<Teacher>{
//操作老师数据
@Override
public void add(Teacher teacher) {
}
@Override
public void delete(Teacher teacher) {
}
@Override
public void update(Teacher teacher) {
}
@Override
public Teacher query(int id) {
return null;
}
}
package GenericityDemo3;
public class StudentData implements Data<Student>{
//专门操作学生对象
@Override
public void add(Student student) {
}
@Override
public void delete(Student student) {
}
@Override
public void update(Student student) {
}
@Override
public Student query(int id) {
return null;
}
}
package GenericityDemo3;
public class GenericDemo3 {
public static void main(String[] args) {
//目标:搞清楚泛型接口的基本应用
//需求:项目需要对学生数据/老师数据都要进行增删查改操作
StudentData studentData = new StudentData();
studentData.add(new Student()); //添加学生数据.对学生操作
Student s = studentData.query(1001);
}
}
通过泛型接口,可以为不同类型的数据定义统一的处理逻辑,同时避免强制类型转换和运行时类型错误。
三、泛型方法
泛型方法(Generic Method) 是通过在方法签名中声明类型参数来实现的方法级别泛型化。它与泛型类的主要区别在于作用范围:泛型方法的类型参数仅作用于该方法内部,而非整个类。
语法
修饰符<类型变量,类型变量,...>返回值类型 方法名(形参列表){
}
示例
现在我需要一个方法将数组的内容打印出来
package GenericDemo4;
public class GenericDemo4 {
//目标:理解泛型方法
public static void main(String[] args) {
//需求:打印任意数组的内容
String[] arr1 = {"hello","world","java"};
printArray(arr1);
Student[] arr2 = new Student[3];
//printArray(arr2);//只能接收String类型数组
Student s1 = getMax(arr2);
printArray2(arr1);
getMax(arr2); //泛型方法,可以接收任意类型的数组,避免强转
}
public static void printArray(String[] arr)
{
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i] + " ");
}
}
public static <T> void printArray2(T[] arr)
{
for (int i = 0; i < arr.length; i++) {
System.out.println(arr[i] + " ");
}
}
public static <T> T getMax(T[] name)
{
return null;
}
}
上述代码有两个方法,一个是printArray一个是printArray2,但printArray有一个缺点就是只能打印字符串数组的内容,若是其他数组则无法接收。因此,我们可以做一个泛型方法即printArray2,它可以接收String数组也可以接收Student数组,实现了一个方法解决多个问题。
四、通配符与上下限
当我们要传的参数为多个集合,每个集合都是继承于某个父类的子类的集合时,我们就可以通过使用通配符来接收不同的子类集合。(注:例如两个集合为ArrayList<Cat>和ArrayList<Dog>,不能用ArrayList<Animal>接收,因为他们本质上是不同的集合,虽然他们的子类继承了同一个父类)
示例
package GenericDemo4;
public class Car {
}
package GenericDemo4;
public class BYD extends Car{
}
package GenericDemo4;
public class BMW extends Car{
}
package GenericDemo4;
import java.util.ArrayList;
public class GenericDemo5 {
public static void main(String[] args) {
//目标:理解通配符和上下限
ArrayList<BYD> byds = new ArrayList<>();
byds.add(new BYD());
byds.add(new BYD());
byds.add(new BYD());
//go(byds); //报错不具备通用性
ArrayList<BMW> bmws = new ArrayList<>();
bmws.add(new BMW());
bmws.add(new BMW());
bmws.add(new BMW());
go(bmws);
}
//模拟极品飞车游戏
//虽然比亚迪和宝马是Car的子类,但ArrayList<BYD>和ArrayList<BMW>和ArrayList<Car>是不同的,因此不能用多态
// public static void go(ArrayList<BMW> cars)
// {
//
// }
//通配符,在使用泛型时代表一切类型
public static void go(ArrayList<?> cars)
{
}
}
然而,若是这么写这个方法能接收的集合范围又太大了,连猫和狗都可以进这个开汽车的方法,因此需要使用上下限来加以限制。
? extends Car ?能接收Car或者Car的子类(泛型上限)
? super Car ?能接收Car或者Car的父类(泛型下限)
package GenericDemo4;
import java.util.ArrayList;
public class GenericDemo5 {
public static void main(String[] args) {
//目标:理解通配符和上下限
ArrayList<BYD> byds = new ArrayList<>();
byds.add(new BYD());
byds.add(new BYD());
byds.add(new BYD());
//go(byds); //报错不具备通用性
ArrayList<BMW> bmws = new ArrayList<>();
bmws.add(new BMW());
bmws.add(new BMW());
bmws.add(new BMW());
go(bmws);
}
//模拟极品飞车游戏
//虽然比亚迪和宝马是Car的子类,但ArrayList<BYD>和ArrayList<BMW>和ArrayList<Car>是不同的,因此不能用多态
// public static void go(ArrayList<BMW> cars)
// {
//
// }
//通配符,在使用泛型时代表一切类型
//为了防止将狗,猫等不属于车的类型传入方法,使用泛型的上下限限制
//? extends Car ?能接收Car或者Car的子类(泛型上限)
//? super Car ?能接收Car或者Car的父类(泛型下限)
public static void go(ArrayList<? extends Car> cars)
{
}
}
这样,就能将这个方法限制在接收汽车类的子类了。
五、泛型支持的数据类型
泛型不支持基本的数据类型,即int,double等数据类型,原因是泛型工作在编译阶段,编译结束后系统会进行泛型擦除,所有类型都会转变为Object类型。而Object是对象类型,接收的是对象,数字等类型不是对象,因此不接收基本的数据类型。为了能够兼容基本的数据类型,java的库里添加了包装类,用于将基本数据类型变为对象。
包装类有8种。
int -> Integer char -> Character ,其它的都是首字母大小写。
package GenericDemo5;
import java.util.ArrayList;
import java.util.Objects;
public class GenericDemo5 {
public static void main(String[] args) {
//目标:搞清楚泛型和集合不支持基本数据类型,只能支持对象类型(引用类型)
//ArrayList<double> list = new ArrayList<>(); //报错,不支持
//泛型擦除:泛型工作在编译阶段,等编译后泛型就没用了,所以泛型在编译后都会被擦除。所有类型会恢复为Object类型。
//Object是对象类型,不能直接指向某个数,只能指向某个对象。
//因此要使用包装类,比如Integer、Double、Character、Boolean等。
//包装类的作用:把基本数据类型包装成对象类型。
//ArrayList<int> list = new ArrayList<>();
//list.add(12); //12不是对象,报错
ArrayList<Integer> list = new ArrayList<>();
list.add(12); //自动装箱,把基本数据类型12包装成Integer对象。
int rs= list.get(0); //自动拆箱,把Integer对象12拆箱成基本数据类型。
//把基本数据类型变成包装类对象
//手工包装
//Integer i = new Integer(12);//从jdk9开始,这个方法被淘汰了(过时)
//推荐用valueOf的原因:valueOf方法中缓存了-128~127的数值,重复创建这个范围内的数字时不会创建新对象,而是直接返回缓存中的对象。
//缓存对象范围是-128~127,如果超过这个范围,就会创建新的对象。
//更优雅,节约内存。
Integer i = Integer.valueOf(12); //此时i为对象,里面存储的是12
Integer i2 = Integer.valueOf(12);
System.out.println(i == i2); //ture, 因为i和i2是同一个对象
//自动装箱
Integer i3 = 12; //与Integer i = Integer.valueOf(12)一个意思
Integer i4 = 12;
System.out.println(i3 == i4);
//自动拆箱:把包装类型的对象直接给基本类型的数据
int i5 = i3;
System.out.println(i5);
System.out.println("========================================================");
//包装类新增的功能
//1、把基本类型的数据变成字符串
int j=23;
String rs1 = Integer.toString(j);
System.out.println(rs1+1); //不是24,说明已经是字符串
Integer i6=j;
String rs2 = i6.toString();
System.out.println(rs2+1);
String rs3 = i6+""; //直接加""转换为字符串
System.out.println(rs3+1);
System.out.println("========================================================");
//把字符串数值转换成对应的基本数据类型
String str = "123";
int i1 = Integer.parseInt(str);
int i9 = Integer.valueOf(str);
System.out.println(i1+1);
System.out.println(i9+1);
}
}